mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-11 21:57:14 +03:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e095c53819 | ||
|
|
3fcd656aaf | ||
|
|
6d973cdea6 |
@@ -9,3 +9,15 @@ The `skill/reference/ios.md` and `skill/reference/android.md` platform reference
|
||||
**Original work:** https://github.com/ehmo/platform-design-skills
|
||||
**Original license:** MIT
|
||||
**Author:** ehmo
|
||||
|
||||
## Static HTML parser bundle
|
||||
|
||||
`cli/engine/vendor/static-html-parsers.mjs` is a generated bundle of the parser packages the static-HTML detector needs at runtime. Skill and plugin installs copy that file with the detector; they do not install these packages from npm.
|
||||
|
||||
| Package | License |
|
||||
|---|---|
|
||||
| htmlparser2 | MIT |
|
||||
| css-select | BSD-2-Clause |
|
||||
| css-tree | MIT |
|
||||
| domutils | BSD-2-Clause |
|
||||
| source-map-js (via css-tree) | BSD-3-Clause |
|
||||
|
||||
@@ -423,10 +423,11 @@ async function detectCli() {
|
||||
}
|
||||
}
|
||||
else process.stderr.write(formatFindings(allFindings, false) + '\n');
|
||||
process.exit(primary.length > 0 ? 2 : 0);
|
||||
} else if (jsonMode) {
|
||||
process.stdout.write('[]\n');
|
||||
}
|
||||
if (jsonMode) process.stdout.write('[]\n');
|
||||
process.exit(0);
|
||||
if (globalThis.__impeccableStaticHtmlDegraded) process.exit(1);
|
||||
process.exit(primary.length > 0 ? 2 : 0);
|
||||
}
|
||||
|
||||
export { formatFindings, handleStdin, confirm, printUsage, detectCli };
|
||||
|
||||
@@ -122,12 +122,8 @@ async function detectHtml(filePath, options = {}) {
|
||||
ruleId: 'import-static-parser',
|
||||
target: filePath,
|
||||
}, async () => {
|
||||
const [htmlparser2, cssSelect, csstree, domutils] = await Promise.all([
|
||||
import('htmlparser2'),
|
||||
import('css-select'),
|
||||
import('css-tree'),
|
||||
import('domutils'),
|
||||
]);
|
||||
const parsers = await import(new URL('../../vendor/static-html-parsers.mjs', import.meta.url).href);
|
||||
const { htmlparser2, cssSelect, csstree, domutils } = parsers;
|
||||
return {
|
||||
parseDocument: htmlparser2.parseDocument,
|
||||
selectAll: cssSelect.selectAll,
|
||||
@@ -137,21 +133,20 @@ async function detectHtml(filePath, options = {}) {
|
||||
domutils,
|
||||
};
|
||||
});
|
||||
} catch (err) {
|
||||
if (!globalThis.__impeccableStaticHtmlWarned) {
|
||||
globalThis.__impeccableStaticHtmlWarned = true;
|
||||
|
||||
process.stderr.write(
|
||||
'impeccable detect: DEGRADED - HTML parser modules unavailable ' +
|
||||
'(htmlparser2, css-select, css-tree, domutils).\n' +
|
||||
'Falling back to regex matching. Custom properties, selector matching and computed ' +
|
||||
'contrast are NOT evaluated; findings are an undercount, not a clean bill of health.\n'
|
||||
);
|
||||
} catch {
|
||||
if (!globalThis.__impeccableStaticHtmlWarned) {
|
||||
globalThis.__impeccableStaticHtmlWarned = true;
|
||||
process.stderr.write(
|
||||
'impeccable detect: DEGRADED - HTML parser modules unavailable ' +
|
||||
'(htmlparser2, css-select, css-tree, domutils).\n' +
|
||||
'Falling back to regex matching. Custom properties, selector matching and computed ' +
|
||||
'contrast are NOT evaluated; findings are an undercount, not a clean bill of health.\n',
|
||||
);
|
||||
}
|
||||
globalThis.__impeccableStaticHtmlDegraded = true;
|
||||
return detectText(html, filePath, options);
|
||||
}
|
||||
|
||||
return detectText(html, filePath, options);
|
||||
}
|
||||
|
||||
const resolvedPath = path.resolve(filePath);
|
||||
const fileDir = path.dirname(resolvedPath);
|
||||
const root = profileStep(profile, {
|
||||
|
||||
+9592
File diff suppressed because one or more lines are too long
+3
-2
@@ -42,9 +42,10 @@
|
||||
"scripts": {
|
||||
"build:skills": "bun run scripts/build.js --skip-root-sync",
|
||||
"build:skills:release": "bun run scripts/build.js",
|
||||
"build": "bun run build:skills && mkdir -p build/_data && rm -rf build/_data/dist && cp -R dist build/_data/dist",
|
||||
"build:release": "bun run build:skills:release && mkdir -p build/_data && rm -rf build/_data/dist && cp -R dist build/_data/dist",
|
||||
"build": "node scripts/build-static-html-parsers.js --check && bun run build:skills && mkdir -p build/_data && rm -rf build/_data/dist && cp -R dist build/_data/dist",
|
||||
"build:release": "node scripts/build-static-html-parsers.js --check && bun run build:skills:release && mkdir -p build/_data && rm -rf build/_data/dist && cp -R dist build/_data/dist",
|
||||
"build:browser": "node scripts/build-browser-detector.js",
|
||||
"build:static-html-parsers": "node scripts/build-static-html-parsers.js",
|
||||
"build:extension": "node scripts/build-extension.js",
|
||||
"clean": "rm -rf dist build",
|
||||
"rebuild": "bun run clean && bun run build",
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Generates cli/engine/vendor/static-html-parsers.mjs
|
||||
* by bundling htmlparser2, css-select, css-tree, and domutils for skill/plugin installs.
|
||||
*
|
||||
* Run: node scripts/build-static-html-parsers.js
|
||||
* Check: node scripts/build-static-html-parsers.js --check
|
||||
*/
|
||||
|
||||
import { createHash } from 'node:crypto';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
|
||||
const ENTRY = path.join(__dirname, 'lib/static-html-parsers.entry.mjs');
|
||||
const OUT_DIR = path.join(ROOT, 'cli/engine/vendor');
|
||||
const OUTPUT = path.join(OUT_DIR, 'static-html-parsers.mjs');
|
||||
const PARSER_PACKAGES = ['htmlparser2', 'css-select', 'css-tree', 'domutils'];
|
||||
const DIGEST_RE = /^\s*\* Source digest: ([0-9a-f]+)\s*$/m;
|
||||
|
||||
function sourceDigest() {
|
||||
const hash = createHash('sha256');
|
||||
hash.update(fs.readFileSync(ENTRY));
|
||||
hash.update('\n');
|
||||
for (const name of PARSER_PACKAGES) {
|
||||
const pkgPath = path.join(ROOT, 'node_modules', name, 'package.json');
|
||||
const version = JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version;
|
||||
hash.update(`${name}@${version}\n`);
|
||||
}
|
||||
return hash.digest('hex').slice(0, 16);
|
||||
}
|
||||
|
||||
function header(digest) {
|
||||
return `/**
|
||||
* GENERATED -- do not edit. Source: scripts/lib/static-html-parsers.entry.mjs
|
||||
* Rebuild: node scripts/build-static-html-parsers.js
|
||||
* Source digest: ${digest}
|
||||
*
|
||||
* Bundles htmlparser2, css-select, css-tree, and domutils for skill/plugin installs.
|
||||
* Third-party licenses: see NOTICE.md.
|
||||
*/
|
||||
`;
|
||||
}
|
||||
|
||||
function generate(outfile) {
|
||||
fs.mkdirSync(path.dirname(outfile), { recursive: true });
|
||||
const result = spawnSync(
|
||||
'bun',
|
||||
['build', ENTRY, '--outfile', outfile, '--target', 'node', '--format', 'esm'],
|
||||
{ cwd: ROOT, encoding: 'utf8' },
|
||||
);
|
||||
if (result.status !== 0) {
|
||||
process.stderr.write(result.stderr || result.stdout || 'bun build failed\n');
|
||||
process.exit(result.status ?? 1);
|
||||
}
|
||||
const output = header(sourceDigest()) + fs.readFileSync(outfile, 'utf8');
|
||||
fs.writeFileSync(outfile, output);
|
||||
return output;
|
||||
}
|
||||
|
||||
if (process.argv.includes('--check')) {
|
||||
const committed = fs.readFileSync(OUTPUT, 'utf8');
|
||||
const found = committed.match(DIGEST_RE)?.[1];
|
||||
const expected = sourceDigest();
|
||||
if (found !== expected) {
|
||||
process.stderr.write(
|
||||
'cli/engine/vendor/static-html-parsers.mjs is stale. Run: node scripts/build-static-html-parsers.js\n',
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
generate(OUTPUT);
|
||||
console.log(`Generated ${path.relative(ROOT, OUTPUT)} (${(fs.statSync(OUTPUT).size / 1024).toFixed(1)} KB)`);
|
||||
@@ -0,0 +1,6 @@
|
||||
export * as htmlparser2 from 'htmlparser2';
|
||||
export * as cssSelect from 'css-select';
|
||||
export * as domutils from 'domutils';
|
||||
import parse from 'css-tree/parser';
|
||||
import generate from 'css-tree/generator';
|
||||
export const csstree = { parse, generate };
|
||||
@@ -25,7 +25,7 @@ export const SUITES = {
|
||||
description: 'Build, provider transforms, CLI helpers, context, and storage unit tests.',
|
||||
triggers: [
|
||||
...COMMON_INFRA_PATTERNS,
|
||||
/^scripts\/(?!benchmark-detector|build-browser-detector|build-extension)/,
|
||||
/^scripts\/(?!benchmark-detector|build-browser-detector|build-static-html-parsers|build-extension|lib\/static-html-parsers\.entry)/,
|
||||
/^skill\/(SKILL\.src\.md|agents\/|reference\/|scripts\/(cleanup-deprecated|comp-diff|comp-spec|build-phase|font-match|data\/font-index|concept-seed|generate-image|context|context-signals|critique-storage|design-parser|doctor|hook|impeccable-paths|is-generated|lib\/(artifact-schema|png|raster|image-metrics|font-fingerprint|font-index|hero-checks|composition-catalog|concept-catalog|provider|staleness|staleness-deep|staleness-notice|surface-briefs|target-slug|template-extensions)|pin|surface-brief))/,
|
||||
/^README(\.npm)?\.md$/,
|
||||
/^cli\/bin\//,
|
||||
@@ -92,7 +92,8 @@ export const SUITES = {
|
||||
...COMMON_INFRA_PATTERNS,
|
||||
/^cli\/engine\//,
|
||||
/^extension\/(background|content|detector|devtools|popup|manifest\.json)/,
|
||||
/^scripts\/(benchmark-detector|build-browser-detector|build-extension)\.js$/,
|
||||
/^scripts\/(benchmark-detector|build-browser-detector|build-static-html-parsers|build-extension)\.js$/,
|
||||
/^scripts\/lib\/static-html-parsers\.entry\.mjs$/,
|
||||
/^site\/(pages\/detector|public\/antipattern|data\/anti-patterns-catalog\.js)/,
|
||||
/^tests\/fixtures\/antipatterns/,
|
||||
],
|
||||
@@ -116,6 +117,7 @@ export const SUITES = {
|
||||
'tests/detect-cli-design-contamination.test.mjs',
|
||||
'tests/detect-cli-design-monorepo.test.mjs',
|
||||
'tests/detect-cli-stdin-dispatch.test.mjs',
|
||||
'tests/detect-static-html-skill-install.test.mjs',
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import { after, describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const engineDir = path.join(root, 'cli', 'engine');
|
||||
const skillDetect = path.join(root, 'skill', 'scripts', 'detect.mjs');
|
||||
const configDep = path.join(root, 'cli', 'lib', 'impeccable-config.mjs');
|
||||
|
||||
const CSS = ':root{--grad:linear-gradient(90deg,#7C3AED,#EC4899)}\n.hero h1{background:var(--grad);-webkit-background-clip:text;background-clip:text;color:transparent}';
|
||||
const HTML = '<html><head><link rel=stylesheet href=s.css></head><body><div class=hero><h1>Hi</h1></div></body></html>';
|
||||
|
||||
function copyDir(src, dest) {
|
||||
fs.mkdirSync(dest, { recursive: true });
|
||||
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
||||
const srcPath = path.join(src, entry.name);
|
||||
const destPath = path.join(dest, entry.name);
|
||||
if (entry.isDirectory()) copyDir(srcPath, destPath);
|
||||
else fs.copyFileSync(srcPath, destPath);
|
||||
}
|
||||
}
|
||||
|
||||
function copyDetectorExternalDeps(scriptsDir) {
|
||||
fs.mkdirSync(path.join(scriptsDir, 'lib'), { recursive: true });
|
||||
fs.copyFileSync(configDep, path.join(scriptsDir, 'lib', 'impeccable-config.mjs'));
|
||||
}
|
||||
|
||||
function writeFixture(dir) {
|
||||
fs.writeFileSync(path.join(dir, 's.css'), CSS);
|
||||
fs.writeFileSync(path.join(dir, 'p.html'), HTML);
|
||||
}
|
||||
|
||||
function runDetect(cwd, detectRel, htmlRel = 'p.html') {
|
||||
const detectPath = path.join(cwd, detectRel);
|
||||
return spawnSync(
|
||||
process.execPath,
|
||||
[detectPath, '--json', '--no-config', '--no-design-system', htmlRel],
|
||||
{ cwd, encoding: 'utf8' },
|
||||
);
|
||||
}
|
||||
|
||||
function assertFullEngine(result) {
|
||||
assert.doesNotMatch(result.stderr, /DEGRADED/);
|
||||
const findings = JSON.parse(result.stdout);
|
||||
assert.ok(findings.some((item) => item.antipattern === 'gradient-text'));
|
||||
assert.equal(result.status, 2);
|
||||
}
|
||||
|
||||
function assertDegraded(result) {
|
||||
assert.match(result.stderr, /DEGRADED/);
|
||||
assert.equal(result.status, 1);
|
||||
JSON.parse(result.stdout);
|
||||
}
|
||||
|
||||
const tempDirs = [];
|
||||
|
||||
after(() => {
|
||||
for (const dir of tempDirs) fs.rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe('static HTML parsers in skill/plugin installs', () => {
|
||||
it('resolves parsers from a skill-shaped scripts/ tree with no node_modules', () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'impeccable-skill-detect-'));
|
||||
tempDirs.push(tmp);
|
||||
fs.mkdirSync(path.join(tmp, 'scripts'), { recursive: true });
|
||||
fs.copyFileSync(skillDetect, path.join(tmp, 'scripts', 'detect.mjs'));
|
||||
copyDir(engineDir, path.join(tmp, 'scripts', 'detector'));
|
||||
copyDetectorExternalDeps(path.join(tmp, 'scripts'));
|
||||
writeFixture(tmp);
|
||||
|
||||
assertFullEngine(runDetect(tmp, path.join('scripts', 'detect.mjs')));
|
||||
});
|
||||
|
||||
it('resolves parsers from a plugin cache skills/impeccable/scripts/ tree', () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'impeccable-plugin-detect-'));
|
||||
tempDirs.push(tmp);
|
||||
const scriptsDir = path.join(tmp, 'skills', 'impeccable', 'scripts');
|
||||
fs.mkdirSync(scriptsDir, { recursive: true });
|
||||
fs.copyFileSync(skillDetect, path.join(scriptsDir, 'detect.mjs'));
|
||||
copyDir(engineDir, path.join(scriptsDir, 'detector'));
|
||||
copyDetectorExternalDeps(scriptsDir);
|
||||
writeFixture(tmp);
|
||||
|
||||
assertFullEngine(runDetect(tmp, path.join('skills', 'impeccable', 'scripts', 'detect.mjs')));
|
||||
});
|
||||
|
||||
it('exits 1 when the vendor bundle is missing', () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'impeccable-skill-degraded-'));
|
||||
tempDirs.push(tmp);
|
||||
fs.mkdirSync(path.join(tmp, 'scripts'), { recursive: true });
|
||||
fs.copyFileSync(skillDetect, path.join(tmp, 'scripts', 'detect.mjs'));
|
||||
copyDir(engineDir, path.join(tmp, 'scripts', 'detector'));
|
||||
copyDetectorExternalDeps(path.join(tmp, 'scripts'));
|
||||
fs.unlinkSync(path.join(tmp, 'scripts', 'detector', 'vendor', 'static-html-parsers.mjs'));
|
||||
writeFixture(tmp);
|
||||
|
||||
assertDegraded(runDetect(tmp, path.join('scripts', 'detect.mjs')));
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { readSourceFiles } from '../../scripts/lib/utils.js';
|
||||
@@ -16,6 +17,35 @@ describe('skill detector bundle', () => {
|
||||
expect(scriptNames.has('detector/detect-antipatterns-browser.js')).toBe(true);
|
||||
expect(scriptNames.has('detector/cli/main.mjs')).toBe(true);
|
||||
expect(scriptNames.has('detector/engines/static-html/detect-html.mjs')).toBe(true);
|
||||
expect(scriptNames.has('detector/vendor/static-html-parsers.mjs')).toBe(true);
|
||||
});
|
||||
|
||||
test('static HTML parser vendor bundle matches the source digest', () => {
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[path.join(ROOT, 'scripts/build-static-html-parsers.js'), '--check'],
|
||||
{ cwd: ROOT, encoding: 'utf8' },
|
||||
);
|
||||
if (result.status !== 0) {
|
||||
throw new Error(result.stderr || result.stdout || `--check exited ${result.status}`);
|
||||
}
|
||||
expect(result.status).toBe(0);
|
||||
});
|
||||
|
||||
test('static HTML parser --check fails when the vendor bundle is stale', () => {
|
||||
const vendor = path.join(ROOT, 'cli/engine/vendor/static-html-parsers.mjs');
|
||||
const original = fs.readFileSync(vendor, 'utf8');
|
||||
try {
|
||||
fs.writeFileSync(vendor, original.replace(/Source digest: [0-9a-f]+/, 'Source digest: deadbeefdeadbeef'));
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[path.join(ROOT, 'scripts/build-static-html-parsers.js'), '--check'],
|
||||
{ cwd: ROOT, encoding: 'utf8' },
|
||||
);
|
||||
expect(result.status).toBe(1);
|
||||
} finally {
|
||||
fs.writeFileSync(vendor, original);
|
||||
}
|
||||
});
|
||||
|
||||
test('critique references the bundled detector command', () => {
|
||||
|
||||
Reference in New Issue
Block a user