@remix-run/assets 0.4.4 → 0.5.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 (74) hide show
  1. package/README.md +322 -56
  2. package/dist/assets.d.ts +2 -1
  3. package/dist/assets.d.ts.map +1 -1
  4. package/dist/assets.js +2 -2
  5. package/dist/lib/access.d.ts +6 -2
  6. package/dist/lib/access.d.ts.map +1 -1
  7. package/dist/lib/access.js +300 -5
  8. package/dist/lib/asset-server.d.ts +118 -5
  9. package/dist/lib/asset-server.d.ts.map +1 -1
  10. package/dist/lib/asset-server.js +260 -24
  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 +7 -6
  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.map +1 -1
  21. package/dist/lib/injected-packages.js +36 -13
  22. package/dist/lib/loaders.d.ts +57 -0
  23. package/dist/lib/loaders.d.ts.map +1 -0
  24. package/dist/lib/loaders.js +1 -0
  25. package/dist/lib/module-store.d.ts +24 -0
  26. package/dist/lib/module-store.d.ts.map +1 -1
  27. package/dist/lib/module-store.js +136 -11
  28. package/dist/lib/routes.js +1 -1
  29. package/dist/lib/scripts/compiler.d.ts +24 -1
  30. package/dist/lib/scripts/compiler.d.ts.map +1 -1
  31. package/dist/lib/scripts/compiler.js +183 -29
  32. package/dist/lib/scripts/conditions.d.ts +2 -0
  33. package/dist/lib/scripts/conditions.d.ts.map +1 -0
  34. package/dist/lib/scripts/conditions.js +1 -0
  35. package/dist/lib/scripts/emit.d.ts +3 -0
  36. package/dist/lib/scripts/emit.d.ts.map +1 -1
  37. package/dist/lib/scripts/emit.js +24 -5
  38. package/dist/lib/scripts/resolve.d.ts +4 -0
  39. package/dist/lib/scripts/resolve.d.ts.map +1 -1
  40. package/dist/lib/scripts/resolve.js +70 -5
  41. package/dist/lib/scripts/transform.d.ts +9 -0
  42. package/dist/lib/scripts/transform.d.ts.map +1 -1
  43. package/dist/lib/scripts/transform.js +222 -18
  44. package/dist/lib/source-maps.js +1 -1
  45. package/dist/lib/styles/compiler.d.ts +14 -1
  46. package/dist/lib/styles/compiler.d.ts.map +1 -1
  47. package/dist/lib/styles/compiler.js +78 -14
  48. package/dist/lib/styles/emit.js +4 -4
  49. package/dist/lib/styles/resolve.d.ts.map +1 -1
  50. package/dist/lib/styles/resolve.js +8 -7
  51. package/dist/lib/styles/transform.js +3 -3
  52. package/dist/lib/target.d.ts +1 -1
  53. package/dist/lib/target.d.ts.map +1 -1
  54. package/dist/lib/watch.js +1 -1
  55. package/dist/types/hmr.d.ts +36 -0
  56. package/package.json +17 -11
  57. package/src/assets.ts +2 -1
  58. package/src/lib/access.ts +386 -5
  59. package/src/lib/asset-server.ts +429 -18
  60. package/src/lib/compilation-error.ts +1 -0
  61. package/src/lib/files/compiler.ts +7 -3
  62. package/src/lib/hmr.ts +397 -0
  63. package/src/lib/injected-packages.ts +41 -11
  64. package/src/lib/loaders.ts +80 -0
  65. package/src/lib/module-store.ts +190 -9
  66. package/src/lib/scripts/compiler.ts +248 -24
  67. package/src/lib/scripts/conditions.ts +1 -0
  68. package/src/lib/scripts/emit.ts +50 -5
  69. package/src/lib/scripts/resolve.ts +124 -2
  70. package/src/lib/scripts/transform.ts +290 -19
  71. package/src/lib/styles/compiler.ts +108 -8
  72. package/src/lib/styles/emit.ts +1 -1
  73. package/src/lib/styles/resolve.ts +11 -7
  74. package/src/types/hmr.d.ts +36 -0
@@ -15,11 +15,15 @@ import type {
15
15
  ResolvedAssetServerFilesOptions,
16
16
  } from './files/config.ts'
17
17
  import { getFingerprintRequestCacheControl, parseFingerprintSuffix } from './fingerprint.ts'
18
+ import { createHmrClientSource } from './hmr.ts'
19
+ import type { HmrPayload } from './hmr.ts'
18
20
  import { getInjectedPackageRouteConfigs } 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'
@@ -43,6 +47,99 @@ interface AssetServerWatchOptions {
43
47
  pollInterval?: number
44
48
  }
45
49
 
50
+ /**
51
+ * Bridge used by an asset server to watch browser source files and publish update events through a
52
+ * server-level HMR runtime.
53
+ *
54
+ * The asset server registers its file handler, keeps the channel's watched-file set in sync with
55
+ * compiled assets, injects `url` into its browser HMR client, and closes the channel when the asset
56
+ * server closes.
57
+ */
58
+ export interface BrowserHmrChannel {
59
+ /** Absolute EventSource URL that receives the browser HMR events produced by this channel. */
60
+ readonly url: string
61
+ /** Releases the channel and all file-watching resources it owns. */
62
+ close(): void
63
+ /**
64
+ * Registers the asset server's handler for file changes reported by the channel's watcher.
65
+ *
66
+ * @param handler Callback that converts a batch of file changes into browser update or reload
67
+ * events.
68
+ * @returns A cleanup function that stops forwarding file changes to this handler.
69
+ */
70
+ onFileEvents(handler: BrowserHmrFileEventHandler): () => void
71
+ /**
72
+ * Applies additions and removals to the channel's set of watched absolute file paths.
73
+ *
74
+ * @param delta Files to add and remove from the watcher.
75
+ */
76
+ updateWatchedFiles(delta: BrowserHmrWatchedFileDelta): void
77
+ }
78
+
79
+ /**
80
+ * Creates the browser HMR channel owned by an asset server instance.
81
+ *
82
+ * Returning `undefined` leaves browser HMR inactive. The asset server invokes this factory once,
83
+ * after construction, and closes a returned channel from `assetServer.close()`.
84
+ *
85
+ * @returns A channel, no channel, or a promise for either result.
86
+ */
87
+ export type BrowserHmrChannelFactory = () =>
88
+ | BrowserHmrChannel
89
+ | undefined
90
+ | Promise<BrowserHmrChannel | undefined>
91
+
92
+ /**
93
+ * Converts a watcher batch into ordered browser update or reload events.
94
+ *
95
+ * @param events File additions, changes, and removals reported together by the watcher.
96
+ * @returns Browser events to publish in their returned order.
97
+ */
98
+ export type BrowserHmrFileEventHandler = (
99
+ events: readonly BrowserHmrFileEvent[],
100
+ ) => Promise<readonly BrowserHmrEvent[]>
101
+
102
+ /**
103
+ * Watched file delta for a browser HMR channel.
104
+ */
105
+ export interface BrowserHmrWatchedFileDelta {
106
+ /** Absolute source file paths newly required by the asset server's compiled module graph. */
107
+ add: readonly string[]
108
+ /** Absolute source file paths no longer required by the asset server's compiled module graph. */
109
+ remove: readonly string[]
110
+ }
111
+
112
+ /**
113
+ * File watcher event reported to a browser HMR channel.
114
+ */
115
+ export type BrowserHmrFileEvent = {
116
+ /** Filesystem operation observed by the channel's watcher. */
117
+ event: 'add' | 'change' | 'unlink'
118
+ /** Absolute path of the source file that changed. */
119
+ filePath: string
120
+ }
121
+
122
+ /**
123
+ * Browser HMR event emitted to connected clients.
124
+ */
125
+ export type BrowserHmrEvent =
126
+ | {
127
+ /** Absolute source file paths that triggered this update. */
128
+ files?: string[]
129
+ /** Update time used to bypass browser module and stylesheet caches. */
130
+ timestamp: number
131
+ /** Browser update event. */
132
+ 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
+ }
136
+ | {
137
+ /** Absolute source file paths that could not be handled in place. */
138
+ files?: string[]
139
+ /** Browser reload event. */
140
+ type: 'reload'
141
+ }
142
+
46
143
  interface FingerprintOptions {
47
144
  /**
48
145
  * Per-build invalidation token that must change whenever fingerprinted asset URLs
@@ -62,6 +159,16 @@ interface AssetServerScriptOptions {
62
159
  define?: Record<string, string>
63
160
  /** Import specifiers to leave unrewritten (CDN URLs, import map entries, etc.) */
64
161
  external?: string[]
162
+ /**
163
+ * Synchronous loaders that post-process compiled JavaScript.
164
+ *
165
+ * Loaders use Node's synchronous `load` hook signature. Later loaders wrap earlier loaders: for
166
+ * `[first, second]`, `second` is entered first and delegates through `first` to the default
167
+ * behavior. As a result, transformations performed after `nextLoad()` are applied in array order.
168
+ * Loaders run after TypeScript/JavaScript transformation and before HMR analysis and minification.
169
+ * Only `format: 'module'` is supported, and import attributes are unsupported.
170
+ */
171
+ loaders?: readonly ModuleLoader[]
65
172
  }
66
173
 
67
174
  const scriptExtensionSet = new Set<string>(supportedScriptExtensions)
@@ -72,7 +179,6 @@ const scriptExtensionSet = new Set<string>(supportedScriptExtensions)
72
179
  export interface AssetServerOptions<transforms extends AssetRequestTransformMap = {}> {
73
180
  /** Public mount path for this asset server, e.g. `'/assets'`. */
74
181
  basePath: string
75
- /** File patterns keyed by public URL patterns relative to `basePath`. */
76
182
  /** File patterns keyed by public URL patterns. */
77
183
  fileMap: Readonly<Record<string, string>>
78
184
  /**
@@ -82,11 +188,16 @@ export interface AssetServerOptions<transforms extends AssetRequestTransformMap
82
188
  /**
83
189
  * Glob patterns or file paths that are allowed to be served. Relative values are resolved from `rootDir`.
84
190
  */
85
- allow: readonly string[]
191
+ allowFiles: readonly string[]
192
+ /**
193
+ * 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`.
195
+ */
196
+ allowPackages?: readonly string[]
86
197
  /**
87
198
  * Glob patterns or file paths that are denied from being served. Relative values are resolved from `rootDir`.
88
199
  */
89
- deny?: readonly string[]
200
+ denyFiles?: readonly string[]
90
201
  /**
91
202
  * Controls optional source-based URL fingerprinting for rewritten asset URLs.
92
203
  *
@@ -125,6 +236,13 @@ export interface AssetServerOptions<transforms extends AssetRequestTransformMap
125
236
  * module extensions are not allowed here.
126
237
  */
127
238
  files?: AssetServerFilesOptions<transforms>
239
+ /**
240
+ * Enables `import.meta.hot` and coordinates browser updates through a server-level HMR runtime.
241
+ *
242
+ * HMR requires `watch` to be enabled. The factory is called once for this asset server. Returning
243
+ * `undefined` leaves HMR inactive; a returned channel is closed by `assetServer.close()`.
244
+ */
245
+ hmr?: BrowserHmrChannelFactory
128
246
  /**
129
247
  * Enable filesystem-backed cache invalidation for long-lived server instances.
130
248
  * Enabled by default. Pass `true` to use the default watcher options, an options
@@ -172,21 +290,26 @@ export interface AssetServer<transforms extends AssetRequestTransformMap = {}> {
172
290
  */
173
291
  getPreloads(filePath: string | readonly string[]): Promise<string[]>
174
292
  /**
175
- * Closes any watcher resources owned by this server instance.
293
+ * Closes this server's filesystem watcher and browser HMR channel.
294
+ *
295
+ * @returns A promise that resolves after owned development resources have been released.
176
296
  */
177
297
  close(): Promise<void>
178
298
  }
179
299
 
180
300
  type ResolvedAssetServerOptions<transforms extends AssetRequestTransformMap> = {
181
- allow: readonly string[]
301
+ allowFiles: readonly string[]
302
+ allowPackages?: readonly string[]
182
303
  basePath: string
183
304
  buildId?: string
184
305
  define?: Record<string, string>
185
- deny?: readonly string[]
306
+ denyFiles?: readonly string[]
186
307
  external: string[]
187
308
  files: ResolvedAssetServerFilesOptions
188
309
  fingerprintAssets: boolean
310
+ hmr: BrowserHmrChannelFactory | null
189
311
  minify: boolean
312
+ loaders: readonly ModuleLoader[]
190
313
  onError: NonNullable<AssetServerOptions['onError']>
191
314
  rootDir: string
192
315
  routes: CompiledRoutes
@@ -228,7 +351,9 @@ export function getInternalWatchTargets<transforms extends AssetRequestTransform
228
351
  * fileMap: {
229
352
  * '/app/*path': 'app/*path',
230
353
  * },
231
- * allow: ['app/**'],
354
+ * allowFiles: ['app/routes.ts', 'app/**\/public/**'],
355
+ * allowPackages: ['remix'],
356
+ * denyFiles: ['app/**\/*.test.*'],
232
357
  * })
233
358
  *
234
359
  * route('/assets/*path', ({ request }) => assetServer.fetch(request))
@@ -239,24 +364,58 @@ export function createAssetServer<const transforms extends AssetRequestTransform
239
364
  ): AssetServer<transforms> {
240
365
  let resolvedOptions = resolveAssetServerOptions(options)
241
366
  let accessPolicy = createAccessPolicy({
242
- allow: resolvedOptions.allow,
243
- deny: resolvedOptions.deny,
367
+ allowFiles: resolvedOptions.allowFiles,
368
+ allowPackages: resolvedOptions.allowPackages,
369
+ denyFiles: resolvedOptions.denyFiles,
370
+ packageSearchRoots: hasPackages(resolvedOptions.allowPackages)
371
+ ? getPackageSearchRoots(options.fileMap, resolvedOptions.rootDir)
372
+ : undefined,
244
373
  rootDir: resolvedOptions.rootDir,
245
374
  })
246
375
  let watcher: AssetServerWatcher | null = null
247
376
  let chokidarWatcher: ChokidarWatcher | null = null
248
377
  let fileCompiler: FileCompiler | null = null
378
+ let closed = false
379
+ let unsubscribeBrowserHmrFileEvents: (() => void) | undefined
380
+ let browserHmrChannelPromise = createBrowserHmrChannel(
381
+ resolvedOptions.hmr,
382
+ handleBrowserHmrFileEvents,
383
+ () => closed,
384
+ (unsubscribe) => {
385
+ unsubscribeBrowserHmrFileEvents = unsubscribe
386
+ },
387
+ )
388
+ void browserHmrChannelPromise.catch(() => {
389
+ // The HMR client request and close() both await this promise. Attach a
390
+ // rejection handler now so async factory failures do not surface first as
391
+ // unhandled rejections.
392
+ })
393
+ let sendHmrPayload = resolvedOptions.hmr ? createHmrPayloadSender() : null
394
+ let hmrPathnames = getHmrPathnames(resolvedOptions.basePath)
249
395
  let scriptCompiler = createScriptCompiler({
250
396
  buildId: resolvedOptions.buildId,
251
397
  define: resolvedOptions.define,
252
398
  external: resolvedOptions.external,
253
399
  fingerprintAssets: resolvedOptions.fingerprintAssets,
400
+ loaders: resolvedOptions.loaders,
401
+ hmr: sendHmrPayload
402
+ ? {
403
+ clientPathname: hmrPathnames.client,
404
+ send(updates) {
405
+ let payload = createScriptHmrPayload(updates)
406
+ if (payload) sendHmrPayload(payload)
407
+ },
408
+ }
409
+ : undefined,
254
410
  isAllowed: accessPolicy.isAllowed,
255
411
  minify: resolvedOptions.minify,
256
412
  onWatchDirectoriesChange: (delta) => {
257
413
  if (!watcher) return
258
414
  watcher.updateWatchedDirectories(delta)
259
415
  },
416
+ onWatchFilesChange: (delta) => {
417
+ updateBrowserHmrWatchedFiles(delta)
418
+ },
260
419
  rootDir: resolvedOptions.rootDir,
261
420
  routes: resolvedOptions.routes,
262
421
  sourceMapSourcePaths: resolvedOptions.sourceMapSourcePaths,
@@ -281,6 +440,22 @@ export function createAssetServer<const transforms extends AssetRequestTransform
281
440
 
282
441
  return fileCompiler.getHref(identityPath, { transform: options.transform })
283
442
  },
443
+ hmr: sendHmrPayload
444
+ ? {
445
+ send(pathname, timestamp) {
446
+ sendHmrPayload({
447
+ timestamp,
448
+ type: 'browser:update',
449
+ updates: [
450
+ {
451
+ path: pathname,
452
+ type: 'css',
453
+ },
454
+ ],
455
+ })
456
+ },
457
+ }
458
+ : undefined,
284
459
  isAllowed: accessPolicy.isAllowed,
285
460
  isServedFilePath(filePath) {
286
461
  return fileCompiler?.isServedFilePath(filePath) ?? false
@@ -290,6 +465,9 @@ export function createAssetServer<const transforms extends AssetRequestTransform
290
465
  if (!watcher) return
291
466
  watcher.updateWatchedDirectories(delta, { includeAncestors: false })
292
467
  },
468
+ onWatchFilesChange: (delta) => {
469
+ updateBrowserHmrWatchedFiles(delta)
470
+ },
293
471
  rootDir: resolvedOptions.rootDir,
294
472
  routes: resolvedOptions.routes,
295
473
  sourceMapSourcePaths: resolvedOptions.sourceMapSourcePaths,
@@ -320,8 +498,11 @@ export function createAssetServer<const transforms extends AssetRequestTransform
320
498
  onFileEvent: handleWatchEvent,
321
499
  rootDir: resolvedOptions.rootDir,
322
500
  })
501
+ watcher.updateWatchedDirectories(
502
+ { add: accessPolicy.getPackageWatchDirectories(), remove: [] },
503
+ { includeAncestors: false },
504
+ )
323
505
  }
324
-
325
506
  async function responseForError(error: unknown): Promise<Response> {
326
507
  try {
327
508
  return (await resolvedOptions.onError(error)) ?? internalServerError()
@@ -334,21 +515,81 @@ export function createAssetServer<const transforms extends AssetRequestTransform
334
515
  async function handleWatchEvent(filePath: string, event: 'add' | 'change' | 'unlink') {
335
516
  try {
336
517
  let normalizedFilePath = normalizeFilePath(filePath)
337
- await scriptCompiler.handleFileEvent(normalizedFilePath, event)
338
- await styleCompiler.handleFileEvent(normalizedFilePath, event)
518
+ accessPolicy.handleFileEvent(normalizedFilePath)
519
+ scriptCompiler.invalidateFileEvent(normalizedFilePath, event)
520
+ styleCompiler.invalidateFileEvent(normalizedFilePath, event)
339
521
  await fileCompiler?.handleFileEvent(normalizedFilePath, event)
340
522
  } catch (error) {
341
523
  console.error(`There was an error invalidating the asset server cache: ${error}`)
342
524
  }
343
525
  }
344
526
 
527
+ async function handleBrowserHmrFileEvents(
528
+ events: readonly BrowserHmrFileEvent[],
529
+ ): Promise<readonly BrowserHmrEvent[]> {
530
+ let browserHmrEvents: BrowserHmrEvent[] = []
531
+
532
+ for (let event of events) {
533
+ let normalizedFilePath = normalizeFilePath(event.filePath)
534
+ let scriptUpdates = await scriptCompiler.classifyHmrFileEvent(normalizedFilePath, event.event)
535
+ let scriptPayload = createScriptHmrPayload(scriptUpdates)
536
+ if (scriptPayload) {
537
+ browserHmrEvents.push(
538
+ createBrowserHmrEvent(scriptPayload, getScriptHmrUpdateFiles(scriptUpdates)),
539
+ )
540
+ }
541
+
542
+ let styleUpdates = await styleCompiler.classifyHmrFileEvent(normalizedFilePath, event.event)
543
+ for (let styleUpdate of styleUpdates) {
544
+ browserHmrEvents.push(
545
+ createBrowserHmrEvent(
546
+ {
547
+ timestamp: styleUpdate.timestamp,
548
+ type: 'browser:update',
549
+ updates: [
550
+ {
551
+ path: styleUpdate.path,
552
+ type: 'css',
553
+ },
554
+ ],
555
+ },
556
+ [styleUpdate.filePath],
557
+ ),
558
+ )
559
+ }
560
+
561
+ await fileCompiler?.handleFileEvent(normalizedFilePath, event.event)
562
+ }
563
+
564
+ return browserHmrEvents
565
+ }
566
+
567
+ function updateBrowserHmrWatchedFiles(delta: BrowserHmrWatchedFileDelta): void {
568
+ browserHmrChannelPromise
569
+ .then((channel) => {
570
+ channel?.updateWatchedFiles(delta)
571
+ })
572
+ .catch((error: unknown) => {
573
+ console.error(`There was an error updating browser HMR watched files: ${error}`)
574
+ })
575
+ }
576
+
345
577
  let assetServer: AssetServer<transforms> = {
346
578
  async fetch(request) {
347
579
  if (request.method !== 'GET' && request.method !== 'HEAD') return null
580
+ let requestPathname = new URL(request.url).pathname
581
+
582
+ if (resolvedOptions.hmr) {
583
+ if (requestPathname === hmrPathnames.client) {
584
+ let browserHmrChannel = await browserHmrChannelPromise
585
+ assertBrowserEventUrl(browserHmrChannel?.url)
586
+ return createHmrClientResponse(browserHmrChannel.url, request.method)
587
+ }
588
+ }
348
589
 
349
590
  let requestUrl = new URL(request.url)
350
591
  let requestedTransform = normalizeRequestedTransformQuery(requestUrl.searchParams)
351
- let parsedRequestPathname = parseAssetRequestPathname(requestUrl.pathname, {
592
+ let parsedRequestPathname = parseAssetRequestPathname(requestPathname, {
352
593
  fingerprintAssets: resolvedOptions.fingerprintAssets,
353
594
  routes: resolvedOptions.routes,
354
595
  })
@@ -453,7 +694,7 @@ export function createAssetServer<const transforms extends AssetRequestTransform
453
694
  method: request.method,
454
695
  })
455
696
  } catch (error) {
456
- // A direct request can race with the filesystem or fail a deeper allow check while
697
+ // A direct request can race with the filesystem or fail a deeper allowFiles check while
457
698
  // compiling imports. In this fetch context, both cases should fall through as "not
458
699
  // handled here" so the outer router can continue to its own 404 behavior.
459
700
  if (
@@ -571,7 +812,17 @@ export function createAssetServer<const transforms extends AssetRequestTransform
571
812
  return mergePreloadLayers(await Promise.all(preloadLayerGroupPromises))
572
813
  },
573
814
  async close() {
574
- await watcher?.close()
815
+ if (closed) return
816
+ closed = true
817
+ let unsubscribe = unsubscribeBrowserHmrFileEvents
818
+ unsubscribeBrowserHmrFileEvents = undefined
819
+ unsubscribe?.()
820
+ try {
821
+ let channel = await browserHmrChannelPromise
822
+ channel?.close()
823
+ } finally {
824
+ await watcher?.close()
825
+ }
575
826
  },
576
827
  }
577
828
 
@@ -617,6 +868,81 @@ function badRequest(message: string): Response {
617
868
  })
618
869
  }
619
870
 
871
+ function getHmrPathnames(basePath: string): { client: string; events: string } {
872
+ let hmrBase = `${basePath}/__remix_hmr`
873
+ return {
874
+ client: `${hmrBase}/client.js`,
875
+ events: `${hmrBase}/events`,
876
+ }
877
+ }
878
+
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',
884
+ },
885
+ })
886
+ }
887
+
888
+ function assertBrowserEventUrl(url: string | undefined): asserts url is string {
889
+ if (url === undefined) {
890
+ throw new TypeError('hmr must be provided')
891
+ }
892
+ }
893
+
894
+ function createHmrPayloadSender(): (payload: HmrPayload) => void {
895
+ return () => {}
896
+ }
897
+
898
+ function createBrowserHmrEvent(
899
+ payload: Extract<HmrPayload, { type: 'browser:reload' | 'browser:update' }>,
900
+ files: readonly string[],
901
+ ): BrowserHmrEvent {
902
+ if (payload.type === 'browser:reload') {
903
+ return {
904
+ ...(files.length === 0 ? {} : { files: [...files] }),
905
+ type: 'reload',
906
+ }
907
+ }
908
+
909
+ return {
910
+ ...(files.length === 0 ? {} : { files: [...files] }),
911
+ timestamp: payload.timestamp,
912
+ type: 'update',
913
+ updates: payload.updates,
914
+ }
915
+ }
916
+
917
+ function getScriptHmrUpdateFiles(updates: ScriptHmrUpdate[]): string[] {
918
+ return [...new Set(updates.map((update) => update.filePath))]
919
+ }
920
+
921
+ export function createScriptHmrPayload(
922
+ updates: ScriptHmrUpdate[],
923
+ ): Extract<HmrPayload, { type: 'browser:reload' | 'browser:update' }> | null {
924
+ let timestamp = updates[0]?.timestamp ?? Date.now()
925
+ let rejectedUpdate = updates.find((update) => !update.accepted)
926
+ if (rejectedUpdate) {
927
+ return {
928
+ type: 'browser:reload',
929
+ }
930
+ }
931
+
932
+ let acceptedUpdates = updates.filter((update) => update.accepted)
933
+ if (acceptedUpdates.length === 0) return null
934
+
935
+ return {
936
+ timestamp,
937
+ type: 'browser:update',
938
+ updates: acceptedUpdates.map((update) => ({
939
+ ...(update.acceptedPath === update.path ? {} : { acceptedPath: update.acceptedPath }),
940
+ path: update.path,
941
+ type: 'js',
942
+ })),
943
+ }
944
+ }
945
+
620
946
  function mergePreloadLayers(preloadLayersByRoot: readonly (readonly string[][])[]): string[] {
621
947
  let urls: string[] = []
622
948
  let seen = new Set<string>()
@@ -678,17 +1004,25 @@ function resolveAssetServerOptions<transforms extends AssetRequestTransformMap>(
678
1004
  fingerprint: options.fingerprint,
679
1005
  watch: options.watch,
680
1006
  })
1007
+ let watchOptions = normalizeWatchOptions(options.watch)
1008
+ let hmrFactory = normalizeHmrFactory(options.hmr)
681
1009
 
1010
+ if (hmrFactory && watchOptions === null) {
1011
+ throw new TypeError('hmr requires watch mode')
1012
+ }
682
1013
  return {
683
- allow: options.allow,
1014
+ allowFiles: options.allowFiles,
1015
+ allowPackages: options.allowPackages,
684
1016
  basePath,
685
1017
  buildId: fingerprintOptions.buildId,
686
1018
  define: scriptOptions.define,
687
- deny: options.deny,
1019
+ denyFiles: options.denyFiles,
688
1020
  external: scriptOptions.external ?? [],
689
1021
  files: normalizeFilesOptions(options.files),
690
1022
  fingerprintAssets: fingerprintOptions.enabled,
1023
+ hmr: hmrFactory,
691
1024
  minify: options.minify ?? false,
1025
+ loaders: scriptOptions.loaders ?? [],
692
1026
  onError: options.onError ?? defaultErrorHandler,
693
1027
  rootDir,
694
1028
  routes: compileRoutes(basePath, [
@@ -702,7 +1036,64 @@ function resolveAssetServerOptions<transforms extends AssetRequestTransformMap>(
702
1036
  sourceMaps: options.sourceMaps,
703
1037
  scriptsTarget: resolveScriptTarget(options.target),
704
1038
  stylesTarget: resolveStyleTarget(options.target),
705
- watchOptions: normalizeWatchOptions(options.watch),
1039
+ watchOptions,
1040
+ }
1041
+ }
1042
+
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')
1048
+ }
1049
+
1050
+ return factory
1051
+ }
1052
+
1053
+ function createBrowserHmrChannel(
1054
+ createChannel: BrowserHmrChannelFactory | null,
1055
+ handleFileEvents: BrowserHmrFileEventHandler,
1056
+ isClosed: () => boolean,
1057
+ setUnsubscribe: (unsubscribe: () => void) => void,
1058
+ ): Promise<BrowserHmrChannel | null> {
1059
+ if (!createChannel) return Promise.resolve(null)
1060
+
1061
+ let channelOrPromise: BrowserHmrChannel | undefined | Promise<BrowserHmrChannel | undefined>
1062
+ try {
1063
+ channelOrPromise = createChannel()
1064
+ } catch (error) {
1065
+ return Promise.reject(error)
1066
+ }
1067
+
1068
+ return Promise.resolve(channelOrPromise).then((channel) => {
1069
+ if (channel === undefined) return null
1070
+ validateBrowserHmrChannel(channel)
1071
+
1072
+ if (isClosed()) {
1073
+ channel.close()
1074
+ return null
1075
+ }
1076
+
1077
+ setUnsubscribe(channel.onFileEvents(handleFileEvents))
1078
+ return channel
1079
+ })
1080
+ }
1081
+
1082
+ function validateBrowserHmrChannel(channel: unknown): asserts channel is BrowserHmrChannel {
1083
+ if (channel === null || typeof channel !== 'object') {
1084
+ throw new TypeError('hmr must create an object')
1085
+ }
1086
+ if (!('url' in channel) || typeof channel.url !== 'string') {
1087
+ throw new TypeError('hmr must create a channel with a string url')
1088
+ }
1089
+ if (!('close' in channel) || typeof channel.close !== 'function') {
1090
+ throw new TypeError('hmr must create a channel with a close function')
1091
+ }
1092
+ if (!('onFileEvents' in channel) || typeof channel.onFileEvents !== 'function') {
1093
+ throw new TypeError('hmr must create a channel with an onFileEvents function')
1094
+ }
1095
+ if (!('updateWatchedFiles' in channel) || typeof channel.updateWatchedFiles !== 'function') {
1096
+ throw new TypeError('hmr must create a channel with an updateWatchedFiles function')
706
1097
  }
707
1098
  }
708
1099
 
@@ -758,6 +1149,26 @@ function normalizeWatchOptions(
758
1149
  return options
759
1150
  }
760
1151
 
1152
+ function hasPackages(packages: readonly string[] | undefined): boolean {
1153
+ return packages !== undefined && packages.length > 0
1154
+ }
1155
+
1156
+ function getPackageSearchRoots(
1157
+ fileMap: AssetServerOptions['fileMap'],
1158
+ rootDir: string,
1159
+ ): 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(/[/\\]*$/, '')
1170
+ }
1171
+
761
1172
  function parseAssetRequestPathname(
762
1173
  pathname: string,
763
1174
  options: {
@@ -9,6 +9,7 @@ type AssetServerCompilationErrorCode =
9
9
  | 'FILE_TRANSFORM_FAILED'
10
10
  | 'COMMONJS_NOT_SUPPORTED'
11
11
  | 'TRANSFORM_FAILED'
12
+ | 'LOADER_FAILED'
12
13
  | 'EMIT_FAILED'
13
14
  | 'IMPORT_RESOLUTION_FAILED'
14
15
  | 'IMPORT_NOT_SUPPORTED'
@@ -554,9 +554,13 @@ export function resolveServedFileOrThrow(filePath: string, args: ResolveArgs): R
554
554
  }
555
555
 
556
556
  if (!args.isAllowed(identityPath)) {
557
- throw createAssetServerCompilationError(`File is not allowed: ${identityPath}`, {
558
- code: 'FILE_NOT_ALLOWED',
559
- })
557
+ throw createAssetServerCompilationError(
558
+ `File "${identityPath}" is not allowed by the asset server access configuration. ` +
559
+ `Add a matching allowFiles or allowPackages rule, or remove a conflicting denyFiles rule.`,
560
+ {
561
+ code: 'FILE_NOT_ALLOWED',
562
+ },
563
+ )
560
564
  }
561
565
 
562
566
  let stableUrlPathname = args.routes.toUrlPathname(identityPath)