diff --git a/lib/design-lint/css.ts b/lib/design-lint/css.ts index a7daa4207..6b4aecf34 100644 --- a/lib/design-lint/css.ts +++ b/lib/design-lint/css.ts @@ -90,6 +90,10 @@ function nestSelector(sel: string, parent: string): string { /** Faces that read as monospace, whether they arrive as a token, a utility or a declaration; one list so every check classifies a face the same way. */ export const MONO_FACE = /mono|code|courier|silkscreen|jetbrains|plex mono|geist mono|commit|fira code|space mono|dm mono|roboto mono|source code|ibm plex mono|press start|vt323|share tech/i; +/** The tracked-caps label: tracking past this (em) on text at or under LABEL_PX reads as the reflex, in CSS and in Tailwind alike. */ +export const TRACKED_EM = 0.03; +export const LABEL_PX = 15; + /** Real stylesheets nest a handful of levels; past this the rest reads as the parent's body instead of recursing. */ const MAX_NESTING = 64; @@ -125,8 +129,10 @@ export function parseCss(source: string, file: string, lineOffset = 0): CssRule[ const blockStart = j + 1; let depth = 1, k = blockStart; while (k < end && depth > 0) { - if (src[k] === '{') depth++; - else if (src[k] === '}') depth--; + const ch = src[k]; + if (ch === '"' || ch === "'") { const q = src.indexOf(ch, k + 1); k = q < 0 || q >= end ? end : q + 1; continue; } + if (ch === '{') depth++; + else if (ch === '}') depth--; k++; } const blockEnd = Math.max(blockStart, k - 1); diff --git a/lib/design-lint/rules/component.ts b/lib/design-lint/rules/component.ts index 79c64e34c..71d3e5ca5 100644 --- a/lib/design-lint/rules/component.ts +++ b/lib/design-lint/rules/component.ts @@ -47,7 +47,7 @@ function reportItem(ctx: LintContext, r: Reporter, it: TwItem, monoOk: boolean): } /** The tells a file carries as a whole: a default palette, hover scale everywhere, a vh hero, three icon tiles. */ -function reportFile(ctx: LintContext, r: Reporter, list: TwItem[]): void { +function reportFile(r: Reporter, list: TwItem[]): void { const doc = list[0].doc; const pal = new Map(); let palAt: TwItem | null = null; @@ -78,7 +78,7 @@ function checkTailwind(ctx: LintContext, r: Reporter): void { byDoc.get(it.file)!.push(it); reportItem(ctx, r, it, monoOk); } - for (const list of byDoc.values()) reportFile(ctx, r, list); + for (const list of byDoc.values()) reportFile(r, list); } // sloplint checkComponentText (gates 63 30a) diff --git a/lib/design-lint/rules/type.ts b/lib/design-lint/rules/type.ts index 75034f815..499166c1d 100644 --- a/lib/design-lint/rules/type.ts +++ b/lib/design-lint/rules/type.ts @@ -7,7 +7,7 @@ import { OVERUSED_FONTS_DISPLAY } from '../../design-catalog'; import type { LintContext } from '../context'; import { fontPx, mergedRules, styleFor, trackingEm, valueOf } from '../context'; -import { MONO_FACE, famKey, firstFamily, GENERIC_FAMILIES, HEADING_SEL, isTokenRule, pxOf, resolveVars, type CssDecl, type CssRule, type MergedRule } from '../css'; +import { GENERIC_FAMILIES, HEADING_SEL, LABEL_PX, MONO_FACE, TRACKED_EM, famKey, firstFamily, isTokenRule, pxOf, resolveVars, type CssDecl, type CssRule, type MergedRule } from '../css'; import { ancestorMap, getAttr, innerOf, plainText } from '../html'; import type { Reporter } from '../report'; import { trunc } from '../report'; @@ -272,7 +272,7 @@ function caps67(ctx: LintContext, bySel: MergedRule[], rule: MergedRule): Caps67 const ownSmallCaps = rule.decls.some((d) => isSmallCaps67(d.prop, resolveVars(d.value, ctx.tokens))); const px = fontSizePxOf(rule, ctx); const ownEm = trackingEm(ctx, rule.decls, px || 16); - if (!(ownUpper === true || ownSmallCaps || (ownEm != null && ownEm > 0.03))) return null; + if (!(ownUpper === true || ownSmallCaps || (ownEm != null && ownEm > TRACKED_EM))) return null; const upper = ownUpper != null ? ownUpper : isUpper(inherited67(bySel, rule.selector, 'text-transform')); const smallCaps = ownSmallCaps || ['font-variant', 'font-variant-caps', 'font-feature-settings'].some((prop) => { const v = inherited67(bySel, rule.selector, prop); @@ -298,7 +298,7 @@ function check67(ctx: LintContext, r: Reporter): void { const caps = caps67(ctx, bySel, rule); if (!caps) continue; const { upper, smallCaps, em, px } = caps; - const tracked = em != null && em > 0.03; + const tracked = em != null && em > TRACKED_EM; const subject = rule.selector.split(',').map((p) => baseSelector(p).split(/[\s>+~]+/).filter(Boolean).pop() || ''); const bigToken = rule.decls.some((d) => d.prop === 'font-size' && /--text-(display|section|title)|clamp\(\s*(?:[2-9]|\d{2,})(?:\.\d+)?rem/.test(d.value)); const labelish = subject.some((s) => LABEL67.test(s)) ? !((px != null && px >= 28) || bigToken) @@ -307,7 +307,7 @@ function check67(ctx: LintContext, r: Reporter): void { if (HEADING_SEL.test(rule.selector) && !labelish && !control) continue; const key = baseSelector(rule.selector); if (seen.has(key)) continue; - const small = px != null && px <= 15; + const small = px != null && px <= LABEL_PX; const at = px != null ? ` at ${Math.round(px)}px` : ''; const tracking = tracked ? ` tracked ${em3(em)}em` : ''; if (control && !labelish) { @@ -352,8 +352,8 @@ function check67Html(ctx: LintContext, r: Reporter): void { const decls = styleFor(ctx, cls, t.name); const px = fontPx(ctx, decls); const em = trackingEm(ctx, decls, px || 16); - const tracked = em != null && em > 0.03; - if (!((px != null && px <= 15) || tracked)) continue; + const tracked = em != null && em > TRACKED_EM; + if (!((px != null && px <= LABEL_PX) || tracked)) continue; const key = cls || t.name; if (seen.has(key)) continue; seen.add(key); diff --git a/lib/design-lint/tailwind.ts b/lib/design-lint/tailwind.ts index 7af17954b..4c5c2b434 100644 --- a/lib/design-lint/tailwind.ts +++ b/lib/design-lint/tailwind.ts @@ -12,7 +12,7 @@ import type { LintContext } from './context'; import { memo } from './context'; import { COLOR_LITERAL, hueIn, oklchOf, parseColor, type Color } from './color'; -import { MONO_FACE, GENERIC_FAMILIES, firstFamily, pxOf, resolveVars, splitTop, type Tokens } from './css'; +import { GENERIC_FAMILIES, LABEL_PX, MONO_FACE, TRACKED_EM, firstFamily, pxOf, resolveVars, splitTop, type Tokens } from './css'; import { braceEnd, closeIndex, clsLits, directChildren, getAttr, innerOf, plainText, type Doc, type Tag } from './html'; import { trunc } from './report'; @@ -619,13 +619,13 @@ export interface TwTrackedCaps { what: string; size: number | null; em: number | /** The tracked-caps label: small text set uppercase, small caps or letter-spaced past 0.03em, outside logos, code and display heads. */ export function twTrackedCaps(it: TwItem, tw: TwGroup): TwTrackedCaps | null { const f = it.info, fr = it.frame; - if (!(f.upper || f.smallCaps || (f.trackingEm != null && f.trackingEm > 0.03))) return null; + if (!(f.upper || f.smallCaps || (f.trackingEm != null && f.trackingEm > TRACKED_EM))) return null; if (fr.logo || fr.logoSelf || fr.noLabel) return null; const size = fr.size, em = fr.em; if ((size != null && size >= 24) || (size == null && /^h[1-6]$/.test(it.tag))) return null; const caps = fr.upper || fr.caps; - const small = size != null && size <= 15; - const tracked = em != null && em > 0.03; + const small = size != null && size <= LABEL_PX; + const tracked = em != null && em > TRACKED_EM; const control = /^(?:button|input)$/.test(it.tag) || /(?:button|btn|cta)$/i.test(it.tag) || (it.tag === 'a' && f.fill && f.padded) || /role\s*=\s*["']?button/i.test(it.attrs); if (tw.upperDisplay && control && (em == null || em <= 0.04)) return null; if (!((caps && (small || tracked)) || (tracked && small))) return null; @@ -638,7 +638,7 @@ export interface TwEyebrow { text: string; next: string; signal: string } export function twEyebrow(it: TwItem): TwEyebrow | null { const f = it.info, fr = it.frame, doc = it.doc; if (it.idx < 0 || !/^(?:p|span|div|small)$/.test(it.tag) || fr.eyeSkip) return null; - if (!(f.upper || (f.trackingEm != null && f.trackingEm > 0.03) || f.accent)) return null; + if (!(f.upper || (f.trackingEm != null && f.trackingEm > TRACKED_EM) || f.accent)) return null; if (fr.size == null || fr.size > 14) return null; const t = doc.tags[it.idx]; if (/role\s*=\s*["']?status|aria-live|aria-current|aria-hidden\s*=\s*["']?true/i.test(t.attrs)) return null; @@ -650,7 +650,7 @@ export function twEyebrow(it: TwItem): TwEyebrow | null { const href = /]*\shref\s*=\s*["']([^"']*)["']/i.exec(inner); if (href && !/^#?$/.test(href[1].trim())) return null; const text = twPlain(doc, inner); - const signal = [f.upper ? 'uppercase' : null, f.trackingEm != null && f.trackingEm > 0.03 ? `tracked ${Math.round(f.trackingEm * 1000) / 1000}em` : null, f.accent].filter(Boolean).join(', '); + const signal = [f.upper ? 'uppercase' : null, f.trackingEm != null && f.trackingEm > TRACKED_EM ? `tracked ${Math.round(f.trackingEm * 1000) / 1000}em` : null, f.accent].filter(Boolean).join(', '); if (!text) return doc.kind !== 'html' && c > it.idx && doc.raw.slice(t.end, doc.tags[c].pos).includes('{') ? { text: '', next: nx.name, signal } : null; const words = text.split(/[\s·/|\u2014\u2013]+/).filter(Boolean); if (words.length > 7 || text.length > 56 || /[\w/-]+\.[a-z]{1,4}$/i.test(text)) return null; diff --git a/test/design-lint-assets.test.ts b/test/design-lint-assets.test.ts index 73a364eaa..c9dc50cd6 100644 --- a/test/design-lint-assets.test.ts +++ b/test/design-lint-assets.test.ts @@ -3,7 +3,7 @@ * for every rule id, one page that is clean for it and one with the tell * planted, checked by id, grade and line. */ -import { describe, expect, test } from 'bun:test'; +import { afterAll, describe, expect, test } from 'bun:test'; import * as fs from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; @@ -11,6 +11,9 @@ import { lint } from '../lib/design-lint/index'; import { formatFinding, type Finding } from '../lib/design-lint/report'; import { lintStatic } from '../lib/design-lint/static'; +const tmpDirs: string[] = []; +afterAll(() => { for (const d of tmpDirs) fs.rmSync(d, { recursive: true, force: true }); }); + const ROOT = path.resolve(import.meta.dir, '..'); const FIXTURES = path.join(ROOT, 'test', 'fixtures', 'design-lint'); @@ -28,6 +31,7 @@ const lineOf = (html: string, needle: string): number => { function run(html: string, files: Record = {}): Finding[] { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'design-lint-assets-')); + tmpDirs.push(dir); fs.writeFileSync(path.join(dir, 'index.html'), html); for (const [name, text] of Object.entries(files)) fs.writeFileSync(path.join(dir, name), text); return lintStatic([dir], { cwd: dir, warn: (l) => { throw new Error(l); } }).findings; diff --git a/test/design-lint-chrome.test.ts b/test/design-lint-chrome.test.ts index 0cbd0ba88..42bbbeaa5 100644 --- a/test/design-lint-chrome.test.ts +++ b/test/design-lint-chrome.test.ts @@ -3,7 +3,7 @@ * chrome-copy.ts, chrome-eyebrow.ts): for every rule id, one page that is * clean for it and one with the tell planted, checked by id, grade and line. */ -import { describe, expect, test } from 'bun:test'; +import { afterAll, describe, expect, test } from 'bun:test'; import * as fs from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; @@ -11,6 +11,9 @@ import { lint } from '../lib/design-lint/index'; import { formatFinding, type Finding } from '../lib/design-lint/report'; import { lintStatic } from '../lib/design-lint/static'; +const tmpDirs: string[] = []; +afterAll(() => { for (const d of tmpDirs) fs.rmSync(d, { recursive: true, force: true }); }); + const ROOT = path.resolve(import.meta.dir, '..'); const FIXTURES = path.join(ROOT, 'test', 'fixtures', 'design-lint'); @@ -29,6 +32,7 @@ const lineOf = (html: string, needle: string): number => { function run(html: string, opts: { job?: string | null; scope?: 'page' | 'component'; files?: Record } = {}): Finding[] { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'design-lint-chrome-')); + tmpDirs.push(dir); const { files = {}, ...rest } = opts; fs.writeFileSync(path.join(dir, 'index.html'), html); for (const [name, text] of Object.entries(files)) fs.writeFileSync(path.join(dir, name), text); diff --git a/test/design-lint-color.test.ts b/test/design-lint-color.test.ts index 88a0988e1..836a8f8fe 100644 --- a/test/design-lint-color.test.ts +++ b/test/design-lint-color.test.ts @@ -1,10 +1,13 @@ -import { describe, expect, test } from 'bun:test'; +import { afterAll, describe, expect, test } from 'bun:test'; import * as fs from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; import { formatFinding, type Finding } from '../lib/design-lint/report'; import { lintStatic } from '../lib/design-lint/static'; +const tmpDirs: string[] = []; +afterAll(() => { for (const d of tmpDirs) fs.rmSync(d, { recursive: true, force: true }); }); + const ROOT = path.resolve(import.meta.dir, '..'); const FIXTURES = path.join(ROOT, 'test', 'fixtures', 'design-lint'); @@ -31,6 +34,7 @@ const page = (css: string, body = '

Ledger

The books close on the firs function lintPage(html: string, name = 'index.html'): { html: string; findings: Finding[]; dir: string } { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'design-lint-color-')); + tmpDirs.push(dir); const file = path.join(dir, name); fs.writeFileSync(file, html); return { html, dir, findings: lintStatic([file], { cwd: dir, warn: (l) => { throw new Error(l); } }).findings }; diff --git a/test/design-lint-finish.test.ts b/test/design-lint-finish.test.ts index fff774011..63f8ad4fd 100644 --- a/test/design-lint-finish.test.ts +++ b/test/design-lint-finish.test.ts @@ -4,7 +4,7 @@ * planted, checked by id, grade and line. Everything here prints at polish * except fake-avatar, which blocks. */ -import { describe, expect, test } from 'bun:test'; +import { afterAll, describe, expect, test } from 'bun:test'; import * as fs from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; @@ -12,6 +12,9 @@ import { lint } from '../lib/design-lint/index'; import { formatFinding, type Finding } from '../lib/design-lint/report'; import { lintStatic } from '../lib/design-lint/static'; +const tmpDirs: string[] = []; +afterAll(() => { for (const d of tmpDirs) fs.rmSync(d, { recursive: true, force: true }); }); + const ROOT = path.resolve(import.meta.dir, '..'); const FIXTURES = path.join(ROOT, 'test', 'fixtures', 'design-lint'); @@ -29,6 +32,7 @@ const lineOf = (html: string, needle: string): number => { function run(html: string, files: Record = {}): Finding[] { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'design-lint-finish-')); + tmpDirs.push(dir); fs.writeFileSync(path.join(dir, 'index.html'), html); for (const [name, text] of Object.entries(files)) fs.writeFileSync(path.join(dir, name), text); return lintStatic([dir], { cwd: dir, warn: (l) => { throw new Error(l); } }).findings; diff --git a/test/design-lint-motion.test.ts b/test/design-lint-motion.test.ts index fb33bf31c..be5e1fc04 100644 --- a/test/design-lint-motion.test.ts +++ b/test/design-lint-motion.test.ts @@ -3,12 +3,15 @@ * planted positive with its line and grade, and a negative control at the edge of the threshold. The shared fixtures * (slopped.html, clean/) are checked for this family's rules too. */ -import { describe, expect, test } from 'bun:test'; +import { afterAll, describe, expect, test } from 'bun:test'; import * as fs from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; import { formatFinding, lint, type Finding } from '../lib/design-lint/index'; +const tmpDirs: string[] = []; +afterAll(() => { for (const d of tmpDirs) fs.rmSync(d, { recursive: true, force: true }); }); + const ROOT = path.resolve(import.meta.dir, '..'); const FIXTURES = path.join(ROOT, 'test', 'fixtures', 'design-lint'); const FAMILY = ['layout-transition', 'spacing-transition', 'hover-tells', 'bounce-easing', 'focus-ring-fade', 'tooltip-focus-delay', 'autorotate-no-pause', 'missing-focus-visible', 'missing-states', 'missing-reduced-motion', 'autoplay-unmuted', 'lazy-lcp', 'video-without-poster', 'input-state-shift', 'control-height', 'content-hidden-at-rest']; @@ -24,6 +27,7 @@ function page(css: string, body: string, opts: { states?: boolean; reduce?: bool async function lintPage(html: string, opts: { scope?: 'page' | 'component' } = {}): Promise { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'design-lint-motion-')); + tmpDirs.push(dir); const file = path.join(dir, 'index.html'); fs.writeFileSync(file, html); return (await lint([file], { cwd: dir, scope: opts.scope, warn: () => {} })).findings; diff --git a/test/design-lint-structure.test.ts b/test/design-lint-structure.test.ts index 900a37d1e..aae292bbd 100644 --- a/test/design-lint-structure.test.ts +++ b/test/design-lint-structure.test.ts @@ -3,12 +3,15 @@ * positive with its line and grade, and a negative control at the edge of the threshold. The shared fixtures * (slopped.html, clean/) are checked for this family's rules too. */ -import { describe, expect, test } from 'bun:test'; +import { afterAll, describe, expect, test } from 'bun:test'; import * as fs from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; import { formatFinding, lint, type Finding } from '../lib/design-lint/index'; +const tmpDirs: string[] = []; +afterAll(() => { for (const d of tmpDirs) fs.rmSync(d, { recursive: true, force: true }); }); + const ROOT = path.resolve(import.meta.dir, '..'); const FIXTURES = path.join(ROOT, 'test', 'fixtures', 'design-lint'); const FAMILY = ['template-cards', 'nested-cards', 'vh-height', 'raw-spacing', 'off-scale-spacing', 'overflow-x-unclipped', 'wide-container', 'bare-fr-track', 'display-head-unwrapped', 'radio-tab-jump', 'double-sticky', 'screen-word-budget']; @@ -24,6 +27,7 @@ function page(css: string, body: string, opts: { clip?: boolean } = {}): string async function lintPage(html: string, opts: { scope?: 'page' | 'component'; name?: string } = {}): Promise { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'design-lint-structure-')); + tmpDirs.push(dir); const file = path.join(dir, opts.name ?? 'index.html'); fs.writeFileSync(file, html); return (await lint([file], { cwd: dir, scope: opts.scope, warn: () => {} })).findings; diff --git a/test/design-lint-tailwind.test.ts b/test/design-lint-tailwind.test.ts index 2ca79e58d..96b32f357 100644 --- a/test/design-lint-tailwind.test.ts +++ b/test/design-lint-tailwind.test.ts @@ -3,7 +3,7 @@ * strings in HTML, JSX, Vue, Svelte and Astro, helper calls, shared tokens) * and the rules it feeds, each with a negative control and a planted tell. */ -import { describe, expect, test } from 'bun:test'; +import { afterAll, describe, expect, test } from 'bun:test'; import * as fs from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; @@ -11,6 +11,9 @@ import { lint, formatFinding, type Finding } from '../lib/design-lint/index'; import { lintStatic } from '../lib/design-lint/static'; import { twGroup, twParse, twVariants } from '../lib/design-lint/tailwind'; +const tmpDirs: string[] = []; +afterAll(() => { for (const d of tmpDirs) fs.rmSync(d, { recursive: true, force: true }); }); + const ROOT = path.resolve(import.meta.dir, '..'); const FIXTURES = path.join(ROOT, 'test', 'fixtures', 'design-lint'); @@ -18,6 +21,7 @@ const MY_RULES = ['layout-transition', 'bounce-easing', 'purple-blue-gradient', function tmpDir(files: Record): string { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'design-lint-tw-')); + tmpDirs.push(dir); for (const [name, text] of Object.entries(files)) { fs.mkdirSync(path.dirname(path.join(dir, name)), { recursive: true }); fs.writeFileSync(path.join(dir, name), text); diff --git a/test/design-lint-type.test.ts b/test/design-lint-type.test.ts index 2e18dc281..f0424e7ce 100644 --- a/test/design-lint-type.test.ts +++ b/test/design-lint-type.test.ts @@ -1,10 +1,13 @@ -import { describe, expect, test } from 'bun:test'; +import { afterAll, describe, expect, test } from 'bun:test'; import * as fs from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; import { formatFinding, type Finding } from '../lib/design-lint/report'; import { lintStatic } from '../lib/design-lint/static'; +const tmpDirs: string[] = []; +afterAll(() => { for (const d of tmpDirs) fs.rmSync(d, { recursive: true, force: true }); }); + const ROOT = path.resolve(import.meta.dir, '..'); const FIXTURES = path.join(ROOT, 'test', 'fixtures', 'design-lint'); @@ -34,6 +37,7 @@ const page = (css: string, body = '

Ledger

The books close on the firs function lintPage(html: string, name = 'index.html'): { html: string; findings: Finding[]; dir: string } { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'design-lint-type-')); + tmpDirs.push(dir); const file = path.join(dir, name); fs.writeFileSync(file, html); return { html, dir, findings: lintStatic([file], { cwd: dir, warn: (l) => { throw new Error(l); } }).findings }; diff --git a/test/design-lint.test.ts b/test/design-lint.test.ts index 35ba54b62..14a714528 100644 --- a/test/design-lint.test.ts +++ b/test/design-lint.test.ts @@ -4,7 +4,7 @@ * (test/design-lint-.test.ts); the render tier is * test/design-lint-render.test.ts. */ -import { describe, expect, test } from 'bun:test'; +import { afterAll, describe, expect, test } from 'bun:test'; import * as fs from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; @@ -19,6 +19,9 @@ import { RULES, RULE_IDS } from '../lib/design-lint/rules/ids'; import { FAMILIES } from '../lib/design-lint/rules'; import { findWaivers, reasonIsBoilerplate, resolveWaivers, applyWaivers, suspectedRules } from '../lib/design-lint/waivers'; +const tmpDirs: string[] = []; +afterAll(() => { for (const d of tmpDirs) fs.rmSync(d, { recursive: true, force: true }); }); + const ROOT = path.resolve(import.meta.dir, '..'); const BIN = path.join(ROOT, 'bin', 'gstack-design-lint'); const FIXTURES = path.join(ROOT, 'test', 'fixtures', 'design-lint'); @@ -30,6 +33,7 @@ function runBin(args: string[], cwd = ROOT) { function tmpPage(html: string, name = 'index.html'): string { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'design-lint-')); + tmpDirs.push(dir); const file = path.join(dir, name); fs.writeFileSync(file, html); return file; @@ -38,6 +42,7 @@ function tmpPage(html: string, name = 'index.html'): string { /** A temporary tree: each key is a relative path (parent directories are created), each value the file's text. */ function tmpTree(files: Record): string { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'design-lint-tree-')); + tmpDirs.push(dir); for (const [rel, text] of Object.entries(files)) { fs.mkdirSync(path.dirname(path.join(dir, rel)), { recursive: true }); fs.writeFileSync(path.join(dir, rel), text);