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
|
@@ -0,0 +1,293 @@
|
|
|
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 const THIRD_PARTY_VERSIONS = {
|
|
34
|
+
'@tailwindcss/vite': '^4.3.3',
|
|
35
|
+
'@types/node': '^26.6.2',
|
|
36
|
+
'@types/react': '^19.3.0',
|
|
37
|
+
'@types/react-dom': '^19.3.0',
|
|
38
|
+
'@vitejs/plugin-react': '^6.1.1',
|
|
39
|
+
'pino-pretty': '^13.1.3',
|
|
40
|
+
react: '^19.3.0',
|
|
41
|
+
'react-dom': '^19.3.0',
|
|
42
|
+
tailwindcss: '^4.3.3',
|
|
43
|
+
tsx: '^4.23.13',
|
|
44
|
+
typescript: '^7.0.2',
|
|
45
|
+
vite: '^8.3.0',
|
|
46
|
+
vitest: '^5.0.1',
|
|
47
|
+
zod: '^4.6.5',
|
|
48
|
+
};
|
|
49
|
+
export const DEFAULT_REGISTRY = 'https://registry.npmjs.org';
|
|
50
|
+
/** pnpm 11's default `minimumReleaseAge`, in minutes (one day). */
|
|
51
|
+
export const DEFAULT_MINIMUM_RELEASE_AGE_MINUTES = 1440;
|
|
52
|
+
const ABBREVIATED_METADATA = 'application/vnd.npm.install-v1+json';
|
|
53
|
+
const PER_REQUEST_TIMEOUT_MS = 5_000;
|
|
54
|
+
const OVERALL_TIMEOUT_MS = 15_000;
|
|
55
|
+
/**
|
|
56
|
+
* Requests in flight at once. A burst of ~35 simultaneous TLS handshakes to one
|
|
57
|
+
* host is markedly slower (and flakier) on poor links than a small pool that
|
|
58
|
+
* reuses keep-alive connections.
|
|
59
|
+
*/
|
|
60
|
+
const CONCURRENCY = 8;
|
|
61
|
+
/** A stable `x.y.z` release (pre-releases and build metadata are refused). */
|
|
62
|
+
const STABLE_SEMVER = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
63
|
+
const CARET_RANGE = /^\^(0|[1-9]\d*)\.\d+\.\d+$/;
|
|
64
|
+
/** Packages whose latest is always taken, regardless of major. */
|
|
65
|
+
export const alwaysLatest = (name) => name.startsWith('@basaltkit/') || name === 'create-basalt';
|
|
66
|
+
/** The major the templates are written for, derived from the fallback range. */
|
|
67
|
+
export function compatibleMajor(fallbackRange) {
|
|
68
|
+
const match = CARET_RANGE.exec(fallbackRange);
|
|
69
|
+
return match ? Number(match[1]) : undefined;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The release-age window in minutes: an explicit option, else the pnpm/npm
|
|
73
|
+
* config env var (a `pnpm create` exports its config), else pnpm 11's default.
|
|
74
|
+
*/
|
|
75
|
+
export function minimumReleaseAgeMinutes(explicit, env = process.env) {
|
|
76
|
+
if (explicit !== undefined && Number.isFinite(explicit) && explicit >= 0)
|
|
77
|
+
return explicit;
|
|
78
|
+
for (const key of ['pnpm_config_minimum_release_age', 'npm_config_minimum_release_age']) {
|
|
79
|
+
const raw = env[key];
|
|
80
|
+
if (raw !== undefined && /^\d+$/.test(raw.trim()))
|
|
81
|
+
return Number(raw.trim());
|
|
82
|
+
}
|
|
83
|
+
return DEFAULT_MINIMUM_RELEASE_AGE_MINUTES;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The registry to query: an explicit option, else `npm_config_registry` (npm
|
|
87
|
+
* and pnpm export it to `create` binaries, so a private mirror is honored),
|
|
88
|
+
* else the public registry. Trailing slashes are normalized; anything that is
|
|
89
|
+
* not an http(s) URL falls back to the default.
|
|
90
|
+
*/
|
|
91
|
+
export function registryUrl(explicit, env = process.env) {
|
|
92
|
+
const candidate = explicit ?? env['npm_config_registry'] ?? env['NPM_CONFIG_REGISTRY'];
|
|
93
|
+
if (!candidate)
|
|
94
|
+
return DEFAULT_REGISTRY;
|
|
95
|
+
try {
|
|
96
|
+
const url = new URL(candidate);
|
|
97
|
+
if (url.protocol !== 'https:' && url.protocol !== 'http:')
|
|
98
|
+
return DEFAULT_REGISTRY;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return DEFAULT_REGISTRY;
|
|
102
|
+
}
|
|
103
|
+
let end = candidate.length;
|
|
104
|
+
while (end > 0 && candidate[end - 1] === '/')
|
|
105
|
+
end--;
|
|
106
|
+
return candidate.slice(0, end);
|
|
107
|
+
}
|
|
108
|
+
/** `<registry>/<name>/latest`, with a scoped name's `/` encoded as `%2f`. */
|
|
109
|
+
export const latestUrl = (registry, name) => `${registry}/${name.replaceAll('/', '%2f')}/latest`;
|
|
110
|
+
/** `<registry>/<name>` — the packument (probed with HEAD for its `last-modified`). */
|
|
111
|
+
export const packumentUrl = (registry, name) => `${registry}/${name.replaceAll('/', '%2f')}`;
|
|
112
|
+
/**
|
|
113
|
+
* Whether `name`'s latest is provably older than `windowMs`. A packument's
|
|
114
|
+
* `last-modified` is never older than its latest publish, so an old header
|
|
115
|
+
* proves maturity. Age is measured against the registry's own `Date` header
|
|
116
|
+
* when present (immune to local clock skew). Never throws.
|
|
117
|
+
*/
|
|
118
|
+
async function probeAge(fetchImpl, registry, name, windowMs, now, signal) {
|
|
119
|
+
try {
|
|
120
|
+
const response = await fetchImpl(packumentUrl(registry, name), {
|
|
121
|
+
method: 'HEAD',
|
|
122
|
+
headers: { accept: ABBREVIATED_METADATA },
|
|
123
|
+
signal,
|
|
124
|
+
});
|
|
125
|
+
if (!response.ok)
|
|
126
|
+
return 'unknown';
|
|
127
|
+
const modified = Date.parse(response.headers.get('last-modified') ?? '');
|
|
128
|
+
if (Number.isNaN(modified))
|
|
129
|
+
return 'unknown';
|
|
130
|
+
const serverNow = Date.parse(response.headers.get('date') ?? '');
|
|
131
|
+
const reference = Number.isNaN(serverNow) ? now() : serverNow;
|
|
132
|
+
return reference - modified >= windowMs ? 'mature' : 'recent';
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return 'unknown';
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async function lookupLatest(fetchImpl, registry, name, signal) {
|
|
139
|
+
try {
|
|
140
|
+
const response = await fetchImpl(latestUrl(registry, name), {
|
|
141
|
+
headers: { accept: 'application/json' },
|
|
142
|
+
signal,
|
|
143
|
+
});
|
|
144
|
+
if (response.status >= 500 || response.status === 429)
|
|
145
|
+
return 'transient';
|
|
146
|
+
if (!response.ok)
|
|
147
|
+
return 'miss';
|
|
148
|
+
const body = (await response.json());
|
|
149
|
+
const version = body?.version;
|
|
150
|
+
return typeof version === 'string' && STABLE_SEMVER.test(version) ? { version } : 'miss';
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
// Network error, per-request timeout, or malformed JSON.
|
|
154
|
+
return 'transient';
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* The latest stable version of `name`, or undefined. A transient failure
|
|
159
|
+
* (network error, timeout, 5xx/429) is retried once — on slow links the first
|
|
160
|
+
* wave of cold TLS handshakes is the part that times out, and the retry rides
|
|
161
|
+
* a warm keep-alive connection. The overall signal bounds both attempts.
|
|
162
|
+
*/
|
|
163
|
+
async function fetchLatest(fetchImpl, registry, name, timeoutMs, overall) {
|
|
164
|
+
for (let attempt = 0; attempt < 2 && !overall.aborted; attempt++) {
|
|
165
|
+
const result = await lookupLatest(fetchImpl, registry, name, AbortSignal.any([AbortSignal.timeout(timeoutMs), overall]));
|
|
166
|
+
if (result === 'miss')
|
|
167
|
+
return undefined;
|
|
168
|
+
if (result !== 'transient')
|
|
169
|
+
return result.version;
|
|
170
|
+
}
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Resolves the latest published version of every package in `fallbacks`
|
|
175
|
+
* (name → fallback range) through a small pool of parallel requests. Never throws: a package the registry
|
|
176
|
+
* cannot answer for keeps its fallback range and is listed in `failed`.
|
|
177
|
+
*/
|
|
178
|
+
export async function resolveLatestVersions(fallbacks, options = {}) {
|
|
179
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
180
|
+
const registry = registryUrl(options.registry);
|
|
181
|
+
const versions = { ...fallbacks };
|
|
182
|
+
const resolved = [];
|
|
183
|
+
const failed = [];
|
|
184
|
+
const heldBack = [];
|
|
185
|
+
const tooFresh = [];
|
|
186
|
+
const names = Object.keys(fallbacks).sort();
|
|
187
|
+
const windowMs = minimumReleaseAgeMinutes(options.minimumReleaseAge) * 60_000;
|
|
188
|
+
const now = options.now ?? Date.now;
|
|
189
|
+
const timeoutMs = options.timeoutMs ?? PER_REQUEST_TIMEOUT_MS;
|
|
190
|
+
if (typeof fetchImpl !== 'function') {
|
|
191
|
+
return { versions, resolved, failed: names, heldBack, tooFresh, registry };
|
|
192
|
+
}
|
|
193
|
+
/** A third-party latest on the template-compatible major (else undefined). */
|
|
194
|
+
const compatible = (name, version) => {
|
|
195
|
+
if (alwaysLatest(name))
|
|
196
|
+
return true;
|
|
197
|
+
const major = compatibleMajor(fallbacks[name]);
|
|
198
|
+
return major !== undefined && Number(version.split('.')[0]) === major;
|
|
199
|
+
};
|
|
200
|
+
const overall = new AbortController();
|
|
201
|
+
const cap = setTimeout(() => overall.abort(), options.overallTimeoutMs ?? OVERALL_TIMEOUT_MS);
|
|
202
|
+
try {
|
|
203
|
+
const latest = [];
|
|
204
|
+
const age = [];
|
|
205
|
+
let next = 0;
|
|
206
|
+
const worker = async () => {
|
|
207
|
+
while (next < names.length) {
|
|
208
|
+
const i = next++;
|
|
209
|
+
const name = names[i];
|
|
210
|
+
const version = await fetchLatest(fetchImpl, registry, name, timeoutMs, overall.signal);
|
|
211
|
+
latest[i] = version;
|
|
212
|
+
age[i] =
|
|
213
|
+
version === undefined || alwaysLatest(name) || windowMs === 0 || !compatible(name, version)
|
|
214
|
+
? 'mature'
|
|
215
|
+
: await probeAge(fetchImpl, registry, name, windowMs, now, AbortSignal.any([AbortSignal.timeout(timeoutMs), overall.signal]));
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
const pool = Math.max(1, Math.min(options.concurrency ?? CONCURRENCY, names.length));
|
|
219
|
+
await Promise.all(Array.from({ length: pool }, worker));
|
|
220
|
+
names.forEach((name, i) => {
|
|
221
|
+
const version = latest[i];
|
|
222
|
+
const fallback = fallbacks[name];
|
|
223
|
+
if (version === undefined) {
|
|
224
|
+
failed.push(name);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (!compatible(name, version)) {
|
|
228
|
+
heldBack.push({ name, latest: version, range: fallback });
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
const verdict = age[i];
|
|
232
|
+
if (verdict === 'recent' || verdict === 'unknown') {
|
|
233
|
+
tooFresh.push({ name, latest: version, range: fallback, reason: verdict });
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
versions[name] = `^${version}`;
|
|
237
|
+
resolved.push(name);
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
finally {
|
|
241
|
+
clearTimeout(cap);
|
|
242
|
+
}
|
|
243
|
+
return { versions, resolved, failed, heldBack, tooFresh, registry };
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Rewrites the dependency ranges of a generated package.json with the
|
|
247
|
+
* resolved versions (keys, order and every other field untouched).
|
|
248
|
+
*/
|
|
249
|
+
export function applyVersions(packageJsonText, versions) {
|
|
250
|
+
const pkg = JSON.parse(packageJsonText);
|
|
251
|
+
for (const field of ['dependencies', 'devDependencies']) {
|
|
252
|
+
const deps = pkg[field];
|
|
253
|
+
if (!deps)
|
|
254
|
+
continue;
|
|
255
|
+
for (const name of Object.keys(deps)) {
|
|
256
|
+
const range = versions[name];
|
|
257
|
+
if (range !== undefined)
|
|
258
|
+
deps[name] = range;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return `${JSON.stringify(pkg, null, 2)}\n`;
|
|
262
|
+
}
|
|
263
|
+
/** Every dependency/devDependency name → range declared in the given package.json texts. */
|
|
264
|
+
export function collectDependencies(packageJsonTexts) {
|
|
265
|
+
const all = {};
|
|
266
|
+
for (const text of packageJsonTexts) {
|
|
267
|
+
const pkg = JSON.parse(text);
|
|
268
|
+
Object.assign(all, pkg.dependencies, pkg.devDependencies);
|
|
269
|
+
}
|
|
270
|
+
return all;
|
|
271
|
+
}
|
|
272
|
+
/** Human-readable lines summarizing a resolution (for the CLI). */
|
|
273
|
+
export function describeResolution(resolution) {
|
|
274
|
+
const lines = [];
|
|
275
|
+
if (resolution.resolved.length > 0) {
|
|
276
|
+
lines.push(`Resolved ${resolution.resolved.length} dependencies to their latest published versions:`, ` ${resolution.resolved.map((name) => `${name}@${resolution.versions[name]}`).join(', ')}`);
|
|
277
|
+
}
|
|
278
|
+
for (const held of resolution.heldBack) {
|
|
279
|
+
lines.push(`Note: ${held.name} ${held.latest} is a new major this template does not target yet — kept ${held.range}.`);
|
|
280
|
+
}
|
|
281
|
+
const recent = resolution.tooFresh.filter((entry) => entry.reason === 'recent');
|
|
282
|
+
const unknown = resolution.tooFresh.filter((entry) => entry.reason === 'unknown');
|
|
283
|
+
for (const fresh of recent) {
|
|
284
|
+
lines.push(`Note: ${fresh.name} ${fresh.latest} was published inside pnpm's minimumReleaseAge window — kept ${fresh.range} (pnpm installs the newest mature version).`);
|
|
285
|
+
}
|
|
286
|
+
if (unknown.length > 0) {
|
|
287
|
+
lines.push(`Note: could not prove the release age of ${unknown.map((entry) => `${entry.name}@${entry.latest}`).join(', ')} — kept the bundled range(s) so minimumReleaseAge cannot block the install.`);
|
|
288
|
+
}
|
|
289
|
+
if (resolution.failed.length > 0) {
|
|
290
|
+
lines.push(`Warning: could not reach ${resolution.registry} for ${resolution.failed.length} package(s) — used the bundled fallback ranges for: ${resolution.failed.join(', ')}.`);
|
|
291
|
+
}
|
|
292
|
+
return lines;
|
|
293
|
+
}
|
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,15 +10,44 @@ 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;
|
|
34
|
+
/**
|
|
35
|
+
* An app-specific env-var prefix derived from the project name
|
|
36
|
+
* (`my-saas` → `MY_SAAS`), suggested for variables whose generic names
|
|
37
|
+
* (`DATABASE_URL`, …) other projects export too.
|
|
38
|
+
*/
|
|
39
|
+
export declare function envPrefix(name: string): string;
|
|
18
40
|
export declare function envExample(options: ProjectOptions): string;
|
|
19
41
|
export declare function appTs(options: ProjectOptions): string;
|
|
20
42
|
export declare function routesTs(options: ProjectOptions): string;
|
|
21
43
|
export declare function serverTs(): string;
|
|
44
|
+
/**
|
|
45
|
+
* `pnpm dev` entrypoint. Opts into NODE_ENV=development (dev secret defaults,
|
|
46
|
+
* pretty logs) unless NODE_ENV is already set, then starts the server. Kept out
|
|
47
|
+
* of server.ts on purpose: `pnpm start` runs server.ts directly, where an unset
|
|
48
|
+
* NODE_ENV counts as production and secrets are required (fail-closed).
|
|
49
|
+
*/
|
|
50
|
+
export declare function devTs(): string;
|
|
22
51
|
export declare function basaltBin(): string;
|
|
23
52
|
export declare function appTest(options: ProjectOptions): string;
|
|
24
53
|
export declare function readme(options: ProjectOptions): string;
|
|
@@ -28,11 +57,25 @@ export declare function readme(options: ProjectOptions): string;
|
|
|
28
57
|
* project root automatically; Claude Desktop uses the same server entry.
|
|
29
58
|
*/
|
|
30
59
|
export declare function mcpJson(_options: ProjectOptions): string;
|
|
60
|
+
/**
|
|
61
|
+
* Keeps secrets and local state out of Docker build contexts, so a
|
|
62
|
+
* `COPY . .` can never bake `.env` or private keys into an image layer. Every
|
|
63
|
+
* rule is `**`-prefixed: .dockerignore patterns are anchored at the context
|
|
64
|
+
* root, so a bare `.env` or `*.pem` would still let `prisma/.env` or
|
|
65
|
+
* `certs/server.key` through.
|
|
66
|
+
*/
|
|
67
|
+
export declare function dockerignore(): string;
|
|
31
68
|
export declare function gitignore(): string;
|
|
32
69
|
/**
|
|
33
70
|
* pnpm settings: esbuild's build script is required by tsx;
|
|
34
71
|
* msgpackr-extract (optional native accelerator via BullMQ) is declined —
|
|
35
72
|
* msgpackr falls back to pure JS. With --ui, `web` is a workspace member so
|
|
36
73
|
* its own dependencies resolve.
|
|
74
|
+
*
|
|
75
|
+
* pnpm 11 notes (BK-002), spelled out in the file because both bite silently:
|
|
76
|
+
* `minimumReleaseAgeExclude` is evaluated first-match-wins BY PACKAGE NAME, so
|
|
77
|
+
* per-version exclusions of one package must be a single `||` union entry; and
|
|
78
|
+
* `verifyDepsBeforeRun` defaults to `install`, so any `pnpm <script>` may run
|
|
79
|
+
* `pnpm install` first — `warn` is offered commented out, never set for you.
|
|
37
80
|
*/
|
|
38
81
|
export declare function pnpmWorkspaceYaml(options: ProjectOptions): string;
|