@remix-run/assets 0.5.0 → 0.6.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 +85 -92
- package/dist/assets.d.ts +2 -0
- 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 +21 -7
- package/dist/lib/asset-server.d.ts.map +1 -1
- package/dist/lib/asset-server.js +30 -15
- package/dist/lib/compilation-error.d.ts +1 -1
- package/dist/lib/compilation-error.d.ts.map +1 -1
- package/dist/lib/files/compiler.js +2 -2
- 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 +11 -3
- package/dist/lib/routes.d.ts.map +1 -1
- package/dist/lib/routes.js +124 -80
- package/dist/lib/scripts/compiler.js +2 -2
- package/dist/lib/scripts/resolve.js +9 -9
- package/dist/lib/scripts/transform.js +2 -2
- package/dist/lib/styles/resolve.js +8 -8
- package/dist/lib/styles/transform.js +2 -2
- package/package.json +5 -6
- package/src/assets.ts +2 -0
- package/src/lib/access.ts +83 -30
- package/src/lib/asset-server.ts +50 -22
- package/src/lib/compilation-error.ts +3 -3
- package/src/lib/files/compiler.ts +2 -2
- 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 +2 -2
- package/src/lib/scripts/resolve.ts +9 -9
- package/src/lib/scripts/transform.ts +2 -2
- package/src/lib/styles/resolve.ts +8 -8
- package/src/lib/styles/transform.ts +2 -2
package/src/lib/asset-server.ts
CHANGED
|
@@ -17,7 +17,7 @@ import type {
|
|
|
17
17
|
import { getFingerprintRequestCacheControl, parseFingerprintSuffix } from './fingerprint.ts'
|
|
18
18
|
import { createHmrClientSource } from './hmr.ts'
|
|
19
19
|
import type { HmrPayload } from './hmr.ts'
|
|
20
|
-
import {
|
|
20
|
+
import { getInjectedPackageMountConfigs } from './injected-packages.ts'
|
|
21
21
|
import type { ModuleLoader } from './loaders.ts'
|
|
22
22
|
import { normalizeFilePath, normalizePathname } from './paths.ts'
|
|
23
23
|
import { compileRoutes } from './routes.ts'
|
|
@@ -29,6 +29,7 @@ import { createResponseForStyle, createStyleCompiler, isStyleFilePath } from './
|
|
|
29
29
|
import { resolveScriptTarget, resolveStyleTarget } from './target.ts'
|
|
30
30
|
import type { AssetTarget, ResolvedScriptTarget, ResolvedStyleTarget } from './target.ts'
|
|
31
31
|
import { createAssetServerWatcher } from './watch.ts'
|
|
32
|
+
import { createAssetInspector, type AssetDetails } from './inspection.ts'
|
|
32
33
|
import type { AssetServerWatcher, ChokidarWatcher } from './watch.ts'
|
|
33
34
|
|
|
34
35
|
interface AssetServerWatchOptions {
|
|
@@ -172,6 +173,10 @@ interface AssetServerScriptOptions {
|
|
|
172
173
|
}
|
|
173
174
|
|
|
174
175
|
const scriptExtensionSet = new Set<string>(supportedScriptExtensions)
|
|
176
|
+
const defaultMounts = {
|
|
177
|
+
app: 'app',
|
|
178
|
+
npm: 'node_modules',
|
|
179
|
+
} as const
|
|
175
180
|
|
|
176
181
|
/**
|
|
177
182
|
* Options used to construct an {@link AssetServer} via {@link createAssetServer}.
|
|
@@ -179,8 +184,14 @@ const scriptExtensionSet = new Set<string>(supportedScriptExtensions)
|
|
|
179
184
|
export interface AssetServerOptions<transforms extends AssetRequestTransformMap = {}> {
|
|
180
185
|
/** Public mount path for this asset server, e.g. `'/assets'`. */
|
|
181
186
|
basePath: string
|
|
182
|
-
/**
|
|
183
|
-
|
|
187
|
+
/**
|
|
188
|
+
* Directories to mount at public URL paths.
|
|
189
|
+
*
|
|
190
|
+
* Each key is a public URL path and its value is a directory relative to `rootDir`. Defaults to
|
|
191
|
+
* `{ app: 'app', npm: 'node_modules' }`. Public paths must not contain query strings, fragments,
|
|
192
|
+
* or encoded dot segments.
|
|
193
|
+
*/
|
|
194
|
+
mounts?: Readonly<Record<string, string>>
|
|
184
195
|
/**
|
|
185
196
|
* Root directory used to resolve relative file paths. Defaults to `process.cwd()`.
|
|
186
197
|
*/
|
|
@@ -191,7 +202,7 @@ export interface AssetServerOptions<transforms extends AssetRequestTransformMap
|
|
|
191
202
|
allowFiles: readonly string[]
|
|
192
203
|
/**
|
|
193
204
|
* Exact package names whose files are allowed to be served. Dependencies and installed optional
|
|
194
|
-
* dependencies are allowed automatically. Package files must still
|
|
205
|
+
* dependencies are allowed automatically. Package files must still be within a configured mount.
|
|
195
206
|
*/
|
|
196
207
|
allowPackages?: readonly string[]
|
|
197
208
|
/**
|
|
@@ -289,6 +300,16 @@ export interface AssetServer<transforms extends AssetRequestTransformMap = {}> {
|
|
|
289
300
|
* Returns preload URLs for one or more served asset files, ordered shallowest-first.
|
|
290
301
|
*/
|
|
291
302
|
getPreloads(filePath: string | readonly string[]): Promise<string[]>
|
|
303
|
+
/**
|
|
304
|
+
* Returns diagnostic details about one public asset URL or file path, including the matched mount
|
|
305
|
+
* roots, access rules, file type, and browser-reachability status.
|
|
306
|
+
*/
|
|
307
|
+
getAssetDetails(input: string): Promise<AssetDetails>
|
|
308
|
+
/**
|
|
309
|
+
* Returns every file currently reachable through this asset server, sorted by public URL and
|
|
310
|
+
* then absolute file path.
|
|
311
|
+
*/
|
|
312
|
+
getAssets(): Promise<AssetDetails[]>
|
|
292
313
|
/**
|
|
293
314
|
* Closes this server's filesystem watcher and browser HMR channel.
|
|
294
315
|
*
|
|
@@ -312,6 +333,7 @@ type ResolvedAssetServerOptions<transforms extends AssetRequestTransformMap> = {
|
|
|
312
333
|
loaders: readonly ModuleLoader[]
|
|
313
334
|
onError: NonNullable<AssetServerOptions['onError']>
|
|
314
335
|
rootDir: string
|
|
336
|
+
mounts: Readonly<Record<string, string>>
|
|
315
337
|
routes: CompiledRoutes
|
|
316
338
|
sourceMapSourcePaths: 'url' | 'absolute'
|
|
317
339
|
sourceMaps?: 'inline' | 'external'
|
|
@@ -339,7 +361,7 @@ export function getInternalWatchTargets<transforms extends AssetRequestTransform
|
|
|
339
361
|
* Create an asset server instance
|
|
340
362
|
*
|
|
341
363
|
* Compiles TypeScript/JavaScript scripts and CSS styles on demand with optional
|
|
342
|
-
* source-based URL fingerprinting, caching, and configurable
|
|
364
|
+
* source-based URL fingerprinting, caching, and configurable directory mounts.
|
|
343
365
|
*
|
|
344
366
|
* @param options Server configuration
|
|
345
367
|
* @returns A {@link AssetServer} with `fetch()`, `getHref()`, and `getPreloads()` methods
|
|
@@ -348,9 +370,6 @@ export function getInternalWatchTargets<transforms extends AssetRequestTransform
|
|
|
348
370
|
* ```ts
|
|
349
371
|
* let assetServer = createAssetServer({
|
|
350
372
|
* basePath: '/assets',
|
|
351
|
-
* fileMap: {
|
|
352
|
-
* '/app/*path': 'app/*path',
|
|
353
|
-
* },
|
|
354
373
|
* allowFiles: ['app/routes.ts', 'app/**\/public/**'],
|
|
355
374
|
* allowPackages: ['remix'],
|
|
356
375
|
* denyFiles: ['app/**\/*.test.*'],
|
|
@@ -368,10 +387,17 @@ export function createAssetServer<const transforms extends AssetRequestTransform
|
|
|
368
387
|
allowPackages: resolvedOptions.allowPackages,
|
|
369
388
|
denyFiles: resolvedOptions.denyFiles,
|
|
370
389
|
packageSearchRoots: hasPackages(resolvedOptions.allowPackages)
|
|
371
|
-
? getPackageSearchRoots(
|
|
390
|
+
? getPackageSearchRoots(resolvedOptions.mounts, resolvedOptions.rootDir)
|
|
372
391
|
: undefined,
|
|
373
392
|
rootDir: resolvedOptions.rootDir,
|
|
374
393
|
})
|
|
394
|
+
let assetInspector = createAssetInspector({
|
|
395
|
+
accessPolicy,
|
|
396
|
+
allowFiles: resolvedOptions.allowFiles,
|
|
397
|
+
fileExtensions: resolvedOptions.files.extensions,
|
|
398
|
+
rootDir: resolvedOptions.rootDir,
|
|
399
|
+
routes: resolvedOptions.routes,
|
|
400
|
+
})
|
|
375
401
|
let watcher: AssetServerWatcher | null = null
|
|
376
402
|
let chokidarWatcher: ChokidarWatcher | null = null
|
|
377
403
|
let fileCompiler: FileCompiler | null = null
|
|
@@ -575,6 +601,12 @@ export function createAssetServer<const transforms extends AssetRequestTransform
|
|
|
575
601
|
}
|
|
576
602
|
|
|
577
603
|
let assetServer: AssetServer<transforms> = {
|
|
604
|
+
getAssetDetails(input) {
|
|
605
|
+
return assetInspector.getAssetDetails(input)
|
|
606
|
+
},
|
|
607
|
+
getAssets() {
|
|
608
|
+
return assetInspector.getAssets()
|
|
609
|
+
},
|
|
578
610
|
async fetch(request) {
|
|
579
611
|
if (request.method !== 'GET' && request.method !== 'HEAD') return null
|
|
580
612
|
let requestPathname = new URL(request.url).pathname
|
|
@@ -1006,10 +1038,14 @@ function resolveAssetServerOptions<transforms extends AssetRequestTransformMap>(
|
|
|
1006
1038
|
})
|
|
1007
1039
|
let watchOptions = normalizeWatchOptions(options.watch)
|
|
1008
1040
|
let hmrFactory = normalizeHmrFactory(options.hmr)
|
|
1041
|
+
let mounts = options.mounts ?? defaultMounts
|
|
1009
1042
|
|
|
1010
1043
|
if (hmrFactory && watchOptions === null) {
|
|
1011
1044
|
throw new TypeError('hmr requires watch mode')
|
|
1012
1045
|
}
|
|
1046
|
+
if (Object.keys(mounts).length === 0) {
|
|
1047
|
+
throw new TypeError('mounts must include at least one entry')
|
|
1048
|
+
}
|
|
1013
1049
|
return {
|
|
1014
1050
|
allowFiles: options.allowFiles,
|
|
1015
1051
|
allowPackages: options.allowPackages,
|
|
@@ -1022,15 +1058,16 @@ function resolveAssetServerOptions<transforms extends AssetRequestTransformMap>(
|
|
|
1022
1058
|
fingerprintAssets: fingerprintOptions.enabled,
|
|
1023
1059
|
hmr: hmrFactory,
|
|
1024
1060
|
minify: options.minify ?? false,
|
|
1061
|
+
mounts,
|
|
1025
1062
|
loaders: scriptOptions.loaders ?? [],
|
|
1026
1063
|
onError: options.onError ?? defaultErrorHandler,
|
|
1027
1064
|
rootDir,
|
|
1028
1065
|
routes: compileRoutes(basePath, [
|
|
1029
1066
|
{
|
|
1030
|
-
|
|
1067
|
+
mounts,
|
|
1031
1068
|
rootDir,
|
|
1032
1069
|
},
|
|
1033
|
-
...
|
|
1070
|
+
...getInjectedPackageMountConfigs(),
|
|
1034
1071
|
]),
|
|
1035
1072
|
sourceMapSourcePaths: options.sourceMapSourcePaths ?? 'url',
|
|
1036
1073
|
sourceMaps: options.sourceMaps,
|
|
@@ -1154,19 +1191,10 @@ function hasPackages(packages: readonly string[] | undefined): boolean {
|
|
|
1154
1191
|
}
|
|
1155
1192
|
|
|
1156
1193
|
function getPackageSearchRoots(
|
|
1157
|
-
|
|
1194
|
+
mounts: Readonly<Record<string, string>>,
|
|
1158
1195
|
rootDir: string,
|
|
1159
1196
|
): readonly string[] {
|
|
1160
|
-
return Object.values(
|
|
1161
|
-
path.resolve(rootDir, getStaticFilePatternPrefix(filePattern)),
|
|
1162
|
-
)
|
|
1163
|
-
}
|
|
1164
|
-
|
|
1165
|
-
function getStaticFilePatternPrefix(filePattern: string): string {
|
|
1166
|
-
let firstDynamicIndex = filePattern.search(/[*:]/)
|
|
1167
|
-
let staticPrefix =
|
|
1168
|
-
firstDynamicIndex === -1 ? filePattern : filePattern.slice(0, firstDynamicIndex)
|
|
1169
|
-
return staticPrefix.replace(/[/\\]*$/, '')
|
|
1197
|
+
return Object.values(mounts).map((fileRoot) => path.resolve(rootDir, fileRoot))
|
|
1170
1198
|
}
|
|
1171
1199
|
|
|
1172
1200
|
function parseAssetRequestPathname(
|
|
@@ -2,7 +2,7 @@ type AssetServerCompilationErrorCode =
|
|
|
2
2
|
| 'FILE_NOT_FOUND'
|
|
3
3
|
| 'FILE_NOT_ALLOWED'
|
|
4
4
|
| 'FILE_NOT_SUPPORTED'
|
|
5
|
-
| '
|
|
5
|
+
| 'FILE_OUTSIDE_MOUNTS'
|
|
6
6
|
| 'FILE_TRANSFORM_QUERY_INVALID'
|
|
7
7
|
| 'FILE_TRANSFORM_NOT_SUPPORTED'
|
|
8
8
|
| 'FILE_TRANSFORM_RESULT_INVALID'
|
|
@@ -14,11 +14,11 @@ type AssetServerCompilationErrorCode =
|
|
|
14
14
|
| 'IMPORT_RESOLUTION_FAILED'
|
|
15
15
|
| 'IMPORT_NOT_SUPPORTED'
|
|
16
16
|
| 'IMPORT_NOT_ALLOWED'
|
|
17
|
-
| '
|
|
17
|
+
| 'IMPORT_OUTSIDE_MOUNTS'
|
|
18
18
|
| 'URL_RESOLUTION_FAILED'
|
|
19
19
|
| 'URL_NOT_SUPPORTED'
|
|
20
20
|
| 'URL_NOT_ALLOWED'
|
|
21
|
-
| '
|
|
21
|
+
| 'URL_OUTSIDE_MOUNTS'
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* Internal error used by the request-time asset compilation pipeline.
|
|
@@ -566,9 +566,9 @@ export function resolveServedFileOrThrow(filePath: string, args: ResolveArgs): R
|
|
|
566
566
|
let stableUrlPathname = args.routes.toUrlPathname(identityPath)
|
|
567
567
|
if (!stableUrlPathname) {
|
|
568
568
|
throw createAssetServerCompilationError(
|
|
569
|
-
`File ${identityPath} is outside all configured
|
|
569
|
+
`File ${identityPath} is outside all configured mounts.`,
|
|
570
570
|
{
|
|
571
|
-
code: '
|
|
571
|
+
code: 'FILE_OUTSIDE_MOUNTS',
|
|
572
572
|
},
|
|
573
573
|
)
|
|
574
574
|
}
|
|
@@ -28,23 +28,29 @@ export function isInjectedPackageFilePath(filePath: string): boolean {
|
|
|
28
28
|
return false
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
export function
|
|
32
|
-
|
|
31
|
+
export function getInjectedPackageMountConfigs(): {
|
|
32
|
+
mounts: Record<string, string>
|
|
33
33
|
rootDir: string
|
|
34
34
|
}[] {
|
|
35
35
|
return injectedPackageNames.map((packageName) => {
|
|
36
36
|
let { packageRoot } = getResolvedInjectedPackage(packageName)
|
|
37
|
-
let {
|
|
37
|
+
let { fileRoot, routeRoot } = getInjectedPackageRoute(packageRoot, packageName)
|
|
38
38
|
|
|
39
39
|
return {
|
|
40
|
-
|
|
41
|
-
[
|
|
40
|
+
mounts: {
|
|
41
|
+
[getInjectedPackageMountPath(packageName)]: fileRoot,
|
|
42
42
|
},
|
|
43
43
|
rootDir: routeRoot,
|
|
44
44
|
}
|
|
45
45
|
})
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
export function getInjectedPackageRoots(): readonly string[] {
|
|
49
|
+
return injectedPackageNames.map(
|
|
50
|
+
(packageName) => getResolvedInjectedPackage(packageName).packageRoot,
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
48
54
|
export function getInjectedPackageNameForSpecifier(specifier: string): string | null {
|
|
49
55
|
for (let injectedSpecifier of generatedInjectedPackageSpecifiers) {
|
|
50
56
|
if (specifier === injectedSpecifier) {
|
|
@@ -119,17 +125,17 @@ function getResolvedInjectedPackage(packageName: string): ResolvedInjectedPackag
|
|
|
119
125
|
return resolvedInjectedPackage
|
|
120
126
|
}
|
|
121
127
|
|
|
122
|
-
function
|
|
123
|
-
return `${injectedPackagesBasePath}/${packageName}
|
|
128
|
+
function getInjectedPackageMountPath(packageName: string): string {
|
|
129
|
+
return `${injectedPackagesBasePath}/${packageName}`
|
|
124
130
|
}
|
|
125
131
|
|
|
126
132
|
function getInjectedPackageRoute(
|
|
127
133
|
packageRoot: string,
|
|
128
134
|
packageName: string,
|
|
129
|
-
): {
|
|
135
|
+
): { fileRoot: string; routeRoot: string } {
|
|
130
136
|
if (!packageRoot.endsWith(`/${packageName}`)) {
|
|
131
137
|
return {
|
|
132
|
-
|
|
138
|
+
fileRoot: packageRoot.slice(getFilePathDirectory(packageRoot).length + 1),
|
|
133
139
|
routeRoot: getFilePathDirectory(packageRoot),
|
|
134
140
|
}
|
|
135
141
|
}
|
|
@@ -141,7 +147,7 @@ function getInjectedPackageRoute(
|
|
|
141
147
|
}
|
|
142
148
|
|
|
143
149
|
return {
|
|
144
|
-
|
|
150
|
+
fileRoot: packageName,
|
|
145
151
|
routeRoot,
|
|
146
152
|
}
|
|
147
153
|
}
|
|
@@ -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
|
+
}
|