feat: publish Regex Tools 0.2.0

This commit is contained in:
2026-07-27 01:06:57 +02:00
parent 873b9b218d
commit 4951c966d4
180 changed files with 35344 additions and 503 deletions

View File

@@ -1,8 +1,37 @@
import { readFile, readdir } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import {
buildPcre2Pack,
parsePcre2BuildArguments,
} from "./pcre2-engine-pack.mjs";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const options = parsePcre2BuildArguments(process.argv.slice(2));
if (options.mode === "help") {
console.log(`Usage:
node scripts/build-engines.mjs
node scripts/build-engines.mjs --pcre2 --source-dir PATH --emcc PATH [--force]
The PCRE2 command is an explicit offline build gate. It verifies exact local
source and compiler identities, writes .engine-build/pcre2, and never
downloads or publishes an engine pack. Install the verified local pack with
npm run engines:pcre2:install.`);
process.exit(0);
}
if (options.mode === "pcre2") {
const result = await buildPcre2Pack(options, root);
console.log(
`Built and verified staged PCRE2 ${result.metadata.engineVersion} pack at ${result.output}.`,
);
console.log(
"The verified pack remains in private staging until the explicit local install command is run.",
);
process.exit(0);
}
const packageJson = JSON.parse(
await readFile(path.join(root, "package.json"), "utf8"),
);
@@ -10,15 +39,16 @@ const engineDirectory = path.join(root, "public", "engines");
const entries = (await readdir(engineDirectory)).sort();
if (
packageJson.version !== "0.1.0" ||
entries.length !== 1 ||
entries[0] !== "README.md"
typeof packageJson.version !== "string" ||
entries.length !== 2 ||
entries[0] !== "README.md" ||
entries[1] !== "pcre2"
) {
throw new Error(
"The ECMAScript release must contain only the documented native-browser engine placeholder.",
"The production build requires the documented PCRE2 runtime pack.",
);
}
console.log(
"Regex Tools 0.1.0 uses the native browser RegExp engine; no external engine pack needs building.",
"The checked-in PCRE2 runtime pack is present; no release-time download or engine compilation is needed.",
);

View File

@@ -7,14 +7,23 @@ const root = join(dirname(fileURLToPath(import.meta.url)), "..");
const sourcePath = join(root, "src", "toolbox", "manifest.source.json");
const outputPath = join(root, "public", "toolbox-app.json");
const packagePath = join(root, "package.json");
const applicationVersionPath = join(root, "src", "version.ts");
const checkOnly = process.argv.includes("--check");
const source = JSON.parse(await readFile(sourcePath, "utf8"));
const packageJson = JSON.parse(await readFile(packagePath, "utf8"));
const applicationVersionSource = await readFile(applicationVersionPath, "utf8");
const applicationVersion =
/^export const APPLICATION_VERSION = "([^"]+)";$/mu.exec(
applicationVersionSource,
)?.[1];
if (source.version !== packageJson.version) {
if (
source.version !== packageJson.version ||
applicationVersion !== packageJson.version
) {
throw new Error(
`Manifest version ${source.version} differs from package version ${packageJson.version}`,
`Version drift: manifest ${source.version}, application ${String(applicationVersion)}, package ${packageJson.version}`,
);
}

View File

@@ -0,0 +1,16 @@
import path from "node:path";
import { fileURLToPath } from "node:url";
import { installPcre2Pack } from "./pcre2-engine-pack.mjs";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const arguments_ = process.argv.slice(2);
if (arguments_.length > 1) {
throw new Error(
"Usage: node scripts/install-pcre2-engine.mjs [.engine-build/pcre2]",
);
}
const source = path.resolve(root, arguments_[0] ?? ".engine-build/pcre2");
const result = await installPcre2Pack(source, root);
console.log(
`Installed verified PCRE2 ${result.metadata.engineVersion} runtime assets at ${result.output}.`,
);

View File

@@ -18,6 +18,7 @@ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const zipEpoch = new Date(1980, 0, 1, 0, 0, 0);
const gplVersion3TextSha256 =
"fb981668c18a279e285fc4d83fba1e836cc84dd4daa73c9697d3cfd2d8aca6e0";
const expectedApplicationVersion = "0.2.0";
const requiredRootFiles = [
"LICENSE",
"README.md",
@@ -332,13 +333,27 @@ async function collectReleaseEntries() {
await readFile(path.join(root, "package.json"), "utf8"),
);
if (
packageJson.version !== "0.1.0" ||
packageJson.version !== expectedApplicationVersion ||
packageJson.license !== "GPL-3.0-or-later" ||
packageJson.repository?.url !==
"git+https://git.add-ideas.de/zemion/regex-tools.git"
) {
throw new Error("Package version, licence or source identity drifted.");
}
const sourceIdentity = await readFile(path.join(root, "SOURCE.md"), "utf8");
if (
!sourceIdentity.includes(
`- Release version: \`${expectedApplicationVersion}\``,
) ||
!sourceIdentity.includes(
`- Release tag: \`v${expectedApplicationVersion}\``,
) ||
!sourceIdentity.includes(
"- Repository: <https://git.add-ideas.de/zemion/regex-tools>",
)
) {
throw new Error("SOURCE.md release identity drifted.");
}
const licence = await readFile(path.join(root, "LICENSE"), "utf8");
if (
!licence.includes("GNU GENERAL PUBLIC LICENSE") ||
@@ -371,6 +386,14 @@ async function collectReleaseEntries() {
"CHANGELOG.md",
"SOURCE.md",
"THIRD_PARTY_NOTICES.md",
"engines/README.md",
"engines/pcre2/LICENSE.txt",
"engines/pcre2/SHA256SUMS",
"engines/pcre2/engine-metadata.json",
"engines/pcre2/pcre2.mjs",
"engines/pcre2/pcre2.wasm",
"LICENSES/Emscripten-MIT.txt",
"LICENSES/PCRE2.txt",
"LICENSES/build/rolldown-1.1.5/LICENSE",
"LICENSES/build/vite-8.1.5/LICENSE.md",
]) {

View File

@@ -0,0 +1,110 @@
import { createHash } from "node:crypto";
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { spawnSync } from "node:child_process";
import { describe, expect, it } from "vitest";
import {
generatePcre2C,
type Pcre2CGenerationRequest,
} from "../src/regex/codegen/pcre2-c";
function request(operation: "match" | "replace"): Pcre2CGenerationRequest {
return {
flavour: "pcre2",
flavourVersion: "PCRE2 10.47 8-bit WebAssembly",
operation,
pattern: String.raw`(?<word>\p{L}+)-(\d+)`,
flags: ["g"],
options: {
matchLimit: 1_000_000,
depthLimit: 1_000,
heapLimitKib: 32_768,
},
subject: "x Grüße-42 y alpha-7",
scanAll: true,
...(operation === "replace" ? { replacement: "$2:$<word>" } : {}),
maximumMatches: 100,
maximumCaptureRows: 1_000,
maximumOutputBytes: 4_096,
};
}
function sha256(value: string): string {
return createHash("sha256").update(value).digest("hex");
}
const exactToolchainPrefix = process.env.PCRE2_CODEGEN_PREFIX;
const compileWithExactPcre2 = exactToolchainPrefix ? it : it.skip;
describe("PCRE2 C code-generation golden", () => {
it("retains a reviewed deterministic source identity", () => {
expect(sha256(generatePcre2C(request("match")).source)).toBe(
"22b91d578a449b0ed5c30e6eadad30f19a2bcb26ac9db355c8ec7a69328675da",
);
expect(sha256(generatePcre2C(request("replace")).source)).toBe(
"ab90a0d9831ea7b7145aa0072ac352b42baf9a795319d81f531b0f341b04a636",
);
});
compileWithExactPcre2(
"compiles and executes against the explicitly supplied PCRE2 10.47 C toolchain",
async () => {
if (!exactToolchainPrefix) return;
const header = path.join(exactToolchainPrefix, "include", "pcre2.h");
const library =
process.env.PCRE2_CODEGEN_LIBRARY ??
path.join(exactToolchainPrefix, "lib64", "libpcre2-8.a");
await expect(readFile(header)).resolves.toBeInstanceOf(Buffer);
await expect(readFile(library)).resolves.toBeInstanceOf(Buffer);
const directory = await mkdtemp(
path.join(tmpdir(), "regex-tools-codegen-"),
);
try {
for (const operation of ["match", "replace"] as const) {
const generated = generatePcre2C(request(operation));
const source = path.join(directory, generated.fileName);
const executable = path.join(directory, `pcre2-${operation}`);
await writeFile(source, generated.source);
const compiled = spawnSync(
process.env.CC ?? "cc",
[
"-std=c17",
"-Wall",
"-Wextra",
"-Werror",
`-I${path.join(exactToolchainPrefix, "include")}`,
source,
library,
"-o",
executable,
],
{ encoding: "utf8" },
);
expect(
`${compiled.stdout}${compiled.stderr}`,
`${operation} fixture did not compile`,
).toBe("");
expect(compiled.status).toBe(0);
const executed = spawnSync(executable, [], { encoding: "utf8" });
expect(
executed.status,
`${operation} fixture failed: ${executed.stdout}${executed.stderr}`,
).toBe(0);
if (operation === "match") {
expect(executed.stdout).toContain("match 1: UTF-8 bytes 2..12");
expect(executed.stdout).toContain("group 1 / word");
expect(executed.stdout).toContain(
"completed: 2 match(es), 4 capture row(s)",
);
} else {
expect(executed.stdout).toBe("x 42:Grüße y 7:alpha");
expect(executed.stderr).toContain("completed: 2 substitution(s)");
}
}
} finally {
await rm(directory, { recursive: true, force: true });
}
},
);
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,89 @@
// @vitest-environment node
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import {
PCRE2_LOCK,
parsePcre2BuildArguments,
verifyPcre2Pack,
} from "./pcre2-engine-pack.mjs";
const temporaryDirectories: string[] = [];
afterEach(async () => {
await Promise.all(
temporaryDirectories
.splice(0)
.map((directory) => rm(directory, { recursive: true, force: true })),
);
});
describe("PCRE2 engine-pack gate", () => {
it("keeps the default build on the checked-in production-assets path", () => {
expect(parsePcre2BuildArguments([])).toEqual({ mode: "ecmascript" });
});
it("requires explicit offline source and compiler paths", () => {
expect(() => parsePcre2BuildArguments(["--pcre2"])).toThrow(
"requires both --source-dir and --emcc",
);
expect(() =>
parsePcre2BuildArguments([
"--source-dir",
"/source",
"--emcc",
"/compiler",
]),
).toThrow("explicit --pcre2 gate");
});
it("parses the exact staged-build contract", () => {
expect(
parsePcre2BuildArguments([
"--pcre2",
"--source-dir",
"/source",
"--emcc",
"/compiler",
"--force",
]),
).toEqual({
mode: "pcre2",
sourceDirectory: "/source",
emccFile: "/compiler",
force: true,
});
});
it.each([
["--unknown"],
["--pcre2", "--pcre2"],
["--pcre2", "--source-dir"],
["--pcre2", "--force", "--force"],
])("rejects ambiguous arguments %j", (...arguments_) => {
expect(() => parsePcre2BuildArguments(arguments_)).toThrow();
});
it("pins source, signed-tag object and compiler identities", () => {
expect(PCRE2_LOCK.source.tag).toBe("pcre2-10.47");
expect(PCRE2_LOCK.source.tagObject).toHaveLength(40);
expect(PCRE2_LOCK.source.commit).toHaveLength(40);
expect(PCRE2_LOCK.source.tree).toHaveLength(40);
expect(PCRE2_LOCK.emscripten.version).toBe("6.0.4");
expect(PCRE2_LOCK.emscripten.emccSha256).toHaveLength(64);
expect(PCRE2_LOCK.bridgeAbi).toBe(3);
expect(PCRE2_LOCK.maximumSubjectBytes).toBe(16 * 1024 * 1024);
expect(PCRE2_LOCK.maximumMatches).toBe(10_000);
});
it("rejects an incomplete staged pack before attempting to load it", async () => {
const directory = await mkdtemp(path.join(tmpdir(), "regex-pcre2-test-"));
temporaryDirectories.push(directory);
await expect(verifyPcre2Pack(directory, process.cwd())).rejects.toThrow(
"must contain only",
);
});
});

View File

@@ -13,6 +13,7 @@ const mediaTypes = new Map([
[".css", "text/css; charset=utf-8"],
[".html", "text/html; charset=utf-8"],
[".js", "text/javascript; charset=utf-8"],
[".mjs", "text/javascript; charset=utf-8"],
[".json", "application/json; charset=utf-8"],
[".svg", "image/svg+xml"],
[".wasm", "application/wasm"],

View File

@@ -1,8 +1,25 @@
import { lstat, readFile, readdir } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { verifyPcre2Pack } from "./pcre2-engine-pack.mjs";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const arguments_ = process.argv.slice(2);
if (arguments_.length > 0) {
if (arguments_.length !== 2 || arguments_[0] !== "--pcre2-pack") {
throw new Error(
"Usage: node scripts/verify-engine-assets.mjs [--pcre2-pack PATH]",
);
}
const pack = path.resolve(root, arguments_[1]);
const metadata = await verifyPcre2Pack(pack, root);
console.log(
`Verified staged PCRE2 ${metadata.engineVersion} pack and WebAssembly bridge smoke test.`,
);
process.exit(0);
}
const engineDirectory = path.join(root, "dist", "engines");
const details = await lstat(engineDirectory);
if (details.isSymbolicLink() || !details.isDirectory()) {
@@ -14,21 +31,21 @@ const entries = (await readdir(engineDirectory, { withFileTypes: true })).sort(
left.name < right.name ? -1 : left.name > right.name ? 1 : 0,
);
if (
entries.length !== 1 ||
entries.length !== 2 ||
!entries[0]?.isFile() ||
entries[0].name !== "README.md"
entries[0].name !== "README.md" ||
!entries[1]?.isDirectory() ||
entries[1].name !== "pcre2"
) {
throw new Error(
"Regex Tools 0.1.0 must not ship an undeclared external engine pack.",
"The production build must contain only README.md and the declared PCRE2 pack.",
);
}
const notice = await readFile(path.join(engineDirectory, "README.md"), "utf8");
if (
!notice.includes("native `RegExp`") ||
!notice.includes("no external engine")
) {
if (!notice.includes("native `RegExp`") || !notice.includes("PCRE2 10.47")) {
throw new Error("The engine asset notice does not describe this release.");
}
await verifyPcre2Pack(path.join(engineDirectory, "pcre2"), root);
console.log("Verified ECMAScript-only engine assets (no WebAssembly pack).");
console.log("Verified native ECMAScript and bundled PCRE2 engine assets.");