@remix-run/assets 0.0.0 → 0.2.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 (87) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +325 -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 +139 -0
  10. package/dist/lib/asset-server.d.ts.map +1 -0
  11. package/dist/lib/asset-server.js +338 -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 +44 -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/module-store.d.ts +41 -0
  22. package/dist/lib/module-store.d.ts.map +1 -0
  23. package/dist/lib/module-store.js +230 -0
  24. package/dist/lib/paths.d.ts +8 -0
  25. package/dist/lib/paths.d.ts.map +1 -0
  26. package/dist/lib/paths.js +50 -0
  27. package/dist/lib/routes.d.ts +13 -0
  28. package/dist/lib/routes.d.ts.map +1 -0
  29. package/dist/lib/routes.js +94 -0
  30. package/dist/lib/scripts/cjs-check.d.ts +3 -0
  31. package/dist/lib/scripts/cjs-check.d.ts.map +1 -0
  32. package/dist/lib/scripts/cjs-check.js +398 -0
  33. package/dist/lib/scripts/compiler.d.ts +62 -0
  34. package/dist/lib/scripts/compiler.d.ts.map +1 -0
  35. package/dist/lib/scripts/compiler.js +439 -0
  36. package/dist/lib/scripts/emit.d.ts +25 -0
  37. package/dist/lib/scripts/emit.d.ts.map +1 -0
  38. package/dist/lib/scripts/emit.js +63 -0
  39. package/dist/lib/scripts/resolve.d.ts +50 -0
  40. package/dist/lib/scripts/resolve.d.ts.map +1 -0
  41. package/dist/lib/scripts/resolve.js +236 -0
  42. package/dist/lib/scripts/transform.d.ts +64 -0
  43. package/dist/lib/scripts/transform.d.ts.map +1 -0
  44. package/dist/lib/scripts/transform.js +373 -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 +62 -0
  48. package/dist/lib/styles/compiler.d.ts +52 -0
  49. package/dist/lib/styles/compiler.d.ts.map +1 -0
  50. package/dist/lib/styles/compiler.js +272 -0
  51. package/dist/lib/styles/emit.d.ts +25 -0
  52. package/dist/lib/styles/emit.d.ts.map +1 -0
  53. package/dist/lib/styles/emit.js +78 -0
  54. package/dist/lib/styles/resolve.d.ts +48 -0
  55. package/dist/lib/styles/resolve.d.ts.map +1 -0
  56. package/dist/lib/styles/resolve.js +188 -0
  57. package/dist/lib/styles/transform.d.ts +47 -0
  58. package/dist/lib/styles/transform.d.ts.map +1 -0
  59. package/dist/lib/styles/transform.js +131 -0
  60. package/dist/lib/target.d.ts +21 -0
  61. package/dist/lib/target.d.ts.map +1 -0
  62. package/dist/lib/target.js +127 -0
  63. package/dist/lib/watch.d.ts +22 -0
  64. package/dist/lib/watch.d.ts.map +1 -0
  65. package/dist/lib/watch.js +96 -0
  66. package/package.json +55 -12
  67. package/src/assets.ts +2 -0
  68. package/src/lib/access.ts +24 -0
  69. package/src/lib/asset-server.ts +537 -0
  70. package/src/lib/compilation-error.ts +61 -0
  71. package/src/lib/file-matcher.ts +63 -0
  72. package/src/lib/fingerprint.ts +65 -0
  73. package/src/lib/module-store.ts +340 -0
  74. package/src/lib/paths.ts +66 -0
  75. package/src/lib/routes.ts +164 -0
  76. package/src/lib/scripts/cjs-check.ts +476 -0
  77. package/src/lib/scripts/compiler.ts +640 -0
  78. package/src/lib/scripts/emit.ts +122 -0
  79. package/src/lib/scripts/resolve.ts +433 -0
  80. package/src/lib/scripts/transform.ts +609 -0
  81. package/src/lib/source-maps.ts +75 -0
  82. package/src/lib/styles/compiler.ts +400 -0
  83. package/src/lib/styles/emit.ts +137 -0
  84. package/src/lib/styles/resolve.ts +316 -0
  85. package/src/lib/styles/transform.ts +226 -0
  86. package/src/lib/target.ts +196 -0
  87. package/src/lib/watch.ts +136 -0
package/package.json CHANGED
@@ -1,21 +1,64 @@
1
1
  {
2
2
  "name": "@remix-run/assets",
3
- "version": "0.0.0",
4
- "description": "Placeholder package for Remix CI/OIDC setup",
5
- "main": "index.js",
6
- "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1"
8
- },
9
- "keywords": [],
10
- "author": "",
3
+ "version": "0.2.0",
4
+ "description": "Fetch-based server for compiling browser JS/TS and CSS assets on demand",
5
+ "author": "Michael Jackson <mjijackson@gmail.com>",
11
6
  "license": "MIT",
12
- "type": "commonjs",
13
7
  "repository": {
14
8
  "type": "git",
15
9
  "url": "git+https://github.com/remix-run/remix.git",
16
10
  "directory": "packages/assets"
17
11
  },
18
- "publishConfig": {
19
- "access": "public"
12
+ "homepage": "https://github.com/remix-run/remix/tree/main/packages/assets#readme",
13
+ "files": [
14
+ "LICENSE",
15
+ "README.md",
16
+ "dist",
17
+ "src",
18
+ "!src/**/*.test.ts"
19
+ ],
20
+ "type": "module",
21
+ "exports": {
22
+ ".": {
23
+ "types": "./dist/assets.d.ts",
24
+ "default": "./dist/assets.js"
25
+ },
26
+ "./package.json": "./package.json"
27
+ },
28
+ "dependencies": {
29
+ "chokidar": "^5.0.0",
30
+ "es-module-lexer": "^2.0.0",
31
+ "get-tsconfig": "^4.13.6",
32
+ "lightningcss": "^1.32.0",
33
+ "magic-string": "^0.30.21",
34
+ "oxc-minify": "^0.121.0",
35
+ "oxc-parser": "^0.121.0",
36
+ "oxc-resolver": "^11.19.1",
37
+ "oxc-transform": "^0.121.0",
38
+ "picomatch": "^4.0.4",
39
+ "source-map-js": "^1.2.1",
40
+ "@remix-run/headers": "0.19.0",
41
+ "@remix-run/route-pattern": "0.20.1"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "^24.6.0",
45
+ "@types/picomatch": "^4.0.3",
46
+ "@typescript/native-preview": "7.0.0-dev.20251125.1"
47
+ },
48
+ "keywords": [
49
+ "remix",
50
+ "scripts",
51
+ "styles",
52
+ "css",
53
+ "assets",
54
+ "transform",
55
+ "server"
56
+ ],
57
+ "scripts": {
58
+ "bench": "pnpm --dir ./bench run bench",
59
+ "build": "tsgo -p tsconfig.build.json",
60
+ "clean": "git clean -fdX",
61
+ "test": "node --disable-warning=ExperimentalWarning --test './src/**/*.test.ts'",
62
+ "typecheck": "tsgo --noEmit"
20
63
  }
21
- }
64
+ }
package/src/assets.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { createAssetServer } from './lib/asset-server.ts'
2
+ export type { AssetServer, AssetServerOptions } from './lib/asset-server.ts'
@@ -0,0 +1,24 @@
1
+ import { createFileMatcher } from './file-matcher.ts'
2
+
3
+ type AccessPolicy = {
4
+ isAllowed(filePath: string): boolean
5
+ }
6
+
7
+ export function createAccessPolicy(options: {
8
+ allow: readonly string[]
9
+ deny?: readonly string[]
10
+ rootDir: string
11
+ }): AccessPolicy {
12
+ let allowMatchers = options.allow.map((pattern) => createFileMatcher(pattern, options.rootDir))
13
+ let denyMatchers = (options.deny ?? []).map((pattern) =>
14
+ createFileMatcher(pattern, options.rootDir),
15
+ )
16
+
17
+ return {
18
+ isAllowed(filePath) {
19
+ if (!allowMatchers.some((matcher) => matcher(filePath))) return false
20
+ if (denyMatchers.length > 0 && denyMatchers.some((matcher) => matcher(filePath))) return false
21
+ return true
22
+ },
23
+ }
24
+ }
@@ -0,0 +1,537 @@
1
+ import * as path from 'node:path'
2
+ import * as fs from 'node:fs'
3
+ import { createAccessPolicy } from './access.ts'
4
+ import { isAssetServerCompilationError } from './compilation-error.ts'
5
+ import { getFingerprintRequestCacheControl, parseFingerprintSuffix } from './fingerprint.ts'
6
+ import { normalizeFilePath } from './paths.ts'
7
+ import { compileRoutes } from './routes.ts'
8
+ import type { CompiledRoutes } from './routes.ts'
9
+ import { createResponseForScript, createScriptCompiler } from './scripts/compiler.ts'
10
+ import { supportedScriptExtensions } from './scripts/resolve.ts'
11
+ import { createResponseForStyle, createStyleCompiler, isStyleFilePath } from './styles/compiler.ts'
12
+ import { resolveScriptTarget, resolveStyleTarget } from './target.ts'
13
+ import type { AssetTarget, ResolvedScriptTarget, ResolvedStyleTarget } from './target.ts'
14
+ import { createAssetServerWatcher } from './watch.ts'
15
+ import type { AssetServerWatcher, ChokidarWatcher } from './watch.ts'
16
+
17
+ interface AssetServerWatchOptions {
18
+ /**
19
+ * Ignore matching glob patterns or file paths. Relative values are resolved
20
+ * from `rootDir`.
21
+ */
22
+ ignore?: readonly string[]
23
+ /**
24
+ * Use polling instead of native filesystem events. Defaults to `false`.
25
+ */
26
+ poll?: boolean
27
+ /**
28
+ * Polling interval in milliseconds when `poll` is enabled. Defaults to `100`.
29
+ */
30
+ pollInterval?: number
31
+ }
32
+
33
+ interface FingerprintOptions {
34
+ /**
35
+ * Per-build invalidation token that must change whenever fingerprinted asset URLs
36
+ * should be invalidated together.
37
+ */
38
+ buildId: string
39
+ }
40
+
41
+ type AssetSourceMaps = 'inline' | 'external'
42
+ type AssetSourceMapSourcePaths = 'url' | 'absolute'
43
+
44
+ interface AssetServerScriptOptions {
45
+ /**
46
+ * Replace global expressions with constant values during transform, e.g.
47
+ * `{ 'process.env.NODE_ENV': '"production"' }`
48
+ */
49
+ define?: Record<string, string>
50
+ /** Import specifiers to leave unrewritten (CDN URLs, import map entries, etc.) */
51
+ external?: string[]
52
+ }
53
+
54
+ const scriptExtensionSet = new Set<string>(supportedScriptExtensions)
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 asset URLs.
73
+ *
74
+ * When omitted, all served assets 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
+ * Shared compatibility target for scripts and styles. Browser targets apply to both
80
+ * pipelines, and `es` only affects scripts.
81
+ */
82
+ target?: AssetTarget
83
+ /**
84
+ * Source map mode for scripts and styles.
85
+ * - `'external'`: serve source maps as separate `.map` files
86
+ * - `'inline'`: embed source maps as a base64 data URL in the compiled asset
87
+ */
88
+ sourceMaps?: AssetSourceMaps
89
+ /**
90
+ * Source path strategy for source map `sources`.
91
+ * - `'url'` (default): use the stable server path (e.g. `'/assets/app/entry.ts'`)
92
+ * - `'absolute'`: use the original filesystem path on disk
93
+ */
94
+ sourceMapSourcePaths?: AssetSourceMapSourcePaths
95
+ /**
96
+ * Minification setting for emitted scripts and styles.
97
+ */
98
+ minify?: boolean
99
+ /**
100
+ * Script-only configuration.
101
+ */
102
+ scripts?: AssetServerScriptOptions
103
+ /**
104
+ * Enable filesystem-backed cache invalidation for long-lived server instances.
105
+ * Enabled by default. Pass `true` to use the default watcher options, an options
106
+ * object to customize watcher behavior, or `false` to disable watching.
107
+ */
108
+ watch?: boolean | AssetServerWatchOptions
109
+ /**
110
+ * Handles unexpected request-time compilation errors. Return a `Response` to override the
111
+ * default `500 Internal Server Error` response, or return nothing to use the default.
112
+ */
113
+ onError?: (error: unknown) => void | Response | Promise<void | Response>
114
+ }
115
+
116
+ export interface AssetServer {
117
+ /**
118
+ * Serves a script or style request. Returns `Response | null` — null means the request
119
+ * was not handled by this server, letting the router fall through to a 404.
120
+ */
121
+ fetch(request: Request): Promise<Response | null>
122
+ /**
123
+ * Returns the request href for a served asset file.
124
+ */
125
+ getHref(filePath: string): Promise<string>
126
+ /**
127
+ * Returns preload URLs for one or more served asset files, ordered shallowest-first.
128
+ */
129
+ getPreloads(filePath: string | readonly string[]): Promise<string[]>
130
+ /**
131
+ * Closes any watcher resources owned by this server instance.
132
+ */
133
+ close(): Promise<void>
134
+ }
135
+
136
+ type ResolvedAssetServerOptions = {
137
+ allow: readonly string[]
138
+ buildId?: string
139
+ define?: Record<string, string>
140
+ deny?: readonly string[]
141
+ external: string[]
142
+ fingerprintAssets: boolean
143
+ minify: boolean
144
+ onError: NonNullable<AssetServerOptions['onError']>
145
+ rootDir: string
146
+ routes: CompiledRoutes
147
+ sourceMapSourcePaths: 'url' | 'absolute'
148
+ sourceMaps?: 'inline' | 'external'
149
+ scriptsTarget?: ResolvedScriptTarget
150
+ stylesTarget?: ResolvedStyleTarget
151
+ watchOptions: AssetServerWatchOptions | null
152
+ }
153
+
154
+ const chokidarWatcherByAssetServer = new WeakMap<AssetServer, ChokidarWatcher>()
155
+ const watcherByAssetServer = new WeakMap<AssetServer, AssetServerWatcher>()
156
+
157
+ export function getInternalChokidarWatcher(assetServer: AssetServer): ChokidarWatcher | undefined {
158
+ return chokidarWatcherByAssetServer.get(assetServer)
159
+ }
160
+
161
+ export function getInternalWatchTargets(assetServer: AssetServer): readonly string[] {
162
+ return watcherByAssetServer.get(assetServer)?.getWatchedTargets() ?? []
163
+ }
164
+
165
+ /**
166
+ * Create an asset server instance
167
+ *
168
+ * Compiles TypeScript/JavaScript scripts and CSS styles on demand with optional
169
+ * source-based URL fingerprinting, caching, and configurable file mapping.
170
+ *
171
+ * @param options Server configuration
172
+ * @returns A {@link AssetServer} with `fetch()`, `getHref()`, and `getPreloads()` methods
173
+ *
174
+ * @example
175
+ * ```ts
176
+ * let assetServer = createAssetServer({
177
+ * fileMap: {
178
+ * '/assets/app/*path': 'app/*path',
179
+ * },
180
+ * allow: ['app/**'],
181
+ * })
182
+ *
183
+ * route('/assets/*path', ({ request }) => assetServer.fetch(request))
184
+ * ```
185
+ */
186
+ export function createAssetServer(options: AssetServerOptions): AssetServer {
187
+ let resolvedOptions = resolveAssetServerOptions(options)
188
+ let accessPolicy = createAccessPolicy({
189
+ allow: resolvedOptions.allow,
190
+ deny: resolvedOptions.deny,
191
+ rootDir: resolvedOptions.rootDir,
192
+ })
193
+ let watcher: AssetServerWatcher | null = null
194
+ let chokidarWatcher: ChokidarWatcher | null = null
195
+ let scriptCompiler = createScriptCompiler({
196
+ buildId: resolvedOptions.buildId,
197
+ define: resolvedOptions.define,
198
+ external: resolvedOptions.external,
199
+ fingerprintAssets: resolvedOptions.fingerprintAssets,
200
+ isAllowed: accessPolicy.isAllowed,
201
+ minify: resolvedOptions.minify,
202
+ onWatchDirectoriesChange: (delta) => {
203
+ if (!watcher) return
204
+ watcher.updateWatchedDirectories(delta)
205
+ },
206
+ rootDir: resolvedOptions.rootDir,
207
+ routes: resolvedOptions.routes,
208
+ sourceMapSourcePaths: resolvedOptions.sourceMapSourcePaths,
209
+ sourceMaps: resolvedOptions.sourceMaps,
210
+ target: resolvedOptions.scriptsTarget,
211
+ watchIgnore: resolvedOptions.watchOptions?.ignore,
212
+ watchMode: resolvedOptions.watchOptions !== null,
213
+ })
214
+ let styleCompiler = createStyleCompiler({
215
+ buildId: resolvedOptions.buildId,
216
+ fingerprintAssets: resolvedOptions.fingerprintAssets,
217
+ isAllowed: accessPolicy.isAllowed,
218
+ minify: resolvedOptions.minify,
219
+ onWatchDirectoriesChange: (delta) => {
220
+ if (!watcher) return
221
+ watcher.updateWatchedDirectories(delta)
222
+ },
223
+ rootDir: resolvedOptions.rootDir,
224
+ routes: resolvedOptions.routes,
225
+ sourceMapSourcePaths: resolvedOptions.sourceMapSourcePaths,
226
+ sourceMaps: resolvedOptions.sourceMaps,
227
+ targets: resolvedOptions.stylesTarget,
228
+ watchIgnore: resolvedOptions.watchOptions?.ignore,
229
+ })
230
+ if (resolvedOptions.watchOptions) {
231
+ watcher = createAssetServerWatcher({
232
+ ...resolvedOptions.watchOptions,
233
+ onChokidarWatcherCreated(createdWatcher) {
234
+ chokidarWatcher = createdWatcher
235
+ },
236
+ onFileEvent: handleWatchEvent,
237
+ rootDir: resolvedOptions.rootDir,
238
+ })
239
+ }
240
+
241
+ async function responseForError(error: unknown): Promise<Response> {
242
+ try {
243
+ return (await resolvedOptions.onError(error)) ?? internalServerError()
244
+ } catch (error) {
245
+ console.error(`There was an error in the asset server error handler: ${error}`)
246
+ return internalServerError()
247
+ }
248
+ }
249
+
250
+ async function handleWatchEvent(filePath: string, event: 'add' | 'change' | 'unlink') {
251
+ try {
252
+ let normalizedFilePath = normalizeFilePath(filePath)
253
+ await scriptCompiler.handleFileEvent(normalizedFilePath, event)
254
+ await styleCompiler.handleFileEvent(normalizedFilePath, event)
255
+ } catch (error) {
256
+ console.error(`There was an error invalidating the asset server cache: ${error}`)
257
+ }
258
+ }
259
+
260
+ let assetServer: AssetServer = {
261
+ async fetch(request) {
262
+ if (request.method !== 'GET' && request.method !== 'HEAD') return null
263
+
264
+ let parsedRequestPathname = parseAssetRequestPathname(new URL(request.url).pathname, {
265
+ fingerprintAssets: resolvedOptions.fingerprintAssets,
266
+ routes: resolvedOptions.routes,
267
+ })
268
+ if (!parsedRequestPathname) return null
269
+
270
+ try {
271
+ let ifNoneMatch = request.headers.get('If-None-Match')
272
+
273
+ if (isStyleFilePath(parsedRequestPathname.filePath)) {
274
+ let styleResult = await styleCompiler.getStyle(parsedRequestPathname.filePath, {
275
+ ifNoneMatch,
276
+ isSourceMapRequest: parsedRequestPathname.isSourceMapRequest,
277
+ requestedFingerprint: parsedRequestPathname.requestedFingerprint,
278
+ })
279
+ if (styleResult.type === 'not-modified') {
280
+ return new Response(null, {
281
+ status: 304,
282
+ headers: { ETag: styleResult.etag },
283
+ })
284
+ }
285
+
286
+ let compiledStyle = styleResult.style
287
+
288
+ if (parsedRequestPathname.requestedFingerprint !== null) {
289
+ if (compiledStyle.fingerprint !== parsedRequestPathname.requestedFingerprint)
290
+ return null
291
+ }
292
+
293
+ return createResponseForStyle(compiledStyle, {
294
+ cacheControl: parsedRequestPathname.cacheControl,
295
+ ifNoneMatch,
296
+ isSourceMapRequest: parsedRequestPathname.isSourceMapRequest,
297
+ method: request.method,
298
+ })
299
+ }
300
+
301
+ if (!isScriptFilePath(parsedRequestPathname.filePath)) {
302
+ return null
303
+ }
304
+
305
+ let scriptResult = await scriptCompiler.getScript(parsedRequestPathname.filePath, {
306
+ ifNoneMatch,
307
+ isSourceMapRequest: parsedRequestPathname.isSourceMapRequest,
308
+ requestedFingerprint: parsedRequestPathname.requestedFingerprint,
309
+ })
310
+ if (scriptResult.type === 'not-modified') {
311
+ return new Response(null, {
312
+ status: 304,
313
+ headers: { ETag: scriptResult.etag },
314
+ })
315
+ }
316
+
317
+ let compiledScript = scriptResult.script
318
+
319
+ if (parsedRequestPathname.requestedFingerprint !== null) {
320
+ if (compiledScript.fingerprint !== parsedRequestPathname.requestedFingerprint) return null
321
+ }
322
+
323
+ return createResponseForScript(compiledScript, {
324
+ cacheControl: parsedRequestPathname.cacheControl,
325
+ ifNoneMatch,
326
+ isSourceMapRequest: parsedRequestPathname.isSourceMapRequest,
327
+ method: request.method,
328
+ })
329
+ } catch (error) {
330
+ // A direct request can race with the filesystem or fail a deeper allow check while
331
+ // compiling imports. In this fetch context, both cases should fall through as "not
332
+ // handled here" so the outer router can continue to its own 404 behavior.
333
+ if (
334
+ isAssetServerCompilationError(error) &&
335
+ (error.code === 'FILE_NOT_FOUND' || error.code === 'FILE_NOT_ALLOWED')
336
+ ) {
337
+ return null
338
+ }
339
+
340
+ return responseForError(error)
341
+ }
342
+ },
343
+
344
+ async getHref(filePath) {
345
+ if (isStyleFilePath(filePath)) {
346
+ return styleCompiler.getHref(filePath)
347
+ }
348
+
349
+ return scriptCompiler.getHref(filePath)
350
+ },
351
+ async getPreloads(filePath) {
352
+ let filePaths = Array.isArray(filePath) ? filePath : [filePath]
353
+ let styleFiles: string[] = []
354
+ let scriptFiles: string[] = []
355
+
356
+ for (let nextFilePath of filePaths) {
357
+ if (isStyleFilePath(nextFilePath)) {
358
+ styleFiles.push(nextFilePath)
359
+ continue
360
+ }
361
+
362
+ scriptFiles.push(nextFilePath)
363
+ }
364
+
365
+ if (styleFiles.length === 0 && scriptFiles.length === 0) {
366
+ return []
367
+ }
368
+
369
+ if (styleFiles.length === 0) {
370
+ return flattenPreloadLayers(await scriptCompiler.getPreloadLayers(filePath))
371
+ }
372
+
373
+ if (scriptFiles.length === 0) {
374
+ return flattenPreloadLayers(await styleCompiler.getPreloadLayers(filePath))
375
+ }
376
+
377
+ // Mixed asset type preloads need to be merged, so we merge in order of first asset type seen
378
+ let scriptPreloadLayersPromise = scriptCompiler.getPreloadLayers(scriptFiles)
379
+ let stylePreloadLayersPromise = styleCompiler.getPreloadLayers(styleFiles)
380
+ let preloadLayerGroups = isStyleFilePath(filePaths[0])
381
+ ? [stylePreloadLayersPromise, scriptPreloadLayersPromise]
382
+ : [scriptPreloadLayersPromise, stylePreloadLayersPromise]
383
+
384
+ return mergePreloadLayers(await Promise.all(preloadLayerGroups))
385
+ },
386
+ async close() {
387
+ await watcher?.close()
388
+ },
389
+ }
390
+
391
+ if (chokidarWatcher) {
392
+ chokidarWatcherByAssetServer.set(assetServer, chokidarWatcher)
393
+ }
394
+ if (watcher) {
395
+ watcherByAssetServer.set(assetServer, watcher)
396
+ }
397
+
398
+ return assetServer
399
+ }
400
+
401
+ function internalServerError(): Response {
402
+ return new Response('Internal Server Error', {
403
+ status: 500,
404
+ headers: { 'Content-Type': 'text/plain; charset=utf-8' },
405
+ })
406
+ }
407
+
408
+ function mergePreloadLayers(preloadLayersByRoot: readonly (readonly string[][])[]): string[] {
409
+ let urls: string[] = []
410
+ let seen = new Set<string>()
411
+ let maxDepth = Math.max(0, ...preloadLayersByRoot.map((layers) => layers.length))
412
+
413
+ for (let depth = 0; depth < maxDepth; depth++) {
414
+ for (let preloadLayers of preloadLayersByRoot) {
415
+ for (let url of preloadLayers[depth] ?? []) {
416
+ if (seen.has(url)) continue
417
+ seen.add(url)
418
+ urls.push(url)
419
+ }
420
+ }
421
+ }
422
+
423
+ return urls
424
+ }
425
+
426
+ function flattenPreloadLayers(preloadLayers: readonly (readonly string[])[]): string[] {
427
+ return preloadLayers.flatMap((layer) => layer)
428
+ }
429
+
430
+ function defaultErrorHandler(error: unknown): void {
431
+ console.error(error)
432
+ }
433
+
434
+ function resolveAssetServerOptions(options: AssetServerOptions): ResolvedAssetServerOptions {
435
+ let rootDir = normalizeFilePath(fs.realpathSync(path.resolve(options.rootDir ?? process.cwd())))
436
+ let scriptOptions = options.scripts ?? {}
437
+ let fingerprintOptions = normalizeFingerprintOptions({
438
+ fingerprint: options.fingerprint,
439
+ watch: options.watch,
440
+ })
441
+
442
+ return {
443
+ allow: options.allow,
444
+ buildId: fingerprintOptions.buildId,
445
+ define: scriptOptions.define,
446
+ deny: options.deny,
447
+ external: scriptOptions.external ?? [],
448
+ fingerprintAssets: fingerprintOptions.enabled,
449
+ minify: options.minify ?? false,
450
+ onError: options.onError ?? defaultErrorHandler,
451
+ rootDir,
452
+ routes: compileRoutes({
453
+ fileMap: options.fileMap,
454
+ rootDir,
455
+ }),
456
+ sourceMapSourcePaths: options.sourceMapSourcePaths ?? 'url',
457
+ sourceMaps: options.sourceMaps,
458
+ scriptsTarget: resolveScriptTarget(options.target),
459
+ stylesTarget: resolveStyleTarget(options.target),
460
+ watchOptions: normalizeWatchOptions(options.watch),
461
+ }
462
+ }
463
+
464
+ function normalizeFingerprintOptions(options: {
465
+ fingerprint: AssetServerOptions['fingerprint']
466
+ watch: AssetServerOptions['watch']
467
+ }):
468
+ | {
469
+ enabled: false
470
+ buildId?: string
471
+ }
472
+ | {
473
+ enabled: true
474
+ buildId: string
475
+ } {
476
+ if (!options.fingerprint) {
477
+ return {
478
+ enabled: false,
479
+ }
480
+ }
481
+
482
+ if (typeof options.fingerprint.buildId !== 'string') {
483
+ throw new TypeError('fingerprint.buildId must be a string')
484
+ }
485
+
486
+ if (options.fingerprint.buildId.length === 0) {
487
+ throw new TypeError('fingerprint.buildId must be a non-empty string')
488
+ }
489
+
490
+ if (options.watch !== false) {
491
+ throw new TypeError('fingerprint cannot be used with watch mode')
492
+ }
493
+
494
+ return {
495
+ enabled: true,
496
+ buildId: options.fingerprint.buildId,
497
+ }
498
+ }
499
+
500
+ function normalizeWatchOptions(
501
+ options: AssetServerOptions['watch'],
502
+ ): AssetServerWatchOptions | null {
503
+ if (options === false) return null
504
+ if (options == null || options === true) return {}
505
+ return options
506
+ }
507
+
508
+ function parseAssetRequestPathname(
509
+ pathname: string,
510
+ options: {
511
+ fingerprintAssets: boolean
512
+ routes: CompiledRoutes
513
+ },
514
+ ): {
515
+ cacheControl: string
516
+ filePath: string
517
+ isSourceMapRequest: boolean
518
+ requestedFingerprint: string | null
519
+ } | null {
520
+ let isSourceMapRequest = pathname.endsWith('.map')
521
+ let pathWithoutMap = isSourceMapRequest ? pathname.slice(0, -4) : pathname
522
+ let fingerprint = parseFingerprintSuffix(pathWithoutMap)
523
+ let filePath = options.routes.resolveUrlPathname(fingerprint.pathname)
524
+ if (!filePath) return null
525
+ if (options.fingerprintAssets && fingerprint.requestedFingerprint === null) return null
526
+
527
+ return {
528
+ cacheControl: getFingerprintRequestCacheControl(fingerprint.requestedFingerprint),
529
+ filePath,
530
+ isSourceMapRequest,
531
+ requestedFingerprint: fingerprint.requestedFingerprint,
532
+ }
533
+ }
534
+
535
+ function isScriptFilePath(filePath: string): boolean {
536
+ return scriptExtensionSet.has(path.extname(filePath).toLowerCase())
537
+ }
@@ -0,0 +1,61 @@
1
+ type AssetServerCompilationErrorCode =
2
+ | 'FILE_NOT_FOUND'
3
+ | 'FILE_NOT_ALLOWED'
4
+ | 'FILE_OUTSIDE_FILE_MAP'
5
+ | 'COMMONJS_NOT_SUPPORTED'
6
+ | 'TRANSFORM_FAILED'
7
+ | '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 asset 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
+ }