octane 0.1.12 → 0.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/compiler/bundler.js +39 -56
  2. package/dist/compiler/client-only-server.js +195 -54
  3. package/dist/compiler/compile-renderer-boundaries.js +448 -536
  4. package/dist/compiler/compile-universal.js +2220 -1500
  5. package/dist/compiler/compile.js +7188 -2959
  6. package/dist/compiler/fat-segments.js +196 -0
  7. package/dist/compiler/hook-deps.js +115 -24
  8. package/dist/compiler/hydrate-boundaries.js +695 -444
  9. package/dist/compiler/native-change-diagnostics.js +2 -1
  10. package/dist/compiler/renderer-boundaries.js +2 -2
  11. package/dist/compiler/renderers.js +16 -1
  12. package/dist/compiler/slot-hooks.js +11 -7
  13. package/dist/compiler/vite.d.ts +8 -2
  14. package/dist/compiler/vite.js +36 -3
  15. package/dist/compiler/volar.js +245 -18
  16. package/dist/constants.d.ts +7 -1
  17. package/dist/constants.js +7 -1
  18. package/dist/devtools-hook.d.ts +64 -0
  19. package/dist/devtools-hook.js +162 -0
  20. package/dist/dom-tables.d.ts +2 -0
  21. package/dist/dom-tables.js +145 -0
  22. package/dist/head-ownership.d.ts +12 -0
  23. package/dist/head-ownership.js +31 -0
  24. package/dist/hydration/event-capture.d.ts +4 -1
  25. package/dist/hydration/event-capture.js +9 -2
  26. package/dist/hydration/types.d.ts +1 -1
  27. package/dist/index.d.ts +1 -1
  28. package/dist/index.js +14 -0
  29. package/dist/jsx-runtime.d.ts +570 -0
  30. package/dist/runtime.d.ts +19 -1
  31. package/dist/runtime.js +1081 -265
  32. package/dist/runtime.server.d.ts +1 -14
  33. package/dist/runtime.server.js +124 -29
  34. package/dist/server/rpc.js +16 -1
  35. package/dist/stream-protocol.d.ts +20 -0
  36. package/dist/stream-protocol.js +68 -0
  37. package/dist/universal-core.d.ts +13 -3
  38. package/dist/universal-core.js +1135 -124
  39. package/dist/universal-dom-boundary.js +49 -6
  40. package/package.json +25 -11
@@ -14,17 +14,14 @@ import * as nodeFs from 'node:fs';
14
14
  import * as nodeModule from 'node:module';
15
15
  import * as nodePath from 'node:path';
16
16
  import { parseModule } from '@tsrx/core';
17
- import { compile, hasOnlyLowerableNullishExits, isVoidJsxCodeBlockFunction } from './compile.js';
18
17
  import {
19
- addSourceMapNeedles,
20
- composeSourceMaps,
21
- validateRendererModuleSource,
22
- } from './compile-universal.js';
23
- import {
24
- hydrateBoundaryPathFromId,
25
- prepareHydrateBoundaries,
26
- prepareServerHydrateBoundaries,
27
- } from './hydrate-boundaries.js';
18
+ compile,
19
+ compileForBundler,
20
+ hasOnlyLowerableNullishExits,
21
+ isVoidJsxCodeBlockFunction,
22
+ } from './compile.js';
23
+ import { validateRendererModuleSource } from './compile-universal.js';
24
+ import { HYDRATE_QUERY_PARAM, hydrateBoundaryPathFromId } from './hydrate-boundaries.js';
28
25
  import {
29
26
  DOM_RENDERER_MODULE,
30
27
  normalizeRendererConfig,
@@ -32,10 +29,7 @@ import {
32
29
  } from './renderers.js';
33
30
  import { findLeadingJsxImportSourcePragma } from './pragma.js';
34
31
  import { normalizeUniversalRuntime } from './universal-runtime.js';
35
- import {
36
- analyzeNativeChangeDiagnostics,
37
- formatCompileDiagnostic,
38
- } from './native-change-diagnostics.js';
32
+ import { formatCompileDiagnostic } from './native-change-diagnostics.js';
39
33
  import { findVoidComponentImports, findVoidRootImports, slotHooks } from './slot-hooks.js';
40
34
  import {
41
35
  assertNoLiveClientOnlyImports,
@@ -199,10 +193,14 @@ function normalizeHmrDialect(value) {
199
193
  */
200
194
  export function findVoidComponentExports(source, id) {
201
195
  let ast;
202
- try {
203
- ast = parseModule(source, id);
204
- } catch {
205
- return [];
196
+ if (source && typeof source === 'object' && source.type === 'Program') {
197
+ ast = source;
198
+ } else {
199
+ try {
200
+ ast = parseModule(source, id);
201
+ } catch {
202
+ return [];
203
+ }
206
204
  }
207
205
  const memoLocals = new Set();
208
206
  const declarations = [];
@@ -873,8 +871,9 @@ class OctaneBundlerCompiler {
873
871
  if (environment === 'server' && clientReference !== null) {
874
872
  const stub = createClientOnlyServerStub(code, filename, renderer.id);
875
873
  return {
874
+ ast: stub.ast,
876
875
  code: stub.code,
877
- map: null,
876
+ map: stub.map,
878
877
  kind: 'client-only-stub',
879
878
  renderer,
880
879
  clientReference,
@@ -883,36 +882,11 @@ class OctaneBundlerCompiler {
883
882
  };
884
883
  }
885
884
  const hasRendererBoundaries = Object.keys(this.renderers.boundaries).length > 0;
886
- const nativeChangeAnalysis = analyzeNativeChangeDiagnostics(
887
- parseModule(code, filename),
888
- code,
889
- filename,
890
- {
891
- dom: renderer.target === 'dom',
892
- renderer,
893
- rendererBoundaries: this.renderers.boundaries,
894
- rendererRegistry: this.renderers.registry,
895
- },
896
- );
897
- const nativeChangeDiagnostics = nativeChangeAnalysis.diagnostics;
898
- const hydratePreparation =
899
- environment === 'client'
900
- ? prepareHydrateBoundaries(code, filename, hydrateBoundaryPath)
901
- : prepareServerHydrateBoundaries(code, filename);
902
- const compileSource = hydratePreparation?.source ?? code;
903
- const out = compile(compileSource, filename, {
904
- __hydratePrepared: true,
905
- __hydrateBoundaryModule: typeof hydratePreparation?.boundaryPath === 'string',
906
- __nativeChangeDiagnostics: nativeChangeDiagnostics,
907
- ...(hydratePreparation === null && !hasRendererBoundaries
908
- ? { __nativeChangeAnalysis: nativeChangeAnalysis }
909
- : null),
910
- // Scoped-style hashes are position-derived; after the extraction
911
- // rewrite the compiler restamps them from these authored
912
- // coordinates so client and server compiles agree (compile.js).
913
- ...(hydratePreparation?.origins != null
914
- ? { __styleRemap: { authored: code, origins: hydratePreparation.origins } }
915
- : null),
885
+ const compileFilename =
886
+ hydrateBoundaryPath === null
887
+ ? filename
888
+ : `${filename}?${HYDRATE_QUERY_PARAM}=${encodeURIComponent(hydrateBoundaryPath)}`;
889
+ const compileOptions = {
916
890
  hmr,
917
891
  mode: environment,
918
892
  dev,
@@ -933,10 +907,17 @@ class OctaneBundlerCompiler {
933
907
  ...(environment === 'client' && typeof options.isVoidComponentImport === 'function'
934
908
  ? { isVoidComponentImport: options.isVoidComponentImport }
935
909
  : null),
936
- });
937
- if (hydratePreparation?.map && out.map) {
938
- out.map = composeSourceMaps(out.map, hydratePreparation.map);
939
- out.map = addSourceMapNeedles(out.map, out.code, code, hydratePreparation.mappingNeedles);
910
+ };
911
+ const collectVoidComponentExports =
912
+ environment === 'client' && options.collectVoidComponentExports === true;
913
+ let out;
914
+ let voidComponentAst = null;
915
+ if (collectVoidComponentExports) {
916
+ const compilation = compileForBundler(code, compileFilename, compileOptions);
917
+ out = compilation.result;
918
+ voidComponentAst = compilation.hydrateAst;
919
+ } else {
920
+ out = compile(code, compileFilename, compileOptions);
940
921
  }
941
922
  this._forwardCompileDiagnostics(out.diagnostics);
942
923
  return {
@@ -947,9 +928,11 @@ class OctaneBundlerCompiler {
947
928
  renderer,
948
929
  ...(out.universalRuntime === undefined ? null : { universalRuntime: out.universalRuntime }),
949
930
  ...(clientReference === null ? null : { clientReference }),
950
- ...(environment === 'client' && options.collectVoidComponentExports === true
951
- ? { voidComponentExports: findVoidComponentExports(compileSource, filename) }
952
- : {}),
931
+ ...(voidComponentAst === null
932
+ ? null
933
+ : {
934
+ voidComponentExports: findVoidComponentExports(voidComponentAst, filename),
935
+ }),
953
936
  ...finishMetadata(collected),
954
937
  };
955
938
  }
@@ -8,7 +8,9 @@
8
8
  * regions have been omitted, so any remaining reference receives a source
9
9
  * diagnostic before a sentinel can affect server behavior.
10
10
  */
11
- import { parseModule } from '@tsrx/core';
11
+ import { builders as b, parseModule } from '@tsrx/core';
12
+ import { print as esrapPrint } from 'esrap';
13
+ import esrapTsx from 'esrap/languages/tsx';
12
14
 
13
15
  const CLIENT_REFERENCE_VERSION = 1;
14
16
  export const CLIENT_REFERENCE_MANIFEST_FILENAME = 'octane-client-references.json';
@@ -26,10 +28,6 @@ function astName(node) {
26
28
  return typeof node?.value === 'string' ? node.value : null;
27
29
  }
28
30
 
29
- function printExportName(name) {
30
- return /^[$A-Z_a-z][$\w]*$/u.test(name) || name === 'default' ? name : JSON.stringify(name);
31
- }
32
-
33
31
  function addPatternNames(pattern, output) {
34
32
  if (!pattern) return;
35
33
  if (pattern.type === 'TSParameterProperty') {
@@ -182,7 +180,10 @@ export function findStaticRuntimeImportRequests(source, filename = 'unknown') {
182
180
  }
183
181
 
184
182
  function collectRuntimeExports(ast, filename) {
185
- const exports = new Set();
183
+ const exports = new Map();
184
+ const addExport = (name, origin) => {
185
+ if (!exports.has(name)) exports.set(name, origin);
186
+ };
186
187
  for (const statement of ast.body ?? []) {
187
188
  if (statement.type === 'ExportDefaultDeclaration') {
188
189
  if (
@@ -190,7 +191,7 @@ function collectRuntimeExports(ast, filename) {
190
191
  statement.declaration?.declare !== true &&
191
192
  statement.declaration?.type !== 'TSInterfaceDeclaration'
192
193
  ) {
193
- exports.add('default');
194
+ addExport('default', statement);
194
195
  }
195
196
  continue;
196
197
  }
@@ -208,14 +209,14 @@ function collectRuntimeExports(ast, filename) {
208
209
  statement.importKind !== 'type' &&
209
210
  statement.id?.name
210
211
  ) {
211
- exports.add(statement.id.name);
212
+ addExport(statement.id.name, statement.id);
212
213
  continue;
213
214
  }
214
215
  if (statement.type === 'ExportAllDeclaration') {
215
216
  if (statement.exportKind === 'type') continue;
216
217
  const exported = astName(statement.exported);
217
218
  if (exported !== null) {
218
- exports.add(exported);
219
+ addExport(exported, statement.exported ?? statement);
219
220
  continue;
220
221
  }
221
222
  throw diagnosticError(
@@ -230,7 +231,11 @@ function collectRuntimeExports(ast, filename) {
230
231
  const declaration = statement.declaration;
231
232
  if (declaration?.declare === true) continue;
232
233
  if (declaration?.type === 'VariableDeclaration') {
233
- for (const item of declaration.declarations ?? []) addPatternNames(item.id, exports);
234
+ for (const item of declaration.declarations ?? []) {
235
+ const names = new Set();
236
+ addPatternNames(item.id, names);
237
+ for (const name of names) addExport(name, item.id);
238
+ }
234
239
  } else if (
235
240
  declaration?.id &&
236
241
  (declaration.type === 'FunctionDeclaration' ||
@@ -238,15 +243,92 @@ function collectRuntimeExports(ast, filename) {
238
243
  declaration.type === 'TSEnumDeclaration' ||
239
244
  declaration.type === 'TSModuleDeclaration')
240
245
  ) {
241
- exports.add(declaration.id.name);
246
+ addExport(declaration.id.name, declaration.id);
242
247
  }
243
248
  for (const specifier of statement.specifiers ?? []) {
244
249
  if (specifier.exportKind === 'type') continue;
245
250
  const exported = astName(specifier.exported);
246
- if (exported !== null) exports.add(exported);
251
+ if (exported !== null) addExport(exported, specifier.exported ?? specifier);
247
252
  }
248
253
  }
249
- return [...exports].sort();
254
+ return [...exports]
255
+ .map(([name, origin]) => ({ name, origin }))
256
+ .sort((left, right) => (left.name < right.name ? -1 : left.name > right.name ? 1 : 0));
257
+ }
258
+
259
+ /**
260
+ * Generated stub scaffolding has no authored syntax of its own, but every
261
+ * printed node still inherits a source origin so the auxiliary module carries
262
+ * a useful esrap map. The tree is compiler-owned; parsed nodes are never
263
+ * embedded or mutated.
264
+ */
265
+ function inheritGeneratedOrigin(root, origin) {
266
+ const seen = new WeakSet();
267
+ const visit = (value) => {
268
+ if (!value || typeof value !== 'object' || seen.has(value)) return;
269
+ seen.add(value);
270
+ if (Array.isArray(value)) {
271
+ for (const item of value) visit(item);
272
+ return;
273
+ }
274
+ if (typeof value.type === 'string' && value.loc == null && origin?.loc != null) {
275
+ value.start = origin.start;
276
+ value.end = origin.end;
277
+ value.loc = origin.loc;
278
+ }
279
+ for (const [key, child] of Object.entries(value)) {
280
+ if (key !== 'loc' && key !== 'metadata') visit(child);
281
+ }
282
+ };
283
+ visit(root);
284
+ return root;
285
+ }
286
+
287
+ function createProgram(body, origin) {
288
+ return inheritGeneratedOrigin(
289
+ {
290
+ type: 'Program',
291
+ sourceType: 'module',
292
+ body,
293
+ metadata: { path: [] },
294
+ },
295
+ origin,
296
+ );
297
+ }
298
+
299
+ const jsonStringifyCall = (expression) => b.call(b.member(b.id('JSON'), 'stringify'), expression);
300
+
301
+ function clientOnlyStubVisitors() {
302
+ const visitors = esrapTsx();
303
+ const printExportSpecifier = visitors.ExportSpecifier;
304
+ return {
305
+ ...visitors,
306
+ ExportSpecifier(node, context) {
307
+ // ESTree represents quoted export names as Literals. esrap 2.3's TS
308
+ // visitor currently prints only Identifier→Identifier aliases; retain
309
+ // the standard AST and teach this one print the missing literal arm.
310
+ if (node.exported.type === 'Literal') {
311
+ context.visit(node.local);
312
+ context.write(' as ');
313
+ context.visit(node.exported);
314
+ return;
315
+ }
316
+ printExportSpecifier(node, context);
317
+ },
318
+ };
319
+ }
320
+
321
+ function printClientOnlyServerStub(program, source, filename, exports) {
322
+ const printed = esrapPrint(program, clientOnlyStubVisitors(), {
323
+ sourceMapSource: filename,
324
+ sourceMapContent: source,
325
+ });
326
+ return {
327
+ ast: program,
328
+ code: printed.code,
329
+ map: printed.map,
330
+ exports,
331
+ };
250
332
  }
251
333
 
252
334
  /**
@@ -255,48 +337,102 @@ function collectRuntimeExports(ast, filename) {
255
337
  */
256
338
  export function createClientOnlyServerStub(source, filename, renderer) {
257
339
  const ast = parseModule(source, filename);
258
- const exports = collectRuntimeExports(ast, filename);
259
- const lines = [
260
- `const __octaneClientOnlyModule = ${JSON.stringify(filename)};`,
261
- `const __octaneClientOnlyRenderer = ${JSON.stringify(renderer)};`,
262
- 'function __octaneClientOnlyExport(name) {',
263
- '\tconst fail = () => {',
264
- '\t\tconst error = new Error(`Client-only export ${JSON.stringify(name)} from ${JSON.stringify(__octaneClientOnlyModule)} (renderer ${JSON.stringify(__octaneClientOnlyRenderer)}) was used by the server graph.`);',
265
- "\t\terror.code = 'OCTANE_CLIENT_ONLY_SERVER_USE';",
266
- '\t\terror.filename = __octaneClientOnlyModule;',
267
- '\t\tthrow error;',
268
- '\t};',
269
- '\treturn new Proxy(fail, {',
270
- '\t\tapply: fail,',
271
- '\t\tconstruct: fail,',
272
- '\t\tdefineProperty: fail,',
273
- '\t\tdeleteProperty: fail,',
274
- '\t\tget: fail,',
275
- '\t\tgetOwnPropertyDescriptor: fail,',
276
- '\t\tgetPrototypeOf: fail,',
277
- '\t\thas: fail,',
278
- '\t\tisExtensible: fail,',
279
- '\t\townKeys: fail,',
280
- '\t\tpreventExtensions: fail,',
281
- '\t\tset: fail,',
282
- '\t\tsetPrototypeOf: fail,',
283
- '\t});',
284
- '}',
285
- ];
286
- for (let index = 0; index < exports.length; index++) {
287
- lines.push(
288
- `const __octaneClientOnlyExport${index} = __octaneClientOnlyExport(${JSON.stringify(exports[index])});`,
289
- );
340
+ const exportEntries = collectRuntimeExports(ast, filename);
341
+ const exports = exportEntries.map((entry) => entry.name);
342
+ if (exportEntries.length === 0) {
343
+ return printClientOnlyServerStub(createProgram([], ast), source, filename, exports);
290
344
  }
291
- if (exports.length === 0) lines.push('export {};');
292
- else {
293
- lines.push(
294
- `export { ${exports
295
- .map((name, index) => `__octaneClientOnlyExport${index} as ${printExportName(name)}`)
296
- .join(', ')} };`,
345
+ const message = b.template(
346
+ [
347
+ b.quasi('Client-only export '),
348
+ b.quasi(' from '),
349
+ b.quasi(' (renderer '),
350
+ b.quasi(') was used by the server graph.', true),
351
+ ],
352
+ [
353
+ jsonStringifyCall(b.id('name')),
354
+ jsonStringifyCall(b.id('__octaneClientOnlyModule')),
355
+ jsonStringifyCall(b.id('__octaneClientOnlyRenderer')),
356
+ ],
357
+ );
358
+ const fail = b.arrow(
359
+ [],
360
+ b.block([
361
+ b.const('error', b.new('Error', undefined, message)),
362
+ b.stmt(
363
+ b.assignment(
364
+ '=',
365
+ b.member(b.id('error'), 'code'),
366
+ b.literal('OCTANE_CLIENT_ONLY_SERVER_USE'),
367
+ ),
368
+ ),
369
+ b.stmt(
370
+ b.assignment('=', b.member(b.id('error'), 'filename'), b.id('__octaneClientOnlyModule')),
371
+ ),
372
+ { type: 'ThrowStatement', argument: b.id('error'), metadata: { path: [] } },
373
+ ]),
374
+ );
375
+ const traps = [
376
+ 'apply',
377
+ 'construct',
378
+ 'defineProperty',
379
+ 'deleteProperty',
380
+ 'get',
381
+ 'getOwnPropertyDescriptor',
382
+ 'getPrototypeOf',
383
+ 'has',
384
+ 'isExtensible',
385
+ 'ownKeys',
386
+ 'preventExtensions',
387
+ 'set',
388
+ 'setPrototypeOf',
389
+ ];
390
+ const body = [
391
+ b.const('__octaneClientOnlyModule', b.literal(filename)),
392
+ b.const('__octaneClientOnlyRenderer', b.literal(renderer)),
393
+ b.function_declaration(
394
+ b.id('__octaneClientOnlyExport'),
395
+ [b.id('name')],
396
+ b.block([
397
+ b.const('fail', fail),
398
+ b.return(
399
+ b.new(
400
+ 'Proxy',
401
+ undefined,
402
+ b.id('fail'),
403
+ b.object(traps.map((name) => b.init(name, b.id('fail')))),
404
+ ),
405
+ ),
406
+ ]),
407
+ ),
408
+ ];
409
+ for (let index = 0; index < exportEntries.length; index++) {
410
+ body.push(
411
+ inheritGeneratedOrigin(
412
+ b.const(
413
+ `__octaneClientOnlyExport${index}`,
414
+ b.call('__octaneClientOnlyExport', b.literal(exportEntries[index].name)),
415
+ ),
416
+ exportEntries[index].origin,
417
+ ),
297
418
  );
298
419
  }
299
- return Object.freeze({ code: lines.join('\n') + '\n', exports: Object.freeze(exports) });
420
+ const exportSpecifiers = exportEntries.map((entry, index) =>
421
+ inheritGeneratedOrigin(
422
+ b.export_specifier(
423
+ `__octaneClientOnlyExport${index}`,
424
+ /^[$A-Z_a-z][$\w]*$/u.test(entry.name) ? b.id(entry.name) : b.literal(entry.name),
425
+ ),
426
+ entry.origin,
427
+ ),
428
+ );
429
+ body.push(b.export(null, exportSpecifiers));
430
+ // A Program can end at column zero when the source has a trailing newline.
431
+ // Structural nodes need an authored syntax range whose end is a real token:
432
+ // esrap anchors a generated closing delimiter immediately before `loc.end`.
433
+ const scaffoldOrigin = exportEntries[0].origin;
434
+ const program = createProgram(body, scaffoldOrigin);
435
+ return printClientOnlyServerStub(program, source, filename, exports);
300
436
  }
301
437
 
302
438
  function bindingNames(pattern) {
@@ -371,10 +507,15 @@ function importedName(specifier) {
371
507
  * rewrite. Side-effect imports and unused bindings are safe because their
372
508
  * target compiles to a no-op stub.
373
509
  */
374
- export function assertNoLiveClientOnlyImports(source, filename, clientOnlyImports = []) {
510
+ export function assertNoLiveClientOnlyImports(
511
+ source,
512
+ filename,
513
+ clientOnlyImports = [],
514
+ parsedAst = null,
515
+ ) {
375
516
  if (!Array.isArray(clientOnlyImports) || clientOnlyImports.length === 0) return;
376
517
  const byRequest = new Map(clientOnlyImports.map((entry) => [entry.request, entry]));
377
- const ast = parseModule(source, filename);
518
+ const ast = parsedAst ?? parseModule(source, filename);
378
519
  const imported = new Map();
379
520
 
380
521
  for (const statement of ast.body ?? []) {