@dfosco/storyboard 0.6.0-beta.2 → 0.6.0-beta.21
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/dist/storyboard-ui.js +3112 -3098
- package/dist/storyboard-ui.js.map +1 -1
- package/mascot/frame-01-peek-left.txt +4 -0
- package/mascot/frame-02-eyes-open.txt +4 -0
- package/mascot/frame-03-peek-right.txt +4 -0
- package/mascot/frame-04-eyes-open.txt +4 -0
- package/mascot/frame-05-eyes-closed.txt +4 -0
- package/mascot/frame-06-eyes-open.txt +4 -0
- package/mascot.config.json +13 -0
- package/package.json +5 -2
- package/scaffold/AGENTS.md +1 -0
- package/scaffold/gitignore +12 -2
- package/scaffold/skills/design-system-catalog/SKILL.md +98 -0
- package/scaffold/skills/design-system-catalog/extract-components.mjs +441 -0
- package/scaffold/skills/design-system-catalog/generate-catalog.sh +255 -0
- package/scaffold/skills/migrate/SKILL.md +72 -50
- package/scaffold/terminal-agent.agent.md +8 -1
- package/src/core/canvas/agent-session.js +103 -17
- package/src/core/canvas/agent-session.test.js +29 -1
- package/src/core/canvas/collision.js +54 -45
- package/src/core/canvas/collision.test.js +39 -0
- package/src/core/canvas/configReader.js +110 -0
- package/src/core/canvas/hot-pool.js +5 -3
- package/src/core/canvas/server.js +32 -13
- package/src/core/canvas/terminal-server.js +156 -91
- package/src/core/cli/agent.js +86 -33
- package/src/core/cli/dev.js +303 -17
- package/src/core/cli/server.js +1 -1
- package/src/core/cli/setup.js +203 -60
- package/src/core/cli/terminal-welcome.js +5 -6
- package/src/core/cli/userState.js +63 -0
- package/src/core/stores/configSchema.js +1 -0
- package/src/core/stores/themeStore.ts +24 -0
- package/src/core/tools/handlers/devtools.test.js +1 -1
- package/src/core/vite/server-plugin.js +107 -10
- package/src/internals/CommandPalette/CommandPalette.jsx +1 -1
- package/src/internals/Viewfinder.jsx +10 -2
- package/src/internals/canvas/CanvasPage.jsx +30 -9
- package/src/internals/canvas/WebGLContextPool.jsx +6 -7
- package/src/internals/canvas/componentIsolate.jsx +7 -8
- package/src/internals/canvas/componentSetIsolate.jsx +7 -8
- package/src/internals/canvas/widgets/PrototypeEmbed.jsx +3 -1
- package/src/internals/canvas/widgets/StorySetWidget.jsx +19 -7
- package/src/internals/canvas/widgets/StoryWidget.jsx +9 -3
- package/src/internals/canvas/widgets/TerminalWidget.jsx +74 -13
- package/src/internals/canvas/widgets/expandUtils.js +4 -2
- package/src/internals/hooks/usePrototypeReloadGuard.js +9 -5
- package/src/internals/vite/data-plugin.js +126 -3
- package/terminal.config.json +66 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "Mascot animation rendered above the Vite dev banner. Each entry in `frames` is either a string (uses frameDurationMs as the default delay) or a [filename, delayMs] tuple for per-frame timing. After loops finish, settleFrame is left on screen with the dev URL beside it.",
|
|
3
|
+
"frames": [
|
|
4
|
+
["frame-01-peek-left.txt", 600],
|
|
5
|
+
["frame-03-peek-right.txt", 600],
|
|
6
|
+
["frame-04-eyes-open.txt", 400],
|
|
7
|
+
["frame-05-eyes-closed.txt", 200],
|
|
8
|
+
["frame-06-eyes-open.txt", 400]
|
|
9
|
+
],
|
|
10
|
+
"loops": 1,
|
|
11
|
+
"settleFrame": "frame-06-eyes-open.txt",
|
|
12
|
+
"enabled": true
|
|
13
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dfosco/storyboard",
|
|
3
|
-
"version": "0.6.0-beta.
|
|
3
|
+
"version": "0.6.0-beta.21",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Storyboard prototyping framework — core engine, React integration, and canvas",
|
|
@@ -18,10 +18,13 @@
|
|
|
18
18
|
"src",
|
|
19
19
|
"dist",
|
|
20
20
|
"scaffold",
|
|
21
|
+
"mascot",
|
|
21
22
|
"toolbar.config.json",
|
|
22
23
|
"widgets.config.json",
|
|
23
24
|
"paste.config.json",
|
|
24
|
-
"commandpalette.config.json"
|
|
25
|
+
"commandpalette.config.json",
|
|
26
|
+
"terminal.config.json",
|
|
27
|
+
"mascot.config.json"
|
|
25
28
|
],
|
|
26
29
|
"scripts": {
|
|
27
30
|
"build:css": "tailwindcss -i src/core/styles/tailwind.css -o dist/tailwind.css --minify",
|
package/scaffold/AGENTS.md
CHANGED
|
@@ -233,6 +233,7 @@ The fix was a one-line compaction (4MB → 9KB). Everything the user reported
|
|
|
233
233
|
- Use **Primer Octicons** from `@primer/octicons-react` for icons
|
|
234
234
|
- **Before importing any icon, verify it exists in `.agents/data/primer-octicons.json`** (the authoritative export list, generated from the installed package). Agents frequently invent icon names like `ScissorsIcon` that do not exist and break the page at runtime. If the icon you want is not in that list, pick the closest available name from the list — never guess.
|
|
235
235
|
- Regenerate the list after bumping `@primer/octicons-react`: `node scripts/gen-octicons-list.mjs`
|
|
236
|
+
- Use the **canvas widget type registry** at `.agents/data/widget-types.json` as the authoritative list of valid canvas widget `type` strings, their props, default sizes, and when-to-use guidance. Never invent widget types. In particular: when showing multiple variants of a single story file, use ONE `component-set` widget — not N `story` widgets. Regenerate after changing the widget registry: `node scripts/gen-widget-types.mjs`.
|
|
236
237
|
- Use **CSS Modules** (`*.module.css`) for component-specific styles
|
|
237
238
|
- If you find any `sx` styled-components styling, migrate them to CSS Modules
|
|
238
239
|
- **Components must live in their own directory:** `src/components/Name/Name.jsx`, `Name.module.css`, `name.story.jsx`. Never place component files flat in `src/components/`.
|
package/scaffold/gitignore
CHANGED
|
@@ -52,8 +52,18 @@ packages/core/dist/storyboard-ui.*
|
|
|
52
52
|
# Agent Browser
|
|
53
53
|
agent-browser.json
|
|
54
54
|
|
|
55
|
-
#
|
|
56
|
-
.storyboard
|
|
55
|
+
# Auto-generated scaffold dir (copies of library config defaults — overwritten on every dev-server boot)
|
|
56
|
+
.storyboard/scaffold/
|
|
57
|
+
|
|
58
|
+
# Real-time canvas selection bridge for Copilot
|
|
59
|
+
.storyboard/.selectedwidgets.json
|
|
60
|
+
|
|
61
|
+
# Runtime/transient state (per-machine, per-session)
|
|
62
|
+
.storyboard/agent-sessions/
|
|
63
|
+
.storyboard/hot-pool/
|
|
64
|
+
.storyboard/logs/
|
|
65
|
+
.storyboard/terminal-buffers/
|
|
66
|
+
.storyboard/messages/
|
|
57
67
|
|
|
58
68
|
# Private canvas images (tilde prefix = not committed)
|
|
59
69
|
src/canvas/images/~*
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Design System Catalog Generator
|
|
2
|
+
|
|
3
|
+
> Triggered by: "generate component catalog", "create component catalog", "catalog components for [package]", "design system catalog", "create catalog for [package]", when the user wants to catalog an installed design system's components
|
|
4
|
+
|
|
5
|
+
## What This Does
|
|
6
|
+
|
|
7
|
+
Generates a **new skill** that catalogs all components from any React design system package installed in `node_modules`. The output skill provides the agent with a complete reference of available components, sub-components, and props.
|
|
8
|
+
|
|
9
|
+
## When This Applies
|
|
10
|
+
|
|
11
|
+
- User installs a new design system and wants an agent-readable component reference
|
|
12
|
+
- User wants to create a component catalog for an existing dependency
|
|
13
|
+
- A design system package is upgraded and the catalog needs regenerating
|
|
14
|
+
|
|
15
|
+
## How It Works
|
|
16
|
+
|
|
17
|
+
The extractor uses a **TypeScript-first** strategy:
|
|
18
|
+
|
|
19
|
+
1. **Find declarations** — Locates the package's `.d.ts` entry point via `package.json` fields (`types`, `typings`, `exports["."].types`, `typesVersions`) or common paths (`dist/index.d.ts`, etc.)
|
|
20
|
+
2. **Walk exports** — Recursively follows `export *` and `export { ... } from` to discover all PascalCase exports (components)
|
|
21
|
+
3. **Extract details** — For each component, parses `.d.ts` files for `*Props` types and sub-component declarations
|
|
22
|
+
4. **Auto-categorize** — Infers categories from the package's directory structure (e.g., `dist/layout/` → Layout). Falls back to alphabetical grouping
|
|
23
|
+
5. **Runtime enrichment** — Optionally tries `import()` to detect compound component sub-components (best-effort, skipped if the package can't be imported in Node)
|
|
24
|
+
|
|
25
|
+
## Prerequisites
|
|
26
|
+
|
|
27
|
+
- The design system package **must be installed** in `node_modules`
|
|
28
|
+
- The package should ship TypeScript declarations (`.d.ts`) or have `@types/*` available
|
|
29
|
+
- Node.js must be available
|
|
30
|
+
|
|
31
|
+
## Steps
|
|
32
|
+
|
|
33
|
+
### Step 1 — Ask for the package
|
|
34
|
+
|
|
35
|
+
Ask the user which design system package to catalog. They should provide:
|
|
36
|
+
- **Package name** (required): e.g., `@primer/react`, `@chakra-ui/react`, `antd`, `@mui/material`
|
|
37
|
+
- **Display name** (optional): e.g., "Primer React", "Chakra UI". Auto-derived if not provided.
|
|
38
|
+
|
|
39
|
+
### Step 2 — Verify installation
|
|
40
|
+
|
|
41
|
+
Check that the package exists in `node_modules`:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
ls node_modules/<package-name>/package.json
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
If not found, tell the user to run `npm install <package-name>` first.
|
|
48
|
+
|
|
49
|
+
### Step 3 — Run the generator
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
|
53
|
+
.agents/skills/design-system-catalog/generate-catalog.sh "<package-name>" "<display-name>"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The script will:
|
|
57
|
+
1. Extract all components from the package's TypeScript declarations
|
|
58
|
+
2. Create a new skill directory: `.agents/skills/<name>-components-catalog/`
|
|
59
|
+
3. Generate `SKILL.md` with the full component catalog
|
|
60
|
+
4. Generate `extract-components.sh` for future regeneration
|
|
61
|
+
|
|
62
|
+
### Step 4 — Report results
|
|
63
|
+
|
|
64
|
+
Tell the user:
|
|
65
|
+
- How many components were found
|
|
66
|
+
- Where the generated skill lives
|
|
67
|
+
- How to regenerate when the package is upgraded
|
|
68
|
+
- Suggest they review the catalog and customize categories if needed
|
|
69
|
+
|
|
70
|
+
## Output Structure
|
|
71
|
+
|
|
72
|
+
The generator creates:
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
.agents/skills/<name>-components-catalog/
|
|
76
|
+
SKILL.md # Skill file with triggers, usage, and full catalog
|
|
77
|
+
extract-components.sh # Thin wrapper to regenerate the catalog
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Where `<name>` is derived from the package name (e.g., `@primer/react` → `primer-react`).
|
|
81
|
+
|
|
82
|
+
## Customization
|
|
83
|
+
|
|
84
|
+
After generation, the user can:
|
|
85
|
+
- Edit category groupings in the generated `SKILL.md`
|
|
86
|
+
- Add codebase-specific conventions (e.g., "never use Box", "prefer CSS Modules over sx")
|
|
87
|
+
- Add or remove components from the catalog
|
|
88
|
+
- Change trigger conditions
|
|
89
|
+
|
|
90
|
+
## Regeneration
|
|
91
|
+
|
|
92
|
+
Each generated skill includes an `extract-components.sh` that calls back to this skill's shared extractor. To regenerate:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
.agents/skills/<name>-components-catalog/extract-components.sh
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
This should be run whenever the design system package is upgraded.
|
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Universal design system component extractor.
|
|
5
|
+
*
|
|
6
|
+
* Extracts React components from any installed design system package.
|
|
7
|
+
* Primary strategy: parse TypeScript declarations (.d.ts).
|
|
8
|
+
* Secondary: optional runtime import for sub-component enrichment.
|
|
9
|
+
*
|
|
10
|
+
* Usage: node extract-components.mjs <package-name>
|
|
11
|
+
* Output: JSON to stdout
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { readFileSync, existsSync } from "fs";
|
|
15
|
+
import { join, dirname, relative, sep } from "path";
|
|
16
|
+
import { createRequire } from "module";
|
|
17
|
+
|
|
18
|
+
// ── Args ────────────────────────────────────────────────────
|
|
19
|
+
const packageName = process.argv[2];
|
|
20
|
+
if (!packageName) {
|
|
21
|
+
process.stderr.write("Usage: node extract-components.mjs <package-name>\n");
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ── Resolve package ─────────────────────────────────────────
|
|
26
|
+
const require_ = createRequire(
|
|
27
|
+
join(process.env.REPO_ROOT || process.cwd(), "package.json")
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
let pkgRoot;
|
|
31
|
+
try {
|
|
32
|
+
const p = require_.resolve(`${packageName}/package.json`);
|
|
33
|
+
pkgRoot = dirname(p);
|
|
34
|
+
} catch {
|
|
35
|
+
const fallback = join(
|
|
36
|
+
process.env.REPO_ROOT || process.cwd(),
|
|
37
|
+
"node_modules",
|
|
38
|
+
...packageName.split("/")
|
|
39
|
+
);
|
|
40
|
+
if (existsSync(join(fallback, "package.json"))) {
|
|
41
|
+
pkgRoot = fallback;
|
|
42
|
+
} else {
|
|
43
|
+
process.stderr.write(`Error: package "${packageName}" not found.\n`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const pkgJson = JSON.parse(
|
|
49
|
+
readFileSync(join(pkgRoot, "package.json"), "utf-8")
|
|
50
|
+
);
|
|
51
|
+
const version = pkgJson.version;
|
|
52
|
+
|
|
53
|
+
// ── Skip lists ──────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
const SKIP_NAMES = new Set([
|
|
56
|
+
"Omit", "Pick", "Partial", "Record", "React", "HTMLElement",
|
|
57
|
+
"PropsWithChildren", "ForwardRefExoticComponent", "FCWithSlotMarker",
|
|
58
|
+
"Provider", "Consumer", "Fragment", "Suspense", "StrictMode",
|
|
59
|
+
"Component", "PureComponent", "Element",
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
const SKIP_PROPS = new Set([
|
|
63
|
+
"children", "className", "style", "ref", "key", "as",
|
|
64
|
+
"dangerouslySetInnerHTML", "suppressHydrationWarning",
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
// ── TypeScript declaration parsing (primary) ────────────────
|
|
68
|
+
|
|
69
|
+
function findTypesEntryPoint() {
|
|
70
|
+
const candidates = [
|
|
71
|
+
pkgJson.types,
|
|
72
|
+
pkgJson.typings,
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
// Crawl the exports map for type entries
|
|
76
|
+
if (pkgJson.exports) {
|
|
77
|
+
const root = pkgJson.exports["."];
|
|
78
|
+
if (typeof root === "string" && root.endsWith(".d.ts")) {
|
|
79
|
+
candidates.push(root);
|
|
80
|
+
} else if (typeof root === "object" && root !== null) {
|
|
81
|
+
candidates.push(
|
|
82
|
+
root.types,
|
|
83
|
+
root.import?.types,
|
|
84
|
+
root.require?.types,
|
|
85
|
+
root.node?.types,
|
|
86
|
+
root.default?.types
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// typesVersions (e.g. {"*": {"*": ["dist/types/*"]}})
|
|
92
|
+
if (pkgJson.typesVersions) {
|
|
93
|
+
for (const range of Object.values(pkgJson.typesVersions)) {
|
|
94
|
+
if (range["*"]) {
|
|
95
|
+
for (const pattern of range["*"]) {
|
|
96
|
+
const resolved = pattern.replace("*", "index");
|
|
97
|
+
candidates.push(resolved);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (range["."]) {
|
|
101
|
+
for (const pattern of range["."]) {
|
|
102
|
+
candidates.push(pattern.replace("*", "index"));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Common fallback locations
|
|
109
|
+
candidates.push(
|
|
110
|
+
"dist/index.d.ts", "dist/index.d.mts", "dist/index.d.cts",
|
|
111
|
+
"lib/index.d.ts", "index.d.ts", "types/index.d.ts",
|
|
112
|
+
"dist/cjs/index.d.ts", "dist/esm/index.d.ts"
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
for (const c of candidates.filter(Boolean)) {
|
|
116
|
+
const resolved = join(pkgRoot, c);
|
|
117
|
+
if (existsSync(resolved)) return resolved;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Try DefinitelyTyped (@types/*)
|
|
121
|
+
const typesPackage = packageName.startsWith("@")
|
|
122
|
+
? `@types/${packageName.slice(1).replace("/", "__")}`
|
|
123
|
+
: `@types/${packageName}`;
|
|
124
|
+
try {
|
|
125
|
+
return require_.resolve(`${typesPackage}/index.d.ts`);
|
|
126
|
+
} catch {
|
|
127
|
+
/* not found */
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function resolveImportPath(fromFile, specifier) {
|
|
134
|
+
const dir = dirname(fromFile);
|
|
135
|
+
for (const ext of [
|
|
136
|
+
".d.ts", ".d.mts", ".d.cts",
|
|
137
|
+
"/index.d.ts", "/index.d.mts", "/index.d.cts",
|
|
138
|
+
]) {
|
|
139
|
+
const candidate = join(dir, specifier + ext);
|
|
140
|
+
if (existsSync(candidate)) return candidate;
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Walk the export tree starting from entryFile.
|
|
147
|
+
* Returns Map<name, { name, sourceFile }>.
|
|
148
|
+
*/
|
|
149
|
+
function parseExports(entryFile, depth = 0, visited = new Set()) {
|
|
150
|
+
if (depth > 8 || visited.has(entryFile) || !existsSync(entryFile))
|
|
151
|
+
return new Map();
|
|
152
|
+
visited.add(entryFile);
|
|
153
|
+
|
|
154
|
+
const content = readFileSync(entryFile, "utf-8");
|
|
155
|
+
const results = new Map();
|
|
156
|
+
|
|
157
|
+
for (const line of content.split("\n")) {
|
|
158
|
+
if (!line.startsWith("export ")) continue;
|
|
159
|
+
if (line.startsWith("export type ")) continue;
|
|
160
|
+
|
|
161
|
+
// export * from './path'
|
|
162
|
+
const starMatch = line.match(/export\s*\*\s*from\s*['"]([^'"]+)['"]/);
|
|
163
|
+
if (starMatch) {
|
|
164
|
+
const resolved = resolveImportPath(entryFile, starMatch[1]);
|
|
165
|
+
if (resolved) {
|
|
166
|
+
for (const [k, v] of parseExports(resolved, depth + 1, visited)) {
|
|
167
|
+
if (!results.has(k)) results.set(k, v);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// export { Foo, Bar, default as Baz } from './path'
|
|
174
|
+
const namedFrom = line.match(
|
|
175
|
+
/export\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/
|
|
176
|
+
);
|
|
177
|
+
if (namedFrom) {
|
|
178
|
+
const resolved = resolveImportPath(entryFile, namedFrom[2]);
|
|
179
|
+
for (const part of namedFrom[1].split(",").map((s) => s.trim())) {
|
|
180
|
+
if (part.startsWith("type ")) continue;
|
|
181
|
+
let name;
|
|
182
|
+
const asDefault = part.match(/default\s+as\s+(\w+)/);
|
|
183
|
+
const asRename = part.match(/(\w+)\s+as\s+(\w+)/);
|
|
184
|
+
const plain = part.match(/^(\w+)$/);
|
|
185
|
+
if (asDefault) name = asDefault[1];
|
|
186
|
+
else if (asRename) name = asRename[2];
|
|
187
|
+
else if (plain) name = plain[1];
|
|
188
|
+
if (name && /^[A-Z]/.test(name) && !SKIP_NAMES.has(name)) {
|
|
189
|
+
results.set(name, { name, sourceFile: resolved || entryFile });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// export declare const/function/class Foo
|
|
196
|
+
const decl = line.match(
|
|
197
|
+
/export\s+declare\s+(?:const|function|class)\s+([A-Z]\w+)/
|
|
198
|
+
);
|
|
199
|
+
if (decl && !SKIP_NAMES.has(decl[1])) {
|
|
200
|
+
results.set(decl[1], { name: decl[1], sourceFile: entryFile });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return results;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Extract sub-components from .d.ts content for a compound component.
|
|
209
|
+
* Handles both property declarations and intersection types.
|
|
210
|
+
*/
|
|
211
|
+
function extractSubComponentsDts(dtsContent, compName) {
|
|
212
|
+
const subs = [];
|
|
213
|
+
const seen = new Set();
|
|
214
|
+
|
|
215
|
+
// Pattern 1: indented property — ` SubName: ...`
|
|
216
|
+
for (const line of dtsContent.split("\n")) {
|
|
217
|
+
const m = line.match(/^\s{2,8}([A-Z][a-zA-Z]+)\s*:/);
|
|
218
|
+
if (m && !SKIP_NAMES.has(m[1]) && !seen.has(m[1])) {
|
|
219
|
+
seen.add(m[1]);
|
|
220
|
+
subs.push(m[1]);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Pattern 2: intersection — `typeof X & { Cell: ...; Header: ... }`
|
|
225
|
+
const intersectionRe = /&\s*\{([^}]+)\}/g;
|
|
226
|
+
let im;
|
|
227
|
+
while ((im = intersectionRe.exec(dtsContent)) !== null) {
|
|
228
|
+
const block = im[1];
|
|
229
|
+
const propRe = /([A-Z][a-zA-Z]+)\s*:/g;
|
|
230
|
+
let pm;
|
|
231
|
+
while ((pm = propRe.exec(block)) !== null) {
|
|
232
|
+
if (!SKIP_NAMES.has(pm[1]) && !seen.has(pm[1])) {
|
|
233
|
+
seen.add(pm[1]);
|
|
234
|
+
subs.push(pm[1]);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return subs;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Extract props from *Props type/interface definitions.
|
|
244
|
+
*/
|
|
245
|
+
function extractPropsDts(dtsContent, compName) {
|
|
246
|
+
const props = [];
|
|
247
|
+
const seen = new Set();
|
|
248
|
+
|
|
249
|
+
// Match type/interface blocks named {CompName}Props or {CompName}BaseProps
|
|
250
|
+
const patterns = [
|
|
251
|
+
new RegExp(
|
|
252
|
+
`(?:export\\s+)?(?:type|interface)\\s+${compName}(?:Base)?Props[^{]*\\{([^}]+(?:\\{[^}]*\\}[^}]*)*)\\}`,
|
|
253
|
+
"gs"
|
|
254
|
+
),
|
|
255
|
+
];
|
|
256
|
+
|
|
257
|
+
for (const pattern of patterns) {
|
|
258
|
+
let match;
|
|
259
|
+
while ((match = pattern.exec(dtsContent)) !== null) {
|
|
260
|
+
const block = match[1];
|
|
261
|
+
const propRe =
|
|
262
|
+
/(?:\/\*\*[\s\S]*?\*\/\s*)?['"]?(\w+)['"]?\s*\??\s*:\s*([^;\n]+)/g;
|
|
263
|
+
let pm;
|
|
264
|
+
while ((pm = propRe.exec(block)) !== null) {
|
|
265
|
+
const propName = pm[1];
|
|
266
|
+
let propType = pm[2].trim();
|
|
267
|
+
if (SKIP_PROPS.has(propName)) continue;
|
|
268
|
+
if (propName.startsWith("_")) continue;
|
|
269
|
+
if (seen.has(propName)) continue;
|
|
270
|
+
if (
|
|
271
|
+
/^[A-Z]/.test(propName) &&
|
|
272
|
+
/(?:ForwardRef|FCWith|ExoticComponent|typeof)/.test(propType)
|
|
273
|
+
)
|
|
274
|
+
continue;
|
|
275
|
+
|
|
276
|
+
propType = propType.replace(/import\([^)]+\)\./g, "");
|
|
277
|
+
if (propType.length > 80) propType = propType.substring(0, 77) + "...";
|
|
278
|
+
|
|
279
|
+
seen.add(propName);
|
|
280
|
+
props.push({ name: propName, type: propType });
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return props;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Read all relevant .d.ts content for a component.
|
|
290
|
+
*/
|
|
291
|
+
function readComponentDts(sourceFile, compName) {
|
|
292
|
+
if (!sourceFile || !existsSync(sourceFile)) return "";
|
|
293
|
+
|
|
294
|
+
const sources = [];
|
|
295
|
+
sources.push(readFileSync(sourceFile, "utf-8"));
|
|
296
|
+
|
|
297
|
+
const dir = dirname(sourceFile);
|
|
298
|
+
|
|
299
|
+
// Also check for types.d.ts in same directory
|
|
300
|
+
for (const extra of ["types.d.ts", `${compName}.d.ts`]) {
|
|
301
|
+
const f = join(dir, extra);
|
|
302
|
+
if (existsSync(f) && f !== sourceFile) {
|
|
303
|
+
sources.push(readFileSync(f, "utf-8"));
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return sources.join("\n");
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ── Auto-categorization ─────────────────────────────────────
|
|
311
|
+
|
|
312
|
+
const CATEGORY_MAP = {
|
|
313
|
+
layout: "Layout", layouts: "Layout", grid: "Layout",
|
|
314
|
+
navigation: "Navigation", nav: "Navigation",
|
|
315
|
+
action: "Actions", actions: "Actions", menu: "Actions",
|
|
316
|
+
menus: "Actions", button: "Actions", buttons: "Actions",
|
|
317
|
+
form: "Forms", forms: "Forms", input: "Forms", inputs: "Forms",
|
|
318
|
+
data: "Data Display", display: "Data Display", "data-display": "Data Display",
|
|
319
|
+
feedback: "Feedback",
|
|
320
|
+
overlay: "Overlays", overlays: "Overlays", modal: "Overlays", dialog: "Overlays",
|
|
321
|
+
content: "Content", typography: "Typography",
|
|
322
|
+
utility: "Utilities", utilities: "Utilities",
|
|
323
|
+
general: "General", common: "General",
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
function categorizeFromPath(sourceFile) {
|
|
327
|
+
if (!sourceFile) return "Components";
|
|
328
|
+
|
|
329
|
+
const rel = relative(pkgRoot, sourceFile);
|
|
330
|
+
const parts = rel
|
|
331
|
+
.split(sep)
|
|
332
|
+
.filter((p) => !["dist", "lib", "src", "cjs", "esm", "types"].includes(p));
|
|
333
|
+
|
|
334
|
+
for (const part of parts) {
|
|
335
|
+
const lower = part.toLowerCase();
|
|
336
|
+
if (CATEGORY_MAP[lower]) return CATEGORY_MAP[lower];
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (parts.length >= 2) {
|
|
340
|
+
const parentDir = parts[parts.length - 2].toLowerCase();
|
|
341
|
+
if (CATEGORY_MAP[parentDir]) return CATEGORY_MAP[parentDir];
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return "Components";
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ── Runtime enrichment (optional, best-effort) ──────────────
|
|
348
|
+
|
|
349
|
+
async function tryRuntimeEnrichment(components) {
|
|
350
|
+
try {
|
|
351
|
+
const mod = await import(packageName);
|
|
352
|
+
|
|
353
|
+
for (const [name, comp] of components) {
|
|
354
|
+
const value = mod[name];
|
|
355
|
+
if (!value) continue;
|
|
356
|
+
|
|
357
|
+
// Enrich sub-components from runtime (most reliable for compound components)
|
|
358
|
+
try {
|
|
359
|
+
const runtimeSubs = [];
|
|
360
|
+
for (const [subName, subValue] of Object.entries(value)) {
|
|
361
|
+
if (!/^[A-Z]/.test(subName)) continue;
|
|
362
|
+
if (SKIP_NAMES.has(subName)) continue;
|
|
363
|
+
const isSub =
|
|
364
|
+
typeof subValue === "function" ||
|
|
365
|
+
(typeof subValue === "object" && subValue?.$$typeof != null) ||
|
|
366
|
+
(typeof subValue === "object" &&
|
|
367
|
+
typeof subValue?.render === "function");
|
|
368
|
+
if (isSub) runtimeSubs.push(subName);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (runtimeSubs.length > 0) {
|
|
372
|
+
const existing = new Set(comp.subComponents);
|
|
373
|
+
for (const sub of runtimeSubs) {
|
|
374
|
+
if (!existing.has(sub)) comp.subComponents.push(sub);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
} catch {
|
|
378
|
+
/* non-iterable export */
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
process.stderr.write("Runtime enrichment: success\n");
|
|
383
|
+
} catch {
|
|
384
|
+
process.stderr.write(
|
|
385
|
+
"Runtime enrichment: skipped (import failed, using .d.ts data only)\n"
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// ── Build component list ────────────────────────────────────
|
|
391
|
+
|
|
392
|
+
const typesEntry = findTypesEntryPoint();
|
|
393
|
+
|
|
394
|
+
if (!typesEntry) {
|
|
395
|
+
process.stderr.write(
|
|
396
|
+
`Error: no TypeScript declarations found for "${packageName}".\n` +
|
|
397
|
+
"Ensure the package ships .d.ts files or install @types/* for it.\n"
|
|
398
|
+
);
|
|
399
|
+
process.exit(1);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
process.stderr.write(`Types entry: ${relative(pkgRoot, typesEntry)}\n`);
|
|
403
|
+
|
|
404
|
+
const tsExports = parseExports(typesEntry);
|
|
405
|
+
|
|
406
|
+
process.stderr.write(
|
|
407
|
+
`Found ${tsExports.size} PascalCase exports in declarations\n`
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
// Build component map from TypeScript exports
|
|
411
|
+
const components = new Map();
|
|
412
|
+
|
|
413
|
+
for (const [name, info] of tsExports) {
|
|
414
|
+
if (name.startsWith("use") || !/^[A-Z]/.test(name)) continue;
|
|
415
|
+
|
|
416
|
+
const dtsContent = readComponentDts(info.sourceFile, name);
|
|
417
|
+
|
|
418
|
+
components.set(name, {
|
|
419
|
+
name,
|
|
420
|
+
importPath: packageName,
|
|
421
|
+
subComponents: extractSubComponentsDts(dtsContent, name),
|
|
422
|
+
props: extractPropsDts(dtsContent, name),
|
|
423
|
+
category: categorizeFromPath(info.sourceFile),
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Try runtime enrichment for sub-components (best-effort)
|
|
428
|
+
await tryRuntimeEnrichment(components);
|
|
429
|
+
|
|
430
|
+
// ── Output ──────────────────────────────────────────────────
|
|
431
|
+
|
|
432
|
+
const output = {
|
|
433
|
+
packageName,
|
|
434
|
+
version,
|
|
435
|
+
totalComponents: components.size,
|
|
436
|
+
components: [...components.values()].sort((a, b) =>
|
|
437
|
+
a.name.localeCompare(b.name)
|
|
438
|
+
),
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
process.stdout.write(JSON.stringify(output, null, 2) + "\n");
|