@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.
Files changed (40) hide show
  1. package/README.md +85 -92
  2. package/dist/assets.d.ts +2 -0
  3. package/dist/assets.d.ts.map +1 -1
  4. package/dist/lib/access.d.ts +29 -2
  5. package/dist/lib/access.d.ts.map +1 -1
  6. package/dist/lib/access.js +52 -26
  7. package/dist/lib/asset-server.d.ts +21 -7
  8. package/dist/lib/asset-server.d.ts.map +1 -1
  9. package/dist/lib/asset-server.js +30 -15
  10. package/dist/lib/compilation-error.d.ts +1 -1
  11. package/dist/lib/compilation-error.d.ts.map +1 -1
  12. package/dist/lib/files/compiler.js +2 -2
  13. package/dist/lib/injected-packages.d.ts +3 -2
  14. package/dist/lib/injected-packages.d.ts.map +1 -1
  15. package/dist/lib/injected-packages.js +11 -8
  16. package/dist/lib/inspection.d.ts +39 -0
  17. package/dist/lib/inspection.d.ts.map +1 -0
  18. package/dist/lib/inspection.js +160 -0
  19. package/dist/lib/routes.d.ts +11 -3
  20. package/dist/lib/routes.d.ts.map +1 -1
  21. package/dist/lib/routes.js +124 -80
  22. package/dist/lib/scripts/compiler.js +2 -2
  23. package/dist/lib/scripts/resolve.js +9 -9
  24. package/dist/lib/scripts/transform.js +2 -2
  25. package/dist/lib/styles/resolve.js +8 -8
  26. package/dist/lib/styles/transform.js +2 -2
  27. package/package.json +5 -6
  28. package/src/assets.ts +2 -0
  29. package/src/lib/access.ts +83 -30
  30. package/src/lib/asset-server.ts +50 -22
  31. package/src/lib/compilation-error.ts +3 -3
  32. package/src/lib/files/compiler.ts +2 -2
  33. package/src/lib/injected-packages.ts +16 -10
  34. package/src/lib/inspection.ts +229 -0
  35. package/src/lib/routes.ts +158 -126
  36. package/src/lib/scripts/compiler.ts +2 -2
  37. package/src/lib/scripts/resolve.ts +9 -9
  38. package/src/lib/scripts/transform.ts +2 -2
  39. package/src/lib/styles/resolve.ts +8 -8
  40. package/src/lib/styles/transform.ts +2 -2
@@ -1,99 +1,143 @@
1
- import { getRoutePatternCaptures, RoutePattern, } from '@remix-run/route-pattern';
2
- import { createHref } from '@remix-run/route-pattern/href';
3
- import { createMatcher } from '@remix-run/route-pattern/match';
4
- import { getRelativeFilePath, isAbsoluteFilePath, normalizeFilePath, normalizePathname, resolveFilePath, } from './paths.js';
5
- function normalizeFilePattern(pattern) {
6
- if (isAbsoluteFilePath(pattern)) {
7
- throw new Error(`File route patterns must be relative to the asset server root.\nPattern: ${pattern}`);
8
- }
9
- return normalizePathname(pattern);
10
- }
11
- export function compileRoutes(basePath, routeConfigs) {
12
- if (routeConfigs.every((routeConfig) => Object.keys(routeConfig.fileMap).length === 0)) {
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
- let normalizedPathname = normalizePathname(pathname);
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
- let normalizedFilePath = normalizeFilePath(filePath);
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 compileRoute(route, options) {
48
- let basePath = normalizePathname(options.basePath).replace(/\/+$/, '') || '/';
49
- let relativeUrlPattern = normalizePathname(route.urlPattern);
50
- let urlPatternSource = normalizePathname(`${basePath.replace(/\/+$/, '')}/${relativeUrlPattern.replace(/^\/+/, '')}`);
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
- rootDir: normalizeFilePath(options.rootDir).replace(/\/+$/, ''),
59
- urlPattern,
60
- urlMatcher: createMatcher(urlPattern),
61
- filePattern,
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 stripDotSegments(pattern) {
66
- let segments = [];
67
- for (let segment of pattern.split('/')) {
68
- if (segment === '' || segment === '.')
69
- continue;
70
- if (segment === '..') {
71
- segments.pop();
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 segments.join('/');
77
+ return url.pathname.replace(/\/+$/, '') || '/';
77
78
  }
78
- function validateRoutePatterns(urlPattern, filePattern) {
79
- let urlCaptures = getPathnameCaptures(urlPattern);
80
- let fileCaptures = getPathnameCaptures(filePattern);
81
- if (urlCaptures.length !== fileCaptures.length) {
82
- throw new Error(`Route patterns must have matching capture structure.\nURL: ${urlPattern}\nFile: ${filePattern}`);
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
- for (let i = 0; i < urlCaptures.length; i++) {
85
- let urlCapture = urlCaptures[i];
86
- let fileCapture = fileCaptures[i];
87
- if (urlCapture.type !== fileCapture.type || urlCapture.name !== fileCapture.name) {
88
- throw new Error(`Route patterns must have matching capture structure.\nURL: ${urlPattern}\nFile: ${filePattern}`);
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 validateNoUnnamedWildcards(pattern, label) {
93
- if (getRoutePatternCaptures(pattern).some((capture) => capture.part === 'pathname' && capture.type === '*' && capture.name === '*')) {
94
- throw new Error(`${label} route patterns must use named wildcards for reversible mapping.\nPattern: ${pattern}`);
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 getPathnameCaptures(pattern) {
98
- return getRoutePatternCaptures(pattern).filter((capture) => capture.part === 'pathname');
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 fileMap entries.`, {
319
- code: 'FILE_OUTSIDE_FILE_MAP',
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 the configured asset server fileMap, or mark it as external.`, {
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 fileMap entries. ` +
58
- `Add a matching fileMap entry for this file path, or mark this import as external.`, {
59
- code: 'IMPORT_OUTSIDE_FILE_MAP',
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 the configured asset server fileMap, or mark it as external.`, {
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 fileMap entries. ` +
121
- `Add a matching fileMap entry for this file path, or mark this import as external.`, {
122
- code: 'IMPORT_OUTSIDE_FILE_MAP',
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 the configured asset server fileMap, or mark it as external.`, {
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 fileMap entries.`, {
113
- code: 'FILE_OUTSIDE_FILE_MAP',
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 fileMap entries.`, {
74
- code: 'FILE_OUTSIDE_FILE_MAP',
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 fileMap entries. ` +
111
- `Add a matching fileMap entry for this file path, or mark this import as external.`, {
112
- code: 'IMPORT_OUTSIDE_FILE_MAP',
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 fileMap entries. ` +
160
- `Add a matching fileMap entry for this file path.`, {
161
- code: 'URL_OUTSIDE_FILE_MAP',
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 fileMap entries.`, {
38
- code: 'FILE_OUTSIDE_FILE_MAP',
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.5.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/route-pattern": "^0.24.0"
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.5.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
- type AccessPolicy = {
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 isAllowedPackage(filePath: string): boolean {
61
- if (allowPackageNames.size === 0) return false
83
+ function getAllowedPackageName(filePath: string): string | undefined {
84
+ if (allowPackageNames.size === 0) return undefined
62
85
  refreshPackageRootPathTries()
63
86
 
64
- return isPathInPackageRootPathTrie(filePath, allowPackageRootPathTrie)
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
- if (isInjectedPackageFilePath(filePath)) return true
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
- packageRoot: boolean
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
- }): Set<string> {
136
- let allowPackageRootPaths = new Set<string>()
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.add(packageRootPath)
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(packageRootPaths: ReadonlySet<string>): PackageRootPathTrie {
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.packageRoot = true
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 isPathInPackageRootPathTrie(filePath: string, trie: PackageRootPathTrie): boolean {
276
+ function getPackageNameFromRootPathTrie(
277
+ filePath: string,
278
+ trie: PackageRootPathTrie,
279
+ ): string | undefined {
227
280
  let node = trie
228
- if (node.packageRoot) return true
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 false
233
- if (childNode.packageRoot) return true
285
+ if (!childNode) return undefined
286
+ if (childNode.packageName !== undefined) return childNode.packageName
234
287
  node = childNode
235
288
  }
236
289
 
237
- return false
290
+ return undefined
238
291
  }
239
292
 
240
293
  function getFilePathSegments(filePath: string): string[] {