@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.
- package/README.md +331 -72
- package/dist/assets.d.ts +4 -1
- package/dist/assets.d.ts.map +1 -1
- package/dist/assets.js +2 -2
- package/dist/lib/access.d.ts +35 -4
- package/dist/lib/access.d.ts.map +1 -1
- package/dist/lib/access.js +332 -11
- package/dist/lib/asset-server.d.ts +138 -11
- package/dist/lib/asset-server.d.ts.map +1 -1
- package/dist/lib/asset-server.js +281 -30
- 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 +9 -8
- 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 +3 -2
- package/dist/lib/injected-packages.d.ts.map +1 -1
- package/dist/lib/injected-packages.js +43 -17
- package/dist/lib/inspection.d.ts +39 -0
- package/dist/lib/inspection.d.ts.map +1 -0
- package/dist/lib/inspection.js +160 -0
- 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 +11 -3
- package/dist/lib/routes.d.ts.map +1 -1
- package/dist/lib/routes.js +124 -80
- 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 +75 -10
- 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 +16 -15
- package/dist/lib/styles/transform.js +5 -5
- 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 +16 -11
- package/src/assets.ts +4 -1
- package/src/lib/access.ts +445 -11
- package/src/lib/asset-server.ts +466 -27
- package/src/lib/compilation-error.ts +4 -3
- package/src/lib/files/compiler.ts +9 -5
- package/src/lib/hmr.ts +397 -0
- package/src/lib/injected-packages.ts +52 -16
- package/src/lib/inspection.ts +229 -0
- package/src/lib/loaders.ts +80 -0
- package/src/lib/module-store.ts +190 -9
- package/src/lib/routes.ts +158 -126
- 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 +129 -7
- 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 +19 -15
- package/src/lib/styles/transform.ts +2 -2
- package/src/types/hmr.d.ts +36 -0
package/src/lib/asset-server.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
/**
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
322
|
+
allowFiles: readonly string[]
|
|
323
|
+
allowPackages?: readonly string[]
|
|
182
324
|
basePath: string
|
|
183
325
|
buildId?: string
|
|
184
326
|
define?: Record<string, string>
|
|
185
|
-
|
|
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
|
|
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
|
-
*
|
|
229
|
-
*
|
|
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
|
-
|
|
243
|
-
|
|
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
|
-
|
|
338
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1050
|
+
allowFiles: options.allowFiles,
|
|
1051
|
+
allowPackages: options.allowPackages,
|
|
684
1052
|
basePath,
|
|
685
1053
|
buildId: fingerprintOptions.buildId,
|
|
686
1054
|
define: scriptOptions.define,
|
|
687
|
-
|
|
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
|
-
|
|
1067
|
+
mounts,
|
|
697
1068
|
rootDir,
|
|
698
1069
|
},
|
|
699
|
-
...
|
|
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
|
|
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: {
|