@tanstack/start-plugin-core 1.167.35 → 1.169.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 (187) hide show
  1. package/dist/esm/import-protection/adapterUtils.d.ts +27 -0
  2. package/dist/esm/import-protection/adapterUtils.js +31 -0
  3. package/dist/esm/import-protection/adapterUtils.js.map +1 -0
  4. package/dist/esm/import-protection/analysis.d.ts +36 -0
  5. package/dist/esm/import-protection/analysis.js +407 -0
  6. package/dist/esm/import-protection/analysis.js.map +1 -0
  7. package/dist/esm/{import-protection-plugin → import-protection}/ast.js +1 -1
  8. package/dist/esm/import-protection/ast.js.map +1 -0
  9. package/dist/esm/import-protection/constants.d.ts +11 -0
  10. package/dist/esm/{import-protection-plugin → import-protection}/constants.js +7 -2
  11. package/dist/esm/import-protection/constants.js.map +1 -0
  12. package/dist/esm/{import-protection-plugin → import-protection}/defaults.js +1 -1
  13. package/dist/esm/import-protection/defaults.js.map +1 -0
  14. package/dist/esm/{import-protection-plugin → import-protection}/extensionlessAbsoluteIdResolver.js +2 -2
  15. package/dist/esm/import-protection/extensionlessAbsoluteIdResolver.js.map +1 -0
  16. package/dist/esm/{import-protection-plugin → import-protection}/matchers.js +1 -1
  17. package/dist/esm/import-protection/matchers.js.map +1 -0
  18. package/dist/esm/{import-protection-plugin/rewriteDeniedImports.d.ts → import-protection/rewrite.d.ts} +0 -4
  19. package/dist/esm/import-protection/rewrite.js +121 -0
  20. package/dist/esm/import-protection/rewrite.js.map +1 -0
  21. package/dist/esm/{import-protection-plugin → import-protection}/sourceLocation.d.ts +32 -3
  22. package/dist/esm/{import-protection-plugin → import-protection}/sourceLocation.js +65 -10
  23. package/dist/esm/import-protection/sourceLocation.js.map +1 -0
  24. package/dist/esm/{import-protection-plugin → import-protection}/trace.d.ts +0 -1
  25. package/dist/esm/{import-protection-plugin → import-protection}/trace.js +1 -1
  26. package/dist/esm/import-protection/trace.js.map +1 -0
  27. package/dist/esm/{import-protection-plugin → import-protection}/utils.d.ts +18 -1
  28. package/dist/esm/{import-protection-plugin → import-protection}/utils.js +13 -20
  29. package/dist/esm/import-protection/utils.js.map +1 -0
  30. package/dist/esm/import-protection/virtualModules.d.ts +25 -0
  31. package/dist/esm/{import-protection-plugin → import-protection}/virtualModules.js +5 -117
  32. package/dist/esm/import-protection/virtualModules.js.map +1 -0
  33. package/dist/esm/index.d.ts +1 -5
  34. package/dist/esm/index.js +2 -4
  35. package/dist/esm/post-build.d.ts +9 -0
  36. package/dist/esm/post-build.js +37 -0
  37. package/dist/esm/post-build.js.map +1 -0
  38. package/dist/esm/prerender.d.ts +11 -0
  39. package/dist/esm/prerender.js +159 -0
  40. package/dist/esm/prerender.js.map +1 -0
  41. package/dist/esm/rsbuild/dev-server.d.ts +21 -0
  42. package/dist/esm/rsbuild/dev-server.js +76 -0
  43. package/dist/esm/rsbuild/dev-server.js.map +1 -0
  44. package/dist/esm/rsbuild/import-protection.d.ts +10 -0
  45. package/dist/esm/rsbuild/import-protection.js +775 -0
  46. package/dist/esm/rsbuild/import-protection.js.map +1 -0
  47. package/dist/esm/rsbuild/index.d.ts +4 -0
  48. package/dist/esm/rsbuild/index.js +3 -0
  49. package/dist/esm/rsbuild/normalized-client-build.d.ts +18 -0
  50. package/dist/esm/rsbuild/normalized-client-build.js +207 -0
  51. package/dist/esm/rsbuild/normalized-client-build.js.map +1 -0
  52. package/dist/esm/rsbuild/planning.d.ts +52 -0
  53. package/dist/esm/rsbuild/planning.js +108 -0
  54. package/dist/esm/rsbuild/planning.js.map +1 -0
  55. package/dist/esm/rsbuild/plugin.d.ts +4 -0
  56. package/dist/esm/rsbuild/plugin.js +344 -0
  57. package/dist/esm/rsbuild/plugin.js.map +1 -0
  58. package/dist/esm/rsbuild/post-build.d.ts +6 -0
  59. package/dist/esm/rsbuild/post-build.js +57 -0
  60. package/dist/esm/rsbuild/post-build.js.map +1 -0
  61. package/dist/esm/rsbuild/schema.d.ts +3372 -0
  62. package/dist/esm/rsbuild/schema.js +12 -0
  63. package/dist/esm/rsbuild/schema.js.map +1 -0
  64. package/dist/esm/rsbuild/start-compiler-host.d.ts +20 -0
  65. package/dist/esm/rsbuild/start-compiler-host.js +150 -0
  66. package/dist/esm/rsbuild/start-compiler-host.js.map +1 -0
  67. package/dist/esm/rsbuild/start-router-plugin.d.ts +18 -0
  68. package/dist/esm/rsbuild/start-router-plugin.js +63 -0
  69. package/dist/esm/rsbuild/start-router-plugin.js.map +1 -0
  70. package/dist/esm/rsbuild/swc-rsc.d.ts +14 -0
  71. package/dist/esm/rsbuild/swc-rsc.js +93 -0
  72. package/dist/esm/rsbuild/swc-rsc.js.map +1 -0
  73. package/dist/esm/rsbuild/types.d.ts +17 -0
  74. package/dist/esm/rsbuild/types.js +0 -0
  75. package/dist/esm/rsbuild/virtual-modules.d.ts +53 -0
  76. package/dist/esm/rsbuild/virtual-modules.js +287 -0
  77. package/dist/esm/rsbuild/virtual-modules.js.map +1 -0
  78. package/dist/esm/schema.d.ts +43 -43
  79. package/dist/esm/start-compiler/compiler.d.ts +1 -1
  80. package/dist/esm/start-compiler/compiler.js +80 -9
  81. package/dist/esm/start-compiler/compiler.js.map +1 -1
  82. package/dist/esm/start-compiler/handleCreateServerFn.js +9 -0
  83. package/dist/esm/start-compiler/handleCreateServerFn.js.map +1 -1
  84. package/dist/esm/start-compiler/host.js +5 -1
  85. package/dist/esm/start-compiler/host.js.map +1 -1
  86. package/dist/esm/start-compiler/types.d.ts +1 -0
  87. package/dist/esm/utils.d.ts +1 -0
  88. package/dist/esm/utils.js +10 -1
  89. package/dist/esm/utils.js.map +1 -1
  90. package/dist/esm/{import-protection-plugin → vite/import-protection-plugin}/plugin.js +41 -92
  91. package/dist/esm/vite/import-protection-plugin/plugin.js.map +1 -0
  92. package/dist/esm/{import-protection-plugin → vite/import-protection-plugin}/types.d.ts +5 -5
  93. package/dist/esm/vite/import-protection-plugin/virtualModules.d.ts +8 -0
  94. package/dist/esm/vite/import-protection-plugin/virtualModules.js +49 -0
  95. package/dist/esm/vite/import-protection-plugin/virtualModules.js.map +1 -0
  96. package/dist/esm/vite/index.d.ts +5 -0
  97. package/dist/esm/vite/index.js +4 -0
  98. package/dist/esm/vite/plugin.js +1 -1
  99. package/dist/esm/vite/plugin.js.map +1 -1
  100. package/dist/esm/vite/post-server-build.js +14 -32
  101. package/dist/esm/vite/post-server-build.js.map +1 -1
  102. package/dist/esm/vite/prerender.d.ts +2 -2
  103. package/dist/esm/vite/prerender.js +17 -147
  104. package/dist/esm/vite/prerender.js.map +1 -1
  105. package/dist/esm/vite/schema.d.ts +23 -23
  106. package/dist/esm/vite/start-compiler-plugin/hot-update.d.ts +2 -0
  107. package/dist/esm/vite/start-compiler-plugin/hot-update.js +16 -0
  108. package/dist/esm/vite/start-compiler-plugin/hot-update.js.map +1 -0
  109. package/dist/esm/vite/start-compiler-plugin/module-specifier.js +9 -4
  110. package/dist/esm/vite/start-compiler-plugin/module-specifier.js.map +1 -1
  111. package/dist/esm/vite/start-compiler-plugin/plugin.js +86 -13
  112. package/dist/esm/vite/start-compiler-plugin/plugin.js.map +1 -1
  113. package/package.json +32 -4
  114. package/src/import-protection/INTERNALS.md +266 -0
  115. package/src/import-protection/adapterUtils.ts +94 -0
  116. package/src/import-protection/analysis.ts +853 -0
  117. package/src/{import-protection-plugin → import-protection}/constants.ts +7 -0
  118. package/src/import-protection/rewrite.ts +229 -0
  119. package/src/{import-protection-plugin → import-protection}/sourceLocation.ts +125 -9
  120. package/src/{import-protection-plugin → import-protection}/trace.ts +0 -1
  121. package/src/{import-protection-plugin → import-protection}/utils.ts +36 -21
  122. package/src/{import-protection-plugin → import-protection}/virtualModules.ts +30 -177
  123. package/src/index.ts +1 -8
  124. package/src/post-build.ts +64 -0
  125. package/src/prerender.ts +292 -0
  126. package/src/rsbuild/INTERNALS-import-protection.md +169 -0
  127. package/src/rsbuild/dev-server.ts +129 -0
  128. package/src/rsbuild/import-protection.ts +1599 -0
  129. package/src/rsbuild/index.ts +4 -0
  130. package/src/rsbuild/normalized-client-build.ts +346 -0
  131. package/src/rsbuild/planning.ts +234 -0
  132. package/src/rsbuild/plugin.ts +754 -0
  133. package/src/rsbuild/post-build.ts +96 -0
  134. package/src/rsbuild/schema.ts +31 -0
  135. package/src/rsbuild/start-compiler-host.ts +250 -0
  136. package/src/rsbuild/start-router-plugin.ts +86 -0
  137. package/src/rsbuild/swc-rsc.ts +166 -0
  138. package/src/rsbuild/types.ts +20 -0
  139. package/src/rsbuild/virtual-modules.ts +565 -0
  140. package/src/start-compiler/compiler.ts +153 -19
  141. package/src/start-compiler/handleCreateServerFn.ts +18 -0
  142. package/src/start-compiler/types.ts +1 -0
  143. package/src/utils.ts +14 -0
  144. package/src/vite/import-protection-plugin/INTERNALS.md +187 -0
  145. package/src/{import-protection-plugin → vite/import-protection-plugin}/plugin.ts +73 -158
  146. package/src/{import-protection-plugin → vite/import-protection-plugin}/types.ts +5 -5
  147. package/src/vite/import-protection-plugin/virtualModules.ts +122 -0
  148. package/src/vite/index.ts +8 -0
  149. package/src/vite/plugin.ts +1 -1
  150. package/src/vite/post-server-build.ts +14 -57
  151. package/src/vite/prerender.ts +19 -260
  152. package/src/vite/start-compiler-plugin/hot-update.ts +24 -0
  153. package/src/vite/start-compiler-plugin/module-specifier.ts +15 -5
  154. package/src/vite/start-compiler-plugin/plugin.ts +193 -18
  155. package/dist/esm/import-protection-plugin/ast.js.map +0 -1
  156. package/dist/esm/import-protection-plugin/constants.d.ts +0 -6
  157. package/dist/esm/import-protection-plugin/constants.js.map +0 -1
  158. package/dist/esm/import-protection-plugin/defaults.js.map +0 -1
  159. package/dist/esm/import-protection-plugin/extensionlessAbsoluteIdResolver.js.map +0 -1
  160. package/dist/esm/import-protection-plugin/matchers.js.map +0 -1
  161. package/dist/esm/import-protection-plugin/plugin.js.map +0 -1
  162. package/dist/esm/import-protection-plugin/postCompileUsage.d.ts +0 -13
  163. package/dist/esm/import-protection-plugin/postCompileUsage.js +0 -63
  164. package/dist/esm/import-protection-plugin/postCompileUsage.js.map +0 -1
  165. package/dist/esm/import-protection-plugin/rewriteDeniedImports.js +0 -205
  166. package/dist/esm/import-protection-plugin/rewriteDeniedImports.js.map +0 -1
  167. package/dist/esm/import-protection-plugin/sourceLocation.js.map +0 -1
  168. package/dist/esm/import-protection-plugin/trace.js.map +0 -1
  169. package/dist/esm/import-protection-plugin/utils.js.map +0 -1
  170. package/dist/esm/import-protection-plugin/virtualModules.d.ts +0 -78
  171. package/dist/esm/import-protection-plugin/virtualModules.js.map +0 -1
  172. package/dist/esm/start-compiler/load-module.d.ts +0 -14
  173. package/dist/esm/start-compiler/load-module.js +0 -18
  174. package/dist/esm/start-compiler/load-module.js.map +0 -1
  175. package/src/import-protection-plugin/INTERNALS.md +0 -700
  176. package/src/import-protection-plugin/postCompileUsage.ts +0 -100
  177. package/src/import-protection-plugin/rewriteDeniedImports.ts +0 -379
  178. package/src/start-compiler/load-module.ts +0 -31
  179. /package/dist/esm/{import-protection-plugin → import-protection}/ast.d.ts +0 -0
  180. /package/dist/esm/{import-protection-plugin → import-protection}/defaults.d.ts +0 -0
  181. /package/dist/esm/{import-protection-plugin → import-protection}/extensionlessAbsoluteIdResolver.d.ts +0 -0
  182. /package/dist/esm/{import-protection-plugin → import-protection}/matchers.d.ts +0 -0
  183. /package/dist/esm/{import-protection-plugin → vite/import-protection-plugin}/plugin.d.ts +0 -0
  184. /package/src/{import-protection-plugin → import-protection}/ast.ts +0 -0
  185. /package/src/{import-protection-plugin → import-protection}/defaults.ts +0 -0
  186. /package/src/{import-protection-plugin → import-protection}/extensionlessAbsoluteIdResolver.ts +0 -0
  187. /package/src/{import-protection-plugin → import-protection}/matchers.ts +0 -0
@@ -0,0 +1,1599 @@
1
+ import { extname, resolve as resolvePath } from 'node:path'
2
+
3
+ import {
4
+ getDefaultImportProtectionRules,
5
+ getMarkerSpecifiers,
6
+ } from '../import-protection/defaults'
7
+ import { normalizePath } from '../utils'
8
+ import { ExtensionlessAbsoluteIdResolver } from '../import-protection/extensionlessAbsoluteIdResolver'
9
+ import { compileMatchers, matchesAny } from '../import-protection/matchers'
10
+ import {
11
+ getImportProtectionEnvType,
12
+ getImportProtectionRelativePath,
13
+ getImportProtectionRulesForEnvironment,
14
+ shouldCheckImportProtectionImporter,
15
+ } from '../import-protection/adapterUtils'
16
+ import {
17
+ findOriginalUnsafeUsagePosFromResult,
18
+ getImportSources,
19
+ getMockExportNamesBySource,
20
+ getNamedExports,
21
+ } from '../import-protection/analysis'
22
+ import { rewriteDeniedImports } from '../import-protection/rewrite'
23
+ import {
24
+ ImportLocCache,
25
+ addTraceImportLocations,
26
+ buildCodeSnippet,
27
+ buildLineIndex,
28
+ createImportSpecifierLocationIndex,
29
+ findImportStatementLocationFromTransformed,
30
+ findOriginalUsageLocation,
31
+ findPostCompileUsageLocation,
32
+ getOrCreateOriginalTransformResult,
33
+ indexToLineColumn,
34
+ normalizeSourceMap,
35
+ pickOriginalCodeFromSourcesContent,
36
+ } from '../import-protection/sourceLocation'
37
+ import {
38
+ ImportGraph,
39
+ buildTrace,
40
+ formatViolation,
41
+ } from '../import-protection/trace'
42
+ import {
43
+ generateDevSelfDenialModule,
44
+ generateSelfContainedMockModule,
45
+ loadMockEdgeModule,
46
+ loadMockRuntimeModule,
47
+ loadSilentMockModule,
48
+ } from '../import-protection/virtualModules'
49
+ import {
50
+ buildResolutionCandidates,
51
+ buildSourceCandidates,
52
+ canonicalizeResolvedId,
53
+ checkFileDenial,
54
+ clearNormalizeFilePathCache,
55
+ dedupePatterns,
56
+ dedupeViolationKey,
57
+ isFileExcluded,
58
+ normalizeFilePath,
59
+ } from '../import-protection/utils'
60
+
61
+ import type {
62
+ ImportProtectionBehavior,
63
+ ImportProtectionOptions,
64
+ } from '../schema'
65
+ import type { CompiledMatcher } from '../import-protection/matchers'
66
+ import type {
67
+ SourceMapLike,
68
+ TransformResult,
69
+ TransformResultProvider,
70
+ } from '../import-protection/sourceLocation'
71
+ import type { Loc, TraceStep, ViolationInfo } from '../import-protection/trace'
72
+ import type { CompileStartFrameworkOptions, GetConfigFn } from '../types'
73
+ import type {
74
+ RsbuildPluginAPI,
75
+ Rspack,
76
+ rspack as rspackNamespaceType,
77
+ } from '@rsbuild/core'
78
+
79
+ type RspackNamespace = typeof rspackNamespaceType
80
+ type RspackVirtualModulesPlugin = InstanceType<
81
+ RspackNamespace['experiments']['VirtualModulesPlugin']
82
+ >
83
+ type ProcessAssetsContext = Parameters<
84
+ Parameters<RsbuildPluginAPI['processAssets']>[1]
85
+ >[0]
86
+ type TransformContext = Parameters<
87
+ Parameters<RsbuildPluginAPI['transform']>[1]
88
+ >[0]
89
+ type RspackCompilation = Rspack.Compilation
90
+ type RspackModule = Rspack.Module
91
+ type RspackModuleGraphConnection = {
92
+ module?: RspackModule | null
93
+ dependency?: unknown
94
+ getActiveState?: (runtime: string | Array<string> | undefined) => unknown
95
+ }
96
+ type OriginalCodeLoader = (file: string) => Promise<string | undefined>
97
+ const importSpecifierLocationIndex = createImportSpecifierLocationIndex()
98
+
99
+ interface EnvRules {
100
+ specifiers: Array<CompiledMatcher>
101
+ files: Array<CompiledMatcher>
102
+ excludeFiles: Array<CompiledMatcher>
103
+ }
104
+
105
+ interface PluginConfig {
106
+ enabled: boolean
107
+ root: string
108
+ command: 'build' | 'serve'
109
+ srcDirectory: string
110
+ framework: CompileStartFrameworkOptions
111
+ effectiveBehavior: ImportProtectionBehavior
112
+ mockAccess: 'error' | 'warn' | 'off'
113
+ logMode: 'once' | 'always'
114
+ maxTraceDepth: number
115
+ compiledRules: {
116
+ client: EnvRules
117
+ server: EnvRules
118
+ }
119
+ includeMatchers: Array<CompiledMatcher>
120
+ excludeMatchers: Array<CompiledMatcher>
121
+ ignoreImporterMatchers: Array<CompiledMatcher>
122
+ markerSpecifiers: {
123
+ serverOnly: Set<string>
124
+ clientOnly: Set<string>
125
+ }
126
+ envTypeMap: Map<string, 'client' | 'server'>
127
+ onViolation?: (
128
+ info: ViolationInfo,
129
+ ) => boolean | void | Promise<boolean | void>
130
+ }
131
+
132
+ interface EnvRuntimeState {
133
+ resolveCache: Map<string, string | null>
134
+ seenViolations: Set<string>
135
+ buildTransformResults: Map<string, TransformResult>
136
+ deferredFileViolations: Array<DeferredFileViolation>
137
+ deferredFileViolationKeys: Set<string>
138
+ }
139
+
140
+ interface DeferredFileViolation {
141
+ importer: string
142
+ specifier: string
143
+ resolved: string
144
+ relativeResolved: string
145
+ pattern: string | RegExp
146
+ useOriginalLocation: boolean
147
+ }
148
+
149
+ interface SharedState {
150
+ root: string
151
+ virtualModules: Map<string, string>
152
+ vmPlugins: Record<string, RspackVirtualModulesPlugin>
153
+ readyVmPlugins: Record<string, boolean>
154
+ inputFileSystems: Record<string, Rspack.Compiler['inputFileSystem']>
155
+ pendingWrites: Map<string, Map<string, string>>
156
+ }
157
+
158
+ interface CompilationEdge {
159
+ importer: string
160
+ specifier?: string
161
+ resolved: string
162
+ }
163
+
164
+ interface MockEdgePayload {
165
+ exports: Array<string>
166
+ runtimeId: string
167
+ violation: {
168
+ env: string
169
+ envType: 'client' | 'server'
170
+ importer: string
171
+ specifier: string
172
+ resolved?: string
173
+ patternText: string
174
+ }
175
+ }
176
+
177
+ const IMPORT_PROTECTION_VIRTUAL_DIR = 'node_modules/.virtual/import-protection'
178
+ const MOCK_EDGE_FILE_PREFIX = 'mock-edge-'
179
+ const MOCK_RUNTIME_FILE_PREFIX = 'mock-runtime-'
180
+ const MOCK_SILENT_FILE = 'mock-silent.mjs'
181
+
182
+ function toBase64Url(input: unknown): string {
183
+ return Buffer.from(JSON.stringify(input), 'utf8').toString('base64url')
184
+ }
185
+
186
+ function fromBase64Url<T>(input: string): T {
187
+ return JSON.parse(Buffer.from(input, 'base64url').toString('utf8')) as T
188
+ }
189
+
190
+ function getRulesForEnvironment(
191
+ config: PluginConfig,
192
+ envName: string,
193
+ ): EnvRules {
194
+ return getImportProtectionRulesForEnvironment(config, envName) as EnvRules
195
+ }
196
+
197
+ function serializePattern(pattern: string | RegExp): string {
198
+ return typeof pattern === 'string' ? pattern : pattern.toString()
199
+ }
200
+
201
+ function dedupeKey(info: ViolationInfo): string {
202
+ return dedupeViolationKey(info)
203
+ }
204
+
205
+ function getOrCreateEnvState(
206
+ envStates: Map<string, EnvRuntimeState>,
207
+ envName: string,
208
+ ): EnvRuntimeState {
209
+ let env = envStates.get(envName)
210
+
211
+ if (!env) {
212
+ env = {
213
+ resolveCache: new Map(),
214
+ seenViolations: new Set(),
215
+ buildTransformResults: new Map(),
216
+ deferredFileViolations: [],
217
+ deferredFileViolationKeys: new Set(),
218
+ }
219
+ envStates.set(envName, env)
220
+ }
221
+
222
+ return env
223
+ }
224
+
225
+ function getVirtualModulePath(
226
+ root: string,
227
+ envName: string,
228
+ filename: string,
229
+ ): string {
230
+ return normalizePath(
231
+ resolvePath(root, IMPORT_PROTECTION_VIRTUAL_DIR, envName, filename),
232
+ )
233
+ }
234
+
235
+ function queuePendingWrite(
236
+ shared: SharedState,
237
+ envName: string,
238
+ filePath: string,
239
+ code: string,
240
+ ): void {
241
+ let writes = shared.pendingWrites.get(envName)
242
+ if (!writes) {
243
+ writes = new Map()
244
+ shared.pendingWrites.set(envName, writes)
245
+ }
246
+
247
+ writes.set(filePath, code)
248
+ }
249
+
250
+ function tryWriteVirtualModule(
251
+ shared: SharedState,
252
+ envName: string,
253
+ filePath: string,
254
+ code: string,
255
+ ): string {
256
+ const current = shared.virtualModules.get(filePath)
257
+ if (current === code) {
258
+ return filePath
259
+ }
260
+
261
+ shared.virtualModules.set(filePath, code)
262
+
263
+ const vmPlugin = shared.vmPlugins[envName]
264
+ if (!vmPlugin || !shared.readyVmPlugins[envName]) {
265
+ queuePendingWrite(shared, envName, filePath, code)
266
+ return filePath
267
+ }
268
+
269
+ vmPlugin.writeModule(filePath, code)
270
+ return filePath
271
+ }
272
+
273
+ function flushPendingWrites(shared: SharedState, envName: string): void {
274
+ const writes = shared.pendingWrites.get(envName)
275
+ if (!writes?.size || !shared.readyVmPlugins[envName]) {
276
+ return
277
+ }
278
+
279
+ for (const [filePath, code] of writes) {
280
+ shared.vmPlugins[envName]?.writeModule(filePath, code)
281
+ writes.delete(filePath)
282
+ }
283
+
284
+ if (writes.size === 0) {
285
+ shared.pendingWrites.delete(envName)
286
+ }
287
+ }
288
+
289
+ function ensureSilentMockModule(shared: SharedState, envName: string): string {
290
+ return tryWriteVirtualModule(
291
+ shared,
292
+ envName,
293
+ getVirtualModulePath(shared.root, envName, MOCK_SILENT_FILE),
294
+ loadSilentMockModule().code,
295
+ )
296
+ }
297
+
298
+ function ensureRuntimeMockModule(opts: {
299
+ shared: SharedState
300
+ envName: string
301
+ mode: 'error' | 'warn' | 'off'
302
+ env: string
303
+ importer: string
304
+ specifier: string
305
+ }): string {
306
+ const encoded = toBase64Url({
307
+ mode: opts.mode,
308
+ env: opts.env,
309
+ importer: opts.importer,
310
+ specifier: opts.specifier,
311
+ trace: [],
312
+ })
313
+
314
+ return tryWriteVirtualModule(
315
+ opts.shared,
316
+ opts.envName,
317
+ getVirtualModulePath(
318
+ opts.shared.root,
319
+ opts.envName,
320
+ `${MOCK_RUNTIME_FILE_PREFIX}${encoded}.mjs`,
321
+ ),
322
+ loadMockRuntimeModule(encoded).code,
323
+ )
324
+ }
325
+
326
+ function ensureMockEdgeModule(opts: {
327
+ shared: SharedState
328
+ envName: string
329
+ payload: MockEdgePayload
330
+ }): string {
331
+ const encoded = toBase64Url(opts.payload)
332
+
333
+ return tryWriteVirtualModule(
334
+ opts.shared,
335
+ opts.envName,
336
+ getVirtualModulePath(
337
+ opts.shared.root,
338
+ opts.envName,
339
+ `${MOCK_EDGE_FILE_PREFIX}${encoded}.mjs`,
340
+ ),
341
+ loadMockEdgeModule(encoded).code,
342
+ )
343
+ }
344
+
345
+ function getMockEdgePayloadFromFile(
346
+ filePath: string,
347
+ ): MockEdgePayload | undefined {
348
+ const match = /(?:^|[\\/])mock-edge-([^/\\]+)\.mjs$/.exec(filePath)
349
+ if (!match) {
350
+ return undefined
351
+ }
352
+
353
+ try {
354
+ return fromBase64Url<MockEdgePayload>(match[1]!)
355
+ } catch {
356
+ return undefined
357
+ }
358
+ }
359
+
360
+ async function loadOriginalCode(
361
+ cache: Map<string, Promise<string | undefined>>,
362
+ file: string,
363
+ loader: OriginalCodeLoader,
364
+ ): Promise<string | undefined> {
365
+ let result = cache.get(file)
366
+ if (!result) {
367
+ result = loader(file)
368
+ cache.set(file, result)
369
+ }
370
+
371
+ return result
372
+ }
373
+
374
+ async function loadOriginalCodeFromInputFileSystem(
375
+ inputFileSystem: NonNullable<RspackCompilation['inputFileSystem']>,
376
+ file: string,
377
+ ): Promise<string | undefined> {
378
+ return new Promise((resolve) => {
379
+ inputFileSystem.readFile(file, (error, data) => {
380
+ if (error || data == null) {
381
+ resolve(undefined)
382
+ return
383
+ }
384
+
385
+ resolve(typeof data === 'string' ? data : data.toString('utf8'))
386
+ })
387
+ })
388
+ }
389
+
390
+ async function resolveAgainstImporter(opts: {
391
+ envState: EnvRuntimeState
392
+ config: PluginConfig
393
+ ctx: TransformContext
394
+ importerId: string
395
+ source: string
396
+ extensionlessResolver: ExtensionlessAbsoluteIdResolver
397
+ }): Promise<string | null> {
398
+ const normalizedImporter = normalizeFilePath(opts.importerId)
399
+ const cacheKey = `${normalizedImporter}:${opts.source}`
400
+
401
+ if (opts.envState.resolveCache.has(cacheKey)) {
402
+ return opts.envState.resolveCache.get(cacheKey) ?? null
403
+ }
404
+
405
+ const importerDir =
406
+ opts.ctx.context ?? opts.importerId.replace(/[/\\][^/\\]*$/, '')
407
+
408
+ const resolved = await new Promise<string | null>((resolve, reject) => {
409
+ opts.ctx.resolve(importerDir, opts.source, (error, result) => {
410
+ if (error) {
411
+ reject(error)
412
+ return
413
+ }
414
+
415
+ resolve(typeof result === 'string' ? result : null)
416
+ })
417
+ }).catch(() => null)
418
+
419
+ if (!resolved) {
420
+ opts.envState.resolveCache.set(cacheKey, null)
421
+ return null
422
+ }
423
+
424
+ const canonical = canonicalizeResolvedId(
425
+ resolved,
426
+ opts.config.root,
427
+ (value) => opts.extensionlessResolver.resolve(value),
428
+ )
429
+
430
+ opts.envState.resolveCache.set(cacheKey, canonical)
431
+ return canonical
432
+ }
433
+
434
+ function getModuleResource(module: RspackModule): string | undefined {
435
+ const candidate = module as RspackModule & {
436
+ nameForCondition?: () => string | undefined
437
+ resourceResolveData?: { resource?: string }
438
+ resource?: string
439
+ userRequest?: string
440
+ request?: string
441
+ }
442
+
443
+ return (
444
+ candidate.nameForCondition() ??
445
+ candidate.resourceResolveData?.resource ??
446
+ candidate.resource ??
447
+ candidate.userRequest ??
448
+ candidate.request
449
+ )
450
+ }
451
+
452
+ function getModuleFile(module: RspackModule): string {
453
+ return normalizeFilePath(getModuleResource(module) ?? module.identifier())
454
+ }
455
+
456
+ const IMPORT_PROTECTION_PARSEABLE_EXTENSIONS = new Set([
457
+ '.ts',
458
+ '.tsx',
459
+ '.mts',
460
+ '.cts',
461
+ '.js',
462
+ '.jsx',
463
+ '.mjs',
464
+ '.cjs',
465
+ ])
466
+
467
+ function isImportProtectionSourceFile(file: string | undefined): boolean {
468
+ if (!file) {
469
+ return false
470
+ }
471
+
472
+ const extension = extname(normalizeFilePath(file))
473
+ return (
474
+ extension.length > 0 &&
475
+ IMPORT_PROTECTION_PARSEABLE_EXTENSIONS.has(extension)
476
+ )
477
+ }
478
+
479
+ function isImportProtectionSourceModule(module: RspackModule): boolean {
480
+ return isImportProtectionSourceFile(getModuleResource(module))
481
+ }
482
+
483
+ function addTransformResult(
484
+ cache: Map<string, TransformResult>,
485
+ key: string,
486
+ result: TransformResult,
487
+ ): void {
488
+ cache.set(normalizePath(key), result)
489
+ cache.set(normalizeFilePath(key), result)
490
+ }
491
+
492
+ function hasTransformResult(
493
+ cache: Map<string, TransformResult>,
494
+ key: string,
495
+ ): boolean {
496
+ return cache.has(normalizePath(key)) || cache.has(normalizeFilePath(key))
497
+ }
498
+
499
+ function deferFileViolation(
500
+ envState: EnvRuntimeState,
501
+ violation: DeferredFileViolation,
502
+ ): void {
503
+ const key = `${violation.importer}:${violation.specifier}:${violation.resolved}:${String(violation.pattern)}`
504
+ if (envState.deferredFileViolationKeys.has(key)) {
505
+ return
506
+ }
507
+
508
+ envState.deferredFileViolationKeys.add(key)
509
+ envState.deferredFileViolations.push(violation)
510
+ }
511
+
512
+ function hasOriginalUnsafeUsage(
513
+ result: TransformResult | undefined,
514
+ source: string,
515
+ envType: 'client' | 'server',
516
+ ): boolean {
517
+ if (!result) {
518
+ return false
519
+ }
520
+
521
+ const originalResult = getOrCreateOriginalTransformResult(result)
522
+ if (!originalResult) {
523
+ return false
524
+ }
525
+
526
+ return !!findOriginalUnsafeUsagePosFromResult(originalResult, source, envType)
527
+ }
528
+
529
+ async function buildTransformResultProvider(opts: {
530
+ modules: Array<RspackModule>
531
+ root: string
532
+ loadOriginalCode: OriginalCodeLoader
533
+ preloaded?: Map<string, TransformResult>
534
+ }): Promise<TransformResultProvider> {
535
+ const cache = new Map<string, TransformResult>()
536
+
537
+ if (opts.preloaded) {
538
+ for (const [key, result] of opts.preloaded) {
539
+ cache.set(key, result)
540
+ }
541
+ }
542
+
543
+ for (const module of opts.modules) {
544
+ const source = module.originalSource()
545
+ if (!source) continue
546
+
547
+ const sourceAndMap = source.sourceAndMap()
548
+ const code = String(sourceAndMap.source)
549
+ const map = normalizeSourceMap(sourceAndMap.map as SourceMapLike | null)
550
+ const file = getModuleFile(module)
551
+ const resource = getModuleResource(module)
552
+
553
+ const originalCode = map?.sourcesContent
554
+ ? (pickOriginalCodeFromSourcesContent(map, resource ?? file, opts.root) ??
555
+ (resource ? await opts.loadOriginalCode(resource) : undefined))
556
+ : resource
557
+ ? await opts.loadOriginalCode(resource)
558
+ : undefined
559
+
560
+ const result: TransformResult = {
561
+ code,
562
+ map,
563
+ originalCode,
564
+ lineIndex: buildLineIndex(code),
565
+ }
566
+
567
+ if (!hasTransformResult(cache, file)) {
568
+ addTransformResult(cache, file, result)
569
+ }
570
+
571
+ if (resource && !hasTransformResult(cache, resource)) {
572
+ addTransformResult(cache, resource, result)
573
+ }
574
+ }
575
+
576
+ return {
577
+ getTransformResult(id: string) {
578
+ return cache.get(normalizePath(id)) ?? cache.get(normalizeFilePath(id))
579
+ },
580
+ }
581
+ }
582
+
583
+ function getConnectionRequest(dependency: unknown): string | undefined {
584
+ const candidate = dependency as { request?: unknown }
585
+ return typeof candidate.request === 'string' ? candidate.request : undefined
586
+ }
587
+
588
+ function addEntryModulesToGraph(opts: {
589
+ compilation: RspackCompilation
590
+ graph: ImportGraph
591
+ }): void {
592
+ for (const entry of opts.compilation.entries.values()) {
593
+ for (const dependency of entry.dependencies) {
594
+ const connection = opts.compilation.moduleGraph.getConnection(dependency)
595
+ const module = connection?.module
596
+ if (!module) continue
597
+ opts.graph.addEntry(getModuleFile(module))
598
+ }
599
+ }
600
+ }
601
+
602
+ function buildCompilationGraph(opts: {
603
+ compilation: RspackCompilation
604
+ modules: Array<RspackModule>
605
+ }): { graph: ImportGraph; edges: Array<CompilationEdge> } {
606
+ const graph = new ImportGraph()
607
+ const edges: Array<CompilationEdge> = []
608
+
609
+ addEntryModulesToGraph({
610
+ compilation: opts.compilation,
611
+ graph,
612
+ })
613
+
614
+ for (const module of opts.modules) {
615
+ const importer = getModuleFile(module)
616
+ const connections =
617
+ opts.compilation.moduleGraph.getOutgoingConnectionsInOrder(module)
618
+
619
+ for (const connection of connections) {
620
+ if (!connection.module) continue
621
+ if (!isActiveConnection(connection)) continue
622
+
623
+ const resolved = getModuleFile(connection.module)
624
+ const specifier = getConnectionRequest(connection.dependency)
625
+ graph.addEdge(resolved, importer, specifier)
626
+ edges.push({ importer, specifier, resolved })
627
+ }
628
+ }
629
+
630
+ return { graph, edges }
631
+ }
632
+
633
+ function isActiveConnection(connection: RspackModuleGraphConnection): boolean {
634
+ if (typeof connection.getActiveState !== 'function') {
635
+ return true
636
+ }
637
+
638
+ return connection.getActiveState(undefined) === true
639
+ }
640
+
641
+ function findImportLocationInOriginalCode(
642
+ provider: TransformResultProvider,
643
+ importer: string,
644
+ source: string,
645
+ ): Loc | undefined {
646
+ const result = provider.getTransformResult(importer)
647
+ if (!result) {
648
+ return undefined
649
+ }
650
+
651
+ const originalResult = getOrCreateOriginalTransformResult(result)
652
+ if (!originalResult) {
653
+ return undefined
654
+ }
655
+
656
+ const index = importSpecifierLocationIndex.find(originalResult, source)
657
+ if (index === -1) {
658
+ return undefined
659
+ }
660
+
661
+ const lineIndex =
662
+ originalResult.lineIndex ??
663
+ (originalResult.lineIndex = buildLineIndex(originalResult.code))
664
+ const loc = indexToLineColumn(lineIndex, index)
665
+
666
+ return {
667
+ file: normalizeFilePath(importer),
668
+ line: loc.line,
669
+ column: loc.column,
670
+ }
671
+ }
672
+
673
+ async function resolveImporterLocation(opts: {
674
+ provider: TransformResultProvider
675
+ importLocCache: ImportLocCache
676
+ importer: string
677
+ sourceCandidates: Iterable<string>
678
+ preferOriginalCode?: boolean
679
+ envType?: 'client' | 'server'
680
+ }): Promise<Loc | undefined> {
681
+ if (opts.preferOriginalCode) {
682
+ for (const candidate of opts.sourceCandidates) {
683
+ const loc =
684
+ findOriginalUsageLocation(
685
+ opts.provider,
686
+ opts.importer,
687
+ candidate,
688
+ opts.envType,
689
+ ) ??
690
+ findImportLocationInOriginalCode(
691
+ opts.provider,
692
+ opts.importer,
693
+ candidate,
694
+ )
695
+ if (loc) {
696
+ return loc
697
+ }
698
+ }
699
+ }
700
+
701
+ for (const candidate of opts.sourceCandidates) {
702
+ const loc =
703
+ (await findPostCompileUsageLocation(
704
+ opts.provider,
705
+ opts.importer,
706
+ candidate,
707
+ )) ||
708
+ (await findImportStatementLocationFromTransformed(
709
+ opts.provider,
710
+ opts.importer,
711
+ candidate,
712
+ opts.importLocCache,
713
+ importSpecifierLocationIndex.find,
714
+ ))
715
+
716
+ if (loc) {
717
+ return loc
718
+ }
719
+ }
720
+
721
+ if (!opts.preferOriginalCode) {
722
+ for (const candidate of opts.sourceCandidates) {
723
+ const loc = findImportLocationInOriginalCode(
724
+ opts.provider,
725
+ opts.importer,
726
+ candidate,
727
+ )
728
+ if (loc) {
729
+ return loc
730
+ }
731
+ }
732
+ }
733
+
734
+ return undefined
735
+ }
736
+
737
+ async function rebuildAndAnnotateTrace(opts: {
738
+ provider: TransformResultProvider
739
+ graph: ImportGraph
740
+ importLocCache: ImportLocCache
741
+ importer: string
742
+ specifier: string
743
+ importerLoc?: Loc
744
+ maxTraceDepth: number
745
+ }): Promise<Array<TraceStep>> {
746
+ const trace = buildTrace(opts.graph, opts.importer, opts.maxTraceDepth)
747
+
748
+ await addTraceImportLocations(
749
+ opts.provider,
750
+ trace,
751
+ opts.importLocCache,
752
+ importSpecifierLocationIndex.find,
753
+ )
754
+
755
+ if (trace.length > 0) {
756
+ const last = trace[trace.length - 1]!
757
+ if (!last.specifier) {
758
+ last.specifier = opts.specifier
759
+ }
760
+ if (opts.importerLoc && last.line == null) {
761
+ last.line = opts.importerLoc.line
762
+ last.column = opts.importerLoc.column
763
+ }
764
+ }
765
+
766
+ return trace
767
+ }
768
+
769
+ async function buildViolationInfo(opts: {
770
+ config: PluginConfig
771
+ provider: TransformResultProvider
772
+ graph: ImportGraph
773
+ importLocCache: ImportLocCache
774
+ envName: string
775
+ envType: 'client' | 'server'
776
+ importer: string
777
+ source: string
778
+ resolved?: string
779
+ type: 'specifier' | 'file' | 'marker'
780
+ pattern?: string | RegExp
781
+ preferOriginalCode?: boolean
782
+ }): Promise<ViolationInfo> {
783
+ const importerLoc = await resolveImporterLocation({
784
+ provider: opts.provider,
785
+ importLocCache: opts.importLocCache,
786
+ importer: opts.importer,
787
+ sourceCandidates: buildSourceCandidates(
788
+ opts.source,
789
+ opts.resolved,
790
+ opts.config.root,
791
+ ),
792
+ preferOriginalCode: opts.preferOriginalCode,
793
+ envType: opts.envType,
794
+ })
795
+
796
+ const trace = await rebuildAndAnnotateTrace({
797
+ provider: opts.provider,
798
+ graph: opts.graph,
799
+ importLocCache: opts.importLocCache,
800
+ importer: opts.importer,
801
+ specifier: opts.source,
802
+ importerLoc,
803
+ maxTraceDepth: opts.config.maxTraceDepth,
804
+ })
805
+
806
+ const snippet = importerLoc
807
+ ? buildCodeSnippet(opts.provider, opts.importer, importerLoc)
808
+ : undefined
809
+
810
+ return {
811
+ env: opts.envName,
812
+ envType: opts.envType,
813
+ behavior: opts.config.effectiveBehavior,
814
+ type: opts.type,
815
+ pattern: opts.pattern,
816
+ specifier: opts.source,
817
+ importer: opts.importer,
818
+ ...(opts.resolved ? { resolved: opts.resolved } : {}),
819
+ ...(importerLoc ? { importerLoc } : {}),
820
+ trace,
821
+ snippet,
822
+ }
823
+ }
824
+
825
+ async function getMarkerKindForFile(opts: {
826
+ config: PluginConfig
827
+ provider: TransformResultProvider
828
+ loadOriginalCode: OriginalCodeLoader
829
+ markerKindCache: Map<string, Promise<'server' | 'client' | undefined>>
830
+ file: string
831
+ }): Promise<'server' | 'client' | undefined> {
832
+ if (!isImportProtectionSourceFile(opts.file)) {
833
+ return undefined
834
+ }
835
+
836
+ let cached = opts.markerKindCache.get(opts.file)
837
+ if (!cached) {
838
+ cached = (async () => {
839
+ const code =
840
+ opts.provider.getTransformResult(opts.file)?.originalCode ??
841
+ (await opts.loadOriginalCode(opts.file))
842
+
843
+ if (!code) {
844
+ return undefined
845
+ }
846
+
847
+ const imports = getImportSources(code)
848
+ const hasServerOnly = imports.some((source) =>
849
+ opts.config.markerSpecifiers.serverOnly.has(source),
850
+ )
851
+ const hasClientOnly = imports.some((source) =>
852
+ opts.config.markerSpecifiers.clientOnly.has(source),
853
+ )
854
+
855
+ if (hasServerOnly && !hasClientOnly) {
856
+ return 'server'
857
+ }
858
+
859
+ if (hasClientOnly && !hasServerOnly) {
860
+ return 'client'
861
+ }
862
+
863
+ return undefined
864
+ })()
865
+ opts.markerKindCache.set(opts.file, cached)
866
+ }
867
+
868
+ return cached
869
+ }
870
+
871
+ async function reportViolation(opts: {
872
+ config: PluginConfig
873
+ envState: EnvRuntimeState
874
+ compilation: RspackCompilation
875
+ rspack: RspackNamespace
876
+ info: ViolationInfo
877
+ }): Promise<void> {
878
+ const key = dedupeKey(opts.info)
879
+ if (
880
+ opts.config.logMode !== 'always' &&
881
+ opts.envState.seenViolations.has(key)
882
+ ) {
883
+ return
884
+ }
885
+
886
+ opts.envState.seenViolations.add(key)
887
+
888
+ if (opts.config.onViolation) {
889
+ const result = await opts.config.onViolation(opts.info)
890
+ if (result === false) {
891
+ return
892
+ }
893
+ }
894
+
895
+ const message = formatViolation(opts.info, opts.config.root)
896
+ const error = new opts.rspack.WebpackError(message)
897
+
898
+ if (opts.config.effectiveBehavior === 'error') {
899
+ opts.compilation.errors.push(error)
900
+ } else {
901
+ opts.compilation.warnings.push(error)
902
+ }
903
+ }
904
+
905
+ export function registerImportProtection(
906
+ api: RsbuildPluginAPI,
907
+ opts: {
908
+ getConfig: GetConfigFn
909
+ framework: CompileStartFrameworkOptions
910
+ environments: Array<{ name: string; type: 'client' | 'server' }>
911
+ },
912
+ ): void {
913
+ const extensionlessResolver = new ExtensionlessAbsoluteIdResolver()
914
+ const envStates = new Map<string, EnvRuntimeState>()
915
+ const fileReadCache = new Map<string, Promise<string | undefined>>()
916
+
917
+ const config: PluginConfig = {
918
+ enabled: true,
919
+ root: '',
920
+ command: api.context.action === 'dev' ? 'serve' : 'build',
921
+ srcDirectory: '',
922
+ framework: opts.framework,
923
+ effectiveBehavior: 'error',
924
+ mockAccess: 'error',
925
+ logMode: 'once',
926
+ maxTraceDepth: 20,
927
+ compiledRules: {
928
+ client: {
929
+ specifiers: [],
930
+ files: [],
931
+ excludeFiles: [],
932
+ },
933
+ server: {
934
+ specifiers: [],
935
+ files: [],
936
+ excludeFiles: [],
937
+ },
938
+ },
939
+ includeMatchers: [],
940
+ excludeMatchers: [],
941
+ ignoreImporterMatchers: [],
942
+ markerSpecifiers: {
943
+ serverOnly: new Set(),
944
+ clientOnly: new Set(),
945
+ },
946
+ envTypeMap: new Map(opts.environments.map((env) => [env.name, env.type])),
947
+ onViolation: undefined,
948
+ }
949
+
950
+ const shared: SharedState = {
951
+ root: '',
952
+ virtualModules: new Map(),
953
+ vmPlugins: {},
954
+ readyVmPlugins: {},
955
+ inputFileSystems: {},
956
+ pendingWrites: new Map(),
957
+ }
958
+
959
+ function applyUserConfig(): void {
960
+ const { startConfig, resolvedStartConfig } = opts.getConfig()
961
+
962
+ config.root = resolvedStartConfig.root
963
+ config.srcDirectory = resolvedStartConfig.srcDirectory
964
+ shared.root = resolvedStartConfig.root
965
+
966
+ const userOpts: ImportProtectionOptions | undefined =
967
+ startConfig.importProtection
968
+
969
+ if (userOpts?.enabled === false) {
970
+ config.enabled = false
971
+ return
972
+ }
973
+
974
+ config.enabled = true
975
+
976
+ const behavior = userOpts?.behavior
977
+ if (typeof behavior === 'string') {
978
+ config.effectiveBehavior = behavior
979
+ } else {
980
+ config.effectiveBehavior =
981
+ config.command === 'serve'
982
+ ? (behavior?.dev ?? 'mock')
983
+ : (behavior?.build ?? 'error')
984
+ }
985
+
986
+ config.logMode = userOpts?.log ?? 'once'
987
+ config.mockAccess = userOpts?.mockAccess ?? 'error'
988
+ config.maxTraceDepth = userOpts?.maxTraceDepth ?? 20
989
+ config.onViolation = userOpts?.onViolation
990
+ ? (info) => userOpts.onViolation?.(info)
991
+ : undefined
992
+
993
+ const defaults = getDefaultImportProtectionRules()
994
+ const pick = <T>(user: Array<T> | undefined, fallback: Array<T>) =>
995
+ user ? [...user] : [...fallback]
996
+
997
+ const clientSpecifiers = dedupePatterns([
998
+ ...defaults.client.specifiers,
999
+ ...(userOpts?.client?.specifiers ?? []),
1000
+ ])
1001
+
1002
+ config.compiledRules.client = {
1003
+ specifiers: compileMatchers(clientSpecifiers),
1004
+ files: compileMatchers(
1005
+ pick(userOpts?.client?.files, defaults.client.files),
1006
+ ),
1007
+ excludeFiles: compileMatchers(
1008
+ pick(userOpts?.client?.excludeFiles, defaults.client.excludeFiles),
1009
+ ),
1010
+ }
1011
+
1012
+ config.compiledRules.server = {
1013
+ specifiers: compileMatchers(
1014
+ dedupePatterns(
1015
+ pick(userOpts?.server?.specifiers, defaults.server.specifiers),
1016
+ ),
1017
+ ),
1018
+ files: compileMatchers(
1019
+ pick(userOpts?.server?.files, defaults.server.files),
1020
+ ),
1021
+ excludeFiles: compileMatchers(
1022
+ pick(userOpts?.server?.excludeFiles, defaults.server.excludeFiles),
1023
+ ),
1024
+ }
1025
+
1026
+ config.includeMatchers = compileMatchers(userOpts?.include ?? [])
1027
+ config.excludeMatchers = compileMatchers(userOpts?.exclude ?? [])
1028
+ config.ignoreImporterMatchers = compileMatchers(
1029
+ userOpts?.ignoreImporters ?? [],
1030
+ )
1031
+
1032
+ const markers = getMarkerSpecifiers()
1033
+ config.markerSpecifiers = {
1034
+ serverOnly: new Set(markers.serverOnly),
1035
+ clientOnly: new Set(markers.clientOnly),
1036
+ }
1037
+ }
1038
+
1039
+ api.onBeforeBuild(() => {
1040
+ applyUserConfig()
1041
+ clearNormalizeFilePathCache()
1042
+ extensionlessResolver.clear()
1043
+ fileReadCache.clear()
1044
+ envStates.clear()
1045
+ })
1046
+
1047
+ api.onBeforeDevCompile(() => {
1048
+ applyUserConfig()
1049
+ clearNormalizeFilePathCache()
1050
+ extensionlessResolver.clear()
1051
+ fileReadCache.clear()
1052
+
1053
+ for (const envState of envStates.values()) {
1054
+ envState.resolveCache.clear()
1055
+ envState.buildTransformResults.clear()
1056
+ envState.deferredFileViolations.length = 0
1057
+ envState.deferredFileViolationKeys.clear()
1058
+ }
1059
+ })
1060
+
1061
+ api.modifyRspackConfig((rspackConfig, utils) => {
1062
+ applyUserConfig()
1063
+
1064
+ const envName = utils.environment.name
1065
+ const VMP = utils.rspack.experiments.VirtualModulesPlugin
1066
+ const vmPlugin = new VMP({})
1067
+
1068
+ shared.vmPlugins[envName] = vmPlugin
1069
+ shared.readyVmPlugins[envName] = false
1070
+
1071
+ rspackConfig.plugins.push(vmPlugin)
1072
+ rspackConfig.plugins.push({
1073
+ apply(compiler: Rspack.Compiler) {
1074
+ shared.inputFileSystems[envName] = compiler.inputFileSystem
1075
+ compiler.hooks.thisCompilation.tap(
1076
+ 'TanStackStartImportProtectionVirtualModulesReady',
1077
+ () => {
1078
+ shared.readyVmPlugins[envName] = true
1079
+ flushPendingWrites(shared, envName)
1080
+ },
1081
+ )
1082
+ },
1083
+ })
1084
+ })
1085
+
1086
+ for (const environment of opts.environments) {
1087
+ api.transform(
1088
+ {
1089
+ test: /\.[cm]?[tj]sx?$/,
1090
+ environments: [environment.name],
1091
+ order: 'post',
1092
+ },
1093
+ async (ctx) => {
1094
+ if (!config.enabled) {
1095
+ return ctx.code
1096
+ }
1097
+
1098
+ const envName = environment.name
1099
+ const envType = getImportProtectionEnvType(config, envName)
1100
+ const envState = getOrCreateEnvState(envStates, envName)
1101
+ const id = ctx.resource
1102
+ const file = normalizeFilePath(ctx.resourcePath)
1103
+
1104
+ if (!shouldCheckImportProtectionImporter(config, file)) {
1105
+ return ctx.code
1106
+ }
1107
+
1108
+ const matchers = getRulesForEnvironment(config, envName)
1109
+ const relativeFile = getImportProtectionRelativePath(config.root, file)
1110
+ const importSources = getImportSources(ctx.code)
1111
+ const transformedImportSources = new Set(importSources)
1112
+ const transformInputFileSystem = shared.inputFileSystems[envName]
1113
+ const loadOriginalCodeForTransform: OriginalCodeLoader =
1114
+ transformInputFileSystem
1115
+ ? (target) =>
1116
+ loadOriginalCodeFromInputFileSystem(
1117
+ transformInputFileSystem,
1118
+ target,
1119
+ )
1120
+ : () => Promise.resolve(undefined)
1121
+ const originalCode =
1122
+ config.command === 'build'
1123
+ ? await loadOriginalCode(
1124
+ fileReadCache,
1125
+ file,
1126
+ loadOriginalCodeForTransform,
1127
+ )
1128
+ : undefined
1129
+ const buildImportSources = originalCode
1130
+ ? getImportSources(originalCode)
1131
+ : []
1132
+ const buildTransformResult: TransformResult | undefined =
1133
+ config.command === 'build'
1134
+ ? {
1135
+ code: ctx.code,
1136
+ map: undefined,
1137
+ originalCode,
1138
+ lineIndex: buildLineIndex(ctx.code),
1139
+ }
1140
+ : undefined
1141
+
1142
+ if (config.command === 'build') {
1143
+ const relativeBuildFile = getImportProtectionRelativePath(
1144
+ config.root,
1145
+ file,
1146
+ )
1147
+ addTransformResult(
1148
+ envState.buildTransformResults,
1149
+ file,
1150
+ buildTransformResult!,
1151
+ )
1152
+ addTransformResult(
1153
+ envState.buildTransformResults,
1154
+ relativeBuildFile,
1155
+ buildTransformResult!,
1156
+ )
1157
+ if (id !== file) {
1158
+ addTransformResult(
1159
+ envState.buildTransformResults,
1160
+ id,
1161
+ buildTransformResult!,
1162
+ )
1163
+ }
1164
+ }
1165
+
1166
+ const hasServerOnlyMarker = importSources.some((source) =>
1167
+ config.markerSpecifiers.serverOnly.has(source),
1168
+ )
1169
+ const hasClientOnlyMarker = importSources.some((source) =>
1170
+ config.markerSpecifiers.clientOnly.has(source),
1171
+ )
1172
+
1173
+ if (hasServerOnlyMarker && hasClientOnlyMarker) {
1174
+ throw new Error(
1175
+ `[import-protection] File "${relativeFile}" has both server-only and client-only markers. This is not allowed.`,
1176
+ )
1177
+ }
1178
+
1179
+ const markerKind = hasServerOnlyMarker
1180
+ ? ('server' as const)
1181
+ : hasClientOnlyMarker
1182
+ ? ('client' as const)
1183
+ : undefined
1184
+
1185
+ const fileMatch = checkFileDenial(relativeFile, matchers)
1186
+ const markerViolation =
1187
+ (envType === 'client' && markerKind === 'server') ||
1188
+ (envType === 'server' && markerKind === 'client')
1189
+
1190
+ if (fileMatch || markerViolation) {
1191
+ let exportNames: Array<string> = []
1192
+
1193
+ try {
1194
+ exportNames = getNamedExports(ctx.code)
1195
+ } catch {
1196
+ exportNames = []
1197
+ }
1198
+
1199
+ if (config.command === 'build') {
1200
+ return generateSelfContainedMockModule(exportNames)
1201
+ }
1202
+
1203
+ const runtimeId = ensureRuntimeMockModule({
1204
+ shared,
1205
+ envName,
1206
+ mode: config.mockAccess,
1207
+ env: envName,
1208
+ importer: file,
1209
+ specifier: relativeFile,
1210
+ })
1211
+
1212
+ return generateDevSelfDenialModule(exportNames, runtimeId)
1213
+ }
1214
+
1215
+ const deniedSpecifierReplacements = new Map<string, string>()
1216
+ const exportsBySource = (() => {
1217
+ try {
1218
+ return getMockExportNamesBySource(ctx.code)
1219
+ } catch {
1220
+ return new Map<string, Array<string>>()
1221
+ }
1222
+ })()
1223
+
1224
+ for (const source of importSources) {
1225
+ const specifierMatch = matchesAny(source, matchers.specifiers)
1226
+ if (!specifierMatch && config.command === 'build') {
1227
+ const resolved = await resolveAgainstImporter({
1228
+ envState,
1229
+ config,
1230
+ ctx,
1231
+ importerId: id,
1232
+ source,
1233
+ extensionlessResolver,
1234
+ })
1235
+
1236
+ if (resolved) {
1237
+ const relativeResolved = getImportProtectionRelativePath(
1238
+ config.root,
1239
+ resolved,
1240
+ )
1241
+ const buildFileMatch = checkFileDenial(relativeResolved, matchers)
1242
+ if (
1243
+ buildFileMatch &&
1244
+ hasOriginalUnsafeUsage(buildTransformResult, source, envType)
1245
+ ) {
1246
+ deferFileViolation(envState, {
1247
+ importer: file,
1248
+ specifier: source,
1249
+ resolved,
1250
+ relativeResolved,
1251
+ pattern: buildFileMatch.pattern,
1252
+ useOriginalLocation: true,
1253
+ })
1254
+ }
1255
+ }
1256
+
1257
+ continue
1258
+ }
1259
+
1260
+ if (!specifierMatch) {
1261
+ continue
1262
+ }
1263
+
1264
+ const resolved = await resolveAgainstImporter({
1265
+ envState,
1266
+ config,
1267
+ ctx,
1268
+ importerId: id,
1269
+ source,
1270
+ extensionlessResolver,
1271
+ })
1272
+
1273
+ const runtimeId =
1274
+ config.command === 'build'
1275
+ ? ensureSilentMockModule(shared, envName)
1276
+ : ensureRuntimeMockModule({
1277
+ shared,
1278
+ envName,
1279
+ mode: config.mockAccess,
1280
+ env: envName,
1281
+ importer: file,
1282
+ specifier: source,
1283
+ })
1284
+
1285
+ const replacement = ensureMockEdgeModule({
1286
+ shared,
1287
+ envName,
1288
+ payload: {
1289
+ exports: exportsBySource.get(source) ?? [],
1290
+ runtimeId,
1291
+ violation: {
1292
+ env: envName,
1293
+ envType,
1294
+ importer: file,
1295
+ specifier: source,
1296
+ ...(resolved ? { resolved } : {}),
1297
+ patternText: serializePattern(specifierMatch.pattern),
1298
+ },
1299
+ },
1300
+ })
1301
+
1302
+ deniedSpecifierReplacements.set(source, replacement)
1303
+ }
1304
+
1305
+ if (config.command === 'build') {
1306
+ for (const source of buildImportSources) {
1307
+ if (transformedImportSources.has(source)) {
1308
+ continue
1309
+ }
1310
+
1311
+ if (matchesAny(source, matchers.specifiers)) {
1312
+ continue
1313
+ }
1314
+
1315
+ const resolved = await resolveAgainstImporter({
1316
+ envState,
1317
+ config,
1318
+ ctx,
1319
+ importerId: id,
1320
+ source,
1321
+ extensionlessResolver,
1322
+ })
1323
+
1324
+ if (!resolved) {
1325
+ continue
1326
+ }
1327
+
1328
+ const relativeResolved = getImportProtectionRelativePath(
1329
+ config.root,
1330
+ resolved,
1331
+ )
1332
+ const buildFileMatch = checkFileDenial(relativeResolved, matchers)
1333
+ if (
1334
+ !buildFileMatch ||
1335
+ !hasOriginalUnsafeUsage(buildTransformResult, source, envType)
1336
+ ) {
1337
+ continue
1338
+ }
1339
+
1340
+ deferFileViolation(envState, {
1341
+ importer: file,
1342
+ specifier: source,
1343
+ resolved,
1344
+ relativeResolved,
1345
+ pattern: buildFileMatch.pattern,
1346
+ useOriginalLocation: true,
1347
+ })
1348
+ }
1349
+ }
1350
+
1351
+ if (deniedSpecifierReplacements.size === 0) {
1352
+ return ctx.code
1353
+ }
1354
+
1355
+ const rewritten = rewriteDeniedImports(
1356
+ ctx.code,
1357
+ id,
1358
+ new Set(deniedSpecifierReplacements.keys()),
1359
+ (source) => deniedSpecifierReplacements.get(source) ?? source,
1360
+ )
1361
+
1362
+ if (!rewritten) {
1363
+ return ctx.code
1364
+ }
1365
+
1366
+ return {
1367
+ code: rewritten.code,
1368
+ map: normalizeSourceMap(rewritten.map) ?? null,
1369
+ }
1370
+ },
1371
+ )
1372
+ }
1373
+
1374
+ api.processAssets(
1375
+ {
1376
+ stage: 'report',
1377
+ environments: opts.environments.map((environment) => environment.name),
1378
+ },
1379
+ async (context: ProcessAssetsContext) => {
1380
+ if (!config.enabled) {
1381
+ return
1382
+ }
1383
+
1384
+ const envName = context.environment.name
1385
+ const envType = getImportProtectionEnvType(config, envName)
1386
+ const envState = getOrCreateEnvState(envStates, envName)
1387
+ const matchers = getRulesForEnvironment(config, envName)
1388
+ const processFileReadCache = new Map<
1389
+ string,
1390
+ Promise<string | undefined>
1391
+ >()
1392
+ const loadOriginalCodeFromCompilation: OriginalCodeLoader = (file) =>
1393
+ loadOriginalCode(
1394
+ processFileReadCache,
1395
+ file,
1396
+ context.compilation.inputFileSystem
1397
+ ? (target) =>
1398
+ loadOriginalCodeFromInputFileSystem(
1399
+ context.compilation.inputFileSystem!,
1400
+ target,
1401
+ )
1402
+ : () => Promise.resolve(undefined),
1403
+ )
1404
+ const allModules = Array.from(context.compilation.modules)
1405
+ const relevantModules = allModules.filter(isImportProtectionSourceModule)
1406
+
1407
+ const provider = await buildTransformResultProvider({
1408
+ modules: relevantModules,
1409
+ root: config.root,
1410
+ loadOriginalCode: loadOriginalCodeFromCompilation,
1411
+ preloaded: envState.buildTransformResults,
1412
+ })
1413
+ const importLocCache = new ImportLocCache()
1414
+ const markerKindCache = new Map<
1415
+ string,
1416
+ Promise<'server' | 'client' | undefined>
1417
+ >()
1418
+ const { graph, edges } = buildCompilationGraph({
1419
+ compilation: context.compilation,
1420
+ modules: relevantModules,
1421
+ })
1422
+ const liveFileEdgeKeys = new Set(
1423
+ edges
1424
+ .filter((edge) => !!edge.specifier)
1425
+ .map(
1426
+ (edge) =>
1427
+ `${normalizeFilePath(edge.importer)}::${edge.specifier!}::${normalizeFilePath(edge.resolved)}`,
1428
+ ),
1429
+ )
1430
+ const survivingModules = new Set<string>()
1431
+ for (const module of relevantModules) {
1432
+ for (const candidate of buildResolutionCandidates(
1433
+ getModuleFile(module),
1434
+ )) {
1435
+ survivingModules.add(candidate)
1436
+ }
1437
+ }
1438
+
1439
+ const didModuleSurvive = (id: string): boolean =>
1440
+ buildResolutionCandidates(id).some((candidate) =>
1441
+ survivingModules.has(candidate),
1442
+ )
1443
+
1444
+ for (const module of relevantModules) {
1445
+ const payload = getMockEdgePayloadFromFile(getModuleFile(module))
1446
+ if (!payload) {
1447
+ continue
1448
+ }
1449
+ if (
1450
+ !shouldCheckImportProtectionImporter(
1451
+ config,
1452
+ payload.violation.importer,
1453
+ )
1454
+ ) {
1455
+ continue
1456
+ }
1457
+
1458
+ const info = await buildViolationInfo({
1459
+ config,
1460
+ provider,
1461
+ graph,
1462
+ importLocCache,
1463
+ envName,
1464
+ envType,
1465
+ importer: payload.violation.importer,
1466
+ source: payload.violation.specifier,
1467
+ resolved: payload.violation.resolved,
1468
+ type: 'specifier',
1469
+ pattern: payload.violation.patternText,
1470
+ preferOriginalCode: true,
1471
+ })
1472
+
1473
+ await reportViolation({
1474
+ config,
1475
+ envState,
1476
+ compilation: context.compilation,
1477
+ rspack: context.compiler.rspack,
1478
+ info,
1479
+ })
1480
+ }
1481
+
1482
+ for (const edge of edges) {
1483
+ if (!edge.specifier) {
1484
+ continue
1485
+ }
1486
+ if (!shouldCheckImportProtectionImporter(config, edge.importer)) {
1487
+ continue
1488
+ }
1489
+
1490
+ const relativeResolved = getImportProtectionRelativePath(
1491
+ config.root,
1492
+ edge.resolved,
1493
+ )
1494
+ if (isFileExcluded(relativeResolved, matchers)) {
1495
+ continue
1496
+ }
1497
+ const fileMatch = checkFileDenial(relativeResolved, matchers)
1498
+ if (fileMatch) {
1499
+ const info = await buildViolationInfo({
1500
+ config,
1501
+ provider,
1502
+ graph,
1503
+ importLocCache,
1504
+ envName,
1505
+ envType,
1506
+ importer: edge.importer,
1507
+ source: edge.specifier,
1508
+ resolved: edge.resolved,
1509
+ type: 'file',
1510
+ pattern: fileMatch.pattern,
1511
+ })
1512
+
1513
+ await reportViolation({
1514
+ config,
1515
+ envState,
1516
+ compilation: context.compilation,
1517
+ rspack: context.compiler.rspack,
1518
+ info,
1519
+ })
1520
+ continue
1521
+ }
1522
+
1523
+ const markerKind = await getMarkerKindForFile({
1524
+ config,
1525
+ provider,
1526
+ loadOriginalCode: loadOriginalCodeFromCompilation,
1527
+ markerKindCache,
1528
+ file: edge.resolved,
1529
+ })
1530
+ const violatesMarker =
1531
+ (envType === 'client' && markerKind === 'server') ||
1532
+ (envType === 'server' && markerKind === 'client')
1533
+
1534
+ if (!violatesMarker) {
1535
+ continue
1536
+ }
1537
+
1538
+ const info = await buildViolationInfo({
1539
+ config,
1540
+ provider,
1541
+ graph,
1542
+ importLocCache,
1543
+ envName,
1544
+ envType,
1545
+ importer: edge.importer,
1546
+ source: edge.specifier,
1547
+ resolved: edge.resolved,
1548
+ type: 'marker',
1549
+ })
1550
+
1551
+ await reportViolation({
1552
+ config,
1553
+ envState,
1554
+ compilation: context.compilation,
1555
+ rspack: context.compiler.rspack,
1556
+ info,
1557
+ })
1558
+ }
1559
+
1560
+ for (const violation of envState.deferredFileViolations) {
1561
+ const liveEdgeKey = `${normalizeFilePath(violation.importer)}::${violation.specifier}::${normalizeFilePath(violation.resolved)}`
1562
+ if (liveFileEdgeKeys.has(liveEdgeKey)) {
1563
+ continue
1564
+ }
1565
+
1566
+ if (!didModuleSurvive(violation.resolved)) {
1567
+ continue
1568
+ }
1569
+
1570
+ if (!didModuleSurvive(violation.importer)) {
1571
+ continue
1572
+ }
1573
+
1574
+ const info = await buildViolationInfo({
1575
+ config,
1576
+ provider,
1577
+ graph,
1578
+ importLocCache,
1579
+ envName,
1580
+ envType,
1581
+ importer: violation.importer,
1582
+ source: violation.specifier,
1583
+ resolved: violation.resolved,
1584
+ type: 'file',
1585
+ pattern: violation.pattern,
1586
+ preferOriginalCode: violation.useOriginalLocation,
1587
+ })
1588
+
1589
+ await reportViolation({
1590
+ config,
1591
+ envState,
1592
+ compilation: context.compilation,
1593
+ rspack: context.compiler.rspack,
1594
+ info,
1595
+ })
1596
+ }
1597
+ },
1598
+ )
1599
+ }