From df07fbe2429c023c1e43de954c684d348efdecae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Greg=20Berg=C3=A9?= Date: Tue, 20 Jan 2026 17:22:24 +0100 Subject: [PATCH] Remove `development` from `package.exports` before publishing (#3917) --- scripts/publish-if-new.sh | 41 ++++++++++++--- scripts/strip-development-exports.mjs | 57 ++++++++++++++++++++ scripts/strip-development-exports.test.mjs | 61 ++++++++++++++++++++++ 3 files changed, 152 insertions(+), 7 deletions(-) create mode 100644 scripts/strip-development-exports.mjs create mode 100644 scripts/strip-development-exports.test.mjs diff --git a/scripts/publish-if-new.sh b/scripts/publish-if-new.sh index 5af3f5b18..2f40d4838 100755 --- a/scripts/publish-if-new.sh +++ b/scripts/publish-if-new.sh @@ -8,6 +8,14 @@ set -euo pipefail +DRY_RUN=false +for arg in "$@"; do + if [[ "${arg}" == "--dry-run" ]]; then + DRY_RUN=true + break + fi +done + NAME=$(node -p "require('./package.json').name") VERSION=$(node -p "require('./package.json').version") @@ -16,6 +24,27 @@ if npm view "${NAME}@${VERSION}" >/dev/null 2>&1; then exit 0 fi +# We compute the workspace root relative to this script +WORKSPACE_ROOT=$(dirname $(dirname $(realpath $0))) + +# Strip development exports before packing and restore afterward. +PACKAGE_JSON_PATH="${PWD}/package.json" +PACKAGE_JSON_BACKUP=$(mktemp) +cp "${PACKAGE_JSON_PATH}" "${PACKAGE_JSON_BACKUP}" + +cleanup() { + if [[ -n "${TARBALL_PATH:-}" && -f "${TARBALL_PATH}" ]]; then + rm -f "${TARBALL_PATH}" + fi + if [[ -f "${PACKAGE_JSON_BACKUP}" ]]; then + mv "${PACKAGE_JSON_BACKUP}" "${PACKAGE_JSON_PATH}" + fi +} + +trap cleanup EXIT + +node "${WORKSPACE_ROOT}/scripts/strip-development-exports.mjs" "${PACKAGE_JSON_PATH}" + # Sanitize the name to make it a valid filename as bun doesn't support @ in filenames SANITIZED_NAME=${NAME//@/} SANITIZED_NAME=${SANITIZED_NAME//\//-} @@ -23,9 +52,6 @@ TARBALL_FILENAME="${SANITIZED_NAME}-${VERSION}.tgz" bun pm pack --filename "${TARBALL_FILENAME}" -# We compute the workspace root relative to this script -WORKSPACE_ROOT=$(dirname $(dirname $(realpath $0))) - # Bun pack puts the tarball in the workspace root TARBALL_PATH="${WORKSPACE_ROOT}/${TARBALL_FILENAME}" @@ -34,8 +60,9 @@ if [[ ! -f "${TARBALL_PATH}" ]]; then exit 1 fi -# Clean up the tarball -trap 'rm -f "${TARBALL_PATH}"' EXIT - # Publish with verbose logging to aid debugging -npm publish "${TARBALL_PATH}" --no-workspaces --provenance +if [[ "${DRY_RUN}" == "true" ]]; then + echo "npm publish \"${TARBALL_PATH}\" --no-workspaces --provenance" +else + npm publish "${TARBALL_PATH}" --no-workspaces --provenance +fi diff --git a/scripts/strip-development-exports.mjs b/scripts/strip-development-exports.mjs new file mode 100644 index 000000000..f87dc9db8 --- /dev/null +++ b/scripts/strip-development-exports.mjs @@ -0,0 +1,57 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { pathToFileURL } from 'node:url'; + +export function stripDevelopmentExports(exportsField) { + if (!exportsField || typeof exportsField !== 'object' || Array.isArray(exportsField)) { + return false; + } + + let changed = false; + + if (Object.prototype.hasOwnProperty.call(exportsField, 'development')) { + delete exportsField.development; + changed = true; + } + + for (const value of Object.values(exportsField)) { + if (value && typeof value === 'object' && !Array.isArray(value)) { + if (stripDevelopmentExports(value)) { + changed = true; + } + } + } + + return changed; +} + +export async function stripDevelopmentExportsInFile(filePath) { + const raw = await fs.readFile(filePath, 'utf8'); + const parsed = JSON.parse(raw); + + if (!parsed.exports) { + return false; + } + + const changed = stripDevelopmentExports(parsed.exports); + + if (!changed) { + return false; + } + + const next = `${JSON.stringify(parsed, null, 4)}\n`; + await fs.writeFile(filePath, next); + return true; +} + +async function main() { + const filePath = process.argv[2] || path.join(process.cwd(), 'package.json'); + await stripDevelopmentExportsInFile(filePath); +} + +if (import.meta.url === pathToFileURL(process.argv[1]).href) { + main().catch((error) => { + console.error('Failed to strip development exports:', error); + process.exit(1); + }); +} diff --git a/scripts/strip-development-exports.test.mjs b/scripts/strip-development-exports.test.mjs new file mode 100644 index 000000000..4370ad023 --- /dev/null +++ b/scripts/strip-development-exports.test.mjs @@ -0,0 +1,61 @@ +import { expect, test } from 'bun:test'; +import fs from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; + +import { stripDevelopmentExportsInFile } from './strip-development-exports.mjs'; + +test('stripDevelopmentExportsInFile removes development conditions', async () => { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'strip-dev-exports-')); + const filePath = path.join(dir, 'package.json'); + + const original = { + name: '@gitbook/example', + exports: { + '.': { + types: './dist/index.d.ts', + development: './src/index.ts', + default: './dist/index.js', + }, + './icons': { + types: './dist/icons.d.ts', + development: './src/icons.ts', + default: './dist/icons.js', + }, + }, + }; + + await fs.writeFile(filePath, `${JSON.stringify(original, null, 4)}\n`); + + const changed = await stripDevelopmentExportsInFile(filePath); + expect(changed).toBe(true); + + const updated = JSON.parse(await fs.readFile(filePath, 'utf8')); + + expect(updated.exports['.'].development).toBeUndefined(); + expect(updated.exports['./icons'].development).toBeUndefined(); + expect(updated.exports['.'].default).toBe('./dist/index.js'); +}); + +test('stripDevelopmentExportsInFile returns false when nothing to remove', async () => { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'strip-dev-exports-')); + const filePath = path.join(dir, 'package.json'); + + const original = { + name: '@gitbook/example', + exports: { + '.': { + types: './dist/index.d.ts', + default: './dist/index.js', + }, + }, + }; + + await fs.writeFile(filePath, `${JSON.stringify(original, null, 4)}\n`); + + const changed = await stripDevelopmentExportsInFile(filePath); + expect(changed).toBe(false); + + const updated = JSON.parse(await fs.readFile(filePath, 'utf8')); + expect(updated).toEqual(original); +});