From 610bb1b03af0f76e3c39d78430bcbd0bb03488d7 Mon Sep 17 00:00:00 2001 From: "L. Sousa" Date: Tue, 14 Jul 2026 16:52:42 +0100 Subject: [PATCH 1/9] Upgrade Lara-JS to TypeScript 7 --- Lara-JS/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lara-JS/package.json b/Lara-JS/package.json index 29c94784..0ee083f8 100644 --- a/Lara-JS/package.json +++ b/Lara-JS/package.json @@ -98,7 +98,7 @@ "jest": "^30.2.0", "node-notifier": "^10.0.1", "ts-jest": "^29.4.6", - "typescript": "^6.0.3", + "typescript": "^7.0.2", "typescript-eslint": "^8.52.0" } } From 023e76b7421c4f408cc21e4b2c41d73c6e201f14 Mon Sep 17 00:00:00 2001 From: "L. Sousa" Date: Tue, 14 Jul 2026 17:33:00 +0100 Subject: [PATCH 2/9] test: release Java bridge handle after Jest --- Lara-JS/jest/jestGlobalTeardown.ts | 2 ++ Lara-JS/package.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Lara-JS/jest/jestGlobalTeardown.ts b/Lara-JS/jest/jestGlobalTeardown.ts index 3891ecf2..09bd5f97 100644 --- a/Lara-JS/jest/jestGlobalTeardown.ts +++ b/Lara-JS/jest/jestGlobalTeardown.ts @@ -1,5 +1,7 @@ import { Weaver } from "@specs-feup/lara/code/Weaver.ts"; +import java from "java"; export default function () { Weaver.shutdown(); + java.stop(); } diff --git a/Lara-JS/package.json b/Lara-JS/package.json index 0ee083f8..ff33128d 100644 --- a/Lara-JS/package.json +++ b/Lara-JS/package.json @@ -47,7 +47,7 @@ "build": "tsc", "build:watch": "npm run build -- --watch", "lint": "eslint --flag unstable_native_nodejs_ts_config .", - "test": "cross-env NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" jest --detectOpenHandles --forceExit", + "test": "cross-env NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" jest --runInBand", "test:api": "npm run test -- api", "test:code": "npm run test -- code", "test:cov": "npm run test -- --coverage", From 3d5292267d6ed3d87a356892d4edf870ece2e3dd Mon Sep 17 00:00:00 2001 From: "L. Sousa" Date: Tue, 14 Jul 2026 17:49:47 +0100 Subject: [PATCH 3/9] migrate tests to vitest Replace Jest and ts-jest with Vitest's native Node TypeScript runner, add shared Vitest setup helpers, and configure Vitest-aware linting and Prettier. --- .../api/LegacyIntegrationTests - API.test.ts | 6 +- .../LegacyIntegrationTests - JsEngine.test.ts | 4 +- .../LegacyIntegrationTests - Weaver.test.ts | 4 +- Lara-JS/api/jest.config.ts | 29 -------- Lara-JS/api/vitest.config.ts | 19 +++++ Lara-JS/code/ChildProcessHandling.test.ts | 12 +-- Lara-JS/code/jest.config.ts | 9 --- Lara-JS/code/vitest.config.ts | 18 +++++ Lara-JS/eslint.config.ts | 73 +++++++++---------- Lara-JS/jest.config.ts | 18 ----- Lara-JS/jest/jestEnvironment.ts | 28 ------- Lara-JS/jest/jestGlobalSetup.ts | 36 --------- Lara-JS/jest/jestGlobalTeardown.ts | 7 -- Lara-JS/jest/setupFiles/importSideEffects.ts | 14 ---- Lara-JS/jest/setupFiles/sharedJavaModule.ts | 7 -- Lara-JS/package.json | 35 +++++---- Lara-JS/prettier.config.ts | 5 ++ Lara-JS/tsconfig.json | 2 +- Lara-JS/vitest.config.ts | 13 ++++ .../{jest => vitest}/WeaverLegacyTester.ts | 18 ++--- Lara-JS/vitest/setupDefaultWeaver.ts | 14 ++++ Lara-JS/vitest/setupWeaver.ts | 33 +++++++++ .../weaverTestHelpers.ts} | 2 +- 23 files changed, 181 insertions(+), 225 deletions(-) delete mode 100644 Lara-JS/api/jest.config.ts create mode 100644 Lara-JS/api/vitest.config.ts delete mode 100644 Lara-JS/code/jest.config.ts create mode 100644 Lara-JS/code/vitest.config.ts delete mode 100644 Lara-JS/jest.config.ts delete mode 100644 Lara-JS/jest/jestEnvironment.ts delete mode 100644 Lara-JS/jest/jestGlobalSetup.ts delete mode 100644 Lara-JS/jest/jestGlobalTeardown.ts delete mode 100644 Lara-JS/jest/setupFiles/importSideEffects.ts delete mode 100644 Lara-JS/jest/setupFiles/sharedJavaModule.ts create mode 100644 Lara-JS/prettier.config.ts create mode 100644 Lara-JS/vitest.config.ts rename Lara-JS/{jest => vitest}/WeaverLegacyTester.ts (94%) create mode 100644 Lara-JS/vitest/setupDefaultWeaver.ts create mode 100644 Lara-JS/vitest/setupWeaver.ts rename Lara-JS/{jest/jestHelpers.ts => vitest/weaverTestHelpers.ts} (96%) diff --git a/Lara-JS/api/LegacyIntegrationTests - API.test.ts b/Lara-JS/api/LegacyIntegrationTests - API.test.ts index d51fe65b..fd7f7197 100644 --- a/Lara-JS/api/LegacyIntegrationTests - API.test.ts +++ b/Lara-JS/api/LegacyIntegrationTests - API.test.ts @@ -1,7 +1,7 @@ -import { WeaverLegacyTester } from "../jest/WeaverLegacyTester.ts"; +import { WeaverLegacyTester } from "../vitest/WeaverLegacyTester.ts"; import path from "path"; -/* eslint-disable jest/expect-expect */ +/* eslint-disable vitest/expect-expect */ describe("ApiTest", () => { function newTester() { return new WeaverLegacyTester( @@ -29,7 +29,7 @@ describe("ApiTest", () => { await newTester().test("LaraCoreTest.js"); }); - it.failing("LocalFolder", async () => { + it.fails("LocalFolder", async () => { await newTester().test("LocalFolderTest.js"); }); diff --git a/Lara-JS/api/LegacyIntegrationTests - JsEngine.test.ts b/Lara-JS/api/LegacyIntegrationTests - JsEngine.test.ts index eb1c1c56..46b0576d 100644 --- a/Lara-JS/api/LegacyIntegrationTests - JsEngine.test.ts +++ b/Lara-JS/api/LegacyIntegrationTests - JsEngine.test.ts @@ -1,7 +1,7 @@ -import { WeaverLegacyTester } from "../jest/WeaverLegacyTester.ts"; +import { WeaverLegacyTester } from "../vitest/WeaverLegacyTester.ts"; import path from "path"; -/* eslint-disable jest/expect-expect */ +/* eslint-disable vitest/expect-expect */ describe("JsEngine", () => { function newTester() { return new WeaverLegacyTester( diff --git a/Lara-JS/api/LegacyIntegrationTests - Weaver.test.ts b/Lara-JS/api/LegacyIntegrationTests - Weaver.test.ts index d6810eab..b83feee0 100644 --- a/Lara-JS/api/LegacyIntegrationTests - Weaver.test.ts +++ b/Lara-JS/api/LegacyIntegrationTests - Weaver.test.ts @@ -1,7 +1,7 @@ -import { WeaverLegacyTester } from "../jest/WeaverLegacyTester.ts"; +import { WeaverLegacyTester } from "../vitest/WeaverLegacyTester.ts"; import path from "path"; -/* eslint-disable jest/expect-expect */ +/* eslint-disable vitest/expect-expect */ describe("Weaver", () => { function newTester() { return new WeaverLegacyTester( diff --git a/Lara-JS/api/jest.config.ts b/Lara-JS/api/jest.config.ts deleted file mode 100644 index 11314bac..00000000 --- a/Lara-JS/api/jest.config.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { defineConfig } from "jest"; -import path from "path"; -import { fileURLToPath } from "url"; - -export const weaverConfig = { - weaverName: "DefaultWeaver", - weaverPrettyName: "Default Weaver", - weaverFileName: "@specs-feup/lara/code/Weaver.ts", - jarPath: path.join( - path.dirname(path.dirname(path.dirname(fileURLToPath(import.meta.url)))), - "./DefaultWeaver/build/install/DefaultWeaver", - ), - javaWeaverQualifiedName: "org.lara.interpreter.weaver.defaultweaver.DWWeaver", -}; - -export default defineConfig({ - preset: "ts-jest/presets/default-esm", - testEnvironment: "@specs-feup/lara/jest/jestEnvironment.ts", - globalSetup: "@specs-feup/lara/jest/jestGlobalSetup.ts", - globalTeardown: "@specs-feup/lara/jest/jestGlobalTeardown.ts", - setupFiles: ["@specs-feup/lara/jest/setupFiles/sharedJavaModule.ts"], - setupFilesAfterEnv: ["@specs-feup/lara/jest/setupFiles/importSideEffects.ts"], - moduleNameMapper: { - "(.+)\\.js": "$1", - }, - testEnvironmentOptions: { - weaverConfig, - }, -}); diff --git a/Lara-JS/api/vitest.config.ts b/Lara-JS/api/vitest.config.ts new file mode 100644 index 00000000..32ff83a8 --- /dev/null +++ b/Lara-JS/api/vitest.config.ts @@ -0,0 +1,19 @@ +import { defineProject } from "vitest/config"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = path.dirname(fileURLToPath(import.meta.url)); + +export default defineProject({ + root, + test: { + experimental: { + viteModuleRunner: false, + }, + fileParallelism: false, + globals: true, + name: "api", + pool: "forks", + setupFiles: ["../vitest/setupDefaultWeaver.ts"], + }, +}); diff --git a/Lara-JS/code/ChildProcessHandling.test.ts b/Lara-JS/code/ChildProcessHandling.test.ts index 74af9391..cc79bfbc 100644 --- a/Lara-JS/code/ChildProcessHandling.test.ts +++ b/Lara-JS/code/ChildProcessHandling.test.ts @@ -1,4 +1,4 @@ -import { jest } from "@jest/globals"; +import { vi } from "vitest"; import { addActiveChildProcess, handleExit, @@ -36,16 +36,16 @@ describe("ChildProcessHandling", () => { getActiveChildProcesses()[childProcess1.pid] = childProcess1; getActiveChildProcesses()[childProcess2.pid] = childProcess2; - const childProcessKillSpy1 = jest + const childProcessKillSpy1 = vi .spyOn(childProcess1, "kill") .mockClear(); - const childProcessOnceSpy1 = jest + const childProcessOnceSpy1 = vi .spyOn(childProcess1, "once") .mockClear(); - const childProcessKillSpy2 = jest + const childProcessKillSpy2 = vi .spyOn(childProcess2, "kill") .mockClear(); - const childProcessOnceSpy2 = jest + const childProcessOnceSpy2 = vi .spyOn(childProcess2, "once") .mockClear(); @@ -56,7 +56,7 @@ describe("ChildProcessHandling", () => { expect(childProcessKillSpy2).toHaveBeenCalledTimes(1); expect(childProcessOnceSpy2).toHaveBeenCalledTimes(1); - jest.restoreAllMocks(); + vi.restoreAllMocks(); }); }); }); diff --git a/Lara-JS/code/jest.config.ts b/Lara-JS/code/jest.config.ts deleted file mode 100644 index 76873b4a..00000000 --- a/Lara-JS/code/jest.config.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { defineConfig } from "jest"; - -export default defineConfig({ - preset: "ts-jest/presets/default-esm", - testEnvironment: "node", - moduleNameMapper: { - "(.+)\\.js": "$1", - }, -}); diff --git a/Lara-JS/code/vitest.config.ts b/Lara-JS/code/vitest.config.ts new file mode 100644 index 00000000..c1ba777d --- /dev/null +++ b/Lara-JS/code/vitest.config.ts @@ -0,0 +1,18 @@ +import { defineProject } from "vitest/config"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = path.dirname(fileURLToPath(import.meta.url)); + +export default defineProject({ + root, + test: { + experimental: { + viteModuleRunner: false, + }, + fileParallelism: false, + globals: true, + name: "code", + pool: "forks", + }, +}); diff --git a/Lara-JS/eslint.config.ts b/Lara-JS/eslint.config.ts index 7df9bdcf..3a6f6255 100644 --- a/Lara-JS/eslint.config.ts +++ b/Lara-JS/eslint.config.ts @@ -1,52 +1,51 @@ import js from "@eslint/js"; import eslintConfigPrettier from "eslint-config-prettier"; -import jest from "eslint-plugin-jest"; import tsdoc from "eslint-plugin-tsdoc"; import { defineConfig } from "eslint/config"; -import { dirname } from "path"; import typescriptEslint from "typescript-eslint"; -import { fileURLToPath } from "url"; +import vitest from "@vitest/eslint-plugin"; -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -export default defineConfig([ - js.configs.recommended, - eslintConfigPrettier, - ...typescriptEslint.configs.recommended, - { - plugins: { - "@typescript-eslint": typescriptEslint.plugin, - tsdoc, - }, +export function createEslintConfig(tsconfigRootDir: string) { + return defineConfig([ + js.configs.recommended, + eslintConfigPrettier, + ...typescriptEslint.configs.recommended, + { + plugins: { + "@typescript-eslint": typescriptEslint.plugin, + tsdoc, + }, - languageOptions: { - parser: typescriptEslint.parser, - ecmaVersion: 5, - sourceType: "script", + languageOptions: { + parser: typescriptEslint.parser, + ecmaVersion: 5, + sourceType: "script", - parserOptions: { - project: ["./tsconfig.json", "./tsconfig.*.json"], - tsconfigRootDir: __dirname, + parserOptions: { + project: ["./tsconfig.json", "./tsconfig.*.json"], + tsconfigRootDir, + }, }, - }, - rules: { - "tsdoc/syntax": "warn", + rules: { + "tsdoc/syntax": "warn", + }, }, - }, - { - ...jest.configs["flat/recommended"], - files: ["**/*.spec.ts", "**/*.test.ts"], + { + ...vitest.configs.recommended, + files: ["**/*.spec.ts", "**/*.test.ts"], - plugins: { - jest, - }, + plugins: { + vitest, + }, - languageOptions: { - globals: { - ...jest.environments.globals.globals, + languageOptions: { + globals: { + ...vitest.environments.env.globals, + }, }, }, - }, -]); + ]); +} + +export default createEslintConfig(import.meta.dirname); diff --git a/Lara-JS/jest.config.ts b/Lara-JS/jest.config.ts deleted file mode 100644 index 3036d5e5..00000000 --- a/Lara-JS/jest.config.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { defineConfig } from "jest"; - -export default defineConfig({ - preset: "ts-jest/presets/default-esm", - testEnvironment: "node", - moduleNameMapper: { - "(.+)\\.js": "$1", - }, - notify: true, - notifyMode: "always", - //verbose: true, - collectCoverage: false, - coverageDirectory: "coverage", - coverageReporters: ["text", "lcov"], - collectCoverageFrom: ["**/*[^.d].(t|j)s"], - coverageProvider: "v8", - projects: ["api", "code"], -}); diff --git a/Lara-JS/jest/jestEnvironment.ts b/Lara-JS/jest/jestEnvironment.ts deleted file mode 100644 index a9cde408..00000000 --- a/Lara-JS/jest/jestEnvironment.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { - EnvironmentContext, - JestEnvironmentConfig, -} from "@jest/environment"; -import type WeaverConfiguration from "@specs-feup/lara/code/WeaverConfiguration.ts"; -import java from "java"; -import { TestEnvironment } from "jest-environment-node"; - -export default class LaraWeaverEnvironment extends TestEnvironment { - private weaverConfig: WeaverConfiguration; - - constructor(config: JestEnvironmentConfig, context: EnvironmentContext) { - super(config, context); - this.weaverConfig = config.projectConfig.testEnvironmentOptions - .weaverConfig as WeaverConfiguration; - } - - async setup() { - await super.setup(); - this.global.__SHARED_MODULE__ = java; - this.global.__LARA_IMPORT_FOR_SIDE_EFFECTS__ = - this.weaverConfig?.importForSideEffects ?? []; - } - - async teardown() { - await super.teardown(); - } -} diff --git a/Lara-JS/jest/jestGlobalSetup.ts b/Lara-JS/jest/jestGlobalSetup.ts deleted file mode 100644 index d4ae593f..00000000 --- a/Lara-JS/jest/jestGlobalSetup.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { Config } from "@jest/types"; -import { Weaver } from "@specs-feup/lara/code/Weaver.ts"; -import type WeaverMessageFromLauncher from "@specs-feup/lara/code/WeaverMessageFromLauncher.ts"; -import type WeaverConfiguration from "../code/WeaverConfiguration.ts"; - -async function oneTimeSetup( - globalConfig: Config.GlobalConfig, - projectConfig: Config.ProjectConfig, -) { - const weaverMessageFromLauncher: WeaverMessageFromLauncher = { - args: { - _: [], - $0: "", - }, - config: projectConfig.testEnvironmentOptions - .weaverConfig as WeaverConfiguration, - }; - - await Weaver.setupWeaver( - weaverMessageFromLauncher.args, - weaverMessageFromLauncher.config, - ); -} - -let setupDone = false; - -export default async function ( - globalConfig: Config.GlobalConfig, - projectConfig: Config.ProjectConfig, -) { - if (!setupDone) { - await oneTimeSetup(globalConfig, projectConfig); - setupDone = true; - } - Weaver.start(); -} diff --git a/Lara-JS/jest/jestGlobalTeardown.ts b/Lara-JS/jest/jestGlobalTeardown.ts deleted file mode 100644 index 09bd5f97..00000000 --- a/Lara-JS/jest/jestGlobalTeardown.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Weaver } from "@specs-feup/lara/code/Weaver.ts"; -import java from "java"; - -export default function () { - Weaver.shutdown(); - java.stop(); -} diff --git a/Lara-JS/jest/setupFiles/importSideEffects.ts b/Lara-JS/jest/setupFiles/importSideEffects.ts deleted file mode 100644 index 87874f1e..00000000 --- a/Lara-JS/jest/setupFiles/importSideEffects.ts +++ /dev/null @@ -1,14 +0,0 @@ -// Take a look at https://github.com/specs-feup/lara-framework/issues/93 for context. - -// This file is executed after the test environment is set up, so we can access -// global variables set in the test environment. - -declare global { - var __LARA_IMPORT_FOR_SIDE_EFFECTS__: string[] | undefined; -} - -const importForSideEffects = globalThis.__LARA_IMPORT_FOR_SIDE_EFFECTS__ ?? []; - -for (const specifier of importForSideEffects) { - await import(specifier); -} diff --git a/Lara-JS/jest/setupFiles/sharedJavaModule.ts b/Lara-JS/jest/setupFiles/sharedJavaModule.ts deleted file mode 100644 index 2c0e6f0f..00000000 --- a/Lara-JS/jest/setupFiles/sharedJavaModule.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { jest } from "@jest/globals"; - -declare global { - var __SHARED_MODULE__: typeof import("java"); -} - -jest.mock("java", () => global.__SHARED_MODULE__); diff --git a/Lara-JS/package.json b/Lara-JS/package.json index ff33128d..7cb8d49e 100644 --- a/Lara-JS/package.json +++ b/Lara-JS/package.json @@ -17,24 +17,26 @@ "./code/*.js": "./code/*.ts", "./code/*": "./code/*.ts", "./code/*.ts": "./code/*.ts", - "./jest/*.test.ts": null, - "./jest/*.config.ts": null, - "./jest/*.js": "./jest/*.ts", - "./jest/*": "./jest/*.ts", - "./jest/*.ts": "./jest/*.ts", + "./vitest/*.test.ts": null, + "./vitest/*.config.ts": null, + "./vitest/*.js": "./vitest/*.ts", + "./vitest/*": "./vitest/*.ts", + "./vitest/*.ts": "./vitest/*.ts", "./eslint.config.ts": "./eslint.config.ts", - "./jest.config.ts": "./jest.config.ts", + "./prettier.config.ts": "./prettier.config.ts", + "./vitest.config.ts": "./vitest.config.ts", "./LaraJoinPointSpecification.json": "./LaraJoinPointSpecification.json", "./typedoc.base.json": "./typedoc.base.json" }, "files": [ "api", "code", - "jest", + "vitest", "scripts", ".gitignore", "eslint.config.ts", - "jest.config.ts", + "prettier.config.ts", + "vitest.config.ts", "LaraJoinPointSpecification.json", "LICENSE", "package.json", @@ -47,7 +49,9 @@ "build": "tsc", "build:watch": "npm run build -- --watch", "lint": "eslint --flag unstable_native_nodejs_ts_config .", - "test": "cross-env NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" jest --runInBand", + "format": "prettier --write .", + "format:check": "prettier --check .", + "test": "vitest run", "test:api": "npm run test -- api", "test:code": "npm run test -- code", "test:cov": "npm run test -- --coverage", @@ -85,20 +89,19 @@ "yargs": "^18.0.0" }, "devDependencies": { - "@jest/globals": "^30.2.0", + "@vitest/coverage-v8": "^4.1.10", + "@vitest/eslint-plugin": "^1.6.23", "@types/debug": "^4.1.12", - "@types/jest": "^30.0.0", "@types/node": "^25.0.0", "@types/yargs": "^17.0.35", - "cross-env": "^10.1.0", "eslint": "^9.39.2", "eslint-config-prettier": "^10.1.8", - "eslint-plugin-jest": "^29.12.1", "eslint-plugin-tsdoc": "^0.5.0", - "jest": "^30.2.0", "node-notifier": "^10.0.1", - "ts-jest": "^29.4.6", + "prettier": "^3.9.5", "typescript": "^7.0.2", - "typescript-eslint": "^8.52.0" + "typescript-eslint": "^8.52.0", + "vite": "^8.0.0", + "vitest": "^4.1.10" } } diff --git a/Lara-JS/prettier.config.ts b/Lara-JS/prettier.config.ts new file mode 100644 index 00000000..d213110f --- /dev/null +++ b/Lara-JS/prettier.config.ts @@ -0,0 +1,5 @@ +import type { Config } from "prettier"; + +export default { + trailingComma: "all", +} satisfies Config; diff --git a/Lara-JS/tsconfig.json b/Lara-JS/tsconfig.json index 45c21145..416c1093 100644 --- a/Lara-JS/tsconfig.json +++ b/Lara-JS/tsconfig.json @@ -6,7 +6,7 @@ "strict": true, "isolatedModules": true, "allowSyntheticDefaultImports": true, - "types": ["node", "jest"], + "types": ["node", "vitest/globals"], "rewriteRelativeImportExtensions": true, "erasableSyntaxOnly": true, "verbatimModuleSyntax": true diff --git a/Lara-JS/vitest.config.ts b/Lara-JS/vitest.config.ts new file mode 100644 index 00000000..3b9aced4 --- /dev/null +++ b/Lara-JS/vitest.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + coverage: { + include: ["**/*[^.d].(t|j)s"], + provider: "v8", + reporter: ["text", "lcov"], + }, + maxWorkers: 1, + projects: ["api/vitest.config.ts", "code/vitest.config.ts"], + }, +}); diff --git a/Lara-JS/jest/WeaverLegacyTester.ts b/Lara-JS/vitest/WeaverLegacyTester.ts similarity index 94% rename from Lara-JS/jest/WeaverLegacyTester.ts rename to Lara-JS/vitest/WeaverLegacyTester.ts index c428a146..26c22654 100644 --- a/Lara-JS/jest/WeaverLegacyTester.ts +++ b/Lara-JS/vitest/WeaverLegacyTester.ts @@ -1,4 +1,4 @@ -import { jest } from "@jest/globals"; +import { vi } from "vitest"; import JavaTypes, { type JavaClasses, } from "@specs-feup/lara/api/lara/util/JavaTypes.ts"; @@ -13,7 +13,7 @@ afterAll(() => { javaDatastore.set( JavaTypes.LaraiKeys.WORKSPACE_FOLDER, - JavaTypes.FileList.newInstance() + JavaTypes.FileList.newInstance(), ); javaWeaver.run(javaDatastore); @@ -128,7 +128,7 @@ export class WeaverLegacyTester { } let out = ""; - const log = jest.spyOn(global.console, "log"); + const log = vi.spyOn(global.console, "log"); log.mockImplementation((data, ...args: unknown[]) => { if (data) { out += util.format(data, ...args); @@ -142,11 +142,11 @@ export class WeaverLegacyTester { for (const codeResource of codeResources) { const javaFile = new JavaTypes.File( - this.buildCodeResource(codeResource) + this.buildCodeResource(codeResource), ); if (!fs.existsSync(javaFile.getAbsolutePath())) { throw new Error( - `Code resource '${codeResource}' does not exist at '${javaFile.getAbsolutePath()}'.` + `Code resource '${codeResource}' does not exist at '${javaFile.getAbsolutePath()}'.`, ); } javaFiles.add(javaFile); @@ -157,7 +157,7 @@ export class WeaverLegacyTester { javaDatastore.set( JavaTypes.LaraiKeys.WORKSPACE_FOLDER, - JavaTypes.FileList.newInstance(javaFiles) + JavaTypes.FileList.newInstance(javaFiles), ); javaWeaver.run(javaDatastore); @@ -192,7 +192,7 @@ export class WeaverLegacyTester { "Could not find resource '" + expectedResource + "'. Actual output:\n" + - out + out, ); throw new Error("Expected outputs not found"); @@ -202,8 +202,8 @@ export class WeaverLegacyTester { WeaverLegacyTester.normalize( fs .readFileSync(expectedResource, "utf8") - .replaceAll(`/**** File '${this.WORK_FOLDER}/`, "/**** File '") - ) + .replaceAll(`/**** File '${this.WORK_FOLDER}/`, "/**** File '"), + ), ); } diff --git a/Lara-JS/vitest/setupDefaultWeaver.ts b/Lara-JS/vitest/setupDefaultWeaver.ts new file mode 100644 index 00000000..5999d74e --- /dev/null +++ b/Lara-JS/vitest/setupDefaultWeaver.ts @@ -0,0 +1,14 @@ +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { setupWeaver } from "./setupWeaver.ts"; + +setupWeaver({ + jarPath: path.join( + path.dirname(path.dirname(path.dirname(fileURLToPath(import.meta.url)))), + "./DefaultWeaver/build/install/DefaultWeaver", + ), + javaWeaverQualifiedName: "org.lara.interpreter.weaver.defaultweaver.DWWeaver", + weaverFileName: "@specs-feup/lara/code/Weaver.ts", + weaverName: "DefaultWeaver", + weaverPrettyName: "Default Weaver", +}); diff --git a/Lara-JS/vitest/setupWeaver.ts b/Lara-JS/vitest/setupWeaver.ts new file mode 100644 index 00000000..403c413d --- /dev/null +++ b/Lara-JS/vitest/setupWeaver.ts @@ -0,0 +1,33 @@ +import { Weaver } from "@specs-feup/lara/code/Weaver.ts"; +import type WeaverMessageFromLauncher from "@specs-feup/lara/code/WeaverMessageFromLauncher.ts"; +import type WeaverConfiguration from "@specs-feup/lara/code/WeaverConfiguration.ts"; +import java from "java"; +import { afterAll, beforeAll } from "vitest"; + +export function setupWeaver(config: WeaverConfiguration): void { + let started = false; + + beforeAll(async () => { + const message: WeaverMessageFromLauncher = { + args: { _: [], $0: "" }, + config, + }; + + await Weaver.setupWeaver(message.args, message.config); + Weaver.start(); + started = true; + + for (const specifier of config.importForSideEffects ?? []) { + await import(specifier); + } + }); + + afterAll(() => { + if (!started) { + return; + } + + Weaver.shutdown(); + java.stop(); + }); +} diff --git a/Lara-JS/jest/jestHelpers.ts b/Lara-JS/vitest/weaverTestHelpers.ts similarity index 96% rename from Lara-JS/jest/jestHelpers.ts rename to Lara-JS/vitest/weaverTestHelpers.ts index 503af5e0..e44d1af6 100644 --- a/Lara-JS/jest/jestHelpers.ts +++ b/Lara-JS/vitest/weaverTestHelpers.ts @@ -33,7 +33,7 @@ export function registerSourceCodes(codes: Record): void { javaDatastore.set( JavaTypes.LaraiKeys.WORKSPACE_FOLDER, - JavaTypes.FileList.newInstance() + JavaTypes.FileList.newInstance(), ); javaWeaver.run(javaDatastore); From 6f96cc19e60e60a5535f5ba72d91ca81b66e4159 Mon Sep 17 00:00:00 2001 From: "L. Sousa" Date: Tue, 14 Jul 2026 18:30:42 +0100 Subject: [PATCH 4/9] build: migrate Lara-JS checks to Oxc Replace ESLint and Prettier scripts/configuration with Oxlint and Oxfmt, retain TSDoc validation through the compatible plugin bridge, and update lint-disable directives. --- Lara-JS/api/LaraJoinPoint.ts | 2 +- .../api/LegacyIntegrationTests - API.test.ts | 2 +- .../LegacyIntegrationTests - JsEngine.test.ts | 4 +- .../LegacyIntegrationTests - Weaver.test.ts | 2 +- .../api/lara/benchmark/BenchmarkInstance.ts | 4 +- Lara-JS/api/lara/benchmark/BenchmarkSet.ts | 4 +- Lara-JS/api/lara/metrics/FileSizeMetric.ts | 2 +- Lara-JS/api/lara/pass/AdapterPass.ts | 2 +- Lara-JS/api/lara/pass/results/PassResult.ts | 2 +- Lara-JS/api/lara/util/JavaTypes.ts | 6 +- Lara-JS/code/JavaError.ts | 2 +- Lara-JS/code/Weaver.ts | 10 +- Lara-JS/eslint.config.ts | 51 ------ Lara-JS/oxlint.config.ts | 160 ++++++++++++++++++ Lara-JS/package.json | 19 +-- Lara-JS/prettier.config.ts | 5 - Lara-JS/scripts/build-LaraJoinPoint.ts | 2 +- 17 files changed, 189 insertions(+), 90 deletions(-) delete mode 100644 Lara-JS/eslint.config.ts create mode 100644 Lara-JS/oxlint.config.ts delete mode 100644 Lara-JS/prettier.config.ts diff --git a/Lara-JS/api/LaraJoinPoint.ts b/Lara-JS/api/LaraJoinPoint.ts index e1d7efd3..595c9ea4 100644 --- a/Lara-JS/api/LaraJoinPoint.ts +++ b/Lara-JS/api/LaraJoinPoint.ts @@ -2,7 +2,7 @@ // This file is generated by build-LaraJoinPoint.ts // ////////////////////////////////////////////////////// -/* eslint-disable @typescript-eslint/no-explicit-any */ +/* oxlint-disable typescript/no-explicit-any */ import JavaTypes from "./lara/util/JavaTypes.ts"; diff --git a/Lara-JS/api/LegacyIntegrationTests - API.test.ts b/Lara-JS/api/LegacyIntegrationTests - API.test.ts index fd7f7197..979b2ee6 100644 --- a/Lara-JS/api/LegacyIntegrationTests - API.test.ts +++ b/Lara-JS/api/LegacyIntegrationTests - API.test.ts @@ -1,7 +1,7 @@ import { WeaverLegacyTester } from "../vitest/WeaverLegacyTester.ts"; import path from "path"; -/* eslint-disable vitest/expect-expect */ +/* oxlint-disable vitest/expect-expect */ describe("ApiTest", () => { function newTester() { return new WeaverLegacyTester( diff --git a/Lara-JS/api/LegacyIntegrationTests - JsEngine.test.ts b/Lara-JS/api/LegacyIntegrationTests - JsEngine.test.ts index 46b0576d..734a0387 100644 --- a/Lara-JS/api/LegacyIntegrationTests - JsEngine.test.ts +++ b/Lara-JS/api/LegacyIntegrationTests - JsEngine.test.ts @@ -1,7 +1,7 @@ import { WeaverLegacyTester } from "../vitest/WeaverLegacyTester.ts"; import path from "path"; -/* eslint-disable vitest/expect-expect */ +/* oxlint-disable vitest/expect-expect */ describe("JsEngine", () => { function newTester() { return new WeaverLegacyTester( @@ -22,7 +22,7 @@ describe("JsEngine", () => { } expect(err).toBeInstanceOf(Error); - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any expect((err as any).message).toContain("throwing exception in bar()"); }); }); diff --git a/Lara-JS/api/LegacyIntegrationTests - Weaver.test.ts b/Lara-JS/api/LegacyIntegrationTests - Weaver.test.ts index b83feee0..99ffe573 100644 --- a/Lara-JS/api/LegacyIntegrationTests - Weaver.test.ts +++ b/Lara-JS/api/LegacyIntegrationTests - Weaver.test.ts @@ -1,7 +1,7 @@ import { WeaverLegacyTester } from "../vitest/WeaverLegacyTester.ts"; import path from "path"; -/* eslint-disable vitest/expect-expect */ +/* oxlint-disable vitest/expect-expect */ describe("Weaver", () => { function newTester() { return new WeaverLegacyTester( diff --git a/Lara-JS/api/lara/benchmark/BenchmarkInstance.ts b/Lara-JS/api/lara/benchmark/BenchmarkInstance.ts index bdc9e646..c175f1c0 100644 --- a/Lara-JS/api/lara/benchmark/BenchmarkInstance.ts +++ b/Lara-JS/api/lara/benchmark/BenchmarkInstance.ts @@ -218,14 +218,14 @@ export default abstract class BenchmarkInstance { */ test( worker: (instance: BenchmarkInstance) => boolean = ( - // eslint-disable-next-line @typescript-eslint/no-unused-vars + // oxlint-disable-next-line typescript/no-unused-vars _instance: BenchmarkInstance ) => { return true; }, executeCode: boolean = false, outputProcessor: (executor: ProcessExecutor) => void = ( - // eslint-disable-next-line @typescript-eslint/no-unused-vars + // oxlint-disable-next-line typescript/no-unused-vars _executor: ProcessExecutor ) => {} ): boolean { diff --git a/Lara-JS/api/lara/benchmark/BenchmarkSet.ts b/Lara-JS/api/lara/benchmark/BenchmarkSet.ts index 6c796a1b..0f669f88 100644 --- a/Lara-JS/api/lara/benchmark/BenchmarkSet.ts +++ b/Lara-JS/api/lara/benchmark/BenchmarkSet.ts @@ -77,14 +77,14 @@ export default abstract class BenchmarkSet { */ test( worker: Parameters[0] = ( - // eslint-disable-next-line @typescript-eslint/no-unused-vars + // oxlint-disable-next-line typescript/no-unused-vars _instance: BenchmarkInstance ) => { return true; }, executeCode: Parameters[1] = false, outputProcessor: Parameters[2] = ( - // eslint-disable-next-line @typescript-eslint/no-unused-vars + // oxlint-disable-next-line typescript/no-unused-vars _executor: ProcessExecutor ) => {} ): string[] { diff --git a/Lara-JS/api/lara/metrics/FileSizeMetric.ts b/Lara-JS/api/lara/metrics/FileSizeMetric.ts index 2034dca6..25ee80f5 100644 --- a/Lara-JS/api/lara/metrics/FileSizeMetric.ts +++ b/Lara-JS/api/lara/metrics/FileSizeMetric.ts @@ -11,7 +11,7 @@ export default class FileSizeMetric extends Metric { super("File Size"); } - // eslint-disable-next-line @typescript-eslint/no-unused-vars + // oxlint-disable-next-line typescript/no-unused-vars instrument($start: LaraJoinPoint, $end: LaraJoinPoint = $start) {} // Override diff --git a/Lara-JS/api/lara/pass/AdapterPass.ts b/Lara-JS/api/lara/pass/AdapterPass.ts index 789d610f..c6333ef8 100644 --- a/Lara-JS/api/lara/pass/AdapterPass.ts +++ b/Lara-JS/api/lara/pass/AdapterPass.ts @@ -70,7 +70,7 @@ class AdapterPass extends SimplePass { } } -// eslint-disable-next-line @typescript-eslint/no-namespace +// oxlint-disable-next-line typescript/no-namespace namespace AdapterPass { /** * @param name - Name of the pass diff --git a/Lara-JS/api/lara/pass/results/PassResult.ts b/Lara-JS/api/lara/pass/results/PassResult.ts index 6e7fbeec..85e21d87 100644 --- a/Lara-JS/api/lara/pass/results/PassResult.ts +++ b/Lara-JS/api/lara/pass/results/PassResult.ts @@ -83,7 +83,7 @@ class PassResult { } } -// eslint-disable-next-line @typescript-eslint/no-namespace +// oxlint-disable-next-line typescript/no-namespace namespace PassResult { export interface PassResultParams { /** diff --git a/Lara-JS/api/lara/util/JavaTypes.ts b/Lara-JS/api/lara/util/JavaTypes.ts index a02153b4..58312539 100644 --- a/Lara-JS/api/lara/util/JavaTypes.ts +++ b/Lara-JS/api/lara/util/JavaTypes.ts @@ -2,7 +2,7 @@ import java from "java"; export const NodeJavaPrefix = "nodeJava_"; -// eslint-disable-next-line @typescript-eslint/no-namespace +// oxlint-disable-next-line typescript/no-namespace export namespace JavaClasses { export interface JavaClass { (...args: unknown[]): any; @@ -10,7 +10,7 @@ export namespace JavaClasses { [key: string]: any; } - /* eslint-disable @typescript-eslint/no-empty-object-type */ + /* oxlint-disable typescript/no-empty-object-type */ export interface LaraI extends JavaClass {} export interface LaraApiTools extends JavaClass {} export interface LaraSystemTools extends JavaClass { @@ -71,7 +71,7 @@ export namespace JavaClasses { } export interface ProgressCounter extends JavaClasses.JavaClass {} export interface LineStream extends JavaClasses.JavaClass {} - /* eslint-enable @typescript-eslint/no-empty-object-type */ + /* oxlint-enable typescript/no-empty-object-type */ } export default class JavaTypes { diff --git a/Lara-JS/code/JavaError.ts b/Lara-JS/code/JavaError.ts index 170f188a..9d8c13db 100644 --- a/Lara-JS/code/JavaError.ts +++ b/Lara-JS/code/JavaError.ts @@ -7,7 +7,7 @@ export default interface JavaError { }; } -// eslint-disable-next-line @typescript-eslint/no-explicit-any +// oxlint-disable-next-line typescript/no-explicit-any export function isJavaError(obj: any): obj is JavaError { return obj.cause != undefined; } diff --git a/Lara-JS/code/Weaver.ts b/Lara-JS/code/Weaver.ts index 4cf0beed..cff954ac 100644 --- a/Lara-JS/code/Weaver.ts +++ b/Lara-JS/code/Weaver.ts @@ -25,9 +25,9 @@ java.asyncOptions = { export class Weaver { private static debug: Debug.Debugger; - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any private static datastore: any; - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // oxlint-disable-next-line typescript/no-explicit-any private static javaWeaver: any; static getDatastore(): any { @@ -77,8 +77,8 @@ export class Weaver { const JavaSpecsSystem = java.import("pt.up.fe.specs.util.SpecsSystem"); JavaSpecsSystem.programStandardInit(); - /* eslint-disable */ - // This code is intentionally ignored by eslint + /* oxlint-disable */ + // This code is intentionally ignored by the linter const JavaArrayList = java.import("java.util.ArrayList"); const JavaFile = java.import("java.io.File"); const JavaFileList = java.import( @@ -155,7 +155,7 @@ export class Weaver { for (const file of config.importForSideEffects ?? []) { await import(file); } - /* eslint-enable */ + /* oxlint-enable */ } static start() { diff --git a/Lara-JS/eslint.config.ts b/Lara-JS/eslint.config.ts deleted file mode 100644 index 3a6f6255..00000000 --- a/Lara-JS/eslint.config.ts +++ /dev/null @@ -1,51 +0,0 @@ -import js from "@eslint/js"; -import eslintConfigPrettier from "eslint-config-prettier"; -import tsdoc from "eslint-plugin-tsdoc"; -import { defineConfig } from "eslint/config"; -import typescriptEslint from "typescript-eslint"; -import vitest from "@vitest/eslint-plugin"; - -export function createEslintConfig(tsconfigRootDir: string) { - return defineConfig([ - js.configs.recommended, - eslintConfigPrettier, - ...typescriptEslint.configs.recommended, - { - plugins: { - "@typescript-eslint": typescriptEslint.plugin, - tsdoc, - }, - - languageOptions: { - parser: typescriptEslint.parser, - ecmaVersion: 5, - sourceType: "script", - - parserOptions: { - project: ["./tsconfig.json", "./tsconfig.*.json"], - tsconfigRootDir, - }, - }, - - rules: { - "tsdoc/syntax": "warn", - }, - }, - { - ...vitest.configs.recommended, - files: ["**/*.spec.ts", "**/*.test.ts"], - - plugins: { - vitest, - }, - - languageOptions: { - globals: { - ...vitest.environments.env.globals, - }, - }, - }, - ]); -} - -export default createEslintConfig(import.meta.dirname); diff --git a/Lara-JS/oxlint.config.ts b/Lara-JS/oxlint.config.ts new file mode 100644 index 00000000..d8ddd430 --- /dev/null +++ b/Lara-JS/oxlint.config.ts @@ -0,0 +1,160 @@ +import { defineConfig } from "oxlint"; + +export default defineConfig({ + plugins: ["unicorn", "typescript"], + jsPlugins: ["eslint-plugin-tsdoc"], + categories: { + correctness: "off", + }, + env: { + builtin: true, + }, + rules: { + "constructor-super": "error", + "for-direction": "error", + "getter-return": "error", + "no-async-promise-executor": "error", + "no-case-declarations": "error", + "no-class-assign": "error", + "no-compare-neg-zero": "error", + "no-cond-assign": "error", + "no-const-assign": "error", + "no-constant-binary-expression": "error", + "no-constant-condition": "error", + "no-control-regex": "error", + "no-debugger": "error", + "no-delete-var": "error", + "no-dupe-class-members": "error", + "no-dupe-else-if": "error", + "no-dupe-keys": "error", + "no-duplicate-case": "error", + "no-empty": "error", + "no-empty-character-class": "error", + "no-empty-pattern": "error", + "no-empty-static-block": "error", + "no-ex-assign": "error", + "no-extra-boolean-cast": "error", + "no-fallthrough": "error", + "no-func-assign": "error", + "no-global-assign": "error", + "no-import-assign": "error", + "no-invalid-regexp": "error", + "no-irregular-whitespace": "error", + "no-loss-of-precision": "error", + "no-misleading-character-class": "error", + "no-new-native-nonconstructor": "error", + "no-nonoctal-decimal-escape": "error", + "no-obj-calls": "error", + "no-prototype-builtins": "error", + "no-redeclare": "error", + "no-regex-spaces": "error", + "no-self-assign": "error", + "no-setter-return": "error", + "no-shadow-restricted-names": "error", + "no-sparse-arrays": "error", + "no-this-before-super": "error", + "no-unreachable": "error", + "no-unsafe-finally": "error", + "no-unsafe-negation": "error", + "no-unsafe-optional-chaining": "error", + "no-unused-labels": "error", + "no-unused-private-class-members": "error", + "no-unused-vars": "error", + "no-useless-backreference": "error", + "no-useless-catch": "error", + "no-useless-escape": "error", + "no-with": "error", + "require-yield": "error", + "use-isnan": "error", + "valid-typeof": "error", + "no-array-constructor": "error", + "no-unused-expressions": "error", + "tsdoc/syntax": "warn", + "typescript/ban-ts-comment": "error", + "typescript/no-duplicate-enum-values": "error", + "typescript/no-empty-object-type": "error", + "typescript/no-explicit-any": "error", + "typescript/no-extra-non-null-assertion": "error", + "typescript/no-misused-new": "error", + "typescript/no-namespace": "error", + "typescript/no-non-null-asserted-optional-chain": "error", + "typescript/no-require-imports": "error", + "typescript/no-this-alias": "error", + "typescript/no-unnecessary-type-constraint": "error", + "typescript/no-unsafe-declaration-merging": "error", + "typescript/no-unsafe-function-type": "error", + "typescript/no-wrapper-object-types": "error", + "typescript/prefer-as-const": "error", + "typescript/prefer-namespace-keyword": "error", + "typescript/triple-slash-reference": "error", + }, + overrides: [ + { + files: ["**/*.ts", "**/*.tsx", "**/*.mts", "**/*.cts"], + rules: { + "constructor-super": "off", + "getter-return": "off", + "no-class-assign": "off", + "no-const-assign": "off", + "no-dupe-class-members": "off", + "no-dupe-keys": "off", + "no-func-assign": "off", + "no-import-assign": "off", + "no-new-native-nonconstructor": "off", + "no-obj-calls": "off", + "no-redeclare": "off", + "no-setter-return": "off", + "no-this-before-super": "off", + "no-unreachable": "off", + "no-unsafe-negation": "off", + "no-var": "error", + "no-with": "off", + "prefer-const": "error", + "prefer-rest-params": "error", + "prefer-spread": "error", + }, + }, + { + files: ["**/*.spec.ts", "**/*.test.ts"], + plugins: ["vitest"], + rules: { + "vitest/expect-expect": "error", + "vitest/no-commented-out-tests": "error", + "vitest/no-conditional-expect": "error", + "vitest/no-disabled-tests": "warn", + "vitest/no-focused-tests": "error", + "vitest/no-identical-title": "error", + "vitest/no-import-node-test": "error", + "vitest/no-interpolation-in-snapshots": "error", + "vitest/no-mocks-import": "error", + "vitest/no-standalone-expect": "error", + "vitest/no-unneeded-async-expect-function": "error", + "vitest/prefer-called-exactly-once-with": "error", + "vitest/require-local-test-context-for-concurrent-snapshots": "error", + "vitest/valid-describe-callback": "error", + "vitest/valid-expect": "error", + "vitest/valid-expect-in-promise": "error", + "vitest/valid-title": "error", + }, + globals: { + suite: "writable", + test: "writable", + describe: "writable", + it: "writable", + expectTypeOf: "writable", + assertType: "writable", + expect: "writable", + assert: "writable", + chai: "writable", + vitest: "writable", + vi: "writable", + beforeAll: "writable", + afterAll: "writable", + beforeEach: "writable", + afterEach: "writable", + onTestFailed: "writable", + onTestFinished: "writable", + }, + }, + ], +}); diff --git a/Lara-JS/package.json b/Lara-JS/package.json index 7cb8d49e..cb31b0dd 100644 --- a/Lara-JS/package.json +++ b/Lara-JS/package.json @@ -22,8 +22,7 @@ "./vitest/*.js": "./vitest/*.ts", "./vitest/*": "./vitest/*.ts", "./vitest/*.ts": "./vitest/*.ts", - "./eslint.config.ts": "./eslint.config.ts", - "./prettier.config.ts": "./prettier.config.ts", + "./oxlint.config.ts": "./oxlint.config.ts", "./vitest.config.ts": "./vitest.config.ts", "./LaraJoinPointSpecification.json": "./LaraJoinPointSpecification.json", "./typedoc.base.json": "./typedoc.base.json" @@ -34,8 +33,7 @@ "vitest", "scripts", ".gitignore", - "eslint.config.ts", - "prettier.config.ts", + "oxlint.config.ts", "vitest.config.ts", "LaraJoinPointSpecification.json", "LICENSE", @@ -48,9 +46,9 @@ "scripts": { "build": "tsc", "build:watch": "npm run build -- --watch", - "lint": "eslint --flag unstable_native_nodejs_ts_config .", - "format": "prettier --write .", - "format:check": "prettier --check .", + "lint": "oxlint .", + "format": "oxfmt .", + "format:check": "oxfmt --check .", "test": "vitest run", "test:api": "npm run test -- api", "test:code": "npm run test -- code", @@ -90,17 +88,14 @@ }, "devDependencies": { "@vitest/coverage-v8": "^4.1.10", - "@vitest/eslint-plugin": "^1.6.23", "@types/debug": "^4.1.12", "@types/node": "^25.0.0", "@types/yargs": "^17.0.35", - "eslint": "^9.39.2", - "eslint-config-prettier": "^10.1.8", "eslint-plugin-tsdoc": "^0.5.0", "node-notifier": "^10.0.1", - "prettier": "^3.9.5", + "oxfmt": "^0.59.0", + "oxlint": "^1.74.0", "typescript": "^7.0.2", - "typescript-eslint": "^8.52.0", "vite": "^8.0.0", "vitest": "^4.1.10" } diff --git a/Lara-JS/prettier.config.ts b/Lara-JS/prettier.config.ts deleted file mode 100644 index d213110f..00000000 --- a/Lara-JS/prettier.config.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { Config } from "prettier"; - -export default { - trailingComma: "all", -} satisfies Config; diff --git a/Lara-JS/scripts/build-LaraJoinPoint.ts b/Lara-JS/scripts/build-LaraJoinPoint.ts index 4610deb2..96d9511d 100644 --- a/Lara-JS/scripts/build-LaraJoinPoint.ts +++ b/Lara-JS/scripts/build-LaraJoinPoint.ts @@ -25,7 +25,7 @@ function buildLaraJoinPoint(inputFileName: string, outputFileName: string) { // This file is generated by build-LaraJoinPoint.ts // ////////////////////////////////////////////////////// -/* eslint-disable @typescript-eslint/no-explicit-any */ +/* oxlint-disable typescript/no-explicit-any */ import JavaTypes from "./lara/util/JavaTypes.ts"; From d1805979e011c9015ac7cdbdf47e48ccea75ceeb Mon Sep 17 00:00:00 2001 From: "L. Sousa" Date: Tue, 14 Jul 2026 18:55:42 +0100 Subject: [PATCH 5/9] Declare shared config peer dependencies --- Lara-JS/package.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Lara-JS/package.json b/Lara-JS/package.json index cb31b0dd..ed1e3953 100644 --- a/Lara-JS/package.json +++ b/Lara-JS/package.json @@ -86,6 +86,11 @@ "supports-color": "^10.2.2", "yargs": "^18.0.0" }, + "peerDependencies": { + "eslint-plugin-tsdoc": "^0.5.0", + "oxlint": "^1.74.0", + "vitest": "^4.1.10" + }, "devDependencies": { "@vitest/coverage-v8": "^4.1.10", "@types/debug": "^4.1.12", From 45241efcb6d71ce6e6663b8967687f69d74982eb Mon Sep 17 00:00:00 2001 From: "L. Sousa" Date: Tue, 14 Jul 2026 21:47:48 +0100 Subject: [PATCH 6/9] test: coordinate Vitest Java shutdown Run Lara tests in one shared fork with a Weaver environment that initializes before every test and stops Java only after Vitest has completed worker reporting. Replace per-project setup/config files and preserve legacy file isolation. --- Lara-JS/api/vitest.config.ts | 19 ------ Lara-JS/code/vitest.config.ts | 18 ------ Lara-JS/vitest.config.ts | 24 +++---- Lara-JS/vitest/WeaverLegacyTester.ts | 13 +++- Lara-JS/vitest/setupDefaultWeaver.ts | 14 ---- Lara-JS/vitest/setupWeaver.ts | 33 ---------- Lara-JS/vitest/weaverEnvironment.ts | 95 ++++++++++++++++++++++++++++ Lara-JS/vitest/weaverVitestConfig.ts | 40 ++++++++++++ 8 files changed, 160 insertions(+), 96 deletions(-) delete mode 100644 Lara-JS/api/vitest.config.ts delete mode 100644 Lara-JS/code/vitest.config.ts delete mode 100644 Lara-JS/vitest/setupDefaultWeaver.ts delete mode 100644 Lara-JS/vitest/setupWeaver.ts create mode 100644 Lara-JS/vitest/weaverEnvironment.ts create mode 100644 Lara-JS/vitest/weaverVitestConfig.ts diff --git a/Lara-JS/api/vitest.config.ts b/Lara-JS/api/vitest.config.ts deleted file mode 100644 index 32ff83a8..00000000 --- a/Lara-JS/api/vitest.config.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { defineProject } from "vitest/config"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -const root = path.dirname(fileURLToPath(import.meta.url)); - -export default defineProject({ - root, - test: { - experimental: { - viteModuleRunner: false, - }, - fileParallelism: false, - globals: true, - name: "api", - pool: "forks", - setupFiles: ["../vitest/setupDefaultWeaver.ts"], - }, -}); diff --git a/Lara-JS/code/vitest.config.ts b/Lara-JS/code/vitest.config.ts deleted file mode 100644 index c1ba777d..00000000 --- a/Lara-JS/code/vitest.config.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { defineProject } from "vitest/config"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -const root = path.dirname(fileURLToPath(import.meta.url)); - -export default defineProject({ - root, - test: { - experimental: { - viteModuleRunner: false, - }, - fileParallelism: false, - globals: true, - name: "code", - pool: "forks", - }, -}); diff --git a/Lara-JS/vitest.config.ts b/Lara-JS/vitest.config.ts index 3b9aced4..d0e21392 100644 --- a/Lara-JS/vitest.config.ts +++ b/Lara-JS/vitest.config.ts @@ -1,13 +1,15 @@ -import { defineConfig } from "vitest/config"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { createWeaverVitestConfig } from "./vitest/weaverVitestConfig.ts"; -export default defineConfig({ - test: { - coverage: { - include: ["**/*[^.d].(t|j)s"], - provider: "v8", - reporter: ["text", "lcov"], - }, - maxWorkers: 1, - projects: ["api/vitest.config.ts", "code/vitest.config.ts"], - }, +export default createWeaverVitestConfig({ + jarPath: path.join( + path.dirname(path.dirname(fileURLToPath(import.meta.url))), + "./DefaultWeaver/build/install/DefaultWeaver", + ), + javaWeaverQualifiedName: + "org.lara.interpreter.weaver.defaultweaver.DWWeaver", + weaverFileName: "@specs-feup/lara/code/Weaver.ts", + weaverName: "DefaultWeaver", + weaverPrettyName: "Default Weaver", }); diff --git a/Lara-JS/vitest/WeaverLegacyTester.ts b/Lara-JS/vitest/WeaverLegacyTester.ts index 26c22654..8623e623 100644 --- a/Lara-JS/vitest/WeaverLegacyTester.ts +++ b/Lara-JS/vitest/WeaverLegacyTester.ts @@ -2,11 +2,16 @@ import { vi } from "vitest"; import JavaTypes, { type JavaClasses, } from "@specs-feup/lara/api/lara/util/JavaTypes.ts"; +import IdGenerator from "@specs-feup/lara/api/lara/util/IdGenerator.ts"; +import PrintOnce from "@specs-feup/lara/api/lara/util/PrintOnce.ts"; import Weaver from "@specs-feup/lara/api/weaver/Weaver.ts"; import fs from "fs"; import path from "path"; +import { pathToFileURL } from "url"; import util from "util"; +let legacyScriptRun = 0; + afterAll(() => { const javaWeaver = Weaver.getWeaverEngine(); const javaDatastore = javaWeaver.getData().get(); @@ -17,6 +22,8 @@ afterAll(() => { ); javaWeaver.run(javaDatastore); + IdGenerator.idCounter.clear(); + PrintOnce.messagesSet.clear(); }); export class WeaverLegacyTester { @@ -161,7 +168,11 @@ export class WeaverLegacyTester { ); javaWeaver.run(javaDatastore); - await import(path.join(this.basePackage, laraResource)); + const scriptUrl = pathToFileURL( + path.join(this.basePackage, laraResource), + ); + scriptUrl.searchParams.set("vitestRun", String(legacyScriptRun++)); + await import(scriptUrl.href); javaWeaver.end(); } finally { log.mockRestore(); diff --git a/Lara-JS/vitest/setupDefaultWeaver.ts b/Lara-JS/vitest/setupDefaultWeaver.ts deleted file mode 100644 index 5999d74e..00000000 --- a/Lara-JS/vitest/setupDefaultWeaver.ts +++ /dev/null @@ -1,14 +0,0 @@ -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import { setupWeaver } from "./setupWeaver.ts"; - -setupWeaver({ - jarPath: path.join( - path.dirname(path.dirname(path.dirname(fileURLToPath(import.meta.url)))), - "./DefaultWeaver/build/install/DefaultWeaver", - ), - javaWeaverQualifiedName: "org.lara.interpreter.weaver.defaultweaver.DWWeaver", - weaverFileName: "@specs-feup/lara/code/Weaver.ts", - weaverName: "DefaultWeaver", - weaverPrettyName: "Default Weaver", -}); diff --git a/Lara-JS/vitest/setupWeaver.ts b/Lara-JS/vitest/setupWeaver.ts deleted file mode 100644 index 403c413d..00000000 --- a/Lara-JS/vitest/setupWeaver.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Weaver } from "@specs-feup/lara/code/Weaver.ts"; -import type WeaverMessageFromLauncher from "@specs-feup/lara/code/WeaverMessageFromLauncher.ts"; -import type WeaverConfiguration from "@specs-feup/lara/code/WeaverConfiguration.ts"; -import java from "java"; -import { afterAll, beforeAll } from "vitest"; - -export function setupWeaver(config: WeaverConfiguration): void { - let started = false; - - beforeAll(async () => { - const message: WeaverMessageFromLauncher = { - args: { _: [], $0: "" }, - config, - }; - - await Weaver.setupWeaver(message.args, message.config); - Weaver.start(); - started = true; - - for (const specifier of config.importForSideEffects ?? []) { - await import(specifier); - } - }); - - afterAll(() => { - if (!started) { - return; - } - - Weaver.shutdown(); - java.stop(); - }); -} diff --git a/Lara-JS/vitest/weaverEnvironment.ts b/Lara-JS/vitest/weaverEnvironment.ts new file mode 100644 index 00000000..644e5271 --- /dev/null +++ b/Lara-JS/vitest/weaverEnvironment.ts @@ -0,0 +1,95 @@ +import { Console } from "node:console"; +import java from "java"; +import type { Environment } from "vitest/environments"; +import { Weaver } from "../code/Weaver.ts"; +import type WeaverConfiguration from "../code/WeaverConfiguration.ts"; + +export interface WeaverEnvironmentOptions extends Record { + javaOptionsEnvironmentVariable?: string; + weaver: WeaverConfiguration; +} + +const exitProcess = process.exit.bind(process); + +const environment: Environment = { + name: "weaver", + viteEnvironment: "ssr", + + async setup(global, rawOptions) { + const options = rawOptions as unknown as WeaverEnvironmentOptions; + let weaverStarted = false; + + applyJavaOptions(options.javaOptionsEnvironmentVariable); + + // Vitest sends all results (including worker coverage) before it terminates + // the fork. Stopping node-java any earlier also closes Vitest's IPC handle. + process.once("SIGTERM", () => { + let exitCode = 0; + + try { + if (weaverStarted) { + Weaver.shutdown(); + } + java.stop(); + } catch (error) { + console.error("Failed to stop the Weaver Java runtime cleanly.", error); + exitCode = 1; + } finally { + exitProcess(exitCode); + } + }); + + const message = { + args: { _: [], $0: "" }, + config: options.weaver, + }; + + await Weaver.setupWeaver(message.args, message.config); + Weaver.start(); + weaverStarted = true; + + global.console.Console = Console; + + return { + teardown() { + delete global.console.Console; + }, + }; + }, +}; + +function applyJavaOptions(environmentVariable: string | undefined): void { + if (environmentVariable === undefined) { + return; + } + + const rawJavaOptions = process.env[environmentVariable]; + if (rawJavaOptions === undefined || rawJavaOptions.trim() === "") { + return; + } + + for (const javaOption of parseJavaOptions(rawJavaOptions)) { + if (!java.options.includes(javaOption)) { + java.options.push(javaOption); + } + } +} + +function parseJavaOptions(rawJavaOptions: string): string[] { + try { + const parsed: unknown = JSON.parse(rawJavaOptions); + + if ( + Array.isArray(parsed) && + parsed.every((javaOption) => typeof javaOption === "string") + ) { + return parsed; + } + } catch { + // Fall back to whitespace splitting for ad-hoc local use. + } + + return rawJavaOptions.split(/\s+/).filter((javaOption) => javaOption !== ""); +} + +export default environment; diff --git a/Lara-JS/vitest/weaverVitestConfig.ts b/Lara-JS/vitest/weaverVitestConfig.ts new file mode 100644 index 00000000..022ec572 --- /dev/null +++ b/Lara-JS/vitest/weaverVitestConfig.ts @@ -0,0 +1,40 @@ +import { fileURLToPath } from "node:url"; +import type WeaverConfiguration from "../code/WeaverConfiguration.ts"; +import type { WeaverEnvironmentOptions } from "./weaverEnvironment.ts"; +import { defineConfig } from "vitest/config"; + +export interface WeaverVitestConfigOptions { + javaOptionsEnvironmentVariable?: string; +} + +export function createWeaverVitestConfig( + weaver: WeaverConfiguration, + options: WeaverVitestConfigOptions = {}, +) { + const environmentOptions: WeaverEnvironmentOptions = { + javaOptionsEnvironmentVariable: options.javaOptionsEnvironmentVariable, + weaver, + }; + + return defineConfig({ + test: { + coverage: { + include: ["**/*[^.d].(t|j)s"], + provider: "v8", + reporter: ["text", "lcov"], + }, + environment: fileURLToPath( + new URL("./weaverEnvironment.ts", import.meta.url), + ), + environmentOptions, + experimental: { + viteModuleRunner: false, + }, + fileParallelism: false, + globals: true, + isolate: false, + maxWorkers: 1, + pool: "forks", + }, + }); +} From f1891de3d48e644fba7be97a89eb65220db6bfab Mon Sep 17 00:00:00 2001 From: "L. Sousa" Date: Wed, 15 Jul 2026 02:22:55 +0100 Subject: [PATCH 7/9] build: add reusable Java binaries validator Expose a fixed-path validator for weaver package distributions and cover missing, symlinked, malformed, and valid java-binaries layouts. --- Lara-JS/package.json | 1 + Lara-JS/scripts/validateJavaBinaries.ts | 41 +++++++++++++ Lara-JS/vitest/validateJavaBinaries.test.ts | 66 +++++++++++++++++++++ 3 files changed, 108 insertions(+) create mode 100644 Lara-JS/scripts/validateJavaBinaries.ts create mode 100644 Lara-JS/vitest/validateJavaBinaries.test.ts diff --git a/Lara-JS/package.json b/Lara-JS/package.json index ed1e3953..6fd82387 100644 --- a/Lara-JS/package.json +++ b/Lara-JS/package.json @@ -22,6 +22,7 @@ "./vitest/*.js": "./vitest/*.ts", "./vitest/*": "./vitest/*.ts", "./vitest/*.ts": "./vitest/*.ts", + "./scripts/validateJavaBinaries.js": "./scripts/validateJavaBinaries.ts", "./oxlint.config.ts": "./oxlint.config.ts", "./vitest.config.ts": "./vitest.config.ts", "./LaraJoinPointSpecification.json": "./LaraJoinPointSpecification.json", diff --git a/Lara-JS/scripts/validateJavaBinaries.ts b/Lara-JS/scripts/validateJavaBinaries.ts new file mode 100644 index 00000000..f92014f7 --- /dev/null +++ b/Lara-JS/scripts/validateJavaBinaries.ts @@ -0,0 +1,41 @@ +import fs from "node:fs"; +import path from "node:path"; + +const remediation = + "Run this weaver's Gradle build or installDist task before packaging."; + +export function validateJavaBinaries(): void { + const javaBinariesPath = path.resolve("java-binaries"); + + if (!fs.existsSync(javaBinariesPath)) { + fail(`Java distribution is missing at '${javaBinariesPath}'.`); + } + + const javaBinariesStats = fs.lstatSync(javaBinariesPath); + if (javaBinariesStats.isSymbolicLink()) { + fail( + `Java distribution at '${javaBinariesPath}' is a symbolic link, whose contents npm will not package.`, + ); + } + + if (!javaBinariesStats.isDirectory()) { + fail(`Java distribution at '${javaBinariesPath}' is not a directory.`); + } + + const libPath = path.join(javaBinariesPath, "lib"); + if (!fs.existsSync(libPath) || !fs.lstatSync(libPath).isDirectory()) { + fail(`Java library directory is missing at '${libPath}'.`); + } + + const hasJar = fs + .readdirSync(libPath, { withFileTypes: true }) + .some((entry) => entry.isFile() && entry.name.endsWith(".jar")); + + if (!hasJar) { + fail(`Java library directory at '${libPath}' contains no JAR files.`); + } +} + +function fail(message: string): never { + throw new Error(`${message} ${remediation}`); +} diff --git a/Lara-JS/vitest/validateJavaBinaries.test.ts b/Lara-JS/vitest/validateJavaBinaries.test.ts new file mode 100644 index 00000000..49997e57 --- /dev/null +++ b/Lara-JS/vitest/validateJavaBinaries.test.ts @@ -0,0 +1,66 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { validateJavaBinaries } from "../scripts/validateJavaBinaries.ts"; + +const originalWorkingDirectory = process.cwd(); +let packageRoot: string; + +beforeEach(() => { + packageRoot = fs.mkdtempSync(path.join(os.tmpdir(), "lara-java-binaries-")); + process.chdir(packageRoot); +}); + +afterEach(() => { + process.chdir(originalWorkingDirectory); + fs.rmSync(packageRoot, { force: true, recursive: true }); +}); + +describe("validateJavaBinaries", () => { + it("rejects a missing Java distribution", () => { + expect(() => validateJavaBinaries()).toThrow("Java distribution is missing"); + }); + + it("rejects a symbolic link", () => { + const target = path.join(packageRoot, "distribution"); + fs.mkdirSync(target); + fs.symlinkSync( + target, + path.join(packageRoot, "java-binaries"), + process.platform === "win32" ? "junction" : "dir", + ); + + expect(() => validateJavaBinaries()).toThrow("is a symbolic link"); + }); + + it("rejects a non-directory distribution", () => { + fs.writeFileSync(path.join(packageRoot, "java-binaries"), "not a directory"); + + expect(() => validateJavaBinaries()).toThrow("is not a directory"); + }); + + it("rejects a missing Java library directory", () => { + fs.mkdirSync(path.join(packageRoot, "java-binaries")); + + expect(() => validateJavaBinaries()).toThrow( + "Java library directory is missing", + ); + }); + + it("rejects a Java library directory without JAR files", () => { + const libPath = path.join(packageRoot, "java-binaries", "lib"); + fs.mkdirSync(libPath, { recursive: true }); + fs.writeFileSync(path.join(libPath, "README.txt"), "not a JAR"); + + expect(() => validateJavaBinaries()).toThrow("contains no JAR files"); + }); + + it("accepts a materialized Java distribution containing JAR files", () => { + const libPath = path.join(packageRoot, "java-binaries", "lib"); + fs.mkdirSync(libPath, { recursive: true }); + fs.writeFileSync(path.join(libPath, "README.txt"), "metadata"); + fs.writeFileSync(path.join(libPath, "weaver.jar"), "JAR contents"); + + expect(() => validateJavaBinaries()).not.toThrow(); + }); +}); From 959c80d0b90cc895a879324a7af655e73d5004ce Mon Sep 17 00:00:00 2001 From: "L. Sousa" Date: Wed, 2 Sep 2026 23:40:21 +0100 Subject: [PATCH 8/9] Manual massaging of the vitest migration --- .vscode/extensions.json | 5 - .vscode/settings.json | 3 - Lara-JS/.npmrc | 1 + .../api/LegacyIntegrationTests - API.test.ts | 8 +- Lara-JS/api/lara/util/IdGenerator.test.ts | 4 + Lara-JS/api/lara/util/PrintOnce.test.ts | 6 +- Lara-JS/oxlint.config.ts | 154 ++---------------- Lara-JS/package.json | 34 +--- Lara-JS/vitest/WeaverLegacyTester.ts | 4 - Lara-JS/vitest/weaverEnvironment.ts | 37 ----- Lara-JS/vitest/weaverVitestConfig.ts | 21 +-- 11 files changed, 43 insertions(+), 234 deletions(-) delete mode 100644 .vscode/extensions.json create mode 100644 Lara-JS/.npmrc diff --git a/.vscode/extensions.json b/.vscode/extensions.json deleted file mode 100644 index 80e963be..00000000 --- a/.vscode/extensions.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "recommendations": [ - "orta.vscode-jest" - ] -} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 0235544b..63b9894c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,7 +2,4 @@ "cmake.ignoreCMakeListsMissing": true, "java.compile.nullAnalysis.mode": "automatic", "java.configuration.updateBuildConfiguration": "automatic", - "jest.runMode": { - "type": "on-demand" - }, } diff --git a/Lara-JS/.npmrc b/Lara-JS/.npmrc new file mode 100644 index 00000000..4fd02195 --- /dev/null +++ b/Lara-JS/.npmrc @@ -0,0 +1 @@ +engine-strict=true \ No newline at end of file diff --git a/Lara-JS/api/LegacyIntegrationTests - API.test.ts b/Lara-JS/api/LegacyIntegrationTests - API.test.ts index 979b2ee6..2fe17327 100644 --- a/Lara-JS/api/LegacyIntegrationTests - API.test.ts +++ b/Lara-JS/api/LegacyIntegrationTests - API.test.ts @@ -1,5 +1,6 @@ import { WeaverLegacyTester } from "../vitest/WeaverLegacyTester.ts"; import path from "path"; +import PrintOnce from "./lara/util/PrintOnce.ts"; /* oxlint-disable vitest/expect-expect */ describe("ApiTest", () => { @@ -124,7 +125,12 @@ describe("ApiTest - Util", () => { }); it("PrintOnce", async () => { - await newTester().test("PrintOnceTest.js"); + { + await newTester().test("PrintOnceTest.js"); + } + + // Clear the PrintOnce messagesSet to avoid affecting other tests + PrintOnce.messagesSet.clear(); }); it("Random", async () => { diff --git a/Lara-JS/api/lara/util/IdGenerator.test.ts b/Lara-JS/api/lara/util/IdGenerator.test.ts index da7e0191..3d2f3b31 100644 --- a/Lara-JS/api/lara/util/IdGenerator.test.ts +++ b/Lara-JS/api/lara/util/IdGenerator.test.ts @@ -1,6 +1,10 @@ import IdGenerator, { IdGeneratorClass } from "./IdGenerator.ts"; describe("IdGenerator", () => { + afterEach(() => { + IdGenerator.idCounter.clear(); + }); + describe("IdGenerator", () => { it("should generate ids with the correct sequence", () => { const id1 = IdGenerator.next("test"); diff --git a/Lara-JS/api/lara/util/PrintOnce.test.ts b/Lara-JS/api/lara/util/PrintOnce.test.ts index 36b9a0f1..8464977c 100644 --- a/Lara-JS/api/lara/util/PrintOnce.test.ts +++ b/Lara-JS/api/lara/util/PrintOnce.test.ts @@ -1,6 +1,10 @@ import PrintOnce from "./PrintOnce.ts" describe("PrintOnce", () => { + afterEach(() => { + PrintOnce.messagesSet.clear(); + }); + describe("message", () => { it("prints the string it receives ignoring duplicates", () => { const originalConsoleLog = console.log; @@ -20,5 +24,3 @@ describe("PrintOnce", () => { }); }); }); - - \ No newline at end of file diff --git a/Lara-JS/oxlint.config.ts b/Lara-JS/oxlint.config.ts index d8ddd430..b2494919 100644 --- a/Lara-JS/oxlint.config.ts +++ b/Lara-JS/oxlint.config.ts @@ -1,160 +1,38 @@ import { defineConfig } from "oxlint"; export default defineConfig({ - plugins: ["unicorn", "typescript"], + plugins: [ + "eslint", + "typescript", + "unicorn", + "oxc", + "import", + "node", + "vitest", + ], jsPlugins: ["eslint-plugin-tsdoc"], categories: { - correctness: "off", - }, - env: { - builtin: true, + correctness: "error", + perf: "warn", }, rules: { - "constructor-super": "error", - "for-direction": "error", - "getter-return": "error", - "no-async-promise-executor": "error", - "no-case-declarations": "error", - "no-class-assign": "error", - "no-compare-neg-zero": "error", - "no-cond-assign": "error", - "no-const-assign": "error", - "no-constant-binary-expression": "error", - "no-constant-condition": "error", - "no-control-regex": "error", - "no-debugger": "error", - "no-delete-var": "error", - "no-dupe-class-members": "error", - "no-dupe-else-if": "error", - "no-dupe-keys": "error", - "no-duplicate-case": "error", + "import/no-cycle": "error", + "no-array-constructor": "error", "no-empty": "error", - "no-empty-character-class": "error", - "no-empty-pattern": "error", - "no-empty-static-block": "error", - "no-ex-assign": "error", - "no-extra-boolean-cast": "error", "no-fallthrough": "error", - "no-func-assign": "error", - "no-global-assign": "error", - "no-import-assign": "error", - "no-invalid-regexp": "error", - "no-irregular-whitespace": "error", - "no-loss-of-precision": "error", - "no-misleading-character-class": "error", - "no-new-native-nonconstructor": "error", - "no-nonoctal-decimal-escape": "error", - "no-obj-calls": "error", "no-prototype-builtins": "error", "no-redeclare": "error", "no-regex-spaces": "error", - "no-self-assign": "error", - "no-setter-return": "error", - "no-shadow-restricted-names": "error", - "no-sparse-arrays": "error", - "no-this-before-super": "error", - "no-unreachable": "error", - "no-unsafe-finally": "error", - "no-unsafe-negation": "error", - "no-unsafe-optional-chaining": "error", - "no-unused-labels": "error", - "no-unused-private-class-members": "error", - "no-unused-vars": "error", - "no-useless-backreference": "error", - "no-useless-catch": "error", - "no-useless-escape": "error", - "no-with": "error", - "require-yield": "error", - "use-isnan": "error", - "valid-typeof": "error", - "no-array-constructor": "error", - "no-unused-expressions": "error", "tsdoc/syntax": "warn", "typescript/ban-ts-comment": "error", - "typescript/no-duplicate-enum-values": "error", "typescript/no-empty-object-type": "error", "typescript/no-explicit-any": "error", - "typescript/no-extra-non-null-assertion": "error", - "typescript/no-misused-new": "error", "typescript/no-namespace": "error", - "typescript/no-non-null-asserted-optional-chain": "error", "typescript/no-require-imports": "error", - "typescript/no-this-alias": "error", "typescript/no-unnecessary-type-constraint": "error", - "typescript/no-unsafe-declaration-merging": "error", "typescript/no-unsafe-function-type": "error", - "typescript/no-wrapper-object-types": "error", - "typescript/prefer-as-const": "error", - "typescript/prefer-namespace-keyword": "error", - "typescript/triple-slash-reference": "error", }, - overrides: [ - { - files: ["**/*.ts", "**/*.tsx", "**/*.mts", "**/*.cts"], - rules: { - "constructor-super": "off", - "getter-return": "off", - "no-class-assign": "off", - "no-const-assign": "off", - "no-dupe-class-members": "off", - "no-dupe-keys": "off", - "no-func-assign": "off", - "no-import-assign": "off", - "no-new-native-nonconstructor": "off", - "no-obj-calls": "off", - "no-redeclare": "off", - "no-setter-return": "off", - "no-this-before-super": "off", - "no-unreachable": "off", - "no-unsafe-negation": "off", - "no-var": "error", - "no-with": "off", - "prefer-const": "error", - "prefer-rest-params": "error", - "prefer-spread": "error", - }, - }, - { - files: ["**/*.spec.ts", "**/*.test.ts"], - plugins: ["vitest"], - rules: { - "vitest/expect-expect": "error", - "vitest/no-commented-out-tests": "error", - "vitest/no-conditional-expect": "error", - "vitest/no-disabled-tests": "warn", - "vitest/no-focused-tests": "error", - "vitest/no-identical-title": "error", - "vitest/no-import-node-test": "error", - "vitest/no-interpolation-in-snapshots": "error", - "vitest/no-mocks-import": "error", - "vitest/no-standalone-expect": "error", - "vitest/no-unneeded-async-expect-function": "error", - "vitest/prefer-called-exactly-once-with": "error", - "vitest/require-local-test-context-for-concurrent-snapshots": "error", - "vitest/valid-describe-callback": "error", - "vitest/valid-expect": "error", - "vitest/valid-expect-in-promise": "error", - "vitest/valid-title": "error", - }, - globals: { - suite: "writable", - test: "writable", - describe: "writable", - it: "writable", - expectTypeOf: "writable", - assertType: "writable", - expect: "writable", - assert: "writable", - chai: "writable", - vitest: "writable", - vi: "writable", - beforeAll: "writable", - afterAll: "writable", - beforeEach: "writable", - afterEach: "writable", - onTestFailed: "writable", - onTestFinished: "writable", - }, - }, - ], + options: { + typeAware: true, + }, }); diff --git a/Lara-JS/package.json b/Lara-JS/package.json index 6fd82387..505bf276 100644 --- a/Lara-JS/package.json +++ b/Lara-JS/package.json @@ -1,6 +1,6 @@ { "name": "@specs-feup/lara", - "version": "3.5.1", + "version": "4.0.0", "description": "A js port of the popular framework for building source-to-source compilers", "type": "module", "bin": { @@ -28,22 +28,6 @@ "./LaraJoinPointSpecification.json": "./LaraJoinPointSpecification.json", "./typedoc.base.json": "./typedoc.base.json" }, - "files": [ - "api", - "code", - "vitest", - "scripts", - ".gitignore", - "oxlint.config.ts", - "vitest.config.ts", - "LaraJoinPointSpecification.json", - "LICENSE", - "package.json", - "README.md", - "tsconfig.json", - "typedoc.base.json", - "typedoc.config.js" - ], "scripts": { "build": "tsc", "build:watch": "npm run build -- --watch", @@ -76,7 +60,8 @@ }, "homepage": "https://github.com/specs-feup/lara-framework#readme", "engines": { - "node": ">=24.0.0" + "node": ">=24.0.0", + "npm": ">=7" }, "dependencies": { "chokidar": "^5.0.0", @@ -88,21 +73,18 @@ "yargs": "^18.0.0" }, "peerDependencies": { + "@vitest/coverage-v8": "^4.1.10", "eslint-plugin-tsdoc": "^0.5.0", + "oxfmt": "^0.59.0", "oxlint": "^1.74.0", + "oxlint-tsgolint": "^7.0.2001", "vitest": "^4.1.10" }, "devDependencies": { - "@vitest/coverage-v8": "^4.1.10", "@types/debug": "^4.1.12", - "@types/node": "^25.0.0", + "@types/node": "^24.0.0", "@types/yargs": "^17.0.35", - "eslint-plugin-tsdoc": "^0.5.0", "node-notifier": "^10.0.1", - "oxfmt": "^0.59.0", - "oxlint": "^1.74.0", - "typescript": "^7.0.2", - "vite": "^8.0.0", - "vitest": "^4.1.10" + "typescript": "^7.0.2" } } diff --git a/Lara-JS/vitest/WeaverLegacyTester.ts b/Lara-JS/vitest/WeaverLegacyTester.ts index 8623e623..94c77746 100644 --- a/Lara-JS/vitest/WeaverLegacyTester.ts +++ b/Lara-JS/vitest/WeaverLegacyTester.ts @@ -2,8 +2,6 @@ import { vi } from "vitest"; import JavaTypes, { type JavaClasses, } from "@specs-feup/lara/api/lara/util/JavaTypes.ts"; -import IdGenerator from "@specs-feup/lara/api/lara/util/IdGenerator.ts"; -import PrintOnce from "@specs-feup/lara/api/lara/util/PrintOnce.ts"; import Weaver from "@specs-feup/lara/api/weaver/Weaver.ts"; import fs from "fs"; import path from "path"; @@ -22,8 +20,6 @@ afterAll(() => { ); javaWeaver.run(javaDatastore); - IdGenerator.idCounter.clear(); - PrintOnce.messagesSet.clear(); }); export class WeaverLegacyTester { diff --git a/Lara-JS/vitest/weaverEnvironment.ts b/Lara-JS/vitest/weaverEnvironment.ts index 644e5271..3776604c 100644 --- a/Lara-JS/vitest/weaverEnvironment.ts +++ b/Lara-JS/vitest/weaverEnvironment.ts @@ -5,7 +5,6 @@ import { Weaver } from "../code/Weaver.ts"; import type WeaverConfiguration from "../code/WeaverConfiguration.ts"; export interface WeaverEnvironmentOptions extends Record { - javaOptionsEnvironmentVariable?: string; weaver: WeaverConfiguration; } @@ -19,8 +18,6 @@ const environment: Environment = { const options = rawOptions as unknown as WeaverEnvironmentOptions; let weaverStarted = false; - applyJavaOptions(options.javaOptionsEnvironmentVariable); - // Vitest sends all results (including worker coverage) before it terminates // the fork. Stopping node-java any earlier also closes Vitest's IPC handle. process.once("SIGTERM", () => { @@ -58,38 +55,4 @@ const environment: Environment = { }, }; -function applyJavaOptions(environmentVariable: string | undefined): void { - if (environmentVariable === undefined) { - return; - } - - const rawJavaOptions = process.env[environmentVariable]; - if (rawJavaOptions === undefined || rawJavaOptions.trim() === "") { - return; - } - - for (const javaOption of parseJavaOptions(rawJavaOptions)) { - if (!java.options.includes(javaOption)) { - java.options.push(javaOption); - } - } -} - -function parseJavaOptions(rawJavaOptions: string): string[] { - try { - const parsed: unknown = JSON.parse(rawJavaOptions); - - if ( - Array.isArray(parsed) && - parsed.every((javaOption) => typeof javaOption === "string") - ) { - return parsed; - } - } catch { - // Fall back to whitespace splitting for ad-hoc local use. - } - - return rawJavaOptions.split(/\s+/).filter((javaOption) => javaOption !== ""); -} - export default environment; diff --git a/Lara-JS/vitest/weaverVitestConfig.ts b/Lara-JS/vitest/weaverVitestConfig.ts index 022ec572..26906056 100644 --- a/Lara-JS/vitest/weaverVitestConfig.ts +++ b/Lara-JS/vitest/weaverVitestConfig.ts @@ -1,21 +1,8 @@ import { fileURLToPath } from "node:url"; import type WeaverConfiguration from "../code/WeaverConfiguration.ts"; -import type { WeaverEnvironmentOptions } from "./weaverEnvironment.ts"; import { defineConfig } from "vitest/config"; -export interface WeaverVitestConfigOptions { - javaOptionsEnvironmentVariable?: string; -} - -export function createWeaverVitestConfig( - weaver: WeaverConfiguration, - options: WeaverVitestConfigOptions = {}, -) { - const environmentOptions: WeaverEnvironmentOptions = { - javaOptionsEnvironmentVariable: options.javaOptionsEnvironmentVariable, - weaver, - }; - +export function createWeaverVitestConfig(weaver: WeaverConfiguration) { return defineConfig({ test: { coverage: { @@ -23,10 +10,8 @@ export function createWeaverVitestConfig( provider: "v8", reporter: ["text", "lcov"], }, - environment: fileURLToPath( - new URL("./weaverEnvironment.ts", import.meta.url), - ), - environmentOptions, + environment: fileURLToPath(new URL("./weaverEnvironment.ts", import.meta.url)), + environmentOptions: { weaver }, experimental: { viteModuleRunner: false, }, From 76163b8c07ccd6d335de81bf8d0c4f3ba9d1bd4b Mon Sep 17 00:00:00 2001 From: "L. Sousa" Date: Sat, 5 Sep 2026 16:40:32 +0100 Subject: [PATCH 9/9] fix: address Vitest review findings - Fix coverage include glob: the Jest-era **/*[^.d].(t|j)s never matched any file (bare parens are not an extglob); use **/*.{t,j}s. - Remove the console.Console swap from the weaver environment: no historical rationale and nothing depends on it. Vitest requires setup to return EnvironmentReturn, so setup returns a no-op teardown. - Make the weaver environment path portable: Vitest only treats names starting with "." or "/" as file paths and resolves them with pathe, so a "/"-prefixed drive path fixes Windows (ERR_UNSUPPORTED_ESM_URL_SCHEME). Full Vitest suite verified locally and in an Ubuntu 24.04 container; CI green on all platforms. --- Lara-JS/vitest/weaverEnvironment.ts | 7 +------ Lara-JS/vitest/weaverVitestConfig.ts | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/Lara-JS/vitest/weaverEnvironment.ts b/Lara-JS/vitest/weaverEnvironment.ts index 3776604c..2c2cdd28 100644 --- a/Lara-JS/vitest/weaverEnvironment.ts +++ b/Lara-JS/vitest/weaverEnvironment.ts @@ -1,4 +1,3 @@ -import { Console } from "node:console"; import java from "java"; import type { Environment } from "vitest/environments"; import { Weaver } from "../code/Weaver.ts"; @@ -45,12 +44,8 @@ const environment: Environment = { Weaver.start(); weaverStarted = true; - global.console.Console = Console; - return { - teardown() { - delete global.console.Console; - }, + teardown() {}, }; }, }; diff --git a/Lara-JS/vitest/weaverVitestConfig.ts b/Lara-JS/vitest/weaverVitestConfig.ts index 26906056..ebd1e51e 100644 --- a/Lara-JS/vitest/weaverVitestConfig.ts +++ b/Lara-JS/vitest/weaverVitestConfig.ts @@ -2,15 +2,26 @@ import { fileURLToPath } from "node:url"; import type WeaverConfiguration from "../code/WeaverConfiguration.ts"; import { defineConfig } from "vitest/config"; +function getEnvironmentPath(url: URL): string { + const path = fileURLToPath(url).replaceAll("\\", "/"); + + // Vitest only treats environment names starting with "." or "/" as file + // paths; a Windows drive path ("C:/...") parses as a URL scheme. The + // leading "/" makes Vitest's pathe-based resolve recover the drive root. + return process.platform === "win32" ? `/${path}` : path; +} + export function createWeaverVitestConfig(weaver: WeaverConfiguration) { return defineConfig({ test: { coverage: { - include: ["**/*[^.d].(t|j)s"], + include: ["**/*.{t,j}s"], provider: "v8", reporter: ["text", "lcov"], }, - environment: fileURLToPath(new URL("./weaverEnvironment.ts", import.meta.url)), + environment: getEnvironmentPath( + new URL("./weaverEnvironment.ts", import.meta.url), + ), environmentOptions: { weaver }, experimental: { viteModuleRunner: false,