octane 0.2.2 → 0.2.4

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/dist/cjs/compatibility.cjs +35 -0
  2. package/dist/cjs/component-flags.cjs +8 -1
  3. package/dist/cjs/css.cjs +9 -0
  4. package/dist/cjs/dom-tables.cjs +8 -1
  5. package/dist/cjs/error-codes.client.generated.cjs +15 -2
  6. package/dist/cjs/error-codes.server.generated.cjs +5 -0
  7. package/dist/cjs/event-names.cjs +39 -0
  8. package/dist/cjs/index.cjs +13 -0
  9. package/dist/cjs/runtime.cjs +1345 -359
  10. package/dist/cjs/runtime.server.cjs +620 -183
  11. package/dist/cjs/server/index.cjs +26 -0
  12. package/dist/cjs/version.cjs +1 -1
  13. package/dist/compatibility.d.ts +7 -0
  14. package/dist/compatibility.js +10 -0
  15. package/dist/compiler/bundler.js +20 -30
  16. package/dist/compiler/compile-renderer-boundaries.js +1 -0
  17. package/dist/compiler/compile-universal.js +3 -0
  18. package/dist/compiler/compile.js +877 -627
  19. package/dist/compiler/hook-deps.js +100 -3
  20. package/dist/compiler/hook-names.js +1 -0
  21. package/dist/compiler/hydrate-boundaries.js +165 -15
  22. package/dist/compiler/manual-hooks.js +171 -0
  23. package/dist/compiler/parser.node.js +34 -8
  24. package/dist/compiler/plain-hook-memo.js +54 -5
  25. package/dist/compiler/slot-hooks.d.ts +43 -0
  26. package/dist/compiler/slot-hooks.js +154 -35
  27. package/dist/compiler/strong-mode.js +836 -3
  28. package/dist/compiler/style-scopes.js +1134 -0
  29. package/dist/compiler/volar.LICENSES.txt +1 -1
  30. package/dist/compiler/volar.js +4543 -2839
  31. package/dist/component-flags.d.ts +1 -1
  32. package/dist/component-flags.js +8 -1
  33. package/dist/css.d.ts +7 -0
  34. package/dist/css.js +8 -0
  35. package/dist/dom-tables.js +8 -1
  36. package/dist/error-codes.client.generated.d.ts +3 -1
  37. package/dist/error-codes.client.generated.js +15 -2
  38. package/dist/error-codes.server.generated.d.ts +1 -0
  39. package/dist/error-codes.server.generated.js +5 -0
  40. package/dist/event-names.d.ts +1 -0
  41. package/dist/event-names.js +15 -0
  42. package/dist/html-tree-validation.d.ts +17 -34
  43. package/dist/index.d.ts +3 -1
  44. package/dist/index.js +12 -1
  45. package/dist/jsx-runtime.d.ts +6 -1
  46. package/dist/public-types.d.ts +188 -0
  47. package/dist/public-types.js +0 -0
  48. package/dist/runtime.d.ts +128 -69
  49. package/dist/runtime.js +1349 -360
  50. package/dist/runtime.server.d.ts +67 -15
  51. package/dist/runtime.server.js +619 -184
  52. package/dist/server/index.d.ts +4 -2
  53. package/dist/server/index.js +23 -0
  54. package/dist/static/index.d.ts +2 -1
  55. package/dist/static/index.js +3 -1
  56. package/dist/universal-core.d.ts +8 -1
  57. package/dist/universal-core.js +95 -9
  58. package/dist/version.js +1 -1
  59. package/package.json +4 -3
@@ -7,6 +7,7 @@ import { print as esrapPrint } from 'esrap';
7
7
  import esrapTsx from 'esrap/languages/tsx';
8
8
  import { METHOD_DEP_IMPORT } from './hook-deps.js';
9
9
  import { nativeReadActivationIndex } from './native-read-codegen.js';
10
+ import { adaptManualHookProviders } from './manual-hooks.js';
10
11
  import {
11
12
  hasInlineMemoDirectEval,
12
13
  inheritHookMemoOrigin,
@@ -126,13 +127,32 @@ function inferredDependencyArray(inferred, state, origin) {
126
127
  );
127
128
  }
128
129
 
129
- // Match the surgical pass's BASE-hook-only policy. Custom hooks remain normal
130
- // calls; their compiled caller owns withSlot. Existing explicit memo slots are
130
+ // Match the surgical pass's base-hook and imported-hook slot policy. Local
131
+ // custom helpers keep their authored boundaries. Existing explicit memo slots are
131
132
  // already the effective third argument, so no unused fourth argument is added.
132
133
  function slotBaseHooks(ast, state, options) {
133
134
  function visit(node) {
134
135
  if (node === null || typeof node !== 'object') return node;
135
136
  if (Array.isArray(node)) return mapChildren(node, visit);
137
+ if (!options.manualSlots && node.type === 'CallExpression' && node._octaneCustomHookCall) {
138
+ const slot = allocateHookSlot(state, node);
139
+ const mapped = mapChildren(node, visit);
140
+ const callee = mapped.typeArguments
141
+ ? {
142
+ type: 'TSInstantiationExpression',
143
+ expression: mapped.callee,
144
+ typeArguments: mapped.typeArguments,
145
+ }
146
+ : mapped.callee;
147
+ return {
148
+ ...mapped,
149
+ callee: b.id(requireHelper(state, 'withSlot', 'octane'), node),
150
+ typeArguments: null,
151
+ // Keep foreign/default parameters and arguments.length unchanged;
152
+ // Octane base aliases resolve their slot from this call's path.
153
+ arguments: [slot, callee, ...mapped.arguments],
154
+ };
155
+ }
136
156
  const imported =
137
157
  node.type === 'CallExpression'
138
158
  ? (node._octaneImportedHook ??
@@ -147,13 +167,12 @@ function slotBaseHooks(ast, state, options) {
147
167
  inferred === undefined &&
148
168
  node.arguments.length === 3 &&
149
169
  !node.arguments.some((argument) => argument.type === 'SpreadElement');
150
- const slot = explicitMemoSlot ? null : allocateHookSlot(state, node);
170
+ const slot = options.manualSlots || explicitMemoSlot ? null : allocateHookSlot(state, node);
151
171
  const mapped = mapChildren(node, visit);
152
172
  const args = mapped.arguments.slice();
153
173
  if (inferred !== undefined) {
154
174
  args.splice(inferred.depsIndex, 0, inferredDependencyArray(inferred, state, node));
155
175
  }
156
- if (slot !== null) args.push(slot);
157
176
  let callee = mapped.callee;
158
177
  if (options.getterCalls.has(node) && options.stateGetterHelpers[imported]) {
159
178
  callee = b.id(requireHelper(state, options.stateGetterHelpers[imported], 'octane'), node);
@@ -161,6 +180,29 @@ function slotBaseHooks(ast, state, options) {
161
180
  if (node._octaneNativeInferredMemo === true) {
162
181
  callee = b.id(requireHelper(state, 'nativePuMemo'), node);
163
182
  }
183
+ if (slot !== null) {
184
+ if (
185
+ (imported === 'useState' || imported === 'useRef') &&
186
+ args.some((arg) => arg.type === 'SpreadElement')
187
+ ) {
188
+ const fn = mapped.typeArguments
189
+ ? {
190
+ type: 'TSInstantiationExpression',
191
+ expression: callee,
192
+ typeArguments: mapped.typeArguments,
193
+ }
194
+ : callee;
195
+ return {
196
+ ...mapped,
197
+ callee: b.id(requireHelper(state, 'withSlot', 'octane'), node),
198
+ typeArguments: null,
199
+ arguments: [slot, fn, ...args],
200
+ };
201
+ }
202
+ if (args.length === 0 && (imported === 'useState' || imported === 'useRef'))
203
+ args.push(b.id('undefined', node));
204
+ args.push(slot);
205
+ }
164
206
  return { ...mapped, callee, arguments: args };
165
207
  }
166
208
  return visit(ast);
@@ -235,7 +277,7 @@ export function inlinePlainHookMemos(ast, source, id, options) {
235
277
  slotBase: null,
236
278
  slotDeclarations: [],
237
279
  };
238
- let transformed = options.manualSlots ? ast : slotBaseHooks(ast, state, options);
280
+ let transformed = slotBaseHooks(ast, state, options);
239
281
  const lowered = lowerSlotMemoFunctions(transformed, {
240
282
  allocateName: (preferred) => allocName(state, preferred),
241
283
  requireRuntime: (imported) => requireHelper(state, imported),
@@ -243,6 +285,13 @@ export function inlinePlainHookMemos(ast, source, id, options) {
243
285
  });
244
286
  if (lowered.lowered === 0) return null;
245
287
  transformed = lowered.ast;
288
+ if (options.manualSlots) {
289
+ transformed = adaptManualHookProviders(
290
+ transformed,
291
+ (name) => requireHelper(state, name, 'octane'),
292
+ (preferred) => allocName(state, preferred),
293
+ );
294
+ }
246
295
  const origin = ast.body[0] ?? ast;
247
296
  const activation = options.nativeReadActivation
248
297
  ? inheritHookMemoOrigin(
@@ -0,0 +1,43 @@
1
+ import type { CompilerProgram, CompileSourceMap } from './index.js';
2
+
3
+ /** Options for the plain-module hook transform used by compiler adapters. */
4
+ export interface SlotHooksOptions {
5
+ environment?: 'client' | 'server';
6
+ strong?: boolean;
7
+ nativeReads?: boolean;
8
+ hmr?: boolean;
9
+ dev?: boolean;
10
+ profile?: boolean;
11
+ profileFilename?: string;
12
+ inlineHookMemo?: boolean;
13
+ manualSlots?: boolean;
14
+ /** Opaque activation signal; the transform only checks its presence. */
15
+ universalRuntime?: unknown;
16
+ renderer?: { target?: string };
17
+ isVoidComponentImport?: (request: string, imported: string) => boolean;
18
+ }
19
+
20
+ export interface SlotHooksResult {
21
+ code: string;
22
+ /** The surgical transform preserves source lines and returns no map. */
23
+ map: CompileSourceMap | null;
24
+ }
25
+
26
+ export interface VoidComponentImport {
27
+ request: string;
28
+ imported: string;
29
+ }
30
+
31
+ /** Returns null when the module can pass through unchanged. */
32
+ export function slotHooks(
33
+ source: string,
34
+ id: string,
35
+ options?: SlotHooksOptions,
36
+ ): SlotHooksResult | null;
37
+
38
+ export function findVoidRootImports(source: string, id: string): VoidComponentImport[];
39
+
40
+ export function findVoidComponentImports(
41
+ source: string | CompilerProgram,
42
+ id: string,
43
+ ): VoidComponentImport[];
@@ -5,15 +5,16 @@
5
5
  // byte-for-byte. Production bundlers may additionally select the separate
6
6
  // whole-AST memo path, which preserves TypeScript and supplies a source map.
7
7
  //
8
- // This pass parses the module (for byte offsets), finds ONLY octane BASE hook
9
- // calls, and splices a trailing compiler slot into each — every other byte (all the
10
- // TS the printer can't handle) passes through verbatim. It does NOT wrap custom
11
- // hooks in `withSlot` (that's done by the `.tsrx`/`.tsx` CALLER, and wrapping a
12
- // hand-written binding that already forwards a slot would double-slot it). In
8
+ // This pass parses the module (for byte offsets), slots Octane base hooks and
9
+ // gives imported custom hooks their own withSlot boundary. Imported aliases can
10
+ // point directly at a base hook, so the enclosing component's boundary alone
11
+ // cannot distinguish their call sites. Local helpers retain their authored slot
12
+ // policy; explicitly manual modules opt out of all injected slots. In
13
13
  // production it reserves a collision-free runtime range because these arbitrary
14
14
  // helpers can execute in a Scope alongside code from any other source module.
15
15
 
16
16
  import { parseModule } from '@tsrx/core';
17
+ import { parseModule as parseFallbackModule } from '#octane/compiler-parser';
17
18
  import { HOOK_NAMES, hookSlotHash } from './compile.js';
18
19
  import { NATIVE_SIGNAL_HOOK_NAMES } from './hook-names.js';
19
20
  import { METHOD_DEP_IMPORT, annotateHookCalls } from './hook-deps.js';
@@ -21,6 +22,7 @@ import { inlinePlainHookMemos } from './plain-hook-memo.js';
21
22
  import { assertStrongMode } from './strong-mode.js';
22
23
  import { assertNativeReadDiagnostics, assertNativeReadOptions } from './native-read-diagnostics.js';
23
24
  import { nativeReadActivationIndex } from './native-read-codegen.js';
25
+ import { findManualHookProviders, manualHookWrapperParameters } from './manual-hooks.js';
24
26
 
25
27
  function importsNativeRenderer(ast) {
26
28
  return ast.body.some(
@@ -42,6 +44,7 @@ function octaneHookLocals(ast, nativeReads = false) {
42
44
  const locals = new Map();
43
45
  let importsHook = false;
44
46
  let hasOctaneImport = false;
47
+ let importsCustomHook = false;
45
48
  for (const node of ast.body || []) {
46
49
  if (
47
50
  (node.type === 'ExportNamedDeclaration' || node.type === 'ExportAllDeclaration') &&
@@ -51,6 +54,15 @@ function octaneHookLocals(ast, nativeReads = false) {
51
54
  continue;
52
55
  }
53
56
  if (node.type !== 'ImportDeclaration') continue;
57
+ if (node.importKind === 'type') continue;
58
+ for (const sp of node.specifiers || []) {
59
+ if (
60
+ sp.importKind !== 'type' &&
61
+ (/^use[A-Z]/.test(sp.imported?.name ?? '') || /^use[A-Z]/.test(sp.local?.name ?? ''))
62
+ ) {
63
+ importsCustomHook = true;
64
+ }
65
+ }
54
66
  const native =
55
67
  nativeReads &&
56
68
  (node.source?.value === 'octane/signals/client' ||
@@ -76,7 +88,11 @@ function octaneHookLocals(ast, nativeReads = false) {
76
88
  importsHook = true;
77
89
  }
78
90
  }
79
- return { locals, importsHook, hasOctaneImport };
91
+ return {
92
+ locals,
93
+ importsHook: importsHook || (hasOctaneImport && importsCustomHook),
94
+ hasOctaneImport,
95
+ };
80
96
  }
81
97
 
82
98
  // Find only the disposable top-level root shape used by production entries:
@@ -158,7 +174,7 @@ function collectVoidRootCandidates(ast) {
158
174
  export function findVoidRootImports(source, id) {
159
175
  let ast;
160
176
  try {
161
- ast = parseModule(source, id);
177
+ ast = parseHookSource(source, id).ast;
162
178
  } catch {
163
179
  return [];
164
180
  }
@@ -236,7 +252,7 @@ export function findVoidComponentImports(source, id) {
236
252
  ast = source;
237
253
  } else {
238
254
  try {
239
- ast = parseModule(source, id);
255
+ ast = parseHookSource(source, id).ast;
240
256
  } catch {
241
257
  return [];
242
258
  }
@@ -737,6 +753,19 @@ function collectStateGetterCalls(ast) {
737
753
  return calls;
738
754
  }
739
755
 
756
+ function collectStateGetterEdit(node, imported, st) {
757
+ if (!st.getterCalls.has(node) || !STATE_GETTER_HELPERS[imported]) return;
758
+ let helper = st.getterHelpers.get(imported);
759
+ if (helper === undefined) {
760
+ const base = `_$${STATE_GETTER_HELPERS[imported]}`;
761
+ helper = base;
762
+ let suffix = 0;
763
+ while (st.source.includes(helper)) helper = `${base}$${++suffix}`;
764
+ st.getterHelpers.set(imported, helper);
765
+ }
766
+ st.edits.push({ pos: node.callee.start, end: node.callee.end, text: helper });
767
+ }
768
+
740
769
  // DFS in SOURCE ORDER, allocating a hook's slot id BEFORE descending into its args
741
770
  // — identical pre-order to rewriteHookCalls, so a base hook nested as an argument
742
771
  // (e.g. in a deps array) gets its own stable id. Collects insertion edits + the
@@ -954,6 +983,20 @@ function collectParallelUseEdits(ast, st) {
954
983
  scan(ast.body, hookOwner(null, 'module'));
955
984
  }
956
985
 
986
+ // Locate the call delimiter after the callee/type arguments without consuming
987
+ // comments or TypeScript syntax. Parenthesized identifier callees are valid too.
988
+ function callOpenParen(node, source) {
989
+ let pos = node.typeArguments?.end ?? node.callee.end;
990
+ while (pos < node.end) {
991
+ if (/\s/.test(source[pos]) || source[pos] === ')') pos++;
992
+ else if (source.startsWith('/*', pos)) pos = source.indexOf('*/', pos + 2) + 2;
993
+ else if (source.startsWith('//', pos)) {
994
+ while (pos < node.end && source[pos] !== '\n' && source[pos] !== '\r') pos++;
995
+ } else return source[pos] === '(' ? pos : -1;
996
+ }
997
+ return -1;
998
+ }
999
+
957
1000
  function walk(node, owner, st) {
958
1001
  if (!node || typeof node !== 'object') return;
959
1002
  if (Array.isArray(node)) {
@@ -976,10 +1019,24 @@ function walk(node, owner, st) {
976
1019
  node.type === 'FunctionDeclaration' && node.id ? hookOwner(node, node.id.name) : owner;
977
1020
 
978
1021
  if (node.type === 'CallExpression') {
1022
+ if (!st.manualSlots && node._octaneCustomHookCall) {
1023
+ const open = callOpenParen(node, st.source);
1024
+ if (open !== -1) {
1025
+ const sym = allocHookSymbol(st, owner, node.callee.name, node._octaneCustomHookCall, node);
1026
+ const helper = requireParallelHelper(st, 'withSlot');
1027
+ // The path stack supplies identity without changing the authored
1028
+ // argument list. An alias may point at a foreign hook whose omitted
1029
+ // parameter has a default, rather than at Octane's trailing-slot ABI.
1030
+ st.edits.push({ pos: node.start, text: `${helper}(${sym}, ` });
1031
+ st.edits.push({ pos: open, end: open + 1, text: node.arguments.length ? ', ' : '' });
1032
+ }
1033
+ }
979
1034
  const imported =
980
1035
  node._octaneImportedHook ??
981
1036
  (st.nativeReads ? node._octaneHookRuntimeImportedHook : undefined);
1037
+ collectStateGetterEdit(node, imported, st);
982
1038
  if (
1039
+ !st.manualSlots &&
983
1040
  imported &&
984
1041
  (HOOK_NAMES.has(imported) || (st.nativeReads && NATIVE_SIGNAL_HOOK_NAMES.has(imported)))
985
1042
  ) {
@@ -996,7 +1053,17 @@ function walk(node, owner, st) {
996
1053
  text: requireParallelHelper(st, 'nativePuMemo'),
997
1054
  });
998
1055
  }
999
- if (inferred !== undefined) {
1056
+ if (
1057
+ (imported === 'useState' || imported === 'useRef') &&
1058
+ node.arguments.some((arg) => arg.type === 'SpreadElement')
1059
+ ) {
1060
+ const open = callOpenParen(node, st.source);
1061
+ if (open !== -1) {
1062
+ const helper = requireParallelHelper(st, 'withSlot');
1063
+ st.edits.push({ pos: node.start, text: `${helper}(${sym}, ` });
1064
+ st.edits.push({ pos: open, end: open + 1, text: ', ' });
1065
+ }
1066
+ } else if (inferred !== undefined) {
1000
1067
  // The dependency callback is already the final user argument. Insert
1001
1068
  // both the generated array and slot in one edit so equal-position edit
1002
1069
  // ordering cannot reverse them. Dependency nodes retain original source
@@ -1016,11 +1083,11 @@ function walk(node, owner, st) {
1016
1083
  text: `, [${deps}], ${sym}`,
1017
1084
  });
1018
1085
  } else if (node.arguments.length === 0) {
1019
- // `useId()` → `useId(_h$N)`. Symbols remain self-identifying when
1020
- // optional user arguments are omitted.
1086
+ // State/ref initializers may themselves be Symbols. Reserve their
1087
+ // authored position even when empty; other hooks keep their ABI.
1021
1088
  st.edits.push({
1022
1089
  pos: node.end - 1,
1023
- text: sym,
1090
+ text: imported === 'useState' || imported === 'useRef' ? `undefined, ${sym}` : sym,
1024
1091
  });
1025
1092
  } else {
1026
1093
  // `useState(0)` → `useState(0, _h$N)` — insert AFTER the last arg's end so
@@ -1030,17 +1097,6 @@ function walk(node, owner, st) {
1030
1097
  text: ', ' + sym,
1031
1098
  });
1032
1099
  }
1033
- if (st.getterCalls.has(node) && STATE_GETTER_HELPERS[imported]) {
1034
- let helper = st.getterHelpers.get(imported);
1035
- if (helper === undefined) {
1036
- const base = `_$${STATE_GETTER_HELPERS[imported]}`;
1037
- helper = base;
1038
- let suffix = 0;
1039
- while (st.source.includes(helper)) helper = `${base}$${++suffix}`;
1040
- st.getterHelpers.set(imported, helper);
1041
- }
1042
- st.edits.push({ pos: node.callee.start, end: node.callee.end, text: helper });
1043
- }
1044
1100
  }
1045
1101
  }
1046
1102
 
@@ -1051,8 +1107,59 @@ function walk(node, owner, st) {
1051
1107
  }
1052
1108
  }
1053
1109
 
1110
+ function collectManualHookEdits(ast, providers, st) {
1111
+ function visit(node) {
1112
+ if (!node || typeof node !== 'object') return;
1113
+ if (Array.isArray(node)) {
1114
+ for (const item of node) visit(item);
1115
+ return;
1116
+ }
1117
+ if (providers.has(node)) {
1118
+ if (node.type === 'FunctionDeclaration') {
1119
+ const helper = requireParallelHelper(st, 'invokeManualHook');
1120
+ const implementation = allocSlotName(st, `_$manual_${node.id.name}`);
1121
+ const params = manualHookWrapperParameters(node)
1122
+ .map((param) => param.name)
1123
+ .join(', ');
1124
+ st.edits.push({
1125
+ pos: node.start,
1126
+ text: `function ${node.id.name}(${params}) { return ${helper}(${implementation}, this, arguments); } `,
1127
+ });
1128
+ st.edits.push({ pos: node.id.start, end: node.id.end, text: implementation });
1129
+ } else {
1130
+ const helper = requireParallelHelper(st, 'manualHook');
1131
+ st.edits.push({ pos: node.start, text: `/* @__PURE__ */ ${helper}(` });
1132
+ st.edits.push({ pos: node.end, text: `, ${JSON.stringify(providers.get(node))})` });
1133
+ }
1134
+ }
1135
+ for (const key in node) {
1136
+ if (key === 'loc' || key === 'metadata' || key === 'parent' || key.startsWith('_octane'))
1137
+ continue;
1138
+ const value = node[key];
1139
+ if (value && typeof value === 'object') visit(value);
1140
+ }
1141
+ }
1142
+ visit(ast);
1143
+ }
1144
+
1145
+ function parseHookSource(source, id) {
1146
+ try {
1147
+ return { ast: parseModule(source, id), canPrint: true };
1148
+ } catch (error) {
1149
+ if (!(error instanceof SyntaxError)) throw error;
1150
+ // Some binding signatures (const generics in function types) need the
1151
+ // native parser. Its object-TS dialect retains types and source offsets,
1152
+ // but drops type parentheses required by the Program printer. Keep that
1153
+ // AST on the surgical path so every authored type stays byte-for-byte.
1154
+ return {
1155
+ ast: parseFallbackModule(source, `${id.split(/[?#]/, 1)[0]}.object.ts`),
1156
+ canPrint: false,
1157
+ };
1158
+ }
1159
+ }
1160
+
1054
1161
  /**
1055
- * Inject per-call-site hook slots into octane BASE hook calls in a plain
1162
+ * Inject per-call-site hook slots into Octane base and imported/aliased hook calls in a plain
1056
1163
  * `.ts`/`.js` module. Returns `null` (pass through unchanged) when the module
1057
1164
  * imports no octane base hook or calls none.
1058
1165
  *
@@ -1064,8 +1171,8 @@ function walk(node, owner, st) {
1064
1171
  * runtime-ranged Symbols. Profiling retains short described Symbols because
1065
1172
  * hook metadata is keyed by Symbol identity.
1066
1173
  * `inlineHookMemo: true` enables the production-client whole-AST memo path;
1067
- * the default remains surgical. `manualSlots: true` permits only that memo
1068
- * rewrite and never injects or changes the module's authored slot policy.
1174
+ * the default remains surgical. `manualSlots: true` permits memo and observed
1175
+ * getter rewrites without injecting or changing the authored slot policy.
1069
1176
  * @returns {{ code: string, map: any } | null}
1070
1177
  */
1071
1178
  export function slotHooks(source, id, options) {
@@ -1077,21 +1184,29 @@ export function slotHooks(source, id, options) {
1077
1184
  );
1078
1185
  }
1079
1186
  let ast;
1187
+ let canPrint;
1080
1188
  try {
1081
- ast = parseModule(source, id);
1189
+ ({ ast, canPrint } = parseHookSource(source, id));
1082
1190
  } catch {
1083
1191
  return null; // let the normal pipeline surface the parse error
1084
1192
  }
1085
1193
  assertStrongMode(ast, source, id, options);
1086
1194
  assertNativeReadDiagnostics(ast, source, id, options);
1087
1195
  const importInfo = octaneHookLocals(ast, options?.nativeReads === true);
1196
+ const manualProviders = options?.manualSlots ? findManualHookProviders(ast) : new Map();
1088
1197
  const nativeReadActivation = options?.nativeReads === true && importsNativeRenderer(ast);
1089
1198
  const canSpecializeRoot =
1199
+ !options?.manualSlots &&
1090
1200
  !options?.hmr &&
1091
1201
  !options?.profile &&
1092
1202
  typeof options?.isVoidComponentImport === 'function' &&
1093
1203
  importInfo.hasOctaneImport;
1094
- if (!importInfo.importsHook && !canSpecializeRoot && !nativeReadActivation) {
1204
+ if (
1205
+ !importInfo.importsHook &&
1206
+ !canSpecializeRoot &&
1207
+ !nativeReadActivation &&
1208
+ !manualProviders.size
1209
+ ) {
1095
1210
  return null;
1096
1211
  }
1097
1212
  // The parsed tree is never mutated: annotateHookCalls returns a COW-rebuilt
@@ -1114,6 +1229,7 @@ export function slotHooks(source, id, options) {
1114
1229
  }
1115
1230
  const getterCalls = importInfo.importsHook ? collectStateGetterCalls(ast) : new WeakSet();
1116
1231
  if (
1232
+ canPrint &&
1117
1233
  options?.inlineHookMemo === true &&
1118
1234
  environment === 'client' &&
1119
1235
  !options?.hmr &&
@@ -1137,9 +1253,8 @@ export function slotHooks(source, id, options) {
1137
1253
  });
1138
1254
  if (inlined !== null) return inlined;
1139
1255
  }
1140
- if (options?.manualSlots === true && !nativeReadActivation) return null;
1141
-
1142
1256
  const st = {
1257
+ manualSlots: options?.manualSlots === true,
1143
1258
  nativeReads: options?.nativeReads === true,
1144
1259
  locals: importInfo.locals,
1145
1260
  source,
@@ -1163,10 +1278,13 @@ export function slotHooks(source, id, options) {
1163
1278
  voidRootName: null,
1164
1279
  };
1165
1280
  if (!st.hmr && !st.profile) st.slotBaseName = allocSlotName(st, '_hs$');
1166
- if (importInfo.importsHook && options?.manualSlots !== true) {
1167
- collectParallelUseEdits(ast, st);
1281
+ if (importInfo.importsHook) {
1282
+ if (!st.manualSlots) collectParallelUseEdits(ast, st);
1168
1283
  for (const node of ast.body || []) walk(node, hookOwner(null, 'module'), st);
1169
1284
  }
1285
+ if (manualProviders.size) {
1286
+ collectManualHookEdits(ast, findManualHookProviders(ast), st);
1287
+ }
1170
1288
  if (canSpecializeRoot) {
1171
1289
  collectVoidRootEdits(ast, st, options.isVoidComponentImport);
1172
1290
  }
@@ -1210,9 +1328,10 @@ export function slotHooks(source, id, options) {
1210
1328
  const otherHelperImports = [...otherHelpers]
1211
1329
  .map(([request, specifiers]) => `import { ${specifiers.join(', ')} } from '${request}';\n`)
1212
1330
  .join('');
1213
- const profileImport = st.profile
1214
- ? "import { __profileHook as _$__profileHook } from 'octane/profiling';\n"
1215
- : '';
1331
+ const profileImport =
1332
+ st.profile && !st.manualSlots
1333
+ ? "import { __profileHook as _$__profileHook } from 'octane/profiling';\n"
1334
+ : '';
1216
1335
  const slotBase =
1217
1336
  !st.hmr && !st.profile && st.nextId > 0
1218
1337
  ? `const ${st.slotBaseName} = /* @__PURE__ */ ${st.hookSlotsName}(${st.nextId});\n`