create-basalt 1.4.2 → 1.6.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 +28 -5
- 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 +125 -0
- package/dist/latest-versions.js +293 -0
- package/dist/templates-ui.d.ts +1 -1
- package/dist/templates-ui.js +70 -62
- package/dist/templates.d.ts +44 -1
- package/dist/templates.js +204 -48
- package/dist/versions.d.ts +1 -0
- package/dist/versions.js +24 -0
- package/package.json +20 -18
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,8 @@ 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.
|
|
107
|
+
- **Release-age window (pnpm `minimumReleaseAge`)**: a third-party `latest` not provably older than the window (default 1440 min = pnpm 11's default; follows `pnpm_config_minimum_release_age` / `npm_config_minimum_release_age`) keeps the bundled range, so the first `pnpm install` is never blocked by a just-published version — pnpm then picks the newest *mature* version inside that range. The age comes from one `HEAD <registry>/<name>` per third-party package (the packument's `last-modified`, measured against the registry's `Date`); a missing header or failed probe counts as "not provably mature" and prints a `Note:`. `@basaltkit/*` is never probed (the generated `pnpm-workspace.yaml` excludes the scope).
|
|
105
108
|
- **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
109
|
- At the end, it prints the created files and the "Next steps" appropriate to your choices.
|
|
107
110
|
|
|
@@ -132,10 +135,10 @@ Always:
|
|
|
132
135
|
my-app/
|
|
133
136
|
├── package.json # scripts: dev, start, test, typecheck (+ basalt with --cli)
|
|
134
137
|
├── tsconfig.json # strict TypeScript, ESM
|
|
135
|
-
├── .env.example # PORT, HOST, LOG_LEVEL, NODE_ENV (+ APP_SECRET with auth)
|
|
138
|
+
├── .env.example # PORT, HOST, LOG_LEVEL, NODE_ENV (+ APP_SECRET with auth) + the --env-file precedence warning
|
|
136
139
|
├── .gitignore
|
|
137
140
|
├── README.md # instructions adapted to the chosen options
|
|
138
|
-
├── pnpm-workspace.yaml # esbuild allowBuilds (+ "web" member with --ui)
|
|
141
|
+
├── pnpm-workspace.yaml # esbuild allowBuilds, @basaltkit/* release-age exclusion (+ "web" member with --ui) + pnpm 11 notes
|
|
139
142
|
├── src/
|
|
140
143
|
│ ├── env.ts # environment variables validated with Zod (@basaltkit/env)
|
|
141
144
|
│ ├── app.ts # buildApp() with the chosen plugins
|
|
@@ -144,7 +147,7 @@ my-app/
|
|
|
144
147
|
└── tests/app.test.ts # smoke test adapted to the options
|
|
145
148
|
```
|
|
146
149
|
|
|
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
|
|
150
|
+
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
151
|
|
|
149
152
|
### With `--ui`: running the API and frontend
|
|
150
153
|
|
|
@@ -164,6 +167,17 @@ pnpm basalt routes # registered HTTP routes
|
|
|
164
167
|
pnpm basalt make:resource Project # generates schema → repository → service → plugin → routes → test
|
|
165
168
|
```
|
|
166
169
|
|
|
170
|
+
With pnpm 11, every `pnpm <script>` and `pnpm exec` first verifies dependencies (`verifyDepsBeforeRun`, default `install`) and runs `pnpm install` when any workspace project is out of sync — so `pnpm basalt …` can need the network. `node_modules/.bin/tsx bin/basalt.ts …` skips that check; `verifyDepsBeforeRun: warn` (commented out in the generated `pnpm-workspace.yaml`) turns it into a warning for the whole project.
|
|
171
|
+
|
|
172
|
+
### `pnpm-workspace.yaml` and pnpm 11
|
|
173
|
+
|
|
174
|
+
- `minimumReleaseAgeExclude` is evaluated **first-match-wins by package name**. To exclude several versions of one package, write ONE entry with a union — `'@types/node@22.20.4 || 26.6.2'` — never two `@types/node@…` entries (only the first would apply). The generated file carries this example as a comment.
|
|
175
|
+
- `verifyDepsBeforeRun: warn` is offered commented out; the scaffold keeps pnpm's secure default (`install`).
|
|
176
|
+
|
|
177
|
+
### Environment variables and `--env-file`
|
|
178
|
+
|
|
179
|
+
Nothing loads `.env` for you. `node --env-file=.env` / `tsx --env-file=.env` **never override a variable already exported in the shell** — in a terminal where another project exported `DATABASE_URL` or `PORT`, the app boots against that value and only fails on the first request that touches it. The generated `.env.example` and README say so and suggest an app-specific prefix derived from the project name (`my-saas` → `MY_SAAS_DATABASE_URL`).
|
|
180
|
+
|
|
167
181
|
### Programmatic usage (Advanced)
|
|
168
182
|
|
|
169
183
|
The package also exports the API used by the executable, for your own scripts:
|
|
@@ -185,7 +199,7 @@ console.log(result.files) // relative paths, sorted
|
|
|
185
199
|
console.log(detectPackageManager()) // 'pnpm' | 'npm' | 'yarn' | 'bun'
|
|
186
200
|
```
|
|
187
201
|
|
|
188
|
-
Note: `createProject` **only writes files** — it doesn't install dependencies or initialize git (that's the executable's job, with `--install`/`--git`).
|
|
202
|
+
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
203
|
|
|
190
204
|
## API reference
|
|
191
205
|
|
|
@@ -205,6 +219,8 @@ Exported from `create-basalt` (in addition to the `create-basalt` executable):
|
|
|
205
219
|
| `ui` | `boolean` | No | `false` | Generate the `web/` frontend |
|
|
206
220
|
| `cli` | `boolean` | No | `false` | Generate the `basalt` CLI |
|
|
207
221
|
| `mcp` | `boolean` | No | `false` | Expose read-only routes as MCP tools at `/mcp` |
|
|
222
|
+
| `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) |
|
|
223
|
+
| `registry` | `ResolveLatestOptions` | No | — | `{ fetch?, registry?, timeoutMs?, overallTimeoutMs?, concurrency?, minimumReleaseAge?, now? }` — injectable fetch (tests), registry URL (default `npm_config_registry` or `https://registry.npmjs.org`), timeouts (5 s per request, 15 s overall), release-age window in minutes (default `pnpm_config_minimum_release_age` / `npm_config_minimum_release_age`, else 1440; `0` disables the probe), clock (tests) |
|
|
208
224
|
|
|
209
225
|
`CreateProjectResult`:
|
|
210
226
|
|
|
@@ -213,6 +229,7 @@ Exported from `create-basalt` (in addition to the `create-basalt` executable):
|
|
|
213
229
|
| `dir` | `string` | Absolute path of the created folder |
|
|
214
230
|
| `files` | `string[]` | Files written (relative, sorted) |
|
|
215
231
|
| `options` | `ProjectOptions` | The options actually applied (with defaults resolved) |
|
|
232
|
+
| `versions` | `VersionResolution \| undefined` | With `resolveLatest`: `{ versions, resolved, failed, heldBack, tooFresh, registry }` — the final ranges, which packages got `^<latest>`, which kept the fallback after a registry failure, third-party packages held back because their latest is a new major, and third-party packages whose latest is not provably older than the release-age window (`{ name, latest, range, reason: 'recent' \| 'unknown' }`) |
|
|
216
233
|
|
|
217
234
|
Throws `TargetNotEmptyError` if the destination folder exists and isn't empty.
|
|
218
235
|
|
|
@@ -252,6 +269,12 @@ Automatic installation failed (network, Node version, etc.). Go into the folder
|
|
|
252
269
|
**I started the app and `GET /auth/login` gives a secret error.**
|
|
253
270
|
With auth on, `src/env.ts` requires `APP_SECRET` with at least 16 characters (there's a development default `change-me-in-production--`). Copy `.env.example` to `.env` and set your own secret before going to production.
|
|
254
271
|
|
|
272
|
+
**My app connects to the wrong database / port.**
|
|
273
|
+
A variable exported in your shell beats `--env-file`. Check `env | grep DATABASE_URL`, start with `env -u DATABASE_URL pnpm dev`, and prefer app-prefixed names (see *Environment variables and `--env-file`*).
|
|
274
|
+
|
|
275
|
+
**`pnpm basalt …` starts by running `pnpm install` (or fails offline).**
|
|
276
|
+
pnpm 11's `verifyDepsBeforeRun` (default `install`). Run `node_modules/.bin/tsx bin/basalt.ts …` instead, or set `verifyDepsBeforeRun: warn` in `pnpm-workspace.yaml`.
|
|
277
|
+
|
|
255
278
|
**I want to change my mind after generating (e.g. add billing).**
|
|
256
279
|
There's no "re-scaffold" command. Either generate a new project with the right flags and compare, or add it by hand: install `@basaltkit/subscriptions` and add the `subscriptionsPlugin` to `src/app.ts` (the generated README and the templates serve as reference).
|
|
257
280
|
|
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, DEFAULT_MINIMUM_RELEASE_AGE_MINUTES, describeResolution, minimumReleaseAgeMinutes, resolveLatestVersions, registryUrl, type FreshVersion, 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, DEFAULT_MINIMUM_RELEASE_AGE_MINUTES, describeResolution, minimumReleaseAgeMinutes, 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,125 @@
|
|
|
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
|
+
* - Third-party packages also respect pnpm's `minimumReleaseAge` (pnpm 11
|
|
13
|
+
* defaults it to one day): `^<latest>` for a version published minutes ago is
|
|
14
|
+
* UNSATISFIABLE under that policy, so the first `pnpm install` would fail. A
|
|
15
|
+
* cheap `HEAD <registry>/<name>` reads the packument's `last-modified` (which
|
|
16
|
+
* is never older than the latest publish): older than the window proves the
|
|
17
|
+
* latest mature and `^<latest>` is written; newer — or no header / a failed
|
|
18
|
+
* probe — keeps the fallback range, from which pnpm picks the newest MATURE
|
|
19
|
+
* version itself. The full packument (the only per-version `time` source) is
|
|
20
|
+
* megabytes for packages like `@types/node`, so it is never fetched; a
|
|
21
|
+
* false "fresh" only costs a lower range floor. `@basaltkit/*` is never
|
|
22
|
+
* probed: the scaffolded pnpm-workspace.yaml excludes that scope.
|
|
23
|
+
* - Any registry failure (offline, timeout, non-2xx, malformed JSON, a bogus
|
|
24
|
+
* version string) keeps the embedded fallback range. The registry never fails
|
|
25
|
+
* a scaffold.
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* Fallback ranges for every third-party package the templates emit (API +
|
|
29
|
+
* web UI). The major of each range is also the TEMPLATE-COMPATIBLE major: the
|
|
30
|
+
* generated code is written for it, and `latest` is accepted only on that
|
|
31
|
+
* major. Bump a range to a new major only together with the templates.
|
|
32
|
+
*/
|
|
33
|
+
export declare const THIRD_PARTY_VERSIONS: Readonly<Record<string, string>>;
|
|
34
|
+
export declare const DEFAULT_REGISTRY = "https://registry.npmjs.org";
|
|
35
|
+
/** pnpm 11's default `minimumReleaseAge`, in minutes (one day). */
|
|
36
|
+
export declare const DEFAULT_MINIMUM_RELEASE_AGE_MINUTES = 1440;
|
|
37
|
+
/** Packages whose latest is always taken, regardless of major. */
|
|
38
|
+
export declare const alwaysLatest: (name: string) => boolean;
|
|
39
|
+
/** The major the templates are written for, derived from the fallback range. */
|
|
40
|
+
export declare function compatibleMajor(fallbackRange: string): number | undefined;
|
|
41
|
+
export interface HeldBackVersion {
|
|
42
|
+
name: string;
|
|
43
|
+
/** The registry's latest version (a new major the templates don't target). */
|
|
44
|
+
latest: string;
|
|
45
|
+
/** The range kept instead. */
|
|
46
|
+
range: string;
|
|
47
|
+
}
|
|
48
|
+
export interface FreshVersion {
|
|
49
|
+
name: string;
|
|
50
|
+
/** The registry's latest version (same major, but possibly inside the release-age window). */
|
|
51
|
+
latest: string;
|
|
52
|
+
/** The range kept instead. */
|
|
53
|
+
range: string;
|
|
54
|
+
/** `recent`: published inside the window; `unknown`: its age could not be proven. */
|
|
55
|
+
reason: 'recent' | 'unknown';
|
|
56
|
+
}
|
|
57
|
+
export interface VersionResolution {
|
|
58
|
+
/** Final range per package name (resolved or fallback). */
|
|
59
|
+
versions: Record<string, string>;
|
|
60
|
+
/** Packages set to `^<latest>` from the registry. */
|
|
61
|
+
resolved: string[];
|
|
62
|
+
/** Packages that kept their fallback range because the registry failed. */
|
|
63
|
+
failed: string[];
|
|
64
|
+
/** Third-party packages whose latest is a new major — fallback kept. */
|
|
65
|
+
heldBack: HeldBackVersion[];
|
|
66
|
+
/**
|
|
67
|
+
* Third-party packages whose latest may be younger than the release-age
|
|
68
|
+
* window (pnpm `minimumReleaseAge`) — fallback kept so installs stay possible.
|
|
69
|
+
*/
|
|
70
|
+
tooFresh: FreshVersion[];
|
|
71
|
+
/** Registry actually queried. */
|
|
72
|
+
registry: string;
|
|
73
|
+
}
|
|
74
|
+
export interface ResolveLatestOptions {
|
|
75
|
+
/** Injectable fetch (tests). Default: the global fetch. */
|
|
76
|
+
fetch?: typeof globalThis.fetch;
|
|
77
|
+
/** Registry base URL. Default: `npm_config_registry`, else the public npm registry. */
|
|
78
|
+
registry?: string;
|
|
79
|
+
/** Per-request timeout in ms. Default 5000. */
|
|
80
|
+
timeoutMs?: number;
|
|
81
|
+
/** Cap on the whole resolution in ms. Default 15000. */
|
|
82
|
+
overallTimeoutMs?: number;
|
|
83
|
+
/** Requests in flight at once. Default 8. */
|
|
84
|
+
concurrency?: number;
|
|
85
|
+
/**
|
|
86
|
+
* Release-age window in minutes (pnpm `minimumReleaseAge`): a third-party
|
|
87
|
+
* latest not provably older than this keeps its fallback range. Default:
|
|
88
|
+
* `pnpm_config_minimum_release_age` / `npm_config_minimum_release_age`, else
|
|
89
|
+
* 1440 (pnpm 11's default). `0` disables the check (no probe requests).
|
|
90
|
+
*/
|
|
91
|
+
minimumReleaseAge?: number;
|
|
92
|
+
/** Clock used when the registry sends no `Date` header (tests). Default: Date.now. */
|
|
93
|
+
now?: () => number;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* The release-age window in minutes: an explicit option, else the pnpm/npm
|
|
97
|
+
* config env var (a `pnpm create` exports its config), else pnpm 11's default.
|
|
98
|
+
*/
|
|
99
|
+
export declare function minimumReleaseAgeMinutes(explicit?: number, env?: NodeJS.ProcessEnv): number;
|
|
100
|
+
/**
|
|
101
|
+
* The registry to query: an explicit option, else `npm_config_registry` (npm
|
|
102
|
+
* and pnpm export it to `create` binaries, so a private mirror is honored),
|
|
103
|
+
* else the public registry. Trailing slashes are normalized; anything that is
|
|
104
|
+
* not an http(s) URL falls back to the default.
|
|
105
|
+
*/
|
|
106
|
+
export declare function registryUrl(explicit?: string, env?: NodeJS.ProcessEnv): string;
|
|
107
|
+
/** `<registry>/<name>/latest`, with a scoped name's `/` encoded as `%2f`. */
|
|
108
|
+
export declare const latestUrl: (registry: string, name: string) => string;
|
|
109
|
+
/** `<registry>/<name>` — the packument (probed with HEAD for its `last-modified`). */
|
|
110
|
+
export declare const packumentUrl: (registry: string, name: string) => string;
|
|
111
|
+
/**
|
|
112
|
+
* Resolves the latest published version of every package in `fallbacks`
|
|
113
|
+
* (name → fallback range) through a small pool of parallel requests. Never throws: a package the registry
|
|
114
|
+
* cannot answer for keeps its fallback range and is listed in `failed`.
|
|
115
|
+
*/
|
|
116
|
+
export declare function resolveLatestVersions(fallbacks: Readonly<Record<string, string>>, options?: ResolveLatestOptions): Promise<VersionResolution>;
|
|
117
|
+
/**
|
|
118
|
+
* Rewrites the dependency ranges of a generated package.json with the
|
|
119
|
+
* resolved versions (keys, order and every other field untouched).
|
|
120
|
+
*/
|
|
121
|
+
export declare function applyVersions(packageJsonText: string, versions: Readonly<Record<string, string>>): string;
|
|
122
|
+
/** Every dependency/devDependency name → range declared in the given package.json texts. */
|
|
123
|
+
export declare function collectDependencies(packageJsonTexts: readonly string[]): Record<string, string>;
|
|
124
|
+
/** Human-readable lines summarizing a resolution (for the CLI). */
|
|
125
|
+
export declare function describeResolution(resolution: VersionResolution): string[];
|