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 }); + } + }); +});