3.7 KiB
// generatePreviewSource (package shape) — emits the preview wrapper body // (written to the generated cache, .design-sync/.cache/previews/.tsx) // for one component, or null when there is nothing real to compose from. // No stories exist in this shape, so preview quality comes from AUTHORED // sources, in order: // 1. a user-authored .design-sync/previews/.tsx — owned by location, // always wins, and this generator is never consulted for it // 2. cfg.previewArgs. — props supplied via config; compiled into a // real preview module like any authored file // 3. null — the html ships the floor card (a single render attempt with // a typographic fallback), which is honest about being unauthored // No guessed variant grids or namespace stubs: with no reference render to // verify against, an elaborate guess and a simple one are equally // unverifiable, and a guess styled like a real preview reads as more // finished than it is.
import { exportName } from './common.mjs';
// smartDefaultProps $raw values — a small closed set of literal expressions. // Whitelist-gated so config-sourced previewArgs can't inject arbitrary JS. const RAW_OK = /^(?:()\s*=>\s*(?:null|undefined|{})|new Date())$/;
// JSON props → JSX attribute string. Functions / React elements drop out.
// $raw values (smartDefaultProps' crash-prevention stubs) emit as bare
// expression containers; everything else as {JSON.stringify(v)}.
export function propsToJsx(args) {
const out = [];
for (const [k, v] of Object.entries(args)) {
if (typeof v === 'function' || (v && typeof v === 'object' && v.$$typeof)) continue;
// Dotted argType keys (Title.as) are sub-component addressing — not a
// valid JSX attr name on the root.
if (k === 'children' || k.includes('.')) continue;
if (v && typeof v === 'object' && typeof v.$raw === 'string') {
if (RAW_OK.test(v.$raw)) out.push( ${k}={${v.$raw}});
} else if (v && typeof v === 'object' && v.$jsx) {
// floor-card-only marker — not expressible as a JSX attr here
} else if (v === true) out.push( ${k});
else {
try { out.push( ${k}={${JSON.stringify(v)}}); } catch { /* skip uncloneable */ }
}
}
return out.join('');
}
// Children between >/< — wrap in {JSON.stringify(...)} so a value
// containing { } < > doesn't reopen the parser. {"plain"} renders the
// same as plain, so always-wrap is correct.
const jsxChildren = (s) => {${JSON.stringify(s)}};
// Generate the preview .tsx body for one component (marker is prepended by
// writePreviewFiles so its hash covers this body only), or null → floor card.
export function generatePreviewSource(c, { smart, exported, pkg, previewArgs }) {
if (!previewArgs) return null;
// smart.props carries crash-prevention stubs from the .d.ts (required
// callbacks → {$raw:'()=>null'}, arrays → [], open/visible → true). Spread
// under explicit args so stubs fill gaps without overriding real values.
const stubs = smart?.props ?? {};
const stubKids = typeof stubs.children === 'string' ? stubs.children : null;
const used = new Set(exported);
const kids = (typeof previewArgs.children === 'string' ? previewArgs.children : null) ?? stubKids;
const attrs = propsToJsx({ ...stubs, ...previewArgs });
const jsx = kids
? <${c.name}${attrs}>${jsxChildren(kids)}</${c.name}>
: <${c.name}${attrs} />;
return import { ${c.name} } from '${pkg}';\n\nexport const ${exportName('Preview', used)} = () => ${jsx};\n;
}