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/README.md +2 -6
- package/dist/browser.d.ts +2 -1
- package/dist/browser.js +105 -58
- package/dist/browser.js.map +1 -1
- package/dist/dependencies.d.ts +17 -3
- package/dist/dependencies.js +40 -11
- package/dist/dependencies.js.map +1 -1
- package/dist/disk.d.ts +18 -1
- package/dist/disk.js +51 -6
- package/dist/disk.js.map +1 -1
- package/dist/entry.d.ts +13 -4
- package/dist/entry.js +56 -21
- package/dist/entry.js.map +1 -1
- package/dist/hash.d.ts +4 -0
- package/dist/hash.js +41 -0
- package/dist/hash.js.map +1 -0
- package/dist/index.d.ts +1 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/key.d.ts +2 -2
- package/dist/key.js.map +1 -1
- package/dist/node.d.ts +2 -1
- package/dist/node.js +1 -0
- package/dist/node.js.map +1 -1
- package/dist/packages.d.ts +5 -0
- package/dist/packages.js +40 -15
- package/dist/packages.js.map +1 -1
- package/dist/prune.d.ts +11 -0
- package/dist/prune.js +71 -0
- package/dist/prune.js.map +1 -0
- package/dist/storage.d.ts +11 -4
- package/dist/storage.js +60 -65
- package/dist/storage.js.map +1 -1
- package/dist/types.d.ts +44 -9
- package/dist/types.js.map +1 -1
- package/dist/vite/index.js +80 -70
- package/dist/vite/index.js.map +1 -1
- package/dist/vite/manifest.js +3 -3
- package/dist/vite/manifest.js.map +1 -1
- package/dist/vite/resolve.d.ts +7 -6
- package/dist/vite/resolve.js +35 -26
- package/dist/vite/resolve.js.map +1 -1
- package/package.json +5 -1
- package/skills/vitecache/SKILL.md +31 -5
package/dist/storage.js
CHANGED
|
@@ -1,98 +1,93 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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
|
|
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).
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
52
|
-
if (
|
|
53
|
-
return
|
|
54
|
-
present.push(
|
|
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(
|
|
61
|
-
return resolvedDependencies(Object.fromEntries(await Promise.all(Object.entries(
|
|
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
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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(
|
|
87
|
+
if (!matches(await snapshot(checked), expected))
|
|
93
88
|
throw new ChangedDependenciesError();
|
|
94
89
|
});
|
|
95
|
-
const record = { contentHash:
|
|
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
|
};
|
package/dist/storage.js.map
CHANGED
|
@@ -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,
|
|
27
|
-
|
|
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
|
-
|
|
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
|
|
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<
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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,
|
|
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"]}
|
package/dist/vite/index.js
CHANGED
|
@@ -1,25 +1,14 @@
|
|
|
1
1
|
import { relative, resolve, sep } from 'node:path';
|
|
2
2
|
import * as disk from '../disk.js';
|
|
3
|
-
import {
|
|
4
|
-
import { dependencies, hashes,
|
|
5
|
-
import { confine,
|
|
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
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
if (
|
|
70
|
-
throw new RequestError(400,
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
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
|
-
|
|
86
|
+
sendJson(response, { vitecache: 1 });
|
|
89
87
|
return;
|
|
90
88
|
}
|
|
91
|
-
if (
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
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
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
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'
|
|
121
|
+
if (request.method !== 'GET') {
|
|
127
122
|
response.statusCode = 405;
|
|
128
|
-
response.setHeader('Allow', 'GET,
|
|
123
|
+
response.setHeader('Allow', 'GET, PUT');
|
|
129
124
|
response.end();
|
|
130
125
|
return;
|
|
131
126
|
}
|
|
132
|
-
const stored = await storage.load(key
|
|
127
|
+
const stored = await storage.load(key);
|
|
133
128
|
if (stored === undefined) {
|
|
134
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
159
|
-
|
|
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
|
|
170
|
-
if (!
|
|
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(
|
|
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 =
|
|
183
|
-
if (!inside(
|
|
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(
|
|
188
|
-
this.emitFile({ type: 'asset', fileName: `${
|
|
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
|
|
192
|
-
if (!
|
|
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(
|
|
201
|
+
if (!path.startsWith(resolved.prefix)) {
|
|
197
202
|
next();
|
|
198
203
|
return;
|
|
199
204
|
}
|
|
200
|
-
void respond(request, response, path.slice(
|
|
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');
|