@archznn/xavva 1.7.0 → 1.8.1

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.
@@ -1,17 +1,24 @@
1
- import type { TomcatConfig } from "../types/config";
1
+ import type { TomcatConfig, AppConfig } from "../types/config";
2
2
  import { Logger } from "../utils/ui";
3
+ import type { Subprocess } from "bun";
4
+ import { ProjectService } from "./ProjectService";
3
5
 
4
6
  export class TomcatService {
5
7
  private activeConfig: TomcatConfig;
6
- private currentProcess: any = null;
8
+ private currentProcess: Subprocess | null = null;
7
9
  private stopStartupSpinner?: (success?: boolean) => void;
8
10
  public onReady?: () => void;
9
11
  private pid: number | null = null;
12
+ private projectService: ProjectService | null = null;
10
13
 
11
14
  constructor(customConfig: TomcatConfig) {
12
15
  this.activeConfig = customConfig;
13
16
  }
14
17
 
18
+ setProjectService(projectService: ProjectService) {
19
+ this.projectService = projectService;
20
+ }
21
+
15
22
  async getMemoryUsage(): Promise<string> {
16
23
  if (!this.pid) return "0 MB";
17
24
  try {
@@ -35,28 +42,45 @@ export class TomcatService {
35
42
  }
36
43
  }
37
44
 
38
- clearWebapps(appName?: string) {
39
- const fs = require("fs");
45
+ async clearWebapps() {
46
+ const fs = require("fs").promises;
47
+ const { existsSync } = require("fs");
40
48
  const path = require("path");
41
49
  const webappsPath = path.join(this.activeConfig.path, "webapps");
42
50
  const workPath = path.join(this.activeConfig.path, "work");
43
51
  const tempPath = path.join(this.activeConfig.path, "temp");
44
52
 
45
53
  try {
46
- [workPath, tempPath].forEach(p => {
47
- if (fs.existsSync(p)) {
48
- fs.rmSync(p, { recursive: true, force: true });
49
- fs.mkdirSync(p);
54
+ const cleanDir = async (p: string) => {
55
+ if (existsSync(p)) {
56
+ await fs.rm(p, { recursive: true, force: true });
57
+
58
+ // Resiliência para Windows: garante que o diretório foi liberado antes do mkdir
59
+ let retries = 10;
60
+ while (retries > 0 && existsSync(p)) {
61
+ await new Promise(r => setTimeout(r, 50));
62
+ retries--;
63
+ }
64
+
65
+ await fs.mkdir(p, { recursive: true });
50
66
  }
51
- });
67
+ };
52
68
 
53
- const files = fs.readdirSync(webappsPath);
54
- for (const file of files) {
55
- const fullPath = path.join(webappsPath, file);
56
- if (file === "ROOT" || file === "manager" || file === "host-manager") continue;
57
-
58
- fs.rmSync(fullPath, { recursive: true, force: true });
69
+ const tasks: Promise<any>[] = [
70
+ cleanDir(workPath),
71
+ cleanDir(tempPath)
72
+ ];
73
+
74
+ if (existsSync(webappsPath)) {
75
+ const files = await fs.readdir(webappsPath);
76
+ for (const file of files) {
77
+ if (file === "ROOT" || file === "manager" || file === "host-manager") continue;
78
+ const fullPath = path.join(webappsPath, file);
79
+ tasks.push(fs.rm(fullPath, { recursive: true, force: true }));
80
+ }
59
81
  }
82
+
83
+ await Promise.all(tasks);
60
84
  } catch (e) {
61
85
  Logger.warn("Não foi possível limpar totalmente a pasta webapps ou cache.");
62
86
  }
@@ -97,48 +121,7 @@ export class TomcatService {
97
121
  }
98
122
  }
99
123
 
100
- private findAllClassPaths(buildTool: 'maven' | 'gradle'): string[] {
101
- const fs = require("fs");
102
- const path = require("path");
103
- const results: string[] = [];
104
- const root = process.cwd();
105
-
106
- const scan = (dir: string) => {
107
- try {
108
- const files = fs.readdirSync(dir, { withFileTypes: true });
109
- for (const file of files) {
110
- if (!file.isDirectory()) continue;
111
-
112
- const name = file.name;
113
- if (name.startsWith('.') || ['node_modules', 'out', 'bin', 'src', 'webapps', '.xavva'].includes(name)) continue;
114
-
115
- const fullPath = path.join(dir, name);
116
-
117
- const isMavenClasses = buildTool === 'maven' && name === 'classes' && dir.endsWith('target');
118
- const isGradleClasses = buildTool === 'gradle' && name === 'main' && dir.endsWith(path.join('classes', 'java'));
119
-
120
- if (isMavenClasses || isGradleClasses) {
121
- results.push(fullPath.replace(/\\/g, "/"));
122
- } else {
123
- scan(fullPath);
124
- }
125
- }
126
- } catch (e) {}
127
- };
128
-
129
- scan(root);
130
-
131
- if (results.length === 0) {
132
- const defaultPath = buildTool === 'maven'
133
- ? path.join(root, "target", "classes")
134
- : path.join(root, "build", "classes", "java", "main");
135
- results.push(defaultPath.replace(/\\/g, "/"));
136
- }
137
-
138
- return results;
139
- }
140
-
141
- async start(config: any, isWatching: boolean = false) {
124
+ async start(config: AppConfig, isWatching: boolean = false) {
142
125
  const binPath = `${this.activeConfig.path}\\bin\\catalina.bat`;
143
126
  const args = (config.project.debug || isWatching) ? ["jpda", "run"] : ["run"];
144
127
 
@@ -181,17 +164,28 @@ export class TomcatService {
181
164
  const xavvaDir = path.join(process.cwd(), ".xavva");
182
165
  if (!fs.existsSync(xavvaDir)) fs.mkdirSync(xavvaDir, { recursive: true });
183
166
 
184
- const classPaths = this.findAllClassPaths(config.project.buildTool);
185
- const extraClasspath = classPaths.join(",");
186
-
187
167
  const propsPath = path.join(xavvaDir, "hotswap-agent.properties");
188
- const propsContent = `autoHotswap=true\nautoHotswap.delay=3000\nwatchResources=false\nextraClasspath=${extraClasspath}\nLOGGER=info`;
168
+ const propsContent = `autoHotswap=true\nautoHotswap.delay=500\nwatchResources=false\nLOGGER=info`;
189
169
  fs.writeFileSync(propsPath, propsContent);
190
170
 
191
171
  catalinaOpts.push(`-Dhotswap-agent.properties.path=${propsPath}`);
172
+
173
+ if (this.projectService) {
174
+ const classPaths = this.projectService.findAllClassPaths();
175
+ if (classPaths.length > 0) {
176
+ catalinaOpts.push(`-Dhotswap.extraClasspath=${classPaths.join(",")}`);
177
+ }
178
+ }
192
179
  }
193
180
  }
194
181
 
182
+ // Otimizações para JSP e Debug de JSP
183
+ catalinaOpts.push(
184
+ "-Dorg.apache.jasper.compiler.development=true",
185
+ "-Dorg.apache.jasper.compiler.disableSmap=false",
186
+ "-Dorg.apache.jasper.compiler.classdebuginfo=true"
187
+ );
188
+
195
189
  if (config.project.skipScan) {
196
190
  catalinaOpts.push(
197
191
  "-Dtomcat.util.scan.StandardJarScanFilter.jarsToSkip=*.jar",
@@ -203,7 +197,7 @@ export class TomcatService {
203
197
  );
204
198
  }
205
199
 
206
- const env: any = {
200
+ const env: Record<string, string | undefined> = {
207
201
  ...process.env,
208
202
  CATALINA_HOME: this.activeConfig.path,
209
203
  CATALINA_OPTS: catalinaOpts.join(" ").trim()
@@ -215,8 +209,8 @@ export class TomcatService {
215
209
  }
216
210
 
217
211
  if (config.project.debug) {
218
- Logger.debug("Java Debugger habilitado na porta 5005");
219
- env.JPDA_ADDRESS = "5005";
212
+ Logger.debug(`Java Debugger habilitado na porta ${config.project.debugPort}`);
213
+ env.JPDA_ADDRESS = String(config.project.debugPort);
220
214
  env.JPDA_TRANSPORT = "dt_socket";
221
215
  }
222
216
 
@@ -227,13 +221,17 @@ export class TomcatService {
227
221
  this.currentProcess = Bun.spawn([binPath, ...args], {
228
222
  stdout: "pipe",
229
223
  stderr: "pipe",
230
- env: env
224
+ env: env as any
231
225
  });
232
226
 
233
227
  this.pid = this.currentProcess.pid;
234
228
 
235
- this.processLogStream(this.currentProcess.stdout, config.project.cleanLogs, config.project.quiet, config.project.verbose, config.tomcat.grep);
236
- this.processLogStream(this.currentProcess.stderr, config.project.cleanLogs, config.project.quiet, config.project.verbose, config.tomcat.grep);
229
+ if (this.currentProcess.stdout) {
230
+ this.processLogStream(this.currentProcess.stdout as any, config.project.cleanLogs, config.project.quiet, config.project.verbose, config.tomcat.grep || "");
231
+ }
232
+ if (this.currentProcess.stderr) {
233
+ this.processLogStream(this.currentProcess.stderr as any, config.project.cleanLogs, config.project.quiet, config.project.verbose, config.tomcat.grep || "");
234
+ }
237
235
  }
238
236
 
239
237
  private async processLogStream(stream: ReadableStream, clean: boolean, quiet: boolean, verbose: boolean, grep: string) {
@@ -0,0 +1,78 @@
1
+ import { watch } from "fs";
2
+ import { Logger } from "../utils/ui";
3
+ import { DeployCommand } from "../commands/DeployCommand";
4
+ import type { AppConfig } from "../types/config";
5
+
6
+ export class WatcherService {
7
+ private isDeploying = false;
8
+ private pendingFullBuild = false;
9
+ private coolingFiles = new Set<string>();
10
+ private debounceTimer?: Timer;
11
+
12
+ constructor(private config: AppConfig, private deployCmd: DeployCommand) {}
13
+
14
+ public async start() {
15
+ await this.run(false);
16
+
17
+ watch(process.cwd(), { recursive: true }, async (event, filename) => {
18
+ if (!filename) return;
19
+
20
+ if (this.coolingFiles.has(filename)) return;
21
+ this.coolingFiles.add(filename);
22
+ setTimeout(() => this.coolingFiles.delete(filename), 500);
23
+
24
+ if (this.isIgnored(filename)) return;
25
+
26
+ const isBuildConfig = filename === "pom.xml" || filename === "build.gradle" || filename === "build.gradle.kts";
27
+ const isJava = filename.endsWith(".java") || isBuildConfig;
28
+ const isResource = this.isResourceFile(filename);
29
+
30
+ if (isBuildConfig) {
31
+ Logger.watcher(`Build configuration changed: ${filename}`, 'warn');
32
+ const { BuildCacheService } = await import("./BuildCacheService");
33
+ new BuildCacheService().clearCache();
34
+ this.pendingFullBuild = true;
35
+ }
36
+
37
+ if (isResource && !isJava) {
38
+ await this.deployCmd.syncResource(this.config, filename);
39
+ return;
40
+ }
41
+
42
+ if (!isJava) return;
43
+
44
+ Logger.watcher(filename, 'watch');
45
+ clearTimeout(this.debounceTimer);
46
+
47
+ this.debounceTimer = setTimeout(() => {
48
+ this.run(this.pendingFullBuild ? false : true);
49
+ this.pendingFullBuild = false;
50
+ }, 1000);
51
+ });
52
+ }
53
+
54
+ private async run(incremental = false) {
55
+ if (this.isDeploying) return;
56
+ this.isDeploying = true;
57
+ try {
58
+ await this.deployCmd.execute(this.config, { watch: true, incremental });
59
+ } catch (e) {
60
+ // Error handled by command
61
+ } finally {
62
+ this.isDeploying = false;
63
+ }
64
+ }
65
+
66
+ private isIgnored(filename: string): boolean {
67
+ return filename.includes("target") ||
68
+ filename.includes("build") ||
69
+ filename.includes("node_modules") ||
70
+ filename.split(/[/\\]/).some(part => part.startsWith("."));
71
+ }
72
+
73
+ private isResourceFile(filename: string): boolean {
74
+ return filename.endsWith(".jsp") || filename.endsWith(".html") ||
75
+ filename.endsWith(".css") || filename.endsWith(".js") ||
76
+ filename.endsWith(".xml") || filename.endsWith(".properties");
77
+ }
78
+ }
@@ -11,10 +11,12 @@ export interface ProjectConfig {
11
11
  profile: string;
12
12
  skipBuild: boolean;
13
13
  skipScan: boolean;
14
- cleanLogs: boolean;
14
+ clean: boolean;
15
15
  quiet: boolean;
16
16
  verbose: boolean;
17
17
  debug: boolean;
18
+ debugPort: number;
19
+ cleanLogs: boolean;
18
20
  grep?: string;
19
21
  }
20
22
 
@@ -22,3 +24,30 @@ export interface AppConfig {
22
24
  tomcat: TomcatConfig;
23
25
  project: ProjectConfig;
24
26
  }
27
+
28
+ export interface CLIArguments {
29
+ path?: string;
30
+ tool?: string;
31
+ name?: string;
32
+ port?: string;
33
+ "no-build"?: boolean;
34
+ scan?: boolean;
35
+ clean?: boolean;
36
+ quiet?: boolean;
37
+ help?: boolean;
38
+ version?: boolean;
39
+ debug?: boolean;
40
+ watch?: boolean;
41
+ profile?: string;
42
+ grep?: string;
43
+ verbose?: boolean;
44
+ dp?: string;
45
+ fix?: boolean;
46
+ incremental?: boolean;
47
+ }
48
+
49
+ export interface CommandContext {
50
+ config: AppConfig;
51
+ positionals: string[];
52
+ values: CLIArguments;
53
+ }
@@ -1,10 +1,10 @@
1
1
  import { parseArgs } from "util";
2
2
  import path from "path";
3
3
  import fs from "fs";
4
- import type { AppConfig } from "../types/config";
4
+ import type { AppConfig, CLIArguments, CommandContext } from "../types/config";
5
5
 
6
6
  export class ConfigManager {
7
- static async load(): Promise<{ config: AppConfig, positionals: string[], values: any }> {
7
+ static async load(): Promise<CommandContext> {
8
8
  const args = Bun.argv.slice(Bun.argv[0].endsWith("bun.exe") || Bun.argv[0].endsWith("bun") ? 2 : 1);
9
9
 
10
10
  const { values, positionals } = parseArgs({
@@ -25,12 +25,14 @@ export class ConfigManager {
25
25
  profile: { type: "string", short: "P" },
26
26
  grep: { type: "string", short: "G" },
27
27
  verbose: { type: "boolean", short: "V" },
28
+ dp: { type: "string" },
28
29
  fix: { type: "boolean" },
29
30
  },
30
31
  strict: false,
31
32
  allowPositionals: true,
32
33
  });
33
34
 
35
+ const cliValues = values as CLIArguments;
34
36
  const isDev = positionals.includes("dev");
35
37
  const isRun = positionals.includes("run") || positionals.includes("debug");
36
38
 
@@ -47,30 +49,32 @@ export class ConfigManager {
47
49
 
48
50
  const config: AppConfig = {
49
51
  tomcat: {
50
- path: String(values.path || envTomcatPath),
51
- port: parseInt(String(values.port || "8080")),
52
+ path: String(cliValues.path || envTomcatPath),
53
+ port: parseInt(String(cliValues.port || "8080")),
52
54
  webapps: "webapps",
53
- grep: values.grep ? String(values.grep) : "",
55
+ grep: cliValues.grep ? String(cliValues.grep) : "",
54
56
  },
55
57
  project: {
56
- appName: values.name ? String(values.name) : "",
57
- buildTool: (values.tool as "maven" | "gradle") || detectedTool,
58
- profile: String(values.profile || ""),
59
- skipBuild: !!values["no-build"],
60
- skipScan: values.scan !== undefined ? !values.scan : true,
61
- cleanLogs: !!(values.clean || isDev),
62
- quiet: !!(values.quiet || isDev),
63
- verbose: !!values.verbose,
64
- debug: !!(values.debug || isDev || isRun),
65
- grep: runClass || (values.grep ? String(values.grep) : ""),
58
+ appName: cliValues.name ? String(cliValues.name) : "",
59
+ buildTool: (cliValues.tool as "maven" | "gradle") || detectedTool,
60
+ profile: String(cliValues.profile || ""),
61
+ skipBuild: !!cliValues["no-build"],
62
+ skipScan: cliValues.scan !== undefined ? !cliValues.scan : true,
63
+ clean: !!cliValues.clean,
64
+ cleanLogs: cliValues.verbose ? false : true,
65
+ quiet: cliValues.verbose ? false : true,
66
+ verbose: !!cliValues.verbose,
67
+ debug: !!(cliValues.debug || isDev || isRun),
68
+ debugPort: parseInt(String(cliValues.dp || "5005")),
69
+ grep: runClass || (cliValues.grep ? String(cliValues.grep) : ""),
66
70
  }
67
71
  };
68
72
 
69
- if (isDev) values.watch = true;
73
+ if (isDev) cliValues.watch = true;
70
74
 
71
75
  this.ensureGitIgnore();
72
76
 
73
- return { config, positionals, values };
77
+ return { config, positionals, values: cliValues };
74
78
  }
75
79
 
76
80
  private static detectBuildTool(): "maven" | "gradle" {
package/src/utils/ui.ts CHANGED
@@ -54,21 +54,21 @@ export class Logger {
54
54
  static banner(command?: string) {
55
55
  console.clear();
56
56
  const git = this.getGitContext();
57
- const name = (process.cwd().split(/[/\\]/).pop() || "JAVA").toUpperCase();
57
+ const name = (process.cwd().split(/[/\\]/).pop() || "PROJECT").toUpperCase();
58
+ const version = `v${pkg.version}`;
58
59
 
59
- const width = 62;
60
- const line = "".repeat(width);
61
-
62
- this.write(`${this.C.gray}╭${line}╮`);
63
- this.write(`${this.C.gray}│ ${this.C.bold}${this.C.blue}${name} CLI${this.C.reset}${" ".repeat(width - name.length - 6)} ${this.C.gray}│`);
60
+ const mode = command?.toUpperCase() || "DEPLOY";
61
+ const modeColor = mode === "DEV" ? this.C.green : this.C.blue;
62
+ const modeIcon = mode === "DEV" ? "⚡" : "🚀";
63
+
64
+ console.log("");
65
+ console.log(` ${this.C.bold}${this.C.cyan}X A V V A${this.C.reset} ${this.C.dim}─${this.C.reset} ${this.C.bold}${this.C.white}${name}${this.C.reset}`);
64
66
 
65
- const info = `Version: ${pkg.version} | Branch: ${git.branch} | ${git.hash}`;
66
- this.write(`${this.C.gray} ${this.C.dim}${info}${" ".repeat(width - info.length - 2)}${this.C.gray}│`);
67
+ const gitInfo = git.branch ? `${this.C.magenta}🌿 ${git.branch}${this.C.reset} ${this.C.dim}•${this.C.reset} ${this.C.yellow}${git.hash}${this.C.reset}` : "";
68
+ console.log(` ${this.C.dim}📦 ${version}${gitInfo ? ` ${this.C.dim}•${this.C.reset} ${gitInfo}` : ""}${this.C.reset}`);
67
69
 
68
- const modeLine = `Mode: ${command?.toUpperCase() || "DEPLOY"}`;
69
- const status = command === 'dev' ? `${this.C.green}🟢` : `${this.C.blue}🔵`;
70
- this.write(`${this.C.gray}│ ${this.C.yellow}${this.C.bold}${modeLine}${" ".repeat(width - modeLine.length - 5)}${status} ${this.C.gray}│`);
71
- this.write(`${this.C.gray}╰${line}╯${this.C.reset}`);
70
+ console.log(` ${modeColor}${this.C.bold}⬢ ${modeIcon} ${mode} MODE${this.C.reset}`);
71
+ console.log(` ${this.C.dim}─────────────────────────────────────────────────${this.C.reset}`);
72
72
  }
73
73
 
74
74
  static section(title: string) {
@@ -162,7 +162,9 @@ export class Logger {
162
162
  "org.apache.catalina.core.StandardContext.setPath", "milliseconds",
163
163
  "org.apache.catalina.startup.HostConfig.deployWAR", "org.apache.catalina.startup.HostConfig.deployDirectory",
164
164
  "Deployment of web application", "Deploying web application archive", "at org.apache",
165
- "Registering directory"
165
+ "Registering directory", "initialized in ClassLoader", "Discovered plugins:",
166
+ "enhanced with plugin initialization", "registerJerseyContainer", "JasperLoader@",
167
+ "Hotswap ready (Plugins:", "autoHotswap.delay", "watchResources=false"
166
168
  ];
167
169
  return noise.some(n => line.includes(n));
168
170
  }