@emeryld/manager 0.3.0 → 0.3.2
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/dist/create-package/index.js +218 -0
- package/dist/create-package/shared.js +44 -0
- package/dist/create-package/variants/client.js +70 -0
- package/dist/create-package/variants/contract.js +125 -0
- package/dist/create-package/variants/docker.js +108 -0
- package/dist/create-package/variants/empty.js +44 -0
- package/dist/create-package/variants/fullstack.js +204 -0
- package/dist/create-package/variants/server.js +106 -0
- package/dist/publish.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { mkdir, readdir, readFile, stat } from 'node:fs/promises';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { stdin as input } from 'node:process';
|
|
5
|
+
import { askLine, promptSingleKey } from '../prompts.js';
|
|
6
|
+
import { colors, logGlobal } from '../utils/log.js';
|
|
7
|
+
import { workspaceRoot } from './shared.js';
|
|
8
|
+
import { clientVariant } from './variants/client.js';
|
|
9
|
+
import { contractVariant } from './variants/contract.js';
|
|
10
|
+
import { dockerVariant } from './variants/docker.js';
|
|
11
|
+
import { emptyVariant } from './variants/empty.js';
|
|
12
|
+
import { fullstackVariant } from './variants/fullstack.js';
|
|
13
|
+
import { serverVariant } from './variants/server.js';
|
|
14
|
+
const VARIANTS = [
|
|
15
|
+
contractVariant,
|
|
16
|
+
serverVariant,
|
|
17
|
+
clientVariant,
|
|
18
|
+
emptyVariant,
|
|
19
|
+
dockerVariant,
|
|
20
|
+
fullstackVariant,
|
|
21
|
+
];
|
|
22
|
+
function derivePackageName(targetDir) {
|
|
23
|
+
const base = path.basename(targetDir) || 'rrr-package';
|
|
24
|
+
return base;
|
|
25
|
+
}
|
|
26
|
+
async function ensureTargetDir(targetDir) {
|
|
27
|
+
try {
|
|
28
|
+
const stats = await stat(targetDir);
|
|
29
|
+
if (!stats.isDirectory()) {
|
|
30
|
+
throw new Error(`Target "${targetDir}" exists and is not a directory.`);
|
|
31
|
+
}
|
|
32
|
+
const entries = await readdir(targetDir);
|
|
33
|
+
if (entries.length > 0) {
|
|
34
|
+
logGlobal(`Target ${path.relative(workspaceRoot, targetDir)} is not empty; existing files will be preserved.`, colors.yellow);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
if (error &&
|
|
39
|
+
typeof error === 'object' &&
|
|
40
|
+
error.code === 'ENOENT') {
|
|
41
|
+
await mkdir(targetDir, { recursive: true });
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async function runCommand(cmd, args, cwd = workspaceRoot) {
|
|
48
|
+
await new Promise((resolve, reject) => {
|
|
49
|
+
const child = spawn(cmd, args, {
|
|
50
|
+
cwd,
|
|
51
|
+
stdio: 'inherit',
|
|
52
|
+
shell: process.platform === 'win32',
|
|
53
|
+
});
|
|
54
|
+
child.on('exit', (code) => {
|
|
55
|
+
if (code === 0)
|
|
56
|
+
resolve();
|
|
57
|
+
else
|
|
58
|
+
reject(new Error(`${cmd} ${args.join(' ')} exited with ${code}`));
|
|
59
|
+
});
|
|
60
|
+
child.on('error', (err) => reject(err));
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
function formatVariantLines(variants, selected) {
|
|
64
|
+
const heading = colors.magenta('Available templates');
|
|
65
|
+
const lines = [heading];
|
|
66
|
+
variants.forEach((variant, index) => {
|
|
67
|
+
const isSelected = index === selected;
|
|
68
|
+
const pointer = isSelected ? `${colors.green('➤')} ` : ' ';
|
|
69
|
+
const numberLabel = colors.cyan(String(index + 1).padStart(2, ' '));
|
|
70
|
+
const label = isSelected ? colors.green(variant.label) : variant.label;
|
|
71
|
+
const meta = colors.dim(variant.defaultDir);
|
|
72
|
+
lines.push(`${pointer}${numberLabel}. ${label} ${meta}`);
|
|
73
|
+
});
|
|
74
|
+
lines.push('');
|
|
75
|
+
lines.push(colors.dim('Use ↑/↓ (or j/k) to move, digits (1-9,0 for 10) to pick, Enter to confirm, Esc/Ctrl+C to exit.'));
|
|
76
|
+
return lines;
|
|
77
|
+
}
|
|
78
|
+
function renderInteractiveList(lines, previousLineCount) {
|
|
79
|
+
if (previousLineCount > 0) {
|
|
80
|
+
process.stdout.write(`\x1b[${previousLineCount}A`);
|
|
81
|
+
process.stdout.write('\x1b[0J');
|
|
82
|
+
}
|
|
83
|
+
lines.forEach((line) => console.log(line));
|
|
84
|
+
return lines.length;
|
|
85
|
+
}
|
|
86
|
+
async function promptForVariant() {
|
|
87
|
+
const supportsRawMode = typeof input.setRawMode === 'function' && input.isTTY;
|
|
88
|
+
if (!supportsRawMode) {
|
|
89
|
+
const fallbackMessage = [
|
|
90
|
+
'Pick a package template:',
|
|
91
|
+
VARIANTS.map((opt, idx) => ` [${idx + 1}] ${opt.label}`).join('\n'),
|
|
92
|
+
`Enter 1-${VARIANTS.length}: `,
|
|
93
|
+
].join('\n');
|
|
94
|
+
return promptSingleKey(fallbackMessage, (key) => {
|
|
95
|
+
const idx = Number.parseInt(key, 10);
|
|
96
|
+
if (Number.isInteger(idx) && idx >= 1 && idx <= VARIANTS.length) {
|
|
97
|
+
return VARIANTS[idx - 1];
|
|
98
|
+
}
|
|
99
|
+
return undefined;
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
const wasRaw = input.isRaw;
|
|
103
|
+
if (!wasRaw) {
|
|
104
|
+
input.setRawMode(true);
|
|
105
|
+
input.resume();
|
|
106
|
+
}
|
|
107
|
+
process.stdout.write('\x1b[?25l');
|
|
108
|
+
return new Promise((resolve) => {
|
|
109
|
+
let selectedIndex = 0;
|
|
110
|
+
let renderedLines = 0;
|
|
111
|
+
const cleanup = () => {
|
|
112
|
+
if (renderedLines > 0) {
|
|
113
|
+
process.stdout.write(`\x1b[${renderedLines}A`);
|
|
114
|
+
process.stdout.write('\x1b[0J');
|
|
115
|
+
renderedLines = 0;
|
|
116
|
+
}
|
|
117
|
+
process.stdout.write('\x1b[?25h');
|
|
118
|
+
if (!wasRaw) {
|
|
119
|
+
input.setRawMode(false);
|
|
120
|
+
input.pause();
|
|
121
|
+
}
|
|
122
|
+
input.removeListener('data', onData);
|
|
123
|
+
};
|
|
124
|
+
const commitSelection = (variant) => {
|
|
125
|
+
cleanup();
|
|
126
|
+
console.log();
|
|
127
|
+
resolve(variant);
|
|
128
|
+
};
|
|
129
|
+
const render = () => {
|
|
130
|
+
const lines = formatVariantLines(VARIANTS, selectedIndex);
|
|
131
|
+
renderedLines = renderInteractiveList(lines, renderedLines);
|
|
132
|
+
};
|
|
133
|
+
const onData = (buffer) => {
|
|
134
|
+
const isArrowUp = buffer.equals(Buffer.from([0x1b, 0x5b, 0x41]));
|
|
135
|
+
const isArrowDown = buffer.equals(Buffer.from([0x1b, 0x5b, 0x42]));
|
|
136
|
+
const isCtrlC = buffer.length === 1 && buffer[0] === 0x03;
|
|
137
|
+
const isEnter = buffer.length === 1 && (buffer[0] === 0x0d || buffer[0] === 0x0a);
|
|
138
|
+
const isEscape = buffer.length === 1 && buffer[0] === 0x1b;
|
|
139
|
+
if (isCtrlC || isEscape) {
|
|
140
|
+
cleanup();
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
if (isArrowUp ||
|
|
144
|
+
(buffer.length === 1 && (buffer[0] === 0x6b || buffer[0] === 0x4b))) {
|
|
145
|
+
selectedIndex = (selectedIndex - 1 + VARIANTS.length) % VARIANTS.length;
|
|
146
|
+
render();
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (isArrowDown ||
|
|
150
|
+
(buffer.length === 1 && (buffer[0] === 0x6a || buffer[0] === 0x4a))) {
|
|
151
|
+
selectedIndex = (selectedIndex + 1) % VARIANTS.length;
|
|
152
|
+
render();
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (isEnter) {
|
|
156
|
+
commitSelection(VARIANTS[selectedIndex]);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (buffer.length === 1 && buffer[0] >= 0x30 && buffer[0] <= 0x39) {
|
|
160
|
+
const numericValue = buffer[0] === 0x30 ? 10 : buffer[0] - 0x30;
|
|
161
|
+
const idx = numericValue - 1;
|
|
162
|
+
if (idx >= 0 && idx < VARIANTS.length) {
|
|
163
|
+
commitSelection(VARIANTS[idx]);
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
process.stdout.write('\x07');
|
|
167
|
+
}
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
input.on('data', onData);
|
|
172
|
+
render();
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
async function promptForTargetDir(fallback) {
|
|
176
|
+
const answer = await askLine(`Path for the new package? (${fallback}): `);
|
|
177
|
+
const normalized = answer || fallback;
|
|
178
|
+
return path.resolve(workspaceRoot, normalized);
|
|
179
|
+
}
|
|
180
|
+
async function postCreateTasks(targetDir) {
|
|
181
|
+
try {
|
|
182
|
+
logGlobal('Running pnpm install…', colors.cyan);
|
|
183
|
+
await runCommand('pnpm', ['install'], workspaceRoot);
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
logGlobal(`pnpm install failed: ${error instanceof Error ? error.message : String(error)}`, colors.yellow);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
try {
|
|
190
|
+
const pkgJsonPath = path.join(targetDir, 'package.json');
|
|
191
|
+
const pkgRaw = await readFile(pkgJsonPath, 'utf8');
|
|
192
|
+
const pkg = JSON.parse(pkgRaw);
|
|
193
|
+
if (pkg.scripts?.build) {
|
|
194
|
+
logGlobal('Running pnpm run build for the new package…', colors.cyan);
|
|
195
|
+
await runCommand('pnpm', ['-C', targetDir, 'run', 'build'], workspaceRoot);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
logGlobal(`Skipping build (could not read package.json): ${error instanceof Error ? error.message : String(error)}`, colors.yellow);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
async function gatherTarget() {
|
|
203
|
+
const variant = await promptForVariant();
|
|
204
|
+
const targetDir = await promptForTargetDir(variant.defaultDir);
|
|
205
|
+
const pkgName = derivePackageName(targetDir);
|
|
206
|
+
await ensureTargetDir(targetDir);
|
|
207
|
+
return { variant, targetDir, pkgName };
|
|
208
|
+
}
|
|
209
|
+
export async function createRrrPackage() {
|
|
210
|
+
const target = await gatherTarget();
|
|
211
|
+
logGlobal(`Creating ${target.variant.label} in ${path.relative(workspaceRoot, target.targetDir) || '.'}`, colors.green);
|
|
212
|
+
await target.variant.scaffold({
|
|
213
|
+
targetDir: target.targetDir,
|
|
214
|
+
pkgName: target.pkgName,
|
|
215
|
+
});
|
|
216
|
+
await postCreateTasks(target.targetDir);
|
|
217
|
+
logGlobal('Scaffold complete. Install/build steps were attempted; ready to run!', colors.green);
|
|
218
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { access, mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
export const workspaceRoot = process.cwd();
|
|
4
|
+
export async function writeFileIfMissing(baseDir, relative, contents) {
|
|
5
|
+
const fullPath = path.join(baseDir, relative);
|
|
6
|
+
await mkdir(path.dirname(fullPath), { recursive: true });
|
|
7
|
+
try {
|
|
8
|
+
await access(fullPath);
|
|
9
|
+
const rel = path.relative(workspaceRoot, fullPath);
|
|
10
|
+
console.log(` skipped ${rel} (already exists)`);
|
|
11
|
+
return 'skipped';
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
if (error &&
|
|
15
|
+
typeof error === 'object' &&
|
|
16
|
+
error.code !== 'ENOENT') {
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
await writeFile(fullPath, contents, 'utf8');
|
|
21
|
+
const rel = path.relative(workspaceRoot, fullPath);
|
|
22
|
+
console.log(` created ${rel}`);
|
|
23
|
+
return 'created';
|
|
24
|
+
}
|
|
25
|
+
export function baseTsConfig(options) {
|
|
26
|
+
return `${JSON.stringify({
|
|
27
|
+
compilerOptions: {
|
|
28
|
+
target: 'ES2020',
|
|
29
|
+
module: 'NodeNext',
|
|
30
|
+
moduleResolution: 'NodeNext',
|
|
31
|
+
outDir: options?.outDir ?? 'dist',
|
|
32
|
+
rootDir: options?.rootDir ?? 'src',
|
|
33
|
+
declaration: true,
|
|
34
|
+
sourceMap: true,
|
|
35
|
+
strict: true,
|
|
36
|
+
esModuleInterop: true,
|
|
37
|
+
skipLibCheck: true,
|
|
38
|
+
lib: options?.lib,
|
|
39
|
+
types: options?.types,
|
|
40
|
+
jsx: options?.jsx,
|
|
41
|
+
},
|
|
42
|
+
include: options?.include ?? ['src/**/*'],
|
|
43
|
+
}, null, 2)}\n`;
|
|
44
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { baseTsConfig, writeFileIfMissing, } from '../shared.js';
|
|
2
|
+
const CONTRACT_IMPORT_PLACEHOLDER = '@your-scope/contract';
|
|
3
|
+
function clientIndexTs(contractImport) {
|
|
4
|
+
return `import { QueryClient } from '@tanstack/react-query'
|
|
5
|
+
import { createRouteClient } from '@emeryld/rrroutes-client'
|
|
6
|
+
import { registry } from '${contractImport}'
|
|
7
|
+
|
|
8
|
+
const baseUrl = process.env.RRR_API_URL ?? 'http://localhost:4000'
|
|
9
|
+
export const queryClient = new QueryClient()
|
|
10
|
+
|
|
11
|
+
export const routeClient = createRouteClient({
|
|
12
|
+
baseUrl,
|
|
13
|
+
queryClient,
|
|
14
|
+
environment: process.env.NODE_ENV === 'production' ? 'production' : 'development',
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
export const healthGet = routeClient.build(registry.byKey['GET /api/health'])
|
|
18
|
+
export const healthPost = routeClient.build(registry.byKey['POST /api/health'])
|
|
19
|
+
`;
|
|
20
|
+
}
|
|
21
|
+
function clientPackageJson(name) {
|
|
22
|
+
return `${JSON.stringify({
|
|
23
|
+
name,
|
|
24
|
+
version: '0.1.0',
|
|
25
|
+
private: true,
|
|
26
|
+
type: 'module',
|
|
27
|
+
main: 'dist/index.js',
|
|
28
|
+
types: 'dist/index.d.ts',
|
|
29
|
+
files: ['dist'],
|
|
30
|
+
scripts: {
|
|
31
|
+
build: 'tsc -p tsconfig.json',
|
|
32
|
+
typecheck: 'tsc -p tsconfig.json --noEmit',
|
|
33
|
+
},
|
|
34
|
+
dependencies: {
|
|
35
|
+
'@emeryld/rrroutes-client': '^2.5.3',
|
|
36
|
+
'@emeryld/rrroutes-contract': '^2.5.2',
|
|
37
|
+
'@tanstack/react-query': '^5.90.12',
|
|
38
|
+
'socket.io-client': '^4.8.3',
|
|
39
|
+
},
|
|
40
|
+
devDependencies: {
|
|
41
|
+
'@types/node': '^24.10.2',
|
|
42
|
+
typescript: '^5.9.3',
|
|
43
|
+
},
|
|
44
|
+
}, null, 2)}\n`;
|
|
45
|
+
}
|
|
46
|
+
function clientFiles(pkgName, contractImport) {
|
|
47
|
+
return {
|
|
48
|
+
'package.json': clientPackageJson(pkgName),
|
|
49
|
+
'tsconfig.json': baseTsConfig({ lib: ['ES2020', 'DOM'], types: ['node'] }),
|
|
50
|
+
'src/index.ts': clientIndexTs(contractImport),
|
|
51
|
+
'README.md': `# ${pkgName}
|
|
52
|
+
|
|
53
|
+
Starter RRRoutes client scaffold.
|
|
54
|
+
- update the contract import in src/index.ts if needed (${contractImport})
|
|
55
|
+
- the generated QueryClient is exported from src/index.ts
|
|
56
|
+
`,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
export const clientVariant = {
|
|
60
|
+
id: 'rrr-client',
|
|
61
|
+
label: 'rrr client',
|
|
62
|
+
defaultDir: 'packages/rrr-client',
|
|
63
|
+
async scaffold(ctx) {
|
|
64
|
+
const files = clientFiles(ctx.pkgName, CONTRACT_IMPORT_PLACEHOLDER);
|
|
65
|
+
for (const [relative, contents] of Object.entries(files)) {
|
|
66
|
+
// eslint-disable-next-line no-await-in-loop
|
|
67
|
+
await writeFileIfMissing(ctx.targetDir, relative, contents);
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { baseTsConfig, writeFileIfMissing, } from '../shared.js';
|
|
2
|
+
const CONTRACT_TS = `import { defineSocketEvents, finalize, resource } from '@emeryld/rrroutes-contract'
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
|
|
5
|
+
const routes = resource('/api')
|
|
6
|
+
.sub(
|
|
7
|
+
resource('health')
|
|
8
|
+
.get({
|
|
9
|
+
outputSchema: z.object({
|
|
10
|
+
status: z.literal('ok'),
|
|
11
|
+
html: z.string().optional(),
|
|
12
|
+
}),
|
|
13
|
+
description: 'Basic GET health probe for uptime + docs.',
|
|
14
|
+
})
|
|
15
|
+
.post({
|
|
16
|
+
bodySchema: z.object({
|
|
17
|
+
echo: z.string().optional(),
|
|
18
|
+
}),
|
|
19
|
+
outputSchema: z.object({
|
|
20
|
+
status: z.literal('ok'),
|
|
21
|
+
received: z.string().optional(),
|
|
22
|
+
}),
|
|
23
|
+
description: 'POST health probe that echoes a payload.',
|
|
24
|
+
})
|
|
25
|
+
.done(),
|
|
26
|
+
)
|
|
27
|
+
.done()
|
|
28
|
+
|
|
29
|
+
export const registry = finalize(routes)
|
|
30
|
+
|
|
31
|
+
const sockets = defineSocketEvents(
|
|
32
|
+
{
|
|
33
|
+
joinMetaMessage: z.object({ room: z.string().optional() }),
|
|
34
|
+
leaveMetaMessage: z.object({ room: z.string().optional() }),
|
|
35
|
+
pingPayload: z.object({
|
|
36
|
+
note: z.string().default('ping'),
|
|
37
|
+
sentAt: z.string(),
|
|
38
|
+
}),
|
|
39
|
+
pongPayload: z.object({
|
|
40
|
+
ok: z.boolean(),
|
|
41
|
+
receivedAt: z.string(),
|
|
42
|
+
echo: z.string().optional(),
|
|
43
|
+
}),
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
'health:connected': {
|
|
47
|
+
message: z.object({
|
|
48
|
+
socketId: z.string(),
|
|
49
|
+
at: z.string(),
|
|
50
|
+
message: z.string(),
|
|
51
|
+
}),
|
|
52
|
+
},
|
|
53
|
+
'health:ping': {
|
|
54
|
+
message: z.object({
|
|
55
|
+
note: z.string().default('ping'),
|
|
56
|
+
}),
|
|
57
|
+
},
|
|
58
|
+
'health:pong': {
|
|
59
|
+
message: z.object({
|
|
60
|
+
ok: z.literal(true),
|
|
61
|
+
at: z.string(),
|
|
62
|
+
echo: z.string().optional(),
|
|
63
|
+
}),
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
export const socketConfig = sockets.config
|
|
69
|
+
export const socketEvents = sockets.events
|
|
70
|
+
export type AppRegistry = typeof registry
|
|
71
|
+
`;
|
|
72
|
+
function contractPackageJson(name) {
|
|
73
|
+
return `${JSON.stringify({
|
|
74
|
+
name,
|
|
75
|
+
version: '0.1.0',
|
|
76
|
+
private: false,
|
|
77
|
+
type: 'module',
|
|
78
|
+
main: 'dist/index.js',
|
|
79
|
+
types: 'dist/index.d.ts',
|
|
80
|
+
exports: {
|
|
81
|
+
'.': {
|
|
82
|
+
types: './dist/index.d.ts',
|
|
83
|
+
import: './dist/index.js',
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
files: ['dist'],
|
|
87
|
+
scripts: {
|
|
88
|
+
build: 'tsc -p tsconfig.json',
|
|
89
|
+
typecheck: 'tsc -p tsconfig.json --noEmit',
|
|
90
|
+
},
|
|
91
|
+
dependencies: {
|
|
92
|
+
'@emeryld/rrroutes-contract': '^2.5.2',
|
|
93
|
+
zod: '^4.2.1',
|
|
94
|
+
},
|
|
95
|
+
devDependencies: {
|
|
96
|
+
typescript: '^5.9.3',
|
|
97
|
+
},
|
|
98
|
+
}, null, 2)}\n`;
|
|
99
|
+
}
|
|
100
|
+
function contractFiles(pkgName) {
|
|
101
|
+
return {
|
|
102
|
+
'package.json': contractPackageJson(pkgName),
|
|
103
|
+
'tsconfig.json': baseTsConfig(),
|
|
104
|
+
'src/index.ts': CONTRACT_TS,
|
|
105
|
+
'README.md': `# ${pkgName}
|
|
106
|
+
|
|
107
|
+
Contract package scaffolded by manager-cli.
|
|
108
|
+
- edit src/index.ts to add routes and socket events
|
|
109
|
+
- build with \`npm run build\`
|
|
110
|
+
- import the registry in your server/client packages
|
|
111
|
+
`,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
export const contractVariant = {
|
|
115
|
+
id: 'rrr-contract',
|
|
116
|
+
label: 'rrr contract',
|
|
117
|
+
defaultDir: 'packages/rrr-contract',
|
|
118
|
+
async scaffold(ctx) {
|
|
119
|
+
const files = contractFiles(ctx.pkgName);
|
|
120
|
+
for (const [relative, contents] of Object.entries(files)) {
|
|
121
|
+
// eslint-disable-next-line no-await-in-loop
|
|
122
|
+
await writeFileIfMissing(ctx.targetDir, relative, contents);
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { baseTsConfig, writeFileIfMissing, } from '../shared.js';
|
|
2
|
+
function dockerPackageJson(name) {
|
|
3
|
+
return `${JSON.stringify({
|
|
4
|
+
name,
|
|
5
|
+
version: '0.1.0',
|
|
6
|
+
private: true,
|
|
7
|
+
type: 'module',
|
|
8
|
+
main: 'dist/index.js',
|
|
9
|
+
types: 'dist/index.d.ts',
|
|
10
|
+
files: ['dist'],
|
|
11
|
+
scripts: {
|
|
12
|
+
dev: 'tsx watch src/index.ts',
|
|
13
|
+
build: 'tsc -p tsconfig.json',
|
|
14
|
+
typecheck: 'tsc -p tsconfig.json --noEmit',
|
|
15
|
+
start: 'node dist/index.js',
|
|
16
|
+
},
|
|
17
|
+
dependencies: {
|
|
18
|
+
cors: '^2.8.5',
|
|
19
|
+
express: '^5.1.0',
|
|
20
|
+
},
|
|
21
|
+
devDependencies: {
|
|
22
|
+
'@types/cors': '^2.8.5',
|
|
23
|
+
'@types/express': '^5.0.6',
|
|
24
|
+
'@types/node': '^24.10.2',
|
|
25
|
+
tsx: '^4.19.0',
|
|
26
|
+
typescript: '^5.9.3',
|
|
27
|
+
},
|
|
28
|
+
}, null, 2)}\n`;
|
|
29
|
+
}
|
|
30
|
+
function dockerIndexTs() {
|
|
31
|
+
return `import express from 'express'
|
|
32
|
+
import cors from 'cors'
|
|
33
|
+
|
|
34
|
+
const app = express()
|
|
35
|
+
app.use(cors({ origin: '*' }))
|
|
36
|
+
app.use(express.json())
|
|
37
|
+
|
|
38
|
+
app.get('/api/health', (_req, res) => {
|
|
39
|
+
res.json({ status: 'ok', at: new Date().toISOString() })
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
const PORT = Number.parseInt(process.env.PORT ?? '3000', 10)
|
|
43
|
+
|
|
44
|
+
app.listen(PORT, () => {
|
|
45
|
+
console.log(\`Docker-ready service listening on http://localhost:\${PORT}\`)
|
|
46
|
+
})
|
|
47
|
+
`;
|
|
48
|
+
}
|
|
49
|
+
const DOCKER_DOCKERIGNORE = `node_modules
|
|
50
|
+
dist
|
|
51
|
+
.git
|
|
52
|
+
.env
|
|
53
|
+
npm-debug.log*
|
|
54
|
+
pnpm-lock.yaml
|
|
55
|
+
yarn.lock
|
|
56
|
+
`;
|
|
57
|
+
function dockerDockerfile() {
|
|
58
|
+
return `FROM node:20-slim AS builder
|
|
59
|
+
WORKDIR /app
|
|
60
|
+
|
|
61
|
+
COPY package*.json ./
|
|
62
|
+
COPY pnpm-lock.yaml* ./
|
|
63
|
+
RUN npm install
|
|
64
|
+
|
|
65
|
+
COPY . .
|
|
66
|
+
RUN npm run build
|
|
67
|
+
|
|
68
|
+
FROM node:20-slim AS runner
|
|
69
|
+
WORKDIR /app
|
|
70
|
+
ENV NODE_ENV=production
|
|
71
|
+
|
|
72
|
+
COPY --from=builder /app/package*.json ./
|
|
73
|
+
COPY --from=builder /app/node_modules ./node_modules
|
|
74
|
+
COPY --from=builder /app/dist ./dist
|
|
75
|
+
|
|
76
|
+
EXPOSE 3000
|
|
77
|
+
CMD ["node", "dist/index.js"]
|
|
78
|
+
`;
|
|
79
|
+
}
|
|
80
|
+
function dockerFiles(pkgName) {
|
|
81
|
+
return {
|
|
82
|
+
'package.json': dockerPackageJson(pkgName),
|
|
83
|
+
'tsconfig.json': baseTsConfig({ types: ['node'] }),
|
|
84
|
+
'src/index.ts': dockerIndexTs(),
|
|
85
|
+
'.dockerignore': DOCKER_DOCKERIGNORE,
|
|
86
|
+
Dockerfile: dockerDockerfile(),
|
|
87
|
+
'README.md': `# ${pkgName}
|
|
88
|
+
|
|
89
|
+
Dockerized service scaffolded by manager-cli.
|
|
90
|
+
- develop locally with \`npm run dev\`
|
|
91
|
+
- build with \`npm run build\` and start with \`npm start\`
|
|
92
|
+
- build/publish container: \`docker build -t ${pkgName}:latest .\`
|
|
93
|
+
- run container: \`docker run -p 3000:3000 ${pkgName}:latest\`
|
|
94
|
+
`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
export const dockerVariant = {
|
|
98
|
+
id: 'rrr-docker',
|
|
99
|
+
label: 'dockerized service',
|
|
100
|
+
defaultDir: 'packages/rrr-docker',
|
|
101
|
+
async scaffold(ctx) {
|
|
102
|
+
const files = dockerFiles(ctx.pkgName);
|
|
103
|
+
for (const [relative, contents] of Object.entries(files)) {
|
|
104
|
+
// eslint-disable-next-line no-await-in-loop
|
|
105
|
+
await writeFileIfMissing(ctx.targetDir, relative, contents);
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { baseTsConfig, writeFileIfMissing, } from '../shared.js';
|
|
2
|
+
function emptyPackageJson(name) {
|
|
3
|
+
return `${JSON.stringify({
|
|
4
|
+
name,
|
|
5
|
+
version: '0.1.0',
|
|
6
|
+
private: true,
|
|
7
|
+
type: 'module',
|
|
8
|
+
main: 'dist/index.js',
|
|
9
|
+
types: 'dist/index.d.ts',
|
|
10
|
+
files: ['dist'],
|
|
11
|
+
scripts: {
|
|
12
|
+
build: 'tsc -p tsconfig.json',
|
|
13
|
+
typecheck: 'tsc -p tsconfig.json --noEmit',
|
|
14
|
+
},
|
|
15
|
+
devDependencies: {
|
|
16
|
+
typescript: '^5.9.3',
|
|
17
|
+
},
|
|
18
|
+
}, null, 2)}\n`;
|
|
19
|
+
}
|
|
20
|
+
function emptyFiles(pkgName) {
|
|
21
|
+
return {
|
|
22
|
+
'package.json': emptyPackageJson(pkgName),
|
|
23
|
+
'tsconfig.json': baseTsConfig({ types: ['node'] }),
|
|
24
|
+
'src/index.ts': "export const hello = 'world'\n",
|
|
25
|
+
'README.md': `# ${pkgName}
|
|
26
|
+
|
|
27
|
+
Empty package scaffolded by manager-cli.
|
|
28
|
+
- edit src/index.ts to start coding
|
|
29
|
+
- build with \`npm run build\`
|
|
30
|
+
`,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export const emptyVariant = {
|
|
34
|
+
id: 'rrr-empty',
|
|
35
|
+
label: 'empty package',
|
|
36
|
+
defaultDir: 'packages/rrr-empty',
|
|
37
|
+
async scaffold(ctx) {
|
|
38
|
+
const files = emptyFiles(ctx.pkgName);
|
|
39
|
+
for (const [relative, contents] of Object.entries(files)) {
|
|
40
|
+
// eslint-disable-next-line no-await-in-loop
|
|
41
|
+
await writeFileIfMissing(ctx.targetDir, relative, contents);
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
};
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { baseTsConfig, writeFileIfMissing, } from '../shared.js';
|
|
2
|
+
function fullstackPackageJson(name) {
|
|
3
|
+
return `${JSON.stringify({
|
|
4
|
+
name,
|
|
5
|
+
version: '0.1.0',
|
|
6
|
+
private: true,
|
|
7
|
+
type: 'module',
|
|
8
|
+
main: 'dist/server/index.js',
|
|
9
|
+
files: ['dist'],
|
|
10
|
+
scripts: {
|
|
11
|
+
dev: 'concurrently "npm:dev:server" "npm:dev:client"',
|
|
12
|
+
'dev:server': 'tsx watch src/server/index.ts',
|
|
13
|
+
'dev:client': 'vite --host --port 5173',
|
|
14
|
+
build: 'npm run build:server && npm run build:client',
|
|
15
|
+
'build:server': 'tsc -p tsconfig.server.json',
|
|
16
|
+
'build:client': 'vite build',
|
|
17
|
+
start: 'node dist/server/index.js',
|
|
18
|
+
typecheck: 'tsc -p tsconfig.json --noEmit',
|
|
19
|
+
},
|
|
20
|
+
dependencies: {
|
|
21
|
+
cors: '^2.8.5',
|
|
22
|
+
express: '^5.1.0',
|
|
23
|
+
react: '^18.3.1',
|
|
24
|
+
'react-dom': '^18.3.1',
|
|
25
|
+
},
|
|
26
|
+
devDependencies: {
|
|
27
|
+
'@types/express': '^5.0.6',
|
|
28
|
+
'@types/node': '^24.10.2',
|
|
29
|
+
'@types/react': '^18.3.27',
|
|
30
|
+
'@types/react-dom': '^18.3.7',
|
|
31
|
+
'@vitejs/plugin-react': '^4.3.4',
|
|
32
|
+
concurrently: '^8.2.0',
|
|
33
|
+
tsx: '^4.19.0',
|
|
34
|
+
typescript: '^5.9.3',
|
|
35
|
+
vite: '^6.4.1',
|
|
36
|
+
},
|
|
37
|
+
}, null, 2)}\n`;
|
|
38
|
+
}
|
|
39
|
+
function fullstackServerIndexTs() {
|
|
40
|
+
return `import { existsSync } from 'node:fs'
|
|
41
|
+
import path from 'node:path'
|
|
42
|
+
import express from 'express'
|
|
43
|
+
import cors from 'cors'
|
|
44
|
+
|
|
45
|
+
const app = express()
|
|
46
|
+
app.use(cors({ origin: '*' }))
|
|
47
|
+
app.use(express.json())
|
|
48
|
+
|
|
49
|
+
app.get('/api/health', (_req, res) => {
|
|
50
|
+
res.json({ status: 'ok', at: new Date().toISOString() })
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const clientDir = path.resolve(__dirname, '../client')
|
|
54
|
+
if (existsSync(clientDir)) {
|
|
55
|
+
app.use(express.static(clientDir))
|
|
56
|
+
app.get('*', (_req, res) => {
|
|
57
|
+
res.sendFile(path.join(clientDir, 'index.html'))
|
|
58
|
+
})
|
|
59
|
+
} else {
|
|
60
|
+
console.warn('Client bundle missing; run "npm run build:client" to enable static assets.')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const PORT = Number.parseInt(process.env.PORT ?? '8080', 10)
|
|
64
|
+
|
|
65
|
+
app.listen(PORT, () => {
|
|
66
|
+
console.log(\`Full stack service running on http://localhost:\${PORT}\`)
|
|
67
|
+
})
|
|
68
|
+
`;
|
|
69
|
+
}
|
|
70
|
+
const FULLSTACK_APP_TSX = `import React from 'react'
|
|
71
|
+
|
|
72
|
+
export function App() {
|
|
73
|
+
return (
|
|
74
|
+
<main style={{ fontFamily: 'Inter, system-ui, sans-serif', padding: 24 }}>
|
|
75
|
+
<h1>Full stack service</h1>
|
|
76
|
+
<p>Backend: <code>/api/health</code> responds with status + timestamp.</p>
|
|
77
|
+
<p>Edit <code>src/client/App.tsx</code> to start building.</p>
|
|
78
|
+
</main>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
`;
|
|
82
|
+
const FULLSTACK_MAIN_TSX = `import React from 'react'
|
|
83
|
+
import ReactDOM from 'react-dom/client'
|
|
84
|
+
import { App } from './App'
|
|
85
|
+
import './styles.css'
|
|
86
|
+
|
|
87
|
+
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
88
|
+
<React.StrictMode>
|
|
89
|
+
<App />
|
|
90
|
+
</React.StrictMode>,
|
|
91
|
+
)
|
|
92
|
+
`;
|
|
93
|
+
const FULLSTACK_STYLES = `:root {
|
|
94
|
+
background: radial-gradient(circle at 20% 20%, #eef2ff, #f7f8fb 45%);
|
|
95
|
+
color: #0b1021;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
body {
|
|
99
|
+
margin: 0;
|
|
100
|
+
}
|
|
101
|
+
`;
|
|
102
|
+
const FULLSTACK_INDEX_HTML = `<!doctype html>
|
|
103
|
+
<html lang="en">
|
|
104
|
+
<head>
|
|
105
|
+
<meta charset="UTF-8" />
|
|
106
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
107
|
+
<title>Full stack service</title>
|
|
108
|
+
</head>
|
|
109
|
+
<body>
|
|
110
|
+
<div id="root"></div>
|
|
111
|
+
<script type="module" src="/src/client/main.tsx"></script>
|
|
112
|
+
</body>
|
|
113
|
+
</html>
|
|
114
|
+
`;
|
|
115
|
+
function fullstackViteConfig() {
|
|
116
|
+
return `import { defineConfig } from 'vite'
|
|
117
|
+
import react from '@vitejs/plugin-react'
|
|
118
|
+
|
|
119
|
+
export default defineConfig({
|
|
120
|
+
plugins: [react()],
|
|
121
|
+
build: {
|
|
122
|
+
outDir: 'dist/client',
|
|
123
|
+
},
|
|
124
|
+
})
|
|
125
|
+
`;
|
|
126
|
+
}
|
|
127
|
+
const FULLSTACK_DOCKERIGNORE = `node_modules
|
|
128
|
+
dist
|
|
129
|
+
.git
|
|
130
|
+
.env
|
|
131
|
+
npm-debug.log*
|
|
132
|
+
pnpm-lock.yaml
|
|
133
|
+
yarn.lock
|
|
134
|
+
`;
|
|
135
|
+
function fullstackDockerfile() {
|
|
136
|
+
return `FROM node:20-slim AS builder
|
|
137
|
+
WORKDIR /app
|
|
138
|
+
|
|
139
|
+
COPY package*.json ./
|
|
140
|
+
COPY pnpm-lock.yaml* ./
|
|
141
|
+
RUN npm install
|
|
142
|
+
|
|
143
|
+
COPY . .
|
|
144
|
+
RUN npm run build
|
|
145
|
+
|
|
146
|
+
FROM node:20-slim AS runner
|
|
147
|
+
WORKDIR /app
|
|
148
|
+
ENV NODE_ENV=production
|
|
149
|
+
|
|
150
|
+
COPY --from=builder /app/package*.json ./
|
|
151
|
+
COPY --from=builder /app/node_modules ./node_modules
|
|
152
|
+
COPY --from=builder /app/dist ./dist
|
|
153
|
+
RUN npm prune --omit=dev || true
|
|
154
|
+
|
|
155
|
+
EXPOSE 8080
|
|
156
|
+
CMD ["node", "dist/server/index.js"]
|
|
157
|
+
`;
|
|
158
|
+
}
|
|
159
|
+
function fullstackFiles(pkgName) {
|
|
160
|
+
return {
|
|
161
|
+
'package.json': fullstackPackageJson(pkgName),
|
|
162
|
+
'tsconfig.json': baseTsConfig({
|
|
163
|
+
lib: ['ES2020', 'DOM'],
|
|
164
|
+
types: ['node'],
|
|
165
|
+
jsx: 'react-jsx',
|
|
166
|
+
include: ['src/**/*', 'vite.config.ts'],
|
|
167
|
+
}),
|
|
168
|
+
'tsconfig.server.json': baseTsConfig({
|
|
169
|
+
types: ['node'],
|
|
170
|
+
rootDir: 'src/server',
|
|
171
|
+
outDir: 'dist/server',
|
|
172
|
+
include: ['src/server/**/*'],
|
|
173
|
+
}),
|
|
174
|
+
'vite.config.ts': fullstackViteConfig(),
|
|
175
|
+
'src/server/index.ts': fullstackServerIndexTs(),
|
|
176
|
+
'src/client/App.tsx': FULLSTACK_APP_TSX,
|
|
177
|
+
'src/client/main.tsx': FULLSTACK_MAIN_TSX,
|
|
178
|
+
'src/client/styles.css': FULLSTACK_STYLES,
|
|
179
|
+
'index.html': FULLSTACK_INDEX_HTML,
|
|
180
|
+
'.dockerignore': FULLSTACK_DOCKERIGNORE,
|
|
181
|
+
Dockerfile: fullstackDockerfile(),
|
|
182
|
+
'.env.example': 'PORT=8080\n',
|
|
183
|
+
'README.md': `# ${pkgName}
|
|
184
|
+
|
|
185
|
+
Full stack (API + Vite web) scaffolded by manager-cli.
|
|
186
|
+
- dev: \`npm run dev\` (runs API + Vite client)
|
|
187
|
+
- build: \`npm run build\` (server to dist/server, client to dist/client)
|
|
188
|
+
- start: \`npm start\` (serves API and static client from dist)
|
|
189
|
+
- docker: \`docker build -t ${pkgName}:latest .\` then \`docker run -p 8080:8080 ${pkgName}:latest\`
|
|
190
|
+
`,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
export const fullstackVariant = {
|
|
194
|
+
id: 'rrr-fullstack',
|
|
195
|
+
label: 'full stack service (api + web)',
|
|
196
|
+
defaultDir: 'packages/rrr-fullstack',
|
|
197
|
+
async scaffold(ctx) {
|
|
198
|
+
const files = fullstackFiles(ctx.pkgName);
|
|
199
|
+
for (const [relative, contents] of Object.entries(files)) {
|
|
200
|
+
// eslint-disable-next-line no-await-in-loop
|
|
201
|
+
await writeFileIfMissing(ctx.targetDir, relative, contents);
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { baseTsConfig, writeFileIfMissing, } from '../shared.js';
|
|
2
|
+
const CONTRACT_IMPORT_PLACEHOLDER = '@your-scope/contract';
|
|
3
|
+
function serverIndexTs(contractImport) {
|
|
4
|
+
return `import 'dotenv/config'
|
|
5
|
+
import http from 'node:http'
|
|
6
|
+
import express from 'express'
|
|
7
|
+
import cors from 'cors'
|
|
8
|
+
import { createRRRoute } from '@emeryld/rrroutes-server'
|
|
9
|
+
import { registry } from '${contractImport}'
|
|
10
|
+
|
|
11
|
+
const app = express()
|
|
12
|
+
app.use(cors({ origin: '*', credentials: true }))
|
|
13
|
+
app.use(express.json())
|
|
14
|
+
|
|
15
|
+
app.get('/', (_req, res) => {
|
|
16
|
+
res.send('<h1>rrr server ready</h1>')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const routes = createRRRoute(app, {
|
|
20
|
+
buildCtx: async () => ({
|
|
21
|
+
requestId: Math.random().toString(36).slice(2),
|
|
22
|
+
}),
|
|
23
|
+
debug:
|
|
24
|
+
process.env.NODE_ENV === 'development'
|
|
25
|
+
? { request: true, handler: true }
|
|
26
|
+
: undefined,
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
routes.registerControllers(registry, {
|
|
30
|
+
'GET /api/health': {
|
|
31
|
+
handler: async ({ ctx }) => ({
|
|
32
|
+
out: {
|
|
33
|
+
status: 'ok',
|
|
34
|
+
requestId: ctx.requestId,
|
|
35
|
+
at: new Date().toISOString(),
|
|
36
|
+
},
|
|
37
|
+
}),
|
|
38
|
+
},
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
const PORT = Number.parseInt(process.env.PORT ?? '4000', 10)
|
|
42
|
+
const server = http.createServer(app)
|
|
43
|
+
|
|
44
|
+
server.listen(PORT, () => {
|
|
45
|
+
console.log(\`rrr server listening on http://localhost:\${PORT}\`)
|
|
46
|
+
})
|
|
47
|
+
`;
|
|
48
|
+
}
|
|
49
|
+
function serverPackageJson(name) {
|
|
50
|
+
return `${JSON.stringify({
|
|
51
|
+
name,
|
|
52
|
+
version: '0.1.0',
|
|
53
|
+
private: false,
|
|
54
|
+
type: 'module',
|
|
55
|
+
main: 'dist/index.js',
|
|
56
|
+
types: 'dist/index.d.ts',
|
|
57
|
+
files: ['dist'],
|
|
58
|
+
scripts: {
|
|
59
|
+
dev: 'node --loader ts-node/esm src/index.ts',
|
|
60
|
+
build: 'tsc -p tsconfig.json',
|
|
61
|
+
typecheck: 'tsc -p tsconfig.json --noEmit',
|
|
62
|
+
start: 'node dist/index.js',
|
|
63
|
+
},
|
|
64
|
+
dependencies: {
|
|
65
|
+
'@emeryld/rrroutes-contract': '^2.5.2',
|
|
66
|
+
'@emeryld/rrroutes-server': '^2.4.1',
|
|
67
|
+
cors: '^2.8.5',
|
|
68
|
+
dotenv: '^16.4.5',
|
|
69
|
+
express: '^5.1.0',
|
|
70
|
+
zod: '^4.2.1',
|
|
71
|
+
},
|
|
72
|
+
devDependencies: {
|
|
73
|
+
'@types/cors': '^2.8.5',
|
|
74
|
+
'@types/express': '^5.0.6',
|
|
75
|
+
'@types/node': '^24.10.2',
|
|
76
|
+
'ts-node': '^10.9.2',
|
|
77
|
+
typescript: '^5.9.3',
|
|
78
|
+
},
|
|
79
|
+
}, null, 2)}\n`;
|
|
80
|
+
}
|
|
81
|
+
function serverFiles(pkgName, contractImport) {
|
|
82
|
+
return {
|
|
83
|
+
'package.json': serverPackageJson(pkgName),
|
|
84
|
+
'tsconfig.json': baseTsConfig({ types: ['node'] }),
|
|
85
|
+
'src/index.ts': serverIndexTs(contractImport),
|
|
86
|
+
'.env.example': 'PORT=4000\n',
|
|
87
|
+
'README.md': `# ${pkgName}
|
|
88
|
+
|
|
89
|
+
Starter RRRoutes server scaffold.
|
|
90
|
+
- update the contract import in src/index.ts if needed (${contractImport})
|
|
91
|
+
- run \`npm install\` then \`npm run dev\` to start the API
|
|
92
|
+
`,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
export const serverVariant = {
|
|
96
|
+
id: 'rrr-server',
|
|
97
|
+
label: 'rrr server',
|
|
98
|
+
defaultDir: 'packages/rrr-server',
|
|
99
|
+
async scaffold(ctx) {
|
|
100
|
+
const files = serverFiles(ctx.pkgName, CONTRACT_IMPORT_PLACEHOLDER);
|
|
101
|
+
for (const [relative, contents] of Object.entries(files)) {
|
|
102
|
+
// eslint-disable-next-line no-await-in-loop
|
|
103
|
+
await writeFileIfMissing(ctx.targetDir, relative, contents);
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
};
|
package/dist/publish.js
CHANGED
|
@@ -5,7 +5,7 @@ import { getOrderedPackages, loadPackages, resolvePackage } from './packages.js'
|
|
|
5
5
|
import { releaseMultiple, releaseSingle, } from './release.js';
|
|
6
6
|
import { ensureWorkingTreeCommitted } from './preflight.js';
|
|
7
7
|
import { publishCliState } from './prompts.js';
|
|
8
|
-
import { createRrrPackage } from './create-package.js';
|
|
8
|
+
import { createRrrPackage } from './create-package/index.js';
|
|
9
9
|
import { colors, logGlobal } from './utils/log.js';
|
|
10
10
|
function resolveTargetsFromArg(packages, arg) {
|
|
11
11
|
if (arg.toLowerCase() === 'all')
|