tutuca 0.9.84 → 0.9.86
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/dist/tutuca-cli.js +149 -3
- package/package.json +5 -4
- package/skill/tutuca/cli.md +58 -0
- package/skill/tutuca/core.md +23 -0
package/dist/tutuca-cli.js
CHANGED
|
@@ -15722,6 +15722,13 @@ function resolveTutucaBase(projectDir, self, forCdn) {
|
|
|
15722
15722
|
return { base: DIST_PREFIX.replace(/\/$/, ""), serveDist: self.distRoot };
|
|
15723
15723
|
return { base: `https://cdn.jsdelivr.net/npm/tutuca@${self.version}/dist`, serveDist: null };
|
|
15724
15724
|
}
|
|
15725
|
+
function tutucaSource(base) {
|
|
15726
|
+
if (base.startsWith("http"))
|
|
15727
|
+
return "CDN";
|
|
15728
|
+
if (base.startsWith("/node_modules"))
|
|
15729
|
+
return "node_modules";
|
|
15730
|
+
return "local dist";
|
|
15731
|
+
}
|
|
15725
15732
|
function buildImports(base, { margaui }) {
|
|
15726
15733
|
const dev = `${base}/tutuca-dev.js`;
|
|
15727
15734
|
const imports = {
|
|
@@ -15809,6 +15816,40 @@ async function runDevTests(projectDir, devModuleUrls) {
|
|
|
15809
15816
|
}
|
|
15810
15817
|
return { totalTests, failedTests, withTests, failures, importErrors };
|
|
15811
15818
|
}
|
|
15819
|
+
async function discoverModules(projectDir, devModuleUrls) {
|
|
15820
|
+
await createNodeEnv();
|
|
15821
|
+
const modules = [];
|
|
15822
|
+
for (const url of devModuleUrls) {
|
|
15823
|
+
const abs = resolve4(projectDir, url.slice(1));
|
|
15824
|
+
try {
|
|
15825
|
+
const mod = await import(abs);
|
|
15826
|
+
const { normalized, present } = normalizeModule(mod, { path: abs });
|
|
15827
|
+
modules.push({
|
|
15828
|
+
url,
|
|
15829
|
+
present: [...present],
|
|
15830
|
+
components: normalized.components.map((c) => c.name),
|
|
15831
|
+
sections: normalized.sections.map((s) => ({
|
|
15832
|
+
title: s.title,
|
|
15833
|
+
description: s.description,
|
|
15834
|
+
items: s.items.map((it) => ({
|
|
15835
|
+
title: it.title,
|
|
15836
|
+
view: it.view,
|
|
15837
|
+
componentName: it.componentName
|
|
15838
|
+
}))
|
|
15839
|
+
})),
|
|
15840
|
+
macros: normalized.macros ? Object.keys(normalized.macros) : [],
|
|
15841
|
+
requestHandlers: normalized.requestHandlers ? Object.keys(normalized.requestHandlers) : [],
|
|
15842
|
+
error: null
|
|
15843
|
+
});
|
|
15844
|
+
} catch (e) {
|
|
15845
|
+
modules.push({
|
|
15846
|
+
url,
|
|
15847
|
+
error: { code: e.code ?? null, message: e.message, where: e.where ?? null }
|
|
15848
|
+
});
|
|
15849
|
+
}
|
|
15850
|
+
}
|
|
15851
|
+
return modules;
|
|
15852
|
+
}
|
|
15812
15853
|
function collectFailures(node, acc) {
|
|
15813
15854
|
if (node.children) {
|
|
15814
15855
|
for (const child of node.children)
|
|
@@ -15844,12 +15885,13 @@ async function run4(argv, opts = {}) {
|
|
|
15844
15885
|
"no-margaui": { type: "boolean", default: false },
|
|
15845
15886
|
"no-check": { type: "boolean", default: false },
|
|
15846
15887
|
"no-tests": { type: "boolean", default: false },
|
|
15888
|
+
"dry-run": { type: "boolean", default: false },
|
|
15847
15889
|
help: { type: "boolean", short: "h", default: false }
|
|
15848
15890
|
},
|
|
15849
15891
|
allowPositionals: true
|
|
15850
15892
|
});
|
|
15851
15893
|
if (parsed.values.help) {
|
|
15852
|
-
process.stdout.write(`tutuca storybook [dir] [--port <n>] [--out <dir>]
|
|
15894
|
+
process.stdout.write(`tutuca storybook [dir] [--port <n>] [--out <dir>] [--dry-run]
|
|
15853
15895
|
[--no-margaui] [--no-check] [--no-tests]
|
|
15854
15896
|
|
|
15855
15897
|
Auto-discovers co-located *.dev.js modules (recursively, skipping
|
|
@@ -15860,6 +15902,9 @@ async function run4(argv, opts = {}) {
|
|
|
15860
15902
|
--port <n> preferred port (default 4321; falls back to a free port)
|
|
15861
15903
|
--out <dir> write a static index.html + bootstrap (CDN import map)
|
|
15862
15904
|
instead of serving; host it from the project root
|
|
15905
|
+
--dry-run do all the prep (discover, import and normalize modules,
|
|
15906
|
+
resolve the runtime, run tests) and print what would be
|
|
15907
|
+
shown instead of serving; pass --json for structured output
|
|
15863
15908
|
--no-margaui skip margaui styling (renders functional but unstyled)
|
|
15864
15909
|
--no-check skip the in-browser check(app) dev validation
|
|
15865
15910
|
--no-tests skip running the modules' getTests() before serving
|
|
@@ -15899,6 +15944,63 @@ async function run4(argv, opts = {}) {
|
|
|
15899
15944
|
`);
|
|
15900
15945
|
return;
|
|
15901
15946
|
}
|
|
15947
|
+
if (parsed.values["dry-run"]) {
|
|
15948
|
+
const { base: base2 } = resolveTutucaBase(projectDir, self, false);
|
|
15949
|
+
const imports2 = buildImports(base2, { margaui });
|
|
15950
|
+
const modules = await discoverModules(projectDir, devModuleUrls);
|
|
15951
|
+
const tests = parsed.values["no-tests"] ? null : await runDevTests(projectDir, devModuleUrls);
|
|
15952
|
+
const source = tutucaSource(base2);
|
|
15953
|
+
const result = {
|
|
15954
|
+
projectDir,
|
|
15955
|
+
tutuca: { source, base: base2, version: self.version },
|
|
15956
|
+
options: { margaui, check, runTests: !parsed.values["no-tests"] },
|
|
15957
|
+
imports: imports2,
|
|
15958
|
+
modules,
|
|
15959
|
+
tests
|
|
15960
|
+
};
|
|
15961
|
+
if (opts.format === "json") {
|
|
15962
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
15963
|
+
`);
|
|
15964
|
+
return;
|
|
15965
|
+
}
|
|
15966
|
+
process.stdout.write(`tutuca storybook dry run (no server started)
|
|
15967
|
+
project: ${projectDir}
|
|
15968
|
+
tutuca runtime: ${source} (${base2}, version ${self.version})
|
|
15969
|
+
margaui: ${margaui ? "on" : "off"}, in-browser check: ${check ? "on" : "off"}
|
|
15970
|
+
${modules.length} dev module(s):
|
|
15971
|
+
`);
|
|
15972
|
+
for (const m of modules) {
|
|
15973
|
+
if (m.error) {
|
|
15974
|
+
process.stdout.write(` error ${m.url} — ${m.error.message}
|
|
15975
|
+
`);
|
|
15976
|
+
continue;
|
|
15977
|
+
}
|
|
15978
|
+
const sectionItems = m.sections.reduce((n, s) => n + s.items.length, 0);
|
|
15979
|
+
process.stdout.write(` ok ${m.url} — ${m.components.length} component(s), ${m.sections.length} section(s), ${sectionItems} example(s)
|
|
15980
|
+
`);
|
|
15981
|
+
}
|
|
15982
|
+
if (tests) {
|
|
15983
|
+
for (const ie of tests.importErrors) {
|
|
15984
|
+
process.stdout.write(` ! skipped tests for ${ie.url}: ${ie.message}
|
|
15985
|
+
`);
|
|
15986
|
+
}
|
|
15987
|
+
if (tests.withTests === 0) {
|
|
15988
|
+
process.stdout.write(` tests: no getTests() in any dev module
|
|
15989
|
+
`);
|
|
15990
|
+
} else {
|
|
15991
|
+
process.stdout.write(` tests: ${tests.totalTests - tests.failedTests}/${tests.totalTests} passed across ${tests.withTests} module(s)
|
|
15992
|
+
`);
|
|
15993
|
+
for (const f of tests.failures) {
|
|
15994
|
+
process.stdout.write(` failed ${f.fullPath}: ${f.error?.message ?? "failed"}
|
|
15995
|
+
`);
|
|
15996
|
+
}
|
|
15997
|
+
}
|
|
15998
|
+
} else {
|
|
15999
|
+
process.stdout.write(` tests: skipped (--no-tests)
|
|
16000
|
+
`);
|
|
16001
|
+
}
|
|
16002
|
+
return;
|
|
16003
|
+
}
|
|
15902
16004
|
if (!parsed.values["no-tests"]) {
|
|
15903
16005
|
const r = await runDevTests(projectDir, devModuleUrls);
|
|
15904
16006
|
for (const ie of r.importErrors) {
|
|
@@ -15949,7 +16051,7 @@ async function run4(argv, opts = {}) {
|
|
|
15949
16051
|
});
|
|
15950
16052
|
server.on("listening", () => {
|
|
15951
16053
|
const actual = server.address().port;
|
|
15952
|
-
const where =
|
|
16054
|
+
const where = tutucaSource(base);
|
|
15953
16055
|
process.stdout.write(`tutuca storybook: http://localhost:${actual}/ (${devModuleUrls.length} dev modules, tutuca from ${where})
|
|
15954
16056
|
`);
|
|
15955
16057
|
});
|
|
@@ -15994,7 +16096,7 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
15994
16096
|
import { dirname, resolve } from "node:path";
|
|
15995
16097
|
import { fileURLToPath } from "node:url";
|
|
15996
16098
|
var describe = "Print a machine-readable schema (commands, flags, exit codes) as JSON.";
|
|
15997
|
-
var SCHEMA_VERSION =
|
|
16099
|
+
var SCHEMA_VERSION = 3;
|
|
15998
16100
|
var GLOBAL_FLAGS = [
|
|
15999
16101
|
{
|
|
16000
16102
|
name: "json",
|
|
@@ -16126,6 +16228,50 @@ var NO_MODULE_COMMANDS_META = {
|
|
|
16126
16228
|
],
|
|
16127
16229
|
positionals: []
|
|
16128
16230
|
},
|
|
16231
|
+
storybook: {
|
|
16232
|
+
describe: "Serve a live storybook for the project, auto-discovering co-located *.dev.js modules.",
|
|
16233
|
+
needsModule: false,
|
|
16234
|
+
flags: [
|
|
16235
|
+
{
|
|
16236
|
+
name: "port",
|
|
16237
|
+
type: "string",
|
|
16238
|
+
description: "Preferred port (default 4321; falls back to a free port)."
|
|
16239
|
+
},
|
|
16240
|
+
{
|
|
16241
|
+
name: "out",
|
|
16242
|
+
type: "string",
|
|
16243
|
+
description: "Write a static index.html + bootstrap (CDN import map) instead of serving."
|
|
16244
|
+
},
|
|
16245
|
+
{
|
|
16246
|
+
name: "no-margaui",
|
|
16247
|
+
type: "boolean",
|
|
16248
|
+
description: "Render unstyled (skip margaui)."
|
|
16249
|
+
},
|
|
16250
|
+
{
|
|
16251
|
+
name: "no-check",
|
|
16252
|
+
type: "boolean",
|
|
16253
|
+
description: "Skip the in-browser check(app)."
|
|
16254
|
+
},
|
|
16255
|
+
{
|
|
16256
|
+
name: "no-tests",
|
|
16257
|
+
type: "boolean",
|
|
16258
|
+
description: "Skip running the modules' getTests() before serving."
|
|
16259
|
+
},
|
|
16260
|
+
{
|
|
16261
|
+
name: "dry-run",
|
|
16262
|
+
type: "boolean",
|
|
16263
|
+
description: "Do all prep (discover + import + normalize modules, resolve runtime, run tests) and print what would be shown instead of serving. Pass --json for structured output."
|
|
16264
|
+
},
|
|
16265
|
+
{ name: "help", short: "h", type: "boolean" }
|
|
16266
|
+
],
|
|
16267
|
+
positionals: [
|
|
16268
|
+
{
|
|
16269
|
+
name: "dir",
|
|
16270
|
+
required: false,
|
|
16271
|
+
description: "Project root to scan and serve (default: cwd)."
|
|
16272
|
+
}
|
|
16273
|
+
]
|
|
16274
|
+
},
|
|
16129
16275
|
"agent-context": {
|
|
16130
16276
|
describe: "Print this schema as JSON.",
|
|
16131
16277
|
needsModule: false,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tutuca",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.86",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Zero-dependency SPA framework with immutable state and virtual DOM",
|
|
6
6
|
"main": "./dist/tutuca.js",
|
|
@@ -22,15 +22,16 @@
|
|
|
22
22
|
"tutuca": "./dist/tutuca-cli.js"
|
|
23
23
|
},
|
|
24
24
|
"scripts": {
|
|
25
|
-
"clean": "rm -rf dist",
|
|
25
|
+
"clean": "rm -rf dist skill",
|
|
26
26
|
"dist": "bun scripts/dist.js",
|
|
27
27
|
"dist-ext": "bun scripts/dist-ext.js",
|
|
28
28
|
"dist-immutable": "bun scripts/dist-immutable.js",
|
|
29
29
|
"dist-all": "bun run dist-immutable && bun run dist && bun run dist-ext && bun run smoke",
|
|
30
30
|
"smoke": "node scripts/smoke.js",
|
|
31
|
-
"release": "bun run dist-all && bun run build-skill && npm publish --access public",
|
|
32
|
-
"release-dry": "bun run dist-all && bun run build-skill && npm publish --dry-run",
|
|
31
|
+
"release": "bun run dist-all && bun run build-skill && (npm publish --access public; bun run clean-skill)",
|
|
32
|
+
"release-dry": "bun run dist-all && bun run build-skill && (npm publish --dry-run; bun run clean-skill)",
|
|
33
33
|
"build-skill": "bun scripts/build-skill.js",
|
|
34
|
+
"clean-skill": "rm -rf skill",
|
|
34
35
|
"test": "bun test test/*.test.js",
|
|
35
36
|
"test-watch": "bun test --watch test/*.test.js",
|
|
36
37
|
"format": "bunx @biomejs/biome format --write",
|
package/skill/tutuca/cli.md
CHANGED
|
@@ -40,6 +40,7 @@ Use `--module=<path>` if the path conflicts with positional parsing.
|
|
|
40
40
|
| `lint <module> [name]` | Run the linter; exits **2** on any error-level finding |
|
|
41
41
|
| `render <module> [name]` | Render examples to HTML in a headless DOM. Filter by component name or `--title`/`--view`. Exits **3** on render crash |
|
|
42
42
|
| `test <module> [name]` | Run tests defined by `getTests({ describe, test, expect })`. Filter by component name, `--grep <pattern>`, or `--bail`. Exits **4** on any failure |
|
|
43
|
+
| `storybook [dir]` | Serve a live storybook for the project, auto-discovering co-located `*.dev.js` modules. Flags: `--port`, `--out`, `--dry-run` (prep + print, don't serve), `--no-margaui`, `--no-check`, `--no-tests`. No module path needed |
|
|
43
44
|
| `help [cmd]` | Show usage. No module path needed |
|
|
44
45
|
| `feedback [message]` | Append a feedback note (positional or stdin) to `~/.tutuca/feedback.jsonl`. No module path needed |
|
|
45
46
|
| `install-skill [name]` | Copy a bundled skill (`tutuca`, `margaui`, `immutable-js`, or `--all`) into `.claude/skills/`. No module path needed |
|
|
@@ -187,6 +188,63 @@ export function getTests({ describe, test, expect }) {
|
|
|
187
188
|
`tutuca test <module> Counter` picks it up. Untagged `test(...)` at the
|
|
188
189
|
top of a tagged `describe` inherits the tag.
|
|
189
190
|
|
|
191
|
+
## storybook — live component catalog
|
|
192
|
+
|
|
193
|
+
`tutuca storybook [dir]` serves a browser storybook for a project with no
|
|
194
|
+
setup. It recursively discovers co-located `*.dev.js` modules (see the
|
|
195
|
+
`.dev.js` convention below), mounts them via the shipped `tutuca/storybook`
|
|
196
|
+
library, and serves an ephemeral page — no config, no HTML to write.
|
|
197
|
+
|
|
198
|
+
```sh
|
|
199
|
+
tutuca storybook # scan + serve the current directory
|
|
200
|
+
tutuca storybook ./packages/ui # scan + serve another directory
|
|
201
|
+
tutuca storybook --port 4321 # preferred port (falls back to a free one if taken)
|
|
202
|
+
tutuca storybook --out ./_site # write a static index.html + bootstrap instead of serving
|
|
203
|
+
tutuca storybook --dry-run # do all the prep + print what would be shown, don't serve (smoke test)
|
|
204
|
+
tutuca storybook --dry-run --json # same, machine-readable for agents
|
|
205
|
+
tutuca storybook --no-tests # skip the pre-serve getTests() run
|
|
206
|
+
tutuca storybook --no-margaui # render unstyled (skip margaui)
|
|
207
|
+
tutuca storybook --no-check # skip the in-browser check(app)
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
It is **batteries-included by default**: before serving it runs each module's
|
|
211
|
+
`getTests()` in the terminal, the page wires margaui styling, and the browser
|
|
212
|
+
runs `check(app)`. Each is individually disablable with the `--no-*` flags.
|
|
213
|
+
|
|
214
|
+
How tutuca itself is resolved (convention over configuration): a local
|
|
215
|
+
`node_modules/tutuca` install if present, else the CLI's own `dist`, else the
|
|
216
|
+
version-pinned CDN. All tutuca specifiers resolve to a single runtime, which
|
|
217
|
+
component scope/identity requires. `--out` always pins the CDN so the static
|
|
218
|
+
artifact is portable (host it from the project root so `/*.dev.js` paths resolve).
|
|
219
|
+
|
|
220
|
+
### The `.dev.js` convention
|
|
221
|
+
|
|
222
|
+
A `*.dev.js` file is a **dev-only module**: it holds stories
|
|
223
|
+
(`getComponents()` + `getExamples()`), tests (`getTests()`), and any other
|
|
224
|
+
development-time helpers for nearby components, and is **never shipped to
|
|
225
|
+
production or the UI**. The `.dev.js` suffix is the contract — your app imports
|
|
226
|
+
its real components directly and never a `.dev.js`, and a production build glob
|
|
227
|
+
can exclude `**/*.dev.js`. Because they follow the full module convention, the
|
|
228
|
+
same files are valid targets for `tutuca test`/`lint`/`render` too.
|
|
229
|
+
|
|
230
|
+
```js
|
|
231
|
+
// counter.dev.js — lives next to counter.js
|
|
232
|
+
import { component, html } from "tutuca";
|
|
233
|
+
import { Counter } from "./counter.js";
|
|
234
|
+
|
|
235
|
+
export function getComponents() {
|
|
236
|
+
return [Counter];
|
|
237
|
+
}
|
|
238
|
+
export function getExamples() {
|
|
239
|
+
return { title: "Counter", items: [{ title: "Basic", value: Counter.make({}) }] };
|
|
240
|
+
}
|
|
241
|
+
export function getTests({ describe, test, expect }) {
|
|
242
|
+
describe(Counter, () => {
|
|
243
|
+
test("starts at zero", () => expect(Counter.make({}).count).toBe(0));
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
190
248
|
## Install skill assets
|
|
191
249
|
|
|
192
250
|
`tutuca install-skill` copies bundled Claude Code skill files into
|
package/skill/tutuca/core.md
CHANGED
|
@@ -69,6 +69,23 @@ edit done:
|
|
|
69
69
|
emitted HTML to verify structure (attributes, nesting, text); omit it
|
|
70
70
|
when you only care that the render didn't crash.
|
|
71
71
|
|
|
72
|
+
4. **Smoke-test the whole project** — when you've touched several
|
|
73
|
+
`*.dev.js` modules, or are about to launch the storybook, do a
|
|
74
|
+
project-wide dry run instead of opening a browser:
|
|
75
|
+
|
|
76
|
+
tutuca storybook --dry-run
|
|
77
|
+
tutuca storybook --dry-run --json # machine-readable for agents
|
|
78
|
+
|
|
79
|
+
It does everything the server would do up front — discovers every
|
|
80
|
+
co-located `*.dev.js`, imports and normalizes each (catching a missing
|
|
81
|
+
`getComponents()` or a malformed `getExamples()` shape), runs their
|
|
82
|
+
`getTests()`, and resolves the runtime import map — then prints what
|
|
83
|
+
it *would* show instead of serving. A broken module is reported in
|
|
84
|
+
place (an `error` line, or `modules[].error` in `--json`) while the
|
|
85
|
+
others still report, so one bad module never hides the rest. This is
|
|
86
|
+
the fast "is the whole catalog wired up correctly?" check; steps 1–3
|
|
87
|
+
stay the per-module loop.
|
|
88
|
+
|
|
72
89
|
Full reference: [cli.md](./cli.md).
|
|
73
90
|
|
|
74
91
|
The Tutuca CLI only catches Tutuca-specific issues. For generic JS
|
|
@@ -988,6 +1005,12 @@ export, alias it instead of teaching tools a new name:
|
|
|
988
1005
|
export { allMyComponents as getComponents } from "./app.js";
|
|
989
1006
|
```
|
|
990
1007
|
|
|
1008
|
+
Put these exports in a co-located **`*.dev.js`** file (a dev-only module
|
|
1009
|
+
holding stories + tests, never shipped) and `tutuca storybook` auto-discovers
|
|
1010
|
+
and renders them with no setup — see [cli.md](./cli.md). The same shape is
|
|
1011
|
+
consumed by the shipped `tutuca/storybook` library (`mountStorybook`,
|
|
1012
|
+
`buildStorybook`) if you want to embed a storybook in your own page.
|
|
1013
|
+
|
|
991
1014
|
## See also
|
|
992
1015
|
|
|
993
1016
|
- [component-design.md](./component-design.md) — design judgment for shaping a
|