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 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,6 @@
1
+ node_modules/
2
+ dist/
3
+
4
+ .env
5
+ .env.*
6
+ !.env.example
@@ -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,4 @@
1
+ export function hello() {
2
+ console.log("Hello, World!");
3
+ }
4
+ hello()
@@ -0,0 +1,12 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "rootDir": "./src",
5
+ "outDir": "./dist",
6
+ "sourceMap": true,
7
+ "declaration": true,
8
+ "declarationMap": true,
9
+ },
10
+ "include": ["src"],
11
+ "exclude": ["**/*.test.ts", "**/*.spec.ts"]
12
+ }
@@ -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
+ });