octane 0.4.2 → 0.4.3

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 (57) hide show
  1. package/dist/cjs/context-identity.cjs +29 -0
  2. package/dist/cjs/dom-stage.cjs +24 -18
  3. package/dist/cjs/error-codes.server.generated.cjs +5 -0
  4. package/dist/cjs/hydration-markers.cjs +12 -0
  5. package/dist/cjs/index.cjs +2 -0
  6. package/dist/cjs/runtime.cjs +253 -393
  7. package/dist/cjs/runtime.server.cjs +137 -35
  8. package/dist/cjs/server/index.cjs +2 -0
  9. package/dist/cjs/server/streamed-signals.cjs +41 -9
  10. package/dist/cjs/signals/client.cjs +1 -6
  11. package/dist/cjs/signals/engine.cjs +1 -12
  12. package/dist/cjs/version.cjs +1 -1
  13. package/dist/compiler/client-only-server.js +1 -28
  14. package/dist/compiler/compile-renderer-boundaries.js +1 -22
  15. package/dist/compiler/compile-universal.js +1 -22
  16. package/dist/compiler/compile.js +782 -359
  17. package/dist/compiler/dom-binding-fixed-props.js +11 -1
  18. package/dist/compiler/generated-origin.js +46 -0
  19. package/dist/compiler/hydrate-boundaries.js +88 -52
  20. package/dist/compiler/plain-hook-memo.js +5 -1
  21. package/dist/compiler/pure-factories.js +66 -0
  22. package/dist/compiler/register.js +4 -1
  23. package/dist/compiler/slot-hooks.js +113 -186
  24. package/dist/compiler/typescript-lowering.js +763 -0
  25. package/dist/compiler/void-roots.js +239 -0
  26. package/dist/compiler/volar.js +91 -38
  27. package/dist/context-identity.d.ts +14 -0
  28. package/dist/context-identity.js +28 -0
  29. package/dist/dom-stage.js +24 -18
  30. package/dist/error-codes.server.generated.d.ts +1 -0
  31. package/dist/error-codes.server.generated.js +5 -0
  32. package/dist/hydration/idle.js +1 -0
  33. package/dist/hydration/independent-island.js +53 -2
  34. package/dist/hydration/media.js +1 -0
  35. package/dist/hydration/types.d.ts +5 -0
  36. package/dist/hydration/visible.js +1 -0
  37. package/dist/hydration-markers.d.ts +8 -0
  38. package/dist/hydration-markers.js +8 -0
  39. package/dist/index.d.ts +1 -1
  40. package/dist/index.js +2 -0
  41. package/dist/runtime.d.ts +25 -40
  42. package/dist/runtime.js +254 -398
  43. package/dist/runtime.server.d.ts +23 -2
  44. package/dist/runtime.server.js +142 -36
  45. package/dist/server/index.d.ts +1 -1
  46. package/dist/server/index.js +2 -0
  47. package/dist/server/streamed-signals.js +41 -9
  48. package/dist/signals/client.js +3 -8
  49. package/dist/signals/engine.d.ts +0 -2
  50. package/dist/signals/engine.js +1 -13
  51. package/dist/universal-core.js +1 -1
  52. package/dist/version.js +1 -1
  53. package/package.json +1 -1
  54. package/dist/cjs/signals/diagnostic-local-hook.cjs +0 -37
  55. package/dist/compiler/local-void-roots.js +0 -154
  56. package/dist/signals/diagnostic-local-hook.d.ts +0 -8
  57. package/dist/signals/diagnostic-local-hook.js +0 -12
@@ -239,8 +239,18 @@ export function fixedBindingProps(fn, fixed, lexical, localDeclaration, isRefere
239
239
  result.type === 'Property' &&
240
240
  result.shorthand &&
241
241
  result.value !== node.value
242
- )
242
+ ) {
243
+ // Expanding this shorthand must retain an own data property, not
244
+ // introduce the object-literal prototype setter syntax.
245
+ if (!node.computed && node.key.name === '__proto__')
246
+ return {
247
+ ...result,
248
+ key: literal('__proto__', node.key),
249
+ computed: true,
250
+ shorthand: false,
251
+ };
243
252
  return { ...result, shorthand: false };
253
+ }
244
254
  return result;
245
255
  };
246
256
  return { fold, known, unknown: UNKNOWN, checks, literal };
@@ -0,0 +1,46 @@
1
+ const SKIP_KEYS = new Set(['type', 'loc', 'start', 'end', 'range', 'metadata', 'parent']);
2
+
3
+ /**
4
+ * Give every location-less node under `root` the authored origin of `origin`,
5
+ * so generated scaffolding still maps into the source. Copy-on-write: adopted
6
+ * parser nodes are never mutated (tests deep-freeze them), and the returned
7
+ * tree must replace `root`. Stylesheet descendants use offsets into their own
8
+ * CSS source rather than JavaScript locations, so they are left intact.
9
+ */
10
+ export function inheritGeneratedOrigin(root, origin) {
11
+ if (origin?.loc == null) return root;
12
+ const seen = new WeakMap();
13
+ const visit = (value) => {
14
+ if (!value || typeof value !== 'object') return value;
15
+ if (value.type === 'StyleSheet') return value;
16
+ if (seen.has(value)) return seen.get(value);
17
+ seen.set(value, value);
18
+ if (Array.isArray(value)) {
19
+ let output = null;
20
+ for (let index = 0; index < value.length; index++) {
21
+ const mapped = visit(value[index]);
22
+ if (output === null && mapped !== value[index]) output = value.slice(0, index);
23
+ if (output !== null) output.push(mapped);
24
+ }
25
+ const result = output ?? value;
26
+ seen.set(value, result);
27
+ return result;
28
+ }
29
+ let output = null;
30
+ if (typeof value.type === 'string' && value.loc == null) {
31
+ output = { ...value, start: origin.start, end: origin.end, loc: origin.loc };
32
+ }
33
+ for (const [key, child] of Object.entries(value)) {
34
+ if (SKIP_KEYS.has(key)) continue;
35
+ const mapped = visit(child);
36
+ if (mapped !== child) {
37
+ if (output === null) output = { ...value };
38
+ output[key] = mapped;
39
+ }
40
+ }
41
+ const result = output ?? value;
42
+ seen.set(value, result);
43
+ return result;
44
+ };
45
+ return visit(root);
46
+ }
@@ -8,6 +8,7 @@
8
8
  * intentionally instantiated afresh for every resource query.
9
9
  */
10
10
  import { builders as b, parseModule, strongHash } from '@tsrx/core';
11
+ import { inheritGeneratedOrigin } from './generated-origin.js';
11
12
  import {
12
13
  createLexicalAnalysis,
13
14
  forEachRuntimeAstChild,
@@ -128,45 +129,6 @@ const TRANSPARENT_TS_EXPRESSIONS = new Set([
128
129
  'TSTypeAssertion',
129
130
  ]);
130
131
 
131
- function inheritGeneratedOrigin(root, origin) {
132
- const seen = new WeakMap();
133
- const visit = (value) => {
134
- if (!value || typeof value !== 'object') return value;
135
- // Stylesheet descendants use offsets into their own CSS source and do
136
- // not carry JavaScript locations. Keep that adopted grammar intact.
137
- if (value.type === 'StyleSheet') return value;
138
- if (seen.has(value)) return seen.get(value);
139
- seen.set(value, value);
140
- if (Array.isArray(value)) {
141
- let output = null;
142
- for (let index = 0; index < value.length; index++) {
143
- const mapped = visit(value[index]);
144
- if (output === null && mapped !== value[index]) output = value.slice(0, index);
145
- if (output !== null) output.push(mapped);
146
- }
147
- const result = output ?? value;
148
- seen.set(value, result);
149
- return result;
150
- }
151
- let output = null;
152
- if (typeof value.type === 'string' && value.loc == null && origin?.loc != null) {
153
- output = { ...value, start: origin.start, end: origin.end, loc: origin.loc };
154
- }
155
- for (const [key, child] of Object.entries(value)) {
156
- if (SKIP_KEYS.has(key)) continue;
157
- const mapped = visit(child);
158
- if (mapped !== child) {
159
- if (output === null) output = { ...value };
160
- output[key] = mapped;
161
- }
162
- }
163
- const result = output ?? value;
164
- seen.set(value, result);
165
- return result;
166
- };
167
- return visit(root);
168
- }
169
-
170
132
  function mapAstCow(value, replace) {
171
133
  if (!value || typeof value !== 'object') return value;
172
134
  if (Array.isArray(value)) {
@@ -416,6 +378,7 @@ function collectImports(ast) {
416
378
  const hydrateNames = new Set();
417
379
  const hookNames = new Set();
418
380
  const neverNames = new Set();
381
+ const conditionNames = new Set();
419
382
  const importBindings = new Set();
420
383
  const declarations = [];
421
384
  for (const statement of ast.body ?? []) {
@@ -448,14 +411,16 @@ function collectImports(ast) {
448
411
  statement.importKind !== 'type' &&
449
412
  specifier.type === 'ImportSpecifier' &&
450
413
  specifier.importKind !== 'type' &&
451
- nameOf(specifier.imported) === 'never' &&
414
+ (nameOf(specifier.imported) === 'never' || nameOf(specifier.imported) === 'condition') &&
452
415
  specifier.local?.name
453
416
  ) {
454
- neverNames.add(specifier.local.name);
417
+ (nameOf(specifier.imported) === 'never' ? neverNames : conditionNames).add(
418
+ specifier.local.name,
419
+ );
455
420
  }
456
421
  }
457
422
  }
458
- return { declarations, hookNames, hydrateNames, neverNames, importBindings };
423
+ return { declarations, hookNames, hydrateNames, neverNames, conditionNames, importBindings };
459
424
  }
460
425
 
461
426
  function addRelevantBindings(pattern, shadowed, relevant) {
@@ -546,6 +511,41 @@ function literalIndependentEnabled(node, filename) {
546
511
  return enabled;
547
512
  }
548
513
 
514
+ /**
515
+ * An independent island activates without its lexical parent, so the client
516
+ * cannot re-evaluate `when`. The server serializes each built-in automatic
517
+ * strategy's parameters; reject the authored forms that need the parent:
518
+ * function-form `when` and `condition()`. Opaque values are checked at SSR.
519
+ */
520
+ function assertIndependentWhen(node, conditionNames, shadowedImports, filename) {
521
+ for (const attribute of node.openingElement?.attributes ?? []) {
522
+ if (jsxAttributeName(attribute) !== 'when') continue;
523
+ const raw =
524
+ attribute.value?.type === 'JSXExpressionContainer'
525
+ ? attribute.value.expression
526
+ : attribute.value;
527
+ const expression = unwrapExpression(raw);
528
+ const callee =
529
+ expression?.type === 'CallExpression' ? unwrapExpression(expression.callee) : null;
530
+ const kind =
531
+ expression?.type === 'ArrowFunctionExpression' || expression?.type === 'FunctionExpression'
532
+ ? 'a function-form `when`'
533
+ : callee?.type === 'Identifier' &&
534
+ conditionNames.has(callee.name) &&
535
+ !shadowedImports.has(callee.name)
536
+ ? '`condition()`'
537
+ : null;
538
+ if (kind !== null) {
539
+ throw extractionError(
540
+ 'OCTANE_HYDRATE_INDEPENDENT_WHEN',
541
+ filename,
542
+ attribute,
543
+ `an independent boundary cannot use ${kind} because its lexical parent never runs on the client to re-evaluate it. Use load(), idle(), visible(), media(), interaction(), or never(), or remove \`independent\``,
544
+ );
545
+ }
546
+ }
547
+ }
548
+
549
549
  /**
550
550
  * Recognize only the exact server-only form whose client subtree may be erased.
551
551
  * Spreads and indirect strategies stay on the ordinary persistent-wrapper path:
@@ -926,6 +926,7 @@ export function analyzeHydrateBoundaries(source, filename = 'unknown.tsrx', pars
926
926
  const index = siblings.length;
927
927
  const path = parentBoundary === null ? String(index) : `${parentBoundary.path}.${index}`;
928
928
  const independent = literalIndependentEnabled(node, filename);
929
+ if (independent) assertIndependentWhen(node, imports.conditionNames, shadowed, filename);
929
930
  boundary = {
930
931
  children: [],
931
932
  disabled: literalSplitDisabled(node),
@@ -1239,9 +1240,41 @@ function captureBindingScope(lexical, node, name) {
1239
1240
  );
1240
1241
  }
1241
1242
 
1243
+ // A pattern binding's value is its declarator initializer or, when that part is
1244
+ // undefined, any default on the path to it (`const { value = new Map() } = props`).
1245
+ function recordPatternInitializers(pattern, scopeNode, values, record) {
1246
+ if (!pattern) return;
1247
+ if (pattern.type === 'TSParameterProperty') {
1248
+ recordPatternInitializers(pattern.parameter, scopeNode, values, record);
1249
+ } else if (pattern.type === 'Identifier') {
1250
+ for (const value of values) record(pattern.name, scopeNode, value);
1251
+ if (values.length === 0) record(pattern.name, scopeNode, null);
1252
+ } else if (pattern.type === 'AssignmentPattern') {
1253
+ recordPatternInitializers(pattern.left, scopeNode, [...values, pattern.right], record);
1254
+ } else if (pattern.type === 'RestElement') {
1255
+ recordPatternInitializers(pattern.argument, scopeNode, values, record);
1256
+ } else if (pattern.type === 'ArrayPattern') {
1257
+ for (const element of pattern.elements ?? []) {
1258
+ recordPatternInitializers(element, scopeNode, values, record);
1259
+ }
1260
+ } else if (pattern.type === 'ObjectPattern') {
1261
+ for (const property of pattern.properties ?? []) {
1262
+ recordPatternInitializers(property.argument ?? property.value, scopeNode, values, record);
1263
+ }
1264
+ }
1265
+ }
1266
+
1242
1267
  function bindingInitializers(ast, lexical) {
1243
1268
  const initializers = new WeakMap();
1244
1269
  const seen = new WeakSet();
1270
+ const record = (name, node, value) => {
1271
+ const scope = captureBindingScope(lexical, node, name);
1272
+ let bindings = initializers.get(scope);
1273
+ if (bindings === undefined) initializers.set(scope, (bindings = new Map()));
1274
+ let values = bindings.get(name);
1275
+ if (values === undefined) bindings.set(name, (values = []));
1276
+ values.push(value);
1277
+ };
1245
1278
  const visit = (node) => {
1246
1279
  if (!node || typeof node !== 'object' || seen.has(node)) return;
1247
1280
  seen.add(node);
@@ -1250,16 +1283,18 @@ function bindingInitializers(ast, lexical) {
1250
1283
  return;
1251
1284
  }
1252
1285
  if (node.type === 'VariableDeclarator') {
1253
- const names = new Set();
1254
- collectBindingNames(node.id, names);
1255
- for (const name of names) {
1256
- const scope = captureBindingScope(lexical, node, name);
1257
- let bindings = initializers.get(scope);
1258
- if (bindings === undefined) initializers.set(scope, (bindings = new Map()));
1259
- let values = bindings.get(name);
1260
- if (values === undefined) bindings.set(name, (values = []));
1261
- values.push(node.init ?? null);
1262
- }
1286
+ recordPatternInitializers(node.id, node, [node.init ?? null], record);
1287
+ } else if (
1288
+ (node.type === 'FunctionDeclaration' || node.type === 'ClassDeclaration') &&
1289
+ node.id?.type === 'Identifier'
1290
+ ) {
1291
+ // A declaration is its own initializer: the parent creates that identity.
1292
+ record(node.id.name, node, node);
1293
+ }
1294
+ if (isFunction(node)) {
1295
+ // Parameter defaults run in the owner when the caller omits the value.
1296
+ for (const param of node.params ?? [])
1297
+ recordPatternInitializers(param, node.body, [], record);
1263
1298
  }
1264
1299
  for (const [key, child] of Object.entries(node)) {
1265
1300
  if (!SKIP_KEYS.has(key)) visit(child);
@@ -1317,6 +1352,7 @@ function unsupportedIndependentInitializer(node, lexical, initializers, assigned
1317
1352
  if (
1318
1353
  isFunction(node) ||
1319
1354
  node.type === 'ClassExpression' ||
1355
+ node.type === 'ClassDeclaration' ||
1320
1356
  node.type === 'NewExpression' ||
1321
1357
  node.type === 'CallExpression' ||
1322
1358
  node.type === 'OptionalCallExpression'
@@ -301,7 +301,11 @@ export function inlinePlainHookMemos(ast, source, id, options) {
301
301
  if (!hasMemo || hasUse) return null;
302
302
  const visitors = esrapTsx({
303
303
  comments: collectComments(ast),
304
- getLeadingComments: (node) => (node.__octanePure ? PURE_COMMENTS : undefined),
304
+ getLeadingComments: (node) =>
305
+ node.__octanePure ||
306
+ (node.type === 'CallExpression' && options.pureCalls?.get(node.start) === node.end)
307
+ ? PURE_COMMENTS
308
+ : undefined,
305
309
  });
306
310
  if (!canPrintProgram(ast, visitors)) return null;
307
311
  const state = {
@@ -0,0 +1,66 @@
1
+ // Octane factories whose runtime declarations carry `/* @__NO_SIDE_EFFECTS__ */`.
2
+ //
3
+ // Rollup (and so Vite) propagates that declaration annotation to call sites in
4
+ // other modules, but esbuild deliberately does not: its tree-shaking decisions
5
+ // are local to the file containing the call (esbuild CHANGELOG 0.18.1). An
6
+ // unused module-scope `const Ctx = createContext(…)` therefore counts as a
7
+ // side effect under esbuild and retains the whole client runtime through
8
+ // createContext's provider body. The compiler owns every Octane module's
9
+ // output, so it annotates these call sites with the portable `/* @__PURE__ */`
10
+ // convention that every bundler honors.
11
+ //
12
+ // Only the client DOM runtime declares all four factories side-effect-free.
13
+ // The server `createContext` registers the context, and universal renderers
14
+ // ship their own factories, so those targets keep the `lazy`-only set.
15
+ const CLIENT_DOM_PURE_FACTORIES = new Set(['createContext', 'createPortal', 'lazy', 'memo']);
16
+ const PORTABLE_PURE_FACTORIES = new Set(['lazy']);
17
+
18
+ /**
19
+ * @param {{ clientDom: boolean }} target
20
+ * @returns {ReadonlySet<string>}
21
+ */
22
+ export function octanePureFactoryNames({ clientDom }) {
23
+ return clientDom ? CLIENT_DOM_PURE_FACTORIES : PORTABLE_PURE_FACTORIES;
24
+ }
25
+
26
+ /**
27
+ * Local names bound by `import { … } from 'octane'` to a pure factory.
28
+ * @param {any[]} body
29
+ * @param {ReadonlySet<string>} names
30
+ * @returns {Set<string>}
31
+ */
32
+ export function collectPureFactoryLocals(body, names) {
33
+ const locals = new Set();
34
+ for (const node of body || []) {
35
+ if (node.type !== 'ImportDeclaration' || node.source?.value !== 'octane') continue;
36
+ if (node.importKind === 'type') continue;
37
+ for (const specifier of node.specifiers || []) {
38
+ if (
39
+ specifier.type === 'ImportSpecifier' &&
40
+ specifier.importKind !== 'type' &&
41
+ names.has(specifier.imported?.name ?? specifier.imported?.value) &&
42
+ specifier.local?.name
43
+ ) {
44
+ locals.add(specifier.local.name);
45
+ }
46
+ }
47
+ }
48
+ return locals;
49
+ }
50
+
51
+ /** Whether `node` is a direct call to one of `locals`. */
52
+ export function isPureFactoryCall(node, locals) {
53
+ return (
54
+ node?.type === 'CallExpression' &&
55
+ node.callee?.type === 'Identifier' &&
56
+ locals.has(node.callee.name)
57
+ );
58
+ }
59
+
60
+ // Authored annotations stay authoritative; never stack a second one.
61
+ const AUTHORED_PURE_BEFORE = /\/\*\s*[#@]__PURE__\s*\*\/\s*$/;
62
+
63
+ /** Whether authored source already annotates the call starting at `start`. */
64
+ export function hasAuthoredPureAnnotation(source, start) {
65
+ return AUTHORED_PURE_BEFORE.test(source.slice(Math.max(0, start - 64), start));
66
+ }
@@ -131,9 +131,12 @@ function registerBunPlugin() {
131
131
  ? 'ts'
132
132
  : 'js';
133
133
  if (result === null || result.kind === 'none') return { contents: source, loader };
134
+ // Compiled `.ts`/`.tsx` output keeps TypeScript with runtime semantics
135
+ // (`enum`, value namespaces), exactly as Vite's own TS transform then
136
+ // consumes it, so it keeps its source loader. `.tsrx` output is JavaScript.
134
137
  return {
135
138
  contents: result.code,
136
- loader: result.kind === 'slots' || result.kind === 'runtime-requests' ? loader : 'js',
139
+ loader: result.kind === 'compile' && path.endsWith('.tsrx') ? 'js' : loader,
137
140
  };
138
141
  });
139
142
  },