mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
Since the UI moved to single-file components, every unit-test process compiled the views it imports from scratch, and the loader ran off-thread (module.register), adding a round trip to every module a test imports, including the ones that are not components. Core's unit tests took about twice as long, and a host running Core's suite on smaller runners passed its CI time limit. register-vue now installs synchronous hooks on the loading thread (module.registerHooks). Compiled components are kept in the system temp directory, keyed by the file's path and text, the Vue version and the loader's own code, so each one compiles once across test processes; the compiler itself loads only on a cache miss. Cached modules run as code, so the directory is used only when it is a real directory owned by the current user and writable by no one else; on Windows it lives in the user's local app data instead of a temp directory that may be shared. The cache never sits inside the package tree, and GEV_VUE_CACHE=0 compiles every time. On four cores the full suite went from 441 s to 240 s from a cold cache.
171 lines
5.7 KiB
JavaScript
171 lines
5.7 KiB
JavaScript
import { createHash, randomBytes } from 'node:crypto';
|
|
import {
|
|
lstatSync,
|
|
mkdirSync,
|
|
readFileSync,
|
|
renameSync,
|
|
rmSync,
|
|
writeFileSync,
|
|
} from 'node:fs';
|
|
import { createRequire } from 'node:module';
|
|
import { tmpdir } from 'node:os';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { VUE_TEMPLATE_OPTIONS } from './vueOptions.js';
|
|
|
|
const require = createRequire(import.meta.url);
|
|
|
|
// The compiler loads on the first component that is not cached, so a test
|
|
// process that imports no views, or only cached ones, never pays for it.
|
|
let compiler = null;
|
|
const sfcCompiler = () => (compiler ??= require('vue/compiler-sfc'));
|
|
|
|
/**
|
|
* Compile a single-file component to an ES module for Node, so unit tests (the
|
|
* application's and a host's) can import components without a bundler.
|
|
* Styles are dropped: tests read the global stylesheet separately.
|
|
*/
|
|
export function compileVueModule(source, filename) {
|
|
const { compileScript, compileTemplate, parse } = sfcCompiler();
|
|
// The template is parsed with the SFC; parse it as the browser build does.
|
|
const { descriptor, errors } = parse(source, {
|
|
filename,
|
|
templateParseOptions: VUE_TEMPLATE_OPTIONS.compilerOptions,
|
|
});
|
|
if (errors.length) throw errors[0];
|
|
const id = filename;
|
|
if (descriptor.scriptSetup) {
|
|
return compileScript(descriptor, {
|
|
id,
|
|
inlineTemplate: true,
|
|
templateOptions: VUE_TEMPLATE_OPTIONS,
|
|
}).content;
|
|
}
|
|
const script = descriptor.script
|
|
? compileScript(descriptor, { id, genDefaultAs: '__sfc__' }).content
|
|
: 'const __sfc__ = {};';
|
|
let template = '';
|
|
if (descriptor.template) {
|
|
const compiled = compileTemplate({
|
|
source: descriptor.template.content,
|
|
filename,
|
|
id,
|
|
...VUE_TEMPLATE_OPTIONS,
|
|
});
|
|
if (compiled.errors.length) throw compiled.errors[0];
|
|
template = compiled.code;
|
|
}
|
|
return `${script}\n${template}\nif (typeof render === 'function') __sfc__.render = render;\nexport default __sfc__;\n`;
|
|
}
|
|
|
|
// The loader's own code is part of every key, so a change to how components
|
|
// compile never reuses output from before it.
|
|
let loaderFingerprint = null;
|
|
const fingerprint = () =>
|
|
(loaderFingerprint ??= createHash('sha256')
|
|
.update(readFileSync(fileURLToPath(import.meta.url)))
|
|
.update(readFileSync(new URL('./vueOptions.js', import.meta.url)))
|
|
.update(require('vue/package.json').version)
|
|
.digest('hex'));
|
|
|
|
/**
|
|
* Where compiled components are kept between test processes, never a
|
|
* package's own tree (a host's checks refuse files inside Core): the system
|
|
* temp directory, or on Windows the per-user local app data directory, since
|
|
* a temp directory there may be shared and its ownership is not checked.
|
|
* GEV_VUE_CACHE=0 compiles every time.
|
|
*/
|
|
export function vueCacheDirectory(
|
|
env = process.env,
|
|
platform = process.platform,
|
|
) {
|
|
if (env.GEV_VUE_CACHE === '0') return null;
|
|
if (platform === 'win32') {
|
|
return env.LOCALAPPDATA ? path.join(env.LOCALAPPDATA, 'gev-vue-sfc') : null;
|
|
}
|
|
return path.join(tmpdir(), `gev-vue-sfc-${process.getuid()}`);
|
|
}
|
|
|
|
/** The cache entry for one component: its path, its text and the loader. */
|
|
export function vueCacheKey(source, filename) {
|
|
return createHash('sha256')
|
|
.update(JSON.stringify([fingerprint(), filename]))
|
|
.update('\0')
|
|
.update(source)
|
|
.digest('hex');
|
|
}
|
|
|
|
/**
|
|
* Compile a component, reusing a previous process's result for the same text.
|
|
* A cache that cannot be read or written only costs the compile.
|
|
*/
|
|
export function cachedVueModule(
|
|
source,
|
|
filename,
|
|
directory = vueCacheDirectory(),
|
|
) {
|
|
if (!directory || !ownCacheDirectory(directory)) {
|
|
return compileVueModule(source, filename);
|
|
}
|
|
const entry = path.join(directory, `${vueCacheKey(source, filename)}.mjs`);
|
|
try {
|
|
return readFileSync(entry, 'utf8');
|
|
} catch {
|
|
// Not compiled yet.
|
|
}
|
|
const compiled = compileVueModule(source, filename);
|
|
// Concurrent test processes and threads may compile the same component;
|
|
// each writes its own new file and the rename makes the entry appear whole.
|
|
const partial = `${entry}.${randomBytes(6).toString('hex')}.tmp`;
|
|
try {
|
|
writeFileSync(partial, compiled, { flag: 'wx', mode: 0o600 });
|
|
renameSync(partial, entry);
|
|
} catch {
|
|
// An unwritable directory, or a rename another process holds open,
|
|
// leaves the cache without this entry.
|
|
try {
|
|
rmSync(partial, { force: true });
|
|
} catch {
|
|
// A partial file that cannot be removed is only clutter.
|
|
}
|
|
}
|
|
return compiled;
|
|
}
|
|
|
|
/**
|
|
* Cached modules run as code, so the directory must be this user's own: a
|
|
* real directory (not a link), owned by the current user and writable by no
|
|
* one else. In a shared temp directory another user could otherwise create it
|
|
* first and plant modules. On Windows it sits in the user's own profile.
|
|
*/
|
|
const checkedDirectories = new Map();
|
|
export function ownCacheDirectory(directory) {
|
|
if (!checkedDirectories.has(directory)) {
|
|
checkedDirectories.set(directory, isOwnDirectory(directory));
|
|
}
|
|
return checkedDirectories.get(directory);
|
|
}
|
|
|
|
function isOwnDirectory(directory) {
|
|
try {
|
|
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
const stats = lstatSync(directory);
|
|
if (!stats.isDirectory()) return false;
|
|
if (process.platform === 'win32') return true;
|
|
return stats.uid === process.getuid() && (stats.mode & 0o022) === 0;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/** Synchronous module hook (module.registerHooks): `.vue` loads compiled. */
|
|
export function load(url, context, nextLoad) {
|
|
if (!url.endsWith('.vue')) return nextLoad(url, context);
|
|
const filename = fileURLToPath(url);
|
|
return {
|
|
format: 'module',
|
|
source: cachedVueModule(readFileSync(filename, 'utf8'), filename),
|
|
shortCircuit: true,
|
|
};
|
|
}
|