diff --git a/.vscode/extensions.json b/.vscode/extensions.json deleted file mode 100644 index 80e963be2..000000000 --- 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 0235544be..63b9894cc 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 000000000..4fd021952 --- /dev/null +++ b/Lara-JS/.npmrc @@ -0,0 +1 @@ +engine-strict=true \ No newline at end of file diff --git a/Lara-JS/api/LaraJoinPoint.ts b/Lara-JS/api/LaraJoinPoint.ts index e1d7efd31..595c9ea45 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 d51fe65b9..2fe17327d 100644 --- a/Lara-JS/api/LegacyIntegrationTests - API.test.ts +++ b/Lara-JS/api/LegacyIntegrationTests - API.test.ts @@ -1,7 +1,8 @@ -import { WeaverLegacyTester } from "../jest/WeaverLegacyTester.ts"; +import { WeaverLegacyTester } from "../vitest/WeaverLegacyTester.ts"; import path from "path"; +import PrintOnce from "./lara/util/PrintOnce.ts"; -/* eslint-disable jest/expect-expect */ +/* oxlint-disable vitest/expect-expect */ describe("ApiTest", () => { function newTester() { return new WeaverLegacyTester( @@ -29,7 +30,7 @@ describe("ApiTest", () => { await newTester().test("LaraCoreTest.js"); }); - it.failing("LocalFolder", async () => { + it.fails("LocalFolder", async () => { await newTester().test("LocalFolderTest.js"); }); @@ -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/LegacyIntegrationTests - JsEngine.test.ts b/Lara-JS/api/LegacyIntegrationTests - JsEngine.test.ts index eb1c1c565..734a0387e 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 */ +/* 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 d6810eabf..99ffe573d 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 */ +/* oxlint-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 11314baca..000000000 --- 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/lara/benchmark/BenchmarkInstance.ts b/Lara-JS/api/lara/benchmark/BenchmarkInstance.ts index bdc9e6465..c175f1c00 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 6c796a1b5..0f669f886 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 2034dca6c..25ee80f55 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 789d610fc..c6333ef86 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 6e7fbeec6..85e21d87e 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/IdGenerator.test.ts b/Lara-JS/api/lara/util/IdGenerator.test.ts index da7e0191f..3d2f3b317 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/JavaTypes.ts b/Lara-JS/api/lara/util/JavaTypes.ts index a02153b43..583125390 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/api/lara/util/PrintOnce.test.ts b/Lara-JS/api/lara/util/PrintOnce.test.ts index 36b9a0f18..8464977c6 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/code/ChildProcessHandling.test.ts b/Lara-JS/code/ChildProcessHandling.test.ts index 74af93916..cc79bfbcf 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/JavaError.ts b/Lara-JS/code/JavaError.ts index 170f188a5..9d8c13db3 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 4cf0beed0..cff954ac3 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/code/jest.config.ts b/Lara-JS/code/jest.config.ts deleted file mode 100644 index 76873b4a2..000000000 --- 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/eslint.config.ts b/Lara-JS/eslint.config.ts deleted file mode 100644 index 7df9bdcf0..000000000 --- a/Lara-JS/eslint.config.ts +++ /dev/null @@ -1,52 +0,0 @@ -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"; - -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, - }, - - languageOptions: { - parser: typescriptEslint.parser, - ecmaVersion: 5, - sourceType: "script", - - parserOptions: { - project: ["./tsconfig.json", "./tsconfig.*.json"], - tsconfigRootDir: __dirname, - }, - }, - - rules: { - "tsdoc/syntax": "warn", - }, - }, - { - ...jest.configs["flat/recommended"], - files: ["**/*.spec.ts", "**/*.test.ts"], - - plugins: { - jest, - }, - - languageOptions: { - globals: { - ...jest.environments.globals.globals, - }, - }, - }, -]); diff --git a/Lara-JS/jest.config.ts b/Lara-JS/jest.config.ts deleted file mode 100644 index 3036d5e59..000000000 --- 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 a9cde408a..000000000 --- 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 d4ae593f3..000000000 --- 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 3891ecf22..000000000 --- a/Lara-JS/jest/jestGlobalTeardown.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Weaver } from "@specs-feup/lara/code/Weaver.ts"; - -export default function () { - Weaver.shutdown(); -} diff --git a/Lara-JS/jest/setupFiles/importSideEffects.ts b/Lara-JS/jest/setupFiles/importSideEffects.ts deleted file mode 100644 index 87874f1ec..000000000 --- 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 2c0e6f0f7..000000000 --- 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/oxlint.config.ts b/Lara-JS/oxlint.config.ts new file mode 100644 index 000000000..b2494919d --- /dev/null +++ b/Lara-JS/oxlint.config.ts @@ -0,0 +1,38 @@ +import { defineConfig } from "oxlint"; + +export default defineConfig({ + plugins: [ + "eslint", + "typescript", + "unicorn", + "oxc", + "import", + "node", + "vitest", + ], + jsPlugins: ["eslint-plugin-tsdoc"], + categories: { + correctness: "error", + perf: "warn", + }, + rules: { + "import/no-cycle": "error", + "no-array-constructor": "error", + "no-empty": "error", + "no-fallthrough": "error", + "no-prototype-builtins": "error", + "no-redeclare": "error", + "no-regex-spaces": "error", + "tsdoc/syntax": "warn", + "typescript/ban-ts-comment": "error", + "typescript/no-empty-object-type": "error", + "typescript/no-explicit-any": "error", + "typescript/no-namespace": "error", + "typescript/no-require-imports": "error", + "typescript/no-unnecessary-type-constraint": "error", + "typescript/no-unsafe-function-type": "error", + }, + options: { + typeAware: true, + }, +}); diff --git a/Lara-JS/package.json b/Lara-JS/package.json index 29c947848..505bf2767 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": { @@ -17,37 +17,24 @@ "./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", - "./eslint.config.ts": "./eslint.config.ts", - "./jest.config.ts": "./jest.config.ts", + "./vitest/*.test.ts": null, + "./vitest/*.config.ts": null, + "./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", "./typedoc.base.json": "./typedoc.base.json" }, - "files": [ - "api", - "code", - "jest", - "scripts", - ".gitignore", - "eslint.config.ts", - "jest.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", - "lint": "eslint --flag unstable_native_nodejs_ts_config .", - "test": "cross-env NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" jest --detectOpenHandles --forceExit", + "lint": "oxlint .", + "format": "oxfmt .", + "format:check": "oxfmt --check .", + "test": "vitest run", "test:api": "npm run test -- api", "test:code": "npm run test -- code", "test:cov": "npm run test -- --coverage", @@ -73,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", @@ -84,21 +72,19 @@ "supports-color": "^10.2.2", "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": { - "@jest/globals": "^30.2.0", "@types/debug": "^4.1.12", - "@types/jest": "^30.0.0", - "@types/node": "^25.0.0", + "@types/node": "^24.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", - "typescript": "^6.0.3", - "typescript-eslint": "^8.52.0" + "typescript": "^7.0.2" } } diff --git a/Lara-JS/scripts/build-LaraJoinPoint.ts b/Lara-JS/scripts/build-LaraJoinPoint.ts index 4610deb20..96d9511dc 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"; diff --git a/Lara-JS/scripts/validateJavaBinaries.ts b/Lara-JS/scripts/validateJavaBinaries.ts new file mode 100644 index 000000000..f92014f73 --- /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/tsconfig.json b/Lara-JS/tsconfig.json index 45c211456..416c10933 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 000000000..d0e213922 --- /dev/null +++ b/Lara-JS/vitest.config.ts @@ -0,0 +1,15 @@ +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { createWeaverVitestConfig } from "./vitest/weaverVitestConfig.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/jest/WeaverLegacyTester.ts b/Lara-JS/vitest/WeaverLegacyTester.ts similarity index 90% rename from Lara-JS/jest/WeaverLegacyTester.ts rename to Lara-JS/vitest/WeaverLegacyTester.ts index c428a1468..94c77746c 100644 --- a/Lara-JS/jest/WeaverLegacyTester.ts +++ b/Lara-JS/vitest/WeaverLegacyTester.ts @@ -1,19 +1,22 @@ -import { jest } from "@jest/globals"; +import { vi } from "vitest"; import JavaTypes, { type JavaClasses, } from "@specs-feup/lara/api/lara/util/JavaTypes.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(); javaDatastore.set( JavaTypes.LaraiKeys.WORKSPACE_FOLDER, - JavaTypes.FileList.newInstance() + JavaTypes.FileList.newInstance(), ); javaWeaver.run(javaDatastore); @@ -128,7 +131,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 +145,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,11 +160,15 @@ export class WeaverLegacyTester { javaDatastore.set( JavaTypes.LaraiKeys.WORKSPACE_FOLDER, - JavaTypes.FileList.newInstance(javaFiles) + JavaTypes.FileList.newInstance(javaFiles), ); 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(); @@ -192,7 +199,7 @@ export class WeaverLegacyTester { "Could not find resource '" + expectedResource + "'. Actual output:\n" + - out + out, ); throw new Error("Expected outputs not found"); @@ -202,8 +209,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/validateJavaBinaries.test.ts b/Lara-JS/vitest/validateJavaBinaries.test.ts new file mode 100644 index 000000000..49997e571 --- /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(); + }); +}); diff --git a/Lara-JS/vitest/weaverEnvironment.ts b/Lara-JS/vitest/weaverEnvironment.ts new file mode 100644 index 000000000..2c2cdd285 --- /dev/null +++ b/Lara-JS/vitest/weaverEnvironment.ts @@ -0,0 +1,53 @@ +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 { + 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; + + // 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; + + return { + teardown() {}, + }; + }, +}; + +export default environment; 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 503af5e0e..e44d1af63 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); diff --git a/Lara-JS/vitest/weaverVitestConfig.ts b/Lara-JS/vitest/weaverVitestConfig.ts new file mode 100644 index 000000000..ebd1e51e1 --- /dev/null +++ b/Lara-JS/vitest/weaverVitestConfig.ts @@ -0,0 +1,36 @@ +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: ["**/*.{t,j}s"], + provider: "v8", + reporter: ["text", "lcov"], + }, + environment: getEnvironmentPath( + new URL("./weaverEnvironment.ts", import.meta.url), + ), + environmentOptions: { weaver }, + experimental: { + viteModuleRunner: false, + }, + fileParallelism: false, + globals: true, + isolate: false, + maxWorkers: 1, + pool: "forks", + }, + }); +}