@remix-run/assets 0.2.0 → 0.4.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 (71) hide show
  1. package/README.md +241 -28
  2. package/dist/assets.d.ts +1 -0
  3. package/dist/assets.d.ts.map +1 -1
  4. package/dist/assets.js +1 -0
  5. package/dist/lib/access.d.ts +1 -0
  6. package/dist/lib/access.d.ts.map +1 -1
  7. package/dist/lib/access.js +10 -1
  8. package/dist/lib/asset-server.d.ts +33 -7
  9. package/dist/lib/asset-server.d.ts.map +1 -1
  10. package/dist/lib/asset-server.js +195 -23
  11. package/dist/lib/compilation-error.d.ts +1 -1
  12. package/dist/lib/compilation-error.d.ts.map +1 -1
  13. package/dist/lib/files/compiler.d.ts +71 -0
  14. package/dist/lib/files/compiler.d.ts.map +1 -0
  15. package/dist/lib/files/compiler.js +552 -0
  16. package/dist/lib/files/config.d.ts +101 -0
  17. package/dist/lib/files/config.d.ts.map +1 -0
  18. package/dist/lib/files/config.js +219 -0
  19. package/dist/lib/files/store.d.ts +31 -0
  20. package/dist/lib/files/store.d.ts.map +1 -0
  21. package/dist/lib/files/store.js +63 -0
  22. package/dist/lib/fingerprint.d.ts +3 -3
  23. package/dist/lib/fingerprint.d.ts.map +1 -1
  24. package/dist/lib/fingerprint.js +5 -5
  25. package/dist/lib/injected-packages.d.ts +11 -0
  26. package/dist/lib/injected-packages.d.ts.map +1 -0
  27. package/dist/lib/injected-packages.js +86 -0
  28. package/dist/lib/paths.d.ts +1 -0
  29. package/dist/lib/paths.d.ts.map +1 -1
  30. package/dist/lib/paths.js +12 -0
  31. package/dist/lib/routes.d.ts +5 -7
  32. package/dist/lib/routes.d.ts.map +1 -1
  33. package/dist/lib/routes.js +41 -36
  34. package/dist/lib/scripts/compiler.d.ts +1 -0
  35. package/dist/lib/scripts/compiler.d.ts.map +1 -1
  36. package/dist/lib/scripts/compiler.js +30 -5
  37. package/dist/lib/scripts/resolve.d.ts.map +1 -1
  38. package/dist/lib/scripts/resolve.js +57 -10
  39. package/dist/lib/scripts/transform.d.ts.map +1 -1
  40. package/dist/lib/scripts/transform.js +79 -3
  41. package/dist/lib/styles/compiler.d.ts +4 -0
  42. package/dist/lib/styles/compiler.d.ts.map +1 -1
  43. package/dist/lib/styles/compiler.js +2 -0
  44. package/dist/lib/styles/emit.d.ts +3 -0
  45. package/dist/lib/styles/emit.d.ts.map +1 -1
  46. package/dist/lib/styles/emit.js +36 -1
  47. package/dist/lib/styles/resolve.d.ts +8 -1
  48. package/dist/lib/styles/resolve.d.ts.map +1 -1
  49. package/dist/lib/styles/resolve.js +85 -32
  50. package/dist/lib/watch.d.ts +2 -0
  51. package/dist/lib/watch.d.ts.map +1 -1
  52. package/dist/lib/watch.js +25 -8
  53. package/package.json +11 -5
  54. package/src/assets.ts +1 -0
  55. package/src/lib/access.ts +10 -1
  56. package/src/lib/asset-server.ts +297 -34
  57. package/src/lib/compilation-error.ts +9 -0
  58. package/src/lib/files/compiler.ts +885 -0
  59. package/src/lib/files/config.ts +479 -0
  60. package/src/lib/files/store.ts +109 -0
  61. package/src/lib/fingerprint.ts +8 -7
  62. package/src/lib/injected-packages.ts +117 -0
  63. package/src/lib/paths.ts +28 -0
  64. package/src/lib/routes.ts +67 -55
  65. package/src/lib/scripts/compiler.ts +43 -5
  66. package/src/lib/scripts/resolve.ts +89 -11
  67. package/src/lib/scripts/transform.ts +105 -3
  68. package/src/lib/styles/compiler.ts +9 -0
  69. package/src/lib/styles/emit.ts +75 -1
  70. package/src/lib/styles/resolve.ts +132 -35
  71. package/src/lib/watch.ts +34 -12
@@ -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,22 +1,31 @@
1
- import * as path from 'node:path'
2
1
  import { RoutePattern } from '@remix-run/route-pattern'
2
+ import { createHref } from '@remix-run/route-pattern/href'
3
+ import { createMatcher, type Matcher } from '@remix-run/route-pattern/match'
3
4
 
4
5
  import {
6
+ getRelativeFilePath,
5
7
  isAbsoluteFilePath,
6
8
  normalizeFilePath,
7
9
  normalizePathname,
8
10
  resolveFilePath,
9
11
  } from './paths.ts'
10
12
 
11
- export interface AssetRouteDefinition {
13
+ interface AssetRouteDefinition {
12
14
  urlPattern: string
13
15
  filePattern: string
14
16
  }
15
17
 
18
+ interface RouteConfig {
19
+ fileMap: Readonly<Record<string, string>>
20
+ rootDir: string
21
+ }
22
+
16
23
  interface CompiledRoute {
17
24
  rootDir: string
18
25
  urlPattern: RoutePattern
26
+ urlMatcher: Matcher
19
27
  filePattern: RoutePattern
28
+ fileMatcher: Matcher
20
29
  }
21
30
 
22
31
  export interface CompiledRoutes {
@@ -34,21 +43,26 @@ function normalizeFilePattern(pattern: string): string {
34
43
  return normalizePathname(pattern)
35
44
  }
36
45
 
37
- export function compileRoutes(options: {
38
- fileMap: Readonly<Record<string, string>>
39
- rootDir: string
40
- }): CompiledRoutes {
41
- if (Object.keys(options.fileMap).length === 0) {
46
+ export function compileRoutes(
47
+ basePath: string,
48
+ routeConfigs: readonly RouteConfig[],
49
+ ): CompiledRoutes {
50
+ if (routeConfigs.every((routeConfig) => Object.keys(routeConfig.fileMap).length === 0)) {
42
51
  throw new Error('createAssetServer() requires at least one configured fileMap entry.')
43
52
  }
44
53
 
45
- let compiledRoutes = Object.entries(options.fileMap).map(([urlPattern, filePattern]) =>
46
- compileRoute(
47
- {
48
- urlPattern,
49
- filePattern,
50
- },
51
- { rootDir: options.rootDir },
54
+ let compiledRoutes = routeConfigs.flatMap((routeConfig) =>
55
+ Object.entries(routeConfig.fileMap).map(([urlPattern, filePattern]) =>
56
+ compileRoute(
57
+ {
58
+ filePattern,
59
+ urlPattern,
60
+ },
61
+ {
62
+ basePath,
63
+ rootDir: routeConfig.rootDir,
64
+ },
65
+ ),
52
66
  ),
53
67
  )
54
68
 
@@ -57,9 +71,9 @@ export function compileRoutes(options: {
57
71
  let normalizedPathname = normalizePathname(pathname)
58
72
 
59
73
  for (let route of compiledRoutes) {
60
- let match = route.urlPattern.match(`http://remix.run${normalizedPathname}`)
74
+ let match = route.urlMatcher.match(`http://remix.run${normalizedPathname}`)
61
75
  if (!match) continue
62
- let relativeFilePath = route.filePattern.href(match.params).replace(/^\/+/, '')
76
+ let relativeFilePath = createHref(route.filePattern, match.params).replace(/^\/+/, '')
63
77
  return resolveFilePath(route.rootDir, relativeFilePath)
64
78
  }
65
79
 
@@ -69,11 +83,10 @@ export function compileRoutes(options: {
69
83
  let normalizedFilePath = normalizeFilePath(filePath)
70
84
 
71
85
  for (let route of compiledRoutes) {
72
- let relativeFilePath = getRelativeFilePath(normalizedFilePath, route.rootDir)
73
- if (relativeFilePath === null) continue
74
- let match = route.filePattern.ast.pathname.match(relativeFilePath)
86
+ let relativeFilePath = getRelativeFilePath(route.rootDir, normalizedFilePath)
87
+ let match = route.fileMatcher.match(`http://remix.run/${relativeFilePath}`)
75
88
  if (!match) continue
76
- return normalizePathname(route.urlPattern.href(getPathnameParams(route.filePattern, match)))
89
+ return normalizePathname(createHref(route.urlPattern, match.params))
77
90
  }
78
91
 
79
92
  return null
@@ -84,14 +97,19 @@ export function compileRoutes(options: {
84
97
  function compileRoute(
85
98
  route: AssetRouteDefinition,
86
99
  options: {
100
+ basePath: string
87
101
  rootDir: string
88
102
  },
89
103
  ): CompiledRoute {
90
- let urlPatternSource = normalizePathname(route.urlPattern)
104
+ let basePath = normalizePathname(options.basePath).replace(/\/+$/, '') || '/'
105
+ let relativeUrlPattern = normalizePathname(route.urlPattern)
106
+ let urlPatternSource = normalizePathname(
107
+ `${basePath.replace(/\/+$/, '')}/${relativeUrlPattern.replace(/^\/+/, '')}`,
108
+ )
91
109
  let filePatternSource = normalizeFilePattern(route.filePattern)
92
110
 
93
- let urlPattern = new RoutePattern(urlPatternSource)
94
- let filePattern = new RoutePattern(filePatternSource)
111
+ let urlPattern = RoutePattern.parse(urlPatternSource)
112
+ let filePattern = RoutePattern.parse(filePatternSource)
95
113
 
96
114
  validateNoUnnamedWildcards(urlPattern, 'URL')
97
115
  validateNoUnnamedWildcards(filePattern, 'File')
@@ -100,42 +118,30 @@ function compileRoute(
100
118
  return {
101
119
  rootDir: normalizeFilePath(options.rootDir).replace(/\/+$/, ''),
102
120
  urlPattern,
121
+ urlMatcher: createMatcher(urlPattern),
103
122
  filePattern,
123
+ fileMatcher: createMatcher(stripDotSegments(filePatternSource)),
104
124
  }
105
125
  }
106
126
 
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
- function getPathnameParams(
113
- pattern: RoutePattern,
114
- match: Array<{ name: string; type: ':' | '*'; value: string }>,
115
- ): Record<string, string | undefined> {
116
- let params: Record<string, string | undefined> = {}
117
-
118
- for (let param of pattern.ast.pathname.params) {
119
- if (param.name === '*') continue
120
- params[param.name] = undefined
121
- }
127
+ function stripDotSegments(pattern: string): string {
128
+ let segments: string[] = []
122
129
 
123
- for (let param of match) {
124
- if (param.name === '*') continue
125
- params[param.name] = param.value
130
+ for (let segment of pattern.split('/')) {
131
+ if (segment === '' || segment === '.') continue
132
+ if (segment === '..') {
133
+ segments.pop()
134
+ continue
135
+ }
136
+ segments.push(segment)
126
137
  }
127
138
 
128
- return params
139
+ return segments.join('/')
129
140
  }
130
141
 
131
142
  function validateRoutePatterns(urlPattern: RoutePattern, filePattern: RoutePattern): void {
132
- let urlParams = urlPattern.ast.pathname.params.map(
133
- (param: { name: string; type: ':' | '*' }) => `${param.type}:${param.name}`,
134
- )
135
- let fileParams = filePattern.ast.pathname.params.map(
136
- (param: { name: string; type: ':' | '*' }) => `${param.type}:${param.name}`,
137
- )
138
-
143
+ let urlParams = getPathnameParams(urlPattern)
144
+ let fileParams = getPathnameParams(filePattern)
139
145
  if (urlParams.length !== fileParams.length) {
140
146
  throw new Error(
141
147
  `Route patterns must have matching capture structure.\nURL: ${urlPattern}\nFile: ${filePattern}`,
@@ -143,7 +149,9 @@ function validateRoutePatterns(urlPattern: RoutePattern, filePattern: RoutePatte
143
149
  }
144
150
 
145
151
  for (let i = 0; i < urlParams.length; i++) {
146
- if (urlParams[i] !== fileParams[i]) {
152
+ let urlParam = urlParams[i]
153
+ let fileParam = fileParams[i]
154
+ if (urlParam.type !== fileParam.type || urlParam.name !== fileParam.name) {
147
155
  throw new Error(
148
156
  `Route patterns must have matching capture structure.\nURL: ${urlPattern}\nFile: ${filePattern}`,
149
157
  )
@@ -152,13 +160,17 @@ function validateRoutePatterns(urlPattern: RoutePattern, filePattern: RoutePatte
152
160
  }
153
161
 
154
162
  function validateNoUnnamedWildcards(pattern: RoutePattern, label: string): void {
155
- if (
156
- pattern.ast.pathname.params.some(
157
- (param: { name: string; type: ':' | '*' }) => param.type === '*' && param.name === '*',
158
- )
159
- ) {
163
+ if (pattern.pathname.tokens.some((token) => token.type === '*' && token.name === '*')) {
160
164
  throw new Error(
161
165
  `${label} route patterns must use named wildcards for reversible mapping.\nPattern: ${pattern}`,
162
166
  )
163
167
  }
164
168
  }
169
+
170
+ type PathnameParam = Extract<RoutePattern['pathname']['tokens'][number], { type: ':' | '*' }>
171
+
172
+ function getPathnameParams(pattern: RoutePattern): Array<PathnameParam> {
173
+ return pattern.pathname.tokens.filter(
174
+ (token): token is PathnameParam => token.type === ':' || token.type === '*',
175
+ )
176
+ }
@@ -66,6 +66,7 @@ type ScriptCompilerOptions = {
66
66
  external: string[]
67
67
  fingerprintAssets: boolean
68
68
  isAllowed(absolutePath: string): boolean
69
+ isDenied(absolutePath: string): boolean
69
70
  minify: boolean
70
71
  onWatchDirectoriesChange?: (delta: { add: string[]; remove: string[] }) => void
71
72
  rootDir: string
@@ -117,8 +118,14 @@ export function createScriptCompiler(options: ScriptCompilerOptions): ScriptComp
117
118
  extensionAlias: resolverExtensionAlias,
118
119
  extensions: resolverExtensions,
119
120
  mainFields: ['browser', 'module', 'main'],
121
+ symlinks: false,
120
122
  tsconfig: 'auto',
121
123
  })
124
+ let resolveModulePathOptions = {
125
+ isAllowed: resolvedOptions.isAllowed,
126
+ isDenied: resolvedOptions.isDenied,
127
+ routes: resolvedOptions.routes,
128
+ }
122
129
  let resolveInFlightByCacheKey = new Map<string, Promise<ResolvedModule>>()
123
130
  let emitInFlightByCacheKey = new Map<string, Promise<EmittedModule>>()
124
131
 
@@ -138,7 +145,9 @@ export function createScriptCompiler(options: ScriptCompilerOptions): ScriptComp
138
145
  let resolveArgs: ResolveArgs = {
139
146
  isAllowed: resolvedOptions.isAllowed,
140
147
  isWatchIgnored,
141
- resolveModulePath,
148
+ resolveModulePath(absolutePath) {
149
+ return resolveModulePath(absolutePath, resolveModulePathOptions)
150
+ },
142
151
  resolverFactory,
143
152
  routes: resolvedOptions.routes,
144
153
  }
@@ -253,7 +262,7 @@ export function createScriptCompiler(options: ScriptCompilerOptions): ScriptComp
253
262
  }
254
263
 
255
264
  function resolveServedScriptOrThrow(absolutePath: string): ResolveModuleResult {
256
- let resolvedModule = resolveModulePath(absolutePath)
265
+ let resolvedModule = resolveModulePath(absolutePath, resolveModulePathOptions)
257
266
  if (!resolvedModule) {
258
267
  throw createAssetServerCompilationError(`File not found: ${absolutePath}`, {
259
268
  code: 'FILE_NOT_FOUND',
@@ -548,11 +557,19 @@ function shouldClearResolverCacheForFileEvent(filePath: string, event: ModuleWat
548
557
  return event !== 'change' || isPackageJsonPath(filePath) || isTsconfigPath(filePath)
549
558
  }
550
559
 
551
- function resolveModulePath(absolutePath: string): ResolveModuleResult | null {
560
+ function resolveModulePath(
561
+ absolutePath: string,
562
+ options: {
563
+ isAllowed(absolutePath: string): boolean
564
+ isDenied(absolutePath: string): boolean
565
+ routes: CompiledRoutes
566
+ },
567
+ ): ResolveModuleResult | null {
568
+ let candidateIdentityPath = normalizeFilePath(absolutePath)
552
569
  let resolvedPath: string
553
570
 
554
571
  try {
555
- resolvedPath = normalizeFilePath(fs.realpathSync(normalizeFilePath(absolutePath)))
572
+ resolvedPath = normalizeFilePath(fs.realpathSync(candidateIdentityPath))
556
573
  } catch (error) {
557
574
  if (isNoEntityError(error)) return null
558
575
  throw error
@@ -563,11 +580,32 @@ function resolveModulePath(absolutePath: string): ResolveModuleResult | null {
563
580
  }
564
581
 
565
582
  return {
566
- identityPath: resolvedPath,
583
+ identityPath: getModuleIdentityPath(candidateIdentityPath, resolvedPath, options),
567
584
  resolvedPath,
568
585
  }
569
586
  }
570
587
 
588
+ function getModuleIdentityPath(
589
+ candidateIdentityPath: string,
590
+ resolvedPath: string,
591
+ options: {
592
+ isAllowed(absolutePath: string): boolean
593
+ isDenied(absolutePath: string): boolean
594
+ routes: CompiledRoutes
595
+ },
596
+ ): string {
597
+ if (candidateIdentityPath === resolvedPath) return resolvedPath
598
+ if (!containsNodeModulesPathSegment(candidateIdentityPath)) return resolvedPath
599
+ if (!options.routes.toUrlPathname(candidateIdentityPath)) return resolvedPath
600
+ if (!options.isAllowed(candidateIdentityPath)) return resolvedPath
601
+ if (options.isDenied(resolvedPath)) return resolvedPath
602
+ return candidateIdentityPath
603
+ }
604
+
605
+ function containsNodeModulesPathSegment(filePath: string): boolean {
606
+ return filePath.split('/').includes('node_modules')
607
+ }
608
+
571
609
  function resolveActualPath(identityPath: string): string | null {
572
610
  try {
573
611
  return normalizeFilePath(fs.realpathSync(identityPath))
@@ -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,
@@ -95,7 +105,7 @@ export async function resolveModule(
95
105
  transformed.unresolvedImports.length > 0
96
106
  ? await batchResolveSpecifiers(
97
107
  getUniqueSpecifiers(transformed.unresolvedImports),
98
- transformed.resolvedPath,
108
+ transformed.identityPath,
99
109
  args.resolverFactory,
100
110
  )
101
111
  : new Map<string, ResolvedSpec>()
@@ -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',
@@ -185,8 +196,10 @@ export async function resolveModule(
185
196
  deps.add(resolvedImport.identityPath)
186
197
 
187
198
  if (transformed.packageSpecifiers.includes(unresolved.specifier)) {
188
- let packageJsonPath =
189
- resolvedSpec.packageJsonPath ?? findNearestPackageJsonPath(resolvedImport.resolvedPath)
199
+ let packageJsonPath = resolvePackageJsonPath(
200
+ resolvedSpec.packageJsonPath,
201
+ resolvedImport.resolvedPath,
202
+ )
190
203
  if (packageJsonPath && !args.isWatchIgnored(packageJsonPath)) {
191
204
  trackedFiles.add(packageJsonPath)
192
205
  }
@@ -224,6 +237,25 @@ export async function resolveModule(
224
237
  }
225
238
  }
226
239
 
240
+ function resolvePackageJsonPath(
241
+ packageJsonPath: string | null,
242
+ resolvedPath: string,
243
+ ): string | null {
244
+ return (
245
+ (packageJsonPath ? resolveExistingPath(packageJsonPath) : null) ??
246
+ findNearestPackageJsonPath(resolvedPath)
247
+ )
248
+ }
249
+
250
+ function resolveExistingPath(filePath: string): string | null {
251
+ try {
252
+ return normalizeFilePath(fs.realpathSync(filePath))
253
+ } catch (error) {
254
+ if (isNoEntityError(error)) return null
255
+ throw error
256
+ }
257
+ }
258
+
227
259
  function findNearestPackageJsonPath(filePath: string): string | null {
228
260
  let directory = path.dirname(filePath)
229
261
 
@@ -239,6 +271,17 @@ function findNearestPackageJsonPath(filePath: string): string | null {
239
271
  }
240
272
  }
241
273
 
274
+ function isNoEntityError(
275
+ error: unknown,
276
+ ): error is NodeJS.ErrnoException & { code: 'ENOENT' | 'ENOTDIR' } {
277
+ return (
278
+ error instanceof Error &&
279
+ 'code' in error &&
280
+ ((error as NodeJS.ErrnoException).code === 'ENOENT' ||
281
+ (error as NodeJS.ErrnoException).code === 'ENOTDIR')
282
+ )
283
+ }
284
+
242
285
  function isRelativeImportSpecifier(specifier: string): boolean {
243
286
  return specifier.startsWith('./') || specifier.startsWith('../')
244
287
  }
@@ -323,11 +366,17 @@ async function batchResolveSpecifiers(
323
366
 
324
367
  try {
325
368
  for (let specifier of specifiers) {
326
- let resolutionResult = await resolverFactory.resolveFileAsync(importerPath, specifier)
369
+ let normalizedResolution = normalizeSpecifierResolution(specifier, importerPath)
370
+ let resolutionResult = await resolverFactory.resolveFileAsync(
371
+ normalizedResolution.importerPath,
372
+ normalizedResolution.specifier,
373
+ )
327
374
  if (resolutionResult.error) {
328
375
  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.`,
376
+ normalizedResolution.importerPath === getInjectedPackageImporterPath()
377
+ ? `Failed to resolve injected import "${specifier}" from asset server.`
378
+ : `Failed to resolve import "${normalizedResolution.specifier}" in ${normalizedResolution.importerPath}. ` +
379
+ `Ensure it resolves to a file within the configured asset server fileMap, or mark it as external.`,
331
380
  {
332
381
  code: 'IMPORT_RESOLUTION_FAILED',
333
382
  },
@@ -370,6 +419,35 @@ function formatUnknownError(error: unknown): string {
370
419
  return error instanceof Error ? error.message : String(error)
371
420
  }
372
421
 
422
+ function normalizeSpecifierResolution(
423
+ specifier: string,
424
+ importerPath: string,
425
+ ): NormalizedSpecifierResolution {
426
+ let authoredInjectedPackageSpecifier = restoreAuthoredInjectedPackageSpecifier(specifier)
427
+ if (authoredInjectedPackageSpecifier) {
428
+ return {
429
+ importerPath,
430
+ specifier: authoredInjectedPackageSpecifier,
431
+ }
432
+ }
433
+
434
+ if (getInjectedPackageNameForSpecifier(specifier)) {
435
+ return {
436
+ importerPath: getInjectedPackageImporterPath(),
437
+ specifier,
438
+ }
439
+ }
440
+
441
+ return {
442
+ importerPath,
443
+ specifier,
444
+ }
445
+ }
446
+
447
+ function getDisplayImportSpecifier(specifier: string): string {
448
+ return restoreAuthoredInjectedPackageSpecifier(specifier) ?? specifier
449
+ }
450
+
373
451
  function failResolve(
374
452
  error: unknown,
375
453
  trackedFiles: ReadonlySet<string>,