subscript 9.2.0 → 10.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +115 -169
- package/feature/access.js +67 -7
- package/feature/accessor.js +49 -0
- package/feature/asi.js +15 -0
- package/feature/async.js +45 -0
- package/feature/block.js +41 -0
- package/feature/class.js +69 -0
- package/feature/collection.js +40 -0
- package/feature/comment.js +25 -5
- package/feature/destruct.js +33 -0
- package/feature/function.js +44 -0
- package/feature/group.js +39 -9
- package/feature/if.js +23 -38
- package/feature/literal.js +13 -0
- package/feature/loop.js +107 -106
- package/feature/module.js +42 -0
- package/feature/number.js +41 -38
- package/feature/op/arithmetic.js +29 -0
- package/feature/op/arrow.js +33 -0
- package/feature/op/assign-logical.js +33 -0
- package/feature/op/assignment.js +47 -0
- package/feature/op/bitwise-unsigned.js +17 -0
- package/feature/op/bitwise.js +29 -0
- package/feature/op/comparison.js +19 -0
- package/feature/op/defer.js +15 -0
- package/feature/op/equality.js +16 -0
- package/feature/op/identity.js +15 -0
- package/feature/op/increment.js +23 -0
- package/feature/op/logical.js +21 -0
- package/feature/op/membership.js +17 -0
- package/feature/op/nullish.js +13 -0
- package/feature/op/optional.js +61 -0
- package/feature/op/pow.js +19 -0
- package/feature/op/range.js +26 -0
- package/feature/op/spread.js +15 -0
- package/feature/op/ternary.js +15 -0
- package/feature/op/type.js +18 -0
- package/feature/op/unary.js +41 -0
- package/feature/prop.js +34 -0
- package/feature/regex.js +31 -0
- package/feature/seq.js +21 -0
- package/feature/string.js +24 -17
- package/feature/switch.js +48 -0
- package/feature/template.js +39 -0
- package/feature/try.js +57 -0
- package/feature/unit.js +35 -0
- package/feature/var.js +51 -41
- package/jessie.js +31 -0
- package/jessie.min.js +8 -0
- package/justin.js +39 -48
- package/justin.min.js +8 -4
- package/package.json +15 -16
- package/parse.js +153 -0
- package/subscript.d.ts +45 -5
- package/subscript.js +62 -22
- package/subscript.min.js +5 -4
- package/util/bundle.js +507 -0
- package/util/stringify.js +172 -0
- package/feature/add.js +0 -22
- package/feature/array.js +0 -11
- package/feature/arrow.js +0 -23
- package/feature/assign.js +0 -11
- package/feature/bitwise.js +0 -11
- package/feature/bool.js +0 -5
- package/feature/call.js +0 -15
- package/feature/compare.js +0 -11
- package/feature/control.js +0 -142
- package/feature/increment.js +0 -11
- package/feature/logic.js +0 -11
- package/feature/mult.js +0 -25
- package/feature/object.js +0 -17
- package/feature/optional.js +0 -23
- package/feature/pow.js +0 -5
- package/feature/shift.js +0 -12
- package/feature/spread.js +0 -6
- package/feature/ternary.js +0 -10
- package/src/compile.d.ts +0 -17
- package/src/compile.js +0 -28
- package/src/const.js +0 -45
- package/src/parse.d.ts +0 -22
- package/src/parse.js +0 -113
- package/src/stringify.js +0 -27
- /package/{LICENSE → license} +0 -0
package/util/bundle.js
ADDED
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESM Bundler using subscript's own parser (dogfooding)
|
|
3
|
+
*
|
|
4
|
+
* Thin layer: scope analysis + tree transform
|
|
5
|
+
* Parser comes from the dialect (jessie by default)
|
|
6
|
+
*/
|
|
7
|
+
import { parse } from '../jessie.js';
|
|
8
|
+
import { codegen } from './stringify.js';
|
|
9
|
+
import { readFile } from 'fs/promises';
|
|
10
|
+
import { resolve } from 'path';
|
|
11
|
+
|
|
12
|
+
// === AST Utilities ===
|
|
13
|
+
|
|
14
|
+
/** Walk AST, call fn(node, parent, key) for each node */
|
|
15
|
+
const walk = (node, fn, parent = null, key = null) => {
|
|
16
|
+
if (!node || typeof node !== 'object') return;
|
|
17
|
+
fn(node, parent, key);
|
|
18
|
+
if (Array.isArray(node)) {
|
|
19
|
+
for (let i = 0; i < node.length; i++) walk(node[i], fn, node, i);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** Deep clone AST */
|
|
24
|
+
const clone = node =>
|
|
25
|
+
!node ? node :
|
|
26
|
+
Array.isArray(node) ? node.map(clone) :
|
|
27
|
+
node instanceof RegExp ? new RegExp(node.source, node.flags) :
|
|
28
|
+
typeof node === 'object' ? Object.fromEntries(Object.entries(node).map(([k,v]) => [k, clone(v)])) :
|
|
29
|
+
node;
|
|
30
|
+
|
|
31
|
+
/** Rename identifier in AST - skip property access positions */
|
|
32
|
+
const renameId = (ast, old, neu) => {
|
|
33
|
+
walk(ast, (node, parent, key) => {
|
|
34
|
+
if (Array.isArray(node)) {
|
|
35
|
+
for (let i = 0; i < node.length; i++) {
|
|
36
|
+
if (node[i] === old) {
|
|
37
|
+
// Don't rename if this is a property name in a '.' or '?.' access
|
|
38
|
+
if ((node[0] === '.' || node[0] === '?.') && i === 2) continue;
|
|
39
|
+
// Don't rename if this is a property name in object literal {a: b} or shorthand {a}
|
|
40
|
+
if (node[0] === ':' && i === 1 && typeof node[1] === 'string') continue;
|
|
41
|
+
node[i] = neu;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
return ast;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** Flatten comma nodes into array: [',', 'a', 'b'] → ['a', 'b'], 'x' → ['x'] */
|
|
50
|
+
const flattenComma = node =>
|
|
51
|
+
Array.isArray(node) && node[0] === ',' ? node.slice(1) :
|
|
52
|
+
node ? [node] : [];
|
|
53
|
+
|
|
54
|
+
// === Module Analysis ===
|
|
55
|
+
|
|
56
|
+
/** Extract string from path node [null, 'path'] (string literal) */
|
|
57
|
+
const getPath = node => Array.isArray(node) && (node[0] === undefined || node[0] === null) ? node[1] : node;
|
|
58
|
+
|
|
59
|
+
/** Extract imports from AST
|
|
60
|
+
* New AST shapes:
|
|
61
|
+
* import './x.js' → ['import', [null, path]]
|
|
62
|
+
* import X from './x.js' → ['import', ['from', 'X', [null, path]]]
|
|
63
|
+
* import {a,b} from './x' → ['import', ['from', ['{}', ...], [null, path]]]
|
|
64
|
+
* import * as X from './x' → ['import', ['from', ['as', '*', 'X'], [null, path]]]
|
|
65
|
+
*/
|
|
66
|
+
const getImports = ast => {
|
|
67
|
+
const imports = [];
|
|
68
|
+
walk(ast, node => {
|
|
69
|
+
if (!Array.isArray(node) || node[0] !== 'import') return;
|
|
70
|
+
const body = node[1];
|
|
71
|
+
const imp = { node };
|
|
72
|
+
|
|
73
|
+
// import './x.js' - bare import: [, 'path'] sparse array with undefined at index 0
|
|
74
|
+
if (Array.isArray(body) && (body[0] === undefined || body[0] === null)) {
|
|
75
|
+
imp.path = body[1];
|
|
76
|
+
imports.push(imp);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// import X from './x.js' or import {...} from './x.js'
|
|
81
|
+
if (Array.isArray(body) && body[0] === 'from') {
|
|
82
|
+
const spec = body[1];
|
|
83
|
+
const pathNode = body[2];
|
|
84
|
+
imp.path = getPath(pathNode);
|
|
85
|
+
|
|
86
|
+
if (typeof spec === 'string') {
|
|
87
|
+
// import X from - default import
|
|
88
|
+
imp.default_ = spec;
|
|
89
|
+
} else if (Array.isArray(spec)) {
|
|
90
|
+
if (spec[0] === '{}') {
|
|
91
|
+
// import { a, b, c as d }
|
|
92
|
+
const items = spec.slice(1).flatMap(flattenComma);
|
|
93
|
+
imp.named = items.map(s =>
|
|
94
|
+
Array.isArray(s) && s[0] === 'as' ? { name: s[1], alias: s[2] } : { name: s, alias: s }
|
|
95
|
+
);
|
|
96
|
+
} else if (spec[0] === 'as' && spec[1] === '*') {
|
|
97
|
+
// import * as X
|
|
98
|
+
imp.namespace = spec[2];
|
|
99
|
+
} else if (spec[0] === '*') {
|
|
100
|
+
// import * as X (alternate shape)
|
|
101
|
+
imp.namespace = spec[1];
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
imports.push(imp);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
return imports;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/** Extract exports from AST
|
|
111
|
+
* New AST shapes:
|
|
112
|
+
* export const x = 1 → ['export', ['const', ['=', 'x', val]]]
|
|
113
|
+
* export default x → ['export', ['default', 'x']]
|
|
114
|
+
* export { a } → ['export', ['{}', 'a']]
|
|
115
|
+
* export { a } from './x' → ['export', ['from', ['{}', 'a'], [null, path]]]
|
|
116
|
+
* export * from './x' → ['export', ['from', '*', [null, path]]]
|
|
117
|
+
*/
|
|
118
|
+
const getExports = ast => {
|
|
119
|
+
const exports = { named: {}, reexports: [], default_: null };
|
|
120
|
+
|
|
121
|
+
walk(ast, node => {
|
|
122
|
+
if (!Array.isArray(node) || node[0] !== 'export') return;
|
|
123
|
+
const spec = node[1];
|
|
124
|
+
|
|
125
|
+
// export { a } from './x' or export * from './x'
|
|
126
|
+
if (Array.isArray(spec) && spec[0] === 'from') {
|
|
127
|
+
const what = spec[1];
|
|
128
|
+
const pathNode = spec[2];
|
|
129
|
+
const path = getPath(pathNode);
|
|
130
|
+
|
|
131
|
+
if (what === '*') {
|
|
132
|
+
exports.reexports.push({ star: true, path });
|
|
133
|
+
} else if (Array.isArray(what) && what[0] === '{}') {
|
|
134
|
+
const items = what.slice(1).flatMap(flattenComma);
|
|
135
|
+
const names = items.map(s =>
|
|
136
|
+
Array.isArray(s) && s[0] === 'as' ? { name: s[1], alias: s[2] } : { name: s, alias: s }
|
|
137
|
+
);
|
|
138
|
+
exports.reexports.push({ names, path });
|
|
139
|
+
}
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// export { a, b }
|
|
144
|
+
if (Array.isArray(spec) && spec[0] === '{}') {
|
|
145
|
+
const items = spec.slice(1).flatMap(flattenComma);
|
|
146
|
+
const names = items.map(s =>
|
|
147
|
+
Array.isArray(s) && s[0] === 'as' ? { name: s[1], alias: s[2] } : { name: s, alias: s }
|
|
148
|
+
);
|
|
149
|
+
for (const { name, alias } of names) exports.named[alias] = name;
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// export default x
|
|
154
|
+
if (Array.isArray(spec) && spec[0] === 'default') {
|
|
155
|
+
exports.default_ = spec[1];
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// export const/let/var x = ... - varargs: ['let', decl1, decl2, ...]
|
|
160
|
+
if (Array.isArray(spec) && (spec[0] === 'const' || spec[0] === 'let' || spec[0] === 'var')) {
|
|
161
|
+
for (let i = 1; i < spec.length; i++) {
|
|
162
|
+
const decl = spec[i];
|
|
163
|
+
if (typeof decl === 'string') {
|
|
164
|
+
exports.named[decl] = decl;
|
|
165
|
+
} else if (Array.isArray(decl) && decl[0] === '=') {
|
|
166
|
+
const name = decl[1];
|
|
167
|
+
if (typeof name === 'string') exports.named[name] = name;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// export function x() {} or export class x {}
|
|
174
|
+
if (Array.isArray(spec) && (spec[0] === 'function' || spec[0] === 'class')) {
|
|
175
|
+
if (typeof spec[1] === 'string') exports.named[spec[1]] = spec[1];
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
return exports;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
/** Get all declared names in AST
|
|
183
|
+
* New AST shapes:
|
|
184
|
+
* const x = 1 → ['const', ['=', 'x', val]]
|
|
185
|
+
* let x → ['let', 'x']
|
|
186
|
+
* function f() → ['function', 'f', ...]
|
|
187
|
+
* const a = 1, b = 2 → ['const', ['=', 'a', ...], ['=', 'b', ...]] (varargs)
|
|
188
|
+
*/
|
|
189
|
+
const getDecls = ast => {
|
|
190
|
+
const decls = new Set();
|
|
191
|
+
|
|
192
|
+
const addDecl = node => {
|
|
193
|
+
if (typeof node === 'string') decls.add(node);
|
|
194
|
+
else if (Array.isArray(node)) {
|
|
195
|
+
if (node[0] === '=') {
|
|
196
|
+
if (typeof node[1] === 'string') decls.add(node[1]);
|
|
197
|
+
} else if (node[0] === ',') {
|
|
198
|
+
// Multiple declarations: const a = 1, b = 2 (older AST shape)
|
|
199
|
+
for (let i = 1; i < node.length; i++) addDecl(node[i]);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
walk(ast, node => {
|
|
205
|
+
if (!Array.isArray(node)) return;
|
|
206
|
+
const op = node[0];
|
|
207
|
+
|
|
208
|
+
if (op === 'const' || op === 'let' || op === 'var') {
|
|
209
|
+
// Handle varargs: ['const', decl1, decl2, ...] for multiple declarations
|
|
210
|
+
for (let i = 1; i < node.length; i++) addDecl(node[i]);
|
|
211
|
+
}
|
|
212
|
+
if (op === 'function' || op === 'class') {
|
|
213
|
+
if (typeof node[1] === 'string') decls.add(node[1]);
|
|
214
|
+
}
|
|
215
|
+
if (op === 'export') {
|
|
216
|
+
const spec = node[1];
|
|
217
|
+
if (Array.isArray(spec) && (spec[0] === 'const' || spec[0] === 'let' || spec[0] === 'var')) {
|
|
218
|
+
addDecl(spec[1]);
|
|
219
|
+
}
|
|
220
|
+
if (Array.isArray(spec) && (spec[0] === 'function' || spec[0] === 'class') && typeof spec[1] === 'string') {
|
|
221
|
+
decls.add(spec[1]);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
return decls;
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
// === AST Transforms ===
|
|
230
|
+
|
|
231
|
+
/** Remove import/export nodes, extract declarations */
|
|
232
|
+
/** Remove import/export nodes, extract declarations
|
|
233
|
+
* New AST shapes for export:
|
|
234
|
+
* export const x = 1 → ['export', ['const', ...]] → keep ['const', ...]
|
|
235
|
+
* export default x → ['export', ['default', x]] → keep, or convert to __default
|
|
236
|
+
* export { a } → ['export', ['{}', ...]] → remove
|
|
237
|
+
* export { a } from './x' → ['export', ['from', ['{}', ...], path]] → remove
|
|
238
|
+
* export * from './x' → ['export', ['from', '*', path]] → remove
|
|
239
|
+
*/
|
|
240
|
+
const stripModuleSyntax = ast => {
|
|
241
|
+
const defaultExpr = { value: null };
|
|
242
|
+
|
|
243
|
+
const process = node => {
|
|
244
|
+
if (!Array.isArray(node)) return node;
|
|
245
|
+
const op = node[0];
|
|
246
|
+
|
|
247
|
+
if (op === 'import') return null;
|
|
248
|
+
|
|
249
|
+
if (op === 'export') {
|
|
250
|
+
const spec = node[1];
|
|
251
|
+
// Re-exports: export { a } from './x' or export * from './x'
|
|
252
|
+
if (Array.isArray(spec) && spec[0] === 'from') return null;
|
|
253
|
+
// Named exports: export { a, b }
|
|
254
|
+
if (Array.isArray(spec) && spec[0] === '{}') return null;
|
|
255
|
+
// Default export
|
|
256
|
+
if (Array.isArray(spec) && spec[0] === 'default') {
|
|
257
|
+
defaultExpr.value = spec[1];
|
|
258
|
+
if (typeof spec[1] === 'string') return null;
|
|
259
|
+
return ['const', ['=', '__default', spec[1]]];
|
|
260
|
+
}
|
|
261
|
+
// Declaration export: export const x = 1
|
|
262
|
+
return spec;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (op === ';') {
|
|
266
|
+
const parts = node.slice(1).map(process).filter(Boolean);
|
|
267
|
+
if (parts.length === 0) return null;
|
|
268
|
+
if (parts.length === 1) return parts[0];
|
|
269
|
+
return [';', ...parts];
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return node;
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
return { ast: process(ast), defaultExpr: defaultExpr.value };
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
// === Path Resolution ===
|
|
279
|
+
|
|
280
|
+
const resolvePath = (from, to) => {
|
|
281
|
+
if (!to.startsWith('.')) return to;
|
|
282
|
+
const base = from.split('/').slice(0, -1);
|
|
283
|
+
for (const part of to.split('/')) {
|
|
284
|
+
if (part === '..') base.pop();
|
|
285
|
+
else if (part !== '.') base.push(part);
|
|
286
|
+
}
|
|
287
|
+
let path = base.join('/');
|
|
288
|
+
if (!path.endsWith('.js')) path += '.js';
|
|
289
|
+
return path;
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
// === Bundler ===
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Bundle ES modules into single file
|
|
296
|
+
* @param {string} entry - Entry file path
|
|
297
|
+
* @param {(path: string) => string|Promise<string>} read - File reader
|
|
298
|
+
*/
|
|
299
|
+
export async function bundle(entry, read) {
|
|
300
|
+
const modules = new Map();
|
|
301
|
+
const order = [];
|
|
302
|
+
|
|
303
|
+
async function load(path) {
|
|
304
|
+
if (modules.has(path)) return;
|
|
305
|
+
modules.set(path, null);
|
|
306
|
+
|
|
307
|
+
const code = await read(path);
|
|
308
|
+
const ast = parse(code);
|
|
309
|
+
const imports = getImports(ast);
|
|
310
|
+
const exports = getExports(ast);
|
|
311
|
+
const decls = getDecls(ast);
|
|
312
|
+
|
|
313
|
+
for (const imp of imports) {
|
|
314
|
+
imp.resolved = resolvePath(path, imp.path);
|
|
315
|
+
await load(imp.resolved);
|
|
316
|
+
}
|
|
317
|
+
for (const re of exports.reexports) {
|
|
318
|
+
re.resolved = resolvePath(path, re.path);
|
|
319
|
+
await load(re.resolved);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
modules.set(path, { ast: clone(ast), imports, exports, decls });
|
|
323
|
+
order.push(path);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
await load(entry);
|
|
327
|
+
|
|
328
|
+
// Detect conflicts
|
|
329
|
+
const allDecls = new Map();
|
|
330
|
+
for (const [path, mod] of modules) {
|
|
331
|
+
const importAliases = new Set();
|
|
332
|
+
for (const imp of mod.imports) {
|
|
333
|
+
if (imp.default_) importAliases.add(imp.default_);
|
|
334
|
+
if (imp.namespace) importAliases.add(imp.namespace);
|
|
335
|
+
if (imp.named) for (const { alias } of imp.named) importAliases.add(alias);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
for (const name of mod.decls) {
|
|
339
|
+
if (importAliases.has(name)) continue;
|
|
340
|
+
if (!allDecls.has(name)) allDecls.set(name, []);
|
|
341
|
+
allDecls.get(name).push(path);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Build rename maps
|
|
346
|
+
const renames = new Map();
|
|
347
|
+
for (const [name, paths] of allDecls) {
|
|
348
|
+
if (paths.length > 1) {
|
|
349
|
+
for (const path of paths) {
|
|
350
|
+
if (!renames.has(path)) renames.set(path, {});
|
|
351
|
+
// Make valid JS identifier: replace non-alphanumeric with underscore
|
|
352
|
+
const prefix = path.split('/').pop().replace('.js', '').replace(/[^a-zA-Z0-9]/g, '_') + '_';
|
|
353
|
+
renames.get(path)[name] = prefix + name;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const traceDefault = path => {
|
|
359
|
+
const mod = modules.get(path);
|
|
360
|
+
if (!mod) return null;
|
|
361
|
+
const def = mod.exports.default_;
|
|
362
|
+
if (!def) return null;
|
|
363
|
+
if (typeof def === 'string') {
|
|
364
|
+
const pathRenames = renames.get(path) || {};
|
|
365
|
+
if (pathRenames[def]) return pathRenames[def];
|
|
366
|
+
const defImp = mod.imports.find(i => i.default_ === def);
|
|
367
|
+
if (defImp) return traceDefault(defImp.resolved);
|
|
368
|
+
return def;
|
|
369
|
+
}
|
|
370
|
+
return '__default';
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
// Transform each module
|
|
374
|
+
const chunks = [];
|
|
375
|
+
for (const path of order) {
|
|
376
|
+
const mod = modules.get(path);
|
|
377
|
+
const pathRenames = renames.get(path) || {};
|
|
378
|
+
|
|
379
|
+
let ast = clone(mod.ast);
|
|
380
|
+
for (const [old, neu] of Object.entries(pathRenames)) {
|
|
381
|
+
renameId(ast, old, neu);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
for (const imp of mod.imports) {
|
|
385
|
+
const dep = modules.get(imp.resolved);
|
|
386
|
+
if (!dep) continue;
|
|
387
|
+
const depRenames = renames.get(imp.resolved) || {};
|
|
388
|
+
|
|
389
|
+
if (imp.named) {
|
|
390
|
+
for (const { name, alias } of imp.named) {
|
|
391
|
+
// `name` is the exported name, look up what local name it maps to in the dep
|
|
392
|
+
const localName = dep.exports.named[name] || name;
|
|
393
|
+
// Check if that local name was renamed in the dep
|
|
394
|
+
const resolved = depRenames[localName] || localName;
|
|
395
|
+
if (alias !== resolved) renameId(ast, alias, resolved);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (imp.default_) {
|
|
400
|
+
const resolved = traceDefault(imp.resolved);
|
|
401
|
+
if (resolved && imp.default_ !== resolved) {
|
|
402
|
+
renameId(ast, imp.default_, resolved);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (imp.namespace) {
|
|
407
|
+
walk(ast, node => {
|
|
408
|
+
if (Array.isArray(node) && node[0] === '.' && node[1] === imp.namespace) {
|
|
409
|
+
const prop = node[2];
|
|
410
|
+
if (typeof prop === 'string' && dep.exports.named[prop]) {
|
|
411
|
+
const resolved = depRenames[dep.exports.named[prop]] || dep.exports.named[prop];
|
|
412
|
+
node.length = 0;
|
|
413
|
+
node.push(resolved);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const { ast: stripped } = stripModuleSyntax(ast);
|
|
421
|
+
|
|
422
|
+
if (stripped) {
|
|
423
|
+
const code = codegen(stripped);
|
|
424
|
+
if (code.trim()) {
|
|
425
|
+
chunks.push(`// === ${path} ===\n${code}`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Generate exports
|
|
431
|
+
const entryMod = modules.get(entry);
|
|
432
|
+
const entryRenames = renames.get(entry) || {};
|
|
433
|
+
const exportLines = [];
|
|
434
|
+
|
|
435
|
+
// Resolve all named exports including from re-exports
|
|
436
|
+
const resolveExports = (path, seen = new Set()) => {
|
|
437
|
+
if (seen.has(path)) return {};
|
|
438
|
+
seen.add(path);
|
|
439
|
+
const mod = modules.get(path);
|
|
440
|
+
if (!mod) return {};
|
|
441
|
+
const pathRenames = renames.get(path) || {};
|
|
442
|
+
const result = {};
|
|
443
|
+
|
|
444
|
+
// Direct named exports
|
|
445
|
+
for (const [exp, local] of Object.entries(mod.exports.named)) {
|
|
446
|
+
result[exp] = pathRenames[local] || local;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Re-exports
|
|
450
|
+
for (const re of mod.exports.reexports) {
|
|
451
|
+
const depRenames = renames.get(re.resolved) || {};
|
|
452
|
+
if (re.star) {
|
|
453
|
+
// export * from './x' - get all exports from that module
|
|
454
|
+
const depExports = resolveExports(re.resolved, seen);
|
|
455
|
+
for (const [exp, resolved] of Object.entries(depExports)) {
|
|
456
|
+
if (!(exp in result)) result[exp] = resolved; // don't override local exports
|
|
457
|
+
}
|
|
458
|
+
} else if (re.names) {
|
|
459
|
+
// export { a, b } from './x'
|
|
460
|
+
const depMod = modules.get(re.resolved);
|
|
461
|
+
if (depMod) {
|
|
462
|
+
for (const { name, alias } of re.names) {
|
|
463
|
+
const local = depMod.exports.named[name] || name;
|
|
464
|
+
result[alias] = depRenames[local] || local;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return result;
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
const allExports = resolveExports(entry);
|
|
473
|
+
for (const [exp, resolved] of Object.entries(allExports)) {
|
|
474
|
+
exportLines.push(exp === resolved ? exp : `${resolved} as ${exp}`);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if (entryMod.exports.default_) {
|
|
478
|
+
const resolved = traceDefault(entry) || '__default';
|
|
479
|
+
exportLines.push(`${resolved} as default`);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
let result = chunks.join('\n\n');
|
|
483
|
+
if (exportLines.length) {
|
|
484
|
+
result += `\n\nexport { ${exportLines.join(', ')} }`;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return result;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/** Bundle with Node.js fs */
|
|
491
|
+
export const bundleFile = (entry) => bundle(resolve(entry), path => readFile(path, 'utf-8'));
|
|
492
|
+
|
|
493
|
+
// CLI
|
|
494
|
+
if (typeof process !== 'undefined' && process.argv[1]?.includes('bundle')) {
|
|
495
|
+
const entry = process.argv[2];
|
|
496
|
+
if (!entry) {
|
|
497
|
+
console.error('Usage: node bundle.js <entry>');
|
|
498
|
+
process.exit(1);
|
|
499
|
+
}
|
|
500
|
+
bundleFile(entry)
|
|
501
|
+
.then(result => console.log(result))
|
|
502
|
+
.catch(e => {
|
|
503
|
+
console.error('Error:', e.message);
|
|
504
|
+
console.error(e.stack);
|
|
505
|
+
process.exit(1);
|
|
506
|
+
});
|
|
507
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AST → JS Source String (codegen)
|
|
3
|
+
*
|
|
4
|
+
* Simple recursive stringifier using pattern matching on AST structure.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Operator-specific overrides
|
|
8
|
+
export const generators = {};
|
|
9
|
+
|
|
10
|
+
// Register custom generator
|
|
11
|
+
export const generator = (op, fn) => generators[op] = fn;
|
|
12
|
+
|
|
13
|
+
// Stringify AST to JS source
|
|
14
|
+
export const codegen = node => {
|
|
15
|
+
// Handle undefined/null
|
|
16
|
+
if (node === undefined) return 'undefined';
|
|
17
|
+
if (node === null) return 'null';
|
|
18
|
+
if (node === '') return '';
|
|
19
|
+
|
|
20
|
+
// Identifier
|
|
21
|
+
if (!Array.isArray(node)) return String(node);
|
|
22
|
+
|
|
23
|
+
const [op, ...args] = node;
|
|
24
|
+
|
|
25
|
+
// Literal: [, value]
|
|
26
|
+
if (op === undefined) return typeof args[0] === 'string' ? JSON.stringify(args[0]) : String(args[0]);
|
|
27
|
+
|
|
28
|
+
// Custom generator
|
|
29
|
+
if (generators[op]) return generators[op](...args);
|
|
30
|
+
|
|
31
|
+
// Brackets: [], {}, ()
|
|
32
|
+
if (op === '[]' || op === '{}' || op === '()') {
|
|
33
|
+
const prefix = args.length > 1 ? codegen(args.shift()) : '';
|
|
34
|
+
// Empty brackets
|
|
35
|
+
if (args[0] === undefined || args[0] === null) return prefix + op;
|
|
36
|
+
// Comma sequence: [',', a, b, c] → a, b, c (null → empty for sparse arrays)
|
|
37
|
+
const inner = args[0]?.[0] === ','
|
|
38
|
+
? args[0].slice(1).map(a => a === null ? '' : codegen(a)).join(', ')
|
|
39
|
+
: codegen(args[0]);
|
|
40
|
+
return prefix + op[0] + inner + op[1];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Unary: [op, a]
|
|
44
|
+
if (args.length === 1) return op + codegen(args[0]);
|
|
45
|
+
|
|
46
|
+
// N-ary/sequence: comma, semicolon
|
|
47
|
+
if (op === ',' || op === ';') return args.filter(Boolean).map(codegen).join(op === ';' ? '; ' : ', ');
|
|
48
|
+
|
|
49
|
+
// Binary: [op, a, b]
|
|
50
|
+
if (args.length === 2) {
|
|
51
|
+
const [a, b] = args;
|
|
52
|
+
// Postfix: [op, a, null]
|
|
53
|
+
if (b === null) return codegen(a) + op;
|
|
54
|
+
// Property access: no spaces
|
|
55
|
+
if (op === '.') return codegen(a) + '.' + b;
|
|
56
|
+
return codegen(a) + ' ' + op + ' ' + codegen(b);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Ternary: a ? b : c
|
|
60
|
+
if (op === '?' && args.length === 3) {
|
|
61
|
+
return codegen(args[0]) + ' ? ' + codegen(args[1]) + ' : ' + codegen(args[2]);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Fallback n-ary
|
|
65
|
+
return args.filter(Boolean).map(codegen).join(op === ';' ? '; ' : ', ');
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// --- Statement generators (need structure) ---
|
|
69
|
+
|
|
70
|
+
generator('block', body => body === undefined ? '{}' : '{ ' + codegen(body) + ' }');
|
|
71
|
+
|
|
72
|
+
// Variables: ['let', decl] or ['let', decl1, decl2, ...]
|
|
73
|
+
const varGen = kw => (...args) => kw + ' ' + args.map(codegen).join(', ');
|
|
74
|
+
generator('let', varGen('let'));
|
|
75
|
+
generator('const', varGen('const'));
|
|
76
|
+
generator('var', varGen('var'));
|
|
77
|
+
|
|
78
|
+
// Control flow
|
|
79
|
+
const wrap = s => s?.[0] === 'block' ? codegen(s) : '{ ' + (s ? codegen(s) : '') + ' }';
|
|
80
|
+
generator('if', (cond, then, els) => 'if (' + codegen(cond) + ') ' + wrap(then) + (els ? ' else ' + wrap(els) : ''));
|
|
81
|
+
generator('while', (cond, body) => 'while (' + codegen(cond) + ') ' + wrap(body));
|
|
82
|
+
generator('do', (body, cond) => 'do ' + wrap(body) + ' while (' + codegen(cond) + ')');
|
|
83
|
+
generator('for', (head, body) => {
|
|
84
|
+
if (head?.[0] === ';') {
|
|
85
|
+
const [, init, cond, step] = head;
|
|
86
|
+
return 'for (' + (init ? codegen(init) : '') + '; ' + (cond ? codegen(cond) : '') + '; ' + (step ? codegen(step) : '') + ') ' + wrap(body);
|
|
87
|
+
}
|
|
88
|
+
return 'for (' + codegen(head) + ') ' + wrap(body);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
generator('return', a => a === undefined ? 'return' : 'return ' + codegen(a));
|
|
92
|
+
generator('break', () => 'break');
|
|
93
|
+
generator('continue', () => 'continue');
|
|
94
|
+
generator('throw', a => 'throw ' + codegen(a));
|
|
95
|
+
|
|
96
|
+
// Try/Catch - nested structure
|
|
97
|
+
generator('try', body => 'try { ' + codegen(body) + ' }');
|
|
98
|
+
generator('catch', (tryExpr, param, body) => codegen(tryExpr) + ' catch (' + codegen(param) + ') { ' + codegen(body) + ' }');
|
|
99
|
+
generator('finally', (expr, body) => codegen(expr) + ' finally { ' + codegen(body) + ' }');
|
|
100
|
+
|
|
101
|
+
// Functions
|
|
102
|
+
generator('function', (name, params, body) => {
|
|
103
|
+
const args = !params ? '' : params[0] === ',' ? params.slice(1).map(codegen).join(', ') : codegen(params);
|
|
104
|
+
const b = body?.[0] === 'block' ? codegen(body) : '{ ' + (body ? codegen(body) : '') + ' }';
|
|
105
|
+
return 'function' + (name ? ' ' + name : '') + '(' + args + ') ' + b;
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
generator('=>', (params, body) => {
|
|
109
|
+
if (params?.[0] === '()') params = params[1];
|
|
110
|
+
const args = !params ? '()' : typeof params === 'string' ? params :
|
|
111
|
+
params[0] === ',' ? '(' + params.slice(1).map(codegen).join(', ') + ')' : '(' + codegen(params) + ')';
|
|
112
|
+
return args + ' => ' + codegen(body);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Class
|
|
116
|
+
generator('class', (name, base, body) =>
|
|
117
|
+
'class' + (name ? ' ' + name : '') + (base ? ' extends ' + codegen(base) : '') + ' { ' + (body ? codegen(body) : '') + ' }');
|
|
118
|
+
|
|
119
|
+
// Async/Await/Yield
|
|
120
|
+
generator('async', fn => 'async ' + codegen(fn));
|
|
121
|
+
generator('await', a => 'await ' + codegen(a));
|
|
122
|
+
generator('yield', a => a !== undefined ? 'yield ' + codegen(a) : 'yield');
|
|
123
|
+
generator('yield*', a => 'yield* ' + codegen(a));
|
|
124
|
+
|
|
125
|
+
// Switch
|
|
126
|
+
generator('switch', (expr, body) => 'switch (' + codegen(expr) + ') ' + codegen(body));
|
|
127
|
+
generator('case', (test, body) => 'case ' + codegen(test) + ':' + (body ? ' ' + codegen(body) : ''));
|
|
128
|
+
generator('default:', body => 'default:' + (body ? ' ' + codegen(body) : ''));
|
|
129
|
+
|
|
130
|
+
// Keywords
|
|
131
|
+
generator('typeof', a => '(typeof ' + codegen(a) + ')');
|
|
132
|
+
generator('void', a => '(void ' + codegen(a) + ')');
|
|
133
|
+
generator('delete', a => '(delete ' + codegen(a) + ')');
|
|
134
|
+
generator('new', a => 'new ' + codegen(a));
|
|
135
|
+
generator('instanceof', (a, b) => '(' + codegen(a) + ' instanceof ' + codegen(b) + ')');
|
|
136
|
+
|
|
137
|
+
// Optional chaining
|
|
138
|
+
generator('?.', (a, b) => codegen(a) + '?.' + b);
|
|
139
|
+
generator('?.[]', (a, b) => codegen(a) + '?.[' + codegen(b) + ']');
|
|
140
|
+
generator('?.()', (a, b) => codegen(a) + '?.(' + (!b ? '' : b[0] === ',' ? b.slice(1).map(codegen).join(', ') : codegen(b)) + ')');
|
|
141
|
+
|
|
142
|
+
// Object literal
|
|
143
|
+
generator(':', (k, v) => (typeof k === 'string' ? k : '[' + codegen(k) + ']') + ': ' + codegen(v));
|
|
144
|
+
|
|
145
|
+
// Template literals
|
|
146
|
+
generator('`', (...parts) => '`' + parts.map(p => p?.[0] === undefined ? String(p[1]).replace(/`/g, '\\`').replace(/\$/g, '\\$') : '${' + codegen(p) + '}').join('') + '`');
|
|
147
|
+
generator('``', (tag, ...parts) => codegen(tag) + '`' + parts.map(p => p?.[0] === undefined ? String(p[1]).replace(/`/g, '\\`').replace(/\$/g, '\\$') : '${' + codegen(p) + '}').join('') + '`');
|
|
148
|
+
|
|
149
|
+
// Getter/Setter
|
|
150
|
+
generator('get', (name, body) => 'get ' + name + '() { ' + (body ? codegen(body) : '') + ' }');
|
|
151
|
+
generator('set', (name, param, body) => 'set ' + name + '(' + param + ') { ' + (body ? codegen(body) : '') + ' }');
|
|
152
|
+
generator('static', a => 'static ' + codegen(a));
|
|
153
|
+
|
|
154
|
+
// Non-JS operators (emit as helpers)
|
|
155
|
+
generator('..', (a, b) => 'range(' + codegen(a) + ', ' + codegen(b) + ')');
|
|
156
|
+
generator('..<', (a, b) => 'range(' + codegen(a) + ', ' + codegen(b) + ', true)');
|
|
157
|
+
generator('as', (a, b) => b ? codegen(a) + ' as ' + b : codegen(a)); // import alias or type assertion
|
|
158
|
+
generator('is', (a, b) => '(' + codegen(a) + ' instanceof ' + codegen(b) + ')');
|
|
159
|
+
generator('defer', a => '/* defer */ ' + codegen(a));
|
|
160
|
+
|
|
161
|
+
// For-in/of helper
|
|
162
|
+
generator('in', (a, b) => codegen(a) + ' in ' + codegen(b));
|
|
163
|
+
generator('of', (a, b) => codegen(a) + ' of ' + codegen(b));
|
|
164
|
+
generator('for await', (head, body) => 'for await (' + codegen(head) + ') ' + wrap(body));
|
|
165
|
+
|
|
166
|
+
// Module syntax
|
|
167
|
+
generator('export', spec => 'export ' + codegen(spec));
|
|
168
|
+
generator('import', spec => 'import ' + codegen(spec));
|
|
169
|
+
generator('from', (what, path) => codegen(what) + ' from ' + codegen(path));
|
|
170
|
+
generator('default', a => 'default ' + codegen(a));
|
|
171
|
+
|
|
172
|
+
export default codegen;
|