@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.
- package/LICENSE +21 -0
- package/README.md +325 -2
- package/dist/assets.d.ts +3 -0
- package/dist/assets.d.ts.map +1 -0
- package/dist/assets.js +1 -0
- package/dist/lib/access.d.ts +10 -0
- package/dist/lib/access.d.ts.map +1 -0
- package/dist/lib/access.js +14 -0
- package/dist/lib/asset-server.d.ts +139 -0
- package/dist/lib/asset-server.d.ts.map +1 -0
- package/dist/lib/asset-server.js +338 -0
- package/dist/lib/compilation-error.d.ts +33 -0
- package/dist/lib/compilation-error.d.ts.map +1 -0
- package/dist/lib/compilation-error.js +32 -0
- package/dist/lib/file-matcher.d.ts +6 -0
- package/dist/lib/file-matcher.d.ts.map +1 -0
- package/dist/lib/file-matcher.js +44 -0
- package/dist/lib/fingerprint.d.ts +12 -0
- package/dist/lib/fingerprint.d.ts.map +1 -0
- package/dist/lib/fingerprint.js +49 -0
- package/dist/lib/module-store.d.ts +41 -0
- package/dist/lib/module-store.d.ts.map +1 -0
- package/dist/lib/module-store.js +230 -0
- package/dist/lib/paths.d.ts +8 -0
- package/dist/lib/paths.d.ts.map +1 -0
- package/dist/lib/paths.js +50 -0
- package/dist/lib/routes.d.ts +13 -0
- package/dist/lib/routes.d.ts.map +1 -0
- package/dist/lib/routes.js +94 -0
- package/dist/lib/scripts/cjs-check.d.ts +3 -0
- package/dist/lib/scripts/cjs-check.d.ts.map +1 -0
- package/dist/lib/scripts/cjs-check.js +398 -0
- package/dist/lib/scripts/compiler.d.ts +62 -0
- package/dist/lib/scripts/compiler.d.ts.map +1 -0
- package/dist/lib/scripts/compiler.js +439 -0
- package/dist/lib/scripts/emit.d.ts +25 -0
- package/dist/lib/scripts/emit.d.ts.map +1 -0
- package/dist/lib/scripts/emit.js +63 -0
- package/dist/lib/scripts/resolve.d.ts +50 -0
- package/dist/lib/scripts/resolve.d.ts.map +1 -0
- package/dist/lib/scripts/resolve.js +236 -0
- package/dist/lib/scripts/transform.d.ts +64 -0
- package/dist/lib/scripts/transform.d.ts.map +1 -0
- package/dist/lib/scripts/transform.js +373 -0
- package/dist/lib/source-maps.d.ts +4 -0
- package/dist/lib/source-maps.d.ts.map +1 -0
- package/dist/lib/source-maps.js +62 -0
- package/dist/lib/styles/compiler.d.ts +52 -0
- package/dist/lib/styles/compiler.d.ts.map +1 -0
- package/dist/lib/styles/compiler.js +272 -0
- package/dist/lib/styles/emit.d.ts +25 -0
- package/dist/lib/styles/emit.d.ts.map +1 -0
- package/dist/lib/styles/emit.js +78 -0
- package/dist/lib/styles/resolve.d.ts +48 -0
- package/dist/lib/styles/resolve.d.ts.map +1 -0
- package/dist/lib/styles/resolve.js +188 -0
- package/dist/lib/styles/transform.d.ts +47 -0
- package/dist/lib/styles/transform.d.ts.map +1 -0
- package/dist/lib/styles/transform.js +131 -0
- package/dist/lib/target.d.ts +21 -0
- package/dist/lib/target.d.ts.map +1 -0
- package/dist/lib/target.js +127 -0
- package/dist/lib/watch.d.ts +22 -0
- package/dist/lib/watch.d.ts.map +1 -0
- package/dist/lib/watch.js +96 -0
- package/package.json +55 -12
- package/src/assets.ts +2 -0
- package/src/lib/access.ts +24 -0
- package/src/lib/asset-server.ts +537 -0
- package/src/lib/compilation-error.ts +61 -0
- package/src/lib/file-matcher.ts +63 -0
- package/src/lib/fingerprint.ts +65 -0
- package/src/lib/module-store.ts +340 -0
- package/src/lib/paths.ts +66 -0
- package/src/lib/routes.ts +164 -0
- package/src/lib/scripts/cjs-check.ts +476 -0
- package/src/lib/scripts/compiler.ts +640 -0
- package/src/lib/scripts/emit.ts +122 -0
- package/src/lib/scripts/resolve.ts +433 -0
- package/src/lib/scripts/transform.ts +609 -0
- package/src/lib/source-maps.ts +75 -0
- package/src/lib/styles/compiler.ts +400 -0
- package/src/lib/styles/emit.ts +137 -0
- package/src/lib/styles/resolve.ts +316 -0
- package/src/lib/styles/transform.ts +226 -0
- package/src/lib/target.ts +196 -0
- 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.
|
|
4
|
-
"description": "
|
|
5
|
-
"
|
|
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
|
-
"
|
|
19
|
-
|
|
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,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
|
+
}
|