@carlos-iso/versioner 0.0.2

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.
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Teste de fumaça sem dependências externas.
3
+ *
4
+ * Cria um repositório Git temporário, roda init/build/minor/major
5
+ * e valida o resultado. Executado automaticamente no prepublishOnly.
6
+ *
7
+ * npm test
8
+ */
9
+
10
+ const fs = require("fs");
11
+ const os = require("os");
12
+ const path = require("path");
13
+ const { execFileSync } = require("child_process");
14
+
15
+ const BIN = path.join(__dirname, "../../", "bin", "versioner.js");
16
+
17
+ let failures = 0;
18
+
19
+ function assert(condition, label) {
20
+ if (condition) {
21
+ console.log(` ✔ ${label}`);
22
+ return;
23
+ }
24
+
25
+ failures += 1;
26
+ console.log(` ✖ ${label}`);
27
+ }
28
+
29
+ function run(cwd, args) {
30
+ return execFileSync(process.execPath, [BIN, ...args], {
31
+ cwd,
32
+ encoding: "utf8",
33
+ stdio: ["ignore", "pipe", "pipe"],
34
+ env: { ...process.env, NO_COLOR: "1" },
35
+ });
36
+ }
37
+
38
+ function git(cwd, args) {
39
+ execFileSync("git", args, { cwd, stdio: "ignore" });
40
+ }
41
+
42
+ function readJSON(file) {
43
+ return JSON.parse(fs.readFileSync(file, "utf8"));
44
+ }
45
+
46
+ function setup() {
47
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "versioner-test-"));
48
+
49
+ git(dir, ["init", "-b", "main"]);
50
+ git(dir, ["config", "user.email", "test@example.com"]);
51
+ git(dir, ["config", "user.name", "Versioner Test"]);
52
+
53
+ fs.writeFileSync(
54
+ path.join(dir, "package.json"),
55
+ JSON.stringify({ name: "demo", version: "0.0.0" }, null, 2),
56
+ );
57
+
58
+ fs.writeFileSync(
59
+ path.join(dir, "app.json"),
60
+ JSON.stringify({ expo: { name: "demo", version: "0.0.0" } }, null, 2),
61
+ );
62
+
63
+ git(dir, ["add", "."]);
64
+ git(dir, ["commit", "-m", "inicial"]);
65
+
66
+ return dir;
67
+ }
68
+
69
+ function main() {
70
+ console.log("\nVersioner · smoke test\n");
71
+
72
+ const dir = setup();
73
+
74
+ // init
75
+ run(dir, ["init", "--yes"]);
76
+
77
+ assert(fs.existsSync(path.join(dir, "versioner.config.json")), "init cria versioner.config.json");
78
+ assert(fs.existsSync(path.join(dir, ".versioner.json")), "init cria .versioner.json");
79
+
80
+ const config = readJSON(path.join(dir, "versioner.config.json"));
81
+
82
+ assert(
83
+ config.files.some((file) => file.path === "package.json"),
84
+ "init detecta package.json",
85
+ );
86
+ assert(
87
+ config.files.some((file) => file.field === "expo.version"),
88
+ "init detecta app.json (expo.version)",
89
+ );
90
+
91
+ // build (sem push: não há remoto)
92
+ run(dir, ["build", "primeira release", "--no-push"]);
93
+
94
+ assert(readJSON(path.join(dir, ".versioner.json")).build === 1, "build incrementa a build");
95
+ assert(readJSON(path.join(dir, "package.json")).version === "0.0.1", "build atualiza package.json");
96
+ assert(readJSON(path.join(dir, "app.json")).expo.version === "0.0.1", "build atualiza app.json");
97
+
98
+ // minor
99
+ run(dir, ["minor", "nova funcionalidade", "--no-push"]);
100
+
101
+ const afterMinor = readJSON(path.join(dir, ".versioner.json"));
102
+
103
+ assert(afterMinor.minor === 1 && afterMinor.build === 2, "minor incrementa minor e build");
104
+
105
+ // major
106
+ run(dir, ["major", "nova arquitetura", "--no-push", "--tag"]);
107
+
108
+ const afterMajor = readJSON(path.join(dir, ".versioner.json"));
109
+
110
+ assert(
111
+ afterMajor.major === 1 && afterMajor.minor === 0 && afterMajor.build === 3,
112
+ "major incrementa major, zera minor e incrementa build",
113
+ );
114
+
115
+ const tags = execFileSync("git", ["tag"], { cwd: dir, encoding: "utf8" });
116
+
117
+ assert(tags.includes("v1.0.3"), "--tag cria a tag da release");
118
+
119
+ // dry-run não altera nada
120
+ run(dir, ["build", "simulacao", "--dry-run"]);
121
+
122
+ assert(
123
+ readJSON(path.join(dir, ".versioner.json")).build === 3,
124
+ "--dry-run não grava alterações",
125
+ );
126
+
127
+ // version / status / help
128
+ assert(run(dir, ["version", "--raw"]).trim() === "1.0.3", "version --raw imprime a versão");
129
+ assert(run(dir, ["status"]).includes("Arquivos monitorados"), "status roda sem erro");
130
+ assert(run(dir, ["help"]).includes("build"), "help lista os comandos");
131
+
132
+ // validação de mensagem
133
+ let rejected = false;
134
+
135
+ try {
136
+ run(dir, ["build"]);
137
+ } catch {
138
+ rejected = true;
139
+ }
140
+
141
+ assert(rejected, "build sem mensagem é rejeitado");
142
+
143
+ // versão não pode ter avançado após a falha (rollback)
144
+ assert(
145
+ readJSON(path.join(dir, ".versioner.json")).build === 3,
146
+ "falha na release não avança a versão",
147
+ );
148
+
149
+ fs.rmSync(dir, { recursive: true, force: true });
150
+
151
+ console.log(`\n${failures ? `${failures} falha(s).` : "Todos os testes passaram."}\n`);
152
+
153
+ process.exit(failures ? 1 : 0);
154
+ }
155
+
156
+ main();
@@ -0,0 +1,60 @@
1
+ const SHORT_FLAGS = {
2
+ y: "yes",
3
+ f: "force",
4
+ n: "no-git",
5
+ d: "dry-run",
6
+ };
7
+
8
+ /**
9
+ * Separa flags de valores posicionais.
10
+ *
11
+ * parse(['build', 'Corrige login', '--no-push'])
12
+ * → { values: ['build', 'Corrige login'], flags: { 'no-push': true } }
13
+ *
14
+ * Suporta `--chave=valor`.
15
+ */
16
+ function parse(argv = []) {
17
+ const values = [];
18
+ const flags = {};
19
+
20
+ for (const arg of argv) {
21
+ if (typeof arg !== "string") {
22
+ continue;
23
+ }
24
+
25
+ if (arg.startsWith("--")) {
26
+ const [key, value] = arg.slice(2).split("=");
27
+
28
+ flags[key] = value === undefined ? true : value;
29
+ continue;
30
+ }
31
+
32
+ if (arg.startsWith("-") && arg.length > 1) {
33
+ for (const letter of arg.slice(1)) {
34
+ flags[SHORT_FLAGS[letter] || letter] = true;
35
+ }
36
+ continue;
37
+ }
38
+
39
+ values.push(arg);
40
+ }
41
+
42
+ return { values, flags };
43
+ }
44
+
45
+ /**
46
+ * Junta os valores posicionais em uma mensagem de commit.
47
+ */
48
+ function toMessage(values) {
49
+ return values.join(" ").trim();
50
+ }
51
+
52
+ function hasFlag(flags, ...names) {
53
+ return names.some((name) => flags[name] === true || flags[name] === "true");
54
+ }
55
+
56
+ module.exports = {
57
+ parse,
58
+ toMessage,
59
+ hasFlag,
60
+ };
@@ -0,0 +1,80 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+
4
+ /**
5
+ * Resolve um caminho a partir do diretório onde a CLI foi executada.
6
+ */
7
+ function root(...paths) {
8
+ return path.join(process.cwd(), ...paths);
9
+ }
10
+
11
+ /**
12
+ * Resolve um caminho a partir de um diretório específico.
13
+ */
14
+ function resolve(cwd, ...paths) {
15
+ return path.join(cwd, ...paths);
16
+ }
17
+
18
+ function exists(file) {
19
+ return fs.existsSync(file);
20
+ }
21
+
22
+ function isDirectory(file) {
23
+ return exists(file) && fs.statSync(file).isDirectory();
24
+ }
25
+
26
+ function readText(file) {
27
+ return fs.readFileSync(file, "utf8");
28
+ }
29
+
30
+ function writeText(file, content) {
31
+ fs.mkdirSync(path.dirname(file), { recursive: true });
32
+
33
+ fs.writeFileSync(file, content, "utf8");
34
+ }
35
+
36
+ function readJSON(file) {
37
+ const content = readText(file);
38
+
39
+ try {
40
+ return JSON.parse(content);
41
+ } catch (error) {
42
+ throw new Error(`JSON inválido em ${path.basename(file)}: ${error.message}`);
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Escreve JSON preservando a indentação já usada no arquivo
48
+ * (evita diffs gigantes em projetos que usam 2 espaços).
49
+ */
50
+ function writeJSON(file, data, indent) {
51
+ const spaces = indent || detectIndent(file);
52
+
53
+ writeText(file, `${JSON.stringify(data, null, spaces)}\n`);
54
+ }
55
+
56
+ function detectIndent(file) {
57
+ if (!exists(file)) {
58
+ return 4;
59
+ }
60
+
61
+ const match = readText(file).match(/^[ \t]+/m);
62
+
63
+ if (!match) {
64
+ return 4;
65
+ }
66
+
67
+ return match[0].includes("\t") ? "\t" : match[0].length;
68
+ }
69
+
70
+ module.exports = {
71
+ root,
72
+ resolve,
73
+ exists,
74
+ isDirectory,
75
+ readText,
76
+ writeText,
77
+ readJSON,
78
+ writeJSON,
79
+ detectIndent,
80
+ };
@@ -0,0 +1,103 @@
1
+ const SUPPORTS_COLOR =
2
+ process.stdout.isTTY && !process.env.NO_COLOR && process.env.TERM !== "dumb";
3
+
4
+ const CODES = {
5
+ reset: "\u001b[0m",
6
+ bold: "\u001b[1m",
7
+ dim: "\u001b[2m",
8
+ red: "\u001b[31m",
9
+ green: "\u001b[32m",
10
+ yellow: "\u001b[33m",
11
+ blue: "\u001b[34m",
12
+ cyan: "\u001b[36m",
13
+ gray: "\u001b[90m",
14
+ };
15
+
16
+ function paint(code, text) {
17
+ if (!SUPPORTS_COLOR) {
18
+ return text;
19
+ }
20
+
21
+ return `${CODES[code]}${text}${CODES.reset}`;
22
+ }
23
+
24
+ class Logger {
25
+ constructor() {
26
+ this.silent = false;
27
+ }
28
+
29
+ setSilent(value) {
30
+ this.silent = Boolean(value);
31
+ }
32
+
33
+ write(text = "") {
34
+ if (this.silent) {
35
+ return;
36
+ }
37
+
38
+ console.log(text);
39
+ }
40
+
41
+ break() {
42
+ this.write("");
43
+ }
44
+
45
+ title(text) {
46
+ this.break();
47
+ this.write(paint("bold", paint("cyan", `▌ ${text}`)));
48
+ this.write(paint("gray", "─".repeat(Math.max(text.length + 2, 24))));
49
+ }
50
+
51
+ section(text) {
52
+ this.break();
53
+ this.write(paint("bold", text));
54
+ }
55
+
56
+ info(text) {
57
+ this.write(`${paint("blue", "ℹ")} ${text}`);
58
+ }
59
+
60
+ success(text) {
61
+ this.write(`${paint("green", "✔")} ${text}`);
62
+ }
63
+
64
+ warn(text) {
65
+ this.write(`${paint("yellow", "!")} ${text}`);
66
+ }
67
+
68
+ error(text) {
69
+ this.write(`${paint("red", "✖")} ${text}`);
70
+ }
71
+
72
+ step(text) {
73
+ this.write(`${paint("gray", "→")} ${text}`);
74
+ }
75
+
76
+ item(text) {
77
+ this.write(` ${paint("gray", "•")} ${text}`);
78
+ }
79
+
80
+ muted(text) {
81
+ this.write(paint("gray", text));
82
+ }
83
+
84
+ /**
85
+ * Linha de "chave: valor" alinhada.
86
+ */
87
+ field(label, value, width = 18) {
88
+ const key = `${label}:`.padEnd(width, " ");
89
+
90
+ this.write(` ${paint("gray", key)} ${value}`);
91
+ }
92
+
93
+ bold(text) {
94
+ return paint("bold", text);
95
+ }
96
+
97
+ dim(text) {
98
+ return paint("gray", text);
99
+ }
100
+ }
101
+
102
+ module.exports = new Logger();
103
+ module.exports.Logger = Logger;
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Define um valor em um caminho aninhado.
3
+ * Exemplo: setValue(json, "expo.version", "1.0.0")
4
+ */
5
+ function setValue(object, path, value) {
6
+ const keys = String(path).split(".");
7
+
8
+ let current = object;
9
+
10
+ while (keys.length > 1) {
11
+ const key = keys.shift();
12
+
13
+ if (typeof current[key] !== "object" || current[key] === null) {
14
+ current[key] = {};
15
+ }
16
+
17
+ current = current[key];
18
+ }
19
+
20
+ current[keys[0]] = value;
21
+
22
+ return object;
23
+ }
24
+
25
+ /**
26
+ * Lê um valor em um caminho aninhado.
27
+ * Retorna `undefined` se o caminho não existir.
28
+ */
29
+ function getValue(object, path) {
30
+ const keys = String(path).split(".");
31
+
32
+ let current = object;
33
+
34
+ for (const key of keys) {
35
+ if (typeof current !== "object" || current === null) {
36
+ return undefined;
37
+ }
38
+
39
+ current = current[key];
40
+ }
41
+
42
+ return current;
43
+ }
44
+
45
+ /**
46
+ * Merge raso-profundo usado para aplicar os defaults da configuração.
47
+ * Valores do `source` sobrescrevem os do `target`.
48
+ */
49
+ function merge(target, source) {
50
+ const result = { ...target };
51
+
52
+ for (const key of Object.keys(source || {})) {
53
+ const value = source[key];
54
+
55
+ if (value === undefined) {
56
+ continue;
57
+ }
58
+
59
+ if (isPlainObject(value) && isPlainObject(result[key])) {
60
+ result[key] = merge(result[key], value);
61
+ continue;
62
+ }
63
+
64
+ result[key] = value;
65
+ }
66
+
67
+ return result;
68
+ }
69
+
70
+ function isPlainObject(value) {
71
+ return typeof value === "object" && value !== null && !Array.isArray(value);
72
+ }
73
+
74
+ module.exports = {
75
+ setValue,
76
+ getValue,
77
+ merge,
78
+ isPlainObject,
79
+ };
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Formata uma duração em milissegundos de forma legível.
3
+ * A unidade já vem embutida no retorno.
4
+ */
5
+ function formatDuration(milliseconds) {
6
+ const ms = Math.max(0, Number(milliseconds) || 0);
7
+
8
+ if (ms < 1000) {
9
+ return `${Math.round(ms)} ms`;
10
+ }
11
+
12
+ const seconds = ms / 1000;
13
+
14
+ if (seconds < 60) {
15
+ return `${seconds.toFixed(2)} s`;
16
+ }
17
+
18
+ const minutes = Math.floor(seconds / 60);
19
+ const remaining = (seconds % 60).toFixed(2);
20
+
21
+ return `${minutes} min ${remaining} s`;
22
+ }
23
+
24
+ /**
25
+ * Data legível para CHANGELOG / logs.
26
+ */
27
+ function formatDate(date) {
28
+ const value = date instanceof Date ? date : new Date(date);
29
+
30
+ return value.toISOString().slice(0, 10);
31
+ }
32
+
33
+ module.exports = {
34
+ formatDuration,
35
+ formatDate,
36
+ };
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Barrel de utilitários.
3
+ * Mantido para compatibilidade com os imports antigos
4
+ * (`require("../utils/utils")`).
5
+ *
6
+ * Em código novo, prefira importar o módulo específico:
7
+ * utils/file, utils/object, utils/time, utils/args.
8
+ */
9
+
10
+ const file = require("./file");
11
+ const object = require("./object");
12
+ const time = require("./time");
13
+ const args = require("./args");
14
+
15
+ module.exports = {
16
+ ...file,
17
+ ...object,
18
+ ...time,
19
+ parseArgs: args.parse,
20
+ };