fwinit 1.5.4 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -5
- package/dist/index.js +461 -16
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -25,6 +25,10 @@ fwinit
|
|
|
25
25
|
fwinit fastify mi-api
|
|
26
26
|
fwinit aspnet mi-app
|
|
27
27
|
fwinit react-native mi-mobile
|
|
28
|
+
fwinit flutter mi-app
|
|
29
|
+
|
|
30
|
+
# Flutter: saltear la pregunta de organización (bundle id)
|
|
31
|
+
fwinit flutter mi-app -o ar.miempresa
|
|
28
32
|
|
|
29
33
|
# Crear un proyecto completo: backend + frontend (+ db incluida)
|
|
30
34
|
fwinit fullstack mi-proyecto
|
|
@@ -75,7 +79,7 @@ fwinit --help
|
|
|
75
79
|
fwinit fullstack mi-proyecto
|
|
76
80
|
```
|
|
77
81
|
|
|
78
|
-
Pregunta qué backend querés (ASP.NET, Express, Fastify, Node.js Vanilla) y qué frontend (React Native). Genera:
|
|
82
|
+
Pregunta qué backend querés (ASP.NET, Express, Fastify, Node.js Vanilla) y qué frontend (React Native, Flutter, Astro, Tauri). Genera:
|
|
79
83
|
|
|
80
84
|
```
|
|
81
85
|
mi-proyecto/
|
|
@@ -104,13 +108,14 @@ fwinit express mi-api --no-backend
|
|
|
104
108
|
fwinit express mi-api --backend
|
|
105
109
|
```
|
|
106
110
|
|
|
107
|
-
Igual para frontend: `fwinit react-native mi-app` pregunta por `frontend/`.
|
|
111
|
+
Igual para frontend: `fwinit react-native mi-app` o `fwinit flutter mi-app` pregunta por `frontend/`.
|
|
108
112
|
|
|
109
113
|
## Flags
|
|
110
114
|
|
|
111
115
|
| Flag | Descripción |
|
|
112
116
|
|------|-------------|
|
|
113
117
|
| `-p, --pm <manager>` | Package manager: `npm`, `pnpm` o `bun` (saltea la pregunta) |
|
|
118
|
+
| `-o, --org <domain>` | Organización reverse-domain del bundle id. Solo aplica a Flutter (saltea la pregunta; default `com.example`) |
|
|
114
119
|
| `--backend` | Empaquetar el código en `backend/` (solo modo single, saltea la pregunta) |
|
|
115
120
|
| `--no-backend` | Dejar el código en la raíz del proyecto (saltea la pregunta) |
|
|
116
121
|
| `-b, --backend-template <tpl>` | Backend a usar en fullstack (saltea la pregunta) |
|
|
@@ -129,8 +134,11 @@ Igual para frontend: `fwinit react-native mi-app` pregunta por `frontend/`.
|
|
|
129
134
|
| `astro` | frontend | Landing estática con Astro 7 y TypeScript | node |
|
|
130
135
|
| `tauri` | frontend | Dashboard de escritorio con React, Vite y Tauri 2 | node |
|
|
131
136
|
| `react-native` | frontend | App móvil con React Native, Expo y TypeScript | node |
|
|
137
|
+
| `flutter` | frontend | App multiplataforma con Flutter, Clean Architecture y Riverpod | flutter |
|
|
138
|
+
|
|
139
|
+
La capa y el runtime son metadata de cada template (`layer` y `runtime` en `templates/<TEMPLATE>/template.json`), pero **el template se registra en `cli/src/templates.ts`**: el `template.json` documenta y aporta los comandos de `postInit`, no resuelve el alias. Para sumar un template nuevo alcanzan la carpeta, su `template.json` y su entrada en `TEMPLATES`.
|
|
132
140
|
|
|
133
|
-
|
|
141
|
+
Flutter es el único template que pregunta una identidad propia: la organización reverse-domain (default `com.example`) para armar los bundle ids nativos. `-o, --org` la saltea; en modo scripting/CI se usa el default.
|
|
134
142
|
|
|
135
143
|
El runtime `bun` de un template se porta automáticamente a `node` (npm/pnpm) si elegís npm o pnpm: scripts con `tsx`, tests con `vitest` y sin `bun-types`.
|
|
136
144
|
|
|
@@ -138,10 +146,10 @@ El runtime `bun` de un template se porta automáticamente a `node` (npm/pnpm) si
|
|
|
138
146
|
|
|
139
147
|
1. Descarga el ZIP del repositorio de templates desde GitHub (una sola vez, también en fullstack)
|
|
140
148
|
2. Extrae y copia el template (o los dos, `backend/` + `frontend/`) al proyecto
|
|
141
|
-
3. Sustituye el nombre del proyecto (package.json, app.json de Expo, proyectos ASP.NET con "Example")
|
|
149
|
+
3. Sustituye el nombre del proyecto (package.json, app.json de Expo, proyectos ASP.NET con "Example", y en Flutter el paquete Dart, los imports, los bundle ids nativos y los nombres de binario)
|
|
142
150
|
4. Si corresponde, porta el template de runtime bun a node (npm/pnpm)
|
|
143
151
|
5. Instala `.opencode/`, `.pi/` y `.agents/` con las skills `create-specs` y `create-specs-from-code`, sus comandos (`/create-specs` rutea entre ambos según el estado del repo) y la biblioteca de diseño (mismo bundle para OpenCode, Pi/Gentle Shell y Agent Skills: elegí el agente que quieras)
|
|
144
|
-
6. Limpia los lock files (cada proyecto instala los suyos)
|
|
152
|
+
6. Limpia los lock files (cada proyecto instala los suyos; incluye `pubspec.lock` en Flutter)
|
|
145
153
|
7. Si lo elegís (o pasás `--git`), inicializa un repositorio git en la raíz
|
|
146
154
|
8. Te muestra los próximos pasos (install y dev de cada carpeta)
|
|
147
155
|
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import ora from "ora";
|
|
|
6
6
|
import chalk from "chalk";
|
|
7
7
|
import { readFileSync } from "fs";
|
|
8
8
|
import { mkdir as mkdir3, cp as cp3, readFile as readFile3 } from "fs/promises";
|
|
9
|
-
import { join as join3, basename } from "path";
|
|
9
|
+
import { join as join3, basename as basename2 } from "path";
|
|
10
10
|
|
|
11
11
|
// src/templates.ts
|
|
12
12
|
var TEMPLATES = [
|
|
@@ -65,6 +65,14 @@ var TEMPLATES = [
|
|
|
65
65
|
folder: "REACT-NATIVE",
|
|
66
66
|
runtime: "node",
|
|
67
67
|
layer: "frontend"
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: "Flutter",
|
|
71
|
+
alias: ["flutter", "dart", "fwiflu"],
|
|
72
|
+
description: "App multiplataforma con Flutter, Clean Architecture y Riverpod",
|
|
73
|
+
folder: "FLUTTER",
|
|
74
|
+
runtime: "flutter",
|
|
75
|
+
layer: "frontend"
|
|
68
76
|
}
|
|
69
77
|
];
|
|
70
78
|
function getTemplatesByLayer(layer) {
|
|
@@ -79,10 +87,191 @@ function findTemplate(input) {
|
|
|
79
87
|
|
|
80
88
|
// src/downloader.ts
|
|
81
89
|
import AdmZip from "adm-zip";
|
|
82
|
-
import { mkdir, rm, cp, readdir, readFile, writeFile, rename } from "fs/promises";
|
|
83
|
-
import { join, extname } from "path";
|
|
90
|
+
import { mkdir, rm, cp, readdir, readFile, writeFile, rename, stat } from "fs/promises";
|
|
91
|
+
import { join, extname, basename, dirname, relative, sep } from "path";
|
|
84
92
|
import { tmpdir } from "os";
|
|
85
93
|
import { randomBytes } from "crypto";
|
|
94
|
+
|
|
95
|
+
// src/flutter-naming.ts
|
|
96
|
+
var FLUTTER_ORG_DEFAULT = "com.example";
|
|
97
|
+
var DART_KEYWORDS = /* @__PURE__ */ new Set([
|
|
98
|
+
"abstract",
|
|
99
|
+
"as",
|
|
100
|
+
"assert",
|
|
101
|
+
"async",
|
|
102
|
+
"await",
|
|
103
|
+
"break",
|
|
104
|
+
"case",
|
|
105
|
+
"catch",
|
|
106
|
+
"class",
|
|
107
|
+
"const",
|
|
108
|
+
"continue",
|
|
109
|
+
"covariant",
|
|
110
|
+
"default",
|
|
111
|
+
"deferred",
|
|
112
|
+
"do",
|
|
113
|
+
"dynamic",
|
|
114
|
+
"else",
|
|
115
|
+
"enum",
|
|
116
|
+
"export",
|
|
117
|
+
"extends",
|
|
118
|
+
"extension",
|
|
119
|
+
"external",
|
|
120
|
+
"factory",
|
|
121
|
+
"final",
|
|
122
|
+
"finally",
|
|
123
|
+
"for",
|
|
124
|
+
"get",
|
|
125
|
+
"hide",
|
|
126
|
+
"if",
|
|
127
|
+
"implements",
|
|
128
|
+
"import",
|
|
129
|
+
"in",
|
|
130
|
+
"interface",
|
|
131
|
+
"is",
|
|
132
|
+
"late",
|
|
133
|
+
"library",
|
|
134
|
+
"mixin",
|
|
135
|
+
"new",
|
|
136
|
+
"on",
|
|
137
|
+
"operator",
|
|
138
|
+
"part",
|
|
139
|
+
"required",
|
|
140
|
+
"rethrow",
|
|
141
|
+
"return",
|
|
142
|
+
"set",
|
|
143
|
+
"show",
|
|
144
|
+
"static",
|
|
145
|
+
"super",
|
|
146
|
+
"switch",
|
|
147
|
+
"sync",
|
|
148
|
+
"this",
|
|
149
|
+
"throw",
|
|
150
|
+
"try",
|
|
151
|
+
"typedef",
|
|
152
|
+
"var",
|
|
153
|
+
"void",
|
|
154
|
+
"while",
|
|
155
|
+
"with",
|
|
156
|
+
"yield"
|
|
157
|
+
]);
|
|
158
|
+
var FLUTTER_SDK_PACKAGES = /* @__PURE__ */ new Set([
|
|
159
|
+
"flutter",
|
|
160
|
+
"flutter_test",
|
|
161
|
+
"flutter_localizations",
|
|
162
|
+
"flutter_web_plugins",
|
|
163
|
+
"sky_engine"
|
|
164
|
+
]);
|
|
165
|
+
var TEMPLATE_DEPENDENCIES = /* @__PURE__ */ new Set([
|
|
166
|
+
"cupertino_icons",
|
|
167
|
+
"flutter_riverpod",
|
|
168
|
+
"go_router",
|
|
169
|
+
"dio",
|
|
170
|
+
"freezed_annotation",
|
|
171
|
+
"json_annotation",
|
|
172
|
+
"shared_preferences",
|
|
173
|
+
"flutter_secure_storage",
|
|
174
|
+
"logger",
|
|
175
|
+
"intl",
|
|
176
|
+
"flutter_lints",
|
|
177
|
+
"build_runner",
|
|
178
|
+
"freezed",
|
|
179
|
+
"json_serializable",
|
|
180
|
+
"mocktail"
|
|
181
|
+
]);
|
|
182
|
+
var RESERVED_DART_PACKAGE_NAMES = [
|
|
183
|
+
DART_KEYWORDS,
|
|
184
|
+
FLUTTER_SDK_PACKAGES,
|
|
185
|
+
TEMPLATE_DEPENDENCIES
|
|
186
|
+
];
|
|
187
|
+
function isReservedDartPackageName(name) {
|
|
188
|
+
return RESERVED_DART_PACKAGE_NAMES.some((reserved) => reserved.has(name));
|
|
189
|
+
}
|
|
190
|
+
function toDartPackageName(projectName) {
|
|
191
|
+
let name = projectName.trim().toLowerCase();
|
|
192
|
+
name = name.replace(/[-_.\s]+/g, "_");
|
|
193
|
+
name = name.replace(/[^a-z0-9_]/g, "");
|
|
194
|
+
name = name.replace(/_+/g, "_").replace(/^_+|_+$/g, "");
|
|
195
|
+
if (name === "") {
|
|
196
|
+
return "app";
|
|
197
|
+
}
|
|
198
|
+
if (!/^[a-z]/.test(name)) {
|
|
199
|
+
name = `app_${name}`;
|
|
200
|
+
}
|
|
201
|
+
if (isReservedDartPackageName(name)) {
|
|
202
|
+
name = `${name}_app`;
|
|
203
|
+
}
|
|
204
|
+
return name;
|
|
205
|
+
}
|
|
206
|
+
function normalizeOrg(org) {
|
|
207
|
+
return org.trim().toLowerCase();
|
|
208
|
+
}
|
|
209
|
+
function isValidOrg(org) {
|
|
210
|
+
return /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/.test(org);
|
|
211
|
+
}
|
|
212
|
+
function checkOrg(value) {
|
|
213
|
+
if (isValidOrg(normalizeOrg(value))) {
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
return "Organizaci\xF3n inv\xE1lida. Us\xE1 formato reverse-domain en min\xFAsculas, por ejemplo com.example o ar.miempresa";
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// src/dart-directives.ts
|
|
220
|
+
var LIBRARY_RE = /^library\s*(?:['"]([^'"]*)['"])?\s*;$/;
|
|
221
|
+
var DIRECTIVE_URI_RE = /^(import|export|part)\s+(['"])(.+?)\2/;
|
|
222
|
+
var GROUP_LIBRARY = 0;
|
|
223
|
+
var GROUP_DART = 1;
|
|
224
|
+
var GROUP_PACKAGE = 2;
|
|
225
|
+
var GROUP_OTHER = 3;
|
|
226
|
+
function parseDirective(line) {
|
|
227
|
+
const trimmed = line.trim();
|
|
228
|
+
const library = trimmed.match(LIBRARY_RE);
|
|
229
|
+
if (library) {
|
|
230
|
+
return { group: GROUP_LIBRARY, uri: library[1] ?? "" };
|
|
231
|
+
}
|
|
232
|
+
const directive = trimmed.match(DIRECTIVE_URI_RE);
|
|
233
|
+
if (!directive) return void 0;
|
|
234
|
+
const uri = directive[3];
|
|
235
|
+
let group = GROUP_OTHER;
|
|
236
|
+
if (uri.startsWith("dart:")) {
|
|
237
|
+
group = GROUP_DART;
|
|
238
|
+
} else if (uri.startsWith("package:")) {
|
|
239
|
+
group = GROUP_PACKAGE;
|
|
240
|
+
}
|
|
241
|
+
return { group, uri };
|
|
242
|
+
}
|
|
243
|
+
function compareKeys(a, b) {
|
|
244
|
+
if (a.group !== b.group) return a.group - b.group;
|
|
245
|
+
return a.uri < b.uri ? -1 : a.uri > b.uri ? 1 : 0;
|
|
246
|
+
}
|
|
247
|
+
function sortDartDirectiveRuns(content) {
|
|
248
|
+
const lines = content.split("\n");
|
|
249
|
+
const result = lines.slice();
|
|
250
|
+
let i = 0;
|
|
251
|
+
while (i < lines.length) {
|
|
252
|
+
const key = parseDirective(lines[i]);
|
|
253
|
+
if (!key) {
|
|
254
|
+
i++;
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
let j = i;
|
|
258
|
+
const run = [];
|
|
259
|
+
while (j < lines.length) {
|
|
260
|
+
const runKey = parseDirective(lines[j]);
|
|
261
|
+
if (!runKey) break;
|
|
262
|
+
run.push({ line: lines[j], key: runKey });
|
|
263
|
+
j++;
|
|
264
|
+
}
|
|
265
|
+
run.sort((a, b) => compareKeys(a.key, b.key));
|
|
266
|
+
for (let k = 0; k < run.length; k++) {
|
|
267
|
+
result[i + k] = run[k].line;
|
|
268
|
+
}
|
|
269
|
+
i = j;
|
|
270
|
+
}
|
|
271
|
+
return result.join("\n");
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// src/downloader.ts
|
|
86
275
|
var GITHUB_REPO = "orlandotellez/fwinit";
|
|
87
276
|
var GITHUB_BRANCH = "main";
|
|
88
277
|
async function downloadAndExtractRepo() {
|
|
@@ -115,8 +304,25 @@ async function downloadAndExtract(templateFolder) {
|
|
|
115
304
|
}
|
|
116
305
|
return { tempDir, templatePath, repoRoot };
|
|
117
306
|
}
|
|
307
|
+
var COPY_EXCLUDED_DIRS = /* @__PURE__ */ new Set([
|
|
308
|
+
".git",
|
|
309
|
+
".dart_tool",
|
|
310
|
+
".idea",
|
|
311
|
+
".atl",
|
|
312
|
+
"node_modules",
|
|
313
|
+
"build"
|
|
314
|
+
]);
|
|
118
315
|
async function copyTemplate(source, destination) {
|
|
119
|
-
await cp(source, destination, {
|
|
316
|
+
await cp(source, destination, {
|
|
317
|
+
recursive: true,
|
|
318
|
+
// Solo se descartan directorios con esos nombres: un archivo homónimo
|
|
319
|
+
// (p. ej. un script llamado `build`) se copia igual.
|
|
320
|
+
filter: async (src) => {
|
|
321
|
+
if (!COPY_EXCLUDED_DIRS.has(basename(src))) return true;
|
|
322
|
+
const stats = await stat(src).catch(() => void 0);
|
|
323
|
+
return stats === void 0 || !stats.isDirectory();
|
|
324
|
+
}
|
|
325
|
+
});
|
|
120
326
|
await cleanTemplate(destination);
|
|
121
327
|
}
|
|
122
328
|
async function cleanTemplate(dir) {
|
|
@@ -124,7 +330,10 @@ async function cleanTemplate(dir) {
|
|
|
124
330
|
"bun.lock",
|
|
125
331
|
"pnpm-lock.yaml",
|
|
126
332
|
"package-lock.json",
|
|
127
|
-
"yarn.lock"
|
|
333
|
+
"yarn.lock",
|
|
334
|
+
// Cada proyecto Flutter instala su propio árbol de dependencias, igual
|
|
335
|
+
// que con los lock files de npm.
|
|
336
|
+
"pubspec.lock"
|
|
128
337
|
];
|
|
129
338
|
for (const file of lockFiles) {
|
|
130
339
|
const path = join(dir, file);
|
|
@@ -137,9 +346,15 @@ async function cleanTemplate(dir) {
|
|
|
137
346
|
async function cleanup(tempDir) {
|
|
138
347
|
await rm(tempDir, { recursive: true, force: true });
|
|
139
348
|
}
|
|
140
|
-
async function substituteTemplate(dir, projectName, templateFolder) {
|
|
349
|
+
async function substituteTemplate(dir, projectName, templateFolder, options = {}) {
|
|
141
350
|
if (templateFolder === "ASPNET") {
|
|
142
351
|
await substituteAspNet(dir, projectName);
|
|
352
|
+
} else if (templateFolder === "FLUTTER") {
|
|
353
|
+
await substituteFlutter(
|
|
354
|
+
dir,
|
|
355
|
+
projectName,
|
|
356
|
+
options.org ?? FLUTTER_ORG_DEFAULT
|
|
357
|
+
);
|
|
143
358
|
} else {
|
|
144
359
|
await substituteNodejs(dir, projectName);
|
|
145
360
|
}
|
|
@@ -190,6 +405,188 @@ async function substituteAspNet(dir, projectName) {
|
|
|
190
405
|
}
|
|
191
406
|
await renameAll(dir, pascalName);
|
|
192
407
|
}
|
|
408
|
+
var FLUTTER_TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
409
|
+
".dart",
|
|
410
|
+
".arb",
|
|
411
|
+
".yaml",
|
|
412
|
+
".yml",
|
|
413
|
+
".json",
|
|
414
|
+
".kt",
|
|
415
|
+
".kts",
|
|
416
|
+
".gradle",
|
|
417
|
+
".xml",
|
|
418
|
+
".plist",
|
|
419
|
+
".pbxproj",
|
|
420
|
+
".xcconfig",
|
|
421
|
+
".xcscheme",
|
|
422
|
+
".xcsettings",
|
|
423
|
+
".xcworkspacedata",
|
|
424
|
+
".storyboard",
|
|
425
|
+
".xib",
|
|
426
|
+
".swift",
|
|
427
|
+
".h",
|
|
428
|
+
".cpp",
|
|
429
|
+
".cc",
|
|
430
|
+
".cmake",
|
|
431
|
+
".txt",
|
|
432
|
+
".rc",
|
|
433
|
+
".properties",
|
|
434
|
+
".manifest",
|
|
435
|
+
".md",
|
|
436
|
+
".html",
|
|
437
|
+
".gitignore",
|
|
438
|
+
".metadata",
|
|
439
|
+
".iml",
|
|
440
|
+
".sh"
|
|
441
|
+
]);
|
|
442
|
+
var FLUTTER_TEXT_BASENAMES = /* @__PURE__ */ new Set([
|
|
443
|
+
"makefile",
|
|
444
|
+
"license",
|
|
445
|
+
"podfile",
|
|
446
|
+
".gitignore",
|
|
447
|
+
".metadata"
|
|
448
|
+
]);
|
|
449
|
+
var FLUTTER_DISPLAY_FILES = /* @__PURE__ */ new Set([
|
|
450
|
+
"lib/l10n/arb/app_en.arb",
|
|
451
|
+
"lib/l10n/arb/app_es.arb",
|
|
452
|
+
"lib/l10n/gen/app_localizations.dart",
|
|
453
|
+
"lib/l10n/gen/app_localizations_en.dart",
|
|
454
|
+
"lib/l10n/gen/app_localizations_es.dart",
|
|
455
|
+
"lib/core/config/app_config.dart",
|
|
456
|
+
"android/app/src/main/AndroidManifest.xml",
|
|
457
|
+
"ios/Runner/Info.plist",
|
|
458
|
+
"web/index.html",
|
|
459
|
+
"web/manifest.json",
|
|
460
|
+
"linux/runner/my_application.cc",
|
|
461
|
+
"windows/runner/main.cpp"
|
|
462
|
+
]);
|
|
463
|
+
var FLUTTER_RUNNER_RC = "windows/runner/Runner.rc";
|
|
464
|
+
var FLUTTER_NATIVE_ROOTS = ["kotlin", "java"];
|
|
465
|
+
function isFlutterTextFile(filePath) {
|
|
466
|
+
if (FLUTTER_TEXT_EXTENSIONS.has(extname(filePath).toLowerCase())) {
|
|
467
|
+
return true;
|
|
468
|
+
}
|
|
469
|
+
return FLUTTER_TEXT_BASENAMES.has(basename(filePath).toLowerCase());
|
|
470
|
+
}
|
|
471
|
+
function replaceToken(content, token, value) {
|
|
472
|
+
return content.split(token).join(value);
|
|
473
|
+
}
|
|
474
|
+
function matchPackageDeclaration(content) {
|
|
475
|
+
const match = content.match(
|
|
476
|
+
/^[ \t]*package[ \t]+([A-Za-z_][\w.]*)[ \t]*;?[ \t]*$/m
|
|
477
|
+
);
|
|
478
|
+
return match?.[1];
|
|
479
|
+
}
|
|
480
|
+
function substituteRunnerRc(content, dartName, displayName, org) {
|
|
481
|
+
return content.split("\n").map((line) => {
|
|
482
|
+
const isInternalName = line.includes("InternalName") || line.includes("OriginalFilename");
|
|
483
|
+
let replaced = replaceToken(
|
|
484
|
+
line,
|
|
485
|
+
"fwiflu",
|
|
486
|
+
isInternalName ? dartName : displayName
|
|
487
|
+
);
|
|
488
|
+
replaced = replaceToken(replaced, "mx.apto", org);
|
|
489
|
+
return replaced;
|
|
490
|
+
}).join("\n");
|
|
491
|
+
}
|
|
492
|
+
async function getAllFilesOrEmpty(dir) {
|
|
493
|
+
try {
|
|
494
|
+
return await getAllFiles(dir);
|
|
495
|
+
} catch {
|
|
496
|
+
return [];
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
async function removeEmptySubdirectories(dir) {
|
|
500
|
+
let entries;
|
|
501
|
+
try {
|
|
502
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
503
|
+
} catch {
|
|
504
|
+
return false;
|
|
505
|
+
}
|
|
506
|
+
let isEmpty = true;
|
|
507
|
+
for (const entry of entries) {
|
|
508
|
+
const childPath = join(dir, entry.name);
|
|
509
|
+
if (!entry.isDirectory()) {
|
|
510
|
+
isEmpty = false;
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
if (await removeEmptySubdirectories(childPath)) {
|
|
514
|
+
await rm(childPath, { recursive: true, force: true });
|
|
515
|
+
} else {
|
|
516
|
+
isEmpty = false;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return isEmpty;
|
|
520
|
+
}
|
|
521
|
+
async function substituteFlutter(dir, projectName, orgInput) {
|
|
522
|
+
const dartName = toDartPackageName(projectName);
|
|
523
|
+
const displayName = projectName.trim();
|
|
524
|
+
const org = normalizeOrg(orgInput);
|
|
525
|
+
const androidMainDir = join(dir, "android", "app", "src", "main");
|
|
526
|
+
const nativeFiles = [];
|
|
527
|
+
for (const root of FLUTTER_NATIVE_ROOTS) {
|
|
528
|
+
const files = await getAllFilesOrEmpty(join(androidMainDir, root));
|
|
529
|
+
for (const filePath of files) {
|
|
530
|
+
const ext = extname(filePath).toLowerCase();
|
|
531
|
+
if (ext !== ".kt" && ext !== ".java") continue;
|
|
532
|
+
try {
|
|
533
|
+
const content = await readFile(filePath, "utf-8");
|
|
534
|
+
if (matchPackageDeclaration(content) !== void 0) {
|
|
535
|
+
nativeFiles.push({ path: filePath, root });
|
|
536
|
+
}
|
|
537
|
+
} catch {
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
const allFiles = await getAllFiles(dir);
|
|
542
|
+
for (const filePath of allFiles) {
|
|
543
|
+
if (!isFlutterTextFile(filePath)) continue;
|
|
544
|
+
let content;
|
|
545
|
+
try {
|
|
546
|
+
content = await readFile(filePath, "utf-8");
|
|
547
|
+
} catch {
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
const relPath = relative(dir, filePath).split(sep).join("/");
|
|
551
|
+
let replaced;
|
|
552
|
+
if (relPath === FLUTTER_RUNNER_RC) {
|
|
553
|
+
replaced = substituteRunnerRc(content, dartName, displayName, org);
|
|
554
|
+
} else if (FLUTTER_DISPLAY_FILES.has(relPath)) {
|
|
555
|
+
replaced = replaceToken(content, "fwiflu", displayName);
|
|
556
|
+
replaced = replaceToken(replaced, "mx.apto", org);
|
|
557
|
+
} else {
|
|
558
|
+
replaced = replaceToken(content, "fwiflu", dartName);
|
|
559
|
+
replaced = replaceToken(replaced, "mx.apto", org);
|
|
560
|
+
}
|
|
561
|
+
if (extname(filePath).toLowerCase() === ".dart") {
|
|
562
|
+
replaced = sortDartDirectiveRuns(replaced);
|
|
563
|
+
}
|
|
564
|
+
if (replaced !== content) {
|
|
565
|
+
await writeFile(filePath, replaced);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
for (const nativeFile of nativeFiles) {
|
|
569
|
+
let content;
|
|
570
|
+
try {
|
|
571
|
+
content = await readFile(nativeFile.path, "utf-8");
|
|
572
|
+
} catch {
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
const newPackage = matchPackageDeclaration(content);
|
|
576
|
+
if (newPackage === void 0) continue;
|
|
577
|
+
const expectedDir = join(
|
|
578
|
+
androidMainDir,
|
|
579
|
+
nativeFile.root,
|
|
580
|
+
...newPackage.split(".")
|
|
581
|
+
);
|
|
582
|
+
if (dirname(nativeFile.path) === expectedDir) continue;
|
|
583
|
+
await mkdir(expectedDir, { recursive: true });
|
|
584
|
+
await rename(nativeFile.path, join(expectedDir, basename(nativeFile.path)));
|
|
585
|
+
}
|
|
586
|
+
for (const root of FLUTTER_NATIVE_ROOTS) {
|
|
587
|
+
await removeEmptySubdirectories(join(androidMainDir, root));
|
|
588
|
+
}
|
|
589
|
+
}
|
|
193
590
|
async function getAllFiles(dir) {
|
|
194
591
|
const results = [];
|
|
195
592
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
@@ -450,6 +847,18 @@ async function selectSkillsAction() {
|
|
|
450
847
|
]);
|
|
451
848
|
return action;
|
|
452
849
|
}
|
|
850
|
+
async function askFlutterOrg() {
|
|
851
|
+
const { org } = await inquirer.prompt([
|
|
852
|
+
{
|
|
853
|
+
type: "input",
|
|
854
|
+
name: "org",
|
|
855
|
+
message: "Organizaci\xF3n para el bundle id (reverse-domain, ej. com.example):",
|
|
856
|
+
default: FLUTTER_ORG_DEFAULT,
|
|
857
|
+
validate: checkOrg
|
|
858
|
+
}
|
|
859
|
+
]);
|
|
860
|
+
return normalizeOrg(org);
|
|
861
|
+
}
|
|
453
862
|
async function askGlobalSkillsToo() {
|
|
454
863
|
const { global } = await inquirer.prompt([
|
|
455
864
|
{
|
|
@@ -908,9 +1317,9 @@ async function readTemplatePostInit(dir) {
|
|
|
908
1317
|
return void 0;
|
|
909
1318
|
}
|
|
910
1319
|
}
|
|
911
|
-
async function createFullStackProject(projectPath, backendTpl, frontendTpl, pm) {
|
|
1320
|
+
async function createFullStackProject(projectPath, backendTpl, frontendTpl, pm, org) {
|
|
912
1321
|
const { tempDir, repoRoot } = await downloadAndExtractRepo();
|
|
913
|
-
const projectName =
|
|
1322
|
+
const projectName = basename2(projectPath);
|
|
914
1323
|
const backendPath = join3(projectPath, "backend");
|
|
915
1324
|
const frontendPath = join3(projectPath, "frontend");
|
|
916
1325
|
await copyTemplate(
|
|
@@ -921,8 +1330,12 @@ async function createFullStackProject(projectPath, backendTpl, frontendTpl, pm)
|
|
|
921
1330
|
join3(repoRoot, "templates", frontendTpl.folder),
|
|
922
1331
|
frontendPath
|
|
923
1332
|
);
|
|
924
|
-
await substituteTemplate(backendPath, projectName, backendTpl.folder
|
|
925
|
-
|
|
1333
|
+
await substituteTemplate(backendPath, projectName, backendTpl.folder, {
|
|
1334
|
+
org
|
|
1335
|
+
});
|
|
1336
|
+
await substituteTemplate(frontendPath, projectName, frontendTpl.folder, {
|
|
1337
|
+
org
|
|
1338
|
+
});
|
|
926
1339
|
await installAgentSkills(repoRoot, projectPath);
|
|
927
1340
|
if (backendTpl.runtime === "bun" && pm !== "bun") {
|
|
928
1341
|
await adaptToNodeRuntime(backendPath);
|
|
@@ -1073,10 +1486,20 @@ program.command("skills").description(
|
|
|
1073
1486
|
process.exit(1);
|
|
1074
1487
|
}
|
|
1075
1488
|
});
|
|
1076
|
-
program.argument("[template]", "Template a usar (o 'fullstack' para backend + frontend)").argument("[project-name]", "Nombre del proyecto").option("-p, --pm <package-manager>", "Package manager a usar: npm, pnpm o bun").option(
|
|
1489
|
+
program.argument("[template]", "Template a usar (o 'fullstack' para backend + frontend)").argument("[project-name]", "Nombre del proyecto").option("-p, --pm <package-manager>", "Package manager a usar: npm, pnpm o bun").option(
|
|
1490
|
+
"-o, --org <domain>",
|
|
1491
|
+
"Organizaci\xF3n reverse-domain para el bundle id de Flutter (ej: ar.miempresa)"
|
|
1492
|
+
).option("--backend", "Empaquetar el c\xF3digo en backend/ (saltea la pregunta)").option("--no-backend", "Dejar el c\xF3digo en la ra\xEDz del proyecto (saltea la pregunta)").option("-b, --backend-template <tpl>", "Backend a usar en fullstack (saltea la pregunta)").option("-f, --frontend-template <tpl>", "Frontend a usar en fullstack (saltea la pregunta)").option("--git", "Inicializar un repositorio git (saltea la pregunta)").option("--no-git", "No inicializar git (saltea la pregunta)").action(
|
|
1077
1493
|
async (templateArg, projectNameArg) => {
|
|
1078
1494
|
try {
|
|
1079
|
-
const {
|
|
1495
|
+
const {
|
|
1496
|
+
pm: pmArg,
|
|
1497
|
+
org: orgArg,
|
|
1498
|
+
backend,
|
|
1499
|
+
backendTemplate,
|
|
1500
|
+
frontendTemplate,
|
|
1501
|
+
git
|
|
1502
|
+
} = program.opts();
|
|
1080
1503
|
if (pmArg && !PACKAGE_MANAGERS.includes(pmArg)) {
|
|
1081
1504
|
console.error(
|
|
1082
1505
|
chalk.red(
|
|
@@ -1163,10 +1586,29 @@ program.argument("[template]", "Template a usar (o 'fullstack' para backend + fr
|
|
|
1163
1586
|
if (!projectName) {
|
|
1164
1587
|
projectName = await askProjectName();
|
|
1165
1588
|
}
|
|
1589
|
+
const runtimes = template ? [template.runtime] : [backendTpl.runtime, frontendTpl.runtime];
|
|
1590
|
+
let org;
|
|
1591
|
+
if (runtimes.includes("flutter")) {
|
|
1592
|
+
if (orgArg) {
|
|
1593
|
+
const check = checkOrg(orgArg);
|
|
1594
|
+
if (check !== true) {
|
|
1595
|
+
console.error(chalk.red(`
|
|
1596
|
+
\u2716 ${check}
|
|
1597
|
+
`));
|
|
1598
|
+
process.exit(1);
|
|
1599
|
+
}
|
|
1600
|
+
org = normalizeOrg(orgArg);
|
|
1601
|
+
} else if (git === void 0) {
|
|
1602
|
+
org = await askFlutterOrg();
|
|
1603
|
+
} else {
|
|
1604
|
+
org = FLUTTER_ORG_DEFAULT;
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1166
1607
|
const layer = template?.layer;
|
|
1167
1608
|
const useLayerLayout = !template ? false : backend === void 0 ? await askLayerLayout(layer) : backend;
|
|
1168
|
-
const
|
|
1169
|
-
|
|
1609
|
+
const needsPm = runtimes.some(
|
|
1610
|
+
(r) => r !== "dotnet" && r !== "flutter"
|
|
1611
|
+
);
|
|
1170
1612
|
if (!pm && needsPm) {
|
|
1171
1613
|
pm = await selectPackageManager();
|
|
1172
1614
|
}
|
|
@@ -1190,7 +1632,8 @@ program.argument("[template]", "Template a usar (o 'fullstack' para backend + fr
|
|
|
1190
1632
|
projectPath,
|
|
1191
1633
|
backendTpl,
|
|
1192
1634
|
frontendTpl,
|
|
1193
|
-
pm ?? "npm"
|
|
1635
|
+
pm ?? "npm",
|
|
1636
|
+
org
|
|
1194
1637
|
);
|
|
1195
1638
|
spinner.succeed("Template descargado");
|
|
1196
1639
|
} else {
|
|
@@ -1198,7 +1641,9 @@ program.argument("[template]", "Template a usar (o 'fullstack' para backend + fr
|
|
|
1198
1641
|
const codePath = useLayerLayout ? join3(projectPath, layer) : projectPath;
|
|
1199
1642
|
spinner.text = "Creando proyecto...";
|
|
1200
1643
|
await copyTemplate(templatePath, codePath);
|
|
1201
|
-
await substituteTemplate(codePath, projectName, template.folder
|
|
1644
|
+
await substituteTemplate(codePath, projectName, template.folder, {
|
|
1645
|
+
org
|
|
1646
|
+
});
|
|
1202
1647
|
await installAgentSkills(repoRoot, projectPath);
|
|
1203
1648
|
if (useLayerLayout) {
|
|
1204
1649
|
await writeRootGitignore(projectPath);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fwinit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "CLI para crear proyectos desde templates",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"dev": "tsup src/index.ts --format esm --watch",
|
|
15
15
|
"start": "node dist/index.js",
|
|
16
16
|
"typecheck": "tsc --noEmit",
|
|
17
|
+
"test": "bun test",
|
|
17
18
|
"prepublishOnly": "chmod +x dist/index.js"
|
|
18
19
|
},
|
|
19
20
|
"dependencies": {
|
|
@@ -25,6 +26,7 @@
|
|
|
25
26
|
},
|
|
26
27
|
"devDependencies": {
|
|
27
28
|
"@types/adm-zip": "^0.5.7",
|
|
29
|
+
"@types/bun": "^1.3.0",
|
|
28
30
|
"@types/node": "^22.13.10",
|
|
29
31
|
"tsup": "^8.4.0",
|
|
30
32
|
"typescript": "^5.8.2"
|