tutuca 0.9.29 → 0.9.31

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
@@ -11,6 +11,9 @@ Zero-dependency batteries included SPA framework.
11
11
 
12
12
  ## Quick Start
13
13
 
14
+ For an interactive walk-through with editable examples, see the
15
+ [tutorial](https://marianoguerra.github.io/tutuca/tutorial.html).
16
+
14
17
  ### CDN (no install)
15
18
 
16
19
  ```html
@@ -92,6 +95,7 @@ tutuca help [command]
92
95
  | `lint [name]` | Run lint checks — all, or one by name (exit 2 on errors) |
93
96
  | `render [name] [--title t] [--view v]` | Render examples to HTML |
94
97
  | `doctor` | Lint + render smoke test over the whole module |
98
+ | `install-skill [--user] [--force]` | Install the tutuca Claude Code skill (no module path needed) |
95
99
 
96
100
  Global flags: `-f, --format <cli\|md\|json\|html>`, `-o, --output <file>`, `--pretty`, `-h, --help`.
97
101
 
@@ -135,6 +139,27 @@ The invocation stays short even without wrapping, but common patterns:
135
139
  - **`justfile` / `Makefile`** — one recipe per subcommand, passing through positionals
136
140
  - **Programmatic** — `import "tutuca/cli"` (the bundled entry) for custom build integration
137
141
 
142
+ ## Use with Claude Code
143
+
144
+ Tutuca ships an LLM-facing reference (`SKILL.md` + `core.md` / `cli.md` /
145
+ `advanced.md`) packaged as a [Claude Code skill](https://docs.claude.com/en/docs/claude-code/skills).
146
+ Once installed, Claude auto-loads it whenever a session touches tutuca
147
+ components, views, macros, or the CLI.
148
+
149
+ ```sh
150
+ # project-scoped: writes ./.claude/skills/tutuca/ (commit it for the team)
151
+ npx tutuca install-skill
152
+
153
+ # or user-scoped: writes ~/.claude/skills/tutuca/
154
+ npx tutuca install-skill --user
155
+
156
+ # overwrite an existing install
157
+ npx tutuca install-skill --force
158
+ ```
159
+
160
+ The skill content is generated from `docs/llm/`, so the same reference
161
+ runs locally (`tutuca <module> doctor`) and inside Claude.
162
+
138
163
  ## License
139
164
 
140
165
  MIT
@@ -1394,7 +1394,7 @@ class ParseContext {
1394
1394
  parseHTML(html) {
1395
1395
  const t = this.document.createElement("template");
1396
1396
  t.innerHTML = html;
1397
- return Array.from(t.content.childNodes);
1397
+ return t.content.childNodes;
1398
1398
  }
1399
1399
  addNodeIf(Class, val, extra) {
1400
1400
  if (val !== null) {
@@ -1614,7 +1614,20 @@ var init_anode = __esm(() => {
1614
1614
  return this.val.toPathItem();
1615
1615
  }
1616
1616
  static parse(html, px) {
1617
- return ANode.fromDOM(px.parseHTML(html)[0] ?? new px.Text(""), px);
1617
+ const nodes = px.parseHTML(html);
1618
+ if (nodes.length === 0)
1619
+ return new TextNode("");
1620
+ if (nodes.length === 1)
1621
+ return ANode.fromDOM(nodes[0], px);
1622
+ const childs = new Array(nodes.length);
1623
+ for (let i = 0;i < nodes.length; i++)
1624
+ childs[i] = ANode.fromDOM(nodes[i], px);
1625
+ const trimmed = condenseChildsWhites(childs);
1626
+ if (trimmed.length === 0)
1627
+ return new TextNode("");
1628
+ if (trimmed.length === 1)
1629
+ return trimmed[0];
1630
+ return new FragmentNode(trimmed);
1618
1631
  }
1619
1632
  static fromDOM(node, px) {
1620
1633
  if (node instanceof px.Text)
@@ -1634,10 +1647,8 @@ var init_anode = __esm(() => {
1634
1647
  switch (name) {
1635
1648
  case "slot":
1636
1649
  return new SlotNode(null, vp.const(value), maybeFragment(childs));
1637
- case "text": {
1638
- const v = vp.parseText(value, px);
1639
- return v !== null ? new RenderTextNode(null, v) : null;
1640
- }
1650
+ case "text":
1651
+ return px.addNodeIf(RenderTextNode, vp.parseText(value, px));
1641
1652
  case "render":
1642
1653
  return px.addNodeIf(RenderNode, vp.parseRender(value, px), as);
1643
1654
  case "render-it":
@@ -2025,7 +2036,11 @@ class ComponentStack {
2025
2036
  }
2026
2037
  }
2027
2038
  registerMacros(macros) {
2028
- Object.assign(this.macros, macros);
2039
+ for (const key in macros) {
2040
+ const lower = key.toLowerCase();
2041
+ console.assert(this.macros[lower] === undefined, "macro key collision", lower);
2042
+ this.macros[lower] = macros[key];
2043
+ }
2029
2044
  }
2030
2045
  getCompFor(v) {
2031
2046
  return this.comps.getCompFor(v);
@@ -2083,7 +2098,10 @@ function checkMacroCallArgs(lx, view, Comp) {
2083
2098
  const { defaults } = macro;
2084
2099
  for (const argName in macroNode.attrs) {
2085
2100
  if (!(argName in defaults)) {
2086
- lx.error(UNKNOWN_MACRO_ARG, { name: argName, macroName: macroNode.name });
2101
+ lx.error(UNKNOWN_MACRO_ARG, {
2102
+ name: argName,
2103
+ macroName: macroNode.name
2104
+ });
2087
2105
  }
2088
2106
  }
2089
2107
  }
@@ -2244,7 +2262,7 @@ function checkConsistentAttrVal(lx, val, fields, proto, computed, scope, alter,
2244
2262
  if (alter[val.name] === undefined) {
2245
2263
  lx.error(ALT_HANDLER_NOT_DEFINED, { name: val.name });
2246
2264
  }
2247
- } else if (valName !== "ConstVal" && valName !== "BindVal") {
2265
+ } else if (valName !== "ConstVal" && valName !== "BindVal" && valName !== "DynVal") {
2248
2266
  console.log(val);
2249
2267
  }
2250
2268
  }
@@ -2258,8 +2276,21 @@ function checkConsistentAttrs(lx, Comp, referencedAlters, referencedComputed) {
2258
2276
  for (const attr of view.ctx.attrs) {
2259
2277
  const { attrs, wrapperAttrs, textChild, isMacroCall } = attr;
2260
2278
  if (attrs?.constructor.name === "DynAttrs") {
2279
+ const seenNames = new Set;
2261
2280
  for (const attr2 of attrs.items) {
2262
- if (attr2?.constructor.name === "Attr") {
2281
+ const name = attr2?.name;
2282
+ if (name !== undefined && name !== "data-eid") {
2283
+ if (seenNames.has(name)) {
2284
+ lx.error(DUPLICATE_ATTR_DEFINITION, { name });
2285
+ } else {
2286
+ seenNames.add(name);
2287
+ }
2288
+ }
2289
+ if (attr2?.constructor.name === "IfAttr") {
2290
+ for (const subVal of [attr2.condVal, attr2.thenVal, attr2.elseVal]) {
2291
+ checkConsistentAttrVal(lx, subVal, fields, proto, computed, scope, alter, referencedAlters, referencedComputed, isMacroCall);
2292
+ }
2293
+ } else if (attr2?.val !== undefined) {
2263
2294
  checkConsistentAttrVal(lx, attr2.val, fields, proto, computed, scope, alter, referencedAlters, referencedComputed, isMacroCall);
2264
2295
  }
2265
2296
  }
@@ -2346,7 +2377,7 @@ class LintContext {
2346
2377
  this.reports.push({ id, info, level, context: { ...this.frame } });
2347
2378
  }
2348
2379
  }
2349
- var ALT_HANDLER_NOT_DEFINED = "ALT_HANDLER_NOT_DEFINED", ALT_HANDLER_NOT_REFERENCED = "ALT_HANDLER_NOT_REFERENCED", RENDER_IT_OUTSIDE_OF_LOOP = "RENDER_IT_OUTSIDE_OF_LOOP", UNKNOWN_EVENT_MODIFIER = "UNKNOWN_EVENT_MODIFIER", UNKNOWN_HANDLER_ARG_NAME = "UNKNOWN_HANDLER_ARG_NAME", INPUT_HANDLER_NOT_IMPLEMENTED = "INPUT_HANDLER_NOT_IMPLEMENTED", INPUT_HANDLER_NOT_REFERENCED = "INPUT_HANDLER_NOT_REFERENCED", INPUT_HANDLER_METHOD_NOT_IMPLEMENTED = "INPUT_HANDLER_METHOD_NOT_IMPLEMENTED", INPUT_HANDLER_FOR_INPUT_HANDLER_METHOD = "INPUT_HANDLER_FOR_INPUT_HANDLER_METHOD", INPUT_HANDLER_METHOD_FOR_INPUT_HANDLER = "INPUT_HANDLER_METHOD_FOR_INPUT_HANDLER", FIELD_VAL_NOT_DEFINED = "FIELD_VAL_NOT_DEFINED", COMPUTED_VAL_NOT_DEFINED = "COMPUTED_VAL_NOT_DEFINED", COMPUTED_NOT_REFERENCED = "COMPUTED_NOT_REFERENCED", UNKNOWN_REQUEST_NAME = "UNKNOWN_REQUEST_NAME", UNKNOWN_COMPONENT_NAME = "UNKNOWN_COMPONENT_NAME", UNKNOWN_MACRO_ARG = "UNKNOWN_MACRO_ARG", LEVEL_WARN = "warn", LEVEL_ERROR = "error", LEVEL_HINT = "hint", NO_WRAPPERS, KNOWN_HANDLER_NAMES, LintParseContext;
2380
+ var ALT_HANDLER_NOT_DEFINED = "ALT_HANDLER_NOT_DEFINED", ALT_HANDLER_NOT_REFERENCED = "ALT_HANDLER_NOT_REFERENCED", RENDER_IT_OUTSIDE_OF_LOOP = "RENDER_IT_OUTSIDE_OF_LOOP", UNKNOWN_EVENT_MODIFIER = "UNKNOWN_EVENT_MODIFIER", UNKNOWN_HANDLER_ARG_NAME = "UNKNOWN_HANDLER_ARG_NAME", INPUT_HANDLER_NOT_IMPLEMENTED = "INPUT_HANDLER_NOT_IMPLEMENTED", INPUT_HANDLER_NOT_REFERENCED = "INPUT_HANDLER_NOT_REFERENCED", INPUT_HANDLER_METHOD_NOT_IMPLEMENTED = "INPUT_HANDLER_METHOD_NOT_IMPLEMENTED", INPUT_HANDLER_FOR_INPUT_HANDLER_METHOD = "INPUT_HANDLER_FOR_INPUT_HANDLER_METHOD", INPUT_HANDLER_METHOD_FOR_INPUT_HANDLER = "INPUT_HANDLER_METHOD_FOR_INPUT_HANDLER", FIELD_VAL_NOT_DEFINED = "FIELD_VAL_NOT_DEFINED", COMPUTED_VAL_NOT_DEFINED = "COMPUTED_VAL_NOT_DEFINED", COMPUTED_NOT_REFERENCED = "COMPUTED_NOT_REFERENCED", DUPLICATE_ATTR_DEFINITION = "DUPLICATE_ATTR_DEFINITION", UNKNOWN_REQUEST_NAME = "UNKNOWN_REQUEST_NAME", UNKNOWN_COMPONENT_NAME = "UNKNOWN_COMPONENT_NAME", UNKNOWN_MACRO_ARG = "UNKNOWN_MACRO_ARG", LEVEL_WARN = "warn", LEVEL_ERROR = "error", LEVEL_HINT = "hint", NO_WRAPPERS, KNOWN_HANDLER_NAMES, LintParseContext;
2350
2381
  var init_lint_check = __esm(() => {
2351
2382
  init_anode();
2352
2383
  NO_WRAPPERS = {};
@@ -7706,7 +7737,7 @@ class Renderer {
7706
7737
  this.cache = new WeakMapDomCache;
7707
7738
  }
7708
7739
  getSeqInfo(seq) {
7709
- return isIndexed(seq) ? imIndexedIter : isKeyed(seq) ? imKeyedIter : seqInfoByClass.get(seq?.constructor) ?? unkIter;
7740
+ return isIndexed(seq) ? imIndexedIter : isKeyed(seq) ? imKeyedIter : seq?.[SEQ_INFO] ?? unkIter;
7710
7741
  }
7711
7742
  renderTag(tag, attrs, childs) {
7712
7743
  return h(tag, attrs, childs);
@@ -7822,12 +7853,12 @@ var DATASET_ATTRS, imIndexedIter = (seq, visit) => {
7822
7853
  }, imKeyedIter = (seq, visit) => {
7823
7854
  for (const [k, v] of seq.toSeq().entries())
7824
7855
  visit(k, v, "sk");
7825
- }, unkIter = () => {}, seqInfoByClass;
7856
+ }, unkIter = () => {}, SEQ_INFO;
7826
7857
  var init_renderer = __esm(() => {
7827
7858
  init_immutable();
7828
7859
  init_vdom();
7829
7860
  DATASET_ATTRS = ["nid", "cid", "eid", "vid", "si", "sk"];
7830
- seqInfoByClass = new Map;
7861
+ SEQ_INFO = Symbol.for("tutuca.seqInfo");
7831
7862
  });
7832
7863
 
7833
7864
  // src/util/render.js
@@ -8027,11 +8058,105 @@ var init__registry = __esm(() => {
8027
8058
  };
8028
8059
  });
8029
8060
 
8061
+ // tools/cli/commands/install-skill.js
8062
+ var exports_install_skill = {};
8063
+ __export(exports_install_skill, {
8064
+ run: () => run,
8065
+ describe: () => describe
8066
+ });
8067
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
8068
+ import { homedir } from "node:os";
8069
+ import { dirname, resolve } from "node:path";
8070
+ import { parseArgs } from "node:util";
8071
+ import { fileURLToPath } from "node:url";
8072
+ function findSkillDir() {
8073
+ const here = dirname(fileURLToPath(import.meta.url));
8074
+ const candidates = [resolve(here, "..", "..", "..", "skill"), resolve(here, "..", "skill")];
8075
+ for (const c of candidates) {
8076
+ if (existsSync(resolve(c, "SKILL.md")))
8077
+ return c;
8078
+ }
8079
+ return null;
8080
+ }
8081
+ function targetDir(scope) {
8082
+ if (scope === "user")
8083
+ return resolve(homedir(), ".claude/skills/tutuca");
8084
+ return resolve(process.cwd(), ".claude/skills/tutuca");
8085
+ }
8086
+ async function run(argv) {
8087
+ const parsed = parseArgs({
8088
+ args: argv,
8089
+ options: {
8090
+ user: { type: "boolean", default: false },
8091
+ project: { type: "boolean", default: false },
8092
+ force: { type: "boolean", short: "f", default: false },
8093
+ help: { type: "boolean", short: "h", default: false }
8094
+ },
8095
+ allowPositionals: false
8096
+ });
8097
+ if (parsed.values.help) {
8098
+ process.stdout.write(`tutuca install-skill [--user | --project] [--force]
8099
+ ` + `
8100
+ ` + ` Copies SKILL.md + core.md + cli.md + advanced.md into
8101
+ ` + ` .claude/skills/tutuca/. Defaults to --project (cwd).
8102
+ ` + ` --user installs at ~/.claude/skills/tutuca/.
8103
+ ` + ` --force overwrites existing files.
8104
+ `);
8105
+ return;
8106
+ }
8107
+ if (parsed.values.user && parsed.values.project) {
8108
+ process.stderr.write(`tutuca: --user and --project are mutually exclusive
8109
+ `);
8110
+ process.exit(1);
8111
+ }
8112
+ const scope = parsed.values.user ? "user" : "project";
8113
+ const target = targetDir(scope);
8114
+ const src = findSkillDir();
8115
+ if (!src) {
8116
+ process.stderr.write(`tutuca: skill assets not found alongside this CLI.
8117
+ ` + "If you're running from a checkout, run `bun scripts/build-skill.js` first.\n");
8118
+ process.exit(1);
8119
+ }
8120
+ if (existsSync(target) && !parsed.values.force) {
8121
+ const existing = readdirSync(target).filter((n) => SKILL_FILES.includes(n));
8122
+ if (existing.length > 0) {
8123
+ process.stderr.write(`tutuca: ${target} already contains skill files. Re-run with --force to overwrite.
8124
+ `);
8125
+ process.exit(1);
8126
+ }
8127
+ }
8128
+ mkdirSync(target, { recursive: true });
8129
+ for (const name of SKILL_FILES) {
8130
+ const from = resolve(src, name);
8131
+ if (!existsSync(from)) {
8132
+ process.stderr.write(`tutuca: missing skill asset: ${from}
8133
+ `);
8134
+ process.exit(1);
8135
+ }
8136
+ const buf = readFileSync(from);
8137
+ writeFileSync(resolve(target, name), buf);
8138
+ }
8139
+ const rel = scope === "project" ? ".claude/skills/tutuca" : target;
8140
+ process.stdout.write(`installed tutuca skill → ${rel}
8141
+ `);
8142
+ process.stdout.write(`Open a Claude Code session in this directory to use it.
8143
+ `);
8144
+ }
8145
+ var describe = "Install the tutuca Claude Code skill into .claude/skills/tutuca/.", SKILL_FILES;
8146
+ var init_install_skill = __esm(() => {
8147
+ SKILL_FILES = ["SKILL.md", "core.md", "cli.md", "advanced.md"];
8148
+ });
8149
+
8030
8150
  // tools/tutuca.js
8031
8151
  init__registry();
8032
8152
 
8033
8153
  // tools/cli/commands/help.js
8034
- var describe = "Show usage. `help <command>` for per-command detail.";
8154
+ var exports_help = {};
8155
+ __export(exports_help, {
8156
+ run: () => run2,
8157
+ describe: () => describe2
8158
+ });
8159
+ var describe2 = "Show usage. `help <command>` for per-command detail.";
8035
8160
  var OVERVIEW = `tutuca — CLI for inspecting, documenting, linting and rendering tutuca
8036
8161
  components defined in an ES module.
8037
8162
 
@@ -8109,6 +8234,11 @@ COMMANDS (no module required)
8109
8234
  Without [command]: prints this full reference.
8110
8235
  With [command]: prints that command's one-line description.
8111
8236
 
8237
+ install-skill [--user | --project] [--force]
8238
+ Copy the bundled Claude Code skill (SKILL.md + core/cli/advanced.md)
8239
+ into .claude/skills/tutuca/. Default scope is --project (cwd);
8240
+ --user installs at ~/.claude/skills/tutuca/. --force overwrites.
8241
+
8112
8242
  GLOBAL FLAGS
8113
8243
  -f, --format <cli|md|json|html>
8114
8244
  Output format. Defaults per command:
@@ -8150,19 +8280,22 @@ EXAMPLES
8150
8280
  # CI smoke test
8151
8281
  tutuca ./src/components.js doctor
8152
8282
  `;
8153
- async function run(argv) {
8283
+ async function run2(argv) {
8154
8284
  const target = argv?.[0];
8155
8285
  if (!target) {
8156
8286
  process.stdout.write(OVERVIEW);
8157
8287
  return;
8158
8288
  }
8159
8289
  if (target === "help") {
8160
- process.stdout.write(`help: ${describe}
8290
+ process.stdout.write(`help: ${describe2}
8161
8291
  `);
8162
8292
  return;
8163
8293
  }
8164
8294
  const { COMMANDS: COMMANDS2 } = await Promise.resolve().then(() => (init__registry(), exports__registry));
8165
- const cmd = COMMANDS2[target];
8295
+ const noModule = {
8296
+ "install-skill": await Promise.resolve().then(() => (init_install_skill(), exports_install_skill))
8297
+ };
8298
+ const cmd = COMMANDS2[target] ?? noModule[target];
8166
8299
  if (!cmd) {
8167
8300
  process.stderr.write(`tutuca: unknown command: ${target}
8168
8301
  `);
@@ -8174,8 +8307,11 @@ async function run(argv) {
8174
8307
  process.stdout.write("Run `tutuca help` for the full reference including signatures and flags.\n");
8175
8308
  }
8176
8309
 
8310
+ // tools/tutuca.js
8311
+ init_install_skill();
8312
+
8177
8313
  // tools/cli/with-module.js
8178
- import { parseArgs } from "node:util";
8314
+ import { parseArgs as parseArgs2 } from "node:util";
8179
8315
 
8180
8316
  // tools/cli/env.js
8181
8317
  init_anode();
@@ -8205,16 +8341,16 @@ async function createNodeEnv() {
8205
8341
  }
8206
8342
 
8207
8343
  // tools/cli/load.js
8208
- import { resolve } from "node:path";
8344
+ import { resolve as resolve2 } from "node:path";
8209
8345
  async function loadAndNormalize(modulePath) {
8210
- const abs = resolve(modulePath);
8346
+ const abs = resolve2(modulePath);
8211
8347
  const mod = await import(abs);
8212
8348
  const { normalized } = normalizeModule(mod, { path: abs });
8213
8349
  return normalized;
8214
8350
  }
8215
8351
 
8216
8352
  // tools/cli/output.js
8217
- import { writeFileSync } from "node:fs";
8353
+ import { writeFileSync as writeFileSync2 } from "node:fs";
8218
8354
 
8219
8355
  // tools/format/cli.js
8220
8356
  var exports_cli = {};
@@ -8606,7 +8742,7 @@ async function formatResult(formatName, result, options = {}) {
8606
8742
  async function emit(result, { format: format5, pretty, output }) {
8607
8743
  const text = await formatResult(format5, result, { pretty });
8608
8744
  if (output) {
8609
- writeFileSync(output, text);
8745
+ writeFileSync2(output, text);
8610
8746
  } else {
8611
8747
  process.stdout.write(text);
8612
8748
  if (!text.endsWith(`
@@ -8618,7 +8754,7 @@ async function emit(result, { format: format5, pretty, output }) {
8618
8754
 
8619
8755
  // tools/cli/with-module.js
8620
8756
  async function runCommand(cmd, argv, globalOpts) {
8621
- const parsed = parseArgs({
8757
+ const parsed = parseArgs2({
8622
8758
  args: argv,
8623
8759
  options: cmd.parseOptions ?? {},
8624
8760
  allowPositionals: true
@@ -8639,6 +8775,10 @@ async function runCommand(cmd, argv, globalOpts) {
8639
8775
  }
8640
8776
 
8641
8777
  // tools/tutuca.js
8778
+ var NO_MODULE_COMMANDS = {
8779
+ help: exports_help,
8780
+ "install-skill": exports_install_skill
8781
+ };
8642
8782
  function usageError(msg) {
8643
8783
  process.stderr.write(`tutuca: ${msg}
8644
8784
  Run \`tutuca help\` for usage.
@@ -8675,13 +8815,13 @@ function extractGlobals(argv) {
8675
8815
  async function main() {
8676
8816
  const { opts, rest } = extractGlobals(process.argv.slice(2));
8677
8817
  if (rest.length === 0 || opts.help && rest.length === 0) {
8678
- await run([], opts);
8818
+ await run2([], opts);
8679
8819
  return;
8680
8820
  }
8681
8821
  let command;
8682
8822
  let commandArgs;
8683
- if (rest[0] === "help") {
8684
- command = "help";
8823
+ if (NO_MODULE_COMMANDS[rest[0]]) {
8824
+ command = rest[0];
8685
8825
  commandArgs = rest.slice(1);
8686
8826
  } else if (opts.module) {
8687
8827
  command = rest[0];
@@ -8693,12 +8833,13 @@ async function main() {
8693
8833
  command = rest[1];
8694
8834
  commandArgs = rest.slice(2);
8695
8835
  }
8696
- if (opts.help) {
8697
- await run([command], opts);
8836
+ if (NO_MODULE_COMMANDS[command]) {
8837
+ const args = opts.help ? [...commandArgs, "--help"] : commandArgs;
8838
+ await NO_MODULE_COMMANDS[command].run(args, opts);
8698
8839
  return;
8699
8840
  }
8700
- if (command === "help") {
8701
- await run(commandArgs, opts);
8841
+ if (opts.help) {
8842
+ await run2([command], opts);
8702
8843
  return;
8703
8844
  }
8704
8845
  const cmd = COMMANDS[command];