@remix-run/assets 0.4.3 → 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.
- package/README.md +322 -56
- package/dist/assets.d.ts +2 -1
- package/dist/assets.d.ts.map +1 -1
- package/dist/assets.js +2 -2
- package/dist/lib/access.d.ts +6 -2
- package/dist/lib/access.d.ts.map +1 -1
- package/dist/lib/access.js +300 -5
- package/dist/lib/asset-server.d.ts +118 -5
- package/dist/lib/asset-server.d.ts.map +1 -1
- package/dist/lib/asset-server.js +260 -24
- package/dist/lib/compilation-error.d.ts +1 -1
- package/dist/lib/compilation-error.d.ts.map +1 -1
- package/dist/lib/file-matcher.js +1 -1
- package/dist/lib/files/compiler.d.ts.map +1 -1
- package/dist/lib/files/compiler.js +7 -6
- package/dist/lib/files/config.js +1 -1
- package/dist/lib/hmr.d.ts +37 -0
- package/dist/lib/hmr.d.ts.map +1 -0
- package/dist/lib/hmr.js +357 -0
- package/dist/lib/injected-packages.d.ts.map +1 -1
- package/dist/lib/injected-packages.js +36 -13
- package/dist/lib/loaders.d.ts +57 -0
- package/dist/lib/loaders.d.ts.map +1 -0
- package/dist/lib/loaders.js +1 -0
- package/dist/lib/module-store.d.ts +24 -0
- package/dist/lib/module-store.d.ts.map +1 -1
- package/dist/lib/module-store.js +136 -11
- package/dist/lib/routes.d.ts.map +1 -1
- package/dist/lib/routes.js +13 -13
- package/dist/lib/scripts/compiler.d.ts +24 -1
- package/dist/lib/scripts/compiler.d.ts.map +1 -1
- package/dist/lib/scripts/compiler.js +183 -29
- package/dist/lib/scripts/conditions.d.ts +2 -0
- package/dist/lib/scripts/conditions.d.ts.map +1 -0
- package/dist/lib/scripts/conditions.js +1 -0
- package/dist/lib/scripts/emit.d.ts +3 -0
- package/dist/lib/scripts/emit.d.ts.map +1 -1
- package/dist/lib/scripts/emit.js +24 -5
- package/dist/lib/scripts/resolve.d.ts +4 -0
- package/dist/lib/scripts/resolve.d.ts.map +1 -1
- package/dist/lib/scripts/resolve.js +70 -5
- package/dist/lib/scripts/transform.d.ts +9 -0
- package/dist/lib/scripts/transform.d.ts.map +1 -1
- package/dist/lib/scripts/transform.js +222 -18
- package/dist/lib/source-maps.js +1 -1
- package/dist/lib/styles/compiler.d.ts +14 -1
- package/dist/lib/styles/compiler.d.ts.map +1 -1
- package/dist/lib/styles/compiler.js +78 -14
- package/dist/lib/styles/emit.js +4 -4
- package/dist/lib/styles/resolve.d.ts.map +1 -1
- package/dist/lib/styles/resolve.js +8 -7
- package/dist/lib/styles/transform.js +3 -3
- package/dist/lib/target.d.ts +1 -1
- package/dist/lib/target.d.ts.map +1 -1
- package/dist/lib/watch.js +1 -1
- package/dist/types/hmr.d.ts +36 -0
- package/package.json +17 -11
- package/src/assets.ts +2 -1
- package/src/lib/access.ts +386 -5
- package/src/lib/asset-server.ts +429 -18
- package/src/lib/compilation-error.ts +1 -0
- package/src/lib/files/compiler.ts +7 -3
- package/src/lib/hmr.ts +397 -0
- package/src/lib/injected-packages.ts +41 -11
- package/src/lib/loaders.ts +80 -0
- package/src/lib/module-store.ts +190 -9
- package/src/lib/routes.ts +24 -14
- package/src/lib/scripts/compiler.ts +248 -24
- package/src/lib/scripts/conditions.ts +1 -0
- package/src/lib/scripts/emit.ts +50 -5
- package/src/lib/scripts/resolve.ts +124 -2
- package/src/lib/scripts/transform.ts +290 -19
- package/src/lib/styles/compiler.ts +108 -8
- package/src/lib/styles/emit.ts +1 -1
- package/src/lib/styles/resolve.ts +11 -7
- package/src/types/hmr.d.ts +36 -0
package/src/lib/asset-server.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
301
|
+
allowFiles: readonly string[]
|
|
302
|
+
allowPackages?: readonly string[]
|
|
182
303
|
basePath: string
|
|
183
304
|
buildId?: string
|
|
184
305
|
define?: Record<string, string>
|
|
185
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
243
|
-
|
|
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
|
-
|
|
338
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1014
|
+
allowFiles: options.allowFiles,
|
|
1015
|
+
allowPackages: options.allowPackages,
|
|
684
1016
|
basePath,
|
|
685
1017
|
buildId: fingerprintOptions.buildId,
|
|
686
1018
|
define: scriptOptions.define,
|
|
687
|
-
|
|
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
|
|
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: {
|
|
@@ -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(
|
|
558
|
-
|
|
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)
|