@archznn/xavva 2.4.0 → 2.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.
@@ -30,6 +30,11 @@ export class BuildService {
30
30
  this.cache.clearCache();
31
31
  }
32
32
 
33
+ // Sempre limpa a pasta de build antes (target/ ou build/)
34
+ if (!incremental) {
35
+ await this.cleanBuildDirectory();
36
+ }
37
+
33
38
  // Cache só é usado se --cache for passado ou em modo incremental
34
39
  const useCache = this.projectConfig.cache || incremental;
35
40
 
@@ -54,7 +59,8 @@ export class BuildService {
54
59
  if (incremental) {
55
60
  command.push("compile");
56
61
  } else {
57
- if (this.projectConfig.clean) command.push("clean");
62
+ // Sempre executa clean antes do build
63
+ command.push("clean");
58
64
  // Use 'package' para gerar .war ou 'war:exploded' para pasta
59
65
  if (this.projectConfig.war) {
60
66
  command.push("package");
@@ -76,7 +82,8 @@ export class BuildService {
76
82
  if (incremental) {
77
83
  command.push("classes");
78
84
  } else {
79
- if (this.projectConfig.clean) command.push("clean");
85
+ // Sempre executa clean antes do build
86
+ command.push("clean");
80
87
  command.push("war");
81
88
  command.push("--parallel", "--build-cache");
82
89
  }
@@ -123,25 +130,147 @@ export class BuildService {
123
130
  }
124
131
  }
125
132
 
133
+ /**
134
+ * Limpa fisicamente o diretório de build (target/ ou build/)
135
+ * Garante build limpo antes de cada execução
136
+ */
137
+ private async cleanBuildDirectory(): Promise<void> {
138
+ const buildDir = this.projectConfig.buildTool === 'maven'
139
+ ? path.join(process.cwd(), 'target')
140
+ : path.join(process.cwd(), 'build');
141
+
142
+ if (existsSync(buildDir)) {
143
+ try {
144
+ Logger.step(`Cleaning ${path.basename(buildDir)}/ directory...`);
145
+ await fs.rm(buildDir, { recursive: true, force: true });
146
+ Logger.debug(`Removed ${buildDir}`);
147
+ } catch (e) {
148
+ Logger.warn(`Could not fully remove ${buildDir}, continuing...`);
149
+ }
150
+ }
151
+ }
152
+
126
153
  async syncExploded(srcDir: string, destDir: string): Promise<void> {
127
154
  if (!existsSync(srcDir)) return;
128
155
  if (!existsSync(destDir)) mkdirSync(destDir, { recursive: true });
129
156
  await this.fastSync(srcDir, destDir);
130
157
  }
131
158
 
132
- async syncClasses(customSrc?: string): Promise<string | null> {
159
+ async syncClasses(changedFiles?: string[]): Promise<string | null> {
133
160
  const appFolder = this.projectService.getInferredAppName();
134
161
  const webappPath = path.join(this.tomcatConfig.path, "webapps", appFolder);
135
162
  const targetLib = path.join(webappPath, "WEB-INF", "classes");
136
- const sourceDir = customSrc || this.projectService.getClassesDir();
163
+ const sourceDir = this.projectService.getClassesDir();
137
164
 
138
165
  if (!existsSync(sourceDir)) return null;
139
166
  if (!existsSync(targetLib)) mkdirSync(targetLib, { recursive: true });
140
167
 
141
- await this.fastSync(sourceDir, targetLib);
168
+ // Se temos uma lista específica de arquivos modificados, sincroniza apenas eles
169
+ if (changedFiles && changedFiles.length > 0) {
170
+ await this.syncSpecificFiles(changedFiles, sourceDir, targetLib);
171
+ } else {
172
+ // Caso contrário, sincroniza tudo (comportamento padrão)
173
+ await this.fastSync(sourceDir, targetLib);
174
+ }
175
+
142
176
  return appFolder;
143
177
  }
144
178
 
179
+ /**
180
+ * Sincroniza apenas arquivos específicos baseado nos arquivos .java modificados.
181
+ * Converte .java para .class e sincroniza apenas os arquivos realmente modificados.
182
+ */
183
+ private async syncSpecificFiles(changedFiles: string[], sourceDir: string, targetLib: string): Promise<void> {
184
+ const tasks: Promise<void>[] = [];
185
+ const syncedCount = { value: 0 };
186
+
187
+ for (const javaFile of changedFiles) {
188
+ // Converte caminho do .java para caminho do .class
189
+ // Ex: src/main/java/com/example/Foo.java -> target/classes/com/example/Foo.class
190
+ const relativePath = this.javaToClassPath(javaFile);
191
+ if (!relativePath) continue;
192
+
193
+ const sourcePath = path.join(sourceDir, relativePath);
194
+ const targetPath = path.join(targetLib, relativePath);
195
+
196
+ if (!existsSync(sourcePath)) {
197
+ // Se o .class não existe, talvez seja um arquivo excluído ou inner class
198
+ // Neste caso, faz sync completo como fallback
199
+ continue;
200
+ }
201
+
202
+ tasks.push((async () => {
203
+ const targetDir = path.dirname(targetPath);
204
+ if (!existsSync(targetDir)) mkdirSync(targetDir, { recursive: true });
205
+
206
+ const srcStat = statSync(sourcePath);
207
+ const destStat = existsSync(targetPath) ? statSync(targetPath) : null;
208
+
209
+ if (!destStat || srcStat.mtimeMs > destStat.mtimeMs) {
210
+ await fs.copyFile(sourcePath, targetPath);
211
+ syncedCount.value++;
212
+ }
213
+ })());
214
+ }
215
+
216
+ await Promise.all(tasks);
217
+
218
+ // Se não conseguimos sincronizar nenhum arquivo específico, faz sync completo
219
+ if (syncedCount.value === 0) {
220
+ await this.fastSync(sourceDir, targetLib);
221
+ } else if (!this.projectConfig.quiet) {
222
+ Logger.info("sync", `${syncedCount.value} classe(s) sincronizada(s)`);
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Converte caminho de arquivo .java para caminho relativo de .class
228
+ */
229
+ private javaToClassPath(javaFile: string): string | null {
230
+ // Remove prefixos comuns de diretórios source
231
+ const parts = javaFile.split(/[/\\]/);
232
+
233
+ // Encontra o índice após "java" ou "src/main/java" ou "src"
234
+ let startIndex = -1;
235
+
236
+ for (let i = 0; i < parts.length; i++) {
237
+ if (parts[i] === "java" && i > 0 && (parts[i-1] === "main" || parts[i-1] === "test")) {
238
+ startIndex = i + 1;
239
+ break;
240
+ }
241
+ }
242
+
243
+ // Se não encontrou padrão maven, tenta achar "src"
244
+ if (startIndex === -1) {
245
+ const srcIndex = parts.indexOf("src");
246
+ if (srcIndex !== -1 && srcIndex < parts.length - 1) {
247
+ // Pula "src" e possível "main/java"
248
+ if (parts[srcIndex + 1] === "main" && parts[srcIndex + 2] === "java") {
249
+ startIndex = srcIndex + 3;
250
+ } else {
251
+ startIndex = srcIndex + 1;
252
+ }
253
+ }
254
+ }
255
+
256
+ // Se ainda não encontrou, assume que o caminho já é relativo ao package
257
+ if (startIndex === -1) {
258
+ startIndex = 0;
259
+ }
260
+
261
+ // Pega o caminho relativo
262
+ const relativeParts = parts.slice(startIndex);
263
+ if (relativeParts.length === 0) return null;
264
+
265
+ // Substitui extensão .java por .class
266
+ const fileName = relativeParts[relativeParts.length - 1];
267
+ if (!fileName || !fileName.endsWith(".java")) return null;
268
+
269
+ relativeParts[relativeParts.length - 1] = fileName.replace(".java", ".class");
270
+
271
+ return path.join(...relativeParts);
272
+ }
273
+
145
274
  private async fastSync(src: string, dest: string) {
146
275
  const entries = readdirSync(src, { withFileTypes: true });
147
276
 
@@ -10,6 +10,17 @@ import {
10
10
  import path from "path";
11
11
  import os from "os";
12
12
  import { spawn } from "child_process";
13
+ import {
14
+ getPlatform,
15
+ isWindows,
16
+ getTomcatArchiveName,
17
+ getTomcatDownloadUrl,
18
+ getTomcatArchiveUrl,
19
+ getExtractCommand,
20
+ getPortCheckCommand,
21
+ getCatalinaScript,
22
+ hasCatalinaScript,
23
+ } from "../utils/platform";
13
24
 
14
25
  export interface EmbeddedTomcatOptions {
15
26
  version?: string;
@@ -35,20 +46,18 @@ export class EmbeddedTomcatService {
35
46
  private isInstalled: boolean = false;
36
47
 
37
48
  // Versões estáveis do Tomcat (atualizadas: 2026-03-04)
49
+ // URLs são construídas dinamicamente baseadas na plataforma
38
50
  private static readonly VERSIONS: Record<
39
51
  string,
40
- { url: string; sha512: string }
52
+ { sha512: string }
41
53
  > = {
42
54
  "10.1.52": {
43
- url: "https://dlcdn.apache.org/tomcat/tomcat-10/v10.1.52/bin/apache-tomcat-10.1.52-windows-x64.zip",
44
55
  sha512: "",
45
56
  },
46
57
  "9.0.115": {
47
- url: "https://dlcdn.apache.org/tomcat/tomcat-9/v9.0.115/bin/apache-tomcat-9.0.115-windows-x64.zip",
48
58
  sha512: "",
49
59
  },
50
60
  "11.0.18": {
51
- url: "https://dlcdn.apache.org/tomcat/tomcat-11/v11.0.18/bin/apache-tomcat-11.0.18-windows-x64.zip",
52
61
  sha512: "",
53
62
  },
54
63
  };
@@ -61,14 +70,14 @@ export class EmbeddedTomcatService {
61
70
  this.baseDir = path.join(os.homedir(), ".xavva", "tomcat");
62
71
  this.tomcatHome = path.join(this.baseDir, this.version);
63
72
 
64
- // Se a versão não está na lista, usa URL padrão
73
+ // Constrói URL de download baseada na plataforma
65
74
  const versionInfo = EmbeddedTomcatService.VERSIONS[this.version];
66
75
  if (versionInfo) {
67
- this.downloadUrl = versionInfo.url;
76
+ // Usa URL primária (CDN Apache)
77
+ this.downloadUrl = getTomcatDownloadUrl(this.version);
68
78
  } else {
69
- // Tenta inferir URL baseado no padrão Apache
70
- const majorVersion = this.version.split(".")[0];
71
- this.downloadUrl = `https://archive.apache.org/dist/tomcat/tomcat-${majorVersion}/v${this.version}/bin/apache-tomcat-${this.version}-windows-x64.zip`;
79
+ // Tenta inferir URL baseado no padrão Apache (archive)
80
+ this.downloadUrl = getTomcatArchiveUrl(this.version);
72
81
  }
73
82
  }
74
83
 
@@ -76,8 +85,7 @@ export class EmbeddedTomcatService {
76
85
  * Verifica se o Tomcat já está instalado
77
86
  */
78
87
  checkInstallation(): boolean {
79
- const catalinaBat = path.join(this.tomcatHome, "bin", "catalina.bat");
80
- this.isInstalled = existsSync(catalinaBat);
88
+ this.isInstalled = hasCatalinaScript(this.tomcatHome);
81
89
  return this.isInstalled;
82
90
  }
83
91
 
@@ -100,13 +108,8 @@ export class EmbeddedTomcatService {
100
108
 
101
109
  for (const entry of entries) {
102
110
  if (entry.isDirectory()) {
103
- const catalinaBat = path.join(
104
- baseDir,
105
- entry.name,
106
- "bin",
107
- "catalina.bat",
108
- );
109
- if (existsSync(catalinaBat)) {
111
+ const tomcatPath = path.join(baseDir, entry.name);
112
+ if (hasCatalinaScript(tomcatPath)) {
110
113
  versions.push(entry.name);
111
114
  }
112
115
  }
@@ -133,10 +136,8 @@ export class EmbeddedTomcatService {
133
136
  mkdirSync(this.baseDir, { recursive: true });
134
137
  }
135
138
 
136
- const zipPath = path.join(
137
- this.baseDir,
138
- `apache-tomcat-${this.version}.zip`,
139
- );
139
+ const archiveName = getTomcatArchiveName(this.version);
140
+ const zipPath = path.join(this.baseDir, archiveName);
140
141
 
141
142
  try {
142
143
  // Download
@@ -300,21 +301,24 @@ export class EmbeddedTomcatService {
300
301
  */
301
302
  async isPortAvailable(): Promise<boolean> {
302
303
  return new Promise((resolve) => {
303
- const netstat = spawn("cmd", [
304
- "/c",
305
- `netstat -ano | findstr :${this.port}`,
306
- ]);
304
+ const cmd = getPortCheckCommand(this.port);
305
+ const checkProcess = spawn(cmd[0], cmd.slice(1));
307
306
  let output = "";
308
307
 
309
- netstat.stdout?.on("data", (data) => {
308
+ checkProcess.stdout?.on("data", (data) => {
309
+ output += data.toString();
310
+ });
311
+
312
+ checkProcess.stderr?.on("data", (data) => {
310
313
  output += data.toString();
311
314
  });
312
315
 
313
- netstat.on("close", () => {
316
+ checkProcess.on("close", () => {
317
+ // Se houver output, a porta está em uso
314
318
  resolve(output.trim().length === 0);
315
319
  });
316
320
 
317
- netstat.on("error", () => {
321
+ checkProcess.on("error", () => {
318
322
  resolve(true); // Assume disponível se não conseguir verificar
319
323
  });
320
324
  });
@@ -382,18 +386,23 @@ export class EmbeddedTomcatService {
382
386
  }
383
387
 
384
388
  /**
385
- * Extrai arquivo ZIP usando PowerShell
389
+ * Extrai arquivo de arquivos (ZIP ou tar.gz)
386
390
  */
387
391
  private async extractZip(zipPath: string, destDir: string): Promise<void> {
388
392
  const spinner = Logger.spinner("Extraindo arquivos...");
389
393
 
390
394
  return new Promise((resolve, reject) => {
391
- const ps = spawn("powershell", [
392
- "-command",
393
- `Expand-Archive -Path '${zipPath}' -DestinationPath '${destDir}' -Force`,
394
- ]);
395
+ const cmd = getExtractCommand(zipPath, destDir);
396
+
397
+ if (!cmd) {
398
+ spinner(false);
399
+ reject(new Error(`Formato de arquivo não suportado: ${path.extname(zipPath)}`));
400
+ return;
401
+ }
402
+
403
+ const extractProcess = spawn(cmd[0], cmd.slice(1));
395
404
 
396
- ps.on("close", (code) => {
405
+ extractProcess.on("close", (code) => {
397
406
  if (code === 0) {
398
407
  spinner(true);
399
408
  resolve();
@@ -403,7 +412,7 @@ export class EmbeddedTomcatService {
403
412
  }
404
413
  });
405
414
 
406
- ps.on("error", (err) => {
415
+ extractProcess.on("error", (err) => {
407
416
  spinner(false);
408
417
  reject(err);
409
418
  });
@@ -77,7 +77,7 @@ export class LogAnalyzer {
77
77
  const isProject = this.projectPrefixes.some(p => trimmed.includes(p));
78
78
 
79
79
  if (isProject) {
80
- return ` ${Logger.C.bold}${Logger.C.yellow}${trimmed}${Logger.C.reset}`;
80
+ return ` ${Logger.C.bold}${Logger.C.warning}${trimmed}${Logger.C.reset}`;
81
81
  } else {
82
82
  return ` ${Logger.C.dim}${trimmed}${Logger.C.reset}`;
83
83
  }
@@ -100,7 +100,7 @@ export class LogAnalyzer {
100
100
 
101
101
  let color = Logger.C.primary;
102
102
  let symbol = "●";
103
- if (level === "WARN") { color = Logger.C.yellow; symbol = "▲"; }
103
+ if (level === "WARN") { color = Logger.C.warning; symbol = "▲"; }
104
104
  else if (level === "ERROR") { color = Logger.C.red; symbol = "✖"; }
105
105
 
106
106
  return `${color}${symbol} ${Logger.C.bold}Hotswap:${Logger.C.reset} ${msg}`;
@@ -113,7 +113,7 @@ export class LogAnalyzer {
113
113
 
114
114
  let color = Logger.C.dim;
115
115
  let symbol = "ℹ";
116
- if (label === "WARNING") { color = Logger.C.yellow; symbol = "▲"; }
116
+ if (label === "WARNING") { color = Logger.C.warning; symbol = "▲"; }
117
117
  else if (label === "SEVERE" || label === "ERROR") { color = Logger.C.red; symbol = "✖"; }
118
118
 
119
119
  msg = msg.replace(/^(org\.apache|com\.sun|java\..*?|org\.glassfish)\.[a-zA-Z0-9.]+\s/, "").trim();
@@ -129,7 +129,7 @@ export class LogAnalyzer {
129
129
 
130
130
  let color = Logger.C.dim;
131
131
  let symbol = "ℹ";
132
- if (label === "WARNING" || label === "WARN") { color = Logger.C.yellow; symbol = "▲"; }
132
+ if (label === "WARNING" || label === "WARN") { color = Logger.C.warning; symbol = "▲"; }
133
133
  else if (label === "SEVERE" || label === "ERROR") { color = Logger.C.red; symbol = "✖"; }
134
134
 
135
135
  msg = msg.replace(/^(org\.apache|com\.sun|java\..*?)\.[a-zA-Z0-9.]+\s/, "").trim();
@@ -5,6 +5,14 @@ import { ProjectService } from "./ProjectService";
5
5
  import { existsSync, mkdirSync, writeFileSync, statSync, promises as fs } from "fs";
6
6
  import path from "path";
7
7
  import os from "os";
8
+ import {
9
+ getCatalinaPath,
10
+ getMemoryCommand,
11
+ getKillCommand,
12
+ getPortCheckCommand,
13
+ getJavaPath,
14
+ isWindows,
15
+ } from "../utils/platform";
8
16
 
9
17
  export class TomcatService {
10
18
  private activeConfig: TomcatConfig;
@@ -25,23 +33,55 @@ export class TomcatService {
25
33
  async getMemoryUsage(): Promise<string> {
26
34
  if (!this.pid) return "0 MB";
27
35
  try {
28
- const { stdout } = Bun.spawnSync(["powershell", "-command", `(Get-Process -Id ${this.pid}).WorkingSet64 / 1MB`]);
36
+ const cmd = getMemoryCommand(this.pid);
37
+ if (!cmd) return "N/A";
38
+
39
+ const { stdout } = Bun.spawnSync(cmd);
29
40
  const mem = await new Response(stdout).text();
30
- return `${Math.round(parseFloat(mem))} MB`;
41
+ const memValue = parseFloat(mem.trim());
42
+
43
+ if (isNaN(memValue)) return "N/A";
44
+
45
+ // No Linux/macOS, ps retorna em KB, convertemos para MB
46
+ if (!isWindows()) {
47
+ return `${Math.round(memValue / 1024)} MB`;
48
+ }
49
+
50
+ return `${Math.round(memValue)} MB`;
31
51
  } catch (e) {
32
52
  return "N/A";
33
53
  }
34
54
  }
35
55
 
36
56
  async killConflict() {
37
- const { stdout } = Bun.spawnSync(["cmd", "/c", `netstat -ano | findstr :${this.activeConfig.port}`]);
57
+ const cmd = getPortCheckCommand(this.activeConfig.port);
58
+ const { stdout } = Bun.spawnSync(cmd);
38
59
  const output = await new Response(stdout).text();
39
60
 
40
61
  if (output) {
41
- const lines = output.trim().split('\n');
42
- const pid = lines[0].trim().split(/\s+/).pop();
43
- Logger.step(`Freeing port ${this.activeConfig.port}`);
44
- Bun.spawnSync(["taskkill", "/F", "/PID", pid]);
62
+ // Extrai PID do output
63
+ let pid: string | undefined;
64
+
65
+ if (isWindows()) {
66
+ // Windows: netstat output, última coluna é o PID
67
+ const lines = output.trim().split('\n');
68
+ pid = lines[0].trim().split(/\s+/).pop();
69
+ } else {
70
+ // Linux/Mac: tenta extrair PID de lsof, ss ou netstat
71
+ // lsof -i :port: formato tem PID na coluna 2
72
+ // ss -tlnp: tem pid=XXXX
73
+ // netstat -tlnp: tem /XXXX no final
74
+ const match = output.match(/\b(\d+)\b/) || // número isolado (lsof)
75
+ output.match(/pid=(\d+)/) || // ss format
76
+ output.match(/\/(\d+)/); // netstat format
77
+ if (match) pid = match[1];
78
+ }
79
+
80
+ if (pid) {
81
+ Logger.step(`Freeing port ${this.activeConfig.port}`);
82
+ const killCmd = getKillCommand(pid);
83
+ Bun.spawnSync(killCmd);
84
+ }
45
85
  }
46
86
  }
47
87
 
@@ -105,21 +145,54 @@ export class TomcatService {
105
145
 
106
146
  Logger.step("Downloading HotswapAgent v2.0.3 (Global)...");
107
147
  const url = "https://github.com/HotswapProjects/HotswapAgent/releases/download/RELEASE-2.0.3/hotswap-agent-2.0.3.jar";
108
- const response = await fetch(url);
109
- if (!response.ok) throw new Error(`Status: ${response.status}`);
148
+
149
+ Logger.debug(`URL: ${url}`);
150
+ Logger.debug(`Destino: ${agentPath}`);
151
+
152
+ const response = await fetch(url, {
153
+ redirect: "follow",
154
+ });
155
+
156
+ if (!response.ok) {
157
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
158
+ }
159
+
160
+ const contentLength = response.headers.get("content-length");
161
+ Logger.debug(`Content-Length: ${contentLength || "unknown"}`);
110
162
 
111
163
  const buffer = await response.arrayBuffer();
164
+ Logger.debug(`Downloaded: ${buffer.byteLength} bytes`);
165
+
166
+ if (buffer.byteLength < 1000) {
167
+ throw new Error(`Arquivo muito pequeno (${buffer.byteLength} bytes)`);
168
+ }
169
+
112
170
  writeFileSync(agentPath, Buffer.from(buffer));
171
+
172
+ // Verifica se foi escrito corretamente
173
+ const stats = statSync(agentPath);
174
+ Logger.debug(`Escrito: ${stats.size} bytes`);
175
+
113
176
  Logger.success("HotswapAgent v2.0.3 installed globally!");
114
177
  return agentPath;
115
- } catch (e) {
116
- Logger.warn("Falha ao baixar HotswapAgent. Usando hot swap padrão da JVM.");
178
+ } catch (e: any) {
179
+ Logger.warn(`Falha ao baixar HotswapAgent: ${e.message}`);
180
+ Logger.warn("Usando hot swap padrão da JVM.");
181
+
182
+ // Limpa arquivo parcial se existir
183
+ if (existsSync(agentPath)) {
184
+ try {
185
+ await fs.unlink(agentPath);
186
+ Logger.debug("Arquivo parcial removido");
187
+ } catch {}
188
+ }
189
+
117
190
  return null;
118
191
  }
119
192
  }
120
193
 
121
194
  async start(config: AppConfig, isWatching: boolean = false) {
122
- const binPath = `${this.activeConfig.path}\\bin\\catalina.bat`;
195
+ const binPath = getCatalinaPath(this.activeConfig.path);
123
196
  const args = (config.project.debug || isWatching) ? ["jpda", "run"] : ["run"];
124
197
 
125
198
  const catalinaOpts = [process.env.CATALINA_OPTS || ""];
@@ -134,7 +207,7 @@ export class TomcatService {
134
207
  javaBin = path.join(process.env.JAVA_HOME, "bin", "java.exe");
135
208
  }
136
209
 
137
- const javaVer = Bun.spawnSync([javaBin, "-version"]);
210
+ const javaVer = Bun.spawnSync([getJavaPath(), "-version"]);
138
211
  const output = (javaVer.stderr.toString() + javaVer.stdout.toString()).toLowerCase();
139
212
 
140
213
  if (output.includes("dcevm") || output.includes("jbr") || output.includes("trava")) {
@@ -9,6 +9,11 @@ export class WatcherService {
9
9
  private pendingFullBuild = false;
10
10
  private coolingFiles = new Set<string>();
11
11
  private debounceTimer?: Timer;
12
+
13
+ // Rastreamento de arquivos modificados para build incremental inteligente
14
+ private modifiedFiles = new Set<string>();
15
+ private pendingFiles = new Set<string>(); // Arquivos modificados durante compilação
16
+ private hasPendingChanges = false;
12
17
 
13
18
  constructor(private config: AppConfig, private deployCmd: DeployCommand) {}
14
19
 
@@ -43,24 +48,57 @@ export class WatcherService {
43
48
  if (!isJava) return;
44
49
 
45
50
  Logger.watcher(filename, 'watch');
51
+
52
+ // Se estiver compilando, acumula na fila de pendentes
53
+ if (this.isDeploying) {
54
+ this.pendingFiles.add(filename);
55
+ this.hasPendingChanges = true;
56
+ return;
57
+ }
58
+
59
+ // Acumula arquivos modificados para o próximo build
60
+ this.modifiedFiles.add(filename);
61
+
46
62
  clearTimeout(this.debounceTimer);
47
63
 
48
64
  this.debounceTimer = setTimeout(() => {
49
- this.run(this.pendingFullBuild ? false : true);
65
+ const filesToCompile = [...this.modifiedFiles];
66
+ this.modifiedFiles.clear();
67
+ this.run(this.pendingFullBuild ? false : true, filesToCompile);
50
68
  this.pendingFullBuild = false;
51
69
  }, WATCHER_DEBOUNCE_MS);
52
70
  });
53
71
  }
54
72
 
55
- private async run(incremental = false) {
73
+ private async run(incremental = false, changedFiles?: string[]) {
56
74
  if (this.isDeploying) return;
57
75
  this.isDeploying = true;
76
+
58
77
  try {
59
- await this.deployCmd.execute(this.config, { watch: true, incremental });
78
+ // Passa os arquivos específicos que foram modificados
79
+ await this.deployCmd.execute(this.config, {
80
+ watch: true,
81
+ incremental,
82
+ changedFiles
83
+ });
60
84
  } catch (e) {
61
85
  // Error handled by command
62
86
  } finally {
63
87
  this.isDeploying = false;
88
+
89
+ // Se houve mudanças durante a compilação, processa imediatamente
90
+ if (this.hasPendingChanges) {
91
+ const pending = [...this.pendingFiles];
92
+ this.pendingFiles.clear();
93
+ this.hasPendingChanges = false;
94
+
95
+ Logger.watcher(`Processing ${pending.length} pending change(s)...`, 'warn');
96
+
97
+ // Pequeno delay para garantir que os arquivos foram salvos completamente
98
+ setTimeout(() => {
99
+ this.run(true, pending);
100
+ }, 100);
101
+ }
64
102
  }
65
103
  }
66
104
 
@@ -61,6 +61,7 @@ export interface CLIArguments {
61
61
  yes?: boolean;
62
62
  war?: boolean;
63
63
  cache?: boolean;
64
+ changedFiles?: string[];
64
65
  }
65
66
 
66
67
  export interface CommandContext {
@@ -10,8 +10,8 @@ export const DEFAULT_DEBUG_PORT = 5005;
10
10
 
11
11
  // Timeouts (em milissegundos)
12
12
  export const TIMEOUT_SHUTDOWN_MS = 5000;
13
- export const WATCHER_DEBOUNCE_MS = 1000;
14
- export const WATCHER_COOLING_MS = 500;
13
+ export const WATCHER_DEBOUNCE_MS = 1500;
14
+ export const WATCHER_COOLING_MS = 1000;
15
15
  export const BROWSER_OPEN_DELAY_MS = 800;
16
16
  export const DEPLOY_HEALTH_CHECK_DELAY_MS = 1500;
17
17
  export const HOTSWAP_DELAY_MS = 500;