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.
- package/dist/cjs/compatibility.cjs +35 -0
- package/dist/cjs/component-flags.cjs +8 -1
- package/dist/cjs/css.cjs +9 -0
- package/dist/cjs/dom-tables.cjs +8 -1
- package/dist/cjs/error-codes.client.generated.cjs +15 -2
- package/dist/cjs/error-codes.server.generated.cjs +5 -0
- package/dist/cjs/event-names.cjs +39 -0
- package/dist/cjs/index.cjs +13 -0
- package/dist/cjs/runtime.cjs +1345 -359
- package/dist/cjs/runtime.server.cjs +620 -183
- package/dist/cjs/server/index.cjs +26 -0
- package/dist/cjs/version.cjs +1 -1
- package/dist/compatibility.d.ts +7 -0
- package/dist/compatibility.js +10 -0
- package/dist/compiler/bundler.js +20 -30
- package/dist/compiler/compile-renderer-boundaries.js +1 -0
- package/dist/compiler/compile-universal.js +3 -0
- package/dist/compiler/compile.js +877 -627
- package/dist/compiler/hook-deps.js +100 -3
- package/dist/compiler/hook-names.js +1 -0
- package/dist/compiler/hydrate-boundaries.js +165 -15
- package/dist/compiler/manual-hooks.js +171 -0
- package/dist/compiler/parser.node.js +34 -8
- package/dist/compiler/plain-hook-memo.js +54 -5
- package/dist/compiler/slot-hooks.d.ts +43 -0
- package/dist/compiler/slot-hooks.js +154 -35
- package/dist/compiler/strong-mode.js +836 -3
- package/dist/compiler/style-scopes.js +1134 -0
- package/dist/compiler/volar.LICENSES.txt +1 -1
- package/dist/compiler/volar.js +4543 -2839
- package/dist/component-flags.d.ts +1 -1
- package/dist/component-flags.js +8 -1
- package/dist/css.d.ts +7 -0
- package/dist/css.js +8 -0
- package/dist/dom-tables.js +8 -1
- package/dist/error-codes.client.generated.d.ts +3 -1
- package/dist/error-codes.client.generated.js +15 -2
- package/dist/error-codes.server.generated.d.ts +1 -0
- package/dist/error-codes.server.generated.js +5 -0
- package/dist/event-names.d.ts +1 -0
- package/dist/event-names.js +15 -0
- package/dist/html-tree-validation.d.ts +17 -34
- package/dist/index.d.ts +3 -1
- package/dist/index.js +12 -1
- package/dist/jsx-runtime.d.ts +6 -1
- package/dist/public-types.d.ts +188 -0
- package/dist/public-types.js +0 -0
- package/dist/runtime.d.ts +128 -69
- package/dist/runtime.js +1349 -360
- package/dist/runtime.server.d.ts +67 -15
- package/dist/runtime.server.js +619 -184
- package/dist/server/index.d.ts +4 -2
- package/dist/server/index.js +23 -0
- package/dist/static/index.d.ts +2 -1
- package/dist/static/index.js +3 -1
- package/dist/universal-core.d.ts +8 -1
- package/dist/universal-core.js +95 -9
- package/dist/version.js +1 -1
- 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
|
|
130
|
-
//
|
|
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 =
|
|
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),
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
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 {
|
|
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 =
|
|
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 =
|
|
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 (
|
|
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
|
-
//
|
|
1020
|
-
//
|
|
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
|
|
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
|
|
1068
|
-
*
|
|
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 =
|
|
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 (
|
|
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
|
|
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 =
|
|
1214
|
-
|
|
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`
|