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 +5 -3
- package/dist/tutuca-cli.js +208 -69
- package/dist/tutuca-components.js +2444 -0
- package/dist/tutuca-dev.ext.js +92 -45
- package/dist/tutuca-dev.js +92 -45
- package/dist/tutuca-dev.min.js +2 -2
- package/dist/tutuca-extra.ext.js +56 -28
- package/dist/tutuca-extra.js +56 -28
- package/dist/tutuca-extra.min.js +2 -2
- package/dist/tutuca-storybook.js +136 -10
- package/dist/tutuca.ext.js +56 -28
- package/dist/tutuca.js +56 -28
- package/dist/tutuca.min.js +2 -2
- package/package.json +5 -3
- package/skill/tutuca/advanced.md +14 -5
- package/skill/tutuca/cli.md +10 -68
- package/skill/tutuca/core.md +103 -89
- package/skill/tutuca/margaui.md +25 -13
- package/skill/tutuca/patterns/README.md +1 -0
- package/skill/tutuca/patterns/filter-a-list.md +3 -1
- package/skill/tutuca/patterns/filter-and-paginate.md +116 -0
- package/skill/tutuca/patterns/paginate-a-list.md +3 -1
- package/skill/tutuca/semantics.md +4 -3
- package/skill/tutuca/storybook.md +7 -2
- package/skill/tutuca/testing.md +14 -6
- package/skill/tutuca-source/tutuca.ext.js +56 -28
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
|
-
- ~
|
|
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`
|
|
150
|
-
`
|
|
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
|
|
package/dist/tutuca-cli.js
CHANGED
|
@@ -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
|
-
|
|
9804
|
-
|
|
9805
|
-
|
|
9806
|
-
|
|
9807
|
-
|
|
9808
|
-
|
|
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
|
-
|
|
9817
|
-
|
|
9818
|
-
|
|
9819
|
-
|
|
9820
|
-
|
|
9821
|
-
|
|
9822
|
-
|
|
9823
|
-
|
|
9824
|
-
|
|
9825
|
-
|
|
9826
|
-
|
|
9827
|
-
|
|
9828
|
-
|
|
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
|
-
}
|
|
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
|
-
},
|
|
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,
|
|
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
|
|
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
|
|
16034
|
-
|
|
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
|
-
|
|
16050
|
-
|
|
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 = [
|
|
16055
|
-
const distRoot = distCandidates.find((d) => existsSync4(
|
|
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 =
|
|
16062
|
-
if (existsSync4(
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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(
|
|
16287
|
-
writeFileSync(
|
|
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 {
|
|
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 =
|
|
16956
|
-
|
|
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
|
}
|