@remix-run/assets 0.3.0 → 0.4.1
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 +206 -8
- package/dist/assets.d.ts +1 -0
- package/dist/assets.d.ts.map +1 -1
- package/dist/assets.js +1 -0
- package/dist/lib/access.d.ts +1 -0
- package/dist/lib/access.d.ts.map +1 -1
- package/dist/lib/access.js +7 -1
- package/dist/lib/asset-server.d.ts +29 -6
- package/dist/lib/asset-server.d.ts.map +1 -1
- package/dist/lib/asset-server.js +176 -17
- package/dist/lib/compilation-error.d.ts +1 -1
- package/dist/lib/compilation-error.d.ts.map +1 -1
- package/dist/lib/files/compiler.d.ts +71 -0
- package/dist/lib/files/compiler.d.ts.map +1 -0
- package/dist/lib/files/compiler.js +552 -0
- package/dist/lib/files/config.d.ts +101 -0
- package/dist/lib/files/config.d.ts.map +1 -0
- package/dist/lib/files/config.js +219 -0
- package/dist/lib/files/store.d.ts +31 -0
- package/dist/lib/files/store.d.ts.map +1 -0
- package/dist/lib/files/store.js +63 -0
- package/dist/lib/fingerprint.d.ts +3 -3
- package/dist/lib/fingerprint.d.ts.map +1 -1
- package/dist/lib/fingerprint.js +5 -5
- package/dist/lib/routes.d.ts.map +1 -1
- package/dist/lib/routes.js +28 -20
- package/dist/lib/scripts/compiler.d.ts +1 -0
- package/dist/lib/scripts/compiler.d.ts.map +1 -1
- package/dist/lib/scripts/compiler.js +31 -14
- package/dist/lib/scripts/resolve.d.ts.map +1 -1
- package/dist/lib/scripts/resolve.js +35 -8
- package/dist/lib/scripts/specifiers.d.ts +2 -0
- package/dist/lib/scripts/specifiers.d.ts.map +1 -0
- package/dist/lib/scripts/specifiers.js +9 -0
- package/dist/lib/scripts/transform.d.ts.map +1 -1
- package/dist/lib/scripts/transform.js +3 -5
- package/dist/lib/styles/compiler.d.ts +4 -0
- package/dist/lib/styles/compiler.d.ts.map +1 -1
- package/dist/lib/styles/compiler.js +2 -0
- package/dist/lib/styles/emit.d.ts +3 -0
- package/dist/lib/styles/emit.d.ts.map +1 -1
- package/dist/lib/styles/emit.js +36 -1
- package/dist/lib/styles/resolve.d.ts +8 -1
- package/dist/lib/styles/resolve.d.ts.map +1 -1
- package/dist/lib/styles/resolve.js +85 -32
- package/dist/lib/watch.d.ts +2 -0
- package/dist/lib/watch.d.ts.map +1 -1
- package/dist/lib/watch.js +25 -8
- package/package.json +7 -5
- package/src/assets.ts +1 -0
- package/src/lib/access.ts +8 -1
- package/src/lib/asset-server.ts +273 -28
- package/src/lib/compilation-error.ts +9 -0
- package/src/lib/files/compiler.ts +885 -0
- package/src/lib/files/config.ts +479 -0
- package/src/lib/files/store.ts +109 -0
- package/src/lib/fingerprint.ts +8 -7
- package/src/lib/routes.ts +36 -33
- package/src/lib/scripts/compiler.ts +44 -17
- package/src/lib/scripts/resolve.ts +57 -9
- package/src/lib/scripts/specifiers.ts +11 -0
- package/src/lib/scripts/transform.ts +3 -6
- package/src/lib/styles/compiler.ts +9 -0
- package/src/lib/styles/emit.ts +75 -1
- package/src/lib/styles/resolve.ts +132 -35
- package/src/lib/watch.ts +34 -12
package/src/lib/access.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { createFileMatcher } from './file-matcher.ts'
|
|
|
2
2
|
import { isInjectedPackageFilePath } from './injected-packages.ts'
|
|
3
3
|
|
|
4
4
|
type AccessPolicy = {
|
|
5
|
+
isDenied(filePath: string): boolean
|
|
5
6
|
isAllowed(filePath: string): boolean
|
|
6
7
|
}
|
|
7
8
|
|
|
@@ -15,11 +16,17 @@ export function createAccessPolicy(options: {
|
|
|
15
16
|
createFileMatcher(pattern, options.rootDir),
|
|
16
17
|
)
|
|
17
18
|
|
|
19
|
+
function isDenied(filePath: string): boolean {
|
|
20
|
+
if (isInjectedPackageFilePath(filePath)) return false
|
|
21
|
+
return denyMatchers.length > 0 && denyMatchers.some((matcher) => matcher(filePath))
|
|
22
|
+
}
|
|
23
|
+
|
|
18
24
|
return {
|
|
25
|
+
isDenied,
|
|
19
26
|
isAllowed(filePath) {
|
|
20
27
|
if (isInjectedPackageFilePath(filePath)) return true
|
|
21
28
|
if (!allowMatchers.some((matcher) => matcher(filePath))) return false
|
|
22
|
-
if (
|
|
29
|
+
if (isDenied(filePath)) return false
|
|
23
30
|
return true
|
|
24
31
|
},
|
|
25
32
|
}
|
package/src/lib/asset-server.ts
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
import * as path from 'node:path'
|
|
2
2
|
import * as fs from 'node:fs'
|
|
3
3
|
import { createAccessPolicy } from './access.ts'
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
createAssetServerCompilationError,
|
|
6
|
+
isAssetServerCompilationError,
|
|
7
|
+
} from './compilation-error.ts'
|
|
8
|
+
import { createFileCompiler, createResponseForFile } from './files/compiler.ts'
|
|
9
|
+
import type { FileCompiler } from './files/compiler.ts'
|
|
10
|
+
import { normalizeFilesOptions, serializeAssetTransformInvocations } from './files/config.ts'
|
|
11
|
+
import type {
|
|
12
|
+
AssetRequestTransformMap,
|
|
13
|
+
AssetServerFilesOptions,
|
|
14
|
+
AssetTransformInvocation,
|
|
15
|
+
ResolvedAssetServerFilesOptions,
|
|
16
|
+
} from './files/config.ts'
|
|
5
17
|
import { getFingerprintRequestCacheControl, parseFingerprintSuffix } from './fingerprint.ts'
|
|
6
18
|
import { getInjectedPackageRouteConfigs } from './injected-packages.ts'
|
|
7
19
|
import { normalizeFilePath, normalizePathname } from './paths.ts'
|
|
@@ -54,10 +66,14 @@ interface AssetServerScriptOptions {
|
|
|
54
66
|
|
|
55
67
|
const scriptExtensionSet = new Set<string>(supportedScriptExtensions)
|
|
56
68
|
|
|
57
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Options used to construct an {@link AssetServer} via {@link createAssetServer}.
|
|
71
|
+
*/
|
|
72
|
+
export interface AssetServerOptions<transforms extends AssetRequestTransformMap = {}> {
|
|
58
73
|
/** Public mount path for this asset server, e.g. `'/assets'`. */
|
|
59
74
|
basePath: string
|
|
60
75
|
/** File patterns keyed by public URL patterns relative to `basePath`. */
|
|
76
|
+
/** File patterns keyed by public URL patterns. */
|
|
61
77
|
fileMap: Readonly<Record<string, string>>
|
|
62
78
|
/**
|
|
63
79
|
* Root directory used to resolve relative file paths. Defaults to `process.cwd()`.
|
|
@@ -103,6 +119,12 @@ export interface AssetServerOptions {
|
|
|
103
119
|
* Script-only configuration.
|
|
104
120
|
*/
|
|
105
121
|
scripts?: AssetServerScriptOptions
|
|
122
|
+
/**
|
|
123
|
+
* Leaf file asset configuration. Files configured here are served directly and can be
|
|
124
|
+
* referenced from CSS `url(...)` rules. Compiled asset extensions like `.css` and script
|
|
125
|
+
* module extensions are not allowed here.
|
|
126
|
+
*/
|
|
127
|
+
files?: AssetServerFilesOptions<transforms>
|
|
106
128
|
/**
|
|
107
129
|
* Enable filesystem-backed cache invalidation for long-lived server instances.
|
|
108
130
|
* Enabled by default. Pass `true` to use the default watcher options, an options
|
|
@@ -116,7 +138,26 @@ export interface AssetServerOptions {
|
|
|
116
138
|
onError?: (error: unknown) => void | Response | Promise<void | Response>
|
|
117
139
|
}
|
|
118
140
|
|
|
119
|
-
|
|
141
|
+
type AssetServerCreateOptions<transforms extends AssetRequestTransformMap> = Omit<
|
|
142
|
+
AssetServerOptions<transforms>,
|
|
143
|
+
'files'
|
|
144
|
+
> & {
|
|
145
|
+
files?: Omit<AssetServerFilesOptions<transforms>, 'transforms'> & {
|
|
146
|
+
transforms?: transforms
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export type AssetServerGetHrefOptions<transforms extends AssetRequestTransformMap> =
|
|
151
|
+
| undefined
|
|
152
|
+
| {
|
|
153
|
+
transform: readonly AssetTransformInvocation<transforms>[]
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Serves compiled scripts and styles for asset requests routed to it.
|
|
158
|
+
* Construct with {@link createAssetServer}.
|
|
159
|
+
*/
|
|
160
|
+
export interface AssetServer<transforms extends AssetRequestTransformMap = {}> {
|
|
120
161
|
/**
|
|
121
162
|
* Serves a script or style request. Returns `Response | null` — null means the request
|
|
122
163
|
* was not handled by this server, letting the router fall through to a 404.
|
|
@@ -125,7 +166,7 @@ export interface AssetServer {
|
|
|
125
166
|
/**
|
|
126
167
|
* Returns the request href for a served asset file.
|
|
127
168
|
*/
|
|
128
|
-
getHref(filePath: string): Promise<string>
|
|
169
|
+
getHref(filePath: string, options?: AssetServerGetHrefOptions<transforms>): Promise<string>
|
|
129
170
|
/**
|
|
130
171
|
* Returns preload URLs for one or more served asset files, ordered shallowest-first.
|
|
131
172
|
*/
|
|
@@ -136,13 +177,14 @@ export interface AssetServer {
|
|
|
136
177
|
close(): Promise<void>
|
|
137
178
|
}
|
|
138
179
|
|
|
139
|
-
type ResolvedAssetServerOptions = {
|
|
180
|
+
type ResolvedAssetServerOptions<transforms extends AssetRequestTransformMap> = {
|
|
140
181
|
allow: readonly string[]
|
|
141
182
|
basePath: string
|
|
142
183
|
buildId?: string
|
|
143
184
|
define?: Record<string, string>
|
|
144
185
|
deny?: readonly string[]
|
|
145
186
|
external: string[]
|
|
187
|
+
files: ResolvedAssetServerFilesOptions
|
|
146
188
|
fingerprintAssets: boolean
|
|
147
189
|
minify: boolean
|
|
148
190
|
onError: NonNullable<AssetServerOptions['onError']>
|
|
@@ -155,14 +197,18 @@ type ResolvedAssetServerOptions = {
|
|
|
155
197
|
watchOptions: AssetServerWatchOptions | null
|
|
156
198
|
}
|
|
157
199
|
|
|
158
|
-
const chokidarWatcherByAssetServer = new WeakMap<
|
|
159
|
-
const watcherByAssetServer = new WeakMap<
|
|
200
|
+
const chokidarWatcherByAssetServer = new WeakMap<object, ChokidarWatcher>()
|
|
201
|
+
const watcherByAssetServer = new WeakMap<object, AssetServerWatcher>()
|
|
160
202
|
|
|
161
|
-
export function getInternalChokidarWatcher
|
|
203
|
+
export function getInternalChokidarWatcher<transforms extends AssetRequestTransformMap>(
|
|
204
|
+
assetServer: AssetServer<transforms>,
|
|
205
|
+
): ChokidarWatcher | undefined {
|
|
162
206
|
return chokidarWatcherByAssetServer.get(assetServer)
|
|
163
207
|
}
|
|
164
208
|
|
|
165
|
-
export function getInternalWatchTargets
|
|
209
|
+
export function getInternalWatchTargets<transforms extends AssetRequestTransformMap>(
|
|
210
|
+
assetServer: AssetServer<transforms>,
|
|
211
|
+
): readonly string[] {
|
|
166
212
|
return watcherByAssetServer.get(assetServer)?.getWatchedTargets() ?? []
|
|
167
213
|
}
|
|
168
214
|
|
|
@@ -188,7 +234,9 @@ export function getInternalWatchTargets(assetServer: AssetServer): readonly stri
|
|
|
188
234
|
* route('/assets/*path', ({ request }) => assetServer.fetch(request))
|
|
189
235
|
* ```
|
|
190
236
|
*/
|
|
191
|
-
export function createAssetServer
|
|
237
|
+
export function createAssetServer<const transforms extends AssetRequestTransformMap = {}>(
|
|
238
|
+
options: AssetServerCreateOptions<transforms>,
|
|
239
|
+
): AssetServer<transforms> {
|
|
192
240
|
let resolvedOptions = resolveAssetServerOptions(options)
|
|
193
241
|
let accessPolicy = createAccessPolicy({
|
|
194
242
|
allow: resolvedOptions.allow,
|
|
@@ -197,12 +245,14 @@ export function createAssetServer(options: AssetServerOptions): AssetServer {
|
|
|
197
245
|
})
|
|
198
246
|
let watcher: AssetServerWatcher | null = null
|
|
199
247
|
let chokidarWatcher: ChokidarWatcher | null = null
|
|
248
|
+
let fileCompiler: FileCompiler | null = null
|
|
200
249
|
let scriptCompiler = createScriptCompiler({
|
|
201
250
|
buildId: resolvedOptions.buildId,
|
|
202
251
|
define: resolvedOptions.define,
|
|
203
252
|
external: resolvedOptions.external,
|
|
204
253
|
fingerprintAssets: resolvedOptions.fingerprintAssets,
|
|
205
254
|
isAllowed: accessPolicy.isAllowed,
|
|
255
|
+
isDenied: accessPolicy.isDenied,
|
|
206
256
|
minify: resolvedOptions.minify,
|
|
207
257
|
onWatchDirectoriesChange: (delta) => {
|
|
208
258
|
if (!watcher) return
|
|
@@ -219,11 +269,27 @@ export function createAssetServer(options: AssetServerOptions): AssetServer {
|
|
|
219
269
|
let styleCompiler = createStyleCompiler({
|
|
220
270
|
buildId: resolvedOptions.buildId,
|
|
221
271
|
fingerprintAssets: resolvedOptions.fingerprintAssets,
|
|
272
|
+
getServedFileUrl: (identityPath: string, options: { transform: readonly string[] | null }) => {
|
|
273
|
+
if (!fileCompiler) {
|
|
274
|
+
throw createAssetServerCompilationError(`File type is not supported: ${identityPath}`, {
|
|
275
|
+
code: 'FILE_NOT_SUPPORTED',
|
|
276
|
+
})
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (options.transform !== null) {
|
|
280
|
+
fileCompiler.validateTransformQuery(options.transform)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return fileCompiler.getHref(identityPath, { transform: options.transform })
|
|
284
|
+
},
|
|
222
285
|
isAllowed: accessPolicy.isAllowed,
|
|
286
|
+
isServedFilePath(filePath) {
|
|
287
|
+
return fileCompiler?.isServedFilePath(filePath) ?? false
|
|
288
|
+
},
|
|
223
289
|
minify: resolvedOptions.minify,
|
|
224
290
|
onWatchDirectoriesChange: (delta) => {
|
|
225
291
|
if (!watcher) return
|
|
226
|
-
watcher.updateWatchedDirectories(delta)
|
|
292
|
+
watcher.updateWatchedDirectories(delta, { includeAncestors: false })
|
|
227
293
|
},
|
|
228
294
|
rootDir: resolvedOptions.rootDir,
|
|
229
295
|
routes: resolvedOptions.routes,
|
|
@@ -232,6 +298,20 @@ export function createAssetServer(options: AssetServerOptions): AssetServer {
|
|
|
232
298
|
targets: resolvedOptions.stylesTarget,
|
|
233
299
|
watchIgnore: resolvedOptions.watchOptions?.ignore,
|
|
234
300
|
})
|
|
301
|
+
if (resolvedOptions.files.extensions.length > 0) {
|
|
302
|
+
fileCompiler = createFileCompiler({
|
|
303
|
+
buildId: resolvedOptions.buildId,
|
|
304
|
+
cache: resolvedOptions.files.cache,
|
|
305
|
+
extensions: resolvedOptions.files.extensions,
|
|
306
|
+
fingerprintAssets: resolvedOptions.fingerprintAssets,
|
|
307
|
+
globalTransforms: resolvedOptions.files.globalTransforms,
|
|
308
|
+
isAllowed: accessPolicy.isAllowed,
|
|
309
|
+
maxRequestTransforms: resolvedOptions.files.maxRequestTransforms,
|
|
310
|
+
transforms: resolvedOptions.files.transforms,
|
|
311
|
+
rootDir: resolvedOptions.rootDir,
|
|
312
|
+
routes: resolvedOptions.routes,
|
|
313
|
+
})
|
|
314
|
+
}
|
|
235
315
|
if (resolvedOptions.watchOptions) {
|
|
236
316
|
watcher = createAssetServerWatcher({
|
|
237
317
|
...resolvedOptions.watchOptions,
|
|
@@ -257,16 +337,19 @@ export function createAssetServer(options: AssetServerOptions): AssetServer {
|
|
|
257
337
|
let normalizedFilePath = normalizeFilePath(filePath)
|
|
258
338
|
await scriptCompiler.handleFileEvent(normalizedFilePath, event)
|
|
259
339
|
await styleCompiler.handleFileEvent(normalizedFilePath, event)
|
|
340
|
+
await fileCompiler?.handleFileEvent(normalizedFilePath, event)
|
|
260
341
|
} catch (error) {
|
|
261
342
|
console.error(`There was an error invalidating the asset server cache: ${error}`)
|
|
262
343
|
}
|
|
263
344
|
}
|
|
264
345
|
|
|
265
|
-
let assetServer: AssetServer = {
|
|
346
|
+
let assetServer: AssetServer<transforms> = {
|
|
266
347
|
async fetch(request) {
|
|
267
348
|
if (request.method !== 'GET' && request.method !== 'HEAD') return null
|
|
268
349
|
|
|
269
|
-
let
|
|
350
|
+
let requestUrl = new URL(request.url)
|
|
351
|
+
let requestedTransform = normalizeRequestedTransformQuery(requestUrl.searchParams)
|
|
352
|
+
let parsedRequestPathname = parseAssetRequestPathname(requestUrl.pathname, {
|
|
270
353
|
fingerprintAssets: resolvedOptions.fingerprintAssets,
|
|
271
354
|
routes: resolvedOptions.routes,
|
|
272
355
|
})
|
|
@@ -276,6 +359,11 @@ export function createAssetServer(options: AssetServerOptions): AssetServer {
|
|
|
276
359
|
let ifNoneMatch = request.headers.get('If-None-Match')
|
|
277
360
|
|
|
278
361
|
if (isStyleFilePath(parsedRequestPathname.filePath)) {
|
|
362
|
+
if (requestedTransform !== null) {
|
|
363
|
+
return badRequest(
|
|
364
|
+
'Asset request transforms are only supported for configured file assets',
|
|
365
|
+
)
|
|
366
|
+
}
|
|
279
367
|
let styleResult = await styleCompiler.getStyle(parsedRequestPathname.filePath, {
|
|
280
368
|
ifNoneMatch,
|
|
281
369
|
isSourceMapRequest: parsedRequestPathname.isSourceMapRequest,
|
|
@@ -304,9 +392,43 @@ export function createAssetServer(options: AssetServerOptions): AssetServer {
|
|
|
304
392
|
}
|
|
305
393
|
|
|
306
394
|
if (!isScriptFilePath(parsedRequestPathname.filePath)) {
|
|
307
|
-
|
|
395
|
+
if (!fileCompiler?.isServedFilePath(parsedRequestPathname.filePath)) {
|
|
396
|
+
return null
|
|
397
|
+
}
|
|
398
|
+
if (parsedRequestPathname.isSourceMapRequest) {
|
|
399
|
+
return null
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
let fileResult = await fileCompiler.getFile(parsedRequestPathname.filePath, {
|
|
403
|
+
ifNoneMatch,
|
|
404
|
+
requestedFingerprint: parsedRequestPathname.requestedFingerprint,
|
|
405
|
+
transform: requestedTransform,
|
|
406
|
+
})
|
|
407
|
+
if (fileResult.type === 'not-modified') {
|
|
408
|
+
return new Response(null, {
|
|
409
|
+
status: 304,
|
|
410
|
+
headers: { ETag: fileResult.etag },
|
|
411
|
+
})
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (parsedRequestPathname.requestedFingerprint !== null) {
|
|
415
|
+
if (fileResult.file.fingerprint !== parsedRequestPathname.requestedFingerprint) {
|
|
416
|
+
return null
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return createResponseForFile(fileResult.file, {
|
|
421
|
+
cacheControl: parsedRequestPathname.cacheControl,
|
|
422
|
+
ifNoneMatch,
|
|
423
|
+
method: request.method,
|
|
424
|
+
})
|
|
308
425
|
}
|
|
309
426
|
|
|
427
|
+
if (requestedTransform !== null) {
|
|
428
|
+
return badRequest(
|
|
429
|
+
'Asset request transforms are only supported for configured file assets',
|
|
430
|
+
)
|
|
431
|
+
}
|
|
310
432
|
let scriptResult = await scriptCompiler.getScript(parsedRequestPathname.filePath, {
|
|
311
433
|
ifNoneMatch,
|
|
312
434
|
isSourceMapRequest: parsedRequestPathname.isSourceMapRequest,
|
|
@@ -341,52 +463,113 @@ export function createAssetServer(options: AssetServerOptions): AssetServer {
|
|
|
341
463
|
) {
|
|
342
464
|
return null
|
|
343
465
|
}
|
|
466
|
+
if (isAssetServerCompilationError(error) && error.code === 'FILE_TRANSFORM_QUERY_INVALID') {
|
|
467
|
+
return new Response(error.message, {
|
|
468
|
+
status: 400,
|
|
469
|
+
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
|
|
470
|
+
})
|
|
471
|
+
}
|
|
344
472
|
|
|
345
473
|
return responseForError(error)
|
|
346
474
|
}
|
|
347
475
|
},
|
|
348
476
|
|
|
349
|
-
async getHref(filePath) {
|
|
350
|
-
|
|
477
|
+
async getHref(filePath, hrefOptions) {
|
|
478
|
+
let transform = getHrefTransform(hrefOptions, resolvedOptions.files)
|
|
479
|
+
let typeCheckFilePath = stripFilePathUrlSuffix(filePath)
|
|
480
|
+
|
|
481
|
+
if (isStyleFilePath(typeCheckFilePath)) {
|
|
482
|
+
if (transform !== null) {
|
|
483
|
+
throw new TypeError(
|
|
484
|
+
'assetServer.getHref() only supports transforms for configured file assets',
|
|
485
|
+
)
|
|
486
|
+
}
|
|
351
487
|
return styleCompiler.getHref(filePath)
|
|
352
488
|
}
|
|
353
489
|
|
|
490
|
+
if (fileCompiler?.isServedFilePath(typeCheckFilePath)) {
|
|
491
|
+
return fileCompiler.getHref(filePath, {
|
|
492
|
+
transform,
|
|
493
|
+
})
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (!isScriptFilePath(typeCheckFilePath)) {
|
|
497
|
+
throw createAssetServerCompilationError(`File type is not supported: ${filePath}`, {
|
|
498
|
+
code: 'FILE_NOT_SUPPORTED',
|
|
499
|
+
})
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (transform !== null) {
|
|
503
|
+
throw new TypeError(
|
|
504
|
+
'assetServer.getHref() only supports transforms for configured file assets',
|
|
505
|
+
)
|
|
506
|
+
}
|
|
507
|
+
|
|
354
508
|
return scriptCompiler.getHref(filePath)
|
|
355
509
|
},
|
|
356
510
|
async getPreloads(filePath) {
|
|
357
511
|
let filePaths = Array.isArray(filePath) ? filePath : [filePath]
|
|
512
|
+
let fileAssetFiles: string[] = []
|
|
358
513
|
let styleFiles: string[] = []
|
|
359
514
|
let scriptFiles: string[] = []
|
|
515
|
+
let groupedAssetTypes: ('script' | 'style' | 'file')[] = []
|
|
360
516
|
|
|
361
517
|
for (let nextFilePath of filePaths) {
|
|
362
|
-
|
|
518
|
+
let typeCheckFilePath = stripFilePathUrlSuffix(nextFilePath)
|
|
519
|
+
|
|
520
|
+
if (isStyleFilePath(typeCheckFilePath)) {
|
|
521
|
+
if (!groupedAssetTypes.includes('style')) {
|
|
522
|
+
groupedAssetTypes.push('style')
|
|
523
|
+
}
|
|
363
524
|
styleFiles.push(nextFilePath)
|
|
364
525
|
continue
|
|
365
526
|
}
|
|
366
527
|
|
|
528
|
+
if (fileCompiler?.isServedFilePath(typeCheckFilePath)) {
|
|
529
|
+
if (!groupedAssetTypes.includes('file')) {
|
|
530
|
+
groupedAssetTypes.push('file')
|
|
531
|
+
}
|
|
532
|
+
fileAssetFiles.push(nextFilePath)
|
|
533
|
+
continue
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (!groupedAssetTypes.includes('script')) {
|
|
537
|
+
groupedAssetTypes.push('script')
|
|
538
|
+
}
|
|
367
539
|
scriptFiles.push(nextFilePath)
|
|
368
540
|
}
|
|
369
541
|
|
|
370
|
-
if (styleFiles.length === 0 && scriptFiles.length === 0) {
|
|
542
|
+
if (styleFiles.length === 0 && scriptFiles.length === 0 && fileAssetFiles.length === 0) {
|
|
371
543
|
return []
|
|
372
544
|
}
|
|
373
545
|
|
|
374
|
-
if (styleFiles.length === 0) {
|
|
546
|
+
if (styleFiles.length === 0 && fileAssetFiles.length === 0) {
|
|
375
547
|
return flattenPreloadLayers(await scriptCompiler.getPreloadLayers(filePath))
|
|
376
548
|
}
|
|
377
549
|
|
|
378
|
-
if (scriptFiles.length === 0) {
|
|
550
|
+
if (scriptFiles.length === 0 && fileAssetFiles.length === 0) {
|
|
379
551
|
return flattenPreloadLayers(await styleCompiler.getPreloadLayers(filePath))
|
|
380
552
|
}
|
|
381
553
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
554
|
+
if (scriptFiles.length === 0 && styleFiles.length === 0) {
|
|
555
|
+
return flattenPreloadLayers(await getFilePreloadLayers(fileCompiler, fileAssetFiles))
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Mixed asset type preloads need to be merged, so we merge in order of first asset type seen.
|
|
559
|
+
let preloadLayerGroupPromises = groupedAssetTypes.flatMap((assetType) => {
|
|
560
|
+
switch (assetType) {
|
|
561
|
+
case 'script':
|
|
562
|
+
return scriptFiles.length > 0 ? [scriptCompiler.getPreloadLayers(scriptFiles)] : []
|
|
563
|
+
case 'style':
|
|
564
|
+
return styleFiles.length > 0 ? [styleCompiler.getPreloadLayers(styleFiles)] : []
|
|
565
|
+
case 'file':
|
|
566
|
+
return fileAssetFiles.length > 0
|
|
567
|
+
? [getFilePreloadLayers(fileCompiler, fileAssetFiles)]
|
|
568
|
+
: []
|
|
569
|
+
}
|
|
570
|
+
})
|
|
388
571
|
|
|
389
|
-
return mergePreloadLayers(await Promise.all(
|
|
572
|
+
return mergePreloadLayers(await Promise.all(preloadLayerGroupPromises))
|
|
390
573
|
},
|
|
391
574
|
async close() {
|
|
392
575
|
await watcher?.close()
|
|
@@ -403,6 +586,24 @@ export function createAssetServer(options: AssetServerOptions): AssetServer {
|
|
|
403
586
|
return assetServer
|
|
404
587
|
}
|
|
405
588
|
|
|
589
|
+
function getHrefTransform<transforms extends AssetRequestTransformMap>(
|
|
590
|
+
options: AssetServerGetHrefOptions<transforms> | undefined,
|
|
591
|
+
files: ResolvedAssetServerFilesOptions,
|
|
592
|
+
): readonly string[] | null {
|
|
593
|
+
if (!options) return null
|
|
594
|
+
let transform = options.transform
|
|
595
|
+
if (transform.length === 0) {
|
|
596
|
+
return null
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
return serializeAssetTransformInvocations(transform, files.transforms, files.maxRequestTransforms)
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function normalizeRequestedTransformQuery(searchParams: URLSearchParams): readonly string[] | null {
|
|
603
|
+
let transforms = searchParams.getAll('transform')
|
|
604
|
+
return transforms.length > 0 ? transforms : null
|
|
605
|
+
}
|
|
606
|
+
|
|
406
607
|
function internalServerError(): Response {
|
|
407
608
|
return new Response('Internal Server Error', {
|
|
408
609
|
status: 500,
|
|
@@ -410,6 +611,13 @@ function internalServerError(): Response {
|
|
|
410
611
|
})
|
|
411
612
|
}
|
|
412
613
|
|
|
614
|
+
function badRequest(message: string): Response {
|
|
615
|
+
return new Response(message, {
|
|
616
|
+
status: 400,
|
|
617
|
+
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
|
|
618
|
+
})
|
|
619
|
+
}
|
|
620
|
+
|
|
413
621
|
function mergePreloadLayers(preloadLayersByRoot: readonly (readonly string[][])[]): string[] {
|
|
414
622
|
let urls: string[] = []
|
|
415
623
|
let seen = new Set<string>()
|
|
@@ -432,11 +640,38 @@ function flattenPreloadLayers(preloadLayers: readonly (readonly string[])[]): st
|
|
|
432
640
|
return preloadLayers.flatMap((layer) => layer)
|
|
433
641
|
}
|
|
434
642
|
|
|
643
|
+
async function getFilePreloadLayers<transforms extends AssetRequestTransformMap>(
|
|
644
|
+
fileCompiler: FileCompiler | null,
|
|
645
|
+
filePaths: readonly string[],
|
|
646
|
+
): Promise<string[][]> {
|
|
647
|
+
if (filePaths.length === 0) {
|
|
648
|
+
return []
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if (!fileCompiler) {
|
|
652
|
+
throw createAssetServerCompilationError('File type is not supported', {
|
|
653
|
+
code: 'FILE_NOT_SUPPORTED',
|
|
654
|
+
})
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
return [
|
|
658
|
+
await Promise.all(
|
|
659
|
+
filePaths.map((filePath) =>
|
|
660
|
+
fileCompiler.getHref(filePath, {
|
|
661
|
+
transform: null,
|
|
662
|
+
}),
|
|
663
|
+
),
|
|
664
|
+
),
|
|
665
|
+
]
|
|
666
|
+
}
|
|
667
|
+
|
|
435
668
|
function defaultErrorHandler(error: unknown): void {
|
|
436
669
|
console.error(error)
|
|
437
670
|
}
|
|
438
671
|
|
|
439
|
-
function resolveAssetServerOptions
|
|
672
|
+
function resolveAssetServerOptions<transforms extends AssetRequestTransformMap>(
|
|
673
|
+
options: AssetServerCreateOptions<transforms>,
|
|
674
|
+
): ResolvedAssetServerOptions<transforms> {
|
|
440
675
|
let rootDir = normalizeFilePath(fs.realpathSync(path.resolve(options.rootDir ?? process.cwd())))
|
|
441
676
|
let basePath = normalizeBasePath(options.basePath)
|
|
442
677
|
let scriptOptions = options.scripts ?? {}
|
|
@@ -452,6 +687,7 @@ function resolveAssetServerOptions(options: AssetServerOptions): ResolvedAssetSe
|
|
|
452
687
|
define: scriptOptions.define,
|
|
453
688
|
deny: options.deny,
|
|
454
689
|
external: scriptOptions.external ?? [],
|
|
690
|
+
files: normalizeFilesOptions(options.files),
|
|
455
691
|
fingerprintAssets: fingerprintOptions.enabled,
|
|
456
692
|
minify: options.minify ?? false,
|
|
457
693
|
onError: options.onError ?? defaultErrorHandler,
|
|
@@ -553,3 +789,12 @@ function parseAssetRequestPathname(
|
|
|
553
789
|
function isScriptFilePath(filePath: string): boolean {
|
|
554
790
|
return scriptExtensionSet.has(path.extname(filePath).toLowerCase())
|
|
555
791
|
}
|
|
792
|
+
|
|
793
|
+
function stripFilePathUrlSuffix(filePath: string): string {
|
|
794
|
+
if (filePath.startsWith('file://')) {
|
|
795
|
+
return new URL(filePath).pathname
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
let queryIndex = filePath.indexOf('?')
|
|
799
|
+
return queryIndex === -1 ? filePath : filePath.slice(0, queryIndex)
|
|
800
|
+
}
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
type AssetServerCompilationErrorCode =
|
|
2
2
|
| 'FILE_NOT_FOUND'
|
|
3
3
|
| 'FILE_NOT_ALLOWED'
|
|
4
|
+
| 'FILE_NOT_SUPPORTED'
|
|
4
5
|
| 'FILE_OUTSIDE_FILE_MAP'
|
|
6
|
+
| 'FILE_TRANSFORM_QUERY_INVALID'
|
|
7
|
+
| 'FILE_TRANSFORM_NOT_SUPPORTED'
|
|
8
|
+
| 'FILE_TRANSFORM_RESULT_INVALID'
|
|
9
|
+
| 'FILE_TRANSFORM_FAILED'
|
|
5
10
|
| 'COMMONJS_NOT_SUPPORTED'
|
|
6
11
|
| 'TRANSFORM_FAILED'
|
|
7
12
|
| 'EMIT_FAILED'
|
|
@@ -9,6 +14,10 @@ type AssetServerCompilationErrorCode =
|
|
|
9
14
|
| 'IMPORT_NOT_SUPPORTED'
|
|
10
15
|
| 'IMPORT_NOT_ALLOWED'
|
|
11
16
|
| 'IMPORT_OUTSIDE_FILE_MAP'
|
|
17
|
+
| 'URL_RESOLUTION_FAILED'
|
|
18
|
+
| 'URL_NOT_SUPPORTED'
|
|
19
|
+
| 'URL_NOT_ALLOWED'
|
|
20
|
+
| 'URL_OUTSIDE_FILE_MAP'
|
|
12
21
|
|
|
13
22
|
/**
|
|
14
23
|
* Internal error used by the request-time asset compilation pipeline.
|