@shazhou/proman-core 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/LICENSE +18 -0
- package/dist/commands/bump.d.ts +13 -0
- package/dist/commands/bump.d.ts.map +1 -0
- package/dist/commands/bump.js +115 -0
- package/dist/commands/deploy.d.ts +9 -0
- package/dist/commands/deploy.d.ts.map +1 -0
- package/dist/commands/deploy.js +42 -0
- package/dist/commands/dev.d.ts +15 -0
- package/dist/commands/dev.d.ts.map +1 -0
- package/dist/commands/dev.js +175 -0
- package/dist/commands/index.d.ts +7 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/index.js +7 -0
- package/dist/commands/init.d.ts +5 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +262 -0
- package/dist/commands/link.d.ts +19 -0
- package/dist/commands/link.d.ts.map +1 -0
- package/dist/commands/link.js +155 -0
- package/dist/commands/publish.d.ts +18 -0
- package/dist/commands/publish.d.ts.map +1 -0
- package/dist/commands/publish.js +125 -0
- package/dist/config/index.d.ts +4 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +2 -0
- package/dist/config/load-config.d.ts +6 -0
- package/dist/config/load-config.d.ts.map +1 -0
- package/dist/config/load-config.js +29 -0
- package/dist/config/types.d.ts +17 -0
- package/dist/config/types.d.ts.map +1 -0
- package/dist/config/types.js +1 -0
- package/dist/config/validate-config.d.ts +7 -0
- package/dist/config/validate-config.d.ts.map +1 -0
- package/dist/config/validate-config.js +72 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/utils/changeset.d.ts +16 -0
- package/dist/utils/changeset.d.ts.map +1 -0
- package/dist/utils/changeset.js +80 -0
- package/dist/utils/fingerprint.d.ts +38 -0
- package/dist/utils/fingerprint.d.ts.map +1 -0
- package/dist/utils/fingerprint.js +182 -0
- package/dist/utils/git.d.ts +23 -0
- package/dist/utils/git.d.ts.map +1 -0
- package/dist/utils/git.js +105 -0
- package/dist/utils/index.d.ts +8 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +8 -0
- package/dist/utils/npm.d.ts +30 -0
- package/dist/utils/npm.d.ts.map +1 -0
- package/dist/utils/npm.js +85 -0
- package/dist/utils/smoke-test.d.ts +7 -0
- package/dist/utils/smoke-test.d.ts.map +1 -0
- package/dist/utils/smoke-test.js +59 -0
- package/dist/utils/version.d.ts +5 -0
- package/dist/utils/version.d.ts.map +1 -0
- package/dist/utils/version.js +36 -0
- package/dist/utils/workspace.d.ts +21 -0
- package/dist/utils/workspace.d.ts.map +1 -0
- package/dist/utils/workspace.js +73 -0
- package/package.json +45 -0
- package/src/commands/bump.ts +131 -0
- package/src/commands/deploy.ts +52 -0
- package/src/commands/dev.ts +214 -0
- package/src/commands/index.ts +7 -0
- package/src/commands/init.integration.test.ts +59 -0
- package/src/commands/init.test.ts +179 -0
- package/src/commands/init.ts +290 -0
- package/src/commands/link.ts +195 -0
- package/src/commands/publish.ts +168 -0
- package/src/config/index.ts +8 -0
- package/src/config/load-config.ts +33 -0
- package/src/config/types.ts +19 -0
- package/src/config/validate-config.ts +81 -0
- package/src/index.ts +29 -0
- package/src/utils/changeset.ts +98 -0
- package/src/utils/fingerprint.ts +199 -0
- package/src/utils/git.ts +119 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/npm.ts +110 -0
- package/src/utils/smoke-test.ts +79 -0
- package/src/utils/version.ts +41 -0
- package/src/utils/workspace.ts +94 -0
- package/tests/build-fingerprint-integration.test.ts +403 -0
- package/tests/bump.test.ts +261 -0
- package/tests/changeset.test.ts +147 -0
- package/tests/deploy.test.ts +98 -0
- package/tests/dev.test.ts +756 -0
- package/tests/fingerprint.test.ts +316 -0
- package/tests/fixtures/api-only/packages/api/.gitkeep +0 -0
- package/tests/fixtures/api-only/proman.yaml +4 -0
- package/tests/fixtures/bad-packages/proman.yaml +1 -0
- package/tests/fixtures/bun-project/packages/a/.gitkeep +0 -0
- package/tests/fixtures/bun-project/proman.yaml +4 -0
- package/tests/fixtures/defaults/proman.yaml +3 -0
- package/tests/fixtures/no-deployable/packages/core/.gitkeep +0 -0
- package/tests/fixtures/no-deployable/packages/mycli/.gitkeep +0 -0
- package/tests/fixtures/no-deployable/proman.yaml +7 -0
- package/tests/fixtures/node-runtime/packages/a/package.json +5 -0
- package/tests/fixtures/node-runtime/proman.yaml +3 -0
- package/tests/fixtures/pnpm-project/packages/a/package.json +1 -0
- package/tests/fixtures/pnpm-project/pnpm-lock.yaml +0 -0
- package/tests/fixtures/pnpm-project/proman.yaml +3 -0
- package/tests/fixtures/typed/packages/api/.gitkeep +0 -0
- package/tests/fixtures/typed/packages/core/.gitkeep +0 -0
- package/tests/fixtures/typed/packages/dashboard/.gitkeep +0 -0
- package/tests/fixtures/typed/packages/mycli/.gitkeep +0 -0
- package/tests/fixtures/typed/proman.yaml +13 -0
- package/tests/fixtures/valid/packages/cli/package.json +5 -0
- package/tests/fixtures/valid/packages/core/package.json +5 -0
- package/tests/fixtures/valid/packages/fs/package.json +5 -0
- package/tests/fixtures/valid/proman.yaml +13 -0
- package/tests/fixtures/webui-only/packages/dashboard/.gitkeep +0 -0
- package/tests/fixtures/webui-only/proman.yaml +4 -0
- package/tests/link.test.ts +419 -0
- package/tests/load-config.test.ts +44 -0
- package/tests/npm.test.ts +199 -0
- package/tests/publish.test.ts +599 -0
- package/tests/smoke-test.test.ts +211 -0
- package/tests/validate-config.test.ts +67 -0
- package/tests/version.test.ts +86 -0
- package/tests/workflow-schema.test.ts +72 -0
- package/tests/workspace.test.ts +160 -0
- package/tsconfig.build.json +14 -0
- package/tsconfig.json +8 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { basename, join, resolve } from 'node:path';
|
|
4
|
+
function jsonStringify(obj) {
|
|
5
|
+
return JSON.stringify(obj, null, 2);
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Sanitize a directory name into a valid npm package name segment.
|
|
9
|
+
*
|
|
10
|
+
* Rules applied (per https://github.com/npm/validate-npm-package-name):
|
|
11
|
+
* - lowercase only
|
|
12
|
+
* - strip characters not in `[a-z0-9._-]` (replaces with hyphens)
|
|
13
|
+
* - strip leading `.`, `_`, or `-`
|
|
14
|
+
* - collapse consecutive hyphens
|
|
15
|
+
* - enforce 214-character npm limit
|
|
16
|
+
* - fall back to `'my-project'` if nothing remains
|
|
17
|
+
*/
|
|
18
|
+
function toPackageName(dirName) {
|
|
19
|
+
return (dirName
|
|
20
|
+
.toLowerCase()
|
|
21
|
+
.replace(/[^a-z0-9._-]/g, '-') // replace invalid chars
|
|
22
|
+
.replace(/^[._-]+/, '') // strip leading dots/hyphens/underscores
|
|
23
|
+
.replace(/-+/g, '-') // collapse consecutive hyphens
|
|
24
|
+
.slice(0, 214) || // npm name length limit
|
|
25
|
+
'my-project'); // fallback if everything was stripped
|
|
26
|
+
}
|
|
27
|
+
export async function init(opts) {
|
|
28
|
+
const targetDir = resolve(opts.targetDir);
|
|
29
|
+
const projectName = toPackageName(basename(targetDir));
|
|
30
|
+
// Check if directory is empty
|
|
31
|
+
if (existsSync(targetDir)) {
|
|
32
|
+
const entries = readdirSync(targetDir);
|
|
33
|
+
if (entries.length > 0) {
|
|
34
|
+
throw new Error(`Directory is not empty: ${targetDir}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
mkdirSync(targetDir, { recursive: true });
|
|
39
|
+
}
|
|
40
|
+
// Create root files
|
|
41
|
+
createRootPackageJson(targetDir, projectName);
|
|
42
|
+
createPromanYaml(targetDir, projectName);
|
|
43
|
+
createPnpmWorkspace(targetDir);
|
|
44
|
+
createBiomeJson(targetDir);
|
|
45
|
+
createTsConfig(targetDir);
|
|
46
|
+
createGitignore(targetDir);
|
|
47
|
+
// Create packages
|
|
48
|
+
createCorePackage(targetDir, projectName);
|
|
49
|
+
createCliPackage(targetDir, projectName);
|
|
50
|
+
// Format all JSON files with biome
|
|
51
|
+
try {
|
|
52
|
+
execSync('pnpm exec biome format --write .', { cwd: targetDir, stdio: 'ignore' });
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// Ignore biome formatting errors during init - user can run format later
|
|
56
|
+
}
|
|
57
|
+
// Print post-init message
|
|
58
|
+
console.log(`✓ Created monorepo in ${targetDir}`);
|
|
59
|
+
console.log('');
|
|
60
|
+
console.log('Next steps:');
|
|
61
|
+
if (targetDir !== process.cwd()) {
|
|
62
|
+
console.log(` cd ${projectName}`);
|
|
63
|
+
}
|
|
64
|
+
console.log(' pnpm install');
|
|
65
|
+
console.log(' proman build');
|
|
66
|
+
}
|
|
67
|
+
function createRootPackageJson(targetDir, projectName) {
|
|
68
|
+
const content = {
|
|
69
|
+
name: projectName,
|
|
70
|
+
private: true,
|
|
71
|
+
type: 'module',
|
|
72
|
+
scripts: {
|
|
73
|
+
build: 'proman build',
|
|
74
|
+
test: 'proman test',
|
|
75
|
+
check: 'proman check',
|
|
76
|
+
format: 'proman format',
|
|
77
|
+
},
|
|
78
|
+
devDependencies: {
|
|
79
|
+
'@biomejs/biome': '^2.4.16',
|
|
80
|
+
'@shazhou/proman': '^0.7.0',
|
|
81
|
+
'@types/node': '^22.0.0',
|
|
82
|
+
typescript: '^5.9.3',
|
|
83
|
+
vitest: '^4.1.8',
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
writeFileSync(join(targetDir, 'package.json'), `${jsonStringify(content)}\n`);
|
|
87
|
+
}
|
|
88
|
+
function createPromanYaml(targetDir, projectName) {
|
|
89
|
+
const content = `packages:
|
|
90
|
+
- name: '@${projectName}/core'
|
|
91
|
+
path: packages/core
|
|
92
|
+
type: lib
|
|
93
|
+
- name: '@${projectName}/cli'
|
|
94
|
+
path: packages/cli
|
|
95
|
+
type: cli
|
|
96
|
+
`;
|
|
97
|
+
writeFileSync(join(targetDir, 'proman.yaml'), content);
|
|
98
|
+
}
|
|
99
|
+
function createPnpmWorkspace(targetDir) {
|
|
100
|
+
const content = `packages:
|
|
101
|
+
- 'packages/*'
|
|
102
|
+
|
|
103
|
+
allowBuilds:
|
|
104
|
+
esbuild: true
|
|
105
|
+
`;
|
|
106
|
+
writeFileSync(join(targetDir, 'pnpm-workspace.yaml'), content);
|
|
107
|
+
}
|
|
108
|
+
function createBiomeJson(targetDir) {
|
|
109
|
+
const content = {
|
|
110
|
+
$schema: 'https://biomejs.dev/schemas/2.4.16/schema.json',
|
|
111
|
+
assist: { actions: { source: { organizeImports: 'on' } } },
|
|
112
|
+
linter: {
|
|
113
|
+
enabled: true,
|
|
114
|
+
rules: { recommended: true },
|
|
115
|
+
},
|
|
116
|
+
formatter: {
|
|
117
|
+
enabled: true,
|
|
118
|
+
indentStyle: 'space',
|
|
119
|
+
indentWidth: 2,
|
|
120
|
+
lineWidth: 100,
|
|
121
|
+
},
|
|
122
|
+
javascript: {
|
|
123
|
+
formatter: {
|
|
124
|
+
quoteStyle: 'single',
|
|
125
|
+
semicolons: 'asNeeded',
|
|
126
|
+
trailingCommas: 'all',
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
files: {
|
|
130
|
+
includes: ['**', '!**/dist', '!**/node_modules', '!**/tests/fixtures', '!.worktrees'],
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
writeFileSync(join(targetDir, 'biome.json'), `${jsonStringify(content)}\n`);
|
|
134
|
+
}
|
|
135
|
+
function createTsConfig(targetDir) {
|
|
136
|
+
const content = {
|
|
137
|
+
compilerOptions: {
|
|
138
|
+
target: 'ESNext',
|
|
139
|
+
module: 'ESNext',
|
|
140
|
+
moduleResolution: 'bundler',
|
|
141
|
+
strict: true,
|
|
142
|
+
skipLibCheck: true,
|
|
143
|
+
verbatimModuleSyntax: true,
|
|
144
|
+
esModuleInterop: true,
|
|
145
|
+
resolveJsonModule: true,
|
|
146
|
+
types: ['node'],
|
|
147
|
+
lib: ['ESNext'],
|
|
148
|
+
},
|
|
149
|
+
references: [{ path: './packages/core' }, { path: './packages/cli' }],
|
|
150
|
+
};
|
|
151
|
+
writeFileSync(join(targetDir, 'tsconfig.json'), `${jsonStringify(content)}\n`);
|
|
152
|
+
}
|
|
153
|
+
function createGitignore(targetDir) {
|
|
154
|
+
const content = `node_modules
|
|
155
|
+
dist
|
|
156
|
+
.proman
|
|
157
|
+
*.tsbuildinfo
|
|
158
|
+
`;
|
|
159
|
+
writeFileSync(join(targetDir, '.gitignore'), content);
|
|
160
|
+
}
|
|
161
|
+
function createCorePackage(targetDir, projectName) {
|
|
162
|
+
const pkgDir = join(targetDir, 'packages', 'core');
|
|
163
|
+
mkdirSync(join(pkgDir, 'src'), { recursive: true });
|
|
164
|
+
// package.json
|
|
165
|
+
const packageJson = {
|
|
166
|
+
name: `@${projectName}/core`,
|
|
167
|
+
version: '0.0.1',
|
|
168
|
+
type: 'module',
|
|
169
|
+
exports: {
|
|
170
|
+
'.': {
|
|
171
|
+
types: './dist/index.d.ts',
|
|
172
|
+
default: './dist/index.js',
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
files: ['dist'],
|
|
176
|
+
scripts: {
|
|
177
|
+
build: 'tsc --build',
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
writeFileSync(join(pkgDir, 'package.json'), `${jsonStringify(packageJson)}\n`);
|
|
181
|
+
// tsconfig.json
|
|
182
|
+
const tsConfig = {
|
|
183
|
+
extends: '../../tsconfig.json',
|
|
184
|
+
compilerOptions: {
|
|
185
|
+
composite: true,
|
|
186
|
+
outDir: 'dist',
|
|
187
|
+
rootDir: 'src',
|
|
188
|
+
noEmit: false,
|
|
189
|
+
declaration: true,
|
|
190
|
+
},
|
|
191
|
+
include: ['src/**/*'],
|
|
192
|
+
};
|
|
193
|
+
writeFileSync(join(pkgDir, 'tsconfig.json'), `${jsonStringify(tsConfig)}\n`);
|
|
194
|
+
// src/index.ts
|
|
195
|
+
const indexTs = `export function hello(): string {
|
|
196
|
+
return 'hello'
|
|
197
|
+
}
|
|
198
|
+
`;
|
|
199
|
+
writeFileSync(join(pkgDir, 'src', 'index.ts'), indexTs);
|
|
200
|
+
// src/index.test.ts
|
|
201
|
+
const testTs = `import { describe, expect, test } from 'vitest'
|
|
202
|
+
import { hello } from './index.js'
|
|
203
|
+
|
|
204
|
+
describe('hello', () => {
|
|
205
|
+
test('returns hello', () => {
|
|
206
|
+
expect(hello()).toBe('hello')
|
|
207
|
+
})
|
|
208
|
+
})
|
|
209
|
+
`;
|
|
210
|
+
writeFileSync(join(pkgDir, 'src', 'index.test.ts'), testTs);
|
|
211
|
+
}
|
|
212
|
+
function createCliPackage(targetDir, projectName) {
|
|
213
|
+
const pkgDir = join(targetDir, 'packages', 'cli');
|
|
214
|
+
mkdirSync(join(pkgDir, 'src'), { recursive: true });
|
|
215
|
+
// package.json
|
|
216
|
+
const packageJson = {
|
|
217
|
+
name: `@${projectName}/cli`,
|
|
218
|
+
version: '0.0.1',
|
|
219
|
+
type: 'module',
|
|
220
|
+
bin: {
|
|
221
|
+
[projectName]: 'dist/cli.js',
|
|
222
|
+
},
|
|
223
|
+
files: ['dist'],
|
|
224
|
+
scripts: {
|
|
225
|
+
build: 'tsc --build',
|
|
226
|
+
},
|
|
227
|
+
dependencies: {
|
|
228
|
+
[`@${projectName}/core`]: 'workspace:*',
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
writeFileSync(join(pkgDir, 'package.json'), `${jsonStringify(packageJson)}\n`);
|
|
232
|
+
// tsconfig.json
|
|
233
|
+
const tsConfig = {
|
|
234
|
+
extends: '../../tsconfig.json',
|
|
235
|
+
compilerOptions: {
|
|
236
|
+
composite: true,
|
|
237
|
+
outDir: 'dist',
|
|
238
|
+
rootDir: 'src',
|
|
239
|
+
noEmit: false,
|
|
240
|
+
},
|
|
241
|
+
include: ['src/**/*'],
|
|
242
|
+
references: [{ path: '../core' }],
|
|
243
|
+
};
|
|
244
|
+
writeFileSync(join(pkgDir, 'tsconfig.json'), `${jsonStringify(tsConfig)}\n`);
|
|
245
|
+
// src/cli.ts
|
|
246
|
+
const cliTs = `#!/usr/bin/env node
|
|
247
|
+
import { hello } from '@${projectName}/core'
|
|
248
|
+
|
|
249
|
+
console.log(hello())
|
|
250
|
+
`;
|
|
251
|
+
writeFileSync(join(pkgDir, 'src', 'cli.ts'), cliTs);
|
|
252
|
+
// src/cli.test.ts
|
|
253
|
+
const testTs = `import { describe, expect, test } from 'vitest'
|
|
254
|
+
|
|
255
|
+
describe('cli', () => {
|
|
256
|
+
test('placeholder test', () => {
|
|
257
|
+
expect(true).toBe(true)
|
|
258
|
+
})
|
|
259
|
+
})
|
|
260
|
+
`;
|
|
261
|
+
writeFileSync(join(pkgDir, 'src', 'cli.test.ts'), testTs);
|
|
262
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type SpawnFn } from '../utils/npm.js';
|
|
2
|
+
export type LinkCommandOptions = {
|
|
3
|
+
cwd: string;
|
|
4
|
+
packageName?: string;
|
|
5
|
+
spawn?: SpawnFn;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Link a package globally (provider mode) or link from global registry (consumer mode)
|
|
9
|
+
*/
|
|
10
|
+
export declare function link(opts: LinkCommandOptions): Promise<void>;
|
|
11
|
+
/**
|
|
12
|
+
* Show currently linked packages
|
|
13
|
+
*/
|
|
14
|
+
export declare function linkStatus(opts: Omit<LinkCommandOptions, 'packageName'>): Promise<string>;
|
|
15
|
+
/**
|
|
16
|
+
* Unlink packages (all or specific)
|
|
17
|
+
*/
|
|
18
|
+
export declare function unlink(opts: LinkCommandOptions): Promise<void>;
|
|
19
|
+
//# sourceMappingURL=link.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"link.d.ts","sourceRoot":"","sources":["../../src/commands/link.ts"],"names":[],"mappings":"AAEA,OAAO,EAA4B,KAAK,OAAO,EAAE,MAAM,iBAAiB,CAAA;AAExE,MAAM,MAAM,kBAAkB,GAAG;IAC/B,GAAG,EAAE,MAAM,CAAA;IACX,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB,CAAA;AA0BD;;GAEG;AACH,wBAAsB,IAAI,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CA+BlE;AAED;;GAEG;AACH,wBAAsB,UAAU,CAAC,IAAI,EAAE,IAAI,CAAC,kBAAkB,EAAE,aAAa,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,CAoD/F;AAED;;GAEG;AACH,wBAAsB,MAAM,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAgEpE"}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { existsSync, lstatSync, readdirSync, readFileSync, readlinkSync } from 'node:fs';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
import { defaultSpawn, runOrThrow } from '../utils/npm.js';
|
|
4
|
+
function readPackageJson(cwd) {
|
|
5
|
+
const pkgPath = join(cwd, 'package.json');
|
|
6
|
+
if (!existsSync(pkgPath)) {
|
|
7
|
+
throw new Error(`Missing package.json in ${cwd}`);
|
|
8
|
+
}
|
|
9
|
+
const json = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
10
|
+
// Runtime validation: ensure parsed value is an object (not array, primitive, or null)
|
|
11
|
+
if (typeof json !== 'object' || json === null || Array.isArray(json)) {
|
|
12
|
+
throw new Error(`Invalid package.json at ${pkgPath}: expected a JSON object`);
|
|
13
|
+
}
|
|
14
|
+
return json;
|
|
15
|
+
}
|
|
16
|
+
function hasDistFolder(cwd) {
|
|
17
|
+
const distPath = join(cwd, 'dist');
|
|
18
|
+
return existsSync(distPath);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Link a package globally (provider mode) or link from global registry (consumer mode)
|
|
22
|
+
*/
|
|
23
|
+
export async function link(opts) {
|
|
24
|
+
const spawn = opts.spawn ?? defaultSpawn;
|
|
25
|
+
const cwd = resolve(opts.cwd);
|
|
26
|
+
// Consumer mode: link specific package from global registry
|
|
27
|
+
if (opts.packageName) {
|
|
28
|
+
const pkg = readPackageJson(cwd);
|
|
29
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
30
|
+
if (!allDeps[opts.packageName]) {
|
|
31
|
+
throw new Error(`Package "${opts.packageName}" not found in dependencies or devDependencies of ${cwd}`);
|
|
32
|
+
}
|
|
33
|
+
await runOrThrow(spawn, ['pnpm', 'link', '--global', opts.packageName], cwd);
|
|
34
|
+
console.log(`✓ Linked ${opts.packageName} from global registry`);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
// Provider mode: link current package globally
|
|
38
|
+
const pkg = readPackageJson(cwd);
|
|
39
|
+
if (!pkg.name) {
|
|
40
|
+
throw new Error(`package.json in ${cwd} is missing a "name" field`);
|
|
41
|
+
}
|
|
42
|
+
if (!hasDistFolder(cwd)) {
|
|
43
|
+
throw new Error(`No dist/ folder in ${cwd}. Run \`proman build\` first.`);
|
|
44
|
+
}
|
|
45
|
+
await runOrThrow(spawn, ['pnpm', 'link', '--global'], cwd);
|
|
46
|
+
console.log(`✓ Linked ${pkg.name} globally`);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Show currently linked packages
|
|
50
|
+
*/
|
|
51
|
+
export async function linkStatus(opts) {
|
|
52
|
+
const cwd = resolve(opts.cwd);
|
|
53
|
+
const nodeModulesDir = join(cwd, 'node_modules');
|
|
54
|
+
if (!existsSync(nodeModulesDir)) {
|
|
55
|
+
return 'No linked packages found';
|
|
56
|
+
}
|
|
57
|
+
const linkedPackages = [];
|
|
58
|
+
// Scan node_modules for symlinks
|
|
59
|
+
function scanDir(dir, prefix = '') {
|
|
60
|
+
if (!existsSync(dir))
|
|
61
|
+
return;
|
|
62
|
+
for (const entry of readdirSync(dir)) {
|
|
63
|
+
const fullPath = join(dir, entry);
|
|
64
|
+
const stat = lstatSync(fullPath);
|
|
65
|
+
// Check if it's a symlink
|
|
66
|
+
if (stat.isSymbolicLink()) {
|
|
67
|
+
const target = readlinkSync(fullPath);
|
|
68
|
+
const resolvedTarget = resolve(dir, target);
|
|
69
|
+
const pkgJsonPath = join(fullPath, 'package.json');
|
|
70
|
+
if (existsSync(pkgJsonPath)) {
|
|
71
|
+
try {
|
|
72
|
+
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
|
|
73
|
+
const name = pkgJson.name || `${prefix}${entry}`;
|
|
74
|
+
linkedPackages.push({ name, target: resolvedTarget });
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// Invalid package.json, skip
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
else if (stat.isDirectory() && entry.startsWith('@')) {
|
|
82
|
+
// Scan scoped packages
|
|
83
|
+
scanDir(fullPath, `${entry}/`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
scanDir(nodeModulesDir);
|
|
88
|
+
if (linkedPackages.length === 0) {
|
|
89
|
+
return 'No linked packages found';
|
|
90
|
+
}
|
|
91
|
+
const lines = ['Linked packages:'];
|
|
92
|
+
for (const { name, target } of linkedPackages) {
|
|
93
|
+
lines.push(`• ${name} → ${target}`);
|
|
94
|
+
}
|
|
95
|
+
return lines.join('\n');
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Unlink packages (all or specific)
|
|
99
|
+
*/
|
|
100
|
+
export async function unlink(opts) {
|
|
101
|
+
const spawn = opts.spawn ?? defaultSpawn;
|
|
102
|
+
const cwd = resolve(opts.cwd);
|
|
103
|
+
// Unlink specific package
|
|
104
|
+
if (opts.packageName) {
|
|
105
|
+
await runOrThrow(spawn, ['pnpm', 'unlink', opts.packageName], cwd);
|
|
106
|
+
await runOrThrow(spawn, ['pnpm', 'install', opts.packageName], cwd);
|
|
107
|
+
console.log(`✓ Unlinked ${opts.packageName} and restored from registry`);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
// Unlink all packages
|
|
111
|
+
const nodeModulesDir = join(cwd, 'node_modules');
|
|
112
|
+
if (!existsSync(nodeModulesDir)) {
|
|
113
|
+
console.log('No linked packages to unlink');
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const linkedPackages = [];
|
|
117
|
+
// Find all symlinked packages
|
|
118
|
+
function scanDir(dir) {
|
|
119
|
+
if (!existsSync(dir))
|
|
120
|
+
return;
|
|
121
|
+
for (const entry of readdirSync(dir)) {
|
|
122
|
+
const fullPath = join(dir, entry);
|
|
123
|
+
const stat = lstatSync(fullPath);
|
|
124
|
+
if (stat.isSymbolicLink()) {
|
|
125
|
+
const pkgJsonPath = join(fullPath, 'package.json');
|
|
126
|
+
if (existsSync(pkgJsonPath)) {
|
|
127
|
+
try {
|
|
128
|
+
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
|
|
129
|
+
if (pkgJson.name) {
|
|
130
|
+
linkedPackages.push(pkgJson.name);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// Invalid package.json, skip
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
else if (stat.isDirectory() && entry.startsWith('@')) {
|
|
139
|
+
scanDir(fullPath);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
scanDir(nodeModulesDir);
|
|
144
|
+
if (linkedPackages.length === 0) {
|
|
145
|
+
console.log('No linked packages to unlink');
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
// Unlink each package
|
|
149
|
+
for (const pkgName of linkedPackages) {
|
|
150
|
+
await runOrThrow(spawn, ['pnpm', 'unlink', pkgName], cwd);
|
|
151
|
+
}
|
|
152
|
+
// Restore all packages
|
|
153
|
+
await runOrThrow(spawn, ['pnpm', 'install'], cwd);
|
|
154
|
+
console.log(`✓ Unlinked ${linkedPackages.length} package(s) and restored from registry`);
|
|
155
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type GitOps } from '../utils/git.js';
|
|
2
|
+
import { type NpmRegistryFetch, type NpmRunner, type SpawnFn } from '../utils/npm.js';
|
|
3
|
+
export type { GitOps } from '../utils/git.js';
|
|
4
|
+
export type { NpmRunner } from '../utils/npm.js';
|
|
5
|
+
export type PublishOptions = {
|
|
6
|
+
skipTests?: boolean;
|
|
7
|
+
cwd?: string;
|
|
8
|
+
git?: GitOps;
|
|
9
|
+
npm?: NpmRunner;
|
|
10
|
+
registryFetch?: NpmRegistryFetch;
|
|
11
|
+
spawn?: SpawnFn;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Publish all packages. Reads each package's version from its own package.json.
|
|
15
|
+
* build → test → check → smoke test tarball → publish → commit → tag → push
|
|
16
|
+
*/
|
|
17
|
+
export declare function publish(opts?: PublishOptions): Promise<void>;
|
|
18
|
+
//# sourceMappingURL=publish.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"publish.d.ts","sourceRoot":"","sources":["../../src/commands/publish.ts"],"names":[],"mappings":"AAGA,OAAO,EAAgB,KAAK,MAAM,EAAE,MAAM,iBAAiB,CAAA;AAC3D,OAAO,EAIL,KAAK,gBAAgB,EACrB,KAAK,SAAS,EACd,KAAK,OAAO,EACb,MAAM,iBAAiB,CAAA;AAGxB,YAAY,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AAC7C,YAAY,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAEhD,MAAM,MAAM,cAAc,GAAG;IAC3B,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,SAAS,CAAA;IACf,aAAa,CAAC,EAAE,gBAAgB,CAAA;IAChC,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB,CAAA;AAoBD;;;GAGG;AACH,wBAAsB,OAAO,CAAC,IAAI,GAAE,cAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CAuHtE"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { loadConfig } from '../config/load-config.js';
|
|
4
|
+
import { createGitOps } from '../utils/git.js';
|
|
5
|
+
import { createNpmRunner, defaultRegistryFetch, defaultSpawn, } from '../utils/npm.js';
|
|
6
|
+
import { smokeTestTarball } from '../utils/smoke-test.js';
|
|
7
|
+
const AUTHOR = '小橘 <xiaoju@shazhou.work>';
|
|
8
|
+
async function readJson(path) {
|
|
9
|
+
const text = await readFile(path, 'utf8');
|
|
10
|
+
return JSON.parse(text);
|
|
11
|
+
}
|
|
12
|
+
function isRcVersion(version) {
|
|
13
|
+
return /-rc\.\d+$/.test(version);
|
|
14
|
+
}
|
|
15
|
+
const ALREADY_PUBLISHED_RE = /cannot publish over the previously published versions|you cannot publish over the previously published version/i;
|
|
16
|
+
function isAlreadyPublished(message) {
|
|
17
|
+
return ALREADY_PUBLISHED_RE.test(message);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Publish all packages. Reads each package's version from its own package.json.
|
|
21
|
+
* build → test → check → smoke test tarball → publish → commit → tag → push
|
|
22
|
+
*/
|
|
23
|
+
export async function publish(opts = {}) {
|
|
24
|
+
const { skipTests = false } = opts;
|
|
25
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
26
|
+
const git = opts.git ?? createGitOps(cwd);
|
|
27
|
+
const fetchVersions = opts.registryFetch ?? defaultRegistryFetch;
|
|
28
|
+
const spawn = opts.spawn ?? defaultSpawn;
|
|
29
|
+
const cfg = loadConfig(cwd);
|
|
30
|
+
const npm = opts.npm ?? createNpmRunner(cwd);
|
|
31
|
+
const pkgJsonMap = {};
|
|
32
|
+
for (const pkg of cfg.packages) {
|
|
33
|
+
const pkgPath = resolve(cwd, pkg.path, 'package.json');
|
|
34
|
+
const json = await readJson(pkgPath);
|
|
35
|
+
const version = json.version;
|
|
36
|
+
if (!version)
|
|
37
|
+
throw new Error(`missing version in ${pkgPath}`);
|
|
38
|
+
pkgJsonMap[pkg.name] = { version, private: json.private === true };
|
|
39
|
+
}
|
|
40
|
+
const publishablePackages = cfg.packages.filter((pkg) => pkg.private !== true && pkgJsonMap[pkg.name]?.private !== true);
|
|
41
|
+
// Read each publishable package's version
|
|
42
|
+
const versions = {};
|
|
43
|
+
for (const pkg of publishablePackages) {
|
|
44
|
+
versions[pkg.name] = pkgJsonMap[pkg.name]?.version;
|
|
45
|
+
}
|
|
46
|
+
// Build + test + check
|
|
47
|
+
await npm.install();
|
|
48
|
+
await npm.build();
|
|
49
|
+
console.log('✓ build');
|
|
50
|
+
if (!skipTests) {
|
|
51
|
+
await npm.test();
|
|
52
|
+
console.log('✓ test');
|
|
53
|
+
}
|
|
54
|
+
await npm.check();
|
|
55
|
+
console.log('✓ check');
|
|
56
|
+
// Log skipped private packages
|
|
57
|
+
for (const pkg of cfg.packages) {
|
|
58
|
+
if (pkg.private === true || pkgJsonMap[pkg.name]?.private === true) {
|
|
59
|
+
console.log(`⏭ skipped ${pkg.name} (private)`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Publish each publishable package
|
|
63
|
+
const access = cfg.release?.access;
|
|
64
|
+
for (let i = 0; i < publishablePackages.length; i++) {
|
|
65
|
+
const entry = publishablePackages[i];
|
|
66
|
+
const version = versions[entry.name];
|
|
67
|
+
const isRc = isRcVersion(version);
|
|
68
|
+
const publishTag = isRc ? 'rc' : 'latest';
|
|
69
|
+
const pkgDir = resolve(cwd, entry.path);
|
|
70
|
+
// Pre-check: skip if already published on registry
|
|
71
|
+
const existingVersions = await fetchVersions(entry.name);
|
|
72
|
+
if (existingVersions.includes(version)) {
|
|
73
|
+
console.log(`⏭ skipped ${entry.name}@${version} (already published)`);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
// Smoke test: validate tarball before publishing
|
|
77
|
+
try {
|
|
78
|
+
await smokeTestTarball(pkgDir, spawn);
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
const message = err.message;
|
|
82
|
+
const published = publishablePackages.slice(0, i).map((p) => p.name);
|
|
83
|
+
const remaining = publishablePackages.slice(i + 1).map((p) => p.name);
|
|
84
|
+
const msg = `smoke test failed for ${entry.name}: ${message}\n` +
|
|
85
|
+
` published: ${published.join(', ') || '(none)'}\n` +
|
|
86
|
+
` unpublished: ${[entry.name, ...remaining].join(', ')}`;
|
|
87
|
+
throw new Error(msg);
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
await npm.publish(pkgDir, { tag: publishTag, ...(access ? { access } : {}) });
|
|
91
|
+
console.log(`✓ published ${entry.name}@${version}`);
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
const message = err.message;
|
|
95
|
+
// Fallback: catch the error in case of race condition
|
|
96
|
+
if (isAlreadyPublished(message)) {
|
|
97
|
+
console.log(`⏭ skipped ${entry.name}@${version} (already published)`);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const published = publishablePackages.slice(0, i).map((p) => p.name);
|
|
101
|
+
const remaining = publishablePackages.slice(i + 1).map((p) => p.name);
|
|
102
|
+
const msg = `publish failed for ${entry.name}: ${message}\n` +
|
|
103
|
+
` published: ${published.join(', ') || '(none)'}\n` +
|
|
104
|
+
` unpublished: ${[entry.name, ...remaining].join(', ')}`;
|
|
105
|
+
throw new Error(msg);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Commit + tag + push all publishable packages
|
|
109
|
+
// Changelog generation and changeset cleanup are now bump's responsibility (issue #74)
|
|
110
|
+
await git.addAll();
|
|
111
|
+
const tagPrefix = cfg.release?.gitTagPrefix ?? 'v';
|
|
112
|
+
const bumpedVersions = Object.entries(versions);
|
|
113
|
+
const commitVersion = bumpedVersions[0]?.[1] ?? 'unknown';
|
|
114
|
+
await git.commit(`release: v${commitVersion}`, AUTHOR);
|
|
115
|
+
for (const [pkgName, version] of bumpedVersions) {
|
|
116
|
+
const tagName = `${pkgName}@${tagPrefix}${version}`;
|
|
117
|
+
await git.tag(tagName, `Release ${pkgName}@${version}`);
|
|
118
|
+
}
|
|
119
|
+
await git.pushTags();
|
|
120
|
+
await git.push('main');
|
|
121
|
+
for (const [pkgName, version] of bumpedVersions) {
|
|
122
|
+
console.log(`✓ tagged ${pkgName}@${tagPrefix}${version}`);
|
|
123
|
+
}
|
|
124
|
+
console.log(`✓ pushed`);
|
|
125
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/config/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAC7C,YAAY,EACV,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,aAAa,GACd,MAAM,YAAY,CAAA;AACnB,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"load-config.d.ts","sourceRoot":"","sources":["../../src/config/load-config.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAiB,MAAM,YAAY,CAAA;AAiB7D;;GAEG;AACH,wBAAgB,UAAU,CAAC,GAAG,GAAE,MAAsB,GAAG,YAAY,CASpE"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { parse } from 'yaml';
|
|
4
|
+
import { validateConfig } from './validate-config.js';
|
|
5
|
+
const DEFAULT_REGISTRY = 'https://registry.npmjs.org';
|
|
6
|
+
const DEFAULT_GIT_TAG_PREFIX = 'v';
|
|
7
|
+
function applyDefaults(config) {
|
|
8
|
+
const release = {
|
|
9
|
+
registry: config.release?.registry ?? DEFAULT_REGISTRY,
|
|
10
|
+
gitTagPrefix: config.release?.gitTagPrefix ?? DEFAULT_GIT_TAG_PREFIX,
|
|
11
|
+
};
|
|
12
|
+
if (config.release?.access !== undefined) {
|
|
13
|
+
release.access = config.release.access;
|
|
14
|
+
}
|
|
15
|
+
return { ...config, release };
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Loads `proman.yaml` from the given cwd (or `process.cwd()`).
|
|
19
|
+
*/
|
|
20
|
+
export function loadConfig(cwd = process.cwd()) {
|
|
21
|
+
const absPath = resolve(cwd, 'proman.yaml');
|
|
22
|
+
if (!existsSync(absPath)) {
|
|
23
|
+
throw new Error(`proman.yaml not found at ${absPath}`);
|
|
24
|
+
}
|
|
25
|
+
const text = readFileSync(absPath, 'utf8');
|
|
26
|
+
const raw = parse(text);
|
|
27
|
+
const validated = validateConfig(raw);
|
|
28
|
+
return applyDefaults(validated);
|
|
29
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type PackageType = 'lib' | 'cli' | 'webui' | 'api';
|
|
2
|
+
export type PackageEntry = {
|
|
3
|
+
name: string;
|
|
4
|
+
path: string;
|
|
5
|
+
type: PackageType;
|
|
6
|
+
private?: boolean;
|
|
7
|
+
};
|
|
8
|
+
export type ReleaseConfig = {
|
|
9
|
+
registry?: string;
|
|
10
|
+
access?: 'public' | 'restricted';
|
|
11
|
+
gitTagPrefix?: string;
|
|
12
|
+
};
|
|
13
|
+
export type PromanConfig = {
|
|
14
|
+
packages: PackageEntry[];
|
|
15
|
+
release?: ReleaseConfig;
|
|
16
|
+
};
|
|
17
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/config/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,GAAG,KAAK,GAAG,KAAK,GAAG,OAAO,GAAG,KAAK,CAAA;AAEzD,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,WAAW,CAAA;IACjB,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB,CAAA;AAED,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,MAAM,CAAC,EAAE,QAAQ,GAAG,YAAY,CAAA;IAChC,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IACzB,QAAQ,EAAE,YAAY,EAAE,CAAA;IACxB,OAAO,CAAC,EAAE,aAAa,CAAA;CACxB,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { PromanConfig } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Pure validator. Throws Error with descriptive message on failure.
|
|
4
|
+
* Returns a typed PromanConfig (does not mutate input, does not apply defaults).
|
|
5
|
+
*/
|
|
6
|
+
export declare function validateConfig(value: unknown): PromanConfig;
|
|
7
|
+
//# sourceMappingURL=validate-config.d.ts.map
|