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.
- package/README.md +5 -0
- package/dist/cjs/index.cjs +2 -0
- package/dist/cjs/runtime.cjs +773 -385
- package/dist/cjs/runtime.server.cjs +319 -76
- package/dist/cjs/version.cjs +1 -1
- package/dist/compiler/bundler.js +6 -6
- package/dist/compiler/compile-universal.js +1 -1
- package/dist/compiler/compile.js +407 -126
- package/dist/compiler/context-use.js +154 -0
- package/dist/compiler/hook-deps.js +17 -1
- package/dist/compiler/index.d.ts +1 -3
- package/dist/compiler/native-read-diagnostics.js +21 -34
- package/dist/compiler/slot-hooks.d.ts +0 -1
- package/dist/compiler/slot-hooks.js +9 -3
- package/dist/compiler/strong-mode.js +716 -39
- package/dist/compiler/text-type-facts.js +38 -24
- package/dist/compiler/typescript.d.ts +8 -1
- package/dist/compiler/typescript.js +48 -20
- package/dist/compiler/vite.d.ts +6 -2
- package/dist/compiler/vite.js +85 -5
- package/dist/compiler/volar.LICENSES.txt +1 -1
- package/dist/compiler/volar.d.ts +1 -1
- package/dist/compiler/volar.js +3498 -3484
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -0
- package/dist/runtime.d.ts +19 -5
- package/dist/runtime.js +770 -385
- package/dist/runtime.server.d.ts +7 -6
- package/dist/runtime.server.js +319 -76
- package/dist/signals/index.d.ts +1 -1
- package/dist/signals/types.d.ts +1 -1
- package/dist/universal-core.d.ts +15 -0
- package/dist/universal-core.js +575 -83
- package/dist/version.js +1 -1
- package/package.json +3 -3
|
@@ -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 (
|
|
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
|
package/dist/compiler/index.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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));
|
|
@@ -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,
|
|
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,
|
|
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,
|