files = Files.list(currentDir)) {
+ boolean hasJavaFiles = files.anyMatch(path ->
+ path.toString().endsWith(".java") && !path.getFileName().toString().equals("package-info.java")
+ );
+
+ if (hasJavaFiles) {
+ String packageName = determinePackageName(sourceRoot, currentDir);
+ if (packageName != null) {
+ createPackageInfo(currentDir, packageName);
+ }
+ }
+ } catch (IOException exception) {
+ getLog().warn("Failed to process directory " + currentDir + ": " + exception.getMessage());
+ }
+ }
+
+ private String determinePackageName(Path sourceRoot, Path currentDir) {
+ Path relativePath = sourceRoot.relativize(currentDir);
+ String packageName = StreamSupport.stream(relativePath.spliterator(), false)
+ .map(Path::toString)
+ .collect(Collectors.joining("."));
+ return packageName.isEmpty() ? null : packageName;
+ }
+
+ private void createPackageInfo(Path dir, String packageName) throws IOException {
+ Path file = dir.resolve("package-info.java");
+
+ if (Files.exists(file)) {
+ return;
+ }
+
+ String content = String.format("%s%npackage %s;%n", ANNOTATION, packageName);
+ Files.writeString(file, content, StandardOpenOption.CREATE_NEW);
+ getLog().info("Created: " + file);
+ }
+}
diff --git a/peakup-tools/src/main/java/com/peakup/tools/generator/TypeScriptClientGenerator.java b/peakup-maven/src/main/java/com/peakup/maven/TypeScriptClientGeneratorMojo.java
similarity index 61%
rename from peakup-tools/src/main/java/com/peakup/tools/generator/TypeScriptClientGenerator.java
rename to peakup-maven/src/main/java/com/peakup/maven/TypeScriptClientGeneratorMojo.java
index a4edc3e..19ff552 100644
--- a/peakup-tools/src/main/java/com/peakup/tools/generator/TypeScriptClientGenerator.java
+++ b/peakup-maven/src/main/java/com/peakup/maven/TypeScriptClientGeneratorMojo.java
@@ -1,4 +1,4 @@
-package com.peakup.tools.generator;
+package com.peakup.maven;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.ws.rs.DELETE;
@@ -17,13 +17,13 @@ import java.lang.reflect.Field;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
-import java.lang.reflect.Parameter;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
+import java.net.URL;
+import java.net.URLClassLoader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
-import java.nio.file.Paths;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
@@ -37,94 +37,131 @@ import java.util.jar.JarFile;
import java.util.stream.Collectors;
import java.util.stream.Stream;
+import org.apache.maven.plugin.AbstractMojo;
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugins.annotations.LifecyclePhase;
+import org.apache.maven.plugins.annotations.Mojo;
+import org.apache.maven.plugins.annotations.Parameter;
+import org.apache.maven.plugins.annotations.ResolutionScope;
+import org.apache.maven.project.MavenProject;
+
/**
* Generates TypeScript REST client and entity types from peakup-backend REST resources.
*
- * Scans all classes extending {@code AbstractResource}, reads JAX-RS annotations and
- * {@code RestResponse} return types via reflection, then emits into
- * {@code peakup-frontend/src/lib/api/}:
+ * Scans all compiled classes extending {@code AbstractResource}, reads JAX-RS annotations
+ * and {@code RestResponse} return types via reflection, then writes into the frontend:
*
* - {@code httpClient.ts} — shared fetch helpers
- * - {@code {Entity}Do.ts} — one interface per domain entity, mirroring the Java class hierarchy
+ * - {@code {Entity}Do.ts} — one interface per domain entity
* - {@code {Name}.ts} — union type per enum
* - {@code {Resource}Client.ts} — one client file per JAX-RS resource
* - {@code hooks.ts} — TanStack Query hooks
*
- *
- * Run from the project root (backend must be compiled first):
- *
- * $MVN -f peakup-backend/pom.xml install -DskipTests -q
- * $MVN -f peakup-tools/pom.xml compile exec:java \
- * -Dexec.mainClass=com.peakup.tools.generator.TypeScriptClientGenerator
- *
*/
-public class TypeScriptClientGenerator {
+@Mojo(
+ name = "generate-typescript",
+ defaultPhase = LifecyclePhase.PROCESS_CLASSES,
+ requiresDependencyResolution = ResolutionScope.COMPILE,
+ threadSafe = true
+)
+public class TypeScriptClientGeneratorMojo extends AbstractMojo {
private static final String BACKEND_JAVA_PREFIX = "com.peakup";
- private static final String OUTPUT_DIR_API = "peakup-frontend/src/api";
- private static final String OUTPUT_DIR_TYPES = "peakup-frontend/src/types";
- // ── Main ─────────────────────────────────────────────────────────────────
+ @Parameter(defaultValue = "${project}", readonly = true, required = true)
+ private MavenProject project;
- public static void main(final String[] args) throws Exception {
- final Path outputDirApi = Paths.get(System.getProperty("user.dir")).resolve(OUTPUT_DIR_API);
- final Path outputDirTypes = Paths.get(System.getProperty("user.dir")).resolve(OUTPUT_DIR_TYPES);
- Files.createDirectories(outputDirApi);
- Files.createDirectories(outputDirTypes);
+ @Parameter(defaultValue = "${project.basedir}/../peakup-frontend/src/api", required = true)
+ private File outputDirApi;
- System.out.println("Scanning for REST resources...");
- final List> resourceClasses = findResourceClasses();
- System.out.println("Found: " + resourceClasses.stream()
- .map(Class::getSimpleName).collect(Collectors.joining(", ")));
+ @Parameter(defaultValue = "${project.basedir}/../peakup-frontend/src/types", required = true)
+ private File outputDirTypes;
- final List resources = resourceClasses.stream()
- .map(TypeScriptClientGenerator::extractResource)
- .filter(r -> !r.endpoints().isEmpty())
- .sorted(Comparator.comparing(ResourceInfo::resourceKey))
- .collect(Collectors.toList());
+ @Override
+ public void execute() throws MojoExecutionException {
+ final Path apiPath = outputDirApi.toPath();
+ final Path typesPath = outputDirTypes.toPath();
- System.out.println("Collecting entity types...");
- final Set> entityTypes = collectEntityTypes(resources);
- System.out.println("Types: " + entityTypes.stream()
- .map(Class::getSimpleName).collect(Collectors.joining(", ")));
-
- writeFile(outputDirApi.resolve("httpClient.ts"), generateHttpClient());
-
- for (final ResourceInfo resource : resources) {
- final String clientFileName = resource.resourceClass().getSimpleName().replace("Resource", "") + "Client.ts";
- writeFile(outputDirApi.resolve(clientFileName), generateResourceClient(resource, entityTypes));
+ try {
+ Files.createDirectories(apiPath);
+ Files.createDirectories(typesPath);
+ } catch (IOException exception) {
+ throw new MojoExecutionException("Failed to create output directories", exception);
}
- for (final Class> entityType : topoSort(entityTypes)) {
- final String doFileName = entityType.isEnum()
- ? entityType.getSimpleName() + ".ts"
- : entityType.getSimpleName() + "Do.ts";
- writeFile(outputDirTypes.resolve(doFileName), generateEntityDoFile(entityType, entityTypes));
+ final URLClassLoader classLoader = buildClassLoader();
+
+ try {
+ getLog().info("Scanning for REST resources...");
+ final List> resourceClasses = findResourceClasses(classLoader);
+ getLog().info("Found: " + resourceClasses.stream()
+ .map(Class::getSimpleName).collect(Collectors.joining(", ")));
+
+ final List resources = resourceClasses.stream()
+ .map(this::extractResource)
+ .filter(resource -> !resource.endpoints().isEmpty())
+ .sorted(Comparator.comparing(ResourceInfo::resourceKey))
+ .collect(Collectors.toList());
+
+ getLog().info("Collecting entity types...");
+ final Set> entityTypes = collectEntityTypes(resources);
+ getLog().info("Types: " + entityTypes.stream()
+ .map(Class::getSimpleName).collect(Collectors.joining(", ")));
+
+ writeFile(apiPath.resolve("httpClient.ts"), generateHttpClient());
+
+ for (final ResourceInfo resource : resources) {
+ final String clientFileName = resource.resourceClass().getSimpleName().replace("Resource", "") + "Client.ts";
+ writeFile(apiPath.resolve(clientFileName), generateResourceClient(resource, entityTypes));
+ }
+
+ for (final Class> entityType : topoSort(entityTypes)) {
+ final String fileName = entityType.isEnum()
+ ? entityType.getSimpleName() + ".ts"
+ : entityType.getSimpleName() + "Do.ts";
+ writeFile(typesPath.resolve(fileName), generateEntityDoFile(entityType, entityTypes));
+ }
+
+ writeFile(apiPath.resolve("hooks.ts"), generateHooks(resources, entityTypes));
+
+ getLog().info("Done → api: " + apiPath.toAbsolutePath() + ", types: " + typesPath.toAbsolutePath());
+ } catch (Exception exception) {
+ throw new MojoExecutionException("TypeScript generation failed", exception);
+ } finally {
+ try {
+ classLoader.close();
+ } catch (IOException exception) {
+ getLog().warn("Failed to close classloader: " + exception.getMessage());
+ }
}
-
- writeFile(outputDirApi.resolve("hooks.ts"), generateHooks(resources, entityTypes));
-
- System.out.println("Done → api: " + outputDirApi.toAbsolutePath() + ", types: " + outputDirTypes.toAbsolutePath());
}
- // ── Classpath Scanning ────────────────────────────────────────────────────
+ // ── Classpath ─────────────────────────────────────────────────────────────
- private static List> findResourceClasses() throws Exception {
- final Class> abstractResource = Class.forName("com.peakup.common.AbstractResource");
+ @SuppressWarnings("unchecked")
+ private URLClassLoader buildClassLoader() throws MojoExecutionException {
+ try {
+ final List elements = (List) project.getCompileClasspathElements();
+ final URL[] urls = new URL[elements.size()];
+ for (int i = 0; i < elements.size(); i++) {
+ urls[i] = new File(elements.get(i)).toURI().toURL();
+ }
+ return new URLClassLoader(urls, getClass().getClassLoader());
+ } catch (Exception exception) {
+ throw new MojoExecutionException("Failed to build classpath URLClassLoader", exception);
+ }
+ }
+
+ private List> findResourceClasses(final URLClassLoader classLoader) throws Exception {
+ final Class> abstractResource = classLoader.loadClass("com.peakup.common.AbstractResource");
final List> result = new ArrayList<>();
- // Collect classpath entries from the system property and the context classloader (exec:java
- // uses an isolated URLClassLoader that is not reflected in java.class.path)
- final Set classpathEntries = new LinkedHashSet<>();
- classpathEntries.addAll(Arrays.asList(System.getProperty("java.class.path").split(File.pathSeparator)));
- collectUrlsFromClassLoader(Thread.currentThread().getContextClassLoader(), classpathEntries);
-
- for (final String entry : classpathEntries) {
- final Path entryPath = Paths.get(entry);
- if (Files.isDirectory(entryPath)) {
- scanDirectory(entryPath, abstractResource, result);
- } else if (entry.endsWith(".jar") && entry.contains("peakup-backend")) {
- scanJar(entryPath, abstractResource, result);
+ for (final URL url : classLoader.getURLs()) {
+ final File file = new File(url.toURI());
+ if (file.isDirectory()) {
+ scanDirectory(file.toPath(), abstractResource, classLoader, result);
+ } else if (file.getName().endsWith(".jar") && file.getName().contains("peakup-backend")) {
+ scanJar(file.toPath(), abstractResource, classLoader, result);
}
}
@@ -132,34 +169,19 @@ public class TypeScriptClientGenerator {
return result;
}
- private static void collectUrlsFromClassLoader(final ClassLoader classLoader, final Set entries) {
- if (classLoader == null) return;
- try {
- final java.lang.reflect.Method getURLs = classLoader.getClass().getMethod("getURLs");
- final java.net.URL[] urls = (java.net.URL[]) getURLs.invoke(classLoader);
- for (final java.net.URL url : urls) {
- if ("file".equals(url.getProtocol())) {
- entries.add(new java.io.File(url.toURI()).getAbsolutePath());
- }
- }
- } catch (final Exception ignored) {
- }
- collectUrlsFromClassLoader(classLoader.getParent(), entries);
- }
-
- private static void scanDirectory(final Path root, final Class> abstractResource, final List> result)
- throws IOException {
+ private void scanDirectory(final Path root, final Class> abstractResource,
+ final URLClassLoader classLoader, final List> result) throws IOException {
try (final Stream walk = Files.walk(root)) {
- walk.filter(p -> p.toString().endsWith(".class"))
- .map(p -> toClassName(root.relativize(p).toString()))
+ walk.filter(path -> path.toString().endsWith(".class"))
+ .map(path -> toClassName(root.relativize(path).toString()))
.filter(name -> name.startsWith(BACKEND_JAVA_PREFIX)
&& !name.contains("$") && !name.endsWith("package-info"))
- .forEach(name -> tryLoadResource(name, abstractResource, result));
+ .forEach(name -> tryLoadResource(name, abstractResource, classLoader, result));
}
}
- private static void scanJar(final Path jarPath, final Class> abstractResource, final List> result)
- throws IOException {
+ private void scanJar(final Path jarPath, final Class> abstractResource,
+ final URLClassLoader classLoader, final List> result) throws IOException {
try (final JarFile jar = new JarFile(jarPath.toFile())) {
jar.entries().asIterator().forEachRemaining(entry -> {
final String name = entry.getName();
@@ -167,15 +189,15 @@ public class TypeScriptClientGenerator {
|| name.contains("$") || name.contains("package-info")) {
return;
}
- tryLoadResource(toClassName(name), abstractResource, result);
+ tryLoadResource(toClassName(name), abstractResource, classLoader, result);
});
}
}
- private static void tryLoadResource(final String className, final Class> abstractResource,
- final List> result) {
+ private void tryLoadResource(final String className, final Class> abstractResource,
+ final URLClassLoader classLoader, final List> result) {
try {
- final Class> cls = Class.forName(className);
+ final Class> cls = classLoader.loadClass(className);
if (abstractResource.isAssignableFrom(cls)
&& !cls.equals(abstractResource)
&& !Modifier.isAbstract(cls.getModifiers())) {
@@ -191,15 +213,15 @@ public class TypeScriptClientGenerator {
// ── Resource Extraction ───────────────────────────────────────────────────
- private static ResourceInfo extractResource(final Class> cls) {
- final jakarta.ws.rs.Path pathAnn = cls.getAnnotation(jakarta.ws.rs.Path.class);
- final String basePath = pathAnn != null ? pathAnn.value() : "";
+ private ResourceInfo extractResource(final Class> cls) {
+ final jakarta.ws.rs.Path pathAnnotation = cls.getAnnotation(jakarta.ws.rs.Path.class);
+ final String basePath = pathAnnotation != null ? pathAnnotation.value() : "";
final String resourceKey = deriveResourceKey(basePath, cls);
final List endpoints = Arrays.stream(cls.getDeclaredMethods())
- .filter(TypeScriptClientGenerator::hasHttpAnnotation)
+ .filter(TypeScriptClientGeneratorMojo::hasHttpAnnotation)
.sorted(Comparator.comparing(Method::getName))
- .map(m -> extractEndpoint(m, basePath))
+ .map(method -> extractEndpoint(method, basePath))
.collect(Collectors.toList());
return new ResourceInfo(cls, basePath, resourceKey, endpoints);
@@ -207,8 +229,8 @@ public class TypeScriptClientGenerator {
private static EndpointInfo extractEndpoint(final Method method, final String basePath) {
final String httpMethod = getHttpMethod(method);
- final jakarta.ws.rs.Path subPathAnn = method.getAnnotation(jakarta.ws.rs.Path.class);
- final String subPath = subPathAnn != null ? subPathAnn.value() : "";
+ final jakarta.ws.rs.Path subPathAnnotation = method.getAnnotation(jakarta.ws.rs.Path.class);
+ final String subPath = subPathAnnotation != null ? subPathAnnotation.value() : "";
final Type javaReturnType = unwrapRestResponse(method.getGenericReturnType());
final String tsReturnType = toTsType(javaReturnType);
@@ -217,25 +239,25 @@ public class TypeScriptClientGenerator {
final List queryParams = new ArrayList<>();
ParamInfo bodyParam = null;
- final Parameter[] params = method.getParameters();
+ final java.lang.reflect.Parameter[] params = method.getParameters();
final Annotation[][] allAnnotations = method.getParameterAnnotations();
final Type[] genericTypes = method.getGenericParameterTypes();
for (int i = 0; i < params.length; i++) {
- final Annotation[] ann = allAnnotations[i];
+ final Annotation[] annotations = allAnnotations[i];
final Type javaType = genericTypes[i];
- final boolean nullable = isNullable(ann);
- final DefaultValue defVal = getAnnotation(ann, DefaultValue.class);
- final PathParam pathParam = getAnnotation(ann, PathParam.class);
- final QueryParam queryParam = getAnnotation(ann, QueryParam.class);
+ final boolean nullable = isNullable(annotations);
+ final DefaultValue defaultValue = getAnnotation(annotations, DefaultValue.class);
+ final PathParam pathParam = getAnnotation(annotations, PathParam.class);
+ final QueryParam queryParam = getAnnotation(annotations, QueryParam.class);
if (pathParam != null) {
pathParams.add(new ParamInfo(pathParam.value(), javaType, toTsType(javaType), false, null));
} else if (queryParam != null) {
- final boolean optional = nullable || defVal != null;
+ final boolean optional = nullable || defaultValue != null;
queryParams.add(new ParamInfo(queryParam.value(), javaType, toTsType(javaType), optional,
- defVal != null ? defVal.value() : null));
- } else if (!isJaxRsSpecialParam(ann)) {
+ defaultValue != null ? defaultValue.value() : null));
+ } else if (!isJaxRsSpecialParam(annotations)) {
bodyParam = new ParamInfo("body", javaType, toTsType(javaType), false, null);
}
}
@@ -245,11 +267,11 @@ public class TypeScriptClientGenerator {
}
private static Type unwrapRestResponse(final Type type) {
- if (type instanceof final ParameterizedType pt) {
- final Type rawType = pt.getRawType();
+ if (type instanceof final ParameterizedType parameterizedType) {
+ final Type rawType = parameterizedType.getRawType();
if (rawType instanceof final Class> rawClass
&& rawClass.getSimpleName().equals("RestResponse")) {
- final Type[] args = pt.getActualTypeArguments();
+ final Type[] args = parameterizedType.getActualTypeArguments();
if (args.length == 1) return args[0];
}
}
@@ -299,7 +321,6 @@ public class TypeScriptClientGenerator {
}
}
- // Also include abstract com.peakup.* superclasses for DO hierarchy generation
for (final Class> cls : new ArrayList<>(visited)) {
if (!cls.isEnum()) {
addAbstractSuperclasses(cls, visited);
@@ -322,11 +343,11 @@ public class TypeScriptClientGenerator {
private static void seedFromType(final Type type, final Queue> queue, final Set> visited) {
if (type instanceof final Class> cls) {
if (isDomainType(cls) && !visited.contains(cls)) queue.add(cls);
- } else if (type instanceof final ParameterizedType pt) {
- seedFromType(pt.getRawType(), queue, visited);
- for (final Type arg : pt.getActualTypeArguments()) seedFromType(arg, queue, visited);
- } else if (type instanceof final GenericArrayType gat) {
- seedFromType(gat.getGenericComponentType(), queue, visited);
+ } else if (type instanceof final ParameterizedType parameterizedType) {
+ seedFromType(parameterizedType.getRawType(), queue, visited);
+ for (final Type arg : parameterizedType.getActualTypeArguments()) seedFromType(arg, queue, visited);
+ } else if (type instanceof final GenericArrayType genericArrayType) {
+ seedFromType(genericArrayType.getGenericComponentType(), queue, visited);
}
}
@@ -361,21 +382,21 @@ public class TypeScriptClientGenerator {
return "unknown";
}
- if (type instanceof final ParameterizedType pt) {
- final Type rawType = pt.getRawType();
+ if (type instanceof final ParameterizedType parameterizedType) {
+ final Type rawType = parameterizedType.getRawType();
if (rawType instanceof final Class> rawClass) {
final String rawName = rawClass.getName();
if (rawName.equals("java.util.List") || rawName.equals("java.util.Collection")
|| rawName.equals("java.util.Set")) {
- final Type[] args = pt.getActualTypeArguments();
+ final Type[] args = parameterizedType.getActualTypeArguments();
if (args.length == 1) return toTsType(args[0]) + "[]";
}
}
return "unknown";
}
- if (type instanceof final GenericArrayType gat) {
- return toTsType(gat.getGenericComponentType()) + "[]";
+ if (type instanceof final GenericArrayType genericArrayType) {
+ return toTsType(genericArrayType.getGenericComponentType()) + "[]";
}
return "unknown";
@@ -384,76 +405,67 @@ public class TypeScriptClientGenerator {
// ── httpClient.ts ─────────────────────────────────────────────────────────
private static String generateHttpClient() {
- final StringBuilder sb = new StringBuilder();
- sb.append("// GENERATED — do not edit manually.\n\n");
- sb.append("const BASE_URL = import.meta.env.VITE_API_URL ?? 'http://localhost:8111';\n\n");
-
- sb.append("export function buildQuery(params: Record): string {\n");
- sb.append(" const p = new URLSearchParams();\n");
- sb.append(" for (const [key, val] of Object.entries(params)) {\n");
- sb.append(" if (val !== undefined) p.set(key, String(val));\n");
- sb.append(" }\n");
- sb.append(" const query = p.toString();\n");
- sb.append(" return query ? `?${query}` : '';\n");
- sb.append("}\n\n");
-
- sb.append("export async function get(path: string): Promise {\n");
- sb.append(" const res = await fetch(`${BASE_URL}${path}`);\n");
- sb.append(" if (!res.ok) throw new Error(`HTTP ${res.status}: ${path}`);\n");
- sb.append(" return res.json() as Promise;\n");
- sb.append("}\n\n");
-
- sb.append("export async function post(path: string, body?: unknown): Promise {\n");
- sb.append(" const res = await fetch(`${BASE_URL}${path}`, {\n");
- sb.append(" method: 'POST',\n");
- sb.append(" headers: { 'Content-Type': 'application/json' },\n");
- sb.append(" body: body !== undefined ? JSON.stringify(body) : undefined,\n");
- sb.append(" });\n");
- sb.append(" if (!res.ok) throw new Error(`HTTP ${res.status}: ${path}`);\n");
- sb.append(" if (res.status === 204) return undefined as T;\n");
- sb.append(" return res.json() as Promise;\n");
- sb.append("}\n\n");
-
- sb.append("export async function put(path: string, body?: unknown): Promise {\n");
- sb.append(" const res = await fetch(`${BASE_URL}${path}`, {\n");
- sb.append(" method: 'PUT',\n");
- sb.append(" headers: { 'Content-Type': 'application/json' },\n");
- sb.append(" body: body !== undefined ? JSON.stringify(body) : undefined,\n");
- sb.append(" });\n");
- sb.append(" if (!res.ok) throw new Error(`HTTP ${res.status}: ${path}`);\n");
- sb.append(" if (res.status === 204) return undefined as T;\n");
- sb.append(" return res.json() as Promise;\n");
- sb.append("}\n\n");
-
- sb.append("export async function del(path: string): Promise {\n");
- sb.append(" const res = await fetch(`${BASE_URL}${path}`, { method: 'DELETE' });\n");
- sb.append(" if (!res.ok) throw new Error(`HTTP ${res.status}: ${path}`);\n");
- sb.append(" if (res.status === 204) return undefined as T;\n");
- sb.append(" return res.json() as Promise;\n");
- sb.append("}\n");
-
- return sb.toString();
+ return "// GENERATED — do not edit manually.\n\n"
+ + "const BASE_URL = import.meta.env.VITE_API_URL ?? 'http://localhost:8111';\n\n"
+ + "export function buildQuery(params: Record): string {\n"
+ + " const p = new URLSearchParams();\n"
+ + " for (const [key, val] of Object.entries(params)) {\n"
+ + " if (val !== undefined) p.set(key, String(val));\n"
+ + " }\n"
+ + " const query = p.toString();\n"
+ + " return query ? `?${query}` : '';\n"
+ + "}\n\n"
+ + "export async function get(path: string): Promise {\n"
+ + " const res = await fetch(`${BASE_URL}${path}`);\n"
+ + " if (!res.ok) throw new Error(`HTTP ${res.status}: ${path}`);\n"
+ + " return res.json() as Promise;\n"
+ + "}\n\n"
+ + "export async function post(path: string, body?: unknown): Promise {\n"
+ + " const res = await fetch(`${BASE_URL}${path}`, {\n"
+ + " method: 'POST',\n"
+ + " headers: { 'Content-Type': 'application/json' },\n"
+ + " body: body !== undefined ? JSON.stringify(body) : undefined,\n"
+ + " });\n"
+ + " if (!res.ok) throw new Error(`HTTP ${res.status}: ${path}`);\n"
+ + " if (res.status === 204) return undefined as T;\n"
+ + " return res.json() as Promise;\n"
+ + "}\n\n"
+ + "export async function put(path: string, body?: unknown): Promise {\n"
+ + " const res = await fetch(`${BASE_URL}${path}`, {\n"
+ + " method: 'PUT',\n"
+ + " headers: { 'Content-Type': 'application/json' },\n"
+ + " body: body !== undefined ? JSON.stringify(body) : undefined,\n"
+ + " });\n"
+ + " if (!res.ok) throw new Error(`HTTP ${res.status}: ${path}`);\n"
+ + " if (res.status === 204) return undefined as T;\n"
+ + " return res.json() as Promise;\n"
+ + "}\n\n"
+ + "export async function del(path: string): Promise {\n"
+ + " const res = await fetch(`${BASE_URL}${path}`, { method: 'DELETE' });\n"
+ + " if (!res.ok) throw new Error(`HTTP ${res.status}: ${path}`);\n"
+ + " if (res.status === 204) return undefined as T;\n"
+ + " return res.json() as Promise;\n"
+ + "}\n";
}
// ── {Entity}Do.ts / {Enum}.ts ─────────────────────────────────────────────
private static String generateEntityDoFile(final Class> cls, final Set> allTypes) {
- final StringBuilder sb = new StringBuilder();
- sb.append("// GENERATED — do not edit manually.\n\n");
+ final StringBuilder stringBuilder = new StringBuilder();
+ stringBuilder.append("// GENERATED — do not edit manually.\n\n");
if (cls.isEnum()) {
final Object[] constants = cls.getEnumConstants();
- sb.append("export type ").append(cls.getSimpleName()).append(" =\n");
+ stringBuilder.append("export type ").append(cls.getSimpleName()).append(" =\n");
for (int i = 0; i < constants.length; i++) {
- sb.append(" | '").append(((Enum>) constants[i]).name()).append("'");
- sb.append(i < constants.length - 1 ? "\n" : ";\n");
+ stringBuilder.append(" | '").append(((Enum>) constants[i]).name()).append("'");
+ stringBuilder.append(i < constants.length - 1 ? "\n" : ";\n");
}
- return sb.toString();
+ return stringBuilder.toString();
}
final Set importStatements = new LinkedHashSet<>();
- // Resolve parent for extends clause
final Class> superClass = cls.getSuperclass();
final String extendsClause;
if (superClass != null && superClass != Object.class && allTypes.contains(superClass)) {
@@ -464,10 +476,9 @@ public class TypeScriptClientGenerator {
extendsClause = "";
}
- // Own declared fields only — each parent gets its own DO file
final List ownFields = Arrays.stream(cls.getDeclaredFields())
- .filter(f -> !Modifier.isStatic(f.getModifiers()))
- .filter(f -> !isJsonIgnored(f))
+ .filter(field -> !Modifier.isStatic(field.getModifiers()))
+ .filter(field -> !isJsonIgnored(field))
.collect(Collectors.toList());
for (final Field field : ownFields) {
@@ -476,22 +487,22 @@ public class TypeScriptClientGenerator {
if (!importStatements.isEmpty()) {
for (final String importStatement : importStatements) {
- sb.append(importStatement).append("\n");
+ stringBuilder.append(importStatement).append("\n");
}
- sb.append("\n");
+ stringBuilder.append("\n");
}
- sb.append("export interface ").append(cls.getSimpleName()).append("Do").append(extendsClause).append(" {\n");
+ stringBuilder.append("export interface ").append(cls.getSimpleName()).append("Do").append(extendsClause).append(" {\n");
for (final Field field : ownFields) {
- final boolean nullable = isNullableField(field);
+ final boolean nullable = isNullable(field.getAnnotations());
final String tsType = toTsType(field.getGenericType());
- sb.append(" ").append(field.getName()).append(": ").append(tsType);
- if (nullable) sb.append(" | null");
- sb.append(";\n");
+ stringBuilder.append(" ").append(field.getName()).append(": ").append(tsType);
+ if (nullable) stringBuilder.append(" | null");
+ stringBuilder.append(";\n");
}
- sb.append("}\n");
+ stringBuilder.append("}\n");
- return sb.toString();
+ return stringBuilder.toString();
}
private static void collectImportsFromType(final Type type, final Set> allTypes,
@@ -501,13 +512,13 @@ public class TypeScriptClientGenerator {
final String tsName = cls.isEnum() ? cls.getSimpleName() : cls.getSimpleName() + "Do";
importStatements.add("import type { " + tsName + " } from '" + typeImportPrefix + tsName + "';");
}
- } else if (type instanceof final ParameterizedType pt) {
- collectImportsFromType(pt.getRawType(), allTypes, importStatements, typeImportPrefix);
- for (final Type arg : pt.getActualTypeArguments()) {
+ } else if (type instanceof final ParameterizedType parameterizedType) {
+ collectImportsFromType(parameterizedType.getRawType(), allTypes, importStatements, typeImportPrefix);
+ for (final Type arg : parameterizedType.getActualTypeArguments()) {
collectImportsFromType(arg, allTypes, importStatements, typeImportPrefix);
}
- } else if (type instanceof final GenericArrayType gat) {
- collectImportsFromType(gat.getGenericComponentType(), allTypes, importStatements, typeImportPrefix);
+ } else if (type instanceof final GenericArrayType genericArrayType) {
+ collectImportsFromType(genericArrayType.getGenericComponentType(), allTypes, importStatements, typeImportPrefix);
}
}
@@ -522,23 +533,23 @@ public class TypeScriptClientGenerator {
}
}
- final StringBuilder sb = new StringBuilder();
- sb.append("// GENERATED — do not edit manually.\n\n");
+ final StringBuilder stringBuilder = new StringBuilder();
+ stringBuilder.append("// GENERATED — do not edit manually.\n\n");
final Set httpHelpers = resolveHttpHelpers(resource.endpoints());
- sb.append("import { ").append(String.join(", ", httpHelpers)).append(" } from './httpClient';\n");
+ stringBuilder.append("import { ").append(String.join(", ", httpHelpers)).append(" } from './httpClient';\n");
for (final String typeImport : typeImportStatements) {
- sb.append(typeImport).append("\n");
+ stringBuilder.append(typeImport).append("\n");
}
- sb.append("\n");
+ stringBuilder.append("\n");
- sb.append("export const ").append(resource.resourceKey()).append(" = {\n");
+ stringBuilder.append("export const ").append(resource.resourceKey()).append(" = {\n");
for (final EndpointInfo endpoint : resource.endpoints()) {
- sb.append(generateClientFunction(endpoint, resource.basePath()));
+ stringBuilder.append(generateClientFunction(endpoint, resource.basePath()));
}
- sb.append("};\n");
+ stringBuilder.append("};\n");
- return sb.toString();
+ return stringBuilder.toString();
}
private static Set resolveHttpHelpers(final List endpoints) {
@@ -580,26 +591,24 @@ public class TypeScriptClientGenerator {
private static String generateClientFunction(final EndpointInfo endpoint, final String basePath) {
final List paramList = new ArrayList<>();
- for (final ParamInfo p : endpoint.pathParams()) {
- paramList.add(p.name() + ": " + p.tsType());
+ for (final ParamInfo paramInfo : endpoint.pathParams()) {
+ paramList.add(paramInfo.name() + ": " + paramInfo.tsType());
}
final List queryParams = endpoint.queryParams();
for (int i = 0; i < queryParams.size(); i++) {
- final ParamInfo p = queryParams.get(i);
- if (p.optional() && p.defaultValue() != null) {
- paramList.add(p.name() + " = " + tsDefaultValue(p.defaultValue(), p.tsType()));
- } else if (p.optional()) {
- // Use `name: type | undefined` instead of `name?: type` when a required param follows,
- // because TypeScript forbids required params after optional params (TS1016).
+ final ParamInfo paramInfo = queryParams.get(i);
+ if (paramInfo.optional() && paramInfo.defaultValue() != null) {
+ paramList.add(paramInfo.name() + " = " + tsDefaultValue(paramInfo.defaultValue(), paramInfo.tsType()));
+ } else if (paramInfo.optional()) {
final boolean requiredParamFollows = endpoint.bodyParam() != null
|| queryParams.subList(i + 1, queryParams.size()).stream().anyMatch(q -> !q.optional());
if (requiredParamFollows) {
- paramList.add(p.name() + ": " + p.tsType() + " | undefined");
+ paramList.add(paramInfo.name() + ": " + paramInfo.tsType() + " | undefined");
} else {
- paramList.add(p.name() + "?: " + p.tsType());
+ paramList.add(paramInfo.name() + "?: " + paramInfo.tsType());
}
} else {
- paramList.add(p.name() + ": " + p.tsType());
+ paramList.add(paramInfo.name() + ": " + paramInfo.tsType());
}
}
if (endpoint.bodyParam() != null) {
@@ -616,9 +625,9 @@ public class TypeScriptClientGenerator {
final String urlExpr;
if (hasQuery) {
- final String qpNames = endpoint.queryParams().stream()
+ final String queryParamNames = endpoint.queryParams().stream()
.map(ParamInfo::name).collect(Collectors.joining(", "));
- urlExpr = "`" + tsPath + "${buildQuery({ " + qpNames + " })}`";
+ urlExpr = "`" + tsPath + "${buildQuery({ " + queryParamNames + " })}`";
} else if (hasTemplate) {
urlExpr = "`" + tsPath + "`";
} else {
@@ -642,9 +651,6 @@ public class TypeScriptClientGenerator {
// ── hooks.ts ──────────────────────────────────────────────────────────────
private static String generateHooks(final List resources, final Set> allTypes) {
- // Only collect type names from mutation endpoints: query hooks infer their return type
- // from queryFn, so they never reference Do types by name in the generated code.
- // Including GET return types causes unused-import warnings (e.g. AthleteDo).
final Set neededTsTypeNames = new LinkedHashSet<>();
for (final ResourceInfo resource : resources) {
for (final EndpointInfo endpoint : resource.endpoints()) {
@@ -657,13 +663,13 @@ public class TypeScriptClientGenerator {
}
}
- final StringBuilder sb = new StringBuilder();
- sb.append("// GENERATED — do not edit manually.\n\n");
- sb.append("import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';\n");
+ final StringBuilder stringBuilder = new StringBuilder();
+ stringBuilder.append("// GENERATED — do not edit manually.\n\n");
+ stringBuilder.append("import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';\n");
for (final ResourceInfo resource : resources) {
final String clientFile = "./" + resource.resourceClass().getSimpleName().replace("Resource", "") + "Client";
- sb.append("import { ").append(resource.resourceKey()).append(" } from '").append(clientFile).append("';\n");
+ stringBuilder.append("import { ").append(resource.resourceKey()).append(" } from '").append(clientFile).append("';\n");
}
final Set typeImports = new LinkedHashSet<>();
@@ -671,45 +677,45 @@ public class TypeScriptClientGenerator {
collectImportsFromTsTypeName(tsTypeName, allTypes, typeImports, "../types/");
}
for (final String typeImport : typeImports) {
- sb.append(typeImport).append("\n");
+ stringBuilder.append(typeImport).append("\n");
}
- sb.append("\n");
+ stringBuilder.append("\n");
- sb.append("export const queryKeys = {\n");
+ stringBuilder.append("export const queryKeys = {\n");
for (final ResourceInfo resource : resources) {
- final List gets = resource.endpoints().stream()
- .filter(e -> e.httpMethod().equals("GET")).collect(Collectors.toList());
- if (gets.isEmpty()) continue;
- sb.append(" ").append(resource.resourceKey()).append(": {\n");
- for (final EndpointInfo endpoint : gets) {
+ final List getEndpoints = resource.endpoints().stream()
+ .filter(endpoint -> endpoint.httpMethod().equals("GET")).collect(Collectors.toList());
+ if (getEndpoints.isEmpty()) continue;
+ stringBuilder.append(" ").append(resource.resourceKey()).append(": {\n");
+ for (final EndpointInfo endpoint : getEndpoints) {
final List allParams = new ArrayList<>();
allParams.addAll(endpoint.pathParams());
allParams.addAll(endpoint.queryParams());
final String paramSig = allParams.stream()
- .map(p -> p.name() + (p.optional() ? "?" : "") + ": " + p.tsType())
+ .map(paramInfo -> paramInfo.name() + (paramInfo.optional() ? "?" : "") + ": " + paramInfo.tsType())
.collect(Collectors.joining(", "));
final String keyItems = allParams.stream().map(ParamInfo::name).collect(Collectors.joining(", "));
final String keyArray = "'" + resource.resourceKey() + "', '" + endpoint.functionName() + "'"
+ (keyItems.isEmpty() ? "" : ", " + keyItems);
- sb.append(" ").append(endpoint.functionName()).append(": (")
+ stringBuilder.append(" ").append(endpoint.functionName()).append(": (")
.append(paramSig).append(") => [").append(keyArray).append("] as const,\n");
}
- sb.append(" },\n");
+ stringBuilder.append(" },\n");
}
- sb.append("};\n\n");
+ stringBuilder.append("};\n\n");
for (final ResourceInfo resource : resources) {
- sb.append("// ── ").append(resource.resourceKey()).append(" ────────────────────────────────────\n\n");
+ stringBuilder.append("// ── ").append(resource.resourceKey()).append(" ────────────────────────────────────\n\n");
for (final EndpointInfo endpoint : resource.endpoints()) {
if (endpoint.httpMethod().equals("GET")) {
- sb.append(generateQueryHook(endpoint, resource));
+ stringBuilder.append(generateQueryHook(endpoint, resource));
} else {
- sb.append(generateMutationHook(endpoint, resource));
+ stringBuilder.append(generateMutationHook(endpoint, resource));
}
}
}
- return sb.toString();
+ return stringBuilder.toString();
}
private static String generateQueryHook(final EndpointInfo endpoint, final ResourceInfo resource) {
@@ -718,13 +724,13 @@ public class TypeScriptClientGenerator {
allParams.addAll(endpoint.pathParams());
allParams.addAll(endpoint.queryParams());
- final String paramSig = allParams.stream().map(p -> {
- if (p.optional() && p.defaultValue() != null) {
- return p.name() + " = " + tsDefaultValue(p.defaultValue(), p.tsType());
- } else if (p.optional()) {
- return p.name() + "?: " + p.tsType();
+ final String paramSig = allParams.stream().map(paramInfo -> {
+ if (paramInfo.optional() && paramInfo.defaultValue() != null) {
+ return paramInfo.name() + " = " + tsDefaultValue(paramInfo.defaultValue(), paramInfo.tsType());
+ } else if (paramInfo.optional()) {
+ return paramInfo.name() + "?: " + paramInfo.tsType();
}
- return p.name() + ": " + p.tsType();
+ return paramInfo.name() + ": " + paramInfo.tsType();
}).collect(Collectors.joining(", "));
final String queryKeyArgs = allParams.stream().map(ParamInfo::name).collect(Collectors.joining(", "));
@@ -764,7 +770,7 @@ public class TypeScriptClientGenerator {
destructure = single.name();
} else {
variablesType = "{ " + mutationParams.stream()
- .map(p -> p.name() + (p.optional() ? "?" : "") + ": " + p.tsType())
+ .map(paramInfo -> paramInfo.name() + (paramInfo.optional() ? "?" : "") + ": " + paramInfo.tsType())
.collect(Collectors.joining("; ")) + " }";
destructure = "{ " + mutationParams.stream()
.map(ParamInfo::name).collect(Collectors.joining(", ")) + " }";
@@ -818,24 +824,20 @@ public class TypeScriptClientGenerator {
}
private static boolean isJsonIgnored(final Field field) {
- final JsonIgnore ann = field.getAnnotation(JsonIgnore.class);
- return ann != null && ann.value();
+ final JsonIgnore annotation = field.getAnnotation(JsonIgnore.class);
+ return annotation != null && annotation.value();
}
private static boolean isNullable(final Annotation[] annotations) {
- for (final Annotation ann : annotations) {
- if (ann.annotationType().getSimpleName().equals("Nullable")) return true;
+ for (final Annotation annotation : annotations) {
+ if (annotation.annotationType().getSimpleName().equals("Nullable")) return true;
}
return false;
}
- private static boolean isNullableField(final Field field) {
- return isNullable(field.getAnnotations());
- }
-
private static boolean isJaxRsSpecialParam(final Annotation[] annotations) {
- for (final Annotation ann : annotations) {
- final String name = ann.annotationType().getSimpleName();
+ for (final Annotation annotation : annotations) {
+ final String name = annotation.annotationType().getSimpleName();
if (name.equals("Context") || name.equals("HeaderParam")
|| name.equals("CookieParam") || name.equals("FormParam")
|| name.equals("BeanParam") || name.equals("Suspended")) {
@@ -847,8 +849,8 @@ public class TypeScriptClientGenerator {
@SuppressWarnings("unchecked")
private static A getAnnotation(final Annotation[] annotations, final Class type) {
- for (final Annotation ann : annotations) {
- if (type.isInstance(ann)) return (A) ann;
+ for (final Annotation annotation : annotations) {
+ if (type.isInstance(annotation)) return (A) annotation;
}
return null;
}
@@ -863,8 +865,8 @@ public class TypeScriptClientGenerator {
private static String buildApiCallArgs(final EndpointInfo endpoint) {
final List args = new ArrayList<>();
- endpoint.pathParams().forEach(p -> args.add(p.name()));
- endpoint.queryParams().forEach(p -> args.add(p.name()));
+ endpoint.pathParams().forEach(paramInfo -> args.add(paramInfo.name()));
+ endpoint.queryParams().forEach(paramInfo -> args.add(paramInfo.name()));
if (endpoint.bodyParam() != null) args.add(endpoint.bodyParam().name());
return String.join(", ", args);
}
@@ -881,18 +883,18 @@ public class TypeScriptClientGenerator {
private static String kebabToCamel(final String kebab) {
final String[] parts = kebab.split("-");
- final StringBuilder sb = new StringBuilder(parts[0]);
+ final StringBuilder stringBuilder = new StringBuilder(parts[0]);
for (int i = 1; i < parts.length; i++) {
if (!parts[i].isEmpty()) {
- sb.append(Character.toUpperCase(parts[i].charAt(0))).append(parts[i].substring(1));
+ stringBuilder.append(Character.toUpperCase(parts[i].charAt(0))).append(parts[i].substring(1));
}
}
- return sb.toString();
+ return stringBuilder.toString();
}
- private static String capitalize(final String s) {
- if (s == null || s.isEmpty()) return s;
- return Character.toUpperCase(s.charAt(0)) + s.substring(1);
+ private static String capitalize(final String value) {
+ if (value == null || value.isEmpty()) return value;
+ return Character.toUpperCase(value.charAt(0)) + value.substring(1);
}
private static String pathToTsTemplate(final String path) {
@@ -919,7 +921,7 @@ public class TypeScriptClientGenerator {
try {
Double.parseDouble(javaDefault);
return javaDefault;
- } catch (final NumberFormatException e) {
+ } catch (final NumberFormatException exception) {
return "0";
}
}
@@ -929,9 +931,9 @@ public class TypeScriptClientGenerator {
return "'" + javaDefault + "'";
}
- private static void writeFile(final Path path, final String content) throws IOException {
+ private void writeFile(final Path path, final String content) throws IOException {
Files.writeString(path, content, StandardCharsets.UTF_8);
- System.out.println("Written: " + path.getFileName());
+ getLog().info("Written: " + path.getFileName());
}
// ── Records ───────────────────────────────────────────────────────────────
diff --git a/peakup-maven/target/classes/META-INF/maven/com.peakup/peakup-maven/plugin-help.xml b/peakup-maven/target/classes/META-INF/maven/com.peakup/peakup-maven/plugin-help.xml
new file mode 100644
index 0000000..6a63858
--- /dev/null
+++ b/peakup-maven/target/classes/META-INF/maven/com.peakup/peakup-maven/plugin-help.xml
@@ -0,0 +1,86 @@
+
+
+
+
+
+ peakup-maven
+
+ com.peakup
+ peakup-maven
+ 1.0-SNAPSHOT
+ peakup
+
+
+ generate-package-info
+ Generates package-info.java files with the @NonNull annotation for all packages that contain Java source files.
+ false
+ true
+ false
+ false
+ false
+ true
+ generate-sources
+ com.peakup.maven.PackageInfoGeneratorMojo
+ java
+ per-lookup
+ once-per-session
+ true
+
+
+ sourceDirectory
+ java.io.File
+ true
+ true
+
+
+
+
+
+
+
+
+ generate-typescript
+ Generates TypeScript REST client and entity types from peakup-backend REST resources.
+Scans all compiled classes extending AbstractResource, reads JAX-RS annotations and RestResponse<T> return types via reflection, then writes into the frontend:
+
+* httpClient.ts — shared fetch helpers
+* {EntityDo.ts} — one interface per domain entity
+* {Name.ts} — union type per enum
+* {ResourceClient.ts} — one client file per JAX-RS resource
+* hooks.ts — TanStack Query hooks
+ compile
+ false
+ true
+ false
+ false
+ false
+ true
+ process-classes
+ com.peakup.maven.TypeScriptClientGeneratorMojo
+ java
+ per-lookup
+ once-per-session
+ true
+
+
+ outputDirApi
+ java.io.File
+ true
+ true
+
+
+
+ outputDirTypes
+ java.io.File
+ true
+ true
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/peakup-maven/target/classes/META-INF/maven/plugin.xml b/peakup-maven/target/classes/META-INF/maven/plugin.xml
new file mode 100644
index 0000000..cb960bf
--- /dev/null
+++ b/peakup-maven/target/classes/META-INF/maven/plugin.xml
@@ -0,0 +1,112 @@
+
+
+
+
+
+ peakup-maven
+
+ com.peakup
+ peakup-maven
+ 1.0-SNAPSHOT
+ peakup
+ false
+ true
+ 21
+ 3.9.9
+
+
+ generate-package-info
+ Generates package-info.java files with the @NonNull annotation for all packages that contain Java source files.
+ false
+ true
+ false
+ false
+ false
+ true
+ generate-sources
+ com.peakup.maven.PackageInfoGeneratorMojo
+ java
+ per-lookup
+ once-per-session
+ true
+
+
+ sourceDirectory
+ java.io.File
+ true
+ true
+
+
+
+
+
+
+
+
+ generate-typescript
+ Generates TypeScript REST client and entity types from peakup-backend REST resources.
+Scans all compiled classes extending AbstractResource, reads JAX-RS annotations and RestResponse<T> return types via reflection, then writes into the frontend:
+
+* httpClient.ts — shared fetch helpers
+* {EntityDo.ts} — one interface per domain entity
+* {Name.ts} — union type per enum
+* {ResourceClient.ts} — one client file per JAX-RS resource
+* hooks.ts — TanStack Query hooks
+ compile
+ false
+ true
+ false
+ false
+ false
+ true
+ process-classes
+ com.peakup.maven.TypeScriptClientGeneratorMojo
+ java
+ per-lookup
+ once-per-session
+ true
+
+
+ outputDirApi
+ java.io.File
+ true
+ true
+
+
+
+ outputDirTypes
+ java.io.File
+ true
+ true
+
+
+
+ project
+ org.apache.maven.project.MavenProject
+ true
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+ jakarta.ws.rs
+ jakarta.ws.rs-api
+ jar
+ 3.1.0
+
+
+ com.fasterxml.jackson.core
+ jackson-annotations
+ jar
+ 2.18.3
+
+
+
\ No newline at end of file
diff --git a/peakup-maven/target/maven-archiver/pom.properties b/peakup-maven/target/maven-archiver/pom.properties
new file mode 100644
index 0000000..f2798b9
--- /dev/null
+++ b/peakup-maven/target/maven-archiver/pom.properties
@@ -0,0 +1,3 @@
+artifactId=peakup-maven
+groupId=com.peakup
+version=1.0-SNAPSHOT
diff --git a/peakup-maven/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/peakup-maven/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
new file mode 100644
index 0000000..4cb4284
--- /dev/null
+++ b/peakup-maven/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
@@ -0,0 +1,5 @@
+com/peakup/maven/TypeScriptClientGeneratorMojo.class
+com/peakup/maven/TypeScriptClientGeneratorMojo$ResourceInfo.class
+com/peakup/maven/PackageInfoGeneratorMojo.class
+com/peakup/maven/TypeScriptClientGeneratorMojo$ParamInfo.class
+com/peakup/maven/TypeScriptClientGeneratorMojo$EndpointInfo.class
diff --git a/peakup-maven/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/peakup-maven/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
new file mode 100644
index 0000000..4242a08
--- /dev/null
+++ b/peakup-maven/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
@@ -0,0 +1,2 @@
+/home/ingo/deve/peakUp/peakup-maven/src/main/java/com/peakup/maven/PackageInfoGeneratorMojo.java
+/home/ingo/deve/peakUp/peakup-maven/src/main/java/com/peakup/maven/TypeScriptClientGeneratorMojo.java
diff --git a/peakup-maven/target/plugin-enhanced.xml b/peakup-maven/target/plugin-enhanced.xml
new file mode 100644
index 0000000..7525b40
--- /dev/null
+++ b/peakup-maven/target/plugin-enhanced.xml
@@ -0,0 +1,113 @@
+
+
+
+
+
+ peakup-maven
+
+ com.peakup
+ peakup-maven
+ 1.0-SNAPSHOT
+ peakup
+ false
+ true
+ 21
+ 3.9.9
+
+
+ generate-package-info
+ Generates package-info.java files with the @NonNull annotation for all packages that contain Java source files.
+ false
+ true
+ false
+ false
+ false
+ true
+ generate-sources
+ com.peakup.maven.PackageInfoGeneratorMojo
+ java
+ per-lookup
+ once-per-session
+ true
+
+
+ sourceDirectory
+ java.io.File
+ true
+ true
+
+
+
+
+
+
+
+
+ generate-typescript
+ Generates TypeScript REST client and entity types from peakup-backend REST resources.
+<p>Scans all compiled classes extending <code>AbstractResource</code>, reads JAX-RS annotations and <code>RestResponse<T></code> return types via reflection, then writes into the frontend:</p>
+<ul>
+ <li><code>httpClient.ts</code> — shared fetch helpers</li>
+ <li><code>{Entity</code>Do.ts} — one interface per domain entity</li>
+ <li><code>{Name</code>.ts} — union type per enum</li>
+ <li><code>{Resource</code>Client.ts} — one client file per JAX-RS resource</li>
+ <li><code>hooks.ts</code> — TanStack Query hooks</li>
+</ul>
+ compile
+ false
+ true
+ false
+ false
+ false
+ true
+ process-classes
+ com.peakup.maven.TypeScriptClientGeneratorMojo
+ java
+ per-lookup
+ once-per-session
+ true
+
+
+ outputDirApi
+ java.io.File
+ true
+ true
+
+
+
+ outputDirTypes
+ java.io.File
+ true
+ true
+
+
+
+ project
+ org.apache.maven.project.MavenProject
+ true
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+ jakarta.ws.rs
+ jakarta.ws.rs-api
+ jar
+ 3.1.0
+
+
+ com.fasterxml.jackson.core
+ jackson-annotations
+ jar
+ 2.18.3
+
+
+
\ No newline at end of file
diff --git a/peakup-tools/src/main/java/com/peakup/tools/generator/PackageInfoGenerator.java b/peakup-tools/src/main/java/com/peakup/tools/generator/PackageInfoGenerator.java
deleted file mode 100644
index d50446d..0000000
--- a/peakup-tools/src/main/java/com/peakup/tools/generator/PackageInfoGenerator.java
+++ /dev/null
@@ -1,79 +0,0 @@
-package com.peakup.tools.generator;
-
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.nio.file.StandardOpenOption;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-import java.util.stream.StreamSupport;
-
-/**
- * Utility zur automatischen Generierung von package-info.java Dateien
- * in bestehenden Package-Strukturen.
- */
-public class PackageInfoGenerator {
-
- private static final String SRC_BASE = "peakup-backend/src/main/java"; // Pfad anpassen falls nötig
- private static final String PACKAGE_PREFIX = "com.peakup";
- private static final String ANNOTATION = "@com.peakup.common.annotation.NonNull";
-
- public static void main(String[] args) {
- Path projectRoot = Paths.get(System.getProperty("user.dir"));
- Path sourceRoot = projectRoot.resolve(SRC_BASE).toAbsolutePath().normalize();
-
- if (!Files.exists(sourceRoot)) {
- System.err.println("Source Root nicht gefunden: " + sourceRoot);
- return;
- }
-
- try (Stream stream = Files.walk(sourceRoot)) {
- stream.filter(Files::isDirectory)
- .forEach(dir -> processDirectory(sourceRoot, dir));
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
-
- private static void processDirectory(Path sourceRoot, Path currentDir) {
- try (Stream files = Files.list(currentDir)) {
- // Nur Verzeichnisse mit .java Dateien (außer package-info selbst)
- boolean hasJavaFiles = files.anyMatch(p ->
- p.toString().endsWith(".java") && !p.getFileName().toString().equals("package-info.java")
- );
-
- if (hasJavaFiles) {
- String packageName = determinePackageName(sourceRoot, currentDir);
- createPackageInfo(currentDir, packageName);
- }
- } catch (IOException e) {
- System.err.println("Fehler in " + currentDir + ": " + e.getMessage());
- }
- }
-
- private static String determinePackageName(Path sourceRoot, Path currentDir) {
- // Erzeugt den relativen Pfad von src/main/java zum aktuellen Ordner
- Path relativePath = sourceRoot.relativize(currentDir);
-
- // Verbindet die Pfad-Elemente mit Punkten (plattformunabhängig)
- String pkg = StreamSupport.stream(relativePath.spliterator(), false)
- .map(Path::toString)
- .collect(Collectors.joining("."));
-
- return pkg.isEmpty() ? null : pkg;
- }
-
- private static void createPackageInfo(Path dir, String packageName) throws IOException {
- Path file = dir.resolve("package-info.java");
-
- if (Files.exists(file)) {
- // System.out.println("Überspringe (existiert bereits): " + file);
- return;
- }
-
- String content = String.format("%s\npackage %s;", ANNOTATION, packageName);
- Files.writeString(file, content, StandardOpenOption.CREATE_NEW);
- System.out.println("Erstellt: " + file.toAbsolutePath());
- }
-}
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 9507b3b..bf33e66 100644
--- a/pom.xml
+++ b/pom.xml
@@ -10,6 +10,7 @@
pom
+ peakup-maven
peakup-backend
peakup-tools