one 1.25.16 → 1.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/dist/cjs/native-transforms.cjs +28 -0
  2. package/dist/cjs/native-transforms.native.js +31 -0
  3. package/dist/cjs/native-transforms.native.js.map +1 -0
  4. package/dist/cjs/router/useViteRoutes.cjs +3 -2
  5. package/dist/cjs/router/useViteRoutes.native.js +3 -2
  6. package/dist/cjs/router/useViteRoutes.native.js.map +1 -1
  7. package/dist/cjs/router/useViteRoutes.test.cjs +101 -1
  8. package/dist/cjs/router/useViteRoutes.test.native.js +109 -1
  9. package/dist/cjs/router/useViteRoutes.test.native.js.map +1 -1
  10. package/dist/cjs/vercel/build/generate/createApiServerlessFunction.cjs +45 -44
  11. package/dist/cjs/vercel/build/generate/createApiServerlessFunction.native.js +64 -39
  12. package/dist/cjs/vercel/build/generate/createApiServerlessFunction.native.js.map +1 -1
  13. package/dist/cjs/vite/one.cjs +10 -2
  14. package/dist/cjs/vite/one.native.js +13 -4
  15. package/dist/cjs/vite/one.native.js.map +1 -1
  16. package/dist/cjs/vite/plugins/clientTreeShakePlugin.cjs +597 -92
  17. package/dist/cjs/vite/plugins/clientTreeShakePlugin.native.js +1358 -113
  18. package/dist/cjs/vite/plugins/clientTreeShakePlugin.native.js.map +1 -1
  19. package/dist/cjs/vite/plugins/clientTreeShakePlugin.test.cjs +185 -0
  20. package/dist/cjs/vite/plugins/clientTreeShakePlugin.test.native.js +185 -0
  21. package/dist/cjs/vite/plugins/clientTreeShakePlugin.test.native.js.map +1 -1
  22. package/dist/cjs/vite/plugins/useDomPlugin.cjs +14 -37
  23. package/dist/cjs/vite/plugins/useDomPlugin.native.js +14 -36
  24. package/dist/cjs/vite/plugins/useDomPlugin.native.js.map +1 -1
  25. package/dist/esm/native-transforms.mjs +3 -0
  26. package/dist/esm/native-transforms.mjs.map +1 -0
  27. package/dist/esm/native-transforms.native.js +3 -0
  28. package/dist/esm/native-transforms.native.js.map +1 -0
  29. package/dist/esm/router/useViteRoutes.mjs +3 -2
  30. package/dist/esm/router/useViteRoutes.mjs.map +1 -1
  31. package/dist/esm/router/useViteRoutes.native.js +3 -2
  32. package/dist/esm/router/useViteRoutes.native.js.map +1 -1
  33. package/dist/esm/router/useViteRoutes.test.mjs +77 -1
  34. package/dist/esm/router/useViteRoutes.test.mjs.map +1 -1
  35. package/dist/esm/router/useViteRoutes.test.native.js +85 -1
  36. package/dist/esm/router/useViteRoutes.test.native.js.map +1 -1
  37. package/dist/esm/vercel/build/generate/createApiServerlessFunction.mjs +45 -44
  38. package/dist/esm/vercel/build/generate/createApiServerlessFunction.mjs.map +1 -1
  39. package/dist/esm/vercel/build/generate/createApiServerlessFunction.native.js +64 -39
  40. package/dist/esm/vercel/build/generate/createApiServerlessFunction.native.js.map +1 -1
  41. package/dist/esm/vite/one.mjs +10 -2
  42. package/dist/esm/vite/one.mjs.map +1 -1
  43. package/dist/esm/vite/one.native.js +13 -4
  44. package/dist/esm/vite/one.native.js.map +1 -1
  45. package/dist/esm/vite/plugins/clientTreeShakePlugin.mjs +597 -92
  46. package/dist/esm/vite/plugins/clientTreeShakePlugin.mjs.map +1 -1
  47. package/dist/esm/vite/plugins/clientTreeShakePlugin.native.js +1358 -113
  48. package/dist/esm/vite/plugins/clientTreeShakePlugin.native.js.map +1 -1
  49. package/dist/esm/vite/plugins/clientTreeShakePlugin.test.mjs +185 -0
  50. package/dist/esm/vite/plugins/clientTreeShakePlugin.test.mjs.map +1 -1
  51. package/dist/esm/vite/plugins/clientTreeShakePlugin.test.native.js +185 -0
  52. package/dist/esm/vite/plugins/clientTreeShakePlugin.test.native.js.map +1 -1
  53. package/dist/esm/vite/plugins/useDomPlugin.mjs +13 -25
  54. package/dist/esm/vite/plugins/useDomPlugin.mjs.map +1 -1
  55. package/dist/esm/vite/plugins/useDomPlugin.native.js +14 -25
  56. package/dist/esm/vite/plugins/useDomPlugin.native.js.map +1 -1
  57. package/package.json +15 -10
  58. package/src/createApp.tsx +0 -1
  59. package/src/headless.tsx +0 -1
  60. package/src/native-transforms.ts +11 -0
  61. package/src/router/useViteRoutes.test.ts +91 -0
  62. package/src/router/useViteRoutes.tsx +7 -4
  63. package/src/vercel/build/generate/createApiServerlessFunction.ts +65 -112
  64. package/src/vite/one.ts +22 -2
  65. package/src/vite/plugins/clientTreeShakePlugin.test.ts +201 -0
  66. package/src/vite/plugins/clientTreeShakePlugin.ts +732 -152
  67. package/src/vite/plugins/useDomPlugin.ts +20 -32
  68. package/src/vite/types.ts +60 -5
  69. package/types/native-transforms.d.ts +11 -0
  70. package/types/native-transforms.d.ts.map +1 -0
  71. package/types/router/useViteRoutes.d.ts.map +1 -1
  72. package/types/vercel/build/generate/createApiServerlessFunction.d.ts.map +1 -1
  73. package/types/vite/one.d.ts.map +1 -1
  74. package/types/vite/plugins/clientTreeShakePlugin.d.ts +1 -9
  75. package/types/vite/plugins/clientTreeShakePlugin.d.ts.map +1 -1
  76. package/types/vite/plugins/useDomPlugin.d.ts.map +1 -1
  77. package/types/vite/types.d.ts +57 -5
  78. package/types/vite/types.d.ts.map +1 -1
@@ -1,56 +1,9 @@
1
1
  import { extname, relative } from 'node:path'
2
- import BabelGenerate from '@babel/generator'
3
- import { parse } from '@babel/parser'
4
- import BabelTraverse from '@babel/traverse'
5
- import type * as t from '@babel/types'
6
- import {
7
- deadCodeElimination,
8
- findReferencedIdentifiers,
9
- } from 'babel-dead-code-elimination'
2
+ import MagicString from 'magic-string'
3
+ import { parseSync } from 'oxc-parser'
10
4
  import type { Plugin } from 'vite'
11
5
  import { EMPTY_LOADER_STRING, makeLoaderRouteIdStub } from '../constants'
12
6
 
13
- const traverse = (BabelTraverse['default'] || BabelTraverse) as typeof BabelTraverse
14
- const generate = (BabelGenerate['default'] ||
15
- BabelGenerate) as any as typeof BabelGenerate
16
-
17
- // Collect type-only imports before dead code elimination runs
18
- // These should never be removed since TypeScript erases them at compile time
19
- function collectTypeImports(ast: t.File): t.ImportDeclaration[] {
20
- const typeImports: t.ImportDeclaration[] = []
21
- traverse(ast, {
22
- ImportDeclaration(path) {
23
- // Check if the entire import is type-only: `import type { X } from '...'`
24
- if (path.node.importKind === 'type') {
25
- typeImports.push(path.node)
26
- }
27
- },
28
- })
29
- return typeImports
30
- }
31
-
32
- // Restore type-only imports that may have been removed by dead code elimination
33
- function restoreTypeImports(ast: t.File, typeImports: t.ImportDeclaration[]) {
34
- if (typeImports.length === 0) return
35
-
36
- // Get existing import sources to avoid duplicates
37
- const existingSources = new Set<string>()
38
- traverse(ast, {
39
- ImportDeclaration(path) {
40
- if (path.node.importKind === 'type') {
41
- existingSources.add(path.node.source.value)
42
- }
43
- },
44
- })
45
-
46
- // Add back any type imports that were removed
47
- for (const typeImport of typeImports) {
48
- if (!existingSources.has(typeImport.source.value)) {
49
- ast.program.body.unshift(typeImport)
50
- }
51
- }
52
- }
53
-
54
7
  export const clientTreeShakePlugin = (opts?: {
55
8
  // 'rolldown' when used in the native rolldown DevEngine (no vite environment context)
56
9
  runtime?: 'vite' | 'rolldown'
@@ -94,6 +47,327 @@ export const clientTreeShakePlugin = (opts?: {
94
47
  } satisfies Plugin
95
48
  }
96
49
 
50
+ function extractPatternBindings(node: any, names: Set<string>): void {
51
+ if (!node) return
52
+ switch (node.type) {
53
+ case 'Identifier':
54
+ names.add(node.name)
55
+ break
56
+ case 'ObjectPattern':
57
+ for (const prop of node.properties) {
58
+ if (prop.type === 'Property') {
59
+ extractPatternBindings(prop.value, names)
60
+ } else if (prop.type === 'RestElement') {
61
+ extractPatternBindings(prop.argument, names)
62
+ }
63
+ }
64
+ break
65
+ case 'ArrayPattern':
66
+ for (const element of node.elements) {
67
+ if (element) {
68
+ extractPatternBindings(element, names)
69
+ }
70
+ }
71
+ break
72
+ case 'AssignmentPattern':
73
+ extractPatternBindings(node.left, names)
74
+ break
75
+ case 'RestElement':
76
+ extractPatternBindings(node.argument, names)
77
+ break
78
+ case 'TSParameterProperty':
79
+ extractPatternBindings(node.parameter, names)
80
+ break
81
+ }
82
+ }
83
+
84
+ function getReferencedIdentifiers(
85
+ rootNode: any,
86
+ candidateNames: Set<string>
87
+ ): Set<string> {
88
+ const referenced = new Set<string>()
89
+ const scopeStack: Set<string>[] = []
90
+
91
+ function isShadowed(name: string): boolean {
92
+ for (let i = scopeStack.length - 1; i >= 0; i--) {
93
+ if (scopeStack[i]!.has(name)) return true
94
+ }
95
+ return false
96
+ }
97
+
98
+ function checkIdentifier(name: string): void {
99
+ if (!isShadowed(name) && candidateNames.has(name)) {
100
+ referenced.add(name)
101
+ }
102
+ }
103
+
104
+ function walk(node: any): void {
105
+ if (!node || typeof node !== 'object') return
106
+
107
+ // Skip TypeScript type declarations and annotations
108
+ if (
109
+ node.type === 'TSTypeAnnotation' ||
110
+ node.type === 'TSTypeReference' ||
111
+ node.type === 'TSTypeAliasDeclaration' ||
112
+ node.type === 'TSInterfaceDeclaration' ||
113
+ node.type === 'TSTypeParameterDeclaration' ||
114
+ node.type === 'TSTypeParameterInstantiation'
115
+ ) {
116
+ return
117
+ }
118
+
119
+ if (
120
+ node.type === 'TSAsExpression' ||
121
+ node.type === 'TSTypeAssertion' ||
122
+ node.type === 'TSSatisfiesExpression' ||
123
+ node.type === 'TSNonNullExpression'
124
+ ) {
125
+ walk(node.expression)
126
+ return
127
+ }
128
+
129
+ function walkPatternExpressions(pat: any): void {
130
+ if (!pat || typeof pat !== 'object') return
131
+ switch (pat.type) {
132
+ case 'AssignmentPattern':
133
+ walk(pat.right)
134
+ walkPatternExpressions(pat.left)
135
+ break
136
+ case 'ObjectPattern':
137
+ for (const prop of pat.properties) {
138
+ if (prop.type === 'Property') {
139
+ if (prop.computed) {
140
+ walk(prop.key)
141
+ }
142
+ walkPatternExpressions(prop.value)
143
+ } else if (prop.type === 'RestElement') {
144
+ walkPatternExpressions(prop.argument)
145
+ }
146
+ }
147
+ break
148
+ case 'ArrayPattern':
149
+ for (const element of pat.elements) {
150
+ if (element) {
151
+ walkPatternExpressions(element)
152
+ }
153
+ }
154
+ break
155
+ case 'RestElement':
156
+ walkPatternExpressions(pat.argument)
157
+ break
158
+ case 'TSParameterProperty':
159
+ walkPatternExpressions(pat.parameter)
160
+ break
161
+ }
162
+ }
163
+
164
+ // Function scopes
165
+ if (
166
+ node.type === 'FunctionDeclaration' ||
167
+ node.type === 'FunctionExpression' ||
168
+ node.type === 'ArrowFunctionExpression'
169
+ ) {
170
+ const scope = new Set<string>()
171
+ if (node.type === 'FunctionExpression' && node.id) {
172
+ scope.add(node.id.name)
173
+ }
174
+ if (node.params) {
175
+ for (const param of node.params) {
176
+ walkPatternExpressions(param)
177
+ extractPatternBindings(param, scope)
178
+ }
179
+ }
180
+ // Add var declarations hoisted in function
181
+ function addVars(n: any): void {
182
+ if (!n || typeof n !== 'object') return
183
+ if (
184
+ n.type === 'FunctionDeclaration' ||
185
+ n.type === 'FunctionExpression' ||
186
+ n.type === 'ArrowFunctionExpression'
187
+ ) {
188
+ return // do not cross into inner functions
189
+ }
190
+ if (n.type === 'VariableDeclaration' && n.kind === 'var') {
191
+ for (const d of n.declarations) {
192
+ extractPatternBindings(d.id, scope)
193
+ }
194
+ }
195
+ for (const k of Object.keys(n)) {
196
+ if (k === 'parent') continue
197
+ const val = n[k]
198
+ if (Array.isArray(val)) {
199
+ for (const item of val) addVars(item)
200
+ } else if (val && typeof val === 'object') {
201
+ addVars(val)
202
+ }
203
+ }
204
+ }
205
+ addVars(node.body)
206
+
207
+ scopeStack.push(scope)
208
+ walk(node.body)
209
+ scopeStack.pop()
210
+ return
211
+ }
212
+
213
+ // Block scopes
214
+ if (node.type === 'BlockStatement') {
215
+ const scope = new Set<string>()
216
+ for (const stmt of node.body) {
217
+ if (
218
+ stmt.type === 'VariableDeclaration' &&
219
+ (stmt.kind === 'let' || stmt.kind === 'const')
220
+ ) {
221
+ for (const decl of stmt.declarations) {
222
+ extractPatternBindings(decl.id, scope)
223
+ }
224
+ } else if (stmt.type === 'FunctionDeclaration' && stmt.id) {
225
+ scope.add(stmt.id.name)
226
+ } else if (stmt.type === 'ClassDeclaration' && stmt.id) {
227
+ scope.add(stmt.id.name)
228
+ }
229
+ }
230
+ scopeStack.push(scope)
231
+ for (const stmt of node.body) {
232
+ walk(stmt)
233
+ }
234
+ scopeStack.pop()
235
+ return
236
+ }
237
+
238
+ // CatchClause
239
+ if (node.type === 'CatchClause') {
240
+ const scope = new Set<string>()
241
+ if (node.param) {
242
+ extractPatternBindings(node.param, scope)
243
+ }
244
+ scopeStack.push(scope)
245
+ walk(node.body)
246
+ scopeStack.pop()
247
+ return
248
+ }
249
+
250
+ // Loop scopes
251
+ if (
252
+ node.type === 'ForStatement' ||
253
+ node.type === 'ForInStatement' ||
254
+ node.type === 'ForOfStatement'
255
+ ) {
256
+ const scope = new Set<string>()
257
+ const initOrLeft = node.init || node.left
258
+ if (
259
+ initOrLeft &&
260
+ initOrLeft.type === 'VariableDeclaration' &&
261
+ initOrLeft.kind !== 'var'
262
+ ) {
263
+ for (const decl of initOrLeft.declarations) {
264
+ extractPatternBindings(decl.id, scope)
265
+ }
266
+ }
267
+ scopeStack.push(scope)
268
+ if (node.init) walk(node.init)
269
+ if (node.left) walk(node.left)
270
+ if (node.right) walk(node.right)
271
+ if (node.test) walk(node.test)
272
+ if (node.update) walk(node.update)
273
+ walk(node.body)
274
+ scopeStack.pop()
275
+ return
276
+ }
277
+
278
+ // Identifiers
279
+ if (node.type === 'Identifier') {
280
+ checkIdentifier(node.name)
281
+ return
282
+ }
283
+
284
+ // JSX
285
+ if (node.type === 'JSXOpeningElement') {
286
+ if (node.name) {
287
+ if (node.name.type === 'JSXIdentifier') {
288
+ if (node.name.name && /^[A-Z]/.test(node.name.name)) {
289
+ checkIdentifier(node.name.name)
290
+ }
291
+ } else if (node.name.type === 'JSXMemberExpression') {
292
+ let rootObj = node.name.object
293
+ while (rootObj && rootObj.type === 'JSXMemberExpression') {
294
+ rootObj = rootObj.object
295
+ }
296
+ if (rootObj && rootObj.type === 'JSXIdentifier') {
297
+ checkIdentifier(rootObj.name)
298
+ }
299
+ }
300
+ }
301
+ if (node.attributes) {
302
+ for (const attr of node.attributes) {
303
+ walk(attr)
304
+ }
305
+ }
306
+ return
307
+ }
308
+
309
+ if (node.type === 'JSXAttribute') {
310
+ if (node.value) walk(node.value)
311
+ return
312
+ }
313
+
314
+ if (node.type === 'JSXClosingElement') {
315
+ return
316
+ }
317
+
318
+ // Property in ObjectExpression
319
+ if (node.type === 'Property') {
320
+ if (node.computed) {
321
+ walk(node.key)
322
+ }
323
+ walk(node.value)
324
+ return
325
+ }
326
+
327
+ // MemberExpression
328
+ if (node.type === 'MemberExpression') {
329
+ walk(node.object)
330
+ if (node.computed) {
331
+ walk(node.property)
332
+ }
333
+ return
334
+ }
335
+
336
+ // VariableDeclarator
337
+ if (node.type === 'VariableDeclarator') {
338
+ walkPatternExpressions(node.id)
339
+ if (node.init) {
340
+ walk(node.init)
341
+ }
342
+ return
343
+ }
344
+
345
+ // ExportSpecifier
346
+ if (node.type === 'ExportSpecifier') {
347
+ if (node.local && node.local.type === 'Identifier') {
348
+ checkIdentifier(node.local.name)
349
+ }
350
+ return
351
+ }
352
+
353
+ // Generic recursive walk
354
+ for (const key of Object.keys(node)) {
355
+ if (key === 'parent') continue
356
+ const child = node[key]
357
+ if (Array.isArray(child)) {
358
+ for (const item of child) {
359
+ if (item && typeof item === 'object') walk(item)
360
+ }
361
+ } else if (child && typeof child === 'object') {
362
+ walk(child)
363
+ }
364
+ }
365
+ }
366
+
367
+ walk(rootNode)
368
+ return referenced
369
+ }
370
+
97
371
  export async function transformTreeShakeClient(
98
372
  code: string,
99
373
  id: string,
@@ -104,16 +378,29 @@ export async function transformTreeShakeClient(
104
378
  return
105
379
  }
106
380
 
107
- let ast: any
381
+ const isProd = process.env.NODE_ENV === 'production'
382
+ const filename = (id || 'file.tsx').split('?')[0]!.split('#')[0]!
383
+
384
+ let parseResult: any
108
385
  try {
109
- // `as any` because babel-dead-code-elimination using @types and it conflicts :/
110
- ast = parse(code, {
111
- sourceType: 'module',
112
- plugins: ['typescript', 'jsx'],
113
- }) as any
386
+ parseResult = parseSync(filename, code)
387
+ if (parseResult.errors && parseResult.errors.length > 0) {
388
+ const errorMessage = parseResult.errors.map((e: any) => e.message).join('\n')
389
+ if (isProd) {
390
+ throw new Error(
391
+ `[one] Failed to parse ${id} with server exports:\n${errorMessage}`
392
+ )
393
+ }
394
+ console.warn(
395
+ `[one] Skipping tree shaking for ${id} due to syntax error:`,
396
+ errorMessage
397
+ )
398
+ return
399
+ }
114
400
  } catch (error) {
115
- // If there's a syntax error, skip transformation and let Vite handle the error
116
- // This prevents the dev server from crashing on syntax errors
401
+ if (isProd) {
402
+ throw error instanceof Error ? error : new Error(String(error))
403
+ }
117
404
  const errorMessage = error instanceof Error ? error.message : String(error)
118
405
  console.warn(
119
406
  `[one] Skipping tree shaking for ${id} due to syntax error:`,
@@ -122,125 +409,418 @@ export async function transformTreeShakeClient(
122
409
  return
123
410
  }
124
411
 
125
- let referenced: any
412
+ const ast = parseResult.program
413
+
126
414
  try {
127
- referenced = findReferencedIdentifiers(ast)
415
+ return doTreeShakeClient(code, id, root, ast, routerRoot)
128
416
  } catch (error) {
129
- // If finding referenced identifiers fails, skip transformation
417
+ if (isProd) {
418
+ throw error instanceof Error ? error : new Error(String(error))
419
+ }
130
420
  const errorMessage = error instanceof Error ? error.message : String(error)
131
421
  console.warn(
132
- `[one] Skipping tree shaking for ${id} due to identifier analysis error:`,
422
+ `[one] Skipping tree shaking for ${id} due to analysis error:`,
133
423
  errorMessage
134
424
  )
135
425
  return
136
426
  }
427
+ }
137
428
 
138
- const removed = {
429
+ function doTreeShakeClient(
430
+ code: string,
431
+ id: string,
432
+ root: string | undefined,
433
+ ast: any,
434
+ routerRoot: string
435
+ ) {
436
+ const removed: Record<string, boolean> = {
139
437
  loader: false,
140
438
  generateStaticParams: false,
141
439
  }
142
440
 
143
- // note: only handles inline declarations (export function loader / export const loader)
144
- // importing a loader from another file (export { loader } from './other') is not supported
145
- // and will break client-side tree shaking. loaders must be defined in the route file.
146
- try {
147
- traverse(ast, {
148
- ExportNamedDeclaration(path) {
149
- if (
150
- path.node.declaration &&
151
- path.node.declaration.type === 'FunctionDeclaration'
152
- ) {
153
- if (!path.node.declaration.id) return
154
- const functionName = path.node.declaration.id.name
155
- if (functionName === 'loader' || functionName === 'generateStaticParams') {
156
- path.remove()
157
- removed[functionName] = true
441
+ const serverExportStmts = new Map<
442
+ any,
443
+ { names: Set<string>; replaceAll: boolean; declaratorsToRemove: Set<any> }
444
+ >()
445
+
446
+ for (const stmt of ast.body) {
447
+ if (stmt.type === 'ExportNamedDeclaration' && stmt.declaration) {
448
+ if (stmt.declaration.type === 'FunctionDeclaration') {
449
+ const fnName = stmt.declaration.id?.name
450
+ if (fnName === 'loader' || fnName === 'generateStaticParams') {
451
+ removed[fnName] = true
452
+ serverExportStmts.set(stmt, {
453
+ names: new Set([fnName]),
454
+ replaceAll: true,
455
+ declaratorsToRemove: new Set(),
456
+ })
457
+ }
458
+ } else if (stmt.declaration.type === 'VariableDeclaration') {
459
+ const found: any[] = []
460
+ for (const d of stmt.declaration.declarations) {
461
+ if (
462
+ d.id?.type === 'Identifier' &&
463
+ (d.id.name === 'loader' || d.id.name === 'generateStaticParams')
464
+ ) {
465
+ found.push(d)
466
+ removed[d.id.name] = true
158
467
  }
159
- } else if (
160
- path.node.declaration &&
161
- path.node.declaration.type === 'VariableDeclaration'
162
- ) {
163
- path.node.declaration.declarations.forEach((declarator, index) => {
164
- if (
165
- declarator.id.type === 'Identifier' &&
166
- (declarator.id.name === 'loader' ||
167
- declarator.id.name === 'generateStaticParams')
168
- ) {
169
- const declaration = path.get('declaration.declarations.' + index)
170
- if (!Array.isArray(declaration) && declaration) {
171
- ;(declaration as any).remove()
172
- removed[declarator.id.name] = true
173
- }
174
- }
468
+ }
469
+ if (found.length > 0) {
470
+ serverExportStmts.set(stmt, {
471
+ names: new Set(found.map((d) => d.id.name)),
472
+ replaceAll: found.length === stmt.declaration.declarations.length,
473
+ declaratorsToRemove: new Set(found),
175
474
  })
176
475
  }
177
- },
178
- })
179
- } catch (error) {
180
- // If traversal fails, skip transformation
181
- const errorMessage = error instanceof Error ? error.message : String(error)
182
- console.warn(
183
- `[one] Skipping tree shaking for ${id} due to traversal error:`,
184
- errorMessage
185
- )
476
+ }
477
+ }
478
+ }
479
+
480
+ const removedFunctions = Object.keys(removed).filter((k) => removed[k])
481
+ if (removedFunctions.length === 0) {
186
482
  return
187
483
  }
188
484
 
189
- const removedFunctions = Object.keys(removed).filter((key) => removed[key])
190
-
191
- if (removedFunctions.length) {
192
- try {
193
- // Collect type-only imports before dead code elimination
194
- // These should be preserved since TypeScript erases them at compile time
195
- const typeImports = collectTypeImports(ast)
196
-
197
- deadCodeElimination(ast, referenced)
198
-
199
- // Restore any type imports that were incorrectly removed
200
- restoreTypeImports(ast, typeImports)
201
-
202
- const out = generate(ast, { retainLines: true })
203
-
204
- // add back in empty or filled loader and genparams
205
- const codeOut =
206
- out.code +
207
- '\n\n' +
208
- removedFunctions
209
- .map((key) => {
210
- if (key === 'loader') {
211
- if (root) {
212
- // compute routeId relative to the router root so it matches the
213
- // route contextKey format buildPage looks the stub up by.
214
- // contextKeys are like "./_layout.tsx", "./matches-test/page1+ssg.tsx"
215
- const fromRoot = relative(root, id).replace(/\\/g, '/')
216
- const prefix = routerRoot.replace(/^\.\//, '').replace(/\/$/, '') + '/'
217
- const routeId =
218
- './' + (fromRoot.startsWith(prefix) ? fromRoot.slice(prefix.length) : fromRoot)
219
- return makeLoaderRouteIdStub(routeId)
220
- }
221
- return EMPTY_LOADER_STRING
485
+ // Collect all module bindings
486
+ const moduleBindings = new Map<
487
+ string,
488
+ {
489
+ kind: 'import' | 'function' | 'variable' | 'class' | 'enum'
490
+ stmt: any
491
+ node: any
492
+ isType?: boolean
493
+ }
494
+ >()
495
+
496
+ for (const stmt of ast.body) {
497
+ if (stmt.type === 'ImportDeclaration') {
498
+ const isDeclType = stmt.importKind === 'type'
499
+ for (const sp of stmt.specifiers) {
500
+ const isType = isDeclType || sp.importKind === 'type'
501
+ moduleBindings.set(sp.local.name, {
502
+ kind: 'import',
503
+ stmt,
504
+ node: sp,
505
+ isType,
506
+ })
507
+ }
508
+ } else if (stmt.type === 'ExportNamedDeclaration' && stmt.declaration) {
509
+ const decl = stmt.declaration
510
+ if (decl.type === 'FunctionDeclaration' && decl.id) {
511
+ moduleBindings.set(decl.id.name, { kind: 'function', stmt, node: decl })
512
+ } else if (decl.type === 'VariableDeclaration') {
513
+ for (const d of decl.declarations) {
514
+ const names = new Set<string>()
515
+ extractPatternBindings(d.id, names)
516
+ for (const name of names) {
517
+ moduleBindings.set(name, { kind: 'variable', stmt, node: d })
518
+ }
519
+ }
520
+ } else if (decl.type === 'ClassDeclaration' && decl.id) {
521
+ moduleBindings.set(decl.id.name, { kind: 'class', stmt, node: decl })
522
+ }
523
+ } else if (stmt.type === 'FunctionDeclaration' && stmt.id) {
524
+ moduleBindings.set(stmt.id.name, { kind: 'function', stmt, node: stmt })
525
+ } else if (stmt.type === 'VariableDeclaration') {
526
+ for (const d of stmt.declarations) {
527
+ const names = new Set<string>()
528
+ extractPatternBindings(d.id, names)
529
+ for (const name of names) {
530
+ moduleBindings.set(name, { kind: 'variable', stmt, node: d })
531
+ }
532
+ }
533
+ } else if (stmt.type === 'ClassDeclaration' && stmt.id) {
534
+ moduleBindings.set(stmt.id.name, { kind: 'class', stmt, node: stmt })
535
+ } else if (stmt.type === 'TSEnumDeclaration' && stmt.id) {
536
+ moduleBindings.set(stmt.id.name, { kind: 'enum', stmt, node: stmt })
537
+ }
538
+ }
539
+
540
+ const allBindingNames = new Set(moduleBindings.keys())
541
+ const topLevelDeps = new Map<string, Set<string>>()
542
+ const clientRootDeps = new Set<string>()
543
+
544
+ // Compute dependencies of all module declarations
545
+ for (const [name, info] of moduleBindings) {
546
+ if (info.kind === 'import') {
547
+ topLevelDeps.set(name, new Set())
548
+ } else if (info.kind === 'function') {
549
+ topLevelDeps.set(name, getReferencedIdentifiers(info.node, allBindingNames))
550
+ } else if (info.kind === 'variable') {
551
+ const deps = getReferencedIdentifiers(info.node, allBindingNames)
552
+ deps.delete(name)
553
+ topLevelDeps.set(name, deps)
554
+ } else if (info.kind === 'class') {
555
+ topLevelDeps.set(name, getReferencedIdentifiers(info.node, allBindingNames))
556
+ } else if (info.kind === 'enum') {
557
+ topLevelDeps.set(name, getReferencedIdentifiers(info.node, allBindingNames))
558
+ }
559
+ }
560
+
561
+ // 1. BFS for server needed (transitive closure of loader and generateStaticParams)
562
+ const serverNeeded = new Set<string>()
563
+ const serverQueue: string[] = []
564
+ for (const fn of removedFunctions) {
565
+ const deps = topLevelDeps.get(fn)
566
+ if (deps) {
567
+ for (const dep of deps) serverQueue.push(dep)
568
+ }
569
+ }
570
+ while (serverQueue.length > 0) {
571
+ const name = serverQueue.shift()!
572
+ if (serverNeeded.has(name)) continue
573
+ serverNeeded.add(name)
574
+ const deps = topLevelDeps.get(name)
575
+ if (deps) {
576
+ for (const dep of deps) {
577
+ if (!serverNeeded.has(dep)) serverQueue.push(dep)
578
+ }
579
+ }
580
+ }
581
+
582
+ // 2. Compute client root dependencies from all retained statements
583
+ for (const stmt of ast.body) {
584
+ if (stmt.type === 'ImportDeclaration') {
585
+ continue
586
+ }
587
+
588
+ if (serverExportStmts.has(stmt)) {
589
+ const info = serverExportStmts.get(stmt)!
590
+ if (!info.replaceAll && stmt.declaration?.declarations) {
591
+ for (const d of stmt.declaration.declarations) {
592
+ if (!info.declaratorsToRemove.has(d)) {
593
+ const deps = getReferencedIdentifiers(d, allBindingNames)
594
+ for (const dep of deps) clientRootDeps.add(dep)
595
+ }
596
+ }
597
+ }
598
+ continue
599
+ }
600
+
601
+ if (stmt.type === 'VariableDeclaration') {
602
+ const allInServer = stmt.declarations.every((d: any) => {
603
+ const names = new Set<string>()
604
+ extractPatternBindings(d.id, names)
605
+ return names.size > 0 && Array.from(names).every((n) => serverNeeded.has(n))
606
+ })
607
+ if (!allInServer) {
608
+ // Retained on client!
609
+ for (const d of stmt.declarations) {
610
+ const names = new Set<string>()
611
+ extractPatternBindings(d.id, names)
612
+ const inServer =
613
+ names.size > 0 && Array.from(names).every((n) => serverNeeded.has(n))
614
+ if (!inServer) {
615
+ const deps = getReferencedIdentifiers(d, allBindingNames)
616
+ for (const dep of deps) clientRootDeps.add(dep)
617
+ }
618
+ }
619
+ }
620
+ continue
621
+ }
622
+
623
+ if (
624
+ (stmt.type === 'FunctionDeclaration' ||
625
+ stmt.type === 'ClassDeclaration' ||
626
+ stmt.type === 'TSEnumDeclaration') &&
627
+ stmt.id &&
628
+ serverNeeded.has(stmt.id.name)
629
+ ) {
630
+ // Local server declaration, do not treat as client root
631
+ continue
632
+ }
633
+
634
+ // All other top-level statements (ExportDefaultDeclaration, ExportNamedDeclaration,
635
+ // ExpressionStatement, IfStatement, ForStatement, WhileStatement, TryStatement, BlockStatement, etc.)
636
+ const deps = getReferencedIdentifiers(stmt, allBindingNames)
637
+ for (const dep of deps) clientRootDeps.add(dep)
638
+ }
639
+
640
+ // 3. BFS for client needed
641
+ const clientNeeded = new Set<string>()
642
+ const clientQueue = Array.from(clientRootDeps)
643
+ while (clientQueue.length > 0) {
644
+ const name = clientQueue.shift()!
645
+ if (clientNeeded.has(name)) continue
646
+ clientNeeded.add(name)
647
+ // Do NOT expand server stub exports into server dependencies
648
+ if (name === 'loader' || name === 'generateStaticParams') continue
649
+ const deps = topLevelDeps.get(name)
650
+ if (deps) {
651
+ for (const dep of deps) {
652
+ if (!clientNeeded.has(dep)) clientQueue.push(dep)
653
+ }
654
+ }
655
+ }
656
+
657
+ // 4. Exclusive server bindings
658
+ const exclusiveServerBindings = new Set<string>()
659
+ for (const name of serverNeeded) {
660
+ if (!clientNeeded.has(name)) {
661
+ exclusiveServerBindings.add(name)
662
+ }
663
+ }
664
+
665
+ // Use MagicString to apply modifications
666
+ const s = new MagicString(code)
667
+
668
+ // 1. Replace server export declarations with stubs
669
+ for (const [stmt, info] of serverExportStmts) {
670
+ const stubs: string[] = []
671
+ if (info.names.has('loader')) {
672
+ if (root) {
673
+ // compute routeId relative to the router root so it matches the
674
+ // route contextKey format buildPage looks the stub up by.
675
+ // contextKeys are like "./_layout.tsx", "./matches-test/page1+ssg.tsx"
676
+ const fromRoot = relative(root, id).replace(/\\/g, '/')
677
+ const prefix = routerRoot.replace(/^\.\//, '').replace(/\/$/, '') + '/'
678
+ const routeId =
679
+ './' + (fromRoot.startsWith(prefix) ? fromRoot.slice(prefix.length) : fromRoot)
680
+ stubs.push(makeLoaderRouteIdStub(routeId))
681
+ } else {
682
+ stubs.push(EMPTY_LOADER_STRING)
683
+ }
684
+ }
685
+ if (info.names.has('generateStaticParams')) {
686
+ stubs.push('export function generateStaticParams() {};')
687
+ }
688
+
689
+ if (info.replaceAll) {
690
+ s.overwrite(stmt.start, stmt.end, stubs.join('\n'))
691
+ } else {
692
+ // Remove only the server declarators
693
+ const decl = stmt.declaration
694
+ for (let i = decl.declarations.length - 1; i >= 0; i--) {
695
+ const d = decl.declarations[i]
696
+ if (info.declaratorsToRemove.has(d)) {
697
+ if (i === 0) {
698
+ const next = decl.declarations[1]
699
+ s.remove(d.start, next.start)
700
+ } else {
701
+ const prev = decl.declarations[i - 1]
702
+ s.remove(prev.end, d.end)
703
+ }
704
+ }
705
+ }
706
+ s.appendRight(stmt.end, '\n' + stubs.join('\n'))
707
+ }
708
+ }
709
+
710
+ // 2. Remove exclusive local declarations (functions, variables, classes)
711
+ for (const stmt of ast.body) {
712
+ if (serverExportStmts.has(stmt) || stmt.type === 'ImportDeclaration') continue
713
+
714
+ if (
715
+ (stmt.type === 'FunctionDeclaration' ||
716
+ stmt.type === 'ClassDeclaration' ||
717
+ stmt.type === 'TSEnumDeclaration') &&
718
+ stmt.id &&
719
+ exclusiveServerBindings.has(stmt.id.name)
720
+ ) {
721
+ let end = stmt.end
722
+ if (code[end] === ';') end++
723
+ if (code[end] === '\r' && code[end + 1] === '\n') end += 2
724
+ else if (code[end] === '\n') end += 1
725
+ s.remove(stmt.start, end)
726
+ } else if (stmt.type === 'VariableDeclaration') {
727
+ const allExclusive = stmt.declarations.every((d: any) => {
728
+ const names = new Set<string>()
729
+ extractPatternBindings(d.id, names)
730
+ return Array.from(names).every((n) => exclusiveServerBindings.has(n))
731
+ })
732
+ if (allExclusive) {
733
+ let end = stmt.end
734
+ if (code[end] === ';') end++
735
+ if (code[end] === '\r' && code[end + 1] === '\n') end += 2
736
+ else if (code[end] === '\n') end += 1
737
+ s.remove(stmt.start, end)
738
+ } else {
739
+ // Check if any individual declarators should be removed
740
+ for (let i = stmt.declarations.length - 1; i >= 0; i--) {
741
+ const d = stmt.declarations[i]
742
+ const names = new Set<string>()
743
+ extractPatternBindings(d.id, names)
744
+ if (Array.from(names).every((n) => exclusiveServerBindings.has(n))) {
745
+ if (i === 0) {
746
+ const next = stmt.declarations[1]
747
+ s.remove(d.start, next.start)
748
+ } else {
749
+ const prev = stmt.declarations[i - 1]
750
+ s.remove(prev.end, d.end)
222
751
  }
752
+ }
753
+ }
754
+ }
755
+ } else if (
756
+ stmt.type === 'ClassDeclaration' &&
757
+ stmt.id &&
758
+ exclusiveServerBindings.has(stmt.id.name)
759
+ ) {
760
+ let end = stmt.end
761
+ if (code[end] === ';') end++
762
+ if (code[end] === '\r' && code[end + 1] === '\n') end += 2
763
+ else if (code[end] === '\n') end += 1
764
+ s.remove(stmt.start, end)
765
+ }
766
+ }
223
767
 
224
- return `export function generateStaticParams() {};`
225
- })
226
- .join('\n')
768
+ // 3. Remove unused import specifiers / declarations
769
+ for (const stmt of ast.body) {
770
+ if (stmt.type !== 'ImportDeclaration') continue
771
+ if (stmt.importKind === 'type') continue // Preserve type-only imports
772
+ if (!stmt.specifiers || stmt.specifiers.length === 0) continue // Preserve side-effect imports
227
773
 
228
- console.info(
229
- ` 🧹 [one] ${relative(process.cwd(), id)} removed ${removedFunctions.length} server-only exports`
230
- )
774
+ const kept = stmt.specifiers.filter((sp: any) => {
775
+ if (sp.importKind === 'type') return true
776
+ return !exclusiveServerBindings.has(sp.local.name)
777
+ })
778
+
779
+ if (kept.length === 0) {
780
+ let end = stmt.end
781
+ if (code[end] === ';') end++
782
+ if (code[end] === '\r' && code[end + 1] === '\n') end += 2
783
+ else if (code[end] === '\n') end += 1
784
+ s.remove(stmt.start, end)
785
+ } else if (kept.length < stmt.specifiers.length) {
786
+ const declCode = code.slice(stmt.start, stmt.source.start)
787
+ const prefixMatch = declCode.match(/^import\s+/)
788
+ const prefixEnd = stmt.start + (prefixMatch ? prefixMatch[0].length : 7)
789
+
790
+ let pos = stmt.source.start - 1
791
+ while (pos >= 0 && /\s/.test(code[pos]!)) pos--
792
+ const fromStart = pos - 3 // 'from' keyword start
793
+
794
+ const defaultSpec = kept.find((sp: any) => sp.type === 'ImportDefaultSpecifier')
795
+ const namespaceSpec = kept.find((sp: any) => sp.type === 'ImportNamespaceSpecifier')
796
+ const namedSpecs = kept.filter((sp: any) => sp.type === 'ImportSpecifier')
231
797
 
232
- return {
233
- code: codeOut,
234
- map: out.map,
798
+ const parts: string[] = []
799
+ if (defaultSpec) parts.push(defaultSpec.local.name)
800
+ if (namespaceSpec) parts.push('* as ' + namespaceSpec.local.name)
801
+ if (namedSpecs.length > 0) {
802
+ const namedStr = namedSpecs
803
+ .map((sp: any) => {
804
+ const typePrefix = sp.importKind === 'type' ? 'type ' : ''
805
+ const importedName = sp.imported.raw || sp.imported.name
806
+ if (importedName === sp.local.name) {
807
+ return typePrefix + sp.local.name
808
+ }
809
+ return typePrefix + importedName + ' as ' + sp.local.name
810
+ })
811
+ .join(', ')
812
+ parts.push('{ ' + namedStr + ' }')
235
813
  }
236
- } catch (error) {
237
- // If code generation fails, skip transformation
238
- const errorMessage = error instanceof Error ? error.message : String(error)
239
- console.warn(
240
- `[one] Skipping tree shaking for ${id} due to code generation error:`,
241
- errorMessage
242
- )
243
- return
814
+ s.overwrite(prefixEnd, fromStart, parts.join(', ') + ' ')
244
815
  }
245
816
  }
817
+
818
+ console.info(
819
+ ` 🧹 [one] ${relative(process.cwd(), id)} removed ${removedFunctions.length} server-only exports`
820
+ )
821
+
822
+ return {
823
+ code: s.toString(),
824
+ map: s.generateMap({ hires: 'boundary', source: id, includeContent: true }),
825
+ }
246
826
  }