tutuca 0.9.83 → 0.9.84
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 +2 -0
- package/dist/tutuca-cli.js +384 -52
- package/dist/tutuca-storybook.js +283 -0
- package/package.json +3 -1
- package/skill/tutuca/core.md +11 -0
package/README.md
CHANGED
|
@@ -68,6 +68,8 @@ Tutuca ships a single-file CLI (`dist/tutuca-cli.js`) for inspecting, linting,
|
|
|
68
68
|
documenting, and rendering components defined in an ES module. The module just
|
|
69
69
|
needs to export `getComponents()` and, for render-time commands, `getExamples()`
|
|
70
70
|
in the storybook shape `{ title, description?, items: [{ title, description?, value, view? }] }` (a single section, or an array of sections).
|
|
71
|
+
Expose **all** of your app's components through `getComponents()` — components
|
|
72
|
+
left out are invisible to `lint`/`render`/`test` and silently lose coverage.
|
|
71
73
|
|
|
72
74
|
### Setup
|
|
73
75
|
|
package/dist/tutuca-cli.js
CHANGED
|
@@ -15629,6 +15629,356 @@ var init_install_skill = __esm(() => {
|
|
|
15629
15629
|
];
|
|
15630
15630
|
});
|
|
15631
15631
|
|
|
15632
|
+
// tools/cli/env.js
|
|
15633
|
+
import { JSDOM, VirtualConsole } from "jsdom";
|
|
15634
|
+
async function createNodeEnv() {
|
|
15635
|
+
const virtualConsole = new VirtualConsole;
|
|
15636
|
+
virtualConsole.forwardTo(console, { jsdomErrors: "none" });
|
|
15637
|
+
virtualConsole.on("jsdomError", (err) => {
|
|
15638
|
+
if (err?.message?.includes("Could not parse CSS stylesheet"))
|
|
15639
|
+
return;
|
|
15640
|
+
console.error(err.message);
|
|
15641
|
+
});
|
|
15642
|
+
const dom = new JSDOM("<!DOCTYPE html><html><head></head><body></body></html>", {
|
|
15643
|
+
virtualConsole
|
|
15644
|
+
});
|
|
15645
|
+
const { document: document2, Text, Comment } = dom.window;
|
|
15646
|
+
globalThis.document = document2;
|
|
15647
|
+
|
|
15648
|
+
class HeadlessParseContext extends ParseContext {
|
|
15649
|
+
constructor() {
|
|
15650
|
+
super(document2, Text, Comment);
|
|
15651
|
+
}
|
|
15652
|
+
}
|
|
15653
|
+
|
|
15654
|
+
class HeadlessLintParseContext extends LintParseContext {
|
|
15655
|
+
constructor() {
|
|
15656
|
+
super(document2, Text, Comment);
|
|
15657
|
+
}
|
|
15658
|
+
}
|
|
15659
|
+
return {
|
|
15660
|
+
document: dom.window.document,
|
|
15661
|
+
ParseContext: HeadlessParseContext,
|
|
15662
|
+
LintParseContext: HeadlessLintParseContext
|
|
15663
|
+
};
|
|
15664
|
+
}
|
|
15665
|
+
var init_env2 = __esm(() => {
|
|
15666
|
+
init_anode();
|
|
15667
|
+
init_lint_check();
|
|
15668
|
+
});
|
|
15669
|
+
|
|
15670
|
+
// tools/cli/commands/storybook.js
|
|
15671
|
+
var exports_storybook = {};
|
|
15672
|
+
__export(exports_storybook, {
|
|
15673
|
+
run: () => run4,
|
|
15674
|
+
describe: () => describe4
|
|
15675
|
+
});
|
|
15676
|
+
import {
|
|
15677
|
+
createReadStream,
|
|
15678
|
+
existsSync as existsSync4,
|
|
15679
|
+
mkdirSync as mkdirSync3,
|
|
15680
|
+
readdirSync as readdirSync2,
|
|
15681
|
+
readFileSync as readFileSync3,
|
|
15682
|
+
statSync,
|
|
15683
|
+
writeFileSync
|
|
15684
|
+
} from "node:fs";
|
|
15685
|
+
import { createServer } from "node:http";
|
|
15686
|
+
import { dirname as dirname4, join, normalize, relative as relative2, resolve as resolve4, sep } from "node:path";
|
|
15687
|
+
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
15688
|
+
import { parseArgs as parseArgs3 } from "node:util";
|
|
15689
|
+
function findDevModules(root, dir, acc) {
|
|
15690
|
+
for (const e of readdirSync2(dir, { withFileTypes: true })) {
|
|
15691
|
+
if (e.name.startsWith(".") || e.name === "node_modules")
|
|
15692
|
+
continue;
|
|
15693
|
+
const full = resolve4(dir, e.name);
|
|
15694
|
+
if (e.isDirectory()) {
|
|
15695
|
+
findDevModules(root, full, acc);
|
|
15696
|
+
} else if (e.isFile() && e.name.endsWith(".dev.js")) {
|
|
15697
|
+
acc.push(`/${relative2(root, full).split(sep).join("/")}`);
|
|
15698
|
+
}
|
|
15699
|
+
}
|
|
15700
|
+
return acc;
|
|
15701
|
+
}
|
|
15702
|
+
function findSelf() {
|
|
15703
|
+
const here = dirname4(fileURLToPath4(import.meta.url));
|
|
15704
|
+
const pkgCandidates = [
|
|
15705
|
+
resolve4(here, "..", "..", "..", "package.json"),
|
|
15706
|
+
resolve4(here, "..", "package.json")
|
|
15707
|
+
];
|
|
15708
|
+
const pkgPath = pkgCandidates.find(existsSync4);
|
|
15709
|
+
const version = pkgPath ? JSON.parse(readFileSync3(pkgPath, "utf8")).version : "latest";
|
|
15710
|
+
const distCandidates = [resolve4(here, "..", "..", "..", "dist"), resolve4(here, ".")];
|
|
15711
|
+
const distRoot = distCandidates.find((d) => existsSync4(resolve4(d, "tutuca-storybook.js"))) ?? null;
|
|
15712
|
+
return { version, distRoot };
|
|
15713
|
+
}
|
|
15714
|
+
function resolveTutucaBase(projectDir, self, forCdn) {
|
|
15715
|
+
if (forCdn)
|
|
15716
|
+
return { base: `https://cdn.jsdelivr.net/npm/tutuca@${self.version}/dist`, serveDist: null };
|
|
15717
|
+
const nm = resolve4(projectDir, "node_modules", "tutuca", "dist");
|
|
15718
|
+
if (existsSync4(resolve4(nm, "tutuca-dev.js"))) {
|
|
15719
|
+
return { base: "/node_modules/tutuca/dist", serveDist: null };
|
|
15720
|
+
}
|
|
15721
|
+
if (self.distRoot)
|
|
15722
|
+
return { base: DIST_PREFIX.replace(/\/$/, ""), serveDist: self.distRoot };
|
|
15723
|
+
return { base: `https://cdn.jsdelivr.net/npm/tutuca@${self.version}/dist`, serveDist: null };
|
|
15724
|
+
}
|
|
15725
|
+
function buildImports(base, { margaui }) {
|
|
15726
|
+
const dev = `${base}/tutuca-dev.js`;
|
|
15727
|
+
const imports = {
|
|
15728
|
+
tutuca: dev,
|
|
15729
|
+
"tutuca/extra": dev,
|
|
15730
|
+
"tutuca/dev": dev,
|
|
15731
|
+
"tutuca/storybook": `${base}/tutuca-storybook.js`
|
|
15732
|
+
};
|
|
15733
|
+
if (margaui)
|
|
15734
|
+
imports.margaui = MARGAUI_CDN;
|
|
15735
|
+
return imports;
|
|
15736
|
+
}
|
|
15737
|
+
function renderIndexHtml(imports, { margaui, bootstrapUrl }) {
|
|
15738
|
+
const theme = margaui ? `
|
|
15739
|
+
<link rel="stylesheet" href="${MARGAUI_THEME}" />` : "";
|
|
15740
|
+
return `<!doctype html>
|
|
15741
|
+
<html lang="en">
|
|
15742
|
+
<head>
|
|
15743
|
+
<meta charset="UTF-8" />
|
|
15744
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
15745
|
+
<title>Storybook</title>${theme}
|
|
15746
|
+
<script type="importmap">
|
|
15747
|
+
${JSON.stringify({ imports }, null, 6)}
|
|
15748
|
+
</script>
|
|
15749
|
+
</head>
|
|
15750
|
+
<body>
|
|
15751
|
+
<div id="app"></div>
|
|
15752
|
+
<script type="module" src="${bootstrapUrl}"></script>
|
|
15753
|
+
</body>
|
|
15754
|
+
</html>
|
|
15755
|
+
`;
|
|
15756
|
+
}
|
|
15757
|
+
function renderBootstrap(devModuleUrls, { margaui, check }) {
|
|
15758
|
+
const lines = ['import { mountStorybook } from "tutuca/storybook";'];
|
|
15759
|
+
if (margaui) {
|
|
15760
|
+
lines.push('import { compileClassesToStyleText } from "tutuca/extra";');
|
|
15761
|
+
lines.push('import { compile } from "margaui";');
|
|
15762
|
+
}
|
|
15763
|
+
if (check)
|
|
15764
|
+
lines.push('import { check } from "tutuca/dev";');
|
|
15765
|
+
devModuleUrls.forEach((url, i) => {
|
|
15766
|
+
lines.push(`import * as m${i} from ${JSON.stringify(url)};`);
|
|
15767
|
+
});
|
|
15768
|
+
const modules = devModuleUrls.map((_, i) => `m${i}`).join(", ");
|
|
15769
|
+
const opts = margaui ? "{ compileCss: (app) => compileClassesToStyleText(app, compile) }" : "{}";
|
|
15770
|
+
lines.push("");
|
|
15771
|
+
lines.push(`const app = await mountStorybook("#app", [${modules}], ${opts});`);
|
|
15772
|
+
if (check)
|
|
15773
|
+
lines.push("check(app);");
|
|
15774
|
+
lines.push("");
|
|
15775
|
+
return lines.join(`
|
|
15776
|
+
`);
|
|
15777
|
+
}
|
|
15778
|
+
async function runDevTests(projectDir, devModuleUrls) {
|
|
15779
|
+
await createNodeEnv();
|
|
15780
|
+
let totalTests = 0;
|
|
15781
|
+
let failedTests = 0;
|
|
15782
|
+
let withTests = 0;
|
|
15783
|
+
const failures = [];
|
|
15784
|
+
const importErrors = [];
|
|
15785
|
+
for (const url of devModuleUrls) {
|
|
15786
|
+
const abs = resolve4(projectDir, url.slice(1));
|
|
15787
|
+
let mod;
|
|
15788
|
+
try {
|
|
15789
|
+
mod = await import(abs);
|
|
15790
|
+
} catch (e) {
|
|
15791
|
+
importErrors.push({ url, message: e.message });
|
|
15792
|
+
continue;
|
|
15793
|
+
}
|
|
15794
|
+
if (typeof mod.getTests !== "function")
|
|
15795
|
+
continue;
|
|
15796
|
+
withTests++;
|
|
15797
|
+
const { normalized } = normalizeModule(mod, { path: abs });
|
|
15798
|
+
const report = await runTests({
|
|
15799
|
+
getTests: mod.getTests,
|
|
15800
|
+
components: normalized.components,
|
|
15801
|
+
path: abs,
|
|
15802
|
+
expect
|
|
15803
|
+
});
|
|
15804
|
+
const m = report.modules[0];
|
|
15805
|
+
totalTests += m.counts.total;
|
|
15806
|
+
failedTests += m.counts.fail;
|
|
15807
|
+
for (const suite of m.suites)
|
|
15808
|
+
collectFailures(suite, failures);
|
|
15809
|
+
}
|
|
15810
|
+
return { totalTests, failedTests, withTests, failures, importErrors };
|
|
15811
|
+
}
|
|
15812
|
+
function collectFailures(node, acc) {
|
|
15813
|
+
if (node.children) {
|
|
15814
|
+
for (const child of node.children)
|
|
15815
|
+
collectFailures(child, acc);
|
|
15816
|
+
} else if (node.status === "fail") {
|
|
15817
|
+
acc.push(node);
|
|
15818
|
+
}
|
|
15819
|
+
}
|
|
15820
|
+
function safeJoin(rootDir, urlPath) {
|
|
15821
|
+
const decoded = decodeURIComponent(urlPath.split("?")[0]);
|
|
15822
|
+
const p = normalize(join(rootDir, decoded));
|
|
15823
|
+
if (p !== rootDir && !p.startsWith(rootDir + sep))
|
|
15824
|
+
return null;
|
|
15825
|
+
return p;
|
|
15826
|
+
}
|
|
15827
|
+
function serveFile(res, filePath) {
|
|
15828
|
+
if (!filePath || !existsSync4(filePath) || statSync(filePath).isDirectory()) {
|
|
15829
|
+
res.writeHead(404);
|
|
15830
|
+
res.end("Not found");
|
|
15831
|
+
return;
|
|
15832
|
+
}
|
|
15833
|
+
const dot = filePath.lastIndexOf(".");
|
|
15834
|
+
const ext = dot >= 0 ? filePath.slice(dot) : "";
|
|
15835
|
+
res.setHeader("Content-Type", MIME[ext] ?? "application/octet-stream");
|
|
15836
|
+
createReadStream(filePath).pipe(res);
|
|
15837
|
+
}
|
|
15838
|
+
async function run4(argv, opts = {}) {
|
|
15839
|
+
const parsed = parseArgs3({
|
|
15840
|
+
args: argv,
|
|
15841
|
+
options: {
|
|
15842
|
+
port: { type: "string" },
|
|
15843
|
+
out: { type: "string" },
|
|
15844
|
+
"no-margaui": { type: "boolean", default: false },
|
|
15845
|
+
"no-check": { type: "boolean", default: false },
|
|
15846
|
+
"no-tests": { type: "boolean", default: false },
|
|
15847
|
+
help: { type: "boolean", short: "h", default: false }
|
|
15848
|
+
},
|
|
15849
|
+
allowPositionals: true
|
|
15850
|
+
});
|
|
15851
|
+
if (parsed.values.help) {
|
|
15852
|
+
process.stdout.write(`tutuca storybook [dir] [--port <n>] [--out <dir>]
|
|
15853
|
+
[--no-margaui] [--no-check] [--no-tests]
|
|
15854
|
+
|
|
15855
|
+
Auto-discovers co-located *.dev.js modules (recursively, skipping
|
|
15856
|
+
node_modules/dotdirs) and serves a live storybook that mounts them via
|
|
15857
|
+
the tutuca/storybook library. Zero setup.
|
|
15858
|
+
|
|
15859
|
+
[dir] project root to scan and serve (default: cwd)
|
|
15860
|
+
--port <n> preferred port (default 4321; falls back to a free port)
|
|
15861
|
+
--out <dir> write a static index.html + bootstrap (CDN import map)
|
|
15862
|
+
instead of serving; host it from the project root
|
|
15863
|
+
--no-margaui skip margaui styling (renders functional but unstyled)
|
|
15864
|
+
--no-check skip the in-browser check(app) dev validation
|
|
15865
|
+
--no-tests skip running the modules' getTests() before serving
|
|
15866
|
+
`);
|
|
15867
|
+
return;
|
|
15868
|
+
}
|
|
15869
|
+
const projectDir = resolve4(parsed.positionals[0] ?? process.cwd());
|
|
15870
|
+
if (!existsSync4(projectDir) || !statSync(projectDir).isDirectory()) {
|
|
15871
|
+
emitError(opts, {
|
|
15872
|
+
code: CODES.USAGE_MISSING_ARGUMENT,
|
|
15873
|
+
message: `not a directory: ${projectDir}`,
|
|
15874
|
+
hint: "Pass a project directory to scan, or omit it to use the current directory."
|
|
15875
|
+
});
|
|
15876
|
+
}
|
|
15877
|
+
const devModuleUrls = findDevModules(projectDir, projectDir, []);
|
|
15878
|
+
if (devModuleUrls.length === 0) {
|
|
15879
|
+
emitError(opts, {
|
|
15880
|
+
code: CODES.USAGE_MISSING_ARGUMENT,
|
|
15881
|
+
message: `no *.dev.js modules found under ${projectDir}`,
|
|
15882
|
+
hint: "Create a co-located <name>.dev.js exporting getComponents() and getExamples()."
|
|
15883
|
+
});
|
|
15884
|
+
}
|
|
15885
|
+
const margaui = !parsed.values["no-margaui"];
|
|
15886
|
+
const check = !parsed.values["no-check"];
|
|
15887
|
+
const self = findSelf();
|
|
15888
|
+
if (parsed.values.out) {
|
|
15889
|
+
const outDir = resolve4(parsed.values.out);
|
|
15890
|
+
mkdirSync3(outDir, { recursive: true });
|
|
15891
|
+
const { base: base2 } = resolveTutucaBase(projectDir, self, true);
|
|
15892
|
+
const imports2 = buildImports(base2, { margaui });
|
|
15893
|
+
const bootstrapName = "tutuca-storybook.bootstrap.js";
|
|
15894
|
+
writeFileSync(resolve4(outDir, "index.html"), renderIndexHtml(imports2, { margaui, bootstrapUrl: `./${bootstrapName}` }));
|
|
15895
|
+
writeFileSync(resolve4(outDir, bootstrapName), renderBootstrap(devModuleUrls, { margaui, check }));
|
|
15896
|
+
process.stdout.write(`wrote static storybook → ${relative2(process.cwd(), outDir) || "."}/
|
|
15897
|
+
index.html + ${bootstrapName} (${devModuleUrls.length} dev modules, CDN import map)
|
|
15898
|
+
Host it from the project root so /*.dev.js paths resolve.
|
|
15899
|
+
`);
|
|
15900
|
+
return;
|
|
15901
|
+
}
|
|
15902
|
+
if (!parsed.values["no-tests"]) {
|
|
15903
|
+
const r = await runDevTests(projectDir, devModuleUrls);
|
|
15904
|
+
for (const ie of r.importErrors) {
|
|
15905
|
+
process.stdout.write(` ! skipped tests for ${ie.url}: ${ie.message}
|
|
15906
|
+
`);
|
|
15907
|
+
}
|
|
15908
|
+
if (r.withTests === 0) {
|
|
15909
|
+
process.stdout.write(`tests: no getTests() in any dev module
|
|
15910
|
+
`);
|
|
15911
|
+
} else {
|
|
15912
|
+
process.stdout.write(`tests: ${r.totalTests - r.failedTests}/${r.totalTests} passed across ${r.withTests} module(s)
|
|
15913
|
+
`);
|
|
15914
|
+
for (const f of r.failures) {
|
|
15915
|
+
process.stdout.write(` ✗ ${f.fullPath}: ${f.error?.message ?? "failed"}
|
|
15916
|
+
`);
|
|
15917
|
+
}
|
|
15918
|
+
}
|
|
15919
|
+
}
|
|
15920
|
+
const { base, serveDist } = resolveTutucaBase(projectDir, self, false);
|
|
15921
|
+
const imports = buildImports(base, { margaui });
|
|
15922
|
+
const indexHtml = renderIndexHtml(imports, { margaui, bootstrapUrl: BOOTSTRAP_URL });
|
|
15923
|
+
const bootstrapJs = renderBootstrap(devModuleUrls, { margaui, check });
|
|
15924
|
+
const server = createServer((req, res) => {
|
|
15925
|
+
const path = req.url.split("?")[0];
|
|
15926
|
+
if (path === "/" || path === "/index.html") {
|
|
15927
|
+
res.setHeader("Content-Type", MIME[".html"]);
|
|
15928
|
+
res.end(indexHtml);
|
|
15929
|
+
return;
|
|
15930
|
+
}
|
|
15931
|
+
if (path === BOOTSTRAP_URL) {
|
|
15932
|
+
res.setHeader("Content-Type", MIME[".js"]);
|
|
15933
|
+
res.end(bootstrapJs);
|
|
15934
|
+
return;
|
|
15935
|
+
}
|
|
15936
|
+
if (serveDist && path.startsWith(DIST_PREFIX)) {
|
|
15937
|
+
serveFile(res, safeJoin(serveDist, `/${path.slice(DIST_PREFIX.length)}`));
|
|
15938
|
+
return;
|
|
15939
|
+
}
|
|
15940
|
+
serveFile(res, safeJoin(projectDir, path));
|
|
15941
|
+
});
|
|
15942
|
+
const preferred = Number.parseInt(parsed.values.port ?? "4321", 10);
|
|
15943
|
+
server.on("error", (e) => {
|
|
15944
|
+
if (e.code === "EADDRINUSE") {
|
|
15945
|
+
server.listen(0);
|
|
15946
|
+
} else {
|
|
15947
|
+
throw e;
|
|
15948
|
+
}
|
|
15949
|
+
});
|
|
15950
|
+
server.on("listening", () => {
|
|
15951
|
+
const actual = server.address().port;
|
|
15952
|
+
const where = base.startsWith("http") ? "CDN" : base.startsWith("/node_modules") ? "node_modules" : "local dist";
|
|
15953
|
+
process.stdout.write(`tutuca storybook: http://localhost:${actual}/ (${devModuleUrls.length} dev modules, tutuca from ${where})
|
|
15954
|
+
`);
|
|
15955
|
+
});
|
|
15956
|
+
server.listen(preferred);
|
|
15957
|
+
}
|
|
15958
|
+
var describe4 = "Serve a storybook for the project's co-located *.dev.js modules (auto-discovered).", BOOTSTRAP_URL = "/__tutuca_storybook__.js", DIST_PREFIX = "/__tutuca__/", MIME, MARGAUI_CDN = "https://cdn.jsdelivr.net/npm/margaui/+esm", MARGAUI_THEME = "https://marianoguerra.github.io/margaui/themes/theme.css";
|
|
15959
|
+
var init_storybook = __esm(() => {
|
|
15960
|
+
init_chai2();
|
|
15961
|
+
init_test();
|
|
15962
|
+
init_env2();
|
|
15963
|
+
init_errors();
|
|
15964
|
+
MIME = {
|
|
15965
|
+
".js": "text/javascript",
|
|
15966
|
+
".mjs": "text/javascript",
|
|
15967
|
+
".css": "text/css",
|
|
15968
|
+
".html": "text/html; charset=utf-8",
|
|
15969
|
+
".json": "application/json",
|
|
15970
|
+
".wasm": "application/wasm",
|
|
15971
|
+
".svg": "image/svg+xml",
|
|
15972
|
+
".png": "image/png",
|
|
15973
|
+
".jpg": "image/jpeg",
|
|
15974
|
+
".jpeg": "image/jpeg",
|
|
15975
|
+
".webp": "image/webp",
|
|
15976
|
+
".gif": "image/gif",
|
|
15977
|
+
".ico": "image/x-icon",
|
|
15978
|
+
".map": "application/json"
|
|
15979
|
+
};
|
|
15980
|
+
});
|
|
15981
|
+
|
|
15632
15982
|
// tools/tutuca.js
|
|
15633
15983
|
init__registry();
|
|
15634
15984
|
|
|
@@ -15854,10 +16204,10 @@ init_feedback();
|
|
|
15854
16204
|
init_chai_jest();
|
|
15855
16205
|
var exports_help = {};
|
|
15856
16206
|
__export(exports_help, {
|
|
15857
|
-
run: () =>
|
|
15858
|
-
describe: () =>
|
|
16207
|
+
run: () => run5,
|
|
16208
|
+
describe: () => describe5
|
|
15859
16209
|
});
|
|
15860
|
-
var
|
|
16210
|
+
var describe5 = "Show usage. `help <command>` for per-command detail.";
|
|
15861
16211
|
var OVERVIEW = `tutuca — CLI for inspecting, documenting, linting and rendering tutuca
|
|
15862
16212
|
components defined in an ES module.
|
|
15863
16213
|
|
|
@@ -15874,8 +16224,8 @@ INVOCATION SHAPE
|
|
|
15874
16224
|
components; pass it to operate on exactly one (e.g. \`show Button\`).
|
|
15875
16225
|
- Per-command flags follow the module path; global flags can appear
|
|
15876
16226
|
anywhere. Unknown flags are rejected by the subcommand's parser.
|
|
15877
|
-
- \`help\`, \`feedback\`, \`install-skill\`, \`agent-context\` do NOT
|
|
15878
|
-
module path.
|
|
16227
|
+
- \`help\`, \`feedback\`, \`install-skill\`, \`storybook\`, \`agent-context\` do NOT
|
|
16228
|
+
take a module path.
|
|
15879
16229
|
|
|
15880
16230
|
MODULE CONVENTION
|
|
15881
16231
|
A module passed to tutuca must export one or more of:
|
|
@@ -15962,6 +16312,20 @@ COMMANDS (no module required)
|
|
|
15962
16312
|
or suggestions about the CLI, skills, docs, or the library.
|
|
15963
16313
|
Message comes from the positional arg or piped stdin.
|
|
15964
16314
|
|
|
16315
|
+
storybook [dir] [--port <n>] [--out <dir>] [--no-margaui] [--no-check] [--no-tests]
|
|
16316
|
+
Serve a live storybook for the project. Recursively auto-discovers
|
|
16317
|
+
co-located *.dev.js modules (dev-only modules holding stories + tests
|
|
16318
|
+
+ helpers, never shipped) and mounts them via the tutuca/storybook
|
|
16319
|
+
library — zero setup. By default also runs their getTests() in the
|
|
16320
|
+
terminal and wires margaui styling + an in-browser check(app).
|
|
16321
|
+
[dir] project root to scan/serve (default: cwd)
|
|
16322
|
+
--port <n> preferred port (default 4321; else a free port)
|
|
16323
|
+
--out <dir> write a static index.html + bootstrap (CDN import map)
|
|
16324
|
+
instead of serving; host from the project root
|
|
16325
|
+
--no-margaui render unstyled (skip margaui)
|
|
16326
|
+
--no-check skip the in-browser check(app)
|
|
16327
|
+
--no-tests skip the pre-serve getTests() run
|
|
16328
|
+
|
|
15965
16329
|
agent-context
|
|
15966
16330
|
Print a machine-readable schema of every command, flag, exit code,
|
|
15967
16331
|
and error code as JSON on stdout. Use this once to teach an agent the
|
|
@@ -16027,21 +16391,22 @@ EXAMPLES
|
|
|
16027
16391
|
tutuca lint ./src/components.js
|
|
16028
16392
|
tutuca render ./src/components.js --title "Disabled state"
|
|
16029
16393
|
`;
|
|
16030
|
-
async function
|
|
16394
|
+
async function run5(argv, opts = {}) {
|
|
16031
16395
|
const target = argv?.[0];
|
|
16032
16396
|
if (!target) {
|
|
16033
16397
|
process.stdout.write(OVERVIEW);
|
|
16034
16398
|
return;
|
|
16035
16399
|
}
|
|
16036
16400
|
if (target === "help") {
|
|
16037
|
-
process.stdout.write(`help: ${
|
|
16401
|
+
process.stdout.write(`help: ${describe5}
|
|
16038
16402
|
`);
|
|
16039
16403
|
return;
|
|
16040
16404
|
}
|
|
16041
16405
|
const { COMMANDS: COMMANDS2 } = await Promise.resolve().then(() => (init__registry(), exports__registry));
|
|
16042
16406
|
const noModule = {
|
|
16043
16407
|
feedback: await Promise.resolve().then(() => (init_feedback(), exports_feedback)),
|
|
16044
|
-
"install-skill": await Promise.resolve().then(() => (init_install_skill(), exports_install_skill))
|
|
16408
|
+
"install-skill": await Promise.resolve().then(() => (init_install_skill(), exports_install_skill)),
|
|
16409
|
+
storybook: await Promise.resolve().then(() => (init_storybook(), exports_storybook))
|
|
16045
16410
|
};
|
|
16046
16411
|
const cmd = COMMANDS2[target] ?? noModule[target];
|
|
16047
16412
|
if (!cmd) {
|
|
@@ -16084,58 +16449,24 @@ ${group}
|
|
|
16084
16449
|
|
|
16085
16450
|
// tools/tutuca.js
|
|
16086
16451
|
init_install_skill();
|
|
16452
|
+
init_storybook();
|
|
16087
16453
|
init_errors();
|
|
16088
16454
|
|
|
16089
16455
|
// tools/cli/with-module.js
|
|
16090
|
-
|
|
16091
|
-
|
|
16092
|
-
// tools/cli/env.js
|
|
16093
|
-
init_anode();
|
|
16094
|
-
init_lint_check();
|
|
16095
|
-
import { JSDOM, VirtualConsole } from "jsdom";
|
|
16096
|
-
async function createNodeEnv() {
|
|
16097
|
-
const virtualConsole = new VirtualConsole;
|
|
16098
|
-
virtualConsole.forwardTo(console, { jsdomErrors: "none" });
|
|
16099
|
-
virtualConsole.on("jsdomError", (err) => {
|
|
16100
|
-
if (err?.message?.includes("Could not parse CSS stylesheet"))
|
|
16101
|
-
return;
|
|
16102
|
-
console.error(err.message);
|
|
16103
|
-
});
|
|
16104
|
-
const dom = new JSDOM("<!DOCTYPE html><html><head></head><body></body></html>", {
|
|
16105
|
-
virtualConsole
|
|
16106
|
-
});
|
|
16107
|
-
const { document: document2, Text, Comment } = dom.window;
|
|
16108
|
-
globalThis.document = document2;
|
|
16109
|
-
|
|
16110
|
-
class HeadlessParseContext extends ParseContext {
|
|
16111
|
-
constructor() {
|
|
16112
|
-
super(document2, Text, Comment);
|
|
16113
|
-
}
|
|
16114
|
-
}
|
|
16115
|
-
|
|
16116
|
-
class HeadlessLintParseContext extends LintParseContext {
|
|
16117
|
-
constructor() {
|
|
16118
|
-
super(document2, Text, Comment);
|
|
16119
|
-
}
|
|
16120
|
-
}
|
|
16121
|
-
return {
|
|
16122
|
-
document: dom.window.document,
|
|
16123
|
-
ParseContext: HeadlessParseContext,
|
|
16124
|
-
LintParseContext: HeadlessLintParseContext
|
|
16125
|
-
};
|
|
16126
|
-
}
|
|
16456
|
+
init_env2();
|
|
16457
|
+
import { parseArgs as parseArgs4 } from "node:util";
|
|
16127
16458
|
|
|
16128
16459
|
// tools/cli/load.js
|
|
16129
|
-
import { resolve as
|
|
16460
|
+
import { resolve as resolve5 } from "node:path";
|
|
16130
16461
|
async function loadAndNormalize(modulePath) {
|
|
16131
|
-
const abs =
|
|
16462
|
+
const abs = resolve5(modulePath);
|
|
16132
16463
|
const mod = await import(abs);
|
|
16133
16464
|
const { normalized } = normalizeModule(mod, { path: abs });
|
|
16134
16465
|
return normalized;
|
|
16135
16466
|
}
|
|
16136
16467
|
|
|
16137
16468
|
// tools/cli/output.js
|
|
16138
|
-
import { writeFileSync } from "node:fs";
|
|
16469
|
+
import { writeFileSync as writeFileSync2 } from "node:fs";
|
|
16139
16470
|
|
|
16140
16471
|
// tools/format/cli.js
|
|
16141
16472
|
var exports_cli = {};
|
|
@@ -16621,7 +16952,7 @@ async function formatResult(formatName, result, options = {}) {
|
|
|
16621
16952
|
async function emit(result, { format: format5, pretty, output }) {
|
|
16622
16953
|
const text = await formatResult(format5, result, { pretty });
|
|
16623
16954
|
if (output) {
|
|
16624
|
-
|
|
16955
|
+
writeFileSync2(output, text);
|
|
16625
16956
|
} else {
|
|
16626
16957
|
process.stdout.write(text);
|
|
16627
16958
|
if (!text.endsWith(`
|
|
@@ -16633,7 +16964,7 @@ async function emit(result, { format: format5, pretty, output }) {
|
|
|
16633
16964
|
|
|
16634
16965
|
// tools/cli/with-module.js
|
|
16635
16966
|
async function runCommand(cmd, argv, globalOpts) {
|
|
16636
|
-
const parsed =
|
|
16967
|
+
const parsed = parseArgs4({
|
|
16637
16968
|
args: argv,
|
|
16638
16969
|
options: cmd.parseOptions ?? {},
|
|
16639
16970
|
allowPositionals: true
|
|
@@ -16658,6 +16989,7 @@ var NO_MODULE_COMMANDS = {
|
|
|
16658
16989
|
help: exports_help,
|
|
16659
16990
|
feedback: exports_feedback,
|
|
16660
16991
|
"install-skill": exports_install_skill,
|
|
16992
|
+
storybook: exports_storybook,
|
|
16661
16993
|
"agent-context": exports_agent_context
|
|
16662
16994
|
};
|
|
16663
16995
|
var VALID_FORMATS = ["cli", "md", "json", "html"];
|
|
@@ -16711,7 +17043,7 @@ function dispatchKnownCommands() {
|
|
|
16711
17043
|
async function main() {
|
|
16712
17044
|
const { opts, rest } = extractGlobals(process.argv.slice(2));
|
|
16713
17045
|
if (rest.length === 0) {
|
|
16714
|
-
await
|
|
17046
|
+
await run5([], opts);
|
|
16715
17047
|
return;
|
|
16716
17048
|
}
|
|
16717
17049
|
const command = rest[0];
|
|
@@ -16722,7 +17054,7 @@ async function main() {
|
|
|
16722
17054
|
return;
|
|
16723
17055
|
}
|
|
16724
17056
|
if (opts.help) {
|
|
16725
|
-
await
|
|
17057
|
+
await run5([command], opts);
|
|
16726
17058
|
return;
|
|
16727
17059
|
}
|
|
16728
17060
|
const cmd = COMMANDS[command];
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
// src/storybook.js
|
|
2
|
+
import { component, html, injectCss, tutuca } from "tutuca";
|
|
3
|
+
var Storybook = component({
|
|
4
|
+
name: "Storybook",
|
|
5
|
+
fields: {
|
|
6
|
+
selectedSectionIndex: 0,
|
|
7
|
+
sections: [],
|
|
8
|
+
filter: "",
|
|
9
|
+
sectionId: null,
|
|
10
|
+
exampleId: null,
|
|
11
|
+
focusExample: null
|
|
12
|
+
},
|
|
13
|
+
methods: {
|
|
14
|
+
selectSectionAtIndex(index) {
|
|
15
|
+
if (this.sections.size === 0)
|
|
16
|
+
return this;
|
|
17
|
+
const safeIndex = index >= 0 && index < this.sections.size ? index : 0;
|
|
18
|
+
const sections = this.sections.map((s, i) => s.setSelected(i === safeIndex));
|
|
19
|
+
return this.setSelectedSectionIndex(safeIndex).setSections(sections);
|
|
20
|
+
},
|
|
21
|
+
selectSectionWithId(id) {
|
|
22
|
+
if (!id)
|
|
23
|
+
return this.selectSectionAtIndex(this.selectedSectionIndex);
|
|
24
|
+
const index = this.sections.findIndex((s) => s.id === id);
|
|
25
|
+
return this.selectSectionAtIndex(index);
|
|
26
|
+
},
|
|
27
|
+
focusExampleByIds(sectionId, exampleId) {
|
|
28
|
+
if (!sectionId || !exampleId) {
|
|
29
|
+
return this;
|
|
30
|
+
}
|
|
31
|
+
const section = this.sections.find((s) => s.id === sectionId);
|
|
32
|
+
const example = section?.items.find((e) => e.id === exampleId);
|
|
33
|
+
if (!example) {
|
|
34
|
+
return this;
|
|
35
|
+
}
|
|
36
|
+
return this.setSectionId(sectionId).setExampleId(exampleId).setFocusExample(example.value);
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
input: {
|
|
40
|
+
onApplyFilter(value, ctx) {
|
|
41
|
+
ctx.request("persistState", [{ key: "sectionFilter", value }]);
|
|
42
|
+
return this.setFilter(value);
|
|
43
|
+
},
|
|
44
|
+
onClearFilter(ctx) {
|
|
45
|
+
ctx.request("persistState", [{ key: "sectionFilter", value: "" }]);
|
|
46
|
+
return this.resetFilter();
|
|
47
|
+
},
|
|
48
|
+
onFocusClose(ctx) {
|
|
49
|
+
ctx.request("persistState", [{ key: "sectionId", value: "" }]);
|
|
50
|
+
ctx.request("persistState", [{ key: "exampleId", value: "" }]);
|
|
51
|
+
return this.setSectionId(null).setExampleId(null).setFocusExample(null);
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
alter: {
|
|
55
|
+
filterSection(_key, section) {
|
|
56
|
+
return this.filter === "" || fuzzyMatch(this.filter, `${section.title} ${section.description}`);
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
bubble: {
|
|
60
|
+
sectionSelected(section, ctx) {
|
|
61
|
+
ctx.stopPropagation();
|
|
62
|
+
ctx.request("persistState", [{ key: "section", value: section.id }]);
|
|
63
|
+
return this.selectSectionAtIndex(this.sections.indexOf(section));
|
|
64
|
+
},
|
|
65
|
+
exampleFocusRequested(example, ctx) {
|
|
66
|
+
ctx.stopPropagation();
|
|
67
|
+
const section = this.sections.get(this.selectedSectionIndex);
|
|
68
|
+
const sectionId = section?.id ?? null;
|
|
69
|
+
ctx.request("persistState", [{ key: "sectionId", value: sectionId }]);
|
|
70
|
+
ctx.request("persistState", [{ key: "exampleId", value: example.id }]);
|
|
71
|
+
return this.setSectionId(sectionId).setExampleId(example.id).setFocusExample(example.value);
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
view: html`<div>
|
|
75
|
+
<div class="flex flex-col gap-3 p-3 h-screen" @show="truthy? .focusExample">
|
|
76
|
+
<div class="flex justify-end">
|
|
77
|
+
<button class="btn btn-ghost btn-sm" @on.click="onFocusClose">
|
|
78
|
+
close
|
|
79
|
+
</button>
|
|
80
|
+
</div>
|
|
81
|
+
<div class="flex-1 overflow-y-auto">
|
|
82
|
+
<x render=".focusExample"></x>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
<div class="flex gap-3 p-3 h-screen" @hide="truthy? .focusExample">
|
|
86
|
+
<div
|
|
87
|
+
class="w-1/4 flex flex-col gap-3 bg-base-100 shadow-md h-full overflow-hidden"
|
|
88
|
+
>
|
|
89
|
+
<input
|
|
90
|
+
class="input w-full outline-0 focus:bg-base-200"
|
|
91
|
+
type="search"
|
|
92
|
+
placeholder="Filter sections"
|
|
93
|
+
:value=".filter"
|
|
94
|
+
@on.input="onApplyFilter value"
|
|
95
|
+
@on.keydown.cancel="onClearFilter"
|
|
96
|
+
/>
|
|
97
|
+
<div class="list h-full flex-1 overflow-y-auto">
|
|
98
|
+
<x render-each=".sections" as="listEntry" when="filterSection"></x>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
<div class="w-full h-full overflow-y-auto">
|
|
102
|
+
<x render=".sections[.selectedSectionIndex]"></x>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
</div>`
|
|
106
|
+
});
|
|
107
|
+
var Section = component({
|
|
108
|
+
name: "Section",
|
|
109
|
+
fields: {
|
|
110
|
+
id: "?",
|
|
111
|
+
title: "No Title Section",
|
|
112
|
+
description: "",
|
|
113
|
+
items: [],
|
|
114
|
+
filter: "",
|
|
115
|
+
selected: false
|
|
116
|
+
},
|
|
117
|
+
statics: {
|
|
118
|
+
fromData({ id, title = "???", description = "", items = [] }) {
|
|
119
|
+
id ??= slugify(title);
|
|
120
|
+
return this.make({
|
|
121
|
+
id,
|
|
122
|
+
title,
|
|
123
|
+
description,
|
|
124
|
+
items: items.map((v) => Example.Class.fromData(v))
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
alter: {
|
|
129
|
+
filterItem(_key, item) {
|
|
130
|
+
return this.filter === "" || fuzzyMatch(this.filter, `${item.title} ${item.description}`);
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
input: {
|
|
134
|
+
onApplyFilter(value, ctx) {
|
|
135
|
+
ctx.request("persistState", [{ key: "exampleFilter", value }]);
|
|
136
|
+
return this.setFilter(value);
|
|
137
|
+
},
|
|
138
|
+
onClearFilter(ctx) {
|
|
139
|
+
ctx.request("persistState", [{ key: "exampleFilter", value: "" }]);
|
|
140
|
+
return this.resetFilter();
|
|
141
|
+
},
|
|
142
|
+
onListItemClick(ctx) {
|
|
143
|
+
ctx.bubble("sectionSelected", [this]);
|
|
144
|
+
return this;
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
view: html`<section class="flex flex-col gap-3">
|
|
148
|
+
<div class="sticky top-0 z-10 bg-base-100 pt-1 pb-2 shadow-sm">
|
|
149
|
+
<h2 class="text-lg font-bold" @text=".title"></h2>
|
|
150
|
+
<p class="text-md italic opacity-60" @text=".description"></p>
|
|
151
|
+
</div>
|
|
152
|
+
<input
|
|
153
|
+
class="input w-full outline-0 focus:bg-base-200"
|
|
154
|
+
type="search"
|
|
155
|
+
placeholder="Filter examples"
|
|
156
|
+
:value=".filter"
|
|
157
|
+
@on.input="onApplyFilter value"
|
|
158
|
+
@on.keydown.cancel="onClearFilter"
|
|
159
|
+
/>
|
|
160
|
+
<div class="flex flex-col gap-3">
|
|
161
|
+
<x render-each=".items" when="filterItem"></x>
|
|
162
|
+
</div>
|
|
163
|
+
</section>`,
|
|
164
|
+
views: {
|
|
165
|
+
listEntry: html`<div
|
|
166
|
+
@if.class=".selected"
|
|
167
|
+
@then="'list-row cursor-pointer text-blue-400 hover:text-blue-500 font-semibold'"
|
|
168
|
+
@else="'list-row cursor-pointer hover:bg-base-200'"
|
|
169
|
+
:title=".description"
|
|
170
|
+
@on.click="onListItemClick"
|
|
171
|
+
>
|
|
172
|
+
<div @text=".title" class="list-col-grow"></div>
|
|
173
|
+
<p
|
|
174
|
+
class="text-xs opacity-60 list-col-wrap truncate"
|
|
175
|
+
@text=".description"
|
|
176
|
+
></p>
|
|
177
|
+
</div> `
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
var Example = component({
|
|
181
|
+
name: "Example",
|
|
182
|
+
fields: { id: "?", title: "?", description: "", value: null, view: "main" },
|
|
183
|
+
statics: {
|
|
184
|
+
fromData({ id, title = "No Title Example", description = "", value = null, view = "main" }) {
|
|
185
|
+
id ??= slugify(title);
|
|
186
|
+
return this.make({
|
|
187
|
+
id,
|
|
188
|
+
title,
|
|
189
|
+
description,
|
|
190
|
+
value,
|
|
191
|
+
view
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
input: {
|
|
196
|
+
onLogSelected() {
|
|
197
|
+
console.log(this.value);
|
|
198
|
+
return this;
|
|
199
|
+
},
|
|
200
|
+
onFocusSelected(ctx) {
|
|
201
|
+
ctx.bubble("exampleFocusRequested", [this]);
|
|
202
|
+
return this;
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
view: html`<div class="card card-border bg-base-100 shadow-md">
|
|
206
|
+
<div class="card-body">
|
|
207
|
+
<h2 class="card-title flex justify-between">
|
|
208
|
+
<a :href="$'#example-{.id}'" :id="$'example-{.id}'" @text=".title"></a>
|
|
209
|
+
<div class="flex gap-2">
|
|
210
|
+
<button class="btn btn-ghost btn-sm" @on.click="onFocusSelected">
|
|
211
|
+
focus
|
|
212
|
+
</button>
|
|
213
|
+
<button class="btn btn-ghost btn-sm" @on.click="onLogSelected">
|
|
214
|
+
log
|
|
215
|
+
</button>
|
|
216
|
+
</div>
|
|
217
|
+
</h2>
|
|
218
|
+
<p class="text-md italic opacity-60" @text=".description"></p>
|
|
219
|
+
<div class="bg-base-100 p-3" @push-view=".view">
|
|
220
|
+
<x render=".value"></x>
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
</div>`
|
|
224
|
+
});
|
|
225
|
+
function fuzzyMatch(query, target) {
|
|
226
|
+
const q = query.toLowerCase(), t = target.toLowerCase();
|
|
227
|
+
let qi = 0;
|
|
228
|
+
for (let ti = 0;ti < t.length && qi < q.length; ti++) {
|
|
229
|
+
if (t[ti] === q[qi])
|
|
230
|
+
qi++;
|
|
231
|
+
}
|
|
232
|
+
return qi === q.length;
|
|
233
|
+
}
|
|
234
|
+
function slugify(str) {
|
|
235
|
+
return String(str).normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
236
|
+
}
|
|
237
|
+
function buildStorybook(modules) {
|
|
238
|
+
const sections = modules.map((m) => Section.Class.fromData(m.getExamples())).sort((a, b) => a.title.localeCompare(b.title));
|
|
239
|
+
const components = new Set([Storybook, Section, Example]);
|
|
240
|
+
const macros = {};
|
|
241
|
+
const requestHandlers = {};
|
|
242
|
+
for (const m of modules) {
|
|
243
|
+
for (const c of m.getComponents?.() ?? []) {
|
|
244
|
+
components.add(c);
|
|
245
|
+
}
|
|
246
|
+
if (m.getMacros)
|
|
247
|
+
Object.assign(macros, m.getMacros());
|
|
248
|
+
if (m.getRequestHandlers)
|
|
249
|
+
Object.assign(requestHandlers, m.getRequestHandlers());
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
root: Storybook.make({ sections }),
|
|
253
|
+
components: [...components],
|
|
254
|
+
macros,
|
|
255
|
+
requestHandlers
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
async function mountStorybook(selector, modules, { compileCss, root } = {}) {
|
|
259
|
+
const app = tutuca(selector);
|
|
260
|
+
const built = buildStorybook(modules);
|
|
261
|
+
app.state.set(root ?? built.root);
|
|
262
|
+
const scope = app.registerComponents(built.components);
|
|
263
|
+
scope.registerMacros(built.macros);
|
|
264
|
+
scope.registerRequestHandlers(built.requestHandlers);
|
|
265
|
+
if (compileCss) {
|
|
266
|
+
injectCss("tutuca-storybook", await compileCss(app));
|
|
267
|
+
}
|
|
268
|
+
app.start();
|
|
269
|
+
return app;
|
|
270
|
+
}
|
|
271
|
+
function getComponents() {
|
|
272
|
+
return [Storybook, Section, Example];
|
|
273
|
+
}
|
|
274
|
+
export {
|
|
275
|
+
slugify,
|
|
276
|
+
mountStorybook,
|
|
277
|
+
getComponents,
|
|
278
|
+
fuzzyMatch,
|
|
279
|
+
buildStorybook,
|
|
280
|
+
Storybook,
|
|
281
|
+
Section,
|
|
282
|
+
Example
|
|
283
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tutuca",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.84",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Zero-dependency SPA framework with immutable state and virtual DOM",
|
|
6
6
|
"main": "./dist/tutuca.js",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"./ext": "./dist/tutuca.ext.js",
|
|
14
14
|
"./extra-ext": "./dist/tutuca-extra.ext.js",
|
|
15
15
|
"./dev-ext": "./dist/tutuca-dev.ext.js",
|
|
16
|
+
"./storybook": "./dist/tutuca-storybook.js",
|
|
16
17
|
"./immutable": "./dist/immutable.js",
|
|
17
18
|
"./chai": "./dist/chai.js",
|
|
18
19
|
"./package.json": "./package.json"
|
|
@@ -54,6 +55,7 @@
|
|
|
54
55
|
"dist/tutuca.ext.js",
|
|
55
56
|
"dist/tutuca-extra.ext.js",
|
|
56
57
|
"dist/tutuca-dev.ext.js",
|
|
58
|
+
"dist/tutuca-storybook.js",
|
|
57
59
|
"dist/immutable.js",
|
|
58
60
|
"dist/chai.js",
|
|
59
61
|
"skill"
|
package/skill/tutuca/core.md
CHANGED
|
@@ -977,6 +977,17 @@ export function getExamples() {
|
|
|
977
977
|
export function getTests({ describe, test, expect }) { /*...*/ } // optional — see cli.md
|
|
978
978
|
```
|
|
979
979
|
|
|
980
|
+
Best practice: have `getComponents()` return **every** component the module
|
|
981
|
+
defines — child and helper components included — and give each one at least
|
|
982
|
+
one item in `getExamples()`. A component left out of `getComponents()` is
|
|
983
|
+
invisible to `tutuca lint`/`render`/`test`, so it silently loses linting and
|
|
984
|
+
render coverage. If your components already live behind a differently named
|
|
985
|
+
export, alias it instead of teaching tools a new name:
|
|
986
|
+
|
|
987
|
+
```js
|
|
988
|
+
export { allMyComponents as getComponents } from "./app.js";
|
|
989
|
+
```
|
|
990
|
+
|
|
980
991
|
## See also
|
|
981
992
|
|
|
982
993
|
- [component-design.md](./component-design.md) — design judgment for shaping a
|