mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-14 15:16:35 +03:00
build:extension: ship the wasm-core extension shell and vendor its detector from the detector release
`bun run build:extension` was broken on this branch: it still imported the deleted JS engine (cli/engine/registry/antipatterns.mjs, scripts/lib/browser-detector-bundle.js). The shipped shell now matches the new design. The content script only snapshots the DOM; an extension-owned offscreen document runs the WebAssembly rule core over that snapshot, so the scanned page's CSP no longer matters. That replaces the old approach of injecting a JS rules bundle into the page. New files: extension/offscreen/offscreen.html, plus the "offscreen" permission and a 'wasm-unsafe-eval' extension_pages CSP in the manifest. The manifest version stays at 1.3.3. The shell's own manifest carried 2.0.0; feature branches never bump versions, so the bump is a release step. The five generated detector pieces (core.js, core_bg.wasm, snapshot.js, overlay.js, antipatterns.json) are vendored at build time into the gitignored extension/detector/ by the new scripts/lib/detector-bundle.mjs, which resolves them the same three ways crates/core/build.rs resolves the native archive: IMPECCABLE_DETECTOR_LIB/extension-detector/, the ~/.impeccable/detector/<DETECTOR_VERSION>/ cache, then a checksum-verified download of detector-browser-bundle.zip from the detector release. antipatterns.json is no longer regenerated here. The zip packaging is unchanged. The Firefox variant still builds so `web-ext lint` keeps covering the shared shell, but it cannot scan: Gecko has no chrome.offscreen API. The build prints a one-line warning saying so. Also here: a referenced-path check that fails the build when the manifest or the service worker points at a file that is not in extension/, a resolver unit test wired into the core suite, and the detector rule count in the READMEs synced to the 61 the vendored registry carries. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
836516a7a0
commit
f2c9aeab5b
+94
-49
@@ -3,70 +3,117 @@
|
||||
/**
|
||||
* Builds the browser DevTools extension (Chrome + Firefox).
|
||||
*
|
||||
* 1. Generates the extension variant of the browser detector
|
||||
* 2. Extracts antipatterns.json for the panel UI
|
||||
* 3. Packages extension.zip (Chrome Web Store) and extension-firefox.zip (AMO)
|
||||
* 1. Vendors the five generated detector pieces (core.js, core_bg.wasm,
|
||||
* snapshot.js, overlay.js, antipatterns.json) into extension/detector/
|
||||
* from the closed detector release, resolved by
|
||||
* scripts/lib/detector-bundle.mjs the same three ways crates/core/build.rs
|
||||
* resolves the native archive.
|
||||
* 2. Checks that every path the manifest and the service worker reference
|
||||
* exists in extension/.
|
||||
* 3. Packages extension.zip (Chrome Web Store) and extension-firefox.zip (AMO).
|
||||
*
|
||||
* The source `extension/manifest.json` is the Chrome manifest. The Firefox
|
||||
* variant is derived at build time: the MV3 background service worker is
|
||||
* declared as an event-page `scripts` entry (the universally-supported path on
|
||||
* Gecko), and `browser_specific_settings.gecko` is added for AMO signing.
|
||||
*
|
||||
* Firefox caveat: the shell runs the WebAssembly rule core in an extension
|
||||
* offscreen document, and Gecko has no `chrome.offscreen` API, so the Firefox
|
||||
* package builds and lints but cannot scan until that gap is closed. The
|
||||
* Firefox artifact is still produced so `web-ext lint` keeps covering the
|
||||
* shared shell.
|
||||
*
|
||||
* Run: node scripts/build-extension.js
|
||||
* IMPECCABLE_DETECTOR_LIB=<dir> use a local `cargo xtask detector-archive` output
|
||||
* IMPECCABLE_DETECTOR_BASE=<url> override the detector release root
|
||||
* IMPECCABLE_DETECTOR_OFFLINE=1 refuse to download
|
||||
*/
|
||||
|
||||
import { execSync } from 'child_process';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { ANTIPATTERNS } from '../cli/engine/registry/antipatterns.mjs';
|
||||
import { bundleBrowserDetectorModules } from './lib/browser-detector-bundle.js';
|
||||
import { DETECTOR_PIECES, vendorDetectorBundle } from './lib/detector-bundle.mjs';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
const EXT_DIR = path.join(ROOT, 'extension');
|
||||
const DETECTOR_DIR = path.join(EXT_DIR, 'detector');
|
||||
|
||||
const DETECTOR_OUTPUT = path.join(EXT_DIR, 'detector/detect.js');
|
||||
const AP_OUTPUT = path.join(EXT_DIR, 'detector/antipatterns.json');
|
||||
// --- 1. Vendor the detector pieces ---
|
||||
|
||||
const code = bundleBrowserDetectorModules(ROOT);
|
||||
const vendored = await vendorDetectorBundle({
|
||||
root: ROOT,
|
||||
destDir: DETECTOR_DIR,
|
||||
log: (line) => console.log(line),
|
||||
});
|
||||
const totalKb = vendored.files.reduce((sum, f) => sum + f.bytes, 0) / 1024;
|
||||
console.log(
|
||||
`Vendored ${DETECTOR_PIECES.length} detector pieces into extension/detector/ ` +
|
||||
`(${totalKb.toFixed(1)} KB, detector v${vendored.version}, source: ${vendored.source})`,
|
||||
);
|
||||
|
||||
// --- 1. Build detector ---
|
||||
const ruleCount = JSON.parse(fs.readFileSync(path.join(DETECTOR_DIR, 'antipatterns.json'), 'utf-8')).length;
|
||||
console.log(` antipatterns.json: ${ruleCount} rules (generated; not regenerated here)`);
|
||||
|
||||
const output = `/**
|
||||
* Anti-Pattern Browser Detector for Impeccable (Extension Variant)
|
||||
* Copyright (c) 2026 Paul Bakaus
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* GENERATED -- do not edit. Source: cli/engine/browser/injected/index.mjs
|
||||
* Rebuild: node scripts/build-extension.js
|
||||
// --- 2. Referenced-file check ---
|
||||
|
||||
const chromeManifest = JSON.parse(fs.readFileSync(path.join(EXT_DIR, 'manifest.json'), 'utf-8'));
|
||||
|
||||
const serviceWorker = chromeManifest.background?.service_worker;
|
||||
if (!serviceWorker) {
|
||||
throw new Error(
|
||||
'extension/manifest.json: expected background.service_worker to derive the Firefox manifest',
|
||||
);
|
||||
}
|
||||
|
||||
/** Every extension-relative path the manifest declares. */
|
||||
function manifestReferences(manifest) {
|
||||
const refs = [];
|
||||
const add = (value) => { if (typeof value === 'string' && value) refs.push(value.replace(/^\//, '')); };
|
||||
add(manifest.background?.service_worker);
|
||||
for (const script of manifest.background?.scripts || []) add(script);
|
||||
add(manifest.devtools_page);
|
||||
add(manifest.action?.default_popup);
|
||||
for (const icon of Object.values(manifest.action?.default_icon || {})) add(icon);
|
||||
for (const icon of Object.values(manifest.icons || {})) add(icon);
|
||||
for (const entry of manifest.content_scripts || []) {
|
||||
for (const file of entry.js || []) add(file);
|
||||
for (const file of entry.css || []) add(file);
|
||||
}
|
||||
for (const entry of manifest.web_accessible_resources || []) {
|
||||
for (const resource of entry.resources || []) add(resource);
|
||||
}
|
||||
return refs;
|
||||
}
|
||||
|
||||
/**
|
||||
* The service worker injects the content script and its generated companions
|
||||
* by path and opens the offscreen document by path, so those files are
|
||||
* referenced without appearing in the manifest.
|
||||
*/
|
||||
(function () {
|
||||
if (typeof window === 'undefined') return;
|
||||
${code}
|
||||
})();
|
||||
`;
|
||||
function serviceWorkerReferences(source) {
|
||||
const refs = [];
|
||||
const offscreen = source.match(/OFFSCREEN_URL\s*=\s*['"]([^'"]+)['"]/);
|
||||
if (offscreen) refs.push(offscreen[1]);
|
||||
for (const block of source.matchAll(/files:\s*\[([^\]]*)\]/g)) {
|
||||
for (const file of block[1].matchAll(/['"]([^'"]+)['"]/g)) refs.push(file[1]);
|
||||
}
|
||||
return refs;
|
||||
}
|
||||
|
||||
fs.mkdirSync(path.dirname(DETECTOR_OUTPUT), { recursive: true });
|
||||
fs.writeFileSync(DETECTOR_OUTPUT, output);
|
||||
console.log(`Generated ${path.relative(ROOT, DETECTOR_OUTPUT)} (${(output.length / 1024).toFixed(1)} KB)`);
|
||||
|
||||
// --- 2. Extract antipatterns.json ---
|
||||
|
||||
// Include description so the devtools panel can show the full rule explanation
|
||||
// in tooltips.
|
||||
const apJson = ANTIPATTERNS.map(({ id, name, category, description }) => ({
|
||||
id,
|
||||
name,
|
||||
category: category || 'quality',
|
||||
description: description || '',
|
||||
}));
|
||||
fs.writeFileSync(AP_OUTPUT, JSON.stringify(apJson, null, 2) + '\n');
|
||||
console.log(`Generated ${path.relative(ROOT, AP_OUTPUT)} (${ANTIPATTERNS.length} rules)`);
|
||||
const swSource = fs.readFileSync(path.join(EXT_DIR, serviceWorker), 'utf-8');
|
||||
const referenced = [...new Set([...manifestReferences(chromeManifest), ...serviceWorkerReferences(swSource)])];
|
||||
const missingRefs = referenced.filter((rel) => !fs.existsSync(path.join(EXT_DIR, rel)));
|
||||
if (missingRefs.length) {
|
||||
throw new Error(
|
||||
`extension/ is missing referenced file(s):\n${missingRefs.map((r) => ` · ${r}`).join('\n')}`,
|
||||
);
|
||||
}
|
||||
console.log(`Checked ${referenced.length} referenced paths; all present in extension/`);
|
||||
|
||||
// --- 3. Zip packaging ---
|
||||
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
const DIST = path.join(ROOT, 'dist');
|
||||
fs.mkdirSync(DIST, { recursive: true });
|
||||
|
||||
@@ -90,15 +137,6 @@ packZip(path.join(DIST, 'extension.zip'), EXT_DIR, ['STORE_LISTING.md', '*.DS_St
|
||||
// --- 3b. Firefox: derive a Gecko-compatible manifest and stage an unpacked
|
||||
// build (consumed by `web-ext lint` in CI), then zip it for AMO. ---
|
||||
|
||||
const chromeManifest = JSON.parse(fs.readFileSync(path.join(EXT_DIR, 'manifest.json'), 'utf-8'));
|
||||
|
||||
const serviceWorker = chromeManifest.background?.service_worker;
|
||||
if (!serviceWorker) {
|
||||
throw new Error(
|
||||
'extension/manifest.json: expected background.service_worker to derive the Firefox manifest',
|
||||
);
|
||||
}
|
||||
|
||||
const firefoxManifest = {
|
||||
...chromeManifest,
|
||||
// Gecko supports MV3 via non-persistent event pages. Declaring `scripts`
|
||||
@@ -116,7 +154,8 @@ const firefoxManifest = {
|
||||
// everything else this extension uses (MV3 action, scripting, devtools,
|
||||
// object-form web_accessible_resources, storage.sync) landed long before.
|
||||
strict_min_version: '140.0',
|
||||
// The detector runs entirely in-page; nothing is transmitted off-device.
|
||||
// The rules run in the extension's own offscreen document; nothing is
|
||||
// transmitted off-device.
|
||||
data_collection_permissions: { required: ['none'] },
|
||||
},
|
||||
},
|
||||
@@ -139,3 +178,9 @@ console.log(`Staged ${path.relative(ROOT, ffStageDir)}/ (Firefox manifest)`);
|
||||
|
||||
// STORE_LISTING.md is already filtered out of the stage dir above.
|
||||
packZip(path.join(DIST, 'extension-firefox.zip'), ffStageDir, ['*.DS_Store']);
|
||||
|
||||
console.warn(
|
||||
'Warning: the Firefox package cannot scan yet. The rule core runs in an ' +
|
||||
'extension offscreen document and Gecko has no chrome.offscreen API. The ' +
|
||||
'artifact is built so web-ext lint keeps covering the shared shell.',
|
||||
);
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
/**
|
||||
* Resolves the browser pieces of the closed detector for `bun run build:extension`.
|
||||
*
|
||||
* The rule engine is proprietary and ships as a prebuilt release (see
|
||||
* docs/ENGINE.md, "The closed detector"). The native archive that
|
||||
* `crates/core/build.rs` links and the browser bundle the extension vendors
|
||||
* come from the same `detector-v<DETECTOR_VERSION>` release, and they resolve
|
||||
* the same three ways:
|
||||
*
|
||||
* 1. `IMPECCABLE_DETECTOR_LIB=<dir>` holding an `extension-detector/`
|
||||
* subdirectory (what the detector repo's `cargo xtask detector-archive`
|
||||
* writes for a local build).
|
||||
* 2. `~/.impeccable/detector/<DETECTOR_VERSION>/extension-detector/`
|
||||
* (`IMPECCABLE_HOME` moves the root).
|
||||
* 3. A download of `detector-browser-bundle.zip` from the release, verified
|
||||
* against its `.sha256` sidecar and extracted into that cache.
|
||||
* `IMPECCABLE_DETECTOR_BASE` overrides the release root;
|
||||
* `IMPECCABLE_DETECTOR_OFFLINE=1` refuses to download.
|
||||
*
|
||||
* The five pieces are generated by the detector repo's `cargo xtask bundle`
|
||||
* and are never tracked here; `extension/detector/` is gitignored.
|
||||
*/
|
||||
import crypto from 'node:crypto';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
||||
|
||||
export const DEFAULT_DETECTOR_BASE = 'https://github.com/pbakaus/impeccable/releases/download';
|
||||
export const BROWSER_BUNDLE_ASSET = 'detector-browser-bundle.zip';
|
||||
/** Where the pieces sit inside a detector-archive dir and inside the cache. */
|
||||
export const BUNDLE_SUBDIR = 'extension-detector';
|
||||
/** Where the pieces sit inside detector-browser-bundle.zip. */
|
||||
export const ZIP_MEMBER_DIR = 'extension-src/detector';
|
||||
|
||||
/** The five generated pieces the extension shell loads. */
|
||||
export const DETECTOR_PIECES = [
|
||||
'core.js',
|
||||
'core_bg.wasm',
|
||||
'snapshot.js',
|
||||
'overlay.js',
|
||||
'antipatterns.json',
|
||||
];
|
||||
|
||||
export function readDetectorVersion(root = ROOT) {
|
||||
return fs.readFileSync(path.join(root, 'DETECTOR_VERSION'), 'utf-8').trim();
|
||||
}
|
||||
|
||||
/** `~/.impeccable`, or `IMPECCABLE_HOME` when set. */
|
||||
export function detectorHome(env = process.env) {
|
||||
return env.IMPECCABLE_HOME ? path.resolve(env.IMPECCABLE_HOME) : path.join(os.homedir(), '.impeccable');
|
||||
}
|
||||
|
||||
/** The version-pinned cache directory the download extracts into. */
|
||||
export function cacheBundleDir(version, env = process.env) {
|
||||
return path.join(detectorHome(env), 'detector', version, BUNDLE_SUBDIR);
|
||||
}
|
||||
|
||||
export function assetUrl(version, asset, base) {
|
||||
return `${base.replace(/\/$/, '')}/detector-v${version}/${asset}`;
|
||||
}
|
||||
|
||||
/** True when `dir` holds all five pieces. */
|
||||
export function isCompleteBundleDir(dir) {
|
||||
return !!dir && DETECTOR_PIECES.every((piece) => fs.existsSync(path.join(dir, piece)));
|
||||
}
|
||||
|
||||
function missingPieces(dir) {
|
||||
return DETECTOR_PIECES.filter((piece) => !fs.existsSync(path.join(dir, piece)));
|
||||
}
|
||||
|
||||
/** A `.sha256` sidecar is either a bare hex digest or `<digest> <filename>`. */
|
||||
function parseSha256(text) {
|
||||
const match = String(text).trim().match(/\b([0-9a-fA-F]{64})\b/);
|
||||
return match ? match[1].toLowerCase() : null;
|
||||
}
|
||||
|
||||
async function fetchBuffer(url, fetchImpl) {
|
||||
const res = await fetchImpl(url, { redirect: 'follow' });
|
||||
if (!res.ok) throw new Error(`GET ${url} failed with HTTP ${res.status}`);
|
||||
return Buffer.from(await res.arrayBuffer());
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads `detector-browser-bundle.zip`, verifies it against its `.sha256`
|
||||
* sidecar, and extracts the five pieces into `destDir`. Refuses to install an
|
||||
* unverified archive.
|
||||
*/
|
||||
export async function downloadBundle({
|
||||
version,
|
||||
base,
|
||||
destDir,
|
||||
fetchImpl = fetch,
|
||||
log = () => {},
|
||||
unzip = defaultUnzip,
|
||||
} = {}) {
|
||||
const zipUrl = assetUrl(version, BROWSER_BUNDLE_ASSET, base);
|
||||
log(`Downloading ${BROWSER_BUNDLE_ASSET} from detector-v${version}`);
|
||||
|
||||
let expected;
|
||||
try {
|
||||
const sidecar = await fetchBuffer(`${zipUrl}.sha256`, fetchImpl);
|
||||
expected = parseSha256(sidecar.toString('utf-8'));
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
`${BROWSER_BUNDLE_ASSET}.sha256 could not be read from ${zipUrl}.sha256 (${err.message}). ` +
|
||||
'Refusing to install an unverified detector bundle.',
|
||||
);
|
||||
}
|
||||
if (!expected) {
|
||||
throw new Error(
|
||||
`${zipUrl}.sha256 did not contain a SHA-256 digest. Refusing to install an unverified detector bundle.`,
|
||||
);
|
||||
}
|
||||
|
||||
const zip = await fetchBuffer(zipUrl, fetchImpl);
|
||||
const actual = crypto.createHash('sha256').update(zip).digest('hex');
|
||||
if (actual !== expected) {
|
||||
throw new Error(
|
||||
`${BROWSER_BUNDLE_ASSET} checksum mismatch for detector-v${version}:\n` +
|
||||
` expected ${expected}\n actual ${actual}\nRefusing to install it.`,
|
||||
);
|
||||
}
|
||||
|
||||
const staging = fs.mkdtempSync(path.join(os.tmpdir(), 'impeccable-detector-'));
|
||||
try {
|
||||
const zipPath = path.join(staging, BROWSER_BUNDLE_ASSET);
|
||||
fs.writeFileSync(zipPath, zip);
|
||||
const extractDir = path.join(staging, 'out');
|
||||
fs.mkdirSync(extractDir, { recursive: true });
|
||||
unzip(zipPath, `${ZIP_MEMBER_DIR}/*`, extractDir);
|
||||
|
||||
const missing = missingPieces(extractDir);
|
||||
if (missing.length) {
|
||||
throw new Error(
|
||||
`detector-v${version} ${BROWSER_BUNDLE_ASSET} is missing ${missing.join(', ')} under ${ZIP_MEMBER_DIR}/.`,
|
||||
);
|
||||
}
|
||||
fs.mkdirSync(destDir, { recursive: true });
|
||||
for (const piece of DETECTOR_PIECES) {
|
||||
fs.copyFileSync(path.join(extractDir, piece), path.join(destDir, piece));
|
||||
}
|
||||
} finally {
|
||||
fs.rmSync(staging, { recursive: true, force: true });
|
||||
}
|
||||
return destDir;
|
||||
}
|
||||
|
||||
/** `unzip -o -j` flattens the member directory, so every piece lands in destDir. */
|
||||
function defaultUnzip(zipPath, member, destDir) {
|
||||
try {
|
||||
execFileSync('unzip', ['-o', '-j', zipPath, member, '-d', destDir], { stdio: 'pipe' });
|
||||
} catch (err) {
|
||||
const detail = err.stderr ? String(err.stderr).trim() : err.message;
|
||||
throw new Error(`unzip failed on ${path.basename(zipPath)}: ${detail}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a directory holding the five detector pieces, downloading the release
|
||||
* bundle into the version cache if neither of the local options has them.
|
||||
*
|
||||
* @returns {Promise<{ dir: string, source: 'env' | 'cache' | 'download', version: string }>}
|
||||
*/
|
||||
export async function resolveDetectorBundle({
|
||||
root = ROOT,
|
||||
version = readDetectorVersion(root),
|
||||
env = process.env,
|
||||
base = env.IMPECCABLE_DETECTOR_BASE || DEFAULT_DETECTOR_BASE,
|
||||
fetchImpl = fetch,
|
||||
log = () => {},
|
||||
unzip = defaultUnzip,
|
||||
} = {}) {
|
||||
if (env.IMPECCABLE_DETECTOR_LIB) {
|
||||
const dir = path.join(path.resolve(env.IMPECCABLE_DETECTOR_LIB), BUNDLE_SUBDIR);
|
||||
if (isCompleteBundleDir(dir)) return { dir, source: 'env', version };
|
||||
throw new Error(
|
||||
`IMPECCABLE_DETECTOR_LIB=${env.IMPECCABLE_DETECTOR_LIB} does not hold a complete ${BUNDLE_SUBDIR}/.\n` +
|
||||
` looked in: ${dir}\n` +
|
||||
` missing: ${(fs.existsSync(dir) ? missingPieces(dir) : DETECTOR_PIECES).join(', ')}\n` +
|
||||
" Run `cargo xtask detector-archive --out <dir>` in the detector repo, or unset " +
|
||||
'IMPECCABLE_DETECTOR_LIB to use the published release.',
|
||||
);
|
||||
}
|
||||
|
||||
const cached = cacheBundleDir(version, env);
|
||||
if (isCompleteBundleDir(cached)) return { dir: cached, source: 'cache', version };
|
||||
|
||||
if (env.IMPECCABLE_DETECTOR_OFFLINE === '1') {
|
||||
throw new Error(
|
||||
`No detector browser bundle for v${version} and IMPECCABLE_DETECTOR_OFFLINE=1 forbids downloading.\n` +
|
||||
` cache: ${cached}\n` +
|
||||
' Set IMPECCABLE_DETECTOR_LIB to a local `cargo xtask detector-archive` output, ' +
|
||||
'or clear IMPECCABLE_DETECTOR_OFFLINE.',
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
await downloadBundle({ version, base, destDir: cached, fetchImpl, log, unzip });
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
`Could not obtain the detector browser bundle for v${version}.\n` +
|
||||
` ${err.message}\n` +
|
||||
` release base: ${base}\n` +
|
||||
' Options: point IMPECCABLE_DETECTOR_LIB at a local `cargo xtask detector-archive` output, ' +
|
||||
'override the release root with IMPECCABLE_DETECTOR_BASE, or publish detector-v' +
|
||||
`${version} (see docs/ENGINE.md).`,
|
||||
);
|
||||
}
|
||||
return { dir: cached, source: 'download', version };
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the bundle and copies the five pieces into `destDir`
|
||||
* (`extension/detector/`).
|
||||
*/
|
||||
export async function vendorDetectorBundle({ destDir, ...options } = {}) {
|
||||
const resolved = await resolveDetectorBundle(options);
|
||||
fs.mkdirSync(destDir, { recursive: true });
|
||||
const files = [];
|
||||
for (const piece of DETECTOR_PIECES) {
|
||||
const to = path.join(destDir, piece);
|
||||
fs.copyFileSync(path.join(resolved.dir, piece), to);
|
||||
files.push({ name: piece, bytes: fs.statSync(to).size });
|
||||
}
|
||||
return { ...resolved, destDir, files };
|
||||
}
|
||||
@@ -62,6 +62,7 @@ export const SUITES = {
|
||||
'tests/openai-plugin.test.mjs',
|
||||
'tests/release.test.mjs',
|
||||
'tests/check-detector-release.test.mjs',
|
||||
'tests/detector-bundle.test.mjs',
|
||||
'tests/skill-reference.test.mjs',
|
||||
'tests/readme-gitignore.test.mjs',
|
||||
'tests/test-suites.test.mjs',
|
||||
@@ -95,7 +96,7 @@ export const SUITES = {
|
||||
description: 'Extension packaging checks (the detector engine itself is tested in the engine repo and by the oracle).',
|
||||
triggers: [
|
||||
...COMMON_INFRA_PATTERNS,
|
||||
/^extension\/(background|content|detector|devtools|popup|manifest\.json)/,
|
||||
/^extension\/(background|content|detector|devtools|offscreen|popup|manifest\.json)/,
|
||||
/^scripts\/(build-browser-detector|build-extension)\.js$/,
|
||||
],
|
||||
commands: [
|
||||
|
||||
Reference in New Issue
Block a user