octane 0.2.6 → 0.2.7
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/runtime.cjs +435 -274
- package/dist/cjs/runtime.server.cjs +13 -11
- 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 +236 -113
- package/dist/compiler/index.d.ts +1 -3
- package/dist/compiler/native-read-diagnostics.js +43 -35
- package/dist/compiler/slot-hooks.d.ts +0 -1
- package/dist/compiler/slot-hooks.js +3 -3
- 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.d.ts +1 -1
- package/dist/compiler/volar.js +12 -7
- package/dist/runtime.d.ts +11 -2
- package/dist/runtime.js +433 -274
- package/dist/runtime.server.d.ts +1 -1
- package/dist/runtime.server.js +13 -11
- package/dist/signals/index.d.ts +1 -1
- package/dist/signals/types.d.ts +1 -1
- package/dist/universal-core.js +59 -51
- package/dist/version.js +1 -1
- package/package.json +1 -1
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. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createLexicalAnalysis } from './compile-universal.js';
|
|
1
|
+
import { createLexicalAnalysis, forEachRuntimeAstChild } 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';
|
|
@@ -64,16 +64,51 @@ function propertyName(node, computed = false) {
|
|
|
64
64
|
: null;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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.
|
|
74
|
+
*/
|
|
75
|
+
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 (
|
|
81
|
+
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);
|
|
70
100
|
}
|
|
101
|
+
visit(ast);
|
|
102
|
+
return { ...options, nativeReads };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function assertNativeReadOptions(options) {
|
|
71
106
|
if (
|
|
72
107
|
options?.nativeReads === true &&
|
|
73
108
|
options.renderer?.target != null &&
|
|
74
109
|
options.renderer.target !== 'dom'
|
|
75
110
|
) {
|
|
76
|
-
throw new Error('Octane
|
|
111
|
+
throw new Error('Octane native signal reads support only the DOM client and server.');
|
|
77
112
|
}
|
|
78
113
|
}
|
|
79
114
|
|
|
@@ -85,34 +120,7 @@ export function assertNativeReadOptions(options) {
|
|
|
85
120
|
*/
|
|
86
121
|
export function analyzeNativeReadDiagnostics(ast, source, filename, options = {}) {
|
|
87
122
|
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
|
-
}
|
|
123
|
+
if (!options.nativeReads) return [];
|
|
116
124
|
if (options.rendererBoundaries && Object.keys(options.rendererBoundaries).length > 0) {
|
|
117
125
|
const { boundaries } = analyzeRendererBoundaries(source, {
|
|
118
126
|
ast,
|
|
@@ -132,7 +140,7 @@ export function analyzeNativeReadDiagnostics(ast, source, filename, options = {}
|
|
|
132
140
|
filename,
|
|
133
141
|
unsupported.tagRange[0],
|
|
134
142
|
unsupported.tagRange[1],
|
|
135
|
-
`Octane
|
|
143
|
+
`Octane native signal reads support only the DOM client and server; renderer ${JSON.stringify(unsupported.childRenderer)} has no native-read integration.`,
|
|
136
144
|
),
|
|
137
145
|
];
|
|
138
146
|
}
|
|
@@ -536,7 +544,7 @@ export function analyzeNativeReadDiagnostics(ast, source, filename, options = {}
|
|
|
536
544
|
report(
|
|
537
545
|
'OCTANE_NATIVE_SIGNAL_HOOK',
|
|
538
546
|
node,
|
|
539
|
-
'useDerived$ is not available
|
|
547
|
+
'useDerived$ is not available. Create derived$ on an explicitly owned Scope and pass the handle to the component.',
|
|
540
548
|
);
|
|
541
549
|
} else if (node.type === 'Property' && parents.get(node)?.type === 'ObjectExpression') {
|
|
542
550
|
checkName(node.key, propertyName(node.key, node.computed), valueOf(node.value));
|
|
@@ -20,7 +20,7 @@ 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';
|
|
@@ -1284,7 +1284,7 @@ function parseHookSource(source, id) {
|
|
|
1284
1284
|
*
|
|
1285
1285
|
* @param {string} source raw module text
|
|
1286
1286
|
* @param {string} id module id (embedded in the stable Symbol.for key)
|
|
1287
|
-
* @param {{ environment?: 'client' | 'server', strong?: boolean,
|
|
1287
|
+
* @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
1288
|
* `Symbol.for(stableKey)` so a re-imported module resolves the same hook
|
|
1289
1289
|
* slots (state survives HMR); off (ordinary prod builds and SSR) emits
|
|
1290
1290
|
* runtime-ranged Symbols. Profiling retains short described Symbols because
|
|
@@ -1295,7 +1295,6 @@ function parseHookSource(source, id) {
|
|
|
1295
1295
|
* @returns {{ code: string, map: any } | null}
|
|
1296
1296
|
*/
|
|
1297
1297
|
export function slotHooks(source, id, options) {
|
|
1298
|
-
assertNativeReadOptions(options);
|
|
1299
1298
|
const environment = options?.environment ?? 'client';
|
|
1300
1299
|
if (environment !== 'client' && environment !== 'server') {
|
|
1301
1300
|
throw new Error(
|
|
@@ -1309,6 +1308,7 @@ export function slotHooks(source, id, options) {
|
|
|
1309
1308
|
} catch {
|
|
1310
1309
|
return null; // let the normal pipeline surface the parse error
|
|
1311
1310
|
}
|
|
1311
|
+
options = nativeReadOptions(ast, options);
|
|
1312
1312
|
assertStrongMode(ast, source, id, options);
|
|
1313
1313
|
assertNativeReadDiagnostics(ast, source, id, options);
|
|
1314
1314
|
const importInfo = octaneHookLocals(
|
|
@@ -35,7 +35,7 @@ export function textTypeSourceVersion(source) {
|
|
|
35
35
|
* because independently falling back on the client/server could change the
|
|
36
36
|
* hydration protocol. Nested ranges are valid when a child expression contains
|
|
37
37
|
* JSX with child expressions of its own.
|
|
38
|
-
* @returns {Set<string> | null}
|
|
38
|
+
* @returns {{ stringRanges: Set<string>, primitiveRanges: Set<string> } | null}
|
|
39
39
|
*/
|
|
40
40
|
export function createTextTypeFactsLookup(facts, filename, source) {
|
|
41
41
|
if (facts === undefined) return null;
|
|
@@ -61,29 +61,43 @@ export function createTextTypeFactsLookup(facts, filename, source) {
|
|
|
61
61
|
if (typeof facts.projectVersion !== 'string' || facts.projectVersion.length === 0) {
|
|
62
62
|
invalid('projectVersion must identify the TypeScript project snapshot');
|
|
63
63
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
64
|
+
const validateRanges = (value, name) => {
|
|
65
|
+
if (!Array.isArray(value)) invalid(`${name} must be an array`);
|
|
66
|
+
const ranges = new Set();
|
|
67
|
+
let previousStart = -1;
|
|
68
|
+
let previousEnd = -1;
|
|
69
|
+
for (const range of value) {
|
|
70
|
+
if (
|
|
71
|
+
!Array.isArray(range) ||
|
|
72
|
+
range.length !== 2 ||
|
|
73
|
+
!Number.isSafeInteger(range[0]) ||
|
|
74
|
+
!Number.isSafeInteger(range[1]) ||
|
|
75
|
+
range[0] < 0 ||
|
|
76
|
+
range[0] >= range[1] ||
|
|
77
|
+
range[1] > source.length
|
|
78
|
+
) {
|
|
79
|
+
invalid(`${name} contains an invalid authored range`);
|
|
80
|
+
}
|
|
81
|
+
const [start, end] = range;
|
|
82
|
+
if (start < previousStart || (start === previousStart && end <= previousEnd)) {
|
|
83
|
+
invalid(`${name} must be sorted and unique`);
|
|
84
|
+
}
|
|
85
|
+
previousStart = start;
|
|
86
|
+
previousEnd = end;
|
|
87
|
+
ranges.add(`${start}:${end}`);
|
|
79
88
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
89
|
+
return ranges;
|
|
90
|
+
};
|
|
91
|
+
const stringRanges = validateRanges(facts.stringChildRanges, 'stringChildRanges');
|
|
92
|
+
// The optional field lets older project adapters keep supplying version-1
|
|
93
|
+
// string-only snapshots. A numeric proof must never masquerade as string
|
|
94
|
+
// evidence in the compiler's concatenation and local-binding analysis.
|
|
95
|
+
const primitiveRanges = validateRanges(
|
|
96
|
+
facts.primitiveTextChildRanges === undefined ? [] : facts.primitiveTextChildRanges,
|
|
97
|
+
'primitiveTextChildRanges',
|
|
98
|
+
);
|
|
99
|
+
for (const range of primitiveRanges) {
|
|
100
|
+
if (stringRanges.has(range)) invalid('a child has conflicting text proofs');
|
|
87
101
|
}
|
|
88
|
-
return
|
|
102
|
+
return { stringRanges, primitiveRanges };
|
|
89
103
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** Source-bound, serializable primitive-text proofs for JSX child holes. */
|
|
2
2
|
export interface TextTypeFacts {
|
|
3
3
|
readonly version: 1;
|
|
4
4
|
/** Clean absolute filename, with forward-slash separators. */
|
|
@@ -9,11 +9,18 @@ export interface TextTypeFacts {
|
|
|
9
9
|
readonly projectVersion: string;
|
|
10
10
|
/** Sorted, unique, half-open UTF-16 ranges of authored child expressions. */
|
|
11
11
|
readonly stringChildRanges: readonly (readonly [start: number, end: number])[];
|
|
12
|
+
/**
|
|
13
|
+
* Number, bigint, and mixed string/number/bigint children. Older version-1
|
|
14
|
+
* snapshots omit this field and retain their string-only behavior.
|
|
15
|
+
*/
|
|
16
|
+
readonly primitiveTextChildRanges?: readonly (readonly [start: number, end: number])[];
|
|
12
17
|
}
|
|
13
18
|
|
|
14
19
|
export interface TextTypeProjectOptions {
|
|
15
20
|
/** Path to the consumer tsconfig. Relative paths resolve from the process cwd. */
|
|
16
21
|
tsconfig: string;
|
|
22
|
+
/** Root for project-relative renderer module IDs; defaults to the tsconfig directory. */
|
|
23
|
+
root?: string;
|
|
17
24
|
/** The same renderer configuration used by the Octane compiler. */
|
|
18
25
|
renderers?: unknown;
|
|
19
26
|
}
|
|
@@ -188,15 +188,18 @@ function mappedChild(child, sourceMap, generatedChildren, generatedLength) {
|
|
|
188
188
|
|
|
189
189
|
/**
|
|
190
190
|
* TypeScript assignability alone is insufficient: `any` and `never` are both
|
|
191
|
-
* assignable to string.
|
|
192
|
-
*
|
|
193
|
-
*
|
|
191
|
+
* assignable to string. A direct child can use the text binding when every
|
|
192
|
+
* possible value is a primitive string, number, or bigint. Keep a distinct
|
|
193
|
+
* string result for the compiler's string-only proofs; a mixed union is text,
|
|
194
|
+
* but it is not evidence for string concatenation. Branded intersections and
|
|
195
|
+
* bounded generic constraints retain their primitive domain. Boxed values,
|
|
196
|
+
* nullish/boolean unions, and unresolved/error types do not.
|
|
194
197
|
*
|
|
195
198
|
* This is a typed-program contract, not runtime validation of inaccurate
|
|
196
199
|
* declarations or values smuggled through `any`.
|
|
197
200
|
*/
|
|
198
|
-
function
|
|
199
|
-
if (!type || seen.has(type)) return
|
|
201
|
+
function primitiveTextKind(type, checker, seen = new Set()) {
|
|
202
|
+
if (!type || seen.has(type)) return 0;
|
|
200
203
|
const flags = type.flags;
|
|
201
204
|
if (
|
|
202
205
|
flags &
|
|
@@ -207,20 +210,33 @@ function isPrimitiveString(type, checker, seen = new Set()) {
|
|
|
207
210
|
ts.TypeFlags.Undefined |
|
|
208
211
|
ts.TypeFlags.Null)
|
|
209
212
|
) {
|
|
210
|
-
return
|
|
213
|
+
return 0;
|
|
211
214
|
}
|
|
212
|
-
if (flags & ts.TypeFlags.StringLike) return
|
|
213
|
-
if (
|
|
215
|
+
if (flags & ts.TypeFlags.StringLike) return 1;
|
|
216
|
+
if (flags & (ts.TypeFlags.NumberLike | ts.TypeFlags.BigIntLike)) return 2;
|
|
217
|
+
if (seen.size >= 64) return 0;
|
|
214
218
|
seen.add(type);
|
|
215
|
-
let result =
|
|
219
|
+
let result = 0;
|
|
216
220
|
if (type.isUnion()) {
|
|
217
|
-
|
|
218
|
-
|
|
221
|
+
if (type.types.length > 0) {
|
|
222
|
+
result = 1;
|
|
223
|
+
for (const part of type.types) {
|
|
224
|
+
const kind = primitiveTextKind(part, checker, seen);
|
|
225
|
+
if (kind === 0) {
|
|
226
|
+
result = 0;
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
if (kind === 2) result = 2;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
219
232
|
} else if (type.isIntersection()) {
|
|
220
|
-
|
|
233
|
+
for (const part of type.types) {
|
|
234
|
+
result = primitiveTextKind(part, checker, seen);
|
|
235
|
+
if (result !== 0) break;
|
|
236
|
+
}
|
|
221
237
|
} else {
|
|
222
238
|
const constraint = checker.getBaseConstraintOfType(type);
|
|
223
|
-
if (constraint && constraint !== type) result =
|
|
239
|
+
if (constraint && constraint !== type) result = primitiveTextKind(constraint, checker, seen);
|
|
224
240
|
}
|
|
225
241
|
seen.delete(type);
|
|
226
242
|
return result;
|
|
@@ -232,18 +248,23 @@ function overlapsDiagnostic(diagnostic, start, end) {
|
|
|
232
248
|
return diagnostic.start < end && diagnostic.start + diagnostic.length > start;
|
|
233
249
|
}
|
|
234
250
|
|
|
235
|
-
function
|
|
251
|
+
function freezeRanges(ranges) {
|
|
236
252
|
const unique = new Map();
|
|
237
253
|
for (const [start, end] of ranges) unique.set(`${start}:${end}`, [start, end]);
|
|
238
254
|
const sorted = [...unique.values()].sort(
|
|
239
255
|
(left, right) => left[0] - right[0] || left[1] - right[1],
|
|
240
256
|
);
|
|
257
|
+
return Object.freeze(sorted.map((range) => Object.freeze(range)));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function freezeFacts(filename, record, projectVersion, stringRanges, primitiveRanges) {
|
|
241
261
|
return Object.freeze({
|
|
242
262
|
version: TEXT_TYPE_FACTS_VERSION,
|
|
243
263
|
filename,
|
|
244
264
|
sourceVersion: record.version,
|
|
245
265
|
projectVersion,
|
|
246
|
-
stringChildRanges:
|
|
266
|
+
stringChildRanges: freezeRanges(stringRanges),
|
|
267
|
+
primitiveTextChildRanges: freezeRanges(primitiveRanges),
|
|
247
268
|
});
|
|
248
269
|
}
|
|
249
270
|
|
|
@@ -257,6 +278,10 @@ export function createTextTypeProject(options) {
|
|
|
257
278
|
}
|
|
258
279
|
const configFilename = absoluteFilename(options.tsconfig, process.cwd());
|
|
259
280
|
const directory = nodePath.dirname(configFilename);
|
|
281
|
+
// TypeScript resolves relative files from its tsconfig; renderer rules use
|
|
282
|
+
// bundler module IDs, which may be relative to a different project root.
|
|
283
|
+
const rendererRoot =
|
|
284
|
+
options.root === undefined ? directory : absoluteFilename(options.root, process.cwd());
|
|
260
285
|
const renderers = normalizeRendererConfig(options.renderers);
|
|
261
286
|
const caseSensitive = ts.sys.useCaseSensitiveFileNames;
|
|
262
287
|
const sources = new FileMap(caseSensitive);
|
|
@@ -394,7 +419,7 @@ export function createTextTypeProject(options) {
|
|
|
394
419
|
if (cached?.version === version) return cached.code;
|
|
395
420
|
let compilation = null;
|
|
396
421
|
try {
|
|
397
|
-
compilation = compileToVolarMappings(source, rendererFilename(file,
|
|
422
|
+
compilation = compileToVolarMappings(source, rendererFilename(file, rendererRoot), {
|
|
398
423
|
renderers,
|
|
399
424
|
});
|
|
400
425
|
} catch {
|
|
@@ -470,6 +495,7 @@ export function createTextTypeProject(options) {
|
|
|
470
495
|
options: program.getCompilerOptions(),
|
|
471
496
|
references: loadConfig().projectReferences ?? [],
|
|
472
497
|
renderers: renderers.signature,
|
|
498
|
+
rendererRoot,
|
|
473
499
|
roots: roots(),
|
|
474
500
|
inputs,
|
|
475
501
|
}),
|
|
@@ -536,7 +562,8 @@ export function createTextTypeProject(options) {
|
|
|
536
562
|
cached.projectVersion === version
|
|
537
563
|
)
|
|
538
564
|
return cached;
|
|
539
|
-
const
|
|
565
|
+
const stringRanges = [];
|
|
566
|
+
const primitiveRanges = [];
|
|
540
567
|
const compilerOptions = program.getCompilerOptions();
|
|
541
568
|
const strictNullChecks = compilerOptions.strictNullChecks ?? compilerOptions.strict ?? false;
|
|
542
569
|
const sourceFile = program.getSourceFile(file);
|
|
@@ -562,8 +589,9 @@ export function createTextTypeProject(options) {
|
|
|
562
589
|
) {
|
|
563
590
|
return;
|
|
564
591
|
}
|
|
565
|
-
|
|
566
|
-
|
|
592
|
+
const kind = primitiveTextKind(checker.getTypeAtLocation(expression), checker);
|
|
593
|
+
if (kind === 1) stringRanges.push([start, end]);
|
|
594
|
+
else if (kind === 2) primitiveRanges.push([start, end]);
|
|
567
595
|
};
|
|
568
596
|
if (compilation) {
|
|
569
597
|
const sourceMap = new SourceMap(compilation.mappings);
|
|
@@ -583,7 +611,7 @@ export function createTextTypeProject(options) {
|
|
|
583
611
|
}
|
|
584
612
|
}
|
|
585
613
|
}
|
|
586
|
-
const facts = freezeFacts(file, record, version,
|
|
614
|
+
const facts = freezeFacts(file, record, version, stringRanges, primitiveRanges);
|
|
587
615
|
factsCache.set(file, facts);
|
|
588
616
|
return facts;
|
|
589
617
|
};
|
package/dist/compiler/vite.d.ts
CHANGED
|
@@ -87,8 +87,12 @@ export interface OctaneVitePluginOptions {
|
|
|
87
87
|
* @default false
|
|
88
88
|
*/
|
|
89
89
|
strong?: boolean;
|
|
90
|
-
/**
|
|
91
|
-
|
|
90
|
+
/**
|
|
91
|
+
* @experimental Infer primitive child text from a TypeScript project in
|
|
92
|
+
* one-shot production builds. Serve and watched builds retain syntax
|
|
93
|
+
* inference. A relative tsconfig is resolved from the Vite project root.
|
|
94
|
+
*/
|
|
95
|
+
textTypes?: { tsconfig: string };
|
|
92
96
|
/**
|
|
93
97
|
* Path fragments excluded from Octane's plain `.ts`/`.js` hook-slot pass.
|
|
94
98
|
* Prefer package manifest `octane.hookSlots.manual` declarations for bindings.
|
package/dist/compiler/vite.js
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
findDescriptorChildrenExports,
|
|
27
27
|
findDescriptorChildrenImports,
|
|
28
28
|
findVoidComponentImports,
|
|
29
|
+
resolveRendererForFile,
|
|
29
30
|
} from './bundler.js';
|
|
30
31
|
import {
|
|
31
32
|
isPlainCssModuleId,
|
|
@@ -533,12 +534,21 @@ function assertProfilingDefineAvailable(definitions, enabled) {
|
|
|
533
534
|
}
|
|
534
535
|
|
|
535
536
|
export function octane(options = {}) {
|
|
536
|
-
if (options.nativeReads !== undefined && typeof options.nativeReads !== 'boolean') {
|
|
537
|
-
throw new TypeError('octane/compiler/vite: nativeReads must be a boolean when provided.');
|
|
538
|
-
}
|
|
539
537
|
if (options.strong !== undefined && typeof options.strong !== 'boolean') {
|
|
540
538
|
throw new TypeError('octane/compiler/vite: `strong` must be a boolean when provided.');
|
|
541
539
|
}
|
|
540
|
+
if (
|
|
541
|
+
options.textTypes !== undefined &&
|
|
542
|
+
(options.textTypes === null ||
|
|
543
|
+
typeof options.textTypes !== 'object' ||
|
|
544
|
+
Array.isArray(options.textTypes) ||
|
|
545
|
+
Object.keys(options.textTypes).some((key) => key !== 'tsconfig') ||
|
|
546
|
+
typeof options.textTypes.tsconfig !== 'string' ||
|
|
547
|
+
options.textTypes.tsconfig.trim() !== options.textTypes.tsconfig ||
|
|
548
|
+
options.textTypes.tsconfig.length === 0)
|
|
549
|
+
) {
|
|
550
|
+
throw new TypeError('octane/compiler/vite: `textTypes` requires { tsconfig: string }.');
|
|
551
|
+
}
|
|
542
552
|
if (
|
|
543
553
|
options.cssModuleConstants !== undefined &&
|
|
544
554
|
typeof options.cssModuleConstants !== 'function'
|
|
@@ -595,6 +605,63 @@ export function octane(options = {}) {
|
|
|
595
605
|
// CSS naming/virtual providers can differ between them, so never key a proof
|
|
596
606
|
// merely by its path or share a previous build's final-module snapshot.
|
|
597
607
|
const cssModuleProofStates = new Map();
|
|
608
|
+
let textTypeProject = null;
|
|
609
|
+
let createTextTypeProject = null;
|
|
610
|
+
let typedTextEnabled = false;
|
|
611
|
+
const textTypeFiles = new Map();
|
|
612
|
+
const releaseTextTypeProject = () => {
|
|
613
|
+
textTypeProject?.dispose();
|
|
614
|
+
textTypeProject = null;
|
|
615
|
+
};
|
|
616
|
+
const resetTextTypes = () => {
|
|
617
|
+
releaseTextTypeProject();
|
|
618
|
+
createTextTypeProject = null;
|
|
619
|
+
typedTextEnabled = false;
|
|
620
|
+
textTypeFiles.clear();
|
|
621
|
+
};
|
|
622
|
+
const textFactsFor = (source, id, environment) => {
|
|
623
|
+
if (!typedTextEnabled || createTextTypeProject === null) return undefined;
|
|
624
|
+
const file = cleanModuleId(id);
|
|
625
|
+
if (!nodePath.isAbsolute(file) || !/\.(?:tsrx|tsx)$/.test(file) || !nodeFs.existsSync(file)) {
|
|
626
|
+
return undefined;
|
|
627
|
+
}
|
|
628
|
+
// Installed source packages may use another tsconfig and need not be in
|
|
629
|
+
// this project's TypeScript graph. Their ordinary syntax path stays intact.
|
|
630
|
+
if (!compiler._isProjectOwnedSource(file)) return undefined;
|
|
631
|
+
const canonical = compiler._canonicalModuleId(file);
|
|
632
|
+
const pragmaOwned = file.endsWith('.tsx') && compiler._pragmaClaimsOwnership(source);
|
|
633
|
+
if (!compiler._passesOwnershipGate(file, canonical, pragmaOwned)) return undefined;
|
|
634
|
+
const renderer = resolveRendererForFile(compiler.renderers, canonical);
|
|
635
|
+
if (
|
|
636
|
+
renderer.target !== 'dom' ||
|
|
637
|
+
(environment === 'server' && renderer.server === 'client-only')
|
|
638
|
+
)
|
|
639
|
+
return undefined;
|
|
640
|
+
textTypeProject ??= createTextTypeProject({
|
|
641
|
+
tsconfig: nodePath.resolve(projectRoot, options.textTypes.tsconfig),
|
|
642
|
+
root: projectRoot,
|
|
643
|
+
renderers: options.renderers,
|
|
644
|
+
});
|
|
645
|
+
const facts = textTypeProject.snapshot(file, source);
|
|
646
|
+
const previous = textTypeFiles.get(canonical);
|
|
647
|
+
if (
|
|
648
|
+
previous !== undefined &&
|
|
649
|
+
(previous.sourceVersion !== facts.sourceVersion ||
|
|
650
|
+
previous.projectVersion !== facts.projectVersion)
|
|
651
|
+
) {
|
|
652
|
+
throw new Error(
|
|
653
|
+
`Octane text types changed during the client/server build for ${JSON.stringify(canonical)}. Restart the build to keep hydration consistent.`,
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
textTypeFiles.set(canonical, {
|
|
657
|
+
sourceVersion: facts.sourceVersion,
|
|
658
|
+
projectVersion: facts.projectVersion,
|
|
659
|
+
});
|
|
660
|
+
// Runtime compiler IDs are portable root-relative paths. TypeScript reads
|
|
661
|
+
// the real file; only its filename is translated after checking that exact
|
|
662
|
+
// source against the project. Both targets reuse this one proof generation.
|
|
663
|
+
return { ...facts, filename: canonical };
|
|
664
|
+
};
|
|
598
665
|
const cssModuleProofState = (context, environment) => {
|
|
599
666
|
const key = context.environment ?? environment;
|
|
600
667
|
let state = cssModuleProofStates.get(key);
|
|
@@ -617,7 +684,6 @@ export function octane(options = {}) {
|
|
|
617
684
|
exclude: options.exclude,
|
|
618
685
|
profile: profileEnabled,
|
|
619
686
|
strong: options.strong,
|
|
620
|
-
nativeReads: options.nativeReads,
|
|
621
687
|
renderers: options.renderers,
|
|
622
688
|
requireDirective,
|
|
623
689
|
warn,
|
|
@@ -626,6 +692,7 @@ export function octane(options = {}) {
|
|
|
626
692
|
const forceSsr = options.ssr;
|
|
627
693
|
|
|
628
694
|
const resetCompiler = (root) => {
|
|
695
|
+
resetTextTypes();
|
|
629
696
|
descriptorSourceCache.clear();
|
|
630
697
|
descriptorExportCache.clear();
|
|
631
698
|
descriptorGraphCache.clear();
|
|
@@ -637,7 +704,6 @@ export function octane(options = {}) {
|
|
|
637
704
|
exclude: options.exclude,
|
|
638
705
|
profile: profileEnabled,
|
|
639
706
|
strong: options.strong,
|
|
640
|
-
nativeReads: options.nativeReads,
|
|
641
707
|
renderers: options.renderers,
|
|
642
708
|
requireDirective,
|
|
643
709
|
warn,
|
|
@@ -706,12 +772,20 @@ export function octane(options = {}) {
|
|
|
706
772
|
// proof to one-shot production builds where the graph is compiled together.
|
|
707
773
|
specializeProductionRoots = config.command === 'build' && config.build?.watch == null;
|
|
708
774
|
specializeCssModuleConstants = specializeProductionRoots && !hmrEnabled;
|
|
775
|
+
typedTextEnabled =
|
|
776
|
+
options.textTypes !== undefined && specializeProductionRoots && !hmrEnabled;
|
|
777
|
+
if (typedTextEnabled) {
|
|
778
|
+
return import('./typescript.js').then((module) => {
|
|
779
|
+
createTextTypeProject = module.createTextTypeProject;
|
|
780
|
+
});
|
|
781
|
+
}
|
|
709
782
|
},
|
|
710
783
|
buildStart() {
|
|
711
784
|
if (this.environment === undefined) cssModuleProofStates.clear();
|
|
712
785
|
else cssModuleProofStates.delete(this.environment);
|
|
713
786
|
},
|
|
714
787
|
buildEnd(error) {
|
|
788
|
+
if (error) releaseTextTypeProject();
|
|
715
789
|
const keys = this.environment === undefined ? ['client', 'server'] : [this.environment];
|
|
716
790
|
for (const key of keys) {
|
|
717
791
|
const state = cssModuleProofStates.get(key);
|
|
@@ -725,6 +799,8 @@ export function octane(options = {}) {
|
|
|
725
799
|
},
|
|
726
800
|
watchChange(id) {
|
|
727
801
|
compiler.invalidate(id);
|
|
802
|
+
// A one-shot build can receive a watch event mid-build and must fail closed.
|
|
803
|
+
textTypeProject?.invalidate(cleanModuleId(id));
|
|
728
804
|
// A barrel can cache a classification reached through this source, so
|
|
729
805
|
// invalidate the small descriptor graph as a unit on authored edits.
|
|
730
806
|
descriptorSourceCache.clear();
|
|
@@ -732,6 +808,9 @@ export function octane(options = {}) {
|
|
|
732
808
|
descriptorGraphCache.clear();
|
|
733
809
|
cssModuleProofStates.clear();
|
|
734
810
|
},
|
|
811
|
+
closeBundle() {
|
|
812
|
+
releaseTextTypeProject();
|
|
813
|
+
},
|
|
735
814
|
generateBundle(_outputOptions, bundle) {
|
|
736
815
|
if (!emitClientReferenceManifest) return;
|
|
737
816
|
const manifest = clientReferenceManifest(this, bundle);
|
|
@@ -819,6 +898,7 @@ export function octane(options = {}) {
|
|
|
819
898
|
? null
|
|
820
899
|
: { _descriptorChildrenExportsProof: preflight.descriptorExportsProof }),
|
|
821
900
|
environment,
|
|
901
|
+
...(typedTextEnabled ? { textTypeFacts: textFactsFor(code, id, environment) } : null),
|
|
822
902
|
hmr: !server && hmrEnabled ? 'vite' : false,
|
|
823
903
|
// DEV server transforms also carry SSR-only diagnostics. HMR itself
|
|
824
904
|
// remains client-only; an explicit `hmr: false` keeps both transforms
|
package/dist/compiler/volar.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
export function compileToVolarMappings(
|
|
4
4
|
source: string,
|
|
5
5
|
filename?: string,
|
|
6
|
-
options?: { loose?: boolean; renderers?: unknown; strong?: boolean
|
|
6
|
+
options?: { loose?: boolean; renderers?: unknown; strong?: boolean },
|
|
7
7
|
): import('./index.js').VolarCompileResult;
|
|
8
8
|
|
|
9
9
|
/** Virtual TSX and authored ranges used by compiler inspection tooling. */
|
package/dist/compiler/volar.js
CHANGED
|
@@ -32643,7 +32643,7 @@ function parseModule(source, filename, options) {
|
|
|
32643
32643
|
import { buildFatSegments, decodeSourceMappings } from "./fat-segments.js";
|
|
32644
32644
|
import { analyzeNativeChangeDiagnostics } from "./native-change-diagnostics.js";
|
|
32645
32645
|
import { analyzeStrongMode } from "./strong-mode.js";
|
|
32646
|
-
import { analyzeNativeReadDiagnostics } from "./native-read-diagnostics.js";
|
|
32646
|
+
import { analyzeNativeReadDiagnostics, nativeReadOptions } from "./native-read-diagnostics.js";
|
|
32647
32647
|
import { jsxImportSourcePragmaModule } from "./pragma.js";
|
|
32648
32648
|
import {
|
|
32649
32649
|
DOM_RENDERER_MODULE,
|
|
@@ -32765,12 +32765,17 @@ function compileToVolarMappings(source, filename, options) {
|
|
|
32765
32765
|
}).diagnostics;
|
|
32766
32766
|
const strongDiagnostics = options?.strong === true || source.includes("use strong") ? analyzeStrongMode(ast, source, filename, options).diagnostics : null;
|
|
32767
32767
|
if (strongDiagnostics !== null) diagnostics.push(...strongDiagnostics);
|
|
32768
|
-
const nativeReadDiagnostics = analyzeNativeReadDiagnostics(
|
|
32769
|
-
|
|
32770
|
-
|
|
32771
|
-
|
|
32772
|
-
|
|
32773
|
-
|
|
32768
|
+
const nativeReadDiagnostics = analyzeNativeReadDiagnostics(
|
|
32769
|
+
ast,
|
|
32770
|
+
source,
|
|
32771
|
+
filename,
|
|
32772
|
+
nativeReadOptions(ast, {
|
|
32773
|
+
...options,
|
|
32774
|
+
renderer,
|
|
32775
|
+
rendererBoundaries: rendererConfig.boundaries,
|
|
32776
|
+
rendererRegistry: rendererConfig.registry
|
|
32777
|
+
})
|
|
32778
|
+
);
|
|
32774
32779
|
diagnostics.push(...nativeReadDiagnostics);
|
|
32775
32780
|
const rendererPragma = hasAuthoredLeadingPragma(ast, comments) ? null : createRendererTypePragma(renderer, ast);
|
|
32776
32781
|
const printComments = rendererPragma === null ? comments : [rendererPragma, ...comments];
|