@remix-run/assets 0.2.0 → 0.4.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 +241 -28
- 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 +10 -1
- package/dist/lib/asset-server.d.ts +33 -7
- package/dist/lib/asset-server.d.ts.map +1 -1
- package/dist/lib/asset-server.js +195 -23
- 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/injected-packages.d.ts +11 -0
- package/dist/lib/injected-packages.d.ts.map +1 -0
- package/dist/lib/injected-packages.js +86 -0
- package/dist/lib/paths.d.ts +1 -0
- package/dist/lib/paths.d.ts.map +1 -1
- package/dist/lib/paths.js +12 -0
- package/dist/lib/routes.d.ts +5 -7
- package/dist/lib/routes.d.ts.map +1 -1
- package/dist/lib/routes.js +41 -36
- 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 +30 -5
- package/dist/lib/scripts/resolve.d.ts.map +1 -1
- package/dist/lib/scripts/resolve.js +57 -10
- package/dist/lib/scripts/transform.d.ts.map +1 -1
- package/dist/lib/scripts/transform.js +79 -3
- 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 +11 -5
- package/src/assets.ts +1 -0
- package/src/lib/access.ts +10 -1
- package/src/lib/asset-server.ts +297 -34
- 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/injected-packages.ts +117 -0
- package/src/lib/paths.ts +28 -0
- package/src/lib/routes.ts +67 -55
- package/src/lib/scripts/compiler.ts +43 -5
- package/src/lib/scripts/resolve.ts +89 -11
- package/src/lib/scripts/transform.ts +105 -3
- 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
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
import type { FileStorage } from '@remix-run/file-storage'
|
|
2
|
+
import { supportedScriptExtensions } from '../scripts/resolve.ts'
|
|
3
|
+
|
|
4
|
+
export interface AssetFileTransformResult {
|
|
5
|
+
content: string | Uint8Array
|
|
6
|
+
extension?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
type AssetRequestTransformParamMode = true | 'optional' | undefined
|
|
10
|
+
|
|
11
|
+
declare const assetRequestTransformTypes: unique symbol
|
|
12
|
+
|
|
13
|
+
type AssetRequestTransformTypes<
|
|
14
|
+
param extends string,
|
|
15
|
+
mode extends AssetRequestTransformParamMode,
|
|
16
|
+
> = {
|
|
17
|
+
input: param
|
|
18
|
+
mode: mode
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface AssetTransformContext {
|
|
22
|
+
extension: string
|
|
23
|
+
filePath: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type AssetRequestTransformRuntimeParam<mode extends AssetRequestTransformParamMode> =
|
|
27
|
+
mode extends true ? string : mode extends 'optional' ? string | undefined : undefined
|
|
28
|
+
|
|
29
|
+
export interface AssetRequestTransformContext<
|
|
30
|
+
mode extends AssetRequestTransformParamMode = undefined,
|
|
31
|
+
> extends AssetTransformContext {
|
|
32
|
+
param: AssetRequestTransformRuntimeParam<mode>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface AssetGlobalTransformContext extends AssetTransformContext {}
|
|
36
|
+
|
|
37
|
+
export type AssetRequestTransform<
|
|
38
|
+
param extends string = string,
|
|
39
|
+
mode extends AssetRequestTransformParamMode = undefined,
|
|
40
|
+
> = {
|
|
41
|
+
readonly [assetRequestTransformTypes]?: AssetRequestTransformTypes<param, mode>
|
|
42
|
+
/**
|
|
43
|
+
* Optional list of file extensions this transform accepts. Values must use `.ext` format.
|
|
44
|
+
* Matching is evaluated against the current extension at this point in the transform pipeline.
|
|
45
|
+
*/
|
|
46
|
+
extensions?: readonly string[]
|
|
47
|
+
transform(
|
|
48
|
+
bytes: Uint8Array,
|
|
49
|
+
context: AssetRequestTransformContext<mode>,
|
|
50
|
+
):
|
|
51
|
+
| string
|
|
52
|
+
| Uint8Array
|
|
53
|
+
| AssetFileTransformResult
|
|
54
|
+
| Promise<string | Uint8Array | AssetFileTransformResult>
|
|
55
|
+
} & (mode extends undefined ? { param?: undefined } : { param: mode })
|
|
56
|
+
|
|
57
|
+
type AssetGlobalTransformHandler = (
|
|
58
|
+
bytes: Uint8Array,
|
|
59
|
+
context: AssetGlobalTransformContext,
|
|
60
|
+
) =>
|
|
61
|
+
| string
|
|
62
|
+
| Uint8Array
|
|
63
|
+
| AssetFileTransformResult
|
|
64
|
+
| null
|
|
65
|
+
| Promise<string | Uint8Array | AssetFileTransformResult | null>
|
|
66
|
+
|
|
67
|
+
export type AssetGlobalTransform =
|
|
68
|
+
| AssetGlobalTransformHandler
|
|
69
|
+
| {
|
|
70
|
+
/**
|
|
71
|
+
* Optional list of file extensions this transform accepts. Values must use `.ext` format.
|
|
72
|
+
* Non-matching files are skipped automatically.
|
|
73
|
+
*/
|
|
74
|
+
extensions?: readonly string[]
|
|
75
|
+
name?: string
|
|
76
|
+
transform: AssetGlobalTransformHandler
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface ResolvedAssetGlobalTransform {
|
|
80
|
+
extensions?: readonly string[]
|
|
81
|
+
name?: string
|
|
82
|
+
transform: AssetGlobalTransformHandler
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export type AssetRequestTransformMap = Readonly<
|
|
86
|
+
Record<string, AssetRequestTransform<string, AssetRequestTransformParamMode>>
|
|
87
|
+
>
|
|
88
|
+
|
|
89
|
+
export type ResolvedAssetRequestTransformMap = ReadonlyMap<
|
|
90
|
+
string,
|
|
91
|
+
AssetRequestTransform<string, AssetRequestTransformParamMode>
|
|
92
|
+
>
|
|
93
|
+
|
|
94
|
+
export interface AssetServerFilesOptions<transforms extends AssetRequestTransformMap = {}> {
|
|
95
|
+
/**
|
|
96
|
+
* File extensions to expose as leaf assets. Values must include the leading dot,
|
|
97
|
+
* for example `['.png', '.svg', '.woff2']`.
|
|
98
|
+
*/
|
|
99
|
+
extensions: readonly string[]
|
|
100
|
+
/**
|
|
101
|
+
* Named transforms that can be requested from asset URLs.
|
|
102
|
+
*/
|
|
103
|
+
transforms?: transforms
|
|
104
|
+
/**
|
|
105
|
+
* Maximum number of request transforms allowed in a single asset URL.
|
|
106
|
+
* Defaults to `5`.
|
|
107
|
+
*/
|
|
108
|
+
maxRequestTransforms?: number
|
|
109
|
+
/**
|
|
110
|
+
* Ordered transforms that run for every served file asset and may return `null`
|
|
111
|
+
* to skip themselves for a given input.
|
|
112
|
+
*/
|
|
113
|
+
globalTransforms?: readonly AssetGlobalTransform[]
|
|
114
|
+
/**
|
|
115
|
+
* Optional backing store for cached transformed file outputs.
|
|
116
|
+
*/
|
|
117
|
+
cache?: FileStorage
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface ResolvedAssetServerFilesOptions {
|
|
121
|
+
cache?: FileStorage
|
|
122
|
+
extensions: readonly string[]
|
|
123
|
+
globalTransforms: readonly ResolvedAssetGlobalTransform[]
|
|
124
|
+
hasTransforms: boolean
|
|
125
|
+
maxRequestTransforms: number
|
|
126
|
+
transforms: ResolvedAssetRequestTransformMap
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
type AssetTransformStep<
|
|
130
|
+
name extends string,
|
|
131
|
+
param extends string,
|
|
132
|
+
mode extends AssetRequestTransformParamMode,
|
|
133
|
+
> = mode extends true
|
|
134
|
+
? readonly [name, param]
|
|
135
|
+
: mode extends 'optional'
|
|
136
|
+
? name | readonly [name] | readonly [name, param]
|
|
137
|
+
: name | readonly [name]
|
|
138
|
+
|
|
139
|
+
export type AssetTransformInvocation<transforms extends AssetRequestTransformMap> = {
|
|
140
|
+
[name in keyof transforms & string]: transforms[name] extends {
|
|
141
|
+
readonly [assetRequestTransformTypes]?: infer types
|
|
142
|
+
}
|
|
143
|
+
? types extends {
|
|
144
|
+
input: infer param extends string
|
|
145
|
+
mode: infer mode extends AssetRequestTransformParamMode
|
|
146
|
+
}
|
|
147
|
+
? AssetTransformStep<name, param, mode>
|
|
148
|
+
: never
|
|
149
|
+
: never
|
|
150
|
+
}[keyof transforms & string]
|
|
151
|
+
|
|
152
|
+
export function defineFileTransform(
|
|
153
|
+
transform: AssetRequestTransform<string, undefined>,
|
|
154
|
+
): AssetRequestTransform<string, undefined>
|
|
155
|
+
export function defineFileTransform<const param extends string = string>(
|
|
156
|
+
transform: AssetRequestTransform<param, true>,
|
|
157
|
+
): AssetRequestTransform<param, true>
|
|
158
|
+
export function defineFileTransform<const param extends string = string>(
|
|
159
|
+
transform: AssetRequestTransform<param, 'optional'>,
|
|
160
|
+
): AssetRequestTransform<param, 'optional'>
|
|
161
|
+
export function defineFileTransform(
|
|
162
|
+
transform: AssetRequestTransform<string, AssetRequestTransformParamMode>,
|
|
163
|
+
): AssetRequestTransform<string, AssetRequestTransformParamMode> {
|
|
164
|
+
return transform
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const reservedFileExtensions = new Set<string>([...supportedScriptExtensions, '.css', '.map'])
|
|
168
|
+
const defaultMaxRequestTransforms = 5
|
|
169
|
+
|
|
170
|
+
export function normalizeFilesOptions<transforms extends AssetRequestTransformMap>(
|
|
171
|
+
files: AssetServerFilesOptions<transforms> | undefined,
|
|
172
|
+
): ResolvedAssetServerFilesOptions {
|
|
173
|
+
if (files == null) {
|
|
174
|
+
return {
|
|
175
|
+
extensions: [],
|
|
176
|
+
globalTransforms: [],
|
|
177
|
+
hasTransforms: false,
|
|
178
|
+
maxRequestTransforms: defaultMaxRequestTransforms,
|
|
179
|
+
transforms: new Map(),
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (!Array.isArray(files.extensions)) {
|
|
184
|
+
throw new TypeError('files.extensions must be an array')
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
let normalizedExtensions: string[] = []
|
|
188
|
+
let seen = new Set<string>()
|
|
189
|
+
|
|
190
|
+
for (let extension of files.extensions) {
|
|
191
|
+
if (typeof extension !== 'string') {
|
|
192
|
+
throw new TypeError('files.extensions values must be strings')
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
let normalizedExtension = extension.trim().toLowerCase()
|
|
196
|
+
if (!/^\.[A-Za-z0-9_-]+$/.test(normalizedExtension)) {
|
|
197
|
+
throw new TypeError(
|
|
198
|
+
`files.extensions values must use ".ext" format. Received "${extension}".`,
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (reservedFileExtensions.has(normalizedExtension)) {
|
|
203
|
+
throw new TypeError(
|
|
204
|
+
`files.extensions cannot include compiled asset extensions like "${normalizedExtension}".`,
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (seen.has(normalizedExtension)) continue
|
|
209
|
+
seen.add(normalizedExtension)
|
|
210
|
+
normalizedExtensions.push(normalizedExtension)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
let transforms = files.transforms ?? {}
|
|
214
|
+
if (transforms === null || typeof transforms !== 'object' || Array.isArray(transforms)) {
|
|
215
|
+
throw new TypeError('files.transforms must be an object')
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
let normalizedTransforms = new Map<
|
|
219
|
+
string,
|
|
220
|
+
AssetRequestTransform<string, AssetRequestTransformParamMode>
|
|
221
|
+
>()
|
|
222
|
+
|
|
223
|
+
for (let [name, transform] of Object.entries(transforms)) {
|
|
224
|
+
if (!/^[A-Za-z0-9_-]+$/.test(name)) {
|
|
225
|
+
throw new TypeError(
|
|
226
|
+
`files.transforms keys must use "transform-name" format. Received "${name}".`,
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (
|
|
231
|
+
transform === null ||
|
|
232
|
+
typeof transform !== 'object' ||
|
|
233
|
+
typeof transform.transform !== 'function'
|
|
234
|
+
) {
|
|
235
|
+
throw new TypeError(`files.transforms.${name} must define a transform() function`)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (
|
|
239
|
+
'param' in transform &&
|
|
240
|
+
transform.param !== undefined &&
|
|
241
|
+
transform.param !== true &&
|
|
242
|
+
transform.param !== 'optional'
|
|
243
|
+
) {
|
|
244
|
+
throw new TypeError(`files.transforms.${name}.param must be true or "optional"`)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
normalizedTransforms.set(name, {
|
|
248
|
+
...transform,
|
|
249
|
+
extensions: normalizeTransformExtensions(
|
|
250
|
+
transform.extensions,
|
|
251
|
+
`files.transforms.${name}.extensions`,
|
|
252
|
+
),
|
|
253
|
+
})
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
let globalTransforms = files.globalTransforms ?? []
|
|
257
|
+
if (!Array.isArray(globalTransforms)) {
|
|
258
|
+
throw new TypeError('files.globalTransforms must be an array')
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
let normalizedGlobalTransforms: ResolvedAssetGlobalTransform[] = []
|
|
262
|
+
|
|
263
|
+
for (let [index, transform] of globalTransforms.entries()) {
|
|
264
|
+
if (typeof transform === 'function') {
|
|
265
|
+
normalizedGlobalTransforms.push({ transform })
|
|
266
|
+
continue
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (transform === null || typeof transform !== 'object') {
|
|
270
|
+
throw new TypeError(`files.globalTransforms[${index}] must be a function or object`)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if ('name' in transform && transform.name !== undefined && typeof transform.name !== 'string') {
|
|
274
|
+
throw new TypeError(`files.globalTransforms[${index}].name must be a string`)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (typeof transform.transform !== 'function') {
|
|
278
|
+
throw new TypeError(`files.globalTransforms[${index}] must define a transform() function`)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
normalizedGlobalTransforms.push({
|
|
282
|
+
...transform,
|
|
283
|
+
extensions: normalizeTransformExtensions(
|
|
284
|
+
transform.extensions,
|
|
285
|
+
`files.globalTransforms[${index}].extensions`,
|
|
286
|
+
),
|
|
287
|
+
})
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
let maxRequestTransforms = files.maxRequestTransforms ?? defaultMaxRequestTransforms
|
|
291
|
+
if (!Number.isInteger(maxRequestTransforms) || maxRequestTransforms < 1) {
|
|
292
|
+
throw new TypeError('files.maxRequestTransforms must be a positive integer')
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (files.cache !== undefined) {
|
|
296
|
+
if (
|
|
297
|
+
files.cache === null ||
|
|
298
|
+
typeof files.cache !== 'object' ||
|
|
299
|
+
typeof files.cache.get !== 'function' ||
|
|
300
|
+
typeof files.cache.set !== 'function'
|
|
301
|
+
) {
|
|
302
|
+
throw new TypeError('files.cache must implement the FileStorage interface')
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
cache: files.cache,
|
|
308
|
+
extensions: normalizedExtensions,
|
|
309
|
+
globalTransforms: normalizedGlobalTransforms,
|
|
310
|
+
hasTransforms: normalizedTransforms.size > 0 || normalizedGlobalTransforms.length > 0,
|
|
311
|
+
maxRequestTransforms,
|
|
312
|
+
transforms: normalizedTransforms,
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function normalizeTransformExtensions(
|
|
317
|
+
extensions: readonly string[] | undefined,
|
|
318
|
+
optionPath: string,
|
|
319
|
+
): readonly string[] | undefined {
|
|
320
|
+
if (extensions === undefined) return undefined
|
|
321
|
+
if (!Array.isArray(extensions)) {
|
|
322
|
+
throw new TypeError(`${optionPath} must be an array`)
|
|
323
|
+
}
|
|
324
|
+
if (extensions.length === 0) {
|
|
325
|
+
throw new TypeError(`${optionPath} must include at least one extension`)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
let normalizedExtensions: string[] = []
|
|
329
|
+
let seen = new Set<string>()
|
|
330
|
+
|
|
331
|
+
for (let extension of extensions) {
|
|
332
|
+
if (typeof extension !== 'string') {
|
|
333
|
+
throw new TypeError(`${optionPath} values must be strings`)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
let normalizedExtension = extension.trim().toLowerCase()
|
|
337
|
+
if (!/^\.[A-Za-z0-9_-]+$/.test(normalizedExtension)) {
|
|
338
|
+
throw new TypeError(`${optionPath} values must use ".ext" format. Received "${extension}".`)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (seen.has(normalizedExtension)) continue
|
|
342
|
+
seen.add(normalizedExtension)
|
|
343
|
+
normalizedExtensions.push(normalizedExtension)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return normalizedExtensions
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export function serializeAssetTransformInvocations<transforms extends AssetRequestTransformMap>(
|
|
350
|
+
transforms: readonly AssetTransformInvocation<transforms>[],
|
|
351
|
+
transformsByName: ResolvedAssetRequestTransformMap,
|
|
352
|
+
maxTransforms = defaultMaxRequestTransforms,
|
|
353
|
+
): string[] {
|
|
354
|
+
if (transforms.length > maxTransforms) {
|
|
355
|
+
throw new TypeError(`Expected at most ${maxTransforms} request transforms`)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return transforms.map((transformInvocation) =>
|
|
359
|
+
normalizeAssetTransformInvocation(transformInvocation, transformsByName, (message) => {
|
|
360
|
+
throw new TypeError(message)
|
|
361
|
+
}),
|
|
362
|
+
)
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export function parseAssetTransformInvocations(
|
|
366
|
+
transformsQuery: readonly string[],
|
|
367
|
+
transformsByName: ResolvedAssetRequestTransformMap,
|
|
368
|
+
maxTransforms = defaultMaxRequestTransforms,
|
|
369
|
+
): readonly (string | readonly [string, string])[] {
|
|
370
|
+
if (transformsQuery.length > maxTransforms) {
|
|
371
|
+
throw new TypeError(`Expected at most ${maxTransforms} request transforms`)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return transformsQuery.map((transformQuery) =>
|
|
375
|
+
parseSerializedAssetTransformInvocation(transformQuery, transformsByName, (message) => {
|
|
376
|
+
throw new TypeError(message)
|
|
377
|
+
}),
|
|
378
|
+
)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function normalizeAssetTransformInvocation(
|
|
382
|
+
transformInvocation: unknown,
|
|
383
|
+
transformsByName: ResolvedAssetRequestTransformMap,
|
|
384
|
+
onError: (message: string) => never,
|
|
385
|
+
): string {
|
|
386
|
+
if (typeof transformInvocation === 'string') {
|
|
387
|
+
if (!/^[A-Za-z0-9_-]+$/.test(transformInvocation)) {
|
|
388
|
+
return onError('Expected each transform name to use "transform-name" format')
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
let transform = transformsByName.get(transformInvocation)
|
|
392
|
+
if (transform === undefined) {
|
|
393
|
+
return onError(`Unknown file transform "${transformInvocation}"`)
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (transform.param === true) {
|
|
397
|
+
return onError(`File transform "${transformInvocation}" requires a param`)
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return transformInvocation
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (!Array.isArray(transformInvocation)) {
|
|
404
|
+
return onError('Expected each transform to be a string name or tuple')
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (transformInvocation.length === 0 || transformInvocation.length > 2) {
|
|
408
|
+
return onError('Expected each transform tuple to have one or two items')
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
let [name, rawParam] = transformInvocation
|
|
412
|
+
if (typeof name !== 'string' || !/^[A-Za-z0-9_-]+$/.test(name)) {
|
|
413
|
+
return onError('Expected each transform name to use "transform-name" format')
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
let transform = transformsByName.get(name)
|
|
417
|
+
if (transform === undefined) {
|
|
418
|
+
return onError(`Unknown file transform "${name}"`)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (transformInvocation.length === 1) {
|
|
422
|
+
if (transform.param === true) {
|
|
423
|
+
return onError(`File transform "${name}" requires a param`)
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return name
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (typeof rawParam !== 'string') {
|
|
430
|
+
return onError(`Invalid param for file transform "${name}": expected a string`)
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (transform.param === undefined) {
|
|
434
|
+
if (transformInvocation.length === 2) {
|
|
435
|
+
return onError(`File transform "${name}" does not accept a param`)
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return name
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return `${name}:${rawParam}`
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function parseSerializedAssetTransformInvocation(
|
|
445
|
+
transformQuery: string,
|
|
446
|
+
transformsByName: ResolvedAssetRequestTransformMap,
|
|
447
|
+
onError: (message: string) => never,
|
|
448
|
+
): string | readonly [string, string] {
|
|
449
|
+
let separatorIndex = transformQuery.indexOf(':')
|
|
450
|
+
let name = separatorIndex === -1 ? transformQuery : transformQuery.slice(0, separatorIndex)
|
|
451
|
+
let param = separatorIndex === -1 ? undefined : transformQuery.slice(separatorIndex + 1)
|
|
452
|
+
|
|
453
|
+
if (!/^[A-Za-z0-9_-]+$/.test(name)) {
|
|
454
|
+
return onError('Expected each transform name to use "transform-name" format')
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
let transform = transformsByName.get(name)
|
|
458
|
+
if (transform === undefined) {
|
|
459
|
+
return onError(`Unknown file transform "${name}"`)
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (transform.param === undefined) {
|
|
463
|
+
if (param !== undefined) {
|
|
464
|
+
return onError(`File transform "${name}" does not accept a param`)
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return name
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (transform.param === true && param === undefined) {
|
|
471
|
+
return onError(`File transform "${name}" requires a param`)
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (param === undefined) {
|
|
475
|
+
return name
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return [name, param]
|
|
479
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
export type FileSnapshot = {
|
|
2
|
+
filePath: string
|
|
3
|
+
mtimeNs: bigint
|
|
4
|
+
size: bigint
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export type SourceFileMetadata = {
|
|
8
|
+
contentType: string
|
|
9
|
+
etag: string
|
|
10
|
+
extension: string
|
|
11
|
+
fingerprint: string | null
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type SourceFileRecordState = {
|
|
15
|
+
metadata?: SourceFileMetadata
|
|
16
|
+
metadataSnapshot?: FileSnapshot
|
|
17
|
+
identityPath: string
|
|
18
|
+
invalidationVersion: number
|
|
19
|
+
staleMetadata?: SourceFileMetadata
|
|
20
|
+
staleMetadataSnapshot?: FileSnapshot
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type SourceFileRecord = Readonly<SourceFileRecordState>
|
|
24
|
+
|
|
25
|
+
type MutableSourceFileRecord = {
|
|
26
|
+
metadata?: SourceFileMetadata
|
|
27
|
+
metadataSnapshot?: FileSnapshot
|
|
28
|
+
identityPath: string
|
|
29
|
+
invalidationVersion: number
|
|
30
|
+
staleMetadata?: SourceFileMetadata
|
|
31
|
+
staleMetadataSnapshot?: FileSnapshot
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type SourceFileStore = {
|
|
35
|
+
get(identityPath: string): SourceFileRecord
|
|
36
|
+
invalidate(identityPath: string, options?: { retainStale: boolean }): void
|
|
37
|
+
invalidateForFileEvent(filePath: string, event: 'add' | 'change' | 'unlink'): void
|
|
38
|
+
set(identityPath: string, metadata: SourceFileMetadata, snapshot: FileSnapshot | null): void
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function createSourceFileStore(): SourceFileStore {
|
|
42
|
+
let recordsByIdentityPath = new Map<string, MutableSourceFileRecord>()
|
|
43
|
+
let invalidationVersionByIdentityPath = new Map<string, number>()
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
get(identityPath) {
|
|
47
|
+
return getOrCreateRecord(identityPath)
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
invalidate(identityPath, options = { retainStale: false }) {
|
|
51
|
+
let record = recordsByIdentityPath.get(identityPath)
|
|
52
|
+
if (!record) return
|
|
53
|
+
invalidateRecord(record, options)
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
invalidateForFileEvent(filePath, event) {
|
|
57
|
+
let record = recordsByIdentityPath.get(filePath)
|
|
58
|
+
if (!record) return
|
|
59
|
+
|
|
60
|
+
invalidateRecord(record, { retainStale: event === 'change' })
|
|
61
|
+
|
|
62
|
+
if (event === 'unlink') {
|
|
63
|
+
recordsByIdentityPath.delete(filePath)
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
set(identityPath, metadata, snapshot) {
|
|
68
|
+
let record = recordsByIdentityPath.get(identityPath)
|
|
69
|
+
if (!record) {
|
|
70
|
+
record = getOrCreateRecord(identityPath)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
record.metadata = metadata
|
|
74
|
+
record.metadataSnapshot = snapshot ?? undefined
|
|
75
|
+
record.staleMetadata = undefined
|
|
76
|
+
record.staleMetadataSnapshot = undefined
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function invalidateRecord(record: MutableSourceFileRecord, options: { retainStale: boolean }) {
|
|
81
|
+
if (!options.retainStale) {
|
|
82
|
+
record.staleMetadata = undefined
|
|
83
|
+
record.staleMetadataSnapshot = undefined
|
|
84
|
+
} else if (record.metadata && record.metadataSnapshot) {
|
|
85
|
+
record.staleMetadata = record.metadata
|
|
86
|
+
record.staleMetadataSnapshot = record.metadataSnapshot
|
|
87
|
+
} else if (!record.staleMetadata || !record.staleMetadataSnapshot) {
|
|
88
|
+
record.staleMetadata = undefined
|
|
89
|
+
record.staleMetadataSnapshot = undefined
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
record.metadata = undefined
|
|
93
|
+
record.metadataSnapshot = undefined
|
|
94
|
+
record.invalidationVersion += 1
|
|
95
|
+
invalidationVersionByIdentityPath.set(record.identityPath, record.invalidationVersion)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function getOrCreateRecord(identityPath: string): MutableSourceFileRecord {
|
|
99
|
+
let existing = recordsByIdentityPath.get(identityPath)
|
|
100
|
+
if (existing) return existing
|
|
101
|
+
|
|
102
|
+
let record: MutableSourceFileRecord = {
|
|
103
|
+
identityPath,
|
|
104
|
+
invalidationVersion: invalidationVersionByIdentityPath.get(identityPath) ?? 0,
|
|
105
|
+
}
|
|
106
|
+
recordsByIdentityPath.set(identityPath, record)
|
|
107
|
+
return record
|
|
108
|
+
}
|
|
109
|
+
}
|
package/src/lib/fingerprint.ts
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
const fingerprintedExtensionRE = /^(.+)\.@([A-Za-z0-9_-]+)(\.[^./]+)$/
|
|
2
2
|
const fingerprintedBasenameRE = /^(.+)\.@([A-Za-z0-9_-]+)$/
|
|
3
3
|
|
|
4
|
-
export async function hashContent(content: string): Promise<string> {
|
|
5
|
-
let
|
|
6
|
-
let
|
|
7
|
-
let hashBuffer = await crypto.subtle.digest('SHA-256', data)
|
|
4
|
+
export async function hashContent(content: string | Uint8Array<ArrayBufferLike>): Promise<string> {
|
|
5
|
+
let bytes = typeof content === 'string' ? new TextEncoder().encode(content) : Buffer.from(content)
|
|
6
|
+
let hashBuffer = await crypto.subtle.digest('SHA-256', bytes)
|
|
8
7
|
return Buffer.from(hashBuffer).toString('base64url').slice(0, 6)
|
|
9
8
|
}
|
|
10
9
|
|
|
11
|
-
export async function generateFingerprint(
|
|
10
|
+
export async function generateFingerprint(args: {
|
|
12
11
|
buildId: string
|
|
13
|
-
content: string
|
|
12
|
+
content: string | Uint8Array<ArrayBufferLike>
|
|
14
13
|
}): Promise<string> {
|
|
15
|
-
|
|
14
|
+
let content =
|
|
15
|
+
typeof args.content === 'string' ? args.content : Buffer.from(args.content).toString('base64')
|
|
16
|
+
return hashContent(JSON.stringify([content, args.buildId]))
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
export function parseFingerprintSuffix(pathname: string): {
|