@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.
Files changed (71) hide show
  1. package/README.md +241 -28
  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 +10 -1
  8. package/dist/lib/asset-server.d.ts +33 -7
  9. package/dist/lib/asset-server.d.ts.map +1 -1
  10. package/dist/lib/asset-server.js +195 -23
  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/injected-packages.d.ts +11 -0
  26. package/dist/lib/injected-packages.d.ts.map +1 -0
  27. package/dist/lib/injected-packages.js +86 -0
  28. package/dist/lib/paths.d.ts +1 -0
  29. package/dist/lib/paths.d.ts.map +1 -1
  30. package/dist/lib/paths.js +12 -0
  31. package/dist/lib/routes.d.ts +5 -7
  32. package/dist/lib/routes.d.ts.map +1 -1
  33. package/dist/lib/routes.js +41 -36
  34. package/dist/lib/scripts/compiler.d.ts +1 -0
  35. package/dist/lib/scripts/compiler.d.ts.map +1 -1
  36. package/dist/lib/scripts/compiler.js +30 -5
  37. package/dist/lib/scripts/resolve.d.ts.map +1 -1
  38. package/dist/lib/scripts/resolve.js +57 -10
  39. package/dist/lib/scripts/transform.d.ts.map +1 -1
  40. package/dist/lib/scripts/transform.js +79 -3
  41. package/dist/lib/styles/compiler.d.ts +4 -0
  42. package/dist/lib/styles/compiler.d.ts.map +1 -1
  43. package/dist/lib/styles/compiler.js +2 -0
  44. package/dist/lib/styles/emit.d.ts +3 -0
  45. package/dist/lib/styles/emit.d.ts.map +1 -1
  46. package/dist/lib/styles/emit.js +36 -1
  47. package/dist/lib/styles/resolve.d.ts +8 -1
  48. package/dist/lib/styles/resolve.d.ts.map +1 -1
  49. package/dist/lib/styles/resolve.js +85 -32
  50. package/dist/lib/watch.d.ts +2 -0
  51. package/dist/lib/watch.d.ts.map +1 -1
  52. package/dist/lib/watch.js +25 -8
  53. package/package.json +11 -5
  54. package/src/assets.ts +1 -0
  55. package/src/lib/access.ts +10 -1
  56. package/src/lib/asset-server.ts +297 -34
  57. package/src/lib/compilation-error.ts +9 -0
  58. package/src/lib/files/compiler.ts +885 -0
  59. package/src/lib/files/config.ts +479 -0
  60. package/src/lib/files/store.ts +109 -0
  61. package/src/lib/fingerprint.ts +8 -7
  62. package/src/lib/injected-packages.ts +117 -0
  63. package/src/lib/paths.ts +28 -0
  64. package/src/lib/routes.ts +67 -55
  65. package/src/lib/scripts/compiler.ts +43 -5
  66. package/src/lib/scripts/resolve.ts +89 -11
  67. package/src/lib/scripts/transform.ts +105 -3
  68. package/src/lib/styles/compiler.ts +9 -0
  69. package/src/lib/styles/emit.ts +75 -1
  70. package/src/lib/styles/resolve.ts +132 -35
  71. package/src/lib/watch.ts +34 -12
@@ -0,0 +1,552 @@
1
+ import * as fs from 'node:fs';
2
+ import * as fsp from 'node:fs/promises';
3
+ import * as path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { IfNoneMatch } from '@remix-run/headers';
6
+ import { detectContentType } from '@remix-run/mime';
7
+ import { createAssetServerCompilationError, isAssetServerCompilationError, } from "../compilation-error.js";
8
+ import { formatFingerprintedPathname, generateFingerprint, hashContent } from "../fingerprint.js";
9
+ import { normalizeFilePath, resolveFilePath } from "../paths.js";
10
+ import { parseAssetTransformInvocations } from "./config.js";
11
+ import { createSourceFileStore } from "./store.js";
12
+ export function createFileCompiler(options) {
13
+ let resolvedOptions = {
14
+ ...options,
15
+ extensionSet: new Set(options.extensions),
16
+ };
17
+ let sourceFileStore = createSourceFileStore();
18
+ let sourceFileInFlightByCacheKey = new Map();
19
+ let transformedAssetMetadataByCacheKey = new Map();
20
+ let transformedCacheKeysByIdentityPath = new Map();
21
+ let transformedEmitInFlightByCacheKey = new Map();
22
+ let cacheEpoch = resolvedOptions.buildId ?? crypto.randomUUID();
23
+ let resolveArgs = {
24
+ extensions: resolvedOptions.extensionSet,
25
+ isAllowed: resolvedOptions.isAllowed,
26
+ routes: resolvedOptions.routes,
27
+ };
28
+ return {
29
+ async getFile(filePath, getOptions) {
30
+ let resolvedFile = resolveServedFileOrThrow(resolveInputFilePath(filePath), resolveArgs);
31
+ let record = getFreshSourceFileRecord(resolvedFile.identityPath);
32
+ if (shouldUseTransformPipeline(getOptions.transform)) {
33
+ let cacheKey = getTransformedRecordCacheKey(cacheEpoch, record, getOptions.transform);
34
+ let notModified = getNotModifiedFile(transformedAssetMetadataByCacheKey.get(cacheKey), getOptions);
35
+ if (notModified)
36
+ return notModified;
37
+ let transformedFile = await getOrCreateTransformedFile(record, getOptions.transform);
38
+ notModified = getNotModifiedFile(toEmittedFileMetadata(transformedFile), getOptions);
39
+ if (notModified)
40
+ return notModified;
41
+ return {
42
+ file: transformedFile,
43
+ type: 'file',
44
+ };
45
+ }
46
+ let notModified = getNotModifiedFile(record.metadata, getOptions);
47
+ if (notModified)
48
+ return notModified;
49
+ if (record.staleMetadata &&
50
+ record.staleMetadataSnapshot &&
51
+ isFileSnapshotFresh(record.staleMetadataSnapshot)) {
52
+ let staleNotModified = getNotModifiedFile(record.staleMetadata, getOptions);
53
+ if (staleNotModified)
54
+ return staleNotModified;
55
+ }
56
+ let file = await getOrCreateSourceFile(record);
57
+ return {
58
+ file,
59
+ type: 'file',
60
+ };
61
+ },
62
+ async getHref(filePath, hrefOptions) {
63
+ let resolvedFile = resolveServedFileOrThrow(resolveInputFilePath(filePath), resolveArgs);
64
+ let record = getFreshSourceFileRecord(resolvedFile.identityPath);
65
+ let href = resolvedOptions.fingerprintAssets
66
+ ? formatFingerprintedPathname(resolvedFile.stableUrlPathname, (await getOrCreateSourceFileMetadata(record)).fingerprint)
67
+ : resolvedFile.stableUrlPathname;
68
+ if (shouldUseTransformPipeline(hrefOptions.transform)) {
69
+ return appendTransformQuery(href, hrefOptions.transform);
70
+ }
71
+ return href;
72
+ },
73
+ async handleFileEvent(filePath, event) {
74
+ if (!isServedFilePath(filePath, resolvedOptions.extensionSet))
75
+ return;
76
+ sourceFileStore.invalidateForFileEvent(filePath, event);
77
+ clearTransformedCacheIndex(filePath);
78
+ },
79
+ isServedFilePath(filePath) {
80
+ return isServedFilePath(filePath, resolvedOptions.extensionSet);
81
+ },
82
+ validateTransformQuery(transformQuery) {
83
+ parseRequestTransforms(transformQuery, resolvedOptions.transforms, resolvedOptions.maxRequestTransforms);
84
+ },
85
+ };
86
+ function resolveInputFilePath(filePath) {
87
+ if (filePath.startsWith('file://')) {
88
+ return normalizeFilePath(fileURLToPath(new URL(filePath)));
89
+ }
90
+ if (filePath.includes('://')) {
91
+ throw new TypeError(`Expected a file path or file:// URL, received "${filePath}"`);
92
+ }
93
+ return resolveFilePath(resolvedOptions.rootDir, filePath);
94
+ }
95
+ function shouldUseTransformPipeline(transformQuery) {
96
+ return ((transformQuery !== null && transformQuery.length > 0) ||
97
+ resolvedOptions.globalTransforms.length > 0);
98
+ }
99
+ function getFreshSourceFileRecord(identityPath) {
100
+ let record = sourceFileStore.get(identityPath);
101
+ if (record.metadataSnapshot && !isFileSnapshotFresh(record.metadataSnapshot)) {
102
+ sourceFileStore.invalidate(identityPath);
103
+ clearTransformedCacheIndex(identityPath);
104
+ record = sourceFileStore.get(identityPath);
105
+ }
106
+ return record;
107
+ }
108
+ async function getOrCreateSourceFile(record) {
109
+ let cacheKey = getRecordCacheKey(record);
110
+ let existing = sourceFileInFlightByCacheKey.get(cacheKey);
111
+ if (existing)
112
+ return existing;
113
+ let promise = (async () => {
114
+ let startedVersion = record.invalidationVersion;
115
+ let body = await readFileContents(record.identityPath);
116
+ let sourceFile = record.metadata
117
+ ? createEmittedFileFromMetadata(body, record.metadata)
118
+ : await createSourceFile(body, record.identityPath);
119
+ if (record.invalidationVersion === startedVersion && !record.metadata) {
120
+ sourceFileStore.set(record.identityPath, toEmittedFileMetadata(sourceFile), getFileSnapshot(record.identityPath));
121
+ }
122
+ return sourceFile;
123
+ })();
124
+ sourceFileInFlightByCacheKey.set(cacheKey, promise);
125
+ try {
126
+ return await promise;
127
+ }
128
+ finally {
129
+ if (sourceFileInFlightByCacheKey.get(cacheKey) === promise) {
130
+ sourceFileInFlightByCacheKey.delete(cacheKey);
131
+ }
132
+ }
133
+ }
134
+ async function getOrCreateSourceFileMetadata(record) {
135
+ if (record.metadata)
136
+ return record.metadata;
137
+ return toEmittedFileMetadata(await getOrCreateSourceFile(record));
138
+ }
139
+ async function getOrCreateTransformedFile(record, transformQuery) {
140
+ let parsedTransforms = parseRequestTransforms(transformQuery, resolvedOptions.transforms, resolvedOptions.maxRequestTransforms);
141
+ let cacheKey = getTransformedRecordCacheKey(cacheEpoch, record, transformQuery);
142
+ let existing = transformedEmitInFlightByCacheKey.get(cacheKey);
143
+ if (existing)
144
+ return existing;
145
+ let promise = (async () => {
146
+ let sourceFile = await getOrCreateSourceFile(record);
147
+ let cachedFile = await getCachedTransformedFile(cacheKey, record.identityPath, sourceFile.fingerprint);
148
+ if (cachedFile)
149
+ return cachedFile;
150
+ let transformedFile = await applyTransforms(record.identityPath, sourceFile, parsedTransforms);
151
+ if (transformedFile === null) {
152
+ return sourceFile;
153
+ }
154
+ let emittedFile = await createEmittedFile(transformedFile.body, {
155
+ extension: transformedFile.extension,
156
+ filePath: record.identityPath,
157
+ fingerprint: sourceFile.fingerprint,
158
+ });
159
+ rememberTransformedAssetMetadata(cacheKey, record.identityPath, toEmittedFileMetadata(emittedFile));
160
+ await setCachedTransformedFile(cacheKey, record.identityPath, emittedFile);
161
+ return emittedFile;
162
+ })();
163
+ transformedEmitInFlightByCacheKey.set(cacheKey, promise);
164
+ try {
165
+ return await promise;
166
+ }
167
+ finally {
168
+ if (transformedEmitInFlightByCacheKey.get(cacheKey) === promise) {
169
+ transformedEmitInFlightByCacheKey.delete(cacheKey);
170
+ }
171
+ }
172
+ }
173
+ async function createSourceFile(body, identityPath) {
174
+ return createEmittedFile(body, {
175
+ extension: path.extname(identityPath).toLowerCase(),
176
+ filePath: identityPath,
177
+ fingerprint: resolvedOptions.fingerprintAssets && resolvedOptions.buildId
178
+ ? await generateFingerprint({
179
+ buildId: resolvedOptions.buildId,
180
+ content: body,
181
+ })
182
+ : null,
183
+ });
184
+ }
185
+ function createEmittedFileFromMetadata(body, metadata) {
186
+ return {
187
+ body,
188
+ ...metadata,
189
+ };
190
+ }
191
+ async function applyTransforms(filePath, source, transforms) {
192
+ let currentBody = source.body;
193
+ let currentExtension = source.extension;
194
+ let appliedTransform = false;
195
+ for (let requestTransform of transforms) {
196
+ let transform = resolvedOptions.transforms.get(requestTransform.name);
197
+ if (transform === undefined) {
198
+ throw createAssetServerCompilationError(`Unknown file transform "${requestTransform.name}" requested for ${filePath}`, { code: 'FILE_TRANSFORM_QUERY_INVALID' });
199
+ }
200
+ if (!supportsTransformExtension(transform.extensions, currentExtension)) {
201
+ throw createAssetServerCompilationError(`File transform "${requestTransform.name}" does not support ${currentExtension} inputs for ${filePath}`, { code: 'FILE_TRANSFORM_NOT_SUPPORTED' });
202
+ }
203
+ let result;
204
+ let transformSubject = {
205
+ kind: 'request',
206
+ name: requestTransform.name,
207
+ };
208
+ try {
209
+ result = await transform.transform(currentBody, {
210
+ extension: currentExtension,
211
+ filePath,
212
+ param: requestTransform.param,
213
+ });
214
+ }
215
+ catch (error) {
216
+ throw toFileTransformFailedError(error, filePath, transformSubject);
217
+ }
218
+ let normalizedResult = normalizeTransformResult(result, {
219
+ currentExtension,
220
+ filePath,
221
+ transformSubject,
222
+ });
223
+ currentBody = normalizedResult.content;
224
+ currentExtension = normalizedResult.extension;
225
+ appliedTransform = true;
226
+ }
227
+ for (let [index, globalTransform] of resolvedOptions.globalTransforms.entries()) {
228
+ if (!supportsTransformExtension(globalTransform.extensions, currentExtension))
229
+ continue;
230
+ let result;
231
+ let transformSubject = getGlobalFileTransformSubject(globalTransform, index);
232
+ try {
233
+ result = await globalTransform.transform(currentBody, {
234
+ extension: currentExtension,
235
+ filePath,
236
+ });
237
+ }
238
+ catch (error) {
239
+ throw toFileTransformFailedError(error, filePath, transformSubject);
240
+ }
241
+ if (result === null)
242
+ continue;
243
+ let normalizedResult = normalizeTransformResult(result, {
244
+ currentExtension,
245
+ filePath,
246
+ transformSubject,
247
+ });
248
+ currentBody = normalizedResult.content;
249
+ currentExtension = normalizedResult.extension;
250
+ appliedTransform = true;
251
+ }
252
+ if (!appliedTransform)
253
+ return null;
254
+ return { body: currentBody, extension: currentExtension };
255
+ }
256
+ async function getCachedTransformedFile(cacheKey, identityPath, fingerprint) {
257
+ if (!resolvedOptions.cache)
258
+ return null;
259
+ let file = await resolvedOptions.cache.get(cacheKey);
260
+ if (!file)
261
+ return null;
262
+ let body = new Uint8Array(await file.arrayBuffer());
263
+ let metadata = transformedAssetMetadataByCacheKey.get(cacheKey) ??
264
+ (await createEmittedFileMetadata(body, {
265
+ extension: path.extname(file.name).toLowerCase(),
266
+ filePath: identityPath,
267
+ fingerprint,
268
+ }));
269
+ rememberTransformedAssetMetadata(cacheKey, identityPath, metadata);
270
+ return {
271
+ body,
272
+ ...metadata,
273
+ };
274
+ }
275
+ async function setCachedTransformedFile(cacheKey, filePath, emittedFile) {
276
+ if (!resolvedOptions.cache)
277
+ return;
278
+ let basename = path.basename(filePath, path.extname(filePath));
279
+ await resolvedOptions.cache.set(cacheKey, new File([Buffer.from(emittedFile.body)], `${basename}${emittedFile.extension}`, {
280
+ type: emittedFile.contentType,
281
+ }));
282
+ }
283
+ async function createEmittedFile(body, options) {
284
+ let metadata = await createEmittedFileMetadata(body, options);
285
+ return {
286
+ body,
287
+ ...metadata,
288
+ };
289
+ }
290
+ function clearTransformedCacheIndex(identityPath) {
291
+ let cacheKeys = transformedCacheKeysByIdentityPath.get(identityPath);
292
+ if (!cacheKeys)
293
+ return;
294
+ for (let cacheKey of cacheKeys) {
295
+ transformedAssetMetadataByCacheKey.delete(cacheKey);
296
+ }
297
+ transformedCacheKeysByIdentityPath.delete(identityPath);
298
+ }
299
+ function rememberTransformedAssetMetadata(cacheKey, identityPath, metadata) {
300
+ transformedAssetMetadataByCacheKey.set(cacheKey, metadata);
301
+ let cacheKeys = transformedCacheKeysByIdentityPath.get(identityPath) ?? new Set();
302
+ cacheKeys.add(cacheKey);
303
+ transformedCacheKeysByIdentityPath.set(identityPath, cacheKeys);
304
+ }
305
+ async function createEmittedFileMetadata(body, options) {
306
+ let contentType = detectContentType(`file${options.extension}`) ??
307
+ detectContentType(options.filePath) ??
308
+ 'application/octet-stream';
309
+ return {
310
+ contentType,
311
+ etag: `W/"${await hashContent(body)}"`,
312
+ extension: options.extension,
313
+ fingerprint: options.fingerprint,
314
+ };
315
+ }
316
+ }
317
+ export function resolveServedFileOrThrow(filePath, args) {
318
+ let identityPath = resolveExistingFilePath(filePath);
319
+ if (!identityPath) {
320
+ throw createAssetServerCompilationError(`File not found: ${filePath}`, {
321
+ code: 'FILE_NOT_FOUND',
322
+ });
323
+ }
324
+ if (!isServedFilePath(identityPath, args.extensions)) {
325
+ throw createAssetServerCompilationError(`File type is not supported: ${identityPath}`, {
326
+ code: 'FILE_NOT_SUPPORTED',
327
+ });
328
+ }
329
+ if (!args.isAllowed(identityPath)) {
330
+ throw createAssetServerCompilationError(`File is not allowed: ${identityPath}`, {
331
+ code: 'FILE_NOT_ALLOWED',
332
+ });
333
+ }
334
+ let stableUrlPathname = args.routes.toUrlPathname(identityPath);
335
+ if (!stableUrlPathname) {
336
+ throw createAssetServerCompilationError(`File ${identityPath} is outside all configured fileMap entries.`, {
337
+ code: 'FILE_OUTSIDE_FILE_MAP',
338
+ });
339
+ }
340
+ return {
341
+ identityPath,
342
+ stableUrlPathname,
343
+ };
344
+ }
345
+ export function createResponseForFile(result, options) {
346
+ if (IfNoneMatch.from(options.ifNoneMatch).matches(result.etag)) {
347
+ return new Response(null, { status: 304, headers: { ETag: result.etag } });
348
+ }
349
+ return new Response(options.method === 'HEAD' ? null : Buffer.from(result.body), {
350
+ headers: {
351
+ 'Cache-Control': options.cacheControl,
352
+ 'Content-Type': result.contentType,
353
+ ETag: result.etag,
354
+ },
355
+ });
356
+ }
357
+ export function isServedFilePath(filePath, extensions) {
358
+ return extensions.has(path.extname(filePath).toLowerCase());
359
+ }
360
+ function getRecordCacheKey(record) {
361
+ return `${record.identityPath}\0${record.invalidationVersion}`;
362
+ }
363
+ function getTransformedRecordCacheKey(cacheEpoch, record, transformQuery) {
364
+ return [
365
+ encodeCacheKeyPart(cacheEpoch),
366
+ encodeCacheKeyPart(record.identityPath),
367
+ String(record.invalidationVersion),
368
+ encodeCacheKeyPart(JSON.stringify(transformQuery ?? [])),
369
+ ].join('/');
370
+ }
371
+ function getFileSnapshot(filePath) {
372
+ try {
373
+ let stats = fs.statSync(filePath, { bigint: true });
374
+ if (!stats.isFile())
375
+ return null;
376
+ return {
377
+ filePath,
378
+ mtimeNs: stats.mtimeNs,
379
+ size: stats.size,
380
+ };
381
+ }
382
+ catch (error) {
383
+ if (isNoEntityError(error))
384
+ return null;
385
+ throw error;
386
+ }
387
+ }
388
+ function getNotModifiedFile(emittedFile, options) {
389
+ if (!emittedFile || options.ifNoneMatch === null)
390
+ return null;
391
+ if (options.requestedFingerprint !== null &&
392
+ emittedFile.fingerprint !== options.requestedFingerprint) {
393
+ return null;
394
+ }
395
+ if (!IfNoneMatch.from(options.ifNoneMatch).matches(emittedFile.etag))
396
+ return null;
397
+ return { etag: emittedFile.etag, type: 'not-modified' };
398
+ }
399
+ function toEmittedFileMetadata(emittedFile) {
400
+ return {
401
+ contentType: emittedFile.contentType,
402
+ etag: emittedFile.etag,
403
+ extension: emittedFile.extension,
404
+ fingerprint: emittedFile.fingerprint,
405
+ };
406
+ }
407
+ function isFileSnapshotFresh(snapshot) {
408
+ let current = getFileSnapshot(snapshot.filePath);
409
+ return current != null && current.mtimeNs === snapshot.mtimeNs && current.size === snapshot.size;
410
+ }
411
+ function appendTransformQuery(href, transformQuery) {
412
+ if (transformQuery === null)
413
+ return href;
414
+ let searchParams = new URLSearchParams();
415
+ for (let transform of transformQuery) {
416
+ searchParams.append('transform', transform);
417
+ }
418
+ let search = searchParams.toString();
419
+ return search.length > 0 ? `${href}?${search}` : href;
420
+ }
421
+ function encodeCacheKeyPart(value) {
422
+ return Buffer.from(value).toString('base64url');
423
+ }
424
+ function normalizeTransformResult(result, options) {
425
+ if (typeof result === 'string') {
426
+ return {
427
+ content: new TextEncoder().encode(result),
428
+ extension: options.currentExtension,
429
+ };
430
+ }
431
+ if (result instanceof Uint8Array) {
432
+ return {
433
+ content: result,
434
+ extension: options.currentExtension,
435
+ };
436
+ }
437
+ if (result === null || typeof result !== 'object') {
438
+ throw createAssetServerCompilationError(`${formatFileTransformSubject(options.transformSubject)} must return a string, Uint8Array, or object for ${options.filePath}`, { code: 'FILE_TRANSFORM_RESULT_INVALID' });
439
+ }
440
+ if (!('content' in result) ||
441
+ (typeof result.content !== 'string' && !(result.content instanceof Uint8Array))) {
442
+ throw createAssetServerCompilationError(`${formatFileTransformSubject(options.transformSubject)} must return a string or Uint8Array content value for ${options.filePath}`, { code: 'FILE_TRANSFORM_RESULT_INVALID' });
443
+ }
444
+ let extension = options.currentExtension;
445
+ if ('extension' in result && result.extension !== undefined) {
446
+ if (typeof result.extension !== 'string') {
447
+ throw createAssetServerCompilationError(`${formatFileTransformSubject(options.transformSubject)} must return a string extension for ${options.filePath}`, { code: 'FILE_TRANSFORM_RESULT_INVALID' });
448
+ }
449
+ extension = normalizeTransformExtension(result.extension, options.filePath, options.transformSubject);
450
+ }
451
+ return {
452
+ content: typeof result.content === 'string'
453
+ ? new TextEncoder().encode(result.content)
454
+ : result.content,
455
+ extension,
456
+ };
457
+ }
458
+ function normalizeTransformExtension(extension, filePath, transformSubject) {
459
+ let normalizedExtension = extension.trim().toLowerCase();
460
+ if (!/^\.[A-Za-z0-9_-]+$/.test(normalizedExtension)) {
461
+ throw createAssetServerCompilationError(`${formatFileTransformSubject(transformSubject)} returned an invalid extension "${extension}" for ${filePath}`, { code: 'FILE_TRANSFORM_RESULT_INVALID' });
462
+ }
463
+ return normalizedExtension;
464
+ }
465
+ function toFileTransformFailedError(error, filePath, transformSubject) {
466
+ if (isAssetServerCompilationError(error))
467
+ return error;
468
+ return createAssetServerCompilationError(`${formatFileTransformSubject(transformSubject)} failed for ${filePath}. ${formatUnknownError(error)}`, {
469
+ cause: error,
470
+ code: 'FILE_TRANSFORM_FAILED',
471
+ });
472
+ }
473
+ function getGlobalFileTransformSubject(globalTransform, index) {
474
+ let functionName = globalTransform.transform.name;
475
+ return {
476
+ index,
477
+ kind: 'global',
478
+ name: globalTransform.name ?? (functionName.length > 0 ? functionName : undefined),
479
+ };
480
+ }
481
+ function formatFileTransformSubject(transformSubject) {
482
+ if (transformSubject.kind === 'request') {
483
+ return `File transform "${transformSubject.name}"`;
484
+ }
485
+ return transformSubject.name === undefined
486
+ ? `Global file transform at index ${transformSubject.index}`
487
+ : `Global file transform "${transformSubject.name}"`;
488
+ }
489
+ function formatUnknownError(error) {
490
+ return error instanceof Error ? error.message : String(error);
491
+ }
492
+ function supportsTransformExtension(extensions, currentExtension) {
493
+ return extensions === undefined || extensions.includes(currentExtension);
494
+ }
495
+ function parseRequestTransforms(transformQuery, transforms, maxRequestTransforms) {
496
+ if (transformQuery === null)
497
+ return [];
498
+ try {
499
+ return parseAssetTransformInvocations(transformQuery, transforms, maxRequestTransforms).map((transformInvocation) => typeof transformInvocation === 'string'
500
+ ? {
501
+ name: transformInvocation,
502
+ param: undefined,
503
+ }
504
+ : {
505
+ name: transformInvocation[0],
506
+ param: transformInvocation[1],
507
+ });
508
+ }
509
+ catch (error) {
510
+ throw createAssetServerCompilationError(error instanceof Error ? error.message : 'Invalid file transforms', {
511
+ code: 'FILE_TRANSFORM_QUERY_INVALID',
512
+ });
513
+ }
514
+ }
515
+ function resolveExistingFilePath(filePath) {
516
+ try {
517
+ return normalizeFilePath(fs.realpathSync(filePath));
518
+ }
519
+ catch (error) {
520
+ if (isNoEntityError(error))
521
+ return null;
522
+ throw error;
523
+ }
524
+ }
525
+ function isNoEntityError(error) {
526
+ return (error instanceof Error &&
527
+ 'code' in error &&
528
+ (error.code === 'ENOENT' ||
529
+ error.code === 'ENOTDIR'));
530
+ }
531
+ async function readFileContents(identityPath) {
532
+ try {
533
+ return new Uint8Array(await fsp.readFile(identityPath));
534
+ }
535
+ catch (error) {
536
+ if (isNoEntityError(error)) {
537
+ throw createAssetServerCompilationError(`File not found: ${identityPath}`, {
538
+ cause: error,
539
+ code: 'FILE_NOT_FOUND',
540
+ });
541
+ }
542
+ throw toEmitError(error, identityPath);
543
+ }
544
+ }
545
+ function toEmitError(error, identityPath) {
546
+ if (isAssetServerCompilationError(error))
547
+ return error;
548
+ return createAssetServerCompilationError(`Failed to read file ${identityPath}. ${error instanceof Error ? error.message : String(error)}`, {
549
+ cause: error,
550
+ code: 'EMIT_FAILED',
551
+ });
552
+ }
@@ -0,0 +1,101 @@
1
+ import type { FileStorage } from '@remix-run/file-storage';
2
+ export interface AssetFileTransformResult {
3
+ content: string | Uint8Array;
4
+ extension?: string;
5
+ }
6
+ type AssetRequestTransformParamMode = true | 'optional' | undefined;
7
+ declare const assetRequestTransformTypes: unique symbol;
8
+ type AssetRequestTransformTypes<param extends string, mode extends AssetRequestTransformParamMode> = {
9
+ input: param;
10
+ mode: mode;
11
+ };
12
+ export interface AssetTransformContext {
13
+ extension: string;
14
+ filePath: string;
15
+ }
16
+ type AssetRequestTransformRuntimeParam<mode extends AssetRequestTransformParamMode> = mode extends true ? string : mode extends 'optional' ? string | undefined : undefined;
17
+ export interface AssetRequestTransformContext<mode extends AssetRequestTransformParamMode = undefined> extends AssetTransformContext {
18
+ param: AssetRequestTransformRuntimeParam<mode>;
19
+ }
20
+ export interface AssetGlobalTransformContext extends AssetTransformContext {
21
+ }
22
+ export type AssetRequestTransform<param extends string = string, mode extends AssetRequestTransformParamMode = undefined> = {
23
+ readonly [assetRequestTransformTypes]?: AssetRequestTransformTypes<param, mode>;
24
+ /**
25
+ * Optional list of file extensions this transform accepts. Values must use `.ext` format.
26
+ * Matching is evaluated against the current extension at this point in the transform pipeline.
27
+ */
28
+ extensions?: readonly string[];
29
+ transform(bytes: Uint8Array, context: AssetRequestTransformContext<mode>): string | Uint8Array | AssetFileTransformResult | Promise<string | Uint8Array | AssetFileTransformResult>;
30
+ } & (mode extends undefined ? {
31
+ param?: undefined;
32
+ } : {
33
+ param: mode;
34
+ });
35
+ type AssetGlobalTransformHandler = (bytes: Uint8Array, context: AssetGlobalTransformContext) => string | Uint8Array | AssetFileTransformResult | null | Promise<string | Uint8Array | AssetFileTransformResult | null>;
36
+ export type AssetGlobalTransform = AssetGlobalTransformHandler | {
37
+ /**
38
+ * Optional list of file extensions this transform accepts. Values must use `.ext` format.
39
+ * Non-matching files are skipped automatically.
40
+ */
41
+ extensions?: readonly string[];
42
+ name?: string;
43
+ transform: AssetGlobalTransformHandler;
44
+ };
45
+ interface ResolvedAssetGlobalTransform {
46
+ extensions?: readonly string[];
47
+ name?: string;
48
+ transform: AssetGlobalTransformHandler;
49
+ }
50
+ export type AssetRequestTransformMap = Readonly<Record<string, AssetRequestTransform<string, AssetRequestTransformParamMode>>>;
51
+ export type ResolvedAssetRequestTransformMap = ReadonlyMap<string, AssetRequestTransform<string, AssetRequestTransformParamMode>>;
52
+ export interface AssetServerFilesOptions<transforms extends AssetRequestTransformMap = {}> {
53
+ /**
54
+ * File extensions to expose as leaf assets. Values must include the leading dot,
55
+ * for example `['.png', '.svg', '.woff2']`.
56
+ */
57
+ extensions: readonly string[];
58
+ /**
59
+ * Named transforms that can be requested from asset URLs.
60
+ */
61
+ transforms?: transforms;
62
+ /**
63
+ * Maximum number of request transforms allowed in a single asset URL.
64
+ * Defaults to `5`.
65
+ */
66
+ maxRequestTransforms?: number;
67
+ /**
68
+ * Ordered transforms that run for every served file asset and may return `null`
69
+ * to skip themselves for a given input.
70
+ */
71
+ globalTransforms?: readonly AssetGlobalTransform[];
72
+ /**
73
+ * Optional backing store for cached transformed file outputs.
74
+ */
75
+ cache?: FileStorage;
76
+ }
77
+ export interface ResolvedAssetServerFilesOptions {
78
+ cache?: FileStorage;
79
+ extensions: readonly string[];
80
+ globalTransforms: readonly ResolvedAssetGlobalTransform[];
81
+ hasTransforms: boolean;
82
+ maxRequestTransforms: number;
83
+ transforms: ResolvedAssetRequestTransformMap;
84
+ }
85
+ type AssetTransformStep<name extends string, param extends string, mode extends AssetRequestTransformParamMode> = mode extends true ? readonly [name, param] : mode extends 'optional' ? name | readonly [name] | readonly [name, param] : name | readonly [name];
86
+ export type AssetTransformInvocation<transforms extends AssetRequestTransformMap> = {
87
+ [name in keyof transforms & string]: transforms[name] extends {
88
+ readonly [assetRequestTransformTypes]?: infer types;
89
+ } ? types extends {
90
+ input: infer param extends string;
91
+ mode: infer mode extends AssetRequestTransformParamMode;
92
+ } ? AssetTransformStep<name, param, mode> : never : never;
93
+ }[keyof transforms & string];
94
+ export declare function defineFileTransform(transform: AssetRequestTransform<string, undefined>): AssetRequestTransform<string, undefined>;
95
+ export declare function defineFileTransform<const param extends string = string>(transform: AssetRequestTransform<param, true>): AssetRequestTransform<param, true>;
96
+ export declare function defineFileTransform<const param extends string = string>(transform: AssetRequestTransform<param, 'optional'>): AssetRequestTransform<param, 'optional'>;
97
+ export declare function normalizeFilesOptions<transforms extends AssetRequestTransformMap>(files: AssetServerFilesOptions<transforms> | undefined): ResolvedAssetServerFilesOptions;
98
+ export declare function serializeAssetTransformInvocations<transforms extends AssetRequestTransformMap>(transforms: readonly AssetTransformInvocation<transforms>[], transformsByName: ResolvedAssetRequestTransformMap, maxTransforms?: number): string[];
99
+ export declare function parseAssetTransformInvocations(transformsQuery: readonly string[], transformsByName: ResolvedAssetRequestTransformMap, maxTransforms?: number): readonly (string | readonly [string, string])[];
100
+ export {};
101
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../src/lib/files/config.ts"],"names":[],"mappings":"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"}