@remix-run/assets 0.3.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 (61) 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 +30 -5
  30. package/dist/lib/scripts/resolve.d.ts.map +1 -1
  31. package/dist/lib/scripts/resolve.js +22 -2
  32. package/dist/lib/scripts/transform.js +1 -1
  33. package/dist/lib/styles/compiler.d.ts +4 -0
  34. package/dist/lib/styles/compiler.d.ts.map +1 -1
  35. package/dist/lib/styles/compiler.js +2 -0
  36. package/dist/lib/styles/emit.d.ts +3 -0
  37. package/dist/lib/styles/emit.d.ts.map +1 -1
  38. package/dist/lib/styles/emit.js +36 -1
  39. package/dist/lib/styles/resolve.d.ts +8 -1
  40. package/dist/lib/styles/resolve.d.ts.map +1 -1
  41. package/dist/lib/styles/resolve.js +85 -32
  42. package/dist/lib/watch.d.ts +2 -0
  43. package/dist/lib/watch.d.ts.map +1 -1
  44. package/dist/lib/watch.js +25 -8
  45. package/package.json +6 -4
  46. package/src/assets.ts +1 -0
  47. package/src/lib/access.ts +8 -1
  48. package/src/lib/asset-server.ts +273 -28
  49. package/src/lib/compilation-error.ts +9 -0
  50. package/src/lib/files/compiler.ts +885 -0
  51. package/src/lib/files/config.ts +479 -0
  52. package/src/lib/files/store.ts +109 -0
  53. package/src/lib/fingerprint.ts +8 -7
  54. package/src/lib/routes.ts +36 -33
  55. package/src/lib/scripts/compiler.ts +43 -5
  56. package/src/lib/scripts/resolve.ts +35 -3
  57. package/src/lib/scripts/transform.ts +1 -1
  58. package/src/lib/styles/compiler.ts +9 -0
  59. package/src/lib/styles/emit.ts +75 -1
  60. package/src/lib/styles/resolve.ts +132 -35
  61. package/src/lib/watch.ts +34 -12
@@ -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)