create-zenbu-app 0.0.6 → 0.0.8
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 +50 -3
- package/dist/index.mjs +349 -40
- package/package.json +9 -6
- package/templates/plugin/_gitignore +3 -0
- package/templates/plugin/package.json +24 -0
- package/templates/plugin/src/main/services/{{projectName}}.ts.tmpl +9 -0
- package/templates/plugin/tsconfig.json +14 -0
- package/templates/plugin/zenbu.plugin.ts.tmpl +6 -0
- package/{template → templates/tailwind}/package.json +1 -1
- package/templates/tailwind/src/main/schema.ts.tmpl +13 -0
- package/{template/src/main/services/app.ts.tmpl → templates/tailwind/src/main/services/init.ts.tmpl} +3 -3
- package/{template → templates/tailwind}/src/renderer/App.tsx.tmpl +7 -9
- package/{template → templates/tailwind}/src/renderer/main.tsx.tmpl +3 -3
- package/{template → templates/tailwind}/zenbu.config.ts.tmpl +7 -6
- package/templates/vanilla/_gitignore +8 -0
- package/templates/vanilla/electron-builder.json +12 -0
- package/templates/vanilla/package.json +45 -0
- package/templates/vanilla/src/main/schema.ts.tmpl +13 -0
- package/templates/vanilla/src/main/services/init.ts.tmpl +11 -0
- package/templates/vanilla/src/main/services/repo.ts.tmpl +11 -0
- package/templates/vanilla/src/renderer/App.tsx.tmpl +83 -0
- package/templates/vanilla/src/renderer/app.css +143 -0
- package/templates/vanilla/src/renderer/index.html +12 -0
- package/templates/vanilla/src/renderer/installing.html +118 -0
- package/templates/vanilla/src/renderer/main.tsx.tmpl +10 -0
- package/templates/vanilla/src/renderer/splash.html +22 -0
- package/templates/vanilla/tsconfig.json.tmpl +18 -0
- package/templates/vanilla/vite.config.ts.tmpl +16 -0
- package/templates/vanilla/zenbu.config.ts.tmpl +56 -0
- package/LICENSE +0 -11
- package/template/src/main/schema.ts.tmpl +0 -16
- /package/{template → templates/tailwind}/_gitignore +0 -0
- /package/{template → templates/tailwind}/electron-builder.json +0 -0
- /package/{template → templates/tailwind}/src/main/services/repo.ts.tmpl +0 -0
- /package/{template → templates/tailwind}/src/renderer/app.css +0 -0
- /package/{template → templates/tailwind}/src/renderer/index.html +0 -0
- /package/{template → templates/tailwind}/src/renderer/installing.html +0 -0
- /package/{template → templates/tailwind}/src/renderer/splash.html +0 -0
- /package/{template → templates/tailwind}/tsconfig.json.tmpl +0 -0
- /package/{template → templates/tailwind}/vite.config.ts.tmpl +0 -0
package/README.md
CHANGED
|
@@ -5,9 +5,56 @@ Scaffold a new [Zenbu](https://github.com/zenbu-labs/zenbu.js) app.
|
|
|
5
5
|
```bash
|
|
6
6
|
pnpm create zenbu-app my-app
|
|
7
7
|
cd my-app
|
|
8
|
-
pnpm install
|
|
9
8
|
pnpm dev
|
|
10
9
|
```
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
## Interactive mode
|
|
12
|
+
|
|
13
|
+
Run with no arguments to be prompted for a project name and config options:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pnpm create zenbu-app
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
You'll be asked for:
|
|
20
|
+
|
|
21
|
+
- **Project name** — defaults to `my-zenbu-app` (just press enter to accept).
|
|
22
|
+
- **Use Tailwind CSS?** — defaults to `yes`.
|
|
23
|
+
|
|
24
|
+
Each prompt's default is selected when you press enter, so a default scaffold
|
|
25
|
+
is just `enter, enter, enter`.
|
|
26
|
+
|
|
27
|
+
## Flags
|
|
28
|
+
|
|
29
|
+
| Flag | Description |
|
|
30
|
+
|---|---|
|
|
31
|
+
| `--yes`, `-y` | Skip every prompt and take each option's default. With no project name, scaffolds into the current directory. |
|
|
32
|
+
| `--no-install` | Skip the post-copy `<pm> install` step. |
|
|
33
|
+
|
|
34
|
+
A few common invocations:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pnpm create zenbu-app # interactive, then scaffolds ./my-zenbu-app
|
|
38
|
+
pnpm create zenbu-app my-app # interactive options, scaffolds ./my-app
|
|
39
|
+
pnpm create zenbu-app . # interactive options, scaffolds into cwd
|
|
40
|
+
pnpm create zenbu-app --yes # all defaults, scaffolds into cwd
|
|
41
|
+
pnpm create zenbu-app my-app --yes # all defaults, scaffolds ./my-app
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Templates
|
|
45
|
+
|
|
46
|
+
The CLI ships full per-config copies of the project under `templates/<slug>/`
|
|
47
|
+
— there are no in-template conditionals. Today:
|
|
48
|
+
|
|
49
|
+
- `templates/tailwind/` — Tailwind CSS v4 wired up via `@tailwindcss/vite`.
|
|
50
|
+
- `templates/vanilla/` — plain CSS, no utility framework.
|
|
51
|
+
|
|
52
|
+
The selected slug is computed from the answered config options (Tailwind
|
|
53
|
+
contributes `tailwind`; the empty set falls back to `vanilla`).
|
|
54
|
+
|
|
55
|
+
## Package manager support
|
|
56
|
+
|
|
57
|
+
The detected invoking package manager is recorded in `zenbu.config.ts` and
|
|
58
|
+
used for the post-copy install. pnpm, npm, yarn, and bun are all supported,
|
|
59
|
+
but the bundled `.app` re-installs from the project's lockfile at first
|
|
60
|
+
launch — so currently the lockfile must be a pnpm one.
|
package/dist/index.mjs
CHANGED
|
@@ -3,15 +3,85 @@ import path from "node:path";
|
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { spawnSync } from "node:child_process";
|
|
6
|
+
import * as p from "@clack/prompts";
|
|
6
7
|
//#region src/index.ts
|
|
7
8
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const flagsSet = new Set(
|
|
11
|
-
const positional =
|
|
9
|
+
const TEMPLATES_DIR = path.resolve(__dirname, "..", "templates");
|
|
10
|
+
const rawArgv = process.argv.slice(2);
|
|
11
|
+
const flagsSet = /* @__PURE__ */ new Set();
|
|
12
|
+
const positional = [];
|
|
13
|
+
const dependsOn = [];
|
|
14
|
+
/**
|
|
15
|
+
* Parse flags. `--depends-on NAME=PATH` is consumed positionally because it
|
|
16
|
+
* carries a value; everything else is a boolean flag or a positional arg.
|
|
17
|
+
*/
|
|
18
|
+
for (let i = 0; i < rawArgv.length; i++) {
|
|
19
|
+
const arg = rawArgv[i];
|
|
20
|
+
if (arg === "--depends-on" || arg === "--dependsOn") {
|
|
21
|
+
const value = rawArgv[++i];
|
|
22
|
+
if (!value) {
|
|
23
|
+
console.error("create-zenbu-app: --depends-on requires NAME=PATH");
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
dependsOn.push(parseDependsOn(value));
|
|
27
|
+
} else if (arg.startsWith("--depends-on=") || arg.startsWith("--dependsOn=")) dependsOn.push(parseDependsOn(arg.slice(arg.indexOf("=") + 1)));
|
|
28
|
+
else if (arg.startsWith("-")) flagsSet.add(arg);
|
|
29
|
+
else positional.push(arg);
|
|
30
|
+
}
|
|
12
31
|
const yes = flagsSet.has("--yes") || flagsSet.has("-y");
|
|
13
32
|
const noInstall = flagsSet.has("--no-install");
|
|
33
|
+
const noGit = flagsSet.has("--no-git");
|
|
34
|
+
const pluginMode = flagsSet.has("--plugin");
|
|
35
|
+
const noAddToHost = flagsSet.has("--no-add-to-host");
|
|
36
|
+
if (dependsOn.length > 0 && !pluginMode) {
|
|
37
|
+
console.error("create-zenbu-app: --depends-on is only valid with --plugin");
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
function parseDependsOn(raw) {
|
|
41
|
+
const eq = raw.indexOf("=");
|
|
42
|
+
if (eq < 0) {
|
|
43
|
+
console.error(`create-zenbu-app: --depends-on must be of the form NAME=PATH (got "${raw}")`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
const name = raw.slice(0, eq).trim();
|
|
47
|
+
const rel = raw.slice(eq + 1).trim();
|
|
48
|
+
if (!name) {
|
|
49
|
+
console.error("create-zenbu-app: --depends-on NAME may not be empty");
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
if (!rel) {
|
|
53
|
+
console.error("create-zenbu-app: --depends-on PATH may not be empty");
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
const abs = path.isAbsolute(rel) ? rel : path.resolve(process.cwd(), rel);
|
|
57
|
+
if (!fs.existsSync(abs)) {
|
|
58
|
+
console.error(`create-zenbu-app: --depends-on path does not exist: ${abs}`);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
name,
|
|
63
|
+
from: abs
|
|
64
|
+
};
|
|
65
|
+
}
|
|
14
66
|
const ZENBU_LOCAL_CORE = process.env.ZENBU_LOCAL_CORE;
|
|
67
|
+
const CONFIG_OPTIONS = [{
|
|
68
|
+
id: "tailwind",
|
|
69
|
+
default: true,
|
|
70
|
+
ask: () => p.confirm({
|
|
71
|
+
message: "Use Tailwind CSS?",
|
|
72
|
+
initialValue: true
|
|
73
|
+
}),
|
|
74
|
+
slug: (v) => v ? "tailwind" : null
|
|
75
|
+
}];
|
|
76
|
+
function resolveSlug(answers) {
|
|
77
|
+
const parts = [];
|
|
78
|
+
for (const opt of CONFIG_OPTIONS) {
|
|
79
|
+
const value = answers[opt.id];
|
|
80
|
+
const fragment = opt.slug(value);
|
|
81
|
+
if (fragment) parts.push(fragment);
|
|
82
|
+
}
|
|
83
|
+
return parts.length > 0 ? parts.join("-") : "vanilla";
|
|
84
|
+
}
|
|
15
85
|
/**
|
|
16
86
|
* Detect the PM that invoked `create-zenbu-app`. Order:
|
|
17
87
|
* 1. `process.versions.bun` → bun (covers `bunx create-zenbu-app`).
|
|
@@ -68,22 +138,61 @@ function probeVersion(pm) {
|
|
|
68
138
|
const match = (res.stdout ?? "").trim().match(/\d+\.\d+\.\d+(?:[-+][\w.]+)?/);
|
|
69
139
|
return match ? match[0] : null;
|
|
70
140
|
}
|
|
71
|
-
function renderTemplate(value,
|
|
72
|
-
return value.replace(/\{\{
|
|
141
|
+
function renderTemplate(value, ctx) {
|
|
142
|
+
return value.replace(/\{\{(\w+)\}\}/g, (full, key) => {
|
|
143
|
+
return Object.prototype.hasOwnProperty.call(ctx, key) ? ctx[key] : full;
|
|
144
|
+
});
|
|
73
145
|
}
|
|
74
|
-
|
|
146
|
+
/**
|
|
147
|
+
* Copy a template directory tree to `dest`. Both file *contents* and
|
|
148
|
+
* file/directory *names* go through `renderTemplate`, so a template can
|
|
149
|
+
* place a file at e.g. `src/main/services/{{projectName}}.ts.tmpl` and have
|
|
150
|
+
* it land at `src/main/services/<projectName>.ts`. `.tmpl` is stripped.
|
|
151
|
+
*/
|
|
152
|
+
function copyDirSync(src, dest, ctx) {
|
|
75
153
|
fs.mkdirSync(dest, { recursive: true });
|
|
76
154
|
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
77
155
|
const srcPath = path.join(src, entry.name);
|
|
78
|
-
|
|
156
|
+
let destName = entry.name.endsWith(".tmpl") ? entry.name.slice(0, -5) : entry.name;
|
|
157
|
+
destName = renderTemplate(destName, ctx);
|
|
79
158
|
const destPath = path.join(dest, destName);
|
|
80
|
-
if (entry.isDirectory()) copyDirSync(srcPath, destPath,
|
|
159
|
+
if (entry.isDirectory()) copyDirSync(srcPath, destPath, ctx);
|
|
81
160
|
else {
|
|
82
161
|
const content = fs.readFileSync(srcPath, "utf8");
|
|
83
|
-
fs.writeFileSync(destPath, renderTemplate(content,
|
|
162
|
+
fs.writeFileSync(destPath, renderTemplate(content, ctx));
|
|
84
163
|
}
|
|
85
164
|
}
|
|
86
165
|
}
|
|
166
|
+
function toPascalCase(s) {
|
|
167
|
+
return s.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
168
|
+
}
|
|
169
|
+
function relPosix(fromDir, toFile) {
|
|
170
|
+
let r = path.relative(fromDir, toFile).split(path.sep).join("/");
|
|
171
|
+
if (!r.startsWith(".")) r = "./" + r;
|
|
172
|
+
return r;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Render a plugin's `dependsOn` literal for the scaffolded `zenbu.plugin.ts`.
|
|
176
|
+
* Returns either an empty string (no deps → field omitted) or a leading-`\n`
|
|
177
|
+
* fragment that slots in after the `services:` line, e.g.
|
|
178
|
+
*
|
|
179
|
+
* \n dependsOn: [\n { name: "app", from: "../../zenbu.config.ts" },\n ],
|
|
180
|
+
*
|
|
181
|
+
* Each `from` is rewritten relative to `pluginDir` so the generated file is
|
|
182
|
+
* stable across moves of the surrounding workspace.
|
|
183
|
+
*/
|
|
184
|
+
function renderDependsOn(pluginDir, deps) {
|
|
185
|
+
if (deps.length === 0) return "";
|
|
186
|
+
const lines = [];
|
|
187
|
+
lines.push("");
|
|
188
|
+
lines.push(" dependsOn: [");
|
|
189
|
+
for (const d of deps) {
|
|
190
|
+
const fromRel = relPosix(pluginDir, d.from);
|
|
191
|
+
lines.push(` { name: ${JSON.stringify(d.name)}, from: ${JSON.stringify(fromRel)} },`);
|
|
192
|
+
}
|
|
193
|
+
lines.push(" ],");
|
|
194
|
+
return lines.join("\n");
|
|
195
|
+
}
|
|
87
196
|
/**
|
|
88
197
|
* Replace the `// {{packageManager}}` marker in the scaffolded
|
|
89
198
|
* `zenbu.config.ts` with a real `packageManager: { ... }` line. Idempotent
|
|
@@ -97,13 +206,29 @@ function seedPackageManager(projectDir, pm) {
|
|
|
97
206
|
const replaced = original.replace(/\/\/\s*\{\{packageManager\}\}/, literal);
|
|
98
207
|
if (replaced !== original) fs.writeFileSync(configPath, replaced);
|
|
99
208
|
}
|
|
100
|
-
|
|
209
|
+
/**
|
|
210
|
+
* Pin `@zenbujs/core` to a local checkout. For apps the dep lives in
|
|
211
|
+
* `dependencies`; for plugins it's in `devDependencies` (plugins peer on
|
|
212
|
+
* core).
|
|
213
|
+
*/
|
|
214
|
+
function rewireToLocalCore(projectDir, corePath, isPlugin) {
|
|
101
215
|
const pkgPath = path.join(projectDir, "package.json");
|
|
102
216
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
103
|
-
|
|
104
|
-
pkg
|
|
217
|
+
const bucket = isPlugin ? "devDependencies" : "dependencies";
|
|
218
|
+
pkg[bucket] = pkg[bucket] ?? {};
|
|
219
|
+
pkg[bucket]["@zenbujs/core"] = `link:${corePath}`;
|
|
105
220
|
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
106
221
|
}
|
|
222
|
+
/** Walk upward from `fromDir` looking for an ancestor with a `.git` dir. */
|
|
223
|
+
function findGitRoot(fromDir) {
|
|
224
|
+
let dir = path.resolve(fromDir);
|
|
225
|
+
while (true) {
|
|
226
|
+
if (fs.existsSync(path.join(dir, ".git"))) return dir;
|
|
227
|
+
const parent = path.dirname(dir);
|
|
228
|
+
if (parent === dir) return null;
|
|
229
|
+
dir = parent;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
107
232
|
function gitInitWithInitialCommit(projectDir) {
|
|
108
233
|
if (spawnSync("git", [
|
|
109
234
|
"init",
|
|
@@ -133,6 +258,66 @@ function gitInitWithInitialCommit(projectDir) {
|
|
|
133
258
|
}
|
|
134
259
|
});
|
|
135
260
|
}
|
|
261
|
+
function appendPluginToHostConfig(hostConfigPath, entry) {
|
|
262
|
+
if (!fs.existsSync(hostConfigPath)) return "missing-file";
|
|
263
|
+
const raw = fs.readFileSync(hostConfigPath, "utf8");
|
|
264
|
+
const pluginsMatch = raw.match(/\bplugins\s*:\s*\[/);
|
|
265
|
+
if (!pluginsMatch) return "unsafe-shape";
|
|
266
|
+
const openIdx = pluginsMatch.index + pluginsMatch[0].length - 1;
|
|
267
|
+
let depth = 1;
|
|
268
|
+
let i = openIdx + 1;
|
|
269
|
+
while (i < raw.length && depth > 0) {
|
|
270
|
+
const ch = raw[i];
|
|
271
|
+
if (ch === "\"" || ch === "'" || ch === "`") {
|
|
272
|
+
const quote = ch;
|
|
273
|
+
i++;
|
|
274
|
+
while (i < raw.length && raw[i] !== quote) {
|
|
275
|
+
if (raw[i] === "\\") i++;
|
|
276
|
+
i++;
|
|
277
|
+
}
|
|
278
|
+
i++;
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (ch === "/" && raw[i + 1] === "/") {
|
|
282
|
+
while (i < raw.length && raw[i] !== "\n") i++;
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (ch === "/" && raw[i + 1] === "*") {
|
|
286
|
+
i += 2;
|
|
287
|
+
while (i < raw.length - 1 && !(raw[i] === "*" && raw[i + 1] === "/")) i++;
|
|
288
|
+
i += 2;
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
if (ch === "[") depth++;
|
|
292
|
+
else if (ch === "]") depth--;
|
|
293
|
+
i++;
|
|
294
|
+
}
|
|
295
|
+
if (depth !== 0) return "unsafe-shape";
|
|
296
|
+
const closeIdx = i - 1;
|
|
297
|
+
const arrayBody = raw.slice(openIdx + 1, closeIdx);
|
|
298
|
+
if (/\.\.\./.test(arrayBody)) return "unsafe-shape";
|
|
299
|
+
if (arrayBody.includes(JSON.stringify(entry))) return "already-present";
|
|
300
|
+
const lines = arrayBody.split("\n");
|
|
301
|
+
let indent = " ";
|
|
302
|
+
for (const line of lines) {
|
|
303
|
+
if (line.trim().length === 0) continue;
|
|
304
|
+
const m = line.match(/^[ \t]+/);
|
|
305
|
+
if (m) {
|
|
306
|
+
indent = m[0];
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
const beforeClose = raw.slice(0, closeIdx);
|
|
311
|
+
const lastLineStart = beforeClose.lastIndexOf("\n") + 1;
|
|
312
|
+
const closeLineIndent = beforeClose.slice(lastLineStart).match(/^[ \t]*/)[0];
|
|
313
|
+
let head = raw.slice(0, closeIdx).replace(/\s*$/, "");
|
|
314
|
+
if (head.length > 0 && !head.endsWith(",") && !head.endsWith("[")) head += ",";
|
|
315
|
+
const insertion = `\n${indent}${JSON.stringify(entry)},\n${closeLineIndent}`;
|
|
316
|
+
const next = head + insertion + raw.slice(closeIdx);
|
|
317
|
+
if (next === raw) return "already-present";
|
|
318
|
+
fs.writeFileSync(hostConfigPath, next);
|
|
319
|
+
return "added";
|
|
320
|
+
}
|
|
136
321
|
/**
|
|
137
322
|
* Run `<pm> install` in the freshly-scaffolded project so the user can go
|
|
138
323
|
* straight to `pnpm dev`/`bun dev`/etc. without the extra step. We run with
|
|
@@ -146,52 +331,176 @@ function runInstall(projectDir, pm) {
|
|
|
146
331
|
stdio: "inherit"
|
|
147
332
|
}).status === 0;
|
|
148
333
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
334
|
+
/** Run `<pm> exec zen link [extraArgs...]` in `cwd`. */
|
|
335
|
+
function runZenLink(cwd, pm, extraArgs = []) {
|
|
336
|
+
const args = [
|
|
337
|
+
"exec",
|
|
338
|
+
"zen",
|
|
339
|
+
"link",
|
|
340
|
+
...extraArgs
|
|
341
|
+
];
|
|
342
|
+
return spawnSync(pm.type === "yarn" && pm.version.startsWith("1.") ? pm.type : pm.type, pm.type === "yarn" && pm.version.startsWith("1.") ? [
|
|
343
|
+
"zen",
|
|
344
|
+
"link",
|
|
345
|
+
...extraArgs
|
|
346
|
+
] : args, {
|
|
347
|
+
cwd,
|
|
348
|
+
stdio: "inherit"
|
|
349
|
+
}).status === 0;
|
|
350
|
+
}
|
|
351
|
+
function bail(reason) {
|
|
352
|
+
p.cancel(reason);
|
|
353
|
+
process.exit(1);
|
|
354
|
+
}
|
|
355
|
+
function validateProjectName(value) {
|
|
356
|
+
const trimmed = (value ?? "").trim();
|
|
357
|
+
if (!trimmed) return "Project name is required.";
|
|
358
|
+
if (trimmed === ".") return void 0;
|
|
359
|
+
if (/[\\/]/.test(trimmed)) return "Project name cannot contain slashes.";
|
|
360
|
+
if (/\s/.test(trimmed)) return "Project name cannot contain whitespace.";
|
|
361
|
+
if (/^[._]/.test(trimmed)) return "Project name cannot start with '.' or '_'.";
|
|
362
|
+
}
|
|
363
|
+
async function promptProjectName() {
|
|
364
|
+
const result = await p.text({
|
|
365
|
+
message: "Project name?",
|
|
366
|
+
placeholder: "my-zenbu-app",
|
|
367
|
+
defaultValue: "my-zenbu-app",
|
|
368
|
+
validate: validateProjectName
|
|
369
|
+
});
|
|
370
|
+
if (p.isCancel(result)) bail("Scaffolding cancelled.");
|
|
371
|
+
return result;
|
|
372
|
+
}
|
|
373
|
+
async function promptOptions() {
|
|
374
|
+
const answers = {};
|
|
375
|
+
for (const opt of CONFIG_OPTIONS) {
|
|
376
|
+
const value = await opt.ask();
|
|
377
|
+
if (p.isCancel(value)) bail("Scaffolding cancelled.");
|
|
378
|
+
answers[opt.id] = value;
|
|
154
379
|
}
|
|
380
|
+
return answers;
|
|
381
|
+
}
|
|
382
|
+
function defaultAnswers() {
|
|
383
|
+
const answers = {};
|
|
384
|
+
for (const opt of CONFIG_OPTIONS) answers[opt.id] = opt.default;
|
|
385
|
+
return answers;
|
|
386
|
+
}
|
|
387
|
+
async function main() {
|
|
388
|
+
p.intro(pluginMode ? "create-zenbu-app (plugin)" : "create-zenbu-app");
|
|
389
|
+
let projectName;
|
|
390
|
+
if (positional[0]) projectName = positional[0];
|
|
391
|
+
else if (yes) projectName = ".";
|
|
392
|
+
else projectName = await promptProjectName();
|
|
155
393
|
const projectDir = path.resolve(process.cwd(), projectName);
|
|
156
394
|
const displayName = path.basename(projectDir);
|
|
157
395
|
if (fs.existsSync(projectDir)) {
|
|
158
396
|
const entries = fs.readdirSync(projectDir).filter((e) => e !== ".git");
|
|
159
|
-
const allowedExisting = projectName === "." && fs.existsSync(path.join(projectDir, "package.json"));
|
|
160
|
-
if (entries.length > 0 && !allowedExisting) {
|
|
161
|
-
console.error(`Error: directory "${projectName}" already exists and is not empty.`);
|
|
162
|
-
process.exit(1);
|
|
163
|
-
}
|
|
397
|
+
const allowedExisting = (projectName === "." || projectDir === process.cwd()) && fs.existsSync(path.join(projectDir, "package.json"));
|
|
398
|
+
if (entries.length > 0 && !allowedExisting) bail(`Directory "${projectName}" already exists and is not empty.`);
|
|
164
399
|
}
|
|
400
|
+
let templateDir;
|
|
401
|
+
let slug;
|
|
402
|
+
if (pluginMode) {
|
|
403
|
+
slug = "plugin";
|
|
404
|
+
templateDir = path.join(TEMPLATES_DIR, "plugin");
|
|
405
|
+
} else {
|
|
406
|
+
slug = resolveSlug(yes ? defaultAnswers() : await promptOptions());
|
|
407
|
+
templateDir = path.join(TEMPLATES_DIR, slug);
|
|
408
|
+
}
|
|
409
|
+
if (!fs.existsSync(templateDir)) bail(`No template found for configuration "${slug}".`);
|
|
165
410
|
const pm = detectPackageManager();
|
|
166
|
-
|
|
167
|
-
if (pm.fallback)
|
|
168
|
-
else
|
|
169
|
-
|
|
170
|
-
|
|
411
|
+
p.log.step(`Scaffolding Zenbu ${pluginMode ? "plugin" : "app"} in "${displayName}" (template: ${slug})`);
|
|
412
|
+
if (pm.fallback) p.log.info(`couldn't detect invoking package manager; defaulting to ${pm.type}@${pm.version}.`);
|
|
413
|
+
else p.log.info(`detected ${pm.type}@${pm.version} as the invoking package manager`);
|
|
414
|
+
const ctx = pluginMode ? {
|
|
415
|
+
projectName: displayName,
|
|
416
|
+
className: toPascalCase(displayName),
|
|
417
|
+
dependsOn: renderDependsOn(projectDir, dependsOn)
|
|
418
|
+
} : { projectName: displayName };
|
|
419
|
+
copyDirSync(templateDir, projectDir, ctx);
|
|
171
420
|
const gi = path.join(projectDir, "_gitignore");
|
|
172
421
|
if (fs.existsSync(gi)) fs.renameSync(gi, path.join(projectDir, ".gitignore"));
|
|
173
|
-
seedPackageManager(projectDir, pm);
|
|
422
|
+
if (!pluginMode) seedPackageManager(projectDir, pm);
|
|
174
423
|
if (ZENBU_LOCAL_CORE) {
|
|
175
424
|
const corePath = path.resolve(ZENBU_LOCAL_CORE);
|
|
176
|
-
rewireToLocalCore(projectDir, corePath);
|
|
177
|
-
|
|
425
|
+
rewireToLocalCore(projectDir, corePath, pluginMode);
|
|
426
|
+
p.log.info(`linked @zenbujs/core -> ${corePath}`);
|
|
178
427
|
}
|
|
179
428
|
let installed = false;
|
|
180
429
|
if (!noInstall) {
|
|
181
|
-
|
|
430
|
+
p.log.step(`running ${pm.type} install`);
|
|
182
431
|
installed = runInstall(projectDir, pm);
|
|
183
|
-
if (!installed)
|
|
432
|
+
if (!installed) p.log.warn(`${pm.type} install failed; you can retry manually after the scaffold completes.`);
|
|
433
|
+
}
|
|
434
|
+
const hostsToLink = /* @__PURE__ */ new Set();
|
|
435
|
+
if (pluginMode && !noAddToHost) for (const dep of dependsOn) {
|
|
436
|
+
if (!path.basename(dep.from).startsWith("zenbu.config.")) continue;
|
|
437
|
+
const hostDir = path.dirname(dep.from);
|
|
438
|
+
const pluginManifestRel = relPosix(hostDir, path.join(projectDir, "zenbu.plugin.ts"));
|
|
439
|
+
let accept = yes;
|
|
440
|
+
if (!yes) {
|
|
441
|
+
const result = await p.confirm({
|
|
442
|
+
message: `Add "${pluginManifestRel}" to ${path.relative(process.cwd(), dep.from) || dep.from} plugins:?`,
|
|
443
|
+
initialValue: true
|
|
444
|
+
});
|
|
445
|
+
if (p.isCancel(result)) bail("Scaffolding cancelled.");
|
|
446
|
+
accept = !!result;
|
|
447
|
+
}
|
|
448
|
+
if (!accept) continue;
|
|
449
|
+
switch (appendPluginToHostConfig(dep.from, pluginManifestRel)) {
|
|
450
|
+
case "added":
|
|
451
|
+
p.log.success(`wired into ${dep.from}`);
|
|
452
|
+
hostsToLink.add(hostDir);
|
|
453
|
+
break;
|
|
454
|
+
case "already-present":
|
|
455
|
+
p.log.info(`already listed in ${dep.from}`);
|
|
456
|
+
hostsToLink.add(hostDir);
|
|
457
|
+
break;
|
|
458
|
+
case "missing-file":
|
|
459
|
+
p.log.warn(`host config not found: ${dep.from}`);
|
|
460
|
+
break;
|
|
461
|
+
case "unsafe-shape":
|
|
462
|
+
p.log.warn(`${dep.from}: couldn't safely edit plugins:[]. Add this manually:\n ${JSON.stringify(pluginManifestRel)},`);
|
|
463
|
+
break;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
if (installed) if (pluginMode) if (hostsToLink.size > 0) for (const hostDir of hostsToLink) {
|
|
467
|
+
p.log.step(`running zen link in ${hostDir}`);
|
|
468
|
+
if (!runZenLink(hostDir, pm)) p.log.warn(`zen link failed in ${hostDir}`);
|
|
469
|
+
}
|
|
470
|
+
else {
|
|
471
|
+
p.log.step(`running zen link --plugin .`);
|
|
472
|
+
if (!runZenLink(projectDir, pm, ["--plugin", "."])) p.log.warn(`zen link --plugin . failed`);
|
|
473
|
+
}
|
|
474
|
+
else {
|
|
475
|
+
p.log.step(`running ${pm.type} run link`);
|
|
476
|
+
if (spawnSync(pm.type, ["run", "link"], {
|
|
477
|
+
cwd: projectDir,
|
|
478
|
+
stdio: "inherit"
|
|
479
|
+
}).status !== 0) p.log.warn(`${pm.type} run link failed; you can retry manually after the scaffold completes.`);
|
|
480
|
+
}
|
|
481
|
+
if (!noGit) {
|
|
482
|
+
const ancestorRepo = findGitRoot(projectDir);
|
|
483
|
+
if (ancestorRepo) p.log.info(`inside existing repo at ${ancestorRepo} — skipping git init`);
|
|
484
|
+
else {
|
|
485
|
+
let doInit = yes;
|
|
486
|
+
if (!yes) {
|
|
487
|
+
const result = await p.confirm({
|
|
488
|
+
message: "Initialize a git repo here?",
|
|
489
|
+
initialValue: true
|
|
490
|
+
});
|
|
491
|
+
if (p.isCancel(result)) bail("Scaffolding cancelled.");
|
|
492
|
+
doInit = !!result;
|
|
493
|
+
}
|
|
494
|
+
if (doInit) gitInitWithInitialCommit(projectDir);
|
|
495
|
+
}
|
|
184
496
|
}
|
|
185
|
-
if (!fs.existsSync(path.join(projectDir, ".git"))) gitInitWithInitialCommit(projectDir);
|
|
186
497
|
const cdHint = projectName === "." ? "" : `cd ${displayName} && `;
|
|
187
|
-
|
|
188
|
-
|
|
498
|
+
const next = pluginMode ? installed ? `${cdHint}${pm.type} run typecheck` : `${cdHint}${pm.type} install` : installed ? `${cdHint}${pm.type} dev` : `${cdHint}${pm.type} install\n ${cdHint}${pm.type} dev`;
|
|
499
|
+
p.outro(`Done. Next:\n\n ${next}\n`);
|
|
189
500
|
}
|
|
190
|
-
|
|
191
|
-
main();
|
|
192
|
-
} catch (err) {
|
|
501
|
+
main().catch((err) => {
|
|
193
502
|
console.error("\nError:", err?.message ?? err);
|
|
194
503
|
process.exit(1);
|
|
195
|
-
}
|
|
504
|
+
});
|
|
196
505
|
//#endregion
|
|
197
506
|
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-zenbu-app",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"description": "Scaffold a new Zenbu app",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -11,8 +11,12 @@
|
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
13
|
"dist/",
|
|
14
|
-
"
|
|
14
|
+
"templates/"
|
|
15
15
|
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsdown",
|
|
18
|
+
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
19
|
+
},
|
|
16
20
|
"keywords": [
|
|
17
21
|
"zenbu",
|
|
18
22
|
"framework",
|
|
@@ -29,8 +33,7 @@
|
|
|
29
33
|
"tsdown": "^0.21.10",
|
|
30
34
|
"typescript": "^5.0.0"
|
|
31
35
|
},
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@clack/prompts": "^1.3.0"
|
|
35
38
|
}
|
|
36
|
-
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{projectName}}",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"link": "zen link --plugin .",
|
|
8
|
+
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
9
|
+
},
|
|
10
|
+
"peerDependencies": {
|
|
11
|
+
"@zenbujs/core": "*",
|
|
12
|
+
"react": ">=18",
|
|
13
|
+
"react-dom": ">=18"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@zenbujs/core": "^0.0.12",
|
|
17
|
+
"@types/node": "^22.0.0",
|
|
18
|
+
"@types/react": "^19.0.0",
|
|
19
|
+
"@types/react-dom": "^19.0.0",
|
|
20
|
+
"react": "^19.0.0",
|
|
21
|
+
"react-dom": "^19.0.0",
|
|
22
|
+
"typescript": "^5.4.5"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"jsx": "react-jsx",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"noEmit": true,
|
|
11
|
+
"types": ["node"]
|
|
12
|
+
},
|
|
13
|
+
"include": ["src"]
|
|
14
|
+
}
|
package/{template/src/main/services/app.ts.tmpl → templates/tailwind/src/main/services/init.ts.tmpl}
RENAMED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { Service } from "@zenbujs/core/runtime"
|
|
2
2
|
import { WindowService } from "@zenbujs/core/services"
|
|
3
3
|
|
|
4
|
-
export class
|
|
5
|
-
key: "
|
|
4
|
+
export class InitService extends Service.create({
|
|
5
|
+
key: "init",
|
|
6
6
|
deps: { window: WindowService },
|
|
7
7
|
}) {
|
|
8
8
|
async evaluate() {
|
|
9
|
-
await this.ctx.window.openView({ type: "
|
|
9
|
+
await this.ctx.window.openView({ type: "entrypoint" })
|
|
10
10
|
}
|
|
11
11
|
}
|