@tanstack/start-plugin-core 1.161.3 → 1.162.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 (32) hide show
  1. package/dist/esm/import-protection-plugin/defaults.d.ts +6 -4
  2. package/dist/esm/import-protection-plugin/defaults.js +3 -12
  3. package/dist/esm/import-protection-plugin/defaults.js.map +1 -1
  4. package/dist/esm/import-protection-plugin/plugin.d.ts +1 -1
  5. package/dist/esm/import-protection-plugin/plugin.js +488 -257
  6. package/dist/esm/import-protection-plugin/plugin.js.map +1 -1
  7. package/dist/esm/import-protection-plugin/postCompileUsage.d.ts +4 -2
  8. package/dist/esm/import-protection-plugin/postCompileUsage.js +31 -150
  9. package/dist/esm/import-protection-plugin/postCompileUsage.js.map +1 -1
  10. package/dist/esm/import-protection-plugin/rewriteDeniedImports.js +13 -9
  11. package/dist/esm/import-protection-plugin/rewriteDeniedImports.js.map +1 -1
  12. package/dist/esm/import-protection-plugin/sourceLocation.d.ts +32 -66
  13. package/dist/esm/import-protection-plugin/sourceLocation.js +129 -56
  14. package/dist/esm/import-protection-plugin/sourceLocation.js.map +1 -1
  15. package/dist/esm/import-protection-plugin/trace.d.ts +10 -0
  16. package/dist/esm/import-protection-plugin/trace.js +30 -44
  17. package/dist/esm/import-protection-plugin/trace.js.map +1 -1
  18. package/dist/esm/import-protection-plugin/utils.d.ts +8 -4
  19. package/dist/esm/import-protection-plugin/utils.js +43 -1
  20. package/dist/esm/import-protection-plugin/utils.js.map +1 -1
  21. package/dist/esm/import-protection-plugin/virtualModules.d.ts +7 -1
  22. package/dist/esm/import-protection-plugin/virtualModules.js +104 -135
  23. package/dist/esm/import-protection-plugin/virtualModules.js.map +1 -1
  24. package/package.json +8 -8
  25. package/src/import-protection-plugin/defaults.ts +8 -19
  26. package/src/import-protection-plugin/plugin.ts +776 -433
  27. package/src/import-protection-plugin/postCompileUsage.ts +57 -229
  28. package/src/import-protection-plugin/rewriteDeniedImports.ts +34 -42
  29. package/src/import-protection-plugin/sourceLocation.ts +184 -185
  30. package/src/import-protection-plugin/trace.ts +38 -49
  31. package/src/import-protection-plugin/utils.ts +62 -1
  32. package/src/import-protection-plugin/virtualModules.ts +163 -177
@@ -1,120 +1,15 @@
1
+ import babel from '@babel/core'
1
2
  import * as t from '@babel/types'
2
3
  import { parseAst } from '@tanstack/router-utils'
3
4
 
4
- export type UsagePos = { line: number; column0: number }
5
-
6
- function collectPatternBindings(
7
- node: t.Node | null | undefined,
8
- out: Set<string>,
9
- ): void {
10
- if (!node) return
11
- if (t.isIdentifier(node)) {
12
- out.add(node.name)
13
- return
14
- }
15
- if (t.isRestElement(node)) {
16
- collectPatternBindings(node.argument, out)
17
- return
18
- }
19
- if (t.isAssignmentPattern(node)) {
20
- collectPatternBindings(node.left, out)
21
- return
22
- }
23
- if (t.isObjectPattern(node)) {
24
- for (const prop of node.properties) {
25
- if (t.isRestElement(prop)) {
26
- collectPatternBindings(prop.argument, out)
27
- } else if (t.isObjectProperty(prop)) {
28
- collectPatternBindings(prop.value as t.Node, out)
29
- }
30
- }
31
- return
32
- }
33
- if (t.isArrayPattern(node)) {
34
- for (const el of node.elements) {
35
- collectPatternBindings(el, out)
36
- }
37
- return
38
- }
39
- }
40
-
41
- function isBindingPosition(node: t.Node, parent: t.Node | null): boolean {
42
- if (!parent) return false
43
- if (t.isFunctionDeclaration(parent) && parent.id === node) return true
44
- if (t.isFunctionExpression(parent) && parent.id === node) return true
45
- if (t.isClassDeclaration(parent) && parent.id === node) return true
46
- if (t.isClassExpression(parent) && parent.id === node) return true
47
- if (t.isVariableDeclarator(parent) && parent.id === node) return true
48
- if (t.isImportSpecifier(parent) && parent.local === node) return true
49
- if (t.isImportDefaultSpecifier(parent) && parent.local === node) return true
50
- if (t.isImportNamespaceSpecifier(parent) && parent.local === node) return true
51
- if (
52
- t.isObjectProperty(parent) &&
53
- parent.key === node &&
54
- !parent.computed &&
55
- // In `{ foo }`, the identifier is also a value reference and must count as
56
- // usage. Babel represents this as `shorthand: true`.
57
- !parent.shorthand
58
- )
59
- return true
60
- if (t.isObjectMethod(parent) && parent.key === node && !parent.computed)
61
- return true
62
- if (t.isExportSpecifier(parent) && parent.exported === node) return true
63
- return false
64
- }
65
-
66
- function isPreferredUsage(node: t.Node, parent: t.Node | null): boolean {
67
- if (!parent) return false
68
- if (t.isCallExpression(parent) && parent.callee === node) return true
69
- if (t.isNewExpression(parent) && parent.callee === node) return true
70
- if (t.isMemberExpression(parent) && parent.object === node) return true
71
- return false
72
- }
73
-
74
- function isScopeNode(node: t.Node): boolean {
75
- return (
76
- t.isProgram(node) ||
77
- t.isFunctionDeclaration(node) ||
78
- t.isFunctionExpression(node) ||
79
- t.isArrowFunctionExpression(node) ||
80
- t.isBlockStatement(node) ||
81
- t.isCatchClause(node)
82
- )
83
- }
84
-
85
- /** `var` hoists to the nearest function or program scope, not block scopes. */
86
- function isFunctionScopeNode(node: t.Node): boolean {
87
- return (
88
- t.isProgram(node) ||
89
- t.isFunctionDeclaration(node) ||
90
- t.isFunctionExpression(node) ||
91
- t.isArrowFunctionExpression(node)
92
- )
93
- }
94
-
95
- function collectScopeBindings(node: t.Node, out: Set<string>): void {
96
- if (
97
- t.isFunctionDeclaration(node) ||
98
- t.isFunctionExpression(node) ||
99
- t.isArrowFunctionExpression(node)
100
- ) {
101
- for (const p of node.params) {
102
- collectPatternBindings(p, out)
103
- }
104
- return
105
- }
106
-
107
- if (t.isCatchClause(node)) {
108
- collectPatternBindings(node.param, out)
109
- return
110
- }
111
- }
5
+ type UsagePos = { line: number; column0: number }
112
6
 
113
7
  /**
114
8
  * Given transformed code, returns the first "meaningful" usage position for an
115
9
  * import from `source` that survives compilation.
116
10
  *
117
- * The returned column is 0-based (Babel loc semantics).
11
+ * "Preferred" positions (call, new, member-access) take priority over bare
12
+ * identifier references. The returned column is 0-based (Babel loc semantics).
118
13
  */
119
14
  export function findPostCompileUsagePos(
120
15
  code: string,
@@ -122,7 +17,7 @@ export function findPostCompileUsagePos(
122
17
  ): UsagePos | undefined {
123
18
  const ast = parseAst({ code })
124
19
 
125
- // 1) Determine local names bound from this specifier
20
+ // Collect local names bound from this specifier
126
21
  const imported = new Set<string>()
127
22
  for (const node of ast.program.body) {
128
23
  if (t.isImportDeclaration(node) && node.source.value === source) {
@@ -138,129 +33,62 @@ export function findPostCompileUsagePos(
138
33
  let preferred: UsagePos | undefined
139
34
  let anyUsage: UsagePos | undefined
140
35
 
141
- // Scope stack (module scope at index 0).
142
- // Each entry tracks bindings and whether it is a function/program scope
143
- // (needed for `var` hoisting).
144
- interface ScopeEntry {
145
- bindings: Set<string>
146
- isFnScope: boolean
147
- }
148
- const scopes: Array<ScopeEntry> = [{ bindings: new Set(), isFnScope: true }]
149
-
150
- function isShadowed(name: string): boolean {
151
- // Check inner scopes only
152
- for (let i = scopes.length - 1; i >= 1; i--) {
153
- if (scopes[i]!.bindings.has(name)) return true
154
- }
155
- return false
156
- }
157
-
158
- function record(node: t.Node, kind: 'preferred' | 'any') {
159
- const loc = node.loc?.start
160
- if (!loc) return
161
- const pos: UsagePos = { line: loc.line, column0: loc.column }
162
- if (kind === 'preferred') {
163
- preferred ||= pos
164
- } else {
165
- anyUsage ||= pos
166
- }
167
- }
168
-
169
- function pushScope(node: t.Node): void {
170
- const bindings = new Set<string>()
171
- collectScopeBindings(node, bindings)
172
- scopes.push({ bindings, isFnScope: isFunctionScopeNode(node) })
173
- }
174
-
175
- function popScope(): void {
176
- scopes.pop()
177
- }
178
-
179
- /** Find the nearest function/program scope entry in the stack. */
180
- function nearestFnScope(): ScopeEntry {
181
- for (let i = scopes.length - 1; i >= 0; i--) {
182
- if (scopes[i]!.isFnScope) return scopes[i]!
183
- }
184
- // Should never happen (index 0 is always a function scope).
185
- return scopes[0]!
186
- }
187
-
188
- // The walker accepts AST nodes, arrays (from node children like
189
- // `body`, `params`, etc.), or null/undefined for optional children.
190
- type Walkable =
191
- | t.Node
192
- | ReadonlyArray<t.Node | null | undefined>
193
- | null
194
- | undefined
195
-
196
- function walk(node: Walkable, parent: t.Node | null) {
197
- if (!node) return
198
- if (preferred && anyUsage) return
199
-
200
- if (Array.isArray(node)) {
201
- for (const n of node) walk(n, parent)
202
- return
203
- }
204
-
205
- // After the array check + early return, node is guaranteed to be t.Node.
206
- // TypeScript doesn't narrow ReadonlyArray from the union, so we assert.
207
- const astNode = node as t.Node
208
-
209
- // Skip import declarations entirely
210
- if (t.isImportDeclaration(astNode)) return
211
-
212
- const enterScope = isScopeNode(astNode)
213
- if (enterScope) {
214
- pushScope(astNode)
215
- }
216
-
217
- // Add lexical bindings for variable declarations and class/function decls.
218
- // Note: function/class *declaration* identifiers bind in the parent scope,
219
- // so we register them before walking children.
220
- if (t.isFunctionDeclaration(astNode) && astNode.id) {
221
- scopes[scopes.length - 2]?.bindings.add(astNode.id.name)
222
- }
223
- if (t.isClassDeclaration(astNode) && astNode.id) {
224
- scopes[scopes.length - 2]?.bindings.add(astNode.id.name)
225
- }
226
- if (t.isVariableDeclarator(astNode)) {
227
- // `var` hoists to the nearest function/program scope, not block scope.
228
- const isVar = t.isVariableDeclaration(parent) && parent.kind === 'var'
229
- const target = isVar
230
- ? nearestFnScope().bindings
231
- : scopes[scopes.length - 1]!.bindings
232
- collectPatternBindings(astNode.id, target)
233
- }
36
+ // babel.traverse can throw on malformed scopes (e.g. duplicate bindings from
37
+ // import + const re-declaration) because parseAst doesn't attach a hub
38
+ try {
39
+ babel.traverse(ast, {
40
+ ImportDeclaration(path) {
41
+ path.skip()
42
+ },
43
+
44
+ Identifier(path: babel.NodePath<t.Identifier>) {
45
+ if (preferred && anyUsage) {
46
+ path.stop()
47
+ return
48
+ }
234
49
 
235
- if (t.isIdentifier(astNode) && imported.has(astNode.name)) {
236
- if (!isBindingPosition(astNode, parent) && !isShadowed(astNode.name)) {
237
- if (isPreferredUsage(astNode, parent)) {
238
- record(astNode, 'preferred')
50
+ const { node, parent, scope } = path
51
+ if (!imported.has(node.name)) return
52
+
53
+ // Skip binding positions (declarations, import specifiers, etc.)
54
+ if (path.isBindingIdentifier()) return
55
+
56
+ // Skip non-shorthand object property keys — they don't reference the import
57
+ if (
58
+ t.isObjectProperty(parent) &&
59
+ parent.key === node &&
60
+ !parent.computed &&
61
+ !parent.shorthand
62
+ )
63
+ return
64
+ if (t.isObjectMethod(parent) && parent.key === node && !parent.computed)
65
+ return
66
+ if (t.isExportSpecifier(parent) && parent.exported === node) return
67
+
68
+ // Skip if shadowed by a closer binding
69
+ const binding = scope.getBinding(node.name)
70
+ if (binding && binding.kind !== 'module') return
71
+
72
+ const loc = node.loc?.start
73
+ if (!loc) return
74
+ const pos: UsagePos = { line: loc.line, column0: loc.column }
75
+
76
+ const isPreferred =
77
+ (t.isCallExpression(parent) && parent.callee === node) ||
78
+ (t.isNewExpression(parent) && parent.callee === node) ||
79
+ (t.isMemberExpression(parent) && parent.object === node)
80
+
81
+ if (isPreferred) {
82
+ preferred ||= pos
239
83
  } else {
240
- record(astNode, 'any')
84
+ anyUsage ||= pos
241
85
  }
242
- }
243
- }
244
-
245
- // Iterate child properties of this AST node. We use a Record cast since
246
- // Babel node types don't expose an index signature, but we need to walk
247
- // all child properties generically.
248
- const record_ = astNode as unknown as Record<string, unknown>
249
- for (const key of Object.keys(record_)) {
250
- const value = record_[key]
251
- if (!value) continue
252
- if (key === 'loc' || key === 'start' || key === 'end') continue
253
- if (key === 'parent') continue
254
- if (typeof value === 'string' || typeof value === 'number') continue
255
- walk(value as Walkable, astNode)
256
- if (preferred && anyUsage) break
257
- }
258
-
259
- if (enterScope) {
260
- popScope()
261
- }
86
+ },
87
+ })
88
+ } catch {
89
+ // Scope analysis failed cannot determine usage positions reliably
90
+ return undefined
262
91
  }
263
92
 
264
- walk(ast.program, null)
265
93
  return preferred ?? anyUsage
266
94
  }
@@ -2,14 +2,36 @@ import * as t from '@babel/types'
2
2
  import { generateFromAst, parseAst } from '@tanstack/router-utils'
3
3
 
4
4
  import { MOCK_MODULE_ID } from './virtualModules'
5
-
6
- // ---------------------------------------------------------------------------
7
- // Export name collection (for dev mock-edge modules)
8
- // ---------------------------------------------------------------------------
5
+ import { getOrCreate } from './utils'
9
6
 
10
7
  export function isValidExportName(name: string): boolean {
11
- if (name === 'default') return false
12
- return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name)
8
+ if (name === 'default' || name.length === 0) return false
9
+ const first = name.charCodeAt(0)
10
+ // First char: A-Z (65-90), a-z (97-122), _ (95), $ (36)
11
+ if (
12
+ !(
13
+ (first >= 65 && first <= 90) ||
14
+ (first >= 97 && first <= 122) ||
15
+ first === 95 ||
16
+ first === 36
17
+ )
18
+ )
19
+ return false
20
+ for (let i = 1; i < name.length; i++) {
21
+ const ch = name.charCodeAt(i)
22
+ // Subsequent: A-Z, a-z, 0-9 (48-57), _, $
23
+ if (
24
+ !(
25
+ (ch >= 65 && ch <= 90) ||
26
+ (ch >= 97 && ch <= 122) ||
27
+ (ch >= 48 && ch <= 57) ||
28
+ ch === 95 ||
29
+ ch === 36
30
+ )
31
+ )
32
+ return false
33
+ }
34
+ return true
13
35
  }
14
36
 
15
37
  /**
@@ -23,13 +45,8 @@ export function collectMockExportNamesBySource(
23
45
 
24
46
  const namesBySource = new Map<string, Set<string>>()
25
47
  const add = (source: string, name: string) => {
26
- if (!isValidExportName(name)) return
27
- let set = namesBySource.get(source)
28
- if (!set) {
29
- set = new Set<string>()
30
- namesBySource.set(source, set)
31
- }
32
- set.add(name)
48
+ if (name === 'default' || name.length === 0) return
49
+ getOrCreate(namesBySource, source, () => new Set<string>()).add(name)
33
50
  }
34
51
 
35
52
  for (const node of ast.program.body) {
@@ -66,10 +83,6 @@ export function collectMockExportNamesBySource(
66
83
  return out
67
84
  }
68
85
 
69
- // ---------------------------------------------------------------------------
70
- // AST-based import rewriting
71
- // ---------------------------------------------------------------------------
72
-
73
86
  /**
74
87
  * Rewrite static imports/re-exports from denied sources using Babel AST transforms.
75
88
  *
@@ -101,16 +114,13 @@ export function rewriteDeniedImports(
101
114
  for (let i = ast.program.body.length - 1; i >= 0; i--) {
102
115
  const node = ast.program.body[i]!
103
116
 
104
- // --- import declarations ---
105
117
  if (t.isImportDeclaration(node)) {
106
- // Skip type-only imports
107
118
  if (node.importKind === 'type') continue
108
119
  if (!deniedSources.has(node.source.value)) continue
109
120
 
110
121
  const mockVar = `__tss_deny_${mockCounter++}`
111
122
  const replacements: Array<t.Statement> = []
112
123
 
113
- // import __tss_deny_N from '<mock>'
114
124
  replacements.push(
115
125
  t.importDeclaration(
116
126
  [t.importDefaultSpecifier(t.identifier(mockVar))],
@@ -119,18 +129,10 @@ export function rewriteDeniedImports(
119
129
  )
120
130
 
121
131
  for (const specifier of node.specifiers) {
122
- if (t.isImportDefaultSpecifier(specifier)) {
123
- // import def from 'denied' -> const def = __tss_deny_N
124
- replacements.push(
125
- t.variableDeclaration('const', [
126
- t.variableDeclarator(
127
- t.identifier(specifier.local.name),
128
- t.identifier(mockVar),
129
- ),
130
- ]),
131
- )
132
- } else if (t.isImportNamespaceSpecifier(specifier)) {
133
- // import * as ns from 'denied' -> const ns = __tss_deny_N
132
+ if (
133
+ t.isImportDefaultSpecifier(specifier) ||
134
+ t.isImportNamespaceSpecifier(specifier)
135
+ ) {
134
136
  replacements.push(
135
137
  t.variableDeclaration('const', [
136
138
  t.variableDeclarator(
@@ -140,9 +142,7 @@ export function rewriteDeniedImports(
140
142
  ]),
141
143
  )
142
144
  } else if (t.isImportSpecifier(specifier)) {
143
- // Skip type-only specifiers
144
145
  if (specifier.importKind === 'type') continue
145
- // import { a as b } from 'denied' -> const b = __tss_deny_N.a
146
146
  const importedName = t.isIdentifier(specifier.imported)
147
147
  ? specifier.imported.name
148
148
  : specifier.imported.value
@@ -165,7 +165,6 @@ export function rewriteDeniedImports(
165
165
  continue
166
166
  }
167
167
 
168
- // --- export { x } from 'denied' ---
169
168
  if (t.isExportNamedDeclaration(node) && node.source) {
170
169
  if (node.exportKind === 'type') continue
171
170
  if (!deniedSources.has(node.source.value)) continue
@@ -173,15 +172,12 @@ export function rewriteDeniedImports(
173
172
  const mockVar = `__tss_deny_${mockCounter++}`
174
173
  const replacements: Array<t.Statement> = []
175
174
 
176
- // import __tss_deny_N from '<mock>'
177
175
  replacements.push(
178
176
  t.importDeclaration(
179
177
  [t.importDefaultSpecifier(t.identifier(mockVar))],
180
178
  t.stringLiteral(getMockModuleId(node.source.value)),
181
179
  ),
182
180
  )
183
-
184
- // For each re-exported specifier, create an exported const
185
181
  const exportSpecifiers: Array<{
186
182
  localName: string
187
183
  exportedName: string
@@ -195,7 +191,6 @@ export function rewriteDeniedImports(
195
191
  : specifier.exported.value
196
192
 
197
193
  const internalVar = `__tss_reexport_${localName}`
198
- // const __tss_reexport_x = __tss_deny_N.x
199
194
  replacements.push(
200
195
  t.variableDeclaration('const', [
201
196
  t.variableDeclarator(
@@ -211,7 +206,6 @@ export function rewriteDeniedImports(
211
206
  }
212
207
  }
213
208
 
214
- // export { __tss_reexport_x as x, ... }
215
209
  if (exportSpecifiers.length > 0) {
216
210
  replacements.push(
217
211
  t.exportNamedDeclaration(
@@ -231,12 +225,10 @@ export function rewriteDeniedImports(
231
225
  continue
232
226
  }
233
227
 
234
- // --- export * from 'denied' ---
235
228
  if (t.isExportAllDeclaration(node)) {
236
229
  if (node.exportKind === 'type') continue
237
230
  if (!deniedSources.has(node.source.value)) continue
238
231
 
239
- // Remove the star re-export entirely
240
232
  ast.program.body.splice(i, 1)
241
233
  modified = true
242
234
  continue