226 lines
9.3 KiB
JavaScript
226 lines
9.3 KiB
JavaScript
import console from "node:console";
|
|
import {
|
|
existsSync,
|
|
mkdirSync,
|
|
mkdtempSync,
|
|
readFileSync,
|
|
readdirSync,
|
|
rmSync,
|
|
writeFileSync,
|
|
} from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { dirname, join, resolve } from "node:path";
|
|
import { spawnSync } from "node:child_process";
|
|
import process from "node:process";
|
|
|
|
const root = resolve(import.meta.dirname, "..");
|
|
const stageRoot = join(root, ".publish");
|
|
const bun = process.env.WRNEXUS_BUN_BINARY || "bun";
|
|
const npmCli = join(dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js");
|
|
if (!existsSync(npmCli)) throw new Error(`Unable to locate the npm CLI at ${npmCli}.`);
|
|
const integrityPath = join(stageRoot, "PACKAGE-INTEGRITY.json");
|
|
if (!existsSync(integrityPath)) {
|
|
throw new Error("No staged packages found. Run the package staging command first.");
|
|
}
|
|
const integrity = JSON.parse(readFileSync(integrityPath, "utf8"));
|
|
const expectedPackageNames = new Set(
|
|
(Array.isArray(integrity.packages) ? integrity.packages : []).map((entry) => entry.name),
|
|
);
|
|
|
|
const packages = readdirSync(stageRoot, { withFileTypes: true })
|
|
.filter((entry) => entry.isDirectory() && existsSync(join(stageRoot, entry.name, "package.json")))
|
|
.map((entry) => ({
|
|
directory: entry.name,
|
|
manifest: JSON.parse(readFileSync(join(stageRoot, entry.name, "package.json"), "utf8")),
|
|
}))
|
|
.sort((left, right) => left.manifest.name.localeCompare(right.manifest.name));
|
|
|
|
const stagedPackageNames = new Set(packages.map((pkg) => pkg.manifest.name));
|
|
const missingPackages = [...expectedPackageNames].filter((name) => !stagedPackageNames.has(name));
|
|
const unexpectedPackages = [...stagedPackageNames].filter(
|
|
(name) => !expectedPackageNames.has(name),
|
|
);
|
|
if (
|
|
expectedPackageNames.size === 0 ||
|
|
packages.length !== expectedPackageNames.size ||
|
|
missingPackages.length > 0 ||
|
|
unexpectedPackages.length > 0
|
|
) {
|
|
throw new Error(
|
|
`Staged package set does not match PACKAGE-INTEGRITY.json (expected ${expectedPackageNames.size}, received ${packages.length}, missing: ${missingPackages.join(", ") || "none"}, unexpected: ${unexpectedPackages.join(", ") || "none"}).`,
|
|
);
|
|
}
|
|
|
|
const consumer = mkdtempSync(join(tmpdir(), "wrnexus-staged-consumer-"));
|
|
try {
|
|
if (process.env.WRNEXUS_NPM_REGISTRY) {
|
|
writeFileSync(
|
|
join(consumer, ".npmrc"),
|
|
`@wrnexus:registry=${process.env.WRNEXUS_NPM_REGISTRY}\n`,
|
|
);
|
|
}
|
|
const tarballRoot = join(consumer, "tarballs");
|
|
mkdirSync(tarballRoot, { recursive: true });
|
|
const dependencies = {};
|
|
for (const pkg of packages) {
|
|
const packed = spawnSync(
|
|
process.execPath,
|
|
[npmCli, "pack", join(stageRoot, pkg.directory), "--pack-destination", tarballRoot, "--json"],
|
|
{ cwd: consumer, encoding: "utf8" },
|
|
);
|
|
if (packed.status !== 0) {
|
|
throw new Error(
|
|
`Unable to pack ${pkg.manifest.name}:\n${packed.error?.message || packed.stderr || packed.stdout || `exit ${packed.status}`}`,
|
|
);
|
|
}
|
|
const result = JSON.parse(packed.stdout);
|
|
const filename = result[0]?.filename;
|
|
if (typeof filename !== "string") {
|
|
throw new Error(`npm pack returned no tarball for ${pkg.manifest.name}.`);
|
|
}
|
|
dependencies[pkg.manifest.name] = `file:${join(tarballRoot, filename).replace(/\\/g, "/")}`;
|
|
}
|
|
if (stagedPackageNames.has("@wrnexus/viewer")) {
|
|
const viewer = JSON.parse(readFileSync(join(root, "packages/viewer/package.json"), "utf8"));
|
|
for (const name of ["fflate", "mammoth", "pdfjs-dist", "xlsx"]) {
|
|
dependencies[name] = viewer.devDependencies[name];
|
|
}
|
|
}
|
|
// The all-package probe imports the optional React adapter explicitly.
|
|
// Ordinary framework consumers still do not need React installed.
|
|
if (stagedPackageNames.has("@wrnexus/react")) {
|
|
const adapter = JSON.parse(readFileSync(join(root, "packages/react/package.json"), "utf8"));
|
|
dependencies.react = adapter.peerDependencies.react;
|
|
dependencies["react-dom"] = adapter.peerDependencies["react-dom"];
|
|
}
|
|
writeFileSync(
|
|
join(consumer, "package.json"),
|
|
JSON.stringify(
|
|
{ name: "wrnexus-staged-consumer", private: true, type: "module", dependencies },
|
|
null,
|
|
2,
|
|
) + "\n",
|
|
);
|
|
const install = spawnSync(
|
|
process.execPath,
|
|
[npmCli, "install", "--ignore-scripts", "--no-audit", "--no-fund"],
|
|
{
|
|
cwd: consumer,
|
|
encoding: "utf8",
|
|
env: { ...process.env, NO_COLOR: "1" },
|
|
},
|
|
);
|
|
if (install.status !== 0) {
|
|
throw new Error(
|
|
`Unable to install staged package tarballs:\n${install.stderr || install.stdout}`,
|
|
);
|
|
}
|
|
|
|
const importable = packages.filter((pkg) => pkg.manifest.name !== "@wrnexus/cli");
|
|
writeFileSync(
|
|
join(consumer, "probe.mjs"),
|
|
`${importable
|
|
.map(
|
|
(pkg) =>
|
|
`if (!await import(${JSON.stringify(pkg.manifest.name)})) throw new Error(${JSON.stringify(`Unable to import ${pkg.manifest.name}`)});`,
|
|
)
|
|
.join(
|
|
"\n",
|
|
)}\nconsole.log(${JSON.stringify(`Imported ${importable.length} staged package roots.`)});\n`,
|
|
);
|
|
const probe = spawnSync(bun, ["probe.mjs"], {
|
|
cwd: consumer,
|
|
encoding: "utf8",
|
|
env: { ...process.env, NO_COLOR: "1" },
|
|
});
|
|
if (probe.status !== 0)
|
|
throw new Error(`Staged package import probe failed:\n${probe.stderr || probe.stdout}`);
|
|
if (probe.stdout) process.stdout.write(probe.stdout);
|
|
|
|
if (stagedPackageNames.has("@wrnexus/ui")) {
|
|
writeFileSync(
|
|
join(consumer, "ui-runtime-probe.mjs"),
|
|
`import { existsSync, readFileSync } from "node:fs";
|
|
import { resolve } from "node:path";
|
|
const root = resolve("node_modules/@wrnexus/ui");
|
|
const manifest = JSON.parse(readFileSync(resolve(root, "package.json"), "utf8"));
|
|
for (const runtime of manifest.wrnexus?.clientRuntimes ?? []) {
|
|
const entry = resolve(root, runtime.entry);
|
|
if (!existsSync(entry) || entry.endsWith(".ts")) throw new Error("Missing compiled UI runtime: " + runtime.id);
|
|
if (runtime.bundle === false) continue;
|
|
const result = await Bun.build({ entrypoints: [entry], target: "browser" });
|
|
if (!result.success) throw new AggregateError(result.logs, "UI browser runtime failed: " + runtime.id);
|
|
}
|
|
if (!existsSync(resolve(root, "components/Chart.wrn"))) throw new Error("Missing Chart component");
|
|
console.log("Published UI component and browser runtimes passed.");
|
|
`,
|
|
);
|
|
const ui = spawnSync(bun, ["ui-runtime-probe.mjs"], { cwd: consumer, encoding: "utf8" });
|
|
if (ui.status !== 0)
|
|
throw new Error(`Staged UI runtime probe failed:\n${ui.stderr || ui.stdout}`);
|
|
if (ui.stdout) process.stdout.write(ui.stdout);
|
|
}
|
|
|
|
if (stagedPackageNames.has("@wrnexus/viewer")) {
|
|
writeFileSync(
|
|
join(consumer, "viewer-probe.mjs"),
|
|
`import { existsSync, readFileSync } from "node:fs";
|
|
import { viewerPlugin } from "@wrnexus/viewer/plugin";
|
|
const manifest = JSON.parse(readFileSync("node_modules/@wrnexus/viewer/package.json", "utf8"));
|
|
if (!manifest.peerDependenciesMeta?.xlsx?.optional || manifest.peerDependencies?.["@wrnexus/uploader"]?.startsWith("workspace:")) throw new Error("Viewer peer metadata was lost");
|
|
const plugin = viewerPlugin({ convert: { converters: [], resolveSource: async () => undefined }, uploads: {} });
|
|
for (const asset of [...plugin.clientRuntimes, ...plugin.routeEntries, ...plugin.styleSources]) {
|
|
if (!existsSync(asset.entry)) throw new Error("Missing published viewer asset: " + asset.entry);
|
|
}
|
|
for (const runtime of plugin.clientRuntimes.filter((runtime) => runtime.bundle !== false)) {
|
|
const result = await Bun.build({ entrypoints: [runtime.entry], target: "browser" });
|
|
if (!result.success) throw new AggregateError(result.logs, "Viewer browser runtime failed: " + runtime.id);
|
|
}
|
|
console.log("Published viewer routes, metadata, and browser runtimes passed.");
|
|
`,
|
|
);
|
|
const viewer = spawnSync(bun, ["viewer-probe.mjs"], {
|
|
cwd: consumer,
|
|
encoding: "utf8",
|
|
});
|
|
if (viewer.status !== 0)
|
|
throw new Error(`Staged viewer probe failed:\n${viewer.stderr || viewer.stdout}`);
|
|
if (viewer.stdout) process.stdout.write(viewer.stdout);
|
|
}
|
|
|
|
const browserPackages = ["@wrnexus/csr", "@wrnexus/reactive"].filter((name) =>
|
|
existsSync(join(consumer, "node_modules", ...name.split("/"))),
|
|
);
|
|
if (browserPackages.length > 0) {
|
|
writeFileSync(
|
|
join(consumer, "browser-probe.js"),
|
|
browserPackages.map((name) => `import ${JSON.stringify(name)};`).join("\n") + "\n",
|
|
);
|
|
const browser = spawnSync(
|
|
bun,
|
|
["build", "browser-probe.js", "--target=browser", "--outfile=browser-probe.bundle.js"],
|
|
{ cwd: consumer, encoding: "utf8", env: { ...process.env, NO_COLOR: "1" } },
|
|
);
|
|
if (browser.status !== 0 || !existsSync(join(consumer, "browser-probe.bundle.js"))) {
|
|
throw new Error(`Staged browser package build failed:\n${browser.stderr || browser.stdout}`);
|
|
}
|
|
}
|
|
|
|
const cliPath = join(consumer, "node_modules", "@wrnexus", "cli", "dist", "index.js");
|
|
if (existsSync(cliPath)) {
|
|
const cli = spawnSync(bun, [cliPath, "--help"], {
|
|
cwd: consumer,
|
|
encoding: "utf8",
|
|
env: { ...process.env, NO_COLOR: "1" },
|
|
});
|
|
if (cli.status !== 0 || !/wrnexus/i.test(`${cli.stdout}\n${cli.stderr}`)) {
|
|
throw new Error(`Staged CLI smoke test failed:\n${cli.stderr || cli.stdout}`);
|
|
}
|
|
}
|
|
console.log(
|
|
"Staged package tarballs, browser runtime bundle, and CLI passed their isolated consumer smoke tests.",
|
|
);
|
|
} finally {
|
|
rmSync(consumer, { recursive: true, force: true });
|
|
}
|