Skip to content

Commit e8dfdca

Browse files
committed
Add scripts for preparing and restoring packages before and after npm publish
1 parent 3dc2a98 commit e8dfdca

File tree

5 files changed

+215
-5
lines changed

5 files changed

+215
-5
lines changed

docs/agents/PUBLISHING.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,20 @@ The `.github/workflows/publish-dev.yml` workflow will automatically:
5252
4. **Test**: Run `pnpm test:run` to ensure tests pass (web DB tests may fail - that's OK)
5353
5. **Build**: Run `pnpm build` to build all packages
5454
6. **Validate**: Run `node bin/lean-spec.js validate` and `cd docs-site && npm run build` to ensure everything works
55-
7. **Commit**: `git add -A && git commit -m "chore: bump version to X.Y.Z"`
56-
8. **Tag**: `git tag vX.Y.Z && git push origin main --tags`
57-
9. **GitHub Release**: `gh release create vX.Y.Z --title "vX.Y.Z - Title" --notes "Release notes here"`
55+
7. **Prepare for publish**: Run `pnpm prepare-publish` to replace `workspace:*` with actual versions
56+
- ⚠️ **CRITICAL**: This step prevents `workspace:*` from leaking into npm packages
57+
- Creates backups of original package.json files
58+
- Replaces all `workspace:*` dependencies with actual versions
59+
8. **Commit**: `git add -A && git commit -m "chore: bump version to X.Y.Z"`
60+
9. **Tag**: `git tag vX.Y.Z && git push origin main --tags`
61+
10. **GitHub Release**: `gh release create vX.Y.Z --title "vX.Y.Z - Title" --notes "Release notes here"`
5862
- This triggers the GitHub Action workflow that publishes both `lean-spec` and `@leanspec/ui` to npm
59-
10. **Verify**:
63+
11. **Restore packages**: Run `pnpm restore-packages` to restore original package.json files with `workspace:*`
64+
12. **Verify**:
6065
- `npm view lean-spec version` to confirm CLI publication
6166
- `npm view @leanspec/ui version` to confirm UI publication
6267
- `npm view lean-spec dependencies` to ensure no `workspace:*` dependencies leaked
68+
- `npm view @leanspec/ui dependencies` to ensure no `workspace:*` dependencies leaked
6369
- Test installation: `npm install -g lean-spec@latest` in a clean environment
6470
- Check GitHub release page: https://github.com/codervisor/lean-spec/releases
6571

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
"test:ui": "vitest --ui",
2020
"test:run": "vitest run",
2121
"test:coverage": "vitest run --coverage",
22+
"prepare-publish": "tsx scripts/prepare-publish.ts",
23+
"restore-packages": "tsx scripts/restore-packages.ts",
2224
"pre-release": "pnpm sync-versions && pnpm typecheck && pnpm test:run && pnpm build && node bin/lean-spec.js validate --warnings-only",
2325
"docs:dev": "pnpm --dir docs-site start",
2426
"docs:build": "pnpm --dir docs-site build",

packages/ui/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@
5454
},
5555
"dependencies": {
5656
"@dagrejs/dagre": "^1.1.8",
57-
"@leanspec/core": "workspace:*",
5857
"@radix-ui/react-avatar": "^1.1.11",
5958
"@radix-ui/react-dialog": "^1.1.15",
6059
"@radix-ui/react-dropdown-menu": "^2.1.16",
@@ -99,6 +98,7 @@
9998
"zod": "^3.25.76"
10099
},
101100
"devDependencies": {
101+
"@leanspec/core": "workspace:*",
102102
"@tailwindcss/typography": "^0.5.15",
103103
"@types/better-sqlite3": "^7.6.12",
104104
"@types/mdast": "^4.0.4",

scripts/prepare-publish.ts

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Prepare packages for npm publish by replacing workspace:* dependencies with actual versions.
4+
* Run this script before publishing to ensure no workspace protocol leaks into npm.
5+
*
6+
* Usage:
7+
* npm run prepare-publish
8+
* pnpm prepare-publish
9+
*
10+
* This script:
11+
* 1. Finds all workspace:* dependencies in packages
12+
* 2. Resolves actual versions from local package.json files
13+
* 3. Creates temporary package.json files with resolved versions
14+
* 4. After publish, restore original package.json files
15+
*/
16+
17+
import { readFileSync, writeFileSync, existsSync } from 'fs';
18+
import { join, dirname } from 'path';
19+
import { fileURLToPath } from 'url';
20+
21+
const __filename = fileURLToPath(import.meta.url);
22+
const __dirname = dirname(__filename);
23+
const ROOT = join(__dirname, '..');
24+
25+
interface PackageJson {
26+
name: string;
27+
version: string;
28+
dependencies?: Record<string, string>;
29+
devDependencies?: Record<string, string>;
30+
peerDependencies?: Record<string, string>;
31+
}
32+
33+
function readPackageJson(pkgPath: string): PackageJson {
34+
return JSON.parse(readFileSync(pkgPath, 'utf-8'));
35+
}
36+
37+
function writePackageJson(pkgPath: string, pkg: PackageJson): void {
38+
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
39+
}
40+
41+
function resolveWorkspaceVersion(depName: string): string | null {
42+
// Map package names to their paths in the monorepo
43+
const pkgMap: Record<string, string> = {
44+
'@leanspec/core': 'packages/core/package.json',
45+
'@leanspec/ui': 'packages/ui/package.json',
46+
'lean-spec': 'packages/cli/package.json',
47+
};
48+
49+
const pkgPath = pkgMap[depName];
50+
if (!pkgPath) {
51+
console.warn(`⚠️ Unknown workspace package: ${depName}`);
52+
return null;
53+
}
54+
55+
const fullPath = join(ROOT, pkgPath);
56+
if (!existsSync(fullPath)) {
57+
console.warn(`⚠️ Package not found: ${fullPath}`);
58+
return null;
59+
}
60+
61+
const pkg = readPackageJson(fullPath);
62+
return pkg.version;
63+
}
64+
65+
function replaceWorkspaceDeps(deps: Record<string, string> | undefined, depType: string): boolean {
66+
if (!deps) return false;
67+
68+
let changed = false;
69+
for (const [name, version] of Object.entries(deps)) {
70+
if (version.startsWith('workspace:')) {
71+
const resolvedVersion = resolveWorkspaceVersion(name);
72+
if (resolvedVersion) {
73+
deps[name] = `^${resolvedVersion}`;
74+
console.log(` ✓ ${depType}.${name}: workspace:* → ^${resolvedVersion}`);
75+
changed = true;
76+
}
77+
}
78+
}
79+
return changed;
80+
}
81+
82+
function processPackage(pkgPath: string): boolean {
83+
const fullPath = join(ROOT, pkgPath);
84+
if (!existsSync(fullPath)) {
85+
console.warn(`⚠️ Package not found: ${fullPath}`);
86+
return false;
87+
}
88+
89+
const pkg = readPackageJson(fullPath);
90+
console.log(`\n📦 Processing ${pkg.name}...`);
91+
92+
let changed = false;
93+
changed = replaceWorkspaceDeps(pkg.dependencies, 'dependencies') || changed;
94+
changed = replaceWorkspaceDeps(pkg.devDependencies, 'devDependencies') || changed;
95+
changed = replaceWorkspaceDeps(pkg.peerDependencies, 'peerDependencies') || changed;
96+
97+
if (changed) {
98+
// Create backup
99+
const backupPath = fullPath + '.backup';
100+
writeFileSync(backupPath, readFileSync(fullPath, 'utf-8'));
101+
console.log(` 💾 Backup saved to ${pkgPath}.backup`);
102+
103+
// Write updated package.json
104+
writePackageJson(fullPath, pkg);
105+
console.log(` ✅ Updated ${pkgPath}`);
106+
return true;
107+
} else {
108+
console.log(` ⏭️ No workspace:* dependencies found`);
109+
return false;
110+
}
111+
}
112+
113+
function main() {
114+
console.log('🚀 Preparing packages for npm publish...\n');
115+
console.log('This will replace workspace:* with actual versions.\n');
116+
117+
const packages = [
118+
'packages/core/package.json',
119+
'packages/cli/package.json',
120+
'packages/ui/package.json',
121+
];
122+
123+
const modified: string[] = [];
124+
for (const pkg of packages) {
125+
if (processPackage(pkg)) {
126+
modified.push(pkg);
127+
}
128+
}
129+
130+
if (modified.length > 0) {
131+
console.log('\n✅ Preparation complete!');
132+
console.log('\nModified packages:');
133+
modified.forEach(pkg => console.log(` - ${pkg}`));
134+
console.log('\n⚠️ IMPORTANT: After publishing, restore original files:');
135+
console.log(' npm run restore-packages');
136+
console.log(' OR manually: mv package.json.backup package.json');
137+
} else {
138+
console.log('\n✅ No workspace:* dependencies found. Ready to publish!');
139+
}
140+
}
141+
142+
main();

scripts/restore-packages.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Restore original package.json files after publishing.
4+
* This reverts the changes made by prepare-publish.ts
5+
*
6+
* Usage:
7+
* npm run restore-packages
8+
* pnpm restore-packages
9+
*/
10+
11+
import { existsSync, renameSync, unlinkSync } from 'fs';
12+
import { join, dirname } from 'path';
13+
import { fileURLToPath } from 'url';
14+
15+
const __filename = fileURLToPath(import.meta.url);
16+
const __dirname = dirname(__filename);
17+
const ROOT = join(__dirname, '..');
18+
19+
function restorePackage(pkgPath: string): boolean {
20+
const fullPath = join(ROOT, pkgPath);
21+
const backupPath = fullPath + '.backup';
22+
23+
if (!existsSync(backupPath)) {
24+
console.log(`⏭️ No backup found for ${pkgPath}`);
25+
return false;
26+
}
27+
28+
console.log(`📦 Restoring ${pkgPath}...`);
29+
30+
// Replace current with backup
31+
renameSync(backupPath, fullPath);
32+
console.log(` ✅ Restored from backup`);
33+
34+
return true;
35+
}
36+
37+
function main() {
38+
console.log('🔄 Restoring original package.json files...\n');
39+
40+
const packages = [
41+
'packages/core/package.json',
42+
'packages/cli/package.json',
43+
'packages/ui/package.json',
44+
];
45+
46+
let restored = 0;
47+
for (const pkg of packages) {
48+
if (restorePackage(pkg)) {
49+
restored++;
50+
}
51+
}
52+
53+
if (restored > 0) {
54+
console.log(`\n✅ Restored ${restored} package(s)`);
55+
} else {
56+
console.log('\n⚠️ No backups found. Nothing to restore.');
57+
}
58+
}
59+
60+
main();

0 commit comments

Comments
 (0)