@tanstack/router-plugin 1.159.11 → 1.159.14
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/dist/cjs/core/code-splitter/compilers.cjs +467 -0
- package/dist/cjs/core/code-splitter/compilers.cjs.map +1 -1
- package/dist/cjs/core/code-splitter/compilers.d.cts +82 -0
- package/dist/cjs/core/constants.cjs +2 -0
- package/dist/cjs/core/constants.cjs.map +1 -1
- package/dist/cjs/core/constants.d.cts +1 -0
- package/dist/cjs/core/router-code-splitter-plugin.cjs +62 -3
- package/dist/cjs/core/router-code-splitter-plugin.cjs.map +1 -1
- package/dist/esm/core/code-splitter/compilers.d.ts +82 -0
- package/dist/esm/core/code-splitter/compilers.js +469 -2
- package/dist/esm/core/code-splitter/compilers.js.map +1 -1
- package/dist/esm/core/constants.d.ts +1 -0
- package/dist/esm/core/constants.js +2 -0
- package/dist/esm/core/constants.js.map +1 -1
- package/dist/esm/core/router-code-splitter-plugin.js +64 -5
- package/dist/esm/core/router-code-splitter-plugin.js.map +1 -1
- package/package.json +4 -4
- package/src/core/code-splitter/compilers.ts +765 -1
- package/src/core/constants.ts +1 -0
- package/src/core/router-code-splitter-plugin.ts +78 -1
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
generateFromAst,
|
|
8
8
|
parseAst,
|
|
9
9
|
} from '@tanstack/router-utils'
|
|
10
|
-
import { tsrSplit } from '../constants'
|
|
10
|
+
import { tsrShared, tsrSplit } from '../constants'
|
|
11
11
|
import { routeHmrStatement } from '../route-hmr-statement'
|
|
12
12
|
import { createIdentifier } from './path-ids'
|
|
13
13
|
import { getFrameworkOptions } from './framework-options'
|
|
@@ -94,6 +94,11 @@ function removeSplitSearchParamFromFilename(filename: string) {
|
|
|
94
94
|
return bareFilename!
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
export function addSharedSearchParamToFilename(filename: string) {
|
|
98
|
+
const [bareFilename] = filename.split('?')
|
|
99
|
+
return `${bareFilename}?${tsrShared}=1`
|
|
100
|
+
}
|
|
101
|
+
|
|
97
102
|
const splittableCreateRouteFns = ['createFileRoute']
|
|
98
103
|
const unsplittableCreateRouteFns = [
|
|
99
104
|
'createRootRoute',
|
|
@@ -104,6 +109,530 @@ const allCreateRouteFns = [
|
|
|
104
109
|
...unsplittableCreateRouteFns,
|
|
105
110
|
]
|
|
106
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Recursively walk an AST node and collect referenced identifier-like names.
|
|
114
|
+
* Much cheaper than babel.traverse — no path/scope overhead.
|
|
115
|
+
*
|
|
116
|
+
* Notes:
|
|
117
|
+
* - Uses @babel/types `isReferenced` to avoid collecting non-references like
|
|
118
|
+
* object keys, member expression properties, or binding identifiers.
|
|
119
|
+
* - Also handles JSX identifiers for component references.
|
|
120
|
+
*/
|
|
121
|
+
export function collectIdentifiersFromNode(node: t.Node): Set<string> {
|
|
122
|
+
const ids = new Set<string>()
|
|
123
|
+
|
|
124
|
+
;(function walk(
|
|
125
|
+
n: t.Node | null | undefined,
|
|
126
|
+
parent?: t.Node,
|
|
127
|
+
grandparent?: t.Node,
|
|
128
|
+
parentKey?: string,
|
|
129
|
+
) {
|
|
130
|
+
if (!n) return
|
|
131
|
+
|
|
132
|
+
if (t.isIdentifier(n)) {
|
|
133
|
+
// When we don't have parent info (node passed in isolation), treat as referenced.
|
|
134
|
+
if (!parent || t.isReferenced(n, parent, grandparent)) {
|
|
135
|
+
ids.add(n.name)
|
|
136
|
+
}
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (t.isJSXIdentifier(n)) {
|
|
141
|
+
// Skip attribute names: <div data-testid="x" />
|
|
142
|
+
if (parent && t.isJSXAttribute(parent) && parentKey === 'name') {
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Skip member properties: <Foo.Bar /> should count Foo, not Bar
|
|
147
|
+
if (
|
|
148
|
+
parent &&
|
|
149
|
+
t.isJSXMemberExpression(parent) &&
|
|
150
|
+
parentKey === 'property'
|
|
151
|
+
) {
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Intrinsic elements (lowercase) are not identifiers
|
|
156
|
+
const first = n.name[0]
|
|
157
|
+
if (first && first === first.toLowerCase()) {
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
ids.add(n.name)
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
for (const key of t.VISITOR_KEYS[n.type] || []) {
|
|
166
|
+
const child = (n as any)[key]
|
|
167
|
+
if (Array.isArray(child)) {
|
|
168
|
+
for (const c of child) {
|
|
169
|
+
if (c && typeof c.type === 'string') {
|
|
170
|
+
walk(c, n, parent, key)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
} else if (child && typeof child.type === 'string') {
|
|
174
|
+
walk(child, n, parent, key)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
})(node)
|
|
178
|
+
|
|
179
|
+
return ids
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Build a map from binding name → declaration AST node for all
|
|
184
|
+
* locally-declared module-level bindings. Built once, O(1) lookup.
|
|
185
|
+
*/
|
|
186
|
+
export function buildDeclarationMap(ast: t.File): Map<string, t.Node> {
|
|
187
|
+
const map = new Map<string, t.Node>()
|
|
188
|
+
for (const stmt of ast.program.body) {
|
|
189
|
+
const decl =
|
|
190
|
+
t.isExportNamedDeclaration(stmt) && stmt.declaration
|
|
191
|
+
? stmt.declaration
|
|
192
|
+
: stmt
|
|
193
|
+
|
|
194
|
+
if (t.isVariableDeclaration(decl)) {
|
|
195
|
+
for (const declarator of decl.declarations) {
|
|
196
|
+
for (const name of collectIdentifiersFromPattern(declarator.id)) {
|
|
197
|
+
map.set(name, declarator)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
} else if (t.isFunctionDeclaration(decl) && decl.id) {
|
|
201
|
+
map.set(decl.id.name, decl)
|
|
202
|
+
} else if (t.isClassDeclaration(decl) && decl.id) {
|
|
203
|
+
map.set(decl.id.name, decl)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return map
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Build a dependency graph: for each local binding, the set of other local
|
|
211
|
+
* bindings its declaration references. Built once via simple node walking.
|
|
212
|
+
*/
|
|
213
|
+
export function buildDependencyGraph(
|
|
214
|
+
declMap: Map<string, t.Node>,
|
|
215
|
+
localBindings: Set<string>,
|
|
216
|
+
): Map<string, Set<string>> {
|
|
217
|
+
const graph = new Map<string, Set<string>>()
|
|
218
|
+
for (const [name, declNode] of declMap) {
|
|
219
|
+
if (!localBindings.has(name)) continue
|
|
220
|
+
const allIds = collectIdentifiersFromNode(declNode)
|
|
221
|
+
const deps = new Set<string>()
|
|
222
|
+
for (const id of allIds) {
|
|
223
|
+
if (id !== name && localBindings.has(id)) deps.add(id)
|
|
224
|
+
}
|
|
225
|
+
graph.set(name, deps)
|
|
226
|
+
}
|
|
227
|
+
return graph
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Computes module-level bindings that are shared between split and non-split
|
|
232
|
+
* route properties. These bindings need to be extracted into a shared virtual
|
|
233
|
+
* module to avoid double-initialization.
|
|
234
|
+
*
|
|
235
|
+
* A binding is "shared" if it is referenced by at least one split property
|
|
236
|
+
* AND at least one non-split property. Only locally-declared module-level
|
|
237
|
+
* bindings are candidates (not imports — bundlers dedupe those).
|
|
238
|
+
*/
|
|
239
|
+
export function computeSharedBindings(opts: {
|
|
240
|
+
code: string
|
|
241
|
+
codeSplitGroupings: CodeSplitGroupings
|
|
242
|
+
}): Set<string> {
|
|
243
|
+
const ast = parseAst(opts)
|
|
244
|
+
|
|
245
|
+
// Early bailout: collect all module-level locally-declared binding names.
|
|
246
|
+
// This is a cheap loop over program.body (no traversal). If the file has
|
|
247
|
+
// no local bindings (aside from `Route`), nothing can be shared — skip
|
|
248
|
+
// the expensive babel.traverse entirely.
|
|
249
|
+
const localModuleLevelBindings = new Set<string>()
|
|
250
|
+
for (const node of ast.program.body) {
|
|
251
|
+
collectLocalBindingsFromStatement(node, localModuleLevelBindings)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// File-based routes always export a route config binding (usually `Route`).
|
|
255
|
+
// This must never be extracted into the shared module.
|
|
256
|
+
localModuleLevelBindings.delete('Route')
|
|
257
|
+
|
|
258
|
+
if (localModuleLevelBindings.size === 0) {
|
|
259
|
+
return new Set()
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function findIndexForSplitNode(str: string) {
|
|
263
|
+
return opts.codeSplitGroupings.findIndex((group) =>
|
|
264
|
+
group.includes(str as any),
|
|
265
|
+
)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Find the route options object — needs babel.traverse for scope resolution
|
|
269
|
+
let routeOptions: t.ObjectExpression | undefined
|
|
270
|
+
|
|
271
|
+
babel.traverse(ast, {
|
|
272
|
+
CallExpression(path) {
|
|
273
|
+
if (!t.isIdentifier(path.node.callee)) return
|
|
274
|
+
if (!splittableCreateRouteFns.includes(path.node.callee.name)) return
|
|
275
|
+
|
|
276
|
+
if (t.isCallExpression(path.parentPath.node)) {
|
|
277
|
+
const opts = resolveIdentifier(path, path.parentPath.node.arguments[0])
|
|
278
|
+
if (t.isObjectExpression(opts)) routeOptions = opts
|
|
279
|
+
} else if (t.isVariableDeclarator(path.parentPath.node)) {
|
|
280
|
+
const caller = resolveIdentifier(path, path.parentPath.node.init)
|
|
281
|
+
if (t.isCallExpression(caller)) {
|
|
282
|
+
const opts = resolveIdentifier(path, caller.arguments[0])
|
|
283
|
+
if (t.isObjectExpression(opts)) routeOptions = opts
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
if (!routeOptions) return new Set()
|
|
290
|
+
|
|
291
|
+
// Fast path: if fewer than 2 distinct groups are referenced by route options,
|
|
292
|
+
// nothing can be shared and we can skip the rest of the work.
|
|
293
|
+
const splitGroupsPresent = new Set<number>()
|
|
294
|
+
let hasNonSplit = false
|
|
295
|
+
for (const prop of routeOptions.properties) {
|
|
296
|
+
if (!t.isObjectProperty(prop) || !t.isIdentifier(prop.key)) continue
|
|
297
|
+
if (prop.key.name === 'codeSplitGroupings') continue
|
|
298
|
+
if (t.isIdentifier(prop.value) && prop.value.name === 'undefined') continue
|
|
299
|
+
const groupIndex = findIndexForSplitNode(prop.key.name) // -1 if non-split
|
|
300
|
+
if (groupIndex === -1) {
|
|
301
|
+
hasNonSplit = true
|
|
302
|
+
} else {
|
|
303
|
+
splitGroupsPresent.add(groupIndex)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (!hasNonSplit && splitGroupsPresent.size < 2) return new Set()
|
|
308
|
+
|
|
309
|
+
// Build dependency graph up front — needed for transitive expansion per-property.
|
|
310
|
+
// This graph excludes `Route` (deleted above) so group attribution works correctly.
|
|
311
|
+
const declMap = buildDeclarationMap(ast)
|
|
312
|
+
const depGraph = buildDependencyGraph(declMap, localModuleLevelBindings)
|
|
313
|
+
|
|
314
|
+
// Build a second dependency graph that includes `Route` so we can detect
|
|
315
|
+
// bindings that transitively depend on it. Such bindings must NOT be
|
|
316
|
+
// extracted into the shared module because they would drag the Route
|
|
317
|
+
// singleton with them, duplicating it across modules.
|
|
318
|
+
const allLocalBindings = new Set(localModuleLevelBindings)
|
|
319
|
+
allLocalBindings.add('Route')
|
|
320
|
+
const fullDepGraph = buildDependencyGraph(declMap, allLocalBindings)
|
|
321
|
+
|
|
322
|
+
// For each route property, track which "group" it belongs to.
|
|
323
|
+
// Non-split properties get group index -1.
|
|
324
|
+
// Split properties get their codeSplitGroupings index (0, 1, ...).
|
|
325
|
+
// A binding is "shared" if it appears in 2+ distinct groups.
|
|
326
|
+
// We expand each property's refs transitively BEFORE comparing groups,
|
|
327
|
+
// so indirect refs (e.g., component: MyComp where MyComp uses `shared`)
|
|
328
|
+
// are correctly attributed.
|
|
329
|
+
const refsByGroup = new Map<string, Set<number>>()
|
|
330
|
+
|
|
331
|
+
for (const prop of routeOptions.properties) {
|
|
332
|
+
if (!t.isObjectProperty(prop) || !t.isIdentifier(prop.key)) continue
|
|
333
|
+
const key = prop.key.name
|
|
334
|
+
|
|
335
|
+
if (key === 'codeSplitGroupings') continue
|
|
336
|
+
|
|
337
|
+
const groupIndex = findIndexForSplitNode(key) // -1 if non-split
|
|
338
|
+
|
|
339
|
+
const directRefs = collectModuleLevelRefsFromNode(
|
|
340
|
+
prop.value,
|
|
341
|
+
localModuleLevelBindings,
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
// Expand transitively: if component references SharedComp which references
|
|
345
|
+
// `shared`, then `shared` is also attributed to component's group.
|
|
346
|
+
const allRefs = new Set(directRefs)
|
|
347
|
+
expandTransitively(allRefs, depGraph)
|
|
348
|
+
|
|
349
|
+
for (const ref of allRefs) {
|
|
350
|
+
let groups = refsByGroup.get(ref)
|
|
351
|
+
if (!groups) {
|
|
352
|
+
groups = new Set()
|
|
353
|
+
refsByGroup.set(ref, groups)
|
|
354
|
+
}
|
|
355
|
+
groups.add(groupIndex)
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Shared = bindings appearing in 2+ distinct groups
|
|
360
|
+
const shared = new Set<string>()
|
|
361
|
+
for (const [name, groups] of refsByGroup) {
|
|
362
|
+
if (groups.size >= 2) shared.add(name)
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Destructured declarators (e.g. `const { a, b } = fn()`) must be treated
|
|
366
|
+
// as a single initialization unit. Even if each binding is referenced by
|
|
367
|
+
// only one group, if *different* bindings from the same declarator are
|
|
368
|
+
// referenced by different groups, the declarator must be extracted to the
|
|
369
|
+
// shared module to avoid double initialization.
|
|
370
|
+
expandSharedDestructuredDeclarators(ast, refsByGroup, shared)
|
|
371
|
+
|
|
372
|
+
if (shared.size === 0) return shared
|
|
373
|
+
|
|
374
|
+
// If any binding from a destructured declaration is shared,
|
|
375
|
+
// all bindings from that declaration must be shared
|
|
376
|
+
expandDestructuredDeclarations(ast, shared)
|
|
377
|
+
|
|
378
|
+
// Remove shared bindings that transitively depend on `Route`.
|
|
379
|
+
// The Route singleton must stay in the reference file; extracting a
|
|
380
|
+
// binding that references it would duplicate Route in the shared module.
|
|
381
|
+
removeBindingsDependingOnRoute(shared, fullDepGraph)
|
|
382
|
+
|
|
383
|
+
return shared
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* If bindings from the same destructured declarator are referenced by
|
|
388
|
+
* different groups, mark all bindings from that declarator as shared.
|
|
389
|
+
*/
|
|
390
|
+
export function expandSharedDestructuredDeclarators(
|
|
391
|
+
ast: t.File,
|
|
392
|
+
refsByGroup: Map<string, Set<number>>,
|
|
393
|
+
shared: Set<string>,
|
|
394
|
+
) {
|
|
395
|
+
for (const stmt of ast.program.body) {
|
|
396
|
+
const decl =
|
|
397
|
+
t.isExportNamedDeclaration(stmt) && stmt.declaration
|
|
398
|
+
? stmt.declaration
|
|
399
|
+
: stmt
|
|
400
|
+
|
|
401
|
+
if (!t.isVariableDeclaration(decl)) continue
|
|
402
|
+
|
|
403
|
+
for (const declarator of decl.declarations) {
|
|
404
|
+
if (!t.isObjectPattern(declarator.id) && !t.isArrayPattern(declarator.id))
|
|
405
|
+
continue
|
|
406
|
+
|
|
407
|
+
const names = collectIdentifiersFromPattern(declarator.id)
|
|
408
|
+
|
|
409
|
+
const usedGroups = new Set<number>()
|
|
410
|
+
for (const name of names) {
|
|
411
|
+
const groups = refsByGroup.get(name)
|
|
412
|
+
if (!groups) continue
|
|
413
|
+
for (const g of groups) usedGroups.add(g)
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (usedGroups.size >= 2) {
|
|
417
|
+
for (const name of names) {
|
|
418
|
+
shared.add(name)
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Collect locally-declared module-level binding names from a statement.
|
|
427
|
+
* Pure node inspection, no traversal.
|
|
428
|
+
*/
|
|
429
|
+
export function collectLocalBindingsFromStatement(
|
|
430
|
+
node: t.Statement | t.ModuleDeclaration,
|
|
431
|
+
bindings: Set<string>,
|
|
432
|
+
) {
|
|
433
|
+
const decl =
|
|
434
|
+
t.isExportNamedDeclaration(node) && node.declaration
|
|
435
|
+
? node.declaration
|
|
436
|
+
: node
|
|
437
|
+
|
|
438
|
+
if (t.isVariableDeclaration(decl)) {
|
|
439
|
+
for (const declarator of decl.declarations) {
|
|
440
|
+
for (const name of collectIdentifiersFromPattern(declarator.id)) {
|
|
441
|
+
bindings.add(name)
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
} else if (t.isFunctionDeclaration(decl) && decl.id) {
|
|
445
|
+
bindings.add(decl.id.name)
|
|
446
|
+
} else if (t.isClassDeclaration(decl) && decl.id) {
|
|
447
|
+
bindings.add(decl.id.name)
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Collect direct module-level binding names referenced from a given AST node.
|
|
453
|
+
* Uses a simple recursive walk instead of babel.traverse.
|
|
454
|
+
*/
|
|
455
|
+
export function collectModuleLevelRefsFromNode(
|
|
456
|
+
node: t.Node,
|
|
457
|
+
localModuleLevelBindings: Set<string>,
|
|
458
|
+
): Set<string> {
|
|
459
|
+
const allIds = collectIdentifiersFromNode(node)
|
|
460
|
+
const refs = new Set<string>()
|
|
461
|
+
for (const name of allIds) {
|
|
462
|
+
if (localModuleLevelBindings.has(name)) refs.add(name)
|
|
463
|
+
}
|
|
464
|
+
return refs
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Expand the shared set transitively using a prebuilt dependency graph.
|
|
469
|
+
* No AST traversals — pure graph BFS.
|
|
470
|
+
*/
|
|
471
|
+
export function expandTransitively(
|
|
472
|
+
shared: Set<string>,
|
|
473
|
+
depGraph: Map<string, Set<string>>,
|
|
474
|
+
) {
|
|
475
|
+
const queue = [...shared]
|
|
476
|
+
const visited = new Set<string>()
|
|
477
|
+
|
|
478
|
+
while (queue.length > 0) {
|
|
479
|
+
const name = queue.pop()!
|
|
480
|
+
if (visited.has(name)) continue
|
|
481
|
+
visited.add(name)
|
|
482
|
+
|
|
483
|
+
const deps = depGraph.get(name)
|
|
484
|
+
if (!deps) continue
|
|
485
|
+
|
|
486
|
+
for (const dep of deps) {
|
|
487
|
+
if (!shared.has(dep)) {
|
|
488
|
+
shared.add(dep)
|
|
489
|
+
queue.push(dep)
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Remove any bindings from `shared` that transitively depend on `Route`.
|
|
497
|
+
* The Route singleton must remain in the reference file; if a shared binding
|
|
498
|
+
* references it (directly or transitively), extracting that binding would
|
|
499
|
+
* duplicate Route in the shared module.
|
|
500
|
+
*
|
|
501
|
+
* Uses `depGraph` which must include `Route` as a node so the dependency
|
|
502
|
+
* chain is visible.
|
|
503
|
+
*/
|
|
504
|
+
export function removeBindingsDependingOnRoute(
|
|
505
|
+
shared: Set<string>,
|
|
506
|
+
depGraph: Map<string, Set<string>>,
|
|
507
|
+
) {
|
|
508
|
+
const reverseGraph = new Map<string, Set<string>>()
|
|
509
|
+
for (const [name, deps] of depGraph) {
|
|
510
|
+
for (const dep of deps) {
|
|
511
|
+
let parents = reverseGraph.get(dep)
|
|
512
|
+
if (!parents) {
|
|
513
|
+
parents = new Set<string>()
|
|
514
|
+
reverseGraph.set(dep, parents)
|
|
515
|
+
}
|
|
516
|
+
parents.add(name)
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Walk backwards from Route to find all bindings that can reach it.
|
|
521
|
+
const visited = new Set<string>()
|
|
522
|
+
const queue = ['Route']
|
|
523
|
+
while (queue.length > 0) {
|
|
524
|
+
const cur = queue.pop()!
|
|
525
|
+
if (visited.has(cur)) continue
|
|
526
|
+
visited.add(cur)
|
|
527
|
+
|
|
528
|
+
const parents = reverseGraph.get(cur)
|
|
529
|
+
if (!parents) continue
|
|
530
|
+
for (const parent of parents) {
|
|
531
|
+
if (!visited.has(parent)) queue.push(parent)
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
for (const name of [...shared]) {
|
|
536
|
+
if (visited.has(name)) {
|
|
537
|
+
shared.delete(name)
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* If any binding from a destructured declaration is shared,
|
|
544
|
+
* ensure all bindings from that same declaration are also shared.
|
|
545
|
+
* Pure node inspection of program.body, no traversal.
|
|
546
|
+
*/
|
|
547
|
+
export function expandDestructuredDeclarations(
|
|
548
|
+
ast: t.File,
|
|
549
|
+
shared: Set<string>,
|
|
550
|
+
) {
|
|
551
|
+
for (const stmt of ast.program.body) {
|
|
552
|
+
const decl =
|
|
553
|
+
t.isExportNamedDeclaration(stmt) && stmt.declaration
|
|
554
|
+
? stmt.declaration
|
|
555
|
+
: stmt
|
|
556
|
+
|
|
557
|
+
if (!t.isVariableDeclaration(decl)) continue
|
|
558
|
+
|
|
559
|
+
for (const declarator of decl.declarations) {
|
|
560
|
+
if (!t.isObjectPattern(declarator.id) && !t.isArrayPattern(declarator.id))
|
|
561
|
+
continue
|
|
562
|
+
|
|
563
|
+
const names = collectIdentifiersFromPattern(declarator.id)
|
|
564
|
+
const hasShared = names.some((n) => shared.has(n))
|
|
565
|
+
if (hasShared) {
|
|
566
|
+
for (const n of names) {
|
|
567
|
+
shared.add(n)
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Find which shared bindings are user-exported in the original source.
|
|
576
|
+
* These need to be re-exported from the shared module.
|
|
577
|
+
*/
|
|
578
|
+
function findExportedSharedBindings(
|
|
579
|
+
ast: t.File,
|
|
580
|
+
sharedBindings: Set<string>,
|
|
581
|
+
): Set<string> {
|
|
582
|
+
const exported = new Set<string>()
|
|
583
|
+
for (const stmt of ast.program.body) {
|
|
584
|
+
if (!t.isExportNamedDeclaration(stmt) || !stmt.declaration) continue
|
|
585
|
+
|
|
586
|
+
if (t.isVariableDeclaration(stmt.declaration)) {
|
|
587
|
+
for (const decl of stmt.declaration.declarations) {
|
|
588
|
+
for (const name of collectIdentifiersFromPattern(decl.id)) {
|
|
589
|
+
if (sharedBindings.has(name)) exported.add(name)
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
} else if (
|
|
593
|
+
t.isFunctionDeclaration(stmt.declaration) &&
|
|
594
|
+
stmt.declaration.id
|
|
595
|
+
) {
|
|
596
|
+
if (sharedBindings.has(stmt.declaration.id.name))
|
|
597
|
+
exported.add(stmt.declaration.id.name)
|
|
598
|
+
} else if (t.isClassDeclaration(stmt.declaration) && stmt.declaration.id) {
|
|
599
|
+
if (sharedBindings.has(stmt.declaration.id.name))
|
|
600
|
+
exported.add(stmt.declaration.id.name)
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
return exported
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Remove declarations of shared bindings from the AST.
|
|
608
|
+
* Handles both plain and exported declarations, including destructured patterns.
|
|
609
|
+
* Removes the entire statement if all bindings in it are shared.
|
|
610
|
+
*/
|
|
611
|
+
function removeSharedDeclarations(ast: t.File, sharedBindings: Set<string>) {
|
|
612
|
+
ast.program.body = ast.program.body.filter((stmt) => {
|
|
613
|
+
const decl =
|
|
614
|
+
t.isExportNamedDeclaration(stmt) && stmt.declaration
|
|
615
|
+
? stmt.declaration
|
|
616
|
+
: stmt
|
|
617
|
+
|
|
618
|
+
if (t.isVariableDeclaration(decl)) {
|
|
619
|
+
// Filter out declarators where all bound names are shared
|
|
620
|
+
decl.declarations = decl.declarations.filter((declarator) => {
|
|
621
|
+
const names = collectIdentifiersFromPattern(declarator.id)
|
|
622
|
+
return !names.every((n) => sharedBindings.has(n))
|
|
623
|
+
})
|
|
624
|
+
// If no declarators remain, remove the entire statement
|
|
625
|
+
if (decl.declarations.length === 0) return false
|
|
626
|
+
} else if (t.isFunctionDeclaration(decl) && decl.id) {
|
|
627
|
+
if (sharedBindings.has(decl.id.name)) return false
|
|
628
|
+
} else if (t.isClassDeclaration(decl) && decl.id) {
|
|
629
|
+
if (sharedBindings.has(decl.id.name)) return false
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
return true
|
|
633
|
+
})
|
|
634
|
+
}
|
|
635
|
+
|
|
107
636
|
export function compileCodeSplitReferenceRoute(
|
|
108
637
|
opts: ParseAstOptions & {
|
|
109
638
|
codeSplitGroupings: CodeSplitGroupings
|
|
@@ -112,6 +641,7 @@ export function compileCodeSplitReferenceRoute(
|
|
|
112
641
|
filename: string
|
|
113
642
|
id: string
|
|
114
643
|
addHmr?: boolean
|
|
644
|
+
sharedBindings?: Set<string>
|
|
115
645
|
},
|
|
116
646
|
): GeneratorResult | null {
|
|
117
647
|
const ast = parseAst(opts)
|
|
@@ -135,6 +665,7 @@ export function compileCodeSplitReferenceRoute(
|
|
|
135
665
|
|
|
136
666
|
let modified = false as boolean
|
|
137
667
|
let hmrAdded = false as boolean
|
|
668
|
+
let sharedExportedNames: Set<string> | undefined
|
|
138
669
|
babel.traverse(ast, {
|
|
139
670
|
Program: {
|
|
140
671
|
enter(programPath) {
|
|
@@ -424,6 +955,56 @@ export function compileCodeSplitReferenceRoute(
|
|
|
424
955
|
},
|
|
425
956
|
})
|
|
426
957
|
}
|
|
958
|
+
|
|
959
|
+
// Handle shared bindings inside the Program visitor so we have
|
|
960
|
+
// access to programPath for cheap refIdents registration.
|
|
961
|
+
if (opts.sharedBindings && opts.sharedBindings.size > 0) {
|
|
962
|
+
sharedExportedNames = findExportedSharedBindings(
|
|
963
|
+
ast,
|
|
964
|
+
opts.sharedBindings,
|
|
965
|
+
)
|
|
966
|
+
removeSharedDeclarations(ast, opts.sharedBindings)
|
|
967
|
+
|
|
968
|
+
const sharedModuleUrl = addSharedSearchParamToFilename(opts.filename)
|
|
969
|
+
|
|
970
|
+
const sharedImportSpecifiers = [...opts.sharedBindings].map((name) =>
|
|
971
|
+
t.importSpecifier(t.identifier(name), t.identifier(name)),
|
|
972
|
+
)
|
|
973
|
+
const [sharedImportPath] = programPath.unshiftContainer(
|
|
974
|
+
'body',
|
|
975
|
+
t.importDeclaration(
|
|
976
|
+
sharedImportSpecifiers,
|
|
977
|
+
t.stringLiteral(sharedModuleUrl),
|
|
978
|
+
),
|
|
979
|
+
)
|
|
980
|
+
|
|
981
|
+
// Register import specifier locals in refIdents so DCE can remove unused ones
|
|
982
|
+
sharedImportPath.traverse({
|
|
983
|
+
Identifier(identPath) {
|
|
984
|
+
if (
|
|
985
|
+
identPath.parentPath.isImportSpecifier() &&
|
|
986
|
+
identPath.key === 'local'
|
|
987
|
+
) {
|
|
988
|
+
refIdents.add(identPath)
|
|
989
|
+
}
|
|
990
|
+
},
|
|
991
|
+
})
|
|
992
|
+
|
|
993
|
+
// Re-export user-exported shared bindings from the shared module
|
|
994
|
+
if (sharedExportedNames.size > 0) {
|
|
995
|
+
const reExportSpecifiers = [...sharedExportedNames].map((name) =>
|
|
996
|
+
t.exportSpecifier(t.identifier(name), t.identifier(name)),
|
|
997
|
+
)
|
|
998
|
+
programPath.pushContainer(
|
|
999
|
+
'body',
|
|
1000
|
+
t.exportNamedDeclaration(
|
|
1001
|
+
null,
|
|
1002
|
+
reExportSpecifiers,
|
|
1003
|
+
t.stringLiteral(sharedModuleUrl),
|
|
1004
|
+
),
|
|
1005
|
+
)
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
427
1008
|
},
|
|
428
1009
|
},
|
|
429
1010
|
})
|
|
@@ -465,11 +1046,18 @@ export function compileCodeSplitVirtualRoute(
|
|
|
465
1046
|
opts: ParseAstOptions & {
|
|
466
1047
|
splitTargets: Array<SplitRouteIdentNodes>
|
|
467
1048
|
filename: string
|
|
1049
|
+
sharedBindings?: Set<string>
|
|
468
1050
|
},
|
|
469
1051
|
): GeneratorResult {
|
|
470
1052
|
const ast = parseAst(opts)
|
|
471
1053
|
const refIdents = findReferencedIdentifiers(ast)
|
|
472
1054
|
|
|
1055
|
+
// Remove shared declarations BEFORE babel.traverse so the scope never sees
|
|
1056
|
+
// conflicting bindings (avoids checkBlockScopedCollisions crash in DCE)
|
|
1057
|
+
if (opts.sharedBindings && opts.sharedBindings.size > 0) {
|
|
1058
|
+
removeSharedDeclarations(ast, opts.sharedBindings)
|
|
1059
|
+
}
|
|
1060
|
+
|
|
473
1061
|
const intendedSplitNodes = new Set(opts.splitTargets)
|
|
474
1062
|
|
|
475
1063
|
const knownExportedIdents = new Set<string>()
|
|
@@ -814,12 +1402,188 @@ export function compileCodeSplitVirtualRoute(
|
|
|
814
1402
|
}
|
|
815
1403
|
},
|
|
816
1404
|
})
|
|
1405
|
+
|
|
1406
|
+
// Add shared bindings import, registering specifiers in refIdents
|
|
1407
|
+
// so DCE can remove unused ones (same pattern as import replacements above).
|
|
1408
|
+
if (opts.sharedBindings && opts.sharedBindings.size > 0) {
|
|
1409
|
+
const sharedImportSpecifiers = [...opts.sharedBindings].map((name) =>
|
|
1410
|
+
t.importSpecifier(t.identifier(name), t.identifier(name)),
|
|
1411
|
+
)
|
|
1412
|
+
const sharedModuleUrl = addSharedSearchParamToFilename(
|
|
1413
|
+
removeSplitSearchParamFromFilename(opts.filename),
|
|
1414
|
+
)
|
|
1415
|
+
const [sharedImportPath] = programPath.unshiftContainer(
|
|
1416
|
+
'body',
|
|
1417
|
+
t.importDeclaration(
|
|
1418
|
+
sharedImportSpecifiers,
|
|
1419
|
+
t.stringLiteral(sharedModuleUrl),
|
|
1420
|
+
),
|
|
1421
|
+
)
|
|
1422
|
+
|
|
1423
|
+
sharedImportPath.traverse({
|
|
1424
|
+
Identifier(identPath) {
|
|
1425
|
+
if (
|
|
1426
|
+
identPath.parentPath.isImportSpecifier() &&
|
|
1427
|
+
identPath.key === 'local'
|
|
1428
|
+
) {
|
|
1429
|
+
refIdents.add(identPath)
|
|
1430
|
+
}
|
|
1431
|
+
},
|
|
1432
|
+
})
|
|
1433
|
+
}
|
|
817
1434
|
},
|
|
818
1435
|
},
|
|
819
1436
|
})
|
|
820
1437
|
|
|
821
1438
|
deadCodeElimination(ast, refIdents)
|
|
822
1439
|
|
|
1440
|
+
// Strip top-level expression statements that reference no locally-bound names.
|
|
1441
|
+
// DCE only removes unused declarations; bare side-effect statements like
|
|
1442
|
+
// `console.log(...)` survive even when the virtual file has no exports.
|
|
1443
|
+
{
|
|
1444
|
+
const locallyBound = new Set<string>()
|
|
1445
|
+
for (const stmt of ast.program.body) {
|
|
1446
|
+
collectLocalBindingsFromStatement(stmt, locallyBound)
|
|
1447
|
+
}
|
|
1448
|
+
ast.program.body = ast.program.body.filter((stmt) => {
|
|
1449
|
+
if (!t.isExpressionStatement(stmt)) return true
|
|
1450
|
+
const refs = collectIdentifiersFromNode(stmt)
|
|
1451
|
+
// Keep if it references at least one locally-bound identifier
|
|
1452
|
+
return [...refs].some((name) => locallyBound.has(name))
|
|
1453
|
+
})
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
// If the body is empty after DCE, strip directive prologues too.
|
|
1457
|
+
// A file containing only `'use client'` with no real code is useless.
|
|
1458
|
+
if (ast.program.body.length === 0) {
|
|
1459
|
+
ast.program.directives = []
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
return generateFromAst(ast, {
|
|
1463
|
+
sourceMaps: true,
|
|
1464
|
+
sourceFileName: opts.filename,
|
|
1465
|
+
filename: opts.filename,
|
|
1466
|
+
})
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
/**
|
|
1470
|
+
* Compile the shared virtual module (`?tsr-shared=1`).
|
|
1471
|
+
* Keeps only shared binding declarations, their transitive dependencies,
|
|
1472
|
+
* and imports they need. Exports all shared bindings.
|
|
1473
|
+
*/
|
|
1474
|
+
export function compileCodeSplitSharedRoute(
|
|
1475
|
+
opts: ParseAstOptions & {
|
|
1476
|
+
sharedBindings: Set<string>
|
|
1477
|
+
filename: string
|
|
1478
|
+
},
|
|
1479
|
+
): GeneratorResult {
|
|
1480
|
+
const ast = parseAst(opts)
|
|
1481
|
+
const refIdents = findReferencedIdentifiers(ast)
|
|
1482
|
+
|
|
1483
|
+
// Collect all names that need to stay: shared bindings + their transitive deps
|
|
1484
|
+
const localBindings = new Set<string>()
|
|
1485
|
+
for (const node of ast.program.body) {
|
|
1486
|
+
collectLocalBindingsFromStatement(node, localBindings)
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
// Route must never be extracted into the shared module.
|
|
1490
|
+
// Excluding it from the dep graph prevents expandTransitively from
|
|
1491
|
+
// pulling it in as a transitive dependency of a shared binding.
|
|
1492
|
+
localBindings.delete('Route')
|
|
1493
|
+
|
|
1494
|
+
const declMap = buildDeclarationMap(ast)
|
|
1495
|
+
const depGraph = buildDependencyGraph(declMap, localBindings)
|
|
1496
|
+
|
|
1497
|
+
// Start with shared bindings and expand transitively
|
|
1498
|
+
const keepBindings = new Set(opts.sharedBindings)
|
|
1499
|
+
keepBindings.delete('Route')
|
|
1500
|
+
expandTransitively(keepBindings, depGraph)
|
|
1501
|
+
|
|
1502
|
+
// Remove all statements except:
|
|
1503
|
+
// - Import declarations (needed for deps; DCE will clean unused ones)
|
|
1504
|
+
// - Declarations of bindings in keepBindings
|
|
1505
|
+
ast.program.body = ast.program.body.filter((stmt) => {
|
|
1506
|
+
// Always keep imports — DCE will remove unused ones
|
|
1507
|
+
if (t.isImportDeclaration(stmt)) return true
|
|
1508
|
+
|
|
1509
|
+
const decl =
|
|
1510
|
+
t.isExportNamedDeclaration(stmt) && stmt.declaration
|
|
1511
|
+
? stmt.declaration
|
|
1512
|
+
: stmt
|
|
1513
|
+
|
|
1514
|
+
if (t.isVariableDeclaration(decl)) {
|
|
1515
|
+
// Keep declarators where at least one binding is in keepBindings
|
|
1516
|
+
decl.declarations = decl.declarations.filter((declarator) => {
|
|
1517
|
+
const names = collectIdentifiersFromPattern(declarator.id)
|
|
1518
|
+
return names.some((n) => keepBindings.has(n))
|
|
1519
|
+
})
|
|
1520
|
+
if (decl.declarations.length === 0) return false
|
|
1521
|
+
|
|
1522
|
+
// Strip the `export` wrapper — shared module controls its own exports
|
|
1523
|
+
if (t.isExportNamedDeclaration(stmt) && stmt.declaration) {
|
|
1524
|
+
return true // keep for now, we'll convert below
|
|
1525
|
+
}
|
|
1526
|
+
return true
|
|
1527
|
+
} else if (t.isFunctionDeclaration(decl) && decl.id) {
|
|
1528
|
+
return keepBindings.has(decl.id.name)
|
|
1529
|
+
} else if (t.isClassDeclaration(decl) && decl.id) {
|
|
1530
|
+
return keepBindings.has(decl.id.name)
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
// Remove everything else (expression statements, other exports, etc.)
|
|
1534
|
+
return false
|
|
1535
|
+
})
|
|
1536
|
+
|
|
1537
|
+
// Convert `export const/function/class` to plain declarations
|
|
1538
|
+
// (we'll add our own export statement at the end)
|
|
1539
|
+
ast.program.body = ast.program.body.map((stmt) => {
|
|
1540
|
+
if (t.isExportNamedDeclaration(stmt) && stmt.declaration) {
|
|
1541
|
+
return stmt.declaration
|
|
1542
|
+
}
|
|
1543
|
+
return stmt
|
|
1544
|
+
})
|
|
1545
|
+
|
|
1546
|
+
// Export all shared bindings (sorted for deterministic output)
|
|
1547
|
+
const exportNames = [...opts.sharedBindings].sort((a, b) =>
|
|
1548
|
+
a.localeCompare(b),
|
|
1549
|
+
)
|
|
1550
|
+
const exportSpecifiers = exportNames.map((name) =>
|
|
1551
|
+
t.exportSpecifier(t.identifier(name), t.identifier(name)),
|
|
1552
|
+
)
|
|
1553
|
+
if (exportSpecifiers.length > 0) {
|
|
1554
|
+
const exportDecl = t.exportNamedDeclaration(null, exportSpecifiers)
|
|
1555
|
+
ast.program.body.push(exportDecl)
|
|
1556
|
+
|
|
1557
|
+
// Register export specifier locals in refIdents so DCE doesn't treat
|
|
1558
|
+
// the exported bindings as unreferenced.
|
|
1559
|
+
babel.traverse(ast, {
|
|
1560
|
+
Program(programPath) {
|
|
1561
|
+
const bodyPaths = programPath.get('body')
|
|
1562
|
+
const last = bodyPaths[bodyPaths.length - 1]
|
|
1563
|
+
if (last && last.isExportNamedDeclaration()) {
|
|
1564
|
+
last.traverse({
|
|
1565
|
+
Identifier(identPath) {
|
|
1566
|
+
if (
|
|
1567
|
+
identPath.parentPath.isExportSpecifier() &&
|
|
1568
|
+
identPath.key === 'local'
|
|
1569
|
+
) {
|
|
1570
|
+
refIdents.add(identPath)
|
|
1571
|
+
}
|
|
1572
|
+
},
|
|
1573
|
+
})
|
|
1574
|
+
}
|
|
1575
|
+
programPath.stop()
|
|
1576
|
+
},
|
|
1577
|
+
})
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
deadCodeElimination(ast, refIdents)
|
|
1581
|
+
|
|
1582
|
+
// If the body is empty after DCE, strip directive prologues too.
|
|
1583
|
+
if (ast.program.body.length === 0) {
|
|
1584
|
+
ast.program.directives = []
|
|
1585
|
+
}
|
|
1586
|
+
|
|
823
1587
|
return generateFromAst(ast, {
|
|
824
1588
|
sourceMaps: true,
|
|
825
1589
|
sourceFileName: opts.filename,
|