wlmaker 1.0.0 → 1.0.2

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.
Files changed (2) hide show
  1. package/dist/cli.mjs +266 -13
  2. package/package.json +4 -2
package/dist/cli.mjs CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { Command } from "commander";
5
+ import chalk3 from "chalk";
5
6
 
6
7
  // src/core/create-bloc.ts
7
8
  import * as fs3 from "fs";
@@ -90,17 +91,17 @@ function hasBuildRunner(projectRoot) {
90
91
  return /build_runner/.test(pubspec);
91
92
  }
92
93
  function runBuildRunner(projectRoot) {
93
- return new Promise((resolve2) => {
94
+ return new Promise((resolve3) => {
94
95
  const child = spawn(
95
96
  "dart",
96
97
  ["run", "build_runner", "build", "--delete-conflicting-outputs"],
97
98
  { cwd: projectRoot, stdio: "inherit" }
98
99
  );
99
100
  child.on("close", (code) => {
100
- resolve2();
101
+ resolve3();
101
102
  });
102
103
  child.on("error", () => {
103
- resolve2();
104
+ resolve3();
104
105
  });
105
106
  });
106
107
  }
@@ -110,16 +111,13 @@ var SNAKE_CASE_REGEX = /^[a-z][a-z0-9_]*$/;
110
111
  async function createBloc(name, options) {
111
112
  const targetDir = path3.resolve(options.dir);
112
113
  if (!name || name.trim().length === 0) {
113
- console.error(chalk.red("Error: Name cannot be empty."));
114
- process.exit(1);
114
+ throw new Error("Name cannot be empty.");
115
115
  }
116
116
  if (!SNAKE_CASE_REGEX.test(name)) {
117
- console.error(chalk.red("Error: Name must be snake_case (lowercase letters, digits, underscores)."));
118
- process.exit(1);
117
+ throw new Error("Name must be snake_case (lowercase letters, digits, underscores).");
119
118
  }
120
119
  if (fs3.existsSync(path3.join(targetDir, name))) {
121
- console.error(chalk.red(`Error: Directory "${name}" already exists.`));
122
- process.exit(1);
120
+ throw new Error(`Directory "${name}" already exists.`);
123
121
  }
124
122
  const pascal = pascalCase(name);
125
123
  const blocDir = path3.join(targetDir, name);
@@ -141,15 +139,270 @@ async function createBloc(name, options) {
141
139
  }
142
140
  }
143
141
  } catch (error) {
144
- console.error(chalk.red(`Failed to create BLoC: ${error}`));
145
- process.exit(1);
142
+ throw new Error(`Failed to create BLoC: ${error}`);
143
+ }
144
+ }
145
+
146
+ // src/interactive.ts
147
+ import * as fs5 from "fs";
148
+ import * as os from "os";
149
+ import * as path5 from "path";
150
+ import * as clack from "@clack/prompts";
151
+ import chalk2 from "chalk";
152
+
153
+ // src/core/project-analyzer.ts
154
+ import * as fs4 from "fs";
155
+ import * as path4 from "path";
156
+ import YAML from "yaml";
157
+ function analyzeProject(startDir) {
158
+ const projectRoot = findPubspecDir2(startDir);
159
+ if (!projectRoot) return null;
160
+ const pubspecPath = path4.join(projectRoot, "pubspec.yaml");
161
+ const content = fs4.readFileSync(pubspecPath, "utf8");
162
+ const pubspec = YAML.parse(content);
163
+ const deps = {
164
+ ...pubspec?.dependencies,
165
+ ...pubspec?.dev_dependencies
166
+ };
167
+ const features = discoverFeatures(projectRoot);
168
+ return {
169
+ projectRoot,
170
+ projectName: pubspec?.name ?? path4.basename(projectRoot),
171
+ hasFreezed: "freezed" in deps || "freezed_annotation" in deps,
172
+ hasBloc: "flutter_bloc" in deps || "bloc" in deps,
173
+ hasBuildRunner: "build_runner" in deps,
174
+ features
175
+ };
176
+ }
177
+ function findPubspecDir2(startDir) {
178
+ let dir = startDir;
179
+ while (dir !== path4.dirname(dir)) {
180
+ if (fs4.existsSync(path4.join(dir, "pubspec.yaml"))) {
181
+ return dir;
182
+ }
183
+ dir = path4.dirname(dir);
184
+ }
185
+ return void 0;
186
+ }
187
+ function discoverFeatures(projectRoot) {
188
+ const featuresDir = path4.join(projectRoot, "lib", "features");
189
+ if (!fs4.existsSync(featuresDir)) return [];
190
+ return fs4.readdirSync(featuresDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort();
191
+ }
192
+ function findMonorepoRoot(startDir) {
193
+ let dir = startDir;
194
+ while (dir !== path4.dirname(dir)) {
195
+ if (fs4.existsSync(path4.join(dir, "melos.yaml"))) {
196
+ return dir;
197
+ }
198
+ dir = path4.dirname(dir);
199
+ }
200
+ return void 0;
201
+ }
202
+ function discoverPackages(monorepoRoot) {
203
+ const packageDirs = ["packages", "packages/features"];
204
+ const projects = [];
205
+ for (const base of packageDirs) {
206
+ const baseDir = path4.join(monorepoRoot, base);
207
+ if (!fs4.existsSync(baseDir)) continue;
208
+ const entries = fs4.readdirSync(baseDir, { withFileTypes: true });
209
+ for (const entry of entries) {
210
+ if (!entry.isDirectory()) continue;
211
+ const candidate = path4.join(baseDir, entry.name);
212
+ if (!fs4.existsSync(path4.join(candidate, "pubspec.yaml"))) continue;
213
+ const project = analyzeProject(candidate);
214
+ if (project && (project.hasFreezed || project.hasBloc)) {
215
+ projects.push(project);
216
+ }
217
+ }
218
+ }
219
+ return projects.sort((a, b) => a.projectName.localeCompare(b.projectName));
220
+ }
221
+ var IGNORED_DIRS = /* @__PURE__ */ new Set([
222
+ "node_modules",
223
+ ".dart_tool",
224
+ "build",
225
+ ".git",
226
+ ".idea",
227
+ ".fvm",
228
+ "coverage"
229
+ ]);
230
+ function discoverProjects(searchDir, maxDepth = 2) {
231
+ const projects = [];
232
+ function walk(dir, depth) {
233
+ if (depth > maxDepth) return;
234
+ const project = analyzeProject(dir);
235
+ if (project && (project.hasFreezed || project.hasBloc)) {
236
+ projects.push(project);
237
+ return;
238
+ }
239
+ if (!fs4.existsSync(dir)) return;
240
+ let entries;
241
+ try {
242
+ entries = fs4.readdirSync(dir, { withFileTypes: true });
243
+ } catch {
244
+ return;
245
+ }
246
+ for (const entry of entries) {
247
+ if (!entry.isDirectory() || IGNORED_DIRS.has(entry.name)) continue;
248
+ walk(path4.join(dir, entry.name), depth + 1);
249
+ }
250
+ }
251
+ walk(searchDir, 0);
252
+ return projects.sort((a, b) => a.projectName.localeCompare(b.projectName));
253
+ }
254
+
255
+ // src/interactive.ts
256
+ var SNAKE_CASE_REGEX2 = /^[a-z][a-z0-9_]*$/;
257
+ async function resolveProject() {
258
+ const s = clack.spinner();
259
+ s.start("Analyzing current directory...");
260
+ const cwdProject = analyzeProject(process.cwd());
261
+ if (cwdProject && (cwdProject.hasFreezed || cwdProject.hasBloc)) {
262
+ s.stop(`Found ${chalk2.green(cwdProject.projectName)}`);
263
+ return cwdProject;
264
+ }
265
+ s.message("Looking for Melos monorepo...");
266
+ const monorepoRoot = findMonorepoRoot(process.cwd());
267
+ if (monorepoRoot) {
268
+ const packages = discoverPackages(monorepoRoot);
269
+ if (packages.length > 0) {
270
+ s.stop(`Found monorepo with ${packages.length} feature package(s)`);
271
+ return selectPackage(packages);
272
+ }
273
+ }
274
+ s.message("Scanning for Flutter projects...");
275
+ const homeDev = path5.join(os.homedir(), "Development");
276
+ if (fs5.existsSync(homeDev)) {
277
+ const projects = discoverProjects(homeDev, 2);
278
+ if (projects.length > 0) {
279
+ s.stop(`Found ${projects.length} Flutter project(s)`);
280
+ return selectPackage(projects);
281
+ }
282
+ }
283
+ s.stop("No Flutter projects found");
284
+ clack.outro(chalk2.red("Could not find any Flutter project with freezed or flutter_bloc."));
285
+ return null;
286
+ }
287
+ async function selectPackage(projects) {
288
+ if (projects.length === 1) {
289
+ clack.log.info(`Using ${chalk2.green(projects[0].projectName)}`);
290
+ return projects[0];
291
+ }
292
+ const selected = await clack.select({
293
+ message: "Select a package",
294
+ options: projects.map((p) => ({
295
+ value: p,
296
+ label: p.projectName,
297
+ hint: path5.relative(os.homedir(), p.projectRoot)
298
+ }))
299
+ });
300
+ if (clack.isCancel(selected)) {
301
+ clack.cancel("Cancelled");
302
+ return null;
303
+ }
304
+ return selected;
305
+ }
306
+ async function interactiveMode() {
307
+ clack.intro(chalk2.bgCyan(chalk2.black(" wlmaker ")));
308
+ const project = await resolveProject();
309
+ if (!project) return;
310
+ const name = await clack.text({
311
+ message: "BLoC name (snake_case)",
312
+ placeholder: "e.g. user_login",
313
+ validate: (value) => {
314
+ if (!value.trim()) return "Name is required";
315
+ if (!SNAKE_CASE_REGEX2.test(value)) return "Must be snake_case (lowercase, digits, underscores)";
316
+ }
317
+ });
318
+ if (clack.isCancel(name)) {
319
+ clack.cancel("Cancelled");
320
+ return;
321
+ }
322
+ let targetDir;
323
+ if (project.features.length > 0) {
324
+ const feature = await clack.select({
325
+ message: "Select feature",
326
+ options: [
327
+ ...project.features.map((f) => ({ value: f, label: f })),
328
+ { value: "__custom__", label: "Custom path..." }
329
+ ]
330
+ });
331
+ if (clack.isCancel(feature)) {
332
+ clack.cancel("Cancelled");
333
+ return;
334
+ }
335
+ if (feature === "__custom__") {
336
+ const customPath = await clack.text({
337
+ message: "Target directory path",
338
+ placeholder: "lib/features/auth",
339
+ validate: (v) => {
340
+ if (!v.trim()) return "Path is required";
341
+ }
342
+ });
343
+ if (clack.isCancel(customPath)) {
344
+ clack.cancel("Cancelled");
345
+ return;
346
+ }
347
+ targetDir = path5.resolve(customPath);
348
+ } else {
349
+ targetDir = path5.join(project.projectRoot, "lib", "features", feature);
350
+ }
351
+ } else {
352
+ clack.note("No lib/features/ directory found. Provide a target path manually.", "Info");
353
+ const customPath = await clack.text({
354
+ message: "Target directory path",
355
+ placeholder: "lib/features/auth",
356
+ validate: (v) => {
357
+ if (!v.trim()) return "Path is required";
358
+ }
359
+ });
360
+ if (clack.isCancel(customPath)) {
361
+ clack.cancel("Cancelled");
362
+ return;
363
+ }
364
+ targetDir = path5.resolve(customPath);
365
+ }
366
+ const defaultRun = project.hasBuildRunner;
367
+ const runBuildRunner2 = await clack.confirm({
368
+ message: "Run build_runner after generation?",
369
+ initialValue: defaultRun
370
+ });
371
+ if (clack.isCancel(runBuildRunner2)) {
372
+ clack.cancel("Cancelled");
373
+ return;
374
+ }
375
+ const genSpinner = clack.spinner();
376
+ genSpinner.start("Generating BLoC files...");
377
+ try {
378
+ await createBloc(name, {
379
+ dir: targetDir,
380
+ buildRunner: runBuildRunner2
381
+ });
382
+ genSpinner.stop("BLoC generated");
383
+ clack.outro(chalk2.green("Done!"));
384
+ } catch (error) {
385
+ genSpinner.stop("Failed");
386
+ clack.outro(chalk2.red(`Error: ${error}`));
146
387
  }
147
388
  }
148
389
 
149
390
  // src/cli.ts
150
391
  var program = new Command();
151
392
  program.name("wlmaker").description("Create Flutter BLoCs with Freezed sealed classes from the terminal").version("1.0.0");
152
- program.command("bloc").description("Create a new BLoC with Freezed sealed classes").argument("<name>", "BLoC name in snake_case (e.g. user_login)").option("-d, --dir <path>", "target directory", process.cwd()).option("--no-build-runner", "skip build_runner execution").action(async (name, options) => {
153
- await createBloc(name, options);
393
+ program.command("bloc").description("Create a new BLoC with Freezed sealed classes").argument("[name]", "BLoC name in snake_case (e.g. user_login)").option("-d, --dir <path>", "target directory", process.cwd()).option("--no-build-runner", "skip build_runner execution").action(async (name, options) => {
394
+ if (!name) {
395
+ await interactiveMode();
396
+ return;
397
+ }
398
+ try {
399
+ await createBloc(name, options);
400
+ } catch (error) {
401
+ console.error(chalk3.red(`Error: ${error}`));
402
+ process.exit(1);
403
+ }
404
+ });
405
+ program.action(async () => {
406
+ await interactiveMode();
154
407
  });
155
408
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wlmaker",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Create Flutter BLoCs with Freezed sealed classes from the terminal",
5
5
  "keywords": [
6
6
  "flutter",
@@ -26,9 +26,11 @@
26
26
  "dev": "tsup --watch"
27
27
  },
28
28
  "dependencies": {
29
+ "@clack/prompts": "^1.2.0",
29
30
  "chalk": "^5.3.0",
30
31
  "change-case": "^5.4.0",
31
- "commander": "^12.0.0"
32
+ "commander": "^12.0.0",
33
+ "yaml": "^2.8.3"
32
34
  },
33
35
  "devDependencies": {
34
36
  "@types/node": "^22.0.0",