@remix-run/assets 0.0.0 → 0.2.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 (87) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +325 -2
  3. package/dist/assets.d.ts +3 -0
  4. package/dist/assets.d.ts.map +1 -0
  5. package/dist/assets.js +1 -0
  6. package/dist/lib/access.d.ts +10 -0
  7. package/dist/lib/access.d.ts.map +1 -0
  8. package/dist/lib/access.js +14 -0
  9. package/dist/lib/asset-server.d.ts +139 -0
  10. package/dist/lib/asset-server.d.ts.map +1 -0
  11. package/dist/lib/asset-server.js +338 -0
  12. package/dist/lib/compilation-error.d.ts +33 -0
  13. package/dist/lib/compilation-error.d.ts.map +1 -0
  14. package/dist/lib/compilation-error.js +32 -0
  15. package/dist/lib/file-matcher.d.ts +6 -0
  16. package/dist/lib/file-matcher.d.ts.map +1 -0
  17. package/dist/lib/file-matcher.js +44 -0
  18. package/dist/lib/fingerprint.d.ts +12 -0
  19. package/dist/lib/fingerprint.d.ts.map +1 -0
  20. package/dist/lib/fingerprint.js +49 -0
  21. package/dist/lib/module-store.d.ts +41 -0
  22. package/dist/lib/module-store.d.ts.map +1 -0
  23. package/dist/lib/module-store.js +230 -0
  24. package/dist/lib/paths.d.ts +8 -0
  25. package/dist/lib/paths.d.ts.map +1 -0
  26. package/dist/lib/paths.js +50 -0
  27. package/dist/lib/routes.d.ts +13 -0
  28. package/dist/lib/routes.d.ts.map +1 -0
  29. package/dist/lib/routes.js +94 -0
  30. package/dist/lib/scripts/cjs-check.d.ts +3 -0
  31. package/dist/lib/scripts/cjs-check.d.ts.map +1 -0
  32. package/dist/lib/scripts/cjs-check.js +398 -0
  33. package/dist/lib/scripts/compiler.d.ts +62 -0
  34. package/dist/lib/scripts/compiler.d.ts.map +1 -0
  35. package/dist/lib/scripts/compiler.js +439 -0
  36. package/dist/lib/scripts/emit.d.ts +25 -0
  37. package/dist/lib/scripts/emit.d.ts.map +1 -0
  38. package/dist/lib/scripts/emit.js +63 -0
  39. package/dist/lib/scripts/resolve.d.ts +50 -0
  40. package/dist/lib/scripts/resolve.d.ts.map +1 -0
  41. package/dist/lib/scripts/resolve.js +236 -0
  42. package/dist/lib/scripts/transform.d.ts +64 -0
  43. package/dist/lib/scripts/transform.d.ts.map +1 -0
  44. package/dist/lib/scripts/transform.js +373 -0
  45. package/dist/lib/source-maps.d.ts +4 -0
  46. package/dist/lib/source-maps.d.ts.map +1 -0
  47. package/dist/lib/source-maps.js +62 -0
  48. package/dist/lib/styles/compiler.d.ts +52 -0
  49. package/dist/lib/styles/compiler.d.ts.map +1 -0
  50. package/dist/lib/styles/compiler.js +272 -0
  51. package/dist/lib/styles/emit.d.ts +25 -0
  52. package/dist/lib/styles/emit.d.ts.map +1 -0
  53. package/dist/lib/styles/emit.js +78 -0
  54. package/dist/lib/styles/resolve.d.ts +48 -0
  55. package/dist/lib/styles/resolve.d.ts.map +1 -0
  56. package/dist/lib/styles/resolve.js +188 -0
  57. package/dist/lib/styles/transform.d.ts +47 -0
  58. package/dist/lib/styles/transform.d.ts.map +1 -0
  59. package/dist/lib/styles/transform.js +131 -0
  60. package/dist/lib/target.d.ts +21 -0
  61. package/dist/lib/target.d.ts.map +1 -0
  62. package/dist/lib/target.js +127 -0
  63. package/dist/lib/watch.d.ts +22 -0
  64. package/dist/lib/watch.d.ts.map +1 -0
  65. package/dist/lib/watch.js +96 -0
  66. package/package.json +55 -12
  67. package/src/assets.ts +2 -0
  68. package/src/lib/access.ts +24 -0
  69. package/src/lib/asset-server.ts +537 -0
  70. package/src/lib/compilation-error.ts +61 -0
  71. package/src/lib/file-matcher.ts +63 -0
  72. package/src/lib/fingerprint.ts +65 -0
  73. package/src/lib/module-store.ts +340 -0
  74. package/src/lib/paths.ts +66 -0
  75. package/src/lib/routes.ts +164 -0
  76. package/src/lib/scripts/cjs-check.ts +476 -0
  77. package/src/lib/scripts/compiler.ts +640 -0
  78. package/src/lib/scripts/emit.ts +122 -0
  79. package/src/lib/scripts/resolve.ts +433 -0
  80. package/src/lib/scripts/transform.ts +609 -0
  81. package/src/lib/source-maps.ts +75 -0
  82. package/src/lib/styles/compiler.ts +400 -0
  83. package/src/lib/styles/emit.ts +137 -0
  84. package/src/lib/styles/resolve.ts +316 -0
  85. package/src/lib/styles/transform.ts +226 -0
  86. package/src/lib/target.ts +196 -0
  87. package/src/lib/watch.ts +136 -0
@@ -0,0 +1,63 @@
1
+ import * as fs from 'node:fs'
2
+ import picomatch from 'picomatch'
3
+
4
+ import { normalizeFilePath, resolveFilePath } from './paths.ts'
5
+
6
+ export type FileMatcher = (filePath: string) => boolean
7
+
8
+ export function createFileMatcher(
9
+ pattern: string,
10
+ rootDir: string,
11
+ options: {
12
+ allowDirectories?: boolean
13
+ allowMissing?: boolean
14
+ } = {},
15
+ ): FileMatcher {
16
+ let resolvedPatternPath = resolveFilePath(rootDir, pattern)
17
+ let allowDirectories = options.allowDirectories ?? true
18
+ let allowMissing = options.allowMissing ?? true
19
+
20
+ if (!containsGlobSyntax(pattern)) {
21
+ try {
22
+ resolvedPatternPath = normalizeFilePath(fs.realpathSync(resolvedPatternPath))
23
+ } catch (error) {
24
+ if (!allowMissing || !isPathNotFoundError(error)) throw error
25
+ }
26
+
27
+ if (allowDirectories) {
28
+ try {
29
+ if (fs.statSync(resolveFilePath(rootDir, pattern)).isDirectory()) {
30
+ return (filePath) => isSameOrDescendantPath(filePath, resolvedPatternPath)
31
+ }
32
+ } catch (error) {
33
+ if (!isPathNotFoundError(error)) throw error
34
+ }
35
+ }
36
+
37
+ return (filePath) => filePath === resolvedPatternPath
38
+ }
39
+
40
+ let globMatcher = picomatch(resolvedPatternPath, { dot: true })
41
+ return (filePath) => globMatcher(filePath)
42
+ }
43
+
44
+ function isSameOrDescendantPath(filePath: string, directoryPath: string): boolean {
45
+ let normalizedDirectoryPath = directoryPath.replace(/\/+$/, '')
46
+
47
+ return filePath === normalizedDirectoryPath || filePath.startsWith(`${normalizedDirectoryPath}/`)
48
+ }
49
+
50
+ function containsGlobSyntax(pattern: string): boolean {
51
+ return /[*?[\]{}()!+@]/.test(pattern)
52
+ }
53
+
54
+ function isPathNotFoundError(
55
+ error: unknown,
56
+ ): error is NodeJS.ErrnoException & { code: 'ENOENT' | 'ENOTDIR' } {
57
+ return (
58
+ error instanceof Error &&
59
+ 'code' in error &&
60
+ ((error as NodeJS.ErrnoException).code === 'ENOENT' ||
61
+ (error as NodeJS.ErrnoException).code === 'ENOTDIR')
62
+ )
63
+ }
@@ -0,0 +1,65 @@
1
+ const fingerprintedExtensionRE = /^(.+)\.@([A-Za-z0-9_-]+)(\.[^./]+)$/
2
+ const fingerprintedBasenameRE = /^(.+)\.@([A-Za-z0-9_-]+)$/
3
+
4
+ export async function hashContent(content: string): Promise<string> {
5
+ let encoder = new TextEncoder()
6
+ let data = encoder.encode(content)
7
+ let hashBuffer = await crypto.subtle.digest('SHA-256', data)
8
+ return Buffer.from(hashBuffer).toString('base64url').slice(0, 6)
9
+ }
10
+
11
+ export async function generateFingerprint(options: {
12
+ buildId: string
13
+ content: string
14
+ }): Promise<string> {
15
+ return hashContent(JSON.stringify([options.content, options.buildId]))
16
+ }
17
+
18
+ export function parseFingerprintSuffix(pathname: string): {
19
+ pathname: string
20
+ requestedFingerprint: string | null
21
+ } {
22
+ let lastSlashIndex = pathname.lastIndexOf('/')
23
+ let directory = lastSlashIndex >= 0 ? pathname.slice(0, lastSlashIndex + 1) : ''
24
+ let basename = lastSlashIndex >= 0 ? pathname.slice(lastSlashIndex + 1) : pathname
25
+ let extensionMatch = basename.match(fingerprintedExtensionRE)
26
+
27
+ if (extensionMatch) {
28
+ return {
29
+ pathname: `${directory}${extensionMatch[1]}${extensionMatch[3]}`,
30
+ requestedFingerprint: extensionMatch[2],
31
+ }
32
+ }
33
+
34
+ let basenameMatch = basename.match(fingerprintedBasenameRE)
35
+ if (basenameMatch) {
36
+ return {
37
+ pathname: `${directory}${basenameMatch[1]}`,
38
+ requestedFingerprint: basenameMatch[2],
39
+ }
40
+ }
41
+
42
+ return {
43
+ pathname,
44
+ requestedFingerprint: null,
45
+ }
46
+ }
47
+
48
+ export function formatFingerprintedPathname(pathname: string, fingerprint: string | null): string {
49
+ if (fingerprint === null) return pathname
50
+
51
+ let lastSlashIndex = pathname.lastIndexOf('/')
52
+ let directory = lastSlashIndex >= 0 ? pathname.slice(0, lastSlashIndex + 1) : ''
53
+ let basename = lastSlashIndex >= 0 ? pathname.slice(lastSlashIndex + 1) : pathname
54
+ let lastDotIndex = basename.lastIndexOf('.')
55
+
56
+ if (lastDotIndex <= 0) {
57
+ return `${pathname}.@${fingerprint}`
58
+ }
59
+
60
+ return `${directory}${basename.slice(0, lastDotIndex)}.@${fingerprint}${basename.slice(lastDotIndex)}`
61
+ }
62
+
63
+ export function getFingerprintRequestCacheControl(requestedFingerprint: string | null): string {
64
+ return requestedFingerprint === null ? 'no-cache' : 'public, max-age=31536000, immutable'
65
+ }
@@ -0,0 +1,340 @@
1
+ import { getFilePathDirectory } from './paths.ts'
2
+
3
+ export type ModuleTracking = {
4
+ trackedFiles: readonly string[]
5
+ trackedDirectories?: readonly string[]
6
+ }
7
+
8
+ export type ModuleWatchEvent = 'change' | 'add' | 'unlink'
9
+
10
+ export type FileSnapshot = {
11
+ mtimeNs: bigint
12
+ size: bigint
13
+ }
14
+
15
+ export type ModuleSnapshot = ReadonlyMap<string, FileSnapshot>
16
+
17
+ type ModuleRecordState<transformed, resolved, emitted> = {
18
+ identityPath: string
19
+ invalidationVersion: number
20
+ transformed?: transformed
21
+ resolved?: resolved
22
+ emitted?: emitted
23
+ emittedSnapshot?: ModuleSnapshot
24
+ staleEmitted?: emitted
25
+ staleEmittedSnapshot?: ModuleSnapshot
26
+ trackedFiles: ReadonlySet<string>
27
+ trackedDirectories: ReadonlySet<string>
28
+ }
29
+
30
+ export type ModuleRecord<transformed, resolved, emitted> = Readonly<
31
+ ModuleRecordState<transformed, resolved, emitted>
32
+ >
33
+
34
+ type MutableModuleRecord<transformed, resolved, emitted> = {
35
+ identityPath: string
36
+ invalidationVersion: number
37
+ transformed?: transformed
38
+ resolved?: resolved
39
+ emitted?: emitted
40
+ emittedSnapshot?: ModuleSnapshot
41
+ staleEmitted?: emitted
42
+ staleEmittedSnapshot?: ModuleSnapshot
43
+ trackedFiles: Set<string>
44
+ trackedDirectories: Set<string>
45
+ }
46
+
47
+ export type ModuleStore<transformed, resolved, emitted> = {
48
+ get(identityPath: string): ModuleRecord<transformed, resolved, emitted>
49
+ clearTransformed(identityPath: string, tracking: readonly ModuleTracking[]): void
50
+ setTransformed(
51
+ identityPath: string,
52
+ transformed: transformed,
53
+ tracking: readonly ModuleTracking[],
54
+ ): void
55
+ setResolved(identityPath: string, resolved: resolved, tracking: readonly ModuleTracking[]): void
56
+ clearResolved(identityPath: string, tracking: readonly ModuleTracking[]): void
57
+ setEmitted(identityPath: string, emitted: emitted, snapshot: ModuleSnapshot | null): void
58
+ invalidateForFileEvent(filePath: string, event: ModuleWatchEvent): void
59
+ invalidateAll(): void
60
+ }
61
+
62
+ export function createModuleStore<transformed, resolved, emitted>(
63
+ options: {
64
+ onWatchDirectoriesChange?: (delta: { add: string[]; remove: string[] }) => void
65
+ } = {},
66
+ ): ModuleStore<transformed, resolved, emitted> {
67
+ let recordsByIdentityPath = new Map<string, MutableModuleRecord<transformed, resolved, emitted>>()
68
+ let recordsByTrackedFile = new Map<string, Set<string>>()
69
+ let watchDirectoryRefCountByPath = new Map<string, number>()
70
+
71
+ return {
72
+ get(identityPath) {
73
+ let existing = recordsByIdentityPath.get(identityPath)
74
+ if (existing) return existing
75
+
76
+ let record: MutableModuleRecord<transformed, resolved, emitted> = {
77
+ identityPath,
78
+ invalidationVersion: 0,
79
+ trackedFiles: new Set(),
80
+ trackedDirectories: new Set(),
81
+ }
82
+ recordsByIdentityPath.set(identityPath, record)
83
+ return record
84
+ },
85
+ clearTransformed(identityPath, tracking) {
86
+ let record = getOrCreateMutableRecord(identityPath)
87
+ record.transformed = undefined
88
+ record.resolved = undefined
89
+ record.emitted = undefined
90
+ record.emittedSnapshot = undefined
91
+ record.staleEmitted = undefined
92
+ record.staleEmittedSnapshot = undefined
93
+ setTracking(record, tracking)
94
+ },
95
+
96
+ setTransformed(identityPath, transformed, tracking) {
97
+ let record = getOrCreateMutableRecord(identityPath)
98
+ record.transformed = transformed
99
+ record.resolved = undefined
100
+ record.emitted = undefined
101
+ record.emittedSnapshot = undefined
102
+ record.staleEmitted = undefined
103
+ record.staleEmittedSnapshot = undefined
104
+ setTracking(record, tracking)
105
+ },
106
+
107
+ setResolved(identityPath, resolved, tracking) {
108
+ let record = getOrCreateMutableRecord(identityPath)
109
+ record.resolved = resolved
110
+ record.emitted = undefined
111
+ record.emittedSnapshot = undefined
112
+ record.staleEmitted = undefined
113
+ record.staleEmittedSnapshot = undefined
114
+ setTracking(record, tracking)
115
+ },
116
+
117
+ clearResolved(identityPath, tracking) {
118
+ let record = getOrCreateMutableRecord(identityPath)
119
+ record.resolved = undefined
120
+ record.emitted = undefined
121
+ record.emittedSnapshot = undefined
122
+ record.staleEmitted = undefined
123
+ record.staleEmittedSnapshot = undefined
124
+ setTracking(record, tracking)
125
+ },
126
+
127
+ setEmitted(identityPath, emitted, snapshot) {
128
+ let record = getOrCreateMutableRecord(identityPath)
129
+ record.emitted = emitted
130
+ record.emittedSnapshot = snapshot ?? undefined
131
+ record.staleEmitted = undefined
132
+ record.staleEmittedSnapshot = undefined
133
+ },
134
+
135
+ invalidateForFileEvent(filePath, event) {
136
+ let affected = new Set<string>(recordsByTrackedFile.get(filePath) ?? [])
137
+
138
+ if (event !== 'change') {
139
+ for (let record of recordsByIdentityPath.values()) {
140
+ if (matchesTrackedDirectory(record.trackedDirectories, filePath)) {
141
+ affected.add(record.identityPath)
142
+ }
143
+ }
144
+ }
145
+
146
+ for (let identityPath of affected) {
147
+ let record = recordsByIdentityPath.get(identityPath)
148
+ if (record) invalidateRecord(record, { retainStale: event === 'change' })
149
+ }
150
+
151
+ if (event === 'unlink') {
152
+ let deletedRecord = recordsByIdentityPath.get(filePath)
153
+ if (deletedRecord) {
154
+ clearTracking(deletedRecord)
155
+ }
156
+ }
157
+ },
158
+
159
+ invalidateAll() {
160
+ for (let record of recordsByIdentityPath.values()) {
161
+ invalidateRecord(record, { retainStale: false })
162
+ }
163
+ },
164
+ }
165
+
166
+ function getOrCreateMutableRecord(
167
+ identityPath: string,
168
+ ): MutableModuleRecord<transformed, resolved, emitted> {
169
+ let existing = recordsByIdentityPath.get(identityPath)
170
+ if (existing) return existing
171
+
172
+ let record: MutableModuleRecord<transformed, resolved, emitted> = {
173
+ identityPath,
174
+ invalidationVersion: 0,
175
+ trackedFiles: new Set(),
176
+ trackedDirectories: new Set(),
177
+ }
178
+ recordsByIdentityPath.set(identityPath, record)
179
+ return record
180
+ }
181
+
182
+ function invalidateRecord(
183
+ record: MutableModuleRecord<transformed, resolved, emitted>,
184
+ options: { retainStale: boolean },
185
+ ) {
186
+ if (!options.retainStale) {
187
+ record.staleEmitted = undefined
188
+ record.staleEmittedSnapshot = undefined
189
+ } else if (record.emitted && record.emittedSnapshot) {
190
+ record.staleEmitted = record.emitted
191
+ record.staleEmittedSnapshot = record.emittedSnapshot
192
+ } else if (!record.staleEmitted || !record.staleEmittedSnapshot) {
193
+ record.staleEmitted = undefined
194
+ record.staleEmittedSnapshot = undefined
195
+ }
196
+
197
+ record.emitted = undefined
198
+ record.emittedSnapshot = undefined
199
+ record.resolved = undefined
200
+ record.transformed = undefined
201
+ record.invalidationVersion += 1
202
+ }
203
+
204
+ function setTracking(
205
+ record: MutableModuleRecord<transformed, resolved, emitted>,
206
+ tracking: readonly ModuleTracking[],
207
+ ) {
208
+ let previousWatchedDirectories = getWatchedDirectories(record)
209
+ removeIndexes(record)
210
+
211
+ let normalizedTracking = mergeTracking(tracking)
212
+ record.trackedFiles = normalizedTracking.trackedFiles
213
+ record.trackedDirectories = normalizedTracking.trackedDirectories
214
+
215
+ for (let trackedFile of record.trackedFiles) {
216
+ addToIndexedSet(recordsByTrackedFile, trackedFile, record.identityPath)
217
+ }
218
+
219
+ let nextWatchedDirectories = getWatchedDirectories(record)
220
+ let delta = updateWatchDirectoryRefCounts(previousWatchedDirectories, nextWatchedDirectories)
221
+ emitWatchDirectoryDelta(delta)
222
+ }
223
+
224
+ function clearTracking(record: MutableModuleRecord<transformed, resolved, emitted>) {
225
+ setTracking(record, [])
226
+ }
227
+
228
+ function removeIndexes(record: MutableModuleRecord<transformed, resolved, emitted>) {
229
+ for (let trackedFile of record.trackedFiles) {
230
+ removeFromIndexedSet(recordsByTrackedFile, trackedFile, record.identityPath)
231
+ }
232
+ }
233
+
234
+ function updateWatchDirectoryRefCounts(
235
+ previousWatchedDirectories: ReadonlySet<string>,
236
+ nextWatchedDirectories: ReadonlySet<string>,
237
+ ): { add: string[]; remove: string[] } {
238
+ let add: string[] = []
239
+ let remove: string[] = []
240
+
241
+ for (let directory of previousWatchedDirectories) {
242
+ if (nextWatchedDirectories.has(directory)) continue
243
+ let previousCount = watchDirectoryRefCountByPath.get(directory)
244
+ if (!previousCount) continue
245
+ if (previousCount === 1) {
246
+ watchDirectoryRefCountByPath.delete(directory)
247
+ remove.push(directory)
248
+ } else {
249
+ watchDirectoryRefCountByPath.set(directory, previousCount - 1)
250
+ }
251
+ }
252
+
253
+ for (let directory of nextWatchedDirectories) {
254
+ if (previousWatchedDirectories.has(directory)) continue
255
+ let previousCount = watchDirectoryRefCountByPath.get(directory) ?? 0
256
+ watchDirectoryRefCountByPath.set(directory, previousCount + 1)
257
+ if (previousCount === 0) {
258
+ add.push(directory)
259
+ }
260
+ }
261
+
262
+ return { add, remove }
263
+ }
264
+
265
+ function emitWatchDirectoryDelta(delta: { add: string[]; remove: string[] }): void {
266
+ if (!options.onWatchDirectoriesChange) return
267
+ if (delta.add.length === 0 && delta.remove.length === 0) return
268
+ options.onWatchDirectoriesChange(delta)
269
+ }
270
+ }
271
+
272
+ function addToIndexedSet(map: Map<string, Set<string>>, key: string, value: string) {
273
+ let existing = map.get(key) ?? new Set<string>()
274
+ existing.add(value)
275
+ map.set(key, existing)
276
+ }
277
+
278
+ function removeFromIndexedSet(map: Map<string, Set<string>>, key: string, value: string) {
279
+ let existing = map.get(key)
280
+ if (!existing) return
281
+ existing.delete(value)
282
+ if (existing.size === 0) {
283
+ map.delete(key)
284
+ }
285
+ }
286
+
287
+ function matchesTrackedDirectory(
288
+ trackedDirectories: ReadonlySet<string>,
289
+ filePath: string,
290
+ ): boolean {
291
+ for (let trackedDirectory of trackedDirectories) {
292
+ if (filePath === trackedDirectory || filePath.startsWith(`${trackedDirectory}/`)) return true
293
+ }
294
+
295
+ return false
296
+ }
297
+
298
+ function normalizeTrackedDirectory(trackedDirectory: string): string {
299
+ return trackedDirectory.replace(/\/+$/, '') || '/'
300
+ }
301
+
302
+ function mergeTracking(tracking: readonly ModuleTracking[]): {
303
+ trackedFiles: Set<string>
304
+ trackedDirectories: Set<string>
305
+ } {
306
+ let trackedFiles = new Set<string>()
307
+ let trackedDirectories = new Set<string>()
308
+
309
+ for (let fragment of tracking) {
310
+ for (let trackedFile of fragment.trackedFiles) {
311
+ trackedFiles.add(trackedFile)
312
+ }
313
+
314
+ for (let trackedDirectory of fragment.trackedDirectories ?? []) {
315
+ trackedDirectories.add(normalizeTrackedDirectory(trackedDirectory))
316
+ }
317
+ }
318
+
319
+ return {
320
+ trackedFiles,
321
+ trackedDirectories,
322
+ }
323
+ }
324
+
325
+ function getWatchedDirectories(record: {
326
+ trackedFiles: ReadonlySet<string>
327
+ trackedDirectories: ReadonlySet<string>
328
+ }): Set<string> {
329
+ let watchedDirectories = new Set<string>()
330
+
331
+ for (let trackedFile of record.trackedFiles) {
332
+ watchedDirectories.add(getFilePathDirectory(trackedFile))
333
+ }
334
+
335
+ for (let trackedDirectory of record.trackedDirectories) {
336
+ watchedDirectories.add(trackedDirectory)
337
+ }
338
+
339
+ return watchedDirectories
340
+ }
@@ -0,0 +1,66 @@
1
+ import * as path from 'node:path'
2
+
3
+ const windowsDriveLetterRE = /^[A-Za-z]:\//
4
+ const uncPrefixRE = /^\/\/[^/]+\/[^/]+/
5
+
6
+ export function normalizeWindowsPath(filePath: string): string {
7
+ return filePath
8
+ .replace(/\\/g, '/')
9
+ .replace(windowsDriveLetterRE, (prefix) => `${prefix[0]!.toUpperCase()}${prefix.slice(1)}`)
10
+ }
11
+
12
+ export function normalizePathname(pathname: string): string {
13
+ let normalized = path.posix.normalize(normalizeWindowsPath(pathname))
14
+
15
+ if (!normalized.startsWith('/')) {
16
+ normalized = `/${normalized}`
17
+ }
18
+
19
+ return normalized
20
+ }
21
+
22
+ export function isAbsoluteFilePath(filePath: string): boolean {
23
+ let normalized = normalizeWindowsPath(filePath)
24
+ return normalized.startsWith('/') || windowsDriveLetterRE.test(normalized)
25
+ }
26
+
27
+ export function normalizeFilePath(filePath: string): string {
28
+ let normalized = normalizeWindowsPath(filePath)
29
+ let uncRoot = getUncRoot(normalized)
30
+
31
+ if (uncRoot) {
32
+ let remainder = normalized.slice(uncRoot.length)
33
+ let normalizedRemainder = path.posix.normalize(remainder || '/')
34
+ return `${uncRoot}${normalizedRemainder === '/' ? '' : normalizedRemainder}`
35
+ }
36
+
37
+ if (windowsDriveLetterRE.test(normalized)) {
38
+ return path.posix.normalize(normalized)
39
+ }
40
+
41
+ if (normalized.startsWith('/')) {
42
+ return path.posix.normalize(normalized)
43
+ }
44
+
45
+ return path.posix.normalize(normalizeWindowsPath(path.resolve(normalized)))
46
+ }
47
+
48
+ export function resolveFilePath(rootDir: string, filePath: string): string {
49
+ if (isAbsoluteFilePath(filePath)) {
50
+ return normalizeFilePath(filePath)
51
+ }
52
+
53
+ return normalizeFilePath(`${rootDir.replace(/\/+$/, '')}/${normalizeWindowsPath(filePath)}`)
54
+ }
55
+
56
+ export function getFilePathDirectory(filePath: string): string {
57
+ return path.posix.dirname(normalizeWindowsPath(filePath))
58
+ }
59
+
60
+ export function getFilePathBaseName(filePath: string): string {
61
+ return path.posix.basename(normalizeWindowsPath(filePath))
62
+ }
63
+
64
+ function getUncRoot(filePath: string): string | null {
65
+ return filePath.startsWith('//') ? (filePath.match(uncPrefixRE)?.[0] ?? null) : null
66
+ }
@@ -0,0 +1,164 @@
1
+ import * as path from 'node:path'
2
+ import { RoutePattern } from '@remix-run/route-pattern'
3
+
4
+ import {
5
+ isAbsoluteFilePath,
6
+ normalizeFilePath,
7
+ normalizePathname,
8
+ resolveFilePath,
9
+ } from './paths.ts'
10
+
11
+ export interface AssetRouteDefinition {
12
+ urlPattern: string
13
+ filePattern: string
14
+ }
15
+
16
+ interface CompiledRoute {
17
+ rootDir: string
18
+ urlPattern: RoutePattern
19
+ filePattern: RoutePattern
20
+ }
21
+
22
+ export interface CompiledRoutes {
23
+ resolveUrlPathname(pathname: string): string | null
24
+ toUrlPathname(filePath: string): string | null
25
+ }
26
+
27
+ function normalizeFilePattern(pattern: string): string {
28
+ if (isAbsoluteFilePath(pattern)) {
29
+ throw new Error(
30
+ `File route patterns must be relative to the asset server root.\nPattern: ${pattern}`,
31
+ )
32
+ }
33
+
34
+ return normalizePathname(pattern)
35
+ }
36
+
37
+ export function compileRoutes(options: {
38
+ fileMap: Readonly<Record<string, string>>
39
+ rootDir: string
40
+ }): CompiledRoutes {
41
+ if (Object.keys(options.fileMap).length === 0) {
42
+ throw new Error('createAssetServer() requires at least one configured fileMap entry.')
43
+ }
44
+
45
+ let compiledRoutes = Object.entries(options.fileMap).map(([urlPattern, filePattern]) =>
46
+ compileRoute(
47
+ {
48
+ urlPattern,
49
+ filePattern,
50
+ },
51
+ { rootDir: options.rootDir },
52
+ ),
53
+ )
54
+
55
+ return {
56
+ resolveUrlPathname(pathname) {
57
+ let normalizedPathname = normalizePathname(pathname)
58
+
59
+ for (let route of compiledRoutes) {
60
+ let match = route.urlPattern.match(`http://remix.run${normalizedPathname}`)
61
+ if (!match) continue
62
+ let relativeFilePath = route.filePattern.href(match.params).replace(/^\/+/, '')
63
+ return resolveFilePath(route.rootDir, relativeFilePath)
64
+ }
65
+
66
+ return null
67
+ },
68
+ toUrlPathname(filePath) {
69
+ let normalizedFilePath = normalizeFilePath(filePath)
70
+
71
+ for (let route of compiledRoutes) {
72
+ let relativeFilePath = getRelativeFilePath(normalizedFilePath, route.rootDir)
73
+ if (relativeFilePath === null) continue
74
+ let match = route.filePattern.ast.pathname.match(relativeFilePath)
75
+ if (!match) continue
76
+ return normalizePathname(route.urlPattern.href(getPathnameParams(route.filePattern, match)))
77
+ }
78
+
79
+ return null
80
+ },
81
+ }
82
+ }
83
+
84
+ function compileRoute(
85
+ route: AssetRouteDefinition,
86
+ options: {
87
+ rootDir: string
88
+ },
89
+ ): CompiledRoute {
90
+ let urlPatternSource = normalizePathname(route.urlPattern)
91
+ let filePatternSource = normalizeFilePattern(route.filePattern)
92
+
93
+ let urlPattern = new RoutePattern(urlPatternSource)
94
+ let filePattern = new RoutePattern(filePatternSource)
95
+
96
+ validateNoUnnamedWildcards(urlPattern, 'URL')
97
+ validateNoUnnamedWildcards(filePattern, 'File')
98
+ validateRoutePatterns(urlPattern, filePattern)
99
+
100
+ return {
101
+ rootDir: normalizeFilePath(options.rootDir).replace(/\/+$/, ''),
102
+ urlPattern,
103
+ filePattern,
104
+ }
105
+ }
106
+
107
+ function getRelativeFilePath(filePath: string, rootDir: string): string | null {
108
+ if (filePath[1] === ':' && rootDir[1] === ':' && filePath[0] !== rootDir[0]) return null
109
+ return path.posix.relative(rootDir, filePath)
110
+ }
111
+
112
+ function getPathnameParams(
113
+ pattern: RoutePattern,
114
+ match: Array<{ name: string; type: ':' | '*'; value: string }>,
115
+ ): Record<string, string | undefined> {
116
+ let params: Record<string, string | undefined> = {}
117
+
118
+ for (let param of pattern.ast.pathname.params) {
119
+ if (param.name === '*') continue
120
+ params[param.name] = undefined
121
+ }
122
+
123
+ for (let param of match) {
124
+ if (param.name === '*') continue
125
+ params[param.name] = param.value
126
+ }
127
+
128
+ return params
129
+ }
130
+
131
+ function validateRoutePatterns(urlPattern: RoutePattern, filePattern: RoutePattern): void {
132
+ let urlParams = urlPattern.ast.pathname.params.map(
133
+ (param: { name: string; type: ':' | '*' }) => `${param.type}:${param.name}`,
134
+ )
135
+ let fileParams = filePattern.ast.pathname.params.map(
136
+ (param: { name: string; type: ':' | '*' }) => `${param.type}:${param.name}`,
137
+ )
138
+
139
+ if (urlParams.length !== fileParams.length) {
140
+ throw new Error(
141
+ `Route patterns must have matching capture structure.\nURL: ${urlPattern}\nFile: ${filePattern}`,
142
+ )
143
+ }
144
+
145
+ for (let i = 0; i < urlParams.length; i++) {
146
+ if (urlParams[i] !== fileParams[i]) {
147
+ throw new Error(
148
+ `Route patterns must have matching capture structure.\nURL: ${urlPattern}\nFile: ${filePattern}`,
149
+ )
150
+ }
151
+ }
152
+ }
153
+
154
+ function validateNoUnnamedWildcards(pattern: RoutePattern, label: string): void {
155
+ if (
156
+ pattern.ast.pathname.params.some(
157
+ (param: { name: string; type: ':' | '*' }) => param.type === '*' && param.name === '*',
158
+ )
159
+ ) {
160
+ throw new Error(
161
+ `${label} route patterns must use named wildcards for reversible mapping.\nPattern: ${pattern}`,
162
+ )
163
+ }
164
+ }