@waniwani/kit 0.1.6 → 0.1.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.
Files changed (45) hide show
  1. package/README.md +67 -35
  2. package/dist/cli/codegen.js +1105 -0
  3. package/dist/cli/codegen.js.map +1 -0
  4. package/{cli/env.mjs → dist/cli/env.js} +5 -6
  5. package/dist/cli/env.js.map +1 -0
  6. package/{cli/framework.mjs → dist/cli/framework.js} +112 -115
  7. package/dist/cli/framework.js.map +1 -0
  8. package/dist/cli/index.js +378 -0
  9. package/dist/cli/index.js.map +1 -0
  10. package/dist/cli/init.js +642 -0
  11. package/dist/cli/init.js.map +1 -0
  12. package/dist/cli/log.js +156 -0
  13. package/dist/cli/log.js.map +1 -0
  14. package/dist/cli/manifest.js +57 -0
  15. package/dist/cli/manifest.js.map +1 -0
  16. package/{cli/peers.mjs → dist/cli/peers.js} +77 -88
  17. package/dist/cli/peers.js.map +1 -0
  18. package/dist/cli/scan.js +100 -0
  19. package/dist/cli/scan.js.map +1 -0
  20. package/dist/cli/template.js +173 -0
  21. package/dist/cli/template.js.map +1 -0
  22. package/dist/cli/types.js +14 -0
  23. package/dist/cli/types.js.map +1 -0
  24. package/dist/cli/validate.js +328 -0
  25. package/dist/cli/validate.js.map +1 -0
  26. package/dist/cli/vercel.js +103 -0
  27. package/dist/cli/vercel.js.map +1 -0
  28. package/dist/server.d.ts +1 -1
  29. package/dist/server.d.ts.map +1 -1
  30. package/dist/server.js +0 -1
  31. package/dist/server.js.map +1 -1
  32. package/dist/web.d.ts +8 -7
  33. package/dist/web.d.ts.map +1 -1
  34. package/dist/web.js +7 -6
  35. package/dist/web.js.map +1 -1
  36. package/package.json +13 -9
  37. package/src/server.ts +7 -9
  38. package/src/web.tsx +12 -13
  39. package/cli/codegen.mjs +0 -1267
  40. package/cli/index.mjs +0 -409
  41. package/cli/init.mjs +0 -575
  42. package/cli/log.mjs +0 -178
  43. package/cli/scan.mjs +0 -112
  44. package/cli/template.mjs +0 -190
  45. package/cli/validate.mjs +0 -391
package/cli/index.mjs DELETED
@@ -1,409 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * The `waniwani` CLI.
4
- *
5
- * waniwani init scaffold a new app folder and install it
6
- * waniwani check validate the app folder
7
- * waniwani dev check, generate, run the dev server, watch for changes
8
- * waniwani build check, generate, build for production
9
- * waniwani start run the production build
10
- * waniwani eject write the plumbing into the repo and hand it over
11
- *
12
- * Every command scans the app folder, validates it, and generates a complete
13
- * framework project under `.waniwani/`. The app repo owns content; this CLI and
14
- * the runtime own everything else.
15
- */
16
-
17
- import { spawn } from "node:child_process";
18
- import { existsSync, readFileSync, watch } from "node:fs";
19
- import { dirname, join, resolve } from "node:path";
20
- import { fileURLToPath } from "node:url";
21
- import { existingPlumbing, generate } from "./codegen.mjs";
22
- import { loadAppEnv } from "./env.mjs";
23
- import { init } from "./init.mjs";
24
- import { banner, bold, dim, green, printReport, red, yellow } from "./log.mjs";
25
- import { scanApp } from "./scan.mjs";
26
- import { devFilter, FRAMEWORK_ENV, frameworkBin, loadBuildSteps, runBuildSteps, startFilter } from "./framework.mjs";
27
- import { DEFAULT_TEMPLATE, describeTemplate, resolveTemplate } from "./template.mjs";
28
- import { validateApp } from "./validate.mjs";
29
-
30
- const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
31
- const PACKAGE_VERSION = JSON.parse(readFileSync(join(PACKAGE_ROOT, "package.json"), "utf-8")).version;
32
-
33
- /** The commands a human sits and watches. `check` and `eject` are often scripted. */
34
- const BANNERED = new Set(["init", "dev", "build", "start"]);
35
-
36
- /**
37
- * Diagnostics about this CLI's own machinery — which template was resolved, how
38
- * many files it copied, which dependencies were overridden, stack traces. None
39
- * of it is actionable from an app folder, so it is off unless asked for.
40
- */
41
- const DEBUG = Boolean(process.env.WANIWANI_DEBUG);
42
-
43
- /**
44
- * Every `node_modules/.bin` from the generated project up to the filesystem
45
- * root. The framework shells out to `vite` and `tsc` by bare name.
46
- */
47
- function binPath(from) {
48
- const dirs = [];
49
- let current = resolve(from);
50
- while (true) {
51
- dirs.push(join(current, "node_modules", ".bin"));
52
- const parent = dirname(current);
53
- if (parent === current) break;
54
- current = parent;
55
- }
56
- return [...dirs, process.env.PATH].join(":");
57
- }
58
-
59
- /**
60
- * Spawn a child under this CLI's PATH and environment.
61
- *
62
- * `stdoutFilter`/`stderrFilter` pipe that one stream through a line rewriter
63
- * instead of inheriting it — the mechanism that keeps the framework's own
64
- * narration out of this CLI's output (see ./framework.mjs). Every unfiltered
65
- * stream is inherited, so an app's logs and tsc's diagnostics arrive untouched.
66
- *
67
- * `shell` is for the framework's build steps, which name their command as one
68
- * string rather than an argv.
69
- */
70
- function run(command, args, { cwd, env, shell = false, stdoutFilter, stderrFilter } = {}) {
71
- return new Promise((resolvePromise) => {
72
- const child = spawn(command, args, {
73
- cwd,
74
- shell,
75
- stdio: ["inherit", stdoutFilter ? "pipe" : "inherit", stderrFilter ? "pipe" : "inherit"],
76
- env: { ...process.env, PATH: binPath(cwd), ...FRAMEWORK_ENV, ...env },
77
- });
78
- for (const [stream, filter] of [
79
- [child.stdout, stdoutFilter],
80
- [child.stderr, stderrFilter],
81
- ]) {
82
- if (!filter) continue;
83
- stream.setEncoding("utf8");
84
- stream.on("data", filter.write);
85
- // Registered before the resolving listener, so a held partial line is
86
- // emitted before the command reports its exit code.
87
- child.on("close", filter.flush);
88
- }
89
- child.on("close", (code) => resolvePromise(code ?? 1));
90
- child.on("error", (error) => {
91
- console.error(red(`failed to run ${command}: ${error.message}`));
92
- resolvePromise(1);
93
- });
94
- });
95
- }
96
-
97
- /** The template source, most specific wins. */
98
- function templateSource(flags) {
99
- return flags.template ?? process.env.WANIWANI_TEMPLATE ?? DEFAULT_TEMPLATE;
100
- }
101
-
102
- /**
103
- * Report what the runtime changed about the template's package.json. This is
104
- * the fleet-wide fix mechanism made visible: every line is a decision taken
105
- * once here instead of in 30 repos.
106
- *
107
- * Every line is about the plumbing rather than the app, so `dev` and `build`
108
- * print it only under WANIWANI_DEBUG. `eject` prints it unconditionally —
109
- * there the plumbing becomes the app's to maintain.
110
- */
111
- function printOverrides(overrides) {
112
- if (overrides.length === 0) return;
113
- console.log(`\n${dim("runtime overrides on top of the template")}`);
114
- for (const { name, from, to, why, conflict, removed } of overrides) {
115
- const marker = conflict ? yellow("!") : dim("·");
116
- const change = removed ? "removed" : from ? `${from} → ${to}` : `+ ${to}`;
117
- console.log(` ${marker} ${name} ${dim(change)}`);
118
- console.log(` ${dim(why)}`);
119
- }
120
- }
121
-
122
- async function prepare(appRoot, flags, { quiet = false } = {}) {
123
- const app = scanApp(appRoot);
124
- const report = await validateApp(app);
125
-
126
- if (!quiet) {
127
- printReport(app, report);
128
- }
129
- if (!report.ok) {
130
- return null;
131
- }
132
-
133
- // Where the plumbing came from, how much of it there was, and which
134
- // dependencies the runtime overrode are all facts about our own machinery.
135
- // An app author can act on none of them, so they are diagnostics: on under
136
- // WANIWANI_DEBUG, off otherwise. A stale or unreachable template is different
137
- // — that one changes what they are running, so it always shows.
138
- const template = await resolveTemplate(templateSource(flags));
139
- if (!quiet && DEBUG) {
140
- console.log(`\n${dim("template")} ${describeTemplate(template)}`);
141
- }
142
- if (!quiet && template.offline) {
143
- console.log(`${yellow("!")} ${dim("GitHub unreachable — using the cached template")}`);
144
- }
145
-
146
- const { outDir, overrides, fromTemplate, manifest, vercelJson } = generate(app, { template });
147
- // Written into the app's own repo rather than the output, so it is worth a
148
- // line even outside debug: it is a tracked file that appeared.
149
- if (!quiet && vercelJson) {
150
- console.log(`${green("+")} ${bold("vercel.json")} ${dim("— deploy config for a git-connected project")}`);
151
- }
152
- if (!quiet && DEBUG) {
153
- console.log(
154
- `${dim(`${fromTemplate.length} files copied`)} ${dim(
155
- manifest ? "· exclusions from the template's manifest" : "· exclusions from the built-in defaults",
156
- )}`,
157
- );
158
- printOverrides(overrides);
159
- }
160
- return { app, outDir, template };
161
- }
162
-
163
- /**
164
- * Build the generated project for production: compile the server, bundle the
165
- * views, and emit a Vercel Build Output tree under `.vercel/output/` — no
166
- * adapter, no vercel.json, nothing for this CLI to stage afterwards.
167
- *
168
- * The framework's own `build` command renders exactly these steps inside a
169
- * branded UI, so the steps are driven here and reported in this CLI's format.
170
- * When the step list can't be loaded, shelling out is the fallback: the build
171
- * still runs, it just narrates itself.
172
- */
173
- async function build(outDir) {
174
- const steps = await loadBuildSteps(outDir);
175
- if (!steps) {
176
- return run("node", [frameworkBin(), "build"], { cwd: outDir });
177
- }
178
-
179
- console.log(`\n${dim("building for production")}`);
180
- return runBuildSteps(steps, {
181
- root: outDir,
182
- // The steps name their command as one string (`tsc -b --force`), and reach
183
- // for `tsc` and `vite` by bare name.
184
- runShell: (command) => run(command, [], { cwd: outDir, shell: true }),
185
- });
186
- }
187
-
188
- /**
189
- * Write the plumbing into the app repo and step out of the way. What comes out
190
- * is an ordinary project on the underlying framework, driven by its own CLI: a
191
- * Dockerfile, a vercel.json, and the runtime vendored as readable source. No
192
- * dependency on this CLI or on Waniwani remains.
193
- */
194
- async function eject(appRoot, flags) {
195
- const app = scanApp(appRoot);
196
- const report = await validateApp(app);
197
- printReport(app, report);
198
- if (!report.ok) return 1;
199
-
200
- const outDir = flags.out ? resolve(flags.out) : appRoot;
201
- const inPlace = outDir === appRoot;
202
-
203
- // Which files count as plumbing depends on what the template ships, so the
204
- // template has to be resolved before the question can be asked.
205
- const template = await resolveTemplate(templateSource(flags));
206
- console.log(`\n${dim("template")} ${describeTemplate(template)}`);
207
-
208
- const clashes = existingPlumbing(outDir, template);
209
- if (clashes.length > 0 && !flags.force) {
210
- console.error(`\n${red("✗")} ${bold("would overwrite existing files")}\n`);
211
- for (const file of clashes) {
212
- console.error(` ${file}`);
213
- }
214
- console.error(`\n${dim("pass --force to overwrite, or --out <dir> to write elsewhere")}`);
215
- return 1;
216
- }
217
-
218
- const { written, overrides, fromTemplate, moved } = generate(app, {
219
- template,
220
- layout: "eject",
221
- outDir,
222
- });
223
-
224
- console.log(`\n${green("✓")} ${bold("Ejected")} ${dim(`→ ${outDir}`)}\n`);
225
- for (const file of written) {
226
- console.log(` ${file}`);
227
- }
228
- console.log(` ${dim(`+ ${fromTemplate.length} files from the template`)}`);
229
- printOverrides(overrides);
230
-
231
- console.log(`\n${bold("What changed")}`);
232
- if (moved.length > 0) {
233
- console.log(
234
- ` ${dim("·")} your source ${bold("moved")} under ${bold("src/app/")}: ${moved.join(", ")}`,
235
- );
236
- console.log(
237
- ` ${dim("the framework compiles from src/ — nothing outside it can be an input")}`,
238
- );
239
- }
240
- console.log(` ${dim("·")} the runtime is now yours, vendored as source in ${bold("src/_runtime/")}`);
241
- console.log(` ${dim("·")} @waniwani/kit imports point at src/_runtime/ — drop the dependency`);
242
- console.log(
243
- ` ${dim("·")} widgets/<name>/ no longer becomes a view — add ${bold("src/views/<name>.tsx")} by hand`,
244
- );
245
- if (inPlace) {
246
- console.log(` ${dim("·")} ${bold(".waniwani/")} is dead weight — delete it`);
247
- }
248
-
249
- console.log(`\n${bold("From here")}`);
250
- console.log(` npm install && npm run dev`);
251
- console.log(`\n${yellow("!")} ${dim("one way — nothing turns an ejected repo back")}`);
252
- return 0;
253
- }
254
-
255
- /**
256
- * Mirror app-folder edits into the build output; nodemon and Vite do the rest.
257
- * The template is resolved once at startup and reused, so a dev loop never
258
- * touches the network.
259
- */
260
- function watchApp(appRoot, template) {
261
- let pending = null;
262
- const rebuild = () => {
263
- clearTimeout(pending);
264
- pending = setTimeout(async () => {
265
- const app = scanApp(appRoot);
266
- const report = await validateApp(app);
267
- if (!report.ok) {
268
- printReport(app, report);
269
- return;
270
- }
271
- // Rewriting the output restarts nodemon and triggers Vite HMR.
272
- generate(app, { template });
273
- console.log(dim(`[waniwani] regenerated ${new Date().toLocaleTimeString()}`));
274
- }, 120);
275
- };
276
-
277
- for (const dir of ["tools", "widgets", "flows", "api"]) {
278
- const path = join(appRoot, dir);
279
- if (existsSync(path)) {
280
- watch(path, { recursive: true }, rebuild);
281
- }
282
- }
283
- const config = join(appRoot, "waniwani.config.ts");
284
- if (existsSync(config)) {
285
- watch(config, rebuild);
286
- }
287
- }
288
-
289
- /**
290
- * The framework's dev server, under this CLI's narration.
291
- *
292
- * `--plain` trades the framework's interactive UI for one plain line per
293
- * diagnostic on stderr, which is what makes the output rewritable. It also drops
294
- * the framework's auto-open of its own DevTools page in the browser; the URL is
295
- * printed instead.
296
- */
297
- function devServer(outDir) {
298
- return run("node", [frameworkBin(), "dev", "--plain"], {
299
- cwd: outDir,
300
- stderrFilter: devFilter(),
301
- });
302
- }
303
-
304
- /** Flags that take a value; everything else is a boolean switch. */
305
- const VALUE_FLAGS = new Set(["out", "template", "name"]);
306
-
307
- /**
308
- * `--out dir` / `--template=github:o/r#ref` alongside a positional app directory.
309
- *
310
- * `--no-install` sets `install` to false, so a switch that is on by default is
311
- * read as one flag with two states instead of two flags a caller can set to
312
- * contradict each other.
313
- */
314
- function parseArgs(argv) {
315
- const flags = {};
316
- const positional = [];
317
- for (let i = 0; i < argv.length; i++) {
318
- const arg = argv[i];
319
- if (!arg.startsWith("--")) {
320
- positional.push(arg);
321
- continue;
322
- }
323
- const [name, inline] = arg.slice(2).split("=");
324
- if (name.startsWith("no-")) {
325
- flags[name.slice(3)] = false;
326
- continue;
327
- }
328
- flags[name] = VALUE_FLAGS.has(name) ? (inline ?? argv[++i]) : true;
329
- }
330
- return { flags, positional };
331
- }
332
-
333
- async function main() {
334
- const [command = "dev", ...rest] = process.argv.slice(2);
335
- const { flags, positional } = parseArgs(rest);
336
- const appRoot = resolve(positional[0] ?? process.cwd());
337
-
338
- if (BANNERED.has(command)) {
339
- banner(PACKAGE_VERSION);
340
- }
341
-
342
- // Before anything is spawned, so every child inherits the app's variables
343
- // whatever order its modules evaluate in. `init` has no app to read yet.
344
- if (command !== "init") {
345
- loadAppEnv(appRoot);
346
- }
347
-
348
- if (command === "init") {
349
- // Whether a directory was named matters only here: with none, the answer to
350
- // the one question decides where the app goes.
351
- process.exit(await init(appRoot, flags, { targeted: positional.length > 0 }));
352
- }
353
-
354
- if (command === "check") {
355
- const app = scanApp(appRoot);
356
- const report = await validateApp(app);
357
- printReport(app, report);
358
- process.exit(report.ok ? 0 : 1);
359
- }
360
-
361
- if (command === "dev") {
362
- const prepared = await prepare(appRoot, flags);
363
- if (!prepared) process.exit(1);
364
- watchApp(appRoot, prepared.template);
365
- process.exit(await devServer(prepared.outDir));
366
- }
367
-
368
- if (command === "build") {
369
- const prepared = await prepare(appRoot, flags);
370
- if (!prepared) process.exit(1);
371
- const code = await build(prepared.outDir);
372
- if (code !== 0) process.exit(code);
373
- console.log(`\n${green("✓")} built ${bold(prepared.outDir)}`);
374
- process.exit(0);
375
- }
376
-
377
- if (command === "start") {
378
- const outDir = join(appRoot, ".waniwani");
379
- if (!existsSync(join(outDir, "dist"))) {
380
- console.error(red("no build output — run `waniwani build` first"));
381
- process.exit(1);
382
- }
383
- process.exit(
384
- await run("node", [frameworkBin(), "start"], { cwd: outDir, stdoutFilter: startFilter() }),
385
- );
386
- }
387
-
388
- if (command === "eject") {
389
- process.exit(await eject(appRoot, flags));
390
- }
391
-
392
- console.error(red(`unknown command: ${command}`));
393
- console.error(dim("usage: waniwani <init|check|dev|build|start|eject> [dir]"));
394
- process.exit(1);
395
- }
396
-
397
- try {
398
- await main();
399
- } catch (error) {
400
- // Template resolution and generation failures are expected conditions —
401
- // a bad ref, an unreachable network, a template whose shape moved.
402
- console.error(`\n${red("✗")} ${error instanceof Error ? error.message : String(error)}`);
403
- if (DEBUG) {
404
- console.error(error);
405
- } else {
406
- console.error(dim("set WANIWANI_DEBUG=1 for the stack trace"));
407
- }
408
- process.exit(1);
409
- }