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