@emeryld/manager 0.3.3 → 0.4.1
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 +40 -42
- package/dist/create-package/index.js +177 -10
- package/dist/create-package/shared.js +183 -0
- package/dist/create-package/variants/client.js +56 -25
- package/dist/create-package/variants/contract.js +46 -21
- package/dist/create-package/variants/docker.js +96 -29
- package/dist/create-package/variants/empty.js +40 -20
- package/dist/create-package/variants/fullstack.js +130 -406
- package/dist/create-package/variants/server.js +59 -26
- package/dist/publish.js +9 -1
- package/package.json +1 -1
|
@@ -1,219 +1,84 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { BASE_LINT_DEV_DEPENDENCIES, basePackageFiles, basePackageJson, baseTsConfig, writeFileIfMissing, } from '../shared.js';
|
|
3
|
+
import { clientVariant } from './client.js';
|
|
4
|
+
import { serverVariant } from './server.js';
|
|
5
|
+
import { dockerVariant } from './docker.js';
|
|
6
|
+
import { contractVariant } from './contract.js';
|
|
7
|
+
const FULLSTACK_SCRIPTS = [
|
|
8
|
+
'setup',
|
|
9
|
+
'dev',
|
|
10
|
+
'build',
|
|
11
|
+
'typecheck',
|
|
12
|
+
'lint',
|
|
13
|
+
'lint:fix',
|
|
14
|
+
'lint-staged',
|
|
15
|
+
'format',
|
|
16
|
+
'format:check',
|
|
17
|
+
'test',
|
|
18
|
+
'clean',
|
|
19
|
+
'prepare',
|
|
20
|
+
'docker:up',
|
|
21
|
+
'docker:dev',
|
|
22
|
+
'docker:logs',
|
|
23
|
+
'docker:stop',
|
|
24
|
+
'docker:reset',
|
|
25
|
+
];
|
|
26
|
+
function deriveNames(baseName) {
|
|
27
|
+
const normalized = baseName.trim();
|
|
28
|
+
return {
|
|
29
|
+
contract: `@${normalized}/contract`,
|
|
30
|
+
server: `@${normalized}/server`,
|
|
31
|
+
client: `@${normalized}/client`,
|
|
32
|
+
docker: `${normalized}-docker`,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function deriveDirs(rootDir, baseName) {
|
|
36
|
+
const packagesRoot = path.join(rootDir, 'packages');
|
|
37
|
+
return {
|
|
38
|
+
root: rootDir,
|
|
39
|
+
packagesRoot,
|
|
40
|
+
contract: path.join(packagesRoot, `${baseName}-contract`),
|
|
41
|
+
server: path.join(packagesRoot, `${baseName}-server`),
|
|
42
|
+
client: path.join(packagesRoot, `${baseName}-client`),
|
|
43
|
+
docker: path.join(packagesRoot, `${baseName}-docker`),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function rootPackageJson(baseName) {
|
|
47
|
+
const dockerPackageDir = `packages/${baseName}-docker`;
|
|
48
|
+
return basePackageJson({
|
|
49
|
+
name: `${baseName}-stack`,
|
|
6
50
|
private: true,
|
|
7
|
-
|
|
8
|
-
main: 'dist/server/index.js',
|
|
9
|
-
files: ['dist'],
|
|
51
|
+
useDefaults: false,
|
|
10
52
|
scripts: {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
'
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
'
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
'docker:
|
|
24
|
-
'docker:
|
|
25
|
-
'
|
|
26
|
-
'
|
|
27
|
-
|
|
28
|
-
dependencies: {
|
|
29
|
-
'@emeryld/rrroutes-client': '^2.5.3',
|
|
30
|
-
'@emeryld/rrroutes-contract': '^2.5.2',
|
|
31
|
-
'@emeryld/rrroutes-server': '^2.4.1',
|
|
32
|
-
'@tanstack/react-query': '^5.90.12',
|
|
33
|
-
cors: '^2.8.5',
|
|
34
|
-
express: '^5.1.0',
|
|
35
|
-
pg: '^8.13.1',
|
|
36
|
-
react: '^18.3.1',
|
|
37
|
-
'react-dom': '^18.3.1',
|
|
38
|
-
zod: '^4.2.1',
|
|
53
|
+
setup: 'pnpm install && pnpm exec husky install',
|
|
54
|
+
dev: 'pnpm -r dev --parallel --if-present',
|
|
55
|
+
build: 'pnpm -r build',
|
|
56
|
+
typecheck: 'pnpm -r typecheck',
|
|
57
|
+
lint: 'pnpm -r lint --if-present',
|
|
58
|
+
'lint:fix': 'pnpm -r lint:fix --if-present',
|
|
59
|
+
'lint-staged': 'lint-staged',
|
|
60
|
+
format: 'pnpm -r format --if-present',
|
|
61
|
+
'format:check': 'pnpm -r format:check --if-present',
|
|
62
|
+
test: 'pnpm -r test --if-present',
|
|
63
|
+
clean: 'pnpm -r clean --if-present && rimraf node_modules .turbo coverage',
|
|
64
|
+
prepare: 'husky install || true',
|
|
65
|
+
'docker:up': `pnpm -C ${dockerPackageDir} run docker:up`,
|
|
66
|
+
'docker:dev': `pnpm -C ${dockerPackageDir} run docker:dev`,
|
|
67
|
+
'docker:logs': `pnpm -C ${dockerPackageDir} run docker:logs`,
|
|
68
|
+
'docker:stop': `pnpm -C ${dockerPackageDir} run docker:stop`,
|
|
69
|
+
'docker:reset': `pnpm -C ${dockerPackageDir} run docker:reset`,
|
|
39
70
|
},
|
|
40
|
-
devDependencies: {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
'@types/react': '^18.3.27',
|
|
44
|
-
'@types/react-dom': '^18.3.7',
|
|
45
|
-
'@types/pg': '^8.11.10',
|
|
46
|
-
'@vitejs/plugin-react': '^4.3.4',
|
|
47
|
-
concurrently: '^8.2.0',
|
|
48
|
-
'docker-cli-js': '^3.0.9',
|
|
49
|
-
tsx: '^4.19.0',
|
|
50
|
-
typescript: '^5.9.3',
|
|
51
|
-
vite: '^6.4.1',
|
|
71
|
+
devDependencies: { ...BASE_LINT_DEV_DEPENDENCIES },
|
|
72
|
+
extraFields: {
|
|
73
|
+
workspaces: ['packages/*'],
|
|
52
74
|
},
|
|
53
|
-
}
|
|
75
|
+
});
|
|
54
76
|
}
|
|
55
|
-
function
|
|
56
|
-
return
|
|
57
|
-
import path from 'node:path'
|
|
58
|
-
import express from 'express'
|
|
59
|
-
import cors from 'cors'
|
|
60
|
-
import { Pool } from 'pg'
|
|
61
|
-
import { createRRRoute } from '@emeryld/rrroutes-server'
|
|
62
|
-
import { registry } from '../contract/index.js'
|
|
63
|
-
|
|
64
|
-
const app = express()
|
|
65
|
-
app.use(cors({ origin: '*' }))
|
|
66
|
-
app.use(express.json())
|
|
67
|
-
|
|
68
|
-
const pool = new Pool({
|
|
69
|
-
connectionString:
|
|
70
|
-
process.env.DATABASE_URL ??
|
|
71
|
-
'postgresql://postgres:postgres@localhost:5432/rrroutes',
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
const routes = createRRRoute(app, {
|
|
75
|
-
buildCtx: async () => ({
|
|
76
|
-
requestId: Math.random().toString(36).slice(2),
|
|
77
|
-
}),
|
|
78
|
-
debug:
|
|
79
|
-
process.env.NODE_ENV === 'development'
|
|
80
|
-
? { request: true, handler: true }
|
|
81
|
-
: undefined,
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
routes.registerControllers(registry, {
|
|
85
|
-
'GET /api/health': {
|
|
86
|
-
handler: async () => {
|
|
87
|
-
let db = 'skipped'
|
|
88
|
-
try {
|
|
89
|
-
await pool.query('select 1')
|
|
90
|
-
db = 'ok'
|
|
91
|
-
} catch (error) {
|
|
92
|
-
db = 'error: ' + String(error)
|
|
93
|
-
}
|
|
94
|
-
return {
|
|
95
|
-
out: {
|
|
96
|
-
status: 'ok',
|
|
97
|
-
at: new Date().toISOString(),
|
|
98
|
-
db,
|
|
99
|
-
},
|
|
100
|
-
}
|
|
101
|
-
},
|
|
102
|
-
},
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
const clientDir = path.resolve(__dirname, '../client')
|
|
106
|
-
if (existsSync(clientDir)) {
|
|
107
|
-
app.use(express.static(clientDir))
|
|
108
|
-
app.get('*', (_req, res) => {
|
|
109
|
-
res.sendFile(path.join(clientDir, 'index.html'))
|
|
110
|
-
})
|
|
111
|
-
} else {
|
|
112
|
-
console.warn('Client bundle missing; run "npm run build:client" to enable static assets.')
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const PORT = Number.parseInt(process.env.PORT ?? '8080', 10)
|
|
116
|
-
|
|
117
|
-
app.listen(PORT, () => {
|
|
118
|
-
console.log(\`Full stack service running on http://localhost:\${PORT}\`)
|
|
119
|
-
})
|
|
120
|
-
`;
|
|
121
|
-
}
|
|
122
|
-
const FULLSTACK_APP_TSX = `import React from 'react'
|
|
123
|
-
import { QueryClientProvider } from '@tanstack/react-query'
|
|
124
|
-
import { queryClient, healthGet } from './client/api'
|
|
125
|
-
|
|
126
|
-
function HealthCard() {
|
|
127
|
-
const health = healthGet.useEndpoint()
|
|
128
|
-
return (
|
|
129
|
-
<section
|
|
130
|
-
style={{
|
|
131
|
-
background: '#fff',
|
|
132
|
-
padding: 16,
|
|
133
|
-
borderRadius: 12,
|
|
134
|
-
boxShadow: '0 8px 24px rgba(12, 18, 32, 0.05)',
|
|
135
|
-
border: '1px solid #e5e7eb',
|
|
136
|
-
}}
|
|
137
|
-
>
|
|
138
|
-
<h2>Health</h2>
|
|
139
|
-
<button onClick={() => health.refetch()}>Ping API</button>
|
|
140
|
-
<pre style={{ whiteSpace: 'pre-wrap' }}>
|
|
141
|
-
{JSON.stringify(health.data ?? { note: 'Waiting for request...' }, null, 2)}
|
|
142
|
-
</pre>
|
|
143
|
-
</section>
|
|
144
|
-
)
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
export function App() {
|
|
148
|
-
return (
|
|
149
|
-
<QueryClientProvider client={queryClient}>
|
|
150
|
-
<main style={{ fontFamily: 'Inter, system-ui, sans-serif', padding: 24 }}>
|
|
151
|
-
<h1>Full stack service</h1>
|
|
152
|
-
<p>
|
|
153
|
-
Contract-driven API + Vite client. Health endpoint uses RRRoutes and checks the
|
|
154
|
-
Postgres connection.
|
|
155
|
-
</p>
|
|
156
|
-
<p>Edit <code>src/contract/index.ts</code> or <code>src/server/index.ts</code> to extend the API.</p>
|
|
157
|
-
<HealthCard />
|
|
158
|
-
</main>
|
|
159
|
-
</QueryClientProvider>
|
|
160
|
-
)
|
|
161
|
-
}
|
|
162
|
-
`;
|
|
163
|
-
const FULLSTACK_MAIN_TSX = `import React from 'react'
|
|
164
|
-
import ReactDOM from 'react-dom/client'
|
|
165
|
-
import { App } from './App'
|
|
166
|
-
import './styles.css'
|
|
167
|
-
|
|
168
|
-
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
169
|
-
<React.StrictMode>
|
|
170
|
-
<App />
|
|
171
|
-
</React.StrictMode>,
|
|
172
|
-
)
|
|
173
|
-
`;
|
|
174
|
-
const FULLSTACK_STYLES = `:root {
|
|
175
|
-
background: radial-gradient(circle at 20% 20%, #eef2ff, #f7f8fb 45%);
|
|
176
|
-
color: #0b1021;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
body {
|
|
180
|
-
margin: 0;
|
|
181
|
-
}
|
|
182
|
-
`;
|
|
183
|
-
const FULLSTACK_INDEX_HTML = `<!doctype html>
|
|
184
|
-
<html lang="en">
|
|
185
|
-
<head>
|
|
186
|
-
<meta charset="UTF-8" />
|
|
187
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
188
|
-
<title>Full stack service</title>
|
|
189
|
-
</head>
|
|
190
|
-
<body>
|
|
191
|
-
<div id="root"></div>
|
|
192
|
-
<script type="module" src="/src/client/main.tsx"></script>
|
|
193
|
-
</body>
|
|
194
|
-
</html>
|
|
195
|
-
`;
|
|
196
|
-
function fullstackViteConfig() {
|
|
197
|
-
return `import { defineConfig } from 'vite'
|
|
198
|
-
import react from '@vitejs/plugin-react'
|
|
199
|
-
|
|
200
|
-
export default defineConfig({
|
|
201
|
-
plugins: [react()],
|
|
202
|
-
build: {
|
|
203
|
-
outDir: 'dist/client',
|
|
204
|
-
},
|
|
205
|
-
})
|
|
206
|
-
`;
|
|
77
|
+
function rootPnpmWorkspace() {
|
|
78
|
+
return "packages:\n - 'packages/*'\n";
|
|
207
79
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
.git
|
|
211
|
-
.env
|
|
212
|
-
npm-debug.log*
|
|
213
|
-
pnpm-lock.yaml
|
|
214
|
-
yarn.lock
|
|
215
|
-
`;
|
|
216
|
-
const FULLSTACK_DOCKER_COMPOSE = `services:
|
|
80
|
+
function stackComposeYaml() {
|
|
81
|
+
return `services:
|
|
217
82
|
db:
|
|
218
83
|
image: postgres:15
|
|
219
84
|
restart: unless-stopped
|
|
@@ -229,211 +94,70 @@ const FULLSTACK_DOCKER_COMPOSE = `services:
|
|
|
229
94
|
volumes:
|
|
230
95
|
db-data:
|
|
231
96
|
`;
|
|
232
|
-
function fullstackDockerfile() {
|
|
233
|
-
return `FROM node:20-slim AS builder
|
|
234
|
-
WORKDIR /app
|
|
235
|
-
|
|
236
|
-
COPY package*.json ./
|
|
237
|
-
COPY pnpm-lock.yaml* ./
|
|
238
|
-
RUN npm install
|
|
239
|
-
|
|
240
|
-
COPY . .
|
|
241
|
-
RUN npm run build
|
|
242
|
-
|
|
243
|
-
FROM node:20-slim AS runner
|
|
244
|
-
WORKDIR /app
|
|
245
|
-
ENV NODE_ENV=production
|
|
246
|
-
|
|
247
|
-
COPY --from=builder /app/package*.json ./
|
|
248
|
-
COPY --from=builder /app/node_modules ./node_modules
|
|
249
|
-
COPY --from=builder /app/dist ./dist
|
|
250
|
-
RUN npm prune --omit=dev || true
|
|
251
|
-
|
|
252
|
-
EXPOSE 8080
|
|
253
|
-
CMD ["node", "dist/server/index.js"]
|
|
254
|
-
`;
|
|
255
97
|
}
|
|
256
|
-
function
|
|
257
|
-
|
|
258
|
-
'package.json':
|
|
98
|
+
async function scaffoldRootFiles(baseDir, baseName) {
|
|
99
|
+
const files = {
|
|
100
|
+
'package.json': rootPackageJson(baseName),
|
|
101
|
+
'pnpm-workspace.yaml': rootPnpmWorkspace(),
|
|
102
|
+
'docker-compose.yml': stackComposeYaml(),
|
|
259
103
|
'tsconfig.json': baseTsConfig({
|
|
260
|
-
|
|
261
|
-
types: ['node'],
|
|
262
|
-
jsx: 'react-jsx',
|
|
263
|
-
include: ['src/**/*', 'vite.config.ts'],
|
|
264
|
-
}),
|
|
265
|
-
'tsconfig.server.json': baseTsConfig({
|
|
266
|
-
types: ['node'],
|
|
267
|
-
rootDir: 'src',
|
|
104
|
+
rootDir: '.',
|
|
268
105
|
outDir: 'dist',
|
|
269
|
-
include: ['
|
|
106
|
+
include: ['packages/**/*', 'scripts/**/*', 'src/**/*'],
|
|
270
107
|
}),
|
|
271
|
-
|
|
272
|
-
'src/contract/index.ts': fullstackContractTs(),
|
|
273
|
-
'src/server/index.ts': fullstackServerIndexTs(),
|
|
274
|
-
'src/server/env.d.ts': "declare namespace NodeJS { interface ProcessEnv { DATABASE_URL?: string; PORT?: string } }\n",
|
|
275
|
-
'src/client/api.ts': fullstackClientApi(),
|
|
276
|
-
'src/client/App.tsx': FULLSTACK_APP_TSX,
|
|
277
|
-
'src/client/main.tsx': FULLSTACK_MAIN_TSX,
|
|
278
|
-
'src/client/styles.css': FULLSTACK_STYLES,
|
|
279
|
-
'index.html': FULLSTACK_INDEX_HTML,
|
|
280
|
-
'scripts/docker.ts': fullstackDockerCliScript(pkgName),
|
|
281
|
-
'.dockerignore': FULLSTACK_DOCKERIGNORE,
|
|
282
|
-
'docker-compose.yml': FULLSTACK_DOCKER_COMPOSE,
|
|
283
|
-
Dockerfile: fullstackDockerfile(),
|
|
284
|
-
'.env.example': 'PORT=8080\nDATABASE_URL=postgresql://postgres:postgres@localhost:5432/rrroutes\nVITE_API_URL=http://localhost:8080\n',
|
|
285
|
-
'README.md': `# ${pkgName}
|
|
286
|
-
|
|
287
|
-
Full stack (API + Vite web) scaffolded by manager-cli.
|
|
288
|
-
- dev: \`npm run dev\` (runs API + Vite client)
|
|
289
|
-
- build: \`npm run build\` (server to dist/server, client to dist/client)
|
|
290
|
-
- start: \`npm start\` (serves API and static client from dist)
|
|
291
|
-
- docker helper: \`npm run docker:up\` (build + run), \`npm run docker:logs\`, \`npm run docker:stop\`
|
|
292
|
-
- db helper: \`npm run db:up\` to start Postgres via docker-compose (uses DATABASE_URL)
|
|
293
|
-
`,
|
|
108
|
+
...basePackageFiles(),
|
|
294
109
|
};
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
import { z } from 'zod'
|
|
299
|
-
|
|
300
|
-
const routes = resource('/api')
|
|
301
|
-
.sub(
|
|
302
|
-
resource('health')
|
|
303
|
-
.get({
|
|
304
|
-
outputSchema: z.object({
|
|
305
|
-
status: z.literal('ok'),
|
|
306
|
-
at: z.string(),
|
|
307
|
-
db: z.string(),
|
|
308
|
-
}),
|
|
309
|
-
description: 'Health check with DB status.',
|
|
310
|
-
})
|
|
311
|
-
.done(),
|
|
312
|
-
)
|
|
313
|
-
.done()
|
|
314
|
-
|
|
315
|
-
export const registry = finalize(routes)
|
|
316
|
-
|
|
317
|
-
const sockets = defineSocketEvents(
|
|
318
|
-
{
|
|
319
|
-
pingPayload: z.object({
|
|
320
|
-
note: z.string().default('ping'),
|
|
321
|
-
sentAt: z.string(),
|
|
322
|
-
}),
|
|
323
|
-
pongPayload: z.object({
|
|
324
|
-
ok: z.boolean(),
|
|
325
|
-
receivedAt: z.string(),
|
|
326
|
-
echo: z.string().optional(),
|
|
327
|
-
}),
|
|
328
|
-
},
|
|
329
|
-
{
|
|
330
|
-
'health:ping': {
|
|
331
|
-
message: z.object({
|
|
332
|
-
note: z.string().default('ping'),
|
|
333
|
-
}),
|
|
334
|
-
},
|
|
335
|
-
'health:pong': {
|
|
336
|
-
message: z.object({
|
|
337
|
-
ok: z.literal(true),
|
|
338
|
-
at: z.string(),
|
|
339
|
-
echo: z.string().optional(),
|
|
340
|
-
}),
|
|
341
|
-
},
|
|
342
|
-
},
|
|
343
|
-
)
|
|
344
|
-
|
|
345
|
-
export const socketConfig = sockets.config
|
|
346
|
-
export const socketEvents = sockets.events
|
|
347
|
-
export type AppRegistry = typeof registry
|
|
348
|
-
`;
|
|
349
|
-
}
|
|
350
|
-
function fullstackClientApi() {
|
|
351
|
-
return `import { QueryClient } from '@tanstack/react-query'
|
|
352
|
-
import { createRouteClient } from '@emeryld/rrroutes-client'
|
|
353
|
-
import { registry } from '../contract'
|
|
354
|
-
|
|
355
|
-
const baseUrl = import.meta.env.VITE_API_URL ?? 'http://localhost:8080'
|
|
356
|
-
export const queryClient = new QueryClient()
|
|
357
|
-
|
|
358
|
-
export const routeClient = createRouteClient({
|
|
359
|
-
baseUrl,
|
|
360
|
-
queryClient,
|
|
361
|
-
environment: import.meta.env.MODE === 'production' ? 'production' : 'development',
|
|
362
|
-
})
|
|
363
|
-
|
|
364
|
-
export const healthGet = routeClient.build(registry.byKey['GET /api/health'])
|
|
365
|
-
`;
|
|
366
|
-
}
|
|
367
|
-
function fullstackDockerCliScript(pkgName) {
|
|
368
|
-
return `#!/usr/bin/env tsx
|
|
369
|
-
import { readFile } from 'node:fs/promises'
|
|
370
|
-
import path from 'node:path'
|
|
371
|
-
import { fileURLToPath } from 'node:url'
|
|
372
|
-
import { Docker } from 'docker-cli-js'
|
|
373
|
-
|
|
374
|
-
const __filename = fileURLToPath(import.meta.url)
|
|
375
|
-
const __dirname = path.dirname(__filename)
|
|
376
|
-
const pkgRaw = await readFile(path.join(__dirname, '..', 'package.json'), 'utf8')
|
|
377
|
-
const pkg = JSON.parse(pkgRaw) as { name?: string }
|
|
378
|
-
const image = \`\${pkg.name ?? '${pkgName}'}:latest\`
|
|
379
|
-
const container =
|
|
380
|
-
(pkg.name ?? '${pkgName}').replace(/[^a-z0-9]/gi, '-').replace(/^-+|-+$/g, '') || 'rrr-fullstack'
|
|
381
|
-
const port = process.env.PORT ?? '8080'
|
|
382
|
-
const docker = new Docker({ spawnOptions: { stdio: 'inherit' } })
|
|
383
|
-
|
|
384
|
-
async function main() {
|
|
385
|
-
const [command = 'help'] = process.argv.slice(2)
|
|
386
|
-
if (command === 'help') return printHelp()
|
|
387
|
-
if (command === 'build') return docker.command(\`build -t \${image} .\`)
|
|
388
|
-
if (command === 'up') {
|
|
389
|
-
await docker.command(\`build -t \${image} .\`)
|
|
390
|
-
return docker.command(\`run -d --rm -p \${port}:\${port} --name \${container} \${image}\`)
|
|
391
|
-
}
|
|
392
|
-
if (command === 'run') {
|
|
393
|
-
return docker.command(\`run -d --rm -p \${port}:\${port} --name \${container} \${image}\`)
|
|
394
|
-
}
|
|
395
|
-
if (command === 'logs') return docker.command(\`logs -f \${container}\`)
|
|
396
|
-
if (command === 'stop') return docker.command(\`stop \${container}\`)
|
|
397
|
-
if (command === 'clean') {
|
|
398
|
-
try {
|
|
399
|
-
await docker.command(\`rm -f \${container}\`)
|
|
400
|
-
} catch (error) {
|
|
401
|
-
console.warn(String(error))
|
|
110
|
+
for (const [relative, contents] of Object.entries(files)) {
|
|
111
|
+
// eslint-disable-next-line no-await-in-loop
|
|
112
|
+
await writeFileIfMissing(baseDir, relative, contents);
|
|
402
113
|
}
|
|
403
|
-
return
|
|
404
|
-
}
|
|
405
|
-
return printHelp()
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
function printHelp() {
|
|
409
|
-
console.log(
|
|
410
|
-
[
|
|
411
|
-
'Docker helper commands:',
|
|
412
|
-
' build -> docker build -t ${pkgName}:latest .',
|
|
413
|
-
' up -> build then run in detached mode',
|
|
414
|
-
' run -> run existing image detached',
|
|
415
|
-
' logs -> docker logs -f <container>',
|
|
416
|
-
' stop -> docker stop <container>',
|
|
417
|
-
' clean -> docker rm -f <container>',
|
|
418
|
-
].join('\\n'),
|
|
419
|
-
)
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
main().catch((err) => {
|
|
423
|
-
console.error(err)
|
|
424
|
-
process.exit(1)
|
|
425
|
-
})
|
|
426
|
-
`;
|
|
427
114
|
}
|
|
428
115
|
export const fullstackVariant = {
|
|
429
116
|
id: 'rrr-fullstack',
|
|
430
|
-
label: '
|
|
431
|
-
defaultDir: '
|
|
117
|
+
label: 'rrr fullstack (contract + server + client + docker)',
|
|
118
|
+
defaultDir: 'rrrfull-stack',
|
|
119
|
+
summary: 'End-to-end RRRoutes stack: pnpm workspace with contract, server, client, and docker helper packages.',
|
|
120
|
+
keyFiles: [
|
|
121
|
+
'package.json',
|
|
122
|
+
'pnpm-workspace.yaml',
|
|
123
|
+
'docker-compose.yml',
|
|
124
|
+
'packages/<name>-contract',
|
|
125
|
+
'packages/<name>-server',
|
|
126
|
+
'packages/<name>-client',
|
|
127
|
+
'packages/<name>-docker',
|
|
128
|
+
],
|
|
129
|
+
scripts: FULLSTACK_SCRIPTS,
|
|
130
|
+
notes: [
|
|
131
|
+
'Generates four packages and a workspace root; great starting point when you need the whole stack.',
|
|
132
|
+
'Use --name to control the workspace prefix (default is the target folder name).',
|
|
133
|
+
],
|
|
432
134
|
async scaffold(ctx) {
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
135
|
+
const baseName = ctx.pkgName;
|
|
136
|
+
const names = deriveNames(baseName);
|
|
137
|
+
const dirs = deriveDirs(ctx.targetDir, baseName);
|
|
138
|
+
// Root workspace files
|
|
139
|
+
await scaffoldRootFiles(dirs.root, baseName);
|
|
140
|
+
// Contract package (reuse contract variant)
|
|
141
|
+
await contractVariant.scaffold({
|
|
142
|
+
targetDir: dirs.contract,
|
|
143
|
+
pkgName: names.contract,
|
|
144
|
+
});
|
|
145
|
+
// Server package
|
|
146
|
+
await serverVariant.scaffold({
|
|
147
|
+
targetDir: dirs.server,
|
|
148
|
+
pkgName: names.server,
|
|
149
|
+
contractName: names.contract,
|
|
150
|
+
});
|
|
151
|
+
// Client package
|
|
152
|
+
await clientVariant.scaffold({
|
|
153
|
+
targetDir: dirs.client,
|
|
154
|
+
pkgName: names.client,
|
|
155
|
+
contractName: names.contract,
|
|
156
|
+
});
|
|
157
|
+
// Docker helper package (reuse docker variant)
|
|
158
|
+
await dockerVariant.scaffold({
|
|
159
|
+
targetDir: dirs.docker,
|
|
160
|
+
pkgName: names.docker,
|
|
161
|
+
});
|
|
438
162
|
},
|
|
439
163
|
};
|
|
@@ -1,6 +1,18 @@
|
|
|
1
|
-
import { baseTsConfig, writeFileIfMissing, } from '../shared.js';
|
|
1
|
+
import { BASE_LINT_DEV_DEPENDENCIES, basePackageFiles, basePackageJson, buildReadme, baseScripts, baseTsConfig, writeFileIfMissing, } from '../shared.js';
|
|
2
|
+
const SERVER_SCRIPTS = [
|
|
3
|
+
'dev',
|
|
4
|
+
'build',
|
|
5
|
+
'typecheck',
|
|
6
|
+
'lint',
|
|
7
|
+
'lint:fix',
|
|
8
|
+
'format',
|
|
9
|
+
'format:check',
|
|
10
|
+
'clean',
|
|
11
|
+
'test',
|
|
12
|
+
'start',
|
|
13
|
+
];
|
|
2
14
|
const CONTRACT_IMPORT_PLACEHOLDER = '@your-scope/contract';
|
|
3
|
-
function serverIndexTs(contractImport) {
|
|
15
|
+
export function serverIndexTs(contractImport) {
|
|
4
16
|
return `import 'dotenv/config'
|
|
5
17
|
import http from 'node:http'
|
|
6
18
|
import express from 'express'
|
|
@@ -46,23 +58,15 @@ server.listen(PORT, () => {
|
|
|
46
58
|
})
|
|
47
59
|
`;
|
|
48
60
|
}
|
|
49
|
-
function serverPackageJson(name) {
|
|
50
|
-
return
|
|
61
|
+
export function serverPackageJson(name, contractName = CONTRACT_IMPORT_PLACEHOLDER) {
|
|
62
|
+
return basePackageJson({
|
|
51
63
|
name,
|
|
52
|
-
version: '0.1.0',
|
|
53
64
|
private: false,
|
|
54
|
-
|
|
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',
|
|
65
|
+
scripts: baseScripts('tsx watch --env-file .env src/index.ts', {
|
|
62
66
|
start: 'node dist/index.js',
|
|
63
|
-
},
|
|
67
|
+
}),
|
|
64
68
|
dependencies: {
|
|
65
|
-
|
|
69
|
+
[contractName]: 'workspace:*',
|
|
66
70
|
'@emeryld/rrroutes-server': '^2.4.1',
|
|
67
71
|
cors: '^2.8.5',
|
|
68
72
|
dotenv: '^16.4.5',
|
|
@@ -70,34 +74,63 @@ function serverPackageJson(name) {
|
|
|
70
74
|
zod: '^4.2.1',
|
|
71
75
|
},
|
|
72
76
|
devDependencies: {
|
|
77
|
+
...BASE_LINT_DEV_DEPENDENCIES,
|
|
73
78
|
'@types/cors': '^2.8.5',
|
|
74
79
|
'@types/express': '^5.0.6',
|
|
75
80
|
'@types/node': '^24.10.2',
|
|
76
|
-
'ts-node': '^10.9.2',
|
|
77
|
-
typescript: '^5.9.3',
|
|
78
81
|
},
|
|
79
|
-
}
|
|
82
|
+
});
|
|
80
83
|
}
|
|
81
84
|
function serverFiles(pkgName, contractImport) {
|
|
82
85
|
return {
|
|
83
|
-
'package.json': serverPackageJson(pkgName),
|
|
86
|
+
'package.json': serverPackageJson(pkgName, contractImport),
|
|
84
87
|
'tsconfig.json': baseTsConfig({ types: ['node'] }),
|
|
88
|
+
...basePackageFiles(),
|
|
85
89
|
'src/index.ts': serverIndexTs(contractImport),
|
|
86
90
|
'.env.example': 'PORT=4000\n',
|
|
87
|
-
'README.md':
|
|
88
|
-
|
|
89
|
-
Starter RRRoutes server scaffold.
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
91
|
+
'README.md': buildReadme({
|
|
92
|
+
name: pkgName,
|
|
93
|
+
description: 'Starter RRRoutes server scaffold.',
|
|
94
|
+
scripts: SERVER_SCRIPTS,
|
|
95
|
+
sections: [
|
|
96
|
+
{
|
|
97
|
+
title: 'Getting Started',
|
|
98
|
+
lines: [
|
|
99
|
+
'- Install deps: `npm install` (or `pnpm install`)',
|
|
100
|
+
'- Copy `.env.example` to `.env` as needed.',
|
|
101
|
+
'- Start dev API: `npm run dev`',
|
|
102
|
+
'- Build output: `npm run build`',
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
title: 'Usage',
|
|
107
|
+
lines: [
|
|
108
|
+
`- Update the contract import in \`src/index.ts\` if needed (${contractImport}).`,
|
|
109
|
+
'- Start compiled server: `npm run start` after building.',
|
|
110
|
+
],
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
title: 'Environment',
|
|
114
|
+
lines: ['- `PORT` sets the HTTP port (default 4000).'],
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
}),
|
|
93
118
|
};
|
|
94
119
|
}
|
|
95
120
|
export const serverVariant = {
|
|
96
121
|
id: 'rrr-server',
|
|
97
122
|
label: 'rrr server',
|
|
98
123
|
defaultDir: 'packages/rrr-server',
|
|
124
|
+
summary: 'Express + RRRoutes server wired to a contract import; ships a health route.',
|
|
125
|
+
keyFiles: ['src/index.ts', '.env.example', 'README.md'],
|
|
126
|
+
scripts: SERVER_SCRIPTS,
|
|
127
|
+
notes: [
|
|
128
|
+
'Set the contract import via --contract or by editing src/index.ts.',
|
|
129
|
+
'Includes start script for compiled output and dotenv-ready dev script.',
|
|
130
|
+
],
|
|
99
131
|
async scaffold(ctx) {
|
|
100
|
-
const
|
|
132
|
+
const contractImport = ctx.contractName ?? CONTRACT_IMPORT_PLACEHOLDER;
|
|
133
|
+
const files = serverFiles(ctx.pkgName, contractImport);
|
|
101
134
|
for (const [relative, contents] of Object.entries(files)) {
|
|
102
135
|
// eslint-disable-next-line no-await-in-loop
|
|
103
136
|
await writeFileIfMissing(ctx.targetDir, relative, contents);
|