@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.
- package/dist/esm/dev-server-plugin/plugin.js +1 -1
- package/dist/esm/dev-server-plugin/plugin.js.map +1 -1
- package/dist/esm/import-protection-plugin/defaults.d.ts +17 -0
- package/dist/esm/import-protection-plugin/defaults.js +36 -0
- package/dist/esm/import-protection-plugin/defaults.js.map +1 -0
- package/dist/esm/import-protection-plugin/matchers.d.ts +13 -0
- package/dist/esm/import-protection-plugin/matchers.js +31 -0
- package/dist/esm/import-protection-plugin/matchers.js.map +1 -0
- package/dist/esm/import-protection-plugin/plugin.d.ts +16 -0
- package/dist/esm/import-protection-plugin/plugin.js +699 -0
- package/dist/esm/import-protection-plugin/plugin.js.map +1 -0
- package/dist/esm/import-protection-plugin/postCompileUsage.d.ts +11 -0
- package/dist/esm/import-protection-plugin/postCompileUsage.js +177 -0
- package/dist/esm/import-protection-plugin/postCompileUsage.js.map +1 -0
- package/dist/esm/import-protection-plugin/rewriteDeniedImports.d.ts +27 -0
- package/dist/esm/import-protection-plugin/rewriteDeniedImports.js +51 -0
- package/dist/esm/import-protection-plugin/rewriteDeniedImports.js.map +1 -0
- package/dist/esm/import-protection-plugin/sourceLocation.d.ts +132 -0
- package/dist/esm/import-protection-plugin/sourceLocation.js +255 -0
- package/dist/esm/import-protection-plugin/sourceLocation.js.map +1 -0
- package/dist/esm/import-protection-plugin/trace.d.ts +67 -0
- package/dist/esm/import-protection-plugin/trace.js +204 -0
- package/dist/esm/import-protection-plugin/trace.js.map +1 -0
- package/dist/esm/import-protection-plugin/utils.d.ts +8 -0
- package/dist/esm/import-protection-plugin/utils.js +29 -0
- package/dist/esm/import-protection-plugin/utils.js.map +1 -0
- package/dist/esm/import-protection-plugin/virtualModules.d.ts +25 -0
- package/dist/esm/import-protection-plugin/virtualModules.js +235 -0
- package/dist/esm/import-protection-plugin/virtualModules.js.map +1 -0
- package/dist/esm/plugin.js +7 -0
- package/dist/esm/plugin.js.map +1 -1
- package/dist/esm/prerender.js +3 -3
- package/dist/esm/prerender.js.map +1 -1
- package/dist/esm/schema.d.ts +260 -0
- package/dist/esm/schema.js +35 -1
- package/dist/esm/schema.js.map +1 -1
- package/dist/esm/start-compiler-plugin/compiler.js +5 -1
- package/dist/esm/start-compiler-plugin/compiler.js.map +1 -1
- package/dist/esm/start-compiler-plugin/handleCreateServerFn.js +2 -2
- package/dist/esm/start-compiler-plugin/handleCreateServerFn.js.map +1 -1
- package/dist/esm/start-compiler-plugin/plugin.js.map +1 -1
- package/dist/esm/start-router-plugin/plugin.js +5 -5
- package/dist/esm/start-router-plugin/plugin.js.map +1 -1
- package/package.json +6 -3
- package/src/dev-server-plugin/plugin.ts +1 -1
- package/src/import-protection-plugin/defaults.ts +56 -0
- package/src/import-protection-plugin/matchers.ts +48 -0
- package/src/import-protection-plugin/plugin.ts +1173 -0
- package/src/import-protection-plugin/postCompileUsage.ts +266 -0
- package/src/import-protection-plugin/rewriteDeniedImports.ts +255 -0
- package/src/import-protection-plugin/sourceLocation.ts +524 -0
- package/src/import-protection-plugin/trace.ts +296 -0
- package/src/import-protection-plugin/utils.ts +32 -0
- package/src/import-protection-plugin/virtualModules.ts +300 -0
- package/src/plugin.ts +7 -0
- package/src/schema.ts +58 -0
- package/src/start-compiler-plugin/compiler.ts +12 -1
- package/src/start-compiler-plugin/plugin.ts +3 -3
|
@@ -0,0 +1,699 @@
|
|
|
1
|
+
import * as path from "pathe";
|
|
2
|
+
import { normalizePath } from "vite";
|
|
3
|
+
import { resolveViteId } from "../utils.js";
|
|
4
|
+
import { VITE_ENVIRONMENT_NAMES } from "../constants.js";
|
|
5
|
+
import { formatViolation, ImportGraph, buildTrace } from "./trace.js";
|
|
6
|
+
import { getDefaultImportProtectionRules, getMarkerSpecifiers } from "./defaults.js";
|
|
7
|
+
import { findPostCompileUsagePos } from "./postCompileUsage.js";
|
|
8
|
+
import { matchesAny, compileMatchers } from "./matchers.js";
|
|
9
|
+
import { normalizeFilePath, dedupePatterns } from "./utils.js";
|
|
10
|
+
import { collectMockExportNamesBySource } from "./rewriteDeniedImports.js";
|
|
11
|
+
import { RESOLVED_MOCK_MODULE_ID, loadSilentMockModule, RESOLVED_MOCK_EDGE_PREFIX, loadMockEdgeModule, RESOLVED_MOCK_RUNTIME_PREFIX, loadMockRuntimeModule, RESOLVED_MARKER_PREFIX, loadMarkerModule, MOCK_MODULE_ID, MOCK_EDGE_PREFIX, MOCK_RUNTIME_PREFIX, MARKER_PREFIX, mockRuntimeModuleIdFromViolation, makeMockEdgeModuleId } from "./virtualModules.js";
|
|
12
|
+
import { pickOriginalCodeFromSourcesContent, buildLineIndex, addTraceImportLocations, findPostCompileUsageLocation, findImportStatementLocationFromTransformed, buildCodeSnippet } from "./sourceLocation.js";
|
|
13
|
+
function importProtectionPlugin(opts) {
|
|
14
|
+
const config = {
|
|
15
|
+
enabled: true,
|
|
16
|
+
root: "",
|
|
17
|
+
command: "build",
|
|
18
|
+
srcDirectory: "",
|
|
19
|
+
framework: opts.framework,
|
|
20
|
+
effectiveBehavior: "error",
|
|
21
|
+
mockAccess: "error",
|
|
22
|
+
logMode: "once",
|
|
23
|
+
maxTraceDepth: 20,
|
|
24
|
+
compiledRules: {
|
|
25
|
+
client: { specifiers: [], files: [] },
|
|
26
|
+
server: { specifiers: [], files: [] }
|
|
27
|
+
},
|
|
28
|
+
includeMatchers: [],
|
|
29
|
+
excludeMatchers: [],
|
|
30
|
+
ignoreImporterMatchers: [],
|
|
31
|
+
markerSpecifiers: { serverOnly: /* @__PURE__ */ new Set(), clientOnly: /* @__PURE__ */ new Set() },
|
|
32
|
+
envTypeMap: new Map(opts.environments.map((e) => [e.name, e.type])),
|
|
33
|
+
onViolation: void 0
|
|
34
|
+
};
|
|
35
|
+
const envStates = /* @__PURE__ */ new Map();
|
|
36
|
+
const shared = { fileMarkerKind: /* @__PURE__ */ new Map() };
|
|
37
|
+
function createImportLocCache(env) {
|
|
38
|
+
const cache = /* @__PURE__ */ new Map();
|
|
39
|
+
const originalSet = cache.set.bind(cache);
|
|
40
|
+
cache.set = function(key, value) {
|
|
41
|
+
originalSet(key, value);
|
|
42
|
+
const sepIdx = key.indexOf("::");
|
|
43
|
+
if (sepIdx !== -1) {
|
|
44
|
+
const file = key.slice(0, sepIdx);
|
|
45
|
+
let fileKeys = env.importLocByFile.get(file);
|
|
46
|
+
if (!fileKeys) {
|
|
47
|
+
fileKeys = /* @__PURE__ */ new Set();
|
|
48
|
+
env.importLocByFile.set(file, fileKeys);
|
|
49
|
+
}
|
|
50
|
+
fileKeys.add(key);
|
|
51
|
+
}
|
|
52
|
+
return this;
|
|
53
|
+
};
|
|
54
|
+
return cache;
|
|
55
|
+
}
|
|
56
|
+
function getMockEdgeExports(env, importerId, source) {
|
|
57
|
+
const importerFile = normalizeFilePath(importerId);
|
|
58
|
+
return env.mockExportsByImporter.get(importerFile)?.get(source) ?? [];
|
|
59
|
+
}
|
|
60
|
+
function getMarkerKindForFile(fileId) {
|
|
61
|
+
const file = normalizeFilePath(fileId);
|
|
62
|
+
return shared.fileMarkerKind.get(file);
|
|
63
|
+
}
|
|
64
|
+
function getTransformResultProvider(env) {
|
|
65
|
+
return {
|
|
66
|
+
getTransformResult(id) {
|
|
67
|
+
const fullKey = normalizePath(id);
|
|
68
|
+
const exact = env.transformResultCache.get(fullKey);
|
|
69
|
+
if (exact) return exact;
|
|
70
|
+
const strippedKey = normalizeFilePath(id);
|
|
71
|
+
return strippedKey !== fullKey ? env.transformResultCache.get(strippedKey) : void 0;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
async function buildViolationInfo(provider, env, envName, envType, importer, normalizedImporter, source, overrides) {
|
|
76
|
+
const trace = buildTrace(
|
|
77
|
+
env.graph,
|
|
78
|
+
normalizedImporter,
|
|
79
|
+
config.maxTraceDepth
|
|
80
|
+
);
|
|
81
|
+
await addTraceImportLocations(provider, trace, env.importLocCache);
|
|
82
|
+
const loc = await findPostCompileUsageLocation(
|
|
83
|
+
provider,
|
|
84
|
+
importer,
|
|
85
|
+
source,
|
|
86
|
+
findPostCompileUsagePos
|
|
87
|
+
) || await findImportStatementLocationFromTransformed(
|
|
88
|
+
provider,
|
|
89
|
+
importer,
|
|
90
|
+
source,
|
|
91
|
+
env.importLocCache
|
|
92
|
+
);
|
|
93
|
+
if (trace.length > 0) {
|
|
94
|
+
const last = trace[trace.length - 1];
|
|
95
|
+
if (!last.specifier) last.specifier = source;
|
|
96
|
+
if (loc && last.line == null) {
|
|
97
|
+
last.line = loc.line;
|
|
98
|
+
last.column = loc.column;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const snippet = loc ? buildCodeSnippet(provider, importer, loc) : void 0;
|
|
102
|
+
return {
|
|
103
|
+
env: envName,
|
|
104
|
+
envType,
|
|
105
|
+
behavior: config.effectiveBehavior,
|
|
106
|
+
specifier: source,
|
|
107
|
+
importer: normalizedImporter,
|
|
108
|
+
...loc ? { importerLoc: loc } : {},
|
|
109
|
+
trace,
|
|
110
|
+
snippet,
|
|
111
|
+
...overrides
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
async function maybeReportMarkerViolationFromResolvedImport(ctx, provider, env, envName, envType, importer, source, resolvedId, relativePath) {
|
|
115
|
+
const markerKind = getMarkerKindForFile(resolvedId);
|
|
116
|
+
const violates = envType === "client" && markerKind === "server" || envType === "server" && markerKind === "client";
|
|
117
|
+
if (!violates) return void 0;
|
|
118
|
+
const normalizedImporter = normalizeFilePath(importer);
|
|
119
|
+
const info = await buildViolationInfo(
|
|
120
|
+
provider,
|
|
121
|
+
env,
|
|
122
|
+
envName,
|
|
123
|
+
envType,
|
|
124
|
+
importer,
|
|
125
|
+
normalizedImporter,
|
|
126
|
+
source,
|
|
127
|
+
{
|
|
128
|
+
type: "marker",
|
|
129
|
+
resolved: normalizeFilePath(resolvedId),
|
|
130
|
+
message: markerKind === "server" ? `Module "${relativePath}" is marked server-only but is imported in the client environment` : `Module "${relativePath}" is marked client-only but is imported in the server environment`
|
|
131
|
+
}
|
|
132
|
+
);
|
|
133
|
+
return handleViolation.call(ctx, env, info);
|
|
134
|
+
}
|
|
135
|
+
function buildMockEdgeModuleId(env, importerId, source, runtimeId) {
|
|
136
|
+
const exports = getMockEdgeExports(env, importerId, source);
|
|
137
|
+
return makeMockEdgeModuleId(exports, source, runtimeId);
|
|
138
|
+
}
|
|
139
|
+
function getEnvType(envName) {
|
|
140
|
+
return config.envTypeMap.get(envName) ?? "server";
|
|
141
|
+
}
|
|
142
|
+
function getRulesForEnvironment(envName) {
|
|
143
|
+
const type = getEnvType(envName);
|
|
144
|
+
return type === "client" ? config.compiledRules.client : config.compiledRules.server;
|
|
145
|
+
}
|
|
146
|
+
const environmentNames = /* @__PURE__ */ new Set([
|
|
147
|
+
VITE_ENVIRONMENT_NAMES.client,
|
|
148
|
+
VITE_ENVIRONMENT_NAMES.server
|
|
149
|
+
]);
|
|
150
|
+
if (opts.providerEnvName !== VITE_ENVIRONMENT_NAMES.server) {
|
|
151
|
+
environmentNames.add(opts.providerEnvName);
|
|
152
|
+
}
|
|
153
|
+
function getEnv(envName) {
|
|
154
|
+
let envState = envStates.get(envName);
|
|
155
|
+
if (!envState) {
|
|
156
|
+
const importLocByFile = /* @__PURE__ */ new Map();
|
|
157
|
+
envState = {
|
|
158
|
+
graph: new ImportGraph(),
|
|
159
|
+
deniedSources: /* @__PURE__ */ new Set(),
|
|
160
|
+
deniedEdges: /* @__PURE__ */ new Map(),
|
|
161
|
+
mockExportsByImporter: /* @__PURE__ */ new Map(),
|
|
162
|
+
resolveCache: /* @__PURE__ */ new Map(),
|
|
163
|
+
resolveCacheByFile: /* @__PURE__ */ new Map(),
|
|
164
|
+
importLocCache: /* @__PURE__ */ new Map(),
|
|
165
|
+
// placeholder, replaced below
|
|
166
|
+
importLocByFile,
|
|
167
|
+
seenViolations: /* @__PURE__ */ new Set(),
|
|
168
|
+
transformResultCache: /* @__PURE__ */ new Map(),
|
|
169
|
+
transformResultKeysByFile: /* @__PURE__ */ new Map()
|
|
170
|
+
};
|
|
171
|
+
envState.importLocCache = createImportLocCache(envState);
|
|
172
|
+
envStates.set(envName, envState);
|
|
173
|
+
}
|
|
174
|
+
return envState;
|
|
175
|
+
}
|
|
176
|
+
function shouldCheckImporter(importer) {
|
|
177
|
+
const relativePath = path.relative(config.root, importer);
|
|
178
|
+
if (config.excludeMatchers.length > 0 && matchesAny(relativePath, config.excludeMatchers)) {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
if (config.ignoreImporterMatchers.length > 0 && matchesAny(relativePath, config.ignoreImporterMatchers)) {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
if (config.includeMatchers.length > 0) {
|
|
185
|
+
return !!matchesAny(relativePath, config.includeMatchers);
|
|
186
|
+
}
|
|
187
|
+
if (config.srcDirectory) {
|
|
188
|
+
return importer.startsWith(config.srcDirectory);
|
|
189
|
+
}
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
function dedupeKey(type, importer, specifier, resolved) {
|
|
193
|
+
return `${type}:${importer}:${specifier}:${resolved ?? ""}`;
|
|
194
|
+
}
|
|
195
|
+
function hasSeen(env, key) {
|
|
196
|
+
if (config.logMode === "always") return false;
|
|
197
|
+
if (env.seenViolations.has(key)) return true;
|
|
198
|
+
env.seenViolations.add(key);
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
function getRelativePath(absolutePath) {
|
|
202
|
+
return normalizePath(path.relative(config.root, absolutePath));
|
|
203
|
+
}
|
|
204
|
+
return [
|
|
205
|
+
{
|
|
206
|
+
name: "tanstack-start-core:import-protection",
|
|
207
|
+
enforce: "pre",
|
|
208
|
+
applyToEnvironment(env) {
|
|
209
|
+
if (!config.enabled) return false;
|
|
210
|
+
return environmentNames.has(env.name);
|
|
211
|
+
},
|
|
212
|
+
configResolved(viteConfig) {
|
|
213
|
+
config.root = viteConfig.root;
|
|
214
|
+
config.command = viteConfig.command;
|
|
215
|
+
const { startConfig, resolvedStartConfig } = opts.getConfig();
|
|
216
|
+
config.srcDirectory = resolvedStartConfig.srcDirectory;
|
|
217
|
+
const userOpts = startConfig.importProtection;
|
|
218
|
+
if (userOpts?.enabled === false) {
|
|
219
|
+
config.enabled = false;
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
config.enabled = true;
|
|
223
|
+
if (userOpts?.behavior) {
|
|
224
|
+
if (typeof userOpts.behavior === "string") {
|
|
225
|
+
config.effectiveBehavior = userOpts.behavior;
|
|
226
|
+
} else {
|
|
227
|
+
config.effectiveBehavior = viteConfig.command === "serve" ? userOpts.behavior.dev ?? "mock" : userOpts.behavior.build ?? "error";
|
|
228
|
+
}
|
|
229
|
+
} else {
|
|
230
|
+
config.effectiveBehavior = viteConfig.command === "serve" ? "mock" : "error";
|
|
231
|
+
}
|
|
232
|
+
config.logMode = userOpts?.log ?? "once";
|
|
233
|
+
config.mockAccess = userOpts?.mockAccess ?? "error";
|
|
234
|
+
config.maxTraceDepth = userOpts?.maxTraceDepth ?? 20;
|
|
235
|
+
config.onViolation = userOpts?.onViolation;
|
|
236
|
+
const defaults = getDefaultImportProtectionRules(opts.framework);
|
|
237
|
+
const clientSpecifiers = dedupePatterns([
|
|
238
|
+
...defaults.client.specifiers,
|
|
239
|
+
...userOpts?.client?.specifiers ?? []
|
|
240
|
+
]);
|
|
241
|
+
const clientFiles = userOpts?.client?.files ? [...userOpts.client.files] : [...defaults.client.files];
|
|
242
|
+
const serverSpecifiers = userOpts?.server?.specifiers ? dedupePatterns([...userOpts.server.specifiers]) : dedupePatterns([...defaults.server.specifiers]);
|
|
243
|
+
const serverFiles = userOpts?.server?.files ? [...userOpts.server.files] : [...defaults.server.files];
|
|
244
|
+
config.compiledRules.client = {
|
|
245
|
+
specifiers: compileMatchers(clientSpecifiers),
|
|
246
|
+
files: compileMatchers(clientFiles)
|
|
247
|
+
};
|
|
248
|
+
config.compiledRules.server = {
|
|
249
|
+
specifiers: compileMatchers(serverSpecifiers),
|
|
250
|
+
files: compileMatchers(serverFiles)
|
|
251
|
+
};
|
|
252
|
+
if (userOpts?.include) {
|
|
253
|
+
config.includeMatchers = compileMatchers(userOpts.include);
|
|
254
|
+
}
|
|
255
|
+
if (userOpts?.exclude) {
|
|
256
|
+
config.excludeMatchers = compileMatchers(userOpts.exclude);
|
|
257
|
+
}
|
|
258
|
+
if (userOpts?.ignoreImporters) {
|
|
259
|
+
config.ignoreImporterMatchers = compileMatchers(
|
|
260
|
+
userOpts.ignoreImporters
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
const markers = getMarkerSpecifiers(opts.framework);
|
|
264
|
+
config.markerSpecifiers = {
|
|
265
|
+
serverOnly: new Set(markers.serverOnly),
|
|
266
|
+
clientOnly: new Set(markers.clientOnly)
|
|
267
|
+
};
|
|
268
|
+
for (const envDef of opts.environments) {
|
|
269
|
+
const envState = getEnv(envDef.name);
|
|
270
|
+
if (resolvedStartConfig.routerFilePath) {
|
|
271
|
+
envState.graph.addEntry(
|
|
272
|
+
normalizePath(resolvedStartConfig.routerFilePath)
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
if (resolvedStartConfig.startFilePath) {
|
|
276
|
+
envState.graph.addEntry(
|
|
277
|
+
normalizePath(resolvedStartConfig.startFilePath)
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
buildStart() {
|
|
283
|
+
if (!config.enabled) return;
|
|
284
|
+
for (const envState of envStates.values()) {
|
|
285
|
+
envState.resolveCache.clear();
|
|
286
|
+
envState.resolveCacheByFile.clear();
|
|
287
|
+
envState.importLocCache.clear();
|
|
288
|
+
envState.importLocByFile.clear();
|
|
289
|
+
envState.seenViolations.clear();
|
|
290
|
+
envState.transformResultCache.clear();
|
|
291
|
+
envState.transformResultKeysByFile.clear();
|
|
292
|
+
envState.graph.clear();
|
|
293
|
+
envState.deniedSources.clear();
|
|
294
|
+
envState.deniedEdges.clear();
|
|
295
|
+
envState.mockExportsByImporter.clear();
|
|
296
|
+
}
|
|
297
|
+
shared.fileMarkerKind.clear();
|
|
298
|
+
for (const envDef of opts.environments) {
|
|
299
|
+
const envState = getEnv(envDef.name);
|
|
300
|
+
const { resolvedStartConfig } = opts.getConfig();
|
|
301
|
+
if (resolvedStartConfig.routerFilePath) {
|
|
302
|
+
envState.graph.addEntry(
|
|
303
|
+
normalizePath(resolvedStartConfig.routerFilePath)
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
if (resolvedStartConfig.startFilePath) {
|
|
307
|
+
envState.graph.addEntry(
|
|
308
|
+
normalizePath(resolvedStartConfig.startFilePath)
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
hotUpdate(ctx) {
|
|
314
|
+
if (!config.enabled) return;
|
|
315
|
+
for (const mod of ctx.modules) {
|
|
316
|
+
if (mod.id) {
|
|
317
|
+
const id = mod.id;
|
|
318
|
+
const importerFile = normalizeFilePath(id);
|
|
319
|
+
shared.fileMarkerKind.delete(importerFile);
|
|
320
|
+
for (const envState of envStates.values()) {
|
|
321
|
+
const locKeys = envState.importLocByFile.get(importerFile);
|
|
322
|
+
if (locKeys) {
|
|
323
|
+
for (const key of locKeys) {
|
|
324
|
+
envState.importLocCache.delete(key);
|
|
325
|
+
}
|
|
326
|
+
envState.importLocByFile.delete(importerFile);
|
|
327
|
+
}
|
|
328
|
+
const resolveKeys = envState.resolveCacheByFile.get(importerFile);
|
|
329
|
+
if (resolveKeys) {
|
|
330
|
+
for (const key of resolveKeys) {
|
|
331
|
+
envState.resolveCache.delete(key);
|
|
332
|
+
}
|
|
333
|
+
envState.resolveCacheByFile.delete(importerFile);
|
|
334
|
+
}
|
|
335
|
+
envState.graph.invalidate(importerFile);
|
|
336
|
+
envState.deniedEdges.delete(importerFile);
|
|
337
|
+
envState.mockExportsByImporter.delete(importerFile);
|
|
338
|
+
const transformKeys = envState.transformResultKeysByFile.get(importerFile);
|
|
339
|
+
if (transformKeys) {
|
|
340
|
+
for (const key of transformKeys) {
|
|
341
|
+
envState.transformResultCache.delete(key);
|
|
342
|
+
}
|
|
343
|
+
envState.transformResultKeysByFile.delete(importerFile);
|
|
344
|
+
} else {
|
|
345
|
+
envState.transformResultCache.delete(importerFile);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
},
|
|
351
|
+
async resolveId(source, importer, _options) {
|
|
352
|
+
if (!config.enabled) return void 0;
|
|
353
|
+
const envName = this.environment.name;
|
|
354
|
+
const env = getEnv(envName);
|
|
355
|
+
const envType = getEnvType(envName);
|
|
356
|
+
const provider = getTransformResultProvider(env);
|
|
357
|
+
if (source === MOCK_MODULE_ID) {
|
|
358
|
+
return RESOLVED_MOCK_MODULE_ID;
|
|
359
|
+
}
|
|
360
|
+
if (source.startsWith(MOCK_EDGE_PREFIX)) {
|
|
361
|
+
return resolveViteId(source);
|
|
362
|
+
}
|
|
363
|
+
if (source.startsWith(MOCK_RUNTIME_PREFIX)) {
|
|
364
|
+
return resolveViteId(source);
|
|
365
|
+
}
|
|
366
|
+
if (source.startsWith(MARKER_PREFIX)) {
|
|
367
|
+
return resolveViteId(source);
|
|
368
|
+
}
|
|
369
|
+
if (!importer) {
|
|
370
|
+
env.graph.addEntry(source);
|
|
371
|
+
return void 0;
|
|
372
|
+
}
|
|
373
|
+
if (source.startsWith("\0") || source.startsWith("virtual:")) {
|
|
374
|
+
return void 0;
|
|
375
|
+
}
|
|
376
|
+
if (config.markerSpecifiers.serverOnly.has(source)) {
|
|
377
|
+
const resolvedImporter = normalizeFilePath(importer);
|
|
378
|
+
const existing = shared.fileMarkerKind.get(resolvedImporter);
|
|
379
|
+
if (existing && existing !== "server") {
|
|
380
|
+
this.error(
|
|
381
|
+
`[import-protection] File "${getRelativePath(resolvedImporter)}" has both server-only and client-only markers. This is not allowed.`
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
shared.fileMarkerKind.set(resolvedImporter, "server");
|
|
385
|
+
if (envType === "client") {
|
|
386
|
+
const info = await buildViolationInfo(
|
|
387
|
+
provider,
|
|
388
|
+
env,
|
|
389
|
+
envName,
|
|
390
|
+
envType,
|
|
391
|
+
importer,
|
|
392
|
+
resolvedImporter,
|
|
393
|
+
source,
|
|
394
|
+
{
|
|
395
|
+
type: "marker",
|
|
396
|
+
message: `Module "${getRelativePath(resolvedImporter)}" is marked server-only but is imported in the client environment`
|
|
397
|
+
}
|
|
398
|
+
);
|
|
399
|
+
handleViolation.call(this, env, info);
|
|
400
|
+
}
|
|
401
|
+
return resolveViteId(`${MARKER_PREFIX}server-only`);
|
|
402
|
+
}
|
|
403
|
+
if (config.markerSpecifiers.clientOnly.has(source)) {
|
|
404
|
+
const resolvedImporter = normalizeFilePath(importer);
|
|
405
|
+
const existing = shared.fileMarkerKind.get(resolvedImporter);
|
|
406
|
+
if (existing && existing !== "client") {
|
|
407
|
+
this.error(
|
|
408
|
+
`[import-protection] File "${getRelativePath(resolvedImporter)}" has both server-only and client-only markers. This is not allowed.`
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
shared.fileMarkerKind.set(resolvedImporter, "client");
|
|
412
|
+
if (envType === "server") {
|
|
413
|
+
const info = await buildViolationInfo(
|
|
414
|
+
provider,
|
|
415
|
+
env,
|
|
416
|
+
envName,
|
|
417
|
+
envType,
|
|
418
|
+
importer,
|
|
419
|
+
resolvedImporter,
|
|
420
|
+
source,
|
|
421
|
+
{
|
|
422
|
+
type: "marker",
|
|
423
|
+
message: `Module "${getRelativePath(resolvedImporter)}" is marked client-only but is imported in the server environment`
|
|
424
|
+
}
|
|
425
|
+
);
|
|
426
|
+
handleViolation.call(this, env, info);
|
|
427
|
+
}
|
|
428
|
+
return resolveViteId(`${MARKER_PREFIX}client-only`);
|
|
429
|
+
}
|
|
430
|
+
const normalizedImporter = normalizeFilePath(importer);
|
|
431
|
+
if (!shouldCheckImporter(normalizedImporter)) {
|
|
432
|
+
return void 0;
|
|
433
|
+
}
|
|
434
|
+
const matchers = getRulesForEnvironment(envName);
|
|
435
|
+
const specifierMatch = matchesAny(source, matchers.specifiers);
|
|
436
|
+
if (specifierMatch) {
|
|
437
|
+
env.graph.addEdge(source, normalizedImporter, source);
|
|
438
|
+
const info = await buildViolationInfo(
|
|
439
|
+
provider,
|
|
440
|
+
env,
|
|
441
|
+
envName,
|
|
442
|
+
envType,
|
|
443
|
+
importer,
|
|
444
|
+
normalizedImporter,
|
|
445
|
+
source,
|
|
446
|
+
{
|
|
447
|
+
type: "specifier",
|
|
448
|
+
pattern: specifierMatch.pattern,
|
|
449
|
+
message: `Import "${source}" is denied in the "${envName}" environment`
|
|
450
|
+
}
|
|
451
|
+
);
|
|
452
|
+
return handleViolation.call(this, env, info);
|
|
453
|
+
}
|
|
454
|
+
const cacheKey = `${normalizedImporter}:${source}`;
|
|
455
|
+
let resolved;
|
|
456
|
+
if (env.resolveCache.has(cacheKey)) {
|
|
457
|
+
resolved = env.resolveCache.get(cacheKey) || null;
|
|
458
|
+
} else {
|
|
459
|
+
const result = await this.resolve(source, importer, {
|
|
460
|
+
skipSelf: true
|
|
461
|
+
});
|
|
462
|
+
resolved = result ? normalizeFilePath(result.id) : null;
|
|
463
|
+
env.resolveCache.set(cacheKey, resolved);
|
|
464
|
+
let fileKeys = env.resolveCacheByFile.get(normalizedImporter);
|
|
465
|
+
if (!fileKeys) {
|
|
466
|
+
fileKeys = /* @__PURE__ */ new Set();
|
|
467
|
+
env.resolveCacheByFile.set(normalizedImporter, fileKeys);
|
|
468
|
+
}
|
|
469
|
+
fileKeys.add(cacheKey);
|
|
470
|
+
}
|
|
471
|
+
if (resolved) {
|
|
472
|
+
const relativePath = getRelativePath(resolved);
|
|
473
|
+
env.graph.addEdge(resolved, normalizedImporter, source);
|
|
474
|
+
const fileMatch = matchers.files.length > 0 ? matchesAny(relativePath, matchers.files) : void 0;
|
|
475
|
+
if (fileMatch) {
|
|
476
|
+
const info = await buildViolationInfo(
|
|
477
|
+
provider,
|
|
478
|
+
env,
|
|
479
|
+
envName,
|
|
480
|
+
envType,
|
|
481
|
+
importer,
|
|
482
|
+
normalizedImporter,
|
|
483
|
+
source,
|
|
484
|
+
{
|
|
485
|
+
type: "file",
|
|
486
|
+
pattern: fileMatch.pattern,
|
|
487
|
+
resolved,
|
|
488
|
+
message: `Import "${source}" (resolved to "${relativePath}") is denied in the "${envName}" environment`
|
|
489
|
+
}
|
|
490
|
+
);
|
|
491
|
+
return handleViolation.call(this, env, info);
|
|
492
|
+
}
|
|
493
|
+
const markerRes = await maybeReportMarkerViolationFromResolvedImport(
|
|
494
|
+
this,
|
|
495
|
+
provider,
|
|
496
|
+
env,
|
|
497
|
+
envName,
|
|
498
|
+
envType,
|
|
499
|
+
importer,
|
|
500
|
+
source,
|
|
501
|
+
resolved,
|
|
502
|
+
relativePath
|
|
503
|
+
);
|
|
504
|
+
if (markerRes !== void 0) {
|
|
505
|
+
return markerRes;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
return void 0;
|
|
509
|
+
},
|
|
510
|
+
load: {
|
|
511
|
+
filter: {
|
|
512
|
+
id: new RegExp(
|
|
513
|
+
`(${RESOLVED_MOCK_MODULE_ID.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}|${RESOLVED_MARKER_PREFIX.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}|${RESOLVED_MOCK_EDGE_PREFIX.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}|${RESOLVED_MOCK_RUNTIME_PREFIX.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})`
|
|
514
|
+
)
|
|
515
|
+
},
|
|
516
|
+
handler(id) {
|
|
517
|
+
if (!config.enabled) return void 0;
|
|
518
|
+
if (id === RESOLVED_MOCK_MODULE_ID) {
|
|
519
|
+
return loadSilentMockModule();
|
|
520
|
+
}
|
|
521
|
+
if (id.startsWith(RESOLVED_MOCK_EDGE_PREFIX)) {
|
|
522
|
+
return loadMockEdgeModule(
|
|
523
|
+
id.slice(RESOLVED_MOCK_EDGE_PREFIX.length)
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
if (id.startsWith(RESOLVED_MOCK_RUNTIME_PREFIX)) {
|
|
527
|
+
return loadMockRuntimeModule(
|
|
528
|
+
id.slice(RESOLVED_MOCK_RUNTIME_PREFIX.length)
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
if (id.startsWith(RESOLVED_MARKER_PREFIX)) {
|
|
532
|
+
return loadMarkerModule();
|
|
533
|
+
}
|
|
534
|
+
return void 0;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
},
|
|
538
|
+
{
|
|
539
|
+
// This plugin runs WITHOUT `enforce` so it executes after all
|
|
540
|
+
// `enforce: 'pre'` transform hooks (including the Start compiler).
|
|
541
|
+
// It captures the transformed code + composed sourcemap for every module
|
|
542
|
+
// so that the `resolveId` hook (in the main plugin above) can look up
|
|
543
|
+
// the importer's transform result and map violation locations back to
|
|
544
|
+
// original source.
|
|
545
|
+
//
|
|
546
|
+
// Why not use `ctx.load()` in `resolveId`?
|
|
547
|
+
// - Vite dev: `this.load()` returns a ModuleInfo proxy that throws on
|
|
548
|
+
// `.code` access — code is not exposed.
|
|
549
|
+
// - Rollup build: `ModuleInfo` has `.code` but NOT `.map`, so we
|
|
550
|
+
// can't map generated positions back to original source.
|
|
551
|
+
//
|
|
552
|
+
// By caching in the transform hook we get both code and the composed
|
|
553
|
+
// sourcemap that chains all the way back to the original file.
|
|
554
|
+
//
|
|
555
|
+
// Performance: only files under `srcDirectory` are cached because only
|
|
556
|
+
// those can be importers in a violation. Third-party code in
|
|
557
|
+
// node_modules is never checked.
|
|
558
|
+
name: "tanstack-start-core:import-protection-transform-cache",
|
|
559
|
+
applyToEnvironment(env) {
|
|
560
|
+
if (!config.enabled) return false;
|
|
561
|
+
return environmentNames.has(env.name);
|
|
562
|
+
},
|
|
563
|
+
transform: {
|
|
564
|
+
filter: {
|
|
565
|
+
id: {
|
|
566
|
+
include: [/\.[cm]?[tj]sx?($|\?)/]
|
|
567
|
+
}
|
|
568
|
+
},
|
|
569
|
+
handler(code, id) {
|
|
570
|
+
if (!config.enabled) return void 0;
|
|
571
|
+
const envName = this.environment.name;
|
|
572
|
+
const file = normalizeFilePath(id);
|
|
573
|
+
if (!shouldCheckImporter(file)) {
|
|
574
|
+
return void 0;
|
|
575
|
+
}
|
|
576
|
+
let map;
|
|
577
|
+
try {
|
|
578
|
+
map = this.getCombinedSourcemap();
|
|
579
|
+
} catch {
|
|
580
|
+
map = void 0;
|
|
581
|
+
}
|
|
582
|
+
let originalCode;
|
|
583
|
+
if (map?.sourcesContent) {
|
|
584
|
+
originalCode = pickOriginalCodeFromSourcesContent(
|
|
585
|
+
map,
|
|
586
|
+
file,
|
|
587
|
+
config.root
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
const lineIndex = buildLineIndex(code);
|
|
591
|
+
const cacheKey = normalizePath(id);
|
|
592
|
+
const envState = getEnv(envName);
|
|
593
|
+
envState.transformResultCache.set(cacheKey, {
|
|
594
|
+
code,
|
|
595
|
+
map,
|
|
596
|
+
originalCode,
|
|
597
|
+
lineIndex
|
|
598
|
+
});
|
|
599
|
+
let keySet = envState.transformResultKeysByFile.get(file);
|
|
600
|
+
if (!keySet) {
|
|
601
|
+
keySet = /* @__PURE__ */ new Set();
|
|
602
|
+
envState.transformResultKeysByFile.set(file, keySet);
|
|
603
|
+
}
|
|
604
|
+
keySet.add(cacheKey);
|
|
605
|
+
if (cacheKey !== file) {
|
|
606
|
+
envState.transformResultCache.set(file, {
|
|
607
|
+
code,
|
|
608
|
+
map,
|
|
609
|
+
originalCode,
|
|
610
|
+
lineIndex
|
|
611
|
+
});
|
|
612
|
+
keySet.add(file);
|
|
613
|
+
}
|
|
614
|
+
return void 0;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
},
|
|
618
|
+
{
|
|
619
|
+
// Separate plugin so the transform can be enabled/disabled per-environment.
|
|
620
|
+
name: "tanstack-start-core:import-protection-mock-rewrite",
|
|
621
|
+
enforce: "pre",
|
|
622
|
+
// Only needed during dev. In build, we rely on Rollup's syntheticNamedExports.
|
|
623
|
+
apply: "serve",
|
|
624
|
+
applyToEnvironment(env) {
|
|
625
|
+
if (!config.enabled) return false;
|
|
626
|
+
if (config.effectiveBehavior !== "mock") return false;
|
|
627
|
+
return environmentNames.has(env.name);
|
|
628
|
+
},
|
|
629
|
+
transform: {
|
|
630
|
+
filter: {
|
|
631
|
+
id: {
|
|
632
|
+
include: [/\.[cm]?[tj]sx?($|\?)/]
|
|
633
|
+
}
|
|
634
|
+
},
|
|
635
|
+
handler(code, id) {
|
|
636
|
+
if (!config.enabled) return void 0;
|
|
637
|
+
const envName = this.environment.name;
|
|
638
|
+
const envState = envStates.get(envName);
|
|
639
|
+
if (!envState) return void 0;
|
|
640
|
+
try {
|
|
641
|
+
const importerFile = normalizeFilePath(id);
|
|
642
|
+
envState.mockExportsByImporter.set(
|
|
643
|
+
importerFile,
|
|
644
|
+
collectMockExportNamesBySource(code)
|
|
645
|
+
);
|
|
646
|
+
} catch {
|
|
647
|
+
}
|
|
648
|
+
return void 0;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
];
|
|
653
|
+
function handleViolation(env, info) {
|
|
654
|
+
const key = dedupeKey(
|
|
655
|
+
info.type,
|
|
656
|
+
info.importer,
|
|
657
|
+
info.specifier,
|
|
658
|
+
info.resolved
|
|
659
|
+
);
|
|
660
|
+
if (config.onViolation) {
|
|
661
|
+
const result = config.onViolation(info);
|
|
662
|
+
if (result === false) {
|
|
663
|
+
return void 0;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
const seen = hasSeen(env, key);
|
|
667
|
+
if (config.effectiveBehavior === "error") {
|
|
668
|
+
if (!seen) this.error(formatViolation(info, config.root));
|
|
669
|
+
return void 0;
|
|
670
|
+
}
|
|
671
|
+
if (!seen) {
|
|
672
|
+
this.warn(formatViolation(info, config.root));
|
|
673
|
+
}
|
|
674
|
+
env.deniedSources.add(info.specifier);
|
|
675
|
+
let edgeSet = env.deniedEdges.get(info.importer);
|
|
676
|
+
if (!edgeSet) {
|
|
677
|
+
edgeSet = /* @__PURE__ */ new Set();
|
|
678
|
+
env.deniedEdges.set(info.importer, edgeSet);
|
|
679
|
+
}
|
|
680
|
+
edgeSet.add(info.specifier);
|
|
681
|
+
if (config.command === "serve") {
|
|
682
|
+
const runtimeId = mockRuntimeModuleIdFromViolation(
|
|
683
|
+
info,
|
|
684
|
+
config.mockAccess,
|
|
685
|
+
config.root
|
|
686
|
+
);
|
|
687
|
+
return resolveViteId(
|
|
688
|
+
buildMockEdgeModuleId(env, info.importer, info.specifier, runtimeId)
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
return { id: RESOLVED_MOCK_MODULE_ID, syntheticNamedExports: true };
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
export {
|
|
695
|
+
RESOLVED_MOCK_MODULE_ID,
|
|
696
|
+
dedupePatterns,
|
|
697
|
+
importProtectionPlugin
|
|
698
|
+
};
|
|
699
|
+
//# sourceMappingURL=plugin.js.map
|