@developer_tribe/react-builder 0.1.0 → 0.1.4

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.
Files changed (137) hide show
  1. package/dist/build-components/Button/Button.d.ts +2 -2
  2. package/dist/build-components/Carousel/Carousel.d.ts +2 -2
  3. package/dist/build-components/CarouselButtons/CarouselButtons.d.ts +2 -2
  4. package/dist/build-components/CarouselDots/CarouselDots.d.ts +2 -2
  5. package/dist/build-components/CarouselItem/CarouselItem.d.ts +2 -2
  6. package/dist/build-components/CarouselProvider/CarouselProvider.d.ts +2 -2
  7. package/dist/build-components/Image/Image.d.ts +2 -2
  8. package/dist/build-components/Onboard/Onboard.d.ts +2 -2
  9. package/dist/build-components/OnboardBoardTitle/OnboardBoardTitle.d.ts +2 -2
  10. package/dist/build-components/OnboardButton/OnboardButton.d.ts +2 -2
  11. package/dist/build-components/OnboardButtons/OnboardButtons.d.ts +2 -2
  12. package/dist/build-components/OnboardExpandingDot/OnboardExpandingDot.d.ts +2 -2
  13. package/dist/build-components/OnboardFooter/OnboardFooter.d.ts +2 -2
  14. package/dist/build-components/OnboardImage/OnboardImage.d.ts +2 -2
  15. package/dist/build-components/OnboardItem/OnboardItem.d.ts +2 -2
  16. package/dist/build-components/OnboardProvider/OnboardProvider.d.ts +2 -2
  17. package/dist/build-components/OnboardSubtitle/OnboardSubtitle.d.ts +2 -2
  18. package/dist/build-components/RenderNode.generated.d.ts +1 -1
  19. package/dist/build-components/Text/Text.d.ts +2 -2
  20. package/dist/build-components/View/View.d.ts +2 -2
  21. package/dist/build-components/other.d.ts +3 -0
  22. package/dist/utils/generateRandomKeyForNode.d.ts +1 -0
  23. package/package.json +12 -5
  24. package/scripts/prebuild/build-components.js +528 -0
  25. package/scripts/prebuild/prebuild.js +11 -0
  26. package/scripts/public/bin.js +57 -0
  27. package/scripts/public/scripts/build/index.js +31 -0
  28. package/scripts/public/scripts/build/info.json +11 -0
  29. package/scripts/public/scripts/build/utils/checkFolderAndFilesValid.js +41 -0
  30. package/scripts/public/scripts/build/utils/checkPathExists.js +9 -0
  31. package/scripts/public/scripts/build/utils/createMissingFoldersAndFiles.js +54 -0
  32. package/scripts/public/scripts/build/utils/createRenderNodeGenerated.js +82 -0
  33. package/scripts/public/scripts/build/utils/getAllComponents.js +11 -0
  34. package/src/AttributesEditor.tsx +107 -0
  35. package/src/RenderMainNode.tsx +37 -0
  36. package/src/RenderPage.tsx +61 -0
  37. package/src/assets/devices.json +730 -0
  38. package/src/assets/samples/carousel-sample.json +108 -0
  39. package/src/assets/samples/getSamples.ts +28 -0
  40. package/src/assets/samples/simple-1.json +46 -0
  41. package/src/assets/samples/simple-2.json +233 -0
  42. package/src/assets/samples/vpn-onboard-1.json +799 -0
  43. package/src/assets/samples/vpn-onboard-2.json +790 -0
  44. package/src/assets/samples/vpn-onboard-3.json +803 -0
  45. package/src/assets/samples/vpn-onboard-4.json +804 -0
  46. package/src/build-components/Button/Button.tsx +13 -0
  47. package/src/build-components/Button/ButtonProps.generated.ts +21 -0
  48. package/src/build-components/Button/pattern.json +25 -0
  49. package/src/build-components/Carousel/Carousel.tsx +27 -0
  50. package/src/build-components/Carousel/CarouselProps.generated.ts +6 -0
  51. package/src/build-components/Carousel/pattern.json +9 -0
  52. package/src/build-components/CarouselButtons/CarouselButtons.tsx +47 -0
  53. package/src/build-components/CarouselButtons/CarouselButtonsProps.generated.ts +9 -0
  54. package/src/build-components/CarouselButtons/pattern.json +12 -0
  55. package/src/build-components/CarouselDots/CarouselDots.tsx +40 -0
  56. package/src/build-components/CarouselDots/CarouselDotsProps.generated.ts +14 -0
  57. package/src/build-components/CarouselDots/pattern.json +18 -0
  58. package/src/build-components/CarouselItem/CarouselItem.tsx +18 -0
  59. package/src/build-components/CarouselItem/CarouselItemProps.generated.ts +6 -0
  60. package/src/build-components/CarouselItem/pattern.json +9 -0
  61. package/src/build-components/CarouselProvider/CarouselProvider.tsx +26 -0
  62. package/src/build-components/CarouselProvider/CarouselProviderProps.generated.ts +6 -0
  63. package/src/build-components/CarouselProvider/pattern.json +9 -0
  64. package/src/build-components/Image/Image.tsx +35 -0
  65. package/src/build-components/Image/ImageProps.generated.ts +12 -0
  66. package/src/build-components/Image/pattern.json +15 -0
  67. package/src/build-components/Onboard/Onboard.tsx +14 -0
  68. package/src/build-components/Onboard/OnboardProps.generated.ts +6 -0
  69. package/src/build-components/Onboard/pattern.json +9 -0
  70. package/src/build-components/OnboardBoardTitle/OnboardBoardTitle.tsx +28 -0
  71. package/src/build-components/OnboardBoardTitle/OnboardBoardTitleProps.generated.ts +21 -0
  72. package/src/build-components/OnboardBoardTitle/pattern.json +25 -0
  73. package/src/build-components/OnboardButton/OnboardButton.tsx +59 -0
  74. package/src/build-components/OnboardButton/OnboardButtonProps.generated.ts +13 -0
  75. package/src/build-components/OnboardButton/pattern.json +16 -0
  76. package/src/build-components/OnboardButtons/OnboardButtons.tsx +76 -0
  77. package/src/build-components/OnboardButtons/OnboardButtonsProps.generated.ts +12 -0
  78. package/src/build-components/OnboardButtons/pattern.json +15 -0
  79. package/src/build-components/OnboardExpandingDot/OnboardExpandingDot.tsx +14 -0
  80. package/src/build-components/OnboardExpandingDot/OnboardExpandingDotProps.generated.ts +14 -0
  81. package/src/build-components/OnboardExpandingDot/pattern.json +18 -0
  82. package/src/build-components/OnboardFooter/OnboardFooter.tsx +13 -0
  83. package/src/build-components/OnboardFooter/OnboardFooterProps.generated.ts +24 -0
  84. package/src/build-components/OnboardFooter/pattern.json +28 -0
  85. package/src/build-components/OnboardImage/OnboardImage.tsx +14 -0
  86. package/src/build-components/OnboardImage/OnboardImageProps.generated.ts +12 -0
  87. package/src/build-components/OnboardImage/pattern.json +15 -0
  88. package/src/build-components/OnboardItem/OnboardItem.tsx +29 -0
  89. package/src/build-components/OnboardItem/OnboardItemProps.generated.ts +6 -0
  90. package/src/build-components/OnboardItem/pattern.json +9 -0
  91. package/src/build-components/OnboardProvider/OnboardProvider.tsx +65 -0
  92. package/src/build-components/OnboardProvider/OnboardProviderProps.generated.ts +6 -0
  93. package/src/build-components/OnboardProvider/pattern.json +9 -0
  94. package/src/build-components/OnboardSubtitle/OnboardSubtitle.tsx +28 -0
  95. package/src/build-components/OnboardSubtitle/OnboardSubtitleProps.generated.ts +21 -0
  96. package/src/build-components/OnboardSubtitle/pattern.json +25 -0
  97. package/src/build-components/RenderNode.generated.tsx +97 -0
  98. package/src/build-components/Text/Text.tsx +23 -0
  99. package/src/build-components/Text/TextProps.generated.ts +21 -0
  100. package/src/build-components/Text/pattern.json +26 -0
  101. package/src/build-components/View/View.tsx +62 -0
  102. package/src/build-components/View/ViewProps.generated.ts +24 -0
  103. package/src/build-components/View/pattern.json +28 -0
  104. package/src/build-components/other.ts +6 -0
  105. package/src/index.ts +24 -0
  106. package/src/styles/index.scss +114 -0
  107. package/src/types/Device.ts +12 -0
  108. package/src/types/Node.ts +19 -0
  109. package/src/types/PreviewConfig.ts +19 -0
  110. package/src/types/Project.ts +11 -0
  111. package/src/types/TargetedScreenSize.ts +4 -0
  112. package/src/utils/analyseNode.ts +77 -0
  113. package/src/utils/generateRandomKeyForNode.ts +3 -0
  114. package/src/utils/getDevices.ts +6 -0
  115. package/src/utils/isCarousel.ts +36 -0
  116. package/src/utils/isOnboard.ts +54 -0
  117. package/src/utils/novaToJson.ts +253 -0
  118. package/src/utils/patterns.ts +63 -0
  119. package/dist/build-components/Button/ButtonProps.d.ts +0 -3
  120. package/dist/build-components/Carousel/CarouselProps.d.ts +0 -3
  121. package/dist/build-components/CarouselButtons/CarouselButtonsProps.d.ts +0 -3
  122. package/dist/build-components/CarouselDots/CarouselDotsProps.d.ts +0 -3
  123. package/dist/build-components/CarouselItem/CarouselItemProps.d.ts +0 -3
  124. package/dist/build-components/CarouselProvider/CarouselProviderProps.d.ts +0 -3
  125. package/dist/build-components/Image/ImageProps.d.ts +0 -3
  126. package/dist/build-components/Onboard/OnboardProps.d.ts +0 -3
  127. package/dist/build-components/OnboardBoardTitle/OnboardBoardTitleProps.d.ts +0 -3
  128. package/dist/build-components/OnboardButton/OnboardButtonProps.d.ts +0 -3
  129. package/dist/build-components/OnboardButtons/OnboardButtonsProps.d.ts +0 -3
  130. package/dist/build-components/OnboardExpandingDot/OnboardExpandingDotProps.d.ts +0 -3
  131. package/dist/build-components/OnboardFooter/OnboardFooterProps.d.ts +0 -3
  132. package/dist/build-components/OnboardImage/OnboardImageProps.d.ts +0 -3
  133. package/dist/build-components/OnboardItem/OnboardItemProps.d.ts +0 -3
  134. package/dist/build-components/OnboardProvider/OnboardProviderProps.d.ts +0 -3
  135. package/dist/build-components/OnboardSubtitle/OnboardSubtitleProps.d.ts +0 -3
  136. package/dist/build-components/Text/TextProps.d.ts +0 -3
  137. package/dist/build-components/View/ViewProps.d.ts +0 -3
@@ -0,0 +1,528 @@
1
+ #!/usr/bin/env node
2
+ // Pre-build component generator for files under `src/build-components`
3
+ // Order of operations follows the user's checklist, implemented as sequential functions.
4
+ //TODO: memo needs optimization
5
+ import { promises as fs } from 'fs';
6
+ import path from 'path';
7
+ import url from 'url';
8
+ import prettier from 'prettier';
9
+ import { ESLint } from 'eslint';
10
+
11
+ const __filename = url.fileURLToPath(import.meta.url);
12
+ const __dirname = path.dirname(__filename);
13
+
14
+ const PROJECT_ROOT = path.resolve(__dirname, '..');
15
+ const SRC_ROOT = path.join(PROJECT_ROOT, 'src');
16
+ const COMPONENTS_ROOT = path.join(SRC_ROOT, 'build-components');
17
+ const PATTERNS_ROOT = path.join(SRC_ROOT, 'patterns');
18
+
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
+ async function run() {
505
+ // First pass: validations only; aborts on first failure
506
+ const validated = await validateAllComponentsOrThrow();
507
+
508
+ // Lint pass: fail if non-generated files have ESLint errors
509
+ //await lintNonGeneratedOrThrow();
510
+
511
+ // Second pass: actions only if all validations passed
512
+ for (const { componentDir, componentName, patternJson } of validated) {
513
+ await createGeneratedProps(componentDir, componentName, patternJson);
514
+ await ensurePropsTs(componentDir, componentName);
515
+ await createComponentTsx(componentDir, componentName);
516
+
517
+ // Final sanity validations
518
+ await validateExistingComponentTsx(componentDir, componentName);
519
+ }
520
+
521
+ // Finally, generate the RenderNode for builder components
522
+ await createRenderNodeGenerated(validated);
523
+
524
+ // Format all TS/TSX files as the last step
525
+ await formatAllSourceFiles();
526
+ }
527
+
528
+ export default run;
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+
3
+ import run from './build-components.js';
4
+
5
+ console.log('Building components...');
6
+ try {
7
+ await run();
8
+ } catch (err) {
9
+ console.error(err?.stack || err?.message || err);
10
+ process.exit(1);
11
+ }
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env node
2
+ import { execSync } from 'child_process';
3
+ import fs from 'fs';
4
+ import yargs from 'yargs/yargs';
5
+ import { hideBin } from 'yargs/helpers';
6
+ import Path from 'path';
7
+ import { fileURLToPath } from 'url';
8
+
9
+ const cli = yargs(hideBin(process.argv));
10
+
11
+ const __dirname = Path.dirname(fileURLToPath(import.meta.url));
12
+ const directoryPath = Path.join(__dirname, 'scripts');
13
+
14
+ const getFolderNames = dirPath => {
15
+ try {
16
+ const items = fs.readdirSync(dirPath, { withFileTypes: true });
17
+
18
+ const folderNames = items
19
+ .filter(item => item.isDirectory()) // Keep only directories
20
+ .map(folder => folder.name); // Extract folder names
21
+
22
+ return folderNames;
23
+ } catch (err) {
24
+ console.error(`Error reading directory: ${err.message}`);
25
+ return [];
26
+ }
27
+ };
28
+ const folderNames = getFolderNames(directoryPath);
29
+
30
+ folderNames.forEach(folderName => {
31
+ const infoPath = Path.join(directoryPath, folderName, 'info.json');
32
+ const res = fs.readFileSync(infoPath, 'utf8');
33
+ const json = JSON.parse(res.toString('utf8'));
34
+ cli.command(
35
+ folderName,
36
+ json.description,
37
+ y => {
38
+ Object.entries(json.options ?? {}).forEach(([key, value]) => {
39
+ y.option(key, value);
40
+ });
41
+ },
42
+ args => {
43
+ const keys = Object.keys(json.options ?? {});
44
+ const dynamicArgs = Object.entries(args ?? {})
45
+ .filter(([key]) => keys.includes(key)) // Only accept specified keys
46
+ .map(([key, value]) => `--${key} "${value}"`)
47
+ .join(' ');
48
+ execSync(
49
+ `node ${Path.join(directoryPath, folderName, 'index.js')} ${dynamicArgs}`,
50
+ {
51
+ stdio: 'inherit',
52
+ }
53
+ );
54
+ }
55
+ );
56
+ });
57
+ cli.demandCommand(1, 'You need to specify a command').help().strict().parse();
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env node
2
+
3
+ import yargs from 'yargs/yargs';
4
+ import { hideBin } from 'yargs/helpers';
5
+ import Path from 'path';
6
+ import { fileURLToPath } from 'url';
7
+ import { checkPathExists } from './utils/checkPathExists.js';
8
+ import { getAllComponents } from './utils/getAllComponents.js';
9
+ import { checkFolderAndFilesValid } from './utils/checkFolderAndFilesValid.js';
10
+ import { createMissingFoldersAndFiles } from './utils/createMissingFoldersAndFiles.js';
11
+ import { createRenderNodeGenerated } from './utils/createRenderNodeGenerated.js';
12
+
13
+ const argv = yargs(hideBin(process.argv)).argv;
14
+ export const args = argv;
15
+ const builderPath = args.path || '/node_modules/@developer_tribe/react-builder';
16
+ const targetPath = process.cwd();
17
+
18
+ function run() {
19
+ const builderComponentsPath = Path.join(
20
+ builderPath,
21
+ 'src',
22
+ 'build-components'
23
+ );
24
+ checkPathExists(builderComponentsPath);
25
+ const components = getAllComponents(builderComponentsPath);
26
+ checkFolderAndFilesValid(targetPath, components);
27
+ createMissingFoldersAndFiles(targetPath, components);
28
+ createRenderNodeGenerated(builderComponentsPath, targetPath, components);
29
+ }
30
+
31
+ run();
@@ -0,0 +1,11 @@
1
+ {
2
+ "description": "command for build",
3
+ "options": {
4
+ "path": {
5
+ "alias": "p",
6
+ "type": "string",
7
+ "describe": "path of project",
8
+ "demandOption": false
9
+ }
10
+ }
11
+ }
@@ -0,0 +1,41 @@
1
+ import fs from 'fs';
2
+ import Path from 'path';
3
+
4
+ // Ensure target build-components folder contains only folders from components list; create missing ones
5
+ export function checkFolderAndFilesValid(targetRootPath, components) {
6
+ const buildComponentsTarget = Path.join(
7
+ targetRootPath,
8
+ 'src',
9
+ 'build-components'
10
+ );
11
+ if (!fs.existsSync(buildComponentsTarget)) {
12
+ fs.mkdirSync(buildComponentsTarget, { recursive: true });
13
+ }
14
+
15
+ const entries = fs.readdirSync(buildComponentsTarget, {
16
+ withFileTypes: true,
17
+ });
18
+ const entryNames = new Set(
19
+ entries.filter(e => e.isDirectory()).map(e => e.name)
20
+ );
21
+
22
+ // Remove folders not in components
23
+ for (const name of Array.from(entryNames)) {
24
+ if (
25
+ !components.includes(name) &&
26
+ name !== 'RenderNode.generated.tsx' &&
27
+ name !== 'other.ts'
28
+ ) {
29
+ const full = Path.join(buildComponentsTarget, name);
30
+ fs.rmSync(full, { recursive: true, force: true });
31
+ }
32
+ }
33
+
34
+ // Create missing component folders
35
+ for (const component of components) {
36
+ const dir = Path.join(buildComponentsTarget, component);
37
+ if (!fs.existsSync(dir)) {
38
+ fs.mkdirSync(dir, { recursive: true });
39
+ }
40
+ }
41
+ }
@@ -0,0 +1,9 @@
1
+ import fs from 'fs';
2
+
3
+ export function checkPathExists(path) {
4
+ if (!fs.existsSync(path)) {
5
+ throw new Error(`Path does not exist: ${path}`);
6
+ }
7
+ }
8
+
9
+ // no default export