create-basalt 1.4.1 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -3
- package/dist/args.d.ts +21 -0
- package/dist/args.js +76 -0
- package/dist/cli.js +15 -69
- package/dist/index.d.ts +13 -0
- package/dist/index.js +13 -2
- package/dist/latest-versions.d.ts +82 -0
- package/dist/latest-versions.js +209 -0
- package/dist/templates-ui.d.ts +1 -1
- package/dist/templates-ui.js +70 -62
- package/dist/templates.d.ts +32 -1
- package/dist/templates.js +134 -46
- package/dist/versions.d.ts +1 -0
- package/dist/versions.js +24 -0
- package/package.json +25 -19
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ yarn create basalt my-app
|
|
|
30
30
|
bun create basalt my-app
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
> Requirements: Node.js
|
|
33
|
+
> Requirements: Node.js 22.5+ and a package manager. Projects with `--ui` require **pnpm** (explained below).
|
|
34
34
|
|
|
35
35
|
## Get started in 5 minutes
|
|
36
36
|
|
|
@@ -93,6 +93,7 @@ Usage: npm create basalt <name> [options]
|
|
|
93
93
|
| `--mcp` | off | Exposes read-only routes as MCP tools (`@basaltkit/mcp`) over HTTP at `POST /mcp` — the overview and health endpoints are opted in via `meta.mcp` |
|
|
94
94
|
| `--install` | off | Installs dependencies at the end (with the detected/chosen manager) |
|
|
95
95
|
| `--git` | off | Runs `git init` + first commit ("Initial commit from create-basalt") |
|
|
96
|
+
| `--offline` | off | Don't query the npm registry for the latest versions; use the ranges bundled with this create-basalt release |
|
|
96
97
|
| `--pm=<manager>` | autodetect | Package manager: `pnpm` \| `npm` \| `yarn` \| `bun` |
|
|
97
98
|
| `-y`, `--yes` | — | Skips the questions and accepts the defaults |
|
|
98
99
|
| `-h`, `--help` | — | Shows help and exits |
|
|
@@ -102,6 +103,7 @@ Behavior notes (faithful to the code):
|
|
|
102
103
|
- **Package manager detection**: by default, detects who invoked the command via the `npm_config_user_agent` variable (set by npm/pnpm/yarn/bun); unknown managers fall back to `npm`. `--pm=` overrides this.
|
|
103
104
|
- **`--ui` forces pnpm**: the `web/` frontend is a member of a pnpm *workspace* (declared in the generated `pnpm-workspace.yaml`). npm, yarn, and bun can't install or run that structure, so if you request `--ui` with another manager, you'll see `Note: --ui projects are pnpm workspaces — using pnpm instead of <manager>.` and pnpm is used.
|
|
104
105
|
- **Interactive wizard**: when you don't pass a name, you're in a terminal (TTY), and you didn't use `--yes`, you get the guided wizard — an intro, a **starting-point preset** (SaaS starter / API only / Full stack / Minimal / Custom), an arrow-key **feature multiselect** on the custom path, package-manager select, and a **summary + confirm** step before anything is written. Ctrl+C (or declining the final confirm) ends cleanly with "Cancelled." (exit code 130).
|
|
106
|
+
- **Latest dependency versions**: before writing files, the CLI asks the npm registry (`npm_config_registry` when set by your package manager, else `registry.npmjs.org`) for the `latest` version of every dependency the project will contain and writes `^<latest>`. `@basaltkit/*` packages always take the latest release. Third-party packages (TypeScript, Vitest, React, Vite, Tailwind, …) take the latest only on the major the templates are written for — a newer major keeps the bundled range and prints a `Note:`. If the registry can't be reached (offline, timeout), the bundled ranges are used with one `Warning:` line; the scaffold never fails because of the registry. `--offline` skips the lookup entirely.
|
|
105
107
|
- **Occupied folder**: if the destination folder exists and isn't empty, the command refuses with `Target directory "<dir>" already exists and is not empty.` and exits with code 1.
|
|
106
108
|
- At the end, it prints the created files and the "Next steps" appropriate to your choices.
|
|
107
109
|
|
|
@@ -144,7 +146,7 @@ my-app/
|
|
|
144
146
|
└── tests/app.test.ts # smoke test adapted to the options
|
|
145
147
|
```
|
|
146
148
|
|
|
147
|
-
With `--cli`, adds `bin/basalt.ts` and the `"basalt": "tsx bin/basalt.ts"` script. With `--ui`, adds the `web/` folder (Vite + React + Tailwind + shadcn
|
|
149
|
+
With `--cli`, adds `bin/basalt.ts` and the `"basalt": "tsx bin/basalt.ts"` script. With `--ui`, adds the `web/` folder (Vite 8 + React 19 + Tailwind CSS 4 via `@tailwindcss/vite` + shadcn — Tailwind is configured in `web/src/index.css`, no `tailwind.config.js`/PostCSS — with `web/src/api.ts` built on top of `@basaltkit/sdk`; with auth on it includes a login/register screen).
|
|
148
150
|
|
|
149
151
|
### With `--ui`: running the API and frontend
|
|
150
152
|
|
|
@@ -185,7 +187,7 @@ console.log(result.files) // relative paths, sorted
|
|
|
185
187
|
console.log(detectPackageManager()) // 'pnpm' | 'npm' | 'yarn' | 'bun'
|
|
186
188
|
```
|
|
187
189
|
|
|
188
|
-
Note: `createProject` **only writes files** — it doesn't install dependencies or initialize git (that's the executable's job, with `--install`/`--git`).
|
|
190
|
+
Note: `createProject` **only writes files** — it doesn't install dependencies or initialize git (that's the executable's job, with `--install`/`--git`). It also uses the bundled dependency ranges unless you pass `resolveLatest: true` (what the executable does), so a script never touches the network by surprise.
|
|
189
191
|
|
|
190
192
|
## API reference
|
|
191
193
|
|
|
@@ -205,6 +207,8 @@ Exported from `create-basalt` (in addition to the `create-basalt` executable):
|
|
|
205
207
|
| `ui` | `boolean` | No | `false` | Generate the `web/` frontend |
|
|
206
208
|
| `cli` | `boolean` | No | `false` | Generate the `basalt` CLI |
|
|
207
209
|
| `mcp` | `boolean` | No | `false` | Expose read-only routes as MCP tools at `/mcp` |
|
|
210
|
+
| `resolveLatest` | `boolean` | No | `false` | Resolve every dependency to `^<latest>` from the npm registry before writing (falls back to the bundled ranges on any registry failure) |
|
|
211
|
+
| `registry` | `ResolveLatestOptions` | No | — | `{ fetch?, registry?, timeoutMs?, overallTimeoutMs?, concurrency? }` — injectable fetch (tests), registry URL (default `npm_config_registry` or `https://registry.npmjs.org`), timeouts (5 s per request, 15 s overall) |
|
|
208
212
|
|
|
209
213
|
`CreateProjectResult`:
|
|
210
214
|
|
|
@@ -213,6 +217,7 @@ Exported from `create-basalt` (in addition to the `create-basalt` executable):
|
|
|
213
217
|
| `dir` | `string` | Absolute path of the created folder |
|
|
214
218
|
| `files` | `string[]` | Files written (relative, sorted) |
|
|
215
219
|
| `options` | `ProjectOptions` | The options actually applied (with defaults resolved) |
|
|
220
|
+
| `versions` | `VersionResolution \| undefined` | With `resolveLatest`: `{ versions, resolved, failed, heldBack, registry }` — the final ranges, which packages got `^<latest>`, which kept the fallback after a registry failure, and third-party packages held back because their latest is a new major |
|
|
216
221
|
|
|
217
222
|
Throws `TargetNotEmptyError` if the destination folder exists and isn't empty.
|
|
218
223
|
|
package/dist/args.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { PackageManager } from './index.js';
|
|
2
|
+
export declare const USAGE = "Usage: npm create basalt <name> [options]\n\nOptions:\n --dir=<path> Target directory (default: ./<name>)\n --no-tenancy Skip multi-tenancy\n --no-auth Skip authentication\n --billing Include subscriptions/billing\n --ui Scaffold a web/ frontend (React + shadcn + SDK)\n --cli Scaffold the 'basalt' CLI (code generators + commands)\n --mcp Expose read-only routes as MCP tools at /mcp\n --install Install dependencies (default: yes in an interactive\n terminal, skipped in CI/non-TTY; --no-install to opt out)\n --no-install Never install dependencies\n --git Initialize a git repository with a first commit\n (same default rule as --install; --no-git to opt out)\n --no-git Never initialize a git repository\n --offline Don't query the npm registry for the latest versions;\n use the ranges bundled with this create-basalt release\n --pm=<manager> Package manager: pnpm | npm | yarn | bun (default: auto-detect)\n -y, --yes Skip prompts and accept defaults\n -h, --help Show this help\n\nRun with no name in a terminal to be prompted interactively.\n";
|
|
3
|
+
export interface Flags {
|
|
4
|
+
name?: string;
|
|
5
|
+
dir?: string;
|
|
6
|
+
tenancy: boolean;
|
|
7
|
+
auth: boolean;
|
|
8
|
+
billing: boolean;
|
|
9
|
+
ui: boolean;
|
|
10
|
+
cli: boolean;
|
|
11
|
+
mcp: boolean;
|
|
12
|
+
/** Tri-state: undefined = decide from the environment (TTY yes, CI no). */
|
|
13
|
+
install?: boolean;
|
|
14
|
+
git?: boolean;
|
|
15
|
+
yes: boolean;
|
|
16
|
+
offline: boolean;
|
|
17
|
+
pm?: PackageManager;
|
|
18
|
+
}
|
|
19
|
+
export declare function parseArgs(argv: string[]): Flags;
|
|
20
|
+
/** Whether the scaffold should query the registry for the latest versions. */
|
|
21
|
+
export declare const resolvesLatest: (flags: Pick<Flags, 'offline'>) => boolean;
|
package/dist/args.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { stdout } from 'node:process';
|
|
2
|
+
export const USAGE = `Usage: npm create basalt <name> [options]
|
|
3
|
+
|
|
4
|
+
Options:
|
|
5
|
+
--dir=<path> Target directory (default: ./<name>)
|
|
6
|
+
--no-tenancy Skip multi-tenancy
|
|
7
|
+
--no-auth Skip authentication
|
|
8
|
+
--billing Include subscriptions/billing
|
|
9
|
+
--ui Scaffold a web/ frontend (React + shadcn + SDK)
|
|
10
|
+
--cli Scaffold the 'basalt' CLI (code generators + commands)
|
|
11
|
+
--mcp Expose read-only routes as MCP tools at /mcp
|
|
12
|
+
--install Install dependencies (default: yes in an interactive
|
|
13
|
+
terminal, skipped in CI/non-TTY; --no-install to opt out)
|
|
14
|
+
--no-install Never install dependencies
|
|
15
|
+
--git Initialize a git repository with a first commit
|
|
16
|
+
(same default rule as --install; --no-git to opt out)
|
|
17
|
+
--no-git Never initialize a git repository
|
|
18
|
+
--offline Don't query the npm registry for the latest versions;
|
|
19
|
+
use the ranges bundled with this create-basalt release
|
|
20
|
+
--pm=<manager> Package manager: pnpm | npm | yarn | bun (default: auto-detect)
|
|
21
|
+
-y, --yes Skip prompts and accept defaults
|
|
22
|
+
-h, --help Show this help
|
|
23
|
+
|
|
24
|
+
Run with no name in a terminal to be prompted interactively.
|
|
25
|
+
`;
|
|
26
|
+
export function parseArgs(argv) {
|
|
27
|
+
const flags = {
|
|
28
|
+
tenancy: true,
|
|
29
|
+
auth: true,
|
|
30
|
+
billing: false,
|
|
31
|
+
ui: false,
|
|
32
|
+
cli: false,
|
|
33
|
+
mcp: false,
|
|
34
|
+
yes: false,
|
|
35
|
+
offline: false,
|
|
36
|
+
};
|
|
37
|
+
for (const token of argv) {
|
|
38
|
+
if (token === '--no-tenancy')
|
|
39
|
+
flags.tenancy = false;
|
|
40
|
+
else if (token === '--no-auth')
|
|
41
|
+
flags.auth = false;
|
|
42
|
+
else if (token === '--billing')
|
|
43
|
+
flags.billing = true;
|
|
44
|
+
else if (token === '--ui')
|
|
45
|
+
flags.ui = true;
|
|
46
|
+
else if (token === '--cli')
|
|
47
|
+
flags.cli = true;
|
|
48
|
+
else if (token === '--mcp')
|
|
49
|
+
flags.mcp = true;
|
|
50
|
+
else if (token === '--install')
|
|
51
|
+
flags.install = true;
|
|
52
|
+
else if (token === '--no-install')
|
|
53
|
+
flags.install = false;
|
|
54
|
+
else if (token === '--git')
|
|
55
|
+
flags.git = true;
|
|
56
|
+
else if (token === '--no-git')
|
|
57
|
+
flags.git = false;
|
|
58
|
+
else if (token === '-y' || token === '--yes')
|
|
59
|
+
flags.yes = true;
|
|
60
|
+
else if (token === '--offline')
|
|
61
|
+
flags.offline = true;
|
|
62
|
+
else if (token.startsWith('--dir='))
|
|
63
|
+
flags.dir = token.slice('--dir='.length);
|
|
64
|
+
else if (token.startsWith('--pm='))
|
|
65
|
+
flags.pm = token.slice('--pm='.length);
|
|
66
|
+
else if (token === '--help' || token === '-h') {
|
|
67
|
+
stdout.write(USAGE);
|
|
68
|
+
process.exit(0);
|
|
69
|
+
}
|
|
70
|
+
else if (!token.startsWith('--') && flags.name === undefined)
|
|
71
|
+
flags.name = token;
|
|
72
|
+
}
|
|
73
|
+
return flags;
|
|
74
|
+
}
|
|
75
|
+
/** Whether the scaffold should query the registry for the latest versions. */
|
|
76
|
+
export const resolvesLatest = (flags) => !flags.offline;
|
package/dist/cli.js
CHANGED
|
@@ -1,75 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
3
|
import { stdin, stdout } from 'node:process';
|
|
4
|
-
import { createProject, detectPackageManager, resolveRunDefaults, runWizard, ttyPrompter, TargetNotEmptyError, WizardCancelledError, } from './index.js';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Options:
|
|
8
|
-
--dir=<path> Target directory (default: ./<name>)
|
|
9
|
-
--no-tenancy Skip multi-tenancy
|
|
10
|
-
--no-auth Skip authentication
|
|
11
|
-
--billing Include subscriptions/billing
|
|
12
|
-
--ui Scaffold a web/ frontend (React + shadcn + SDK)
|
|
13
|
-
--cli Scaffold the 'basalt' CLI (code generators + commands)
|
|
14
|
-
--mcp Expose read-only routes as MCP tools at /mcp
|
|
15
|
-
--install Install dependencies (default: yes in an interactive
|
|
16
|
-
terminal, skipped in CI/non-TTY; --no-install to opt out)
|
|
17
|
-
--no-install Never install dependencies
|
|
18
|
-
--git Initialize a git repository with a first commit
|
|
19
|
-
(same default rule as --install; --no-git to opt out)
|
|
20
|
-
--no-git Never initialize a git repository
|
|
21
|
-
--pm=<manager> Package manager: pnpm | npm | yarn | bun (default: auto-detect)
|
|
22
|
-
-y, --yes Skip prompts and accept defaults
|
|
23
|
-
-h, --help Show this help
|
|
24
|
-
|
|
25
|
-
Run with no name in a terminal to be prompted interactively.
|
|
26
|
-
`;
|
|
27
|
-
function parseArgs(argv) {
|
|
28
|
-
const flags = {
|
|
29
|
-
tenancy: true,
|
|
30
|
-
auth: true,
|
|
31
|
-
billing: false,
|
|
32
|
-
ui: false,
|
|
33
|
-
cli: false,
|
|
34
|
-
mcp: false,
|
|
35
|
-
yes: false,
|
|
36
|
-
};
|
|
37
|
-
for (const token of argv) {
|
|
38
|
-
if (token === '--no-tenancy')
|
|
39
|
-
flags.tenancy = false;
|
|
40
|
-
else if (token === '--no-auth')
|
|
41
|
-
flags.auth = false;
|
|
42
|
-
else if (token === '--billing')
|
|
43
|
-
flags.billing = true;
|
|
44
|
-
else if (token === '--ui')
|
|
45
|
-
flags.ui = true;
|
|
46
|
-
else if (token === '--cli')
|
|
47
|
-
flags.cli = true;
|
|
48
|
-
else if (token === '--mcp')
|
|
49
|
-
flags.mcp = true;
|
|
50
|
-
else if (token === '--install')
|
|
51
|
-
flags.install = true;
|
|
52
|
-
else if (token === '--no-install')
|
|
53
|
-
flags.install = false;
|
|
54
|
-
else if (token === '--git')
|
|
55
|
-
flags.git = true;
|
|
56
|
-
else if (token === '--no-git')
|
|
57
|
-
flags.git = false;
|
|
58
|
-
else if (token === '-y' || token === '--yes')
|
|
59
|
-
flags.yes = true;
|
|
60
|
-
else if (token.startsWith('--dir='))
|
|
61
|
-
flags.dir = token.slice('--dir='.length);
|
|
62
|
-
else if (token.startsWith('--pm='))
|
|
63
|
-
flags.pm = token.slice('--pm='.length);
|
|
64
|
-
else if (token === '--help' || token === '-h') {
|
|
65
|
-
stdout.write(USAGE);
|
|
66
|
-
process.exit(0);
|
|
67
|
-
}
|
|
68
|
-
else if (!token.startsWith('--') && flags.name === undefined)
|
|
69
|
-
flags.name = token;
|
|
70
|
-
}
|
|
71
|
-
return flags;
|
|
72
|
-
}
|
|
4
|
+
import { createProject, describeResolution, detectPackageManager, resolveRunDefaults, runWizard, ttyPrompter, TargetNotEmptyError, WizardCancelledError, } from './index.js';
|
|
5
|
+
import { parseArgs, resolvesLatest, USAGE } from './args.js';
|
|
73
6
|
/** Runs a command inheriting stdio; resolves false on non-zero exit (never throws). */
|
|
74
7
|
function run(command, args, cwd) {
|
|
75
8
|
return new Promise((resolve) => {
|
|
@@ -124,6 +57,8 @@ if (flags.ui && pm !== 'pnpm') {
|
|
|
124
57
|
pm = 'pnpm';
|
|
125
58
|
}
|
|
126
59
|
try {
|
|
60
|
+
if (resolvesLatest(flags))
|
|
61
|
+
console.log('Resolving the latest published dependency versions…');
|
|
127
62
|
const result = await createProject({
|
|
128
63
|
name: flags.name,
|
|
129
64
|
...(flags.dir ? { dir: flags.dir } : {}),
|
|
@@ -133,10 +68,21 @@ try {
|
|
|
133
68
|
ui: flags.ui,
|
|
134
69
|
cli: flags.cli,
|
|
135
70
|
mcp: flags.mcp,
|
|
71
|
+
// New apps get the latest published version of every dependency (with the
|
|
72
|
+
// bundled ranges as an offline fallback); --offline skips the registry.
|
|
73
|
+
resolveLatest: resolvesLatest(flags),
|
|
136
74
|
});
|
|
137
75
|
console.log(`\nCreated ${result.options.name} in ${result.dir}\n`);
|
|
138
76
|
for (const file of result.files)
|
|
139
77
|
console.log(` ${file}`);
|
|
78
|
+
if (result.versions) {
|
|
79
|
+
const lines = describeResolution(result.versions);
|
|
80
|
+
if (lines.length > 0)
|
|
81
|
+
console.log(`\n${lines.join('\n')}`);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
console.log('\n--offline: used the dependency ranges bundled with this create-basalt release.');
|
|
85
|
+
}
|
|
140
86
|
const run_ = resolveRunDefaults({
|
|
141
87
|
install: flags.install,
|
|
142
88
|
git: flags.git,
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ProjectOptions } from './templates.js';
|
|
2
|
+
import { type ResolveLatestOptions, type VersionResolution } from './latest-versions.js';
|
|
2
3
|
export type { ProjectOptions } from './templates.js';
|
|
3
4
|
export interface CreateProjectInput {
|
|
4
5
|
name: string;
|
|
@@ -13,11 +14,22 @@ export interface CreateProjectInput {
|
|
|
13
14
|
cli?: boolean;
|
|
14
15
|
/** Expose opted-in routes as MCP tools over HTTP at `/mcp`. Default: false. */
|
|
15
16
|
mcp?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Resolve every dependency to its latest published version (npm registry)
|
|
19
|
+
* before writing files. Default: false — the embedded fallback ranges are
|
|
20
|
+
* used and no network is touched. The `create-basalt` CLI turns it on
|
|
21
|
+
* (unless `--offline`). Registry failures never fail the scaffold.
|
|
22
|
+
*/
|
|
23
|
+
resolveLatest?: boolean;
|
|
24
|
+
/** Registry options for `resolveLatest` (injectable fetch, registry, timeouts). */
|
|
25
|
+
registry?: ResolveLatestOptions;
|
|
16
26
|
}
|
|
17
27
|
export interface CreateProjectResult {
|
|
18
28
|
dir: string;
|
|
19
29
|
files: string[];
|
|
20
30
|
options: ProjectOptions;
|
|
31
|
+
/** How dependency versions were resolved — present only with `resolveLatest`. */
|
|
32
|
+
versions?: VersionResolution;
|
|
21
33
|
}
|
|
22
34
|
export declare class TargetNotEmptyError extends Error {
|
|
23
35
|
constructor(dir: string);
|
|
@@ -48,5 +60,6 @@ export declare function resolveRunDefaults(input: {
|
|
|
48
60
|
};
|
|
49
61
|
/** Generates a ready-to-run Basalt app. Does not install dependencies. */
|
|
50
62
|
export declare function createProject(input: CreateProjectInput): Promise<CreateProjectResult>;
|
|
63
|
+
export { THIRD_PARTY_VERSIONS, DEFAULT_REGISTRY, describeResolution, resolveLatestVersions, registryUrl, type HeldBackVersion, type ResolveLatestOptions, type VersionResolution, } from './latest-versions.js';
|
|
51
64
|
export { runWizard, validateProjectName, PRESETS, FEATURES, type WizardResult, type WizardOptions, type FeatureKey } from './wizard.js';
|
|
52
65
|
export { ttyPrompter, scriptedPrompter, WizardCancelledError, type Prompter, type Choice } from './prompt.js';
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { mkdir, readdir, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { dirname, join, resolve } from 'node:path';
|
|
3
|
-
import { appTest, appTs, envExample, envTs, gitignore, mcpJson, basaltBin, packageJson, pnpmWorkspaceYaml, readme, routesTs, serverTs, tsconfigJson, } from './templates.js';
|
|
3
|
+
import { appTest, appTs, devTs, dockerignore, envExample, envTs, gitignore, mcpJson, basaltBin, packageJson, pnpmWorkspaceYaml, readme, routesTs, serverTs, tsconfigJson, } from './templates.js';
|
|
4
4
|
import { uiFiles } from './templates-ui.js';
|
|
5
|
+
import { applyVersions, collectDependencies, resolveLatestVersions, } from './latest-versions.js';
|
|
5
6
|
export class TargetNotEmptyError extends Error {
|
|
6
7
|
constructor(dir) {
|
|
7
8
|
super(`Target directory "${dir}" already exists and is not empty.`);
|
|
@@ -54,23 +55,33 @@ export async function createProject(input) {
|
|
|
54
55
|
'tsconfig.json': tsconfigJson(),
|
|
55
56
|
'.env.example': envExample(options),
|
|
56
57
|
'.gitignore': gitignore(),
|
|
58
|
+
'.dockerignore': dockerignore(),
|
|
57
59
|
'README.md': readme(options),
|
|
58
60
|
'pnpm-workspace.yaml': pnpmWorkspaceYaml(options),
|
|
59
61
|
'src/env.ts': envTs(options),
|
|
60
62
|
'src/app.ts': appTs(options),
|
|
61
63
|
'src/routes.ts': routesTs(options),
|
|
62
64
|
'src/server.ts': serverTs(),
|
|
65
|
+
'src/dev.ts': devTs(),
|
|
63
66
|
'tests/app.test.ts': appTest(options),
|
|
64
67
|
...(options.cli ? { 'bin/basalt.ts': basaltBin() } : {}),
|
|
65
68
|
...(options.mcp ? { '.mcp.json': mcpJson(options) } : {}),
|
|
66
69
|
...(options.ui ? uiFiles(options) : {}),
|
|
67
70
|
};
|
|
71
|
+
let versions;
|
|
72
|
+
if (input.resolveLatest) {
|
|
73
|
+
const manifests = Object.keys(files).filter((path) => path === 'package.json' || path.endsWith('/package.json'));
|
|
74
|
+
versions = await resolveLatestVersions(collectDependencies(manifests.map((path) => files[path])), input.registry);
|
|
75
|
+
for (const path of manifests)
|
|
76
|
+
files[path] = applyVersions(files[path], versions.versions);
|
|
77
|
+
}
|
|
68
78
|
for (const [path, content] of Object.entries(files)) {
|
|
69
79
|
const target = join(dir, path);
|
|
70
80
|
await mkdir(dirname(target), { recursive: true });
|
|
71
81
|
await writeFile(target, content);
|
|
72
82
|
}
|
|
73
|
-
return { dir, files: Object.keys(files).sort(), options };
|
|
83
|
+
return { dir, files: Object.keys(files).sort(), options, ...(versions ? { versions } : {}) };
|
|
74
84
|
}
|
|
85
|
+
export { THIRD_PARTY_VERSIONS, DEFAULT_REGISTRY, describeResolution, resolveLatestVersions, registryUrl, } from './latest-versions.js';
|
|
75
86
|
export { runWizard, validateProjectName, PRESETS, FEATURES } from './wizard.js';
|
|
76
87
|
export { ttyPrompter, scriptedPrompter, WizardCancelledError } from './prompt.js';
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scaffold-time dependency resolution: a freshly generated app should depend on
|
|
3
|
+
* the LATEST PUBLISHED version of every package, not on the ranges that were
|
|
4
|
+
* current the day this create-basalt release was built.
|
|
5
|
+
*
|
|
6
|
+
* - `@basaltkit/*` packages always take the registry's `latest` dist-tag — the
|
|
7
|
+
* framework's own release line is the whole point of the scaffold.
|
|
8
|
+
* - Third-party packages take `latest` only while it stays on the major the
|
|
9
|
+
* templates are written for (the major of the fallback range in
|
|
10
|
+
* {@link THIRD_PARTY_VERSIONS}). A new breaking major keeps the fallback range
|
|
11
|
+
* and is reported as a notice, so it can never silently produce a broken app.
|
|
12
|
+
* - Any registry failure (offline, timeout, non-2xx, malformed JSON, a bogus
|
|
13
|
+
* version string) keeps the embedded fallback range. The registry never fails
|
|
14
|
+
* a scaffold.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Fallback ranges for every third-party package the templates emit (API +
|
|
18
|
+
* web UI). The major of each range is also the TEMPLATE-COMPATIBLE major: the
|
|
19
|
+
* generated code is written for it, and `latest` is accepted only on that
|
|
20
|
+
* major. Bump a range to a new major only together with the templates.
|
|
21
|
+
*/
|
|
22
|
+
export declare const THIRD_PARTY_VERSIONS: Readonly<Record<string, string>>;
|
|
23
|
+
export declare const DEFAULT_REGISTRY = "https://registry.npmjs.org";
|
|
24
|
+
/** Packages whose latest is always taken, regardless of major. */
|
|
25
|
+
export declare const alwaysLatest: (name: string) => boolean;
|
|
26
|
+
/** The major the templates are written for, derived from the fallback range. */
|
|
27
|
+
export declare function compatibleMajor(fallbackRange: string): number | undefined;
|
|
28
|
+
export interface HeldBackVersion {
|
|
29
|
+
name: string;
|
|
30
|
+
/** The registry's latest version (a new major the templates don't target). */
|
|
31
|
+
latest: string;
|
|
32
|
+
/** The range kept instead. */
|
|
33
|
+
range: string;
|
|
34
|
+
}
|
|
35
|
+
export interface VersionResolution {
|
|
36
|
+
/** Final range per package name (resolved or fallback). */
|
|
37
|
+
versions: Record<string, string>;
|
|
38
|
+
/** Packages set to `^<latest>` from the registry. */
|
|
39
|
+
resolved: string[];
|
|
40
|
+
/** Packages that kept their fallback range because the registry failed. */
|
|
41
|
+
failed: string[];
|
|
42
|
+
/** Third-party packages whose latest is a new major — fallback kept. */
|
|
43
|
+
heldBack: HeldBackVersion[];
|
|
44
|
+
/** Registry actually queried. */
|
|
45
|
+
registry: string;
|
|
46
|
+
}
|
|
47
|
+
export interface ResolveLatestOptions {
|
|
48
|
+
/** Injectable fetch (tests). Default: the global fetch. */
|
|
49
|
+
fetch?: typeof globalThis.fetch;
|
|
50
|
+
/** Registry base URL. Default: `npm_config_registry`, else the public npm registry. */
|
|
51
|
+
registry?: string;
|
|
52
|
+
/** Per-request timeout in ms. Default 5000. */
|
|
53
|
+
timeoutMs?: number;
|
|
54
|
+
/** Cap on the whole resolution in ms. Default 15000. */
|
|
55
|
+
overallTimeoutMs?: number;
|
|
56
|
+
/** Requests in flight at once. Default 8. */
|
|
57
|
+
concurrency?: number;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The registry to query: an explicit option, else `npm_config_registry` (npm
|
|
61
|
+
* and pnpm export it to `create` binaries, so a private mirror is honored),
|
|
62
|
+
* else the public registry. Trailing slashes are normalized; anything that is
|
|
63
|
+
* not an http(s) URL falls back to the default.
|
|
64
|
+
*/
|
|
65
|
+
export declare function registryUrl(explicit?: string, env?: NodeJS.ProcessEnv): string;
|
|
66
|
+
/** `<registry>/<name>/latest`, with a scoped name's `/` encoded as `%2f`. */
|
|
67
|
+
export declare const latestUrl: (registry: string, name: string) => string;
|
|
68
|
+
/**
|
|
69
|
+
* Resolves the latest published version of every package in `fallbacks`
|
|
70
|
+
* (name → fallback range) through a small pool of parallel requests. Never throws: a package the registry
|
|
71
|
+
* cannot answer for keeps its fallback range and is listed in `failed`.
|
|
72
|
+
*/
|
|
73
|
+
export declare function resolveLatestVersions(fallbacks: Readonly<Record<string, string>>, options?: ResolveLatestOptions): Promise<VersionResolution>;
|
|
74
|
+
/**
|
|
75
|
+
* Rewrites the dependency ranges of a generated package.json with the
|
|
76
|
+
* resolved versions (keys, order and every other field untouched).
|
|
77
|
+
*/
|
|
78
|
+
export declare function applyVersions(packageJsonText: string, versions: Readonly<Record<string, string>>): string;
|
|
79
|
+
/** Every dependency/devDependency name → range declared in the given package.json texts. */
|
|
80
|
+
export declare function collectDependencies(packageJsonTexts: readonly string[]): Record<string, string>;
|
|
81
|
+
/** Human-readable lines summarizing a resolution (for the CLI). */
|
|
82
|
+
export declare function describeResolution(resolution: VersionResolution): string[];
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scaffold-time dependency resolution: a freshly generated app should depend on
|
|
3
|
+
* the LATEST PUBLISHED version of every package, not on the ranges that were
|
|
4
|
+
* current the day this create-basalt release was built.
|
|
5
|
+
*
|
|
6
|
+
* - `@basaltkit/*` packages always take the registry's `latest` dist-tag — the
|
|
7
|
+
* framework's own release line is the whole point of the scaffold.
|
|
8
|
+
* - Third-party packages take `latest` only while it stays on the major the
|
|
9
|
+
* templates are written for (the major of the fallback range in
|
|
10
|
+
* {@link THIRD_PARTY_VERSIONS}). A new breaking major keeps the fallback range
|
|
11
|
+
* and is reported as a notice, so it can never silently produce a broken app.
|
|
12
|
+
* - Any registry failure (offline, timeout, non-2xx, malformed JSON, a bogus
|
|
13
|
+
* version string) keeps the embedded fallback range. The registry never fails
|
|
14
|
+
* a scaffold.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Fallback ranges for every third-party package the templates emit (API +
|
|
18
|
+
* web UI). The major of each range is also the TEMPLATE-COMPATIBLE major: the
|
|
19
|
+
* generated code is written for it, and `latest` is accepted only on that
|
|
20
|
+
* major. Bump a range to a new major only together with the templates.
|
|
21
|
+
*/
|
|
22
|
+
export const THIRD_PARTY_VERSIONS = {
|
|
23
|
+
'@tailwindcss/vite': '^4.3.3',
|
|
24
|
+
'@types/node': '^26.6.2',
|
|
25
|
+
'@types/react': '^19.3.0',
|
|
26
|
+
'@types/react-dom': '^19.3.0',
|
|
27
|
+
'@vitejs/plugin-react': '^6.1.1',
|
|
28
|
+
'pino-pretty': '^13.1.3',
|
|
29
|
+
react: '^19.3.0',
|
|
30
|
+
'react-dom': '^19.3.0',
|
|
31
|
+
tailwindcss: '^4.3.3',
|
|
32
|
+
tsx: '^4.23.13',
|
|
33
|
+
typescript: '^7.0.2',
|
|
34
|
+
vite: '^8.3.0',
|
|
35
|
+
vitest: '^5.0.1',
|
|
36
|
+
zod: '^4.6.5',
|
|
37
|
+
};
|
|
38
|
+
export const DEFAULT_REGISTRY = 'https://registry.npmjs.org';
|
|
39
|
+
const PER_REQUEST_TIMEOUT_MS = 5_000;
|
|
40
|
+
const OVERALL_TIMEOUT_MS = 15_000;
|
|
41
|
+
/**
|
|
42
|
+
* Requests in flight at once. A burst of ~35 simultaneous TLS handshakes to one
|
|
43
|
+
* host is markedly slower (and flakier) on poor links than a small pool that
|
|
44
|
+
* reuses keep-alive connections.
|
|
45
|
+
*/
|
|
46
|
+
const CONCURRENCY = 8;
|
|
47
|
+
/** A stable `x.y.z` release (pre-releases and build metadata are refused). */
|
|
48
|
+
const STABLE_SEMVER = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
49
|
+
const CARET_RANGE = /^\^(0|[1-9]\d*)\.\d+\.\d+$/;
|
|
50
|
+
/** Packages whose latest is always taken, regardless of major. */
|
|
51
|
+
export const alwaysLatest = (name) => name.startsWith('@basaltkit/') || name === 'create-basalt';
|
|
52
|
+
/** The major the templates are written for, derived from the fallback range. */
|
|
53
|
+
export function compatibleMajor(fallbackRange) {
|
|
54
|
+
const match = CARET_RANGE.exec(fallbackRange);
|
|
55
|
+
return match ? Number(match[1]) : undefined;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The registry to query: an explicit option, else `npm_config_registry` (npm
|
|
59
|
+
* and pnpm export it to `create` binaries, so a private mirror is honored),
|
|
60
|
+
* else the public registry. Trailing slashes are normalized; anything that is
|
|
61
|
+
* not an http(s) URL falls back to the default.
|
|
62
|
+
*/
|
|
63
|
+
export function registryUrl(explicit, env = process.env) {
|
|
64
|
+
const candidate = explicit ?? env['npm_config_registry'] ?? env['NPM_CONFIG_REGISTRY'];
|
|
65
|
+
if (!candidate)
|
|
66
|
+
return DEFAULT_REGISTRY;
|
|
67
|
+
try {
|
|
68
|
+
const url = new URL(candidate);
|
|
69
|
+
if (url.protocol !== 'https:' && url.protocol !== 'http:')
|
|
70
|
+
return DEFAULT_REGISTRY;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return DEFAULT_REGISTRY;
|
|
74
|
+
}
|
|
75
|
+
let end = candidate.length;
|
|
76
|
+
while (end > 0 && candidate[end - 1] === '/')
|
|
77
|
+
end--;
|
|
78
|
+
return candidate.slice(0, end);
|
|
79
|
+
}
|
|
80
|
+
/** `<registry>/<name>/latest`, with a scoped name's `/` encoded as `%2f`. */
|
|
81
|
+
export const latestUrl = (registry, name) => `${registry}/${name.replaceAll('/', '%2f')}/latest`;
|
|
82
|
+
async function lookupLatest(fetchImpl, registry, name, signal) {
|
|
83
|
+
try {
|
|
84
|
+
const response = await fetchImpl(latestUrl(registry, name), {
|
|
85
|
+
headers: { accept: 'application/json' },
|
|
86
|
+
signal,
|
|
87
|
+
});
|
|
88
|
+
if (response.status >= 500 || response.status === 429)
|
|
89
|
+
return 'transient';
|
|
90
|
+
if (!response.ok)
|
|
91
|
+
return 'miss';
|
|
92
|
+
const body = (await response.json());
|
|
93
|
+
const version = body?.version;
|
|
94
|
+
return typeof version === 'string' && STABLE_SEMVER.test(version) ? { version } : 'miss';
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// Network error, per-request timeout, or malformed JSON.
|
|
98
|
+
return 'transient';
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The latest stable version of `name`, or undefined. A transient failure
|
|
103
|
+
* (network error, timeout, 5xx/429) is retried once — on slow links the first
|
|
104
|
+
* wave of cold TLS handshakes is the part that times out, and the retry rides
|
|
105
|
+
* a warm keep-alive connection. The overall signal bounds both attempts.
|
|
106
|
+
*/
|
|
107
|
+
async function fetchLatest(fetchImpl, registry, name, timeoutMs, overall) {
|
|
108
|
+
for (let attempt = 0; attempt < 2 && !overall.aborted; attempt++) {
|
|
109
|
+
const result = await lookupLatest(fetchImpl, registry, name, AbortSignal.any([AbortSignal.timeout(timeoutMs), overall]));
|
|
110
|
+
if (result === 'miss')
|
|
111
|
+
return undefined;
|
|
112
|
+
if (result !== 'transient')
|
|
113
|
+
return result.version;
|
|
114
|
+
}
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Resolves the latest published version of every package in `fallbacks`
|
|
119
|
+
* (name → fallback range) through a small pool of parallel requests. Never throws: a package the registry
|
|
120
|
+
* cannot answer for keeps its fallback range and is listed in `failed`.
|
|
121
|
+
*/
|
|
122
|
+
export async function resolveLatestVersions(fallbacks, options = {}) {
|
|
123
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
124
|
+
const registry = registryUrl(options.registry);
|
|
125
|
+
const versions = { ...fallbacks };
|
|
126
|
+
const resolved = [];
|
|
127
|
+
const failed = [];
|
|
128
|
+
const heldBack = [];
|
|
129
|
+
const names = Object.keys(fallbacks).sort();
|
|
130
|
+
if (typeof fetchImpl !== 'function') {
|
|
131
|
+
return { versions, resolved, failed: names, heldBack, registry };
|
|
132
|
+
}
|
|
133
|
+
const overall = new AbortController();
|
|
134
|
+
const cap = setTimeout(() => overall.abort(), options.overallTimeoutMs ?? OVERALL_TIMEOUT_MS);
|
|
135
|
+
try {
|
|
136
|
+
const latest = [];
|
|
137
|
+
let next = 0;
|
|
138
|
+
const worker = async () => {
|
|
139
|
+
while (next < names.length) {
|
|
140
|
+
const i = next++;
|
|
141
|
+
latest[i] = await fetchLatest(fetchImpl, registry, names[i], options.timeoutMs ?? PER_REQUEST_TIMEOUT_MS, overall.signal);
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
const pool = Math.max(1, Math.min(options.concurrency ?? CONCURRENCY, names.length));
|
|
145
|
+
await Promise.all(Array.from({ length: pool }, worker));
|
|
146
|
+
names.forEach((name, i) => {
|
|
147
|
+
const version = latest[i];
|
|
148
|
+
const fallback = fallbacks[name];
|
|
149
|
+
if (version === undefined) {
|
|
150
|
+
failed.push(name);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (!alwaysLatest(name)) {
|
|
154
|
+
const major = compatibleMajor(fallback);
|
|
155
|
+
if (major === undefined || Number(version.split('.')[0]) !== major) {
|
|
156
|
+
heldBack.push({ name, latest: version, range: fallback });
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
versions[name] = `^${version}`;
|
|
161
|
+
resolved.push(name);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
finally {
|
|
165
|
+
clearTimeout(cap);
|
|
166
|
+
}
|
|
167
|
+
return { versions, resolved, failed, heldBack, registry };
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Rewrites the dependency ranges of a generated package.json with the
|
|
171
|
+
* resolved versions (keys, order and every other field untouched).
|
|
172
|
+
*/
|
|
173
|
+
export function applyVersions(packageJsonText, versions) {
|
|
174
|
+
const pkg = JSON.parse(packageJsonText);
|
|
175
|
+
for (const field of ['dependencies', 'devDependencies']) {
|
|
176
|
+
const deps = pkg[field];
|
|
177
|
+
if (!deps)
|
|
178
|
+
continue;
|
|
179
|
+
for (const name of Object.keys(deps)) {
|
|
180
|
+
const range = versions[name];
|
|
181
|
+
if (range !== undefined)
|
|
182
|
+
deps[name] = range;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return `${JSON.stringify(pkg, null, 2)}\n`;
|
|
186
|
+
}
|
|
187
|
+
/** Every dependency/devDependency name → range declared in the given package.json texts. */
|
|
188
|
+
export function collectDependencies(packageJsonTexts) {
|
|
189
|
+
const all = {};
|
|
190
|
+
for (const text of packageJsonTexts) {
|
|
191
|
+
const pkg = JSON.parse(text);
|
|
192
|
+
Object.assign(all, pkg.dependencies, pkg.devDependencies);
|
|
193
|
+
}
|
|
194
|
+
return all;
|
|
195
|
+
}
|
|
196
|
+
/** Human-readable lines summarizing a resolution (for the CLI). */
|
|
197
|
+
export function describeResolution(resolution) {
|
|
198
|
+
const lines = [];
|
|
199
|
+
if (resolution.resolved.length > 0) {
|
|
200
|
+
lines.push(`Resolved ${resolution.resolved.length} dependencies to their latest published versions:`, ` ${resolution.resolved.map((name) => `${name}@${resolution.versions[name]}`).join(', ')}`);
|
|
201
|
+
}
|
|
202
|
+
for (const held of resolution.heldBack) {
|
|
203
|
+
lines.push(`Note: ${held.name} ${held.latest} is a new major this template does not target yet — kept ${held.range}.`);
|
|
204
|
+
}
|
|
205
|
+
if (resolution.failed.length > 0) {
|
|
206
|
+
lines.push(`Warning: could not reach ${resolution.registry} for ${resolution.failed.length} package(s) — used the bundled fallback ranges for: ${resolution.failed.join(', ')}.`);
|
|
207
|
+
}
|
|
208
|
+
return lines;
|
|
209
|
+
}
|
package/dist/templates-ui.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ProjectOptions } from './templates.js';
|
|
2
2
|
/**
|
|
3
3
|
* The `web/` frontend emitted when `--ui` is passed: a Vite + React app on
|
|
4
4
|
* @basaltkit/admin-shadcn (authentic shadcn/ui components) talking to the API
|
package/dist/templates-ui.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
const BASALT_VERSION = '^1.0.0';
|
|
1
|
+
import { thirdPartyVersionOf, versionOf } from './templates.js';
|
|
3
2
|
/**
|
|
4
3
|
* The `web/` frontend emitted when `--ui` is passed: a Vite + React app on
|
|
5
4
|
* @basaltkit/admin-shadcn (authentic shadcn/ui components) talking to the API
|
|
@@ -13,8 +12,6 @@ export function uiFiles(options) {
|
|
|
13
12
|
'web/vite.config.ts': webViteConfig(),
|
|
14
13
|
'web/tsconfig.json': webTsconfig(),
|
|
15
14
|
'web/index.html': webIndexHtml(options),
|
|
16
|
-
'web/postcss.config.js': webPostcss(),
|
|
17
|
-
'web/tailwind.config.js': webTailwind(),
|
|
18
15
|
'web/src/main.tsx': webMain(),
|
|
19
16
|
'web/src/crypto-shim.ts': webCryptoShim(),
|
|
20
17
|
'web/src/index.css': webCss(),
|
|
@@ -27,34 +24,38 @@ function webPackageJson(options) {
|
|
|
27
24
|
name: `${options.name}-web`,
|
|
28
25
|
private: true,
|
|
29
26
|
type: 'module',
|
|
30
|
-
scripts: { dev: 'vite', build: 'vite build', preview: 'vite preview' },
|
|
27
|
+
scripts: { dev: 'vite', build: 'vite build', preview: 'vite preview', typecheck: 'tsc --noEmit' },
|
|
31
28
|
dependencies: {
|
|
32
|
-
'@basaltkit/admin':
|
|
33
|
-
'@basaltkit/admin-shadcn':
|
|
34
|
-
'@basaltkit/sdk':
|
|
35
|
-
react: '
|
|
36
|
-
'react-dom': '
|
|
37
|
-
zod
|
|
38
|
-
|
|
39
|
-
devDependencies: {
|
|
40
|
-
'@types/react': '^18.3.0',
|
|
41
|
-
'@types/react-dom': '^18.3.0',
|
|
42
|
-
'@vitejs/plugin-react': '^4.3.0',
|
|
43
|
-
autoprefixer: '^10.4.0',
|
|
44
|
-
postcss: '^8.4.0',
|
|
45
|
-
tailwindcss: '^3.4.0',
|
|
46
|
-
typescript: '^5.8.0',
|
|
47
|
-
vite: '^6.0.0',
|
|
29
|
+
'@basaltkit/admin': versionOf('@basaltkit/admin'),
|
|
30
|
+
'@basaltkit/admin-shadcn': versionOf('@basaltkit/admin-shadcn'),
|
|
31
|
+
'@basaltkit/sdk': versionOf('@basaltkit/sdk'),
|
|
32
|
+
react: thirdPartyVersionOf('react'),
|
|
33
|
+
'react-dom': thirdPartyVersionOf('react-dom'),
|
|
34
|
+
// Matches the peer every @basaltkit package declares (zod 4).
|
|
35
|
+
zod: thirdPartyVersionOf('zod'),
|
|
48
36
|
},
|
|
37
|
+
// Tailwind 4 runs as a Vite plugin (@tailwindcss/vite) — no PostCSS or
|
|
38
|
+
// autoprefixer config; the theme lives in src/index.css.
|
|
39
|
+
devDependencies: Object.fromEntries([
|
|
40
|
+
'@tailwindcss/vite',
|
|
41
|
+
'@types/node',
|
|
42
|
+
'@types/react',
|
|
43
|
+
'@types/react-dom',
|
|
44
|
+
'@vitejs/plugin-react',
|
|
45
|
+
'tailwindcss',
|
|
46
|
+
'typescript',
|
|
47
|
+
'vite',
|
|
48
|
+
].map((pkg) => [pkg, thirdPartyVersionOf(pkg)])),
|
|
49
49
|
}, null, 2)}\n`;
|
|
50
50
|
}
|
|
51
51
|
function webViteConfig() {
|
|
52
52
|
return `import { fileURLToPath } from 'node:url'
|
|
53
|
+
import tailwindcss from '@tailwindcss/vite'
|
|
53
54
|
import react from '@vitejs/plugin-react'
|
|
54
55
|
import { defineConfig } from 'vite'
|
|
55
56
|
|
|
56
57
|
export default defineConfig({
|
|
57
|
-
plugins: [react()],
|
|
58
|
+
plugins: [react(), tailwindcss()],
|
|
58
59
|
resolve: {
|
|
59
60
|
alias: {
|
|
60
61
|
// @basaltkit/admin uses randomUUID from crypto; map it to Web Crypto in the browser.
|
|
@@ -89,7 +90,9 @@ function webTsconfig() {
|
|
|
89
90
|
noEmit: true,
|
|
90
91
|
allowImportingTsExtensions: true,
|
|
91
92
|
verbatimModuleSyntax: true,
|
|
92
|
-
types
|
|
93
|
+
// vite/client types the CSS/asset imports (TS 6+ checks side-effect
|
|
94
|
+
// imports such as `import './index.css'`) and import.meta.env.
|
|
95
|
+
types: ['node', 'vite/client'],
|
|
93
96
|
},
|
|
94
97
|
include: ['src'],
|
|
95
98
|
}, null, 2)}\n`;
|
|
@@ -109,41 +112,6 @@ function webIndexHtml(options) {
|
|
|
109
112
|
</html>
|
|
110
113
|
`;
|
|
111
114
|
}
|
|
112
|
-
function webPostcss() {
|
|
113
|
-
return `export default {
|
|
114
|
-
plugins: {
|
|
115
|
-
tailwindcss: {},
|
|
116
|
-
autoprefixer: {},
|
|
117
|
-
},
|
|
118
|
-
}
|
|
119
|
-
`;
|
|
120
|
-
}
|
|
121
|
-
function webTailwind() {
|
|
122
|
-
return `export default {
|
|
123
|
-
darkMode: ['class'],
|
|
124
|
-
content: ['./index.html', './src/**/*.{ts,tsx}', './node_modules/@basaltkit/admin-shadcn/dist/**/*.js'],
|
|
125
|
-
theme: {
|
|
126
|
-
extend: {
|
|
127
|
-
colors: {
|
|
128
|
-
border: 'hsl(var(--border))',
|
|
129
|
-
input: 'hsl(var(--input))',
|
|
130
|
-
ring: 'hsl(var(--ring))',
|
|
131
|
-
background: 'hsl(var(--background))',
|
|
132
|
-
foreground: 'hsl(var(--foreground))',
|
|
133
|
-
primary: { DEFAULT: 'hsl(var(--primary))', foreground: 'hsl(var(--primary-foreground))' },
|
|
134
|
-
secondary: { DEFAULT: 'hsl(var(--secondary))', foreground: 'hsl(var(--secondary-foreground))' },
|
|
135
|
-
destructive: { DEFAULT: 'hsl(var(--destructive))', foreground: 'hsl(var(--destructive-foreground))' },
|
|
136
|
-
muted: { DEFAULT: 'hsl(var(--muted))', foreground: 'hsl(var(--muted-foreground))' },
|
|
137
|
-
accent: { DEFAULT: 'hsl(var(--accent))', foreground: 'hsl(var(--accent-foreground))' },
|
|
138
|
-
card: { DEFAULT: 'hsl(var(--card))', foreground: 'hsl(var(--card-foreground))' },
|
|
139
|
-
},
|
|
140
|
-
borderRadius: { lg: 'var(--radius)', md: 'calc(var(--radius) - 2px)', sm: 'calc(var(--radius) - 4px)' },
|
|
141
|
-
},
|
|
142
|
-
},
|
|
143
|
-
plugins: [],
|
|
144
|
-
}
|
|
145
|
-
`;
|
|
146
|
-
}
|
|
147
115
|
function webMain() {
|
|
148
116
|
return `import { StrictMode } from 'react'
|
|
149
117
|
import { createRoot } from 'react-dom/client'
|
|
@@ -162,9 +130,39 @@ function webCryptoShim() {
|
|
|
162
130
|
`;
|
|
163
131
|
}
|
|
164
132
|
function webCss() {
|
|
165
|
-
return `@
|
|
166
|
-
|
|
167
|
-
@
|
|
133
|
+
return `@import 'tailwindcss';
|
|
134
|
+
|
|
135
|
+
/* @basaltkit/admin-shadcn ships precompiled components: let Tailwind scan them
|
|
136
|
+
(paths are relative to this file, so ../node_modules is web/node_modules). */
|
|
137
|
+
@source '../node_modules/@basaltkit/admin-shadcn/dist';
|
|
138
|
+
|
|
139
|
+
/* Class-based dark mode: <html class="dark"> (the theme toggle sets it). */
|
|
140
|
+
@custom-variant dark (&:is(.dark *));
|
|
141
|
+
|
|
142
|
+
/* shadcn/ui tokens → Tailwind utilities (bg-primary, text-muted-foreground,
|
|
143
|
+
border-input, rounded-lg, …). The values are the CSS variables below. */
|
|
144
|
+
@theme inline {
|
|
145
|
+
--color-border: hsl(var(--border));
|
|
146
|
+
--color-input: hsl(var(--input));
|
|
147
|
+
--color-ring: hsl(var(--ring));
|
|
148
|
+
--color-background: hsl(var(--background));
|
|
149
|
+
--color-foreground: hsl(var(--foreground));
|
|
150
|
+
--color-primary: hsl(var(--primary));
|
|
151
|
+
--color-primary-foreground: hsl(var(--primary-foreground));
|
|
152
|
+
--color-secondary: hsl(var(--secondary));
|
|
153
|
+
--color-secondary-foreground: hsl(var(--secondary-foreground));
|
|
154
|
+
--color-destructive: hsl(var(--destructive));
|
|
155
|
+
--color-destructive-foreground: hsl(var(--destructive-foreground));
|
|
156
|
+
--color-muted: hsl(var(--muted));
|
|
157
|
+
--color-muted-foreground: hsl(var(--muted-foreground));
|
|
158
|
+
--color-accent: hsl(var(--accent));
|
|
159
|
+
--color-accent-foreground: hsl(var(--accent-foreground));
|
|
160
|
+
--color-card: hsl(var(--card));
|
|
161
|
+
--color-card-foreground: hsl(var(--card-foreground));
|
|
162
|
+
--radius-lg: var(--radius);
|
|
163
|
+
--radius-md: calc(var(--radius) - 2px);
|
|
164
|
+
--radius-sm: calc(var(--radius) - 4px);
|
|
165
|
+
}
|
|
168
166
|
|
|
169
167
|
@layer base {
|
|
170
168
|
:root {
|
|
@@ -206,9 +204,19 @@ function webCss() {
|
|
|
206
204
|
--input: 240 3.7% 18%;
|
|
207
205
|
--ring: 243 75% 68%;
|
|
208
206
|
}
|
|
209
|
-
|
|
207
|
+
/* Tailwind 4 defaults borders to currentColor; keep the themed border. */
|
|
208
|
+
*,
|
|
209
|
+
::after,
|
|
210
|
+
::before,
|
|
211
|
+
::backdrop,
|
|
212
|
+
::file-selector-button {
|
|
210
213
|
border-color: hsl(var(--border));
|
|
211
214
|
}
|
|
215
|
+
/* Tailwind 4 dropped the pointer cursor on buttons; keep it. */
|
|
216
|
+
button:not(:disabled),
|
|
217
|
+
[role='button']:not(:disabled) {
|
|
218
|
+
cursor: pointer;
|
|
219
|
+
}
|
|
212
220
|
body {
|
|
213
221
|
margin: 0;
|
|
214
222
|
background-color: hsl(var(--background));
|
package/dist/templates.d.ts
CHANGED
|
@@ -10,8 +10,24 @@ export interface ProjectOptions {
|
|
|
10
10
|
/** Expose opted-in routes as MCP tools (@basaltkit/mcp) over HTTP at `/mcp`. */
|
|
11
11
|
mcp: boolean;
|
|
12
12
|
}
|
|
13
|
-
|
|
13
|
+
export { SCAFFOLD_VERSIONS } from './versions.js';
|
|
14
|
+
export { THIRD_PARTY_VERSIONS } from './latest-versions.js';
|
|
15
|
+
/**
|
|
16
|
+
* The dependency range for a @basaltkit/* package: the current release line
|
|
17
|
+
* (`^<major>.<minor>.0`) of its workspace version, from the generated
|
|
18
|
+
* {@link SCAFFOLD_VERSIONS} map. Throws for an unknown package rather than
|
|
19
|
+
* guessing a range — a stale guess once pinned new apps to frozen 1.x lines.
|
|
20
|
+
*/
|
|
14
21
|
export declare const versionOf: (pkg: string) => string;
|
|
22
|
+
/**
|
|
23
|
+
* The fallback range for a third-party package the templates emit, from
|
|
24
|
+
* {@link THIRD_PARTY_VERSIONS}. At scaffold time the CLI replaces it with
|
|
25
|
+
* `^<latest>` when the registry's latest is on the same major. Throws for an
|
|
26
|
+
* unknown package so every emitted range lives in that single table.
|
|
27
|
+
*/
|
|
28
|
+
export declare const thirdPartyVersionOf: (pkg: string) => string;
|
|
29
|
+
/** True when the scaffold binds users to the tenant they act on (tenancy + auth). */
|
|
30
|
+
export declare const enforcesMembership: (options: ProjectOptions) => boolean;
|
|
15
31
|
export declare function packageJson(options: ProjectOptions): string;
|
|
16
32
|
export declare function tsconfigJson(): string;
|
|
17
33
|
export declare function envTs(options: ProjectOptions): string;
|
|
@@ -19,6 +35,13 @@ export declare function envExample(options: ProjectOptions): string;
|
|
|
19
35
|
export declare function appTs(options: ProjectOptions): string;
|
|
20
36
|
export declare function routesTs(options: ProjectOptions): string;
|
|
21
37
|
export declare function serverTs(): string;
|
|
38
|
+
/**
|
|
39
|
+
* `pnpm dev` entrypoint. Opts into NODE_ENV=development (dev secret defaults,
|
|
40
|
+
* pretty logs) unless NODE_ENV is already set, then starts the server. Kept out
|
|
41
|
+
* of server.ts on purpose: `pnpm start` runs server.ts directly, where an unset
|
|
42
|
+
* NODE_ENV counts as production and secrets are required (fail-closed).
|
|
43
|
+
*/
|
|
44
|
+
export declare function devTs(): string;
|
|
22
45
|
export declare function basaltBin(): string;
|
|
23
46
|
export declare function appTest(options: ProjectOptions): string;
|
|
24
47
|
export declare function readme(options: ProjectOptions): string;
|
|
@@ -28,6 +51,14 @@ export declare function readme(options: ProjectOptions): string;
|
|
|
28
51
|
* project root automatically; Claude Desktop uses the same server entry.
|
|
29
52
|
*/
|
|
30
53
|
export declare function mcpJson(_options: ProjectOptions): string;
|
|
54
|
+
/**
|
|
55
|
+
* Keeps secrets and local state out of Docker build contexts, so a
|
|
56
|
+
* `COPY . .` can never bake `.env` or private keys into an image layer. Every
|
|
57
|
+
* rule is `**`-prefixed: .dockerignore patterns are anchored at the context
|
|
58
|
+
* root, so a bare `.env` or `*.pem` would still let `prisma/.env` or
|
|
59
|
+
* `certs/server.key` through.
|
|
60
|
+
*/
|
|
61
|
+
export declare function dockerignore(): string;
|
|
31
62
|
export declare function gitignore(): string;
|
|
32
63
|
/**
|
|
33
64
|
* pnpm settings: esbuild's build script is required by tsx;
|
package/dist/templates.js
CHANGED
|
@@ -1,55 +1,63 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { THIRD_PARTY_VERSIONS } from './latest-versions.js';
|
|
2
|
+
import { SCAFFOLD_VERSIONS } from './versions.js';
|
|
3
|
+
export { SCAFFOLD_VERSIONS } from './versions.js';
|
|
4
|
+
export { THIRD_PARTY_VERSIONS } from './latest-versions.js';
|
|
3
5
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
6
|
+
* The dependency range for a @basaltkit/* package: the current release line
|
|
7
|
+
* (`^<major>.<minor>.0`) of its workspace version, from the generated
|
|
8
|
+
* {@link SCAFFOLD_VERSIONS} map. Throws for an unknown package rather than
|
|
9
|
+
* guessing a range — a stale guess once pinned new apps to frozen 1.x lines.
|
|
7
10
|
*/
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
+
export const versionOf = (pkg) => {
|
|
12
|
+
const range = SCAFFOLD_VERSIONS[pkg];
|
|
13
|
+
if (range === undefined) {
|
|
14
|
+
throw new Error(`create-basalt: no release line for ${pkg} — add it to scripts/sync-versions.mjs.`);
|
|
15
|
+
}
|
|
16
|
+
return range;
|
|
11
17
|
};
|
|
12
|
-
/**
|
|
13
|
-
|
|
18
|
+
/**
|
|
19
|
+
* The fallback range for a third-party package the templates emit, from
|
|
20
|
+
* {@link THIRD_PARTY_VERSIONS}. At scaffold time the CLI replaces it with
|
|
21
|
+
* `^<latest>` when the registry's latest is on the same major. Throws for an
|
|
22
|
+
* unknown package so every emitted range lives in that single table.
|
|
23
|
+
*/
|
|
24
|
+
export const thirdPartyVersionOf = (pkg) => {
|
|
25
|
+
const range = THIRD_PARTY_VERSIONS[pkg];
|
|
26
|
+
if (range === undefined) {
|
|
27
|
+
throw new Error(`create-basalt: no fallback range for ${pkg} — add it to THIRD_PARTY_VERSIONS.`);
|
|
28
|
+
}
|
|
29
|
+
return range;
|
|
30
|
+
};
|
|
31
|
+
/** True when the scaffold binds users to the tenant they act on (tenancy + auth). */
|
|
32
|
+
export const enforcesMembership = (options) => options.tenancy && options.auth;
|
|
14
33
|
export function packageJson(options) {
|
|
15
|
-
const
|
|
16
|
-
'@basaltkit/config': BASALT_VERSION,
|
|
17
|
-
'@basaltkit/core': BASALT_VERSION,
|
|
18
|
-
'@basaltkit/env': BASALT_VERSION,
|
|
19
|
-
'@basaltkit/events': BASALT_VERSION,
|
|
20
|
-
'@basaltkit/fastify': BASALT_VERSION,
|
|
21
|
-
'@basaltkit/logger': BASALT_VERSION,
|
|
22
|
-
zod: '^4.0.0',
|
|
23
|
-
};
|
|
34
|
+
const basalt = ['@basaltkit/config', '@basaltkit/core', '@basaltkit/env', '@basaltkit/events', '@basaltkit/fastify', '@basaltkit/logger'];
|
|
24
35
|
if (options.tenancy)
|
|
25
|
-
|
|
36
|
+
basalt.push('@basaltkit/tenancy');
|
|
26
37
|
if (options.auth)
|
|
27
|
-
|
|
38
|
+
basalt.push('@basaltkit/auth');
|
|
39
|
+
// Tenancy + auth: @basaltkit/teams binds each user to the tenants they belong
|
|
40
|
+
// to (tenantMembershipPlugin) — without it x-tenant-id/Host is a free choice.
|
|
41
|
+
if (enforcesMembership(options))
|
|
42
|
+
basalt.push('@basaltkit/teams');
|
|
28
43
|
if (options.billing)
|
|
29
|
-
|
|
44
|
+
basalt.push('@basaltkit/subscriptions');
|
|
30
45
|
if (options.mcp)
|
|
31
|
-
|
|
46
|
+
basalt.push('@basaltkit/mcp');
|
|
32
47
|
if (options.cli) {
|
|
33
48
|
// Runtime deps: app.ts uses commandsPlugin (@basaltkit/cli); prisma powers
|
|
34
49
|
// prismaPlugin + `basalt prisma:sync`. @basaltkit/generator is dev-only (below).
|
|
35
|
-
|
|
36
|
-
dependencies['@basaltkit/prisma'] = BASALT_VERSION;
|
|
37
|
-
}
|
|
38
|
-
// Apply per-package range overrides. Only @basaltkit/* packages: versionOf
|
|
39
|
-
// falls back to BASALT_VERSION, which would clobber third-party ranges
|
|
40
|
-
// (this once rewrote zod's range to the @basalt base line).
|
|
41
|
-
for (const pkg of Object.keys(dependencies)) {
|
|
42
|
-
if (pkg.startsWith('@basaltkit/'))
|
|
43
|
-
dependencies[pkg] = versionOf(pkg);
|
|
50
|
+
basalt.push('@basaltkit/cli', '@basaltkit/prisma');
|
|
44
51
|
}
|
|
52
|
+
const dependencies = { zod: thirdPartyVersionOf('zod') };
|
|
53
|
+
for (const pkg of basalt)
|
|
54
|
+
dependencies[pkg] = versionOf(pkg);
|
|
45
55
|
const devDependencies = {
|
|
46
56
|
'@basaltkit/testing': versionOf('@basaltkit/testing'),
|
|
47
|
-
'@types/node': '^22.15.0',
|
|
48
|
-
'pino-pretty': '^13.0.0',
|
|
49
|
-
tsx: '^4.19.0',
|
|
50
|
-
typescript: '^5.8.0',
|
|
51
|
-
vitest: '^3.1.0',
|
|
52
57
|
};
|
|
58
|
+
for (const pkg of ['@types/node', 'pino-pretty', 'tsx', 'typescript', 'vitest']) {
|
|
59
|
+
devDependencies[pkg] = thirdPartyVersionOf(pkg);
|
|
60
|
+
}
|
|
53
61
|
if (options.cli) {
|
|
54
62
|
// Dev-only: only bin/basalt.ts imports the code generator — the runtime server
|
|
55
63
|
// never does, so the app runs completely without the codegen/AI layer.
|
|
@@ -67,7 +75,9 @@ export function packageJson(options) {
|
|
|
67
75
|
private: true,
|
|
68
76
|
type: 'module',
|
|
69
77
|
scripts: {
|
|
70
|
-
dev
|
|
78
|
+
// dev opts into NODE_ENV=development (src/dev.ts); start does not, so an
|
|
79
|
+
// unset NODE_ENV counts as production and secrets are required.
|
|
80
|
+
dev: 'tsx watch src/dev.ts',
|
|
71
81
|
start: 'tsx src/server.ts',
|
|
72
82
|
test: 'vitest run',
|
|
73
83
|
typecheck: 'tsc --noEmit',
|
|
@@ -104,10 +114,12 @@ export const env = defineEnv({
|
|
|
104
114
|
// Typed against the logger's LogLevel union — a free-form string here fails
|
|
105
115
|
// \`pnpm typecheck\` where loggerPlugin({ level }) consumes it.
|
|
106
116
|
LOG_LEVEL: z.enum(LOG_LEVELS).default('info'),
|
|
107
|
-
|
|
117
|
+
// Unset counts as production (fail-closed); \`pnpm dev\` sets development.
|
|
118
|
+
NODE_ENV: z.enum(['development', 'production', 'test']).default('production'),${options.auth
|
|
108
119
|
? `
|
|
109
|
-
// Signs JWTs and sessions. secret() is fail-closed: required
|
|
110
|
-
// (no fallback
|
|
120
|
+
// Signs JWTs and sessions. secret() is fail-closed: required unless NODE_ENV
|
|
121
|
+
// is explicitly development/test (no fallback when NODE_ENV is unset),
|
|
122
|
+
// rejected if it looks like a placeholder. \`pnpm dev\` uses devDefault.
|
|
111
123
|
APP_SECRET: secret({ minLength: 32, devDefault: 'dev-only-insecure-secret-please-change-me' }),`
|
|
112
124
|
: ''}
|
|
113
125
|
})
|
|
@@ -118,7 +130,7 @@ export function envExample(options) {
|
|
|
118
130
|
HOST=0.0.0.0
|
|
119
131
|
LOG_LEVEL=info
|
|
120
132
|
NODE_ENV=development
|
|
121
|
-
${options.auth ? '# Required
|
|
133
|
+
${options.auth ? '# Required unless NODE_ENV is development/test — `pnpm start` refuses to boot\n# without it. Generate a strong one: openssl rand -base64 48\n# APP_SECRET=\n' : ''}`;
|
|
122
134
|
}
|
|
123
135
|
export function appTs(options) {
|
|
124
136
|
const imports = [
|
|
@@ -134,8 +146,11 @@ export function appTs(options) {
|
|
|
134
146
|
`eventsPlugin()`,
|
|
135
147
|
`securityPlugin({
|
|
136
148
|
// Secure response headers (HSTS, X-Frame-Options, nosniff, …) are on by
|
|
137
|
-
// default
|
|
138
|
-
//
|
|
149
|
+
// default, and so is a global per-IP rate limit (auth routes add their
|
|
150
|
+
// own stricter budget). Behind a proxy, make the adapter set request.ip
|
|
151
|
+
// from it; with several replicas, pass a shared \`store\`.
|
|
152
|
+
rateLimit: { limit: 120, windowMs: 60_000 },
|
|
153
|
+
// Add a CORS allow-list if a browser app on another origin calls the API:
|
|
139
154
|
// cors: { origin: ['https://app.example.com'], credentials: true },
|
|
140
155
|
})`,
|
|
141
156
|
];
|
|
@@ -162,6 +177,25 @@ export function appTs(options) {
|
|
|
162
177
|
// password recovery. mfaRoutes(): TOTP enroll/activate/status/disable.
|
|
163
178
|
routesExpression = '[...appRoutes, ...authRoutes(), ...mfaRoutes()]';
|
|
164
179
|
}
|
|
180
|
+
if (enforcesMembership(options)) {
|
|
181
|
+
// Secure by default: the tenant comes from client input (x-tenant-id / Host),
|
|
182
|
+
// so it is identification, never authorization. tenantMembershipPlugin()
|
|
183
|
+
// rejects (403) every authenticated request for a tenant the user is not a
|
|
184
|
+
// member of. Routes that act outside a single tenant opt out with
|
|
185
|
+
// meta: { central: true }.
|
|
186
|
+
imports[0] = `import { createApp, definePlugin } from '@basaltkit/core'`;
|
|
187
|
+
imports.push(`import { TEAMS, teamsPlugin, tenantMembershipPlugin } from '@basaltkit/teams'`);
|
|
188
|
+
plugins.push(`teamsPlugin({
|
|
189
|
+
// TODO: the default membership/invitation stores are in-memory — pass
|
|
190
|
+
// persistent ones (memberships, invitations) before production.
|
|
191
|
+
})`);
|
|
192
|
+
plugins.push(`// Rejects authenticated requests for a tenant the user is not a member of.
|
|
193
|
+
tenantMembershipPlugin()`);
|
|
194
|
+
plugins.push(`// Dev-only seed: each new registrant joins the 'demo' tenant so the scaffold
|
|
195
|
+
// works out of the box. Never in production — add members explicitly there
|
|
196
|
+
// (on tenant creation, or via TEAMS invite()/accept()).
|
|
197
|
+
...(env.NODE_ENV === 'production' ? [] : [demoMembershipSeed()])`);
|
|
198
|
+
}
|
|
165
199
|
if (options.billing) {
|
|
166
200
|
imports.push(`import { definePlans, subscriptionsPlugin } from '@basaltkit/subscriptions'`);
|
|
167
201
|
plugins.push(`subscriptionsPlugin({
|
|
@@ -211,7 +245,21 @@ export function buildApp(options: BuildAppOptions = {}) {
|
|
|
211
245
|
],
|
|
212
246
|
})
|
|
213
247
|
}
|
|
214
|
-
|
|
248
|
+
${enforcesMembership(options)
|
|
249
|
+
? `
|
|
250
|
+
/** Dev-only: adds each new registrant as a member of the 'demo' tenant. */
|
|
251
|
+
function demoMembershipSeed() {
|
|
252
|
+
return definePlugin({
|
|
253
|
+
name: 'app:demo-membership-seed',
|
|
254
|
+
register({ container, hooks }) {
|
|
255
|
+
hooks.on('auth:registered', async ({ user }) => {
|
|
256
|
+
await container.get(TEAMS).addMember('demo', user.id, 'member')
|
|
257
|
+
})
|
|
258
|
+
},
|
|
259
|
+
})
|
|
260
|
+
}
|
|
261
|
+
`
|
|
262
|
+
: ''}`;
|
|
215
263
|
}
|
|
216
264
|
export function routesTs(options) {
|
|
217
265
|
const endpoints = [
|
|
@@ -283,12 +331,29 @@ for (const signal of ['SIGINT', 'SIGTERM'] as const) {
|
|
|
283
331
|
}
|
|
284
332
|
`;
|
|
285
333
|
}
|
|
334
|
+
/**
|
|
335
|
+
* `pnpm dev` entrypoint. Opts into NODE_ENV=development (dev secret defaults,
|
|
336
|
+
* pretty logs) unless NODE_ENV is already set, then starts the server. Kept out
|
|
337
|
+
* of server.ts on purpose: `pnpm start` runs server.ts directly, where an unset
|
|
338
|
+
* NODE_ENV counts as production and secrets are required (fail-closed).
|
|
339
|
+
*/
|
|
340
|
+
export function devTs() {
|
|
341
|
+
return `// Development entrypoint (\`pnpm dev\`). \`pnpm start\` runs server.ts directly,
|
|
342
|
+
// where an unset NODE_ENV counts as production: secrets are then required.
|
|
343
|
+
process.env['NODE_ENV'] ??= 'development'
|
|
344
|
+
await import('./server.js')
|
|
345
|
+
`;
|
|
346
|
+
}
|
|
286
347
|
export function basaltBin() {
|
|
287
348
|
return `#!/usr/bin/env node
|
|
288
349
|
import { runCli } from '@basaltkit/cli'
|
|
289
350
|
import { generatorCommands } from '@basaltkit/generator'
|
|
290
351
|
import { prismaSyncCommand } from '@basaltkit/prisma'
|
|
291
|
-
|
|
352
|
+
|
|
353
|
+
// Dev tooling: opt into development defaults unless NODE_ENV is already set
|
|
354
|
+
// (imported dynamically below so env.ts is evaluated after this line).
|
|
355
|
+
process.env['NODE_ENV'] ??= 'development'
|
|
356
|
+
const { buildApp } = await import('../src/app.js')
|
|
292
357
|
|
|
293
358
|
// The 'basalt' CLI: boots the app WITH the dev/CLI commands, runs one, shuts down.
|
|
294
359
|
// The dev tools (@basaltkit/generator; add @basaltkit/ai for ai:*) are imported
|
|
@@ -433,6 +498,29 @@ export function mcpJson(_options) {
|
|
|
433
498
|
},
|
|
434
499
|
}, null, 2)}\n`;
|
|
435
500
|
}
|
|
501
|
+
/**
|
|
502
|
+
* Keeps secrets and local state out of Docker build contexts, so a
|
|
503
|
+
* `COPY . .` can never bake `.env` or private keys into an image layer. Every
|
|
504
|
+
* rule is `**`-prefixed: .dockerignore patterns are anchored at the context
|
|
505
|
+
* root, so a bare `.env` or `*.pem` would still let `prisma/.env` or
|
|
506
|
+
* `certs/server.key` through.
|
|
507
|
+
*/
|
|
508
|
+
export function dockerignore() {
|
|
509
|
+
return `**/.env
|
|
510
|
+
**/.env.*
|
|
511
|
+
!**/.env.example
|
|
512
|
+
**/.npmrc
|
|
513
|
+
**/.git
|
|
514
|
+
**/node_modules
|
|
515
|
+
**/coverage
|
|
516
|
+
**/*.log
|
|
517
|
+
**/*.pem
|
|
518
|
+
**/*.key
|
|
519
|
+
**/*.p12
|
|
520
|
+
**/*.pfx
|
|
521
|
+
**/.DS_Store
|
|
522
|
+
`;
|
|
523
|
+
}
|
|
436
524
|
export function gitignore() {
|
|
437
525
|
return `node_modules/
|
|
438
526
|
dist/
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const SCAFFOLD_VERSIONS: Readonly<Record<string, string>>;
|
package/dist/versions.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// GENERATED by scripts/sync-versions.mjs (runs before every build) — do not edit.
|
|
2
|
+
// The dependency range a new app gets for each @basaltkit/* package: the
|
|
3
|
+
// current release line (^<major>.<minor>.0) of the workspace version.
|
|
4
|
+
export const SCAFFOLD_VERSIONS = {
|
|
5
|
+
'@basaltkit/admin': '^2.0.0',
|
|
6
|
+
'@basaltkit/admin-shadcn': '^1.1.0',
|
|
7
|
+
'@basaltkit/ai-mcp': '^0.1.0',
|
|
8
|
+
'@basaltkit/auth': '^3.0.0',
|
|
9
|
+
'@basaltkit/cli': '^1.2.0',
|
|
10
|
+
'@basaltkit/config': '^1.1.0',
|
|
11
|
+
'@basaltkit/core': '^1.3.0',
|
|
12
|
+
'@basaltkit/env': '^3.0.0',
|
|
13
|
+
'@basaltkit/events': '^1.2.0',
|
|
14
|
+
'@basaltkit/fastify': '^2.1.0',
|
|
15
|
+
'@basaltkit/generator': '^1.4.0',
|
|
16
|
+
'@basaltkit/logger': '^1.3.0',
|
|
17
|
+
'@basaltkit/mcp': '^3.0.0',
|
|
18
|
+
'@basaltkit/prisma': '^2.0.0',
|
|
19
|
+
'@basaltkit/sdk': '^2.0.0',
|
|
20
|
+
'@basaltkit/subscriptions': '^4.0.0',
|
|
21
|
+
'@basaltkit/teams': '^3.0.0',
|
|
22
|
+
'@basaltkit/tenancy': '^2.0.0',
|
|
23
|
+
'@basaltkit/testing': '^2.0.0',
|
|
24
|
+
};
|
package/package.json
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-basalt",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
|
+
"engines": {
|
|
5
|
+
"node": ">=22.5.0"
|
|
6
|
+
},
|
|
4
7
|
"description": "Scaffold a production-shaped Basalt SaaS app: npm create basalt my-saas [--billing] [--ui] [--cli] [--install] [--git]",
|
|
5
8
|
"license": "MIT",
|
|
6
9
|
"type": "module",
|
|
10
|
+
"sideEffects": false,
|
|
7
11
|
"bin": {
|
|
8
12
|
"create-basalt": "./dist/cli.js"
|
|
9
13
|
},
|
|
@@ -17,24 +21,25 @@
|
|
|
17
21
|
"dist"
|
|
18
22
|
],
|
|
19
23
|
"devDependencies": {
|
|
20
|
-
"@types/node": "^26.
|
|
24
|
+
"@types/node": "^26.5.1",
|
|
21
25
|
"typescript": "^7.0.2",
|
|
22
|
-
"vitest": "^
|
|
23
|
-
"zod": "^4.
|
|
24
|
-
"@basaltkit/auth": "^
|
|
25
|
-
"@basaltkit/
|
|
26
|
-
"@basaltkit/
|
|
27
|
-
"@basaltkit/
|
|
28
|
-
"@basaltkit/
|
|
29
|
-
"@basaltkit/
|
|
30
|
-
"@basaltkit/
|
|
31
|
-
"@basaltkit/
|
|
32
|
-
"@basaltkit/logger": "^1.
|
|
33
|
-
"@basaltkit/mcp": "^
|
|
34
|
-
"@basaltkit/
|
|
35
|
-
"@basaltkit/
|
|
36
|
-
"@basaltkit/
|
|
37
|
-
"@basaltkit/
|
|
26
|
+
"vitest": "^5.0.0",
|
|
27
|
+
"zod": "^4.5.4",
|
|
28
|
+
"@basaltkit/auth": "^3.0.0",
|
|
29
|
+
"@basaltkit/cli": "^1.2.5",
|
|
30
|
+
"@basaltkit/config": "^1.1.2",
|
|
31
|
+
"@basaltkit/core": "^1.3.1",
|
|
32
|
+
"@basaltkit/env": "^3.0.0",
|
|
33
|
+
"@basaltkit/events": "^1.2.0",
|
|
34
|
+
"@basaltkit/fastify": "^2.1.0",
|
|
35
|
+
"@basaltkit/generator": "^1.4.0",
|
|
36
|
+
"@basaltkit/logger": "^1.3.0",
|
|
37
|
+
"@basaltkit/mcp": "^3.0.0",
|
|
38
|
+
"@basaltkit/prisma": "^2.0.0",
|
|
39
|
+
"@basaltkit/subscriptions": "^4.0.0",
|
|
40
|
+
"@basaltkit/teams": "^3.0.0",
|
|
41
|
+
"@basaltkit/tenancy": "^2.0.0",
|
|
42
|
+
"@basaltkit/testing": "^2.0.1",
|
|
38
43
|
"@basaltkit/tsconfig": "^0.24.0"
|
|
39
44
|
},
|
|
40
45
|
"publishConfig": {
|
|
@@ -57,7 +62,8 @@
|
|
|
57
62
|
"boilerplate"
|
|
58
63
|
],
|
|
59
64
|
"scripts": {
|
|
60
|
-
"build": "tsc -p tsconfig.build.json",
|
|
65
|
+
"build": "node scripts/sync-versions.mjs && tsc -p tsconfig.build.json",
|
|
66
|
+
"sync-versions": "node scripts/sync-versions.mjs",
|
|
61
67
|
"test": "vitest run",
|
|
62
68
|
"typecheck": "tsc --noEmit"
|
|
63
69
|
}
|