@octanejs/tanstack-start 0.1.2 → 0.1.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@octanejs/tanstack-start",
3
- "version": "0.1.2",
3
+ "version": "0.1.6",
4
4
  "description": "TanStack Start for Octane, including file-route generation, server functions, SSR streaming, hydration, and Vite integration.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -121,7 +121,7 @@
121
121
  "magic-string": "^0.30.21",
122
122
  "pathe": "^2.0.3",
123
123
  "picomatch": "^4.0.3",
124
- "prettier": "^3.9.5",
124
+ "prettier": "^3.9.6",
125
125
  "seroval": "^1.5.4",
126
126
  "source-map": "^0.7.6",
127
127
  "srvx": "^0.11.9",
@@ -131,11 +131,11 @@
131
131
  "vitefu": "^1.1.1",
132
132
  "xmlbuilder2": "^4.0.3",
133
133
  "zod": "^4.4.3",
134
- "@octanejs/tanstack-router": "0.1.12"
134
+ "@octanejs/tanstack-router": "0.1.16"
135
135
  },
136
136
  "peerDependencies": {
137
137
  "vite": ">=7.0.0",
138
- "octane": "0.1.13"
138
+ "octane": "0.1.17"
139
139
  },
140
140
  "peerDependenciesMeta": {
141
141
  "vite": {
@@ -146,7 +146,7 @@
146
146
  "@types/node": "^24.13.3",
147
147
  "vite": "^8.1.5",
148
148
  "vitest": "^4.1.10",
149
- "octane": "0.1.13"
149
+ "octane": "0.1.17"
150
150
  },
151
151
  "scripts": {
152
152
  "test": "cd ../.. && vitest run --project tanstack-start"
@@ -1,22 +1,11 @@
1
+ import MagicString from 'magic-string';
1
2
  import { compileToVolarMappings } from 'octane/compiler/volar';
2
3
  import { START_ENVIRONMENT_NAMES } from '#tanstack-start/plugin-core/vite';
3
4
 
4
5
  /**
5
- * Octane counterpart of the start-compiler's `handleClientOnlyJSX` babel pass:
6
- * on the SERVER environment, `<ClientOnly>` children are removed at compile
7
- * time (only the `fallback` prop may render during SSR). The upstream pass
8
- * operates on React JSX via babel; octane's `.tsrx` compiles its JSX away
9
- * before any babel-based pass could see it, so this plugin performs the strip
10
- * on the `.tsrx` SOURCE — replacing the children span with a `{null}` hole —
11
- * before `octane/compiler/vite` runs.
12
- *
13
- * Removing the children (rather than relying on the runtime `ClientOnly`,
14
- * which already renders nothing during SSR) matters for the STATIC graph:
15
- * client-only subtrees routinely reference `*.client.*` modules, and Start's
16
- * import-protection verifies after tree-shaking that no denied module stays
17
- * reachable from the server bundle. With the children intact, the octane
18
- * server build retained those edges and failed the build where the react
19
- * build passes.
6
+ * Remove the children of the Router ClientOnly binding before Octane compiles
7
+ * server TSRX. This keeps client-only imports out of the server module graph,
8
+ * while preserving fallback content and identically-named local components.
20
9
  */
21
10
  export function octaneClientOnlyServerStrip() {
22
11
  return {
@@ -31,46 +20,442 @@ export function octaneClientOnlyServerStrip() {
31
20
  code: { include: ['ClientOnly'] },
32
21
  },
33
22
  handler(code, id) {
34
- if (!code.includes('<ClientOnly')) return undefined;
35
-
36
- const { sourceAst } = compileToVolarMappings(code, id.split('?')[0]);
37
- const spans = [];
38
- const seen = new Set();
39
- const elementName = (node) => node.openingElement?.name?.name ?? node.id?.name;
40
- const visit = (value) => {
41
- if (!value || typeof value !== 'object' || seen.has(value)) return;
42
- seen.add(value);
43
- if (Array.isArray(value)) {
44
- for (const item of value) visit(item);
45
- return;
46
- }
47
- const children = value.children;
48
- if (
49
- elementName(value) === 'ClientOnly' &&
50
- Array.isArray(children) &&
51
- children.length > 0
52
- ) {
53
- const start = children[0].start;
54
- const end = children[children.length - 1].end;
55
- if (typeof start === 'number' && typeof end === 'number' && end > start) {
56
- spans.push([start, end]);
57
- }
58
- }
59
- for (const key in value) {
60
- if (key !== 'metadata' && key !== 'loc') visit(value[key]);
61
- }
62
- };
63
- visit(sourceAst);
64
- if (spans.length === 0) return undefined;
65
-
66
- // Replace back-to-front so earlier spans keep their offsets.
67
- spans.sort((a, b) => b[0] - a[0]);
68
- let out = code;
69
- for (const [start, end] of spans) {
70
- out = `${out.slice(0, start)}{null}${out.slice(end)}`;
23
+ if (!code.includes('ClientOnly')) return undefined;
24
+
25
+ const filename = id.split('?', 1)[0];
26
+ const { sourceAst } = compileToVolarMappings(code, filename);
27
+ const childReplacements = stripClientOnlyChildren(sourceAst);
28
+ if (childReplacements.length === 0) return undefined;
29
+
30
+ const prunedImportSpecifiers = findImportsUsedOnlyInRanges(sourceAst, childReplacements);
31
+ const replacements = [
32
+ ...rewritePrunedImports(code, sourceAst, prunedImportSpecifiers),
33
+ ...childReplacements,
34
+ ];
35
+ const output = new MagicString(code);
36
+ for (const replacement of replacements.sort((a, b) => b.start - a.start)) {
37
+ output.overwrite(replacement.start, replacement.end, replacement.content);
71
38
  }
72
- return { code: out, map: null };
39
+
40
+ return {
41
+ code: output.toString(),
42
+ map: output.generateMap({
43
+ source: filename,
44
+ includeContent: true,
45
+ hires: true,
46
+ }),
47
+ };
73
48
  },
74
49
  },
75
50
  };
76
51
  }
52
+
53
+ function rewritePrunedImports(code, program, prunedImportSpecifiers) {
54
+ const replacements = [];
55
+
56
+ for (const statement of asNodes(program.body)) {
57
+ if (
58
+ statement.type !== 'ImportDeclaration' ||
59
+ !hasRange(statement) ||
60
+ !hasRange(statement.source)
61
+ ) {
62
+ continue;
63
+ }
64
+
65
+ const prunedSpecifiers = prunedImportSpecifiers.get(statement);
66
+ if (!prunedSpecifiers?.size) continue;
67
+
68
+ const remainingSpecifiers = (statement.specifiers ?? []).filter(
69
+ (specifier) => !prunedSpecifiers.has(specifier),
70
+ );
71
+ replacements.push({
72
+ start: statement.start,
73
+ end: statement.end,
74
+ content: printRemainingImport(code, statement, remainingSpecifiers),
75
+ });
76
+ }
77
+
78
+ return replacements;
79
+ }
80
+
81
+ function findImportsUsedOnlyInRanges(program, removedRanges) {
82
+ const bindings = new Map();
83
+ for (const statement of asNodes(program.body)) {
84
+ if (statement.type !== 'ImportDeclaration' || statement.importKind === 'type') {
85
+ continue;
86
+ }
87
+ for (const specifier of statement.specifiers ?? []) {
88
+ const localName = specifier.local?.name;
89
+ if (specifier.importKind === 'type' || !localName) continue;
90
+ bindings.set(localName, {
91
+ declaration: statement,
92
+ specifier,
93
+ removed: false,
94
+ live: false,
95
+ });
96
+ }
97
+ }
98
+
99
+ if (bindings.size === 0 || removedRanges.length === 0) return new Map();
100
+
101
+ visitImportedBindingReferences(program, bindings, removedRanges);
102
+
103
+ const result = new Map();
104
+ for (const usage of bindings.values()) {
105
+ // Keep imports that were already unused: importing may intentionally run
106
+ // module initialization. Only remove bindings whose uses were stripped.
107
+ if (!usage.removed || usage.live) continue;
108
+ const specifiers = result.get(usage.declaration) ?? new Set();
109
+ specifiers.add(usage.specifier);
110
+ result.set(usage.declaration, specifiers);
111
+ }
112
+ return result;
113
+ }
114
+
115
+ function visitImportedBindingReferences(program, bindings, removedRanges) {
116
+ const visit = (value, shadowed, parent, parentKey, bindingPattern = false) => {
117
+ if (!value || typeof value !== 'object') return;
118
+ if (Array.isArray(value)) {
119
+ for (const item of value) {
120
+ visit(item, shadowed, parent, parentKey, bindingPattern);
121
+ }
122
+ return;
123
+ }
124
+
125
+ const node = value;
126
+ if (node.type === 'ImportDeclaration') return;
127
+
128
+ const scopedNames = scopeBindings(node);
129
+ const nextShadowed = scopedNames.size ? new Set([...shadowed, ...scopedNames]) : shadowed;
130
+
131
+ if (
132
+ !bindingPattern &&
133
+ isBindingReference(node, parent, parentKey) &&
134
+ node.name &&
135
+ !nextShadowed.has(node.name)
136
+ ) {
137
+ const usage = bindings.get(node.name);
138
+ if (usage && hasRange(node)) {
139
+ if (isInsideRange(node, removedRanges)) usage.removed = true;
140
+ else usage.live = true;
141
+ }
142
+ }
143
+
144
+ for (const [key, child] of Object.entries(node)) {
145
+ if (key === 'metadata' || key === 'loc' || key === 'parent') continue;
146
+ visit(child, nextShadowed, node, key, isBindingPatternChild(node, key, bindingPattern));
147
+ }
148
+ };
149
+
150
+ visit(program, new Set());
151
+ }
152
+
153
+ function isBindingReference(node, parent, parentKey) {
154
+ if (node.type === 'Identifier') {
155
+ if (!parent) return true;
156
+ if (
157
+ (parent.type === 'MemberExpression' || parent.type === 'OptionalMemberExpression') &&
158
+ parentKey === 'property' &&
159
+ !parent.computed
160
+ ) {
161
+ return false;
162
+ }
163
+ if (
164
+ (parent.type === 'Property' ||
165
+ parent.type === 'PropertyDefinition' ||
166
+ parent.type === 'MethodDefinition') &&
167
+ parentKey === 'key' &&
168
+ !parent.computed
169
+ ) {
170
+ return Boolean(parent.shorthand);
171
+ }
172
+ if (parent.type === 'ExportSpecifier') return parentKey === 'local';
173
+ if (
174
+ (parent.type === 'LabeledStatement' ||
175
+ parent.type === 'BreakStatement' ||
176
+ parent.type === 'ContinueStatement') &&
177
+ parentKey === 'label'
178
+ ) {
179
+ return false;
180
+ }
181
+ return true;
182
+ }
183
+
184
+ if (node.type !== 'JSXIdentifier' || !node.name || !parent) return false;
185
+ if (
186
+ (parent.type === 'JSXOpeningElement' || parent.type === 'JSXClosingElement') &&
187
+ parentKey === 'name'
188
+ ) {
189
+ return true;
190
+ }
191
+ return parent.type === 'JSXMemberExpression' && parentKey === 'object';
192
+ }
193
+
194
+ function isBindingPatternChild(parent, key, parentIsBindingPattern) {
195
+ if (parentIsBindingPattern) {
196
+ if (parent.type === 'AssignmentPattern') return key === 'left';
197
+ if (parent.type === 'Property') {
198
+ return key === 'value' || (key === 'key' && !parent.computed);
199
+ }
200
+ return true;
201
+ }
202
+
203
+ if (parent.type === 'VariableDeclarator') return key === 'id';
204
+ if (isFunction(parent)) return key === 'id' || key === 'params';
205
+ if (parent.type === 'ClassDeclaration' || parent.type === 'ClassExpression') {
206
+ return key === 'id';
207
+ }
208
+ if (parent.type === 'CatchClause') return key === 'param';
209
+ if (parent.type === 'ImportSpecifier') return true;
210
+ return false;
211
+ }
212
+
213
+ function isInsideRange(node, ranges) {
214
+ return ranges.some((range) => range.start <= node.start && range.end >= node.end);
215
+ }
216
+
217
+ function printRemainingImport(code, statement, specifiers) {
218
+ const sourceNode = statement.source;
219
+ if (specifiers.length === 0 || !hasRange(sourceNode)) return '';
220
+
221
+ const defaultSpecifier = specifiers.find(
222
+ (specifier) => specifier.type === 'ImportDefaultSpecifier',
223
+ );
224
+ const namespaceSpecifier = specifiers.find(
225
+ (specifier) => specifier.type === 'ImportNamespaceSpecifier',
226
+ );
227
+ const namedSpecifiers = specifiers.filter((specifier) => specifier.type === 'ImportSpecifier');
228
+ const clauses = [];
229
+
230
+ if (defaultSpecifier && hasRange(defaultSpecifier)) {
231
+ clauses.push(code.slice(defaultSpecifier.start, defaultSpecifier.end));
232
+ }
233
+ if (namespaceSpecifier && hasRange(namespaceSpecifier)) {
234
+ clauses.push(code.slice(namespaceSpecifier.start, namespaceSpecifier.end));
235
+ }
236
+ if (namedSpecifiers.length > 0) {
237
+ clauses.push(
238
+ `{ ${namedSpecifiers
239
+ .filter(hasRange)
240
+ .map((specifier) => code.slice(specifier.start, specifier.end))
241
+ .join(', ')} }`,
242
+ );
243
+ }
244
+
245
+ const source = code.slice(sourceNode.start, sourceNode.end);
246
+ const suffix = hasRange(statement) ? code.slice(sourceNode.end, statement.end) : '';
247
+ return `import ${clauses.join(', ')} from ${source}${suffix}`;
248
+ }
249
+
250
+ function stripClientOnlyChildren(program) {
251
+ const importedNames = new Set();
252
+ for (const statement of asNodes(program.body)) {
253
+ if (statement.type !== 'ImportDeclaration' || statement.importKind === 'type') {
254
+ continue;
255
+ }
256
+
257
+ for (const specifier of statement.specifiers ?? []) {
258
+ if (
259
+ specifier.type === 'ImportSpecifier' &&
260
+ specifier.importKind !== 'type' &&
261
+ specifier.imported?.name === 'ClientOnly' &&
262
+ specifier.local?.name
263
+ ) {
264
+ importedNames.add(specifier.local.name);
265
+ }
266
+ }
267
+ }
268
+
269
+ if (importedNames.size === 0) return [];
270
+
271
+ const replacements = [];
272
+ const visited = new WeakSet();
273
+ const visit = (value, shadowed) => {
274
+ if (!value || typeof value !== 'object' || visited.has(value)) return;
275
+ visited.add(value);
276
+
277
+ if (Array.isArray(value)) {
278
+ for (const item of value) visit(item, shadowed);
279
+ return;
280
+ }
281
+
282
+ const node = value;
283
+ const scopedNames = scopeBindings(node);
284
+ const nextShadowed = scopedNames.size ? new Set([...shadowed, ...scopedNames]) : shadowed;
285
+ const elementName = node.openingElement?.name?.name;
286
+
287
+ if (
288
+ node.type === 'JSXElement' &&
289
+ elementName &&
290
+ importedNames.has(elementName) &&
291
+ !nextShadowed.has(elementName) &&
292
+ node.children?.length
293
+ ) {
294
+ const first = node.children[0];
295
+ const last = node.children[node.children.length - 1];
296
+ if (first && last && hasRange(first) && hasRange(last) && last.end > first.start) {
297
+ replacements.push({
298
+ start: first.start,
299
+ end: last.end,
300
+ content: '{null}',
301
+ });
302
+ }
303
+ }
304
+
305
+ for (const [key, child] of Object.entries(node)) {
306
+ if (key !== 'metadata' && key !== 'loc' && key !== 'parent') {
307
+ visit(child, nextShadowed);
308
+ }
309
+ }
310
+ };
311
+
312
+ visit(program, new Set());
313
+
314
+ // Replacing an outer ClientOnly child range also removes nested boundaries.
315
+ return replacements.filter(
316
+ (candidate, index) =>
317
+ !replacements.some(
318
+ (other, otherIndex) =>
319
+ otherIndex !== index && other.start <= candidate.start && other.end >= candidate.end,
320
+ ),
321
+ );
322
+ }
323
+
324
+ function scopeBindings(node) {
325
+ const names = new Set();
326
+
327
+ if (isFunction(node)) {
328
+ for (const param of node.params ?? []) collectBindingNames(param, names);
329
+ collectBindingNames(node.id, names);
330
+ for (const statement of directStatements(node.body)) {
331
+ collectStatementBindings(statement, names);
332
+ }
333
+ collectFunctionVarBindings(node.body, names);
334
+ } else if (node.type === 'BlockStatement' || node.type === 'JSXCodeBlock') {
335
+ for (const statement of directStatements(node)) {
336
+ collectStatementBindings(statement, names);
337
+ }
338
+ } else if (node.type === 'CatchClause') {
339
+ collectBindingNames(node.param, names);
340
+ } else if (
341
+ node.type === 'ForStatement' ||
342
+ node.type === 'ForInStatement' ||
343
+ node.type === 'ForOfStatement'
344
+ ) {
345
+ const declaration = node.init ?? node.left;
346
+ if (declaration?.type === 'VariableDeclaration' && declaration.kind !== 'var') {
347
+ for (const item of declaration.declarations ?? []) {
348
+ collectBindingNames(item.id, names);
349
+ }
350
+ }
351
+ } else if (node.type === 'SwitchStatement') {
352
+ for (const switchCase of asNodes(node.cases)) {
353
+ for (const statement of asNodes(switchCase.consequent)) {
354
+ collectStatementBindings(statement, names);
355
+ }
356
+ }
357
+ } else if (node.type === 'StaticBlock') {
358
+ for (const statement of directStatements(node)) {
359
+ collectStatementBindings(statement, names);
360
+ }
361
+ } else if (node.type === 'ClassDeclaration' || node.type === 'ClassExpression') {
362
+ collectBindingNames(node.id, names);
363
+ }
364
+
365
+ return names;
366
+ }
367
+
368
+ function directStatements(node) {
369
+ if (Array.isArray(node)) return node;
370
+ if (!node || !Array.isArray(node.body)) return [];
371
+ return asNodes(node.body);
372
+ }
373
+
374
+ function collectStatementBindings(statement, output) {
375
+ const declaration =
376
+ statement.type === 'ExportNamedDeclaration' || statement.type === 'ExportDefaultDeclaration'
377
+ ? statement.declaration
378
+ : statement;
379
+
380
+ if (declaration?.type === 'VariableDeclaration') {
381
+ for (const item of declaration.declarations ?? []) {
382
+ collectBindingNames(item.id, output);
383
+ }
384
+ } else if (
385
+ declaration?.type === 'FunctionDeclaration' ||
386
+ declaration?.type === 'ClassDeclaration'
387
+ ) {
388
+ collectBindingNames(declaration.id, output);
389
+ }
390
+ }
391
+
392
+ function collectFunctionVarBindings(value, output) {
393
+ const visited = new WeakSet();
394
+ const visit = (child, root = false) => {
395
+ if (!child || typeof child !== 'object' || visited.has(child)) return;
396
+ visited.add(child);
397
+
398
+ if (Array.isArray(child)) {
399
+ for (const item of child) visit(item);
400
+ return;
401
+ }
402
+
403
+ const node = child;
404
+ if (!root && isFunction(node)) return;
405
+ if (node.type === 'VariableDeclaration' && node.kind === 'var') {
406
+ for (const item of node.declarations ?? []) {
407
+ collectBindingNames(item.id, output);
408
+ }
409
+ }
410
+ for (const [key, nested] of Object.entries(node)) {
411
+ if (key !== 'metadata' && key !== 'loc' && key !== 'parent') {
412
+ visit(nested);
413
+ }
414
+ }
415
+ };
416
+
417
+ visit(value, true);
418
+ }
419
+
420
+ function collectBindingNames(pattern, output) {
421
+ if (!pattern) return;
422
+ if (pattern.type === 'Identifier' && pattern.name) {
423
+ output.add(pattern.name);
424
+ return;
425
+ }
426
+ if (pattern.type === 'RestElement') {
427
+ collectBindingNames(pattern.argument, output);
428
+ return;
429
+ }
430
+ if (pattern.type === 'AssignmentPattern') {
431
+ collectBindingNames(pattern.left, output);
432
+ return;
433
+ }
434
+ if (pattern.type === 'ArrayPattern') {
435
+ for (const element of asNodes(pattern.elements)) {
436
+ collectBindingNames(element, output);
437
+ }
438
+ return;
439
+ }
440
+ if (pattern.type === 'ObjectPattern') {
441
+ for (const property of asNodes(pattern.properties)) {
442
+ collectBindingNames(property.argument ?? property.value, output);
443
+ }
444
+ }
445
+ }
446
+
447
+ function isFunction(node) {
448
+ return (
449
+ node.type === 'FunctionDeclaration' ||
450
+ node.type === 'FunctionExpression' ||
451
+ node.type === 'ArrowFunctionExpression'
452
+ );
453
+ }
454
+
455
+ function hasRange(node) {
456
+ return typeof node?.start === 'number' && typeof node.end === 'number';
457
+ }
458
+
459
+ function asNodes(value) {
460
+ return Array.isArray(value) ? value : [];
461
+ }
@@ -34,7 +34,7 @@ function createRouterCodeSplitterPlugin(options = {}, routerPluginContext) {
34
34
  if (typeof options === 'function') userConfig = options();
35
35
  else userConfig = getConfig(options, ROOT);
36
36
  }
37
- const isProduction = process.env.NODE_ENV === 'production';
37
+ let isProduction = process.env.NODE_ENV === 'production';
38
38
  const sharedBindingsMap = /* @__PURE__ */ new Map();
39
39
  const getGlobalCodeSplitGroupings = () => {
40
40
  return userConfig.codeSplittingOptions?.defaultBehavior || defaultCodeSplitGroupings;
@@ -159,6 +159,7 @@ function createRouterCodeSplitterPlugin(options = {}, routerPluginContext) {
159
159
  },
160
160
  vite: {
161
161
  configResolved(config) {
162
+ isProduction = config.command === 'build';
162
163
  ROOT = config.root;
163
164
  initUserConfig();
164
165
  validateFrameworkPluginOrder({
@@ -173,11 +174,13 @@ function createRouterCodeSplitterPlugin(options = {}, routerPluginContext) {
173
174
  return true;
174
175
  },
175
176
  },
176
- rspack() {
177
+ rspack(compiler) {
178
+ isProduction = compiler.options.mode === 'production';
177
179
  ROOT = process.cwd();
178
180
  initUserConfig();
179
181
  },
180
- webpack() {
182
+ webpack(compiler) {
183
+ isProduction = compiler.options.mode === 'production';
181
184
  ROOT = process.cwd();
182
185
  initUserConfig();
183
186
  },
@@ -1,4 +1,3 @@
1
- export declare const SERVER_FN_LOOKUP_QUERY = '?server-fn-module-lookup';
2
1
  export declare const MOCK_MODULE_ID = 'tanstack-start-import-protection:mock';
3
2
  export declare const MOCK_BUILD_PREFIX = 'tanstack-start-import-protection:mock:build:';
4
3
  export declare const MOCK_EDGE_PREFIX = 'tanstack-start-import-protection:mock-edge:';
@@ -1,6 +1,4 @@
1
- import { SERVER_FN_LOOKUP } from '../constants.js';
2
1
  //#region src/import-protection/constants.ts
3
- var SERVER_FN_LOOKUP_QUERY = `?${SERVER_FN_LOOKUP}`;
4
2
  var MOCK_MODULE_ID = 'tanstack-start-import-protection:mock';
5
3
  var MOCK_BUILD_PREFIX = 'tanstack-start-import-protection:mock:build:';
6
4
  var MOCK_EDGE_PREFIX = 'tanstack-start-import-protection:mock-edge:';
@@ -34,6 +32,5 @@ export {
34
32
  MOCK_EDGE_PREFIX,
35
33
  MOCK_MODULE_ID,
36
34
  MOCK_RUNTIME_PREFIX,
37
- SERVER_FN_LOOKUP_QUERY,
38
35
  VITE_BROWSER_VIRTUAL_PREFIX,
39
36
  };
@@ -850,7 +850,7 @@ var StartCompiler = class {
850
850
  const binding = (await this.getModuleInfo(id)).bindings.get(ident);
851
851
  if (!binding) return 'None';
852
852
  if (binding.resolvedKind) return binding.resolvedKind;
853
- const vKey = `${cleanId(id)}:${ident}`;
853
+ const vKey = `${id}:${ident}`;
854
854
  if (visited.has(vKey)) return 'None';
855
855
  visited.add(vKey);
856
856
  const resolvedKind = await this.resolveBindingKind(binding, id, visited);
@@ -904,7 +904,7 @@ var StartCompiler = class {
904
904
  if (isBuildMode) this.getExportResolutionCache(moduleInfo.id).set(exportName, null);
905
905
  }
906
906
  async resolveBindingTarget(resolution, visited = /* @__PURE__ */ new Set()) {
907
- const key = `${cleanId(resolution.moduleInfo.id)}:${resolution.localName}`;
907
+ const key = `${resolution.moduleInfo.id}:${resolution.localName}`;
908
908
  if (visited.has(key)) return;
909
909
  visited.add(key);
910
910
  if (resolution.binding.type !== 'import') return resolution;
@@ -936,7 +936,7 @@ var StartCompiler = class {
936
936
  const target = found ? ((await this.resolveBindingTarget(found)) ?? found) : void 0;
937
937
  if (
938
938
  target &&
939
- cleanId(resolved.moduleInfo.id) === cleanId(target.moduleInfo.id) &&
939
+ resolved.moduleInfo.id === target.moduleInfo.id &&
940
940
  resolved.localName === target.localName
941
941
  )
942
942
  return kind;
@@ -965,7 +965,7 @@ var StartCompiler = class {
965
965
  return knownKind;
966
966
  }
967
967
  if (found.binding.resolvedKind) return found.binding.resolvedKind;
968
- const vKey = `${cleanId(found.moduleInfo.id)}:${found.localName}`;
968
+ const vKey = `${found.moduleInfo.id}:${found.localName}`;
969
969
  if (visited.has(vKey)) return 'None';
970
970
  visited.add(vKey);
971
971
  const resolvedKind = await this.resolveBindingKind(found.binding, found.moduleInfo.id, visited);
@@ -14,6 +14,13 @@ export declare function codeFrameError(
14
14
  },
15
15
  message: string,
16
16
  ): Error;
17
+ /**
18
+ * Converts a bundler module ID to its physical-file identity for diagnostics,
19
+ * filesystem matching, and file-based invalidation.
20
+ *
21
+ * Do not use this for IDs passed to resolve/load hooks or as module cache keys:
22
+ * virtual prefixes and queries can be part of the module's semantic identity.
23
+ */
17
24
  export declare function cleanId(id: string): string;
18
25
  /**
19
26
  * Strips a method call by replacing it with its callee object.
@@ -15,6 +15,13 @@ function codeFrameError(code, loc, message) {
15
15
  );
16
16
  return new Error(frame);
17
17
  }
18
+ /**
19
+ * Converts a bundler module ID to its physical-file identity for diagnostics,
20
+ * filesystem matching, and file-based invalidation.
21
+ *
22
+ * Do not use this for IDs passed to resolve/load hooks or as module cache keys:
23
+ * virtual prefixes and queries can be part of the module's semantic identity.
24
+ */
18
25
  function cleanId(id) {
19
26
  if (id.startsWith('\0')) id = id.slice(1);
20
27
  const queryIndex = id.indexOf('?');
@@ -1,9 +1,8 @@
1
- import { TRANSFORM_ID_REGEX, VITE_ENVIRONMENT_NAMES } from '../../constants.js';
1
+ import { SERVER_FN_LOOKUP, TRANSFORM_ID_REGEX, VITE_ENVIRONMENT_NAMES } from '../../constants.js';
2
2
  import { escapeRegExp, resolveViteId } from '../../utils.js';
3
3
  import {
4
4
  IMPORT_PROTECTION_DEBUG,
5
5
  MOCK_BUILD_PREFIX,
6
- SERVER_FN_LOOKUP_QUERY,
7
6
  VITE_BROWSER_VIRTUAL_PREFIX,
8
7
  } from '../../import-protection/constants.js';
9
8
  import {
@@ -64,6 +63,7 @@ import {
64
63
  resolveInternalVirtualModuleId,
65
64
  resolvedMarkerVirtualModuleId,
66
65
  } from './virtualModules.js';
66
+ import { hasIdQueryFlag } from '../module-id.js';
67
67
  import { dirname, relative } from 'pathe';
68
68
  import { writeFileSync } from 'node:fs';
69
69
  import { normalizePath } from 'vite';
@@ -694,7 +694,7 @@ function importProtectionPlugin(opts) {
694
694
  let merged = null;
695
695
  if (keySet)
696
696
  for (const k of keySet) {
697
- if (k.includes(SERVER_FN_LOOKUP_QUERY)) continue;
697
+ if (hasIdQueryFlag(k, SERVER_FN_LOOKUP)) continue;
698
698
  const imports = env.postTransformImports.get(k);
699
699
  if (imports)
700
700
  if (!merged) merged = new Set(imports);
@@ -721,7 +721,7 @@ function importProtectionPlugin(opts) {
721
721
  let anyVariantCached = false;
722
722
  if (keySet)
723
723
  for (const k of keySet) {
724
- if (k.includes(SERVER_FN_LOOKUP_QUERY)) continue;
724
+ if (hasIdQueryFlag(k, SERVER_FN_LOOKUP)) continue;
725
725
  const imports = env.postTransformImports.get(k);
726
726
  if (imports) {
727
727
  anyVariantCached = true;
@@ -1091,7 +1091,7 @@ function importProtectionPlugin(opts) {
1091
1091
  }
1092
1092
  if (source.startsWith('\0') || source.startsWith('virtual:')) return;
1093
1093
  const normalizedImporter = normalizeFilePath(importer);
1094
- const isDirectLookup = importer.includes(SERVER_FN_LOOKUP_QUERY);
1094
+ const isDirectLookup = hasIdQueryFlag(importer, SERVER_FN_LOOKUP);
1095
1095
  if (config.command === 'serve' && config.bundledDev && envType === 'client') {
1096
1096
  if (
1097
1097
  isInsideDirectory(normalizedImporter, normalizePath(`${config.srcDirectory}/routes`))
@@ -1432,7 +1432,7 @@ function importProtectionPlugin(opts) {
1432
1432
  }
1433
1433
  const cacheKey = normalizePath(id);
1434
1434
  const envState = getEnv(envName);
1435
- const isServerFnLookup = id.includes(SERVER_FN_LOOKUP_QUERY);
1435
+ const isServerFnLookup = hasIdQueryFlag(id, SERVER_FN_LOOKUP);
1436
1436
  if (isServerFnLookup) envState.serverFnLookupModules.add(file);
1437
1437
  const result = {
1438
1438
  code,
@@ -0,0 +1,6 @@
1
+ /** Checks for a query parameter without rewriting the opaque module ID. */
2
+ export declare function hasIdQueryFlag(id: string, flag: string): boolean;
3
+ /** Appends an owned query flag without normalizing the existing query. */
4
+ export declare function appendIdQueryFlag(id: string, flag: string): string;
5
+ /** Removes the owned query flag appended by {@link appendIdQueryFlag}. */
6
+ export declare function removeIdQueryFlag(id: string, flag: string): string;
@@ -0,0 +1,23 @@
1
+ //#region src/vite/module-id.ts
2
+ /** Checks for a query parameter without rewriting the opaque module ID. */
3
+ function hasIdQueryFlag(id, flag) {
4
+ const queryIndex = id.indexOf('?');
5
+ if (queryIndex === -1) return false;
6
+ return new URLSearchParams(id.slice(queryIndex + 1)).has(flag);
7
+ }
8
+ /** Appends an owned query flag without normalizing the existing query. */
9
+ function appendIdQueryFlag(id, flag) {
10
+ if (!id.includes('?')) return `${id}?${flag}`;
11
+ const separator = id.endsWith('&') ? '' : '&';
12
+ return `${id}${separator}${flag}`;
13
+ }
14
+ /** Removes the owned query flag appended by {@link appendIdQueryFlag}. */
15
+ function removeIdQueryFlag(id, flag) {
16
+ for (const separator of ['?', '&']) {
17
+ const suffix = `${separator}${flag}`;
18
+ if (id.endsWith(suffix)) return id.slice(0, -suffix.length);
19
+ }
20
+ return id;
21
+ }
22
+ //#endregion
23
+ export { appendIdQueryFlag, hasIdQueryFlag, removeIdQueryFlag };
@@ -15,6 +15,7 @@ import {
15
15
  MissingHydrateSourceError,
16
16
  createHydrateCompilerPlugin,
17
17
  } from '../../hydrate-when-transform.js';
18
+ import { appendIdQueryFlag, removeIdQueryFlag } from '../module-id.js';
18
19
  import {
19
20
  createViteDevServerFnModuleSpecifierEncoder,
20
21
  decodeViteDevServerModuleSpecifier,
@@ -184,7 +185,7 @@ function startCompilerPlugin(opts) {
184
185
  const code = await loadViteModuleFromEnvironment(this.environment, id, {
185
186
  load: (options) => this.load(options),
186
187
  error: (message) => this.error(message),
187
- devId: `${id}?${SERVER_FN_LOOKUP}`,
188
+ devId: appendIdQueryFlag(id, SERVER_FN_LOOKUP),
188
189
  });
189
190
  if (code !== void 0)
190
191
  compiler.ingestModule({
@@ -195,7 +196,12 @@ function startCompilerPlugin(opts) {
195
196
  resolveId: async (source, importer) => {
196
197
  const r = await this.resolve(source, importer);
197
198
  if (r) {
198
- if (!r.external) return cleanId(r.id);
199
+ if (!r.external) {
200
+ // Keep the resolved ID intact because it is passed back to
201
+ // Vite's load hook. Virtual-module prefixes and queries are
202
+ // part of that load identity, not compiler-only metadata.
203
+ return r.id;
204
+ }
199
205
  }
200
206
  return null;
201
207
  },
@@ -288,7 +294,7 @@ function startCompilerPlugin(opts) {
288
294
  handler(code, id) {
289
295
  compilers.get(this.environment.name)?.ingestModule({
290
296
  code,
291
- id: cleanId(id),
297
+ id: removeIdQueryFlag(id, SERVER_FN_LOOKUP),
292
298
  });
293
299
  },
294
300
  },
@@ -12,7 +12,15 @@ export type OctaneRendererDescriptor = Exclude<OctaneRendererRegistryEntry, stri
12
12
  export type OctaneRendererBoundary = OctaneRendererBoundaryOptions;
13
13
  export type OctaneRendererRule = OctaneRendererRuleOptions;
14
14
  export type OctaneRendererConfig = OctaneRendererConfigOptions;
15
- export type OctaneCompilerOptions = Omit<OctaneVitePluginOptions, 'ssr'>;
15
+ export type OctaneCompilerOptions = Omit<OctaneVitePluginOptions, 'ssr'> & {
16
+ /**
17
+ * Shorthand for the compiler's command-aware profiling signal (mirrors
18
+ * `@octanejs/vite-plugin`'s `devtools` option): enables profiling in `vite
19
+ * dev` only, fully compiled out of `vite build`. An explicit `profile`
20
+ * always takes precedence over `devtools`.
21
+ */
22
+ devtools?: boolean;
23
+ };
16
24
 
17
25
  export type TanStackOctaneStartViteInputConfig = TanStackStartViteInputConfig & {
18
26
  octane?: OctaneCompilerOptions;
@@ -31,6 +31,20 @@ export function tanstackStart(options) {
31
31
  const { octane: octaneOptions, ...startOptions } = options ?? {};
32
32
  validateOctaneCompilerOptions(octaneOptions);
33
33
 
34
+ // `devtools: true` is Start's shorthand for the compiler's command-aware
35
+ // profiling signal (mirrors `@octanejs/vite-plugin`'s `devtools` option): an
36
+ // explicit `profile` always wins, otherwise `devtools` maps to `'auto'`
37
+ // (profiling on in `vite dev`, compiled out of `vite build`). `octane()`
38
+ // itself only understands `profile`, so this is resolved before the plugin
39
+ // is created and the `devtools` key never reaches it.
40
+ const resolvedOctaneOptions = octaneOptions ? { ...octaneOptions } : octaneOptions;
41
+ if (resolvedOctaneOptions) {
42
+ if (resolvedOctaneOptions.profile === undefined && resolvedOctaneOptions.devtools === true) {
43
+ resolvedOctaneOptions.profile = 'auto';
44
+ }
45
+ delete resolvedOctaneOptions.devtools;
46
+ }
47
+
34
48
  const corePluginOptions = {
35
49
  framework: 'octane',
36
50
  defaultEntryPaths: octaneStartDefaultEntryPaths,
@@ -44,7 +58,7 @@ export function tanstackStart(options) {
44
58
  // Must run before the octane compiler: strips <ClientOnly> children on
45
59
  // the server (the octane analogue of start-compiler handleClientOnlyJSX).
46
60
  octaneClientOnlyServerStrip(),
47
- octane(octaneOptions),
61
+ octane(resolvedOctaneOptions),
48
62
  {
49
63
  name: 'octanejs-tanstack-start:workspace-source-deps',
50
64
  configEnvironment(environmentName, environmentOptions) {