@remix-run/assets 0.0.0 → 0.1.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 (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +319 -2
  3. package/dist/assets.d.ts +3 -0
  4. package/dist/assets.d.ts.map +1 -0
  5. package/dist/assets.js +1 -0
  6. package/dist/lib/access.d.ts +10 -0
  7. package/dist/lib/access.d.ts.map +1 -0
  8. package/dist/lib/access.js +14 -0
  9. package/dist/lib/asset-server.d.ts +137 -0
  10. package/dist/lib/asset-server.d.ts.map +1 -0
  11. package/dist/lib/asset-server.js +242 -0
  12. package/dist/lib/compilation-error.d.ts +33 -0
  13. package/dist/lib/compilation-error.d.ts.map +1 -0
  14. package/dist/lib/compilation-error.js +32 -0
  15. package/dist/lib/file-matcher.d.ts +6 -0
  16. package/dist/lib/file-matcher.d.ts.map +1 -0
  17. package/dist/lib/file-matcher.js +43 -0
  18. package/dist/lib/fingerprint.d.ts +12 -0
  19. package/dist/lib/fingerprint.d.ts.map +1 -0
  20. package/dist/lib/fingerprint.js +49 -0
  21. package/dist/lib/paths.d.ts +8 -0
  22. package/dist/lib/paths.d.ts.map +1 -0
  23. package/dist/lib/paths.js +50 -0
  24. package/dist/lib/routes.d.ts +13 -0
  25. package/dist/lib/routes.d.ts.map +1 -0
  26. package/dist/lib/routes.js +94 -0
  27. package/dist/lib/scripts/cjs-check.d.ts +3 -0
  28. package/dist/lib/scripts/cjs-check.d.ts.map +1 -0
  29. package/dist/lib/scripts/cjs-check.js +398 -0
  30. package/dist/lib/scripts/compiler.d.ts +62 -0
  31. package/dist/lib/scripts/compiler.d.ts.map +1 -0
  32. package/dist/lib/scripts/compiler.js +435 -0
  33. package/dist/lib/scripts/emit.d.ts +25 -0
  34. package/dist/lib/scripts/emit.d.ts.map +1 -0
  35. package/dist/lib/scripts/emit.js +63 -0
  36. package/dist/lib/scripts/resolve.d.ts +60 -0
  37. package/dist/lib/scripts/resolve.d.ts.map +1 -0
  38. package/dist/lib/scripts/resolve.js +230 -0
  39. package/dist/lib/scripts/store.d.ts +40 -0
  40. package/dist/lib/scripts/store.d.ts.map +1 -0
  41. package/dist/lib/scripts/store.js +228 -0
  42. package/dist/lib/scripts/transform.d.ts +62 -0
  43. package/dist/lib/scripts/transform.d.ts.map +1 -0
  44. package/dist/lib/scripts/transform.js +362 -0
  45. package/dist/lib/source-maps.d.ts +4 -0
  46. package/dist/lib/source-maps.d.ts.map +1 -0
  47. package/dist/lib/source-maps.js +56 -0
  48. package/dist/lib/watch.d.ts +22 -0
  49. package/dist/lib/watch.d.ts.map +1 -0
  50. package/dist/lib/watch.js +96 -0
  51. package/package.json +50 -12
  52. package/src/assets.ts +2 -0
  53. package/src/lib/access.ts +24 -0
  54. package/src/lib/asset-server.ts +415 -0
  55. package/src/lib/compilation-error.ts +61 -0
  56. package/src/lib/file-matcher.ts +62 -0
  57. package/src/lib/fingerprint.ts +65 -0
  58. package/src/lib/paths.ts +66 -0
  59. package/src/lib/routes.ts +164 -0
  60. package/src/lib/scripts/cjs-check.ts +476 -0
  61. package/src/lib/scripts/compiler.ts +622 -0
  62. package/src/lib/scripts/emit.ts +122 -0
  63. package/src/lib/scripts/resolve.ts +422 -0
  64. package/src/lib/scripts/store.ts +327 -0
  65. package/src/lib/scripts/transform.ts +594 -0
  66. package/src/lib/source-maps.ts +68 -0
  67. package/src/lib/watch.ts +136 -0
@@ -0,0 +1,415 @@
1
+ import * as path from 'node:path'
2
+ import * as fs from 'node:fs'
3
+ import { isAssetServerCompilationError } from './compilation-error.ts'
4
+ import { createAccessPolicy } from './access.ts'
5
+ import { createModuleCompiler, createResponseForModule } from './scripts/compiler.ts'
6
+ import { normalizeFilePath } from './paths.ts'
7
+ import { compileRoutes } from './routes.ts'
8
+ import type { CompiledRoutes } from './routes.ts'
9
+ import { createAssetServerWatcher } from './watch.ts'
10
+ import type { ChokidarWatcher } from './watch.ts'
11
+ import type { AssetServerWatcher } from './watch.ts'
12
+
13
+ interface AssetServerWatchOptions {
14
+ /**
15
+ * Ignore matching glob patterns or file paths. Relative values are resolved
16
+ * from `rootDir`.
17
+ */
18
+ ignore?: readonly string[]
19
+ /**
20
+ * Use polling instead of native filesystem events. Defaults to `false`.
21
+ */
22
+ poll?: boolean
23
+ /**
24
+ * Polling interval in milliseconds when `poll` is enabled. Defaults to `100`.
25
+ */
26
+ pollInterval?: number
27
+ }
28
+
29
+ interface FingerprintOptions {
30
+ /**
31
+ * Per-build invalidation token that must change whenever fingerprinted module URLs
32
+ * should be invalidated together.
33
+ */
34
+ buildId: string
35
+ }
36
+
37
+ const scriptTargets = [
38
+ 'es2015',
39
+ 'es2016',
40
+ 'es2017',
41
+ 'es2018',
42
+ 'es2019',
43
+ 'es2020',
44
+ 'es2021',
45
+ 'es2022',
46
+ 'es2023',
47
+ 'es2024',
48
+ 'es2025',
49
+ 'es2026',
50
+ 'esnext',
51
+ ] as const
52
+ const scriptTargetSet = new Set<string>(scriptTargets)
53
+
54
+ export type ScriptsTarget = (typeof scriptTargets)[number]
55
+
56
+ export interface AssetServerOptions {
57
+ /** File patterns keyed by public URL patterns. */
58
+ fileMap: Readonly<Record<string, string>>
59
+ /**
60
+ * Root directory used to resolve relative file paths. Defaults to `process.cwd()`.
61
+ */
62
+ rootDir?: string
63
+ /**
64
+ * Glob patterns or file paths that are allowed to be served. Relative values are resolved from `rootDir`.
65
+ */
66
+ allow: readonly string[]
67
+ /**
68
+ * Glob patterns or file paths that are denied from being served. Relative values are resolved from `rootDir`.
69
+ */
70
+ deny?: readonly string[]
71
+ /**
72
+ * Controls optional source-based URL fingerprinting for rewritten import URLs.
73
+ *
74
+ * When omitted, all served modules use stable non-fingerprinted URLs with `Cache-Control: no-cache`.
75
+ * Cannot be used together with active watch mode. Set `watch: false` when fingerprinting.
76
+ */
77
+ fingerprint?: FingerprintOptions
78
+ /**
79
+ * Script pipeline configuration. Omit to use defaults.
80
+ */
81
+ scripts?: {
82
+ /**
83
+ * Source map mode (disabled when omitted).
84
+ * - `'external'`: serve source maps as separate `.map` files; adds `//# sourceMappingURL=` comment
85
+ * - `'inline'`: embed source maps as a base64 data URL directly in the JS; no separate `.map` file
86
+ */
87
+ sourceMaps?: 'inline' | 'external'
88
+ /**
89
+ * Controls the source paths written into source map `sources`.
90
+ * - `'url'` (default): use the stable server path (e.g. `'/assets/app/entry.ts'`)
91
+ * - `'absolute'`: use the original filesystem path on disk
92
+ */
93
+ sourceMapSourcePaths?: 'url' | 'absolute'
94
+ /**
95
+ * Minify emitted modules.
96
+ */
97
+ minify?: boolean
98
+ /**
99
+ * Replace global expressions with constant values during transform, e.g.
100
+ * `{ 'process.env.NODE_ENV': '"production"' }`
101
+ */
102
+ define?: Record<string, string>
103
+ /**
104
+ * Lower emitted syntax to a specific ECMAScript target. Omit this option to preserve
105
+ * modern syntax unless project configuration already requests a lower target.
106
+ */
107
+ target?: ScriptsTarget
108
+ /** Import specifiers to leave unrewritten (CDN URLs, import map entries, etc.) */
109
+ external?: string[]
110
+ }
111
+ /**
112
+ * Enable filesystem-backed cache invalidation for long-lived server instances.
113
+ * Enabled by default. Pass `true` to use the default watcher options, an options
114
+ * object to customize watcher behavior, or `false` to disable watching.
115
+ */
116
+ watch?: boolean | AssetServerWatchOptions
117
+ /**
118
+ * Handles unexpected request-time compilation errors. Return a `Response` to override the
119
+ * default `500 Internal Server Error` response, or return nothing to use the default.
120
+ */
121
+ onError?: (error: unknown) => void | Response | Promise<void | Response>
122
+ }
123
+
124
+ export interface AssetServer {
125
+ /**
126
+ * Serves a script request. Returns `Response | null` — null means the request was not
127
+ * handled by this server, letting the router fall through to a 404.
128
+ */
129
+ fetch(request: Request): Promise<Response | null>
130
+ /**
131
+ * Returns the request href for a served module file.
132
+ */
133
+ getHref(filePath: string): Promise<string>
134
+ /**
135
+ * Returns preload URLs for one or more served module files, ordered shallowest-first.
136
+ */
137
+ getPreloads(filePath: string | readonly string[]): Promise<string[]>
138
+ /**
139
+ * Closes any watcher resources owned by this server instance.
140
+ */
141
+ close(): Promise<void>
142
+ }
143
+
144
+ type ResolvedAssetServerOptions = {
145
+ allow: readonly string[]
146
+ buildId?: string
147
+ define?: Record<string, string>
148
+ deny?: readonly string[]
149
+ external: string[]
150
+ fingerprintModules: boolean
151
+ minify: boolean
152
+ onError: NonNullable<AssetServerOptions['onError']>
153
+ rootDir: string
154
+ routes: CompiledRoutes
155
+ sourceMapSourcePaths: 'url' | 'absolute'
156
+ sourceMaps?: 'inline' | 'external'
157
+ scriptsTarget?: ScriptsTarget
158
+ watchOptions: AssetServerWatchOptions | null
159
+ }
160
+
161
+ const chokidarWatcherByAssetServer = new WeakMap<AssetServer, ChokidarWatcher>()
162
+ const watcherByAssetServer = new WeakMap<AssetServer, AssetServerWatcher>()
163
+
164
+ export function getInternalChokidarWatcher(assetServer: AssetServer): ChokidarWatcher | undefined {
165
+ return chokidarWatcherByAssetServer.get(assetServer)
166
+ }
167
+
168
+ export function getInternalWatchTargets(assetServer: AssetServer): readonly string[] {
169
+ return watcherByAssetServer.get(assetServer)?.getWatchedTargets() ?? []
170
+ }
171
+
172
+ /**
173
+ * Create an asset server instance
174
+ *
175
+ * Compiles TypeScript/JavaScript modules on demand with optional source-based URL
176
+ * fingerprinting, caching, and configurable file mapping.
177
+ *
178
+ * @param options Server configuration
179
+ * @returns A {@link AssetServer} with `fetch()`, `getHref()`, and `getPreloads()` methods
180
+ *
181
+ * @example
182
+ * ```ts
183
+ * let assetServer = createAssetServer({
184
+ * fileMap: {
185
+ * '/assets/app/*path': 'app/*path',
186
+ * },
187
+ * allow: ['app/**'],
188
+ * })
189
+ *
190
+ * route('/assets/*path', ({ request }) => assetServer.fetch(request))
191
+ * ```
192
+ */
193
+ export function createAssetServer(options: AssetServerOptions): AssetServer {
194
+ let resolvedOptions = resolveAssetServerOptions(options)
195
+ let accessPolicy = createAccessPolicy({
196
+ allow: resolvedOptions.allow,
197
+ deny: resolvedOptions.deny,
198
+ rootDir: resolvedOptions.rootDir,
199
+ })
200
+ let watcher: AssetServerWatcher | null = null
201
+ let chokidarWatcher: ChokidarWatcher | null = null
202
+ let moduleCompiler = createModuleCompiler({
203
+ buildId: resolvedOptions.buildId,
204
+ define: resolvedOptions.define,
205
+ external: resolvedOptions.external,
206
+ fingerprintModules: resolvedOptions.fingerprintModules,
207
+ isAllowed: accessPolicy.isAllowed,
208
+ minify: resolvedOptions.minify,
209
+ onWatchDirectoriesChange: (delta) => {
210
+ if (!watcher) return
211
+ watcher.updateWatchedDirectories(delta)
212
+ },
213
+ rootDir: resolvedOptions.rootDir,
214
+ routes: resolvedOptions.routes,
215
+ sourceMapSourcePaths: resolvedOptions.sourceMapSourcePaths,
216
+ sourceMaps: resolvedOptions.sourceMaps,
217
+ target: resolvedOptions.scriptsTarget,
218
+ watchIgnore: resolvedOptions.watchOptions?.ignore,
219
+ watchMode: resolvedOptions.watchOptions !== null,
220
+ })
221
+ if (resolvedOptions.watchOptions) {
222
+ watcher = createAssetServerWatcher({
223
+ ...resolvedOptions.watchOptions,
224
+ onChokidarWatcherCreated(createdWatcher) {
225
+ chokidarWatcher = createdWatcher
226
+ },
227
+ onFileEvent: handleWatchEvent,
228
+ rootDir: resolvedOptions.rootDir,
229
+ })
230
+ }
231
+
232
+ async function responseForError(error: unknown): Promise<Response> {
233
+ try {
234
+ return (await resolvedOptions.onError(error)) ?? internalServerError()
235
+ } catch (error) {
236
+ console.error(`There was an error in the asset server error handler: ${error}`)
237
+ return internalServerError()
238
+ }
239
+ }
240
+
241
+ async function handleWatchEvent(filePath: string, event: 'add' | 'change' | 'unlink') {
242
+ try {
243
+ let normalizedFilePath = normalizeFilePath(filePath)
244
+ await moduleCompiler.handleFileEvent(normalizedFilePath, event)
245
+ } catch (error) {
246
+ console.error(`There was an error invalidating the asset server cache: ${error}`)
247
+ }
248
+ }
249
+
250
+ let assetServer: AssetServer = {
251
+ async fetch(request) {
252
+ if (request.method !== 'GET' && request.method !== 'HEAD') return null
253
+
254
+ let parsedRequestPathname = moduleCompiler.parseRequestPathname(new URL(request.url).pathname)
255
+ if (!parsedRequestPathname) return null
256
+
257
+ try {
258
+ let ifNoneMatch = request.headers.get('If-None-Match')
259
+ let moduleResult = await moduleCompiler.getModule(parsedRequestPathname.filePath, {
260
+ ifNoneMatch,
261
+ isSourceMapRequest: parsedRequestPathname.isSourceMapRequest,
262
+ requestedFingerprint: parsedRequestPathname.requestedFingerprint,
263
+ })
264
+ if (moduleResult.type === 'not-modified') {
265
+ return new Response(null, {
266
+ status: 304,
267
+ headers: { ETag: moduleResult.etag },
268
+ })
269
+ }
270
+
271
+ let compiledModule = moduleResult.module
272
+
273
+ if (parsedRequestPathname.requestedFingerprint !== null) {
274
+ if (compiledModule.fingerprint !== parsedRequestPathname.requestedFingerprint) return null
275
+ }
276
+
277
+ return createResponseForModule(compiledModule, {
278
+ cacheControl: parsedRequestPathname.cacheControl,
279
+ ifNoneMatch,
280
+ isSourceMapRequest: parsedRequestPathname.isSourceMapRequest,
281
+ method: request.method,
282
+ })
283
+ } catch (error) {
284
+ // A direct request can race with the filesystem or fail a deeper allow check while
285
+ // compiling imports. In this fetch context, both cases should fall through as "not
286
+ // handled here" so the outer router can continue to its own 404 behavior.
287
+ if (
288
+ isAssetServerCompilationError(error) &&
289
+ (error.code === 'MODULE_NOT_FOUND' || error.code === 'MODULE_NOT_ALLOWED')
290
+ ) {
291
+ return null
292
+ }
293
+
294
+ return responseForError(error)
295
+ }
296
+ },
297
+
298
+ async getHref(filePath) {
299
+ return moduleCompiler.getHref(filePath)
300
+ },
301
+ async getPreloads(filePath) {
302
+ return moduleCompiler.getPreloadUrls(filePath)
303
+ },
304
+ async close() {
305
+ await watcher?.close()
306
+ },
307
+ }
308
+
309
+ if (chokidarWatcher) {
310
+ chokidarWatcherByAssetServer.set(assetServer, chokidarWatcher)
311
+ }
312
+ if (watcher) {
313
+ watcherByAssetServer.set(assetServer, watcher)
314
+ }
315
+
316
+ return assetServer
317
+ }
318
+
319
+ function internalServerError(): Response {
320
+ return new Response('Internal Server Error', {
321
+ status: 500,
322
+ headers: { 'Content-Type': 'text/plain; charset=utf-8' },
323
+ })
324
+ }
325
+
326
+ function defaultErrorHandler(error: unknown): void {
327
+ console.error(error)
328
+ }
329
+
330
+ function resolveAssetServerOptions(options: AssetServerOptions): ResolvedAssetServerOptions {
331
+ let rootDir = normalizeFilePath(fs.realpathSync(path.resolve(options.rootDir ?? process.cwd())))
332
+ let scriptOptions = options.scripts ?? {}
333
+ let fingerprintOptions = normalizeFingerprintOptions({
334
+ fingerprint: options.fingerprint,
335
+ watch: options.watch,
336
+ })
337
+
338
+ return {
339
+ allow: options.allow,
340
+ buildId: fingerprintOptions.buildId,
341
+ define: scriptOptions.define,
342
+ deny: options.deny,
343
+ external: scriptOptions.external ?? [],
344
+ fingerprintModules: fingerprintOptions.enabled,
345
+ minify: scriptOptions.minify ?? false,
346
+ onError: options.onError ?? defaultErrorHandler,
347
+ rootDir,
348
+ routes: compileRoutes({
349
+ fileMap: options.fileMap,
350
+ rootDir,
351
+ }),
352
+ sourceMapSourcePaths: scriptOptions.sourceMapSourcePaths ?? 'url',
353
+ sourceMaps: scriptOptions.sourceMaps,
354
+ scriptsTarget: normalizeTarget(scriptOptions.target),
355
+ watchOptions: normalizeWatchOptions(options.watch),
356
+ }
357
+ }
358
+
359
+ function normalizeTarget(
360
+ target: NonNullable<AssetServerOptions['scripts']>['target'],
361
+ ): ScriptsTarget | undefined {
362
+ if (target == null) return undefined
363
+
364
+ if (typeof target !== 'string' || !scriptTargetSet.has(target)) {
365
+ throw new TypeError(
366
+ `Expected target to be one of ${scriptTargets.map((value) => `"${value}"`).join(', ')}. Received "${target}".`,
367
+ )
368
+ }
369
+
370
+ return target as ScriptsTarget
371
+ }
372
+
373
+ function normalizeFingerprintOptions(options: {
374
+ fingerprint: AssetServerOptions['fingerprint']
375
+ watch: AssetServerOptions['watch']
376
+ }):
377
+ | {
378
+ enabled: false
379
+ buildId?: string
380
+ }
381
+ | {
382
+ enabled: true
383
+ buildId: string
384
+ } {
385
+ if (!options.fingerprint) {
386
+ return {
387
+ enabled: false,
388
+ }
389
+ }
390
+
391
+ if (typeof options.fingerprint.buildId !== 'string') {
392
+ throw new TypeError('fingerprint.buildId must be a string')
393
+ }
394
+
395
+ if (options.fingerprint.buildId.length === 0) {
396
+ throw new TypeError('fingerprint.buildId must be a non-empty string')
397
+ }
398
+
399
+ if (options.watch !== false) {
400
+ throw new TypeError('fingerprint cannot be used with watch mode')
401
+ }
402
+
403
+ return {
404
+ enabled: true,
405
+ buildId: options.fingerprint.buildId,
406
+ }
407
+ }
408
+
409
+ function normalizeWatchOptions(
410
+ options: AssetServerOptions['watch'],
411
+ ): AssetServerWatchOptions | null {
412
+ if (options === false) return null
413
+ if (options == null || options === true) return {}
414
+ return options
415
+ }
@@ -0,0 +1,61 @@
1
+ type AssetServerCompilationErrorCode =
2
+ | 'MODULE_NOT_FOUND'
3
+ | 'MODULE_NOT_ALLOWED'
4
+ | 'MODULE_OUTSIDE_FILE_MAP'
5
+ | 'MODULE_COMMONJS_NOT_SUPPORTED'
6
+ | 'MODULE_TRANSFORM_FAILED'
7
+ | 'MODULE_EMIT_FAILED'
8
+ | 'IMPORT_RESOLUTION_FAILED'
9
+ | 'IMPORT_NOT_SUPPORTED'
10
+ | 'IMPORT_NOT_ALLOWED'
11
+ | 'IMPORT_OUTSIDE_FILE_MAP'
12
+
13
+ /**
14
+ * Internal error used by the request-time module compilation pipeline.
15
+ */
16
+ export class AssetServerCompilationError extends Error {
17
+ code: AssetServerCompilationErrorCode
18
+
19
+ constructor(
20
+ message: string,
21
+ options: {
22
+ cause?: unknown
23
+ code: AssetServerCompilationErrorCode
24
+ },
25
+ ) {
26
+ super(message, options.cause === undefined ? undefined : { cause: options.cause })
27
+ this.name = 'AssetServerCompilationError'
28
+ this.code = options.code
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Returns true when a value is an `AssetServerCompilationError`.
34
+ *
35
+ * @param error Value thrown by the compilation pipeline.
36
+ * @returns Whether the value is an `AssetServerCompilationError`.
37
+ */
38
+ export function isAssetServerCompilationError(
39
+ error: unknown,
40
+ ): error is AssetServerCompilationError {
41
+ return error instanceof AssetServerCompilationError
42
+ }
43
+
44
+ /**
45
+ * Creates an `AssetServerCompilationError` with a stable internal code.
46
+ *
47
+ * @param message Human-readable error message.
48
+ * @param options Structured internal error details.
49
+ * @param options.cause Original error cause, when available.
50
+ * @param options.code Stable internal compilation error code.
51
+ * @returns A `AssetServerCompilationError`.
52
+ */
53
+ export function createAssetServerCompilationError(
54
+ message: string,
55
+ options: {
56
+ cause?: unknown
57
+ code: AssetServerCompilationErrorCode
58
+ },
59
+ ): AssetServerCompilationError {
60
+ return new AssetServerCompilationError(message, options)
61
+ }
@@ -0,0 +1,62 @@
1
+ import * as fs from 'node:fs'
2
+ import * as path from 'node:path'
3
+
4
+ import { normalizeFilePath, resolveFilePath } from './paths.ts'
5
+
6
+ export type FileMatcher = (filePath: string) => boolean
7
+
8
+ export function createFileMatcher(
9
+ pattern: string,
10
+ rootDir: string,
11
+ options: {
12
+ allowDirectories?: boolean
13
+ allowMissing?: boolean
14
+ } = {},
15
+ ): FileMatcher {
16
+ let resolvedPatternPath = resolveFilePath(rootDir, pattern)
17
+ let allowDirectories = options.allowDirectories ?? true
18
+ let allowMissing = options.allowMissing ?? true
19
+
20
+ if (!containsGlobSyntax(pattern)) {
21
+ try {
22
+ resolvedPatternPath = normalizeFilePath(fs.realpathSync(resolvedPatternPath))
23
+ } catch (error) {
24
+ if (!allowMissing || !isPathNotFoundError(error)) throw error
25
+ }
26
+
27
+ if (allowDirectories) {
28
+ try {
29
+ if (fs.statSync(resolveFilePath(rootDir, pattern)).isDirectory()) {
30
+ return (filePath) => isSameOrDescendantPath(filePath, resolvedPatternPath)
31
+ }
32
+ } catch (error) {
33
+ if (!isPathNotFoundError(error)) throw error
34
+ }
35
+ }
36
+
37
+ return (filePath) => filePath === resolvedPatternPath
38
+ }
39
+
40
+ return (filePath) => path.posix.matchesGlob(filePath, resolvedPatternPath)
41
+ }
42
+
43
+ function isSameOrDescendantPath(filePath: string, directoryPath: string): boolean {
44
+ let normalizedDirectoryPath = directoryPath.replace(/\/+$/, '')
45
+
46
+ return filePath === normalizedDirectoryPath || filePath.startsWith(`${normalizedDirectoryPath}/`)
47
+ }
48
+
49
+ function containsGlobSyntax(pattern: string): boolean {
50
+ return /[*?[\]{}()!+@]/.test(pattern)
51
+ }
52
+
53
+ function isPathNotFoundError(
54
+ error: unknown,
55
+ ): error is NodeJS.ErrnoException & { code: 'ENOENT' | 'ENOTDIR' } {
56
+ return (
57
+ error instanceof Error &&
58
+ 'code' in error &&
59
+ ((error as NodeJS.ErrnoException).code === 'ENOENT' ||
60
+ (error as NodeJS.ErrnoException).code === 'ENOTDIR')
61
+ )
62
+ }
@@ -0,0 +1,65 @@
1
+ const fingerprintedExtensionRE = /^(.+)\.@([A-Za-z0-9_-]+)(\.[^./]+)$/
2
+ const fingerprintedBasenameRE = /^(.+)\.@([A-Za-z0-9_-]+)$/
3
+
4
+ export async function hashContent(content: string): Promise<string> {
5
+ let encoder = new TextEncoder()
6
+ let data = encoder.encode(content)
7
+ let hashBuffer = await crypto.subtle.digest('SHA-256', data)
8
+ return Buffer.from(hashBuffer).toString('base64url').slice(0, 6)
9
+ }
10
+
11
+ export async function generateFingerprint(options: {
12
+ buildId: string
13
+ content: string
14
+ }): Promise<string> {
15
+ return hashContent(JSON.stringify([options.content, options.buildId]))
16
+ }
17
+
18
+ export function parseFingerprintSuffix(pathname: string): {
19
+ pathname: string
20
+ requestedFingerprint: string | null
21
+ } {
22
+ let lastSlashIndex = pathname.lastIndexOf('/')
23
+ let directory = lastSlashIndex >= 0 ? pathname.slice(0, lastSlashIndex + 1) : ''
24
+ let basename = lastSlashIndex >= 0 ? pathname.slice(lastSlashIndex + 1) : pathname
25
+ let extensionMatch = basename.match(fingerprintedExtensionRE)
26
+
27
+ if (extensionMatch) {
28
+ return {
29
+ pathname: `${directory}${extensionMatch[1]}${extensionMatch[3]}`,
30
+ requestedFingerprint: extensionMatch[2],
31
+ }
32
+ }
33
+
34
+ let basenameMatch = basename.match(fingerprintedBasenameRE)
35
+ if (basenameMatch) {
36
+ return {
37
+ pathname: `${directory}${basenameMatch[1]}`,
38
+ requestedFingerprint: basenameMatch[2],
39
+ }
40
+ }
41
+
42
+ return {
43
+ pathname,
44
+ requestedFingerprint: null,
45
+ }
46
+ }
47
+
48
+ export function formatFingerprintedPathname(pathname: string, fingerprint: string | null): string {
49
+ if (fingerprint === null) return pathname
50
+
51
+ let lastSlashIndex = pathname.lastIndexOf('/')
52
+ let directory = lastSlashIndex >= 0 ? pathname.slice(0, lastSlashIndex + 1) : ''
53
+ let basename = lastSlashIndex >= 0 ? pathname.slice(lastSlashIndex + 1) : pathname
54
+ let lastDotIndex = basename.lastIndexOf('.')
55
+
56
+ if (lastDotIndex <= 0) {
57
+ return `${pathname}.@${fingerprint}`
58
+ }
59
+
60
+ return `${directory}${basename.slice(0, lastDotIndex)}.@${fingerprint}${basename.slice(lastDotIndex)}`
61
+ }
62
+
63
+ export function getFingerprintRequestCacheControl(requestedFingerprint: string | null): string {
64
+ return requestedFingerprint === null ? 'no-cache' : 'public, max-age=31536000, immutable'
65
+ }
@@ -0,0 +1,66 @@
1
+ import * as path from 'node:path'
2
+
3
+ const windowsDriveLetterRE = /^[A-Za-z]:\//
4
+ const uncPrefixRE = /^\/\/[^/]+\/[^/]+/
5
+
6
+ export function normalizeWindowsPath(filePath: string): string {
7
+ return filePath
8
+ .replace(/\\/g, '/')
9
+ .replace(windowsDriveLetterRE, (prefix) => `${prefix[0]!.toUpperCase()}${prefix.slice(1)}`)
10
+ }
11
+
12
+ export function normalizePathname(pathname: string): string {
13
+ let normalized = path.posix.normalize(normalizeWindowsPath(pathname))
14
+
15
+ if (!normalized.startsWith('/')) {
16
+ normalized = `/${normalized}`
17
+ }
18
+
19
+ return normalized
20
+ }
21
+
22
+ export function isAbsoluteFilePath(filePath: string): boolean {
23
+ let normalized = normalizeWindowsPath(filePath)
24
+ return normalized.startsWith('/') || windowsDriveLetterRE.test(normalized)
25
+ }
26
+
27
+ export function normalizeFilePath(filePath: string): string {
28
+ let normalized = normalizeWindowsPath(filePath)
29
+ let uncRoot = getUncRoot(normalized)
30
+
31
+ if (uncRoot) {
32
+ let remainder = normalized.slice(uncRoot.length)
33
+ let normalizedRemainder = path.posix.normalize(remainder || '/')
34
+ return `${uncRoot}${normalizedRemainder === '/' ? '' : normalizedRemainder}`
35
+ }
36
+
37
+ if (windowsDriveLetterRE.test(normalized)) {
38
+ return path.posix.normalize(normalized)
39
+ }
40
+
41
+ if (normalized.startsWith('/')) {
42
+ return path.posix.normalize(normalized)
43
+ }
44
+
45
+ return path.posix.normalize(normalizeWindowsPath(path.resolve(normalized)))
46
+ }
47
+
48
+ export function resolveFilePath(rootDir: string, filePath: string): string {
49
+ if (isAbsoluteFilePath(filePath)) {
50
+ return normalizeFilePath(filePath)
51
+ }
52
+
53
+ return normalizeFilePath(`${rootDir.replace(/\/+$/, '')}/${normalizeWindowsPath(filePath)}`)
54
+ }
55
+
56
+ export function getFilePathDirectory(filePath: string): string {
57
+ return path.posix.dirname(normalizeWindowsPath(filePath))
58
+ }
59
+
60
+ export function getFilePathBaseName(filePath: string): string {
61
+ return path.posix.basename(normalizeWindowsPath(filePath))
62
+ }
63
+
64
+ function getUncRoot(filePath: string): string | null {
65
+ return filePath.startsWith('//') ? (filePath.match(uncPrefixRE)?.[0] ?? null) : null
66
+ }