@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.
@@ -0,0 +1,83 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import crypto from "crypto";
4
+
5
+ export interface CacheData {
6
+ lastConfigHash: string;
7
+ lastBuildTime: number;
8
+ }
9
+
10
+ export class BuildCacheService {
11
+ private cacheDir: string;
12
+ private cacheFile: string;
13
+
14
+ constructor() {
15
+ this.cacheDir = path.join(process.cwd(), ".xavva");
16
+ this.cacheFile = path.join(this.cacheDir, "build-cache.json");
17
+ if (!fs.existsSync(this.cacheDir)) {
18
+ fs.mkdirSync(this.cacheDir, { recursive: true });
19
+ }
20
+ }
21
+
22
+ getHash(filePath: string): string {
23
+ if (!fs.existsSync(filePath)) return "";
24
+ const content = fs.readFileSync(filePath);
25
+ return crypto.createHash("md5").update(content).digest("hex");
26
+ }
27
+
28
+ getConfigHash(tool: "maven" | "gradle"): string {
29
+ const file = tool === "maven" ? "pom.xml" : "build.gradle";
30
+ const configPath = path.join(process.cwd(), file);
31
+ let hash = this.getHash(configPath);
32
+
33
+ // Se for gradle, também checar build.gradle.kts e settings
34
+ if (tool === "gradle") {
35
+ const kts = path.join(process.cwd(), "build.gradle.kts");
36
+ const settings = path.join(process.cwd(), "settings.gradle");
37
+ if (fs.existsSync(kts)) hash += this.getHash(kts);
38
+ if (fs.existsSync(settings)) hash += this.getHash(settings);
39
+ }
40
+
41
+ return crypto.createHash("md5").update(hash).digest("hex");
42
+ }
43
+
44
+ shouldRebuild(tool: "maven" | "gradle", projectService?: any): boolean {
45
+ if (!fs.existsSync(this.cacheFile)) return true;
46
+
47
+ try {
48
+ const currentHash = this.getConfigHash(tool);
49
+ const cache: CacheData = JSON.parse(fs.readFileSync(this.cacheFile, "utf-8"));
50
+
51
+ // Se o pom/gradle mudou, precisa de rebuild completo
52
+ if (currentHash !== cache.lastConfigHash) return true;
53
+
54
+ // Verificar se o artefato (.war) ainda existe fisicamente
55
+ if (projectService) {
56
+ try {
57
+ const artifact = projectService.getArtifact();
58
+ if (!fs.existsSync(artifact.path)) return true;
59
+ } catch (e) {
60
+ return true;
61
+ }
62
+ }
63
+
64
+ return false;
65
+ } catch (e) {
66
+ return true;
67
+ }
68
+ }
69
+
70
+ saveCache(tool: "maven" | "gradle") {
71
+ const data: CacheData = {
72
+ lastConfigHash: this.getConfigHash(tool),
73
+ lastBuildTime: Date.now()
74
+ };
75
+ fs.writeFileSync(this.cacheFile, JSON.stringify(data, null, 2));
76
+ }
77
+
78
+ clearCache() {
79
+ if (fs.existsSync(this.cacheFile)) {
80
+ fs.unlinkSync(this.cacheFile);
81
+ }
82
+ }
83
+ }
@@ -1,47 +1,78 @@
1
- import { readdirSync, copyFileSync, existsSync, statSync, mkdirSync } from "fs";
1
+ import { readdirSync, existsSync, statSync, mkdirSync, promises as fs } from "fs";
2
2
  import path from "path";
3
3
  import type { ProjectConfig, TomcatConfig } from "../types/config";
4
4
  import { Logger } from "../utils/ui";
5
+ import { BuildCacheService } from "./BuildCacheService";
6
+ import { ProjectService } from "./ProjectService";
5
7
 
6
8
  export class BuildService {
7
- private inferredAppName: string | null = null;
8
-
9
- constructor(private projectConfig: ProjectConfig, private tomcatConfig: TomcatConfig) { }
9
+ constructor(
10
+ private projectConfig: ProjectConfig,
11
+ private tomcatConfig: TomcatConfig,
12
+ private projectService: ProjectService,
13
+ private cache: BuildCacheService
14
+ ) { }
10
15
 
11
16
  async runBuild(incremental = false) {
17
+ if (this.projectConfig.clean) {
18
+ this.cache.clearCache();
19
+ }
20
+
21
+ if (!incremental && !this.projectConfig.skipBuild) {
22
+ if (!this.projectConfig.clean && !this.cache.shouldRebuild(this.projectConfig.buildTool, this.projectService)) {
23
+ Logger.success("Build cache hit! Skipping full build.");
24
+ return;
25
+ }
26
+ }
27
+
12
28
  const command = [];
29
+ const env = { ...process.env };
13
30
 
14
31
  if (this.projectConfig.buildTool === 'maven') {
15
32
  command.push("mvn");
33
+
34
+ if (!this.cache.shouldRebuild('maven', this.projectService)) {
35
+ command.push("-o");
36
+ }
37
+
16
38
  if (incremental) {
17
39
  command.push("compile");
18
40
  } else {
19
- command.push("clean", "package");
41
+ if (this.projectConfig.clean) command.push("clean");
42
+ command.push("compile", "war:exploded");
43
+ command.push("-T", "1C");
20
44
  }
21
- command.push("-DskipTests");
45
+ command.push("-Dmaven.test.skip=true", "-Dmaven.javadoc.skip=true");
22
46
  if (this.projectConfig.profile) command.push(`-P${this.projectConfig.profile}`);
47
+
48
+ env.MAVEN_OPTS = "-Xms512m -Xmx1024m -XX:+UseParallelGC";
23
49
  } else {
24
50
  command.push("gradle");
25
51
  if (incremental) {
26
52
  command.push("classes");
27
53
  } else {
28
- command.push("clean", "build");
54
+ if (this.projectConfig.clean) command.push("clean");
55
+ command.push("war");
56
+ command.push("--parallel", "--build-cache");
29
57
  }
30
- command.push("-x", "test");
58
+ command.push("-x", "test", "-x", "javadoc");
31
59
  if (this.projectConfig.profile) command.push(`-Pprofile=${this.projectConfig.profile}`);
60
+
61
+ env.GRADLE_OPTS = "-Xmx1024m -Dorg.gradle.daemon=true";
32
62
  }
33
63
 
34
64
  const stopSpinner = (this.projectConfig.verbose) ? () => {} : Logger.spinner(incremental ? "Incremental compilation" : "Full project build");
35
65
 
36
66
  const proc = Bun.spawn(command, {
37
67
  stdout: "pipe",
38
- stderr: "pipe"
68
+ stderr: "pipe",
69
+ env: env as any
39
70
  });
40
71
 
41
72
  if (this.projectConfig.verbose) {
42
73
  await Promise.all([
43
- this.processBuildLogs(proc.stdout, false),
44
- this.processBuildLogs(proc.stderr, false)
74
+ this.processBuildLogs(proc.stdout as ReadableStream, false),
75
+ this.processBuildLogs(proc.stderr as ReadableStream, false)
45
76
  ]);
46
77
  }
47
78
 
@@ -56,6 +87,10 @@ export class BuildService {
56
87
  Logger.error(`${this.projectConfig.buildTool.toUpperCase()} build failed!`);
57
88
  throw new Error("Falha no build do Java!");
58
89
  }
90
+
91
+ if (!incremental) {
92
+ this.cache.saveCache(this.projectConfig.buildTool);
93
+ }
59
94
  }
60
95
 
61
96
  private async processBuildLogs(stream: ReadableStream, quiet: boolean) {
@@ -97,98 +132,86 @@ export class BuildService {
97
132
  }
98
133
  }
99
134
 
100
- async syncClasses(): Promise<string | null> {
101
- const fs = require("fs");
102
- let appFolder = this.projectConfig.appName || this.inferredAppName || "";
103
-
135
+ async syncClasses(customSrc?: string): Promise<string | null> {
136
+ const appFolder = this.projectService.getInferredAppName();
104
137
  const webappsPath = path.join(this.tomcatConfig.path, this.tomcatConfig.webapps);
105
138
 
106
- if (!appFolder && fs.existsSync(webappsPath)) {
107
- const folders = fs.readdirSync(webappsPath, { withFileTypes: true })
108
- .filter((dirent: any) => dirent.isDirectory() && !["ROOT", "manager", "host-manager", "docs", "examples"].includes(dirent.name));
109
-
110
- if (folders.length === 1) {
111
- appFolder = folders[0].name;
112
- } else if (folders.length > 1) {
113
- const sorted = folders.map((f: any) => ({
114
- name: f.name,
115
- time: fs.statSync(path.join(webappsPath, f.name)).mtimeMs
116
- })).sort((a: any, b: any) => b.time - a.time);
117
- appFolder = sorted[0].name;
118
- }
119
- }
120
-
121
- const sourceDir = this.projectConfig.buildTool === 'maven' ? 'target/classes' : 'build/classes/java/main';
122
- const destDir = path.join(webappsPath, appFolder, "WEB-INF", "classes");
139
+ const sourceDir = customSrc || this.projectService.getClassesDir();
140
+ const destDir = customSrc ? path.join(webappsPath, appFolder) : path.join(webappsPath, appFolder, "WEB-INF", "classes");
123
141
 
124
- if (!fs.existsSync(sourceDir)) return null;
125
-
126
- if (!appFolder || !fs.existsSync(destDir)) {
127
- return null;
142
+ if (!existsSync(sourceDir)) return null;
143
+ if (!appFolder || !existsSync(destDir)) {
144
+ if (customSrc && appFolder) {
145
+ mkdirSync(destDir, { recursive: true });
146
+ } else {
147
+ return null;
148
+ }
128
149
  }
129
150
 
130
- const copyDir = (src: string, dest: string) => {
131
- if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
132
- const list = fs.readdirSync(src, { withFileTypes: true });
133
- for (const item of list) {
151
+ const fastSync = async (src: string, dest: string) => {
152
+ if (!existsSync(dest)) await fs.mkdir(dest, { recursive: true });
153
+ const list = await fs.readdir(src, { withFileTypes: true });
154
+
155
+ const tasks = list.map(async (item) => {
134
156
  const s = path.join(src, item.name);
135
157
  const d = path.join(dest, item.name);
158
+
136
159
  if (item.isDirectory()) {
137
- copyDir(s, d);
160
+ await fastSync(s, d);
138
161
  } else {
139
- if (!fs.existsSync(d) || fs.statSync(s).mtimeMs > fs.statSync(d).mtimeMs) {
140
- fs.copyFileSync(s, d);
162
+ const sStat = await fs.stat(s);
163
+ let shouldCopy = false;
164
+
165
+ if (!existsSync(d)) {
166
+ shouldCopy = true;
167
+ } else {
168
+ const dStat = await fs.stat(d);
169
+ if (sStat.mtimeMs > dStat.mtimeMs || dStat.size === 0) {
170
+ shouldCopy = true;
171
+ }
172
+ }
173
+
174
+ if (shouldCopy) {
175
+ let retries = 3;
176
+ while (retries > 0) {
177
+ try {
178
+ await fs.copyFile(s, d);
179
+ const finalStat = await fs.stat(d);
180
+ if (item.name.endsWith(".jar") && finalStat.size === 0 && sStat.size > 0) {
181
+ throw new Error("Zero byte copy detected");
182
+ }
183
+ await fs.utimes(d, sStat.atime, sStat.mtime);
184
+ break;
185
+ } catch (e) {
186
+ retries--;
187
+ if (retries === 0) {
188
+ Logger.warn(`Failed to copy ${item.name} after retries.`);
189
+ } else {
190
+ await new Promise(r => setTimeout(r, 100));
191
+ }
192
+ }
193
+ }
141
194
  }
142
195
  }
143
- }
196
+ });
197
+
198
+ await Promise.all(tasks);
144
199
  };
145
200
 
146
- copyDir(sourceDir, destDir);
201
+ await fastSync(sourceDir, destDir);
147
202
  return appFolder;
148
203
  }
149
204
 
150
- async deployToWebapps(): Promise<{ path: string, finalName: string }> {
151
- const destDir = path.join(this.tomcatConfig.path, this.tomcatConfig.webapps);
152
-
205
+ async deployToWebapps(): Promise<{ path: string, finalName: string, isDirectory: boolean }> {
153
206
  Logger.step("Searching for generated artifacts");
154
207
 
155
- const findWars = (dir: string): string[] => {
156
- let results: string[] = [];
157
- const list = readdirSync(dir, { withFileTypes: true });
158
- for (const item of list) {
159
- const res = path.resolve(dir, item.name);
160
- if (item.isDirectory()) {
161
- if (item.name === 'target' || item.name === 'build') {
162
- results = results.concat(findWars(res));
163
- } else if (!['node_modules', '.git', 'src', 'webapps', 'bin', 'conf', 'lib', 'logs', 'temp', 'work'].includes(item.name)) {
164
- results = results.concat(findWars(res));
165
- }
166
- } else if (item.name.endsWith('.war')) {
167
- results.push(res);
168
- }
169
- }
170
- return results;
171
- };
172
-
173
- const allWars = findWars(process.cwd())
174
- .map(f => ({ path: f, name: path.basename(f), time: statSync(f).mtime.getTime() }))
175
- .sort((a, b) => b.time - a.time);
176
-
177
- if (allWars.length === 0) {
178
- throw new Error('Nenhum arquivo .war encontrado! Verifique se o build realmente gerou um artefato.');
179
- }
180
-
181
- const warFile = allWars[0];
182
- const finalName = this.projectConfig.appName ? `${this.projectConfig.appName}.war` : warFile.name;
208
+ const artifact = this.projectService.getArtifact();
183
209
 
184
210
  if (!this.projectConfig.quiet) {
185
- Logger.info("Artifact", warFile.name);
186
- if (this.projectConfig.appName) Logger.info("Deploy as", finalName);
187
- } else {
188
- Logger.process(`Deploying ${this.projectConfig.appName ? this.projectConfig.appName : warFile.name.replace(".war", "")}...`);
211
+ Logger.info(artifact.isDirectory ? "Exploded Dir" : "Artifact", path.basename(artifact.path));
212
+ if (this.projectConfig.appName) Logger.info("Deploy as", artifact.name);
189
213
  }
190
214
 
191
- this.inferredAppName = finalName.replace(".war", "");
192
- return { path: warFile.path, finalName };
215
+ return { path: artifact.path, finalName: artifact.name, isDirectory: artifact.isDirectory };
193
216
  }
194
217
  }
@@ -18,6 +18,23 @@ export class EndpointService {
18
18
  const content = readFileSync(res, 'utf8');
19
19
  const fileEndpoints = this.parseJavaFile(content, item.name, contextPath);
20
20
  endpoints.push(...fileEndpoints);
21
+ } else if (item.name.endsWith('.jsp')) {
22
+ const parts = res.split(/[/\\]/);
23
+ const webappIndex = parts.indexOf("webapp");
24
+ const webContentIndex = parts.indexOf("WebContent");
25
+ const rootIndex = webappIndex !== -1 ? webappIndex : webContentIndex;
26
+
27
+ if (rootIndex !== -1) {
28
+ const relPath = "/" + parts.slice(rootIndex + 1).join("/");
29
+ endpoints.push({
30
+ method: "GET",
31
+ path: relPath,
32
+ fullPath: this.combinePaths(contextPath, relPath),
33
+ className: "JSP",
34
+ methodName: item.name,
35
+ parameters: []
36
+ });
37
+ }
21
38
  }
22
39
  }
23
40
  };
@@ -0,0 +1,126 @@
1
+ import { existsSync, readdirSync, statSync } from "fs";
2
+ import path from "path";
3
+ import type { ProjectConfig } from "../types/config";
4
+
5
+ export class ProjectService {
6
+ constructor(private config: ProjectConfig) {}
7
+
8
+ getBuildOutputDir(): string {
9
+ return path.join(process.cwd(), this.config.buildTool === "maven" ? "target" : "build");
10
+ }
11
+
12
+ getClassesDir(): string {
13
+ return this.config.buildTool === "maven"
14
+ ? path.join(process.cwd(), "target", "classes")
15
+ : path.join(process.cwd(), "build", "classes", "java", "main");
16
+ }
17
+
18
+ getSourceDirs(): string[] {
19
+ return [
20
+ path.join(process.cwd(), "src", "main", "java"),
21
+ path.join(process.cwd(), "src", "main", "resources"),
22
+ path.join(process.cwd(), "src", "main", "webapp")
23
+ ].filter(d => existsSync(d));
24
+ }
25
+
26
+ getArtifact(): { path: string; name: string; isDirectory: boolean } {
27
+ const buildDir = this.getBuildOutputDir();
28
+ const artifacts = this.searchArtifacts(buildDir).sort((a, b) => b.time - a.time);
29
+
30
+ if (artifacts.length === 0) {
31
+ throw new Error(`Nenhum artefato (.war ou pasta exploded) encontrado em ${buildDir}!`);
32
+ }
33
+
34
+ const artifact = artifacts[0];
35
+ return {
36
+ path: artifact.path,
37
+ name: this.config.appName ? `${this.config.appName}.war` : artifact.name,
38
+ isDirectory: artifact.isDirectory
39
+ };
40
+ }
41
+
42
+ private searchArtifacts(dir: string): { path: string; name: string; time: number; isDirectory: boolean }[] {
43
+ let results: { path: string; name: string; time: number; isDirectory: boolean }[] = [];
44
+ if (!existsSync(dir)) return results;
45
+
46
+ const list = readdirSync(dir, { withFileTypes: true });
47
+ for (const item of list) {
48
+ const fullPath = path.resolve(dir, item.name);
49
+
50
+ if (item.isDirectory()) {
51
+ // Se for Maven e tiver WEB-INF, é um exploded war
52
+ if (this.config.buildTool === 'maven' && existsSync(path.join(fullPath, "WEB-INF"))) {
53
+ results.push({
54
+ path: fullPath,
55
+ name: `${item.name}.war`,
56
+ time: statSync(fullPath).mtime.getTime(),
57
+ isDirectory: true
58
+ });
59
+ } else if (item.name.endsWith('.war')) { // Algumas ferramentas podem gerar pastas .war
60
+ results.push({
61
+ path: fullPath,
62
+ name: item.name,
63
+ time: statSync(fullPath).mtime.getTime(),
64
+ isDirectory: true
65
+ });
66
+ } else if (['libs', 'distributions'].includes(item.name)) { // Gradle common dirs
67
+ results = results.concat(this.searchArtifacts(fullPath));
68
+ }
69
+ } else if (item.name.endsWith('.war')) {
70
+ results.push({
71
+ path: fullPath,
72
+ name: item.name,
73
+ time: statSync(fullPath).mtime.getTime(),
74
+ isDirectory: false
75
+ });
76
+ }
77
+ }
78
+ return results;
79
+ }
80
+
81
+ getInferredAppName(): string {
82
+ if (this.config.appName) return this.config.appName;
83
+ try {
84
+ const artifact = this.getArtifact();
85
+ return artifact.name.replace(".war", "");
86
+ } catch (e) {
87
+ return "ROOT";
88
+ }
89
+ }
90
+
91
+ findAllClassPaths(): string[] {
92
+ const results: string[] = [];
93
+ const root = process.cwd();
94
+
95
+ const scan = (dir: string) => {
96
+ try {
97
+ const files = readdirSync(dir, { withFileTypes: true });
98
+ for (const file of files) {
99
+ if (!file.isDirectory()) continue;
100
+
101
+ const name = file.name;
102
+ if (name.startsWith('.') || ['node_modules', 'out', 'bin', 'src', 'webapps', '.xavva'].includes(name)) continue;
103
+
104
+ const fullPath = path.join(dir, name);
105
+
106
+ const isMavenClasses = this.config.buildTool === 'maven' && name === 'classes' && dir.endsWith('target');
107
+ const isGradleClasses = this.config.buildTool === 'gradle' && name === 'main' && dir.endsWith(path.join('classes', 'java'));
108
+
109
+ if (isMavenClasses || isGradleClasses) {
110
+ results.push(fullPath.replace(/\\/g, "/"));
111
+ } else {
112
+ scan(fullPath);
113
+ }
114
+ }
115
+ } catch (e) {}
116
+ };
117
+
118
+ scan(root);
119
+
120
+ if (results.length === 0) {
121
+ results.push(this.getClassesDir().replace(/\\/g, "/"));
122
+ }
123
+
124
+ return results;
125
+ }
126
+ }