octane 0.1.39 → 0.1.40

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.
@@ -21,7 +21,7 @@ __export(version_exports, {
21
21
  version: () => version
22
22
  });
23
23
  module.exports = __toCommonJS(version_exports);
24
- const version = "0.1.39";
24
+ const version = "0.1.40";
25
25
  // Annotate the CommonJS export names for ESM import in node:
26
26
  0 && (module.exports = {
27
27
  version
@@ -3,8 +3,9 @@
3
3
  * runtime.
4
4
  *
5
5
  * Architecture:
6
- * 1. Parse TSRX via @tsrx/core's parseModule and run its target-neutral
7
- * semantic analysis on the authored module.
6
+ * 1. Parse TSRX through the environment-selected @tsrx/core-compatible
7
+ * parser (native oxc-tsrx in Node, pure JavaScript elsewhere), then run
8
+ * @tsrx/core's target-neutral semantic analysis on the authored module.
8
9
  * 2. For each top-level node:
9
10
  * - Component (`@{ … }` body or a return-JSX function) → compile to a
10
11
  * function taking the props-first ABI `(…userParams, __s, __extra)`.
@@ -30,7 +31,6 @@
30
31
  import {
31
32
  analyzeCss,
32
33
  analyzeTsrx,
33
- parseModule,
34
34
  prepareStylesheetForRender,
35
35
  renderStylesheets,
36
36
  builders as b,
@@ -38,6 +38,7 @@ import {
38
38
  clone_ast_node as cloneAstNode,
39
39
  strongHash,
40
40
  } from '@tsrx/core';
41
+ import { parseModule } from '#octane/compiler-parser';
41
42
  import { print as esrapPrint } from 'esrap';
42
43
  import esrapTsx from 'esrap/languages/tsx';
43
44
  import { buildFatSegments } from './fat-segments.js';
@@ -118,12 +118,14 @@ function unwrapExport(node) {
118
118
  return node;
119
119
  }
120
120
 
121
- function predeclareDirect(statements, scope, hookRuntimeModules) {
121
+ function predeclareDirect(statements, scope, hookRuntimeModules, bindingsOnly = false) {
122
122
  for (const original of statements || []) {
123
123
  if (original.type === 'ImportDeclaration') {
124
+ if (bindingsOnly && original.importKind === 'type') continue;
124
125
  const isHookRuntime = hookRuntimeModules.has(original.source?.value);
125
126
  const isOctane = original.source?.value === 'octane';
126
127
  for (const specifier of original.specifiers || []) {
128
+ if (bindingsOnly && specifier.importKind === 'type') continue;
127
129
  const imported = specifier.imported?.name;
128
130
  declareName(scope, specifier.local.name, {
129
131
  imported: true,
@@ -156,19 +158,29 @@ function predeclareDirect(statements, scope, hookRuntimeModules) {
156
158
  }
157
159
  }
158
160
 
159
- function collectHoistedVars(node, functionScope, root = true) {
161
+ function collectHoistedVars(node, functionScope, root = true, bindingsOnly = false) {
160
162
  if (!node || typeof node !== 'object') return;
161
163
  if (Array.isArray(node)) {
162
- for (const child of node) collectHoistedVars(child, functionScope, false);
164
+ for (const child of node) collectHoistedVars(child, functionScope, false, bindingsOnly);
163
165
  return;
164
166
  }
165
167
  if (!root && isFunction(node)) return;
168
+ if (
169
+ bindingsOnly &&
170
+ (isFunction(node) ||
171
+ (!root && node.type === 'StaticBlock') ||
172
+ node.type === 'ClassDeclaration' ||
173
+ node.type === 'ClassExpression' ||
174
+ node.type === 'TSModuleDeclaration')
175
+ ) {
176
+ return;
177
+ }
166
178
  if (node.type === 'VariableDeclaration' && node.kind === 'var') {
167
179
  for (const decl of node.declarations || []) declarePattern(decl.id, functionScope);
168
180
  }
169
181
  for (const key in node) {
170
182
  if (AST_META_KEYS.has(key)) continue;
171
- collectHoistedVars(node[key], functionScope, false);
183
+ collectHoistedVars(node[key], functionScope, false, bindingsOnly);
172
184
  }
173
185
  }
174
186
 
@@ -277,7 +289,7 @@ function markReassignedPattern(pattern, scope) {
277
289
  }
278
290
  }
279
291
 
280
- function buildScopes(ast, onlyImported, hookRuntimeModules) {
292
+ function buildScopes(ast, onlyImported, hookRuntimeModules, bindingsOnly = false) {
281
293
  nextBindingId = 0;
282
294
  const moduleScope = createScope(null, 'module');
283
295
  const nodeScopes = new WeakMap();
@@ -289,8 +301,35 @@ function buildScopes(ast, onlyImported, hookRuntimeModules) {
289
301
  const functionRecords = new WeakMap();
290
302
  const trustedHookNames = new WeakMap();
291
303
  const callAnnotations = new Map();
292
- predeclareDirect(ast.body, moduleScope, hookRuntimeModules);
293
- collectHoistedVars(ast, moduleScope);
304
+ const declarationBindings = bindingsOnly ? new Map() : null;
305
+ const firstDefinitions = bindingsOnly ? new Map() : null;
306
+ predeclareDirect(ast.body, moduleScope, hookRuntimeModules, bindingsOnly);
307
+ collectHoistedVars(ast, moduleScope, true, bindingsOnly);
308
+
309
+ function rememberBindings(bindings, definesValue = true) {
310
+ if (declarationBindings === null || firstDefinitions === null) return;
311
+ for (const { pattern, binding } of bindings) {
312
+ declarationBindings.set(pattern, binding);
313
+ if (!definesValue) continue;
314
+ const previous = firstDefinitions.get(binding);
315
+ if (previous !== undefined && previous !== pattern) binding.reassigned = true;
316
+ else firstDefinitions.set(binding, pattern);
317
+ }
318
+ }
319
+
320
+ function rememberPattern(pattern, scope, definesValue = true) {
321
+ if (!bindingsOnly) return;
322
+ if (pattern?.type === 'TSParameterProperty') pattern = pattern.parameter;
323
+ const bindings = [];
324
+ collectPatternBindings(pattern, scope, bindings);
325
+ rememberBindings(bindings, definesValue);
326
+ }
327
+
328
+ function invalidateVisibleBindings(scope) {
329
+ for (let current = scope; current !== null; current = current.parent) {
330
+ for (const binding of current.bindings.values()) binding.reassigned = true;
331
+ }
332
+ }
294
333
 
295
334
  function walk(node, scope) {
296
335
  if (!node || typeof node !== 'object') return;
@@ -298,34 +337,103 @@ function buildScopes(ast, onlyImported, hookRuntimeModules) {
298
337
  for (const child of node) walk(child, scope);
299
338
  return;
300
339
  }
301
- nodeScopes.set(node, scope);
340
+ if (!bindingsOnly) nodeScopes.set(node, scope);
341
+ if (
342
+ bindingsOnly &&
343
+ (node.type === 'TSInstantiationExpression' || node.type === 'TSExportAssignment')
344
+ ) {
345
+ walk(node.expression, scope);
346
+ return;
347
+ }
302
348
 
303
349
  if (isFunction(node)) {
304
350
  const fnScope = createScope(scope, 'function');
305
- functionScopes.set(node, fnScope);
306
- const record = {
307
- node,
308
- scope: fnScope,
309
- binding:
310
- node.type === 'FunctionDeclaration' && node.id
311
- ? resolveBinding(scope, node.id.name)
312
- : null,
313
- parameters: null,
314
- stableDefinition: node.type === 'FunctionDeclaration',
315
- };
316
- functions.push(record);
317
- functionRecords.set(node, record);
351
+ const record = bindingsOnly
352
+ ? null
353
+ : {
354
+ node,
355
+ scope: fnScope,
356
+ binding:
357
+ node.type === 'FunctionDeclaration' && node.id
358
+ ? resolveBinding(scope, node.id.name)
359
+ : null,
360
+ parameters: null,
361
+ stableDefinition: node.type === 'FunctionDeclaration',
362
+ };
363
+ if (record !== null) {
364
+ functionScopes.set(node, fnScope);
365
+ functions.push(record);
366
+ functionRecords.set(node, record);
367
+ }
368
+ if (bindingsOnly && node.type === 'FunctionDeclaration' && node.id) {
369
+ rememberPattern(node.id, scope);
370
+ }
318
371
  if (node.type === 'FunctionExpression' && node.id) {
319
372
  declareName(fnScope, node.id.name);
373
+ if (bindingsOnly) rememberPattern(node.id, fnScope);
320
374
  }
321
375
  if (node.type !== 'ArrowFunctionExpression') declareName(fnScope, 'arguments');
322
- for (const param of node.params || []) declarePattern(param, fnScope);
323
- record.parameters = (node.params || []).map((param) =>
324
- directParameterBinding(param, fnScope),
325
- );
326
- collectHoistedVars(node.body, fnScope);
327
- for (const param of node.params || []) walkPatternDefaults(param, fnScope);
328
- walk(node.body, fnScope);
376
+ for (const param of node.params || []) {
377
+ declarePattern(
378
+ bindingsOnly && param.type === 'TSParameterProperty' ? param.parameter : param,
379
+ fnScope,
380
+ );
381
+ }
382
+ if (record !== null) {
383
+ record.parameters = (node.params || []).map((param) =>
384
+ directParameterBinding(param, fnScope),
385
+ );
386
+ }
387
+ if (bindingsOnly) {
388
+ for (const param of node.params || []) rememberPattern(param, fnScope);
389
+ // Parameter defaults cannot see declarations in the function body.
390
+ // Afterwards, share the direct body with the parameters so legal var
391
+ // and function redeclarations count as competing value definitions.
392
+ for (const param of node.params || []) walkPatternDefaults(param, fnScope);
393
+ collectHoistedVars(node.body, fnScope, true, true);
394
+ if (node.body?.type === 'BlockStatement' || node.body?.type === 'JSXCodeBlock') {
395
+ predeclareDirect(node.body.body, fnScope, hookRuntimeModules, true);
396
+ for (const statement of node.body.body || []) walk(statement, fnScope);
397
+ if (node.body.type === 'JSXCodeBlock') walk(node.body.render, fnScope);
398
+ } else {
399
+ walk(node.body, fnScope);
400
+ }
401
+ } else {
402
+ collectHoistedVars(node.body, fnScope);
403
+ for (const param of node.params || []) walkPatternDefaults(param, fnScope);
404
+ walk(node.body, fnScope);
405
+ }
406
+ return;
407
+ }
408
+
409
+ if (bindingsOnly && node.type === 'ImportDeclaration') {
410
+ if (node.importKind !== 'type') {
411
+ for (const specifier of node.specifiers || []) {
412
+ if (specifier.importKind !== 'type') rememberPattern(specifier.local, scope);
413
+ }
414
+ }
415
+ return;
416
+ }
417
+
418
+ if (
419
+ bindingsOnly &&
420
+ node.declare !== true &&
421
+ ((node.type === 'TSModuleDeclaration' && node.kind !== 'global') ||
422
+ node.type === 'TSEnumDeclaration')
423
+ ) {
424
+ // The dependency walk deliberately skips TypeScript-only syntax. Runtime
425
+ // namespaces and enum initializers can still write their enclosing scope.
426
+ invalidateVisibleBindings(scope);
427
+ return;
428
+ }
429
+
430
+ if (bindingsOnly && (node.type === 'ClassDeclaration' || node.type === 'ClassExpression')) {
431
+ if (node.type === 'ClassDeclaration' && node.id) rememberPattern(node.id, scope);
432
+ const classScope = createScope(scope, 'block');
433
+ if (node.id) declarePattern(node.id, classScope);
434
+ for (const decorator of node.decorators || []) walk(decorator, scope);
435
+ walk(node.superClass, classScope);
436
+ walk(node.body, classScope);
329
437
  return;
330
438
  }
331
439
 
@@ -334,8 +442,14 @@ function buildScopes(ast, onlyImported, hookRuntimeModules) {
334
442
  node.type === 'StaticBlock' ||
335
443
  node.type === 'JSXCodeBlock'
336
444
  ) {
337
- const blockScope = createScope(scope, 'block');
338
- predeclareDirect(node.body, blockScope, hookRuntimeModules);
445
+ const blockScope = createScope(
446
+ scope,
447
+ bindingsOnly && node.type === 'StaticBlock' ? 'function' : 'block',
448
+ );
449
+ if (bindingsOnly && node.type === 'StaticBlock') {
450
+ collectHoistedVars(node, blockScope, true, true);
451
+ }
452
+ predeclareDirect(node.body, blockScope, hookRuntimeModules, bindingsOnly);
339
453
  for (const statement of node.body || []) walk(statement, blockScope);
340
454
  // TSRX's final render node lives beside the setup-statement list.
341
455
  if (node.type === 'JSXCodeBlock') walk(node.render, blockScope);
@@ -345,12 +459,17 @@ function buildScopes(ast, onlyImported, hookRuntimeModules) {
345
459
  if (node.type === 'CatchClause') {
346
460
  const catchScope = createScope(scope, 'block');
347
461
  declarePattern(node.param, catchScope);
462
+ if (bindingsOnly) rememberPattern(node.param, catchScope);
463
+ if (bindingsOnly && node.resetParam) {
464
+ declarePattern(node.resetParam, catchScope);
465
+ rememberPattern(node.resetParam, catchScope);
466
+ }
348
467
  walkPatternDefaults(node.param, catchScope);
349
468
  walk(node.body, catchScope);
350
469
  return;
351
470
  }
352
471
 
353
- if (node.type === 'SwitchStatement') {
472
+ if (node.type === 'SwitchStatement' || (bindingsOnly && node.type === 'JSXSwitchExpression')) {
354
473
  // A switch body is one lexical scope shared by every unbraced case.
355
474
  // Predeclare the direct case statements together so let/const/function
356
475
  // captures resolve even when their declaration appears in a case list
@@ -360,10 +479,10 @@ function buildScopes(ast, onlyImported, hookRuntimeModules) {
360
479
  for (const switchCase of node.cases || []) {
361
480
  statements.push(...(switchCase.consequent || []));
362
481
  }
363
- predeclareDirect(statements, switchScope, hookRuntimeModules);
482
+ predeclareDirect(statements, switchScope, hookRuntimeModules, bindingsOnly);
364
483
  walk(node.discriminant, scope);
365
484
  for (const switchCase of node.cases || []) {
366
- nodeScopes.set(switchCase, switchScope);
485
+ if (!bindingsOnly) nodeScopes.set(switchCase, switchScope);
367
486
  walk(switchCase.test, switchScope);
368
487
  for (const statement of switchCase.consequent || []) walk(statement, switchScope);
369
488
  }
@@ -373,32 +492,45 @@ function buildScopes(ast, onlyImported, hookRuntimeModules) {
373
492
  if (
374
493
  node.type === 'ForStatement' ||
375
494
  node.type === 'ForInStatement' ||
376
- node.type === 'ForOfStatement'
495
+ node.type === 'ForOfStatement' ||
496
+ (bindingsOnly && node.type === 'JSXForExpression')
377
497
  ) {
378
498
  const loopScope = createScope(scope, 'block');
379
- const declaration = node.type === 'ForStatement' ? node.init : node.left;
499
+ const loopType = node.type === 'JSXForExpression' ? node.statementType : node.type;
500
+ const declaration = loopType === 'ForStatement' ? node.init : node.left;
380
501
  if (declaration?.type === 'VariableDeclaration' && declaration.kind !== 'var') {
381
502
  for (const decl of declaration.declarations || []) declarePattern(decl.id, loopScope);
382
503
  }
383
- if (node.type === 'ForStatement') {
504
+ if (loopType === 'ForStatement') {
384
505
  walk(node.init, loopScope);
385
506
  walk(node.test, loopScope);
386
507
  walk(node.update, loopScope);
387
508
  } else {
388
509
  walk(node.left, loopScope);
389
510
  if (node.left?.type !== 'VariableDeclaration') markReassignedPattern(node.left, loopScope);
390
- walk(node.right, loopScope);
511
+ else if (bindingsOnly && node.left.kind === 'var') {
512
+ for (const decl of node.left.declarations || [])
513
+ markReassignedPattern(decl.id, loopScope);
514
+ }
515
+ walk(node.right, node.type === 'JSXForExpression' ? scope : loopScope);
516
+ }
517
+ if (bindingsOnly && node.type === 'JSXForExpression') {
518
+ declarePattern(node.index, loopScope);
519
+ rememberPattern(node.index, loopScope);
520
+ walk(node.key, loopScope);
391
521
  }
392
522
  walk(node.body, loopScope);
523
+ if (bindingsOnly && node.type === 'JSXForExpression') walk(node.empty, scope);
393
524
  return;
394
525
  }
395
526
 
396
527
  if (node.type === 'VariableDeclaration') {
397
528
  for (const decl of node.declarations || []) {
398
- nodeScopes.set(decl, scope);
529
+ if (!bindingsOnly) nodeScopes.set(decl, scope);
399
530
  const bindings = [];
400
531
  collectPatternBindings(decl.id, scope, bindings);
401
- declarators.push({ decl, bindings, kind: node.kind });
532
+ if (bindingsOnly) rememberBindings(bindings, node.kind !== 'var' || decl.init != null);
533
+ else declarators.push({ decl, bindings, kind: node.kind });
402
534
  walkPatternDefaults(decl.id, scope);
403
535
  walk(decl.init, scope);
404
536
  }
@@ -411,7 +543,21 @@ function buildScopes(ast, onlyImported, hookRuntimeModules) {
411
543
  markReassignedPattern(node.argument, scope);
412
544
  }
413
545
 
414
- if (node.type === 'CallExpression') {
546
+ if (bindingsOnly && node.type === 'CallExpression') {
547
+ let callee = unwrapValue(node.callee);
548
+ while (callee?.type === 'TSInstantiationExpression') {
549
+ callee = unwrapValue(callee.expression);
550
+ }
551
+ if (
552
+ node.optional !== true &&
553
+ callee?.type === 'Identifier' &&
554
+ callee.name === 'eval' &&
555
+ resolveBinding(scope, callee.name) === null
556
+ ) {
557
+ // Direct eval can reassign any lexically reachable writable binding.
558
+ invalidateVisibleBindings(scope);
559
+ }
560
+ } else if (node.type === 'CallExpression') {
415
561
  // Preserve lexical import identity for the later slotting pass. A module-
416
562
  // level name map is insufficient: a component can shadow either a named
417
563
  // alias or an Octane namespace inside any nested scope. The annotation is
@@ -471,6 +617,9 @@ function buildScopes(ast, onlyImported, hookRuntimeModules) {
471
617
  function walkPatternDefaults(pattern, scope) {
472
618
  if (!pattern) return;
473
619
  switch (pattern.type) {
620
+ case 'TSParameterProperty':
621
+ if (bindingsOnly) walkPatternDefaults(pattern.parameter, scope);
622
+ return;
474
623
  case 'AssignmentPattern':
475
624
  walkPatternDefaults(pattern.left, scope);
476
625
  walk(pattern.right, scope);
@@ -513,7 +662,9 @@ function buildScopes(ast, onlyImported, hookRuntimeModules) {
513
662
  functions,
514
663
  trustedHookNames,
515
664
  callAnnotations,
665
+ declarationBindings,
516
666
  };
667
+ if (bindingsOnly) return analysis;
517
668
  // The surgical plain-TS pass slots base hooks only; without a custom-hook
518
669
  // withSlot boundary, two local wrapper calls would share their inner slots.
519
670
  // Restrict custom-call inference to the full TSRX/TSX compiler, which emits
@@ -1252,6 +1403,25 @@ function analyzeInternal(ast, options) {
1252
1403
  return { analysis, inferred };
1253
1404
  }
1254
1405
 
1406
+ /**
1407
+ * Return authored declaration identifiers whose bindings have a write or a
1408
+ * competing value definition. This is a read-only, scope-aware proof for callers
1409
+ * that recreate lexical scopes per invocation. Absence from the set does not
1410
+ * make a mutable declaration kind immutable by itself.
1411
+ *
1412
+ * @param {any} ast
1413
+ * @returns {WeakSet<import('estree').Identifier>}
1414
+ */
1415
+ export function collectReassignedBindings(ast) {
1416
+ const { declarationBindings } = buildScopes(ast, true, new Set(), true);
1417
+ /** @type {WeakSet<import('estree').Identifier>} */
1418
+ const reassigned = new WeakSet();
1419
+ for (const [node, binding] of declarationBindings ?? []) {
1420
+ if (binding.reassigned) reassigned.add(node);
1421
+ }
1422
+ return reassigned;
1423
+ }
1424
+
1255
1425
  /**
1256
1426
  * Return inferred dependency expressions for every supported hook call whose
1257
1427
  * dependency argument is omitted. Explicit arrays, `null`, and any other
@@ -0,0 +1 @@
1
+ export { parseModule } from '@tsrx/core';
@@ -0,0 +1 @@
1
+ export { parseModule } from 'oxc-tsrx/tsrx-core-compat';