@remix-run/assets 0.4.4 → 0.5.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 (74) hide show
  1. package/README.md +322 -56
  2. package/dist/assets.d.ts +2 -1
  3. package/dist/assets.d.ts.map +1 -1
  4. package/dist/assets.js +2 -2
  5. package/dist/lib/access.d.ts +6 -2
  6. package/dist/lib/access.d.ts.map +1 -1
  7. package/dist/lib/access.js +300 -5
  8. package/dist/lib/asset-server.d.ts +118 -5
  9. package/dist/lib/asset-server.d.ts.map +1 -1
  10. package/dist/lib/asset-server.js +260 -24
  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/file-matcher.js +1 -1
  14. package/dist/lib/files/compiler.d.ts.map +1 -1
  15. package/dist/lib/files/compiler.js +7 -6
  16. package/dist/lib/files/config.js +1 -1
  17. package/dist/lib/hmr.d.ts +37 -0
  18. package/dist/lib/hmr.d.ts.map +1 -0
  19. package/dist/lib/hmr.js +357 -0
  20. package/dist/lib/injected-packages.d.ts.map +1 -1
  21. package/dist/lib/injected-packages.js +36 -13
  22. package/dist/lib/loaders.d.ts +57 -0
  23. package/dist/lib/loaders.d.ts.map +1 -0
  24. package/dist/lib/loaders.js +1 -0
  25. package/dist/lib/module-store.d.ts +24 -0
  26. package/dist/lib/module-store.d.ts.map +1 -1
  27. package/dist/lib/module-store.js +136 -11
  28. package/dist/lib/routes.js +1 -1
  29. package/dist/lib/scripts/compiler.d.ts +24 -1
  30. package/dist/lib/scripts/compiler.d.ts.map +1 -1
  31. package/dist/lib/scripts/compiler.js +183 -29
  32. package/dist/lib/scripts/conditions.d.ts +2 -0
  33. package/dist/lib/scripts/conditions.d.ts.map +1 -0
  34. package/dist/lib/scripts/conditions.js +1 -0
  35. package/dist/lib/scripts/emit.d.ts +3 -0
  36. package/dist/lib/scripts/emit.d.ts.map +1 -1
  37. package/dist/lib/scripts/emit.js +24 -5
  38. package/dist/lib/scripts/resolve.d.ts +4 -0
  39. package/dist/lib/scripts/resolve.d.ts.map +1 -1
  40. package/dist/lib/scripts/resolve.js +70 -5
  41. package/dist/lib/scripts/transform.d.ts +9 -0
  42. package/dist/lib/scripts/transform.d.ts.map +1 -1
  43. package/dist/lib/scripts/transform.js +222 -18
  44. package/dist/lib/source-maps.js +1 -1
  45. package/dist/lib/styles/compiler.d.ts +14 -1
  46. package/dist/lib/styles/compiler.d.ts.map +1 -1
  47. package/dist/lib/styles/compiler.js +78 -14
  48. package/dist/lib/styles/emit.js +4 -4
  49. package/dist/lib/styles/resolve.d.ts.map +1 -1
  50. package/dist/lib/styles/resolve.js +8 -7
  51. package/dist/lib/styles/transform.js +3 -3
  52. package/dist/lib/target.d.ts +1 -1
  53. package/dist/lib/target.d.ts.map +1 -1
  54. package/dist/lib/watch.js +1 -1
  55. package/dist/types/hmr.d.ts +36 -0
  56. package/package.json +17 -11
  57. package/src/assets.ts +2 -1
  58. package/src/lib/access.ts +386 -5
  59. package/src/lib/asset-server.ts +429 -18
  60. package/src/lib/compilation-error.ts +1 -0
  61. package/src/lib/files/compiler.ts +7 -3
  62. package/src/lib/hmr.ts +397 -0
  63. package/src/lib/injected-packages.ts +41 -11
  64. package/src/lib/loaders.ts +80 -0
  65. package/src/lib/module-store.ts +190 -9
  66. package/src/lib/scripts/compiler.ts +248 -24
  67. package/src/lib/scripts/conditions.ts +1 -0
  68. package/src/lib/scripts/emit.ts +50 -5
  69. package/src/lib/scripts/resolve.ts +124 -2
  70. package/src/lib/scripts/transform.ts +290 -19
  71. package/src/lib/styles/compiler.ts +108 -8
  72. package/src/lib/styles/emit.ts +1 -1
  73. package/src/lib/styles/resolve.ts +11 -7
  74. package/src/types/hmr.d.ts +36 -0
@@ -1,6 +1,7 @@
1
1
  import * as fs from 'node:fs'
2
2
  import * as fsp from 'node:fs/promises'
3
3
  import * as path from 'node:path'
4
+ import { pathToFileURL } from 'node:url'
4
5
  import { getTsconfig } from 'get-tsconfig'
5
6
  import MagicString from 'magic-string'
6
7
  import { minify } from 'oxc-minify'
@@ -24,16 +25,24 @@ import {
24
25
  restoreAuthoredInjectedPackageSpecifier,
25
26
  } from '../injected-packages.ts'
26
27
  import type { ModuleRecord, ModuleTracking } from '../module-store.ts'
28
+ import type {
29
+ ModuleLoadContext,
30
+ ModuleLoader,
31
+ ModuleLoadResult,
32
+ NextModuleLoader,
33
+ } from '../loaders.ts'
27
34
  import { normalizeFilePath } from '../paths.ts'
28
35
  import type { CompiledRoutes } from '../routes.ts'
29
36
  import { composeSourceMaps, rewriteSourceMapSources, stringifySourceMap } from '../source-maps.ts'
30
37
  import type { EmittedModule } from './emit.ts'
31
38
  import type { ResolvedScriptTarget } from '../target.ts'
32
39
  import type { ResolvedModule } from './resolve.ts'
40
+ import { scriptLoaderConditions } from './conditions.ts'
33
41
 
34
42
  type ScriptRecord = ModuleRecord<TransformedModule, ResolvedModule, EmittedModule>
35
43
 
36
44
  type SourceLanguage = 'js' | 'jsx' | 'ts' | 'tsx'
45
+ type ModuleLoadSource = NonNullable<ModuleLoadResult['source']>
37
46
 
38
47
  const scriptModuleTypes = [
39
48
  { extension: '.js', lang: 'js' },
@@ -71,8 +80,15 @@ type UnresolvedImport = {
71
80
  start: number
72
81
  }
73
82
 
83
+ type HmrAcceptedDependency = UnresolvedImport
84
+
74
85
  export type TransformedModule = {
75
86
  fingerprint: string | null
87
+ hmr: {
88
+ acceptedDeps: HmrAcceptedDependency[]
89
+ selfAccepting: boolean
90
+ usesImportMetaHot: boolean
91
+ }
76
92
  identityPath: string
77
93
  importerDir: string
78
94
  packageSpecifiers: string[]
@@ -110,6 +126,7 @@ export type TransformArgs = {
110
126
  externalSet: ReadonlySet<string>
111
127
  isWatchIgnored(filePath: string): boolean
112
128
  minify: boolean
129
+ loaders: readonly ModuleLoader[]
113
130
  resolveActualPath(identityPath: string): string | null
114
131
  routes: CompiledRoutes
115
132
  sourceMapSourcePaths: 'absolute' | 'url'
@@ -205,9 +222,21 @@ export async function transformModule(
205
222
  }
206
223
 
207
224
  try {
225
+ let stableUrlPathname = args.routes.toUrlPathname(record.identityPath)
226
+ if (!stableUrlPathname) {
227
+ throw createAssetServerCompilationError(
228
+ `File ${record.identityPath} is outside all configured fileMap entries.`,
229
+ {
230
+ code: 'FILE_OUTSIDE_FILE_MAP',
231
+ },
232
+ )
233
+ }
234
+
208
235
  let analysis = await analyzeModuleSource(sourceText, resolvedPath, transformOptions, {
209
236
  define: args.define ?? undefined,
210
237
  minify: args.minify,
238
+ loaders: args.loaders,
239
+ moduleUrl: stableUrlPathname,
211
240
  sourceMaps: args.sourceMaps ?? undefined,
212
241
  target: args.target ?? undefined,
213
242
  })
@@ -227,16 +256,6 @@ export async function transformModule(
227
256
  )
228
257
  }
229
258
 
230
- let stableUrlPathname = args.routes.toUrlPathname(record.identityPath)
231
- if (!stableUrlPathname) {
232
- throw createAssetServerCompilationError(
233
- `File ${record.identityPath} is outside all configured fileMap entries.`,
234
- {
235
- code: 'FILE_OUTSIDE_FILE_MAP',
236
- },
237
- )
238
- }
239
-
240
259
  let sourceMap = analysis.sourceMap
241
260
  ? rewriteSourceMapSources(
242
261
  analysis.sourceMap,
@@ -260,6 +279,7 @@ export async function transformModule(
260
279
  buildId: args.buildId,
261
280
  content: sourceText,
262
281
  }),
282
+ hmr: getHmrAnalysis(analysis.rawCode),
263
283
  identityPath: record.identityPath,
264
284
  importerDir: path.dirname(resolvedPath),
265
285
  packageSpecifiers: analysis.unresolvedImports
@@ -284,6 +304,121 @@ export async function transformModule(
284
304
  }
285
305
  }
286
306
 
307
+ export function getHmrAnalysis(rawCode: string): TransformedModule['hmr'] {
308
+ let mayUseImportMetaHot = rawCode.includes('import.meta.hot')
309
+ let usesImportMetaHot = false
310
+ let acceptedDeps: HmrAcceptedDependency[] = []
311
+ let selfAccepting = false
312
+
313
+ if (mayUseImportMetaHot) {
314
+ try {
315
+ let parseResult = parseSync('hmr-analysis.js', rawCode, {
316
+ lang: 'js',
317
+ sourceType: 'module',
318
+ })
319
+
320
+ if (parseResult.errors.length > 0) {
321
+ throw createAssetServerCompilationError(
322
+ `Failed to analyze HMR usage in transformed script. ${parseResult.errors[0]?.message ?? 'Unknown parse error'}`,
323
+ {
324
+ code: 'TRANSFORM_FAILED',
325
+ },
326
+ )
327
+ }
328
+
329
+ walkAst(parseResult.program, (node) => {
330
+ if (isImportMetaHotNode(node)) {
331
+ usesImportMetaHot = true
332
+ }
333
+
334
+ if (node.type !== 'CallExpression') return
335
+ if (!isImportMetaHotAcceptCallee(node.callee)) return
336
+
337
+ let [firstArgument] = node.arguments
338
+ if (firstArgument === undefined || !isAcceptedDependencyArgument(firstArgument)) {
339
+ selfAccepting = true
340
+ return
341
+ }
342
+
343
+ acceptedDeps.push(...getAcceptedDependencies(firstArgument, rawCode))
344
+ })
345
+ } catch (error) {
346
+ if (isAssetServerCompilationError(error)) throw error
347
+ throw createAssetServerCompilationError(
348
+ `Failed to analyze HMR usage in transformed script.`,
349
+ {
350
+ cause: error,
351
+ code: 'TRANSFORM_FAILED',
352
+ },
353
+ )
354
+ }
355
+ }
356
+
357
+ return {
358
+ acceptedDeps,
359
+ selfAccepting,
360
+ usesImportMetaHot,
361
+ }
362
+ }
363
+
364
+ function isImportMetaHotAcceptCallee(node: Node): boolean {
365
+ let callee = unwrapChainExpression(node)
366
+ if (callee.type !== 'MemberExpression') return false
367
+ if (callee.computed || !isIdentifierNode(callee.property, 'accept')) return false
368
+
369
+ return isImportMetaHotNode(callee.object)
370
+ }
371
+
372
+ function isImportMetaHotNode(node: Node): boolean {
373
+ let hot = unwrapChainExpression(node)
374
+ if (hot.type !== 'MemberExpression') return false
375
+ if (hot.computed || !isIdentifierNode(hot.property, 'hot')) return false
376
+
377
+ let meta = unwrapChainExpression(hot.object)
378
+ return (
379
+ meta.type === 'MetaProperty' &&
380
+ isIdentifierNode(meta.meta, 'import') &&
381
+ isIdentifierNode(meta.property, 'meta')
382
+ )
383
+ }
384
+
385
+ function unwrapChainExpression(node: Node): Node {
386
+ return node.type === 'ChainExpression' ? node.expression : node
387
+ }
388
+
389
+ function isIdentifierNode(node: Node, name: string): boolean {
390
+ return node.type === 'Identifier' && node.name === name
391
+ }
392
+
393
+ function isAcceptedDependencyArgument(node: Node): boolean {
394
+ return isStringLiteralNode(node) || node.type === 'ArrayExpression'
395
+ }
396
+
397
+ function getAcceptedDependencies(node: Node, source: string): HmrAcceptedDependency[] {
398
+ if (isStringLiteralNode(node)) {
399
+ return [
400
+ {
401
+ end: node.end - 1,
402
+ specifier: node.value,
403
+ start: node.start + 1,
404
+ },
405
+ ]
406
+ }
407
+
408
+ if (node.type !== 'ArrayExpression') return []
409
+
410
+ let deps: HmrAcceptedDependency[] = []
411
+ for (let element of node.elements) {
412
+ if (!isStringLiteralNode(element)) continue
413
+ deps.push({
414
+ end: element.end - 1,
415
+ specifier: element.value,
416
+ start: element.start + 1,
417
+ })
418
+ }
419
+ return deps
420
+ }
421
+
287
422
  function findNearestTsconfigPath(directory: string): string | null {
288
423
  let currentDirectory = directory
289
424
 
@@ -318,6 +453,8 @@ async function analyzeModuleSource(
318
453
  options: {
319
454
  define?: Record<string, string>
320
455
  minify: boolean
456
+ loaders: readonly ModuleLoader[]
457
+ moduleUrl: string
321
458
  sourceMaps?: 'external' | 'inline'
322
459
  target?: ResolvedScriptTarget
323
460
  },
@@ -345,6 +482,40 @@ async function analyzeModuleSource(
345
482
  let rawCode = transformResult.code.trimEnd()
346
483
  let sourceMap = stringifySourceMap(transformResult.map)
347
484
 
485
+ if (options.loaders.length > 0) {
486
+ let loaderUrl = pathToFileURL(resolvedPath).href
487
+ let loaderInputSource = sourceMap ? appendInlineSourceMap(rawCode, sourceMap) : rawCode
488
+ let loaderOutputSource: string
489
+ try {
490
+ let loaderResult = runModuleLoaders(loaderUrl, {
491
+ format: 'module',
492
+ loaders: options.loaders,
493
+ moduleUrl: options.moduleUrl,
494
+ source: loaderInputSource,
495
+ })
496
+ loaderOutputSource = moduleLoadSourceToString(getModuleLoadSource(loaderUrl, loaderResult))
497
+ } catch (error) {
498
+ if (isAssetServerCompilationError(error)) throw error
499
+ throw createAssetServerCompilationError(
500
+ `Module loader failed for ${loaderUrl}. ${formatUnknownError(error)}`,
501
+ {
502
+ cause: error,
503
+ code: 'LOADER_FAILED',
504
+ },
505
+ )
506
+ }
507
+ if (loaderOutputSource !== loaderInputSource) {
508
+ let loaderOutput = extractInlineSourceMap(loaderOutputSource)
509
+ rawCode = loaderOutput.code.trimEnd()
510
+ sourceMap =
511
+ loaderOutput.sourceMap && loaderOutput.sourceMap !== sourceMap
512
+ ? sourceMap
513
+ ? composeSourceMaps(loaderOutput.sourceMap, sourceMap)
514
+ : loaderOutput.sourceMap
515
+ : null
516
+ }
517
+ }
518
+
348
519
  if (options.minify) {
349
520
  let minifyResult = await minifyModule(rawCode, resolvedPath, options.target, options.sourceMaps)
350
521
  rawCode = minifyResult.code.trimEnd()
@@ -364,6 +535,110 @@ async function analyzeModuleSource(
364
535
  }
365
536
  }
366
537
 
538
+ function runModuleLoaders(
539
+ url: string,
540
+ options: {
541
+ format: string
542
+ loaders: readonly ModuleLoader[]
543
+ moduleUrl: string
544
+ source: string
545
+ },
546
+ ): ModuleLoadResult {
547
+ let loadContext: ModuleLoadContext = {
548
+ conditions: [...scriptLoaderConditions],
549
+ format: options.format,
550
+ importAttributes: {},
551
+ moduleUrl: options.moduleUrl,
552
+ }
553
+
554
+ let load: NextModuleLoader = (_url, context) => ({
555
+ format: context?.format ?? options.format,
556
+ source: options.source,
557
+ })
558
+
559
+ for (let loader of options.loaders) {
560
+ let nextLoad = load
561
+ load = (nextUrl, nextContext) => {
562
+ let nextLoadCalled = false
563
+ let wrappedNextLoad: NextModuleLoader = (wrappedUrl, wrappedContext) => {
564
+ if (wrappedUrl !== nextUrl) {
565
+ throw createAssetServerCompilationError(
566
+ `Module loader for ${nextUrl} attempted to change the module URL to ${wrappedUrl}. Loaders cannot change module URLs.`,
567
+ {
568
+ code: 'LOADER_FAILED',
569
+ },
570
+ )
571
+ }
572
+ nextLoadCalled = true
573
+ return nextLoad(wrappedUrl, wrappedContext)
574
+ }
575
+ let result = loader(
576
+ nextUrl,
577
+ {
578
+ conditions: nextContext?.conditions ?? loadContext.conditions,
579
+ format: nextContext?.format ?? loadContext.format,
580
+ importAttributes: nextContext?.importAttributes ?? loadContext.importAttributes,
581
+ moduleUrl: nextContext?.moduleUrl ?? loadContext.moduleUrl,
582
+ },
583
+ wrappedNextLoad,
584
+ )
585
+ if (!nextLoadCalled && result.shortCircuit !== true) {
586
+ throw createAssetServerCompilationError(
587
+ `Module loader for ${nextUrl} returned without calling nextLoad or setting shortCircuit: true.`,
588
+ {
589
+ code: 'LOADER_FAILED',
590
+ },
591
+ )
592
+ }
593
+ return result
594
+ }
595
+ }
596
+
597
+ return load(url, loadContext)
598
+ }
599
+
600
+ function getModuleLoadSource(url: string, result: ModuleLoadResult): ModuleLoadSource {
601
+ if (result.format !== 'module') {
602
+ throw createAssetServerCompilationError(
603
+ `Module loader for ${url} returned unsupported format ${JSON.stringify(result.format)}. Only "module" is supported.`,
604
+ {
605
+ code: 'LOADER_FAILED',
606
+ },
607
+ )
608
+ }
609
+
610
+ if (result.source !== undefined && result.source !== null) return result.source
611
+
612
+ throw createAssetServerCompilationError(`Module loader for ${url} did not return source.`, {
613
+ code: 'LOADER_FAILED',
614
+ })
615
+ }
616
+
617
+ function moduleLoadSourceToString(source: ModuleLoadSource): string {
618
+ if (typeof source === 'string') return source
619
+ return new TextDecoder().decode(source)
620
+ }
621
+
622
+ function appendInlineSourceMap(source: string, sourceMap: string): string {
623
+ let encoded = Buffer.from(sourceMap).toString('base64')
624
+ return `${source}\n//# sourceMappingURL=data:application/json;base64,${encoded}`
625
+ }
626
+
627
+ function extractInlineSourceMap(source: string): {
628
+ code: string
629
+ sourceMap: string | null
630
+ } {
631
+ let sourceMapPattern =
632
+ /(?:\/\/# sourceMappingURL=data:application\/json;base64,([A-Za-z0-9+/=]+)|\/\*# sourceMappingURL=data:application\/json;base64,([A-Za-z0-9+/=]+) \*\/)\s*$/g
633
+ let sourceMap: string | null = null
634
+ let code = source.replace(sourceMapPattern, (_match, lineComment, blockComment) => {
635
+ sourceMap = Buffer.from(lineComment ?? blockComment, 'base64').toString('utf-8')
636
+ return ''
637
+ })
638
+
639
+ return { code, sourceMap }
640
+ }
641
+
367
642
  async function minifyModule(
368
643
  rawCode: string,
369
644
  resolvedPath: string,
@@ -546,7 +821,7 @@ async function getUnresolvedImportsFromLexer(rawCode: string): Promise<Unresolve
546
821
 
547
822
  for (let imported of imports) {
548
823
  let specifier = getStaticImportSpecifier(rawCode, imported)
549
- if (specifier == null || shouldSkipImportSpecifier(specifier)) continue
824
+ if (specifier == null || isBrowserExternalModuleUrl(specifier)) continue
550
825
  unresolvedImports.push({
551
826
  specifier,
552
827
  start: imported.s,
@@ -562,6 +837,10 @@ function getDisplayImportSpecifier(specifier: string): string {
562
837
  return restoreAuthoredInjectedPackageSpecifier(specifier) ?? specifier
563
838
  }
564
839
 
840
+ function isBrowserExternalModuleUrl(url: string): boolean {
841
+ return url.startsWith('data:') || url.startsWith('http://') || url.startsWith('https://')
842
+ }
843
+
565
844
  function maskAuthoredInjectedPackageImports(sourceText: string, resolvedPath: string): string {
566
845
  if (!mayContainInjectedPackageSpecifier(sourceText)) {
567
846
  return sourceText
@@ -675,14 +954,6 @@ function isStaticTemplateLiteral(specifier: string): boolean {
675
954
  return specifier.startsWith('`') && specifier.endsWith('`') && !specifier.includes('${')
676
955
  }
677
956
 
678
- function shouldSkipImportSpecifier(specifier: string): boolean {
679
- return (
680
- specifier.startsWith('data:') ||
681
- specifier.startsWith('http://') ||
682
- specifier.startsWith('https://')
683
- )
684
- }
685
-
686
957
  function getImportQuote(source: string, start: number): '"' | "'" | '`' | undefined {
687
958
  let firstCharacter = source[start]
688
959
  if (firstCharacter === '"' || firstCharacter === "'" || firstCharacter === '`') {
@@ -50,10 +50,14 @@ type StyleCompilerOptions = {
50
50
  transform: readonly string[] | null
51
51
  },
52
52
  ): Promise<string>
53
+ hmr?: {
54
+ send(pathname: string, timestamp: number): void
55
+ }
53
56
  isAllowed(absolutePath: string): boolean
54
57
  isServedFilePath(filePath: string): boolean
55
58
  minify: boolean
56
59
  onWatchDirectoriesChange?: (delta: { add: string[]; remove: string[] }) => void
60
+ onWatchFilesChange?: (delta: { add: string[]; remove: string[] }) => void
57
61
  rootDir: string
58
62
  routes: CompiledRoutes
59
63
  sourceMapSourcePaths: 'absolute' | 'url'
@@ -66,7 +70,17 @@ type StyleCompiler = {
66
70
  getHref(filePath: string): Promise<string>
67
71
  getPreloadLayers(filePath: string | readonly string[]): Promise<string[][]>
68
72
  getStyle(filePath: string, options: StyleGetOptions): Promise<StyleGetResult>
69
- handleFileEvent(filePath: string, event: 'add' | 'change' | 'unlink'): Promise<void>
73
+ classifyHmrFileEvent(
74
+ filePath: string,
75
+ event: 'add' | 'change' | 'unlink',
76
+ ): Promise<StyleHmrUpdate[]>
77
+ invalidateFileEvent(filePath: string, event: 'add' | 'change' | 'unlink'): void
78
+ }
79
+
80
+ export type StyleHmrUpdate = {
81
+ filePath: string
82
+ path: string
83
+ timestamp: number
70
84
  }
71
85
 
72
86
  const preloadConcurrency = Math.max(1, Math.min(8, os.availableParallelism() - 1))
@@ -80,7 +94,11 @@ export function createStyleCompiler(options: StyleCompilerOptions): StyleCompile
80
94
  ),
81
95
  }
82
96
  let styleStore: StyleStore = createModuleStore<TransformedStyle, ResolvedStyle, EmittedStyle>({
97
+ getDependencies(resolvedStyle) {
98
+ return resolvedStyle.deps
99
+ },
83
100
  onWatchDirectoriesChange: options.onWatchDirectoriesChange,
101
+ onWatchFilesChange: options.onWatchFilesChange,
84
102
  })
85
103
  let resolveInFlightByCacheKey = new Map<string, Promise<ResolvedStyle>>()
86
104
  let emitInFlightByCacheKey = new Map<string, Promise<EmittedStyle>>()
@@ -149,7 +167,12 @@ export function createStyleCompiler(options: StyleCompilerOptions): StyleCompile
149
167
  async getStyle(filePath, getOptions) {
150
168
  let resolvedStyle = resolveServedStyleOrThrow(resolveInputFilePath(filePath), resolveArgs)
151
169
  let record = styleStore.get(resolvedStyle.identityPath)
152
- let notModified = getNotModifiedStyle(record.emitted, getOptions)
170
+ let notModified = getNotModifiedStyle(
171
+ record,
172
+ styleStore,
173
+ getOptions,
174
+ hasHmrTimestampedDependency,
175
+ )
153
176
  if (notModified) return notModified
154
177
 
155
178
  let emitted = await getOrCreateEmittedStyle(record)
@@ -159,7 +182,26 @@ export function createStyleCompiler(options: StyleCompilerOptions): StyleCompile
159
182
  }
160
183
  },
161
184
 
162
- async handleFileEvent(filePath, event) {
185
+ async classifyHmrFileEvent(filePath, event) {
186
+ let normalizedFilePath = normalizeFilePath(filePath)
187
+ if (isWatchIgnored(normalizedFilePath)) return []
188
+ let timestamp = Date.now()
189
+ let updatePathnames =
190
+ event === 'change' ? getHmrUpdatePathnames(normalizedFilePath, timestamp) : []
191
+
192
+ styleStore.invalidateForFileEvent(normalizedFilePath, event)
193
+
194
+ for (let updatePathname of updatePathnames) {
195
+ resolvedOptions.hmr?.send(updatePathname, timestamp)
196
+ }
197
+ return updatePathnames.map((path) => ({
198
+ filePath: normalizedFilePath,
199
+ path,
200
+ timestamp,
201
+ }))
202
+ },
203
+
204
+ invalidateFileEvent(filePath, event) {
163
205
  let normalizedFilePath = normalizeFilePath(filePath)
164
206
  if (isWatchIgnored(normalizedFilePath)) return
165
207
  styleStore.invalidateForFileEvent(normalizedFilePath, event)
@@ -185,7 +227,7 @@ export function createStyleCompiler(options: StyleCompilerOptions): StyleCompile
185
227
  }
186
228
 
187
229
  async function getOrCreateResolvedStyle(record: StyleRecord): Promise<ResolvedStyle> {
188
- if (record.resolved) return record.resolved
230
+ if (record.resolved && styleStore.isResolvedFresh(record)) return record.resolved
189
231
 
190
232
  let cacheKey = getRecordCacheKey(record)
191
233
  let existing = resolveInFlightByCacheKey.get(cacheKey)
@@ -224,7 +266,7 @@ export function createStyleCompiler(options: StyleCompilerOptions): StyleCompile
224
266
  }
225
267
 
226
268
  async function getOrCreateTransformedStyle(record: StyleRecord): Promise<TransformedStyle> {
227
- if (record.transformed) return record.transformed
269
+ if (record.transformed && styleStore.isTransformedFresh(record)) return record.transformed
228
270
 
229
271
  let startedVersion = record.invalidationVersion
230
272
  let transformStyleResult = await transformStyle(record, transformArgs)
@@ -246,7 +288,13 @@ export function createStyleCompiler(options: StyleCompilerOptions): StyleCompile
246
288
  }
247
289
 
248
290
  async function getOrCreateEmittedStyle(record: StyleRecord): Promise<EmittedStyle> {
249
- if (record.emitted) return record.emitted
291
+ if (
292
+ record.emitted &&
293
+ styleStore.isEmittedFresh(record) &&
294
+ !hasHmrTimestampedDependency(record.resolved)
295
+ ) {
296
+ return record.emitted
297
+ }
250
298
 
251
299
  let cacheKey = getRecordCacheKey(record)
252
300
  let existing = emitInFlightByCacheKey.get(cacheKey)
@@ -290,10 +338,49 @@ export function createStyleCompiler(options: StyleCompilerOptions): StyleCompile
290
338
  }
291
339
 
292
340
  function getServedUrlForResolvedStyle(resolvedStyle: ResolvedStyle): string {
293
- return formatFingerprintedPathname(
341
+ let pathname = formatFingerprintedPathname(
294
342
  resolvedStyle.stableUrlPathname,
295
343
  resolvedOptions.fingerprintAssets ? resolvedStyle.fingerprint : null,
296
344
  )
345
+ let timestamp = styleStore.getHmrUpdateTimestamp(resolvedStyle.identityPath)
346
+ return timestamp ? appendTimestamp(pathname, timestamp) : pathname
347
+ }
348
+
349
+ function getHmrUpdatePathnames(identityPath: string, timestamp: number): string[] {
350
+ let resolvedStyles = findHmrUpdateStyles(identityPath, new Set())
351
+ // Mark update timestamps before invalidating so re-emitted importer stylesheets
352
+ // can rewrite imports to the changed dependency with this HMR timestamp.
353
+ for (let resolvedStyle of resolvedStyles) {
354
+ styleStore.setHmrUpdateTimestamp(resolvedStyle.identityPath, timestamp)
355
+ }
356
+
357
+ return dedupeHmrUpdatePathnames(
358
+ resolvedStyles
359
+ .filter((resolvedStyle) => styleStore.getImporters(resolvedStyle.identityPath).size === 0)
360
+ .map((resolvedStyle) => resolvedStyle.stableUrlPathname),
361
+ )
362
+ }
363
+
364
+ function findHmrUpdateStyles(identityPath: string, traversed: Set<string>): ResolvedStyle[] {
365
+ if (traversed.has(identityPath)) return []
366
+ traversed.add(identityPath)
367
+
368
+ let resolvedStyle = styleStore.getLastResolved(identityPath)
369
+ if (!resolvedStyle) return []
370
+
371
+ let resolvedStyles = [resolvedStyle]
372
+ for (let importerPath of styleStore.getImporters(identityPath)) {
373
+ resolvedStyles.push(...findHmrUpdateStyles(importerPath, traversed))
374
+ }
375
+
376
+ return resolvedStyles
377
+ }
378
+
379
+ function hasHmrTimestampedDependency(resolvedStyle: ResolvedStyle | undefined): boolean {
380
+ return (
381
+ resolvedStyle?.deps.some((depPath) => styleStore.getHmrUpdateTimestamp(depPath) != null) ===
382
+ true
383
+ )
297
384
  }
298
385
 
299
386
  function isWatchIgnored(filePath: string): boolean {
@@ -310,9 +397,14 @@ function isFresh(record: StyleRecord, version: number): boolean {
310
397
  }
311
398
 
312
399
  function getNotModifiedStyle(
313
- emittedStyle: EmittedStyle | undefined,
400
+ record: StyleRecord,
401
+ styleStore: StyleStore,
314
402
  options: StyleGetOptions,
403
+ hasHmrTimestampedDependency: (resolvedStyle: ResolvedStyle | undefined) => boolean,
315
404
  ): StyleGetResult | null {
405
+ if (hasHmrTimestampedDependency(record.resolved)) return null
406
+ if (!styleStore.isEmittedFresh(record)) return null
407
+ let emittedStyle = record.emitted
316
408
  if (!emittedStyle || options.ifNoneMatch === null) return null
317
409
 
318
410
  if (
@@ -329,6 +421,14 @@ function getNotModifiedStyle(
329
421
  return { etag: asset.etag, type: 'not-modified' }
330
422
  }
331
423
 
424
+ function appendTimestamp(pathname: string, timestamp: number): string {
425
+ return `${pathname}${pathname.includes('?') ? '&' : '?'}t=${timestamp}`
426
+ }
427
+
428
+ function dedupeHmrUpdatePathnames(pathnames: string[]): string[] {
429
+ return [...new Set(pathnames)]
430
+ }
431
+
332
432
  function getEmittedAssetForRequest(
333
433
  emittedStyle: EmittedStyle,
334
434
  isSourceMapRequest: boolean,
@@ -105,7 +105,7 @@ async function rewriteDependencies(
105
105
  dependency.kind === 'external'
106
106
  ? dependency.replacement
107
107
  : dependency.kind === 'style'
108
- ? `${await options.getServedUrl(dependency.depPath)}${dependency.suffix}`
108
+ ? appendUrlSuffix(await options.getServedUrl(dependency.depPath), dependency.suffix)
109
109
  : appendUrlSuffix(
110
110
  await getServedFileUrl(options, resolvedStyle.identityPath, dependency.depPath, {
111
111
  transform: dependency.requestTransform,
@@ -163,9 +163,13 @@ export function resolveServedStyleOrThrow(
163
163
  }
164
164
 
165
165
  if (!args.isAllowed(identityPath)) {
166
- throw createAssetServerCompilationError(`File is not allowed: ${identityPath}`, {
167
- code: 'FILE_NOT_ALLOWED',
168
- })
166
+ throw createAssetServerCompilationError(
167
+ `File "${identityPath}" is not allowed by the asset server access configuration. ` +
168
+ `Add a matching allowFiles or allowPackages rule, or remove a conflicting denyFiles rule.`,
169
+ {
170
+ code: 'FILE_NOT_ALLOWED',
171
+ },
172
+ )
169
173
  }
170
174
 
171
175
  let stableUrlPathname = args.routes.toUrlPathname(identityPath)
@@ -218,8 +222,8 @@ function resolveImportDependency(
218
222
 
219
223
  if (!args.isAllowed(identityPath)) {
220
224
  throw createAssetServerCompilationError(
221
- `Import "${url}" in ${importerPath}, resolved to "${identityPath}", is not allowed by the asset server allow/deny configuration. ` +
222
- `Add a matching allow rule for this file path, remove a conflicting deny rule for this file path, or mark this import as external.`,
225
+ `Import "${url}" in ${importerPath}, resolved to "${identityPath}", is not allowed by the asset server access configuration. ` +
226
+ `Add a matching allowFiles or allowPackages rule, remove a conflicting denyFiles rule, or mark this import as external.`,
223
227
  {
224
228
  code: 'IMPORT_NOT_ALLOWED',
225
229
  },
@@ -288,8 +292,8 @@ function resolveUrlDependency(
288
292
 
289
293
  if (!args.isAllowed(identityPath)) {
290
294
  throw createAssetServerCompilationError(
291
- `URL "${url}" in ${importerPath}, resolved to "${identityPath}", is not allowed by the asset server allow/deny configuration. ` +
292
- `Add a matching allow rule for this file path or remove a conflicting deny rule for this file path.`,
295
+ `URL "${url}" in ${importerPath}, resolved to "${identityPath}", is not allowed by the asset server access configuration. ` +
296
+ `Add a matching allowFiles or allowPackages rule, or remove a conflicting denyFiles rule.`,
293
297
  {
294
298
  code: 'URL_NOT_ALLOWED',
295
299
  },