mirror of
https://github.com/Piebald-AI/claude-code-system-prompts.git
synced 2026-06-13 14:43:33 +08:00
251 lines
15 KiB
Markdown
251 lines
15 KiB
Markdown
<!--
|
|
name: 'Data: Design sync story imports module'
|
|
description: Bundled design sync story-imports module that controls preview compile-time resolution between shipped bundle globals, story source, and configured shims
|
|
ccVersion: 2.1.169
|
|
-->
|
|
// How story modules resolve at preview-compile time. Small on purpose and
|
|
// FORKABLE: copy to .design-sync/overrides/story-imports.mjs (declare in
|
|
// cfg.libOverrides) when a repo's layout needs different rules — this seam
|
|
// owns ALL resolution policy, so a fork never touches generation or build
|
|
// orchestration. Lighter tweaks need no fork: cfg.storyImports.shim /
|
|
// cfg.storyImports.bundle are substring patterns matched against resolved
|
|
// paths (any import style — relative, tsconfig alias, bare workspace name)
|
|
// that force a module to the bundle global / to source bundling, and
|
|
// cfg.storyImports.loaders merges over STORY_LOADERS.
|
|
//
|
|
// Rules:
|
|
// 1. Package + extraEntries imports → `window.<GLOBAL>` (the shipped bundle).
|
|
// Subpaths whose last segment is an exported component (`<pkg>/Button`)
|
|
// shim with that export as the default; every other subpath
|
|
// (`<pkg>/locales/en.json`, `<pkg>/utils`) bundles normally — a wrong
|
|
// shim is silent, a missing module is loud (and the fix is named:
|
|
// cfg.extraEntries merges a subpath's exports onto the global).
|
|
// 2. ANY import that RESOLVES to an EXPORTED component's module →
|
|
// `window.<GLOBAL>` too, however it was spelled (relative `../Button` —
|
|
// the dominant story convention — tsconfig alias, or monorepo path). This
|
|
// keeps previews rendering the SHIPPED bundle instead of a duplicate
|
|
// source copy — which breaks React context identity (consumers throw
|
|
// their missing-provider errors) and drops co-located styles. Story files
|
|
// themselves and anything under node_modules are never redirected.
|
|
// Default imports get the matched export as `default` (default-importing
|
|
// the component is a common story convention; a bare namespace shim
|
|
// renders "Element type is invalid" in every such cell).
|
|
// 3. Every other import (fixtures, helpers, internal contexts) bundles from
|
|
// source; component imports INSIDE those modules recurse through rule 2.
|
|
// The honest residue: a story needing a component-PRIVATE context that
|
|
// must share identity with the global component renders a cell error and
|
|
// falls to grading/hand-fix — no shim can fix that, by construction.
|
|
// 4. @storybook/* runtime → functional stubs. manager/preview/client-api get
|
|
// real no-op hooks (useGlobals/useArgs/addons — module-scope
|
|
// `addons.register()` or a decorator calling `useGlobals()` on an empty
|
|
// stub takes the whole module down); everything else gets an inert
|
|
// callable proxy so the canonical CSF idiom — `args: { onClick: fn() }`,
|
|
// `action('click')` at module scope — evaluates instead of throwing.
|
|
// 5. Styles/assets → LOADERS below (styles ship via _ds_bundle.css/styles.css;
|
|
// images inline as data URLs so fixtures keep working offline). Exception:
|
|
// `.module.css` falls through to esbuild's local-css default — class names
|
|
// resolve and the compiled stylesheet lands at _preview/<Name>.css, which
|
|
// the emitted html links when present.
|
|
|
|
import { existsSync, realpathSync } from 'node:fs';
|
|
import { resolve } from 'node:path';
|
|
|
|
// Storybook's preview-api also re-exports React-compatible hooks for use in
|
|
// render functions — those delegate to the page's React (an inert stub there
|
|
// is a guaranteed render crash: destructuring a non-iterable).
|
|
const MANAGER_API_STUB =
|
|
'const noopChannel={on(){},off(){},once(){},emit(){},removeListener(){}};' +
|
|
'const addons={register(){},add(){},getChannel(){return noopChannel},setConfig(){},getConfig(){return{}}};' +
|
|
'const R=function(){return window.React||{}};' +
|
|
'module.exports={addons,types:{},useGlobals(){return[{},function(){}]},useArgs(){return[{},function(){},function(){}]},useParameter(){},useStorybookApi(){return{}},' +
|
|
'useState(){return R().useState.apply(null,arguments)},useCallback(){return R().useCallback.apply(null,arguments)},useRef(){return R().useRef.apply(null,arguments)},' +
|
|
'useMemo(){return R().useMemo.apply(null,arguments)},useEffect(){return R().useEffect.apply(null,arguments)},useReducer(){return R().useReducer.apply(null,arguments)},' +
|
|
'useChannel(){return function(){}}};';
|
|
|
|
// Inert callable proxy: every member access yields another inert callable, so
|
|
// `fn()`, `action("x")`, `expect.anything()`, `userEvent.click(...)` all
|
|
// evaluate to harmless values at module scope. Named imports are copied by
|
|
// esbuild's CJS interop from own enumerable props, so the common API surface
|
|
// is materialized explicitly (Object.assign keeps them as own props of the
|
|
// callable default — do not change the proxy target's own-property shape);
|
|
// everything else resolves through the get trap. The DEFAULT export is a
|
|
// children-passthrough component: stories render addon defaults as JSX
|
|
// (@storybook/addon-links `<LinkTo>…</LinkTo>`), and an object default
|
|
// throws "Element type is invalid" the instant React mounts it. Both traps
|
|
// hand back the REAL `prototype` — React's shouldConstruct() probes
|
|
// `.prototype.isReactComponent`, and a truthy proxy answer classifies the
|
|
// stub as a CLASS component, silently swallowing the children.
|
|
const INERT_STUB =
|
|
'var inert=new Proxy(function(){},{' +
|
|
'get:function(t,k){if(k==="then")return void 0;if(k==="prototype")return t.prototype;if(k==="valueOf"||k==="toString"||k===Symbol.toPrimitive)return function(){return""};return inert},' +
|
|
'apply:function(){return inert},construct:function(){return{}}});' +
|
|
'var m={};"fn action actions expect userEvent within waitFor screen fireEvent spyOn mocked jest vi configureActions decorateAction setupWorker http HttpResponse graphql rest".split(" ").forEach(function(k){m[k]=inert});' +
|
|
'var def=function(p){return p&&p.children!==void 0?p.children:null};Object.assign(def,m);' +
|
|
'module.exports=new Proxy(def,{get:function(t,k){if(k==="then")return void 0;if(k==="prototype")return t.prototype;return k in m?m[k]:k==="__esModule"?void 0:inert}});';
|
|
|
|
export const STORY_FILE_RE = /\.stor(?:y|ies)\.[cm]?[jt]sx?$/;
|
|
|
|
export const STORY_LOADERS = {
|
|
// jsx is a strict syntax superset of js — JSX-in-.js story files are a
|
|
// common convention and plain .js parses identically.
|
|
'.js': 'jsx',
|
|
'.css': 'empty', '.scss': 'empty', '.sass': 'empty', '.less': 'empty', '.styl': 'empty',
|
|
'.png': 'dataurl', '.jpg': 'dataurl', '.jpeg': 'dataurl', '.gif': 'dataurl',
|
|
'.webp': 'dataurl', '.avif': 'dataurl', '.svg': 'dataurl', '.ico': 'dataurl',
|
|
'.woff': 'dataurl', '.woff2': 'dataurl', '.ttf': 'dataurl', '.eot': 'empty',
|
|
'.md': 'text', '.mdx': 'empty', '.mp4': 'empty', '.webm': 'empty', '.mov': 'empty',
|
|
};
|
|
|
|
// Which exported component (if any) does a resolved file path look like the
|
|
// source module of? Matches `<...>/Button/Button.tsx`, `<...>/Button/index.ts`,
|
|
// and bare `<...>/Button.tsx`; returns the export name or null. A helper
|
|
// coincidentally named like an export (`utils/Text.ts`) would false-positive —
|
|
// that's what cfg.storyImports.bundle is for; over-shimming surfaces
|
|
// immediately as undefined-component cell errors, never as silent wrong
|
|
// renders.
|
|
function exportedComponentFor(p, exported) {
|
|
const segs = p.replace(/\\/g, '/').split('/');
|
|
const file = (segs[segs.length - 1] ?? '').replace(/\.[cm]?[jt]sx?$/, '');
|
|
const dir = segs[segs.length - 2] ?? '';
|
|
if (exported.has(file)) return file;
|
|
if ((file === 'index' || file === dir) && exported.has(dir)) return dir;
|
|
return null;
|
|
}
|
|
|
|
// The @storybook/* stub plugin alone — also used by the decorator bundler.
|
|
export function storybookStubPlugin() {
|
|
return {
|
|
name: 'sb-stub',
|
|
setup(b) {
|
|
b.onResolve({ filter: /^(@storybook\/|storybook(\/|$)|msw(\/|$)|@mswjs\/)/ }, (a) => ({ path: a.path, namespace: 'sb-stub' }));
|
|
b.onLoad({ filter: /.*/, namespace: 'sb-stub' }, (a) => ({
|
|
contents: /(^|\/)(manager|preview|client)-api$/.test(a.path) ? MANAGER_API_STUB : INERT_STUB,
|
|
loader: 'js',
|
|
}));
|
|
},
|
|
};
|
|
}
|
|
|
|
// Build the esbuild plugin set for compiling preview .tsx files (generated
|
|
// story-module wrappers AND hand-authored previews — same rules for both).
|
|
// IMPORTANT for callers: any tsconfig-paths plugin must be registered AFTER
|
|
// these (buildPreviews does this) — the policy plugin resolves aliases via
|
|
// b.resolve, so a paths plugin registered first would bypass rule 2.
|
|
export function storyImportPlugins({ PKG, GLOBAL, extraEntries = [], exported, cfg, pkgDir }) {
|
|
const escRx = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
const pkgRx = new RegExp(`^(?:${[PKG, ...extraEntries].map(escRx).join('|')})(?:/.*)?$`);
|
|
const force = cfg?.storyImports ?? {};
|
|
const matches = (p, pats) => Array.isArray(pats) && pats.some((s) => typeof s === 'string' && p.includes(s));
|
|
// ESM facade shim, NOT CJS: in a `"type":"module"` repo esbuild applies
|
|
// node's ESM-CJS interop to the importing file — `default` becomes the
|
|
// whole exports object and `__esModule` is ignored — which breaks every
|
|
// `import Button from '<pkg>/Button'` (the style most docs examples use).
|
|
// An ESM module binds `default` explicitly under BOTH interop modes; the
|
|
// star re-export of the raw CJS global keeps dynamic named access working
|
|
// (hooks, constants — anything on the global beyond the component list).
|
|
const shimFor = (name) =>
|
|
`export * from "__ds_raw__";var g=window.${GLOBAL};export default ${
|
|
name ? `g[${JSON.stringify(name)}]!==void 0?g[${JSON.stringify(name)}]:g` : `"default" in g?g.default:g`
|
|
};`;
|
|
const shimResult = (name) => ({ path: name ? `ds:${name}` : 'ds', namespace: 'ds-shim' });
|
|
|
|
const dsShim = {
|
|
name: 'ds-global',
|
|
setup(b) {
|
|
const entryNames = new Set([PKG, ...extraEntries]);
|
|
b.onResolve({ filter: pkgRx }, (a) => {
|
|
if (matches(a.path, force.bundle)) return null; // explicit bundle wins
|
|
if (!entryNames.has(a.path)) {
|
|
// Subpath import: a named component shims default-aware; anything
|
|
// else bundles normally — a wrong root-namespace shim is silent
|
|
// (undefined members), a missing module is loud, and the loud
|
|
// path's fix is named (cfg.extraEntries / node_modules symlink in
|
|
// the package's own source repo).
|
|
const name = (a.path.split('/').pop() ?? '').replace(/\.[cm]?[jt]sx?$/, '');
|
|
return exported.has(name) ? shimResult(name) : null;
|
|
}
|
|
return shimResult(null);
|
|
});
|
|
b.onLoad({ filter: /.*/, namespace: 'ds-shim' }, (a) => ({
|
|
contents: shimFor(a.path.startsWith('ds:') ? a.path.slice(3) : null),
|
|
loader: 'js',
|
|
}));
|
|
// Location-independent story imports emitted by the preview generator:
|
|
// `@ds-stories/<repo-root-relative path>` resolves against cwd, so the
|
|
// same wrapper compiles from the generated cache or from
|
|
// .design-sync/previews/ after a promote. Extensionless — esbuild
|
|
// appends its resolve extensions.
|
|
b.onResolve({ filter: /^@ds-stories\// }, (a) => {
|
|
const base = resolve(process.cwd(), a.path.slice('@ds-stories/'.length));
|
|
for (const ext of ['', '.tsx', '.ts', '.jsx', '.js', '.mjs', '.cjs', '.mdx']) {
|
|
if (existsSync(base + ext)) return { path: base + ext };
|
|
}
|
|
return { errors: [{ text: `@ds-stories path not found: ${a.path} (resolved against ${process.cwd()})` }] };
|
|
});
|
|
// The raw CJS module the ESM facade star-re-exports — dynamic names
|
|
// (everything on the global) without a static export list.
|
|
b.onResolve({ filter: /^__ds_raw__$/ }, () => ({ path: '__ds_raw__', namespace: 'ds-raw' }));
|
|
b.onLoad({ filter: /.*/, namespace: 'ds-raw' }, () => ({
|
|
contents: `module.exports=window.${GLOBAL};`,
|
|
loader: 'js',
|
|
}));
|
|
},
|
|
};
|
|
|
|
// Rule 2: resolve every remaining import and shim the ones that land on an
|
|
// exported component's module — regardless of how the import was spelled.
|
|
// Returning the b.resolve result (instead of null) keeps resolution single-pass.
|
|
// The package's own source BARREL (src/index.* under the build cwd OR under
|
|
// the package dir — monorepos build from the repo root while the barrel
|
|
// lives at packages/<x>/src/) shims to the root namespace: `import { X }
|
|
// from "../src"` would otherwise bundle a second copy of the whole library
|
|
// with its own React contexts.
|
|
const CWD = process.cwd().replace(/\\/g, '/');
|
|
// realpath both roots — esbuild's resolver returns symlink-resolved paths,
|
|
// and a merely-resolve()'d root (symlinked tmpdir, symlinked package dir)
|
|
// would never prefix-match them.
|
|
const real = (p) => { try { return realpathSync(p).replace(/\\/g, '/'); } catch { return null; } };
|
|
const barrelRoots = [...new Set([CWD, real(process.cwd()), pkgDir && resolve(pkgDir).replace(/\\/g, '/'), pkgDir && real(pkgDir)].filter(Boolean))];
|
|
const policyRedirect = {
|
|
name: 'ds-import-policy',
|
|
setup(b) {
|
|
b.onResolve({ filter: /.*/ }, async (a) => {
|
|
if (a.pluginData === 'ds-resolving') return null; // our own re-entry
|
|
if (a.kind === 'entry-point' || (a.namespace && a.namespace !== 'file')) return null;
|
|
const r = await b.resolve(a.path, {
|
|
kind: a.kind, resolveDir: a.resolveDir, importer: a.importer,
|
|
pluginData: 'ds-resolving',
|
|
});
|
|
if (r.errors.length > 0 || !r.path) return null;
|
|
if (r.namespace && r.namespace !== 'file') return r; // claimed by another plugin
|
|
const p = r.path.replace(/\\/g, '/');
|
|
if (STORY_FILE_RE.test(p)) return r; // never the story itself
|
|
if (matches(p, force.bundle)) return r; // explicit bundle wins
|
|
if (matches(p, force.shim)) return shimResult(exportedComponentFor(p, exported));
|
|
if (p.includes('/node_modules/')) return r; // third-party stays put
|
|
if (barrelRoots.some((root) => p.startsWith(`${root}/`) && /^src\/index\.[cm]?[jt]sx?$/.test(p.slice(root.length + 1)))) {
|
|
return shimResult(null); // package source barrel
|
|
}
|
|
const name = exportedComponentFor(p, exported);
|
|
return name ? shimResult(name) : r;
|
|
});
|
|
},
|
|
};
|
|
|
|
// Bare `import console from "console"` (and node:console) appears in real
|
|
// story files; node builtins can't bundle for the browser, but this one has
|
|
// an exact page-global equivalent.
|
|
const consoleStub = {
|
|
name: 'node-console-stub',
|
|
setup(b) {
|
|
b.onResolve({ filter: /^(node:)?console$/ }, () => ({ path: 'console', namespace: 'node-console' }));
|
|
b.onLoad({ filter: /.*/, namespace: 'node-console' }, () => ({ contents: 'module.exports=console;', loader: 'js' }));
|
|
},
|
|
};
|
|
|
|
return {
|
|
plugins: [dsShim, storybookStubPlugin(), consoleStub, policyRedirect],
|
|
loaders: { ...STORY_LOADERS, ...(force.loaders ?? {}) },
|
|
};
|
|
}
|