@developer_tribe/react-builder 0.1.4 → 0.1.8
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/build-components/Button/Button.d.ts +1 -5
- package/dist/build-components/Button/ButtonProps.generated.d.ts +4 -0
- package/dist/build-components/Carousel/Carousel.d.ts +1 -5
- package/dist/build-components/Carousel/CarouselProps.generated.d.ts +4 -0
- package/dist/build-components/CarouselButtons/CarouselButtons.d.ts +1 -5
- package/dist/build-components/CarouselButtons/CarouselButtonsProps.generated.d.ts +4 -0
- package/dist/build-components/CarouselDots/CarouselDots.d.ts +1 -5
- package/dist/build-components/CarouselDots/CarouselDotsProps.generated.d.ts +4 -0
- package/dist/build-components/CarouselItem/CarouselItem.d.ts +1 -5
- package/dist/build-components/CarouselItem/CarouselItemProps.generated.d.ts +4 -0
- package/dist/build-components/CarouselProvider/CarouselProvider.d.ts +1 -5
- package/dist/build-components/CarouselProvider/CarouselProviderProps.generated.d.ts +4 -0
- package/dist/build-components/Image/Image.d.ts +1 -5
- package/dist/build-components/Image/ImageProps.generated.d.ts +4 -0
- package/dist/build-components/Onboard/Onboard.d.ts +1 -5
- package/dist/build-components/Onboard/OnboardProps.generated.d.ts +4 -0
- package/dist/build-components/OnboardBoardTitle/OnboardBoardTitle.d.ts +1 -5
- package/dist/build-components/OnboardBoardTitle/OnboardBoardTitleProps.generated.d.ts +4 -0
- package/dist/build-components/OnboardButton/OnboardButton.d.ts +1 -5
- package/dist/build-components/OnboardButton/OnboardButtonProps.generated.d.ts +4 -0
- package/dist/build-components/OnboardButtons/OnboardButtons.d.ts +1 -5
- package/dist/build-components/OnboardButtons/OnboardButtonsProps.generated.d.ts +4 -0
- package/dist/build-components/OnboardDot/OnboardDot.d.ts +5 -0
- package/dist/build-components/OnboardDot/OnboardDotProps.generated.d.ts +10 -0
- package/dist/build-components/OnboardDot/OnboardExpandingDotProps.generated.d.ts +10 -0
- package/dist/build-components/OnboardFooter/OnboardFooter.d.ts +1 -5
- package/dist/build-components/OnboardFooter/OnboardFooterProps.generated.d.ts +4 -0
- package/dist/build-components/OnboardImage/OnboardImage.d.ts +1 -5
- package/dist/build-components/OnboardImage/OnboardImageProps.generated.d.ts +4 -0
- package/dist/build-components/OnboardItem/OnboardItem.d.ts +1 -5
- package/dist/build-components/OnboardItem/OnboardItemProps.generated.d.ts +4 -0
- package/dist/build-components/OnboardProvider/OnboardProvider.d.ts +1 -5
- package/dist/build-components/OnboardProvider/OnboardProviderProps.generated.d.ts +4 -0
- package/dist/build-components/OnboardSubtitle/OnboardSubtitle.d.ts +1 -5
- package/dist/build-components/OnboardSubtitle/OnboardSubtitleProps.generated.d.ts +4 -0
- package/dist/build-components/Text/Text.d.ts +1 -5
- package/dist/build-components/Text/TextProps.generated.d.ts +4 -0
- package/dist/build-components/View/View.d.ts +1 -5
- package/dist/build-components/View/ViewProps.generated.d.ts +4 -0
- package/dist/build-components/index.d.ts +19 -0
- package/dist/index.cjs.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.esm.js +1 -1
- package/package.json +4 -3
- package/scripts/prebuild/build-components.js +20 -502
- package/scripts/prebuild/utils/createBuildComponentsIndex.js +24 -0
- package/scripts/prebuild/utils/createComponentTsx.js +21 -0
- package/scripts/prebuild/utils/createGeneratedProps.js +65 -0
- package/scripts/prebuild/utils/createRenderNodeGenerated.js +69 -0
- package/scripts/prebuild/utils/ensureDir.js +5 -0
- package/scripts/prebuild/utils/ensurePropsTs.js +15 -0
- package/scripts/prebuild/utils/fail.js +7 -0
- package/scripts/prebuild/utils/formatAllSourceFiles.js +39 -0
- package/scripts/prebuild/utils/formatWithPrettier.js +10 -0
- package/scripts/prebuild/utils/index.js +15 -0
- package/scripts/prebuild/utils/lintNonGeneratedOrThrow.js +27 -0
- package/scripts/prebuild/utils/validateAllComponentsOrThrow.js +156 -0
- package/scripts/prebuild/utils/validateExistingComponentTsx.js +40 -0
- package/scripts/prebuild/utils/validatePatternJson.js +77 -0
- package/scripts/public/bin.js +1 -6
- package/scripts/public/scripts/build/index.js +15 -4
- package/scripts/public/scripts/build/info.json +6 -0
- package/src/build-components/Button/Button.tsx +1 -6
- package/src/build-components/Button/ButtonProps.generated.ts +6 -0
- package/src/build-components/Carousel/Carousel.tsx +1 -6
- package/src/build-components/Carousel/CarouselProps.generated.ts +6 -0
- package/src/build-components/CarouselButtons/CarouselButtons.tsx +1 -6
- package/src/build-components/CarouselButtons/CarouselButtonsProps.generated.ts +6 -0
- package/src/build-components/CarouselDots/CarouselDots.tsx +1 -6
- package/src/build-components/CarouselDots/CarouselDotsProps.generated.ts +6 -0
- package/src/build-components/CarouselItem/CarouselItem.tsx +1 -6
- package/src/build-components/CarouselItem/CarouselItemProps.generated.ts +6 -0
- package/src/build-components/CarouselProvider/CarouselProvider.tsx +1 -5
- package/src/build-components/CarouselProvider/CarouselProviderProps.generated.ts +6 -0
- package/src/build-components/Image/Image.tsx +1 -6
- package/src/build-components/Image/ImageProps.generated.ts +6 -0
- package/src/build-components/Onboard/Onboard.tsx +1 -6
- package/src/build-components/Onboard/OnboardProps.generated.ts +6 -0
- package/src/build-components/OnboardBoardTitle/OnboardBoardTitle.tsx +1 -6
- package/src/build-components/OnboardBoardTitle/OnboardBoardTitleProps.generated.ts +6 -0
- package/src/build-components/OnboardButton/OnboardButton.tsx +1 -6
- package/src/build-components/OnboardButton/OnboardButtonProps.generated.ts +6 -0
- package/src/build-components/OnboardButtons/OnboardButtons.tsx +2 -6
- package/src/build-components/OnboardButtons/OnboardButtonsProps.generated.ts +6 -0
- package/src/build-components/OnboardDot/OnboardDot.tsx +9 -0
- package/src/build-components/OnboardDot/OnboardDotProps.generated.ts +20 -0
- package/src/build-components/{OnboardExpandingDot → OnboardDot}/OnboardExpandingDotProps.generated.ts +7 -1
- package/src/build-components/{OnboardExpandingDot → OnboardDot}/pattern.json +1 -1
- package/src/build-components/OnboardFooter/OnboardFooter.tsx +1 -6
- package/src/build-components/OnboardFooter/OnboardFooterProps.generated.ts +6 -0
- package/src/build-components/OnboardImage/OnboardImage.tsx +1 -6
- package/src/build-components/OnboardImage/OnboardImageProps.generated.ts +6 -0
- package/src/build-components/OnboardItem/OnboardItem.tsx +1 -5
- package/src/build-components/OnboardItem/OnboardItemProps.generated.ts +6 -0
- package/src/build-components/OnboardProvider/OnboardProvider.tsx +1 -5
- package/src/build-components/OnboardProvider/OnboardProviderProps.generated.ts +6 -0
- package/src/build-components/OnboardSubtitle/OnboardSubtitle.tsx +1 -6
- package/src/build-components/OnboardSubtitle/OnboardSubtitleProps.generated.ts +6 -0
- package/src/build-components/RenderNode.generated.tsx +3 -3
- package/src/build-components/Text/Text.tsx +1 -6
- package/src/build-components/Text/TextProps.generated.ts +6 -0
- package/src/build-components/View/View.tsx +5 -6
- package/src/build-components/View/ViewProps.generated.ts +6 -0
- package/src/build-components/index.ts +78 -0
- package/src/index.ts +1 -0
- package/src/utils/novaToJson.ts +1 -1
- package/src/utils/patterns.ts +1 -1
- package/dist/build-components/OnboardExpandingDot/OnboardExpandingDot.d.ts +0 -9
- package/dist/build-components/OnboardExpandingDot/OnboardExpandingDotProps.generated.d.ts +0 -6
- package/src/build-components/OnboardExpandingDot/OnboardExpandingDot.tsx +0 -14
|
@@ -1,528 +1,46 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Pre-build component generator for files under `src/build-components`
|
|
3
|
-
//
|
|
3
|
+
// Orchestrator only; implementation lives in `./utils/functions.js`.
|
|
4
4
|
//TODO: memo needs optimization
|
|
5
|
-
import { promises as fs } from 'fs';
|
|
6
5
|
import path from 'path';
|
|
7
6
|
import url from 'url';
|
|
8
|
-
|
|
9
|
-
import {
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
validateAllComponentsOrThrow,
|
|
10
|
+
createGeneratedProps,
|
|
11
|
+
ensurePropsTs,
|
|
12
|
+
createComponentTsx,
|
|
13
|
+
validateExistingComponentTsx,
|
|
14
|
+
createRenderNodeGenerated,
|
|
15
|
+
createBuildComponentsIndex,
|
|
16
|
+
formatAllSourceFiles,
|
|
17
|
+
// lintNonGeneratedOrThrow,
|
|
18
|
+
} from './utils/index.js';
|
|
10
19
|
|
|
11
20
|
const __filename = url.fileURLToPath(import.meta.url);
|
|
12
21
|
const __dirname = path.dirname(__filename);
|
|
13
22
|
|
|
14
|
-
const PROJECT_ROOT = path.resolve(__dirname, '
|
|
23
|
+
const PROJECT_ROOT = path.resolve(__dirname, '../..');
|
|
15
24
|
const SRC_ROOT = path.join(PROJECT_ROOT, 'src');
|
|
16
25
|
const COMPONENTS_ROOT = path.join(SRC_ROOT, 'build-components');
|
|
17
26
|
const PATTERNS_ROOT = path.join(SRC_ROOT, 'patterns');
|
|
18
27
|
|
|
19
|
-
/**
|
|
20
|
-
* Utility: read JSON with helpful error messages
|
|
21
|
-
*/
|
|
22
|
-
async function readJson(filePath) {
|
|
23
|
-
try {
|
|
24
|
-
const content = await fs.readFile(filePath, 'utf8');
|
|
25
|
-
return JSON.parse(content);
|
|
26
|
-
} catch (error) {
|
|
27
|
-
const reason =
|
|
28
|
-
error instanceof SyntaxError
|
|
29
|
-
? 'Invalid JSON'
|
|
30
|
-
: error.code || error.message;
|
|
31
|
-
return fail(`Failed to read JSON at ${filePath}: ${reason}`);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Utility: ensure directory exists
|
|
37
|
-
*/
|
|
38
|
-
async function ensureDir(dirPath) {
|
|
39
|
-
await fs.mkdir(dirPath, { recursive: true });
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Helper: log the error for readability and then throw
|
|
44
|
-
*/
|
|
45
|
-
function fail(message) {
|
|
46
|
-
console.error(message);
|
|
47
|
-
const err = new Error(message);
|
|
48
|
-
// eslint-disable-next-line no-underscore-dangle
|
|
49
|
-
err._alreadyLogged = true;
|
|
50
|
-
throw err;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Prettier helper
|
|
55
|
-
*/
|
|
56
|
-
async function formatWithPrettier(tempFileText) {
|
|
57
|
-
return prettier.format(tempFileText, {
|
|
58
|
-
parser: 'typescript',
|
|
59
|
-
semi: true,
|
|
60
|
-
singleQuote: true,
|
|
61
|
-
tabWidth: 2,
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* ESLint validation for non-generated files under src
|
|
67
|
-
* Fails if any non-generated TS/TSX file has errors
|
|
68
|
-
*/
|
|
69
|
-
async function lintNonGeneratedOrThrow() {
|
|
70
|
-
const eslint = new ESLint({ cwd: PROJECT_ROOT });
|
|
71
|
-
const results = await eslint.lintFiles(['src/**/*.ts', 'src/**/*.tsx']);
|
|
72
|
-
|
|
73
|
-
const isGenerated = filePath =>
|
|
74
|
-
filePath.endsWith('.generated.ts') ||
|
|
75
|
-
filePath.endsWith('RenderNode.generated.tsx');
|
|
76
|
-
|
|
77
|
-
const errorResults = results.filter(
|
|
78
|
-
r => !isGenerated(r.filePath) && r.errorCount > 0
|
|
79
|
-
);
|
|
80
|
-
|
|
81
|
-
if (errorResults.length > 0) {
|
|
82
|
-
const summary = errorResults
|
|
83
|
-
.map(
|
|
84
|
-
r =>
|
|
85
|
-
`${path.relative(PROJECT_ROOT, r.filePath)}: ${r.errorCount} error(s)`
|
|
86
|
-
) // keep concise
|
|
87
|
-
.join('\n');
|
|
88
|
-
return fail(`ESLint failed on non-generated files:\n${summary}`);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// ===== Ensuring & Validation =====
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* 1) Get all files in the component root
|
|
96
|
-
*/
|
|
97
|
-
async function getAllEntriesInComponentsRoot() {
|
|
98
|
-
const exists = await fs
|
|
99
|
-
.stat(COMPONENTS_ROOT)
|
|
100
|
-
.then(() => true)
|
|
101
|
-
.catch(() => false);
|
|
102
|
-
if (!exists) {
|
|
103
|
-
return fail(`Components root not found at ${COMPONENTS_ROOT}`);
|
|
104
|
-
}
|
|
105
|
-
return await fs
|
|
106
|
-
.readdir(COMPONENTS_ROOT, { withFileTypes: true })
|
|
107
|
-
.then(dirents => {
|
|
108
|
-
return dirents.filter(
|
|
109
|
-
d => d.name !== 'RenderNode.generated.tsx' && d.name !== 'other.ts'
|
|
110
|
-
);
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* 2) Make sure all entries are folders (throw error with reason)
|
|
116
|
-
*/
|
|
117
|
-
function ensureAllEntriesAreFolders(dirents) {
|
|
118
|
-
const notDirs = dirents.filter(d => !d.isDirectory()).map(d => d.name);
|
|
119
|
-
if (notDirs.length > 0) {
|
|
120
|
-
return fail(
|
|
121
|
-
`All entries in ${COMPONENTS_ROOT} must be directories. Non-directories found: ${notDirs.join(', ')}`
|
|
122
|
-
);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* 3) Validate pattern.json has correct props
|
|
128
|
-
* Required structure:
|
|
129
|
-
* - schemaVersion: number
|
|
130
|
-
* - allowUnknownAttributes: boolean
|
|
131
|
-
* - pattern: {
|
|
132
|
-
* type: string
|
|
133
|
-
* children: string
|
|
134
|
-
* attributes: Record<string, 'string' | 'number' | string[]>
|
|
135
|
-
* }
|
|
136
|
-
*/
|
|
137
|
-
async function validatePatternJson(componentDir, componentName) {
|
|
138
|
-
const patternPath = path.join(componentDir, 'pattern.json');
|
|
139
|
-
const exists = await fs
|
|
140
|
-
.stat(patternPath)
|
|
141
|
-
.then(() => true)
|
|
142
|
-
.catch(() => false);
|
|
143
|
-
if (!exists) {
|
|
144
|
-
return fail(
|
|
145
|
-
`Missing pattern.json for component ${componentName} at ${patternPath}`
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
const data = await readJson(patternPath);
|
|
149
|
-
|
|
150
|
-
if (typeof data.schemaVersion !== 'number') {
|
|
151
|
-
return fail(
|
|
152
|
-
`[${componentName}] pattern.json -> 'schemaVersion' must be a number`
|
|
153
|
-
);
|
|
154
|
-
}
|
|
155
|
-
if (typeof data.allowUnknownAttributes !== 'boolean') {
|
|
156
|
-
return fail(
|
|
157
|
-
`[${componentName}] pattern.json -> 'allowUnknownAttributes' must be a boolean`
|
|
158
|
-
);
|
|
159
|
-
}
|
|
160
|
-
if (typeof data.pattern !== 'object' || data.pattern == null) {
|
|
161
|
-
return fail(
|
|
162
|
-
`[${componentName}] pattern.json -> 'pattern' must be an object`
|
|
163
|
-
);
|
|
164
|
-
}
|
|
165
|
-
const { pattern } = data;
|
|
166
|
-
if (typeof pattern.type !== 'string') {
|
|
167
|
-
return fail(
|
|
168
|
-
`[${componentName}] pattern.json -> 'pattern.type' must be a string`
|
|
169
|
-
);
|
|
170
|
-
}
|
|
171
|
-
if (
|
|
172
|
-
typeof pattern.children !== 'string' &&
|
|
173
|
-
!Array.isArray(pattern.children)
|
|
174
|
-
) {
|
|
175
|
-
return fail(
|
|
176
|
-
`[${componentName}] pattern.json -> 'pattern.children' must be a string or an array`
|
|
177
|
-
);
|
|
178
|
-
}
|
|
179
|
-
if (typeof pattern.attributes !== 'object' || pattern.attributes == null) {
|
|
180
|
-
return fail(
|
|
181
|
-
`[${componentName}] pattern.json -> 'pattern.attributes' must be an object`
|
|
182
|
-
);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
for (const [attrName, attrType] of Object.entries(pattern.attributes)) {
|
|
186
|
-
const isValidType =
|
|
187
|
-
typeof attrType === 'string' &&
|
|
188
|
-
(attrType === 'string' || attrType === 'number' || attrType === 'boolean')
|
|
189
|
-
? true
|
|
190
|
-
: Array.isArray(attrType) && attrType.every(v => typeof v === 'string');
|
|
191
|
-
if (!isValidType) {
|
|
192
|
-
return fail(
|
|
193
|
-
`[${componentName}] pattern.json -> 'pattern.attributes.${attrName}' must be 'string' | 'number' | 'boolean' | string[]`
|
|
194
|
-
);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
return data;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Helper to derive TS type from attribute type specifier
|
|
203
|
-
*/
|
|
204
|
-
// (Moved to Actions section) tsTypeFromAttributeType
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* 4) Create <Name>Props.generated.ts (child, attributes) inside the component folder
|
|
208
|
-
*/
|
|
209
|
-
// (Moved to Actions section) createGeneratedProps
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* 5) Create <Name>Props.ts if it doesn't exist inside the component folder. Must export interface extending Generated.
|
|
213
|
-
*/
|
|
214
|
-
// (Moved to Actions section) ensurePropsTs
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* 6) Custom <Name>Props.ts are deprecated; validation removed.
|
|
218
|
-
*/
|
|
219
|
-
// (intentionally left blank)
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* 7) Create <Name>.tsx inside its component folder
|
|
223
|
-
* Should return "hello world" and default export React.memo(Name)
|
|
224
|
-
*/
|
|
225
|
-
// (Moved to Actions section) createComponentTsx
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* 8) If component file exists, check name, memo exists, export default memo
|
|
229
|
-
*/
|
|
230
|
-
async function validateExistingComponentTsx(componentDir, componentName) {
|
|
231
|
-
const filePath = path.join(componentDir, `${componentName}.tsx`);
|
|
232
|
-
const exists = await fs
|
|
233
|
-
.stat(filePath)
|
|
234
|
-
.then(() => true)
|
|
235
|
-
.catch(() => false);
|
|
236
|
-
if (!exists) return; // Created in step 7 if missing
|
|
237
|
-
|
|
238
|
-
const content = await fs.readFile(filePath, 'utf8');
|
|
239
|
-
const hasConstDeclaration = new RegExp(`const\\s+${componentName}\\s*:`).test(
|
|
240
|
-
content
|
|
241
|
-
);
|
|
242
|
-
const hasFunctionDeclaration = new RegExp(
|
|
243
|
-
`function\\s+${componentName}\\s*\\(`
|
|
244
|
-
).test(content);
|
|
245
|
-
const hasDeclaration = hasConstDeclaration || hasFunctionDeclaration;
|
|
246
|
-
const usesMemo = /React\.memo\(/.test(content);
|
|
247
|
-
const defaultExportMemo = new RegExp(
|
|
248
|
-
`export\\s+default\\s+React\\.memo\\(\\s*${componentName}\\s*\\)\\s*;?`
|
|
249
|
-
).test(content);
|
|
250
|
-
|
|
251
|
-
if (!hasDeclaration) {
|
|
252
|
-
return fail(
|
|
253
|
-
`${filePath} must declare component as 'const ${componentName}: React.FC = (...)' or 'function ${componentName}()'`
|
|
254
|
-
);
|
|
255
|
-
}
|
|
256
|
-
if (!usesMemo) {
|
|
257
|
-
return fail(`${filePath} must use React.memo`);
|
|
258
|
-
}
|
|
259
|
-
if (!defaultExportMemo) {
|
|
260
|
-
return fail(`${filePath} must default export React.memo(${componentName})`);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
/**
|
|
265
|
-
* Validation aggregator per component: run all validations without creating files
|
|
266
|
-
* Returns the patternJson to be reused by actions
|
|
267
|
-
*/
|
|
268
|
-
async function validateComponent(componentDir, componentName) {
|
|
269
|
-
const patternJson = await validatePatternJson(componentDir, componentName);
|
|
270
|
-
await validateExistingComponentTsx(componentDir, componentName);
|
|
271
|
-
return { componentDir, componentName, patternJson };
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* Validate all components first; if any fails, throw and skip all actions
|
|
276
|
-
*/
|
|
277
|
-
async function validateAllComponentsOrThrow() {
|
|
278
|
-
const dirents = await getAllEntriesInComponentsRoot();
|
|
279
|
-
ensureAllEntriesAreFolders(dirents);
|
|
280
|
-
|
|
281
|
-
const validated = [];
|
|
282
|
-
for (const dirent of dirents) {
|
|
283
|
-
const componentName = dirent.name;
|
|
284
|
-
const componentDir = path.join(COMPONENTS_ROOT, componentName);
|
|
285
|
-
const result = await validateComponent(componentDir, componentName);
|
|
286
|
-
validated.push(result);
|
|
287
|
-
}
|
|
288
|
-
// Ensure fallback renderer exists for unknown component types
|
|
289
|
-
await ensureOtherTsExists();
|
|
290
|
-
return validated;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
/**
|
|
294
|
-
* Ensure src/build-components/other.ts exists. Create with default implementation if missing.
|
|
295
|
-
*/
|
|
296
|
-
async function ensureOtherTsExists() {
|
|
297
|
-
const otherPath = path.join(COMPONENTS_ROOT, 'other.ts');
|
|
298
|
-
const exists = await fs
|
|
299
|
-
.stat(otherPath)
|
|
300
|
-
.then(() => true)
|
|
301
|
-
.catch(() => false);
|
|
302
|
-
if (exists) return;
|
|
303
|
-
|
|
304
|
-
const content =
|
|
305
|
-
`import React from 'react';\n` +
|
|
306
|
-
`import type { Node } from '../types/Node';\n\n` +
|
|
307
|
-
`export function other(type: string, node: Node): React.ReactNode {\n` +
|
|
308
|
-
` return null;\n` +
|
|
309
|
-
`}\n`;
|
|
310
|
-
const formatted = await formatWithPrettier(content);
|
|
311
|
-
await fs.writeFile(otherPath, formatted, 'utf8');
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// ===== Actions & Generators =====
|
|
315
|
-
|
|
316
|
-
/** Helper to derive TS type from attribute type specifier */
|
|
317
|
-
function tsTypeFromAttributeType(attrType) {
|
|
318
|
-
if (attrType === 'string') return 'string';
|
|
319
|
-
if (attrType === 'number') return 'number';
|
|
320
|
-
if (attrType === 'boolean') return 'boolean';
|
|
321
|
-
if (Array.isArray(attrType)) {
|
|
322
|
-
const literals = attrType.map(v => JSON.stringify(v)).join(' | ');
|
|
323
|
-
return literals.length > 0 ? literals : 'string';
|
|
324
|
-
}
|
|
325
|
-
// Fallback
|
|
326
|
-
return 'string';
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
/** Create <Name>Props.generated.ts (child, attributes) inside the component folder */
|
|
330
|
-
async function createGeneratedProps(componentDir, componentName, patternJson) {
|
|
331
|
-
await ensureDir(componentDir);
|
|
332
|
-
const fileName = `${componentName}Props.generated.ts`;
|
|
333
|
-
const filePath = path.join(componentDir, fileName);
|
|
334
|
-
|
|
335
|
-
const { pattern, allowUnknownAttributes } = patternJson;
|
|
336
|
-
const attributes = pattern.attributes || {};
|
|
337
|
-
const attributeLines = Object.entries(attributes).map(([key, t]) => {
|
|
338
|
-
const tsType = tsTypeFromAttributeType(t);
|
|
339
|
-
return ` ${key}?: ${tsType};`;
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
const indexSignature = allowUnknownAttributes
|
|
343
|
-
? ' [key: string]: string | number | boolean | undefined;\n'
|
|
344
|
-
: '';
|
|
345
|
-
|
|
346
|
-
const childTsType =
|
|
347
|
-
typeof pattern.children === 'string' ? pattern.children : 'string';
|
|
348
|
-
const normalizedChildTsType = ['string', 'number', 'boolean'].includes(
|
|
349
|
-
childTsType
|
|
350
|
-
)
|
|
351
|
-
? childTsType
|
|
352
|
-
: 'string';
|
|
353
|
-
|
|
354
|
-
const fileContent =
|
|
355
|
-
`/* AUTO-GENERATED FILE - DO NOT EDIT */\n` +
|
|
356
|
-
`\n` +
|
|
357
|
-
`export interface ${componentName}PropsGenerated {\n` +
|
|
358
|
-
` child: ${normalizedChildTsType};\n` +
|
|
359
|
-
` attributes: {\n` +
|
|
360
|
-
(attributeLines.length ? attributeLines.join('\n') + '\n' : '') +
|
|
361
|
-
indexSignature +
|
|
362
|
-
` };\n` +
|
|
363
|
-
`}\n`;
|
|
364
|
-
|
|
365
|
-
const formatted = await formatWithPrettier(fileContent);
|
|
366
|
-
await fs.writeFile(filePath, formatted, 'utf8');
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
/** Delete <Name>Props.ts if it exists inside the component folder */
|
|
370
|
-
async function ensurePropsTs(componentDir, componentName) {
|
|
371
|
-
await ensureDir(componentDir);
|
|
372
|
-
const fileName = `${componentName}Props.ts`;
|
|
373
|
-
const filePath = path.join(componentDir, fileName);
|
|
374
|
-
const exists = await fs
|
|
375
|
-
.stat(filePath)
|
|
376
|
-
.then(() => true)
|
|
377
|
-
.catch(() => false);
|
|
378
|
-
if (!exists) return;
|
|
379
|
-
await fs.unlink(filePath);
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
/** Create <Name>.tsx inside its component folder */
|
|
383
|
-
async function createComponentTsx(componentDir, componentName) {
|
|
384
|
-
const filePath = path.join(componentDir, `${componentName}.tsx`);
|
|
385
|
-
const exists = await fs
|
|
386
|
-
.stat(filePath)
|
|
387
|
-
.then(() => true)
|
|
388
|
-
.catch(() => false);
|
|
389
|
-
if (exists) return; // Will be validated in step 8
|
|
390
|
-
|
|
391
|
-
const content =
|
|
392
|
-
`import React from 'react';\n\n` +
|
|
393
|
-
`function ${componentName}() {\n` +
|
|
394
|
-
` return "hello world";\n` +
|
|
395
|
-
`}\n\n` +
|
|
396
|
-
`export default React.memo(${componentName});\n`;
|
|
397
|
-
const formatted = await formatWithPrettier(content);
|
|
398
|
-
await fs.writeFile(filePath, formatted, 'utf8');
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
/** Generate src/build-components/RenderNode.generated.tsx based on validated components */
|
|
402
|
-
async function createRenderNodeGenerated(validated) {
|
|
403
|
-
const targetPath = path.join(COMPONENTS_ROOT, 'RenderNode.generated.tsx');
|
|
404
|
-
|
|
405
|
-
// Build imports for all components discovered
|
|
406
|
-
const componentImports = validated
|
|
407
|
-
.map(
|
|
408
|
-
({ componentName }) =>
|
|
409
|
-
`import ${componentName} from './${componentName}/${componentName}';`
|
|
410
|
-
)
|
|
411
|
-
.join('\n');
|
|
412
|
-
|
|
413
|
-
// Build switch cases from each component's pattern.type
|
|
414
|
-
const cases = validated
|
|
415
|
-
.map(({ componentName, patternJson }) => {
|
|
416
|
-
const type = patternJson?.pattern?.type;
|
|
417
|
-
return typeof type === 'string'
|
|
418
|
-
? ` case ${JSON.stringify(type)}:\n return <${componentName} node={simpleNode} />;`
|
|
419
|
-
: null;
|
|
420
|
-
})
|
|
421
|
-
.filter(Boolean)
|
|
422
|
-
.join('\n');
|
|
423
|
-
|
|
424
|
-
const fileContent =
|
|
425
|
-
`/* AUTO-GENERATED FILE - DO NOT EDIT */\n` +
|
|
426
|
-
`import React from 'react';\n\n` +
|
|
427
|
-
`import {\n` +
|
|
428
|
-
` Node,\n` +
|
|
429
|
-
` NodeData,\n` +
|
|
430
|
-
` isNodeArray,\n` +
|
|
431
|
-
` isNodeNullOrUndefined,\n` +
|
|
432
|
-
` isNodeString,\n` +
|
|
433
|
-
`} from '../index';\n\n` +
|
|
434
|
-
`import { other } from './other';\n\n` +
|
|
435
|
-
`// Builder components\n` +
|
|
436
|
-
componentImports +
|
|
437
|
-
`\n\n` +
|
|
438
|
-
`function RenderNode({ node }: { node: Node }) {\n` +
|
|
439
|
-
` if (isNodeNullOrUndefined(node)) {\n` +
|
|
440
|
-
` return null;\n` +
|
|
441
|
-
` }\n` +
|
|
442
|
-
` if (isNodeString(node)) {\n` +
|
|
443
|
-
` return <Text node={{ children: node as string, type: 'text' }} />;\n` +
|
|
444
|
-
` }\n` +
|
|
445
|
-
` if (isNodeArray(node)) {\n` +
|
|
446
|
-
` return (\n` +
|
|
447
|
-
` <>\n` +
|
|
448
|
-
` {(node as Node[]).map((item: Node, index) => (\n` +
|
|
449
|
-
` <RenderNode key={index} node={item} />\n` +
|
|
450
|
-
` ))}\n` +
|
|
451
|
-
` </>\n` +
|
|
452
|
-
` );\n` +
|
|
453
|
-
` }\n\n` +
|
|
454
|
-
` const simpleNode = node as NodeData;\n` +
|
|
455
|
-
` switch (simpleNode?.type) {\n` +
|
|
456
|
-
cases +
|
|
457
|
-
`\n default:\n` +
|
|
458
|
-
` return other(simpleNode?.type, node);\n` +
|
|
459
|
-
` }\n` +
|
|
460
|
-
`}\n\n` +
|
|
461
|
-
`export default React.memo(RenderNode);\n`;
|
|
462
|
-
|
|
463
|
-
const formatted = await formatWithPrettier(fileContent);
|
|
464
|
-
await fs.writeFile(targetPath, formatted, 'utf8');
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
/** Format all TS/TSX files under src with Prettier */
|
|
468
|
-
async function formatAllSourceFiles() {
|
|
469
|
-
async function* walk(dir) {
|
|
470
|
-
const dirents = await fs.readdir(dir, { withFileTypes: true });
|
|
471
|
-
for (const dirent of dirents) {
|
|
472
|
-
const full = path.join(dir, dirent.name);
|
|
473
|
-
if (dirent.isDirectory()) {
|
|
474
|
-
// Skip common non-source directories
|
|
475
|
-
if (dirent.name === 'node_modules' || dirent.name === 'dist') continue;
|
|
476
|
-
yield* walk(full);
|
|
477
|
-
} else {
|
|
478
|
-
yield full;
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
const targets = [];
|
|
484
|
-
for await (const filePath of walk(SRC_ROOT)) {
|
|
485
|
-
if (filePath.endsWith('.ts') || filePath.endsWith('.tsx')) {
|
|
486
|
-
targets.push(filePath);
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
await Promise.all(
|
|
491
|
-
targets.map(async filePath => {
|
|
492
|
-
const original = await fs.readFile(filePath, 'utf8');
|
|
493
|
-
const formatted = await formatWithPrettier(original);
|
|
494
|
-
if (formatted !== original) {
|
|
495
|
-
await fs.writeFile(filePath, formatted, 'utf8');
|
|
496
|
-
}
|
|
497
|
-
})
|
|
498
|
-
);
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
/**
|
|
502
|
-
* Orchestrator
|
|
503
|
-
*/
|
|
504
28
|
async function run() {
|
|
505
|
-
|
|
506
|
-
const validated = await validateAllComponentsOrThrow();
|
|
29
|
+
const paths = { PROJECT_ROOT, SRC_ROOT, COMPONENTS_ROOT, PATTERNS_ROOT };
|
|
507
30
|
|
|
508
|
-
|
|
509
|
-
//await lintNonGeneratedOrThrow();
|
|
31
|
+
const validated = await validateAllComponentsOrThrow(paths);
|
|
32
|
+
// await lintNonGeneratedOrThrow(paths);
|
|
510
33
|
|
|
511
|
-
// Second pass: actions only if all validations passed
|
|
512
34
|
for (const { componentDir, componentName, patternJson } of validated) {
|
|
513
35
|
await createGeneratedProps(componentDir, componentName, patternJson);
|
|
514
36
|
await ensurePropsTs(componentDir, componentName);
|
|
515
37
|
await createComponentTsx(componentDir, componentName);
|
|
516
|
-
|
|
517
|
-
// Final sanity validations
|
|
518
38
|
await validateExistingComponentTsx(componentDir, componentName);
|
|
519
39
|
}
|
|
520
40
|
|
|
521
|
-
|
|
522
|
-
await
|
|
523
|
-
|
|
524
|
-
// Format all TS/TSX files as the last step
|
|
525
|
-
await formatAllSourceFiles();
|
|
41
|
+
await createRenderNodeGenerated(validated, paths);
|
|
42
|
+
await createBuildComponentsIndex(validated, paths);
|
|
43
|
+
await formatAllSourceFiles(paths);
|
|
526
44
|
}
|
|
527
45
|
|
|
528
46
|
export default run;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { promises as fs } from 'fs';
|
|
3
|
+
import { formatWithPrettier } from './formatWithPrettier.js';
|
|
4
|
+
|
|
5
|
+
// Creates src/build-components/index.ts exporting only props types for each component
|
|
6
|
+
export async function createBuildComponentsIndex(validated, paths) {
|
|
7
|
+
const { COMPONENTS_ROOT } = paths;
|
|
8
|
+
const targetPath = path.join(COMPONENTS_ROOT, 'index.ts');
|
|
9
|
+
|
|
10
|
+
const exportLines = validated
|
|
11
|
+
.map(({ componentName }) => {
|
|
12
|
+
const base = `./${componentName}/${componentName}Props.generated`;
|
|
13
|
+
return `export type { ${componentName}PropsGenerated, ${componentName}ComponentProps } from '${base}';`;
|
|
14
|
+
})
|
|
15
|
+
.join('\n');
|
|
16
|
+
|
|
17
|
+
const fileContent =
|
|
18
|
+
`/* AUTO-GENERATED FILE - DO NOT EDIT */\n\n` +
|
|
19
|
+
exportLines +
|
|
20
|
+
(exportLines ? '\n' : '');
|
|
21
|
+
|
|
22
|
+
const formatted = await formatWithPrettier(fileContent);
|
|
23
|
+
await fs.writeFile(targetPath, formatted, 'utf8');
|
|
24
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { promises as fs } from 'fs';
|
|
3
|
+
import { formatWithPrettier } from './formatWithPrettier.js';
|
|
4
|
+
|
|
5
|
+
export async function createComponentTsx(componentDir, componentName) {
|
|
6
|
+
const filePath = path.join(componentDir, `${componentName}.tsx`);
|
|
7
|
+
const exists = await fs
|
|
8
|
+
.stat(filePath)
|
|
9
|
+
.then(() => true)
|
|
10
|
+
.catch(() => false);
|
|
11
|
+
if (exists) return; // Will be validated later
|
|
12
|
+
|
|
13
|
+
const content =
|
|
14
|
+
`import React from 'react';\n\n` +
|
|
15
|
+
`function ${componentName}() {\n` +
|
|
16
|
+
` return "hello world";\n` +
|
|
17
|
+
`}\n\n` +
|
|
18
|
+
`export default React.memo(${componentName});\n`;
|
|
19
|
+
const formatted = await formatWithPrettier(content);
|
|
20
|
+
await fs.writeFile(filePath, formatted, 'utf8');
|
|
21
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { promises as fs } from 'fs';
|
|
3
|
+
import { ensureDir } from './ensureDir.js';
|
|
4
|
+
// inlined from tsTypeFromAttributeType.js
|
|
5
|
+
function tsTypeFromAttributeType(attrType) {
|
|
6
|
+
if (attrType === 'string') return 'string';
|
|
7
|
+
if (attrType === 'number') return 'number';
|
|
8
|
+
if (attrType === 'boolean') return 'boolean';
|
|
9
|
+
if (Array.isArray(attrType)) {
|
|
10
|
+
const literals = attrType.map(v => JSON.stringify(v)).join(' | ');
|
|
11
|
+
return literals.length > 0 ? literals : 'string';
|
|
12
|
+
}
|
|
13
|
+
return 'string';
|
|
14
|
+
}
|
|
15
|
+
import { formatWithPrettier } from './formatWithPrettier.js';
|
|
16
|
+
|
|
17
|
+
export async function createGeneratedProps(
|
|
18
|
+
componentDir,
|
|
19
|
+
componentName,
|
|
20
|
+
patternJson
|
|
21
|
+
) {
|
|
22
|
+
await ensureDir(componentDir);
|
|
23
|
+
const fileName = `${componentName}Props.generated.ts`;
|
|
24
|
+
const filePath = path.join(componentDir, fileName);
|
|
25
|
+
|
|
26
|
+
const { pattern, allowUnknownAttributes } = patternJson;
|
|
27
|
+
const attributes = pattern.attributes || {};
|
|
28
|
+
const attributeLines = Object.entries(attributes).map(([key, t]) => {
|
|
29
|
+
const tsType = tsTypeFromAttributeType(t);
|
|
30
|
+
return ` ${key}?: ${tsType};`;
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const indexSignature = allowUnknownAttributes
|
|
34
|
+
? ' [key: string]: string | number | boolean | undefined;\n'
|
|
35
|
+
: '';
|
|
36
|
+
|
|
37
|
+
const childTsType =
|
|
38
|
+
typeof pattern.children === 'string' ? pattern.children : 'string';
|
|
39
|
+
const normalizedChildTsType = ['string', 'number', 'boolean'].includes(
|
|
40
|
+
childTsType
|
|
41
|
+
)
|
|
42
|
+
? childTsType
|
|
43
|
+
: 'string';
|
|
44
|
+
|
|
45
|
+
const fileContent =
|
|
46
|
+
`/* AUTO-GENERATED FILE - DO NOT EDIT */\n` +
|
|
47
|
+
`\n` +
|
|
48
|
+
// Re-export a component props helper to avoid repeating the local type in each component file
|
|
49
|
+
`import type { NodeData } from '../../types/Node';\n` +
|
|
50
|
+
`\n` +
|
|
51
|
+
`export interface ${componentName}PropsGenerated {\n` +
|
|
52
|
+
` child: ${normalizedChildTsType};\n` +
|
|
53
|
+
` attributes: {\n` +
|
|
54
|
+
(attributeLines.length ? attributeLines.join('\n') + '\n' : '') +
|
|
55
|
+
indexSignature +
|
|
56
|
+
` };\n` +
|
|
57
|
+
`}\n` +
|
|
58
|
+
`\n` +
|
|
59
|
+
`export interface ${componentName}ComponentProps {\n` +
|
|
60
|
+
` node: NodeData<${componentName}PropsGenerated['attributes']>;\n` +
|
|
61
|
+
`};\n`;
|
|
62
|
+
|
|
63
|
+
const formatted = await formatWithPrettier(fileContent);
|
|
64
|
+
await fs.writeFile(filePath, formatted, 'utf8');
|
|
65
|
+
}
|