octane 0.2.7 → 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.
@@ -92,6 +92,7 @@ import {
92
92
  wrapNativeWarmScope,
93
93
  } from './native-read-codegen.js';
94
94
  import { createTextTypeFactsLookup } from './text-type-facts.js';
95
+ import { collectProvenContextBindings, isProvenContextUse } from './context-use.js';
95
96
  import { applyCssModuleConstants } from './css-module-constants.js';
96
97
  import { assertUniversalRuntimeTarget, normalizeUniversalRuntime } from './universal-runtime.js';
97
98
 
@@ -5110,7 +5111,9 @@ function collectImmutableModuleFunctions(body) {
5110
5111
  const declared = new Map();
5111
5112
  for (const statement of body) {
5112
5113
  const declaration =
5113
- statement.type === 'ExportNamedDeclaration' ? statement.declaration : statement;
5114
+ statement.type === 'ExportNamedDeclaration' || statement.type === 'ExportDefaultDeclaration'
5115
+ ? statement.declaration
5116
+ : statement;
5114
5117
  if (declaration?.type === 'FunctionDeclaration' && declaration.id?.type === 'Identifier') {
5115
5118
  declared.set(declaration.id.name, declaration);
5116
5119
  }
@@ -5125,10 +5128,31 @@ function collectImmutableModuleFunctions(body) {
5125
5128
  }
5126
5129
  if (seen.has(n)) return;
5127
5130
  seen.add(n);
5128
- if (n.type === 'AssignmentExpression' && n.left?.type === 'Identifier') {
5129
- declared.delete(n.left.name);
5130
- } else if (n.type === 'UpdateExpression' && n.argument?.type === 'Identifier') {
5131
- declared.delete(n.argument.name);
5131
+ const writeTarget =
5132
+ n.type === 'AssignmentExpression'
5133
+ ? n.left
5134
+ : n.type === 'UpdateExpression'
5135
+ ? n.argument
5136
+ : n.type === 'VariableDeclarator'
5137
+ ? n.id
5138
+ : (n.type === 'ForOfStatement' || n.type === 'ForInStatement') &&
5139
+ n.left?.type !== 'VariableDeclaration'
5140
+ ? n.left
5141
+ : null;
5142
+ if (writeTarget?.type === 'Identifier') {
5143
+ declared.delete(writeTarget.name);
5144
+ } else if (writeTarget !== null) {
5145
+ const names = new Set();
5146
+ collectPatternNames(unwrapTsExpr(writeTarget), names);
5147
+ for (const name of names) declared.delete(name);
5148
+ } else if (
5149
+ n.type === 'CallExpression' &&
5150
+ unwrapTsExpr(n.callee)?.type === 'Identifier' &&
5151
+ unwrapTsExpr(n.callee).name === 'eval'
5152
+ ) {
5153
+ // Direct eval can write a module function binding without a visible
5154
+ // assignment node. Exclude those names from identity proofs.
5155
+ declared.clear();
5132
5156
  }
5133
5157
  for (const key in n) {
5134
5158
  if (AST_WALK_SKIP_KEYS.has(key)) continue;
@@ -9185,6 +9209,7 @@ function compileInternal(
9185
9209
  ctx.octaneImportLocals = imports.locals;
9186
9210
  ctx.octaneImportNamespaces = imports.namespaces;
9187
9211
  ctx.foreignImportLocals = imports.foreignLocals;
9212
+ ctx.provenContextBindings = collectProvenContextBindings(ast);
9188
9213
  ctx.activityModuleAst = ast;
9189
9214
  }
9190
9215
  if (ctx.dev) {
@@ -10456,6 +10481,7 @@ function compileServer(
10456
10481
  ctx.octaneImportLocals = imports.locals;
10457
10482
  ctx.octaneImportNamespaces = imports.namespaces;
10458
10483
  ctx.foreignImportLocals = imports.foreignLocals;
10484
+ ctx.provenContextBindings = collectProvenContextBindings(ast);
10459
10485
  ctx.activityModuleAst = ast;
10460
10486
  }
10461
10487
  // M3 inherit-range exclusion set — must match the client compile's
@@ -14036,6 +14062,7 @@ function compileFunctionBody(node, ctx, name, parentNs = 'html', cssHash = null,
14036
14062
  ctx.inlineHookMemo && ctx.mode !== 'server' && ctx._universalRuntimeUnit == null;
14037
14063
  ctx._puInlineLowering = inlineLowering;
14038
14064
  let warmThunk = null;
14065
+ let staticChildOnly = false;
14039
14066
  if (options && options.autoCallback) {
14040
14067
  const creations = [];
14041
14068
  const warmChildren = [];
@@ -14094,9 +14121,21 @@ function compileFunctionBody(node, ctx, name, parentNs = 'html', cssHash = null,
14094
14121
  jsxNodes = parallelUseWalkJsx(jsxNodes, ctx, name, creations, warmChildren, [], new Set());
14095
14122
  const warm = buildWarmArtifacts(node, ctx, name, creations, warmChildren);
14096
14123
  warmThunk = warm.thunk;
14124
+ staticChildOnly = warm.staticChildOnly === true;
14097
14125
  ctx._pendingWarm = warm.warmNode;
14098
14126
  }
14099
- workingStatements = rewriteParallelUse(workingStatements, ctx, name, warmThunk);
14127
+ workingStatements = rewriteParallelUse(
14128
+ workingStatements,
14129
+ ctx,
14130
+ name,
14131
+ warmThunk,
14132
+ staticChildOnly &&
14133
+ ctx.mode !== 'server' &&
14134
+ ctx._universalRuntimeUnit == null &&
14135
+ ctx.moduleFunctionDeclarations.get(name)?.start === node.start
14136
+ ? staticChildWarmPlanProps(node, name)
14137
+ : null,
14138
+ );
14100
14139
 
14101
14140
  // De-callback the hook memo tier (production client compile). Authored and
14102
14141
  // auto-generated useMemo/useCallback declarations become inline flat-cache
@@ -14822,6 +14861,7 @@ function transformUniversalParallelUse(ast, ctx, metadata) {
14822
14861
  // Names bound by a binding pattern (params, declarator ids). Mirrors the
14823
14862
  // pattern handling of collectFreeIdentifiers' collectBindings.
14824
14863
  function collectPatternNames(pat, into) {
14864
+ pat = unwrapTsExpr(pat);
14825
14865
  if (!pat) return into;
14826
14866
  switch (pat.type) {
14827
14867
  case 'Identifier':
@@ -16121,8 +16161,61 @@ function isWarmSafeExpr(expr, paramNames, componentLocals, armLocals) {
16121
16161
  return true;
16122
16162
  }
16123
16163
 
16164
+ // The registration records props now and invokes the already-attached module
16165
+ // plan only if a descendant suspends. A reassigned parameter would differ from
16166
+ // the old thunk's live lexical capture, including writes in JSX expressions or
16167
+ // nested callbacks. A same-name write in a shadowed scope merely declines the
16168
+ // optimization. Dynamic bindings are conservatively excluded as well.
16169
+ function staticChildWarmPlanProps(node, componentName) {
16170
+ const params = node.params || [];
16171
+ if (params.length > 1 || (params.length === 1 && params[0].type !== 'Identifier')) {
16172
+ return null;
16173
+ }
16174
+ const name = params[0]?.name ?? null;
16175
+ if (name === componentName) return null;
16176
+ const seen = new WeakSet();
16177
+ const bindsName = (pattern, binding) => {
16178
+ if (binding === null || pattern === null) return false;
16179
+ if (pattern.type === 'Identifier') return pattern.name === binding;
16180
+ const names = new Set();
16181
+ collectPatternNames(unwrapTsExpr(pattern), names);
16182
+ return names.has(binding);
16183
+ };
16184
+ const unsafe = (value) => {
16185
+ if (value === null || typeof value !== 'object') return false;
16186
+ if (Array.isArray(value)) return value.some(unsafe);
16187
+ if (seen.has(value)) return false;
16188
+ seen.add(value);
16189
+ if (
16190
+ (value.type === 'AssignmentExpression' && bindsName(value.left, name)) ||
16191
+ (value.type === 'UpdateExpression' && bindsName(value.argument, name)) ||
16192
+ (value.type === 'VariableDeclarator' &&
16193
+ (bindsName(value.id, name) || bindsName(value.id, componentName))) ||
16194
+ ((value.type === 'FunctionDeclaration' || value.type === 'ClassDeclaration') &&
16195
+ value.id?.name === componentName) ||
16196
+ (value.type === 'CatchClause' && bindsName(value.param, componentName)) ||
16197
+ ((value.type === 'ForOfStatement' || value.type === 'ForInStatement') &&
16198
+ bindsName(value.left, name)) ||
16199
+ (value.type === 'CallExpression' &&
16200
+ unwrapTsExpr(value.callee)?.type === 'Identifier' &&
16201
+ unwrapTsExpr(value.callee).name === 'eval') ||
16202
+ (value.type === 'Identifier' && value.name === 'arguments') ||
16203
+ value.type === 'ThisExpression' ||
16204
+ value.type === 'Super' ||
16205
+ value.type === 'MetaProperty'
16206
+ ) {
16207
+ return true;
16208
+ }
16209
+ for (const key in value) {
16210
+ if (!AST_WALK_SKIP_KEYS.has(key) && unsafe(value[key])) return true;
16211
+ }
16212
+ return false;
16213
+ };
16214
+ return unsafe(node.body) ? null : name === null ? b.unary('void', b.literal(0, '0')) : b.id(name);
16215
+ }
16216
+
16124
16217
  // ── Pass B: run detection + hoist + batch ───────────────────────────────────
16125
- function rewriteParallelUse(statements, ctx, componentName, warmThunk) {
16218
+ function rewriteParallelUse(statements, ctx, componentName, warmThunk, staticPlanProps = null) {
16126
16219
  let firstBatch = true;
16127
16220
  const output = transformList(statements);
16128
16221
  if (warmThunk) {
@@ -16132,17 +16225,57 @@ function rewriteParallelUse(statements, ctx, componentName, warmThunk) {
16132
16225
  // setup preserves early-return reachability: an alternate returned tree must
16133
16226
  // not activate the final template's plan. The first direct batch receives the
16134
16227
  // same thunk below so it can still warm before setup reaches this registration.
16135
- const batchHelper = ctx.mode === 'server' ? 'puBatch' : 'useBatch';
16136
- const batchAlias = requireRuntimeForContext(ctx, batchHelper);
16137
- const registration = inheritOriginLoc(
16138
- b.stmt(b.call(batchAlias, b.array([]), warmThunk)),
16228
+ // Only the DOM client has registerWarmPlan. Its two-argument form pushes
16229
+ // the same block, thunk, and undefined props as an empty useBatch call,
16230
+ // without creating an array on every render. Server and universal units
16231
+ // continue to use their own batch helpers.
16232
+ const fallbackHelper =
16233
+ ctx.mode === 'client' && ctx._universalRuntimeUnit == null
16234
+ ? 'registerWarmPlan'
16235
+ : ctx.mode === 'server'
16236
+ ? 'puBatch'
16237
+ : 'useBatch';
16238
+ const fallback = inheritOriginLoc(
16239
+ b.stmt(
16240
+ fallbackHelper === 'registerWarmPlan'
16241
+ ? b.call(
16242
+ requireRuntimeForContext(ctx, fallbackHelper),
16243
+ warmThunk,
16244
+ b.unary('void', b.literal(0, '0')),
16245
+ )
16246
+ : b.call(requireRuntimeForContext(ctx, fallbackHelper), b.array([]), warmThunk),
16247
+ ),
16139
16248
  warmThunk,
16140
16249
  );
16250
+ let registration = [fallback];
16251
+ if (staticPlanProps !== null) {
16252
+ // Hoisted functions can render before the module-tail markWarm stamp,
16253
+ // and route extraction can leave that stamp behind. Only the common
16254
+ // attached-plan path may omit the in-body closure and array.
16255
+ const planName = allocCompilerName(ctx, '__warmPlan');
16256
+ registration = [
16257
+ inheritOriginLoc(b.const(planName, b.member(b.id(componentName), '__warm')), warmThunk),
16258
+ inheritOriginLoc(
16259
+ b.if(
16260
+ b.binary('===', b.unary('typeof', b.id(planName)), b.literal('function', "'function'")),
16261
+ b.stmt(
16262
+ b.call(
16263
+ requireRuntimeForContext(ctx, 'registerWarmPlan'),
16264
+ b.id(planName),
16265
+ staticPlanProps,
16266
+ ),
16267
+ ),
16268
+ fallback,
16269
+ ),
16270
+ warmThunk,
16271
+ ),
16272
+ ];
16273
+ }
16141
16274
  const finalIndex =
16142
16275
  output.length > 0 && output[output.length - 1].type === 'ReturnStatement'
16143
16276
  ? output.length - 1
16144
16277
  : output.length;
16145
- output.splice(finalIndex, 0, registration);
16278
+ output.splice(finalIndex, 0, ...registration);
16146
16279
  }
16147
16280
  return output;
16148
16281
 
@@ -16236,6 +16369,19 @@ function rewriteParallelUse(statements, ctx, componentName, warmThunk) {
16236
16369
  for (const m of run.members) out.push(m.stmt);
16237
16370
  return;
16238
16371
  }
16372
+ if (
16373
+ uses.every(
16374
+ (m) =>
16375
+ m.call._octaneImportedHook === 'use' &&
16376
+ isProvenContextUse(m.creation, ctx.provenContextBindings),
16377
+ )
16378
+ ) {
16379
+ // Context reads cannot suspend. Preserve the authored statements in
16380
+ // their original order, and leave the first warm thunk available for
16381
+ // the next real data batch. The trailing child-only registration stays.
16382
+ for (const m of run.members) out.push(m.stmt);
16383
+ return;
16384
+ }
16239
16385
  // Hoist each creation into a temp, batch, then re-emit the original
16240
16386
  // statements with creations swapped for the temps. Unwrap order (and
16241
16387
  // hydration-seed order with it) is untouched.
@@ -16426,7 +16572,19 @@ function buildWarmArtifacts(node, ctx, componentName, creations, warmChildren) {
16426
16572
  node,
16427
16573
  );
16428
16574
  }
16429
- return { thunk, warmNode };
16575
+ return {
16576
+ thunk,
16577
+ warmNode,
16578
+ // The module plan contains own creations before child calls. Reuse it
16579
+ // for this component's registration only when it is child-only like the
16580
+ // original thunk; otherwise a descendant suspend could refetch its own
16581
+ // earlier memo. Memoized child props stay on the existing path too.
16582
+ staticChildOnly:
16583
+ warmNode !== null &&
16584
+ thunk !== null &&
16585
+ warmMemos.length === 0 &&
16586
+ warmKids.every((child) => child.props.every((prop) => prop.memo == null)),
16587
+ };
16430
16588
  }
16431
16589
 
16432
16590
  // ===========================================================================
@@ -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
@@ -1,4 +1,4 @@
1
- import { createLexicalAnalysis, forEachRuntimeAstChild } from './compile-universal.js';
1
+ import { createLexicalAnalysis } from './compile-universal.js';
2
2
  import { analyzeRendererBoundaries } from './renderer-boundaries.js';
3
3
  import { NATIVE_SIGNAL_NAME, NATIVE_MEMO_READ, nativeReadDiagnostic } from './native-read-facts.js';
4
4
  export { NATIVE_SIGNAL_NAME, NATIVE_MEMO_READ } from './native-read-facts.js';
@@ -65,40 +65,19 @@ function propertyName(node, computed = false) {
65
65
  }
66
66
 
67
67
  /**
68
- * Signal capabilities are part of authored source, not build configuration.
69
- * A `$` capability can arrive through props, a namespace, or an imported helper;
70
- * do not require a direct signals import or proof of the eventual read target.
71
- * Once selected, the entire module captures actual reads, including opaque calls.
72
- * Inspect runtime syntax rather than text so erased types, comments, and string
73
- * contents do not opt an ordinary module into native memoization and its adapter.
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.
74
71
  */
75
72
  export function nativeReadOptions(ast, options) {
76
- let nativeReads = false;
77
- function visit(node) {
78
- if (nativeReads || !node || typeof node !== 'object') return;
79
- if (node.importKind === 'type' || node.exportKind === 'type') return;
80
- if (
73
+ const nativeReads = ast.body.some(
74
+ (node) =>
81
75
  node.type === 'ImportDeclaration' &&
82
- node.specifiers.length > 0 &&
83
- node.specifiers.every((specifier) => specifier.importKind === 'type')
84
- ) {
85
- return;
86
- }
87
- if (
88
- ((node.type === 'Identifier' || node.type === 'JSXIdentifier') && node.name.endsWith('$')) ||
89
- (node.type === 'ImportDeclaration' &&
90
- (node.source?.value === SIGNALS_MODULE || LOCAL_MODULES.has(node.source?.value))) ||
91
- ((node.type === 'MemberExpression' || node.type === 'OptionalMemberExpression') &&
92
- String(propertyName(node.property, node.computed) ?? '').endsWith('$')) ||
93
- (node.type === 'Property' &&
94
- String(propertyName(node.key, node.computed) ?? '').endsWith('$'))
95
- ) {
96
- nativeReads = true;
97
- return;
98
- }
99
- forEachRuntimeAstChild(node, visit);
100
- }
101
- visit(ast);
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
+ );
102
81
  return { ...options, nativeReads };
103
82
  }
104
83
 
@@ -24,6 +24,7 @@ import { assertNativeReadDiagnostics, nativeReadOptions } from './native-read-di
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'
@@ -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,