├── .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 |
6 |
7 | [](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 | }
--------------------------------------------------------------------------------