@remix-run/assets 0.5.0 → 0.7.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/README.md +185 -104
- package/dist/assets.d.ts +4 -1
- package/dist/assets.d.ts.map +1 -1
- package/dist/lib/access.d.ts +29 -2
- package/dist/lib/access.d.ts.map +1 -1
- package/dist/lib/access.js +52 -26
- package/dist/lib/asset-server.d.ts +61 -25
- package/dist/lib/asset-server.d.ts.map +1 -1
- package/dist/lib/asset-server.js +127 -55
- package/dist/lib/compilation-error.d.ts +1 -1
- package/dist/lib/compilation-error.d.ts.map +1 -1
- package/dist/lib/files/compiler.d.ts +1 -1
- package/dist/lib/files/compiler.d.ts.map +1 -1
- package/dist/lib/files/compiler.js +28 -31
- package/dist/lib/files/config.d.ts +6 -0
- package/dist/lib/files/config.d.ts.map +1 -1
- package/dist/lib/files/config.js +9 -0
- package/dist/lib/fingerprint.d.ts +0 -4
- package/dist/lib/fingerprint.d.ts.map +1 -1
- package/dist/lib/fingerprint.js +0 -4
- package/dist/lib/hmr.d.ts +7 -0
- package/dist/lib/hmr.d.ts.map +1 -1
- package/dist/lib/hmr.js +198 -25
- package/dist/lib/injected-packages.d.ts +3 -2
- package/dist/lib/injected-packages.d.ts.map +1 -1
- package/dist/lib/injected-packages.js +11 -8
- package/dist/lib/inspection.d.ts +39 -0
- package/dist/lib/inspection.d.ts.map +1 -0
- package/dist/lib/inspection.js +160 -0
- package/dist/lib/routes.d.ts +12 -3
- package/dist/lib/routes.d.ts.map +1 -1
- package/dist/lib/routes.js +124 -80
- package/dist/lib/scripts/compiler.d.ts +8 -2
- package/dist/lib/scripts/compiler.d.ts.map +1 -1
- package/dist/lib/scripts/compiler.js +188 -38
- package/dist/lib/scripts/emit.d.ts +2 -1
- package/dist/lib/scripts/emit.d.ts.map +1 -1
- package/dist/lib/scripts/emit.js +25 -16
- package/dist/lib/scripts/resolve.d.ts +7 -1
- package/dist/lib/scripts/resolve.d.ts.map +1 -1
- package/dist/lib/scripts/resolve.js +119 -12
- package/dist/lib/scripts/specifiers.d.ts +2 -0
- package/dist/lib/scripts/specifiers.d.ts.map +1 -0
- package/dist/lib/scripts/specifiers.js +9 -0
- package/dist/lib/scripts/transform.d.ts +2 -3
- package/dist/lib/scripts/transform.d.ts.map +1 -1
- package/dist/lib/scripts/transform.js +4 -18
- package/dist/lib/styles/compiler.d.ts +0 -1
- package/dist/lib/styles/compiler.d.ts.map +1 -1
- package/dist/lib/styles/compiler.js +105 -25
- package/dist/lib/styles/emit.d.ts +2 -1
- package/dist/lib/styles/emit.d.ts.map +1 -1
- package/dist/lib/styles/emit.js +12 -10
- package/dist/lib/styles/resolve.d.ts +0 -1
- package/dist/lib/styles/resolve.d.ts.map +1 -1
- package/dist/lib/styles/resolve.js +8 -9
- package/dist/lib/styles/transform.d.ts +0 -2
- package/dist/lib/styles/transform.d.ts.map +1 -1
- package/dist/lib/styles/transform.js +2 -9
- package/dist/lib/virtual-store.d.ts +8 -0
- package/dist/lib/virtual-store.d.ts.map +1 -0
- package/dist/lib/virtual-store.js +100 -0
- package/package.json +6 -6
- package/src/assets.ts +9 -1
- package/src/lib/access.ts +83 -30
- package/src/lib/asset-server.ts +224 -94
- package/src/lib/compilation-error.ts +4 -3
- package/src/lib/files/compiler.ts +32 -42
- package/src/lib/files/config.ts +17 -0
- package/src/lib/fingerprint.ts +0 -9
- package/src/lib/hmr.ts +210 -26
- package/src/lib/injected-packages.ts +16 -10
- package/src/lib/inspection.ts +229 -0
- package/src/lib/routes.ts +158 -126
- package/src/lib/scripts/compiler.ts +240 -54
- package/src/lib/scripts/emit.ts +34 -19
- package/src/lib/scripts/resolve.ts +175 -13
- package/src/lib/scripts/specifiers.ts +11 -0
- package/src/lib/scripts/transform.ts +6 -25
- package/src/lib/styles/compiler.ts +137 -39
- package/src/lib/styles/emit.ts +18 -13
- package/src/lib/styles/resolve.ts +8 -10
- package/src/lib/styles/transform.ts +2 -12
- package/src/lib/virtual-store.ts +124 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import * as fs from 'node:fs'
|
|
2
|
+
import * as fsPromises from 'node:fs/promises'
|
|
3
|
+
import * as path from 'node:path'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
|
|
6
|
+
import type { AccessPolicy, AssetAccessDetails } from './access.ts'
|
|
7
|
+
import { parseFingerprintSuffix } from './fingerprint.ts'
|
|
8
|
+
import { getInjectedPackageRoots } from './injected-packages.ts'
|
|
9
|
+
import { isAbsoluteFilePath, normalizeFilePath, resolveFilePath } from './paths.ts'
|
|
10
|
+
import type { AssetRouteMatch, CompiledRoutes } from './routes.ts'
|
|
11
|
+
import { supportedScriptExtensions } from './scripts/resolve.ts'
|
|
12
|
+
import { isStyleFilePath } from './styles/compiler.ts'
|
|
13
|
+
|
|
14
|
+
const scriptExtensions = new Set<string>(supportedScriptExtensions)
|
|
15
|
+
const globSyntaxPattern = /[*?[\]{}()!+@]/
|
|
16
|
+
|
|
17
|
+
/** How the asset server handles an inspected file. */
|
|
18
|
+
export type AssetKind = 'file' | 'script' | 'style' | 'unsupported'
|
|
19
|
+
|
|
20
|
+
/** Browser-reachability result for an inspected asset. */
|
|
21
|
+
export type AssetStatus =
|
|
22
|
+
| 'denied'
|
|
23
|
+
| 'missing'
|
|
24
|
+
| 'not-allowed'
|
|
25
|
+
| 'reachable'
|
|
26
|
+
| 'unmapped'
|
|
27
|
+
| 'unsupported'
|
|
28
|
+
|
|
29
|
+
/** Diagnostic information about a configured asset URL or file path. */
|
|
30
|
+
export interface AssetDetails {
|
|
31
|
+
/** Access-control decision and the rules responsible for it. */
|
|
32
|
+
access?: AssetAccessDetails
|
|
33
|
+
/** Absolute mapped file path. */
|
|
34
|
+
filePath?: string
|
|
35
|
+
/** Configured filesystem mount root that matched the asset. */
|
|
36
|
+
fileRoot?: string
|
|
37
|
+
/** Browser-reachability result. */
|
|
38
|
+
status: AssetStatus
|
|
39
|
+
/** How the asset server handles the file. */
|
|
40
|
+
type?: AssetKind
|
|
41
|
+
/** Stable public URL pathname for the asset. */
|
|
42
|
+
url?: string
|
|
43
|
+
/** Public mount root that matched the asset. */
|
|
44
|
+
urlRoot?: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface AssetInspectorOptions {
|
|
48
|
+
accessPolicy: AccessPolicy
|
|
49
|
+
allowFiles: readonly string[]
|
|
50
|
+
fileExtensions: readonly string[]
|
|
51
|
+
rootDir: string
|
|
52
|
+
routes: CompiledRoutes
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface AssetInspector {
|
|
56
|
+
/** Returns diagnostic information for a public URL or file path. */
|
|
57
|
+
getAssetDetails(input: string): Promise<AssetDetails>
|
|
58
|
+
/** Returns every file currently reachable through the configured asset server. */
|
|
59
|
+
getAssets(): Promise<AssetDetails[]>
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function createAssetInspector(options: AssetInspectorOptions): AssetInspector {
|
|
63
|
+
let fileExtensions = new Set(options.fileExtensions.map((extension) => extension.toLowerCase()))
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
async getAssetDetails(input) {
|
|
67
|
+
let routeMatch = await resolveInput(input, options)
|
|
68
|
+
if (routeMatch === null) return { status: 'unmapped' }
|
|
69
|
+
return inspectRouteMatch(routeMatch, options, fileExtensions)
|
|
70
|
+
},
|
|
71
|
+
async getAssets() {
|
|
72
|
+
let filePaths = await discoverFilePaths(options)
|
|
73
|
+
let assets: AssetDetails[] = []
|
|
74
|
+
|
|
75
|
+
for (let filePath of filePaths) {
|
|
76
|
+
let routeMatch = options.routes.matchFilePath(filePath)
|
|
77
|
+
if (routeMatch === null) continue
|
|
78
|
+
|
|
79
|
+
let details = await inspectRouteMatch(routeMatch, options, fileExtensions)
|
|
80
|
+
if (details.status === 'reachable') assets.push(details)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
assets.sort((left, right) => {
|
|
84
|
+
let urlOrder = (left.url ?? '').localeCompare(right.url ?? '')
|
|
85
|
+
return urlOrder === 0 ? (left.filePath ?? '').localeCompare(right.filePath ?? '') : urlOrder
|
|
86
|
+
})
|
|
87
|
+
return assets
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function resolveInput(
|
|
93
|
+
input: string,
|
|
94
|
+
options: Pick<AssetInspectorOptions, 'rootDir' | 'routes'>,
|
|
95
|
+
): Promise<AssetRouteMatch | null> {
|
|
96
|
+
if (input.startsWith('file://')) {
|
|
97
|
+
return options.routes.matchFilePath(fileURLToPath(input))
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (/^[A-Za-z][A-Za-z\d+.-]*:\/\//.test(input)) {
|
|
101
|
+
let pathname = parseFingerprintSuffix(new URL(input).pathname).pathname
|
|
102
|
+
return options.routes.matchUrlPathname(pathname)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let filePath = resolveFilePath(options.rootDir, input)
|
|
106
|
+
if (!input.startsWith('/') || isAbsoluteFilePath(input)) {
|
|
107
|
+
if (await pathExists(filePath)) return options.routes.matchFilePath(filePath)
|
|
108
|
+
if (!input.startsWith('/')) return options.routes.matchFilePath(filePath)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
let pathname = parseFingerprintSuffix(new URL(input, 'http://remix.run').pathname).pathname
|
|
112
|
+
return options.routes.matchUrlPathname(pathname)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function inspectRouteMatch(
|
|
116
|
+
routeMatch: AssetRouteMatch,
|
|
117
|
+
options: Pick<AssetInspectorOptions, 'accessPolicy' | 'rootDir'>,
|
|
118
|
+
fileExtensions: ReadonlySet<string>,
|
|
119
|
+
): Promise<AssetDetails> {
|
|
120
|
+
let exists = await pathExists(routeMatch.filePath)
|
|
121
|
+
let identityPath = exists ? fs.realpathSync(routeMatch.filePath) : routeMatch.filePath
|
|
122
|
+
let normalizedIdentityPath = normalizeFilePath(identityPath)
|
|
123
|
+
let access = options.accessPolicy.inspect(normalizedIdentityPath)
|
|
124
|
+
|
|
125
|
+
let type = getAssetKind(routeMatch.filePath, fileExtensions)
|
|
126
|
+
let details = {
|
|
127
|
+
access,
|
|
128
|
+
filePath: routeMatch.filePath,
|
|
129
|
+
fileRoot: routeMatch.fileRoot,
|
|
130
|
+
type,
|
|
131
|
+
url: routeMatch.urlPathname,
|
|
132
|
+
urlRoot: routeMatch.urlRoot,
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!exists) return { ...details, status: 'missing' }
|
|
136
|
+
if (!access.allowed) {
|
|
137
|
+
return { ...details, status: access.deniedBy === undefined ? 'not-allowed' : 'denied' }
|
|
138
|
+
}
|
|
139
|
+
if (type === 'unsupported') return { ...details, status: 'unsupported' }
|
|
140
|
+
return { ...details, status: 'reachable' }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function getAssetKind(filePath: string, fileExtensions: ReadonlySet<string>): AssetKind {
|
|
144
|
+
let extension = path.extname(filePath).toLowerCase()
|
|
145
|
+
if (scriptExtensions.has(extension)) return 'script'
|
|
146
|
+
if (isStyleFilePath(filePath)) return 'style'
|
|
147
|
+
if (fileExtensions.has(extension)) return 'file'
|
|
148
|
+
return 'unsupported'
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function discoverFilePaths(options: AssetInspectorOptions): Promise<string[]> {
|
|
152
|
+
let roots = new Set<string>()
|
|
153
|
+
|
|
154
|
+
for (let pattern of options.allowFiles) {
|
|
155
|
+
roots.add(resolveDiscoveryRoot(options.rootDir, pattern))
|
|
156
|
+
}
|
|
157
|
+
for (let packageRoot of options.accessPolicy.getAllowedPackageRoots()) {
|
|
158
|
+
roots.add(packageRoot)
|
|
159
|
+
}
|
|
160
|
+
for (let packageRoot of getInjectedPackageRoots()) {
|
|
161
|
+
roots.add(packageRoot)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
let filePaths = new Set<string>()
|
|
165
|
+
for (let root of roots) {
|
|
166
|
+
await collectFiles(root, filePaths)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return [...filePaths]
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function resolveDiscoveryRoot(rootDir: string, pattern: string): string {
|
|
173
|
+
let dynamicIndex = pattern.search(globSyntaxPattern)
|
|
174
|
+
if (dynamicIndex === -1) return resolveFilePath(rootDir, pattern)
|
|
175
|
+
|
|
176
|
+
let rawStaticPrefix = pattern.slice(0, dynamicIndex)
|
|
177
|
+
let staticPrefix = rawStaticPrefix.replace(/[/\\]+$/, '')
|
|
178
|
+
if (staticPrefix.length === 0) return rootDir
|
|
179
|
+
return resolveFilePath(
|
|
180
|
+
rootDir,
|
|
181
|
+
/[/\\]$/.test(rawStaticPrefix) ? staticPrefix : path.dirname(staticPrefix),
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function collectFiles(root: string, filePaths: Set<string>): Promise<void> {
|
|
186
|
+
let stat
|
|
187
|
+
try {
|
|
188
|
+
stat = await fsPromises.stat(root)
|
|
189
|
+
} catch (error) {
|
|
190
|
+
if (isPathNotFoundError(error)) return
|
|
191
|
+
throw error
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (stat.isFile()) {
|
|
195
|
+
filePaths.add(normalizeFilePath(root))
|
|
196
|
+
return
|
|
197
|
+
}
|
|
198
|
+
if (!stat.isDirectory()) return
|
|
199
|
+
|
|
200
|
+
let entries = await fsPromises.readdir(root, { withFileTypes: true })
|
|
201
|
+
for (let entry of entries) {
|
|
202
|
+
let entryPath = path.join(root, entry.name)
|
|
203
|
+
if (entry.isDirectory()) {
|
|
204
|
+
await collectFiles(entryPath, filePaths)
|
|
205
|
+
} else if (entry.isFile()) {
|
|
206
|
+
filePaths.add(normalizeFilePath(entryPath))
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function pathExists(filePath: string): Promise<boolean> {
|
|
212
|
+
try {
|
|
213
|
+
await fsPromises.access(filePath)
|
|
214
|
+
return true
|
|
215
|
+
} catch (error) {
|
|
216
|
+
if (isPathNotFoundError(error)) return false
|
|
217
|
+
throw error
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function isPathNotFoundError(
|
|
222
|
+
error: unknown,
|
|
223
|
+
): error is NodeJS.ErrnoException & { code: 'ENOENT' | 'ENOTDIR' } {
|
|
224
|
+
return (
|
|
225
|
+
error instanceof Error &&
|
|
226
|
+
'code' in error &&
|
|
227
|
+
(error.code === 'ENOENT' || error.code === 'ENOTDIR')
|
|
228
|
+
)
|
|
229
|
+
}
|
package/src/lib/routes.ts
CHANGED
|
@@ -1,186 +1,218 @@
|
|
|
1
|
-
import
|
|
2
|
-
getRoutePatternCaptures,
|
|
3
|
-
RoutePattern,
|
|
4
|
-
type RoutePatternCapture,
|
|
5
|
-
} from '@remix-run/route-pattern'
|
|
6
|
-
import { createHref } from '@remix-run/route-pattern/href'
|
|
7
|
-
import { createMatcher, type Matcher } from '@remix-run/route-pattern/match'
|
|
1
|
+
import * as fs from 'node:fs'
|
|
8
2
|
|
|
9
3
|
import {
|
|
10
|
-
getRelativeFilePath,
|
|
11
4
|
isAbsoluteFilePath,
|
|
12
5
|
normalizeFilePath,
|
|
13
6
|
normalizePathname,
|
|
14
7
|
resolveFilePath,
|
|
15
8
|
} from './paths.ts'
|
|
16
9
|
|
|
17
|
-
interface
|
|
18
|
-
|
|
19
|
-
filePattern: string
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
interface RouteConfig {
|
|
23
|
-
fileMap: Readonly<Record<string, string>>
|
|
10
|
+
interface MountConfig {
|
|
11
|
+
mounts: Readonly<Record<string, string>>
|
|
24
12
|
rootDir: string
|
|
25
13
|
}
|
|
26
14
|
|
|
27
|
-
interface
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
fileMatcher: Matcher
|
|
15
|
+
interface CompiledMount {
|
|
16
|
+
fileRoot: string
|
|
17
|
+
fileRootValue: string
|
|
18
|
+
urlRoot: string
|
|
19
|
+
urlRootKey: string
|
|
33
20
|
}
|
|
34
21
|
|
|
35
22
|
export interface CompiledRoutes {
|
|
36
23
|
resolveUrlPathname(pathname: string): string | null
|
|
24
|
+
matchUrlPathname(pathname: string): AssetRouteMatch | null
|
|
37
25
|
toUrlPathname(filePath: string): string | null
|
|
26
|
+
matchFilePath(filePath: string): AssetRouteMatch | null
|
|
38
27
|
}
|
|
39
28
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return normalizePathname(pattern)
|
|
29
|
+
export interface AssetRouteMatch {
|
|
30
|
+
filePath: string
|
|
31
|
+
fileRoot: string
|
|
32
|
+
urlPathname: string
|
|
33
|
+
urlRoot: string
|
|
48
34
|
}
|
|
49
35
|
|
|
50
36
|
export function compileRoutes(
|
|
51
37
|
basePath: string,
|
|
52
|
-
|
|
38
|
+
mountConfigs: readonly MountConfig[],
|
|
53
39
|
): CompiledRoutes {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
{
|
|
66
|
-
basePath,
|
|
67
|
-
rootDir: routeConfig.rootDir,
|
|
68
|
-
},
|
|
69
|
-
),
|
|
70
|
-
),
|
|
71
|
-
)
|
|
40
|
+
let compiledMounts = mountConfigs.flatMap((mountConfig) => {
|
|
41
|
+
let configMounts = Object.entries(mountConfig.mounts).map(([urlRoot, fileRoot]) =>
|
|
42
|
+
compileMount(urlRoot, fileRoot, {
|
|
43
|
+
basePath,
|
|
44
|
+
rootDir: mountConfig.rootDir,
|
|
45
|
+
}),
|
|
46
|
+
)
|
|
47
|
+
validateNoOverlappingMounts(configMounts)
|
|
48
|
+
return configMounts
|
|
49
|
+
})
|
|
50
|
+
validateNoOverlappingUrlRoots(compiledMounts)
|
|
72
51
|
|
|
73
52
|
return {
|
|
74
53
|
resolveUrlPathname(pathname) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
for (let route of compiledRoutes) {
|
|
78
|
-
let match = route.urlMatcher.match(`http://remix.run${normalizedPathname}`)
|
|
79
|
-
if (!match) continue
|
|
80
|
-
let relativeFilePath = decodeURIComponent(
|
|
81
|
-
createHref(route.filePattern, match.params),
|
|
82
|
-
).replace(/^\/+/, '')
|
|
83
|
-
return resolveFilePath(route.rootDir, relativeFilePath)
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return null
|
|
54
|
+
return matchUrlPathname(pathname)?.filePath ?? null
|
|
87
55
|
},
|
|
56
|
+
matchUrlPathname,
|
|
88
57
|
toUrlPathname(filePath) {
|
|
89
|
-
|
|
58
|
+
return matchFilePath(filePath)?.urlPathname ?? null
|
|
59
|
+
},
|
|
60
|
+
matchFilePath,
|
|
61
|
+
}
|
|
90
62
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
63
|
+
function matchUrlPathname(pathname: string): AssetRouteMatch | null {
|
|
64
|
+
let normalizedPathname = normalizePathname(pathname)
|
|
65
|
+
|
|
66
|
+
for (let mount of compiledMounts) {
|
|
67
|
+
let relativePathname = getPathWithinRoot(mount.urlRoot, normalizedPathname)
|
|
68
|
+
if (relativePathname === null) continue
|
|
69
|
+
let filePath = resolveFilePath(mount.fileRoot, decodeURIComponent(relativePathname))
|
|
70
|
+
if (getPathWithinRoot(mount.fileRoot, filePath) === null) return null
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
filePath,
|
|
74
|
+
fileRoot: mount.fileRootValue,
|
|
75
|
+
urlPathname: normalizedPathname,
|
|
76
|
+
urlRoot: mount.urlRoot,
|
|
96
77
|
}
|
|
78
|
+
}
|
|
97
79
|
|
|
98
|
-
|
|
99
|
-
|
|
80
|
+
return null
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function matchFilePath(filePath: string): AssetRouteMatch | null {
|
|
84
|
+
let normalizedFilePath = normalizeFilePath(filePath)
|
|
85
|
+
|
|
86
|
+
for (let mount of compiledMounts) {
|
|
87
|
+
let relativeFilePath = getPathWithinRoot(mount.fileRoot, normalizedFilePath)
|
|
88
|
+
if (relativeFilePath === null) continue
|
|
89
|
+
let encodedFilePath = relativeFilePath.split('/').map(encodeURIComponent).join('/')
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
filePath: normalizedFilePath,
|
|
93
|
+
fileRoot: mount.fileRootValue,
|
|
94
|
+
urlPathname: joinUrlPath(mount.urlRoot, encodedFilePath),
|
|
95
|
+
urlRoot: mount.urlRoot,
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return null
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
function
|
|
104
|
-
|
|
103
|
+
function compileMount(
|
|
104
|
+
urlRoot: string,
|
|
105
|
+
fileRoot: string,
|
|
105
106
|
options: {
|
|
106
107
|
basePath: string
|
|
107
108
|
rootDir: string
|
|
108
109
|
},
|
|
109
|
-
):
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
`${basePath.replace(/\/+$/, '')}/${relativeUrlPattern.replace(/^\/+/, '')}`,
|
|
114
|
-
)
|
|
115
|
-
let filePatternSource = normalizeFilePattern(route.filePattern)
|
|
116
|
-
|
|
117
|
-
let urlPattern = RoutePattern.parse(urlPatternSource)
|
|
118
|
-
let filePattern = RoutePattern.parse(filePatternSource)
|
|
119
|
-
|
|
120
|
-
validateNoUnnamedWildcards(urlPattern, 'URL')
|
|
121
|
-
validateNoUnnamedWildcards(filePattern, 'File')
|
|
122
|
-
validateRoutePatterns(urlPattern, filePattern)
|
|
110
|
+
): CompiledMount {
|
|
111
|
+
if (isAbsoluteFilePath(fileRoot)) {
|
|
112
|
+
throw new TypeError(`mounts values must be relative to rootDir. Received "${fileRoot}".`)
|
|
113
|
+
}
|
|
123
114
|
|
|
124
115
|
return {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
fileMatcher: createMatcher(stripDotSegments(filePatternSource)),
|
|
116
|
+
fileRoot: resolveMountFileRoot(options.rootDir, fileRoot),
|
|
117
|
+
fileRootValue: fileRoot,
|
|
118
|
+
urlRoot: joinUrlPath(normalizeMountUrlRoot(options.basePath), normalizeMountUrlRoot(urlRoot)),
|
|
119
|
+
urlRootKey: urlRoot,
|
|
130
120
|
}
|
|
131
121
|
}
|
|
132
122
|
|
|
133
|
-
function
|
|
134
|
-
let
|
|
123
|
+
function normalizeMountUrlRoot(urlRoot: string): string {
|
|
124
|
+
let normalizedRoot = normalizePathname(urlRoot).replace(/\/+$/, '') || '/'
|
|
125
|
+
let url = new URL(normalizedRoot, 'http://remix.run')
|
|
135
126
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
127
|
+
if (
|
|
128
|
+
url.search !== '' ||
|
|
129
|
+
url.hash !== '' ||
|
|
130
|
+
getUrlPathSegmentCount(url.pathname) !== getUrlPathSegmentCount(normalizedRoot)
|
|
131
|
+
) {
|
|
132
|
+
throw new TypeError(
|
|
133
|
+
`mounts keys must be URL pathnames without query strings, fragments, or encoded dot segments. Received "${urlRoot}".`,
|
|
134
|
+
)
|
|
143
135
|
}
|
|
144
136
|
|
|
145
|
-
return
|
|
137
|
+
return url.pathname.replace(/\/+$/, '') || '/'
|
|
146
138
|
}
|
|
147
139
|
|
|
148
|
-
function
|
|
149
|
-
let
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
)
|
|
140
|
+
export function resolveMountFileRoot(rootDir: string, fileRoot: string): string {
|
|
141
|
+
let resolvedRoot = resolveFilePath(rootDir, fileRoot)
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
resolvedRoot = normalizeFilePath(fs.realpathSync(resolvedRoot))
|
|
145
|
+
} catch (error) {
|
|
146
|
+
if (!isUnresolvedPathError(error, resolvedRoot)) throw error
|
|
155
147
|
}
|
|
156
148
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
149
|
+
return resolvedRoot.replace(/\/+$/, '') || '/'
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function getUrlPathSegmentCount(pathname: string): number {
|
|
153
|
+
return pathname.split('/').filter((segment) => segment !== '').length
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function joinUrlPath(root: string, path: string): string {
|
|
157
|
+
if (path === '' || path === '/') return normalizeMountUrlRoot(root)
|
|
158
|
+
return normalizePathname(`${root.replace(/\/+$/, '')}/${path.replace(/^\/+/, '')}`)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function getPathWithinRoot(root: string, value: string): string | null {
|
|
162
|
+
if (value === root) return ''
|
|
163
|
+
if (root === '/') return value.slice(1)
|
|
164
|
+
if (!value.startsWith(`${root}/`)) return null
|
|
165
|
+
return value.slice(root.length + 1)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function validateNoOverlappingMounts(mounts: readonly CompiledMount[]): void {
|
|
169
|
+
for (let index = 0; index < mounts.length; index++) {
|
|
170
|
+
let mount = mounts[index]
|
|
171
|
+
|
|
172
|
+
for (let otherIndex = index + 1; otherIndex < mounts.length; otherIndex++) {
|
|
173
|
+
let otherMount = mounts[otherIndex]
|
|
174
|
+
|
|
175
|
+
if (rootsOverlap(mount.fileRoot, otherMount.fileRoot)) {
|
|
176
|
+
throw new TypeError(
|
|
177
|
+
`mounts values must not overlap. Received "${mount.fileRootValue}" and "${otherMount.fileRootValue}", resolving to "${mount.fileRoot}" and "${otherMount.fileRoot}".`,
|
|
178
|
+
)
|
|
179
|
+
}
|
|
164
180
|
}
|
|
165
181
|
}
|
|
166
182
|
}
|
|
167
183
|
|
|
168
|
-
function
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
)
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
184
|
+
function validateNoOverlappingUrlRoots(mounts: readonly CompiledMount[]): void {
|
|
185
|
+
for (let index = 0; index < mounts.length; index++) {
|
|
186
|
+
let mount = mounts[index]
|
|
187
|
+
|
|
188
|
+
for (let otherIndex = index + 1; otherIndex < mounts.length; otherIndex++) {
|
|
189
|
+
let otherMount = mounts[otherIndex]
|
|
190
|
+
if (!rootsOverlap(mount.urlRoot, otherMount.urlRoot)) continue
|
|
191
|
+
|
|
192
|
+
throw new TypeError(
|
|
193
|
+
`mounts keys must not overlap. Received "${mount.urlRootKey}" and "${otherMount.urlRootKey}".`,
|
|
194
|
+
)
|
|
195
|
+
}
|
|
177
196
|
}
|
|
178
197
|
}
|
|
179
198
|
|
|
180
|
-
|
|
199
|
+
function rootsOverlap(root: string, otherRoot: string): boolean {
|
|
200
|
+
return (
|
|
201
|
+
root === otherRoot ||
|
|
202
|
+
root === '/' ||
|
|
203
|
+
otherRoot === '/' ||
|
|
204
|
+
root.startsWith(`${otherRoot}/`) ||
|
|
205
|
+
otherRoot.startsWith(`${root}/`)
|
|
206
|
+
)
|
|
207
|
+
}
|
|
181
208
|
|
|
182
|
-
function
|
|
183
|
-
|
|
184
|
-
|
|
209
|
+
function isUnresolvedPathError(error: unknown, filePath: string): boolean {
|
|
210
|
+
// Windows reports UNKNOWN rather than ENOENT when a UNC share cannot be reached.
|
|
211
|
+
return (
|
|
212
|
+
error instanceof Error &&
|
|
213
|
+
'code' in error &&
|
|
214
|
+
(error.code === 'ENOENT' ||
|
|
215
|
+
error.code === 'ENOTDIR' ||
|
|
216
|
+
(error.code === 'UNKNOWN' && filePath.startsWith('//')))
|
|
185
217
|
)
|
|
186
218
|
}
|