@remix-run/assets 0.2.0 → 0.4.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 (71) hide show
  1. package/README.md +241 -28
  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 +10 -1
  8. package/dist/lib/asset-server.d.ts +33 -7
  9. package/dist/lib/asset-server.d.ts.map +1 -1
  10. package/dist/lib/asset-server.js +195 -23
  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/injected-packages.d.ts +11 -0
  26. package/dist/lib/injected-packages.d.ts.map +1 -0
  27. package/dist/lib/injected-packages.js +86 -0
  28. package/dist/lib/paths.d.ts +1 -0
  29. package/dist/lib/paths.d.ts.map +1 -1
  30. package/dist/lib/paths.js +12 -0
  31. package/dist/lib/routes.d.ts +5 -7
  32. package/dist/lib/routes.d.ts.map +1 -1
  33. package/dist/lib/routes.js +41 -36
  34. package/dist/lib/scripts/compiler.d.ts +1 -0
  35. package/dist/lib/scripts/compiler.d.ts.map +1 -1
  36. package/dist/lib/scripts/compiler.js +30 -5
  37. package/dist/lib/scripts/resolve.d.ts.map +1 -1
  38. package/dist/lib/scripts/resolve.js +57 -10
  39. package/dist/lib/scripts/transform.d.ts.map +1 -1
  40. package/dist/lib/scripts/transform.js +79 -3
  41. package/dist/lib/styles/compiler.d.ts +4 -0
  42. package/dist/lib/styles/compiler.d.ts.map +1 -1
  43. package/dist/lib/styles/compiler.js +2 -0
  44. package/dist/lib/styles/emit.d.ts +3 -0
  45. package/dist/lib/styles/emit.d.ts.map +1 -1
  46. package/dist/lib/styles/emit.js +36 -1
  47. package/dist/lib/styles/resolve.d.ts +8 -1
  48. package/dist/lib/styles/resolve.d.ts.map +1 -1
  49. package/dist/lib/styles/resolve.js +85 -32
  50. package/dist/lib/watch.d.ts +2 -0
  51. package/dist/lib/watch.d.ts.map +1 -1
  52. package/dist/lib/watch.js +25 -8
  53. package/package.json +11 -5
  54. package/src/assets.ts +1 -0
  55. package/src/lib/access.ts +10 -1
  56. package/src/lib/asset-server.ts +297 -34
  57. package/src/lib/compilation-error.ts +9 -0
  58. package/src/lib/files/compiler.ts +885 -0
  59. package/src/lib/files/config.ts +479 -0
  60. package/src/lib/files/store.ts +109 -0
  61. package/src/lib/fingerprint.ts +8 -7
  62. package/src/lib/injected-packages.ts +117 -0
  63. package/src/lib/paths.ts +28 -0
  64. package/src/lib/routes.ts +67 -55
  65. package/src/lib/scripts/compiler.ts +43 -5
  66. package/src/lib/scripts/resolve.ts +89 -11
  67. package/src/lib/scripts/transform.ts +105 -3
  68. package/src/lib/styles/compiler.ts +9 -0
  69. package/src/lib/styles/emit.ts +75 -1
  70. package/src/lib/styles/resolve.ts +132 -35
  71. package/src/lib/watch.ts +34 -12
@@ -2,10 +2,13 @@ import * as fs from 'node:fs'
2
2
  import * as fsp from 'node:fs/promises'
3
3
  import * as path from 'node:path'
4
4
  import { getTsconfig } from 'get-tsconfig'
5
+ import MagicString from 'magic-string'
5
6
  import { minify } from 'oxc-minify'
7
+ import { parseSync, visitorKeys } from 'oxc-parser'
6
8
  import { transform as oxcTransform } from 'oxc-transform'
7
9
  import { init as esModuleLexerInit, parse as esModuleLexer } from 'es-module-lexer'
8
10
  import type { Cache, TsConfigJsonResolved } from 'get-tsconfig'
11
+ import type { Node, Program } from 'oxc-parser'
9
12
  import type { TransformOptions as OxcTransformOptions } from 'oxc-transform'
10
13
 
11
14
  import { isCommonJS, mayContainCommonJSModuleGlobals } from './cjs-check.ts'
@@ -15,6 +18,11 @@ import {
15
18
  } from '../compilation-error.ts'
16
19
  import type { AssetServerCompilationError } from '../compilation-error.ts'
17
20
  import { generateFingerprint } from '../fingerprint.ts'
21
+ import {
22
+ maskAuthoredInjectedPackageSpecifier,
23
+ mayContainInjectedPackageSpecifier,
24
+ restoreAuthoredInjectedPackageSpecifier,
25
+ } from '../injected-packages.ts'
18
26
  import type { ModuleRecord, ModuleTracking } from '../module-store.ts'
19
27
  import { normalizeFilePath } from '../paths.ts'
20
28
  import type { CompiledRoutes } from '../routes.ts'
@@ -205,7 +213,7 @@ export async function transformModule(
205
213
  })
206
214
 
207
215
  analysis.unresolvedImports = analysis.unresolvedImports.filter(
208
- (unresolved) => !args.externalSet.has(unresolved.specifier),
216
+ (unresolved) => !args.externalSet.has(getDisplayImportSpecifier(unresolved.specifier)),
209
217
  )
210
218
 
211
219
  if (mayContainCommonJSModuleGlobals(sourceText) && isCommonJS(analysis.rawCode)) {
@@ -253,7 +261,7 @@ export async function transformModule(
253
261
  content: sourceText,
254
262
  }),
255
263
  identityPath: record.identityPath,
256
- importerDir: path.dirname(resolvedPath),
264
+ importerDir: path.dirname(record.identityPath),
257
265
  packageSpecifiers: analysis.unresolvedImports
258
266
  .filter((unresolved) => isPackageImportSpecifier(unresolved.specifier))
259
267
  .map((unresolved) => unresolved.specifier),
@@ -306,11 +314,12 @@ async function analyzeModuleSource(
306
314
  target?: ResolvedScriptTarget
307
315
  },
308
316
  ) {
317
+ let maskedSourceText = maskAuthoredInjectedPackageImports(sourceText, resolvedPath)
309
318
  let transformResult: { code: string; errors?: Array<{ message?: string }>; map?: unknown }
310
319
  try {
311
320
  transformResult = await oxcTransform(
312
321
  resolvedPath,
313
- sourceText,
322
+ maskedSourceText,
314
323
  getTransformOptions(resolvedPath, transformOptions, options),
315
324
  )
316
325
  assertNoCompilerErrors(transformResult.errors, resolvedPath, 'transform')
@@ -541,6 +550,99 @@ async function getUnresolvedImportsFromLexer(rawCode: string): Promise<Unresolve
541
550
  return unresolvedImports
542
551
  }
543
552
 
553
+ function getDisplayImportSpecifier(specifier: string): string {
554
+ return restoreAuthoredInjectedPackageSpecifier(specifier) ?? specifier
555
+ }
556
+
557
+ function maskAuthoredInjectedPackageImports(sourceText: string, resolvedPath: string): string {
558
+ if (!mayContainInjectedPackageSpecifier(sourceText)) {
559
+ return sourceText
560
+ }
561
+
562
+ let parseResult = parseSync(resolvedPath, sourceText, {
563
+ lang: getSourceLanguageForPath(resolvedPath),
564
+ sourceType: 'module',
565
+ })
566
+ if (parseResult.errors.length > 0) {
567
+ return sourceText
568
+ }
569
+
570
+ let replacements: Array<{ end: number; specifier: string; start: number }> = []
571
+
572
+ walkAst(parseResult.program, (node) => {
573
+ if (
574
+ node.type !== 'ImportDeclaration' &&
575
+ node.type !== 'ExportAllDeclaration' &&
576
+ node.type !== 'ExportNamedDeclaration' &&
577
+ node.type !== 'ImportExpression'
578
+ ) {
579
+ return
580
+ }
581
+
582
+ let source = 'source' in node ? node.source : null
583
+ if (!isStringLiteralNode(source)) return
584
+
585
+ let maskedSpecifier = maskAuthoredInjectedPackageSpecifier(source.value)
586
+ if (maskedSpecifier == null) return
587
+
588
+ replacements.push({
589
+ end: source.end - 1,
590
+ specifier: maskedSpecifier,
591
+ start: source.start + 1,
592
+ })
593
+ })
594
+
595
+ if (replacements.length === 0) return sourceText
596
+
597
+ let rewrittenSource = new MagicString(sourceText)
598
+ for (let replacement of replacements) {
599
+ rewrittenSource.overwrite(replacement.start, replacement.end, replacement.specifier)
600
+ }
601
+
602
+ return rewrittenSource.toString()
603
+ }
604
+
605
+ function walkAst(node: Program | Node, visit: (node: Program | Node) => void): void {
606
+ visit(node)
607
+
608
+ let keys = visitorKeys[node.type]
609
+ if (!keys) return
610
+
611
+ let walkableNode = node as unknown as Record<string, unknown>
612
+ for (let key of keys) {
613
+ let value = walkableNode[key]
614
+ if (Array.isArray(value)) {
615
+ for (let child of value) {
616
+ if (isAstNode(child)) {
617
+ walkAst(child, visit)
618
+ }
619
+ }
620
+ continue
621
+ }
622
+
623
+ if (isAstNode(value)) {
624
+ walkAst(value, visit)
625
+ }
626
+ }
627
+ }
628
+
629
+ function isAstNode(value: unknown): value is Node {
630
+ return typeof value === 'object' && value !== null && 'type' in value
631
+ }
632
+
633
+ function isStringLiteralNode(node: Node | null | undefined): node is Node & {
634
+ end: number
635
+ start: number
636
+ value: string
637
+ } {
638
+ return (
639
+ node?.type === 'Literal' &&
640
+ typeof node.start === 'number' &&
641
+ typeof node.end === 'number' &&
642
+ typeof node.value === 'string'
643
+ )
644
+ }
645
+
544
646
  function getStaticImportSpecifier(
545
647
  source: string,
546
648
  imported: ReturnType<typeof esModuleLexer>[0][number],
@@ -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,
@@ -1,5 +1,6 @@
1
1
  import * as fs from 'node:fs'
2
2
  import * as path from 'node:path'
3
+ import { fileURLToPath, pathToFileURL } from 'node:url'
3
4
 
4
5
  import {
5
6
  createAssetServerCompilationError,
@@ -22,10 +23,17 @@ type ResolvedDependency =
22
23
  }
23
24
  | {
24
25
  depPath: string
25
- kind: 'local'
26
+ kind: 'style'
26
27
  placeholder: string
27
28
  suffix: string
28
29
  }
30
+ | {
31
+ depPath: string
32
+ kind: 'file'
33
+ placeholder: string
34
+ requestTransform: readonly string[] | null
35
+ suffix: string
36
+ }
29
37
 
30
38
  export type ResolvedStyle = {
31
39
  dependencies: ResolvedDependency[]
@@ -41,6 +49,7 @@ export type ResolvedStyle = {
41
49
 
42
50
  export type ResolveArgs = {
43
51
  isAllowed(absolutePath: string): boolean
52
+ isServedFilePath(filePath: string): boolean
44
53
  isWatchIgnored(filePath: string): boolean
45
54
  routes: CompiledRoutes
46
55
  }
@@ -58,6 +67,12 @@ type ResolveResult = {
58
67
  }
59
68
  )
60
69
 
70
+ type ParsedRelativeUrl = {
71
+ hash: string
72
+ resolvedPath: string
73
+ search: string
74
+ }
75
+
61
76
  export async function resolveStyle(
62
77
  record: StyleRecord,
63
78
  transformed: TransformedStyle,
@@ -71,7 +86,7 @@ export async function resolveStyle(
71
86
  let trackedFile =
72
87
  unresolved.type === 'import'
73
88
  ? getTrackedImportFilePath(unresolved.url, transformed.resolvedPath)
74
- : null
89
+ : getTrackedUrlFilePath(unresolved.url, transformed.resolvedPath)
75
90
  if (trackedFile && !args.isWatchIgnored(trackedFile)) {
76
91
  trackedFiles.add(trackedFile)
77
92
  }
@@ -85,14 +100,22 @@ export async function resolveStyle(
85
100
  unresolved.placeholder,
86
101
  args,
87
102
  )
88
- : resolveUrlDependency(unresolved.url, unresolved.placeholder)
103
+ : resolveUrlDependency(
104
+ unresolved.url,
105
+ transformed.resolvedPath,
106
+ unresolved.placeholder,
107
+ args,
108
+ )
89
109
 
90
110
  dependencies.push(resolved)
91
111
 
92
- if (resolved.kind === 'local') {
112
+ if (resolved.kind === 'style' || resolved.kind === 'file') {
93
113
  if (!args.isWatchIgnored(resolved.depPath)) {
94
114
  trackedFiles.add(resolved.depPath)
95
115
  }
116
+ }
117
+
118
+ if (resolved.kind === 'style') {
96
119
  deps.add(resolved.depPath)
97
120
  }
98
121
  } catch (error) {
@@ -172,16 +195,8 @@ function resolveImportDependency(
172
195
  }
173
196
  }
174
197
 
175
- let { pathname, suffix } = splitUrlSuffix(url)
176
- if (pathname.length === 0 || pathname === '#') {
177
- return {
178
- kind: 'external',
179
- placeholder,
180
- replacement: url,
181
- }
182
- }
183
-
184
- if (pathname.startsWith('/')) {
198
+ let parsedUrl = parseRelativeUrl(url, importerPath)
199
+ if (parsedUrl === null) {
185
200
  return {
186
201
  kind: 'external',
187
202
  placeholder,
@@ -189,7 +204,8 @@ function resolveImportDependency(
189
204
  }
190
205
  }
191
206
 
192
- let resolvedFilePath = normalizeFilePath(path.resolve(path.dirname(importerPath), pathname))
207
+ let { hash, resolvedPath, search } = parsedUrl
208
+ let resolvedFilePath = resolvedPath
193
209
  let identityPath = resolveExistingFilePath(resolvedFilePath)
194
210
  if (!identityPath || !isStyleFilePath(identityPath)) {
195
211
  throw createAssetServerCompilationError(
@@ -222,17 +238,82 @@ function resolveImportDependency(
222
238
 
223
239
  return {
224
240
  depPath: identityPath,
225
- kind: 'local',
241
+ kind: 'style',
226
242
  placeholder,
227
- suffix,
243
+ suffix: `${search}${hash}`,
228
244
  }
229
245
  }
230
246
 
231
- function resolveUrlDependency(url: string, placeholder: string): ResolvedDependency {
247
+ function resolveUrlDependency(
248
+ url: string,
249
+ importerPath: string,
250
+ placeholder: string,
251
+ args: ResolveArgs,
252
+ ): ResolvedDependency {
253
+ if (isExternalUrl(url)) {
254
+ return {
255
+ kind: 'external',
256
+ placeholder,
257
+ replacement: url,
258
+ }
259
+ }
260
+
261
+ let parsedUrl = parseRelativeUrl(url, importerPath)
262
+ if (parsedUrl === null) {
263
+ return {
264
+ kind: 'external',
265
+ placeholder,
266
+ replacement: url,
267
+ }
268
+ }
269
+
270
+ let { hash, resolvedPath, search } = parsedUrl
271
+ let resolvedFilePath = resolvedPath
272
+ let identityPath = resolveExistingFilePath(resolvedFilePath)
273
+ if (!identityPath) {
274
+ throw createAssetServerCompilationError(`Failed to resolve url("${url}") in ${importerPath}.`, {
275
+ code: 'URL_RESOLUTION_FAILED',
276
+ })
277
+ }
278
+
279
+ if (!args.isServedFilePath(identityPath)) {
280
+ throw createAssetServerCompilationError(
281
+ `URL "${url}" in ${importerPath}, resolved to "${identityPath}", is not a supported file asset. ` +
282
+ `Add the file extension to files.extensions to serve this asset.`,
283
+ {
284
+ code: 'URL_NOT_SUPPORTED',
285
+ },
286
+ )
287
+ }
288
+
289
+ if (!args.isAllowed(identityPath)) {
290
+ 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.`,
293
+ {
294
+ code: 'URL_NOT_ALLOWED',
295
+ },
296
+ )
297
+ }
298
+
299
+ if (!args.routes.toUrlPathname(identityPath)) {
300
+ throw createAssetServerCompilationError(
301
+ `URL "${url}" in ${importerPath}, resolved to "${identityPath}", is outside all configured fileMap entries. ` +
302
+ `Add a matching fileMap entry for this file path.`,
303
+ {
304
+ code: 'URL_OUTSIDE_FILE_MAP',
305
+ },
306
+ )
307
+ }
308
+
309
+ let parsedRequest = parseResolvedFileRequest({ hash, search })
310
+
232
311
  return {
233
- kind: 'external',
312
+ depPath: identityPath,
313
+ kind: 'file',
234
314
  placeholder,
235
- replacement: url,
315
+ requestTransform: parsedRequest.transform,
316
+ suffix: parsedRequest.suffix,
236
317
  }
237
318
  }
238
319
 
@@ -245,34 +326,50 @@ function resolveExistingFilePath(filePath: string): string | null {
245
326
  }
246
327
  }
247
328
 
248
- function splitUrlSuffix(url: string): {
249
- pathname: string
329
+ function parseRelativeUrl(url: string, importerPath: string): ParsedRelativeUrl | null {
330
+ if (url.length === 0 || url.startsWith('?') || url.startsWith('/') || isExternalUrl(url)) {
331
+ return null
332
+ }
333
+
334
+ let parsed = new URL(url, pathToFileURL(importerPath))
335
+
336
+ return {
337
+ hash: parsed.hash,
338
+ resolvedPath: normalizeFilePath(fileURLToPath(parsed)),
339
+ search: parsed.search,
340
+ }
341
+ }
342
+
343
+ function parseResolvedFileRequest(url: Pick<ParsedRelativeUrl, 'hash' | 'search'>): {
250
344
  suffix: string
345
+ transform: readonly string[] | null
251
346
  } {
252
- let queryIndex = url.indexOf('?')
253
- let hashIndex = url.indexOf('#')
254
- let endIndex = [queryIndex, hashIndex].filter((index) => index >= 0).sort((a, b) => a - b)[0]
347
+ let { hash, search } = url
255
348
 
256
- if (endIndex == null) {
349
+ if (search.length === 0) {
257
350
  return {
258
- pathname: url,
259
- suffix: '',
351
+ suffix: hash,
352
+ transform: null,
260
353
  }
261
354
  }
262
355
 
356
+ let searchParams = new URLSearchParams(search.slice(1))
357
+ let transform = searchParams.getAll('transform')
358
+ searchParams.delete('transform')
359
+ let remainingSearch = searchParams.toString()
360
+
263
361
  return {
264
- pathname: url.slice(0, endIndex),
265
- suffix: url.slice(endIndex),
362
+ suffix: `${remainingSearch.length > 0 ? `?${remainingSearch}` : ''}${hash}`,
363
+ transform: transform.length > 0 ? transform : null,
266
364
  }
267
365
  }
268
366
 
269
367
  function getTrackedImportFilePath(specifier: string, importerPath: string): string | null {
270
- let { pathname } = splitUrlSuffix(specifier)
271
- if (pathname.startsWith('./') || pathname.startsWith('../')) {
272
- return normalizeFilePath(path.resolve(path.dirname(importerPath), pathname))
273
- }
368
+ return parseRelativeUrl(specifier, importerPath)?.resolvedPath ?? null
369
+ }
274
370
 
275
- return null
371
+ function getTrackedUrlFilePath(url: string, importerPath: string): string | null {
372
+ return parseRelativeUrl(url, importerPath)?.resolvedPath ?? null
276
373
  }
277
374
 
278
375
  function isStyleFilePath(filePath: string): boolean {
package/src/lib/watch.ts CHANGED
@@ -17,7 +17,10 @@ export type ChokidarWatcher = ReturnType<typeof chokidar.watch>
17
17
  export type AssetServerWatcher = {
18
18
  close(): Promise<void>
19
19
  getWatchedTargets(): readonly string[]
20
- updateWatchedDirectories(delta: { add: readonly string[]; remove: readonly string[] }): void
20
+ updateWatchedDirectories(
21
+ delta: { add: readonly string[]; remove: readonly string[] },
22
+ options?: { includeAncestors?: boolean },
23
+ ): void
21
24
  }
22
25
 
23
26
  export function createAssetServerWatcher(options: AssetServerWatcherOptions): AssetServerWatcher {
@@ -27,7 +30,8 @@ export function createAssetServerWatcher(options: AssetServerWatcherOptions): As
27
30
  ...resolveChokidarWatchOptions(options),
28
31
  })
29
32
  options.onChokidarWatcherCreated?.(watcher)
30
- let watchedDirectories = new Set<string>()
33
+ let watchedResolutionDirectories = new Set<string>()
34
+ let watchedFileDirectories = new Set<string>()
31
35
  let watchedTargets = new Set<string>()
32
36
 
33
37
  for (let event of ['add', 'change', 'unlink'] as const) {
@@ -46,8 +50,12 @@ export function createAssetServerWatcher(options: AssetServerWatcherOptions): As
46
50
  getWatchedTargets() {
47
51
  return [...watchedTargets]
48
52
  },
49
- updateWatchedDirectories(delta) {
50
- let nextWatchedDirectories = new Set(watchedDirectories)
53
+ updateWatchedDirectories(delta, updateOptions = {}) {
54
+ let includeAncestors = updateOptions.includeAncestors ?? true
55
+ let previousDirectories = includeAncestors
56
+ ? watchedResolutionDirectories
57
+ : watchedFileDirectories
58
+ let nextWatchedDirectories = new Set(previousDirectories)
51
59
 
52
60
  for (let directory of delta.add) {
53
61
  nextWatchedDirectories.add(directory)
@@ -56,7 +64,17 @@ export function createAssetServerWatcher(options: AssetServerWatcherOptions): As
56
64
  nextWatchedDirectories.delete(directory)
57
65
  }
58
66
 
59
- let nextTargets = getWatchTargetsForDirectories(options.rootDir, [...nextWatchedDirectories])
67
+ if (includeAncestors) {
68
+ watchedResolutionDirectories = nextWatchedDirectories
69
+ } else {
70
+ watchedFileDirectories = nextWatchedDirectories
71
+ }
72
+
73
+ let nextTargets = getWatchTargets({
74
+ rootDir: options.rootDir,
75
+ fileDirectories: [...watchedFileDirectories],
76
+ resolutionDirectories: [...watchedResolutionDirectories],
77
+ })
60
78
  let targetsToAdd = [...nextTargets].filter((target) => !watchedTargets.has(target))
61
79
  let targetsToRemove = [...watchedTargets].filter((target) => !nextTargets.has(target))
62
80
 
@@ -67,7 +85,6 @@ export function createAssetServerWatcher(options: AssetServerWatcherOptions): As
67
85
  watcher.add(targetsToAdd)
68
86
  }
69
87
 
70
- watchedDirectories = nextWatchedDirectories
71
88
  watchedTargets = nextTargets
72
89
  },
73
90
  }
@@ -88,15 +105,20 @@ function resolveChokidarWatchOptions(
88
105
  }
89
106
  }
90
107
 
91
- function getWatchTargetsForDirectories(
92
- rootDir: string,
93
- directories: readonly string[],
94
- ): Set<string> {
95
- let normalizedRootDir = normalizeFilePath(rootDir)
108
+ function getWatchTargets(options: {
109
+ fileDirectories: readonly string[]
110
+ resolutionDirectories: readonly string[]
111
+ rootDir: string
112
+ }): Set<string> {
113
+ let normalizedRootDir = normalizeFilePath(options.rootDir)
96
114
  let targets = new Set<string>()
97
115
  let configAncestors = new Set<string>()
98
116
 
99
- for (let directory of directories) {
117
+ for (let directory of options.fileDirectories) {
118
+ targets.add(normalizeFilePath(directory).replace(/\/+$/, ''))
119
+ }
120
+
121
+ for (let directory of options.resolutionDirectories) {
100
122
  let normalizedDirectory = normalizeFilePath(directory).replace(/\/+$/, '')
101
123
 
102
124
  targets.add(normalizedDirectory)