create-quadrokit 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,16 +16,20 @@ bun create-quadrokit/src/index.ts --template dashboard --dir /path/to/my-app
16
16
  | `--template <name>` | `dashboard` \| `website` \| `ecommerce` \| `admin-shell` |
17
17
  | `--dir <path>` | Target directory (created; must be empty) |
18
18
  | `--keep-workspace` | Keep `workspace:*` in `package.json` (for development inside the QuadroKit monorepo only) |
19
+ | `--git` / `--no-git` | Initialize a git repo after scaffolding (skip the prompt) |
20
+ | `--install` / `--no-install` | Run `bun install` or `npm install` after scaffolding (skip the prompt) |
19
21
 
20
- Interactive mode: run without `--template` / `--dir` to be prompted.
22
+ On start, the CLI prints a short **prerequisites** check (Node.js, Bun, Git). Node 18+ is required; Bun is optional (falls back to npm for installs). Git is only needed if you choose to init a repository.
23
+
24
+ Interactive mode: run without `--template` / `--dir` to be prompted for those; you are then asked about **git** and **install** unless overridden with the flags above.
21
25
 
22
26
  ## What it does
23
27
 
24
- 1. Copies the chosen template from `create-quadrokit/templates/<name>` when installed from npm, or `packages/templates/<name>` when you run the CLI from the monorepo (skips `node_modules`, `dist`).
25
- 2. Copies `packages/sample-client/generated` `<dir>/.quadrokit/generated`.
26
- 3. Rewrites `src/lib/quadro-client.ts` to import from `../../.quadrokit/generated/client.gen.js`.
27
- 4. Removes `@quadrokit/sample-client` and rewrites other `workspace:*` deps to `^0.2.0` (unless `--keep-workspace`).
28
- 5. Writes `QUADROKIT.md` with proxy and `quadrokit-client generate` instructions.
28
+ 1. Copies the chosen template from `create-quadrokit/templates/<name>` when installed from npm, or `packages/templates/<name>` when you run the CLI from the monorepo (skips `node_modules`, `dist`, `.quadrokit`, lockfiles).
29
+ 2. Rewrites `src/lib/quadro-client.ts` to import from `../../.quadrokit/generated/client.gen.js`.
30
+ 3. Removes `@quadrokit/sample-client` and rewrites `workspace:*` on `@quadrokit/*` deps to `^<version>` matching **create-quadrokit**’s own `package.json` `version` (run `bun run version:set` at repo root to bump public packages) unless `--keep-workspace`.
31
+ 4. Writes `QUADROKIT.md` with proxy and `quadrokit:generate` instructions. Does **not** copy `.quadrokit/generated` run `bun run quadrokit:generate` after install.
32
+ 5. Optionally runs `git init` and installs dependencies (`bun` if available, otherwise `npm`).
29
33
 
30
34
  ## Published usage (future)
31
35
 
@@ -1,8 +1,58 @@
1
1
  #!/usr/bin/env node
2
+ import { spawnSync } from 'node:child_process';
3
+ import { readFileSync } from 'node:fs';
2
4
  import { cp, mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises';
3
5
  import path from 'node:path';
4
6
  import { fileURLToPath } from 'node:url';
5
7
  import prompts from 'prompts';
8
+ const MIN_NODE_MAJOR = 18;
9
+ function cmdVersion(cmd, args) {
10
+ const r = spawnSync(cmd, args, { encoding: 'utf8', shell: false });
11
+ if (r.error || r.status !== 0)
12
+ return null;
13
+ const line = r.stdout.trim().split('\n')[0];
14
+ return line || null;
15
+ }
16
+ function nodeMajor() {
17
+ const m = /^v?(\d+)/.exec(process.version);
18
+ return m ? Number(m[1]) : 0;
19
+ }
20
+ function checkPrereqs() {
21
+ const nodeVersion = process.version;
22
+ const nodeOk = nodeMajor() >= MIN_NODE_MAJOR;
23
+ const bunVersion = cmdVersion('bun', ['--version']);
24
+ const gitVersion = cmdVersion('git', ['--version']);
25
+ return {
26
+ nodeOk,
27
+ nodeVersion,
28
+ bunOk: bunVersion !== null,
29
+ bunVersion,
30
+ gitOk: gitVersion !== null,
31
+ gitVersion,
32
+ };
33
+ }
34
+ function printWelcome() {
35
+ console.log(`
36
+ ${'✨'.repeat(2)} ${'\u001b[1mcreate-quadrokit\u001b[0m'} ${'✨'.repeat(2)}
37
+ ${'🚀'.repeat(2)} Scaffold a QuadroKit Vite + React app for 4D REST
38
+ `);
39
+ }
40
+ function printPrereqReport(p) {
41
+ console.log(' Prerequisites');
42
+ const line = (ok, emoji, label, detail) => {
43
+ const mark = ok ? '✅' : '⚠️ ';
44
+ console.log(` ${mark} ${emoji} ${label}: ${detail}`);
45
+ };
46
+ line(p.nodeOk, '📗', 'Node.js', p.nodeOk ? p.nodeVersion : `${p.nodeVersion} (need >= ${MIN_NODE_MAJOR})`);
47
+ line(p.bunOk, '🥟', 'Bun', p.bunVersion ?? 'not found (you can use npm install instead)');
48
+ line(p.gitOk, '📌', 'Git', p.gitVersion ?? 'not found (needed only to init a repository)');
49
+ console.log('');
50
+ }
51
+ async function runInDir(cwd, cmd, args) {
52
+ const shell = process.platform === 'win32';
53
+ const r = spawnSync(cmd, args, { cwd, stdio: 'inherit', shell });
54
+ return r.status === 0;
55
+ }
6
56
  const TEMPLATES = ['dashboard', 'website', 'ecommerce', 'admin-shell'];
7
57
  /** Directory of the create-quadrokit package (works when installed under node_modules). */
8
58
  function packageRoot() {
@@ -32,22 +82,13 @@ async function resolveBiomeJsonPath() {
32
82
  }
33
83
  return null;
34
84
  }
35
- async function resolveGeneratedClientDir() {
36
- const pkg = packageRoot();
37
- const bundled = path.join(pkg, 'vendor', 'generated');
38
- if (await pathExists(bundled)) {
39
- return bundled;
40
- }
41
- const monorepo = path.join(pkg, '..', 'packages', 'sample-client', 'generated');
42
- if (await pathExists(monorepo)) {
43
- return monorepo;
44
- }
45
- return null;
46
- }
47
85
  function parseArgs(argv) {
48
86
  let template;
49
87
  let dir;
50
88
  let keepWorkspace = false;
89
+ /** `undefined` = prompt later */
90
+ let initGit;
91
+ let installDeps;
51
92
  for (let i = 0; i < argv.length; i++) {
52
93
  const a = argv[i];
53
94
  if (a === '--template' && argv[i + 1]) {
@@ -59,8 +100,20 @@ function parseArgs(argv) {
59
100
  else if (a === '--keep-workspace') {
60
101
  keepWorkspace = true;
61
102
  }
103
+ else if (a === '--git') {
104
+ initGit = true;
105
+ }
106
+ else if (a === '--no-git') {
107
+ initGit = false;
108
+ }
109
+ else if (a === '--install') {
110
+ installDeps = true;
111
+ }
112
+ else if (a === '--no-install') {
113
+ installDeps = false;
114
+ }
62
115
  }
63
- return { template, dir, keepWorkspace };
116
+ return { template, dir, keepWorkspace, initGit, installDeps };
64
117
  }
65
118
  async function pathExists(p) {
66
119
  try {
@@ -71,11 +124,22 @@ async function pathExists(p) {
71
124
  return false;
72
125
  }
73
126
  }
127
+ const SKIP_TEMPLATE_NAMES = new Set([
128
+ 'node_modules',
129
+ 'dist',
130
+ '.turbo',
131
+ '.quadrokit',
132
+ 'bun.lock',
133
+ 'bun.lockb',
134
+ 'package-lock.json',
135
+ 'pnpm-lock.yaml',
136
+ 'yarn.lock',
137
+ ]);
74
138
  async function copyTemplate(src, dest) {
75
139
  await mkdir(dest, { recursive: true });
76
140
  const entries = await readdir(src, { withFileTypes: true });
77
141
  for (const e of entries) {
78
- if (e.name === 'node_modules' || e.name === 'dist' || e.name === '.turbo') {
142
+ if (SKIP_TEMPLATE_NAMES.has(e.name)) {
79
143
  continue;
80
144
  }
81
145
  const from = path.join(src, e.name);
@@ -88,15 +152,29 @@ async function copyTemplate(src, dest) {
88
152
  }
89
153
  }
90
154
  }
155
+ /** Caret range for published @quadrokit/* deps (matches create-quadrokit release line). */
156
+ function quadrokitPublishedSemverRange() {
157
+ try {
158
+ const raw = readFileSync(path.join(packageRoot(), 'package.json'), 'utf8');
159
+ const v = JSON.parse(raw).version;
160
+ if (v)
161
+ return `^${v}`;
162
+ }
163
+ catch {
164
+ /* ignore */
165
+ }
166
+ return '^0.0.0';
167
+ }
91
168
  function rewriteWorkspaceDeps(pkg) {
169
+ const range = quadrokitPublishedSemverRange();
92
170
  for (const key of ['dependencies', 'devDependencies']) {
93
171
  const deps = pkg[key];
94
172
  if (!deps) {
95
173
  continue;
96
174
  }
97
175
  for (const name of Object.keys(deps)) {
98
- if (deps[name] === 'workspace:*') {
99
- deps[name] = '^0.2.0';
176
+ if (deps[name] === 'workspace:*' && name.startsWith('@quadrokit/')) {
177
+ deps[name] = range;
100
178
  }
101
179
  }
102
180
  }
@@ -141,23 +219,13 @@ async function writeStandaloneBiome(dest, keepWorkspace) {
141
219
  }
142
220
  async function writeQuadroClientImport(dest) {
143
221
  const p = path.join(dest, 'src', 'lib', 'quadro-client.ts');
144
- const body = `import { createClient } from '../../.quadrokit/generated/client.gen.js';
222
+ const body = `import { createClient } from '../../.quadrokit/generated/client.gen.mjs';
145
223
 
146
224
  /** Same-origin \`/rest\` in dev (Vite proxy) and production (reverse proxy). */
147
225
  export const quadro = createClient({ baseURL: '/rest' });
148
226
  `;
149
227
  await writeFile(p, body, 'utf8');
150
228
  }
151
- async function seedGenerated(dest) {
152
- const genSrc = await resolveGeneratedClientDir();
153
- const genDest = path.join(dest, '.quadrokit', 'generated');
154
- if (!genSrc) {
155
- console.warn('Warning: sample generated client not found — run `quadrokit-client generate` after install.');
156
- return;
157
- }
158
- await mkdir(path.join(dest, '.quadrokit'), { recursive: true });
159
- await cp(genSrc, genDest, { recursive: true });
160
- }
161
229
  async function writeReadme(dest, template) {
162
230
  const text = `# ${path.basename(dest)}
163
231
 
@@ -165,14 +233,20 @@ Created with **create-quadrokit** (template: \`${template}\`).
165
233
 
166
234
  ## Next steps
167
235
 
168
- 1. \`cd ${path.basename(dest)}\` and \`bun install\`
169
- 2. Copy \`.env.example\` to \`.env\` and set \`VITE_4D_ORIGIN\` to your 4D web server.
170
- 3. Regenerate the typed client when your REST catalog changes:
236
+ 1. \`cd ${path.basename(dest)}\` and install dependencies (\`bun install\` or \`npm install\`).
237
+ 2. Generate the typed REST client (required before \`dev\` / \`build\`):
238
+
239
+ \`\`\`bash
240
+ bun run quadrokit:generate
241
+ \`\`\`
242
+
243
+ This uses \`assets/catalog.placeholder.json\` by default (demo catalog from QuadroKit). Point at your live 4D catalog:
171
244
 
172
245
  \`\`\`bash
173
- bunx quadrokit-client generate --url "http://localhost:7080/rest/\\$catalog" --token YOUR_TOKEN --out .quadrokit/generated
246
+ QUADROKIT_CATALOG_URL='http://localhost:7080/rest/\\$catalog' QUADROKIT_CATALOG_TOKEN='…' bun run quadrokit:generate
174
247
  \`\`\`
175
248
 
249
+ 3. Copy \`.env.example\` to \`.env\` and set \`VITE_4D_ORIGIN\` to your 4D web server.
176
250
  4. \`bun run dev\` — the dev server proxies \`/rest\` to \`VITE_4D_ORIGIN\` so **4DSID_** cookies stay same-origin.
177
251
 
178
252
  Production: serve the SPA and reverse-proxy \`/rest\` to 4D on the **same host** as the UI.
@@ -181,7 +255,14 @@ Production: serve the SPA and reverse-proxy \`/rest\` to 4D on the **same host**
181
255
  }
182
256
  async function main() {
183
257
  const argv = process.argv.slice(2);
184
- const { template: tArg, dir: dirArg, keepWorkspace } = parseArgs(argv);
258
+ const { template: tArg, dir: dirArg, keepWorkspace, initGit: gitFlag, installDeps: installFlag, } = parseArgs(argv);
259
+ printWelcome();
260
+ const prereqs = checkPrereqs();
261
+ printPrereqReport(prereqs);
262
+ if (!prereqs.nodeOk) {
263
+ console.error(` ❌ Node.js ${MIN_NODE_MAJOR}+ is required.`);
264
+ process.exit(1);
265
+ }
185
266
  const template = tArg && TEMPLATES.includes(tArg)
186
267
  ? tArg
187
268
  : (await prompts({
@@ -218,19 +299,75 @@ async function main() {
218
299
  }
219
300
  await copyTemplate(src, dest);
220
301
  await writeStandaloneBiome(dest, keepWorkspace);
221
- await seedGenerated(dest);
222
302
  await patchPackageJson(dest, path
223
303
  .basename(dest)
224
304
  .replace(/[^a-z0-9-]/gi, '-')
225
305
  .toLowerCase() || 'quadro-app', keepWorkspace);
226
306
  await writeQuadroClientImport(dest);
227
307
  await writeReadme(dest, template);
228
- console.log(`
229
- Created QuadroKit project at ${dest}
308
+ console.log(`\n 🎉 Project files are ready at ${'\u001b[1m'}${dest}${'\u001b[0m'}\n`);
309
+ let initGit = gitFlag;
310
+ if (initGit === undefined) {
311
+ const g = await prompts({
312
+ type: 'confirm',
313
+ name: 'v',
314
+ message: 'Initialize a git repository here?',
315
+ initial: true,
316
+ });
317
+ if (typeof g.v !== 'boolean') {
318
+ process.exit(1);
319
+ }
320
+ initGit = g.v;
321
+ }
322
+ if (initGit) {
323
+ if (!prereqs.gitOk) {
324
+ console.log(' ⚠️ Skipping git init — Git is not available on your PATH.\n');
325
+ }
326
+ else {
327
+ const ok = await runInDir(dest, 'git', ['init']);
328
+ if (ok) {
329
+ console.log(' ✅ Git repository initialized.\n');
330
+ }
331
+ else {
332
+ console.log(' ⚠️ `git init` failed — you can run it manually later.\n');
333
+ }
334
+ }
335
+ }
336
+ let installDeps = installFlag;
337
+ if (installDeps === undefined) {
338
+ const ins = await prompts({
339
+ type: 'confirm',
340
+ name: 'v',
341
+ message: 'Install dependencies now?',
342
+ initial: true,
343
+ });
344
+ if (typeof ins.v !== 'boolean') {
345
+ process.exit(1);
346
+ }
347
+ installDeps = ins.v;
348
+ }
349
+ if (installDeps) {
350
+ const useBun = prereqs.bunOk;
351
+ const cmd = useBun ? 'bun' : 'npm';
352
+ const args = useBun ? ['install'] : ['install'];
353
+ console.log(` 📦 Running ${cmd} ${args.join(' ')} …\n`);
354
+ const ok = await runInDir(dest, cmd, args);
355
+ if (ok) {
356
+ console.log(` ✅ Dependencies installed with ${cmd}.\n`);
357
+ }
358
+ else {
359
+ console.log(` ⚠️ Install failed — run \`${useBun ? 'bun install' : 'npm install'}\` inside the project.\n`);
360
+ }
361
+ }
362
+ const pm = prereqs.bunOk ? 'bun' : 'npm';
363
+ const runDev = prereqs.bunOk ? 'bun run dev' : 'npm run dev';
364
+ const installLine = installDeps ? ` → ${runDev}` : ` → ${pm} install\n → ${runDev}`;
365
+ console.log(` 🚀🚀 Next steps
366
+ → cd ${path.basename(dest)}
367
+ ${installLine}
368
+ → Copy .env.example → .env and set VITE_4D_ORIGIN
230
369
 
231
- cd ${path.basename(dest)}
232
- bun install
233
- bun run dev
370
+ Happy building! ✨
234
371
  `);
235
372
  }
236
373
  main().catch((e) => {
package/package.json CHANGED
@@ -1,17 +1,16 @@
1
1
  {
2
2
  "name": "create-quadrokit",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Scaffold a QuadroKit Vite + React app from a template",
5
5
  "type": "module",
6
- "bin": "./dist/index.js",
6
+ "bin": "./dist/index.mjs",
7
7
  "files": [
8
8
  "dist",
9
9
  "templates",
10
- "vendor",
11
10
  "biome.monorepo.json"
12
11
  ],
13
12
  "scripts": {
14
- "build": "tsc -p tsconfig.build.json",
13
+ "build": "tsc -p tsconfig.build.json && bun ../scripts/rename-dist-js-to-mjs.ts dist",
15
14
  "typecheck": "tsc -p tsconfig.json --noEmit",
16
15
  "prepublishOnly": "bun run scripts/sync-for-publish.ts && bun run build"
17
16
  },
@@ -34,4 +34,6 @@ Each template has a [`biome.json`](dashboard/biome.json) with `"root": false` an
34
34
 
35
35
  ## Scaffolding outside the monorepo
36
36
 
37
- Use [`create-quadrokit`](../../create-quadrokit/README.md); it copies a template, seeds `.quadrokit/generated`, and rewrites `workspace:*` to published semver.
37
+ Use [`create-quadrokit`](../../create-quadrokit/README.md); it copies a template, rewrites `workspace:*` to published semver, and skips lockfiles and `.quadrokit/` (so new projects run `bun run quadrokit:generate` after install).
38
+
39
+ For **monorepo** template packages, `.quadrokit/generated` is kept (regenerate with `bun run quadrokit:generate` in a template after changing `assets/catalog.placeholder.json`) so `tsc` can typecheck against the demo catalog.
@@ -4,6 +4,7 @@
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {
7
+ "quadrokit:generate": "node scripts/quadrokit-generate.mjs",
7
8
  "dev": "vite",
8
9
  "build": "tsc -b && vite build",
9
10
  "preview": "vite preview",
@@ -4,6 +4,7 @@
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {
7
+ "quadrokit:generate": "node scripts/quadrokit-generate.mjs",
7
8
  "dev": "vite",
8
9
  "build": "tsc -b && vite build",
9
10
  "preview": "vite preview",
@@ -4,6 +4,7 @@
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {
7
+ "quadrokit:generate": "node scripts/quadrokit-generate.mjs",
7
8
  "dev": "vite",
8
9
  "build": "tsc -b && vite build",
9
10
  "preview": "vite preview",
@@ -4,6 +4,7 @@
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {
7
+ "quadrokit:generate": "node scripts/quadrokit-generate.mjs",
7
8
  "dev": "vite",
8
9
  "build": "tsc -b && vite build",
9
10
  "preview": "vite preview",