Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ web_modules/
# Output of 'npm pack'
*.tgz
bulma-ui/CLAUDE.md.bak
bestax-migrate/package.json.pack-backup

# Yarn Integrity file
.yarn-integrity
Expand Down
4 changes: 4 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ React component library for **Bulma v1** in TypeScript. pnpm monorepo orchestrat
- `bulma-ui/` — the library, published as `@allxsmith/bestax-bulma` (has its own CLAUDE.md)
- `docs/` — Docusaurus site → https://bestax.io (has its own CLAUDE.md)
- `create-bestax/` — the `npm create bestax` scaffolder (has its own CLAUDE.md)
- `bestax-migrate/` — the `bestax-migrate` codemod CLI (has its own CLAUDE.md)
- `skills/` — Agent Skills, a **shipped product** bundled into create-bestax (has its own CLAUDE.md)
- `scripts/gen-component-catalog.mjs` — generates the skill component catalog (`pnpm gen:catalog`)

Expand Down Expand Up @@ -70,6 +71,9 @@ Full versioning details (breaking-change footers, tag formats): `VERSIONING.md`.
- Install/postinstall scripts are **blocked by default** — new native deps need an `allowBuilds` entry.
- `minimumReleaseAge` cooldown: versions younger than 3 days won't install.
- Isolated node linker: undeclared (phantom) dependencies fail — declare everything you import.
- Published packages must not ship a `workspace:` specifier — `npm publish` (what
semantic-release runs) does not resolve it, and the tarball becomes uninstallable (#412).
`check:conformance --only=publishable-manifests` enforces this.

## Workflow

Expand Down
10 changes: 9 additions & 1 deletion bestax-migrate/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,15 @@ each source library registers in `src/sources/registry.ts`; the first is
## Releases

Independent semantic-release, keyed off the `bestax-migrate` commit scope
(`release.config.js`, tag `bestax-migrate@x.y.z`). The skill lives at repo-root
(`release.config.js`, tag `bestax-migrate@x.y.z`). Publishing goes through
`npm publish`, which — unlike `pnpm publish` — does **not** resolve pnpm's
`workspace:` protocol, so `workspace:^` shipped verbatim in 1.0.0 and made the
package uninstallable (#412). Two rules follow: `@allxsmith/bestax-bulma` stays
a **devDependency** (it is only the typecheck target for the e2e, never
imported at runtime — consumers of a codemod CLI must not be made to install
the component library), and `scripts/pack-manifest.mjs` resolves any remaining
`workspace:` specifier during `prepack`/`postpack`. `pnpm check:conformance
--only=publishable-manifests` enforces both. The skill lives at repo-root
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
`skills/bestax-migrate/` and is deliberately **not** bundled into create-bestax (existing
sites only) — keep `create-bestax/scripts/sync-skills.mjs` untouched. When the mapping
gains or loses coverage, update `skills/bestax-migrate/references/` and the docs migration
Expand Down
6 changes: 4 additions & 2 deletions bestax-migrate/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@
"format": "prettier --write \"src/**/*.{ts,tsx}\" \"e2e/**/*.ts\"",
"format:check": "prettier --check \"src/**/*.{ts,tsx}\" \"e2e/**/*.ts\"",
"clean": "rimraf dist",
"release": "npx semantic-release"
"release": "npx semantic-release",
"prepack": "node scripts/pack-manifest.mjs prepack",
"postpack": "node scripts/pack-manifest.mjs postpack"
},
"keywords": [
"bestax",
Expand All @@ -48,13 +50,13 @@
"url": "https://github.com/allxsmith/bestax/issues"
},
"dependencies": {
"@allxsmith/bestax-bulma": "workspace:^",
"@babel/parser": "^7.26.0",
"chalk": "^5.6.2",
"commander": "^15.0.0",
"jscodeshift": "^17.3.0"
},
"devDependencies": {
"@allxsmith/bestax-bulma": "workspace:^",
"@jest/globals": "^30.0.0",
"@types/jest": "^30.0.0",
"@types/jscodeshift": "^17.3.0",
Expand Down
119 changes: 119 additions & 0 deletions bestax-migrate/scripts/pack-manifest.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
#!/usr/bin/env node
/**
* Resolves pnpm `workspace:` specifiers in package.json while the tarball is
* packed (issue #412).
*
* `pnpm publish` rewrites `workspace:^` to the real semver range at pack time;
* `npm publish` does NOT — and the release pipeline runs `@semantic-release/npm`,
* which shells out to `npm publish`. That shipped `bestax-migrate@1.0.0` with a
* literal `"workspace:^"` in its manifest, making it uninstallable by every
* package manager (`EUNSUPPORTEDPROTOCOL`).
*
* So do the rewrite ourselves. The repo file must come back untouched — the
* release commits package.json — so `prepack` backs it up and `postpack`
* restores it, exactly like bulma-ui/scripts/pack-pointer-files.mjs.
*
* This runs over EVERY dependency section, not just `dependencies`: a
* `workspace:` specifier is meaningless outside the workspace wherever it
* appears. The companion guard is the `publishable-manifests` sub-check in
* scripts/check-conformance.mjs, which fails CI if a published package ever
* declares a workspace dep in `dependencies` (a runtime dep the tarball would
* need) rather than `devDependencies`.
*/
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const pkgRoot = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
const manifest = path.join(pkgRoot, 'package.json');
const backup = path.join(pkgRoot, 'package.json.pack-backup');

const DEP_SECTIONS = [
'dependencies',
'devDependencies',
'peerDependencies',
'optionalDependencies',
];

/** Version of a workspace package, read through the linked node_modules entry. */
function resolveWorkspaceVersion(name) {
const linked = path.join(pkgRoot, 'node_modules', name, 'package.json');
if (!fs.existsSync(linked)) {
console.error(
`pack-manifest: cannot resolve the workspace dependency "${name}" — ` +
`${path.relative(pkgRoot, linked)} does not exist.\n` +
'Run `pnpm install` from the repo root before packing.'
);
process.exit(1);
}
const { version } = JSON.parse(fs.readFileSync(linked, 'utf8'));
if (!version) {
console.error(`pack-manifest: "${name}" has no version in its manifest`);
process.exit(1);
}
return version;
}

/**
* `workspace:^` / `workspace:~` / `workspace:*` take the prefix from the
* protocol and the version from the linked package; `workspace:<range>`
* (e.g. `workspace:^5.0.0`) already carries its own range, so just unwrap it.
*/
function resolveSpecifier(name, spec) {
const rest = spec.slice('workspace:'.length);
if (rest === '^' || rest === '~')
return `${rest}${resolveWorkspaceVersion(name)}`;
if (rest === '*') return resolveWorkspaceVersion(name);
return rest;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

const mode = process.argv[2];

if (mode === 'prepack') {
if (fs.existsSync(backup)) {
console.error(
'pack-manifest: package.json.pack-backup already exists — a previous ' +
'pack did not finish.\n' +
'Restore the workspace manifest first: mv package.json.pack-backup package.json'
);
process.exit(1);
}

const pkg = JSON.parse(fs.readFileSync(manifest, 'utf8'));
const rewritten = [];

for (const section of DEP_SECTIONS) {
for (const [name, spec] of Object.entries(pkg[section] ?? {})) {
if (typeof spec !== 'string' || !spec.startsWith('workspace:')) continue;
const resolved = resolveSpecifier(name, spec);
pkg[section][name] = resolved;
rewritten.push(`${section}.${name}: ${spec} -> ${resolved}`);
}
}

if (!rewritten.length) {
console.log('pack-manifest: no workspace: specifiers to resolve');
process.exit(0);
}

fs.copyFileSync(manifest, backup);
// Keep npm's own formatting (2-space + trailing newline) so the restored
// file and the packed one differ only in the specifiers.
fs.writeFileSync(manifest, `${JSON.stringify(pkg, null, 2)}\n`);
console.log(
`pack-manifest: resolved ${rewritten.length} workspace specifier(s)`
);
for (const line of rewritten) console.log(` ${line}`);
} else if (mode === 'postpack') {
if (!fs.existsSync(backup)) {
// prepack exits early when there is nothing to rewrite; that is not an error.
console.log('pack-manifest: no backup to restore');
process.exit(0);
}
fs.copyFileSync(backup, manifest);
fs.rmSync(backup);
console.log('pack-manifest: workspace manifest restored');
} else {
console.error('Usage: node scripts/pack-manifest.mjs <prepack|postpack>');
process.exit(1);
}
67 changes: 18 additions & 49 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading