@nage-api/cli 1.0.0-beta.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/LICENSE +202 -0
- package/README.md +196 -0
- package/dist/cli.d.ts +25 -0
- package/dist/cli.js +276 -0
- package/dist/commands/create.d.ts +56 -0
- package/dist/commands/create.js +219 -0
- package/dist/commands/doctor.d.ts +47 -0
- package/dist/commands/doctor.js +208 -0
- package/dist/commands/features.d.ts +56 -0
- package/dist/commands/features.js +229 -0
- package/dist/commands/generate.d.ts +37 -0
- package/dist/commands/generate.js +151 -0
- package/dist/fs/file-tree.d.ts +57 -0
- package/dist/fs/file-tree.js +136 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +80 -0
- package/dist/main.d.ts +11 -0
- package/dist/main.js +43 -0
- package/dist/naming.d.ts +36 -0
- package/dist/naming.js +72 -0
- package/dist/templates/app.template.d.ts +19 -0
- package/dist/templates/app.template.js +601 -0
- package/dist/templates/resource.template.d.ts +39 -0
- package/dist/templates/resource.template.js +600 -0
- package/dist/templates/workspace.template.d.ts +22 -0
- package/dist/templates/workspace.template.js +457 -0
- package/dist/workspace/manifest.d.ts +70 -0
- package/dist/workspace/manifest.js +162 -0
- package/dist/workspace/wiring.d.ts +33 -0
- package/dist/workspace/wiring.js +112 -0
- package/package.json +51 -0
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Workspace-level files emitted by `nage create` (PLAN.md §9.1).
|
|
4
|
+
*
|
|
5
|
+
* The generated workspace is monorepo-first: one lockfile, one CI pipeline, one
|
|
6
|
+
* set of tooling, seeded with a single app so the simple case stays simple.
|
|
7
|
+
* Everything here is production-shaped from the first commit — strict TypeScript,
|
|
8
|
+
* a CI workflow, Docker, and a `.env.example` documenting every variable — because
|
|
9
|
+
* retrofitting those onto a project that started loose rarely happens.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.driverPackageFor = driverPackageFor;
|
|
13
|
+
exports.workspaceFiles = workspaceFiles;
|
|
14
|
+
exports.json = json;
|
|
15
|
+
const manifest_js_1 = require("../workspace/manifest.js");
|
|
16
|
+
/** The driver package that matches the workspace engine (§14.1: one, never both). */
|
|
17
|
+
function driverPackageFor(engine) {
|
|
18
|
+
return engine === 'mongodb' ? '@nage-api/data-mongo' : '@nage-api/data-sql';
|
|
19
|
+
}
|
|
20
|
+
function workspaceFiles(input) {
|
|
21
|
+
const { manifest } = input;
|
|
22
|
+
const version = manifest.frameworkVersion;
|
|
23
|
+
return [
|
|
24
|
+
{ path: 'nage.workspace.json', contents: (0, manifest_js_1.serialiseManifest)(manifest) },
|
|
25
|
+
{
|
|
26
|
+
path: 'package.json',
|
|
27
|
+
contents: json({
|
|
28
|
+
name: manifest.name,
|
|
29
|
+
version: '0.0.0',
|
|
30
|
+
private: true,
|
|
31
|
+
packageManager: 'pnpm@10.33.0',
|
|
32
|
+
engines: { node: '>=22.0.0' },
|
|
33
|
+
scripts: {
|
|
34
|
+
build: 'turbo run build',
|
|
35
|
+
dev: 'turbo run dev',
|
|
36
|
+
lint: 'turbo run build && eslint .',
|
|
37
|
+
typecheck: 'turbo run typecheck',
|
|
38
|
+
test: 'turbo run test',
|
|
39
|
+
'db:migrate': 'nage db migrate',
|
|
40
|
+
'db:seed': 'nage db seed',
|
|
41
|
+
doctor: 'nage doctor',
|
|
42
|
+
},
|
|
43
|
+
devDependencies: {
|
|
44
|
+
'@nage-api/cli': version,
|
|
45
|
+
// `eslint.config.mjs` imports this; without it `pnpm lint` fails to
|
|
46
|
+
// resolve the config before linting a single file.
|
|
47
|
+
'@nage-api/eslint-config': version,
|
|
48
|
+
'@types/node': '22.20.1',
|
|
49
|
+
eslint: '10.8.1',
|
|
50
|
+
prettier: '3.9.6',
|
|
51
|
+
// Generated migrations live at the workspace root and import the
|
|
52
|
+
// driver's own types, so the root — not just the app — has to declare
|
|
53
|
+
// them. `sequelize` is a peer dependency of `@nage-api/data-sql`, which
|
|
54
|
+
// means the consumer provides it.
|
|
55
|
+
...(manifest.engine === 'mongodb'
|
|
56
|
+
? {}
|
|
57
|
+
: { [driverPackageFor(manifest.engine)]: version, sequelize: '^6.37.0' }),
|
|
58
|
+
turbo: '2.10.9',
|
|
59
|
+
typescript: '5.9.3',
|
|
60
|
+
vitest: '4.1.10',
|
|
61
|
+
},
|
|
62
|
+
}),
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
path: 'pnpm-workspace.yaml',
|
|
66
|
+
contents: [
|
|
67
|
+
'packages:',
|
|
68
|
+
" - 'apps/*'",
|
|
69
|
+
" - 'packages/*'",
|
|
70
|
+
'',
|
|
71
|
+
'# Local packages are copied into the consumers that depend on them rather',
|
|
72
|
+
"# than symlinked, so `pnpm deploy` — which is how an app's Dockerfile builds",
|
|
73
|
+
'# its production node_modules — can produce a tree that stands on its own.',
|
|
74
|
+
'# Left symlinked, a deployed `@app/*` dependency is a link to a path that',
|
|
75
|
+
"# does not exist in the runtime stage, and the image dies on Node's",
|
|
76
|
+
'# `Cannot find module`. pnpm 10 also refuses to deploy at all without this.',
|
|
77
|
+
'injectWorkspacePackages: true',
|
|
78
|
+
'',
|
|
79
|
+
'# What that costs: a consumer holds a copy, so an edit to a local package is',
|
|
80
|
+
'# invisible to it until the copy is refreshed. Refreshing after `build` is',
|
|
81
|
+
'# enough, because `turbo run build` builds a dependency before its consumer.',
|
|
82
|
+
'syncInjectedDepsAfterScripts:',
|
|
83
|
+
' - build',
|
|
84
|
+
'',
|
|
85
|
+
].join('\n'),
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
path: 'turbo.json',
|
|
89
|
+
contents: json({
|
|
90
|
+
$schema: 'https://turbo.build/schema.json',
|
|
91
|
+
tasks: {
|
|
92
|
+
build: { dependsOn: ['^build'], outputs: ['dist/**'] },
|
|
93
|
+
dev: { cache: false, persistent: true },
|
|
94
|
+
typecheck: { dependsOn: ['^build'], outputs: [] },
|
|
95
|
+
test: { dependsOn: ['^build'], outputs: [] },
|
|
96
|
+
},
|
|
97
|
+
}),
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
path: 'tsconfig.base.json',
|
|
101
|
+
contents: json({
|
|
102
|
+
compilerOptions: {
|
|
103
|
+
target: 'ES2023',
|
|
104
|
+
lib: ['ES2023'],
|
|
105
|
+
module: 'NodeNext',
|
|
106
|
+
moduleResolution: 'NodeNext',
|
|
107
|
+
// The same strictness the framework holds itself to (§13): a project
|
|
108
|
+
// that starts strict stays strict, and one that does not, never does.
|
|
109
|
+
strict: true,
|
|
110
|
+
noUncheckedIndexedAccess: true,
|
|
111
|
+
exactOptionalPropertyTypes: true,
|
|
112
|
+
noImplicitOverride: true,
|
|
113
|
+
useUnknownInCatchVariables: true,
|
|
114
|
+
experimentalDecorators: true,
|
|
115
|
+
emitDecoratorMetadata: true,
|
|
116
|
+
declaration: true,
|
|
117
|
+
sourceMap: true,
|
|
118
|
+
skipLibCheck: true,
|
|
119
|
+
paths: {},
|
|
120
|
+
},
|
|
121
|
+
exclude: ['**/node_modules', '**/dist'],
|
|
122
|
+
}),
|
|
123
|
+
},
|
|
124
|
+
{ path: 'tsconfig.json', contents: json({ files: [], references: [] }) },
|
|
125
|
+
{
|
|
126
|
+
// Migrations and seeds are TypeScript, and under type-aware ESLint a file
|
|
127
|
+
// belonging to no project is a hard parsing error rather than a skip — so
|
|
128
|
+
// `nage generate resource` used to make `pnpm lint` fail on the migration it
|
|
129
|
+
// had just written.
|
|
130
|
+
//
|
|
131
|
+
// Deliberately *not* referenced from the solution `tsconfig.json` above: a
|
|
132
|
+
// fresh workspace has no migrations, and an empty `include` makes `tsc -b`
|
|
133
|
+
// fail with "No inputs were found". ESLint's project service finds this
|
|
134
|
+
// config by walking up from the file, which needs no reference.
|
|
135
|
+
path: 'database/tsconfig.json',
|
|
136
|
+
contents: json({
|
|
137
|
+
extends: '../tsconfig.base.json',
|
|
138
|
+
compilerOptions: { noEmit: true, types: ['node'] },
|
|
139
|
+
include: ['migrations/**/*.ts', 'seeds/**/*.ts'],
|
|
140
|
+
}),
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
path: '.gitignore',
|
|
144
|
+
contents: [
|
|
145
|
+
'node_modules/',
|
|
146
|
+
'dist/',
|
|
147
|
+
'coverage/',
|
|
148
|
+
'.turbo/',
|
|
149
|
+
'.env',
|
|
150
|
+
// A committed `tsbuildinfo` makes `tsc -b` believe an absent `dist` is
|
|
151
|
+
// already up to date, so a fresh checkout — or a Docker build — emits
|
|
152
|
+
// nothing and the image ships without its entry point.
|
|
153
|
+
'*.tsbuildinfo',
|
|
154
|
+
'*.log',
|
|
155
|
+
'',
|
|
156
|
+
].join('\n'),
|
|
157
|
+
},
|
|
158
|
+
{ path: '.dockerignore', contents: dockerIgnore() },
|
|
159
|
+
{ path: '.nvmrc', contents: '22\n' },
|
|
160
|
+
{
|
|
161
|
+
path: '.env.example',
|
|
162
|
+
contents: envExample(manifest),
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
path: 'eslint.config.mjs',
|
|
166
|
+
contents: [
|
|
167
|
+
"import nage from '@nage-api/eslint-config';",
|
|
168
|
+
'',
|
|
169
|
+
'export default nage({ tsconfigRootDir: import.meta.dirname });',
|
|
170
|
+
'',
|
|
171
|
+
].join('\n'),
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
path: '.prettierrc.json',
|
|
175
|
+
contents: json({ singleQuote: true, semi: true, trailingComma: 'all', printWidth: 100 }),
|
|
176
|
+
},
|
|
177
|
+
// SQLite is a file, not a server: a compose file with a database service in
|
|
178
|
+
// it would be a lie, and one with no services is not a valid compose file.
|
|
179
|
+
...(manifest.engine === 'sqlite'
|
|
180
|
+
? []
|
|
181
|
+
: [{ path: 'docker-compose.yml', contents: dockerCompose(manifest.engine) }]),
|
|
182
|
+
{
|
|
183
|
+
path: '.github/workflows/ci.yml',
|
|
184
|
+
contents: workspaceCi(),
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
path: 'database/migrations/.gitkeep',
|
|
188
|
+
contents: '',
|
|
189
|
+
},
|
|
190
|
+
{ path: 'database/seeds/.gitkeep', contents: '' },
|
|
191
|
+
{ path: 'README.md', contents: readme(manifest) },
|
|
192
|
+
];
|
|
193
|
+
}
|
|
194
|
+
function envExample(manifest) {
|
|
195
|
+
const lines = [
|
|
196
|
+
'# Every variable the workspace reads. `nage doctor` checks this file is current.',
|
|
197
|
+
'NODE_ENV=development',
|
|
198
|
+
'',
|
|
199
|
+
'# Comma-separated allow-list. Omit to disable CORS entirely (the safe default).',
|
|
200
|
+
'CORS_ORIGINS=http://localhost:3000',
|
|
201
|
+
'',
|
|
202
|
+
];
|
|
203
|
+
if (manifest.engine === 'mongodb') {
|
|
204
|
+
lines.push('DATABASE_URL=mongodb://localhost:27017/app', '');
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
lines.push(`DATABASE_URL=${manifest.engine}://app:app@localhost:5432/app`, '');
|
|
208
|
+
}
|
|
209
|
+
for (const app of manifest.apps) {
|
|
210
|
+
lines.push(`# ${app.name}`, `${app.name.toUpperCase()}_PORT=${String(app.port)}`, '');
|
|
211
|
+
}
|
|
212
|
+
return lines.join('\n');
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* What must never reach a build layer.
|
|
216
|
+
*
|
|
217
|
+
* Read from the context root — the workspace root, which is where a build has to
|
|
218
|
+
* start (`docker build -f apps/<app>/Dockerfile .`). A file named
|
|
219
|
+
* `apps/<app>/.dockerignore` is read by no builder: the classic one looks only at
|
|
220
|
+
* the context root, and BuildKit additionally accepts
|
|
221
|
+
* `apps/<app>/Dockerfile.dockerignore` but not that. That is how the first
|
|
222
|
+
* version of this template shipped a build stage with the developer's `.env`
|
|
223
|
+
* inside it — verified by reading the file back out of the build layer.
|
|
224
|
+
*/
|
|
225
|
+
function dockerIgnore() {
|
|
226
|
+
return [
|
|
227
|
+
'# Read from the build context root, which is the only place Docker looks.',
|
|
228
|
+
'#',
|
|
229
|
+
'# Every pattern is `**/`-prefixed because these are not recursive otherwise:',
|
|
230
|
+
"# a bare `node_modules` excludes the workspace root's and leaves",
|
|
231
|
+
'# `apps/*/node_modules` — and `apps/*/.env` — in the context. That is measured,',
|
|
232
|
+
'# not assumed: without the prefix a probe of the build context still finds',
|
|
233
|
+
'# `apps/<app>/dist`, `apps/<app>/node_modules` and the stale tsbuildinfo.',
|
|
234
|
+
'#',
|
|
235
|
+
'# `.env` first, because it is the file here that must never reach a layer:',
|
|
236
|
+
'# `.gitignore` keeps it out of the repository but not out of the build',
|
|
237
|
+
'# context, and `COPY . .` in the build stage would bake live credentials into',
|
|
238
|
+
'# an image whose layers and build cache outlive the container.',
|
|
239
|
+
'**/.env',
|
|
240
|
+
'**/.env.*',
|
|
241
|
+
'**/*.pem',
|
|
242
|
+
'**/*.key',
|
|
243
|
+
'',
|
|
244
|
+
'# Never copied in, always produced inside the image: a stale `dist` or',
|
|
245
|
+
'# `tsbuildinfo` from the host makes `tsc -b` treat an absent `dist` as up to',
|
|
246
|
+
'# date and emit nothing, and the host `node_modules` is a symlink farm built',
|
|
247
|
+
"# against the host's paths and platform.",
|
|
248
|
+
'**/node_modules',
|
|
249
|
+
'**/dist',
|
|
250
|
+
'**/*.tsbuildinfo',
|
|
251
|
+
'**/.turbo',
|
|
252
|
+
'',
|
|
253
|
+
'# Noise: bytes in the context are bytes over the daemon socket on every build.',
|
|
254
|
+
'.git',
|
|
255
|
+
'.github',
|
|
256
|
+
'**/coverage',
|
|
257
|
+
'**/*.log',
|
|
258
|
+
'',
|
|
259
|
+
].join('\n');
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* The engines an app talks to, each with a healthcheck.
|
|
263
|
+
*
|
|
264
|
+
* The healthchecks are the point (PLAN.md §21). `docker compose up -d` returns as
|
|
265
|
+
* soon as containers are *created*, so `pnpm db:migrate` on the next line of the
|
|
266
|
+
* README raced a server that had not finished initialising. Worse, the official
|
|
267
|
+
* images serve that initialisation from a temporary server bound to a unix socket
|
|
268
|
+
* only: measured on postgres:16, the socket accepts connections at t+0.86s and
|
|
269
|
+
* the real listener the app connects to appears at t+1.19s — so a check that does
|
|
270
|
+
* not name a TCP host reports ready before the database exists, and the gap grows
|
|
271
|
+
* with the size of the data directory. Each check below therefore goes through
|
|
272
|
+
* 127.0.0.1, and `up -d --wait` blocks until it passes.
|
|
273
|
+
*/
|
|
274
|
+
function dockerCompose(engine) {
|
|
275
|
+
const database = engine === 'mongodb'
|
|
276
|
+
? [
|
|
277
|
+
' mongo:',
|
|
278
|
+
' image: mongo:7',
|
|
279
|
+
' ports: [' + "'27017:27017'" + ']',
|
|
280
|
+
' volumes: [mongo-data:/data/db]',
|
|
281
|
+
' healthcheck:',
|
|
282
|
+
// `mongosh --eval` exits non-zero when it cannot connect, which is what
|
|
283
|
+
// makes it usable as a check; `db.adminCommand('ping')` needs no auth.
|
|
284
|
+
' test: [' +
|
|
285
|
+
"'CMD-SHELL', 'mongosh --quiet --host 127.0.0.1 " +
|
|
286
|
+
'--eval "db.adminCommand({ ping: 1 }).ok" | grep -q 1\'' +
|
|
287
|
+
']',
|
|
288
|
+
' interval: 5s',
|
|
289
|
+
' timeout: 5s',
|
|
290
|
+
' retries: 12',
|
|
291
|
+
' start_period: 10s',
|
|
292
|
+
]
|
|
293
|
+
: engine === 'mariadb'
|
|
294
|
+
? [
|
|
295
|
+
' mariadb:',
|
|
296
|
+
// MariaDB, not mysql:8: the workspace picked the MariaDB dialect, and
|
|
297
|
+
// developing against a server that is only nearly compatible with it
|
|
298
|
+
// moves the surprises to production.
|
|
299
|
+
' image: mariadb:11',
|
|
300
|
+
' environment:',
|
|
301
|
+
' MARIADB_DATABASE: app',
|
|
302
|
+
' MARIADB_ROOT_PASSWORD: app',
|
|
303
|
+
' ports: [' + "'3306:3306'" + ']',
|
|
304
|
+
' volumes: [mariadb-data:/var/lib/mysql]',
|
|
305
|
+
' healthcheck:',
|
|
306
|
+
// Shipped in the image for this purpose, and needs no credentials on
|
|
307
|
+
// the command line. `--innodb_initialized` is the half that matters:
|
|
308
|
+
// the server answers a ping before InnoDB has finished recovery.
|
|
309
|
+
' test: [' + "'CMD', 'healthcheck.sh', '--connect', '--innodb_initialized'" + ']',
|
|
310
|
+
' interval: 5s',
|
|
311
|
+
' timeout: 5s',
|
|
312
|
+
' retries: 12',
|
|
313
|
+
' start_period: 10s',
|
|
314
|
+
]
|
|
315
|
+
: engine === 'mysql'
|
|
316
|
+
? [
|
|
317
|
+
' mysql:',
|
|
318
|
+
' image: mysql:8',
|
|
319
|
+
' environment:',
|
|
320
|
+
' MYSQL_DATABASE: app',
|
|
321
|
+
' MYSQL_ROOT_PASSWORD: app',
|
|
322
|
+
' ports: [' + "'3306:3306'" + ']',
|
|
323
|
+
' volumes: [mysql-data:/var/lib/mysql]',
|
|
324
|
+
' healthcheck:',
|
|
325
|
+
// `$$` so compose passes the variable through for the container's
|
|
326
|
+
// shell to expand, rather than substituting it from the host
|
|
327
|
+
// environment — where it is not set. Through `MYSQL_PWD` rather
|
|
328
|
+
// than `-p`, which prints "using a password on the command line is
|
|
329
|
+
// insecure" into the healthcheck log on every probe and tempts the
|
|
330
|
+
// next reader to drop the credentials — leaving a check that
|
|
331
|
+
// passes on a server still rejecting every login.
|
|
332
|
+
' test: [' +
|
|
333
|
+
"'CMD-SHELL', 'MYSQL_PWD=\"$$MYSQL_ROOT_PASSWORD\" " +
|
|
334
|
+
"mysqladmin ping -h 127.0.0.1 -u root --silent'" +
|
|
335
|
+
']',
|
|
336
|
+
' interval: 5s',
|
|
337
|
+
' timeout: 5s',
|
|
338
|
+
' retries: 12',
|
|
339
|
+
' start_period: 10s',
|
|
340
|
+
]
|
|
341
|
+
: [
|
|
342
|
+
' postgres:',
|
|
343
|
+
' image: postgres:16',
|
|
344
|
+
' environment:',
|
|
345
|
+
' POSTGRES_USER: app',
|
|
346
|
+
' POSTGRES_PASSWORD: app',
|
|
347
|
+
' POSTGRES_DB: app',
|
|
348
|
+
' ports: [' + "'5432:5432'" + ']',
|
|
349
|
+
' volumes: [postgres-data:/var/lib/postgresql/data]',
|
|
350
|
+
' healthcheck:',
|
|
351
|
+
// Without `-h 127.0.0.1`, pg_isready answers over the unix socket
|
|
352
|
+
// the initialisation phase serves on, and reports ready some seconds
|
|
353
|
+
// before the database the app connects to exists.
|
|
354
|
+
' test: [' + "'CMD-SHELL', 'pg_isready -h 127.0.0.1 -U app -d app'" + ']',
|
|
355
|
+
' interval: 5s',
|
|
356
|
+
' timeout: 5s',
|
|
357
|
+
// A minute of retries: initialising a data directory on a slow disk
|
|
358
|
+
// takes longer than a healthcheck's patience would suggest.
|
|
359
|
+
' retries: 12',
|
|
360
|
+
' start_period: 10s',
|
|
361
|
+
];
|
|
362
|
+
const volumeName = engine === 'mongodb'
|
|
363
|
+
? 'mongo-data'
|
|
364
|
+
: engine === 'mariadb'
|
|
365
|
+
? 'mariadb-data'
|
|
366
|
+
: engine === 'mysql'
|
|
367
|
+
? 'mysql-data'
|
|
368
|
+
: 'postgres-data';
|
|
369
|
+
return ['services:', ...database, '', 'volumes:', ` ${volumeName}:`, ''].join('\n');
|
|
370
|
+
}
|
|
371
|
+
function workspaceCi() {
|
|
372
|
+
return [
|
|
373
|
+
'name: CI',
|
|
374
|
+
'',
|
|
375
|
+
'on:',
|
|
376
|
+
' push:',
|
|
377
|
+
' branches: [main]',
|
|
378
|
+
' pull_request:',
|
|
379
|
+
'',
|
|
380
|
+
'jobs:',
|
|
381
|
+
' ci:',
|
|
382
|
+
' runs-on: ubuntu-latest',
|
|
383
|
+
' steps:',
|
|
384
|
+
' - uses: actions/checkout@v4',
|
|
385
|
+
' - uses: pnpm/action-setup@v4',
|
|
386
|
+
' - uses: actions/setup-node@v4',
|
|
387
|
+
' with:',
|
|
388
|
+
' node-version-file: .nvmrc',
|
|
389
|
+
' cache: pnpm',
|
|
390
|
+
' - run: pnpm install --frozen-lockfile',
|
|
391
|
+
' # Build first: type-aware lint resolves workspace packages through',
|
|
392
|
+
' # their build output.',
|
|
393
|
+
' - run: pnpm build',
|
|
394
|
+
' - run: pnpm lint',
|
|
395
|
+
' - run: pnpm typecheck',
|
|
396
|
+
' - run: pnpm test',
|
|
397
|
+
' - run: pnpm doctor',
|
|
398
|
+
' # Every app image, on every pull request. A Dockerfile that is only built',
|
|
399
|
+
' # at release time is a Dockerfile that has never been built, and it fails',
|
|
400
|
+
' # at the least convenient moment there is. The loop covers apps added',
|
|
401
|
+
' # later without being edited.',
|
|
402
|
+
' - name: Build app images',
|
|
403
|
+
' run: |',
|
|
404
|
+
' for dockerfile in apps/*/Dockerfile; do',
|
|
405
|
+
' docker build -f "$dockerfile" -t "$(basename "$(dirname "$dockerfile")"):ci" .',
|
|
406
|
+
' done',
|
|
407
|
+
'',
|
|
408
|
+
].join('\n');
|
|
409
|
+
}
|
|
410
|
+
function readme(manifest) {
|
|
411
|
+
return [
|
|
412
|
+
`# ${manifest.name}`,
|
|
413
|
+
'',
|
|
414
|
+
'Generated with `nage create`.',
|
|
415
|
+
'',
|
|
416
|
+
'## Getting started',
|
|
417
|
+
'',
|
|
418
|
+
'```bash',
|
|
419
|
+
'pnpm install',
|
|
420
|
+
'cp .env.example .env',
|
|
421
|
+
...(manifest.engine === 'sqlite'
|
|
422
|
+
? // SQLite needs no server, so there is no compose file to bring up.
|
|
423
|
+
[]
|
|
424
|
+
: [
|
|
425
|
+
'# --wait, not bare `up -d`: it blocks until the healthcheck passes, and',
|
|
426
|
+
'# a migration run against a database still initialising fails.',
|
|
427
|
+
'docker compose up -d --wait',
|
|
428
|
+
]),
|
|
429
|
+
'pnpm db:migrate',
|
|
430
|
+
'pnpm dev',
|
|
431
|
+
'```',
|
|
432
|
+
'',
|
|
433
|
+
'## Layout',
|
|
434
|
+
'',
|
|
435
|
+
'```',
|
|
436
|
+
'apps/ deployable applications',
|
|
437
|
+
'packages/ shared local packages (@app/*)',
|
|
438
|
+
'database/ migrations and seeds shared by every app',
|
|
439
|
+
'```',
|
|
440
|
+
'',
|
|
441
|
+
'## Commands',
|
|
442
|
+
'',
|
|
443
|
+
'| Command | Purpose |',
|
|
444
|
+
'|---|---|',
|
|
445
|
+
'| `nage new app <name>` | Add another deployable app |',
|
|
446
|
+
'| `nage new package <name>` | Add a shared local package |',
|
|
447
|
+
'| `nage g resource <name>` | Generate a wired CRUD resource |',
|
|
448
|
+
'| `nage add <feature>` | Enable an optional package for an app |',
|
|
449
|
+
'| `nage doctor` | Check workspace integrity and configuration |',
|
|
450
|
+
'',
|
|
451
|
+
].join('\n');
|
|
452
|
+
}
|
|
453
|
+
/** Deterministic JSON with a trailing newline, so regeneration is diff-free. */
|
|
454
|
+
function json(value) {
|
|
455
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
456
|
+
}
|
|
457
|
+
//# sourceMappingURL=workspace.template.js.map
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `nage.workspace.json` — the source of truth for the workspace (PLAN.md §10.4).
|
|
3
|
+
*
|
|
4
|
+
* Successor to the legacy single-file `.nac-metadata.json`, extended for many
|
|
5
|
+
* apps. `new app` / `remove app` / `add` / `list` / `doctor` all read and write
|
|
6
|
+
* it, and it is serialised deterministically so a regenerated manifest produces
|
|
7
|
+
* an empty diff rather than reordered noise.
|
|
8
|
+
*/
|
|
9
|
+
import type { DatabaseDriver } from '@nage-api/contracts';
|
|
10
|
+
export declare const MANIFEST_FILE = "nage.workspace.json";
|
|
11
|
+
/** App shapes the CLI can scaffold (§10.2). */
|
|
12
|
+
export type Preset = 'minimal' | 'api' | 'api-realtime' | 'worker';
|
|
13
|
+
/** Optional packages an app can enable (§10.1). */
|
|
14
|
+
export type FeatureName = 'auth' | 'cache' | 'queue' | 'storage' | 'realtime' | 'notify' | 'observability';
|
|
15
|
+
export interface AppEntry {
|
|
16
|
+
readonly name: string;
|
|
17
|
+
readonly preset: Preset;
|
|
18
|
+
/** Distinct per app; `doctor` reports a collision. */
|
|
19
|
+
readonly port: number;
|
|
20
|
+
readonly features: readonly FeatureName[];
|
|
21
|
+
}
|
|
22
|
+
export interface PackageEntry {
|
|
23
|
+
readonly name: string;
|
|
24
|
+
/** Import alias, e.g. `@app/domain`. */
|
|
25
|
+
readonly alias: string;
|
|
26
|
+
}
|
|
27
|
+
export interface WorkspaceManifest {
|
|
28
|
+
readonly version: 1;
|
|
29
|
+
readonly name: string;
|
|
30
|
+
/**
|
|
31
|
+
* One engine per workspace (§10.2): apps share `packages/domain` entities and
|
|
32
|
+
* `database/migrations`, which only stays coherent with a single engine.
|
|
33
|
+
*/
|
|
34
|
+
readonly engine: DatabaseDriver;
|
|
35
|
+
/** The `@nage-api/*` version line every app pins to; `upgrade` moves it. */
|
|
36
|
+
readonly frameworkVersion: string;
|
|
37
|
+
readonly apps: readonly AppEntry[];
|
|
38
|
+
readonly packages: readonly PackageEntry[];
|
|
39
|
+
}
|
|
40
|
+
export declare const DEFAULT_PORT = 3000;
|
|
41
|
+
export declare function createManifest(input: {
|
|
42
|
+
name: string;
|
|
43
|
+
engine: DatabaseDriver;
|
|
44
|
+
frameworkVersion: string;
|
|
45
|
+
}): WorkspaceManifest;
|
|
46
|
+
/** Serialise deterministically: sorted collections, stable key order, trailing newline. */
|
|
47
|
+
export declare function serialiseManifest(manifest: WorkspaceManifest): string;
|
|
48
|
+
/** Parse and validate a manifest, naming what is wrong rather than throwing a cast error. */
|
|
49
|
+
export declare function parseManifest(contents: string, location?: string): WorkspaceManifest;
|
|
50
|
+
/**
|
|
51
|
+
* Walk up from `start` looking for a workspace root.
|
|
52
|
+
*
|
|
53
|
+
* Commands can then be run from anywhere inside the workspace, which is how
|
|
54
|
+
* `--app` stays optional: the current directory usually says which app is meant.
|
|
55
|
+
*/
|
|
56
|
+
export declare function findWorkspaceRoot(start: string): string | undefined;
|
|
57
|
+
/** Load the manifest for the workspace containing `start`. */
|
|
58
|
+
export declare function loadWorkspace(start: string): Promise<{
|
|
59
|
+
root: string;
|
|
60
|
+
manifest: WorkspaceManifest;
|
|
61
|
+
}>;
|
|
62
|
+
/** Which app a command targets: `--app`, or the one the cwd sits inside. */
|
|
63
|
+
export declare function resolveTargetApp(manifest: WorkspaceManifest, options: {
|
|
64
|
+
root: string;
|
|
65
|
+
cwd: string;
|
|
66
|
+
app?: string;
|
|
67
|
+
}): AppEntry;
|
|
68
|
+
/** Next free port, so two apps never default to the same one. */
|
|
69
|
+
export declare function nextAvailablePort(manifest: WorkspaceManifest): number;
|
|
70
|
+
//# sourceMappingURL=manifest.d.ts.map
|