tutuca 0.9.85 → 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.
@@ -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 = base.startsWith("http") ? "CDN" : base.startsWith("/node_modules") ? "node_modules" : "local dist";
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
  });
@@ -16155,6 +16257,11 @@ var NO_MODULE_COMMANDS_META = {
16155
16257
  type: "boolean",
16156
16258
  description: "Skip running the modules' getTests() before serving."
16157
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
+ },
16158
16265
  { name: "help", short: "h", type: "boolean" }
16159
16266
  ],
16160
16267
  positionals: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tutuca",
3
- "version": "0.9.85",
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",
@@ -40,7 +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`, `--no-margaui`, `--no-check`, `--no-tests`. No module path needed |
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 |
44
44
  | `help [cmd]` | Show usage. No module path needed |
45
45
  | `feedback [message]` | Append a feedback note (positional or stdin) to `~/.tutuca/feedback.jsonl`. No module path needed |
46
46
  | `install-skill [name]` | Copy a bundled skill (`tutuca`, `margaui`, `immutable-js`, or `--all`) into `.claude/skills/`. No module path needed |
@@ -200,6 +200,8 @@ tutuca storybook # scan + serve the current directory
200
200
  tutuca storybook ./packages/ui # scan + serve another directory
201
201
  tutuca storybook --port 4321 # preferred port (falls back to a free one if taken)
202
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
203
205
  tutuca storybook --no-tests # skip the pre-serve getTests() run
204
206
  tutuca storybook --no-margaui # render unstyled (skip margaui)
205
207
  tutuca storybook --no-check # skip the in-browser check(app)
@@ -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