baldart 4.75.0 → 4.76.1

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.
@@ -0,0 +1,59 @@
1
+ const StorybookAdapter = require('./storybook');
2
+ const ClaudeDesignSeedAdapter = require('./claude-design-seed');
3
+
4
+ /**
5
+ * Render-harness adapter registry (Stage C, since v4.76.0).
6
+ *
7
+ * Mirrors the lsp/toolchain/routine adapter-registry pattern (REGISTRY +
8
+ * getAdapter + detectAll). An adapter turns a `render-manifest.json` into a real
9
+ * render surface (mounted primitives) per ONE strategy. The harness exists to
10
+ * render registry primitives in isolation — today BALDART documents components but
11
+ * never renders them isolated.
12
+ *
13
+ * Adding a render strategy:
14
+ * 1. Create `src/utils/render-adapters/<name>.js` with the shape of
15
+ * StorybookAdapter (static get name, get label, static detect(cwd),
16
+ * generate({manifest,outDir})).
17
+ * 2. Add it to REGISTRY below, in PRIORITY order (detectAll returns the first
18
+ * that fires — Storybook is canonical when present).
19
+ *
20
+ * REFUTED (do NOT add): a per-stack framework-native preview-route generator +
21
+ * an auto-provider-shim. Those are code-generators with unbounded surface that
22
+ * mount providers out-of-tree → wrong-but-green renders (false fidelity). Use
23
+ * Storybook where present; the seed-gallery only for the one-time seed case.
24
+ */
25
+ const REGISTRY = {
26
+ storybook: StorybookAdapter,
27
+ 'claude-design-seed': ClaudeDesignSeedAdapter,
28
+ };
29
+
30
+ // Priority order for detection: Storybook is the canonical render path when
31
+ // present; the seed gallery is a conditional one-time fallback.
32
+ const PRIORITY = ['storybook', 'claude-design-seed'];
33
+
34
+ function listAdapters() { return Object.keys(REGISTRY); }
35
+
36
+ function getAdapter(name, cwd) {
37
+ const Cls = REGISTRY[name];
38
+ if (!Cls) throw new Error(`Unknown render adapter: ${name}. Available: ${listAdapters().join(', ')}`);
39
+ return new Cls(cwd);
40
+ }
41
+
42
+ /**
43
+ * Return the FIRST adapter (by priority) whose static detect() fires, or null
44
+ * when none does (→ the harness is a no-op for this consumer, which is fine:
45
+ * Stage C degrades cleanly without Storybook). Pure: no side effects.
46
+ */
47
+ function detect(cwd = process.cwd()) {
48
+ for (const name of PRIORITY) {
49
+ try { if (REGISTRY[name].detect(cwd)) return name; } catch (_) { /* keep scanning */ }
50
+ }
51
+ return null;
52
+ }
53
+
54
+ /** All adapters whose detect() fires (diagnostic). */
55
+ function detectAll(cwd = process.cwd()) {
56
+ return PRIORITY.filter((name) => { try { return REGISTRY[name].detect(cwd); } catch { return false; } });
57
+ }
58
+
59
+ module.exports = { REGISTRY, listAdapters, getAdapter, detect, detectAll };
@@ -0,0 +1,104 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ /**
5
+ * Storybook render adapter (Stage C, v1 — the ONLY render path the adversarial
6
+ * pass kept). When the consumer already has Storybook, we generate EPHEMERAL CSF
7
+ * stories from the render-manifest and let Storybook's OWN runtime do the
8
+ * context-shimming (ThemeProvider/router/i18n via the project's real
9
+ * `.storybook/preview` decorators). We never re-grep a provider chain and never
10
+ * mount out-of-tree — that refuted "auto-provider-shim" is exactly what produces
11
+ * wrong-but-green renders. Storybook exists to solve isolated mounting; reuse it.
12
+ *
13
+ * The generated stories carry a marker so they are identifiable + safe to delete;
14
+ * co-located hand-authored stories are NEVER touched.
15
+ */
16
+ const MARKER = '// @baldart-render-harness — generated from render-manifest.json, safe to delete';
17
+
18
+ class StorybookAdapter {
19
+ constructor(cwd = process.cwd()) { this.cwd = cwd; }
20
+
21
+ static get name() { return 'storybook'; }
22
+ get label() { return 'Storybook'; }
23
+
24
+ /** Detect: a `.storybook/` config dir at the repo root (matches configure.js:234). */
25
+ static detect(cwd = process.cwd()) {
26
+ return fs.existsSync(path.join(cwd, '.storybook'));
27
+ }
28
+
29
+ /**
30
+ * Generate one ephemeral `.stories.tsx` per component into outDir.
31
+ * Returns { written: [paths], skipped: [names] }.
32
+ * i18n note (M6): the project's own preview decorators supply the i18n provider
33
+ * if it has one; if it doesn't, the story still renders but text shows raw keys —
34
+ * such PNGs are flagged i18n-incomplete by `render shot`, never fed to a fidelity diff.
35
+ */
36
+ generate({ manifest, outDir }) {
37
+ fs.mkdirSync(outDir, { recursive: true });
38
+ const written = [];
39
+ const skipped = [];
40
+ for (const comp of (manifest.components || [])) {
41
+ if (!comp.source) { skipped.push(comp.name); continue; } // no source → can't import → skip honestly
42
+ const file = path.join(outDir, `${comp.name}.baldart.stories.tsx`);
43
+ fs.writeFileSync(file, this._story(comp, outDir));
44
+ written.push(file);
45
+ }
46
+ return { written, skipped };
47
+ }
48
+
49
+ _story(comp, outDir) {
50
+ const abs = path.join(this.cwd, comp.source);
51
+ let imp = path.relative(outDir, abs).replace(/\.(tsx?|jsx?)$/, '');
52
+ if (!imp.startsWith('.')) imp = './' + imp;
53
+ imp = imp.split(path.sep).join('/');
54
+ const lines = [
55
+ MARKER,
56
+ `import { ${comp.name} } from '${imp}';`,
57
+ ``,
58
+ `export default { title: 'Baldart Harness/${comp.name}', component: ${comp.name} };`,
59
+ ``,
60
+ ];
61
+ const usedNames = new Set();
62
+ for (const entry of (comp.entries || [])) {
63
+ if (entry.truncated) continue;
64
+ let exp = this._exportName(entry, comp.name);
65
+ while (usedNames.has(exp)) exp += '_';
66
+ usedNames.add(exp);
67
+ lines.push(`export const ${exp} = { args: ${this._args(entry.props)} };`);
68
+ }
69
+ return lines.join('\n') + '\n';
70
+ }
71
+
72
+ _exportName(entry, compName) {
73
+ let tail = entry.id.startsWith(compName + '--') ? entry.id.slice(compName.length + 2) : entry.id;
74
+ const parts = tail.split('--').map((p) => p.replace(/[^A-Za-z0-9]+/g, ' ').trim());
75
+ let name = parts.map((p) => p.split(' ').map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join('')).join('');
76
+ if (!name || /^\d/.test(name)) name = 'Story' + name;
77
+ return name;
78
+ }
79
+
80
+ _args(props) {
81
+ const out = Object.entries(props || {}).map(([k, v]) => `${JSON.stringify(k)}: ${this._coerce(v)}`);
82
+ return `{ ${out.join(', ')} }`;
83
+ }
84
+
85
+ _coerce(v) {
86
+ if (v === 'true' || v === true) return 'true';
87
+ if (v === 'false' || v === false) return 'false';
88
+ if (typeof v === 'string' && /^-?\d+(\.\d+)?$/.test(v)) return v;
89
+ return JSON.stringify(v);
90
+ }
91
+
92
+ /** How `render shot` builds a static render surface (consumer-run, needs deps). */
93
+ buildSurfaceCommand() {
94
+ return { cmd: 'npx', args: ['storybook', 'build', '-o', '.harness/storybook-static'] };
95
+ }
96
+
97
+ /** The iframe URL pattern for a story id in a static Storybook build. */
98
+ storyUrl(baseUrl, storyId) {
99
+ return `${baseUrl}/iframe.html?id=${storyId}&viewMode=story`;
100
+ }
101
+ }
102
+
103
+ module.exports = StorybookAdapter;
104
+ module.exports.MARKER = MARKER;