slicejs-cli 3.1.0 โ 3.3.0
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/.github/ISSUE_TEMPLATE/bug_report.md +29 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +25 -0
- package/.github/pull_request_template.md +22 -0
- package/.github/workflows/docs-render-cicd.yml +65 -0
- package/CODE_OF_CONDUCT.md +126 -0
- package/ECOSYSTEM.md +9 -0
- package/LICENSE +21 -0
- package/README.md +104 -308
- package/client.js +644 -557
- package/commands/Print.js +167 -167
- package/commands/Validations.js +103 -103
- package/commands/build/build.js +40 -40
- package/commands/buildProduction/buildProduction.js +579 -579
- package/commands/bundle/bundle.js +235 -235
- package/commands/createComponent/VisualComponentTemplate.js +55 -55
- package/commands/createComponent/createComponent.js +126 -126
- package/commands/deleteComponent/deleteComponent.js +77 -77
- package/commands/doctor/doctor.js +369 -369
- package/commands/getComponent/getComponent.js +747 -747
- package/commands/init/init.js +261 -261
- package/commands/listComponents/listComponents.js +175 -175
- package/commands/startServer/startServer.js +264 -264
- package/commands/startServer/watchServer.js +79 -79
- package/commands/types/types.js +538 -0
- package/commands/utils/LocalCliDelegation.js +53 -53
- package/commands/utils/PathHelper.js +68 -68
- package/commands/utils/VersionChecker.js +167 -167
- package/commands/utils/bundling/BundleGenerator.js +2292 -2292
- package/commands/utils/bundling/DependencyAnalyzer.js +933 -933
- package/commands/utils/updateManager.js +453 -453
- package/docs/superpowers/specs/2026-05-10-pwa-generate-design.md +182 -0
- package/package.json +46 -46
- package/post.js +65 -25
- package/tests/bundle-generator.test.js +708 -708
- package/tests/bundle-v2-register-output.test.js +470 -470
- package/tests/client-launcher-contract.test.js +211 -211
- package/tests/client-update-flow-contract.test.js +272 -272
- package/tests/dependency-analyzer.test.js +24 -24
- package/tests/local-cli-delegation.test.js +79 -79
- package/tests/postinstall-command.test.js +72 -0
- package/tests/types-generator.test.js +356 -0
- package/tests/update-manager-notifications.test.js +88 -88
- package/refactor.md +0 -271
|
@@ -1,79 +1,79 @@
|
|
|
1
|
-
import chokidar from 'chokidar';
|
|
2
|
-
import chalk from 'chalk';
|
|
3
|
-
import Print from '../Print.js';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Configura el watcher para archivos del proyecto
|
|
7
|
-
* @param {ChildProcess} serverProcess - Proceso del servidor
|
|
8
|
-
* @returns {FSWatcher} - Watcher de chokidar
|
|
9
|
-
*/
|
|
10
|
-
export default function setupWatcher(serverProcess, onRestart) {
|
|
11
|
-
Print.info('Watch mode enabled - monitoring file changes...');
|
|
12
|
-
Print.newLine();
|
|
13
|
-
|
|
14
|
-
const watcher = chokidar.watch(['src/**/*', 'api/**/*'], {
|
|
15
|
-
ignored: [
|
|
16
|
-
/(^|[\/\\])\../, // archivos ocultos
|
|
17
|
-
'**/node_modules/**',
|
|
18
|
-
'**/dist/**',
|
|
19
|
-
'**/bundles/**',
|
|
20
|
-
'**/*.log'
|
|
21
|
-
],
|
|
22
|
-
persistent: true,
|
|
23
|
-
ignoreInitial: true,
|
|
24
|
-
awaitWriteFinish: {
|
|
25
|
-
stabilityThreshold: 100,
|
|
26
|
-
pollInterval: 50
|
|
27
|
-
}
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
let reloadTimeout;
|
|
31
|
-
|
|
32
|
-
watcher
|
|
33
|
-
.on('change', (path) => {
|
|
34
|
-
// Debounce para evitar mรบltiples reloads
|
|
35
|
-
clearTimeout(reloadTimeout);
|
|
36
|
-
reloadTimeout = setTimeout(() => {
|
|
37
|
-
if(onRestart) {
|
|
38
|
-
console.log(chalk.yellow('๐ Changes detected, restarting server...'));
|
|
39
|
-
onRestart(path);
|
|
40
|
-
} else {
|
|
41
|
-
console.log(chalk.yellow('๐ Changes detected, server will reload automatically... (No handler)'));
|
|
42
|
-
}
|
|
43
|
-
}, 500);
|
|
44
|
-
})
|
|
45
|
-
.on('add', (path) => {
|
|
46
|
-
// console.log(chalk.green(`โ New file added: ${path}`));
|
|
47
|
-
clearTimeout(reloadTimeout);
|
|
48
|
-
reloadTimeout = setTimeout(() => {
|
|
49
|
-
if (onRestart) onRestart(path);
|
|
50
|
-
}, 500);
|
|
51
|
-
})
|
|
52
|
-
.on('unlink', (path) => {
|
|
53
|
-
// console.log(chalk.red(`โ File removed: ${path}`));
|
|
54
|
-
clearTimeout(reloadTimeout);
|
|
55
|
-
reloadTimeout = setTimeout(() => {
|
|
56
|
-
if (onRestart) onRestart(path);
|
|
57
|
-
}, 500);
|
|
58
|
-
})
|
|
59
|
-
.on('error', (error) => {
|
|
60
|
-
Print.error(`Watcher error: ${error.message}`);
|
|
61
|
-
})
|
|
62
|
-
.on('ready', () => {
|
|
63
|
-
console.log(chalk.gray('๐ Watching for file changes...'));
|
|
64
|
-
Print.newLine();
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
return watcher;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Detiene el watcher de forma segura
|
|
72
|
-
* @param {FSWatcher} watcher - Watcher a detener
|
|
73
|
-
*/
|
|
74
|
-
export function stopWatcher(watcher) {
|
|
75
|
-
if (watcher) {
|
|
76
|
-
watcher.close();
|
|
77
|
-
console.log(chalk.gray('Watch mode stopped'));
|
|
78
|
-
}
|
|
79
|
-
}
|
|
1
|
+
import chokidar from 'chokidar';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import Print from '../Print.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Configura el watcher para archivos del proyecto
|
|
7
|
+
* @param {ChildProcess} serverProcess - Proceso del servidor
|
|
8
|
+
* @returns {FSWatcher} - Watcher de chokidar
|
|
9
|
+
*/
|
|
10
|
+
export default function setupWatcher(serverProcess, onRestart) {
|
|
11
|
+
Print.info('Watch mode enabled - monitoring file changes...');
|
|
12
|
+
Print.newLine();
|
|
13
|
+
|
|
14
|
+
const watcher = chokidar.watch(['src/**/*', 'api/**/*'], {
|
|
15
|
+
ignored: [
|
|
16
|
+
/(^|[\/\\])\../, // archivos ocultos
|
|
17
|
+
'**/node_modules/**',
|
|
18
|
+
'**/dist/**',
|
|
19
|
+
'**/bundles/**',
|
|
20
|
+
'**/*.log'
|
|
21
|
+
],
|
|
22
|
+
persistent: true,
|
|
23
|
+
ignoreInitial: true,
|
|
24
|
+
awaitWriteFinish: {
|
|
25
|
+
stabilityThreshold: 100,
|
|
26
|
+
pollInterval: 50
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
let reloadTimeout;
|
|
31
|
+
|
|
32
|
+
watcher
|
|
33
|
+
.on('change', (path) => {
|
|
34
|
+
// Debounce para evitar mรบltiples reloads
|
|
35
|
+
clearTimeout(reloadTimeout);
|
|
36
|
+
reloadTimeout = setTimeout(() => {
|
|
37
|
+
if(onRestart) {
|
|
38
|
+
console.log(chalk.yellow('๐ Changes detected, restarting server...'));
|
|
39
|
+
onRestart(path);
|
|
40
|
+
} else {
|
|
41
|
+
console.log(chalk.yellow('๐ Changes detected, server will reload automatically... (No handler)'));
|
|
42
|
+
}
|
|
43
|
+
}, 500);
|
|
44
|
+
})
|
|
45
|
+
.on('add', (path) => {
|
|
46
|
+
// console.log(chalk.green(`โ New file added: ${path}`));
|
|
47
|
+
clearTimeout(reloadTimeout);
|
|
48
|
+
reloadTimeout = setTimeout(() => {
|
|
49
|
+
if (onRestart) onRestart(path);
|
|
50
|
+
}, 500);
|
|
51
|
+
})
|
|
52
|
+
.on('unlink', (path) => {
|
|
53
|
+
// console.log(chalk.red(`โ File removed: ${path}`));
|
|
54
|
+
clearTimeout(reloadTimeout);
|
|
55
|
+
reloadTimeout = setTimeout(() => {
|
|
56
|
+
if (onRestart) onRestart(path);
|
|
57
|
+
}, 500);
|
|
58
|
+
})
|
|
59
|
+
.on('error', (error) => {
|
|
60
|
+
Print.error(`Watcher error: ${error.message}`);
|
|
61
|
+
})
|
|
62
|
+
.on('ready', () => {
|
|
63
|
+
console.log(chalk.gray('๐ Watching for file changes...'));
|
|
64
|
+
Print.newLine();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return watcher;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Detiene el watcher de forma segura
|
|
72
|
+
* @param {FSWatcher} watcher - Watcher a detener
|
|
73
|
+
*/
|
|
74
|
+
export function stopWatcher(watcher) {
|
|
75
|
+
if (watcher) {
|
|
76
|
+
watcher.close();
|
|
77
|
+
console.log(chalk.gray('Watch mode stopped'));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,538 @@
|
|
|
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
|
+
|
|
8
|
+
const TYPE_MAP = {
|
|
9
|
+
string: 'string',
|
|
10
|
+
number: 'number',
|
|
11
|
+
boolean: 'boolean',
|
|
12
|
+
object: 'Record<string, unknown>',
|
|
13
|
+
array: 'unknown[]',
|
|
14
|
+
function: '(...args: unknown[]) => unknown',
|
|
15
|
+
any: 'unknown'
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const literalValueFromAst = (node) => {
|
|
19
|
+
if (!node) return undefined;
|
|
20
|
+
if (node.type === 'StringLiteral' || node.type === 'NumericLiteral' || node.type === 'BooleanLiteral') {
|
|
21
|
+
return node.value;
|
|
22
|
+
}
|
|
23
|
+
if (node.type === 'NullLiteral') {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
return undefined;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const keyNameFromAst = (keyNode) => {
|
|
30
|
+
if (!keyNode) return null;
|
|
31
|
+
if (keyNode.type === 'Identifier') return keyNode.name;
|
|
32
|
+
if (keyNode.type === 'StringLiteral') return keyNode.value;
|
|
33
|
+
return null;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const astNodeToValue = (node) => {
|
|
37
|
+
if (!node) return undefined;
|
|
38
|
+
|
|
39
|
+
const literal = literalValueFromAst(node);
|
|
40
|
+
if (literal !== undefined || (node && node.type === 'NullLiteral')) {
|
|
41
|
+
return literal;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (node.type === 'ArrayExpression') {
|
|
45
|
+
return (node.elements || [])
|
|
46
|
+
.map((element) => astNodeToValue(element))
|
|
47
|
+
.filter((value) => value !== undefined);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (node.type === 'ObjectExpression') {
|
|
51
|
+
const out = {};
|
|
52
|
+
for (const property of node.properties || []) {
|
|
53
|
+
if (property.type !== 'ObjectProperty') continue;
|
|
54
|
+
const key = keyNameFromAst(property.key);
|
|
55
|
+
if (!key) continue;
|
|
56
|
+
const value = astNodeToValue(property.value);
|
|
57
|
+
if (value !== undefined) {
|
|
58
|
+
out[key] = value;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return undefined;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const normalizePropConfig = (rawConfig) => {
|
|
68
|
+
if (!rawConfig || typeof rawConfig !== 'object' || Array.isArray(rawConfig)) {
|
|
69
|
+
return { type: 'any', required: false };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const config = {
|
|
73
|
+
type: typeof rawConfig.type === 'string' ? rawConfig.type.toLowerCase() : 'any',
|
|
74
|
+
required: rawConfig.required === true
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
if (Array.isArray(rawConfig.allowedValues) && rawConfig.allowedValues.length > 0) {
|
|
78
|
+
config.allowedValues = rawConfig.allowedValues;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (rawConfig.schema && typeof rawConfig.schema === 'object' && !Array.isArray(rawConfig.schema)) {
|
|
82
|
+
const schema = {};
|
|
83
|
+
for (const [name, nestedRaw] of Object.entries(rawConfig.schema)) {
|
|
84
|
+
schema[name] = normalizePropConfig(nestedRaw);
|
|
85
|
+
}
|
|
86
|
+
config.schema = schema;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (rawConfig.items && typeof rawConfig.items === 'object' && !Array.isArray(rawConfig.items)) {
|
|
90
|
+
config.items = normalizePropConfig(rawConfig.items);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return config;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const extractStaticPropsFromObjectExpression = (objectExpressionNode) => {
|
|
97
|
+
const raw = astNodeToValue(objectExpressionNode);
|
|
98
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
|
|
99
|
+
|
|
100
|
+
const props = {};
|
|
101
|
+
for (const [propName, rawConfig] of Object.entries(raw)) {
|
|
102
|
+
props[propName] = normalizePropConfig(rawConfig);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return Object.keys(props).length > 0 ? props : null;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const sortKeys = (obj) => {
|
|
109
|
+
return Object.keys(obj)
|
|
110
|
+
.sort((a, b) => a.localeCompare(b))
|
|
111
|
+
.reduce((acc, key) => {
|
|
112
|
+
acc[key] = obj[key];
|
|
113
|
+
return acc;
|
|
114
|
+
}, {});
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const parseComponentsRegistry = (content) => {
|
|
118
|
+
const match = content.match(/const components = ({[\s\S]*?});/);
|
|
119
|
+
if (!match) {
|
|
120
|
+
throw new Error('Invalid components.js format. Expected: const components = { ... };');
|
|
121
|
+
}
|
|
122
|
+
return JSON.parse(match[1]);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const extractStaticPropsFromSource = (source) => {
|
|
126
|
+
let ast;
|
|
127
|
+
try {
|
|
128
|
+
ast = parse(source, {
|
|
129
|
+
sourceType: 'module',
|
|
130
|
+
plugins: ['classProperties']
|
|
131
|
+
});
|
|
132
|
+
} catch {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
let staticPropsObject = null;
|
|
137
|
+
traverse.default(ast, {
|
|
138
|
+
ClassProperty(pathRef) {
|
|
139
|
+
const node = pathRef.node;
|
|
140
|
+
if (!node.static || !node.key || keyNameFromAst(node.key) !== 'props') return;
|
|
141
|
+
if (node.value && node.value.type === 'ObjectExpression') {
|
|
142
|
+
staticPropsObject = node.value;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
if (!staticPropsObject) return null;
|
|
148
|
+
return extractStaticPropsFromObjectExpression(staticPropsObject);
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const typeFromProp = (propMeta) => {
|
|
152
|
+
if (propMeta.type === 'object' && propMeta.schema && typeof propMeta.schema === 'object') {
|
|
153
|
+
const schemaEntries = Object.entries(sortKeys(propMeta.schema));
|
|
154
|
+
const inner = schemaEntries
|
|
155
|
+
.map(([name, meta]) => {
|
|
156
|
+
const optionalMark = meta.required ? '' : '?';
|
|
157
|
+
return `${name}${optionalMark}: ${typeFromProp(meta)};`;
|
|
158
|
+
})
|
|
159
|
+
.join(' ');
|
|
160
|
+
return `{ ${inner} }`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (propMeta.type === 'array' && propMeta.items && typeof propMeta.items === 'object') {
|
|
164
|
+
return `${typeFromProp(propMeta.items)}[]`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const allowedValues = Array.isArray(propMeta.allowedValues) ? propMeta.allowedValues : [];
|
|
168
|
+
|
|
169
|
+
if (allowedValues.length > 0 && propMeta.type === 'string' && allowedValues.every((value) => typeof value === 'string')) {
|
|
170
|
+
return allowedValues.map((value) => `'${String(value).replace(/'/g, "\\'")}'`).join(' | ');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (allowedValues.length > 0 && propMeta.type === 'number' && allowedValues.every((value) => typeof value === 'number' && Number.isFinite(value))) {
|
|
174
|
+
return allowedValues.join(' | ');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return TYPE_MAP[propMeta.type] || 'unknown';
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const interfaceNameFor = (componentName) => `${componentName}Props`;
|
|
181
|
+
const DYNAMIC_FALLBACK_PROP = '__dynamicPropsFallback';
|
|
182
|
+
|
|
183
|
+
const DEFAULT_EDITOR_COMPILER_OPTIONS = {
|
|
184
|
+
allowJs: true,
|
|
185
|
+
checkJs: true,
|
|
186
|
+
strict: false,
|
|
187
|
+
noImplicitAny: false,
|
|
188
|
+
strictNullChecks: false,
|
|
189
|
+
maxNodeModuleJsDepth: 2
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const DEFAULT_EDITOR_INCLUDE = ['src/Components/**/*.js', 'src/**/*.d.ts'];
|
|
193
|
+
const DEFAULT_EDITOR_EXCLUDE = ['node_modules', 'dist', 'src/libs/**', 'tests/**'];
|
|
194
|
+
const NOISY_INCLUDE_PATTERNS = new Set(['src/**/*.js', 'api/**/*.js', 'tests/**/*.js']);
|
|
195
|
+
|
|
196
|
+
const readPublicFolderExcludes = async (projectRoot) => {
|
|
197
|
+
const configPath = path.join(projectRoot, 'src', 'sliceConfig.json');
|
|
198
|
+
if (!(await fs.pathExists(configPath))) return [];
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
const raw = await fs.readFile(configPath, 'utf8');
|
|
202
|
+
const parsed = JSON.parse(raw);
|
|
203
|
+
const folders = Array.isArray(parsed?.publicFolders) ? parsed.publicFolders : [];
|
|
204
|
+
return folders
|
|
205
|
+
.map((folder) => String(folder || '').trim())
|
|
206
|
+
.filter(Boolean)
|
|
207
|
+
.map((folder) => folder.replace(/^[/\\]+/, ''))
|
|
208
|
+
.map((folder) => `src/${folder}/**`);
|
|
209
|
+
} catch {
|
|
210
|
+
return [];
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const collectJavaScriptFiles = async (dirPath) => {
|
|
215
|
+
if (!(await fs.pathExists(dirPath))) return [];
|
|
216
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
217
|
+
const files = [];
|
|
218
|
+
|
|
219
|
+
for (const entry of entries) {
|
|
220
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
221
|
+
if (entry.isDirectory()) {
|
|
222
|
+
const nested = await collectJavaScriptFiles(fullPath);
|
|
223
|
+
files.push(...nested);
|
|
224
|
+
} else if (entry.isFile() && entry.name.toLowerCase().endsWith('.js')) {
|
|
225
|
+
files.push(fullPath);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return files;
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const ensureNoCheckInPublicVendorFiles = async (projectRoot) => {
|
|
233
|
+
const configPath = path.join(projectRoot, 'src', 'sliceConfig.json');
|
|
234
|
+
if (!(await fs.pathExists(configPath))) return { updatedFiles: 0, scannedFiles: 0 };
|
|
235
|
+
|
|
236
|
+
let parsed;
|
|
237
|
+
try {
|
|
238
|
+
parsed = JSON.parse(await fs.readFile(configPath, 'utf8'));
|
|
239
|
+
} catch {
|
|
240
|
+
return { updatedFiles: 0, scannedFiles: 0 };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const publicFolders = Array.isArray(parsed?.publicFolders) ? parsed.publicFolders : [];
|
|
244
|
+
const candidateDirs = publicFolders
|
|
245
|
+
.map((folder) => String(folder || '').trim())
|
|
246
|
+
.filter(Boolean)
|
|
247
|
+
.map((folder) => folder.replace(/^[/\\]+/, ''))
|
|
248
|
+
.map((folder) => path.join(projectRoot, 'src', folder));
|
|
249
|
+
|
|
250
|
+
const uniqueDirs = Array.from(new Set(candidateDirs));
|
|
251
|
+
let scannedFiles = 0;
|
|
252
|
+
let updatedFiles = 0;
|
|
253
|
+
|
|
254
|
+
for (const dirPath of uniqueDirs) {
|
|
255
|
+
const jsFiles = await collectJavaScriptFiles(dirPath);
|
|
256
|
+
for (const filePath of jsFiles) {
|
|
257
|
+
scannedFiles += 1;
|
|
258
|
+
const raw = await fs.readFile(filePath, 'utf8');
|
|
259
|
+
if (raw.startsWith('// @ts-nocheck')) {
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
await fs.writeFile(filePath, `// @ts-nocheck\n${raw}`, 'utf8');
|
|
264
|
+
updatedFiles += 1;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return { updatedFiles, scannedFiles };
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
const generateDeclarationContent = (componentPropsMap) => {
|
|
272
|
+
const componentsSorted = sortKeys(componentPropsMap);
|
|
273
|
+
const lines = [];
|
|
274
|
+
|
|
275
|
+
lines.push('/* Auto-generated by slice types generate. Do not edit manually. */');
|
|
276
|
+
lines.push('');
|
|
277
|
+
|
|
278
|
+
for (const [componentName, props] of Object.entries(componentsSorted)) {
|
|
279
|
+
lines.push(`export interface ${interfaceNameFor(componentName)} {`);
|
|
280
|
+
lines.push(' [key: string]: unknown;');
|
|
281
|
+
const sortedProps = sortKeys(props);
|
|
282
|
+
const isDynamicFallback = Object.keys(sortedProps).length === 1 && sortedProps[DYNAMIC_FALLBACK_PROP];
|
|
283
|
+
if (!isDynamicFallback) {
|
|
284
|
+
for (const [propName, propMeta] of Object.entries(sortedProps)) {
|
|
285
|
+
const optionalMark = propMeta.required ? '' : '?';
|
|
286
|
+
lines.push(` ${propName}${optionalMark}: ${typeFromProp(propMeta)};`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
lines.push('}');
|
|
290
|
+
lines.push('');
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
lines.push('export interface SliceComponentPropsMap {');
|
|
294
|
+
for (const componentName of Object.keys(componentsSorted)) {
|
|
295
|
+
lines.push(` ${componentName}: ${interfaceNameFor(componentName)};`);
|
|
296
|
+
}
|
|
297
|
+
lines.push('}');
|
|
298
|
+
lines.push('');
|
|
299
|
+
lines.push('export type SliceComponentName = keyof SliceComponentPropsMap;');
|
|
300
|
+
lines.push('export type SliceDynamicElement = HTMLElement & Record<string, any>;');
|
|
301
|
+
lines.push('');
|
|
302
|
+
lines.push('declare global {');
|
|
303
|
+
lines.push(' const slice: SliceBuildApi & Record<string, any>;');
|
|
304
|
+
lines.push('');
|
|
305
|
+
lines.push(' interface Event {');
|
|
306
|
+
lines.push(' detail: any;');
|
|
307
|
+
lines.push(' key: any;');
|
|
308
|
+
lines.push(' request: any;');
|
|
309
|
+
lines.push(' waitUntil: any;');
|
|
310
|
+
lines.push(' respondWith: any;');
|
|
311
|
+
lines.push(' target: any;');
|
|
312
|
+
lines.push(' currentTarget: any;');
|
|
313
|
+
lines.push(' }');
|
|
314
|
+
lines.push('');
|
|
315
|
+
lines.push(' interface Element {');
|
|
316
|
+
lines.push(' querySelector<E extends Element = HTMLElement>(selectors: string): E | null;');
|
|
317
|
+
lines.push(' querySelectorAll<E extends Element = HTMLElement>(selectors: string): NodeListOf<E>;');
|
|
318
|
+
lines.push(' }');
|
|
319
|
+
lines.push(' interface HTMLElement {');
|
|
320
|
+
lines.push(' [key: string]: any;');
|
|
321
|
+
lines.push(' }');
|
|
322
|
+
lines.push(' interface EventTarget {');
|
|
323
|
+
lines.push(' [key: string]: any;');
|
|
324
|
+
lines.push(' }');
|
|
325
|
+
lines.push('');
|
|
326
|
+
lines.push(' interface SliceBuildApi {');
|
|
327
|
+
lines.push(' build<K extends SliceComponentName>(');
|
|
328
|
+
lines.push(' name: K,');
|
|
329
|
+
lines.push(' props?: SliceComponentPropsMap[K]');
|
|
330
|
+
lines.push(' ): Promise<SliceDynamicElement | null>;');
|
|
331
|
+
lines.push(' }');
|
|
332
|
+
lines.push('}');
|
|
333
|
+
lines.push('');
|
|
334
|
+
lines.push("declare module 'slicejs-web-framework' {");
|
|
335
|
+
lines.push(' interface SliceApi {');
|
|
336
|
+
lines.push(' build<K extends SliceComponentName>(');
|
|
337
|
+
lines.push(' name: K,');
|
|
338
|
+
lines.push(' props?: SliceComponentPropsMap[K]');
|
|
339
|
+
lines.push(' ): Promise<SliceDynamicElement | null>;');
|
|
340
|
+
lines.push(' getComponent<T extends SliceDynamicElement = SliceDynamicElement>(');
|
|
341
|
+
lines.push(' componentSliceId: string');
|
|
342
|
+
lines.push(' ): T | undefined;');
|
|
343
|
+
lines.push(' }');
|
|
344
|
+
lines.push('}');
|
|
345
|
+
lines.push('');
|
|
346
|
+
lines.push('export {};');
|
|
347
|
+
lines.push('');
|
|
348
|
+
|
|
349
|
+
return lines.join('\n');
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
const readCategoryPathFromConfig = async (configPath, category) => {
|
|
353
|
+
const raw = await fs.readFile(configPath, 'utf8');
|
|
354
|
+
const config = JSON.parse(raw);
|
|
355
|
+
const categoryEntry = config?.paths?.components?.[category];
|
|
356
|
+
if (!categoryEntry || !categoryEntry.path) {
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
return categoryEntry.path.replace(/^[/\\]+/, '');
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
const loadComponentStaticProps = async ({ projectRoot, registryMap }) => {
|
|
363
|
+
const configPath = path.join(projectRoot, 'src', 'sliceConfig.json');
|
|
364
|
+
const categoryPathCache = new Map();
|
|
365
|
+
const componentPropsMap = {};
|
|
366
|
+
|
|
367
|
+
for (const [componentName, category] of Object.entries(sortKeys(registryMap))) {
|
|
368
|
+
if (!categoryPathCache.has(category)) {
|
|
369
|
+
const categoryPath = await readCategoryPathFromConfig(configPath, category);
|
|
370
|
+
categoryPathCache.set(category, categoryPath);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const categoryPath = categoryPathCache.get(category);
|
|
374
|
+
if (!categoryPath) {
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const componentFile = path.join(projectRoot, 'src', categoryPath, componentName, `${componentName}.js`);
|
|
379
|
+
if (!(await fs.pathExists(componentFile))) {
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const source = await fs.readFile(componentFile, 'utf8');
|
|
384
|
+
const props = extractStaticPropsFromSource(source);
|
|
385
|
+
componentPropsMap[componentName] = props || { [DYNAMIC_FALLBACK_PROP]: { type: 'any', required: false } };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return componentPropsMap;
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
const generateTypesFile = async ({ projectRoot, outputPath }) => {
|
|
392
|
+
const registryPath = path.join(projectRoot, 'src', 'Components', 'components.js');
|
|
393
|
+
const registryContent = await fs.readFile(registryPath, 'utf8');
|
|
394
|
+
const registryMap = parseComponentsRegistry(registryContent);
|
|
395
|
+
|
|
396
|
+
const componentPropsMap = await loadComponentStaticProps({ projectRoot, registryMap });
|
|
397
|
+
|
|
398
|
+
const declaration = generateDeclarationContent(componentPropsMap);
|
|
399
|
+
await fs.ensureDir(path.dirname(outputPath));
|
|
400
|
+
await fs.writeFile(outputPath, declaration, 'utf8');
|
|
401
|
+
|
|
402
|
+
return {
|
|
403
|
+
outputPath,
|
|
404
|
+
componentsProcessed: Object.keys(componentPropsMap).length
|
|
405
|
+
};
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
const toPosixRelative = (projectRoot, targetPath) => {
|
|
409
|
+
const relative = path.relative(projectRoot, targetPath).replace(/\\/g, '/');
|
|
410
|
+
return relative;
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
const ensureEditorConfigForTypes = async ({ projectRoot, outputPath }) => {
|
|
414
|
+
try {
|
|
415
|
+
const tsconfigPath = path.join(projectRoot, 'tsconfig.json');
|
|
416
|
+
if (await fs.pathExists(tsconfigPath)) {
|
|
417
|
+
return { mode: 'tsconfig_exists', filePath: tsconfigPath, includeAdded: false };
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const jsconfigPath = path.join(projectRoot, 'jsconfig.json');
|
|
421
|
+
const declarationGlob = (() => {
|
|
422
|
+
const relative = toPosixRelative(projectRoot, outputPath);
|
|
423
|
+
if (!relative) return 'src/**/*.d.ts';
|
|
424
|
+
const idx = relative.lastIndexOf('/');
|
|
425
|
+
if (idx === -1) return relative;
|
|
426
|
+
const dir = relative.slice(0, idx);
|
|
427
|
+
return `${dir}/**/*.d.ts`;
|
|
428
|
+
})();
|
|
429
|
+
|
|
430
|
+
if (!(await fs.pathExists(jsconfigPath))) {
|
|
431
|
+
const jsconfig = {
|
|
432
|
+
compilerOptions: { ...DEFAULT_EDITOR_COMPILER_OPTIONS },
|
|
433
|
+
include: [...DEFAULT_EDITOR_INCLUDE, declarationGlob],
|
|
434
|
+
exclude: [...DEFAULT_EDITOR_EXCLUDE]
|
|
435
|
+
};
|
|
436
|
+
await fs.writeFile(jsconfigPath, `${JSON.stringify(jsconfig, null, 2)}\n`, 'utf8');
|
|
437
|
+
return { mode: 'created_jsconfig', filePath: jsconfigPath, includeAdded: true };
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
let jsconfigRaw;
|
|
441
|
+
try {
|
|
442
|
+
jsconfigRaw = await fs.readFile(jsconfigPath, 'utf8');
|
|
443
|
+
} catch {
|
|
444
|
+
return { mode: 'jsconfig_unreadable', filePath: jsconfigPath, includeAdded: false };
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
let parsed;
|
|
448
|
+
try {
|
|
449
|
+
parsed = JSON.parse(jsconfigRaw);
|
|
450
|
+
} catch {
|
|
451
|
+
return { mode: 'jsconfig_invalid_json', filePath: jsconfigPath, includeAdded: false };
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const include = Array.isArray(parsed.include) ? parsed.include : [];
|
|
455
|
+
const needsInclude = !include.includes(declarationGlob);
|
|
456
|
+
const hasAllDefaultInclude = DEFAULT_EDITOR_INCLUDE.every((entry) => include.includes(entry));
|
|
457
|
+
const hasNoisyInclude = include.some((entry) => NOISY_INCLUDE_PATTERNS.has(entry));
|
|
458
|
+
const exclude = Array.isArray(parsed.exclude) ? parsed.exclude : [];
|
|
459
|
+
const publicFolderExcludes = await readPublicFolderExcludes(projectRoot);
|
|
460
|
+
const desiredExcludes = Array.from(new Set([...DEFAULT_EDITOR_EXCLUDE, ...publicFolderExcludes]));
|
|
461
|
+
const hasAllDefaultExclude = desiredExcludes.every((entry) => exclude.includes(entry));
|
|
462
|
+
|
|
463
|
+
const compilerOptions = parsed && typeof parsed.compilerOptions === 'object' && parsed.compilerOptions !== null
|
|
464
|
+
? { ...parsed.compilerOptions }
|
|
465
|
+
: {};
|
|
466
|
+
|
|
467
|
+
let compilerOptionsChanged = false;
|
|
468
|
+
for (const [key, value] of Object.entries(DEFAULT_EDITOR_COMPILER_OPTIONS)) {
|
|
469
|
+
if (compilerOptions[key] === undefined) {
|
|
470
|
+
compilerOptions[key] = value;
|
|
471
|
+
compilerOptionsChanged = true;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (!needsInclude && !compilerOptionsChanged && hasAllDefaultInclude && hasAllDefaultExclude && !hasNoisyInclude) {
|
|
476
|
+
return { mode: 'jsconfig_already_has_include', filePath: jsconfigPath, includeAdded: false };
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
parsed.compilerOptions = compilerOptions;
|
|
480
|
+
parsed.include = needsInclude ? [...include, declarationGlob] : include;
|
|
481
|
+
const includeSet = new Set(
|
|
482
|
+
(Array.isArray(parsed.include) ? parsed.include : []).filter((entry) => !NOISY_INCLUDE_PATTERNS.has(entry))
|
|
483
|
+
);
|
|
484
|
+
DEFAULT_EDITOR_INCLUDE.forEach((entry) => includeSet.add(entry));
|
|
485
|
+
parsed.include = Array.from(includeSet);
|
|
486
|
+
|
|
487
|
+
const excludeSet = new Set(Array.isArray(parsed.exclude) ? parsed.exclude : []);
|
|
488
|
+
desiredExcludes.forEach((entry) => excludeSet.add(entry));
|
|
489
|
+
parsed.exclude = Array.from(excludeSet);
|
|
490
|
+
await fs.writeFile(jsconfigPath, `${JSON.stringify(parsed, null, 2)}\n`, 'utf8');
|
|
491
|
+
return { mode: 'updated_jsconfig', filePath: jsconfigPath, includeAdded: needsInclude };
|
|
492
|
+
} catch (error) {
|
|
493
|
+
return {
|
|
494
|
+
mode: 'editor_config_error',
|
|
495
|
+
filePath: projectRoot,
|
|
496
|
+
includeAdded: false,
|
|
497
|
+
errorMessage: error?.message || 'Unknown editor config setup error'
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
const runGenerateTypes = async ({ projectRoot, outputPath }) => {
|
|
503
|
+
const result = await generateTypesFile({ projectRoot, outputPath });
|
|
504
|
+
const editorConfig = await ensureEditorConfigForTypes({ projectRoot, outputPath });
|
|
505
|
+
const publicVendorSuppression = await ensureNoCheckInPublicVendorFiles(projectRoot);
|
|
506
|
+
Print.success(`Generated TypeScript declarations at ${result.outputPath}`);
|
|
507
|
+
Print.info(`Components with static props: ${result.componentsProcessed}`);
|
|
508
|
+
if (editorConfig.mode === 'created_jsconfig') {
|
|
509
|
+
Print.info(`Created jsconfig.json and included declaration glob for editor IntelliSense.`);
|
|
510
|
+
} else if (editorConfig.mode === 'updated_jsconfig') {
|
|
511
|
+
Print.info(`Updated jsconfig.json include list for declaration IntelliSense.`);
|
|
512
|
+
} else if (editorConfig.mode === 'jsconfig_already_has_include') {
|
|
513
|
+
Print.info(`jsconfig.json already includes declaration glob. Editor IntelliSense should pick generated types.`);
|
|
514
|
+
} else if (editorConfig.mode === 'tsconfig_exists') {
|
|
515
|
+
Print.info(`tsconfig.json detected. Types declaration is generated; ensure include covers ${toPosixRelative(projectRoot, outputPath)}.`);
|
|
516
|
+
} else if (editorConfig.mode === 'jsconfig_invalid_json') {
|
|
517
|
+
Print.warning(`Could not update jsconfig.json because it contains invalid JSON.`);
|
|
518
|
+
Print.info(`Fix ${editorConfig.filePath} and run 'slice types generate' again.`);
|
|
519
|
+
} else if (editorConfig.mode === 'jsconfig_unreadable') {
|
|
520
|
+
Print.warning(`Could not read jsconfig.json to verify declaration include.`);
|
|
521
|
+
Print.info(`Check permissions for ${editorConfig.filePath} and run 'slice types generate' again.`);
|
|
522
|
+
} else if (editorConfig.mode === 'editor_config_error') {
|
|
523
|
+
Print.warning(`Unexpected editor config setup error: ${editorConfig.errorMessage}`);
|
|
524
|
+
}
|
|
525
|
+
if (publicVendorSuppression.updatedFiles > 0) {
|
|
526
|
+
Print.info(`Added // @ts-nocheck to ${publicVendorSuppression.updatedFiles} vendor JS files from publicFolders.`);
|
|
527
|
+
}
|
|
528
|
+
return result;
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
export {
|
|
532
|
+
ensureNoCheckInPublicVendorFiles,
|
|
533
|
+
ensureEditorConfigForTypes,
|
|
534
|
+
extractStaticPropsFromSource,
|
|
535
|
+
generateDeclarationContent,
|
|
536
|
+
generateTypesFile,
|
|
537
|
+
runGenerateTypes
|
|
538
|
+
};
|