@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 +5 -5
- package/src/client-only-server-strip.js +438 -53
- package/src/internal/router-plugin/core/router-code-splitter-plugin.js +6 -3
- package/src/internal/start-plugin-core/import-protection/constants.d.ts +0 -1
- package/src/internal/start-plugin-core/import-protection/constants.js +0 -3
- package/src/internal/start-plugin-core/start-compiler/compiler.js +4 -4
- package/src/internal/start-plugin-core/start-compiler/utils.d.ts +7 -0
- package/src/internal/start-plugin-core/start-compiler/utils.js +7 -0
- package/src/internal/start-plugin-core/vite/import-protection-plugin/plugin.js +6 -6
- package/src/internal/start-plugin-core/vite/module-id.d.ts +6 -0
- package/src/internal/start-plugin-core/vite/module-id.js +23 -0
- package/src/internal/start-plugin-core/vite/start-compiler-plugin/plugin.js +9 -3
- package/src/plugin-vite.d.ts +9 -1
- package/src/plugin-vite.js +15 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octanejs/tanstack-start",
|
|
3
|
-
"version": "0.1.
|
|
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.
|
|
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.
|
|
134
|
+
"@octanejs/tanstack-router": "0.1.16"
|
|
135
135
|
},
|
|
136
136
|
"peerDependencies": {
|
|
137
137
|
"vite": ">=7.0.0",
|
|
138
|
-
"octane": "0.1.
|
|
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.
|
|
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
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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('
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 = `${
|
|
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 = `${
|
|
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
|
-
|
|
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 = `${
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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)
|
|
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:
|
|
297
|
+
id: removeIdQueryFlag(id, SERVER_FN_LOOKUP),
|
|
292
298
|
});
|
|
293
299
|
},
|
|
294
300
|
},
|
package/src/plugin-vite.d.ts
CHANGED
|
@@ -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;
|
package/src/plugin-vite.js
CHANGED
|
@@ -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(
|
|
61
|
+
octane(resolvedOctaneOptions),
|
|
48
62
|
{
|
|
49
63
|
name: 'octanejs-tanstack-start:workspace-source-deps',
|
|
50
64
|
configEnvironment(environmentName, environmentOptions) {
|