@vitongovisk/forge 1.0.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 +1 -0
- package/dist/cli.js +752 -0
- package/dist/templates/api-client-instance.tpl +17 -0
- package/dist/templates/api-client.ts.tpl +50 -0
- package/dist/templates/api.tpl +5 -0
- package/dist/templates/api.types.tpl +29 -0
- package/dist/templates/api.utils.tpl +26 -0
- package/dist/templates/global.types.tpl +17 -0
- package/dist/templates/model-type.tpl +3 -0
- package/dist/templates/operation-hook.tpl +9 -0
- package/dist/templates/operation-types.tpl +17 -0
- package/dist/templates/service.tpl +6 -0
- package/dist/templates/shared.types.tpl +16 -0
- package/package.json +56 -0
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"# forge"
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,752 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/bin/cli.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/init.ts
|
|
7
|
+
import path4 from "path";
|
|
8
|
+
import readline from "readline";
|
|
9
|
+
import { execSync } from "child_process";
|
|
10
|
+
|
|
11
|
+
// src/utils/file.ts
|
|
12
|
+
import fs from "fs-extra";
|
|
13
|
+
import path from "path";
|
|
14
|
+
function fileExists(path11) {
|
|
15
|
+
return fs.existsSync(path11);
|
|
16
|
+
}
|
|
17
|
+
function readJson(path11) {
|
|
18
|
+
return fs.readJSONSync(path11);
|
|
19
|
+
}
|
|
20
|
+
async function writeFileSafe(filePath, content) {
|
|
21
|
+
await fs.ensureDir(path.dirname(filePath));
|
|
22
|
+
await fs.writeFile(filePath, content);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// src/utils/template.ts
|
|
26
|
+
import fs2 from "fs-extra";
|
|
27
|
+
import path2 from "path";
|
|
28
|
+
function loadTemplate(templateName) {
|
|
29
|
+
const cliPath = process.argv[1];
|
|
30
|
+
const distDir = path2.dirname(cliPath);
|
|
31
|
+
const templatePath = path2.join(distDir, "templates", templateName);
|
|
32
|
+
if (!fs2.existsSync(templatePath)) {
|
|
33
|
+
throw new Error(`Template n\xE3o encontrado: ${templatePath}`);
|
|
34
|
+
}
|
|
35
|
+
return fs2.readFileSync(templatePath, "utf-8");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// src/utils/config.ts
|
|
39
|
+
import fs3 from "fs-extra";
|
|
40
|
+
import path3 from "path";
|
|
41
|
+
var CONFIG_NAME = ".forge.json";
|
|
42
|
+
var DATA_NAME = "forge.data.json";
|
|
43
|
+
var STATE_NAME = "forge.state.json";
|
|
44
|
+
function getConfigPath() {
|
|
45
|
+
return path3.join(process.cwd(), CONFIG_NAME);
|
|
46
|
+
}
|
|
47
|
+
function getDataPath() {
|
|
48
|
+
return path3.join(process.cwd(), DATA_NAME);
|
|
49
|
+
}
|
|
50
|
+
function getStatePath() {
|
|
51
|
+
return path3.join(process.cwd(), STATE_NAME);
|
|
52
|
+
}
|
|
53
|
+
function ensureForgeInitialized() {
|
|
54
|
+
const configPath = getConfigPath();
|
|
55
|
+
if (!fs3.existsSync(configPath)) {
|
|
56
|
+
console.error("\u274C Forge n\xE3o foi inicializado. Rode 'forge init'.");
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
return fs3.readJSONSync(configPath);
|
|
60
|
+
}
|
|
61
|
+
function ensureForgeData() {
|
|
62
|
+
const dataPath = getDataPath();
|
|
63
|
+
if (!fs3.existsSync(dataPath)) {
|
|
64
|
+
return { resources: [] };
|
|
65
|
+
}
|
|
66
|
+
return fs3.readJSONSync(dataPath);
|
|
67
|
+
}
|
|
68
|
+
function ensureForgeState() {
|
|
69
|
+
const statePath = getStatePath();
|
|
70
|
+
if (!fs3.existsSync(statePath)) {
|
|
71
|
+
return {
|
|
72
|
+
project: { architecture: "", lastSync: "" },
|
|
73
|
+
sync: { mode: "", lastRun: "" },
|
|
74
|
+
resources: []
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return fs3.readJSONSync(statePath);
|
|
78
|
+
}
|
|
79
|
+
async function saveConfig(config) {
|
|
80
|
+
await fs3.writeJSON(getConfigPath(), config, { spaces: 2 });
|
|
81
|
+
}
|
|
82
|
+
async function saveData(data) {
|
|
83
|
+
await fs3.writeJSON(getDataPath(), data, { spaces: 2 });
|
|
84
|
+
}
|
|
85
|
+
async function saveState(state) {
|
|
86
|
+
await fs3.writeJSON(getStatePath(), state, { spaces: 2 });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// src/commands/init.ts
|
|
90
|
+
function askUser(question) {
|
|
91
|
+
const rl = readline.createInterface({
|
|
92
|
+
input: process.stdin,
|
|
93
|
+
output: process.stdout
|
|
94
|
+
});
|
|
95
|
+
return new Promise((resolve) => {
|
|
96
|
+
rl.question(question, (answer) => {
|
|
97
|
+
rl.close();
|
|
98
|
+
resolve(answer);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
async function initCommand(options) {
|
|
103
|
+
const cwd = process.cwd();
|
|
104
|
+
const configPath = getConfigPath();
|
|
105
|
+
if (fileExists(configPath) && !options.overwrite) {
|
|
106
|
+
console.log("\u26A0\uFE0F Forge j\xE1 foi inicializado neste projeto.");
|
|
107
|
+
console.log("\u{1F449} Use --overwrite para reconfigurar.\n");
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (fileExists(configPath) && options.overwrite) {
|
|
111
|
+
console.log("\u267B\uFE0F Reconfigurando projeto Forge...\n");
|
|
112
|
+
}
|
|
113
|
+
const packagePath = path4.join(cwd, "package.json");
|
|
114
|
+
if (!fileExists(packagePath)) {
|
|
115
|
+
console.error("\u274C package.json n\xE3o encontrado.");
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
const pkg = readJson(packagePath);
|
|
119
|
+
const hasAxios = !!pkg.dependencies?.axios;
|
|
120
|
+
const hasTanstack = !!pkg.dependencies?.["@tanstack/react-query"];
|
|
121
|
+
if (!hasAxios || !hasTanstack) {
|
|
122
|
+
console.log("\u274C Depend\xEAncias obrigat\xF3rias n\xE3o encontradas:\n");
|
|
123
|
+
if (!hasAxios) console.log("- axios");
|
|
124
|
+
if (!hasTanstack) console.log("- @tanstack/react-query");
|
|
125
|
+
console.log("\n\u26A0\uFE0F Essas depend\xEAncias s\xE3o obrigat\xF3rias para o Forge.");
|
|
126
|
+
const answer = await askUser("\nDeseja instalar automaticamente? (Y/N): ");
|
|
127
|
+
if (answer.toLowerCase() !== "y") {
|
|
128
|
+
console.log("\n\u274C Instala\xE7\xE3o cancelada.");
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
console.log("\n\u{1F4E6} Instalando depend\xEAncias...\n");
|
|
133
|
+
execSync("npm install axios @tanstack/react-query", {
|
|
134
|
+
stdio: "inherit"
|
|
135
|
+
});
|
|
136
|
+
console.log("\n\u2705 Depend\xEAncias instaladas!\n");
|
|
137
|
+
} catch {
|
|
138
|
+
console.error("\n\u274C Erro ao instalar depend\xEAncias.");
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const structureAnswer = await askUser(
|
|
143
|
+
"\nQual estrutura deseja usar?\n1 - Por camada\n2 - Por m\xF3dulo\nEscolha (1/2): "
|
|
144
|
+
);
|
|
145
|
+
const syncAnswer = await askUser(
|
|
146
|
+
"\nQual o modo de sincroniza\xE7\xE3o?\n1 - Autom\xE1tico (auto)\n2 - Manual (manual)\nEscolha (1/2): "
|
|
147
|
+
);
|
|
148
|
+
let config;
|
|
149
|
+
const syncMode = syncAnswer === "2" ? "manual" : "auto";
|
|
150
|
+
if (structureAnswer === "2") {
|
|
151
|
+
config = {
|
|
152
|
+
project: {
|
|
153
|
+
architecture: "module",
|
|
154
|
+
paths: {
|
|
155
|
+
api: "",
|
|
156
|
+
service: "",
|
|
157
|
+
types: "",
|
|
158
|
+
hooks: ""
|
|
159
|
+
},
|
|
160
|
+
modulePath: "src/modules",
|
|
161
|
+
apiFile: "src/modules/api/api-client.ts"
|
|
162
|
+
},
|
|
163
|
+
sync: {
|
|
164
|
+
mode: syncMode
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
} else {
|
|
168
|
+
config = {
|
|
169
|
+
project: {
|
|
170
|
+
architecture: "layer",
|
|
171
|
+
paths: {
|
|
172
|
+
api: "src/api",
|
|
173
|
+
service: "src/services",
|
|
174
|
+
types: "src/types",
|
|
175
|
+
hooks: "src/hooks"
|
|
176
|
+
},
|
|
177
|
+
modulePath: "",
|
|
178
|
+
apiFile: "src/api/api-client.ts"
|
|
179
|
+
},
|
|
180
|
+
sync: {
|
|
181
|
+
mode: syncMode
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
await saveConfig(config);
|
|
186
|
+
await saveData({ resources: [] });
|
|
187
|
+
await saveState({
|
|
188
|
+
project: {
|
|
189
|
+
architecture: config.project.architecture,
|
|
190
|
+
lastSync: (/* @__PURE__ */ new Date()).toISOString()
|
|
191
|
+
},
|
|
192
|
+
sync: {
|
|
193
|
+
mode: config.sync.mode,
|
|
194
|
+
lastRun: ""
|
|
195
|
+
},
|
|
196
|
+
resources: []
|
|
197
|
+
});
|
|
198
|
+
let apiClientPath = "";
|
|
199
|
+
let apiTypesPath = "";
|
|
200
|
+
if (config.project.architecture === "module") {
|
|
201
|
+
apiClientPath = path4.join(cwd, "src/modules/api/api-client.ts");
|
|
202
|
+
apiTypesPath = path4.join(cwd, "src/modules/api/api.types.ts");
|
|
203
|
+
} else {
|
|
204
|
+
apiClientPath = path4.join(cwd, "src/api/api-client.ts");
|
|
205
|
+
apiTypesPath = path4.join(cwd, "src/types/api.types.ts");
|
|
206
|
+
}
|
|
207
|
+
if (fileExists(apiClientPath) && !options.overwrite) {
|
|
208
|
+
console.log("\u2139\uFE0F api-client.ts j\xE1 existe.");
|
|
209
|
+
} else {
|
|
210
|
+
const template = loadTemplate("api-client.ts.tpl");
|
|
211
|
+
await writeFileSafe(apiClientPath, template);
|
|
212
|
+
console.log(
|
|
213
|
+
fileExists(apiClientPath) ? "\u267B\uFE0F api-client.ts sobrescrito." : "\u{1F4E1} api-client.ts criado."
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
const globalTypesPath = path4.join(cwd, "src/types/global.types.ts");
|
|
217
|
+
const sharedTypesPath = path4.join(cwd, "src/types/shared.types.ts");
|
|
218
|
+
const globalTemplate = loadTemplate("global.types.tpl");
|
|
219
|
+
const apiTemplate = loadTemplate("api.types.tpl");
|
|
220
|
+
const sharedTemplate = loadTemplate("shared.types.tpl");
|
|
221
|
+
if (!fileExists(globalTypesPath) || options.overwrite) {
|
|
222
|
+
await writeFileSafe(globalTypesPath, globalTemplate);
|
|
223
|
+
console.log("\u{1F4E6} global.types criado.");
|
|
224
|
+
}
|
|
225
|
+
if (!fileExists(apiTypesPath) || options.overwrite) {
|
|
226
|
+
await writeFileSafe(apiTypesPath, apiTemplate);
|
|
227
|
+
console.log("\u{1F4E6} api.types criado.");
|
|
228
|
+
}
|
|
229
|
+
if (!fileExists(sharedTypesPath) || options.overwrite) {
|
|
230
|
+
await writeFileSafe(sharedTypesPath, sharedTemplate);
|
|
231
|
+
console.log("\u{1F4E6} shared.types criado.");
|
|
232
|
+
}
|
|
233
|
+
const apiUtilsPath = path4.join(cwd, "src/utils/api.utils.ts");
|
|
234
|
+
const apiUtilsTemplate = loadTemplate("api.utils.tpl");
|
|
235
|
+
if (!fileExists(apiUtilsPath) || options.overwrite) {
|
|
236
|
+
await writeFileSafe(apiUtilsPath, apiUtilsTemplate);
|
|
237
|
+
console.log("\u{1F9E0} api.utils criado.");
|
|
238
|
+
}
|
|
239
|
+
console.log("\n\u2705 Forge inicializado com sucesso!");
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// src/commands/make-client.ts
|
|
243
|
+
import fs4 from "fs-extra";
|
|
244
|
+
import path5 from "path";
|
|
245
|
+
|
|
246
|
+
// src/utils/string.ts
|
|
247
|
+
function toCamelCase(str) {
|
|
248
|
+
return str.charAt(0).toLowerCase() + str.slice(1);
|
|
249
|
+
}
|
|
250
|
+
function toPascalCase(str) {
|
|
251
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
252
|
+
}
|
|
253
|
+
function toUpperSnake(str) {
|
|
254
|
+
return str.replace(/([a-z])([A-Z])/g, "$1_$2").toUpperCase();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// src/commands/make-client.ts
|
|
258
|
+
async function makeClientCommand(name, options) {
|
|
259
|
+
const cwd = process.cwd();
|
|
260
|
+
const config = ensureForgeInitialized();
|
|
261
|
+
const apiClientPath = path5.join(cwd, config.project.apiFile);
|
|
262
|
+
if (!fs4.existsSync(apiClientPath)) {
|
|
263
|
+
console.error("\u274C api-client.ts n\xE3o encontrado.");
|
|
264
|
+
process.exit(1);
|
|
265
|
+
}
|
|
266
|
+
const camelName = toCamelCase(name);
|
|
267
|
+
const upperName = toUpperSnake(name);
|
|
268
|
+
const fileContent = await fs4.readFile(apiClientPath, "utf-8");
|
|
269
|
+
const alreadyExists = fileContent.includes(
|
|
270
|
+
`export const ${camelName}Api`
|
|
271
|
+
);
|
|
272
|
+
if (alreadyExists && !options.overwrite) {
|
|
273
|
+
console.log(
|
|
274
|
+
`\u26A0\uFE0F ${camelName}Api j\xE1 existe. Use --overwrite para recriar.`
|
|
275
|
+
);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
const templateRaw = loadTemplate("api-client-instance.tpl");
|
|
279
|
+
const template = templateRaw.replace(/{{camelName}}/g, camelName).replace(/{{upperName}}/g, upperName);
|
|
280
|
+
let newContent = fileContent;
|
|
281
|
+
if (alreadyExists) {
|
|
282
|
+
const regex = new RegExp(
|
|
283
|
+
`export const ${camelName}Api[\\s\\S]*?setupInterceptors\\(${camelName}Api.*?\\);`,
|
|
284
|
+
"g"
|
|
285
|
+
);
|
|
286
|
+
newContent = newContent.replace(regex, template);
|
|
287
|
+
} else {
|
|
288
|
+
newContent += `
|
|
289
|
+
|
|
290
|
+
${template}
|
|
291
|
+
`;
|
|
292
|
+
}
|
|
293
|
+
await writeFileSafe(apiClientPath, newContent);
|
|
294
|
+
console.log(
|
|
295
|
+
alreadyExists ? `\u267B\uFE0F ${camelName}Api sobrescrito.` : `\u2705 ${camelName}Api criado.`
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// src/commands/make-api.ts
|
|
300
|
+
import fs6 from "fs-extra";
|
|
301
|
+
import path7 from "path";
|
|
302
|
+
|
|
303
|
+
// src/commands/sync.ts
|
|
304
|
+
import fs5 from "fs-extra";
|
|
305
|
+
import path6 from "path";
|
|
306
|
+
async function syncCommand() {
|
|
307
|
+
console.log("\u{1F504} Sincronizando projeto...");
|
|
308
|
+
const config = ensureForgeInitialized();
|
|
309
|
+
const data = ensureForgeData();
|
|
310
|
+
const state = ensureForgeState();
|
|
311
|
+
const cwd = process.cwd();
|
|
312
|
+
state.project.architecture = config.project.architecture;
|
|
313
|
+
state.project.lastSync = (/* @__PURE__ */ new Date()).toISOString();
|
|
314
|
+
state.sync.mode = config.sync.mode;
|
|
315
|
+
state.sync.lastRun = (/* @__PURE__ */ new Date()).toISOString();
|
|
316
|
+
const observedResources = [];
|
|
317
|
+
for (const intendedResource of data.resources) {
|
|
318
|
+
const name = intendedResource.name;
|
|
319
|
+
let resourcePath = "";
|
|
320
|
+
if (config.project.architecture === "module") {
|
|
321
|
+
resourcePath = path6.join(config.project.modulePath, name);
|
|
322
|
+
}
|
|
323
|
+
const files = {
|
|
324
|
+
api: { exists: false, path: "" },
|
|
325
|
+
service: { exists: false, path: "" },
|
|
326
|
+
type: { exists: false, path: "" },
|
|
327
|
+
hooks: { exists: false, path: "" }
|
|
328
|
+
};
|
|
329
|
+
if (config.project.architecture === "module") {
|
|
330
|
+
files.api.path = path6.join(resourcePath, `${name}.api.ts`);
|
|
331
|
+
files.service.path = path6.join(resourcePath, `${name}.service.ts`);
|
|
332
|
+
files.type.path = path6.join(resourcePath, `${name}.type.ts`);
|
|
333
|
+
files.hooks.path = path6.join(resourcePath, `${name}.hooks.ts`);
|
|
334
|
+
} else {
|
|
335
|
+
files.api.path = path6.join(config.project.paths.api, `${name}.api.ts`);
|
|
336
|
+
files.service.path = path6.join(config.project.paths.service, `${name}.service.ts`);
|
|
337
|
+
files.type.path = path6.join(config.project.paths.types, `${name}.type.ts`);
|
|
338
|
+
files.hooks.path = path6.join(config.project.paths.hooks, `${name}.hooks.ts`);
|
|
339
|
+
}
|
|
340
|
+
files.api.exists = fileExists(path6.join(cwd, files.api.path));
|
|
341
|
+
files.service.exists = fileExists(path6.join(cwd, files.service.path));
|
|
342
|
+
files.type.exists = fileExists(path6.join(cwd, files.type.path));
|
|
343
|
+
files.hooks.exists = fileExists(path6.join(cwd, files.hooks.path));
|
|
344
|
+
const isMissingCritical = !files.api.exists || !files.service.exists;
|
|
345
|
+
const status = isMissingCritical ? "missing" : "active";
|
|
346
|
+
observedResources.push({
|
|
347
|
+
name,
|
|
348
|
+
status,
|
|
349
|
+
files,
|
|
350
|
+
methods: {}
|
|
351
|
+
// TODO: Implementar scan de métodos no futuro
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
const foundResources = /* @__PURE__ */ new Set();
|
|
355
|
+
if (config.project.architecture === "module") {
|
|
356
|
+
const moduleDir = path6.join(cwd, config.project.modulePath);
|
|
357
|
+
if (fs5.existsSync(moduleDir)) {
|
|
358
|
+
const dirs = fs5.readdirSync(moduleDir, { withFileTypes: true });
|
|
359
|
+
for (const dir of dirs) {
|
|
360
|
+
if (dir.isDirectory() && dir.name !== "api") {
|
|
361
|
+
foundResources.add(dir.name);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
} else {
|
|
366
|
+
const pathsToScan = [
|
|
367
|
+
config.project.paths.api,
|
|
368
|
+
config.project.paths.service,
|
|
369
|
+
config.project.paths.types,
|
|
370
|
+
config.project.paths.hooks
|
|
371
|
+
];
|
|
372
|
+
for (const p of pathsToScan) {
|
|
373
|
+
if (!p) continue;
|
|
374
|
+
const dir = path6.join(cwd, p);
|
|
375
|
+
if (fs5.existsSync(dir)) {
|
|
376
|
+
const files = fs5.readdirSync(dir);
|
|
377
|
+
for (const file of files) {
|
|
378
|
+
const match = file.match(/^(.+)\.(api|service|type|hooks)\.ts$/);
|
|
379
|
+
if (match) {
|
|
380
|
+
foundResources.add(match[1]);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
for (const name of foundResources) {
|
|
387
|
+
if (!data.resources.find((r) => r.name === name)) {
|
|
388
|
+
let resourcePath = "";
|
|
389
|
+
if (config.project.architecture === "module") {
|
|
390
|
+
resourcePath = path6.join(config.project.modulePath, name);
|
|
391
|
+
}
|
|
392
|
+
const files = {
|
|
393
|
+
api: { exists: false, path: "" },
|
|
394
|
+
service: { exists: false, path: "" },
|
|
395
|
+
type: { exists: false, path: "" },
|
|
396
|
+
hooks: { exists: false, path: "" }
|
|
397
|
+
};
|
|
398
|
+
if (config.project.architecture === "module") {
|
|
399
|
+
files.api.path = path6.join(resourcePath, `${name}.api.ts`);
|
|
400
|
+
files.service.path = path6.join(resourcePath, `${name}.service.ts`);
|
|
401
|
+
files.type.path = path6.join(resourcePath, `${name}.type.ts`);
|
|
402
|
+
files.hooks.path = path6.join(resourcePath, `${name}.hooks.ts`);
|
|
403
|
+
} else {
|
|
404
|
+
files.api.path = path6.join(config.project.paths.api, `${name}.api.ts`);
|
|
405
|
+
files.service.path = path6.join(config.project.paths.service, `${name}.service.ts`);
|
|
406
|
+
files.type.path = path6.join(config.project.paths.types, `${name}.type.ts`);
|
|
407
|
+
files.hooks.path = path6.join(config.project.paths.hooks, `${name}.hooks.ts`);
|
|
408
|
+
}
|
|
409
|
+
files.api.exists = fileExists(path6.join(cwd, files.api.path));
|
|
410
|
+
files.service.exists = fileExists(path6.join(cwd, files.service.path));
|
|
411
|
+
files.type.exists = fileExists(path6.join(cwd, files.type.path));
|
|
412
|
+
files.hooks.exists = fileExists(path6.join(cwd, files.hooks.path));
|
|
413
|
+
observedResources.push({
|
|
414
|
+
name,
|
|
415
|
+
status: "orphan",
|
|
416
|
+
files,
|
|
417
|
+
methods: {}
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
state.resources = observedResources;
|
|
422
|
+
await saveState(state);
|
|
423
|
+
console.log("\n\u2705 Sync finalizado!");
|
|
424
|
+
const missing = state.resources.filter((r) => r.status === "missing");
|
|
425
|
+
if (missing.length > 0) {
|
|
426
|
+
console.log(`
|
|
427
|
+
\u26A0\uFE0F ${missing.length} recursos com arquivos faltando:`);
|
|
428
|
+
missing.forEach((r) => console.log(` - ${r.name}`));
|
|
429
|
+
}
|
|
430
|
+
const orphans = state.resources.filter((r) => r.status === "orphan");
|
|
431
|
+
if (orphans.length > 0) {
|
|
432
|
+
console.log(`
|
|
433
|
+
\u{1F47B} ${orphans.length} recursos \xF3rf\xE3os detectados no disco:`);
|
|
434
|
+
orphans.forEach((r) => console.log(` - ${r.name}`));
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// src/commands/make-api.ts
|
|
439
|
+
async function makeApiCommand(name) {
|
|
440
|
+
const cwd = process.cwd();
|
|
441
|
+
const config = ensureForgeInitialized();
|
|
442
|
+
const pascalName = toPascalCase(name);
|
|
443
|
+
const camelName = toCamelCase(name);
|
|
444
|
+
let filePath = "";
|
|
445
|
+
let apiImportPath = "";
|
|
446
|
+
if (config.project.architecture === "layer") {
|
|
447
|
+
filePath = path7.join(cwd, config.project.paths.api, `${camelName}.api.ts`);
|
|
448
|
+
apiImportPath = "./api-client";
|
|
449
|
+
} else {
|
|
450
|
+
const moduleDir = path7.join(cwd, config.project.modulePath, camelName);
|
|
451
|
+
await fs6.ensureDir(moduleDir);
|
|
452
|
+
filePath = path7.join(moduleDir, `${camelName}.api.ts`);
|
|
453
|
+
apiImportPath = "@/api/api-client";
|
|
454
|
+
}
|
|
455
|
+
if (fs6.existsSync(filePath)) {
|
|
456
|
+
console.log("\u26A0\uFE0F API j\xE1 existe.");
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
const templateRaw = loadTemplate("api.tpl");
|
|
460
|
+
const content = templateRaw.replace(/{{PascalName}}/g, pascalName).replace(/{{camelName}}/g, camelName).replace(/{{apiImportPath}}/g, apiImportPath);
|
|
461
|
+
await writeFileSafe(filePath, content);
|
|
462
|
+
console.log(`\u2705 ${pascalName}API criada com sucesso.`);
|
|
463
|
+
const data = ensureForgeData();
|
|
464
|
+
if (!data.resources.find((r) => r.name === name)) {
|
|
465
|
+
data.resources.push({ name, methods: [] });
|
|
466
|
+
await saveData(data);
|
|
467
|
+
}
|
|
468
|
+
if (config.sync.mode === "auto") {
|
|
469
|
+
await syncCommand();
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// src/commands/make-service.ts
|
|
474
|
+
import fs7 from "fs-extra";
|
|
475
|
+
import path8 from "path";
|
|
476
|
+
async function makeServiceCommand(name) {
|
|
477
|
+
const config = ensureForgeInitialized();
|
|
478
|
+
const cwd = process.cwd();
|
|
479
|
+
const pascalName = toPascalCase(name);
|
|
480
|
+
const camelName = toCamelCase(name);
|
|
481
|
+
let filePath = "";
|
|
482
|
+
let apiImportPath = "";
|
|
483
|
+
let utilsImportPath = "@/utils/api.utils";
|
|
484
|
+
if (config.project.architecture === "layer") {
|
|
485
|
+
filePath = path8.join(cwd, config.project.paths.service, `${camelName}.service.ts`);
|
|
486
|
+
apiImportPath = `@/api/${camelName}.api`;
|
|
487
|
+
} else {
|
|
488
|
+
const moduleDir = path8.join(cwd, config.project.modulePath, camelName);
|
|
489
|
+
await fs7.ensureDir(moduleDir);
|
|
490
|
+
filePath = path8.join(moduleDir, `${camelName}.service.ts`);
|
|
491
|
+
apiImportPath = `./${camelName}.api`;
|
|
492
|
+
}
|
|
493
|
+
if (fs7.existsSync(filePath)) {
|
|
494
|
+
console.log("\u26A0\uFE0F Service j\xE1 existe.");
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
const templateRaw = loadTemplate("service.tpl");
|
|
498
|
+
const content = templateRaw.replace(/{{PascalName}}/g, pascalName).replace(/{{apiImportPath}}/g, apiImportPath).replace(/{{utilsImportPath}}/g, utilsImportPath);
|
|
499
|
+
await writeFileSafe(filePath, content);
|
|
500
|
+
console.log(`\u2705 ${pascalName}Service criado com sucesso.`);
|
|
501
|
+
const data = ensureForgeData();
|
|
502
|
+
if (!data.resources.find((r) => r.name === name)) {
|
|
503
|
+
data.resources.push({ name, methods: [] });
|
|
504
|
+
await saveData(data);
|
|
505
|
+
}
|
|
506
|
+
if (config.sync.mode === "auto") {
|
|
507
|
+
await syncCommand();
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// src/commands/make-type.ts
|
|
512
|
+
import fs8 from "fs-extra";
|
|
513
|
+
import path9 from "path";
|
|
514
|
+
async function makeTypeCommand(name, options) {
|
|
515
|
+
const config = ensureForgeInitialized();
|
|
516
|
+
const cwd = process.cwd();
|
|
517
|
+
const pascalName = toPascalCase(name);
|
|
518
|
+
const camelName = toCamelCase(name);
|
|
519
|
+
let filePath = "";
|
|
520
|
+
if (config.project.architecture === "layer") {
|
|
521
|
+
filePath = path9.join(cwd, config.project.paths.types, `${camelName}.type.ts`);
|
|
522
|
+
} else {
|
|
523
|
+
const moduleDir = path9.join(cwd, config.project.modulePath, camelName);
|
|
524
|
+
await fs8.ensureDir(moduleDir);
|
|
525
|
+
filePath = path9.join(moduleDir, `${camelName}.type.ts`);
|
|
526
|
+
}
|
|
527
|
+
if (fs8.existsSync(filePath)) {
|
|
528
|
+
console.log("\u26A0\uFE0F Type j\xE1 existe.");
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
const template = loadTemplate("model-type.tpl");
|
|
532
|
+
let fields = ` id: string;`;
|
|
533
|
+
if (!!options.timestamps) {
|
|
534
|
+
fields += `
|
|
535
|
+
created_at: string;
|
|
536
|
+
updated_at: string;`;
|
|
537
|
+
}
|
|
538
|
+
const content = template.replace(/{{PascalName}}/g, pascalName).replace(/{{fields}}/g, fields);
|
|
539
|
+
await writeFileSafe(filePath, content);
|
|
540
|
+
console.log(`\u2705 ${pascalName} type criado com sucesso.`);
|
|
541
|
+
const data = ensureForgeData();
|
|
542
|
+
if (!data.resources.find((r) => r.name === name)) {
|
|
543
|
+
data.resources.push({ name, methods: [] });
|
|
544
|
+
await saveData(data);
|
|
545
|
+
}
|
|
546
|
+
if (config.sync.mode === "auto") {
|
|
547
|
+
await syncCommand();
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// src/commands/make-resource.ts
|
|
552
|
+
async function makeResourceCommand(name, options) {
|
|
553
|
+
const config = ensureForgeInitialized();
|
|
554
|
+
const pascal = toPascalCase(name);
|
|
555
|
+
console.log(`
|
|
556
|
+
\u{1F680} Creating resource: ${pascal}
|
|
557
|
+
`);
|
|
558
|
+
try {
|
|
559
|
+
const data = ensureForgeData();
|
|
560
|
+
const exists = data.resources.find((r) => r.name === name);
|
|
561
|
+
if (!exists) {
|
|
562
|
+
data.resources.push({
|
|
563
|
+
name,
|
|
564
|
+
methods: []
|
|
565
|
+
});
|
|
566
|
+
await saveData(data);
|
|
567
|
+
}
|
|
568
|
+
console.log("\u{1F4E6} Creating type...");
|
|
569
|
+
await makeTypeCommand(name, { timestamps: true });
|
|
570
|
+
console.log("\u{1F310} Creating api...");
|
|
571
|
+
await makeApiCommand(name);
|
|
572
|
+
console.log("\u2699\uFE0F Creating service...");
|
|
573
|
+
await makeServiceCommand(name);
|
|
574
|
+
console.log(`
|
|
575
|
+
\u2705 Resource ${pascal} created successfully!
|
|
576
|
+
`);
|
|
577
|
+
const shouldSync = options.sync !== false && (config.sync.mode === "auto" || options.sync === true);
|
|
578
|
+
if (shouldSync) {
|
|
579
|
+
await syncCommand();
|
|
580
|
+
}
|
|
581
|
+
} catch (err) {
|
|
582
|
+
console.error("\u274C Error creating resource:", err);
|
|
583
|
+
process.exit(1);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// src/commands/consume.ts
|
|
588
|
+
import fs9 from "fs-extra";
|
|
589
|
+
import path10 from "path";
|
|
590
|
+
import readline2 from "readline";
|
|
591
|
+
import { Project, SyntaxKind } from "ts-morph";
|
|
592
|
+
function askUser2(question) {
|
|
593
|
+
const rl = readline2.createInterface({
|
|
594
|
+
input: process.stdin,
|
|
595
|
+
output: process.stdout
|
|
596
|
+
});
|
|
597
|
+
return new Promise((resolve) => {
|
|
598
|
+
rl.question(question, (answer) => {
|
|
599
|
+
rl.close();
|
|
600
|
+
resolve(answer);
|
|
601
|
+
});
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
async function consumeCommand(method, action, resource, apiInstance = "api") {
|
|
605
|
+
const config = ensureForgeInitialized();
|
|
606
|
+
const data = ensureForgeData();
|
|
607
|
+
const state = ensureForgeState();
|
|
608
|
+
const cwd = process.cwd();
|
|
609
|
+
const actionPascal = toPascalCase(action);
|
|
610
|
+
const actionCamel = toCamelCase(action);
|
|
611
|
+
if (!resource) {
|
|
612
|
+
if (state.resources.length === 0) {
|
|
613
|
+
console.error(`\u274C Nenhum resource encontrado. Crie um resource primeiro com 'forge make:resource'.`);
|
|
614
|
+
process.exit(1);
|
|
615
|
+
}
|
|
616
|
+
const validResources = state.resources.filter((r) => r.status !== "missing");
|
|
617
|
+
if (validResources.length === 0) {
|
|
618
|
+
console.error(`\u274C Todos os resources est\xE3o incompletos ('missing'). N\xE3o \xE9 poss\xEDvel consumir.`);
|
|
619
|
+
process.exit(1);
|
|
620
|
+
}
|
|
621
|
+
let question = "\nQual resource deseja usar?\n";
|
|
622
|
+
validResources.forEach((r, idx) => {
|
|
623
|
+
question += `${idx + 1} - ${r.name}
|
|
624
|
+
`;
|
|
625
|
+
});
|
|
626
|
+
question += `Escolha (1-${validResources.length}): `;
|
|
627
|
+
const answer = await askUser2(question);
|
|
628
|
+
const index = parseInt(answer.trim(), 10) - 1;
|
|
629
|
+
if (isNaN(index) || index < 0 || index >= validResources.length) {
|
|
630
|
+
console.error("\n\u274C Op\xE7\xE3o inv\xE1lida.");
|
|
631
|
+
process.exit(1);
|
|
632
|
+
}
|
|
633
|
+
resource = validResources[index].name;
|
|
634
|
+
}
|
|
635
|
+
const resourcePascal = toPascalCase(resource);
|
|
636
|
+
let resourceIntent = data.resources.find((r) => r.name === resource);
|
|
637
|
+
if (!resourceIntent) {
|
|
638
|
+
console.log(`
|
|
639
|
+
\u{1F47B} Resource "${resource}" estava \xF3rf\xE3o. Registrando inten\xE7\xE3o automaticamente...`);
|
|
640
|
+
resourceIntent = { name: resource, methods: [] };
|
|
641
|
+
data.resources.push(resourceIntent);
|
|
642
|
+
await saveData(data);
|
|
643
|
+
}
|
|
644
|
+
const resourceState = state.resources.find((r) => r.name === resource);
|
|
645
|
+
if (!resourceState || resourceState.status === "missing") {
|
|
646
|
+
console.error(`\u274C Resource "${resource}" est\xE1 incompleto ou ausente no filesystem.`);
|
|
647
|
+
process.exit(1);
|
|
648
|
+
}
|
|
649
|
+
console.log(`
|
|
650
|
+
\u{1F680} Consumindo opera\xE7\xE3o: ${method} ${action} em ${resource}
|
|
651
|
+
`);
|
|
652
|
+
const project = new Project();
|
|
653
|
+
const apiFilePath = path10.join(cwd, resourceState.files.api.path);
|
|
654
|
+
const moduleDir = path10.dirname(apiFilePath);
|
|
655
|
+
const hooksDir = path10.join(moduleDir, "hooks");
|
|
656
|
+
const endpointsDir = path10.join(moduleDir, "endpoints");
|
|
657
|
+
await fs9.ensureDir(hooksDir);
|
|
658
|
+
await fs9.ensureDir(endpointsDir);
|
|
659
|
+
const apiSource = project.addSourceFileAtPath(apiFilePath);
|
|
660
|
+
const apiObject = apiSource.getVariableDeclaration(`${resourcePascal}API`)?.getInitializerIfKind(SyntaxKind.ObjectLiteralExpression);
|
|
661
|
+
if (apiObject) {
|
|
662
|
+
const paramsStr = method === "PUT" || method === "DELETE" || method === "GET" ? "`${payload.params.id}`" : "";
|
|
663
|
+
const bodyStr = method === "POST" || method === "PUT" ? ", payload.body" : "";
|
|
664
|
+
const urlSuffix = paramsStr ? `/${resource}/${paramsStr}` : `"/${resource}/${actionCamel}"`;
|
|
665
|
+
apiSource.addImportDeclaration({
|
|
666
|
+
moduleSpecifier: `./endpoints/${actionCamel}.types`,
|
|
667
|
+
namedImports: [`${actionPascal}Payload`, `${actionPascal}Response`]
|
|
668
|
+
});
|
|
669
|
+
apiObject.addPropertyAssignment({
|
|
670
|
+
name: actionCamel,
|
|
671
|
+
initializer: `async (payload: ${actionPascal}Payload):Promise<${actionPascal}Response> => await ${apiInstance}.${method.toLowerCase()}<${actionPascal}Response>(${urlSuffix}${bodyStr})`
|
|
672
|
+
});
|
|
673
|
+
await apiSource.save();
|
|
674
|
+
console.log(`\u{1F310} API (${resource}.api.ts) atualizada.`);
|
|
675
|
+
}
|
|
676
|
+
const serviceFilePath = path10.join(cwd, resourceState.files.service.path);
|
|
677
|
+
const serviceSource = project.addSourceFileAtPath(serviceFilePath);
|
|
678
|
+
const serviceObject = serviceSource.getVariableDeclaration(`${resourcePascal}Service`)?.getInitializerIfKind(SyntaxKind.ObjectLiteralExpression);
|
|
679
|
+
if (serviceObject) {
|
|
680
|
+
serviceSource.addImportDeclaration({
|
|
681
|
+
moduleSpecifier: `./endpoints/${actionCamel}.types`,
|
|
682
|
+
namedImports: [`${actionPascal}Payload`, `${actionPascal}Response`]
|
|
683
|
+
});
|
|
684
|
+
serviceObject.addPropertyAssignment({
|
|
685
|
+
name: actionCamel,
|
|
686
|
+
initializer: `async (payload: ${actionPascal}Payload):Promise<${actionPascal}Response> => {
|
|
687
|
+
try {
|
|
688
|
+
const { data } = await ${resourcePascal}API.${actionCamel}(payload);
|
|
689
|
+
return data;
|
|
690
|
+
} catch (error) {
|
|
691
|
+
throw parseApiError(error);
|
|
692
|
+
}
|
|
693
|
+
}`
|
|
694
|
+
});
|
|
695
|
+
await serviceSource.save();
|
|
696
|
+
console.log(`\u2699\uFE0F Service (${resource}.service.ts) atualizado.`);
|
|
697
|
+
}
|
|
698
|
+
const typesFilePath = path10.join(endpointsDir, `${actionCamel}.types.ts`);
|
|
699
|
+
const modelExists = resourceState.files.type.exists;
|
|
700
|
+
const modelName = resourcePascal;
|
|
701
|
+
let typesContent = loadTemplate("operation-types.tpl");
|
|
702
|
+
const errorMaps = {
|
|
703
|
+
GET: " 403: ForbiddenError;\n 404: NotFoundError;",
|
|
704
|
+
POST: " 403: ForbiddenError;\n 422: ${ActionPascal}ValidationError;",
|
|
705
|
+
PUT: " 403: ForbiddenError;\n 404: NotFoundError;\n 422: ${ActionPascal}ValidationError;",
|
|
706
|
+
DELETE: " 403: ForbiddenError;\n 404: NotFoundError;"
|
|
707
|
+
};
|
|
708
|
+
const valErrTpl = `export interface \${ActionPascal}ValidationError extends BaseError {
|
|
709
|
+
errors: TFieldValidationError<keyof \${ModelName}>[];
|
|
710
|
+
}`;
|
|
711
|
+
let payloadStr = "";
|
|
712
|
+
if (method === "PUT" || method === "DELETE") {
|
|
713
|
+
payloadStr = " params: { id: string };\n body: Partial<${ModelName}>;";
|
|
714
|
+
} else if (method === "POST") {
|
|
715
|
+
payloadStr = " body: Partial<${ModelName}>;";
|
|
716
|
+
} else {
|
|
717
|
+
payloadStr = " params?: { id?: string };";
|
|
718
|
+
}
|
|
719
|
+
typesContent = typesContent.replace(/{{ActionPascal}}/g, actionPascal).replace(/{{ModelName}}/g, modelExists ? modelName : "any").replace(/{{ModelImport}}/g, modelExists ? `import { ${modelName} } from "../${resource}.type";` : "").replace(/{{ValidationErrorInterface}}/g, (method === "POST" || method === "PUT") && modelExists ? valErrTpl.replace(/\${ActionPascal}/g, actionPascal).replace(/\${ModelName}/g, modelName) : "").replace(/{{PayloadStructure}}/g, payloadStr.replace(/\${ModelName}/g, modelExists ? modelName : "any")).replace(/{{ResponseStructure}}/g, ` ${toCamelCase(resource)}: ${modelExists ? modelName : "any"}`).replace(/{{ErrorMapStructure}}/g, errorMaps[method].replace(/\${ActionPascal}/g, actionPascal));
|
|
720
|
+
await writeFileSafe(typesFilePath, typesContent);
|
|
721
|
+
console.log(`\u{1F4E6} Types (endpoints/${actionCamel}.types.ts) gerado.`);
|
|
722
|
+
const hookFilePath = path10.join(hooksDir, `use${actionPascal}.hook.ts`);
|
|
723
|
+
let hookContent = loadTemplate("operation-hook.tpl");
|
|
724
|
+
const isMutation = method !== "GET";
|
|
725
|
+
hookContent = hookContent.replace(/{{HookType}}/g, isMutation ? "useMutation" : "useQuery").replace(/{{HookProperty}}/g, isMutation ? "mutationFn" : "queryFn").replace(/{{ActionPascal}}/g, actionPascal).replace(/{{ResourcePascal}}/g, resourcePascal).replace(/{{resource}}/g, `../${resource}`).replace(/{{ActionPath}}/g, `../endpoints/${actionCamel}`).replace(/{{ActionCamel}}/g, actionCamel);
|
|
726
|
+
await writeFileSafe(hookFilePath, hookContent);
|
|
727
|
+
console.log(`\u{1FA9D} Hook (hooks/use${actionPascal}.hook.ts) gerado.`);
|
|
728
|
+
if (!resourceIntent.methods.includes(actionCamel)) {
|
|
729
|
+
resourceIntent.methods.push(actionCamel);
|
|
730
|
+
await saveData(data);
|
|
731
|
+
}
|
|
732
|
+
await syncCommand();
|
|
733
|
+
console.log(`
|
|
734
|
+
\u2705 Opera\xE7\xE3o ${actionCamel} injetada com sucesso!`);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// src/bin/cli.ts
|
|
738
|
+
var program = new Command();
|
|
739
|
+
program.name("forge").description("API contract CLI");
|
|
740
|
+
program.command("init").option("--overwrite", "Sobrescrever arquivos existentes").action(initCommand);
|
|
741
|
+
program.command("sync").description("Sincroniza o estado do projeto com o registro de inten\xE7\xE3o").action(syncCommand);
|
|
742
|
+
program.command("make:client").argument("<name>").option("--overwrite", "Sobrescrever arquivos existentes").action(makeClientCommand);
|
|
743
|
+
program.command("make:resource").argument("<name>").option("--sync", "For\xE7ar sincroniza\xE7\xE3o ap\xF3s cria\xE7\xE3o").option("--no-sync", "Pular sincroniza\xE7\xE3o autom\xE1tica").action(makeResourceCommand);
|
|
744
|
+
program.command("make:type").argument("<name>").option("--timestamps", "Add timestamps", false).action(makeTypeCommand);
|
|
745
|
+
program.command("make:service").argument("<name>").action(makeServiceCommand);
|
|
746
|
+
program.command("make:api").argument("<name>").action(makeApiCommand);
|
|
747
|
+
["GET", "POST", "PUT", "DELETE", "get", "post", "put", "delete"].forEach((method) => {
|
|
748
|
+
program.command(`consume:${method}`).argument("<action>").argument("[resource]").argument("[api]").action(async (action, resource, api) => {
|
|
749
|
+
await consumeCommand(method.toUpperCase(), action, resource, api || "api");
|
|
750
|
+
});
|
|
751
|
+
});
|
|
752
|
+
program.parse();
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const {{camelName}}Api = axios.create({
|
|
2
|
+
baseURL: import.meta.env.VITE_API_{{upperName}}_URL,
|
|
3
|
+
timeout: 1000 * 15,
|
|
4
|
+
});
|
|
5
|
+
|
|
6
|
+
setupInterceptors({{camelName}}Api, false);
|
|
7
|
+
|
|
8
|
+
async {{camelCaseName}}(payload: {{payload}}): Promise<{{response}}> {
|
|
9
|
+
try{
|
|
10
|
+
const {data} = await api.post("/{{route}}", payload);
|
|
11
|
+
|
|
12
|
+
return data;
|
|
13
|
+
}catch(error){
|
|
14
|
+
throw parseApiError<{{errorMap}}>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
},
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import type { AxiosInstance } from "axios";
|
|
3
|
+
|
|
4
|
+
type LogoutCallback = () => void;
|
|
5
|
+
|
|
6
|
+
let logoutHandlers: LogoutCallback[] = [];
|
|
7
|
+
|
|
8
|
+
export function setLogoutHandler(callback: LogoutCallback) {
|
|
9
|
+
logoutHandlers.push(callback);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const setupInterceptors = (
|
|
13
|
+
instance: AxiosInstance,
|
|
14
|
+
shouldLogoutOnUnauthorizedException = false
|
|
15
|
+
) => {
|
|
16
|
+
instance.interceptors.request.use((config) => {
|
|
17
|
+
const token = localStorage.getItem("access_token");
|
|
18
|
+
|
|
19
|
+
if (token && !config.headers.Authorization) {
|
|
20
|
+
config.headers.Authorization = `Bearer ${token}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return config;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
instance.interceptors.response.use(
|
|
27
|
+
(response) => response,
|
|
28
|
+
(error) => {
|
|
29
|
+
const status = error.response?.status;
|
|
30
|
+
|
|
31
|
+
if (status === 401 && shouldLogoutOnUnauthorizedException) {
|
|
32
|
+
if (logoutHandlers.length === 0) {
|
|
33
|
+
console.error("Logout handler not specified");
|
|
34
|
+
return Promise.reject(error);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
logoutHandlers.forEach((handler) => handler());
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return Promise.reject(error);
|
|
41
|
+
}
|
|
42
|
+
);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const api = axios.create({
|
|
46
|
+
baseURL: import.meta.env.VITE_API_URL,
|
|
47
|
+
timeout: 1000 * 15,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
setupInterceptors(api);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type HttpStatus = 400 | 401 | 403 | 404 | 409 | 422 | 500;
|
|
2
|
+
|
|
3
|
+
export interface BaseError {
|
|
4
|
+
message: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface UnauthorizedError extends BaseError {}
|
|
8
|
+
export interface ForbiddenError extends BaseError {}
|
|
9
|
+
export interface NotFoundError extends BaseError {}
|
|
10
|
+
|
|
11
|
+
export type ApiError<
|
|
12
|
+
TMap extends Partial<Record<HttpStatus, any>>
|
|
13
|
+
> = {
|
|
14
|
+
[K in keyof TMap & HttpStatus]: {
|
|
15
|
+
status: K;
|
|
16
|
+
message: string;
|
|
17
|
+
error_data: TMap[K];
|
|
18
|
+
};
|
|
19
|
+
}[keyof TMap & HttpStatus];
|
|
20
|
+
|
|
21
|
+
export type DefaultErrorMap = {
|
|
22
|
+
400: BaseError;
|
|
23
|
+
401: UnauthorizedError;
|
|
24
|
+
403: ForbiddenError;
|
|
25
|
+
404: NotFoundError;
|
|
26
|
+
409: BaseError;
|
|
27
|
+
422: BaseError;
|
|
28
|
+
500: BaseError;
|
|
29
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { AxiosError } from "axios";
|
|
2
|
+
import type { BaseErrorResponse } from "@/types/global.types";
|
|
3
|
+
import type { ApiError, HttpStatus } from "@/types/api.types";
|
|
4
|
+
|
|
5
|
+
export function parseApiError<
|
|
6
|
+
TMap extends Partial<Record<HttpStatus, any>>
|
|
7
|
+
>(
|
|
8
|
+
error: unknown
|
|
9
|
+
): ApiError<TMap> {
|
|
10
|
+
const axiosError = error as AxiosError<BaseErrorResponse<any>>;
|
|
11
|
+
|
|
12
|
+
const rawStatus = axiosError.response?.status;
|
|
13
|
+
|
|
14
|
+
const status = (rawStatus ?? 500) as keyof TMap;
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
status,
|
|
18
|
+
message:
|
|
19
|
+
axiosError.response?.data?.message ||
|
|
20
|
+
axiosError.message ||
|
|
21
|
+
"Erro desconhecido",
|
|
22
|
+
error_data:
|
|
23
|
+
axiosError.response?.data?.error_data ??
|
|
24
|
+
({} as TMap[keyof TMap]),
|
|
25
|
+
} as ApiError<TMap>;
|
|
26
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type BasePayload<T extends {
|
|
2
|
+
queryParams?: object;
|
|
3
|
+
routeParams?: object;
|
|
4
|
+
body?: object;
|
|
5
|
+
}> = T;
|
|
6
|
+
|
|
7
|
+
export type BaseSuccessResponse<TData> = {
|
|
8
|
+
message: string;
|
|
9
|
+
data: TData;
|
|
10
|
+
status: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type BaseErrorResponse<TErrorData> = {
|
|
14
|
+
status: number;
|
|
15
|
+
error_data: TErrorData;
|
|
16
|
+
message: string;
|
|
17
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { {{HookType}} } from "@tanstack/react-query";
|
|
2
|
+
import { {{ResourcePascal}}Service } from "./{{resource}}.service";
|
|
3
|
+
import { {{ActionPascal}}Payload, {{ActionPascal}}Response } from "./{{ActionPath}}.types";
|
|
4
|
+
|
|
5
|
+
export const use{{ActionPascal}} = () => {
|
|
6
|
+
return {{HookType}}<{{ActionPascal}}Response, Error, {{ActionPascal}}Payload>({
|
|
7
|
+
{{HookProperty}}: (payload) => {{ResourcePascal}}Service.{{ActionCamel}}(payload),
|
|
8
|
+
});
|
|
9
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { BasePayload, BaseSuccessResponse, ForbiddenError, NotFoundError, BaseError } from "@/api/api.types";
|
|
2
|
+
import { TFieldValidationError } from "@/types/shared.types";
|
|
3
|
+
{{ModelImport}}
|
|
4
|
+
|
|
5
|
+
{{ValidationErrorInterface}}
|
|
6
|
+
|
|
7
|
+
export interface {{ActionPascal}}Payload extends BasePayload<{
|
|
8
|
+
{{PayloadStructure}}
|
|
9
|
+
}> {}
|
|
10
|
+
|
|
11
|
+
export interface {{ActionPascal}}Response extends BaseSuccessResponse<{
|
|
12
|
+
{{ResponseStructure}}
|
|
13
|
+
}> {}
|
|
14
|
+
|
|
15
|
+
export type {{ActionPascal}}ErrorMap = {
|
|
16
|
+
{{ErrorMapStructure}}
|
|
17
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type TPagination<TData> = {
|
|
2
|
+
current_page: number;
|
|
3
|
+
data: TData[];
|
|
4
|
+
next_page_url?: string;
|
|
5
|
+
path: string;
|
|
6
|
+
per_page: number;
|
|
7
|
+
prev_page_url?: string;
|
|
8
|
+
to: number;
|
|
9
|
+
total: number;
|
|
10
|
+
last_page: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type TFieldValidationError<TField extends string> = {
|
|
14
|
+
field: TField;
|
|
15
|
+
error: string;
|
|
16
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vitongovisk/forge",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "./dist/cli.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"forge": "./dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsup src/bin/cli.ts --format esm --clean && npm run copy:templates",
|
|
14
|
+
"copy:templates": "cpy src/templates/**/* dist/templates",
|
|
15
|
+
"dev": "node dist/cli.js",
|
|
16
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
17
|
+
},
|
|
18
|
+
"author": "",
|
|
19
|
+
"license": "ISC",
|
|
20
|
+
"type": "module",
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@tanstack/react-query": "^5.100.8",
|
|
23
|
+
"axios": "^1.16.0",
|
|
24
|
+
"chalk": "^5.6.2",
|
|
25
|
+
"commander": "^14.0.3",
|
|
26
|
+
"fs-extra": "^11.3.4",
|
|
27
|
+
"inquirer": "^13.4.2",
|
|
28
|
+
"ts-morph": "^28.0.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/fs-extra": "^11.0.4",
|
|
32
|
+
"@types/node": "^25.6.0",
|
|
33
|
+
"cpy-cli": "^7.0.0",
|
|
34
|
+
"ts-node": "^10.9.2",
|
|
35
|
+
"tsup": "^8.5.1",
|
|
36
|
+
"typescript": "^6.0.3"
|
|
37
|
+
},
|
|
38
|
+
"exports": {
|
|
39
|
+
".": "./dist/cli.js"
|
|
40
|
+
},
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=18"
|
|
43
|
+
},
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
},
|
|
47
|
+
"keywords": [
|
|
48
|
+
"cli",
|
|
49
|
+
"typescript",
|
|
50
|
+
"axios",
|
|
51
|
+
"tanstack-query",
|
|
52
|
+
"codegen",
|
|
53
|
+
"frontend",
|
|
54
|
+
"architecture"
|
|
55
|
+
]
|
|
56
|
+
}
|