diff --git a/package-lock.json b/package-lock.json index 34319d21f..3a9d15eaa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "wp-codebox-workspace", - "version": "0.12.29", + "version": "0.13.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "wp-codebox-workspace", - "version": "0.12.29", + "version": "0.13.0", "hasInstallScript": true, "license": "ISC", "workspaces": [ @@ -5236,7 +5236,7 @@ }, "packages/cli": { "name": "@automattic/wp-codebox-cli", - "version": "0.12.29", + "version": "0.13.0", "dependencies": { "@automattic/wp-codebox-core": "file:../runtime-core", "@automattic/wp-codebox-playground": "file:../runtime-playground" @@ -5258,14 +5258,14 @@ }, "packages/runtime-core": { "name": "@automattic/wp-codebox-core", - "version": "0.12.29", + "version": "0.13.0", "dependencies": { "ajv": "^8.20.0" } }, "packages/runtime-playground": { "name": "@automattic/wp-codebox-playground", - "version": "0.12.29", + "version": "0.13.0", "dependencies": { "@automattic/wp-codebox-core": "file:../runtime-core", "@php-wasm/node": "3.1.46", diff --git a/package.json b/package.json index 5544f5241..c4e75597a 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,7 @@ "packages/runtime-playground/package.json", "packages/cli/dist", "packages/cli/package.json", + "scripts/apply-development-patches.mjs", "README.md", "LICENSE" ], @@ -88,7 +89,7 @@ "cloudflare:dry-run:d1": "npm exec -- wrangler deploy --dry-run --config packages/runtime-cloudflare/wrangler.d1.jsonc", "cloudflare:local-gate": "node scripts/cloudflare-local-gate.mjs", "cloudflare:local-gate:d1": "node scripts/cloudflare-local-gate.mjs --coordinator=d1", - "postinstall": "patch-package", + "postinstall": "node scripts/apply-development-patches.mjs", "generate:cloudflare-wordpress-runtime-corpus": "tsx scripts/generate-cloudflare-wordpress-runtime-corpus.ts", "provision:cloudflare-wordpress-runtime-corpus": "node scripts/provision-cloudflare-wordpress-runtime-corpus.mjs", "prepare": "npm run build", diff --git a/scripts/apply-development-patches.mjs b/scripts/apply-development-patches.mjs new file mode 100644 index 000000000..bc89c7435 --- /dev/null +++ b/scripts/apply-development-patches.mjs @@ -0,0 +1,11 @@ +import { execFileSync } from "node:child_process" +import { existsSync } from "node:fs" +import { resolve } from "node:path" + +const patchPackageBin = resolve(import.meta.dirname, "..", "node_modules", "patch-package", "index.js") + +if (existsSync(patchPackageBin)) { + execFileSync(process.execPath, [patchPackageBin], { stdio: "inherit" }) +} else { + process.stdout.write("Skipping development patches in the production package.\n") +} diff --git a/scripts/package-release-artifact.ts b/scripts/package-release-artifact.ts index e092a2c04..2cb7c50e0 100644 --- a/scripts/package-release-artifact.ts +++ b/scripts/package-release-artifact.ts @@ -41,6 +41,7 @@ try { cwd: packageRoot, maxBuffer: 1024 * 1024 * 20, }) + await materializeWorkspacePackages(packageRoot) await bundleNodeRuntime(packageRoot, platformName, archName) const binDir = join(packageRoot, "bin") @@ -51,7 +52,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)" NODE_BIN="\${WP_CODEBOX_NODE_BIN:-}" if [ -z "\${NODE_BIN}" ]; then - if [ -x "\${SCRIPT_DIR}/../vendor/node/bin/node" ]; then + if [ -x "\${SCRIPT_DIR}/../vendor/node/bin/node" ] && "\${SCRIPT_DIR}/../vendor/node/bin/node" --version >/dev/null 2>&1; then NODE_BIN="\${SCRIPT_DIR}/../vendor/node/bin/node" elif command -v node >/dev/null 2>&1; then NODE_BIN="$(command -v node)" @@ -101,6 +102,22 @@ async function copyIfPresent(relativePath: string): Promise { } } +async function materializeWorkspacePackages(root: string): Promise { + const automatticModules = join(root, "node_modules", "@automattic") + await mkdir(automatticModules, { recursive: true }) + + const packages = new Map([ + ["runtime-core", "wp-codebox-core"], + ["runtime-playground", "wp-codebox-playground"], + ["cli", "wp-codebox-cli"], + ]) + for (const [sourceName, packageName] of packages) { + const target = join(automatticModules, packageName) + await rm(target, recursiveRmOptions) + await cp(join(root, "packages", sourceName), target, { recursive: true, dereference: true }) + } +} + async function bundleNodeRuntime(root: string, platformName: string, archName: string): Promise { const nodePackageName = nodeRuntimePackageName(platformName, archName) const runtimeRoot = join(root, "vendor", "node") diff --git a/tests/release-package-coverage.test.ts b/tests/release-package-coverage.test.ts index 3cf6cb2de..543804e70 100644 --- a/tests/release-package-coverage.test.ts +++ b/tests/release-package-coverage.test.ts @@ -1,7 +1,8 @@ import assert from "node:assert/strict" import { execFile } from "node:child_process" -import { readFile } from "node:fs/promises" -import { resolve } from "node:path" +import { cp, lstat, mkdir, mkdtemp, readFile, rm } from "node:fs/promises" +import { tmpdir } from "node:os" +import { join, resolve } from "node:path" import { promisify } from "node:util" const execFileAsync = promisify(execFile) @@ -19,6 +20,11 @@ assert.deepEqual(homeboy.release?.package_coverage, [{ const { stdout } = await execFileAsync("npm", ["run", "release:package"], { cwd: repositoryRoot, + env: { + ...process.env, + WP_CODEBOX_RELEASE_PLATFORM: "linux", + WP_CODEBOX_RELEASE_ARCH: "x64", + }, maxBuffer: 1024 * 1024 * 20, }) const artifacts = JSON.parse(stdout.trim().split("\n").at(-1) ?? "[]") @@ -59,4 +65,77 @@ const { stdout: tarEntries } = await execFileAsync("tar", ["-tzf", cliArtifact.p }) assert.ok(tarEntries.split("\n").some((path) => path === "wp-codebox-cli/"), "CLI tarball root changed") +const extractionRoot = await mkdtemp(join(tmpdir(), "wp-codebox-release-coverage-")) +try { + const pluginExtraction = join(extractionRoot, "plugin") + const cliExtraction = join(extractionRoot, "cli") + await mkdir(pluginExtraction, { recursive: true }) + await mkdir(cliExtraction, { recursive: true }) + await execFileAsync("unzip", ["-q", resolve(repositoryRoot, pluginArtifact), "-d", pluginExtraction]) + await execFileAsync("tar", ["-xzf", resolve(repositoryRoot, cliArtifact.path), "-C", cliExtraction]) + + const pluginCliRoot = join(pluginExtraction, "wp-codebox", "vendor", "wp-codebox-cli") + const tarCliRoot = join(cliExtraction, "wp-codebox-cli") + for (const root of [pluginCliRoot, tarCliRoot]) { + for (const packageName of ["wp-codebox-cli", "wp-codebox-core", "wp-codebox-playground"]) { + const packagePath = join(root, "node_modules", "@automattic", packageName) + const packageStat = await lstat(packagePath) + assert.equal(packageStat.isDirectory(), true, `${packagePath} must be a materialized directory`) + assert.equal(packageStat.isSymbolicLink(), false, `${packagePath} must not depend on archive symlinks`) + await lstat(join(packagePath, "package.json")) + } + + const cliEntrypoint = join(root, "packages", "cli", "dist", "index.js") + const { stdout: version } = await execFileAsync(process.execPath, [cliEntrypoint, "--version"]) + assert.match(version, /^\d+\.\d+\.\d+\s*$/) + await execFileAsync(process.execPath, [cliEntrypoint, "commands"]) + + const wrapper = join(root, "bin", "wp-codebox") + const { stdout: wrapperVersion } = await execFileAsync(wrapper, ["--version"], { + env: { ...process.env, WP_CODEBOX_NODE_BIN: "" }, + }) + assert.equal(wrapperVersion, version, "wrapper must fall back to host Node when bundled Node is incompatible") + } +} finally { + await rm(extractionRoot, { recursive: true, force: true }) +} + +const rootPackage = JSON.parse(await readFile(resolve(repositoryRoot, "package.json"), "utf8")) +assert.equal(rootPackage.scripts.postinstall, "node scripts/apply-development-patches.mjs") +assert.ok(rootPackage.files.includes("scripts/apply-development-patches.mjs")) + +const lifecycleRoot = await mkdtemp(join(tmpdir(), "wp-codebox-production-lifecycle-")) +try { + const lifecycleScript = resolve(repositoryRoot, "scripts", "apply-development-patches.mjs") + const packedLifecycleScript = join(lifecycleRoot, "scripts", "apply-development-patches.mjs") + await mkdir(join(lifecycleRoot, "scripts"), { recursive: true }) + await cp(lifecycleScript, packedLifecycleScript) + const { stdout: lifecycleOutput } = await execFileAsync(process.execPath, [packedLifecycleScript], { cwd: lifecycleRoot }) + assert.equal(lifecycleOutput, "Skipping development patches in the production package.\n") +} finally { + await rm(lifecycleRoot, { recursive: true, force: true }) +} + +const consumerRoot = await mkdtemp(join(tmpdir(), "wp-codebox-consumer-install-")) +try { + const packRoot = join(consumerRoot, "pack") + const installRoot = join(consumerRoot, "install") + await mkdir(packRoot, { recursive: true }) + const { stdout: packOutput } = await execFileAsync("npm", ["pack", "--ignore-scripts", "--json", "--pack-destination", packRoot], { + cwd: repositoryRoot, + maxBuffer: 1024 * 1024 * 20, + }) + const [packed] = JSON.parse(packOutput) as Array<{ filename: string }> + await execFileAsync("npm", ["install", "--global", "--prefix", installRoot, join(packRoot, packed.filename), "--omit=dev", "--no-audit", "--no-fund"], { + cwd: consumerRoot, + maxBuffer: 1024 * 1024 * 20, + }) + const installedCli = join(installRoot, "bin", "wp-codebox") + const { stdout: installedVersion } = await execFileAsync(installedCli, ["--version"]) + assert.match(installedVersion, /^\d+\.\d+\.\d+\s*$/) + await execFileAsync(installedCli, ["commands"]) +} finally { + await rm(consumerRoot, { recursive: true, force: true }) +} + console.log("release package coverage passed")