@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/dist/lib/routes.js
CHANGED
|
@@ -1,99 +1,143 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
throw new Error('createAssetServer() requires at least one configured fileMap entry.');
|
|
14
|
-
}
|
|
15
|
-
let compiledRoutes = routeConfigs.flatMap((routeConfig) => Object.entries(routeConfig.fileMap).map(([urlPattern, filePattern]) => compileRoute({
|
|
16
|
-
filePattern,
|
|
17
|
-
urlPattern,
|
|
18
|
-
}, {
|
|
19
|
-
basePath,
|
|
20
|
-
rootDir: routeConfig.rootDir,
|
|
21
|
-
})));
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import { isAbsoluteFilePath, normalizeFilePath, normalizePathname, resolveFilePath, } from './paths.js';
|
|
3
|
+
export function compileRoutes(basePath, mountConfigs) {
|
|
4
|
+
let compiledMounts = mountConfigs.flatMap((mountConfig) => {
|
|
5
|
+
let configMounts = Object.entries(mountConfig.mounts).map(([urlRoot, fileRoot]) => compileMount(urlRoot, fileRoot, {
|
|
6
|
+
basePath,
|
|
7
|
+
rootDir: mountConfig.rootDir,
|
|
8
|
+
}));
|
|
9
|
+
validateNoOverlappingMounts(configMounts);
|
|
10
|
+
return configMounts;
|
|
11
|
+
});
|
|
12
|
+
validateNoOverlappingUrlRoots(compiledMounts);
|
|
22
13
|
return {
|
|
23
14
|
resolveUrlPathname(pathname) {
|
|
24
|
-
|
|
25
|
-
for (let route of compiledRoutes) {
|
|
26
|
-
let match = route.urlMatcher.match(`http://remix.run${normalizedPathname}`);
|
|
27
|
-
if (!match)
|
|
28
|
-
continue;
|
|
29
|
-
let relativeFilePath = decodeURIComponent(createHref(route.filePattern, match.params)).replace(/^\/+/, '');
|
|
30
|
-
return resolveFilePath(route.rootDir, relativeFilePath);
|
|
31
|
-
}
|
|
32
|
-
return null;
|
|
15
|
+
return matchUrlPathname(pathname)?.filePath ?? null;
|
|
33
16
|
},
|
|
17
|
+
matchUrlPathname,
|
|
34
18
|
toUrlPathname(filePath) {
|
|
35
|
-
|
|
36
|
-
for (let route of compiledRoutes) {
|
|
37
|
-
let relativeFilePath = getRelativeFilePath(route.rootDir, normalizedFilePath);
|
|
38
|
-
let match = route.fileMatcher.match(`http://remix.run/${relativeFilePath}`);
|
|
39
|
-
if (!match)
|
|
40
|
-
continue;
|
|
41
|
-
return normalizePathname(createHref(route.urlPattern, match.params));
|
|
42
|
-
}
|
|
43
|
-
return null;
|
|
19
|
+
return matchFilePath(filePath)?.urlPathname ?? null;
|
|
44
20
|
},
|
|
21
|
+
matchFilePath,
|
|
45
22
|
};
|
|
23
|
+
function matchUrlPathname(pathname) {
|
|
24
|
+
let normalizedPathname = normalizePathname(pathname);
|
|
25
|
+
for (let mount of compiledMounts) {
|
|
26
|
+
let relativePathname = getPathWithinRoot(mount.urlRoot, normalizedPathname);
|
|
27
|
+
if (relativePathname === null)
|
|
28
|
+
continue;
|
|
29
|
+
let filePath = resolveFilePath(mount.fileRoot, decodeURIComponent(relativePathname));
|
|
30
|
+
if (getPathWithinRoot(mount.fileRoot, filePath) === null)
|
|
31
|
+
return null;
|
|
32
|
+
return {
|
|
33
|
+
filePath,
|
|
34
|
+
fileRoot: mount.fileRootValue,
|
|
35
|
+
urlPathname: normalizedPathname,
|
|
36
|
+
urlRoot: mount.urlRoot,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
function matchFilePath(filePath) {
|
|
42
|
+
let normalizedFilePath = normalizeFilePath(filePath);
|
|
43
|
+
for (let mount of compiledMounts) {
|
|
44
|
+
let relativeFilePath = getPathWithinRoot(mount.fileRoot, normalizedFilePath);
|
|
45
|
+
if (relativeFilePath === null)
|
|
46
|
+
continue;
|
|
47
|
+
let encodedFilePath = relativeFilePath.split('/').map(encodeURIComponent).join('/');
|
|
48
|
+
return {
|
|
49
|
+
filePath: normalizedFilePath,
|
|
50
|
+
fileRoot: mount.fileRootValue,
|
|
51
|
+
urlPathname: joinUrlPath(mount.urlRoot, encodedFilePath),
|
|
52
|
+
urlRoot: mount.urlRoot,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
46
57
|
}
|
|
47
|
-
function
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
let filePatternSource = normalizeFilePattern(route.filePattern);
|
|
52
|
-
let urlPattern = RoutePattern.parse(urlPatternSource);
|
|
53
|
-
let filePattern = RoutePattern.parse(filePatternSource);
|
|
54
|
-
validateNoUnnamedWildcards(urlPattern, 'URL');
|
|
55
|
-
validateNoUnnamedWildcards(filePattern, 'File');
|
|
56
|
-
validateRoutePatterns(urlPattern, filePattern);
|
|
58
|
+
function compileMount(urlRoot, fileRoot, options) {
|
|
59
|
+
if (isAbsoluteFilePath(fileRoot)) {
|
|
60
|
+
throw new TypeError(`mounts values must be relative to rootDir. Received "${fileRoot}".`);
|
|
61
|
+
}
|
|
57
62
|
return {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
fileMatcher: createMatcher(stripDotSegments(filePatternSource)),
|
|
63
|
+
fileRoot: resolveMountFileRoot(options.rootDir, fileRoot),
|
|
64
|
+
fileRootValue: fileRoot,
|
|
65
|
+
urlRoot: joinUrlPath(normalizeMountUrlRoot(options.basePath), normalizeMountUrlRoot(urlRoot)),
|
|
66
|
+
urlRootKey: urlRoot,
|
|
63
67
|
};
|
|
64
68
|
}
|
|
65
|
-
function
|
|
66
|
-
let
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
74
|
-
segments.push(segment);
|
|
69
|
+
function normalizeMountUrlRoot(urlRoot) {
|
|
70
|
+
let normalizedRoot = normalizePathname(urlRoot).replace(/\/+$/, '') || '/';
|
|
71
|
+
let url = new URL(normalizedRoot, 'http://remix.run');
|
|
72
|
+
if (url.search !== '' ||
|
|
73
|
+
url.hash !== '' ||
|
|
74
|
+
getUrlPathSegmentCount(url.pathname) !== getUrlPathSegmentCount(normalizedRoot)) {
|
|
75
|
+
throw new TypeError(`mounts keys must be URL pathnames without query strings, fragments, or encoded dot segments. Received "${urlRoot}".`);
|
|
75
76
|
}
|
|
76
|
-
return
|
|
77
|
+
return url.pathname.replace(/\/+$/, '') || '/';
|
|
77
78
|
}
|
|
78
|
-
function
|
|
79
|
-
let
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
79
|
+
function resolveMountFileRoot(rootDir, fileRoot) {
|
|
80
|
+
let resolvedRoot = resolveFilePath(rootDir, fileRoot);
|
|
81
|
+
try {
|
|
82
|
+
resolvedRoot = normalizeFilePath(fs.realpathSync(resolvedRoot));
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
if (!isUnresolvedPathError(error, resolvedRoot))
|
|
86
|
+
throw error;
|
|
83
87
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
88
|
+
return resolvedRoot.replace(/\/+$/, '') || '/';
|
|
89
|
+
}
|
|
90
|
+
function getUrlPathSegmentCount(pathname) {
|
|
91
|
+
return pathname.split('/').filter((segment) => segment !== '').length;
|
|
92
|
+
}
|
|
93
|
+
function joinUrlPath(root, path) {
|
|
94
|
+
if (path === '' || path === '/')
|
|
95
|
+
return normalizeMountUrlRoot(root);
|
|
96
|
+
return normalizePathname(`${root.replace(/\/+$/, '')}/${path.replace(/^\/+/, '')}`);
|
|
97
|
+
}
|
|
98
|
+
function getPathWithinRoot(root, value) {
|
|
99
|
+
if (value === root)
|
|
100
|
+
return '';
|
|
101
|
+
if (root === '/')
|
|
102
|
+
return value.slice(1);
|
|
103
|
+
if (!value.startsWith(`${root}/`))
|
|
104
|
+
return null;
|
|
105
|
+
return value.slice(root.length + 1);
|
|
106
|
+
}
|
|
107
|
+
function validateNoOverlappingMounts(mounts) {
|
|
108
|
+
for (let index = 0; index < mounts.length; index++) {
|
|
109
|
+
let mount = mounts[index];
|
|
110
|
+
for (let otherIndex = index + 1; otherIndex < mounts.length; otherIndex++) {
|
|
111
|
+
let otherMount = mounts[otherIndex];
|
|
112
|
+
if (rootsOverlap(mount.fileRoot, otherMount.fileRoot)) {
|
|
113
|
+
throw new TypeError(`mounts values must not overlap. Received "${mount.fileRootValue}" and "${otherMount.fileRootValue}", resolving to "${mount.fileRoot}" and "${otherMount.fileRoot}".`);
|
|
114
|
+
}
|
|
89
115
|
}
|
|
90
116
|
}
|
|
91
117
|
}
|
|
92
|
-
function
|
|
93
|
-
|
|
94
|
-
|
|
118
|
+
function validateNoOverlappingUrlRoots(mounts) {
|
|
119
|
+
for (let index = 0; index < mounts.length; index++) {
|
|
120
|
+
let mount = mounts[index];
|
|
121
|
+
for (let otherIndex = index + 1; otherIndex < mounts.length; otherIndex++) {
|
|
122
|
+
let otherMount = mounts[otherIndex];
|
|
123
|
+
if (!rootsOverlap(mount.urlRoot, otherMount.urlRoot))
|
|
124
|
+
continue;
|
|
125
|
+
throw new TypeError(`mounts keys must not overlap. Received "${mount.urlRootKey}" and "${otherMount.urlRootKey}".`);
|
|
126
|
+
}
|
|
95
127
|
}
|
|
96
128
|
}
|
|
97
|
-
function
|
|
98
|
-
return
|
|
129
|
+
function rootsOverlap(root, otherRoot) {
|
|
130
|
+
return (root === otherRoot ||
|
|
131
|
+
root === '/' ||
|
|
132
|
+
otherRoot === '/' ||
|
|
133
|
+
root.startsWith(`${otherRoot}/`) ||
|
|
134
|
+
otherRoot.startsWith(`${root}/`));
|
|
135
|
+
}
|
|
136
|
+
function isUnresolvedPathError(error, filePath) {
|
|
137
|
+
// Windows reports UNKNOWN rather than ENOENT when a UNC share cannot be reached.
|
|
138
|
+
return (error instanceof Error &&
|
|
139
|
+
'code' in error &&
|
|
140
|
+
(error.code === 'ENOENT' ||
|
|
141
|
+
error.code === 'ENOTDIR' ||
|
|
142
|
+
(error.code === 'UNKNOWN' && filePath.startsWith('//'))));
|
|
99
143
|
}
|
|
@@ -315,8 +315,8 @@ export function createScriptCompiler(options) {
|
|
|
315
315
|
function getStableUrl(identityPath) {
|
|
316
316
|
let stableUrlPathname = resolvedOptions.routes.toUrlPathname(identityPath);
|
|
317
317
|
if (!stableUrlPathname) {
|
|
318
|
-
throw createAssetServerCompilationError(`File ${identityPath} is outside all configured
|
|
319
|
-
code: '
|
|
318
|
+
throw createAssetServerCompilationError(`File ${identityPath} is outside all configured mounts.`, {
|
|
319
|
+
code: 'FILE_OUTSIDE_MOUNTS',
|
|
320
320
|
});
|
|
321
321
|
}
|
|
322
322
|
return stableUrlPathname;
|
|
@@ -35,7 +35,7 @@ export async function resolveModule(record, transformed, args) {
|
|
|
35
35
|
let resolvedSpec = resolvedImports.get(unresolved.specifier);
|
|
36
36
|
if (!resolvedSpec?.absolutePath) {
|
|
37
37
|
return failResolve(createAssetServerCompilationError(`Failed to resolve import "${displaySpecifier}" in ${transformed.resolvedPath}. ` +
|
|
38
|
-
`Ensure it resolves to a file within
|
|
38
|
+
`Ensure it resolves to a file within a configured asset server mount, or mark it as external.`, {
|
|
39
39
|
code: 'IMPORT_RESOLUTION_FAILED',
|
|
40
40
|
}), trackedFiles, trackedResolutions, transformed.resolvedPath, { isWatchIgnored: args.isWatchIgnored, trackedResolution });
|
|
41
41
|
}
|
|
@@ -54,9 +54,9 @@ export async function resolveModule(record, transformed, args) {
|
|
|
54
54
|
}
|
|
55
55
|
let stableUrlPathname = args.routes.toUrlPathname(resolvedImport.identityPath);
|
|
56
56
|
if (!stableUrlPathname) {
|
|
57
|
-
return failResolve(createAssetServerCompilationError(`Import "${displaySpecifier}" in ${transformed.resolvedPath}, resolved to "${resolvedImport.identityPath}", is outside all configured
|
|
58
|
-
`Add a matching
|
|
59
|
-
code: '
|
|
57
|
+
return failResolve(createAssetServerCompilationError(`Import "${displaySpecifier}" in ${transformed.resolvedPath}, resolved to "${resolvedImport.identityPath}", is outside all configured mounts. ` +
|
|
58
|
+
`Add a matching mount for this file path, or mark this import as external.`, {
|
|
59
|
+
code: 'IMPORT_OUTSIDE_MOUNTS',
|
|
60
60
|
}), trackedFiles, trackedResolutions, transformed.resolvedPath, { isWatchIgnored: args.isWatchIgnored, trackedResolution });
|
|
61
61
|
}
|
|
62
62
|
deps.add(resolvedImport.identityPath);
|
|
@@ -98,7 +98,7 @@ export async function resolveModule(record, transformed, args) {
|
|
|
98
98
|
}
|
|
99
99
|
if (!resolvedSpec?.absolutePath) {
|
|
100
100
|
return failResolve(createAssetServerCompilationError(`Failed to resolve accepted HMR dependency "${displaySpecifier}" in ${transformed.resolvedPath}. ` +
|
|
101
|
-
`Ensure it resolves to a file within
|
|
101
|
+
`Ensure it resolves to a file within a configured asset server mount, or mark it as external.`, {
|
|
102
102
|
code: 'IMPORT_RESOLUTION_FAILED',
|
|
103
103
|
}), trackedFiles, trackedResolutions, transformed.resolvedPath, { isWatchIgnored: args.isWatchIgnored, trackedResolution });
|
|
104
104
|
}
|
|
@@ -117,9 +117,9 @@ export async function resolveModule(record, transformed, args) {
|
|
|
117
117
|
}
|
|
118
118
|
let stableUrlPathname = args.routes.toUrlPathname(resolvedImport.identityPath);
|
|
119
119
|
if (!stableUrlPathname) {
|
|
120
|
-
return failResolve(createAssetServerCompilationError(`Accepted HMR dependency "${displaySpecifier}" in ${transformed.resolvedPath}, resolved to "${resolvedImport.identityPath}", is outside all configured
|
|
121
|
-
`Add a matching
|
|
122
|
-
code: '
|
|
120
|
+
return failResolve(createAssetServerCompilationError(`Accepted HMR dependency "${displaySpecifier}" in ${transformed.resolvedPath}, resolved to "${resolvedImport.identityPath}", is outside all configured mounts. ` +
|
|
121
|
+
`Add a matching mount for this file path, or mark this import as external.`, {
|
|
122
|
+
code: 'IMPORT_OUTSIDE_MOUNTS',
|
|
123
123
|
}), trackedFiles, trackedResolutions, transformed.resolvedPath, { isWatchIgnored: args.isWatchIgnored, trackedResolution });
|
|
124
124
|
}
|
|
125
125
|
if (trackedResolution) {
|
|
@@ -234,7 +234,7 @@ async function batchResolveSpecifiers(specifiers, importerPath, resolverFactory)
|
|
|
234
234
|
throw createAssetServerCompilationError(normalizedResolution.importerPath === getInjectedPackageImporterPath()
|
|
235
235
|
? `Failed to resolve injected import "${specifier}" from asset server.`
|
|
236
236
|
: `Failed to resolve import "${normalizedResolution.specifier}" in ${normalizedResolution.importerPath}. ` +
|
|
237
|
-
`Ensure it resolves to a file within
|
|
237
|
+
`Ensure it resolves to a file within a configured asset server mount, or mark it as external.`, {
|
|
238
238
|
code: 'IMPORT_RESOLUTION_FAILED',
|
|
239
239
|
});
|
|
240
240
|
}
|
|
@@ -109,8 +109,8 @@ export async function transformModule(record, args) {
|
|
|
109
109
|
try {
|
|
110
110
|
let stableUrlPathname = args.routes.toUrlPathname(record.identityPath);
|
|
111
111
|
if (!stableUrlPathname) {
|
|
112
|
-
throw createAssetServerCompilationError(`File ${record.identityPath} is outside all configured
|
|
113
|
-
code: '
|
|
112
|
+
throw createAssetServerCompilationError(`File ${record.identityPath} is outside all configured mounts.`, {
|
|
113
|
+
code: 'FILE_OUTSIDE_MOUNTS',
|
|
114
114
|
});
|
|
115
115
|
}
|
|
116
116
|
let analysis = await analyzeModuleSource(sourceText, resolvedPath, transformOptions, {
|
|
@@ -70,8 +70,8 @@ export function resolveServedStyleOrThrow(filePath, args) {
|
|
|
70
70
|
}
|
|
71
71
|
let stableUrlPathname = args.routes.toUrlPathname(identityPath);
|
|
72
72
|
if (!stableUrlPathname) {
|
|
73
|
-
throw createAssetServerCompilationError(`File ${identityPath} is outside all configured
|
|
74
|
-
code: '
|
|
73
|
+
throw createAssetServerCompilationError(`File ${identityPath} is outside all configured mounts.`, {
|
|
74
|
+
code: 'FILE_OUTSIDE_MOUNTS',
|
|
75
75
|
});
|
|
76
76
|
}
|
|
77
77
|
return { identityPath, stableUrlPathname };
|
|
@@ -107,9 +107,9 @@ function resolveImportDependency(url, importerPath, placeholder, args) {
|
|
|
107
107
|
});
|
|
108
108
|
}
|
|
109
109
|
if (!args.routes.toUrlPathname(identityPath)) {
|
|
110
|
-
throw createAssetServerCompilationError(`Import "${url}" in ${importerPath}, resolved to "${identityPath}", is outside all configured
|
|
111
|
-
`Add a matching
|
|
112
|
-
code: '
|
|
110
|
+
throw createAssetServerCompilationError(`Import "${url}" in ${importerPath}, resolved to "${identityPath}", is outside all configured mounts. ` +
|
|
111
|
+
`Add a matching mount for this file path, or mark this import as external.`, {
|
|
112
|
+
code: 'IMPORT_OUTSIDE_MOUNTS',
|
|
113
113
|
});
|
|
114
114
|
}
|
|
115
115
|
return {
|
|
@@ -156,9 +156,9 @@ function resolveUrlDependency(url, importerPath, placeholder, args) {
|
|
|
156
156
|
});
|
|
157
157
|
}
|
|
158
158
|
if (!args.routes.toUrlPathname(identityPath)) {
|
|
159
|
-
throw createAssetServerCompilationError(`URL "${url}" in ${importerPath}, resolved to "${identityPath}", is outside all configured
|
|
160
|
-
`Add a matching
|
|
161
|
-
code: '
|
|
159
|
+
throw createAssetServerCompilationError(`URL "${url}" in ${importerPath}, resolved to "${identityPath}", is outside all configured mounts. ` +
|
|
160
|
+
`Add a matching mount for this file path.`, {
|
|
161
|
+
code: 'URL_OUTSIDE_MOUNTS',
|
|
162
162
|
});
|
|
163
163
|
}
|
|
164
164
|
let parsedRequest = parseResolvedFileRequest({ hash, search });
|
|
@@ -34,8 +34,8 @@ export async function transformStyle(record, args) {
|
|
|
34
34
|
try {
|
|
35
35
|
let stableUrlPathname = args.routes.toUrlPathname(record.identityPath);
|
|
36
36
|
if (!stableUrlPathname) {
|
|
37
|
-
throw createAssetServerCompilationError(`File ${record.identityPath} is outside all configured
|
|
38
|
-
code: '
|
|
37
|
+
throw createAssetServerCompilationError(`File ${record.identityPath} is outside all configured mounts.`, {
|
|
38
|
+
code: 'FILE_OUTSIDE_MOUNTS',
|
|
39
39
|
});
|
|
40
40
|
}
|
|
41
41
|
let transformResult = runLightningTransform(resolvedPath, rawBytes, {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remix-run/assets",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Fetch-based server for compiling browser JS/TS and CSS assets on demand",
|
|
5
5
|
"author": "Michael Jackson <mjijackson@gmail.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -42,19 +42,18 @@
|
|
|
42
42
|
"picomatch": "^4.0.4",
|
|
43
43
|
"source-map-js": "^1.2.1",
|
|
44
44
|
"@remix-run/file-storage": "^0.13.7",
|
|
45
|
-
"@remix-run/headers": "^0.21.1",
|
|
46
45
|
"@remix-run/mime": "^0.4.2",
|
|
47
|
-
"@remix-run/
|
|
46
|
+
"@remix-run/headers": "^0.21.1"
|
|
48
47
|
},
|
|
49
48
|
"devDependencies": {
|
|
50
49
|
"@types/node": "^24.6.0",
|
|
51
50
|
"@types/picomatch": "^4.0.3",
|
|
52
51
|
"typescript": "^7.0.2",
|
|
53
|
-
"@remix-run/assert": "0.3.0",
|
|
54
|
-
"@remix-run/node-hmr": "^0.1.0",
|
|
55
52
|
"@remix-run/node-tsx": "^0.1.1",
|
|
53
|
+
"@remix-run/node-hmr": "^0.1.0",
|
|
54
|
+
"@remix-run/assert": "0.3.0",
|
|
56
55
|
"@remix-run/test": "0.6.0",
|
|
57
|
-
"@remix-run/ui": "0.
|
|
56
|
+
"@remix-run/ui": "0.8.0"
|
|
58
57
|
},
|
|
59
58
|
"keywords": [
|
|
60
59
|
"remix",
|
package/src/assets.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { createAssetServer } from './lib/asset-server.ts'
|
|
2
2
|
export { defineFileTransform } from './lib/files/config.ts'
|
|
3
|
+
export type { AssetAccessDetails, AssetAccessRule } from './lib/access.ts'
|
|
3
4
|
export type { AssetServer, AssetServerOptions, BrowserHmrChannel } from './lib/asset-server.ts'
|
|
5
|
+
export type { AssetDetails, AssetKind, AssetStatus } from './lib/inspection.ts'
|
|
4
6
|
export type { ModuleLoader } from './lib/loaders.ts'
|
package/src/lib/access.ts
CHANGED
|
@@ -4,9 +4,30 @@ import { createFileMatcher } from './file-matcher.ts'
|
|
|
4
4
|
import { isInjectedPackageFilePath } from './injected-packages.ts'
|
|
5
5
|
import { normalizeFilePath } from './paths.ts'
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
/** Access-policy result for an inspected asset file. */
|
|
8
|
+
export interface AssetAccessDetails {
|
|
9
|
+
/** Whether the asset server may serve the file. */
|
|
10
|
+
allowed: boolean
|
|
11
|
+
/** The first configured rule that allowed the file, when one matched. */
|
|
12
|
+
allowedBy?: AssetAccessRule
|
|
13
|
+
/** The first matching `denyFiles` pattern, when access was denied. */
|
|
14
|
+
deniedBy?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Rule that allows an inspected asset file to be served. */
|
|
18
|
+
export type AssetAccessRule =
|
|
19
|
+
/** A matching `allowFiles` entry. */
|
|
20
|
+
| { kind: 'file'; value: string }
|
|
21
|
+
/** A runtime file provided internally by the asset server. */
|
|
22
|
+
| { kind: 'injected'; value: string }
|
|
23
|
+
/** A matching `allowPackages` entry. */
|
|
24
|
+
| { kind: 'package'; value: string }
|
|
25
|
+
|
|
26
|
+
export type AccessPolicy = {
|
|
27
|
+
getAllowedPackageRoots(): readonly string[]
|
|
8
28
|
getPackageWatchDirectories(): readonly string[]
|
|
9
29
|
handleFileEvent(filePath: string): void
|
|
30
|
+
inspect(filePath: string): AssetAccessDetails
|
|
10
31
|
isAllowed(filePath: string): boolean
|
|
11
32
|
}
|
|
12
33
|
|
|
@@ -29,13 +50,15 @@ export function createAccessPolicy(options: {
|
|
|
29
50
|
packageSearchRoots?: readonly string[]
|
|
30
51
|
rootDir: string
|
|
31
52
|
}): AccessPolicy {
|
|
32
|
-
let allowMatchers = options.allowFiles.map((pattern) =>
|
|
33
|
-
createFileMatcher(pattern, options.rootDir),
|
|
34
|
-
|
|
53
|
+
let allowMatchers = options.allowFiles.map((pattern) => ({
|
|
54
|
+
matcher: createFileMatcher(pattern, options.rootDir),
|
|
55
|
+
pattern,
|
|
56
|
+
}))
|
|
35
57
|
let allowPackageNames = normalizePackageNames(options.allowPackages, 'allowPackages')
|
|
36
|
-
let denyMatchers = (options.denyFiles ?? []).map((pattern) =>
|
|
37
|
-
createFileMatcher(pattern, options.rootDir),
|
|
38
|
-
|
|
58
|
+
let denyMatchers = (options.denyFiles ?? []).map((pattern) => ({
|
|
59
|
+
matcher: createFileMatcher(pattern, options.rootDir),
|
|
60
|
+
pattern,
|
|
61
|
+
}))
|
|
39
62
|
let packageSearchRoots = [options.rootDir, ...(options.packageSearchRoots ?? [])]
|
|
40
63
|
let packageRootPaths = createPackageRootPaths({
|
|
41
64
|
allowPackageNames,
|
|
@@ -57,14 +80,44 @@ export function createAccessPolicy(options: {
|
|
|
57
80
|
packageRootsDirty = false
|
|
58
81
|
}
|
|
59
82
|
|
|
60
|
-
function
|
|
61
|
-
if (allowPackageNames.size === 0) return
|
|
83
|
+
function getAllowedPackageName(filePath: string): string | undefined {
|
|
84
|
+
if (allowPackageNames.size === 0) return undefined
|
|
62
85
|
refreshPackageRootPathTries()
|
|
63
86
|
|
|
64
|
-
return
|
|
87
|
+
return getPackageNameFromRootPathTrie(filePath, allowPackageRootPathTrie)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function inspect(filePath: string): AssetAccessDetails {
|
|
91
|
+
if (isInjectedPackageFilePath(filePath)) {
|
|
92
|
+
return { allowed: true, allowedBy: { kind: 'injected', value: '@remix-run/assets' } }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let allowedBy: AssetAccessRule | undefined
|
|
96
|
+
let allowMatch = allowMatchers.find(({ matcher }) => matcher(filePath))
|
|
97
|
+
if (allowMatch) {
|
|
98
|
+
allowedBy = { kind: 'file', value: allowMatch.pattern }
|
|
99
|
+
} else {
|
|
100
|
+
let packageName = getAllowedPackageName(filePath)
|
|
101
|
+
if (packageName !== undefined) {
|
|
102
|
+
allowedBy = { kind: 'package', value: packageName }
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!allowedBy) return { allowed: false }
|
|
107
|
+
|
|
108
|
+
let denyMatch = denyMatchers.find(({ matcher }) => matcher(filePath))
|
|
109
|
+
if (denyMatch) {
|
|
110
|
+
return { allowed: false, allowedBy, deniedBy: denyMatch.pattern }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return { allowed: true, allowedBy }
|
|
65
114
|
}
|
|
66
115
|
|
|
67
116
|
return {
|
|
117
|
+
getAllowedPackageRoots() {
|
|
118
|
+
refreshPackageRootPathTries()
|
|
119
|
+
return [...packageRootPaths.keys()]
|
|
120
|
+
},
|
|
68
121
|
getPackageWatchDirectories() {
|
|
69
122
|
if (allowPackageNames.size === 0) return []
|
|
70
123
|
return packageStateDirectories
|
|
@@ -75,13 +128,9 @@ export function createAccessPolicy(options: {
|
|
|
75
128
|
|
|
76
129
|
packageRootsDirty = true
|
|
77
130
|
},
|
|
131
|
+
inspect,
|
|
78
132
|
isAllowed(filePath) {
|
|
79
|
-
|
|
80
|
-
if (!allowMatchers.some((matcher) => matcher(filePath)) && !isAllowedPackage(filePath)) {
|
|
81
|
-
return false
|
|
82
|
-
}
|
|
83
|
-
if (denyMatchers.length > 0 && denyMatchers.some((matcher) => matcher(filePath))) return false
|
|
84
|
-
return true
|
|
133
|
+
return inspect(filePath).allowed
|
|
85
134
|
},
|
|
86
135
|
}
|
|
87
136
|
}
|
|
@@ -121,7 +170,7 @@ type PackageJson = {
|
|
|
121
170
|
|
|
122
171
|
type PackageRootPathTrie = {
|
|
123
172
|
children: Map<string, PackageRootPathTrie>
|
|
124
|
-
|
|
173
|
+
packageName?: string
|
|
125
174
|
}
|
|
126
175
|
|
|
127
176
|
type PackageRootQueueItem = {
|
|
@@ -132,8 +181,8 @@ type PackageRootQueueItem = {
|
|
|
132
181
|
function createPackageRootPaths(options: {
|
|
133
182
|
allowPackageNames: ReadonlySet<string>
|
|
134
183
|
searchRoots: readonly string[]
|
|
135
|
-
}):
|
|
136
|
-
let allowPackageRootPaths = new
|
|
184
|
+
}): Map<string, string> {
|
|
185
|
+
let allowPackageRootPaths = new Map<string, string>()
|
|
137
186
|
let allowQueue: PackageRootQueueItem[] = []
|
|
138
187
|
let seenAllowedPackageRoots = new Set<string>()
|
|
139
188
|
let searchRoots = normalizePackageSearchRoots(options.searchRoots)
|
|
@@ -155,13 +204,13 @@ function createPackageRootPaths(options: {
|
|
|
155
204
|
}
|
|
156
205
|
|
|
157
206
|
while (allowQueue.length > 0) {
|
|
158
|
-
let { packageJsonPath } = allowQueue.shift()!
|
|
207
|
+
let { packageJsonPath, packageName } = allowQueue.shift()!
|
|
159
208
|
let packageRootPath = normalizeFilePath(path.dirname(packageJsonPath))
|
|
160
209
|
if (seenAllowedPackageRoots.has(packageRootPath)) continue
|
|
161
210
|
seenAllowedPackageRoots.add(packageRootPath)
|
|
162
211
|
|
|
163
212
|
let packageJson = readPackageJson(packageJsonPath)
|
|
164
|
-
allowPackageRootPaths.
|
|
213
|
+
allowPackageRootPaths.set(packageRootPath, packageName)
|
|
165
214
|
|
|
166
215
|
for (let dependencyName of Object.keys(packageJson.dependencies ?? {})) {
|
|
167
216
|
validatePackageName(
|
|
@@ -197,10 +246,12 @@ function createPackageRootPaths(options: {
|
|
|
197
246
|
return allowPackageRootPaths
|
|
198
247
|
}
|
|
199
248
|
|
|
200
|
-
function createPackageRootPathTrie(
|
|
249
|
+
function createPackageRootPathTrie(
|
|
250
|
+
packageRootPaths: ReadonlyMap<string, string>,
|
|
251
|
+
): PackageRootPathTrie {
|
|
201
252
|
let rootNode = createPackageRootPathTrieNode()
|
|
202
253
|
|
|
203
|
-
for (let packageRootPath of packageRootPaths) {
|
|
254
|
+
for (let [packageRootPath, packageName] of packageRootPaths) {
|
|
204
255
|
let node = rootNode
|
|
205
256
|
for (let segment of getFilePathSegments(packageRootPath)) {
|
|
206
257
|
let childNode = node.children.get(segment)
|
|
@@ -210,7 +261,7 @@ function createPackageRootPathTrie(packageRootPaths: ReadonlySet<string>): Packa
|
|
|
210
261
|
}
|
|
211
262
|
node = childNode
|
|
212
263
|
}
|
|
213
|
-
node.
|
|
264
|
+
node.packageName = packageName
|
|
214
265
|
}
|
|
215
266
|
|
|
216
267
|
return rootNode
|
|
@@ -219,22 +270,24 @@ function createPackageRootPathTrie(packageRootPaths: ReadonlySet<string>): Packa
|
|
|
219
270
|
function createPackageRootPathTrieNode(): PackageRootPathTrie {
|
|
220
271
|
return {
|
|
221
272
|
children: new Map(),
|
|
222
|
-
packageRoot: false,
|
|
223
273
|
}
|
|
224
274
|
}
|
|
225
275
|
|
|
226
|
-
function
|
|
276
|
+
function getPackageNameFromRootPathTrie(
|
|
277
|
+
filePath: string,
|
|
278
|
+
trie: PackageRootPathTrie,
|
|
279
|
+
): string | undefined {
|
|
227
280
|
let node = trie
|
|
228
|
-
if (node.
|
|
281
|
+
if (node.packageName !== undefined) return node.packageName
|
|
229
282
|
|
|
230
283
|
for (let segment of getFilePathSegments(filePath)) {
|
|
231
284
|
let childNode = node.children.get(segment)
|
|
232
|
-
if (!childNode) return
|
|
233
|
-
if (childNode.
|
|
285
|
+
if (!childNode) return undefined
|
|
286
|
+
if (childNode.packageName !== undefined) return childNode.packageName
|
|
234
287
|
node = childNode
|
|
235
288
|
}
|
|
236
289
|
|
|
237
|
-
return
|
|
290
|
+
return undefined
|
|
238
291
|
}
|
|
239
292
|
|
|
240
293
|
function getFilePathSegments(filePath: string): string[] {
|