@abhinav2-3/core 0.1.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/dist/index.d.mts +115 -0
- package/dist/index.d.ts +115 -0
- package/dist/index.js +2505 -0
- package/dist/index.mjs +2493 -0
- package/package.json +26 -0
- package/src/constants/paths.ts +55 -0
- package/src/environment/index.ts +16 -0
- package/src/features/apply-features.ts +171 -0
- package/src/features/index.ts +3 -0
- package/src/features/registry.ts +222 -0
- package/src/features/resolver.ts +19 -0
- package/src/filesystem/index.ts +45 -0
- package/src/generator/generateProject.ts +91 -0
- package/src/git/index.ts +22 -0
- package/src/index.ts +3 -0
- package/src/installers/base.ts +31 -0
- package/src/installers/implementations.ts +28 -0
- package/src/installers/index.ts +16 -0
- package/src/resolvers/templateResolver.ts +26 -0
- package/src/template-engine/index.ts +21 -0
- package/src/types.ts +38 -0
- package/tsconfig.json +13 -0
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@abhinav2-3/core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"chalk": "^4.1.2",
|
|
11
|
+
"ejs": "^3.1.9",
|
|
12
|
+
"fs-extra": "^11.1.1"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@types/ejs": "^3.1.2",
|
|
16
|
+
"@types/fs-extra": "^11.0.2",
|
|
17
|
+
"execa": "^5.0.0",
|
|
18
|
+
"ts-node": "^10.9.2",
|
|
19
|
+
"tsup": "^8.0.0"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsup src/index.ts --format cjs,esm --dts",
|
|
23
|
+
"dev": "tsup src/index.ts --format cjs,esm --watch --dts",
|
|
24
|
+
"lint": "eslint src/**/*.ts"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Resolves the root directory for assets (templates and features).
|
|
6
|
+
* It looks for these directories in various locations to support both
|
|
7
|
+
* development (monorepo) and production (installed package).
|
|
8
|
+
*/
|
|
9
|
+
export const getAssetsRoot = (): string => {
|
|
10
|
+
// 1. Check if environment variable is set
|
|
11
|
+
if (process.env.MONSTACK_ASSETS_PATH) {
|
|
12
|
+
return process.env.MONSTACK_ASSETS_PATH;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// 2. Try to find relative to __dirname (handles different build outputs)
|
|
16
|
+
// In dev: packages/core/src/constants -> ../../.. (monorepo root)
|
|
17
|
+
// In dist: packages/core/dist/constants -> ../../.. (packages/core)
|
|
18
|
+
|
|
19
|
+
const possiblePaths = [
|
|
20
|
+
// Monorepo root (dev)
|
|
21
|
+
path.resolve(__dirname, '../../../../'),
|
|
22
|
+
// Core package root (dist)
|
|
23
|
+
path.resolve(__dirname, '../../../'),
|
|
24
|
+
// Current working directory
|
|
25
|
+
process.cwd(),
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
for (const root of possiblePaths) {
|
|
29
|
+
const templatesExists = fs.pathExistsSync(path.join(root, 'templates'));
|
|
30
|
+
const featuresExists = fs.pathExistsSync(path.join(root, 'features'));
|
|
31
|
+
|
|
32
|
+
// If we are in the monorepo root, templates/features are under packages/
|
|
33
|
+
const pkgTemplatesExists = fs.pathExistsSync(path.join(root, 'packages/templates'));
|
|
34
|
+
const pkgFeaturesExists = fs.pathExistsSync(path.join(root, 'packages/features'));
|
|
35
|
+
|
|
36
|
+
if (templatesExists && featuresExists) {
|
|
37
|
+
return root;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (pkgTemplatesExists && pkgFeaturesExists) {
|
|
41
|
+
return path.join(root, 'packages');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Fallback to monorepo dev structure
|
|
46
|
+
return path.resolve(__dirname, '../../../../packages');
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const getTemplatesPath = (): string => {
|
|
50
|
+
return path.join(getAssetsRoot(), 'templates');
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const getFeaturesPath = (): string => {
|
|
54
|
+
return path.join(getAssetsRoot(), 'features');
|
|
55
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
|
|
5
|
+
export const setupEnvironmentFiles = async (
|
|
6
|
+
projectPath: string,
|
|
7
|
+
): Promise<void> => {
|
|
8
|
+
const examplePath = path.join(projectPath, '.env.example');
|
|
9
|
+
const envPath = path.join(projectPath, '.env');
|
|
10
|
+
|
|
11
|
+
if (await fs.pathExists(examplePath)) {
|
|
12
|
+
console.log(chalk.cyan('š Creating .env file...'));
|
|
13
|
+
await fs.copy(examplePath, envPath);
|
|
14
|
+
console.log(chalk.green('ā
.env file created.'));
|
|
15
|
+
}
|
|
16
|
+
};
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { GeneratorConfig, FeatureDependencies } from '../types';
|
|
5
|
+
import { getAllFiles, copyFile } from '../filesystem';
|
|
6
|
+
import {
|
|
7
|
+
renderTemplate,
|
|
8
|
+
isTemplateFile,
|
|
9
|
+
getTargetFileName,
|
|
10
|
+
} from '../template-engine';
|
|
11
|
+
import { resolveFeatures } from './resolver';
|
|
12
|
+
import { getFeatureRegistry } from './registry';
|
|
13
|
+
import { createRequire } from 'module';
|
|
14
|
+
|
|
15
|
+
// Use a custom require to safely load hooks in both CJS and ESM environments.
|
|
16
|
+
const customRequire =
|
|
17
|
+
typeof require !== 'undefined'
|
|
18
|
+
? require
|
|
19
|
+
: createRequire(eval('import.meta.url'));
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Applies all selected features to the generated project.
|
|
23
|
+
*/
|
|
24
|
+
export const applyFeatures = async (
|
|
25
|
+
projectPath: string,
|
|
26
|
+
config: GeneratorConfig,
|
|
27
|
+
options: {
|
|
28
|
+
renderData?: Record<string, any>;
|
|
29
|
+
assetsRoot?: string;
|
|
30
|
+
} = {},
|
|
31
|
+
): Promise<void> => {
|
|
32
|
+
const selectedFeatures = resolveFeatures(config);
|
|
33
|
+
|
|
34
|
+
if (selectedFeatures.length === 0) return;
|
|
35
|
+
|
|
36
|
+
const registry = await getFeatureRegistry();
|
|
37
|
+
const validation = registry.validateFeatures(
|
|
38
|
+
selectedFeatures,
|
|
39
|
+
config.framework,
|
|
40
|
+
config.architecture,
|
|
41
|
+
config.database,
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
if (!validation.valid) {
|
|
45
|
+
console.error(chalk.red('\nā Feature validation failed:'));
|
|
46
|
+
validation.errors.forEach((err) => console.error(chalk.red(` - ${err}`)));
|
|
47
|
+
throw new Error('Feature validation failed');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
console.log(chalk.cyan('\nš§© Applying features...'));
|
|
51
|
+
|
|
52
|
+
for (const featureName of selectedFeatures) {
|
|
53
|
+
const feature = registry.getFeature(featureName)!;
|
|
54
|
+
await applySingleFeature(
|
|
55
|
+
featureName,
|
|
56
|
+
feature.path,
|
|
57
|
+
projectPath,
|
|
58
|
+
config,
|
|
59
|
+
options.renderData,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Applies a single feature to the project.
|
|
66
|
+
*/
|
|
67
|
+
const applySingleFeature = async (
|
|
68
|
+
featureName: string,
|
|
69
|
+
featureDir: string,
|
|
70
|
+
projectPath: string,
|
|
71
|
+
config: GeneratorConfig,
|
|
72
|
+
renderData?: Record<string, any>,
|
|
73
|
+
): Promise<void> => {
|
|
74
|
+
console.log(chalk.blue(` - Applying ${featureName}...`));
|
|
75
|
+
|
|
76
|
+
// 1. Copy Files with EJS rendering support
|
|
77
|
+
const filesDir = path.join(featureDir, 'files');
|
|
78
|
+
if (await fs.pathExists(filesDir)) {
|
|
79
|
+
const files = await getAllFiles(filesDir);
|
|
80
|
+
for (const file of files) {
|
|
81
|
+
const relativePath = path.relative(filesDir, file);
|
|
82
|
+
const targetPath = path.join(
|
|
83
|
+
projectPath,
|
|
84
|
+
isTemplateFile(relativePath)
|
|
85
|
+
? getTargetFileName(relativePath)
|
|
86
|
+
: relativePath,
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
// If it's an EJS template, render it
|
|
90
|
+
if (isTemplateFile(file)) {
|
|
91
|
+
const templateData = renderData || {};
|
|
92
|
+
const renderedContent = await renderTemplate(file, templateData);
|
|
93
|
+
await fs.ensureDir(path.dirname(targetPath));
|
|
94
|
+
await fs.writeFile(targetPath, renderedContent, 'utf-8');
|
|
95
|
+
} else {
|
|
96
|
+
await copyFile(file, targetPath);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 2. Merge Dependencies
|
|
102
|
+
const depFile = path.join(featureDir, 'dependencies.json');
|
|
103
|
+
if (await fs.pathExists(depFile)) {
|
|
104
|
+
const deps: FeatureDependencies = await fs.readJson(depFile);
|
|
105
|
+
await mergeDependencies(projectPath, deps);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 3. Run Post-Apply Hooks
|
|
109
|
+
// Note: Using a safer way to load hooks for production might be needed later
|
|
110
|
+
const hookFile = path.join(featureDir, 'index.ts');
|
|
111
|
+
const hookFileJs = path.join(featureDir, 'index.js');
|
|
112
|
+
|
|
113
|
+
let hookModule: any = null;
|
|
114
|
+
|
|
115
|
+
if (await fs.pathExists(hookFile)) {
|
|
116
|
+
try {
|
|
117
|
+
// In development, use ts-node
|
|
118
|
+
customRequire('ts-node').register({
|
|
119
|
+
transpileOnly: true,
|
|
120
|
+
compilerOptions: {
|
|
121
|
+
module: 'CommonJS',
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
hookModule = customRequire(hookFile);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
// Fallback or ignore if ts-node fails
|
|
127
|
+
}
|
|
128
|
+
} else if (await fs.pathExists(hookFileJs)) {
|
|
129
|
+
hookModule = customRequire(hookFileJs);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (hookModule && hookModule.postApply) {
|
|
133
|
+
try {
|
|
134
|
+
await hookModule.postApply(projectPath, config);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
console.error(
|
|
137
|
+
chalk.red(` - Error running postApply hook for ${featureName}:`),
|
|
138
|
+
error,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Merges feature dependencies into the project's package.json
|
|
146
|
+
*/
|
|
147
|
+
const mergeDependencies = async (
|
|
148
|
+
projectPath: string,
|
|
149
|
+
featureDeps: FeatureDependencies,
|
|
150
|
+
): Promise<void> => {
|
|
151
|
+
const pkgPath = path.join(projectPath, 'package.json');
|
|
152
|
+
if (!(await fs.pathExists(pkgPath))) return;
|
|
153
|
+
|
|
154
|
+
const pkg = await fs.readJson(pkgPath);
|
|
155
|
+
|
|
156
|
+
if (featureDeps.dependencies) {
|
|
157
|
+
pkg.dependencies = {
|
|
158
|
+
...(pkg.dependencies || {}),
|
|
159
|
+
...featureDeps.dependencies,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (featureDeps.devDependencies) {
|
|
164
|
+
pkg.devDependencies = {
|
|
165
|
+
...(pkg.devDependencies || {}),
|
|
166
|
+
...featureDeps.devDependencies,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
171
|
+
};
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import { FeatureMetadata } from '../types';
|
|
4
|
+
import { getFeaturesPath } from '../constants/paths';
|
|
5
|
+
|
|
6
|
+
export interface RegisteredFeature {
|
|
7
|
+
name: string;
|
|
8
|
+
metadata: FeatureMetadata;
|
|
9
|
+
path: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class FeatureRegistry {
|
|
13
|
+
private features: Map<string, RegisteredFeature> = new Map();
|
|
14
|
+
private readonly featuresBaseDir: string;
|
|
15
|
+
|
|
16
|
+
constructor() {
|
|
17
|
+
this.featuresBaseDir = getFeaturesPath();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Load and register all available features from the features directory
|
|
22
|
+
*/
|
|
23
|
+
async discoverFeatures(): Promise<void> {
|
|
24
|
+
const featuresDir = this.featuresBaseDir;
|
|
25
|
+
|
|
26
|
+
if (!(await fs.pathExists(featuresDir))) {
|
|
27
|
+
console.warn(`Features directory not found at ${featuresDir}`);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const entries = await fs.readdir(featuresDir);
|
|
32
|
+
|
|
33
|
+
for (const entry of entries) {
|
|
34
|
+
const featurePath = path.join(featuresDir, entry);
|
|
35
|
+
const stat = await fs.stat(featurePath);
|
|
36
|
+
|
|
37
|
+
if (!stat.isDirectory() || entry === 'src') continue;
|
|
38
|
+
|
|
39
|
+
await this.registerFeature(entry, featurePath);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Register a single feature by loading its metadata
|
|
45
|
+
*/
|
|
46
|
+
private async registerFeature(
|
|
47
|
+
name: string,
|
|
48
|
+
featurePath: string,
|
|
49
|
+
): Promise<void> {
|
|
50
|
+
const metadataPath = path.join(featurePath, 'metadata.json');
|
|
51
|
+
|
|
52
|
+
if (!(await fs.pathExists(metadataPath))) {
|
|
53
|
+
console.warn(`Metadata not found for feature: ${name}`);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const metadata: FeatureMetadata = await fs.readJson(metadataPath);
|
|
59
|
+
this.features.set(name, {
|
|
60
|
+
name,
|
|
61
|
+
metadata,
|
|
62
|
+
path: featurePath,
|
|
63
|
+
});
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.error(`Failed to load metadata for feature ${name}:`, error);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get a registered feature by name
|
|
71
|
+
*/
|
|
72
|
+
getFeature(name: string): RegisteredFeature | undefined {
|
|
73
|
+
return this.features.get(name);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get metadata for a feature
|
|
78
|
+
*/
|
|
79
|
+
getFeatureMetadata(name: string): FeatureMetadata | undefined {
|
|
80
|
+
return this.features.get(name)?.metadata;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get path to a feature directory
|
|
85
|
+
*/
|
|
86
|
+
getFeaturePath(name: string): string | undefined {
|
|
87
|
+
return this.features.get(name)?.path;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get all registered features
|
|
92
|
+
*/
|
|
93
|
+
getAllFeatures(): RegisteredFeature[] {
|
|
94
|
+
return Array.from(this.features.values());
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Check if a feature is registered
|
|
99
|
+
*/
|
|
100
|
+
hasFeature(name: string): boolean {
|
|
101
|
+
return this.features.has(name);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get features that support a specific framework
|
|
106
|
+
*/
|
|
107
|
+
getFeaturesByFramework(framework: string): RegisteredFeature[] {
|
|
108
|
+
return Array.from(this.features.values()).filter(
|
|
109
|
+
(f) =>
|
|
110
|
+
!f.metadata.frameworks ||
|
|
111
|
+
f.metadata.frameworks.includes(framework as any),
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get features that support a specific architecture
|
|
117
|
+
*/
|
|
118
|
+
getFeaturesByArchitecture(architecture: string): RegisteredFeature[] {
|
|
119
|
+
return Array.from(this.features.values()).filter(
|
|
120
|
+
(f) =>
|
|
121
|
+
!f.metadata.architectures ||
|
|
122
|
+
f.metadata.architectures.includes(architecture as any),
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Get features that support a specific database
|
|
128
|
+
*/
|
|
129
|
+
getFeaturesByDatabase(database: string): RegisteredFeature[] {
|
|
130
|
+
return Array.from(this.features.values()).filter(
|
|
131
|
+
(f) =>
|
|
132
|
+
!f.metadata.databases || f.metadata.databases.includes(database as any),
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Get all features compatible with given config
|
|
138
|
+
*/
|
|
139
|
+
getCompatibleFeatures(
|
|
140
|
+
framework: string,
|
|
141
|
+
architecture: string,
|
|
142
|
+
database: string,
|
|
143
|
+
): RegisteredFeature[] {
|
|
144
|
+
return Array.from(this.features.values()).filter(
|
|
145
|
+
(f) =>
|
|
146
|
+
(!f.metadata.frameworks ||
|
|
147
|
+
f.metadata.frameworks.includes(framework as any)) &&
|
|
148
|
+
(!f.metadata.architectures ||
|
|
149
|
+
f.metadata.architectures.includes(architecture as any)) &&
|
|
150
|
+
(!f.metadata.databases ||
|
|
151
|
+
f.metadata.databases.includes(database as any)),
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Validate that requested features are available and compatible
|
|
157
|
+
*/
|
|
158
|
+
validateFeatures(
|
|
159
|
+
featureNames: string[],
|
|
160
|
+
framework: string,
|
|
161
|
+
architecture: string,
|
|
162
|
+
database: string,
|
|
163
|
+
): {
|
|
164
|
+
valid: boolean;
|
|
165
|
+
errors: string[];
|
|
166
|
+
} {
|
|
167
|
+
const errors: string[] = [];
|
|
168
|
+
|
|
169
|
+
for (const featureName of featureNames) {
|
|
170
|
+
const feature = this.getFeature(featureName);
|
|
171
|
+
|
|
172
|
+
if (!feature) {
|
|
173
|
+
errors.push(`Feature "${featureName}" not found in registry`);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const { metadata } = feature;
|
|
178
|
+
|
|
179
|
+
if (
|
|
180
|
+
metadata.frameworks &&
|
|
181
|
+
!metadata.frameworks.includes(framework as any)
|
|
182
|
+
) {
|
|
183
|
+
errors.push(
|
|
184
|
+
`Feature "${featureName}" does not support framework "${framework}"`,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (
|
|
189
|
+
metadata.architectures &&
|
|
190
|
+
!metadata.architectures.includes(architecture as any)
|
|
191
|
+
) {
|
|
192
|
+
errors.push(
|
|
193
|
+
`Feature "${featureName}" does not support architecture "${architecture}"`,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (metadata.databases && !metadata.databases.includes(database as any)) {
|
|
198
|
+
errors.push(
|
|
199
|
+
`Feature "${featureName}" does not support database "${database}"`,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
valid: errors.length === 0,
|
|
206
|
+
errors,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Create and return a singleton instance of the feature registry
|
|
213
|
+
*/
|
|
214
|
+
let registryInstance: FeatureRegistry | null = null;
|
|
215
|
+
|
|
216
|
+
export async function getFeatureRegistry(): Promise<FeatureRegistry> {
|
|
217
|
+
if (!registryInstance) {
|
|
218
|
+
registryInstance = new FeatureRegistry();
|
|
219
|
+
await registryInstance.discoverFeatures();
|
|
220
|
+
}
|
|
221
|
+
return registryInstance;
|
|
222
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { GeneratorConfig } from '../types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolves selected feature names from the generator configuration.
|
|
5
|
+
* Currently, it simply returns the features selected by the user.
|
|
6
|
+
* In the future, this can handle dependencies between features.
|
|
7
|
+
*/
|
|
8
|
+
export const resolveFeatures = (config: GeneratorConfig): string[] => {
|
|
9
|
+
if (!config.features || config.features.length === 0) {
|
|
10
|
+
return [];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Filter out 'none' if present
|
|
14
|
+
const selectedFeatures = config.features.filter(
|
|
15
|
+
(feature) => feature !== 'none',
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
return [...new Set(selectedFeatures)];
|
|
19
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
export const copyFile = async (src: string, dest: string): Promise<void> => {
|
|
5
|
+
await fs.ensureDir(path.dirname(dest));
|
|
6
|
+
await fs.copy(src, dest);
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const writeRenderedFile = async (
|
|
10
|
+
dest: string,
|
|
11
|
+
content: string,
|
|
12
|
+
): Promise<void> => {
|
|
13
|
+
await fs.ensureDir(path.dirname(dest));
|
|
14
|
+
await fs.writeFile(dest, content, 'utf-8');
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const getAllFiles = async (dirPath: string): Promise<string[]> => {
|
|
18
|
+
const files: string[] = [];
|
|
19
|
+
const items = await fs.readdir(dirPath, { withFileTypes: true });
|
|
20
|
+
|
|
21
|
+
for (const item of items) {
|
|
22
|
+
const fullPath = path.join(dirPath, item.name);
|
|
23
|
+
if (item.isDirectory()) {
|
|
24
|
+
files.push(...(await getAllFiles(fullPath)));
|
|
25
|
+
} else {
|
|
26
|
+
files.push(fullPath);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return files;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const exists = async (path: string): Promise<boolean> => {
|
|
34
|
+
return fs.pathExists(path);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const replaceInFile = async (
|
|
38
|
+
filePath: string,
|
|
39
|
+
searchValue: string | RegExp,
|
|
40
|
+
replaceValue: string,
|
|
41
|
+
): Promise<void> => {
|
|
42
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
43
|
+
const updatedContent = content.replace(searchValue, replaceValue);
|
|
44
|
+
await fs.writeFile(filePath, updatedContent, 'utf-8');
|
|
45
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
import { GeneratorConfig } from '../types';
|
|
5
|
+
import { resolveTemplatePath } from '../resolvers/templateResolver';
|
|
6
|
+
import { getAllFiles, writeRenderedFile, copyFile } from '../filesystem';
|
|
7
|
+
import {
|
|
8
|
+
renderTemplate,
|
|
9
|
+
isTemplateFile,
|
|
10
|
+
getTargetFileName,
|
|
11
|
+
} from '../template-engine';
|
|
12
|
+
import { resolveInstaller } from '../installers';
|
|
13
|
+
import { initializeGit } from '../git';
|
|
14
|
+
import { setupEnvironmentFiles } from '../environment';
|
|
15
|
+
import { applyFeatures } from '../features/apply-features';
|
|
16
|
+
|
|
17
|
+
export interface GeneratorOptions {
|
|
18
|
+
assetsRoot?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const generateProject = async (
|
|
22
|
+
config: GeneratorConfig,
|
|
23
|
+
options: GeneratorOptions = {},
|
|
24
|
+
): Promise<void> => {
|
|
25
|
+
const targetDir = path.join(process.cwd(), config.projectName);
|
|
26
|
+
|
|
27
|
+
if (await fs.pathExists(targetDir)) {
|
|
28
|
+
throw new Error(`Target directory ${config.projectName} already exists.`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
console.log(chalk.cyan(`\nšļø Generating project: ${config.projectName}...`));
|
|
32
|
+
|
|
33
|
+
const templatePath = await resolveTemplatePath(config, options.assetsRoot);
|
|
34
|
+
const files = await getAllFiles(templatePath);
|
|
35
|
+
|
|
36
|
+
// Prepare rendering data (UPPERCASE_SNAKE_CASE as per Phase 2)
|
|
37
|
+
const renderData = {
|
|
38
|
+
PROJECT_NAME: config.projectName,
|
|
39
|
+
PORT: 3000, // Default port for now
|
|
40
|
+
PACKAGE_MANAGER: config.packageManager,
|
|
41
|
+
NODE_ENV: 'development',
|
|
42
|
+
DATABASE: config.database,
|
|
43
|
+
ORM: config.orm,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// 1. Generate Files
|
|
47
|
+
for (const file of files) {
|
|
48
|
+
const relativePath = path.relative(templatePath, file);
|
|
49
|
+
|
|
50
|
+
if (
|
|
51
|
+
relativePath.includes('node_modules') ||
|
|
52
|
+
relativePath.includes('dist')
|
|
53
|
+
) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const targetFilePath = path.join(
|
|
58
|
+
targetDir,
|
|
59
|
+
getTargetFileName(relativePath),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
if (isTemplateFile(file)) {
|
|
63
|
+
const renderedContent = await renderTemplate(file, renderData);
|
|
64
|
+
await writeRenderedFile(targetFilePath, renderedContent);
|
|
65
|
+
} else {
|
|
66
|
+
await copyFile(file, targetFilePath);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 2. Setup Environment Files (.env)
|
|
71
|
+
await setupEnvironmentFiles(targetDir);
|
|
72
|
+
|
|
73
|
+
// 3. Apply Modular Features
|
|
74
|
+
await applyFeatures(targetDir, config, {
|
|
75
|
+
renderData,
|
|
76
|
+
assetsRoot: options.assetsRoot,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// 4. Install Dependencies
|
|
80
|
+
const installer = resolveInstaller(config.packageManager);
|
|
81
|
+
await installer.install(targetDir);
|
|
82
|
+
|
|
83
|
+
// 5. Initialize Git
|
|
84
|
+
await initializeGit(targetDir);
|
|
85
|
+
|
|
86
|
+
console.log(
|
|
87
|
+
chalk.green(
|
|
88
|
+
`\nā
Project ${config.projectName} generated successfully at ${targetDir}`,
|
|
89
|
+
),
|
|
90
|
+
);
|
|
91
|
+
};
|
package/src/git/index.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import execa from 'execa';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
|
|
4
|
+
export const initializeGit = async (projectPath: string): Promise<void> => {
|
|
5
|
+
console.log(chalk.cyan('\ngit Initializing git repository...'));
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
await execa('git', ['init'], { cwd: projectPath });
|
|
9
|
+
await execa('git', ['add', '.'], { cwd: projectPath });
|
|
10
|
+
await execa('git', ['commit', '-m', 'Initial commit from MonStack CLI'], {
|
|
11
|
+
cwd: projectPath,
|
|
12
|
+
});
|
|
13
|
+
console.log(chalk.green('ā
Git initialized successfully.'));
|
|
14
|
+
} catch (error) {
|
|
15
|
+
console.log(
|
|
16
|
+
chalk.yellow(
|
|
17
|
+
'ā ļø Failed to initialize git repository. This is non-blocking.',
|
|
18
|
+
),
|
|
19
|
+
);
|
|
20
|
+
// Non-blocking as per requirements
|
|
21
|
+
}
|
|
22
|
+
};
|
package/src/index.ts
ADDED