@contentful/experience-design-system-cli 2.2.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.
- package/README.md +532 -0
- package/bin/cli.js +58 -0
- package/dist/package.json +56 -0
- package/dist/src/analyze/command.d.ts +3 -0
- package/dist/src/analyze/command.js +175 -0
- package/dist/src/analyze/extract/astro.d.ts +5 -0
- package/dist/src/analyze/extract/astro.js +280 -0
- package/dist/src/analyze/extract/pipeline.d.ts +6 -0
- package/dist/src/analyze/extract/pipeline.js +298 -0
- package/dist/src/analyze/extract/react.d.ts +2 -0
- package/dist/src/analyze/extract/react.js +1949 -0
- package/dist/src/analyze/extract/slot-detection.d.ts +35 -0
- package/dist/src/analyze/extract/slot-detection.js +101 -0
- package/dist/src/analyze/extract/stencil.d.ts +2 -0
- package/dist/src/analyze/extract/stencil.js +293 -0
- package/dist/src/analyze/extract/tsx-shared.d.ts +8 -0
- package/dist/src/analyze/extract/tsx-shared.js +263 -0
- package/dist/src/analyze/extract/vue-tsx.d.ts +2 -0
- package/dist/src/analyze/extract/vue-tsx.js +498 -0
- package/dist/src/analyze/extract/vue.d.ts +5 -0
- package/dist/src/analyze/extract/vue.js +647 -0
- package/dist/src/analyze/extract/web-components.d.ts +2 -0
- package/dist/src/analyze/extract/web-components.js +866 -0
- package/dist/src/analyze/pre-classify.d.ts +17 -0
- package/dist/src/analyze/pre-classify.js +144 -0
- package/dist/src/analyze/select/command.d.ts +2 -0
- package/dist/src/analyze/select/command.js +256 -0
- package/dist/src/analyze/select/index.d.ts +6 -0
- package/dist/src/analyze/select/index.js +5 -0
- package/dist/src/analyze/select/parser.d.ts +6 -0
- package/dist/src/analyze/select/parser.js +53 -0
- package/dist/src/analyze/select/persistence.d.ts +9 -0
- package/dist/src/analyze/select/persistence.js +42 -0
- package/dist/src/analyze/select/stdout.d.ts +7 -0
- package/dist/src/analyze/select/stdout.js +3 -0
- package/dist/src/analyze/select/tui/App.d.ts +8 -0
- package/dist/src/analyze/select/tui/App.js +491 -0
- package/dist/src/analyze/select/tui/components/ComponentDetail.d.ts +20 -0
- package/dist/src/analyze/select/tui/components/ComponentDetail.js +43 -0
- package/dist/src/analyze/select/tui/components/FieldEditor.d.ts +11 -0
- package/dist/src/analyze/select/tui/components/FieldEditor.js +531 -0
- package/dist/src/analyze/select/tui/components/FinalizeDialog.d.ts +10 -0
- package/dist/src/analyze/select/tui/components/FinalizeDialog.js +15 -0
- package/dist/src/analyze/select/tui/components/HelpOverlay.d.ts +7 -0
- package/dist/src/analyze/select/tui/components/HelpOverlay.js +11 -0
- package/dist/src/analyze/select/tui/components/JsonEditor.d.ts +11 -0
- package/dist/src/analyze/select/tui/components/JsonEditor.js +154 -0
- package/dist/src/analyze/select/tui/components/JsonPanel.d.ts +11 -0
- package/dist/src/analyze/select/tui/components/JsonPanel.js +62 -0
- package/dist/src/analyze/select/tui/components/PreviewSummaryBar.d.ts +8 -0
- package/dist/src/analyze/select/tui/components/PreviewSummaryBar.js +29 -0
- package/dist/src/analyze/select/tui/components/QuitDialog.d.ts +8 -0
- package/dist/src/analyze/select/tui/components/QuitDialog.js +14 -0
- package/dist/src/analyze/select/tui/components/Sidebar.d.ts +15 -0
- package/dist/src/analyze/select/tui/components/Sidebar.js +48 -0
- package/dist/src/analyze/select/tui/components/SourcePanel.d.ts +11 -0
- package/dist/src/analyze/select/tui/components/SourcePanel.js +52 -0
- package/dist/src/analyze/select/tui/components/StatusBar.d.ts +11 -0
- package/dist/src/analyze/select/tui/components/StatusBar.js +6 -0
- package/dist/src/analyze/select/tui/components/TopBar.d.ts +10 -0
- package/dist/src/analyze/select/tui/components/TopBar.js +5 -0
- package/dist/src/analyze/select/tui/hooks/useImmediateInput.d.ts +24 -0
- package/dist/src/analyze/select/tui/hooks/useImmediateInput.js +68 -0
- package/dist/src/analyze/select/tui/hooks/useKeymap.d.ts +24 -0
- package/dist/src/analyze/select/tui/hooks/useKeymap.js +67 -0
- package/dist/src/analyze/select/tui/hooks/useSession.d.ts +19 -0
- package/dist/src/analyze/select/tui/hooks/useSession.js +52 -0
- package/dist/src/analyze/select/tui/hooks/useUndo.d.ts +8 -0
- package/dist/src/analyze/select/tui/hooks/useUndo.js +26 -0
- package/dist/src/analyze/select/types.d.ts +46 -0
- package/dist/src/analyze/select/types.js +20 -0
- package/dist/src/analyze/select-agent/command.d.ts +2 -0
- package/dist/src/analyze/select-agent/command.js +208 -0
- package/dist/src/analyze/tui/AnalyzeView.d.ts +24 -0
- package/dist/src/analyze/tui/AnalyzeView.js +38 -0
- package/dist/src/apply/api-client.d.ts +35 -0
- package/dist/src/apply/api-client.js +143 -0
- package/dist/src/apply/command.d.ts +6 -0
- package/dist/src/apply/command.js +787 -0
- package/dist/src/apply/manifest.d.ts +1 -0
- package/dist/src/apply/manifest.js +1 -0
- package/dist/src/apply/tui/SelectView.d.ts +18 -0
- package/dist/src/apply/tui/SelectView.js +34 -0
- package/dist/src/apply/tui/ServerApplyView.d.ts +32 -0
- package/dist/src/apply/tui/ServerApplyView.js +42 -0
- package/dist/src/apply/tui/ServerPreviewView.d.ts +9 -0
- package/dist/src/apply/tui/ServerPreviewView.js +21 -0
- package/dist/src/credentials-store.d.ts +8 -0
- package/dist/src/credentials-store.js +30 -0
- package/dist/src/generate/agent-runner.d.ts +86 -0
- package/dist/src/generate/agent-runner.js +314 -0
- package/dist/src/generate/command.d.ts +2 -0
- package/dist/src/generate/command.js +545 -0
- package/dist/src/generate/edit/command.d.ts +2 -0
- package/dist/src/generate/edit/command.js +126 -0
- package/dist/src/generate/prompt-builder.d.ts +18 -0
- package/dist/src/generate/prompt-builder.js +202 -0
- package/dist/src/generate/tui/GenerateView.d.ts +12 -0
- package/dist/src/generate/tui/GenerateView.js +10 -0
- package/dist/src/import/command.d.ts +2 -0
- package/dist/src/import/command.js +96 -0
- package/dist/src/import/orchestrator.d.ts +37 -0
- package/dist/src/import/orchestrator.js +374 -0
- package/dist/src/import/path-utils.d.ts +15 -0
- package/dist/src/import/path-utils.js +30 -0
- package/dist/src/import/tui/WizardApp.d.ts +10 -0
- package/dist/src/import/tui/WizardApp.js +906 -0
- package/dist/src/import/tui/steps/CredentialsStep.d.ts +15 -0
- package/dist/src/import/tui/steps/CredentialsStep.js +79 -0
- package/dist/src/import/tui/steps/DoneStep.d.ts +20 -0
- package/dist/src/import/tui/steps/DoneStep.js +17 -0
- package/dist/src/import/tui/steps/ErrorStep.d.ts +8 -0
- package/dist/src/import/tui/steps/ErrorStep.js +11 -0
- package/dist/src/import/tui/steps/GateStep.d.ts +14 -0
- package/dist/src/import/tui/steps/GateStep.js +20 -0
- package/dist/src/import/tui/steps/GenerateReviewStep.d.ts +8 -0
- package/dist/src/import/tui/steps/GenerateReviewStep.js +208 -0
- package/dist/src/import/tui/steps/PathValidationStep.d.ts +10 -0
- package/dist/src/import/tui/steps/PathValidationStep.js +151 -0
- package/dist/src/import/tui/steps/PreviewStep.d.ts +21 -0
- package/dist/src/import/tui/steps/PreviewStep.js +36 -0
- package/dist/src/import/tui/steps/RunningStep.d.ts +10 -0
- package/dist/src/import/tui/steps/RunningStep.js +20 -0
- package/dist/src/import/tui/steps/TokenInputStep.d.ts +8 -0
- package/dist/src/import/tui/steps/TokenInputStep.js +70 -0
- package/dist/src/import/tui/steps/WelcomeStep.d.ts +7 -0
- package/dist/src/import/tui/steps/WelcomeStep.js +33 -0
- package/dist/src/import/tui/steps/WizardPreviewStep.d.ts +15 -0
- package/dist/src/import/tui/steps/WizardPreviewStep.js +121 -0
- package/dist/src/import/tui/steps/preview-diff.d.ts +10 -0
- package/dist/src/import/tui/steps/preview-diff.js +132 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +2 -0
- package/dist/src/output/format.d.ts +23 -0
- package/dist/src/output/format.js +110 -0
- package/dist/src/print/command.d.ts +2 -0
- package/dist/src/print/command.js +199 -0
- package/dist/src/print/validate/tui/ValidateView.d.ts +15 -0
- package/dist/src/print/validate/tui/ValidateView.js +37 -0
- package/dist/src/print/validate/validators/cdf-validator.d.ts +2 -0
- package/dist/src/print/validate/validators/cdf-validator.js +104 -0
- package/dist/src/print/validate/validators/dtcg-validator.d.ts +2 -0
- package/dist/src/print/validate/validators/dtcg-validator.js +110 -0
- package/dist/src/print/validate/validators/format-errors.d.ts +12 -0
- package/dist/src/print/validate/validators/format-errors.js +18 -0
- package/dist/src/program.d.ts +2 -0
- package/dist/src/program.js +25 -0
- package/dist/src/session/command.d.ts +2 -0
- package/dist/src/session/command.js +261 -0
- package/dist/src/session/db.d.ts +111 -0
- package/dist/src/session/db.js +1114 -0
- package/dist/src/session/migration.d.ts +4 -0
- package/dist/src/session/migration.js +117 -0
- package/dist/src/session/session-id.d.ts +1 -0
- package/dist/src/session/session-id.js +212 -0
- package/dist/src/session/stats.d.ts +27 -0
- package/dist/src/session/stats.js +89 -0
- package/dist/src/setup/command.d.ts +2 -0
- package/dist/src/setup/command.js +765 -0
- package/dist/src/types.d.ts +48 -0
- package/dist/src/types.js +1 -0
- package/package.json +55 -0
- package/skills/generate-components.md +361 -0
- package/skills/generate-tokens.md +194 -0
- package/skills/select-components.md +180 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { createElement } from 'react';
|
|
2
|
+
import { render } from 'ink';
|
|
3
|
+
import { mkdir, readdir, stat } from 'node:fs/promises';
|
|
4
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
5
|
+
import { extractComponents } from './extract/pipeline.js';
|
|
6
|
+
import { AnalyzeView } from './tui/AnalyzeView.js';
|
|
7
|
+
import { registerAnalyzeEditCommand } from './select/command.js';
|
|
8
|
+
import { registerAnalyzeSelectAgentCommand } from './select-agent/command.js';
|
|
9
|
+
import { openPipelineDb, getOrCreateSession, createStep, updateStep, storeRawComponents } from '../session/db.js';
|
|
10
|
+
import { preClassifyComponent } from './pre-classify.js';
|
|
11
|
+
const SCANNED_FILE_EXTENSIONS = new Set(['.astro', '.js', '.jsx', '.ts', '.tsx', '.vue']);
|
|
12
|
+
const IGNORED_DIRECTORY_NAMES = new Set([
|
|
13
|
+
'.git',
|
|
14
|
+
'.next',
|
|
15
|
+
'.nuxt',
|
|
16
|
+
'build',
|
|
17
|
+
'coverage',
|
|
18
|
+
'demo',
|
|
19
|
+
'demos',
|
|
20
|
+
'dist',
|
|
21
|
+
'example',
|
|
22
|
+
'examples',
|
|
23
|
+
'node_modules',
|
|
24
|
+
'out',
|
|
25
|
+
'storybook-static',
|
|
26
|
+
]);
|
|
27
|
+
const IGNORED_FILE_SUFFIXES = new Set([
|
|
28
|
+
'.stories.ts',
|
|
29
|
+
'.stories.tsx',
|
|
30
|
+
'.stories.js',
|
|
31
|
+
'.stories.jsx',
|
|
32
|
+
'.story.ts',
|
|
33
|
+
'.story.tsx',
|
|
34
|
+
'.story.js',
|
|
35
|
+
'.story.jsx',
|
|
36
|
+
'.spec.ts',
|
|
37
|
+
'.spec.tsx',
|
|
38
|
+
'.test.ts',
|
|
39
|
+
'.test.tsx',
|
|
40
|
+
]);
|
|
41
|
+
function pluralize(count, singular, plural = `${singular}s`) {
|
|
42
|
+
return `${count} ${count === 1 ? singular : plural}`;
|
|
43
|
+
}
|
|
44
|
+
function resolveFromProjectRoot(projectRoot, inputPath) {
|
|
45
|
+
return isAbsolute(inputPath) ? inputPath : resolve(projectRoot, inputPath);
|
|
46
|
+
}
|
|
47
|
+
async function pathExists(path) {
|
|
48
|
+
return Boolean(await stat(path).catch(() => null));
|
|
49
|
+
}
|
|
50
|
+
export async function collectSourceFiles(directory, onProgress) {
|
|
51
|
+
const files = [];
|
|
52
|
+
async function visit(currentDirectory) {
|
|
53
|
+
const entries = await readdir(currentDirectory, { withFileTypes: true });
|
|
54
|
+
const subdirs = [];
|
|
55
|
+
for (const entry of entries) {
|
|
56
|
+
const fullPath = join(currentDirectory, entry.name);
|
|
57
|
+
if (entry.isDirectory()) {
|
|
58
|
+
if (!IGNORED_DIRECTORY_NAMES.has(entry.name)) {
|
|
59
|
+
subdirs.push(fullPath);
|
|
60
|
+
}
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (!entry.isFile()) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const extension = entry.name.slice(entry.name.lastIndexOf('.'));
|
|
67
|
+
if (!SCANNED_FILE_EXTENSIONS.has(extension) || entry.name.endsWith('.d.ts')) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if ([...IGNORED_FILE_SUFFIXES].some((suffix) => entry.name.endsWith(suffix))) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
files.push(fullPath);
|
|
74
|
+
onProgress?.(files.length);
|
|
75
|
+
}
|
|
76
|
+
await Promise.all(subdirs.map((subdir) => visit(subdir)));
|
|
77
|
+
}
|
|
78
|
+
await visit(directory);
|
|
79
|
+
return files.sort();
|
|
80
|
+
}
|
|
81
|
+
export function registerAnalyzeCommand(program) {
|
|
82
|
+
const analyze = program
|
|
83
|
+
.command('analyze')
|
|
84
|
+
.description('Extract component definitions from a project, or correct analysis output');
|
|
85
|
+
analyze
|
|
86
|
+
.command('extract')
|
|
87
|
+
.description('Extract component definitions from a project')
|
|
88
|
+
.requiredOption('--project <path>', 'Path to the project root')
|
|
89
|
+
.option('--dir <path>', 'Path to the component source directory relative to the project root')
|
|
90
|
+
.action(async (opts) => {
|
|
91
|
+
const projectRoot = resolve(opts.project);
|
|
92
|
+
const outDir = join(projectRoot, '.contentful');
|
|
93
|
+
let sourceDirectory;
|
|
94
|
+
if (opts.dir !== undefined) {
|
|
95
|
+
sourceDirectory = resolveFromProjectRoot(projectRoot, opts.dir);
|
|
96
|
+
if (!(await pathExists(sourceDirectory))) {
|
|
97
|
+
process.stderr.write(`Error: source directory does not exist: ${sourceDirectory}\n`);
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
const srcPath = resolveFromProjectRoot(projectRoot, 'src');
|
|
103
|
+
sourceDirectory = (await pathExists(srcPath)) ? srcPath : projectRoot;
|
|
104
|
+
}
|
|
105
|
+
const sourceFiles = await collectSourceFiles(sourceDirectory, (count) => {
|
|
106
|
+
if (!process.stdout.isTTY) {
|
|
107
|
+
process.stderr.write(`progress=scan:${count}\n`);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
const extraction = await extractComponents(sourceFiles, ({ filesProcessed, componentsFound }) => {
|
|
111
|
+
if (!process.stdout.isTTY) {
|
|
112
|
+
process.stderr.write(`progress=extract:${filesProcessed}/${sourceFiles.length}:${componentsFound}\n`);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
await mkdir(outDir, { recursive: true });
|
|
116
|
+
const db = openPipelineDb();
|
|
117
|
+
const { sessionId } = getOrCreateSession(db, undefined, undefined, {
|
|
118
|
+
command: 'analyze extract',
|
|
119
|
+
inputPath: projectRoot,
|
|
120
|
+
outDir,
|
|
121
|
+
});
|
|
122
|
+
const stepId = createStep(db, sessionId, 'analyze extract', { project: projectRoot });
|
|
123
|
+
const classifiedComponents = extraction.components.map(preClassifyComponent);
|
|
124
|
+
storeRawComponents(db, sessionId, classifiedComponents);
|
|
125
|
+
updateStep(db, stepId, 'complete', { sessionId });
|
|
126
|
+
db.close();
|
|
127
|
+
const zeroPropComponents = classifiedComponents.filter((c) => c.props.length === 0 && c.slots.length === 0);
|
|
128
|
+
const analyzeResult = {
|
|
129
|
+
sourceDirectory,
|
|
130
|
+
sessionId,
|
|
131
|
+
fileCount: sourceFiles.length,
|
|
132
|
+
components: classifiedComponents.map((c) => ({
|
|
133
|
+
name: c.name,
|
|
134
|
+
framework: c.framework,
|
|
135
|
+
propCount: c.props.length,
|
|
136
|
+
slotCount: c.slots.length,
|
|
137
|
+
warnings: extraction.warnings.filter((w) => w.startsWith(c.name + ':')),
|
|
138
|
+
})),
|
|
139
|
+
totalWarnings: extraction.warnings.length,
|
|
140
|
+
zeroPropComponents: zeroPropComponents.map((c) => ({ name: c.name, source: c.source })),
|
|
141
|
+
};
|
|
142
|
+
if (process.stdout.isTTY) {
|
|
143
|
+
const { waitUntilExit } = render(createElement(AnalyzeView, {
|
|
144
|
+
result: analyzeResult,
|
|
145
|
+
onExit: () => process.exit(0),
|
|
146
|
+
}));
|
|
147
|
+
await waitUntilExit();
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
const sessionLine = `session=${sessionId}\n`;
|
|
151
|
+
process.stdout.write(sessionLine);
|
|
152
|
+
const summaryLines = [
|
|
153
|
+
`Scanned ${pluralize(sourceFiles.length, 'source file')} in ${sourceDirectory}`,
|
|
154
|
+
`Extracted ${pluralize(extraction.components.length, 'component')}`,
|
|
155
|
+
];
|
|
156
|
+
if (zeroPropComponents.length > 0) {
|
|
157
|
+
summaryLines.push(`Warning: ${pluralize(zeroPropComponents.length, 'component')} extracted with 0 props and 0 slots:`);
|
|
158
|
+
summaryLines.push(...zeroPropComponents.map((c) => ` ${c.name} (${c.source})`));
|
|
159
|
+
summaryLines.push('These may be Storybook stories, context providers, or SSR utilities.');
|
|
160
|
+
summaryLines.push("Review them in 'analyze select' before generating.");
|
|
161
|
+
}
|
|
162
|
+
if (extraction.warnings.length > 0) {
|
|
163
|
+
summaryLines.push(`Warnings (${extraction.warnings.length}):`);
|
|
164
|
+
summaryLines.push(...extraction.warnings.map((w) => `- ${w}`));
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
summaryLines.push('Warnings: none');
|
|
168
|
+
}
|
|
169
|
+
process.stderr.write(summaryLines.join('\n') + '\n');
|
|
170
|
+
process.exit(0);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
registerAnalyzeEditCommand(analyze);
|
|
174
|
+
registerAnalyzeSelectAgentCommand(analyze);
|
|
175
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { basename } from 'node:path';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { Project, Node } from 'ts-morph';
|
|
5
|
+
function extractAllowedValues(typeText) {
|
|
6
|
+
// Check if the type is a union of string literals like 'a' | 'b' | 'c'
|
|
7
|
+
const parts = typeText.split('|').map((p) => p.trim());
|
|
8
|
+
const literals = parts.filter((p) => /^['"]/.test(p)).map((p) => p.replace(/^['"]|['"]$/g, ''));
|
|
9
|
+
return literals.length >= 2 ? literals.sort() : undefined;
|
|
10
|
+
}
|
|
11
|
+
function usesAstroProps(initializer) {
|
|
12
|
+
if (!initializer)
|
|
13
|
+
return false;
|
|
14
|
+
if (initializer.getText() === 'Astro.props')
|
|
15
|
+
return true;
|
|
16
|
+
let found = false;
|
|
17
|
+
initializer.forEachDescendant((node) => {
|
|
18
|
+
if (found)
|
|
19
|
+
return false;
|
|
20
|
+
if (Node.isPropertyAccessExpression(node) && node.getText() === 'Astro.props') {
|
|
21
|
+
found = true;
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
return undefined;
|
|
25
|
+
});
|
|
26
|
+
return found;
|
|
27
|
+
}
|
|
28
|
+
function extractBindingPropName(element) {
|
|
29
|
+
if (element.getText().startsWith('...'))
|
|
30
|
+
return null;
|
|
31
|
+
return element.getPropertyNameNode()?.getText() ?? element.getNameNode().getText();
|
|
32
|
+
}
|
|
33
|
+
function extractFallbackPropsFromFrontmatter(frontmatter) {
|
|
34
|
+
const project = new Project({
|
|
35
|
+
compilerOptions: {
|
|
36
|
+
strict: false,
|
|
37
|
+
target: 99,
|
|
38
|
+
module: 99,
|
|
39
|
+
allowJs: true,
|
|
40
|
+
},
|
|
41
|
+
useInMemoryFileSystem: true,
|
|
42
|
+
skipAddingFilesFromTsConfig: true,
|
|
43
|
+
});
|
|
44
|
+
const sf = project.createSourceFile('__frontmatter__.ts', frontmatter);
|
|
45
|
+
const props = new Map();
|
|
46
|
+
sf.forEachDescendant((node) => {
|
|
47
|
+
if (!Node.isVariableDeclaration(node))
|
|
48
|
+
return;
|
|
49
|
+
const initializer = node.getInitializer();
|
|
50
|
+
if (!usesAstroProps(initializer))
|
|
51
|
+
return;
|
|
52
|
+
const nameNode = node.getNameNode();
|
|
53
|
+
if (!Node.isObjectBindingPattern(nameNode))
|
|
54
|
+
return;
|
|
55
|
+
for (const element of nameNode.getElements()) {
|
|
56
|
+
const propName = extractBindingPropName(element);
|
|
57
|
+
if (!propName)
|
|
58
|
+
continue;
|
|
59
|
+
props.set(propName, {
|
|
60
|
+
name: propName,
|
|
61
|
+
type: 'any',
|
|
62
|
+
required: !element.getInitializer(),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
return [...props.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
67
|
+
}
|
|
68
|
+
function mergeProps(...propGroups) {
|
|
69
|
+
const merged = new Map();
|
|
70
|
+
for (const props of propGroups) {
|
|
71
|
+
for (const prop of props) {
|
|
72
|
+
const existing = merged.get(prop.name);
|
|
73
|
+
merged.set(prop.name, existing
|
|
74
|
+
? {
|
|
75
|
+
...existing,
|
|
76
|
+
...prop,
|
|
77
|
+
required: existing.required && prop.required,
|
|
78
|
+
}
|
|
79
|
+
: prop);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return [...merged.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
83
|
+
}
|
|
84
|
+
function extractPropsFromFrontmatter(frontmatter) {
|
|
85
|
+
const project = new Project({
|
|
86
|
+
compilerOptions: {
|
|
87
|
+
strict: false,
|
|
88
|
+
target: 99, // ESNext
|
|
89
|
+
module: 99, // ESNext
|
|
90
|
+
allowJs: true,
|
|
91
|
+
},
|
|
92
|
+
useInMemoryFileSystem: true,
|
|
93
|
+
skipAddingFilesFromTsConfig: true,
|
|
94
|
+
});
|
|
95
|
+
const sf = project.createSourceFile('__frontmatter__.ts', frontmatter);
|
|
96
|
+
const props = [];
|
|
97
|
+
// Find `interface Props` or `type Props = ...`
|
|
98
|
+
const propsInterface = sf.getInterface('Props');
|
|
99
|
+
const propsTypeAlias = sf.getTypeAlias('Props');
|
|
100
|
+
if (propsInterface) {
|
|
101
|
+
for (const member of propsInterface.getProperties()) {
|
|
102
|
+
const name = member.getName();
|
|
103
|
+
const typeText = member.getTypeNode()?.getText() ?? 'any';
|
|
104
|
+
const required = !member.hasQuestionToken();
|
|
105
|
+
const allowedValues = extractAllowedValues(typeText);
|
|
106
|
+
props.push({
|
|
107
|
+
name,
|
|
108
|
+
type: typeText,
|
|
109
|
+
required,
|
|
110
|
+
...(allowedValues && { allowedValues }),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
else if (propsTypeAlias) {
|
|
115
|
+
const type = propsTypeAlias.getType();
|
|
116
|
+
for (const property of type.getProperties()) {
|
|
117
|
+
const name = property.getName();
|
|
118
|
+
const decl = property.getValueDeclaration() ?? property.getDeclarations()[0];
|
|
119
|
+
if (!decl)
|
|
120
|
+
continue;
|
|
121
|
+
const propType = property.getTypeAtLocation(decl);
|
|
122
|
+
const typeText = propType.getText(decl);
|
|
123
|
+
const required = !property.isOptional();
|
|
124
|
+
const allowedValues = extractAllowedValues(typeText);
|
|
125
|
+
props.push({
|
|
126
|
+
name,
|
|
127
|
+
type: typeText,
|
|
128
|
+
required,
|
|
129
|
+
...(allowedValues && { allowedValues }),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return props.sort((a, b) => a.name.localeCompare(b.name));
|
|
134
|
+
}
|
|
135
|
+
function extractDefaultsFromFrontmatter(frontmatter) {
|
|
136
|
+
const defaults = new Map();
|
|
137
|
+
const project = new Project({
|
|
138
|
+
compilerOptions: {
|
|
139
|
+
strict: false,
|
|
140
|
+
target: 99,
|
|
141
|
+
module: 99,
|
|
142
|
+
allowJs: true,
|
|
143
|
+
},
|
|
144
|
+
useInMemoryFileSystem: true,
|
|
145
|
+
skipAddingFilesFromTsConfig: true,
|
|
146
|
+
});
|
|
147
|
+
const sf = project.createSourceFile('__frontmatter__.ts', frontmatter);
|
|
148
|
+
sf.forEachDescendant((node) => {
|
|
149
|
+
if (!Node.isVariableDeclaration(node))
|
|
150
|
+
return;
|
|
151
|
+
const initializer = node.getInitializer();
|
|
152
|
+
if (!initializer)
|
|
153
|
+
return;
|
|
154
|
+
if (!usesAstroProps(initializer))
|
|
155
|
+
return;
|
|
156
|
+
const nameNode = node.getNameNode();
|
|
157
|
+
if (!Node.isObjectBindingPattern(nameNode))
|
|
158
|
+
return;
|
|
159
|
+
for (const element of nameNode.getElements()) {
|
|
160
|
+
const propName = extractBindingPropName(element);
|
|
161
|
+
if (!propName)
|
|
162
|
+
continue;
|
|
163
|
+
const elementInitializer = element.getInitializer();
|
|
164
|
+
if (!elementInitializer)
|
|
165
|
+
continue;
|
|
166
|
+
const value = elementInitializer.getText().replace(/^['"]|['"]$/g, '');
|
|
167
|
+
defaults.set(propName, value);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
return defaults;
|
|
171
|
+
}
|
|
172
|
+
function extractSlotsFromTemplate(template) {
|
|
173
|
+
const slots = [];
|
|
174
|
+
const seen = new Set();
|
|
175
|
+
const slotRegex = /<slot(?:\s+name=["']([^"']+)["'])?\s*\/?>/g;
|
|
176
|
+
let match;
|
|
177
|
+
while ((match = slotRegex.exec(template)) !== null) {
|
|
178
|
+
const slotName = match[1] ?? 'default';
|
|
179
|
+
if (!seen.has(slotName)) {
|
|
180
|
+
seen.add(slotName);
|
|
181
|
+
slots.push({ name: slotName, isDefault: slotName === 'default' });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return slots;
|
|
185
|
+
}
|
|
186
|
+
function extractSlotsFromFrontmatter(frontmatter) {
|
|
187
|
+
const slots = [];
|
|
188
|
+
const seen = new Set();
|
|
189
|
+
const slotRenderRegex = /Astro\.slots\.render\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
190
|
+
let match;
|
|
191
|
+
while ((match = slotRenderRegex.exec(frontmatter)) !== null) {
|
|
192
|
+
const slotName = match[1];
|
|
193
|
+
if (!seen.has(slotName)) {
|
|
194
|
+
seen.add(slotName);
|
|
195
|
+
slots.push({ name: slotName, isDefault: slotName === 'default' });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return slots;
|
|
199
|
+
}
|
|
200
|
+
function mergeSlots(...slotGroups) {
|
|
201
|
+
const merged = [];
|
|
202
|
+
const seen = new Set();
|
|
203
|
+
for (const slots of slotGroups) {
|
|
204
|
+
for (const slot of slots) {
|
|
205
|
+
if (seen.has(slot.name))
|
|
206
|
+
continue;
|
|
207
|
+
seen.add(slot.name);
|
|
208
|
+
merged.push(slot);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return merged;
|
|
212
|
+
}
|
|
213
|
+
function extractFromAstroFile(filePath, source) {
|
|
214
|
+
const name = basename(filePath, '.astro');
|
|
215
|
+
// Split on `---` fences: frontmatter is between first and second `---`
|
|
216
|
+
// If there is no `---`, the entire file is a template-only component
|
|
217
|
+
const fenceIndex = source.startsWith('---') ? 0 : -1;
|
|
218
|
+
let frontmatter = '';
|
|
219
|
+
let template = source;
|
|
220
|
+
if (fenceIndex !== -1) {
|
|
221
|
+
const endFenceIndex = source.indexOf('---', fenceIndex + 3);
|
|
222
|
+
if (endFenceIndex !== -1) {
|
|
223
|
+
frontmatter = source.slice(fenceIndex + 3, endFenceIndex);
|
|
224
|
+
template = source.slice(endFenceIndex + 3);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
const props = frontmatter
|
|
228
|
+
? mergeProps(extractFallbackPropsFromFrontmatter(frontmatter), extractPropsFromFrontmatter(frontmatter))
|
|
229
|
+
: [];
|
|
230
|
+
const defaults = frontmatter ? extractDefaultsFromFrontmatter(frontmatter) : new Map();
|
|
231
|
+
const propsWithDefaults = props.map((p) => {
|
|
232
|
+
const defaultValue = defaults.get(p.name);
|
|
233
|
+
return defaultValue ? { ...p, defaultValue } : p;
|
|
234
|
+
});
|
|
235
|
+
const slots = mergeSlots(extractSlotsFromTemplate(template), extractSlotsFromFrontmatter(frontmatter));
|
|
236
|
+
return {
|
|
237
|
+
name,
|
|
238
|
+
source: filePath,
|
|
239
|
+
framework: 'astro',
|
|
240
|
+
props: propsWithDefaults,
|
|
241
|
+
slots,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
const ASTRO_EXTRACT_CONCURRENCY = Number(process.env['EDS_EXTRACT_CONCURRENCY'] ?? 0) || os.cpus().length;
|
|
245
|
+
export async function extractAstroComponents(filePaths, onProgress) {
|
|
246
|
+
const astroFiles = filePaths.filter((f) => f.endsWith('.astro'));
|
|
247
|
+
if (astroFiles.length === 0) {
|
|
248
|
+
return { components: [], warnings: [] };
|
|
249
|
+
}
|
|
250
|
+
const warnings = [];
|
|
251
|
+
const components = [];
|
|
252
|
+
let filesProcessed = 0;
|
|
253
|
+
let componentsFound = 0;
|
|
254
|
+
const queue = [...astroFiles];
|
|
255
|
+
async function worker() {
|
|
256
|
+
while (queue.length > 0) {
|
|
257
|
+
const filePath = queue.shift();
|
|
258
|
+
if (!filePath)
|
|
259
|
+
break;
|
|
260
|
+
try {
|
|
261
|
+
const source = await readFile(filePath, 'utf-8');
|
|
262
|
+
const component = extractFromAstroFile(filePath, source);
|
|
263
|
+
if (component) {
|
|
264
|
+
components.push(component);
|
|
265
|
+
componentsFound++;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
catch (e) {
|
|
269
|
+
warnings.push(`Failed to extract from ${filePath}: ${e instanceof Error ? e.message : String(e)}`);
|
|
270
|
+
}
|
|
271
|
+
filesProcessed++;
|
|
272
|
+
onProgress?.({ filesProcessed, componentsFound });
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
await Promise.all(Array.from({ length: Math.min(ASTRO_EXTRACT_CONCURRENCY, astroFiles.length) }, worker));
|
|
276
|
+
return {
|
|
277
|
+
components: components.sort((a, b) => a.name.localeCompare(b.name)),
|
|
278
|
+
warnings,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ComponentExtractionResult } from '../../types.js';
|
|
2
|
+
export type ExtractProgress = {
|
|
3
|
+
filesProcessed: number;
|
|
4
|
+
componentsFound: number;
|
|
5
|
+
};
|
|
6
|
+
export declare function extractComponents(filePaths: string[], onProgress?: (progress: ExtractProgress) => void): Promise<ComponentExtractionResult>;
|