@remix-run/assets 0.2.0 → 0.3.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 +35 -20
- package/dist/lib/access.d.ts.map +1 -1
- package/dist/lib/access.js +3 -0
- package/dist/lib/asset-server.d.ts +5 -2
- package/dist/lib/asset-server.d.ts.map +1 -1
- package/dist/lib/asset-server.js +19 -6
- package/dist/lib/injected-packages.d.ts +11 -0
- package/dist/lib/injected-packages.d.ts.map +1 -0
- package/dist/lib/injected-packages.js +86 -0
- package/dist/lib/paths.d.ts +1 -0
- package/dist/lib/paths.d.ts.map +1 -1
- package/dist/lib/paths.js +12 -0
- package/dist/lib/routes.d.ts +5 -7
- package/dist/lib/routes.d.ts.map +1 -1
- package/dist/lib/routes.js +13 -16
- package/dist/lib/scripts/resolve.d.ts.map +1 -1
- package/dist/lib/scripts/resolve.js +35 -8
- package/dist/lib/scripts/transform.d.ts.map +1 -1
- package/dist/lib/scripts/transform.js +78 -2
- package/package.json +7 -3
- package/src/lib/access.ts +2 -0
- package/src/lib/asset-server.ts +25 -7
- package/src/lib/injected-packages.ts +117 -0
- package/src/lib/paths.ts +28 -0
- package/src/lib/routes.ts +31 -22
- package/src/lib/scripts/resolve.ts +54 -8
- package/src/lib/scripts/transform.ts +104 -2
|
@@ -2,12 +2,15 @@ import * as fs from 'node:fs';
|
|
|
2
2
|
import * as fsp from 'node:fs/promises';
|
|
3
3
|
import * as path from 'node:path';
|
|
4
4
|
import { getTsconfig } from 'get-tsconfig';
|
|
5
|
+
import MagicString from 'magic-string';
|
|
5
6
|
import { minify } from 'oxc-minify';
|
|
7
|
+
import { parseSync, visitorKeys } from 'oxc-parser';
|
|
6
8
|
import { transform as oxcTransform } from 'oxc-transform';
|
|
7
9
|
import { init as esModuleLexerInit, parse as esModuleLexer } from 'es-module-lexer';
|
|
8
10
|
import { isCommonJS, mayContainCommonJSModuleGlobals } from "./cjs-check.js";
|
|
9
11
|
import { createAssetServerCompilationError, isAssetServerCompilationError, } from "../compilation-error.js";
|
|
10
12
|
import { generateFingerprint } from "../fingerprint.js";
|
|
13
|
+
import { maskAuthoredInjectedPackageSpecifier, mayContainInjectedPackageSpecifier, restoreAuthoredInjectedPackageSpecifier, } from "../injected-packages.js";
|
|
11
14
|
import { normalizeFilePath } from "../paths.js";
|
|
12
15
|
import { composeSourceMaps, rewriteSourceMapSources, stringifySourceMap } from "../source-maps.js";
|
|
13
16
|
const scriptModuleTypes = [
|
|
@@ -108,7 +111,7 @@ export async function transformModule(record, args) {
|
|
|
108
111
|
sourceMaps: args.sourceMaps ?? undefined,
|
|
109
112
|
target: args.target ?? undefined,
|
|
110
113
|
});
|
|
111
|
-
analysis.unresolvedImports = analysis.unresolvedImports.filter((unresolved) => !args.externalSet.has(unresolved.specifier));
|
|
114
|
+
analysis.unresolvedImports = analysis.unresolvedImports.filter((unresolved) => !args.externalSet.has(getDisplayImportSpecifier(unresolved.specifier)));
|
|
112
115
|
if (mayContainCommonJSModuleGlobals(sourceText) && isCommonJS(analysis.rawCode)) {
|
|
113
116
|
throw createAssetServerCompilationError(`CommonJS module detected: ${resolvedPath}. ` +
|
|
114
117
|
`This module uses CommonJS (require/module.exports) which is not supported. ` +
|
|
@@ -178,9 +181,10 @@ function isPackageImportSpecifier(specifier) {
|
|
|
178
181
|
return !specifier.startsWith('./') && !specifier.startsWith('../') && !specifier.startsWith('/');
|
|
179
182
|
}
|
|
180
183
|
async function analyzeModuleSource(sourceText, resolvedPath, transformOptions, options) {
|
|
184
|
+
let maskedSourceText = maskAuthoredInjectedPackageImports(sourceText, resolvedPath);
|
|
181
185
|
let transformResult;
|
|
182
186
|
try {
|
|
183
|
-
transformResult = await oxcTransform(resolvedPath,
|
|
187
|
+
transformResult = await oxcTransform(resolvedPath, maskedSourceText, getTransformOptions(resolvedPath, transformOptions, options));
|
|
184
188
|
assertNoCompilerErrors(transformResult.errors, resolvedPath, 'transform');
|
|
185
189
|
}
|
|
186
190
|
catch (error) {
|
|
@@ -325,6 +329,78 @@ async function getUnresolvedImportsFromLexer(rawCode) {
|
|
|
325
329
|
}
|
|
326
330
|
return unresolvedImports;
|
|
327
331
|
}
|
|
332
|
+
function getDisplayImportSpecifier(specifier) {
|
|
333
|
+
return restoreAuthoredInjectedPackageSpecifier(specifier) ?? specifier;
|
|
334
|
+
}
|
|
335
|
+
function maskAuthoredInjectedPackageImports(sourceText, resolvedPath) {
|
|
336
|
+
if (!mayContainInjectedPackageSpecifier(sourceText)) {
|
|
337
|
+
return sourceText;
|
|
338
|
+
}
|
|
339
|
+
let parseResult = parseSync(resolvedPath, sourceText, {
|
|
340
|
+
lang: getSourceLanguageForPath(resolvedPath),
|
|
341
|
+
sourceType: 'module',
|
|
342
|
+
});
|
|
343
|
+
if (parseResult.errors.length > 0) {
|
|
344
|
+
return sourceText;
|
|
345
|
+
}
|
|
346
|
+
let replacements = [];
|
|
347
|
+
walkAst(parseResult.program, (node) => {
|
|
348
|
+
if (node.type !== 'ImportDeclaration' &&
|
|
349
|
+
node.type !== 'ExportAllDeclaration' &&
|
|
350
|
+
node.type !== 'ExportNamedDeclaration' &&
|
|
351
|
+
node.type !== 'ImportExpression') {
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
let source = 'source' in node ? node.source : null;
|
|
355
|
+
if (!isStringLiteralNode(source))
|
|
356
|
+
return;
|
|
357
|
+
let maskedSpecifier = maskAuthoredInjectedPackageSpecifier(source.value);
|
|
358
|
+
if (maskedSpecifier == null)
|
|
359
|
+
return;
|
|
360
|
+
replacements.push({
|
|
361
|
+
end: source.end - 1,
|
|
362
|
+
specifier: maskedSpecifier,
|
|
363
|
+
start: source.start + 1,
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
if (replacements.length === 0)
|
|
367
|
+
return sourceText;
|
|
368
|
+
let rewrittenSource = new MagicString(sourceText);
|
|
369
|
+
for (let replacement of replacements) {
|
|
370
|
+
rewrittenSource.overwrite(replacement.start, replacement.end, replacement.specifier);
|
|
371
|
+
}
|
|
372
|
+
return rewrittenSource.toString();
|
|
373
|
+
}
|
|
374
|
+
function walkAst(node, visit) {
|
|
375
|
+
visit(node);
|
|
376
|
+
let keys = visitorKeys[node.type];
|
|
377
|
+
if (!keys)
|
|
378
|
+
return;
|
|
379
|
+
let walkableNode = node;
|
|
380
|
+
for (let key of keys) {
|
|
381
|
+
let value = walkableNode[key];
|
|
382
|
+
if (Array.isArray(value)) {
|
|
383
|
+
for (let child of value) {
|
|
384
|
+
if (isAstNode(child)) {
|
|
385
|
+
walkAst(child, visit);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
if (isAstNode(value)) {
|
|
391
|
+
walkAst(value, visit);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
function isAstNode(value) {
|
|
396
|
+
return typeof value === 'object' && value !== null && 'type' in value;
|
|
397
|
+
}
|
|
398
|
+
function isStringLiteralNode(node) {
|
|
399
|
+
return (node?.type === 'Literal' &&
|
|
400
|
+
typeof node.start === 'number' &&
|
|
401
|
+
typeof node.end === 'number' &&
|
|
402
|
+
typeof node.value === 'string');
|
|
403
|
+
}
|
|
328
404
|
function getStaticImportSpecifier(source, imported) {
|
|
329
405
|
if (imported.n != null) {
|
|
330
406
|
return imported.n;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remix-run/assets",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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",
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"./package.json": "./package.json"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
+
"@oxc-project/runtime": "^0.121.0",
|
|
29
30
|
"chokidar": "^5.0.0",
|
|
30
31
|
"es-module-lexer": "^2.0.0",
|
|
31
32
|
"get-tsconfig": "^4.13.6",
|
|
@@ -43,7 +44,9 @@
|
|
|
43
44
|
"devDependencies": {
|
|
44
45
|
"@types/node": "^24.6.0",
|
|
45
46
|
"@types/picomatch": "^4.0.3",
|
|
46
|
-
"@typescript/native-preview": "7.0.0-dev.20251125.1"
|
|
47
|
+
"@typescript/native-preview": "7.0.0-dev.20251125.1",
|
|
48
|
+
"@remix-run/assert": "0.2.0",
|
|
49
|
+
"@remix-run/test": "0.3.0"
|
|
47
50
|
},
|
|
48
51
|
"keywords": [
|
|
49
52
|
"remix",
|
|
@@ -58,7 +61,8 @@
|
|
|
58
61
|
"bench": "pnpm --dir ./bench run bench",
|
|
59
62
|
"build": "tsgo -p tsconfig.build.json",
|
|
60
63
|
"clean": "git clean -fdX",
|
|
61
|
-
"test": "
|
|
64
|
+
"test": "remix-test",
|
|
65
|
+
"test:bun": "bun x --bun remix-test",
|
|
62
66
|
"typecheck": "tsgo --noEmit"
|
|
63
67
|
}
|
|
64
68
|
}
|
package/src/lib/access.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createFileMatcher } from './file-matcher.ts'
|
|
2
|
+
import { isInjectedPackageFilePath } from './injected-packages.ts'
|
|
2
3
|
|
|
3
4
|
type AccessPolicy = {
|
|
4
5
|
isAllowed(filePath: string): boolean
|
|
@@ -16,6 +17,7 @@ export function createAccessPolicy(options: {
|
|
|
16
17
|
|
|
17
18
|
return {
|
|
18
19
|
isAllowed(filePath) {
|
|
20
|
+
if (isInjectedPackageFilePath(filePath)) return true
|
|
19
21
|
if (!allowMatchers.some((matcher) => matcher(filePath))) return false
|
|
20
22
|
if (denyMatchers.length > 0 && denyMatchers.some((matcher) => matcher(filePath))) return false
|
|
21
23
|
return true
|
package/src/lib/asset-server.ts
CHANGED
|
@@ -3,7 +3,8 @@ import * as fs from 'node:fs'
|
|
|
3
3
|
import { createAccessPolicy } from './access.ts'
|
|
4
4
|
import { isAssetServerCompilationError } from './compilation-error.ts'
|
|
5
5
|
import { getFingerprintRequestCacheControl, parseFingerprintSuffix } from './fingerprint.ts'
|
|
6
|
-
import {
|
|
6
|
+
import { getInjectedPackageRouteConfigs } from './injected-packages.ts'
|
|
7
|
+
import { normalizeFilePath, normalizePathname } from './paths.ts'
|
|
7
8
|
import { compileRoutes } from './routes.ts'
|
|
8
9
|
import type { CompiledRoutes } from './routes.ts'
|
|
9
10
|
import { createResponseForScript, createScriptCompiler } from './scripts/compiler.ts'
|
|
@@ -54,7 +55,9 @@ interface AssetServerScriptOptions {
|
|
|
54
55
|
const scriptExtensionSet = new Set<string>(supportedScriptExtensions)
|
|
55
56
|
|
|
56
57
|
export interface AssetServerOptions {
|
|
57
|
-
/**
|
|
58
|
+
/** Public mount path for this asset server, e.g. `'/assets'`. */
|
|
59
|
+
basePath: string
|
|
60
|
+
/** File patterns keyed by public URL patterns relative to `basePath`. */
|
|
58
61
|
fileMap: Readonly<Record<string, string>>
|
|
59
62
|
/**
|
|
60
63
|
* Root directory used to resolve relative file paths. Defaults to `process.cwd()`.
|
|
@@ -135,6 +138,7 @@ export interface AssetServer {
|
|
|
135
138
|
|
|
136
139
|
type ResolvedAssetServerOptions = {
|
|
137
140
|
allow: readonly string[]
|
|
141
|
+
basePath: string
|
|
138
142
|
buildId?: string
|
|
139
143
|
define?: Record<string, string>
|
|
140
144
|
deny?: readonly string[]
|
|
@@ -174,8 +178,9 @@ export function getInternalWatchTargets(assetServer: AssetServer): readonly stri
|
|
|
174
178
|
* @example
|
|
175
179
|
* ```ts
|
|
176
180
|
* let assetServer = createAssetServer({
|
|
181
|
+
* basePath: '/assets',
|
|
177
182
|
* fileMap: {
|
|
178
|
-
* '/
|
|
183
|
+
* '/app/*path': 'app/*path',
|
|
179
184
|
* },
|
|
180
185
|
* allow: ['app/**'],
|
|
181
186
|
* })
|
|
@@ -433,6 +438,7 @@ function defaultErrorHandler(error: unknown): void {
|
|
|
433
438
|
|
|
434
439
|
function resolveAssetServerOptions(options: AssetServerOptions): ResolvedAssetServerOptions {
|
|
435
440
|
let rootDir = normalizeFilePath(fs.realpathSync(path.resolve(options.rootDir ?? process.cwd())))
|
|
441
|
+
let basePath = normalizeBasePath(options.basePath)
|
|
436
442
|
let scriptOptions = options.scripts ?? {}
|
|
437
443
|
let fingerprintOptions = normalizeFingerprintOptions({
|
|
438
444
|
fingerprint: options.fingerprint,
|
|
@@ -441,6 +447,7 @@ function resolveAssetServerOptions(options: AssetServerOptions): ResolvedAssetSe
|
|
|
441
447
|
|
|
442
448
|
return {
|
|
443
449
|
allow: options.allow,
|
|
450
|
+
basePath,
|
|
444
451
|
buildId: fingerprintOptions.buildId,
|
|
445
452
|
define: scriptOptions.define,
|
|
446
453
|
deny: options.deny,
|
|
@@ -449,10 +456,13 @@ function resolveAssetServerOptions(options: AssetServerOptions): ResolvedAssetSe
|
|
|
449
456
|
minify: options.minify ?? false,
|
|
450
457
|
onError: options.onError ?? defaultErrorHandler,
|
|
451
458
|
rootDir,
|
|
452
|
-
routes: compileRoutes(
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
459
|
+
routes: compileRoutes(basePath, [
|
|
460
|
+
{
|
|
461
|
+
fileMap: options.fileMap,
|
|
462
|
+
rootDir,
|
|
463
|
+
},
|
|
464
|
+
...getInjectedPackageRouteConfigs(),
|
|
465
|
+
]),
|
|
456
466
|
sourceMapSourcePaths: options.sourceMapSourcePaths ?? 'url',
|
|
457
467
|
sourceMaps: options.sourceMaps,
|
|
458
468
|
scriptsTarget: resolveScriptTarget(options.target),
|
|
@@ -461,6 +471,14 @@ function resolveAssetServerOptions(options: AssetServerOptions): ResolvedAssetSe
|
|
|
461
471
|
}
|
|
462
472
|
}
|
|
463
473
|
|
|
474
|
+
function normalizeBasePath(basePath: string): string {
|
|
475
|
+
if (typeof basePath !== 'string') {
|
|
476
|
+
throw new TypeError('basePath must be a string')
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return normalizePathname(basePath || '/').replace(/\/+$/, '') || '/'
|
|
480
|
+
}
|
|
481
|
+
|
|
464
482
|
function normalizeFingerprintOptions(options: {
|
|
465
483
|
fingerprint: AssetServerOptions['fingerprint']
|
|
466
484
|
watch: AssetServerOptions['watch']
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import * as fs from 'node:fs'
|
|
2
|
+
import { fileURLToPath } from 'node:url'
|
|
3
|
+
|
|
4
|
+
import { getFilePathDirectory, normalizeFilePath } from './paths.ts'
|
|
5
|
+
|
|
6
|
+
type ResolvedInjectedPackage = {
|
|
7
|
+
packageJsonPath: string
|
|
8
|
+
packageRoot: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const injectedPackageNames = ['@oxc-project/runtime'] as const
|
|
12
|
+
const injectedPackagesBasePath = '/__@remix/injected'
|
|
13
|
+
|
|
14
|
+
const resolvedInjectedPackages = new Map<string, ResolvedInjectedPackage>()
|
|
15
|
+
|
|
16
|
+
export function isInjectedPackageFilePath(filePath: string): boolean {
|
|
17
|
+
let normalizedFilePath = normalizeFilePath(filePath)
|
|
18
|
+
|
|
19
|
+
for (let packageName of injectedPackageNames) {
|
|
20
|
+
let packageRoot = getResolvedInjectedPackage(packageName).packageRoot
|
|
21
|
+
if (normalizedFilePath === packageRoot || normalizedFilePath.startsWith(`${packageRoot}/`)) {
|
|
22
|
+
return true
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return false
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getInjectedPackageRouteConfigs(): {
|
|
30
|
+
fileMap: Record<string, string>
|
|
31
|
+
rootDir: string
|
|
32
|
+
}[] {
|
|
33
|
+
return injectedPackageNames.map((packageName) => {
|
|
34
|
+
let { packageRoot } = getResolvedInjectedPackage(packageName)
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
fileMap: {
|
|
38
|
+
[getInjectedPackageRoutePattern(packageName)]: `${packageName}/*path`,
|
|
39
|
+
},
|
|
40
|
+
rootDir: getInjectedPackageRouteRoot(packageRoot, packageName),
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function getInjectedPackageNameForSpecifier(specifier: string): string | null {
|
|
46
|
+
for (let packageName of injectedPackageNames) {
|
|
47
|
+
if (specifier === packageName || specifier.startsWith(`${packageName}/`)) {
|
|
48
|
+
return packageName
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return null
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function mayContainInjectedPackageSpecifier(sourceText: string): boolean {
|
|
56
|
+
return injectedPackageNames.some((packageName) => sourceText.includes(packageName))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function maskAuthoredInjectedPackageSpecifier(specifier: string): string | null {
|
|
60
|
+
let packageName = getInjectedPackageNameForSpecifier(specifier)
|
|
61
|
+
if (!packageName) return null
|
|
62
|
+
|
|
63
|
+
let maskedPackageName = getMaskedInjectedPackageName(packageName)
|
|
64
|
+
return `${maskedPackageName}${specifier.slice(packageName.length)}`
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function restoreAuthoredInjectedPackageSpecifier(specifier: string): string | null {
|
|
68
|
+
for (let packageName of injectedPackageNames) {
|
|
69
|
+
let maskedPackageName = getMaskedInjectedPackageName(packageName)
|
|
70
|
+
if (specifier === maskedPackageName) {
|
|
71
|
+
return packageName
|
|
72
|
+
}
|
|
73
|
+
if (specifier.startsWith(`${maskedPackageName}/`)) {
|
|
74
|
+
return `${packageName}${specifier.slice(maskedPackageName.length)}`
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return null
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function getMaskedInjectedPackageName(packageName: string): string {
|
|
82
|
+
return `~${packageName.slice(1)}`
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function getInjectedPackageImporterPath(): string {
|
|
86
|
+
return normalizeFilePath(fileURLToPath(import.meta.url))
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function getResolvedInjectedPackage(packageName: string): ResolvedInjectedPackage {
|
|
90
|
+
let existing = resolvedInjectedPackages.get(packageName)
|
|
91
|
+
if (existing) return existing
|
|
92
|
+
|
|
93
|
+
let packageJsonUrl = import.meta.resolve(`${packageName}/package.json`)
|
|
94
|
+
let packageJsonPath = normalizeFilePath(fs.realpathSync(fileURLToPath(packageJsonUrl)))
|
|
95
|
+
|
|
96
|
+
let resolvedInjectedPackage = {
|
|
97
|
+
packageJsonPath,
|
|
98
|
+
packageRoot: normalizeFilePath(fs.realpathSync(getFilePathDirectory(packageJsonPath))),
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
resolvedInjectedPackages.set(packageName, resolvedInjectedPackage)
|
|
102
|
+
return resolvedInjectedPackage
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function getInjectedPackageRoutePattern(packageName: string): string {
|
|
106
|
+
return `${injectedPackagesBasePath}/${packageName}/*path`
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function getInjectedPackageRouteRoot(packageRoot: string, packageName: string): string {
|
|
110
|
+
let routeRoot = packageRoot
|
|
111
|
+
|
|
112
|
+
for (let _segment of packageName.split('/')) {
|
|
113
|
+
routeRoot = getFilePathDirectory(routeRoot)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return routeRoot
|
|
117
|
+
}
|
package/src/lib/paths.ts
CHANGED
|
@@ -57,6 +57,34 @@ export function getFilePathDirectory(filePath: string): string {
|
|
|
57
57
|
return path.posix.dirname(normalizeWindowsPath(filePath))
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
export function getRelativeFilePath(fromPath: string, toPath: string): string {
|
|
61
|
+
let normalizedFromPath = normalizeFilePath(fromPath)
|
|
62
|
+
let normalizedToPath = normalizeFilePath(toPath)
|
|
63
|
+
|
|
64
|
+
if (normalizedFromPath.startsWith('//') || normalizedToPath.startsWith('//')) {
|
|
65
|
+
return normalizeWindowsPath(
|
|
66
|
+
path.win32.relative(
|
|
67
|
+
normalizedFromPath.replace(/\//g, '\\'),
|
|
68
|
+
normalizedToPath.replace(/\//g, '\\'),
|
|
69
|
+
),
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (
|
|
74
|
+
windowsDriveLetterRE.test(normalizedFromPath) ||
|
|
75
|
+
windowsDriveLetterRE.test(normalizedToPath)
|
|
76
|
+
) {
|
|
77
|
+
return normalizeWindowsPath(
|
|
78
|
+
path.win32.relative(
|
|
79
|
+
normalizedFromPath.replace(/\//g, '\\'),
|
|
80
|
+
normalizedToPath.replace(/\//g, '\\'),
|
|
81
|
+
),
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return path.posix.relative(normalizedFromPath, normalizedToPath)
|
|
86
|
+
}
|
|
87
|
+
|
|
60
88
|
export function getFilePathBaseName(filePath: string): string {
|
|
61
89
|
return path.posix.basename(normalizeWindowsPath(filePath))
|
|
62
90
|
}
|
package/src/lib/routes.ts
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
1
|
-
import * as path from 'node:path'
|
|
2
1
|
import { RoutePattern } from '@remix-run/route-pattern'
|
|
3
2
|
|
|
4
3
|
import {
|
|
4
|
+
getRelativeFilePath,
|
|
5
5
|
isAbsoluteFilePath,
|
|
6
6
|
normalizeFilePath,
|
|
7
7
|
normalizePathname,
|
|
8
8
|
resolveFilePath,
|
|
9
9
|
} from './paths.ts'
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
interface AssetRouteDefinition {
|
|
12
12
|
urlPattern: string
|
|
13
13
|
filePattern: string
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
interface RouteConfig {
|
|
17
|
+
fileMap: Readonly<Record<string, string>>
|
|
18
|
+
rootDir: string
|
|
19
|
+
}
|
|
20
|
+
|
|
16
21
|
interface CompiledRoute {
|
|
17
22
|
rootDir: string
|
|
18
23
|
urlPattern: RoutePattern
|
|
@@ -34,21 +39,26 @@ function normalizeFilePattern(pattern: string): string {
|
|
|
34
39
|
return normalizePathname(pattern)
|
|
35
40
|
}
|
|
36
41
|
|
|
37
|
-
export function compileRoutes(
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (Object.keys(
|
|
42
|
+
export function compileRoutes(
|
|
43
|
+
basePath: string,
|
|
44
|
+
routeConfigs: readonly RouteConfig[],
|
|
45
|
+
): CompiledRoutes {
|
|
46
|
+
if (routeConfigs.every((routeConfig) => Object.keys(routeConfig.fileMap).length === 0)) {
|
|
42
47
|
throw new Error('createAssetServer() requires at least one configured fileMap entry.')
|
|
43
48
|
}
|
|
44
49
|
|
|
45
|
-
let compiledRoutes =
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
let compiledRoutes = routeConfigs.flatMap((routeConfig) =>
|
|
51
|
+
Object.entries(routeConfig.fileMap).map(([urlPattern, filePattern]) =>
|
|
52
|
+
compileRoute(
|
|
53
|
+
{
|
|
54
|
+
filePattern,
|
|
55
|
+
urlPattern,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
basePath,
|
|
59
|
+
rootDir: routeConfig.rootDir,
|
|
60
|
+
},
|
|
61
|
+
),
|
|
52
62
|
),
|
|
53
63
|
)
|
|
54
64
|
|
|
@@ -69,8 +79,7 @@ export function compileRoutes(options: {
|
|
|
69
79
|
let normalizedFilePath = normalizeFilePath(filePath)
|
|
70
80
|
|
|
71
81
|
for (let route of compiledRoutes) {
|
|
72
|
-
let relativeFilePath = getRelativeFilePath(
|
|
73
|
-
if (relativeFilePath === null) continue
|
|
82
|
+
let relativeFilePath = getRelativeFilePath(route.rootDir, normalizedFilePath)
|
|
74
83
|
let match = route.filePattern.ast.pathname.match(relativeFilePath)
|
|
75
84
|
if (!match) continue
|
|
76
85
|
return normalizePathname(route.urlPattern.href(getPathnameParams(route.filePattern, match)))
|
|
@@ -84,10 +93,15 @@ export function compileRoutes(options: {
|
|
|
84
93
|
function compileRoute(
|
|
85
94
|
route: AssetRouteDefinition,
|
|
86
95
|
options: {
|
|
96
|
+
basePath: string
|
|
87
97
|
rootDir: string
|
|
88
98
|
},
|
|
89
99
|
): CompiledRoute {
|
|
90
|
-
let
|
|
100
|
+
let basePath = normalizePathname(options.basePath).replace(/\/+$/, '') || '/'
|
|
101
|
+
let relativeUrlPattern = normalizePathname(route.urlPattern)
|
|
102
|
+
let urlPatternSource = normalizePathname(
|
|
103
|
+
`${basePath.replace(/\/+$/, '')}/${relativeUrlPattern.replace(/^\/+/, '')}`,
|
|
104
|
+
)
|
|
91
105
|
let filePatternSource = normalizeFilePattern(route.filePattern)
|
|
92
106
|
|
|
93
107
|
let urlPattern = new RoutePattern(urlPatternSource)
|
|
@@ -104,11 +118,6 @@ function compileRoute(
|
|
|
104
118
|
}
|
|
105
119
|
}
|
|
106
120
|
|
|
107
|
-
function getRelativeFilePath(filePath: string, rootDir: string): string | null {
|
|
108
|
-
if (filePath[1] === ':' && rootDir[1] === ':' && filePath[0] !== rootDir[0]) return null
|
|
109
|
-
return path.posix.relative(rootDir, filePath)
|
|
110
|
-
}
|
|
111
|
-
|
|
112
121
|
function getPathnameParams(
|
|
113
122
|
pattern: RoutePattern,
|
|
114
123
|
match: Array<{ name: string; type: ':' | '*'; value: string }>,
|
|
@@ -7,6 +7,11 @@ import {
|
|
|
7
7
|
isAssetServerCompilationError,
|
|
8
8
|
} from '../compilation-error.ts'
|
|
9
9
|
import type { AssetServerCompilationError } from '../compilation-error.ts'
|
|
10
|
+
import {
|
|
11
|
+
getInjectedPackageNameForSpecifier,
|
|
12
|
+
getInjectedPackageImporterPath,
|
|
13
|
+
restoreAuthoredInjectedPackageSpecifier,
|
|
14
|
+
} from '../injected-packages.ts'
|
|
10
15
|
import type { ModuleRecord, ModuleTracking } from '../module-store.ts'
|
|
11
16
|
import { normalizeFilePath } from '../paths.ts'
|
|
12
17
|
import type { CompiledRoutes } from '../routes.ts'
|
|
@@ -81,6 +86,11 @@ type ResolvedSpec = {
|
|
|
81
86
|
specifier: string
|
|
82
87
|
}
|
|
83
88
|
|
|
89
|
+
type NormalizedSpecifierResolution = {
|
|
90
|
+
importerPath: string
|
|
91
|
+
specifier: string
|
|
92
|
+
}
|
|
93
|
+
|
|
84
94
|
export async function resolveModule(
|
|
85
95
|
record: ScriptRecord,
|
|
86
96
|
transformed: TransformedModule,
|
|
@@ -109,9 +119,10 @@ export async function resolveModule(
|
|
|
109
119
|
let deps = new Set<string>()
|
|
110
120
|
|
|
111
121
|
for (let unresolved of transformed.unresolvedImports) {
|
|
122
|
+
let displaySpecifier = getDisplayImportSpecifier(unresolved.specifier)
|
|
112
123
|
let trackedResolution = getTrackedRelativeImportResolution(
|
|
113
124
|
transformed.importerDir,
|
|
114
|
-
|
|
125
|
+
displaySpecifier,
|
|
115
126
|
args.isWatchIgnored,
|
|
116
127
|
)
|
|
117
128
|
|
|
@@ -119,7 +130,7 @@ export async function resolveModule(
|
|
|
119
130
|
if (!resolvedSpec?.absolutePath) {
|
|
120
131
|
return failResolve(
|
|
121
132
|
createAssetServerCompilationError(
|
|
122
|
-
`Failed to resolve import "${
|
|
133
|
+
`Failed to resolve import "${displaySpecifier}" in ${transformed.resolvedPath}. ` +
|
|
123
134
|
`Ensure it resolves to a file within the configured asset server fileMap, or mark it as external.`,
|
|
124
135
|
{
|
|
125
136
|
code: 'IMPORT_RESOLUTION_FAILED',
|
|
@@ -136,7 +147,7 @@ export async function resolveModule(
|
|
|
136
147
|
if (!resolvedImport) {
|
|
137
148
|
return failResolve(
|
|
138
149
|
createAssetServerCompilationError(
|
|
139
|
-
`Import "${
|
|
150
|
+
`Import "${displaySpecifier}" in ${transformed.resolvedPath}, resolved to "${resolvedSpec.absolutePath}", is not a supported script file. ` +
|
|
140
151
|
`Supported extensions are ${supportedScriptExtensions.join(', ')}.`,
|
|
141
152
|
{
|
|
142
153
|
code: 'IMPORT_NOT_SUPPORTED',
|
|
@@ -152,7 +163,7 @@ export async function resolveModule(
|
|
|
152
163
|
if (!args.isAllowed(resolvedImport.identityPath)) {
|
|
153
164
|
return failResolve(
|
|
154
165
|
createAssetServerCompilationError(
|
|
155
|
-
`Import "${
|
|
166
|
+
`Import "${displaySpecifier}" in ${transformed.resolvedPath}, resolved to "${resolvedImport.identityPath}", is not allowed by the asset server allow/deny configuration. ` +
|
|
156
167
|
`Add a matching allow rule for this file path, remove a conflicting deny rule for this file path, or mark this import as external.`,
|
|
157
168
|
{
|
|
158
169
|
code: 'IMPORT_NOT_ALLOWED',
|
|
@@ -169,7 +180,7 @@ export async function resolveModule(
|
|
|
169
180
|
if (!stableUrlPathname) {
|
|
170
181
|
return failResolve(
|
|
171
182
|
createAssetServerCompilationError(
|
|
172
|
-
`Import "${
|
|
183
|
+
`Import "${displaySpecifier}" in ${transformed.resolvedPath}, resolved to "${resolvedImport.identityPath}", is outside all configured fileMap entries. ` +
|
|
173
184
|
`Add a matching fileMap entry for this file path, or mark this import as external.`,
|
|
174
185
|
{
|
|
175
186
|
code: 'IMPORT_OUTSIDE_FILE_MAP',
|
|
@@ -323,11 +334,17 @@ async function batchResolveSpecifiers(
|
|
|
323
334
|
|
|
324
335
|
try {
|
|
325
336
|
for (let specifier of specifiers) {
|
|
326
|
-
let
|
|
337
|
+
let normalizedResolution = normalizeSpecifierResolution(specifier, importerPath)
|
|
338
|
+
let resolutionResult = await resolverFactory.resolveFileAsync(
|
|
339
|
+
normalizedResolution.importerPath,
|
|
340
|
+
normalizedResolution.specifier,
|
|
341
|
+
)
|
|
327
342
|
if (resolutionResult.error) {
|
|
328
343
|
throw createAssetServerCompilationError(
|
|
329
|
-
|
|
330
|
-
`
|
|
344
|
+
normalizedResolution.importerPath === getInjectedPackageImporterPath()
|
|
345
|
+
? `Failed to resolve injected import "${specifier}" from asset server.`
|
|
346
|
+
: `Failed to resolve import "${normalizedResolution.specifier}" in ${normalizedResolution.importerPath}. ` +
|
|
347
|
+
`Ensure it resolves to a file within the configured asset server fileMap, or mark it as external.`,
|
|
331
348
|
{
|
|
332
349
|
code: 'IMPORT_RESOLUTION_FAILED',
|
|
333
350
|
},
|
|
@@ -370,6 +387,35 @@ function formatUnknownError(error: unknown): string {
|
|
|
370
387
|
return error instanceof Error ? error.message : String(error)
|
|
371
388
|
}
|
|
372
389
|
|
|
390
|
+
function normalizeSpecifierResolution(
|
|
391
|
+
specifier: string,
|
|
392
|
+
importerPath: string,
|
|
393
|
+
): NormalizedSpecifierResolution {
|
|
394
|
+
let authoredInjectedPackageSpecifier = restoreAuthoredInjectedPackageSpecifier(specifier)
|
|
395
|
+
if (authoredInjectedPackageSpecifier) {
|
|
396
|
+
return {
|
|
397
|
+
importerPath,
|
|
398
|
+
specifier: authoredInjectedPackageSpecifier,
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (getInjectedPackageNameForSpecifier(specifier)) {
|
|
403
|
+
return {
|
|
404
|
+
importerPath: getInjectedPackageImporterPath(),
|
|
405
|
+
specifier,
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return {
|
|
410
|
+
importerPath,
|
|
411
|
+
specifier,
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function getDisplayImportSpecifier(specifier: string): string {
|
|
416
|
+
return restoreAuthoredInjectedPackageSpecifier(specifier) ?? specifier
|
|
417
|
+
}
|
|
418
|
+
|
|
373
419
|
function failResolve(
|
|
374
420
|
error: unknown,
|
|
375
421
|
trackedFiles: ReadonlySet<string>,
|