@remix-run/assets 0.3.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.
Files changed (61) hide show
  1. package/README.md +206 -8
  2. package/dist/assets.d.ts +1 -0
  3. package/dist/assets.d.ts.map +1 -1
  4. package/dist/assets.js +1 -0
  5. package/dist/lib/access.d.ts +1 -0
  6. package/dist/lib/access.d.ts.map +1 -1
  7. package/dist/lib/access.js +7 -1
  8. package/dist/lib/asset-server.d.ts +29 -6
  9. package/dist/lib/asset-server.d.ts.map +1 -1
  10. package/dist/lib/asset-server.js +176 -17
  11. package/dist/lib/compilation-error.d.ts +1 -1
  12. package/dist/lib/compilation-error.d.ts.map +1 -1
  13. package/dist/lib/files/compiler.d.ts +71 -0
  14. package/dist/lib/files/compiler.d.ts.map +1 -0
  15. package/dist/lib/files/compiler.js +552 -0
  16. package/dist/lib/files/config.d.ts +101 -0
  17. package/dist/lib/files/config.d.ts.map +1 -0
  18. package/dist/lib/files/config.js +219 -0
  19. package/dist/lib/files/store.d.ts +31 -0
  20. package/dist/lib/files/store.d.ts.map +1 -0
  21. package/dist/lib/files/store.js +63 -0
  22. package/dist/lib/fingerprint.d.ts +3 -3
  23. package/dist/lib/fingerprint.d.ts.map +1 -1
  24. package/dist/lib/fingerprint.js +5 -5
  25. package/dist/lib/routes.d.ts.map +1 -1
  26. package/dist/lib/routes.js +28 -20
  27. package/dist/lib/scripts/compiler.d.ts +1 -0
  28. package/dist/lib/scripts/compiler.d.ts.map +1 -1
  29. package/dist/lib/scripts/compiler.js +30 -5
  30. package/dist/lib/scripts/resolve.d.ts.map +1 -1
  31. package/dist/lib/scripts/resolve.js +22 -2
  32. package/dist/lib/scripts/transform.js +1 -1
  33. package/dist/lib/styles/compiler.d.ts +4 -0
  34. package/dist/lib/styles/compiler.d.ts.map +1 -1
  35. package/dist/lib/styles/compiler.js +2 -0
  36. package/dist/lib/styles/emit.d.ts +3 -0
  37. package/dist/lib/styles/emit.d.ts.map +1 -1
  38. package/dist/lib/styles/emit.js +36 -1
  39. package/dist/lib/styles/resolve.d.ts +8 -1
  40. package/dist/lib/styles/resolve.d.ts.map +1 -1
  41. package/dist/lib/styles/resolve.js +85 -32
  42. package/dist/lib/watch.d.ts +2 -0
  43. package/dist/lib/watch.d.ts.map +1 -1
  44. package/dist/lib/watch.js +25 -8
  45. package/package.json +6 -4
  46. package/src/assets.ts +1 -0
  47. package/src/lib/access.ts +8 -1
  48. package/src/lib/asset-server.ts +273 -28
  49. package/src/lib/compilation-error.ts +9 -0
  50. package/src/lib/files/compiler.ts +885 -0
  51. package/src/lib/files/config.ts +479 -0
  52. package/src/lib/files/store.ts +109 -0
  53. package/src/lib/fingerprint.ts +8 -7
  54. package/src/lib/routes.ts +36 -33
  55. package/src/lib/scripts/compiler.ts +43 -5
  56. package/src/lib/scripts/resolve.ts +35 -3
  57. package/src/lib/scripts/transform.ts +1 -1
  58. package/src/lib/styles/compiler.ts +9 -0
  59. package/src/lib/styles/emit.ts +75 -1
  60. package/src/lib/styles/resolve.ts +132 -35
  61. 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
+ }
@@ -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 encoder = new TextEncoder()
6
- let data = encoder.encode(content)
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(options: {
10
+ export async function generateFingerprint(args: {
12
11
  buildId: string
13
- content: string
12
+ content: string | Uint8Array<ArrayBufferLike>
14
13
  }): Promise<string> {
15
- return hashContent(JSON.stringify([options.content, options.buildId]))
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): {