├── .github └── workflows │ └── tests.yml ├── .gitignore ├── .npmignore ├── .prettierrc.json ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── app ├── cmd │ ├── graph.ts │ ├── info.ts │ └── split.ts ├── index.ts └── lib │ ├── cmd.ts │ ├── helper.ts │ └── load.ts ├── lib ├── analyze.ts ├── extractor.ts ├── helper.ts ├── internal │ ├── analyze │ │ ├── block.ts │ │ ├── expression.ts │ │ ├── helper.ts │ │ └── module.ts │ └── moddef.ts ├── interpret.ts ├── lift.ts ├── name.ts └── render.ts ├── package.json ├── pnpm-lock.yaml ├── release.sh ├── site ├── demo-kuto.ts ├── index.html └── index.ts ├── test-info.js ├── test ├── cases │ ├── async.js │ ├── await.js │ ├── default-unnamed.js │ ├── default-var.js │ ├── default1.js │ ├── default2.js │ ├── dep │ │ └── a.js │ ├── dup.js │ ├── iife.js │ ├── import.js │ ├── inline-fn-use.js │ ├── semi.js │ └── shadow.js ├── index.ts └── valid │ ├── a.ts │ ├── b.ts │ └── index.ts └── tsconfig.json /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: checkout 15 | uses: actions/checkout@v4 16 | 17 | - name: node 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version: '20.x' 21 | 22 | - uses: pnpm/action-setup@v3 23 | with: 24 | run_install: true 25 | version: 8 26 | 27 | - name: test 28 | run: pnpm run test 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | DONOTCHECKIN* 2 | .DS_Store 3 | node_modules/ 4 | dist/ 5 | demo/ 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | cmd/ 2 | demo/ 3 | dist/raw/ 4 | dist/split/ 5 | lib/ 6 | test/ 7 | *.sh 8 | /*.ts 9 | .* 10 | tsconfig.json 11 | site/ 12 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "printWidth": 100, 4 | "singleQuote": true, 5 | "semi": true 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.defaultFormatter": "esbenp.prettier-vscode" 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 🌈 Kuto makes updating your site's JS better, faster, harder, stronger. 2 | It reduces your download size by re-using code you've already shipped. 3 | Read more [on the blog](https://samthor.au/2024/kuto/), or watch [Theo.gg's video](https://www.youtube.com/watch?v=_sxwQBWJQHA)! 🌈 4 | 5 | Kuto tool logo 6 | 7 | [![Tests](https://github.com/samthor/kuto/actions/workflows/tests.yml/badge.svg)](https://github.com/samthor/kuto/actions/workflows/tests.yml) 8 | 9 | It does this by splitting JS files (in ESM) into 'main' and static parts. 10 | The static parts can be cached by clients forever, as they have no side-effects, and can be used as a 'corpus' or dictionary of code that can be called later. 11 | Chromium even caches [the bytecode](https://v8.dev/blog/code-caching-for-devs) of previously shipped files. 12 | 13 | Ideally, Kuto operates on a large output bundle from another tool. 14 | Think of it as doing code-splitting 'last', rather than first. 15 | 16 | ## Requirements 17 | 18 | Node 16+. 19 | 20 | Kuto works best on large (>1mb) singular JS bundles 'bundled' to ESM — the tool works on a statement level, and an IIFE/webpack output is one giant statement. 21 | 22 | ## Usage 23 | 24 | You can install via "kuto" and then run `npx kuto` to learn more. 25 | 26 | To split a JS file, you can then run: 27 | 28 | ```bash 29 | $ kuto split yourbundle.js out/ 30 | ``` 31 | 32 | This will generate a 'main' part and a corpus of code. 33 | If you build or change "yourbundle.js" and run Kuto _again_, this will re-use the existing corpus where possible, and remove completely disused corpus files. 34 | 35 | Note that you'll **need to keep the old generated code around** for Kuto to work. 36 | By default, this looks in the output dir, but you can change it with a flag. 37 | 38 | ### Flags 39 | 40 | - `-c ` where to find old corpus data (default: use output dir) 41 | 42 | Normally, Kuto reads old corpus data from the output path, but this flag can be used to point to a different location. 43 | It can also point directly to a Kuto-generated 'main' file. 44 | This could be useful to point to the currently published version of your code. 45 | 46 | For example: 47 | 48 | - `-c https://example.com/source.js` to load a Kuto-generated 'main' from a real website (uses `fetch()`) 49 | - `-c path/to/old/source/dir` to use any corpus in this dir 50 | 51 | - `-d` dedups callables (default: `false`) 52 | 53 | With this flag enabled, if two classes or callables are exactly the same, they'll be merged into one. 54 | For example: 55 | 56 | ```ts 57 | var A = class {}; 58 | var B = class {}; 59 | 60 | // will be 'true' in `-d` mode 61 | new A() instanceof B; 62 | ``` 63 | 64 | This is turned off by default, as it can be dangerous. 65 | Kuto will still dedup other code where it is safe to do so! 66 | 67 | - `-m ` only yeet code which is larger than this (default: `32`) 68 | 69 | There's overhead to splitting the static parts of your code out, so this limits the process to larger statements. 70 | 71 | - `-k ` keep this number of static bundles around (default: `4`) 72 | 73 | Kuto can create high levels of fragmentation over time. 74 | For most sites, you'll end up with one HUGE bundle that contains the bulk of your code, dependencies, etc. 75 | This flag sets the number of 'corpus' files to keep around, ordered by size. 76 | 77 | (This may not actually be the best way to keep chunks around. 78 | This flag will probably evolve over time.) 79 | 80 | - `-n ` use this basename for the output (default: basename of input) 81 | 82 | Normally, Kuto creates output files with the same name as the input files. 83 | Instead, use this to output e.g., "index.js" regardless of source name. 84 | 85 | ## Best Practice 86 | 87 | One good way to understand what Kuto does is to run `./release.sh`, which builds Kuto itself. 88 | Try running a release, changing something in the source, and releasing again—you'll see extra static files appear. 89 | 90 | This release process runs in three steps: 91 | 92 | 1. use esbuild to create one bundle (without minification) 93 | 2. use kuto to split the bundle 94 | 3. use esbuild on all resulting files, _purely_ to minify 95 | 96 | ## Notes 97 | 98 | Kuto bundles [acorn](https://www.npmjs.com/package/acorn) to do its parsing. 99 | 100 | There is also has a `kuto info` command to give basic information about an ES Module. 101 | This is the origin of the tool; I wanted something that could inform me about side-effects. 102 | 103 | Both `info` and `split` will transparently compile TS/etc via "esbuild" if installed, but it's not a `peerDependency`. 104 | -------------------------------------------------------------------------------- /app/cmd/graph.ts: -------------------------------------------------------------------------------- 1 | import { buildResolver } from 'esm-resolve'; 2 | import { LoadResult, loadAndMaybeTransform, parse } from '../lib/load.ts'; 3 | import { aggregateImports } from '../../lib/internal/analyze/module.ts'; 4 | import * as path from 'node:path'; 5 | import { isLocalImport, relativize } from '../../lib/helper.ts'; 6 | 7 | export type ValidArgs = { 8 | paths: string[]; 9 | }; 10 | 11 | type FoundInfo = { 12 | importer: string[]; 13 | tags?: { local?: string[]; nested?: string[] }; 14 | found?: boolean; 15 | }; 16 | 17 | const matchTag = /@kuto-(\w+)/g; 18 | 19 | export default async function cmdGraph(args: ValidArgs) { 20 | const pending = new Set(); 21 | const mod: Record = {}; 22 | 23 | for (const raw of args.paths) { 24 | const entrypoint = relativize(raw); 25 | pending.add(entrypoint); 26 | mod[entrypoint] = { importer: [] }; 27 | } 28 | 29 | for (const p of pending) { 30 | pending.delete(p); 31 | if (!isLocalImport(p)) { 32 | continue; // rn we ignore "foo" 33 | } 34 | 35 | const info = mod[p]!; 36 | 37 | let x: LoadResult; 38 | try { 39 | x = await loadAndMaybeTransform(p); 40 | } catch (e) { 41 | info.found = false; 42 | continue; 43 | } 44 | info.found = true; 45 | const prog = parse(x.source, (comment) => { 46 | comment.replaceAll(matchTag, (_, tag) => { 47 | info.tags ??= {}; 48 | info.tags.local ??= []; 49 | if (!info.tags.local.includes(tag)) { 50 | info.tags.local.push(tag); 51 | } 52 | return ''; 53 | }); 54 | }); 55 | 56 | // -- resolve additional imports 57 | 58 | const resolver = buildResolver(p, { 59 | allowImportingExtraExtensions: true, 60 | resolveToAbsolute: true, 61 | }); 62 | 63 | const imports = aggregateImports(prog); 64 | 65 | const resolved: string[] = []; 66 | for (const source of imports.mod.importSources()) { 67 | let key = source.name; 68 | 69 | if (isLocalImport(source.name)) { 70 | const r = resolver(source.name); 71 | if (!r) { 72 | continue; 73 | } 74 | key = relativize(path.relative(process.cwd(), r)); 75 | resolved.push(key); 76 | } 77 | 78 | // create graph to thingo 79 | const prev = mod[key]; 80 | if (prev !== undefined) { 81 | prev.importer.push(p); 82 | } else { 83 | mod[key] = { importer: [p] }; 84 | pending.add(key); 85 | } 86 | } 87 | } 88 | 89 | // descend tags 90 | 91 | const expandTree = (key: string) => { 92 | const all = new Set(); 93 | const pending = [key]; 94 | 95 | while (pending.length) { 96 | const next = pending.pop()!; 97 | all.add(next); 98 | 99 | for (const importer of mod[next].importer) { 100 | if (all.has(importer)) { 101 | continue; 102 | } 103 | pending.push(importer); 104 | } 105 | } 106 | 107 | all.delete(key); 108 | return [...all]; 109 | }; 110 | 111 | for (const key of Object.keys(mod)) { 112 | const o = mod[key]; 113 | const tree = expandTree(key); 114 | 115 | for (const localTag of o.tags?.local ?? []) { 116 | for (const okey of tree) { 117 | const o = mod[okey]; 118 | o.tags ??= {}; 119 | o.tags.nested ??= []; 120 | if (!o.tags.nested.includes(localTag)) { 121 | o.tags.nested.push(localTag); 122 | } 123 | } 124 | } 125 | } 126 | 127 | const out = { 128 | mod, 129 | }; 130 | console.info(JSON.stringify(out, undefined, 2)); 131 | } 132 | -------------------------------------------------------------------------------- /app/cmd/info.ts: -------------------------------------------------------------------------------- 1 | import { aggregateImports } from '../../lib/internal/analyze/module.ts'; 2 | import { VarInfo, analyzeBlock } from '../../lib/internal/analyze/block.ts'; 3 | import { findVars, resolveConst } from '../../lib/interpret.ts'; 4 | import { createBlock } from '../../lib/internal/analyze/helper.ts'; 5 | import { loadAndMaybeTransform, parse } from '../lib/load.ts'; 6 | import { relativize } from '../../lib/helper.ts'; 7 | 8 | export type InfoArgs = { 9 | path: string; 10 | }; 11 | 12 | export default async function cmdInfo(args: InfoArgs) { 13 | const { source } = await loadAndMaybeTransform(args.path); 14 | const p = parse(source); 15 | 16 | const agg = aggregateImports(p); 17 | const block = createBlock(...agg.rest); 18 | const analysis = analyzeBlock(block); 19 | resolveConst(agg, analysis); 20 | 21 | const toplevelVars = new Map(); 22 | const nestedVars = new Map(); 23 | for (const [cand, info] of analysis.vars) { 24 | info.local && toplevelVars.set(cand, info); 25 | info.nested && nestedVars.set(cand, info); 26 | } 27 | const toplevelFind = findVars({ find: toplevelVars, vars: analysis.vars, mod: agg.mod }); 28 | const nestedFind = findVars({ find: nestedVars, vars: analysis.vars, mod: agg.mod }); 29 | 30 | console.info('#', JSON.stringify(relativize(args.path))); 31 | 32 | // TODO: not useful right now 33 | // const sideEffects = toplevelFind.immediateAccess; 34 | // console.info('\nSide-effects:', sideEffects ? 'Unknown' : 'No!'); 35 | 36 | console.info('\nImports:'); 37 | for (const { name, info } of agg.mod.importSources()) { 38 | console.info(`- ${JSON.stringify(name)}`); 39 | info.imports.forEach((names, remote) => { 40 | for (const name of names) { 41 | const left = name === remote ? name : `${remote || '*'} as ${name}`; 42 | console.info(` - ${left}`); 43 | } 44 | }); 45 | } 46 | 47 | console.info('\nExports:'); 48 | for (const { name, info } of agg.mod.importSources()) { 49 | if (info.reexportAll) { 50 | console.info(`- * from ${JSON.stringify(name)}`); 51 | } 52 | } 53 | for (const e of agg.mod.exported()) { 54 | const left = e.exportedName === e.name ? e.name : `${e.name || '*'} as ${e.exportedName}`; 55 | let suffix = ''; 56 | 57 | const lookup = agg.mod.lookupImport(e.name); 58 | if (lookup) { 59 | suffix = ` from ${JSON.stringify(lookup.import)}`; 60 | } else if (e.import) { 61 | suffix = ` from ${JSON.stringify(e.import)} (re-export)`; 62 | } else if (!agg.localConst.has(e.name)) { 63 | suffix = ` (mutable, may change)`; 64 | } else { 65 | suffix = ` (immutable)`; 66 | } 67 | console.info(`- ${left}${suffix}`); 68 | } 69 | 70 | console.info('\nGlobals used at top-level:'); 71 | for (const [g, rw] of toplevelFind.globals) { 72 | console.info(`- ${g}${rw ? ' (written)' : ''}`); 73 | } 74 | 75 | console.info('\nImports used at top-level:'); 76 | for (const g of toplevelFind.imports.keys()) { 77 | console.info(`- ${g}`); 78 | } 79 | 80 | console.info('\nGlobals used in callables:'); 81 | for (const [g, rw] of nestedFind.globals) { 82 | console.info(`- ${g}${rw ? ' (written)' : ''}`); 83 | } 84 | 85 | console.info('\nImports used in callables:'); 86 | for (const g of nestedFind.imports.keys()) { 87 | console.info(`- ${g}`); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /app/cmd/split.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'node:fs'; 2 | import * as path from 'node:path'; 3 | import { StaticExtractor } from '../../lib/extractor.ts'; 4 | import { liftDefault } from '../../lib/lift.ts'; 5 | import { loadAndMaybeTransform, parse } from '../lib/load.ts'; 6 | import { loadExisting } from '../lib/load.ts'; 7 | import { relativize } from '../../lib/helper.ts'; 8 | import { buildCorpusName } from '../../lib/name.ts'; 9 | 10 | export type SpiltArgs = { 11 | min: number; 12 | keep: number; 13 | sourcePath: string; 14 | oldPath: string; 15 | dist: string; 16 | dedupCallables: boolean; 17 | basename: string; 18 | }; 19 | 20 | export default async function cmdSplit(args: SpiltArgs) { 21 | const { sourcePath, dist } = args; 22 | 23 | fs.mkdirSync(dist, { recursive: true }); 24 | 25 | const name = path.parse(args.basename || sourcePath).name + '.js'; 26 | const sourceName = relativize(name); 27 | const staticName = relativize(buildCorpusName(name)); 28 | 29 | const existing = await loadExisting({ 30 | from: args.oldPath || args.dist, 31 | keep: args.keep, 32 | }); 33 | 34 | const { source } = await loadAndMaybeTransform(args.sourcePath); 35 | const prog = parse(source); 36 | 37 | const e = new StaticExtractor({ 38 | p: prog, 39 | source, 40 | sourceName, 41 | staticName, 42 | existingStaticSource: existing.existingStaticSource, 43 | dedupCallables: args.dedupCallables, 44 | }); 45 | const liftStats = liftDefault(e, args.min); 46 | 47 | // run 48 | const out = e.build(); 49 | const disused = existing.prior.filter((name) => !out.static.has(name)); 50 | 51 | // generate stats, show most recent first 52 | const sizes: Record = {}; 53 | sizes[sourceName] = out.main.length; 54 | let totalSize = out.main.length; 55 | const statics = [...out.static].sort(([a], [b]) => b.localeCompare(a)); 56 | statics.forEach(([name, code]) => { 57 | sizes[name] = code.length; 58 | totalSize += code.length; 59 | }); 60 | 61 | console.info('stats', { 62 | source: { size: source.length }, 63 | sizes, 64 | disused, 65 | lift: liftStats, 66 | }); 67 | console.info('overhead:', toPercentChange(totalSize / source.length)); 68 | 69 | // write new files, nuke old ones IF they're in the output dir 70 | for (const name of disused) { 71 | try { 72 | fs.rmSync(path.join(dist, name)); 73 | } catch {} 74 | } 75 | fs.writeFileSync(path.join(dist, sourceName), out.main); 76 | for (const [name, code] of out.static) { 77 | fs.writeFileSync(path.join(dist, name), code); 78 | } 79 | 80 | console.info('Ok!'); 81 | } 82 | 83 | const toPercentChange = (v: number) => { 84 | const sign = v < 1.0 ? '' : '+'; 85 | return sign + ((v - 1.0) * 100).toFixed(1) + '%'; 86 | }; 87 | -------------------------------------------------------------------------------- /app/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import * as cmd from './lib/cmd.ts'; 4 | 5 | cmd.register('info', { 6 | description: 'Show information about a JS module file', 7 | positional: true, 8 | usageSuffix: '', 9 | async handler(res) { 10 | if (res.positionals.length !== 1) { 11 | throw new cmd.CommandError(); 12 | } 13 | 14 | const { default: cmdInfo } = await import('./cmd/info.ts'); 15 | return cmdInfo({ path: res.positionals[0] }); 16 | }, 17 | }); 18 | 19 | cmd.register('graph', { 20 | description: 'Validate a module graph based on conditions', 21 | positional: true, 22 | usageSuffix: ' ', 23 | async handler(res) { 24 | if (res.positionals.length < 1) { 25 | throw new cmd.CommandError(); 26 | } 27 | 28 | const { default: cmdGraph } = await import('./cmd/graph.ts'); 29 | return cmdGraph({ paths: res.positionals }); 30 | }, 31 | }); 32 | 33 | cmd.register('split', { 34 | description: 'Split a JS module into runtime and static code', 35 | flags: { 36 | min: { 37 | type: 'string', 38 | default: '32', 39 | short: 'm', 40 | help: 'only staticify nodes larger than this', 41 | }, 42 | keep: { 43 | type: 'string', 44 | default: '4', 45 | short: 'k', 46 | help: 'always keep this many top-sized static bundle(s)', 47 | }, 48 | 'dedup-callables': { 49 | type: 'boolean', 50 | default: false, 51 | short: 'd', 52 | help: 'dedup callables (may cause inheritance issues)', 53 | }, 54 | corpus: { 55 | type: 'string', 56 | default: '', 57 | short: 'c', 58 | help: 'alternative path to historic corpus', 59 | }, 60 | name: { 61 | type: 'string', 62 | default: '', 63 | short: 'n', 64 | help: 'output basename (default to basename of source)', 65 | }, 66 | }, 67 | positional: true, 68 | usageSuffix: ' ', 69 | async handler(res) { 70 | if (res.positionals.length !== 2) { 71 | throw new cmd.CommandError(); 72 | } 73 | 74 | const { default: cmdSplit } = await import('./cmd/split.ts'); 75 | return cmdSplit({ 76 | min: +(res.values['min'] ?? 0), 77 | keep: +(res.values['keep'] ?? 0), 78 | sourcePath: res.positionals[0], 79 | dist: res.positionals[1], 80 | oldPath: (res.values['corpus'] as string) || '', 81 | dedupCallables: Boolean(res.values['dedup-callables']), 82 | basename: (res.values['name'] as string) || '', 83 | }); 84 | }, 85 | }); 86 | 87 | await cmd.run(); 88 | -------------------------------------------------------------------------------- /app/lib/cmd.ts: -------------------------------------------------------------------------------- 1 | import { ParseArgsConfig, parseArgs } from 'node:util'; 2 | 3 | type ParseOptions = NonNullable; 4 | type ParsedResults = ReturnType; 5 | 6 | const cmds = new Map>(); 7 | 8 | const helpOptions: ParseOptions = { 9 | help: { 10 | type: 'boolean', 11 | default: false, 12 | short: 'h', 13 | }, 14 | }; 15 | 16 | type CommandConfig = { 17 | description: string; 18 | flags?: T extends ParseOptions ? T : never; 19 | positional?: boolean; 20 | usageSuffix?: string; 21 | handler: (res: ParsedResults) => any; 22 | }; 23 | 24 | export const register = (cmd: string, config: CommandConfig) => { 25 | cmds.set(cmd, config); 26 | }; 27 | 28 | export const run = (argv = process.argv.slice(2)): any => { 29 | const cmd = argv[0]; 30 | const matched = cmds.get(cmd); 31 | argv = argv.slice(1); 32 | 33 | if (!matched) { 34 | const args = parseArgs({ options: helpOptions, args: argv }); 35 | 36 | console.warn(`Usage: kuto [command]\n\nCommands:`); 37 | 38 | const minWidth = [...cmds.keys()].reduce((prev, c) => Math.max(c.length, prev), 0); 39 | for (const [key, c] of cmds) { 40 | console.warn(' ', key.padEnd(minWidth), c.description); 41 | } 42 | 43 | console.warn('\nmore info: https://kuto.dev'); 44 | process.exit(args.values['help'] ? 0 : 1); 45 | throw 'should not get here'; 46 | } 47 | 48 | const args = parseArgs({ 49 | allowPositionals: matched.positional ?? false, 50 | options: { ...helpOptions, ...matched.flags }, 51 | args: argv, 52 | }); 53 | const v = args.values as any; 54 | 55 | if (!v['help']) { 56 | try { 57 | return matched.handler(args); 58 | } catch (e) { 59 | if (!(e instanceof CommandError)) { 60 | throw e; 61 | } 62 | } 63 | } 64 | 65 | console.warn( 66 | `Usage: kuto ${cmd} ${matched.usageSuffix ?? ''}\nDescription: ${matched.description}`, 67 | ); 68 | if (matched.flags) { 69 | console.warn(); 70 | const flags: ParseOptions = matched.flags; 71 | for (const [f, config] of Object.entries(flags)) { 72 | const help = (config as any)['help'] ?? '?'; 73 | const defaultPart = 'default' in config ? ` (default ${config.default})` : ''; 74 | const shortPart = config.short ? `-${config.short}, ` : ''; 75 | console.warn(` ${shortPart}--${f}${defaultPart}: ${help}`); 76 | } 77 | } 78 | console.warn('\nmore info: https://kuto.dev'); 79 | process.exit(v['help'] ? 0 : 1); 80 | }; 81 | 82 | export class CommandError extends Error {} 83 | -------------------------------------------------------------------------------- /app/lib/helper.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'node:path'; 2 | import { relativize } from '../../lib/helper.ts'; 3 | 4 | export function isUrl(s: string) { 5 | try { 6 | new URL(s); 7 | return true; 8 | } catch { 9 | return false; 10 | } 11 | } 12 | 13 | export function buildJoin(s: string) { 14 | if (isUrl(s)) { 15 | return (other: string) => { 16 | const u = new URL(other, s); 17 | return u.toString(); 18 | }; 19 | } 20 | return (other: string) => path.join(s, other); 21 | } 22 | 23 | export function urlAgnosticRelativeBasename(s: string) { 24 | if (isUrl(s)) { 25 | // e.g. "https://example.com/src/x.js" => "x.js" 26 | const u = new URL(s); 27 | return relativize(path.basename(u.pathname)); 28 | } 29 | return relativize(path.basename(s)); 30 | } 31 | -------------------------------------------------------------------------------- /app/lib/load.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'node:fs'; 2 | import * as path from 'node:path'; 3 | import { buildJoin, urlAgnosticRelativeBasename } from '../lib/helper.ts'; 4 | import { aggregateImports } from '../../lib/internal/analyze/module.ts'; 5 | import * as acorn from 'acorn'; 6 | 7 | export function parse(source: string, comment?: (text: string) => void) { 8 | const args: acorn.Options = { 9 | ecmaVersion: 'latest', 10 | sourceType: 'module', 11 | }; 12 | if (comment) { 13 | args.onComment = (isBlock, text) => { 14 | comment(text); 15 | }; 16 | } 17 | return acorn.parse(source, args); 18 | } 19 | 20 | function hasCorpusSuffix(s: string) { 21 | return /\.kt-\w+.js$/.test(s); 22 | } 23 | 24 | export type LoadExistingArgs = { 25 | from: string; 26 | keep: number; 27 | }; 28 | 29 | function isDir(s: fs.PathLike) { 30 | try { 31 | const stat = fs.statSync(s); 32 | return stat.isDirectory(); 33 | } catch { 34 | return false; 35 | } 36 | } 37 | 38 | async function loadSource(s: string) { 39 | if (s.startsWith('https://') || s.startsWith('http://')) { 40 | const r = await fetch(s); 41 | if (!r.ok) { 42 | throw new Error( 43 | `couldn't fetch old source from ${JSON.stringify(s)}: ${r.status} ${r.statusText}`, 44 | ); 45 | } 46 | return r.text(); 47 | } 48 | 49 | return fs.readFileSync(s, 'utf-8'); 50 | } 51 | 52 | export async function loadExisting(args: LoadExistingArgs) { 53 | let cand: string[]; 54 | 55 | if (isDir(args.from)) { 56 | cand = fs.readdirSync(args.from).map((c) => path.join(args.from, c)); 57 | } else { 58 | const text = await loadSource(args.from); 59 | const join = buildJoin(args.from); 60 | 61 | const p = parse(text); 62 | const agg = aggregateImports(p); 63 | 64 | cand = [...agg.mod.importSources()].map(({ name }) => join(name)); 65 | cand.unshift(args.from); 66 | } 67 | 68 | const load = cand.filter((x) => hasCorpusSuffix(x)); 69 | 70 | const existing = await Promise.all( 71 | load.map(async (name) => { 72 | const text = await loadSource(name); 73 | return { name: urlAgnosticRelativeBasename(name), text, skip: true }; 74 | }), 75 | ); 76 | 77 | // keep the top-n largest static bundles 78 | const existingBySize = existing.sort(({ text: a }, { text: b }) => b.length - a.length); 79 | const keepN = Math.min(args.keep, existingBySize.length); 80 | for (let i = 0; i < keepN; ++i) { 81 | existingBySize[i].skip = false; 82 | } 83 | 84 | // load 85 | const out = new Map(); 86 | const prior = new Set(); 87 | for (const e of existing) { 88 | if (!e.skip) { 89 | if (out.has(e.name)) { 90 | throw new Error(`duplicate corpus: ${e.name}`); 91 | } 92 | out.set(e.name, e.text); 93 | } 94 | prior.add(e.name); 95 | } 96 | 97 | return { existingStaticSource: out, prior: [...prior] }; 98 | } 99 | 100 | const needsBuildExt = (ext: string) => ['.ts', '.tsx', '.jsx'].includes(ext); 101 | 102 | export type LoadResult = { 103 | name: string; 104 | source: string; 105 | stat: fs.Stats; 106 | }; 107 | 108 | export async function loadAndMaybeTransform(name: string): Promise { 109 | const { ext } = path.parse(name); 110 | let source = fs.readFileSync(name, 'utf-8'); 111 | const stat = fs.statSync(name); 112 | 113 | // lazily compile with esbuild (throws if not available) 114 | if (needsBuildExt(ext)) { 115 | const esbuild = await import('esbuild'); 116 | const t = esbuild.transformSync(source, { 117 | loader: ext.endsWith('x') ? 'tsx' : 'ts', 118 | format: 'esm', 119 | platform: 'neutral', 120 | legalComments: 'inline', 121 | }); 122 | source = t.code; 123 | } 124 | 125 | return { name, source, stat }; 126 | } 127 | -------------------------------------------------------------------------------- /lib/analyze.ts: -------------------------------------------------------------------------------- 1 | import type * as acorn from 'acorn'; 2 | import { createBlock, createExpressionStatement } from './internal/analyze/helper.ts'; 3 | import { AnalyzeBlock, analyzeBlock } from './internal/analyze/block.ts'; 4 | 5 | /** 6 | * Given a function, determine what it uses from outside the function. 7 | */ 8 | export function analyzeFunction(f: acorn.Function): AnalyzeBlock { 9 | let expr: acorn.FunctionExpression | acorn.ArrowFunctionExpression; 10 | 11 | if (!f.expression) { 12 | expr = { 13 | ...(f as acorn.FunctionDeclaration), 14 | type: 'FunctionExpression', 15 | }; 16 | } else { 17 | expr = f as acorn.FunctionExpression; 18 | } 19 | 20 | const b = createBlock(createExpressionStatement(expr)); 21 | const internal = analyzeBlock(b); 22 | 23 | for (const [key, info] of internal.vars) { 24 | if (info.local?.kind) { 25 | internal.vars.delete(key); 26 | } 27 | } 28 | 29 | return internal; 30 | } 31 | -------------------------------------------------------------------------------- /lib/extractor.ts: -------------------------------------------------------------------------------- 1 | import * as acorn from 'acorn'; 2 | import { analyzeFunction } from './analyze.ts'; 3 | import { ModDef } from './internal/moddef.ts'; 4 | import { findVars, resolveConst } from './interpret.ts'; 5 | import { AnalyzeBlock, VarInfo, analyzeBlock } from './internal/analyze/block.ts'; 6 | import { AggregateImports, aggregateImports } from './internal/analyze/module.ts'; 7 | import { createBlock, createExpressionStatement } from './internal/analyze/helper.ts'; 8 | import { renderOnly, renderSkip } from './render.ts'; 9 | import { relativize, withDefault } from './helper.ts'; 10 | 11 | const normalStaticPrefix = '_'; 12 | const callableStaticPrefix = '$'; 13 | 14 | export type ExtractStaticArgs = { 15 | source: string; 16 | p: acorn.Program; 17 | sourceName: string; 18 | staticName: string; 19 | existingStaticSource?: Map; 20 | dedupCallables: boolean; 21 | }; 22 | 23 | function extractExistingStaticCode(raw: Iterable<[string, string]>) { 24 | const existingByCode: Map = new Map(); 25 | 26 | for (const [path, source] of raw) { 27 | const p = acorn.parse(source, { ecmaVersion: 'latest', sourceType: 'module' }); 28 | const agg = aggregateImports(p); 29 | 30 | // ensure disambiguation from node imports 31 | const relPath = relativize(path); 32 | 33 | const add = (node: acorn.Node, name: string) => { 34 | const code = source.substring(node.start, node.end); 35 | if (!existingByCode.has(code)) { 36 | existingByCode.set(code, { name, import: relPath }); 37 | } 38 | }; 39 | 40 | // TODO: this doesn't check that the things are exported _as_ this name, but it's what we build 41 | 42 | for (const r of agg.rest) { 43 | switch (r.type) { 44 | case 'FunctionDeclaration': 45 | add(r, r.id.name); 46 | break; 47 | 48 | case 'VariableDeclaration': { 49 | for (const decl of r.declarations) { 50 | if (!(decl.init && decl.id.type === 'Identifier')) { 51 | continue; 52 | } 53 | const name = decl.id.name; 54 | if (name.startsWith(callableStaticPrefix)) { 55 | if (decl.init.type !== 'ArrowFunctionExpression') { 56 | continue; 57 | } 58 | add(decl.init.body, name); 59 | } else { 60 | add(decl.init, name); 61 | } 62 | } 63 | break; 64 | } 65 | } 66 | } 67 | } 68 | 69 | return existingByCode; 70 | } 71 | 72 | /** 73 | * Helper that wraps up the behavior of extracting static code from a main source file. 74 | * 75 | * Users basically pass in the arguments, pull back the generated {@link acorn.BlockStatement}, and find parts that will be swapped out. 76 | */ 77 | export class StaticExtractor { 78 | private args: ExtractStaticArgs; 79 | private agg: AggregateImports; 80 | private vars: Map; 81 | private existingByCode: Map; 82 | private _block: acorn.BlockStatement; 83 | 84 | /** 85 | * If we need to export A from main, but A is already used 'for something real', then store what we shipped A as. 86 | */ 87 | private mainLocalExportAs = new Map(); 88 | 89 | /** 90 | * Needed in cases where a decl/expression is exported as default without a name. 91 | */ 92 | private exportDefaultName?: string; 93 | 94 | private staticToWrite = new Map< 95 | string, 96 | { 97 | globalInMain: string; 98 | mod: ModDef; 99 | exported: Map; 100 | here: Set; 101 | } 102 | >(); 103 | private staticVars = new Set(); 104 | 105 | private nodesToReplace = new Map(); 106 | 107 | constructor(args: ExtractStaticArgs) { 108 | this.args = { 109 | ...args, 110 | staticName: relativize(args.staticName), 111 | sourceName: relativize(args.sourceName), 112 | }; 113 | 114 | // analyze all provided existing statics, record used vars 115 | this.existingByCode = extractExistingStaticCode(args.existingStaticSource ?? new Map()); 116 | for (const info of this.existingByCode.values()) { 117 | this.staticVars.add(`${info.name}~${info.import}`); 118 | } 119 | 120 | // analyze parsed 121 | const agg = aggregateImports(args.p); 122 | this._block = createBlock(...agg.rest); 123 | const analysis = analyzeBlock(this._block); 124 | this.agg = agg; 125 | this.vars = analysis.vars; 126 | 127 | // we can't operate with this reexport _because_ we might shadow things 128 | // you can still `export * as x from ...` 129 | const hasExportAllFrom = this.agg.mod.hasExportAllFrom(); 130 | if (hasExportAllFrom) { 131 | const inner = `export * from ${JSON.stringify(hasExportAllFrom)};`; 132 | throw new Error( 133 | `Kuto cannot split files that re-export in the top scope, e.g.: \`${inner}\``, 134 | ); 135 | } 136 | 137 | // create fake name for hole: this is inefficient (can't reuse default locally anyway) 138 | if (this.agg.exportDefaultHole) { 139 | this.exportDefaultName = this.varForMain(); 140 | this.agg.mod.removeExportLocal('default'); 141 | this.agg.mod.addExportLocal('default', this.exportDefaultName); 142 | } 143 | 144 | // resolve whether local vars are const - look for writes inside later callables 145 | // TODO: this doesn't explicitly mark their exported names as const, can look later? 146 | // nb. not _actually_ used yet 147 | resolveConst(agg, analysis); 148 | } 149 | 150 | get block() { 151 | return this._block; 152 | } 153 | 154 | /** 155 | * Finds and returns a new valid variable name for the static file. 156 | */ 157 | private varForStatic(staticName: string, prefix: string) { 158 | for (let i = 1; i < 100_000; ++i) { 159 | const cand = `${prefix}${i.toString(36)}`; 160 | const check = `${cand}~${staticName}`; 161 | if (!this.staticVars.has(check) && !this.vars.has(cand)) { 162 | this.staticVars.add(check); 163 | return cand; 164 | } 165 | } 166 | throw new Error(`could not make var for static: ${staticName}`); 167 | } 168 | 169 | private varForMain(prefix = '$') { 170 | for (let i = 1; i < 10_000; ++i) { 171 | const cand = `${prefix}${i.toString(36)}`; 172 | if (!this.vars.has(cand)) { 173 | // pretend to be global 174 | this.vars.set(cand, { 175 | local: { writes: 0, kind: 'var' }, 176 | }); 177 | return cand; 178 | } 179 | } 180 | throw new Error(`could not make var for main`); 181 | } 182 | 183 | private addCodeToStatic(args: { node: acorn.Node; analysis: AnalyzeBlock; var?: boolean }) { 184 | const find = findVars({ find: args.analysis.vars, vars: this.vars, mod: this.agg.mod }); 185 | if (find.rw) { 186 | return null; // no support for rw 187 | } 188 | 189 | let name: string = ''; 190 | let targetStaticName = this.args.staticName; 191 | let code = this.args.source.substring(args.node.start, args.node.end); 192 | const existing = this.existingByCode.get(code); 193 | if (existing) { 194 | targetStaticName = existing.import; 195 | name = existing.name; 196 | 197 | // if this code _was already shipped_ and it has callables, normally don't include it twice 198 | // would cause this problem: 199 | // const a = function() {} 200 | // const b = function() {} 201 | // (new a !== new b) 202 | if (existing.here && args.analysis.hasNested) { 203 | if (!this.args.dedupCallables) { 204 | return null; 205 | } 206 | } 207 | } 208 | 209 | // determine what name this has (generated or part of the fn/class hoisted) 210 | if (!name) { 211 | name = this.varForStatic( 212 | targetStaticName, 213 | find.immediateAccess ? callableStaticPrefix : normalStaticPrefix, 214 | ); 215 | } 216 | if (!name || name === 'default') { 217 | throw new Error(`could not name code put into static: ${args}`); 218 | } 219 | 220 | // future callers may _also_ get this - maybe the source code does the same thing a lot? 221 | if (!existing) { 222 | this.existingByCode.set(code, { name, import: targetStaticName, here: true }); 223 | } else { 224 | existing.here = true; 225 | } 226 | 227 | // add to static file 228 | const targetStatic = withDefault(this.staticToWrite, targetStaticName, () => ({ 229 | globalInMain: this.varForMain(), 230 | mod: new ModDef(), 231 | exported: new Map(), 232 | here: new Set(), 233 | })); 234 | if (find.immediateAccess) { 235 | // we don't know what's here so need ()'s (could be "foo,bar") 236 | // acorn 'eats' the extra () before it returns, so nothing is needed on the other side 237 | code = `_=>(${code})`; 238 | if (args.analysis.hasAwait) { 239 | code = `async ${code}`; 240 | } 241 | } 242 | targetStatic.exported.set(name, code); 243 | 244 | // update how we reference the now yeeted code from the main file 245 | if (args.var) { 246 | // TODO: referencing a global import isn't nessecarily smaller 247 | let replacedCode = 248 | `${targetStatic.globalInMain}.${name}` + (find.immediateAccess ? '()' : ''); 249 | if (args.analysis.hasAwait) { 250 | // this can generate TLA in the static bundle - seems fine? 251 | // like this is a bad expr, nothing sensible can be awaited if we're here 252 | replacedCode = `await ${replacedCode}`; 253 | } 254 | 255 | this.nodesToReplace.set(args.node, replacedCode); 256 | this.agg.mod.addGlobalImport(targetStaticName, targetStatic.globalInMain); 257 | } else { 258 | const decl = args.node as acorn.ClassDeclaration | acorn.FunctionDeclaration; 259 | if (!(decl.type === 'ClassDeclaration' || decl.type === 'FunctionDeclaration')) { 260 | throw new Error(`can't hoist decl without name`); 261 | } 262 | // static had faux-name of 'default'; we can't define this locally, use the fake 263 | const localName = decl.id.name === 'default' ? this.exportDefaultName! : decl.id.name; 264 | this.nodesToReplace.set(args.node, ''); 265 | this.agg.mod.addImport(targetStaticName, localName, name); 266 | } 267 | 268 | // clone imports needed to run this code (order is maintained in main file) 269 | for (const [key, importInfo] of find.imports) { 270 | if (importInfo.remote) { 271 | targetStatic.mod.addImport(importInfo.import, key, importInfo.remote); 272 | } else { 273 | targetStatic.mod.addGlobalImport(importInfo.import, key); 274 | } 275 | } 276 | 277 | // import locals from main (this might be a complex redir) 278 | for (const mainLocal of find.locals.keys()) { 279 | // if we have something like: 280 | // const A = 1, B = 2; 281 | // export { B as A }; 282 | // ..but we need A in the bundle, we need to rename it for the 'journey'; this seems rare for 283 | // hand-crafted code, but very possible with bundlers 284 | 285 | let skipExport = false; 286 | 287 | // TODO: this code could use a tidy up 288 | let nameForTransport = mainLocal; 289 | const prev = this.agg.mod.getExport(nameForTransport); 290 | if (prev?.name !== mainLocal) { 291 | const alreadyShadowed = this.mainLocalExportAs.get(mainLocal); 292 | if (alreadyShadowed) { 293 | continue; 294 | } 295 | nameForTransport = this.varForMain(); 296 | this.mainLocalExportAs.set(mainLocal, nameForTransport); 297 | } else if (prev) { 298 | // we're already exported from the main file; just re-use that 299 | skipExport = true; 300 | } 301 | 302 | if (!skipExport) { 303 | this.agg.mod.addExportLocal(nameForTransport, mainLocal); 304 | } 305 | targetStatic.mod.addImport(this.args.sourceName, mainLocal, nameForTransport); 306 | } 307 | return {}; 308 | } 309 | 310 | liftFunctionDeclaration(fn: acorn.FunctionDeclaration) { 311 | const vi = this.vars.get(fn.id.name); 312 | if (!vi?.local || vi.local.writes !== 1 || vi.nested?.writes) { 313 | return null; // discard complex 314 | } 315 | 316 | const analysis = analyzeFunction(fn); 317 | return this.addCodeToStatic({ node: fn, analysis }); 318 | } 319 | 320 | liftClassDeclaration(c: acorn.ClassDeclaration) { 321 | const analysis = analyzeBlock(createBlock(c)); 322 | // TODO: bit of a hack, otherwise we think class is written internally 323 | // (which is impossible) 324 | const self = analysis.vars.get(c.id.name); 325 | if (!self || self.nested?.writes || self.local?.writes !== 1) { 326 | throw new Error(`inconsistent class decl`); 327 | } 328 | analysis.vars.delete(c.id.name); 329 | return this.addCodeToStatic({ node: c, analysis }); 330 | } 331 | 332 | /** 333 | * Lifts the given expression. 334 | * If any of the variables used in `dirty` are used (r/rw), skip. 335 | */ 336 | liftExpression(e: acorn.Expression, dirty: string[] = []) { 337 | const analysis = analyzeBlock(createBlock(createExpressionStatement(e))); 338 | for (const d of dirty) { 339 | if (analysis.vars.has(d)) { 340 | return null; 341 | } 342 | } 343 | 344 | return this.addCodeToStatic({ node: e, analysis, var: true }); 345 | } 346 | 347 | // TODO: `pretty` doesn't really go everywhere yet 348 | build(args?: { pretty: boolean }) { 349 | const s = this.args.source; 350 | 351 | const newlineSuffix = args?.pretty ? '\n' : ''; 352 | 353 | // render statics 354 | const outStatic = new Map(); 355 | for (const [targetStaticName, info] of this.staticToWrite) { 356 | if (!info.exported.size) { 357 | // otherwise why does this exist?? 358 | throw new Error(`no vars to export in static file?`); 359 | } 360 | const code = 361 | info.mod.renderSource() + 362 | `export var ` + 363 | [...info.exported.entries()] 364 | .map(([name, code]) => `${name}=${code}`) 365 | .join(',' + newlineSuffix) + 366 | ';'; 367 | 368 | outStatic.set(targetStaticName, code); 369 | } 370 | 371 | // render main 372 | 373 | const { out: sourceWithoutModules, holes: skipHoles } = renderOnly(s, this.agg.rest); 374 | const skip: { start: number; end: number; replace?: string }[] = [ 375 | skipHoles, 376 | [...this.nodesToReplace.entries()].map(([node, replace]) => { 377 | return { ...node, replace }; 378 | }), 379 | ].flat(); 380 | 381 | // if this is `export default "foo";`, we need to reassign in case it was yeeted 382 | if (this.agg.exportDefaultHole?.decl === false) { 383 | const h = this.agg.exportDefaultHole; 384 | skip.push({ start: h.start, end: h.end, replace: `const ${this.exportDefaultName}=` }); 385 | skip.push({ start: h.after, end: h.after, replace: ';' }); 386 | } 387 | 388 | let outMain = ''; 389 | 390 | // persist shebang if present 391 | if (s.startsWith('#!')) { 392 | const indexOf = s.indexOf('\n'); 393 | outMain += s.substring(0, indexOf + 1 || s.length); 394 | } 395 | 396 | outMain += renderSkip(sourceWithoutModules, skip); 397 | outMain += this.agg.mod.renderSource(); 398 | 399 | return { main: outMain, static: outStatic }; 400 | } 401 | } 402 | -------------------------------------------------------------------------------- /lib/helper.ts: -------------------------------------------------------------------------------- 1 | export function withDefault(m: Map, k: K, build: (k: K) => V): V { 2 | if (m.has(k)) { 3 | return m.get(k)!; 4 | } 5 | const update = build(k); 6 | m.set(k, update); 7 | return update; 8 | } 9 | 10 | export function relativize(s: string) { 11 | if (s.startsWith('./') || s.startsWith('../')) { 12 | return s; 13 | } 14 | try { 15 | new URL(s); 16 | return s; 17 | } catch (e) {} 18 | return './' + s; 19 | } 20 | 21 | export function isLocalImport(s: string) { 22 | if (/^\.{0,2}\//.test(s)) { 23 | return true; 24 | } 25 | return false; 26 | } 27 | -------------------------------------------------------------------------------- /lib/internal/analyze/block.ts: -------------------------------------------------------------------------------- 1 | import type * as acorn from 'acorn'; 2 | import { createBlock, createExpressionStatement, processPattern } from './helper.ts'; 3 | import { MarkIdentifierFn, processExpression } from './expression.ts'; 4 | 5 | function reductifyStatement( 6 | b: acorn.Statement, 7 | ): acorn.BlockStatement | acorn.Expression | acorn.VariableDeclaration | void { 8 | switch (b.type) { 9 | case 'LabeledStatement': 10 | return reductifyStatement(b.body); 11 | 12 | case 'EmptyStatement': 13 | case 'ContinueStatement': 14 | case 'BreakStatement': 15 | case 'DebuggerStatement': 16 | return undefined; 17 | 18 | case 'BlockStatement': 19 | return b; 20 | 21 | case 'ExpressionStatement': 22 | return b.expression; 23 | 24 | case 'ReturnStatement': 25 | case 'ThrowStatement': 26 | return b.argument ?? undefined; 27 | 28 | case 'VariableDeclaration': 29 | return b; 30 | 31 | case 'FunctionDeclaration': { 32 | // pretend to be "let foo = function foo() { ... }" 33 | const decl: acorn.VariableDeclarator = { 34 | type: 'VariableDeclarator', 35 | start: -1, 36 | end: -1, 37 | id: b.id, 38 | init: { 39 | type: 'FunctionExpression', 40 | start: -1, 41 | end: -1, 42 | async: b.async, 43 | generator: b.generator, 44 | params: b.params, 45 | expression: true, // not really but fine 46 | id: b.id, 47 | body: b.body, 48 | }, 49 | }; 50 | return { 51 | type: 'VariableDeclaration', 52 | start: -1, 53 | end: -1, 54 | kind: 'let', 55 | declarations: [decl], 56 | }; 57 | } 58 | 59 | case 'ClassDeclaration': { 60 | // pretend to be "const Foo = class Foo { ... }" 61 | const decl: acorn.VariableDeclarator = { 62 | type: 'VariableDeclarator', 63 | start: -1, 64 | end: -1, 65 | id: b.id, 66 | init: { 67 | type: 'ClassExpression', 68 | start: -1, 69 | end: -1, 70 | id: b.id, 71 | superClass: b.superClass, 72 | body: b.body, 73 | }, 74 | }; 75 | return { 76 | type: 'VariableDeclaration', 77 | start: -1, 78 | end: -1, 79 | kind: 'const', 80 | declarations: [decl], 81 | }; 82 | } 83 | 84 | case 'IfStatement': { 85 | const body: acorn.Statement[] = [createExpressionStatement(b.test), b.consequent]; 86 | b.alternate && body.push(b.alternate); 87 | return createBlock(...body); 88 | } 89 | 90 | case 'WhileStatement': 91 | return createBlock(createExpressionStatement(b.test), b.body); 92 | 93 | case 'DoWhileStatement': 94 | return createBlock(b.body, createExpressionStatement(b.test)); 95 | 96 | case 'SwitchStatement': { 97 | const body: acorn.Statement[] = [createExpressionStatement(b.discriminant)]; 98 | for (const c of b.cases) { 99 | c.test && body.push(createExpressionStatement(c.test)); 100 | body.push(...c.consequent); 101 | } 102 | return createBlock(...body); 103 | } 104 | 105 | case 'ForStatement': { 106 | const body: acorn.Statement[] = []; 107 | 108 | if (b.init?.type === 'VariableDeclaration') { 109 | body.push(b.init); 110 | } else if (b.init) { 111 | body.push(createExpressionStatement(b.init)); 112 | } 113 | b.test && body.push(createExpressionStatement(b.test)); 114 | b.update && body.push(createExpressionStatement(b.update)); 115 | body.push(b.body); 116 | 117 | return createBlock(...body); 118 | } 119 | 120 | case 'ForInStatement': 121 | case 'ForOfStatement': { 122 | const body: acorn.Statement[] = []; 123 | 124 | if (b.left.type === 'VariableDeclaration') { 125 | body.push(b.left); 126 | } else { 127 | const { expression } = processPattern(b.left); 128 | body.push(createExpressionStatement(expression)); 129 | } 130 | 131 | body.push(createExpressionStatement(b.right)); 132 | body.push(b.body); 133 | 134 | return createBlock(...body); 135 | } 136 | 137 | case 'TryStatement': { 138 | const rest: acorn.Statement[] = []; 139 | 140 | if (b.handler) { 141 | if (b.handler.param) { 142 | // "catch (foo)" => something like "let foo" 143 | const decl: acorn.VariableDeclaration = { 144 | type: 'VariableDeclaration', 145 | start: -1, 146 | end: -1, 147 | declarations: [ 148 | { 149 | type: 'VariableDeclarator', 150 | start: -1, 151 | end: -1, 152 | id: b.handler.param, 153 | }, 154 | ], 155 | kind: 'let', 156 | }; 157 | rest.push(createBlock(decl, b.handler.body)); 158 | } else { 159 | rest.push(b.handler.body); 160 | } 161 | } 162 | b.finalizer && rest.push(b.finalizer); 163 | 164 | return createBlock(b.block, ...rest); 165 | } 166 | 167 | default: 168 | throw new Error(`unsupported: ${b.type}`); 169 | } 170 | } 171 | 172 | export type VarInfo = { 173 | /** 174 | * Is this used locally, and how often is it written. 175 | */ 176 | local?: { 177 | writes: number; 178 | kind?: 'let' | 'var' | 'const'; 179 | }; 180 | 181 | /** 182 | * Is this used in a nested callable block, and if so, how many times is it written. 183 | */ 184 | nested?: { writes: number }; 185 | }; 186 | 187 | export type AnalyzeBlock = { 188 | vars: Map; 189 | hasNested: boolean; 190 | hasAwait: boolean; 191 | }; 192 | 193 | /** 194 | * Analyze a block for its variable interactions. 195 | * 196 | * The `nest` argument is default `true`. 197 | */ 198 | export function analyzeBlock(b: acorn.BlockStatement, args?: { nest?: boolean }): AnalyzeBlock { 199 | const out: AnalyzeBlock = { vars: new Map(), hasNested: false, hasAwait: false }; 200 | const mark: MarkIdentifierFn = (name, arg) => { 201 | if ('special' in arg) { 202 | // this is a short-circuit to mark hasNested 203 | if (name) { 204 | throw new Error(`mark special called wrong`); 205 | } 206 | if (arg.special.nested) { 207 | out.hasNested = true; 208 | } 209 | if (arg.special.await) { 210 | out.hasAwait = true; 211 | } 212 | return; 213 | } 214 | if (!name) { 215 | throw new Error(`should be called with special`); 216 | } 217 | const { nested, writes } = arg; 218 | 219 | const info = out.vars.get(name); 220 | if (info === undefined) { 221 | // pure use: not declared here 222 | out.vars.set(name, nested ? { nested: { writes } } : { local: { writes } }); 223 | } else if (nested) { 224 | // used by a callable within here 225 | info.nested ??= { writes: 0 }; 226 | info.nested.writes += writes; 227 | } else { 228 | // used locally 229 | info.local ??= { writes: 0 }; 230 | info.local.writes += writes; 231 | } 232 | }; 233 | 234 | for (const raw of b.body) { 235 | const simple = reductifyStatement(raw) ?? { type: 'EmptyStatement' }; 236 | switch (simple.type) { 237 | case 'BlockStatement': { 238 | if (args?.nest === false) { 239 | break; // we can skip descending in some cases 240 | } 241 | 242 | const inner = analyzeBlock(simple); 243 | 244 | for (const [key, info] of inner.vars) { 245 | if (info.local?.kind && ['let', 'const'].includes(info.local?.kind)) { 246 | // doesn't escape inner block 247 | continue; 248 | } 249 | 250 | const prev = out.vars.get(key); 251 | if (prev === undefined) { 252 | // no merging required: either 'var' or external ref 253 | out.vars.set(key, info); 254 | continue; 255 | } 256 | 257 | if (info.local) { 258 | prev.local ??= { writes: 0 }; 259 | prev.local.writes += info.local.writes; 260 | 261 | if (info.local.kind === 'var') { 262 | if (prev.local?.kind && prev.local.kind !== 'var') { 263 | // inner 'var' found an outer 'let' or 'const' 264 | throw new Error(`got kind mismatch: inner 'var' found outer '${prev.local.kind}'`); 265 | } 266 | prev.local.kind = 'var'; 267 | } 268 | } 269 | 270 | if (info.nested) { 271 | prev.nested ??= { writes: 0 }; 272 | prev.nested.writes += info.nested.writes; 273 | } 274 | } 275 | break; 276 | } 277 | 278 | case 'VariableDeclaration': { 279 | for (const declaration of simple.declarations) { 280 | const p = processPattern(declaration.id); 281 | 282 | // can be unwritten in let/var without initializer 283 | const written = Boolean(p.init || declaration.init); 284 | const writes = written ? 1 : 0; 285 | 286 | for (const name of p.names) { 287 | const prev = out.vars.get(name); 288 | if (prev === undefined) { 289 | out.vars.set(name, { local: { writes, kind: simple.kind } }); 290 | continue; 291 | } 292 | 293 | if (prev.local?.kind && (simple.kind !== 'var' || prev.local.kind !== 'var')) { 294 | throw new Error( 295 | `got kind mismatch: can only redeclare 'var', was '${prev.local.kind}' got '${simple.kind}'`, 296 | ); 297 | } 298 | prev.local ??= { writes: 0 }; 299 | prev.local.kind = simple.kind; 300 | prev.local.writes += writes; 301 | } 302 | 303 | p.init && processExpression(p.init, mark); 304 | declaration.init && processExpression(declaration.init, mark); 305 | } 306 | break; 307 | } 308 | 309 | case 'EmptyStatement': 310 | continue; 311 | 312 | default: 313 | processExpression(simple, mark); 314 | } 315 | } 316 | 317 | return out; 318 | } 319 | -------------------------------------------------------------------------------- /lib/internal/analyze/expression.ts: -------------------------------------------------------------------------------- 1 | import type * as acorn from 'acorn'; 2 | import { analyzeBlock } from './block.ts'; 3 | import { 4 | createBlock, 5 | createExpressionStatement, 6 | createSequenceExpression, 7 | processPattern, 8 | } from './helper.ts'; 9 | 10 | export type MarkIdentifierFn = ( 11 | name: string, 12 | arg: { nested: boolean; writes: number } | { special: { await?: boolean; nested?: boolean } }, 13 | ) => void; 14 | 15 | /** 16 | * Returns the following statements inside an immediately-invoked function expression. 17 | */ 18 | function createIife(body: acorn.Statement[]): acorn.CallExpression { 19 | return { 20 | type: 'CallExpression', 21 | arguments: [], 22 | callee: { 23 | type: 'FunctionExpression', 24 | body: { 25 | type: 'BlockStatement', 26 | body, 27 | start: -1, 28 | end: -1, 29 | }, 30 | start: -1, 31 | end: -1, 32 | params: [], 33 | async: false, 34 | expression: true, 35 | generator: false, 36 | // nb. this does NOT have an id; having an id makes this callable again 37 | }, 38 | start: -1, 39 | end: -1, 40 | optional: false, 41 | }; 42 | } 43 | 44 | export function patternsToDeclaration(...p: acorn.Pattern[]): acorn.VariableDeclaration { 45 | const decl: acorn.VariableDeclaration = { 46 | type: 'VariableDeclaration', 47 | start: -1, 48 | end: -1, 49 | kind: 'var', 50 | declarations: p.map((id): acorn.VariableDeclarator => { 51 | return { 52 | type: 'VariableDeclarator', 53 | start: id.start, 54 | end: id.end, 55 | id, 56 | }; 57 | }), 58 | }; 59 | return decl; 60 | } 61 | 62 | export function namesFromDeclaration(d: acorn.VariableDeclaration) {} 63 | 64 | /** 65 | * Returns the given "class" as a number of simple component parts. 66 | * This can't be used or run but is the same from an analysis point of view. 67 | */ 68 | function reductifyClassParts(c: acorn.Class): acorn.Expression { 69 | const e: acorn.Expression[] = []; 70 | c.superClass && e.push(c.superClass); 71 | 72 | for (const part of c.body.body) { 73 | switch (part.type) { 74 | case 'MethodDefinition': 75 | case 'PropertyDefinition': 76 | if (part.computed) { 77 | e.push(part.key as acorn.Expression); 78 | } 79 | if (!part.value) { 80 | break; 81 | } 82 | if (part.static || part.type === 'MethodDefinition') { 83 | // evaluated here 84 | e.push(part.value); 85 | break; 86 | } 87 | // otherwise pretend to be a method, not "called" immediately 88 | e.push({ 89 | type: 'ArrowFunctionExpression', 90 | start: -1, 91 | end: -1, 92 | body: part.value, 93 | params: [], 94 | generator: false, 95 | async: false, 96 | expression: true, 97 | }); 98 | break; 99 | 100 | case 'StaticBlock': 101 | // push self-evaluated function expr 102 | e.push(createIife(part.body)); 103 | break; 104 | } 105 | } 106 | 107 | return e.length === 1 ? e[0] : createSequenceExpression(...e); 108 | } 109 | 110 | export function reductifyFunction(f: acorn.Function): acorn.BlockStatement { 111 | const body: acorn.Statement[] = []; 112 | 113 | // our own function name becomes something we can reference 114 | if (f.id?.name && f.id.name !== 'default') { 115 | const decl: acorn.VariableDeclaration = { 116 | type: 'VariableDeclaration', 117 | start: -1, 118 | end: -1, 119 | kind: 'var', 120 | declarations: [ 121 | { 122 | type: 'VariableDeclarator', 123 | start: f.id.start, 124 | end: f.id.end, 125 | id: f.id, 126 | }, 127 | ], 128 | }; 129 | body.push(decl); 130 | } 131 | 132 | if (f.params.length) { 133 | body.push(patternsToDeclaration(...f.params)); 134 | } else if (f.body.type === 'BlockStatement') { 135 | return f.body; 136 | } 137 | 138 | body.push(f.body.type === 'BlockStatement' ? f.body : createExpressionStatement(f.body)); 139 | return createBlock(...body); 140 | } 141 | 142 | export function processExpression( 143 | e: acorn.Expression | acorn.Super | acorn.PrivateIdentifier | acorn.SpreadElement, 144 | mark: MarkIdentifierFn, 145 | ): void { 146 | switch (e.type) { 147 | case 'PrivateIdentifier': 148 | case 'Super': 149 | case 'Literal': 150 | case 'ThisExpression': 151 | case 'MetaProperty': 152 | break; 153 | 154 | case 'ChainExpression': 155 | case 'ParenthesizedExpression': 156 | processExpression(e.expression, mark); 157 | break; 158 | 159 | case 'AwaitExpression': 160 | mark('', { special: { await: true } }); 161 | processExpression(e.argument, mark); 162 | break; 163 | 164 | case 'SpreadElement': 165 | case 'YieldExpression': 166 | case 'UnaryExpression': 167 | e.argument && processExpression(e.argument, mark); 168 | break; 169 | 170 | case 'Identifier': 171 | mark(e.name, { nested: false, writes: 0 }); 172 | break; 173 | 174 | case 'AssignmentExpression': { 175 | const p = processPattern(e.left); 176 | p.names.forEach((name) => mark(name, { nested: false, writes: 1 })); 177 | p.init && processExpression(p.init, mark); 178 | processExpression(e.right, mark); 179 | break; 180 | } 181 | 182 | case 'UpdateExpression': { 183 | if (e.argument.type === 'Identifier') { 184 | // nb. acorn unwraps "((foo))++" for us, so this is probably safe 185 | mark(e.argument.name, { nested: false, writes: 1 }); 186 | } else { 187 | processExpression(e.argument, mark); 188 | } 189 | break; 190 | } 191 | 192 | case 'ImportExpression': 193 | // we basically use the global 'import', even though this is a keyword 194 | mark('import', { nested: false, writes: 0 }); 195 | processExpression(e.source, mark); 196 | break; 197 | 198 | case 'NewExpression': 199 | case 'CallExpression': 200 | e.arguments.forEach((arg) => processExpression(arg, mark)); 201 | if ( 202 | !(e.callee.type === 'ArrowFunctionExpression' || e.callee.type === 'FunctionExpression') 203 | ) { 204 | processExpression(e.callee, mark); 205 | break; 206 | } 207 | 208 | // this is an IIFE, 'run' immediately and pretend access isn't nested 209 | const block = reductifyFunction(e.callee); 210 | const inner = analyzeBlock(block); 211 | for (const [key, info] of inner.vars) { 212 | if (!info?.local?.kind) { 213 | const writes = (info.local?.writes ?? 0) + (info.nested?.writes ?? 0); 214 | mark(key, { nested: false, writes }); 215 | } 216 | } 217 | break; 218 | 219 | case 'TemplateLiteral': 220 | case 'SequenceExpression': 221 | e.expressions.forEach((arg) => processExpression(arg, mark)); 222 | break; 223 | 224 | case 'ArrayExpression': 225 | e.elements.forEach((el) => el && processExpression(el, mark)); 226 | break; 227 | 228 | case 'ConditionalExpression': 229 | processExpression(e.test, mark); 230 | processExpression(e.consequent, mark); 231 | processExpression(e.alternate, mark); 232 | break; 233 | 234 | case 'BinaryExpression': 235 | case 'LogicalExpression': 236 | processExpression(e.left, mark); 237 | processExpression(e.right, mark); 238 | break; 239 | 240 | case 'ClassExpression': 241 | mark('', { special: { nested: true } }); 242 | processExpression(reductifyClassParts(e), mark); 243 | break; 244 | 245 | case 'FunctionExpression': 246 | case 'ArrowFunctionExpression': { 247 | mark('', { special: { nested: true } }); 248 | 249 | const block = reductifyFunction(e); 250 | const inner = analyzeBlock(block); 251 | 252 | for (const [key, info] of inner.vars) { 253 | if (!info?.local?.kind) { 254 | const writes = (info.local?.writes ?? 0) + (info.nested?.writes ?? 0); 255 | mark(key, { nested: true, writes }); 256 | } 257 | } 258 | break; 259 | } 260 | 261 | case 'TaggedTemplateExpression': 262 | processExpression(e.tag, mark); 263 | processExpression(e.quasi, mark); 264 | break; 265 | 266 | case 'MemberExpression': 267 | processExpression(e.object, mark); 268 | e.computed && processExpression(e.property, mark); 269 | break; 270 | 271 | case 'ObjectExpression': { 272 | for (const prop of e.properties) { 273 | if (prop.type === 'SpreadElement') { 274 | processExpression(prop.argument, mark); 275 | } else { 276 | prop.computed && processExpression(prop.key, mark); 277 | processExpression(prop.value, mark); 278 | } 279 | } 280 | break; 281 | } 282 | 283 | default: 284 | throw new Error(`should not get here: ${(e as any).type}`); 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /lib/internal/analyze/helper.ts: -------------------------------------------------------------------------------- 1 | import type * as acorn from 'acorn'; 2 | 3 | export function createSequenceExpression(...parts: acorn.Expression[]): acorn.SequenceExpression { 4 | return { 5 | type: 'SequenceExpression', 6 | start: -1, 7 | end: -1, 8 | expressions: parts, 9 | }; 10 | } 11 | 12 | export function createBlock(...body: acorn.Statement[]): acorn.BlockStatement { 13 | return { 14 | type: 'BlockStatement', 15 | start: -1, 16 | end: -1, 17 | body, 18 | }; 19 | } 20 | 21 | export function createExpressionStatement(...body: acorn.Expression[]): acorn.ExpressionStatement { 22 | return { 23 | type: 'ExpressionStatement', 24 | start: -1, 25 | end: -1, 26 | expression: body.length === 1 ? body[0] : createSequenceExpression(...body), 27 | }; 28 | } 29 | 30 | export function processPattern(p: acorn.Pattern) { 31 | const names: string[] = []; 32 | const expr: acorn.Expression[] = []; 33 | const pending = [p]; 34 | 35 | while (pending.length) { 36 | const p = pending.shift()!; 37 | switch (p.type) { 38 | case 'Identifier': 39 | names.push(p.name); 40 | continue; 41 | 42 | case 'RestElement': 43 | pending.push(p.argument); 44 | continue; 45 | 46 | case 'ArrayPattern': 47 | for (const e of p.elements) { 48 | e && pending.push(e); 49 | } 50 | continue; 51 | 52 | case 'AssignmentPattern': 53 | pending.push(p.left); 54 | expr.push(p.right); 55 | continue; 56 | 57 | case 'ObjectPattern': 58 | for (const prop of p.properties) { 59 | if (prop.type !== 'Property') { 60 | pending.push(prop); 61 | continue; 62 | } 63 | 64 | prop.computed && expr.push(prop.key); 65 | pending.push(prop.value); 66 | } 67 | continue; 68 | 69 | case 'MemberExpression': 70 | if (p.object.type !== 'Super') { 71 | expr.push(p.object); 72 | } 73 | if (p.computed && p.property.type !== 'PrivateIdentifier') { 74 | expr.push(p.property); 75 | // don't push Identifier, this makes us get "foo.bar.zing" all together 76 | } 77 | continue; 78 | } 79 | 80 | throw `should not get here`; 81 | } 82 | 83 | const init = expr.slice(); 84 | 85 | for (const name of names) { 86 | expr.push({ 87 | type: 'Identifier', 88 | start: -1, 89 | end: -1, 90 | name: name, 91 | }); 92 | } 93 | 94 | return { 95 | names, 96 | expression: createSequenceExpression(...expr), 97 | init: init.length ? createSequenceExpression(...init) : undefined, 98 | }; 99 | } 100 | -------------------------------------------------------------------------------- /lib/internal/analyze/module.ts: -------------------------------------------------------------------------------- 1 | import type * as acorn from 'acorn'; 2 | import { ModDef } from '../moddef.ts'; 3 | import { createExpressionStatement, processPattern } from './helper.ts'; 4 | 5 | export type AggregateImports = { 6 | mod: ModDef; 7 | localConst: Set; 8 | rest: acorn.Statement[]; 9 | 10 | /** 11 | * Set only if this is an unnamed default export, e.g. `export default function () { ... }` or `export default "foo";`. 12 | * 13 | * It is _not_ set if it's named `export default class Foo {}`. 14 | */ 15 | exportDefaultHole?: { 16 | start: number; 17 | end: number; 18 | after: number; 19 | decl: boolean; 20 | }; 21 | }; 22 | 23 | const fakeDefaultIdentifier: acorn.Identifier = Object.freeze({ 24 | start: -1, 25 | end: -1, 26 | type: 'Identifier', 27 | name: 'default', 28 | }); 29 | 30 | const buildFakeDefaultConst = (expr: acorn.Expression): acorn.VariableDeclaration => { 31 | const decl: acorn.VariableDeclarator = { 32 | start: -1, 33 | end: -1, 34 | type: 'VariableDeclarator', 35 | id: { 36 | start: -1, 37 | end: -1, 38 | type: 'Identifier', 39 | name: 'default', 40 | }, 41 | init: expr, 42 | }; 43 | return { 44 | start: -1, 45 | end: -1, 46 | type: 'VariableDeclaration', 47 | declarations: [decl], 48 | kind: 'const', 49 | }; 50 | }; 51 | 52 | function nodeToString(source: acorn.Literal | acorn.Identifier | string): string { 53 | if (typeof source === 'string') { 54 | return source; 55 | } 56 | 57 | if (source.type === 'Identifier') { 58 | return source.name; 59 | } 60 | 61 | if (typeof source.value === 'string') { 62 | return source.value; 63 | } 64 | 65 | throw new Error(`importing non-string?`); 66 | } 67 | 68 | export function aggregateImports(p: acorn.Program): AggregateImports { 69 | const out: AggregateImports = { 70 | mod: new ModDef(), 71 | localConst: new Set(), 72 | rest: [], 73 | }; 74 | 75 | // early pass: ordering + record module parts 76 | 77 | for (const node of p.body) { 78 | switch (node.type) { 79 | case 'ImportDeclaration': 80 | case 'ExportAllDeclaration': 81 | case 'ExportNamedDeclaration': 82 | if (node.source) { 83 | const importSource = node.source.value as string; 84 | out.mod.addSource(importSource); 85 | } 86 | break; 87 | } 88 | } 89 | 90 | // main pass 91 | 92 | for (const node of p.body) { 93 | switch (node.type) { 94 | case 'ImportDeclaration': { 95 | const importSource = node.source.value as string; 96 | out.mod.addSource(importSource); 97 | 98 | for (const s of node.specifiers) { 99 | const local = s.local.name; 100 | switch (s.type) { 101 | case 'ImportNamespaceSpecifier': 102 | out.mod.addGlobalImport(importSource, local); 103 | break; 104 | case 'ImportDefaultSpecifier': 105 | out.mod.addImport(importSource, local, 'default'); 106 | break; 107 | case 'ImportSpecifier': 108 | out.mod.addImport(importSource, local, nodeToString(s.imported)); 109 | break; 110 | } 111 | } 112 | break; 113 | } 114 | 115 | case 'ExportAllDeclaration': { 116 | const importSource = node.source.value as string; 117 | 118 | // this is a re-export of all 119 | if (!node.exported) { 120 | // ...without a name, e.g. "export * from '...'" 121 | // kuto can't handle this 122 | out.mod.markExportAllFrom(importSource); 123 | } else { 124 | // with a name 125 | out.mod.addExportFrom(importSource, nodeToString(node.exported), ''); 126 | } 127 | continue; 128 | } 129 | 130 | case 'ExportNamedDeclaration': 131 | if (!node.declaration) { 132 | const names: { exportedName: string; name: string }[] = []; 133 | 134 | for (const s of node.specifiers) { 135 | names.push({ exportedName: nodeToString(s.exported), name: nodeToString(s.local) }); 136 | } 137 | 138 | if (node.source) { 139 | // direct re-export 140 | const importSource = node.source.value as string; 141 | out.mod.addSource(importSource); 142 | 143 | for (const { exportedName, name } of names) { 144 | out.mod.addExportFrom(importSource, exportedName, name); 145 | } 146 | } else { 147 | // local export 148 | for (const { exportedName, name } of names) { 149 | out.mod.addExportLocal(exportedName, name); 150 | } 151 | } 152 | continue; 153 | } else if (node.declaration.type === 'VariableDeclaration') { 154 | const isConst = node.declaration.kind === 'const'; 155 | 156 | for (const s of node.declaration.declarations) { 157 | const p = processPattern(s.id); 158 | p.names.forEach((name) => { 159 | out.mod.addExportLocal(name); 160 | isConst && out.localConst.add(name); 161 | }); 162 | } 163 | 164 | out.rest.push(node.declaration); 165 | continue; 166 | } 167 | // fall-through 168 | 169 | case 'ExportDefaultDeclaration': { 170 | const d = node.declaration!; 171 | const isDefault = node.type === 'ExportDefaultDeclaration'; 172 | 173 | switch (d.type) { 174 | case 'FunctionDeclaration': 175 | case 'ClassDeclaration': 176 | break; 177 | 178 | case 'VariableDeclaration': 179 | throw new Error(`TS confused`); 180 | 181 | default: // default is an expr, so evaluated immediately: always const 182 | if (!isDefault) { 183 | throw new Error(`default but not default?`); 184 | } 185 | out.mod.addExportLocal('default', 'default'); 186 | out.localConst.add('default'); 187 | out.exportDefaultHole = { start: node.start, end: d.start, after: d.end, decl: false }; 188 | // don't use helper, it doesn't include start/end properly 189 | out.rest.push({ 190 | type: 'ExpressionStatement', 191 | start: d.start, 192 | end: d.end, 193 | expression: d, 194 | }); 195 | continue; 196 | } 197 | 198 | if (d.id && isDefault) { 199 | // e.g., "export default class foo {}; foo = 123;" 200 | out.mod.addExportLocal('default', d.id.name); 201 | } else if (d.id) { 202 | // normal 203 | out.mod.addExportLocal(d.id.name, d.id.name); 204 | } else if (isDefault) { 205 | // can't reassign unnamed declaration 206 | out.mod.addExportLocal('default', 'default'); 207 | out.localConst.add('default'); 208 | out.exportDefaultHole = { start: node.start, end: d.start, after: d.end, decl: true }; 209 | } else { 210 | throw new Error(`unnamed declaration`); 211 | } 212 | 213 | out.rest.push({ ...d, id: d.id || fakeDefaultIdentifier }); 214 | continue; 215 | } 216 | 217 | case 'VariableDeclaration': 218 | out.rest.push(node); 219 | 220 | if (node.kind !== 'const') { 221 | continue; 222 | } 223 | 224 | // store top-level const in case it's exported as something else 225 | for (const s of node.declarations) { 226 | const p = processPattern(s.id); 227 | p.names.forEach((name) => out.localConst.add(name)); 228 | } 229 | continue; 230 | 231 | default: // boring expr 232 | out.rest.push(node); 233 | continue; 234 | } 235 | } 236 | 237 | return out; 238 | } 239 | -------------------------------------------------------------------------------- /lib/internal/moddef.ts: -------------------------------------------------------------------------------- 1 | import { withDefault } from '../helper.ts'; 2 | 3 | type SourceInfo = { 4 | /** 5 | * Remote name (singular) to all local name(s). 6 | */ 7 | imports: Map>; 8 | 9 | /** 10 | * Remote name (singular) to directly exported name(s). 11 | */ 12 | exports: Map>; 13 | 14 | /** 15 | * Whether we include a re-export of all. 16 | */ 17 | reexportAll: boolean; 18 | }; 19 | 20 | function safeImportAs(from: string, to: string = from) { 21 | if (!from || !to) { 22 | throw new Error(`cannot safeImportAs: from=${from} to=${to}`); 23 | } 24 | 25 | // TODO: make actually safe 26 | if (from === to) { 27 | return from; 28 | } 29 | return `${from} as ${to}`; 30 | } 31 | 32 | export type ImportInfo = { 33 | import: string; 34 | remote: string; 35 | }; 36 | 37 | /** 38 | * ModDef contains mutable module import/export information for a single file. 39 | */ 40 | export class ModDef { 41 | private bySource: Map = new Map(); 42 | private byLocalName: Map = new Map(); 43 | private _exports: Map = new Map(); 44 | private allLocalExported: Map = new Map(); 45 | 46 | /** 47 | * Yields the names of local variables that are later exported. 48 | * Does not yield their exported name(s). 49 | */ 50 | *localExported(): Generator { 51 | const seen = new Set(); 52 | for (const info of this._exports.values()) { 53 | if (info.import || seen.has(info.name)) { 54 | continue; 55 | } 56 | seen.add(info.name); 57 | yield info.name; 58 | } 59 | } 60 | 61 | *importSources(): Generator<{ name: string; info: SourceInfo }, void, void> { 62 | for (const [name, info] of this.bySource) { 63 | yield { name, info }; 64 | } 65 | } 66 | 67 | *importByName(): Generator<{ name: string; info: ImportInfo }, void, void> { 68 | for (const [name, info] of this.byLocalName) { 69 | yield { name, info }; 70 | } 71 | } 72 | 73 | *exported(): Generator<{ exportedName: string; import?: string; name: string }, void, void> { 74 | for (const [exportedName, info] of this._exports) { 75 | yield { exportedName, ...info }; 76 | } 77 | } 78 | 79 | lookupImport(name: string): ImportInfo | undefined { 80 | const o = this.byLocalName.get(name); 81 | return o ? { ...o } : undefined; 82 | } 83 | 84 | addSource(importSource: string): SourceInfo { 85 | return withDefault(this.bySource, importSource, () => ({ 86 | imports: new Map(), 87 | exports: new Map(), 88 | reexportAll: false, 89 | })); 90 | } 91 | 92 | private _addImport(importSource: string, localName: string, remoteName: string) { 93 | const prev = this.byLocalName.get(localName); 94 | if (prev) { 95 | if (prev.import !== importSource || prev.remote !== remoteName) { 96 | // only throw if different 97 | throw new Error( 98 | `already imported differently: ${localName} (was ${JSON.stringify( 99 | prev, 100 | )}, update ${JSON.stringify({ 101 | import: importSource, 102 | remote: remoteName, 103 | })})`, 104 | ); 105 | } 106 | return; 107 | } 108 | 109 | if (localName === '') { 110 | throw new Error(`can't have blank localName`); 111 | } 112 | this.byLocalName.set(localName, { import: importSource, remote: remoteName }); 113 | 114 | const info = this.addSource(importSource); 115 | const s = withDefault(info.imports, remoteName, () => new Set()); 116 | s.add(localName); 117 | } 118 | 119 | addGlobalImport(importSource: string, localName: string) { 120 | return this._addImport(importSource, localName, ''); 121 | } 122 | 123 | addImport(importSource: string, localName: string, remoteName: string = localName) { 124 | if (remoteName === '') { 125 | throw new Error(`can't have blank remoteName`); 126 | } 127 | return this._addImport(importSource, localName, remoteName); 128 | } 129 | 130 | removeImport(localName: string) { 131 | const prev = this.byLocalName.get(localName); 132 | if (!prev) { 133 | return false; 134 | } 135 | 136 | this.byLocalName.delete(localName); 137 | const info = this.bySource.get(prev.import)!; 138 | const s = withDefault(info.imports, prev.remote, () => new Set()); 139 | s.delete(localName); 140 | if (s.size === 0) { 141 | info.imports.delete(prev.remote); 142 | } 143 | return false; 144 | } 145 | 146 | addExportFrom(importSource: string, exportedName: string, remoteName: string = '') { 147 | const prev = this._exports.get(exportedName); 148 | if (prev !== undefined) { 149 | if (prev.import !== importSource && prev.name !== remoteName) { 150 | throw new Error( 151 | `already exported: ${exportedName} (was ${JSON.stringify(prev)}, update ${JSON.stringify({ 152 | import: importSource, 153 | name: remoteName, 154 | })})`, 155 | ); 156 | } 157 | // got called twice for some reason 158 | } 159 | this._exports.set(exportedName, { import: importSource, name: remoteName }); 160 | 161 | const info = this.addSource(importSource); 162 | const s = withDefault(info.exports, remoteName, () => new Set()); 163 | s.add(exportedName); 164 | } 165 | 166 | markExportAllFrom(importSource: string) { 167 | const info = this.addSource(importSource); 168 | info.reexportAll = true; 169 | } 170 | 171 | hasExportAllFrom() { 172 | for (const [name, info] of this.bySource.entries()) { 173 | if (info.reexportAll) { 174 | return name; 175 | } 176 | } 177 | } 178 | 179 | getExport(exportedName: string) { 180 | const prev = this._exports.get(exportedName); 181 | return prev ? { ...prev } : undefined; 182 | } 183 | 184 | addExportLocal(exportedName: string, sourceName: string = exportedName) { 185 | const prev = this._exports.get(exportedName); 186 | if (prev) { 187 | if (prev.import || prev.name !== sourceName) { 188 | // only throw if different 189 | throw new Error( 190 | `already exported: ${exportedName} (was ${JSON.stringify(prev)}, update ${JSON.stringify({ 191 | name: sourceName, 192 | })})`, 193 | ); 194 | } 195 | return; 196 | } 197 | const p = { name: sourceName }; 198 | this._exports.set(exportedName, p); 199 | this.allLocalExported.set(exportedName, p); 200 | } 201 | 202 | removeExportLocal(exportedName: string) { 203 | const prev = this._exports.get(exportedName); 204 | if (!prev) { 205 | return false; 206 | } 207 | this._exports.delete(exportedName); 208 | this.allLocalExported.delete(exportedName); 209 | return true; 210 | } 211 | 212 | renderSource() { 213 | const lines: string[] = []; 214 | 215 | for (const [path, info] of this.bySource) { 216 | const pj = JSON.stringify(path); 217 | let any = false; 218 | 219 | for (const localName of info.imports.get('') ?? []) { 220 | lines.push(`import * as ${localName} from ${pj}`); 221 | any = true; 222 | } 223 | 224 | const parts: string[] = []; 225 | for (const [remote, local] of info.imports) { 226 | if (remote === '') { 227 | continue; 228 | } 229 | for (const l of local) { 230 | parts.push(safeImportAs(remote, l)); 231 | } 232 | } 233 | if (parts.length) { 234 | lines.push(`import {${parts.join(',')}} from ${pj}`); 235 | any = true; 236 | } 237 | 238 | if (info.reexportAll) { 239 | lines.push(`export * from ${pj}`); 240 | } 241 | 242 | // TODO: if these are here with local names, we could instead pick one and re-export 243 | const reexportParts: string[] = []; 244 | for (const [remote, exported] of info.exports) { 245 | for (const e of exported) { 246 | if (remote !== '') { 247 | reexportParts.push(safeImportAs(remote, e)); 248 | continue; 249 | } 250 | lines.push(`export * as ${e} from ${pj}`); 251 | any = true; 252 | } 253 | } 254 | if (reexportParts.length) { 255 | lines.push(`export {${reexportParts.join(',')}} from ${pj}`); 256 | any = true; 257 | } 258 | 259 | if (!any) { 260 | lines.push(`import ${pj}`); 261 | } 262 | } 263 | 264 | if (this.allLocalExported.size) { 265 | const parts: string[] = []; 266 | for (const [remote, { name }] of this.allLocalExported) { 267 | parts.push(safeImportAs(name, remote)); 268 | } 269 | lines.push(`export {${parts.join(',')}}`); 270 | } 271 | 272 | lines.push(''); 273 | return lines.join(';'); 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /lib/interpret.ts: -------------------------------------------------------------------------------- 1 | import { AnalyzeBlock, VarInfo } from './internal/analyze/block.ts'; 2 | import { AggregateImports } from './internal/analyze/module.ts'; 3 | import { ImportInfo, ModDef } from './internal/moddef.ts'; 4 | 5 | export function resolveConst(agg: AggregateImports, analysis: AnalyzeBlock) { 6 | // resolve whether local vars are const - look for writes inside later callables 7 | // TODO: this doesn't explicitly mark their exported names as const, can look later? 8 | // nb. not _actually_ used yet 9 | for (const name of agg.mod.localExported()) { 10 | if (agg.localConst.has(name)) { 11 | continue; 12 | } 13 | 14 | const v = analysis.vars.get(name); 15 | if (v?.local && !v.nested?.writes) { 16 | // only update _if actually local_ 17 | // just check it's not written nested (read is fine) 18 | agg.localConst.add(name); 19 | } 20 | } 21 | 22 | return agg.localConst; 23 | } 24 | 25 | type FindVarsArgs = { 26 | find: Map; 27 | vars: Map; 28 | mod: ModDef; 29 | }; 30 | 31 | export function findVars({ find, vars, mod }: FindVarsArgs) { 32 | const globals = new Map(); 33 | const imports = new Map(); 34 | const locals = new Map(); 35 | let anyRw = false; 36 | let immediateAccess = false; 37 | 38 | for (const [key, check] of find) { 39 | const rw = Boolean(check.local?.writes || check.nested?.writes); 40 | anyRw ||= rw; 41 | immediateAccess ||= Boolean(check.local); 42 | 43 | const vi = vars.get(key)!; 44 | if (!vi.local?.kind) { 45 | const importInfo = mod.lookupImport(key); 46 | if (importInfo) { 47 | imports.set(key, importInfo); 48 | continue; 49 | } 50 | globals.set(key, rw); 51 | } else { 52 | locals.set(key, rw); 53 | } 54 | } 55 | 56 | return { globals, imports, locals, rw: anyRw, immediateAccess }; 57 | } 58 | -------------------------------------------------------------------------------- /lib/lift.ts: -------------------------------------------------------------------------------- 1 | import { StaticExtractor } from './extractor.ts'; 2 | import type * as acorn from 'acorn'; 3 | import { analyzeBlock } from './internal/analyze/block.ts'; 4 | import { reductifyFunction } from './internal/analyze/expression.ts'; 5 | import { createBlock } from './internal/analyze/helper.ts'; 6 | 7 | export function liftDefault(e: StaticExtractor, minSize: number) { 8 | const stats = { 9 | fn: 0, 10 | class: 0, 11 | expr: 0, 12 | assignment: 0, 13 | _skip: 0, 14 | }; 15 | 16 | // lift a subpath of a complex statement 17 | const innerLiftMaybeBlock = (b: acorn.Statement | null | undefined, dirty: string[]) => { 18 | if (!b) { 19 | return; 20 | } 21 | if (b.type !== 'BlockStatement') { 22 | if (b.type === 'VariableDeclaration') { 23 | // only valid thing is 'if (1) var x = ...', nope nope nope 24 | return; 25 | } 26 | b = createBlock(b); // treat as miniblock 27 | } else { 28 | const a = analyzeBlock(b, { nest: false }); 29 | const declaredHere: string[] = []; 30 | a.vars.forEach((info, key) => { 31 | if (info.local?.kind) { 32 | declaredHere.push(key); 33 | } 34 | }); 35 | dirty = dirty.concat(declaredHere); 36 | } 37 | 38 | innerLift(b, dirty); 39 | }; 40 | 41 | const innerLift = (b: acorn.BlockStatement, dirty: string[]) => { 42 | const maybeLift = (expr: acorn.Expression | null | undefined, ok: () => void) => { 43 | if (!expr) { 44 | return; 45 | } 46 | const size = expr.end - expr.start; 47 | if (size < minSize) { 48 | return; 49 | } 50 | if (e.liftExpression(expr, dirty)) { 51 | ok(); 52 | } else { 53 | ++stats._skip; 54 | } 55 | }; 56 | 57 | for (const part of b.body) { 58 | switch (part.type) { 59 | case 'ExpressionStatement': 60 | if ( 61 | part.expression.type === 'CallExpression' && 62 | (part.expression.callee.type === 'FunctionExpression' || 63 | part.expression.callee.type === 'ArrowFunctionExpression') 64 | ) { 65 | // IIFE 66 | const r = reductifyFunction(part.expression.callee); 67 | innerLiftMaybeBlock(r, dirty); 68 | } else if (part.expression.type === 'AssignmentExpression') { 69 | // find things on the right of "=" 70 | // this won't lift normally (the left part changes) 71 | maybeLift(part.expression.right, () => stats.assignment++); 72 | } else { 73 | // try the whole thing? :shrug: 74 | maybeLift(part.expression, () => stats.expr++); 75 | } 76 | continue; 77 | 78 | case 'VariableDeclaration': 79 | for (const decl of part.declarations) { 80 | maybeLift(decl.init, () => stats.assignment++); 81 | } 82 | continue; 83 | 84 | case 'ReturnStatement': 85 | // why not? might be big 86 | maybeLift(part.argument, () => stats.expr++); 87 | continue; 88 | 89 | // -- nested control statements below here 90 | 91 | case 'WhileStatement': 92 | case 'DoWhileStatement': 93 | innerLiftMaybeBlock(part.body, dirty); 94 | break; 95 | 96 | case 'IfStatement': 97 | innerLiftMaybeBlock(part.consequent, dirty); 98 | innerLiftMaybeBlock(part.alternate, dirty); 99 | break; 100 | 101 | case 'BlockStatement': 102 | innerLiftMaybeBlock(part, dirty); 103 | break; 104 | 105 | case 'TryStatement': 106 | innerLiftMaybeBlock(part.block, dirty); 107 | // TODO: include handler (maybe declares var) 108 | innerLiftMaybeBlock(part.finalizer, dirty); 109 | break; 110 | 111 | // TODO: include for/etc which can declare vars 112 | } 113 | } 114 | }; 115 | 116 | // lift top-level fn blocks 117 | for (const part of e.block.body) { 118 | const size = part.end - part.start; 119 | if (size < minSize) { 120 | continue; 121 | } 122 | 123 | switch (part.type) { 124 | case 'FunctionDeclaration': 125 | if (e.liftFunctionDeclaration(part)) { 126 | ++stats.fn; 127 | } else { 128 | ++stats._skip; 129 | } 130 | break; 131 | 132 | case 'ClassDeclaration': 133 | // TODO: esbuild (and friends?) _already_ transform these to `const ClassName = class { ... }`, 134 | // so in already bundled code you don't actually see this. So it's important but less immediate. 135 | if (e.liftClassDeclaration(part)) { 136 | ++stats.class; 137 | } else { 138 | ++stats._skip; 139 | } 140 | } 141 | } 142 | 143 | // follow top-level statements 144 | innerLift(e.block, []); 145 | 146 | return stats; 147 | } 148 | -------------------------------------------------------------------------------- /lib/name.ts: -------------------------------------------------------------------------------- 1 | const startOfTime = 1710925200000; // 2024-03-24 20:00 SYD time 2 | 3 | export function buildCorpusName(sourceName: string, now = new Date()) { 4 | const v = +now - startOfTime; 5 | if (v <= 0) { 6 | throw new Error(`in past`); 7 | } 8 | // it doesn't matter what base this is, or what number it is; later runs 'prefer' files sorted earlier 9 | const key = toBase62(v, 7); 10 | const suffix = `.kt-${key}.js`; 11 | 12 | const out = sourceName.replace(/\.js$/, suffix); 13 | if (!out.endsWith(suffix)) { 14 | throw new Error(`couldn't convert source name: ${sourceName}`); 15 | } 16 | return out; 17 | } 18 | 19 | export function toBase62(v: number, pad: number = 0) { 20 | const b62digit = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; 21 | let result = ''; 22 | while (v > 0) { 23 | result = b62digit[v % b62digit.length] + result; 24 | v = Math.floor(v / b62digit.length); 25 | } 26 | return result.padStart(pad, '0'); 27 | } 28 | -------------------------------------------------------------------------------- /lib/render.ts: -------------------------------------------------------------------------------- 1 | export function renderSkip( 2 | raw: string, 3 | skip: Iterable<{ start: number; end: number; replace?: string }>, 4 | ): string { 5 | const replaces = [...skip]; 6 | replaces.sort(({ start: a }, { start: b }) => a - b); 7 | 8 | let out = raw.substring(0, replaces.at(0)?.start); 9 | for (let i = 0; i < replaces.length; ++i) { 10 | if (replaces[i].replace !== undefined) { 11 | out += replaces[i].replace; 12 | } 13 | const part = raw.substring(replaces[i].end, replaces.at(i + 1)?.start); 14 | out += part; 15 | } 16 | return out; 17 | } 18 | 19 | export function renderOnly(raw: string, include: { start: number; end: number }[]) { 20 | include = [...include].sort(({ start: a }, { start: b }) => a - b); 21 | 22 | // we don't think there's a "final" statement to include a semicolon before; hack it in 23 | if (include.length) { 24 | include.push({ start: raw.length, end: raw.length + 1 }); 25 | } 26 | 27 | const holes: { start: number; end: number }[] = []; 28 | 29 | let lastPart = ''; 30 | let lastEnd = 0; 31 | const out = include 32 | .map(({ start, end }) => { 33 | const holeLength = start - lastEnd; 34 | let space = ''.padEnd(holeLength); 35 | 36 | if (!holeLength) { 37 | // zero padding (or start) 38 | lastEnd = end; 39 | } else { 40 | const hole = { start: lastEnd, end: start }; 41 | holes.push(hole); 42 | lastEnd = end; 43 | 44 | if (partNeedsSemi(lastPart)) { 45 | ++hole.start; 46 | space = ';'.padEnd(holeLength); 47 | } 48 | } 49 | 50 | const part = raw.substring(start, end); 51 | lastPart = part; 52 | return space + part; 53 | }) 54 | .join(''); 55 | 56 | return { out, holes }; 57 | } 58 | 59 | function partNeedsSemi(raw: string) { 60 | if (/^(class|function)\b/.test(raw)) { 61 | return false; 62 | } 63 | if (raw.endsWith(';')) { 64 | return false; 65 | } 66 | return true; 67 | } 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "author": "Sam Thorogood ", 4 | "repository": "git@github.com:samthor/kuto.git", 5 | "license": "Apache-2.0", 6 | "devDependencies": { 7 | "@types/node": "^20.11.28", 8 | "acorn": "^8.11.3", 9 | "esbuild": "^0.20.2", 10 | "esm-resolve": "^1.0.11", 11 | "tsx": "^4.7.1", 12 | "vite": "^5.2.6", 13 | "zx": "^7.2.3" 14 | }, 15 | "scripts": { 16 | "prepublishOnly": "bash release.sh", 17 | "test": "npx tsx test/index.ts" 18 | }, 19 | "bin": { 20 | "kuto": "./dist/app.js" 21 | }, 22 | "name": "kuto", 23 | "version": "0.3.7-beta.0" 24 | } 25 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '6.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | devDependencies: 8 | '@types/node': 9 | specifier: ^20.11.28 10 | version: 20.11.28 11 | acorn: 12 | specifier: ^8.11.3 13 | version: 8.11.3 14 | esbuild: 15 | specifier: ^0.20.2 16 | version: 0.20.2 17 | esm-resolve: 18 | specifier: ^1.0.11 19 | version: 1.0.11 20 | tsx: 21 | specifier: ^4.7.1 22 | version: 4.7.1 23 | vite: 24 | specifier: ^5.2.6 25 | version: 5.2.6(@types/node@20.11.28) 26 | zx: 27 | specifier: ^7.2.3 28 | version: 7.2.3 29 | 30 | packages: 31 | 32 | /@esbuild/aix-ppc64@0.19.12: 33 | resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==} 34 | engines: {node: '>=12'} 35 | cpu: [ppc64] 36 | os: [aix] 37 | requiresBuild: true 38 | dev: true 39 | optional: true 40 | 41 | /@esbuild/aix-ppc64@0.20.2: 42 | resolution: {integrity: sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==} 43 | engines: {node: '>=12'} 44 | cpu: [ppc64] 45 | os: [aix] 46 | requiresBuild: true 47 | dev: true 48 | optional: true 49 | 50 | /@esbuild/android-arm64@0.19.12: 51 | resolution: {integrity: sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==} 52 | engines: {node: '>=12'} 53 | cpu: [arm64] 54 | os: [android] 55 | requiresBuild: true 56 | dev: true 57 | optional: true 58 | 59 | /@esbuild/android-arm64@0.20.2: 60 | resolution: {integrity: sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==} 61 | engines: {node: '>=12'} 62 | cpu: [arm64] 63 | os: [android] 64 | requiresBuild: true 65 | dev: true 66 | optional: true 67 | 68 | /@esbuild/android-arm@0.19.12: 69 | resolution: {integrity: sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==} 70 | engines: {node: '>=12'} 71 | cpu: [arm] 72 | os: [android] 73 | requiresBuild: true 74 | dev: true 75 | optional: true 76 | 77 | /@esbuild/android-arm@0.20.2: 78 | resolution: {integrity: sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==} 79 | engines: {node: '>=12'} 80 | cpu: [arm] 81 | os: [android] 82 | requiresBuild: true 83 | dev: true 84 | optional: true 85 | 86 | /@esbuild/android-x64@0.19.12: 87 | resolution: {integrity: sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==} 88 | engines: {node: '>=12'} 89 | cpu: [x64] 90 | os: [android] 91 | requiresBuild: true 92 | dev: true 93 | optional: true 94 | 95 | /@esbuild/android-x64@0.20.2: 96 | resolution: {integrity: sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==} 97 | engines: {node: '>=12'} 98 | cpu: [x64] 99 | os: [android] 100 | requiresBuild: true 101 | dev: true 102 | optional: true 103 | 104 | /@esbuild/darwin-arm64@0.19.12: 105 | resolution: {integrity: sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==} 106 | engines: {node: '>=12'} 107 | cpu: [arm64] 108 | os: [darwin] 109 | requiresBuild: true 110 | dev: true 111 | optional: true 112 | 113 | /@esbuild/darwin-arm64@0.20.2: 114 | resolution: {integrity: sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==} 115 | engines: {node: '>=12'} 116 | cpu: [arm64] 117 | os: [darwin] 118 | requiresBuild: true 119 | dev: true 120 | optional: true 121 | 122 | /@esbuild/darwin-x64@0.19.12: 123 | resolution: {integrity: sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==} 124 | engines: {node: '>=12'} 125 | cpu: [x64] 126 | os: [darwin] 127 | requiresBuild: true 128 | dev: true 129 | optional: true 130 | 131 | /@esbuild/darwin-x64@0.20.2: 132 | resolution: {integrity: sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==} 133 | engines: {node: '>=12'} 134 | cpu: [x64] 135 | os: [darwin] 136 | requiresBuild: true 137 | dev: true 138 | optional: true 139 | 140 | /@esbuild/freebsd-arm64@0.19.12: 141 | resolution: {integrity: sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==} 142 | engines: {node: '>=12'} 143 | cpu: [arm64] 144 | os: [freebsd] 145 | requiresBuild: true 146 | dev: true 147 | optional: true 148 | 149 | /@esbuild/freebsd-arm64@0.20.2: 150 | resolution: {integrity: sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==} 151 | engines: {node: '>=12'} 152 | cpu: [arm64] 153 | os: [freebsd] 154 | requiresBuild: true 155 | dev: true 156 | optional: true 157 | 158 | /@esbuild/freebsd-x64@0.19.12: 159 | resolution: {integrity: sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==} 160 | engines: {node: '>=12'} 161 | cpu: [x64] 162 | os: [freebsd] 163 | requiresBuild: true 164 | dev: true 165 | optional: true 166 | 167 | /@esbuild/freebsd-x64@0.20.2: 168 | resolution: {integrity: sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==} 169 | engines: {node: '>=12'} 170 | cpu: [x64] 171 | os: [freebsd] 172 | requiresBuild: true 173 | dev: true 174 | optional: true 175 | 176 | /@esbuild/linux-arm64@0.19.12: 177 | resolution: {integrity: sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==} 178 | engines: {node: '>=12'} 179 | cpu: [arm64] 180 | os: [linux] 181 | requiresBuild: true 182 | dev: true 183 | optional: true 184 | 185 | /@esbuild/linux-arm64@0.20.2: 186 | resolution: {integrity: sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==} 187 | engines: {node: '>=12'} 188 | cpu: [arm64] 189 | os: [linux] 190 | requiresBuild: true 191 | dev: true 192 | optional: true 193 | 194 | /@esbuild/linux-arm@0.19.12: 195 | resolution: {integrity: sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==} 196 | engines: {node: '>=12'} 197 | cpu: [arm] 198 | os: [linux] 199 | requiresBuild: true 200 | dev: true 201 | optional: true 202 | 203 | /@esbuild/linux-arm@0.20.2: 204 | resolution: {integrity: sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==} 205 | engines: {node: '>=12'} 206 | cpu: [arm] 207 | os: [linux] 208 | requiresBuild: true 209 | dev: true 210 | optional: true 211 | 212 | /@esbuild/linux-ia32@0.19.12: 213 | resolution: {integrity: sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==} 214 | engines: {node: '>=12'} 215 | cpu: [ia32] 216 | os: [linux] 217 | requiresBuild: true 218 | dev: true 219 | optional: true 220 | 221 | /@esbuild/linux-ia32@0.20.2: 222 | resolution: {integrity: sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==} 223 | engines: {node: '>=12'} 224 | cpu: [ia32] 225 | os: [linux] 226 | requiresBuild: true 227 | dev: true 228 | optional: true 229 | 230 | /@esbuild/linux-loong64@0.19.12: 231 | resolution: {integrity: sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==} 232 | engines: {node: '>=12'} 233 | cpu: [loong64] 234 | os: [linux] 235 | requiresBuild: true 236 | dev: true 237 | optional: true 238 | 239 | /@esbuild/linux-loong64@0.20.2: 240 | resolution: {integrity: sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==} 241 | engines: {node: '>=12'} 242 | cpu: [loong64] 243 | os: [linux] 244 | requiresBuild: true 245 | dev: true 246 | optional: true 247 | 248 | /@esbuild/linux-mips64el@0.19.12: 249 | resolution: {integrity: sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==} 250 | engines: {node: '>=12'} 251 | cpu: [mips64el] 252 | os: [linux] 253 | requiresBuild: true 254 | dev: true 255 | optional: true 256 | 257 | /@esbuild/linux-mips64el@0.20.2: 258 | resolution: {integrity: sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==} 259 | engines: {node: '>=12'} 260 | cpu: [mips64el] 261 | os: [linux] 262 | requiresBuild: true 263 | dev: true 264 | optional: true 265 | 266 | /@esbuild/linux-ppc64@0.19.12: 267 | resolution: {integrity: sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==} 268 | engines: {node: '>=12'} 269 | cpu: [ppc64] 270 | os: [linux] 271 | requiresBuild: true 272 | dev: true 273 | optional: true 274 | 275 | /@esbuild/linux-ppc64@0.20.2: 276 | resolution: {integrity: sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==} 277 | engines: {node: '>=12'} 278 | cpu: [ppc64] 279 | os: [linux] 280 | requiresBuild: true 281 | dev: true 282 | optional: true 283 | 284 | /@esbuild/linux-riscv64@0.19.12: 285 | resolution: {integrity: sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==} 286 | engines: {node: '>=12'} 287 | cpu: [riscv64] 288 | os: [linux] 289 | requiresBuild: true 290 | dev: true 291 | optional: true 292 | 293 | /@esbuild/linux-riscv64@0.20.2: 294 | resolution: {integrity: sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==} 295 | engines: {node: '>=12'} 296 | cpu: [riscv64] 297 | os: [linux] 298 | requiresBuild: true 299 | dev: true 300 | optional: true 301 | 302 | /@esbuild/linux-s390x@0.19.12: 303 | resolution: {integrity: sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==} 304 | engines: {node: '>=12'} 305 | cpu: [s390x] 306 | os: [linux] 307 | requiresBuild: true 308 | dev: true 309 | optional: true 310 | 311 | /@esbuild/linux-s390x@0.20.2: 312 | resolution: {integrity: sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==} 313 | engines: {node: '>=12'} 314 | cpu: [s390x] 315 | os: [linux] 316 | requiresBuild: true 317 | dev: true 318 | optional: true 319 | 320 | /@esbuild/linux-x64@0.19.12: 321 | resolution: {integrity: sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==} 322 | engines: {node: '>=12'} 323 | cpu: [x64] 324 | os: [linux] 325 | requiresBuild: true 326 | dev: true 327 | optional: true 328 | 329 | /@esbuild/linux-x64@0.20.2: 330 | resolution: {integrity: sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==} 331 | engines: {node: '>=12'} 332 | cpu: [x64] 333 | os: [linux] 334 | requiresBuild: true 335 | dev: true 336 | optional: true 337 | 338 | /@esbuild/netbsd-x64@0.19.12: 339 | resolution: {integrity: sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==} 340 | engines: {node: '>=12'} 341 | cpu: [x64] 342 | os: [netbsd] 343 | requiresBuild: true 344 | dev: true 345 | optional: true 346 | 347 | /@esbuild/netbsd-x64@0.20.2: 348 | resolution: {integrity: sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==} 349 | engines: {node: '>=12'} 350 | cpu: [x64] 351 | os: [netbsd] 352 | requiresBuild: true 353 | dev: true 354 | optional: true 355 | 356 | /@esbuild/openbsd-x64@0.19.12: 357 | resolution: {integrity: sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==} 358 | engines: {node: '>=12'} 359 | cpu: [x64] 360 | os: [openbsd] 361 | requiresBuild: true 362 | dev: true 363 | optional: true 364 | 365 | /@esbuild/openbsd-x64@0.20.2: 366 | resolution: {integrity: sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==} 367 | engines: {node: '>=12'} 368 | cpu: [x64] 369 | os: [openbsd] 370 | requiresBuild: true 371 | dev: true 372 | optional: true 373 | 374 | /@esbuild/sunos-x64@0.19.12: 375 | resolution: {integrity: sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==} 376 | engines: {node: '>=12'} 377 | cpu: [x64] 378 | os: [sunos] 379 | requiresBuild: true 380 | dev: true 381 | optional: true 382 | 383 | /@esbuild/sunos-x64@0.20.2: 384 | resolution: {integrity: sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==} 385 | engines: {node: '>=12'} 386 | cpu: [x64] 387 | os: [sunos] 388 | requiresBuild: true 389 | dev: true 390 | optional: true 391 | 392 | /@esbuild/win32-arm64@0.19.12: 393 | resolution: {integrity: sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==} 394 | engines: {node: '>=12'} 395 | cpu: [arm64] 396 | os: [win32] 397 | requiresBuild: true 398 | dev: true 399 | optional: true 400 | 401 | /@esbuild/win32-arm64@0.20.2: 402 | resolution: {integrity: sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==} 403 | engines: {node: '>=12'} 404 | cpu: [arm64] 405 | os: [win32] 406 | requiresBuild: true 407 | dev: true 408 | optional: true 409 | 410 | /@esbuild/win32-ia32@0.19.12: 411 | resolution: {integrity: sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==} 412 | engines: {node: '>=12'} 413 | cpu: [ia32] 414 | os: [win32] 415 | requiresBuild: true 416 | dev: true 417 | optional: true 418 | 419 | /@esbuild/win32-ia32@0.20.2: 420 | resolution: {integrity: sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==} 421 | engines: {node: '>=12'} 422 | cpu: [ia32] 423 | os: [win32] 424 | requiresBuild: true 425 | dev: true 426 | optional: true 427 | 428 | /@esbuild/win32-x64@0.19.12: 429 | resolution: {integrity: sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==} 430 | engines: {node: '>=12'} 431 | cpu: [x64] 432 | os: [win32] 433 | requiresBuild: true 434 | dev: true 435 | optional: true 436 | 437 | /@esbuild/win32-x64@0.20.2: 438 | resolution: {integrity: sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==} 439 | engines: {node: '>=12'} 440 | cpu: [x64] 441 | os: [win32] 442 | requiresBuild: true 443 | dev: true 444 | optional: true 445 | 446 | /@nodelib/fs.scandir@2.1.5: 447 | resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} 448 | engines: {node: '>= 8'} 449 | dependencies: 450 | '@nodelib/fs.stat': 2.0.5 451 | run-parallel: 1.2.0 452 | dev: true 453 | 454 | /@nodelib/fs.stat@2.0.5: 455 | resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} 456 | engines: {node: '>= 8'} 457 | dev: true 458 | 459 | /@nodelib/fs.walk@1.2.8: 460 | resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} 461 | engines: {node: '>= 8'} 462 | dependencies: 463 | '@nodelib/fs.scandir': 2.1.5 464 | fastq: 1.17.1 465 | dev: true 466 | 467 | /@rollup/rollup-android-arm-eabi@4.13.1: 468 | resolution: {integrity: sha512-4C4UERETjXpC4WpBXDbkgNVgHyWfG3B/NKY46e7w5H134UDOFqUJKpsLm0UYmuupW+aJmRgeScrDNfvZ5WV80A==} 469 | cpu: [arm] 470 | os: [android] 471 | requiresBuild: true 472 | dev: true 473 | optional: true 474 | 475 | /@rollup/rollup-android-arm64@4.13.1: 476 | resolution: {integrity: sha512-TrTaFJ9pXgfXEiJKQ3yQRelpQFqgRzVR9it8DbeRzG0RX7mKUy0bqhCFsgevwXLJepQKTnLl95TnPGf9T9AMOA==} 477 | cpu: [arm64] 478 | os: [android] 479 | requiresBuild: true 480 | dev: true 481 | optional: true 482 | 483 | /@rollup/rollup-darwin-arm64@4.13.1: 484 | resolution: {integrity: sha512-fz7jN6ahTI3cKzDO2otQuybts5cyu0feymg0bjvYCBrZQ8tSgE8pc0sSNEuGvifrQJWiwx9F05BowihmLxeQKw==} 485 | cpu: [arm64] 486 | os: [darwin] 487 | requiresBuild: true 488 | dev: true 489 | optional: true 490 | 491 | /@rollup/rollup-darwin-x64@4.13.1: 492 | resolution: {integrity: sha512-WTvdz7SLMlJpektdrnWRUN9C0N2qNHwNbWpNo0a3Tod3gb9leX+yrYdCeB7VV36OtoyiPAivl7/xZ3G1z5h20g==} 493 | cpu: [x64] 494 | os: [darwin] 495 | requiresBuild: true 496 | dev: true 497 | optional: true 498 | 499 | /@rollup/rollup-linux-arm-gnueabihf@4.13.1: 500 | resolution: {integrity: sha512-dBHQl+7wZzBYcIF6o4k2XkAfwP2ks1mYW2q/Gzv9n39uDcDiAGDqEyml08OdY0BIct0yLSPkDTqn4i6czpBLLw==} 501 | cpu: [arm] 502 | os: [linux] 503 | requiresBuild: true 504 | dev: true 505 | optional: true 506 | 507 | /@rollup/rollup-linux-arm64-gnu@4.13.1: 508 | resolution: {integrity: sha512-bur4JOxvYxfrAmocRJIW0SADs3QdEYK6TQ7dTNz6Z4/lySeu3Z1H/+tl0a4qDYv0bCdBpUYM0sYa/X+9ZqgfSQ==} 509 | cpu: [arm64] 510 | os: [linux] 511 | requiresBuild: true 512 | dev: true 513 | optional: true 514 | 515 | /@rollup/rollup-linux-arm64-musl@4.13.1: 516 | resolution: {integrity: sha512-ssp77SjcDIUSoUyj7DU7/5iwM4ZEluY+N8umtCT9nBRs3u045t0KkW02LTyHouHDomnMXaXSZcCSr2bdMK63kA==} 517 | cpu: [arm64] 518 | os: [linux] 519 | requiresBuild: true 520 | dev: true 521 | optional: true 522 | 523 | /@rollup/rollup-linux-riscv64-gnu@4.13.1: 524 | resolution: {integrity: sha512-Jv1DkIvwEPAb+v25/Unrnnq9BO3F5cbFPT821n3S5litkz+O5NuXuNhqtPx5KtcwOTtaqkTsO+IVzJOsxd11aQ==} 525 | cpu: [riscv64] 526 | os: [linux] 527 | requiresBuild: true 528 | dev: true 529 | optional: true 530 | 531 | /@rollup/rollup-linux-s390x-gnu@4.13.1: 532 | resolution: {integrity: sha512-U564BrhEfaNChdATQaEODtquCC7Ez+8Hxz1h5MAdMYj0AqD0GA9rHCpElajb/sQcaFL6NXmHc5O+7FXpWMa73Q==} 533 | cpu: [s390x] 534 | os: [linux] 535 | requiresBuild: true 536 | dev: true 537 | optional: true 538 | 539 | /@rollup/rollup-linux-x64-gnu@4.13.1: 540 | resolution: {integrity: sha512-zGRDulLTeDemR8DFYyFIQ8kMP02xpUsX4IBikc7lwL9PrwR3gWmX2NopqiGlI2ZVWMl15qZeUjumTwpv18N7sQ==} 541 | cpu: [x64] 542 | os: [linux] 543 | requiresBuild: true 544 | dev: true 545 | optional: true 546 | 547 | /@rollup/rollup-linux-x64-musl@4.13.1: 548 | resolution: {integrity: sha512-VTk/MveyPdMFkYJJPCkYBw07KcTkGU2hLEyqYMsU4NjiOfzoaDTW9PWGRsNwiOA3qI0k/JQPjkl/4FCK1smskQ==} 549 | cpu: [x64] 550 | os: [linux] 551 | requiresBuild: true 552 | dev: true 553 | optional: true 554 | 555 | /@rollup/rollup-win32-arm64-msvc@4.13.1: 556 | resolution: {integrity: sha512-L+hX8Dtibb02r/OYCsp4sQQIi3ldZkFI0EUkMTDwRfFykXBPptoz/tuuGqEd3bThBSLRWPR6wsixDSgOx/U3Zw==} 557 | cpu: [arm64] 558 | os: [win32] 559 | requiresBuild: true 560 | dev: true 561 | optional: true 562 | 563 | /@rollup/rollup-win32-ia32-msvc@4.13.1: 564 | resolution: {integrity: sha512-+dI2jVPfM5A8zme8riEoNC7UKk0Lzc7jCj/U89cQIrOjrZTCWZl/+IXUeRT2rEZ5j25lnSA9G9H1Ob9azaF/KQ==} 565 | cpu: [ia32] 566 | os: [win32] 567 | requiresBuild: true 568 | dev: true 569 | optional: true 570 | 571 | /@rollup/rollup-win32-x64-msvc@4.13.1: 572 | resolution: {integrity: sha512-YY1Exxo2viZ/O2dMHuwQvimJ0SqvL+OAWQLLY6rvXavgQKjhQUzn7nc1Dd29gjB5Fqi00nrBWctJBOyfVMIVxw==} 573 | cpu: [x64] 574 | os: [win32] 575 | requiresBuild: true 576 | dev: true 577 | optional: true 578 | 579 | /@types/estree@1.0.5: 580 | resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} 581 | dev: true 582 | 583 | /@types/fs-extra@11.0.4: 584 | resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==} 585 | dependencies: 586 | '@types/jsonfile': 6.1.4 587 | '@types/node': 20.11.28 588 | dev: true 589 | 590 | /@types/jsonfile@6.1.4: 591 | resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==} 592 | dependencies: 593 | '@types/node': 20.11.28 594 | dev: true 595 | 596 | /@types/minimist@1.2.5: 597 | resolution: {integrity: sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==} 598 | dev: true 599 | 600 | /@types/node@18.19.26: 601 | resolution: {integrity: sha512-+wiMJsIwLOYCvUqSdKTrfkS8mpTp+MPINe6+Np4TAGFWWRWiBQ5kSq9nZGCSPkzx9mvT+uEukzpX4MOSCydcvw==} 602 | dependencies: 603 | undici-types: 5.26.5 604 | dev: true 605 | 606 | /@types/node@20.11.28: 607 | resolution: {integrity: sha512-M/GPWVS2wLkSkNHVeLkrF2fD5Lx5UC4PxA0uZcKc6QqbIQUJyW1jVjueJYi1z8n0I5PxYrtpnPnWglE+y9A0KA==} 608 | dependencies: 609 | undici-types: 5.26.5 610 | dev: true 611 | 612 | /@types/ps-tree@1.1.6: 613 | resolution: {integrity: sha512-PtrlVaOaI44/3pl3cvnlK+GxOM3re2526TJvPvh7W+keHIXdV4TE0ylpPBAcvFQCbGitaTXwL9u+RF7qtVeazQ==} 614 | dev: true 615 | 616 | /@types/which@3.0.3: 617 | resolution: {integrity: sha512-2C1+XoY0huExTbs8MQv1DuS5FS86+SEjdM9F/+GS61gg5Hqbtj8ZiDSx8MfWcyei907fIPbfPGCOrNUTnVHY1g==} 618 | dev: true 619 | 620 | /acorn@8.11.3: 621 | resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==} 622 | engines: {node: '>=0.4.0'} 623 | hasBin: true 624 | dev: true 625 | 626 | /braces@3.0.2: 627 | resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} 628 | engines: {node: '>=8'} 629 | dependencies: 630 | fill-range: 7.0.1 631 | dev: true 632 | 633 | /chalk@5.3.0: 634 | resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} 635 | engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} 636 | dev: true 637 | 638 | /data-uri-to-buffer@4.0.1: 639 | resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} 640 | engines: {node: '>= 12'} 641 | dev: true 642 | 643 | /dir-glob@3.0.1: 644 | resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} 645 | engines: {node: '>=8'} 646 | dependencies: 647 | path-type: 4.0.0 648 | dev: true 649 | 650 | /duplexer@0.1.2: 651 | resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} 652 | dev: true 653 | 654 | /esbuild@0.19.12: 655 | resolution: {integrity: sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==} 656 | engines: {node: '>=12'} 657 | hasBin: true 658 | requiresBuild: true 659 | optionalDependencies: 660 | '@esbuild/aix-ppc64': 0.19.12 661 | '@esbuild/android-arm': 0.19.12 662 | '@esbuild/android-arm64': 0.19.12 663 | '@esbuild/android-x64': 0.19.12 664 | '@esbuild/darwin-arm64': 0.19.12 665 | '@esbuild/darwin-x64': 0.19.12 666 | '@esbuild/freebsd-arm64': 0.19.12 667 | '@esbuild/freebsd-x64': 0.19.12 668 | '@esbuild/linux-arm': 0.19.12 669 | '@esbuild/linux-arm64': 0.19.12 670 | '@esbuild/linux-ia32': 0.19.12 671 | '@esbuild/linux-loong64': 0.19.12 672 | '@esbuild/linux-mips64el': 0.19.12 673 | '@esbuild/linux-ppc64': 0.19.12 674 | '@esbuild/linux-riscv64': 0.19.12 675 | '@esbuild/linux-s390x': 0.19.12 676 | '@esbuild/linux-x64': 0.19.12 677 | '@esbuild/netbsd-x64': 0.19.12 678 | '@esbuild/openbsd-x64': 0.19.12 679 | '@esbuild/sunos-x64': 0.19.12 680 | '@esbuild/win32-arm64': 0.19.12 681 | '@esbuild/win32-ia32': 0.19.12 682 | '@esbuild/win32-x64': 0.19.12 683 | dev: true 684 | 685 | /esbuild@0.20.2: 686 | resolution: {integrity: sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==} 687 | engines: {node: '>=12'} 688 | hasBin: true 689 | requiresBuild: true 690 | optionalDependencies: 691 | '@esbuild/aix-ppc64': 0.20.2 692 | '@esbuild/android-arm': 0.20.2 693 | '@esbuild/android-arm64': 0.20.2 694 | '@esbuild/android-x64': 0.20.2 695 | '@esbuild/darwin-arm64': 0.20.2 696 | '@esbuild/darwin-x64': 0.20.2 697 | '@esbuild/freebsd-arm64': 0.20.2 698 | '@esbuild/freebsd-x64': 0.20.2 699 | '@esbuild/linux-arm': 0.20.2 700 | '@esbuild/linux-arm64': 0.20.2 701 | '@esbuild/linux-ia32': 0.20.2 702 | '@esbuild/linux-loong64': 0.20.2 703 | '@esbuild/linux-mips64el': 0.20.2 704 | '@esbuild/linux-ppc64': 0.20.2 705 | '@esbuild/linux-riscv64': 0.20.2 706 | '@esbuild/linux-s390x': 0.20.2 707 | '@esbuild/linux-x64': 0.20.2 708 | '@esbuild/netbsd-x64': 0.20.2 709 | '@esbuild/openbsd-x64': 0.20.2 710 | '@esbuild/sunos-x64': 0.20.2 711 | '@esbuild/win32-arm64': 0.20.2 712 | '@esbuild/win32-ia32': 0.20.2 713 | '@esbuild/win32-x64': 0.20.2 714 | dev: true 715 | 716 | /esm-resolve@1.0.11: 717 | resolution: {integrity: sha512-LxF0wfUQm3ldUDHkkV2MIbvvY0TgzIpJ420jHSV1Dm+IlplBEWiJTKWM61GtxUfvjV6iD4OtTYFGAGM2uuIUWg==} 718 | dev: true 719 | 720 | /event-stream@3.3.4: 721 | resolution: {integrity: sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==} 722 | dependencies: 723 | duplexer: 0.1.2 724 | from: 0.1.7 725 | map-stream: 0.1.0 726 | pause-stream: 0.0.11 727 | split: 0.3.3 728 | stream-combiner: 0.0.4 729 | through: 2.3.8 730 | dev: true 731 | 732 | /fast-glob@3.3.2: 733 | resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} 734 | engines: {node: '>=8.6.0'} 735 | dependencies: 736 | '@nodelib/fs.stat': 2.0.5 737 | '@nodelib/fs.walk': 1.2.8 738 | glob-parent: 5.1.2 739 | merge2: 1.4.1 740 | micromatch: 4.0.5 741 | dev: true 742 | 743 | /fastq@1.17.1: 744 | resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} 745 | dependencies: 746 | reusify: 1.0.4 747 | dev: true 748 | 749 | /fetch-blob@3.2.0: 750 | resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} 751 | engines: {node: ^12.20 || >= 14.13} 752 | dependencies: 753 | node-domexception: 1.0.0 754 | web-streams-polyfill: 3.3.3 755 | dev: true 756 | 757 | /fill-range@7.0.1: 758 | resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} 759 | engines: {node: '>=8'} 760 | dependencies: 761 | to-regex-range: 5.0.1 762 | dev: true 763 | 764 | /formdata-polyfill@4.0.10: 765 | resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} 766 | engines: {node: '>=12.20.0'} 767 | dependencies: 768 | fetch-blob: 3.2.0 769 | dev: true 770 | 771 | /from@0.1.7: 772 | resolution: {integrity: sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==} 773 | dev: true 774 | 775 | /fs-extra@11.2.0: 776 | resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==} 777 | engines: {node: '>=14.14'} 778 | dependencies: 779 | graceful-fs: 4.2.11 780 | jsonfile: 6.1.0 781 | universalify: 2.0.1 782 | dev: true 783 | 784 | /fsevents@2.3.3: 785 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 786 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 787 | os: [darwin] 788 | requiresBuild: true 789 | dev: true 790 | optional: true 791 | 792 | /fx@33.0.0: 793 | resolution: {integrity: sha512-uW/UAi9G04+o7dD/RyIH7mP9Cyf12TdiaWQ19QbvnxkKQ2yiffXiZMz65zqbWMstLd2vwla++G9lMabG3nXxYQ==} 794 | hasBin: true 795 | dev: true 796 | 797 | /get-tsconfig@4.7.3: 798 | resolution: {integrity: sha512-ZvkrzoUA0PQZM6fy6+/Hce561s+faD1rsNwhnO5FelNjyy7EMGJ3Rz1AQ8GYDWjhRs/7dBLOEJvhK8MiEJOAFg==} 799 | dependencies: 800 | resolve-pkg-maps: 1.0.0 801 | dev: true 802 | 803 | /glob-parent@5.1.2: 804 | resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} 805 | engines: {node: '>= 6'} 806 | dependencies: 807 | is-glob: 4.0.3 808 | dev: true 809 | 810 | /globby@13.2.2: 811 | resolution: {integrity: sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==} 812 | engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} 813 | dependencies: 814 | dir-glob: 3.0.1 815 | fast-glob: 3.3.2 816 | ignore: 5.3.1 817 | merge2: 1.4.1 818 | slash: 4.0.0 819 | dev: true 820 | 821 | /graceful-fs@4.2.11: 822 | resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} 823 | dev: true 824 | 825 | /ignore@5.3.1: 826 | resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} 827 | engines: {node: '>= 4'} 828 | dev: true 829 | 830 | /is-extglob@2.1.1: 831 | resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} 832 | engines: {node: '>=0.10.0'} 833 | dev: true 834 | 835 | /is-glob@4.0.3: 836 | resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} 837 | engines: {node: '>=0.10.0'} 838 | dependencies: 839 | is-extglob: 2.1.1 840 | dev: true 841 | 842 | /is-number@7.0.0: 843 | resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} 844 | engines: {node: '>=0.12.0'} 845 | dev: true 846 | 847 | /isexe@2.0.0: 848 | resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} 849 | dev: true 850 | 851 | /jsonfile@6.1.0: 852 | resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} 853 | dependencies: 854 | universalify: 2.0.1 855 | optionalDependencies: 856 | graceful-fs: 4.2.11 857 | dev: true 858 | 859 | /map-stream@0.1.0: 860 | resolution: {integrity: sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==} 861 | dev: true 862 | 863 | /merge2@1.4.1: 864 | resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} 865 | engines: {node: '>= 8'} 866 | dev: true 867 | 868 | /micromatch@4.0.5: 869 | resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} 870 | engines: {node: '>=8.6'} 871 | dependencies: 872 | braces: 3.0.2 873 | picomatch: 2.3.1 874 | dev: true 875 | 876 | /minimist@1.2.8: 877 | resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} 878 | dev: true 879 | 880 | /nanoid@3.3.7: 881 | resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} 882 | engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 883 | hasBin: true 884 | dev: true 885 | 886 | /node-domexception@1.0.0: 887 | resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} 888 | engines: {node: '>=10.5.0'} 889 | dev: true 890 | 891 | /node-fetch@3.3.1: 892 | resolution: {integrity: sha512-cRVc/kyto/7E5shrWca1Wsea4y6tL9iYJE5FBCius3JQfb/4P4I295PfhgbJQBLTx6lATE4z+wK0rPM4VS2uow==} 893 | engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} 894 | dependencies: 895 | data-uri-to-buffer: 4.0.1 896 | fetch-blob: 3.2.0 897 | formdata-polyfill: 4.0.10 898 | dev: true 899 | 900 | /path-type@4.0.0: 901 | resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} 902 | engines: {node: '>=8'} 903 | dev: true 904 | 905 | /pause-stream@0.0.11: 906 | resolution: {integrity: sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==} 907 | dependencies: 908 | through: 2.3.8 909 | dev: true 910 | 911 | /picocolors@1.0.0: 912 | resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} 913 | dev: true 914 | 915 | /picomatch@2.3.1: 916 | resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} 917 | engines: {node: '>=8.6'} 918 | dev: true 919 | 920 | /postcss@8.4.38: 921 | resolution: {integrity: sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==} 922 | engines: {node: ^10 || ^12 || >=14} 923 | dependencies: 924 | nanoid: 3.3.7 925 | picocolors: 1.0.0 926 | source-map-js: 1.2.0 927 | dev: true 928 | 929 | /ps-tree@1.2.0: 930 | resolution: {integrity: sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA==} 931 | engines: {node: '>= 0.10'} 932 | hasBin: true 933 | dependencies: 934 | event-stream: 3.3.4 935 | dev: true 936 | 937 | /queue-microtask@1.2.3: 938 | resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} 939 | dev: true 940 | 941 | /resolve-pkg-maps@1.0.0: 942 | resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} 943 | dev: true 944 | 945 | /reusify@1.0.4: 946 | resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} 947 | engines: {iojs: '>=1.0.0', node: '>=0.10.0'} 948 | dev: true 949 | 950 | /rollup@4.13.1: 951 | resolution: {integrity: sha512-hFi+fU132IvJ2ZuihN56dwgpltpmLZHZWsx27rMCTZ2sYwrqlgL5sECGy1eeV2lAihD8EzChBVVhsXci0wD4Tg==} 952 | engines: {node: '>=18.0.0', npm: '>=8.0.0'} 953 | hasBin: true 954 | dependencies: 955 | '@types/estree': 1.0.5 956 | optionalDependencies: 957 | '@rollup/rollup-android-arm-eabi': 4.13.1 958 | '@rollup/rollup-android-arm64': 4.13.1 959 | '@rollup/rollup-darwin-arm64': 4.13.1 960 | '@rollup/rollup-darwin-x64': 4.13.1 961 | '@rollup/rollup-linux-arm-gnueabihf': 4.13.1 962 | '@rollup/rollup-linux-arm64-gnu': 4.13.1 963 | '@rollup/rollup-linux-arm64-musl': 4.13.1 964 | '@rollup/rollup-linux-riscv64-gnu': 4.13.1 965 | '@rollup/rollup-linux-s390x-gnu': 4.13.1 966 | '@rollup/rollup-linux-x64-gnu': 4.13.1 967 | '@rollup/rollup-linux-x64-musl': 4.13.1 968 | '@rollup/rollup-win32-arm64-msvc': 4.13.1 969 | '@rollup/rollup-win32-ia32-msvc': 4.13.1 970 | '@rollup/rollup-win32-x64-msvc': 4.13.1 971 | fsevents: 2.3.3 972 | dev: true 973 | 974 | /run-parallel@1.2.0: 975 | resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} 976 | dependencies: 977 | queue-microtask: 1.2.3 978 | dev: true 979 | 980 | /slash@4.0.0: 981 | resolution: {integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==} 982 | engines: {node: '>=12'} 983 | dev: true 984 | 985 | /source-map-js@1.2.0: 986 | resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} 987 | engines: {node: '>=0.10.0'} 988 | dev: true 989 | 990 | /split@0.3.3: 991 | resolution: {integrity: sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA==} 992 | dependencies: 993 | through: 2.3.8 994 | dev: true 995 | 996 | /stream-combiner@0.0.4: 997 | resolution: {integrity: sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==} 998 | dependencies: 999 | duplexer: 0.1.2 1000 | dev: true 1001 | 1002 | /through@2.3.8: 1003 | resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} 1004 | dev: true 1005 | 1006 | /to-regex-range@5.0.1: 1007 | resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} 1008 | engines: {node: '>=8.0'} 1009 | dependencies: 1010 | is-number: 7.0.0 1011 | dev: true 1012 | 1013 | /tsx@4.7.1: 1014 | resolution: {integrity: sha512-8d6VuibXHtlN5E3zFkgY8u4DX7Y3Z27zvvPKVmLon/D4AjuKzarkUBTLDBgj9iTQ0hg5xM7c/mYiRVM+HETf0g==} 1015 | engines: {node: '>=18.0.0'} 1016 | hasBin: true 1017 | dependencies: 1018 | esbuild: 0.19.12 1019 | get-tsconfig: 4.7.3 1020 | optionalDependencies: 1021 | fsevents: 2.3.3 1022 | dev: true 1023 | 1024 | /undici-types@5.26.5: 1025 | resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} 1026 | dev: true 1027 | 1028 | /universalify@2.0.1: 1029 | resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} 1030 | engines: {node: '>= 10.0.0'} 1031 | dev: true 1032 | 1033 | /vite@5.2.6(@types/node@20.11.28): 1034 | resolution: {integrity: sha512-FPtnxFlSIKYjZ2eosBQamz4CbyrTizbZ3hnGJlh/wMtCrlp1Hah6AzBLjGI5I2urTfNnpovpHdrL6YRuBOPnCA==} 1035 | engines: {node: ^18.0.0 || >=20.0.0} 1036 | hasBin: true 1037 | peerDependencies: 1038 | '@types/node': ^18.0.0 || >=20.0.0 1039 | less: '*' 1040 | lightningcss: ^1.21.0 1041 | sass: '*' 1042 | stylus: '*' 1043 | sugarss: '*' 1044 | terser: ^5.4.0 1045 | peerDependenciesMeta: 1046 | '@types/node': 1047 | optional: true 1048 | less: 1049 | optional: true 1050 | lightningcss: 1051 | optional: true 1052 | sass: 1053 | optional: true 1054 | stylus: 1055 | optional: true 1056 | sugarss: 1057 | optional: true 1058 | terser: 1059 | optional: true 1060 | dependencies: 1061 | '@types/node': 20.11.28 1062 | esbuild: 0.20.2 1063 | postcss: 8.4.38 1064 | rollup: 4.13.1 1065 | optionalDependencies: 1066 | fsevents: 2.3.3 1067 | dev: true 1068 | 1069 | /web-streams-polyfill@3.3.3: 1070 | resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} 1071 | engines: {node: '>= 8'} 1072 | dev: true 1073 | 1074 | /webpod@0.0.2: 1075 | resolution: {integrity: sha512-cSwwQIeg8v4i3p4ajHhwgR7N6VyxAf+KYSSsY6Pd3aETE+xEU4vbitz7qQkB0I321xnhDdgtxuiSfk5r/FVtjg==} 1076 | hasBin: true 1077 | dev: true 1078 | 1079 | /which@3.0.1: 1080 | resolution: {integrity: sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==} 1081 | engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} 1082 | hasBin: true 1083 | dependencies: 1084 | isexe: 2.0.0 1085 | dev: true 1086 | 1087 | /yaml@2.4.1: 1088 | resolution: {integrity: sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==} 1089 | engines: {node: '>= 14'} 1090 | hasBin: true 1091 | dev: true 1092 | 1093 | /zx@7.2.3: 1094 | resolution: {integrity: sha512-QODu38nLlYXg/B/Gw7ZKiZrvPkEsjPN3LQ5JFXM7h0JvwhEdPNNl+4Ao1y4+o3CLNiDUNcwzQYZ4/Ko7kKzCMA==} 1095 | engines: {node: '>= 16.0.0'} 1096 | hasBin: true 1097 | dependencies: 1098 | '@types/fs-extra': 11.0.4 1099 | '@types/minimist': 1.2.5 1100 | '@types/node': 18.19.26 1101 | '@types/ps-tree': 1.1.6 1102 | '@types/which': 3.0.3 1103 | chalk: 5.3.0 1104 | fs-extra: 11.2.0 1105 | fx: 33.0.0 1106 | globby: 13.2.2 1107 | minimist: 1.2.8 1108 | node-fetch: 3.3.1 1109 | ps-tree: 1.2.0 1110 | webpod: 0.0.2 1111 | which: 3.0.1 1112 | yaml: 2.4.1 1113 | dev: true 1114 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eu 4 | 5 | TARGET=node16 6 | OUTFILE=dist/raw/app.js 7 | 8 | esbuild \ 9 | --bundle \ 10 | --format=esm \ 11 | --outfile=${OUTFILE} \ 12 | --platform=node \ 13 | --external:esbuild \ 14 | --target=${TARGET} \ 15 | app/index.ts 16 | 17 | node ${OUTFILE} split ${OUTFILE} dist/split/ 18 | 19 | rm dist/*.js &2>/dev/null || true 20 | for X in dist/split/*; do 21 | BASE=$(basename $X) 22 | esbuild --format=esm --outfile=dist/$BASE --minify $X 23 | done 24 | 25 | # confirms the binary runs at all 26 | node dist/app.js info dist/app.js >/dev/null 27 | -------------------------------------------------------------------------------- /site/demo-kuto.ts: -------------------------------------------------------------------------------- 1 | import { parse } from 'acorn'; 2 | import { StaticExtractor } from '../lib/extractor.ts'; 3 | import { buildCorpusName } from '../lib/name.ts'; 4 | import { liftDefault } from '../lib/lift.ts'; 5 | 6 | export class DemoKutoElement extends HTMLElement { 7 | private root: ShadowRoot; 8 | private textarea: HTMLTextAreaElement; 9 | private article: HTMLElement; 10 | private priors?: Map; 11 | private buildCount: number = 0; 12 | private statusEl: HTMLElement; 13 | 14 | constructor() { 15 | super(); 16 | 17 | this.root = this.attachShadow({ mode: 'open' }); 18 | this.root.innerHTML = ` 19 | 66 | 67 |
68 |
69 | 70 | 71 |
72 |
73 |
74 |
75 | `; 76 | 77 | this.article = this.root.querySelector('article')!; 78 | const textarea = this.root.querySelector('textarea')!; 79 | const button = this.root.querySelector('button')!; 80 | button.addEventListener('click', async () => { 81 | try { 82 | await this.run(textarea.value); 83 | } catch (e) { 84 | console.warn(e); 85 | } 86 | }); 87 | this.textarea = textarea; 88 | 89 | this.statusEl = this.root.getElementById('status')!; 90 | } 91 | 92 | private async run(source: string) { 93 | const p = parse(source, { ecmaVersion: 'latest', sourceType: 'module' }); 94 | const staticName = buildCorpusName('./src.js'); 95 | 96 | const e = new StaticExtractor({ 97 | p, 98 | source, 99 | sourceName: './src.js', 100 | staticName, 101 | existingStaticSource: this.priors ?? new Map(), 102 | dedupCallables: false, 103 | }); 104 | const liftStats = liftDefault(e, 32); 105 | 106 | const out = e.build({ pretty: true }); 107 | console.info({ liftStats, out }); 108 | 109 | const localBuild = ++this.buildCount; 110 | this.statusEl.textContent = `Build #${localBuild}`; 111 | 112 | this.article.textContent = ''; 113 | 114 | const render = (content: string, filename: string, className = '') => { 115 | const out = document.createElement('pre'); 116 | out.textContent = content; 117 | out.id = `code-${generateId(filename)}`; 118 | 119 | if (filename) { 120 | const heading = document.createElement('strong'); 121 | heading.textContent = `// ${filename}\n`; 122 | out.prepend(heading); 123 | } 124 | 125 | if (className) { 126 | out.className = className; 127 | } 128 | 129 | this.article.append(out); 130 | return out; 131 | }; 132 | 133 | render(out.main, './src.js'); 134 | 135 | const outByName = [...out.static.keys()]; 136 | outByName.sort(); 137 | 138 | outByName.forEach((name) => { 139 | const content = out.static.get(name)!; 140 | 141 | const actions = document.createElement('div'); 142 | actions.className = 'actions'; 143 | this.article.append(actions); 144 | 145 | const node = render(content, name, 'corpus'); 146 | 147 | const deleteNode = document.createElement('button'); 148 | deleteNode.textContent = `Remove ${name}`; 149 | actions.append(deleteNode); 150 | 151 | deleteNode.addEventListener('click', () => { 152 | deleteNode.disabled = true; 153 | node.classList.add('gone'); 154 | this.priors?.delete(name); 155 | }); 156 | }); 157 | 158 | this.priors = out.static; 159 | } 160 | 161 | set value(v: string) { 162 | this.textarea.value = v; 163 | } 164 | } 165 | 166 | const generateId = (filename: string) => { 167 | const chars = Array.from(btoa(filename)); 168 | chars.reverse(); 169 | while (chars[0] === '=') { 170 | chars.shift(); 171 | } 172 | return chars.join(''); 173 | }; 174 | 175 | customElements.define('demo-kuto', DemoKutoElement); 176 | -------------------------------------------------------------------------------- /site/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Kuto 7 | 8 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /site/index.ts: -------------------------------------------------------------------------------- 1 | import { DemoKutoElement } from './demo-kuto.ts'; 2 | 3 | const kd = new DemoKutoElement(); 4 | document.body.append(kd); 5 | 6 | kd.value = `const A = () => 'something that is interesting but when exported, shadows a local'; 7 | 8 | const B = 'something else that is very lonfg hahaha'; 9 | 10 | console.info('this is a long statement that uses B', { B }); 11 | 12 | export { A as B }; 13 | 14 | const _1 = 'Lol shadow Kuto vars'; 15 | 16 | console.info('YET another long statement that uses', { B, _1 }); 17 | console.info(_1); 18 | `; 19 | -------------------------------------------------------------------------------- /test-info.js: -------------------------------------------------------------------------------- 1 | //export let x = 123; 2 | 3 | import * as Something from './somewhere-else'; 4 | import { Foo } from 'bar'; 5 | 6 | Something(); 7 | 8 | export function foo() {} 9 | 10 | function x() { 11 | Something(); 12 | { 13 | Foo.bar++; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/cases/async.js: -------------------------------------------------------------------------------- 1 | async function hoistable_with_long_name_to_make_statement_hoist() { 2 | console.info('long long long'); 3 | return Promise.resolve(345); 4 | } 5 | 6 | const q = hoistable_with_long_name_to_make_statement_hoist(); 7 | q.then((out) => { 8 | console.info(out); 9 | }); 10 | 11 | export { hoistable_with_long_name_to_make_statement_hoist as x }; 12 | -------------------------------------------------------------------------------- /test/cases/await.js: -------------------------------------------------------------------------------- 1 | let ok = false; 2 | 3 | const doGlobal = () => { 4 | ok = true; 5 | }; 6 | 7 | const complexExpressionThatIsLong = async () => { 8 | await Promise.resolve(); 9 | doGlobal(); 10 | }; 11 | await complexExpressionThatIsLong(); 12 | 13 | if (!ok) { 14 | throw new Error(`did not await`); 15 | } 16 | 17 | await (1 + 2 + 3 + 4 + 1 + 2 + 3 + 4 + 1 + 2 + 3 + 4 + 1 + 2 + 3 + 4); 18 | 19 | const complexAwaited = () => { 20 | return +55; // long is long long long ong 21 | }; 22 | 23 | const y = await (1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + complexAwaited()); 24 | console.info(y); 25 | 26 | const asyncInlineMethod = async (x = 123) => { 27 | console.info('long fn expr'); 28 | await 123; // force await check 29 | }; 30 | -------------------------------------------------------------------------------- /test/cases/default-unnamed.js: -------------------------------------------------------------------------------- 1 | export default function () { 2 | console.info('This should be renamed because `default` is not a real name'); 3 | } -------------------------------------------------------------------------------- /test/cases/default-var.js: -------------------------------------------------------------------------------- 1 | export default 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10; -------------------------------------------------------------------------------- /test/cases/default1.js: -------------------------------------------------------------------------------- 1 | export default "long expression that isn't a callable or whatever" 2 | 3 | export function foo() { 4 | console.info('This will not change between default1 and default2'); 5 | } -------------------------------------------------------------------------------- /test/cases/default2.js: -------------------------------------------------------------------------------- 1 | export default function xxx() { 2 | console.info('This is inside a default fn that is complex'); 3 | } 4 | 5 | foo(); 6 | 7 | export function foo() { 8 | console.info('This will not change between default1 and default2'); 9 | } -------------------------------------------------------------------------------- /test/cases/dep/a.js: -------------------------------------------------------------------------------- 1 | // intentionally empty 2 | export {}; 3 | -------------------------------------------------------------------------------- /test/cases/dup.js: -------------------------------------------------------------------------------- 1 | // has a callable; can't be deduped 2 | export const x = () => 'This is very long and the same as below and will NOT be deduped'; 3 | export const y = () => 'This is very long and the same as below and will NOT be deduped'; 4 | 5 | export const a = "This is a long value which will be deduped as there's no callables"; 6 | export const b = "This is a long value which will be deduped as there's no callables"; 7 | -------------------------------------------------------------------------------- /test/cases/iife.js: -------------------------------------------------------------------------------- 1 | function something_long_call() {} 2 | const foo = { bar() {} }; 3 | 4 | (function (q) { 5 | console.info('Kuto should extract this long statement', q); 6 | something_long_call(123, 'hello there long'); 7 | foo.bar(q); 8 | })(); 9 | 10 | if (1) { 11 | var x = 123; 12 | console.info('long statement that uses inner var', x); 13 | console.info('long statement that is otherwise boring AF'); 14 | } 15 | 16 | if (1) console.info('long thing that can be yeetyed'); 17 | -------------------------------------------------------------------------------- /test/cases/import.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | console.info('Long to force hoist'); 3 | process.exit(0); 4 | })(); 5 | 6 | await import('./dep/a.js').then((complex) => { 7 | console.info('does something tricky'); 8 | }); 9 | -------------------------------------------------------------------------------- /test/cases/inline-fn-use.js: -------------------------------------------------------------------------------- 1 | const hoistable_what = async function hoistable() { 2 | // just a really long function 3 | }; 4 | 5 | let randomGlobal = 123; 6 | 7 | hoistable_what().then((out) => { 8 | randomGlobal; 9 | }); 10 | 11 | export { hoistable_what }; 12 | -------------------------------------------------------------------------------- /test/cases/semi.js: -------------------------------------------------------------------------------- 1 | function do_long_and_complicated_thing_over_length() {} 2 | 3 | do_long_and_complicated_thing_over_length() // no semicolon 4 | -------------------------------------------------------------------------------- /test/cases/shadow.js: -------------------------------------------------------------------------------- 1 | const a = () => 'Something long defined with `a`'; 2 | 3 | const b = () => 'Something long defined with `b`'; 4 | 5 | // we need to export real `b` as well; can Kuto work around this? 6 | export { a as b }; 7 | 8 | const useB = 'Something long that _uses_ `b`: ' + b(); 9 | 10 | export { useB }; 11 | 12 | console.info(useB); 13 | console.info('Ok'); 14 | 15 | // vars typically used by Kuto 16 | export const _1 = ''; 17 | export const $1 = ''; 18 | -------------------------------------------------------------------------------- /test/index.ts: -------------------------------------------------------------------------------- 1 | import { $ } from 'zx'; 2 | import * as path from 'node:path'; 3 | import * as url from 'node:url'; 4 | import * as fs from 'node:fs'; 5 | import * as process from 'node:process'; 6 | 7 | const dir = path.dirname(url.fileURLToPath(new URL(import.meta.url))); 8 | process.chdir(dir); 9 | 10 | const onlyRun = process.argv.slice(2); 11 | 12 | // #1: run simple test cases; these just confirm they can compile once, and run 13 | 14 | const casesDir = 'cases'; 15 | try { 16 | fs.rmSync('dist/', { recursive: true }); 17 | } catch {} 18 | 19 | let lastSoloFailure: string = ''; 20 | 21 | const success: string[] = []; 22 | const errors: string[] = []; 23 | const cases = fs 24 | .readdirSync(casesDir) 25 | .filter((x) => x.endsWith('.js')) 26 | .toSorted(); 27 | for (const caseToRun of cases) { 28 | const { name } = path.parse(caseToRun); 29 | 30 | // strip any trailing number: test "foo2" will run after "foo1" 31 | const soloName = name.replace(/\d+$/, ''); 32 | if (onlyRun.length && !onlyRun.includes(soloName)) { 33 | continue; 34 | } 35 | 36 | try { 37 | if (soloName === lastSoloFailure) { 38 | continue; 39 | } 40 | } finally { 41 | lastSoloFailure = ''; 42 | } 43 | console.info('#', name); 44 | 45 | try { 46 | const script = path.join('test', casesDir, caseToRun); 47 | await $`npx tsx ./app split ${script} test/dist/${soloName} -n index`; 48 | await $`node test/dist/${soloName}/index.js`; 49 | success.push(caseToRun); 50 | } catch { 51 | errors.push(caseToRun); 52 | lastSoloFailure = soloName; 53 | } 54 | 55 | console.info(); 56 | } 57 | 58 | const total = success.length + errors.length; 59 | if (total === 0) { 60 | console.warn('no tests matched'); 61 | process.exit(2); // no test run 62 | } 63 | console.info(success.length + '/' + total, 'passed', { errors }); 64 | if (errors.length) { 65 | process.exit(1); 66 | } 67 | -------------------------------------------------------------------------------- /test/valid/a.ts: -------------------------------------------------------------------------------- 1 | const x = 1; 2 | export { x as x }; 3 | 4 | import './b.ts'; 5 | 6 | /** top-level sth */ 7 | // whatever 8 | 9 | import 'we-ignore-this'; 10 | -------------------------------------------------------------------------------- /test/valid/b.ts: -------------------------------------------------------------------------------- 1 | //!@kuto-b 2 | //!@kuto-whatever 3 | -------------------------------------------------------------------------------- /test/valid/index.ts: -------------------------------------------------------------------------------- 1 | //!@kuto-index 2 | //!@kuto-whatever 3 | 4 | import { x as y } from './a.ts'; 5 | 6 | console.info(y); 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "checkJs": true, 4 | "noEmit": true, 5 | 6 | // if you'd like to warn if you're using modern features, change these 7 | // both to e.g., "es2017" 8 | "module": "NodeNext", 9 | "target": "ESNext", 10 | "moduleResolution": "NodeNext", 11 | 12 | // proper es6 support 13 | "esModuleInterop": true, 14 | "allowSyntheticDefaultImports": true, 15 | 16 | // configure as you like: these are my preferred defaults! 17 | "strict": true, 18 | "skipLibCheck": true, 19 | "forceConsistentCasingInFileNames": true, 20 | 21 | // "strict" implies this, and it's good for new projects, but difficult to retrofit to old 22 | // untyped projects 23 | "noImplicitAny": true, 24 | 25 | // modern TS stuff that allows lit-element to work 26 | "experimentalDecorators": true, 27 | "useDefineForClassFields": false, 28 | "allowImportingTsExtensions": true, 29 | }, 30 | "include": [ 31 | "*.js", 32 | "**/*.js", 33 | "*.ts", 34 | "**/*.ts", 35 | "*.tsx", 36 | "**/*.tsx", 37 | ], 38 | } --------------------------------------------------------------------------------