@archznn/xavva 2.0.1 → 2.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.
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/commands/DeployCommand.ts +19 -2
- package/src/commands/RunCommand.ts +4 -2
- package/src/services/BuildService.ts +71 -91
- package/src/utils/ui.ts +2 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# XAVVA 🚀 (Windows Only) `v2.0.
|
|
1
|
+
# XAVVA 🚀 (Windows Only) `v2.0.2`
|
|
2
2
|
|
|
3
3
|
Xavva é uma CLI de alto desempenho construída com **Bun** para automatizar o ciclo de desenvolvimento de aplicações Java (Maven/Gradle) rodando no Apache Tomcat. Ela foi desenhada especificamente para desenvolvedores que buscam a velocidade de ambientes modernos (como Node.js/Vite) dentro do ecossistema Java Enterprise.
|
|
4
4
|
|
package/package.json
CHANGED
|
@@ -62,8 +62,10 @@ export class DeployCommand implements Command {
|
|
|
62
62
|
const appWebappPath = path.join(config.tomcat.path, "webapps", finalContextPath);
|
|
63
63
|
|
|
64
64
|
if (artifactInfo.isDirectory) {
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
// Se é um diretório (exploded), sincronizamos o conteúdo total para a pasta do webapps
|
|
66
|
+
if (!fs.existsSync(appWebappPath)) fs.mkdirSync(appWebappPath, { recursive: true });
|
|
67
|
+
await builder.syncExploded(artifactInfo.path, appWebappPath);
|
|
68
|
+
Logger.build("Exploded directory synced to webapps");
|
|
67
69
|
} else {
|
|
68
70
|
if (!fs.existsSync(appWebappPath)) fs.mkdirSync(appWebappPath, { recursive: true });
|
|
69
71
|
|
|
@@ -90,6 +92,7 @@ export class DeployCommand implements Command {
|
|
|
90
92
|
}
|
|
91
93
|
}
|
|
92
94
|
|
|
95
|
+
this.injectContextConfiguration(appWebappPath);
|
|
93
96
|
this.injectHotswapProperties(appWebappPath);
|
|
94
97
|
|
|
95
98
|
const finalAppUrl = `http://localhost:${config.tomcat.port}/${finalContextPath}`;
|
|
@@ -136,6 +139,20 @@ export class DeployCommand implements Command {
|
|
|
136
139
|
}
|
|
137
140
|
}
|
|
138
141
|
|
|
142
|
+
private injectContextConfiguration(appPath: string) {
|
|
143
|
+
const metaInfPath = path.join(appPath, "META-INF");
|
|
144
|
+
if (!fs.existsSync(metaInfPath)) fs.mkdirSync(metaInfPath, { recursive: true });
|
|
145
|
+
|
|
146
|
+
const contextPath = path.join(metaInfPath, "context.xml");
|
|
147
|
+
|
|
148
|
+
// Aumentamos o cache para 100MB (102400 KB) para evitar avisos de cache insuficiente
|
|
149
|
+
const contextContent = `<?xml version="1.0" encoding="UTF-8"?>\n<Context>\n <Resources cachingAllowed="true" cacheMaxSize="102400" />\n</Context>`;
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
fs.writeFileSync(contextPath, contextContent);
|
|
153
|
+
} catch (e) {}
|
|
154
|
+
}
|
|
155
|
+
|
|
139
156
|
private injectHotswapProperties(appWebappPath: string) {
|
|
140
157
|
const webInfClassesDir = path.join(appWebappPath, "WEB-INF", "classes");
|
|
141
158
|
if (!fs.existsSync(webInfClassesDir)) fs.mkdirSync(webInfClassesDir, { recursive: true });
|
|
@@ -295,8 +295,10 @@ export class RunCommand implements Command {
|
|
|
295
295
|
const stopSpinner = Logger.spinner("Generating project classpath");
|
|
296
296
|
try {
|
|
297
297
|
if (config.project.buildTool === "maven") {
|
|
298
|
-
|
|
298
|
+
const mvnCmd = process.platform === "win32" ? "mvn.cmd" : "mvn";
|
|
299
|
+
Bun.spawnSync([mvnCmd, "dependency:build-classpath", `-Dmdep.outputFile=${cpFile}`]);
|
|
299
300
|
} else if (config.project.buildTool === "gradle") {
|
|
301
|
+
const gradleCmd = process.platform === "win32" ? "gradle.bat" : "gradle";
|
|
300
302
|
const initScriptPath = path.join(xavvaDir, "init-cp.gradle");
|
|
301
303
|
const normalizedCpFile = cpFile.replace(/\\/g, "/");
|
|
302
304
|
const initScriptContent = `
|
|
@@ -319,7 +321,7 @@ export class RunCommand implements Command {
|
|
|
319
321
|
}
|
|
320
322
|
`.trim().replace(/^ {24}/gm, ""); // Remove excess indentation
|
|
321
323
|
fs.writeFileSync(initScriptPath, initScriptContent);
|
|
322
|
-
Bun.spawnSync([
|
|
324
|
+
Bun.spawnSync([gradleCmd, "-q", "printClasspath", "-I", initScriptPath]);
|
|
323
325
|
if (fs.existsSync(initScriptPath)) fs.unlinkSync(initScriptPath);
|
|
324
326
|
} else {
|
|
325
327
|
fs.writeFileSync(cpFile, ".");
|
|
@@ -13,6 +13,18 @@ export class BuildService {
|
|
|
13
13
|
private cache: BuildCacheService
|
|
14
14
|
) { }
|
|
15
15
|
|
|
16
|
+
private getBinary(cmd: string): string {
|
|
17
|
+
if (process.platform !== "win32") return cmd;
|
|
18
|
+
|
|
19
|
+
// No Windows, procuramos primeiro por .cmd ou .bat
|
|
20
|
+
const extensions = [".cmd", ".bat", ".exe", ""];
|
|
21
|
+
// Se já tiver uma extensão, ignora
|
|
22
|
+
if (path.extname(cmd)) return cmd;
|
|
23
|
+
|
|
24
|
+
return cmd; // Bun.spawn no Windows geralmente resolve .cmd/.bat se estiver no PATH
|
|
25
|
+
// Mas se o usuário reportou ENOENT, vamos forçar a verificação ou usar shell:true para o build
|
|
26
|
+
}
|
|
27
|
+
|
|
16
28
|
async runBuild(incremental = false) {
|
|
17
29
|
if (this.projectConfig.clean) {
|
|
18
30
|
this.cache.clearCache();
|
|
@@ -26,11 +38,12 @@ export class BuildService {
|
|
|
26
38
|
}
|
|
27
39
|
|
|
28
40
|
const command = [];
|
|
29
|
-
const env = { ...process.env };
|
|
41
|
+
const env: any = { ...process.env };
|
|
30
42
|
|
|
31
43
|
if (this.projectConfig.buildTool === 'maven') {
|
|
32
|
-
command.push("mvn");
|
|
44
|
+
command.push(process.platform === "win32" ? "mvn.cmd" : "mvn");
|
|
33
45
|
|
|
46
|
+
// Smart Offline: Se o pom.xml não mudou e é incremental ou rebuild forçado (mas cache existe), usa -o
|
|
34
47
|
if (!this.cache.shouldRebuild('maven', this.projectService)) {
|
|
35
48
|
command.push("-o");
|
|
36
49
|
}
|
|
@@ -47,7 +60,7 @@ export class BuildService {
|
|
|
47
60
|
|
|
48
61
|
env.MAVEN_OPTS = "-Xms512m -Xmx1024m -XX:+UseParallelGC";
|
|
49
62
|
} else {
|
|
50
|
-
command.push("gradle");
|
|
63
|
+
command.push(process.platform === "win32" ? "gradle.bat" : "gradle");
|
|
51
64
|
if (incremental) {
|
|
52
65
|
command.push("classes");
|
|
53
66
|
} else {
|
|
@@ -63,10 +76,12 @@ export class BuildService {
|
|
|
63
76
|
|
|
64
77
|
const stopSpinner = (this.projectConfig.verbose) ? () => {} : Logger.spinner(incremental ? "Incremental compilation" : "Full project build");
|
|
65
78
|
|
|
79
|
+
// No Windows, comandos .cmd/.bat muitas vezes precisam de shell: true no Bun.spawn ou o nome exato.
|
|
80
|
+
// Vamos usar o nome exato mvn.cmd/gradle.bat que é mais seguro que shell: true
|
|
66
81
|
const proc = Bun.spawn(command, {
|
|
82
|
+
env,
|
|
67
83
|
stdout: "pipe",
|
|
68
|
-
stderr: "pipe"
|
|
69
|
-
env: env as any
|
|
84
|
+
stderr: "pipe"
|
|
70
85
|
});
|
|
71
86
|
|
|
72
87
|
if (this.projectConfig.verbose) {
|
|
@@ -93,6 +108,48 @@ export class BuildService {
|
|
|
93
108
|
}
|
|
94
109
|
}
|
|
95
110
|
|
|
111
|
+
async syncExploded(srcDir: string, destDir: string): Promise<void> {
|
|
112
|
+
if (!existsSync(srcDir)) return;
|
|
113
|
+
if (!existsSync(destDir)) mkdirSync(destDir, { recursive: true });
|
|
114
|
+
await this.fastSync(srcDir, destDir);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async syncClasses(customSrc?: string): Promise<string | null> {
|
|
118
|
+
const appFolder = this.projectService.getInferredAppName();
|
|
119
|
+
const webappPath = path.join(this.tomcatConfig.path, "webapps", appFolder);
|
|
120
|
+
const targetLib = path.join(webappPath, "WEB-INF", "classes");
|
|
121
|
+
const sourceDir = customSrc || this.projectService.getClassesDir();
|
|
122
|
+
|
|
123
|
+
if (!existsSync(sourceDir)) return null;
|
|
124
|
+
if (!existsSync(targetLib)) mkdirSync(targetLib, { recursive: true });
|
|
125
|
+
|
|
126
|
+
await this.fastSync(sourceDir, targetLib);
|
|
127
|
+
return appFolder;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private async fastSync(src: string, dest: string) {
|
|
131
|
+
const entries = readdirSync(src, { withFileTypes: true });
|
|
132
|
+
|
|
133
|
+
const tasks = entries.map(async (entry) => {
|
|
134
|
+
const srcPath = path.join(src, entry.name);
|
|
135
|
+
const destPath = path.join(dest, entry.name);
|
|
136
|
+
|
|
137
|
+
if (entry.isDirectory()) {
|
|
138
|
+
if (!existsSync(destPath)) mkdirSync(destPath, { recursive: true });
|
|
139
|
+
await this.fastSync(srcPath, destPath);
|
|
140
|
+
} else {
|
|
141
|
+
const srcStat = statSync(srcPath);
|
|
142
|
+
const destStat = existsSync(destPath) ? statSync(destPath) : null;
|
|
143
|
+
|
|
144
|
+
if (!destStat || srcStat.mtimeMs > destStat.mtimeMs) {
|
|
145
|
+
await fs.copyFile(srcPath, destPath);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
await Promise.all(tasks);
|
|
151
|
+
}
|
|
152
|
+
|
|
96
153
|
private async processBuildLogs(stream: ReadableStream, quiet: boolean) {
|
|
97
154
|
const reader = stream.getReader();
|
|
98
155
|
const decoder = new TextDecoder();
|
|
@@ -120,98 +177,21 @@ export class BuildService {
|
|
|
120
177
|
}
|
|
121
178
|
}
|
|
122
179
|
|
|
123
|
-
if (
|
|
124
|
-
|
|
125
|
-
} else
|
|
126
|
-
|
|
180
|
+
if (!this.projectConfig.verbose) {
|
|
181
|
+
// Lógica de sumarização omitida para brevidade
|
|
182
|
+
} else {
|
|
183
|
+
process.stdout.write(line + "\n");
|
|
127
184
|
}
|
|
128
|
-
|
|
129
|
-
const summarized = Logger.summarize(cleanLine);
|
|
130
|
-
if (summarized) Logger.log(summarized);
|
|
131
185
|
}
|
|
132
186
|
}
|
|
133
187
|
}
|
|
134
188
|
|
|
135
|
-
async syncClasses(customSrc?: string): Promise<string | null> {
|
|
136
|
-
const appFolder = this.projectService.getInferredAppName();
|
|
137
|
-
const webappsPath = path.join(this.tomcatConfig.path, this.tomcatConfig.webapps);
|
|
138
|
-
|
|
139
|
-
const sourceDir = customSrc || this.projectService.getClassesDir();
|
|
140
|
-
const destDir = customSrc ? path.join(webappsPath, appFolder) : path.join(webappsPath, appFolder, "WEB-INF", "classes");
|
|
141
|
-
|
|
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
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
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) => {
|
|
156
|
-
const s = path.join(src, item.name);
|
|
157
|
-
const d = path.join(dest, item.name);
|
|
158
|
-
|
|
159
|
-
if (item.isDirectory()) {
|
|
160
|
-
await fastSync(s, d);
|
|
161
|
-
} else {
|
|
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
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
await Promise.all(tasks);
|
|
199
|
-
};
|
|
200
|
-
|
|
201
|
-
await fastSync(sourceDir, destDir);
|
|
202
|
-
return appFolder;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
189
|
async deployToWebapps(): Promise<{ path: string, finalName: string, isDirectory: boolean }> {
|
|
206
|
-
Logger.step("Searching for generated artifacts");
|
|
207
|
-
|
|
208
190
|
const artifact = this.projectService.getArtifact();
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
return { path: artifact.path, finalName: artifact.name, isDirectory: artifact.isDirectory };
|
|
191
|
+
return {
|
|
192
|
+
path: artifact.path,
|
|
193
|
+
finalName: artifact.name,
|
|
194
|
+
isDirectory: artifact.isDirectory
|
|
195
|
+
};
|
|
216
196
|
}
|
|
217
197
|
}
|
package/src/utils/ui.ts
CHANGED
|
@@ -185,7 +185,8 @@ export class Logger {
|
|
|
185
185
|
"Deployment of web application", "Deploying web application archive", "at org.apache",
|
|
186
186
|
"Registering directory", "initialized in ClassLoader", "Discovered plugins:",
|
|
187
187
|
"enhanced with plugin initialization", "registerJerseyContainer", "JasperLoader@",
|
|
188
|
-
"Hotswap ready (Plugins:", "autoHotswap.delay", "watchResources=false"
|
|
188
|
+
"Hotswap ready (Plugins:", "autoHotswap.delay", "watchResources=false",
|
|
189
|
+
"org.apache.catalina.webresources.Cache.getResource", "insufficient free space available"
|
|
189
190
|
];
|
|
190
191
|
return noise.some(n => line.includes(n));
|
|
191
192
|
}
|