Files
regex-tools/scripts/package-release.mjs

699 lines
23 KiB
JavaScript

import {
lstat,
mkdir,
mkdtemp,
readFile,
readdir,
rename,
rm,
writeFile,
} from "node:fs/promises";
import path from "node:path";
import { createRequire } from "node:module";
import { fileURLToPath, pathToFileURL } from "node:url";
import { zipSync } from "fflate";
import { checksumLine, sha256 } from "./checksum-release.mjs";
import { assertNoUnexpectedLocalPaths } from "./release-path-hygiene.mjs";
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.4.1";
const requiredRootFiles = [
"LICENSE",
"README.md",
"CHANGELOG.md",
"SOURCE.md",
"THIRD_PARTY_NOTICES.md",
];
function compareText(left, right) {
return left < right ? -1 : left > right ? 1 : 0;
}
export function safeArchivePath(fileName) {
if (
typeof fileName !== "string" ||
fileName.length === 0 ||
fileName.includes("\0") ||
fileName.includes("\\") ||
fileName.startsWith("/") ||
/^[A-Za-z]:\//u.test(fileName)
) {
throw new Error(`Invalid release path: ${String(fileName)}`);
}
const parts = fileName.split("/");
if (
parts.some(
(component) =>
component === "" || component === "." || component === "..",
) ||
path.posix.normalize(fileName) !== fileName
) {
throw new Error(`Non-canonical release path: ${fileName}`);
}
return fileName;
}
async function collectDirectory(directory, prefix = "") {
const result = [];
const visit = async (current) => {
const children = await readdir(current, { withFileTypes: true });
children.sort((left, right) => compareText(left.name, right.name));
for (const child of children) {
const absolute = path.join(current, child.name);
const details = await lstat(absolute);
if (details.isSymbolicLink()) {
throw new Error(`Release input contains a symlink: ${absolute}`);
}
if (details.isDirectory()) {
await visit(absolute);
} else if (details.isFile()) {
const relative = path
.relative(directory, absolute)
.split(path.sep)
.join("/");
result.push({
name: safeArchivePath(
prefix ? path.posix.join(prefix, relative) : relative,
),
data: await readFile(absolute),
});
} else {
throw new Error(`Release input contains a special file: ${absolute}`);
}
}
};
await visit(directory);
return result;
}
function safePackageDirectory(name, version) {
const normalized = `${name.replace(/^@/u, "").replaceAll("/", "__")}-${
version
}`;
if (!/^[A-Za-z0-9._@+-]+$/u.test(normalized)) {
throw new Error(`Unsafe package identity: ${name}@${version}`);
}
return normalized;
}
async function collectRuntimePackages(packageJson) {
const packages = new Map();
const visit = async (name, fromDirectory) => {
const resolver = createRequire(path.join(fromDirectory, "package.json"));
let packagePath;
try {
packagePath = resolver.resolve(`${name}/package.json`);
} catch {
const entry = resolver.resolve(name);
let directory = path.dirname(entry);
while (directory !== path.dirname(directory)) {
const candidate = path.join(directory, "package.json");
const details = await lstat(candidate).catch(() => null);
if (details?.isFile()) {
const candidateJson = JSON.parse(await readFile(candidate, "utf8"));
if (candidateJson.name === name) {
packagePath = candidate;
break;
}
}
directory = path.dirname(directory);
}
}
if (!packagePath) {
throw new Error(`Could not resolve runtime package ${name}.`);
}
const details = JSON.parse(await readFile(packagePath, "utf8"));
const key = `${details.name}@${details.version}`;
if (packages.has(key)) return;
const directory = path.dirname(packagePath);
packages.set(key, { directory, details });
for (const dependency of Object.keys(details.dependencies ?? {}).sort()) {
await visit(dependency, directory);
}
};
for (const dependency of Object.keys(packageJson.dependencies ?? {}).sort()) {
await visit(dependency, root);
}
return [...packages.values()].sort((left, right) =>
compareText(
`${left.details.name}@${left.details.version}`,
`${right.details.name}@${right.details.version}`,
),
);
}
async function collectRuntimeLicenceEntries(packageJson) {
const packages = await collectRuntimePackages(packageJson);
const entries = [];
const rows = [
"# Runtime npm dependency licences",
"",
"| Package | Version | Licence | Repository |",
"| --- | --- | --- | --- |",
];
for (const runtimePackage of packages) {
const { details, directory } = runtimePackage;
const legalFiles = (await readdir(directory, { withFileTypes: true }))
.filter(
(entry) =>
entry.isFile() &&
/^(?:licen[cs]e|copying|notice)(?:\.|$)/iu.test(entry.name),
)
.sort((left, right) => compareText(left.name, right.name));
if (legalFiles.length === 0) {
throw new Error(
`Runtime package ${details.name}@${details.version} has no licence file.`,
);
}
const packageDirectory = safePackageDirectory(
details.name,
details.version,
);
for (const legalFile of legalFiles) {
entries.push({
name: safeArchivePath(
`LICENSES/npm/${packageDirectory}/${legalFile.name}`,
),
data: await readFile(path.join(directory, legalFile.name)),
});
}
const repository =
typeof details.repository === "string"
? details.repository
: (details.repository?.url ?? "");
rows.push(
`| \`${details.name}\` | ${details.version} | ${
details.license ?? "See packaged licence"
} | ${String(repository).replaceAll("|", "\\|")} |`,
);
}
entries.push({
name: "LICENSES/npm/DEPENDENCIES.md",
data: Buffer.from(`${rows.join("\n")}\n`),
});
return entries;
}
async function collectBuildGeneratedLicenceEntries() {
const resolver = createRequire(path.join(root, "package.json"));
const dependencies = [
{
name: "rolldown",
version: "1.1.5",
license: "MIT",
legalFile: "LICENSE",
},
{
name: "vite",
version: "8.1.5",
license: "MIT",
legalFile: "LICENSE.md",
},
];
const entries = [];
for (const dependency of dependencies) {
const packagePath = resolver.resolve(`${dependency.name}/package.json`);
const details = JSON.parse(await readFile(packagePath, "utf8"));
if (
details.name !== dependency.name ||
details.version !== dependency.version ||
details.license !== dependency.license
) {
throw new Error(
`Build dependency identity drifted: expected ${dependency.name}@${dependency.version} (${dependency.license}).`,
);
}
const legalPath = path.join(
path.dirname(packagePath),
dependency.legalFile,
);
const legalDetails = await lstat(legalPath);
if (legalDetails.isSymbolicLink() || !legalDetails.isFile()) {
throw new Error(
`Build dependency legal file is not a regular file: ${legalPath}.`,
);
}
entries.push({
name: safeArchivePath(
`LICENSES/build/${safePackageDirectory(
details.name,
details.version,
)}/${dependency.legalFile}`,
),
data: await readFile(legalPath),
});
}
return entries;
}
function scanEntry(entry) {
if (entry.name.endsWith(".map")) {
throw new Error(`Source map is not approved for release: ${entry.name}`);
}
const components = entry.name.toLowerCase().split("/");
if (
components.some((component) =>
[
".env",
".npmrc",
".git",
"credentials",
"id_rsa",
"id_ed25519",
].includes(component),
) ||
/\.(?:key|pem|p12|pfx)$/iu.test(entry.name)
) {
throw new Error(`Credential-like release path: ${entry.name}`);
}
const knownVirtualHomes =
/^(?:assets\/php\.worker-[A-Za-z0-9_-]+\.js|engines\/cpp\/cpp-regex\.mjs|engines\/dotnet\/_framework\/dotnet\.native\.[a-z0-9]+\.js|engines\/perl\/emperl\.js|engines\/php\/engine-metadata\.json|engines\/php\/php-loader\.mjs|engines\/python\/pyodide(?:\.asm)?\.mjs)$/u.test(
entry.name,
)
? ["/home/web_user", "/home/pyodide"]
: [];
assertNoUnexpectedLocalPaths(entry.data, entry.name, {
allowedVirtualHomes: knownVirtualHomes,
});
if (!/\.(?:css|html|js|json|md|mjs|pl|rst|svg|txt)$/iu.test(entry.name)) {
return;
}
const text = entry.data.toString("utf8");
if (/-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/u.test(text)) {
throw new Error(`Private key material detected in ${entry.name}`);
}
if (/https?:\/\/[^/@\s]+:[^/@\s]+@/iu.test(text)) {
throw new Error(`Credential-bearing URL detected in ${entry.name}`);
}
}
export function createDeterministicZip(inputEntries) {
const entries = inputEntries
.map((entry) => ({
name: safeArchivePath(entry.name),
data: Buffer.from(entry.data),
}))
.sort((left, right) => compareText(left.name, right.name));
const seen = new Set();
const zippable = Object.create(null);
let totalBytes = 0;
for (const entry of entries) {
if (seen.has(entry.name)) {
throw new Error(`Duplicate release path: ${entry.name}`);
}
seen.add(entry.name);
totalBytes += entry.data.byteLength;
if (entry.data.byteLength > 128 * 1024 * 1024) {
throw new Error(`Release entry is too large: ${entry.name}`);
}
if (totalBytes > 512 * 1024 * 1024) {
throw new Error("Release exceeds the 512 MiB uncompressed limit.");
}
scanEntry(entry);
zippable[entry.name] = [
new Uint8Array(entry.data),
{
attrs: (0o100644 << 16) >>> 0,
level: 9,
mtime: zipEpoch,
os: 3,
},
];
}
return Buffer.from(
zipSync(zippable, {
attrs: (0o100644 << 16) >>> 0,
level: 9,
mtime: zipEpoch,
os: 3,
}),
);
}
async function collectReleaseEntries() {
const packageJson = JSON.parse(
await readFile(path.join(root, "package.json"), "utf8"),
);
if (
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") ||
!licence.includes("Version 3, 29 June 2007") ||
!licence.includes("END OF TERMS AND CONDITIONS") ||
sha256(Buffer.from(licence)) !== gplVersion3TextSha256
) {
throw new Error(
"Root LICENSE is not the exact complete GPL version 3 text.",
);
}
const entries = await collectDirectory(path.join(root, "dist"));
entries.push(
...(await collectDirectory(path.join(root, "LICENSES"), "LICENSES")),
);
entries.push(...(await collectRuntimeLicenceEntries(packageJson)));
entries.push(...(await collectBuildGeneratedLicenceEntries()));
for (const fileName of requiredRootFiles) {
entries.push({
name: fileName,
data: await readFile(path.join(root, fileName)),
});
}
const names = new Set(entries.map((entry) => entry.name));
for (const required of [
"index.html",
"toolbox-app.json",
"favicon.svg",
"LICENSE",
"CHANGELOG.md",
"SOURCE.md",
"THIRD_PARTY_NOTICES.md",
"engines/README.md",
"engines/java/LICENSE.txt",
"engines/java/NOTICE.txt",
"engines/java/SHA256SUMS",
"engines/java/engine-metadata.json",
"engines/java/java-regex.mjs",
"engines/cpp/LICENSE-Emscripten.txt",
"engines/cpp/LICENSE-compiler-rt.txt",
"engines/cpp/LICENSE-libcxx.txt",
"engines/cpp/LICENSE-libcxxabi.txt",
"engines/cpp/LICENSE-libunwind.txt",
"engines/cpp/LICENSE-musl.txt",
"engines/cpp/SHA256SUMS",
"engines/cpp/SOURCE-MANIFEST.json",
"engines/cpp/cpp-regex.mjs",
"engines/cpp/cpp-regex.wasm",
"engines/cpp/engine-metadata.json",
"engines/dotnet/LICENSE-dotnet.txt",
"engines/dotnet/SHA256SUMS",
"engines/dotnet/THIRD-PARTY-NOTICES.txt",
"engines/dotnet/_framework/dotnet.js",
"engines/dotnet/engine-metadata.json",
"engines/go/LICENSE-Go.txt",
"engines/go/SHA256SUMS",
"engines/go/engine-metadata.json",
"engines/go/go-regex.wasm",
"engines/go/wasm_exec.mjs",
"engines/perl/LICENSE-WebPerl-Artistic.txt",
"engines/perl/LICENSE-WebPerl-GPL.txt",
"engines/perl/LICENSE-Devel-StackTrace-Artistic-2.0.txt",
"engines/perl/LICENSE-Emscripten.txt",
"engines/perl/NOTICE.txt",
"engines/perl/SHA256SUMS",
"engines/perl/emperl.data",
"engines/perl/emperl.js",
"engines/perl/emperl.wasm",
"engines/perl/engine-metadata.json",
"engines/perl/perl-runtime.worker.js",
"engines/perl/regex_tools_bridge.pl",
"engines/php/LICENSE.Emscripten-4.0.19.txt",
"engines/php/LICENSE.Oniguruma-6.9.10.txt",
"engines/php/LICENSE.OpenSSL-1.1.1t.txt",
"engines/php/LICENSE.PCRE2-10.44.txt",
"engines/php/LICENSE.PHP-4.0.txt",
"engines/php/LICENSE.PHP-CLI-http-parser-MIT.txt",
"engines/php/LICENSE.PHP-Lexbor-Apache-2.0.txt",
"engines/php/LICENSE.PHP-Zend-2.0.txt",
"engines/php/LICENSE.PHP-bcmath-LGPL-2.1.txt",
"engines/php/LICENSE.PHP-libavifinfo-BSD-2-Clause.txt",
"engines/php/LICENSE.PHP-libmagic-BSD.txt",
"engines/php/LICENSE.PHP-libmbfl-LGPL-2.1.txt",
"engines/php/LICENSE.PHP-timelib-MIT.txt",
"engines/php/LICENSE.PHP-uriparser-BSD-3-Clause.txt",
"engines/php/LICENSE.curl-7.69.1.txt",
"engines/php/LICENSE.ini-ISC.txt",
"engines/php/LICENSE.libaom-3.12.1.txt",
"engines/php/LICENSE.libavif-1.3.0.txt",
"engines/php/LICENSE.libcxx.txt",
"engines/php/LICENSE.libcxxabi.txt",
"engines/php/LICENSE.libgd-2.3.3.txt",
"engines/php/LICENSE.libiconv-1.17-LGPL-2.1.txt",
"engines/php/LICENSE.libjpeg-turbo-3.0.3.txt",
"engines/php/LICENSE.libpng-1.6.39.txt",
"engines/php/LICENSE.libwebp.txt",
"engines/php/LICENSE.libxml2-2.9.10.txt",
"engines/php/LICENSE.libyuv.txt",
"engines/php/LICENSE.libzip-1.9.2.txt",
"engines/php/LICENSE.llvm-compiler-rt.txt",
"engines/php/LICENSE.musl.txt",
"engines/php/LICENSE.php-wasm-GPL-2.0-or-later.txt",
"engines/php/LICENSE.wasm-feature-detect-Apache-2.0.txt",
"engines/php/LICENSE.zlib-1.2.13.txt",
"engines/php/NOTICE.IANA-TZDATA-2026.1.txt",
"engines/php/NOTICE.PHP-Lexbor.txt",
"engines/php/NOTICE.PHP-REDIST-BINS.txt",
"engines/php/NOTICE.PHP-public-domain-hash-code.txt",
"engines/php/NOTICE.SQLite-3.51.0.txt",
"engines/php/NOTICE.dlmalloc-2.8.6.txt",
"engines/php/PATENTS.PHP-libavifinfo.txt",
"engines/php/PATENTS.libaom-3.12.1.txt",
"engines/php/PATENTS.libwebp.txt",
"engines/php/SHA256SUMS",
"engines/php/engine-metadata.json",
"engines/php/native-components.json",
"engines/php/php-loader.mjs",
"engines/php/php.wasm",
"engines/pcre2/LICENSE.txt",
"engines/pcre2/SHA256SUMS",
"engines/pcre2/engine-metadata.json",
"engines/pcre2/pcre2.mjs",
"engines/pcre2/pcre2.wasm",
"engines/python/LICENSE.cpython.txt",
"engines/python/LICENSE.bzip2.txt",
"engines/python/LICENSE.emscripten-mini-lz4.txt",
"engines/python/LICENSE.emscripten.txt",
"engines/python/LICENSE.expat.txt",
"engines/python/LICENSE.hacl-mit.txt",
"engines/python/LICENSE.hiwire.txt",
"engines/python/LICENSE.libffi.txt",
"engines/python/LICENSE.libmpdec.txt",
"engines/python/LICENSE.llvm-compiler-rt.txt",
"engines/python/LICENSE.llvm-libcxx.txt",
"engines/python/LICENSE.llvm-libcxxabi.txt",
"engines/python/LICENSE.llvm-libunwind.txt",
"engines/python/LICENSE.pyodide.txt",
"engines/python/LICENSE.pyodide-stacktrace-vendors.txt",
"engines/python/LICENSE.sqlite.txt",
"engines/python/LICENSE.xz.txt",
"engines/python/LICENSE.zlib.txt",
"engines/python/LICENSE.zstd.txt",
"engines/python/NOTICE.cpython-bundled.rst",
"engines/python/NOTICE.emscripten-dlmalloc.txt",
"engines/python/NOTICE.emscripten-musl.txt",
"engines/python/SHA256SUMS",
"engines/python/SOURCE-MANIFEST.json",
"engines/python/engine-metadata.json",
"engines/python/pyodide-lock.json",
"engines/python/pyodide.asm.mjs",
"engines/python/pyodide.asm.wasm",
"engines/python/pyodide.mjs",
"engines/python/python_stdlib.zip",
"engines/ruby/LICENSE.browser-wasi-shim-Apache-2.0.txt",
"engines/ruby/LICENSE.browser-wasi-shim-MIT.txt",
"engines/ruby/LICENSE.ruby-wasm.txt",
"engines/ruby/LICENSE.tslib.txt",
"engines/ruby/NOTICE.txt",
"engines/ruby/SHA256SUMS",
"engines/ruby/engine-metadata.json",
"engines/ruby/ruby.wasm",
"engines/rust/LICENSE-Apache-2.0.txt",
"engines/rust/LICENSE-MIT.txt",
"engines/rust/LICENSE-Unicode-3.0.txt",
"engines/rust/RUST-STDLIB-COPYRIGHT.html",
"engines/rust/SHA256SUMS",
"engines/rust/THIRD_PARTY_LICENSES.txt",
"engines/rust/engine-metadata.json",
"engines/rust/rust-regex.mjs",
"engines/rust/rust-regex_bg.wasm",
"LICENSES/Emscripten-MIT.txt",
"LICENSES/PCRE2.txt",
"LICENSES/build/rolldown-1.1.5/LICENSE",
"LICENSES/build/vite-8.1.5/LICENSE.md",
]) {
if (!names.has(required)) {
throw new Error(`Required release file is missing: ${required}`);
}
}
if (![...names].some((name) => name.startsWith("assets/"))) {
throw new Error("Release has no production assets.");
}
for (const requiredDotNetPattern of [
/^engines\/dotnet\/_framework\/RegexTools\.DotNet\..+\.wasm$/u,
/^engines\/dotnet\/_framework\/System\.Text\.RegularExpressions\..+\.wasm$/u,
/^engines\/dotnet\/_framework\/dotnet\.native\..+\.wasm$/u,
/^engines\/dotnet\/_framework\/dotnet\.runtime\..+\.js$/u,
]) {
if (![...names].some((name) => requiredDotNetPattern.test(name))) {
throw new Error(
`Required .NET runtime asset is missing: ${requiredDotNetPattern}`,
);
}
}
if (![...names].some((name) => name.startsWith("LICENSES/"))) {
throw new Error("Release has no third-party licence directory.");
}
const manifest = JSON.parse(
entries
.find((entry) => entry.name === "toolbox-app.json")
.data.toString("utf8"),
);
if (
manifest.id !== "de.add-ideas.regex-tools" ||
manifest.version !== packageJson.version ||
manifest.source?.repository !==
"https://git.add-ideas.de/zemion/regex-tools" ||
manifest.source?.license !== "GPL-3.0-or-later"
) {
throw new Error("Packaged manifest identity is inconsistent.");
}
const indexHtml = entries
.find((entry) => entry.name === "index.html")
.data.toString("utf8");
if (/(?:src|href)\s*=\s*["']\/(?!\/)/iu.test(indexHtml)) {
throw new Error("index.html contains a root-absolute asset path.");
}
return { entries, packageJson };
}
export function parseReleaseArguments(argumentsList) {
const allowed = new Set(["--force"]);
const unknown = argumentsList.filter((argument) => !allowed.has(argument));
if (unknown.length > 0) {
throw new Error(`Unknown release argument: ${unknown.join(", ")}`);
}
return { force: argumentsList.includes("--force") };
}
async function assertWritableTarget(target, force) {
try {
const details = await lstat(target);
if (details.isSymbolicLink() || !details.isFile()) {
throw new Error(`Release target is not a regular file: ${target}`);
}
if (!force) {
throw new Error(
`${path.relative(root, target)} exists; pass --force to replace this exact version.`,
);
}
return true;
} catch (error) {
if (error && typeof error === "object" && error.code === "ENOENT") {
return false;
}
throw error;
}
}
async function publishReleasePair({
archive,
archivePath,
checksum,
checksumPath,
archiveExists,
checksumExists,
}) {
const releaseDirectory = path.dirname(archivePath);
const stage = await mkdtemp(
path.join(releaseDirectory, ".regex-tools-release-"),
);
const stagedArchive = path.join(stage, path.basename(archivePath));
const stagedChecksum = path.join(stage, path.basename(checksumPath));
const previousArchive = path.join(stage, "previous-archive");
const previousChecksum = path.join(stage, "previous-checksum");
let archiveBackedUp = false;
let checksumBackedUp = false;
let archivePublished = false;
let checksumPublished = false;
try {
await writeFile(stagedArchive, archive, { flag: "wx", mode: 0o644 });
await writeFile(stagedChecksum, checksum, {
encoding: "utf8",
flag: "wx",
mode: 0o644,
});
if (archiveExists) {
await rename(archivePath, previousArchive);
archiveBackedUp = true;
}
if (checksumExists) {
await rename(checksumPath, previousChecksum);
checksumBackedUp = true;
}
await rename(stagedArchive, archivePath);
archivePublished = true;
await rename(stagedChecksum, checksumPath);
checksumPublished = true;
} catch (error) {
if (checksumPublished) await rm(checksumPath, { force: true });
if (archivePublished) await rm(archivePath, { force: true });
if (checksumBackedUp) await rename(previousChecksum, checksumPath);
if (archiveBackedUp) await rename(previousArchive, archivePath);
throw error;
} finally {
await rm(stage, { recursive: true, force: true });
}
}
async function main() {
const options = parseReleaseArguments(process.argv.slice(2));
const { entries, packageJson } = await collectReleaseEntries();
const archive = createDeterministicZip(entries);
const fileName = `regex-tools-${packageJson.version}.zip`;
const digest = sha256(archive);
const releaseDirectory = path.join(root, "release");
await mkdir(releaseDirectory, { recursive: true });
const releaseDirectoryDetails = await lstat(releaseDirectory);
if (
releaseDirectoryDetails.isSymbolicLink() ||
!releaseDirectoryDetails.isDirectory()
) {
throw new Error("release/ must be a real directory.");
}
const archivePath = path.join(releaseDirectory, fileName);
const checksumPath = `${archivePath}.sha256`;
const archiveExists = await assertWritableTarget(archivePath, options.force);
const checksumExists = await assertWritableTarget(
checksumPath,
options.force,
);
await publishReleasePair({
archive,
archivePath,
checksum: checksumLine(digest, fileName),
checksumPath,
archiveExists,
checksumExists,
});
console.log(
`Created release/${fileName} (${archive.byteLength} bytes, SHA-256 ${digest})`,
);
}
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
await main();
}