octane 0.2.6 → 0.2.8

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.
@@ -0,0 +1,154 @@
1
+ // A direct module const initialized by Octane's createContext is the only
2
+ // context identity we can prove without following values between modules.
3
+ // Reject a name if any nested scope binds it: both compiler pipelines can
4
+ // then use this module-wide proof without guessing which scope owns a use().
5
+ export function collectProvenContextBindings(ast) {
6
+ const factories = new Set();
7
+ for (const statement of ast.body || []) {
8
+ if (
9
+ statement.type !== 'ImportDeclaration' ||
10
+ statement.importKind === 'type' ||
11
+ statement.source?.value !== 'octane'
12
+ )
13
+ continue;
14
+ for (const specifier of statement.specifiers || []) {
15
+ if (
16
+ specifier.type === 'ImportSpecifier' &&
17
+ specifier.importKind !== 'type' &&
18
+ (specifier.imported?.name ?? specifier.imported?.value) === 'createContext' &&
19
+ specifier.local?.name
20
+ )
21
+ factories.add(specifier.local.name);
22
+ }
23
+ }
24
+ if (factories.size === 0) return new Set();
25
+
26
+ const contexts = new Set();
27
+ for (const original of ast.body || []) {
28
+ const declaration = unwrapExport(original);
29
+ if (declaration?.type !== 'VariableDeclaration' || declaration.kind !== 'const') continue;
30
+ for (const entry of declaration.declarations || []) {
31
+ if (
32
+ entry.id?.type === 'Identifier' &&
33
+ entry.init?.type === 'CallExpression' &&
34
+ entry.init.optional !== true &&
35
+ entry.init.callee?.type === 'Identifier' &&
36
+ factories.has(entry.init.callee.name)
37
+ )
38
+ contexts.add(entry.id.name);
39
+ }
40
+ }
41
+ if (contexts.size === 0) return contexts;
42
+
43
+ const shadowed = new Set();
44
+ const seen = new WeakSet();
45
+ const bind = (pattern) => {
46
+ if (!pattern) return;
47
+ switch (pattern.type) {
48
+ case 'Identifier':
49
+ if (contexts.has(pattern.name)) shadowed.add(pattern.name);
50
+ break;
51
+ case 'ArrayPattern':
52
+ for (const item of pattern.elements || []) bind(item);
53
+ break;
54
+ case 'ObjectPattern':
55
+ for (const item of pattern.properties || [])
56
+ bind(item.type === 'RestElement' ? item.argument : item.value);
57
+ break;
58
+ case 'AssignmentPattern':
59
+ bind(pattern.left);
60
+ break;
61
+ case 'RestElement':
62
+ bind(pattern.argument);
63
+ break;
64
+ case 'TSParameterProperty':
65
+ bind(pattern.parameter);
66
+ break;
67
+ }
68
+ };
69
+ const scan = (node) => {
70
+ if (!node || typeof node !== 'object') return;
71
+ if (Array.isArray(node)) {
72
+ for (const item of node) scan(item);
73
+ return;
74
+ }
75
+ if (seen.has(node)) return;
76
+ seen.add(node);
77
+ if (node.type === 'ImportDeclaration') return;
78
+ switch (node.type) {
79
+ case 'VariableDeclarator':
80
+ bind(node.id);
81
+ break;
82
+ case 'FunctionDeclaration':
83
+ case 'FunctionExpression':
84
+ case 'ArrowFunctionExpression':
85
+ case 'TSDeclareFunction':
86
+ bind(node.id);
87
+ for (const param of node.params || []) bind(param);
88
+ break;
89
+ case 'ClassDeclaration':
90
+ case 'ClassExpression':
91
+ case 'TSEnumDeclaration':
92
+ case 'TSModuleDeclaration':
93
+ bind(node.id);
94
+ break;
95
+ case 'CatchClause':
96
+ bind(node.param);
97
+ break;
98
+ case 'JSXForExpression':
99
+ if (node.left?.type !== 'VariableDeclaration') bind(node.left);
100
+ bind(node.index);
101
+ break;
102
+ case 'JSXTryExpression':
103
+ bind(node.handler?.param);
104
+ bind(node.handler?.resetParam);
105
+ break;
106
+ }
107
+ for (const key in node) {
108
+ if (
109
+ key === 'loc' ||
110
+ key === 'start' ||
111
+ key === 'end' ||
112
+ key === 'range' ||
113
+ key === 'parent' ||
114
+ key === 'metadata' ||
115
+ key.startsWith('_octane')
116
+ )
117
+ continue;
118
+ scan(node[key]);
119
+ }
120
+ };
121
+ for (const original of ast.body || []) {
122
+ if (original.type === 'ImportDeclaration') continue;
123
+ const declaration = unwrapExport(original);
124
+ if (declaration?.type === 'VariableDeclaration') {
125
+ // These IDs belong to the module itself, including the proven
126
+ // contexts. Their initializers can still contain nested shadows.
127
+ for (const entry of declaration.declarations || []) scan(entry.init);
128
+ } else {
129
+ scan(declaration);
130
+ }
131
+ }
132
+ for (const name of shadowed) contexts.delete(name);
133
+ return contexts;
134
+ }
135
+
136
+ export function isProvenContextUse(argument, contexts) {
137
+ let value = argument;
138
+ while (
139
+ value?.type === 'TSAsExpression' ||
140
+ value?.type === 'TSTypeAssertion' ||
141
+ value?.type === 'TSSatisfiesExpression' ||
142
+ value?.type === 'TSNonNullExpression' ||
143
+ value?.type === 'TSInstantiationExpression' ||
144
+ value?.type === 'ParenthesizedExpression'
145
+ )
146
+ value = value.expression;
147
+ return value?.type === 'Identifier' && contexts.has(value.name);
148
+ }
149
+
150
+ function unwrapExport(node) {
151
+ return node?.type === 'ExportNamedDeclaration' || node?.type === 'ExportDefaultDeclaration'
152
+ ? node.declaration
153
+ : node;
154
+ }
@@ -178,6 +178,7 @@ function collectHoistedVars(node, functionScope, root = true, bindingsOnly = fal
178
178
  if (
179
179
  bindingsOnly &&
180
180
  (isFunction(node) ||
181
+ node.type === 'JSXSwitchExpression' ||
181
182
  (!root && node.type === 'StaticBlock') ||
182
183
  node.type === 'ClassDeclaration' ||
183
184
  node.type === 'ClassExpression' ||
@@ -479,7 +480,22 @@ function buildScopes(ast, onlyImported, hookRuntimeModules, bindingsOnly = false
479
480
  return;
480
481
  }
481
482
 
482
- if (node.type === 'SwitchStatement' || (bindingsOnly && node.type === 'JSXSwitchExpression')) {
483
+ if (bindingsOnly && node.type === 'JSXSwitchExpression') {
484
+ // Each template arm is a separate callback. Its `var` declarations
485
+ // belong to that callback, rather than to the enclosing function or
486
+ // another arm with a declaration of the same name.
487
+ walk(node.discriminant, scope);
488
+ for (const switchCase of node.cases || []) {
489
+ walk(switchCase.test, scope);
490
+ const caseScope = createScope(scope, 'function');
491
+ collectHoistedVars(switchCase.consequent, caseScope, true, true);
492
+ predeclareDirect(switchCase.consequent, caseScope, hookRuntimeModules, true);
493
+ for (const statement of switchCase.consequent || []) walk(statement, caseScope);
494
+ }
495
+ return;
496
+ }
497
+
498
+ if (node.type === 'SwitchStatement') {
483
499
  // A switch body is one lexical scope shared by every unbraced case.
484
500
  // Predeclare the direct case statements together so let/const/function
485
501
  // captures resolve even when their declaration appears in a case list
@@ -57,8 +57,6 @@ export interface CompileOptions {
57
57
  dev?: boolean;
58
58
  /** Assert pure immutable-snapshot renders, enable bounded checks, and trust production call memoization. */
59
59
  strong?: boolean;
60
- /** Experimental native signal reads in DOM client/server render scopes. */
61
- nativeReads?: boolean;
62
60
  profile?: boolean;
63
61
  profileFilename?: string;
64
62
  /** Include out-of-band source-origin inspection data. */
@@ -219,7 +217,7 @@ export function compile(source: string, filename: string, options?: CompileOptio
219
217
  export function compileToVolarMappings(
220
218
  source: string,
221
219
  filename?: string,
222
- options?: { loose?: boolean; renderers?: unknown; strong?: boolean; nativeReads?: boolean },
220
+ options?: { loose?: boolean; renderers?: unknown; strong?: boolean },
223
221
  ): VolarCompileResult;
224
222
 
225
223
  /** @internal Shared authored-JSX diagnostic analysis for compiler integrations. */
@@ -64,16 +64,30 @@ function propertyName(node, computed = false) {
64
64
  : null;
65
65
  }
66
66
 
67
+ /**
68
+ * Native reads are an explicit module capability. A runtime signals import,
69
+ * including a bare import for opaque readers, opts the entire module into
70
+ * capturing actual reads. Type-only imports and `$` names alone do not.
71
+ */
72
+ export function nativeReadOptions(ast, options) {
73
+ const nativeReads = ast.body.some(
74
+ (node) =>
75
+ node.type === 'ImportDeclaration' &&
76
+ node.importKind !== 'type' &&
77
+ (node.source?.value === SIGNALS_MODULE || LOCAL_MODULES.has(node.source?.value)) &&
78
+ (node.specifiers.length === 0 ||
79
+ node.specifiers.some((specifier) => specifier.importKind !== 'type')),
80
+ );
81
+ return { ...options, nativeReads };
82
+ }
83
+
67
84
  export function assertNativeReadOptions(options) {
68
- if (options?.nativeReads !== undefined && typeof options.nativeReads !== 'boolean') {
69
- throw new TypeError('Octane nativeReads must be a boolean.');
70
- }
71
85
  if (
72
86
  options?.nativeReads === true &&
73
87
  options.renderer?.target != null &&
74
88
  options.renderer.target !== 'dom'
75
89
  ) {
76
- throw new Error('Octane nativeReads currently supports only the DOM client and server.');
90
+ throw new Error('Octane native signal reads support only the DOM client and server.');
77
91
  }
78
92
  }
79
93
 
@@ -85,34 +99,7 @@ export function assertNativeReadOptions(options) {
85
99
  */
86
100
  export function analyzeNativeReadDiagnostics(ast, source, filename, options = {}) {
87
101
  assertNativeReadOptions(options);
88
- if (options.nativeReads !== true) {
89
- for (const statement of ast.body ?? []) {
90
- if (
91
- statement.type !== 'ImportDeclaration' ||
92
- statement.importKind === 'type' ||
93
- !LOCAL_MODULES.has(statement.source?.value)
94
- )
95
- continue;
96
- const capability = (statement.specifiers ?? []).find(
97
- (specifier) =>
98
- specifier.importKind !== 'type' &&
99
- (specifier.type === 'ImportNamespaceSpecifier' ||
100
- LOCAL_HOOKS.has(specifier.imported?.name ?? specifier.imported?.value)),
101
- );
102
- if (capability)
103
- return [
104
- nativeReadDiagnostic(
105
- 'OCTANE_NATIVE_READ_OPT_IN',
106
- source,
107
- filename,
108
- capability.start ?? statement.start,
109
- capability.end ?? statement.end,
110
- 'Local native signal hooks require nativeReads: true in the Octane compiler configuration.',
111
- ),
112
- ];
113
- }
114
- return [];
115
- }
102
+ if (!options.nativeReads) return [];
116
103
  if (options.rendererBoundaries && Object.keys(options.rendererBoundaries).length > 0) {
117
104
  const { boundaries } = analyzeRendererBoundaries(source, {
118
105
  ast,
@@ -132,7 +119,7 @@ export function analyzeNativeReadDiagnostics(ast, source, filename, options = {}
132
119
  filename,
133
120
  unsupported.tagRange[0],
134
121
  unsupported.tagRange[1],
135
- `Octane nativeReads currently supports only the DOM client and server; renderer ${JSON.stringify(unsupported.childRenderer)} has no native-read integration.`,
122
+ `Octane native signal reads support only the DOM client and server; renderer ${JSON.stringify(unsupported.childRenderer)} has no native-read integration.`,
136
123
  ),
137
124
  ];
138
125
  }
@@ -536,7 +523,7 @@ export function analyzeNativeReadDiagnostics(ast, source, filename, options = {}
536
523
  report(
537
524
  'OCTANE_NATIVE_SIGNAL_HOOK',
538
525
  node,
539
- 'useDerived$ is not available in this experimental integration. Create derived$ on an explicitly owned Scope and pass the handle to the component.',
526
+ 'useDerived$ is not available. Create derived$ on an explicitly owned Scope and pass the handle to the component.',
540
527
  );
541
528
  } else if (node.type === 'Property' && parents.get(node)?.type === 'ObjectExpression') {
542
529
  checkName(node.key, propertyName(node.key, node.computed), valueOf(node.value));
@@ -4,7 +4,6 @@ import type { CompilerProgram, CompileSourceMap } from './index.js';
4
4
  export interface SlotHooksOptions {
5
5
  environment?: 'client' | 'server';
6
6
  strong?: boolean;
7
- nativeReads?: boolean;
8
7
  hmr?: boolean;
9
8
  dev?: boolean;
10
9
  profile?: boolean;
@@ -20,10 +20,11 @@ import { NATIVE_SIGNAL_HOOK_NAMES } from './hook-names.js';
20
20
  import { METHOD_DEP_IMPORT, annotateHookCalls } from './hook-deps.js';
21
21
  import { inlinePlainHookMemos } from './plain-hook-memo.js';
22
22
  import { assertStrongMode } from './strong-mode.js';
23
- import { assertNativeReadDiagnostics, assertNativeReadOptions } from './native-read-diagnostics.js';
23
+ import { assertNativeReadDiagnostics, nativeReadOptions } from './native-read-diagnostics.js';
24
24
  import { nativeReadActivationIndex } from './native-read-codegen.js';
25
25
  import { findManualHookProviders, manualHookWrapperParameters } from './manual-hooks.js';
26
26
  import { findLeadingJsxImportSourcePragma } from './pragma.js';
27
+ import { collectProvenContextBindings, isProvenContextUse } from './context-use.js';
27
28
  import {
28
29
  hookMethodName,
29
30
  hasHookMethods,
@@ -868,6 +869,10 @@ function requireParallelHelper(st, imported) {
868
869
 
869
870
  function emitParallelUseRun(run, owner, st) {
870
871
  if (run.uses.length === 0) return;
872
+ if (run.uses.every((entry) => isProvenContextUse(entry.arg, st.provenContextBindings))) {
873
+ // Leave plain source untouched when every read is a proven context.
874
+ return;
875
+ }
871
876
  const memoName = st.nativeReads
872
877
  ? 'nativePuMemo'
873
878
  : st.environment === 'server'
@@ -1284,7 +1289,7 @@ function parseHookSource(source, id) {
1284
1289
  *
1285
1290
  * @param {string} source raw module text
1286
1291
  * @param {string} id module id (embedded in the stable Symbol.for key)
1287
- * @param {{ environment?: 'client' | 'server', strong?: boolean, nativeReads?: boolean, hmr?: boolean, dev?: boolean, profile?: boolean, profileFilename?: string, inlineHookMemo?: boolean, manualSlots?: boolean, universalRuntime?: unknown, renderer?: { target?: string }, isVoidComponentImport?: (request: string, imported: string) => boolean }} [options] `hmr: true` (dev serve) emits
1292
+ * @param {{ environment?: 'client' | 'server', strong?: boolean, hmr?: boolean, dev?: boolean, profile?: boolean, profileFilename?: string, inlineHookMemo?: boolean, manualSlots?: boolean, universalRuntime?: unknown, renderer?: { target?: string }, isVoidComponentImport?: (request: string, imported: string) => boolean }} [options] `hmr: true` (dev serve) emits
1288
1293
  * `Symbol.for(stableKey)` so a re-imported module resolves the same hook
1289
1294
  * slots (state survives HMR); off (ordinary prod builds and SSR) emits
1290
1295
  * runtime-ranged Symbols. Profiling retains short described Symbols because
@@ -1295,7 +1300,6 @@ function parseHookSource(source, id) {
1295
1300
  * @returns {{ code: string, map: any } | null}
1296
1301
  */
1297
1302
  export function slotHooks(source, id, options) {
1298
- assertNativeReadOptions(options);
1299
1303
  const environment = options?.environment ?? 'client';
1300
1304
  if (environment !== 'client' && environment !== 'server') {
1301
1305
  throw new Error(
@@ -1309,6 +1313,7 @@ export function slotHooks(source, id, options) {
1309
1313
  } catch {
1310
1314
  return null; // let the normal pipeline surface the parse error
1311
1315
  }
1316
+ options = nativeReadOptions(ast, options);
1312
1317
  assertStrongMode(ast, source, id, options);
1313
1318
  assertNativeReadDiagnostics(ast, source, id, options);
1314
1319
  const importInfo = octaneHookLocals(
@@ -1396,6 +1401,7 @@ export function slotHooks(source, id, options) {
1396
1401
  edits: [],
1397
1402
  decls: [],
1398
1403
  parallelHelpers: new Map(),
1404
+ provenContextBindings: collectProvenContextBindings(ast),
1399
1405
  usedNames: collectIdentifierNames(ast),
1400
1406
  slotBaseName: null,
1401
1407
  hookSlotsName: null,