diff --git a/.eslintrc.js b/.eslintrc.js
index 1916ca7..f7f5ce4 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -22,8 +22,22 @@ module.exports = {
parserOptions: {
ecmaVersion: 2018,
requireConfigFile: false,
- sourceType: "module"
+ sourceType: "module",
+ babelOptions: {
+ parserOpts: {
+ plugins: ["typescript"]
+ }
+ }
},
+ overrides: [
+ {
+ files: ["**/*.d.ts"],
+ parser: "@typescript-eslint/parser",
+ rules: {
+ "getter-return": "off"
+ }
+ }
+ ],
rules: {
"indent": "off" // Managed by prettier
}
diff --git a/.github/workflows/mega-linter.yml b/.github/workflows/mega-linter.yml
index 0767fde..e3b107b 100644
--- a/.github/workflows/mega-linter.yml
+++ b/.github/workflows/mega-linter.yml
@@ -30,7 +30,7 @@ jobs:
# Mega-Linter
- name: Mega-Linter
- uses: oxsecurity/megalinter/flavors/cupcake@v9
+ uses: oxsecurity/megalinter/flavors/javascript@v9
env:
# All available variables are described in documentation
# https://github.com/oxsecurity/megalinter#configuration
diff --git a/.mega-linter.yml b/.mega-linter.yml
index 33b3699..4ffeda0 100644
--- a/.mega-linter.yml
+++ b/.mega-linter.yml
@@ -1,4 +1,9 @@
+DISABLE_LINTERS:
+ - TYPESCRIPT_STANDARD
+ - TYPESCRIPT_PRETTIER
+TYPESCRIPT_STANDARD_DISABLE_ERRORS: true
DISABLE_ERRORS_LINTERS:
- ACTION_ACTIONLINT
- SPELL_LYCHEE
FILTER_REGEX_EXCLUDE: (docs\/github-dependents-info\.md|package-lock\.json)
+TYPESCRIPT_DEFAULT_STYLE: prettier
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3b3ee4d..1e29773 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,12 @@
## Unreleased
+## [4.3.3] 2025-02-03
+
+- Add types definition to make library compliant with typescript usage
+- Upgrade dependencies
+- CI: Use MegaLinter javascript flavor for better performance
+
## [4.3.2] 2025-01-24
- Upgrade dependencies
diff --git a/docs/typescript-usage.md b/docs/typescript-usage.md
new file mode 100644
index 0000000..74a299d
--- /dev/null
+++ b/docs/typescript-usage.md
@@ -0,0 +1,62 @@
+# TypeScript Support Example
+
+This example demonstrates how to use java-caller with TypeScript, including strict mode compatibility.
+
+## Setup
+
+```bash
+npm install java-caller typescript @types/node
+```
+
+## Usage
+
+### Basic TypeScript Example
+
+```typescript
+import { JavaCaller, JavaCallerOptions } from "java-caller";
+
+// Define options with full type safety
+const options: JavaCallerOptions = {
+ classPath: 'java/MyApp.jar',
+ mainClass: 'com.example.MyApp',
+ minimumJavaVersion: 11,
+ javaType: "jre"
+};
+
+// Create instance with autocomplete support
+const java = new JavaCaller(options);
+
+// Run with type-safe result
+const result = await java.run(['-arg1', 'value']);
+console.log(`Status: ${result.status}`);
+console.log(`Output: ${result.stdout}`);
+```
+
+### Strict Mode Compatibility
+
+The type definitions are fully compatible with TypeScript's strict mode:
+
+```typescript
+// tsconfig.json
+{
+ "compilerOptions": {
+ "strict": true,
+ "noImplicitAny": true,
+ "strictNullChecks": true
+ }
+}
+```
+
+### ES Module Import
+
+```typescript
+import { JavaCaller, JavaCallerCli } from "java-caller";
+```
+
+### CommonJS Require
+
+```typescript
+const { JavaCaller, JavaCallerCli } = require("java-caller");
+```
+
+Both import styles work with full TypeScript support!
diff --git a/lib/index.d.ts b/lib/index.d.ts
new file mode 100644
index 0000000..a007ab4
--- /dev/null
+++ b/lib/index.d.ts
@@ -0,0 +1,203 @@
+
+/* eslint-disable no-unused-vars */
+///
+
+/**
+ * Options for JavaCaller constructor
+ */
+export interface JavaCallerOptions {
+ /**
+ * Path to executable jar file
+ */
+ jar?: string;
+
+ /**
+ * If jar parameter is not set, classpath to use.
+ * Use : as separator (it will be converted if run on Windows), or use a string array.
+ */
+ classPath?: string | string[];
+
+ /**
+ * Set to true if classpaths should not be based on the rootPath
+ */
+ useAbsoluteClassPaths?: boolean;
+
+ /**
+ * If classPath set, main class to call
+ */
+ mainClass?: string;
+
+ /**
+ * Minimum java version to be used to call java command.
+ * If the java version found on machine is lower, java-caller will try to install and use the appropriate one
+ * @default 8 (11 on macOS)
+ */
+ minimumJavaVersion?: number;
+
+ /**
+ * Maximum java version to be used to call java command.
+ * If the java version found on machine is upper, java-caller will try to install and use the appropriate one
+ */
+ maximumJavaVersion?: number;
+
+ /**
+ * jre or jdk (if not defined and installation is required, jre will be installed)
+ */
+ javaType?: "jre" | "jdk";
+
+ /**
+ * If classPath elements are not relative to the current folder, you can define a root path.
+ * You may use __dirname if you classes / jars are in your module folder
+ * @default "." (current folder)
+ */
+ rootPath?: string;
+
+ /**
+ * You can force to use a defined java executable, instead of letting java-caller find/install one.
+ * Can also be defined with env var JAVA_CALLER_JAVA_EXECUTABLE
+ */
+ javaExecutable?: string;
+
+ /**
+ * Additional parameters for JVM that will be added in every JavaCaller instance runs
+ */
+ additionalJavaArgs?: string[];
+
+ /**
+ * Output mode: "none" or "console"
+ * @default "none"
+ */
+ output?: "none" | "console";
+}
+
+/**
+ * Options for JavaCaller run method
+ */
+export interface JavaCallerRunOptions {
+ /**
+ * If set to true, node will not wait for the java command to be completed.
+ * In that case, childJavaProcess property will be returned, but stdout and stderr may be empty
+ * @default false
+ */
+ detached?: boolean;
+
+ /**
+ * Adds control on spawn process stdout
+ * @default "utf8"
+ */
+ stdoutEncoding?: string;
+
+ /**
+ * If detached is true, number of milliseconds to wait to detect an error before exiting JavaCaller run
+ * @default 500
+ */
+ waitForErrorMs?: number;
+
+ /**
+ * You can override cwd of spawn called by JavaCaller runner
+ * @default process.cwd()
+ */
+ cwd?: string;
+
+ /**
+ * List of arguments for JVM only, not the JAR or the class
+ */
+ javaArgs?: string[];
+
+ /**
+ * No quoting or escaping of arguments is done on Windows. Ignored on Unix.
+ * This is set to true automatically when shell is specified and is CMD.
+ * @default true
+ */
+ windowsVerbatimArguments?: boolean;
+
+ /**
+ * If windowless is true, JavaCaller calls javaw instead of java to not create any windows,
+ * useful when using detached on Windows. Ignored on Unix.
+ * @default false
+ */
+ windowless?: boolean;
+}
+
+/**
+ * Result returned by JavaCaller run method
+ */
+export interface JavaCallerResult {
+ /**
+ * Exit status code of the java command
+ */
+ status: number | null;
+
+ /**
+ * Standard output of the java command
+ */
+ stdout: string;
+
+ /**
+ * Standard error output of the java command
+ */
+ stderr: string;
+
+ /**
+ * Child process object (useful when detached is true)
+ */
+ childJavaProcess?: import('child_process').ChildProcess;
+}
+
+/**
+ * JavaCaller class for calling Java commands from Node.js
+ */
+export class JavaCaller {
+ minimumJavaVersion: number;
+ maximumJavaVersion?: number;
+ javaType?: "jre" | "jdk";
+ rootPath: string;
+ jar?: string;
+ classPath: string | string[];
+ useAbsoluteClassPaths: boolean;
+ mainClass?: string;
+ output: string;
+ status: number | null;
+ javaSupportDir?: string;
+ javaExecutable: string;
+ javaExecutableWindowless: string;
+ additionalJavaArgs: string[];
+ commandJavaArgs: string[];
+ javaHome?: string;
+ javaBin?: string;
+ javaExecutableFromNodeJavaCaller?: string | null;
+ prevPath?: string;
+ prevJavaHome?: string;
+
+ /**
+ * Creates a JavaCaller instance
+ * @param opts - Run options
+ */
+ constructor(opts: JavaCallerOptions);
+
+ /**
+ * Runs java command of a JavaCaller instance
+ * @param userArguments - Java command line arguments
+ * @param runOptions - Run options
+ * @returns Command result (status, stdout, stderr, childJavaProcess)
+ */
+ run(userArguments?: string[], runOptions?: JavaCallerRunOptions): Promise;
+}
+
+/**
+ * JavaCallerCli class for using java-caller from command line
+ */
+export class JavaCallerCli {
+ javaCallerOptions: JavaCallerOptions;
+
+ /**
+ * Creates a JavaCallerCli instance
+ * @param baseDir - Base directory containing java-caller-config.json
+ */
+ constructor(baseDir: string);
+
+ /**
+ * Process command line arguments and run java command
+ */
+ process(): Promise;
+}
diff --git a/package-lock.json b/package-lock.json
index df5b8e9..e1a1078 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,10 +16,12 @@
},
"devDependencies": {
"@babel/eslint-parser": "^7.22.15",
+ "@types/node": "^25.2.0",
"eslint": "^9.0.0",
"mocha": "^11.0.0",
"nyc": "^17.0.0",
"prettier": "^3.1.0",
+ "typescript": "^5.9.3",
"which": "^6.0.0"
},
"engines": {
@@ -744,6 +746,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/node": {
+ "version": "25.2.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.0.tgz",
+ "integrity": "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.16.0"
+ }
+ },
"node_modules/acorn": {
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
@@ -3515,9 +3527,9 @@
}
},
"node_modules/tar": {
- "version": "7.5.6",
- "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.6.tgz",
- "integrity": "sha512-xqUeu2JAIJpXyvskvU3uvQW8PAmHrtXp2KDuMJwQqW8Sqq0CaZBAQ+dKS3RBXVhU4wC5NjAdKrmh84241gO9cA==",
+ "version": "7.5.7",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz",
+ "integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==",
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/fs-minipass": "^4.0.0",
@@ -3615,6 +3627,27 @@
"is-typedarray": "^1.0.0"
}
},
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "7.16.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
+ "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/universalify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
diff --git a/package.json b/package.json
index 833a254..a749ab1 100644
--- a/package.json
+++ b/package.json
@@ -3,6 +3,7 @@
"version": "4.3.2",
"description": "Library to easily call java from node sources. Automatically installs java if not present",
"main": "./lib/index.js",
+ "types": "./lib/index.d.ts",
"files": [
"lib/"
],
@@ -26,6 +27,7 @@
"node",
"npm",
"javascript",
+ "typescript",
"class"
],
"author": "Nicolas Vuillamy",
@@ -42,10 +44,12 @@
},
"devDependencies": {
"@babel/eslint-parser": "^7.22.15",
+ "@types/node": "^25.2.0",
"eslint": "^9.0.0",
"mocha": "^11.0.0",
"nyc": "^17.0.0",
"prettier": "^3.1.0",
+ "typescript": "^5.9.3",
"which": "^6.0.0"
},
"engines": {
diff --git a/test/typescript-usage.test.js b/test/typescript-usage.test.js
new file mode 100644
index 0000000..5974574
--- /dev/null
+++ b/test/typescript-usage.test.js
@@ -0,0 +1,84 @@
+#! /usr/bin/env node
+"use strict";
+
+const fs = require("fs");
+const os = require("os");
+const path = require("path");
+const ts = require("typescript");
+const { beforeEachTestCase } = require("./helpers/common");
+
+// This test ensures the published TypeScript declarations remain valid for a consumer project.
+describe("TypeScript usage", () => {
+ beforeEach(beforeEachTestCase);
+
+ it("type-checks a sample consumer", () => {
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "java-caller-ts-"));
+ const sourcePath = path.join(tempDir, "example.ts");
+
+ const config = {
+ compilerOptions: {
+ target: "ES2019",
+ module: "CommonJS",
+ moduleResolution: "Node",
+ strict: true,
+ esModuleInterop: true,
+ allowSyntheticDefaultImports: true,
+ baseUrl: process.cwd(),
+ paths: {
+ "java-caller": ["lib/index.d.ts"]
+ },
+ types: ["node"]
+ },
+ include: ["example.ts"]
+ };
+
+ fs.writeFileSync(sourcePath, `import { JavaCaller, JavaCallerOptions, JavaCallerResult } from "java-caller";
+
+const options: JavaCallerOptions = {
+ classPath: "test/java/dist",
+ mainClass: "com.nvuillam.javacaller.JavaCallerTester",
+ minimumJavaVersion: 8,
+ javaType: "jre"
+};
+
+async function runExample(): Promise {
+ const java = new JavaCaller(options);
+ const result = await java.run(["--sleep"], { detached: true, stdoutEncoding: "utf8" });
+ if (result.childJavaProcess) {
+ result.childJavaProcess.kill("SIGINT");
+ }
+ return result;
+}
+
+async function run(): Promise {
+ const result = await runExample();
+ const statusText: string = result.status === 0 ? "ok" : "ko";
+ console.log(statusText, result.stdout, result.stderr);
+}
+
+run();
+`);
+
+ try {
+ const parsed = ts.parseJsonConfigFileContent(config, ts.sys, tempDir);
+ const program = ts.createProgram({ rootNames: parsed.fileNames, options: parsed.options });
+ const diagnostics = ts.getPreEmitDiagnostics(program);
+
+ if (diagnostics.length) {
+ const formatted = diagnostics
+ .map(diag => {
+ if (diag.file && typeof diag.start === "number") {
+ const { line, character } = diag.file.getLineAndCharacterOfPosition(diag.start);
+ const message = ts.flattenDiagnosticMessageText(diag.messageText, "\n");
+ return `${diag.file.fileName} (${line + 1},${character + 1}): ${message}`;
+ }
+ return ts.flattenDiagnosticMessageText(diag.messageText, "\n");
+ })
+ .join("\n");
+ throw new Error(`TypeScript compilation failed:\n${formatted}`);
+ }
+ } finally {
+ fs.rmSync(tempDir, { recursive: true, force: true });
+ }
+ });
+});