@jigs.design/compiler 0.0.0-stage → 0.1.1

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/dist/script.js ADDED
@@ -0,0 +1,695 @@
1
+ import { parse } from "acorn";
2
+ //#region src/script.ts
3
+ const OPTIONS = {
4
+ ecmaVersion: "latest",
5
+ preserveParens: true,
6
+ sourceType: "script",
7
+ strict: true
8
+ };
9
+ /** `.constructor` and friends reach the realm's internals. */
10
+ const FORBIDDEN_MEMBERS = /* @__PURE__ */ new Set([
11
+ "__proto__",
12
+ "constructor",
13
+ "prototype"
14
+ ]);
15
+ /** Calls whose result changes on every render. */
16
+ const UNSTABLE_CALLS = /* @__PURE__ */ new Map([
17
+ ["Date", /* @__PURE__ */ new Set(["now"])],
18
+ ["Math", /* @__PURE__ */ new Set(["random"])],
19
+ ["performance", /* @__PURE__ */ new Set(["now"])]
20
+ ]);
21
+ /** Calls that read the clock, which rounding can make stable. */
22
+ const CLOCK_CALLS = /* @__PURE__ */ new Set(["Date.now", "performance.now"]);
23
+ /** `Math` functions that round a number, as in `Math.floor(Date.now() / DAY)`. */
24
+ const ROUNDING = /* @__PURE__ */ new Set([
25
+ "ceil",
26
+ "floor",
27
+ "round",
28
+ "trunc"
29
+ ]);
30
+ /**
31
+ * Reads of `new Date()` that give the time to the second or finer. Others,
32
+ * such as `.getMonth()` or `.toISOString().slice(0, 10)`, can be stable, so
33
+ * they are left to the runtime, which stops a query whose input keeps changing.
34
+ */
35
+ const FINE_DATE_READS = /* @__PURE__ */ new Set([
36
+ "getMilliseconds",
37
+ "getSeconds",
38
+ "getTime",
39
+ "getUTCMilliseconds",
40
+ "getUTCSeconds",
41
+ "valueOf"
42
+ ]);
43
+ /** `new Date()` with no arguments: the current time. */
44
+ const isNewDate = (node) => node.type === "NewExpression" && node.callee.type === "Identifier" && node.callee.name === "Date" && node.arguments.length === 0;
45
+ /** `Math.floor(… / …)` and the like: a quotient rounded to a whole number. */
46
+ function roundsQuotient(node) {
47
+ const [argument] = node.arguments;
48
+ const inner = argument?.type === "ParenthesizedExpression" ? argument.expression : argument;
49
+ return node.callee.type === "MemberExpression" && !node.callee.computed && node.callee.object.type === "Identifier" && node.callee.object.name === "Math" && node.callee.property.type === "Identifier" && ROUNDING.has(node.callee.property.name) && inner?.type === "BinaryExpression" && inner.operator === "/";
50
+ }
51
+ const LOOPS = {
52
+ DoWhileStatement: "do…while",
53
+ ForInStatement: "for…in",
54
+ ForOfStatement: "for…of",
55
+ ForStatement: "for",
56
+ WhileStatement: "while"
57
+ };
58
+ const unsupported = (syntax, advice) => `${syntax} isn't part of Pickle's JavaScript: ${advice}`;
59
+ const MESSAGES = {
60
+ class: unsupported("`class`", "use plain objects and arrow functions"),
61
+ fetch: "There is no `fetch` in Pickle: read data with `useQuery` and a query the prompt lists",
62
+ function: (name) => unsupported(`\`function ${name}()\``, `write \`const ${name} = (…) => …\` instead`),
63
+ getter: unsupported("A getter or setter", "compute the value in a {@body} line instead"),
64
+ import: unsupported("`import()`", "everything a spec can use is built in"),
65
+ label: unsupported("A label", "use array methods instead of loops"),
66
+ loop: (keyword) => unsupported(`A \`${keyword}\` loop`, "repeat markup with <each>, and compute values with .map, .filter or .reduce"),
67
+ member: (name) => unsupported(`\`${name}\``, "read the fields you need directly"),
68
+ queryOption: (key) => `\`useQuery\` takes only \`{ enabled }\` as its third argument; "${key}" does nothing`,
69
+ regex: unsupported("A regular expression literal", "use string methods such as .includes, .startsWith, .split or .replaceAll"),
70
+ this: unsupported("`this`", "it is undefined in a spec"),
71
+ try: unsupported("`try`", "an error a line throws is reported on that line, and the rest of the page still renders"),
72
+ unstable: (call) => `\`${call}\` in a useQuery input changes on every render, so the query starts again each time and never shows data. Keep the value in state, e.g. \`{@body const [now] = useState(${call})}\`, and build the input from it`,
73
+ variable: (kind) => unsupported(`\`${kind}\``, "declare values with `const`, and keep values that change in useState")
74
+ };
75
+ /** The parser's message without its `(line:column)` suffix. */
76
+ function syntaxMessage(error) {
77
+ if (typeof error === "object" && error !== null && "message" in error && typeof error.message === "string") {
78
+ const offset = "pos" in error && typeof error.pos === "number" ? error.pos : 0;
79
+ return {
80
+ message: error.message.replace(/\s*\(\d+:\d+\)$/, ""),
81
+ offset
82
+ };
83
+ }
84
+ return {
85
+ message: String(error),
86
+ offset: 0
87
+ };
88
+ }
89
+ const isNode = (value) => typeof value === "object" && value !== null && "type" in value && typeof value.type === "string";
90
+ /** Child nodes in source order, for node types with no scoping of their own. */
91
+ function childNodes(node) {
92
+ const out = [];
93
+ for (const [key, value] of Object.entries(node)) {
94
+ if (key === "type" || key === "start" || key === "end") continue;
95
+ if (Array.isArray(value)) {
96
+ for (const item of value) if (isNode(item)) out.push(item);
97
+ } else if (isNode(value)) out.push(value);
98
+ }
99
+ return out;
100
+ }
101
+ function patternNames(pattern, out) {
102
+ if (pattern === null || pattern === void 0) return;
103
+ switch (pattern.type) {
104
+ case "Identifier":
105
+ out.push(pattern.name);
106
+ return;
107
+ case "ObjectPattern":
108
+ for (const property of pattern.properties) patternNames(property.type === "RestElement" ? property.argument : property.value, out);
109
+ return;
110
+ case "ArrayPattern":
111
+ for (const element of pattern.elements) patternNames(element, out);
112
+ return;
113
+ case "RestElement":
114
+ patternNames(pattern.argument, out);
115
+ return;
116
+ case "AssignmentPattern":
117
+ patternNames(pattern.left, out);
118
+ return;
119
+ case "MemberExpression": return;
120
+ }
121
+ }
122
+ /** `let`, `const`, `class` and `function` declared directly in `body`. */
123
+ function lexicalNames(body) {
124
+ const names = [];
125
+ for (const statement of body) if (statement.type === "VariableDeclaration" && statement.kind !== "var") for (const declarator of statement.declarations) patternNames(declarator.id, names);
126
+ else if ((statement.type === "FunctionDeclaration" || statement.type === "ClassDeclaration") && statement.id !== null) names.push(statement.id.name);
127
+ return names;
128
+ }
129
+ const isFunction = (node) => node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression";
130
+ /** `var` declarations anywhere in `node`, not counting nested functions. */
131
+ function varNames(node, out) {
132
+ if (node.type === "VariableDeclaration" && node.kind === "var") for (const declarator of node.declarations) patternNames(declarator.id, out);
133
+ for (const child of childNodes(node)) if (!isFunction(child)) varNames(child, out);
134
+ }
135
+ /** The name a callee is written as, bare or as `Pickle.name`. */
136
+ function calleeName(node) {
137
+ return node.type === "Identifier" ? node.name : node.type === "MemberExpression" && !node.computed && node.object.type === "Identifier" && node.object.name === "Pickle" && node.property.type === "Identifier" ? node.property.name : null;
138
+ }
139
+ /** `useQuery`, bare or as `Pickle.useQuery`. */
140
+ function hookCallee(node) {
141
+ const name = calleeName(node);
142
+ return name === "useMutation" || name === "useQuery" || name === "useState" ? name : null;
143
+ }
144
+ /** A procedure call's callee: a query or mutation hook, or `useAction`, which models sometimes guess. */
145
+ function procedureCallee(node) {
146
+ const name = calleeName(node);
147
+ return name === "useAction" || name === "useMutation" || name === "useQuery" ? name : null;
148
+ }
149
+ /** An object literal's key written out as a name, string or number. */
150
+ function keyName(node) {
151
+ if (node.type === "Identifier") return node.name;
152
+ return node.type === "Literal" && node.value !== null ? String(node.value) : null;
153
+ }
154
+ /** The text of a string argument: a quoted literal or a template with no `${}`. */
155
+ function stringArgument(node) {
156
+ if (node?.type === "Literal" && node.raw !== void 0 && /^["']/.test(node.raw)) return String(node.value);
157
+ if (node?.type === "TemplateLiteral" && node.expressions.length === 0) return node.quasis[0]?.value.cooked ?? null;
158
+ return null;
159
+ }
160
+ function resolves(scope, name) {
161
+ for (let current = scope; current !== null; current = current.parent) if (current.names.has(name)) return true;
162
+ return false;
163
+ }
164
+ function propertyName(node) {
165
+ if (!node.computed && node.property.type === "Identifier") return node.property.name;
166
+ if (node.computed && node.property.type === "Literal" && typeof node.property.value === "string") return node.property.value;
167
+ return null;
168
+ }
169
+ /** Array methods that return undefined when nothing is there. */
170
+ const MAYBE_UNDEFINED_CALLS = /* @__PURE__ */ new Set([
171
+ "at",
172
+ "find",
173
+ "findLast"
174
+ ]);
175
+ /**
176
+ * Whether `node` may evaluate to undefined though it reads as a value: a
177
+ * `find`, `findLast` or `at` call, an index such as `items[0]`, or a
178
+ * condition, `&&`, `||` or `??` one of whose results is one.
179
+ */
180
+ function mayBeUndefined(node) {
181
+ switch (node.type) {
182
+ case "ParenthesizedExpression": return mayBeUndefined(node.expression);
183
+ case "ChainExpression": return mayBeUndefined(node.expression);
184
+ case "CallExpression": {
185
+ const name = node.callee.type === "MemberExpression" ? propertyName(node.callee) : null;
186
+ return name !== null && MAYBE_UNDEFINED_CALLS.has(name);
187
+ }
188
+ case "MemberExpression": return node.computed && propertyName(node) === null;
189
+ case "ConditionalExpression": return mayBeUndefined(node.consequent) || mayBeUndefined(node.alternate);
190
+ case "LogicalExpression": return mayBeUndefined(node.right) || node.operator === "&&" && mayBeUndefined(node.left);
191
+ default: return false;
192
+ }
193
+ }
194
+ const isNullLiteral = (node) => node.type === "Literal" && node.raw === "null";
195
+ /** `text` is what was parsed, less the `shift` an expression's wrapping adds. */
196
+ function collectFacts(program, shift, text) {
197
+ const facts = {
198
+ calls: [],
199
+ chains: [],
200
+ declared: [],
201
+ hookCalls: [],
202
+ hooks: [],
203
+ hookValues: [],
204
+ issues: [],
205
+ keyed: [],
206
+ maybeUndefined: [],
207
+ members: [],
208
+ nullCompares: [],
209
+ procedures: [],
210
+ references: []
211
+ };
212
+ const at = (node) => Math.max(0, node.start - shift);
213
+ const until = (node) => Math.max(0, node.end - shift);
214
+ /**
215
+ * The calls a top-level declaration's initializer evaluates first and
216
+ * unconditionally: the initializer itself, or what it reads a member of,
217
+ * compares, or tests with `||`, `??` or `&&`, as in
218
+ * `useQuery("a").data ?? []`. A hook call there is at the top; one
219
+ * anywhere else is not.
220
+ */
221
+ const topCalls = /* @__PURE__ */ new Set();
222
+ for (const statement of program.body) {
223
+ if (statement.type !== "VariableDeclaration") continue;
224
+ for (const declarator of statement.declarations) {
225
+ let root = declarator.init;
226
+ while (root !== null && root !== void 0) {
227
+ if (root.type === "CallExpression") {
228
+ topCalls.add(root);
229
+ break;
230
+ }
231
+ root = root.type === "MemberExpression" ? root.object : root.type === "LogicalExpression" || root.type === "BinaryExpression" ? root.left : root.type === "ParenthesizedExpression" || root.type === "ChainExpression" ? root.expression : null;
232
+ }
233
+ }
234
+ }
235
+ const issue = (node, message, code = "unsupported_syntax") => {
236
+ facts.issues.push({
237
+ code,
238
+ end: until(node),
239
+ message,
240
+ offset: at(node)
241
+ });
242
+ };
243
+ let queryInput = 0;
244
+ /** Inside a rounded quotient, where reading the clock gives a stable value. */
245
+ let rounded = 0;
246
+ const unstable = (node, call, clock) => {
247
+ if (queryInput > 0 && !(clock && rounded > 0)) issue(node, MESSAGES.unstable(call), "unstable_input");
248
+ };
249
+ const reference = (name, node, scope, deferred) => {
250
+ if (!resolves(scope, name)) facts.references.push({
251
+ deferred,
252
+ end: until(node),
253
+ name,
254
+ offset: at(node)
255
+ });
256
+ };
257
+ /** Records `node` when it compares a free name strictly with null. */
258
+ const nullCompare = (node, scope) => {
259
+ if (node.operator !== "!==" && node.operator !== "===") return;
260
+ const named = isNullLiteral(node.right) ? node.left : isNullLiteral(node.left) ? node.right : null;
261
+ if (named?.type !== "Identifier" || resolves(scope, named.name)) return;
262
+ const operatorOffset = text.indexOf(node.operator, until(node.left));
263
+ if (operatorOffset === -1) return;
264
+ facts.nullCompares.push({
265
+ end: until(node),
266
+ name: named.name,
267
+ offset: at(node),
268
+ operator: node.operator,
269
+ operatorEnd: operatorOffset + node.operator.length,
270
+ operatorOffset
271
+ });
272
+ };
273
+ /** Members that are the object of another, whose chain the outer one records. */
274
+ const nested = /* @__PURE__ */ new WeakSet();
275
+ /** Records the chain `node` ends, when it starts at a free name. */
276
+ const chain = (node, scope) => {
277
+ const members = [];
278
+ let current = node;
279
+ while (current.type === "MemberExpression") {
280
+ members.unshift(current);
281
+ current = current.object;
282
+ }
283
+ if (current.type !== "Identifier" || resolves(scope, current.name)) return;
284
+ const path = [];
285
+ const ends = [];
286
+ for (const member of members) {
287
+ const name = propertyName(member);
288
+ if (name === null) break;
289
+ path.push(name);
290
+ ends.push(until(member));
291
+ }
292
+ if (path.length > 0) facts.chains.push({
293
+ ends,
294
+ object: current.name,
295
+ offset: at(current),
296
+ path
297
+ });
298
+ };
299
+ const visitMember = (node, scope, deferred, called) => {
300
+ const property = propertyName(node);
301
+ if (property !== null && FORBIDDEN_MEMBERS.has(property)) issue(node, MESSAGES.member(`.${property}`));
302
+ if (!nested.has(node)) chain(node, scope);
303
+ if (node.object.type === "MemberExpression") nested.add(node.object);
304
+ if (property !== null && !node.computed && node.object.type === "Identifier" && !resolves(scope, node.object.name)) facts.members.push({
305
+ called,
306
+ deferred,
307
+ end: until(node),
308
+ object: node.object.name,
309
+ offset: at(node),
310
+ property
311
+ });
312
+ if (called && property !== null && node.object.type === "Identifier" && UNSTABLE_CALLS.get(node.object.name)?.has(property) === true) {
313
+ const call = `${node.object.name}.${property}`;
314
+ unstable(node, `${call}()`, CLOCK_CALLS.has(call));
315
+ }
316
+ if (called && property !== null && FINE_DATE_READS.has(property) && isNewDate(node.object)) unstable(node, `new Date().${property}()`, true);
317
+ visit(node.object, scope, deferred);
318
+ if (node.computed) visit(node.property, scope, deferred);
319
+ };
320
+ /** Default values and computed keys in a binding pattern; the names themselves are declared elsewhere. */
321
+ const visitPattern = (pattern, scope, deferred) => {
322
+ if (pattern === null || pattern === void 0) return;
323
+ switch (pattern.type) {
324
+ case "Identifier": return;
325
+ case "ObjectPattern":
326
+ for (const property of pattern.properties) {
327
+ if (property.type === "RestElement") {
328
+ visitPattern(property.argument, scope, deferred);
329
+ continue;
330
+ }
331
+ if (property.computed) visit(property.key, scope, deferred);
332
+ visitPattern(property.value, scope, deferred);
333
+ }
334
+ return;
335
+ case "ArrayPattern":
336
+ for (const element of pattern.elements) visitPattern(element, scope, deferred);
337
+ return;
338
+ case "RestElement":
339
+ visitPattern(pattern.argument, scope, deferred);
340
+ return;
341
+ case "AssignmentPattern":
342
+ visitPattern(pattern.left, scope, deferred);
343
+ visit(pattern.right, scope, deferred);
344
+ return;
345
+ case "MemberExpression":
346
+ visitMember(pattern, scope, deferred, false);
347
+ return;
348
+ }
349
+ };
350
+ /** The left of `=`: a name written to is read from scope like any other. */
351
+ const visitTarget = (target, scope, deferred) => {
352
+ const names = [];
353
+ patternNames(target, names);
354
+ for (const name of names) reference(name, target, scope, deferred);
355
+ visitPattern(target, scope, deferred);
356
+ };
357
+ const visitFunction = (node, scope) => {
358
+ const names = [];
359
+ if (node.type === "FunctionExpression" && node.id) names.push(node.id.name);
360
+ for (const param of node.params) patternNames(param, names);
361
+ if (node.body.type === "BlockStatement") {
362
+ varNames(node.body, names);
363
+ names.push(...lexicalNames(node.body.body));
364
+ }
365
+ const inner = {
366
+ names: new Set(names),
367
+ parent: scope
368
+ };
369
+ for (const param of node.params) visitPattern(param, inner, true);
370
+ if (node.body.type === "BlockStatement") for (const statement of node.body.body) visit(statement, inner, true);
371
+ else visit(node.body, inner, true);
372
+ };
373
+ const visitCall = (node, scope, deferred) => {
374
+ const callee = node.callee;
375
+ const hook = hookCallee(callee);
376
+ if (callee.type === "Identifier") {
377
+ if (!resolves(scope, callee.name)) {
378
+ facts.calls.push({
379
+ callee: callee.name,
380
+ deferred,
381
+ end: until(node),
382
+ offset: at(node)
383
+ });
384
+ if (callee.name === "fetch") issue(node, MESSAGES.fetch);
385
+ }
386
+ if (hook === null || resolves(scope, callee.name)) visit(callee, scope, deferred);
387
+ else reference(callee.name, callee, scope, deferred);
388
+ } else if (callee.type === "MemberExpression") visitMember(callee, scope, deferred, true);
389
+ else visit(callee, scope, deferred);
390
+ const free = hook !== null && (callee.type !== "Identifier" || !resolves(scope, callee.name));
391
+ if (free) facts.hookCalls.push({
392
+ end: until(node),
393
+ hook,
394
+ offset: at(node),
395
+ place: deferred ? "handler" : topCalls.has(node) ? "top" : "nested",
396
+ prefixed: callee.type === "MemberExpression"
397
+ });
398
+ const procedure = procedureCallee(callee);
399
+ if (procedure !== null) {
400
+ const [first] = node.arguments;
401
+ facts.procedures.push({
402
+ end: until(node),
403
+ hook: procedure,
404
+ name: stringArgument(first),
405
+ offset: at(node),
406
+ tanstack: first?.type === "ObjectExpression"
407
+ });
408
+ }
409
+ if (hook === "useMutation" || hook === "useState") {
410
+ const last = node.arguments.at(-1);
411
+ facts.keyed.push({
412
+ arguments: node.arguments.length,
413
+ hook,
414
+ insertAt: (last === void 0 ? node.end - 1 : last.end) - shift,
415
+ start: at(node)
416
+ });
417
+ }
418
+ const isQuery = hook === "useQuery";
419
+ const options = node.arguments[2];
420
+ if (free && isQuery && options?.type === "ObjectExpression") for (const property of options.properties) {
421
+ const key = property.type === "Property" && !property.computed ? keyName(property.key) : null;
422
+ if (key !== null && key !== "enabled") issue(property, MESSAGES.queryOption(key), "query_options");
423
+ }
424
+ const rounds = roundsQuotient(node) ? 1 : 0;
425
+ rounded += rounds;
426
+ for (const [index, argument] of node.arguments.entries()) {
427
+ const input = isQuery && index === 1;
428
+ queryInput += input ? 1 : 0;
429
+ visit(argument, scope, deferred);
430
+ queryInput -= input ? 1 : 0;
431
+ }
432
+ rounded -= rounds;
433
+ };
434
+ const visitBlock = (body, scope, deferred) => {
435
+ const inner = {
436
+ names: new Set(lexicalNames(body)),
437
+ parent: scope
438
+ };
439
+ for (const statement of body) visit(statement, inner, deferred);
440
+ };
441
+ function visit(node, scope, deferred) {
442
+ switch (node.type) {
443
+ case "Identifier": {
444
+ const hook = hookCallee(node);
445
+ if (hook !== null && !resolves(scope, node.name)) facts.hookValues.push({
446
+ end: until(node),
447
+ hook,
448
+ offset: at(node)
449
+ });
450
+ reference(node.name, node, scope, deferred);
451
+ return;
452
+ }
453
+ case "MemberExpression":
454
+ visitMember(node, scope, deferred, false);
455
+ return;
456
+ case "CallExpression":
457
+ visitCall(node, scope, deferred);
458
+ return;
459
+ case "ThisExpression":
460
+ issue(node, MESSAGES.this);
461
+ return;
462
+ case "Literal":
463
+ if (node.regex !== void 0) issue(node, MESSAGES.regex);
464
+ return;
465
+ case "FunctionDeclaration":
466
+ issue(node, MESSAGES.function(node.id?.name ?? "name"));
467
+ visitFunction(node, scope);
468
+ return;
469
+ case "FunctionExpression":
470
+ case "ArrowFunctionExpression":
471
+ visitFunction(node, scope);
472
+ return;
473
+ case "ClassDeclaration":
474
+ case "ClassExpression": {
475
+ issue(node, MESSAGES.class);
476
+ const names = node.id ? [node.id.name] : [];
477
+ const inner = {
478
+ names: new Set(names),
479
+ parent: scope
480
+ };
481
+ if (node.superClass) visit(node.superClass, scope, deferred);
482
+ for (const member of node.body.body) {
483
+ if (member.type === "StaticBlock") {
484
+ visitBlock(member.body, inner, true);
485
+ continue;
486
+ }
487
+ if (member.computed) visit(member.key, inner, deferred);
488
+ if (member.value) visit(member.value, inner, true);
489
+ }
490
+ return;
491
+ }
492
+ case "BinaryExpression":
493
+ nullCompare(node, scope);
494
+ visit(node.left, scope, deferred);
495
+ visit(node.right, scope, deferred);
496
+ return;
497
+ case "VariableDeclaration":
498
+ if (node.kind === "var" || node.kind === "let") issue(node, MESSAGES.variable(node.kind));
499
+ for (const declarator of node.declarations) {
500
+ visitPattern(declarator.id, scope, deferred);
501
+ if (declarator.init) visit(declarator.init, scope, deferred);
502
+ }
503
+ return;
504
+ case "Property":
505
+ if (node.kind !== "init") issue(node, MESSAGES.getter);
506
+ if (!node.computed && (node.key.type === "Identifier" && node.key.name === "__proto__" || node.key.type === "Literal" && node.key.value === "__proto__")) issue(node, MESSAGES.member("__proto__"));
507
+ if (node.computed) visit(node.key, scope, deferred);
508
+ visit(node.value, scope, deferred);
509
+ return;
510
+ case "AssignmentExpression":
511
+ if (node.left.type === "MemberExpression") visitMember(node.left, scope, deferred, false);
512
+ else visitTarget(node.left, scope, deferred);
513
+ visit(node.right, scope, deferred);
514
+ return;
515
+ case "BlockStatement":
516
+ case "StaticBlock":
517
+ visitBlock(node.body, scope, deferred);
518
+ return;
519
+ case "SwitchStatement": {
520
+ visit(node.discriminant, scope, deferred);
521
+ const inner = {
522
+ names: new Set(lexicalNames(node.cases.flatMap((entry) => entry.consequent))),
523
+ parent: scope
524
+ };
525
+ for (const entry of node.cases) {
526
+ if (entry.test) visit(entry.test, inner, deferred);
527
+ for (const statement of entry.consequent) visit(statement, inner, deferred);
528
+ }
529
+ return;
530
+ }
531
+ case "TryStatement":
532
+ issue(node, MESSAGES.try);
533
+ visit(node.block, scope, deferred);
534
+ if (node.handler) {
535
+ const names = [];
536
+ patternNames(node.handler.param, names);
537
+ const inner = {
538
+ names: new Set(names),
539
+ parent: scope
540
+ };
541
+ visitPattern(node.handler.param, inner, deferred);
542
+ visit(node.handler.body, inner, deferred);
543
+ }
544
+ if (node.finalizer) visit(node.finalizer, scope, deferred);
545
+ return;
546
+ case "ForStatement":
547
+ case "ForInStatement":
548
+ case "ForOfStatement":
549
+ case "WhileStatement":
550
+ case "DoWhileStatement": {
551
+ issue(node, MESSAGES.loop(LOOPS[node.type]));
552
+ const head = node.type === "ForStatement" ? node.init : node.type === "ForInStatement" || node.type === "ForOfStatement" ? node.left : null;
553
+ const inner = {
554
+ names: new Set(head?.type === "VariableDeclaration" ? lexicalNames([head]) : []),
555
+ parent: scope
556
+ };
557
+ const target = (node.type === "ForInStatement" || node.type === "ForOfStatement") && node.left.type !== "VariableDeclaration" ? node.left : null;
558
+ for (const child of childNodes(node)) {
559
+ if (target !== null && child === target) {
560
+ visitTarget(target, inner, deferred);
561
+ continue;
562
+ }
563
+ visit(child, inner, deferred);
564
+ }
565
+ return;
566
+ }
567
+ case "LabeledStatement":
568
+ issue(node, MESSAGES.label);
569
+ visit(node.body, scope, deferred);
570
+ return;
571
+ case "ImportExpression":
572
+ issue(node, MESSAGES.import);
573
+ visit(node.source, scope, deferred);
574
+ return;
575
+ case "BreakStatement":
576
+ case "ContinueStatement":
577
+ case "MetaProperty":
578
+ case "PrivateIdentifier":
579
+ case "Super":
580
+ case "TemplateElement": return;
581
+ default: for (const child of childNodes(node)) visit(child, scope, deferred);
582
+ }
583
+ }
584
+ const top = [...lexicalNames(program.body)];
585
+ varNames(program, top);
586
+ facts.declared = [...new Set(top)];
587
+ const root = {
588
+ names: new Set(top),
589
+ parent: null
590
+ };
591
+ for (const statement of program.body) {
592
+ visit(statement, root, false);
593
+ if (statement.type !== "VariableDeclaration") continue;
594
+ for (const declarator of statement.declarations) {
595
+ if (declarator.id.type === "Identifier" && declarator.init !== null && declarator.init !== void 0 && mayBeUndefined(declarator.init)) facts.maybeUndefined.push(declarator.id.name);
596
+ const init = declarator.init?.type === "CallExpression" ? declarator.init : null;
597
+ const hook = init === null ? null : hookCallee(init.callee);
598
+ if (hook === null) continue;
599
+ const procedure = stringArgument(init?.arguments[0]);
600
+ if (declarator.id.type === "Identifier") {
601
+ facts.hooks.push({
602
+ hook,
603
+ name: declarator.id.name,
604
+ procedure,
605
+ property: null
606
+ });
607
+ continue;
608
+ }
609
+ if (declarator.id.type !== "ObjectPattern") continue;
610
+ for (const property of declarator.id.properties) {
611
+ if (property.type === "RestElement" || property.computed) continue;
612
+ const key = property.key.type === "Identifier" ? property.key.name : null;
613
+ const local = property.value.type === "Identifier" ? property.value.name : property.value.type === "AssignmentPattern" && property.value.left.type === "Identifier" ? property.value.left.name : null;
614
+ if (key !== null && local !== null) facts.hooks.push({
615
+ hook,
616
+ name: local,
617
+ procedure,
618
+ property: key
619
+ });
620
+ }
621
+ }
622
+ }
623
+ return facts;
624
+ }
625
+ /** Whether `source` would parse outside strict mode, for a clearer message. */
626
+ function sloppyParses(source) {
627
+ try {
628
+ parse(source, {
629
+ ...OPTIONS,
630
+ strict: false
631
+ });
632
+ return true;
633
+ } catch {
634
+ return false;
635
+ }
636
+ }
637
+ function parseCode(code, kind) {
638
+ const source = kind === "expression" ? `(${code}\n)` : code;
639
+ const shift = kind === "expression" ? 1 : 0;
640
+ let program;
641
+ try {
642
+ program = parse(source, OPTIONS);
643
+ } catch (error) {
644
+ const found = syntaxMessage(error);
645
+ return {
646
+ message: sloppyParses(source) ? `${found.message}${found.message.endsWith(" in strict mode") ? "" : " in strict mode"}, which specs run in` : found.message,
647
+ offset: Math.max(0, found.offset - shift)
648
+ };
649
+ }
650
+ if (kind === "statement") return {
651
+ program,
652
+ shift
653
+ };
654
+ const [only] = program.body;
655
+ if (program.body.length !== 1 || only?.type !== "ExpressionStatement" || only.expression.type !== "ParenthesizedExpression" || only.expression.start !== 0 || only.expression.end !== source.length) return {
656
+ message: "This is more than one expression",
657
+ offset: 0
658
+ };
659
+ return {
660
+ program,
661
+ shift
662
+ };
663
+ }
664
+ const MAX_CACHED = 4e3;
665
+ const cache = /* @__PURE__ */ new Map();
666
+ /** Parses `code` in strict mode and reads what it declares, reads and calls. Cached. */
667
+ function analyze(code, kind) {
668
+ const key = `${kind}\u0000${code}`;
669
+ const cached = cache.get(key);
670
+ if (cached !== void 0) return cached;
671
+ const parsed = parseCode(code, kind);
672
+ const result = "program" in parsed ? {
673
+ ok: true,
674
+ ...collectFacts(parsed.program, parsed.shift, code)
675
+ } : {
676
+ message: parsed.message,
677
+ offset: parsed.offset,
678
+ ok: false
679
+ };
680
+ if (cache.size >= MAX_CACHED) cache.clear();
681
+ cache.set(key, result);
682
+ return result;
683
+ }
684
+ /** Whether `name` can be bound as a variable in strict mode. */
685
+ function isValidBinding(name) {
686
+ return /^[A-Za-z_$][\w$]*$/.test(name) && analyze(`(${name}) => 0`, "expression").ok;
687
+ }
688
+ /** The 1-based line of `offset` in `code`, which starts on `line`. */
689
+ function lineOf(code, line, offset) {
690
+ let result = line;
691
+ for (let index = 0; index < offset && index < code.length; index++) if (code.charAt(index) === "\n") result++;
692
+ return result;
693
+ }
694
+ //#endregion
695
+ export { analyze, isValidBinding, lineOf };
@@ -0,0 +1,5 @@
1
+ import { Recipe } from "./compile.js";
2
+ //#region src/signature.d.ts
3
+ /** `<statCard label value delta={0} note="">…</statCard>` when the define has a slot. */
4
+ export declare function recipeSignature(recipe: Recipe): string;
5
+ //#endregion