generator-madge-capture 1.0.4
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/.editorconfig +11 -0
- package/.eslintignore +3 -0
- package/README.md +12 -0
- package/generators/app/index.js +314 -0
- package/generators/app/lib/extractComponents.js +201 -0
- package/generators/app/lib/prompts.js +63 -0
- package/generators/app/lib/saveMadgeReports.js +43 -0
- package/generators/app/templates/postcss.config.js +6 -0
- package/generators/app/templates/sandbox/.storybook/main.js +13 -0
- package/generators/app/templates/sandbox/.storybook/preview.js +18 -0
- package/generators/app/templates/sandbox/Component.stories.jsx +18 -0
- package/generators/app/templates/sandbox/README.md +19 -0
- package/generators/app/templates/sandbox/index.html +21 -0
- package/generators/app/templates/sandbox/package.json +22 -0
- package/generators/app/templates/sandbox/vite.config.js +22 -0
- package/package.json +63 -0
package/.editorconfig
ADDED
package/.eslintignore
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# generator-nextjs-orm-app
|
|
2
|
+
A yeoman generator to scaffold a fully functioning Nextjs app with drizzle orm.
|
|
3
|
+
|
|
4
|
+
This is my first attempt at a serious yeoman generator.
|
|
5
|
+
I based the Next js code from the "Max Programming" Youtube channel which can be found [here](https://www.youtube.com/watch?v=SxuPB-04Tdw "Max Programming")
|
|
6
|
+
|
|
7
|
+
The instructions for installing Yeoman can be found [here](https://yeoman.io/learning/) but basically it's as simple as
|
|
8
|
+
`npm install -g yo`
|
|
9
|
+
|
|
10
|
+
You *should* end up with a simple Next JS app that uses drizzle as the intermediary between the front end and the server, storing the data in a small Sqlite database.
|
|
11
|
+
|
|
12
|
+
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import Generator from "yeoman-generator";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import yosay from "yosay";
|
|
4
|
+
import commandExists from "command-exists";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import madge from "madge";
|
|
7
|
+
import path from "path";
|
|
8
|
+
import fs, { rmSync } from "fs";
|
|
9
|
+
|
|
10
|
+
import { saveMadgeReports } from "./lib/saveMadgeReports.js"; // Remember the .js!
|
|
11
|
+
import {
|
|
12
|
+
openExplorer,
|
|
13
|
+
syncDependencies,
|
|
14
|
+
findCommonBase,
|
|
15
|
+
getSourceVersions,
|
|
16
|
+
} from "./lib/extractComponents.js";
|
|
17
|
+
import { getPrompts } from "./lib/prompts.js";
|
|
18
|
+
import { component } from "0g";
|
|
19
|
+
|
|
20
|
+
export default class extends Generator {
|
|
21
|
+
initializing() {}
|
|
22
|
+
|
|
23
|
+
welcome() {
|
|
24
|
+
this.log("MADGE CAPTURE P R O J E C T");
|
|
25
|
+
}
|
|
26
|
+
async prompting() {
|
|
27
|
+
let msgText =
|
|
28
|
+
"This generator will run MADGE at a given location and save the output.\n\n";
|
|
29
|
+
msgText +=
|
|
30
|
+
"It will create a new folder with the name you specify, and populate it with the necessary madge files.\n";
|
|
31
|
+
this.log(
|
|
32
|
+
yosay(
|
|
33
|
+
chalk.red.bold("Welcome to the Madge Capture Project generator!\n\n") +
|
|
34
|
+
chalk.whiteBright(msgText),
|
|
35
|
+
),
|
|
36
|
+
);
|
|
37
|
+
// Don't really need this as madge is added to
|
|
38
|
+
// dependencies in package.json.
|
|
39
|
+
if (commandExists("madge")) {
|
|
40
|
+
this.log("Madge already installed!");
|
|
41
|
+
} else {
|
|
42
|
+
this.log("Madge not found. Adding to dependencies...");
|
|
43
|
+
}
|
|
44
|
+
const homeDir = os.homedir(); // โ "C:\Users\<USERNAME>"
|
|
45
|
+
const defaultCaptureBase = path.join(homeDir, "madge-capture");
|
|
46
|
+
|
|
47
|
+
this.answers = await this.prompt(getPrompts(defaultCaptureBase));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async writing() {
|
|
51
|
+
const { sourcePath, mode } = this.answers;
|
|
52
|
+
|
|
53
|
+
// Validate existence
|
|
54
|
+
if (!fs.existsSync(sourcePath)) {
|
|
55
|
+
this.log.error(`File not found: ${sourcePath}`);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const componentName = path.parse(sourcePath).name;
|
|
60
|
+
let finalTarget;
|
|
61
|
+
|
|
62
|
+
if (mode === "existing") {
|
|
63
|
+
finalTarget = path.join(
|
|
64
|
+
this.answers.existingProjectPath,
|
|
65
|
+
this.answers.componentSubDir,
|
|
66
|
+
componentName,
|
|
67
|
+
);
|
|
68
|
+
} else {
|
|
69
|
+
finalTarget = path.join(this.answers.outputPath, componentName);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Clean up previous extraction...
|
|
73
|
+
if (fs.existsSync(finalTarget)) {
|
|
74
|
+
this.log(`๐งน Cleaning up old extraction at ${finalTarget}...`);
|
|
75
|
+
// Recursive delete to ensure a fresh start
|
|
76
|
+
rmSync(finalTarget, { recursive: true, force: true });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
this.log(`๐ Analyzing ${componentName}...`);
|
|
80
|
+
// =============================================
|
|
81
|
+
// Run Madge
|
|
82
|
+
// =============================================
|
|
83
|
+
try {
|
|
84
|
+
const res = await madge(sourcePath, {
|
|
85
|
+
baseDir: path.dirname(sourcePath),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const madgeObj = res.obj();
|
|
89
|
+
// 1. Get ALL absolute paths (including the entry file)
|
|
90
|
+
const absoluteList = [path.resolve(sourcePath)];
|
|
91
|
+
// 2. Resolve all unique absolute paths
|
|
92
|
+
const uniquePaths = new Set();
|
|
93
|
+
uniquePaths.add(path.resolve(sourcePath)); // Add the entry file itself
|
|
94
|
+
|
|
95
|
+
Object.entries(madgeObj).forEach(([file, deps]) => {
|
|
96
|
+
const dir = path.dirname(sourcePath);
|
|
97
|
+
absoluteList.push(path.resolve(dir, file));
|
|
98
|
+
deps.forEach((d) => absoluteList.push(path.resolve(dir, d)));
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// 2. Find the Common Ancestor (The "New Horizon")
|
|
102
|
+
const commonBase = findCommonBase(absoluteList);
|
|
103
|
+
this.log(`๐ Common Base identified: ${commonBase}`);
|
|
104
|
+
let relativeComponentPath = path
|
|
105
|
+
.relative(commonBase, sourcePath)
|
|
106
|
+
.replace(/\\/g, "/");
|
|
107
|
+
|
|
108
|
+
// Check if the source was a .js file that we likely renamed to .jsx
|
|
109
|
+
if (relativeComponentPath.endsWith(".js")) {
|
|
110
|
+
const content = fs.readFileSync(sourcePath, "utf8");
|
|
111
|
+
if (/<[A-Z]/.test(content) || /import.*React/i.test(content)) {
|
|
112
|
+
relativeComponentPath = relativeComponentPath.replace(
|
|
113
|
+
/\.js$/,
|
|
114
|
+
".jsx",
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
this.log(
|
|
120
|
+
`๐ Relative Component Path identified: ${relativeComponentPath}`,
|
|
121
|
+
);
|
|
122
|
+
const assetExtensions = [
|
|
123
|
+
".css",
|
|
124
|
+
".scss",
|
|
125
|
+
".sass",
|
|
126
|
+
".svg",
|
|
127
|
+
".png",
|
|
128
|
+
".jpg",
|
|
129
|
+
];
|
|
130
|
+
const expandedList = new Set(absoluteList);
|
|
131
|
+
|
|
132
|
+
// Look for siblings of our JS dependencies
|
|
133
|
+
absoluteList.forEach((filePath) => {
|
|
134
|
+
const dir = path.dirname(filePath);
|
|
135
|
+
const siblings = fs.readdirSync(dir);
|
|
136
|
+
|
|
137
|
+
siblings.forEach((file) => {
|
|
138
|
+
const ext = path.extname(file).toLowerCase();
|
|
139
|
+
if (assetExtensions.includes(ext)) {
|
|
140
|
+
expandedList.add(path.resolve(dir, file));
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const finalCopyList = Array.from(expandedList);
|
|
146
|
+
this.log(
|
|
147
|
+
`๐จ Added ${finalCopyList.length - absoluteList.length} assets (CSS/SVGs) to the queue.`,
|
|
148
|
+
);
|
|
149
|
+
// 3. Sync using the commonBase as the anchor
|
|
150
|
+
// This prevents the ../../ from ever leaving the finalTarget folder
|
|
151
|
+
syncDependencies(this, finalCopyList, commonBase, finalTarget);
|
|
152
|
+
|
|
153
|
+
await saveMadgeReports(res, finalTarget, componentName);
|
|
154
|
+
|
|
155
|
+
this.log(`โ
Reports saved to: ${finalTarget}`);
|
|
156
|
+
// =============================================
|
|
157
|
+
// Populate sandbox components
|
|
158
|
+
// =============================================
|
|
159
|
+
const shouldGenerateTemplates =
|
|
160
|
+
(mode === "new" && this.answers.createSandBox) || mode === "existing";
|
|
161
|
+
|
|
162
|
+
if (shouldGenerateTemplates) {
|
|
163
|
+
this.log(`โ
Copying templates: ${componentName}`);
|
|
164
|
+
const peerDepsToSync = [
|
|
165
|
+
"react",
|
|
166
|
+
"react-dom",
|
|
167
|
+
"react-datepicker",
|
|
168
|
+
"react-select",
|
|
169
|
+
"react-router",
|
|
170
|
+
"prop-types",
|
|
171
|
+
"date-fns",
|
|
172
|
+
"lucide-react",
|
|
173
|
+
"@mui/material",
|
|
174
|
+
"framer-motion",
|
|
175
|
+
"styled-components",
|
|
176
|
+
"lodash",
|
|
177
|
+
"react-hot-toast",
|
|
178
|
+
];
|
|
179
|
+
// Grab the actual versions from your D: drive
|
|
180
|
+
const syncedVersions = getSourceVersions(
|
|
181
|
+
path.dirname(sourcePath),
|
|
182
|
+
peerDepsToSync,
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
// Merge with your defaults (fallback to 18.2.0 if not found)
|
|
186
|
+
const finalDeps = {
|
|
187
|
+
react: syncedVersions["react"] || "^18.2.0",
|
|
188
|
+
"react-dom": syncedVersions["react-dom"] || "^18.2.0",
|
|
189
|
+
...syncedVersions,
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
this.log(
|
|
193
|
+
`๐ฆ Synced ${Object.keys(syncedVersions).length} peer dependencies from source.`,
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
if (mode === "new") {
|
|
197
|
+
// Pass finalDeps to the Template
|
|
198
|
+
this.fs.copyTpl(
|
|
199
|
+
this.templatePath("sandbox/package.json"),
|
|
200
|
+
path.join(finalTarget, "package.json"),
|
|
201
|
+
{
|
|
202
|
+
componentName,
|
|
203
|
+
dependenciesJSON: JSON.stringify(finalDeps, null, 2).replace(
|
|
204
|
+
/\n/g,
|
|
205
|
+
"\n ",
|
|
206
|
+
),
|
|
207
|
+
},
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
this.fs.copyTpl(
|
|
211
|
+
this.templatePath("sandbox/vite.config.js"),
|
|
212
|
+
path.join(finalTarget, "vite.config.js"),
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
this.fs.copyTpl(
|
|
216
|
+
this.templatePath("sandbox/index.html"),
|
|
217
|
+
path.join(finalTarget, "index.html"),
|
|
218
|
+
{ componentName, relativeComponentPath },
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
// Define the storybook config directory
|
|
222
|
+
const sbConfigDir = path.join(finalTarget, ".storybook");
|
|
223
|
+
|
|
224
|
+
// Copy main.js
|
|
225
|
+
this.fs.copyTpl(
|
|
226
|
+
this.templatePath("sandbox/.storybook/main.js"),
|
|
227
|
+
path.join(sbConfigDir, "main.js"),
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
// Copy preview.js
|
|
231
|
+
this.fs.copyTpl(
|
|
232
|
+
this.templatePath("sandbox/.storybook/preview.js"),
|
|
233
|
+
path.join(sbConfigDir, "preview.js"),
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
// Readme.md file
|
|
237
|
+
this.fs.copyTpl(
|
|
238
|
+
this.templatePath("sandbox/README.md"),
|
|
239
|
+
path.join(finalTarget, "README.md"),
|
|
240
|
+
{
|
|
241
|
+
componentName,
|
|
242
|
+
sourcePath: this.answers.sourcePath, // The D: drive path
|
|
243
|
+
commonBase, // The anchor point
|
|
244
|
+
relativeComponentPath,
|
|
245
|
+
},
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
this.fs.copyTpl(
|
|
250
|
+
this.templatePath("sandbox/Component.stories.jsx"),
|
|
251
|
+
path.join(finalTarget, `${componentName}.stories.jsx`),
|
|
252
|
+
{
|
|
253
|
+
componentName,
|
|
254
|
+
relativeComponentPath, // Point to the extracted location
|
|
255
|
+
},
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
await this.fs.commit(); // Forces Yeoman to write templates to disk NOW
|
|
259
|
+
}
|
|
260
|
+
} catch (err) {
|
|
261
|
+
this.log.error(`Failed to process reports: ${err.message}`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
async install() {
|
|
265
|
+
if (this.answers.mode === "new" && this.answers.createSandBox) {
|
|
266
|
+
console.log("Installing dependencies, please wait...");
|
|
267
|
+
const componentName = path.parse(this.answers.sourcePath).name;
|
|
268
|
+
const finalTarget = path.join(this.answers.outputPath, componentName);
|
|
269
|
+
|
|
270
|
+
this.log(`\n๐ฆ Running npm install in ${finalTarget}...`);
|
|
271
|
+
|
|
272
|
+
// We use spawnSync to ensure it finishes before the 'end' phase
|
|
273
|
+
this.spawnSync("npm", ["install"], {
|
|
274
|
+
cwd: finalTarget,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
async end() {
|
|
279
|
+
const componentName = path.parse(this.answers.sourcePath).name;
|
|
280
|
+
let finalPath;
|
|
281
|
+
if (this.answers.mode === "existing") {
|
|
282
|
+
finalPath = path.join(
|
|
283
|
+
this.answers.existingProjectPath,
|
|
284
|
+
this.answers.componentSubDir,
|
|
285
|
+
componentName,
|
|
286
|
+
);
|
|
287
|
+
} else {
|
|
288
|
+
finalPath = path.join(this.answers.outputPath, componentName);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
this.log("\n" + "=".repeat(40));
|
|
292
|
+
this.log("๐ EXTRACTION COMPLETE!");
|
|
293
|
+
this.log("=".repeat(40));
|
|
294
|
+
this.log(`๐ Location: ${finalPath}`);
|
|
295
|
+
this.log("=".repeat(40));
|
|
296
|
+
|
|
297
|
+
if (this.answers.mode === "new" && this.answers.createSandBox) {
|
|
298
|
+
this.log(`\nTo start your component, run:`);
|
|
299
|
+
this.log(`1. cd "${finalPath}"`);
|
|
300
|
+
this.log(`2. npm run storybook <-- View component in isolation`);
|
|
301
|
+
this.log(`3. npm run dev <-- View raw Vite app`);
|
|
302
|
+
} else if (this.answers.mode === "existing") {
|
|
303
|
+
this.log(`\nComponent added to existing project.`);
|
|
304
|
+
this.log(`You may need to install missing dependencies manually.`);
|
|
305
|
+
} else if (this.answers.mode === "dependency_only") {
|
|
306
|
+
this.log(`To view your component:`);
|
|
307
|
+
this.log(`โ cd "${finalPath}"`);
|
|
308
|
+
this.log(`You may need to install missing dependencies manually.`);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// open the folder for the user if requested
|
|
312
|
+
if (this.answers.openExplorer) openExplorer(finalPath);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import { exec } from "child_process";
|
|
4
|
+
|
|
5
|
+
export const extractComponent = (
|
|
6
|
+
generator,
|
|
7
|
+
madgeObj,
|
|
8
|
+
sourceFile,
|
|
9
|
+
targetDir,
|
|
10
|
+
) => {
|
|
11
|
+
const sourceRoot = path.dirname(sourceFile);
|
|
12
|
+
|
|
13
|
+
// 1. Get all unique file paths from Madge (keys and values)
|
|
14
|
+
const allFiles = new Set([sourceFile]);
|
|
15
|
+
Object.entries(madgeObj).forEach(([file, deps]) => {
|
|
16
|
+
// Madge paths are relative to the sourceFile directory
|
|
17
|
+
allFiles.add(path.resolve(sourceRoot, file));
|
|
18
|
+
deps.forEach((dep) => allFiles.add(path.resolve(sourceRoot, dep)));
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
generator.log(`Found ${allFiles.size} total files to extract.`);
|
|
22
|
+
|
|
23
|
+
// 2. Copy files and maintain structure
|
|
24
|
+
allFiles.forEach((absolutePath) => {
|
|
25
|
+
if (fs.existsSync(absolutePath) && fs.lstatSync(absolutePath).isFile()) {
|
|
26
|
+
// Calculate where it should go in the target
|
|
27
|
+
// We want to keep the relative relationship it had with the sourceRoot
|
|
28
|
+
const relativeToRoot = path.relative(sourceRoot, absolutePath);
|
|
29
|
+
const destination = path.join(targetDir, relativeToRoot);
|
|
30
|
+
|
|
31
|
+
// Use Yeoman's file system (standard fs works too for external paths)
|
|
32
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
33
|
+
fs.copyFileSync(absolutePath, destination);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const getAbsoluteFiles = (dependencyJson, sourceFile) => {
|
|
39
|
+
const sourceDir = path.dirname(sourceFile);
|
|
40
|
+
const uniquePaths = new Set();
|
|
41
|
+
|
|
42
|
+
Object.entries(dependencyJson).forEach(([key, deps]) => {
|
|
43
|
+
// 1. Resolve the 'key' (the file itself)
|
|
44
|
+
// path.resolve automatically handles those ../../ and gives a clean D:\ path
|
|
45
|
+
uniquePaths.add(path.resolve(sourceDir, key));
|
|
46
|
+
|
|
47
|
+
// 2. Resolve every dependency listed for that file
|
|
48
|
+
deps.forEach((dep) => {
|
|
49
|
+
uniquePaths.add(path.resolve(sourceDir, dep));
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return Array.from(uniquePaths);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Copies dependency files while preserving folder structure.
|
|
58
|
+
* @param {Generator} gen - The Yeoman generator instance for logging
|
|
59
|
+
* @param {string[]} absolutePaths - Array of resolved absolute source paths
|
|
60
|
+
* @param {string} sourceRoot - The directory of the entry component (D:\...\Form)
|
|
61
|
+
* @param {string} targetDir - The destination root (C:\...\madge-capture\Form)
|
|
62
|
+
*/
|
|
63
|
+
export const syncDependencies = (gen, absolutePaths, sourceRoot, targetDir) => {
|
|
64
|
+
let copiedCount = 0;
|
|
65
|
+
let missingCount = 0;
|
|
66
|
+
|
|
67
|
+
absolutePaths.forEach((srcPath) => {
|
|
68
|
+
try {
|
|
69
|
+
// Check if file exists
|
|
70
|
+
if (!fs.existsSync(srcPath)) {
|
|
71
|
+
gen.log.error(`File missing (skipped): ${srcPath}`);
|
|
72
|
+
missingCount++;
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Determine relative path from the source component
|
|
77
|
+
// e.g., if sourceRoot is .../Form and srcPath is .../utils/date.js
|
|
78
|
+
// relativePart will be "../../utils/date.js"
|
|
79
|
+
const relativePart = path.relative(sourceRoot, srcPath);
|
|
80
|
+
|
|
81
|
+
// Create the final destination path
|
|
82
|
+
let destPath = path.join(targetDir, relativePart);
|
|
83
|
+
let content = fs.readFileSync(srcPath, "utf8");
|
|
84
|
+
let isReactFile = false;
|
|
85
|
+
|
|
86
|
+
// --- Extension Logic ---
|
|
87
|
+
if (srcPath.endsWith(".js")) {
|
|
88
|
+
isReactFile =
|
|
89
|
+
/import.*React/i.test(content) ||
|
|
90
|
+
/<[A-Z]/.test(content) ||
|
|
91
|
+
/return\s*\(/.test(content);
|
|
92
|
+
if (isReactFile) {
|
|
93
|
+
destPath = destPath.replace(/\.js$/, ".jsx");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// The Regex Import Renamer
|
|
97
|
+
// This regex looks for:
|
|
98
|
+
// - Strings starting with 'from' or 'import'
|
|
99
|
+
// - Followed by a quote (' or ")
|
|
100
|
+
// - Followed by a relative path (./ or ../)
|
|
101
|
+
// - Ending with .js before the closing quote
|
|
102
|
+
const importRegex = /(from|import)\s+(['"])((\.\.?\/)+.*)\.js(['"])/g;
|
|
103
|
+
// We only perform the replacement on JS/JSX files
|
|
104
|
+
if (srcPath.match(/\.(js|jsx)$/)) {
|
|
105
|
+
content = content.replace(importRegex, (match, p1, p2, p3, p4, p5) => {
|
|
106
|
+
// We check if the file being imported is one of the ones we are extracting
|
|
107
|
+
// For simplicity, we assume if it's a relative import, it needs the .jsx swap
|
|
108
|
+
return `${p1} ${p2}${p3}.jsx${p5}`;
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
// Ensure destination folder exists
|
|
112
|
+
const destFolder = path.dirname(destPath);
|
|
113
|
+
if (!fs.existsSync(destFolder)) {
|
|
114
|
+
fs.mkdirSync(destFolder, { recursive: true });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Perform the copy
|
|
118
|
+
// We use writeFileSync instead of copyFileSync to save our modified content
|
|
119
|
+
fs.writeFileSync(destPath, content);
|
|
120
|
+
copiedCount++;
|
|
121
|
+
} catch (err) {
|
|
122
|
+
gen.log.error(`Failed to copy ${srcPath}: ${err.message}`);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
gen.log(`\nFinal Sync Report:`);
|
|
127
|
+
gen.log(`โ
Copied: ${copiedCount}`);
|
|
128
|
+
if (missingCount > 0) gen.log(`โ ๏ธ Missing: ${missingCount}`);
|
|
129
|
+
};
|
|
130
|
+
/**
|
|
131
|
+
* Finds the shortest common parent directory for an array of absolute paths.
|
|
132
|
+
*/
|
|
133
|
+
export const findCommonBase = (files) => {
|
|
134
|
+
if (files.length === 0) return "";
|
|
135
|
+
|
|
136
|
+
// Split paths into segments
|
|
137
|
+
const splitPaths = files.map((f) => f.split(path.sep));
|
|
138
|
+
let common = splitPaths[0];
|
|
139
|
+
|
|
140
|
+
for (let i = 1; i < splitPaths.length; i++) {
|
|
141
|
+
let j = 0;
|
|
142
|
+
while (
|
|
143
|
+
j < common.length &&
|
|
144
|
+
j < splitPaths[i].length &&
|
|
145
|
+
common[j] === splitPaths[i][j]
|
|
146
|
+
) {
|
|
147
|
+
j++;
|
|
148
|
+
}
|
|
149
|
+
common = common.slice(0, j);
|
|
150
|
+
}
|
|
151
|
+
return common.join(path.sep);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Finds the nearest package.json and extracts versions for requested deps
|
|
156
|
+
* @param {string} startPath - Directory to start searching from
|
|
157
|
+
* @param {string[]} depNames - Array of package names to find
|
|
158
|
+
* @returns {Object} - Key/Value pair of package names and versions
|
|
159
|
+
*/
|
|
160
|
+
export const getSourceVersions = (startPath, depNames) => {
|
|
161
|
+
let currentDir = startPath;
|
|
162
|
+
let foundPath = null;
|
|
163
|
+
|
|
164
|
+
// 1. Climb up the tree to find the nearest package.json
|
|
165
|
+
while (currentDir !== path.parse(currentDir).root) {
|
|
166
|
+
const checkPath = path.join(currentDir, "package.json");
|
|
167
|
+
if (fs.existsSync(checkPath)) {
|
|
168
|
+
foundPath = checkPath;
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
currentDir = path.dirname(currentDir);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (!foundPath) return {};
|
|
175
|
+
|
|
176
|
+
const pkg = JSON.parse(fs.readFileSync(foundPath, "utf8"));
|
|
177
|
+
const allAvailable = { ...pkg.devDependencies, ...pkg.dependencies };
|
|
178
|
+
|
|
179
|
+
const results = {};
|
|
180
|
+
depNames.forEach((name) => {
|
|
181
|
+
if (allAvailable[name]) {
|
|
182
|
+
results[name] = allAvailable[name];
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
return results;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Opens a folder in the native OS file explorer
|
|
191
|
+
* @param {string} folderPath
|
|
192
|
+
*/
|
|
193
|
+
export const openExplorer = (folderPath) => {
|
|
194
|
+
// On Windows, 'explorer' is the command.
|
|
195
|
+
// We wrap the path in quotes to handle spaces in folder names.
|
|
196
|
+
exec(`explorer "${folderPath}"`, (err) => {
|
|
197
|
+
if (err) {
|
|
198
|
+
console.error(`Could not open explorer: ${err.message}`);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export const getPrompts = (defaultCaptureBase) => [
|
|
2
|
+
{
|
|
3
|
+
type: "input",
|
|
4
|
+
name: "sourcePath",
|
|
5
|
+
message: "Enter the location of the component:",
|
|
6
|
+
default: "D:\\Web.Application\\React\\Components\\Form.js",
|
|
7
|
+
store: true,
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
type: "list",
|
|
11
|
+
name: "mode",
|
|
12
|
+
message: "How do you want to export this component?",
|
|
13
|
+
choices: [
|
|
14
|
+
{ name: "Create a new Storybook Project", value: "new" },
|
|
15
|
+
{ name: "Add to an existing Storybook Project", value: "existing" },
|
|
16
|
+
{
|
|
17
|
+
name: "Create component dependency folder and files only",
|
|
18
|
+
value: "dependency_only",
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
default: "new",
|
|
22
|
+
store: true,
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
type: "input",
|
|
26
|
+
name: "outputPath",
|
|
27
|
+
message: "Where do you want to save the output files?",
|
|
28
|
+
default: defaultCaptureBase,
|
|
29
|
+
when: (answers) =>
|
|
30
|
+
answers.mode === "new" || answers.mode === "dependency_only",
|
|
31
|
+
store: true,
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
type: "input",
|
|
35
|
+
name: "existingProjectPath",
|
|
36
|
+
message: "Enter the root path of your existing project:",
|
|
37
|
+
when: (answers) => answers.mode === "existing",
|
|
38
|
+
store: true,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
type: "input",
|
|
42
|
+
name: "componentSubDir",
|
|
43
|
+
message: "Sub-directory for the component (relative to project root):",
|
|
44
|
+
default: "src/components",
|
|
45
|
+
when: (answers) => answers.mode === "existing",
|
|
46
|
+
store: true,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
type: "confirm",
|
|
50
|
+
name: "createSandBox",
|
|
51
|
+
message: "Create sandbox files to run in StoryBoard?",
|
|
52
|
+
default: true,
|
|
53
|
+
when: (answers) => answers.mode === "new",
|
|
54
|
+
store: true,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
type: "confirm",
|
|
58
|
+
name: "openExplorer",
|
|
59
|
+
message: "Open explorer to show copied files?",
|
|
60
|
+
default: true,
|
|
61
|
+
store: true,
|
|
62
|
+
},
|
|
63
|
+
];
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generates and saves both JSON and Markdown dependency reports.
|
|
6
|
+
* @param {Object} res - The result object from madge()
|
|
7
|
+
* @param {string} outputDir - Directory to save files
|
|
8
|
+
* @param {string} baseName - Filename without extension
|
|
9
|
+
*/
|
|
10
|
+
export const saveMadgeReports = async (res, outputDir, baseName) => {
|
|
11
|
+
// 1. Generate Markdown Content
|
|
12
|
+
const deps = res.obj();
|
|
13
|
+
const circular = res.circular();
|
|
14
|
+
const date = new Date().toLocaleDateString();
|
|
15
|
+
|
|
16
|
+
let markdown = `# Dependency Report: ${baseName}\n`;
|
|
17
|
+
markdown += `*Generated on ${date}*\n\n`;
|
|
18
|
+
markdown += `## Summary\n* **Total Files:** ${Object.keys(deps).length}\n`;
|
|
19
|
+
markdown += `* **Circular Dependencies:** ${circular.length > 0 ? `โ ๏ธ ${circular.length}` : "โ
None"}\n\n`;
|
|
20
|
+
|
|
21
|
+
markdown += `## Dependency Details\n| File | Depends On |\n| :--- | :--- |\n`;
|
|
22
|
+
Object.entries(deps).forEach(([file, childDeps]) => {
|
|
23
|
+
const depList =
|
|
24
|
+
childDeps.length > 0
|
|
25
|
+
? childDeps.map((d) => `\`${d}\``).join(", ")
|
|
26
|
+
: "_None_";
|
|
27
|
+
markdown += `| \`${file}\` | ${depList} |\n`;
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// 2. Prepare Paths
|
|
31
|
+
if (!fs.existsSync(outputDir)) {
|
|
32
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const jsonPath = path.join(outputDir, `${baseName}.json`);
|
|
36
|
+
const mdPath = path.join(outputDir, `${baseName}.md`);
|
|
37
|
+
|
|
38
|
+
// 3. Write Files
|
|
39
|
+
fs.writeFileSync(jsonPath, JSON.stringify(deps, null, 2));
|
|
40
|
+
fs.writeFileSync(mdPath, markdown);
|
|
41
|
+
|
|
42
|
+
return { jsonPath, mdPath };
|
|
43
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** @type { import('@storybook/react-vite').StorybookConfig } */
|
|
2
|
+
const config = {
|
|
3
|
+
stories: ["../**/*.stories.@(js|jsx|mjs|ts|tsx)"],
|
|
4
|
+
addons: ["@storybook/addon-essentials"],
|
|
5
|
+
framework: {
|
|
6
|
+
name: "@storybook/react-vite",
|
|
7
|
+
options: {},
|
|
8
|
+
},
|
|
9
|
+
docs: {
|
|
10
|
+
autodocs: "tag",
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
export default config;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** @type { import('@storybook/react').Preview } */
|
|
2
|
+
const preview = {
|
|
3
|
+
parameters: {
|
|
4
|
+
actions: { argTypesRegex: "^on[A-Z].*" },
|
|
5
|
+
controls: {
|
|
6
|
+
matchers: {
|
|
7
|
+
color: /(background|color)$/i,
|
|
8
|
+
date: /Date$/i,
|
|
9
|
+
},
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// If you have a global CSS file you've extracted,
|
|
15
|
+
// you would import it here:
|
|
16
|
+
// import '../global.css';
|
|
17
|
+
|
|
18
|
+
export default preview;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import Component from "./<%= relativeComponentPath %>";
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
title: "Extracted/<%= componentName %>",
|
|
5
|
+
component: Component,
|
|
6
|
+
tags: ["autodocs"], // This tells Storybook to build the prop table automatically
|
|
7
|
+
argTypes: {
|
|
8
|
+
// You can manually override prop controls here if needed
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const Primary = {
|
|
13
|
+
args: {
|
|
14
|
+
// You can set default prop values here
|
|
15
|
+
// label: 'Click Me',
|
|
16
|
+
// primary: true,
|
|
17
|
+
},
|
|
18
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# ๐งช Sandbox: <%= componentName %>
|
|
2
|
+
|
|
3
|
+
This is an isolated sandbox for the `<%= componentName %>` component, extracted from the main monorepo for testing and documentation.
|
|
4
|
+
|
|
5
|
+
## ๐ Source Information
|
|
6
|
+
- **Original Path:** `<%= sourcePath %>`
|
|
7
|
+
- **Extracted On:** <%= new Date().toLocaleString() %>
|
|
8
|
+
- **Common Base Anchor:** `<%= commonBase %>`
|
|
9
|
+
|
|
10
|
+
## ๐ Features
|
|
11
|
+
- **Vite:** Fast HMR development environment.
|
|
12
|
+
- **Storybook 8:** Interactive component documentation and testing.
|
|
13
|
+
- **Automatic JSX Conversion:** Legacy `.js` files have been converted to `.jsx`.
|
|
14
|
+
|
|
15
|
+
## ๐ Getting Started
|
|
16
|
+
|
|
17
|
+
To start the development server:
|
|
18
|
+
```bash
|
|
19
|
+
npm run dev
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Sandbox: <%= componentName %></title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module">
|
|
11
|
+
import React from 'react';
|
|
12
|
+
import ReactDOM from 'react-dom/client';
|
|
13
|
+
// We dynamically point this to the extracted component
|
|
14
|
+
import Component from './<%= relativeComponentPath %>';
|
|
15
|
+
|
|
16
|
+
ReactDOM.createRoot(document.getElementById('root')).render(
|
|
17
|
+
React.createElement(Component)
|
|
18
|
+
);
|
|
19
|
+
</script>
|
|
20
|
+
</body>
|
|
21
|
+
</html>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sandbox-<%= componentName %>",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "vite build",
|
|
9
|
+
"preview": "vite preview",
|
|
10
|
+
"storybook": "storybook dev -p 6006",
|
|
11
|
+
"build-storybook": "storybook build"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": <%- dependenciesJSON %>,
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@vitejs/plugin-react": "^4.0.0",
|
|
16
|
+
"vite": "^4.4.0",
|
|
17
|
+
"storybook": "^8.0.0",
|
|
18
|
+
"@storybook/react": "^8.0.0",
|
|
19
|
+
"@storybook/react-vite": "^8.0.0",
|
|
20
|
+
"@storybook/addon-essentials": "^8.0.0"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { defineConfig } from "vite";
|
|
2
|
+
import react from "@vitejs/plugin-react";
|
|
3
|
+
import path from "path";
|
|
4
|
+
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
plugins: [react()],
|
|
7
|
+
optimizeDeps: {
|
|
8
|
+
// Force Vite to pre-bundle these even if it's confused by the imports
|
|
9
|
+
include: ["react-hot-toast", "prop-types"],
|
|
10
|
+
},
|
|
11
|
+
resolve: {
|
|
12
|
+
alias: {
|
|
13
|
+
// If your monorepo uses aliases (e.g. '@utils'),
|
|
14
|
+
// you can map them here to the folders you extracted.
|
|
15
|
+
// '@utils': path.resolve(__dirname, './utils'),
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
server: {
|
|
19
|
+
open: true, // Automatically open the browser
|
|
20
|
+
port: 3000,
|
|
21
|
+
},
|
|
22
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "generator-madge-capture",
|
|
3
|
+
"version": "1.0.4",
|
|
4
|
+
"description": "Capture madge output for a given file",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Tony Edwards",
|
|
7
|
+
"url": "https://github.com/vomoir/generator-madge-capture"
|
|
8
|
+
},
|
|
9
|
+
"main": "index.js",
|
|
10
|
+
"type": "module",
|
|
11
|
+
"module": "dist/react-lib.esm.js",
|
|
12
|
+
"jsnext:main": "dist/react-lib.esm.js",
|
|
13
|
+
"scripts": {
|
|
14
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"yeoman-generator",
|
|
18
|
+
"yeoman",
|
|
19
|
+
"generator",
|
|
20
|
+
"inception",
|
|
21
|
+
"init",
|
|
22
|
+
"create",
|
|
23
|
+
"boilerplate"
|
|
24
|
+
],
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/vomoir/generator-madge-capture.git"
|
|
28
|
+
},
|
|
29
|
+
"license": "ISC",
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"chalk": "^4.0.0",
|
|
32
|
+
"command-exists": "^1.2.9",
|
|
33
|
+
"yeoman-generator": "^6.0.0",
|
|
34
|
+
"yosay": "^2.0.2",
|
|
35
|
+
"madge": "^8.0.0"
|
|
36
|
+
},
|
|
37
|
+
"eslintConfig": {
|
|
38
|
+
"extends": [
|
|
39
|
+
"xo-space/esnext",
|
|
40
|
+
"prettier"
|
|
41
|
+
],
|
|
42
|
+
"env": {
|
|
43
|
+
"jest": true,
|
|
44
|
+
"node": true
|
|
45
|
+
},
|
|
46
|
+
"rules": {
|
|
47
|
+
"prettier/prettier": [
|
|
48
|
+
"error",
|
|
49
|
+
{
|
|
50
|
+
"singleQuote": true,
|
|
51
|
+
"printWidth": 90
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
},
|
|
55
|
+
"plugins": [
|
|
56
|
+
"prettier"
|
|
57
|
+
]
|
|
58
|
+
},
|
|
59
|
+
"bugs": {
|
|
60
|
+
"url": "https://github.com/vomoir/generator-madge-capture/issues"
|
|
61
|
+
},
|
|
62
|
+
"homepage": "https://github.com/vomoir/generator-madge-capture"
|
|
63
|
+
}
|