@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.
- package/LICENSE +21 -0
- package/README.md +325 -2
- package/dist/assets.d.ts +3 -0
- package/dist/assets.d.ts.map +1 -0
- package/dist/assets.js +1 -0
- package/dist/lib/access.d.ts +10 -0
- package/dist/lib/access.d.ts.map +1 -0
- package/dist/lib/access.js +14 -0
- package/dist/lib/asset-server.d.ts +139 -0
- package/dist/lib/asset-server.d.ts.map +1 -0
- package/dist/lib/asset-server.js +338 -0
- package/dist/lib/compilation-error.d.ts +33 -0
- package/dist/lib/compilation-error.d.ts.map +1 -0
- package/dist/lib/compilation-error.js +32 -0
- package/dist/lib/file-matcher.d.ts +6 -0
- package/dist/lib/file-matcher.d.ts.map +1 -0
- package/dist/lib/file-matcher.js +44 -0
- package/dist/lib/fingerprint.d.ts +12 -0
- package/dist/lib/fingerprint.d.ts.map +1 -0
- package/dist/lib/fingerprint.js +49 -0
- package/dist/lib/module-store.d.ts +41 -0
- package/dist/lib/module-store.d.ts.map +1 -0
- package/dist/lib/module-store.js +230 -0
- package/dist/lib/paths.d.ts +8 -0
- package/dist/lib/paths.d.ts.map +1 -0
- package/dist/lib/paths.js +50 -0
- package/dist/lib/routes.d.ts +13 -0
- package/dist/lib/routes.d.ts.map +1 -0
- package/dist/lib/routes.js +94 -0
- package/dist/lib/scripts/cjs-check.d.ts +3 -0
- package/dist/lib/scripts/cjs-check.d.ts.map +1 -0
- package/dist/lib/scripts/cjs-check.js +398 -0
- package/dist/lib/scripts/compiler.d.ts +62 -0
- package/dist/lib/scripts/compiler.d.ts.map +1 -0
- package/dist/lib/scripts/compiler.js +439 -0
- package/dist/lib/scripts/emit.d.ts +25 -0
- package/dist/lib/scripts/emit.d.ts.map +1 -0
- package/dist/lib/scripts/emit.js +63 -0
- package/dist/lib/scripts/resolve.d.ts +50 -0
- package/dist/lib/scripts/resolve.d.ts.map +1 -0
- package/dist/lib/scripts/resolve.js +236 -0
- package/dist/lib/scripts/transform.d.ts +64 -0
- package/dist/lib/scripts/transform.d.ts.map +1 -0
- package/dist/lib/scripts/transform.js +373 -0
- package/dist/lib/source-maps.d.ts +4 -0
- package/dist/lib/source-maps.d.ts.map +1 -0
- package/dist/lib/source-maps.js +62 -0
- package/dist/lib/styles/compiler.d.ts +52 -0
- package/dist/lib/styles/compiler.d.ts.map +1 -0
- package/dist/lib/styles/compiler.js +272 -0
- package/dist/lib/styles/emit.d.ts +25 -0
- package/dist/lib/styles/emit.d.ts.map +1 -0
- package/dist/lib/styles/emit.js +78 -0
- package/dist/lib/styles/resolve.d.ts +48 -0
- package/dist/lib/styles/resolve.d.ts.map +1 -0
- package/dist/lib/styles/resolve.js +188 -0
- package/dist/lib/styles/transform.d.ts +47 -0
- package/dist/lib/styles/transform.d.ts.map +1 -0
- package/dist/lib/styles/transform.js +131 -0
- package/dist/lib/target.d.ts +21 -0
- package/dist/lib/target.d.ts.map +1 -0
- package/dist/lib/target.js +127 -0
- package/dist/lib/watch.d.ts +22 -0
- package/dist/lib/watch.d.ts.map +1 -0
- package/dist/lib/watch.js +96 -0
- package/package.json +55 -12
- package/src/assets.ts +2 -0
- package/src/lib/access.ts +24 -0
- package/src/lib/asset-server.ts +537 -0
- package/src/lib/compilation-error.ts +61 -0
- package/src/lib/file-matcher.ts +63 -0
- package/src/lib/fingerprint.ts +65 -0
- package/src/lib/module-store.ts +340 -0
- package/src/lib/paths.ts +66 -0
- package/src/lib/routes.ts +164 -0
- package/src/lib/scripts/cjs-check.ts +476 -0
- package/src/lib/scripts/compiler.ts +640 -0
- package/src/lib/scripts/emit.ts +122 -0
- package/src/lib/scripts/resolve.ts +433 -0
- package/src/lib/scripts/transform.ts +609 -0
- package/src/lib/source-maps.ts +75 -0
- package/src/lib/styles/compiler.ts +400 -0
- package/src/lib/styles/emit.ts +137 -0
- package/src/lib/styles/resolve.ts +316 -0
- package/src/lib/styles/transform.ts +226 -0
- package/src/lib/target.ts +196 -0
- 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
|
+
}
|
package/src/lib/paths.ts
ADDED
|
@@ -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
|
+
}
|