@archznn/xavva 1.6.5 → 1.8.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 +84 -48
- package/package.json +1 -1
- package/src/commands/AuditCommand.ts +6 -10
- package/src/commands/BuildCommand.ts +3 -3
- package/src/commands/Command.ts +2 -2
- package/src/commands/CommandRegistry.ts +36 -0
- package/src/commands/DeployCommand.ts +181 -105
- package/src/commands/DocsCommand.ts +11 -11
- package/src/commands/DoctorCommand.ts +331 -68
- package/src/commands/HelpCommand.ts +2 -1
- package/src/commands/RunCommand.ts +108 -31
- package/src/commands/StartCommand.ts +4 -2
- package/src/index.ts +43 -115
- package/src/services/AuditService.ts +7 -7
- package/src/services/BrowserService.ts +41 -0
- package/src/services/BuildCacheService.ts +75 -0
- package/src/services/BuildService.ts +96 -88
- package/src/services/EndpointService.ts +17 -3
- package/src/services/ProjectService.ts +126 -0
- package/src/services/TomcatService.ts +125 -18
- package/src/services/WatcherService.ts +78 -0
- package/src/types/config.ts +30 -1
- package/src/utils/config.ts +22 -18
- package/src/utils/ui.ts +198 -102
|
@@ -1,14 +1,30 @@
|
|
|
1
|
-
import { readdirSync,
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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)) {
|
|
23
|
+
Logger.success("Build cache hit! Skipping full build.");
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
12
28
|
const command = [];
|
|
13
29
|
|
|
14
30
|
if (this.projectConfig.buildTool === 'maven') {
|
|
@@ -16,18 +32,22 @@ export class BuildService {
|
|
|
16
32
|
if (incremental) {
|
|
17
33
|
command.push("compile");
|
|
18
34
|
} else {
|
|
19
|
-
command.push("clean"
|
|
35
|
+
if (this.projectConfig.clean) command.push("clean");
|
|
36
|
+
command.push("compile", "war:exploded");
|
|
37
|
+
command.push("-T", "1C");
|
|
20
38
|
}
|
|
21
|
-
command.push("-
|
|
39
|
+
command.push("-Dmaven.test.skip=true", "-Dmaven.javadoc.skip=true");
|
|
22
40
|
if (this.projectConfig.profile) command.push(`-P${this.projectConfig.profile}`);
|
|
23
41
|
} else {
|
|
24
42
|
command.push("gradle");
|
|
25
43
|
if (incremental) {
|
|
26
44
|
command.push("classes");
|
|
27
45
|
} else {
|
|
28
|
-
command.push("clean"
|
|
46
|
+
if (this.projectConfig.clean) command.push("clean");
|
|
47
|
+
command.push("war");
|
|
48
|
+
command.push("--parallel", "--build-cache");
|
|
29
49
|
}
|
|
30
|
-
command.push("-x", "test");
|
|
50
|
+
command.push("-x", "test", "-x", "javadoc");
|
|
31
51
|
if (this.projectConfig.profile) command.push(`-Pprofile=${this.projectConfig.profile}`);
|
|
32
52
|
}
|
|
33
53
|
|
|
@@ -40,8 +60,8 @@ export class BuildService {
|
|
|
40
60
|
|
|
41
61
|
if (this.projectConfig.verbose) {
|
|
42
62
|
await Promise.all([
|
|
43
|
-
this.processBuildLogs(proc.stdout, false),
|
|
44
|
-
this.processBuildLogs(proc.stderr, false)
|
|
63
|
+
this.processBuildLogs(proc.stdout as ReadableStream, false),
|
|
64
|
+
this.processBuildLogs(proc.stderr as ReadableStream, false)
|
|
45
65
|
]);
|
|
46
66
|
}
|
|
47
67
|
|
|
@@ -51,11 +71,15 @@ export class BuildService {
|
|
|
51
71
|
if (proc.exitCode !== 0) {
|
|
52
72
|
if (!this.projectConfig.verbose) {
|
|
53
73
|
const err = await new Response(proc.stderr).text();
|
|
54
|
-
|
|
74
|
+
Logger.log(err);
|
|
55
75
|
}
|
|
56
76
|
Logger.error(`${this.projectConfig.buildTool.toUpperCase()} build failed!`);
|
|
57
77
|
throw new Error("Falha no build do Java!");
|
|
58
78
|
}
|
|
79
|
+
|
|
80
|
+
if (!incremental) {
|
|
81
|
+
this.cache.saveCache(this.projectConfig.buildTool);
|
|
82
|
+
}
|
|
59
83
|
}
|
|
60
84
|
|
|
61
85
|
private async processBuildLogs(stream: ReadableStream, quiet: boolean) {
|
|
@@ -79,7 +103,7 @@ export class BuildService {
|
|
|
79
103
|
errorCount++;
|
|
80
104
|
if (errorCount > maxErrors && !this.projectConfig.verbose) {
|
|
81
105
|
if (errorCount === maxErrors + 1) {
|
|
82
|
-
|
|
106
|
+
Logger.warn("... e mais erros ocultos. Use -V para ver todos.");
|
|
83
107
|
}
|
|
84
108
|
continue;
|
|
85
109
|
}
|
|
@@ -92,107 +116,91 @@ export class BuildService {
|
|
|
92
116
|
}
|
|
93
117
|
|
|
94
118
|
const summarized = Logger.summarize(cleanLine);
|
|
95
|
-
if (summarized)
|
|
119
|
+
if (summarized) Logger.log(summarized);
|
|
96
120
|
}
|
|
97
121
|
}
|
|
98
122
|
}
|
|
99
123
|
|
|
100
|
-
async syncClasses(): Promise<string | null> {
|
|
101
|
-
const
|
|
102
|
-
let appFolder = this.projectConfig.appName || this.inferredAppName || "";
|
|
103
|
-
|
|
124
|
+
async syncClasses(customSrc?: string): Promise<string | null> {
|
|
125
|
+
const appFolder = this.projectService.getInferredAppName();
|
|
104
126
|
const webappsPath = path.join(this.tomcatConfig.path, this.tomcatConfig.webapps);
|
|
105
127
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
.filter((dirent: any) => dirent.isDirectory() && !["ROOT", "manager", "host-manager", "docs"].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");
|
|
128
|
+
const sourceDir = customSrc || this.projectService.getClassesDir();
|
|
129
|
+
const destDir = customSrc ? path.join(webappsPath, appFolder) : path.join(webappsPath, appFolder, "WEB-INF", "classes");
|
|
123
130
|
|
|
124
|
-
if (!
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
131
|
+
if (!existsSync(sourceDir)) return null;
|
|
132
|
+
if (!appFolder || !existsSync(destDir)) {
|
|
133
|
+
if (customSrc && appFolder) {
|
|
134
|
+
mkdirSync(destDir, { recursive: true });
|
|
135
|
+
} else {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
129
138
|
}
|
|
130
139
|
|
|
131
|
-
const
|
|
132
|
-
if (!
|
|
133
|
-
const list = fs.
|
|
134
|
-
|
|
140
|
+
const fastSync = async (src: string, dest: string) => {
|
|
141
|
+
if (!existsSync(dest)) await fs.mkdir(dest, { recursive: true });
|
|
142
|
+
const list = await fs.readdir(src, { withFileTypes: true });
|
|
143
|
+
|
|
144
|
+
const tasks = list.map(async (item) => {
|
|
135
145
|
const s = path.join(src, item.name);
|
|
136
146
|
const d = path.join(dest, item.name);
|
|
147
|
+
|
|
137
148
|
if (item.isDirectory()) {
|
|
138
|
-
|
|
149
|
+
await fastSync(s, d);
|
|
139
150
|
} else {
|
|
140
|
-
|
|
141
|
-
|
|
151
|
+
const sStat = await fs.stat(s);
|
|
152
|
+
let shouldCopy = false;
|
|
153
|
+
|
|
154
|
+
if (!existsSync(d)) {
|
|
155
|
+
shouldCopy = true;
|
|
156
|
+
} else {
|
|
157
|
+
const dStat = await fs.stat(d);
|
|
158
|
+
if (sStat.mtimeMs > dStat.mtimeMs || dStat.size === 0) {
|
|
159
|
+
shouldCopy = true;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (shouldCopy) {
|
|
164
|
+
let retries = 3;
|
|
165
|
+
while (retries > 0) {
|
|
166
|
+
try {
|
|
167
|
+
await fs.copyFile(s, d);
|
|
168
|
+
const finalStat = await fs.stat(d);
|
|
169
|
+
if (item.name.endsWith(".jar") && finalStat.size === 0 && sStat.size > 0) {
|
|
170
|
+
throw new Error("Zero byte copy detected");
|
|
171
|
+
}
|
|
172
|
+
await fs.utimes(d, sStat.atime, sStat.mtime);
|
|
173
|
+
break;
|
|
174
|
+
} catch (e) {
|
|
175
|
+
retries--;
|
|
176
|
+
if (retries === 0) {
|
|
177
|
+
Logger.warn(`Failed to copy ${item.name} after retries.`);
|
|
178
|
+
} else {
|
|
179
|
+
await new Promise(r => setTimeout(r, 100));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
142
183
|
}
|
|
143
184
|
}
|
|
144
|
-
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
await Promise.all(tasks);
|
|
145
188
|
};
|
|
146
189
|
|
|
147
|
-
|
|
148
|
-
Logger.success("Classes swapped in running Tomcat");
|
|
190
|
+
await fastSync(sourceDir, destDir);
|
|
149
191
|
return appFolder;
|
|
150
192
|
}
|
|
151
193
|
|
|
152
|
-
async deployToWebapps(): Promise<string> {
|
|
153
|
-
const destDir = path.join(this.tomcatConfig.path, this.tomcatConfig.webapps);
|
|
154
|
-
|
|
194
|
+
async deployToWebapps(): Promise<{ path: string, finalName: string, isDirectory: boolean }> {
|
|
155
195
|
Logger.step("Searching for generated artifacts");
|
|
156
196
|
|
|
157
|
-
const
|
|
158
|
-
let results: string[] = [];
|
|
159
|
-
const list = readdirSync(dir, { withFileTypes: true });
|
|
160
|
-
for (const item of list) {
|
|
161
|
-
const res = path.resolve(dir, item.name);
|
|
162
|
-
if (item.isDirectory()) {
|
|
163
|
-
if (item.name === 'target' || item.name === 'build') {
|
|
164
|
-
results = results.concat(findWars(res));
|
|
165
|
-
} else if (!['node_modules', '.git', 'src', 'webapps', 'bin', 'conf', 'lib', 'logs', 'temp', 'work'].includes(item.name)) {
|
|
166
|
-
results = results.concat(findWars(res));
|
|
167
|
-
}
|
|
168
|
-
} else if (item.name.endsWith('.war')) {
|
|
169
|
-
results.push(res);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
return results;
|
|
173
|
-
};
|
|
174
|
-
|
|
175
|
-
const allWars = findWars(process.cwd())
|
|
176
|
-
.map(f => ({ path: f, name: path.basename(f), time: statSync(f).mtime.getTime() }))
|
|
177
|
-
.sort((a, b) => b.time - a.time);
|
|
178
|
-
|
|
179
|
-
if (allWars.length === 0) {
|
|
180
|
-
throw new Error('Nenhum arquivo .war encontrado! Verifique se o build realmente gerou um artefato.');
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const warFile = allWars[0];
|
|
184
|
-
const finalName = this.projectConfig.appName ? `${this.projectConfig.appName}.war` : warFile.name;
|
|
197
|
+
const artifact = this.projectService.getArtifact();
|
|
185
198
|
|
|
186
199
|
if (!this.projectConfig.quiet) {
|
|
187
|
-
Logger.info("Artifact",
|
|
188
|
-
if (this.projectConfig.appName) Logger.info("Deploy as",
|
|
189
|
-
} else {
|
|
190
|
-
const displayName = this.projectConfig.appName ? `${this.projectConfig.appName}` : warFile.name.replace(".war", "");
|
|
191
|
-
process.stdout.write(` ${"\x1b[90m"}➜${"\x1b[0m"} Deploying ${"\x1b[1m"}${displayName}${"\x1b[0m"}...\n`);
|
|
200
|
+
Logger.info(artifact.isDirectory ? "Exploded Dir" : "Artifact", path.basename(artifact.path));
|
|
201
|
+
if (this.projectConfig.appName) Logger.info("Deploy as", artifact.name);
|
|
192
202
|
}
|
|
193
203
|
|
|
194
|
-
|
|
195
|
-
this.inferredAppName = finalName.replace(".war", "");
|
|
196
|
-
return finalName;
|
|
204
|
+
return { path: artifact.path, finalName: artifact.name, isDirectory: artifact.isDirectory };
|
|
197
205
|
}
|
|
198
206
|
}
|
|
@@ -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
|
};
|
|
@@ -33,11 +50,9 @@ export class EndpointService {
|
|
|
33
50
|
const endpoints: ApiEndpoint[] = [];
|
|
34
51
|
const className = fileName.replace(".java", "");
|
|
35
52
|
|
|
36
|
-
// Find class-level mapping
|
|
37
53
|
const classPathMatch = content.match(/@(Path|RequestMapping)\s*\(\s*["'](.*?)["']\s*\)/);
|
|
38
54
|
const basePath = classPathMatch ? this.normalizePath(classPathMatch[2]) : "";
|
|
39
55
|
|
|
40
|
-
// Common mapping annotations
|
|
41
56
|
const methodRegex = /@(GET|POST|PUT|DELETE|PATCH|Path|RequestMapping|GetMapping|PostMapping|PutMapping|DeleteMapping|PatchMapping)\s*(\(\s*["'](.*?)["']\s*\))?\s*([\s\S]*?)\s+([a-zA-Z0-9_]+)\s*\(([\s\S]*?)\)/g;
|
|
42
57
|
|
|
43
58
|
let match;
|
|
@@ -98,7 +113,6 @@ export class EndpointService {
|
|
|
98
113
|
for (const p of individualParams) {
|
|
99
114
|
const trimmed = p.trim();
|
|
100
115
|
|
|
101
|
-
// Check for annotations
|
|
102
116
|
const pathParam = trimmed.match(/@PathParam\s*\(\s*["'](.*?)["']\s*\)\s*(\w+)\s+(\w+)/);
|
|
103
117
|
const pathVariable = trimmed.match(/@PathVariable\s*\(\s*["'](.*?)["']\s*\)\s*(\w+)\s+(\w+)/);
|
|
104
118
|
const queryParam = trimmed.match(/@QueryParam\s*\(\s*["'](.*?)["']\s*\)\s*(\w+)\s+(\w+)/);
|
|
@@ -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
|
+
}
|
|
@@ -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:
|
|
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,7 +42,7 @@ export class TomcatService {
|
|
|
35
42
|
}
|
|
36
43
|
}
|
|
37
44
|
|
|
38
|
-
clearWebapps(
|
|
45
|
+
clearWebapps() {
|
|
39
46
|
const fs = require("fs");
|
|
40
47
|
const path = require("path");
|
|
41
48
|
const webappsPath = path.join(this.activeConfig.path, "webapps");
|
|
@@ -70,13 +77,99 @@ export class TomcatService {
|
|
|
70
77
|
}
|
|
71
78
|
}
|
|
72
79
|
|
|
73
|
-
|
|
80
|
+
private async ensureHotswapAgent(): Promise<string | null> {
|
|
81
|
+
const fs = require("fs");
|
|
82
|
+
const path = require("path");
|
|
83
|
+
const os = require("os");
|
|
84
|
+
const agentDir = path.join(os.homedir(), ".xavva", "agents");
|
|
85
|
+
const agentPath = path.join(agentDir, "hotswap-agent-2.0.3.jar");
|
|
86
|
+
|
|
87
|
+
if (fs.existsSync(agentPath) && fs.statSync(agentPath).size > 1000) return agentPath;
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
if (!fs.existsSync(agentDir)) fs.mkdirSync(agentDir, { recursive: true });
|
|
91
|
+
|
|
92
|
+
Logger.step("Downloading HotswapAgent v2.0.3 (Global)...");
|
|
93
|
+
const url = "https://github.com/HotswapProjects/HotswapAgent/releases/download/RELEASE-2.0.3/hotswap-agent-2.0.3.jar";
|
|
94
|
+
const response = await fetch(url);
|
|
95
|
+
if (!response.ok) throw new Error(`Status: ${response.status}`);
|
|
96
|
+
|
|
97
|
+
const buffer = await response.arrayBuffer();
|
|
98
|
+
fs.writeFileSync(agentPath, Buffer.from(buffer));
|
|
99
|
+
Logger.success("HotswapAgent v2.0.3 installed globally!");
|
|
100
|
+
return agentPath;
|
|
101
|
+
} catch (e) {
|
|
102
|
+
Logger.warn("Falha ao baixar HotswapAgent. Usando hot swap padrão da JVM.");
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async start(config: AppConfig, isWatching: boolean = false) {
|
|
74
108
|
const binPath = `${this.activeConfig.path}\\bin\\catalina.bat`;
|
|
75
|
-
const args = debug ? ["jpda", "run"] : ["run"];
|
|
109
|
+
const args = (config.project.debug || isWatching) ? ["jpda", "run"] : ["run"];
|
|
76
110
|
|
|
77
111
|
const catalinaOpts = [process.env.CATALINA_OPTS || ""];
|
|
78
112
|
|
|
79
|
-
if (
|
|
113
|
+
if (config.project.debug || isWatching) {
|
|
114
|
+
const agentPath = await this.ensureHotswapAgent();
|
|
115
|
+
if (agentPath) {
|
|
116
|
+
catalinaOpts.push(`-javaagent:${agentPath}`);
|
|
117
|
+
|
|
118
|
+
let javaBin = "java";
|
|
119
|
+
if (process.env.JAVA_HOME) {
|
|
120
|
+
javaBin = require("path").join(process.env.JAVA_HOME, "bin", "java.exe");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const javaVer = Bun.spawnSync([javaBin, "-version"]);
|
|
124
|
+
const output = (javaVer.stderr.toString() + javaVer.stdout.toString()).toLowerCase();
|
|
125
|
+
|
|
126
|
+
if (output.includes("dcevm") || output.includes("jbr") || output.includes("trava")) {
|
|
127
|
+
catalinaOpts.push("-XX:+AllowEnhancedClassRedefinition");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
catalinaOpts.push(
|
|
131
|
+
"--add-opens=java.base/jdk.internal.loader=ALL-UNNAMED",
|
|
132
|
+
"--add-opens=java.base/java.lang=ALL-UNNAMED",
|
|
133
|
+
"--add-opens=java.base/java.io=ALL-UNNAMED",
|
|
134
|
+
"--add-opens=java.base/java.net=ALL-UNNAMED",
|
|
135
|
+
"--add-opens=java.base/java.util=ALL-UNNAMED",
|
|
136
|
+
"--add-opens=java.base/java.util.concurrent=ALL-UNNAMED",
|
|
137
|
+
"--add-opens=java.base/java.security=ALL-UNNAMED",
|
|
138
|
+
"--add-opens=java.base/jdk.internal.reflect=ALL-UNNAMED",
|
|
139
|
+
"--add-opens=java.base/java.lang.reflect=ALL-UNNAMED",
|
|
140
|
+
"--add-opens=java.base/sun.nio.ch=ALL-UNNAMED",
|
|
141
|
+
"--add-opens=java.base/java.util.jar=ALL-UNNAMED",
|
|
142
|
+
"--add-opens=java.desktop/java.beans=ALL-UNNAMED"
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const fs = require("fs");
|
|
146
|
+
const path = require("path");
|
|
147
|
+
const xavvaDir = path.join(process.cwd(), ".xavva");
|
|
148
|
+
if (!fs.existsSync(xavvaDir)) fs.mkdirSync(xavvaDir, { recursive: true });
|
|
149
|
+
|
|
150
|
+
const propsPath = path.join(xavvaDir, "hotswap-agent.properties");
|
|
151
|
+
const propsContent = `autoHotswap=true\nautoHotswap.delay=500\nwatchResources=false\nLOGGER=info`;
|
|
152
|
+
fs.writeFileSync(propsPath, propsContent);
|
|
153
|
+
|
|
154
|
+
catalinaOpts.push(`-Dhotswap-agent.properties.path=${propsPath}`);
|
|
155
|
+
|
|
156
|
+
if (this.projectService) {
|
|
157
|
+
const classPaths = this.projectService.findAllClassPaths();
|
|
158
|
+
if (classPaths.length > 0) {
|
|
159
|
+
catalinaOpts.push(`-Dhotswap.extraClasspath=${classPaths.join(",")}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Otimizações para JSP e Debug de JSP
|
|
166
|
+
catalinaOpts.push(
|
|
167
|
+
"-Dorg.apache.jasper.compiler.development=true",
|
|
168
|
+
"-Dorg.apache.jasper.compiler.disableSmap=false",
|
|
169
|
+
"-Dorg.apache.jasper.compiler.classdebuginfo=true"
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
if (config.project.skipScan) {
|
|
80
173
|
catalinaOpts.push(
|
|
81
174
|
"-Dtomcat.util.scan.StandardJarScanFilter.jarsToSkip=*.jar",
|
|
82
175
|
"-Dtomcat.util.scan.StandardJarScanFilter.jarsToScan=",
|
|
@@ -87,35 +180,44 @@ export class TomcatService {
|
|
|
87
180
|
);
|
|
88
181
|
}
|
|
89
182
|
|
|
90
|
-
const env:
|
|
183
|
+
const env: Record<string, string | undefined> = {
|
|
91
184
|
...process.env,
|
|
92
185
|
CATALINA_HOME: this.activeConfig.path,
|
|
93
186
|
CATALINA_OPTS: catalinaOpts.join(" ").trim()
|
|
94
187
|
};
|
|
95
188
|
|
|
96
|
-
if (
|
|
97
|
-
|
|
98
|
-
env.
|
|
189
|
+
if (process.env.JAVA_HOME) {
|
|
190
|
+
env.JAVA_HOME = process.env.JAVA_HOME;
|
|
191
|
+
env.JRE_HOME = process.env.JAVA_HOME;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (config.project.debug) {
|
|
195
|
+
Logger.debug(`Java Debugger habilitado na porta ${config.project.debugPort}`);
|
|
196
|
+
env.JPDA_ADDRESS = String(config.project.debugPort);
|
|
99
197
|
env.JPDA_TRANSPORT = "dt_socket";
|
|
100
198
|
}
|
|
101
199
|
|
|
102
|
-
if (cleanLogs || quiet) {
|
|
200
|
+
if ((config.project.cleanLogs || config.project.quiet) && !config.project.verbose) {
|
|
103
201
|
this.stopStartupSpinner = Logger.spinner("Starting Tomcat server");
|
|
104
202
|
}
|
|
105
203
|
|
|
106
204
|
this.currentProcess = Bun.spawn([binPath, ...args], {
|
|
107
205
|
stdout: "pipe",
|
|
108
206
|
stderr: "pipe",
|
|
109
|
-
env: env
|
|
207
|
+
env: env as any
|
|
110
208
|
});
|
|
111
209
|
|
|
112
210
|
this.pid = this.currentProcess.pid;
|
|
113
211
|
|
|
114
|
-
|
|
115
|
-
|
|
212
|
+
if (this.currentProcess.stdout) {
|
|
213
|
+
this.processLogStream(this.currentProcess.stdout as any, config.project.cleanLogs, config.project.quiet, config.project.verbose, config.tomcat.grep || "");
|
|
214
|
+
}
|
|
215
|
+
if (this.currentProcess.stderr) {
|
|
216
|
+
this.processLogStream(this.currentProcess.stderr as any, config.project.cleanLogs, config.project.quiet, config.project.verbose, config.tomcat.grep || "");
|
|
217
|
+
}
|
|
116
218
|
}
|
|
117
219
|
|
|
118
|
-
private async processLogStream(stream: ReadableStream, clean: boolean, quiet: boolean) {
|
|
220
|
+
private async processLogStream(stream: ReadableStream, clean: boolean, quiet: boolean, verbose: boolean, grep: string) {
|
|
119
221
|
const reader = stream.getReader();
|
|
120
222
|
const decoder = new TextDecoder();
|
|
121
223
|
|
|
@@ -141,6 +243,11 @@ export class TomcatService {
|
|
|
141
243
|
}
|
|
142
244
|
}
|
|
143
245
|
|
|
246
|
+
if (verbose) {
|
|
247
|
+
Logger.log(cleanLine);
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
|
|
144
251
|
if (clean) {
|
|
145
252
|
if (quiet && !Logger.isEssential(cleanLine)) {
|
|
146
253
|
if (Logger.isSystemNoise(cleanLine)) continue;
|
|
@@ -149,14 +256,14 @@ export class TomcatService {
|
|
|
149
256
|
continue;
|
|
150
257
|
}
|
|
151
258
|
|
|
152
|
-
if (
|
|
259
|
+
if (grep && !cleanLine.toLowerCase().includes(grep.toLowerCase())) {
|
|
153
260
|
if (!Logger.isEssential(cleanLine)) continue;
|
|
154
261
|
}
|
|
155
262
|
|
|
156
263
|
const summarized = Logger.summarize(cleanLine);
|
|
157
|
-
|
|
264
|
+
if (summarized) Logger.log(summarized);
|
|
158
265
|
} else {
|
|
159
|
-
|
|
266
|
+
Logger.log(cleanLine);
|
|
160
267
|
}
|
|
161
268
|
}
|
|
162
269
|
}
|