vitecache 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -22
- package/dist/browser.d.ts +9 -6
- package/dist/browser.js +28 -178
- package/dist/browser.js.map +1 -1
- package/dist/discover.d.ts +15 -0
- package/dist/discover.js +131 -0
- package/dist/discover.js.map +1 -0
- package/dist/disk.d.ts +2 -26
- package/dist/disk.js +8 -107
- package/dist/disk.js.map +1 -1
- package/dist/entry.d.ts +29 -17
- package/dist/entry.js +11 -114
- package/dist/entry.js.map +1 -1
- package/dist/node.d.ts +9 -6
- package/dist/node.js +101 -6
- package/dist/node.js.map +1 -1
- package/dist/shared.d.ts +17 -0
- package/dist/shared.js +5 -0
- package/dist/shared.js.map +1 -0
- package/dist/vite.d.ts +12 -0
- package/dist/vite.js +253 -0
- package/dist/vite.js.map +1 -0
- package/package.json +17 -13
- package/skills/vitecache/SKILL.md +41 -46
- package/dist/dependencies.d.ts +0 -23
- package/dist/dependencies.js +0 -147
- package/dist/dependencies.js.map +0 -1
- package/dist/hash.d.ts +0 -4
- package/dist/hash.js +0 -41
- package/dist/hash.js.map +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -2
- package/dist/index.js.map +0 -1
- package/dist/key.d.ts +0 -3
- package/dist/key.js +0 -10
- package/dist/key.js.map +0 -1
- package/dist/packages.d.ts +0 -6
- package/dist/packages.js +0 -52
- package/dist/packages.js.map +0 -1
- package/dist/prune.d.ts +0 -11
- package/dist/prune.js +0 -71
- package/dist/prune.js.map +0 -1
- package/dist/storage.d.ts +0 -12
- package/dist/storage.js +0 -95
- package/dist/storage.js.map +0 -1
- package/dist/types.d.ts +0 -89
- package/dist/types.js +0 -2
- package/dist/types.js.map +0 -1
- package/dist/vite/index.d.ts +0 -8
- package/dist/vite/index.js +0 -220
- package/dist/vite/index.js.map +0 -1
- package/dist/vite/manifest.d.ts +0 -3
- package/dist/vite/manifest.js +0 -34
- package/dist/vite/manifest.js.map +0 -1
- package/dist/vite/resolve.d.ts +0 -9
- package/dist/vite/resolve.js +0 -90
- package/dist/vite/resolve.js.map +0 -1
package/dist/vite.js
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { appendFile, readdir, rm } from 'node:fs/promises';
|
|
2
|
+
import { join, relative, resolve } from 'node:path';
|
|
3
|
+
import colors from 'picocolors';
|
|
4
|
+
import picomatch from 'picomatch';
|
|
5
|
+
import { glob } from 'tinyglobby';
|
|
6
|
+
import { createServer, normalizePath } from 'vite';
|
|
7
|
+
import { missing, read } from './disk.js';
|
|
8
|
+
import { discover } from './discover.js';
|
|
9
|
+
import { shared } from './shared.js';
|
|
10
|
+
/** Adds the cache directory to the project's .gitignore, since its entries are generated. */
|
|
11
|
+
async function ignore(root, directory) {
|
|
12
|
+
const line = `${relative(root, directory).split('\\').join('/')}/*`;
|
|
13
|
+
const file = join(root, '.gitignore');
|
|
14
|
+
const contents = new TextDecoder().decode(await read(file));
|
|
15
|
+
if (contents.split(/\r?\n/).includes(line))
|
|
16
|
+
return;
|
|
17
|
+
await appendFile(file, `${contents && !contents.endsWith('\n') ? '\n' : ''}${line}\n`);
|
|
18
|
+
}
|
|
19
|
+
function stack(error) {
|
|
20
|
+
return error instanceof Error ? (error.stack ?? error.message) : String(error);
|
|
21
|
+
}
|
|
22
|
+
/** Source files discovery scans, as Tailwind scans for class names, outside dependencies and outputs. */
|
|
23
|
+
const sources = '**/*.{js,jsx,ts,tsx,mjs,mts}';
|
|
24
|
+
/**
|
|
25
|
+
* Finds cached loader loads in the project's source files and warms them in Node, so that browsers read what Node
|
|
26
|
+
* saved, then deletes the entries no load uses. In development it scans and warms at startup and again after every
|
|
27
|
+
* file change, holds cache requests until warming finishes, and deletes entries unused for `pruneAfter`. A build scans
|
|
28
|
+
* and warms once, checks that a second pass saves nothing, and deletes every entry it did not use.
|
|
29
|
+
*/
|
|
30
|
+
export function vitecache({ pruneAfter = 60_000 } = {}) {
|
|
31
|
+
// The server a build runs loaders with loads the config, and so this plugin, again.
|
|
32
|
+
if (shared.running)
|
|
33
|
+
return { name: 'vitecache' };
|
|
34
|
+
/** The loads each source file holds. */
|
|
35
|
+
const files = new Map();
|
|
36
|
+
let config;
|
|
37
|
+
let directory;
|
|
38
|
+
/** Runs loaders: the dev server itself, or in a build a server started for that. */
|
|
39
|
+
let server;
|
|
40
|
+
let queue = Promise.resolve();
|
|
41
|
+
/** Whether a pass is queued but has not started, which the next change then need not queue again. */
|
|
42
|
+
let queued = false;
|
|
43
|
+
/** When the latest pass started. Once the queue is idle, every load has used its entry since then. */
|
|
44
|
+
let started = 0;
|
|
45
|
+
let failed = false;
|
|
46
|
+
/** Set once the dev server closes, after which a running pass stops and nothing is pruned. */
|
|
47
|
+
let closing = false;
|
|
48
|
+
let timer;
|
|
49
|
+
/** Project-relative patterns of what is not scanned: dependencies, the build output, and public files. */
|
|
50
|
+
let excluded;
|
|
51
|
+
let isSource;
|
|
52
|
+
/** Records a file's loads. Development logs failures, since a file being edited can fail to parse. */
|
|
53
|
+
async function scan(file) {
|
|
54
|
+
try {
|
|
55
|
+
const code = new TextDecoder().decode(await read(file));
|
|
56
|
+
if (/Cached\w*Loader/.test(code))
|
|
57
|
+
files.set(file, discover(code, file));
|
|
58
|
+
else
|
|
59
|
+
files.delete(file);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
if (config.command === 'build')
|
|
63
|
+
throw error;
|
|
64
|
+
log(`${colors.red('cannot scan')} ${error instanceof Error ? error.message : String(error)}`, 'error');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async function run({ importer, source, name, args }) {
|
|
68
|
+
if (!server) {
|
|
69
|
+
shared.running = true;
|
|
70
|
+
try {
|
|
71
|
+
server = await createServer({
|
|
72
|
+
configFile: config.configFile ?? false,
|
|
73
|
+
root: config.root,
|
|
74
|
+
mode: config.mode,
|
|
75
|
+
logLevel: 'warn',
|
|
76
|
+
server: { middlewareMode: true, hmr: false, ws: false, watch: null },
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
shared.running = false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const resolved = await server.environments.ssr.pluginContainer.resolveId(source, importer);
|
|
84
|
+
if (!resolved)
|
|
85
|
+
throw new Error(`vitecache cannot resolve ${source} from ${importer}`);
|
|
86
|
+
const module = await server.ssrLoadModule(resolved.id);
|
|
87
|
+
const exported = module[name];
|
|
88
|
+
if (typeof exported !== 'function')
|
|
89
|
+
throw new Error(`${source} does not export a class ${name}`);
|
|
90
|
+
const instance = Reflect.construct(exported, []);
|
|
91
|
+
const loadable = typeof instance === 'object' && instance !== null && 'loadAsync' in instance;
|
|
92
|
+
if (!loadable || typeof instance.loadAsync !== 'function')
|
|
93
|
+
throw new Error(`${name} has no loadAsync method`);
|
|
94
|
+
await instance.loadAsync(...args);
|
|
95
|
+
}
|
|
96
|
+
/** Runs every load once, one at a time, logging progress and failures. */
|
|
97
|
+
/** Logs as Vite does with a timestamp, but tagged [vitecache], since Vite's own timestamp option tags lines [vite]. */
|
|
98
|
+
function log(message, level = 'info') {
|
|
99
|
+
const tag = colors.bold(level === 'info' ? colors.cyan('[vitecache]') : colors.red('[vitecache]'));
|
|
100
|
+
config.logger[level](`${colors.dim(new Date().toLocaleTimeString())} ${tag} ${message}`);
|
|
101
|
+
}
|
|
102
|
+
/** Runs every load once, one at a time. Hits are silent; a load that computes is logged, and announced when slow. */
|
|
103
|
+
async function pass() {
|
|
104
|
+
queued = false;
|
|
105
|
+
started = Date.now();
|
|
106
|
+
for (const load of [...files.values()].flat()) {
|
|
107
|
+
if (closing)
|
|
108
|
+
return;
|
|
109
|
+
const file = normalizePath(relative(config.root, load.importer));
|
|
110
|
+
const where = `${load.name} ${colors.dim(`${file}:${load.position}`)}`;
|
|
111
|
+
const start = Date.now();
|
|
112
|
+
const saves = shared.saves;
|
|
113
|
+
const slow = setTimeout(() => log(`${colors.yellow('computing')} ${where}…`), 500);
|
|
114
|
+
try {
|
|
115
|
+
await run(load);
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
failed = true;
|
|
119
|
+
log(`${colors.red('failed')} ${where}\n${stack(error)}`, 'error');
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
clearTimeout(slow);
|
|
124
|
+
}
|
|
125
|
+
const computed = shared.saves - saves;
|
|
126
|
+
if (!computed)
|
|
127
|
+
continue;
|
|
128
|
+
const time = Date.now() - start;
|
|
129
|
+
const took = time < 1000 ? `${time} ms` : `${(time / 1000).toFixed(1)} s`;
|
|
130
|
+
const entries = computed > 1 ? `, ${computed} entries` : '';
|
|
131
|
+
log(`${colors.green('computed')} ${where} ${colors.dim(`in ${took}${entries}`)}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/** Queues a pass unless one is already waiting. */
|
|
135
|
+
function warm() {
|
|
136
|
+
if (!queued) {
|
|
137
|
+
queued = true;
|
|
138
|
+
queue = queue.then(pass);
|
|
139
|
+
}
|
|
140
|
+
return idle();
|
|
141
|
+
}
|
|
142
|
+
async function idle() {
|
|
143
|
+
let current;
|
|
144
|
+
do {
|
|
145
|
+
current = queue;
|
|
146
|
+
await current;
|
|
147
|
+
} while (current !== queue);
|
|
148
|
+
}
|
|
149
|
+
/** Deletes the entries no load used since `before`. */
|
|
150
|
+
async function prune(before) {
|
|
151
|
+
const names = await readdir(directory).catch((error) => {
|
|
152
|
+
if (missing(error))
|
|
153
|
+
return [];
|
|
154
|
+
throw error;
|
|
155
|
+
});
|
|
156
|
+
for (const name of names) {
|
|
157
|
+
const key = /^([a-f0-9]{64})\.(?:bin|json)$/.exec(name)?.[1];
|
|
158
|
+
if (key && (shared.used.get(key) ?? 0) < before)
|
|
159
|
+
await rm(join(directory, name), { force: true });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/** Prunes in development, once idle: entries the latest pass used stay, others until `pruneAfter` past their use. */
|
|
163
|
+
async function expire() {
|
|
164
|
+
await idle();
|
|
165
|
+
if (closing)
|
|
166
|
+
return;
|
|
167
|
+
await prune(Math.min(started, Date.now() - pruneAfter));
|
|
168
|
+
}
|
|
169
|
+
async function scanAll() {
|
|
170
|
+
for (const file of await glob(sources, { cwd: config.root, absolute: true, ignore: excluded }))
|
|
171
|
+
await scan(file);
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
name: 'vitecache',
|
|
175
|
+
async configResolved(resolved) {
|
|
176
|
+
config = resolved;
|
|
177
|
+
if (!config.publicDir)
|
|
178
|
+
throw new Error('vitecache requires Vite publicDir');
|
|
179
|
+
// Normalized, as are the files hotUpdate is given.
|
|
180
|
+
directory = normalizePath(join(config.publicDir, '_vitecache'));
|
|
181
|
+
shared.root = config.root;
|
|
182
|
+
shared.directory = directory;
|
|
183
|
+
const outputs = [config.build.outDir, config.publicDir].map((path) => relative(config.root, resolve(config.root, path)));
|
|
184
|
+
// An output at the root itself excludes nothing, since the root holds the sources.
|
|
185
|
+
excluded = ['**/node_modules/**', ...outputs.filter(Boolean).map((path) => `${normalizePath(path)}/**`)];
|
|
186
|
+
isSource = picomatch(sources, { ignore: excluded });
|
|
187
|
+
await ignore(config.root, directory);
|
|
188
|
+
},
|
|
189
|
+
async buildStart() {
|
|
190
|
+
if (config.command !== 'build' || this.environment.name !== 'client')
|
|
191
|
+
return;
|
|
192
|
+
const start = Date.now();
|
|
193
|
+
failed = false;
|
|
194
|
+
try {
|
|
195
|
+
await scanAll();
|
|
196
|
+
await warm();
|
|
197
|
+
if (failed)
|
|
198
|
+
throw new Error('vitecache: warming failed, see the errors above.');
|
|
199
|
+
const saves = shared.saves;
|
|
200
|
+
await warm();
|
|
201
|
+
if (failed)
|
|
202
|
+
throw new Error('vitecache: warming failed, see the errors above.');
|
|
203
|
+
if (shared.saves !== saves)
|
|
204
|
+
throw new Error('vitecache: a second warming saved entries again, so their keys are not stable.');
|
|
205
|
+
}
|
|
206
|
+
finally {
|
|
207
|
+
await server?.close();
|
|
208
|
+
server = undefined;
|
|
209
|
+
}
|
|
210
|
+
await prune(start);
|
|
211
|
+
},
|
|
212
|
+
configureServer(devServer) {
|
|
213
|
+
server = devServer;
|
|
214
|
+
const prefix = `${config.base}_vitecache/`;
|
|
215
|
+
// Serves entries itself, after warming: Vite's public file serving learns of new files only through its watcher.
|
|
216
|
+
devServer.middlewares.use((request, response, next) => {
|
|
217
|
+
if (!request.url?.startsWith(prefix))
|
|
218
|
+
return next();
|
|
219
|
+
const name = new URL(request.url, 'http://vitecache').pathname.slice(prefix.length);
|
|
220
|
+
if (!/^[a-f0-9]{64}\.bin$/.test(name))
|
|
221
|
+
return next();
|
|
222
|
+
void idle()
|
|
223
|
+
.then(() => read(join(directory, name)))
|
|
224
|
+
.then((bytes) => {
|
|
225
|
+
if (!bytes)
|
|
226
|
+
return next();
|
|
227
|
+
response.setHeader('Content-Type', 'application/octet-stream');
|
|
228
|
+
response.setHeader('Cache-Control', 'no-cache');
|
|
229
|
+
response.end(bytes);
|
|
230
|
+
}, next);
|
|
231
|
+
});
|
|
232
|
+
const report = (error) => log(stack(error), 'error');
|
|
233
|
+
// Queued, so that requests wait for the scan too.
|
|
234
|
+
queue = queue.then(scanAll).catch(report);
|
|
235
|
+
void warm().then(expire).catch(report);
|
|
236
|
+
timer = setInterval(() => void expire().catch(report), pruneAfter);
|
|
237
|
+
},
|
|
238
|
+
async hotUpdate({ file }) {
|
|
239
|
+
if (this.environment.name !== 'client' || file.startsWith(directory))
|
|
240
|
+
return;
|
|
241
|
+
if (isSource(normalizePath(relative(config.root, file))))
|
|
242
|
+
await scan(file);
|
|
243
|
+
// Any file can be an input, and an unchanged entry warms with one load. Saves are file changes too.
|
|
244
|
+
void warm();
|
|
245
|
+
},
|
|
246
|
+
async closeBundle() {
|
|
247
|
+
closing = true;
|
|
248
|
+
clearInterval(timer);
|
|
249
|
+
await idle();
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
//# sourceMappingURL=vite.js.map
|
package/dist/vite.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vite.js","sourceRoot":"","sources":["../src/vite.ts"],"names":[],"mappings":"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","sourcesContent":["import { appendFile, readdir, rm } from 'node:fs/promises';\nimport { join, relative, resolve } from 'node:path';\nimport colors from 'picocolors';\nimport picomatch from 'picomatch';\nimport { glob } from 'tinyglobby';\nimport { createServer, normalizePath, type Plugin, type ResolvedConfig, type ViteDevServer } from 'vite';\nimport { missing, read } from './disk.js';\nimport { discover, type Load } from './discover.js';\nimport { shared } from './shared.js';\n\n/** Adds the cache directory to the project's .gitignore, since its entries are generated. */\nasync function ignore(root: string, directory: string): Promise<void> {\n const line = `${relative(root, directory).split('\\\\').join('/')}/*`;\n const file = join(root, '.gitignore');\n const contents = new TextDecoder().decode(await read(file));\n if (contents.split(/\\r?\\n/).includes(line)) return;\n await appendFile(file, `${contents && !contents.endsWith('\\n') ? '\\n' : ''}${line}\\n`);\n}\n\nfunction stack(error: unknown): string {\n return error instanceof Error ? (error.stack ?? error.message) : String(error);\n}\n\n/** Source files discovery scans, as Tailwind scans for class names, outside dependencies and outputs. */\nconst sources = '**/*.{js,jsx,ts,tsx,mjs,mts}';\n\nexport interface VitecacheOptions {\n /** Development: milliseconds an entry stays after the last load that used it, so that an undone edit reuses it. */\n pruneAfter?: number;\n}\n\n/**\n * Finds cached loader loads in the project's source files and warms them in Node, so that browsers read what Node\n * saved, then deletes the entries no load uses. In development it scans and warms at startup and again after every\n * file change, holds cache requests until warming finishes, and deletes entries unused for `pruneAfter`. A build scans\n * and warms once, checks that a second pass saves nothing, and deletes every entry it did not use.\n */\nexport function vitecache({ pruneAfter = 60_000 }: VitecacheOptions = {}): Plugin {\n // The server a build runs loaders with loads the config, and so this plugin, again.\n if (shared.running) return { name: 'vitecache' };\n /** The loads each source file holds. */\n const files = new Map<string, Load[]>();\n let config: ResolvedConfig;\n let directory: string;\n /** Runs loaders: the dev server itself, or in a build a server started for that. */\n let server: ViteDevServer | undefined;\n let queue = Promise.resolve();\n /** Whether a pass is queued but has not started, which the next change then need not queue again. */\n let queued = false;\n /** When the latest pass started. Once the queue is idle, every load has used its entry since then. */\n let started = 0;\n let failed = false;\n /** Set once the dev server closes, after which a running pass stops and nothing is pruned. */\n let closing = false;\n let timer: NodeJS.Timeout | undefined;\n\n /** Project-relative patterns of what is not scanned: dependencies, the build output, and public files. */\n let excluded: string[];\n let isSource: (file: string) => boolean;\n\n /** Records a file's loads. Development logs failures, since a file being edited can fail to parse. */\n async function scan(file: string): Promise<void> {\n try {\n const code = new TextDecoder().decode(await read(file));\n if (/Cached\\w*Loader/.test(code)) files.set(file, discover(code, file));\n else files.delete(file);\n } catch (error) {\n if (config.command === 'build') throw error;\n log(`${colors.red('cannot scan')} ${error instanceof Error ? error.message : String(error)}`, 'error');\n }\n }\n\n async function run({ importer, source, name, args }: Load): Promise<void> {\n if (!server) {\n shared.running = true;\n try {\n server = await createServer({\n configFile: config.configFile ?? false,\n root: config.root,\n mode: config.mode,\n logLevel: 'warn',\n server: { middlewareMode: true, hmr: false, ws: false, watch: null },\n });\n } finally {\n shared.running = false;\n }\n }\n const resolved = await server.environments.ssr.pluginContainer.resolveId(source, importer);\n if (!resolved) throw new Error(`vitecache cannot resolve ${source} from ${importer}`);\n const module: Record<string, unknown> = await server.ssrLoadModule(resolved.id);\n const exported = module[name];\n if (typeof exported !== 'function') throw new Error(`${source} does not export a class ${name}`);\n const instance: unknown = Reflect.construct(exported, []);\n const loadable = typeof instance === 'object' && instance !== null && 'loadAsync' in instance;\n if (!loadable || typeof instance.loadAsync !== 'function') throw new Error(`${name} has no loadAsync method`);\n await instance.loadAsync(...args);\n }\n\n /** Runs every load once, one at a time, logging progress and failures. */\n /** Logs as Vite does with a timestamp, but tagged [vitecache], since Vite's own timestamp option tags lines [vite]. */\n function log(message: string, level: 'info' | 'error' = 'info'): void {\n const tag = colors.bold(level === 'info' ? colors.cyan('[vitecache]') : colors.red('[vitecache]'));\n config.logger[level](`${colors.dim(new Date().toLocaleTimeString())} ${tag} ${message}`);\n }\n\n /** Runs every load once, one at a time. Hits are silent; a load that computes is logged, and announced when slow. */\n async function pass(): Promise<void> {\n queued = false;\n started = Date.now();\n for (const load of [...files.values()].flat()) {\n if (closing) return;\n const file = normalizePath(relative(config.root, load.importer));\n const where = `${load.name} ${colors.dim(`${file}:${load.position}`)}`;\n const start = Date.now();\n const saves = shared.saves;\n const slow = setTimeout(() => log(`${colors.yellow('computing')} ${where}…`), 500);\n try {\n await run(load);\n } catch (error) {\n failed = true;\n log(`${colors.red('failed')} ${where}\\n${stack(error)}`, 'error');\n continue;\n } finally {\n clearTimeout(slow);\n }\n const computed = shared.saves - saves;\n if (!computed) continue;\n const time = Date.now() - start;\n const took = time < 1000 ? `${time} ms` : `${(time / 1000).toFixed(1)} s`;\n const entries = computed > 1 ? `, ${computed} entries` : '';\n log(`${colors.green('computed')} ${where} ${colors.dim(`in ${took}${entries}`)}`);\n }\n }\n\n /** Queues a pass unless one is already waiting. */\n function warm(): Promise<void> {\n if (!queued) {\n queued = true;\n queue = queue.then(pass);\n }\n return idle();\n }\n\n async function idle(): Promise<void> {\n let current: Promise<void>;\n do {\n current = queue;\n await current;\n } while (current !== queue);\n }\n\n /** Deletes the entries no load used since `before`. */\n async function prune(before: number): Promise<void> {\n const names = await readdir(directory).catch((error: unknown) => {\n if (missing(error)) return [];\n throw error;\n });\n for (const name of names) {\n const key = /^([a-f0-9]{64})\\.(?:bin|json)$/.exec(name)?.[1];\n if (key && (shared.used.get(key) ?? 0) < before) await rm(join(directory, name), { force: true });\n }\n }\n\n /** Prunes in development, once idle: entries the latest pass used stay, others until `pruneAfter` past their use. */\n async function expire(): Promise<void> {\n await idle();\n if (closing) return;\n await prune(Math.min(started, Date.now() - pruneAfter));\n }\n\n async function scanAll(): Promise<void> {\n for (const file of await glob(sources, { cwd: config.root, absolute: true, ignore: excluded })) await scan(file);\n }\n\n return {\n name: 'vitecache',\n async configResolved(resolved) {\n config = resolved;\n if (!config.publicDir) throw new Error('vitecache requires Vite publicDir');\n // Normalized, as are the files hotUpdate is given.\n directory = normalizePath(join(config.publicDir, '_vitecache'));\n shared.root = config.root;\n shared.directory = directory;\n const outputs = [config.build.outDir, config.publicDir].map((path) =>\n relative(config.root, resolve(config.root, path)),\n );\n // An output at the root itself excludes nothing, since the root holds the sources.\n excluded = ['**/node_modules/**', ...outputs.filter(Boolean).map((path) => `${normalizePath(path)}/**`)];\n isSource = picomatch(sources, { ignore: excluded });\n await ignore(config.root, directory);\n },\n async buildStart() {\n if (config.command !== 'build' || this.environment.name !== 'client') return;\n const start = Date.now();\n failed = false;\n try {\n await scanAll();\n await warm();\n if (failed) throw new Error('vitecache: warming failed, see the errors above.');\n const saves = shared.saves;\n await warm();\n if (failed) throw new Error('vitecache: warming failed, see the errors above.');\n if (shared.saves !== saves)\n throw new Error('vitecache: a second warming saved entries again, so their keys are not stable.');\n } finally {\n await server?.close();\n server = undefined;\n }\n await prune(start);\n },\n configureServer(devServer) {\n server = devServer;\n const prefix = `${config.base}_vitecache/`;\n // Serves entries itself, after warming: Vite's public file serving learns of new files only through its watcher.\n devServer.middlewares.use((request, response, next) => {\n if (!request.url?.startsWith(prefix)) return next();\n const name = new URL(request.url, 'http://vitecache').pathname.slice(prefix.length);\n if (!/^[a-f0-9]{64}\\.bin$/.test(name)) return next();\n void idle()\n .then(() => read(join(directory, name)))\n .then((bytes) => {\n if (!bytes) return next();\n response.setHeader('Content-Type', 'application/octet-stream');\n response.setHeader('Cache-Control', 'no-cache');\n response.end(bytes);\n }, next);\n });\n const report = (error: unknown) => log(stack(error), 'error');\n // Queued, so that requests wait for the scan too.\n queue = queue.then(scanAll).catch(report);\n void warm().then(expire).catch(report);\n timer = setInterval(() => void expire().catch(report), pruneAfter);\n },\n async hotUpdate({ file }) {\n if (this.environment.name !== 'client' || file.startsWith(directory)) return;\n if (isSource(normalizePath(relative(config.root, file)))) await scan(file);\n // Any file can be an input, and an unchanged entry warms with one load. Saves are file changes too.\n void warm();\n },\n async closeBundle() {\n closing = true;\n clearInterval(timer);\n await idle();\n },\n };\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vitecache",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Persistent binary caching for expensive computation in browsers and Node.",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Persistent binary caching for expensive computation, warmed in Node by a Vite plugin and read in browsers and Node.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -22,22 +22,18 @@
|
|
|
22
22
|
"typescript"
|
|
23
23
|
],
|
|
24
24
|
"sideEffects": false,
|
|
25
|
-
"main": "./dist/
|
|
26
|
-
"types": "./dist/
|
|
25
|
+
"main": "./dist/browser.js",
|
|
26
|
+
"types": "./dist/browser.d.ts",
|
|
27
27
|
"exports": {
|
|
28
28
|
".": {
|
|
29
|
-
"types": "./dist/
|
|
29
|
+
"types": "./dist/browser.d.ts",
|
|
30
30
|
"browser": "./dist/browser.js",
|
|
31
31
|
"node": "./dist/node.js",
|
|
32
32
|
"default": "./dist/browser.js"
|
|
33
33
|
},
|
|
34
|
-
"./prune": {
|
|
35
|
-
"types": "./dist/prune.d.ts",
|
|
36
|
-
"default": "./dist/prune.js"
|
|
37
|
-
},
|
|
38
34
|
"./vite": {
|
|
39
|
-
"types": "./dist/vite
|
|
40
|
-
"default": "./dist/vite
|
|
35
|
+
"types": "./dist/vite.d.ts",
|
|
36
|
+
"default": "./dist/vite.js"
|
|
41
37
|
}
|
|
42
38
|
},
|
|
43
39
|
"files": [
|
|
@@ -56,10 +52,16 @@
|
|
|
56
52
|
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json --noEmit"
|
|
57
53
|
},
|
|
58
54
|
"dependencies": {
|
|
59
|
-
"@noble/hashes": "^2.0.1"
|
|
55
|
+
"@noble/hashes": "^2.0.1",
|
|
56
|
+
"json5": "2.2.3",
|
|
57
|
+
"picocolors": "1.1.1",
|
|
58
|
+
"picomatch": "4.0.3",
|
|
59
|
+
"safe-stable-stringify": "2.5.0",
|
|
60
|
+
"tinyglobby": "0.2.15",
|
|
61
|
+
"write-file-atomic": "6.0.0"
|
|
60
62
|
},
|
|
61
63
|
"peerDependencies": {
|
|
62
|
-
"vite": ">=
|
|
64
|
+
"vite": ">=8.0.0"
|
|
63
65
|
},
|
|
64
66
|
"peerDependenciesMeta": {
|
|
65
67
|
"vite": {
|
|
@@ -67,6 +69,8 @@
|
|
|
67
69
|
}
|
|
68
70
|
},
|
|
69
71
|
"devDependencies": {
|
|
72
|
+
"@types/picomatch": "4.0.2",
|
|
73
|
+
"@types/write-file-atomic": "4.0.3",
|
|
70
74
|
"vite": "^8.0.0",
|
|
71
75
|
"vitest": "^4.0.17"
|
|
72
76
|
},
|
|
@@ -1,54 +1,49 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: vitecache
|
|
3
|
-
description: Cache expensive computation with vitecache in
|
|
3
|
+
description: Cache expensive computation with vitecache: wrap it in a cached loader that the Vite plugin finds and computes in Node during development and builds, so browsers and Node read the saved bytes instead of recomputing.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# vitecache
|
|
7
7
|
|
|
8
|
-
Use vitecache when recomputing a result costs more than reading its encoded bytes. The application owns encoding and decoding.
|
|
8
|
+
Use vitecache when recomputing a result costs more than reading its encoded bytes. The application owns encoding and decoding.
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
## Cached loaders
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
Load once and compute on a miss. Reuse that entry when saving: loading captures file contents and package versions before computation, allowing saving to reject a result if those dependencies changed in the meantime. A new entry or a save without a preceding load cannot establish that relationship. Entries containing only value dependencies can save directly. If saving rejects with `ChangedDependenciesError`, load again and recompute before retrying. Use a separate entry for each concurrent computation.
|
|
12
|
+
Put each computation in an exported class whose name matches `Cached…Loader`, with a `loadAsync` method, as a three.js `Loader` has; extending one works. The plugin constructs it without arguments, so let only `loadAsync`'s arguments select the result: constructor options, such as a `LoadingManager`, must not change the bytes it saves. `loadAsync` creates a `CacheEntry`, loads, computes on a miss, and saves with the same entry. Call `load()` on every call, without a memo in front of it: the plugin keeps entries that loads use, and an entry a memo answers for goes unused and is deleted. `save()` records the inputs as the preceding `load()` found them, so one that changes during the computation makes the next load miss instead of serving a stale result. Import the computation lazily inside the miss branch, so pages that hit never download its code.
|
|
15
13
|
|
|
16
14
|
```ts
|
|
17
15
|
import { CacheEntry } from "vitecache"
|
|
18
|
-
import modelUrl from "./models/chair.glb?url"
|
|
19
|
-
|
|
20
|
-
const entry = new CacheEntry({
|
|
21
|
-
version: "simplify@2",
|
|
22
|
-
dependencies: {
|
|
23
|
-
settings: { value: { ratio: 0.5, preserveUVs: true } },
|
|
24
|
-
algorithm: { path: "src/simplify.ts" },
|
|
25
|
-
model: { url: modelUrl },
|
|
26
|
-
},
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
let bytes = await entry.load()
|
|
30
|
-
|
|
31
|
-
if (bytes === undefined) {
|
|
32
|
-
bytes = encodeMesh(await simplify())
|
|
33
|
-
await entry.save(bytes)
|
|
34
|
-
}
|
|
35
16
|
|
|
36
|
-
|
|
17
|
+
export class CachedMeshLoader {
|
|
18
|
+
async loadAsync(settings: { ratio: number }): Promise<Mesh> {
|
|
19
|
+
const entry = new CacheEntry({
|
|
20
|
+
version: "simplify@2",
|
|
21
|
+
dependencies: {
|
|
22
|
+
settings: { value: settings },
|
|
23
|
+
algorithm: { path: "src/simplify.ts" },
|
|
24
|
+
engine: { package: "meshoptimizer" },
|
|
25
|
+
},
|
|
26
|
+
})
|
|
27
|
+
let bytes = await entry.load()
|
|
28
|
+
if (bytes === undefined) {
|
|
29
|
+
const { simplify } = await import("./simplify.ts")
|
|
30
|
+
bytes = encodeMesh(await simplify(settings))
|
|
31
|
+
await entry.save(bytes)
|
|
32
|
+
}
|
|
33
|
+
return decodeMesh(bytes)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
37
36
|
```
|
|
38
37
|
|
|
39
|
-
|
|
38
|
+
Declare every input that affects the result, under a stable name. A `{ value }` is JSON that selects which result the entry holds: changing it, a name, a path or a package name changes the key, while reordering object properties does not. File contents and package versions invalidate the entry without changing its key.
|
|
40
39
|
|
|
41
|
-
|
|
42
|
-
const entry = new CacheEntry({ version: "bake@1", dependencies: { scene: { url: sceneUrl } } })
|
|
43
|
-
let bytes = await entry.load()
|
|
44
|
-
if (bytes === undefined) {
|
|
45
|
-
const { result, files } = await bake(sceneUrl) // files: URLs the bake read
|
|
46
|
-
bytes = encode(result)
|
|
47
|
-
await entry.save(bytes, { dependencies: Object.fromEntries(files.map((url, i) => [`file:${i}`, { url }])) })
|
|
48
|
-
}
|
|
49
|
-
```
|
|
40
|
+
A `{ path }` is relative to the project root, never to the importing module. A `{ package }` tracks only that package's installed version, so declare transitive packages separately when their versions matter. Bump `version` for changes the dependencies do not capture, such as a linked workspace package changed without a new version.
|
|
50
41
|
|
|
51
|
-
|
|
42
|
+
When inputs are only discovered while computing, such as the textures a model references, pass them to `save()` as its `dependencies`, like a compiler's depfile. They are checked on every load without being part of the key.
|
|
43
|
+
|
|
44
|
+
## Using loaders
|
|
45
|
+
|
|
46
|
+
Add the plugin:
|
|
52
47
|
|
|
53
48
|
```ts
|
|
54
49
|
import { defineConfig } from "vite"
|
|
@@ -57,20 +52,20 @@ import { vitecache } from "vitecache/vite"
|
|
|
57
52
|
export default defineConfig({ plugins: [vitecache()] })
|
|
58
53
|
```
|
|
59
54
|
|
|
60
|
-
|
|
55
|
+
A load is a `loadAsync` call with literal JSON arguments on `new CachedMeshLoader()`, or on a variable initialized with it, as in `new CachedMeshLoader().loadAsync({ ratio: 0.5 })` or `const meshes = new CachedMeshLoader()` and later `meshes.loadAsync({ ratio: 0.5 })`. Any other use fails the build, and in development is logged: passing a loader or such a variable around, exporting or reassigning it, calling another of its methods such as `setPath`, subclassing the loader, or loading with a variable. To serve several fixed variants, write one literal load per variant.
|
|
61
56
|
|
|
62
|
-
|
|
57
|
+
The plugin finds loads by scanning the project's JavaScript and TypeScript files, as Tailwind scans for class names, and matches the loader's imported name through aliases and namespace imports. It skips `node_modules`, the build output and `publicDir`, so a library's loaders are warmed only where the app uses them. It does not read `.vue` or `.svelte` files, and it warms a load in a file the app never imports, such as a test. Since variables are matched by name in the module, give a loader variable a name no other variable in that module uses.
|
|
63
58
|
|
|
64
|
-
The
|
|
59
|
+
The plugin repeats each load in Node by importing the loader's module through Vite's SSR loading. That module and its top-level imports must load without browser globals, and whatever the loader does after loading, such as decoding, runs in Node too. A build loads the Vite config file again to run the loaders, so configure plugins they rely on there rather than inline in a programmatic build.
|
|
65
60
|
|
|
66
|
-
|
|
67
|
-
import { prune } from "vitecache/prune"
|
|
61
|
+
In development the plugin warms every load at startup and again after every file change, and the page's cache requests wait for it, so the browser normally hits. It deletes entries no load uses: at startup, those left from earlier sessions, and later, an entry replaced by an edit once it has gone unused for `pruneAfter`, a minute by default, so that undoing the edit within that time reuses it. A build warms every load, then repeats them all and fails if that second pass saves anything, which means a key that changes between runs, such as one built from a timestamp. It then deletes every entry the build did not use, so the deployment holds exactly what the app requests.
|
|
68
62
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
63
|
+
Development and builds log each load that computes, by loader and source location, and announce a load still running after half a second; loads that hit are silent.
|
|
64
|
+
|
|
65
|
+
## Browsers and deployment
|
|
66
|
+
|
|
67
|
+
Browsers only read entries. A browser that misses, for a load the plugin could not see, computes the result itself each time, and `save()` warns once per version instead of writing. A deployed app serves entries without checking their inputs, so build again after changing them.
|
|
73
68
|
|
|
74
|
-
Entries
|
|
69
|
+
Entries keep their file names when their contents change, so browsers revalidate them before each use. Serve the cache directory with `ETag` or `Last-Modified`, as most static hosts do, and do not mark it immutable. When deploying below a URL prefix, pass that prefix with the cache path as `basePath`, such as `/app/_vitecache/`; the entry does not infer Vite's base.
|
|
75
70
|
|
|
76
|
-
|
|
71
|
+
The plugin adds the cache directory, `public/_vitecache/`, to `.gitignore`. A Node script outside Vite can use the same loaders. Its entries use `public/_vitecache/` below the working directory, or below `root` when passed.
|
package/dist/dependencies.d.ts
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import type { Checked, Dependencies, Dependency, Hashable, Hashes, JsonValue, Paths, Resolved, SavedDependencies } from './types.js';
|
|
2
|
-
/** A file or package dependency has no backing file or installation, so its state cannot be checked. */
|
|
3
|
-
export declare class MissingDependencyError extends Error {
|
|
4
|
-
name: string;
|
|
5
|
-
}
|
|
6
|
-
/** A file or package dependency changed between load() and save(), so the result may be built from stale inputs. */
|
|
7
|
-
export declare class ChangedDependenciesError extends Error {
|
|
8
|
-
name: string;
|
|
9
|
-
constructor();
|
|
10
|
-
}
|
|
11
|
-
export declare function paths(value: unknown): Paths;
|
|
12
|
-
/** Validate and snapshot JSON before asynchronous dependency resolution starts. */
|
|
13
|
-
export declare function json(value: unknown, ancestors?: Set<object>): JsonValue;
|
|
14
|
-
export declare function dependencies(value: unknown): Dependencies;
|
|
15
|
-
export declare function isChecked<D extends Dependency>(dependency: D): dependency is Checked<D>;
|
|
16
|
-
/** Dependencies after resolution, which turns URLs into paths. */
|
|
17
|
-
export declare function resolvedDependencies(value: unknown): Dependencies<Resolved>;
|
|
18
|
-
/** Saved dependencies are rechecked later, so they cannot be values: values select the result and belong to the key. */
|
|
19
|
-
export declare function savedDependencies(value: unknown): SavedDependencies;
|
|
20
|
-
/** Resolved file and package dependencies: the ones whose current state can be hashed. */
|
|
21
|
-
export declare function hashableDependencies(value: unknown): Dependencies<Hashable>;
|
|
22
|
-
export declare function hashes(value: unknown): Hashes;
|
|
23
|
-
export declare function equalHashes(first: Hashes, second: Hashes): boolean;
|