slicejs-cli 3.6.3 → 3.6.5
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/LICENSE +21 -21
- package/README.md +225 -226
- package/client.js +740 -744
- package/commands/Print.js +163 -163
- package/commands/Validations.js +92 -92
- package/commands/build/build.js +40 -40
- package/commands/buildProduction/buildProduction.js +577 -579
- package/commands/bundle/bundle.js +236 -234
- package/commands/createComponent/VisualComponentTemplate.js +55 -55
- package/commands/createComponent/createComponent.js +128 -128
- package/commands/deleteComponent/deleteComponent.js +81 -81
- package/commands/doctor/doctor.js +516 -440
- package/commands/getComponent/getComponent.js +695 -701
- package/commands/init/init.js +473 -467
- package/commands/listComponents/listComponents.js +172 -172
- package/commands/startServer/startServer.js +240 -261
- package/commands/startServer/watchServer.js +66 -66
- package/commands/types/types.js +583 -580
- package/commands/utils/LocalCliDelegation.js +53 -53
- package/commands/utils/PackageManager.js +148 -148
- package/commands/utils/PathHelper.js +75 -75
- package/commands/utils/VersionChecker.js +169 -169
- package/commands/utils/bundling/BundleGenerator.js +2525 -2525
- package/commands/utils/bundling/DependencyAnalyzer.js +925 -925
- package/commands/utils/loadConfig.js +31 -31
- package/commands/utils/sliceScripts.js +48 -23
- package/commands/utils/updateManager.js +471 -471
- package/package.json +71 -74
- package/post.js +0 -60
package/commands/types/types.js
CHANGED
|
@@ -1,580 +1,583 @@
|
|
|
1
|
-
import fs from 'fs-extra';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import { parse } from '@babel/parser';
|
|
4
|
-
import traverse from '@babel/traverse';
|
|
5
|
-
|
|
6
|
-
import Print from '../Print.js';
|
|
7
|
-
import { getConfigPath, getComponentsJsPath, joinRoot } from '../utils/PathHelper.js';
|
|
8
|
-
|
|
9
|
-
const TYPE_MAP = {
|
|
10
|
-
string: 'string',
|
|
11
|
-
number: 'number',
|
|
12
|
-
boolean: 'boolean',
|
|
13
|
-
object: 'Record<string, unknown>',
|
|
14
|
-
array: 'unknown[]',
|
|
15
|
-
function: '(...args: unknown[]) => unknown',
|
|
16
|
-
any: 'unknown'
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
const literalValueFromAst = (node) => {
|
|
20
|
-
if (!node) return undefined;
|
|
21
|
-
if (node.type === 'StringLiteral' || node.type === 'NumericLiteral' || node.type === 'BooleanLiteral') {
|
|
22
|
-
return node.value;
|
|
23
|
-
}
|
|
24
|
-
if (node.type === 'NullLiteral') {
|
|
25
|
-
return null;
|
|
26
|
-
}
|
|
27
|
-
return undefined;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
const keyNameFromAst = (keyNode) => {
|
|
31
|
-
if (!keyNode) return null;
|
|
32
|
-
if (keyNode.type === 'Identifier') return keyNode.name;
|
|
33
|
-
if (keyNode.type === 'StringLiteral') return keyNode.value;
|
|
34
|
-
return null;
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
const astNodeToValue = (node) => {
|
|
38
|
-
if (!node) return undefined;
|
|
39
|
-
|
|
40
|
-
const literal = literalValueFromAst(node);
|
|
41
|
-
if (literal !== undefined || (node && node.type === 'NullLiteral')) {
|
|
42
|
-
return literal;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (node.type === 'ArrayExpression') {
|
|
46
|
-
return (node.elements || [])
|
|
47
|
-
.map((element) => astNodeToValue(element))
|
|
48
|
-
.filter((value) => value !== undefined);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (node.type === 'ObjectExpression') {
|
|
52
|
-
const out = {};
|
|
53
|
-
for (const property of node.properties || []) {
|
|
54
|
-
if (property.type !== 'ObjectProperty') continue;
|
|
55
|
-
const key = keyNameFromAst(property.key);
|
|
56
|
-
if (!key) continue;
|
|
57
|
-
const value = astNodeToValue(property.value);
|
|
58
|
-
if (value !== undefined) {
|
|
59
|
-
out[key] = value;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
return out;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return undefined;
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
const normalizePropConfig = (rawConfig) => {
|
|
69
|
-
if (!rawConfig || typeof rawConfig !== 'object' || Array.isArray(rawConfig)) {
|
|
70
|
-
return { type: 'any', required: false };
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const config = {
|
|
74
|
-
type: typeof rawConfig.type === 'string' ? rawConfig.type.toLowerCase() : 'any',
|
|
75
|
-
required: rawConfig.required === true
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
if (Array.isArray(rawConfig.allowedValues) && rawConfig.allowedValues.length > 0) {
|
|
79
|
-
config.allowedValues = rawConfig.allowedValues;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (rawConfig.schema && typeof rawConfig.schema === 'object' && !Array.isArray(rawConfig.schema)) {
|
|
83
|
-
const schema = {};
|
|
84
|
-
for (const [name, nestedRaw] of Object.entries(rawConfig.schema)) {
|
|
85
|
-
schema[name] = normalizePropConfig(nestedRaw);
|
|
86
|
-
}
|
|
87
|
-
config.schema = schema;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
if (rawConfig.items && typeof rawConfig.items === 'object' && !Array.isArray(rawConfig.items)) {
|
|
91
|
-
config.items = normalizePropConfig(rawConfig.items);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return config;
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
const extractStaticPropsFromObjectExpression = (objectExpressionNode) => {
|
|
98
|
-
const raw = astNodeToValue(objectExpressionNode);
|
|
99
|
-
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
|
|
100
|
-
|
|
101
|
-
const props = {};
|
|
102
|
-
for (const [propName, rawConfig] of Object.entries(raw)) {
|
|
103
|
-
props[propName] = normalizePropConfig(rawConfig);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return Object.keys(props).length > 0 ? props : null;
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
const sortKeys = (obj) => {
|
|
110
|
-
return Object.keys(obj)
|
|
111
|
-
.sort((a, b) => a.localeCompare(b))
|
|
112
|
-
.reduce((acc, key) => {
|
|
113
|
-
acc[key] = obj[key];
|
|
114
|
-
return acc;
|
|
115
|
-
}, {});
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
const parseComponentsRegistry = (content, filePath) => {
|
|
119
|
-
const match = content.match(/const components = ({[\s\S]*?});/);
|
|
120
|
-
if (!match) {
|
|
121
|
-
throw new Error(`Invalid format in ${filePath}. Expected: const components = { ... };`);
|
|
122
|
-
}
|
|
123
|
-
try {
|
|
124
|
-
return JSON.parse(match[1]);
|
|
125
|
-
} catch (parseError) {
|
|
126
|
-
throw new Error(`Failed to parse components registry in ${filePath}: ${parseError.message}`);
|
|
127
|
-
}
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
const extractStaticPropsFromSource = (source, filePath) => {
|
|
131
|
-
let ast;
|
|
132
|
-
try {
|
|
133
|
-
ast = parse(source, {
|
|
134
|
-
sourceType: 'module',
|
|
135
|
-
plugins: ['classProperties']
|
|
136
|
-
});
|
|
137
|
-
} catch (parseError) {
|
|
138
|
-
const sourceDesc = filePath || 'unknown file';
|
|
139
|
-
const loc = parseError.loc ? ` at line ${parseError.loc.line}, column ${parseError.loc.column}` : '';
|
|
140
|
-
Print.warning(`Parse error in ${sourceDesc}${loc}: ${parseError.message}`);
|
|
141
|
-
return null;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
let staticPropsObject = null;
|
|
145
|
-
traverse.default(ast, {
|
|
146
|
-
ClassProperty(pathRef) {
|
|
147
|
-
const node = pathRef.node;
|
|
148
|
-
if (!node.static || !node.key || keyNameFromAst(node.key) !== 'props') return;
|
|
149
|
-
if (node.value && node.value.type === 'ObjectExpression') {
|
|
150
|
-
staticPropsObject = node.value;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
if (!staticPropsObject) return null;
|
|
156
|
-
return extractStaticPropsFromObjectExpression(staticPropsObject);
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
const typeFromProp = (propMeta) => {
|
|
160
|
-
if (propMeta.type === 'object' && propMeta.schema && typeof propMeta.schema === 'object') {
|
|
161
|
-
const schemaEntries = Object.entries(sortKeys(propMeta.schema));
|
|
162
|
-
const inner = schemaEntries
|
|
163
|
-
.map(([name, meta]) => {
|
|
164
|
-
const optionalMark = meta.required ? '' : '?';
|
|
165
|
-
return `${name}${optionalMark}: ${typeFromProp(meta)};`;
|
|
166
|
-
})
|
|
167
|
-
.join(' ');
|
|
168
|
-
return `{ ${inner} }`;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
if (propMeta.type === 'array' && propMeta.items && typeof propMeta.items === 'object') {
|
|
172
|
-
return `${typeFromProp(propMeta.items)}[]`;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const allowedValues = Array.isArray(propMeta.allowedValues) ? propMeta.allowedValues : [];
|
|
176
|
-
|
|
177
|
-
if (allowedValues.length > 0 && propMeta.type === 'string' && allowedValues.every((value) => typeof value === 'string')) {
|
|
178
|
-
return allowedValues.map((value) => `'${String(value).replace(/'/g, "\\'")}'`).join(' | ');
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
if (allowedValues.length > 0 && propMeta.type === 'number' && allowedValues.every((value) => typeof value === 'number' && Number.isFinite(value))) {
|
|
182
|
-
return allowedValues.join(' | ');
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
return TYPE_MAP[propMeta.type] || 'unknown';
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
const interfaceNameFor = (componentName) => `${componentName}Props`;
|
|
189
|
-
const DYNAMIC_FALLBACK_PROP = '__dynamicPropsFallback';
|
|
190
|
-
|
|
191
|
-
const DEFAULT_EDITOR_COMPILER_OPTIONS = {
|
|
192
|
-
allowJs: true,
|
|
193
|
-
checkJs: true,
|
|
194
|
-
strict: false,
|
|
195
|
-
noImplicitAny: false,
|
|
196
|
-
strictNullChecks: false,
|
|
197
|
-
maxNodeModuleJsDepth: 2
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
const DEFAULT_EDITOR_INCLUDE = ['src/Components/**/*.js', 'src/**/*.d.ts'];
|
|
201
|
-
const DEFAULT_EDITOR_EXCLUDE = ['node_modules', 'dist', 'src/libs/**', 'tests/**'];
|
|
202
|
-
const NOISY_INCLUDE_PATTERNS = new Set(['src/**/*.js', 'api/**/*.js', 'tests/**/*.js']);
|
|
203
|
-
|
|
204
|
-
const readPublicFolderExcludes = async (projectRoot) => {
|
|
205
|
-
const configPath = getConfigPath(import.meta.url, projectRoot);
|
|
206
|
-
if (!(await fs.pathExists(configPath))) return [];
|
|
207
|
-
|
|
208
|
-
try {
|
|
209
|
-
const raw = await fs.readFile(configPath, 'utf8');
|
|
210
|
-
const parsed = JSON.parse(raw);
|
|
211
|
-
const folders = Array.isArray(parsed?.publicFolders) ? parsed.publicFolders : [];
|
|
212
|
-
return folders
|
|
213
|
-
.map((folder) => String(folder || '').trim())
|
|
214
|
-
.filter(Boolean)
|
|
215
|
-
.map((folder) => folder.replace(/^[/\\]+/, ''))
|
|
216
|
-
.map((folder) => `src/${folder}/**`);
|
|
217
|
-
} catch {
|
|
218
|
-
return [];
|
|
219
|
-
}
|
|
220
|
-
};
|
|
221
|
-
|
|
222
|
-
const collectJavaScriptFiles = async (dirPath) => {
|
|
223
|
-
if (!(await fs.pathExists(dirPath))) return [];
|
|
224
|
-
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
225
|
-
const files = [];
|
|
226
|
-
|
|
227
|
-
for (const entry of entries) {
|
|
228
|
-
const fullPath = path.join(dirPath, entry.name);
|
|
229
|
-
if (entry.isDirectory()) {
|
|
230
|
-
const nested = await collectJavaScriptFiles(fullPath);
|
|
231
|
-
files.push(...nested);
|
|
232
|
-
} else if (entry.isFile() && entry.name.toLowerCase().endsWith('.js')) {
|
|
233
|
-
files.push(fullPath);
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
return files;
|
|
238
|
-
};
|
|
239
|
-
|
|
240
|
-
const ensureNoCheckInPublicVendorFiles = async (projectRoot) => {
|
|
241
|
-
const configPath = getConfigPath(import.meta.url, projectRoot);
|
|
242
|
-
if (!(await fs.pathExists(configPath))) return { updatedFiles: 0, scannedFiles: 0 };
|
|
243
|
-
|
|
244
|
-
let parsed;
|
|
245
|
-
try {
|
|
246
|
-
parsed = JSON.parse(await fs.readFile(configPath, 'utf8'));
|
|
247
|
-
} catch {
|
|
248
|
-
return { updatedFiles: 0, scannedFiles: 0 };
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
const publicFolders = Array.isArray(parsed?.publicFolders) ? parsed.publicFolders : [];
|
|
252
|
-
const candidateDirs = publicFolders
|
|
253
|
-
.map((folder) => String(folder || '').trim())
|
|
254
|
-
.filter(Boolean)
|
|
255
|
-
.map((folder) => folder.replace(/^[/\\]+/, ''))
|
|
256
|
-
.map((folder) => joinRoot(projectRoot, 'src', folder));
|
|
257
|
-
|
|
258
|
-
const uniqueDirs = Array.from(new Set(candidateDirs));
|
|
259
|
-
let scannedFiles = 0;
|
|
260
|
-
let updatedFiles = 0;
|
|
261
|
-
|
|
262
|
-
for (const dirPath of uniqueDirs) {
|
|
263
|
-
const jsFiles = await collectJavaScriptFiles(dirPath);
|
|
264
|
-
for (const filePath of jsFiles) {
|
|
265
|
-
scannedFiles += 1;
|
|
266
|
-
const raw = await fs.readFile(filePath, 'utf8');
|
|
267
|
-
if (raw.startsWith('// @ts-nocheck')) {
|
|
268
|
-
continue;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
await fs.writeFile(filePath, `// @ts-nocheck\n${raw}`, 'utf8');
|
|
272
|
-
updatedFiles += 1;
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
return { updatedFiles, scannedFiles };
|
|
277
|
-
};
|
|
278
|
-
|
|
279
|
-
const generateDeclarationContent = (componentPropsMap) => {
|
|
280
|
-
const componentsSorted = sortKeys(componentPropsMap);
|
|
281
|
-
const lines = [];
|
|
282
|
-
|
|
283
|
-
lines.push('/* Auto-generated by slice types generate. Do not edit manually. */');
|
|
284
|
-
lines.push('');
|
|
285
|
-
|
|
286
|
-
for (const [componentName, props] of Object.entries(componentsSorted)) {
|
|
287
|
-
lines.push(`export interface ${interfaceNameFor(componentName)} {`);
|
|
288
|
-
lines.push(' [key: string]: unknown;');
|
|
289
|
-
const sortedProps = sortKeys(props);
|
|
290
|
-
const isDynamicFallback = Object.keys(sortedProps).length === 1 && sortedProps[DYNAMIC_FALLBACK_PROP];
|
|
291
|
-
if (!isDynamicFallback) {
|
|
292
|
-
for (const [propName, propMeta] of Object.entries(sortedProps)) {
|
|
293
|
-
const optionalMark = propMeta.required ? '' : '?';
|
|
294
|
-
lines.push(` ${propName}${optionalMark}: ${typeFromProp(propMeta)};`);
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
lines.push('}');
|
|
298
|
-
lines.push('');
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
lines.push('export interface SliceComponentPropsMap {');
|
|
302
|
-
for (const componentName of Object.keys(componentsSorted)) {
|
|
303
|
-
lines.push(` ${componentName}: ${interfaceNameFor(componentName)};`);
|
|
304
|
-
}
|
|
305
|
-
lines.push('}');
|
|
306
|
-
lines.push('');
|
|
307
|
-
lines.push('export type SliceComponentName = keyof SliceComponentPropsMap;');
|
|
308
|
-
lines.push('export type SliceDynamicElement = HTMLElement & Record<string, any>;');
|
|
309
|
-
lines.push('');
|
|
310
|
-
lines.push('declare global {');
|
|
311
|
-
lines.push(' const slice: SliceBuildApi & Record<string, any>;');
|
|
312
|
-
lines.push('');
|
|
313
|
-
lines.push(' interface Event {');
|
|
314
|
-
lines.push(' detail: any;');
|
|
315
|
-
lines.push(' key: any;');
|
|
316
|
-
lines.push(' request: any;');
|
|
317
|
-
lines.push(' waitUntil: any;');
|
|
318
|
-
lines.push(' respondWith: any;');
|
|
319
|
-
lines.push(' target: any;');
|
|
320
|
-
lines.push(' currentTarget: any;');
|
|
321
|
-
lines.push(' }');
|
|
322
|
-
lines.push('');
|
|
323
|
-
lines.push(' interface Element {');
|
|
324
|
-
lines.push(' querySelector<E extends Element = HTMLElement>(selectors: string): E | null;');
|
|
325
|
-
lines.push(' querySelectorAll<E extends Element = HTMLElement>(selectors: string): NodeListOf<E>;');
|
|
326
|
-
lines.push(' }');
|
|
327
|
-
lines.push(' interface HTMLElement {');
|
|
328
|
-
lines.push(' [key: string]: any;');
|
|
329
|
-
lines.push(' }');
|
|
330
|
-
lines.push(' interface EventTarget {');
|
|
331
|
-
lines.push(' [key: string]: any;');
|
|
332
|
-
lines.push(' }');
|
|
333
|
-
lines.push('');
|
|
334
|
-
lines.push(' interface SliceBuildApi {');
|
|
335
|
-
lines.push(' build<K extends SliceComponentName>(');
|
|
336
|
-
lines.push(' name: K,');
|
|
337
|
-
lines.push(' props?: SliceComponentPropsMap[K]');
|
|
338
|
-
lines.push(' ): Promise<SliceDynamicElement | null>;');
|
|
339
|
-
lines.push(' }');
|
|
340
|
-
lines.push('}');
|
|
341
|
-
lines.push('');
|
|
342
|
-
lines.push("declare module 'slicejs-web-framework' {");
|
|
343
|
-
lines.push(' interface SliceApi {');
|
|
344
|
-
lines.push(' build<K extends SliceComponentName>(');
|
|
345
|
-
lines.push(' name: K,');
|
|
346
|
-
lines.push(' props?: SliceComponentPropsMap[K]');
|
|
347
|
-
lines.push(' ): Promise<SliceDynamicElement | null>;');
|
|
348
|
-
lines.push(' getComponent<
|
|
349
|
-
lines.push(' componentSliceId: string');
|
|
350
|
-
lines.push(' ):
|
|
351
|
-
lines.push('
|
|
352
|
-
lines.push('
|
|
353
|
-
lines.push('');
|
|
354
|
-
lines.push('
|
|
355
|
-
lines.push('');
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
componentPropsMap[componentName] =
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
await
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
await
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
const
|
|
501
|
-
const
|
|
502
|
-
const
|
|
503
|
-
const
|
|
504
|
-
const
|
|
505
|
-
|
|
506
|
-
const
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
Print.
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
Print.warning(`
|
|
565
|
-
|
|
566
|
-
if (
|
|
567
|
-
Print.
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
};
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { parse } from '@babel/parser';
|
|
4
|
+
import traverse from '@babel/traverse';
|
|
5
|
+
|
|
6
|
+
import Print from '../Print.js';
|
|
7
|
+
import { getConfigPath, getComponentsJsPath, joinRoot } from '../utils/PathHelper.js';
|
|
8
|
+
|
|
9
|
+
const TYPE_MAP = {
|
|
10
|
+
string: 'string',
|
|
11
|
+
number: 'number',
|
|
12
|
+
boolean: 'boolean',
|
|
13
|
+
object: 'Record<string, unknown>',
|
|
14
|
+
array: 'unknown[]',
|
|
15
|
+
function: '(...args: unknown[]) => unknown',
|
|
16
|
+
any: 'unknown'
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const literalValueFromAst = (node) => {
|
|
20
|
+
if (!node) return undefined;
|
|
21
|
+
if (node.type === 'StringLiteral' || node.type === 'NumericLiteral' || node.type === 'BooleanLiteral') {
|
|
22
|
+
return node.value;
|
|
23
|
+
}
|
|
24
|
+
if (node.type === 'NullLiteral') {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
return undefined;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const keyNameFromAst = (keyNode) => {
|
|
31
|
+
if (!keyNode) return null;
|
|
32
|
+
if (keyNode.type === 'Identifier') return keyNode.name;
|
|
33
|
+
if (keyNode.type === 'StringLiteral') return keyNode.value;
|
|
34
|
+
return null;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const astNodeToValue = (node) => {
|
|
38
|
+
if (!node) return undefined;
|
|
39
|
+
|
|
40
|
+
const literal = literalValueFromAst(node);
|
|
41
|
+
if (literal !== undefined || (node && node.type === 'NullLiteral')) {
|
|
42
|
+
return literal;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (node.type === 'ArrayExpression') {
|
|
46
|
+
return (node.elements || [])
|
|
47
|
+
.map((element) => astNodeToValue(element))
|
|
48
|
+
.filter((value) => value !== undefined);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (node.type === 'ObjectExpression') {
|
|
52
|
+
const out = {};
|
|
53
|
+
for (const property of node.properties || []) {
|
|
54
|
+
if (property.type !== 'ObjectProperty') continue;
|
|
55
|
+
const key = keyNameFromAst(property.key);
|
|
56
|
+
if (!key) continue;
|
|
57
|
+
const value = astNodeToValue(property.value);
|
|
58
|
+
if (value !== undefined) {
|
|
59
|
+
out[key] = value;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return undefined;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const normalizePropConfig = (rawConfig) => {
|
|
69
|
+
if (!rawConfig || typeof rawConfig !== 'object' || Array.isArray(rawConfig)) {
|
|
70
|
+
return { type: 'any', required: false };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const config = {
|
|
74
|
+
type: typeof rawConfig.type === 'string' ? rawConfig.type.toLowerCase() : 'any',
|
|
75
|
+
required: rawConfig.required === true
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
if (Array.isArray(rawConfig.allowedValues) && rawConfig.allowedValues.length > 0) {
|
|
79
|
+
config.allowedValues = rawConfig.allowedValues;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (rawConfig.schema && typeof rawConfig.schema === 'object' && !Array.isArray(rawConfig.schema)) {
|
|
83
|
+
const schema = {};
|
|
84
|
+
for (const [name, nestedRaw] of Object.entries(rawConfig.schema)) {
|
|
85
|
+
schema[name] = normalizePropConfig(nestedRaw);
|
|
86
|
+
}
|
|
87
|
+
config.schema = schema;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (rawConfig.items && typeof rawConfig.items === 'object' && !Array.isArray(rawConfig.items)) {
|
|
91
|
+
config.items = normalizePropConfig(rawConfig.items);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return config;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const extractStaticPropsFromObjectExpression = (objectExpressionNode) => {
|
|
98
|
+
const raw = astNodeToValue(objectExpressionNode);
|
|
99
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
|
|
100
|
+
|
|
101
|
+
const props = {};
|
|
102
|
+
for (const [propName, rawConfig] of Object.entries(raw)) {
|
|
103
|
+
props[propName] = normalizePropConfig(rawConfig);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return Object.keys(props).length > 0 ? props : null;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const sortKeys = (obj) => {
|
|
110
|
+
return Object.keys(obj)
|
|
111
|
+
.sort((a, b) => a.localeCompare(b))
|
|
112
|
+
.reduce((acc, key) => {
|
|
113
|
+
acc[key] = obj[key];
|
|
114
|
+
return acc;
|
|
115
|
+
}, {});
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const parseComponentsRegistry = (content, filePath) => {
|
|
119
|
+
const match = content.match(/const components = ({[\s\S]*?});/);
|
|
120
|
+
if (!match) {
|
|
121
|
+
throw new Error(`Invalid format in ${filePath}. Expected: const components = { ... };`);
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
return JSON.parse(match[1]);
|
|
125
|
+
} catch (parseError) {
|
|
126
|
+
throw new Error(`Failed to parse components registry in ${filePath}: ${parseError.message}`);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const extractStaticPropsFromSource = (source, filePath) => {
|
|
131
|
+
let ast;
|
|
132
|
+
try {
|
|
133
|
+
ast = parse(source, {
|
|
134
|
+
sourceType: 'module',
|
|
135
|
+
plugins: ['classProperties']
|
|
136
|
+
});
|
|
137
|
+
} catch (parseError) {
|
|
138
|
+
const sourceDesc = filePath || 'unknown file';
|
|
139
|
+
const loc = parseError.loc ? ` at line ${parseError.loc.line}, column ${parseError.loc.column}` : '';
|
|
140
|
+
Print.warning(`Parse error in ${sourceDesc}${loc}: ${parseError.message}`);
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let staticPropsObject = null;
|
|
145
|
+
traverse.default(ast, {
|
|
146
|
+
ClassProperty(pathRef) {
|
|
147
|
+
const node = pathRef.node;
|
|
148
|
+
if (!node.static || !node.key || keyNameFromAst(node.key) !== 'props') return;
|
|
149
|
+
if (node.value && node.value.type === 'ObjectExpression') {
|
|
150
|
+
staticPropsObject = node.value;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
if (!staticPropsObject) return null;
|
|
156
|
+
return extractStaticPropsFromObjectExpression(staticPropsObject);
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const typeFromProp = (propMeta) => {
|
|
160
|
+
if (propMeta.type === 'object' && propMeta.schema && typeof propMeta.schema === 'object') {
|
|
161
|
+
const schemaEntries = Object.entries(sortKeys(propMeta.schema));
|
|
162
|
+
const inner = schemaEntries
|
|
163
|
+
.map(([name, meta]) => {
|
|
164
|
+
const optionalMark = meta.required ? '' : '?';
|
|
165
|
+
return `${name}${optionalMark}: ${typeFromProp(meta)};`;
|
|
166
|
+
})
|
|
167
|
+
.join(' ');
|
|
168
|
+
return `{ ${inner} }`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (propMeta.type === 'array' && propMeta.items && typeof propMeta.items === 'object') {
|
|
172
|
+
return `${typeFromProp(propMeta.items)}[]`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const allowedValues = Array.isArray(propMeta.allowedValues) ? propMeta.allowedValues : [];
|
|
176
|
+
|
|
177
|
+
if (allowedValues.length > 0 && propMeta.type === 'string' && allowedValues.every((value) => typeof value === 'string')) {
|
|
178
|
+
return allowedValues.map((value) => `'${String(value).replace(/'/g, "\\'")}'`).join(' | ');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (allowedValues.length > 0 && propMeta.type === 'number' && allowedValues.every((value) => typeof value === 'number' && Number.isFinite(value))) {
|
|
182
|
+
return allowedValues.join(' | ');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return TYPE_MAP[propMeta.type] || 'unknown';
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const interfaceNameFor = (componentName) => `${componentName}Props`;
|
|
189
|
+
const DYNAMIC_FALLBACK_PROP = '__dynamicPropsFallback';
|
|
190
|
+
|
|
191
|
+
const DEFAULT_EDITOR_COMPILER_OPTIONS = {
|
|
192
|
+
allowJs: true,
|
|
193
|
+
checkJs: true,
|
|
194
|
+
strict: false,
|
|
195
|
+
noImplicitAny: false,
|
|
196
|
+
strictNullChecks: false,
|
|
197
|
+
maxNodeModuleJsDepth: 2
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const DEFAULT_EDITOR_INCLUDE = ['src/Components/**/*.js', 'src/**/*.d.ts'];
|
|
201
|
+
const DEFAULT_EDITOR_EXCLUDE = ['node_modules', 'dist', 'src/libs/**', 'tests/**'];
|
|
202
|
+
const NOISY_INCLUDE_PATTERNS = new Set(['src/**/*.js', 'api/**/*.js', 'tests/**/*.js']);
|
|
203
|
+
|
|
204
|
+
const readPublicFolderExcludes = async (projectRoot) => {
|
|
205
|
+
const configPath = getConfigPath(import.meta.url, projectRoot);
|
|
206
|
+
if (!(await fs.pathExists(configPath))) return [];
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
const raw = await fs.readFile(configPath, 'utf8');
|
|
210
|
+
const parsed = JSON.parse(raw);
|
|
211
|
+
const folders = Array.isArray(parsed?.publicFolders) ? parsed.publicFolders : [];
|
|
212
|
+
return folders
|
|
213
|
+
.map((folder) => String(folder || '').trim())
|
|
214
|
+
.filter(Boolean)
|
|
215
|
+
.map((folder) => folder.replace(/^[/\\]+/, ''))
|
|
216
|
+
.map((folder) => `src/${folder}/**`);
|
|
217
|
+
} catch {
|
|
218
|
+
return [];
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const collectJavaScriptFiles = async (dirPath) => {
|
|
223
|
+
if (!(await fs.pathExists(dirPath))) return [];
|
|
224
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
225
|
+
const files = [];
|
|
226
|
+
|
|
227
|
+
for (const entry of entries) {
|
|
228
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
229
|
+
if (entry.isDirectory()) {
|
|
230
|
+
const nested = await collectJavaScriptFiles(fullPath);
|
|
231
|
+
files.push(...nested);
|
|
232
|
+
} else if (entry.isFile() && entry.name.toLowerCase().endsWith('.js')) {
|
|
233
|
+
files.push(fullPath);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return files;
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const ensureNoCheckInPublicVendorFiles = async (projectRoot) => {
|
|
241
|
+
const configPath = getConfigPath(import.meta.url, projectRoot);
|
|
242
|
+
if (!(await fs.pathExists(configPath))) return { updatedFiles: 0, scannedFiles: 0 };
|
|
243
|
+
|
|
244
|
+
let parsed;
|
|
245
|
+
try {
|
|
246
|
+
parsed = JSON.parse(await fs.readFile(configPath, 'utf8'));
|
|
247
|
+
} catch {
|
|
248
|
+
return { updatedFiles: 0, scannedFiles: 0 };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const publicFolders = Array.isArray(parsed?.publicFolders) ? parsed.publicFolders : [];
|
|
252
|
+
const candidateDirs = publicFolders
|
|
253
|
+
.map((folder) => String(folder || '').trim())
|
|
254
|
+
.filter(Boolean)
|
|
255
|
+
.map((folder) => folder.replace(/^[/\\]+/, ''))
|
|
256
|
+
.map((folder) => joinRoot(projectRoot, 'src', folder));
|
|
257
|
+
|
|
258
|
+
const uniqueDirs = Array.from(new Set(candidateDirs));
|
|
259
|
+
let scannedFiles = 0;
|
|
260
|
+
let updatedFiles = 0;
|
|
261
|
+
|
|
262
|
+
for (const dirPath of uniqueDirs) {
|
|
263
|
+
const jsFiles = await collectJavaScriptFiles(dirPath);
|
|
264
|
+
for (const filePath of jsFiles) {
|
|
265
|
+
scannedFiles += 1;
|
|
266
|
+
const raw = await fs.readFile(filePath, 'utf8');
|
|
267
|
+
if (raw.startsWith('// @ts-nocheck')) {
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
await fs.writeFile(filePath, `// @ts-nocheck\n${raw}`, 'utf8');
|
|
272
|
+
updatedFiles += 1;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return { updatedFiles, scannedFiles };
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
const generateDeclarationContent = (componentPropsMap) => {
|
|
280
|
+
const componentsSorted = sortKeys(componentPropsMap);
|
|
281
|
+
const lines = [];
|
|
282
|
+
|
|
283
|
+
lines.push('/* Auto-generated by slice types generate. Do not edit manually. */');
|
|
284
|
+
lines.push('');
|
|
285
|
+
|
|
286
|
+
for (const [componentName, props] of Object.entries(componentsSorted)) {
|
|
287
|
+
lines.push(`export interface ${interfaceNameFor(componentName)} {`);
|
|
288
|
+
lines.push(' [key: string]: unknown;');
|
|
289
|
+
const sortedProps = sortKeys(props);
|
|
290
|
+
const isDynamicFallback = Object.keys(sortedProps).length === 1 && sortedProps[DYNAMIC_FALLBACK_PROP];
|
|
291
|
+
if (!isDynamicFallback) {
|
|
292
|
+
for (const [propName, propMeta] of Object.entries(sortedProps)) {
|
|
293
|
+
const optionalMark = propMeta.required ? '' : '?';
|
|
294
|
+
lines.push(` ${propName}${optionalMark}: ${typeFromProp(propMeta)};`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
lines.push('}');
|
|
298
|
+
lines.push('');
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
lines.push('export interface SliceComponentPropsMap {');
|
|
302
|
+
for (const componentName of Object.keys(componentsSorted)) {
|
|
303
|
+
lines.push(` ${componentName}: ${interfaceNameFor(componentName)};`);
|
|
304
|
+
}
|
|
305
|
+
lines.push('}');
|
|
306
|
+
lines.push('');
|
|
307
|
+
lines.push('export type SliceComponentName = keyof SliceComponentPropsMap;');
|
|
308
|
+
lines.push('export type SliceDynamicElement = HTMLElement & Record<string, any>;');
|
|
309
|
+
lines.push('');
|
|
310
|
+
lines.push('declare global {');
|
|
311
|
+
lines.push(' const slice: SliceBuildApi & Record<string, any>;');
|
|
312
|
+
lines.push('');
|
|
313
|
+
lines.push(' interface Event {');
|
|
314
|
+
lines.push(' detail: any;');
|
|
315
|
+
lines.push(' key: any;');
|
|
316
|
+
lines.push(' request: any;');
|
|
317
|
+
lines.push(' waitUntil: any;');
|
|
318
|
+
lines.push(' respondWith: any;');
|
|
319
|
+
lines.push(' target: any;');
|
|
320
|
+
lines.push(' currentTarget: any;');
|
|
321
|
+
lines.push(' }');
|
|
322
|
+
lines.push('');
|
|
323
|
+
lines.push(' interface Element {');
|
|
324
|
+
lines.push(' querySelector<E extends Element = HTMLElement>(selectors: string): E | null;');
|
|
325
|
+
lines.push(' querySelectorAll<E extends Element = HTMLElement>(selectors: string): NodeListOf<E>;');
|
|
326
|
+
lines.push(' }');
|
|
327
|
+
lines.push(' interface HTMLElement {');
|
|
328
|
+
lines.push(' [key: string]: any;');
|
|
329
|
+
lines.push(' }');
|
|
330
|
+
lines.push(' interface EventTarget {');
|
|
331
|
+
lines.push(' [key: string]: any;');
|
|
332
|
+
lines.push(' }');
|
|
333
|
+
lines.push('');
|
|
334
|
+
lines.push(' interface SliceBuildApi {');
|
|
335
|
+
lines.push(' build<K extends SliceComponentName>(');
|
|
336
|
+
lines.push(' name: K,');
|
|
337
|
+
lines.push(' props?: SliceComponentPropsMap[K]');
|
|
338
|
+
lines.push(' ): Promise<SliceDynamicElement | null>;');
|
|
339
|
+
lines.push(' }');
|
|
340
|
+
lines.push('}');
|
|
341
|
+
lines.push('');
|
|
342
|
+
lines.push("declare module 'slicejs-web-framework' {");
|
|
343
|
+
lines.push(' interface SliceApi {');
|
|
344
|
+
lines.push(' build<K extends SliceComponentName>(');
|
|
345
|
+
lines.push(' name: K,');
|
|
346
|
+
lines.push(' props?: SliceComponentPropsMap[K]');
|
|
347
|
+
lines.push(' ): Promise<SliceDynamicElement | null>;');
|
|
348
|
+
lines.push(' getComponent<K extends SliceComponentName>(');
|
|
349
|
+
lines.push(' componentSliceId: K | `${K}-${string}`');
|
|
350
|
+
lines.push(' ): SliceDynamicElement | undefined;');
|
|
351
|
+
lines.push(' getComponent<T extends SliceDynamicElement = SliceDynamicElement>(');
|
|
352
|
+
lines.push(' componentSliceId: string');
|
|
353
|
+
lines.push(' ): T | undefined;');
|
|
354
|
+
lines.push(' }');
|
|
355
|
+
lines.push('}');
|
|
356
|
+
lines.push('');
|
|
357
|
+
lines.push('export {};');
|
|
358
|
+
lines.push('');
|
|
359
|
+
|
|
360
|
+
return lines.join('\n');
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
const readCategoryPathFromConfig = async (configPath, category) => {
|
|
364
|
+
const raw = await fs.readFile(configPath, 'utf8');
|
|
365
|
+
const config = JSON.parse(raw);
|
|
366
|
+
const categoryEntry = config?.paths?.components?.[category];
|
|
367
|
+
if (!categoryEntry || !categoryEntry.path) {
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
return categoryEntry.path.replace(/^[/\\]+/, '');
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
const loadComponentStaticProps = async ({ projectRoot, registryMap }) => {
|
|
374
|
+
const configPath = getConfigPath(import.meta.url, projectRoot);
|
|
375
|
+
const categoryPathCache = new Map();
|
|
376
|
+
const componentPropsMap = {};
|
|
377
|
+
let skippedCount = 0;
|
|
378
|
+
let processedCount = 0;
|
|
379
|
+
|
|
380
|
+
for (const [componentName, category] of Object.entries(sortKeys(registryMap))) {
|
|
381
|
+
if (!categoryPathCache.has(category)) {
|
|
382
|
+
const categoryPath = await readCategoryPathFromConfig(configPath, category);
|
|
383
|
+
categoryPathCache.set(category, categoryPath);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const categoryPath = categoryPathCache.get(category);
|
|
387
|
+
if (!categoryPath) {
|
|
388
|
+
skippedCount++;
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const componentFile = joinRoot(projectRoot, 'src', categoryPath, componentName, `${componentName}.js`);
|
|
393
|
+
if (!(await fs.pathExists(componentFile))) {
|
|
394
|
+
skippedCount++;
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
let source;
|
|
399
|
+
try {
|
|
400
|
+
source = await fs.readFile(componentFile, 'utf8');
|
|
401
|
+
} catch (readError) {
|
|
402
|
+
Print.warning(`Cannot read ${componentFile}: ${readError.message}`);
|
|
403
|
+
skippedCount++;
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const props = extractStaticPropsFromSource(source, componentFile);
|
|
408
|
+
if (props) {
|
|
409
|
+
componentPropsMap[componentName] = props;
|
|
410
|
+
processedCount++;
|
|
411
|
+
} else {
|
|
412
|
+
componentPropsMap[componentName] = { [DYNAMIC_FALLBACK_PROP]: { type: 'any', required: false } };
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (skippedCount > 0) {
|
|
417
|
+
Print.info(`Skipped ${skippedCount} component(s) with missing or unreadable files`);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return componentPropsMap;
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
const generateTypesFile = async ({ projectRoot, outputPath }) => {
|
|
424
|
+
const registryPath = getComponentsJsPath(import.meta.url, projectRoot);
|
|
425
|
+
let registryContent;
|
|
426
|
+
try {
|
|
427
|
+
registryContent = await fs.readFile(registryPath, 'utf8');
|
|
428
|
+
} catch (readError) {
|
|
429
|
+
throw new Error(`Cannot read components registry at ${registryPath}: ${readError.message}`);
|
|
430
|
+
}
|
|
431
|
+
const registryMap = parseComponentsRegistry(registryContent, registryPath);
|
|
432
|
+
|
|
433
|
+
const componentPropsMap = await loadComponentStaticProps({ projectRoot, registryMap });
|
|
434
|
+
|
|
435
|
+
const declaration = generateDeclarationContent(componentPropsMap);
|
|
436
|
+
await fs.ensureDir(path.dirname(outputPath));
|
|
437
|
+
await fs.writeFile(outputPath, declaration, 'utf8');
|
|
438
|
+
|
|
439
|
+
return {
|
|
440
|
+
outputPath,
|
|
441
|
+
componentsProcessed: Object.keys(componentPropsMap).length
|
|
442
|
+
};
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
const toPosixRelative = (projectRoot, targetPath) => {
|
|
446
|
+
const relative = path.relative(projectRoot, targetPath).replace(/\\/g, '/');
|
|
447
|
+
return relative;
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
const ensureEditorConfigForTypes = async ({ projectRoot, outputPath }) => {
|
|
451
|
+
try {
|
|
452
|
+
const tsconfigPath = joinRoot(projectRoot, 'tsconfig.json');
|
|
453
|
+
if (await fs.pathExists(tsconfigPath)) {
|
|
454
|
+
return { mode: 'tsconfig_exists', filePath: tsconfigPath, includeAdded: false };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const jsconfigPath = joinRoot(projectRoot, 'jsconfig.json');
|
|
458
|
+
const declarationGlob = (() => {
|
|
459
|
+
const relative = toPosixRelative(projectRoot, outputPath);
|
|
460
|
+
if (!relative) return 'src/**/*.d.ts';
|
|
461
|
+
const idx = relative.lastIndexOf('/');
|
|
462
|
+
if (idx === -1) return relative;
|
|
463
|
+
const dir = relative.slice(0, idx);
|
|
464
|
+
return `${dir}/**/*.d.ts`;
|
|
465
|
+
})();
|
|
466
|
+
|
|
467
|
+
const writeDefaultJsconfig = async () => {
|
|
468
|
+
const jsconfig = {
|
|
469
|
+
compilerOptions: { ...DEFAULT_EDITOR_COMPILER_OPTIONS },
|
|
470
|
+
include: [...DEFAULT_EDITOR_INCLUDE, declarationGlob],
|
|
471
|
+
exclude: [...DEFAULT_EDITOR_EXCLUDE]
|
|
472
|
+
};
|
|
473
|
+
await fs.writeFile(jsconfigPath, `${JSON.stringify(jsconfig, null, 2)}\n`, 'utf8');
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
if (!(await fs.pathExists(jsconfigPath))) {
|
|
477
|
+
await writeDefaultJsconfig();
|
|
478
|
+
return { mode: 'created_jsconfig', filePath: jsconfigPath, includeAdded: true };
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
let jsconfigRaw;
|
|
482
|
+
try {
|
|
483
|
+
jsconfigRaw = await fs.readFile(jsconfigPath, 'utf8');
|
|
484
|
+
} catch {
|
|
485
|
+
// Don't fail — fall back to writing the default options.
|
|
486
|
+
await writeDefaultJsconfig();
|
|
487
|
+
return { mode: 'reset_jsconfig', reason: 'unreadable', filePath: jsconfigPath, includeAdded: true };
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
let parsed;
|
|
491
|
+
try {
|
|
492
|
+
parsed = JSON.parse(jsconfigRaw);
|
|
493
|
+
} catch {
|
|
494
|
+
// The jsconfig was edited into invalid JSON (a typo, comments, trailing commas...).
|
|
495
|
+
// Don't fail — just write the default options so editor IntelliSense keeps working.
|
|
496
|
+
await writeDefaultJsconfig();
|
|
497
|
+
return { mode: 'reset_jsconfig', reason: 'invalid_json', filePath: jsconfigPath, includeAdded: true };
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const include = Array.isArray(parsed.include) ? parsed.include : [];
|
|
501
|
+
const needsInclude = !include.includes(declarationGlob);
|
|
502
|
+
const hasAllDefaultInclude = DEFAULT_EDITOR_INCLUDE.every((entry) => include.includes(entry));
|
|
503
|
+
const hasNoisyInclude = include.some((entry) => NOISY_INCLUDE_PATTERNS.has(entry));
|
|
504
|
+
const exclude = Array.isArray(parsed.exclude) ? parsed.exclude : [];
|
|
505
|
+
const publicFolderExcludes = await readPublicFolderExcludes(projectRoot);
|
|
506
|
+
const desiredExcludes = Array.from(new Set([...DEFAULT_EDITOR_EXCLUDE, ...publicFolderExcludes]));
|
|
507
|
+
const hasAllDefaultExclude = desiredExcludes.every((entry) => exclude.includes(entry));
|
|
508
|
+
|
|
509
|
+
const compilerOptions = parsed && typeof parsed.compilerOptions === 'object' && parsed.compilerOptions !== null
|
|
510
|
+
? { ...parsed.compilerOptions }
|
|
511
|
+
: {};
|
|
512
|
+
|
|
513
|
+
let compilerOptionsChanged = false;
|
|
514
|
+
for (const [key, value] of Object.entries(DEFAULT_EDITOR_COMPILER_OPTIONS)) {
|
|
515
|
+
if (compilerOptions[key] === undefined) {
|
|
516
|
+
compilerOptions[key] = value;
|
|
517
|
+
compilerOptionsChanged = true;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (!needsInclude && !compilerOptionsChanged && hasAllDefaultInclude && hasAllDefaultExclude && !hasNoisyInclude) {
|
|
522
|
+
return { mode: 'jsconfig_already_has_include', filePath: jsconfigPath, includeAdded: false };
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
parsed.compilerOptions = compilerOptions;
|
|
526
|
+
parsed.include = needsInclude ? [...include, declarationGlob] : include;
|
|
527
|
+
const includeSet = new Set(
|
|
528
|
+
(Array.isArray(parsed.include) ? parsed.include : []).filter((entry) => !NOISY_INCLUDE_PATTERNS.has(entry))
|
|
529
|
+
);
|
|
530
|
+
DEFAULT_EDITOR_INCLUDE.forEach((entry) => includeSet.add(entry));
|
|
531
|
+
parsed.include = Array.from(includeSet);
|
|
532
|
+
|
|
533
|
+
const excludeSet = new Set(Array.isArray(parsed.exclude) ? parsed.exclude : []);
|
|
534
|
+
desiredExcludes.forEach((entry) => excludeSet.add(entry));
|
|
535
|
+
parsed.exclude = Array.from(excludeSet);
|
|
536
|
+
await fs.writeFile(jsconfigPath, `${JSON.stringify(parsed, null, 2)}\n`, 'utf8');
|
|
537
|
+
return { mode: 'updated_jsconfig', filePath: jsconfigPath, includeAdded: needsInclude };
|
|
538
|
+
} catch (error) {
|
|
539
|
+
return {
|
|
540
|
+
mode: 'editor_config_error',
|
|
541
|
+
filePath: projectRoot,
|
|
542
|
+
includeAdded: false,
|
|
543
|
+
errorMessage: error?.message || 'Unknown editor config setup error'
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
const runGenerateTypes = async ({ projectRoot, outputPath }) => {
|
|
549
|
+
const result = await generateTypesFile({ projectRoot, outputPath });
|
|
550
|
+
const editorConfig = await ensureEditorConfigForTypes({ projectRoot, outputPath });
|
|
551
|
+
const publicVendorSuppression = await ensureNoCheckInPublicVendorFiles(projectRoot);
|
|
552
|
+
Print.success(`Generated TypeScript declarations at ${result.outputPath}`);
|
|
553
|
+
Print.info(`Components with static props: ${result.componentsProcessed}`);
|
|
554
|
+
if (editorConfig.mode === 'created_jsconfig') {
|
|
555
|
+
Print.info(`Created jsconfig.json and included declaration glob for editor IntelliSense.`);
|
|
556
|
+
} else if (editorConfig.mode === 'updated_jsconfig') {
|
|
557
|
+
Print.info(`Updated jsconfig.json include list for declaration IntelliSense.`);
|
|
558
|
+
} else if (editorConfig.mode === 'jsconfig_already_has_include') {
|
|
559
|
+
Print.info(`jsconfig.json already includes declaration glob. Editor IntelliSense should pick generated types.`);
|
|
560
|
+
} else if (editorConfig.mode === 'tsconfig_exists') {
|
|
561
|
+
Print.info(`tsconfig.json detected. Types declaration is generated; ensure include covers ${toPosixRelative(projectRoot, outputPath)}.`);
|
|
562
|
+
} else if (editorConfig.mode === 'reset_jsconfig') {
|
|
563
|
+
const why = editorConfig.reason === 'invalid_json' ? 'contained invalid JSON' : 'could not be read';
|
|
564
|
+
Print.warning(`jsconfig.json ${why}; wrote the default options so editor IntelliSense keeps working.`);
|
|
565
|
+
Print.info(`Review ${editorConfig.filePath} if you had custom settings there.`);
|
|
566
|
+
} else if (editorConfig.mode === 'editor_config_error') {
|
|
567
|
+
Print.warning(`Unexpected editor config setup error: ${editorConfig.errorMessage}`);
|
|
568
|
+
}
|
|
569
|
+
if (publicVendorSuppression.updatedFiles > 0) {
|
|
570
|
+
Print.info(`Added // @ts-nocheck to ${publicVendorSuppression.updatedFiles} vendor JS files from publicFolders.`);
|
|
571
|
+
}
|
|
572
|
+
return result;
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
export {
|
|
576
|
+
ensureNoCheckInPublicVendorFiles,
|
|
577
|
+
ensureEditorConfigForTypes,
|
|
578
|
+
extractStaticPropsFromSource,
|
|
579
|
+
generateDeclarationContent,
|
|
580
|
+
generateTypesFile,
|
|
581
|
+
parseComponentsRegistry,
|
|
582
|
+
runGenerateTypes
|
|
583
|
+
};
|