vitecache 0.2.1 → 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,160 +1,93 @@
1
- import { createHash } from 'node:crypto';
2
- import { realpath, stat } 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, MissingDependencyError, resolvedDependencies, savedDependencies } 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
7
  /** A metadata file vitecache wrote; any failure to read it names the file, since no request caused it. */
17
8
  function metadata(bytes, file) {
18
9
  try {
19
- return parse(bytes);
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
26
  catch (cause) {
22
27
  throw new Error(`Invalid vitecache metadata: ${file}`, { cause });
23
28
  }
24
29
  }
25
- function parse(bytes) {
26
- const value = JSON.parse(new TextDecoder().decode(bytes));
27
- if (typeof value !== 'object' || value === null || !('contentHash' in value) || !('dependencies' in value)) {
28
- throw new Error('Expected contentHash and dependencies.');
29
- }
30
- const { contentHash, dependencies } = value;
31
- if (typeof contentHash !== 'string' || !/^[a-f0-9]{64}$/.test(contentHash))
32
- throw new Error('Expected a content hash.');
33
- if (!('saved' in value))
34
- return { contentHash, dependencies: hashes(dependencies) };
35
- if (typeof value.saved !== 'object' || value.saved === null)
36
- throw new Error('Expected saved dependencies.');
37
- const saved = Object.entries(value.saved).map(([name, item]) => {
38
- if (typeof item !== 'object' || item === null || !('dependency' in item) || !('hash' in item))
39
- throw new Error('Expected a saved dependency and its hash.');
40
- const dependency = resolvedDependencies(savedDependencies({ [name]: item.dependency }))[name];
41
- const hash = hashes({ [name]: item.hash })[name];
42
- if (dependency === undefined || hash === undefined)
43
- throw new Error('Expected a saved dependency and its hash.');
44
- return [name, { dependency, hash }];
45
- });
46
- return { contentHash, dependencies: hashes(dependencies), saved: Object.fromEntries(saved) };
47
- }
48
30
  function matches(current, expected) {
49
- return current.status === 'available' && equalHashes(current.hashes, expected);
31
+ return current.status === 'current' && equalHashes(current.hashes, expected);
50
32
  }
51
33
  export function createStorage(options) {
52
34
  const { root, directory } = disk.location(options);
53
- const fileHashes = new Map();
54
- async function fileHash(path) {
55
- if (!options.memoizeHashes) {
56
- const bytes = await disk.load(path);
57
- return bytes === undefined ? undefined : digest(bytes);
58
- }
59
- try {
60
- const { size, mtimeNs: mtime, ctimeNs: ctime } = await stat(path, { bigint: true });
61
- const cached = fileHashes.get(path);
62
- if (cached?.size === size && cached.mtime === mtime && cached.ctime === ctime)
63
- return await cached.hash;
64
- const hash = disk.load(path).then((bytes) => bytes === undefined ? undefined : digest(bytes));
65
- fileHashes.set(path, { size, mtime, ctime, hash });
66
- const value = await hash;
67
- if (value === undefined && fileHashes.get(path)?.hash === hash)
68
- fileHashes.delete(path);
69
- return value;
70
- }
71
- catch (error) {
72
- fileHashes.delete(path);
73
- throw error;
74
- }
75
- }
76
35
  async function snapshot(dependencies) {
77
- const entries = await Promise.all(Object.entries(dependencies).filter(([, dependency]) => !('value' in dependency)).map(async ([name, dependency]) => {
78
- try {
79
- if (dependency.package !== undefined) {
80
- const version = await packageVersion(root, dependency.package);
81
- return version === undefined ? null : [name, digest(new TextEncoder().encode(version))];
82
- }
83
- if (dependency.path === undefined)
84
- throw new TypeError('Expected a resolved dependency.');
85
- const hash = await fileHash(resolve(root, dependency.path));
86
- return hash === undefined ? null : [name, hash];
87
- }
88
- catch (error) {
89
- if (!disk.missingDependency(error))
90
- throw error;
91
- return null;
92
- }
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))];
93
41
  }));
94
42
  const present = [];
95
- for (const entry of entries) {
96
- if (entry === null)
97
- return { status: 'missing' };
98
- 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]);
99
47
  }
100
- return { status: 'available', hashes: 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 };
101
65
  }
102
66
  return {
103
67
  async canSave() { return true; },
104
- async resolve(dependencies) {
105
- return resolvedDependencies(Object.fromEntries(await Promise.all(Object.entries(dependencies).map(async ([name, dependency]) => {
68
+ async resolve(declared) {
69
+ return resolvedDependencies(Object.fromEntries(await Promise.all(Object.entries(declared).map(async ([name, dependency]) => {
106
70
  if (dependency.url === undefined)
107
71
  return [name, dependency];
108
72
  const url = new URL(dependency.url);
109
73
  if (url.protocol !== 'file:') {
110
74
  throw new Error('Node file dependency URLs must use file:. Use a project-root-relative path for Vite-served files.');
111
75
  }
112
- const file = fileURLToPath(url);
113
- let path = relative(root, file);
114
- if (path === '..' || path.startsWith(`..${sep}`) || isAbsolute(path)) {
115
- path = relative(await realpath(root), await realpath(file));
116
- }
117
- return [name, { path: path.split(sep).join('/') }];
76
+ return [name, { path: await disk.rootRelative(root, fileURLToPath(url)) }];
118
77
  }))));
119
78
  },
120
79
  snapshot,
121
- async load(key) {
122
- const file = join(directory, `${key}.json`);
123
- const record = await disk.load(file);
124
- if (record === undefined)
125
- return undefined;
126
- const { contentHash, dependencies, saved } = metadata(record, file);
127
- if (saved) {
128
- const entries = Object.entries(saved);
129
- const current = await snapshot(Object.fromEntries(entries.map(([name, { dependency }]) => [name, dependency])));
130
- if (!matches(current, Object.fromEntries(entries.map(([name, { hash }]) => [name, hash]))))
131
- return undefined;
132
- }
133
- const bytes = await disk.load(join(directory, `${key}.bin`));
134
- if (bytes === undefined || digest(bytes) !== contentHash)
135
- return undefined;
136
- return { bytes, hashes: dependencies };
137
- },
138
- async save(key, value, validation, expected, saved) {
139
- if (!matches(await snapshot(validation), expected))
140
- throw new ChangedDependenciesError();
80
+ load,
81
+ async save(key, value, { checked, expected, saved }) {
141
82
  const current = await snapshot(saved);
142
- if (current.status !== 'available')
143
- throw new MissingDependencyError('Cannot save a cache entry with missing saved dependencies.');
83
+ if (current.status !== 'current')
84
+ throw new MissingDependencyError(`Cannot save a cache entry with a missing saved dependency. ${current.message}`);
144
85
  await disk.ignore(root, directory);
145
86
  await disk.save(join(directory, `${key}.bin`), value, async () => {
146
- if (!matches(await snapshot(validation), expected))
87
+ if (!matches(await snapshot(checked), expected))
147
88
  throw new ChangedDependenciesError();
148
89
  });
149
- const record = { contentHash: digest(value), dependencies: expected };
150
- const entries = Object.entries(saved).map(([name, dependency]) => {
151
- const hash = current.hashes[name];
152
- if (hash === undefined)
153
- throw new Error(`vitecache did not hash saved dependency ${name}.`);
154
- return [name, { dependency, hash }];
155
- });
156
- if (entries.length)
157
- record.saved = Object.fromEntries(entries);
90
+ const record = { contentHash: contentHash(value), dependencies: expected, saved, savedHashes: current.hashes };
158
91
  await disk.save(join(directory, `${key}.json`), new TextEncoder().encode(JSON.stringify(record)));
159
92
  },
160
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, stat } 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, MissingDependencyError, resolvedDependencies, savedDependencies } from './dependencies.js';\nimport { packageVersion } from './packages.js';\nimport type { Backend, CacheEntryOptions, Dependencies, Hashes, ResolvedDependencies, ResolvedDependency, Snapshot } 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 /** Saved dependencies with their hashes at save time; absent when there were none. */\n saved?: Readonly<Record<string, { dependency: ResolvedDependency; hash: string }>>;\n}\n\nexport class ChangedDependenciesError extends Error {\n constructor() {\n super('vitecache dependencies changed during computation.');\n }\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 {\n try {\n return parse(bytes);\n } catch (cause) {\n throw new Error(`Invalid vitecache metadata: ${file}`, { cause });\n }\n}\n\nfunction parse(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('Expected contentHash and dependencies.');\n }\n const { contentHash, dependencies } = value;\n if (typeof contentHash !== 'string' || !/^[a-f0-9]{64}$/.test(contentHash)) throw new Error('Expected a content hash.');\n if (!('saved' in value)) return { contentHash, dependencies: hashes(dependencies) };\n if (typeof value.saved !== 'object' || value.saved === null) throw new Error('Expected saved dependencies.');\n const saved = Object.entries(value.saved).map(([name, item]: [string, unknown]) => {\n if (typeof item !== 'object' || item === null || !('dependency' in item) || !('hash' in item)) throw new Error('Expected a saved dependency and its hash.');\n const dependency = resolvedDependencies(savedDependencies({ [name]: item.dependency }))[name];\n const hash = hashes({ [name]: item.hash })[name];\n if (dependency === undefined || hash === undefined) throw new Error('Expected a saved dependency and its hash.');\n return [name, { dependency, hash }] as const;\n });\n return { contentHash, dependencies: hashes(dependencies), saved: Object.fromEntries(saved) };\n}\n\nfunction matches(current: Snapshot, expected: Hashes): boolean {\n return current.status === 'available' && equalHashes(current.hashes, expected);\n}\n\nexport function createStorage(options: Pick<CacheEntryOptions, 'root' | 'basePath'> & { memoizeHashes?: boolean }): Backend {\n const { root, directory } = disk.location(options);\n const fileHashes = new Map<string, { size: bigint; mtime: bigint; ctime: bigint; hash: Promise<string | undefined> }>();\n\n async function fileHash(path: string): Promise<string | undefined> {\n if (!options.memoizeHashes) {\n const bytes = await disk.load(path);\n return bytes === undefined ? undefined : digest(bytes);\n }\n try {\n const { size, mtimeNs: mtime, ctimeNs: ctime } = await stat(path, { bigint: true });\n const cached = fileHashes.get(path);\n if (cached?.size === size && cached.mtime === mtime && cached.ctime === ctime) return await cached.hash;\n const hash = disk.load(path).then((bytes) => bytes === undefined ? undefined : digest(bytes));\n fileHashes.set(path, { size, mtime, ctime, hash });\n const value = await hash;\n if (value === undefined && fileHashes.get(path)?.hash === hash) fileHashes.delete(path);\n return value;\n } catch (error) {\n fileHashes.delete(path);\n throw error;\n }\n }\n\n async function snapshot(dependencies: ResolvedDependencies): Promise<Snapshot> {\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 hash = await fileHash(resolve(root, dependency.path));\n return hash === undefined ? null : [name, hash] 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 { status: 'missing' };\n present.push(entry);\n }\n return { status: 'available', hashes: Object.fromEntries(present) };\n }\n\n return {\n async canSave() { return true; },\n async resolve(dependencies: Dependencies) {\n return resolvedDependencies(Object.fromEntries(await Promise.all(Object.entries(dependencies).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 file = join(directory, `${key}.json`);\n const record = await disk.load(file);\n if (record === undefined) return undefined;\n const { contentHash, dependencies, saved } = metadata(record, file);\n if (saved) {\n const entries = Object.entries(saved);\n const current = await snapshot(Object.fromEntries(entries.map(([name, { dependency }]) => [name, dependency])));\n if (!matches(current, Object.fromEntries(entries.map(([name, { hash }]) => [name, hash])))) return undefined;\n }\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, validation, expected, saved) {\n if (!matches(await snapshot(validation), expected)) throw new ChangedDependenciesError();\n const current = await snapshot(saved);\n if (current.status !== 'available') throw new MissingDependencyError('Cannot save a cache entry with missing saved dependencies.');\n await disk.ignore(root, directory);\n await disk.save(join(directory, `${key}.bin`), value, async () => {\n if (!matches(await snapshot(validation), expected)) throw new ChangedDependenciesError();\n });\n const record: Metadata = { contentHash: digest(value), dependencies: expected };\n const entries = Object.entries(saved).map(([name, dependency]) => {\n const hash = current.hashes[name];\n if (hash === undefined) throw new Error(`vitecache did not hash saved dependency ${name}.`);\n return [name, { dependency, hash }] as const;\n });\n if (entries.length) record.saved = Object.fromEntries(entries);\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,37 @@ 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
- /** A file, URL, or package dependency: one whose current state can be checked again later. */
31
- export type SavedDependency = Exclude<Dependency, {
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, {
32
33
  readonly value: JsonValue;
33
34
  }>;
34
- export type SavedDependencies = Readonly<Record<string, SavedDependency>>;
35
- export type ResolvedDependencies = Readonly<Record<string, ResolvedDependency>>;
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>;
36
40
  export type Paths = Readonly<Record<string, string>>;
37
41
  /** Dependency names mapped to file-content or installed-package-version hashes. */
38
42
  export type Hashes = Readonly<Record<string, string>>;
39
- /** Current dependency hashes, a missing dependency, or unavailable browser validation. */
43
+ /** Current dependency hashes, a missing dependency, or dependencies a browser without the plugin cannot check. */
40
44
  export type Snapshot = {
41
- readonly status: 'available';
45
+ readonly status: 'current';
42
46
  readonly hashes: Hashes;
43
47
  } | {
44
48
  readonly status: 'missing';
49
+ readonly message: string;
45
50
  } | {
46
- readonly status: 'unavailable';
51
+ readonly status: 'unchecked';
47
52
  };
48
53
  export interface CacheEntryOptions {
49
54
  /** Identifies the computation and its implementation version, for example "simplify@2". */
50
55
  version: string;
51
- /** 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. */
52
57
  dependencies?: Dependencies;
53
58
  /** Disk directory in Node (default public/_vitecache); URL prefix in browsers (default /_vitecache/). Excluded from the key. */
54
59
  basePath?: string;
@@ -71,9 +76,14 @@ export interface Stored {
71
76
  }
72
77
  export interface Backend {
73
78
  canSave(): Promise<boolean>;
74
- resolve(dependencies: Dependencies): Promise<ResolvedDependencies>;
75
- snapshot(dependencies: ResolvedDependencies): Promise<Snapshot>;
76
- load(key: string, validate: boolean): Promise<Stored | undefined>;
77
- /** `validation` must still match `expected`; `saved` is hashed now and rechecked on load. Neither holds values. */
78
- save(key: string, value: Uint8Array, validation: ResolvedDependencies, expected: Hashes, saved: ResolvedDependencies): 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>;
79
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 }>;\n/** A file, URL, or package dependency: one whose current state can be checked again later. */\nexport type SavedDependency = Exclude<Dependency, { readonly value: JsonValue }>;\nexport type SavedDependencies = Readonly<Record<string, SavedDependency>>;\nexport type ResolvedDependencies = Readonly<Record<string, ResolvedDependency>>;\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 unavailable browser validation. */\nexport type Snapshot =\n | { readonly status: 'available'; readonly hashes: Hashes }\n | { readonly status: 'missing' }\n | { readonly status: 'unavailable' };\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 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<ResolvedDependencies>;\n snapshot(dependencies: ResolvedDependencies): Promise<Snapshot>;\n load(key: string, validate: boolean): Promise<Stored | undefined>;\n /** `validation` must still match `expected`; `saved` is hashed now and rechecked on load. Neither holds values. */\n save(key: string, value: Uint8Array, validation: ResolvedDependencies, expected: Hashes, saved: ResolvedDependencies): 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,27 +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, MissingDependencyError, resolvedDependencies, savedDependencies } 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 MissingDependencyError)
20
- return 404;
21
- if (error instanceof OutsideRootError)
22
- return 403;
23
- if (error instanceof TypeError || error instanceof URIError)
24
- return 400;
25
12
  return 500;
26
13
  }
27
14
  function allowed(request) {
@@ -56,7 +43,7 @@ function parse(value) {
56
43
  throw new RequestError(400, 'Expected valid JSON');
57
44
  }
58
45
  }
59
- function validate(value, parser) {
46
+ function parseRequest(value, parser) {
60
47
  try {
61
48
  return parser(value);
62
49
  }
@@ -74,12 +61,21 @@ function upload(bytes) {
74
61
  if (end > bytes.byteLength)
75
62
  throw new RequestError(400, 'Truncated save metadata');
76
63
  const metadata = parse(new TextDecoder().decode(bytes.subarray(4, end)));
77
- if (typeof metadata !== 'object' || metadata === null || !('dependencies' in metadata) || !('hashes' in metadata) || !('saved' in metadata)) {
78
- throw new RequestError(400, 'Expected dependencies, hashes, and saved dependencies');
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');
79
66
  }
80
67
  return { metadata, value: bytes.subarray(end) };
81
68
  }
82
- function json(response, value) {
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;
77
+ }
78
+ function sendJson(response, value) {
83
79
  response.setHeader('Content-Type', 'application/json');
84
80
  response.end(JSON.stringify(value));
85
81
  }
@@ -87,25 +83,19 @@ async function respond(request, response, name, storage, server) {
87
83
  const root = server.config.root;
88
84
  response.setHeader('Cache-Control', 'no-store');
89
85
  if (name === '__vitecache' && request.method === 'GET') {
90
- json(response, { vitecache: 1 });
86
+ sendJson(response, { vitecache: 1 });
91
87
  return;
92
88
  }
93
- if ((name === '__vitecache/snapshot' || name === '__vitecache/resolve') && request.method === 'POST') {
94
- if (!allowed(request))
95
- throw new RequestError(403, 'Cross-origin requests are forbidden');
96
- const value = parse(new TextDecoder().decode(await body(request)));
97
- if (typeof value !== 'object' || value === null || !('dependencies' in value)) {
98
- throw new RequestError(400, 'Expected dependencies');
99
- }
100
- if (name === '__vitecache/resolve') {
101
- const deps = validate(value.dependencies, dependencies);
102
- const origin = `${server.config.server.https ? 'https' : 'http'}://${request.headers.host}`;
103
- json(response, { dependencies: await resolveDependencies(server, deps, origin) });
104
- return;
105
- }
106
- 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);
107
97
  await confine(root, deps);
108
- json(response, await storage.snapshot(deps));
98
+ sendJson(response, await storage.snapshot(deps));
109
99
  return;
110
100
  }
111
101
  if (!/^[a-f0-9]{64}\.bin$/.test(name)) {
@@ -118,37 +108,46 @@ async function respond(request, response, name, storage, server) {
118
108
  if (!allowed(request))
119
109
  throw new RequestError(403, 'Cross-origin requests are forbidden');
120
110
  const { metadata, value } = upload(await body(request));
121
- const validation = validate(metadata.dependencies, resolvedDependencies);
122
- const expected = validate(metadata.hashes, hashes);
123
- const saved = validate(metadata.saved, (input) => resolvedDependencies(savedDependencies(input)));
124
- await confine(root, validation);
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);
125
115
  await confine(root, saved);
126
- await storage.save(key, value, validation, expected, saved);
116
+ await storage.save(key, value, { checked, expected, saved });
127
117
  response.statusCode = 204;
128
118
  response.end();
129
119
  return;
130
120
  }
131
- if (request.method !== 'GET' && request.method !== 'HEAD') {
121
+ if (request.method !== 'GET') {
132
122
  response.statusCode = 405;
133
- response.setHeader('Allow', 'GET, HEAD, PUT');
123
+ response.setHeader('Allow', 'GET, PUT');
134
124
  response.end();
135
125
  return;
136
126
  }
137
- const stored = await storage.load(key, true);
127
+ const stored = await storage.load(key);
138
128
  if (stored === undefined) {
139
129
  // Browsers log every 404 fetch as an error, and a miss is an expected answer.
140
130
  response.statusCode = 204;
141
131
  response.end();
142
132
  return;
143
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;
141
+ response.end();
142
+ return;
143
+ }
144
144
  response.setHeader('Content-Type', 'application/octet-stream');
145
145
  response.setHeader('Content-Length', stored.bytes.byteLength);
146
- response.setHeader('X-Vitecache-Dependencies', encodeURIComponent(JSON.stringify(stored.hashes)));
147
- response.end(request.method === 'HEAD' ? undefined : stored.bytes);
146
+ response.end(stored.bytes);
148
147
  }
149
148
  /** Persist and validate entries in development; preserve asset URL mappings in production builds. */
150
149
  export function vitecache(options = {}) {
151
- let resolved;
150
+ let state;
152
151
  return {
153
152
  name: 'vitecache',
154
153
  enforce: 'post',
@@ -157,12 +156,12 @@ export function vitecache(options = {}) {
157
156
  throw new Error('vitecache requires Vite publicDir');
158
157
  const { root, directory } = disk.location({ root: config.root, ...options });
159
158
  const path = relative(config.publicDir, directory);
160
- if (!path || !inside(config.publicDir, directory)) {
159
+ if (!path || !disk.inside(config.publicDir, directory)) {
161
160
  throw new Error('vitecache basePath must be a directory inside Vite publicDir');
162
161
  }
163
162
  const base = new URL(config.base, 'http://vitecache').pathname.replace(/\/$/, '');
164
- resolved = {
165
- backend: createStorage({ root, ...options, memoizeHashes: config.command === 'serve' }),
163
+ state = {
164
+ storage: createStorage({ root, ...options }),
166
165
  root,
167
166
  directory,
168
167
  config,
@@ -172,10 +171,10 @@ export function vitecache(options = {}) {
172
171
  await disk.ignore(root, directory);
173
172
  },
174
173
  async generateBundle(_options, bundle) {
175
- const storage = resolved;
176
- if (!storage)
174
+ const resolved = state;
175
+ if (!resolved)
177
176
  throw new Error('vitecache configuration was not resolved');
178
- 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)));
179
178
  for (const asset of Object.values(bundle)) {
180
179
  if (asset.type !== 'asset')
181
180
  continue;
@@ -185,30 +184,31 @@ export function vitecache(options = {}) {
185
184
  const original = originals[0];
186
185
  if (!original)
187
186
  continue;
188
- const file = relative(storage.root, resolve(storage.root, original));
189
- if (!inside(storage.root, resolve(storage.root, original)))
187
+ const file = resolve(resolved.root, original);
188
+ if (!disk.inside(resolved.root, file))
190
189
  continue;
191
- paths.set(assetUrl(asset.fileName), file.split(sep).join('/'));
190
+ paths.set(assetUrl(asset.fileName), relative(resolved.root, file).split(sep).join('/'));
192
191
  }
193
- const manifest = { paths: Object.fromEntries(paths), root: '../'.repeat(storage.cachePath.split('/').length + 1) };
194
- 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) });
195
194
  },
196
195
  configureServer(server) {
197
- const storage = resolved;
198
- if (!storage)
196
+ const resolved = state;
197
+ if (!resolved)
199
198
  throw new Error('vitecache configuration was not resolved');
200
199
  server.middlewares.use((request, response, next) => {
201
200
  const path = new URL(request.url ?? '/', 'http://vitecache').pathname;
202
- if (!path.startsWith(storage.prefix)) {
201
+ if (!path.startsWith(resolved.prefix)) {
203
202
  next();
204
203
  return;
205
204
  }
206
- void respond(request, response, path.slice(storage.prefix.length), storage.backend, server).catch((error) => {
207
- response.statusCode = status(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.
208
207
  if (error instanceof MissingDependencyError) {
209
- json(response, { error: 'missing-dependency', message: error.message });
208
+ sendJson(response, { status: 'missing', message: error.message });
210
209
  return;
211
210
  }
211
+ response.statusCode = status(error);
212
212
  response.setHeader('Content-Type', 'text/plain');
213
213
  response.end(error instanceof Error ? error.message : 'vitecache request failed');
214
214
  });