@remix-run/assets 0.3.0 → 0.4.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 (61) hide show
  1. package/README.md +206 -8
  2. package/dist/assets.d.ts +1 -0
  3. package/dist/assets.d.ts.map +1 -1
  4. package/dist/assets.js +1 -0
  5. package/dist/lib/access.d.ts +1 -0
  6. package/dist/lib/access.d.ts.map +1 -1
  7. package/dist/lib/access.js +7 -1
  8. package/dist/lib/asset-server.d.ts +29 -6
  9. package/dist/lib/asset-server.d.ts.map +1 -1
  10. package/dist/lib/asset-server.js +176 -17
  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/files/compiler.d.ts +71 -0
  14. package/dist/lib/files/compiler.d.ts.map +1 -0
  15. package/dist/lib/files/compiler.js +552 -0
  16. package/dist/lib/files/config.d.ts +101 -0
  17. package/dist/lib/files/config.d.ts.map +1 -0
  18. package/dist/lib/files/config.js +219 -0
  19. package/dist/lib/files/store.d.ts +31 -0
  20. package/dist/lib/files/store.d.ts.map +1 -0
  21. package/dist/lib/files/store.js +63 -0
  22. package/dist/lib/fingerprint.d.ts +3 -3
  23. package/dist/lib/fingerprint.d.ts.map +1 -1
  24. package/dist/lib/fingerprint.js +5 -5
  25. package/dist/lib/routes.d.ts.map +1 -1
  26. package/dist/lib/routes.js +28 -20
  27. package/dist/lib/scripts/compiler.d.ts +1 -0
  28. package/dist/lib/scripts/compiler.d.ts.map +1 -1
  29. package/dist/lib/scripts/compiler.js +30 -5
  30. package/dist/lib/scripts/resolve.d.ts.map +1 -1
  31. package/dist/lib/scripts/resolve.js +22 -2
  32. package/dist/lib/scripts/transform.js +1 -1
  33. package/dist/lib/styles/compiler.d.ts +4 -0
  34. package/dist/lib/styles/compiler.d.ts.map +1 -1
  35. package/dist/lib/styles/compiler.js +2 -0
  36. package/dist/lib/styles/emit.d.ts +3 -0
  37. package/dist/lib/styles/emit.d.ts.map +1 -1
  38. package/dist/lib/styles/emit.js +36 -1
  39. package/dist/lib/styles/resolve.d.ts +8 -1
  40. package/dist/lib/styles/resolve.d.ts.map +1 -1
  41. package/dist/lib/styles/resolve.js +85 -32
  42. package/dist/lib/watch.d.ts +2 -0
  43. package/dist/lib/watch.d.ts.map +1 -1
  44. package/dist/lib/watch.js +25 -8
  45. package/package.json +6 -4
  46. package/src/assets.ts +1 -0
  47. package/src/lib/access.ts +8 -1
  48. package/src/lib/asset-server.ts +273 -28
  49. package/src/lib/compilation-error.ts +9 -0
  50. package/src/lib/files/compiler.ts +885 -0
  51. package/src/lib/files/config.ts +479 -0
  52. package/src/lib/files/store.ts +109 -0
  53. package/src/lib/fingerprint.ts +8 -7
  54. package/src/lib/routes.ts +36 -33
  55. package/src/lib/scripts/compiler.ts +43 -5
  56. package/src/lib/scripts/resolve.ts +35 -3
  57. package/src/lib/scripts/transform.ts +1 -1
  58. package/src/lib/styles/compiler.ts +9 -0
  59. package/src/lib/styles/emit.ts +75 -1
  60. package/src/lib/styles/resolve.ts +132 -35
  61. package/src/lib/watch.ts +34 -12
@@ -1,7 +1,19 @@
1
1
  import * as path from 'node:path'
2
2
  import * as fs from 'node:fs'
3
3
  import { createAccessPolicy } from './access.ts'
4
- import { isAssetServerCompilationError } from './compilation-error.ts'
4
+ import {
5
+ createAssetServerCompilationError,
6
+ isAssetServerCompilationError,
7
+ } from './compilation-error.ts'
8
+ import { createFileCompiler, createResponseForFile } from './files/compiler.ts'
9
+ import type { FileCompiler } from './files/compiler.ts'
10
+ import { normalizeFilesOptions, serializeAssetTransformInvocations } from './files/config.ts'
11
+ import type {
12
+ AssetRequestTransformMap,
13
+ AssetServerFilesOptions,
14
+ AssetTransformInvocation,
15
+ ResolvedAssetServerFilesOptions,
16
+ } from './files/config.ts'
5
17
  import { getFingerprintRequestCacheControl, parseFingerprintSuffix } from './fingerprint.ts'
6
18
  import { getInjectedPackageRouteConfigs } from './injected-packages.ts'
7
19
  import { normalizeFilePath, normalizePathname } from './paths.ts'
@@ -54,10 +66,14 @@ interface AssetServerScriptOptions {
54
66
 
55
67
  const scriptExtensionSet = new Set<string>(supportedScriptExtensions)
56
68
 
57
- export interface AssetServerOptions {
69
+ /**
70
+ * Options used to construct an {@link AssetServer} via {@link createAssetServer}.
71
+ */
72
+ export interface AssetServerOptions<transforms extends AssetRequestTransformMap = {}> {
58
73
  /** Public mount path for this asset server, e.g. `'/assets'`. */
59
74
  basePath: string
60
75
  /** File patterns keyed by public URL patterns relative to `basePath`. */
76
+ /** File patterns keyed by public URL patterns. */
61
77
  fileMap: Readonly<Record<string, string>>
62
78
  /**
63
79
  * Root directory used to resolve relative file paths. Defaults to `process.cwd()`.
@@ -103,6 +119,12 @@ export interface AssetServerOptions {
103
119
  * Script-only configuration.
104
120
  */
105
121
  scripts?: AssetServerScriptOptions
122
+ /**
123
+ * Leaf file asset configuration. Files configured here are served directly and can be
124
+ * referenced from CSS `url(...)` rules. Compiled asset extensions like `.css` and script
125
+ * module extensions are not allowed here.
126
+ */
127
+ files?: AssetServerFilesOptions<transforms>
106
128
  /**
107
129
  * Enable filesystem-backed cache invalidation for long-lived server instances.
108
130
  * Enabled by default. Pass `true` to use the default watcher options, an options
@@ -116,7 +138,26 @@ export interface AssetServerOptions {
116
138
  onError?: (error: unknown) => void | Response | Promise<void | Response>
117
139
  }
118
140
 
119
- export interface AssetServer {
141
+ type AssetServerCreateOptions<transforms extends AssetRequestTransformMap> = Omit<
142
+ AssetServerOptions<transforms>,
143
+ 'files'
144
+ > & {
145
+ files?: Omit<AssetServerFilesOptions<transforms>, 'transforms'> & {
146
+ transforms?: transforms
147
+ }
148
+ }
149
+
150
+ export type AssetServerGetHrefOptions<transforms extends AssetRequestTransformMap> =
151
+ | undefined
152
+ | {
153
+ transform: readonly AssetTransformInvocation<transforms>[]
154
+ }
155
+
156
+ /**
157
+ * Serves compiled scripts and styles for asset requests routed to it.
158
+ * Construct with {@link createAssetServer}.
159
+ */
160
+ export interface AssetServer<transforms extends AssetRequestTransformMap = {}> {
120
161
  /**
121
162
  * Serves a script or style request. Returns `Response | null` — null means the request
122
163
  * was not handled by this server, letting the router fall through to a 404.
@@ -125,7 +166,7 @@ export interface AssetServer {
125
166
  /**
126
167
  * Returns the request href for a served asset file.
127
168
  */
128
- getHref(filePath: string): Promise<string>
169
+ getHref(filePath: string, options?: AssetServerGetHrefOptions<transforms>): Promise<string>
129
170
  /**
130
171
  * Returns preload URLs for one or more served asset files, ordered shallowest-first.
131
172
  */
@@ -136,13 +177,14 @@ export interface AssetServer {
136
177
  close(): Promise<void>
137
178
  }
138
179
 
139
- type ResolvedAssetServerOptions = {
180
+ type ResolvedAssetServerOptions<transforms extends AssetRequestTransformMap> = {
140
181
  allow: readonly string[]
141
182
  basePath: string
142
183
  buildId?: string
143
184
  define?: Record<string, string>
144
185
  deny?: readonly string[]
145
186
  external: string[]
187
+ files: ResolvedAssetServerFilesOptions
146
188
  fingerprintAssets: boolean
147
189
  minify: boolean
148
190
  onError: NonNullable<AssetServerOptions['onError']>
@@ -155,14 +197,18 @@ type ResolvedAssetServerOptions = {
155
197
  watchOptions: AssetServerWatchOptions | null
156
198
  }
157
199
 
158
- const chokidarWatcherByAssetServer = new WeakMap<AssetServer, ChokidarWatcher>()
159
- const watcherByAssetServer = new WeakMap<AssetServer, AssetServerWatcher>()
200
+ const chokidarWatcherByAssetServer = new WeakMap<object, ChokidarWatcher>()
201
+ const watcherByAssetServer = new WeakMap<object, AssetServerWatcher>()
160
202
 
161
- export function getInternalChokidarWatcher(assetServer: AssetServer): ChokidarWatcher | undefined {
203
+ export function getInternalChokidarWatcher<transforms extends AssetRequestTransformMap>(
204
+ assetServer: AssetServer<transforms>,
205
+ ): ChokidarWatcher | undefined {
162
206
  return chokidarWatcherByAssetServer.get(assetServer)
163
207
  }
164
208
 
165
- export function getInternalWatchTargets(assetServer: AssetServer): readonly string[] {
209
+ export function getInternalWatchTargets<transforms extends AssetRequestTransformMap>(
210
+ assetServer: AssetServer<transforms>,
211
+ ): readonly string[] {
166
212
  return watcherByAssetServer.get(assetServer)?.getWatchedTargets() ?? []
167
213
  }
168
214
 
@@ -188,7 +234,9 @@ export function getInternalWatchTargets(assetServer: AssetServer): readonly stri
188
234
  * route('/assets/*path', ({ request }) => assetServer.fetch(request))
189
235
  * ```
190
236
  */
191
- export function createAssetServer(options: AssetServerOptions): AssetServer {
237
+ export function createAssetServer<const transforms extends AssetRequestTransformMap = {}>(
238
+ options: AssetServerCreateOptions<transforms>,
239
+ ): AssetServer<transforms> {
192
240
  let resolvedOptions = resolveAssetServerOptions(options)
193
241
  let accessPolicy = createAccessPolicy({
194
242
  allow: resolvedOptions.allow,
@@ -197,12 +245,14 @@ export function createAssetServer(options: AssetServerOptions): AssetServer {
197
245
  })
198
246
  let watcher: AssetServerWatcher | null = null
199
247
  let chokidarWatcher: ChokidarWatcher | null = null
248
+ let fileCompiler: FileCompiler | null = null
200
249
  let scriptCompiler = createScriptCompiler({
201
250
  buildId: resolvedOptions.buildId,
202
251
  define: resolvedOptions.define,
203
252
  external: resolvedOptions.external,
204
253
  fingerprintAssets: resolvedOptions.fingerprintAssets,
205
254
  isAllowed: accessPolicy.isAllowed,
255
+ isDenied: accessPolicy.isDenied,
206
256
  minify: resolvedOptions.minify,
207
257
  onWatchDirectoriesChange: (delta) => {
208
258
  if (!watcher) return
@@ -219,11 +269,27 @@ export function createAssetServer(options: AssetServerOptions): AssetServer {
219
269
  let styleCompiler = createStyleCompiler({
220
270
  buildId: resolvedOptions.buildId,
221
271
  fingerprintAssets: resolvedOptions.fingerprintAssets,
272
+ getServedFileUrl: (identityPath: string, options: { transform: readonly string[] | null }) => {
273
+ if (!fileCompiler) {
274
+ throw createAssetServerCompilationError(`File type is not supported: ${identityPath}`, {
275
+ code: 'FILE_NOT_SUPPORTED',
276
+ })
277
+ }
278
+
279
+ if (options.transform !== null) {
280
+ fileCompiler.validateTransformQuery(options.transform)
281
+ }
282
+
283
+ return fileCompiler.getHref(identityPath, { transform: options.transform })
284
+ },
222
285
  isAllowed: accessPolicy.isAllowed,
286
+ isServedFilePath(filePath) {
287
+ return fileCompiler?.isServedFilePath(filePath) ?? false
288
+ },
223
289
  minify: resolvedOptions.minify,
224
290
  onWatchDirectoriesChange: (delta) => {
225
291
  if (!watcher) return
226
- watcher.updateWatchedDirectories(delta)
292
+ watcher.updateWatchedDirectories(delta, { includeAncestors: false })
227
293
  },
228
294
  rootDir: resolvedOptions.rootDir,
229
295
  routes: resolvedOptions.routes,
@@ -232,6 +298,20 @@ export function createAssetServer(options: AssetServerOptions): AssetServer {
232
298
  targets: resolvedOptions.stylesTarget,
233
299
  watchIgnore: resolvedOptions.watchOptions?.ignore,
234
300
  })
301
+ if (resolvedOptions.files.extensions.length > 0) {
302
+ fileCompiler = createFileCompiler({
303
+ buildId: resolvedOptions.buildId,
304
+ cache: resolvedOptions.files.cache,
305
+ extensions: resolvedOptions.files.extensions,
306
+ fingerprintAssets: resolvedOptions.fingerprintAssets,
307
+ globalTransforms: resolvedOptions.files.globalTransforms,
308
+ isAllowed: accessPolicy.isAllowed,
309
+ maxRequestTransforms: resolvedOptions.files.maxRequestTransforms,
310
+ transforms: resolvedOptions.files.transforms,
311
+ rootDir: resolvedOptions.rootDir,
312
+ routes: resolvedOptions.routes,
313
+ })
314
+ }
235
315
  if (resolvedOptions.watchOptions) {
236
316
  watcher = createAssetServerWatcher({
237
317
  ...resolvedOptions.watchOptions,
@@ -257,16 +337,19 @@ export function createAssetServer(options: AssetServerOptions): AssetServer {
257
337
  let normalizedFilePath = normalizeFilePath(filePath)
258
338
  await scriptCompiler.handleFileEvent(normalizedFilePath, event)
259
339
  await styleCompiler.handleFileEvent(normalizedFilePath, event)
340
+ await fileCompiler?.handleFileEvent(normalizedFilePath, event)
260
341
  } catch (error) {
261
342
  console.error(`There was an error invalidating the asset server cache: ${error}`)
262
343
  }
263
344
  }
264
345
 
265
- let assetServer: AssetServer = {
346
+ let assetServer: AssetServer<transforms> = {
266
347
  async fetch(request) {
267
348
  if (request.method !== 'GET' && request.method !== 'HEAD') return null
268
349
 
269
- let parsedRequestPathname = parseAssetRequestPathname(new URL(request.url).pathname, {
350
+ let requestUrl = new URL(request.url)
351
+ let requestedTransform = normalizeRequestedTransformQuery(requestUrl.searchParams)
352
+ let parsedRequestPathname = parseAssetRequestPathname(requestUrl.pathname, {
270
353
  fingerprintAssets: resolvedOptions.fingerprintAssets,
271
354
  routes: resolvedOptions.routes,
272
355
  })
@@ -276,6 +359,11 @@ export function createAssetServer(options: AssetServerOptions): AssetServer {
276
359
  let ifNoneMatch = request.headers.get('If-None-Match')
277
360
 
278
361
  if (isStyleFilePath(parsedRequestPathname.filePath)) {
362
+ if (requestedTransform !== null) {
363
+ return badRequest(
364
+ 'Asset request transforms are only supported for configured file assets',
365
+ )
366
+ }
279
367
  let styleResult = await styleCompiler.getStyle(parsedRequestPathname.filePath, {
280
368
  ifNoneMatch,
281
369
  isSourceMapRequest: parsedRequestPathname.isSourceMapRequest,
@@ -304,9 +392,43 @@ export function createAssetServer(options: AssetServerOptions): AssetServer {
304
392
  }
305
393
 
306
394
  if (!isScriptFilePath(parsedRequestPathname.filePath)) {
307
- return null
395
+ if (!fileCompiler?.isServedFilePath(parsedRequestPathname.filePath)) {
396
+ return null
397
+ }
398
+ if (parsedRequestPathname.isSourceMapRequest) {
399
+ return null
400
+ }
401
+
402
+ let fileResult = await fileCompiler.getFile(parsedRequestPathname.filePath, {
403
+ ifNoneMatch,
404
+ requestedFingerprint: parsedRequestPathname.requestedFingerprint,
405
+ transform: requestedTransform,
406
+ })
407
+ if (fileResult.type === 'not-modified') {
408
+ return new Response(null, {
409
+ status: 304,
410
+ headers: { ETag: fileResult.etag },
411
+ })
412
+ }
413
+
414
+ if (parsedRequestPathname.requestedFingerprint !== null) {
415
+ if (fileResult.file.fingerprint !== parsedRequestPathname.requestedFingerprint) {
416
+ return null
417
+ }
418
+ }
419
+
420
+ return createResponseForFile(fileResult.file, {
421
+ cacheControl: parsedRequestPathname.cacheControl,
422
+ ifNoneMatch,
423
+ method: request.method,
424
+ })
308
425
  }
309
426
 
427
+ if (requestedTransform !== null) {
428
+ return badRequest(
429
+ 'Asset request transforms are only supported for configured file assets',
430
+ )
431
+ }
310
432
  let scriptResult = await scriptCompiler.getScript(parsedRequestPathname.filePath, {
311
433
  ifNoneMatch,
312
434
  isSourceMapRequest: parsedRequestPathname.isSourceMapRequest,
@@ -341,52 +463,113 @@ export function createAssetServer(options: AssetServerOptions): AssetServer {
341
463
  ) {
342
464
  return null
343
465
  }
466
+ if (isAssetServerCompilationError(error) && error.code === 'FILE_TRANSFORM_QUERY_INVALID') {
467
+ return new Response(error.message, {
468
+ status: 400,
469
+ headers: { 'Content-Type': 'text/plain; charset=utf-8' },
470
+ })
471
+ }
344
472
 
345
473
  return responseForError(error)
346
474
  }
347
475
  },
348
476
 
349
- async getHref(filePath) {
350
- if (isStyleFilePath(filePath)) {
477
+ async getHref(filePath, hrefOptions) {
478
+ let transform = getHrefTransform(hrefOptions, resolvedOptions.files)
479
+ let typeCheckFilePath = stripFilePathUrlSuffix(filePath)
480
+
481
+ if (isStyleFilePath(typeCheckFilePath)) {
482
+ if (transform !== null) {
483
+ throw new TypeError(
484
+ 'assetServer.getHref() only supports transforms for configured file assets',
485
+ )
486
+ }
351
487
  return styleCompiler.getHref(filePath)
352
488
  }
353
489
 
490
+ if (fileCompiler?.isServedFilePath(typeCheckFilePath)) {
491
+ return fileCompiler.getHref(filePath, {
492
+ transform,
493
+ })
494
+ }
495
+
496
+ if (!isScriptFilePath(typeCheckFilePath)) {
497
+ throw createAssetServerCompilationError(`File type is not supported: ${filePath}`, {
498
+ code: 'FILE_NOT_SUPPORTED',
499
+ })
500
+ }
501
+
502
+ if (transform !== null) {
503
+ throw new TypeError(
504
+ 'assetServer.getHref() only supports transforms for configured file assets',
505
+ )
506
+ }
507
+
354
508
  return scriptCompiler.getHref(filePath)
355
509
  },
356
510
  async getPreloads(filePath) {
357
511
  let filePaths = Array.isArray(filePath) ? filePath : [filePath]
512
+ let fileAssetFiles: string[] = []
358
513
  let styleFiles: string[] = []
359
514
  let scriptFiles: string[] = []
515
+ let groupedAssetTypes: ('script' | 'style' | 'file')[] = []
360
516
 
361
517
  for (let nextFilePath of filePaths) {
362
- if (isStyleFilePath(nextFilePath)) {
518
+ let typeCheckFilePath = stripFilePathUrlSuffix(nextFilePath)
519
+
520
+ if (isStyleFilePath(typeCheckFilePath)) {
521
+ if (!groupedAssetTypes.includes('style')) {
522
+ groupedAssetTypes.push('style')
523
+ }
363
524
  styleFiles.push(nextFilePath)
364
525
  continue
365
526
  }
366
527
 
528
+ if (fileCompiler?.isServedFilePath(typeCheckFilePath)) {
529
+ if (!groupedAssetTypes.includes('file')) {
530
+ groupedAssetTypes.push('file')
531
+ }
532
+ fileAssetFiles.push(nextFilePath)
533
+ continue
534
+ }
535
+
536
+ if (!groupedAssetTypes.includes('script')) {
537
+ groupedAssetTypes.push('script')
538
+ }
367
539
  scriptFiles.push(nextFilePath)
368
540
  }
369
541
 
370
- if (styleFiles.length === 0 && scriptFiles.length === 0) {
542
+ if (styleFiles.length === 0 && scriptFiles.length === 0 && fileAssetFiles.length === 0) {
371
543
  return []
372
544
  }
373
545
 
374
- if (styleFiles.length === 0) {
546
+ if (styleFiles.length === 0 && fileAssetFiles.length === 0) {
375
547
  return flattenPreloadLayers(await scriptCompiler.getPreloadLayers(filePath))
376
548
  }
377
549
 
378
- if (scriptFiles.length === 0) {
550
+ if (scriptFiles.length === 0 && fileAssetFiles.length === 0) {
379
551
  return flattenPreloadLayers(await styleCompiler.getPreloadLayers(filePath))
380
552
  }
381
553
 
382
- // Mixed asset type preloads need to be merged, so we merge in order of first asset type seen
383
- let scriptPreloadLayersPromise = scriptCompiler.getPreloadLayers(scriptFiles)
384
- let stylePreloadLayersPromise = styleCompiler.getPreloadLayers(styleFiles)
385
- let preloadLayerGroups = isStyleFilePath(filePaths[0])
386
- ? [stylePreloadLayersPromise, scriptPreloadLayersPromise]
387
- : [scriptPreloadLayersPromise, stylePreloadLayersPromise]
554
+ if (scriptFiles.length === 0 && styleFiles.length === 0) {
555
+ return flattenPreloadLayers(await getFilePreloadLayers(fileCompiler, fileAssetFiles))
556
+ }
557
+
558
+ // Mixed asset type preloads need to be merged, so we merge in order of first asset type seen.
559
+ let preloadLayerGroupPromises = groupedAssetTypes.flatMap((assetType) => {
560
+ switch (assetType) {
561
+ case 'script':
562
+ return scriptFiles.length > 0 ? [scriptCompiler.getPreloadLayers(scriptFiles)] : []
563
+ case 'style':
564
+ return styleFiles.length > 0 ? [styleCompiler.getPreloadLayers(styleFiles)] : []
565
+ case 'file':
566
+ return fileAssetFiles.length > 0
567
+ ? [getFilePreloadLayers(fileCompiler, fileAssetFiles)]
568
+ : []
569
+ }
570
+ })
388
571
 
389
- return mergePreloadLayers(await Promise.all(preloadLayerGroups))
572
+ return mergePreloadLayers(await Promise.all(preloadLayerGroupPromises))
390
573
  },
391
574
  async close() {
392
575
  await watcher?.close()
@@ -403,6 +586,24 @@ export function createAssetServer(options: AssetServerOptions): AssetServer {
403
586
  return assetServer
404
587
  }
405
588
 
589
+ function getHrefTransform<transforms extends AssetRequestTransformMap>(
590
+ options: AssetServerGetHrefOptions<transforms> | undefined,
591
+ files: ResolvedAssetServerFilesOptions,
592
+ ): readonly string[] | null {
593
+ if (!options) return null
594
+ let transform = options.transform
595
+ if (transform.length === 0) {
596
+ return null
597
+ }
598
+
599
+ return serializeAssetTransformInvocations(transform, files.transforms, files.maxRequestTransforms)
600
+ }
601
+
602
+ function normalizeRequestedTransformQuery(searchParams: URLSearchParams): readonly string[] | null {
603
+ let transforms = searchParams.getAll('transform')
604
+ return transforms.length > 0 ? transforms : null
605
+ }
606
+
406
607
  function internalServerError(): Response {
407
608
  return new Response('Internal Server Error', {
408
609
  status: 500,
@@ -410,6 +611,13 @@ function internalServerError(): Response {
410
611
  })
411
612
  }
412
613
 
614
+ function badRequest(message: string): Response {
615
+ return new Response(message, {
616
+ status: 400,
617
+ headers: { 'Content-Type': 'text/plain; charset=utf-8' },
618
+ })
619
+ }
620
+
413
621
  function mergePreloadLayers(preloadLayersByRoot: readonly (readonly string[][])[]): string[] {
414
622
  let urls: string[] = []
415
623
  let seen = new Set<string>()
@@ -432,11 +640,38 @@ function flattenPreloadLayers(preloadLayers: readonly (readonly string[])[]): st
432
640
  return preloadLayers.flatMap((layer) => layer)
433
641
  }
434
642
 
643
+ async function getFilePreloadLayers<transforms extends AssetRequestTransformMap>(
644
+ fileCompiler: FileCompiler | null,
645
+ filePaths: readonly string[],
646
+ ): Promise<string[][]> {
647
+ if (filePaths.length === 0) {
648
+ return []
649
+ }
650
+
651
+ if (!fileCompiler) {
652
+ throw createAssetServerCompilationError('File type is not supported', {
653
+ code: 'FILE_NOT_SUPPORTED',
654
+ })
655
+ }
656
+
657
+ return [
658
+ await Promise.all(
659
+ filePaths.map((filePath) =>
660
+ fileCompiler.getHref(filePath, {
661
+ transform: null,
662
+ }),
663
+ ),
664
+ ),
665
+ ]
666
+ }
667
+
435
668
  function defaultErrorHandler(error: unknown): void {
436
669
  console.error(error)
437
670
  }
438
671
 
439
- function resolveAssetServerOptions(options: AssetServerOptions): ResolvedAssetServerOptions {
672
+ function resolveAssetServerOptions<transforms extends AssetRequestTransformMap>(
673
+ options: AssetServerCreateOptions<transforms>,
674
+ ): ResolvedAssetServerOptions<transforms> {
440
675
  let rootDir = normalizeFilePath(fs.realpathSync(path.resolve(options.rootDir ?? process.cwd())))
441
676
  let basePath = normalizeBasePath(options.basePath)
442
677
  let scriptOptions = options.scripts ?? {}
@@ -452,6 +687,7 @@ function resolveAssetServerOptions(options: AssetServerOptions): ResolvedAssetSe
452
687
  define: scriptOptions.define,
453
688
  deny: options.deny,
454
689
  external: scriptOptions.external ?? [],
690
+ files: normalizeFilesOptions(options.files),
455
691
  fingerprintAssets: fingerprintOptions.enabled,
456
692
  minify: options.minify ?? false,
457
693
  onError: options.onError ?? defaultErrorHandler,
@@ -553,3 +789,12 @@ function parseAssetRequestPathname(
553
789
  function isScriptFilePath(filePath: string): boolean {
554
790
  return scriptExtensionSet.has(path.extname(filePath).toLowerCase())
555
791
  }
792
+
793
+ function stripFilePathUrlSuffix(filePath: string): string {
794
+ if (filePath.startsWith('file://')) {
795
+ return new URL(filePath).pathname
796
+ }
797
+
798
+ let queryIndex = filePath.indexOf('?')
799
+ return queryIndex === -1 ? filePath : filePath.slice(0, queryIndex)
800
+ }
@@ -1,7 +1,12 @@
1
1
  type AssetServerCompilationErrorCode =
2
2
  | 'FILE_NOT_FOUND'
3
3
  | 'FILE_NOT_ALLOWED'
4
+ | 'FILE_NOT_SUPPORTED'
4
5
  | 'FILE_OUTSIDE_FILE_MAP'
6
+ | 'FILE_TRANSFORM_QUERY_INVALID'
7
+ | 'FILE_TRANSFORM_NOT_SUPPORTED'
8
+ | 'FILE_TRANSFORM_RESULT_INVALID'
9
+ | 'FILE_TRANSFORM_FAILED'
5
10
  | 'COMMONJS_NOT_SUPPORTED'
6
11
  | 'TRANSFORM_FAILED'
7
12
  | 'EMIT_FAILED'
@@ -9,6 +14,10 @@ type AssetServerCompilationErrorCode =
9
14
  | 'IMPORT_NOT_SUPPORTED'
10
15
  | 'IMPORT_NOT_ALLOWED'
11
16
  | 'IMPORT_OUTSIDE_FILE_MAP'
17
+ | 'URL_RESOLUTION_FAILED'
18
+ | 'URL_NOT_SUPPORTED'
19
+ | 'URL_NOT_ALLOWED'
20
+ | 'URL_OUTSIDE_FILE_MAP'
12
21
 
13
22
  /**
14
23
  * Internal error used by the request-time asset compilation pipeline.