form0-core 0.3.1 → 0.4.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.
Files changed (59) hide show
  1. package/README.md +63 -0
  2. package/package.json +5 -2
  3. package/src/builtins/builtin-definitions.js +320 -0
  4. package/src/builtins/choice/choicelabel.js +2 -17
  5. package/src/builtins/choice/choicelabels.js +2 -17
  6. package/src/builtins/choice/choicevalue.js +2 -20
  7. package/src/builtins/choice/choicevalues.js +2 -17
  8. package/src/builtins/choice/hasother.js +2 -20
  9. package/src/builtins/choice/other.js +2 -20
  10. package/src/builtins/control/eval.js +78 -31
  11. package/src/builtins/control/setresult.js +29 -17
  12. package/src/builtins/date/dateadd.js +24 -0
  13. package/src/builtins/date/days.js +22 -0
  14. package/src/builtins/event/control/off.js +3 -27
  15. package/src/builtins/event/control/on.js +3 -19
  16. package/src/builtins/event/event-operations-collector.js +33 -9
  17. package/src/builtins/event/field/setvalue.js +3 -32
  18. package/src/builtins/event/ui/alert.js +3 -11
  19. package/src/builtins/logical/and.js +2 -17
  20. package/src/builtins/logical/array.js +2 -35
  21. package/src/builtins/logical/count.js +2 -20
  22. package/src/builtins/logical/counta.js +2 -20
  23. package/src/builtins/logical/countblank.js +2 -20
  24. package/src/builtins/logical/if.js +2 -20
  25. package/src/builtins/logical/or.js +2 -17
  26. package/src/builtins/math/abs.js +2 -20
  27. package/src/builtins/math/ceiling.js +2 -20
  28. package/src/builtins/math/cos.js +2 -20
  29. package/src/builtins/math/round.js +2 -23
  30. package/src/builtins/math/sin.js +2 -20
  31. package/src/builtins/registry.js +23 -40
  32. package/src/builtins/schema/datanames.js +9 -20
  33. package/src/builtins/schema/form.js +2 -20
  34. package/src/builtins/string/upper.js +2 -20
  35. package/src/engine/calculation-diagnostics.js +107 -0
  36. package/src/engine/calculation.js +65 -32
  37. package/src/engine/evaluator.js +56 -18
  38. package/src/engine/event-operation-scope.js +61 -0
  39. package/src/engine/event-source.js +164 -0
  40. package/src/engine/events.js +105 -19
  41. package/src/engine/form-engine.js +61 -16
  42. package/src/engine/warning-system.js +7 -5
  43. package/src/schema/field-specs.js +2 -2
  44. package/src/security/validation.js +10 -8
  45. package/src/utilities/calculation-editor.js +42 -6
  46. package/src/utilities/date-utils.js +72 -0
  47. package/src/builtins/docs/choice/choicelabel.mdx +0 -110
  48. package/src/builtins/docs/choice/choicelabels.mdx +0 -230
  49. package/src/builtins/docs/choice/choicevalue.mdx +0 -109
  50. package/src/builtins/docs/choice/choicevalues.mdx +0 -196
  51. package/src/builtins/docs/choice/hasother.mdx +0 -133
  52. package/src/builtins/docs/choice/other.mdx +0 -150
  53. package/src/builtins/docs/control/setresult.mdx +0 -203
  54. package/src/builtins/docs/event/field/setvalue.mdx +0 -239
  55. package/src/builtins/docs/index.mdx +0 -234
  56. package/src/builtins/docs/logical/and.mdx +0 -141
  57. package/src/builtins/docs/logical/if.mdx +0 -94
  58. package/src/builtins/docs/logical/or.mdx +0 -191
  59. package/src/builtins/docs/overview.mdx +0 -258
@@ -16,13 +16,14 @@ export function evaluateCalculatedFields(
16
16
  contextResolver = null,
17
17
  warningSystem = null,
18
18
  runtimeDiagnostics = null,
19
- calculationPlan = null
19
+ calculationPlan = null,
20
+ diagnostics = null
20
21
  ) {
21
22
  const resolver = contextResolver || new ContextResolver(schema);
22
23
  const warnings = warningSystem || new WarningSystem();
23
24
  const plan = calculationPlan || buildCalculationDependencyPlan(schema, resolver);
24
25
 
25
- emitDependencyPlanWarnings(plan, warnings, runtimeDiagnostics);
26
+ emitDependencyPlanWarnings(plan, warnings, runtimeDiagnostics, diagnostics);
26
27
 
27
28
  if (plan.totalCalculatedFieldCount === 0) {
28
29
  return;
@@ -47,7 +48,8 @@ export function evaluateCalculatedFields(
47
48
  securityConfig,
48
49
  resolver,
49
50
  warnings,
50
- runtimeDiagnostics
51
+ runtimeDiagnostics,
52
+ diagnostics
51
53
  );
52
54
  return;
53
55
  }
@@ -73,7 +75,8 @@ export function evaluateCalculatedFields(
73
75
  securityConfig,
74
76
  resolver,
75
77
  warnings,
76
- runtimeDiagnostics
78
+ runtimeDiagnostics,
79
+ diagnostics
77
80
  );
78
81
  }
79
82
  });
@@ -103,7 +106,8 @@ function buildScopedContext(
103
106
  executionContext,
104
107
  contextResolver,
105
108
  warningSystem,
106
- expressionCode
109
+ expressionCode,
110
+ diagnostics
107
111
  ) {
108
112
  const ctx = { ...helpers };
109
113
 
@@ -139,12 +143,14 @@ function buildScopedContext(
139
143
  fieldName,
140
144
  'restricted'
141
145
  );
142
- warningSystem.emitWarning(warning);
146
+ if (diagnostics) diagnostics.emitWarning(warning);
147
+ else warningSystem.emitWarning(warning);
143
148
  });
144
149
 
145
150
  notFoundAccessedFields.forEach((fieldName) => {
146
151
  const warning = contextResolver.generateAccessWarning(executionContext, fieldName, 'not_found');
147
- warningSystem.emitWarning(warning);
152
+ if (diagnostics) diagnostics.emitWarning(warning);
153
+ else warningSystem.emitWarning(warning);
148
154
  });
149
155
 
150
156
  return ctx;
@@ -181,10 +187,12 @@ function pushRuntimeDiagnostic(
181
187
  });
182
188
  }
183
189
 
184
- function emitDependencyPlanWarnings(plan, warningSystem, runtimeDiagnostics) {
190
+ function emitDependencyPlanWarnings(plan, warningSystem, runtimeDiagnostics, diagnostics) {
191
+ const emit = (warning, fieldNames) =>
192
+ diagnostics ? diagnostics.emitWarning(warning, fieldNames) : warningSystem.emitWarning(warning);
185
193
  plan.dynamicFieldNames.forEach((fieldName) => {
186
194
  const message = `CalculatedField '${fieldName}' uses dynamic field access. Runtime evaluation falls back to bounded stabilization.`;
187
- warningSystem.emitWarning({
195
+ emit({
188
196
  type: 'calculation_dependency',
189
197
  reason: 'dynamic_dependencies',
190
198
  message,
@@ -215,22 +223,25 @@ function emitDependencyPlanWarnings(plan, warningSystem, runtimeDiagnostics) {
215
223
 
216
224
  const fieldNames = component.fieldNames;
217
225
  const message = `CalculatedField dependency cycle detected: ${fieldNames.join(' -> ')}. Runtime evaluation is disabled for the involved fields until the cycle is removed.`;
218
- warningSystem.emitWarning({
219
- type: 'calculation_dependency',
220
- reason: 'cyclic_dependencies',
221
- severity: 'error',
222
- message,
223
- suggestion:
224
- 'Remove the cycle or break it with a source field so calculated values can be evaluated deterministically.',
225
- executionContext: {
226
- type: 'calculation',
227
- fieldName: fieldNames[0] || null,
228
- },
229
- fieldContext: {
230
- fieldNames: [...fieldNames],
231
- cyclic: true,
226
+ emit(
227
+ {
228
+ type: 'calculation_dependency',
229
+ reason: 'cyclic_dependencies',
230
+ severity: 'error',
231
+ message,
232
+ suggestion:
233
+ 'Remove the cycle or break it with a source field so calculated values can be evaluated deterministically.',
234
+ executionContext: {
235
+ type: 'calculation',
236
+ fieldName: fieldNames[0] || null,
237
+ },
238
+ fieldContext: {
239
+ fieldNames: [...fieldNames],
240
+ cyclic: true,
241
+ },
232
242
  },
233
- });
243
+ fieldNames
244
+ );
234
245
 
235
246
  fieldNames.forEach((fieldName) => {
236
247
  pushRuntimeDiagnostic(
@@ -252,7 +263,8 @@ function evaluateCalculatedField(
252
263
  securityConfig,
253
264
  resolver,
254
265
  warnings,
255
- runtimeDiagnostics
266
+ runtimeDiagnostics,
267
+ diagnostics
256
268
  ) {
257
269
  if (!field?.data_name || !field.calculate) {
258
270
  return { changed: false, hasError: false };
@@ -266,11 +278,20 @@ function evaluateCalculatedField(
266
278
  executionContext,
267
279
  resolver,
268
280
  warnings,
269
- field.calculate
281
+ field.calculate,
282
+ diagnostics
270
283
  );
271
284
  const previousValue = values[field.data_name];
272
285
  const nextValue = runExpression(field.calculate, context, securityConfig, false, schema, {
273
- suppressConsoleWarning: Array.isArray(runtimeDiagnostics),
286
+ requireResult: true,
287
+ fieldName: field.data_name,
288
+ suppressConsoleWarning:
289
+ diagnostics?.consoleOverride !== undefined || Array.isArray(runtimeDiagnostics),
290
+ onDiagnostic: (diagnostic) => diagnostics?.record(field.data_name, diagnostic),
291
+ evalReporting: {
292
+ suppressConsole: diagnostics?.consoleOverride !== undefined,
293
+ onDiagnostic: (diagnostic) => diagnostics?.record(field.data_name, diagnostic),
294
+ },
274
295
  onError: (error) => {
275
296
  pushRuntimeDiagnostic(
276
297
  runtimeDiagnostics,
@@ -288,13 +309,21 @@ function evaluateCalculatedField(
288
309
  hasError: false,
289
310
  };
290
311
  } catch (error) {
312
+ diagnostics?.record(field.data_name, {
313
+ code: 'runtime_exception',
314
+ phase: 'runtime',
315
+ message:
316
+ error instanceof Error && error.message
317
+ ? error.message
318
+ : 'Unknown calculation runtime error.',
319
+ });
291
320
  pushRuntimeDiagnostic(
292
321
  runtimeDiagnostics,
293
322
  field.data_name,
294
323
  error instanceof Error && error.message ? error.message : 'Unknown calculation runtime error.'
295
324
  );
296
325
 
297
- if (!Array.isArray(runtimeDiagnostics)) {
326
+ if (diagnostics?.consoleOverride === undefined && !Array.isArray(runtimeDiagnostics)) {
298
327
  console.warn(`Calculation failed for ${field.data_name}:`, error?.message);
299
328
  }
300
329
 
@@ -311,7 +340,8 @@ function evaluateFieldSequenceUntilStable(
311
340
  securityConfig,
312
341
  resolver,
313
342
  warnings,
314
- runtimeDiagnostics
343
+ runtimeDiagnostics,
344
+ diagnostics
315
345
  ) {
316
346
  const maxPasses = Math.max(2, fieldNames.length);
317
347
  let stillChangingFieldNames = [];
@@ -333,7 +363,8 @@ function evaluateFieldSequenceUntilStable(
333
363
  securityConfig,
334
364
  resolver,
335
365
  warnings,
336
- runtimeDiagnostics
366
+ runtimeDiagnostics,
367
+ diagnostics
337
368
  );
338
369
 
339
370
  if (result.changed) {
@@ -347,7 +378,7 @@ function evaluateFieldSequenceUntilStable(
347
378
  }
348
379
 
349
380
  const message = `Calculated fields did not stabilize within ${maxPasses} evaluation passes: ${stillChangingFieldNames.join(', ')}.`;
350
- warnings.emitWarning({
381
+ const warning = {
351
382
  type: 'calculation_dependency',
352
383
  reason: 'non_converging_runtime',
353
384
  message,
@@ -361,7 +392,9 @@ function evaluateFieldSequenceUntilStable(
361
392
  fieldNames: [...stillChangingFieldNames],
362
393
  maxPasses,
363
394
  },
364
- });
395
+ };
396
+ if (diagnostics) diagnostics.emitWarning(warning, stillChangingFieldNames);
397
+ else warnings.emitWarning(warning);
365
398
 
366
399
  stillChangingFieldNames.forEach((fieldName) => {
367
400
  pushRuntimeDiagnostic(
@@ -1,6 +1,6 @@
1
- import { __consumeResult, __resetResult } from '../builtins/registry.js';
2
- import { __setEvalContext, __clearEvalContext } from '../builtins/control/eval.js';
3
- import { __setDataNamesContext, __clearDataNamesContext } from '../builtins/schema/datanames.js';
1
+ import { __beginResultScope, __consumeResult } from '../builtins/registry.js';
2
+ import { __setEvalContext } from '../builtins/control/eval.js';
3
+ import { __setDataNamesContext } from '../builtins/schema/datanames.js';
4
4
  import { validateExpression, createSecureContext, withTimeout } from '../security/validation.js';
5
5
  import { DEFAULT_SECURITY_CONFIG } from '../security/config.js';
6
6
  import {
@@ -8,6 +8,24 @@ import {
8
8
  normalizeInlineCalculationExpression,
9
9
  } from '../utilities/calculation-expression-utils.js';
10
10
 
11
+ const MISSING_RESULT_MESSAGE =
12
+ 'Multiline calculation produced no value because it neither calls SETRESULT() nor returns a value.';
13
+
14
+ function reportMissingResult(options) {
15
+ options.onDiagnostic?.({
16
+ code: 'missing_result',
17
+ severity: 'warning',
18
+ phase: 'runtime',
19
+ message: MISSING_RESULT_MESSAGE,
20
+ suggestion:
21
+ 'Wrap the final value in SETRESULT(), use an explicit return statement, or write the calculation on a single line.',
22
+ });
23
+ if (!options.suppressConsoleWarning) {
24
+ const field = options.fieldName ? ` (${options.fieldName})` : '';
25
+ console.warn(`[form0] missing_result${field}: ${MISSING_RESULT_MESSAGE}`);
26
+ }
27
+ }
28
+
11
29
  export function runExpression(
12
30
  expr,
13
31
  context = {},
@@ -16,13 +34,18 @@ export function runExpression(
16
34
  schema = null,
17
35
  options = {}
18
36
  ) {
19
- // SETRESULT uses synchronous evaluation-scoped state. Clear it at both boundaries so a
20
- // previously failed expression can never influence this evaluation.
21
- __resetResult();
37
+ const restoreResultScope = __beginResultScope();
38
+ let phase = 'runtime';
22
39
  try {
40
+ const sourceExpression = options.sourceExpression ?? expr;
23
41
  // Validate expression based on security mode
24
- const validation = validateExpression(expr, securityConfig, includeEventBuiltins);
42
+ const validation = validateExpression(sourceExpression, securityConfig, includeEventBuiltins);
25
43
  if (!validation.valid) {
44
+ options.onDiagnostic?.({
45
+ code: 'expression_validation_failed',
46
+ phase: 'validation',
47
+ message: validation.reason,
48
+ });
26
49
  if (!options.suppressConsoleWarning) {
27
50
  console.warn('[form0] Expression validation failed:', validation.reason);
28
51
  }
@@ -37,37 +60,40 @@ export function runExpression(
37
60
  // Execute expression
38
61
  const executeExpression = () => {
39
62
  // Set context for EVAL() before execution
40
- __setEvalContext(secureContext);
41
- if (schema) {
42
- __setDataNamesContext(schema);
43
- }
63
+ const restoreEvalContext = __setEvalContext(secureContext, options.evalReporting);
64
+ const restoreDataNamesContext = schema ? __setDataNamesContext(schema) : null;
44
65
 
45
66
  try {
46
67
  // Handle both expressions and multi-line code (Windows-safe)
47
- const isMultiLine = isMultilineCalculationExpression(expr);
68
+ const isMultiLine =
69
+ options.evaluateAsExpression !== true &&
70
+ isMultilineCalculationExpression(sourceExpression);
48
71
 
49
72
  if (isMultiLine) {
50
73
  // Execute as code block (recompile each time for now)
74
+ phase = 'syntax';
51
75
  const fn = new Function(...keys, expr);
76
+ phase = 'runtime';
52
77
  const result = fn(...values);
53
78
  // Check for consumed result from SETRESULT() calls in multiline code
54
79
  const consumed = __consumeResult();
80
+ if (options.requireResult && !consumed.called && result === undefined) {
81
+ reportMissingResult(options);
82
+ }
55
83
  return consumed.called ? consumed.value : result;
56
84
  } else {
57
85
  // Execute as expression (existing behavior)
58
86
  const inlineExpression = normalizeInlineCalculationExpression(expr);
87
+ phase = 'syntax';
59
88
  const fn = new Function(...keys, `return (${inlineExpression});`);
89
+ phase = 'runtime';
60
90
  const result = fn(...values);
61
91
  const consumed = __consumeResult();
62
92
  return consumed.called ? consumed.value : result;
63
93
  }
64
94
  } finally {
65
- __resetResult();
66
- // Always clear EVAL context after execution
67
- __clearEvalContext();
68
- if (schema) {
69
- __clearDataNamesContext();
70
- }
95
+ restoreDataNamesContext?.();
96
+ restoreEvalContext();
71
97
  }
72
98
  };
73
99
 
@@ -80,6 +106,16 @@ export function runExpression(
80
106
 
81
107
  return executeExpression();
82
108
  } catch (e) {
109
+ options.onDiagnostic?.({
110
+ code:
111
+ phase === 'syntax'
112
+ ? e instanceof SyntaxError
113
+ ? 'invalid_syntax'
114
+ : 'compilation_error'
115
+ : 'runtime_exception',
116
+ phase,
117
+ message: e instanceof Error && e.message ? e.message : 'Unknown calculation runtime error.',
118
+ });
83
119
  if (typeof options.onError === 'function') {
84
120
  options.onError(e);
85
121
  }
@@ -87,5 +123,7 @@ export function runExpression(
87
123
  console.warn('[form0] Expression evaluation failed:', e.message);
88
124
  }
89
125
  return null;
126
+ } finally {
127
+ restoreResultScope();
90
128
  }
91
129
  }
@@ -0,0 +1,61 @@
1
+ import {
2
+ __invokeBoundEventBuiltin,
3
+ __runWithEventOperationScope,
4
+ } from '../builtins/event/event-operations-collector.js';
5
+
6
+ const EVENT_OPERATION_BUILTINS = ['ALERT', 'SETVALUE', 'ON', 'OFF'];
7
+
8
+ /**
9
+ * Create an operation scope owned by one synchronous event execution.
10
+ * @param {Function} onLateOperation - Receives only the discarded operation name
11
+ * @returns {Object} Private scope
12
+ */
13
+ export function createEventOperationScope(onLateOperation) {
14
+ let open = true;
15
+ let operations = [];
16
+
17
+ return {
18
+ collect(operation) {
19
+ if (open) {
20
+ operations.push(operation);
21
+ return;
22
+ }
23
+
24
+ onLateOperation(operation?.operation || operation?.type || 'unknown');
25
+ },
26
+ close() {
27
+ if (!open) return [];
28
+ open = false;
29
+ const collected = operations;
30
+ operations = [];
31
+ return collected;
32
+ },
33
+ };
34
+ }
35
+
36
+ /**
37
+ * Bind canonical event operation builtins to their originating scope.
38
+ * Intentional helper overrides are left untouched.
39
+ */
40
+ export function bindEventBuiltins(context, canonicalBuiltins, scope) {
41
+ const boundContext = { ...context };
42
+
43
+ for (const name of EVENT_OPERATION_BUILTINS) {
44
+ const builtin = canonicalBuiltins[name];
45
+ if (context[name] === builtin) {
46
+ boundContext[name] = (...args) => __invokeBoundEventBuiltin(scope, builtin, args);
47
+ }
48
+ }
49
+
50
+ return boundContext;
51
+ }
52
+
53
+ /** Run synchronous work while raw canonical builtins target this scope. */
54
+ export function runInEventOperationScope(scope, work) {
55
+ return __runWithEventOperationScope(scope, work);
56
+ }
57
+
58
+ /** Close a scope and return its operations in creation order. */
59
+ export function closeEventOperationScope(scope) {
60
+ return scope.close();
61
+ }
@@ -0,0 +1,164 @@
1
+ import { parse } from 'acorn';
2
+
3
+ /** Retains authored callback source without depending on engine-specific toString output. */
4
+ export function createEventSourceRegistry() {
5
+ const sources = new WeakMap();
6
+
7
+ function prepare(code, context) {
8
+ let tree;
9
+ try {
10
+ // Event scripts are Function bodies, rather than modules or global scripts.
11
+ tree = parse(code, { ecmaVersion: 'latest', sourceType: 'commonjs' });
12
+ } catch {
13
+ // Let the existing evaluator validate and report invalid/unsupported source.
14
+ return { code, context };
15
+ }
16
+
17
+ const identifiers = new Set();
18
+ function collectIdentifiers(node) {
19
+ if (!node || typeof node !== 'object') return;
20
+ if (node.type === 'Identifier') identifiers.add(node.name);
21
+ for (const value of Object.values(node)) {
22
+ if (Array.isArray(value)) value.forEach(collectIdentifiers);
23
+ else if (value && typeof value === 'object') collectIdentifiers(value);
24
+ }
25
+ }
26
+ collectIdentifiers(tree);
27
+ let captureName = '__form0CaptureEventSource';
28
+ while (identifiers.has(captureName) || Object.hasOwn(context, captureName)) captureName += '_';
29
+ const edits = [];
30
+ const propertyNames = new Map();
31
+
32
+ function inferredName(node, parent) {
33
+ if (node.id) return '';
34
+ if (parent?.type === 'VariableDeclarator' && parent.id.type === 'Identifier')
35
+ return parent.id.name;
36
+ if (
37
+ parent?.type === 'AssignmentExpression' &&
38
+ ['=', '||=', '&&=', '??='].includes(parent.operator) &&
39
+ parent.left.type === 'Identifier'
40
+ )
41
+ return parent.left.name;
42
+ if (parent?.type === 'AssignmentPattern' && parent.left.type === 'Identifier')
43
+ return parent.left.name;
44
+ if (parent?.type === 'Property' && parent.computed) {
45
+ // Preserve ToPropertyKey's single coercion and the inferred function name.
46
+ const key = parent.key;
47
+ edits.push({ position: key.start, rank: 0, text: `${captureName}.key(` });
48
+ edits.push({ position: key.end, rank: 2, text: `,${node.start})` });
49
+ return { computed: node.start };
50
+ }
51
+ if (
52
+ (parent?.type === 'Property' || parent?.type === 'PropertyDefinition') &&
53
+ (!parent.computed || parent.key.type === 'Literal')
54
+ ) {
55
+ if (parent.key.type === 'PrivateIdentifier') return `#${parent.key.name}`;
56
+ return parent.key.type === 'Identifier' ? parent.key.name : String(parent.key.value);
57
+ }
58
+ return '';
59
+ }
60
+
61
+ function visit(node, parent) {
62
+ if (!node || typeof node !== 'object') return;
63
+ if (node.type === 'Program' || node.type === 'BlockStatement') {
64
+ const declarations = node.body.filter(
65
+ (statement) => statement.type === 'FunctionDeclaration'
66
+ );
67
+ if (declarations.length) {
68
+ let position = node.type === 'Program' ? node.start : node.start + 1;
69
+ for (const statement of node.body) {
70
+ if (
71
+ statement.type !== 'ExpressionStatement' ||
72
+ statement.expression.type !== 'Literal' ||
73
+ typeof statement.expression.value !== 'string'
74
+ )
75
+ break;
76
+ position = statement.end;
77
+ }
78
+ const text = declarations
79
+ .map(
80
+ (declaration) =>
81
+ `${captureName}(${declaration.id.name},${JSON.stringify(code.slice(declaration.start, declaration.end))});`
82
+ )
83
+ .join('');
84
+ edits.push({ position, rank: 1, text: `;${text}` });
85
+ }
86
+ }
87
+ if (
88
+ (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') &&
89
+ !(parent?.type === 'Property' && (parent.method || parent.kind !== 'init')) &&
90
+ parent?.type !== 'MethodDefinition' &&
91
+ // Dynamic class keys need class-specific name metadata; keep their legacy path.
92
+ !(parent?.type === 'PropertyDefinition' && parent.computed && parent.key.type !== 'Literal')
93
+ ) {
94
+ const source = code.slice(node.start, node.end);
95
+ const name = inferredName(node, parent);
96
+ const nameExpression =
97
+ typeof name === 'object'
98
+ ? `${captureName}.nameFor(${name.computed})`
99
+ : JSON.stringify(name);
100
+ edits.push({ position: node.start, rank: 0, text: `${captureName}(` });
101
+ edits.push({
102
+ position: node.end,
103
+ rank: 2,
104
+ text: `,${JSON.stringify(source)},${nameExpression})`,
105
+ });
106
+ }
107
+ for (const [key, value] of Object.entries(node)) {
108
+ if (key === 'start' || key === 'end') continue;
109
+ if (Array.isArray(value)) value.forEach((child) => visit(child, node));
110
+ else if (value && typeof value === 'object') visit(value, node);
111
+ }
112
+ }
113
+ visit(tree, null);
114
+
115
+ let prepared = code;
116
+ // At shared boundaries apply openers first, then hoisted captures, then closers.
117
+ for (const { position, text } of edits.sort(
118
+ (a, b) => b.position - a.position || a.rank - b.rank
119
+ )) {
120
+ prepared = prepared.slice(0, position) + text + prepared.slice(position);
121
+ }
122
+
123
+ const capture = (callback, source, name) => {
124
+ sources.set(callback, source);
125
+ if (name && !callback.name)
126
+ Object.defineProperty(callback, 'name', { value: name, configurable: true });
127
+ return callback;
128
+ };
129
+ capture.key = (value, id) => {
130
+ const key = Reflect.ownKeys({ [value]: null })[0];
131
+ propertyNames.set(
132
+ id,
133
+ typeof key === 'symbol'
134
+ ? key.description === undefined
135
+ ? ''
136
+ : `[${key.description}]`
137
+ : key
138
+ );
139
+ return key;
140
+ };
141
+ capture.nameFor = (id) => propertyNames.get(id);
142
+
143
+ return {
144
+ code: prepared,
145
+ context: {
146
+ ...context,
147
+ [captureName]: capture,
148
+ },
149
+ };
150
+ }
151
+
152
+ function getSource(callback) {
153
+ if (sources.has(callback)) return sources.get(callback);
154
+ const source = callback.toString();
155
+ if (/^function\b[^]*\{\s*\[\s*(?:native\s+code|bytecode)\s*\]\s*\}\s*$/.test(source.trim())) {
156
+ throw new Error(
157
+ 'Event callback source is unavailable on this JavaScript engine. Define the callback in form.events.code.'
158
+ );
159
+ }
160
+ return source;
161
+ }
162
+
163
+ return { prepare, getSource };
164
+ }