@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/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,3 @@
1
+ export * from './registry';
2
+ export * from './resolver';
3
+ export * from './apply-features';
@@ -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
+ };
@@ -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
@@ -0,0 +1,3 @@
1
+ export * from './types';
2
+ export * from './generator/generateProject';
3
+ export * from './features';