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 ADDED
@@ -0,0 +1,11 @@
1
+ root = true
2
+
3
+ [*]
4
+ indent_style = space
5
+ indent_size = 2
6
+ charset = utf-8
7
+ trim_trailing_whitespace = true
8
+ insert_final_newline = true
9
+
10
+ [*.md]
11
+ trim_trailing_whitespace = false
package/.eslintignore ADDED
@@ -0,0 +1,3 @@
1
+ node_modules
2
+ coverage
3
+ app/templates
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,6 @@
1
+ module.exports = {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ };
@@ -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
+ }