@remix-run/assets 0.4.4 → 0.6.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 (83) hide show
  1. package/README.md +331 -72
  2. package/dist/assets.d.ts +4 -1
  3. package/dist/assets.d.ts.map +1 -1
  4. package/dist/assets.js +2 -2
  5. package/dist/lib/access.d.ts +35 -4
  6. package/dist/lib/access.d.ts.map +1 -1
  7. package/dist/lib/access.js +332 -11
  8. package/dist/lib/asset-server.d.ts +138 -11
  9. package/dist/lib/asset-server.d.ts.map +1 -1
  10. package/dist/lib/asset-server.js +281 -30
  11. package/dist/lib/compilation-error.d.ts +1 -1
  12. package/dist/lib/compilation-error.d.ts.map +1 -1
  13. package/dist/lib/file-matcher.js +1 -1
  14. package/dist/lib/files/compiler.d.ts.map +1 -1
  15. package/dist/lib/files/compiler.js +9 -8
  16. package/dist/lib/files/config.js +1 -1
  17. package/dist/lib/hmr.d.ts +37 -0
  18. package/dist/lib/hmr.d.ts.map +1 -0
  19. package/dist/lib/hmr.js +357 -0
  20. package/dist/lib/injected-packages.d.ts +3 -2
  21. package/dist/lib/injected-packages.d.ts.map +1 -1
  22. package/dist/lib/injected-packages.js +43 -17
  23. package/dist/lib/inspection.d.ts +39 -0
  24. package/dist/lib/inspection.d.ts.map +1 -0
  25. package/dist/lib/inspection.js +160 -0
  26. package/dist/lib/loaders.d.ts +57 -0
  27. package/dist/lib/loaders.d.ts.map +1 -0
  28. package/dist/lib/loaders.js +1 -0
  29. package/dist/lib/module-store.d.ts +24 -0
  30. package/dist/lib/module-store.d.ts.map +1 -1
  31. package/dist/lib/module-store.js +136 -11
  32. package/dist/lib/routes.d.ts +11 -3
  33. package/dist/lib/routes.d.ts.map +1 -1
  34. package/dist/lib/routes.js +124 -80
  35. package/dist/lib/scripts/compiler.d.ts +24 -1
  36. package/dist/lib/scripts/compiler.d.ts.map +1 -1
  37. package/dist/lib/scripts/compiler.js +183 -29
  38. package/dist/lib/scripts/conditions.d.ts +2 -0
  39. package/dist/lib/scripts/conditions.d.ts.map +1 -0
  40. package/dist/lib/scripts/conditions.js +1 -0
  41. package/dist/lib/scripts/emit.d.ts +3 -0
  42. package/dist/lib/scripts/emit.d.ts.map +1 -1
  43. package/dist/lib/scripts/emit.js +24 -5
  44. package/dist/lib/scripts/resolve.d.ts +4 -0
  45. package/dist/lib/scripts/resolve.d.ts.map +1 -1
  46. package/dist/lib/scripts/resolve.js +75 -10
  47. package/dist/lib/scripts/transform.d.ts +9 -0
  48. package/dist/lib/scripts/transform.d.ts.map +1 -1
  49. package/dist/lib/scripts/transform.js +222 -18
  50. package/dist/lib/source-maps.js +1 -1
  51. package/dist/lib/styles/compiler.d.ts +14 -1
  52. package/dist/lib/styles/compiler.d.ts.map +1 -1
  53. package/dist/lib/styles/compiler.js +78 -14
  54. package/dist/lib/styles/emit.js +4 -4
  55. package/dist/lib/styles/resolve.d.ts.map +1 -1
  56. package/dist/lib/styles/resolve.js +16 -15
  57. package/dist/lib/styles/transform.js +5 -5
  58. package/dist/lib/target.d.ts +1 -1
  59. package/dist/lib/target.d.ts.map +1 -1
  60. package/dist/lib/watch.js +1 -1
  61. package/dist/types/hmr.d.ts +36 -0
  62. package/package.json +16 -11
  63. package/src/assets.ts +4 -1
  64. package/src/lib/access.ts +445 -11
  65. package/src/lib/asset-server.ts +466 -27
  66. package/src/lib/compilation-error.ts +4 -3
  67. package/src/lib/files/compiler.ts +9 -5
  68. package/src/lib/hmr.ts +397 -0
  69. package/src/lib/injected-packages.ts +52 -16
  70. package/src/lib/inspection.ts +229 -0
  71. package/src/lib/loaders.ts +80 -0
  72. package/src/lib/module-store.ts +190 -9
  73. package/src/lib/routes.ts +158 -126
  74. package/src/lib/scripts/compiler.ts +248 -24
  75. package/src/lib/scripts/conditions.ts +1 -0
  76. package/src/lib/scripts/emit.ts +50 -5
  77. package/src/lib/scripts/resolve.ts +129 -7
  78. package/src/lib/scripts/transform.ts +290 -19
  79. package/src/lib/styles/compiler.ts +108 -8
  80. package/src/lib/styles/emit.ts +1 -1
  81. package/src/lib/styles/resolve.ts +19 -15
  82. package/src/lib/styles/transform.ts +2 -2
  83. package/src/types/hmr.d.ts +36 -0
@@ -15,16 +15,21 @@ import type {
15
15
  ResolvedAssetServerFilesOptions,
16
16
  } from './files/config.ts'
17
17
  import { getFingerprintRequestCacheControl, parseFingerprintSuffix } from './fingerprint.ts'
18
- import { getInjectedPackageRouteConfigs } from './injected-packages.ts'
18
+ import { createHmrClientSource } from './hmr.ts'
19
+ import type { HmrPayload } from './hmr.ts'
20
+ import { getInjectedPackageMountConfigs } from './injected-packages.ts'
21
+ import type { ModuleLoader } from './loaders.ts'
19
22
  import { normalizeFilePath, normalizePathname } from './paths.ts'
20
23
  import { compileRoutes } from './routes.ts'
21
24
  import type { CompiledRoutes } from './routes.ts'
22
25
  import { createResponseForScript, createScriptCompiler } from './scripts/compiler.ts'
26
+ import type { ScriptHmrUpdate } from './scripts/compiler.ts'
23
27
  import { supportedScriptExtensions } from './scripts/resolve.ts'
24
28
  import { createResponseForStyle, createStyleCompiler, isStyleFilePath } from './styles/compiler.ts'
25
29
  import { resolveScriptTarget, resolveStyleTarget } from './target.ts'
26
30
  import type { AssetTarget, ResolvedScriptTarget, ResolvedStyleTarget } from './target.ts'
27
31
  import { createAssetServerWatcher } from './watch.ts'
32
+ import { createAssetInspector, type AssetDetails } from './inspection.ts'
28
33
  import type { AssetServerWatcher, ChokidarWatcher } from './watch.ts'
29
34
 
30
35
  interface AssetServerWatchOptions {
@@ -43,6 +48,99 @@ interface AssetServerWatchOptions {
43
48
  pollInterval?: number
44
49
  }
45
50
 
51
+ /**
52
+ * Bridge used by an asset server to watch browser source files and publish update events through a
53
+ * server-level HMR runtime.
54
+ *
55
+ * The asset server registers its file handler, keeps the channel's watched-file set in sync with
56
+ * compiled assets, injects `url` into its browser HMR client, and closes the channel when the asset
57
+ * server closes.
58
+ */
59
+ export interface BrowserHmrChannel {
60
+ /** Absolute EventSource URL that receives the browser HMR events produced by this channel. */
61
+ readonly url: string
62
+ /** Releases the channel and all file-watching resources it owns. */
63
+ close(): void
64
+ /**
65
+ * Registers the asset server's handler for file changes reported by the channel's watcher.
66
+ *
67
+ * @param handler Callback that converts a batch of file changes into browser update or reload
68
+ * events.
69
+ * @returns A cleanup function that stops forwarding file changes to this handler.
70
+ */
71
+ onFileEvents(handler: BrowserHmrFileEventHandler): () => void
72
+ /**
73
+ * Applies additions and removals to the channel's set of watched absolute file paths.
74
+ *
75
+ * @param delta Files to add and remove from the watcher.
76
+ */
77
+ updateWatchedFiles(delta: BrowserHmrWatchedFileDelta): void
78
+ }
79
+
80
+ /**
81
+ * Creates the browser HMR channel owned by an asset server instance.
82
+ *
83
+ * Returning `undefined` leaves browser HMR inactive. The asset server invokes this factory once,
84
+ * after construction, and closes a returned channel from `assetServer.close()`.
85
+ *
86
+ * @returns A channel, no channel, or a promise for either result.
87
+ */
88
+ export type BrowserHmrChannelFactory = () =>
89
+ | BrowserHmrChannel
90
+ | undefined
91
+ | Promise<BrowserHmrChannel | undefined>
92
+
93
+ /**
94
+ * Converts a watcher batch into ordered browser update or reload events.
95
+ *
96
+ * @param events File additions, changes, and removals reported together by the watcher.
97
+ * @returns Browser events to publish in their returned order.
98
+ */
99
+ export type BrowserHmrFileEventHandler = (
100
+ events: readonly BrowserHmrFileEvent[],
101
+ ) => Promise<readonly BrowserHmrEvent[]>
102
+
103
+ /**
104
+ * Watched file delta for a browser HMR channel.
105
+ */
106
+ export interface BrowserHmrWatchedFileDelta {
107
+ /** Absolute source file paths newly required by the asset server's compiled module graph. */
108
+ add: readonly string[]
109
+ /** Absolute source file paths no longer required by the asset server's compiled module graph. */
110
+ remove: readonly string[]
111
+ }
112
+
113
+ /**
114
+ * File watcher event reported to a browser HMR channel.
115
+ */
116
+ export type BrowserHmrFileEvent = {
117
+ /** Filesystem operation observed by the channel's watcher. */
118
+ event: 'add' | 'change' | 'unlink'
119
+ /** Absolute path of the source file that changed. */
120
+ filePath: string
121
+ }
122
+
123
+ /**
124
+ * Browser HMR event emitted to connected clients.
125
+ */
126
+ export type BrowserHmrEvent =
127
+ | {
128
+ /** Absolute source file paths that triggered this update. */
129
+ files?: string[]
130
+ /** Update time used to bypass browser module and stylesheet caches. */
131
+ timestamp: number
132
+ /** Browser update event. */
133
+ type: 'update'
134
+ /** Accepted JavaScript and CSS module updates for the browser to apply in place. */
135
+ updates: Extract<HmrPayload, { type: 'browser:update' }>['updates']
136
+ }
137
+ | {
138
+ /** Absolute source file paths that could not be handled in place. */
139
+ files?: string[]
140
+ /** Browser reload event. */
141
+ type: 'reload'
142
+ }
143
+
46
144
  interface FingerprintOptions {
47
145
  /**
48
146
  * Per-build invalidation token that must change whenever fingerprinted asset URLs
@@ -62,9 +160,23 @@ interface AssetServerScriptOptions {
62
160
  define?: Record<string, string>
63
161
  /** Import specifiers to leave unrewritten (CDN URLs, import map entries, etc.) */
64
162
  external?: string[]
163
+ /**
164
+ * Synchronous loaders that post-process compiled JavaScript.
165
+ *
166
+ * Loaders use Node's synchronous `load` hook signature. Later loaders wrap earlier loaders: for
167
+ * `[first, second]`, `second` is entered first and delegates through `first` to the default
168
+ * behavior. As a result, transformations performed after `nextLoad()` are applied in array order.
169
+ * Loaders run after TypeScript/JavaScript transformation and before HMR analysis and minification.
170
+ * Only `format: 'module'` is supported, and import attributes are unsupported.
171
+ */
172
+ loaders?: readonly ModuleLoader[]
65
173
  }
66
174
 
67
175
  const scriptExtensionSet = new Set<string>(supportedScriptExtensions)
176
+ const defaultMounts = {
177
+ app: 'app',
178
+ npm: 'node_modules',
179
+ } as const
68
180
 
69
181
  /**
70
182
  * Options used to construct an {@link AssetServer} via {@link createAssetServer}.
@@ -72,9 +184,14 @@ const scriptExtensionSet = new Set<string>(supportedScriptExtensions)
72
184
  export interface AssetServerOptions<transforms extends AssetRequestTransformMap = {}> {
73
185
  /** Public mount path for this asset server, e.g. `'/assets'`. */
74
186
  basePath: string
75
- /** File patterns keyed by public URL patterns relative to `basePath`. */
76
- /** File patterns keyed by public URL patterns. */
77
- fileMap: Readonly<Record<string, string>>
187
+ /**
188
+ * Directories to mount at public URL paths.
189
+ *
190
+ * Each key is a public URL path and its value is a directory relative to `rootDir`. Defaults to
191
+ * `{ app: 'app', npm: 'node_modules' }`. Public paths must not contain query strings, fragments,
192
+ * or encoded dot segments.
193
+ */
194
+ mounts?: Readonly<Record<string, string>>
78
195
  /**
79
196
  * Root directory used to resolve relative file paths. Defaults to `process.cwd()`.
80
197
  */
@@ -82,11 +199,16 @@ export interface AssetServerOptions<transforms extends AssetRequestTransformMap
82
199
  /**
83
200
  * Glob patterns or file paths that are allowed to be served. Relative values are resolved from `rootDir`.
84
201
  */
85
- allow: readonly string[]
202
+ allowFiles: readonly string[]
203
+ /**
204
+ * Exact package names whose files are allowed to be served. Dependencies and installed optional
205
+ * dependencies are allowed automatically. Package files must still be within a configured mount.
206
+ */
207
+ allowPackages?: readonly string[]
86
208
  /**
87
209
  * Glob patterns or file paths that are denied from being served. Relative values are resolved from `rootDir`.
88
210
  */
89
- deny?: readonly string[]
211
+ denyFiles?: readonly string[]
90
212
  /**
91
213
  * Controls optional source-based URL fingerprinting for rewritten asset URLs.
92
214
  *
@@ -125,6 +247,13 @@ export interface AssetServerOptions<transforms extends AssetRequestTransformMap
125
247
  * module extensions are not allowed here.
126
248
  */
127
249
  files?: AssetServerFilesOptions<transforms>
250
+ /**
251
+ * Enables `import.meta.hot` and coordinates browser updates through a server-level HMR runtime.
252
+ *
253
+ * HMR requires `watch` to be enabled. The factory is called once for this asset server. Returning
254
+ * `undefined` leaves HMR inactive; a returned channel is closed by `assetServer.close()`.
255
+ */
256
+ hmr?: BrowserHmrChannelFactory
128
257
  /**
129
258
  * Enable filesystem-backed cache invalidation for long-lived server instances.
130
259
  * Enabled by default. Pass `true` to use the default watcher options, an options
@@ -172,23 +301,39 @@ export interface AssetServer<transforms extends AssetRequestTransformMap = {}> {
172
301
  */
173
302
  getPreloads(filePath: string | readonly string[]): Promise<string[]>
174
303
  /**
175
- * Closes any watcher resources owned by this server instance.
304
+ * Returns diagnostic details about one public asset URL or file path, including the matched mount
305
+ * roots, access rules, file type, and browser-reachability status.
306
+ */
307
+ getAssetDetails(input: string): Promise<AssetDetails>
308
+ /**
309
+ * Returns every file currently reachable through this asset server, sorted by public URL and
310
+ * then absolute file path.
311
+ */
312
+ getAssets(): Promise<AssetDetails[]>
313
+ /**
314
+ * Closes this server's filesystem watcher and browser HMR channel.
315
+ *
316
+ * @returns A promise that resolves after owned development resources have been released.
176
317
  */
177
318
  close(): Promise<void>
178
319
  }
179
320
 
180
321
  type ResolvedAssetServerOptions<transforms extends AssetRequestTransformMap> = {
181
- allow: readonly string[]
322
+ allowFiles: readonly string[]
323
+ allowPackages?: readonly string[]
182
324
  basePath: string
183
325
  buildId?: string
184
326
  define?: Record<string, string>
185
- deny?: readonly string[]
327
+ denyFiles?: readonly string[]
186
328
  external: string[]
187
329
  files: ResolvedAssetServerFilesOptions
188
330
  fingerprintAssets: boolean
331
+ hmr: BrowserHmrChannelFactory | null
189
332
  minify: boolean
333
+ loaders: readonly ModuleLoader[]
190
334
  onError: NonNullable<AssetServerOptions['onError']>
191
335
  rootDir: string
336
+ mounts: Readonly<Record<string, string>>
192
337
  routes: CompiledRoutes
193
338
  sourceMapSourcePaths: 'url' | 'absolute'
194
339
  sourceMaps?: 'inline' | 'external'
@@ -216,7 +361,7 @@ export function getInternalWatchTargets<transforms extends AssetRequestTransform
216
361
  * Create an asset server instance
217
362
  *
218
363
  * Compiles TypeScript/JavaScript scripts and CSS styles on demand with optional
219
- * source-based URL fingerprinting, caching, and configurable file mapping.
364
+ * source-based URL fingerprinting, caching, and configurable directory mounts.
220
365
  *
221
366
  * @param options Server configuration
222
367
  * @returns A {@link AssetServer} with `fetch()`, `getHref()`, and `getPreloads()` methods
@@ -225,10 +370,9 @@ export function getInternalWatchTargets<transforms extends AssetRequestTransform
225
370
  * ```ts
226
371
  * let assetServer = createAssetServer({
227
372
  * basePath: '/assets',
228
- * fileMap: {
229
- * '/app/*path': 'app/*path',
230
- * },
231
- * allow: ['app/**'],
373
+ * allowFiles: ['app/routes.ts', 'app/**\/public/**'],
374
+ * allowPackages: ['remix'],
375
+ * denyFiles: ['app/**\/*.test.*'],
232
376
  * })
233
377
  *
234
378
  * route('/assets/*path', ({ request }) => assetServer.fetch(request))
@@ -239,24 +383,65 @@ export function createAssetServer<const transforms extends AssetRequestTransform
239
383
  ): AssetServer<transforms> {
240
384
  let resolvedOptions = resolveAssetServerOptions(options)
241
385
  let accessPolicy = createAccessPolicy({
242
- allow: resolvedOptions.allow,
243
- deny: resolvedOptions.deny,
386
+ allowFiles: resolvedOptions.allowFiles,
387
+ allowPackages: resolvedOptions.allowPackages,
388
+ denyFiles: resolvedOptions.denyFiles,
389
+ packageSearchRoots: hasPackages(resolvedOptions.allowPackages)
390
+ ? getPackageSearchRoots(resolvedOptions.mounts, resolvedOptions.rootDir)
391
+ : undefined,
392
+ rootDir: resolvedOptions.rootDir,
393
+ })
394
+ let assetInspector = createAssetInspector({
395
+ accessPolicy,
396
+ allowFiles: resolvedOptions.allowFiles,
397
+ fileExtensions: resolvedOptions.files.extensions,
244
398
  rootDir: resolvedOptions.rootDir,
399
+ routes: resolvedOptions.routes,
245
400
  })
246
401
  let watcher: AssetServerWatcher | null = null
247
402
  let chokidarWatcher: ChokidarWatcher | null = null
248
403
  let fileCompiler: FileCompiler | null = null
404
+ let closed = false
405
+ let unsubscribeBrowserHmrFileEvents: (() => void) | undefined
406
+ let browserHmrChannelPromise = createBrowserHmrChannel(
407
+ resolvedOptions.hmr,
408
+ handleBrowserHmrFileEvents,
409
+ () => closed,
410
+ (unsubscribe) => {
411
+ unsubscribeBrowserHmrFileEvents = unsubscribe
412
+ },
413
+ )
414
+ void browserHmrChannelPromise.catch(() => {
415
+ // The HMR client request and close() both await this promise. Attach a
416
+ // rejection handler now so async factory failures do not surface first as
417
+ // unhandled rejections.
418
+ })
419
+ let sendHmrPayload = resolvedOptions.hmr ? createHmrPayloadSender() : null
420
+ let hmrPathnames = getHmrPathnames(resolvedOptions.basePath)
249
421
  let scriptCompiler = createScriptCompiler({
250
422
  buildId: resolvedOptions.buildId,
251
423
  define: resolvedOptions.define,
252
424
  external: resolvedOptions.external,
253
425
  fingerprintAssets: resolvedOptions.fingerprintAssets,
426
+ loaders: resolvedOptions.loaders,
427
+ hmr: sendHmrPayload
428
+ ? {
429
+ clientPathname: hmrPathnames.client,
430
+ send(updates) {
431
+ let payload = createScriptHmrPayload(updates)
432
+ if (payload) sendHmrPayload(payload)
433
+ },
434
+ }
435
+ : undefined,
254
436
  isAllowed: accessPolicy.isAllowed,
255
437
  minify: resolvedOptions.minify,
256
438
  onWatchDirectoriesChange: (delta) => {
257
439
  if (!watcher) return
258
440
  watcher.updateWatchedDirectories(delta)
259
441
  },
442
+ onWatchFilesChange: (delta) => {
443
+ updateBrowserHmrWatchedFiles(delta)
444
+ },
260
445
  rootDir: resolvedOptions.rootDir,
261
446
  routes: resolvedOptions.routes,
262
447
  sourceMapSourcePaths: resolvedOptions.sourceMapSourcePaths,
@@ -281,6 +466,22 @@ export function createAssetServer<const transforms extends AssetRequestTransform
281
466
 
282
467
  return fileCompiler.getHref(identityPath, { transform: options.transform })
283
468
  },
469
+ hmr: sendHmrPayload
470
+ ? {
471
+ send(pathname, timestamp) {
472
+ sendHmrPayload({
473
+ timestamp,
474
+ type: 'browser:update',
475
+ updates: [
476
+ {
477
+ path: pathname,
478
+ type: 'css',
479
+ },
480
+ ],
481
+ })
482
+ },
483
+ }
484
+ : undefined,
284
485
  isAllowed: accessPolicy.isAllowed,
285
486
  isServedFilePath(filePath) {
286
487
  return fileCompiler?.isServedFilePath(filePath) ?? false
@@ -290,6 +491,9 @@ export function createAssetServer<const transforms extends AssetRequestTransform
290
491
  if (!watcher) return
291
492
  watcher.updateWatchedDirectories(delta, { includeAncestors: false })
292
493
  },
494
+ onWatchFilesChange: (delta) => {
495
+ updateBrowserHmrWatchedFiles(delta)
496
+ },
293
497
  rootDir: resolvedOptions.rootDir,
294
498
  routes: resolvedOptions.routes,
295
499
  sourceMapSourcePaths: resolvedOptions.sourceMapSourcePaths,
@@ -320,8 +524,11 @@ export function createAssetServer<const transforms extends AssetRequestTransform
320
524
  onFileEvent: handleWatchEvent,
321
525
  rootDir: resolvedOptions.rootDir,
322
526
  })
527
+ watcher.updateWatchedDirectories(
528
+ { add: accessPolicy.getPackageWatchDirectories(), remove: [] },
529
+ { includeAncestors: false },
530
+ )
323
531
  }
324
-
325
532
  async function responseForError(error: unknown): Promise<Response> {
326
533
  try {
327
534
  return (await resolvedOptions.onError(error)) ?? internalServerError()
@@ -334,21 +541,87 @@ export function createAssetServer<const transforms extends AssetRequestTransform
334
541
  async function handleWatchEvent(filePath: string, event: 'add' | 'change' | 'unlink') {
335
542
  try {
336
543
  let normalizedFilePath = normalizeFilePath(filePath)
337
- await scriptCompiler.handleFileEvent(normalizedFilePath, event)
338
- await styleCompiler.handleFileEvent(normalizedFilePath, event)
544
+ accessPolicy.handleFileEvent(normalizedFilePath)
545
+ scriptCompiler.invalidateFileEvent(normalizedFilePath, event)
546
+ styleCompiler.invalidateFileEvent(normalizedFilePath, event)
339
547
  await fileCompiler?.handleFileEvent(normalizedFilePath, event)
340
548
  } catch (error) {
341
549
  console.error(`There was an error invalidating the asset server cache: ${error}`)
342
550
  }
343
551
  }
344
552
 
553
+ async function handleBrowserHmrFileEvents(
554
+ events: readonly BrowserHmrFileEvent[],
555
+ ): Promise<readonly BrowserHmrEvent[]> {
556
+ let browserHmrEvents: BrowserHmrEvent[] = []
557
+
558
+ for (let event of events) {
559
+ let normalizedFilePath = normalizeFilePath(event.filePath)
560
+ let scriptUpdates = await scriptCompiler.classifyHmrFileEvent(normalizedFilePath, event.event)
561
+ let scriptPayload = createScriptHmrPayload(scriptUpdates)
562
+ if (scriptPayload) {
563
+ browserHmrEvents.push(
564
+ createBrowserHmrEvent(scriptPayload, getScriptHmrUpdateFiles(scriptUpdates)),
565
+ )
566
+ }
567
+
568
+ let styleUpdates = await styleCompiler.classifyHmrFileEvent(normalizedFilePath, event.event)
569
+ for (let styleUpdate of styleUpdates) {
570
+ browserHmrEvents.push(
571
+ createBrowserHmrEvent(
572
+ {
573
+ timestamp: styleUpdate.timestamp,
574
+ type: 'browser:update',
575
+ updates: [
576
+ {
577
+ path: styleUpdate.path,
578
+ type: 'css',
579
+ },
580
+ ],
581
+ },
582
+ [styleUpdate.filePath],
583
+ ),
584
+ )
585
+ }
586
+
587
+ await fileCompiler?.handleFileEvent(normalizedFilePath, event.event)
588
+ }
589
+
590
+ return browserHmrEvents
591
+ }
592
+
593
+ function updateBrowserHmrWatchedFiles(delta: BrowserHmrWatchedFileDelta): void {
594
+ browserHmrChannelPromise
595
+ .then((channel) => {
596
+ channel?.updateWatchedFiles(delta)
597
+ })
598
+ .catch((error: unknown) => {
599
+ console.error(`There was an error updating browser HMR watched files: ${error}`)
600
+ })
601
+ }
602
+
345
603
  let assetServer: AssetServer<transforms> = {
604
+ getAssetDetails(input) {
605
+ return assetInspector.getAssetDetails(input)
606
+ },
607
+ getAssets() {
608
+ return assetInspector.getAssets()
609
+ },
346
610
  async fetch(request) {
347
611
  if (request.method !== 'GET' && request.method !== 'HEAD') return null
612
+ let requestPathname = new URL(request.url).pathname
613
+
614
+ if (resolvedOptions.hmr) {
615
+ if (requestPathname === hmrPathnames.client) {
616
+ let browserHmrChannel = await browserHmrChannelPromise
617
+ assertBrowserEventUrl(browserHmrChannel?.url)
618
+ return createHmrClientResponse(browserHmrChannel.url, request.method)
619
+ }
620
+ }
348
621
 
349
622
  let requestUrl = new URL(request.url)
350
623
  let requestedTransform = normalizeRequestedTransformQuery(requestUrl.searchParams)
351
- let parsedRequestPathname = parseAssetRequestPathname(requestUrl.pathname, {
624
+ let parsedRequestPathname = parseAssetRequestPathname(requestPathname, {
352
625
  fingerprintAssets: resolvedOptions.fingerprintAssets,
353
626
  routes: resolvedOptions.routes,
354
627
  })
@@ -453,7 +726,7 @@ export function createAssetServer<const transforms extends AssetRequestTransform
453
726
  method: request.method,
454
727
  })
455
728
  } catch (error) {
456
- // A direct request can race with the filesystem or fail a deeper allow check while
729
+ // A direct request can race with the filesystem or fail a deeper allowFiles check while
457
730
  // compiling imports. In this fetch context, both cases should fall through as "not
458
731
  // handled here" so the outer router can continue to its own 404 behavior.
459
732
  if (
@@ -571,7 +844,17 @@ export function createAssetServer<const transforms extends AssetRequestTransform
571
844
  return mergePreloadLayers(await Promise.all(preloadLayerGroupPromises))
572
845
  },
573
846
  async close() {
574
- await watcher?.close()
847
+ if (closed) return
848
+ closed = true
849
+ let unsubscribe = unsubscribeBrowserHmrFileEvents
850
+ unsubscribeBrowserHmrFileEvents = undefined
851
+ unsubscribe?.()
852
+ try {
853
+ let channel = await browserHmrChannelPromise
854
+ channel?.close()
855
+ } finally {
856
+ await watcher?.close()
857
+ }
575
858
  },
576
859
  }
577
860
 
@@ -617,6 +900,81 @@ function badRequest(message: string): Response {
617
900
  })
618
901
  }
619
902
 
903
+ function getHmrPathnames(basePath: string): { client: string; events: string } {
904
+ let hmrBase = `${basePath}/__remix_hmr`
905
+ return {
906
+ client: `${hmrBase}/client.js`,
907
+ events: `${hmrBase}/events`,
908
+ }
909
+ }
910
+
911
+ function createHmrClientResponse(eventPathname: string, method: string): Response {
912
+ return new Response(method === 'HEAD' ? null : createHmrClientSource({ eventPathname }), {
913
+ headers: {
914
+ 'Cache-Control': 'no-cache',
915
+ 'Content-Type': 'application/javascript; charset=utf-8',
916
+ },
917
+ })
918
+ }
919
+
920
+ function assertBrowserEventUrl(url: string | undefined): asserts url is string {
921
+ if (url === undefined) {
922
+ throw new TypeError('hmr must be provided')
923
+ }
924
+ }
925
+
926
+ function createHmrPayloadSender(): (payload: HmrPayload) => void {
927
+ return () => {}
928
+ }
929
+
930
+ function createBrowserHmrEvent(
931
+ payload: Extract<HmrPayload, { type: 'browser:reload' | 'browser:update' }>,
932
+ files: readonly string[],
933
+ ): BrowserHmrEvent {
934
+ if (payload.type === 'browser:reload') {
935
+ return {
936
+ ...(files.length === 0 ? {} : { files: [...files] }),
937
+ type: 'reload',
938
+ }
939
+ }
940
+
941
+ return {
942
+ ...(files.length === 0 ? {} : { files: [...files] }),
943
+ timestamp: payload.timestamp,
944
+ type: 'update',
945
+ updates: payload.updates,
946
+ }
947
+ }
948
+
949
+ function getScriptHmrUpdateFiles(updates: ScriptHmrUpdate[]): string[] {
950
+ return [...new Set(updates.map((update) => update.filePath))]
951
+ }
952
+
953
+ export function createScriptHmrPayload(
954
+ updates: ScriptHmrUpdate[],
955
+ ): Extract<HmrPayload, { type: 'browser:reload' | 'browser:update' }> | null {
956
+ let timestamp = updates[0]?.timestamp ?? Date.now()
957
+ let rejectedUpdate = updates.find((update) => !update.accepted)
958
+ if (rejectedUpdate) {
959
+ return {
960
+ type: 'browser:reload',
961
+ }
962
+ }
963
+
964
+ let acceptedUpdates = updates.filter((update) => update.accepted)
965
+ if (acceptedUpdates.length === 0) return null
966
+
967
+ return {
968
+ timestamp,
969
+ type: 'browser:update',
970
+ updates: acceptedUpdates.map((update) => ({
971
+ ...(update.acceptedPath === update.path ? {} : { acceptedPath: update.acceptedPath }),
972
+ path: update.path,
973
+ type: 'js',
974
+ })),
975
+ }
976
+ }
977
+
620
978
  function mergePreloadLayers(preloadLayersByRoot: readonly (readonly string[][])[]): string[] {
621
979
  let urls: string[] = []
622
980
  let seen = new Set<string>()
@@ -678,31 +1036,101 @@ function resolveAssetServerOptions<transforms extends AssetRequestTransformMap>(
678
1036
  fingerprint: options.fingerprint,
679
1037
  watch: options.watch,
680
1038
  })
1039
+ let watchOptions = normalizeWatchOptions(options.watch)
1040
+ let hmrFactory = normalizeHmrFactory(options.hmr)
1041
+ let mounts = options.mounts ?? defaultMounts
681
1042
 
1043
+ if (hmrFactory && watchOptions === null) {
1044
+ throw new TypeError('hmr requires watch mode')
1045
+ }
1046
+ if (Object.keys(mounts).length === 0) {
1047
+ throw new TypeError('mounts must include at least one entry')
1048
+ }
682
1049
  return {
683
- allow: options.allow,
1050
+ allowFiles: options.allowFiles,
1051
+ allowPackages: options.allowPackages,
684
1052
  basePath,
685
1053
  buildId: fingerprintOptions.buildId,
686
1054
  define: scriptOptions.define,
687
- deny: options.deny,
1055
+ denyFiles: options.denyFiles,
688
1056
  external: scriptOptions.external ?? [],
689
1057
  files: normalizeFilesOptions(options.files),
690
1058
  fingerprintAssets: fingerprintOptions.enabled,
1059
+ hmr: hmrFactory,
691
1060
  minify: options.minify ?? false,
1061
+ mounts,
1062
+ loaders: scriptOptions.loaders ?? [],
692
1063
  onError: options.onError ?? defaultErrorHandler,
693
1064
  rootDir,
694
1065
  routes: compileRoutes(basePath, [
695
1066
  {
696
- fileMap: options.fileMap,
1067
+ mounts,
697
1068
  rootDir,
698
1069
  },
699
- ...getInjectedPackageRouteConfigs(),
1070
+ ...getInjectedPackageMountConfigs(),
700
1071
  ]),
701
1072
  sourceMapSourcePaths: options.sourceMapSourcePaths ?? 'url',
702
1073
  sourceMaps: options.sourceMaps,
703
1074
  scriptsTarget: resolveScriptTarget(options.target),
704
1075
  stylesTarget: resolveStyleTarget(options.target),
705
- watchOptions: normalizeWatchOptions(options.watch),
1076
+ watchOptions,
1077
+ }
1078
+ }
1079
+
1080
+ function normalizeHmrFactory(factory: AssetServerOptions['hmr']): BrowserHmrChannelFactory | null {
1081
+ if (factory === undefined) return null
1082
+
1083
+ if (typeof factory !== 'function') {
1084
+ throw new TypeError('hmr must be a function')
1085
+ }
1086
+
1087
+ return factory
1088
+ }
1089
+
1090
+ function createBrowserHmrChannel(
1091
+ createChannel: BrowserHmrChannelFactory | null,
1092
+ handleFileEvents: BrowserHmrFileEventHandler,
1093
+ isClosed: () => boolean,
1094
+ setUnsubscribe: (unsubscribe: () => void) => void,
1095
+ ): Promise<BrowserHmrChannel | null> {
1096
+ if (!createChannel) return Promise.resolve(null)
1097
+
1098
+ let channelOrPromise: BrowserHmrChannel | undefined | Promise<BrowserHmrChannel | undefined>
1099
+ try {
1100
+ channelOrPromise = createChannel()
1101
+ } catch (error) {
1102
+ return Promise.reject(error)
1103
+ }
1104
+
1105
+ return Promise.resolve(channelOrPromise).then((channel) => {
1106
+ if (channel === undefined) return null
1107
+ validateBrowserHmrChannel(channel)
1108
+
1109
+ if (isClosed()) {
1110
+ channel.close()
1111
+ return null
1112
+ }
1113
+
1114
+ setUnsubscribe(channel.onFileEvents(handleFileEvents))
1115
+ return channel
1116
+ })
1117
+ }
1118
+
1119
+ function validateBrowserHmrChannel(channel: unknown): asserts channel is BrowserHmrChannel {
1120
+ if (channel === null || typeof channel !== 'object') {
1121
+ throw new TypeError('hmr must create an object')
1122
+ }
1123
+ if (!('url' in channel) || typeof channel.url !== 'string') {
1124
+ throw new TypeError('hmr must create a channel with a string url')
1125
+ }
1126
+ if (!('close' in channel) || typeof channel.close !== 'function') {
1127
+ throw new TypeError('hmr must create a channel with a close function')
1128
+ }
1129
+ if (!('onFileEvents' in channel) || typeof channel.onFileEvents !== 'function') {
1130
+ throw new TypeError('hmr must create a channel with an onFileEvents function')
1131
+ }
1132
+ if (!('updateWatchedFiles' in channel) || typeof channel.updateWatchedFiles !== 'function') {
1133
+ throw new TypeError('hmr must create a channel with an updateWatchedFiles function')
706
1134
  }
707
1135
  }
708
1136
 
@@ -758,6 +1186,17 @@ function normalizeWatchOptions(
758
1186
  return options
759
1187
  }
760
1188
 
1189
+ function hasPackages(packages: readonly string[] | undefined): boolean {
1190
+ return packages !== undefined && packages.length > 0
1191
+ }
1192
+
1193
+ function getPackageSearchRoots(
1194
+ mounts: Readonly<Record<string, string>>,
1195
+ rootDir: string,
1196
+ ): readonly string[] {
1197
+ return Object.values(mounts).map((fileRoot) => path.resolve(rootDir, fileRoot))
1198
+ }
1199
+
761
1200
  function parseAssetRequestPathname(
762
1201
  pathname: string,
763
1202
  options: {