@tanstack/start-plugin-core 1.160.2 → 1.161.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dist/esm/dev-server-plugin/plugin.js +1 -1
  2. package/dist/esm/dev-server-plugin/plugin.js.map +1 -1
  3. package/dist/esm/import-protection-plugin/defaults.d.ts +17 -0
  4. package/dist/esm/import-protection-plugin/defaults.js +36 -0
  5. package/dist/esm/import-protection-plugin/defaults.js.map +1 -0
  6. package/dist/esm/import-protection-plugin/matchers.d.ts +13 -0
  7. package/dist/esm/import-protection-plugin/matchers.js +31 -0
  8. package/dist/esm/import-protection-plugin/matchers.js.map +1 -0
  9. package/dist/esm/import-protection-plugin/plugin.d.ts +16 -0
  10. package/dist/esm/import-protection-plugin/plugin.js +699 -0
  11. package/dist/esm/import-protection-plugin/plugin.js.map +1 -0
  12. package/dist/esm/import-protection-plugin/postCompileUsage.d.ts +11 -0
  13. package/dist/esm/import-protection-plugin/postCompileUsage.js +177 -0
  14. package/dist/esm/import-protection-plugin/postCompileUsage.js.map +1 -0
  15. package/dist/esm/import-protection-plugin/rewriteDeniedImports.d.ts +27 -0
  16. package/dist/esm/import-protection-plugin/rewriteDeniedImports.js +51 -0
  17. package/dist/esm/import-protection-plugin/rewriteDeniedImports.js.map +1 -0
  18. package/dist/esm/import-protection-plugin/sourceLocation.d.ts +132 -0
  19. package/dist/esm/import-protection-plugin/sourceLocation.js +255 -0
  20. package/dist/esm/import-protection-plugin/sourceLocation.js.map +1 -0
  21. package/dist/esm/import-protection-plugin/trace.d.ts +67 -0
  22. package/dist/esm/import-protection-plugin/trace.js +204 -0
  23. package/dist/esm/import-protection-plugin/trace.js.map +1 -0
  24. package/dist/esm/import-protection-plugin/utils.d.ts +8 -0
  25. package/dist/esm/import-protection-plugin/utils.js +29 -0
  26. package/dist/esm/import-protection-plugin/utils.js.map +1 -0
  27. package/dist/esm/import-protection-plugin/virtualModules.d.ts +25 -0
  28. package/dist/esm/import-protection-plugin/virtualModules.js +235 -0
  29. package/dist/esm/import-protection-plugin/virtualModules.js.map +1 -0
  30. package/dist/esm/plugin.js +7 -0
  31. package/dist/esm/plugin.js.map +1 -1
  32. package/dist/esm/prerender.js +3 -3
  33. package/dist/esm/prerender.js.map +1 -1
  34. package/dist/esm/schema.d.ts +260 -0
  35. package/dist/esm/schema.js +35 -1
  36. package/dist/esm/schema.js.map +1 -1
  37. package/dist/esm/start-compiler-plugin/compiler.js +5 -1
  38. package/dist/esm/start-compiler-plugin/compiler.js.map +1 -1
  39. package/dist/esm/start-compiler-plugin/handleCreateServerFn.js +2 -2
  40. package/dist/esm/start-compiler-plugin/handleCreateServerFn.js.map +1 -1
  41. package/dist/esm/start-compiler-plugin/plugin.js.map +1 -1
  42. package/dist/esm/start-router-plugin/plugin.js +5 -5
  43. package/dist/esm/start-router-plugin/plugin.js.map +1 -1
  44. package/package.json +6 -3
  45. package/src/dev-server-plugin/plugin.ts +1 -1
  46. package/src/import-protection-plugin/defaults.ts +56 -0
  47. package/src/import-protection-plugin/matchers.ts +48 -0
  48. package/src/import-protection-plugin/plugin.ts +1173 -0
  49. package/src/import-protection-plugin/postCompileUsage.ts +266 -0
  50. package/src/import-protection-plugin/rewriteDeniedImports.ts +255 -0
  51. package/src/import-protection-plugin/sourceLocation.ts +524 -0
  52. package/src/import-protection-plugin/trace.ts +296 -0
  53. package/src/import-protection-plugin/utils.ts +32 -0
  54. package/src/import-protection-plugin/virtualModules.ts +300 -0
  55. package/src/plugin.ts +7 -0
  56. package/src/schema.ts +58 -0
  57. package/src/start-compiler-plugin/compiler.ts +12 -1
  58. package/src/start-compiler-plugin/plugin.ts +3 -3
@@ -0,0 +1,255 @@
1
+ import { SourceMapConsumer } from "source-map";
2
+ import * as path from "pathe";
3
+ import { normalizeFilePath } from "./utils.js";
4
+ function buildLineIndex(code) {
5
+ const offsets = [0];
6
+ for (let i = 0; i < code.length; i++) {
7
+ if (code.charCodeAt(i) === 10) {
8
+ offsets.push(i + 1);
9
+ }
10
+ }
11
+ return { offsets };
12
+ }
13
+ function upperBound(values, x) {
14
+ let lo = 0;
15
+ let hi = values.length;
16
+ while (lo < hi) {
17
+ const mid = lo + hi >> 1;
18
+ if (values[mid] <= x) lo = mid + 1;
19
+ else hi = mid;
20
+ }
21
+ return lo;
22
+ }
23
+ function indexToLineColWithIndex(lineIndex, idx) {
24
+ let line = 1;
25
+ const offsets = lineIndex.offsets;
26
+ const ub = upperBound(offsets, idx);
27
+ const lineIdx = Math.max(0, ub - 1);
28
+ line = lineIdx + 1;
29
+ const lineStart = offsets[lineIdx] ?? 0;
30
+ return { line, column0: Math.max(0, idx - lineStart) };
31
+ }
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
+ function pickOriginalCodeFromSourcesContent(map, importerFile, root) {
49
+ if (!map?.sourcesContent || map.sources.length === 0) {
50
+ return void 0;
51
+ }
52
+ const file = normalizeFilePath(importerFile);
53
+ const sourceRoot = map.sourceRoot;
54
+ let bestIdx = -1;
55
+ let bestScore = -1;
56
+ for (let i = 0; i < map.sources.length; i++) {
57
+ const content = map.sourcesContent[i];
58
+ if (typeof content !== "string") continue;
59
+ const src = map.sources[i] ?? "";
60
+ const normalizedSrc = normalizeFilePath(src);
61
+ if (normalizedSrc === file) {
62
+ return content;
63
+ }
64
+ const resolved = normalizeSourceCandidate(src, root, sourceRoot);
65
+ if (resolved === file) {
66
+ return content;
67
+ }
68
+ const score = Math.max(
69
+ suffixSegmentScore(normalizedSrc, file),
70
+ suffixSegmentScore(resolved, file)
71
+ );
72
+ if (score > bestScore) {
73
+ bestScore = score;
74
+ bestIdx = i;
75
+ }
76
+ }
77
+ if (bestIdx !== -1 && bestScore >= 1) {
78
+ const best = map.sourcesContent[bestIdx];
79
+ return typeof best === "string" ? best : void 0;
80
+ }
81
+ const fallback = map.sourcesContent[0];
82
+ return typeof fallback === "string" ? fallback : void 0;
83
+ }
84
+ async function mapGeneratedToOriginal(map, generated, fallbackFile) {
85
+ const fallback = {
86
+ file: fallbackFile,
87
+ line: generated.line,
88
+ column: generated.column0 + 1
89
+ };
90
+ if (!map) {
91
+ return fallback;
92
+ }
93
+ const consumer = await getSourceMapConsumer(map);
94
+ if (!consumer) return fallback;
95
+ try {
96
+ const orig = consumer.originalPositionFor({
97
+ line: generated.line,
98
+ column: generated.column0
99
+ });
100
+ if (orig.line != null && orig.column != null) {
101
+ return {
102
+ file: orig.source ? normalizeFilePath(orig.source) : fallbackFile,
103
+ line: orig.line,
104
+ column: orig.column + 1
105
+ };
106
+ }
107
+ } catch {
108
+ }
109
+ return fallback;
110
+ }
111
+ const consumerCache = /* @__PURE__ */ new WeakMap();
112
+ async function getSourceMapConsumer(map) {
113
+ const cached = consumerCache.get(map);
114
+ if (cached) return cached;
115
+ const promise = (async () => {
116
+ try {
117
+ return await new SourceMapConsumer(map);
118
+ } catch {
119
+ return null;
120
+ }
121
+ })();
122
+ consumerCache.set(map, promise);
123
+ return promise;
124
+ }
125
+ function findFirstImportSpecifierIndex(code, source) {
126
+ const escaped = source.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
127
+ const patterns = [
128
+ // import 'x'
129
+ new RegExp(`\\bimport\\s+(['"])${escaped}\\1`),
130
+ // import ... from 'x' / export ... from 'x'
131
+ new RegExp(`\\bfrom\\s+(['"])${escaped}\\1`),
132
+ // import('x')
133
+ new RegExp(`\\bimport\\s*\\(\\s*(['"])${escaped}\\1\\s*\\)`)
134
+ ];
135
+ let best = -1;
136
+ for (const re of patterns) {
137
+ const m = re.exec(code);
138
+ if (!m) continue;
139
+ const idx = m.index + m[0].indexOf(source);
140
+ if (idx === -1) continue;
141
+ if (best === -1 || idx < best) best = idx;
142
+ }
143
+ return best;
144
+ }
145
+ async function findImportStatementLocationFromTransformed(provider, importerId, source, importLocCache) {
146
+ const importerFile = normalizeFilePath(importerId);
147
+ const cacheKey = `${importerFile}::${source}`;
148
+ if (importLocCache.has(cacheKey)) {
149
+ return importLocCache.get(cacheKey) || void 0;
150
+ }
151
+ try {
152
+ const res = provider.getTransformResult(importerId);
153
+ if (!res) {
154
+ importLocCache.set(cacheKey, null);
155
+ return void 0;
156
+ }
157
+ const { code, map } = res;
158
+ if (typeof code !== "string") {
159
+ importLocCache.set(cacheKey, null);
160
+ return void 0;
161
+ }
162
+ const lineIndex = res.lineIndex ?? buildLineIndex(code);
163
+ const idx = findFirstImportSpecifierIndex(code, source);
164
+ if (idx === -1) {
165
+ importLocCache.set(cacheKey, null);
166
+ return void 0;
167
+ }
168
+ const generated = indexToLineColWithIndex(lineIndex, idx);
169
+ const loc = await mapGeneratedToOriginal(map, generated, importerFile);
170
+ importLocCache.set(cacheKey, loc);
171
+ return loc;
172
+ } catch {
173
+ importLocCache.set(cacheKey, null);
174
+ return void 0;
175
+ }
176
+ }
177
+ async function findPostCompileUsageLocation(provider, importerId, source, findPostCompileUsagePos) {
178
+ try {
179
+ const importerFile = normalizeFilePath(importerId);
180
+ const res = provider.getTransformResult(importerId);
181
+ if (!res) return void 0;
182
+ const { code, map } = res;
183
+ if (typeof code !== "string") return void 0;
184
+ if (!res.lineIndex) {
185
+ res.lineIndex = buildLineIndex(code);
186
+ }
187
+ const pos = findPostCompileUsagePos(code, source);
188
+ if (!pos) return void 0;
189
+ return await mapGeneratedToOriginal(map, pos, importerFile);
190
+ } catch {
191
+ return void 0;
192
+ }
193
+ }
194
+ async function addTraceImportLocations(provider, trace, importLocCache) {
195
+ for (const step of trace) {
196
+ if (!step.specifier) continue;
197
+ if (step.line != null && step.column != null) continue;
198
+ const loc = await findImportStatementLocationFromTransformed(
199
+ provider,
200
+ step.file,
201
+ step.specifier,
202
+ importLocCache
203
+ );
204
+ if (!loc) continue;
205
+ step.line = loc.line;
206
+ step.column = loc.column;
207
+ }
208
+ }
209
+ function buildCodeSnippet(provider, moduleId, loc, contextLines = 2) {
210
+ try {
211
+ const importerFile = normalizeFilePath(moduleId);
212
+ const res = provider.getTransformResult(moduleId);
213
+ if (!res) return void 0;
214
+ const { code: transformedCode, originalCode } = res;
215
+ if (typeof transformedCode !== "string") return void 0;
216
+ const sourceCode = originalCode ?? transformedCode;
217
+ const allLines = sourceCode.split(/\r?\n/);
218
+ const targetLine = loc.line;
219
+ const targetCol = loc.column;
220
+ if (targetLine < 1 || targetLine > allLines.length) return void 0;
221
+ const startLine = Math.max(1, targetLine - contextLines);
222
+ const endLine = Math.min(allLines.length, targetLine + contextLines);
223
+ const gutterWidth = String(endLine).length;
224
+ const sourceFile = loc.file ?? importerFile;
225
+ const snippetLines = [];
226
+ for (let i = startLine; i <= endLine; i++) {
227
+ const lineContent = allLines[i - 1] ?? "";
228
+ const lineNum = String(i).padStart(gutterWidth, " ");
229
+ const marker = i === targetLine ? ">" : " ";
230
+ snippetLines.push(` ${marker} ${lineNum} | ${lineContent}`);
231
+ if (i === targetLine && targetCol > 0) {
232
+ const padding = " ".repeat(targetCol - 1);
233
+ snippetLines.push(` ${" ".repeat(gutterWidth)} | ${padding}^`);
234
+ }
235
+ }
236
+ return {
237
+ lines: snippetLines,
238
+ highlightLine: targetLine,
239
+ location: `${sourceFile}:${targetLine}:${targetCol}`
240
+ };
241
+ } catch {
242
+ return void 0;
243
+ }
244
+ }
245
+ export {
246
+ addTraceImportLocations,
247
+ buildCodeSnippet,
248
+ buildLineIndex,
249
+ findFirstImportSpecifierIndex,
250
+ findImportStatementLocationFromTransformed,
251
+ findPostCompileUsageLocation,
252
+ mapGeneratedToOriginal,
253
+ pickOriginalCodeFromSourcesContent
254
+ };
255
+ //# sourceMappingURL=sourceLocation.js.map
@@ -0,0 +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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"}
@@ -0,0 +1,67 @@
1
+ export interface TraceEdge {
2
+ importer: string;
3
+ specifier?: string;
4
+ }
5
+ /**
6
+ * Per-environment reverse import graph.
7
+ * Maps a resolved module id to the set of modules that import it.
8
+ */
9
+ export declare class ImportGraph {
10
+ /**
11
+ * resolvedId -> Map<importer, specifier>
12
+ *
13
+ * We use a Map instead of a Set of objects so edges dedupe correctly.
14
+ */
15
+ readonly reverseEdges: Map<string, Map<string, string | undefined>>;
16
+ /**
17
+ * Forward-edge index: importer -> Set<resolvedId>.
18
+ *
19
+ * Maintained alongside reverseEdges so that {@link invalidate} can remove
20
+ * all outgoing edges for a file in O(outgoing-edges) instead of scanning
21
+ * every reverse-edge map in the graph.
22
+ */
23
+ private readonly forwardEdges;
24
+ readonly entries: Set<string>;
25
+ addEdge(resolved: string, importer: string, specifier?: string): void;
26
+ /** Convenience for tests/debugging. */
27
+ getEdges(resolved: string): Set<TraceEdge> | undefined;
28
+ addEntry(id: string): void;
29
+ clear(): void;
30
+ invalidate(id: string): void;
31
+ }
32
+ export interface TraceStep {
33
+ file: string;
34
+ specifier?: string;
35
+ line?: number;
36
+ column?: number;
37
+ }
38
+ export interface Loc {
39
+ file?: string;
40
+ line: number;
41
+ column: number;
42
+ }
43
+ /**
44
+ * BFS from a node upward through reverse edges to find the shortest
45
+ * path to an entry module.
46
+ */
47
+ export declare function buildTrace(graph: ImportGraph, startNode: string, maxDepth?: number): Array<TraceStep>;
48
+ export interface ViolationInfo {
49
+ env: string;
50
+ envType: 'client' | 'server';
51
+ type: 'specifier' | 'file' | 'marker';
52
+ behavior: 'error' | 'mock';
53
+ pattern?: string | RegExp;
54
+ specifier: string;
55
+ importer: string;
56
+ importerLoc?: Loc;
57
+ resolved?: string;
58
+ trace: Array<TraceStep>;
59
+ message: string;
60
+ /** Vitest-style code snippet showing the offending usage in the leaf module. */
61
+ snippet?: {
62
+ lines: Array<string>;
63
+ highlightLine: number;
64
+ location: string;
65
+ };
66
+ }
67
+ export declare function formatViolation(info: ViolationInfo, root: string): string;
@@ -0,0 +1,204 @@
1
+ import * as path from "pathe";
2
+ class ImportGraph {
3
+ /**
4
+ * resolvedId -> Map<importer, specifier>
5
+ *
6
+ * We use a Map instead of a Set of objects so edges dedupe correctly.
7
+ */
8
+ reverseEdges = /* @__PURE__ */ new Map();
9
+ /**
10
+ * Forward-edge index: importer -> Set<resolvedId>.
11
+ *
12
+ * Maintained alongside reverseEdges so that {@link invalidate} can remove
13
+ * all outgoing edges for a file in O(outgoing-edges) instead of scanning
14
+ * every reverse-edge map in the graph.
15
+ */
16
+ forwardEdges = /* @__PURE__ */ new Map();
17
+ entries = /* @__PURE__ */ new Set();
18
+ addEdge(resolved, importer, specifier) {
19
+ let importers = this.reverseEdges.get(resolved);
20
+ if (!importers) {
21
+ importers = /* @__PURE__ */ new Map();
22
+ this.reverseEdges.set(resolved, importers);
23
+ }
24
+ importers.set(importer, specifier);
25
+ let targets = this.forwardEdges.get(importer);
26
+ if (!targets) {
27
+ targets = /* @__PURE__ */ new Set();
28
+ this.forwardEdges.set(importer, targets);
29
+ }
30
+ targets.add(resolved);
31
+ }
32
+ /** Convenience for tests/debugging. */
33
+ getEdges(resolved) {
34
+ const importers = this.reverseEdges.get(resolved);
35
+ if (!importers) return void 0;
36
+ const out = /* @__PURE__ */ new Set();
37
+ for (const [importer, specifier] of importers) {
38
+ out.add({ importer, specifier });
39
+ }
40
+ return out;
41
+ }
42
+ addEntry(id) {
43
+ this.entries.add(id);
44
+ }
45
+ clear() {
46
+ this.reverseEdges.clear();
47
+ this.forwardEdges.clear();
48
+ this.entries.clear();
49
+ }
50
+ invalidate(id) {
51
+ const targets = this.forwardEdges.get(id);
52
+ if (targets) {
53
+ for (const resolved of targets) {
54
+ this.reverseEdges.get(resolved)?.delete(id);
55
+ }
56
+ this.forwardEdges.delete(id);
57
+ }
58
+ this.reverseEdges.delete(id);
59
+ }
60
+ }
61
+ function buildTrace(graph, startNode, maxDepth = 20) {
62
+ const visited = /* @__PURE__ */ new Set([startNode]);
63
+ const depthByNode = /* @__PURE__ */ new Map([[startNode, 0]]);
64
+ const down = /* @__PURE__ */ new Map();
65
+ const queue = [startNode];
66
+ let queueIndex = 0;
67
+ let root = null;
68
+ while (queueIndex < queue.length) {
69
+ const node = queue[queueIndex++];
70
+ const depth = depthByNode.get(node) ?? 0;
71
+ const importers = graph.reverseEdges.get(node);
72
+ if (node !== startNode) {
73
+ const isEntry = graph.entries.has(node) || !importers || importers.size === 0;
74
+ if (isEntry) {
75
+ root = node;
76
+ break;
77
+ }
78
+ }
79
+ if (depth >= maxDepth) {
80
+ continue;
81
+ }
82
+ if (!importers || importers.size === 0) {
83
+ continue;
84
+ }
85
+ for (const [importer, specifier] of importers) {
86
+ if (visited.has(importer)) continue;
87
+ visited.add(importer);
88
+ depthByNode.set(importer, depth + 1);
89
+ down.set(importer, { next: node, specifier });
90
+ queue.push(importer);
91
+ }
92
+ }
93
+ if (!root) {
94
+ root = startNode;
95
+ }
96
+ const trace = [];
97
+ let current = root;
98
+ for (let i = 0; i <= maxDepth + 1; i++) {
99
+ const link = down.get(current);
100
+ trace.push({ file: current, specifier: link?.specifier });
101
+ if (!link) break;
102
+ current = link.next;
103
+ }
104
+ return trace;
105
+ }
106
+ function formatViolation(info, root) {
107
+ const rel = (p) => {
108
+ if (p.startsWith(root)) {
109
+ return path.relative(root, p);
110
+ }
111
+ return p;
112
+ };
113
+ const relLoc = (p, loc) => {
114
+ const r = rel(p);
115
+ const file = loc?.file ? rel(loc.file) : r;
116
+ return loc ? `${file}:${loc.line}:${loc.column}` : r;
117
+ };
118
+ const relTraceStep = (step) => {
119
+ const file = rel(step.file);
120
+ if (step.line == null) return file;
121
+ const col = step.column ?? 1;
122
+ return `${file}:${step.line}:${col}`;
123
+ };
124
+ const lines = [];
125
+ lines.push(``);
126
+ lines.push(`[import-protection] Import denied in ${info.envType} environment`);
127
+ lines.push(``);
128
+ if (info.type === "specifier") {
129
+ lines.push(` Denied by specifier pattern: ${String(info.pattern)}`);
130
+ } else if (info.type === "file") {
131
+ lines.push(` Denied by file pattern: ${String(info.pattern)}`);
132
+ } else {
133
+ lines.push(
134
+ ` Denied by marker: module is restricted to the opposite environment`
135
+ );
136
+ }
137
+ lines.push(` Importer: ${relLoc(info.importer, info.importerLoc)}`);
138
+ lines.push(` Import: "${rel(info.specifier)}"`);
139
+ if (info.resolved) {
140
+ lines.push(` Resolved: ${rel(info.resolved)}`);
141
+ }
142
+ if (info.trace.length > 0) {
143
+ lines.push(``);
144
+ lines.push(` Trace:`);
145
+ for (let i = 0; i < info.trace.length; i++) {
146
+ const step = info.trace[i];
147
+ const isEntry = i === 0;
148
+ const tag = isEntry ? " (entry)" : "";
149
+ const spec = step.specifier ? ` (import "${rel(step.specifier)}")` : "";
150
+ lines.push(` ${i + 1}. ${relTraceStep(step)}${tag}${spec}`);
151
+ }
152
+ }
153
+ if (info.snippet) {
154
+ lines.push(``);
155
+ lines.push(` Code:`);
156
+ for (const snippetLine of info.snippet.lines) {
157
+ lines.push(snippetLine);
158
+ }
159
+ lines.push(``);
160
+ lines.push(` ${rel(info.snippet.location)}`);
161
+ }
162
+ lines.push(``);
163
+ if (info.envType === "client") {
164
+ lines.push(` Suggestions:`);
165
+ lines.push(
166
+ ` - Use createServerFn().handler(() => ...) to keep the logic on the server and call it from the client via an RPC bridge`
167
+ );
168
+ lines.push(
169
+ ` - Use createServerOnlyFn(() => ...) to mark it as server-only (it will throw if accidentally called from the client)`
170
+ );
171
+ lines.push(
172
+ ` - Use createIsomorphicFn().client(() => ...).server(() => ...) to provide separate client and server implementations`
173
+ );
174
+ lines.push(
175
+ ` - Move the server-only import out of this file into a separate .server.ts module that is not imported by any client code`
176
+ );
177
+ } else {
178
+ const snippetText = info.snippet?.lines.join("\n") ?? "";
179
+ const looksLikeJsx = /<[A-Z]/.test(snippetText) || /\{.*\(.*\).*\}/.test(snippetText) && /</.test(snippetText);
180
+ lines.push(` Suggestions:`);
181
+ if (looksLikeJsx) {
182
+ lines.push(
183
+ ` - Wrap the JSX in <ClientOnly fallback={<Loading />}>...</ClientOnly> so it only renders in the browser after hydration`
184
+ );
185
+ }
186
+ lines.push(
187
+ ` - Use createClientOnlyFn(() => ...) to mark it as client-only (returns undefined on the server)`
188
+ );
189
+ lines.push(
190
+ ` - Use createIsomorphicFn().client(() => ...).server(() => ...) to provide separate client and server implementations`
191
+ );
192
+ lines.push(
193
+ ` - Move the client-only import out of this file into a separate .client.ts module that is not imported by any server code`
194
+ );
195
+ }
196
+ lines.push(``);
197
+ return lines.join("\n");
198
+ }
199
+ export {
200
+ ImportGraph,
201
+ buildTrace,
202
+ formatViolation
203
+ };
204
+ //# sourceMappingURL=trace.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trace.js","sources":["../../../src/import-protection-plugin/trace.ts"],"sourcesContent":["import * as path from 'pathe'\n\nexport interface TraceEdge {\n importer: string\n specifier?: string\n}\n\n/**\n * Per-environment reverse import graph.\n * Maps a resolved module id to the set of modules that import it.\n */\nexport class ImportGraph {\n /**\n * resolvedId -> Map<importer, specifier>\n *\n * We use a Map instead of a Set of objects so edges dedupe correctly.\n */\n readonly reverseEdges: Map<string, Map<string, string | undefined>> =\n new Map()\n\n /**\n * Forward-edge index: importer -> Set<resolvedId>.\n *\n * Maintained alongside reverseEdges so that {@link invalidate} can remove\n * all outgoing edges for a file in O(outgoing-edges) instead of scanning\n * every reverse-edge map in the graph.\n */\n private readonly forwardEdges: Map<string, Set<string>> = new Map()\n\n readonly entries: Set<string> = new Set()\n\n addEdge(resolved: string, importer: string, specifier?: string): void {\n let importers = this.reverseEdges.get(resolved)\n if (!importers) {\n importers = new Map()\n this.reverseEdges.set(resolved, importers)\n }\n // Last writer wins; good enough for trace display.\n importers.set(importer, specifier)\n\n // Maintain forward index\n let targets = this.forwardEdges.get(importer)\n if (!targets) {\n targets = new Set()\n this.forwardEdges.set(importer, targets)\n }\n targets.add(resolved)\n }\n\n /** Convenience for tests/debugging. */\n getEdges(resolved: string): Set<TraceEdge> | undefined {\n const importers = this.reverseEdges.get(resolved)\n if (!importers) return undefined\n const out = new Set<TraceEdge>()\n for (const [importer, specifier] of importers) {\n out.add({ importer, specifier })\n }\n return out\n }\n\n addEntry(id: string): void {\n this.entries.add(id)\n }\n\n clear(): void {\n this.reverseEdges.clear()\n this.forwardEdges.clear()\n this.entries.clear()\n }\n\n invalidate(id: string): void {\n // Remove all outgoing edges (id as importer) using the forward index.\n const targets = this.forwardEdges.get(id)\n if (targets) {\n for (const resolved of targets) {\n this.reverseEdges.get(resolved)?.delete(id)\n }\n this.forwardEdges.delete(id)\n }\n // Remove as a target (id as resolved module)\n this.reverseEdges.delete(id)\n }\n}\n\nexport interface TraceStep {\n file: string\n specifier?: string\n line?: number\n column?: number\n}\n\nexport interface Loc {\n file?: string\n line: number\n column: number\n}\n\n/**\n * BFS from a node upward through reverse edges to find the shortest\n * path to an entry module.\n */\nexport function buildTrace(\n graph: ImportGraph,\n startNode: string,\n maxDepth: number = 20,\n): Array<TraceStep> {\n // BFS upward (startNode -> importers -> ...)\n const visited = new Set<string>([startNode])\n const depthByNode = new Map<string, number>([[startNode, 0]])\n\n // For any importer we visit, store the \"down\" link back toward startNode.\n // importer --(specifier)--> next\n const down = new Map<string, { next: string; specifier?: string }>()\n\n const queue: Array<string> = [startNode]\n let queueIndex = 0\n\n let root: string | null = null\n\n while (queueIndex < queue.length) {\n const node = queue[queueIndex++]!\n const depth = depthByNode.get(node) ?? 0\n const importers = graph.reverseEdges.get(node)\n\n if (node !== startNode) {\n const isEntry =\n graph.entries.has(node) || !importers || importers.size === 0\n if (isEntry) {\n root = node\n break\n }\n }\n\n if (depth >= maxDepth) {\n continue\n }\n\n if (!importers || importers.size === 0) {\n continue\n }\n\n for (const [importer, specifier] of importers) {\n if (visited.has(importer)) continue\n visited.add(importer)\n depthByNode.set(importer, depth + 1)\n down.set(importer, { next: node, specifier })\n queue.push(importer)\n }\n }\n\n // Best-effort: if we never found a root, just start from the original node.\n if (!root) {\n root = startNode\n }\n\n const trace: Array<TraceStep> = []\n let current = root\n for (let i = 0; i <= maxDepth + 1; i++) {\n const link = down.get(current)\n trace.push({ file: current, specifier: link?.specifier })\n if (!link) break\n current = link.next\n }\n\n return trace\n}\n\nexport interface ViolationInfo {\n env: string\n envType: 'client' | 'server'\n type: 'specifier' | 'file' | 'marker'\n behavior: 'error' | 'mock'\n pattern?: string | RegExp\n specifier: string\n importer: string\n importerLoc?: Loc\n resolved?: string\n trace: Array<TraceStep>\n message: string\n /** Vitest-style code snippet showing the offending usage in the leaf module. */\n snippet?: {\n lines: Array<string>\n highlightLine: number\n location: string\n }\n}\n\nexport function formatViolation(info: ViolationInfo, root: string): string {\n const rel = (p: string) => {\n if (p.startsWith(root)) {\n return path.relative(root, p)\n }\n return p\n }\n\n const relLoc = (p: string, loc?: Loc) => {\n const r = rel(p)\n const file = loc?.file ? rel(loc.file) : r\n return loc ? `${file}:${loc.line}:${loc.column}` : r\n }\n\n const relTraceStep = (step: TraceStep): string => {\n const file = rel(step.file)\n if (step.line == null) return file\n const col = step.column ?? 1\n return `${file}:${step.line}:${col}`\n }\n\n const lines: Array<string> = []\n lines.push(``)\n lines.push(`[import-protection] Import denied in ${info.envType} environment`)\n lines.push(``)\n\n if (info.type === 'specifier') {\n lines.push(` Denied by specifier pattern: ${String(info.pattern)}`)\n } else if (info.type === 'file') {\n lines.push(` Denied by file pattern: ${String(info.pattern)}`)\n } else {\n lines.push(\n ` Denied by marker: module is restricted to the opposite environment`,\n )\n }\n\n lines.push(` Importer: ${relLoc(info.importer, info.importerLoc)}`)\n lines.push(` Import: \"${rel(info.specifier)}\"`)\n if (info.resolved) {\n lines.push(` Resolved: ${rel(info.resolved)}`)\n }\n\n if (info.trace.length > 0) {\n lines.push(``)\n lines.push(` Trace:`)\n for (let i = 0; i < info.trace.length; i++) {\n const step = info.trace[i]!\n const isEntry = i === 0\n const tag = isEntry ? ' (entry)' : ''\n const spec = step.specifier ? ` (import \"${rel(step.specifier)}\")` : ''\n lines.push(` ${i + 1}. ${relTraceStep(step)}${tag}${spec}`)\n }\n }\n\n if (info.snippet) {\n lines.push(``)\n lines.push(` Code:`)\n for (const snippetLine of info.snippet.lines) {\n lines.push(snippetLine)\n }\n lines.push(``)\n lines.push(` ${rel(info.snippet.location)}`)\n }\n\n lines.push(``)\n\n // Add suggestions\n if (info.envType === 'client') {\n // Server-only code leaking into the client environment\n lines.push(` Suggestions:`)\n lines.push(\n ` - Use createServerFn().handler(() => ...) to keep the logic on the server and call it from the client via an RPC bridge`,\n )\n lines.push(\n ` - Use createServerOnlyFn(() => ...) to mark it as server-only (it will throw if accidentally called from the client)`,\n )\n lines.push(\n ` - Use createIsomorphicFn().client(() => ...).server(() => ...) to provide separate client and server implementations`,\n )\n lines.push(\n ` - Move the server-only import out of this file into a separate .server.ts module that is not imported by any client code`,\n )\n } else {\n // Client-only code leaking into the server environment\n const snippetText = info.snippet?.lines.join('\\n') ?? ''\n const looksLikeJsx =\n /<[A-Z]/.test(snippetText) ||\n (/\\{.*\\(.*\\).*\\}/.test(snippetText) && /</.test(snippetText))\n\n lines.push(` Suggestions:`)\n if (looksLikeJsx) {\n lines.push(\n ` - Wrap the JSX in <ClientOnly fallback={<Loading />}>...</ClientOnly> so it only renders in the browser after hydration`,\n )\n }\n lines.push(\n ` - Use createClientOnlyFn(() => ...) to mark it as client-only (returns undefined on the server)`,\n )\n lines.push(\n ` - Use createIsomorphicFn().client(() => ...).server(() => ...) to provide separate client and server implementations`,\n )\n lines.push(\n ` - Move the client-only import out of this file into a separate .client.ts module that is not imported by any server code`,\n )\n }\n\n lines.push(``)\n return lines.join('\\n')\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ export type Pattern = string | RegExp;
2
+ export declare function dedupePatterns(patterns: Array<Pattern>): Array<Pattern>;
3
+ export declare function stripViteQuery(id: string): string;
4
+ /**
5
+ * Strip Vite query parameters and normalize the path in one step.
6
+ * Replaces the repeated `normalizePath(stripViteQuery(id))` pattern.
7
+ */
8
+ export declare function normalizeFilePath(id: string): string;
@@ -0,0 +1,29 @@
1
+ import { normalizePath } from "vite";
2
+ function dedupePatterns(patterns) {
3
+ const out = [];
4
+ const seen = /* @__PURE__ */ new Set();
5
+ for (const p of patterns) {
6
+ const key = typeof p === "string" ? `s:${p}` : `r:${p.toString()}`;
7
+ if (seen.has(key)) continue;
8
+ seen.add(key);
9
+ out.push(p);
10
+ }
11
+ return out;
12
+ }
13
+ function stripViteQuery(id) {
14
+ const q = id.indexOf("?");
15
+ const h = id.indexOf("#");
16
+ if (q === -1 && h === -1) return id;
17
+ if (q === -1) return id.slice(0, h);
18
+ if (h === -1) return id.slice(0, q);
19
+ return id.slice(0, Math.min(q, h));
20
+ }
21
+ function normalizeFilePath(id) {
22
+ return normalizePath(stripViteQuery(id));
23
+ }
24
+ export {
25
+ dedupePatterns,
26
+ normalizeFilePath,
27
+ stripViteQuery
28
+ };
29
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","sources":["../../../src/import-protection-plugin/utils.ts"],"sourcesContent":["import { normalizePath } from 'vite'\n\nexport type Pattern = string | RegExp\n\nexport function dedupePatterns(patterns: Array<Pattern>): Array<Pattern> {\n const out: Array<Pattern> = []\n const seen = new Set<string>()\n for (const p of patterns) {\n const key = typeof p === 'string' ? `s:${p}` : `r:${p.toString()}`\n if (seen.has(key)) continue\n seen.add(key)\n out.push(p)\n }\n return out\n}\n\nexport function stripViteQuery(id: string): string {\n const q = id.indexOf('?')\n const h = id.indexOf('#')\n if (q === -1 && h === -1) return id\n if (q === -1) return id.slice(0, h)\n if (h === -1) return id.slice(0, q)\n return id.slice(0, Math.min(q, h))\n}\n\n/**\n * Strip Vite query parameters and normalize the path in one step.\n * Replaces the repeated `normalizePath(stripViteQuery(id))` pattern.\n */\nexport function normalizeFilePath(id: string): string {\n return normalizePath(stripViteQuery(id))\n}\n"],"names":[],"mappings":";AAIO,SAAS,eAAe,UAA0C;AACvE,QAAM,MAAsB,CAAA;AAC5B,QAAM,2BAAW,IAAA;AACjB,aAAW,KAAK,UAAU;AACxB,UAAM,MAAM,OAAO,MAAM,WAAW,KAAK,CAAC,KAAK,KAAK,EAAE,SAAA,CAAU;AAChE,QAAI,KAAK,IAAI,GAAG,EAAG;AACnB,SAAK,IAAI,GAAG;AACZ,QAAI,KAAK,CAAC;AAAA,EACZ;AACA,SAAO;AACT;AAEO,SAAS,eAAe,IAAoB;AACjD,QAAM,IAAI,GAAG,QAAQ,GAAG;AACxB,QAAM,IAAI,GAAG,QAAQ,GAAG;AACxB,MAAI,MAAM,MAAM,MAAM,GAAI,QAAO;AACjC,MAAI,MAAM,GAAI,QAAO,GAAG,MAAM,GAAG,CAAC;AAClC,MAAI,MAAM,GAAI,QAAO,GAAG,MAAM,GAAG,CAAC;AAClC,SAAO,GAAG,MAAM,GAAG,KAAK,IAAI,GAAG,CAAC,CAAC;AACnC;AAMO,SAAS,kBAAkB,IAAoB;AACpD,SAAO,cAAc,eAAe,EAAE,CAAC;AACzC;"}