@dunx/create-app 3.0.2 → 3.0.4
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 +77 -11
- package/dist/agents.d.ts +5 -0
- package/dist/banner.d.ts +14 -0
- package/dist/{chunk-nn9ekg83.js → chunk-5tn3ybzv.js} +170 -68
- package/dist/cli.js +750 -68
- package/dist/features.d.ts +2 -2
- package/dist/generate.d.ts +0 -3
- package/dist/index.d.ts +2 -1
- package/dist/index.js +9 -1
- package/dist/keys.d.ts +48 -0
- package/dist/prompt.d.ts +43 -0
- package/dist/prompts.d.ts +53 -0
- package/dist/scaffold.d.ts +17 -0
- package/dist/style.d.ts +26 -0
- package/dist/tty.d.ts +58 -0
- package/dist/wizard.d.ts +35 -0
- package/package.json +1 -1
- package/templates/features/assets/assets.module.ts +1 -1
- package/templates/features/auth/auth.module.ts +1 -1
- package/templates/features/http/http.module.ts +1 -1
- package/templates/minimal/package.json +3 -2
package/README.md
CHANGED
|
@@ -13,18 +13,52 @@ bunx @dunx/create-app my-api
|
|
|
13
13
|
```
|
|
14
14
|
cd my-api
|
|
15
15
|
bun install
|
|
16
|
-
bun run
|
|
16
|
+
bun run dev # http://localhost:3000/greetings, restarting on a change
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
## The questions
|
|
20
|
+
|
|
21
|
+
There is no flag for choosing features. The command opens a list:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
? Features 2 chosen, 1 pulled in
|
|
25
|
+
○ notes CRUD routes with zod validation. The smallest real feature.
|
|
26
|
+
◉ openapi OpenAPI 3.1 from the routes own schemas, plus the Swagger UI page.
|
|
27
|
+
◈ database drizzle over bun:sqlite, with a schema, seeds and migrations.
|
|
28
|
+
❯ ◉ users A repository, a service and validated routes over the database.
|
|
29
|
+
○ auth better-auth mounted, with SessionGuard and an audit trail.
|
|
30
|
+
database comes along as a requirement.
|
|
31
|
+
Space toggles. ↑↓ moves. a all, n none. Enter continues.
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
| Key | Does |
|
|
35
|
+
| --------------- | --------------------------------------------- |
|
|
36
|
+
| `↑` `↓`, `k` `j`, Tab | Move the cursor |
|
|
37
|
+
| Space | Toggle the feature under it |
|
|
38
|
+
| `a` / `n` | Everything / nothing |
|
|
39
|
+
| Enter | Take the selection |
|
|
40
|
+
| Ctrl+C, Esc | Stop, having written nothing |
|
|
41
|
+
|
|
42
|
+
`◉` is chosen, `◈` is pulled in by something else you chose, `○` is neither. The
|
|
43
|
+
two lines under the list update as you go: what your selection drags in, and which
|
|
44
|
+
of it needs Redis or Postgres running to do anything.
|
|
45
|
+
|
|
46
|
+
Three more questions appear only when there is something to ask: a directory, when
|
|
47
|
+
the command line named none; a package name, when the directory's is one npm would
|
|
48
|
+
reject; and whether to write into a directory that already has files in it.
|
|
49
|
+
|
|
19
50
|
## Options
|
|
20
51
|
|
|
21
|
-
| Flag
|
|
22
|
-
|
|
|
23
|
-
| `--name <name>`
|
|
24
|
-
| `--
|
|
25
|
-
| `--
|
|
26
|
-
| `--
|
|
27
|
-
|
|
52
|
+
| Flag | Default | Meaning |
|
|
53
|
+
| --------------- | ------------------ | --------------------------------------------- |
|
|
54
|
+
| `--name <name>` | the directory name | Package name for the generated app |
|
|
55
|
+
| `--force` | off | Write into a directory that already has files |
|
|
56
|
+
| `--yes`, `-y` | off | Skip the questions, take the minimal template |
|
|
57
|
+
| `--help` | | Print usage |
|
|
58
|
+
|
|
59
|
+
**Piped, redirected or in CI it asks nothing** and writes the minimal template, so
|
|
60
|
+
a script never hangs on a question nothing can answer. To choose features without
|
|
61
|
+
a terminal, call [`scaffold`](#programmatic-use) rather than passing flags.
|
|
28
62
|
|
|
29
63
|
The name is validated against npm's rules **before** anything is created, because
|
|
30
64
|
an invalid one would otherwise surface as a confusing `bun install` failure inside
|
|
@@ -38,9 +72,31 @@ bunx @dunx/create-app .
|
|
|
38
72
|
```
|
|
39
73
|
|
|
40
74
|
`.git`, `.gitkeep`, `.DS_Store` and `LICENSE` do not count as contents, so a fresh
|
|
41
|
-
repo or a clone of an empty GitHub repository is a valid target without
|
|
75
|
+
repo or a clone of an empty GitHub repository is a valid target without a question.
|
|
42
76
|
Nothing else is ignored: `.gitignore` and `README.md` both come out of the template,
|
|
43
|
-
and overwriting your copy of either is what
|
|
77
|
+
and overwriting your copy of either is what the last question asks about.
|
|
78
|
+
|
|
79
|
+
## What a composed app looks like
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
my-api/
|
|
83
|
+
src/
|
|
84
|
+
main.ts exports createApp, and serves it when run directly
|
|
85
|
+
app.module.ts the root module, importing every feature
|
|
86
|
+
config.ts one validation function, flat env in and a shaped object out
|
|
87
|
+
users/ one directory per feature you chose
|
|
88
|
+
package.json dev, start, test, typecheck
|
|
89
|
+
bunfig.toml the transform preload
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Three files are generated for the selection and the feature directories are copied.
|
|
93
|
+
`main.ts` is one file rather than two: a test imports `createApp` from it, and the
|
|
94
|
+
`import.meta.main` block at the bottom is what stops that starting a server.
|
|
95
|
+
|
|
96
|
+
**There is no worker entry point, even with queues.** `QueueModule` is given
|
|
97
|
+
`consume: true`, so the container opens the bullmq workers at `onInit` and closes
|
|
98
|
+
them before the connections the handlers use. A handler marked `background: true` is
|
|
99
|
+
forked by bullmq itself into `src/jobs/jobs.processor.ts`.
|
|
44
100
|
|
|
45
101
|
## What it generates
|
|
46
102
|
|
|
@@ -49,7 +105,12 @@ The `minimal` template, the same app as
|
|
|
49
105
|
server, and the `bunfig.toml` preload line that makes constructor injection work.
|
|
50
106
|
|
|
51
107
|
Its `src/` is a **byte-for-byte copy** of that example, and a test in this package
|
|
52
|
-
fails if the two ever drift.
|
|
108
|
+
fails if the two ever drift.
|
|
109
|
+
|
|
110
|
+
Every app also gets an `AGENTS.md` naming its layout, its commands and the rules
|
|
111
|
+
dunx fails at boot over, plus a `CLAUDE.md` pointing at it. Both link
|
|
112
|
+
<https://petarzarkov.github.io/dunx/setup.md>, which is served per release, rather
|
|
113
|
+
than copying the framework's own instructions into your repository. The example is the one CI boots, so keeping them
|
|
53
114
|
identical is what makes the template trustworthy rather than merely plausible.
|
|
54
115
|
|
|
55
116
|
## Two details
|
|
@@ -80,9 +141,14 @@ import { scaffold } from '@dunx/create-app';
|
|
|
80
141
|
const { directory, files } = await scaffold({
|
|
81
142
|
target: 'my-api',
|
|
82
143
|
name: '@acme/my-api',
|
|
144
|
+
features: ['users', 'openapi'],
|
|
83
145
|
});
|
|
84
146
|
```
|
|
85
147
|
|
|
148
|
+
This is the scripted path the removed `--with` flag used to be. `features` takes
|
|
149
|
+
the same names the list shows, in any order, and pulls in what they require;
|
|
150
|
+
`FEATURES` exports the set. Omitting it writes the minimal template.
|
|
151
|
+
|
|
86
152
|
`scaffold` throws `ScaffoldError` for anything the caller can fix - an unknown
|
|
87
153
|
template, an unusable package name, a non-empty target without `force` - and lets
|
|
88
154
|
everything else propagate.
|
package/dist/agents.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Feature } from './features.js';
|
|
2
|
+
export declare const CLAUDE_POINTER = "# CLAUDE.md\n\nThe instructions for this application live in `AGENTS.md`, imported below so every\nagent reads one file.\n\n@AGENTS.md\n";
|
|
3
|
+
export declare const agents: (name: string, features: readonly Feature[]) => string;
|
|
4
|
+
/** Both files, written for a fixed template and a composed app alike. */
|
|
5
|
+
export declare const agentFiles: (name: string, features: readonly Feature[]) => Readonly<Record<string, string>>;
|
package/dist/banner.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Style } from './style.js';
|
|
2
|
+
/**
|
|
3
|
+
* The header the questions run under.
|
|
4
|
+
*
|
|
5
|
+
* Printed once, before the first prompt, and only when there is a terminal to
|
|
6
|
+
* print it into: a logo piped into a log file is noise. A terminal too narrow for
|
|
7
|
+
* the logo gets the one-line form rather than a wrapped one, which is what
|
|
8
|
+
* `Bun.stringWidth` is measured against.
|
|
9
|
+
*/
|
|
10
|
+
export declare class Banner {
|
|
11
|
+
#private;
|
|
12
|
+
constructor(style: Style);
|
|
13
|
+
lines(width: number, version: string): readonly string[];
|
|
14
|
+
}
|
|
@@ -221,7 +221,7 @@ var FEATURES = [
|
|
|
221
221
|
{
|
|
222
222
|
name: "jobs",
|
|
223
223
|
source: "jobs",
|
|
224
|
-
summary: "bullmq queues
|
|
224
|
+
summary: "bullmq queues over Bun.RedisClient, background handlers forked.",
|
|
225
225
|
requires: ["images"],
|
|
226
226
|
module: { klass: "JobsModule", from: "./jobs/jobs.module.js" },
|
|
227
227
|
dependencies: ["@dunx/infra", "bullmq", "ioredis", "zod"],
|
|
@@ -318,6 +318,119 @@ import { basename, dirname, join, resolve } from "path";
|
|
|
318
318
|
import { fileURLToPath } from "url";
|
|
319
319
|
var {Glob } = globalThis.Bun;
|
|
320
320
|
|
|
321
|
+
// src/agents.ts
|
|
322
|
+
var SETUP_URL = "https://petarzarkov.github.io/dunx/setup.md";
|
|
323
|
+
var LLMS_URL = "https://petarzarkov.github.io/dunx/llms.txt";
|
|
324
|
+
var CLAUDE_POINTER = `# CLAUDE.md
|
|
325
|
+
|
|
326
|
+
The instructions for this application live in \`AGENTS.md\`, imported below so every
|
|
327
|
+
agent reads one file.
|
|
328
|
+
|
|
329
|
+
@AGENTS.md
|
|
330
|
+
`;
|
|
331
|
+
var RULES = `## Rules that produce a boot error when broken
|
|
332
|
+
|
|
333
|
+
- **No \`@Injectable()\`, no \`@Inject()\`.** Listing a class in a module's
|
|
334
|
+
\`providers\` is what makes it injectable. dunx uses TC39 standard decorators,
|
|
335
|
+
which have no parameter decorators. For a value with no constructor parameter to
|
|
336
|
+
hang off, use \`inject(Token)\` in a field initializer.
|
|
337
|
+
- **Do not add \`reflect-metadata\`, \`experimentalDecorators\` or
|
|
338
|
+
\`emitDecoratorMetadata\`.** \`bunfig.toml\` preloads \`@dunx/transform\`, which
|
|
339
|
+
records each class's constructor parameter types. Removing that line makes every
|
|
340
|
+
provider fail at boot.
|
|
341
|
+
- **A constructor parameter whose type is erased fails at boot, naming the
|
|
342
|
+
parameter.** An interface, a primitive, a union, a class type parameter, or a
|
|
343
|
+
\`import type\` at an injection site all record as \`unresolved\`. Inject a class,
|
|
344
|
+
and drop \`type\` from the import.
|
|
345
|
+
- **Relative imports carry \`.js\`**: \`'./users.service.js'\`, never
|
|
346
|
+
\`'./users.service'\`.
|
|
347
|
+
- **A module's \`exports\` is its public surface.** The container is scoped per
|
|
348
|
+
module, so a provider another module injects has to be exported by the module that
|
|
349
|
+
declares it.
|
|
350
|
+
- **\`bun\` only.** No \`npm\`, \`npx\`, \`yarn\` or \`pnpm\`; run tools with \`bunx\`.
|
|
351
|
+
`;
|
|
352
|
+
var layout = (features) => features.length === 0 ? `- \`src/main.ts\` - the entry point
|
|
353
|
+
- \`src/app.module.ts\` - the root module; \`controllers\` get routes, \`providers\` do not
|
|
354
|
+
- \`src/greetings.controller.ts\`, \`src/greetings.service.ts\` - one route and its provider
|
|
355
|
+
- \`src/app.test.ts\` - the whole app behind a real server on port 0
|
|
356
|
+
- \`bunfig.toml\` - the preload line constructor injection needs
|
|
357
|
+
` : `- \`src/main.ts\` - exports \`createApp\`, and serves it when run directly
|
|
358
|
+
- \`src/app.module.ts\` - the root module, importing every feature
|
|
359
|
+
- \`src/config.ts\` - one validation function, flat env in and a shaped object out
|
|
360
|
+
${features.map((feature) => `- \`src/${feature.source}/\` - ${feature.name}`).join(`
|
|
361
|
+
`)}
|
|
362
|
+
- \`bunfig.toml\` - the preload line constructor injection needs
|
|
363
|
+
|
|
364
|
+
\`main.ts\`, \`app.module.ts\` and \`config.ts\` were generated for the features chosen
|
|
365
|
+
at scaffold time. Everything else was copied from dunx's \`examples/full\`. The
|
|
366
|
+
\`*.demo.ts\` files are that example's scripted walkthroughs; delete one and its
|
|
367
|
+
\`providers\` entry to drop it.
|
|
368
|
+
|
|
369
|
+
A test imports \`createApp\` from \`./main.js\`; the \`import.meta.main\` block at the
|
|
370
|
+
bottom is what stops that starting a server.
|
|
371
|
+
`;
|
|
372
|
+
var agents = (name, features) => {
|
|
373
|
+
const services = features.filter((feature) => feature.service !== undefined);
|
|
374
|
+
const hasJobs = features.some((feature) => feature.name === "jobs");
|
|
375
|
+
return `# ${name}
|
|
376
|
+
|
|
377
|
+
Notes for an agent working in this application. It is a
|
|
378
|
+
[dunx](https://github.com/petarzarkov/dunx) app, scaffolded by
|
|
379
|
+
\`bunx @dunx/create-app\`.
|
|
380
|
+
|
|
381
|
+
## Commands
|
|
382
|
+
|
|
383
|
+
\`\`\`bash
|
|
384
|
+
bun install
|
|
385
|
+
bun run dev # http://localhost:3000, restarting on a change
|
|
386
|
+
bun run start
|
|
387
|
+
bun test
|
|
388
|
+
bun run typecheck
|
|
389
|
+
\`\`\`${hasJobs ? `
|
|
390
|
+
|
|
391
|
+
**There is no worker command.** \`QueueModule\` is given \`consume: true\`, so the
|
|
392
|
+
container opens the workers at \`onInit\` and closes them before the connections they
|
|
393
|
+
use. A handler marked \`background: true\` is forked by bullmq into
|
|
394
|
+
\`src/jobs/jobs.processor.ts\`, which nobody runs by hand.` : ""}
|
|
395
|
+
|
|
396
|
+
## Layout
|
|
397
|
+
|
|
398
|
+
${layout(features)}
|
|
399
|
+
${features.length === 0 ? "" : `## What is wired up
|
|
400
|
+
|
|
401
|
+
${features.map((feature) => `- **${feature.name}** - ${feature.summary}`).join(`
|
|
402
|
+
`)}
|
|
403
|
+
|
|
404
|
+
`}${services.length === 0 ? "" : `## Services
|
|
405
|
+
|
|
406
|
+
Each of these reports itself degraded rather than failing the boot, so the app
|
|
407
|
+
starts without them.
|
|
408
|
+
|
|
409
|
+
${services.map((feature) => `- **${feature.name}** needs ${feature.service}`).join(`
|
|
410
|
+
`)}
|
|
411
|
+
|
|
412
|
+
`}${RULES}
|
|
413
|
+
## Reading this app instead of grepping it
|
|
414
|
+
|
|
415
|
+
\`\`\`bash
|
|
416
|
+
bunx @dunx/mcp ./src/app.module.ts
|
|
417
|
+
\`\`\`
|
|
418
|
+
|
|
419
|
+
An MCP server over stdio answering what routes, providers, modules and gateways
|
|
420
|
+
exist, and which constructor parameters would fail to resolve. It reads the module
|
|
421
|
+
graph and never boots the app.
|
|
422
|
+
|
|
423
|
+
## The framework's own instructions
|
|
424
|
+
|
|
425
|
+
- <${SETUP_URL}> - installing, wiring and verifying a dunx app
|
|
426
|
+
- <${LLMS_URL}> - every dunx document, as raw markdown
|
|
427
|
+
`;
|
|
428
|
+
};
|
|
429
|
+
var agentFiles = (name, features) => ({
|
|
430
|
+
"AGENTS.md": agents(name, features),
|
|
431
|
+
"CLAUDE.md": CLAUDE_POINTER
|
|
432
|
+
});
|
|
433
|
+
|
|
321
434
|
// src/generate.ts
|
|
322
435
|
var HEADER = (name) => `// Generated by @dunx/create-app for ${name}. Yours to edit.
|
|
323
436
|
`;
|
|
@@ -344,13 +457,11 @@ var manifest = (features) => {
|
|
|
344
457
|
dependencies[dep] = DUNX.test(dep) ? "__DUNX_VERSION__" : versionOf(dep);
|
|
345
458
|
}
|
|
346
459
|
const scripts = {
|
|
460
|
+
dev: "bun --watch src/main.ts",
|
|
347
461
|
start: "bun src/main.ts",
|
|
348
462
|
test: "bun test",
|
|
349
463
|
typecheck: "tsc --noEmit"
|
|
350
464
|
};
|
|
351
|
-
if (features.some((feature) => feature.name === "jobs")) {
|
|
352
|
-
scripts["worker"] = "bun src/worker.ts";
|
|
353
|
-
}
|
|
354
465
|
return `${JSON.stringify({
|
|
355
466
|
name: "__DUNX_APP_NAME__",
|
|
356
467
|
version: "0.1.0",
|
|
@@ -477,15 +588,17 @@ ${chosen.map(([, group]) => ` ${group.map}`).join(`
|
|
|
477
588
|
`;
|
|
478
589
|
};
|
|
479
590
|
var has = (features, name) => features.some((feature) => feature.name === name);
|
|
480
|
-
var
|
|
591
|
+
var main = (name, features) => {
|
|
481
592
|
const openapi = has(features, "openapi");
|
|
482
593
|
const websockets = has(features, "websockets");
|
|
483
594
|
const http = has(features, "http");
|
|
595
|
+
const health = has(features, "health");
|
|
484
596
|
const documentsAuth = openapi && has(features, "auth");
|
|
485
597
|
const assets = has(features, "assets");
|
|
486
598
|
const throttle = has(features, "throttle");
|
|
487
599
|
const imports = [
|
|
488
600
|
...documentsAuth ? ["import { Auth, betterAuthDocument } from '@dunx/auth';"] : [],
|
|
601
|
+
"import { Logger } from '@dunx/core';",
|
|
489
602
|
`import { ${[
|
|
490
603
|
"HttpFactory",
|
|
491
604
|
...websockets ? ["RedisRelay"] : [],
|
|
@@ -495,12 +608,9 @@ var bootstrap = (name, features) => {
|
|
|
495
608
|
].join(", ")} } from '@dunx/http';`,
|
|
496
609
|
...openapi ? ["import { OpenApiModule } from '@dunx/openapi';"] : [],
|
|
497
610
|
"import { AppModule } from './app.module.js';",
|
|
498
|
-
`import { ${[
|
|
499
|
-
...http ? ["AppConfigService"] : [],
|
|
500
|
-
...websockets ? ["RELAY_CHANNEL"] : []
|
|
501
|
-
].join(", ")} } from './config.js';`,
|
|
611
|
+
`import { ${["AppConfigService", ...websockets ? ["RELAY_CHANNEL"] : []].join(", ")} } from './config.js';`,
|
|
502
612
|
...http ? ["import { RequestTrailMiddleware } from './http/request-trail.js';"] : []
|
|
503
|
-
]
|
|
613
|
+
];
|
|
504
614
|
const root = documentsAuth ? `OpenApiModule.forRootAsync({
|
|
505
615
|
root: AppModule,
|
|
506
616
|
inject: [Auth] as const,
|
|
@@ -538,12 +648,20 @@ var bootstrap = (name, features) => {
|
|
|
538
648
|
] : [],
|
|
539
649
|
...throttle ? ["app.use(ThrottleGuard);"] : []
|
|
540
650
|
];
|
|
651
|
+
const urls = [
|
|
652
|
+
...openapi ? [
|
|
653
|
+
"logger.info(`docs ${new URL('api/docs', url).href}`);",
|
|
654
|
+
"logger.info(`openapi ${new URL('api/openapi.json', url).href}`);"
|
|
655
|
+
] : [],
|
|
656
|
+
...health ? ["logger.info(`health ${new URL('api/health', url).href}`);"] : []
|
|
657
|
+
];
|
|
541
658
|
return `${HEADER(name)}${imports.join(`
|
|
542
659
|
`)}
|
|
543
660
|
|
|
544
661
|
/**
|
|
545
|
-
* One app
|
|
546
|
-
*
|
|
662
|
+
* One app for \`bun run start\`, \`bun run dev\` and the tests, and one file:
|
|
663
|
+
* \`createApp\` is exported for a caller that wants the shape without a server, and
|
|
664
|
+
* the block at the bottom serves it when this file is the entry point.
|
|
547
665
|
*
|
|
548
666
|
* \`create()\` boots the container and discovers routes and gateways; \`listen()\` is
|
|
549
667
|
* what builds the \`Bun.serve\` route table. Everything between the two still gets to
|
|
@@ -564,23 +682,8 @@ ${shaping.map((line) => ` ${line}`).join(`
|
|
|
564
682
|
|
|
565
683
|
return app;
|
|
566
684
|
};
|
|
567
|
-
`;
|
|
568
|
-
};
|
|
569
|
-
var main = (name, features) => {
|
|
570
|
-
const health = has(features, "health");
|
|
571
|
-
const openapi = has(features, "openapi");
|
|
572
|
-
const lines = [
|
|
573
|
-
...openapi ? [
|
|
574
|
-
"logger.info(`docs ${new URL('api/docs', url).href}`);",
|
|
575
|
-
"logger.info(`openapi ${new URL('api/openapi.json', url).href}`);"
|
|
576
|
-
] : [],
|
|
577
|
-
...health ? ["logger.info(`health ${new URL('api/health', url).href}`);"] : []
|
|
578
|
-
];
|
|
579
|
-
return `${HEADER(name)}import { Logger } from '@dunx/core';
|
|
580
|
-
import { createApp } from './bootstrap.js';
|
|
581
|
-
import { AppConfigService } from './config.js';
|
|
582
685
|
|
|
583
|
-
async
|
|
686
|
+
const start = async (): Promise<void> => {
|
|
584
687
|
const app = await createApp();
|
|
585
688
|
app.enableShutdownHooks();
|
|
586
689
|
|
|
@@ -589,31 +692,24 @@ async function bootstrap(): Promise<void> {
|
|
|
589
692
|
const url = await app.listen(config.get('port'));
|
|
590
693
|
|
|
591
694
|
logger.info(\`listening on \${url}\`);
|
|
592
|
-
${
|
|
593
|
-
`)}${
|
|
695
|
+
${urls.map((line) => ` ${line}`).join(`
|
|
696
|
+
`)}${urls.length > 0 ? `
|
|
594
697
|
` : ""}
|
|
595
698
|
// Nothing else to do: the server holds the process open, and the shutdown hooks
|
|
596
699
|
// resolve this once a signal arrives.
|
|
597
700
|
await app.closed;
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
bootstrap().catch((error: unknown) => {
|
|
601
|
-
console.error('failed to start', error);
|
|
602
|
-
process.exit(1);
|
|
603
|
-
});
|
|
604
|
-
`;
|
|
605
701
|
};
|
|
606
|
-
var worker = (name) => `${HEADER(name)}import { AppFactory } from '@dunx/core';
|
|
607
|
-
import { AppModule } from './app.module.js';
|
|
608
702
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
703
|
+
// False when a test imports this file for \`createApp\` alone, which is what lets one
|
|
704
|
+
// module be both the entry point and the app's definition.
|
|
705
|
+
if (import.meta.main) {
|
|
706
|
+
start().catch((error: unknown) => {
|
|
707
|
+
console.error('failed to start', error);
|
|
708
|
+
process.exit(1);
|
|
709
|
+
});
|
|
710
|
+
}
|
|
616
711
|
`;
|
|
712
|
+
};
|
|
617
713
|
var envExample = (groups) => {
|
|
618
714
|
const lines = groups.flatMap((group) => CONFIG_GROUPS[group]?.env ?? []).map((entry) => `${entry.name}=${entry.value}`);
|
|
619
715
|
return lines.length === 0 ? `# Every variable has a default, so this file is optional.
|
|
@@ -630,6 +726,7 @@ Scaffolded with \`bunx @dunx/create-app\`.
|
|
|
630
726
|
|
|
631
727
|
\`\`\`bash
|
|
632
728
|
bun install
|
|
729
|
+
bun run dev # restarts on a change
|
|
633
730
|
bun run start
|
|
634
731
|
\`\`\`
|
|
635
732
|
|
|
@@ -648,17 +745,19 @@ ${services.map((feature) => `- **${feature.name}** needs ${feature.service}`).jo
|
|
|
648
745
|
|
|
649
746
|
`}## Layout
|
|
650
747
|
|
|
651
|
-
- \`src/main.ts\` -
|
|
652
|
-
- \`src/bootstrap.ts\` - builds the app; shared by \`start\` and the tests
|
|
748
|
+
- \`src/main.ts\` - exports \`createApp\`, and serves it when run directly
|
|
653
749
|
- \`src/app.module.ts\` - the root module, importing every feature
|
|
654
750
|
- \`src/config.ts\` - one validation function, flat env in and a shaped object out
|
|
655
751
|
${features.map((feature) => `- \`src/${feature.source}/\` - ${feature.name}`).join(`
|
|
656
752
|
`)}
|
|
657
753
|
|
|
658
|
-
\`main.ts\`, \`
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
754
|
+
\`main.ts\`, \`app.module.ts\` and \`config.ts\` were generated for the features you
|
|
755
|
+
chose; everything else is copied from dunx's \`examples/full\`, which is run and
|
|
756
|
+
toured in CI on every push. The \`*.demo.ts\` files are that example's scripted
|
|
757
|
+
walkthroughs - delete one and its \`providers\` entry when you do not want it.
|
|
758
|
+
|
|
759
|
+
A test imports \`createApp\` from \`./main.js\` and never starts a server: the
|
|
760
|
+
\`import.meta.main\` block at the bottom of the file is false for an import.
|
|
662
761
|
|
|
663
762
|
## Constructor injection
|
|
664
763
|
|
|
@@ -681,13 +780,14 @@ var IGNORED_WHEN_EMPTY = new Set([
|
|
|
681
780
|
".gitkeep",
|
|
682
781
|
"LICENSE"
|
|
683
782
|
]);
|
|
783
|
+
var blockingEntries = (directory) => existsSync(directory) ? readdirSync(directory).filter((entry) => !IGNORED_WHEN_EMPTY.has(entry)).sort() : [];
|
|
684
784
|
|
|
685
785
|
class ScaffoldError extends Error {
|
|
686
786
|
name = "ScaffoldError";
|
|
687
787
|
}
|
|
688
788
|
var templatesRoot = () => resolve(dirname(fileURLToPath(import.meta.url)), "..", "templates");
|
|
689
789
|
var isValidPackageName = (name) => /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(name);
|
|
690
|
-
var
|
|
790
|
+
var packageVersion = async () => {
|
|
691
791
|
const file = Bun.file(join(templatesRoot(), "..", "package.json"));
|
|
692
792
|
const json = await file.json();
|
|
693
793
|
return json.version ?? "0.0.0";
|
|
@@ -695,19 +795,14 @@ var readPackageVersion = async () => {
|
|
|
695
795
|
var fill = (contents, name, version) => contents.replaceAll(VERSION_PLACEHOLDER, version).replaceAll("__DUNX_APP_NAME__", name);
|
|
696
796
|
var generated = (name, features) => {
|
|
697
797
|
const groups = configGroupsFor(features);
|
|
698
|
-
|
|
798
|
+
return {
|
|
699
799
|
"package.json": manifest(features),
|
|
700
800
|
"README.md": readme(name, features),
|
|
701
801
|
".env.example": envExample(groups),
|
|
702
802
|
"src/main.ts": main(name, features),
|
|
703
|
-
"src/bootstrap.ts": bootstrap(name, features),
|
|
704
803
|
"src/app.module.ts": appModule(name, features),
|
|
705
804
|
"src/config.ts": config(name, groups)
|
|
706
805
|
};
|
|
707
|
-
if (features.some((feature) => feature.name === "jobs")) {
|
|
708
|
-
files["src/worker.ts"] = worker(name);
|
|
709
|
-
}
|
|
710
|
-
return files;
|
|
711
806
|
};
|
|
712
807
|
var scaffold = async (options) => {
|
|
713
808
|
const template = options.template ?? "minimal";
|
|
@@ -727,15 +822,15 @@ var scaffold = async (options) => {
|
|
|
727
822
|
if (!isValidPackageName(name)) {
|
|
728
823
|
throw new ScaffoldError(`"${name}" is not a usable package name. Pass --name to choose one.`);
|
|
729
824
|
}
|
|
730
|
-
if (
|
|
731
|
-
const blocking =
|
|
825
|
+
if (options.force !== true) {
|
|
826
|
+
const blocking = blockingEntries(directory);
|
|
732
827
|
if (blocking.length > 0) {
|
|
733
|
-
const shown = blocking.
|
|
828
|
+
const shown = blocking.slice(0, 3).join(", ");
|
|
734
829
|
const rest = blocking.length > 3 ? `, +${blocking.length - 3} more` : "";
|
|
735
830
|
throw new ScaffoldError(`${directory} is not empty (${shown}${rest}). ` + `Pass --force to write into it anyway.`);
|
|
736
831
|
}
|
|
737
832
|
}
|
|
738
|
-
const version = options.version ?? `^${await
|
|
833
|
+
const version = options.version ?? `^${await packageVersion()}`;
|
|
739
834
|
const written = [];
|
|
740
835
|
const copyTree = async (from, into) => {
|
|
741
836
|
for await (const relative of new Glob("**/*").scan({
|
|
@@ -751,12 +846,19 @@ var scaffold = async (options) => {
|
|
|
751
846
|
written.push(target);
|
|
752
847
|
}
|
|
753
848
|
};
|
|
849
|
+
const writeAll = async (files) => {
|
|
850
|
+
for (const [target, contents] of Object.entries(files)) {
|
|
851
|
+
await Bun.write(join(directory, target), fill(contents, name, version));
|
|
852
|
+
written.push(target);
|
|
853
|
+
}
|
|
854
|
+
};
|
|
754
855
|
if (!composing) {
|
|
755
856
|
const source = join(templatesRoot(), template);
|
|
756
857
|
if (!existsSync(source)) {
|
|
757
858
|
throw new ScaffoldError(`Template "${template}" is missing from ${source}.`);
|
|
758
859
|
}
|
|
759
860
|
await copyTree(source, ".");
|
|
861
|
+
await writeAll(agentFiles(name, []));
|
|
760
862
|
return {
|
|
761
863
|
directory,
|
|
762
864
|
name,
|
|
@@ -777,10 +879,10 @@ var scaffold = async (options) => {
|
|
|
777
879
|
}
|
|
778
880
|
await copyTree(from, join("src", feature.source));
|
|
779
881
|
}
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
}
|
|
882
|
+
await writeAll({
|
|
883
|
+
...generated(name, features),
|
|
884
|
+
...agentFiles(name, features)
|
|
885
|
+
});
|
|
784
886
|
return {
|
|
785
887
|
directory,
|
|
786
888
|
name,
|
|
@@ -790,4 +892,4 @@ var scaffold = async (options) => {
|
|
|
790
892
|
};
|
|
791
893
|
};
|
|
792
894
|
|
|
793
|
-
export { FEATURES, featureNames, impliedBy, TEMPLATES, VERSION_PLACEHOLDER, ScaffoldError, scaffold };
|
|
895
|
+
export { FEATURES, featureNames, resolveFeatures, impliedBy, TEMPLATES, VERSION_PLACEHOLDER, blockingEntries, ScaffoldError, isValidPackageName, packageVersion, scaffold };
|