@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.
- package/dist/esm/import-protection-plugin/defaults.d.ts +6 -4
- package/dist/esm/import-protection-plugin/defaults.js +3 -12
- package/dist/esm/import-protection-plugin/defaults.js.map +1 -1
- package/dist/esm/import-protection-plugin/plugin.d.ts +1 -1
- package/dist/esm/import-protection-plugin/plugin.js +488 -257
- package/dist/esm/import-protection-plugin/plugin.js.map +1 -1
- package/dist/esm/import-protection-plugin/postCompileUsage.d.ts +4 -2
- package/dist/esm/import-protection-plugin/postCompileUsage.js +31 -150
- package/dist/esm/import-protection-plugin/postCompileUsage.js.map +1 -1
- package/dist/esm/import-protection-plugin/rewriteDeniedImports.js +13 -9
- package/dist/esm/import-protection-plugin/rewriteDeniedImports.js.map +1 -1
- package/dist/esm/import-protection-plugin/sourceLocation.d.ts +32 -66
- package/dist/esm/import-protection-plugin/sourceLocation.js +129 -56
- package/dist/esm/import-protection-plugin/sourceLocation.js.map +1 -1
- package/dist/esm/import-protection-plugin/trace.d.ts +10 -0
- package/dist/esm/import-protection-plugin/trace.js +30 -44
- package/dist/esm/import-protection-plugin/trace.js.map +1 -1
- package/dist/esm/import-protection-plugin/utils.d.ts +8 -4
- package/dist/esm/import-protection-plugin/utils.js +43 -1
- package/dist/esm/import-protection-plugin/utils.js.map +1 -1
- package/dist/esm/import-protection-plugin/virtualModules.d.ts +7 -1
- package/dist/esm/import-protection-plugin/virtualModules.js +104 -135
- package/dist/esm/import-protection-plugin/virtualModules.js.map +1 -1
- package/package.json +8 -8
- package/src/import-protection-plugin/defaults.ts +8 -19
- package/src/import-protection-plugin/plugin.ts +776 -433
- package/src/import-protection-plugin/postCompileUsage.ts +57 -229
- package/src/import-protection-plugin/rewriteDeniedImports.ts +34 -42
- package/src/import-protection-plugin/sourceLocation.ts +184 -185
- package/src/import-protection-plugin/trace.ts +38 -49
- package/src/import-protection-plugin/utils.ts +62 -1
- package/src/import-protection-plugin/virtualModules.ts +163 -177
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rewriteDeniedImports.js","sources":["../../../src/import-protection-plugin/rewriteDeniedImports.ts"],"sourcesContent":["import * as t from '@babel/types'\nimport { generateFromAst, parseAst } from '@tanstack/router-utils'\n\nimport { MOCK_MODULE_ID } from './virtualModules'\
|
|
1
|
+
{"version":3,"file":"rewriteDeniedImports.js","sources":["../../../src/import-protection-plugin/rewriteDeniedImports.ts"],"sourcesContent":["import * as t from '@babel/types'\nimport { generateFromAst, parseAst } from '@tanstack/router-utils'\n\nimport { MOCK_MODULE_ID } from './virtualModules'\nimport { getOrCreate } from './utils'\n\nexport function isValidExportName(name: string): boolean {\n if (name === 'default' || name.length === 0) return false\n const first = name.charCodeAt(0)\n // First char: A-Z (65-90), a-z (97-122), _ (95), $ (36)\n if (\n !(\n (first >= 65 && first <= 90) ||\n (first >= 97 && first <= 122) ||\n first === 95 ||\n first === 36\n )\n )\n return false\n for (let i = 1; i < name.length; i++) {\n const ch = name.charCodeAt(i)\n // Subsequent: A-Z, a-z, 0-9 (48-57), _, $\n if (\n !(\n (ch >= 65 && ch <= 90) ||\n (ch >= 97 && ch <= 122) ||\n (ch >= 48 && ch <= 57) ||\n ch === 95 ||\n ch === 36\n )\n )\n return false\n }\n return true\n}\n\n/**\n * Best-effort static analysis of an importer's source to determine which\n * named exports are needed per specifier, to keep native ESM valid in dev.\n */\nexport function collectMockExportNamesBySource(\n code: string,\n): Map<string, Array<string>> {\n const ast = parseAst({ code })\n\n const namesBySource = new Map<string, Set<string>>()\n const add = (source: string, name: string) => {\n if (name === 'default' || name.length === 0) return\n getOrCreate(namesBySource, source, () => new Set<string>()).add(name)\n }\n\n for (const node of ast.program.body) {\n if (t.isImportDeclaration(node)) {\n if (node.importKind === 'type') continue\n const source = node.source.value\n for (const s of node.specifiers) {\n if (!t.isImportSpecifier(s)) continue\n if (s.importKind === 'type') continue\n const importedName = t.isIdentifier(s.imported)\n ? s.imported.name\n : s.imported.value\n // `import { default as x } from 'm'` only requires a default export.\n if (importedName === 'default') continue\n add(source, importedName)\n }\n }\n\n if (t.isExportNamedDeclaration(node) && node.source?.value) {\n if (node.exportKind === 'type') continue\n const source = node.source.value\n for (const s of node.specifiers) {\n if (!t.isExportSpecifier(s)) continue\n if (s.exportKind === 'type') continue\n add(source, s.local.name)\n }\n }\n }\n\n const out = new Map<string, Array<string>>()\n for (const [source, set] of namesBySource) {\n out.set(source, Array.from(set).sort())\n }\n return out\n}\n\n/**\n * Rewrite static imports/re-exports from denied sources using Babel AST transforms.\n *\n * Transforms:\n * import { a as b, c } from 'denied'\n * Into:\n * import __tss_deny_0 from 'tanstack-start-import-protection:mock'\n * const b = __tss_deny_0.a\n * const c = __tss_deny_0.c\n *\n * Also handles:\n * import def from 'denied' -> import def from mock\n * import * as ns from 'denied' -> import ns from mock\n * export { x } from 'denied' -> export const x = mock.x\n * export * from 'denied' -> removed\n * export { x as y } from 'denied' -> export const y = mock.x\n */\nexport function rewriteDeniedImports(\n code: string,\n id: string,\n deniedSources: Set<string>,\n getMockModuleId: (source: string) => string = () => MOCK_MODULE_ID,\n): { code: string; map?: object | null } | undefined {\n const ast = parseAst({ code })\n let modified = false\n let mockCounter = 0\n\n // Walk program body in reverse so splice indices stay valid\n for (let i = ast.program.body.length - 1; i >= 0; i--) {\n const node = ast.program.body[i]!\n\n if (t.isImportDeclaration(node)) {\n if (node.importKind === 'type') continue\n if (!deniedSources.has(node.source.value)) continue\n\n const mockVar = `__tss_deny_${mockCounter++}`\n const replacements: Array<t.Statement> = []\n\n replacements.push(\n t.importDeclaration(\n [t.importDefaultSpecifier(t.identifier(mockVar))],\n t.stringLiteral(getMockModuleId(node.source.value)),\n ),\n )\n\n for (const specifier of node.specifiers) {\n if (\n t.isImportDefaultSpecifier(specifier) ||\n t.isImportNamespaceSpecifier(specifier)\n ) {\n replacements.push(\n t.variableDeclaration('const', [\n t.variableDeclarator(\n t.identifier(specifier.local.name),\n t.identifier(mockVar),\n ),\n ]),\n )\n } else if (t.isImportSpecifier(specifier)) {\n if (specifier.importKind === 'type') continue\n const importedName = t.isIdentifier(specifier.imported)\n ? specifier.imported.name\n : specifier.imported.value\n replacements.push(\n t.variableDeclaration('const', [\n t.variableDeclarator(\n t.identifier(specifier.local.name),\n t.memberExpression(\n t.identifier(mockVar),\n t.identifier(importedName),\n ),\n ),\n ]),\n )\n }\n }\n\n ast.program.body.splice(i, 1, ...replacements)\n modified = true\n continue\n }\n\n if (t.isExportNamedDeclaration(node) && node.source) {\n if (node.exportKind === 'type') continue\n if (!deniedSources.has(node.source.value)) continue\n\n const mockVar = `__tss_deny_${mockCounter++}`\n const replacements: Array<t.Statement> = []\n\n replacements.push(\n t.importDeclaration(\n [t.importDefaultSpecifier(t.identifier(mockVar))],\n t.stringLiteral(getMockModuleId(node.source.value)),\n ),\n )\n const exportSpecifiers: Array<{\n localName: string\n exportedName: string\n }> = []\n for (const specifier of node.specifiers) {\n if (t.isExportSpecifier(specifier)) {\n if (specifier.exportKind === 'type') continue\n const localName = specifier.local.name\n const exportedName = t.isIdentifier(specifier.exported)\n ? specifier.exported.name\n : specifier.exported.value\n\n const internalVar = `__tss_reexport_${localName}`\n replacements.push(\n t.variableDeclaration('const', [\n t.variableDeclarator(\n t.identifier(internalVar),\n t.memberExpression(\n t.identifier(mockVar),\n t.identifier(localName),\n ),\n ),\n ]),\n )\n exportSpecifiers.push({ localName: internalVar, exportedName })\n }\n }\n\n if (exportSpecifiers.length > 0) {\n replacements.push(\n t.exportNamedDeclaration(\n null,\n exportSpecifiers.map((s) =>\n t.exportSpecifier(\n t.identifier(s.localName),\n t.identifier(s.exportedName),\n ),\n ),\n ),\n )\n }\n\n ast.program.body.splice(i, 1, ...replacements)\n modified = true\n continue\n }\n\n if (t.isExportAllDeclaration(node)) {\n if (node.exportKind === 'type') continue\n if (!deniedSources.has(node.source.value)) continue\n\n ast.program.body.splice(i, 1)\n modified = true\n continue\n }\n }\n\n if (!modified) return undefined\n\n const result = generateFromAst(ast, {\n sourceMaps: true,\n sourceFileName: id,\n filename: id,\n })\n\n return { code: result.code, map: result.map }\n}\n"],"names":[],"mappings":";;;AAMO,SAAS,kBAAkB,MAAuB;AACvD,MAAI,SAAS,aAAa,KAAK,WAAW,EAAG,QAAO;AACpD,QAAM,QAAQ,KAAK,WAAW,CAAC;AAE/B,MACE,EACG,SAAS,MAAM,SAAS,MACxB,SAAS,MAAM,SAAS,OACzB,UAAU,MACV,UAAU;AAGZ,WAAO;AACT,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,UAAM,KAAK,KAAK,WAAW,CAAC;AAE5B,QACE,EACG,MAAM,MAAM,MAAM,MAClB,MAAM,MAAM,MAAM,OAClB,MAAM,MAAM,MAAM,MACnB,OAAO,MACP,OAAO;AAGT,aAAO;AAAA,EACX;AACA,SAAO;AACT;AAMO,SAAS,+BACd,MAC4B;AAC5B,QAAM,MAAM,SAAS,EAAE,MAAM;AAE7B,QAAM,oCAAoB,IAAA;AAC1B,QAAM,MAAM,CAAC,QAAgB,SAAiB;AAC5C,QAAI,SAAS,aAAa,KAAK,WAAW,EAAG;AAC7C,gBAAY,eAAe,QAAQ,MAAM,oBAAI,KAAa,EAAE,IAAI,IAAI;AAAA,EACtE;AAEA,aAAW,QAAQ,IAAI,QAAQ,MAAM;AACnC,QAAI,EAAE,oBAAoB,IAAI,GAAG;AAC/B,UAAI,KAAK,eAAe,OAAQ;AAChC,YAAM,SAAS,KAAK,OAAO;AAC3B,iBAAW,KAAK,KAAK,YAAY;AAC/B,YAAI,CAAC,EAAE,kBAAkB,CAAC,EAAG;AAC7B,YAAI,EAAE,eAAe,OAAQ;AAC7B,cAAM,eAAe,EAAE,aAAa,EAAE,QAAQ,IAC1C,EAAE,SAAS,OACX,EAAE,SAAS;AAEf,YAAI,iBAAiB,UAAW;AAChC,YAAI,QAAQ,YAAY;AAAA,MAC1B;AAAA,IACF;AAEA,QAAI,EAAE,yBAAyB,IAAI,KAAK,KAAK,QAAQ,OAAO;AAC1D,UAAI,KAAK,eAAe,OAAQ;AAChC,YAAM,SAAS,KAAK,OAAO;AAC3B,iBAAW,KAAK,KAAK,YAAY;AAC/B,YAAI,CAAC,EAAE,kBAAkB,CAAC,EAAG;AAC7B,YAAI,EAAE,eAAe,OAAQ;AAC7B,YAAI,QAAQ,EAAE,MAAM,IAAI;AAAA,MAC1B;AAAA,IACF;AAAA,EACF;AAEA,QAAM,0BAAU,IAAA;AAChB,aAAW,CAAC,QAAQ,GAAG,KAAK,eAAe;AACzC,QAAI,IAAI,QAAQ,MAAM,KAAK,GAAG,EAAE,MAAM;AAAA,EACxC;AACA,SAAO;AACT;"}
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import { Loc } from './trace.js';
|
|
2
2
|
/**
|
|
3
3
|
* Minimal source-map shape used throughout the import-protection plugin.
|
|
4
|
-
*
|
|
5
|
-
* Structurally compatible with both Rollup's `SourceMap` (version: number)
|
|
6
|
-
* and the `source-map` package's `RawSourceMap` (version: string).
|
|
7
4
|
*/
|
|
8
5
|
export interface SourceMapLike {
|
|
9
6
|
file?: string;
|
|
@@ -14,17 +11,6 @@ export interface SourceMapLike {
|
|
|
14
11
|
sourcesContent?: Array<string | null>;
|
|
15
12
|
mappings: string;
|
|
16
13
|
}
|
|
17
|
-
/**
|
|
18
|
-
* A cached transform result for a single module.
|
|
19
|
-
*
|
|
20
|
-
* - `code` – fully-transformed source (after all plugins).
|
|
21
|
-
* - `map` – composed sourcemap (chains back to the original file).
|
|
22
|
-
* - `originalCode` – the untransformed source, extracted from the
|
|
23
|
-
* sourcemap's `sourcesContent[0]` during the transform
|
|
24
|
-
* hook. Used by {@link buildCodeSnippet} so we never
|
|
25
|
-
* have to re-derive it via a flaky `sourceContentFor`
|
|
26
|
-
* lookup at display time.
|
|
27
|
-
*/
|
|
28
14
|
export interface TransformResult {
|
|
29
15
|
code: string;
|
|
30
16
|
map: SourceMapLike | undefined;
|
|
@@ -35,21 +21,8 @@ export interface TransformResult {
|
|
|
35
21
|
/**
|
|
36
22
|
* Provides the transformed code and composed sourcemap for a module.
|
|
37
23
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* Rollup's `ModuleInfo` has `.code` but not `.map`.
|
|
41
|
-
*
|
|
42
|
-
* Instead, we populate this cache from a late-running transform hook that
|
|
43
|
-
* stores `{ code, map, originalCode }` for every module as it passes through
|
|
44
|
-
* the pipeline. By the time `resolveId` fires for an import, the importer
|
|
45
|
-
* has already been fully transformed, so the cache always has the data we
|
|
46
|
-
* need.
|
|
47
|
-
*
|
|
48
|
-
* The `id` parameter is the **raw** module ID (may include Vite query
|
|
49
|
-
* parameters like `?tsr-split=component`). Implementations should look up
|
|
50
|
-
* with the full ID first, then fall back to the query-stripped path so that
|
|
51
|
-
* virtual-module variants are resolved correctly without losing the base-file
|
|
52
|
-
* fallback.
|
|
24
|
+
* Populated from a late-running transform hook. By the time `resolveId`
|
|
25
|
+
* fires for an import, the importer has already been fully transformed.
|
|
53
26
|
*/
|
|
54
27
|
export interface TransformResultProvider {
|
|
55
28
|
getTransformResult: (id: string) => TransformResult | undefined;
|
|
@@ -59,37 +32,40 @@ export type LineIndex = {
|
|
|
59
32
|
};
|
|
60
33
|
export declare function buildLineIndex(code: string): LineIndex;
|
|
61
34
|
/**
|
|
62
|
-
* Pick the most-likely original source text for `importerFile
|
|
63
|
-
*
|
|
64
|
-
* Sourcemaps can contain multiple sources (composed maps), so `sourcesContent[0]`
|
|
65
|
-
* is not guaranteed to represent the importer.
|
|
35
|
+
* Pick the most-likely original source text for `importerFile` from
|
|
36
|
+
* a sourcemap that may contain multiple sources.
|
|
66
37
|
*/
|
|
67
38
|
export declare function pickOriginalCodeFromSourcesContent(map: SourceMapLike | undefined, importerFile: string, root: string): string | undefined;
|
|
68
|
-
export
|
|
39
|
+
export type ImportLocEntry = {
|
|
40
|
+
file?: string;
|
|
69
41
|
line: number;
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
export declare function findFirstImportSpecifierIndex(code: string, source: string): number;
|
|
42
|
+
column: number;
|
|
43
|
+
};
|
|
73
44
|
/**
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
45
|
+
* Cache for import statement locations with reverse index for O(1)
|
|
46
|
+
* invalidation by file. Keys: `${importerFile}::${source}`.
|
|
47
|
+
*/
|
|
48
|
+
export declare class ImportLocCache {
|
|
49
|
+
private cache;
|
|
50
|
+
private reverseIndex;
|
|
51
|
+
has(key: string): boolean;
|
|
52
|
+
get(key: string): ImportLocEntry | null | undefined;
|
|
53
|
+
set(key: string, value: ImportLocEntry | null): void;
|
|
54
|
+
clear(): void;
|
|
55
|
+
/** Remove all cache entries where the importer matches `file`. */
|
|
56
|
+
deleteByFile(file: string): void;
|
|
57
|
+
}
|
|
58
|
+
export declare function clearImportPatternCache(): void;
|
|
59
|
+
/**
|
|
60
|
+
* Find the location of an import statement in a transformed module
|
|
61
|
+
* by searching the post-transform code and mapping back via sourcemap.
|
|
80
62
|
* Results are cached in `importLocCache`.
|
|
81
63
|
*/
|
|
82
|
-
export declare function findImportStatementLocationFromTransformed(provider: TransformResultProvider, importerId: string, source: string, importLocCache:
|
|
83
|
-
file?: string;
|
|
84
|
-
line: number;
|
|
85
|
-
column: number;
|
|
86
|
-
} | null>): Promise<Loc | undefined>;
|
|
64
|
+
export declare function findImportStatementLocationFromTransformed(provider: TransformResultProvider, importerId: string, source: string, importLocCache: ImportLocCache): Promise<Loc | undefined>;
|
|
87
65
|
/**
|
|
88
66
|
* Find the first post-compile usage location for a denied import specifier.
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
* {@link TransformResultProvider}, finds the first non-import usage of
|
|
92
|
-
* an imported binding, and maps back to original source via sourcemap.
|
|
67
|
+
* Best-effort: searches transformed code for non-import uses of imported
|
|
68
|
+
* bindings and maps back to original source via sourcemap.
|
|
93
69
|
*/
|
|
94
70
|
export declare function findPostCompileUsageLocation(provider: TransformResultProvider, importerId: string, source: string, findPostCompileUsagePos: (code: string, source: string) => {
|
|
95
71
|
line: number;
|
|
@@ -104,11 +80,7 @@ export declare function addTraceImportLocations(provider: TransformResultProvide
|
|
|
104
80
|
specifier?: string;
|
|
105
81
|
line?: number;
|
|
106
82
|
column?: number;
|
|
107
|
-
}>, importLocCache:
|
|
108
|
-
file?: string;
|
|
109
|
-
line: number;
|
|
110
|
-
column: number;
|
|
111
|
-
} | null>): Promise<void>;
|
|
83
|
+
}>, importLocCache: ImportLocCache): Promise<void>;
|
|
112
84
|
export interface CodeSnippet {
|
|
113
85
|
/** Source lines with line numbers, e.g. `[" 6 | import { getSecret } from './secret.server'", ...]` */
|
|
114
86
|
lines: Array<string>;
|
|
@@ -118,15 +90,9 @@ export interface CodeSnippet {
|
|
|
118
90
|
location: string;
|
|
119
91
|
}
|
|
120
92
|
/**
|
|
121
|
-
* Build a vitest-style code snippet showing
|
|
122
|
-
*
|
|
123
|
-
* Uses the `originalCode` stored in the transform result cache (extracted from
|
|
124
|
-
* `sourcesContent[0]` of the composed sourcemap at transform time). This is
|
|
125
|
-
* reliable regardless of how the sourcemap names its sources.
|
|
126
|
-
*
|
|
127
|
-
* Falls back to the transformed code only when `originalCode` is unavailable
|
|
128
|
-
* (e.g. a virtual module with no sourcemap).
|
|
93
|
+
* Build a vitest-style code snippet showing lines surrounding a location.
|
|
129
94
|
*
|
|
130
|
-
*
|
|
95
|
+
* Prefers `originalCode` from the sourcemap's sourcesContent; falls back
|
|
96
|
+
* to transformed code when unavailable.
|
|
131
97
|
*/
|
|
132
98
|
export declare function buildCodeSnippet(provider: TransformResultProvider, moduleId: string, loc: Loc, contextLines?: number): CodeSnippet | undefined;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SourceMapConsumer } from "source-map";
|
|
2
2
|
import * as path from "pathe";
|
|
3
|
-
import { normalizeFilePath } from "./utils.js";
|
|
3
|
+
import { normalizeFilePath, getOrCreate, escapeRegExp } from "./utils.js";
|
|
4
4
|
function buildLineIndex(code) {
|
|
5
5
|
const offsets = [0];
|
|
6
6
|
for (let i = 0; i < code.length; i++) {
|
|
@@ -21,36 +21,21 @@ function upperBound(values, x) {
|
|
|
21
21
|
return lo;
|
|
22
22
|
}
|
|
23
23
|
function indexToLineColWithIndex(lineIndex, idx) {
|
|
24
|
-
let line = 1;
|
|
25
24
|
const offsets = lineIndex.offsets;
|
|
26
25
|
const ub = upperBound(offsets, idx);
|
|
27
26
|
const lineIdx = Math.max(0, ub - 1);
|
|
28
|
-
line = lineIdx + 1;
|
|
27
|
+
const line = lineIdx + 1;
|
|
29
28
|
const lineStart = offsets[lineIdx] ?? 0;
|
|
30
29
|
return { line, column0: Math.max(0, idx - lineStart) };
|
|
31
30
|
}
|
|
32
|
-
function suffixSegmentScore(a, b) {
|
|
33
|
-
const aSeg = a.split("/").filter(Boolean);
|
|
34
|
-
const bSeg = b.split("/").filter(Boolean);
|
|
35
|
-
let score = 0;
|
|
36
|
-
for (let i = aSeg.length - 1, j = bSeg.length - 1; i >= 0 && j >= 0; i--, j--) {
|
|
37
|
-
if (aSeg[i] !== bSeg[j]) break;
|
|
38
|
-
score++;
|
|
39
|
-
}
|
|
40
|
-
return score;
|
|
41
|
-
}
|
|
42
|
-
function normalizeSourceCandidate(source, root, sourceRoot) {
|
|
43
|
-
if (!source) return "";
|
|
44
|
-
if (path.isAbsolute(source)) return normalizeFilePath(source);
|
|
45
|
-
const base = sourceRoot ? path.resolve(root, sourceRoot) : root;
|
|
46
|
-
return normalizeFilePath(path.resolve(base, source));
|
|
47
|
-
}
|
|
48
31
|
function pickOriginalCodeFromSourcesContent(map, importerFile, root) {
|
|
49
32
|
if (!map?.sourcesContent || map.sources.length === 0) {
|
|
50
33
|
return void 0;
|
|
51
34
|
}
|
|
52
35
|
const file = normalizeFilePath(importerFile);
|
|
53
36
|
const sourceRoot = map.sourceRoot;
|
|
37
|
+
const fileSeg = file.split("/").filter(Boolean);
|
|
38
|
+
const resolveBase = sourceRoot ? path.resolve(root, sourceRoot) : root;
|
|
54
39
|
let bestIdx = -1;
|
|
55
40
|
let bestScore = -1;
|
|
56
41
|
for (let i = 0; i < map.sources.length; i++) {
|
|
@@ -61,13 +46,22 @@ function pickOriginalCodeFromSourcesContent(map, importerFile, root) {
|
|
|
61
46
|
if (normalizedSrc === file) {
|
|
62
47
|
return content;
|
|
63
48
|
}
|
|
64
|
-
|
|
49
|
+
let resolved;
|
|
50
|
+
if (!src) {
|
|
51
|
+
resolved = "";
|
|
52
|
+
} else if (path.isAbsolute(src)) {
|
|
53
|
+
resolved = normalizeFilePath(src);
|
|
54
|
+
} else {
|
|
55
|
+
resolved = normalizeFilePath(path.resolve(resolveBase, src));
|
|
56
|
+
}
|
|
65
57
|
if (resolved === file) {
|
|
66
58
|
return content;
|
|
67
59
|
}
|
|
60
|
+
const normalizedSrcSeg = normalizedSrc.split("/").filter(Boolean);
|
|
61
|
+
const resolvedSeg = resolved !== normalizedSrc ? resolved.split("/").filter(Boolean) : normalizedSrcSeg;
|
|
68
62
|
const score = Math.max(
|
|
69
|
-
|
|
70
|
-
|
|
63
|
+
segmentSuffixScore(normalizedSrcSeg, fileSeg),
|
|
64
|
+
segmentSuffixScore(resolvedSeg, fileSeg)
|
|
71
65
|
);
|
|
72
66
|
if (score > bestScore) {
|
|
73
67
|
bestScore = score;
|
|
@@ -75,11 +69,17 @@ function pickOriginalCodeFromSourcesContent(map, importerFile, root) {
|
|
|
75
69
|
}
|
|
76
70
|
}
|
|
77
71
|
if (bestIdx !== -1 && bestScore >= 1) {
|
|
78
|
-
|
|
79
|
-
return typeof best === "string" ? best : void 0;
|
|
72
|
+
return map.sourcesContent[bestIdx] ?? void 0;
|
|
80
73
|
}
|
|
81
|
-
|
|
82
|
-
|
|
74
|
+
return map.sourcesContent[0] ?? void 0;
|
|
75
|
+
}
|
|
76
|
+
function segmentSuffixScore(aSeg, bSeg) {
|
|
77
|
+
let score = 0;
|
|
78
|
+
for (let i = aSeg.length - 1, j = bSeg.length - 1; i >= 0 && j >= 0; i--, j--) {
|
|
79
|
+
if (aSeg[i] !== bSeg[j]) break;
|
|
80
|
+
score++;
|
|
81
|
+
}
|
|
82
|
+
return score;
|
|
83
83
|
}
|
|
84
84
|
async function mapGeneratedToOriginal(map, generated, fallbackFile) {
|
|
85
85
|
const fallback = {
|
|
@@ -109,12 +109,20 @@ async function mapGeneratedToOriginal(map, generated, fallbackFile) {
|
|
|
109
109
|
return fallback;
|
|
110
110
|
}
|
|
111
111
|
const consumerCache = /* @__PURE__ */ new WeakMap();
|
|
112
|
+
function toRawSourceMap(map) {
|
|
113
|
+
return {
|
|
114
|
+
...map,
|
|
115
|
+
file: map.file ?? "",
|
|
116
|
+
version: Number(map.version),
|
|
117
|
+
sourcesContent: map.sourcesContent?.map((s) => s ?? "") ?? []
|
|
118
|
+
};
|
|
119
|
+
}
|
|
112
120
|
async function getSourceMapConsumer(map) {
|
|
113
121
|
const cached = consumerCache.get(map);
|
|
114
122
|
if (cached) return cached;
|
|
115
123
|
const promise = (async () => {
|
|
116
124
|
try {
|
|
117
|
-
return await new SourceMapConsumer(map);
|
|
125
|
+
return await new SourceMapConsumer(toRawSourceMap(map));
|
|
118
126
|
} catch {
|
|
119
127
|
return null;
|
|
120
128
|
}
|
|
@@ -122,16 +130,50 @@ async function getSourceMapConsumer(map) {
|
|
|
122
130
|
consumerCache.set(map, promise);
|
|
123
131
|
return promise;
|
|
124
132
|
}
|
|
133
|
+
class ImportLocCache {
|
|
134
|
+
cache = /* @__PURE__ */ new Map();
|
|
135
|
+
reverseIndex = /* @__PURE__ */ new Map();
|
|
136
|
+
has(key) {
|
|
137
|
+
return this.cache.has(key);
|
|
138
|
+
}
|
|
139
|
+
get(key) {
|
|
140
|
+
return this.cache.get(key);
|
|
141
|
+
}
|
|
142
|
+
set(key, value) {
|
|
143
|
+
this.cache.set(key, value);
|
|
144
|
+
const file = key.slice(0, key.indexOf("::"));
|
|
145
|
+
getOrCreate(this.reverseIndex, file, () => /* @__PURE__ */ new Set()).add(key);
|
|
146
|
+
}
|
|
147
|
+
clear() {
|
|
148
|
+
this.cache.clear();
|
|
149
|
+
this.reverseIndex.clear();
|
|
150
|
+
}
|
|
151
|
+
/** Remove all cache entries where the importer matches `file`. */
|
|
152
|
+
deleteByFile(file) {
|
|
153
|
+
const keys = this.reverseIndex.get(file);
|
|
154
|
+
if (keys) {
|
|
155
|
+
for (const key of keys) {
|
|
156
|
+
this.cache.delete(key);
|
|
157
|
+
}
|
|
158
|
+
this.reverseIndex.delete(file);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const importPatternCache = /* @__PURE__ */ new Map();
|
|
163
|
+
function clearImportPatternCache() {
|
|
164
|
+
importPatternCache.clear();
|
|
165
|
+
}
|
|
125
166
|
function findFirstImportSpecifierIndex(code, source) {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
167
|
+
let patterns = importPatternCache.get(source);
|
|
168
|
+
if (!patterns) {
|
|
169
|
+
const escaped = escapeRegExp(source);
|
|
170
|
+
patterns = [
|
|
171
|
+
new RegExp(`\\bimport\\s+(['"])${escaped}\\1`),
|
|
172
|
+
new RegExp(`\\bfrom\\s+(['"])${escaped}\\1`),
|
|
173
|
+
new RegExp(`\\bimport\\s*\\(\\s*(['"])${escaped}\\1\\s*\\)`)
|
|
174
|
+
];
|
|
175
|
+
importPatternCache.set(source, patterns);
|
|
176
|
+
}
|
|
135
177
|
let best = -1;
|
|
136
178
|
for (const re of patterns) {
|
|
137
179
|
const m = re.exec(code);
|
|
@@ -146,7 +188,7 @@ async function findImportStatementLocationFromTransformed(provider, importerId,
|
|
|
146
188
|
const importerFile = normalizeFilePath(importerId);
|
|
147
189
|
const cacheKey = `${importerFile}::${source}`;
|
|
148
190
|
if (importLocCache.has(cacheKey)) {
|
|
149
|
-
return importLocCache.get(cacheKey)
|
|
191
|
+
return importLocCache.get(cacheKey) ?? void 0;
|
|
150
192
|
}
|
|
151
193
|
try {
|
|
152
194
|
const res = provider.getTransformResult(importerId);
|
|
@@ -155,10 +197,6 @@ async function findImportStatementLocationFromTransformed(provider, importerId,
|
|
|
155
197
|
return void 0;
|
|
156
198
|
}
|
|
157
199
|
const { code, map } = res;
|
|
158
|
-
if (typeof code !== "string") {
|
|
159
|
-
importLocCache.set(cacheKey, null);
|
|
160
|
-
return void 0;
|
|
161
|
-
}
|
|
162
200
|
const lineIndex = res.lineIndex ?? buildLineIndex(code);
|
|
163
201
|
const idx = findFirstImportSpecifierIndex(code, source);
|
|
164
202
|
if (idx === -1) {
|
|
@@ -180,7 +218,6 @@ async function findPostCompileUsageLocation(provider, importerId, source, findPo
|
|
|
180
218
|
const res = provider.getTransformResult(importerId);
|
|
181
219
|
if (!res) return void 0;
|
|
182
220
|
const { code, map } = res;
|
|
183
|
-
if (typeof code !== "string") return void 0;
|
|
184
221
|
if (!res.lineIndex) {
|
|
185
222
|
res.lineIndex = buildLineIndex(code);
|
|
186
223
|
}
|
|
@@ -212,23 +249,59 @@ function buildCodeSnippet(provider, moduleId, loc, contextLines = 2) {
|
|
|
212
249
|
const res = provider.getTransformResult(moduleId);
|
|
213
250
|
if (!res) return void 0;
|
|
214
251
|
const { code: transformedCode, originalCode } = res;
|
|
215
|
-
if (typeof transformedCode !== "string") return void 0;
|
|
216
252
|
const sourceCode = originalCode ?? transformedCode;
|
|
217
|
-
const allLines = sourceCode.split(/\r?\n/);
|
|
218
253
|
const targetLine = loc.line;
|
|
219
254
|
const targetCol = loc.column;
|
|
220
|
-
if (targetLine < 1
|
|
221
|
-
const
|
|
222
|
-
const
|
|
223
|
-
|
|
255
|
+
if (targetLine < 1) return void 0;
|
|
256
|
+
const wantStart = Math.max(1, targetLine - contextLines);
|
|
257
|
+
const wantEnd = targetLine + contextLines;
|
|
258
|
+
let lineNum = 1;
|
|
259
|
+
let pos = 0;
|
|
260
|
+
while (lineNum < wantStart && pos < sourceCode.length) {
|
|
261
|
+
const ch = sourceCode.charCodeAt(pos);
|
|
262
|
+
if (ch === 10) {
|
|
263
|
+
lineNum++;
|
|
264
|
+
} else if (ch === 13) {
|
|
265
|
+
lineNum++;
|
|
266
|
+
if (pos + 1 < sourceCode.length && sourceCode.charCodeAt(pos + 1) === 10)
|
|
267
|
+
pos++;
|
|
268
|
+
}
|
|
269
|
+
pos++;
|
|
270
|
+
}
|
|
271
|
+
if (lineNum < wantStart) return void 0;
|
|
272
|
+
const lines = [];
|
|
273
|
+
let curLine = wantStart;
|
|
274
|
+
while (curLine <= wantEnd && pos <= sourceCode.length) {
|
|
275
|
+
let eol = pos;
|
|
276
|
+
while (eol < sourceCode.length) {
|
|
277
|
+
const ch = sourceCode.charCodeAt(eol);
|
|
278
|
+
if (ch === 10 || ch === 13) break;
|
|
279
|
+
eol++;
|
|
280
|
+
}
|
|
281
|
+
lines.push(sourceCode.slice(pos, eol));
|
|
282
|
+
curLine++;
|
|
283
|
+
if (eol < sourceCode.length) {
|
|
284
|
+
if (sourceCode.charCodeAt(eol) === 13 && eol + 1 < sourceCode.length && sourceCode.charCodeAt(eol + 1) === 10) {
|
|
285
|
+
pos = eol + 2;
|
|
286
|
+
} else {
|
|
287
|
+
pos = eol + 1;
|
|
288
|
+
}
|
|
289
|
+
} else {
|
|
290
|
+
pos = eol + 1;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (targetLine > wantStart + lines.length - 1) return void 0;
|
|
294
|
+
const actualEnd = wantStart + lines.length - 1;
|
|
295
|
+
const gutterWidth = String(actualEnd).length;
|
|
224
296
|
const sourceFile = loc.file ?? importerFile;
|
|
225
297
|
const snippetLines = [];
|
|
226
|
-
for (let i =
|
|
227
|
-
const
|
|
228
|
-
const
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
298
|
+
for (let i = 0; i < lines.length; i++) {
|
|
299
|
+
const ln = wantStart + i;
|
|
300
|
+
const lineContent = lines[i];
|
|
301
|
+
const lineNumStr = String(ln).padStart(gutterWidth, " ");
|
|
302
|
+
const marker = ln === targetLine ? ">" : " ";
|
|
303
|
+
snippetLines.push(` ${marker} ${lineNumStr} | ${lineContent}`);
|
|
304
|
+
if (ln === targetLine && targetCol > 0) {
|
|
232
305
|
const padding = " ".repeat(targetCol - 1);
|
|
233
306
|
snippetLines.push(` ${" ".repeat(gutterWidth)} | ${padding}^`);
|
|
234
307
|
}
|
|
@@ -243,13 +316,13 @@ function buildCodeSnippet(provider, moduleId, loc, contextLines = 2) {
|
|
|
243
316
|
}
|
|
244
317
|
}
|
|
245
318
|
export {
|
|
319
|
+
ImportLocCache,
|
|
246
320
|
addTraceImportLocations,
|
|
247
321
|
buildCodeSnippet,
|
|
248
322
|
buildLineIndex,
|
|
249
|
-
|
|
323
|
+
clearImportPatternCache,
|
|
250
324
|
findImportStatementLocationFromTransformed,
|
|
251
325
|
findPostCompileUsageLocation,
|
|
252
|
-
mapGeneratedToOriginal,
|
|
253
326
|
pickOriginalCodeFromSourcesContent
|
|
254
327
|
};
|
|
255
328
|
//# sourceMappingURL=sourceLocation.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sourceLocation.js","sources":["../../../src/import-protection-plugin/sourceLocation.ts"],"sourcesContent":["import { SourceMapConsumer } from 'source-map'\nimport * as path from 'pathe'\n\nimport { normalizeFilePath } from './utils'\nimport type { Loc } from './trace'\nimport type { RawSourceMap } from 'source-map'\n\n// ---------------------------------------------------------------------------\n// Source-map type compatible with both Rollup's SourceMap and source-map's\n// RawSourceMap. We define our own structural type so that the value returned\n// by `getCombinedSourcemap()` (version: number) flows seamlessly into\n// `SourceMapConsumer` (version: string) without requiring a cast.\n// ---------------------------------------------------------------------------\n\n/**\n * Minimal source-map shape used throughout the import-protection plugin.\n *\n * Structurally compatible with both Rollup's `SourceMap` (version: number)\n * and the `source-map` package's `RawSourceMap` (version: string).\n */\nexport interface SourceMapLike {\n file?: string\n sourceRoot?: string\n version: number | string\n sources: Array<string>\n names: Array<string>\n sourcesContent?: Array<string | null>\n mappings: string\n}\n\n// ---------------------------------------------------------------------------\n// Transform result provider (replaces ctx.load() which doesn't work in dev)\n// ---------------------------------------------------------------------------\n\n/**\n * A cached transform result for a single module.\n *\n * - `code` – fully-transformed source (after all plugins).\n * - `map` – composed sourcemap (chains back to the original file).\n * - `originalCode` – the untransformed source, extracted from the\n * sourcemap's `sourcesContent[0]` during the transform\n * hook. Used by {@link buildCodeSnippet} so we never\n * have to re-derive it via a flaky `sourceContentFor`\n * lookup at display time.\n */\nexport interface TransformResult {\n code: string\n map: SourceMapLike | undefined\n originalCode: string | undefined\n /** Precomputed line index for `code` (index → line/col). */\n lineIndex?: LineIndex\n}\n\n/**\n * Provides the transformed code and composed sourcemap for a module.\n *\n * During `resolveId`, Vite's `this.load()` does NOT return code/map in dev\n * mode (the ModuleInfo proxy throws on `.code` access). Even in build mode,\n * Rollup's `ModuleInfo` has `.code` but not `.map`.\n *\n * Instead, we populate this cache from a late-running transform hook that\n * stores `{ code, map, originalCode }` for every module as it passes through\n * the pipeline. By the time `resolveId` fires for an import, the importer\n * has already been fully transformed, so the cache always has the data we\n * need.\n *\n * The `id` parameter is the **raw** module ID (may include Vite query\n * parameters like `?tsr-split=component`). Implementations should look up\n * with the full ID first, then fall back to the query-stripped path so that\n * virtual-module variants are resolved correctly without losing the base-file\n * fallback.\n */\nexport interface TransformResultProvider {\n getTransformResult: (id: string) => TransformResult | undefined\n}\n\n// ---------------------------------------------------------------------------\n// Index → line/column conversion\n// ---------------------------------------------------------------------------\n\nexport type LineIndex = {\n offsets: Array<number>\n}\n\nexport function buildLineIndex(code: string): LineIndex {\n const offsets: Array<number> = [0]\n for (let i = 0; i < code.length; i++) {\n if (code.charCodeAt(i) === 10) {\n offsets.push(i + 1)\n }\n }\n return { offsets }\n}\n\nfunction upperBound(values: Array<number>, x: number): number {\n let lo = 0\n let hi = values.length\n while (lo < hi) {\n const mid = (lo + hi) >> 1\n if (values[mid]! <= x) lo = mid + 1\n else hi = mid\n }\n return lo\n}\n\nfunction indexToLineColWithIndex(\n lineIndex: LineIndex,\n idx: number,\n): { line: number; column0: number } {\n let line = 1\n\n const offsets = lineIndex.offsets\n const ub = upperBound(offsets, idx)\n const lineIdx = Math.max(0, ub - 1)\n line = lineIdx + 1\n\n const lineStart = offsets[lineIdx] ?? 0\n return { line, column0: Math.max(0, idx - lineStart) }\n}\n\n// ---------------------------------------------------------------------------\n// Pick the best original source from sourcesContent\n// ---------------------------------------------------------------------------\n\nfunction suffixSegmentScore(a: string, b: string): number {\n const aSeg = a.split('/').filter(Boolean)\n const bSeg = b.split('/').filter(Boolean)\n let score = 0\n for (\n let i = aSeg.length - 1, j = bSeg.length - 1;\n i >= 0 && j >= 0;\n i--, j--\n ) {\n if (aSeg[i] !== bSeg[j]) break\n score++\n }\n return score\n}\n\nfunction normalizeSourceCandidate(\n source: string,\n root: string,\n sourceRoot: string | undefined,\n): string {\n // Prefer resolving relative source paths against root/sourceRoot when present.\n if (!source) return ''\n if (path.isAbsolute(source)) return normalizeFilePath(source)\n const base = sourceRoot ? path.resolve(root, sourceRoot) : root\n return normalizeFilePath(path.resolve(base, source))\n}\n\n/**\n * Pick the most-likely original source text for `importerFile`.\n *\n * Sourcemaps can contain multiple sources (composed maps), so `sourcesContent[0]`\n * is not guaranteed to represent the importer.\n */\nexport function pickOriginalCodeFromSourcesContent(\n map: SourceMapLike | undefined,\n importerFile: string,\n root: string,\n): string | undefined {\n if (!map?.sourcesContent || map.sources.length === 0) {\n return undefined\n }\n\n const file = normalizeFilePath(importerFile)\n const sourceRoot = map.sourceRoot\n\n let bestIdx = -1\n let bestScore = -1\n\n for (let i = 0; i < map.sources.length; i++) {\n const content = map.sourcesContent[i]\n if (typeof content !== 'string') continue\n\n const src = map.sources[i] ?? ''\n\n // Exact match via raw normalized source.\n const normalizedSrc = normalizeFilePath(src)\n if (normalizedSrc === file) {\n return content\n }\n\n // Exact match via resolved absolute candidate.\n const resolved = normalizeSourceCandidate(src, root, sourceRoot)\n if (resolved === file) {\n return content\n }\n\n const score = Math.max(\n suffixSegmentScore(normalizedSrc, file),\n suffixSegmentScore(resolved, file),\n )\n\n if (score > bestScore) {\n bestScore = score\n bestIdx = i\n }\n }\n\n // Require at least a basename match; otherwise fall back to index 0.\n if (bestIdx !== -1 && bestScore >= 1) {\n const best = map.sourcesContent[bestIdx]\n return typeof best === 'string' ? best : undefined\n }\n\n const fallback = map.sourcesContent[0]\n return typeof fallback === 'string' ? fallback : undefined\n}\n\n// ---------------------------------------------------------------------------\n// Sourcemap: generated → original mapping\n// ---------------------------------------------------------------------------\n\nexport async function mapGeneratedToOriginal(\n map: SourceMapLike | undefined,\n generated: { line: number; column0: number },\n fallbackFile: string,\n): Promise<Loc> {\n const fallback: Loc = {\n file: fallbackFile,\n line: generated.line,\n column: generated.column0 + 1,\n }\n\n if (!map) {\n return fallback\n }\n\n const consumer = await getSourceMapConsumer(map)\n if (!consumer) return fallback\n\n try {\n const orig = consumer.originalPositionFor({\n line: generated.line,\n column: generated.column0,\n })\n if (orig.line != null && orig.column != null) {\n return {\n file: orig.source ? normalizeFilePath(orig.source) : fallbackFile,\n line: orig.line,\n column: orig.column + 1,\n }\n }\n } catch {\n // Invalid or malformed sourcemap — fall through to fallback.\n }\n\n return fallback\n}\n\n// Cache SourceMapConsumer per sourcemap object.\nconst consumerCache = new WeakMap<object, Promise<SourceMapConsumer | null>>()\n\nasync function getSourceMapConsumer(\n map: SourceMapLike,\n): Promise<SourceMapConsumer | null> {\n // WeakMap requires an object key; SourceMapLike should be an object in all\n // real cases, but guard anyway.\n // (TypeScript already guarantees `map` is an object here.)\n\n const cached = consumerCache.get(map)\n if (cached) return cached\n\n const promise = (async () => {\n try {\n return await new SourceMapConsumer(map as unknown as RawSourceMap)\n } catch {\n return null\n }\n })()\n\n consumerCache.set(map, promise)\n return promise\n}\n\n// ---------------------------------------------------------------------------\n// Import specifier search (regex-based, no AST needed)\n// ---------------------------------------------------------------------------\n\nexport function findFirstImportSpecifierIndex(\n code: string,\n source: string,\n): number {\n const escaped = source.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n\n const patterns: Array<RegExp> = [\n // import 'x'\n new RegExp(`\\\\bimport\\\\s+(['\"])${escaped}\\\\1`),\n // import ... from 'x' / export ... from 'x'\n new RegExp(`\\\\bfrom\\\\s+(['\"])${escaped}\\\\1`),\n // import('x')\n new RegExp(`\\\\bimport\\\\s*\\\\(\\\\s*(['\"])${escaped}\\\\1\\\\s*\\\\)`),\n ]\n\n let best = -1\n for (const re of patterns) {\n const m = re.exec(code)\n if (!m) continue\n const idx = m.index + m[0].indexOf(source)\n if (idx === -1) continue\n if (best === -1 || idx < best) best = idx\n }\n return best\n}\n\n// ---------------------------------------------------------------------------\n// High-level location finders (use transform result cache, no disk reads)\n// ---------------------------------------------------------------------------\n\n/**\n * Find the location of an import statement in a transformed module.\n *\n * Looks up the module's transformed code + composed sourcemap from the\n * {@link TransformResultProvider}, finds the import specifier in the\n * transformed code, and maps back to the original source via the sourcemap.\n *\n * Results are cached in `importLocCache`.\n */\nexport async function findImportStatementLocationFromTransformed(\n provider: TransformResultProvider,\n importerId: string,\n source: string,\n importLocCache: Map<\n string,\n { file?: string; line: number; column: number } | null\n >,\n): Promise<Loc | undefined> {\n const importerFile = normalizeFilePath(importerId)\n const cacheKey = `${importerFile}::${source}`\n if (importLocCache.has(cacheKey)) {\n return importLocCache.get(cacheKey) || undefined\n }\n\n try {\n // Pass the raw importerId so the provider can look up the exact virtual\n // module variant (e.g. ?tsr-split=component) before falling back to the\n // base file path.\n const res = provider.getTransformResult(importerId)\n if (!res) {\n importLocCache.set(cacheKey, null)\n return undefined\n }\n\n const { code, map } = res\n if (typeof code !== 'string') {\n importLocCache.set(cacheKey, null)\n return undefined\n }\n\n const lineIndex = res.lineIndex ?? buildLineIndex(code)\n\n const idx = findFirstImportSpecifierIndex(code, source)\n if (idx === -1) {\n importLocCache.set(cacheKey, null)\n return undefined\n }\n\n const generated = indexToLineColWithIndex(lineIndex, idx)\n const loc = await mapGeneratedToOriginal(map, generated, importerFile)\n importLocCache.set(cacheKey, loc)\n return loc\n } catch {\n importLocCache.set(cacheKey, null)\n return undefined\n }\n}\n\n/**\n * Find the first post-compile usage location for a denied import specifier.\n *\n * Best-effort: looks up the module's transformed output from the\n * {@link TransformResultProvider}, finds the first non-import usage of\n * an imported binding, and maps back to original source via sourcemap.\n */\nexport async function findPostCompileUsageLocation(\n provider: TransformResultProvider,\n importerId: string,\n source: string,\n findPostCompileUsagePos: (\n code: string,\n source: string,\n ) => { line: number; column0: number } | undefined,\n): Promise<Loc | undefined> {\n try {\n const importerFile = normalizeFilePath(importerId)\n // Pass the raw importerId so the provider can look up the exact virtual\n // module variant (e.g. ?tsr-split=component) before falling back to the\n // base file path.\n const res = provider.getTransformResult(importerId)\n if (!res) return undefined\n const { code, map } = res\n if (typeof code !== 'string') return undefined\n\n // Ensure we have a line index ready for any downstream mapping.\n // (We don't currently need it here, but keeping it hot improves locality\n // for callers that also need import-statement mapping.)\n if (!res.lineIndex) {\n res.lineIndex = buildLineIndex(code)\n }\n\n const pos = findPostCompileUsagePos(code, source)\n if (!pos) return undefined\n\n return await mapGeneratedToOriginal(map, pos, importerFile)\n } catch {\n return undefined\n }\n}\n\n/**\n * Annotate each trace hop with the location of the import that created the\n * edge (file:line:col). Skips steps that already have a location.\n */\nexport async function addTraceImportLocations(\n provider: TransformResultProvider,\n trace: Array<{\n file: string\n specifier?: string\n line?: number\n column?: number\n }>,\n importLocCache: Map<\n string,\n { file?: string; line: number; column: number } | null\n >,\n): Promise<void> {\n for (const step of trace) {\n if (!step.specifier) continue\n if (step.line != null && step.column != null) continue\n const loc = await findImportStatementLocationFromTransformed(\n provider,\n step.file,\n step.specifier,\n importLocCache,\n )\n if (!loc) continue\n step.line = loc.line\n step.column = loc.column\n }\n}\n\n// ---------------------------------------------------------------------------\n// Code snippet extraction (vitest-style context around a location)\n// ---------------------------------------------------------------------------\n\nexport interface CodeSnippet {\n /** Source lines with line numbers, e.g. `[\" 6 | import { getSecret } from './secret.server'\", ...]` */\n lines: Array<string>\n /** The highlighted line (1-indexed original line number) */\n highlightLine: number\n /** Clickable file:line reference */\n location: string\n}\n\n/**\n * Build a vitest-style code snippet showing the lines surrounding a location.\n *\n * Uses the `originalCode` stored in the transform result cache (extracted from\n * `sourcesContent[0]` of the composed sourcemap at transform time). This is\n * reliable regardless of how the sourcemap names its sources.\n *\n * Falls back to the transformed code only when `originalCode` is unavailable\n * (e.g. a virtual module with no sourcemap).\n *\n * @param contextLines Number of lines to show above/below the target line (default 2).\n */\nexport function buildCodeSnippet(\n provider: TransformResultProvider,\n moduleId: string,\n loc: Loc,\n contextLines: number = 2,\n): CodeSnippet | undefined {\n try {\n const importerFile = normalizeFilePath(moduleId)\n // Pass the raw moduleId so the provider can look up the exact virtual\n // module variant (e.g. ?tsr-split=component) before falling back to the\n // base file path.\n const res = provider.getTransformResult(moduleId)\n if (!res) return undefined\n\n const { code: transformedCode, originalCode } = res\n if (typeof transformedCode !== 'string') return undefined\n\n // Prefer the original source that was captured at transform time from the\n // sourcemap's sourcesContent. This avoids the source-name-mismatch\n // problem that plagued the old sourceContentFor()-based lookup.\n const sourceCode = originalCode ?? transformedCode\n\n const allLines = sourceCode.split(/\\r?\\n/)\n const targetLine = loc.line // 1-indexed\n const targetCol = loc.column // 1-indexed\n\n if (targetLine < 1 || targetLine > allLines.length) return undefined\n\n const startLine = Math.max(1, targetLine - contextLines)\n const endLine = Math.min(allLines.length, targetLine + contextLines)\n const gutterWidth = String(endLine).length\n\n const sourceFile = loc.file ?? importerFile\n const snippetLines: Array<string> = []\n for (let i = startLine; i <= endLine; i++) {\n const lineContent = allLines[i - 1] ?? ''\n const lineNum = String(i).padStart(gutterWidth, ' ')\n const marker = i === targetLine ? '>' : ' '\n snippetLines.push(` ${marker} ${lineNum} | ${lineContent}`)\n\n // Add column pointer on the target line\n if (i === targetLine && targetCol > 0) {\n const padding = ' '.repeat(targetCol - 1)\n snippetLines.push(` ${' '.repeat(gutterWidth)} | ${padding}^`)\n }\n }\n\n return {\n lines: snippetLines,\n highlightLine: targetLine,\n location: `${sourceFile}:${targetLine}:${targetCol}`,\n }\n } catch {\n return undefined\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"sourceLocation.js","sources":["../../../src/import-protection-plugin/sourceLocation.ts"],"sourcesContent":["import { SourceMapConsumer } from 'source-map'\nimport * as path from 'pathe'\n\nimport { escapeRegExp, getOrCreate, normalizeFilePath } from './utils'\nimport type { Loc } from './trace'\nimport type { RawSourceMap } from 'source-map'\n\n// Source-map type compatible with both Rollup's SourceMap and source-map's\n// RawSourceMap. Structural type avoids version: number vs string mismatch.\n\n/**\n * Minimal source-map shape used throughout the import-protection plugin.\n */\nexport interface SourceMapLike {\n file?: string\n sourceRoot?: string\n version: number | string\n sources: Array<string>\n names: Array<string>\n sourcesContent?: Array<string | null>\n mappings: string\n}\n\n// Transform result provider (replaces ctx.load() which doesn't work in dev)\nexport interface TransformResult {\n code: string\n map: SourceMapLike | undefined\n originalCode: string | undefined\n /** Precomputed line index for `code` (index → line/col). */\n lineIndex?: LineIndex\n}\n\n/**\n * Provides the transformed code and composed sourcemap for a module.\n *\n * Populated from a late-running transform hook. By the time `resolveId`\n * fires for an import, the importer has already been fully transformed.\n */\nexport interface TransformResultProvider {\n getTransformResult: (id: string) => TransformResult | undefined\n}\n\n// Index → line/column conversion\n\nexport type LineIndex = {\n offsets: Array<number>\n}\n\nexport function buildLineIndex(code: string): LineIndex {\n const offsets: Array<number> = [0]\n for (let i = 0; i < code.length; i++) {\n if (code.charCodeAt(i) === 10) {\n offsets.push(i + 1)\n }\n }\n return { offsets }\n}\n\nfunction upperBound(values: Array<number>, x: number): number {\n let lo = 0\n let hi = values.length\n while (lo < hi) {\n const mid = (lo + hi) >> 1\n if (values[mid]! <= x) lo = mid + 1\n else hi = mid\n }\n return lo\n}\n\nfunction indexToLineColWithIndex(\n lineIndex: LineIndex,\n idx: number,\n): { line: number; column0: number } {\n const offsets = lineIndex.offsets\n const ub = upperBound(offsets, idx)\n const lineIdx = Math.max(0, ub - 1)\n const line = lineIdx + 1\n\n const lineStart = offsets[lineIdx] ?? 0\n return { line, column0: Math.max(0, idx - lineStart) }\n}\n\n/**\n * Pick the most-likely original source text for `importerFile` from\n * a sourcemap that may contain multiple sources.\n */\nexport function pickOriginalCodeFromSourcesContent(\n map: SourceMapLike | undefined,\n importerFile: string,\n root: string,\n): string | undefined {\n if (!map?.sourcesContent || map.sources.length === 0) {\n return undefined\n }\n\n const file = normalizeFilePath(importerFile)\n const sourceRoot = map.sourceRoot\n const fileSeg = file.split('/').filter(Boolean)\n\n const resolveBase = sourceRoot ? path.resolve(root, sourceRoot) : root\n\n let bestIdx = -1\n let bestScore = -1\n\n for (let i = 0; i < map.sources.length; i++) {\n const content = map.sourcesContent[i]\n if (typeof content !== 'string') continue\n\n const src = map.sources[i] ?? ''\n\n const normalizedSrc = normalizeFilePath(src)\n if (normalizedSrc === file) {\n return content\n }\n\n let resolved: string\n if (!src) {\n resolved = ''\n } else if (path.isAbsolute(src)) {\n resolved = normalizeFilePath(src)\n } else {\n resolved = normalizeFilePath(path.resolve(resolveBase, src))\n }\n if (resolved === file) {\n return content\n }\n\n // Count matching path segments from the end.\n const normalizedSrcSeg = normalizedSrc.split('/').filter(Boolean)\n const resolvedSeg =\n resolved !== normalizedSrc\n ? resolved.split('/').filter(Boolean)\n : normalizedSrcSeg\n const score = Math.max(\n segmentSuffixScore(normalizedSrcSeg, fileSeg),\n segmentSuffixScore(resolvedSeg, fileSeg),\n )\n\n if (score > bestScore) {\n bestScore = score\n bestIdx = i\n }\n }\n\n if (bestIdx !== -1 && bestScore >= 1) {\n return map.sourcesContent[bestIdx] ?? undefined\n }\n\n return map.sourcesContent[0] ?? undefined\n}\n\n/** Count matching path segments from the end of `aSeg` against `bSeg`. */\nfunction segmentSuffixScore(aSeg: Array<string>, bSeg: Array<string>): number {\n let score = 0\n for (\n let i = aSeg.length - 1, j = bSeg.length - 1;\n i >= 0 && j >= 0;\n i--, j--\n ) {\n if (aSeg[i] !== bSeg[j]) break\n score++\n }\n return score\n}\n\nasync function mapGeneratedToOriginal(\n map: SourceMapLike | undefined,\n generated: { line: number; column0: number },\n fallbackFile: string,\n): Promise<Loc> {\n const fallback: Loc = {\n file: fallbackFile,\n line: generated.line,\n column: generated.column0 + 1,\n }\n\n if (!map) {\n return fallback\n }\n\n const consumer = await getSourceMapConsumer(map)\n if (!consumer) return fallback\n\n try {\n const orig = consumer.originalPositionFor({\n line: generated.line,\n column: generated.column0,\n })\n if (orig.line != null && orig.column != null) {\n return {\n file: orig.source ? normalizeFilePath(orig.source) : fallbackFile,\n line: orig.line,\n column: orig.column + 1,\n }\n }\n } catch {\n // Malformed sourcemap\n }\n\n return fallback\n}\n\nconst consumerCache = new WeakMap<object, Promise<SourceMapConsumer | null>>()\n\nfunction toRawSourceMap(map: SourceMapLike): RawSourceMap {\n return {\n ...map,\n file: map.file ?? '',\n version: Number(map.version),\n sourcesContent: map.sourcesContent?.map((s) => s ?? '') ?? [],\n }\n}\n\nasync function getSourceMapConsumer(\n map: SourceMapLike,\n): Promise<SourceMapConsumer | null> {\n const cached = consumerCache.get(map)\n if (cached) return cached\n\n const promise = (async () => {\n try {\n return await new SourceMapConsumer(toRawSourceMap(map))\n } catch {\n return null\n }\n })()\n\n consumerCache.set(map, promise)\n return promise\n}\n\nexport type ImportLocEntry = { file?: string; line: number; column: number }\n\n/**\n * Cache for import statement locations with reverse index for O(1)\n * invalidation by file. Keys: `${importerFile}::${source}`.\n */\nexport class ImportLocCache {\n private cache = new Map<string, ImportLocEntry | null>()\n private reverseIndex = new Map<string, Set<string>>()\n\n has(key: string): boolean {\n return this.cache.has(key)\n }\n\n get(key: string): ImportLocEntry | null | undefined {\n return this.cache.get(key)\n }\n\n set(key: string, value: ImportLocEntry | null): void {\n this.cache.set(key, value)\n const file = key.slice(0, key.indexOf('::'))\n getOrCreate(this.reverseIndex, file, () => new Set()).add(key)\n }\n\n clear(): void {\n this.cache.clear()\n this.reverseIndex.clear()\n }\n\n /** Remove all cache entries where the importer matches `file`. */\n deleteByFile(file: string): void {\n const keys = this.reverseIndex.get(file)\n if (keys) {\n for (const key of keys) {\n this.cache.delete(key)\n }\n this.reverseIndex.delete(file)\n }\n }\n}\n\n// Import specifier search (regex-based)\n\nconst importPatternCache = new Map<string, Array<RegExp>>()\n\nexport function clearImportPatternCache(): void {\n importPatternCache.clear()\n}\n\nfunction findFirstImportSpecifierIndex(code: string, source: string): number {\n let patterns = importPatternCache.get(source)\n if (!patterns) {\n const escaped = escapeRegExp(source)\n patterns = [\n new RegExp(`\\\\bimport\\\\s+(['\"])${escaped}\\\\1`),\n new RegExp(`\\\\bfrom\\\\s+(['\"])${escaped}\\\\1`),\n new RegExp(`\\\\bimport\\\\s*\\\\(\\\\s*(['\"])${escaped}\\\\1\\\\s*\\\\)`),\n ]\n importPatternCache.set(source, patterns)\n }\n\n let best = -1\n for (const re of patterns) {\n const m = re.exec(code)\n if (!m) continue\n const idx = m.index + m[0].indexOf(source)\n if (idx === -1) continue\n if (best === -1 || idx < best) best = idx\n }\n return best\n}\n\n/**\n * Find the location of an import statement in a transformed module\n * by searching the post-transform code and mapping back via sourcemap.\n * Results are cached in `importLocCache`.\n */\nexport async function findImportStatementLocationFromTransformed(\n provider: TransformResultProvider,\n importerId: string,\n source: string,\n importLocCache: ImportLocCache,\n): Promise<Loc | undefined> {\n const importerFile = normalizeFilePath(importerId)\n const cacheKey = `${importerFile}::${source}`\n if (importLocCache.has(cacheKey)) {\n return importLocCache.get(cacheKey) ?? undefined\n }\n\n try {\n const res = provider.getTransformResult(importerId)\n if (!res) {\n importLocCache.set(cacheKey, null)\n return undefined\n }\n\n const { code, map } = res\n\n const lineIndex = res.lineIndex ?? buildLineIndex(code)\n\n const idx = findFirstImportSpecifierIndex(code, source)\n if (idx === -1) {\n importLocCache.set(cacheKey, null)\n return undefined\n }\n\n const generated = indexToLineColWithIndex(lineIndex, idx)\n const loc = await mapGeneratedToOriginal(map, generated, importerFile)\n importLocCache.set(cacheKey, loc)\n return loc\n } catch {\n importLocCache.set(cacheKey, null)\n return undefined\n }\n}\n\n/**\n * Find the first post-compile usage location for a denied import specifier.\n * Best-effort: searches transformed code for non-import uses of imported\n * bindings and maps back to original source via sourcemap.\n */\nexport async function findPostCompileUsageLocation(\n provider: TransformResultProvider,\n importerId: string,\n source: string,\n findPostCompileUsagePos: (\n code: string,\n source: string,\n ) => { line: number; column0: number } | undefined,\n): Promise<Loc | undefined> {\n try {\n const importerFile = normalizeFilePath(importerId)\n const res = provider.getTransformResult(importerId)\n if (!res) return undefined\n const { code, map } = res\n\n if (!res.lineIndex) {\n res.lineIndex = buildLineIndex(code)\n }\n\n const pos = findPostCompileUsagePos(code, source)\n if (!pos) return undefined\n\n return await mapGeneratedToOriginal(map, pos, importerFile)\n } catch {\n return undefined\n }\n}\n\n/**\n * Annotate each trace hop with the location of the import that created the\n * edge (file:line:col). Skips steps that already have a location.\n */\nexport async function addTraceImportLocations(\n provider: TransformResultProvider,\n trace: Array<{\n file: string\n specifier?: string\n line?: number\n column?: number\n }>,\n importLocCache: ImportLocCache,\n): Promise<void> {\n for (const step of trace) {\n if (!step.specifier) continue\n if (step.line != null && step.column != null) continue\n const loc = await findImportStatementLocationFromTransformed(\n provider,\n step.file,\n step.specifier,\n importLocCache,\n )\n if (!loc) continue\n step.line = loc.line\n step.column = loc.column\n }\n}\n\n// Code snippet extraction (vitest-style context around a location)\n\nexport interface CodeSnippet {\n /** Source lines with line numbers, e.g. `[\" 6 | import { getSecret } from './secret.server'\", ...]` */\n lines: Array<string>\n /** The highlighted line (1-indexed original line number) */\n highlightLine: number\n /** Clickable file:line reference */\n location: string\n}\n\n/**\n * Build a vitest-style code snippet showing lines surrounding a location.\n *\n * Prefers `originalCode` from the sourcemap's sourcesContent; falls back\n * to transformed code when unavailable.\n */\nexport function buildCodeSnippet(\n provider: TransformResultProvider,\n moduleId: string,\n loc: Loc,\n contextLines: number = 2,\n): CodeSnippet | undefined {\n try {\n const importerFile = normalizeFilePath(moduleId)\n const res = provider.getTransformResult(moduleId)\n if (!res) return undefined\n\n const { code: transformedCode, originalCode } = res\n\n const sourceCode = originalCode ?? transformedCode\n const targetLine = loc.line // 1-indexed\n const targetCol = loc.column // 1-indexed\n\n if (targetLine < 1) return undefined\n\n const wantStart = Math.max(1, targetLine - contextLines)\n const wantEnd = targetLine + contextLines\n\n // Advance to wantStart\n let lineNum = 1\n let pos = 0\n while (lineNum < wantStart && pos < sourceCode.length) {\n const ch = sourceCode.charCodeAt(pos)\n if (ch === 10) {\n lineNum++\n } else if (ch === 13) {\n lineNum++\n if (\n pos + 1 < sourceCode.length &&\n sourceCode.charCodeAt(pos + 1) === 10\n )\n pos++\n }\n pos++\n }\n if (lineNum < wantStart) return undefined\n\n const lines: Array<string> = []\n let curLine = wantStart\n while (curLine <= wantEnd && pos <= sourceCode.length) {\n // Find end of current line\n let eol = pos\n while (eol < sourceCode.length) {\n const ch = sourceCode.charCodeAt(eol)\n if (ch === 10 || ch === 13) break\n eol++\n }\n lines.push(sourceCode.slice(pos, eol))\n curLine++\n if (eol < sourceCode.length) {\n if (\n sourceCode.charCodeAt(eol) === 13 &&\n eol + 1 < sourceCode.length &&\n sourceCode.charCodeAt(eol + 1) === 10\n ) {\n pos = eol + 2\n } else {\n pos = eol + 1\n }\n } else {\n pos = eol + 1\n }\n }\n\n if (targetLine > wantStart + lines.length - 1) return undefined\n\n const actualEnd = wantStart + lines.length - 1\n const gutterWidth = String(actualEnd).length\n\n const sourceFile = loc.file ?? importerFile\n const snippetLines: Array<string> = []\n for (let i = 0; i < lines.length; i++) {\n const ln = wantStart + i\n const lineContent = lines[i]!\n const lineNumStr = String(ln).padStart(gutterWidth, ' ')\n const marker = ln === targetLine ? '>' : ' '\n snippetLines.push(` ${marker} ${lineNumStr} | ${lineContent}`)\n\n if (ln === targetLine && targetCol > 0) {\n const padding = ' '.repeat(targetCol - 1)\n snippetLines.push(` ${' '.repeat(gutterWidth)} | ${padding}^`)\n }\n }\n\n return {\n lines: snippetLines,\n highlightLine: targetLine,\n location: `${sourceFile}:${targetLine}:${targetCol}`,\n }\n } catch {\n return undefined\n }\n}\n"],"names":[],"mappings":";;;AAgDO,SAAS,eAAe,MAAyB;AACtD,QAAM,UAAyB,CAAC,CAAC;AACjC,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,QAAI,KAAK,WAAW,CAAC,MAAM,IAAI;AAC7B,cAAQ,KAAK,IAAI,CAAC;AAAA,IACpB;AAAA,EACF;AACA,SAAO,EAAE,QAAA;AACX;AAEA,SAAS,WAAW,QAAuB,GAAmB;AAC5D,MAAI,KAAK;AACT,MAAI,KAAK,OAAO;AAChB,SAAO,KAAK,IAAI;AACd,UAAM,MAAO,KAAK,MAAO;AACzB,QAAI,OAAO,GAAG,KAAM,QAAQ,MAAM;AAAA,QAC7B,MAAK;AAAA,EACZ;AACA,SAAO;AACT;AAEA,SAAS,wBACP,WACA,KACmC;AACnC,QAAM,UAAU,UAAU;AAC1B,QAAM,KAAK,WAAW,SAAS,GAAG;AAClC,QAAM,UAAU,KAAK,IAAI,GAAG,KAAK,CAAC;AAClC,QAAM,OAAO,UAAU;AAEvB,QAAM,YAAY,QAAQ,OAAO,KAAK;AACtC,SAAO,EAAE,MAAM,SAAS,KAAK,IAAI,GAAG,MAAM,SAAS,EAAA;AACrD;AAMO,SAAS,mCACd,KACA,cACA,MACoB;AACpB,MAAI,CAAC,KAAK,kBAAkB,IAAI,QAAQ,WAAW,GAAG;AACpD,WAAO;AAAA,EACT;AAEA,QAAM,OAAO,kBAAkB,YAAY;AAC3C,QAAM,aAAa,IAAI;AACvB,QAAM,UAAU,KAAK,MAAM,GAAG,EAAE,OAAO,OAAO;AAE9C,QAAM,cAAc,aAAa,KAAK,QAAQ,MAAM,UAAU,IAAI;AAElE,MAAI,UAAU;AACd,MAAI,YAAY;AAEhB,WAAS,IAAI,GAAG,IAAI,IAAI,QAAQ,QAAQ,KAAK;AAC3C,UAAM,UAAU,IAAI,eAAe,CAAC;AACpC,QAAI,OAAO,YAAY,SAAU;AAEjC,UAAM,MAAM,IAAI,QAAQ,CAAC,KAAK;AAE9B,UAAM,gBAAgB,kBAAkB,GAAG;AAC3C,QAAI,kBAAkB,MAAM;AAC1B,aAAO;AAAA,IACT;AAEA,QAAI;AACJ,QAAI,CAAC,KAAK;AACR,iBAAW;AAAA,IACb,WAAW,KAAK,WAAW,GAAG,GAAG;AAC/B,iBAAW,kBAAkB,GAAG;AAAA,IAClC,OAAO;AACL,iBAAW,kBAAkB,KAAK,QAAQ,aAAa,GAAG,CAAC;AAAA,IAC7D;AACA,QAAI,aAAa,MAAM;AACrB,aAAO;AAAA,IACT;AAGA,UAAM,mBAAmB,cAAc,MAAM,GAAG,EAAE,OAAO,OAAO;AAChE,UAAM,cACJ,aAAa,gBACT,SAAS,MAAM,GAAG,EAAE,OAAO,OAAO,IAClC;AACN,UAAM,QAAQ,KAAK;AAAA,MACjB,mBAAmB,kBAAkB,OAAO;AAAA,MAC5C,mBAAmB,aAAa,OAAO;AAAA,IAAA;AAGzC,QAAI,QAAQ,WAAW;AACrB,kBAAY;AACZ,gBAAU;AAAA,IACZ;AAAA,EACF;AAEA,MAAI,YAAY,MAAM,aAAa,GAAG;AACpC,WAAO,IAAI,eAAe,OAAO,KAAK;AAAA,EACxC;AAEA,SAAO,IAAI,eAAe,CAAC,KAAK;AAClC;AAGA,SAAS,mBAAmB,MAAqB,MAA6B;AAC5E,MAAI,QAAQ;AACZ,WACM,IAAI,KAAK,SAAS,GAAG,IAAI,KAAK,SAAS,GAC3C,KAAK,KAAK,KAAK,GACf,KAAK,KACL;AACA,QAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAG;AACzB;AAAA,EACF;AACA,SAAO;AACT;AAEA,eAAe,uBACb,KACA,WACA,cACc;AACd,QAAM,WAAgB;AAAA,IACpB,MAAM;AAAA,IACN,MAAM,UAAU;AAAA,IAChB,QAAQ,UAAU,UAAU;AAAA,EAAA;AAG9B,MAAI,CAAC,KAAK;AACR,WAAO;AAAA,EACT;AAEA,QAAM,WAAW,MAAM,qBAAqB,GAAG;AAC/C,MAAI,CAAC,SAAU,QAAO;AAEtB,MAAI;AACF,UAAM,OAAO,SAAS,oBAAoB;AAAA,MACxC,MAAM,UAAU;AAAA,MAChB,QAAQ,UAAU;AAAA,IAAA,CACnB;AACD,QAAI,KAAK,QAAQ,QAAQ,KAAK,UAAU,MAAM;AAC5C,aAAO;AAAA,QACL,MAAM,KAAK,SAAS,kBAAkB,KAAK,MAAM,IAAI;AAAA,QACrD,MAAM,KAAK;AAAA,QACX,QAAQ,KAAK,SAAS;AAAA,MAAA;AAAA,IAE1B;AAAA,EACF,QAAQ;AAAA,EAER;AAEA,SAAO;AACT;AAEA,MAAM,oCAAoB,QAAA;AAE1B,SAAS,eAAe,KAAkC;AACxD,SAAO;AAAA,IACL,GAAG;AAAA,IACH,MAAM,IAAI,QAAQ;AAAA,IAClB,SAAS,OAAO,IAAI,OAAO;AAAA,IAC3B,gBAAgB,IAAI,gBAAgB,IAAI,CAAC,MAAM,KAAK,EAAE,KAAK,CAAA;AAAA,EAAC;AAEhE;AAEA,eAAe,qBACb,KACmC;AACnC,QAAM,SAAS,cAAc,IAAI,GAAG;AACpC,MAAI,OAAQ,QAAO;AAEnB,QAAM,WAAW,YAAY;AAC3B,QAAI;AACF,aAAO,MAAM,IAAI,kBAAkB,eAAe,GAAG,CAAC;AAAA,IACxD,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF,GAAA;AAEA,gBAAc,IAAI,KAAK,OAAO;AAC9B,SAAO;AACT;AAQO,MAAM,eAAe;AAAA,EAClB,4BAAY,IAAA;AAAA,EACZ,mCAAmB,IAAA;AAAA,EAE3B,IAAI,KAAsB;AACxB,WAAO,KAAK,MAAM,IAAI,GAAG;AAAA,EAC3B;AAAA,EAEA,IAAI,KAAgD;AAClD,WAAO,KAAK,MAAM,IAAI,GAAG;AAAA,EAC3B;AAAA,EAEA,IAAI,KAAa,OAAoC;AACnD,SAAK,MAAM,IAAI,KAAK,KAAK;AACzB,UAAM,OAAO,IAAI,MAAM,GAAG,IAAI,QAAQ,IAAI,CAAC;AAC3C,gBAAY,KAAK,cAAc,MAAM,0BAAU,IAAA,CAAK,EAAE,IAAI,GAAG;AAAA,EAC/D;AAAA,EAEA,QAAc;AACZ,SAAK,MAAM,MAAA;AACX,SAAK,aAAa,MAAA;AAAA,EACpB;AAAA;AAAA,EAGA,aAAa,MAAoB;AAC/B,UAAM,OAAO,KAAK,aAAa,IAAI,IAAI;AACvC,QAAI,MAAM;AACR,iBAAW,OAAO,MAAM;AACtB,aAAK,MAAM,OAAO,GAAG;AAAA,MACvB;AACA,WAAK,aAAa,OAAO,IAAI;AAAA,IAC/B;AAAA,EACF;AACF;AAIA,MAAM,yCAAyB,IAAA;AAExB,SAAS,0BAAgC;AAC9C,qBAAmB,MAAA;AACrB;AAEA,SAAS,8BAA8B,MAAc,QAAwB;AAC3E,MAAI,WAAW,mBAAmB,IAAI,MAAM;AAC5C,MAAI,CAAC,UAAU;AACb,UAAM,UAAU,aAAa,MAAM;AACnC,eAAW;AAAA,MACT,IAAI,OAAO,sBAAsB,OAAO,KAAK;AAAA,MAC7C,IAAI,OAAO,oBAAoB,OAAO,KAAK;AAAA,MAC3C,IAAI,OAAO,6BAA6B,OAAO,YAAY;AAAA,IAAA;AAE7D,uBAAmB,IAAI,QAAQ,QAAQ;AAAA,EACzC;AAEA,MAAI,OAAO;AACX,aAAW,MAAM,UAAU;AACzB,UAAM,IAAI,GAAG,KAAK,IAAI;AACtB,QAAI,CAAC,EAAG;AACR,UAAM,MAAM,EAAE,QAAQ,EAAE,CAAC,EAAE,QAAQ,MAAM;AACzC,QAAI,QAAQ,GAAI;AAChB,QAAI,SAAS,MAAM,MAAM,KAAM,QAAO;AAAA,EACxC;AACA,SAAO;AACT;AAOA,eAAsB,2CACpB,UACA,YACA,QACA,gBAC0B;AAC1B,QAAM,eAAe,kBAAkB,UAAU;AACjD,QAAM,WAAW,GAAG,YAAY,KAAK,MAAM;AAC3C,MAAI,eAAe,IAAI,QAAQ,GAAG;AAChC,WAAO,eAAe,IAAI,QAAQ,KAAK;AAAA,EACzC;AAEA,MAAI;AACF,UAAM,MAAM,SAAS,mBAAmB,UAAU;AAClD,QAAI,CAAC,KAAK;AACR,qBAAe,IAAI,UAAU,IAAI;AACjC,aAAO;AAAA,IACT;AAEA,UAAM,EAAE,MAAM,IAAA,IAAQ;AAEtB,UAAM,YAAY,IAAI,aAAa,eAAe,IAAI;AAEtD,UAAM,MAAM,8BAA8B,MAAM,MAAM;AACtD,QAAI,QAAQ,IAAI;AACd,qBAAe,IAAI,UAAU,IAAI;AACjC,aAAO;AAAA,IACT;AAEA,UAAM,YAAY,wBAAwB,WAAW,GAAG;AACxD,UAAM,MAAM,MAAM,uBAAuB,KAAK,WAAW,YAAY;AACrE,mBAAe,IAAI,UAAU,GAAG;AAChC,WAAO;AAAA,EACT,QAAQ;AACN,mBAAe,IAAI,UAAU,IAAI;AACjC,WAAO;AAAA,EACT;AACF;AAOA,eAAsB,6BACpB,UACA,YACA,QACA,yBAI0B;AAC1B,MAAI;AACF,UAAM,eAAe,kBAAkB,UAAU;AACjD,UAAM,MAAM,SAAS,mBAAmB,UAAU;AAClD,QAAI,CAAC,IAAK,QAAO;AACjB,UAAM,EAAE,MAAM,IAAA,IAAQ;AAEtB,QAAI,CAAC,IAAI,WAAW;AAClB,UAAI,YAAY,eAAe,IAAI;AAAA,IACrC;AAEA,UAAM,MAAM,wBAAwB,MAAM,MAAM;AAChD,QAAI,CAAC,IAAK,QAAO;AAEjB,WAAO,MAAM,uBAAuB,KAAK,KAAK,YAAY;AAAA,EAC5D,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAMA,eAAsB,wBACpB,UACA,OAMA,gBACe;AACf,aAAW,QAAQ,OAAO;AACxB,QAAI,CAAC,KAAK,UAAW;AACrB,QAAI,KAAK,QAAQ,QAAQ,KAAK,UAAU,KAAM;AAC9C,UAAM,MAAM,MAAM;AAAA,MAChB;AAAA,MACA,KAAK;AAAA,MACL,KAAK;AAAA,MACL;AAAA,IAAA;AAEF,QAAI,CAAC,IAAK;AACV,SAAK,OAAO,IAAI;AAChB,SAAK,SAAS,IAAI;AAAA,EACpB;AACF;AAmBO,SAAS,iBACd,UACA,UACA,KACA,eAAuB,GACE;AACzB,MAAI;AACF,UAAM,eAAe,kBAAkB,QAAQ;AAC/C,UAAM,MAAM,SAAS,mBAAmB,QAAQ;AAChD,QAAI,CAAC,IAAK,QAAO;AAEjB,UAAM,EAAE,MAAM,iBAAiB,aAAA,IAAiB;AAEhD,UAAM,aAAa,gBAAgB;AACnC,UAAM,aAAa,IAAI;AACvB,UAAM,YAAY,IAAI;AAEtB,QAAI,aAAa,EAAG,QAAO;AAE3B,UAAM,YAAY,KAAK,IAAI,GAAG,aAAa,YAAY;AACvD,UAAM,UAAU,aAAa;AAG7B,QAAI,UAAU;AACd,QAAI,MAAM;AACV,WAAO,UAAU,aAAa,MAAM,WAAW,QAAQ;AACrD,YAAM,KAAK,WAAW,WAAW,GAAG;AACpC,UAAI,OAAO,IAAI;AACb;AAAA,MACF,WAAW,OAAO,IAAI;AACpB;AACA,YACE,MAAM,IAAI,WAAW,UACrB,WAAW,WAAW,MAAM,CAAC,MAAM;AAEnC;AAAA,MACJ;AACA;AAAA,IACF;AACA,QAAI,UAAU,UAAW,QAAO;AAEhC,UAAM,QAAuB,CAAA;AAC7B,QAAI,UAAU;AACd,WAAO,WAAW,WAAW,OAAO,WAAW,QAAQ;AAErD,UAAI,MAAM;AACV,aAAO,MAAM,WAAW,QAAQ;AAC9B,cAAM,KAAK,WAAW,WAAW,GAAG;AACpC,YAAI,OAAO,MAAM,OAAO,GAAI;AAC5B;AAAA,MACF;AACA,YAAM,KAAK,WAAW,MAAM,KAAK,GAAG,CAAC;AACrC;AACA,UAAI,MAAM,WAAW,QAAQ;AAC3B,YACE,WAAW,WAAW,GAAG,MAAM,MAC/B,MAAM,IAAI,WAAW,UACrB,WAAW,WAAW,MAAM,CAAC,MAAM,IACnC;AACA,gBAAM,MAAM;AAAA,QACd,OAAO;AACL,gBAAM,MAAM;AAAA,QACd;AAAA,MACF,OAAO;AACL,cAAM,MAAM;AAAA,MACd;AAAA,IACF;AAEA,QAAI,aAAa,YAAY,MAAM,SAAS,EAAG,QAAO;AAEtD,UAAM,YAAY,YAAY,MAAM,SAAS;AAC7C,UAAM,cAAc,OAAO,SAAS,EAAE;AAEtC,UAAM,aAAa,IAAI,QAAQ;AAC/B,UAAM,eAA8B,CAAA;AACpC,aAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,YAAM,KAAK,YAAY;AACvB,YAAM,cAAc,MAAM,CAAC;AAC3B,YAAM,aAAa,OAAO,EAAE,EAAE,SAAS,aAAa,GAAG;AACvD,YAAM,SAAS,OAAO,aAAa,MAAM;AACzC,mBAAa,KAAK,KAAK,MAAM,IAAI,UAAU,MAAM,WAAW,EAAE;AAE9D,UAAI,OAAO,cAAc,YAAY,GAAG;AACtC,cAAM,UAAU,IAAI,OAAO,YAAY,CAAC;AACxC,qBAAa,KAAK,OAAO,IAAI,OAAO,WAAW,CAAC,MAAM,OAAO,GAAG;AAAA,MAClE;AAAA,IACF;AAEA,WAAO;AAAA,MACL,OAAO;AAAA,MACP,eAAe;AAAA,MACf,UAAU,GAAG,UAAU,IAAI,UAAU,IAAI,SAAS;AAAA,IAAA;AAAA,EAEtD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;"}
|
|
@@ -64,4 +64,14 @@ export interface ViolationInfo {
|
|
|
64
64
|
location: string;
|
|
65
65
|
};
|
|
66
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Suggestion strings for server-only code leaking into client environments.
|
|
69
|
+
* Used by both `formatViolation` (terminal) and runtime mock modules (browser).
|
|
70
|
+
*/
|
|
71
|
+
export declare const CLIENT_ENV_SUGGESTIONS: readonly ["Use createServerFn().handler(() => ...) to keep the logic on the server and call it from the client via an RPC bridge", "Use createServerOnlyFn(() => ...) to mark it as server-only (it will throw if accidentally called from the client)", "Use createIsomorphicFn().client(() => ...).server(() => ...) to provide separate client and server implementations", "Move the server-only import out of this file into a separate .server.ts module that is not imported by any client code"];
|
|
72
|
+
/**
|
|
73
|
+
* Suggestion strings for client-only code leaking into server environments.
|
|
74
|
+
* The JSX-specific suggestion is conditionally prepended by `formatViolation`.
|
|
75
|
+
*/
|
|
76
|
+
export declare const SERVER_ENV_SUGGESTIONS: readonly ["Use createClientOnlyFn(() => ...) to mark it as client-only (returns undefined on the server)", "Use createIsomorphicFn().client(() => ...).server(() => ...) to provide separate client and server implementations", "Move the client-only import out of this file into a separate .client.ts module that is not imported by any server code"];
|
|
67
77
|
export declare function formatViolation(info: ViolationInfo, root: string): string;
|