@tanstack/start-plugin-core 1.162.6 → 1.162.7
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/plugin.js +74 -17
- package/dist/esm/import-protection-plugin/plugin.js.map +1 -1
- package/dist/esm/import-protection-plugin/trace.js +1 -1
- package/dist/esm/import-protection-plugin/virtualModules.d.ts +7 -0
- package/dist/esm/import-protection-plugin/virtualModules.js +4 -0
- package/dist/esm/import-protection-plugin/virtualModules.js.map +1 -1
- package/dist/esm/schema.d.ts +9 -9
- package/dist/esm/schema.js +7 -1
- package/dist/esm/schema.js.map +1 -1
- package/package.json +3 -3
- package/src/import-protection-plugin/INTERNALS.md +290 -0
- package/src/import-protection-plugin/plugin.ts +147 -22
- package/src/import-protection-plugin/virtualModules.ts +8 -0
- package/src/schema.ts +7 -1
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
MOCK_MODULE_ID,
|
|
27
27
|
MOCK_RUNTIME_PREFIX,
|
|
28
28
|
RESOLVED_MARKER_PREFIX,
|
|
29
|
+
RESOLVED_MOCK_BUILD_PREFIX,
|
|
29
30
|
RESOLVED_MOCK_EDGE_PREFIX,
|
|
30
31
|
RESOLVED_MOCK_MODULE_ID,
|
|
31
32
|
RESOLVED_MOCK_RUNTIME_PREFIX,
|
|
@@ -122,7 +123,9 @@ interface PluginConfig {
|
|
|
122
123
|
markerSpecifiers: { serverOnly: Set<string>; clientOnly: Set<string> }
|
|
123
124
|
envTypeMap: Map<string, 'client' | 'server'>
|
|
124
125
|
|
|
125
|
-
onViolation?: (
|
|
126
|
+
onViolation?: (
|
|
127
|
+
info: ViolationInfo,
|
|
128
|
+
) => boolean | void | Promise<boolean | void>
|
|
126
129
|
}
|
|
127
130
|
|
|
128
131
|
/**
|
|
@@ -192,6 +195,13 @@ interface EnvState {
|
|
|
192
195
|
* determine whether the importer is still reachable from an entry point.
|
|
193
196
|
*/
|
|
194
197
|
pendingViolations: Map<string, Array<PendingViolation>>
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Violations deferred in build mode (both mock and error). Each gets a
|
|
201
|
+
* unique mock module ID so we can check which ones survived tree-shaking
|
|
202
|
+
* in `generateBundle`.
|
|
203
|
+
*/
|
|
204
|
+
deferredBuildViolations: Array<DeferredBuildViolation>
|
|
195
205
|
}
|
|
196
206
|
|
|
197
207
|
interface PendingViolation {
|
|
@@ -200,6 +210,12 @@ interface PendingViolation {
|
|
|
200
210
|
mockReturnValue: string
|
|
201
211
|
}
|
|
202
212
|
|
|
213
|
+
interface DeferredBuildViolation {
|
|
214
|
+
info: ViolationInfo
|
|
215
|
+
/** Unique mock module ID assigned to this violation. */
|
|
216
|
+
mockModuleId: string
|
|
217
|
+
}
|
|
218
|
+
|
|
203
219
|
/**
|
|
204
220
|
* Intentionally cross-env shared mutable state.
|
|
205
221
|
*
|
|
@@ -569,6 +585,7 @@ export function importProtectionPlugin(
|
|
|
569
585
|
postTransformImports: new Map(),
|
|
570
586
|
serverFnLookupModules: new Set(),
|
|
571
587
|
pendingViolations: new Map(),
|
|
588
|
+
deferredBuildViolations: [],
|
|
572
589
|
}
|
|
573
590
|
envStates.set(envName, envState)
|
|
574
591
|
}
|
|
@@ -772,7 +789,7 @@ export function importProtectionPlugin(
|
|
|
772
789
|
}
|
|
773
790
|
|
|
774
791
|
if (config.onViolation) {
|
|
775
|
-
const result = config.onViolation(pv.info)
|
|
792
|
+
const result = await config.onViolation(pv.info)
|
|
776
793
|
if (result === false) continue
|
|
777
794
|
}
|
|
778
795
|
warnFn(formatViolation(pv.info, config.root))
|
|
@@ -812,12 +829,20 @@ export function importProtectionPlugin(
|
|
|
812
829
|
})
|
|
813
830
|
}
|
|
814
831
|
|
|
815
|
-
|
|
832
|
+
/** Counter for generating unique per-violation mock module IDs in build mode. */
|
|
833
|
+
let buildViolationCounter = 0
|
|
834
|
+
|
|
835
|
+
type HandleViolationResult =
|
|
836
|
+
| { id: string; syntheticNamedExports: boolean }
|
|
837
|
+
| string
|
|
838
|
+
| undefined
|
|
839
|
+
|
|
840
|
+
async function handleViolation(
|
|
816
841
|
ctx: ViolationReporter,
|
|
817
842
|
env: EnvState,
|
|
818
843
|
info: ViolationInfo,
|
|
819
844
|
violationOpts?: { silent?: boolean },
|
|
820
|
-
):
|
|
845
|
+
): Promise<HandleViolationResult> {
|
|
821
846
|
const key = dedupeKey(
|
|
822
847
|
info.type,
|
|
823
848
|
info.importer,
|
|
@@ -827,24 +852,35 @@ export function importProtectionPlugin(
|
|
|
827
852
|
|
|
828
853
|
if (!violationOpts?.silent) {
|
|
829
854
|
if (config.onViolation) {
|
|
830
|
-
const result = config.onViolation(info)
|
|
855
|
+
const result = await config.onViolation(info)
|
|
831
856
|
if (result === false) {
|
|
832
857
|
return undefined
|
|
833
858
|
}
|
|
834
859
|
}
|
|
835
860
|
|
|
836
|
-
const seen = hasSeen(env, key)
|
|
837
|
-
|
|
838
861
|
if (config.effectiveBehavior === 'error') {
|
|
839
|
-
|
|
840
|
-
|
|
862
|
+
// Dev+error: throw immediately.
|
|
863
|
+
// Always throw on error — do NOT deduplicate via hasSeen().
|
|
864
|
+
// Rollup may resolve the same specifier multiple times (e.g.
|
|
865
|
+
// commonjs--resolver's nested this.resolve() fires before
|
|
866
|
+
// getResolveStaticDependencyPromises). If we record the key
|
|
867
|
+
// on the first (nested) throw, the second (real) resolve
|
|
868
|
+
// silently returns undefined and the build succeeds — which
|
|
869
|
+
// is the bug this fixes.
|
|
870
|
+
//
|
|
871
|
+
// Build mode never reaches here — all build violations are
|
|
872
|
+
// deferred via shouldDefer and handled silently.
|
|
873
|
+
|
|
874
|
+
return ctx.error(formatViolation(info, config.root))
|
|
841
875
|
}
|
|
842
876
|
|
|
877
|
+
const seen = hasSeen(env, key)
|
|
878
|
+
|
|
843
879
|
if (!seen) {
|
|
844
880
|
ctx.warn(formatViolation(info, config.root))
|
|
845
881
|
}
|
|
846
882
|
} else {
|
|
847
|
-
if (config.effectiveBehavior === 'error') {
|
|
883
|
+
if (config.effectiveBehavior === 'error' && config.command !== 'build') {
|
|
848
884
|
return undefined
|
|
849
885
|
}
|
|
850
886
|
}
|
|
@@ -868,17 +904,23 @@ export function importProtectionPlugin(
|
|
|
868
904
|
)
|
|
869
905
|
}
|
|
870
906
|
|
|
871
|
-
// Build:
|
|
872
|
-
|
|
907
|
+
// Build: unique per-violation mock IDs so generateBundle can check
|
|
908
|
+
// which violations survived tree-shaking (both mock and error mode).
|
|
909
|
+
const mockId = `${RESOLVED_MOCK_BUILD_PREFIX}${buildViolationCounter++}`
|
|
910
|
+
return { id: mockId, syntheticNamedExports: true }
|
|
873
911
|
}
|
|
874
912
|
|
|
875
913
|
/**
|
|
876
914
|
* Unified violation dispatch: either defers or reports immediately.
|
|
877
915
|
*
|
|
878
916
|
* When `shouldDefer` is true, calls `handleViolation` silently to obtain
|
|
879
|
-
* the mock module ID, stores the violation
|
|
880
|
-
*
|
|
881
|
-
*
|
|
917
|
+
* the mock module ID, then stores the violation for later verification:
|
|
918
|
+
* - Dev mock mode: defers to `pendingViolations` for graph-reachability
|
|
919
|
+
* checking via `processPendingViolations`.
|
|
920
|
+
* - Build mode (mock + error): defers to `deferredBuildViolations` for
|
|
921
|
+
* tree-shaking verification in `generateBundle`.
|
|
922
|
+
*
|
|
923
|
+
* Otherwise reports (or silences for pre-transform resolves) immediately.
|
|
882
924
|
*
|
|
883
925
|
* Returns the mock module ID / resolve result from `handleViolation`.
|
|
884
926
|
*/
|
|
@@ -889,14 +931,24 @@ export function importProtectionPlugin(
|
|
|
889
931
|
info: ViolationInfo,
|
|
890
932
|
shouldDefer: boolean,
|
|
891
933
|
isPreTransformResolve: boolean,
|
|
892
|
-
): Promise<
|
|
934
|
+
): Promise<HandleViolationResult> {
|
|
893
935
|
if (shouldDefer) {
|
|
894
|
-
const result = handleViolation(ctx, env, info, { silent: true })
|
|
895
|
-
|
|
896
|
-
|
|
936
|
+
const result = await handleViolation(ctx, env, info, { silent: true })
|
|
937
|
+
|
|
938
|
+
if (config.command === 'build') {
|
|
939
|
+
// Build mode: store for generateBundle tree-shaking check.
|
|
940
|
+
// The unique mock ID is inside `result`.
|
|
941
|
+
const mockId = typeof result === 'string' ? result : (result?.id ?? '')
|
|
942
|
+
env.deferredBuildViolations.push({ info, mockModuleId: mockId })
|
|
943
|
+
} else {
|
|
944
|
+
// Dev mock: store for graph-reachability check.
|
|
945
|
+
deferViolation(env, importerFile, info, result)
|
|
946
|
+
await processPendingViolations(env, ctx.warn.bind(ctx))
|
|
947
|
+
}
|
|
948
|
+
|
|
897
949
|
return result
|
|
898
950
|
}
|
|
899
|
-
return handleViolation(ctx, env, info, {
|
|
951
|
+
return await handleViolation(ctx, env, info, {
|
|
900
952
|
silent: isPreTransformResolve,
|
|
901
953
|
})
|
|
902
954
|
}
|
|
@@ -1027,6 +1079,8 @@ export function importProtectionPlugin(
|
|
|
1027
1079
|
envState.transformResultKeysByFile.clear()
|
|
1028
1080
|
envState.postTransformImports.clear()
|
|
1029
1081
|
envState.serverFnLookupModules.clear()
|
|
1082
|
+
envState.pendingViolations.clear()
|
|
1083
|
+
envState.deferredBuildViolations.length = 0
|
|
1030
1084
|
envState.graph.clear()
|
|
1031
1085
|
envState.deniedSources.clear()
|
|
1032
1086
|
envState.deniedEdges.clear()
|
|
@@ -1156,10 +1210,13 @@ export function importProtectionPlugin(
|
|
|
1156
1210
|
|
|
1157
1211
|
// Dev mock mode: defer violations until post-transform data is
|
|
1158
1212
|
// available, then confirm/discard via graph reachability.
|
|
1213
|
+
// Build mode (both mock and error): defer violations until
|
|
1214
|
+
// generateBundle so tree-shaking can eliminate false positives.
|
|
1159
1215
|
const isDevMock =
|
|
1160
1216
|
config.command === 'serve' && config.effectiveBehavior === 'mock'
|
|
1217
|
+
const isBuild = config.command === 'build'
|
|
1161
1218
|
|
|
1162
|
-
const shouldDefer = isDevMock && !isPreTransformResolve
|
|
1219
|
+
const shouldDefer = (isDevMock && !isPreTransformResolve) || isBuild
|
|
1163
1220
|
|
|
1164
1221
|
// Check if this is a marker import
|
|
1165
1222
|
const markerKind = config.markerSpecifiers.serverOnly.has(source)
|
|
@@ -1198,7 +1255,7 @@ export function importProtectionPlugin(
|
|
|
1198
1255
|
: `Module "${getRelativePath(normalizedImporter)}" is marked client-only but is imported in the server environment`,
|
|
1199
1256
|
},
|
|
1200
1257
|
)
|
|
1201
|
-
await reportOrDeferViolation(
|
|
1258
|
+
const markerResult = await reportOrDeferViolation(
|
|
1202
1259
|
this,
|
|
1203
1260
|
env,
|
|
1204
1261
|
normalizedImporter,
|
|
@@ -1206,6 +1263,16 @@ export function importProtectionPlugin(
|
|
|
1206
1263
|
shouldDefer,
|
|
1207
1264
|
isPreTransformResolve,
|
|
1208
1265
|
)
|
|
1266
|
+
|
|
1267
|
+
// In build mode, if the violation was deferred, return the unique
|
|
1268
|
+
// build mock ID instead of the marker module. This lets
|
|
1269
|
+
// generateBundle check whether the importer (and thus its marker
|
|
1270
|
+
// import) survived tree-shaking. The mock is side-effect-free just
|
|
1271
|
+
// like the marker module, and the bare import has no bindings, so
|
|
1272
|
+
// replacing it is transparent.
|
|
1273
|
+
if (isBuild && markerResult != null) {
|
|
1274
|
+
return markerResult
|
|
1275
|
+
}
|
|
1209
1276
|
}
|
|
1210
1277
|
|
|
1211
1278
|
return markerKind === 'server'
|
|
@@ -1338,6 +1405,7 @@ export function importProtectionPlugin(
|
|
|
1338
1405
|
id: new RegExp(
|
|
1339
1406
|
[
|
|
1340
1407
|
RESOLVED_MOCK_MODULE_ID,
|
|
1408
|
+
RESOLVED_MOCK_BUILD_PREFIX,
|
|
1341
1409
|
RESOLVED_MARKER_PREFIX,
|
|
1342
1410
|
RESOLVED_MOCK_EDGE_PREFIX,
|
|
1343
1411
|
RESOLVED_MOCK_RUNTIME_PREFIX,
|
|
@@ -1360,6 +1428,11 @@ export function importProtectionPlugin(
|
|
|
1360
1428
|
return loadSilentMockModule()
|
|
1361
1429
|
}
|
|
1362
1430
|
|
|
1431
|
+
// Per-violation build mock modules — same silent mock code
|
|
1432
|
+
if (id.startsWith(RESOLVED_MOCK_BUILD_PREFIX)) {
|
|
1433
|
+
return loadSilentMockModule()
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1363
1436
|
if (id.startsWith(RESOLVED_MOCK_EDGE_PREFIX)) {
|
|
1364
1437
|
return loadMockEdgeModule(
|
|
1365
1438
|
id.slice(RESOLVED_MOCK_EDGE_PREFIX.length),
|
|
@@ -1379,6 +1452,58 @@ export function importProtectionPlugin(
|
|
|
1379
1452
|
return undefined
|
|
1380
1453
|
},
|
|
1381
1454
|
},
|
|
1455
|
+
|
|
1456
|
+
async generateBundle(_options, bundle) {
|
|
1457
|
+
const envName = this.environment.name
|
|
1458
|
+
const env = envStates.get(envName)
|
|
1459
|
+
if (!env || env.deferredBuildViolations.length === 0) return
|
|
1460
|
+
|
|
1461
|
+
// Collect all module IDs that survived tree-shaking in this bundle.
|
|
1462
|
+
const survivingModules = new Set<string>()
|
|
1463
|
+
for (const chunk of Object.values(bundle)) {
|
|
1464
|
+
if (chunk.type === 'chunk') {
|
|
1465
|
+
for (const moduleId of Object.keys(chunk.modules)) {
|
|
1466
|
+
survivingModules.add(moduleId)
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
// Check each deferred violation: if its unique mock module survived
|
|
1472
|
+
// in the bundle, the import was NOT tree-shaken — real leak.
|
|
1473
|
+
const realViolations: Array<ViolationInfo> = []
|
|
1474
|
+
for (const { info, mockModuleId } of env.deferredBuildViolations) {
|
|
1475
|
+
if (!survivingModules.has(mockModuleId)) continue
|
|
1476
|
+
|
|
1477
|
+
if (config.onViolation) {
|
|
1478
|
+
const result = await config.onViolation(info)
|
|
1479
|
+
if (result === false) continue
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
realViolations.push(info)
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
if (realViolations.length === 0) return
|
|
1486
|
+
|
|
1487
|
+
if (config.effectiveBehavior === 'error') {
|
|
1488
|
+
// Error mode: fail the build on the first real violation.
|
|
1489
|
+
this.error(formatViolation(realViolations[0]!, config.root))
|
|
1490
|
+
} else {
|
|
1491
|
+
// Mock mode: warn for each surviving violation.
|
|
1492
|
+
const seen = new Set<string>()
|
|
1493
|
+
for (const info of realViolations) {
|
|
1494
|
+
const key = dedupeKey(
|
|
1495
|
+
info.type,
|
|
1496
|
+
info.importer,
|
|
1497
|
+
info.specifier,
|
|
1498
|
+
info.resolved,
|
|
1499
|
+
)
|
|
1500
|
+
if (!seen.has(key)) {
|
|
1501
|
+
seen.add(key)
|
|
1502
|
+
this.warn(formatViolation(info, config.root))
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
},
|
|
1382
1507
|
},
|
|
1383
1508
|
{
|
|
1384
1509
|
// Captures transformed code + composed sourcemap for location mapping.
|
|
@@ -8,6 +8,14 @@ import type { ViolationInfo } from './trace'
|
|
|
8
8
|
export const MOCK_MODULE_ID = 'tanstack-start-import-protection:mock'
|
|
9
9
|
export const RESOLVED_MOCK_MODULE_ID = resolveViteId(MOCK_MODULE_ID)
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Per-violation mock prefix used in build+error mode.
|
|
13
|
+
* Each deferred violation gets a unique ID so we can check which ones
|
|
14
|
+
* survived tree-shaking in `generateBundle`.
|
|
15
|
+
*/
|
|
16
|
+
export const MOCK_BUILD_PREFIX = 'tanstack-start-import-protection:mock:build:'
|
|
17
|
+
export const RESOLVED_MOCK_BUILD_PREFIX = resolveViteId(MOCK_BUILD_PREFIX)
|
|
18
|
+
|
|
11
19
|
export const MOCK_EDGE_PREFIX = 'tanstack-start-import-protection:mock-edge:'
|
|
12
20
|
export const RESOLVED_MOCK_EDGE_PREFIX = resolveViteId(MOCK_EDGE_PREFIX)
|
|
13
21
|
|
package/src/schema.ts
CHANGED
|
@@ -42,7 +42,13 @@ const importProtectionOptionsSchema = z
|
|
|
42
42
|
onViolation: z
|
|
43
43
|
.function()
|
|
44
44
|
.args(z.any())
|
|
45
|
-
.returns(
|
|
45
|
+
.returns(
|
|
46
|
+
z.union([
|
|
47
|
+
z.boolean(),
|
|
48
|
+
z.void(),
|
|
49
|
+
z.promise(z.union([z.boolean(), z.void()])),
|
|
50
|
+
]),
|
|
51
|
+
)
|
|
46
52
|
.optional(),
|
|
47
53
|
include: z.array(patternSchema).optional(),
|
|
48
54
|
exclude: z.array(patternSchema).optional(),
|