muscle-config 1.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/LICENSE +21 -0
- package/README.md +430 -0
- package/dist/commands/create.command.js +104 -0
- package/dist/config/projectConfig.js +1 -0
- package/dist/features/architecture.feature.js +49 -0
- package/dist/features/css.feature.js +107 -0
- package/dist/features/feature.interface.js +1 -0
- package/dist/features/git.feature.js +52 -0
- package/dist/features/mui.feature.js +138 -0
- package/dist/features/prettier.feature.js +73 -0
- package/dist/features/tailwind.feature.js +116 -0
- package/dist/generators/architecture.generator.js +75 -0
- package/dist/generators/css-plain.generator.js +215 -0
- package/dist/generators/css.generator.js +45 -0
- package/dist/generators/mui.generator.js +176 -0
- package/dist/generators/react.generator.js +38 -0
- package/dist/generators/tailwind.config.generator.js +17 -0
- package/dist/generators/toggle.generator.js +26 -0
- package/dist/index.js +11 -0
- package/dist/prompts/architecture.prompt.js +131 -0
- package/dist/prompts/css.prompt.js +217 -0
- package/dist/prompts/directory.prompt.js +16 -0
- package/dist/prompts/framework.prompt.js +15 -0
- package/dist/prompts/git.prompt.js +23 -0
- package/dist/prompts/mui.prompt.js +104 -0
- package/dist/prompts/prettier.prompt.js +42 -0
- package/dist/prompts/projectName.prompt.js +13 -0
- package/dist/prompts/styling.prompt.js +17 -0
- package/dist/prompts/tailwind.prompt.js +175 -0
- package/dist/templates/react/css/App.jsx +41 -0
- package/dist/templates/react/css/App.tsx +41 -0
- package/dist/templates/react/mui/App.jsx +42 -0
- package/dist/templates/react/mui/App.tsx +42 -0
- package/dist/templates/react/tailwind-v4/index.css +20 -0
- package/dist/templates/react/tailwind-v4/vite.config.js +7 -0
- package/dist/templates/react/tailwind-v4/vite.config.ts +7 -0
- package/dist/utils/directory.js +43 -0
- package/dist/utils/install.js +7 -0
- package/dist/utils/logger.js +10 -0
- package/dist/utils/rollback.js +63 -0
- package/dist/utils/spinner.js +13 -0
- package/dist/utils/welcome.js +46 -0
- package/package.json +57 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
import { spinner } from "../utils/spinner.js";
|
|
5
|
+
import { logger } from "../utils/logger.js";
|
|
6
|
+
import { generateCssReset, generateCssVariables, generateCssTypography, generateCssIndex, generateCssIndexWithImports, generateCssThemeToggle, } from "../generators/css-plain.generator.js";
|
|
7
|
+
import { saveSnapshot, restoreSnapshots, rollbackFeature, } from "../utils/rollback.js";
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
export function cssFeature(config) {
|
|
10
|
+
return {
|
|
11
|
+
name: "css",
|
|
12
|
+
async run(projectPath) {
|
|
13
|
+
try {
|
|
14
|
+
if (config.mode === "skip") {
|
|
15
|
+
logger.info("Skipping CSS configuration — using Vite default.");
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* STEP 1
|
|
20
|
+
* Detect TypeScript or JavaScript
|
|
21
|
+
*/
|
|
22
|
+
const tsConfigPath = path.join(projectPath, "vite.config.ts");
|
|
23
|
+
let isTypeScript = false;
|
|
24
|
+
try {
|
|
25
|
+
await fs.access(tsConfigPath);
|
|
26
|
+
isTypeScript = true;
|
|
27
|
+
}
|
|
28
|
+
catch { }
|
|
29
|
+
/**
|
|
30
|
+
* STEP 2
|
|
31
|
+
* Generate CSS files — separate files or single file mode
|
|
32
|
+
*/
|
|
33
|
+
if (config.separateFiles) {
|
|
34
|
+
spinner.start("Generating CSS files...");
|
|
35
|
+
const stylesDir = path.join(projectPath, "src/styles");
|
|
36
|
+
await fs.mkdir(stylesDir, { recursive: true });
|
|
37
|
+
if (config.reset) {
|
|
38
|
+
await fs.writeFile(path.join(stylesDir, "reset.css"), generateCssReset());
|
|
39
|
+
}
|
|
40
|
+
await fs.writeFile(path.join(stylesDir, "variables.css"), generateCssVariables(config));
|
|
41
|
+
await fs.writeFile(path.join(stylesDir, "typography.css"), generateCssTypography(config));
|
|
42
|
+
spinner.succeed("CSS files generated!");
|
|
43
|
+
/**
|
|
44
|
+
* STEP 3 (separate files)
|
|
45
|
+
* Overwrite src/index.css with @import statements pointing to styles/
|
|
46
|
+
*/
|
|
47
|
+
spinner.start("Updating main CSS entry...");
|
|
48
|
+
const mainCssPath = path.join(projectPath, "src/index.css");
|
|
49
|
+
await saveSnapshot(mainCssPath);
|
|
50
|
+
await fs.writeFile(mainCssPath, generateCssIndexWithImports(config));
|
|
51
|
+
spinner.succeed("Main CSS entry updated!");
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
/**
|
|
55
|
+
* STEP 2 (single file)
|
|
56
|
+
* Overwrite src/index.css with all generated styles inline
|
|
57
|
+
*/
|
|
58
|
+
spinner.start("Generating CSS styles...");
|
|
59
|
+
const mainCssPath = path.join(projectPath, "src/index.css");
|
|
60
|
+
await saveSnapshot(mainCssPath);
|
|
61
|
+
await fs.writeFile(mainCssPath, generateCssIndex(config));
|
|
62
|
+
spinner.succeed("CSS styles generated!");
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* STEP 4
|
|
66
|
+
* Generate ThemeToggle component if class dark mode + toggle selected
|
|
67
|
+
*/
|
|
68
|
+
if (config.darkModeToggle) {
|
|
69
|
+
spinner.start("Creating ThemeToggle component...");
|
|
70
|
+
const componentDir = path.join(projectPath, "src/components");
|
|
71
|
+
await fs.mkdir(componentDir, { recursive: true });
|
|
72
|
+
const toggleFile = path.join(componentDir, isTypeScript ? "ThemeToggle.tsx" : "ThemeToggle.jsx");
|
|
73
|
+
await fs.writeFile(toggleFile, generateCssThemeToggle(isTypeScript));
|
|
74
|
+
spinner.succeed("ThemeToggle component created!");
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* STEP 5
|
|
78
|
+
* Copy demo App template if user selected it
|
|
79
|
+
*/
|
|
80
|
+
if (config.demo) {
|
|
81
|
+
spinner.start("Copying CSS demo template...");
|
|
82
|
+
const templateDir = path.join(__dirname, "../templates/react/css");
|
|
83
|
+
const appTemplatePath = path.join(templateDir, isTypeScript ? "App.tsx" : "App.jsx");
|
|
84
|
+
const appTargetPath = path.join(projectPath, isTypeScript ? "src/App.tsx" : "src/App.jsx");
|
|
85
|
+
await fs.copyFile(appTemplatePath, appTargetPath);
|
|
86
|
+
spinner.succeed("Demo template copied!");
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* FINAL SUCCESS MESSAGE
|
|
90
|
+
*/
|
|
91
|
+
logger.success("CSS setup complete!");
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
spinner.fail("Failed to setup CSS.");
|
|
95
|
+
await restoreSnapshots();
|
|
96
|
+
await rollbackFeature([
|
|
97
|
+
path.join(projectPath, "src/styles"),
|
|
98
|
+
path.join(projectPath, "src/components/ThemeToggle.tsx"),
|
|
99
|
+
path.join(projectPath, "src/components/ThemeToggle.jsx"),
|
|
100
|
+
path.join(projectPath, "src/App.tsx"),
|
|
101
|
+
path.join(projectPath, "src/App.jsx"),
|
|
102
|
+
]);
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { execa } from "execa";
|
|
2
|
+
import { spinner } from "../utils/spinner.js";
|
|
3
|
+
import { logger } from "../utils/logger.js";
|
|
4
|
+
import { rollbackFeature } from "../utils/rollback.js";
|
|
5
|
+
import path from "path";
|
|
6
|
+
export function gitFeature(config) {
|
|
7
|
+
return {
|
|
8
|
+
name: "git",
|
|
9
|
+
async run(projectPath) {
|
|
10
|
+
try {
|
|
11
|
+
if (!config.init) {
|
|
12
|
+
logger.info("Skipping Git initialization.");
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* STEP 1
|
|
17
|
+
* Initialize a new Git repository in the project folder
|
|
18
|
+
*/
|
|
19
|
+
spinner.start("Initializing Git repository...");
|
|
20
|
+
await execa("git", ["init"], { cwd: projectPath });
|
|
21
|
+
spinner.succeed("Git repository initialized!");
|
|
22
|
+
if (!config.initialCommit)
|
|
23
|
+
return;
|
|
24
|
+
/**
|
|
25
|
+
* STEP 2
|
|
26
|
+
* Stage all generated files
|
|
27
|
+
*/
|
|
28
|
+
spinner.start("Staging files...");
|
|
29
|
+
await execa("git", ["add", "."], { cwd: projectPath });
|
|
30
|
+
spinner.succeed("Files staged!");
|
|
31
|
+
/**
|
|
32
|
+
* STEP 3
|
|
33
|
+
* Create the initial commit with a hardcoded conventional message
|
|
34
|
+
*/
|
|
35
|
+
spinner.start("Creating initial commit...");
|
|
36
|
+
await execa("git", ["commit", "-m", "chore: initial commit"], {
|
|
37
|
+
cwd: projectPath,
|
|
38
|
+
});
|
|
39
|
+
spinner.succeed("Initial commit created!");
|
|
40
|
+
/**
|
|
41
|
+
* FINAL SUCCESS MESSAGE
|
|
42
|
+
*/
|
|
43
|
+
logger.success("Git setup complete!");
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
spinner.fail("Failed to setup Git.");
|
|
47
|
+
await rollbackFeature([path.join(projectPath, ".git")]);
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { execa } from "execa";
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import { spinner } from "../utils/spinner.js";
|
|
6
|
+
import { logger } from "../utils/logger.js";
|
|
7
|
+
import { generateMuiTheme, generateMuiThemeToggle, generateThemeContextProvider, generateAppMuiThemeProvider, generateMuiMain, generateMuiIndexCss, } from "../generators/mui.generator.js";
|
|
8
|
+
import { restoreSnapshots, rollbackFeature, saveSnapshot, } from "../utils/rollback.js";
|
|
9
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
export function muiFeature(config) {
|
|
11
|
+
return {
|
|
12
|
+
name: "mui",
|
|
13
|
+
async run(projectPath) {
|
|
14
|
+
try {
|
|
15
|
+
/**
|
|
16
|
+
* STEP 1
|
|
17
|
+
* Detect TypeScript or JavaScript
|
|
18
|
+
*/
|
|
19
|
+
const tsConfigPath = path.join(projectPath, "vite.config.ts");
|
|
20
|
+
let isTypeScript = false;
|
|
21
|
+
try {
|
|
22
|
+
await fs.access(tsConfigPath);
|
|
23
|
+
isTypeScript = true;
|
|
24
|
+
}
|
|
25
|
+
catch { }
|
|
26
|
+
/**
|
|
27
|
+
* STEP 2
|
|
28
|
+
* Install MUI core + emotion peer deps
|
|
29
|
+
*/
|
|
30
|
+
spinner.start("Installing MUI...");
|
|
31
|
+
await execa("npm", ["install", "@mui/material", "@emotion/react", "@emotion/styled"], { cwd: projectPath });
|
|
32
|
+
spinner.succeed("MUI installed!");
|
|
33
|
+
/**
|
|
34
|
+
* STEP 3
|
|
35
|
+
* Install MUI Icons if selected or needed for ThemeToggle
|
|
36
|
+
*/
|
|
37
|
+
if (config.icons || config.darkModeToggle) {
|
|
38
|
+
spinner.start("Installing MUI Icons...");
|
|
39
|
+
await execa("npm", ["install", "@mui/icons-material"], {
|
|
40
|
+
cwd: projectPath,
|
|
41
|
+
});
|
|
42
|
+
spinner.succeed("MUI Icons installed!");
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* STEP 4
|
|
46
|
+
* Generate src/themes/theme.ts or theme.js
|
|
47
|
+
*/
|
|
48
|
+
spinner.start("Generating MUI theme...");
|
|
49
|
+
const themesDir = path.join(projectPath, "src/themes");
|
|
50
|
+
await fs.mkdir(themesDir, { recursive: true });
|
|
51
|
+
const themeFile = path.join(themesDir, isTypeScript ? "theme.ts" : "theme.js");
|
|
52
|
+
await fs.writeFile(themeFile, generateMuiTheme(config, isTypeScript));
|
|
53
|
+
spinner.succeed("MUI theme generated!");
|
|
54
|
+
/**
|
|
55
|
+
* STEP 5
|
|
56
|
+
* Apply font to src/index.css if a font was selected
|
|
57
|
+
*/
|
|
58
|
+
if (config.font !== "none") {
|
|
59
|
+
spinner.start("Applying font...");
|
|
60
|
+
const indexCssPath = path.join(projectPath, "src/index.css");
|
|
61
|
+
await saveSnapshot(indexCssPath);
|
|
62
|
+
await fs.writeFile(indexCssPath, generateMuiIndexCss(config));
|
|
63
|
+
spinner.succeed("Font applied!");
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* STEP 6
|
|
67
|
+
* Generate src/context/ThemeContextProvider
|
|
68
|
+
*/
|
|
69
|
+
spinner.start("Generating ThemeContextProvider...");
|
|
70
|
+
const contextDir = path.join(projectPath, "src/context");
|
|
71
|
+
await fs.mkdir(contextDir, { recursive: true });
|
|
72
|
+
const contextFile = path.join(contextDir, isTypeScript
|
|
73
|
+
? "ThemeContextProvider.tsx"
|
|
74
|
+
: "ThemeContextProvider.jsx");
|
|
75
|
+
await fs.writeFile(contextFile, generateThemeContextProvider(config, isTypeScript));
|
|
76
|
+
spinner.succeed("ThemeContextProvider generated!");
|
|
77
|
+
/**
|
|
78
|
+
* STEP 7
|
|
79
|
+
* Generate src/themes/AppMuiThemeProvider
|
|
80
|
+
*/
|
|
81
|
+
spinner.start("Generating AppMuiThemeProvider...");
|
|
82
|
+
const providerFile = path.join(themesDir, isTypeScript ? "AppMuiThemeProvider.tsx" : "AppMuiThemeProvider.jsx");
|
|
83
|
+
await fs.writeFile(providerFile, generateAppMuiThemeProvider(isTypeScript));
|
|
84
|
+
spinner.succeed("AppMuiThemeProvider generated!");
|
|
85
|
+
/**
|
|
86
|
+
* STEP 8
|
|
87
|
+
* Generate ThemeToggle component if dark mode toggle selected
|
|
88
|
+
*/
|
|
89
|
+
if (config.darkModeToggle) {
|
|
90
|
+
spinner.start("Creating ThemeToggle component...");
|
|
91
|
+
const componentDir = path.join(projectPath, "src/components");
|
|
92
|
+
await fs.mkdir(componentDir, { recursive: true });
|
|
93
|
+
const toggleFile = path.join(componentDir, isTypeScript ? "ThemeToggle.tsx" : "ThemeToggle.jsx");
|
|
94
|
+
await fs.writeFile(toggleFile, generateMuiThemeToggle());
|
|
95
|
+
spinner.succeed("ThemeToggle component created!");
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* STEP 9
|
|
99
|
+
* Overwrite main.tsx / main.jsx with provider composition
|
|
100
|
+
*/
|
|
101
|
+
spinner.start("Configuring main entry point...");
|
|
102
|
+
const mainFile = path.join(projectPath, isTypeScript ? "src/main.tsx" : "src/main.jsx");
|
|
103
|
+
await saveSnapshot(mainFile);
|
|
104
|
+
await fs.writeFile(mainFile, generateMuiMain(isTypeScript));
|
|
105
|
+
spinner.succeed("Main entry point configured!");
|
|
106
|
+
/**
|
|
107
|
+
* STEP 10
|
|
108
|
+
* Copy demo App template if user selected it
|
|
109
|
+
*/
|
|
110
|
+
if (config.demo) {
|
|
111
|
+
spinner.start("Copying MUI demo template...");
|
|
112
|
+
const templateDir = path.join(__dirname, "../templates/react/mui");
|
|
113
|
+
const appTemplatePath = path.join(templateDir, isTypeScript ? "App.tsx" : "App.jsx");
|
|
114
|
+
const appTargetPath = path.join(projectPath, isTypeScript ? "src/App.tsx" : "src/App.jsx");
|
|
115
|
+
await fs.copyFile(appTemplatePath, appTargetPath);
|
|
116
|
+
spinner.succeed("Demo template copied!");
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* FINAL SUCCESS MESSAGE
|
|
120
|
+
*/
|
|
121
|
+
logger.success("MUI added successfully!");
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
spinner.fail("Failed to setup MUI.");
|
|
125
|
+
await restoreSnapshots();
|
|
126
|
+
await rollbackFeature([
|
|
127
|
+
path.join(projectPath, "src/themes"),
|
|
128
|
+
path.join(projectPath, "src/context"),
|
|
129
|
+
path.join(projectPath, "src/components/ThemeToggle.tsx"),
|
|
130
|
+
path.join(projectPath, "src/components/ThemeToggle.jsx"),
|
|
131
|
+
path.join(projectPath, "src/App.tsx"),
|
|
132
|
+
path.join(projectPath, "src/App.jsx"),
|
|
133
|
+
]);
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { spinner } from "../utils/spinner.js";
|
|
4
|
+
import { logger } from "../utils/logger.js";
|
|
5
|
+
import { installPackages } from "../utils/install.js";
|
|
6
|
+
import { rollbackFeature } from "../utils/rollback.js";
|
|
7
|
+
export function prettierFeature(config) {
|
|
8
|
+
return {
|
|
9
|
+
name: "prettier",
|
|
10
|
+
async run(projectPath) {
|
|
11
|
+
try {
|
|
12
|
+
if (!config.enabled) {
|
|
13
|
+
logger.info("Skipping Prettier setup.");
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* STEP 1
|
|
18
|
+
* Install Prettier
|
|
19
|
+
*/
|
|
20
|
+
spinner.start("Installing Prettier...");
|
|
21
|
+
await installPackages(["prettier"], projectPath, true);
|
|
22
|
+
spinner.succeed("Prettier installed!");
|
|
23
|
+
/**
|
|
24
|
+
* STEP 2
|
|
25
|
+
* Build the Prettier config object from user choices
|
|
26
|
+
*/
|
|
27
|
+
const prettierConfig = {
|
|
28
|
+
singleQuote: config.singleQuote,
|
|
29
|
+
semi: config.semi,
|
|
30
|
+
tabWidth: config.tabWidth,
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* STEP 3
|
|
34
|
+
* Check if .prettierrc already exists (Tailwind plugin case)
|
|
35
|
+
* If yes — read it and merge, preserving the plugins array
|
|
36
|
+
* If no — create fresh
|
|
37
|
+
*/
|
|
38
|
+
const prettierrcPath = path.join(projectPath, ".prettierrc");
|
|
39
|
+
let existingConfig = {};
|
|
40
|
+
try {
|
|
41
|
+
const raw = await fs.readFile(prettierrcPath, "utf-8");
|
|
42
|
+
existingConfig = JSON.parse(raw);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// file doesn't exist — start fresh
|
|
46
|
+
}
|
|
47
|
+
const merged = { ...prettierConfig, ...existingConfig };
|
|
48
|
+
spinner.start("Writing .prettierrc...");
|
|
49
|
+
await fs.writeFile(prettierrcPath, JSON.stringify(merged, null, 2));
|
|
50
|
+
spinner.succeed(".prettierrc written!");
|
|
51
|
+
/**
|
|
52
|
+
* STEP 4
|
|
53
|
+
* Create .prettierignore
|
|
54
|
+
*/
|
|
55
|
+
spinner.start("Creating .prettierignore...");
|
|
56
|
+
await fs.writeFile(path.join(projectPath, ".prettierignore"), ["node_modules", "dist", "build", ""].join("\n"));
|
|
57
|
+
spinner.succeed(".prettierignore created!");
|
|
58
|
+
/**
|
|
59
|
+
* FINAL SUCCESS MESSAGE
|
|
60
|
+
*/
|
|
61
|
+
logger.success("Prettier setup complete!");
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
spinner.fail("Failed to setup Prettier.");
|
|
65
|
+
await rollbackFeature([
|
|
66
|
+
path.join(projectPath, ".prettierrc"),
|
|
67
|
+
path.join(projectPath, ".prettierignore"),
|
|
68
|
+
]);
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { execa } from "execa";
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import { spinner } from "../utils/spinner.js";
|
|
6
|
+
import { logger } from "../utils/logger.js";
|
|
7
|
+
import { generateIndexCss } from "../generators/css.generator.js";
|
|
8
|
+
import { generateThemeToggle } from "../generators/toggle.generator.js";
|
|
9
|
+
import { generateTailwindConfig } from "../generators/tailwind.config.generator.js";
|
|
10
|
+
import { restoreSnapshots, rollbackFeature, saveSnapshot, } from "../utils/rollback.js";
|
|
11
|
+
import { installPackages } from "../utils/install.js";
|
|
12
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
export function tailwindFeature(config) {
|
|
14
|
+
return {
|
|
15
|
+
name: "tailwind",
|
|
16
|
+
async run(projectPath) {
|
|
17
|
+
try {
|
|
18
|
+
/**
|
|
19
|
+
* STEP 1
|
|
20
|
+
* Install Tailwind v4 and the Vite plugin
|
|
21
|
+
*/
|
|
22
|
+
spinner.start("Installing Tailwind v4...");
|
|
23
|
+
await execa("npm", ["install", "tailwindcss", "@tailwindcss/vite", "--legacy-peer-deps"], { cwd: projectPath });
|
|
24
|
+
spinner.succeed("Tailwind installed!");
|
|
25
|
+
/**
|
|
26
|
+
* STEP 2
|
|
27
|
+
* Detect TypeScript or JavaScript
|
|
28
|
+
*/
|
|
29
|
+
const tsConfigPath = path.join(projectPath, "vite.config.ts");
|
|
30
|
+
const jsConfigPath = path.join(projectPath, "vite.config.js");
|
|
31
|
+
let viteConfigPath;
|
|
32
|
+
let isTypeScript = false;
|
|
33
|
+
try {
|
|
34
|
+
await fs.access(tsConfigPath);
|
|
35
|
+
viteConfigPath = tsConfigPath;
|
|
36
|
+
isTypeScript = true;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
try {
|
|
40
|
+
await fs.access(jsConfigPath);
|
|
41
|
+
viteConfigPath = jsConfigPath;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
throw new Error("Could not find vite.config.ts or vite.config.js");
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* STEP 3
|
|
49
|
+
* Copy the Vite config template that includes the Tailwind plugin
|
|
50
|
+
*/
|
|
51
|
+
spinner.start("Configuring Vite...");
|
|
52
|
+
const templateDir = path.join(__dirname, "../templates/react/tailwind-v4");
|
|
53
|
+
const viteTemplatePath = path.join(templateDir, isTypeScript ? "vite.config.ts" : "vite.config.js");
|
|
54
|
+
await fs.copyFile(viteTemplatePath, viteConfigPath);
|
|
55
|
+
spinner.succeed("Vite configured!");
|
|
56
|
+
/**
|
|
57
|
+
* STEP 4
|
|
58
|
+
* Generate tailwind.config based on user choices
|
|
59
|
+
*/
|
|
60
|
+
spinner.start("Generating Tailwind config...");
|
|
61
|
+
const tailwindConfigPath = path.join(projectPath, isTypeScript ? "tailwind.config.ts" : "tailwind.config.js");
|
|
62
|
+
await fs.writeFile(tailwindConfigPath, generateTailwindConfig(config));
|
|
63
|
+
spinner.succeed("Tailwind config generated!");
|
|
64
|
+
/**
|
|
65
|
+
* STEP 5
|
|
66
|
+
* Generate src/index.css with font, colors, and dark mode setup
|
|
67
|
+
*/
|
|
68
|
+
spinner.start("Generating Tailwind styles...");
|
|
69
|
+
const cssPath = path.join(projectPath, "src/index.css");
|
|
70
|
+
await saveSnapshot(cssPath);
|
|
71
|
+
await fs.writeFile(cssPath, generateIndexCss(config));
|
|
72
|
+
spinner.succeed("Tailwind styles generated!");
|
|
73
|
+
/**
|
|
74
|
+
* STEP 6
|
|
75
|
+
* Generate ThemeToggle component if user selected dark mode toggle
|
|
76
|
+
*/
|
|
77
|
+
if (config.darkModeToggle) {
|
|
78
|
+
spinner.start("Creating ThemeToggle component...");
|
|
79
|
+
const componentDir = path.join(projectPath, "src/components");
|
|
80
|
+
await fs.mkdir(componentDir, { recursive: true });
|
|
81
|
+
const toggleFile = path.join(componentDir, isTypeScript ? "ThemeToggle.tsx" : "ThemeToggle.jsx");
|
|
82
|
+
await fs.writeFile(toggleFile, generateThemeToggle(isTypeScript));
|
|
83
|
+
spinner.succeed("ThemeToggle component created!");
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* STEP 7
|
|
87
|
+
* Install Prettier + Tailwind class sorting plugin if selected
|
|
88
|
+
*/
|
|
89
|
+
if (config.prettierTailwind) {
|
|
90
|
+
spinner.start("Installing Prettier + Tailwind plugin...");
|
|
91
|
+
await installPackages(["prettier", "prettier-plugin-tailwindcss"], projectPath, true);
|
|
92
|
+
spinner.succeed("Prettier installed!");
|
|
93
|
+
spinner.start("Creating .prettierrc...");
|
|
94
|
+
await fs.writeFile(path.join(projectPath, ".prettierrc"), JSON.stringify({ plugins: ["prettier-plugin-tailwindcss"] }, null, 2));
|
|
95
|
+
spinner.succeed(".prettierrc created!");
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* FINAL SUCCESS MESSAGE
|
|
99
|
+
*/
|
|
100
|
+
logger.success("Tailwind v4 added successfully!");
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
spinner.fail("Failed to setup Tailwind.");
|
|
104
|
+
await restoreSnapshots();
|
|
105
|
+
await rollbackFeature([
|
|
106
|
+
path.join(projectPath, "tailwind.config.ts"),
|
|
107
|
+
path.join(projectPath, "tailwind.config.js"),
|
|
108
|
+
path.join(projectPath, "src/components/ThemeToggle.tsx"),
|
|
109
|
+
path.join(projectPath, "src/components/ThemeToggle.jsx"),
|
|
110
|
+
path.join(projectPath, ".prettierrc"),
|
|
111
|
+
]);
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
// ─── Helpers ───
|
|
4
|
+
async function createFolder(folderPath) {
|
|
5
|
+
await fs.mkdir(folderPath, { recursive: true });
|
|
6
|
+
}
|
|
7
|
+
async function createGitkeep(folderPath) {
|
|
8
|
+
await fs.writeFile(path.join(folderPath, ".gitkeep"), "");
|
|
9
|
+
}
|
|
10
|
+
async function createIndexFile(folderPath, isTypeScript) {
|
|
11
|
+
const filename = isTypeScript ? "index.ts" : "index.js";
|
|
12
|
+
await fs.writeFile(path.join(folderPath, filename), "export {};\n");
|
|
13
|
+
}
|
|
14
|
+
async function folderExists(folderPath) {
|
|
15
|
+
try {
|
|
16
|
+
await fs.access(folderPath);
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// ─── Main generator ───
|
|
24
|
+
export async function generateArchitecture(config, projectPath, isTypeScript) {
|
|
25
|
+
if (config.style === "skip")
|
|
26
|
+
return;
|
|
27
|
+
const srcPath = path.join(projectPath, "src");
|
|
28
|
+
if (config.style === "feature-based") {
|
|
29
|
+
// Create src/features/ folder
|
|
30
|
+
const featuresDir = path.join(srcPath, "features");
|
|
31
|
+
await createFolder(featuresDir);
|
|
32
|
+
// Create each feature with its selected folders
|
|
33
|
+
for (const feature of config.features) {
|
|
34
|
+
const featureDir = path.join(featuresDir, feature);
|
|
35
|
+
await createFolder(featureDir);
|
|
36
|
+
for (const folder of config.featureFolders) {
|
|
37
|
+
const folderPath = path.join(featureDir, folder);
|
|
38
|
+
await createFolder(folderPath);
|
|
39
|
+
if (config.indexFiles.includes(folder)) {
|
|
40
|
+
await createIndexFile(folderPath, isTypeScript);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
await createGitkeep(folderPath);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// Create shared folders in src/ — skip if already exists
|
|
48
|
+
for (const folder of config.sharedFolders) {
|
|
49
|
+
const folderPath = path.join(srcPath, folder);
|
|
50
|
+
if (await folderExists(folderPath))
|
|
51
|
+
continue;
|
|
52
|
+
await createFolder(folderPath);
|
|
53
|
+
if (config.indexFiles.includes(folder)) {
|
|
54
|
+
await createIndexFile(folderPath, isTypeScript);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
await createGitkeep(folderPath);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (config.style === "layered") {
|
|
62
|
+
for (const folder of config.sharedFolders) {
|
|
63
|
+
const folderPath = path.join(srcPath, folder);
|
|
64
|
+
if (await folderExists(folderPath))
|
|
65
|
+
continue;
|
|
66
|
+
await createFolder(folderPath);
|
|
67
|
+
if (config.indexFiles.includes(folder)) {
|
|
68
|
+
await createIndexFile(folderPath, isTypeScript);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
await createGitkeep(folderPath);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|