fidelity-kit 0.1.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 (90) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +15 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/bin.d.ts +3 -0
  5. package/dist/bin.d.ts.map +1 -0
  6. package/dist/bin.js +13 -0
  7. package/dist/bin.js.map +1 -0
  8. package/dist/commands/build.d.ts +8 -0
  9. package/dist/commands/build.d.ts.map +1 -0
  10. package/dist/commands/build.js +32 -0
  11. package/dist/commands/build.js.map +1 -0
  12. package/dist/commands/dev.d.ts +10 -0
  13. package/dist/commands/dev.d.ts.map +1 -0
  14. package/dist/commands/dev.js +17 -0
  15. package/dist/commands/dev.js.map +1 -0
  16. package/dist/commands/hash.d.ts +6 -0
  17. package/dist/commands/hash.d.ts.map +1 -0
  18. package/dist/commands/hash.js +23 -0
  19. package/dist/commands/hash.js.map +1 -0
  20. package/dist/commands/process.d.ts +6 -0
  21. package/dist/commands/process.d.ts.map +1 -0
  22. package/dist/commands/process.js +22 -0
  23. package/dist/commands/process.js.map +1 -0
  24. package/dist/commands/serve.d.ts +14 -0
  25. package/dist/commands/serve.d.ts.map +1 -0
  26. package/dist/commands/serve.js +28 -0
  27. package/dist/commands/serve.js.map +1 -0
  28. package/dist/core/compare.d.ts +32 -0
  29. package/dist/core/compare.d.ts.map +1 -0
  30. package/dist/core/compare.js +81 -0
  31. package/dist/core/compare.js.map +1 -0
  32. package/dist/core/etag.d.ts +39 -0
  33. package/dist/core/etag.d.ts.map +1 -0
  34. package/dist/core/etag.js +121 -0
  35. package/dist/core/etag.js.map +1 -0
  36. package/dist/core/hash.d.ts +30 -0
  37. package/dist/core/hash.d.ts.map +1 -0
  38. package/dist/core/hash.js +65 -0
  39. package/dist/core/hash.js.map +1 -0
  40. package/dist/core/hash.test.d.ts +2 -0
  41. package/dist/core/hash.test.d.ts.map +1 -0
  42. package/dist/core/hash.test.js +71 -0
  43. package/dist/core/hash.test.js.map +1 -0
  44. package/dist/core/index.d.ts +8 -0
  45. package/dist/core/index.d.ts.map +1 -0
  46. package/dist/core/index.js +8 -0
  47. package/dist/core/index.js.map +1 -0
  48. package/dist/core/paths.d.ts +7 -0
  49. package/dist/core/paths.d.ts.map +1 -0
  50. package/dist/core/paths.js +12 -0
  51. package/dist/core/paths.js.map +1 -0
  52. package/dist/core/process.d.ts +24 -0
  53. package/dist/core/process.d.ts.map +1 -0
  54. package/dist/core/process.js +65 -0
  55. package/dist/core/process.js.map +1 -0
  56. package/dist/core/scan.d.ts +26 -0
  57. package/dist/core/scan.d.ts.map +1 -0
  58. package/dist/core/scan.js +55 -0
  59. package/dist/core/scan.js.map +1 -0
  60. package/dist/core/schema.d.ts +23 -0
  61. package/dist/core/schema.d.ts.map +1 -0
  62. package/dist/core/schema.js +19 -0
  63. package/dist/core/schema.js.map +1 -0
  64. package/dist/core/suite.test.d.ts +2 -0
  65. package/dist/core/suite.test.d.ts.map +1 -0
  66. package/dist/core/suite.test.js +41 -0
  67. package/dist/core/suite.test.js.map +1 -0
  68. package/dist/index.d.ts +5 -0
  69. package/dist/index.d.ts.map +1 -0
  70. package/dist/index.js +29 -0
  71. package/dist/index.js.map +1 -0
  72. package/dist/run.d.ts +11 -0
  73. package/dist/run.d.ts.map +1 -0
  74. package/dist/run.js +23 -0
  75. package/dist/run.js.map +1 -0
  76. package/dist/server.d.ts +34 -0
  77. package/dist/server.d.ts.map +1 -0
  78. package/dist/server.js +90 -0
  79. package/dist/server.js.map +1 -0
  80. package/dist/server.test.d.ts +2 -0
  81. package/dist/server.test.d.ts.map +1 -0
  82. package/dist/server.test.js +162 -0
  83. package/dist/server.test.js.map +1 -0
  84. package/package.json +44 -0
  85. package/viewer/assets/ViewControls-hn4PfGV3.js +54 -0
  86. package/viewer/assets/_-BAkV94Fq.js +1 -0
  87. package/viewer/assets/index-D8YJNCs_.css +2 -0
  88. package/viewer/assets/index-D9G4C6Ns.js +11 -0
  89. package/viewer/assets/routes-DmeR6gGl.js +1 -0
  90. package/viewer/index.html +13 -0
@@ -0,0 +1,8 @@
1
+ export * from './compare.js';
2
+ export * from './etag.js';
3
+ export * from './hash.js';
4
+ export * from './paths.js';
5
+ export * from './process.js';
6
+ export * from './scan.js';
7
+ export * from './schema.js';
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC;AAC7B,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC"}
@@ -0,0 +1,8 @@
1
+ export * from './compare.js';
2
+ export * from './etag.js';
3
+ export * from './hash.js';
4
+ export * from './paths.js';
5
+ export * from './process.js';
6
+ export * from './scan.js';
7
+ export * from './schema.js';
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC;AAC7B,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC"}
@@ -0,0 +1,7 @@
1
+ export declare const IMAGE_EXT = ".avif";
2
+ export declare const imageFile: (renderer: string) => string;
3
+ export declare const metricsFile: (renderer: string, reference: string) => string;
4
+ export declare const deltaFile: (renderer: string, reference: string) => string;
5
+ /** The only suite files the viewer may read; also what `build` exports and `hash` covers. */
6
+ export declare function isDataFile(rel: string): boolean;
7
+ //# sourceMappingURL=paths.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../../src/core/paths.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,SAAS,UAAU,CAAC;AACjC,eAAO,MAAM,SAAS,aAAc,MAAM,WAA8B,CAAC;AACzE,eAAO,MAAM,WAAW,aAAc,MAAM,aAAa,MAAM,WAA+C,CAAC;AAC/G,eAAO,MAAM,SAAS,aAAc,MAAM,aAAa,MAAM,WAAoD,CAAC;AAElH,6FAA6F;AAC7F,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAG/C"}
@@ -0,0 +1,12 @@
1
+ // Client-safe file naming (no node imports): shared by the CLI and the viewer.
2
+ export const IMAGE_EXT = '.avif';
3
+ export const imageFile = (renderer) => `${renderer}${IMAGE_EXT}`;
4
+ export const metricsFile = (renderer, reference) => `${renderer}.vs-${reference}.metrics.json`;
5
+ export const deltaFile = (renderer, reference) => `${renderer}.vs-${reference}.delta${IMAGE_EXT}`;
6
+ /** The only suite files the viewer may read; also what `build` exports and `hash` covers. */
7
+ export function isDataFile(rel) {
8
+ if (rel.split('/').some((s) => s.startsWith('.') || s === '..'))
9
+ return false;
10
+ return rel === 'index.json' || rel === 'README.md' || rel.endsWith('/README.md') || rel.endsWith(IMAGE_EXT);
11
+ }
12
+ //# sourceMappingURL=paths.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.js","sourceRoot":"","sources":["../../src/core/paths.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,MAAM,CAAC,MAAM,SAAS,GAAG,OAAO,CAAC;AACjC,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,QAAgB,EAAE,EAAE,CAAC,GAAG,QAAQ,GAAG,SAAS,EAAE,CAAC;AACzE,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,QAAgB,EAAE,SAAiB,EAAE,EAAE,CAAC,GAAG,QAAQ,OAAO,SAAS,eAAe,CAAC;AAC/G,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,QAAgB,EAAE,SAAiB,EAAE,EAAE,CAAC,GAAG,QAAQ,OAAO,SAAS,SAAS,SAAS,EAAE,CAAC;AAElH,6FAA6F;AAC7F,MAAM,UAAU,UAAU,CAAC,GAAW;IACpC,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IAC9E,OAAO,GAAG,KAAK,YAAY,IAAI,GAAG,KAAK,WAAW,IAAI,GAAG,CAAC,QAAQ,CAAC,YAAY,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;AAC9G,CAAC"}
@@ -0,0 +1,24 @@
1
+ import { type ImageMetrics } from './compare.js';
2
+ import { type Suite } from './scan.js';
3
+ export interface MetricsRecord extends ImageMetrics {
4
+ width: number;
5
+ height: number;
6
+ generatedAt: string;
7
+ }
8
+ export type SuiteIndex = Suite & {
9
+ metrics: Record<string, MetricsRecord>;
10
+ };
11
+ export interface ProcessResult {
12
+ computed: number;
13
+ skipped: number;
14
+ failed: {
15
+ file: string;
16
+ error: string;
17
+ }[];
18
+ }
19
+ /** Writes metrics + delta for every (scene, output, reference, renderer) pair whose outputs are older than either input, then `index.json`. */
20
+ export declare function processSuite(root: string, opts?: {
21
+ force?: boolean;
22
+ onCompute?: (file: string) => void;
23
+ }): Promise<ProcessResult>;
24
+ //# sourceMappingURL=process.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"process.d.ts","sourceRoot":"","sources":["../../src/core/process.ts"],"names":[],"mappings":"AAEA,OAAO,EAAiB,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AAChE,OAAO,EAAwB,KAAK,KAAK,EAAE,MAAM,WAAW,CAAC;AAG7D,MAAM,WAAW,aAAc,SAAQ,YAAY;IACjD,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;CACrB;AAQD,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAA;CAAE,CAAC;AAE5E,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAC3C;AAED,+IAA+I;AAC/I,wBAAsB,YAAY,CAChC,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;CAAO,GACjE,OAAO,CAAC,aAAa,CAAC,CAqCxB"}
@@ -0,0 +1,65 @@
1
+ import { mkdir, readFile, stat, writeFile } from 'node:fs/promises';
2
+ import { dirname, join } from 'node:path';
3
+ import { compareImages } from './compare.js';
4
+ import { allScenes, scanSuite } from './scan.js';
5
+ import { deltaFile, imageFile, metricsFile } from './paths.js';
6
+ const mtime = (p) => stat(p).then((s) => s.mtimeMs, () => 0);
7
+ /** Writes metrics + delta for every (scene, output, reference, renderer) pair whose outputs are older than either input, then `index.json`. */
8
+ export async function processSuite(root, opts = {}) {
9
+ const suite = await scanSuite(root);
10
+ const refs = suite.config.renderers.filter((r) => r.reference).map((r) => r.id);
11
+ const result = { computed: 0, skipped: 0, failed: [] };
12
+ for (const scene of allScenes(suite.root)) {
13
+ for (const [output, renderers] of Object.entries(scene.images)) {
14
+ const dir = join(root, scene.path, output);
15
+ for (const ref of refs.filter((r) => renderers.includes(r))) {
16
+ for (const r of renderers.filter((x) => x !== ref)) {
17
+ const [refPath, testPath] = [join(dir, imageFile(ref)), join(dir, imageFile(r))];
18
+ const metricsPath = join(dir, metricsFile(r, ref));
19
+ const deltaPath = join(dir, deltaFile(r, ref));
20
+ const inputs = Math.max(await mtime(refPath), await mtime(testPath));
21
+ const outputs = [metricsPath, ...(suite.config.delta ? [deltaPath] : [])];
22
+ const oldest = Math.min(...(await Promise.all(outputs.map(mtime))));
23
+ if (!opts.force && oldest > inputs) {
24
+ result.skipped += 1;
25
+ continue;
26
+ }
27
+ try {
28
+ const { metrics, width, height, deltaImage } = await compareImages(refPath, testPath);
29
+ const rec = { ...metrics, width, height, generatedAt: new Date().toISOString() };
30
+ await writeFile(metricsPath, JSON.stringify(rec, null, 2) + '\n');
31
+ if (suite.config.delta)
32
+ await writeFile(deltaPath, deltaImage);
33
+ result.computed += 1;
34
+ opts.onCompute?.(metricsPath);
35
+ }
36
+ catch (e) {
37
+ result.failed.push({ file: testPath, error: e instanceof Error ? e.message : String(e) });
38
+ }
39
+ }
40
+ }
41
+ }
42
+ }
43
+ await writeIndex(root, await scanSuite(root));
44
+ return result;
45
+ }
46
+ /** `index.json`: the scan plus every metrics record, so the viewer never walks the disk per request. */
47
+ async function writeIndex(root, suite) {
48
+ const metrics = {};
49
+ for (const scene of allScenes(suite.root)) {
50
+ for (const [output, renderers] of Object.entries(scene.images)) {
51
+ for (const r of renderers) {
52
+ for (const ref of suite.config.renderers.filter((x) => x.reference)) {
53
+ const rel = `${scene.path}/${output}/${metricsFile(r, ref.id)}`;
54
+ const raw = await readFile(join(root, rel), 'utf8').catch(() => null);
55
+ if (raw)
56
+ metrics[rel] = JSON.parse(raw);
57
+ }
58
+ }
59
+ }
60
+ }
61
+ const file = join(root, 'index.json');
62
+ await mkdir(dirname(file), { recursive: true });
63
+ await writeFile(file, JSON.stringify({ ...suite, metrics }, null, 2) + '\n');
64
+ }
65
+ //# sourceMappingURL=process.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"process.js","sourceRoot":"","sources":["../../src/core/process.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ import { type FidelityConfig } from './schema.js';
2
+ export interface SceneNode {
3
+ /** Slash-joined path from the root, e.g. `surfaces/standard_surface/brass`. */
4
+ path: string;
5
+ title: string;
6
+ tags: string[];
7
+ hasReadme: boolean;
8
+ /** output id -> renderer ids that have an image */
9
+ images: Record<string, string[]>;
10
+ }
11
+ export interface GroupNode {
12
+ path: string;
13
+ hasReadme: boolean;
14
+ groups: GroupNode[];
15
+ scenes: SceneNode[];
16
+ }
17
+ export interface Suite {
18
+ config: FidelityConfig;
19
+ hasReadme: boolean;
20
+ root: GroupNode;
21
+ }
22
+ export declare function readConfig(root: string): Promise<FidelityConfig>;
23
+ /** A directory is a scene when any `<output>/<renderer>.avif` exists in it; every other directory is a group. */
24
+ export declare function scanSuite(root: string): Promise<Suite>;
25
+ export declare function allScenes(g: GroupNode): Generator<SceneNode>;
26
+ //# sourceMappingURL=scan.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scan.d.ts","sourceRoot":"","sources":["../../src/core/scan.ts"],"names":[],"mappings":"AAGA,OAAO,EAAiC,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAEjF,MAAM,WAAW,SAAS;IACxB,+EAA+E;IAC/E,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,SAAS,EAAE,OAAO,CAAC;IACnB,mDAAmD;IACnD,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;CAClC;AACD,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,OAAO,CAAC;IACnB,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,MAAM,EAAE,SAAS,EAAE,CAAC;CACrB;AACD,MAAM,WAAW,KAAK;IACpB,MAAM,EAAE,cAAc,CAAC;IACvB,SAAS,EAAE,OAAO,CAAC;IACnB,IAAI,EAAE,SAAS,CAAC;CACjB;AAaD,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAEtE;AAED,iHAAiH;AACjH,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,CAkC5D;AAED,wBAAiB,SAAS,CAAC,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC,SAAS,CAAC,CAG7D"}
@@ -0,0 +1,55 @@
1
+ import { readdir, readFile, stat } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { imageFile } from './paths.js';
4
+ import { configSchema, sceneMetaSchema } from './schema.js';
5
+ const exists = (p) => stat(p).then(() => true, () => false);
6
+ const isDir = (p) => stat(p).then((s) => s.isDirectory(), () => false);
7
+ export async function readConfig(root) {
8
+ return configSchema.parse(JSON.parse(await readFile(join(root, 'fidelity.json'), 'utf8')));
9
+ }
10
+ /** A directory is a scene when any `<output>/<renderer>.avif` exists in it; every other directory is a group. */
11
+ export async function scanSuite(root) {
12
+ const config = await readConfig(root);
13
+ const rendererIds = config.renderers.map((r) => r.id);
14
+ async function visit(rel) {
15
+ const dir = join(root, rel);
16
+ const images = {};
17
+ for (const o of config.outputs) {
18
+ const found = [];
19
+ for (const r of rendererIds)
20
+ if (await exists(join(dir, o.id, imageFile(r))))
21
+ found.push(r);
22
+ if (found.length)
23
+ images[o.id] = found;
24
+ }
25
+ const hasReadme = await exists(join(dir, 'README.md'));
26
+ if (Object.keys(images).length) {
27
+ const meta = sceneMetaSchema.parse((await exists(join(dir, 'scene.json'))) ? JSON.parse(await readFile(join(dir, 'scene.json'), 'utf8')) : {});
28
+ return { path: rel, title: meta.title ?? rel.split('/').pop(), tags: meta.tags, hasReadme, images };
29
+ }
30
+ const group = { path: rel, hasReadme, groups: [], scenes: [] };
31
+ const outputIds = new Set(config.outputs.map((o) => o.id));
32
+ for (const e of (await readdir(dir, { withFileTypes: true })).toSorted((a, b) => a.name.localeCompare(b.name))) {
33
+ if (!e.isDirectory() || e.name.startsWith('.') || outputIds.has(e.name))
34
+ continue;
35
+ const child = await visit(rel ? `${rel}/${e.name}` : e.name);
36
+ if (!child)
37
+ continue;
38
+ if ('scenes' in child)
39
+ group.groups.push(child);
40
+ else
41
+ group.scenes.push(child);
42
+ }
43
+ return group.groups.length || group.scenes.length ? group : null;
44
+ }
45
+ if (!(await isDir(root)))
46
+ throw new Error(`Not a directory: ${root}`);
47
+ const top = (await visit('')) ?? { path: '', hasReadme: false, groups: [], scenes: [] };
48
+ return { config, hasReadme: await exists(join(root, 'README.md')), root: top };
49
+ }
50
+ export function* allScenes(g) {
51
+ yield* g.scenes;
52
+ for (const c of g.groups)
53
+ yield* allScenes(c);
54
+ }
55
+ //# sourceMappingURL=scan.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scan.js","sourceRoot":"","sources":["../../src/core/scan.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ import { z } from 'zod';
2
+ /** `<root>/fidelity.json`. Renderer/output ids and labels are entirely up to the suite. */
3
+ export declare const configSchema: z.ZodObject<{
4
+ title: z.ZodString;
5
+ renderers: z.ZodArray<z.ZodObject<{
6
+ id: z.ZodString;
7
+ label: z.ZodOptional<z.ZodString>;
8
+ reference: z.ZodOptional<z.ZodBoolean>;
9
+ category: z.ZodOptional<z.ZodString>;
10
+ }, z.core.$strip>>;
11
+ outputs: z.ZodDefault<z.ZodArray<z.ZodObject<{
12
+ id: z.ZodString;
13
+ label: z.ZodOptional<z.ZodString>;
14
+ }, z.core.$strip>>>;
15
+ delta: z.ZodDefault<z.ZodBoolean>;
16
+ }, z.core.$strip>;
17
+ export type FidelityConfig = z.infer<typeof configSchema>;
18
+ /** Optional `<scene>/scene.json`. */
19
+ export declare const sceneMetaSchema: z.ZodObject<{
20
+ title: z.ZodOptional<z.ZodString>;
21
+ tags: z.ZodDefault<z.ZodArray<z.ZodString>>;
22
+ }, z.core.$strip>;
23
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/core/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,2FAA2F;AAC3F,eAAO,MAAM,YAAY;;;;;;;;;;;;;iBAcvB,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAE1D,qCAAqC;AACrC,eAAO,MAAM,eAAe;;;iBAAoF,CAAC"}
@@ -0,0 +1,19 @@
1
+ import { z } from 'zod';
2
+ const id = z.string().regex(/^[a-z0-9][a-z0-9._-]*$/);
3
+ /** `<root>/fidelity.json`. Renderer/output ids and labels are entirely up to the suite. */
4
+ export const configSchema = z.object({
5
+ title: z.string(),
6
+ renderers: z
7
+ .array(z.object({
8
+ id,
9
+ label: z.string().optional(),
10
+ reference: z.boolean().optional(),
11
+ category: z.string().optional(),
12
+ }))
13
+ .refine((r) => r.some((x) => x.reference), 'at least one renderer needs "reference": true'),
14
+ outputs: z.array(z.object({ id, label: z.string().optional() })).default([{ id: 'beauty' }]),
15
+ delta: z.boolean().default(true),
16
+ });
17
+ /** Optional `<scene>/scene.json`. */
18
+ export const sceneMetaSchema = z.object({ title: z.string().optional(), tags: z.array(z.string()).default([]) });
19
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/core/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,MAAM,EAAE,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,wBAAwB,CAAC,CAAC;AAEtD,2FAA2F;AAC3F,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,CAAC,MAAM,CAAC;IACnC,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;IACjB,SAAS,EAAE,CAAC;SACT,KAAK,CACJ,CAAC,CAAC,MAAM,CAAC;QACP,EAAE;QACF,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;QAC5B,SAAS,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE;QACjC,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;KAChC,CAAC,CACH;SACA,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,EAAE,+CAA+C,CAAC;IAC7F,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC;IAC5F,KAAK,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC;CACjC,CAAC,CAAC;AAGH,qCAAqC;AACrC,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=suite.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"suite.test.d.ts","sourceRoot":"","sources":["../../src/core/suite.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,41 @@
1
+ import { mkdtemp, mkdir, readFile, utimes, writeFile } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import sharp from 'sharp';
5
+ import { expect, test } from 'vitest';
6
+ import { fileResponse, processSuite, scanSuite } from './index.js';
7
+ const png = (v) => sharp({ create: { width: 8, height: 8, channels: 3, background: { r: v, g: v, b: v } } })
8
+ .avif({ lossless: true })
9
+ .toBuffer();
10
+ test('scan, process, staleness, etag', async () => {
11
+ const root = await mkdtemp(join(tmpdir(), 'fk-'));
12
+ await writeFile(join(root, 'fidelity.json'), JSON.stringify({ title: 'T', renderers: [{ id: 'ref', reference: true }, { id: 'a' }, { id: 'b' }] }));
13
+ const dir = join(root, 'g1', 'g2', 's1', 'beauty');
14
+ await mkdir(dir, { recursive: true });
15
+ await writeFile(join(root, 'g1', 'g2', 's1', 'scene.json'), JSON.stringify({ tags: ['x'] }));
16
+ await writeFile(join(dir, 'ref.avif'), await png(100));
17
+ await writeFile(join(dir, 'a.avif'), await png(100));
18
+ await writeFile(join(dir, 'b.avif'), await png(110));
19
+ const suite = await scanSuite(root);
20
+ expect(suite.root.groups[0].groups[0].scenes[0]).toMatchObject({
21
+ path: 'g1/g2/s1',
22
+ tags: ['x'],
23
+ images: { beauty: ['ref', 'a', 'b'] },
24
+ });
25
+ expect(await processSuite(root)).toMatchObject({ computed: 2, skipped: 0, failed: [] });
26
+ const a = JSON.parse(await readFile(join(dir, 'a.vs-ref.metrics.json'), 'utf8'));
27
+ const b = JSON.parse(await readFile(join(dir, 'b.vs-ref.metrics.json'), 'utf8'));
28
+ expect(a.psnr).toBeNull();
29
+ expect(b.maxError).toBeGreaterThan(0);
30
+ expect(await processSuite(root)).toMatchObject({ computed: 0, skipped: 2 });
31
+ const future = new Date(Date.now() + 5000);
32
+ await utimes(join(dir, 'b.avif'), future, future);
33
+ expect(await processSuite(root)).toMatchObject({ computed: 1, skipped: 1 });
34
+ const url = 'http://x/';
35
+ const first = await fileResponse(new Request(url), join(dir, 'a.avif'));
36
+ const tag = first.headers.get('etag');
37
+ await first.body?.cancel();
38
+ const second = await fileResponse(new Request(url, { headers: { 'if-none-match': tag } }), join(dir, 'a.avif'));
39
+ expect(second.status).toBe(304);
40
+ });
41
+ //# sourceMappingURL=suite.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"suite.test.js","sourceRoot":"","sources":["../../src/core/suite.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ import type { OpenCliDocument } from '@clidoc/core';
2
+ /** OpenCLI document of this CLI (clidoc), served by `cli __opencli`. */
3
+ export declare function cliDocument(): Promise<OpenCliDocument>;
4
+ export declare function runCli(argv?: string[]): Promise<void>;
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAYpD,wEAAwE;AACxE,wBAAsB,WAAW,IAAI,OAAO,CAAC,eAAe,CAAC,CAI5D;AAED,wBAAsB,MAAM,CAAC,IAAI,WAAwB,GAAG,OAAO,CAAC,IAAI,CAAC,CAaxE"}
package/dist/index.js ADDED
@@ -0,0 +1,29 @@
1
+ import { fileURLToPath } from 'node:url';
2
+ import { createDocgenCommand, fromYargsAsync } from '@clidoc/yargs';
3
+ import yargs from 'yargs';
4
+ import { hideBin } from 'yargs/helpers';
5
+ import { fileCommands } from 'yargs-file-commands';
6
+ // Commands are the files in ./commands (process, ...).
7
+ async function loadCommands() {
8
+ const commandsDir = fileURLToPath(new URL('./commands', import.meta.url));
9
+ return fileCommands({ commandDirs: [commandsDir] });
10
+ }
11
+ /** OpenCLI document of this CLI (clidoc), served by `cli __opencli`. */
12
+ export async function cliDocument() {
13
+ const commands = await loadCommands();
14
+ const docgen = createDocgenCommand(() => cliDocument());
15
+ return fromYargsAsync([...commands, docgen], { title: 'fidelity-kit', binary: 'fidelity-kit', version: '0.1.0' });
16
+ }
17
+ export async function runCli(argv = hideBin(process.argv)) {
18
+ const commands = await loadCommands();
19
+ const docgen = createDocgenCommand(() => fromYargsAsync([...commands, docgen], { title: 'fidelity-kit', binary: 'fidelity-kit', version: '0.1.0' }));
20
+ await yargs(argv)
21
+ .scriptName('fidelity-kit')
22
+ .usage('$0 <command>')
23
+ .command([...commands, docgen])
24
+ .strictCommands()
25
+ .demandCommand(1)
26
+ .help()
27
+ .parseAsync();
28
+ }
29
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACpE,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AACxC,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAEnD,uDAAuD;AACvD,KAAK,UAAU,YAAY;IACzB,MAAM,WAAW,GAAG,aAAa,CAAC,IAAI,GAAG,CAAC,YAAY,EAAE,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IAC1E,OAAO,YAAY,CAAC,EAAE,WAAW,EAAE,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;AACtD,CAAC;AAED,wEAAwE;AACxE,MAAM,CAAC,KAAK,UAAU,WAAW;IAC/B,MAAM,QAAQ,GAAG,MAAM,YAAY,EAAE,CAAC;IACtC,MAAM,MAAM,GAAG,mBAAmB,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC;IACxD,OAAO,cAAc,CAAC,CAAC,GAAG,QAAQ,EAAE,MAAM,CAAC,EAAE,EAAE,KAAK,EAAE,cAAc,EAAE,MAAM,EAAE,cAAc,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;AACpH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,MAAM,CAAC,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC;IACvD,MAAM,QAAQ,GAAG,MAAM,YAAY,EAAE,CAAC;IACtC,MAAM,MAAM,GAAG,mBAAmB,CAAC,GAAG,EAAE,CACtC,cAAc,CAAC,CAAC,GAAG,QAAQ,EAAE,MAAM,CAAC,EAAE,EAAE,KAAK,EAAE,cAAc,EAAE,MAAM,EAAE,cAAc,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAC3G,CAAC;IACF,MAAM,KAAK,CAAC,IAAI,CAAC;SACd,UAAU,CAAC,cAAc,CAAC;SAC1B,KAAK,CAAC,cAAc,CAAC;SACrB,OAAO,CAAC,CAAC,GAAG,QAAQ,EAAE,MAAM,CAAC,CAAC;SAC9B,cAAc,EAAE;SAChB,aAAa,CAAC,CAAC,CAAC;SAChB,IAAI,EAAE;SACN,UAAU,EAAE,CAAC;AAClB,CAAC"}
package/dist/run.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ export interface RunArgs {
2
+ root: string;
3
+ port: number;
4
+ host: string;
5
+ process: boolean;
6
+ maxAge?: number;
7
+ staleWhileRevalidate?: number;
8
+ }
9
+ /** Shared by `serve` (cached, lazy ETags) and `dev` (always fresh). */
10
+ export declare function run(argv: RunArgs, dev: boolean): Promise<void>;
11
+ //# sourceMappingURL=run.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,uEAAuE;AACvE,wBAAsB,GAAG,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,iBAepD"}
package/dist/run.js ADDED
@@ -0,0 +1,23 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { processSuite } from './core/index.js';
4
+ import { assertViewerBuilt, createHandler, defaultCachePolicy, serve } from './server.js';
5
+ /** Shared by `serve` (cached, lazy ETags) and `dev` (always fresh). */
6
+ export async function run(argv, dev) {
7
+ assertViewerBuilt();
8
+ const root = resolve(argv.root);
9
+ if (argv.process) {
10
+ const r = await processSuite(root);
11
+ console.log(`${r.computed} computed, ${r.skipped} up to date, ${r.failed.length} failed`);
12
+ }
13
+ else if (!existsSync(`${root}/index.json`)) {
14
+ throw new Error(`No index.json in ${root}; run \`fidelity-kit process\` or drop --no-process.`);
15
+ }
16
+ const cache = {
17
+ maxAge: argv.maxAge ?? defaultCachePolicy.maxAge,
18
+ staleWhileRevalidate: argv.staleWhileRevalidate ?? defaultCachePolicy.staleWhileRevalidate,
19
+ };
20
+ await serve(createHandler(root, { cache, dev }), argv.port, argv.host);
21
+ console.log(`Serving ${root} at http://${argv.host}:${argv.port}${dev ? ' (dev: nothing cached)' : ''}`);
22
+ }
23
+ //# sourceMappingURL=run.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run.js","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,kBAAkB,EAAE,KAAK,EAAoB,MAAM,aAAa,CAAC;AAW5G,uEAAuE;AACvE,MAAM,CAAC,KAAK,UAAU,GAAG,CAAC,IAAa,EAAE,GAAY;IACnD,iBAAiB,EAAE,CAAC;IACpB,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAChC,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;QACjB,MAAM,CAAC,GAAG,MAAM,YAAY,CAAC,IAAI,CAAC,CAAC;QACnC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,cAAc,CAAC,CAAC,OAAO,gBAAgB,CAAC,CAAC,MAAM,CAAC,MAAM,SAAS,CAAC,CAAC;IAC5F,CAAC;SAAM,IAAI,CAAC,UAAU,CAAC,GAAG,IAAI,aAAa,CAAC,EAAE,CAAC;QAC7C,MAAM,IAAI,KAAK,CAAC,oBAAoB,IAAI,sDAAsD,CAAC,CAAC;IAClG,CAAC;IACD,MAAM,KAAK,GAAgB;QACzB,MAAM,EAAE,IAAI,CAAC,MAAM,IAAI,kBAAkB,CAAC,MAAM;QAChD,oBAAoB,EAAE,IAAI,CAAC,oBAAoB,IAAI,kBAAkB,CAAC,oBAAoB;KAC3F,CAAC;IACF,MAAM,KAAK,CAAC,aAAa,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;IACvE,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,cAAc,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,IAAI,GAAG,GAAG,CAAC,CAAC,CAAC,wBAAwB,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AAC3G,CAAC"}
@@ -0,0 +1,34 @@
1
+ import { type Server } from 'node:http';
2
+ import { isDataFile } from './core/paths.js';
3
+ /** The prebuilt viewer SPA (built by `pnpm build` into packages/cli/viewer, shipped in the npm package). */
4
+ export declare const viewerDir: string;
5
+ export declare function assertViewerBuilt(): void;
6
+ export { isDataFile };
7
+ export interface CachePolicy {
8
+ /** Seconds an image is served from cache without contacting the origin. */
9
+ maxAge: number;
10
+ /** Seconds after that a stale image may be served while a background refresh runs (also used for stale-if-error). */
11
+ staleWhileRevalidate: number;
12
+ }
13
+ export declare const defaultCachePolicy: CachePolicy;
14
+ export interface HandlerOptions {
15
+ /** Directory of the built viewer. */
16
+ assets?: string;
17
+ /** Image cache policy (serve mode). */
18
+ cache?: CachePolicy;
19
+ /**
20
+ * Dev mode: every file is served fresh with `no-store`, no ETag/Last-Modified, no conditional requests. Nothing is
21
+ * hashed: `/data/image-hashes.json` is an empty `{}` (a 404 would log a console error in the browser), `?v=` is ignored and `image-hashes.json` is not even loaded.
22
+ */
23
+ dev?: boolean;
24
+ }
25
+ /**
26
+ * `/data/*` = the suite (allowlisted files); everything else = the viewer SPA (hash routing, so no fallback needed).
27
+ * Serve mode: images are shared-cacheable per `cache`; `index.json`, READMEs and `index.html` always revalidate (cheap
28
+ * 304s) so a re-run of `process` shows up immediately; hashed `/assets/*` are immutable. Dev mode: nothing is cached.
29
+ * Serve mode also keeps a lazy content-hash map (pre-populated from `<root>/image-hashes.json`), lists it at
30
+ * `/data/image-hashes.json`, and serves `image?v=<hash>` as immutable only when `<hash>` is the file's current hash.
31
+ */
32
+ export declare function createHandler(root: string, { assets, cache, dev }?: HandlerOptions): (req: Request) => Promise<Response>;
33
+ export declare function serve(handler: (req: Request) => Promise<Response>, port: number, host: string): Promise<Server>;
34
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,MAAM,EAAE,MAAM,WAAW,CAAC;AAetD,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7C,4GAA4G;AAC5G,eAAO,MAAM,SAAS,QAAuD,CAAC;AAE9E,wBAAgB,iBAAiB,SAEhC;AAED,OAAO,EAAE,UAAU,EAAE,CAAC;AAEtB,MAAM,WAAW,WAAW;IAC1B,2EAA2E;IAC3E,MAAM,EAAE,MAAM,CAAC;IACf,qHAAqH;IACrH,oBAAoB,EAAE,MAAM,CAAC;CAC9B;AACD,eAAO,MAAM,kBAAkB,EAAE,WAA0D,CAAC;AAE5F,MAAM,WAAW,cAAc;IAC7B,qCAAqC;IACrC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,uCAAuC;IACvC,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB;;;OAGG;IACH,GAAG,CAAC,EAAE,OAAO,CAAC;CACf;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,EACZ,EAAE,MAAkB,EAAE,KAA0B,EAAE,GAAW,EAAE,GAAE,cAAmB,SA6BjE,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC,CAsB/C;AAED,wBAAgB,KAAK,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAgB/G"}
package/dist/server.js ADDED
@@ -0,0 +1,90 @@
1
+ import { createServer } from 'node:http';
2
+ import { Readable } from 'node:stream';
3
+ import { pipeline } from 'node:stream/promises';
4
+ import { existsSync } from 'node:fs';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { relative, resolve as resolvePath, sep } from 'node:path';
7
+ import { fileResponse, freshFileResponse, HashStore, IMMUTABLE, notModified, readHashFile, resolveInside, } from './core/index.js';
8
+ import { isDataFile } from './core/paths.js';
9
+ /** The prebuilt viewer SPA (built by `pnpm build` into packages/cli/viewer, shipped in the npm package). */
10
+ export const viewerDir = fileURLToPath(new URL('../viewer', import.meta.url));
11
+ export function assertViewerBuilt() {
12
+ if (!existsSync(`${viewerDir}/index.html`))
13
+ throw new Error(`Viewer not built (${viewerDir}). Run \`pnpm build\`.`);
14
+ }
15
+ export { isDataFile };
16
+ export const defaultCachePolicy = { maxAge: 300, staleWhileRevalidate: 86400 };
17
+ /**
18
+ * `/data/*` = the suite (allowlisted files); everything else = the viewer SPA (hash routing, so no fallback needed).
19
+ * Serve mode: images are shared-cacheable per `cache`; `index.json`, READMEs and `index.html` always revalidate (cheap
20
+ * 304s) so a re-run of `process` shows up immediately; hashed `/assets/*` are immutable. Dev mode: nothing is cached.
21
+ * Serve mode also keeps a lazy content-hash map (pre-populated from `<root>/image-hashes.json`), lists it at
22
+ * `/data/image-hashes.json`, and serves `image?v=<hash>` as immutable only when `<hash>` is the file's current hash.
23
+ */
24
+ export function createHandler(root, { assets = viewerDir, cache = defaultCachePolicy, dev = false } = {}) {
25
+ const imageCache = `public, max-age=${cache.maxAge}, stale-while-revalidate=${cache.staleWhileRevalidate}, stale-if-error=${cache.staleWhileRevalidate}`;
26
+ const store = new HashStore();
27
+ const bootId = Date.now().toString(36); // keeps ETags of the hash listing from colliding across restarts
28
+ const base = resolvePath(root);
29
+ const loaded = dev
30
+ ? undefined
31
+ : readHashFile(root).then((files) => {
32
+ for (const [rel, e] of Object.entries(files))
33
+ store.preload(resolvePath(base, rel), e);
34
+ });
35
+ const send = (req, file, cacheControl, immutableIf) => dev ? freshFileResponse(file) : fileResponse(req, file, { cacheControl, immutableIf, store });
36
+ /** `{ "<rel path>": "<hash>" }` for every hash known right now (entries whose file changed are dropped first). */
37
+ const hashListing = async (req) => {
38
+ await loaded;
39
+ const known = await store.known();
40
+ const body = JSON.stringify(Object.fromEntries([...known]
41
+ .filter(([p]) => p.endsWith('.avif'))
42
+ .map(([p, h]) => [relative(base, p).split(sep).join('/'), h])
43
+ .toSorted(([a], [b]) => (a < b ? -1 : 1))));
44
+ const headers = { ETag: `"${bootId}-${store.version.toString(36)}"`, 'Cache-Control': 'no-cache' };
45
+ if (notModified(req, headers.ETag, Infinity))
46
+ return new Response(null, { status: 304, headers });
47
+ return new Response(body, { headers: { ...headers, 'Content-Type': 'application/json' } });
48
+ };
49
+ return async (req) => {
50
+ const path = decodeURIComponent(new URL(req.url).pathname);
51
+ const notFound = new Response('Not found', {
52
+ status: 404,
53
+ headers: { 'Cache-Control': dev ? 'no-store' : 'no-cache' },
54
+ });
55
+ if (path.startsWith('/data/')) {
56
+ const rel = path.slice('/data/'.length);
57
+ if (rel === 'image-hashes.json')
58
+ return dev
59
+ ? new Response('{}', { headers: { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' } })
60
+ : hashListing(req);
61
+ const full = isDataFile(rel) ? resolveInside(root, rel) : null;
62
+ if (!full)
63
+ return notFound;
64
+ await loaded;
65
+ const isImage = rel.endsWith('.avif');
66
+ return send(req, full, isImage ? imageCache : undefined, isImage ? new URL(req.url).searchParams.get('v') : null);
67
+ }
68
+ const rel = path === '/' ? 'index.html' : path.slice(1);
69
+ const full = resolveInside(assets, rel);
70
+ return full ? send(req, full, rel.startsWith('assets/') ? IMMUTABLE : undefined) : notFound;
71
+ };
72
+ }
73
+ export function serve(handler, port, host) {
74
+ const server = createServer(async (nreq, nres) => {
75
+ const headers = new Headers();
76
+ for (const [k, v] of Object.entries(nreq.headers))
77
+ if (typeof v === 'string')
78
+ headers.set(k, v);
79
+ const res = nreq.method === 'GET' || nreq.method === 'HEAD'
80
+ ? await handler(new Request(`http://${headers.get('host') ?? 'localhost'}${nreq.url}`, { headers })).catch((e) => new Response(String(e), { status: 500 }))
81
+ : new Response('Method not allowed', { status: 405 });
82
+ nres.writeHead(res.status, Object.fromEntries(res.headers));
83
+ if (nreq.method === 'HEAD' || !res.body)
84
+ return void nres.end();
85
+ // Stream with backpressure; a client that navigates away just aborts its transfer.
86
+ await pipeline(Readable.fromWeb(res.body), nres).catch(() => { });
87
+ });
88
+ return new Promise((resolve) => server.listen(port, host, 1024, () => resolve(server)));
89
+ }
90
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=server.test.d.ts.map