openzoo 0.50.99 → 0.51.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/bin/openzoo.js +33 -0
- package/lib/cursorbackend.js +6 -1
- package/lib/cursorbackend.js.bak-hiddenbots +4072 -0
- package/lib/models.js +6 -1
- package/package.json +7 -4
- package/transmute/README.md +352 -0
- package/transmute/bin/openzoo-transmute.js +7 -0
- package/transmute/docs/OPENZOO_CLI_PATCH.md +139 -0
- package/transmute/docs/VERCEL_TO_SOLANA.md +381 -0
- package/transmute/lib/build.js +311 -0
- package/transmute/lib/cli.js +231 -0
- package/transmute/lib/compile/index.js +146 -0
- package/transmute/lib/compile/ir.js +1634 -0
- package/transmute/lib/compile/parse.js +753 -0
- package/transmute/lib/compile/rust.js +777 -0
- package/transmute/lib/deploy.js +209 -0
- package/transmute/lib/eligibility.js +211 -0
- package/transmute/lib/gateway.js +704 -0
- package/transmute/lib/hub.js +249 -0
- package/transmute/lib/solana.js +339 -0
- package/transmute/lib/vercel.js +296 -0
- package/transmute/lib/wallet.js +41 -0
- package/transmute/lib/wire.js +167 -0
- package/transmute/runtime/zoo-host/Cargo.lock +588 -0
- package/transmute/runtime/zoo-host/Cargo.toml +28 -0
- package/transmute/runtime/zoo-host/src/assets.rs +170 -0
- package/transmute/runtime/zoo-host/src/ctx.rs +322 -0
- package/transmute/runtime/zoo-host/src/json.rs +257 -0
- package/transmute/runtime/zoo-host/src/kv.rs +182 -0
- package/transmute/runtime/zoo-host/src/lib.rs +123 -0
- package/transmute/runtime/zoo-host/src/val.rs +1044 -0
- package/transmute/runtime/zoo-host/src/wire.rs +239 -0
- package/transmute/site/.env.production +2 -0
- package/transmute/site/api/echo.js +10 -0
- package/transmute/site/api/hello.js +12 -0
- package/transmute/site/api/hits.js +13 -0
- package/transmute/site/api/time.js +5 -0
- package/transmute/site/public/app.js +16 -0
- package/transmute/site/public/index.html +62 -0
|
@@ -0,0 +1,753 @@
|
|
|
1
|
+
// Source → AST → module shape.
|
|
2
|
+
//
|
|
3
|
+
// * `stripTypes` is a conservative, position-preserving TypeScript stripper
|
|
4
|
+
// (removed text becomes spaces, newlines stay) so acorn can parse `.ts`
|
|
5
|
+
// route files and every reported line number still points at the source.
|
|
6
|
+
// * `parseModule` runs acorn (module first, script second for CommonJS).
|
|
7
|
+
// * `readModule` finds the Lambda handler(s): `export default (req, res)`,
|
|
8
|
+
// app-router `export function GET/POST/...`, `module.exports = ...`, and
|
|
9
|
+
// collects imports, module-scope declarations and config exports.
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import * as acorn from 'acorn';
|
|
12
|
+
import { Ineligible } from '../eligibility.js';
|
|
13
|
+
|
|
14
|
+
export const HTTP_METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD'];
|
|
15
|
+
const TS_EXTS = new Set(['.ts', '.tsx', '.mts', '.cts']);
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------- code mask
|
|
18
|
+
|
|
19
|
+
const isWord = (c) => /[A-Za-z0-9_$]/.test(c);
|
|
20
|
+
const isSpace = (c) => c === ' ' || c === '\t' || c === '\n' || c === '\r';
|
|
21
|
+
const REGEX_PREV_WORDS = new Set(['return', 'typeof', 'instanceof', 'in', 'of', 'new', 'delete', 'void', 'throw', 'case', 'do', 'else', 'yield', 'await']);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 1 for characters that are code, 0 for characters inside strings, template
|
|
25
|
+
* literal text, comments and regex literals. Template `${...}` holes are code.
|
|
26
|
+
*/
|
|
27
|
+
export function codeMask(src) {
|
|
28
|
+
const n = src.length;
|
|
29
|
+
const mask = new Uint8Array(n).fill(1);
|
|
30
|
+
const blank = (a, b) => { for (let k = a; k < b && k < n; k++) mask[k] = 0; };
|
|
31
|
+
function scanString(i) {
|
|
32
|
+
const q = src[i];
|
|
33
|
+
let j = i + 1;
|
|
34
|
+
while (j < n) {
|
|
35
|
+
const c = src[j];
|
|
36
|
+
if (c === '\\') { j += 2; continue; }
|
|
37
|
+
if (c === q || c === '\n') { j++; break; }
|
|
38
|
+
j++;
|
|
39
|
+
}
|
|
40
|
+
blank(i, j);
|
|
41
|
+
return j;
|
|
42
|
+
}
|
|
43
|
+
function scanRegex(i) {
|
|
44
|
+
let j = i + 1;
|
|
45
|
+
let inClass = false;
|
|
46
|
+
while (j < n) {
|
|
47
|
+
const c = src[j];
|
|
48
|
+
if (c === '\\') { j += 2; continue; }
|
|
49
|
+
if (c === '\n') break;
|
|
50
|
+
if (inClass) { if (c === ']') inClass = false; j++; continue; }
|
|
51
|
+
if (c === '[') { inClass = true; j++; continue; }
|
|
52
|
+
if (c === '/') { j++; break; }
|
|
53
|
+
j++;
|
|
54
|
+
}
|
|
55
|
+
while (j < n && /[a-z]/.test(src[j])) j++;
|
|
56
|
+
blank(i, j);
|
|
57
|
+
return j;
|
|
58
|
+
}
|
|
59
|
+
function scanTemplate(i) {
|
|
60
|
+
mask[i] = 0;
|
|
61
|
+
let j = i + 1;
|
|
62
|
+
while (j < n) {
|
|
63
|
+
const c = src[j];
|
|
64
|
+
if (c === '\\') { mask[j] = 0; if (j + 1 < n) mask[j + 1] = 0; j += 2; continue; }
|
|
65
|
+
if (c === '`') { mask[j] = 0; return j + 1; }
|
|
66
|
+
if (c === '$' && src[j + 1] === '{') {
|
|
67
|
+
mask[j] = 0; mask[j + 1] = 0;
|
|
68
|
+
const k = scanCode(j + 2, true);
|
|
69
|
+
if (k < n) mask[k] = 0;
|
|
70
|
+
j = k + 1;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
mask[j] = 0;
|
|
74
|
+
j++;
|
|
75
|
+
}
|
|
76
|
+
return j;
|
|
77
|
+
}
|
|
78
|
+
function scanCode(i, stopAtBrace) {
|
|
79
|
+
let depth = 0;
|
|
80
|
+
let lastSig = '';
|
|
81
|
+
let lastWord = '';
|
|
82
|
+
while (i < n) {
|
|
83
|
+
const c = src[i];
|
|
84
|
+
const d = src[i + 1];
|
|
85
|
+
if (c === '/' && d === '/') { const e = src.indexOf('\n', i); const end = e < 0 ? n : e; blank(i, end); i = end; continue; }
|
|
86
|
+
if (c === '/' && d === '*') { const e = src.indexOf('*/', i + 2); const end = e < 0 ? n : e + 2; blank(i, end); i = end; continue; }
|
|
87
|
+
if (c === '"' || c === "'") { i = scanString(i); lastSig = '"'; lastWord = ''; continue; }
|
|
88
|
+
if (c === '`') { i = scanTemplate(i); lastSig = '"'; lastWord = ''; continue; }
|
|
89
|
+
if (c === '/') {
|
|
90
|
+
const prevIsOperand = lastSig === ')' || lastSig === ']' || lastSig === '"' || (isWord(lastSig) && !REGEX_PREV_WORDS.has(lastWord));
|
|
91
|
+
if (!prevIsOperand) { i = scanRegex(i); lastSig = ')'; lastWord = ''; continue; }
|
|
92
|
+
}
|
|
93
|
+
if (c === '{') depth++;
|
|
94
|
+
if (c === '}') { if (depth === 0 && stopAtBrace) return i; depth--; }
|
|
95
|
+
if (!isSpace(c)) { lastSig = c; lastWord = isWord(c) ? lastWord + c : ''; }
|
|
96
|
+
i++;
|
|
97
|
+
}
|
|
98
|
+
return i;
|
|
99
|
+
}
|
|
100
|
+
scanCode(0, false);
|
|
101
|
+
return mask;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ---------------------------------------------------------------- TypeScript stripping
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Best-effort removal of TypeScript syntax: type annotations on params /
|
|
108
|
+
* returns / variables, `as` / `satisfies` casts, generics, `import type`,
|
|
109
|
+
* `interface` / `type` declarations, non-null `!`, optional-param `?`.
|
|
110
|
+
* Positions are preserved (removed text becomes spaces).
|
|
111
|
+
*/
|
|
112
|
+
export function stripTypes(src) {
|
|
113
|
+
const mask = codeMask(src);
|
|
114
|
+
const out = src.split('');
|
|
115
|
+
const n = out.length;
|
|
116
|
+
const code = (i) => i >= 0 && i < n && mask[i] === 1;
|
|
117
|
+
const ch = (i) => (i >= 0 && i < n ? out[i] : '');
|
|
118
|
+
const blank = (a, b) => { for (let k = a; k < b && k < n; k++) if (out[k] !== '\n') out[k] = ' '; };
|
|
119
|
+
const skipWs = (i) => { while (i < n && (!code(i) || isSpace(out[i]))) i++; return i; };
|
|
120
|
+
const skipWsNoNl = (i) => { while (i < n && (!code(i) || out[i] === ' ' || out[i] === '\t' || out[i] === '\r')) i++; return i; };
|
|
121
|
+
const wordAt = (i) => { let j = i; while (j < n && code(j) && isWord(out[j])) j++; return out.slice(i, j).join(''); };
|
|
122
|
+
const prevSig = (i) => { let j = i - 1; while (j >= 0 && (isSpace(out[j]) || (!code(j) && out[j] !== '"' && out[j] !== "'" && out[j] !== '`' && mask[j] === 0 && isBlankComment(j)))) j--; return j; };
|
|
123
|
+
// A masked char that belongs to a comment (not a string) — treat like whitespace when looking backwards.
|
|
124
|
+
function isBlankComment(j) {
|
|
125
|
+
// walk back to the start of the masked run and see whether it begins with // or /*
|
|
126
|
+
let k = j;
|
|
127
|
+
while (k > 0 && mask[k - 1] === 0) k--;
|
|
128
|
+
return out[k] === '/' && (out[k + 1] === '/' || out[k + 1] === '*');
|
|
129
|
+
}
|
|
130
|
+
const OPEN = { '(': ')', '[': ']', '{': '}', '<': '>' };
|
|
131
|
+
/** Index of the bracket closing the one at i (only code chars count). `<` groups skip `=>`. */
|
|
132
|
+
function matchClose(i) {
|
|
133
|
+
const o = out[i];
|
|
134
|
+
const c = OPEN[o];
|
|
135
|
+
let depth = 0;
|
|
136
|
+
for (let j = i; j < n; j++) {
|
|
137
|
+
if (!code(j)) continue;
|
|
138
|
+
const x = out[j];
|
|
139
|
+
if (o === '<' && x === '=' && out[j + 1] === '>') { j++; continue; }
|
|
140
|
+
if (x === o) depth++;
|
|
141
|
+
else if (x === c) { depth--; if (depth === 0) return j; }
|
|
142
|
+
else if (o === '<' && (x === ';' || x === '\n')) return -1;
|
|
143
|
+
}
|
|
144
|
+
return -1;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Scan a type starting at i (first char after `:`/`as`). Returns the index just
|
|
148
|
+
* past the type. `stops` are chars that end it at depth 0; `=>` ends it unless
|
|
149
|
+
* the type started with a parenthesized group (function type).
|
|
150
|
+
*/
|
|
151
|
+
function scanType(i, stops) {
|
|
152
|
+
i = skipWs(i);
|
|
153
|
+
let j = i;
|
|
154
|
+
let first = true;
|
|
155
|
+
let startedWithParen = false;
|
|
156
|
+
let sawArrow = false;
|
|
157
|
+
while (j < n) {
|
|
158
|
+
if (!code(j)) { j++; continue; }
|
|
159
|
+
const x = out[j];
|
|
160
|
+
if (x === '\n') {
|
|
161
|
+
// a type continues on the next line only with a continuation token
|
|
162
|
+
const k = skipWs(j);
|
|
163
|
+
if (k < n && (out[k] === '|' || out[k] === '&' || out[k] === '.' || (out[k] === '<' && !first))) { j = k; continue; }
|
|
164
|
+
return j;
|
|
165
|
+
}
|
|
166
|
+
if (x === ' ' || x === '\t' || x === '\r') { j++; continue; }
|
|
167
|
+
if (x === '=' && out[j + 1] === '>') {
|
|
168
|
+
if (startedWithParen && !sawArrow) { sawArrow = true; j += 2; first = false; continue; }
|
|
169
|
+
return j;
|
|
170
|
+
}
|
|
171
|
+
if (stops.includes(x)) return j;
|
|
172
|
+
if (x === '{' && !first) return j;
|
|
173
|
+
if ((x === '|' && out[j + 1] === '|') || (x === '&' && out[j + 1] === '&')) return j;
|
|
174
|
+
if (x in OPEN) {
|
|
175
|
+
if (first && x === '(') startedWithParen = true;
|
|
176
|
+
const m = matchClose(j);
|
|
177
|
+
if (m < 0) return j;
|
|
178
|
+
j = m + 1;
|
|
179
|
+
first = false;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (x === ')' || x === ']' || x === '}' || x === '>') return j;
|
|
183
|
+
if (isWord(x) || x === '|' || x === '&' || x === '.' || x === '?' || x === ':' || x === '-' || x === '+') {
|
|
184
|
+
// keywords that continue a type
|
|
185
|
+
j++;
|
|
186
|
+
first = false;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
return j;
|
|
190
|
+
}
|
|
191
|
+
return j;
|
|
192
|
+
}
|
|
193
|
+
// -- 0. protect import/export { ... } groups from `as` stripping, remove `type` specifiers
|
|
194
|
+
const protectedRanges = [];
|
|
195
|
+
for (let i = 0; i < n; i++) {
|
|
196
|
+
if (!code(i)) continue;
|
|
197
|
+
if (!(out[i] === 'i' || out[i] === 'e') || (i > 0 && isWord(ch(i - 1)))) continue;
|
|
198
|
+
const w = wordAt(i);
|
|
199
|
+
if (w !== 'import' && w !== 'export') continue;
|
|
200
|
+
let j = skipWs(i + w.length);
|
|
201
|
+
const w2 = wordAt(j);
|
|
202
|
+
if (w2 === 'type') {
|
|
203
|
+
// `import type X from`, `export type { X }`, `export type X = ...`
|
|
204
|
+
const k = skipWs(j + 4);
|
|
205
|
+
if (w === 'import' || out[k] === '{') {
|
|
206
|
+
let end = k;
|
|
207
|
+
if (out[k] === '{') { const m = matchClose(k); end = m < 0 ? k : m + 1; }
|
|
208
|
+
// to end of statement: the module string or `;`/newline
|
|
209
|
+
while (end < n && out[end] !== ';' && out[end] !== '\n') end++;
|
|
210
|
+
blank(i, end + (out[end] === ';' ? 1 : 0));
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
// export type X = T;
|
|
214
|
+
let e = k;
|
|
215
|
+
while (e < n && code(e) && out[e] !== '=' && out[e] !== '\n') e++;
|
|
216
|
+
if (out[e] === '=') e = scanType(e + 1, [';']);
|
|
217
|
+
blank(i, e + (out[e] === ';' ? 1 : 0));
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (w === 'export' && (w2 === 'interface' || w2 === 'declare' || w2 === 'abstract')) continue; // handled below
|
|
221
|
+
if (w === 'export' && w2 === 'default') { j = skipWs(j + 7); }
|
|
222
|
+
if (out[j] === '*') { j = skipWs(j + 1); if (wordAt(j) === 'as') { j = skipWs(j + 2); j += wordAt(j).length; } }
|
|
223
|
+
if (out[j] === '{') {
|
|
224
|
+
const m = matchClose(j);
|
|
225
|
+
if (m > 0) {
|
|
226
|
+
protectedRanges.push([j, m]);
|
|
227
|
+
// `type X,` / `type X as Y,` inside the braces
|
|
228
|
+
let k = j + 1;
|
|
229
|
+
while (k < m) {
|
|
230
|
+
k = skipWs(k);
|
|
231
|
+
if (k >= m) break;
|
|
232
|
+
if (wordAt(k) === 'type' && isSpace(ch(k + 4))) {
|
|
233
|
+
let e = skipWs(k + 4);
|
|
234
|
+
e += wordAt(e).length;
|
|
235
|
+
let e2 = skipWs(e);
|
|
236
|
+
if (wordAt(e2) === 'as') { e2 = skipWs(e2 + 2); e2 += wordAt(e2).length; e = e2; }
|
|
237
|
+
e2 = skipWs(e);
|
|
238
|
+
if (out[e2] === ',') e = e2 + 1;
|
|
239
|
+
blank(k, e);
|
|
240
|
+
k = e;
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
while (k < m && out[k] !== ',') k++;
|
|
244
|
+
k++;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
const isProtected = (i) => protectedRanges.some(([a, b]) => i > a && i < b);
|
|
250
|
+
|
|
251
|
+
// -- 1. interface / type / declare statements
|
|
252
|
+
for (let i = 0; i < n; i++) {
|
|
253
|
+
if (!code(i) || !isWord(out[i]) || (i > 0 && isWord(ch(i - 1)))) continue;
|
|
254
|
+
const w = wordAt(i);
|
|
255
|
+
if (w !== 'interface' && w !== 'type' && w !== 'declare' && w !== 'abstract') continue;
|
|
256
|
+
// statement start: preceded by newline / ; / } / start or `export`
|
|
257
|
+
const p = prevSig(i);
|
|
258
|
+
let stmtStart = i;
|
|
259
|
+
const pw = p >= 0 ? wordEndingAt(p) : '';
|
|
260
|
+
if (pw === 'export') stmtStart = p - 5;
|
|
261
|
+
else if (!(p < 0 || out[p] === ';' || out[p] === '}' || out[p] === '{' || out[p] === '\n' || lineBreakBetween(p, i))) continue;
|
|
262
|
+
const next = skipWs(i + w.length);
|
|
263
|
+
if (w === 'type') {
|
|
264
|
+
if (!isWord(ch(next))) continue; // `type` used as an identifier
|
|
265
|
+
let e = next;
|
|
266
|
+
while (e < n && (code(e) ? out[e] !== '=' && out[e] !== '\n' : true)) e++;
|
|
267
|
+
if (out[e] !== '=') continue;
|
|
268
|
+
e = scanType(e + 1, [';']);
|
|
269
|
+
blank(stmtStart, e + (out[e] === ';' ? 1 : 0));
|
|
270
|
+
i = e;
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
if (w === 'interface') {
|
|
274
|
+
let k = next;
|
|
275
|
+
while (k < n && out[k] !== '{') k++;
|
|
276
|
+
const m = matchClose(k);
|
|
277
|
+
if (m < 0) continue;
|
|
278
|
+
blank(stmtStart, m + 1);
|
|
279
|
+
i = m;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
if (w === 'declare' || w === 'abstract') {
|
|
283
|
+
if (!isWord(ch(next))) continue;
|
|
284
|
+
let k = next;
|
|
285
|
+
let m = -1;
|
|
286
|
+
while (k < n && out[k] !== ';' && out[k] !== '\n') { if (code(k) && out[k] === '{') { m = matchClose(k); break; } k++; }
|
|
287
|
+
const end = m >= 0 ? m + 1 : k + (out[k] === ';' ? 1 : 0);
|
|
288
|
+
blank(stmtStart, end);
|
|
289
|
+
i = end;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
function wordEndingAt(p) {
|
|
293
|
+
let s = p;
|
|
294
|
+
while (s >= 0 && isWord(ch(s))) s--;
|
|
295
|
+
return out.slice(s + 1, p + 1).join('');
|
|
296
|
+
}
|
|
297
|
+
function lineBreakBetween(a, b) {
|
|
298
|
+
for (let k = a; k < b; k++) if (out[k] === '\n') return true;
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// -- 2. function generics, parameter lists, return types
|
|
303
|
+
function stripParamList(open) {
|
|
304
|
+
const close = matchClose(open);
|
|
305
|
+
if (close < 0) return open;
|
|
306
|
+
let i = open + 1;
|
|
307
|
+
while (i < close) {
|
|
308
|
+
if (!code(i)) { i++; continue; }
|
|
309
|
+
const x = out[i];
|
|
310
|
+
if (x === '(' || x === '[' || x === '{') { const m = matchClose(i); if (m < 0) break; i = m + 1; continue; }
|
|
311
|
+
if (x === '?') {
|
|
312
|
+
const k = skipWs(i + 1);
|
|
313
|
+
if (out[k] === ':' || out[k] === ',' || out[k] === ')') { blank(i, i + 1); i++; continue; }
|
|
314
|
+
}
|
|
315
|
+
if (x === ':') {
|
|
316
|
+
const e = scanType(i + 1, [',', ')', '=']);
|
|
317
|
+
blank(i, e);
|
|
318
|
+
i = e;
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
if (x === '=' && out[i + 1] !== '>') {
|
|
322
|
+
// default value: skip to the next top-level , or )
|
|
323
|
+
let k = i + 1;
|
|
324
|
+
while (k < close) {
|
|
325
|
+
if (!code(k)) { k++; continue; }
|
|
326
|
+
if (out[k] === '(' || out[k] === '[' || out[k] === '{') { const m = matchClose(k); if (m < 0) break; k = m + 1; continue; }
|
|
327
|
+
if (out[k] === ',') break;
|
|
328
|
+
k++;
|
|
329
|
+
}
|
|
330
|
+
i = k;
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
i++;
|
|
334
|
+
}
|
|
335
|
+
return close;
|
|
336
|
+
}
|
|
337
|
+
function stripReturnType(afterClose, needArrow) {
|
|
338
|
+
let k = skipWsNoNl(afterClose);
|
|
339
|
+
if (out[k] !== ':') return needArrow ? -1 : k;
|
|
340
|
+
const e = scanType(k + 1, ['{', ';', ',', ')', '=']);
|
|
341
|
+
const after = skipWs(e);
|
|
342
|
+
if (needArrow && !(out[after] === '=' && out[after + 1] === '>')) return -1;
|
|
343
|
+
blank(k, e);
|
|
344
|
+
return e;
|
|
345
|
+
}
|
|
346
|
+
for (let i = 0; i < n; i++) {
|
|
347
|
+
if (!code(i)) continue;
|
|
348
|
+
const x = out[i];
|
|
349
|
+
if (isWord(x) && !(i > 0 && isWord(ch(i - 1)))) {
|
|
350
|
+
const w = wordAt(i);
|
|
351
|
+
if (w === 'function' || w === 'catch') {
|
|
352
|
+
let j = skipWs(i + w.length);
|
|
353
|
+
if (out[j] === '*') j = skipWs(j + 1);
|
|
354
|
+
if (isWord(ch(j))) j = skipWs(j + wordAt(j).length);
|
|
355
|
+
if (out[j] === '<') { const m = matchClose(j); if (m > 0) { blank(j, m + 1); j = skipWs(m + 1); } }
|
|
356
|
+
if (out[j] === '(') {
|
|
357
|
+
const close = stripParamList(j);
|
|
358
|
+
if (w === 'function') stripReturnType(close + 1, false);
|
|
359
|
+
i = j; // continue scanning inside the params (arrow defaults etc.)
|
|
360
|
+
}
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
i += w.length - 1;
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
if (x === '(') {
|
|
367
|
+
const close = matchClose(i);
|
|
368
|
+
if (close < 0) continue;
|
|
369
|
+
// arrow function? ( ... ) [: T] =>
|
|
370
|
+
let k = skipWsNoNl(close + 1);
|
|
371
|
+
let isArrow = out[k] === '=' && out[k + 1] === '>';
|
|
372
|
+
if (!isArrow && out[k] === ':') isArrow = stripReturnType(close + 1, true) >= 0;
|
|
373
|
+
if (isArrow) stripParamList(i);
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
if (x === '<') {
|
|
377
|
+
// generic arrow `<T>(x: T) => ...` or `<T,>(...)`: previous is not an operand
|
|
378
|
+
const p = prevSig(i);
|
|
379
|
+
const pc = ch(p);
|
|
380
|
+
if (p < 0 || '=(,:?[{;'.includes(pc) || wordEndingAt(p) === 'return') {
|
|
381
|
+
const m = matchClose(i);
|
|
382
|
+
if (m > 0) {
|
|
383
|
+
const k = skipWs(m + 1);
|
|
384
|
+
if (out[k] === '(') {
|
|
385
|
+
const close = matchClose(k);
|
|
386
|
+
const a = close > 0 ? skipWs(close + 1) : -1;
|
|
387
|
+
const arrow = a >= 0 && ((out[a] === '=' && out[a + 1] === '>') || out[a] === ':');
|
|
388
|
+
if (arrow) blank(i, m + 1);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// -- 3. variable annotations: const x: T = ...
|
|
396
|
+
for (let i = 0; i < n; i++) {
|
|
397
|
+
if (!code(i) || !isWord(out[i]) || (i > 0 && isWord(ch(i - 1)))) continue;
|
|
398
|
+
const w = wordAt(i);
|
|
399
|
+
if (w !== 'const' && w !== 'let' && w !== 'var') { i += w.length - 1; continue; }
|
|
400
|
+
let j = skipWs(i + w.length);
|
|
401
|
+
if (out[j] === '{' || out[j] === '[') { const m = matchClose(j); if (m < 0) continue; j = m + 1; }
|
|
402
|
+
else if (isWord(ch(j))) j += wordAt(j).length;
|
|
403
|
+
else continue;
|
|
404
|
+
let k = skipWsNoNl(j);
|
|
405
|
+
if (out[k] === '!') { blank(k, k + 1); k = skipWsNoNl(k + 1); }
|
|
406
|
+
if (out[k] === ':') {
|
|
407
|
+
const e = scanType(k + 1, ['=', ';', ',']);
|
|
408
|
+
blank(k, e);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// -- 4. `as T` / `satisfies T`
|
|
413
|
+
for (let i = 0; i < n; i++) {
|
|
414
|
+
if (!code(i) || (out[i] !== 'a' && out[i] !== 's') || (i > 0 && isWord(ch(i - 1))) || isProtected(i)) continue;
|
|
415
|
+
const w = wordAt(i);
|
|
416
|
+
if (w !== 'as' && w !== 'satisfies') { i += Math.max(0, w.length - 1); continue; }
|
|
417
|
+
const p = prevSig(i);
|
|
418
|
+
if (p < 0) continue;
|
|
419
|
+
const pc = out[p];
|
|
420
|
+
const operand = isWord(pc) || pc === ')' || pc === ']' || pc === '}' || mask[p] === 0 || pc === '>';
|
|
421
|
+
if (!operand) continue;
|
|
422
|
+
if (isWord(pc)) {
|
|
423
|
+
const pw = wordEndingAt(p);
|
|
424
|
+
if (['return', 'typeof', 'case', 'in', 'of', 'import', 'export', 'new', 'await', 'yield', 'throw', 'delete', 'void'].includes(pw)) continue;
|
|
425
|
+
}
|
|
426
|
+
const after = skipWsNoNl(i + w.length);
|
|
427
|
+
if (!(isWord(ch(after)) || out[after] === '{' || out[after] === '(' || out[after] === '[' || mask[after] === 0)) continue;
|
|
428
|
+
const e = scanType(i + w.length, [',', ')', ']', '}', ';', '=', '?', ':', '!', '+', '-', '*', '/', '%', '^', '~', '<']);
|
|
429
|
+
blank(i, e);
|
|
430
|
+
i = e;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// -- 5. call / new generics: `name<T>(`
|
|
434
|
+
for (let i = 0; i < n; i++) {
|
|
435
|
+
if (!code(i) || out[i] !== '<') continue;
|
|
436
|
+
const p = i - 1;
|
|
437
|
+
if (!(p >= 0 && code(p) && isWord(out[p]))) continue;
|
|
438
|
+
const m = matchClose(i);
|
|
439
|
+
if (m < 0) continue;
|
|
440
|
+
const inner = out.slice(i + 1, m).join('');
|
|
441
|
+
if (/[;\n]/.test(inner) || /&&|\|\|/.test(inner) || inner.trim() === '') continue;
|
|
442
|
+
const k = skipWsNoNl(m + 1);
|
|
443
|
+
if (out[k] === '(' || out[k] === '`') blank(i, m + 1);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// -- 6. non-null assertions `x!`
|
|
447
|
+
for (let i = 1; i < n; i++) {
|
|
448
|
+
if (!code(i) || out[i] !== '!' || out[i + 1] === '=') continue;
|
|
449
|
+
const pc = out[i - 1];
|
|
450
|
+
if (code(i - 1) && (isWord(pc) || pc === ')' || pc === ']')) {
|
|
451
|
+
const k = skipWsNoNl(i + 1);
|
|
452
|
+
if (out[k] === '.' || out[k] === ')' || out[k] === ',' || out[k] === ';' || out[k] === ']' || out[k] === '[' || out[k] === '\n' || out[k] === '}' || out[k] === '?' || out[k] === ':' || k >= n) blank(i, i + 1);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return out.join('');
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// ---------------------------------------------------------------- acorn
|
|
459
|
+
|
|
460
|
+
/** Parse a handler source file. Returns { ast, kind: 'esm'|'cjs', source, stripped }. Throws Ineligible. */
|
|
461
|
+
export function parseModule(source, { file = '' } = {}) {
|
|
462
|
+
const ext = path.extname(file || '').toLowerCase();
|
|
463
|
+
let text = source;
|
|
464
|
+
let stripped = false;
|
|
465
|
+
if (TS_EXTS.has(ext)) { text = stripTypes(source); stripped = true; }
|
|
466
|
+
const opts = { ecmaVersion: 'latest', locations: true, allowHashBang: true, allowAwaitOutsideFunction: true, allowReturnOutsideFunction: false };
|
|
467
|
+
let ast;
|
|
468
|
+
let esmErr;
|
|
469
|
+
try {
|
|
470
|
+
ast = acorn.parse(text, { ...opts, sourceType: 'module' });
|
|
471
|
+
return { ast, kind: hasEsmSyntax(ast) ? 'esm' : 'cjs', source, text, stripped };
|
|
472
|
+
} catch (e) {
|
|
473
|
+
esmErr = e;
|
|
474
|
+
}
|
|
475
|
+
try {
|
|
476
|
+
ast = acorn.parse(text, { ...opts, sourceType: 'script' });
|
|
477
|
+
return { ast, kind: 'cjs', source, text, stripped };
|
|
478
|
+
} catch {
|
|
479
|
+
const line = esmErr?.loc?.line ?? null;
|
|
480
|
+
let hint = '';
|
|
481
|
+
if (stripped) hint = ' (after TypeScript type stripping)';
|
|
482
|
+
else if (ext === '.jsx' || ext === '.tsx') hint = ' (JSX is not supported in route handlers)';
|
|
483
|
+
if (/\benum\b/.test(source)) hint += '; TypeScript enums are not supported, use a const object';
|
|
484
|
+
throw new Ineligible(`parse error${hint}: ${esmErr.message.replace(/\s*\(\d+:\d+\)$/, '')}`, { file, line });
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function hasEsmSyntax(ast) {
|
|
489
|
+
return ast.body.some((s) => s.type === 'ImportDeclaration' || s.type === 'ExportNamedDeclaration' || s.type === 'ExportDefaultDeclaration' || s.type === 'ExportAllDeclaration');
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// ---------------------------------------------------------------- AST helpers
|
|
493
|
+
|
|
494
|
+
/** Visit every node (depth-first). `visit(node, parent)` returning false skips children. */
|
|
495
|
+
export function walk(node, visit, parent = null) {
|
|
496
|
+
if (!node || typeof node.type !== 'string') return;
|
|
497
|
+
if (visit(node, parent) === false) return;
|
|
498
|
+
for (const key of Object.keys(node)) {
|
|
499
|
+
if (key === 'loc' || key === 'type' || key === 'start' || key === 'end') continue;
|
|
500
|
+
const v = node[key];
|
|
501
|
+
if (Array.isArray(v)) { for (const c of v) if (c && typeof c.type === 'string') walk(c, visit, node); }
|
|
502
|
+
else if (v && typeof v.type === 'string') walk(v, visit, node);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/** Names bound by a declaration pattern. */
|
|
507
|
+
export function patternNames(p, out = []) {
|
|
508
|
+
if (!p) return out;
|
|
509
|
+
switch (p.type) {
|
|
510
|
+
case 'Identifier': out.push(p.name); break;
|
|
511
|
+
case 'ObjectPattern': for (const pr of p.properties) patternNames(pr.type === 'RestElement' ? pr.argument : pr.value, out); break;
|
|
512
|
+
case 'ArrayPattern': for (const el of p.elements) if (el) patternNames(el, out); break;
|
|
513
|
+
case 'AssignmentPattern': patternNames(p.left, out); break;
|
|
514
|
+
case 'RestElement': patternNames(p.argument, out); break;
|
|
515
|
+
default: break;
|
|
516
|
+
}
|
|
517
|
+
return out;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const FN_TYPES = new Set(['FunctionDeclaration', 'FunctionExpression', 'ArrowFunctionExpression']);
|
|
521
|
+
export const isFunctionNode = (n) => !!n && FN_TYPES.has(n.type);
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Free identifiers of a function (referenced but not declared inside it),
|
|
525
|
+
* with the AST node of the first reference. Property keys and member
|
|
526
|
+
* property names are not references.
|
|
527
|
+
*/
|
|
528
|
+
export function freeVars(fnNode) {
|
|
529
|
+
const declared = new Set();
|
|
530
|
+
// params
|
|
531
|
+
for (const p of fnNode.params) for (const nm of patternNames(p)) declared.add(nm);
|
|
532
|
+
if (fnNode.id) declared.add(fnNode.id.name);
|
|
533
|
+
// declarations inside (all scopes flattened: conservative — a name declared anywhere inside is "local")
|
|
534
|
+
walk(fnNode.body, (n) => {
|
|
535
|
+
if (n.type === 'VariableDeclaration') for (const d of n.declarations) for (const nm of patternNames(d.id)) declared.add(nm);
|
|
536
|
+
else if (n.type === 'FunctionDeclaration' || n.type === 'ClassDeclaration') { if (n.id) declared.add(n.id.name); }
|
|
537
|
+
else if (n.type === 'FunctionExpression' || n.type === 'ArrowFunctionExpression') { for (const p of n.params) for (const nm of patternNames(p)) declared.add(nm); if (n.id) declared.add(n.id.name); }
|
|
538
|
+
else if (n.type === 'CatchClause' && n.param) for (const nm of patternNames(n.param)) declared.add(nm);
|
|
539
|
+
});
|
|
540
|
+
const free = new Map();
|
|
541
|
+
const visit = (n, parent) => {
|
|
542
|
+
if (n.type === 'Identifier') {
|
|
543
|
+
if (parent) {
|
|
544
|
+
if (parent.type === 'MemberExpression' && parent.property === n && !parent.computed) return;
|
|
545
|
+
if (parent.type === 'Property' && parent.key === n && !parent.computed && !parent.shorthand) return;
|
|
546
|
+
if (parent.type === 'Property' && parent.key === n && parent.shorthand && parent.value !== n) return;
|
|
547
|
+
if (parent.type === 'MethodDefinition' && parent.key === n) return;
|
|
548
|
+
if (parent.type === 'LabeledStatement' || parent.type === 'BreakStatement' || parent.type === 'ContinueStatement') return;
|
|
549
|
+
}
|
|
550
|
+
if (!declared.has(n.name) && !free.has(n.name)) free.set(n.name, n);
|
|
551
|
+
}
|
|
552
|
+
};
|
|
553
|
+
for (const p of fnNode.params) walk(p, visit);
|
|
554
|
+
walk(fnNode.body, visit);
|
|
555
|
+
return free;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// ---------------------------------------------------------------- module shape
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Discover handlers, imports and module-scope declarations.
|
|
562
|
+
* Returns { kind, ast, source, text, imports, handlers, decls, meta, sideEffects }.
|
|
563
|
+
*/
|
|
564
|
+
export function readModule(source, { file = '' } = {}) {
|
|
565
|
+
const parsed = parseModule(source, { file });
|
|
566
|
+
const { ast, kind } = parsed;
|
|
567
|
+
const imports = [];
|
|
568
|
+
const decls = new Map(); // name → { kind, node, init, line, exported }
|
|
569
|
+
const handlers = { default: null, methods: {} };
|
|
570
|
+
const meta = {};
|
|
571
|
+
const sideEffects = [];
|
|
572
|
+
const setDecl = (name, entry) => { if (!decls.has(name)) decls.set(name, entry); };
|
|
573
|
+
const setMethod = (name, node, line) => { if (node) handlers.methods[name] = { node, line, name }; };
|
|
574
|
+
const line = (n) => n?.loc?.start?.line ?? null;
|
|
575
|
+
|
|
576
|
+
const registerVarDecl = (decl, exported = false) => {
|
|
577
|
+
for (const d of decl.declarations) {
|
|
578
|
+
// CommonJS require
|
|
579
|
+
const req = requireSource(d.init);
|
|
580
|
+
if (req) {
|
|
581
|
+
const specs = [];
|
|
582
|
+
if (d.id.type === 'Identifier') specs.push({ local: d.id.name, imported: req.member ?? '*' });
|
|
583
|
+
else if (d.id.type === 'ObjectPattern') {
|
|
584
|
+
for (const p of d.id.properties) {
|
|
585
|
+
if (p.type === 'RestElement') continue;
|
|
586
|
+
const imported = p.key.type === 'Identifier' ? p.key.name : String(p.key.value);
|
|
587
|
+
const local = p.value.type === 'Identifier' ? p.value.name : p.value.type === 'AssignmentPattern' && p.value.left.type === 'Identifier' ? p.value.left.name : null;
|
|
588
|
+
if (local) specs.push({ local, imported });
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
imports.push({ source: req.source, specifiers: specs, line: line(d) });
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
if (d.id.type === 'Identifier') {
|
|
595
|
+
setDecl(d.id.name, { kind: decl.kind, node: d, init: d.init, line: line(d), exported });
|
|
596
|
+
} else {
|
|
597
|
+
for (const nm of patternNames(d.id)) setDecl(nm, { kind: decl.kind, node: d, init: null, pattern: d.id, line: line(d), exported });
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
for (const s of ast.body) {
|
|
603
|
+
switch (s.type) {
|
|
604
|
+
case 'ImportDeclaration': {
|
|
605
|
+
if (s.importKind === 'type') break;
|
|
606
|
+
const specs = s.specifiers.map((sp) => ({
|
|
607
|
+
local: sp.local.name,
|
|
608
|
+
imported: sp.type === 'ImportDefaultSpecifier' ? 'default' : sp.type === 'ImportNamespaceSpecifier' ? '*' : (sp.imported.name ?? sp.imported.value),
|
|
609
|
+
}));
|
|
610
|
+
imports.push({ source: s.source.value, specifiers: specs, line: line(s) });
|
|
611
|
+
break;
|
|
612
|
+
}
|
|
613
|
+
case 'ExportDefaultDeclaration': {
|
|
614
|
+
handlers.default = { node: s.declaration, line: line(s), name: 'default' };
|
|
615
|
+
if (s.declaration.type === 'FunctionDeclaration' && s.declaration.id) setDecl(s.declaration.id.name, { kind: 'function', node: s.declaration, line: line(s), exported: true });
|
|
616
|
+
break;
|
|
617
|
+
}
|
|
618
|
+
case 'ExportNamedDeclaration': {
|
|
619
|
+
if (s.declaration) {
|
|
620
|
+
const d = s.declaration;
|
|
621
|
+
if (d.type === 'FunctionDeclaration') {
|
|
622
|
+
setDecl(d.id.name, { kind: 'function', node: d, line: line(d), exported: true });
|
|
623
|
+
if (HTTP_METHODS.includes(d.id.name)) setMethod(d.id.name, d, line(d));
|
|
624
|
+
} else if (d.type === 'VariableDeclaration') {
|
|
625
|
+
registerVarDecl(d, true);
|
|
626
|
+
for (const v of d.declarations) {
|
|
627
|
+
if (v.id.type !== 'Identifier') continue;
|
|
628
|
+
if (HTTP_METHODS.includes(v.id.name)) setMethod(v.id.name, v.init, line(v));
|
|
629
|
+
else if (['config', 'runtime', 'dynamic', 'revalidate', 'maxDuration', 'preferredRegion', 'fetchCache', 'dynamicParams'].includes(v.id.name)) meta[v.id.name] = literalValue(v.init);
|
|
630
|
+
}
|
|
631
|
+
} else if (d.type === 'ClassDeclaration') {
|
|
632
|
+
setDecl(d.id.name, { kind: 'class', node: d, line: line(d), exported: true });
|
|
633
|
+
}
|
|
634
|
+
} else {
|
|
635
|
+
for (const sp of s.specifiers) {
|
|
636
|
+
const exportedName = sp.exported.name ?? sp.exported.value;
|
|
637
|
+
const localName = sp.local.name ?? sp.local.value;
|
|
638
|
+
if (exportedName === 'default') handlers.default = { node: { type: 'Identifier', name: localName, loc: sp.loc }, line: line(sp), name: 'default' };
|
|
639
|
+
else if (HTTP_METHODS.includes(exportedName)) setMethod(exportedName, { type: 'Identifier', name: localName, loc: sp.loc }, line(sp));
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
break;
|
|
643
|
+
}
|
|
644
|
+
case 'ExportAllDeclaration': sideEffects.push({ node: s, what: 'export * from' }); break;
|
|
645
|
+
case 'FunctionDeclaration': setDecl(s.id.name, { kind: 'function', node: s, line: line(s) }); break;
|
|
646
|
+
case 'ClassDeclaration': setDecl(s.id.name, { kind: 'class', node: s, line: line(s) }); break;
|
|
647
|
+
case 'VariableDeclaration': registerVarDecl(s); break;
|
|
648
|
+
case 'ExpressionStatement': {
|
|
649
|
+
const e = s.expression;
|
|
650
|
+
// module.exports = X ; exports.X = Y ; module.exports.X = Y
|
|
651
|
+
if (e.type === 'AssignmentExpression' && e.operator === '=') {
|
|
652
|
+
const target = memberPath(e.left);
|
|
653
|
+
if (target && (target[0] === 'module' && target[1] === 'exports' || target[0] === 'exports')) {
|
|
654
|
+
const rest = target[0] === 'module' ? target.slice(2) : target.slice(1);
|
|
655
|
+
if (rest.length === 0) {
|
|
656
|
+
// module.exports = handler | { GET, POST }
|
|
657
|
+
if (e.right.type === 'ObjectExpression') {
|
|
658
|
+
for (const p of e.right.properties) {
|
|
659
|
+
if (p.type !== 'Property') continue;
|
|
660
|
+
const k = p.key.name ?? p.key.value;
|
|
661
|
+
if (k === 'default') handlers.default = { node: p.value, line: line(p), name: 'default' };
|
|
662
|
+
else if (HTTP_METHODS.includes(k)) setMethod(k, p.value, line(p));
|
|
663
|
+
}
|
|
664
|
+
} else handlers.default = { node: e.right, line: line(s), name: 'default' };
|
|
665
|
+
} else if (rest.length === 1) {
|
|
666
|
+
if (rest[0] === 'default') handlers.default = { node: e.right, line: line(s), name: 'default' };
|
|
667
|
+
else if (HTTP_METHODS.includes(rest[0])) setMethod(rest[0], e.right, line(s));
|
|
668
|
+
else if (rest[0] === 'config') meta.config = literalValue(e.right);
|
|
669
|
+
}
|
|
670
|
+
break;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
if (e.type === 'Literal' && typeof e.value === 'string') break; // 'use strict'
|
|
674
|
+
sideEffects.push({ node: s, what: 'top-level statement' });
|
|
675
|
+
break;
|
|
676
|
+
}
|
|
677
|
+
case 'EmptyStatement': break;
|
|
678
|
+
default: sideEffects.push({ node: s, what: `top-level ${s.type}` });
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
// record whether module-scope bindings are reassigned anywhere
|
|
682
|
+
const reassigned = new Set();
|
|
683
|
+
walk(ast, (n) => {
|
|
684
|
+
if (n.type === 'AssignmentExpression' || n.type === 'UpdateExpression') {
|
|
685
|
+
const t = n.type === 'AssignmentExpression' ? n.left : n.argument;
|
|
686
|
+
if (t.type === 'Identifier') reassigned.add(t.name);
|
|
687
|
+
for (const nm of (t.type === 'ObjectPattern' || t.type === 'ArrayPattern') ? patternNames(t) : []) reassigned.add(nm);
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
for (const [name, d] of decls) d.reassigned = reassigned.has(name);
|
|
691
|
+
return { ...parsed, file, imports, handlers, decls, meta, sideEffects };
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function memberPath(n) {
|
|
695
|
+
const out = [];
|
|
696
|
+
while (n) {
|
|
697
|
+
if (n.type === 'Identifier') { out.unshift(n.name); return out; }
|
|
698
|
+
if (n.type === 'MemberExpression' && !n.computed && n.property.type === 'Identifier') { out.unshift(n.property.name); n = n.object; continue; }
|
|
699
|
+
if (n.type === 'MemberExpression' && n.computed && n.property.type === 'Literal') { out.unshift(String(n.property.value)); n = n.object; continue; }
|
|
700
|
+
return null;
|
|
701
|
+
}
|
|
702
|
+
return null;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function requireSource(init) {
|
|
706
|
+
if (!init) return null;
|
|
707
|
+
if (init.type === 'CallExpression' && init.callee.type === 'Identifier' && init.callee.name === 'require' && init.arguments[0]?.type === 'Literal') {
|
|
708
|
+
return { source: String(init.arguments[0].value), member: null };
|
|
709
|
+
}
|
|
710
|
+
if (init.type === 'MemberExpression' && !init.computed && init.object.type === 'CallExpression') {
|
|
711
|
+
const inner = requireSource(init.object);
|
|
712
|
+
if (inner) return { source: inner.source, member: init.property.name };
|
|
713
|
+
}
|
|
714
|
+
return null;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/** Literal / plain-object value of a config export, or undefined. */
|
|
718
|
+
export function literalValue(n) {
|
|
719
|
+
if (!n) return undefined;
|
|
720
|
+
switch (n.type) {
|
|
721
|
+
case 'Literal': return n.value;
|
|
722
|
+
case 'TemplateLiteral': return n.expressions.length ? undefined : n.quasis.map((q) => q.value.cooked).join('');
|
|
723
|
+
case 'ObjectExpression': {
|
|
724
|
+
const o = {};
|
|
725
|
+
for (const p of n.properties) { if (p.type !== 'Property') continue; o[p.key.name ?? p.key.value] = literalValue(p.value); }
|
|
726
|
+
return o;
|
|
727
|
+
}
|
|
728
|
+
case 'ArrayExpression': return n.elements.map(literalValue);
|
|
729
|
+
case 'UnaryExpression': return n.operator === '-' && n.argument.type === 'Literal' ? -n.argument.value : undefined;
|
|
730
|
+
default: return undefined;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/** Resolve a handler reference (identifier → its declaration's function node). */
|
|
735
|
+
export function resolveHandlerNode(ref, mod) {
|
|
736
|
+
let node = ref?.node;
|
|
737
|
+
if (!node) return null;
|
|
738
|
+
if (node.type === 'Identifier') {
|
|
739
|
+
const d = mod.decls.get(node.name);
|
|
740
|
+
if (!d) throw new Ineligible(`handler \`${node.name}\` is exported but never declared in this file`, { file: mod.file, node });
|
|
741
|
+
if (d.kind === 'function') return d.node;
|
|
742
|
+
if (d.init && isFunctionNode(d.init)) return d.init;
|
|
743
|
+
if (d.init?.type === 'CallExpression') throw new Ineligible(`handler \`${node.name}\` is wrapped in a call (${describeCallee(d.init)}); higher-order wrappers are not transmuted, export the plain handler`, { file: mod.file, node: d.init });
|
|
744
|
+
throw new Ineligible(`handler \`${node.name}\` is not a function`, { file: mod.file, node: d.node });
|
|
745
|
+
}
|
|
746
|
+
if (isFunctionNode(node)) return node;
|
|
747
|
+
if (node.type === 'CallExpression') throw new Ineligible(`the exported handler is wrapped in a call (${describeCallee(node)}); higher-order wrappers are not transmuted, export the plain handler`, { file: mod.file, node });
|
|
748
|
+
throw new Ineligible(`the exported handler is a ${node.type}, not a function`, { file: mod.file, node });
|
|
749
|
+
}
|
|
750
|
+
function describeCallee(call) {
|
|
751
|
+
const p = memberPath(call.callee);
|
|
752
|
+
return p ? p.join('.') + '(...)' : 'call expression';
|
|
753
|
+
}
|