octane 0.2.4 → 0.2.6

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.
@@ -13,7 +13,7 @@
13
13
  // production it reserves a collision-free runtime range because these arbitrary
14
14
  // helpers can execute in a Scope alongside code from any other source module.
15
15
 
16
- import { parseModule } from '@tsrx/core';
16
+ import { parseModule, builders as b } from '@tsrx/core';
17
17
  import { parseModule as parseFallbackModule } from '#octane/compiler-parser';
18
18
  import { HOOK_NAMES, hookSlotHash } from './compile.js';
19
19
  import { NATIVE_SIGNAL_HOOK_NAMES } from './hook-names.js';
@@ -23,6 +23,13 @@ import { assertStrongMode } from './strong-mode.js';
23
23
  import { assertNativeReadDiagnostics, assertNativeReadOptions } from './native-read-diagnostics.js';
24
24
  import { nativeReadActivationIndex } from './native-read-codegen.js';
25
25
  import { findManualHookProviders, manualHookWrapperParameters } from './manual-hooks.js';
26
+ import { findLeadingJsxImportSourcePragma } from './pragma.js';
27
+ import {
28
+ hookMethodName,
29
+ hasHookMethods,
30
+ assertSynchronousHookMethod,
31
+ lowerHookMethodChain,
32
+ } from './hook-methods.js';
26
33
 
27
34
  function importsNativeRenderer(ast) {
28
35
  return ast.body.some(
@@ -40,7 +47,7 @@ function importsNativeRenderer(ast) {
40
47
  // Build a cheap import-presence gate. Precise call identity is annotated by the
41
48
  // lexical scope analysis in analyzeHookDependencies below; this gate only avoids
42
49
  // doing the surgical edit walk for modules that cannot contain an Octane hook.
43
- function octaneHookLocals(ast, nativeReads = false) {
50
+ function octaneHookLocals(ast, nativeReads = false, explicitlyOwned = false) {
44
51
  const locals = new Map();
45
52
  let importsHook = false;
46
53
  let hasOctaneImport = false;
@@ -90,7 +97,9 @@ function octaneHookLocals(ast, nativeReads = false) {
90
97
  }
91
98
  return {
92
99
  locals,
93
- importsHook: importsHook || (hasOctaneImport && importsCustomHook),
100
+ importsHook:
101
+ importsHook ||
102
+ ((hasOctaneImport || explicitlyOwned) && (importsCustomHook || hasHookMethods(ast))),
94
103
  hasOctaneImport,
95
104
  };
96
105
  }
@@ -836,7 +845,9 @@ function parallelUseCallOfStatement(statement) {
836
845
 
837
846
  function requireParallelHelper(st, imported) {
838
847
  const request =
839
- imported === 'nativePuMemo' || imported === 'enableNativeReadCollection'
848
+ imported === 'nativePuMemo' ||
849
+ imported === 'enableNativeReadCollection' ||
850
+ imported === 'callWithReceiver'
840
851
  ? st.environment === 'server'
841
852
  ? 'octane/internal/server'
842
853
  : 'octane/internal/client'
@@ -997,6 +1008,102 @@ function callOpenParen(node, source) {
997
1008
  return -1;
998
1009
  }
999
1010
 
1011
+ // Keep this line-preserving text edit within the surgical pass. Authored leaves
1012
+ // remain source slices; generated guards never pass through an AST printer.
1013
+ function emitHookMethodChain(node, owner, st) {
1014
+ const deleting = node.type === 'UnaryExpression' && node.operator === 'delete';
1015
+ if ((deleting ? node.argument : node)?.type !== 'ChainExpression') return false;
1016
+ const leaves = new WeakMap();
1017
+ const visit = (leaf) => {
1018
+ if (leaf.type === 'SpreadElement') return { ...leaf, argument: visit(leaf.argument) };
1019
+ const outerEdits = st.edits;
1020
+ st.edits = [];
1021
+ let edits;
1022
+ try {
1023
+ walk(leaf, owner, st);
1024
+ edits = st.edits;
1025
+ } finally {
1026
+ st.edits = outerEdits;
1027
+ }
1028
+ let source = st.source.slice(leaf.start, leaf.end);
1029
+ for (const edit of edits.sort((a, b) => b.pos - a.pos)) {
1030
+ source =
1031
+ source.slice(0, edit.pos - leaf.start) +
1032
+ edit.text +
1033
+ source.slice((edit.end ?? edit.pos) - leaf.start);
1034
+ }
1035
+ const opaque = b.id('_source');
1036
+ leaves.set(opaque, source);
1037
+ return opaque;
1038
+ };
1039
+ const lowered = lowerHookMethodChain(
1040
+ deleting ? node.argument : node,
1041
+ {
1042
+ locals: st.locals,
1043
+ allocateName: (name) => allocSlotName(st, name),
1044
+ visit,
1045
+ requireReceiver: () => requireParallelHelper(st, 'callWithReceiver'),
1046
+ wrap: (call, origin, method) =>
1047
+ b.call(
1048
+ requireParallelHelper(st, 'withSlot'),
1049
+ b.id(allocHookSymbol(st, owner, method, method, origin)),
1050
+ b.arrow([], call),
1051
+ ),
1052
+ },
1053
+ deleting,
1054
+ );
1055
+ if (lowered === null) return false;
1056
+ // The lowerer produces only these guard/call shapes. Opaque authored leaves
1057
+ // are keyed by node identity, so strings and property names cannot be replaced.
1058
+ function emit(value) {
1059
+ if (leaves.has(value)) return `(${leaves.get(value)})`;
1060
+ switch (value.type) {
1061
+ case 'Identifier':
1062
+ return value.name;
1063
+ case 'PrivateIdentifier':
1064
+ return `#${value.name}`;
1065
+ case 'Super':
1066
+ return 'super';
1067
+ case 'ThisExpression':
1068
+ return 'this';
1069
+ case 'Literal':
1070
+ return JSON.stringify(value.value);
1071
+ case 'SpreadElement':
1072
+ return `...${emit(value.argument)}`;
1073
+ case 'TSNonNullExpression':
1074
+ return `(${emit(value.expression)})!`;
1075
+ case 'MemberExpression': {
1076
+ const object = value.object.type === 'Super' ? 'super' : `(${emit(value.object)})`;
1077
+ return value.computed
1078
+ ? `${object}[${emit(value.property)}]`
1079
+ : `${object}.${emit(value.property)}`;
1080
+ }
1081
+ case 'CallExpression': {
1082
+ const types = value.typeArguments
1083
+ ? st.source.slice(value.typeArguments.start, value.typeArguments.end)
1084
+ : '';
1085
+ return `(${emit(value.callee)})${types}(${value.arguments.map(emit).join(', ')})`;
1086
+ }
1087
+ case 'ArrowFunctionExpression':
1088
+ return `((${value.params.map(emit).join(', ')}) => ${emit(value.body)})`;
1089
+ case 'BinaryExpression':
1090
+ case 'LogicalExpression':
1091
+ return `(${emit(value.left)} ${value.operator} ${emit(value.right)})`;
1092
+ case 'ConditionalExpression':
1093
+ return `(${emit(value.test)} ? ${emit(value.consequent)} : ${emit(value.alternate)})`;
1094
+ case 'UnaryExpression':
1095
+ return `(${value.operator} ${emit(value.argument)})`;
1096
+ default:
1097
+ throw new Error(`Unexpected optional hook-chain node: ${value.type}`);
1098
+ }
1099
+ }
1100
+ let text = emit(lowered);
1101
+ const originalLines = st.source.slice(node.start, node.end).split('\n').length;
1102
+ text += '\n'.repeat(Math.max(0, originalLines - text.split('\n').length));
1103
+ st.edits.push({ pos: node.start, end: node.end, text });
1104
+ return true;
1105
+ }
1106
+
1000
1107
  function walk(node, owner, st) {
1001
1108
  if (!node || typeof node !== 'object') return;
1002
1109
  if (Array.isArray(node)) {
@@ -1018,7 +1125,19 @@ function walk(node, owner, st) {
1018
1125
  const childOwner =
1019
1126
  node.type === 'FunctionDeclaration' && node.id ? hookOwner(node, node.id.name) : owner;
1020
1127
 
1128
+ if (!st.manualSlots && emitHookMethodChain(node, childOwner, st)) return;
1129
+
1021
1130
  if (node.type === 'CallExpression') {
1131
+ const method = hookMethodName(node, st.locals);
1132
+ if (!st.manualSlots && method !== null) {
1133
+ assertSynchronousHookMethod(node);
1134
+ const sym = allocHookSymbol(st, owner, method, method, node);
1135
+ const helper = requireParallelHelper(st, 'withSlot');
1136
+ // Keep the complete method expression inside the boundary: receivers,
1137
+ // getters, optional calls, and argument counts retain their semantics.
1138
+ st.edits.push({ pos: node.start, text: `${helper}(${sym}, () => ` });
1139
+ st.edits.push({ pos: node.end, text: ')' });
1140
+ }
1022
1141
  if (!st.manualSlots && node._octaneCustomHookCall) {
1023
1142
  const open = callOpenParen(node, st.source);
1024
1143
  if (open !== -1) {
@@ -1192,7 +1311,11 @@ export function slotHooks(source, id, options) {
1192
1311
  }
1193
1312
  assertStrongMode(ast, source, id, options);
1194
1313
  assertNativeReadDiagnostics(ast, source, id, options);
1195
- const importInfo = octaneHookLocals(ast, options?.nativeReads === true);
1314
+ const importInfo = octaneHookLocals(
1315
+ ast,
1316
+ options?.nativeReads === true,
1317
+ findLeadingJsxImportSourcePragma(source) === 'octane',
1318
+ );
1196
1319
  const manualProviders = options?.manualSlots ? findManualHookProviders(ast) : new Map();
1197
1320
  const nativeReadActivation = options?.nativeReads === true && importsNativeRenderer(ast);
1198
1321
  const canSpecializeRoot =
@@ -1240,6 +1363,7 @@ export function slotHooks(source, id, options) {
1240
1363
  !(canSpecializeRoot && collectVoidRootCandidates(ast).length > 0)
1241
1364
  ) {
1242
1365
  const inlined = inlinePlainHookMemos(ast, source, id, {
1366
+ hookLocals: importInfo.locals,
1243
1367
  manualSlots: options?.manualSlots === true,
1244
1368
  hookNames:
1245
1369
  options?.nativeReads === true
@@ -69,7 +69,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
69
69
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
70
70
  SOFTWARE.
71
71
 
72
- @tsrx/core@0.1.67
72
+ @tsrx/core@0.1.69
73
73
 
74
74
  MIT License
75
75