@remix-run/assets 0.6.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 (72) hide show
  1. package/README.md +101 -13
  2. package/dist/assets.d.ts +2 -1
  3. package/dist/assets.d.ts.map +1 -1
  4. package/dist/lib/asset-server.d.ts +41 -19
  5. package/dist/lib/asset-server.d.ts.map +1 -1
  6. package/dist/lib/asset-server.js +98 -41
  7. package/dist/lib/compilation-error.d.ts +1 -1
  8. package/dist/lib/compilation-error.d.ts.map +1 -1
  9. package/dist/lib/files/compiler.d.ts +1 -1
  10. package/dist/lib/files/compiler.d.ts.map +1 -1
  11. package/dist/lib/files/compiler.js +26 -29
  12. package/dist/lib/files/config.d.ts +6 -0
  13. package/dist/lib/files/config.d.ts.map +1 -1
  14. package/dist/lib/files/config.js +9 -0
  15. package/dist/lib/fingerprint.d.ts +0 -4
  16. package/dist/lib/fingerprint.d.ts.map +1 -1
  17. package/dist/lib/fingerprint.js +0 -4
  18. package/dist/lib/hmr.d.ts +7 -0
  19. package/dist/lib/hmr.d.ts.map +1 -1
  20. package/dist/lib/hmr.js +198 -25
  21. package/dist/lib/routes.d.ts +1 -0
  22. package/dist/lib/routes.d.ts.map +1 -1
  23. package/dist/lib/routes.js +1 -1
  24. package/dist/lib/scripts/compiler.d.ts +8 -2
  25. package/dist/lib/scripts/compiler.d.ts.map +1 -1
  26. package/dist/lib/scripts/compiler.js +186 -36
  27. package/dist/lib/scripts/emit.d.ts +2 -1
  28. package/dist/lib/scripts/emit.d.ts.map +1 -1
  29. package/dist/lib/scripts/emit.js +25 -16
  30. package/dist/lib/scripts/resolve.d.ts +7 -1
  31. package/dist/lib/scripts/resolve.d.ts.map +1 -1
  32. package/dist/lib/scripts/resolve.js +110 -3
  33. package/dist/lib/scripts/specifiers.d.ts +2 -0
  34. package/dist/lib/scripts/specifiers.d.ts.map +1 -0
  35. package/dist/lib/scripts/specifiers.js +9 -0
  36. package/dist/lib/scripts/transform.d.ts +2 -3
  37. package/dist/lib/scripts/transform.d.ts.map +1 -1
  38. package/dist/lib/scripts/transform.js +2 -16
  39. package/dist/lib/styles/compiler.d.ts +0 -1
  40. package/dist/lib/styles/compiler.d.ts.map +1 -1
  41. package/dist/lib/styles/compiler.js +105 -25
  42. package/dist/lib/styles/emit.d.ts +2 -1
  43. package/dist/lib/styles/emit.d.ts.map +1 -1
  44. package/dist/lib/styles/emit.js +12 -10
  45. package/dist/lib/styles/resolve.d.ts +0 -1
  46. package/dist/lib/styles/resolve.d.ts.map +1 -1
  47. package/dist/lib/styles/resolve.js +0 -1
  48. package/dist/lib/styles/transform.d.ts +0 -2
  49. package/dist/lib/styles/transform.d.ts.map +1 -1
  50. package/dist/lib/styles/transform.js +0 -7
  51. package/dist/lib/virtual-store.d.ts +8 -0
  52. package/dist/lib/virtual-store.d.ts.map +1 -0
  53. package/dist/lib/virtual-store.js +100 -0
  54. package/package.json +6 -5
  55. package/src/assets.ts +7 -1
  56. package/src/lib/asset-server.ts +175 -73
  57. package/src/lib/compilation-error.ts +1 -0
  58. package/src/lib/files/compiler.ts +30 -40
  59. package/src/lib/files/config.ts +17 -0
  60. package/src/lib/fingerprint.ts +0 -9
  61. package/src/lib/hmr.ts +210 -26
  62. package/src/lib/routes.ts +1 -1
  63. package/src/lib/scripts/compiler.ts +238 -52
  64. package/src/lib/scripts/emit.ts +34 -19
  65. package/src/lib/scripts/resolve.ts +166 -4
  66. package/src/lib/scripts/specifiers.ts +11 -0
  67. package/src/lib/scripts/transform.ts +4 -23
  68. package/src/lib/styles/compiler.ts +137 -39
  69. package/src/lib/styles/emit.ts +18 -13
  70. package/src/lib/styles/resolve.ts +0 -2
  71. package/src/lib/styles/transform.ts +0 -10
  72. package/src/lib/virtual-store.ts +124 -0
@@ -17,6 +17,7 @@ import { normalizeFilePath } from '../paths.ts'
17
17
  import type { CompiledRoutes } from '../routes.ts'
18
18
  import type { ResolveModuleResult, TransformedModule } from './transform.ts'
19
19
  import type { EmittedModule } from './emit.ts'
20
+ import { isBareImportSpecifier } from './specifiers.ts'
20
21
 
21
22
  type ScriptRecord = ModuleRecord<TransformedModule, ResolvedModule, EmittedModule>
22
23
 
@@ -31,14 +32,24 @@ export const supportedScriptExtensions = ['.ts', '.tsx', '.js', '.jsx', '.mts',
31
32
  const supportedScriptExtensionSet = new Set<string>(supportedScriptExtensions)
32
33
 
33
34
  type ResolvedImport = {
35
+ compiledSpecifier: string
34
36
  depPath: string
35
37
  end: number
36
38
  quote?: '"' | "'" | '`'
39
+ scopePathname?: string
40
+ specifier: string
37
41
  start: number
38
42
  }
39
43
 
40
44
  type ResolvedHmrAcceptedDependency = ResolvedImport
41
45
 
46
+ type PendingBareImportScope = {
47
+ imported: ResolvedImport
48
+ resolvedIdentityPath: string
49
+ specifier: string
50
+ trackedResolution: RelativeImportResolution | null
51
+ }
52
+
42
53
  type RelativeImportResolution = {
43
54
  candidatePaths: readonly string[]
44
55
  candidatePrefixes: readonly string[]
@@ -51,7 +62,6 @@ type TrackedResolution = RelativeImportResolution & {
51
62
 
52
63
  export type ResolvedModule = {
53
64
  deps: string[]
54
- fingerprint: string | null
55
65
  hmr: Omit<TransformedModule['hmr'], 'acceptedDeps'> & {
56
66
  acceptedDeps: ResolvedHmrAcceptedDependency[]
57
67
  }
@@ -61,6 +71,7 @@ export type ResolvedModule = {
61
71
  rawCode: string
62
72
  resolvedPath: string
63
73
  sourceMap: string | null
74
+ staticDeps: string[]
64
75
  stableUrlPathname: string
65
76
  }
66
77
 
@@ -78,10 +89,13 @@ type ResolveResult = {
78
89
  )
79
90
 
80
91
  export type ResolveArgs = {
92
+ concurrency: number
93
+ isDirectoryResolutionFileIndependent(directory: string): boolean
81
94
  isAllowed(absolutePath: string): boolean
82
95
  isWatchIgnored(filePath: string): boolean
83
96
  resolveModulePath(absolutePath: string): ResolveModuleResult | null
84
97
  resolverFactory: ResolverFactory
98
+ resolveDirectorySpecifierIdentity(directory: string, specifier: string): Promise<string | null>
85
99
  routes: CompiledRoutes
86
100
  }
87
101
 
@@ -121,8 +135,10 @@ export async function resolveModule(
121
135
  }
122
136
 
123
137
  let importsWithPaths: ResolvedImport[] = []
138
+ let pendingBareImportScopes: PendingBareImportScope[] = []
124
139
  let acceptedDepsWithPaths: ResolvedHmrAcceptedDependency[] = []
125
140
  let deps = new Set<string>()
141
+ let staticDeps = new Set<string>()
126
142
 
127
143
  for (let unresolved of transformed.unresolvedImports) {
128
144
  let displaySpecifier = getDisplayImportSpecifier(unresolved.specifier)
@@ -200,6 +216,7 @@ export async function resolveModule(
200
216
  }
201
217
 
202
218
  deps.add(resolvedImport.identityPath)
219
+ if (!unresolved.dynamic) staticDeps.add(resolvedImport.identityPath)
203
220
 
204
221
  if (transformed.packageSpecifiers.includes(unresolved.specifier)) {
205
222
  let packageJsonPath =
@@ -216,12 +233,50 @@ export async function resolveModule(
216
233
  })
217
234
  }
218
235
 
219
- importsWithPaths.push({
236
+ let imported: ResolvedImport = {
237
+ compiledSpecifier: unresolved.specifier,
220
238
  depPath: resolvedImport.identityPath,
221
239
  end: unresolved.end,
222
240
  quote: unresolved.quote,
241
+ specifier: displaySpecifier,
223
242
  start: unresolved.start,
224
- })
243
+ }
244
+ importsWithPaths.push(imported)
245
+
246
+ if (isBareImportSpecifier(displaySpecifier)) {
247
+ pendingBareImportScopes.push({
248
+ imported,
249
+ resolvedIdentityPath: resolvedImport.identityPath,
250
+ specifier: normalizeSpecifierResolution(unresolved.specifier, transformed.resolvedPath)
251
+ .specifier,
252
+ trackedResolution,
253
+ })
254
+ }
255
+ }
256
+
257
+ if (pendingBareImportScopes.length > 0) {
258
+ let scopeResults = await resolveBareImportScopes(
259
+ pendingBareImportScopes,
260
+ transformed.resolvedPath,
261
+ args,
262
+ )
263
+ for (let index = 0; index < scopeResults.length; index++) {
264
+ let result = scopeResults[index]
265
+ let pending = pendingBareImportScopes[index]
266
+ if (!result.ok) {
267
+ return failResolve(
268
+ result.error,
269
+ trackedFiles,
270
+ trackedResolutions,
271
+ transformed.resolvedPath,
272
+ {
273
+ isWatchIgnored: args.isWatchIgnored,
274
+ trackedResolution: pending.trackedResolution,
275
+ },
276
+ )
277
+ }
278
+ pending.imported.scopePathname = result.scopePathname
279
+ }
225
280
  }
226
281
 
227
282
  for (let unresolved of transformed.hmr.acceptedDeps) {
@@ -324,9 +379,11 @@ export async function resolveModule(
324
379
  }
325
380
 
326
381
  acceptedDepsWithPaths.push({
382
+ compiledSpecifier: unresolved.specifier,
327
383
  depPath: resolvedImport.identityPath,
328
384
  end: unresolved.end,
329
385
  quote: unresolved.quote,
386
+ specifier: displaySpecifier,
330
387
  start: unresolved.start,
331
388
  })
332
389
  }
@@ -336,7 +393,6 @@ export async function resolveModule(
336
393
  tracking: toResolveTracking(trackedFiles, trackedResolutions),
337
394
  value: {
338
395
  deps: [...deps],
339
- fingerprint: transformed.fingerprint,
340
396
  hmr: {
341
397
  acceptedDeps: acceptedDepsWithPaths,
342
398
  selfAccepting: transformed.hmr.selfAccepting,
@@ -348,11 +404,117 @@ export async function resolveModule(
348
404
  rawCode: transformed.rawCode,
349
405
  resolvedPath: transformed.resolvedPath,
350
406
  sourceMap: transformed.sourceMap,
407
+ staticDeps: [...staticDeps],
351
408
  stableUrlPathname: transformed.stableUrlPathname,
352
409
  },
353
410
  }
354
411
  }
355
412
 
413
+ async function resolveBareImportScopes(
414
+ pendingScopes: PendingBareImportScope[],
415
+ importerPath: string,
416
+ args: ResolveArgs,
417
+ ): Promise<
418
+ ({ ok: true; scopePathname: string } | { ok: false; error: AssetServerCompilationError })[]
419
+ > {
420
+ let results = new Array<
421
+ { ok: true; scopePathname: string } | { ok: false; error: AssetServerCompilationError }
422
+ >(pendingScopes.length)
423
+ let nextIndex = 0
424
+
425
+ async function worker(): Promise<void> {
426
+ while (nextIndex < pendingScopes.length) {
427
+ let index = nextIndex++
428
+ let pending = pendingScopes[index]
429
+ try {
430
+ results[index] = {
431
+ ok: true,
432
+ scopePathname: await getBareImportScopePathname({
433
+ importerPath,
434
+ resolvedIdentityPath: pending.resolvedIdentityPath,
435
+ specifier: pending.specifier,
436
+ ...args,
437
+ }),
438
+ }
439
+ } catch (error) {
440
+ results[index] = {
441
+ ok: false,
442
+ error: isAssetServerCompilationError(error)
443
+ ? error
444
+ : createAssetServerCompilationError(
445
+ `Failed to determine an import map scope for "${pending.imported.specifier}" in ${importerPath}. ${formatUnknownError(error)}`,
446
+ { cause: error, code: 'IMPORT_RESOLUTION_FAILED' },
447
+ ),
448
+ }
449
+ }
450
+ }
451
+ }
452
+
453
+ if (pendingScopes.length === 1) {
454
+ await worker()
455
+ } else {
456
+ await Promise.all(
457
+ Array.from({ length: Math.min(args.concurrency, pendingScopes.length) }, () => worker()),
458
+ )
459
+ }
460
+ return results
461
+ }
462
+
463
+ async function getBareImportScopePathname(args: {
464
+ importerPath: string
465
+ isDirectoryResolutionFileIndependent(directory: string): boolean
466
+ resolvedIdentityPath: string
467
+ resolveDirectorySpecifierIdentity(directory: string, specifier: string): Promise<string | null>
468
+ routes: CompiledRoutes
469
+ specifier: string
470
+ }): Promise<string> {
471
+ let importerDirectory = normalizeFilePath(path.dirname(args.importerPath))
472
+ let importerScopePathname = args.routes.toUrlPathname(importerDirectory)
473
+ if (!importerScopePathname) {
474
+ throw new Error(`Expected a URL pathname for ${importerDirectory}`)
475
+ }
476
+
477
+ let importerDirectoryResolution: string | null = args.resolvedIdentityPath
478
+ if (!args.isDirectoryResolutionFileIndependent(importerDirectory)) {
479
+ importerDirectoryResolution = await args.resolveDirectorySpecifierIdentity(
480
+ importerDirectory,
481
+ args.specifier,
482
+ )
483
+ if (importerDirectoryResolution !== args.resolvedIdentityPath) {
484
+ throw createAssetServerCompilationError(
485
+ `Bare import "${args.specifier}" in ${args.importerPath} resolves differently based on the importer file. ` +
486
+ `Browser module resolution must be uniform for all files in the same directory.`,
487
+ { code: 'IMPORT_RESOLUTION_NOT_DIRECTORY_UNIFORM' },
488
+ )
489
+ }
490
+ }
491
+
492
+ let directory = importerDirectory
493
+ let scopePathname = importerScopePathname
494
+ let broadestScopePathname = importerScopePathname
495
+ while (true) {
496
+ let resolvedIdentityPath =
497
+ directory === importerDirectory
498
+ ? importerDirectoryResolution
499
+ : await args.resolveDirectorySpecifierIdentity(directory, args.specifier)
500
+ if (resolvedIdentityPath !== args.resolvedIdentityPath) break
501
+
502
+ broadestScopePathname = scopePathname
503
+ let parentDirectory = normalizeFilePath(path.dirname(directory))
504
+ if (parentDirectory === directory) break
505
+ let parentScopePathname = args.routes.toUrlPathname(parentDirectory)
506
+ if (!parentScopePathname) break
507
+ directory = parentDirectory
508
+ scopePathname = parentScopePathname
509
+ }
510
+
511
+ return ensureTrailingSlash(broadestScopePathname)
512
+ }
513
+
514
+ function ensureTrailingSlash(value: string): string {
515
+ return value.endsWith('/') ? value : `${value}/`
516
+ }
517
+
356
518
  function findNearestPackageJsonPath(filePath: string): string | null {
357
519
  let directory = path.dirname(filePath)
358
520
 
@@ -0,0 +1,11 @@
1
+ export function isBareImportSpecifier(specifier: string): boolean {
2
+ return (
3
+ !specifier.startsWith('./') &&
4
+ !specifier.startsWith('../') &&
5
+ !specifier.startsWith('/') &&
6
+ !specifier.startsWith('file:') &&
7
+ !specifier.startsWith('data:') &&
8
+ !specifier.startsWith('http://') &&
9
+ !specifier.startsWith('https://')
10
+ )
11
+ }
@@ -18,7 +18,6 @@ import {
18
18
  isAssetServerCompilationError,
19
19
  } from '../compilation-error.ts'
20
20
  import type { AssetServerCompilationError } from '../compilation-error.ts'
21
- import { generateFingerprint } from '../fingerprint.ts'
22
21
  import {
23
22
  maskAuthoredInjectedPackageSpecifier,
24
23
  mayContainInjectedPackageSpecifier,
@@ -38,6 +37,7 @@ import type { EmittedModule } from './emit.ts'
38
37
  import type { ResolvedScriptTarget } from '../target.ts'
39
38
  import type { ResolvedModule } from './resolve.ts'
40
39
  import { scriptLoaderConditions } from './conditions.ts'
40
+ import { isBareImportSpecifier } from './specifiers.ts'
41
41
 
42
42
  type ScriptRecord = ModuleRecord<TransformedModule, ResolvedModule, EmittedModule>
43
43
 
@@ -74,16 +74,16 @@ export type ResolveModuleResult = {
74
74
  }
75
75
 
76
76
  type UnresolvedImport = {
77
+ dynamic: boolean
77
78
  end: number
78
79
  quote?: '"' | "'" | '`'
79
80
  specifier: string
80
81
  start: number
81
82
  }
82
83
 
83
- type HmrAcceptedDependency = UnresolvedImport
84
+ type HmrAcceptedDependency = Omit<UnresolvedImport, 'dynamic'>
84
85
 
85
86
  export type TransformedModule = {
86
- fingerprint: string | null
87
87
  hmr: {
88
88
  acceptedDeps: HmrAcceptedDependency[]
89
89
  selfAccepting: boolean
@@ -121,7 +121,6 @@ type TsconfigTransformOptions = {
121
121
  type TsconfigTransformOptionsResolver = ReturnType<typeof createTsconfigTransformOptionsResolver>
122
122
 
123
123
  export type TransformArgs = {
124
- buildId: string | null
125
124
  define: Record<string, string> | null
126
125
  externalSet: ReadonlySet<string>
127
126
  isWatchIgnored(filePath: string): boolean
@@ -272,13 +271,6 @@ export async function transformModule(
272
271
  trackedFiles,
273
272
  },
274
273
  value: {
275
- fingerprint:
276
- args.buildId === null
277
- ? null
278
- : await generateFingerprint({
279
- buildId: args.buildId,
280
- content: sourceText,
281
- }),
282
274
  hmr: getHmrAnalysis(analysis.rawCode),
283
275
  identityPath: record.identityPath,
284
276
  importerDir: path.dirname(resolvedPath),
@@ -434,18 +426,6 @@ function findNearestTsconfigPath(directory: string): string | null {
434
426
  }
435
427
  }
436
428
 
437
- function isBareImportSpecifier(specifier: string): boolean {
438
- return (
439
- !specifier.startsWith('./') &&
440
- !specifier.startsWith('../') &&
441
- !specifier.startsWith('/') &&
442
- !specifier.startsWith('file:') &&
443
- !specifier.startsWith('data:') &&
444
- !specifier.startsWith('http://') &&
445
- !specifier.startsWith('https://')
446
- )
447
- }
448
-
449
429
  async function analyzeModuleSource(
450
430
  sourceText: string,
451
431
  resolvedPath: string,
@@ -823,6 +803,7 @@ async function getUnresolvedImportsFromLexer(rawCode: string): Promise<Unresolve
823
803
  let specifier = getStaticImportSpecifier(rawCode, imported)
824
804
  if (specifier == null || isBrowserExternalModuleUrl(specifier)) continue
825
805
  unresolvedImports.push({
806
+ dynamic: imported.d !== -1,
826
807
  specifier,
827
808
  start: imported.s,
828
809
  end: imported.e,
@@ -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
  }