@remix-run/assets 0.4.4 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/README.md +331 -72
  2. package/dist/assets.d.ts +4 -1
  3. package/dist/assets.d.ts.map +1 -1
  4. package/dist/assets.js +2 -2
  5. package/dist/lib/access.d.ts +35 -4
  6. package/dist/lib/access.d.ts.map +1 -1
  7. package/dist/lib/access.js +332 -11
  8. package/dist/lib/asset-server.d.ts +138 -11
  9. package/dist/lib/asset-server.d.ts.map +1 -1
  10. package/dist/lib/asset-server.js +281 -30
  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/file-matcher.js +1 -1
  14. package/dist/lib/files/compiler.d.ts.map +1 -1
  15. package/dist/lib/files/compiler.js +9 -8
  16. package/dist/lib/files/config.js +1 -1
  17. package/dist/lib/hmr.d.ts +37 -0
  18. package/dist/lib/hmr.d.ts.map +1 -0
  19. package/dist/lib/hmr.js +357 -0
  20. package/dist/lib/injected-packages.d.ts +3 -2
  21. package/dist/lib/injected-packages.d.ts.map +1 -1
  22. package/dist/lib/injected-packages.js +43 -17
  23. package/dist/lib/inspection.d.ts +39 -0
  24. package/dist/lib/inspection.d.ts.map +1 -0
  25. package/dist/lib/inspection.js +160 -0
  26. package/dist/lib/loaders.d.ts +57 -0
  27. package/dist/lib/loaders.d.ts.map +1 -0
  28. package/dist/lib/loaders.js +1 -0
  29. package/dist/lib/module-store.d.ts +24 -0
  30. package/dist/lib/module-store.d.ts.map +1 -1
  31. package/dist/lib/module-store.js +136 -11
  32. package/dist/lib/routes.d.ts +11 -3
  33. package/dist/lib/routes.d.ts.map +1 -1
  34. package/dist/lib/routes.js +124 -80
  35. package/dist/lib/scripts/compiler.d.ts +24 -1
  36. package/dist/lib/scripts/compiler.d.ts.map +1 -1
  37. package/dist/lib/scripts/compiler.js +183 -29
  38. package/dist/lib/scripts/conditions.d.ts +2 -0
  39. package/dist/lib/scripts/conditions.d.ts.map +1 -0
  40. package/dist/lib/scripts/conditions.js +1 -0
  41. package/dist/lib/scripts/emit.d.ts +3 -0
  42. package/dist/lib/scripts/emit.d.ts.map +1 -1
  43. package/dist/lib/scripts/emit.js +24 -5
  44. package/dist/lib/scripts/resolve.d.ts +4 -0
  45. package/dist/lib/scripts/resolve.d.ts.map +1 -1
  46. package/dist/lib/scripts/resolve.js +75 -10
  47. package/dist/lib/scripts/transform.d.ts +9 -0
  48. package/dist/lib/scripts/transform.d.ts.map +1 -1
  49. package/dist/lib/scripts/transform.js +222 -18
  50. package/dist/lib/source-maps.js +1 -1
  51. package/dist/lib/styles/compiler.d.ts +14 -1
  52. package/dist/lib/styles/compiler.d.ts.map +1 -1
  53. package/dist/lib/styles/compiler.js +78 -14
  54. package/dist/lib/styles/emit.js +4 -4
  55. package/dist/lib/styles/resolve.d.ts.map +1 -1
  56. package/dist/lib/styles/resolve.js +16 -15
  57. package/dist/lib/styles/transform.js +5 -5
  58. package/dist/lib/target.d.ts +1 -1
  59. package/dist/lib/target.d.ts.map +1 -1
  60. package/dist/lib/watch.js +1 -1
  61. package/dist/types/hmr.d.ts +36 -0
  62. package/package.json +16 -11
  63. package/src/assets.ts +4 -1
  64. package/src/lib/access.ts +445 -11
  65. package/src/lib/asset-server.ts +466 -27
  66. package/src/lib/compilation-error.ts +4 -3
  67. package/src/lib/files/compiler.ts +9 -5
  68. package/src/lib/hmr.ts +397 -0
  69. package/src/lib/injected-packages.ts +52 -16
  70. package/src/lib/inspection.ts +229 -0
  71. package/src/lib/loaders.ts +80 -0
  72. package/src/lib/module-store.ts +190 -9
  73. package/src/lib/routes.ts +158 -126
  74. package/src/lib/scripts/compiler.ts +248 -24
  75. package/src/lib/scripts/conditions.ts +1 -0
  76. package/src/lib/scripts/emit.ts +50 -5
  77. package/src/lib/scripts/resolve.ts +129 -7
  78. package/src/lib/scripts/transform.ts +290 -19
  79. package/src/lib/styles/compiler.ts +108 -8
  80. package/src/lib/styles/emit.ts +1 -1
  81. package/src/lib/styles/resolve.ts +19 -15
  82. package/src/lib/styles/transform.ts +2 -2
  83. package/src/types/hmr.d.ts +36 -0
package/src/lib/access.ts CHANGED
@@ -1,26 +1,460 @@
1
+ import * as fs from 'node:fs'
2
+ import * as path from 'node:path'
1
3
  import { createFileMatcher } from './file-matcher.ts'
2
4
  import { isInjectedPackageFilePath } from './injected-packages.ts'
5
+ import { normalizeFilePath } from './paths.ts'
3
6
 
4
- type AccessPolicy = {
7
+ /** Access-policy result for an inspected asset file. */
8
+ export interface AssetAccessDetails {
9
+ /** Whether the asset server may serve the file. */
10
+ allowed: boolean
11
+ /** The first configured rule that allowed the file, when one matched. */
12
+ allowedBy?: AssetAccessRule
13
+ /** The first matching `denyFiles` pattern, when access was denied. */
14
+ deniedBy?: string
15
+ }
16
+
17
+ /** Rule that allows an inspected asset file to be served. */
18
+ export type AssetAccessRule =
19
+ /** A matching `allowFiles` entry. */
20
+ | { kind: 'file'; value: string }
21
+ /** A runtime file provided internally by the asset server. */
22
+ | { kind: 'injected'; value: string }
23
+ /** A matching `allowPackages` entry. */
24
+ | { kind: 'package'; value: string }
25
+
26
+ export type AccessPolicy = {
27
+ getAllowedPackageRoots(): readonly string[]
28
+ getPackageWatchDirectories(): readonly string[]
29
+ handleFileEvent(filePath: string): void
30
+ inspect(filePath: string): AssetAccessDetails
5
31
  isAllowed(filePath: string): boolean
6
32
  }
7
33
 
34
+ const packageStateFileNames = new Set([
35
+ 'bun.lock',
36
+ 'bun.lockb',
37
+ 'npm-shrinkwrap.json',
38
+ 'package-lock.json',
39
+ 'pnpm-lock.yaml',
40
+ 'pnpm-workspace.yaml',
41
+ 'yarn.lock',
42
+ ])
43
+ const packageManagerRootFileNames = packageStateFileNames
44
+ const packageNamePartPattern = /^[A-Za-z0-9._~-]+$/
45
+
8
46
  export function createAccessPolicy(options: {
9
- allow: readonly string[]
10
- deny?: readonly string[]
47
+ allowFiles: readonly string[]
48
+ allowPackages?: readonly string[]
49
+ denyFiles?: readonly string[]
50
+ packageSearchRoots?: readonly string[]
11
51
  rootDir: string
12
52
  }): AccessPolicy {
13
- let allowMatchers = options.allow.map((pattern) => createFileMatcher(pattern, options.rootDir))
14
- let denyMatchers = (options.deny ?? []).map((pattern) =>
15
- createFileMatcher(pattern, options.rootDir),
16
- )
53
+ let allowMatchers = options.allowFiles.map((pattern) => ({
54
+ matcher: createFileMatcher(pattern, options.rootDir),
55
+ pattern,
56
+ }))
57
+ let allowPackageNames = normalizePackageNames(options.allowPackages, 'allowPackages')
58
+ let denyMatchers = (options.denyFiles ?? []).map((pattern) => ({
59
+ matcher: createFileMatcher(pattern, options.rootDir),
60
+ pattern,
61
+ }))
62
+ let packageSearchRoots = [options.rootDir, ...(options.packageSearchRoots ?? [])]
63
+ let packageRootPaths = createPackageRootPaths({
64
+ allowPackageNames,
65
+ searchRoots: packageSearchRoots,
66
+ })
67
+ let allowPackageRootPathTrie = createPackageRootPathTrie(packageRootPaths)
68
+ let packageStateDirectories =
69
+ allowPackageNames.size === 0 ? [] : getPackageStateDirectories(packageSearchRoots)
70
+ let packageRootsDirty = false
71
+
72
+ function refreshPackageRootPathTries(): void {
73
+ if (!packageRootsDirty) return
74
+
75
+ packageRootPaths = createPackageRootPaths({
76
+ allowPackageNames,
77
+ searchRoots: packageSearchRoots,
78
+ })
79
+ allowPackageRootPathTrie = createPackageRootPathTrie(packageRootPaths)
80
+ packageRootsDirty = false
81
+ }
82
+
83
+ function getAllowedPackageName(filePath: string): string | undefined {
84
+ if (allowPackageNames.size === 0) return undefined
85
+ refreshPackageRootPathTries()
86
+
87
+ return getPackageNameFromRootPathTrie(filePath, allowPackageRootPathTrie)
88
+ }
89
+
90
+ function inspect(filePath: string): AssetAccessDetails {
91
+ if (isInjectedPackageFilePath(filePath)) {
92
+ return { allowed: true, allowedBy: { kind: 'injected', value: '@remix-run/assets' } }
93
+ }
94
+
95
+ let allowedBy: AssetAccessRule | undefined
96
+ let allowMatch = allowMatchers.find(({ matcher }) => matcher(filePath))
97
+ if (allowMatch) {
98
+ allowedBy = { kind: 'file', value: allowMatch.pattern }
99
+ } else {
100
+ let packageName = getAllowedPackageName(filePath)
101
+ if (packageName !== undefined) {
102
+ allowedBy = { kind: 'package', value: packageName }
103
+ }
104
+ }
105
+
106
+ if (!allowedBy) return { allowed: false }
107
+
108
+ let denyMatch = denyMatchers.find(({ matcher }) => matcher(filePath))
109
+ if (denyMatch) {
110
+ return { allowed: false, allowedBy, deniedBy: denyMatch.pattern }
111
+ }
112
+
113
+ return { allowed: true, allowedBy }
114
+ }
17
115
 
18
116
  return {
117
+ getAllowedPackageRoots() {
118
+ refreshPackageRootPathTries()
119
+ return [...packageRootPaths.keys()]
120
+ },
121
+ getPackageWatchDirectories() {
122
+ if (allowPackageNames.size === 0) return []
123
+ return packageStateDirectories
124
+ },
125
+ handleFileEvent(filePath) {
126
+ if (allowPackageNames.size === 0) return
127
+ if (!isPackageStateFileEvent(filePath, packageStateDirectories)) return
128
+
129
+ packageRootsDirty = true
130
+ },
131
+ inspect,
19
132
  isAllowed(filePath) {
20
- if (isInjectedPackageFilePath(filePath)) return true
21
- if (!allowMatchers.some((matcher) => matcher(filePath))) return false
22
- if (denyMatchers.length > 0 && denyMatchers.some((matcher) => matcher(filePath))) return false
23
- return true
133
+ return inspect(filePath).allowed
24
134
  },
25
135
  }
26
136
  }
137
+
138
+ function normalizePackageNames(
139
+ packageOption: readonly string[] | undefined,
140
+ optionName: 'allowPackages',
141
+ ): Set<string> {
142
+ let packageNames = new Set<string>()
143
+
144
+ for (let packageName of packageOption ?? []) {
145
+ if (typeof packageName !== 'string') {
146
+ throw new TypeError(`${optionName} values must be strings`)
147
+ }
148
+
149
+ let normalizedPackageName = packageName.trim()
150
+ if (!isValidPackageName(normalizedPackageName)) {
151
+ throw new TypeError(`${optionName} values must be package names. Received "${packageName}".`)
152
+ }
153
+
154
+ packageNames.add(normalizedPackageName)
155
+ }
156
+
157
+ return packageNames
158
+ }
159
+
160
+ function validatePackageName(packageName: string, message: string): void {
161
+ if (!isValidPackageName(packageName)) {
162
+ throw new TypeError(message)
163
+ }
164
+ }
165
+
166
+ type PackageJson = {
167
+ dependencies?: Record<string, string>
168
+ optionalDependencies?: Record<string, string>
169
+ }
170
+
171
+ type PackageRootPathTrie = {
172
+ children: Map<string, PackageRootPathTrie>
173
+ packageName?: string
174
+ }
175
+
176
+ type PackageRootQueueItem = {
177
+ packageJsonPath: string
178
+ packageName: string
179
+ }
180
+
181
+ function createPackageRootPaths(options: {
182
+ allowPackageNames: ReadonlySet<string>
183
+ searchRoots: readonly string[]
184
+ }): Map<string, string> {
185
+ let allowPackageRootPaths = new Map<string, string>()
186
+ let allowQueue: PackageRootQueueItem[] = []
187
+ let seenAllowedPackageRoots = new Set<string>()
188
+ let searchRoots = normalizePackageSearchRoots(options.searchRoots)
189
+
190
+ for (let packageName of options.allowPackageNames) {
191
+ let foundPackage = false
192
+
193
+ for (let searchRoot of searchRoots) {
194
+ let packageJsonPath = findPackageJsonPath(packageName, searchRoot)
195
+ if (packageJsonPath === null) continue
196
+
197
+ foundPackage = true
198
+ allowQueue.push({ packageJsonPath, packageName })
199
+ }
200
+
201
+ if (!foundPackage) {
202
+ throw new TypeError(`Could not resolve allowed package "${packageName}".`)
203
+ }
204
+ }
205
+
206
+ while (allowQueue.length > 0) {
207
+ let { packageJsonPath, packageName } = allowQueue.shift()!
208
+ let packageRootPath = normalizeFilePath(path.dirname(packageJsonPath))
209
+ if (seenAllowedPackageRoots.has(packageRootPath)) continue
210
+ seenAllowedPackageRoots.add(packageRootPath)
211
+
212
+ let packageJson = readPackageJson(packageJsonPath)
213
+ allowPackageRootPaths.set(packageRootPath, packageName)
214
+
215
+ for (let dependencyName of Object.keys(packageJson.dependencies ?? {})) {
216
+ validatePackageName(
217
+ dependencyName,
218
+ `Dependency "${dependencyName}" from ${packageJsonPath} must be a package name.`,
219
+ )
220
+ let dependencyPackageJsonPath = findPackageJsonPath(dependencyName, packageRootPath)
221
+ if (dependencyPackageJsonPath === null) {
222
+ throw new TypeError(
223
+ `Could not resolve dependency "${dependencyName}" from ${packageJsonPath}.`,
224
+ )
225
+ }
226
+ allowQueue.push({
227
+ packageJsonPath: dependencyPackageJsonPath,
228
+ packageName: dependencyName,
229
+ })
230
+ }
231
+ for (let dependencyName of Object.keys(packageJson.optionalDependencies ?? {})) {
232
+ validatePackageName(
233
+ dependencyName,
234
+ `Optional dependency "${dependencyName}" from ${packageJsonPath} must be a package name.`,
235
+ )
236
+ let dependencyPackageJsonPath = findPackageJsonPath(dependencyName, packageRootPath)
237
+ if (dependencyPackageJsonPath !== null) {
238
+ allowQueue.push({
239
+ packageJsonPath: dependencyPackageJsonPath,
240
+ packageName: dependencyName,
241
+ })
242
+ }
243
+ }
244
+ }
245
+
246
+ return allowPackageRootPaths
247
+ }
248
+
249
+ function createPackageRootPathTrie(
250
+ packageRootPaths: ReadonlyMap<string, string>,
251
+ ): PackageRootPathTrie {
252
+ let rootNode = createPackageRootPathTrieNode()
253
+
254
+ for (let [packageRootPath, packageName] of packageRootPaths) {
255
+ let node = rootNode
256
+ for (let segment of getFilePathSegments(packageRootPath)) {
257
+ let childNode = node.children.get(segment)
258
+ if (!childNode) {
259
+ childNode = createPackageRootPathTrieNode()
260
+ node.children.set(segment, childNode)
261
+ }
262
+ node = childNode
263
+ }
264
+ node.packageName = packageName
265
+ }
266
+
267
+ return rootNode
268
+ }
269
+
270
+ function createPackageRootPathTrieNode(): PackageRootPathTrie {
271
+ return {
272
+ children: new Map(),
273
+ }
274
+ }
275
+
276
+ function getPackageNameFromRootPathTrie(
277
+ filePath: string,
278
+ trie: PackageRootPathTrie,
279
+ ): string | undefined {
280
+ let node = trie
281
+ if (node.packageName !== undefined) return node.packageName
282
+
283
+ for (let segment of getFilePathSegments(filePath)) {
284
+ let childNode = node.children.get(segment)
285
+ if (!childNode) return undefined
286
+ if (childNode.packageName !== undefined) return childNode.packageName
287
+ node = childNode
288
+ }
289
+
290
+ return undefined
291
+ }
292
+
293
+ function getFilePathSegments(filePath: string): string[] {
294
+ return normalizeFilePath(filePath).split('/')
295
+ }
296
+
297
+ function normalizePackageSearchRoots(searchRoots: readonly string[]): string[] {
298
+ let normalizedSearchRoots = new Set<string>()
299
+
300
+ for (let searchRoot of searchRoots) {
301
+ let normalizedSearchRoot = normalizeFilePath(searchRoot)
302
+ normalizedSearchRoots.add(normalizedSearchRoot)
303
+
304
+ if (path.basename(normalizedSearchRoot) === 'node_modules') {
305
+ normalizedSearchRoots.add(path.dirname(normalizedSearchRoot))
306
+ }
307
+ }
308
+
309
+ return [...normalizedSearchRoots]
310
+ }
311
+
312
+ function getPackageStateDirectories(searchRoots: readonly string[]): string[] {
313
+ let packageStateDirectories = new Set<string>()
314
+
315
+ for (let searchRoot of searchRoots) {
316
+ let packageManagerRoot = findPackageManagerRoot(searchRoot)
317
+ if (packageManagerRoot !== null) {
318
+ packageStateDirectories.add(packageManagerRoot)
319
+ }
320
+ }
321
+
322
+ return [...packageStateDirectories]
323
+ }
324
+
325
+ function isPackageStateFileEvent(
326
+ filePath: string,
327
+ packageStateDirectories: readonly string[],
328
+ ): boolean {
329
+ let normalizedFilePath = normalizeFilePath(filePath)
330
+ let fileName = path.basename(normalizedFilePath)
331
+ if (!packageStateFileNames.has(fileName)) return false
332
+
333
+ let directory = path.dirname(normalizedFilePath)
334
+ return packageStateDirectories.some(
335
+ (packageStateDirectory) => directory === packageStateDirectory,
336
+ )
337
+ }
338
+
339
+ function findPackageManagerRoot(startDirectory: string): string | null {
340
+ let directory = normalizePackageStateSearchRoot(startDirectory)
341
+
342
+ while (true) {
343
+ if (hasPackageManagerRootFile(directory)) return directory
344
+
345
+ let parentDirectory = path.dirname(directory)
346
+ if (parentDirectory === directory) return null
347
+ directory = parentDirectory
348
+ }
349
+ }
350
+
351
+ function normalizePackageStateSearchRoot(searchRoot: string): string {
352
+ let normalizedSearchRoot = normalizeFilePath(searchRoot)
353
+ return path.basename(normalizedSearchRoot) === 'node_modules'
354
+ ? path.dirname(normalizedSearchRoot)
355
+ : normalizedSearchRoot
356
+ }
357
+
358
+ function hasPackageManagerRootFile(directory: string): boolean {
359
+ for (let fileName of packageManagerRootFileNames) {
360
+ try {
361
+ let stat = fs.statSync(path.join(directory, fileName))
362
+ if (stat.isFile()) return true
363
+ } catch (error) {
364
+ if (isPathNotFoundError(error)) continue
365
+ throw error
366
+ }
367
+ }
368
+
369
+ return false
370
+ }
371
+
372
+ function findPackageJsonPath(packageName: string, startDirectory: string): string | null {
373
+ let directory = normalizeFilePath(startDirectory)
374
+
375
+ while (true) {
376
+ let packageJsonPath = resolvePackageJsonPath(directory, packageName)
377
+ if (packageJsonPath !== null) return packageJsonPath
378
+
379
+ let parentDirectory = path.dirname(directory)
380
+ if (parentDirectory === directory) return null
381
+ directory = parentDirectory
382
+ }
383
+ }
384
+
385
+ function resolvePackageJsonPath(directory: string, packageName: string): string | null {
386
+ let packagePath =
387
+ path.basename(directory) === 'node_modules'
388
+ ? path.join(directory, packageName, 'package.json')
389
+ : path.join(directory, 'node_modules', packageName, 'package.json')
390
+
391
+ try {
392
+ return normalizeFilePath(fs.realpathSync(packagePath))
393
+ } catch (error) {
394
+ if (isPathNotFoundError(error)) return null
395
+ throw error
396
+ }
397
+ }
398
+
399
+ function readPackageJson(packageJsonPath: string): PackageJson {
400
+ let packageJson: unknown
401
+
402
+ try {
403
+ packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')) as unknown
404
+ } catch (error) {
405
+ if (isPathNotFoundError(error)) return {}
406
+ throw error
407
+ }
408
+
409
+ if (packageJson === null || typeof packageJson !== 'object') {
410
+ return {}
411
+ }
412
+
413
+ return {
414
+ dependencies: readDependencyMap(packageJson, 'dependencies'),
415
+ optionalDependencies: readDependencyMap(packageJson, 'optionalDependencies'),
416
+ }
417
+ }
418
+
419
+ function readDependencyMap(packageJson: object, key: string): Record<string, string> | undefined {
420
+ if (!(key in packageJson)) return undefined
421
+
422
+ let value = packageJson[key as keyof typeof packageJson]
423
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
424
+ return undefined
425
+ }
426
+
427
+ let dependencies: Record<string, string> = {}
428
+ for (let [dependencyName, dependencyVersion] of Object.entries(value)) {
429
+ if (typeof dependencyVersion === 'string') {
430
+ dependencies[dependencyName] = dependencyVersion
431
+ }
432
+ }
433
+
434
+ return dependencies
435
+ }
436
+
437
+ function isValidPackageName(packageName: string): boolean {
438
+ if (packageName.length === 0) return false
439
+
440
+ let packageNameParts = packageName.startsWith('@')
441
+ ? packageName.slice(1).split('/')
442
+ : packageName.split('/')
443
+
444
+ if (packageNameParts.length !== (packageName.startsWith('@') ? 2 : 1)) return false
445
+
446
+ return packageNameParts.every(
447
+ (part) => part.length > 0 && part !== '.' && part !== '..' && packageNamePartPattern.test(part),
448
+ )
449
+ }
450
+
451
+ function isPathNotFoundError(
452
+ error: unknown,
453
+ ): error is NodeJS.ErrnoException & { code: 'ENOENT' | 'ENOTDIR' } {
454
+ return (
455
+ error instanceof Error &&
456
+ 'code' in error &&
457
+ ((error as NodeJS.ErrnoException).code === 'ENOENT' ||
458
+ (error as NodeJS.ErrnoException).code === 'ENOTDIR')
459
+ )
460
+ }