@waniwani/kit 0.1.1 → 0.1.4
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 +121 -22
- package/cli/codegen.mjs +36 -40
- package/cli/env.mjs +37 -0
- package/cli/framework.mjs +0 -1
- package/cli/index.mjs +31 -190
- package/cli/init.mjs +582 -0
- package/cli/log.mjs +5 -4
- package/cli/scan.mjs +42 -14
- package/cli/validate.mjs +81 -2
- package/dist/index.d.ts +40 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +7 -4
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +76 -49
- package/dist/server.js.map +1 -1
- package/package.json +7 -6
- package/src/index.ts +45 -7
- package/src/server.ts +79 -56
- package/cli/account.mjs +0 -264
- package/cli/tunnel.mjs +0 -140
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @waniwani/kit
|
|
2
2
|
|
|
3
|
-
**Build an MCP app as a folder.** You write tools, widgets
|
|
3
|
+
**Build an MCP app as a folder.** You write tools, widgets and flows into
|
|
4
4
|
a directory, and one CLI turns that directory into a deployable MCP server. Your
|
|
5
5
|
repo holds none of the plumbing: server bootstrap, transport wiring, build
|
|
6
6
|
configuration.
|
|
@@ -15,8 +15,7 @@ oney/ # what you write
|
|
|
15
15
|
├── waniwani.config.ts
|
|
16
16
|
├── tools/check-eligibility.ts
|
|
17
17
|
├── widgets/select-plan/{widget.ts,ui.tsx}
|
|
18
|
-
|
|
19
|
-
└── docs/*.md
|
|
18
|
+
└── flows/split-payment.ts
|
|
20
19
|
|
|
21
20
|
waniwani build # → .waniwani/, an ordinary npm project
|
|
22
21
|
```
|
|
@@ -82,9 +81,25 @@ registers them come from the kit.
|
|
|
82
81
|
|
|
83
82
|
## Quickstart
|
|
84
83
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
84
|
+
```bash
|
|
85
|
+
npx @waniwani/kit init oney
|
|
86
|
+
cd oney && npm run dev
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`init` writes a folder that already answers: an app config, one tool, and the
|
|
90
|
+
widget that displays what the tool returned. It installs, and the dev
|
|
91
|
+
server is one command away. `--minimal` scaffolds the config and the tool alone,
|
|
92
|
+
`--name` sets the MCP server name, and running it inside an existing repo merges
|
|
93
|
+
into that repo's `package.json` and `.gitignore` instead of replacing them.
|
|
94
|
+
|
|
95
|
+
Where the app lands follows the argument. `init oney` creates `oney/`, `init .`
|
|
96
|
+
uses the current folder, and a bare `init` asks for a name and reads the answer
|
|
97
|
+
as both: a name of its own creates `./<name>/`, while the offered default, your
|
|
98
|
+
current folder's name, scaffolds in place.
|
|
99
|
+
|
|
100
|
+
The rest of this section is what those files hold, written out by hand.
|
|
101
|
+
[examples/oney](https://github.com/WaniWani-AI/kit/blob/main/examples/oney) is the same app finished, if you would rather read
|
|
102
|
+
it than type it.
|
|
88
103
|
|
|
89
104
|
```bash
|
|
90
105
|
mkdir oney && cd oney
|
|
@@ -235,9 +250,8 @@ oney/
|
|
|
235
250
|
│ └── ui.tsx export default function Component()
|
|
236
251
|
├── flows/
|
|
237
252
|
│ └── split-payment.ts export default createFlow(...).compile() ← SDK
|
|
238
|
-
├──
|
|
239
|
-
│
|
|
240
|
-
│ └── eligibility.md
|
|
253
|
+
├── api/
|
|
254
|
+
│ └── cal/slots.ts export default defineEndpoint({ ..., handler })
|
|
241
255
|
└── lib/ anything else is just modules
|
|
242
256
|
```
|
|
243
257
|
|
|
@@ -251,7 +265,7 @@ unwired.
|
|
|
251
265
|
| `tools/<name>.ts` | one MCP tool | `.ts`, `.tsx` and `.mts` are picked up |
|
|
252
266
|
| `widgets/<name>/` | one MCP tool plus a `ui://` resource | needs `widget.ts` and `ui.tsx` |
|
|
253
267
|
| `flows/<name>.ts` | one MCP tool, registered from the SDK unchanged | whatever `.compile()` returns |
|
|
254
|
-
| `
|
|
268
|
+
| `api/<path>.ts` | one HTTP endpoint at `/api/<path>` | for the browser, invisible to the model |
|
|
255
269
|
| anything else | plain modules | the CLI leaves it alone |
|
|
256
270
|
|
|
257
271
|
The app folder imports `@waniwani/kit`, plus `@waniwani/sdk` when it uses flows,
|
|
@@ -316,6 +330,70 @@ export default createFlow({ id: "split_payment", title, description, state })
|
|
|
316
330
|
`showWidget({ tool: "select-plan" })` names a widget by its folder name, and the
|
|
317
331
|
build check verifies that the folder exists.
|
|
318
332
|
|
|
333
|
+
### The api/ folder is for the browser
|
|
334
|
+
|
|
335
|
+
A widget runs in an iframe on another origin, and it can call its own server
|
|
336
|
+
without going through the model at all. Booking a slot, loading a calendar,
|
|
337
|
+
receiving a webhook: `api/` is where those live. The path comes from the file's
|
|
338
|
+
position, the folder name included, so there is nothing to keep in step with the
|
|
339
|
+
`fetch()` on the other side:
|
|
340
|
+
|
|
341
|
+
```
|
|
342
|
+
api/cal/slots.ts → /api/cal/slots
|
|
343
|
+
api/webhooks/stripe.ts → /api/webhooks/stripe
|
|
344
|
+
api/cal/index.ts → /api/cal
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
```ts
|
|
348
|
+
// api/cal/slots.ts
|
|
349
|
+
import { defineEndpoint } from "@waniwani/kit";
|
|
350
|
+
import { fetchCalSlots } from "../../lib/cal.js";
|
|
351
|
+
|
|
352
|
+
export default defineEndpoint({
|
|
353
|
+
method: "post",
|
|
354
|
+
handler: async (req, res) => {
|
|
355
|
+
const { timeZone } = req.body;
|
|
356
|
+
res.json({ slots: await fetchCalSlots(regionFor(timeZone)) });
|
|
357
|
+
},
|
|
358
|
+
});
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
The widget reaches it at the origin the host hands the view, which is the dev
|
|
362
|
+
port locally and the deployed origin inside ChatGPT or Claude:
|
|
363
|
+
|
|
364
|
+
```tsx
|
|
365
|
+
const apiUrl = (path: string) => `${window.skybridge?.serverUrl ?? ""}${path}`;
|
|
366
|
+
const response = await fetch(apiUrl("/api/cal/slots"), { method: "POST", body });
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
Four things arrive with every endpoint, so no app writes them:
|
|
370
|
+
|
|
371
|
+
| | what the runtime does |
|
|
372
|
+
|---|---|
|
|
373
|
+
| **CORS** | on by default, preflight included, advertising the methods `method` declares and no others |
|
|
374
|
+
| **JSON body** | `express.json()`, because the framework installs no parser of its own and `req.body` would be `undefined` |
|
|
375
|
+
| **method guard** | anything outside `method` gets a 405 and an `Allow` header, instead of reaching a handler written for a POST |
|
|
376
|
+
| **errors** | a handler that throws answers JSON with the message, logged as `[waniwani] endpoint "/api/..." failed`, so a `fetch()` waiting on JSON never receives an HTML error page |
|
|
377
|
+
|
|
378
|
+
`cors: false` and `json: false` opt out of the first two. Leaving `method` off
|
|
379
|
+
accepts every method.
|
|
380
|
+
|
|
381
|
+
**Reach for a tool instead when the model is the caller.** An endpoint appears in
|
|
382
|
+
no `tools/list`, costs no turn and no tokens, and the model cannot see that it
|
|
383
|
+
happened. That is what suits a calendar the widget paints for itself, and what
|
|
384
|
+
rules it out for anything the model has to reason about or quote back.
|
|
385
|
+
|
|
386
|
+
Endpoints share the process with `/mcp`, so `lib/` is one set of modules for
|
|
387
|
+
both, and the build check prints what it mounted:
|
|
388
|
+
|
|
389
|
+
```
|
|
390
|
+
✓ Build check passed — 1 widget, 1 flow, 2 endpoints
|
|
391
|
+
widget show-book-call
|
|
392
|
+
flow demo-qualification
|
|
393
|
+
api /api/cal/book
|
|
394
|
+
api /api/cal/slots
|
|
395
|
+
```
|
|
396
|
+
|
|
319
397
|
### Styling is Tailwind, and only Tailwind
|
|
320
398
|
|
|
321
399
|
A widget styles itself with utility classes in its `ui.tsx`. There is no
|
|
@@ -392,15 +470,16 @@ letting it sit there doing nothing:
|
|
|
392
470
|
## Commands
|
|
393
471
|
|
|
394
472
|
```bash
|
|
473
|
+
waniwani init [dir] # scaffold an app folder, install, ready to dev
|
|
395
474
|
waniwani check # validate the folder
|
|
396
475
|
waniwani dev # generate + dev server + regenerate on change
|
|
397
476
|
waniwani build # generate + production build
|
|
398
477
|
waniwani start # run the production build
|
|
399
|
-
waniwani deploy # generate + vercel deploy
|
|
400
478
|
waniwani eject [--out dir] # hand the plumbing over and step out
|
|
401
479
|
```
|
|
402
480
|
|
|
403
|
-
|
|
481
|
+
`init` writes files and stops there. Every other command runs the same four
|
|
482
|
+
stages before doing its own work.
|
|
404
483
|
|
|
405
484
|
```mermaid
|
|
406
485
|
flowchart LR
|
|
@@ -409,7 +488,7 @@ flowchart LR
|
|
|
409
488
|
tools["tools/*.ts"]
|
|
410
489
|
widgets["widgets/<name>/<br/>widget.ts + ui.tsx"]
|
|
411
490
|
flows["flows/*.ts"]
|
|
412
|
-
|
|
491
|
+
api["api/**/*.ts"]
|
|
413
492
|
end
|
|
414
493
|
|
|
415
494
|
subgraph tpl["WaniWani-AI/mcp-distribution-template (public, separate repo)"]
|
|
@@ -433,7 +512,7 @@ flowchart LR
|
|
|
433
512
|
app --> scan --> check --> gen --> out
|
|
434
513
|
runtime -.imported by.-> server
|
|
435
514
|
raw -.fetched at a pinned SHA, copied byte for byte.-> deployfiles
|
|
436
|
-
out --> deploy["dev · build · start
|
|
515
|
+
out --> deploy["dev · build · start"]
|
|
437
516
|
app -.waniwani eject.-> ejected["a plain repo<br/><i>no CLI, no @waniwani/kit</i>"]
|
|
438
517
|
```
|
|
439
518
|
|
|
@@ -450,6 +529,28 @@ flowchart LR
|
|
|
450
529
|
project carrying a `vercel.json`, which is what lets `vercel deploy` inside it
|
|
451
530
|
work with no special support.
|
|
452
531
|
|
|
532
|
+
### Secrets live in the app's .env
|
|
533
|
+
|
|
534
|
+
`.env` and `.env.local` sit next to `waniwani.config.ts`, and every command reads
|
|
535
|
+
them before it runs anything. A variable already exported in the shell or set by
|
|
536
|
+
CI wins over both files, and a hosted deploy sets its variables on the platform
|
|
537
|
+
and reads no file at all.
|
|
538
|
+
|
|
539
|
+
Loading them this early is what lets a module build its client at import time:
|
|
540
|
+
|
|
541
|
+
```ts
|
|
542
|
+
// lib/waniwani.ts
|
|
543
|
+
export const wani = waniwani({ apiKey: process.env.WANIWANI_API_KEY });
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
The generated project runs from `.waniwani/`, one level below the file, and a
|
|
547
|
+
module's imports are evaluated before any line of the module that pulled it in,
|
|
548
|
+
so neither `dotenv/config` nor a load inside generated code arrives in time.
|
|
549
|
+
`waniwani check` reads the same files for the same reason: it imports every
|
|
550
|
+
server-safe module for real, and a flow whose store comes from
|
|
551
|
+
`WANIWANI_API_KEY` would otherwise fail its own build check over a variable
|
|
552
|
+
sitting in the file next to it.
|
|
553
|
+
|
|
453
554
|
### What the build check catches
|
|
454
555
|
|
|
455
556
|
Errors that would otherwise surface as a 500 at request time, or as a widget
|
|
@@ -513,9 +614,9 @@ under `src/app/`:
|
|
|
513
614
|
|
|
514
615
|
```
|
|
515
616
|
oney/
|
|
516
|
-
├── src/app/{tools,widgets,flows,
|
|
617
|
+
├── src/app/{tools,widgets,flows,lib}/ your code, moved
|
|
517
618
|
├── src/_runtime/ the runtime, vendored as source
|
|
518
|
-
├── src/{server,waniwani
|
|
619
|
+
├── src/{server,waniwani}.ts entry and registration
|
|
519
620
|
├── src/views/<widget>.tsx view entries
|
|
520
621
|
├── vite.config.ts vercel.json alpic.json bundling and deploy
|
|
521
622
|
├── Dockerfile .dockerignore container deploy
|
|
@@ -545,8 +646,6 @@ What an ejected repo gives up is the generator, and with it:
|
|
|
545
646
|
- **the build check**, so `showWidget("typo")` becomes a runtime failure again
|
|
546
647
|
- **name-from-filesystem**, so adding a widget means editing `src/waniwani.ts`
|
|
547
648
|
and adding an entry under `src/views/`
|
|
548
|
-
- **docs auto-scan**, since `src/docs.ts` is a snapshot and new `docs/*.md` need
|
|
549
|
-
hand-wiring
|
|
550
649
|
- **runtime fixes**, since `src/_runtime/` is a fork from the moment it lands
|
|
551
650
|
|
|
552
651
|
## Status
|
|
@@ -555,12 +654,12 @@ What an ejected repo gives up is the generator, and with it:
|
|
|
555
654
|
|
|
556
655
|
- **`@waniwani/cli` owns the `waniwani` bin on npm**, so installing both
|
|
557
656
|
collides.
|
|
558
|
-
-
|
|
559
|
-
|
|
657
|
+
- **`waniwani init` scaffolds one shape of app**, a tool with the widget that
|
|
658
|
+
displays it. A flow is not among the files it writes.
|
|
659
|
+
- **Deploying is manual.** `.waniwani/` carries a `vercel.json`, so
|
|
660
|
+
`vercel deploy` inside it works, but no command wraps that.
|
|
560
661
|
- **`useWidget` does not track yet.** Emitting `widget_render` and click events
|
|
561
662
|
through `useWaniwani` automatically is the next step.
|
|
562
|
-
- **Docs search is a term-match** rather than the hosted KB. Swapping it for
|
|
563
|
-
`wani.kb.search` when `WANIWANI_API_KEY` is set is a runtime change only.
|
|
564
663
|
|
|
565
664
|
Template pinning, the CI contract, publishing requirements and the rest of the
|
|
566
665
|
gap list are in [INTERNALS.md](https://github.com/WaniWani-AI/kit/blob/main/INTERNALS.md).
|
package/cli/codegen.mjs
CHANGED
|
@@ -201,7 +201,7 @@ const SCRIPT_ADDITIONS = {
|
|
|
201
201
|
*/
|
|
202
202
|
const SCRIPT_REMOVALS = {
|
|
203
203
|
"kb:ingest": {
|
|
204
|
-
why: "ingests knowledge-base/, which is the example's; an app
|
|
204
|
+
why: "ingests knowledge-base/, which is the example's; an app has no such folder",
|
|
205
205
|
},
|
|
206
206
|
};
|
|
207
207
|
|
|
@@ -247,9 +247,7 @@ const NOT_SOURCE = new Set([
|
|
|
247
247
|
".skybridge",
|
|
248
248
|
".vercel",
|
|
249
249
|
// The repo's own, not the app's. An in-place eject moves what it copies, and
|
|
250
|
-
// a README that reappears under `src/app/` is a bad surprise.
|
|
251
|
-
// actually serves live in `docs/*.md` and are inlined separately, so nothing
|
|
252
|
-
// is lost by skipping these at every level.
|
|
250
|
+
// a README that reappears under `src/app/` is a bad surprise.
|
|
253
251
|
"README.md",
|
|
254
252
|
"LICENSE",
|
|
255
253
|
// Lockfiles describe the repo's install, and the generated package.json is
|
|
@@ -268,7 +266,7 @@ const NOT_SOURCE = new Set([
|
|
|
268
266
|
* tools in it, and reads `src/waniwani.ts` — the one file this generates into
|
|
269
267
|
* the template's tree.
|
|
270
268
|
*/
|
|
271
|
-
const GENERATED = ["src/waniwani.ts", "
|
|
269
|
+
const GENERATED = ["src/waniwani.ts", "tsconfig.json", ".template.json"];
|
|
272
270
|
|
|
273
271
|
/** `select-plan` -> `selectPlan`, for generated identifiers. */
|
|
274
272
|
function camel(name) {
|
|
@@ -592,20 +590,22 @@ function templateStyleDomains(template) {
|
|
|
592
590
|
|
|
593
591
|
// ------------------------------------------------------------ generated files
|
|
594
592
|
|
|
595
|
-
function generateServerApp(app, layout, { runtime, styleDomains }) {
|
|
593
|
+
function generateServerApp(app, layout, { runtime, styleDomains, version }) {
|
|
596
594
|
const from = appFrom(layout);
|
|
597
595
|
|
|
598
596
|
const imports = [
|
|
599
597
|
`import { config as loadEnv } from "dotenv";`,
|
|
600
598
|
`import type { McpServer } from "skybridge/server";`,
|
|
601
599
|
`import { registerApp as register } from "${runtime.server}";`,
|
|
602
|
-
app.docs.length > 0 ? `import { docs } from "./docs.js";` : null,
|
|
603
600
|
`import config from "${from}/waniwani.config.js";`,
|
|
604
601
|
...app.tools.map((t) => `import tool_${camel(t.name)} from "${from}/tools/${t.name}.js";`),
|
|
605
602
|
...app.widgets.map(
|
|
606
603
|
(w) => `import widget_${camel(w.name)} from "${from}/widgets/${w.name}/widget.js";`,
|
|
607
604
|
),
|
|
608
605
|
...app.flows.map((f) => `import flow_${camel(f.name)} from "${from}/flows/${f.name}.js";`),
|
|
606
|
+
...app.endpoints.map(
|
|
607
|
+
(e) => `import endpoint_${camel(e.segments.join("-"))} from "${from}/api/${e.segments.join("/")}.js";`,
|
|
608
|
+
),
|
|
609
609
|
].filter(Boolean);
|
|
610
610
|
|
|
611
611
|
const list = (items) => (items.length === 0 ? "[]" : `[\n\t\t${items.join(",\n\t\t")},\n\t]`);
|
|
@@ -618,10 +618,12 @@ ${imports.join("\n")}
|
|
|
618
618
|
// on whether this is a generated build or an ejected project.
|
|
619
619
|
loadEnv({ path: ["../.env", ".env"], quiet: true });
|
|
620
620
|
|
|
621
|
+
// The version the app's package.json carries is the fallback, so a bumped
|
|
622
|
+
// release shows up in the connector UI without a second edit here.
|
|
621
623
|
export const app = {
|
|
622
624
|
name: config.name,
|
|
623
625
|
title: config.title,
|
|
624
|
-
version: config.version ?? "0.0.0",
|
|
626
|
+
version: config.version ?? ${JSON.stringify(version ?? "0.0.0")},
|
|
625
627
|
instructions: config.instructions,
|
|
626
628
|
};
|
|
627
629
|
|
|
@@ -630,7 +632,13 @@ export async function registerApp(server: McpServer): Promise<void> {
|
|
|
630
632
|
tools: ${list(app.tools.map((t) => `{ name: "${t.name}", def: tool_${camel(t.name)} }`))},
|
|
631
633
|
widgets: ${list(app.widgets.map((w) => `{ name: "${w.name}", def: widget_${camel(w.name)} }`))},
|
|
632
634
|
flows: ${list(app.flows.map((f) => `flow_${camel(f.name)}`))},
|
|
633
|
-
|
|
635
|
+
// Served by the same Express app as /mcp, at the path each file's position
|
|
636
|
+
// produced. For the browser — a widget's fetch — not for the model.
|
|
637
|
+
endpoints: ${list(
|
|
638
|
+
app.endpoints.map(
|
|
639
|
+
(e) => `{ path: "${e.path}", def: endpoint_${camel(e.segments.join("-"))} }`,
|
|
640
|
+
),
|
|
641
|
+
)},
|
|
634
642
|
// Read off the template's ${STYLE_ENTRY}, which every view imports.
|
|
635
643
|
styleDomains: ${list(styleDomains.map((origin) => `"${origin}"`))},
|
|
636
644
|
});
|
|
@@ -638,22 +646,6 @@ export async function registerApp(server: McpServer): Promise<void> {
|
|
|
638
646
|
`;
|
|
639
647
|
}
|
|
640
648
|
|
|
641
|
-
/**
|
|
642
|
-
* Docs are inlined into a module rather than read from disk, so they survive a
|
|
643
|
-
* serverless bundle with no filesystem.
|
|
644
|
-
*/
|
|
645
|
-
function generateDocs(app, { runtime }) {
|
|
646
|
-
return `// Generated from docs/*.md.
|
|
647
|
-
import type { DocEntry } from "${runtime.index}";
|
|
648
|
-
|
|
649
|
-
export const docs: DocEntry[] = ${JSON.stringify(
|
|
650
|
-
app.docs.map((doc) => ({ slug: doc.slug, title: doc.title, body: doc.body })),
|
|
651
|
-
null,
|
|
652
|
-
2,
|
|
653
|
-
)};
|
|
654
|
-
`;
|
|
655
|
-
}
|
|
656
|
-
|
|
657
649
|
function generateWidgetShim(widget, layout) {
|
|
658
650
|
// From `src/views/` up to `src/`, then out to the app's source.
|
|
659
651
|
const from = `../${basename(layout.appDir)}`;
|
|
@@ -740,7 +732,6 @@ function generateBiome(template, layout) {
|
|
|
740
732
|
// Generated and vendored code is not the app author's to fix.
|
|
741
733
|
`!${layout.runtimeDir}/**`,
|
|
742
734
|
"!src/server.ts",
|
|
743
|
-
"!src/docs.ts",
|
|
744
735
|
"!src/views/**",
|
|
745
736
|
...negative,
|
|
746
737
|
],
|
|
@@ -861,7 +852,7 @@ function assertSeam(template) {
|
|
|
861
852
|
|
|
862
853
|
throw new Error(
|
|
863
854
|
`the template at ${template.source} never calls ${SEAM.symbol}(), so this app's\n` +
|
|
864
|
-
` tools, widgets
|
|
855
|
+
` tools, widgets and flows would be built and then silently dropped.\n\n` +
|
|
865
856
|
` Add to its ${SEAM.file}:\n\n` +
|
|
866
857
|
` import { app, registerApp } from "./waniwani.js";\n\n` +
|
|
867
858
|
` const server = new McpServer(\n` +
|
|
@@ -999,14 +990,19 @@ export function generate(app, { template, layout: layoutName = "build", outDir }
|
|
|
999
990
|
}
|
|
1000
991
|
}
|
|
1001
992
|
|
|
993
|
+
const appPackageJsonPath = join(app.root, "package.json");
|
|
994
|
+
const appPackageJson = existsSync(appPackageJsonPath)
|
|
995
|
+
? JSON.parse(readFileSync(appPackageJsonPath, "utf-8"))
|
|
996
|
+
: undefined;
|
|
997
|
+
|
|
1002
998
|
emit(
|
|
1003
999
|
"src/waniwani.ts",
|
|
1004
|
-
generateServerApp(app, layout, {
|
|
1000
|
+
generateServerApp(app, layout, {
|
|
1001
|
+
runtime,
|
|
1002
|
+
styleDomains: templateStyleDomains(template),
|
|
1003
|
+
version: appPackageJson?.version,
|
|
1004
|
+
}),
|
|
1005
1005
|
);
|
|
1006
|
-
if (app.docs.length > 0) {
|
|
1007
|
-
emit("src/docs.ts", generateDocs(app, { runtime }));
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
1006
|
// `src/views/` is shared: the template's own views sit alongside the app's,
|
|
1011
1007
|
// so it cannot be wiped. Only the entries a previous build wrote are
|
|
1012
1008
|
// removed, which is what clears a widget the app has since deleted.
|
|
@@ -1019,11 +1015,6 @@ export function generate(app, { template, layout: layoutName = "build", outDir }
|
|
|
1019
1015
|
emit(`src/views/${widget.name}.tsx`, generateWidgetShim(widget, layout));
|
|
1020
1016
|
}
|
|
1021
1017
|
|
|
1022
|
-
const appPackageJsonPath = join(app.root, "package.json");
|
|
1023
|
-
const appPackageJson = existsSync(appPackageJsonPath)
|
|
1024
|
-
? JSON.parse(readFileSync(appPackageJsonPath, "utf-8"))
|
|
1025
|
-
: undefined;
|
|
1026
|
-
|
|
1027
1018
|
const { packageJson, overrides } = generatePackageJson(app, appPackageJson, template, layout);
|
|
1028
1019
|
|
|
1029
1020
|
emit("tsconfig.json", `${JSON.stringify(generateTsconfig(template, layout), null, 2)}\n`);
|
|
@@ -1046,9 +1037,14 @@ export function generate(app, { template, layout: layoutName = "build", outDir }
|
|
|
1046
1037
|
sha: template.sha,
|
|
1047
1038
|
local: template.local,
|
|
1048
1039
|
manifest: manifest ? MANIFEST_FILE : undefined,
|
|
1049
|
-
// What survived to the end
|
|
1050
|
-
//
|
|
1051
|
-
|
|
1040
|
+
// What survived to the end, copied and generated alike. The
|
|
1041
|
+
// copy is the raw list minus whatever a generated file replaced,
|
|
1042
|
+
// and the generated half is here so that a build which stops
|
|
1043
|
+
// emitting one — `src/docs.ts` when docs left the framework —
|
|
1044
|
+
// cleans up the copy the previous build left behind.
|
|
1045
|
+
files: [...new Set([...fromTemplate, ...GENERATED])].filter((file) =>
|
|
1046
|
+
existsSync(join(root, file)),
|
|
1047
|
+
),
|
|
1052
1048
|
// Tracked separately because `src/views/` is shared with the
|
|
1053
1049
|
// template — the next build needs to know which entries were
|
|
1054
1050
|
// ours before it removes any.
|
package/cli/env.mjs
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The app's `.env`, loaded into this CLI's own environment.
|
|
3
|
+
*
|
|
4
|
+
* Two problems make this the only place it can happen.
|
|
5
|
+
*
|
|
6
|
+
* The framework runs from the generated project, so `process.cwd()` is
|
|
7
|
+
* `.waniwani/` and a bare `dotenv/config` looks for `.waniwani/.env` — a file
|
|
8
|
+
* nobody wrote. The app's `.env` sits one level up, next to `waniwani.config.ts`.
|
|
9
|
+
*
|
|
10
|
+
* Loading it from generated code inside that project is too late anyway. ESM
|
|
11
|
+
* evaluates a module's imports before its body, so by the time any line of
|
|
12
|
+
* `src/waniwani.ts` runs, every app module it imports has already been
|
|
13
|
+
* evaluated — and a module that builds an API client at import time has already
|
|
14
|
+
* read the empty environment and captured it. `createFlow(...).compile()` is the
|
|
15
|
+
* loud version of this: it throws at import time when `WANIWANI_API_KEY` is
|
|
16
|
+
* absent.
|
|
17
|
+
*
|
|
18
|
+
* Loading here, before anything is spawned, puts the variables in the
|
|
19
|
+
* environment every child process inherits, whatever order its modules load in.
|
|
20
|
+
* Hosted deploys set their variables on the platform and never reach this path.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { join } from "node:path";
|
|
24
|
+
import dotenv from "dotenv";
|
|
25
|
+
|
|
26
|
+
const loaded = new Set();
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* `.env.local` before `.env`: the first file to define a variable wins, and
|
|
30
|
+
* dotenv never overwrites one already in the environment, so a value exported in
|
|
31
|
+
* the shell or set by CI outranks both files.
|
|
32
|
+
*/
|
|
33
|
+
export function loadAppEnv(root) {
|
|
34
|
+
if (loaded.has(root)) return;
|
|
35
|
+
loaded.add(root);
|
|
36
|
+
dotenv.config({ path: [join(root, ".env.local"), join(root, ".env")], quiet: true });
|
|
37
|
+
}
|
package/cli/framework.mjs
CHANGED
|
@@ -91,7 +91,6 @@ function reword(line) {
|
|
|
91
91
|
*
|
|
92
92
|
* The framework's own tunnel is one of those lines. No command here asks for it,
|
|
93
93
|
* so what arrives is its offer of one, and the emoji it carries drops the line.
|
|
94
|
-
* A public hostname comes from `waniwani tunnel` instead (see ./tunnel.mjs).
|
|
95
94
|
*
|
|
96
95
|
* Each pattern leads with `\W*` to absorb whatever emoji prefixes the line and
|
|
97
96
|
* ends at `$`, so a rule reads the framework's whole line and can't fire on an
|