@waniwani/kit 0.1.6 → 0.1.7
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 +39 -39
- package/dist/cli/codegen.js +1105 -0
- package/dist/cli/codegen.js.map +1 -0
- package/{cli/env.mjs → dist/cli/env.js} +5 -6
- package/dist/cli/env.js.map +1 -0
- package/{cli/framework.mjs → dist/cli/framework.js} +112 -115
- package/dist/cli/framework.js.map +1 -0
- package/dist/cli/index.js +378 -0
- package/dist/cli/index.js.map +1 -0
- package/{cli/init.mjs → dist/cli/init.js} +218 -259
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/log.js +156 -0
- package/dist/cli/log.js.map +1 -0
- package/dist/cli/manifest.js +57 -0
- package/dist/cli/manifest.js.map +1 -0
- package/{cli/peers.mjs → dist/cli/peers.js} +77 -88
- package/dist/cli/peers.js.map +1 -0
- package/dist/cli/scan.js +100 -0
- package/dist/cli/scan.js.map +1 -0
- package/dist/cli/template.js +173 -0
- package/dist/cli/template.js.map +1 -0
- package/dist/cli/types.js +14 -0
- package/dist/cli/types.js.map +1 -0
- package/dist/cli/validate.js +328 -0
- package/dist/cli/validate.js.map +1 -0
- package/dist/cli/vercel.js +103 -0
- package/dist/cli/vercel.js.map +1 -0
- package/dist/server.d.ts +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +0 -1
- package/dist/server.js.map +1 -1
- package/dist/web.d.ts +8 -7
- package/dist/web.d.ts.map +1 -1
- package/dist/web.js +7 -6
- package/dist/web.js.map +1 -1
- package/package.json +13 -9
- package/src/server.ts +7 -9
- package/src/web.tsx +12 -13
- package/cli/codegen.mjs +0 -1267
- package/cli/index.mjs +0 -409
- package/cli/log.mjs +0 -178
- package/cli/scan.mjs +0 -112
- package/cli/template.mjs +0 -190
- 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
|
-
}
|
package/cli/log.mjs
DELETED
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
/** Build output formatting. Errors point at a file and say how to fix it. */
|
|
2
|
-
|
|
3
|
-
/** ESC by char code: a raw control byte in source is invisible and fragile. */
|
|
4
|
-
const ESC = String.fromCharCode(27);
|
|
5
|
-
|
|
6
|
-
const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
7
|
-
const wrap = (code) => (text) => (useColor ? `${ESC}[${code}m${text}${ESC}[0m` : text);
|
|
8
|
-
|
|
9
|
-
export const red = wrap("31");
|
|
10
|
-
export const green = wrap("32");
|
|
11
|
-
export const yellow = wrap("33");
|
|
12
|
-
export const dim = wrap("2");
|
|
13
|
-
export const bold = wrap("1");
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* The wordmark, at half the height of the art `waniwani login` prints.
|
|
17
|
-
*
|
|
18
|
-
* Same letterforms: the tall version is six rows of full blocks with a box-glyph
|
|
19
|
-
* drop shadow, and each row here folds two of those together — `▀` where only
|
|
20
|
-
* the upper row had ink, `▄` where only the lower did, `█` where both. Three
|
|
21
|
-
* rows instead of six, at the same width, and nothing about the shapes changes.
|
|
22
|
-
*
|
|
23
|
-
* One colour per row rather than per glyph: a vertical ramp needs three escape
|
|
24
|
-
* sequences instead of two hundred, and reads the same.
|
|
25
|
-
*/
|
|
26
|
-
const LOGO = [
|
|
27
|
-
"██ ██ ▄█▀▀▀█▄ ███▄ ██ ██ ██ ██ ▄█▀▀▀█▄ ███▄ ██ ██",
|
|
28
|
-
"██ ▄█▄ ██ ██▀▀▀██ ██ ▀█▄ ██ ██ ██ ▄█▄ ██ ██▀▀▀██ ██ ▀█▄ ██ ██",
|
|
29
|
-
" ▀▀▀ ▀▀▀ ▀▀ ▀▀ ▀▀ ▀▀▀▀ ▀▀ ▀▀▀ ▀▀▀ ▀▀ ▀▀ ▀▀ ▀▀▀▀ ▀▀",
|
|
30
|
-
];
|
|
31
|
-
|
|
32
|
-
const LOGO_WIDTH = 65;
|
|
33
|
-
|
|
34
|
-
/** The brand accent, `#04d916`. */
|
|
35
|
-
const ACCENT = [4, 217, 22];
|
|
36
|
-
|
|
37
|
-
/** How far the first and last rows are mixed toward white and black. */
|
|
38
|
-
const LIGHTEN = 0.42;
|
|
39
|
-
const DARKEN = 0.35;
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* The ramp stop for one row: a tint of the accent at the top, the accent itself
|
|
43
|
-
* in the middle, a shade of it at the bottom — so the wordmark reads as one
|
|
44
|
-
* object lit from above.
|
|
45
|
-
*
|
|
46
|
-
* Interpolated from the row count rather than written out, so the art and the
|
|
47
|
-
* ramp cannot drift apart when either changes.
|
|
48
|
-
*/
|
|
49
|
-
function rampStop(index, rows) {
|
|
50
|
-
// -1 on the first row, 0 in the middle, +1 on the last.
|
|
51
|
-
const position = rows === 1 ? 0 : (index / (rows - 1)) * 2 - 1;
|
|
52
|
-
if (position <= 0) {
|
|
53
|
-
const mix = LIGHTEN * -position;
|
|
54
|
-
return ACCENT.map((channel) => Math.round(channel + (255 - channel) * mix));
|
|
55
|
-
}
|
|
56
|
-
return ACCENT.map((channel) => Math.round(channel * (1 - DARKEN * position)));
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** 24-bit colour is used only where the terminal says it has it. */
|
|
60
|
-
const truecolor = ["truecolor", "24bit"].includes(process.env.COLORTERM ?? "");
|
|
61
|
-
|
|
62
|
-
/** The six levels each channel can take in the xterm 256-colour cube. */
|
|
63
|
-
const CUBE_LEVELS = [0, 95, 135, 175, 215, 255];
|
|
64
|
-
|
|
65
|
-
/** The nearest cube entry, for a terminal that does not announce 24-bit colour. */
|
|
66
|
-
function cube([r, g, b]) {
|
|
67
|
-
const nearest = (channel) =>
|
|
68
|
-
CUBE_LEVELS.reduce(
|
|
69
|
-
(best, _, index) =>
|
|
70
|
-
Math.abs(CUBE_LEVELS[index] - channel) < Math.abs(CUBE_LEVELS[best] - channel) ? index : best,
|
|
71
|
-
0,
|
|
72
|
-
);
|
|
73
|
-
return 16 + 36 * nearest(r) + 6 * nearest(g) + nearest(b);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** The foreground escape for one row of the ramp. */
|
|
77
|
-
function ramp(index, rows) {
|
|
78
|
-
const rgb = rampStop(index, rows);
|
|
79
|
-
if (!truecolor) {
|
|
80
|
-
return `${ESC}[38;5;${cube(rgb)}m`;
|
|
81
|
-
}
|
|
82
|
-
return `${ESC}[38;2;${rgb.join(";")}m`;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Print the banner a command opens with.
|
|
87
|
-
*
|
|
88
|
-
* The art needs 65 columns and a terminal that wants colour. A narrower one, a
|
|
89
|
-
* pipe, a CI log or `NO_COLOR` gets the wordmark on one line instead — the same
|
|
90
|
-
* information, and nothing that turns into wrapped garbage in a build log.
|
|
91
|
-
*/
|
|
92
|
-
export function banner(version) {
|
|
93
|
-
// `||`, not `??`: a pty whose window size was never set reports 0 columns,
|
|
94
|
-
// which is unknown rather than narrow.
|
|
95
|
-
const columns = process.stdout.columns || 80;
|
|
96
|
-
if (!useColor || columns < LOGO_WIDTH) {
|
|
97
|
-
// The wordmark still carries the accent — it is the same banner, narrowed.
|
|
98
|
-
const mark = useColor ? `${ramp(0, 1)}${ESC}[1mwaniwani${ESC}[0m` : "waniwani";
|
|
99
|
-
console.log(`\n${mark} ${dim(`v${version}`)}`);
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
console.log("");
|
|
103
|
-
for (const [index, line] of LOGO.entries()) {
|
|
104
|
-
console.log(`${ramp(index, LOGO.length)}${line}${ESC}[0m`);
|
|
105
|
-
}
|
|
106
|
-
console.log(dim(`v${version}`.padStart(LOGO_WIDTH)));
|
|
107
|
-
console.log("");
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* One URL a command hands the developer, in the shape every command uses for
|
|
112
|
-
* them. The label is padded so a list of endpoints aligns whether the framework
|
|
113
|
-
* printed the line or this CLI did.
|
|
114
|
-
*/
|
|
115
|
-
export function endpoint(label, url) {
|
|
116
|
-
return ` ${dim(label.padEnd(8))} ${green(url)}`;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function printGroup(entries, marker, color) {
|
|
120
|
-
const byFile = new Map();
|
|
121
|
-
for (const entry of entries) {
|
|
122
|
-
const list = byFile.get(entry.where) ?? [];
|
|
123
|
-
list.push(entry);
|
|
124
|
-
byFile.set(entry.where, list);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
for (const [where, list] of byFile) {
|
|
128
|
-
console.log(` ${bold(where)}`);
|
|
129
|
-
for (const entry of list) {
|
|
130
|
-
console.log(` ${color(marker)} ${entry.message}`);
|
|
131
|
-
if (entry.hint) {
|
|
132
|
-
console.log(` ${dim(entry.hint)}`);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
console.log("");
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export function printReport(app, report) {
|
|
140
|
-
if (!report.ok) {
|
|
141
|
-
console.log(`\n${red("✗")} ${bold("Build check failed")}\n`);
|
|
142
|
-
printGroup(report.errors, "└", red);
|
|
143
|
-
if (report.warnings.length > 0) {
|
|
144
|
-
printGroup(report.warnings, "└", yellow);
|
|
145
|
-
}
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const counts = [
|
|
150
|
-
[app.widgets.length, "widget"],
|
|
151
|
-
[app.tools.length, "tool"],
|
|
152
|
-
[app.flows.length, "flow"],
|
|
153
|
-
[app.endpoints.length, "endpoint"],
|
|
154
|
-
]
|
|
155
|
-
.filter(([count]) => count > 0)
|
|
156
|
-
.map(([count, label]) => `${count} ${label}${count === 1 ? "" : "s"}`);
|
|
157
|
-
|
|
158
|
-
console.log(`${green("✓")} ${bold("Build check passed")} ${dim(`— ${counts.join(", ")}`)}`);
|
|
159
|
-
|
|
160
|
-
for (const widget of app.widgets) {
|
|
161
|
-
console.log(` ${dim("widget")} ${widget.name}`);
|
|
162
|
-
}
|
|
163
|
-
for (const tool of app.tools) {
|
|
164
|
-
console.log(` ${dim("tool ")} ${tool.name}`);
|
|
165
|
-
}
|
|
166
|
-
for (const flow of app.flows) {
|
|
167
|
-
console.log(` ${dim("flow ")} ${flow.name}`);
|
|
168
|
-
}
|
|
169
|
-
// The path, not the filename: what a widget writes into a `fetch()` is the
|
|
170
|
-
// thing worth checking against this line.
|
|
171
|
-
for (const endpoint of app.endpoints) {
|
|
172
|
-
console.log(` ${dim("api ")} ${endpoint.path}`);
|
|
173
|
-
}
|
|
174
|
-
if (report.warnings.length > 0) {
|
|
175
|
-
console.log("");
|
|
176
|
-
printGroup(report.warnings, "└", yellow);
|
|
177
|
-
}
|
|
178
|
-
}
|
package/cli/scan.mjs
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Discover an app by walking its folders. Convention, not configuration.
|
|
3
|
-
*
|
|
4
|
-
* waniwani.config.ts
|
|
5
|
-
* tools/<name>.ts
|
|
6
|
-
* widgets/<name>/{widget.ts,ui.tsx}
|
|
7
|
-
* flows/<name>.ts
|
|
8
|
-
* api/<path>.ts
|
|
9
|
-
*
|
|
10
|
-
* There is no CSS in that list. Styling is Tailwind, from the distribution
|
|
11
|
-
* template's `src/index.css` — its `@theme` tokens and its `dark` variant — and
|
|
12
|
-
* a widget writes utility classes in `ui.tsx`. Stray `styles.css` files are
|
|
13
|
-
* collected only so the build check can tell an author they are dead.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
17
|
-
import { basename, extname, join } from "node:path";
|
|
18
|
-
|
|
19
|
-
const CODE_EXT = new Set([".ts", ".tsx", ".mts"]);
|
|
20
|
-
|
|
21
|
-
function listFiles(dir) {
|
|
22
|
-
if (!existsSync(dir)) return [];
|
|
23
|
-
return readdirSync(dir)
|
|
24
|
-
.filter((entry) => !entry.startsWith(".") && !entry.startsWith("_"))
|
|
25
|
-
.map((entry) => join(dir, entry))
|
|
26
|
-
.filter((path) => statSync(path).isFile());
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function listDirs(dir) {
|
|
30
|
-
if (!existsSync(dir)) return [];
|
|
31
|
-
return readdirSync(dir)
|
|
32
|
-
.filter((entry) => !entry.startsWith(".") && !entry.startsWith("_"))
|
|
33
|
-
.map((entry) => join(dir, entry))
|
|
34
|
-
.filter((path) => statSync(path).isDirectory());
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function stripExt(path) {
|
|
38
|
-
return basename(path, extname(path));
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Every code file under `dir`, depth first, as `{ file, segments }` where
|
|
43
|
-
* `segments` is its path below `dir` with the extension gone.
|
|
44
|
-
*
|
|
45
|
-
* `api/` is the one convention folder that nests: an HTTP path has more than
|
|
46
|
-
* one segment, and the only place it can come from without a registry is the
|
|
47
|
-
* filesystem.
|
|
48
|
-
*/
|
|
49
|
-
function listTree(dir, trail = []) {
|
|
50
|
-
if (!existsSync(dir)) return [];
|
|
51
|
-
|
|
52
|
-
return readdirSync(dir)
|
|
53
|
-
.filter((entry) => !entry.startsWith(".") && !entry.startsWith("_"))
|
|
54
|
-
.flatMap((entry) => {
|
|
55
|
-
const path = join(dir, entry);
|
|
56
|
-
if (statSync(path).isDirectory()) {
|
|
57
|
-
return listTree(path, [...trail, entry]);
|
|
58
|
-
}
|
|
59
|
-
if (!CODE_EXT.has(extname(path))) return [];
|
|
60
|
-
return [{ file: path, segments: [...trail, stripExt(path)] }];
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* The URL an endpoint file is served at: its position under the app root,
|
|
66
|
-
* `/api` included, since that is the folder's name.
|
|
67
|
-
*
|
|
68
|
-
* `index` names the directory itself, so `api/cal/index.ts` answers `/api/cal`
|
|
69
|
-
* — the one place a filename is not taken verbatim, and the convention every
|
|
70
|
-
* web framework already uses.
|
|
71
|
-
*/
|
|
72
|
-
function endpointPath(segments) {
|
|
73
|
-
const parts = segments.at(-1) === "index" ? segments.slice(0, -1) : segments;
|
|
74
|
-
return `/api/${parts.join("/")}`.replace(/\/$/, "") || "/api";
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export function scanApp(root) {
|
|
78
|
-
const configFile = [join(root, "waniwani.config.ts"), join(root, "waniwani.config.js")].find(
|
|
79
|
-
existsSync,
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
const tools = listFiles(join(root, "tools"))
|
|
83
|
-
.filter((file) => CODE_EXT.has(extname(file)))
|
|
84
|
-
.map((file) => ({ name: stripExt(file), file }));
|
|
85
|
-
|
|
86
|
-
const widgets = listDirs(join(root, "widgets")).map((dir) => {
|
|
87
|
-
const name = basename(dir);
|
|
88
|
-
const contract = [join(dir, "widget.ts"), join(dir, "widget.tsx")].find(existsSync);
|
|
89
|
-
const ui = [join(dir, "ui.tsx"), join(dir, "ui.jsx")].find(existsSync);
|
|
90
|
-
return { name, dir, contract, ui };
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
const flows = listFiles(join(root, "flows"))
|
|
94
|
-
.filter((file) => CODE_EXT.has(extname(file)))
|
|
95
|
-
.map((file) => ({ name: stripExt(file), file }));
|
|
96
|
-
|
|
97
|
-
const endpoints = listTree(join(root, "api")).map(({ file, segments }) => ({
|
|
98
|
-
path: endpointPath(segments),
|
|
99
|
-
segments,
|
|
100
|
-
file,
|
|
101
|
-
}));
|
|
102
|
-
|
|
103
|
-
// The two paths an author is most likely to expect the kit to pick up. It
|
|
104
|
-
// imports neither, so a file at either one is styling that never reaches the
|
|
105
|
-
// browser — the kind of silent no-op the build check exists to name.
|
|
106
|
-
const strayStyles = [
|
|
107
|
-
join(root, "styles.css"),
|
|
108
|
-
...widgets.map((widget) => join(widget.dir, "styles.css")),
|
|
109
|
-
].filter(existsSync);
|
|
110
|
-
|
|
111
|
-
return { root, configFile, tools, widgets, flows, endpoints, strayStyles };
|
|
112
|
-
}
|