@tanstack/start-plugin-core 1.160.2 → 1.161.1

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 (58) hide show
  1. package/dist/esm/dev-server-plugin/plugin.js +1 -1
  2. package/dist/esm/dev-server-plugin/plugin.js.map +1 -1
  3. package/dist/esm/import-protection-plugin/defaults.d.ts +17 -0
  4. package/dist/esm/import-protection-plugin/defaults.js +36 -0
  5. package/dist/esm/import-protection-plugin/defaults.js.map +1 -0
  6. package/dist/esm/import-protection-plugin/matchers.d.ts +13 -0
  7. package/dist/esm/import-protection-plugin/matchers.js +31 -0
  8. package/dist/esm/import-protection-plugin/matchers.js.map +1 -0
  9. package/dist/esm/import-protection-plugin/plugin.d.ts +16 -0
  10. package/dist/esm/import-protection-plugin/plugin.js +699 -0
  11. package/dist/esm/import-protection-plugin/plugin.js.map +1 -0
  12. package/dist/esm/import-protection-plugin/postCompileUsage.d.ts +11 -0
  13. package/dist/esm/import-protection-plugin/postCompileUsage.js +177 -0
  14. package/dist/esm/import-protection-plugin/postCompileUsage.js.map +1 -0
  15. package/dist/esm/import-protection-plugin/rewriteDeniedImports.d.ts +27 -0
  16. package/dist/esm/import-protection-plugin/rewriteDeniedImports.js +51 -0
  17. package/dist/esm/import-protection-plugin/rewriteDeniedImports.js.map +1 -0
  18. package/dist/esm/import-protection-plugin/sourceLocation.d.ts +132 -0
  19. package/dist/esm/import-protection-plugin/sourceLocation.js +255 -0
  20. package/dist/esm/import-protection-plugin/sourceLocation.js.map +1 -0
  21. package/dist/esm/import-protection-plugin/trace.d.ts +67 -0
  22. package/dist/esm/import-protection-plugin/trace.js +204 -0
  23. package/dist/esm/import-protection-plugin/trace.js.map +1 -0
  24. package/dist/esm/import-protection-plugin/utils.d.ts +8 -0
  25. package/dist/esm/import-protection-plugin/utils.js +29 -0
  26. package/dist/esm/import-protection-plugin/utils.js.map +1 -0
  27. package/dist/esm/import-protection-plugin/virtualModules.d.ts +25 -0
  28. package/dist/esm/import-protection-plugin/virtualModules.js +235 -0
  29. package/dist/esm/import-protection-plugin/virtualModules.js.map +1 -0
  30. package/dist/esm/plugin.js +7 -0
  31. package/dist/esm/plugin.js.map +1 -1
  32. package/dist/esm/prerender.js +3 -3
  33. package/dist/esm/prerender.js.map +1 -1
  34. package/dist/esm/schema.d.ts +260 -0
  35. package/dist/esm/schema.js +35 -1
  36. package/dist/esm/schema.js.map +1 -1
  37. package/dist/esm/start-compiler-plugin/compiler.js +5 -1
  38. package/dist/esm/start-compiler-plugin/compiler.js.map +1 -1
  39. package/dist/esm/start-compiler-plugin/handleCreateServerFn.js +2 -2
  40. package/dist/esm/start-compiler-plugin/handleCreateServerFn.js.map +1 -1
  41. package/dist/esm/start-compiler-plugin/plugin.js.map +1 -1
  42. package/dist/esm/start-router-plugin/plugin.js +5 -5
  43. package/dist/esm/start-router-plugin/plugin.js.map +1 -1
  44. package/package.json +9 -6
  45. package/src/dev-server-plugin/plugin.ts +1 -1
  46. package/src/import-protection-plugin/defaults.ts +56 -0
  47. package/src/import-protection-plugin/matchers.ts +48 -0
  48. package/src/import-protection-plugin/plugin.ts +1173 -0
  49. package/src/import-protection-plugin/postCompileUsage.ts +266 -0
  50. package/src/import-protection-plugin/rewriteDeniedImports.ts +255 -0
  51. package/src/import-protection-plugin/sourceLocation.ts +524 -0
  52. package/src/import-protection-plugin/trace.ts +296 -0
  53. package/src/import-protection-plugin/utils.ts +32 -0
  54. package/src/import-protection-plugin/virtualModules.ts +300 -0
  55. package/src/plugin.ts +7 -0
  56. package/src/schema.ts +58 -0
  57. package/src/start-compiler-plugin/compiler.ts +12 -1
  58. package/src/start-compiler-plugin/plugin.ts +3 -3
@@ -0,0 +1,266 @@
1
+ import * as t from '@babel/types'
2
+ import { parseAst } from '@tanstack/router-utils'
3
+
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
+ }
112
+
113
+ /**
114
+ * Given transformed code, returns the first "meaningful" usage position for an
115
+ * import from `source` that survives compilation.
116
+ *
117
+ * The returned column is 0-based (Babel loc semantics).
118
+ */
119
+ export function findPostCompileUsagePos(
120
+ code: string,
121
+ source: string,
122
+ ): UsagePos | undefined {
123
+ const ast = parseAst({ code })
124
+
125
+ // 1) Determine local names bound from this specifier
126
+ const imported = new Set<string>()
127
+ for (const node of ast.program.body) {
128
+ if (t.isImportDeclaration(node) && node.source.value === source) {
129
+ if (node.importKind === 'type') continue
130
+ for (const s of node.specifiers) {
131
+ if (t.isImportSpecifier(s) && s.importKind === 'type') continue
132
+ imported.add(s.local.name)
133
+ }
134
+ }
135
+ }
136
+ if (imported.size === 0) return undefined
137
+
138
+ let preferred: UsagePos | undefined
139
+ let anyUsage: UsagePos | undefined
140
+
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
+ }
234
+
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')
239
+ } else {
240
+ record(astNode, 'any')
241
+ }
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
+ }
262
+ }
263
+
264
+ walk(ast.program, null)
265
+ return preferred ?? anyUsage
266
+ }
@@ -0,0 +1,255 @@
1
+ import * as t from '@babel/types'
2
+ import { generateFromAst, parseAst } from '@tanstack/router-utils'
3
+
4
+ import { MOCK_MODULE_ID } from './virtualModules'
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Export name collection (for dev mock-edge modules)
8
+ // ---------------------------------------------------------------------------
9
+
10
+ export function isValidExportName(name: string): boolean {
11
+ if (name === 'default') return false
12
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name)
13
+ }
14
+
15
+ /**
16
+ * Best-effort static analysis of an importer's source to determine which
17
+ * named exports are needed per specifier, to keep native ESM valid in dev.
18
+ */
19
+ export function collectMockExportNamesBySource(
20
+ code: string,
21
+ ): Map<string, Array<string>> {
22
+ const ast = parseAst({ code })
23
+
24
+ const namesBySource = new Map<string, Set<string>>()
25
+ 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)
33
+ }
34
+
35
+ for (const node of ast.program.body) {
36
+ if (t.isImportDeclaration(node)) {
37
+ if (node.importKind === 'type') continue
38
+ const source = node.source.value
39
+ for (const s of node.specifiers) {
40
+ if (!t.isImportSpecifier(s)) continue
41
+ if (s.importKind === 'type') continue
42
+ const importedName = t.isIdentifier(s.imported)
43
+ ? s.imported.name
44
+ : s.imported.value
45
+ // `import { default as x } from 'm'` only requires a default export.
46
+ if (importedName === 'default') continue
47
+ add(source, importedName)
48
+ }
49
+ }
50
+
51
+ if (t.isExportNamedDeclaration(node) && node.source?.value) {
52
+ if (node.exportKind === 'type') continue
53
+ const source = node.source.value
54
+ for (const s of node.specifiers) {
55
+ if (!t.isExportSpecifier(s)) continue
56
+ if (s.exportKind === 'type') continue
57
+ add(source, s.local.name)
58
+ }
59
+ }
60
+ }
61
+
62
+ const out = new Map<string, Array<string>>()
63
+ for (const [source, set] of namesBySource) {
64
+ out.set(source, Array.from(set).sort())
65
+ }
66
+ return out
67
+ }
68
+
69
+ // ---------------------------------------------------------------------------
70
+ // AST-based import rewriting
71
+ // ---------------------------------------------------------------------------
72
+
73
+ /**
74
+ * Rewrite static imports/re-exports from denied sources using Babel AST transforms.
75
+ *
76
+ * Transforms:
77
+ * import { a as b, c } from 'denied'
78
+ * Into:
79
+ * import __tss_deny_0 from 'tanstack-start-import-protection:mock'
80
+ * const b = __tss_deny_0.a
81
+ * const c = __tss_deny_0.c
82
+ *
83
+ * Also handles:
84
+ * import def from 'denied' -> import def from mock
85
+ * import * as ns from 'denied' -> import ns from mock
86
+ * export { x } from 'denied' -> export const x = mock.x
87
+ * export * from 'denied' -> removed
88
+ * export { x as y } from 'denied' -> export const y = mock.x
89
+ */
90
+ export function rewriteDeniedImports(
91
+ code: string,
92
+ id: string,
93
+ deniedSources: Set<string>,
94
+ getMockModuleId: (source: string) => string = () => MOCK_MODULE_ID,
95
+ ): { code: string; map?: object | null } | undefined {
96
+ const ast = parseAst({ code })
97
+ let modified = false
98
+ let mockCounter = 0
99
+
100
+ // Walk program body in reverse so splice indices stay valid
101
+ for (let i = ast.program.body.length - 1; i >= 0; i--) {
102
+ const node = ast.program.body[i]!
103
+
104
+ // --- import declarations ---
105
+ if (t.isImportDeclaration(node)) {
106
+ // Skip type-only imports
107
+ if (node.importKind === 'type') continue
108
+ if (!deniedSources.has(node.source.value)) continue
109
+
110
+ const mockVar = `__tss_deny_${mockCounter++}`
111
+ const replacements: Array<t.Statement> = []
112
+
113
+ // import __tss_deny_N from '<mock>'
114
+ replacements.push(
115
+ t.importDeclaration(
116
+ [t.importDefaultSpecifier(t.identifier(mockVar))],
117
+ t.stringLiteral(getMockModuleId(node.source.value)),
118
+ ),
119
+ )
120
+
121
+ 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
134
+ replacements.push(
135
+ t.variableDeclaration('const', [
136
+ t.variableDeclarator(
137
+ t.identifier(specifier.local.name),
138
+ t.identifier(mockVar),
139
+ ),
140
+ ]),
141
+ )
142
+ } else if (t.isImportSpecifier(specifier)) {
143
+ // Skip type-only specifiers
144
+ if (specifier.importKind === 'type') continue
145
+ // import { a as b } from 'denied' -> const b = __tss_deny_N.a
146
+ const importedName = t.isIdentifier(specifier.imported)
147
+ ? specifier.imported.name
148
+ : specifier.imported.value
149
+ replacements.push(
150
+ t.variableDeclaration('const', [
151
+ t.variableDeclarator(
152
+ t.identifier(specifier.local.name),
153
+ t.memberExpression(
154
+ t.identifier(mockVar),
155
+ t.identifier(importedName),
156
+ ),
157
+ ),
158
+ ]),
159
+ )
160
+ }
161
+ }
162
+
163
+ ast.program.body.splice(i, 1, ...replacements)
164
+ modified = true
165
+ continue
166
+ }
167
+
168
+ // --- export { x } from 'denied' ---
169
+ if (t.isExportNamedDeclaration(node) && node.source) {
170
+ if (node.exportKind === 'type') continue
171
+ if (!deniedSources.has(node.source.value)) continue
172
+
173
+ const mockVar = `__tss_deny_${mockCounter++}`
174
+ const replacements: Array<t.Statement> = []
175
+
176
+ // import __tss_deny_N from '<mock>'
177
+ replacements.push(
178
+ t.importDeclaration(
179
+ [t.importDefaultSpecifier(t.identifier(mockVar))],
180
+ t.stringLiteral(getMockModuleId(node.source.value)),
181
+ ),
182
+ )
183
+
184
+ // For each re-exported specifier, create an exported const
185
+ const exportSpecifiers: Array<{
186
+ localName: string
187
+ exportedName: string
188
+ }> = []
189
+ for (const specifier of node.specifiers) {
190
+ if (t.isExportSpecifier(specifier)) {
191
+ if (specifier.exportKind === 'type') continue
192
+ const localName = specifier.local.name
193
+ const exportedName = t.isIdentifier(specifier.exported)
194
+ ? specifier.exported.name
195
+ : specifier.exported.value
196
+
197
+ const internalVar = `__tss_reexport_${localName}`
198
+ // const __tss_reexport_x = __tss_deny_N.x
199
+ replacements.push(
200
+ t.variableDeclaration('const', [
201
+ t.variableDeclarator(
202
+ t.identifier(internalVar),
203
+ t.memberExpression(
204
+ t.identifier(mockVar),
205
+ t.identifier(localName),
206
+ ),
207
+ ),
208
+ ]),
209
+ )
210
+ exportSpecifiers.push({ localName: internalVar, exportedName })
211
+ }
212
+ }
213
+
214
+ // export { __tss_reexport_x as x, ... }
215
+ if (exportSpecifiers.length > 0) {
216
+ replacements.push(
217
+ t.exportNamedDeclaration(
218
+ null,
219
+ exportSpecifiers.map((s) =>
220
+ t.exportSpecifier(
221
+ t.identifier(s.localName),
222
+ t.identifier(s.exportedName),
223
+ ),
224
+ ),
225
+ ),
226
+ )
227
+ }
228
+
229
+ ast.program.body.splice(i, 1, ...replacements)
230
+ modified = true
231
+ continue
232
+ }
233
+
234
+ // --- export * from 'denied' ---
235
+ if (t.isExportAllDeclaration(node)) {
236
+ if (node.exportKind === 'type') continue
237
+ if (!deniedSources.has(node.source.value)) continue
238
+
239
+ // Remove the star re-export entirely
240
+ ast.program.body.splice(i, 1)
241
+ modified = true
242
+ continue
243
+ }
244
+ }
245
+
246
+ if (!modified) return undefined
247
+
248
+ const result = generateFromAst(ast, {
249
+ sourceMaps: true,
250
+ sourceFileName: id,
251
+ filename: id,
252
+ })
253
+
254
+ return { code: result.code, map: result.map }
255
+ }