tutuca 0.9.97 → 0.9.99

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 CHANGED
@@ -7,7 +7,7 @@ Zero-dependency batteries included SPA framework.
7
7
  - **Fits in your head** (and the context window)
8
8
  - **View source friendly** — step through the whole stack
9
9
  - **As much HTML as possible, as little JS as needed**
10
- - ~177KB minified, ~40KB brotli compressed
10
+ - ~182KB minified, ~41KB brotli compressed
11
11
 
12
12
  ## Quick Start
13
13
 
@@ -95,6 +95,7 @@ tutuca help [command]
95
95
  | `lint <module> [name]` | Run lint checks — all, or one by name (exit 2 on errors) |
96
96
  | `render <module> [name] [--title t] [--view v]` | Render examples to HTML |
97
97
  | `test <module> [name] [--grep p] [--bail]` | Run `getTests()` (exit 4 on failures) |
98
+ | `storybook [dir]` | Serve a live storybook, auto-discovering co-located `*.dev.js` modules (`--port`, `--out`, `--dry-run`, `--no-margaui`, `--no-check`, `--no-tests`; no module path needed) |
98
99
  | `feedback [message]` | Append a feedback note (positional or stdin) to `~/.tutuca/feedback.jsonl` (no module path needed) |
99
100
  | `install-skill [--user\|--project] [--margaui-skill\|--immutable-skill\|--all] [--dot-agents] [--dry-run] [--force]` | Install bundled Claude Code skills (no module path needed) |
100
101
  | `agent-context` | Print a versioned JSON schema of the entire CLI surface (no module path needed) |
@@ -146,8 +147,9 @@ The invocation stays short even without wrapping, but common patterns:
146
147
 
147
148
  ## Use with Claude Code
148
149
 
149
- Tutuca ships an LLM-facing reference (`SKILL.md` + `core.md` / `cli.md` /
150
- `advanced.md` / `testing.md`) packaged as a [Claude Code skill](https://docs.claude.com/en/docs/claude-code/skills).
150
+ Tutuca ships an LLM-facing reference (`SKILL.md` plus topic files such as
151
+ `core.md`, `cli.md`, `advanced.md`, `testing.md`, `storybook.md`, and more)
152
+ packaged as a [Claude Code skill](https://docs.claude.com/en/docs/claude-code/skills).
151
153
  Once installed, Claude auto-loads it whenever a session touches tutuca
152
154
  components, views, macros, or the CLI.
153
155
 
@@ -9799,37 +9799,47 @@ class Renderer {
9799
9799
  renderEach(stack, iterInfo, node, viewName) {
9800
9800
  const { seq, filter, loopWith } = iterInfo.eval(stack);
9801
9801
  const r = [];
9802
- const { iterData, start, end } = unpackLoopResult(loopWith.call(stack.it, seq), seq);
9803
- getSeqInfo(seq)(seq, (key, value, attrName) => {
9804
- if (filter.call(stack.it, key, value, iterData)) {
9805
- const dom = this.renderIt(stack.enter(value, { key }, true), node, key, viewName);
9806
- this.pushEachEntry(r, node.nodeId, attrName, key, dom);
9807
- }
9808
- }, start, end);
9802
+ const { iterData, start, end, keys } = unpackLoopResult(loopWith.call(stack.it, seq, makeLoopCtx(stack, filter)), seq);
9803
+ const renderOne = (key, value, attrName) => {
9804
+ const dom = this.renderIt(stack.enter(value, { key }, true), node, key, viewName);
9805
+ this.pushEachEntry(r, node.nodeId, attrName, key, dom);
9806
+ };
9807
+ if (keys)
9808
+ imKeysIter(seq, renderOne, keys);
9809
+ else
9810
+ getSeqInfo(seq)(seq, (key, value, attrName) => {
9811
+ if (filter.call(stack.it, key, value, iterData))
9812
+ renderOne(key, value, attrName);
9813
+ }, start, end);
9809
9814
  return r;
9810
9815
  }
9811
9816
  renderEachWhen(stack, iterInfo, view, nid) {
9812
9817
  const { seq, filter, loopWith, enricher } = iterInfo.eval(stack);
9813
9818
  const r = [];
9814
9819
  const it = stack.it;
9815
- const { iterData, start, end } = unpackLoopResult(loopWith.call(it, seq), seq);
9816
- getSeqInfo(seq)(seq, (key, value, attrName) => {
9817
- if (filter.call(it, key, value, iterData)) {
9818
- const cachePath = enricher ? [view, it, value] : [view, value];
9819
- const binds = { key, value };
9820
- const cacheKey = `${nid}-${key}`;
9821
- if (enricher)
9822
- enricher.call(it, binds, key, value, iterData);
9823
- const cachedNode = this.cache.get(cachePath, cacheKey);
9824
- if (cachedNode)
9825
- this.pushEachEntry(r, nid, attrName, key, cachedNode);
9826
- else {
9827
- const dom = this.renderView(view, stack.enter(value, binds, false));
9828
- this.pushEachEntry(r, nid, attrName, key, dom);
9829
- this.cache.set(cachePath, cacheKey, dom);
9830
- }
9820
+ const { iterData, start, end, keys } = unpackLoopResult(loopWith.call(it, seq, makeLoopCtx(stack, filter)), seq);
9821
+ const renderOne = (key, value, attrName) => {
9822
+ const cachePath = enricher ? [view, it, value] : [view, value];
9823
+ const binds = { key, value };
9824
+ const cacheKey = `${nid}-${key}`;
9825
+ if (enricher)
9826
+ enricher.call(it, binds, key, value, iterData);
9827
+ const cachedNode = this.cache.get(cachePath, cacheKey);
9828
+ if (cachedNode)
9829
+ this.pushEachEntry(r, nid, attrName, key, cachedNode);
9830
+ else {
9831
+ const dom = this.renderView(view, stack.enter(value, binds, false));
9832
+ this.pushEachEntry(r, nid, attrName, key, dom);
9833
+ this.cache.set(cachePath, cacheKey, dom);
9831
9834
  }
9832
- }, start, end);
9835
+ };
9836
+ if (keys)
9837
+ imKeysIter(seq, renderOne, keys);
9838
+ else
9839
+ getSeqInfo(seq)(seq, (key, value, attrName) => {
9840
+ if (filter.call(it, key, value, iterData))
9841
+ renderOne(key, value, attrName);
9842
+ }, start, end);
9833
9843
  return r;
9834
9844
  }
9835
9845
  renderView(view, stack) {
@@ -9861,8 +9871,15 @@ var DATASET_ATTRS, getSeqInfo = (seq) => isIndexed(seq) ? imIndexedIter : isKeye
9861
9871
  return [s, e < s ? s : e];
9862
9872
  }, filterAlwaysTrue = (_v, _k, _seq) => true, nullLoopWith = (seq) => ({ iterData: { seq } }), unpackLoopResult = (result, seq) => {
9863
9873
  const r = result ?? {};
9864
- return { iterData: r.iterData ?? { seq }, start: r.start, end: r.end };
9865
- }, imIndexedIter = (seq, visit, start, end) => {
9874
+ return { iterData: r.iterData ?? { seq }, start: r.start, end: r.end, keys: r.keys };
9875
+ }, imKeysIter = (seq, visit, keys) => {
9876
+ const attrName = isIndexed(seq) ? "si" : "sk";
9877
+ for (const key of keys)
9878
+ visit(key, seq.get(key), attrName);
9879
+ }, makeLoopCtx = (stack, filter) => ({
9880
+ lookup: (name) => stack.lookupBind(name),
9881
+ filter: (key, value, iterData) => filter.call(stack.it, key, value, iterData)
9882
+ }), imIndexedIter = (seq, visit, start, end) => {
9866
9883
  const [s, e] = normalizeRange(start, end, seq.size);
9867
9884
  for (let i = s;i < e; i++)
9868
9885
  visit(i, seq.get(i), "si");
@@ -10049,11 +10066,11 @@ class IterInfo {
10049
10066
  return { seq, filter, loopWith, enricher };
10050
10067
  }
10051
10068
  enrichBinds(stack, key) {
10052
- const { seq, loopWith, enricher } = this.eval(stack);
10069
+ const { seq, filter, loopWith, enricher } = this.eval(stack);
10053
10070
  const value = seq?.get ? seq.get(key, null) : null;
10054
10071
  const binds = { key, value };
10055
10072
  if (enricher) {
10056
- const { iterData } = unpackLoopResult(loopWith.call(stack.it, seq), seq);
10073
+ const { iterData } = unpackLoopResult(loopWith.call(stack.it, seq, makeLoopCtx(stack, filter)), seq);
10057
10074
  enricher.call(stack.it, binds, key, value, iterData);
10058
10075
  }
10059
10076
  return binds;
@@ -11377,7 +11394,7 @@ var init_html_tokenizer = __esm(() => {
11377
11394
  });
11378
11395
 
11379
11396
  // tools/core/htmllinter-tables.js
11380
- var VOID_ELEMENTS, RAW_TEXT_ELEMENTS, RCDATA_ELEMENTS, SPECIAL_ELEMENTS, FORMATTING_ELEMENTS, DEFAULT_SCOPE_BOUNDARIES, MATHML_TEXT_INTEGRATION_POINT_NAMES, SVG_HTML_INTEGRATION_POINT_NAMES, SCOPE_LIST_ITEM, SCOPE_BUTTON, SCOPE_DEFAULT, SCOPE_TABLE, SCOPE_SELECT, STANDARD_SVG_CAMEL_ELEMENTS, STANDARD_SVG_CAMEL_ATTRS, MATHML_CAMEL_ATTRS, SVG_ATTR_LOWERCASE_TO_CAMEL, MATHML_ATTR_LOWERCASE_TO_CAMEL, FOREIGN_BREAKOUT_TAGS, MATHML_TEXT_INTEGRATION_POINTS, BLOCK_LEVEL_AUTO_CLOSE_P, SELECT_VALID_CHILDREN, SELECT_BREAKOUT_TAGS, MODES, NS, FRAGMENT_CONTEXT_MODES;
11397
+ var VOID_ELEMENTS, RAW_TEXT_ELEMENTS, SPECIAL_ELEMENTS, FORMATTING_ELEMENTS, DEFAULT_SCOPE_BOUNDARIES, MATHML_TEXT_INTEGRATION_POINT_NAMES, SVG_HTML_INTEGRATION_POINT_NAMES, SCOPE_LIST_ITEM, SCOPE_BUTTON, SCOPE_DEFAULT, SCOPE_TABLE, STANDARD_SVG_CAMEL_ELEMENTS, STANDARD_SVG_CAMEL_ATTRS, MATHML_CAMEL_ATTRS, SVG_ATTR_LOWERCASE_TO_CAMEL, MATHML_ATTR_LOWERCASE_TO_CAMEL, FOREIGN_BREAKOUT_TAGS, BLOCK_LEVEL_AUTO_CLOSE_P, SELECT_BREAKOUT_TAGS, MODES, NS, FRAGMENT_CONTEXT_MODES;
11381
11398
  var init_htmllinter_tables = __esm(() => {
11382
11399
  VOID_ELEMENTS = new Set([
11383
11400
  "area",
@@ -11404,7 +11421,6 @@ var init_htmllinter_tables = __esm(() => {
11404
11421
  "xmp",
11405
11422
  "plaintext"
11406
11423
  ]);
11407
- RCDATA_ELEMENTS = new Set(["textarea", "title"]);
11408
11424
  SPECIAL_ELEMENTS = new Set([
11409
11425
  "address",
11410
11426
  "applet",
@@ -11519,7 +11535,6 @@ var init_htmllinter_tables = __esm(() => {
11519
11535
  SCOPE_BUTTON = new Set([...DEFAULT_SCOPE_BOUNDARIES, "button"]);
11520
11536
  SCOPE_DEFAULT = DEFAULT_SCOPE_BOUNDARIES;
11521
11537
  SCOPE_TABLE = new Set(["html", "table", "template"]);
11522
- SCOPE_SELECT = new Set;
11523
11538
  STANDARD_SVG_CAMEL_ELEMENTS = new Set([
11524
11539
  "altGlyph",
11525
11540
  "altGlyphDef",
@@ -11671,7 +11686,6 @@ var init_htmllinter_tables = __esm(() => {
11671
11686
  "ul",
11672
11687
  "var"
11673
11688
  ]);
11674
- MATHML_TEXT_INTEGRATION_POINTS = new Set(["mi", "mo", "mn", "ms", "mtext"]);
11675
11689
  BLOCK_LEVEL_AUTO_CLOSE_P = new Set([
11676
11690
  "address",
11677
11691
  "article",
@@ -11714,7 +11728,6 @@ var init_htmllinter_tables = __esm(() => {
11714
11728
  "dd",
11715
11729
  "dt"
11716
11730
  ]);
11717
- SELECT_VALID_CHILDREN = new Set(["option", "optgroup", "hr", "script", "template"]);
11718
11731
  SELECT_BREAKOUT_TAGS = new Set(["input", "keygen", "textarea", "select"]);
11719
11732
  MODES = Object.freeze({
11720
11733
  inBody: "inBody",
@@ -14760,6 +14773,13 @@ function phaseOps(phase) {
14760
14773
  function resolveArgs(args, self) {
14761
14774
  return typeof args === "function" ? args(self) ?? [] : args ?? [];
14762
14775
  }
14776
+ function phaseHasBubble(phase) {
14777
+ if (!phase)
14778
+ return false;
14779
+ if (phase.bubble?.length)
14780
+ return true;
14781
+ return (phase.do ?? []).some((op) => op.type === "bubble");
14782
+ }
14763
14783
  function dispatchPhase(dispatcher, targetPath, phase, self) {
14764
14784
  if (!phase)
14765
14785
  return;
@@ -14938,6 +14958,8 @@ async function driveStack(stack, value, phase, opts = {}) {
14938
14958
  const t = info?.transaction;
14939
14959
  opts.onMessage({ kind: t?.handlerProp ?? "input", name: t?.name, args: t?.args, path: t?.path }, old, val);
14940
14960
  });
14961
+ if (phaseHasBubble(phase))
14962
+ console.warn("drive(): a `bubble` action is a no-op here — drive originates at the root and bubbles travel child→parent, so there is no ancestor to receive it (and the root's own bubble handler is skipped). To exercise a bubble handler, call it directly.");
14941
14963
  dispatchPhase(rootDispatcher(transactor), new Path([]), phase, value);
14942
14964
  await transactor.settle();
14943
14965
  return transactor.state.val;
@@ -15113,13 +15135,17 @@ var init__registry = __esm(() => {
15113
15135
  exitOn: (result) => result.hasErrors ? 3 : 0
15114
15136
  },
15115
15137
  test: {
15116
- describe: "Run tests defined by getTests() (optional <name> to filter by component).",
15138
+ describe: "Run tests defined by getTests() (optional <name> to filter by component). Pass a directory to run every *.test.js / *.dev.js under it.",
15117
15139
  defaultFormat: "cli",
15118
15140
  needsEnv: true,
15119
15141
  parseOptions: {
15120
15142
  grep: { type: "string" },
15121
15143
  bail: { type: "boolean" }
15122
15144
  },
15145
+ acceptsDir: true,
15146
+ dirMatch: (name) => name.endsWith(".test.js") || name.endsWith(".dev.js"),
15147
+ dirFilter: (normalized) => typeof normalized.mod.getTests === "function",
15148
+ mergeResults: (reports) => new TestReport({ modules: reports.flatMap((r) => r.modules) }),
15123
15149
  run: (normalized, { values, positionals }) => runTests({
15124
15150
  getTests: normalized.mod.getTests,
15125
15151
  components: normalized.components,
@@ -16011,6 +16037,23 @@ var init_env2 = __esm(() => {
16011
16037
  init_lint_check();
16012
16038
  });
16013
16039
 
16040
+ // tools/cli/walk.js
16041
+ import { readdirSync as readdirSync2 } from "node:fs";
16042
+ import { resolve as resolve4 } from "node:path";
16043
+ function walkFiles(dir, { match }, acc = []) {
16044
+ for (const e of readdirSync2(dir, { withFileTypes: true })) {
16045
+ if (e.name.startsWith(".") || e.name === "node_modules")
16046
+ continue;
16047
+ const full = resolve4(dir, e.name);
16048
+ if (e.isDirectory())
16049
+ walkFiles(full, { match }, acc);
16050
+ else if (e.isFile() && match(e.name))
16051
+ acc.push(full);
16052
+ }
16053
+ return acc;
16054
+ }
16055
+ var init_walk = () => {};
16056
+
16014
16057
  // tools/cli/commands/storybook.js
16015
16058
  var exports_storybook = {};
16016
16059
  __export(exports_storybook, {
@@ -16021,45 +16064,34 @@ import {
16021
16064
  createReadStream,
16022
16065
  existsSync as existsSync4,
16023
16066
  mkdirSync as mkdirSync3,
16024
- readdirSync as readdirSync2,
16025
16067
  readFileSync as readFileSync3,
16026
16068
  statSync,
16027
16069
  writeFileSync
16028
16070
  } from "node:fs";
16029
16071
  import { createServer } from "node:http";
16030
- import { dirname as dirname4, join, normalize, relative as relative2, resolve as resolve4, sep } from "node:path";
16072
+ import { dirname as dirname4, join, normalize, relative as relative2, resolve as resolve5, sep } from "node:path";
16031
16073
  import { fileURLToPath as fileURLToPath4 } from "node:url";
16032
16074
  import { parseArgs as parseArgs3 } from "node:util";
16033
- function findDevModules(root, dir, acc) {
16034
- for (const e of readdirSync2(dir, { withFileTypes: true })) {
16035
- if (e.name.startsWith(".") || e.name === "node_modules")
16036
- continue;
16037
- const full = resolve4(dir, e.name);
16038
- if (e.isDirectory()) {
16039
- findDevModules(root, full, acc);
16040
- } else if (e.isFile() && e.name.endsWith(".dev.js")) {
16041
- acc.push(`/${relative2(root, full).split(sep).join("/")}`);
16042
- }
16043
- }
16044
- return acc;
16075
+ function findDevModules(root) {
16076
+ return walkFiles(root, { match: (name) => name.endsWith(".dev.js") }).map((full) => `/${relative2(root, full).split(sep).join("/")}`);
16045
16077
  }
16046
16078
  function findSelf() {
16047
16079
  const here = dirname4(fileURLToPath4(import.meta.url));
16048
16080
  const pkgCandidates = [
16049
- resolve4(here, "..", "..", "..", "package.json"),
16050
- resolve4(here, "..", "package.json")
16081
+ resolve5(here, "..", "..", "..", "package.json"),
16082
+ resolve5(here, "..", "package.json")
16051
16083
  ];
16052
16084
  const pkgPath = pkgCandidates.find(existsSync4);
16053
16085
  const version = pkgPath ? JSON.parse(readFileSync3(pkgPath, "utf8")).version : "latest";
16054
- const distCandidates = [resolve4(here, "..", "..", "..", "dist"), resolve4(here, ".")];
16055
- const distRoot = distCandidates.find((d) => existsSync4(resolve4(d, "tutuca-storybook.js"))) ?? null;
16086
+ const distCandidates = [resolve5(here, "..", "..", "..", "dist"), resolve5(here, ".")];
16087
+ const distRoot = distCandidates.find((d) => existsSync4(resolve5(d, "tutuca-storybook.js"))) ?? null;
16056
16088
  return { version, distRoot };
16057
16089
  }
16058
16090
  function resolveTutucaBase(projectDir, self, forCdn) {
16059
16091
  if (forCdn)
16060
16092
  return { base: `https://cdn.jsdelivr.net/npm/tutuca@${self.version}/dist`, serveDist: null };
16061
- const nm = resolve4(projectDir, "node_modules", "tutuca", "dist");
16062
- if (existsSync4(resolve4(nm, "tutuca-dev.js"))) {
16093
+ const nm = resolve5(projectDir, "node_modules", "tutuca", "dist");
16094
+ if (existsSync4(resolve5(nm, "tutuca-dev.js"))) {
16063
16095
  return { base: "/node_modules/tutuca/dist", serveDist: null };
16064
16096
  }
16065
16097
  if (self.distRoot)
@@ -16079,7 +16111,8 @@ function buildImports(base, { margaui }) {
16079
16111
  tutuca: dev,
16080
16112
  "tutuca/extra": dev,
16081
16113
  "tutuca/dev": dev,
16082
- "tutuca/storybook": `${base}/tutuca-storybook.js`
16114
+ "tutuca/storybook": `${base}/tutuca-storybook.js`,
16115
+ "tutuca/components": `${base}/tutuca-components.js`
16083
16116
  };
16084
16117
  if (margaui)
16085
16118
  imports.margaui = MARGAUI_CDN;
@@ -16105,19 +16138,27 @@ ${JSON.stringify({ imports }, null, 6)}
16105
16138
  </html>
16106
16139
  `;
16107
16140
  }
16108
- function renderBootstrap(devModuleUrls, { margaui, check }) {
16141
+ function renderBootstrap(devModuleUrls, { margaui, check, inspect: inspect3 }) {
16109
16142
  const lines = ['import { mountStorybook } from "tutuca/storybook";'];
16110
16143
  if (margaui) {
16111
16144
  lines.push('import { compileClassesToStyleText } from "tutuca/extra";');
16112
16145
  lines.push('import { compile } from "margaui";');
16113
16146
  }
16147
+ if (inspect3) {
16148
+ lines.push('import { shadowCheckComponent, runTests, expect } from "tutuca/dev";');
16149
+ }
16114
16150
  if (check)
16115
16151
  lines.push('import { check } from "tutuca/dev";');
16116
16152
  devModuleUrls.forEach((url, i) => {
16117
16153
  lines.push(`import * as m${i} from ${JSON.stringify(url)};`);
16118
16154
  });
16119
16155
  const modules = devModuleUrls.map((_, i) => `m${i}`).join(", ");
16120
- const opts = margaui ? "{ compileCss: (app) => compileClassesToStyleText(app, compile) }" : "{}";
16156
+ const optParts = [];
16157
+ if (margaui)
16158
+ optParts.push("compileCss: (app) => compileClassesToStyleText(app, compile)");
16159
+ if (inspect3)
16160
+ optParts.push("dev: { shadowCheckComponent, runTests, expect }");
16161
+ const opts = optParts.length ? `{ ${optParts.join(", ")} }` : "{}";
16121
16162
  lines.push("");
16122
16163
  lines.push(`const app = await mountStorybook("#app", [${modules}], ${opts});`);
16123
16164
  if (check)
@@ -16134,7 +16175,7 @@ async function runDevTests(projectDir, devModuleUrls) {
16134
16175
  const failures = [];
16135
16176
  const importErrors = [];
16136
16177
  for (const url of devModuleUrls) {
16137
- const abs = resolve4(projectDir, url.slice(1));
16178
+ const abs = resolve5(projectDir, url.slice(1));
16138
16179
  let mod;
16139
16180
  try {
16140
16181
  mod = await import(abs);
@@ -16166,7 +16207,7 @@ async function discoverModules(projectDir, devModuleUrls) {
16166
16207
  await createNodeEnv();
16167
16208
  const modules = [];
16168
16209
  for (const url of devModuleUrls) {
16169
- const abs = resolve4(projectDir, url.slice(1));
16210
+ const abs = resolve5(projectDir, url.slice(1));
16170
16211
  try {
16171
16212
  const mod = await import(abs);
16172
16213
  const { normalized, present } = normalizeModule(mod, { path: abs });
@@ -16231,6 +16272,7 @@ async function run4(argv, opts = {}) {
16231
16272
  out: { type: "string" },
16232
16273
  "no-margaui": { type: "boolean", default: false },
16233
16274
  "no-check": { type: "boolean", default: false },
16275
+ "no-inspect": { type: "boolean", default: false },
16234
16276
  "no-tests": { type: "boolean", default: false },
16235
16277
  "dry-run": { type: "boolean", default: false },
16236
16278
  help: { type: "boolean", short: "h", default: false }
@@ -16239,7 +16281,7 @@ async function run4(argv, opts = {}) {
16239
16281
  });
16240
16282
  if (parsed.values.help) {
16241
16283
  process.stdout.write(`tutuca storybook [dir] [--port <n>] [--out <dir>] [--dry-run]
16242
- [--no-margaui] [--no-check] [--no-tests]
16284
+ [--no-margaui] [--no-check] [--no-inspect] [--no-tests]
16243
16285
 
16244
16286
  Auto-discovers co-located *.dev.js modules (recursively, skipping
16245
16287
  node_modules/dotdirs) and serves a live storybook that mounts them via
@@ -16254,11 +16296,12 @@ async function run4(argv, opts = {}) {
16254
16296
  shown instead of serving; pass --json for structured output
16255
16297
  --no-margaui skip margaui styling (renders functional but unstyled)
16256
16298
  --no-check skip the in-browser check(app) dev validation
16299
+ --no-inspect skip the per-example Component/Instance/Data/Lint/Test tabs
16257
16300
  --no-tests skip running the modules' getTests() before serving
16258
16301
  `);
16259
16302
  return;
16260
16303
  }
16261
- const projectDir = resolve4(parsed.positionals[0] ?? process.cwd());
16304
+ const projectDir = resolve5(parsed.positionals[0] ?? process.cwd());
16262
16305
  if (!existsSync4(projectDir) || !statSync(projectDir).isDirectory()) {
16263
16306
  emitError(opts, {
16264
16307
  code: CODES.USAGE_MISSING_ARGUMENT,
@@ -16266,7 +16309,7 @@ async function run4(argv, opts = {}) {
16266
16309
  hint: "Pass a project directory to scan, or omit it to use the current directory."
16267
16310
  });
16268
16311
  }
16269
- const devModuleUrls = findDevModules(projectDir, projectDir, []);
16312
+ const devModuleUrls = findDevModules(projectDir);
16270
16313
  if (devModuleUrls.length === 0) {
16271
16314
  emitError(opts, {
16272
16315
  code: CODES.USAGE_MISSING_ARGUMENT,
@@ -16276,15 +16319,16 @@ async function run4(argv, opts = {}) {
16276
16319
  }
16277
16320
  const margaui = !parsed.values["no-margaui"];
16278
16321
  const check = !parsed.values["no-check"];
16322
+ const inspect3 = !parsed.values["no-inspect"];
16279
16323
  const self = findSelf();
16280
16324
  if (parsed.values.out) {
16281
- const outDir = resolve4(parsed.values.out);
16325
+ const outDir = resolve5(parsed.values.out);
16282
16326
  mkdirSync3(outDir, { recursive: true });
16283
16327
  const { base: base2 } = resolveTutucaBase(projectDir, self, true);
16284
16328
  const imports2 = buildImports(base2, { margaui });
16285
16329
  const bootstrapName = "tutuca-storybook.bootstrap.js";
16286
- writeFileSync(resolve4(outDir, "index.html"), renderIndexHtml(imports2, { margaui, bootstrapUrl: `./${bootstrapName}` }));
16287
- writeFileSync(resolve4(outDir, bootstrapName), renderBootstrap(devModuleUrls, { margaui, check }));
16330
+ writeFileSync(resolve5(outDir, "index.html"), renderIndexHtml(imports2, { margaui, bootstrapUrl: `./${bootstrapName}` }));
16331
+ writeFileSync(resolve5(outDir, bootstrapName), renderBootstrap(devModuleUrls, { margaui, check, inspect: inspect3 }));
16288
16332
  process.stdout.write(`wrote static storybook → ${relative2(process.cwd(), outDir) || "."}/
16289
16333
  index.html + ${bootstrapName} (${devModuleUrls.length} dev modules, CDN import map)
16290
16334
  Host it from the project root so /*.dev.js paths resolve.
@@ -16369,7 +16413,7 @@ async function run4(argv, opts = {}) {
16369
16413
  const { base, serveDist } = resolveTutucaBase(projectDir, self, false);
16370
16414
  const imports = buildImports(base, { margaui });
16371
16415
  const indexHtml = renderIndexHtml(imports, { margaui, bootstrapUrl: BOOTSTRAP_URL });
16372
- const bootstrapJs = renderBootstrap(devModuleUrls, { margaui, check });
16416
+ const bootstrapJs = renderBootstrap(devModuleUrls, { margaui, check, inspect: inspect3 });
16373
16417
  const server = createServer((req, res) => {
16374
16418
  const path = req.url.split("?")[0];
16375
16419
  if (path === "/" || path === "/index.html") {
@@ -16410,6 +16454,7 @@ var init_storybook = __esm(() => {
16410
16454
  init_test();
16411
16455
  init_env2();
16412
16456
  init_errors();
16457
+ init_walk();
16413
16458
  MIME = {
16414
16459
  ".js": "text/javascript",
16415
16460
  ".mjs": "text/javascript",
@@ -16943,17 +16988,60 @@ ${group}
16943
16988
  // tools/tutuca.js
16944
16989
  init_install_skill();
16945
16990
  init_storybook();
16991
+
16992
+ // tools/cli/dev-build-hook.js
16993
+ import { register } from "node:module";
16994
+ function installDevBuildResolveHook() {
16995
+ let devUrl;
16996
+ try {
16997
+ devUrl = import.meta.resolve("tutuca/dev");
16998
+ } catch {
16999
+ return false;
17000
+ }
17001
+ const hookSource = `
17002
+ let devUrl;
17003
+ export async function initialize(data) { devUrl = data.devUrl; }
17004
+ export async function resolve(specifier, context, nextResolve) {
17005
+ return specifier === "tutuca"
17006
+ ? { url: devUrl, shortCircuit: true }
17007
+ : nextResolve(specifier, context);
17008
+ }`;
17009
+ try {
17010
+ register(`data:text/javascript,${encodeURIComponent(hookSource)}`, import.meta.url, {
17011
+ data: { devUrl }
17012
+ });
17013
+ return true;
17014
+ } catch {
17015
+ return false;
17016
+ }
17017
+ }
17018
+
17019
+ // tools/tutuca.js
16946
17020
  init_errors();
16947
17021
 
16948
17022
  // tools/cli/with-module.js
16949
17023
  init_env2();
17024
+ init_errors();
17025
+ import { statSync as statSync2 } from "node:fs";
16950
17026
  import { parseArgs as parseArgs4 } from "node:util";
16951
17027
 
16952
17028
  // tools/cli/load.js
16953
- import { resolve as resolve5 } from "node:path";
17029
+ import { existsSync as existsSync5 } from "node:fs";
17030
+ import { resolve as resolve6 } from "node:path";
16954
17031
  async function loadAndNormalize(modulePath) {
16955
- const abs = resolve5(modulePath);
16956
- const mod = await import(abs);
17032
+ const abs = resolve6(modulePath);
17033
+ let mod;
17034
+ try {
17035
+ mod = await import(abs);
17036
+ } catch (e) {
17037
+ const entryProblem = e?.code === "ERR_UNSUPPORTED_DIR_IMPORT" || e?.code === "ERR_MODULE_NOT_FOUND" && !existsSync5(abs);
17038
+ if (entryProblem) {
17039
+ const err = new Error(e.code === "ERR_UNSUPPORTED_DIR_IMPORT" ? `expected a module file, got a directory: ${modulePath}` : `module not found: ${modulePath}`);
17040
+ err.code = "ERR_MODULE_LOAD_FAILED";
17041
+ throw err;
17042
+ }
17043
+ throw e;
17044
+ }
16957
17045
  const { normalized } = normalizeModule(mod, { path: abs });
16958
17046
  return normalized;
16959
17047
  }
@@ -17456,15 +17544,57 @@ async function emit(result, { format: format5, pretty, output }) {
17456
17544
  }
17457
17545
 
17458
17546
  // tools/cli/with-module.js
17547
+ init_walk();
17459
17548
  async function runCommand(cmd, argv, globalOpts) {
17460
17549
  const parsed = parseArgs4({
17461
17550
  args: argv,
17462
17551
  options: cmd.parseOptions ?? {},
17463
17552
  allowPositionals: true
17464
17553
  });
17554
+ let stat = null;
17555
+ try {
17556
+ stat = statSync2(globalOpts.module);
17557
+ } catch {}
17558
+ if (stat?.isDirectory()) {
17559
+ await runOnDir(cmd, parsed, globalOpts);
17560
+ return;
17561
+ }
17465
17562
  const normalized = await loadAndNormalize(globalOpts.module);
17466
17563
  const env = cmd.needsEnv ? await createNodeEnv() : null;
17467
17564
  const result = await cmd.run(normalized, parsed, env);
17565
+ await emitResult(cmd, result, globalOpts);
17566
+ }
17567
+ async function runOnDir(cmd, parsed, globalOpts) {
17568
+ if (!cmd.acceptsDir) {
17569
+ emitError(globalOpts, {
17570
+ code: CODES.MODULE_LOAD_FAILED,
17571
+ message: `expected a module file, got a directory: ${globalOpts.module}`,
17572
+ hint: "This command takes a single module file (.js). Pass a file path."
17573
+ });
17574
+ return;
17575
+ }
17576
+ const files = walkFiles(globalOpts.module, { match: cmd.dirMatch });
17577
+ const normalizedAll = [];
17578
+ for (const file of files) {
17579
+ const normalized = await loadAndNormalize(file);
17580
+ if (!cmd.dirFilter || cmd.dirFilter(normalized))
17581
+ normalizedAll.push(normalized);
17582
+ }
17583
+ if (normalizedAll.length === 0) {
17584
+ emitError(globalOpts, {
17585
+ code: CODES.MODULE_LOAD_FAILED,
17586
+ message: `no test modules found under ${globalOpts.module}`,
17587
+ hint: "Test modules are *.test.js / *.dev.js files that export getTests()."
17588
+ });
17589
+ return;
17590
+ }
17591
+ const env = cmd.needsEnv ? await createNodeEnv() : null;
17592
+ const results = [];
17593
+ for (const normalized of normalizedAll)
17594
+ results.push(await cmd.run(normalized, parsed, env));
17595
+ await emitResult(cmd, cmd.mergeResults(results), globalOpts);
17596
+ }
17597
+ async function emitResult(cmd, result, globalOpts) {
17468
17598
  await emit(result, {
17469
17599
  format: globalOpts.format ?? cmd.defaultFormat,
17470
17600
  pretty: globalOpts.pretty,
@@ -17573,6 +17703,8 @@ async function main() {
17573
17703
  opts.module = rest[1];
17574
17704
  commandArgs = rest.slice(2);
17575
17705
  }
17706
+ if (command === "test")
17707
+ installDevBuildResolveHook();
17576
17708
  try {
17577
17709
  await runCommand(cmd, commandArgs, opts);
17578
17710
  } catch (e) {
@@ -17610,6 +17742,13 @@ async function main() {
17610
17742
  hint: shape.hint ?? `Run \`tutuca help ${command}\` for valid flags.`
17611
17743
  });
17612
17744
  }
17745
+ if (e?.code === "ERR_MODULE_LOAD_FAILED") {
17746
+ emitError(opts, {
17747
+ code: CODES.MODULE_LOAD_FAILED,
17748
+ message: e.message,
17749
+ hint: "Pass a module file path. `tutuca test` also accepts a directory to run every test module under it."
17750
+ });
17751
+ }
17613
17752
  throw e;
17614
17753
  }
17615
17754
  }