@jsarc/initiator 0.0.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/README.md +440 -0
- package/config.ts +153 -0
- package/index.ts +13 -0
- package/intl.ts +353 -0
- package/js/config.js +110 -0
- package/js/index.js +10 -0
- package/js/intl.js +274 -0
- package/js/provider.js +323 -0
- package/js/routing.js +473 -0
- package/package.json +20 -0
- package/provider.ts +423 -0
- package/tsconfig.json +27 -0
package/provider.ts
ADDED
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { Pajo } from '@jsarc/pajo';
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
|
|
9
|
+
export interface ProviderConfig {
|
|
10
|
+
srcDir: string;
|
|
11
|
+
modulesDir: string;
|
|
12
|
+
providersDir: string;
|
|
13
|
+
outputFile: string;
|
|
14
|
+
globalProvidersDir: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ProviderInfo {
|
|
18
|
+
filePath: string;
|
|
19
|
+
componentName: string;
|
|
20
|
+
moduleName?: string;
|
|
21
|
+
priority: number;
|
|
22
|
+
isGlobal: boolean;
|
|
23
|
+
providerType: 'context' | 'redux' | 'router' | 'theme' | 'other';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class ProviderGenerator {
|
|
27
|
+
private config: ProviderConfig;
|
|
28
|
+
private providers: ProviderInfo[] = [];
|
|
29
|
+
private modules: Set<string> = new Set();
|
|
30
|
+
|
|
31
|
+
constructor(
|
|
32
|
+
config: Partial<ProviderConfig> = {},
|
|
33
|
+
dirname: string | undefined = __dirname,
|
|
34
|
+
) {
|
|
35
|
+
this.config = {
|
|
36
|
+
srcDir: config.srcDir || Pajo.join(dirname, 'src') || '',
|
|
37
|
+
modulesDir: config.modulesDir || Pajo.join(dirname, 'src\\modules') || '',
|
|
38
|
+
providersDir: config.providersDir || Pajo.join(dirname, 'src\\providers') || '',
|
|
39
|
+
outputFile: config.outputFile || Pajo.join(dirname, 'src\\auto-provider.tsx') || '',
|
|
40
|
+
globalProvidersDir: config.globalProvidersDir || Pajo.join(dirname, 'src\\providers') || ''
|
|
41
|
+
};
|
|
42
|
+
console.log(`--> ProviderGenerator config:: `, this.config);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public async generate(): Promise<void> {
|
|
46
|
+
console.log('🔍 Scanning for provider files...');
|
|
47
|
+
|
|
48
|
+
await this.findProviderFiles();
|
|
49
|
+
|
|
50
|
+
await this.generateAutoProviderFile();
|
|
51
|
+
|
|
52
|
+
console.log(`✅ Generated ${this.config.outputFile} with ${this.providers.length} providers`);
|
|
53
|
+
console.log(`📦 Found modules with providers: ${Array.from(this.modules).join(', ')}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private async findProviderFiles(): Promise<void> {
|
|
57
|
+
this.providers = [];
|
|
58
|
+
|
|
59
|
+
await this.scanDirectoryForProviders(this.config.globalProvidersDir, undefined, true);
|
|
60
|
+
|
|
61
|
+
if (fs.existsSync(this.config.modulesDir)) {
|
|
62
|
+
const moduleDirs = fs.readdirSync(this.config.modulesDir, { withFileTypes: true })
|
|
63
|
+
.filter(dirent => dirent.isDirectory())
|
|
64
|
+
.map(dirent => dirent.name);
|
|
65
|
+
|
|
66
|
+
for (const moduleName of moduleDirs) {
|
|
67
|
+
const moduleProvidersDir = path.join(this.config.modulesDir, moduleName, 'providers');
|
|
68
|
+
if (fs.existsSync(moduleProvidersDir)) {
|
|
69
|
+
this.modules.add(moduleName);
|
|
70
|
+
await this.scanDirectoryForProviders(moduleProvidersDir, moduleName, false);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private async scanDirectoryForProviders(dir: string, moduleName?: string, isGlobal: boolean = false): Promise<void> {
|
|
77
|
+
try {
|
|
78
|
+
if (!fs.existsSync(dir)) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
83
|
+
|
|
84
|
+
for (const item of items) {
|
|
85
|
+
const fullPath = path.join(dir, item.name);
|
|
86
|
+
|
|
87
|
+
if (item.name === 'node_modules' ||
|
|
88
|
+
item.name === 'dist' ||
|
|
89
|
+
item.name === 'build' ||
|
|
90
|
+
item.name.startsWith('.') ||
|
|
91
|
+
item.name === 'index.ts' ||
|
|
92
|
+
item.name === 'index.tsx' ||
|
|
93
|
+
item.name === 'auto-provider.tsx') {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (item.isDirectory()) {
|
|
98
|
+
|
|
99
|
+
await this.scanDirectoryForProviders(fullPath, moduleName, isGlobal);
|
|
100
|
+
} else if (item.isFile()) {
|
|
101
|
+
|
|
102
|
+
const ext = path.extname(item.name).toLowerCase();
|
|
103
|
+
if (['.tsx', '.jsx', '.ts', '.js'].includes(ext)) {
|
|
104
|
+
const fileName = path.basename(item.name, ext);
|
|
105
|
+
|
|
106
|
+
if (fileName.includes('Provider') || this.isLikelyProviderFile(fullPath)) {
|
|
107
|
+
await this.processProviderFile(fullPath, moduleName, isGlobal);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
} catch (error) {
|
|
113
|
+
console.error(`Error scanning directory ${dir}:`, error);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private async processProviderFile(filePath: string, moduleName?: string, isGlobal: boolean = false): Promise<void> {
|
|
118
|
+
try {
|
|
119
|
+
|
|
120
|
+
const componentName = this.extractComponentName(filePath, moduleName, isGlobal);
|
|
121
|
+
|
|
122
|
+
const providerType = this.determineProviderType(filePath, componentName);
|
|
123
|
+
const priority = this.determineProviderPriority(providerType, componentName, isGlobal);
|
|
124
|
+
|
|
125
|
+
const providerInfo: ProviderInfo = {
|
|
126
|
+
filePath,
|
|
127
|
+
componentName,
|
|
128
|
+
moduleName,
|
|
129
|
+
priority,
|
|
130
|
+
isGlobal,
|
|
131
|
+
providerType
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
this.providers.push(providerInfo);
|
|
135
|
+
console.log(`📦 Found provider: ${componentName} (${providerType}, priority: ${priority})`);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
console.error(`Error processing provider file ${filePath}:`, error);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private extractComponentName(filePath: string, moduleName?: string, isGlobal?: boolean): string {
|
|
142
|
+
const fileName = path.basename(filePath, path.extname(filePath));
|
|
143
|
+
|
|
144
|
+
let componentName = fileName
|
|
145
|
+
.replace(/\.provider$/i, '')
|
|
146
|
+
.replace(/Provider$/i, '')
|
|
147
|
+
.replace(/\.context$/i, '');
|
|
148
|
+
|
|
149
|
+
if (!componentName.match(/^[A-Z]/)) {
|
|
150
|
+
componentName = this.toPascalCase(componentName);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!componentName.endsWith('Provider')) {
|
|
154
|
+
componentName += 'Provider';
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (moduleName && isGlobal === false) {
|
|
158
|
+
const modulePrefix = this.toPascalCase(moduleName);
|
|
159
|
+
if (!componentName.startsWith(modulePrefix)) {
|
|
160
|
+
componentName = modulePrefix + componentName;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return componentName;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private toPascalCase(str: string): string {
|
|
168
|
+
if (!str) return '';
|
|
169
|
+
return str
|
|
170
|
+
.split(/[-_]/)
|
|
171
|
+
.map(part => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
|
|
172
|
+
.join('');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private isLikelyProviderFile(filePath: string): boolean {
|
|
176
|
+
try {
|
|
177
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
178
|
+
|
|
179
|
+
const providerPatterns = [
|
|
180
|
+
/createContext/i,
|
|
181
|
+
/Context\.Provider/i,
|
|
182
|
+
/Provider.*children/i,
|
|
183
|
+
/<Provider/i,
|
|
184
|
+
/export.*Provider/i
|
|
185
|
+
];
|
|
186
|
+
|
|
187
|
+
return providerPatterns.some(pattern => pattern.test(content));
|
|
188
|
+
} catch (error) {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private determineProviderType(filePath: string, componentName: string): ProviderInfo['providerType'] {
|
|
194
|
+
try {
|
|
195
|
+
const content = fs.readFileSync(filePath, 'utf-8').toLowerCase();
|
|
196
|
+
const fileName = path.basename(filePath).toLowerCase();
|
|
197
|
+
|
|
198
|
+
if (content.includes('redux') || content.includes('store') || fileName.includes('redux') || fileName.includes('store')) {
|
|
199
|
+
return 'redux';
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (content.includes('react-router') || content.includes('browserrouter') || fileName.includes('router')) {
|
|
203
|
+
return 'router';
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (content.includes('theme') || content.includes('themeprovider') || fileName.includes('theme')) {
|
|
207
|
+
return 'theme';
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (content.includes('createcontext') || content.includes('context.provider') || fileName.includes('context')) {
|
|
211
|
+
return 'context';
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return 'other';
|
|
215
|
+
} catch (error) {
|
|
216
|
+
return 'other';
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private determineProviderPriority(type: ProviderInfo['providerType'], componentName: string, isGlobal: boolean): number {
|
|
221
|
+
|
|
222
|
+
const basePriority = isGlobal ? 100 : 200;
|
|
223
|
+
|
|
224
|
+
const typePriority: Record<ProviderInfo['providerType'], number> = {
|
|
225
|
+
'router': 10,
|
|
226
|
+
'redux': 20,
|
|
227
|
+
'context': 30,
|
|
228
|
+
'theme': 40,
|
|
229
|
+
'other': 50
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
if (componentName.includes('Router') || componentName.includes('BrowserRouter')) {
|
|
233
|
+
return basePriority + 5;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (componentName.includes('Error') || componentName.includes('Boundary')) {
|
|
237
|
+
return basePriority + 100;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (componentName.includes('Toast') || componentName.includes('Notification')) {
|
|
241
|
+
return basePriority + 90;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return basePriority + (typePriority[type] || 50);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private async generateAutoProviderFile(): Promise<void> {
|
|
248
|
+
const outputDir = path.dirname(this.config.outputFile);
|
|
249
|
+
|
|
250
|
+
if (!fs.existsSync(outputDir)) {
|
|
251
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const content = this.generateFileContent();
|
|
255
|
+
fs.writeFileSync(this.config.outputFile, content, 'utf-8');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private getRelativeImportPath(targetPath: string): string {
|
|
259
|
+
const outputDir = path.dirname(this.config.outputFile);
|
|
260
|
+
const relative = path.relative(outputDir, targetPath);
|
|
261
|
+
|
|
262
|
+
let importPath = relative.replace(/\\/g, '/');
|
|
263
|
+
|
|
264
|
+
if (!importPath.startsWith('.') && !importPath.startsWith('/')) {
|
|
265
|
+
importPath = './' + importPath;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
importPath = importPath.replace(/\.(tsx|jsx|ts|js)$/, '');
|
|
269
|
+
|
|
270
|
+
return importPath;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
private generateFileContent(): string {
|
|
274
|
+
|
|
275
|
+
const sortedProviders = [...this.providers].sort((a, b) => a.priority - b.priority);
|
|
276
|
+
|
|
277
|
+
const providersByModule: Record<string, ProviderInfo[]> = {};
|
|
278
|
+
const globalProviders: ProviderInfo[] = [];
|
|
279
|
+
|
|
280
|
+
sortedProviders.forEach(provider => {
|
|
281
|
+
if (provider.isGlobal || !provider.moduleName) {
|
|
282
|
+
globalProviders.push(provider);
|
|
283
|
+
} else {
|
|
284
|
+
if (!providersByModule[provider.moduleName]) {
|
|
285
|
+
providersByModule[provider.moduleName] = [];
|
|
286
|
+
}
|
|
287
|
+
providersByModule[provider.moduleName].push(provider);
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
const imports: string[] = [
|
|
292
|
+
`import React, { ReactNode, lazy } from 'react';`,
|
|
293
|
+
`import { Suspense } from 'react';`,
|
|
294
|
+
``,
|
|
295
|
+
`// Loading fallback component`,
|
|
296
|
+
`const LoadingFallback = ({ children }: { children: ReactNode }) => (`,
|
|
297
|
+
` <div style={{`,
|
|
298
|
+
` display: 'flex',`,
|
|
299
|
+
` justifyContent: 'center',`,
|
|
300
|
+
` alignItems: 'center',`,
|
|
301
|
+
` minHeight: '100vh'`,
|
|
302
|
+
` }}>`,
|
|
303
|
+
` <div>Loading...</div>`,
|
|
304
|
+
` </div>`,
|
|
305
|
+
`);`,
|
|
306
|
+
``
|
|
307
|
+
];
|
|
308
|
+
|
|
309
|
+
const allProviders = [...globalProviders, ...Object.values(providersByModule).flat()];
|
|
310
|
+
allProviders.forEach(provider => {
|
|
311
|
+
const importPath = this.getRelativeImportPath(provider.filePath);
|
|
312
|
+
|
|
313
|
+
const actualExportName = this.tryGetExportName(provider.filePath, provider.componentName);
|
|
314
|
+
|
|
315
|
+
imports.push(`const ${provider.componentName} = lazy(() => import('${importPath}').then(module => ({ default: module.${actualExportName} || module.default || module })));`);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
imports.push(``);
|
|
319
|
+
|
|
320
|
+
const moduleComponents: string[] = [];
|
|
321
|
+
|
|
322
|
+
Object.entries(providersByModule).forEach(([moduleName, moduleProviders]) => {
|
|
323
|
+
const pascalModuleName = this.toPascalCase(moduleName);
|
|
324
|
+
const componentName = `${pascalModuleName}Providers`;
|
|
325
|
+
|
|
326
|
+
const sortedModuleProviders = moduleProviders.sort((a, b) => a.priority - b.priority);
|
|
327
|
+
|
|
328
|
+
let nestedJSX = '{children}';
|
|
329
|
+
sortedModuleProviders.forEach(provider => {
|
|
330
|
+
nestedJSX = `<${provider.componentName}>\n ${nestedJSX}\n </${provider.componentName}>`;
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
moduleComponents.push(`const ${componentName} = ({ children }: { children: ReactNode }) => (
|
|
334
|
+
${nestedJSX}
|
|
335
|
+
);`);
|
|
336
|
+
|
|
337
|
+
moduleComponents.push(``);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
const sortedGlobalProviders = globalProviders.sort((a, b) => a.priority - b.priority);
|
|
341
|
+
let globalNestedJSX = '{children}';
|
|
342
|
+
sortedGlobalProviders.forEach(provider => {
|
|
343
|
+
globalNestedJSX = `<${provider.componentName}>\n ${globalNestedJSX}\n </${provider.componentName}>`;
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
moduleComponents.push(`const GlobalProviders = ({ children }: { children: ReactNode }) => (
|
|
347
|
+
${globalNestedJSX}
|
|
348
|
+
);`);
|
|
349
|
+
moduleComponents.push(``);
|
|
350
|
+
|
|
351
|
+
const allModuleNames = Object.keys(providersByModule);
|
|
352
|
+
|
|
353
|
+
let mainNestedJSX = '{children}';
|
|
354
|
+
|
|
355
|
+
allModuleNames.sort().forEach(moduleName => {
|
|
356
|
+
const pascalModuleName = this.toPascalCase(moduleName);
|
|
357
|
+
const componentName = `${pascalModuleName}Providers`;
|
|
358
|
+
mainNestedJSX = `<${componentName}>\n ${mainNestedJSX}\n </${componentName}>`;
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
mainNestedJSX = `<GlobalProviders>\n ${mainNestedJSX}\n</GlobalProviders>`;
|
|
362
|
+
|
|
363
|
+
moduleComponents.push(`export const AppProvider = ({ children }: { children: ReactNode }) => (
|
|
364
|
+
<Suspense fallback={<LoadingFallback>{children}</LoadingFallback>}>
|
|
365
|
+
${mainNestedJSX}
|
|
366
|
+
</Suspense>
|
|
367
|
+
);`);
|
|
368
|
+
|
|
369
|
+
moduleComponents.push(``);
|
|
370
|
+
moduleComponents.push(`export default AppProvider;`);
|
|
371
|
+
|
|
372
|
+
return [
|
|
373
|
+
...imports,
|
|
374
|
+
...moduleComponents
|
|
375
|
+
].join('\n');
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
private tryGetExportName(filePath: string, defaultName: string): string {
|
|
379
|
+
try {
|
|
380
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
381
|
+
|
|
382
|
+
const exportRegex = /export\s+(?:const|let|var|function|class)\s+(\w+)/g;
|
|
383
|
+
const exports: string[] = [];
|
|
384
|
+
let match;
|
|
385
|
+
|
|
386
|
+
while ((match = exportRegex.exec(content)) !== null) {
|
|
387
|
+
exports.push(match[1]);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const defaultExportRegex = /export\s+default\s+(\w+)/;
|
|
391
|
+
const defaultMatch = defaultExportRegex.exec(content);
|
|
392
|
+
|
|
393
|
+
if (exports.includes(defaultName)) {
|
|
394
|
+
return defaultName;
|
|
395
|
+
} else if (defaultMatch) {
|
|
396
|
+
return defaultMatch[1];
|
|
397
|
+
} else if (exports.length > 0) {
|
|
398
|
+
return exports[0];
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return 'default';
|
|
402
|
+
} catch (error) {
|
|
403
|
+
return 'default';
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async function ProviderInitiator(
|
|
409
|
+
dirname: string | undefined = __dirname
|
|
410
|
+
) {
|
|
411
|
+
const config: Partial<ProviderConfig> = {};
|
|
412
|
+
|
|
413
|
+
const generator = new ProviderGenerator(config, dirname);
|
|
414
|
+
|
|
415
|
+
try {
|
|
416
|
+
await generator.generate();
|
|
417
|
+
console.log('🎉 Provider generation completed successfully!');
|
|
418
|
+
} catch (err) {
|
|
419
|
+
console.error('❌ Failed to generate providers: ', err);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export default ProviderInitiator;
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES6",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable", "ESNext"],
|
|
6
|
+
"esModuleInterop": true,
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
"declaration": false,
|
|
9
|
+
"sourceMap": false,
|
|
10
|
+
"allowSyntheticDefaultImports": true,
|
|
11
|
+
"noEmit": false,
|
|
12
|
+
|
|
13
|
+
"strict": false,
|
|
14
|
+
"noImplicitAny": false,
|
|
15
|
+
"strictNullChecks": false,
|
|
16
|
+
"strictFunctionTypes": false,
|
|
17
|
+
"strictBindCallApply": false,
|
|
18
|
+
"strictPropertyInitialization": false,
|
|
19
|
+
"noImplicitThis": false,
|
|
20
|
+
"alwaysStrict": false
|
|
21
|
+
},
|
|
22
|
+
"include": [],
|
|
23
|
+
"exclude": [
|
|
24
|
+
"node_modules",
|
|
25
|
+
"**/*.d.ts"
|
|
26
|
+
]
|
|
27
|
+
}
|