@remix-run/assets 0.3.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 (61) hide show
  1. package/README.md +206 -8
  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 +7 -1
  8. package/dist/lib/asset-server.d.ts +29 -6
  9. package/dist/lib/asset-server.d.ts.map +1 -1
  10. package/dist/lib/asset-server.js +176 -17
  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/routes.d.ts.map +1 -1
  26. package/dist/lib/routes.js +28 -20
  27. package/dist/lib/scripts/compiler.d.ts +1 -0
  28. package/dist/lib/scripts/compiler.d.ts.map +1 -1
  29. package/dist/lib/scripts/compiler.js +30 -5
  30. package/dist/lib/scripts/resolve.d.ts.map +1 -1
  31. package/dist/lib/scripts/resolve.js +22 -2
  32. package/dist/lib/scripts/transform.js +1 -1
  33. package/dist/lib/styles/compiler.d.ts +4 -0
  34. package/dist/lib/styles/compiler.d.ts.map +1 -1
  35. package/dist/lib/styles/compiler.js +2 -0
  36. package/dist/lib/styles/emit.d.ts +3 -0
  37. package/dist/lib/styles/emit.d.ts.map +1 -1
  38. package/dist/lib/styles/emit.js +36 -1
  39. package/dist/lib/styles/resolve.d.ts +8 -1
  40. package/dist/lib/styles/resolve.d.ts.map +1 -1
  41. package/dist/lib/styles/resolve.js +85 -32
  42. package/dist/lib/watch.d.ts +2 -0
  43. package/dist/lib/watch.d.ts.map +1 -1
  44. package/dist/lib/watch.js +25 -8
  45. package/package.json +6 -4
  46. package/src/assets.ts +1 -0
  47. package/src/lib/access.ts +8 -1
  48. package/src/lib/asset-server.ts +273 -28
  49. package/src/lib/compilation-error.ts +9 -0
  50. package/src/lib/files/compiler.ts +885 -0
  51. package/src/lib/files/config.ts +479 -0
  52. package/src/lib/files/store.ts +109 -0
  53. package/src/lib/fingerprint.ts +8 -7
  54. package/src/lib/routes.ts +36 -33
  55. package/src/lib/scripts/compiler.ts +43 -5
  56. package/src/lib/scripts/resolve.ts +35 -3
  57. package/src/lib/scripts/transform.ts +1 -1
  58. package/src/lib/styles/compiler.ts +9 -0
  59. package/src/lib/styles/emit.ts +75 -1
  60. package/src/lib/styles/resolve.ts +132 -35
  61. package/src/lib/watch.ts +34 -12
package/src/lib/routes.ts CHANGED
@@ -1,4 +1,6 @@
1
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'
2
4
 
3
5
  import {
4
6
  getRelativeFilePath,
@@ -21,7 +23,9 @@ interface RouteConfig {
21
23
  interface CompiledRoute {
22
24
  rootDir: string
23
25
  urlPattern: RoutePattern
26
+ urlMatcher: Matcher
24
27
  filePattern: RoutePattern
28
+ fileMatcher: Matcher
25
29
  }
26
30
 
27
31
  export interface CompiledRoutes {
@@ -67,9 +71,9 @@ export function compileRoutes(
67
71
  let normalizedPathname = normalizePathname(pathname)
68
72
 
69
73
  for (let route of compiledRoutes) {
70
- let match = route.urlPattern.match(`http://remix.run${normalizedPathname}`)
74
+ let match = route.urlMatcher.match(`http://remix.run${normalizedPathname}`)
71
75
  if (!match) continue
72
- let relativeFilePath = route.filePattern.href(match.params).replace(/^\/+/, '')
76
+ let relativeFilePath = createHref(route.filePattern, match.params).replace(/^\/+/, '')
73
77
  return resolveFilePath(route.rootDir, relativeFilePath)
74
78
  }
75
79
 
@@ -80,9 +84,9 @@ export function compileRoutes(
80
84
 
81
85
  for (let route of compiledRoutes) {
82
86
  let relativeFilePath = getRelativeFilePath(route.rootDir, normalizedFilePath)
83
- let match = route.filePattern.ast.pathname.match(relativeFilePath)
87
+ let match = route.fileMatcher.match(`http://remix.run/${relativeFilePath}`)
84
88
  if (!match) continue
85
- return normalizePathname(route.urlPattern.href(getPathnameParams(route.filePattern, match)))
89
+ return normalizePathname(createHref(route.urlPattern, match.params))
86
90
  }
87
91
 
88
92
  return null
@@ -104,8 +108,8 @@ function compileRoute(
104
108
  )
105
109
  let filePatternSource = normalizeFilePattern(route.filePattern)
106
110
 
107
- let urlPattern = new RoutePattern(urlPatternSource)
108
- let filePattern = new RoutePattern(filePatternSource)
111
+ let urlPattern = RoutePattern.parse(urlPatternSource)
112
+ let filePattern = RoutePattern.parse(filePatternSource)
109
113
 
110
114
  validateNoUnnamedWildcards(urlPattern, 'URL')
111
115
  validateNoUnnamedWildcards(filePattern, 'File')
@@ -114,37 +118,30 @@ function compileRoute(
114
118
  return {
115
119
  rootDir: normalizeFilePath(options.rootDir).replace(/\/+$/, ''),
116
120
  urlPattern,
121
+ urlMatcher: createMatcher(urlPattern),
117
122
  filePattern,
123
+ fileMatcher: createMatcher(stripDotSegments(filePatternSource)),
118
124
  }
119
125
  }
120
126
 
121
- function getPathnameParams(
122
- pattern: RoutePattern,
123
- match: Array<{ name: string; type: ':' | '*'; value: string }>,
124
- ): Record<string, string | undefined> {
125
- let params: Record<string, string | undefined> = {}
127
+ function stripDotSegments(pattern: string): string {
128
+ let segments: string[] = []
126
129
 
127
- for (let param of pattern.ast.pathname.params) {
128
- if (param.name === '*') continue
129
- params[param.name] = undefined
130
- }
131
-
132
- for (let param of match) {
133
- if (param.name === '*') continue
134
- 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)
135
137
  }
136
138
 
137
- return params
139
+ return segments.join('/')
138
140
  }
139
141
 
140
142
  function validateRoutePatterns(urlPattern: RoutePattern, filePattern: RoutePattern): void {
141
- let urlParams = urlPattern.ast.pathname.params.map(
142
- (param: { name: string; type: ':' | '*' }) => `${param.type}:${param.name}`,
143
- )
144
- let fileParams = filePattern.ast.pathname.params.map(
145
- (param: { name: string; type: ':' | '*' }) => `${param.type}:${param.name}`,
146
- )
147
-
143
+ let urlParams = getPathnameParams(urlPattern)
144
+ let fileParams = getPathnameParams(filePattern)
148
145
  if (urlParams.length !== fileParams.length) {
149
146
  throw new Error(
150
147
  `Route patterns must have matching capture structure.\nURL: ${urlPattern}\nFile: ${filePattern}`,
@@ -152,7 +149,9 @@ function validateRoutePatterns(urlPattern: RoutePattern, filePattern: RoutePatte
152
149
  }
153
150
 
154
151
  for (let i = 0; i < urlParams.length; i++) {
155
- 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) {
156
155
  throw new Error(
157
156
  `Route patterns must have matching capture structure.\nURL: ${urlPattern}\nFile: ${filePattern}`,
158
157
  )
@@ -161,13 +160,17 @@ function validateRoutePatterns(urlPattern: RoutePattern, filePattern: RoutePatte
161
160
  }
162
161
 
163
162
  function validateNoUnnamedWildcards(pattern: RoutePattern, label: string): void {
164
- if (
165
- pattern.ast.pathname.params.some(
166
- (param: { name: string; type: ':' | '*' }) => param.type === '*' && param.name === '*',
167
- )
168
- ) {
163
+ if (pattern.pathname.tokens.some((token) => token.type === '*' && token.name === '*')) {
169
164
  throw new Error(
170
165
  `${label} route patterns must use named wildcards for reversible mapping.\nPattern: ${pattern}`,
171
166
  )
172
167
  }
173
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))
@@ -105,7 +105,7 @@ export async function resolveModule(
105
105
  transformed.unresolvedImports.length > 0
106
106
  ? await batchResolveSpecifiers(
107
107
  getUniqueSpecifiers(transformed.unresolvedImports),
108
- transformed.resolvedPath,
108
+ transformed.identityPath,
109
109
  args.resolverFactory,
110
110
  )
111
111
  : new Map<string, ResolvedSpec>()
@@ -196,8 +196,10 @@ export async function resolveModule(
196
196
  deps.add(resolvedImport.identityPath)
197
197
 
198
198
  if (transformed.packageSpecifiers.includes(unresolved.specifier)) {
199
- let packageJsonPath =
200
- resolvedSpec.packageJsonPath ?? findNearestPackageJsonPath(resolvedImport.resolvedPath)
199
+ let packageJsonPath = resolvePackageJsonPath(
200
+ resolvedSpec.packageJsonPath,
201
+ resolvedImport.resolvedPath,
202
+ )
201
203
  if (packageJsonPath && !args.isWatchIgnored(packageJsonPath)) {
202
204
  trackedFiles.add(packageJsonPath)
203
205
  }
@@ -235,6 +237,25 @@ export async function resolveModule(
235
237
  }
236
238
  }
237
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
+
238
259
  function findNearestPackageJsonPath(filePath: string): string | null {
239
260
  let directory = path.dirname(filePath)
240
261
 
@@ -250,6 +271,17 @@ function findNearestPackageJsonPath(filePath: string): string | null {
250
271
  }
251
272
  }
252
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
+
253
285
  function isRelativeImportSpecifier(specifier: string): boolean {
254
286
  return specifier.startsWith('./') || specifier.startsWith('../')
255
287
  }
@@ -261,7 +261,7 @@ export async function transformModule(
261
261
  content: sourceText,
262
262
  }),
263
263
  identityPath: record.identityPath,
264
- importerDir: path.dirname(resolvedPath),
264
+ importerDir: path.dirname(record.identityPath),
265
265
  packageSpecifiers: analysis.unresolvedImports
266
266
  .filter((unresolved) => isPackageImportSpecifier(unresolved.specifier))
267
267
  .map((unresolved) => unresolved.specifier),
@@ -44,7 +44,14 @@ type StyleGetOptions = {
44
44
  type StyleCompilerOptions = {
45
45
  buildId?: string
46
46
  fingerprintAssets: boolean
47
+ getServedFileUrl?(
48
+ identityPath: string,
49
+ options: {
50
+ transform: readonly string[] | null
51
+ },
52
+ ): Promise<string>
47
53
  isAllowed(absolutePath: string): boolean
54
+ isServedFilePath(filePath: string): boolean
48
55
  minify: boolean
49
56
  onWatchDirectoriesChange?: (delta: { add: string[]; remove: string[] }) => void
50
57
  rootDir: string
@@ -79,6 +86,7 @@ export function createStyleCompiler(options: StyleCompilerOptions): StyleCompile
79
86
  let emitInFlightByCacheKey = new Map<string, Promise<EmittedStyle>>()
80
87
  let resolveArgs: ResolveArgs = {
81
88
  isAllowed: resolvedOptions.isAllowed,
89
+ isServedFilePath: resolvedOptions.isServedFilePath,
82
90
  isWatchIgnored,
83
91
  routes: resolvedOptions.routes,
84
92
  }
@@ -248,6 +256,7 @@ export function createStyleCompiler(options: StyleCompilerOptions): StyleCompile
248
256
  let startedVersion = record.invalidationVersion
249
257
  let resolvedStyle = await getOrCreateResolvedStyle(record)
250
258
  let emitResolvedStyleResult = await emitResolvedStyle(resolvedStyle, {
259
+ getServedFileUrl: resolvedOptions.getServedFileUrl,
251
260
  getServedUrl,
252
261
  sourceMaps: resolvedOptions.sourceMaps,
253
262
  })
@@ -34,6 +34,12 @@ type EmitResult =
34
34
  export async function emitResolvedStyle(
35
35
  resolvedStyle: ResolvedStyle,
36
36
  options: {
37
+ getServedFileUrl?(
38
+ identityPath: string,
39
+ options: {
40
+ transform: readonly string[] | null
41
+ },
42
+ ): Promise<string>
37
43
  getServedUrl(identityPath: string): Promise<string>
38
44
  sourceMaps?: 'external' | 'inline'
39
45
  },
@@ -77,6 +83,12 @@ async function rewriteDependencies(
77
83
  resolvedStyle: ResolvedStyle,
78
84
  options: {
79
85
  getServedUrl(identityPath: string): Promise<string>
86
+ getServedFileUrl?(
87
+ identityPath: string,
88
+ options: {
89
+ transform: readonly string[] | null
90
+ },
91
+ ): Promise<string>
80
92
  },
81
93
  ): Promise<{ code: string; sourceMap: string | null }> {
82
94
  if (resolvedStyle.dependencies.length === 0) {
@@ -92,7 +104,14 @@ async function rewriteDependencies(
92
104
  let replacement =
93
105
  dependency.kind === 'external'
94
106
  ? dependency.replacement
95
- : `${await options.getServedUrl(dependency.depPath)}${dependency.suffix}`
107
+ : dependency.kind === 'style'
108
+ ? `${await options.getServedUrl(dependency.depPath)}${dependency.suffix}`
109
+ : appendUrlSuffix(
110
+ await getServedFileUrl(options, resolvedStyle.identityPath, dependency.depPath, {
111
+ transform: dependency.requestTransform,
112
+ }),
113
+ dependency.suffix,
114
+ )
96
115
  let start = resolvedStyle.rawCode.indexOf(dependency.placeholder)
97
116
  if (start < 0) {
98
117
  throw createAssetServerCompilationError(
@@ -117,6 +136,61 @@ async function rewriteDependencies(
117
136
  }
118
137
  }
119
138
 
139
+ function appendUrlSuffix(url: string, suffix: string): string {
140
+ if (!suffix.startsWith('?') || !url.includes('?')) {
141
+ return `${url}${suffix}`
142
+ }
143
+
144
+ return `${url}&${suffix.slice(1)}`
145
+ }
146
+
147
+ async function getServedFileUrl(
148
+ options: {
149
+ getServedFileUrl?(
150
+ identityPath: string,
151
+ options: {
152
+ transform: readonly string[] | null
153
+ },
154
+ ): Promise<string>
155
+ getServedUrl(identityPath: string): Promise<string>
156
+ },
157
+ importerPath: string,
158
+ identityPath: string,
159
+ request: {
160
+ transform: readonly string[] | null
161
+ },
162
+ ): Promise<string> {
163
+ if (!options.getServedFileUrl) {
164
+ throw createAssetServerCompilationError(`Missing file URL resolver for ${identityPath}.`, {
165
+ code: 'EMIT_FAILED',
166
+ })
167
+ }
168
+
169
+ try {
170
+ return await options.getServedFileUrl(identityPath, request)
171
+ } catch (error) {
172
+ if (
173
+ request.transform !== null &&
174
+ isAssetServerCompilationError(error) &&
175
+ error.code === 'FILE_TRANSFORM_QUERY_INVALID'
176
+ ) {
177
+ console.warn(
178
+ `Invalid file transform request "${request.transform.join(',')}" in CSS asset ${importerPath} for ${identityPath}: ${error.message}`,
179
+ )
180
+ let href = await options.getServedFileUrl(identityPath, { transform: null })
181
+ let searchParams = new URLSearchParams()
182
+ for (let transform of request.transform) {
183
+ searchParams.append('transform', transform)
184
+ }
185
+
186
+ let search = searchParams.toString()
187
+ return search.length > 0 ? `${href}?${search}` : href
188
+ }
189
+
190
+ throw error
191
+ }
192
+ }
193
+
120
194
  async function createEmittedAsset(content: string): Promise<EmittedAsset> {
121
195
  return {
122
196
  content,