@remix-run/assets 0.5.0 → 0.7.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 (84) hide show
  1. package/README.md +185 -104
  2. package/dist/assets.d.ts +4 -1
  3. package/dist/assets.d.ts.map +1 -1
  4. package/dist/lib/access.d.ts +29 -2
  5. package/dist/lib/access.d.ts.map +1 -1
  6. package/dist/lib/access.js +52 -26
  7. package/dist/lib/asset-server.d.ts +61 -25
  8. package/dist/lib/asset-server.d.ts.map +1 -1
  9. package/dist/lib/asset-server.js +127 -55
  10. package/dist/lib/compilation-error.d.ts +1 -1
  11. package/dist/lib/compilation-error.d.ts.map +1 -1
  12. package/dist/lib/files/compiler.d.ts +1 -1
  13. package/dist/lib/files/compiler.d.ts.map +1 -1
  14. package/dist/lib/files/compiler.js +28 -31
  15. package/dist/lib/files/config.d.ts +6 -0
  16. package/dist/lib/files/config.d.ts.map +1 -1
  17. package/dist/lib/files/config.js +9 -0
  18. package/dist/lib/fingerprint.d.ts +0 -4
  19. package/dist/lib/fingerprint.d.ts.map +1 -1
  20. package/dist/lib/fingerprint.js +0 -4
  21. package/dist/lib/hmr.d.ts +7 -0
  22. package/dist/lib/hmr.d.ts.map +1 -1
  23. package/dist/lib/hmr.js +198 -25
  24. package/dist/lib/injected-packages.d.ts +3 -2
  25. package/dist/lib/injected-packages.d.ts.map +1 -1
  26. package/dist/lib/injected-packages.js +11 -8
  27. package/dist/lib/inspection.d.ts +39 -0
  28. package/dist/lib/inspection.d.ts.map +1 -0
  29. package/dist/lib/inspection.js +160 -0
  30. package/dist/lib/routes.d.ts +12 -3
  31. package/dist/lib/routes.d.ts.map +1 -1
  32. package/dist/lib/routes.js +124 -80
  33. package/dist/lib/scripts/compiler.d.ts +8 -2
  34. package/dist/lib/scripts/compiler.d.ts.map +1 -1
  35. package/dist/lib/scripts/compiler.js +188 -38
  36. package/dist/lib/scripts/emit.d.ts +2 -1
  37. package/dist/lib/scripts/emit.d.ts.map +1 -1
  38. package/dist/lib/scripts/emit.js +25 -16
  39. package/dist/lib/scripts/resolve.d.ts +7 -1
  40. package/dist/lib/scripts/resolve.d.ts.map +1 -1
  41. package/dist/lib/scripts/resolve.js +119 -12
  42. package/dist/lib/scripts/specifiers.d.ts +2 -0
  43. package/dist/lib/scripts/specifiers.d.ts.map +1 -0
  44. package/dist/lib/scripts/specifiers.js +9 -0
  45. package/dist/lib/scripts/transform.d.ts +2 -3
  46. package/dist/lib/scripts/transform.d.ts.map +1 -1
  47. package/dist/lib/scripts/transform.js +4 -18
  48. package/dist/lib/styles/compiler.d.ts +0 -1
  49. package/dist/lib/styles/compiler.d.ts.map +1 -1
  50. package/dist/lib/styles/compiler.js +105 -25
  51. package/dist/lib/styles/emit.d.ts +2 -1
  52. package/dist/lib/styles/emit.d.ts.map +1 -1
  53. package/dist/lib/styles/emit.js +12 -10
  54. package/dist/lib/styles/resolve.d.ts +0 -1
  55. package/dist/lib/styles/resolve.d.ts.map +1 -1
  56. package/dist/lib/styles/resolve.js +8 -9
  57. package/dist/lib/styles/transform.d.ts +0 -2
  58. package/dist/lib/styles/transform.d.ts.map +1 -1
  59. package/dist/lib/styles/transform.js +2 -9
  60. package/dist/lib/virtual-store.d.ts +8 -0
  61. package/dist/lib/virtual-store.d.ts.map +1 -0
  62. package/dist/lib/virtual-store.js +100 -0
  63. package/package.json +6 -6
  64. package/src/assets.ts +9 -1
  65. package/src/lib/access.ts +83 -30
  66. package/src/lib/asset-server.ts +224 -94
  67. package/src/lib/compilation-error.ts +4 -3
  68. package/src/lib/files/compiler.ts +32 -42
  69. package/src/lib/files/config.ts +17 -0
  70. package/src/lib/fingerprint.ts +0 -9
  71. package/src/lib/hmr.ts +210 -26
  72. package/src/lib/injected-packages.ts +16 -10
  73. package/src/lib/inspection.ts +229 -0
  74. package/src/lib/routes.ts +158 -126
  75. package/src/lib/scripts/compiler.ts +240 -54
  76. package/src/lib/scripts/emit.ts +34 -19
  77. package/src/lib/scripts/resolve.ts +175 -13
  78. package/src/lib/scripts/specifiers.ts +11 -0
  79. package/src/lib/scripts/transform.ts +6 -25
  80. package/src/lib/styles/compiler.ts +137 -39
  81. package/src/lib/styles/emit.ts +18 -13
  82. package/src/lib/styles/resolve.ts +8 -10
  83. package/src/lib/styles/transform.ts +2 -12
  84. package/src/lib/virtual-store.ts +124 -0
@@ -2,6 +2,7 @@ import * as os from 'node:os'
2
2
  import * as path from 'node:path'
3
3
  import { fileURLToPath } from 'node:url'
4
4
  import { IfNoneMatch } from '@remix-run/headers/if-none-match'
5
+ import { createAssetServerCompilationError } from '../compilation-error.ts'
5
6
  import { createFileMatcher } from '../file-matcher.ts'
6
7
  import { formatFingerprintedPathname } from '../fingerprint.ts'
7
8
  import { createModuleStore } from '../module-store.ts'
@@ -18,6 +19,11 @@ import type { TransformArgs, TransformedStyle } from './transform.ts'
18
19
 
19
20
  type StyleRecord = ModuleRecord<TransformedStyle, ResolvedStyle, EmittedStyle>
20
21
  type StyleStore = ModuleStore<TransformedStyle, ResolvedStyle, EmittedStyle>
22
+ type ResolvedStyleGraphEntry = {
23
+ invalidationVersion: number
24
+ resolvedStyle: ResolvedStyle
25
+ }
26
+ type ResolvedStyleGraph = ReadonlyMap<string, ResolvedStyleGraphEntry>
21
27
 
22
28
  type StyleCompileResult = {
23
29
  code: EmittedAsset
@@ -42,7 +48,6 @@ type StyleGetOptions = {
42
48
  }
43
49
 
44
50
  type StyleCompilerOptions = {
45
- buildId?: string
46
51
  fingerprintAssets: boolean
47
52
  getServedFileUrl?(
48
53
  identityPath: string,
@@ -109,7 +114,6 @@ export function createStyleCompiler(options: StyleCompilerOptions): StyleCompile
109
114
  routes: resolvedOptions.routes,
110
115
  }
111
116
  let transformArgs: TransformArgs = {
112
- buildId: resolvedOptions.buildId ?? null,
113
117
  isWatchIgnored,
114
118
  minify: resolvedOptions.minify,
115
119
  routes: resolvedOptions.routes,
@@ -121,7 +125,8 @@ export function createStyleCompiler(options: StyleCompilerOptions): StyleCompile
121
125
  return {
122
126
  async getHref(filePath) {
123
127
  let resolvedStyle = resolveServedStyleOrThrow(resolveInputFilePath(filePath), resolveArgs)
124
- return getServedUrl(resolvedStyle.identityPath)
128
+ let graph = await resolveStyleGraph(resolvedStyle.identityPath)
129
+ return getServedUrlFromGraph(resolvedStyle.identityPath, graph)
125
130
  },
126
131
 
127
132
  async getPreloadLayers(filePath) {
@@ -139,17 +144,18 @@ export function createStyleCompiler(options: StyleCompilerOptions): StyleCompile
139
144
  let visited = new Set(resolvedEntries)
140
145
  let queue = [...resolvedEntries]
141
146
  let layers: string[][] = []
147
+ let graph = await resolveStyleGraph(resolvedEntries)
142
148
 
143
149
  while (queue.length > 0) {
144
150
  let frontier = queue
145
151
  queue = []
146
- let resolvedStyles = await getOrCreateResolvedStyles(
147
- frontier.map((identityPath) => styleStore.get(identityPath)),
148
- )
149
152
  let layer: string[] = []
150
153
 
151
- for (let resolvedStyle of resolvedStyles) {
152
- layer.push(getServedUrlForResolvedStyle(resolvedStyle))
154
+ for (let identityPath of frontier) {
155
+ let graphEntry = graph.get(identityPath)
156
+ if (!graphEntry) throw new Error(`Missing resolved style ${identityPath}`)
157
+ let resolvedStyle = graphEntry.resolvedStyle
158
+ layer.push(await getServedUrlFromGraph(identityPath, graph))
153
159
 
154
160
  for (let dep of resolvedStyle.deps) {
155
161
  if (visited.has(dep)) continue
@@ -220,12 +226,6 @@ export function createStyleCompiler(options: StyleCompilerOptions): StyleCompile
220
226
  return resolveFilePath(resolvedOptions.rootDir, filePath)
221
227
  }
222
228
 
223
- async function getOrCreateResolvedStyles(records: StyleRecord[]): Promise<ResolvedStyle[]> {
224
- return mapWithConcurrency(records, preloadConcurrency, (record) =>
225
- getOrCreateResolvedStyle(record),
226
- )
227
- }
228
-
229
229
  async function getOrCreateResolvedStyle(record: StyleRecord): Promise<ResolvedStyle> {
230
230
  if (record.resolved && styleStore.isResolvedFresh(record)) return record.resolved
231
231
 
@@ -296,16 +296,39 @@ export function createStyleCompiler(options: StyleCompilerOptions): StyleCompile
296
296
  return record.emitted
297
297
  }
298
298
 
299
- let cacheKey = getRecordCacheKey(record)
299
+ let graph = await resolveStyleGraph(record.identityPath)
300
+ return getOrCreateEmittedStyleFromGraph(record, graph)
301
+ }
302
+
303
+ async function getOrCreateEmittedStyleFromGraph(
304
+ record: StyleRecord,
305
+ graph: ResolvedStyleGraph,
306
+ ): Promise<EmittedStyle> {
307
+ let graphEntry = graph.get(record.identityPath)
308
+ if (!graphEntry) {
309
+ throw new Error(`Missing resolved style ${record.identityPath}`)
310
+ }
311
+
312
+ if (
313
+ record.invalidationVersion === graphEntry.invalidationVersion &&
314
+ record.emitted &&
315
+ styleStore.isEmittedFresh(record) &&
316
+ !hasHmrTimestampedDependency(record.resolved)
317
+ ) {
318
+ return record.emitted
319
+ }
320
+
321
+ let cacheKey = getCacheKey(record.identityPath, graphEntry.invalidationVersion)
300
322
  let existing = emitInFlightByCacheKey.get(cacheKey)
301
323
  if (existing) return existing
302
324
 
303
325
  let promise = (async () => {
304
- let startedVersion = record.invalidationVersion
305
- let resolvedStyle = await getOrCreateResolvedStyle(record)
306
- let emitResolvedStyleResult = await emitResolvedStyle(resolvedStyle, {
326
+ let emitResolvedStyleResult = await emitResolvedStyle(graphEntry.resolvedStyle, {
327
+ fingerprintAssets: resolvedOptions.fingerprintAssets,
307
328
  getServedFileUrl: resolvedOptions.getServedFileUrl,
308
- getServedUrl,
329
+ getServedUrl(identityPath) {
330
+ return getServedUrlFromGraph(identityPath, graph)
331
+ },
309
332
  sourceMaps: resolvedOptions.sourceMaps,
310
333
  })
311
334
 
@@ -313,7 +336,7 @@ export function createStyleCompiler(options: StyleCompilerOptions): StyleCompile
313
336
  throw emitResolvedStyleResult.error
314
337
  }
315
338
 
316
- if (isFresh(record, startedVersion)) {
339
+ if (isFresh(record, graphEntry.invalidationVersion)) {
317
340
  styleStore.setEmitted(record.identityPath, emitResolvedStyleResult.value, null)
318
341
  }
319
342
 
@@ -331,21 +354,95 @@ export function createStyleCompiler(options: StyleCompilerOptions): StyleCompile
331
354
  }
332
355
  }
333
356
 
334
- async function getServedUrl(identityPath: string): Promise<string> {
335
- return getServedUrlForResolvedStyle(
336
- await getOrCreateResolvedStyle(styleStore.get(identityPath)),
337
- )
338
- }
357
+ async function getServedUrlFromGraph(
358
+ identityPath: string,
359
+ graph: ResolvedStyleGraph,
360
+ ): Promise<string> {
361
+ let graphEntry = graph.get(identityPath)
362
+ if (!graphEntry) throw new Error(`Missing resolved style ${identityPath}`)
363
+ let pathname = graphEntry.resolvedStyle.stableUrlPathname
364
+
365
+ if (resolvedOptions.fingerprintAssets) {
366
+ let emittedStyle = await getOrCreateEmittedStyleFromGraph(styleStore.get(identityPath), graph)
367
+ pathname = formatFingerprintedPathname(
368
+ graphEntry.resolvedStyle.stableUrlPathname,
369
+ emittedStyle.fingerprint,
370
+ )
371
+ }
339
372
 
340
- function getServedUrlForResolvedStyle(resolvedStyle: ResolvedStyle): string {
341
- let pathname = formatFingerprintedPathname(
342
- resolvedStyle.stableUrlPathname,
343
- resolvedOptions.fingerprintAssets ? resolvedStyle.fingerprint : null,
344
- )
345
- let timestamp = styleStore.getHmrUpdateTimestamp(resolvedStyle.identityPath)
373
+ let timestamp = styleStore.getHmrUpdateTimestamp(graphEntry.resolvedStyle.identityPath)
346
374
  return timestamp ? appendTimestamp(pathname, timestamp) : pathname
347
375
  }
348
376
 
377
+ async function resolveStyleGraph(
378
+ rootPath: string | readonly string[],
379
+ ): Promise<ResolvedStyleGraph> {
380
+ let resolvedByPath = new Map<string, ResolvedStyleGraphEntry>()
381
+ let rootPaths = Array.isArray(rootPath) ? rootPath : [rootPath]
382
+ let discovered = new Set(rootPaths)
383
+ let queue = [...rootPaths]
384
+
385
+ while (queue.length > 0) {
386
+ let frontier = queue
387
+ queue = []
388
+ let graphEntries = await mapWithConcurrency(
389
+ frontier,
390
+ preloadConcurrency,
391
+ async (identityPath): Promise<ResolvedStyleGraphEntry> => {
392
+ let record = styleStore.get(identityPath)
393
+ let invalidationVersion = record.invalidationVersion
394
+ let resolvedStyle = await getOrCreateResolvedStyle(record)
395
+ return {
396
+ invalidationVersion,
397
+ resolvedStyle,
398
+ }
399
+ },
400
+ )
401
+
402
+ for (let [index, identityPath] of frontier.entries()) {
403
+ let graphEntry = graphEntries[index]
404
+ resolvedByPath.set(identityPath, graphEntry)
405
+ for (let depPath of graphEntry.resolvedStyle.deps) {
406
+ if (discovered.has(depPath)) continue
407
+ discovered.add(depPath)
408
+ queue.push(depPath)
409
+ }
410
+ }
411
+ }
412
+
413
+ assertNoCircularImports(resolvedByPath, rootPaths)
414
+ return resolvedByPath
415
+ }
416
+
417
+ function assertNoCircularImports(graph: ResolvedStyleGraph, rootPaths: readonly string[]): void {
418
+ let visited = new Set<string>()
419
+ let path: string[] = []
420
+ let pathIndexByIdentity = new Map<string, number>()
421
+
422
+ function visit(identityPath: string): void {
423
+ let cycleStart = pathIndexByIdentity.get(identityPath)
424
+ if (cycleStart !== undefined) {
425
+ let cycle = [...path.slice(cycleStart), identityPath]
426
+ throw createAssetServerCompilationError(
427
+ `Circular CSS imports are not supported: ${cycle.join(' -> ')}`,
428
+ { code: 'EMIT_FAILED' },
429
+ )
430
+ }
431
+ if (visited.has(identityPath)) return
432
+
433
+ let graphEntry = graph.get(identityPath)
434
+ if (!graphEntry) throw new Error(`Missing resolved style ${identityPath}`)
435
+ pathIndexByIdentity.set(identityPath, path.length)
436
+ path.push(identityPath)
437
+ for (let depPath of graphEntry.resolvedStyle.deps) visit(depPath)
438
+ path.pop()
439
+ pathIndexByIdentity.delete(identityPath)
440
+ visited.add(identityPath)
441
+ }
442
+
443
+ for (let identityPath of rootPaths) visit(identityPath)
444
+ }
445
+
349
446
  function getHmrUpdatePathnames(identityPath: string, timestamp: number): string[] {
350
447
  let resolvedStyles = findHmrUpdateStyles(identityPath, new Set())
351
448
  // Mark update timestamps before invalidating so re-emitted importer stylesheets
@@ -389,7 +486,11 @@ export function createStyleCompiler(options: StyleCompilerOptions): StyleCompile
389
486
  }
390
487
 
391
488
  function getRecordCacheKey(record: StyleRecord): string {
392
- return `${record.identityPath}\0${record.invalidationVersion}`
489
+ return getCacheKey(record.identityPath, record.invalidationVersion)
490
+ }
491
+
492
+ function getCacheKey(identityPath: string, version: number): string {
493
+ return `${identityPath}\0${version}`
393
494
  }
394
495
 
395
496
  function isFresh(record: StyleRecord, version: number): boolean {
@@ -407,16 +508,13 @@ function getNotModifiedStyle(
407
508
  let emittedStyle = record.emitted
408
509
  if (!emittedStyle || options.ifNoneMatch === null) return null
409
510
 
410
- if (
411
- options.requestedFingerprint !== null &&
412
- emittedStyle.fingerprint !== options.requestedFingerprint
413
- ) {
414
- return null
415
- }
416
-
417
511
  let asset = getEmittedAssetForRequest(emittedStyle, options.isSourceMapRequest)
418
512
  if (!asset) return null
419
513
 
514
+ if (options.requestedFingerprint !== null && asset.fingerprint !== options.requestedFingerprint) {
515
+ return null
516
+ }
517
+
420
518
  if (!IfNoneMatch.from(options.ifNoneMatch).matches(asset.etag)) return null
421
519
  return { etag: asset.etag, type: 'not-modified' }
422
520
  }
@@ -4,7 +4,7 @@ import {
4
4
  createAssetServerCompilationError,
5
5
  isAssetServerCompilationError,
6
6
  } from '../compilation-error.ts'
7
- import { hashContent } from '../fingerprint.ts'
7
+ import { formatFingerprintedPathname, hashContent } from '../fingerprint.ts'
8
8
  import { composeSourceMaps } from '../source-maps.ts'
9
9
  import type { AssetServerCompilationError } from '../compilation-error.ts'
10
10
  import type { ResolvedStyle } from './resolve.ts'
@@ -12,12 +12,12 @@ import type { ResolvedStyle } from './resolve.ts'
12
12
  export type EmittedAsset = {
13
13
  content: string
14
14
  etag: string
15
+ fingerprint: string
15
16
  }
16
17
 
17
18
  export type EmittedStyle = {
18
19
  code: EmittedAsset
19
20
  fingerprint: string | null
20
- importUrls: string[]
21
21
  sourceMap: EmittedAsset | null
22
22
  }
23
23
 
@@ -34,6 +34,7 @@ type EmitResult =
34
34
  export async function emitResolvedStyle(
35
35
  resolvedStyle: ResolvedStyle,
36
36
  options: {
37
+ fingerprintAssets: boolean
37
38
  getServedFileUrl?(
38
39
  identityPath: string,
39
40
  options: {
@@ -45,30 +46,32 @@ export async function emitResolvedStyle(
45
46
  },
46
47
  ): Promise<EmitResult> {
47
48
  try {
48
- let importUrls = await Promise.all(
49
- resolvedStyle.deps.map((depPath) => options.getServedUrl(depPath)),
50
- )
51
49
  let rewriteResult = await rewriteDependencies(resolvedStyle, options)
52
50
  let finalCode = rewriteResult.code
51
+ let sourceMap = rewriteResult.sourceMap
52
+ ? await createEmittedAsset(rewriteResult.sourceMap)
53
+ : null
53
54
 
54
55
  if (rewriteResult.sourceMap) {
55
56
  if (options.sourceMaps === 'inline') {
56
57
  let encoded = Buffer.from(rewriteResult.sourceMap).toString('base64')
57
58
  finalCode += `\n/*# sourceMappingURL=data:application/json;base64,${encoded} */`
58
59
  } else if (options.sourceMaps === 'external') {
59
- finalCode += `\n/*# sourceMappingURL=${await options.getServedUrl(resolvedStyle.identityPath)}.map */`
60
+ finalCode += `\n/*# sourceMappingURL=${formatFingerprintedPathname(
61
+ resolvedStyle.stableUrlPathname,
62
+ options.fingerprintAssets && sourceMap ? sourceMap.fingerprint : null,
63
+ )}.map */`
60
64
  }
61
65
  }
62
66
 
67
+ let code = await createEmittedAsset(finalCode)
68
+
63
69
  return {
64
70
  ok: true,
65
71
  value: {
66
- code: await createEmittedAsset(finalCode),
67
- fingerprint: resolvedStyle.fingerprint,
68
- importUrls,
69
- sourceMap: rewriteResult.sourceMap
70
- ? await createEmittedAsset(rewriteResult.sourceMap)
71
- : null,
72
+ code,
73
+ fingerprint: options.fingerprintAssets ? code.fingerprint : null,
74
+ sourceMap,
72
75
  },
73
76
  }
74
77
  } catch (error) {
@@ -192,9 +195,11 @@ async function getServedFileUrl(
192
195
  }
193
196
 
194
197
  async function createEmittedAsset(content: string): Promise<EmittedAsset> {
198
+ let fingerprint = await hashContent(content)
195
199
  return {
196
200
  content,
197
- etag: `W/"${await hashContent(content)}"`,
201
+ etag: `W/"${fingerprint}"`,
202
+ fingerprint,
198
203
  }
199
204
  }
200
205
 
@@ -38,7 +38,6 @@ type ResolvedDependency =
38
38
  export type ResolvedStyle = {
39
39
  dependencies: ResolvedDependency[]
40
40
  deps: string[]
41
- fingerprint: string | null
42
41
  identityPath: string
43
42
  rawCode: string
44
43
  resolvedPath: string
@@ -131,7 +130,6 @@ export async function resolveStyle(
131
130
  value: {
132
131
  dependencies,
133
132
  deps: [...deps],
134
- fingerprint: transformed.fingerprint,
135
133
  identityPath: record.identityPath,
136
134
  rawCode: transformed.rawCode,
137
135
  resolvedPath: transformed.resolvedPath,
@@ -175,9 +173,9 @@ export function resolveServedStyleOrThrow(
175
173
  let stableUrlPathname = args.routes.toUrlPathname(identityPath)
176
174
  if (!stableUrlPathname) {
177
175
  throw createAssetServerCompilationError(
178
- `File ${identityPath} is outside all configured fileMap entries.`,
176
+ `File ${identityPath} is outside all configured mounts.`,
179
177
  {
180
- code: 'FILE_OUTSIDE_FILE_MAP',
178
+ code: 'FILE_OUTSIDE_MOUNTS',
181
179
  },
182
180
  )
183
181
  }
@@ -232,10 +230,10 @@ function resolveImportDependency(
232
230
 
233
231
  if (!args.routes.toUrlPathname(identityPath)) {
234
232
  throw createAssetServerCompilationError(
235
- `Import "${url}" in ${importerPath}, resolved to "${identityPath}", is outside all configured fileMap entries. ` +
236
- `Add a matching fileMap entry for this file path, or mark this import as external.`,
233
+ `Import "${url}" in ${importerPath}, resolved to "${identityPath}", is outside all configured mounts. ` +
234
+ `Add a matching mount for this file path, or mark this import as external.`,
237
235
  {
238
- code: 'IMPORT_OUTSIDE_FILE_MAP',
236
+ code: 'IMPORT_OUTSIDE_MOUNTS',
239
237
  },
240
238
  )
241
239
  }
@@ -302,10 +300,10 @@ function resolveUrlDependency(
302
300
 
303
301
  if (!args.routes.toUrlPathname(identityPath)) {
304
302
  throw createAssetServerCompilationError(
305
- `URL "${url}" in ${importerPath}, resolved to "${identityPath}", is outside all configured fileMap entries. ` +
306
- `Add a matching fileMap entry for this file path.`,
303
+ `URL "${url}" in ${importerPath}, resolved to "${identityPath}", is outside all configured mounts. ` +
304
+ `Add a matching mount for this file path.`,
307
305
  {
308
- code: 'URL_OUTSIDE_FILE_MAP',
306
+ code: 'URL_OUTSIDE_MOUNTS',
309
307
  },
310
308
  )
311
309
  }
@@ -5,7 +5,6 @@ import {
5
5
  createAssetServerCompilationError,
6
6
  isAssetServerCompilationError,
7
7
  } from '../compilation-error.ts'
8
- import { generateFingerprint } from '../fingerprint.ts'
9
8
  import type { ModuleTracking } from '../module-store.ts'
10
9
  import { rewriteSourceMapSources, stringifySourceMap } from '../source-maps.ts'
11
10
  import type { AssetServerCompilationError } from '../compilation-error.ts'
@@ -25,7 +24,6 @@ type TransformedStyleDependency =
25
24
  }
26
25
 
27
26
  export type TransformedStyle = {
28
- fingerprint: string | null
29
27
  identityPath: string
30
28
  rawCode: string
31
29
  resolvedPath: string
@@ -49,7 +47,6 @@ type TransformResult = {
49
47
  )
50
48
 
51
49
  export type TransformArgs = {
52
- buildId: string | null
53
50
  isWatchIgnored(filePath: string): boolean
54
51
  minify: boolean
55
52
  routes: CompiledRoutes
@@ -99,9 +96,9 @@ export async function transformStyle(
99
96
  let stableUrlPathname = args.routes.toUrlPathname(record.identityPath)
100
97
  if (!stableUrlPathname) {
101
98
  throw createAssetServerCompilationError(
102
- `File ${record.identityPath} is outside all configured fileMap entries.`,
99
+ `File ${record.identityPath} is outside all configured mounts.`,
103
100
  {
104
- code: 'FILE_OUTSIDE_FILE_MAP',
101
+ code: 'FILE_OUTSIDE_MOUNTS',
105
102
  },
106
103
  )
107
104
  }
@@ -149,13 +146,6 @@ export async function transformStyle(
149
146
  trackedFiles,
150
147
  },
151
148
  value: {
152
- fingerprint:
153
- args.buildId === null
154
- ? null
155
- : await generateFingerprint({
156
- buildId: args.buildId,
157
- content: sourceText,
158
- }),
159
149
  identityPath: record.identityPath,
160
150
  rawCode: Buffer.from(transformResult.code).toString('utf8'),
161
151
  resolvedPath,
@@ -0,0 +1,124 @@
1
+ import * as fs from 'node:fs'
2
+ import * as path from 'node:path'
3
+
4
+ import {
5
+ getFilePathBaseName,
6
+ getFilePathDirectory,
7
+ normalizeFilePath,
8
+ resolveFilePath,
9
+ } from './paths.ts'
10
+ import { resolveMountFileRoot } from './routes.ts'
11
+
12
+ const modulesStateFileName = '.modules.yaml'
13
+ const virtualStoreBasePath = '/__@remix/virtual-store'
14
+ const virtualStoreDirLineRE = /^virtualStoreDir:[ \t]*(.+)$/m
15
+
16
+ // pnpm's global virtual store links packages from a machine-wide directory, so package realpaths
17
+ // land outside `rootDir` and would otherwise have no public URL. The store location comes from
18
+ // `node_modules/.modules.yaml`, which pnpm rewrites on every install.
19
+ export function getVirtualStoreMountConfigs(options: {
20
+ mounts: Readonly<Record<string, string>>
21
+ rootDir: string
22
+ }): {
23
+ mounts: Record<string, string>
24
+ rootDir: string
25
+ }[] {
26
+ let virtualStoreDir = findVirtualStoreDir(options.rootDir)
27
+ if (virtualStoreDir === null) return []
28
+ // A store inside a mount, such as pnpm's default `node_modules/.pnpm`, already has URLs.
29
+ if (isWithinMounts(virtualStoreDir, options)) return []
30
+
31
+ return [
32
+ {
33
+ mounts: {
34
+ [virtualStoreBasePath]: getFilePathBaseName(virtualStoreDir),
35
+ },
36
+ rootDir: getFilePathDirectory(virtualStoreDir),
37
+ },
38
+ ]
39
+ }
40
+
41
+ function findVirtualStoreDir(rootDir: string): string | null {
42
+ let directory = normalizeFilePath(rootDir)
43
+
44
+ // Workspace members share the workspace root's file, so walk up to the nearest one.
45
+ while (true) {
46
+ let modulesDir = `${directory}/node_modules`
47
+ let modulesState = readModulesState(`${modulesDir}/${modulesStateFileName}`)
48
+ if (modulesState !== null) {
49
+ let virtualStoreDir = readVirtualStoreDir(modulesState)
50
+ if (virtualStoreDir === null) return null
51
+ return realpathDirectory(resolveFilePath(modulesDir, virtualStoreDir))
52
+ }
53
+
54
+ let parentDirectory = normalizeFilePath(path.dirname(directory))
55
+ if (parentDirectory === directory) return null
56
+ directory = parentDirectory
57
+ }
58
+ }
59
+
60
+ function readVirtualStoreDir(modulesState: string): string | null {
61
+ // pnpm 10.34 and later write JSON, earlier versions write YAML, and package managers that
62
+ // mirror the global virtual store write either one. Reading the one scalar this needs keeps
63
+ // both formats working without a YAML parser.
64
+ try {
65
+ let parsedState = JSON.parse(modulesState) as unknown
66
+ if (parsedState === null || typeof parsedState !== 'object') return null
67
+
68
+ let virtualStoreDir = (parsedState as Record<string, unknown>).virtualStoreDir
69
+ return typeof virtualStoreDir === 'string' ? virtualStoreDir : null
70
+ } catch {
71
+ let match = virtualStoreDirLineRE.exec(modulesState)
72
+ return match === null ? null : unquoteScalar(match[1].trim())
73
+ }
74
+ }
75
+
76
+ function unquoteScalar(value: string): string {
77
+ let quote = value[0]
78
+ if (value.length > 1 && (quote === "'" || quote === '"') && value.endsWith(quote)) {
79
+ return value.slice(1, -1)
80
+ }
81
+
82
+ return value
83
+ }
84
+
85
+ function isWithinMounts(
86
+ directory: string,
87
+ options: {
88
+ mounts: Readonly<Record<string, string>>
89
+ rootDir: string
90
+ },
91
+ ): boolean {
92
+ return Object.values(options.mounts).some((fileRoot) => {
93
+ let mountRoot = resolveMountFileRoot(options.rootDir, fileRoot)
94
+ return directory === mountRoot || directory.startsWith(`${mountRoot}/`)
95
+ })
96
+ }
97
+
98
+ function readModulesState(filePath: string): string | null {
99
+ try {
100
+ return fs.readFileSync(filePath, 'utf-8')
101
+ } catch (error) {
102
+ if (isPathNotFoundError(error)) return null
103
+ throw error
104
+ }
105
+ }
106
+
107
+ function realpathDirectory(filePath: string): string | null {
108
+ try {
109
+ let realPath = fs.realpathSync(filePath)
110
+ return fs.statSync(realPath).isDirectory() ? normalizeFilePath(realPath) : null
111
+ } catch (error) {
112
+ if (isPathNotFoundError(error)) return null
113
+ throw error
114
+ }
115
+ }
116
+
117
+ function isPathNotFoundError(error: unknown): boolean {
118
+ return (
119
+ error instanceof Error &&
120
+ 'code' in error &&
121
+ ((error as NodeJS.ErrnoException).code === 'ENOENT' ||
122
+ (error as NodeJS.ErrnoException).code === 'ENOTDIR')
123
+ )
124
+ }