@emeryld/manager 0.4.5 → 0.5.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/README.md +1 -1
- package/dist/create-package/index.js +53 -2
- package/dist/create-package/shared.js +92 -22
- package/dist/create-package/variants/client.js +34 -17
- package/dist/create-package/variants/contract.js +9 -5
- package/dist/create-package/variants/docker.js +9 -5
- package/dist/create-package/variants/empty.js +12 -5
- package/dist/create-package/variants/fullstack.js +54 -10
- package/dist/create-package/variants/server.js +44 -29
- package/dist/packages.js +86 -54
- package/dist/publish.js +2 -2
- package/dist/release.js +3 -3
- package/dist/utils/log.js +1 -1
- package/dist/workspace.js +40 -24
- package/package.json +2 -3
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ Use `pnpm manager-cli create --describe <variant>` to print the scripts, key fil
|
|
|
30
30
|
- `--skip-install` / `--skip-build` — bypass post-create steps when you want to install/build later
|
|
31
31
|
|
|
32
32
|
## Release helper (existing packages)
|
|
33
|
-
- Run from the workspace root
|
|
33
|
+
- Run from the workspace root; the CLI discovers any workspace packages with a `package.json`.
|
|
34
34
|
- `pnpm manager-cli` launches an interactive menu: pick packages, then run update → test → build → publish (or any single step).
|
|
35
35
|
- Non-interactive publish: `pnpm manager-cli <pkg|all> --non-interactive --bump patch` (see CLI prompts for flags like `--sync` and `--tag`).
|
|
36
36
|
|
|
@@ -4,7 +4,8 @@ import path from 'node:path';
|
|
|
4
4
|
import { stdin as input } from 'node:process';
|
|
5
5
|
import { askLine, promptSingleKey } from '../prompts.js';
|
|
6
6
|
import { colors, logGlobal } from '../utils/log.js';
|
|
7
|
-
import {
|
|
7
|
+
import { loadPackages } from '../packages.js';
|
|
8
|
+
import { SCRIPT_DESCRIPTIONS, workspaceRoot, ensureWorkspaceToolingFiles, } from './shared.js';
|
|
8
9
|
import { clientVariant } from './variants/client.js';
|
|
9
10
|
import { contractVariant } from './variants/contract.js';
|
|
10
11
|
import { dockerVariant } from './variants/docker.js';
|
|
@@ -24,6 +25,49 @@ function derivePackageName(targetDir) {
|
|
|
24
25
|
const base = path.basename(targetDir) || 'rrr-package';
|
|
25
26
|
return base;
|
|
26
27
|
}
|
|
28
|
+
async function promptForContractName() {
|
|
29
|
+
let packages = [];
|
|
30
|
+
try {
|
|
31
|
+
packages = await loadPackages();
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
const message = error instanceof Error ? error.message : 'unknown error discovering packages';
|
|
35
|
+
logGlobal(`Could not auto-discover packages (${message}).`, colors.yellow);
|
|
36
|
+
}
|
|
37
|
+
const candidates = packages
|
|
38
|
+
.filter((pkg) => pkg.relativeDir !== '.')
|
|
39
|
+
.map((pkg, index) => ({
|
|
40
|
+
label: `${pkg.name}${pkg.relativeDir ? colors.dim(` (${pkg.relativeDir})`) : ''}`,
|
|
41
|
+
value: pkg.name,
|
|
42
|
+
index,
|
|
43
|
+
}));
|
|
44
|
+
if (candidates.length === 0) {
|
|
45
|
+
const manual = await askLine('Contract package name? (Enter to skip, e.g. @scope/contract): ');
|
|
46
|
+
return manual.trim() || undefined;
|
|
47
|
+
}
|
|
48
|
+
console.log(colors.magenta('Select a contract package (or skip):'));
|
|
49
|
+
candidates.forEach((opt, idx) => {
|
|
50
|
+
const prefix = colors.cyan(String(idx + 1).padStart(2, ' '));
|
|
51
|
+
console.log(` [${prefix}] ${opt.label}`);
|
|
52
|
+
});
|
|
53
|
+
console.log(` [${colors.cyan('m ')}] Enter manually`);
|
|
54
|
+
console.log(` [${colors.cyan('s ')}] Skip (no contract)`);
|
|
55
|
+
// eslint-disable-next-line no-constant-condition
|
|
56
|
+
while (true) {
|
|
57
|
+
const choice = (await askLine('Choice: ')).trim().toLowerCase();
|
|
58
|
+
if (!choice || choice === 's')
|
|
59
|
+
return undefined;
|
|
60
|
+
if (choice === 'm') {
|
|
61
|
+
const manual = await askLine('Contract package name (e.g. @scope/contract): ');
|
|
62
|
+
return manual.trim() || undefined;
|
|
63
|
+
}
|
|
64
|
+
const idx = Number.parseInt(choice, 10);
|
|
65
|
+
if (Number.isInteger(idx) && idx >= 1 && idx <= candidates.length) {
|
|
66
|
+
return candidates[idx - 1].value;
|
|
67
|
+
}
|
|
68
|
+
console.log(colors.yellow('Enter a number, "m" to type a name, or "s" to skip.'));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
27
71
|
function resolveVariant(key) {
|
|
28
72
|
if (!key)
|
|
29
73
|
return undefined;
|
|
@@ -372,17 +416,24 @@ async function gatherTarget(initial = {}) {
|
|
|
372
416
|
: initial.pkgName;
|
|
373
417
|
const pkgName = (nameAnswer || fallbackName).trim() || fallbackName;
|
|
374
418
|
await ensureTargetDir(targetDir, { reset: initial.reset });
|
|
419
|
+
let contractName = initial.contractName;
|
|
420
|
+
if ((variant.id === 'rrr-server' || variant.id === 'rrr-client') &&
|
|
421
|
+
contractName === undefined) {
|
|
422
|
+
contractName = await promptForContractName();
|
|
423
|
+
}
|
|
375
424
|
return {
|
|
376
425
|
variant,
|
|
377
426
|
targetDir,
|
|
378
427
|
pkgName,
|
|
379
|
-
contractName
|
|
428
|
+
contractName,
|
|
380
429
|
reset: initial.reset,
|
|
381
430
|
};
|
|
382
431
|
}
|
|
383
432
|
export async function createRrrPackage(options = {}) {
|
|
384
433
|
const target = await gatherTarget(options);
|
|
385
434
|
logGlobal(`Creating ${target.variant.label} in ${path.relative(workspaceRoot, target.targetDir) || '.'}`, colors.green);
|
|
435
|
+
const toolingRoot = target.variant.id === 'rrr-fullstack' ? target.targetDir : workspaceRoot;
|
|
436
|
+
await ensureWorkspaceToolingFiles(toolingRoot);
|
|
386
437
|
await target.variant.scaffold({
|
|
387
438
|
targetDir: target.targetDir,
|
|
388
439
|
pkgName: target.pkgName,
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { access, mkdir, writeFile } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { promptYesNoAll } from '../prompts.js';
|
|
4
5
|
export const workspaceRoot = process.cwd();
|
|
6
|
+
export function isWorkspaceRoot(dir) {
|
|
7
|
+
return path.resolve(dir) === path.resolve(workspaceRoot);
|
|
8
|
+
}
|
|
5
9
|
function pathExists(target) {
|
|
6
10
|
return access(target)
|
|
7
11
|
.then(() => true)
|
|
@@ -31,26 +35,45 @@ export async function writeFileIfMissing(baseDir, relative, contents) {
|
|
|
31
35
|
console.log(` created ${rel}`);
|
|
32
36
|
return 'created';
|
|
33
37
|
}
|
|
38
|
+
export async function writeFileWithPrompt(baseDir, relative, contents) {
|
|
39
|
+
const fullPath = path.join(baseDir, relative);
|
|
40
|
+
await mkdir(path.dirname(fullPath), { recursive: true });
|
|
41
|
+
const rel = path.relative(workspaceRoot, fullPath);
|
|
42
|
+
const exists = await pathExists(fullPath);
|
|
43
|
+
if (exists) {
|
|
44
|
+
const answer = await promptYesNoAll(`Overwrite existing ${rel}?`);
|
|
45
|
+
if (answer !== 'yes') {
|
|
46
|
+
console.log(` kept existing ${rel}`);
|
|
47
|
+
return 'skipped';
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
await writeFile(fullPath, contents, 'utf8');
|
|
51
|
+
console.log(` ${exists ? 'updated' : 'created'} ${rel}`);
|
|
52
|
+
return exists ? 'updated' : 'created';
|
|
53
|
+
}
|
|
34
54
|
export function baseTsConfig(options) {
|
|
35
55
|
const config = {
|
|
36
56
|
...(options?.extends ? { extends: options.extends } : {}),
|
|
37
57
|
compilerOptions: {
|
|
38
58
|
target: 'ES2020',
|
|
39
|
-
module: '
|
|
40
|
-
moduleResolution: '
|
|
41
|
-
outDir: options
|
|
42
|
-
rootDir: options
|
|
43
|
-
declaration: true,
|
|
44
|
-
sourceMap: true,
|
|
45
|
-
strict: true,
|
|
46
|
-
esModuleInterop: true,
|
|
47
|
-
skipLibCheck: true,
|
|
59
|
+
module: 'ESNext',
|
|
60
|
+
moduleResolution: 'Bundler',
|
|
61
|
+
...(options?.outDir ? { outDir: options.outDir } : {}),
|
|
62
|
+
...(options?.rootDir ? { rootDir: options.rootDir } : {}),
|
|
63
|
+
declaration: options?.declaration ?? true,
|
|
64
|
+
sourceMap: options?.sourceMap ?? true,
|
|
65
|
+
strict: options?.strict ?? true,
|
|
66
|
+
esModuleInterop: options?.esModuleInterop ?? true,
|
|
67
|
+
skipLibCheck: options?.skipLibCheck ?? true,
|
|
68
|
+
resolveJsonModule: options?.resolveJsonModule ?? true,
|
|
69
|
+
forceConsistentCasingInFileNames: options?.forceConsistentCasingInFileNames ?? true,
|
|
70
|
+
baseUrl: options?.baseUrl ?? '.',
|
|
48
71
|
lib: options?.lib,
|
|
49
72
|
types: options?.types,
|
|
50
73
|
jsx: options?.jsx,
|
|
51
74
|
},
|
|
52
|
-
include: options?.include
|
|
53
|
-
exclude: options?.exclude
|
|
75
|
+
include: options?.include,
|
|
76
|
+
exclude: options?.exclude,
|
|
54
77
|
};
|
|
55
78
|
return `${JSON.stringify(config, null, 2)}\n`;
|
|
56
79
|
}
|
|
@@ -145,7 +168,8 @@ const DEFAULT_GITIGNORE_ENTRIES = [
|
|
|
145
168
|
export function gitignoreFrom(entries = DEFAULT_GITIGNORE_ENTRIES) {
|
|
146
169
|
return entries.join('\n');
|
|
147
170
|
}
|
|
148
|
-
export function baseScripts(devCommand, extras) {
|
|
171
|
+
export function baseScripts(devCommand, extras, options) {
|
|
172
|
+
const includePrepare = options?.includePrepare ?? true;
|
|
149
173
|
return {
|
|
150
174
|
dev: devCommand,
|
|
151
175
|
build: 'tsc -p tsconfig.json',
|
|
@@ -157,18 +181,36 @@ export function baseScripts(devCommand, extras) {
|
|
|
157
181
|
'format:check': 'prettier . --check',
|
|
158
182
|
clean: 'rimraf dist .turbo coverage',
|
|
159
183
|
test: "node -e \"console.log('No tests yet')\"",
|
|
160
|
-
|
|
184
|
+
...(includePrepare
|
|
185
|
+
? { prepare: 'git rev-parse --is-inside-work-tree >/dev/null 2>&1 && husky || true' }
|
|
186
|
+
: {}),
|
|
161
187
|
...extras,
|
|
162
188
|
};
|
|
163
189
|
}
|
|
164
190
|
export function basePackageFiles(options) {
|
|
165
191
|
return {
|
|
166
|
-
'
|
|
192
|
+
'.gitignore': gitignoreFrom(options?.gitignoreEntries),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
export async function ensureWorkspaceToolingFiles(baseDir, options) {
|
|
196
|
+
const defaultTsconfig = path.join(baseDir, 'tsconfig.json');
|
|
197
|
+
const resolvedRootConfig = options?.tsconfigPath
|
|
198
|
+
? path.resolve(baseDir, options.tsconfigPath)
|
|
199
|
+
: await resolveRootTsconfig(baseDir);
|
|
200
|
+
const tsconfigPath = toPosixPath(path.relative(baseDir, resolvedRootConfig ?? defaultTsconfig) || './tsconfig.json');
|
|
201
|
+
const files = workspaceToolingFiles(tsconfigPath);
|
|
202
|
+
for (const [relative, contents] of Object.entries(files)) {
|
|
203
|
+
// eslint-disable-next-line no-await-in-loop
|
|
204
|
+
await writeFileWithPrompt(baseDir, relative, contents);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
export function workspaceToolingFiles(tsconfigPath) {
|
|
208
|
+
return {
|
|
209
|
+
'eslint.config.js': baseEslintConfig(tsconfigPath),
|
|
167
210
|
'prettier.config.js': basePrettierConfig(),
|
|
168
211
|
'.prettierignore': `${PRETTIER_IGNORE}\n`,
|
|
169
212
|
'.vscode/settings.json': vscodeSettings(),
|
|
170
213
|
'.husky/pre-commit': HUSKY_PRE_COMMIT,
|
|
171
|
-
'.gitignore': gitignoreFrom(options?.gitignoreEntries),
|
|
172
214
|
};
|
|
173
215
|
}
|
|
174
216
|
export const SCRIPT_DESCRIPTIONS = {
|
|
@@ -261,27 +303,55 @@ function readRootPackageManager() {
|
|
|
261
303
|
return undefined;
|
|
262
304
|
}
|
|
263
305
|
}
|
|
264
|
-
async function resolveRootTsconfig() {
|
|
306
|
+
async function resolveRootTsconfig(baseDir = workspaceRoot) {
|
|
265
307
|
const candidates = ['tsconfig.base.json', 'tsconfig.json'];
|
|
266
308
|
for (const candidate of candidates) {
|
|
267
|
-
const fullPath = path.join(
|
|
309
|
+
const fullPath = path.join(baseDir, candidate);
|
|
268
310
|
if (await pathExists(fullPath)) {
|
|
269
311
|
return fullPath;
|
|
270
312
|
}
|
|
271
313
|
}
|
|
272
314
|
return undefined;
|
|
273
315
|
}
|
|
316
|
+
async function findNearestTsconfig(startDir) {
|
|
317
|
+
let current = path.resolve(startDir);
|
|
318
|
+
// eslint-disable-next-line no-constant-condition
|
|
319
|
+
while (true) {
|
|
320
|
+
const found = await resolveRootTsconfig(current);
|
|
321
|
+
if (found)
|
|
322
|
+
return found;
|
|
323
|
+
const parent = path.dirname(current);
|
|
324
|
+
if (parent === current)
|
|
325
|
+
return undefined;
|
|
326
|
+
current = parent;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
274
329
|
export async function packageTsConfig(targetDir, options) {
|
|
275
330
|
const extendsFromRoot = options?.extendsFromRoot ?? true;
|
|
276
331
|
let extendsPath;
|
|
277
332
|
if (extendsFromRoot) {
|
|
278
|
-
const rootConfig = await
|
|
333
|
+
const rootConfig = await findNearestTsconfig(targetDir);
|
|
279
334
|
if (rootConfig) {
|
|
280
335
|
const relative = path.relative(targetDir, rootConfig);
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
}
|
|
336
|
+
const normalized = toPosixPath(relative || './tsconfig.base.json');
|
|
337
|
+
extendsPath = normalized.startsWith('.') ? normalized : `./${normalized}`;
|
|
284
338
|
}
|
|
285
339
|
}
|
|
286
|
-
|
|
340
|
+
const compilerOptions = stripUndefined({
|
|
341
|
+
rootDir: options?.rootDir ?? 'src',
|
|
342
|
+
outDir: options?.outDir ?? 'dist',
|
|
343
|
+
tsBuildInfoFile: 'dist/.tsbuildinfo',
|
|
344
|
+
jsx: options?.jsx,
|
|
345
|
+
types: options?.types,
|
|
346
|
+
lib: options?.lib,
|
|
347
|
+
esModuleInterop: options?.esModuleInterop ?? true,
|
|
348
|
+
allowSyntheticDefaultImports: options?.esModuleInterop ?? true,
|
|
349
|
+
skipLibCheck: options?.skipLibCheck ?? true,
|
|
350
|
+
});
|
|
351
|
+
const config = stripUndefined({
|
|
352
|
+
extends: extendsPath,
|
|
353
|
+
compilerOptions,
|
|
354
|
+
include: options?.include ?? ['src/**/*.ts'],
|
|
355
|
+
});
|
|
356
|
+
return `${JSON.stringify(config, null, 2)}\n`;
|
|
287
357
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BASE_LINT_DEV_DEPENDENCIES, basePackageFiles, basePackageJson, buildReadme, baseScripts, packageTsConfig, writeFileIfMissing, } from '../shared.js';
|
|
1
|
+
import { BASE_LINT_DEV_DEPENDENCIES, basePackageFiles, basePackageJson, buildReadme, baseScripts, packageTsConfig, isWorkspaceRoot, writeFileIfMissing, } from '../shared.js';
|
|
2
2
|
const CLIENT_SCRIPTS = [
|
|
3
3
|
'dev',
|
|
4
4
|
'build',
|
|
@@ -10,11 +10,19 @@ const CLIENT_SCRIPTS = [
|
|
|
10
10
|
'clean',
|
|
11
11
|
'test',
|
|
12
12
|
];
|
|
13
|
-
const CONTRACT_IMPORT_PLACEHOLDER = '@your-scope/contract';
|
|
14
13
|
export function clientIndexTs(contractImport) {
|
|
14
|
+
const contractImportLine = contractImport
|
|
15
|
+
? `import { registry } from '${contractImport}'`
|
|
16
|
+
: "// TODO: import { registry } from '@your-scope/contract'";
|
|
17
|
+
const routeExports = contractImport
|
|
18
|
+
? `export const healthGet = routeClient.build(registry.byKey['GET /api/health'])
|
|
19
|
+
export const healthPost = routeClient.build(registry.byKey['POST /api/health'])`
|
|
20
|
+
: `// TODO: add route builders after wiring your contract registry
|
|
21
|
+
// export const healthGet = routeClient.build(registry.byKey['GET /api/health'])
|
|
22
|
+
// export const healthPost = routeClient.build(registry.byKey['POST /api/health'])`;
|
|
15
23
|
return `import { QueryClient } from '@tanstack/react-query'
|
|
16
24
|
import { createRouteClient } from '@emeryld/rrroutes-client'
|
|
17
|
-
|
|
25
|
+
${contractImportLine}
|
|
18
26
|
|
|
19
27
|
const baseUrl = process.env.RRR_API_URL ?? 'http://localhost:4000'
|
|
20
28
|
export const queryClient = new QueryClient()
|
|
@@ -25,20 +33,23 @@ export const routeClient = createRouteClient({
|
|
|
25
33
|
environment: process.env.NODE_ENV === 'production' ? 'production' : 'development',
|
|
26
34
|
})
|
|
27
35
|
|
|
28
|
-
|
|
29
|
-
export const healthPost = routeClient.build(registry.byKey['POST /api/health'])
|
|
36
|
+
${routeExports}
|
|
30
37
|
`;
|
|
31
38
|
}
|
|
32
|
-
export function clientPackageJson(name, contractName
|
|
39
|
+
export function clientPackageJson(name, contractName, options) {
|
|
40
|
+
const dependencies = {
|
|
41
|
+
'@emeryld/rrroutes-client': '^2.5.3',
|
|
42
|
+
'@tanstack/react-query': '^5.90.12',
|
|
43
|
+
'socket.io-client': '^4.8.3',
|
|
44
|
+
};
|
|
45
|
+
if (contractName)
|
|
46
|
+
dependencies[contractName] = 'workspace:*';
|
|
33
47
|
return basePackageJson({
|
|
34
48
|
name,
|
|
35
|
-
scripts: baseScripts('tsx watch src/index.ts'
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
'@tanstack/react-query': '^5.90.12',
|
|
40
|
-
'socket.io-client': '^4.8.3',
|
|
41
|
-
},
|
|
49
|
+
scripts: baseScripts('tsx watch src/index.ts', undefined, {
|
|
50
|
+
includePrepare: options?.includePrepare,
|
|
51
|
+
}),
|
|
52
|
+
dependencies,
|
|
42
53
|
devDependencies: {
|
|
43
54
|
...BASE_LINT_DEV_DEPENDENCIES,
|
|
44
55
|
'@types/node': '^24.10.2',
|
|
@@ -46,12 +57,15 @@ export function clientPackageJson(name, contractName = CONTRACT_IMPORT_PLACEHOLD
|
|
|
46
57
|
});
|
|
47
58
|
}
|
|
48
59
|
async function clientFiles(pkgName, contractImport, targetDir) {
|
|
60
|
+
const includePrepare = isWorkspaceRoot(targetDir);
|
|
49
61
|
const tsconfig = await packageTsConfig(targetDir, {
|
|
62
|
+
include: ['src/**/*.ts', 'src/**/*.tsx'],
|
|
50
63
|
lib: ['ES2020', 'DOM'],
|
|
51
|
-
types: ['
|
|
64
|
+
types: ['react', 'react-native'],
|
|
65
|
+
jsx: 'react-jsx',
|
|
52
66
|
});
|
|
53
67
|
return {
|
|
54
|
-
'package.json': clientPackageJson(pkgName, contractImport),
|
|
68
|
+
'package.json': clientPackageJson(pkgName, contractImport, { includePrepare }),
|
|
55
69
|
'tsconfig.json': tsconfig,
|
|
56
70
|
...basePackageFiles(),
|
|
57
71
|
'src/index.ts': clientIndexTs(contractImport),
|
|
@@ -71,7 +85,9 @@ async function clientFiles(pkgName, contractImport, targetDir) {
|
|
|
71
85
|
{
|
|
72
86
|
title: 'Usage',
|
|
73
87
|
lines: [
|
|
74
|
-
|
|
88
|
+
contractImport
|
|
89
|
+
? `- Contract wired to ${contractImport}; adjust the import in \`src/index.ts\` if needed.`
|
|
90
|
+
: '- Add your contract import to `src/index.ts` and wire the registry when ready.',
|
|
75
91
|
'- Use the exported `queryClient` and built route clients from `src/index.ts`.',
|
|
76
92
|
],
|
|
77
93
|
},
|
|
@@ -91,11 +107,12 @@ export const clientVariant = {
|
|
|
91
107
|
keyFiles: ['src/index.ts', 'README.md'],
|
|
92
108
|
scripts: CLIENT_SCRIPTS,
|
|
93
109
|
notes: [
|
|
110
|
+
'Pick a contract from discovered workspace packages (or skip) during scaffolding.',
|
|
94
111
|
'Set the contract import via --contract or by editing src/index.ts.',
|
|
95
112
|
'Exports query client + typed route builders to plug into React apps.',
|
|
96
113
|
],
|
|
97
114
|
async scaffold(ctx) {
|
|
98
|
-
const contractImport = ctx.contractName
|
|
115
|
+
const contractImport = ctx.contractName;
|
|
99
116
|
const files = await clientFiles(ctx.pkgName, contractImport, ctx.targetDir);
|
|
100
117
|
for (const [relative, contents] of Object.entries(files)) {
|
|
101
118
|
// eslint-disable-next-line no-await-in-loop
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BASE_LINT_DEV_DEPENDENCIES, basePackageFiles, basePackageJson, buildReadme, baseScripts, packageTsConfig, writeFileIfMissing, } from '../shared.js';
|
|
1
|
+
import { BASE_LINT_DEV_DEPENDENCIES, basePackageFiles, basePackageJson, buildReadme, baseScripts, packageTsConfig, isWorkspaceRoot, writeFileIfMissing, } from '../shared.js';
|
|
2
2
|
const CONTRACT_SCRIPTS = [
|
|
3
3
|
'dev',
|
|
4
4
|
'build',
|
|
@@ -93,7 +93,7 @@ export const socketConfig = sockets.config
|
|
|
93
93
|
export const socketEvents = sockets.events
|
|
94
94
|
export type AppRegistry = typeof registry
|
|
95
95
|
`;
|
|
96
|
-
function contractPackageJson(name) {
|
|
96
|
+
function contractPackageJson(name, options) {
|
|
97
97
|
return basePackageJson({
|
|
98
98
|
name,
|
|
99
99
|
private: false,
|
|
@@ -104,7 +104,7 @@ function contractPackageJson(name) {
|
|
|
104
104
|
},
|
|
105
105
|
},
|
|
106
106
|
// ✅ Solution #2: dev continuously emits dist/*.js + dist/*.d.ts
|
|
107
|
-
scripts: baseScripts('tsc -p tsconfig.json --watch --preserveWatchOutput'),
|
|
107
|
+
scripts: baseScripts('tsc -p tsconfig.json --watch --preserveWatchOutput', undefined, { includePrepare: options?.includePrepare }),
|
|
108
108
|
// You can keep this pinned if you want, but the import strategy above prevents breakage
|
|
109
109
|
// across different module export shapes.
|
|
110
110
|
dependencies: {
|
|
@@ -117,9 +117,13 @@ function contractPackageJson(name) {
|
|
|
117
117
|
});
|
|
118
118
|
}
|
|
119
119
|
async function contractFiles(pkgName, targetDir) {
|
|
120
|
-
const
|
|
120
|
+
const includePrepare = isWorkspaceRoot(targetDir);
|
|
121
|
+
const tsconfig = await packageTsConfig(targetDir, {
|
|
122
|
+
include: ['src/**/*.ts', 'src/**/*.tsx'],
|
|
123
|
+
skipLibCheck: true,
|
|
124
|
+
});
|
|
121
125
|
return {
|
|
122
|
-
'package.json': contractPackageJson(pkgName),
|
|
126
|
+
'package.json': contractPackageJson(pkgName, { includePrepare }),
|
|
123
127
|
'tsconfig.json': tsconfig,
|
|
124
128
|
...basePackageFiles(),
|
|
125
129
|
'src/index.ts': CONTRACT_TS,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BASE_LINT_DEV_DEPENDENCIES, basePackageFiles, basePackageJson, buildReadme, baseScripts, packageTsConfig, writeFileIfMissing, } from '../shared.js';
|
|
1
|
+
import { BASE_LINT_DEV_DEPENDENCIES, basePackageFiles, basePackageJson, buildReadme, baseScripts, packageTsConfig, isWorkspaceRoot, writeFileIfMissing, } from '../shared.js';
|
|
2
2
|
const DOCKER_SCRIPTS = [
|
|
3
3
|
'dev',
|
|
4
4
|
'build',
|
|
@@ -18,7 +18,7 @@ const DOCKER_SCRIPTS = [
|
|
|
18
18
|
'docker:clean',
|
|
19
19
|
'docker:reset',
|
|
20
20
|
];
|
|
21
|
-
function dockerPackageJson(name) {
|
|
21
|
+
function dockerPackageJson(name, options) {
|
|
22
22
|
return basePackageJson({
|
|
23
23
|
name,
|
|
24
24
|
scripts: baseScripts('tsx watch src/index.ts', {
|
|
@@ -31,7 +31,7 @@ function dockerPackageJson(name) {
|
|
|
31
31
|
'docker:stop': 'npm run docker:cli -- stop',
|
|
32
32
|
'docker:clean': 'npm run docker:cli -- clean',
|
|
33
33
|
'docker:reset': 'npm run docker:cli -- reset',
|
|
34
|
-
}),
|
|
34
|
+
}, { includePrepare: options?.includePrepare }),
|
|
35
35
|
dependencies: {
|
|
36
36
|
cors: '^2.8.5',
|
|
37
37
|
express: '^5.1.0',
|
|
@@ -96,9 +96,13 @@ CMD ["node", "dist/index.js"]
|
|
|
96
96
|
`;
|
|
97
97
|
}
|
|
98
98
|
async function dockerFiles(pkgName, targetDir) {
|
|
99
|
-
const
|
|
99
|
+
const includePrepare = isWorkspaceRoot(targetDir);
|
|
100
|
+
const tsconfig = await packageTsConfig(targetDir, {
|
|
101
|
+
include: ['src/**/*.ts'],
|
|
102
|
+
types: ['node'],
|
|
103
|
+
});
|
|
100
104
|
return {
|
|
101
|
-
'package.json': dockerPackageJson(pkgName),
|
|
105
|
+
'package.json': dockerPackageJson(pkgName, { includePrepare }),
|
|
102
106
|
'tsconfig.json': tsconfig,
|
|
103
107
|
'src/index.ts': dockerIndexTs(),
|
|
104
108
|
'scripts/docker.ts': dockerCliScript(pkgName),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BASE_LINT_DEV_DEPENDENCIES, basePackageFiles, basePackageJson, buildReadme, baseScripts, packageTsConfig, writeFileIfMissing, } from '../shared.js';
|
|
1
|
+
import { BASE_LINT_DEV_DEPENDENCIES, basePackageFiles, basePackageJson, buildReadme, baseScripts, packageTsConfig, isWorkspaceRoot, writeFileIfMissing, } from '../shared.js';
|
|
2
2
|
const EMPTY_SCRIPTS = [
|
|
3
3
|
'dev',
|
|
4
4
|
'build',
|
|
@@ -10,19 +10,26 @@ const EMPTY_SCRIPTS = [
|
|
|
10
10
|
'clean',
|
|
11
11
|
'test',
|
|
12
12
|
];
|
|
13
|
-
function emptyPackageJson(name) {
|
|
13
|
+
function emptyPackageJson(name, options) {
|
|
14
14
|
return basePackageJson({
|
|
15
15
|
name,
|
|
16
|
-
scripts: baseScripts('tsx watch src/index.ts'
|
|
16
|
+
scripts: baseScripts('tsx watch src/index.ts', undefined, {
|
|
17
|
+
includePrepare: options?.includePrepare,
|
|
18
|
+
}),
|
|
17
19
|
devDependencies: {
|
|
18
20
|
...BASE_LINT_DEV_DEPENDENCIES,
|
|
19
21
|
},
|
|
20
22
|
});
|
|
21
23
|
}
|
|
22
24
|
async function emptyFiles(pkgName, targetDir) {
|
|
23
|
-
const
|
|
25
|
+
const includePrepare = isWorkspaceRoot(targetDir);
|
|
26
|
+
const tsconfig = await packageTsConfig(targetDir, {
|
|
27
|
+
include: ['src/**/*.ts', 'src/**/*.tsx'],
|
|
28
|
+
types: ['node'],
|
|
29
|
+
skipLibCheck: true,
|
|
30
|
+
});
|
|
24
31
|
return {
|
|
25
|
-
'package.json': emptyPackageJson(pkgName),
|
|
32
|
+
'package.json': emptyPackageJson(pkgName, { includePrepare }),
|
|
26
33
|
'tsconfig.json': tsconfig,
|
|
27
34
|
...basePackageFiles(),
|
|
28
35
|
'src/index.ts': "export const hello = 'world'\n",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import { BASE_LINT_DEV_DEPENDENCIES, basePackageFiles, basePackageJson,
|
|
2
|
+
import { BASE_LINT_DEV_DEPENDENCIES, basePackageFiles, basePackageJson, writeFileIfMissing, } from '../shared.js';
|
|
3
3
|
import { clientVariant } from './client.js';
|
|
4
4
|
import { serverVariant } from './server.js';
|
|
5
5
|
import { dockerVariant } from './docker.js';
|
|
@@ -43,6 +43,9 @@ function deriveDirs(rootDir, baseName) {
|
|
|
43
43
|
docker: path.join(packagesRoot, `${baseName}-docker`),
|
|
44
44
|
};
|
|
45
45
|
}
|
|
46
|
+
function toPosixPath(value) {
|
|
47
|
+
return value.split(path.sep).join('/');
|
|
48
|
+
}
|
|
46
49
|
function rootPackageJson(baseName) {
|
|
47
50
|
const dockerPackageDir = `packages/${baseName}-docker`;
|
|
48
51
|
return basePackageJson({
|
|
@@ -50,7 +53,7 @@ function rootPackageJson(baseName) {
|
|
|
50
53
|
private: true,
|
|
51
54
|
useDefaults: false,
|
|
52
55
|
scripts: {
|
|
53
|
-
setup: 'pnpm install && pnpm exec husky
|
|
56
|
+
setup: 'pnpm install && (git rev-parse --is-inside-work-tree >/dev/null 2>&1 && pnpm exec husky || true)',
|
|
54
57
|
dev: 'pnpm -r dev --parallel --if-present',
|
|
55
58
|
build: 'pnpm -r build',
|
|
56
59
|
typecheck: 'pnpm -r typecheck',
|
|
@@ -61,7 +64,7 @@ function rootPackageJson(baseName) {
|
|
|
61
64
|
'format:check': 'pnpm -r format:check --if-present',
|
|
62
65
|
test: 'pnpm -r test --if-present',
|
|
63
66
|
clean: 'pnpm -r clean --if-present && rimraf node_modules .turbo coverage',
|
|
64
|
-
prepare: 'husky
|
|
67
|
+
prepare: 'git rev-parse --is-inside-work-tree >/dev/null 2>&1 && husky || true',
|
|
65
68
|
'docker:up': `pnpm -C ${dockerPackageDir} run docker:up`,
|
|
66
69
|
'docker:dev': `pnpm -C ${dockerPackageDir} run docker:dev`,
|
|
67
70
|
'docker:logs': `pnpm -C ${dockerPackageDir} run docker:logs`,
|
|
@@ -77,6 +80,50 @@ function rootPackageJson(baseName) {
|
|
|
77
80
|
function rootPnpmWorkspace() {
|
|
78
81
|
return "packages:\n - 'packages/*'\n";
|
|
79
82
|
}
|
|
83
|
+
function rootTsconfigBase(baseName, names) {
|
|
84
|
+
const paths = {
|
|
85
|
+
[names.contract]: [`packages/${baseName}-contract/src`],
|
|
86
|
+
[`${names.contract}/*`]: [`packages/${baseName}-contract/src/*`],
|
|
87
|
+
[names.server]: [`packages/${baseName}-server/src`],
|
|
88
|
+
[`${names.server}/*`]: [`packages/${baseName}-server/src/*`],
|
|
89
|
+
[names.client]: [`packages/${baseName}-client/src`],
|
|
90
|
+
[`${names.client}/*`]: [`packages/${baseName}-client/src/*`],
|
|
91
|
+
[names.docker]: [`packages/${baseName}-docker/src`],
|
|
92
|
+
[`${names.docker}/*`]: [`packages/${baseName}-docker/src/*`],
|
|
93
|
+
};
|
|
94
|
+
const config = {
|
|
95
|
+
$schema: 'https://json.schemastore.org/tsconfig',
|
|
96
|
+
compilerOptions: {
|
|
97
|
+
target: 'ES2020',
|
|
98
|
+
module: 'ESNext',
|
|
99
|
+
moduleResolution: 'Bundler',
|
|
100
|
+
jsx: 'react-jsx',
|
|
101
|
+
strict: true,
|
|
102
|
+
skipLibCheck: true,
|
|
103
|
+
resolveJsonModule: true,
|
|
104
|
+
forceConsistentCasingInFileNames: true,
|
|
105
|
+
sourceMap: true,
|
|
106
|
+
baseUrl: '.',
|
|
107
|
+
paths,
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
return `${JSON.stringify(config, null, 2)}\n`;
|
|
111
|
+
}
|
|
112
|
+
function rootSolutionTsconfig(dirs) {
|
|
113
|
+
const references = [
|
|
114
|
+
dirs.contract,
|
|
115
|
+
dirs.server,
|
|
116
|
+
dirs.client,
|
|
117
|
+
dirs.docker,
|
|
118
|
+
].map((pkgDir) => ({ path: toPosixPath(path.relative(dirs.root, pkgDir)) }));
|
|
119
|
+
const config = {
|
|
120
|
+
$schema: 'https://json.schemastore.org/tsconfig',
|
|
121
|
+
extends: './tsconfig.base.json',
|
|
122
|
+
files: [],
|
|
123
|
+
references,
|
|
124
|
+
};
|
|
125
|
+
return `${JSON.stringify(config, null, 2)}\n`;
|
|
126
|
+
}
|
|
80
127
|
function stackComposeYaml() {
|
|
81
128
|
return `services:
|
|
82
129
|
db:
|
|
@@ -95,16 +142,13 @@ volumes:
|
|
|
95
142
|
db-data:
|
|
96
143
|
`;
|
|
97
144
|
}
|
|
98
|
-
async function scaffoldRootFiles(baseDir, baseName) {
|
|
145
|
+
async function scaffoldRootFiles(baseDir, baseName, names, dirs) {
|
|
99
146
|
const files = {
|
|
100
147
|
'package.json': rootPackageJson(baseName),
|
|
101
148
|
'pnpm-workspace.yaml': rootPnpmWorkspace(),
|
|
102
149
|
'docker-compose.yml': stackComposeYaml(),
|
|
103
|
-
'tsconfig.json':
|
|
104
|
-
|
|
105
|
-
outDir: 'dist',
|
|
106
|
-
include: ['packages/**/*', 'scripts/**/*', 'src/**/*'],
|
|
107
|
-
}),
|
|
150
|
+
'tsconfig.base.json': rootTsconfigBase(baseName, names),
|
|
151
|
+
'tsconfig.json': rootSolutionTsconfig(dirs),
|
|
108
152
|
...basePackageFiles(),
|
|
109
153
|
};
|
|
110
154
|
for (const [relative, contents] of Object.entries(files)) {
|
|
@@ -136,7 +180,7 @@ export const fullstackVariant = {
|
|
|
136
180
|
const names = deriveNames(baseName);
|
|
137
181
|
const dirs = deriveDirs(ctx.targetDir, baseName);
|
|
138
182
|
// Root workspace files
|
|
139
|
-
await scaffoldRootFiles(dirs.root, baseName);
|
|
183
|
+
await scaffoldRootFiles(dirs.root, baseName, names, dirs);
|
|
140
184
|
// Contract package (reuse contract variant)
|
|
141
185
|
await contractVariant.scaffold({
|
|
142
186
|
targetDir: dirs.contract,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BASE_LINT_DEV_DEPENDENCIES, basePackageFiles, basePackageJson, buildReadme, baseScripts, packageTsConfig, writeFileIfMissing, } from '../shared.js';
|
|
1
|
+
import { BASE_LINT_DEV_DEPENDENCIES, basePackageFiles, basePackageJson, buildReadme, baseScripts, packageTsConfig, isWorkspaceRoot, writeFileIfMissing, } from '../shared.js';
|
|
2
2
|
const SERVER_SCRIPTS = [
|
|
3
3
|
'dev',
|
|
4
4
|
'build',
|
|
@@ -11,14 +11,30 @@ const SERVER_SCRIPTS = [
|
|
|
11
11
|
'test',
|
|
12
12
|
'start',
|
|
13
13
|
];
|
|
14
|
-
const CONTRACT_IMPORT_PLACEHOLDER = '@your-scope/contract';
|
|
15
14
|
export function serverIndexTs(contractImport) {
|
|
15
|
+
const contractImportLine = contractImport
|
|
16
|
+
? `import { registry } from '${contractImport}'`
|
|
17
|
+
: "// import { registry } from '@your-scope/contract'";
|
|
18
|
+
const registerBlock = contractImport
|
|
19
|
+
? `routes.registerControllers(registry, {
|
|
20
|
+
'GET /api/health': {
|
|
21
|
+
handler: async ({ ctx }) => ({
|
|
22
|
+
out: {
|
|
23
|
+
status: 'ok',
|
|
24
|
+
requestId: ctx.requestId,
|
|
25
|
+
at: new Date().toISOString(),
|
|
26
|
+
},
|
|
27
|
+
}),
|
|
28
|
+
},
|
|
29
|
+
})`
|
|
30
|
+
: `// TODO: import your contract registry and register controllers
|
|
31
|
+
// routes.registerControllers(registry)`;
|
|
16
32
|
return `import 'dotenv/config'
|
|
17
33
|
import http from 'node:http'
|
|
18
34
|
import express from 'express'
|
|
19
35
|
import cors from 'cors'
|
|
20
36
|
import { createRRRoute } from '@emeryld/rrroutes-server'
|
|
21
|
-
|
|
37
|
+
${contractImportLine}
|
|
22
38
|
|
|
23
39
|
const app = express()
|
|
24
40
|
app.use(cors({ origin: '*', credentials: true }))
|
|
@@ -38,17 +54,7 @@ const routes = createRRRoute(app, {
|
|
|
38
54
|
: undefined,
|
|
39
55
|
})
|
|
40
56
|
|
|
41
|
-
|
|
42
|
-
'GET /api/health': {
|
|
43
|
-
handler: async ({ ctx }) => ({
|
|
44
|
-
out: {
|
|
45
|
-
status: 'ok',
|
|
46
|
-
requestId: ctx.requestId,
|
|
47
|
-
at: new Date().toISOString(),
|
|
48
|
-
},
|
|
49
|
-
}),
|
|
50
|
-
},
|
|
51
|
-
})
|
|
57
|
+
${registerBlock}
|
|
52
58
|
|
|
53
59
|
const PORT = Number.parseInt(process.env.PORT ?? '4000', 10)
|
|
54
60
|
const server = http.createServer(app)
|
|
@@ -58,21 +64,23 @@ server.listen(PORT, () => {
|
|
|
58
64
|
})
|
|
59
65
|
`;
|
|
60
66
|
}
|
|
61
|
-
export function serverPackageJson(name, contractName
|
|
67
|
+
export function serverPackageJson(name, contractName, options) {
|
|
68
|
+
const dependencies = {
|
|
69
|
+
'@emeryld/rrroutes-server': '^2.4.1',
|
|
70
|
+
cors: '^2.8.5',
|
|
71
|
+
dotenv: '^16.4.5',
|
|
72
|
+
express: '^5.1.0',
|
|
73
|
+
zod: '^4.2.1',
|
|
74
|
+
};
|
|
75
|
+
if (contractName)
|
|
76
|
+
dependencies[contractName] = 'workspace:*';
|
|
62
77
|
return basePackageJson({
|
|
63
78
|
name,
|
|
64
79
|
private: false,
|
|
65
80
|
scripts: baseScripts('tsx watch --env-file .env src/index.ts', {
|
|
66
81
|
start: 'node dist/index.js',
|
|
67
|
-
}),
|
|
68
|
-
dependencies
|
|
69
|
-
[contractName]: 'workspace:*',
|
|
70
|
-
'@emeryld/rrroutes-server': '^2.4.1',
|
|
71
|
-
cors: '^2.8.5',
|
|
72
|
-
dotenv: '^16.4.5',
|
|
73
|
-
express: '^5.1.0',
|
|
74
|
-
zod: '^4.2.1',
|
|
75
|
-
},
|
|
82
|
+
}, { includePrepare: options?.includePrepare }),
|
|
83
|
+
dependencies,
|
|
76
84
|
devDependencies: {
|
|
77
85
|
...BASE_LINT_DEV_DEPENDENCIES,
|
|
78
86
|
'@types/cors': '^2.8.5',
|
|
@@ -82,9 +90,13 @@ export function serverPackageJson(name, contractName = CONTRACT_IMPORT_PLACEHOLD
|
|
|
82
90
|
});
|
|
83
91
|
}
|
|
84
92
|
async function serverFiles(pkgName, contractImport, targetDir) {
|
|
85
|
-
const
|
|
93
|
+
const includePrepare = isWorkspaceRoot(targetDir);
|
|
94
|
+
const tsconfig = await packageTsConfig(targetDir, {
|
|
95
|
+
include: ['src/**/*.ts'],
|
|
96
|
+
types: ['node'],
|
|
97
|
+
});
|
|
86
98
|
return {
|
|
87
|
-
'package.json': serverPackageJson(pkgName, contractImport),
|
|
99
|
+
'package.json': serverPackageJson(pkgName, contractImport, { includePrepare }),
|
|
88
100
|
'tsconfig.json': tsconfig,
|
|
89
101
|
...basePackageFiles(),
|
|
90
102
|
'src/index.ts': serverIndexTs(contractImport),
|
|
@@ -106,7 +118,9 @@ async function serverFiles(pkgName, contractImport, targetDir) {
|
|
|
106
118
|
{
|
|
107
119
|
title: 'Usage',
|
|
108
120
|
lines: [
|
|
109
|
-
|
|
121
|
+
contractImport
|
|
122
|
+
? `- Contract wired to ${contractImport}; adjust the import in \`src/index.ts\` if needed.`
|
|
123
|
+
: '- Add your contract import to `src/index.ts` when ready and register controllers.',
|
|
110
124
|
'- Start compiled server: `npm run start` after building.',
|
|
111
125
|
],
|
|
112
126
|
},
|
|
@@ -126,11 +140,12 @@ export const serverVariant = {
|
|
|
126
140
|
keyFiles: ['src/index.ts', '.env.example', 'README.md'],
|
|
127
141
|
scripts: SERVER_SCRIPTS,
|
|
128
142
|
notes: [
|
|
129
|
-
'
|
|
143
|
+
'Pick a contract from discovered workspace packages (or skip) during scaffolding.',
|
|
144
|
+
'Set/adjust the contract import via --contract or by editing src/index.ts.',
|
|
130
145
|
'Includes start script for compiled output and dotenv-ready dev script.',
|
|
131
146
|
],
|
|
132
147
|
async scaffold(ctx) {
|
|
133
|
-
const contractImport = ctx.contractName
|
|
148
|
+
const contractImport = ctx.contractName;
|
|
134
149
|
const files = await serverFiles(ctx.pkgName, contractImport, ctx.targetDir);
|
|
135
150
|
for (const [relative, contents] of Object.entries(files)) {
|
|
136
151
|
// eslint-disable-next-line no-await-in-loop
|
package/dist/packages.js
CHANGED
|
@@ -3,7 +3,16 @@ import path from 'node:path';
|
|
|
3
3
|
import { pathToFileURL } from 'node:url';
|
|
4
4
|
import { readdir, readFile } from 'node:fs/promises';
|
|
5
5
|
const rootDir = process.cwd();
|
|
6
|
-
|
|
6
|
+
const ignoredDirs = new Set([
|
|
7
|
+
'node_modules',
|
|
8
|
+
'.git',
|
|
9
|
+
'.turbo',
|
|
10
|
+
'.next',
|
|
11
|
+
'dist',
|
|
12
|
+
'build',
|
|
13
|
+
'.cache',
|
|
14
|
+
'coverage',
|
|
15
|
+
]);
|
|
7
16
|
const colorPalette = ['cyan', 'green', 'yellow', 'magenta', 'red'];
|
|
8
17
|
let manifestState;
|
|
9
18
|
function manifestFilePath() {
|
|
@@ -19,7 +28,7 @@ function normalizeManifestPath(value) {
|
|
|
19
28
|
const absolute = path.resolve(rootDir, value || '');
|
|
20
29
|
let relative = path.relative(rootDir, absolute);
|
|
21
30
|
if (!relative)
|
|
22
|
-
return '';
|
|
31
|
+
return '.';
|
|
23
32
|
relative = relative.replace(/\\/g, '/');
|
|
24
33
|
return relative.replace(/^(?:\.\/)+/, '');
|
|
25
34
|
}
|
|
@@ -42,6 +51,35 @@ function deriveSubstitute(name) {
|
|
|
42
51
|
.join(' ');
|
|
43
52
|
return transformed || trimmed;
|
|
44
53
|
}
|
|
54
|
+
async function findPackageJsonFiles(baseDir) {
|
|
55
|
+
const results = new Set();
|
|
56
|
+
const queue = [baseDir];
|
|
57
|
+
while (queue.length) {
|
|
58
|
+
const current = queue.shift();
|
|
59
|
+
let entries;
|
|
60
|
+
try {
|
|
61
|
+
entries = await readdir(current, { withFileTypes: true });
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
for (const entry of entries) {
|
|
67
|
+
if (entry.isFile() && entry.name === 'package.json') {
|
|
68
|
+
results.add(path.join(current, entry.name));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
for (const entry of entries) {
|
|
72
|
+
if (!entry.isDirectory())
|
|
73
|
+
continue;
|
|
74
|
+
if (entry.isSymbolicLink())
|
|
75
|
+
continue;
|
|
76
|
+
if (ignoredDirs.has(entry.name))
|
|
77
|
+
continue;
|
|
78
|
+
queue.push(path.join(current, entry.name));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return [...results].sort();
|
|
82
|
+
}
|
|
45
83
|
async function loadWorkspaceManifest() {
|
|
46
84
|
const manifestPath = manifestFilePath();
|
|
47
85
|
try {
|
|
@@ -58,33 +96,26 @@ async function loadWorkspaceManifest() {
|
|
|
58
96
|
return undefined;
|
|
59
97
|
}
|
|
60
98
|
async function inferManifestFromWorkspace() {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
catch {
|
|
80
|
-
continue;
|
|
81
|
-
}
|
|
99
|
+
const manifest = [];
|
|
100
|
+
const pkgJsonPaths = await findPackageJsonFiles(rootDir);
|
|
101
|
+
for (const pkgJsonPath of pkgJsonPaths) {
|
|
102
|
+
try {
|
|
103
|
+
const raw = await readFile(pkgJsonPath, 'utf8');
|
|
104
|
+
const json = JSON.parse(raw);
|
|
105
|
+
const pkgDir = path.dirname(pkgJsonPath);
|
|
106
|
+
const pkgName = json.name?.trim() || path.basename(pkgDir) || 'package';
|
|
107
|
+
manifest.push({
|
|
108
|
+
name: pkgName,
|
|
109
|
+
path: normalizeManifestPath(path.relative(rootDir, pkgDir)),
|
|
110
|
+
color: colorFromSeed(pkgName),
|
|
111
|
+
substitute: deriveSubstitute(pkgName),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
continue;
|
|
82
116
|
}
|
|
83
|
-
return manifest;
|
|
84
|
-
}
|
|
85
|
-
catch {
|
|
86
|
-
return [];
|
|
87
117
|
}
|
|
118
|
+
return manifest;
|
|
88
119
|
}
|
|
89
120
|
function mergeManifestEntries(inferred, overrides) {
|
|
90
121
|
const normalizedOverrides = new Map();
|
|
@@ -117,8 +148,8 @@ function mergeManifestEntries(inferred, overrides) {
|
|
|
117
148
|
});
|
|
118
149
|
return merged;
|
|
119
150
|
}
|
|
120
|
-
async function ensureManifestState() {
|
|
121
|
-
if (manifestState)
|
|
151
|
+
async function ensureManifestState(forceReload = false) {
|
|
152
|
+
if (manifestState && !forceReload)
|
|
122
153
|
return manifestState;
|
|
123
154
|
const [workspaceManifest, inferred] = await Promise.all([
|
|
124
155
|
loadWorkspaceManifest(),
|
|
@@ -136,35 +167,34 @@ async function ensureManifestState() {
|
|
|
136
167
|
return manifestState;
|
|
137
168
|
}
|
|
138
169
|
export async function loadPackages() {
|
|
139
|
-
const entries = await readdir(packagesDir, { withFileTypes: true });
|
|
140
170
|
const packages = [];
|
|
141
|
-
const { byName, byPath } = await ensureManifestState();
|
|
171
|
+
const { byName, byPath, entries } = await ensureManifestState(true);
|
|
142
172
|
for (const entry of entries) {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const pkgJsonPath = path.join(packagesDir, entry.name, 'package.json');
|
|
173
|
+
const pkgDir = path.resolve(rootDir, entry.path || '.');
|
|
174
|
+
const pkgJsonPath = path.join(pkgDir, 'package.json');
|
|
146
175
|
try {
|
|
147
176
|
const raw = await readFile(pkgJsonPath, 'utf8');
|
|
148
177
|
const json = JSON.parse(raw);
|
|
149
|
-
const pkgName = json.name ?? entry.name;
|
|
150
|
-
const relativePath = normalizeManifestPath(path.relative(rootDir,
|
|
178
|
+
const pkgName = json.name?.trim() ?? entry.name;
|
|
179
|
+
const relativePath = normalizeManifestPath(path.relative(rootDir, pkgDir));
|
|
151
180
|
const meta = byPath.get(relativePath.toLowerCase()) ??
|
|
152
|
-
byName.get(pkgName.toLowerCase());
|
|
153
|
-
const substitute = meta?.substitute ?? deriveSubstitute(pkgName) ??
|
|
181
|
+
byName.get((pkgName ?? '').toLowerCase());
|
|
182
|
+
const substitute = meta?.substitute ?? deriveSubstitute(pkgName) ?? path.basename(pkgDir);
|
|
154
183
|
const color = meta?.color ?? colorFromSeed(pkgName);
|
|
155
184
|
packages.push({
|
|
156
|
-
dirName:
|
|
157
|
-
|
|
185
|
+
dirName: path.basename(pkgDir),
|
|
186
|
+
relativeDir: relativePath,
|
|
187
|
+
path: pkgDir,
|
|
158
188
|
packageJsonPath: pkgJsonPath,
|
|
159
189
|
json,
|
|
160
190
|
version: json.version,
|
|
161
|
-
name: pkgName,
|
|
191
|
+
name: pkgName ?? path.basename(pkgDir),
|
|
162
192
|
substitute,
|
|
163
193
|
color,
|
|
164
194
|
});
|
|
165
195
|
}
|
|
166
196
|
catch (error) {
|
|
167
|
-
console.warn(`Skipping ${entry.
|
|
197
|
+
console.warn(`Skipping ${entry.path || 'workspace root'}: ${String(error)}`);
|
|
168
198
|
}
|
|
169
199
|
}
|
|
170
200
|
return packages;
|
|
@@ -175,10 +205,12 @@ export function resolvePackage(packages, key) {
|
|
|
175
205
|
const normalized = key.toLowerCase();
|
|
176
206
|
return packages.find((pkg) => {
|
|
177
207
|
const dirMatch = pkg.dirName.toLowerCase() === normalized;
|
|
208
|
+
const pathMatch = pkg.relativeDir.toLowerCase() === normalized;
|
|
178
209
|
const nameMatch = (pkg.name ?? '').toLowerCase() === normalized;
|
|
179
210
|
const aliasMatch = (pkg.substitute ?? '').toLowerCase() === normalized;
|
|
180
|
-
const fuzzyMatch = (pkg.name ?? '').toLowerCase().includes(normalized)
|
|
181
|
-
|
|
211
|
+
const fuzzyMatch = (pkg.name ?? '').toLowerCase().includes(normalized) ||
|
|
212
|
+
pkg.relativeDir.toLowerCase().includes(normalized);
|
|
213
|
+
return dirMatch || pathMatch || nameMatch || aliasMatch || fuzzyMatch;
|
|
182
214
|
});
|
|
183
215
|
}
|
|
184
216
|
/**
|
|
@@ -194,7 +226,7 @@ export function getOrderedPackages(packages) {
|
|
|
194
226
|
for (const p of packages) {
|
|
195
227
|
if (p.name)
|
|
196
228
|
byName.set(p.name, p);
|
|
197
|
-
byDir.set(p.
|
|
229
|
+
byDir.set(p.relativeDir, p);
|
|
198
230
|
}
|
|
199
231
|
// Build adjacency list: edge dep -> pkg (dep must publish first)
|
|
200
232
|
const depsOf = (p) => {
|
|
@@ -216,24 +248,24 @@ export function getOrderedPackages(packages) {
|
|
|
216
248
|
.map((n) => byName.get(n))
|
|
217
249
|
.filter((x) => Boolean(x));
|
|
218
250
|
};
|
|
219
|
-
const nodes = new Set(packages.map((p) => p.
|
|
251
|
+
const nodes = new Set(packages.map((p) => p.relativeDir));
|
|
220
252
|
const inDegree = new Map();
|
|
221
253
|
const adj = new Map();
|
|
222
254
|
for (const p of packages) {
|
|
223
|
-
inDegree.set(p.
|
|
224
|
-
adj.set(p.
|
|
255
|
+
inDegree.set(p.relativeDir, 0);
|
|
256
|
+
adj.set(p.relativeDir, new Set());
|
|
225
257
|
}
|
|
226
258
|
for (const p of packages) {
|
|
227
259
|
for (const dep of depsOf(p)) {
|
|
228
260
|
// dep -> p
|
|
229
|
-
if (!nodes.has(dep.
|
|
261
|
+
if (!nodes.has(dep.relativeDir))
|
|
230
262
|
continue;
|
|
231
|
-
if (dep.
|
|
263
|
+
if (dep.relativeDir === p.relativeDir)
|
|
232
264
|
continue;
|
|
233
|
-
const set = adj.get(dep.
|
|
234
|
-
if (!set.has(p.
|
|
235
|
-
set.add(p.
|
|
236
|
-
inDegree.set(p.
|
|
265
|
+
const set = adj.get(dep.relativeDir);
|
|
266
|
+
if (!set.has(p.relativeDir)) {
|
|
267
|
+
set.add(p.relativeDir);
|
|
268
|
+
inDegree.set(p.relativeDir, (inDegree.get(p.relativeDir) ?? 0) + 1);
|
|
237
269
|
}
|
|
238
270
|
}
|
|
239
271
|
}
|
package/dist/publish.js
CHANGED
|
@@ -134,7 +134,7 @@ async function main() {
|
|
|
134
134
|
// If user provided non-interactive flags, run headless path
|
|
135
135
|
if (parsed.nonInteractive) {
|
|
136
136
|
if (packages.length === 0) {
|
|
137
|
-
throw new Error('No packages found in
|
|
137
|
+
throw new Error('No packages with a package.json found in this workspace');
|
|
138
138
|
}
|
|
139
139
|
publishCliState.autoConfirmAll = true;
|
|
140
140
|
if (!parsed.selectionArg) {
|
|
@@ -153,7 +153,7 @@ async function main() {
|
|
|
153
153
|
return;
|
|
154
154
|
}
|
|
155
155
|
if (packages.length === 0) {
|
|
156
|
-
logGlobal('No packages found
|
|
156
|
+
logGlobal('No packages found (no package.json discovered). Use "Create package" to scaffold one.', colors.yellow);
|
|
157
157
|
}
|
|
158
158
|
// Interactive flow (unchanged): selection menu then step menu
|
|
159
159
|
if (parsed.selectionArg) {
|
package/dist/release.js
CHANGED
|
@@ -155,8 +155,8 @@ export async function promptVersionStrategy(targetPkg, selection, opts) {
|
|
|
155
155
|
return viaOpts;
|
|
156
156
|
console.log('\nCurrent package versions:');
|
|
157
157
|
for (const pkg of selection) {
|
|
158
|
-
const marker = selection.length === 1 && pkg.
|
|
159
|
-
console.log(` ${marker} ${formatPkgName(pkg)} ${pkg.
|
|
158
|
+
const marker = selection.length === 1 && pkg.relativeDir === targetPkg.relativeDir ? '*' : '•';
|
|
159
|
+
console.log(` ${marker} ${formatPkgName(pkg)} ${pkg.relativeDir} ${colors.yellow(pkg.version)}`);
|
|
160
160
|
}
|
|
161
161
|
console.log('\nSelect version strategy:');
|
|
162
162
|
console.log(' 1) Patch');
|
|
@@ -228,7 +228,7 @@ export async function promptVersionStrategy(targetPkg, selection, opts) {
|
|
|
228
228
|
if (selection.length === 1) {
|
|
229
229
|
const proposed = bumpVersion(targetPkg.version, bumpType, preid);
|
|
230
230
|
console.log(`\nTarget package: ${formatPkgName(targetPkg)} -> ${colors.green(proposed)}`);
|
|
231
|
-
const others = selection.filter((pkg) => pkg.
|
|
231
|
+
const others = selection.filter((pkg) => pkg.relativeDir !== targetPkg.relativeDir);
|
|
232
232
|
if (others.length) {
|
|
233
233
|
console.log('Other packages:');
|
|
234
234
|
others.forEach((pkg) => {
|
package/dist/utils/log.js
CHANGED
|
@@ -5,7 +5,7 @@ export const globalEmoji = '🚀';
|
|
|
5
5
|
export const formatPkgName = (pkg) => {
|
|
6
6
|
const primary = pkg.substitute;
|
|
7
7
|
const displayName = pkg.name ?? pkg.dirName;
|
|
8
|
-
const fileLabel = pkg.dirName;
|
|
8
|
+
const fileLabel = pkg.relativeDir || pkg.dirName;
|
|
9
9
|
const colorizer = colors[pkg.color ?? defaultPackageColor];
|
|
10
10
|
return `${colorizer(colors.bold(primary))} ${colors.dim(`(${displayName}) [${fileLabel}]`)}`;
|
|
11
11
|
};
|
package/dist/workspace.js
CHANGED
|
@@ -26,24 +26,39 @@ function dependencyPathsFromStatus(status) {
|
|
|
26
26
|
.map(extractPathFromStatus)
|
|
27
27
|
.filter((p) => Boolean(p && dependencyFiles.has(p.split('/').pop() ?? '')));
|
|
28
28
|
}
|
|
29
|
+
function normalizePathForMatch(p) {
|
|
30
|
+
return p.replace(/\\/g, '/');
|
|
31
|
+
}
|
|
32
|
+
function findPackageForPath(p, targets) {
|
|
33
|
+
const normalized = normalizePathForMatch(p);
|
|
34
|
+
return targets.find((pkg) => {
|
|
35
|
+
const rel = normalizePathForMatch(pkg.relativeDir || '');
|
|
36
|
+
if (!rel || rel === '.')
|
|
37
|
+
return normalized === 'package.json';
|
|
38
|
+
const prefix = `${rel}/`;
|
|
39
|
+
return normalized === `${rel}/package.json` || normalized.startsWith(prefix);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
29
42
|
function formatPkgLabel(pkg) {
|
|
30
43
|
return pkg.substitute ?? pkg.name ?? pkg.dirName;
|
|
31
44
|
}
|
|
32
45
|
function logDependencyChanges(paths, targets) {
|
|
33
46
|
if (paths.length === 0)
|
|
34
47
|
return;
|
|
35
|
-
const byDir = new Map(targets.map((t) => [t.dirName, formatPkgLabel(t)]));
|
|
36
48
|
logGlobal('Dependency file changes detected:', colors.cyan);
|
|
37
49
|
paths.forEach((p) => {
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
const fileLabel =
|
|
44
|
-
|
|
50
|
+
const normalized = normalizePathForMatch(p);
|
|
51
|
+
const matchedPkg = findPackageForPath(normalized, targets);
|
|
52
|
+
if (matchedPkg) {
|
|
53
|
+
const rel = normalizePathForMatch(matchedPkg.relativeDir);
|
|
54
|
+
const base = rel === '.' ? '' : `${rel}/`;
|
|
55
|
+
const fileLabel = normalized.startsWith(base)
|
|
56
|
+
? normalized.slice(base.length)
|
|
57
|
+
: normalized;
|
|
58
|
+
console.log(` • ${colors.dim(`${formatPkgLabel(matchedPkg)} (${fileLabel || 'package.json'})`)}`);
|
|
45
59
|
return;
|
|
46
60
|
}
|
|
61
|
+
const file = normalized.split('/').pop() ?? '';
|
|
47
62
|
if (file === 'pnpm-lock.yaml' ||
|
|
48
63
|
file === 'package-lock.json' ||
|
|
49
64
|
file === 'npm-shrinkwrap.json') {
|
|
@@ -100,16 +115,12 @@ function parseVersionChanges(path, label) {
|
|
|
100
115
|
}));
|
|
101
116
|
}
|
|
102
117
|
function summarizeVersionChanges(paths, targets) {
|
|
103
|
-
const byDir = new Map(targets.map((t) => [t.dirName, formatPkgLabel(t)]));
|
|
104
118
|
const changes = [];
|
|
105
119
|
paths
|
|
106
120
|
.filter((p) => p.endsWith('package.json'))
|
|
107
121
|
.forEach((p) => {
|
|
108
|
-
const
|
|
109
|
-
const
|
|
110
|
-
const label = pkgIndex !== -1 && parts[pkgIndex + 1]
|
|
111
|
-
? (byDir.get(parts[pkgIndex + 1]) ?? parts[pkgIndex + 1])
|
|
112
|
-
: 'workspace';
|
|
122
|
+
const pkg = findPackageForPath(p, targets);
|
|
123
|
+
const label = pkg ? formatPkgLabel(pkg) : 'workspace';
|
|
113
124
|
changes.push(...parseVersionChanges(p, label));
|
|
114
125
|
});
|
|
115
126
|
if (changes.length === 0)
|
|
@@ -133,15 +144,13 @@ function buildUpdateCommitMessage(paths, targets) {
|
|
|
133
144
|
if (changeSummary)
|
|
134
145
|
return `chore(deps): ${changeSummary}`;
|
|
135
146
|
const labels = new Set();
|
|
136
|
-
const byDir = new Map(targets.map((t) => [t.dirName, formatPkgLabel(t)]));
|
|
137
147
|
paths.forEach((p) => {
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const dir = parts[pkgIndex + 1];
|
|
142
|
-
labels.add(byDir.get(dir) ?? dir);
|
|
148
|
+
const matchedPkg = findPackageForPath(p, targets);
|
|
149
|
+
if (matchedPkg) {
|
|
150
|
+
labels.add(formatPkgLabel(matchedPkg));
|
|
143
151
|
return;
|
|
144
152
|
}
|
|
153
|
+
const parts = normalizePathForMatch(p).split('/');
|
|
145
154
|
if (parts[parts.length - 1] === 'package.json') {
|
|
146
155
|
labels.add('workspace deps');
|
|
147
156
|
}
|
|
@@ -175,6 +184,13 @@ async function promptCommitMessage(proposed) {
|
|
|
175
184
|
}
|
|
176
185
|
return custom.trim();
|
|
177
186
|
}
|
|
187
|
+
function packageFilterArg(pkg) {
|
|
188
|
+
if (pkg.name)
|
|
189
|
+
return pkg.name;
|
|
190
|
+
if (!pkg.relativeDir || pkg.relativeDir === '.')
|
|
191
|
+
return '.';
|
|
192
|
+
return `./${pkg.relativeDir}`;
|
|
193
|
+
}
|
|
178
194
|
export async function runCleanInstall() {
|
|
179
195
|
logGlobal('Cleaning workspace…', colors.cyan);
|
|
180
196
|
await run('pnpm', ['run', 'clean']);
|
|
@@ -184,7 +200,7 @@ export async function runCleanInstall() {
|
|
|
184
200
|
export async function updateDependencies(targets) {
|
|
185
201
|
const preStatus = await collectGitStatus();
|
|
186
202
|
if (targets.length === 1) {
|
|
187
|
-
const filterArg = targets[0]
|
|
203
|
+
const filterArg = packageFilterArg(targets[0]);
|
|
188
204
|
logPkg(targets[0], `Updating dependencies…`);
|
|
189
205
|
await run('pnpm', ['-r', '--filter', filterArg, 'update']);
|
|
190
206
|
}
|
|
@@ -219,7 +235,7 @@ export async function typecheckAll() {
|
|
|
219
235
|
await run('pnpm', ['typecheck']);
|
|
220
236
|
}
|
|
221
237
|
export async function typecheckSingle(pkg) {
|
|
222
|
-
const filterArg = pkg
|
|
238
|
+
const filterArg = packageFilterArg(pkg);
|
|
223
239
|
logPkg(pkg, `Running typecheck…`);
|
|
224
240
|
await run('pnpm', ['run', '--filter', filterArg, 'typecheck']);
|
|
225
241
|
}
|
|
@@ -228,7 +244,7 @@ export async function buildAll() {
|
|
|
228
244
|
await run('pnpm', ['build']);
|
|
229
245
|
}
|
|
230
246
|
export async function buildSingle(pkg) {
|
|
231
|
-
const filterArg = pkg
|
|
247
|
+
const filterArg = packageFilterArg(pkg);
|
|
232
248
|
logPkg(pkg, `Running build…`);
|
|
233
249
|
await run('pnpm', ['run', '--filter', filterArg, 'build']);
|
|
234
250
|
}
|
|
@@ -242,5 +258,5 @@ export async function testAll() {
|
|
|
242
258
|
}
|
|
243
259
|
export async function testSingle(pkg) {
|
|
244
260
|
logPkg(pkg, `Running tests…`);
|
|
245
|
-
await run('pnpm', ['test', '--',
|
|
261
|
+
await run('pnpm', ['test', '--', pkg.relativeDir === '.' ? '.' : pkg.relativeDir]);
|
|
246
262
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@emeryld/manager",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Interactive manager for pnpm monorepos (update/test/build/publish).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -30,8 +30,7 @@
|
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@types/node": "^20.17.0",
|
|
32
32
|
"@types/semver": "^7.7.1",
|
|
33
|
-
"cross-env": "^7.0.3"
|
|
34
|
-
"@emeryld/manager": "^0.4.1"
|
|
33
|
+
"cross-env": "^7.0.3"
|
|
35
34
|
},
|
|
36
35
|
"ts-node": {
|
|
37
36
|
"esm": true,
|