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.
@@ -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; nativeReads?: 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
- export function assertNativeReadOptions(options) {
68
- if (options?.nativeReads !== undefined && typeof options.nativeReads !== 'boolean') {
69
- throw new TypeError('Octane nativeReads must be a boolean.');
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 nativeReads currently supports only the DOM client and server.');
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 !== true) {
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 nativeReads currently supports only the DOM client and server; renderer ${JSON.stringify(unsupported.childRenderer)} has no native-read integration.`,
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 in this experimental integration. Create derived$ on an explicitly owned Scope and pass the handle to the component.',
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));
@@ -4,7 +4,6 @@ import type { CompilerProgram, CompileSourceMap } from './index.js';
4
4
  export interface SlotHooksOptions {
5
5
  environment?: 'client' | 'server';
6
6
  strong?: boolean;
7
- nativeReads?: boolean;
8
7
  hmr?: boolean;
9
8
  dev?: boolean;
10
9
  profile?: boolean;
@@ -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, assertNativeReadOptions } from './native-read-diagnostics.js';
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, nativeReads?: 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
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
- if (!Array.isArray(facts.stringChildRanges)) invalid('stringChildRanges must be an array');
65
- const ranges = new Set();
66
- let previousStart = -1;
67
- let previousEnd = -1;
68
- for (const range of facts.stringChildRanges) {
69
- if (
70
- !Array.isArray(range) ||
71
- range.length !== 2 ||
72
- !Number.isSafeInteger(range[0]) ||
73
- !Number.isSafeInteger(range[1]) ||
74
- range[0] < 0 ||
75
- range[0] >= range[1] ||
76
- range[1] > source.length
77
- ) {
78
- invalid('stringChildRanges contains an invalid authored range');
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
- const [start, end] = range;
81
- if (start < previousStart || (start === previousStart && end <= previousEnd)) {
82
- invalid('stringChildRanges must be sorted and unique');
83
- }
84
- previousStart = start;
85
- previousEnd = end;
86
- ranges.add(`${start}:${end}`);
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 ranges;
102
+ return { stringRanges, primitiveRanges };
89
103
  }
@@ -1,4 +1,4 @@
1
- /** A source-bound, serializable primitive-string proof for JSX child holes. */
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. Only a primitive-string domain is useful here. Branded
192
- * string intersections and bounded generic constraints retain that domain;
193
- * mixed unions, boxed String, and unresolved/error types do not.
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 isPrimitiveString(type, checker, seen = new Set()) {
199
- if (!type || seen.has(type)) return false;
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 false;
213
+ return 0;
211
214
  }
212
- if (flags & ts.TypeFlags.StringLike) return true;
213
- if (seen.size >= 64) return false;
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 = false;
219
+ let result = 0;
216
220
  if (type.isUnion()) {
217
- result =
218
- type.types.length > 0 && type.types.every((part) => isPrimitiveString(part, checker, seen));
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
- result = type.types.some((part) => isPrimitiveString(part, checker, seen));
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 = isPrimitiveString(constraint, checker, seen);
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 freezeFacts(filename, record, projectVersion, ranges) {
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: Object.freeze(sorted.map((range) => Object.freeze(range))),
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, directory), {
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 ranges = [];
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
- if (isPrimitiveString(checker.getTypeAtLocation(expression), checker))
566
- ranges.push([start, end]);
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, ranges);
614
+ const facts = freezeFacts(file, record, version, stringRanges, primitiveRanges);
587
615
  factsCache.set(file, facts);
588
616
  return facts;
589
617
  };
@@ -87,8 +87,12 @@ export interface OctaneVitePluginOptions {
87
87
  * @default false
88
88
  */
89
89
  strong?: boolean;
90
- /** Experimental native signal reads in DOM client/server render scopes. */
91
- nativeReads?: boolean;
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.
@@ -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
@@ -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; nativeReads?: 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. */
@@ -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(ast, source, filename, {
32769
- ...options,
32770
- renderer,
32771
- rendererBoundaries: rendererConfig.boundaries,
32772
- rendererRegistry: rendererConfig.registry
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];