@remix-run/assets 0.1.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/README.md +53 -47
- package/dist/lib/asset-server.d.ts +45 -43
- package/dist/lib/asset-server.d.ts.map +1 -1
- package/dist/lib/asset-server.js +142 -46
- package/dist/lib/compilation-error.d.ts +2 -2
- package/dist/lib/compilation-error.d.ts.map +1 -1
- package/dist/lib/compilation-error.js +1 -1
- package/dist/lib/file-matcher.d.ts.map +1 -1
- package/dist/lib/file-matcher.js +3 -2
- package/dist/lib/module-store.d.ts +41 -0
- package/dist/lib/module-store.d.ts.map +1 -0
- package/dist/lib/{scripts/store.js → module-store.js} +43 -41
- package/dist/lib/scripts/compiler.d.ts +15 -15
- package/dist/lib/scripts/compiler.d.ts.map +1 -1
- package/dist/lib/scripts/compiler.js +50 -46
- package/dist/lib/scripts/emit.js +2 -2
- package/dist/lib/scripts/resolve.d.ts +7 -17
- package/dist/lib/scripts/resolve.d.ts.map +1 -1
- package/dist/lib/scripts/resolve.js +15 -9
- package/dist/lib/scripts/transform.d.ts +11 -9
- package/dist/lib/scripts/transform.d.ts.map +1 -1
- package/dist/lib/scripts/transform.js +32 -21
- package/dist/lib/source-maps.d.ts +1 -1
- package/dist/lib/source-maps.d.ts.map +1 -1
- package/dist/lib/source-maps.js +7 -1
- 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/package.json +7 -2
- package/src/lib/asset-server.ts +218 -96
- package/src/lib/compilation-error.ts +7 -7
- package/src/lib/file-matcher.ts +3 -2
- package/src/lib/{scripts/store.ts → module-store.ts} +100 -87
- package/src/lib/scripts/compiler.ts +88 -70
- package/src/lib/scripts/emit.ts +2 -2
- package/src/lib/scripts/resolve.ts +34 -23
- package/src/lib/scripts/transform.ts +49 -34
- package/src/lib/source-maps.ts +8 -1
- 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/dist/lib/scripts/store.d.ts +0 -40
- package/dist/lib/scripts/store.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# assets
|
|
2
2
|
|
|
3
|
-
Fetch-based server for compiling browser JS/TS assets on demand.
|
|
3
|
+
Fetch-based server for compiling browser JS/TS and CSS assets on demand.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
- **On-Demand Compilation** - Compile browser
|
|
7
|
+
- **On-Demand Compilation** - Compile browser scripts and styles on demand
|
|
8
8
|
- **Custom File Mapping** - Define patterns for mapping public URLs to file paths on disk
|
|
9
9
|
- **Access Control** - Control exactly which files can be served with allow and deny rules
|
|
10
|
-
- **Preloads** - Generate
|
|
10
|
+
- **Preloads** - Generate preload URLs for scripts and styles based on imports
|
|
11
11
|
- **Caching** - Conservative caching by default with stable URLs, ETags, and revalidation
|
|
12
12
|
- **Optional Fingerprinting** - Source-based fingerprinted URLs for long-lived browser caching
|
|
13
13
|
- **Source Maps** - Serve inline or external sourcemaps
|
|
@@ -20,7 +20,7 @@ npm i remix
|
|
|
20
20
|
|
|
21
21
|
## Usage
|
|
22
22
|
|
|
23
|
-
Use `createAssetServer` to serve browser
|
|
23
|
+
Use `createAssetServer` to serve browser JS/TS and CSS assets from a URL namespace in your app.
|
|
24
24
|
|
|
25
25
|
```ts
|
|
26
26
|
import { createRouter } from 'remix/fetch-router'
|
|
@@ -41,7 +41,7 @@ router.get('/assets/*', ({ request }) => {
|
|
|
41
41
|
})
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
-
This example gives you an `/assets/*` endpoint that serves compiled browser
|
|
44
|
+
This example gives you an `/assets/*` endpoint that serves compiled browser assets from `app/assets` and `node_modules`.
|
|
45
45
|
|
|
46
46
|
## Root Directory
|
|
47
47
|
|
|
@@ -151,7 +151,7 @@ let src = await assetServer.getHref('app/assets/entry.tsx')
|
|
|
151
151
|
|
|
152
152
|
## Preloads
|
|
153
153
|
|
|
154
|
-
Use `assetServer.getPreloads()` when rendering HTML so you can turn the returned URLs into `<link rel="modulepreload"
|
|
154
|
+
Use `assetServer.getPreloads()` when rendering HTML so you can turn the returned URLs into `<link rel="modulepreload">`, stylesheet preload tags, or `Link` headers for one or more assets and their dependencies. You can provide root-relative or absolute file paths, or `file://` URLs.
|
|
155
155
|
|
|
156
156
|
```ts
|
|
157
157
|
let preloads = await assetServer.getPreloads(['app/assets/entry.tsx', 'app/assets/search.tsx'])
|
|
@@ -159,16 +159,16 @@ let preloads = await assetServer.getPreloads(['app/assets/entry.tsx', 'app/asset
|
|
|
159
159
|
// '/assets/app/assets/entry.tsx',
|
|
160
160
|
// '/assets/app/assets/search.tsx',
|
|
161
161
|
// '/assets/app/assets/utils.ts',
|
|
162
|
-
// '/assets/npm/@remix-run/
|
|
162
|
+
// '/assets/npm/@remix-run/ui/index.js',
|
|
163
163
|
// ...etc
|
|
164
164
|
// ]
|
|
165
165
|
```
|
|
166
166
|
|
|
167
167
|
## Fingerprinting
|
|
168
168
|
|
|
169
|
-
By default,
|
|
169
|
+
By default, assets are served at stable URLs with ETags and `Cache-Control: no-cache`. Responses are cached for the lifetime of the asset server instance.
|
|
170
170
|
|
|
171
|
-
If you want clients to cache
|
|
171
|
+
If you want clients to cache assets aggressively without revalidation, you can opt into source-based fingerprinting.
|
|
172
172
|
|
|
173
173
|
```ts
|
|
174
174
|
import { createAssetServer } from 'remix/assets'
|
|
@@ -183,15 +183,13 @@ let assetServer = createAssetServer({
|
|
|
183
183
|
})
|
|
184
184
|
```
|
|
185
185
|
|
|
186
|
-
When fingerprinting is enabled,
|
|
186
|
+
When fingerprinting is enabled, assets use a `.@<fingerprint>` segment before the file extension and are served with `Cache-Control: public, max-age=31536000, immutable`.
|
|
187
187
|
|
|
188
|
-
Source fingerprints are based on the original file contents and the build ID. The build ID must change for each deployment so that fingerprinted
|
|
188
|
+
Source fingerprints are based on the original file contents and the build ID. The build ID must change for each deployment so that fingerprinted assets are invalidated together. This fingerprinting strategy assumes that files on disk won't change, so fingerprinting requires `watch: false`.
|
|
189
189
|
|
|
190
|
-
##
|
|
191
|
-
|
|
192
|
-
### Minification
|
|
190
|
+
## Target
|
|
193
191
|
|
|
194
|
-
|
|
192
|
+
Use `target` to lower emitted syntax to a specific browser support policy and/or ECMAScript version.
|
|
195
193
|
|
|
196
194
|
```ts
|
|
197
195
|
import { createAssetServer } from 'remix/assets'
|
|
@@ -199,15 +197,19 @@ import { createAssetServer } from 'remix/assets'
|
|
|
199
197
|
let assetServer = createAssetServer({
|
|
200
198
|
fileMap: { '/assets/app/*path': 'app/*path' },
|
|
201
199
|
allow: ['app/assets/**'],
|
|
202
|
-
|
|
203
|
-
|
|
200
|
+
target: {
|
|
201
|
+
chrome: '109',
|
|
202
|
+
ios: '15.6',
|
|
203
|
+
es: '2020',
|
|
204
204
|
},
|
|
205
205
|
})
|
|
206
206
|
```
|
|
207
207
|
|
|
208
|
-
|
|
208
|
+
Supported target options are `chrome`, `firefox`, `safari`, `edge`, `opera`, `ios`, `samsung`, and `es` (ECMAScript version).
|
|
209
|
+
|
|
210
|
+
### Source Maps
|
|
209
211
|
|
|
210
|
-
|
|
212
|
+
Enable sourcemaps with either `'external'` or `'inline'` using `sourceMaps`:
|
|
211
213
|
|
|
212
214
|
```ts
|
|
213
215
|
import { createAssetServer } from 'remix/assets'
|
|
@@ -215,38 +217,26 @@ import { createAssetServer } from 'remix/assets'
|
|
|
215
217
|
let assetServer = createAssetServer({
|
|
216
218
|
fileMap: { '/assets/app/*path': 'app/*path' },
|
|
217
219
|
allow: ['app/assets/**'],
|
|
218
|
-
|
|
219
|
-
target: 'es2019',
|
|
220
|
-
},
|
|
220
|
+
sourceMaps: 'external',
|
|
221
221
|
})
|
|
222
222
|
```
|
|
223
223
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
### Define
|
|
227
|
-
|
|
228
|
-
Use `scripts.define` to replace global identifiers with constant expressions.
|
|
224
|
+
By default, sourcemap `sources` use URLs so they're presented alongside the compiled output in your browser's developer tools. You can also use file system paths instead with `sourceMapSourcePaths`:
|
|
229
225
|
|
|
230
226
|
```ts
|
|
231
227
|
import { createAssetServer } from 'remix/assets'
|
|
232
228
|
|
|
233
229
|
let assetServer = createAssetServer({
|
|
234
230
|
fileMap: { '/assets/app/*path': 'app/*path' },
|
|
235
|
-
allow: ['app/assets/**'
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
'process.env.NODE_ENV': '"production"',
|
|
239
|
-
},
|
|
240
|
-
minify: true,
|
|
241
|
-
},
|
|
231
|
+
allow: ['app/assets/**'],
|
|
232
|
+
sourceMaps: 'inline',
|
|
233
|
+
sourceMapSourcePaths: 'absolute',
|
|
242
234
|
})
|
|
243
235
|
```
|
|
244
236
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
### Source Maps
|
|
237
|
+
### Minification
|
|
248
238
|
|
|
249
|
-
Enable
|
|
239
|
+
Enable minification with `minify`:
|
|
250
240
|
|
|
251
241
|
```ts
|
|
252
242
|
import { createAssetServer } from 'remix/assets'
|
|
@@ -254,27 +244,32 @@ import { createAssetServer } from 'remix/assets'
|
|
|
254
244
|
let assetServer = createAssetServer({
|
|
255
245
|
fileMap: { '/assets/app/*path': 'app/*path' },
|
|
256
246
|
allow: ['app/assets/**'],
|
|
257
|
-
|
|
258
|
-
sourceMaps: 'external',
|
|
259
|
-
},
|
|
247
|
+
minify: true,
|
|
260
248
|
})
|
|
261
249
|
```
|
|
262
250
|
|
|
263
|
-
|
|
251
|
+
## Script Options
|
|
252
|
+
|
|
253
|
+
### Define
|
|
254
|
+
|
|
255
|
+
Use `scripts.define` to replace global identifiers with constant expressions.
|
|
264
256
|
|
|
265
257
|
```ts
|
|
266
258
|
import { createAssetServer } from 'remix/assets'
|
|
267
259
|
|
|
268
260
|
let assetServer = createAssetServer({
|
|
269
261
|
fileMap: { '/assets/app/*path': 'app/*path' },
|
|
270
|
-
allow: ['app/assets/**'],
|
|
262
|
+
allow: ['app/assets/**', 'app/node_modules/**'],
|
|
271
263
|
scripts: {
|
|
272
|
-
|
|
273
|
-
|
|
264
|
+
define: {
|
|
265
|
+
'process.env.NODE_ENV': '"production"',
|
|
266
|
+
},
|
|
274
267
|
},
|
|
275
268
|
})
|
|
276
269
|
```
|
|
277
270
|
|
|
271
|
+
Values are injected exactly as defined, so string literals must include their own quotes, e.g. `process.env.NODE_ENV` must be `"production"` rather than `production`.
|
|
272
|
+
|
|
278
273
|
### External Imports
|
|
279
274
|
|
|
280
275
|
Use `scripts.external` to leave specific import specifiers unchanged by providing an array of specifiers.
|
|
@@ -291,6 +286,17 @@ let assetServer = createAssetServer({
|
|
|
291
286
|
})
|
|
292
287
|
```
|
|
293
288
|
|
|
289
|
+
## CSS Imports
|
|
290
|
+
|
|
291
|
+
Relative CSS `@import` rules are rewritten to asset server URLs. External `@import` URLs are left unchanged automatically. `url()` references are preserved as authored.
|
|
292
|
+
|
|
293
|
+
```css
|
|
294
|
+
/* Rewritten to asset server URL: */
|
|
295
|
+
@import './reset.css';
|
|
296
|
+
/* External URL: */
|
|
297
|
+
@import 'https://fonts.googleapis.com/css2?family=Inter';
|
|
298
|
+
```
|
|
299
|
+
|
|
294
300
|
## Error Handling
|
|
295
301
|
|
|
296
302
|
Use `onError` to report unexpected compilation failures and/or return a custom response.
|
|
@@ -302,8 +308,8 @@ let assetServer = createAssetServer({
|
|
|
302
308
|
fileMap: { '/assets/app/*path': 'app/*path' },
|
|
303
309
|
allow: ['app/assets/**'],
|
|
304
310
|
onError(error) {
|
|
305
|
-
console.error('Failed to build client
|
|
306
|
-
return new Response('Client
|
|
311
|
+
console.error('Failed to build client assets', error)
|
|
312
|
+
return new Response('Client asset build failed', { status: 500 })
|
|
307
313
|
},
|
|
308
314
|
})
|
|
309
315
|
```
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AssetTarget } from './target.ts';
|
|
1
2
|
import type { ChokidarWatcher } from './watch.ts';
|
|
2
3
|
interface AssetServerWatchOptions {
|
|
3
4
|
/**
|
|
@@ -16,13 +17,22 @@ interface AssetServerWatchOptions {
|
|
|
16
17
|
}
|
|
17
18
|
interface FingerprintOptions {
|
|
18
19
|
/**
|
|
19
|
-
* Per-build invalidation token that must change whenever fingerprinted
|
|
20
|
+
* Per-build invalidation token that must change whenever fingerprinted asset URLs
|
|
20
21
|
* should be invalidated together.
|
|
21
22
|
*/
|
|
22
23
|
buildId: string;
|
|
23
24
|
}
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
type AssetSourceMaps = 'inline' | 'external';
|
|
26
|
+
type AssetSourceMapSourcePaths = 'url' | 'absolute';
|
|
27
|
+
interface AssetServerScriptOptions {
|
|
28
|
+
/**
|
|
29
|
+
* Replace global expressions with constant values during transform, e.g.
|
|
30
|
+
* `{ 'process.env.NODE_ENV': '"production"' }`
|
|
31
|
+
*/
|
|
32
|
+
define?: Record<string, string>;
|
|
33
|
+
/** Import specifiers to leave unrewritten (CDN URLs, import map entries, etc.) */
|
|
34
|
+
external?: string[];
|
|
35
|
+
}
|
|
26
36
|
export interface AssetServerOptions {
|
|
27
37
|
/** File patterns keyed by public URL patterns. */
|
|
28
38
|
fileMap: Readonly<Record<string, string>>;
|
|
@@ -39,45 +49,37 @@ export interface AssetServerOptions {
|
|
|
39
49
|
*/
|
|
40
50
|
deny?: readonly string[];
|
|
41
51
|
/**
|
|
42
|
-
* Controls optional source-based URL fingerprinting for rewritten
|
|
52
|
+
* Controls optional source-based URL fingerprinting for rewritten asset URLs.
|
|
43
53
|
*
|
|
44
|
-
* When omitted, all served
|
|
54
|
+
* When omitted, all served assets use stable non-fingerprinted URLs with `Cache-Control: no-cache`.
|
|
45
55
|
* Cannot be used together with active watch mode. Set `watch: false` when fingerprinting.
|
|
46
56
|
*/
|
|
47
57
|
fingerprint?: FingerprintOptions;
|
|
48
58
|
/**
|
|
49
|
-
*
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Lower emitted syntax to a specific ECMAScript target. Omit this option to preserve
|
|
75
|
-
* modern syntax unless project configuration already requests a lower target.
|
|
76
|
-
*/
|
|
77
|
-
target?: ScriptsTarget;
|
|
78
|
-
/** Import specifiers to leave unrewritten (CDN URLs, import map entries, etc.) */
|
|
79
|
-
external?: string[];
|
|
80
|
-
};
|
|
59
|
+
* Shared compatibility target for scripts and styles. Browser targets apply to both
|
|
60
|
+
* pipelines, and `es` only affects scripts.
|
|
61
|
+
*/
|
|
62
|
+
target?: AssetTarget;
|
|
63
|
+
/**
|
|
64
|
+
* Source map mode for scripts and styles.
|
|
65
|
+
* - `'external'`: serve source maps as separate `.map` files
|
|
66
|
+
* - `'inline'`: embed source maps as a base64 data URL in the compiled asset
|
|
67
|
+
*/
|
|
68
|
+
sourceMaps?: AssetSourceMaps;
|
|
69
|
+
/**
|
|
70
|
+
* Source path strategy for source map `sources`.
|
|
71
|
+
* - `'url'` (default): use the stable server path (e.g. `'/assets/app/entry.ts'`)
|
|
72
|
+
* - `'absolute'`: use the original filesystem path on disk
|
|
73
|
+
*/
|
|
74
|
+
sourceMapSourcePaths?: AssetSourceMapSourcePaths;
|
|
75
|
+
/**
|
|
76
|
+
* Minification setting for emitted scripts and styles.
|
|
77
|
+
*/
|
|
78
|
+
minify?: boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Script-only configuration.
|
|
81
|
+
*/
|
|
82
|
+
scripts?: AssetServerScriptOptions;
|
|
81
83
|
/**
|
|
82
84
|
* Enable filesystem-backed cache invalidation for long-lived server instances.
|
|
83
85
|
* Enabled by default. Pass `true` to use the default watcher options, an options
|
|
@@ -92,16 +94,16 @@ export interface AssetServerOptions {
|
|
|
92
94
|
}
|
|
93
95
|
export interface AssetServer {
|
|
94
96
|
/**
|
|
95
|
-
* Serves a script request. Returns `Response | null` — null means the request
|
|
96
|
-
* handled by this server, letting the router fall through to a 404.
|
|
97
|
+
* Serves a script or style request. Returns `Response | null` — null means the request
|
|
98
|
+
* was not handled by this server, letting the router fall through to a 404.
|
|
97
99
|
*/
|
|
98
100
|
fetch(request: Request): Promise<Response | null>;
|
|
99
101
|
/**
|
|
100
|
-
* Returns the request href for a served
|
|
102
|
+
* Returns the request href for a served asset file.
|
|
101
103
|
*/
|
|
102
104
|
getHref(filePath: string): Promise<string>;
|
|
103
105
|
/**
|
|
104
|
-
* Returns preload URLs for one or more served
|
|
106
|
+
* Returns preload URLs for one or more served asset files, ordered shallowest-first.
|
|
105
107
|
*/
|
|
106
108
|
getPreloads(filePath: string | readonly string[]): Promise<string[]>;
|
|
107
109
|
/**
|
|
@@ -114,8 +116,8 @@ export declare function getInternalWatchTargets(assetServer: AssetServer): reado
|
|
|
114
116
|
/**
|
|
115
117
|
* Create an asset server instance
|
|
116
118
|
*
|
|
117
|
-
* Compiles TypeScript/JavaScript
|
|
118
|
-
* fingerprinting, caching, and configurable file mapping.
|
|
119
|
+
* Compiles TypeScript/JavaScript scripts and CSS styles on demand with optional
|
|
120
|
+
* source-based URL fingerprinting, caching, and configurable file mapping.
|
|
119
121
|
*
|
|
120
122
|
* @param options Server configuration
|
|
121
123
|
* @returns A {@link AssetServer} with `fetch()`, `getHref()`, and `getPreloads()` methods
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"asset-server.d.ts","sourceRoot":"","sources":["../../src/lib/asset-server.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"asset-server.d.ts","sourceRoot":"","sources":["../../src/lib/asset-server.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,WAAW,EAA6C,MAAM,aAAa,CAAA;AAEzF,OAAO,KAAK,EAAsB,eAAe,EAAE,MAAM,YAAY,CAAA;AAErE,UAAU,uBAAuB;IAC/B;;;OAGG;IACH,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAC1B;;OAEG;IACH,IAAI,CAAC,EAAE,OAAO,CAAA;IACd;;OAEG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,UAAU,kBAAkB;IAC1B;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,KAAK,eAAe,GAAG,QAAQ,GAAG,UAAU,CAAA;AAC5C,KAAK,yBAAyB,GAAG,KAAK,GAAG,UAAU,CAAA;AAEnD,UAAU,wBAAwB;IAChC;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC/B,kFAAkF;IAClF,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;CACpB;AAID,MAAM,WAAW,kBAAkB;IACjC,kDAAkD;IAClD,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IACzC;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;OAEG;IACH,KAAK,EAAE,SAAS,MAAM,EAAE,CAAA;IACxB;;OAEG;IACH,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IACxB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,kBAAkB,CAAA;IAChC;;;OAGG;IACH,MAAM,CAAC,EAAE,WAAW,CAAA;IACpB;;;;OAIG;IACH,UAAU,CAAC,EAAE,eAAe,CAAA;IAC5B;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,yBAAyB,CAAA;IAChD;;OAEG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB;;OAEG;IACH,OAAO,CAAC,EAAE,wBAAwB,CAAA;IAClC;;;;OAIG;IACH,KAAK,CAAC,EAAE,OAAO,GAAG,uBAAuB,CAAA;IACzC;;;OAGG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,GAAG,QAAQ,GAAG,OAAO,CAAC,IAAI,GAAG,QAAQ,CAAC,CAAA;CACzE;AAED,MAAM,WAAW,WAAW;IAC1B;;;OAGG;IACH,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAA;IACjD;;OAEG;IACH,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IAC1C;;OAEG;IACH,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAA;IACpE;;OAEG;IACH,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACvB;AAuBD,wBAAgB,0BAA0B,CAAC,WAAW,EAAE,WAAW,GAAG,eAAe,GAAG,SAAS,CAEhG;AAED,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,WAAW,GAAG,SAAS,MAAM,EAAE,CAEnF;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,kBAAkB,GAAG,WAAW,CAqN1E"}
|
package/dist/lib/asset-server.js
CHANGED
|
@@ -1,27 +1,16 @@
|
|
|
1
1
|
import * as path from 'node:path';
|
|
2
2
|
import * as fs from 'node:fs';
|
|
3
|
-
import { isAssetServerCompilationError } from "./compilation-error.js";
|
|
4
3
|
import { createAccessPolicy } from "./access.js";
|
|
5
|
-
import {
|
|
4
|
+
import { isAssetServerCompilationError } from "./compilation-error.js";
|
|
5
|
+
import { getFingerprintRequestCacheControl, parseFingerprintSuffix } from "./fingerprint.js";
|
|
6
6
|
import { normalizeFilePath } from "./paths.js";
|
|
7
7
|
import { compileRoutes } from "./routes.js";
|
|
8
|
+
import { createResponseForScript, createScriptCompiler } from "./scripts/compiler.js";
|
|
9
|
+
import { supportedScriptExtensions } from "./scripts/resolve.js";
|
|
10
|
+
import { createResponseForStyle, createStyleCompiler, isStyleFilePath } from "./styles/compiler.js";
|
|
11
|
+
import { resolveScriptTarget, resolveStyleTarget } from "./target.js";
|
|
8
12
|
import { createAssetServerWatcher } from "./watch.js";
|
|
9
|
-
const
|
|
10
|
-
'es2015',
|
|
11
|
-
'es2016',
|
|
12
|
-
'es2017',
|
|
13
|
-
'es2018',
|
|
14
|
-
'es2019',
|
|
15
|
-
'es2020',
|
|
16
|
-
'es2021',
|
|
17
|
-
'es2022',
|
|
18
|
-
'es2023',
|
|
19
|
-
'es2024',
|
|
20
|
-
'es2025',
|
|
21
|
-
'es2026',
|
|
22
|
-
'esnext',
|
|
23
|
-
];
|
|
24
|
-
const scriptTargetSet = new Set(scriptTargets);
|
|
13
|
+
const scriptExtensionSet = new Set(supportedScriptExtensions);
|
|
25
14
|
const chokidarWatcherByAssetServer = new WeakMap();
|
|
26
15
|
const watcherByAssetServer = new WeakMap();
|
|
27
16
|
export function getInternalChokidarWatcher(assetServer) {
|
|
@@ -33,8 +22,8 @@ export function getInternalWatchTargets(assetServer) {
|
|
|
33
22
|
/**
|
|
34
23
|
* Create an asset server instance
|
|
35
24
|
*
|
|
36
|
-
* Compiles TypeScript/JavaScript
|
|
37
|
-
* fingerprinting, caching, and configurable file mapping.
|
|
25
|
+
* Compiles TypeScript/JavaScript scripts and CSS styles on demand with optional
|
|
26
|
+
* source-based URL fingerprinting, caching, and configurable file mapping.
|
|
38
27
|
*
|
|
39
28
|
* @param options Server configuration
|
|
40
29
|
* @returns A {@link AssetServer} with `fetch()`, `getHref()`, and `getPreloads()` methods
|
|
@@ -60,11 +49,11 @@ export function createAssetServer(options) {
|
|
|
60
49
|
});
|
|
61
50
|
let watcher = null;
|
|
62
51
|
let chokidarWatcher = null;
|
|
63
|
-
let
|
|
52
|
+
let scriptCompiler = createScriptCompiler({
|
|
64
53
|
buildId: resolvedOptions.buildId,
|
|
65
54
|
define: resolvedOptions.define,
|
|
66
55
|
external: resolvedOptions.external,
|
|
67
|
-
|
|
56
|
+
fingerprintAssets: resolvedOptions.fingerprintAssets,
|
|
68
57
|
isAllowed: accessPolicy.isAllowed,
|
|
69
58
|
minify: resolvedOptions.minify,
|
|
70
59
|
onWatchDirectoriesChange: (delta) => {
|
|
@@ -80,6 +69,23 @@ export function createAssetServer(options) {
|
|
|
80
69
|
watchIgnore: resolvedOptions.watchOptions?.ignore,
|
|
81
70
|
watchMode: resolvedOptions.watchOptions !== null,
|
|
82
71
|
});
|
|
72
|
+
let styleCompiler = createStyleCompiler({
|
|
73
|
+
buildId: resolvedOptions.buildId,
|
|
74
|
+
fingerprintAssets: resolvedOptions.fingerprintAssets,
|
|
75
|
+
isAllowed: accessPolicy.isAllowed,
|
|
76
|
+
minify: resolvedOptions.minify,
|
|
77
|
+
onWatchDirectoriesChange: (delta) => {
|
|
78
|
+
if (!watcher)
|
|
79
|
+
return;
|
|
80
|
+
watcher.updateWatchedDirectories(delta);
|
|
81
|
+
},
|
|
82
|
+
rootDir: resolvedOptions.rootDir,
|
|
83
|
+
routes: resolvedOptions.routes,
|
|
84
|
+
sourceMapSourcePaths: resolvedOptions.sourceMapSourcePaths,
|
|
85
|
+
sourceMaps: resolvedOptions.sourceMaps,
|
|
86
|
+
targets: resolvedOptions.stylesTarget,
|
|
87
|
+
watchIgnore: resolvedOptions.watchOptions?.ignore,
|
|
88
|
+
});
|
|
83
89
|
if (resolvedOptions.watchOptions) {
|
|
84
90
|
watcher = createAssetServerWatcher({
|
|
85
91
|
...resolvedOptions.watchOptions,
|
|
@@ -102,7 +108,8 @@ export function createAssetServer(options) {
|
|
|
102
108
|
async function handleWatchEvent(filePath, event) {
|
|
103
109
|
try {
|
|
104
110
|
let normalizedFilePath = normalizeFilePath(filePath);
|
|
105
|
-
await
|
|
111
|
+
await scriptCompiler.handleFileEvent(normalizedFilePath, event);
|
|
112
|
+
await styleCompiler.handleFileEvent(normalizedFilePath, event);
|
|
106
113
|
}
|
|
107
114
|
catch (error) {
|
|
108
115
|
console.error(`There was an error invalidating the asset server cache: ${error}`);
|
|
@@ -112,28 +119,58 @@ export function createAssetServer(options) {
|
|
|
112
119
|
async fetch(request) {
|
|
113
120
|
if (request.method !== 'GET' && request.method !== 'HEAD')
|
|
114
121
|
return null;
|
|
115
|
-
let parsedRequestPathname =
|
|
122
|
+
let parsedRequestPathname = parseAssetRequestPathname(new URL(request.url).pathname, {
|
|
123
|
+
fingerprintAssets: resolvedOptions.fingerprintAssets,
|
|
124
|
+
routes: resolvedOptions.routes,
|
|
125
|
+
});
|
|
116
126
|
if (!parsedRequestPathname)
|
|
117
127
|
return null;
|
|
118
128
|
try {
|
|
119
129
|
let ifNoneMatch = request.headers.get('If-None-Match');
|
|
120
|
-
|
|
130
|
+
if (isStyleFilePath(parsedRequestPathname.filePath)) {
|
|
131
|
+
let styleResult = await styleCompiler.getStyle(parsedRequestPathname.filePath, {
|
|
132
|
+
ifNoneMatch,
|
|
133
|
+
isSourceMapRequest: parsedRequestPathname.isSourceMapRequest,
|
|
134
|
+
requestedFingerprint: parsedRequestPathname.requestedFingerprint,
|
|
135
|
+
});
|
|
136
|
+
if (styleResult.type === 'not-modified') {
|
|
137
|
+
return new Response(null, {
|
|
138
|
+
status: 304,
|
|
139
|
+
headers: { ETag: styleResult.etag },
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
let compiledStyle = styleResult.style;
|
|
143
|
+
if (parsedRequestPathname.requestedFingerprint !== null) {
|
|
144
|
+
if (compiledStyle.fingerprint !== parsedRequestPathname.requestedFingerprint)
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
return createResponseForStyle(compiledStyle, {
|
|
148
|
+
cacheControl: parsedRequestPathname.cacheControl,
|
|
149
|
+
ifNoneMatch,
|
|
150
|
+
isSourceMapRequest: parsedRequestPathname.isSourceMapRequest,
|
|
151
|
+
method: request.method,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
if (!isScriptFilePath(parsedRequestPathname.filePath)) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
let scriptResult = await scriptCompiler.getScript(parsedRequestPathname.filePath, {
|
|
121
158
|
ifNoneMatch,
|
|
122
159
|
isSourceMapRequest: parsedRequestPathname.isSourceMapRequest,
|
|
123
160
|
requestedFingerprint: parsedRequestPathname.requestedFingerprint,
|
|
124
161
|
});
|
|
125
|
-
if (
|
|
162
|
+
if (scriptResult.type === 'not-modified') {
|
|
126
163
|
return new Response(null, {
|
|
127
164
|
status: 304,
|
|
128
|
-
headers: { ETag:
|
|
165
|
+
headers: { ETag: scriptResult.etag },
|
|
129
166
|
});
|
|
130
167
|
}
|
|
131
|
-
let
|
|
168
|
+
let compiledScript = scriptResult.script;
|
|
132
169
|
if (parsedRequestPathname.requestedFingerprint !== null) {
|
|
133
|
-
if (
|
|
170
|
+
if (compiledScript.fingerprint !== parsedRequestPathname.requestedFingerprint)
|
|
134
171
|
return null;
|
|
135
172
|
}
|
|
136
|
-
return
|
|
173
|
+
return createResponseForScript(compiledScript, {
|
|
137
174
|
cacheControl: parsedRequestPathname.cacheControl,
|
|
138
175
|
ifNoneMatch,
|
|
139
176
|
isSourceMapRequest: parsedRequestPathname.isSourceMapRequest,
|
|
@@ -145,17 +182,45 @@ export function createAssetServer(options) {
|
|
|
145
182
|
// compiling imports. In this fetch context, both cases should fall through as "not
|
|
146
183
|
// handled here" so the outer router can continue to its own 404 behavior.
|
|
147
184
|
if (isAssetServerCompilationError(error) &&
|
|
148
|
-
(error.code === '
|
|
185
|
+
(error.code === 'FILE_NOT_FOUND' || error.code === 'FILE_NOT_ALLOWED')) {
|
|
149
186
|
return null;
|
|
150
187
|
}
|
|
151
188
|
return responseForError(error);
|
|
152
189
|
}
|
|
153
190
|
},
|
|
154
191
|
async getHref(filePath) {
|
|
155
|
-
|
|
192
|
+
if (isStyleFilePath(filePath)) {
|
|
193
|
+
return styleCompiler.getHref(filePath);
|
|
194
|
+
}
|
|
195
|
+
return scriptCompiler.getHref(filePath);
|
|
156
196
|
},
|
|
157
197
|
async getPreloads(filePath) {
|
|
158
|
-
|
|
198
|
+
let filePaths = Array.isArray(filePath) ? filePath : [filePath];
|
|
199
|
+
let styleFiles = [];
|
|
200
|
+
let scriptFiles = [];
|
|
201
|
+
for (let nextFilePath of filePaths) {
|
|
202
|
+
if (isStyleFilePath(nextFilePath)) {
|
|
203
|
+
styleFiles.push(nextFilePath);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
scriptFiles.push(nextFilePath);
|
|
207
|
+
}
|
|
208
|
+
if (styleFiles.length === 0 && scriptFiles.length === 0) {
|
|
209
|
+
return [];
|
|
210
|
+
}
|
|
211
|
+
if (styleFiles.length === 0) {
|
|
212
|
+
return flattenPreloadLayers(await scriptCompiler.getPreloadLayers(filePath));
|
|
213
|
+
}
|
|
214
|
+
if (scriptFiles.length === 0) {
|
|
215
|
+
return flattenPreloadLayers(await styleCompiler.getPreloadLayers(filePath));
|
|
216
|
+
}
|
|
217
|
+
// Mixed asset type preloads need to be merged, so we merge in order of first asset type seen
|
|
218
|
+
let scriptPreloadLayersPromise = scriptCompiler.getPreloadLayers(scriptFiles);
|
|
219
|
+
let stylePreloadLayersPromise = styleCompiler.getPreloadLayers(styleFiles);
|
|
220
|
+
let preloadLayerGroups = isStyleFilePath(filePaths[0])
|
|
221
|
+
? [stylePreloadLayersPromise, scriptPreloadLayersPromise]
|
|
222
|
+
: [scriptPreloadLayersPromise, stylePreloadLayersPromise];
|
|
223
|
+
return mergePreloadLayers(await Promise.all(preloadLayerGroups));
|
|
159
224
|
},
|
|
160
225
|
async close() {
|
|
161
226
|
await watcher?.close();
|
|
@@ -175,6 +240,25 @@ function internalServerError() {
|
|
|
175
240
|
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
|
|
176
241
|
});
|
|
177
242
|
}
|
|
243
|
+
function mergePreloadLayers(preloadLayersByRoot) {
|
|
244
|
+
let urls = [];
|
|
245
|
+
let seen = new Set();
|
|
246
|
+
let maxDepth = Math.max(0, ...preloadLayersByRoot.map((layers) => layers.length));
|
|
247
|
+
for (let depth = 0; depth < maxDepth; depth++) {
|
|
248
|
+
for (let preloadLayers of preloadLayersByRoot) {
|
|
249
|
+
for (let url of preloadLayers[depth] ?? []) {
|
|
250
|
+
if (seen.has(url))
|
|
251
|
+
continue;
|
|
252
|
+
seen.add(url);
|
|
253
|
+
urls.push(url);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return urls;
|
|
258
|
+
}
|
|
259
|
+
function flattenPreloadLayers(preloadLayers) {
|
|
260
|
+
return preloadLayers.flatMap((layer) => layer);
|
|
261
|
+
}
|
|
178
262
|
function defaultErrorHandler(error) {
|
|
179
263
|
console.error(error);
|
|
180
264
|
}
|
|
@@ -191,28 +275,21 @@ function resolveAssetServerOptions(options) {
|
|
|
191
275
|
define: scriptOptions.define,
|
|
192
276
|
deny: options.deny,
|
|
193
277
|
external: scriptOptions.external ?? [],
|
|
194
|
-
|
|
195
|
-
minify:
|
|
278
|
+
fingerprintAssets: fingerprintOptions.enabled,
|
|
279
|
+
minify: options.minify ?? false,
|
|
196
280
|
onError: options.onError ?? defaultErrorHandler,
|
|
197
281
|
rootDir,
|
|
198
282
|
routes: compileRoutes({
|
|
199
283
|
fileMap: options.fileMap,
|
|
200
284
|
rootDir,
|
|
201
285
|
}),
|
|
202
|
-
sourceMapSourcePaths:
|
|
203
|
-
sourceMaps:
|
|
204
|
-
scriptsTarget:
|
|
286
|
+
sourceMapSourcePaths: options.sourceMapSourcePaths ?? 'url',
|
|
287
|
+
sourceMaps: options.sourceMaps,
|
|
288
|
+
scriptsTarget: resolveScriptTarget(options.target),
|
|
289
|
+
stylesTarget: resolveStyleTarget(options.target),
|
|
205
290
|
watchOptions: normalizeWatchOptions(options.watch),
|
|
206
291
|
};
|
|
207
292
|
}
|
|
208
|
-
function normalizeTarget(target) {
|
|
209
|
-
if (target == null)
|
|
210
|
-
return undefined;
|
|
211
|
-
if (typeof target !== 'string' || !scriptTargetSet.has(target)) {
|
|
212
|
-
throw new TypeError(`Expected target to be one of ${scriptTargets.map((value) => `"${value}"`).join(', ')}. Received "${target}".`);
|
|
213
|
-
}
|
|
214
|
-
return target;
|
|
215
|
-
}
|
|
216
293
|
function normalizeFingerprintOptions(options) {
|
|
217
294
|
if (!options.fingerprint) {
|
|
218
295
|
return {
|
|
@@ -240,3 +317,22 @@ function normalizeWatchOptions(options) {
|
|
|
240
317
|
return {};
|
|
241
318
|
return options;
|
|
242
319
|
}
|
|
320
|
+
function parseAssetRequestPathname(pathname, options) {
|
|
321
|
+
let isSourceMapRequest = pathname.endsWith('.map');
|
|
322
|
+
let pathWithoutMap = isSourceMapRequest ? pathname.slice(0, -4) : pathname;
|
|
323
|
+
let fingerprint = parseFingerprintSuffix(pathWithoutMap);
|
|
324
|
+
let filePath = options.routes.resolveUrlPathname(fingerprint.pathname);
|
|
325
|
+
if (!filePath)
|
|
326
|
+
return null;
|
|
327
|
+
if (options.fingerprintAssets && fingerprint.requestedFingerprint === null)
|
|
328
|
+
return null;
|
|
329
|
+
return {
|
|
330
|
+
cacheControl: getFingerprintRequestCacheControl(fingerprint.requestedFingerprint),
|
|
331
|
+
filePath,
|
|
332
|
+
isSourceMapRequest,
|
|
333
|
+
requestedFingerprint: fingerprint.requestedFingerprint,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
function isScriptFilePath(filePath) {
|
|
337
|
+
return scriptExtensionSet.has(path.extname(filePath).toLowerCase());
|
|
338
|
+
}
|