@tanstack/start-plugin-core 1.163.2 → 1.163.4
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/esm/constants.d.ts +1 -0
- package/dist/esm/constants.js +2 -0
- package/dist/esm/constants.js.map +1 -1
- package/dist/esm/import-protection-plugin/ast.d.ts +3 -0
- package/dist/esm/import-protection-plugin/ast.js +8 -0
- package/dist/esm/import-protection-plugin/ast.js.map +1 -0
- package/dist/esm/import-protection-plugin/constants.d.ts +6 -0
- package/dist/esm/import-protection-plugin/constants.js +24 -0
- package/dist/esm/import-protection-plugin/constants.js.map +1 -0
- package/dist/esm/import-protection-plugin/extensionlessAbsoluteIdResolver.d.ts +22 -0
- package/dist/esm/import-protection-plugin/extensionlessAbsoluteIdResolver.js +95 -0
- package/dist/esm/import-protection-plugin/extensionlessAbsoluteIdResolver.js.map +1 -0
- package/dist/esm/import-protection-plugin/plugin.d.ts +2 -13
- package/dist/esm/import-protection-plugin/plugin.js +684 -299
- package/dist/esm/import-protection-plugin/plugin.js.map +1 -1
- package/dist/esm/import-protection-plugin/postCompileUsage.js +4 -2
- package/dist/esm/import-protection-plugin/postCompileUsage.js.map +1 -1
- package/dist/esm/import-protection-plugin/rewriteDeniedImports.d.ts +4 -5
- package/dist/esm/import-protection-plugin/rewriteDeniedImports.js +225 -3
- package/dist/esm/import-protection-plugin/rewriteDeniedImports.js.map +1 -1
- package/dist/esm/import-protection-plugin/sourceLocation.d.ts +4 -7
- package/dist/esm/import-protection-plugin/sourceLocation.js +18 -73
- package/dist/esm/import-protection-plugin/sourceLocation.js.map +1 -1
- package/dist/esm/import-protection-plugin/types.d.ts +94 -0
- package/dist/esm/import-protection-plugin/utils.d.ts +33 -1
- package/dist/esm/import-protection-plugin/utils.js +69 -3
- package/dist/esm/import-protection-plugin/utils.js.map +1 -1
- package/dist/esm/import-protection-plugin/virtualModules.d.ts +30 -2
- package/dist/esm/import-protection-plugin/virtualModules.js +66 -23
- package/dist/esm/import-protection-plugin/virtualModules.js.map +1 -1
- package/dist/esm/start-compiler-plugin/plugin.d.ts +2 -1
- package/dist/esm/start-compiler-plugin/plugin.js +1 -2
- package/dist/esm/start-compiler-plugin/plugin.js.map +1 -1
- package/package.json +6 -6
- package/src/constants.ts +2 -0
- package/src/import-protection-plugin/INTERNALS.md +462 -60
- package/src/import-protection-plugin/ast.ts +7 -0
- package/src/import-protection-plugin/constants.ts +25 -0
- package/src/import-protection-plugin/extensionlessAbsoluteIdResolver.ts +121 -0
- package/src/import-protection-plugin/plugin.ts +1080 -597
- package/src/import-protection-plugin/postCompileUsage.ts +8 -2
- package/src/import-protection-plugin/rewriteDeniedImports.ts +141 -9
- package/src/import-protection-plugin/sourceLocation.ts +19 -89
- package/src/import-protection-plugin/types.ts +103 -0
- package/src/import-protection-plugin/utils.ts +123 -4
- package/src/import-protection-plugin/virtualModules.ts +117 -31
- package/src/start-compiler-plugin/plugin.ts +7 -2
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import babel from '@babel/core'
|
|
2
2
|
import * as t from '@babel/types'
|
|
3
|
-
import {
|
|
3
|
+
import { parseImportProtectionAst } from './ast'
|
|
4
|
+
import type { ParsedAst } from './ast'
|
|
4
5
|
|
|
5
6
|
type UsagePos = { line: number; column0: number }
|
|
6
7
|
|
|
@@ -15,8 +16,13 @@ export function findPostCompileUsagePos(
|
|
|
15
16
|
code: string,
|
|
16
17
|
source: string,
|
|
17
18
|
): UsagePos | undefined {
|
|
18
|
-
|
|
19
|
+
return findPostCompileUsagePosFromAst(parseImportProtectionAst(code), source)
|
|
20
|
+
}
|
|
19
21
|
|
|
22
|
+
function findPostCompileUsagePosFromAst(
|
|
23
|
+
ast: ParsedAst,
|
|
24
|
+
source: string,
|
|
25
|
+
): UsagePos | undefined {
|
|
20
26
|
// Collect local names bound from this specifier
|
|
21
27
|
const imported = new Set<string>()
|
|
22
28
|
for (const node of ast.program.body) {
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import * as t from '@babel/types'
|
|
2
|
-
import { generateFromAst
|
|
2
|
+
import { generateFromAst } from '@tanstack/router-utils'
|
|
3
3
|
|
|
4
4
|
import { MOCK_MODULE_ID } from './virtualModules'
|
|
5
5
|
import { getOrCreate } from './utils'
|
|
6
|
+
import { parseImportProtectionAst } from './ast'
|
|
7
|
+
import type { SourceMapLike } from './sourceLocation'
|
|
8
|
+
import type { ParsedAst } from './ast'
|
|
6
9
|
|
|
7
10
|
export function isValidExportName(name: string): boolean {
|
|
8
11
|
if (name === 'default' || name.length === 0) return false
|
|
@@ -34,16 +37,17 @@ export function isValidExportName(name: string): boolean {
|
|
|
34
37
|
return true
|
|
35
38
|
}
|
|
36
39
|
|
|
37
|
-
/**
|
|
38
|
-
* Best-effort static analysis of an importer's source to determine which
|
|
39
|
-
* named exports are needed per specifier, to keep native ESM valid in dev.
|
|
40
|
-
*/
|
|
41
40
|
export function collectMockExportNamesBySource(
|
|
42
41
|
code: string,
|
|
43
42
|
): Map<string, Array<string>> {
|
|
44
|
-
|
|
43
|
+
return collectMockExportNamesBySourceFromAst(parseImportProtectionAst(code))
|
|
44
|
+
}
|
|
45
45
|
|
|
46
|
+
function collectMockExportNamesBySourceFromAst(
|
|
47
|
+
ast: ParsedAst,
|
|
48
|
+
): Map<string, Array<string>> {
|
|
46
49
|
const namesBySource = new Map<string, Set<string>>()
|
|
50
|
+
const memberBindingToSource = new Map<string, string>()
|
|
47
51
|
const add = (source: string, name: string) => {
|
|
48
52
|
if (name === 'default' || name.length === 0) return
|
|
49
53
|
getOrCreate(namesBySource, source, () => new Set<string>()).add(name)
|
|
@@ -54,6 +58,14 @@ export function collectMockExportNamesBySource(
|
|
|
54
58
|
if (node.importKind === 'type') continue
|
|
55
59
|
const source = node.source.value
|
|
56
60
|
for (const s of node.specifiers) {
|
|
61
|
+
if (t.isImportNamespaceSpecifier(s)) {
|
|
62
|
+
memberBindingToSource.set(s.local.name, source)
|
|
63
|
+
continue
|
|
64
|
+
}
|
|
65
|
+
if (t.isImportDefaultSpecifier(s)) {
|
|
66
|
+
memberBindingToSource.set(s.local.name, source)
|
|
67
|
+
continue
|
|
68
|
+
}
|
|
57
69
|
if (!t.isImportSpecifier(s)) continue
|
|
58
70
|
if (s.importKind === 'type') continue
|
|
59
71
|
const importedName = t.isIdentifier(s.imported)
|
|
@@ -76,6 +88,45 @@ export function collectMockExportNamesBySource(
|
|
|
76
88
|
}
|
|
77
89
|
}
|
|
78
90
|
|
|
91
|
+
// For namespace/default imports, collect property names used as
|
|
92
|
+
// `binding.foo`/`binding?.foo` so mock-edge modules can expose explicit ESM
|
|
93
|
+
// named exports required by Rolldown/native ESM.
|
|
94
|
+
if (memberBindingToSource.size > 0) {
|
|
95
|
+
const visit = (node: t.Node): void => {
|
|
96
|
+
if (t.isMemberExpression(node)) {
|
|
97
|
+
const object = node.object
|
|
98
|
+
if (t.isIdentifier(object)) {
|
|
99
|
+
const source = memberBindingToSource.get(object.name)
|
|
100
|
+
if (source) {
|
|
101
|
+
const property = node.property
|
|
102
|
+
if (!node.computed && t.isIdentifier(property)) {
|
|
103
|
+
add(source, property.name)
|
|
104
|
+
} else if (node.computed && t.isStringLiteral(property)) {
|
|
105
|
+
add(source, property.value)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const keys = t.VISITOR_KEYS[node.type]
|
|
112
|
+
if (!keys) return
|
|
113
|
+
for (const key of keys) {
|
|
114
|
+
const child = (node as unknown as Record<string, unknown>)[key]
|
|
115
|
+
if (Array.isArray(child)) {
|
|
116
|
+
for (const item of child) {
|
|
117
|
+
if (item && typeof item === 'object' && 'type' in item) {
|
|
118
|
+
visit(item as t.Node)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
} else if (child && typeof child === 'object' && 'type' in child) {
|
|
122
|
+
visit(child as t.Node)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
visit(ast.program)
|
|
128
|
+
}
|
|
129
|
+
|
|
79
130
|
const out = new Map<string, Array<string>>()
|
|
80
131
|
for (const [source, set] of namesBySource) {
|
|
81
132
|
out.set(source, Array.from(set).sort())
|
|
@@ -83,6 +134,71 @@ export function collectMockExportNamesBySource(
|
|
|
83
134
|
return out
|
|
84
135
|
}
|
|
85
136
|
|
|
137
|
+
/** Collect all valid named export identifiers from the given code. */
|
|
138
|
+
export function collectNamedExports(code: string): Array<string> {
|
|
139
|
+
return collectNamedExportsFromAst(parseImportProtectionAst(code))
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function collectIdentifiersFromPattern(
|
|
143
|
+
pattern: t.LVal,
|
|
144
|
+
add: (name: string) => void,
|
|
145
|
+
): void {
|
|
146
|
+
if (t.isIdentifier(pattern)) {
|
|
147
|
+
add(pattern.name)
|
|
148
|
+
} else if (t.isObjectPattern(pattern)) {
|
|
149
|
+
for (const prop of pattern.properties) {
|
|
150
|
+
if (t.isRestElement(prop)) {
|
|
151
|
+
collectIdentifiersFromPattern(prop.argument as t.LVal, add)
|
|
152
|
+
} else {
|
|
153
|
+
collectIdentifiersFromPattern(prop.value as t.LVal, add)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
} else if (t.isArrayPattern(pattern)) {
|
|
157
|
+
for (const elem of pattern.elements) {
|
|
158
|
+
if (elem) collectIdentifiersFromPattern(elem as t.LVal, add)
|
|
159
|
+
}
|
|
160
|
+
} else if (t.isAssignmentPattern(pattern)) {
|
|
161
|
+
collectIdentifiersFromPattern(pattern.left, add)
|
|
162
|
+
} else if (t.isRestElement(pattern)) {
|
|
163
|
+
collectIdentifiersFromPattern(pattern.argument as t.LVal, add)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function collectNamedExportsFromAst(ast: ParsedAst): Array<string> {
|
|
168
|
+
const names = new Set<string>()
|
|
169
|
+
const add = (name: string) => {
|
|
170
|
+
if (isValidExportName(name)) names.add(name)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
for (const node of ast.program.body) {
|
|
174
|
+
if (t.isExportNamedDeclaration(node)) {
|
|
175
|
+
if (node.exportKind === 'type') continue
|
|
176
|
+
|
|
177
|
+
if (node.declaration) {
|
|
178
|
+
const decl = node.declaration
|
|
179
|
+
if (t.isFunctionDeclaration(decl) || t.isClassDeclaration(decl)) {
|
|
180
|
+
if (decl.id?.name) add(decl.id.name)
|
|
181
|
+
} else if (t.isVariableDeclaration(decl)) {
|
|
182
|
+
for (const d of decl.declarations) {
|
|
183
|
+
collectIdentifiersFromPattern(d.id as t.LVal, add)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
for (const s of node.specifiers) {
|
|
189
|
+
if (!t.isExportSpecifier(s)) continue
|
|
190
|
+
if (s.exportKind === 'type') continue
|
|
191
|
+
const exportedName = t.isIdentifier(s.exported)
|
|
192
|
+
? s.exported.name
|
|
193
|
+
: s.exported.value
|
|
194
|
+
add(exportedName)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return Array.from(names).sort()
|
|
200
|
+
}
|
|
201
|
+
|
|
86
202
|
/**
|
|
87
203
|
* Rewrite static imports/re-exports from denied sources using Babel AST transforms.
|
|
88
204
|
*
|
|
@@ -105,8 +221,21 @@ export function rewriteDeniedImports(
|
|
|
105
221
|
id: string,
|
|
106
222
|
deniedSources: Set<string>,
|
|
107
223
|
getMockModuleId: (source: string) => string = () => MOCK_MODULE_ID,
|
|
108
|
-
): { code: string; map?:
|
|
109
|
-
|
|
224
|
+
): { code: string; map?: SourceMapLike } | undefined {
|
|
225
|
+
return rewriteDeniedImportsFromAst(
|
|
226
|
+
parseImportProtectionAst(code),
|
|
227
|
+
id,
|
|
228
|
+
deniedSources,
|
|
229
|
+
getMockModuleId,
|
|
230
|
+
)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function rewriteDeniedImportsFromAst(
|
|
234
|
+
ast: ParsedAst,
|
|
235
|
+
id: string,
|
|
236
|
+
deniedSources: Set<string>,
|
|
237
|
+
getMockModuleId: (source: string) => string = () => MOCK_MODULE_ID,
|
|
238
|
+
): { code: string; map?: SourceMapLike } | undefined {
|
|
110
239
|
let modified = false
|
|
111
240
|
let mockCounter = 0
|
|
112
241
|
|
|
@@ -243,5 +372,8 @@ export function rewriteDeniedImports(
|
|
|
243
372
|
filename: id,
|
|
244
373
|
})
|
|
245
374
|
|
|
246
|
-
return {
|
|
375
|
+
return {
|
|
376
|
+
code: result.code,
|
|
377
|
+
...(result.map ? { map: result.map as SourceMapLike } : {}),
|
|
378
|
+
}
|
|
247
379
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { SourceMapConsumer } from 'source-map'
|
|
2
2
|
import * as path from 'pathe'
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { findPostCompileUsagePos } from './postCompileUsage'
|
|
5
|
+
import { getOrCreate, normalizeFilePath } from './utils'
|
|
5
6
|
import type { Loc } from './trace'
|
|
6
7
|
import type { RawSourceMap } from 'source-map'
|
|
7
8
|
|
|
@@ -270,36 +271,7 @@ export class ImportLocCache {
|
|
|
270
271
|
}
|
|
271
272
|
}
|
|
272
273
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
const importPatternCache = new Map<string, Array<RegExp>>()
|
|
276
|
-
|
|
277
|
-
export function clearImportPatternCache(): void {
|
|
278
|
-
importPatternCache.clear()
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
function findFirstImportSpecifierIndex(code: string, source: string): number {
|
|
282
|
-
let patterns = importPatternCache.get(source)
|
|
283
|
-
if (!patterns) {
|
|
284
|
-
const escaped = escapeRegExp(source)
|
|
285
|
-
patterns = [
|
|
286
|
-
new RegExp(`\\bimport\\s+(['"])${escaped}\\1`),
|
|
287
|
-
new RegExp(`\\bfrom\\s+(['"])${escaped}\\1`),
|
|
288
|
-
new RegExp(`\\bimport\\s*\\(\\s*(['"])${escaped}\\1\\s*\\)`),
|
|
289
|
-
]
|
|
290
|
-
importPatternCache.set(source, patterns)
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
let best = -1
|
|
294
|
-
for (const re of patterns) {
|
|
295
|
-
const m = re.exec(code)
|
|
296
|
-
if (!m) continue
|
|
297
|
-
const idx = m.index + m[0].indexOf(source)
|
|
298
|
-
if (idx === -1) continue
|
|
299
|
-
if (best === -1 || idx < best) best = idx
|
|
300
|
-
}
|
|
301
|
-
return best
|
|
302
|
-
}
|
|
274
|
+
export type FindImportSpecifierIndex = (code: string, source: string) => number
|
|
303
275
|
|
|
304
276
|
/**
|
|
305
277
|
* Find the location of an import statement in a transformed module
|
|
@@ -311,6 +283,7 @@ export async function findImportStatementLocationFromTransformed(
|
|
|
311
283
|
importerId: string,
|
|
312
284
|
source: string,
|
|
313
285
|
importLocCache: ImportLocCache,
|
|
286
|
+
findImportSpecifierIndex: FindImportSpecifierIndex,
|
|
314
287
|
): Promise<Loc | undefined> {
|
|
315
288
|
const importerFile = normalizeFilePath(importerId)
|
|
316
289
|
const cacheKey = `${importerFile}::${source}`
|
|
@@ -329,7 +302,7 @@ export async function findImportStatementLocationFromTransformed(
|
|
|
329
302
|
|
|
330
303
|
const lineIndex = res.lineIndex ?? buildLineIndex(code)
|
|
331
304
|
|
|
332
|
-
const idx =
|
|
305
|
+
const idx = findImportSpecifierIndex(code, source)
|
|
333
306
|
if (idx === -1) {
|
|
334
307
|
importLocCache.set(cacheKey, null)
|
|
335
308
|
return undefined
|
|
@@ -354,10 +327,6 @@ export async function findPostCompileUsageLocation(
|
|
|
354
327
|
provider: TransformResultProvider,
|
|
355
328
|
importerId: string,
|
|
356
329
|
source: string,
|
|
357
|
-
findPostCompileUsagePos: (
|
|
358
|
-
code: string,
|
|
359
|
-
source: string,
|
|
360
|
-
) => { line: number; column0: number } | undefined,
|
|
361
330
|
): Promise<Loc | undefined> {
|
|
362
331
|
try {
|
|
363
332
|
const importerFile = normalizeFilePath(importerId)
|
|
@@ -391,6 +360,7 @@ export async function addTraceImportLocations(
|
|
|
391
360
|
column?: number
|
|
392
361
|
}>,
|
|
393
362
|
importLocCache: ImportLocCache,
|
|
363
|
+
findImportSpecifierIndex: FindImportSpecifierIndex,
|
|
394
364
|
): Promise<void> {
|
|
395
365
|
for (const step of trace) {
|
|
396
366
|
if (!step.specifier) continue
|
|
@@ -400,6 +370,7 @@ export async function addTraceImportLocations(
|
|
|
400
370
|
step.file,
|
|
401
371
|
step.specifier,
|
|
402
372
|
importLocCache,
|
|
373
|
+
findImportSpecifierIndex,
|
|
403
374
|
)
|
|
404
375
|
if (!loc) continue
|
|
405
376
|
step.line = loc.line
|
|
@@ -435,67 +406,26 @@ export function buildCodeSnippet(
|
|
|
435
406
|
const res = provider.getTransformResult(moduleId)
|
|
436
407
|
if (!res) return undefined
|
|
437
408
|
|
|
438
|
-
const
|
|
439
|
-
|
|
440
|
-
const sourceCode = originalCode ?? transformedCode
|
|
409
|
+
const sourceCode = res.originalCode ?? res.code
|
|
441
410
|
const targetLine = loc.line // 1-indexed
|
|
442
411
|
const targetCol = loc.column // 1-indexed
|
|
443
412
|
|
|
444
413
|
if (targetLine < 1) return undefined
|
|
445
414
|
|
|
446
|
-
const
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
let pos = 0
|
|
452
|
-
while (lineNum < wantStart && pos < sourceCode.length) {
|
|
453
|
-
const ch = sourceCode.charCodeAt(pos)
|
|
454
|
-
if (ch === 10) {
|
|
455
|
-
lineNum++
|
|
456
|
-
} else if (ch === 13) {
|
|
457
|
-
lineNum++
|
|
458
|
-
if (
|
|
459
|
-
pos + 1 < sourceCode.length &&
|
|
460
|
-
sourceCode.charCodeAt(pos + 1) === 10
|
|
461
|
-
)
|
|
462
|
-
pos++
|
|
463
|
-
}
|
|
464
|
-
pos++
|
|
465
|
-
}
|
|
466
|
-
if (lineNum < wantStart) return undefined
|
|
467
|
-
|
|
468
|
-
const lines: Array<string> = []
|
|
469
|
-
let curLine = wantStart
|
|
470
|
-
while (curLine <= wantEnd && pos <= sourceCode.length) {
|
|
471
|
-
// Find end of current line
|
|
472
|
-
let eol = pos
|
|
473
|
-
while (eol < sourceCode.length) {
|
|
474
|
-
const ch = sourceCode.charCodeAt(eol)
|
|
475
|
-
if (ch === 10 || ch === 13) break
|
|
476
|
-
eol++
|
|
477
|
-
}
|
|
478
|
-
lines.push(sourceCode.slice(pos, eol))
|
|
479
|
-
curLine++
|
|
480
|
-
if (eol < sourceCode.length) {
|
|
481
|
-
if (
|
|
482
|
-
sourceCode.charCodeAt(eol) === 13 &&
|
|
483
|
-
eol + 1 < sourceCode.length &&
|
|
484
|
-
sourceCode.charCodeAt(eol + 1) === 10
|
|
485
|
-
) {
|
|
486
|
-
pos = eol + 2
|
|
487
|
-
} else {
|
|
488
|
-
pos = eol + 1
|
|
489
|
-
}
|
|
490
|
-
} else {
|
|
491
|
-
pos = eol + 1
|
|
492
|
-
}
|
|
415
|
+
const allLines = sourceCode.split('\n')
|
|
416
|
+
// Strip trailing \r from \r\n line endings
|
|
417
|
+
for (let i = 0; i < allLines.length; i++) {
|
|
418
|
+
const line = allLines[i]!
|
|
419
|
+
if (line.endsWith('\r')) allLines[i] = line.slice(0, -1)
|
|
493
420
|
}
|
|
494
421
|
|
|
495
|
-
|
|
422
|
+
const wantStart = Math.max(1, targetLine - contextLines)
|
|
423
|
+
const wantEnd = Math.min(allLines.length, targetLine + contextLines)
|
|
424
|
+
|
|
425
|
+
if (targetLine > allLines.length) return undefined
|
|
496
426
|
|
|
497
|
-
const
|
|
498
|
-
const gutterWidth = String(
|
|
427
|
+
const lines = allLines.slice(wantStart - 1, wantEnd)
|
|
428
|
+
const gutterWidth = String(wantEnd).length
|
|
499
429
|
|
|
500
430
|
const sourceFile = loc.file ?? importerFile
|
|
501
431
|
const snippetLines: Array<string> = []
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { CompileStartFrameworkOptions, GetConfigFn } from '../types'
|
|
2
|
+
import type { ImportProtectionBehavior } from '../schema'
|
|
3
|
+
import type { CompiledMatcher } from './matchers'
|
|
4
|
+
import type { ImportGraph, ViolationInfo } from './trace'
|
|
5
|
+
import type {
|
|
6
|
+
ImportLocCache,
|
|
7
|
+
TransformResult,
|
|
8
|
+
TransformResultProvider,
|
|
9
|
+
} from './sourceLocation'
|
|
10
|
+
|
|
11
|
+
/** Compiled deny/exclude patterns for one environment (client or server). */
|
|
12
|
+
export interface EnvRules {
|
|
13
|
+
specifiers: Array<CompiledMatcher>
|
|
14
|
+
files: Array<CompiledMatcher>
|
|
15
|
+
excludeFiles: Array<CompiledMatcher>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface PluginConfig {
|
|
19
|
+
enabled: boolean
|
|
20
|
+
root: string
|
|
21
|
+
command: 'build' | 'serve'
|
|
22
|
+
srcDirectory: string
|
|
23
|
+
framework: CompileStartFrameworkOptions
|
|
24
|
+
effectiveBehavior: ImportProtectionBehavior
|
|
25
|
+
mockAccess: 'error' | 'warn' | 'off'
|
|
26
|
+
logMode: 'once' | 'always'
|
|
27
|
+
maxTraceDepth: number
|
|
28
|
+
compiledRules: {
|
|
29
|
+
client: EnvRules
|
|
30
|
+
server: EnvRules
|
|
31
|
+
}
|
|
32
|
+
includeMatchers: Array<CompiledMatcher>
|
|
33
|
+
excludeMatchers: Array<CompiledMatcher>
|
|
34
|
+
ignoreImporterMatchers: Array<CompiledMatcher>
|
|
35
|
+
markerSpecifiers: { serverOnly: Set<string>; clientOnly: Set<string> }
|
|
36
|
+
envTypeMap: Map<string, 'client' | 'server'>
|
|
37
|
+
onViolation?: (
|
|
38
|
+
info: ViolationInfo,
|
|
39
|
+
) => boolean | void | Promise<boolean | void>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface EnvState {
|
|
43
|
+
graph: ImportGraph
|
|
44
|
+
mockExportsByImporter: Map<string, Map<string, Array<string>>>
|
|
45
|
+
resolveCache: Map<string, string | null>
|
|
46
|
+
resolveCacheByFile: Map<string, Set<string>>
|
|
47
|
+
importLocCache: ImportLocCache
|
|
48
|
+
seenViolations: Set<string>
|
|
49
|
+
serverFnLookupModules: Set<string>
|
|
50
|
+
transformResultCache: Map<string, TransformResult>
|
|
51
|
+
transformResultKeysByFile: Map<string, Set<string>>
|
|
52
|
+
transformResultProvider: TransformResultProvider
|
|
53
|
+
postTransformImports: Map<string, Set<string>>
|
|
54
|
+
pendingViolations: Map<string, Array<PendingViolation>>
|
|
55
|
+
deferredBuildViolations: Array<DeferredBuildViolation>
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface PendingViolation {
|
|
59
|
+
info: ViolationInfo
|
|
60
|
+
/** True when the violation originates from a pre-transform resolveId call
|
|
61
|
+
* (e.g. server-fn lookup). These need edge-survival verification because
|
|
62
|
+
* the Start compiler may strip the import later. */
|
|
63
|
+
fromPreTransformResolve?: boolean
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface DeferredBuildViolation {
|
|
67
|
+
info: ViolationInfo
|
|
68
|
+
mockModuleId: string
|
|
69
|
+
checkModuleId?: string
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface SharedState {
|
|
73
|
+
fileMarkerKind: Map<string, 'server' | 'client'>
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface ImportProtectionPluginOptions {
|
|
77
|
+
getConfig: GetConfigFn
|
|
78
|
+
framework: CompileStartFrameworkOptions
|
|
79
|
+
environments: Array<{ name: string; type: 'client' | 'server' }>
|
|
80
|
+
providerEnvName: string
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export type ModuleGraphNode = {
|
|
84
|
+
id?: string | null
|
|
85
|
+
url?: string
|
|
86
|
+
importers: Set<ModuleGraphNode>
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export type ViolationReporter = {
|
|
90
|
+
warn: (msg: string) => void
|
|
91
|
+
error: (msg: string) => never
|
|
92
|
+
resolve?: (
|
|
93
|
+
source: string,
|
|
94
|
+
importer?: string,
|
|
95
|
+
options?: {
|
|
96
|
+
skipSelf?: boolean
|
|
97
|
+
custom?: Record<string, unknown>
|
|
98
|
+
},
|
|
99
|
+
) => Promise<{ id: string; external?: boolean | 'absolute' } | null>
|
|
100
|
+
getModuleInfo?: (id: string) => { code?: string | null } | null
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export type HandleViolationResult = string | undefined
|
|
@@ -1,5 +1,17 @@
|
|
|
1
|
+
import {
|
|
2
|
+
extname,
|
|
3
|
+
isAbsolute,
|
|
4
|
+
relative,
|
|
5
|
+
resolve as resolvePath,
|
|
6
|
+
} from 'node:path'
|
|
1
7
|
import { normalizePath } from 'vite'
|
|
2
8
|
|
|
9
|
+
import {
|
|
10
|
+
IMPORT_PROTECTION_DEBUG,
|
|
11
|
+
IMPORT_PROTECTION_DEBUG_FILTER,
|
|
12
|
+
KNOWN_SOURCE_EXTENSIONS,
|
|
13
|
+
} from './constants'
|
|
14
|
+
|
|
3
15
|
export type Pattern = string | RegExp
|
|
4
16
|
|
|
5
17
|
export function dedupePatterns(patterns: Array<Pattern>): Array<Pattern> {
|
|
@@ -14,7 +26,8 @@ export function dedupePatterns(patterns: Array<Pattern>): Array<Pattern> {
|
|
|
14
26
|
return out
|
|
15
27
|
}
|
|
16
28
|
|
|
17
|
-
|
|
29
|
+
/** Strip both `?query` and `#hash` from a module ID. */
|
|
30
|
+
export function stripQueryAndHash(id: string): string {
|
|
18
31
|
const q = id.indexOf('?')
|
|
19
32
|
const h = id.indexOf('#')
|
|
20
33
|
if (q === -1 && h === -1) return id
|
|
@@ -24,8 +37,7 @@ export function stripViteQuery(id: string): string {
|
|
|
24
37
|
}
|
|
25
38
|
|
|
26
39
|
/**
|
|
27
|
-
* Strip Vite query parameters and normalize the path in one step.
|
|
28
|
-
* Replaces the repeated `normalizePath(stripViteQuery(id))` pattern.
|
|
40
|
+
* Strip Vite query/hash parameters and normalize the path in one step.
|
|
29
41
|
*
|
|
30
42
|
* Results are memoized because the same module IDs are processed many
|
|
31
43
|
* times across resolveId, transform, and trace-building hooks.
|
|
@@ -34,7 +46,7 @@ const normalizeFilePathCache = new Map<string, string>()
|
|
|
34
46
|
export function normalizeFilePath(id: string): string {
|
|
35
47
|
let result = normalizeFilePathCache.get(id)
|
|
36
48
|
if (result === undefined) {
|
|
37
|
-
result = normalizePath(
|
|
49
|
+
result = normalizePath(stripQueryAndHash(id))
|
|
38
50
|
normalizeFilePathCache.set(id, result)
|
|
39
51
|
}
|
|
40
52
|
return result
|
|
@@ -91,3 +103,110 @@ export function extractImportSources(code: string): Array<string> {
|
|
|
91
103
|
}
|
|
92
104
|
return sources
|
|
93
105
|
}
|
|
106
|
+
|
|
107
|
+
/** Log import-protection debug output when debug mode is enabled. */
|
|
108
|
+
export function debugLog(...args: Array<unknown>): void {
|
|
109
|
+
if (!IMPORT_PROTECTION_DEBUG) return
|
|
110
|
+
console.warn('[import-protection:debug]', ...args)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Check if any value matches the configured debug filter (if present). */
|
|
114
|
+
export function matchesDebugFilter(...values: Array<string>): boolean {
|
|
115
|
+
const debugFilter = IMPORT_PROTECTION_DEBUG_FILTER
|
|
116
|
+
if (!debugFilter) return true
|
|
117
|
+
return values.some((v) => v.includes(debugFilter))
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Strip `?query` (but not `#hash`) from a module ID. */
|
|
121
|
+
export function stripQuery(id: string): string {
|
|
122
|
+
const queryIndex = id.indexOf('?')
|
|
123
|
+
return queryIndex === -1 ? id : id.slice(0, queryIndex)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function withoutKnownExtension(id: string): string {
|
|
127
|
+
const ext = extname(id)
|
|
128
|
+
return KNOWN_SOURCE_EXTENSIONS.has(ext) ? id.slice(0, -ext.length) : id
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Check whether `filePath` is contained inside `directory` using a
|
|
133
|
+
* boundary-safe comparison. A naïve `filePath.startsWith(directory)`
|
|
134
|
+
* would incorrectly treat `/app/src2/foo.ts` as inside `/app/src`.
|
|
135
|
+
*/
|
|
136
|
+
export function isInsideDirectory(
|
|
137
|
+
filePath: string,
|
|
138
|
+
directory: string,
|
|
139
|
+
): boolean {
|
|
140
|
+
const rel = relative(resolvePath(directory), resolvePath(filePath))
|
|
141
|
+
return rel.length > 0 && !rel.startsWith('..') && !isAbsolute(rel)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Decide whether a violation should be deferred for later verification
|
|
146
|
+
* rather than reported immediately.
|
|
147
|
+
*
|
|
148
|
+
* Build mode: always defer — generateBundle checks tree-shaking.
|
|
149
|
+
* Dev mock mode: always defer — edge-survival verifies whether the Start
|
|
150
|
+
* compiler strips the import (factory-safe pattern). All violation
|
|
151
|
+
* types and specifier formats are handled uniformly by the
|
|
152
|
+
* edge-survival mechanism in processPendingViolations.
|
|
153
|
+
* Dev error mode: never defer — throw immediately (no mock fallback).
|
|
154
|
+
*/
|
|
155
|
+
export function shouldDeferViolation(opts: {
|
|
156
|
+
isBuild: boolean
|
|
157
|
+
isDevMock: boolean
|
|
158
|
+
}): boolean {
|
|
159
|
+
return opts.isBuild || opts.isDevMock
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function buildSourceCandidates(
|
|
163
|
+
source: string,
|
|
164
|
+
resolved: string | undefined,
|
|
165
|
+
root: string,
|
|
166
|
+
): Set<string> {
|
|
167
|
+
const candidates = new Set<string>()
|
|
168
|
+
const push = (value: string | undefined) => {
|
|
169
|
+
if (!value) return
|
|
170
|
+
candidates.add(value)
|
|
171
|
+
candidates.add(stripQuery(value))
|
|
172
|
+
candidates.add(withoutKnownExtension(stripQuery(value)))
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
push(source)
|
|
176
|
+
if (resolved) {
|
|
177
|
+
push(resolved)
|
|
178
|
+
const relativeResolved = relativizePath(resolved, root)
|
|
179
|
+
push(relativeResolved)
|
|
180
|
+
push(`./${relativeResolved}`)
|
|
181
|
+
push(`/${relativeResolved}`)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return candidates
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function buildResolutionCandidates(id: string): Array<string> {
|
|
188
|
+
const normalized = normalizeFilePath(id)
|
|
189
|
+
const stripped = stripQuery(normalized)
|
|
190
|
+
|
|
191
|
+
return [...new Set([id, normalized, stripped])]
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function canonicalizeResolvedId(
|
|
195
|
+
id: string,
|
|
196
|
+
root: string,
|
|
197
|
+
resolveExtensionlessAbsoluteId: (value: string) => string,
|
|
198
|
+
): string {
|
|
199
|
+
const stripped = stripQuery(id)
|
|
200
|
+
let normalized = normalizeFilePath(stripped)
|
|
201
|
+
|
|
202
|
+
if (
|
|
203
|
+
!isAbsolute(normalized) &&
|
|
204
|
+
!normalized.startsWith('.') &&
|
|
205
|
+
!normalized.startsWith('\0') &&
|
|
206
|
+
!/^[a-zA-Z]+:/.test(normalized)
|
|
207
|
+
) {
|
|
208
|
+
normalized = normalizeFilePath(resolvePath(root, normalized))
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return resolveExtensionlessAbsoluteId(normalized)
|
|
212
|
+
}
|