wlmaker 1.2.9 → 1.2.11

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 +191 -462
  2. package/package.json +1 -1
package/dist/cli.mjs CHANGED
@@ -968,139 +968,49 @@ import * as path8 from "path";
968
968
  import chalk4 from "chalk";
969
969
  import { pascalCase as pascalCase4, camelCase } from "change-case";
970
970
 
971
- // src/core/json-to-dart.ts
972
- function jsonToFields(jsonStr) {
973
- const parsed = JSON.parse(jsonStr);
974
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
975
- throw new Error("Expected a JSON object at the top level");
976
- }
977
- return objectToFields(parsed);
978
- }
979
- function objectToFields(obj) {
980
- const fields = [];
981
- for (const [key, value] of Object.entries(obj)) {
982
- fields.push({
983
- name: key,
984
- dartType: dartTypeOf(value, key),
985
- isNullable: value === null
986
- });
987
- }
988
- return fields;
989
- }
990
- function dartTypeOf(value, nameHint) {
991
- if (value === null) return "dynamic";
992
- if (typeof value === "string") return "String";
993
- if (typeof value === "boolean") return "bool";
994
- if (typeof value === "number") {
995
- return Number.isInteger(value) ? "int" : "double";
996
- }
997
- if (Array.isArray(value)) {
998
- if (value.length === 0) return "List<dynamic>";
999
- const elementType = dartTypeOf(value[0], nameHint);
1000
- return `List<${elementType}>`;
1001
- }
1002
- if (typeof value === "object") {
1003
- return "Map<String, dynamic>";
1004
- }
1005
- return "dynamic";
1006
- }
1007
- function fieldsToConstructorParams(fields, indent = " ") {
1008
- return fields.map((f) => {
1009
- const nullable = f.isNullable ? "?" : "";
1010
- return `${indent}required ${f.dartType}${nullable} ${f.name},`;
1011
- }).join("\n");
1012
- }
1013
- function fieldsToFromJson(fields, modelName) {
1014
- const entries = fields.map((f) => {
1015
- if (f.dartType.startsWith("List<")) {
1016
- return ` ${f.name}: (${_jsonFieldName(f)} as List<dynamic>).map((e) => e as ${_extractGenericType(f.dartType)}).toList(),`;
1017
- }
1018
- return ` ${f.name}: ${_jsonFieldName(f)} as ${f.dartType}${f.isNullable ? "?" : ""},`;
1019
- }).join("\n");
1020
- return `factory ${modelName}.fromJson(Map<String, dynamic> json) => ${modelName}(
1021
- ${entries}
1022
- );`;
1023
- }
1024
- function fieldsToJson(fields, modelName) {
1025
- const entries = fields.map((f) => ` '${f.name}': ${f.name},`).join("\n");
1026
- return `Map<String, dynamic> toJson() => {
1027
- ${entries}
1028
- };`;
1029
- }
1030
- function _jsonFieldName(f) {
1031
- return `json['${f.name}']`;
1032
- }
1033
- function _extractGenericType(listType) {
1034
- const match = listType.match(/^List<(.+)>$/);
1035
- return match ? match[1] : "dynamic";
1036
- }
1037
-
1038
971
  // src/core/endpoint-templates.ts
1039
- function entityTemplate(name, pascal, fields) {
1040
- const params = fieldsToConstructorParams(fields);
1041
- const props = fields.map((f) => {
1042
- const nullable = f.isNullable ? "?" : "";
1043
- return ` final ${f.dartType}${nullable} ${f.name};`;
1044
- }).join("\n");
972
+ function entityBoilerplate(pascal) {
1045
973
  return `class ${pascal}Entity {
1046
- ${props}
974
+ // TODO: Define fields
1047
975
 
1048
- const ${pascal}Entity({
1049
- ${params}
1050
- });
976
+ const ${pascal}Entity();
1051
977
  }
1052
978
  `;
1053
979
  }
1054
- function modelTemplate(name, pascal, fields) {
1055
- const params = fieldsToConstructorParams(fields);
1056
- const props = fields.map((f) => {
1057
- const nullable = f.isNullable ? "?" : "";
1058
- return ` final ${f.dartType}${nullable} ${f.name};`;
1059
- }).join("\n");
1060
- const fromJson = fieldsToFromJson(fields, `${pascal}Model`);
1061
- const toJson = fieldsToJson(fields, `${pascal}Model`);
1062
- return `import 'package:json_annotation/json_annotation.dart';
1063
- import '../../domain/entities/${name}/${name}_entity.dart';
980
+ function modelBoilerplate(name, pascal, projectName) {
981
+ return `import 'package:${projectName}/core.dart';
982
+ import 'package:json_annotation/json_annotation.dart';
1064
983
 
1065
984
  part '${name}_model.g.dart';
1066
985
 
1067
- @JsonSerializable()
1068
- class ${pascal}Model extends ${pascal}Entity {
1069
- const ${pascal}Model({
1070
- ${params}
1071
- }) : super(
1072
- ${fields.map((f) => ` ${f.name}: ${f.name},`).join("\n")}
1073
- );
986
+ @JsonSerializable(createToJson: true)
987
+ class ${pascal}Model {
988
+ // TODO: Define constructor parameters and fields
1074
989
 
1075
- ${fromJson}
1076
-
1077
- ${toJson}
990
+ const ${pascal}Model();
1078
991
 
1079
992
  factory ${pascal}Model.fromJson(Map<String, dynamic> json) =>
1080
993
  _$${pascal}ModelFromJson(json);
1081
994
 
1082
- @override
995
+ ${pascal}Entity toEntity() {
996
+ // TODO: Implement toEntity mapping
997
+ throw UnimplementedError();
998
+ }
999
+
1083
1000
  Map<String, dynamic> toJson() => _$${pascal}ModelToJson(this);
1084
1001
  }
1085
1002
  `;
1086
1003
  }
1087
- function requestModelTemplate(name, pascal, fields) {
1088
- const params = fieldsToConstructorParams(fields);
1089
- const props = fields.map((f) => {
1090
- const nullable = f.isNullable ? "?" : "";
1091
- return ` final ${f.dartType}${nullable} ${f.name};`;
1092
- }).join("\n");
1004
+ function requestModelBoilerplate(name, pascal) {
1093
1005
  return `import 'package:json_annotation/json_annotation.dart';
1094
1006
 
1095
1007
  part '${name}_request_model.g.dart';
1096
1008
 
1097
- @JsonSerializable()
1009
+ @JsonSerializable(createToJson: true)
1098
1010
  class ${pascal}RequestModel {
1099
- ${props}
1011
+ // TODO: Define constructor parameters and fields
1100
1012
 
1101
- ${pascal}RequestModel({
1102
- ${params}
1103
- });
1013
+ const ${pascal}RequestModel();
1104
1014
 
1105
1015
  factory ${pascal}RequestModel.fromJson(Map<String, dynamic> json) =>
1106
1016
  _$${pascal}RequestModelFromJson(json);
@@ -1156,12 +1066,13 @@ function repositoryInterfaceMethod(methodName, returnType, params) {
1156
1066
  const paramList = params.map((p) => `${p.type} ${p.name}`).join(", ");
1157
1067
  return `Future<${returnType}> ${methodName}(${paramList});`;
1158
1068
  }
1159
- function repositoryImplMethod(methodName, returnType, params, datasourceName) {
1069
+ function repositoryImplMethod(methodName, returnType, modelType, params, datasourceName) {
1160
1070
  const paramList = params.map((p) => `${p.type} ${p.name}`).join(", ");
1161
1071
  const args = params.map((p) => p.name).join(", ");
1162
1072
  return `@override
1163
1073
  Future<${returnType}> ${methodName}(${paramList}) async {
1164
- return await ${datasourceName}.${methodName}(${args});
1074
+ final model = await ${datasourceName}.${methodName}(${args});
1075
+ return model.toEntity();
1165
1076
  }`;
1166
1077
  }
1167
1078
 
@@ -1234,102 +1145,108 @@ function injectExport(filePath, exportLine) {
1234
1145
  // src/core/create-endpoint.ts
1235
1146
  async function createEndpoint(options) {
1236
1147
  const lib = path8.join(options.projectRoot, "lib");
1237
- const pascal = pascalCase4(options.responseModelName);
1148
+ const pascal = pascalCase4(options.useCaseName);
1238
1149
  const useCasePascal = pascalCase4(options.useCaseName);
1239
1150
  const useCaseSnake = options.useCaseName;
1240
- const hasRequestBody = ["POST", "PUT", "PATCH"].includes(options.httpMethod) && options.requestFields;
1151
+ const needsBody = ["POST", "PUT", "PATCH"].includes(options.httpMethod);
1152
+ const domain = extractDomain(options.bffApiFile);
1153
+ const datasourceFile = path8.join(lib, "data", "datasources", `${domain}_rest_datasource.dart`);
1154
+ const datasourceClassName = `${pascalCase4(domain)}RestDataSource`;
1155
+ const repositoryInterfaceFile = path8.join(lib, "domain", "repositories", `${domain}_repository.dart`);
1156
+ const repositoryInterfaceName = `${pascalCase4(domain)}Repository`;
1157
+ const repositoryImplFile = path8.join(lib, "data", "repositories", `${domain}_repository_data.dart`);
1158
+ const repositoryImplClassName = `${pascalCase4(domain)}RepositoryData`;
1159
+ const feature = domain;
1241
1160
  const pathParams = extractPathParams(options.endpointPath);
1242
- const methodParams = buildMethodParams(options, pathParams, hasRequestBody);
1161
+ const methodParams = buildMethodParams(options, pathParams, needsBody);
1243
1162
  const methodParamsForSignature = methodParams.map((p) => ({ name: p.name, type: p.type }));
1244
- const returnType = `${pascal}Entity`;
1245
- const repositoryInterfaceSnake = path8.basename(options.repositoryInterfaceFile, ".dart");
1163
+ const modelType = `${pascal}Model`;
1164
+ const entityType = `${pascal}Entity`;
1165
+ const repositoryInterfaceSnake = path8.basename(repositoryInterfaceFile, ".dart");
1246
1166
  const spinner2 = (msg) => console.log(chalk4.cyan(` \u2192 ${msg}`));
1247
- if (options.responseFields && options.responseFields.length > 0) {
1248
- spinner2("Generating entity");
1249
- const entityDir = path8.join(lib, "domain", "entities", options.feature);
1250
- fs9.mkdirSync(entityDir, { recursive: true });
1251
- fs9.writeFileSync(
1252
- path8.join(entityDir, `${options.responseModelName}_entity.dart`),
1253
- entityTemplate(options.responseModelName, pascal, options.responseFields)
1254
- );
1255
- spinner2("Generating model");
1256
- const modelDir = path8.join(lib, "data", "models", options.feature);
1257
- fs9.mkdirSync(modelDir, { recursive: true });
1258
- fs9.writeFileSync(
1259
- path8.join(modelDir, `${options.responseModelName}_model.dart`),
1260
- modelTemplate(options.responseModelName, pascal, options.responseFields)
1261
- );
1262
- }
1263
- if (hasRequestBody && options.requestFields && options.requestModelName) {
1167
+ spinner2("Generating entity");
1168
+ const entityDir = path8.join(lib, "domain", "entities", feature);
1169
+ fs9.mkdirSync(entityDir, { recursive: true });
1170
+ fs9.writeFileSync(
1171
+ path8.join(entityDir, `${useCaseSnake}_entity.dart`),
1172
+ entityBoilerplate(pascal)
1173
+ );
1174
+ spinner2("Generating model");
1175
+ const modelDir = path8.join(lib, "data", "models", feature);
1176
+ fs9.mkdirSync(modelDir, { recursive: true });
1177
+ fs9.writeFileSync(
1178
+ path8.join(modelDir, `${useCaseSnake}_model.dart`),
1179
+ modelBoilerplate(useCaseSnake, pascal, options.projectName)
1180
+ );
1181
+ if (needsBody) {
1264
1182
  spinner2("Generating request model");
1265
- const reqPascal = pascalCase4(options.requestModelName);
1266
- const reqModelDir = path8.join(lib, "data", "models", options.feature);
1183
+ const reqModelDir = path8.join(lib, "data", "models", feature);
1267
1184
  fs9.mkdirSync(reqModelDir, { recursive: true });
1268
1185
  fs9.writeFileSync(
1269
- path8.join(reqModelDir, `${options.requestModelName}_request_model.dart`),
1270
- requestModelTemplate(options.requestModelName, reqPascal, options.requestFields)
1186
+ path8.join(reqModelDir, `${useCaseSnake}_request_model.dart`),
1187
+ requestModelBoilerplate(useCaseSnake, pascal)
1271
1188
  );
1272
1189
  }
1273
1190
  spinner2("Injecting Retrofit method");
1274
1191
  const bffPath = path8.resolve(options.bffApiFile);
1275
1192
  if (fs9.existsSync(bffPath)) {
1276
- const retrofitParams = buildRetrofitParams(options, pathParams, hasRequestBody);
1277
- const retrofitReturnType = `${pascal}Model`;
1193
+ const retrofitParams = buildRetrofitParams(options, pathParams, needsBody);
1278
1194
  const method = retrofitMethod(
1279
1195
  camelCase(options.useCaseName),
1280
1196
  options.endpointPath,
1281
1197
  options.httpMethod,
1282
1198
  retrofitParams,
1283
- retrofitReturnType
1199
+ modelType
1284
1200
  );
1285
1201
  injectMethod(bffPath, extractClassName(bffPath), method);
1286
- const modelImport = `import 'package:${options.feature}/data/models/${options.responseModelName}/${options.responseModelName}_model.dart';`;
1202
+ const modelImport = `import 'package:${options.projectName}/data/models/${feature}/${useCaseSnake}_model.dart';`;
1287
1203
  injectImport(bffPath, modelImport);
1288
1204
  } else {
1289
1205
  console.log(chalk4.yellow(` \u26A0 BFF API file not found: ${bffPath}`));
1290
1206
  }
1291
1207
  spinner2("Injecting datasource method");
1292
- const dsPath = path8.resolve(options.datasourceFile);
1208
+ const dsPath = path8.resolve(datasourceFile);
1293
1209
  if (fs9.existsSync(dsPath)) {
1294
1210
  const dsMethod = datasourceMethod(
1295
1211
  camelCase(options.useCaseName),
1296
- returnType,
1212
+ modelType,
1297
1213
  methodParamsForSignature
1298
1214
  );
1299
- injectMethod(dsPath, options.datasourceClassName, dsMethod);
1215
+ injectMethod(dsPath, datasourceClassName, dsMethod);
1300
1216
  } else {
1301
1217
  console.log(chalk4.yellow(` \u26A0 Datasource file not found: ${dsPath}`));
1302
1218
  }
1303
1219
  spinner2("Injecting repository interface method");
1304
- const repoIfacePath = path8.resolve(options.repositoryInterfaceFile);
1220
+ const repoIfacePath = path8.resolve(repositoryInterfaceFile);
1305
1221
  if (fs9.existsSync(repoIfacePath)) {
1306
1222
  const ifaceMethod = repositoryInterfaceMethod(
1307
1223
  camelCase(options.useCaseName),
1308
- returnType,
1224
+ entityType,
1309
1225
  methodParamsForSignature
1310
1226
  );
1311
- injectMethod(repoIfacePath, options.repositoryInterfaceName, ifaceMethod);
1312
- const entityImport = `import 'package:${options.feature}/domain/entities/${options.responseModelName}/${options.responseModelName}_entity.dart';`;
1227
+ injectMethod(repoIfacePath, repositoryInterfaceName, ifaceMethod);
1228
+ const entityImport = `import 'package:${options.projectName}/domain/entities/${feature}/${useCaseSnake}_entity.dart';`;
1313
1229
  injectImport(repoIfacePath, entityImport);
1314
1230
  } else {
1315
1231
  console.log(chalk4.yellow(` \u26A0 Repository interface not found: ${repoIfacePath}`));
1316
1232
  }
1317
- spinner2("Injectating repository implementation method");
1318
- const repoImplPath = path8.resolve(options.repositoryImplFile);
1233
+ spinner2("Injecting repository implementation method");
1234
+ const repoImplPath = path8.resolve(repositoryImplFile);
1319
1235
  if (fs9.existsSync(repoImplPath)) {
1320
- const dsVarName = camelCase(options.datasourceClassName);
1236
+ const dsVarName = camelCase(datasourceClassName);
1321
1237
  const implMethod = repositoryImplMethod(
1322
1238
  camelCase(options.useCaseName),
1323
- returnType,
1239
+ entityType,
1240
+ modelType,
1324
1241
  methodParamsForSignature,
1325
1242
  dsVarName
1326
1243
  );
1327
- injectMethod(repoImplPath, options.repositoryImplClassName, implMethod);
1244
+ injectMethod(repoImplPath, repositoryImplClassName, implMethod);
1328
1245
  } else {
1329
1246
  console.log(chalk4.yellow(` \u26A0 Repository implementation not found: ${repoImplPath}`));
1330
1247
  }
1331
1248
  spinner2("Generating UseCase");
1332
- const useCaseDir = path8.join(lib, "domain", "usecases", options.feature);
1249
+ const useCaseDir = path8.join(lib, "domain", "usecases", feature);
1333
1250
  fs9.mkdirSync(useCaseDir, { recursive: true });
1334
1251
  fs9.writeFileSync(
1335
1252
  path8.join(useCaseDir, `${useCaseSnake}_usecase.dart`),
@@ -1338,27 +1255,41 @@ async function createEndpoint(options) {
1338
1255
  useCasePascal,
1339
1256
  camelCase(options.useCaseName),
1340
1257
  methodParamsForSignature,
1341
- returnType,
1342
- repositoryInterfaceName
1258
+ entityType,
1259
+ repositoryInterfaceSnake
1343
1260
  )
1344
1261
  );
1345
- spinner2("Updating barrel file");
1346
- const barrelPath = path8.join(useCaseDir, "usecases.dart");
1347
- const exportLine = `export '${useCaseSnake}_usecase.dart';`;
1348
- injectExport(barrelPath, exportLine);
1349
- if (options.runBuildRunner) {
1350
- const projectRoot = findPubspecDir(options.projectRoot);
1351
- if (projectRoot && hasBuildRunner(projectRoot)) {
1352
- spinner2("Running build_runner...");
1353
- await runBuildRunner(projectRoot);
1354
- console.log(chalk4.green(" \u2713 build_runner completed."));
1355
- } else {
1356
- console.log(chalk4.yellow(" \u26A0 Skipping build_runner (not found)."));
1357
- }
1262
+ spinner2("Updating barrel files");
1263
+ const entityBarrel = path8.join(lib, "domain", "entities", feature, `${feature}.dart`);
1264
+ injectExport(entityBarrel, `export '${useCaseSnake}_entity.dart';`);
1265
+ injectExport(
1266
+ path8.join(lib, "domain", "entities", "entities.dart"),
1267
+ `export '${feature}/${feature}.dart';`
1268
+ );
1269
+ const modelBarrel = path8.join(lib, "data", "models", feature, `${feature}.dart`);
1270
+ injectExport(modelBarrel, `export '${useCaseSnake}_model.dart';`);
1271
+ if (needsBody) {
1272
+ injectExport(modelBarrel, `export '${useCaseSnake}_request_model.dart';`);
1358
1273
  }
1274
+ injectExport(
1275
+ path8.join(lib, "data", "models", "models.dart"),
1276
+ `export '${feature}/${feature}.dart';`
1277
+ );
1278
+ const useCaseBarrel = path8.join(useCaseDir, `${feature}.dart`);
1279
+ injectExport(useCaseBarrel, `export '${useCaseSnake}_usecase.dart';`);
1280
+ injectExport(
1281
+ path8.join(lib, "domain", "usecases", "usecases.dart"),
1282
+ `export '${feature}/${feature}.dart';`
1283
+ );
1359
1284
  console.log(chalk4.green(`
1360
1285
  \u2713 Endpoint "${options.useCaseName}" generated successfully.`));
1361
1286
  }
1287
+ function extractDomain(bffApiFile) {
1288
+ const baseName = path8.basename(bffApiFile);
1289
+ const match = baseName.match(/^bff_(.+)_api\.dart$/);
1290
+ if (match) return match[1];
1291
+ return baseName.replace(/\.dart$/, "");
1292
+ }
1362
1293
  function extractPathParams(endpointPath) {
1363
1294
  const regex = /\{(\w+)\}/g;
1364
1295
  const params = [];
@@ -1370,8 +1301,8 @@ function extractPathParams(endpointPath) {
1370
1301
  }
1371
1302
  function buildMethodParams(options, pathParams, hasRequestBody) {
1372
1303
  const params = [...pathParams];
1373
- if (hasRequestBody && options.requestModelName) {
1374
- const reqPascal = pascalCase4(options.requestModelName);
1304
+ if (hasRequestBody) {
1305
+ const reqPascal = pascalCase4(options.useCaseName);
1375
1306
  params.push({ name: "body", type: `${reqPascal}RequestModel` });
1376
1307
  }
1377
1308
  return params;
@@ -1381,8 +1312,8 @@ function buildRetrofitParams(options, pathParams, hasRequestBody) {
1381
1312
  for (const pp of pathParams) {
1382
1313
  params.push({ ...pp, isPath: true });
1383
1314
  }
1384
- if (hasRequestBody && options.requestModelName) {
1385
- const reqPascal = pascalCase4(options.requestModelName);
1315
+ if (hasRequestBody) {
1316
+ const reqPascal = pascalCase4(options.useCaseName);
1386
1317
  params.push({ name: "body", type: `${reqPascal}RequestModel`, isBody: true });
1387
1318
  }
1388
1319
  return params;
@@ -1395,7 +1326,6 @@ function extractClassName(filePath) {
1395
1326
  }
1396
1327
 
1397
1328
  // src/interactive.ts
1398
- import { pascalCase as pascalCase5 } from "change-case";
1399
1329
  var SNAKE_CASE_REGEX4 = /^[a-z][a-z0-9_]*$/;
1400
1330
  async function resolveProject() {
1401
1331
  const s = clack.spinner();
@@ -1683,8 +1613,76 @@ async function useCaseFlow(project) {
1683
1613
  clack.outro(chalk5.red(`Error: ${error}`));
1684
1614
  }
1685
1615
  }
1686
- async function endpointFlow(project) {
1616
+ function findBffFiles(dir) {
1617
+ const results = [];
1618
+ if (!fs10.existsSync(dir)) return results;
1619
+ const entries = fs10.readdirSync(dir, { withFileTypes: true });
1620
+ for (const entry of entries) {
1621
+ const fullPath = path9.join(dir, entry.name);
1622
+ if (entry.isDirectory()) {
1623
+ results.push(...findBffFiles(fullPath));
1624
+ } else if (entry.isFile() && entry.name.endsWith(".dart") && !entry.name.includes(".g.")) {
1625
+ results.push(fullPath);
1626
+ }
1627
+ }
1628
+ return results.sort();
1629
+ }
1630
+ function inferBffFile(bffFiles, endpointPath) {
1631
+ const firstSegment = endpointPath.replace(/^\//, "").split("/")[0].toLowerCase();
1632
+ if (!firstSegment) return null;
1633
+ const withDomains = bffFiles.map((f) => {
1634
+ const base = path9.basename(f, ".dart");
1635
+ const domain = base.replace(/^bff_/, "").replace(/_api$/, "");
1636
+ return { file: f, domain };
1637
+ });
1638
+ const exact = withDomains.filter((x) => x.domain === firstSegment);
1639
+ if (exact.length === 1) return exact[0].file;
1640
+ const singular = firstSegment.replace(/s$/, "");
1641
+ const singularMatch = withDomains.filter((x) => x.domain === singular);
1642
+ if (singularMatch.length === 1) return singularMatch[0].file;
1643
+ const endsWith = withDomains.filter(
1644
+ (x) => x.domain.endsWith(firstSegment) || x.domain.endsWith(singular)
1645
+ );
1646
+ if (endsWith.length === 1) return endsWith[0].file;
1647
+ const contains = withDomains.filter(
1648
+ (x) => firstSegment.includes(x.domain) || singular.includes(x.domain)
1649
+ );
1650
+ if (contains.length === 1) return contains[0].file;
1651
+ return null;
1652
+ }
1653
+ async function resolveEndpointProject() {
1654
+ const s = clack.spinner();
1655
+ s.start("Finding BFF package...");
1656
+ const monorepoRoot = findMonorepoRoot(process.cwd());
1657
+ if (monorepoRoot) {
1658
+ const packages = discoverPackages(monorepoRoot);
1659
+ const bffPackage = packages.find(
1660
+ (p) => fs10.existsSync(path9.join(p.projectRoot, "lib", "data", "api", "bff"))
1661
+ );
1662
+ if (bffPackage) {
1663
+ s.stop(`Using ${chalk5.green(bffPackage.projectName)}`);
1664
+ return bffPackage;
1665
+ }
1666
+ }
1667
+ const cwdProject = analyzeProject(process.cwd());
1668
+ if (cwdProject && fs10.existsSync(path9.join(cwdProject.projectRoot, "lib", "data", "api", "bff"))) {
1669
+ s.stop(`Using ${chalk5.green(cwdProject.projectName)}`);
1670
+ return cwdProject;
1671
+ }
1672
+ s.stop("No BFF package found");
1673
+ clack.outro(chalk5.red("Could not find a package with lib/data/api/bff/."));
1674
+ return null;
1675
+ }
1676
+ async function endpointFlow() {
1677
+ const project = await resolveEndpointProject();
1678
+ if (!project) return;
1687
1679
  const lib = path9.join(project.projectRoot, "lib");
1680
+ const bffDir = path9.join(lib, "data", "api", "bff");
1681
+ const bffFiles = findBffFiles(bffDir);
1682
+ if (bffFiles.length === 0) {
1683
+ clack.outro(chalk5.red("No BFF API files found. Ensure lib/data/api/bff/ exists with .dart files."));
1684
+ return;
1685
+ }
1688
1686
  const httpMethod = await clack.select({
1689
1687
  message: "HTTP Method",
1690
1688
  options: [
@@ -1700,7 +1698,7 @@ async function endpointFlow(project) {
1700
1698
  return;
1701
1699
  }
1702
1700
  const endpointPath = await clack.text({
1703
- message: "Endpoint path (e.g. /cp/{zipCode})",
1701
+ message: "Endpoint path (e.g. /products/{id})",
1704
1702
  placeholder: "/api/users/{id}",
1705
1703
  validate: (v) => {
1706
1704
  if (!v.trim()) return "Path is required";
@@ -1710,191 +1708,26 @@ async function endpointFlow(project) {
1710
1708
  clack.cancel("Cancelled");
1711
1709
  return;
1712
1710
  }
1713
- const bffDir = path9.join(lib, "data", "api", "bff");
1714
- let bffApiFile = "";
1715
- if (fs10.existsSync(bffDir)) {
1716
- const dartFiles = fs10.readdirSync(bffDir).filter((f) => f.endsWith(".dart")).sort();
1717
- if (dartFiles.length > 0) {
1718
- const selected = await clack.select({
1719
- message: "Select BFF API file",
1720
- options: [
1721
- ...dartFiles.map((f) => ({
1722
- value: path9.join(bffDir, f),
1723
- label: f
1724
- })),
1725
- { value: "__custom__", label: "Custom path..." }
1726
- ]
1727
- });
1728
- if (clack.isCancel(selected)) {
1729
- clack.cancel("Cancelled");
1730
- return;
1731
- }
1732
- if (selected === "__custom__") {
1733
- const customPath = await clack.text({
1734
- message: "BFF API file path",
1735
- placeholder: "lib/data/api/bff/my_api.dart",
1736
- validate: (v) => {
1737
- if (!v.trim()) return "Path is required";
1738
- }
1739
- });
1740
- if (clack.isCancel(customPath)) {
1741
- clack.cancel("Cancelled");
1742
- return;
1743
- }
1744
- bffApiFile = path9.resolve(customPath);
1745
- } else {
1746
- bffApiFile = selected;
1747
- }
1748
- } else {
1749
- const customPath = await clack.text({
1750
- message: "BFF API file path (no .dart files found in bff/)",
1751
- placeholder: "lib/data/api/bff/my_api.dart",
1752
- validate: (v) => {
1753
- if (!v.trim()) return "Path is required";
1754
- }
1755
- });
1756
- if (clack.isCancel(customPath)) {
1757
- clack.cancel("Cancelled");
1758
- return;
1759
- }
1760
- bffApiFile = path9.resolve(customPath);
1761
- }
1762
- } else {
1763
- const customPath = await clack.text({
1764
- message: "BFF API file path (bff/ dir not found)",
1765
- placeholder: "lib/data/api/bff/my_api.dart",
1766
- validate: (v) => {
1767
- if (!v.trim()) return "Path is required";
1768
- }
1769
- });
1770
- if (clack.isCancel(customPath)) {
1771
- clack.cancel("Cancelled");
1772
- return;
1773
- }
1774
- bffApiFile = path9.resolve(customPath);
1775
- }
1776
- const responseChoice = await clack.select({
1777
- message: "Response model",
1778
- options: [
1779
- { value: "existing", label: "Use existing model", hint: "Reference an already created model" },
1780
- { value: "json", label: "Paste JSON", hint: "Generate entity + model from JSON" }
1781
- ]
1782
- });
1783
- if (clack.isCancel(responseChoice)) {
1784
- clack.cancel("Cancelled");
1785
- return;
1786
- }
1787
- let responseModelName = "";
1788
- let responseFields;
1789
- if (responseChoice === "json") {
1790
- const modelName = await clack.text({
1791
- message: "Response model name (snake_case)",
1792
- placeholder: "e.g. user_profile",
1793
- validate: (v) => {
1794
- if (!v.trim()) return "Name is required";
1795
- if (!SNAKE_CASE_REGEX4.test(v)) return "Must be snake_case";
1796
- }
1797
- });
1798
- if (clack.isCancel(modelName)) {
1799
- clack.cancel("Cancelled");
1800
- return;
1801
- }
1802
- responseModelName = modelName;
1803
- const jsonStr = await clack.text({
1804
- message: "Paste the JSON response",
1805
- placeholder: '{ "id": 1, "name": "John" }',
1806
- validate: (v) => {
1807
- if (!v.trim()) return "JSON is required";
1808
- try {
1809
- JSON.parse(v);
1810
- } catch {
1811
- return "Invalid JSON";
1812
- }
1813
- }
1814
- });
1815
- if (clack.isCancel(jsonStr)) {
1816
- clack.cancel("Cancelled");
1817
- return;
1818
- }
1819
- responseFields = jsonToFields(jsonStr);
1711
+ let bffApiFile = inferBffFile(bffFiles, endpointPath);
1712
+ if (bffApiFile) {
1713
+ clack.log.info(`Auto-detected BFF file: ${chalk5.cyan(path9.basename(bffApiFile))}`);
1820
1714
  } else {
1821
- const modelName = await clack.text({
1822
- message: "Existing response model name (snake_case)",
1823
- placeholder: "e.g. user_profile",
1824
- validate: (v) => {
1825
- if (!v.trim()) return "Name is required";
1826
- }
1715
+ const selected = await clack.select({
1716
+ message: "Could not auto-detect BFF file. Select one:",
1717
+ options: bffFiles.map((f) => ({
1718
+ value: f,
1719
+ label: path9.relative(bffDir, f)
1720
+ }))
1827
1721
  });
1828
- if (clack.isCancel(modelName)) {
1722
+ if (clack.isCancel(selected)) {
1829
1723
  clack.cancel("Cancelled");
1830
1724
  return;
1831
1725
  }
1832
- responseModelName = modelName;
1833
- }
1834
- let requestModelName;
1835
- let requestFields;
1836
- const needsBody = ["POST", "PUT", "PATCH"].includes(httpMethod);
1837
- if (needsBody) {
1838
- const requestChoice = await clack.select({
1839
- message: "Request body",
1840
- options: [
1841
- { value: "json", label: "Paste JSON", hint: "Generate request model from JSON" },
1842
- { value: "existing", label: "Use existing model", hint: "Reference an already created model" },
1843
- { value: "none", label: "No body", hint: "Just path/query params" }
1844
- ]
1845
- });
1846
- if (clack.isCancel(requestChoice)) {
1847
- clack.cancel("Cancelled");
1848
- return;
1849
- }
1850
- if (requestChoice === "json") {
1851
- const reqName = await clack.text({
1852
- message: "Request model name (snake_case)",
1853
- placeholder: `e.g. create_${responseModelName}`,
1854
- validate: (v) => {
1855
- if (!v.trim()) return "Name is required";
1856
- if (!SNAKE_CASE_REGEX4.test(v)) return "Must be snake_case";
1857
- }
1858
- });
1859
- if (clack.isCancel(reqName)) {
1860
- clack.cancel("Cancelled");
1861
- return;
1862
- }
1863
- requestModelName = reqName;
1864
- const reqJson = await clack.text({
1865
- message: "Paste the request JSON body",
1866
- placeholder: '{ "name": "John", "email": "john@example.com" }',
1867
- validate: (v) => {
1868
- if (!v.trim()) return "JSON is required";
1869
- try {
1870
- JSON.parse(v);
1871
- } catch {
1872
- return "Invalid JSON";
1873
- }
1874
- }
1875
- });
1876
- if (clack.isCancel(reqJson)) {
1877
- clack.cancel("Cancelled");
1878
- return;
1879
- }
1880
- requestFields = jsonToFields(reqJson);
1881
- } else if (requestChoice === "existing") {
1882
- const reqName = await clack.text({
1883
- message: "Existing request model name (snake_case)",
1884
- placeholder: `e.g. create_${responseModelName}`,
1885
- validate: (v) => {
1886
- if (!v.trim()) return "Name is required";
1887
- }
1888
- });
1889
- if (clack.isCancel(reqName)) {
1890
- clack.cancel("Cancelled");
1891
- return;
1892
- }
1893
- requestModelName = reqName;
1894
- }
1726
+ bffApiFile = selected;
1895
1727
  }
1728
+ const methodLower = httpMethod.toLowerCase();
1896
1729
  const pathSegments = endpointPath.replace(/^\//, "").split("/").filter((s) => !s.startsWith("{"));
1897
- const inferredName = pathSegments.length > 0 ? "get_" + pathSegments.join("_") : "get_data";
1730
+ const inferredName = pathSegments.length > 0 ? `${methodLower}_${pathSegments.join("_")}` : `${methodLower}_data`;
1898
1731
  const useCaseName = await clack.text({
1899
1732
  message: "UseCase name (snake_case)",
1900
1733
  placeholder: inferredName,
@@ -1908,117 +1741,16 @@ async function endpointFlow(project) {
1908
1741
  clack.cancel("Cancelled");
1909
1742
  return;
1910
1743
  }
1911
- const datasourceFile = await clack.text({
1912
- message: "Datasource file path",
1913
- placeholder: `lib/data/datasources/${responseModelName}_datasource.dart`,
1914
- validate: (v) => {
1915
- if (!v.trim()) return "Path is required";
1916
- }
1917
- });
1918
- if (clack.isCancel(datasourceFile)) {
1919
- clack.cancel("Cancelled");
1920
- return;
1921
- }
1922
- const datasourceClassName = await clack.text({
1923
- message: "Datasource class name",
1924
- placeholder: `${pascalCase5(responseModelName)}Datasource`,
1925
- initialValue: `${pascalCase5(responseModelName)}Datasource`,
1926
- validate: (v) => {
1927
- if (!v.trim()) return "Class name is required";
1928
- }
1929
- });
1930
- if (clack.isCancel(datasourceClassName)) {
1931
- clack.cancel("Cancelled");
1932
- return;
1933
- }
1934
- const repositoryInterfaceFile = await clack.text({
1935
- message: "Repository interface file path",
1936
- placeholder: `lib/domain/repositories/${responseModelName}_repository.dart`,
1937
- validate: (v) => {
1938
- if (!v.trim()) return "Path is required";
1939
- }
1940
- });
1941
- if (clack.isCancel(repositoryInterfaceFile)) {
1942
- clack.cancel("Cancelled");
1943
- return;
1944
- }
1945
- const repositoryInterfaceName2 = await clack.text({
1946
- message: "Repository interface name",
1947
- placeholder: `${pascalCase5(responseModelName)}Repository`,
1948
- initialValue: `${pascalCase5(responseModelName)}Repository`,
1949
- validate: (v) => {
1950
- if (!v.trim()) return "Name is required";
1951
- }
1952
- });
1953
- if (clack.isCancel(repositoryInterfaceName2)) {
1954
- clack.cancel("Cancelled");
1955
- return;
1956
- }
1957
- const repositoryImplFile = await clack.text({
1958
- message: "Repository implementation file path",
1959
- placeholder: `lib/data/repositories/${responseModelName}_repository_impl.dart`,
1960
- validate: (v) => {
1961
- if (!v.trim()) return "Path is required";
1962
- }
1963
- });
1964
- if (clack.isCancel(repositoryImplFile)) {
1965
- clack.cancel("Cancelled");
1966
- return;
1967
- }
1968
- const repositoryImplClassName = await clack.text({
1969
- message: "Repository implementation class name",
1970
- placeholder: `${pascalCase5(responseModelName)}RepositoryImpl`,
1971
- initialValue: `${pascalCase5(responseModelName)}RepositoryImpl`,
1972
- validate: (v) => {
1973
- if (!v.trim()) return "Name is required";
1974
- }
1975
- });
1976
- if (clack.isCancel(repositoryImplClassName)) {
1977
- clack.cancel("Cancelled");
1978
- return;
1979
- }
1980
- const feature = await clack.text({
1981
- message: "Feature name (snake_case, for directory grouping)",
1982
- placeholder: responseModelName,
1983
- initialValue: responseModelName,
1984
- validate: (v) => {
1985
- if (!v.trim()) return "Feature name is required";
1986
- if (!SNAKE_CASE_REGEX4.test(v)) return "Must be snake_case";
1987
- }
1988
- });
1989
- if (clack.isCancel(feature)) {
1990
- clack.cancel("Cancelled");
1991
- return;
1992
- }
1993
- const runBuildRunner2 = await clack.confirm({
1994
- message: "Run build_runner after generation?",
1995
- initialValue: project.hasBuildRunner
1996
- });
1997
- if (clack.isCancel(runBuildRunner2)) {
1998
- clack.cancel("Cancelled");
1999
- return;
2000
- }
2001
1744
  const genSpinner = clack.spinner();
2002
1745
  genSpinner.start("Generating endpoint stack...");
2003
1746
  try {
2004
1747
  await createEndpoint({
2005
1748
  projectRoot: project.projectRoot,
1749
+ projectName: project.projectName,
2006
1750
  httpMethod,
2007
1751
  endpointPath,
2008
1752
  bffApiFile,
2009
- responseModelName,
2010
- responseFields,
2011
- requestModelName,
2012
- requestFields,
2013
- useCaseName,
2014
- datasourceFile,
2015
- datasourceClassName,
2016
- repositoryInterfaceFile,
2017
- repositoryInterfaceName: repositoryInterfaceName2,
2018
- repositoryImplFile,
2019
- repositoryImplClassName,
2020
- feature,
2021
- runBuildRunner: runBuildRunner2
1753
+ useCaseName
2022
1754
  });
2023
1755
  genSpinner.stop("Endpoint generated");
2024
1756
  clack.outro(chalk5.green("Done!"));
@@ -2067,12 +1799,9 @@ async function interactiveMode() {
2067
1799
  await useCaseFlow(project);
2068
1800
  break;
2069
1801
  }
2070
- case "endpoint": {
2071
- const project = await resolveProject();
2072
- if (!project) return;
2073
- await endpointFlow(project);
1802
+ case "endpoint":
1803
+ await endpointFlow();
2074
1804
  break;
2075
- }
2076
1805
  }
2077
1806
  }
2078
1807
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wlmaker",
3
- "version": "1.2.9",
3
+ "version": "1.2.11",
4
4
  "description": "Create Flutter BLoCs with Freezed sealed classes from the terminal",
5
5
  "keywords": [
6
6
  "flutter",