vitecache 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/storage.js CHANGED
@@ -1,98 +1,93 @@
1
- import { createHash } from 'node:crypto';
2
- import { realpath } from 'node:fs/promises';
3
- import { isAbsolute, join, relative, resolve, sep } from 'node:path';
1
+ import { join, resolve } from 'node:path';
4
2
  import { fileURLToPath } from 'node:url';
5
3
  import * as disk from './disk.js';
6
- import { equalHashes, hashes, resolvedDependencies } from './dependencies.js';
4
+ import { ChangedDependenciesError, equalHashes, hashableDependencies, hashes, MissingDependencyError, resolvedDependencies } from './dependencies.js';
5
+ import { contentHash, fileHash } from './hash.js';
7
6
  import { packageVersion } from './packages.js';
8
- function digest(bytes) {
9
- return createHash('sha256').update(bytes).digest('hex');
10
- }
11
- export class ChangedDependenciesError extends Error {
12
- constructor() {
13
- super('vitecache dependencies changed during computation.');
14
- }
15
- }
16
- function metadata(bytes) {
17
- const value = JSON.parse(new TextDecoder().decode(bytes));
18
- if (typeof value !== 'object' || value === null || !('contentHash' in value) || !('dependencies' in value)) {
19
- throw new Error('Invalid vitecache metadata.');
7
+ /** A metadata file vitecache wrote; any failure to read it names the file, since no request caused it. */
8
+ function metadata(bytes, file) {
9
+ try {
10
+ const value = JSON.parse(new TextDecoder().decode(bytes));
11
+ if (typeof value !== 'object' || value === null || !('contentHash' in value) || !('dependencies' in value)) {
12
+ throw new Error('Expected contentHash and dependencies.');
13
+ }
14
+ const hash = value.contentHash;
15
+ if (typeof hash !== 'string' || !/^[a-f0-9]{64}$/.test(hash))
16
+ throw new Error('Expected a content hash.');
17
+ const validated = hashes(value.dependencies);
18
+ // Metadata without saved dependencies: all that vitecache 0.1 wrote, and what 0.2 wrote for entries that had none.
19
+ if (!('saved' in value))
20
+ return { contentHash: hash, dependencies: validated, saved: {}, savedHashes: {} };
21
+ // vitecache 0.2 kept each saved dependency with its hash in one record. Its entry misses until a save replaces it.
22
+ if (!('savedHashes' in value))
23
+ return undefined;
24
+ return { contentHash: hash, dependencies: validated, saved: hashableDependencies(value.saved), savedHashes: hashes(value.savedHashes) };
20
25
  }
21
- const { contentHash, dependencies } = value;
22
- if (typeof contentHash !== 'string' || !/^[a-f0-9]{64}$/.test(contentHash)) {
23
- throw new Error('Invalid vitecache metadata.');
26
+ catch (cause) {
27
+ throw new Error(`Invalid vitecache metadata: ${file}`, { cause });
24
28
  }
25
- return { contentHash, dependencies: hashes(dependencies) };
26
29
  }
27
30
  function matches(current, expected) {
28
- return current !== null && equalHashes(current, expected);
31
+ return current.status === 'current' && equalHashes(current.hashes, expected);
29
32
  }
30
33
  export function createStorage(options) {
31
34
  const { root, directory } = disk.location(options);
32
35
  async function snapshot(dependencies) {
33
- const entries = await Promise.all(Object.entries(dependencies).filter(([, dependency]) => !('value' in dependency)).map(async ([name, dependency]) => {
34
- try {
35
- if (dependency.package !== undefined) {
36
- const version = await packageVersion(root, dependency.package);
37
- return version === undefined ? null : [name, digest(new TextEncoder().encode(version))];
38
- }
39
- if (dependency.path === undefined)
40
- throw new TypeError('Expected a resolved dependency.');
41
- const bytes = await disk.load(resolve(root, dependency.path));
42
- return bytes === undefined ? null : [name, digest(bytes)];
43
- }
44
- catch (error) {
45
- if (!disk.missingDependency(error))
46
- throw error;
47
- return null;
48
- }
36
+ const entries = await Promise.all(Object.entries(dependencies).map(async ([name, dependency]) => {
37
+ if (dependency.package === undefined)
38
+ return [name, await fileHash(resolve(root, dependency.path))];
39
+ const version = await packageVersion(root, dependency.package);
40
+ return [name, version === undefined ? undefined : contentHash(new TextEncoder().encode(version))];
49
41
  }));
50
42
  const present = [];
51
- for (const entry of entries) {
52
- if (entry === null)
53
- return null;
54
- present.push(entry);
43
+ for (const [name, hash] of entries) {
44
+ if (hash === undefined)
45
+ return { status: 'missing', message: `Missing file or package dependency: ${name}` };
46
+ present.push([name, hash]);
55
47
  }
56
- return Object.fromEntries(present);
48
+ return { status: 'current', hashes: Object.fromEntries(present) };
49
+ }
50
+ async function load(key) {
51
+ const file = join(directory, `${key}.json`);
52
+ const raw = await disk.load(file);
53
+ if (raw === undefined)
54
+ return undefined;
55
+ const parsed = metadata(raw, file);
56
+ if (!parsed || !matches(await snapshot(parsed.saved), parsed.savedHashes))
57
+ return undefined;
58
+ const bytes = await disk.load(join(directory, `${key}.bin`));
59
+ if (bytes === undefined || contentHash(bytes) !== parsed.contentHash)
60
+ return undefined;
61
+ // The metadata file's modification time is the entry's last use. The bytes keep theirs, so static hosts keep their
62
+ // validators.
63
+ await disk.touch(file);
64
+ return { tag: contentHash(raw), hashes: parsed.dependencies, bytes };
57
65
  }
58
66
  return {
59
67
  async canSave() { return true; },
60
- async resolve(files) {
61
- return resolvedDependencies(Object.fromEntries(await Promise.all(Object.entries(files).map(async ([name, dependency]) => {
68
+ async resolve(declared) {
69
+ return resolvedDependencies(Object.fromEntries(await Promise.all(Object.entries(declared).map(async ([name, dependency]) => {
62
70
  if (dependency.url === undefined)
63
71
  return [name, dependency];
64
72
  const url = new URL(dependency.url);
65
73
  if (url.protocol !== 'file:') {
66
74
  throw new Error('Node file dependency URLs must use file:. Use a project-root-relative path for Vite-served files.');
67
75
  }
68
- const file = fileURLToPath(url);
69
- let path = relative(root, file);
70
- if (path === '..' || path.startsWith(`..${sep}`) || isAbsolute(path)) {
71
- path = relative(await realpath(root), await realpath(file));
72
- }
73
- return [name, { path: path.split(sep).join('/') }];
76
+ return [name, { path: await disk.rootRelative(root, fileURLToPath(url)) }];
74
77
  }))));
75
78
  },
76
79
  snapshot,
77
- async load(key) {
78
- const record = await disk.load(join(directory, `${key}.json`));
79
- if (record === undefined)
80
- return undefined;
81
- const { contentHash, dependencies } = metadata(record);
82
- const bytes = await disk.load(join(directory, `${key}.bin`));
83
- if (bytes === undefined || digest(bytes) !== contentHash)
84
- return undefined;
85
- return { bytes, hashes: dependencies };
86
- },
87
- async save(key, value, files, expected) {
88
- if (!matches(await snapshot(files), expected))
89
- throw new ChangedDependenciesError();
80
+ load,
81
+ async save(key, value, { checked, expected, saved }) {
82
+ const current = await snapshot(saved);
83
+ if (current.status !== 'current')
84
+ throw new MissingDependencyError(`Cannot save a cache entry with a missing saved dependency. ${current.message}`);
90
85
  await disk.ignore(root, directory);
91
86
  await disk.save(join(directory, `${key}.bin`), value, async () => {
92
- if (!matches(await snapshot(files), expected))
87
+ if (!matches(await snapshot(checked), expected))
93
88
  throw new ChangedDependenciesError();
94
89
  });
95
- const record = { contentHash: digest(value), dependencies: expected };
90
+ const record = { contentHash: contentHash(value), dependencies: expected, saved, savedHashes: current.hashes };
96
91
  await disk.save(join(directory, `${key}.json`), new TextEncoder().encode(JSON.stringify(record)));
97
92
  },
98
93
  };
@@ -1 +1 @@
1
- {"version":3,"file":"storage.js","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from 'node:crypto';\nimport { realpath } from 'node:fs/promises';\nimport { isAbsolute, join, relative, resolve, sep } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport * as disk from './disk.js';\nimport { equalHashes, hashes, resolvedDependencies } from './dependencies.js';\nimport { packageVersion } from './packages.js';\nimport type { Backend, CacheEntryOptions, Dependencies, Hashes, ResolvedDependencies } from './types.js';\n\nfunction digest(bytes: Uint8Array): string {\n return createHash('sha256').update(bytes).digest('hex');\n}\n\ninterface Metadata {\n contentHash: string;\n dependencies: Hashes;\n}\n\nexport class ChangedDependenciesError extends Error {\n constructor() {\n super('vitecache dependencies changed during computation.');\n }\n}\n\nfunction metadata(bytes: Uint8Array): Metadata {\n const value: unknown = JSON.parse(new TextDecoder().decode(bytes));\n if (typeof value !== 'object' || value === null || !('contentHash' in value) || !('dependencies' in value)) {\n throw new Error('Invalid vitecache metadata.');\n }\n const { contentHash, dependencies } = value;\n if (typeof contentHash !== 'string' || !/^[a-f0-9]{64}$/.test(contentHash)) {\n throw new Error('Invalid vitecache metadata.');\n }\n return { contentHash, dependencies: hashes(dependencies) };\n}\n\nfunction matches(current: Hashes | null, expected: Hashes): boolean {\n return current !== null && equalHashes(current, expected);\n}\n\nexport function createStorage(options: Pick<CacheEntryOptions, 'root' | 'basePath'>): Backend {\n const { root, directory } = disk.location(options);\n\n async function snapshot(dependencies: ResolvedDependencies): Promise<Hashes | null> {\n const entries = await Promise.all(Object.entries(dependencies).filter(([, dependency]) => !('value' in dependency)).map(async ([name, dependency]) => {\n try {\n if (dependency.package !== undefined) {\n const version = await packageVersion(root, dependency.package);\n return version === undefined ? null : [name, digest(new TextEncoder().encode(version))] satisfies [string, string];\n }\n if (dependency.path === undefined) throw new TypeError('Expected a resolved dependency.');\n const bytes = await disk.load(resolve(root, dependency.path));\n return bytes === undefined ? null : [name, digest(bytes)] satisfies [string, string];\n } catch (error) {\n if (!disk.missingDependency(error)) throw error;\n return null;\n }\n }));\n const present: [string, string][] = [];\n for (const entry of entries) {\n if (entry === null) return null;\n present.push(entry);\n }\n return Object.fromEntries(present);\n }\n\n return {\n async canSave() { return true; },\n async resolve(files: Dependencies) {\n return resolvedDependencies(Object.fromEntries(await Promise.all(Object.entries(files).map(async ([name, dependency]) => {\n if (dependency.url === undefined) return [name, dependency];\n const url = new URL(dependency.url);\n if (url.protocol !== 'file:') {\n throw new Error('Node file dependency URLs must use file:. Use a project-root-relative path for Vite-served files.');\n }\n const file = fileURLToPath(url);\n let path = relative(root, file);\n if (path === '..' || path.startsWith(`..${sep}`) || isAbsolute(path)) {\n path = relative(await realpath(root), await realpath(file));\n }\n return [name, { path: path.split(sep).join('/') }];\n }))));\n },\n snapshot,\n async load(key) {\n const record = await disk.load(join(directory, `${key}.json`));\n if (record === undefined) return undefined;\n const { contentHash, dependencies } = metadata(record);\n const bytes = await disk.load(join(directory, `${key}.bin`));\n if (bytes === undefined || digest(bytes) !== contentHash) return undefined;\n return { bytes, hashes: dependencies };\n },\n async save(key, value, files, expected) {\n if (!matches(await snapshot(files), expected)) throw new ChangedDependenciesError();\n await disk.ignore(root, directory);\n await disk.save(join(directory, `${key}.bin`), value, async () => {\n if (!matches(await snapshot(files), expected)) throw new ChangedDependenciesError();\n });\n const record: Metadata = { contentHash: digest(value), dependencies: expected };\n await disk.save(join(directory, `${key}.json`), new TextEncoder().encode(JSON.stringify(record)));\n },\n };\n}\n"]}
1
+ {"version":3,"file":"storage.js","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"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","sourcesContent":["import { join, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport * as disk from './disk.js';\nimport { ChangedDependenciesError, equalHashes, hashableDependencies, hashes, MissingDependencyError, resolvedDependencies } from './dependencies.js';\nimport { contentHash, fileHash } from './hash.js';\nimport { packageVersion } from './packages.js';\nimport type { Backend, CacheEntryOptions, Dependencies, Hashable, Hashes, Snapshot, Stored } from './types.js';\n\ninterface Metadata {\n contentHash: string;\n dependencies: Hashes;\n saved: Dependencies<Hashable>;\n /** The saved dependencies' hashes when the entry was saved. */\n savedHashes: Hashes;\n}\n\n/** A metadata file vitecache wrote; any failure to read it names the file, since no request caused it. */\nfunction metadata(bytes: Uint8Array, file: string): Metadata | undefined {\n try {\n const value: unknown = JSON.parse(new TextDecoder().decode(bytes));\n if (typeof value !== 'object' || value === null || !('contentHash' in value) || !('dependencies' in value)) {\n throw new Error('Expected contentHash and dependencies.');\n }\n const hash = value.contentHash;\n if (typeof hash !== 'string' || !/^[a-f0-9]{64}$/.test(hash)) throw new Error('Expected a content hash.');\n const validated = hashes(value.dependencies);\n // Metadata without saved dependencies: all that vitecache 0.1 wrote, and what 0.2 wrote for entries that had none.\n if (!('saved' in value)) return { contentHash: hash, dependencies: validated, saved: {}, savedHashes: {} };\n // vitecache 0.2 kept each saved dependency with its hash in one record. Its entry misses until a save replaces it.\n if (!('savedHashes' in value)) return undefined;\n return { contentHash: hash, dependencies: validated, saved: hashableDependencies(value.saved), savedHashes: hashes(value.savedHashes) };\n } catch (cause) {\n throw new Error(`Invalid vitecache metadata: ${file}`, { cause });\n }\n}\n\n/** A stored entry as storage reads it, its hashes always known. */\nexport interface Found extends Stored {\n /** The entity tag's opaque value: it changes whenever the entry's bytes or dependency hashes do. */\n tag: string;\n hashes: Hashes;\n}\n\nexport interface Storage extends Backend {\n /** Reads a valid entry and records its use for prune(). */\n load(key: string): Promise<Found | undefined>;\n}\n\nfunction matches(current: Snapshot, expected: Hashes): boolean {\n return current.status === 'current' && equalHashes(current.hashes, expected);\n}\n\nexport function createStorage(options: Pick<CacheEntryOptions, 'root' | 'basePath'>): Storage {\n const { root, directory } = disk.location(options);\n\n async function snapshot(dependencies: Dependencies<Hashable>): Promise<Exclude<Snapshot, { status: 'unchecked' }>> {\n const entries = await Promise.all(Object.entries(dependencies).map(async ([name, dependency]) => {\n if (dependency.package === undefined) return [name, await fileHash(resolve(root, dependency.path))] as const;\n const version = await packageVersion(root, dependency.package);\n return [name, version === undefined ? undefined : contentHash(new TextEncoder().encode(version))] as const;\n }));\n const present: [string, string][] = [];\n for (const [name, hash] of entries) {\n if (hash === undefined) return { status: 'missing', message: `Missing file or package dependency: ${name}` };\n present.push([name, hash]);\n }\n return { status: 'current', hashes: Object.fromEntries(present) };\n }\n\n async function load(key: string): Promise<Found | undefined> {\n const file = join(directory, `${key}.json`);\n const raw = await disk.load(file);\n if (raw === undefined) return undefined;\n const parsed = metadata(raw, file);\n if (!parsed || !matches(await snapshot(parsed.saved), parsed.savedHashes)) return undefined;\n const bytes = await disk.load(join(directory, `${key}.bin`));\n if (bytes === undefined || contentHash(bytes) !== parsed.contentHash) return undefined;\n // The metadata file's modification time is the entry's last use. The bytes keep theirs, so static hosts keep their\n // validators.\n await disk.touch(file);\n return { tag: contentHash(raw), hashes: parsed.dependencies, bytes };\n }\n\n return {\n async canSave() { return true; },\n async resolve(declared: Dependencies) {\n return resolvedDependencies(Object.fromEntries(await Promise.all(Object.entries(declared).map(async ([name, dependency]) => {\n if (dependency.url === undefined) return [name, dependency];\n const url = new URL(dependency.url);\n if (url.protocol !== 'file:') {\n throw new Error('Node file dependency URLs must use file:. Use a project-root-relative path for Vite-served files.');\n }\n return [name, { path: await disk.rootRelative(root, fileURLToPath(url)) }];\n }))));\n },\n snapshot,\n load,\n async save(key, value, { checked, expected, saved }) {\n const current = await snapshot(saved);\n if (current.status !== 'current') throw new MissingDependencyError(`Cannot save a cache entry with a missing saved dependency. ${current.message}`);\n await disk.ignore(root, directory);\n await disk.save(join(directory, `${key}.bin`), value, async () => {\n if (!matches(await snapshot(checked), expected)) throw new ChangedDependenciesError();\n });\n const record: Metadata = { contentHash: contentHash(value), dependencies: expected, saved, savedHashes: current.hashes };\n await disk.save(join(directory, `${key}.json`), new TextEncoder().encode(JSON.stringify(record)));\n },\n };\n}\n"]}
package/dist/types.d.ts CHANGED
@@ -23,32 +23,67 @@ export type Dependency = {
23
23
  readonly path?: never;
24
24
  readonly url?: never;
25
25
  };
26
- export type Dependencies = Readonly<Record<string, Dependency>>;
27
- export type ResolvedDependency = Exclude<Dependency, {
26
+ export type Dependencies<D extends Dependency = Dependency> = Readonly<Record<string, D>>;
27
+ /** A dependency whose URL, if it had one, was resolved to a path. */
28
+ export type Resolved<D extends Dependency = Dependency> = Exclude<D, {
28
29
  readonly url: string;
29
30
  }>;
30
- export type ResolvedDependencies = Readonly<Record<string, ResolvedDependency>>;
31
+ /** A file, URL, or package dependency: unlike a value, one whose current state can be checked again later. */
32
+ export type Checked<D extends Dependency = Dependency> = Exclude<D, {
33
+ readonly value: JsonValue;
34
+ }>;
35
+ /** A dependency passed to save(): only checked kinds, since values select the result. */
36
+ export type SavedDependency = Checked;
37
+ export type SavedDependencies = Dependencies<SavedDependency>;
38
+ /** A resolved checked dependency, a file path or a package: one whose current state can be hashed. */
39
+ export type Hashable = Resolved<Checked>;
31
40
  export type Paths = Readonly<Record<string, string>>;
41
+ /** Dependency names mapped to file-content or installed-package-version hashes. */
32
42
  export type Hashes = Readonly<Record<string, string>>;
43
+ /** Current dependency hashes, a missing dependency, or dependencies a browser without the plugin cannot check. */
44
+ export type Snapshot = {
45
+ readonly status: 'current';
46
+ readonly hashes: Hashes;
47
+ } | {
48
+ readonly status: 'missing';
49
+ readonly message: string;
50
+ } | {
51
+ readonly status: 'unchecked';
52
+ };
33
53
  export interface CacheEntryOptions {
34
54
  /** Identifies the computation and its implementation version, for example "simplify@2". */
35
55
  version: string;
36
- /** Named dependencies sorted by name. JSON values affect the key; file contents and installed package versions are validated in Node or through the Vite plugin. Paths are always project-root-relative. URLs resolve to those paths through Vite, or from local file: URLs in Node. Packages track only the declared package's installed version, without importing it. */
56
+ /** Named dependencies. JSON values affect the key; file contents and installed package versions are validated in Node or through the Vite plugin. Paths are always project-root-relative. URLs resolve to those paths through Vite, or from local file: URLs in Node. Packages track only the declared package's installed version, without importing it. */
37
57
  dependencies?: Dependencies;
38
58
  /** Disk directory in Node (default public/_vitecache); URL prefix in browsers (default /_vitecache/). Excluded from the key. */
39
59
  basePath?: string;
40
60
  /** Node project root, default process.cwd(). Browser dependencies resolve against the Vite root. Excluded from the key. */
41
61
  root?: string;
42
62
  }
63
+ export interface SaveOptions {
64
+ /**
65
+ * What the result was built from, when known only after computing it. They do not affect the key, so a save with
66
+ * different saved dependencies replaces the same entry. Rechecked by every load() where file contents are validated;
67
+ * a changed or missing one is a miss. Hashed when save() runs, so one that changed after the computation read it goes
68
+ * unnoticed until it changes again. One without a backing file makes save() throw MissingDependencyError. Values select
69
+ * the result, so they belong in the constructor.
70
+ */
71
+ dependencies?: SavedDependencies;
72
+ }
43
73
  export interface Stored {
44
74
  bytes: Uint8Array;
45
75
  hashes?: Hashes;
46
76
  }
47
77
  export interface Backend {
48
78
  canSave(): Promise<boolean>;
49
- resolve(dependencies: Dependencies): Promise<ResolvedDependencies>;
50
- /** null means a missing dependency; undefined means validation is unavailable. Values need no fingerprint. */
51
- snapshot(dependencies: ResolvedDependencies): Promise<Hashes | null | undefined>;
52
- load(key: string, validate: boolean): Promise<Stored | undefined>;
53
- save(key: string, value: Uint8Array, dependencies: ResolvedDependencies, expected: Hashes): Promise<void>;
79
+ resolve(dependencies: Dependencies): Promise<Dependencies<Resolved>>;
80
+ snapshot(dependencies: Dependencies<Hashable>): Promise<Snapshot>;
81
+ load(key: string): Promise<Stored | undefined>;
82
+ save(key: string, value: Uint8Array, check: {
83
+ /** Must still match `expected`. */
84
+ checked: Dependencies<Hashable>;
85
+ expected: Hashes;
86
+ /** Hashed now and rechecked on load. */
87
+ saved: Dependencies<Hashable>;
88
+ }): Promise<void>;
54
89
  }
package/dist/types.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"","sourcesContent":["export type JsonValue = null | boolean | number | string | readonly JsonValue[] | { readonly [key: string]: JsonValue };\n\n/** Exactly one source per dependency. Names and descriptors participate in cache identity. */\nexport type Dependency =\n | { readonly value: JsonValue; readonly path?: never; readonly url?: never; readonly package?: never }\n | { readonly path: string; readonly value?: never; readonly url?: never; readonly package?: never }\n | { readonly url: string; readonly value?: never; readonly path?: never; readonly package?: never }\n | { readonly package: string; readonly value?: never; readonly path?: never; readonly url?: never };\nexport type Dependencies = Readonly<Record<string, Dependency>>;\nexport type ResolvedDependency = Exclude<Dependency, { readonly url: string }>;\nexport type ResolvedDependencies = Readonly<Record<string, ResolvedDependency>>;\nexport type Paths = Readonly<Record<string, string>>;\nexport type Hashes = Readonly<Record<string, string>>;\n\nexport interface CacheEntryOptions {\n /** Identifies the computation and its implementation version, for example \"simplify@2\". */\n version: string;\n /** Named dependencies sorted by name. JSON values affect the key; file contents and installed package versions are validated in Node or through the Vite plugin. Paths are always project-root-relative. URLs resolve to those paths through Vite, or from local file: URLs in Node. Packages track only the declared package's installed version, without importing it. */\n dependencies?: Dependencies;\n /** Disk directory in Node (default public/_vitecache); URL prefix in browsers (default /_vitecache/). Excluded from the key. */\n basePath?: string;\n /** Node project root, default process.cwd(). Browser dependencies resolve against the Vite root. Excluded from the key. */\n root?: string;\n}\n\nexport interface Stored {\n bytes: Uint8Array;\n hashes?: Hashes;\n}\n\nexport interface Backend {\n canSave(): Promise<boolean>;\n resolve(dependencies: Dependencies): Promise<ResolvedDependencies>;\n /** null means a missing dependency; undefined means validation is unavailable. Values need no fingerprint. */\n snapshot(dependencies: ResolvedDependencies): Promise<Hashes | null | undefined>;\n load(key: string, validate: boolean): Promise<Stored | undefined>;\n save(key: string, value: Uint8Array, dependencies: ResolvedDependencies, expected: Hashes): Promise<void>;\n}\n"]}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"","sourcesContent":["export type JsonValue = null | boolean | number | string | readonly JsonValue[] | { readonly [key: string]: JsonValue };\n\n/** Exactly one source per dependency. Names and descriptors participate in cache identity. */\nexport type Dependency =\n | { readonly value: JsonValue; readonly path?: never; readonly url?: never; readonly package?: never }\n | { readonly path: string; readonly value?: never; readonly url?: never; readonly package?: never }\n | { readonly url: string; readonly value?: never; readonly path?: never; readonly package?: never }\n | { readonly package: string; readonly value?: never; readonly path?: never; readonly url?: never };\nexport type Dependencies<D extends Dependency = Dependency> = Readonly<Record<string, D>>;\n/** A dependency whose URL, if it had one, was resolved to a path. */\nexport type Resolved<D extends Dependency = Dependency> = Exclude<D, { readonly url: string }>;\n/** A file, URL, or package dependency: unlike a value, one whose current state can be checked again later. */\nexport type Checked<D extends Dependency = Dependency> = Exclude<D, { readonly value: JsonValue }>;\n/** A dependency passed to save(): only checked kinds, since values select the result. */\nexport type SavedDependency = Checked;\nexport type SavedDependencies = Dependencies<SavedDependency>;\n/** A resolved checked dependency, a file path or a package: one whose current state can be hashed. */\nexport type Hashable = Resolved<Checked>;\nexport type Paths = Readonly<Record<string, string>>;\n/** Dependency names mapped to file-content or installed-package-version hashes. */\nexport type Hashes = Readonly<Record<string, string>>;\n/** Current dependency hashes, a missing dependency, or dependencies a browser without the plugin cannot check. */\nexport type Snapshot =\n | { readonly status: 'current'; readonly hashes: Hashes }\n | { readonly status: 'missing'; readonly message: string }\n | { readonly status: 'unchecked' };\n\nexport interface CacheEntryOptions {\n /** Identifies the computation and its implementation version, for example \"simplify@2\". */\n version: string;\n /** Named dependencies. JSON values affect the key; file contents and installed package versions are validated in Node or through the Vite plugin. Paths are always project-root-relative. URLs resolve to those paths through Vite, or from local file: URLs in Node. Packages track only the declared package's installed version, without importing it. */\n dependencies?: Dependencies;\n /** Disk directory in Node (default public/_vitecache); URL prefix in browsers (default /_vitecache/). Excluded from the key. */\n basePath?: string;\n /** Node project root, default process.cwd(). Browser dependencies resolve against the Vite root. Excluded from the key. */\n root?: string;\n}\n\nexport interface SaveOptions {\n /**\n * What the result was built from, when known only after computing it. They do not affect the key, so a save with\n * different saved dependencies replaces the same entry. Rechecked by every load() where file contents are validated;\n * a changed or missing one is a miss. Hashed when save() runs, so one that changed after the computation read it goes\n * unnoticed until it changes again. One without a backing file makes save() throw MissingDependencyError. Values select\n * the result, so they belong in the constructor.\n */\n dependencies?: SavedDependencies;\n}\n\nexport interface Stored {\n bytes: Uint8Array;\n hashes?: Hashes;\n}\n\nexport interface Backend {\n canSave(): Promise<boolean>;\n resolve(dependencies: Dependencies): Promise<Dependencies<Resolved>>;\n snapshot(dependencies: Dependencies<Hashable>): Promise<Snapshot>;\n load(key: string): Promise<Stored | undefined>;\n save(key: string, value: Uint8Array, check: {\n /** Must still match `expected`. */\n checked: Dependencies<Hashable>;\n expected: Hashes;\n /** Hashed now and rechecked on load. */\n saved: Dependencies<Hashable>;\n }): Promise<void>;\n}\n"]}
@@ -1,25 +1,14 @@
1
1
  import { relative, resolve, sep } from 'node:path';
2
2
  import * as disk from '../disk.js';
3
- import { ChangedDependenciesError, createStorage } from '../storage.js';
4
- import { dependencies, hashes, resolvedDependencies } from '../dependencies.js';
5
- import { confine, inside, OutsideRootError, resolveDependencies } from './resolve.js';
3
+ import { createStorage } from '../storage.js';
4
+ import { ChangedDependenciesError, dependencies, hashableDependencies, hashes, MissingDependencyError } from '../dependencies.js';
5
+ import { confine, RequestError, resolveDependencies } from './resolve.js';
6
6
  import { assetUrl, publicPaths } from './manifest.js';
7
- class RequestError extends Error {
8
- status;
9
- constructor(status, message) {
10
- super(message);
11
- this.status = status;
12
- }
13
- }
14
7
  function status(error) {
15
8
  if (error instanceof RequestError)
16
9
  return error.status;
17
10
  if (error instanceof ChangedDependenciesError)
18
11
  return 409;
19
- if (error instanceof OutsideRootError)
20
- return 403;
21
- if (error instanceof TypeError || error instanceof URIError)
22
- return 400;
23
12
  return 500;
24
13
  }
25
14
  function allowed(request) {
@@ -54,7 +43,7 @@ function parse(value) {
54
43
  throw new RequestError(400, 'Expected valid JSON');
55
44
  }
56
45
  }
57
- function validate(value, parser) {
46
+ function parseRequest(value, parser) {
58
47
  try {
59
48
  return parser(value);
60
49
  }
@@ -64,20 +53,29 @@ function validate(value, parser) {
64
53
  throw new RequestError(400, error.message);
65
54
  }
66
55
  }
67
- function header(request, name) {
68
- const value = request.headers[name];
69
- if (typeof value !== 'string')
70
- throw new RequestError(400, `Missing ${name} header`);
71
- try {
72
- return parse(decodeURIComponent(value));
73
- }
74
- catch (error) {
75
- if (!(error instanceof URIError))
76
- throw error;
77
- throw new RequestError(400, 'Invalid encoded request header');
78
- }
56
+ /** A save body: a 4-byte big-endian metadata length, that much JSON metadata, then the cached bytes. */
57
+ function upload(bytes) {
58
+ if (bytes.byteLength < 4)
59
+ throw new RequestError(400, 'Expected a metadata length');
60
+ const end = 4 + new DataView(bytes.buffer, bytes.byteOffset, 4).getUint32(0);
61
+ if (end > bytes.byteLength)
62
+ throw new RequestError(400, 'Truncated save metadata');
63
+ const metadata = parse(new TextDecoder().decode(bytes.subarray(4, end)));
64
+ if (typeof metadata !== 'object' || metadata === null || !('checked' in metadata) || !('expected' in metadata) || !('saved' in metadata)) {
65
+ throw new RequestError(400, 'Expected checked, expected, and saved');
66
+ }
67
+ return { metadata, value: bytes.subarray(end) };
68
+ }
69
+ /** The `dependencies` of a same-origin POST's JSON body. */
70
+ async function posted(request) {
71
+ if (!allowed(request))
72
+ throw new RequestError(403, 'Cross-origin requests are forbidden');
73
+ const value = parse(new TextDecoder().decode(await body(request)));
74
+ if (typeof value !== 'object' || value === null || !('dependencies' in value))
75
+ throw new RequestError(400, 'Expected dependencies');
76
+ return value.dependencies;
79
77
  }
80
- function json(response, value) {
78
+ function sendJson(response, value) {
81
79
  response.setHeader('Content-Type', 'application/json');
82
80
  response.end(JSON.stringify(value));
83
81
  }
@@ -85,25 +83,19 @@ async function respond(request, response, name, storage, server) {
85
83
  const root = server.config.root;
86
84
  response.setHeader('Cache-Control', 'no-store');
87
85
  if (name === '__vitecache' && request.method === 'GET') {
88
- json(response, { vitecache: 1 });
86
+ sendJson(response, { vitecache: 1 });
89
87
  return;
90
88
  }
91
- if ((name === '__vitecache/snapshot' || name === '__vitecache/resolve') && request.method === 'POST') {
92
- if (!allowed(request))
93
- throw new RequestError(403, 'Cross-origin requests are forbidden');
94
- const value = parse(new TextDecoder().decode(await body(request)));
95
- if (typeof value !== 'object' || value === null || !('dependencies' in value)) {
96
- throw new RequestError(400, 'Expected dependencies');
97
- }
98
- if (name === '__vitecache/resolve') {
99
- const deps = validate(value.dependencies, dependencies);
100
- const origin = `${server.config.server.https ? 'https' : 'http'}://${request.headers.host}`;
101
- json(response, { dependencies: await resolveDependencies(server, deps, origin) });
102
- return;
103
- }
104
- const deps = validate(value.dependencies, resolvedDependencies);
89
+ if (name === '__vitecache/resolve' && request.method === 'POST') {
90
+ const deps = parseRequest(await posted(request), dependencies);
91
+ const origin = `${server.config.server.https ? 'https' : 'http'}://${request.headers.host}`;
92
+ sendJson(response, { dependencies: await resolveDependencies(server, deps, origin) });
93
+ return;
94
+ }
95
+ if (name === '__vitecache/snapshot' && request.method === 'POST') {
96
+ const deps = parseRequest(await posted(request), hashableDependencies);
105
97
  await confine(root, deps);
106
- json(response, { hashes: await storage.snapshot(deps) });
98
+ sendJson(response, await storage.snapshot(deps));
107
99
  return;
108
100
  }
109
101
  if (!/^[a-f0-9]{64}\.bin$/.test(name)) {
@@ -115,34 +107,47 @@ async function respond(request, response, name, storage, server) {
115
107
  if (request.method === 'PUT') {
116
108
  if (!allowed(request))
117
109
  throw new RequestError(403, 'Cross-origin requests are forbidden');
118
- const deps = validate(header(request, 'x-vitecache-dependencies'), resolvedDependencies);
119
- const expected = validate(header(request, 'x-vitecache-hashes'), hashes);
120
- await confine(root, deps);
121
- await storage.save(key, await body(request), deps, expected);
110
+ const { metadata, value } = upload(await body(request));
111
+ const checked = parseRequest(metadata.checked, hashableDependencies);
112
+ const expected = parseRequest(metadata.expected, hashes);
113
+ const saved = parseRequest(metadata.saved, hashableDependencies);
114
+ await confine(root, checked);
115
+ await confine(root, saved);
116
+ await storage.save(key, value, { checked, expected, saved });
122
117
  response.statusCode = 204;
123
118
  response.end();
124
119
  return;
125
120
  }
126
- if (request.method !== 'GET' && request.method !== 'HEAD') {
121
+ if (request.method !== 'GET') {
127
122
  response.statusCode = 405;
128
- response.setHeader('Allow', 'GET, HEAD, PUT');
123
+ response.setHeader('Allow', 'GET, PUT');
129
124
  response.end();
130
125
  return;
131
126
  }
132
- const stored = await storage.load(key, true);
127
+ const stored = await storage.load(key);
133
128
  if (stored === undefined) {
134
- response.statusCode = 404;
129
+ // Browsers log every 404 fetch as an error, and a miss is an expected answer.
130
+ response.statusCode = 204;
131
+ response.end();
132
+ return;
133
+ }
134
+ // Browsers keep the bytes and revalidate them on every load, so an unchanged entry costs a 304 rather than a download.
135
+ response.setHeader('Cache-Control', 'no-cache');
136
+ const tag = `"${stored.tag}"`;
137
+ response.setHeader('ETag', tag);
138
+ response.setHeader('X-Vitecache-Dependencies', encodeURIComponent(JSON.stringify(stored.hashes)));
139
+ if (request.headers['if-none-match'] === tag) {
140
+ response.statusCode = 304;
135
141
  response.end();
136
142
  return;
137
143
  }
138
144
  response.setHeader('Content-Type', 'application/octet-stream');
139
145
  response.setHeader('Content-Length', stored.bytes.byteLength);
140
- response.setHeader('X-Vitecache-Dependencies', encodeURIComponent(JSON.stringify(stored.hashes)));
141
- response.end(request.method === 'HEAD' ? undefined : stored.bytes);
146
+ response.end(stored.bytes);
142
147
  }
143
148
  /** Persist and validate entries in development; preserve asset URL mappings in production builds. */
144
149
  export function vitecache(options = {}) {
145
- let resolved;
150
+ let state;
146
151
  return {
147
152
  name: 'vitecache',
148
153
  enforce: 'post',
@@ -151,12 +156,12 @@ export function vitecache(options = {}) {
151
156
  throw new Error('vitecache requires Vite publicDir');
152
157
  const { root, directory } = disk.location({ root: config.root, ...options });
153
158
  const path = relative(config.publicDir, directory);
154
- if (!path || !inside(config.publicDir, directory)) {
159
+ if (!path || !disk.inside(config.publicDir, directory)) {
155
160
  throw new Error('vitecache basePath must be a directory inside Vite publicDir');
156
161
  }
157
162
  const base = new URL(config.base, 'http://vitecache').pathname.replace(/\/$/, '');
158
- resolved = {
159
- backend: createStorage({ root, ...options }),
163
+ state = {
164
+ storage: createStorage({ root, ...options }),
160
165
  root,
161
166
  directory,
162
167
  config,
@@ -166,10 +171,10 @@ export function vitecache(options = {}) {
166
171
  await disk.ignore(root, directory);
167
172
  },
168
173
  async generateBundle(_options, bundle) {
169
- const storage = resolved;
170
- if (!storage)
174
+ const resolved = state;
175
+ if (!resolved)
171
176
  throw new Error('vitecache configuration was not resolved');
172
- const paths = new Map(Object.entries(await publicPaths(storage.config, storage.directory)));
177
+ const paths = new Map(Object.entries(await publicPaths(resolved.config, resolved.directory)));
173
178
  for (const asset of Object.values(bundle)) {
174
179
  if (asset.type !== 'asset')
175
180
  continue;
@@ -179,25 +184,30 @@ export function vitecache(options = {}) {
179
184
  const original = originals[0];
180
185
  if (!original)
181
186
  continue;
182
- const file = relative(storage.root, resolve(storage.root, original));
183
- if (!inside(storage.root, resolve(storage.root, original)))
187
+ const file = resolve(resolved.root, original);
188
+ if (!disk.inside(resolved.root, file))
184
189
  continue;
185
- paths.set(assetUrl(asset.fileName), file.split(sep).join('/'));
190
+ paths.set(assetUrl(asset.fileName), relative(resolved.root, file).split(sep).join('/'));
186
191
  }
187
- const manifest = { paths: Object.fromEntries(paths), root: '../'.repeat(storage.cachePath.split('/').length + 1) };
188
- this.emitFile({ type: 'asset', fileName: `${storage.cachePath}/__vitecache/paths.json`, source: JSON.stringify(manifest) });
192
+ const manifest = { paths: Object.fromEntries(paths), root: '../'.repeat(resolved.cachePath.split('/').length + 1) };
193
+ this.emitFile({ type: 'asset', fileName: `${resolved.cachePath}/__vitecache/paths.json`, source: JSON.stringify(manifest) });
189
194
  },
190
195
  configureServer(server) {
191
- const storage = resolved;
192
- if (!storage)
196
+ const resolved = state;
197
+ if (!resolved)
193
198
  throw new Error('vitecache configuration was not resolved');
194
199
  server.middlewares.use((request, response, next) => {
195
200
  const path = new URL(request.url ?? '/', 'http://vitecache').pathname;
196
- if (!path.startsWith(storage.prefix)) {
201
+ if (!path.startsWith(resolved.prefix)) {
197
202
  next();
198
203
  return;
199
204
  }
200
- void respond(request, response, path.slice(storage.prefix.length), storage.backend, server).catch((error) => {
205
+ void respond(request, response, path.slice(resolved.prefix.length), resolved.storage, server).catch((error) => {
206
+ // A missing dependency is an expected answer, as in a snapshot: a body, and no error status for browsers to log.
207
+ if (error instanceof MissingDependencyError) {
208
+ sendJson(response, { status: 'missing', message: error.message });
209
+ return;
210
+ }
201
211
  response.statusCode = status(error);
202
212
  response.setHeader('Content-Type', 'text/plain');
203
213
  response.end(error instanceof Error ? error.message : 'vitecache request failed');