octane 0.1.13 → 0.1.17

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.
@@ -193,10 +193,14 @@ function normalizeHmrDialect(value) {
193
193
  */
194
194
  export function findVoidComponentExports(source, id) {
195
195
  let ast;
196
- try {
197
- ast = parseModule(source, id);
198
- } catch {
199
- 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
+ }
200
204
  }
201
205
  const memoLocals = new Set();
202
206
  const declarations = [];
@@ -867,8 +871,9 @@ class OctaneBundlerCompiler {
867
871
  if (environment === 'server' && clientReference !== null) {
868
872
  const stub = createClientOnlyServerStub(code, filename, renderer.id);
869
873
  return {
874
+ ast: stub.ast,
870
875
  code: stub.code,
871
- map: null,
876
+ map: stub.map,
872
877
  kind: 'client-only-stub',
873
878
  renderer,
874
879
  clientReference,
@@ -906,11 +911,11 @@ class OctaneBundlerCompiler {
906
911
  const collectVoidComponentExports =
907
912
  environment === 'client' && options.collectVoidComponentExports === true;
908
913
  let out;
909
- let voidComponentSource = null;
914
+ let voidComponentAst = null;
910
915
  if (collectVoidComponentExports) {
911
916
  const compilation = compileForBundler(code, compileFilename, compileOptions);
912
917
  out = compilation.result;
913
- voidComponentSource = compilation.hydrateSource;
918
+ voidComponentAst = compilation.hydrateAst;
914
919
  } else {
915
920
  out = compile(code, compileFilename, compileOptions);
916
921
  }
@@ -923,10 +928,10 @@ class OctaneBundlerCompiler {
923
928
  renderer,
924
929
  ...(out.universalRuntime === undefined ? null : { universalRuntime: out.universalRuntime }),
925
930
  ...(clientReference === null ? null : { clientReference }),
926
- ...(voidComponentSource === null
931
+ ...(voidComponentAst === null
927
932
  ? null
928
933
  : {
929
- voidComponentExports: findVoidComponentExports(voidComponentSource, filename),
934
+ voidComponentExports: findVoidComponentExports(voidComponentAst, filename),
930
935
  }),
931
936
  ...finishMetadata(collected),
932
937
  };
@@ -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 ?? []) {