@remix-run/assets 0.2.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 (71) hide show
  1. package/README.md +241 -28
  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 +10 -1
  8. package/dist/lib/asset-server.d.ts +33 -7
  9. package/dist/lib/asset-server.d.ts.map +1 -1
  10. package/dist/lib/asset-server.js +195 -23
  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/injected-packages.d.ts +11 -0
  26. package/dist/lib/injected-packages.d.ts.map +1 -0
  27. package/dist/lib/injected-packages.js +86 -0
  28. package/dist/lib/paths.d.ts +1 -0
  29. package/dist/lib/paths.d.ts.map +1 -1
  30. package/dist/lib/paths.js +12 -0
  31. package/dist/lib/routes.d.ts +5 -7
  32. package/dist/lib/routes.d.ts.map +1 -1
  33. package/dist/lib/routes.js +41 -36
  34. package/dist/lib/scripts/compiler.d.ts +1 -0
  35. package/dist/lib/scripts/compiler.d.ts.map +1 -1
  36. package/dist/lib/scripts/compiler.js +30 -5
  37. package/dist/lib/scripts/resolve.d.ts.map +1 -1
  38. package/dist/lib/scripts/resolve.js +57 -10
  39. package/dist/lib/scripts/transform.d.ts.map +1 -1
  40. package/dist/lib/scripts/transform.js +79 -3
  41. package/dist/lib/styles/compiler.d.ts +4 -0
  42. package/dist/lib/styles/compiler.d.ts.map +1 -1
  43. package/dist/lib/styles/compiler.js +2 -0
  44. package/dist/lib/styles/emit.d.ts +3 -0
  45. package/dist/lib/styles/emit.d.ts.map +1 -1
  46. package/dist/lib/styles/emit.js +36 -1
  47. package/dist/lib/styles/resolve.d.ts +8 -1
  48. package/dist/lib/styles/resolve.d.ts.map +1 -1
  49. package/dist/lib/styles/resolve.js +85 -32
  50. package/dist/lib/watch.d.ts +2 -0
  51. package/dist/lib/watch.d.ts.map +1 -1
  52. package/dist/lib/watch.js +25 -8
  53. package/package.json +11 -5
  54. package/src/assets.ts +1 -0
  55. package/src/lib/access.ts +10 -1
  56. package/src/lib/asset-server.ts +297 -34
  57. package/src/lib/compilation-error.ts +9 -0
  58. package/src/lib/files/compiler.ts +885 -0
  59. package/src/lib/files/config.ts +479 -0
  60. package/src/lib/files/store.ts +109 -0
  61. package/src/lib/fingerprint.ts +8 -7
  62. package/src/lib/injected-packages.ts +117 -0
  63. package/src/lib/paths.ts +28 -0
  64. package/src/lib/routes.ts +67 -55
  65. package/src/lib/scripts/compiler.ts +43 -5
  66. package/src/lib/scripts/resolve.ts +89 -11
  67. package/src/lib/scripts/transform.ts +105 -3
  68. package/src/lib/styles/compiler.ts +9 -0
  69. package/src/lib/styles/emit.ts +75 -1
  70. package/src/lib/styles/resolve.ts +132 -35
  71. package/src/lib/watch.ts +34 -12
@@ -1,9 +1,22 @@
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
- import { normalizeFilePath } from './paths.ts'
18
+ import { getInjectedPackageRouteConfigs } from './injected-packages.ts'
19
+ import { normalizeFilePath, normalizePathname } from './paths.ts'
7
20
  import { compileRoutes } from './routes.ts'
8
21
  import type { CompiledRoutes } from './routes.ts'
9
22
  import { createResponseForScript, createScriptCompiler } from './scripts/compiler.ts'
@@ -53,7 +66,13 @@ interface AssetServerScriptOptions {
53
66
 
54
67
  const scriptExtensionSet = new Set<string>(supportedScriptExtensions)
55
68
 
56
- export interface AssetServerOptions {
69
+ /**
70
+ * Options used to construct an {@link AssetServer} via {@link createAssetServer}.
71
+ */
72
+ export interface AssetServerOptions<transforms extends AssetRequestTransformMap = {}> {
73
+ /** Public mount path for this asset server, e.g. `'/assets'`. */
74
+ basePath: string
75
+ /** File patterns keyed by public URL patterns relative to `basePath`. */
57
76
  /** File patterns keyed by public URL patterns. */
58
77
  fileMap: Readonly<Record<string, string>>
59
78
  /**
@@ -100,6 +119,12 @@ export interface AssetServerOptions {
100
119
  * Script-only configuration.
101
120
  */
102
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>
103
128
  /**
104
129
  * Enable filesystem-backed cache invalidation for long-lived server instances.
105
130
  * Enabled by default. Pass `true` to use the default watcher options, an options
@@ -113,7 +138,26 @@ export interface AssetServerOptions {
113
138
  onError?: (error: unknown) => void | Response | Promise<void | Response>
114
139
  }
115
140
 
116
- 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 = {}> {
117
161
  /**
118
162
  * Serves a script or style request. Returns `Response | null` — null means the request
119
163
  * was not handled by this server, letting the router fall through to a 404.
@@ -122,7 +166,7 @@ export interface AssetServer {
122
166
  /**
123
167
  * Returns the request href for a served asset file.
124
168
  */
125
- getHref(filePath: string): Promise<string>
169
+ getHref(filePath: string, options?: AssetServerGetHrefOptions<transforms>): Promise<string>
126
170
  /**
127
171
  * Returns preload URLs for one or more served asset files, ordered shallowest-first.
128
172
  */
@@ -133,12 +177,14 @@ export interface AssetServer {
133
177
  close(): Promise<void>
134
178
  }
135
179
 
136
- type ResolvedAssetServerOptions = {
180
+ type ResolvedAssetServerOptions<transforms extends AssetRequestTransformMap> = {
137
181
  allow: readonly string[]
182
+ basePath: string
138
183
  buildId?: string
139
184
  define?: Record<string, string>
140
185
  deny?: readonly string[]
141
186
  external: string[]
187
+ files: ResolvedAssetServerFilesOptions
142
188
  fingerprintAssets: boolean
143
189
  minify: boolean
144
190
  onError: NonNullable<AssetServerOptions['onError']>
@@ -151,14 +197,18 @@ type ResolvedAssetServerOptions = {
151
197
  watchOptions: AssetServerWatchOptions | null
152
198
  }
153
199
 
154
- const chokidarWatcherByAssetServer = new WeakMap<AssetServer, ChokidarWatcher>()
155
- const watcherByAssetServer = new WeakMap<AssetServer, AssetServerWatcher>()
200
+ const chokidarWatcherByAssetServer = new WeakMap<object, ChokidarWatcher>()
201
+ const watcherByAssetServer = new WeakMap<object, AssetServerWatcher>()
156
202
 
157
- export function getInternalChokidarWatcher(assetServer: AssetServer): ChokidarWatcher | undefined {
203
+ export function getInternalChokidarWatcher<transforms extends AssetRequestTransformMap>(
204
+ assetServer: AssetServer<transforms>,
205
+ ): ChokidarWatcher | undefined {
158
206
  return chokidarWatcherByAssetServer.get(assetServer)
159
207
  }
160
208
 
161
- export function getInternalWatchTargets(assetServer: AssetServer): readonly string[] {
209
+ export function getInternalWatchTargets<transforms extends AssetRequestTransformMap>(
210
+ assetServer: AssetServer<transforms>,
211
+ ): readonly string[] {
162
212
  return watcherByAssetServer.get(assetServer)?.getWatchedTargets() ?? []
163
213
  }
164
214
 
@@ -174,8 +224,9 @@ export function getInternalWatchTargets(assetServer: AssetServer): readonly stri
174
224
  * @example
175
225
  * ```ts
176
226
  * let assetServer = createAssetServer({
227
+ * basePath: '/assets',
177
228
  * fileMap: {
178
- * '/assets/app/*path': 'app/*path',
229
+ * '/app/*path': 'app/*path',
179
230
  * },
180
231
  * allow: ['app/**'],
181
232
  * })
@@ -183,7 +234,9 @@ export function getInternalWatchTargets(assetServer: AssetServer): readonly stri
183
234
  * route('/assets/*path', ({ request }) => assetServer.fetch(request))
184
235
  * ```
185
236
  */
186
- export function createAssetServer(options: AssetServerOptions): AssetServer {
237
+ export function createAssetServer<const transforms extends AssetRequestTransformMap = {}>(
238
+ options: AssetServerCreateOptions<transforms>,
239
+ ): AssetServer<transforms> {
187
240
  let resolvedOptions = resolveAssetServerOptions(options)
188
241
  let accessPolicy = createAccessPolicy({
189
242
  allow: resolvedOptions.allow,
@@ -192,12 +245,14 @@ export function createAssetServer(options: AssetServerOptions): AssetServer {
192
245
  })
193
246
  let watcher: AssetServerWatcher | null = null
194
247
  let chokidarWatcher: ChokidarWatcher | null = null
248
+ let fileCompiler: FileCompiler | null = null
195
249
  let scriptCompiler = createScriptCompiler({
196
250
  buildId: resolvedOptions.buildId,
197
251
  define: resolvedOptions.define,
198
252
  external: resolvedOptions.external,
199
253
  fingerprintAssets: resolvedOptions.fingerprintAssets,
200
254
  isAllowed: accessPolicy.isAllowed,
255
+ isDenied: accessPolicy.isDenied,
201
256
  minify: resolvedOptions.minify,
202
257
  onWatchDirectoriesChange: (delta) => {
203
258
  if (!watcher) return
@@ -214,11 +269,27 @@ export function createAssetServer(options: AssetServerOptions): AssetServer {
214
269
  let styleCompiler = createStyleCompiler({
215
270
  buildId: resolvedOptions.buildId,
216
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
+ },
217
285
  isAllowed: accessPolicy.isAllowed,
286
+ isServedFilePath(filePath) {
287
+ return fileCompiler?.isServedFilePath(filePath) ?? false
288
+ },
218
289
  minify: resolvedOptions.minify,
219
290
  onWatchDirectoriesChange: (delta) => {
220
291
  if (!watcher) return
221
- watcher.updateWatchedDirectories(delta)
292
+ watcher.updateWatchedDirectories(delta, { includeAncestors: false })
222
293
  },
223
294
  rootDir: resolvedOptions.rootDir,
224
295
  routes: resolvedOptions.routes,
@@ -227,6 +298,20 @@ export function createAssetServer(options: AssetServerOptions): AssetServer {
227
298
  targets: resolvedOptions.stylesTarget,
228
299
  watchIgnore: resolvedOptions.watchOptions?.ignore,
229
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
+ }
230
315
  if (resolvedOptions.watchOptions) {
231
316
  watcher = createAssetServerWatcher({
232
317
  ...resolvedOptions.watchOptions,
@@ -252,16 +337,19 @@ export function createAssetServer(options: AssetServerOptions): AssetServer {
252
337
  let normalizedFilePath = normalizeFilePath(filePath)
253
338
  await scriptCompiler.handleFileEvent(normalizedFilePath, event)
254
339
  await styleCompiler.handleFileEvent(normalizedFilePath, event)
340
+ await fileCompiler?.handleFileEvent(normalizedFilePath, event)
255
341
  } catch (error) {
256
342
  console.error(`There was an error invalidating the asset server cache: ${error}`)
257
343
  }
258
344
  }
259
345
 
260
- let assetServer: AssetServer = {
346
+ let assetServer: AssetServer<transforms> = {
261
347
  async fetch(request) {
262
348
  if (request.method !== 'GET' && request.method !== 'HEAD') return null
263
349
 
264
- 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, {
265
353
  fingerprintAssets: resolvedOptions.fingerprintAssets,
266
354
  routes: resolvedOptions.routes,
267
355
  })
@@ -271,6 +359,11 @@ export function createAssetServer(options: AssetServerOptions): AssetServer {
271
359
  let ifNoneMatch = request.headers.get('If-None-Match')
272
360
 
273
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
+ }
274
367
  let styleResult = await styleCompiler.getStyle(parsedRequestPathname.filePath, {
275
368
  ifNoneMatch,
276
369
  isSourceMapRequest: parsedRequestPathname.isSourceMapRequest,
@@ -299,9 +392,43 @@ export function createAssetServer(options: AssetServerOptions): AssetServer {
299
392
  }
300
393
 
301
394
  if (!isScriptFilePath(parsedRequestPathname.filePath)) {
302
- 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
+ })
303
425
  }
304
426
 
427
+ if (requestedTransform !== null) {
428
+ return badRequest(
429
+ 'Asset request transforms are only supported for configured file assets',
430
+ )
431
+ }
305
432
  let scriptResult = await scriptCompiler.getScript(parsedRequestPathname.filePath, {
306
433
  ifNoneMatch,
307
434
  isSourceMapRequest: parsedRequestPathname.isSourceMapRequest,
@@ -336,52 +463,113 @@ export function createAssetServer(options: AssetServerOptions): AssetServer {
336
463
  ) {
337
464
  return null
338
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
+ }
339
472
 
340
473
  return responseForError(error)
341
474
  }
342
475
  },
343
476
 
344
- async getHref(filePath) {
345
- 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
+ }
346
487
  return styleCompiler.getHref(filePath)
347
488
  }
348
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
+
349
508
  return scriptCompiler.getHref(filePath)
350
509
  },
351
510
  async getPreloads(filePath) {
352
511
  let filePaths = Array.isArray(filePath) ? filePath : [filePath]
512
+ let fileAssetFiles: string[] = []
353
513
  let styleFiles: string[] = []
354
514
  let scriptFiles: string[] = []
515
+ let groupedAssetTypes: ('script' | 'style' | 'file')[] = []
355
516
 
356
517
  for (let nextFilePath of filePaths) {
357
- if (isStyleFilePath(nextFilePath)) {
518
+ let typeCheckFilePath = stripFilePathUrlSuffix(nextFilePath)
519
+
520
+ if (isStyleFilePath(typeCheckFilePath)) {
521
+ if (!groupedAssetTypes.includes('style')) {
522
+ groupedAssetTypes.push('style')
523
+ }
358
524
  styleFiles.push(nextFilePath)
359
525
  continue
360
526
  }
361
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
+ }
362
539
  scriptFiles.push(nextFilePath)
363
540
  }
364
541
 
365
- if (styleFiles.length === 0 && scriptFiles.length === 0) {
542
+ if (styleFiles.length === 0 && scriptFiles.length === 0 && fileAssetFiles.length === 0) {
366
543
  return []
367
544
  }
368
545
 
369
- if (styleFiles.length === 0) {
546
+ if (styleFiles.length === 0 && fileAssetFiles.length === 0) {
370
547
  return flattenPreloadLayers(await scriptCompiler.getPreloadLayers(filePath))
371
548
  }
372
549
 
373
- if (scriptFiles.length === 0) {
550
+ if (scriptFiles.length === 0 && fileAssetFiles.length === 0) {
374
551
  return flattenPreloadLayers(await styleCompiler.getPreloadLayers(filePath))
375
552
  }
376
553
 
377
- // Mixed asset type preloads need to be merged, so we merge in order of first asset type seen
378
- let scriptPreloadLayersPromise = scriptCompiler.getPreloadLayers(scriptFiles)
379
- let stylePreloadLayersPromise = styleCompiler.getPreloadLayers(styleFiles)
380
- let preloadLayerGroups = isStyleFilePath(filePaths[0])
381
- ? [stylePreloadLayersPromise, scriptPreloadLayersPromise]
382
- : [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
+ })
383
571
 
384
- return mergePreloadLayers(await Promise.all(preloadLayerGroups))
572
+ return mergePreloadLayers(await Promise.all(preloadLayerGroupPromises))
385
573
  },
386
574
  async close() {
387
575
  await watcher?.close()
@@ -398,6 +586,24 @@ export function createAssetServer(options: AssetServerOptions): AssetServer {
398
586
  return assetServer
399
587
  }
400
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
+
401
607
  function internalServerError(): Response {
402
608
  return new Response('Internal Server Error', {
403
609
  status: 500,
@@ -405,6 +611,13 @@ function internalServerError(): Response {
405
611
  })
406
612
  }
407
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
+
408
621
  function mergePreloadLayers(preloadLayersByRoot: readonly (readonly string[][])[]): string[] {
409
622
  let urls: string[] = []
410
623
  let seen = new Set<string>()
@@ -427,12 +640,40 @@ function flattenPreloadLayers(preloadLayers: readonly (readonly string[])[]): st
427
640
  return preloadLayers.flatMap((layer) => layer)
428
641
  }
429
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
+
430
668
  function defaultErrorHandler(error: unknown): void {
431
669
  console.error(error)
432
670
  }
433
671
 
434
- function resolveAssetServerOptions(options: AssetServerOptions): ResolvedAssetServerOptions {
672
+ function resolveAssetServerOptions<transforms extends AssetRequestTransformMap>(
673
+ options: AssetServerCreateOptions<transforms>,
674
+ ): ResolvedAssetServerOptions<transforms> {
435
675
  let rootDir = normalizeFilePath(fs.realpathSync(path.resolve(options.rootDir ?? process.cwd())))
676
+ let basePath = normalizeBasePath(options.basePath)
436
677
  let scriptOptions = options.scripts ?? {}
437
678
  let fingerprintOptions = normalizeFingerprintOptions({
438
679
  fingerprint: options.fingerprint,
@@ -441,18 +682,23 @@ function resolveAssetServerOptions(options: AssetServerOptions): ResolvedAssetSe
441
682
 
442
683
  return {
443
684
  allow: options.allow,
685
+ basePath,
444
686
  buildId: fingerprintOptions.buildId,
445
687
  define: scriptOptions.define,
446
688
  deny: options.deny,
447
689
  external: scriptOptions.external ?? [],
690
+ files: normalizeFilesOptions(options.files),
448
691
  fingerprintAssets: fingerprintOptions.enabled,
449
692
  minify: options.minify ?? false,
450
693
  onError: options.onError ?? defaultErrorHandler,
451
694
  rootDir,
452
- routes: compileRoutes({
453
- fileMap: options.fileMap,
454
- rootDir,
455
- }),
695
+ routes: compileRoutes(basePath, [
696
+ {
697
+ fileMap: options.fileMap,
698
+ rootDir,
699
+ },
700
+ ...getInjectedPackageRouteConfigs(),
701
+ ]),
456
702
  sourceMapSourcePaths: options.sourceMapSourcePaths ?? 'url',
457
703
  sourceMaps: options.sourceMaps,
458
704
  scriptsTarget: resolveScriptTarget(options.target),
@@ -461,6 +707,14 @@ function resolveAssetServerOptions(options: AssetServerOptions): ResolvedAssetSe
461
707
  }
462
708
  }
463
709
 
710
+ function normalizeBasePath(basePath: string): string {
711
+ if (typeof basePath !== 'string') {
712
+ throw new TypeError('basePath must be a string')
713
+ }
714
+
715
+ return normalizePathname(basePath || '/').replace(/\/+$/, '') || '/'
716
+ }
717
+
464
718
  function normalizeFingerprintOptions(options: {
465
719
  fingerprint: AssetServerOptions['fingerprint']
466
720
  watch: AssetServerOptions['watch']
@@ -535,3 +789,12 @@ function parseAssetRequestPathname(
535
789
  function isScriptFilePath(filePath: string): boolean {
536
790
  return scriptExtensionSet.has(path.extname(filePath).toLowerCase())
537
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.