@remix-run/assets 0.3.0 → 0.4.1

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 (66) hide show
  1. package/README.md +206 -8
  2. package/dist/assets.d.ts +1 -0
  3. package/dist/assets.d.ts.map +1 -1
  4. package/dist/assets.js +1 -0
  5. package/dist/lib/access.d.ts +1 -0
  6. package/dist/lib/access.d.ts.map +1 -1
  7. package/dist/lib/access.js +7 -1
  8. package/dist/lib/asset-server.d.ts +29 -6
  9. package/dist/lib/asset-server.d.ts.map +1 -1
  10. package/dist/lib/asset-server.js +176 -17
  11. package/dist/lib/compilation-error.d.ts +1 -1
  12. package/dist/lib/compilation-error.d.ts.map +1 -1
  13. package/dist/lib/files/compiler.d.ts +71 -0
  14. package/dist/lib/files/compiler.d.ts.map +1 -0
  15. package/dist/lib/files/compiler.js +552 -0
  16. package/dist/lib/files/config.d.ts +101 -0
  17. package/dist/lib/files/config.d.ts.map +1 -0
  18. package/dist/lib/files/config.js +219 -0
  19. package/dist/lib/files/store.d.ts +31 -0
  20. package/dist/lib/files/store.d.ts.map +1 -0
  21. package/dist/lib/files/store.js +63 -0
  22. package/dist/lib/fingerprint.d.ts +3 -3
  23. package/dist/lib/fingerprint.d.ts.map +1 -1
  24. package/dist/lib/fingerprint.js +5 -5
  25. package/dist/lib/routes.d.ts.map +1 -1
  26. package/dist/lib/routes.js +28 -20
  27. package/dist/lib/scripts/compiler.d.ts +1 -0
  28. package/dist/lib/scripts/compiler.d.ts.map +1 -1
  29. package/dist/lib/scripts/compiler.js +31 -14
  30. package/dist/lib/scripts/resolve.d.ts.map +1 -1
  31. package/dist/lib/scripts/resolve.js +35 -8
  32. package/dist/lib/scripts/specifiers.d.ts +2 -0
  33. package/dist/lib/scripts/specifiers.d.ts.map +1 -0
  34. package/dist/lib/scripts/specifiers.js +9 -0
  35. package/dist/lib/scripts/transform.d.ts.map +1 -1
  36. package/dist/lib/scripts/transform.js +3 -5
  37. package/dist/lib/styles/compiler.d.ts +4 -0
  38. package/dist/lib/styles/compiler.d.ts.map +1 -1
  39. package/dist/lib/styles/compiler.js +2 -0
  40. package/dist/lib/styles/emit.d.ts +3 -0
  41. package/dist/lib/styles/emit.d.ts.map +1 -1
  42. package/dist/lib/styles/emit.js +36 -1
  43. package/dist/lib/styles/resolve.d.ts +8 -1
  44. package/dist/lib/styles/resolve.d.ts.map +1 -1
  45. package/dist/lib/styles/resolve.js +85 -32
  46. package/dist/lib/watch.d.ts +2 -0
  47. package/dist/lib/watch.d.ts.map +1 -1
  48. package/dist/lib/watch.js +25 -8
  49. package/package.json +7 -5
  50. package/src/assets.ts +1 -0
  51. package/src/lib/access.ts +8 -1
  52. package/src/lib/asset-server.ts +273 -28
  53. package/src/lib/compilation-error.ts +9 -0
  54. package/src/lib/files/compiler.ts +885 -0
  55. package/src/lib/files/config.ts +479 -0
  56. package/src/lib/files/store.ts +109 -0
  57. package/src/lib/fingerprint.ts +8 -7
  58. package/src/lib/routes.ts +36 -33
  59. package/src/lib/scripts/compiler.ts +44 -17
  60. package/src/lib/scripts/resolve.ts +57 -9
  61. package/src/lib/scripts/specifiers.ts +11 -0
  62. package/src/lib/scripts/transform.ts +3 -6
  63. package/src/lib/styles/compiler.ts +9 -0
  64. package/src/lib/styles/emit.ts +75 -1
  65. package/src/lib/styles/resolve.ts +132 -35
  66. package/src/lib/watch.ts +34 -12
package/src/lib/routes.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import { RoutePattern } from '@remix-run/route-pattern'
2
+ import { createHref } from '@remix-run/route-pattern/href'
3
+ import { createMatcher, type Matcher } from '@remix-run/route-pattern/match'
2
4
 
3
5
  import {
4
6
  getRelativeFilePath,
@@ -21,7 +23,9 @@ interface RouteConfig {
21
23
  interface CompiledRoute {
22
24
  rootDir: string
23
25
  urlPattern: RoutePattern
26
+ urlMatcher: Matcher
24
27
  filePattern: RoutePattern
28
+ fileMatcher: Matcher
25
29
  }
26
30
 
27
31
  export interface CompiledRoutes {
@@ -67,9 +71,9 @@ export function compileRoutes(
67
71
  let normalizedPathname = normalizePathname(pathname)
68
72
 
69
73
  for (let route of compiledRoutes) {
70
- let match = route.urlPattern.match(`http://remix.run${normalizedPathname}`)
74
+ let match = route.urlMatcher.match(`http://remix.run${normalizedPathname}`)
71
75
  if (!match) continue
72
- let relativeFilePath = route.filePattern.href(match.params).replace(/^\/+/, '')
76
+ let relativeFilePath = createHref(route.filePattern, match.params).replace(/^\/+/, '')
73
77
  return resolveFilePath(route.rootDir, relativeFilePath)
74
78
  }
75
79
 
@@ -80,9 +84,9 @@ export function compileRoutes(
80
84
 
81
85
  for (let route of compiledRoutes) {
82
86
  let relativeFilePath = getRelativeFilePath(route.rootDir, normalizedFilePath)
83
- let match = route.filePattern.ast.pathname.match(relativeFilePath)
87
+ let match = route.fileMatcher.match(`http://remix.run/${relativeFilePath}`)
84
88
  if (!match) continue
85
- return normalizePathname(route.urlPattern.href(getPathnameParams(route.filePattern, match)))
89
+ return normalizePathname(createHref(route.urlPattern, match.params))
86
90
  }
87
91
 
88
92
  return null
@@ -104,8 +108,8 @@ function compileRoute(
104
108
  )
105
109
  let filePatternSource = normalizeFilePattern(route.filePattern)
106
110
 
107
- let urlPattern = new RoutePattern(urlPatternSource)
108
- let filePattern = new RoutePattern(filePatternSource)
111
+ let urlPattern = RoutePattern.parse(urlPatternSource)
112
+ let filePattern = RoutePattern.parse(filePatternSource)
109
113
 
110
114
  validateNoUnnamedWildcards(urlPattern, 'URL')
111
115
  validateNoUnnamedWildcards(filePattern, 'File')
@@ -114,37 +118,30 @@ function compileRoute(
114
118
  return {
115
119
  rootDir: normalizeFilePath(options.rootDir).replace(/\/+$/, ''),
116
120
  urlPattern,
121
+ urlMatcher: createMatcher(urlPattern),
117
122
  filePattern,
123
+ fileMatcher: createMatcher(stripDotSegments(filePatternSource)),
118
124
  }
119
125
  }
120
126
 
121
- function getPathnameParams(
122
- pattern: RoutePattern,
123
- match: Array<{ name: string; type: ':' | '*'; value: string }>,
124
- ): Record<string, string | undefined> {
125
- let params: Record<string, string | undefined> = {}
127
+ function stripDotSegments(pattern: string): string {
128
+ let segments: string[] = []
126
129
 
127
- for (let param of pattern.ast.pathname.params) {
128
- if (param.name === '*') continue
129
- params[param.name] = undefined
130
- }
131
-
132
- for (let param of match) {
133
- if (param.name === '*') continue
134
- params[param.name] = param.value
130
+ for (let segment of pattern.split('/')) {
131
+ if (segment === '' || segment === '.') continue
132
+ if (segment === '..') {
133
+ segments.pop()
134
+ continue
135
+ }
136
+ segments.push(segment)
135
137
  }
136
138
 
137
- return params
139
+ return segments.join('/')
138
140
  }
139
141
 
140
142
  function validateRoutePatterns(urlPattern: RoutePattern, filePattern: RoutePattern): void {
141
- let urlParams = urlPattern.ast.pathname.params.map(
142
- (param: { name: string; type: ':' | '*' }) => `${param.type}:${param.name}`,
143
- )
144
- let fileParams = filePattern.ast.pathname.params.map(
145
- (param: { name: string; type: ':' | '*' }) => `${param.type}:${param.name}`,
146
- )
147
-
143
+ let urlParams = getPathnameParams(urlPattern)
144
+ let fileParams = getPathnameParams(filePattern)
148
145
  if (urlParams.length !== fileParams.length) {
149
146
  throw new Error(
150
147
  `Route patterns must have matching capture structure.\nURL: ${urlPattern}\nFile: ${filePattern}`,
@@ -152,7 +149,9 @@ function validateRoutePatterns(urlPattern: RoutePattern, filePattern: RoutePatte
152
149
  }
153
150
 
154
151
  for (let i = 0; i < urlParams.length; i++) {
155
- if (urlParams[i] !== fileParams[i]) {
152
+ let urlParam = urlParams[i]
153
+ let fileParam = fileParams[i]
154
+ if (urlParam.type !== fileParam.type || urlParam.name !== fileParam.name) {
156
155
  throw new Error(
157
156
  `Route patterns must have matching capture structure.\nURL: ${urlPattern}\nFile: ${filePattern}`,
158
157
  )
@@ -161,13 +160,17 @@ function validateRoutePatterns(urlPattern: RoutePattern, filePattern: RoutePatte
161
160
  }
162
161
 
163
162
  function validateNoUnnamedWildcards(pattern: RoutePattern, label: string): void {
164
- if (
165
- pattern.ast.pathname.params.some(
166
- (param: { name: string; type: ':' | '*' }) => param.type === '*' && param.name === '*',
167
- )
168
- ) {
163
+ if (pattern.pathname.tokens.some((token) => token.type === '*' && token.name === '*')) {
169
164
  throw new Error(
170
165
  `${label} route patterns must use named wildcards for reversible mapping.\nPattern: ${pattern}`,
171
166
  )
172
167
  }
173
168
  }
169
+
170
+ type PathnameParam = Extract<RoutePattern['pathname']['tokens'][number], { type: ':' | '*' }>
171
+
172
+ function getPathnameParams(pattern: RoutePattern): Array<PathnameParam> {
173
+ return pattern.pathname.tokens.filter(
174
+ (token): token is PathnameParam => token.type === ':' || token.type === '*',
175
+ )
176
+ }
@@ -34,6 +34,7 @@ import { createTsconfigTransformOptionsResolver, transformModule } from './trans
34
34
  import type { ResolveModuleResult, TransformArgs, TransformedModule } from './transform.ts'
35
35
  import { ResolverFactory } from 'oxc-resolver'
36
36
  import type { EmittedAsset, EmittedModule } from './emit.ts'
37
+ import { isBareImportSpecifier } from './specifiers.ts'
37
38
 
38
39
  type ScriptRecord = ModuleRecord<TransformedModule, ResolvedModule, EmittedModule>
39
40
  type ScriptStore = ModuleStore<TransformedModule, ResolvedModule, EmittedModule>
@@ -66,6 +67,7 @@ type ScriptCompilerOptions = {
66
67
  external: string[]
67
68
  fingerprintAssets: boolean
68
69
  isAllowed(absolutePath: string): boolean
70
+ isDenied(absolutePath: string): boolean
69
71
  minify: boolean
70
72
  onWatchDirectoriesChange?: (delta: { add: string[]; remove: string[] }) => void
71
73
  rootDir: string
@@ -117,8 +119,14 @@ export function createScriptCompiler(options: ScriptCompilerOptions): ScriptComp
117
119
  extensionAlias: resolverExtensionAlias,
118
120
  extensions: resolverExtensions,
119
121
  mainFields: ['browser', 'module', 'main'],
122
+ symlinks: false,
120
123
  tsconfig: 'auto',
121
124
  })
125
+ let resolveModulePathOptions = {
126
+ isAllowed: resolvedOptions.isAllowed,
127
+ isDenied: resolvedOptions.isDenied,
128
+ routes: resolvedOptions.routes,
129
+ }
122
130
  let resolveInFlightByCacheKey = new Map<string, Promise<ResolvedModule>>()
123
131
  let emitInFlightByCacheKey = new Map<string, Promise<EmittedModule>>()
124
132
 
@@ -138,7 +146,9 @@ export function createScriptCompiler(options: ScriptCompilerOptions): ScriptComp
138
146
  let resolveArgs: ResolveArgs = {
139
147
  isAllowed: resolvedOptions.isAllowed,
140
148
  isWatchIgnored,
141
- resolveModulePath,
149
+ resolveModulePath(absolutePath) {
150
+ return resolveModulePath(absolutePath, resolveModulePathOptions)
151
+ },
142
152
  resolverFactory,
143
153
  routes: resolvedOptions.routes,
144
154
  }
@@ -253,7 +263,7 @@ export function createScriptCompiler(options: ScriptCompilerOptions): ScriptComp
253
263
  }
254
264
 
255
265
  function resolveServedScriptOrThrow(absolutePath: string): ResolveModuleResult {
256
- let resolvedModule = resolveModulePath(absolutePath)
266
+ let resolvedModule = resolveModulePath(absolutePath, resolveModulePathOptions)
257
267
  if (!resolvedModule) {
258
268
  throw createAssetServerCompilationError(`File not found: ${absolutePath}`, {
259
269
  code: 'FILE_NOT_FOUND',
@@ -548,11 +558,19 @@ function shouldClearResolverCacheForFileEvent(filePath: string, event: ModuleWat
548
558
  return event !== 'change' || isPackageJsonPath(filePath) || isTsconfigPath(filePath)
549
559
  }
550
560
 
551
- function resolveModulePath(absolutePath: string): ResolveModuleResult | null {
561
+ function resolveModulePath(
562
+ absolutePath: string,
563
+ options: {
564
+ isAllowed(absolutePath: string): boolean
565
+ isDenied(absolutePath: string): boolean
566
+ routes: CompiledRoutes
567
+ },
568
+ ): ResolveModuleResult | null {
569
+ let candidateIdentityPath = normalizeFilePath(absolutePath)
552
570
  let resolvedPath: string
553
571
 
554
572
  try {
555
- resolvedPath = normalizeFilePath(fs.realpathSync(normalizeFilePath(absolutePath)))
573
+ resolvedPath = normalizeFilePath(fs.realpathSync(candidateIdentityPath))
556
574
  } catch (error) {
557
575
  if (isNoEntityError(error)) return null
558
576
  throw error
@@ -563,11 +581,32 @@ function resolveModulePath(absolutePath: string): ResolveModuleResult | null {
563
581
  }
564
582
 
565
583
  return {
566
- identityPath: resolvedPath,
584
+ identityPath: getModuleIdentityPath(candidateIdentityPath, resolvedPath, options),
567
585
  resolvedPath,
568
586
  }
569
587
  }
570
588
 
589
+ function getModuleIdentityPath(
590
+ candidateIdentityPath: string,
591
+ resolvedPath: string,
592
+ options: {
593
+ isAllowed(absolutePath: string): boolean
594
+ isDenied(absolutePath: string): boolean
595
+ routes: CompiledRoutes
596
+ },
597
+ ): string {
598
+ if (candidateIdentityPath === resolvedPath) return resolvedPath
599
+ if (!containsNodeModulesPathSegment(candidateIdentityPath)) return resolvedPath
600
+ if (!options.routes.toUrlPathname(candidateIdentityPath)) return resolvedPath
601
+ if (!options.isAllowed(candidateIdentityPath)) return resolvedPath
602
+ if (options.isDenied(resolvedPath)) return resolvedPath
603
+ return candidateIdentityPath
604
+ }
605
+
606
+ function containsNodeModulesPathSegment(filePath: string): boolean {
607
+ return filePath.split('/').includes('node_modules')
608
+ }
609
+
571
610
  function resolveActualPath(identityPath: string): string | null {
572
611
  try {
573
612
  return normalizeFilePath(fs.realpathSync(identityPath))
@@ -577,18 +616,6 @@ function resolveActualPath(identityPath: string): string | null {
577
616
  }
578
617
  }
579
618
 
580
- function isBareImportSpecifier(specifier: string): boolean {
581
- return (
582
- !specifier.startsWith('./') &&
583
- !specifier.startsWith('../') &&
584
- !specifier.startsWith('/') &&
585
- !specifier.startsWith('file:') &&
586
- !specifier.startsWith('data:') &&
587
- !specifier.startsWith('http://') &&
588
- !specifier.startsWith('https://')
589
- )
590
- }
591
-
592
619
  function isNoEntityError(
593
620
  error: unknown,
594
621
  ): error is NodeJS.ErrnoException & { code: 'ENOENT' | 'ENOTDIR' } {
@@ -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
 
@@ -91,6 +92,11 @@ type NormalizedSpecifierResolution = {
91
92
  specifier: string
92
93
  }
93
94
 
95
+ type SpecifierResolutionImporter = {
96
+ identityPath: string
97
+ resolvedPath: string
98
+ }
99
+
94
100
  export async function resolveModule(
95
101
  record: ScriptRecord,
96
102
  transformed: TransformedModule,
@@ -105,7 +111,10 @@ export async function resolveModule(
105
111
  transformed.unresolvedImports.length > 0
106
112
  ? await batchResolveSpecifiers(
107
113
  getUniqueSpecifiers(transformed.unresolvedImports),
108
- transformed.resolvedPath,
114
+ {
115
+ identityPath: transformed.identityPath,
116
+ resolvedPath: transformed.resolvedPath,
117
+ },
109
118
  args.resolverFactory,
110
119
  )
111
120
  : new Map<string, ResolvedSpec>()
@@ -196,8 +205,10 @@ export async function resolveModule(
196
205
  deps.add(resolvedImport.identityPath)
197
206
 
198
207
  if (transformed.packageSpecifiers.includes(unresolved.specifier)) {
199
- let packageJsonPath =
200
- resolvedSpec.packageJsonPath ?? findNearestPackageJsonPath(resolvedImport.resolvedPath)
208
+ let packageJsonPath = resolvePackageJsonPath(
209
+ resolvedSpec.packageJsonPath,
210
+ resolvedImport.resolvedPath,
211
+ )
201
212
  if (packageJsonPath && !args.isWatchIgnored(packageJsonPath)) {
202
213
  trackedFiles.add(packageJsonPath)
203
214
  }
@@ -235,6 +246,25 @@ export async function resolveModule(
235
246
  }
236
247
  }
237
248
 
249
+ function resolvePackageJsonPath(
250
+ packageJsonPath: string | null,
251
+ resolvedPath: string,
252
+ ): string | null {
253
+ return (
254
+ (packageJsonPath ? resolveExistingPath(packageJsonPath) : null) ??
255
+ findNearestPackageJsonPath(resolvedPath)
256
+ )
257
+ }
258
+
259
+ function resolveExistingPath(filePath: string): string | null {
260
+ try {
261
+ return normalizeFilePath(fs.realpathSync(filePath))
262
+ } catch (error) {
263
+ if (isNoEntityError(error)) return null
264
+ throw error
265
+ }
266
+ }
267
+
238
268
  function findNearestPackageJsonPath(filePath: string): string | null {
239
269
  let directory = path.dirname(filePath)
240
270
 
@@ -250,6 +280,17 @@ function findNearestPackageJsonPath(filePath: string): string | null {
250
280
  }
251
281
  }
252
282
 
283
+ function isNoEntityError(
284
+ error: unknown,
285
+ ): error is NodeJS.ErrnoException & { code: 'ENOENT' | 'ENOTDIR' } {
286
+ return (
287
+ error instanceof Error &&
288
+ 'code' in error &&
289
+ ((error as NodeJS.ErrnoException).code === 'ENOENT' ||
290
+ (error as NodeJS.ErrnoException).code === 'ENOTDIR')
291
+ )
292
+ }
293
+
253
294
  function isRelativeImportSpecifier(specifier: string): boolean {
254
295
  return specifier.startsWith('./') || specifier.startsWith('../')
255
296
  }
@@ -326,7 +367,7 @@ function resolveCandidateBasePath(importerDir: string, specifier: string): strin
326
367
 
327
368
  async function batchResolveSpecifiers(
328
369
  specifiers: string[],
329
- importerPath: string,
370
+ importer: SpecifierResolutionImporter,
330
371
  resolverFactory: ResolveArgs['resolverFactory'],
331
372
  ): Promise<Map<string, ResolvedSpec>> {
332
373
  let resolvedBySpecifier = new Map<string, ResolvedSpec>()
@@ -334,7 +375,7 @@ async function batchResolveSpecifiers(
334
375
 
335
376
  try {
336
377
  for (let specifier of specifiers) {
337
- let normalizedResolution = normalizeSpecifierResolution(specifier, importerPath)
378
+ let normalizedResolution = normalizeSpecifierResolution(specifier, importer)
338
379
  let resolutionResult = await resolverFactory.resolveFileAsync(
339
380
  normalizedResolution.importerPath,
340
381
  normalizedResolution.specifier,
@@ -368,7 +409,7 @@ async function batchResolveSpecifiers(
368
409
  }
369
410
 
370
411
  throw createAssetServerCompilationError(
371
- `Failed to resolve imports in ${importerPath}. ${formatUnknownError(error)}`,
412
+ `Failed to resolve imports in ${importer.identityPath}. ${formatUnknownError(error)}`,
372
413
  {
373
414
  cause: error,
374
415
  code: 'IMPORT_RESOLUTION_FAILED',
@@ -389,12 +430,12 @@ function formatUnknownError(error: unknown): string {
389
430
 
390
431
  function normalizeSpecifierResolution(
391
432
  specifier: string,
392
- importerPath: string,
433
+ importer: SpecifierResolutionImporter,
393
434
  ): NormalizedSpecifierResolution {
394
435
  let authoredInjectedPackageSpecifier = restoreAuthoredInjectedPackageSpecifier(specifier)
395
436
  if (authoredInjectedPackageSpecifier) {
396
437
  return {
397
- importerPath,
438
+ importerPath: getSpecifierImporterPath(authoredInjectedPackageSpecifier, importer),
398
439
  specifier: authoredInjectedPackageSpecifier,
399
440
  }
400
441
  }
@@ -407,11 +448,18 @@ function normalizeSpecifierResolution(
407
448
  }
408
449
 
409
450
  return {
410
- importerPath,
451
+ importerPath: getSpecifierImporterPath(specifier, importer),
411
452
  specifier,
412
453
  }
413
454
  }
414
455
 
456
+ function getSpecifierImporterPath(
457
+ specifier: string,
458
+ importer: SpecifierResolutionImporter,
459
+ ): string {
460
+ return isBareImportSpecifier(specifier) ? importer.resolvedPath : importer.identityPath
461
+ }
462
+
415
463
  function getDisplayImportSpecifier(specifier: string): string {
416
464
  return restoreAuthoredInjectedPackageSpecifier(specifier) ?? specifier
417
465
  }
@@ -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
+ }
@@ -30,6 +30,7 @@ import { composeSourceMaps, rewriteSourceMapSources, stringifySourceMap } from '
30
30
  import type { EmittedModule } from './emit.ts'
31
31
  import type { ResolvedScriptTarget } from '../target.ts'
32
32
  import type { ResolvedModule } from './resolve.ts'
33
+ import { isBareImportSpecifier } from './specifiers.ts'
33
34
 
34
35
  type ScriptRecord = ModuleRecord<TransformedModule, ResolvedModule, EmittedModule>
35
36
 
@@ -261,9 +262,9 @@ export async function transformModule(
261
262
  content: sourceText,
262
263
  }),
263
264
  identityPath: record.identityPath,
264
- importerDir: path.dirname(resolvedPath),
265
+ importerDir: path.dirname(record.identityPath),
265
266
  packageSpecifiers: analysis.unresolvedImports
266
- .filter((unresolved) => isPackageImportSpecifier(unresolved.specifier))
267
+ .filter((unresolved) => isBareImportSpecifier(unresolved.specifier))
267
268
  .map((unresolved) => unresolved.specifier),
268
269
  rawCode: analysis.rawCode,
269
270
  resolvedPath,
@@ -299,10 +300,6 @@ function findNearestTsconfigPath(directory: string): string | null {
299
300
  }
300
301
  }
301
302
 
302
- function isPackageImportSpecifier(specifier: string): boolean {
303
- return !specifier.startsWith('./') && !specifier.startsWith('../') && !specifier.startsWith('/')
304
- }
305
-
306
303
  async function analyzeModuleSource(
307
304
  sourceText: string,
308
305
  resolvedPath: string,
@@ -44,7 +44,14 @@ type StyleGetOptions = {
44
44
  type StyleCompilerOptions = {
45
45
  buildId?: string
46
46
  fingerprintAssets: boolean
47
+ getServedFileUrl?(
48
+ identityPath: string,
49
+ options: {
50
+ transform: readonly string[] | null
51
+ },
52
+ ): Promise<string>
47
53
  isAllowed(absolutePath: string): boolean
54
+ isServedFilePath(filePath: string): boolean
48
55
  minify: boolean
49
56
  onWatchDirectoriesChange?: (delta: { add: string[]; remove: string[] }) => void
50
57
  rootDir: string
@@ -79,6 +86,7 @@ export function createStyleCompiler(options: StyleCompilerOptions): StyleCompile
79
86
  let emitInFlightByCacheKey = new Map<string, Promise<EmittedStyle>>()
80
87
  let resolveArgs: ResolveArgs = {
81
88
  isAllowed: resolvedOptions.isAllowed,
89
+ isServedFilePath: resolvedOptions.isServedFilePath,
82
90
  isWatchIgnored,
83
91
  routes: resolvedOptions.routes,
84
92
  }
@@ -248,6 +256,7 @@ export function createStyleCompiler(options: StyleCompilerOptions): StyleCompile
248
256
  let startedVersion = record.invalidationVersion
249
257
  let resolvedStyle = await getOrCreateResolvedStyle(record)
250
258
  let emitResolvedStyleResult = await emitResolvedStyle(resolvedStyle, {
259
+ getServedFileUrl: resolvedOptions.getServedFileUrl,
251
260
  getServedUrl,
252
261
  sourceMaps: resolvedOptions.sourceMaps,
253
262
  })
@@ -34,6 +34,12 @@ type EmitResult =
34
34
  export async function emitResolvedStyle(
35
35
  resolvedStyle: ResolvedStyle,
36
36
  options: {
37
+ getServedFileUrl?(
38
+ identityPath: string,
39
+ options: {
40
+ transform: readonly string[] | null
41
+ },
42
+ ): Promise<string>
37
43
  getServedUrl(identityPath: string): Promise<string>
38
44
  sourceMaps?: 'external' | 'inline'
39
45
  },
@@ -77,6 +83,12 @@ async function rewriteDependencies(
77
83
  resolvedStyle: ResolvedStyle,
78
84
  options: {
79
85
  getServedUrl(identityPath: string): Promise<string>
86
+ getServedFileUrl?(
87
+ identityPath: string,
88
+ options: {
89
+ transform: readonly string[] | null
90
+ },
91
+ ): Promise<string>
80
92
  },
81
93
  ): Promise<{ code: string; sourceMap: string | null }> {
82
94
  if (resolvedStyle.dependencies.length === 0) {
@@ -92,7 +104,14 @@ async function rewriteDependencies(
92
104
  let replacement =
93
105
  dependency.kind === 'external'
94
106
  ? dependency.replacement
95
- : `${await options.getServedUrl(dependency.depPath)}${dependency.suffix}`
107
+ : dependency.kind === 'style'
108
+ ? `${await options.getServedUrl(dependency.depPath)}${dependency.suffix}`
109
+ : appendUrlSuffix(
110
+ await getServedFileUrl(options, resolvedStyle.identityPath, dependency.depPath, {
111
+ transform: dependency.requestTransform,
112
+ }),
113
+ dependency.suffix,
114
+ )
96
115
  let start = resolvedStyle.rawCode.indexOf(dependency.placeholder)
97
116
  if (start < 0) {
98
117
  throw createAssetServerCompilationError(
@@ -117,6 +136,61 @@ async function rewriteDependencies(
117
136
  }
118
137
  }
119
138
 
139
+ function appendUrlSuffix(url: string, suffix: string): string {
140
+ if (!suffix.startsWith('?') || !url.includes('?')) {
141
+ return `${url}${suffix}`
142
+ }
143
+
144
+ return `${url}&${suffix.slice(1)}`
145
+ }
146
+
147
+ async function getServedFileUrl(
148
+ options: {
149
+ getServedFileUrl?(
150
+ identityPath: string,
151
+ options: {
152
+ transform: readonly string[] | null
153
+ },
154
+ ): Promise<string>
155
+ getServedUrl(identityPath: string): Promise<string>
156
+ },
157
+ importerPath: string,
158
+ identityPath: string,
159
+ request: {
160
+ transform: readonly string[] | null
161
+ },
162
+ ): Promise<string> {
163
+ if (!options.getServedFileUrl) {
164
+ throw createAssetServerCompilationError(`Missing file URL resolver for ${identityPath}.`, {
165
+ code: 'EMIT_FAILED',
166
+ })
167
+ }
168
+
169
+ try {
170
+ return await options.getServedFileUrl(identityPath, request)
171
+ } catch (error) {
172
+ if (
173
+ request.transform !== null &&
174
+ isAssetServerCompilationError(error) &&
175
+ error.code === 'FILE_TRANSFORM_QUERY_INVALID'
176
+ ) {
177
+ console.warn(
178
+ `Invalid file transform request "${request.transform.join(',')}" in CSS asset ${importerPath} for ${identityPath}: ${error.message}`,
179
+ )
180
+ let href = await options.getServedFileUrl(identityPath, { transform: null })
181
+ let searchParams = new URLSearchParams()
182
+ for (let transform of request.transform) {
183
+ searchParams.append('transform', transform)
184
+ }
185
+
186
+ let search = searchParams.toString()
187
+ return search.length > 0 ? `${href}?${search}` : href
188
+ }
189
+
190
+ throw error
191
+ }
192
+ }
193
+
120
194
  async function createEmittedAsset(content: string): Promise<EmittedAsset> {
121
195
  return {
122
196
  content,