@remix-run/assets 0.5.0 → 0.7.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 (84) hide show
  1. package/README.md +185 -104
  2. package/dist/assets.d.ts +4 -1
  3. package/dist/assets.d.ts.map +1 -1
  4. package/dist/lib/access.d.ts +29 -2
  5. package/dist/lib/access.d.ts.map +1 -1
  6. package/dist/lib/access.js +52 -26
  7. package/dist/lib/asset-server.d.ts +61 -25
  8. package/dist/lib/asset-server.d.ts.map +1 -1
  9. package/dist/lib/asset-server.js +127 -55
  10. package/dist/lib/compilation-error.d.ts +1 -1
  11. package/dist/lib/compilation-error.d.ts.map +1 -1
  12. package/dist/lib/files/compiler.d.ts +1 -1
  13. package/dist/lib/files/compiler.d.ts.map +1 -1
  14. package/dist/lib/files/compiler.js +28 -31
  15. package/dist/lib/files/config.d.ts +6 -0
  16. package/dist/lib/files/config.d.ts.map +1 -1
  17. package/dist/lib/files/config.js +9 -0
  18. package/dist/lib/fingerprint.d.ts +0 -4
  19. package/dist/lib/fingerprint.d.ts.map +1 -1
  20. package/dist/lib/fingerprint.js +0 -4
  21. package/dist/lib/hmr.d.ts +7 -0
  22. package/dist/lib/hmr.d.ts.map +1 -1
  23. package/dist/lib/hmr.js +198 -25
  24. package/dist/lib/injected-packages.d.ts +3 -2
  25. package/dist/lib/injected-packages.d.ts.map +1 -1
  26. package/dist/lib/injected-packages.js +11 -8
  27. package/dist/lib/inspection.d.ts +39 -0
  28. package/dist/lib/inspection.d.ts.map +1 -0
  29. package/dist/lib/inspection.js +160 -0
  30. package/dist/lib/routes.d.ts +12 -3
  31. package/dist/lib/routes.d.ts.map +1 -1
  32. package/dist/lib/routes.js +124 -80
  33. package/dist/lib/scripts/compiler.d.ts +8 -2
  34. package/dist/lib/scripts/compiler.d.ts.map +1 -1
  35. package/dist/lib/scripts/compiler.js +188 -38
  36. package/dist/lib/scripts/emit.d.ts +2 -1
  37. package/dist/lib/scripts/emit.d.ts.map +1 -1
  38. package/dist/lib/scripts/emit.js +25 -16
  39. package/dist/lib/scripts/resolve.d.ts +7 -1
  40. package/dist/lib/scripts/resolve.d.ts.map +1 -1
  41. package/dist/lib/scripts/resolve.js +119 -12
  42. package/dist/lib/scripts/specifiers.d.ts +2 -0
  43. package/dist/lib/scripts/specifiers.d.ts.map +1 -0
  44. package/dist/lib/scripts/specifiers.js +9 -0
  45. package/dist/lib/scripts/transform.d.ts +2 -3
  46. package/dist/lib/scripts/transform.d.ts.map +1 -1
  47. package/dist/lib/scripts/transform.js +4 -18
  48. package/dist/lib/styles/compiler.d.ts +0 -1
  49. package/dist/lib/styles/compiler.d.ts.map +1 -1
  50. package/dist/lib/styles/compiler.js +105 -25
  51. package/dist/lib/styles/emit.d.ts +2 -1
  52. package/dist/lib/styles/emit.d.ts.map +1 -1
  53. package/dist/lib/styles/emit.js +12 -10
  54. package/dist/lib/styles/resolve.d.ts +0 -1
  55. package/dist/lib/styles/resolve.d.ts.map +1 -1
  56. package/dist/lib/styles/resolve.js +8 -9
  57. package/dist/lib/styles/transform.d.ts +0 -2
  58. package/dist/lib/styles/transform.d.ts.map +1 -1
  59. package/dist/lib/styles/transform.js +2 -9
  60. package/dist/lib/virtual-store.d.ts +8 -0
  61. package/dist/lib/virtual-store.d.ts.map +1 -0
  62. package/dist/lib/virtual-store.js +100 -0
  63. package/package.json +6 -6
  64. package/src/assets.ts +9 -1
  65. package/src/lib/access.ts +83 -30
  66. package/src/lib/asset-server.ts +224 -94
  67. package/src/lib/compilation-error.ts +4 -3
  68. package/src/lib/files/compiler.ts +32 -42
  69. package/src/lib/files/config.ts +17 -0
  70. package/src/lib/fingerprint.ts +0 -9
  71. package/src/lib/hmr.ts +210 -26
  72. package/src/lib/injected-packages.ts +16 -10
  73. package/src/lib/inspection.ts +229 -0
  74. package/src/lib/routes.ts +158 -126
  75. package/src/lib/scripts/compiler.ts +240 -54
  76. package/src/lib/scripts/emit.ts +34 -19
  77. package/src/lib/scripts/resolve.ts +175 -13
  78. package/src/lib/scripts/specifiers.ts +11 -0
  79. package/src/lib/scripts/transform.ts +6 -25
  80. package/src/lib/styles/compiler.ts +137 -39
  81. package/src/lib/styles/emit.ts +18 -13
  82. package/src/lib/styles/resolve.ts +8 -10
  83. package/src/lib/styles/transform.ts +2 -12
  84. package/src/lib/virtual-store.ts +124 -0
@@ -17,19 +17,21 @@ import type {
17
17
  import { getFingerprintRequestCacheControl, parseFingerprintSuffix } from './fingerprint.ts'
18
18
  import { createHmrClientSource } from './hmr.ts'
19
19
  import type { HmrPayload } from './hmr.ts'
20
- import { getInjectedPackageRouteConfigs } from './injected-packages.ts'
20
+ import { getInjectedPackageMountConfigs } from './injected-packages.ts'
21
21
  import type { ModuleLoader } from './loaders.ts'
22
22
  import { normalizeFilePath, normalizePathname } from './paths.ts'
23
23
  import { compileRoutes } from './routes.ts'
24
24
  import type { CompiledRoutes } from './routes.ts'
25
25
  import { createResponseForScript, createScriptCompiler } from './scripts/compiler.ts'
26
- import type { ScriptHmrUpdate } from './scripts/compiler.ts'
26
+ import type { ScriptHmrUpdate, ScriptImportMap } from './scripts/compiler.ts'
27
27
  import { supportedScriptExtensions } from './scripts/resolve.ts'
28
28
  import { createResponseForStyle, createStyleCompiler, isStyleFilePath } from './styles/compiler.ts'
29
29
  import { resolveScriptTarget, resolveStyleTarget } from './target.ts'
30
30
  import type { AssetTarget, ResolvedScriptTarget, ResolvedStyleTarget } from './target.ts'
31
31
  import { createAssetServerWatcher } from './watch.ts'
32
+ import { createAssetInspector, type AssetDetails } from './inspection.ts'
32
33
  import type { AssetServerWatcher, ChokidarWatcher } from './watch.ts'
34
+ import { getVirtualStoreMountConfigs } from './virtual-store.ts'
33
35
 
34
36
  interface AssetServerWatchOptions {
35
37
  /**
@@ -89,6 +91,18 @@ export type BrowserHmrChannelFactory = () =>
89
91
  | undefined
90
92
  | Promise<BrowserHmrChannel | undefined>
91
93
 
94
+ /** Browser HMR integration options. */
95
+ export interface BrowserHmrOptions {
96
+ /** Creates the channel that delivers browser HMR events. */
97
+ channel: BrowserHmrChannelFactory
98
+ /**
99
+ * Module specifier resolved relative to the asset server's root directory. It must point to a
100
+ * browser module exporting `importModule(specifier, parentUrl)` for evaluating JavaScript updates.
101
+ * The module and its dependencies must be allowed and reachable through configured mounts.
102
+ */
103
+ moduleImporter?: string
104
+ }
105
+
92
106
  /**
93
107
  * Converts a watcher batch into ordered browser update or reload events.
94
108
  *
@@ -124,14 +138,12 @@ export type BrowserHmrFileEvent = {
124
138
  */
125
139
  export type BrowserHmrEvent =
126
140
  | {
141
+ /** Consumer-owned data keyed by a stable, versioned namespace. */
142
+ data: Record<string, BrowserHmrData>
127
143
  /** Absolute source file paths that triggered this update. */
128
144
  files?: string[]
129
- /** Update time used to bypass browser module and stylesheet caches. */
130
- timestamp: number
131
145
  /** Browser update event. */
132
146
  type: 'update'
133
- /** Accepted JavaScript and CSS module updates for the browser to apply in place. */
134
- updates: Extract<HmrPayload, { type: 'browser:update' }>['updates']
135
147
  }
136
148
  | {
137
149
  /** Absolute source file paths that could not be handled in place. */
@@ -140,16 +152,15 @@ export type BrowserHmrEvent =
140
152
  type: 'reload'
141
153
  }
142
154
 
143
- interface FingerprintOptions {
144
- /**
145
- * Per-build invalidation token that must change whenever fingerprinted asset URLs
146
- * should be invalidated together.
147
- */
148
- buildId: string
149
- }
150
-
151
155
  type AssetSourceMaps = 'inline' | 'external'
152
156
  type AssetSourceMapSourcePaths = 'url' | 'absolute'
157
+ type BrowserHmrData =
158
+ | null
159
+ | boolean
160
+ | number
161
+ | string
162
+ | BrowserHmrData[]
163
+ | { [key: string]: BrowserHmrData }
153
164
 
154
165
  interface AssetServerScriptOptions {
155
166
  /**
@@ -157,7 +168,7 @@ interface AssetServerScriptOptions {
157
168
  * `{ 'process.env.NODE_ENV': '"production"' }`
158
169
  */
159
170
  define?: Record<string, string>
160
- /** Import specifiers to leave unrewritten (CDN URLs, import map entries, etc.) */
171
+ /** Import specifiers to treat as external dependencies (CDN URLs, import map entries, etc.) */
161
172
  external?: string[]
162
173
  /**
163
174
  * Synchronous loaders that post-process compiled JavaScript.
@@ -172,6 +183,10 @@ interface AssetServerScriptOptions {
172
183
  }
173
184
 
174
185
  const scriptExtensionSet = new Set<string>(supportedScriptExtensions)
186
+ const defaultMounts = {
187
+ app: 'app',
188
+ npm: 'node_modules',
189
+ } as const
175
190
 
176
191
  /**
177
192
  * Options used to construct an {@link AssetServer} via {@link createAssetServer}.
@@ -179,8 +194,14 @@ const scriptExtensionSet = new Set<string>(supportedScriptExtensions)
179
194
  export interface AssetServerOptions<transforms extends AssetRequestTransformMap = {}> {
180
195
  /** Public mount path for this asset server, e.g. `'/assets'`. */
181
196
  basePath: string
182
- /** File patterns keyed by public URL patterns. */
183
- fileMap: Readonly<Record<string, string>>
197
+ /**
198
+ * Directories to mount at public URL paths.
199
+ *
200
+ * Each key is a public URL path and its value is a directory relative to `rootDir`. Defaults to
201
+ * `{ app: 'app', npm: 'node_modules' }`. Public paths must not contain query strings, fragments,
202
+ * or encoded dot segments.
203
+ */
204
+ mounts?: Readonly<Record<string, string>>
184
205
  /**
185
206
  * Root directory used to resolve relative file paths. Defaults to `process.cwd()`.
186
207
  */
@@ -191,7 +212,7 @@ export interface AssetServerOptions<transforms extends AssetRequestTransformMap
191
212
  allowFiles: readonly string[]
192
213
  /**
193
214
  * Exact package names whose files are allowed to be served. Dependencies and installed optional
194
- * dependencies are allowed automatically. Package files must still match `fileMap`.
215
+ * dependencies are allowed automatically. Package files must still be within a configured mount.
195
216
  */
196
217
  allowPackages?: readonly string[]
197
218
  /**
@@ -199,12 +220,12 @@ export interface AssetServerOptions<transforms extends AssetRequestTransformMap
199
220
  */
200
221
  denyFiles?: readonly string[]
201
222
  /**
202
- * Controls optional source-based URL fingerprinting for rewritten asset URLs.
223
+ * Controls optional content-based URL fingerprinting for served asset URLs.
203
224
  *
204
225
  * When omitted, all served assets use stable non-fingerprinted URLs with `Cache-Control: no-cache`.
205
226
  * Cannot be used together with active watch mode. Set `watch: false` when fingerprinting.
206
227
  */
207
- fingerprint?: FingerprintOptions
228
+ fingerprint?: boolean
208
229
  /**
209
230
  * Shared compatibility target for scripts and styles. Browser targets apply to both
210
231
  * pipelines, and `es` only affects scripts.
@@ -242,7 +263,7 @@ export interface AssetServerOptions<transforms extends AssetRequestTransformMap
242
263
  * HMR requires `watch` to be enabled. The factory is called once for this asset server. Returning
243
264
  * `undefined` leaves HMR inactive; a returned channel is closed by `assetServer.close()`.
244
265
  */
245
- hmr?: BrowserHmrChannelFactory
266
+ hmr?: BrowserHmrChannelFactory | BrowserHmrOptions
246
267
  /**
247
268
  * Enable filesystem-backed cache invalidation for long-lived server instances.
248
269
  * Enabled by default. Pass `true` to use the default watcher options, an options
@@ -271,6 +292,18 @@ export type AssetServerGetHrefOptions<transforms extends AssetRequestTransformMa
271
292
  transform: readonly AssetTransformInvocation<transforms>[]
272
293
  }
273
294
 
295
+ /**
296
+ * Metadata needed to render or load a script entry module.
297
+ */
298
+ export interface ScriptEntry {
299
+ /** Public URL for the script entry module. */
300
+ href: string
301
+ /** Public URLs that should be emitted as `modulepreload` hints for this script graph. */
302
+ preloads: string[]
303
+ /** Import map entries required to resolve this script graph in the browser. */
304
+ importMap: ScriptImportMap
305
+ }
306
+
274
307
  /**
275
308
  * Serves compiled scripts and styles for asset requests routed to it.
276
309
  * Construct with {@link createAssetServer}.
@@ -285,10 +318,28 @@ export interface AssetServer<transforms extends AssetRequestTransformMap = {}> {
285
318
  * Returns the request href for a served asset file.
286
319
  */
287
320
  getHref(filePath: string, options?: AssetServerGetHrefOptions<transforms>): Promise<string>
321
+ /**
322
+ * Returns the href, preload URLs, and import map for a script entry module.
323
+ */
324
+ getScriptEntry(filePath: string): Promise<ScriptEntry>
288
325
  /**
289
326
  * Returns preload URLs for one or more served asset files, ordered shallowest-first.
290
327
  */
291
328
  getPreloads(filePath: string | readonly string[]): Promise<string[]>
329
+ /**
330
+ * Returns an import map for one or more script entry modules.
331
+ */
332
+ getImportMap(filePath: string | readonly string[]): Promise<ScriptImportMap>
333
+ /**
334
+ * Returns diagnostic details about one public asset URL or file path, including the matched mount
335
+ * roots, access rules, file type, and browser-reachability status.
336
+ */
337
+ getAssetDetails(input: string): Promise<AssetDetails>
338
+ /**
339
+ * Returns every file currently reachable through this asset server, sorted by public URL and
340
+ * then absolute file path.
341
+ */
342
+ getAssets(): Promise<AssetDetails[]>
292
343
  /**
293
344
  * Closes this server's filesystem watcher and browser HMR channel.
294
345
  *
@@ -301,17 +352,18 @@ type ResolvedAssetServerOptions<transforms extends AssetRequestTransformMap> = {
301
352
  allowFiles: readonly string[]
302
353
  allowPackages?: readonly string[]
303
354
  basePath: string
304
- buildId?: string
305
355
  define?: Record<string, string>
306
356
  denyFiles?: readonly string[]
307
357
  external: string[]
308
358
  files: ResolvedAssetServerFilesOptions
309
359
  fingerprintAssets: boolean
310
360
  hmr: BrowserHmrChannelFactory | null
361
+ hmrModuleImporter: string | null
311
362
  minify: boolean
312
363
  loaders: readonly ModuleLoader[]
313
364
  onError: NonNullable<AssetServerOptions['onError']>
314
365
  rootDir: string
366
+ mounts: Readonly<Record<string, string>>
315
367
  routes: CompiledRoutes
316
368
  sourceMapSourcePaths: 'url' | 'absolute'
317
369
  sourceMaps?: 'inline' | 'external'
@@ -339,7 +391,7 @@ export function getInternalWatchTargets<transforms extends AssetRequestTransform
339
391
  * Create an asset server instance
340
392
  *
341
393
  * Compiles TypeScript/JavaScript scripts and CSS styles on demand with optional
342
- * source-based URL fingerprinting, caching, and configurable file mapping.
394
+ * content-based URL fingerprinting, caching, and configurable directory mounts.
343
395
  *
344
396
  * @param options Server configuration
345
397
  * @returns A {@link AssetServer} with `fetch()`, `getHref()`, and `getPreloads()` methods
@@ -348,9 +400,6 @@ export function getInternalWatchTargets<transforms extends AssetRequestTransform
348
400
  * ```ts
349
401
  * let assetServer = createAssetServer({
350
402
  * basePath: '/assets',
351
- * fileMap: {
352
- * '/app/*path': 'app/*path',
353
- * },
354
403
  * allowFiles: ['app/routes.ts', 'app/**\/public/**'],
355
404
  * allowPackages: ['remix'],
356
405
  * denyFiles: ['app/**\/*.test.*'],
@@ -368,10 +417,17 @@ export function createAssetServer<const transforms extends AssetRequestTransform
368
417
  allowPackages: resolvedOptions.allowPackages,
369
418
  denyFiles: resolvedOptions.denyFiles,
370
419
  packageSearchRoots: hasPackages(resolvedOptions.allowPackages)
371
- ? getPackageSearchRoots(options.fileMap, resolvedOptions.rootDir)
420
+ ? getPackageSearchRoots(resolvedOptions.mounts, resolvedOptions.rootDir)
372
421
  : undefined,
373
422
  rootDir: resolvedOptions.rootDir,
374
423
  })
424
+ let assetInspector = createAssetInspector({
425
+ accessPolicy,
426
+ allowFiles: resolvedOptions.allowFiles,
427
+ fileExtensions: resolvedOptions.files.extensions,
428
+ rootDir: resolvedOptions.rootDir,
429
+ routes: resolvedOptions.routes,
430
+ })
375
431
  let watcher: AssetServerWatcher | null = null
376
432
  let chokidarWatcher: ChokidarWatcher | null = null
377
433
  let fileCompiler: FileCompiler | null = null
@@ -392,8 +448,8 @@ export function createAssetServer<const transforms extends AssetRequestTransform
392
448
  })
393
449
  let sendHmrPayload = resolvedOptions.hmr ? createHmrPayloadSender() : null
394
450
  let hmrPathnames = getHmrPathnames(resolvedOptions.basePath)
451
+ let hmrDataKey = `remix/assets@1:${resolvedOptions.basePath}`
395
452
  let scriptCompiler = createScriptCompiler({
396
- buildId: resolvedOptions.buildId,
397
453
  define: resolvedOptions.define,
398
454
  external: resolvedOptions.external,
399
455
  fingerprintAssets: resolvedOptions.fingerprintAssets,
@@ -425,7 +481,6 @@ export function createAssetServer<const transforms extends AssetRequestTransform
425
481
  watchMode: resolvedOptions.watchOptions !== null,
426
482
  })
427
483
  let styleCompiler = createStyleCompiler({
428
- buildId: resolvedOptions.buildId,
429
484
  fingerprintAssets: resolvedOptions.fingerprintAssets,
430
485
  getServedFileUrl: (identityPath: string, options: { transform: readonly string[] | null }) => {
431
486
  if (!fileCompiler) {
@@ -477,8 +532,8 @@ export function createAssetServer<const transforms extends AssetRequestTransform
477
532
  })
478
533
  if (resolvedOptions.files.extensions.length > 0) {
479
534
  fileCompiler = createFileCompiler({
480
- buildId: resolvedOptions.buildId,
481
535
  cache: resolvedOptions.files.cache,
536
+ cacheKey: resolvedOptions.files.cacheKey,
482
537
  extensions: resolvedOptions.files.extensions,
483
538
  fingerprintAssets: resolvedOptions.fingerprintAssets,
484
539
  globalTransforms: resolvedOptions.files.globalTransforms,
@@ -532,10 +587,10 @@ export function createAssetServer<const transforms extends AssetRequestTransform
532
587
  for (let event of events) {
533
588
  let normalizedFilePath = normalizeFilePath(event.filePath)
534
589
  let scriptUpdates = await scriptCompiler.classifyHmrFileEvent(normalizedFilePath, event.event)
535
- let scriptPayload = createScriptHmrPayload(scriptUpdates)
590
+ let scriptPayload = await createScriptHmrPayloadWithImportMaps(scriptUpdates)
536
591
  if (scriptPayload) {
537
592
  browserHmrEvents.push(
538
- createBrowserHmrEvent(scriptPayload, getScriptHmrUpdateFiles(scriptUpdates)),
593
+ createBrowserHmrEvent(scriptPayload, getScriptHmrUpdateFiles(scriptUpdates), hmrDataKey),
539
594
  )
540
595
  }
541
596
 
@@ -554,6 +609,7 @@ export function createAssetServer<const transforms extends AssetRequestTransform
554
609
  ],
555
610
  },
556
611
  [styleUpdate.filePath],
612
+ hmrDataKey,
557
613
  ),
558
614
  )
559
615
  }
@@ -562,6 +618,32 @@ export function createAssetServer<const transforms extends AssetRequestTransform
562
618
  }
563
619
 
564
620
  return browserHmrEvents
621
+
622
+ async function createScriptHmrPayloadWithImportMaps(
623
+ updates: ScriptHmrUpdate[],
624
+ ): Promise<Extract<HmrPayload, { type: 'browser:reload' | 'browser:update' }> | null> {
625
+ let payload = createScriptHmrPayload(updates)
626
+ if (!payload || payload.type === 'browser:reload') return payload
627
+
628
+ let acceptedUpdates = updates.filter((update) => update.accepted)
629
+ let importMaps = await Promise.all(
630
+ acceptedUpdates.map(async (update) => {
631
+ try {
632
+ return await scriptCompiler.getImportMap(update.acceptedFilePath)
633
+ } catch (error) {
634
+ if (!isAssetServerCompilationError(error)) throw error
635
+ return undefined
636
+ }
637
+ }),
638
+ )
639
+ return {
640
+ ...payload,
641
+ updates: payload.updates.map((update, index) => ({
642
+ ...update,
643
+ ...(importMaps[index] ? { importMap: importMaps[index] } : null),
644
+ })),
645
+ }
646
+ }
565
647
  }
566
648
 
567
649
  function updateBrowserHmrWatchedFiles(delta: BrowserHmrWatchedFileDelta): void {
@@ -575,6 +657,12 @@ export function createAssetServer<const transforms extends AssetRequestTransform
575
657
  }
576
658
 
577
659
  let assetServer: AssetServer<transforms> = {
660
+ getAssetDetails(input) {
661
+ return assetInspector.getAssetDetails(input)
662
+ },
663
+ getAssets() {
664
+ return assetInspector.getAssets()
665
+ },
578
666
  async fetch(request) {
579
667
  if (request.method !== 'GET' && request.method !== 'HEAD') return null
580
668
  let requestPathname = new URL(request.url).pathname
@@ -583,7 +671,13 @@ export function createAssetServer<const transforms extends AssetRequestTransform
583
671
  if (requestPathname === hmrPathnames.client) {
584
672
  let browserHmrChannel = await browserHmrChannelPromise
585
673
  assertBrowserEventUrl(browserHmrChannel?.url)
586
- return createHmrClientResponse(browserHmrChannel.url, request.method)
674
+ return await createHmrClientResponse(
675
+ browserHmrChannel.url,
676
+ hmrDataKey,
677
+ resolvedOptions.hmrModuleImporter,
678
+ request.method,
679
+ scriptCompiler,
680
+ )
587
681
  }
588
682
  }
589
683
 
@@ -619,8 +713,10 @@ export function createAssetServer<const transforms extends AssetRequestTransform
619
713
  let compiledStyle = styleResult.style
620
714
 
621
715
  if (parsedRequestPathname.requestedFingerprint !== null) {
622
- if (compiledStyle.fingerprint !== parsedRequestPathname.requestedFingerprint)
623
- return null
716
+ let fingerprint = parsedRequestPathname.isSourceMapRequest
717
+ ? compiledStyle.sourceMap?.fingerprint
718
+ : compiledStyle.fingerprint
719
+ if (fingerprint !== parsedRequestPathname.requestedFingerprint) return null
624
720
  }
625
721
 
626
722
  return createResponseForStyle(compiledStyle, {
@@ -684,7 +780,10 @@ export function createAssetServer<const transforms extends AssetRequestTransform
684
780
  let compiledScript = scriptResult.script
685
781
 
686
782
  if (parsedRequestPathname.requestedFingerprint !== null) {
687
- if (compiledScript.fingerprint !== parsedRequestPathname.requestedFingerprint) return null
783
+ let fingerprint = parsedRequestPathname.isSourceMapRequest
784
+ ? compiledScript.sourceMap?.fingerprint
785
+ : compiledScript.fingerprint
786
+ if (fingerprint !== parsedRequestPathname.requestedFingerprint) return null
688
787
  }
689
788
 
690
789
  return createResponseForScript(compiledScript, {
@@ -747,6 +846,15 @@ export function createAssetServer<const transforms extends AssetRequestTransform
747
846
 
748
847
  return scriptCompiler.getHref(filePath)
749
848
  },
849
+ async getScriptEntry(filePath) {
850
+ let [href, preloads, importMap] = await Promise.all([
851
+ assetServer.getHref(filePath),
852
+ assetServer.getPreloads(filePath),
853
+ assetServer.getImportMap(filePath),
854
+ ])
855
+
856
+ return { href, preloads, importMap }
857
+ },
750
858
  async getPreloads(filePath) {
751
859
  let filePaths = Array.isArray(filePath) ? filePath : [filePath]
752
860
  let fileAssetFiles: string[] = []
@@ -811,6 +919,19 @@ export function createAssetServer<const transforms extends AssetRequestTransform
811
919
 
812
920
  return mergePreloadLayers(await Promise.all(preloadLayerGroupPromises))
813
921
  },
922
+ async getImportMap(filePath) {
923
+ let filePaths = Array.isArray(filePath) ? filePath : [filePath]
924
+ for (let nextFilePath of filePaths) {
925
+ let typeCheckFilePath = stripFilePathUrlSuffix(nextFilePath)
926
+ if (!isScriptFilePath(typeCheckFilePath)) {
927
+ throw new TypeError(
928
+ `assetServer.getImportMap() only supports script files: ${nextFilePath}`,
929
+ )
930
+ }
931
+ }
932
+
933
+ return scriptCompiler.getImportMap(filePath)
934
+ },
814
935
  async close() {
815
936
  if (closed) return
816
937
  closed = true
@@ -876,13 +997,27 @@ function getHmrPathnames(basePath: string): { client: string; events: string } {
876
997
  }
877
998
  }
878
999
 
879
- function createHmrClientResponse(eventPathname: string, method: string): Response {
880
- return new Response(method === 'HEAD' ? null : createHmrClientSource({ eventPathname }), {
881
- headers: {
882
- 'Cache-Control': 'no-cache',
883
- 'Content-Type': 'application/javascript; charset=utf-8',
1000
+ async function createHmrClientResponse(
1001
+ eventPathname: string,
1002
+ dataKey: string,
1003
+ moduleImporter: string | null,
1004
+ method: string,
1005
+ scriptCompiler: ReturnType<typeof createScriptCompiler>,
1006
+ ): Promise<Response> {
1007
+ let moduleImporterHref = moduleImporter
1008
+ ? await scriptCompiler.resolveSpecifierFromRoot(moduleImporter)
1009
+ : null
1010
+ return new Response(
1011
+ method === 'HEAD'
1012
+ ? null
1013
+ : createHmrClientSource({ dataKey, eventPathname, moduleImporter: moduleImporterHref }),
1014
+ {
1015
+ headers: {
1016
+ 'Cache-Control': 'no-cache',
1017
+ 'Content-Type': 'application/javascript; charset=utf-8',
1018
+ },
884
1019
  },
885
- })
1020
+ )
886
1021
  }
887
1022
 
888
1023
  function assertBrowserEventUrl(url: string | undefined): asserts url is string {
@@ -898,6 +1033,7 @@ function createHmrPayloadSender(): (payload: HmrPayload) => void {
898
1033
  function createBrowserHmrEvent(
899
1034
  payload: Extract<HmrPayload, { type: 'browser:reload' | 'browser:update' }>,
900
1035
  files: readonly string[],
1036
+ dataKey: string,
901
1037
  ): BrowserHmrEvent {
902
1038
  if (payload.type === 'browser:reload') {
903
1039
  return {
@@ -907,10 +1043,14 @@ function createBrowserHmrEvent(
907
1043
  }
908
1044
 
909
1045
  return {
1046
+ data: {
1047
+ [dataKey]: {
1048
+ timestamp: payload.timestamp,
1049
+ updates: payload.updates,
1050
+ },
1051
+ },
910
1052
  ...(files.length === 0 ? {} : { files: [...files] }),
911
- timestamp: payload.timestamp,
912
1053
  type: 'update',
913
- updates: payload.updates,
914
1054
  }
915
1055
  }
916
1056
 
@@ -936,7 +1076,9 @@ export function createScriptHmrPayload(
936
1076
  timestamp,
937
1077
  type: 'browser:update',
938
1078
  updates: acceptedUpdates.map((update) => ({
939
- ...(update.acceptedPath === update.path ? {} : { acceptedPath: update.acceptedPath }),
1079
+ ...(update.acceptedUrlPathname === update.path
1080
+ ? {}
1081
+ : { acceptedPath: update.acceptedUrlPathname }),
940
1082
  path: update.path,
941
1083
  type: 'js',
942
1084
  })),
@@ -1000,37 +1142,43 @@ function resolveAssetServerOptions<transforms extends AssetRequestTransformMap>(
1000
1142
  let rootDir = normalizeFilePath(fs.realpathSync(path.resolve(options.rootDir ?? process.cwd())))
1001
1143
  let basePath = normalizeBasePath(options.basePath)
1002
1144
  let scriptOptions = options.scripts ?? {}
1003
- let fingerprintOptions = normalizeFingerprintOptions({
1145
+ let fingerprintAssets = normalizeFingerprintOptions({
1004
1146
  fingerprint: options.fingerprint,
1005
1147
  watch: options.watch,
1006
1148
  })
1007
1149
  let watchOptions = normalizeWatchOptions(options.watch)
1008
- let hmrFactory = normalizeHmrFactory(options.hmr)
1150
+ let hmr = normalizeHmrOptions(options.hmr)
1151
+ let mounts = options.mounts ?? defaultMounts
1009
1152
 
1010
- if (hmrFactory && watchOptions === null) {
1153
+ if (hmr.channel && watchOptions === null) {
1011
1154
  throw new TypeError('hmr requires watch mode')
1012
1155
  }
1156
+ if (Object.keys(mounts).length === 0) {
1157
+ throw new TypeError('mounts must include at least one entry')
1158
+ }
1013
1159
  return {
1014
1160
  allowFiles: options.allowFiles,
1015
1161
  allowPackages: options.allowPackages,
1016
1162
  basePath,
1017
- buildId: fingerprintOptions.buildId,
1018
1163
  define: scriptOptions.define,
1019
1164
  denyFiles: options.denyFiles,
1020
1165
  external: scriptOptions.external ?? [],
1021
1166
  files: normalizeFilesOptions(options.files),
1022
- fingerprintAssets: fingerprintOptions.enabled,
1023
- hmr: hmrFactory,
1167
+ fingerprintAssets,
1168
+ hmr: hmr.channel,
1169
+ hmrModuleImporter: hmr.moduleImporter,
1024
1170
  minify: options.minify ?? false,
1171
+ mounts,
1025
1172
  loaders: scriptOptions.loaders ?? [],
1026
1173
  onError: options.onError ?? defaultErrorHandler,
1027
1174
  rootDir,
1028
1175
  routes: compileRoutes(basePath, [
1029
1176
  {
1030
- fileMap: options.fileMap,
1177
+ mounts,
1031
1178
  rootDir,
1032
1179
  },
1033
- ...getInjectedPackageRouteConfigs(),
1180
+ ...getInjectedPackageMountConfigs(),
1181
+ ...getVirtualStoreMountConfigs({ mounts, rootDir }),
1034
1182
  ]),
1035
1183
  sourceMapSourcePaths: options.sourceMapSourcePaths ?? 'url',
1036
1184
  sourceMaps: options.sourceMaps,
@@ -1040,14 +1188,22 @@ function resolveAssetServerOptions<transforms extends AssetRequestTransformMap>(
1040
1188
  }
1041
1189
  }
1042
1190
 
1043
- function normalizeHmrFactory(factory: AssetServerOptions['hmr']): BrowserHmrChannelFactory | null {
1044
- if (factory === undefined) return null
1045
-
1046
- if (typeof factory !== 'function') {
1047
- throw new TypeError('hmr must be a function')
1191
+ function normalizeHmrOptions(options: AssetServerOptions['hmr']): {
1192
+ channel: BrowserHmrChannelFactory | null
1193
+ moduleImporter: string | null
1194
+ } {
1195
+ if (options === undefined) return { channel: null, moduleImporter: null }
1196
+ if (typeof options === 'function') return { channel: options, moduleImporter: null }
1197
+ if (!options || typeof options !== 'object' || typeof options.channel !== 'function') {
1198
+ throw new TypeError('hmr must be a function or an object with a channel function')
1048
1199
  }
1049
-
1050
- return factory
1200
+ if (
1201
+ options.moduleImporter !== undefined &&
1202
+ (typeof options.moduleImporter !== 'string' || options.moduleImporter.trim().length === 0)
1203
+ ) {
1204
+ throw new TypeError('hmr.moduleImporter must be a non-empty string')
1205
+ }
1206
+ return { channel: options.channel, moduleImporter: options.moduleImporter ?? null }
1051
1207
  }
1052
1208
 
1053
1209
  function createBrowserHmrChannel(
@@ -1108,37 +1264,20 @@ function normalizeBasePath(basePath: string): string {
1108
1264
  function normalizeFingerprintOptions(options: {
1109
1265
  fingerprint: AssetServerOptions['fingerprint']
1110
1266
  watch: AssetServerOptions['watch']
1111
- }):
1112
- | {
1113
- enabled: false
1114
- buildId?: string
1115
- }
1116
- | {
1117
- enabled: true
1118
- buildId: string
1119
- } {
1120
- if (!options.fingerprint) {
1121
- return {
1122
- enabled: false,
1123
- }
1267
+ }): boolean {
1268
+ if (options.fingerprint !== undefined && typeof options.fingerprint !== 'boolean') {
1269
+ throw new TypeError('fingerprint must be a boolean')
1124
1270
  }
1125
1271
 
1126
- if (typeof options.fingerprint.buildId !== 'string') {
1127
- throw new TypeError('fingerprint.buildId must be a string')
1128
- }
1129
-
1130
- if (options.fingerprint.buildId.length === 0) {
1131
- throw new TypeError('fingerprint.buildId must be a non-empty string')
1272
+ if (!options.fingerprint) {
1273
+ return false
1132
1274
  }
1133
1275
 
1134
1276
  if (options.watch !== false) {
1135
1277
  throw new TypeError('fingerprint cannot be used with watch mode')
1136
1278
  }
1137
1279
 
1138
- return {
1139
- enabled: true,
1140
- buildId: options.fingerprint.buildId,
1141
- }
1280
+ return true
1142
1281
  }
1143
1282
 
1144
1283
  function normalizeWatchOptions(
@@ -1154,19 +1293,10 @@ function hasPackages(packages: readonly string[] | undefined): boolean {
1154
1293
  }
1155
1294
 
1156
1295
  function getPackageSearchRoots(
1157
- fileMap: AssetServerOptions['fileMap'],
1296
+ mounts: Readonly<Record<string, string>>,
1158
1297
  rootDir: string,
1159
1298
  ): readonly string[] {
1160
- return Object.values(fileMap).map((filePattern) =>
1161
- path.resolve(rootDir, getStaticFilePatternPrefix(filePattern)),
1162
- )
1163
- }
1164
-
1165
- function getStaticFilePatternPrefix(filePattern: string): string {
1166
- let firstDynamicIndex = filePattern.search(/[*:]/)
1167
- let staticPrefix =
1168
- firstDynamicIndex === -1 ? filePattern : filePattern.slice(0, firstDynamicIndex)
1169
- return staticPrefix.replace(/[/\\]*$/, '')
1299
+ return Object.values(mounts).map((fileRoot) => path.resolve(rootDir, fileRoot))
1170
1300
  }
1171
1301
 
1172
1302
  function parseAssetRequestPathname(
@@ -2,7 +2,7 @@ type AssetServerCompilationErrorCode =
2
2
  | 'FILE_NOT_FOUND'
3
3
  | 'FILE_NOT_ALLOWED'
4
4
  | 'FILE_NOT_SUPPORTED'
5
- | 'FILE_OUTSIDE_FILE_MAP'
5
+ | 'FILE_OUTSIDE_MOUNTS'
6
6
  | 'FILE_TRANSFORM_QUERY_INVALID'
7
7
  | 'FILE_TRANSFORM_NOT_SUPPORTED'
8
8
  | 'FILE_TRANSFORM_RESULT_INVALID'
@@ -14,11 +14,12 @@ type AssetServerCompilationErrorCode =
14
14
  | 'IMPORT_RESOLUTION_FAILED'
15
15
  | 'IMPORT_NOT_SUPPORTED'
16
16
  | 'IMPORT_NOT_ALLOWED'
17
- | 'IMPORT_OUTSIDE_FILE_MAP'
17
+ | 'IMPORT_OUTSIDE_MOUNTS'
18
+ | 'IMPORT_RESOLUTION_NOT_DIRECTORY_UNIFORM'
18
19
  | 'URL_RESOLUTION_FAILED'
19
20
  | 'URL_NOT_SUPPORTED'
20
21
  | 'URL_NOT_ALLOWED'
21
- | 'URL_OUTSIDE_FILE_MAP'
22
+ | 'URL_OUTSIDE_MOUNTS'
22
23
 
23
24
  /**
24
25
  * Internal error used by the request-time asset compilation pipeline.