@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.
@@ -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, sourceText, getTransformOptions(resolvedPath, transformOptions, options));
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.2.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": "node --disable-warning=ExperimentalWarning --test './src/**/*.test.ts'",
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
@@ -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 { normalizeFilePath } from './paths.ts'
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
- /** File patterns keyed by public URL patterns. */
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
- * '/assets/app/*path': 'app/*path',
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
- fileMap: options.fileMap,
454
- rootDir,
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
- export interface AssetRouteDefinition {
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(options: {
38
- fileMap: Readonly<Record<string, string>>
39
- rootDir: string
40
- }): CompiledRoutes {
41
- if (Object.keys(options.fileMap).length === 0) {
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 = Object.entries(options.fileMap).map(([urlPattern, filePattern]) =>
46
- compileRoute(
47
- {
48
- urlPattern,
49
- filePattern,
50
- },
51
- { rootDir: options.rootDir },
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(normalizedFilePath, route.rootDir)
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 urlPatternSource = normalizePathname(route.urlPattern)
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
- unresolved.specifier,
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 "${unresolved.specifier}" in ${transformed.resolvedPath}. ` +
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 "${unresolved.specifier}" in ${transformed.resolvedPath}, resolved to "${resolvedSpec.absolutePath}", is not a supported script file. ` +
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 "${unresolved.specifier}" in ${transformed.resolvedPath}, resolved to "${resolvedImport.identityPath}", is not allowed by the asset server allow/deny configuration. ` +
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 "${unresolved.specifier}" in ${transformed.resolvedPath}, resolved to "${resolvedImport.identityPath}", is outside all configured fileMap entries. ` +
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 resolutionResult = await resolverFactory.resolveFileAsync(importerPath, specifier)
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
- `Failed to resolve import "${specifier}" in ${importerPath}. ` +
330
- `Ensure it resolves to a file within the configured asset server fileMap, or mark it as external.`,
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>,