diff --git a/doc/PUBLISHING.md b/doc/PUBLISHING.md index 433860153f..26094fdfe0 100644 --- a/doc/PUBLISHING.md +++ b/doc/PUBLISHING.md @@ -41,7 +41,9 @@ This script: 3. bundles the CLI entrypoint with esbuild into `cli/dist/index.js` 4. verifies the bundled entrypoint with `node --check` 5. rewrites `cli/package.json` into a publishable npm manifest and stores the dev copy as `cli/package.dev.json` -6. copies the repo `README.md` into `cli/README.md` for npm metadata +6. copies the repo `README.md` into `cli/README.md` for npm metadata, rewriting + repository-relative image assets to raw GitHub URLs pinned to the source + commit After the release script exits, the dev manifest and temporary files are restored automatically. diff --git a/scripts/build-npm.sh b/scripts/build-npm.sh index 74d801fdfe..937a850212 100755 --- a/scripts/build-npm.sh +++ b/scripts/build-npm.sh @@ -64,8 +64,18 @@ echo " [5/6] Generating publishable package.json..." cp "$CLI_DIR/package.json" "$CLI_DIR/package.dev.json" node "$REPO_ROOT/scripts/generate-npm-package-json.mjs" -# Copy root README so npm shows the repo README on the package page -cp "$REPO_ROOT/README.md" "$CLI_DIR/README.md" +# Copy the root README so npm shows the repo README on the package page, but +# rewrite repository-relative image assets because npm resolves README links +# under the package's `repository.directory` (`cli`), not the repository root. +README_ASSET_REF="${PAPERCLIP_README_ASSET_REF:-}" +if [ -z "$README_ASSET_REF" ]; then + README_ASSET_REF="$(git -C "$REPO_ROOT" rev-parse HEAD 2>/dev/null || true)" +fi +README_ASSET_REF="${README_ASSET_REF:-master}" +node "$REPO_ROOT/scripts/prepare-npm-readme.mjs" \ + "$REPO_ROOT/README.md" \ + "$CLI_DIR/README.md" \ + "$README_ASSET_REF" # ── Step 6: Summary ─────────────────────────────────────────────────────────── BUNDLE_SIZE=$(wc -c < "$DIST_DIR/index.js" | xargs) diff --git a/scripts/e2e-install-lifecycle.sh b/scripts/e2e-install-lifecycle.sh index 653b4cbc9d..272b74dddd 100755 --- a/scripts/e2e-install-lifecycle.sh +++ b/scripts/e2e-install-lifecycle.sh @@ -68,7 +68,8 @@ if corepack pnpm install --frozen-lockfile > "$HOME/e2e-bootstrap-install.log" 2 else tail -40 "$HOME/e2e-bootstrap-install.log"; fail_ "1b bootstrap pnpm install"; exit 1 fi -if bash scripts/build-npm.sh --skip-checks --skip-typecheck > "$HOME/e2e-bootstrap-build.log" 2>&1; then +if PAPERCLIP_README_ASSET_REF="$E2E_REF" \ + bash scripts/build-npm.sh --skip-checks --skip-typecheck > "$HOME/e2e-bootstrap-build.log" 2>&1; then pass "1c bootstrap build-npm.sh" else tail -40 "$HOME/e2e-bootstrap-build.log"; fail_ "1c bootstrap build-npm.sh"; exit 1 diff --git a/scripts/e2e-update-migrations.sh b/scripts/e2e-update-migrations.sh index e152888e40..b5d57f661a 100755 --- a/scripts/e2e-update-migrations.sh +++ b/scripts/e2e-update-migrations.sh @@ -97,7 +97,7 @@ else | tar -xz --strip-components=1 -C "$BOOT" || { fail_ "1a bootstrap tarball"; exit 1; } ( cd "$BOOT" \ && corepack pnpm install --frozen-lockfile > "$HOME/e2e-upd-bootstrap-install.log" 2>&1 \ - && bash scripts/build-npm.sh --skip-checks --skip-typecheck > "$HOME/e2e-upd-bootstrap-build.log" 2>&1 ) \ + && PAPERCLIP_README_ASSET_REF="$BASE_REF" bash scripts/build-npm.sh --skip-checks --skip-typecheck > "$HOME/e2e-upd-bootstrap-build.log" 2>&1 ) \ || { tail -40 "$HOME"/e2e-upd-bootstrap-*.log; fail_ "1b bootstrap build"; exit 1; } TARBALL="$(cd "$BOOT/cli" && npm pack --silent 2>/dev/null | tail -1)" mkdir -p "$HOME/e2e-upd-bootstrap-cli" diff --git a/scripts/prepare-npm-readme.mjs b/scripts/prepare-npm-readme.mjs new file mode 100644 index 0000000000..fbefd3a15d --- /dev/null +++ b/scripts/prepare-npm-readme.mjs @@ -0,0 +1,38 @@ +import { readFileSync, writeFileSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +export function prepareNpmReadme(readme, assetRef) { + if (!assetRef) { + throw new Error("an immutable README asset ref is required"); + } + + const assetBaseUrl = + `https://raw.githubusercontent.com/paperclipai/paperclip/${assetRef}/doc/assets/`; + + return readme.replace( + /((?:src|srcset)=["'])([^"']*)(["'])/g, + (_match, prefix, value, suffix) => + `${prefix}${value.replace( + /(^|,\s*)doc\/assets\//g, + `$1${assetBaseUrl}`, + )}${suffix}`, + ); +} + +const isDirectRun = + process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url); + +if (isDirectRun) { + const [sourcePath, destinationPath, assetRef] = process.argv.slice(2); + if (!sourcePath || !destinationPath || !assetRef) { + throw new Error( + "usage: prepare-npm-readme.mjs ", + ); + } + + writeFileSync( + destinationPath, + prepareNpmReadme(readFileSync(sourcePath, "utf8"), assetRef), + ); +} diff --git a/scripts/prepare-npm-readme.test.mjs b/scripts/prepare-npm-readme.test.mjs new file mode 100644 index 0000000000..a5d40678e8 --- /dev/null +++ b/scripts/prepare-npm-readme.test.mjs @@ -0,0 +1,48 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import test from "node:test"; + +import { prepareNpmReadme } from "./prepare-npm-readme.mjs"; + +const assetBase = + "https://raw.githubusercontent.com/paperclipai/paperclip/master/doc/assets/"; +const releaseAssetBase = + "https://raw.githubusercontent.com/paperclipai/paperclip/abc123/doc/assets/"; + +test("rewrites repository-relative image sources for npm", () => { + const readme = [ + '', + '', + '[docs](doc/assets/not-an-image.md)', + '', + '', + ].join("\n"); + + assert.equal( + prepareNpmReadme(readme, "master"), + [ + ``, + ``, + "[docs](doc/assets/not-an-image.md)", + '', + '', + ].join("\n"), + ); +}); + +test("prepares the repository README without package-relative image sources", () => { + const readme = readFileSync(new URL("../README.md", import.meta.url), "utf8"); + const npmReadme = prepareNpmReadme(readme, "master"); + + assert.doesNotMatch( + npmReadme, + /(?:src|srcset)=["'](?:doc\/assets\/|[^"']*,\s*doc\/assets\/)/, + ); +}); + +test("supports immutable release refs for generated asset URLs", () => { + assert.equal( + prepareNpmReadme('', "abc123"), + ``, + ); +});