Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ Glint adds per-file overhead (TS program build, module rewrite, TypeChecker call

### Caching

Glint results are content-addressed and cached on disk under `node_modules/.cache/html-validate-ember/glint/`. Set `HVE_NO_CACHE=1` to bypass the cache.
Glint results, the transform output and (for `validate-gts`) the report of each file are cached on disk under `node_modules/.cache/html-validate-ember/`. An entry is keyed on the file's content plus the content of every project file it imports, transitively (through relative paths and tsconfig `paths`), the project's ambient `.d.ts` files, the lockfile, the tsconfig and the plugin — so editing an imported component invalidates its consumers. Set `HVE_NO_CACHE=1` to bypass the caches.

## Silencing rules

Expand Down
18 changes: 11 additions & 7 deletions lib/backend/ts6.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { createRequire } from 'node:module';
import type * as TS from 'typescript';

import { isComponentTag } from '../../blank.js';
import { readCache, writeCache } from '../cache.js';
import { cacheDependencies, readCache, writeCache } from '../cache.js';
import type {
OpenedFile,
PreloadProgress,
Expand Down Expand Up @@ -479,7 +479,8 @@ export function createTs6Backend(deps: Ts6Deps, tsconfigPath: string): TypeBacke
}
// If a cached extraction exists for this file, skip the rewrite —
// we'll never need its rewritten contents in the program.
if (readCache(filename, contents, tsconfigPath, 'ts6')) {
const dependencies = cacheDependencies(filename, contents, tsconfigPath);
if (readCache(filename, contents, tsconfigPath, 'ts6', dependencies)) {
cached++;
onProgress?.({ done, total: filenames.length, phase: 'rewrite' });
continue;
Expand All @@ -503,11 +504,14 @@ export function createTs6Backend(deps: Ts6Deps, tsconfigPath: string): TypeBacke
// `<template>` block. Without this, no-template files
// perpetually show up under "analyzed" in the summary, which
// reads as "why aren't these cached?" noise.
writeCache(filename, contents, tsconfigPath, 'ts6', {
attrTypeMap: new Map(),
componentTagMap: new Map(),
componentAttrMap: new Map(),
});
writeCache(
filename,
contents,
tsconfigPath,
'ts6',
{ attrTypeMap: new Map(), componentTagMap: new Map(), componentAttrMap: new Map() },
dependencies,
);
skips.rewriteEmpty.push({ file: filename });
onProgress?.({ done, total: filenames.length, phase: 'rewrite' });
continue;
Expand Down
18 changes: 11 additions & 7 deletions lib/backend/tsgo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { Preprocessor } from 'content-tag';
import { preprocess as glimmerPreprocess, type AST } from '@glimmer/syntax';

import { isComponentTag } from '../../blank.js';
import { readCache, writeCache } from '../cache.js';
import { cacheDependencies, readCache, writeCache } from '../cache.js';
import { ts6Syntax } from './ts6.js';
import type {
CheckerLike,
Expand Down Expand Up @@ -509,16 +509,20 @@ export function createTsgoBackend(mods: TsgoModules, tsconfigPath: string): Type
});
continue;
}
if (readCache(filename, contents, tsconfigPath, 'tsgo')) {
const dependencies = cacheDependencies(filename, contents, tsconfigPath);
if (readCache(filename, contents, tsconfigPath, 'tsgo', dependencies)) {
cached++;
continue;
}
if (templateBlocks(contents, filename).length === 0) {
writeCache(filename, contents, tsconfigPath, 'tsgo', {
attrTypeMap: new Map(),
componentTagMap: new Map(),
componentAttrMap: new Map(),
});
writeCache(
filename,
contents,
tsconfigPath,
'tsgo',
{ attrTypeMap: new Map(), componentTagMap: new Map(), componentAttrMap: new Map() },
dependencies,
);
skips.rewriteEmpty.push({ file: filename });
continue;
}
Expand Down
46 changes: 35 additions & 11 deletions lib/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
// The Glint pipeline (`extractAttrTypeMap`) is the dominant cost when
// `--glint` is on — TS program rebuilds + per-file `rewriteModule` +
// `getTypeChecker().getTypeAtLocation` calls. The result is a pure
// function of (file content + tsconfig content + type backend + plugin
// version + plugin source content), so we can cache it on disk and skip the work
// function of (file content + the content of the files it depends on,
// see `lib/deps.ts` + tsconfig content + type backend + plugin version
// + plugin source content), so we can cache it on disk and skip the work
// on repeat runs (CI, pre-commit hooks, IDE re-validation).
//
// Why plugin source content (not just version)? On release the package
Expand All @@ -20,8 +21,9 @@
// One entry per file path, not per (content × tsconfig × version)
// combination — so editing a file overwrites its previous entry rather
// than leaving stale entries to accumulate. The stored entry carries
// the file SHA, tsconfig SHA, and plugin version inside the JSON; the
// reader compares stored vs. current and treats any mismatch as a miss.
// the file SHA, dependency SHA, tsconfig SHA, and plugin version inside
// the JSON; the reader compares stored vs. current and treats any
// mismatch as a miss.
//
// `node_modules/.cache/` is the conventional ignored cache location used
// by Babel, ESLint, etc. — your existing `.gitignore` already excludes
Expand All @@ -36,6 +38,7 @@ import crypto from 'node:crypto';
import { fileURLToPath } from 'node:url';

import type { ComponentAttrs } from './builtin-components.js';
import { dependencySha, sha256 } from './deps.js';

// Walk up from this module looking for the nearest `package.json` so the
// version is found regardless of whether we're running from source
Expand Down Expand Up @@ -130,6 +133,7 @@ interface CacheEntry {
pluginSourceSha: string;
tsconfigSha: string;
fileSha: string;
dependencySha: string;
attrTypeMap: Array<[string, AttrTypeInfo]>;
componentTagMap: Array<[string, string]>;
componentAttrMap: Array<[string, ComponentAttrs]>;
Expand All @@ -139,10 +143,6 @@ interface CacheEntry {
// process; tsconfigs rarely change mid-run).
const tsconfigShaCache = new Map<string, string>();

function sha256(input: string): string {
return crypto.createHash('sha256').update(input).digest('hex');
}

function getTsconfigSha(tsconfigPath: string): string {
const cached = tsconfigShaCache.get(tsconfigPath);
if (cached !== undefined) return cached;
Expand Down Expand Up @@ -187,6 +187,15 @@ function deserializeMap<K, V>(arr: Array<[K, V]> | undefined): Map<K, V> {
return new Map(arr ?? []);
}

/**
* The dependency sha a Glint result is read and written under. Computed
* once by the caller and passed to both `readCache` and `writeCache`; not
* computed at all when the cache is off.
*/
export function cacheDependencies(filename: string, contents: string, tsconfigPath: string): string {
return CACHE_DISABLED ? 'disabled' : dependencySha(filename, contents, tsconfigPath);
}

// Returns cached extraction results for `filename` when the stored
// entry's SHAs match current (file content + tsconfig + plugin version).
// Returns null on miss / stale / disabled / read or parse error.
Expand All @@ -195,6 +204,7 @@ export function readCache(
contents: string,
tsconfigPath: string,
backend: string,
dependencies = dependencySha(filename, contents, tsconfigPath),
): ExtractionResult | null {
if (CACHE_DISABLED) return null;
const cacheDir = findCacheDir(filename);
Expand All @@ -220,7 +230,8 @@ export function readCache(
parsed.backend !== backend ||
parsed.pluginSourceSha !== PLUGIN_SOURCE_SHA ||
parsed.tsconfigSha !== tsconfigSha ||
parsed.fileSha !== fileSha
parsed.fileSha !== fileSha ||
parsed.dependencySha !== dependencies
) {
return null;
}
Expand All @@ -234,13 +245,16 @@ export function readCache(
// Write extraction results to the cache. Best-effort: failure is swallowed
// (cache miss next time, but validation still works). Overwrites the
// file's existing entry — there's only ever one cache file per source
// file path.
// file path. `dependencies` is the sha the result was computed under
// (from the matching `readCache`), so a dependency edited during the
// analysis cannot be stored as if it were reflected in the result.
export function writeCache(
filename: string,
contents: string,
tsconfigPath: string,
backend: string,
result: ExtractionResult,
dependencies = dependencySha(filename, contents, tsconfigPath),
): void {
if (CACHE_DISABLED) return;
const cacheDir = findCacheDir(filename);
Expand All @@ -252,6 +266,7 @@ export function writeCache(
pluginSourceSha: PLUGIN_SOURCE_SHA,
tsconfigSha: getTsconfigSha(tsconfigPath),
fileSha: sha256(contents),
dependencySha: dependencies,
attrTypeMap: serializeMap(result.attrTypeMap),
componentTagMap: serializeMap(result.componentTagMap),
componentAttrMap: serializeMap(result.componentAttrMap),
Expand Down Expand Up @@ -291,10 +306,17 @@ interface TransformCacheEntry {
}

/** Everything the transform's output depends on besides the plugin itself. */
export function transformCacheKey(data: string, tsconfigPath: string | null, backendKind: string): string {
// Without Glint nothing crosses file boundaries, so the closure is not
// part of the key (and not computed).
function dependenciesForKey(filename: string, contents: string, tsconfigPath: string | null): string {
return process.env['HVE_GLINT'] === '0' ? 'no-glint' : dependencySha(filename, contents, tsconfigPath);
Comment on lines +311 to +312
}

export function transformCacheKey(filename: string, data: string, tsconfigPath: string | null, backendKind: string): string {
return sha256(
[
data,
dependenciesForKey(filename, data, tsconfigPath),
tsconfigPath ? getTsconfigSha(tsconfigPath) : 'no-tsconfig',
backendKind,
process.env['HVE_GLINT'] ?? '',
Expand Down Expand Up @@ -362,6 +384,7 @@ interface ReportCacheEntry<Result> extends CachedReport<Result> {
}

export function reportCacheKey(
filename: string,
contents: string,
config: unknown,
htmlValidateVersion: string,
Expand All @@ -371,6 +394,7 @@ export function reportCacheKey(
return sha256(
[
contents,
dependenciesForKey(filename, contents, tsconfigPath),
JSON.stringify(config ?? null),
htmlValidateVersion,
tsconfigPath ? getTsconfigSha(tsconfigPath) : 'no-tsconfig',
Expand Down
Loading
Loading