@remix-run/assets 0.1.0 → 0.3.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 (72) hide show
  1. package/README.md +88 -67
  2. package/dist/lib/access.d.ts.map +1 -1
  3. package/dist/lib/access.js +3 -0
  4. package/dist/lib/asset-server.d.ts +50 -45
  5. package/dist/lib/asset-server.d.ts.map +1 -1
  6. package/dist/lib/asset-server.js +159 -50
  7. package/dist/lib/compilation-error.d.ts +2 -2
  8. package/dist/lib/compilation-error.d.ts.map +1 -1
  9. package/dist/lib/compilation-error.js +1 -1
  10. package/dist/lib/file-matcher.d.ts.map +1 -1
  11. package/dist/lib/file-matcher.js +3 -2
  12. package/dist/lib/injected-packages.d.ts +11 -0
  13. package/dist/lib/injected-packages.d.ts.map +1 -0
  14. package/dist/lib/injected-packages.js +86 -0
  15. package/dist/lib/module-store.d.ts +41 -0
  16. package/dist/lib/module-store.d.ts.map +1 -0
  17. package/dist/lib/{scripts/store.js → module-store.js} +43 -41
  18. package/dist/lib/paths.d.ts +1 -0
  19. package/dist/lib/paths.d.ts.map +1 -1
  20. package/dist/lib/paths.js +12 -0
  21. package/dist/lib/routes.d.ts +5 -7
  22. package/dist/lib/routes.d.ts.map +1 -1
  23. package/dist/lib/routes.js +13 -16
  24. package/dist/lib/scripts/compiler.d.ts +15 -15
  25. package/dist/lib/scripts/compiler.d.ts.map +1 -1
  26. package/dist/lib/scripts/compiler.js +50 -46
  27. package/dist/lib/scripts/emit.js +2 -2
  28. package/dist/lib/scripts/resolve.d.ts +7 -17
  29. package/dist/lib/scripts/resolve.d.ts.map +1 -1
  30. package/dist/lib/scripts/resolve.js +47 -14
  31. package/dist/lib/scripts/transform.d.ts +11 -9
  32. package/dist/lib/scripts/transform.d.ts.map +1 -1
  33. package/dist/lib/scripts/transform.js +110 -23
  34. package/dist/lib/source-maps.d.ts +1 -1
  35. package/dist/lib/source-maps.d.ts.map +1 -1
  36. package/dist/lib/source-maps.js +7 -1
  37. package/dist/lib/styles/compiler.d.ts +52 -0
  38. package/dist/lib/styles/compiler.d.ts.map +1 -0
  39. package/dist/lib/styles/compiler.js +272 -0
  40. package/dist/lib/styles/emit.d.ts +25 -0
  41. package/dist/lib/styles/emit.d.ts.map +1 -0
  42. package/dist/lib/styles/emit.js +78 -0
  43. package/dist/lib/styles/resolve.d.ts +48 -0
  44. package/dist/lib/styles/resolve.d.ts.map +1 -0
  45. package/dist/lib/styles/resolve.js +188 -0
  46. package/dist/lib/styles/transform.d.ts +47 -0
  47. package/dist/lib/styles/transform.d.ts.map +1 -0
  48. package/dist/lib/styles/transform.js +131 -0
  49. package/dist/lib/target.d.ts +21 -0
  50. package/dist/lib/target.d.ts.map +1 -0
  51. package/dist/lib/target.js +127 -0
  52. package/package.json +13 -4
  53. package/src/lib/access.ts +2 -0
  54. package/src/lib/asset-server.ts +239 -99
  55. package/src/lib/compilation-error.ts +7 -7
  56. package/src/lib/file-matcher.ts +3 -2
  57. package/src/lib/injected-packages.ts +117 -0
  58. package/src/lib/{scripts/store.ts → module-store.ts} +100 -87
  59. package/src/lib/paths.ts +28 -0
  60. package/src/lib/routes.ts +31 -22
  61. package/src/lib/scripts/compiler.ts +88 -70
  62. package/src/lib/scripts/emit.ts +2 -2
  63. package/src/lib/scripts/resolve.ts +85 -28
  64. package/src/lib/scripts/transform.ts +153 -36
  65. package/src/lib/source-maps.ts +8 -1
  66. package/src/lib/styles/compiler.ts +400 -0
  67. package/src/lib/styles/emit.ts +137 -0
  68. package/src/lib/styles/resolve.ts +316 -0
  69. package/src/lib/styles/transform.ts +226 -0
  70. package/src/lib/target.ts +196 -0
  71. package/dist/lib/scripts/store.d.ts +0 -40
  72. package/dist/lib/scripts/store.d.ts.map +0 -1
@@ -1,14 +1,19 @@
1
1
  import * as path from 'node:path'
2
2
  import * as fs from 'node:fs'
3
- import { isAssetServerCompilationError } from './compilation-error.ts'
4
3
  import { createAccessPolicy } from './access.ts'
5
- import { createModuleCompiler, createResponseForModule } from './scripts/compiler.ts'
6
- import { normalizeFilePath } from './paths.ts'
4
+ import { isAssetServerCompilationError } from './compilation-error.ts'
5
+ import { getFingerprintRequestCacheControl, parseFingerprintSuffix } from './fingerprint.ts'
6
+ import { getInjectedPackageRouteConfigs } from './injected-packages.ts'
7
+ import { normalizeFilePath, normalizePathname } from './paths.ts'
7
8
  import { compileRoutes } from './routes.ts'
8
9
  import type { CompiledRoutes } from './routes.ts'
10
+ import { createResponseForScript, createScriptCompiler } from './scripts/compiler.ts'
11
+ import { supportedScriptExtensions } from './scripts/resolve.ts'
12
+ import { createResponseForStyle, createStyleCompiler, isStyleFilePath } from './styles/compiler.ts'
13
+ import { resolveScriptTarget, resolveStyleTarget } from './target.ts'
14
+ import type { AssetTarget, ResolvedScriptTarget, ResolvedStyleTarget } from './target.ts'
9
15
  import { createAssetServerWatcher } from './watch.ts'
10
- import type { ChokidarWatcher } from './watch.ts'
11
- import type { AssetServerWatcher } from './watch.ts'
16
+ import type { AssetServerWatcher, ChokidarWatcher } from './watch.ts'
12
17
 
13
18
  interface AssetServerWatchOptions {
14
19
  /**
@@ -28,33 +33,31 @@ interface AssetServerWatchOptions {
28
33
 
29
34
  interface FingerprintOptions {
30
35
  /**
31
- * Per-build invalidation token that must change whenever fingerprinted module URLs
36
+ * Per-build invalidation token that must change whenever fingerprinted asset URLs
32
37
  * should be invalidated together.
33
38
  */
34
39
  buildId: string
35
40
  }
36
41
 
37
- const scriptTargets = [
38
- 'es2015',
39
- 'es2016',
40
- 'es2017',
41
- 'es2018',
42
- 'es2019',
43
- 'es2020',
44
- 'es2021',
45
- 'es2022',
46
- 'es2023',
47
- 'es2024',
48
- 'es2025',
49
- 'es2026',
50
- 'esnext',
51
- ] as const
52
- const scriptTargetSet = new Set<string>(scriptTargets)
53
-
54
- export type ScriptsTarget = (typeof scriptTargets)[number]
42
+ type AssetSourceMaps = 'inline' | 'external'
43
+ type AssetSourceMapSourcePaths = 'url' | 'absolute'
44
+
45
+ interface AssetServerScriptOptions {
46
+ /**
47
+ * Replace global expressions with constant values during transform, e.g.
48
+ * `{ 'process.env.NODE_ENV': '"production"' }`
49
+ */
50
+ define?: Record<string, string>
51
+ /** Import specifiers to leave unrewritten (CDN URLs, import map entries, etc.) */
52
+ external?: string[]
53
+ }
54
+
55
+ const scriptExtensionSet = new Set<string>(supportedScriptExtensions)
55
56
 
56
57
  export interface AssetServerOptions {
57
- /** File patterns keyed by public URL patterns. */
58
+ /** Public mount path for this asset server, e.g. `'/assets'`. */
59
+ basePath: string
60
+ /** File patterns keyed by public URL patterns relative to `basePath`. */
58
61
  fileMap: Readonly<Record<string, string>>
59
62
  /**
60
63
  * Root directory used to resolve relative file paths. Defaults to `process.cwd()`.
@@ -69,45 +72,37 @@ export interface AssetServerOptions {
69
72
  */
70
73
  deny?: readonly string[]
71
74
  /**
72
- * Controls optional source-based URL fingerprinting for rewritten import URLs.
75
+ * Controls optional source-based URL fingerprinting for rewritten asset URLs.
73
76
  *
74
- * When omitted, all served modules use stable non-fingerprinted URLs with `Cache-Control: no-cache`.
77
+ * When omitted, all served assets use stable non-fingerprinted URLs with `Cache-Control: no-cache`.
75
78
  * Cannot be used together with active watch mode. Set `watch: false` when fingerprinting.
76
79
  */
77
80
  fingerprint?: FingerprintOptions
78
81
  /**
79
- * Script pipeline configuration. Omit to use defaults.
82
+ * Shared compatibility target for scripts and styles. Browser targets apply to both
83
+ * pipelines, and `es` only affects scripts.
80
84
  */
81
- scripts?: {
82
- /**
83
- * Source map mode (disabled when omitted).
84
- * - `'external'`: serve source maps as separate `.map` files; adds `//# sourceMappingURL=` comment
85
- * - `'inline'`: embed source maps as a base64 data URL directly in the JS; no separate `.map` file
86
- */
87
- sourceMaps?: 'inline' | 'external'
88
- /**
89
- * Controls the source paths written into source map `sources`.
90
- * - `'url'` (default): use the stable server path (e.g. `'/assets/app/entry.ts'`)
91
- * - `'absolute'`: use the original filesystem path on disk
92
- */
93
- sourceMapSourcePaths?: 'url' | 'absolute'
94
- /**
95
- * Minify emitted modules.
96
- */
97
- minify?: boolean
98
- /**
99
- * Replace global expressions with constant values during transform, e.g.
100
- * `{ 'process.env.NODE_ENV': '"production"' }`
101
- */
102
- define?: Record<string, string>
103
- /**
104
- * Lower emitted syntax to a specific ECMAScript target. Omit this option to preserve
105
- * modern syntax unless project configuration already requests a lower target.
106
- */
107
- target?: ScriptsTarget
108
- /** Import specifiers to leave unrewritten (CDN URLs, import map entries, etc.) */
109
- external?: string[]
110
- }
85
+ target?: AssetTarget
86
+ /**
87
+ * Source map mode for scripts and styles.
88
+ * - `'external'`: serve source maps as separate `.map` files
89
+ * - `'inline'`: embed source maps as a base64 data URL in the compiled asset
90
+ */
91
+ sourceMaps?: AssetSourceMaps
92
+ /**
93
+ * Source path strategy for source map `sources`.
94
+ * - `'url'` (default): use the stable server path (e.g. `'/assets/app/entry.ts'`)
95
+ * - `'absolute'`: use the original filesystem path on disk
96
+ */
97
+ sourceMapSourcePaths?: AssetSourceMapSourcePaths
98
+ /**
99
+ * Minification setting for emitted scripts and styles.
100
+ */
101
+ minify?: boolean
102
+ /**
103
+ * Script-only configuration.
104
+ */
105
+ scripts?: AssetServerScriptOptions
111
106
  /**
112
107
  * Enable filesystem-backed cache invalidation for long-lived server instances.
113
108
  * Enabled by default. Pass `true` to use the default watcher options, an options
@@ -123,16 +118,16 @@ export interface AssetServerOptions {
123
118
 
124
119
  export interface AssetServer {
125
120
  /**
126
- * Serves a script request. Returns `Response | null` — null means the request was not
127
- * handled by this server, letting the router fall through to a 404.
121
+ * Serves a script or style request. Returns `Response | null` — null means the request
122
+ * was not handled by this server, letting the router fall through to a 404.
128
123
  */
129
124
  fetch(request: Request): Promise<Response | null>
130
125
  /**
131
- * Returns the request href for a served module file.
126
+ * Returns the request href for a served asset file.
132
127
  */
133
128
  getHref(filePath: string): Promise<string>
134
129
  /**
135
- * Returns preload URLs for one or more served module files, ordered shallowest-first.
130
+ * Returns preload URLs for one or more served asset files, ordered shallowest-first.
136
131
  */
137
132
  getPreloads(filePath: string | readonly string[]): Promise<string[]>
138
133
  /**
@@ -143,18 +138,20 @@ export interface AssetServer {
143
138
 
144
139
  type ResolvedAssetServerOptions = {
145
140
  allow: readonly string[]
141
+ basePath: string
146
142
  buildId?: string
147
143
  define?: Record<string, string>
148
144
  deny?: readonly string[]
149
145
  external: string[]
150
- fingerprintModules: boolean
146
+ fingerprintAssets: boolean
151
147
  minify: boolean
152
148
  onError: NonNullable<AssetServerOptions['onError']>
153
149
  rootDir: string
154
150
  routes: CompiledRoutes
155
151
  sourceMapSourcePaths: 'url' | 'absolute'
156
152
  sourceMaps?: 'inline' | 'external'
157
- scriptsTarget?: ScriptsTarget
153
+ scriptsTarget?: ResolvedScriptTarget
154
+ stylesTarget?: ResolvedStyleTarget
158
155
  watchOptions: AssetServerWatchOptions | null
159
156
  }
160
157
 
@@ -172,8 +169,8 @@ export function getInternalWatchTargets(assetServer: AssetServer): readonly stri
172
169
  /**
173
170
  * Create an asset server instance
174
171
  *
175
- * Compiles TypeScript/JavaScript modules on demand with optional source-based URL
176
- * fingerprinting, caching, and configurable file mapping.
172
+ * Compiles TypeScript/JavaScript scripts and CSS styles on demand with optional
173
+ * source-based URL fingerprinting, caching, and configurable file mapping.
177
174
  *
178
175
  * @param options Server configuration
179
176
  * @returns A {@link AssetServer} with `fetch()`, `getHref()`, and `getPreloads()` methods
@@ -181,8 +178,9 @@ export function getInternalWatchTargets(assetServer: AssetServer): readonly stri
181
178
  * @example
182
179
  * ```ts
183
180
  * let assetServer = createAssetServer({
181
+ * basePath: '/assets',
184
182
  * fileMap: {
185
- * '/assets/app/*path': 'app/*path',
183
+ * '/app/*path': 'app/*path',
186
184
  * },
187
185
  * allow: ['app/**'],
188
186
  * })
@@ -199,11 +197,11 @@ export function createAssetServer(options: AssetServerOptions): AssetServer {
199
197
  })
200
198
  let watcher: AssetServerWatcher | null = null
201
199
  let chokidarWatcher: ChokidarWatcher | null = null
202
- let moduleCompiler = createModuleCompiler({
200
+ let scriptCompiler = createScriptCompiler({
203
201
  buildId: resolvedOptions.buildId,
204
202
  define: resolvedOptions.define,
205
203
  external: resolvedOptions.external,
206
- fingerprintModules: resolvedOptions.fingerprintModules,
204
+ fingerprintAssets: resolvedOptions.fingerprintAssets,
207
205
  isAllowed: accessPolicy.isAllowed,
208
206
  minify: resolvedOptions.minify,
209
207
  onWatchDirectoriesChange: (delta) => {
@@ -218,6 +216,22 @@ export function createAssetServer(options: AssetServerOptions): AssetServer {
218
216
  watchIgnore: resolvedOptions.watchOptions?.ignore,
219
217
  watchMode: resolvedOptions.watchOptions !== null,
220
218
  })
219
+ let styleCompiler = createStyleCompiler({
220
+ buildId: resolvedOptions.buildId,
221
+ fingerprintAssets: resolvedOptions.fingerprintAssets,
222
+ isAllowed: accessPolicy.isAllowed,
223
+ minify: resolvedOptions.minify,
224
+ onWatchDirectoriesChange: (delta) => {
225
+ if (!watcher) return
226
+ watcher.updateWatchedDirectories(delta)
227
+ },
228
+ rootDir: resolvedOptions.rootDir,
229
+ routes: resolvedOptions.routes,
230
+ sourceMapSourcePaths: resolvedOptions.sourceMapSourcePaths,
231
+ sourceMaps: resolvedOptions.sourceMaps,
232
+ targets: resolvedOptions.stylesTarget,
233
+ watchIgnore: resolvedOptions.watchOptions?.ignore,
234
+ })
221
235
  if (resolvedOptions.watchOptions) {
222
236
  watcher = createAssetServerWatcher({
223
237
  ...resolvedOptions.watchOptions,
@@ -241,7 +255,8 @@ export function createAssetServer(options: AssetServerOptions): AssetServer {
241
255
  async function handleWatchEvent(filePath: string, event: 'add' | 'change' | 'unlink') {
242
256
  try {
243
257
  let normalizedFilePath = normalizeFilePath(filePath)
244
- await moduleCompiler.handleFileEvent(normalizedFilePath, event)
258
+ await scriptCompiler.handleFileEvent(normalizedFilePath, event)
259
+ await styleCompiler.handleFileEvent(normalizedFilePath, event)
245
260
  } catch (error) {
246
261
  console.error(`There was an error invalidating the asset server cache: ${error}`)
247
262
  }
@@ -251,30 +266,66 @@ export function createAssetServer(options: AssetServerOptions): AssetServer {
251
266
  async fetch(request) {
252
267
  if (request.method !== 'GET' && request.method !== 'HEAD') return null
253
268
 
254
- let parsedRequestPathname = moduleCompiler.parseRequestPathname(new URL(request.url).pathname)
269
+ let parsedRequestPathname = parseAssetRequestPathname(new URL(request.url).pathname, {
270
+ fingerprintAssets: resolvedOptions.fingerprintAssets,
271
+ routes: resolvedOptions.routes,
272
+ })
255
273
  if (!parsedRequestPathname) return null
256
274
 
257
275
  try {
258
276
  let ifNoneMatch = request.headers.get('If-None-Match')
259
- let moduleResult = await moduleCompiler.getModule(parsedRequestPathname.filePath, {
277
+
278
+ if (isStyleFilePath(parsedRequestPathname.filePath)) {
279
+ let styleResult = await styleCompiler.getStyle(parsedRequestPathname.filePath, {
280
+ ifNoneMatch,
281
+ isSourceMapRequest: parsedRequestPathname.isSourceMapRequest,
282
+ requestedFingerprint: parsedRequestPathname.requestedFingerprint,
283
+ })
284
+ if (styleResult.type === 'not-modified') {
285
+ return new Response(null, {
286
+ status: 304,
287
+ headers: { ETag: styleResult.etag },
288
+ })
289
+ }
290
+
291
+ let compiledStyle = styleResult.style
292
+
293
+ if (parsedRequestPathname.requestedFingerprint !== null) {
294
+ if (compiledStyle.fingerprint !== parsedRequestPathname.requestedFingerprint)
295
+ return null
296
+ }
297
+
298
+ return createResponseForStyle(compiledStyle, {
299
+ cacheControl: parsedRequestPathname.cacheControl,
300
+ ifNoneMatch,
301
+ isSourceMapRequest: parsedRequestPathname.isSourceMapRequest,
302
+ method: request.method,
303
+ })
304
+ }
305
+
306
+ if (!isScriptFilePath(parsedRequestPathname.filePath)) {
307
+ return null
308
+ }
309
+
310
+ let scriptResult = await scriptCompiler.getScript(parsedRequestPathname.filePath, {
260
311
  ifNoneMatch,
261
312
  isSourceMapRequest: parsedRequestPathname.isSourceMapRequest,
262
313
  requestedFingerprint: parsedRequestPathname.requestedFingerprint,
263
314
  })
264
- if (moduleResult.type === 'not-modified') {
315
+ if (scriptResult.type === 'not-modified') {
265
316
  return new Response(null, {
266
317
  status: 304,
267
- headers: { ETag: moduleResult.etag },
318
+ headers: { ETag: scriptResult.etag },
268
319
  })
269
320
  }
270
321
 
271
- let compiledModule = moduleResult.module
322
+ let compiledScript = scriptResult.script
272
323
 
273
324
  if (parsedRequestPathname.requestedFingerprint !== null) {
274
- if (compiledModule.fingerprint !== parsedRequestPathname.requestedFingerprint) return null
325
+ if (compiledScript.fingerprint !== parsedRequestPathname.requestedFingerprint) return null
275
326
  }
276
327
 
277
- return createResponseForModule(compiledModule, {
328
+ return createResponseForScript(compiledScript, {
278
329
  cacheControl: parsedRequestPathname.cacheControl,
279
330
  ifNoneMatch,
280
331
  isSourceMapRequest: parsedRequestPathname.isSourceMapRequest,
@@ -286,7 +337,7 @@ export function createAssetServer(options: AssetServerOptions): AssetServer {
286
337
  // handled here" so the outer router can continue to its own 404 behavior.
287
338
  if (
288
339
  isAssetServerCompilationError(error) &&
289
- (error.code === 'MODULE_NOT_FOUND' || error.code === 'MODULE_NOT_ALLOWED')
340
+ (error.code === 'FILE_NOT_FOUND' || error.code === 'FILE_NOT_ALLOWED')
290
341
  ) {
291
342
  return null
292
343
  }
@@ -296,10 +347,46 @@ export function createAssetServer(options: AssetServerOptions): AssetServer {
296
347
  },
297
348
 
298
349
  async getHref(filePath) {
299
- return moduleCompiler.getHref(filePath)
350
+ if (isStyleFilePath(filePath)) {
351
+ return styleCompiler.getHref(filePath)
352
+ }
353
+
354
+ return scriptCompiler.getHref(filePath)
300
355
  },
301
356
  async getPreloads(filePath) {
302
- return moduleCompiler.getPreloadUrls(filePath)
357
+ let filePaths = Array.isArray(filePath) ? filePath : [filePath]
358
+ let styleFiles: string[] = []
359
+ let scriptFiles: string[] = []
360
+
361
+ for (let nextFilePath of filePaths) {
362
+ if (isStyleFilePath(nextFilePath)) {
363
+ styleFiles.push(nextFilePath)
364
+ continue
365
+ }
366
+
367
+ scriptFiles.push(nextFilePath)
368
+ }
369
+
370
+ if (styleFiles.length === 0 && scriptFiles.length === 0) {
371
+ return []
372
+ }
373
+
374
+ if (styleFiles.length === 0) {
375
+ return flattenPreloadLayers(await scriptCompiler.getPreloadLayers(filePath))
376
+ }
377
+
378
+ if (scriptFiles.length === 0) {
379
+ return flattenPreloadLayers(await styleCompiler.getPreloadLayers(filePath))
380
+ }
381
+
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]
388
+
389
+ return mergePreloadLayers(await Promise.all(preloadLayerGroups))
303
390
  },
304
391
  async close() {
305
392
  await watcher?.close()
@@ -323,12 +410,35 @@ function internalServerError(): Response {
323
410
  })
324
411
  }
325
412
 
413
+ function mergePreloadLayers(preloadLayersByRoot: readonly (readonly string[][])[]): string[] {
414
+ let urls: string[] = []
415
+ let seen = new Set<string>()
416
+ let maxDepth = Math.max(0, ...preloadLayersByRoot.map((layers) => layers.length))
417
+
418
+ for (let depth = 0; depth < maxDepth; depth++) {
419
+ for (let preloadLayers of preloadLayersByRoot) {
420
+ for (let url of preloadLayers[depth] ?? []) {
421
+ if (seen.has(url)) continue
422
+ seen.add(url)
423
+ urls.push(url)
424
+ }
425
+ }
426
+ }
427
+
428
+ return urls
429
+ }
430
+
431
+ function flattenPreloadLayers(preloadLayers: readonly (readonly string[])[]): string[] {
432
+ return preloadLayers.flatMap((layer) => layer)
433
+ }
434
+
326
435
  function defaultErrorHandler(error: unknown): void {
327
436
  console.error(error)
328
437
  }
329
438
 
330
439
  function resolveAssetServerOptions(options: AssetServerOptions): ResolvedAssetServerOptions {
331
440
  let rootDir = normalizeFilePath(fs.realpathSync(path.resolve(options.rootDir ?? process.cwd())))
441
+ let basePath = normalizeBasePath(options.basePath)
332
442
  let scriptOptions = options.scripts ?? {}
333
443
  let fingerprintOptions = normalizeFingerprintOptions({
334
444
  fingerprint: options.fingerprint,
@@ -337,37 +447,36 @@ function resolveAssetServerOptions(options: AssetServerOptions): ResolvedAssetSe
337
447
 
338
448
  return {
339
449
  allow: options.allow,
450
+ basePath,
340
451
  buildId: fingerprintOptions.buildId,
341
452
  define: scriptOptions.define,
342
453
  deny: options.deny,
343
454
  external: scriptOptions.external ?? [],
344
- fingerprintModules: fingerprintOptions.enabled,
345
- minify: scriptOptions.minify ?? false,
455
+ fingerprintAssets: fingerprintOptions.enabled,
456
+ minify: options.minify ?? false,
346
457
  onError: options.onError ?? defaultErrorHandler,
347
458
  rootDir,
348
- routes: compileRoutes({
349
- fileMap: options.fileMap,
350
- rootDir,
351
- }),
352
- sourceMapSourcePaths: scriptOptions.sourceMapSourcePaths ?? 'url',
353
- sourceMaps: scriptOptions.sourceMaps,
354
- scriptsTarget: normalizeTarget(scriptOptions.target),
459
+ routes: compileRoutes(basePath, [
460
+ {
461
+ fileMap: options.fileMap,
462
+ rootDir,
463
+ },
464
+ ...getInjectedPackageRouteConfigs(),
465
+ ]),
466
+ sourceMapSourcePaths: options.sourceMapSourcePaths ?? 'url',
467
+ sourceMaps: options.sourceMaps,
468
+ scriptsTarget: resolveScriptTarget(options.target),
469
+ stylesTarget: resolveStyleTarget(options.target),
355
470
  watchOptions: normalizeWatchOptions(options.watch),
356
471
  }
357
472
  }
358
473
 
359
- function normalizeTarget(
360
- target: NonNullable<AssetServerOptions['scripts']>['target'],
361
- ): ScriptsTarget | undefined {
362
- if (target == null) return undefined
363
-
364
- if (typeof target !== 'string' || !scriptTargetSet.has(target)) {
365
- throw new TypeError(
366
- `Expected target to be one of ${scriptTargets.map((value) => `"${value}"`).join(', ')}. Received "${target}".`,
367
- )
474
+ function normalizeBasePath(basePath: string): string {
475
+ if (typeof basePath !== 'string') {
476
+ throw new TypeError('basePath must be a string')
368
477
  }
369
478
 
370
- return target as ScriptsTarget
479
+ return normalizePathname(basePath || '/').replace(/\/+$/, '') || '/'
371
480
  }
372
481
 
373
482
  function normalizeFingerprintOptions(options: {
@@ -413,3 +522,34 @@ function normalizeWatchOptions(
413
522
  if (options == null || options === true) return {}
414
523
  return options
415
524
  }
525
+
526
+ function parseAssetRequestPathname(
527
+ pathname: string,
528
+ options: {
529
+ fingerprintAssets: boolean
530
+ routes: CompiledRoutes
531
+ },
532
+ ): {
533
+ cacheControl: string
534
+ filePath: string
535
+ isSourceMapRequest: boolean
536
+ requestedFingerprint: string | null
537
+ } | null {
538
+ let isSourceMapRequest = pathname.endsWith('.map')
539
+ let pathWithoutMap = isSourceMapRequest ? pathname.slice(0, -4) : pathname
540
+ let fingerprint = parseFingerprintSuffix(pathWithoutMap)
541
+ let filePath = options.routes.resolveUrlPathname(fingerprint.pathname)
542
+ if (!filePath) return null
543
+ if (options.fingerprintAssets && fingerprint.requestedFingerprint === null) return null
544
+
545
+ return {
546
+ cacheControl: getFingerprintRequestCacheControl(fingerprint.requestedFingerprint),
547
+ filePath,
548
+ isSourceMapRequest,
549
+ requestedFingerprint: fingerprint.requestedFingerprint,
550
+ }
551
+ }
552
+
553
+ function isScriptFilePath(filePath: string): boolean {
554
+ return scriptExtensionSet.has(path.extname(filePath).toLowerCase())
555
+ }
@@ -1,17 +1,17 @@
1
1
  type AssetServerCompilationErrorCode =
2
- | 'MODULE_NOT_FOUND'
3
- | 'MODULE_NOT_ALLOWED'
4
- | 'MODULE_OUTSIDE_FILE_MAP'
5
- | 'MODULE_COMMONJS_NOT_SUPPORTED'
6
- | 'MODULE_TRANSFORM_FAILED'
7
- | 'MODULE_EMIT_FAILED'
2
+ | 'FILE_NOT_FOUND'
3
+ | 'FILE_NOT_ALLOWED'
4
+ | 'FILE_OUTSIDE_FILE_MAP'
5
+ | 'COMMONJS_NOT_SUPPORTED'
6
+ | 'TRANSFORM_FAILED'
7
+ | 'EMIT_FAILED'
8
8
  | 'IMPORT_RESOLUTION_FAILED'
9
9
  | 'IMPORT_NOT_SUPPORTED'
10
10
  | 'IMPORT_NOT_ALLOWED'
11
11
  | 'IMPORT_OUTSIDE_FILE_MAP'
12
12
 
13
13
  /**
14
- * Internal error used by the request-time module compilation pipeline.
14
+ * Internal error used by the request-time asset compilation pipeline.
15
15
  */
16
16
  export class AssetServerCompilationError extends Error {
17
17
  code: AssetServerCompilationErrorCode
@@ -1,5 +1,5 @@
1
1
  import * as fs from 'node:fs'
2
- import * as path from 'node:path'
2
+ import picomatch from 'picomatch'
3
3
 
4
4
  import { normalizeFilePath, resolveFilePath } from './paths.ts'
5
5
 
@@ -37,7 +37,8 @@ export function createFileMatcher(
37
37
  return (filePath) => filePath === resolvedPatternPath
38
38
  }
39
39
 
40
- return (filePath) => path.posix.matchesGlob(filePath, resolvedPatternPath)
40
+ let globMatcher = picomatch(resolvedPatternPath, { dot: true })
41
+ return (filePath) => globMatcher(filePath)
41
42
  }
42
43
 
43
44
  function isSameOrDescendantPath(filePath: string, directoryPath: string): boolean {
@@ -0,0 +1,117 @@
1
+ import * as fs from 'node:fs'
2
+ import { fileURLToPath } from 'node:url'
3
+
4
+ import { getFilePathDirectory, normalizeFilePath } from './paths.ts'
5
+
6
+ type ResolvedInjectedPackage = {
7
+ packageJsonPath: string
8
+ packageRoot: string
9
+ }
10
+
11
+ const injectedPackageNames = ['@oxc-project/runtime'] as const
12
+ const injectedPackagesBasePath = '/__@remix/injected'
13
+
14
+ const resolvedInjectedPackages = new Map<string, ResolvedInjectedPackage>()
15
+
16
+ export function isInjectedPackageFilePath(filePath: string): boolean {
17
+ let normalizedFilePath = normalizeFilePath(filePath)
18
+
19
+ for (let packageName of injectedPackageNames) {
20
+ let packageRoot = getResolvedInjectedPackage(packageName).packageRoot
21
+ if (normalizedFilePath === packageRoot || normalizedFilePath.startsWith(`${packageRoot}/`)) {
22
+ return true
23
+ }
24
+ }
25
+
26
+ return false
27
+ }
28
+
29
+ export function getInjectedPackageRouteConfigs(): {
30
+ fileMap: Record<string, string>
31
+ rootDir: string
32
+ }[] {
33
+ return injectedPackageNames.map((packageName) => {
34
+ let { packageRoot } = getResolvedInjectedPackage(packageName)
35
+
36
+ return {
37
+ fileMap: {
38
+ [getInjectedPackageRoutePattern(packageName)]: `${packageName}/*path`,
39
+ },
40
+ rootDir: getInjectedPackageRouteRoot(packageRoot, packageName),
41
+ }
42
+ })
43
+ }
44
+
45
+ export function getInjectedPackageNameForSpecifier(specifier: string): string | null {
46
+ for (let packageName of injectedPackageNames) {
47
+ if (specifier === packageName || specifier.startsWith(`${packageName}/`)) {
48
+ return packageName
49
+ }
50
+ }
51
+
52
+ return null
53
+ }
54
+
55
+ export function mayContainInjectedPackageSpecifier(sourceText: string): boolean {
56
+ return injectedPackageNames.some((packageName) => sourceText.includes(packageName))
57
+ }
58
+
59
+ export function maskAuthoredInjectedPackageSpecifier(specifier: string): string | null {
60
+ let packageName = getInjectedPackageNameForSpecifier(specifier)
61
+ if (!packageName) return null
62
+
63
+ let maskedPackageName = getMaskedInjectedPackageName(packageName)
64
+ return `${maskedPackageName}${specifier.slice(packageName.length)}`
65
+ }
66
+
67
+ export function restoreAuthoredInjectedPackageSpecifier(specifier: string): string | null {
68
+ for (let packageName of injectedPackageNames) {
69
+ let maskedPackageName = getMaskedInjectedPackageName(packageName)
70
+ if (specifier === maskedPackageName) {
71
+ return packageName
72
+ }
73
+ if (specifier.startsWith(`${maskedPackageName}/`)) {
74
+ return `${packageName}${specifier.slice(maskedPackageName.length)}`
75
+ }
76
+ }
77
+
78
+ return null
79
+ }
80
+
81
+ function getMaskedInjectedPackageName(packageName: string): string {
82
+ return `~${packageName.slice(1)}`
83
+ }
84
+
85
+ export function getInjectedPackageImporterPath(): string {
86
+ return normalizeFilePath(fileURLToPath(import.meta.url))
87
+ }
88
+
89
+ function getResolvedInjectedPackage(packageName: string): ResolvedInjectedPackage {
90
+ let existing = resolvedInjectedPackages.get(packageName)
91
+ if (existing) return existing
92
+
93
+ let packageJsonUrl = import.meta.resolve(`${packageName}/package.json`)
94
+ let packageJsonPath = normalizeFilePath(fs.realpathSync(fileURLToPath(packageJsonUrl)))
95
+
96
+ let resolvedInjectedPackage = {
97
+ packageJsonPath,
98
+ packageRoot: normalizeFilePath(fs.realpathSync(getFilePathDirectory(packageJsonPath))),
99
+ }
100
+
101
+ resolvedInjectedPackages.set(packageName, resolvedInjectedPackage)
102
+ return resolvedInjectedPackage
103
+ }
104
+
105
+ function getInjectedPackageRoutePattern(packageName: string): string {
106
+ return `${injectedPackagesBasePath}/${packageName}/*path`
107
+ }
108
+
109
+ function getInjectedPackageRouteRoot(packageRoot: string, packageName: string): string {
110
+ let routeRoot = packageRoot
111
+
112
+ for (let _segment of packageName.split('/')) {
113
+ routeRoot = getFilePathDirectory(routeRoot)
114
+ }
115
+
116
+ return routeRoot
117
+ }