create-sette-ts 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 +47 -0
- package/bin/create-sette-ts.js +379 -0
- package/package.json +28 -0
- package/template/.env.example +1 -0
- package/template/_gitignore +6 -0
- package/template/package.json +23 -0
- package/template/src/index.test.ts +17 -0
- package/template/src/index.ts +4 -0
- package/template/tsconfig.build.json +12 -0
- package/template/tsconfig.json +18 -0
- package/template/vitest.config.ts +13 -0
package/README.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# create-sette-ts
|
|
2
|
+
|
|
3
|
+
CLI para criar um projeto TypeScript novo com a estrutura deste repositório.
|
|
4
|
+
|
|
5
|
+
## Uso local
|
|
6
|
+
|
|
7
|
+
Enquanto estiver desenvolvendo o gerador, execute na raiz deste repositório:
|
|
8
|
+
|
|
9
|
+
```powershell
|
|
10
|
+
node .\bin\create-sette-ts.js meu-projeto
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Para testar o pacote exatamente como ele será distribuído:
|
|
14
|
+
|
|
15
|
+
```powershell
|
|
16
|
+
npm pack
|
|
17
|
+
npx .\create-sette-ts-1.0.0.tgz meu-projeto
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
O instalador cria a pasta, troca o nome no `package.json`, instala as dependências e mostra os próximos comandos.
|
|
21
|
+
|
|
22
|
+
Opções disponíveis:
|
|
23
|
+
|
|
24
|
+
- `--no-install`: cria os arquivos sem instalar dependências;
|
|
25
|
+
- `--git`: executa `git init` no projeto criado;
|
|
26
|
+
- `--help`: exibe a ajuda.
|
|
27
|
+
|
|
28
|
+
## Publicação no npm
|
|
29
|
+
|
|
30
|
+
Antes da primeira publicação, confirme se o nome `create-sette-ts` está disponível. Depois, autentique-se e publique:
|
|
31
|
+
|
|
32
|
+
```powershell
|
|
33
|
+
npm login
|
|
34
|
+
npm publish
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Após a publicação, qualquer pessoa poderá usar:
|
|
38
|
+
|
|
39
|
+
```powershell
|
|
40
|
+
npx create-sette-ts@latest meu-projeto
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Ou, no formato `npm create`:
|
|
44
|
+
|
|
45
|
+
```powershell
|
|
46
|
+
npm create sette-ts@latest meu-projeto
|
|
47
|
+
```
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { constants } from "node:fs";
|
|
5
|
+
import {
|
|
6
|
+
access,
|
|
7
|
+
cp,
|
|
8
|
+
mkdir,
|
|
9
|
+
readFile,
|
|
10
|
+
readdir,
|
|
11
|
+
rename,
|
|
12
|
+
rm,
|
|
13
|
+
writeFile,
|
|
14
|
+
} from "node:fs/promises";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import process from "node:process";
|
|
17
|
+
import { createInterface } from "node:readline/promises";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
|
|
20
|
+
const templateDir = fileURLToPath(
|
|
21
|
+
new URL("../template", import.meta.url),
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const colors = {
|
|
25
|
+
cyan: "\u001b[36m",
|
|
26
|
+
green: "\u001b[32m",
|
|
27
|
+
red: "\u001b[31m",
|
|
28
|
+
yellow: "\u001b[33m",
|
|
29
|
+
reset: "\u001b[0m",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function paint(color, message) {
|
|
33
|
+
return process.stdout.isTTY
|
|
34
|
+
? `${colors[color]}${message}${colors.reset}`
|
|
35
|
+
: message;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function printHelp() {
|
|
39
|
+
console.log(`
|
|
40
|
+
Uso:
|
|
41
|
+
create-sette-ts <nome-do-projeto> [opções]
|
|
42
|
+
|
|
43
|
+
Opções:
|
|
44
|
+
--no-install Não instala as dependências
|
|
45
|
+
--git Inicializa um repositório Git
|
|
46
|
+
-h, --help Exibe esta ajuda
|
|
47
|
+
|
|
48
|
+
Exemplos:
|
|
49
|
+
npx create-sette-ts meu-projeto
|
|
50
|
+
npm create sette-ts@latest meu-projeto -- --git
|
|
51
|
+
`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function parseArgs(args) {
|
|
55
|
+
const options = {
|
|
56
|
+
install: true,
|
|
57
|
+
git: false,
|
|
58
|
+
help: false,
|
|
59
|
+
projectDir: undefined,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
for (const arg of args) {
|
|
63
|
+
if (arg === "-h" || arg === "--help") {
|
|
64
|
+
options.help = true;
|
|
65
|
+
} else if (arg === "--no-install") {
|
|
66
|
+
options.install = false;
|
|
67
|
+
} else if (arg === "--git") {
|
|
68
|
+
options.git = true;
|
|
69
|
+
} else if (arg.startsWith("-")) {
|
|
70
|
+
throw new Error(`Opção desconhecida: ${arg}`);
|
|
71
|
+
} else if (!options.projectDir) {
|
|
72
|
+
options.projectDir = arg;
|
|
73
|
+
} else {
|
|
74
|
+
throw new Error(`Argumento inesperado: ${arg}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return options;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function askProjectDir() {
|
|
82
|
+
if (!process.stdin.isTTY) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
"Informe o nome do projeto. Exemplo: create-sette-ts meu-projeto",
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const prompt = createInterface({
|
|
89
|
+
input: process.stdin,
|
|
90
|
+
output: process.stdout,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const answer = await prompt.question("Nome do projeto: ");
|
|
95
|
+
return answer.trim();
|
|
96
|
+
} finally {
|
|
97
|
+
prompt.close();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function getPackageName(projectDir) {
|
|
102
|
+
return path.basename(path.resolve(projectDir)).toLowerCase();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function validatePackageName(packageName) {
|
|
106
|
+
return (
|
|
107
|
+
Boolean(packageName) &&
|
|
108
|
+
packageName.length <= 214 &&
|
|
109
|
+
/^(?![._])[a-z0-9][a-z0-9._-]*$/.test(packageName)
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function pathExists(target) {
|
|
114
|
+
try {
|
|
115
|
+
await access(target);
|
|
116
|
+
return true;
|
|
117
|
+
} catch (error) {
|
|
118
|
+
if (error.code === "ENOENT") {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function isEmpty(directory) {
|
|
127
|
+
try {
|
|
128
|
+
return (await readdir(directory)).length === 0;
|
|
129
|
+
} catch (error) {
|
|
130
|
+
if (error.code === "ENOENT") {
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function detectPackageManager() {
|
|
139
|
+
const userAgent =
|
|
140
|
+
process.env.npm_config_user_agent ?? "npm";
|
|
141
|
+
|
|
142
|
+
const name = userAgent.split("/")[0];
|
|
143
|
+
|
|
144
|
+
return ["npm", "pnpm", "yarn", "bun"].includes(name)
|
|
145
|
+
? name
|
|
146
|
+
: "npm";
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function run(command, args, cwd) {
|
|
150
|
+
let result;
|
|
151
|
+
|
|
152
|
+
const requiresWindowsShell =
|
|
153
|
+
process.platform === "win32" &&
|
|
154
|
+
["npm", "pnpm", "yarn"].includes(command);
|
|
155
|
+
|
|
156
|
+
if (requiresWindowsShell) {
|
|
157
|
+
/*
|
|
158
|
+
* npm, pnpm e yarn normalmente são .cmd no Windows.
|
|
159
|
+
* Chamamos cmd.exe explicitamente em vez de usar
|
|
160
|
+
* spawnSync(..., { shell: true }), evitando o DEP0190.
|
|
161
|
+
*
|
|
162
|
+
* Os comandos e argumentos passados para esta função
|
|
163
|
+
* são definidos internamente pelo CLI.
|
|
164
|
+
*/
|
|
165
|
+
result = spawnSync(
|
|
166
|
+
process.env.ComSpec ?? "cmd.exe",
|
|
167
|
+
[
|
|
168
|
+
"/d",
|
|
169
|
+
"/s",
|
|
170
|
+
"/c",
|
|
171
|
+
`${command} ${args.join(" ")}`,
|
|
172
|
+
],
|
|
173
|
+
{
|
|
174
|
+
cwd,
|
|
175
|
+
stdio: "inherit",
|
|
176
|
+
},
|
|
177
|
+
);
|
|
178
|
+
} else {
|
|
179
|
+
/*
|
|
180
|
+
* git e bun possuem executáveis que podem ser
|
|
181
|
+
* chamados diretamente no Windows.
|
|
182
|
+
*
|
|
183
|
+
* Linux/macOS também entram aqui.
|
|
184
|
+
*/
|
|
185
|
+
result = spawnSync(command, args, {
|
|
186
|
+
cwd,
|
|
187
|
+
stdio: "inherit",
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (result.error) {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return result.status === 0;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function createProject(options) {
|
|
199
|
+
const projectDir =
|
|
200
|
+
options.projectDir || (await askProjectDir());
|
|
201
|
+
|
|
202
|
+
if (!projectDir) {
|
|
203
|
+
throw new Error("O nome do projeto não pode ser vazio.");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const packageName = getPackageName(projectDir);
|
|
207
|
+
|
|
208
|
+
if (!validatePackageName(packageName)) {
|
|
209
|
+
throw new Error(
|
|
210
|
+
`"${packageName}" não é um nome de pacote npm válido.`,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const targetDir = path.resolve(
|
|
215
|
+
process.cwd(),
|
|
216
|
+
projectDir,
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
if (!(await isEmpty(targetDir))) {
|
|
220
|
+
throw new Error(
|
|
221
|
+
`A pasta ${targetDir} não está vazia.`,
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const directoryAlreadyExisted =
|
|
226
|
+
await pathExists(targetDir);
|
|
227
|
+
|
|
228
|
+
/*
|
|
229
|
+
* Esta etapa é a criação propriamente dita.
|
|
230
|
+
*
|
|
231
|
+
* Se algo falhar enquanto copia/configura o template,
|
|
232
|
+
* fazemos rollback caso a pasta tenha sido criada
|
|
233
|
+
* pelo próprio CLI.
|
|
234
|
+
*/
|
|
235
|
+
try {
|
|
236
|
+
await mkdir(targetDir, {
|
|
237
|
+
recursive: true,
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
await cp(templateDir, targetDir, {
|
|
241
|
+
recursive: true,
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const packageJsonPath = path.join(
|
|
245
|
+
targetDir,
|
|
246
|
+
"package.json",
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
const packageJson = await readFile(
|
|
250
|
+
packageJsonPath,
|
|
251
|
+
"utf8",
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
await writeFile(
|
|
255
|
+
packageJsonPath,
|
|
256
|
+
packageJson.replaceAll(
|
|
257
|
+
"{{projectName}}",
|
|
258
|
+
packageName,
|
|
259
|
+
),
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
await rename(
|
|
263
|
+
path.join(targetDir, "_gitignore"),
|
|
264
|
+
path.join(targetDir, ".gitignore"),
|
|
265
|
+
);
|
|
266
|
+
} catch (error) {
|
|
267
|
+
if (!directoryAlreadyExisted) {
|
|
268
|
+
await rm(targetDir, {
|
|
269
|
+
recursive: true,
|
|
270
|
+
force: true,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
throw error;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const packageManager = detectPackageManager();
|
|
278
|
+
|
|
279
|
+
/*
|
|
280
|
+
* A partir daqui o projeto já está criado.
|
|
281
|
+
*
|
|
282
|
+
* Se npm install falhar por internet, registry,
|
|
283
|
+
* permissão etc., não faz sentido apagar tudo.
|
|
284
|
+
*/
|
|
285
|
+
let installed = false;
|
|
286
|
+
|
|
287
|
+
if (options.install) {
|
|
288
|
+
console.log(
|
|
289
|
+
`\n${paint(
|
|
290
|
+
"cyan",
|
|
291
|
+
`Instalando dependências com ${packageManager}...`,
|
|
292
|
+
)}`,
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
installed = run(
|
|
296
|
+
packageManager,
|
|
297
|
+
["install"],
|
|
298
|
+
targetDir,
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
if (!installed) {
|
|
302
|
+
console.warn(
|
|
303
|
+
`\n${paint(
|
|
304
|
+
"yellow",
|
|
305
|
+
"Aviso:",
|
|
306
|
+
)} não foi possível instalar as dependências.`,
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
console.warn(
|
|
310
|
+
`Execute manualmente: ${packageManager} install`,
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (options.git) {
|
|
316
|
+
console.log(
|
|
317
|
+
`\n${paint(
|
|
318
|
+
"cyan",
|
|
319
|
+
"Inicializando o Git...",
|
|
320
|
+
)}`,
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
if (!run("git", ["init"], targetDir)) {
|
|
324
|
+
console.warn(
|
|
325
|
+
`${paint(
|
|
326
|
+
"yellow",
|
|
327
|
+
"Aviso:",
|
|
328
|
+
)} não foi possível inicializar o Git.`,
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const relativeDir =
|
|
334
|
+
path.relative(process.cwd(), targetDir) || ".";
|
|
335
|
+
|
|
336
|
+
console.log(
|
|
337
|
+
`\n${paint(
|
|
338
|
+
"green",
|
|
339
|
+
"Projeto criado com sucesso!",
|
|
340
|
+
)}\n`,
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
if (relativeDir !== ".") {
|
|
344
|
+
console.log(` cd ${relativeDir}`);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (!options.install || !installed) {
|
|
348
|
+
console.log(` ${packageManager} install`);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
console.log(
|
|
352
|
+
` ${packageManager} run dev\n`,
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
try {
|
|
357
|
+
await access(templateDir, constants.R_OK);
|
|
358
|
+
|
|
359
|
+
const options = parseArgs(
|
|
360
|
+
process.argv.slice(2),
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
if (options.help) {
|
|
364
|
+
printHelp();
|
|
365
|
+
} else {
|
|
366
|
+
await createProject(options);
|
|
367
|
+
}
|
|
368
|
+
} catch (error) {
|
|
369
|
+
const message =
|
|
370
|
+
error instanceof Error
|
|
371
|
+
? error.message
|
|
372
|
+
: String(error);
|
|
373
|
+
|
|
374
|
+
console.error(
|
|
375
|
+
`\n${paint("red", "Erro:")} ${message}\n`,
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
process.exitCode = 1;
|
|
379
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-sette-ts",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI para criar projetos TypeScript a partir do template sette-ts.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-sette-ts": "bin/create-sette-ts.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"template"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"test": "node --test --test-isolation=none test/create-project.test.js",
|
|
15
|
+
"test:cli": "node --test --test-isolation=none test/create-project.test.js"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"cli",
|
|
19
|
+
"create-app",
|
|
20
|
+
"typescript",
|
|
21
|
+
"starter"
|
|
22
|
+
],
|
|
23
|
+
"author": "Sette0o0",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=20.0.0"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Adicione aqui as variáveis de ambiente do projeto.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{projectName}}",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "tsx watch src/index.ts",
|
|
8
|
+
"build": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json",
|
|
9
|
+
"dist": "node dist/index.js",
|
|
10
|
+
"test": "vitest run"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"dotenv": "^17.4.2"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@types/node": "^26.4.1",
|
|
17
|
+
"nodemon": "^3.1.14",
|
|
18
|
+
"tsc-alias": "^1.9.4",
|
|
19
|
+
"tsx": "^4.23.13",
|
|
20
|
+
"typescript": "^7.0.2",
|
|
21
|
+
"vitest": "^5.0.0"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { hello } from "@/index.js";
|
|
4
|
+
|
|
5
|
+
describe("create-sette-ts", () => {
|
|
6
|
+
it('should log "Hello, World!"', () => {
|
|
7
|
+
const consoleSpy = vi
|
|
8
|
+
.spyOn(console, "log")
|
|
9
|
+
.mockImplementation(() => {});
|
|
10
|
+
|
|
11
|
+
hello();
|
|
12
|
+
|
|
13
|
+
expect(consoleSpy).toHaveBeenCalledExactlyOnceWith("Hello, World!");
|
|
14
|
+
|
|
15
|
+
consoleSpy.mockRestore();
|
|
16
|
+
});
|
|
17
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"module": "nodenext",
|
|
4
|
+
"target": "esnext",
|
|
5
|
+
"types": ["node"],
|
|
6
|
+
"noUncheckedIndexedAccess": true,
|
|
7
|
+
"exactOptionalPropertyTypes": true,
|
|
8
|
+
"strict": true,
|
|
9
|
+
"verbatimModuleSyntax": true,
|
|
10
|
+
"isolatedModules": true,
|
|
11
|
+
"noUncheckedSideEffectImports": true,
|
|
12
|
+
"moduleDetection": "force",
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"paths": {
|
|
15
|
+
"@/*": ["./src/*"]
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
import { config } from "dotenv";
|
|
3
|
+
import { defineConfig } from "vitest/config";
|
|
4
|
+
|
|
5
|
+
config({ path: ".test.env", override: true });
|
|
6
|
+
|
|
7
|
+
export default defineConfig({
|
|
8
|
+
resolve: {
|
|
9
|
+
alias: {
|
|
10
|
+
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
});
|