feat: publish Regex Tools 0.2.0
This commit is contained in:
@@ -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.",
|
||||
);
|
||||
|
||||
@@ -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}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
16
scripts/install-pcre2-engine.mjs
Normal file
16
scripts/install-pcre2-engine.mjs
Normal 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}.`,
|
||||
);
|
||||
@@ -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",
|
||||
]) {
|
||||
|
||||
110
scripts/pcre2-codegen-golden.test.ts
Normal file
110
scripts/pcre2-codegen-golden.test.ts
Normal 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 });
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
1018
scripts/pcre2-engine-pack.mjs
Normal file
1018
scripts/pcre2-engine-pack.mjs
Normal file
File diff suppressed because it is too large
Load Diff
89
scripts/pcre2-engine-pack.test.ts
Normal file
89
scripts/pcre2-engine-pack.test.ts
Normal 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",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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"],
|
||||
|
||||
@@ -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.");
|
||||
|
||||
Reference in New Issue
Block a user