From c3fcc1ae3d2f680a973e66c138d9be7ae22eee26 Mon Sep 17 00:00:00 2001 From: Kristóf Marussy Date: Sun, 9 Apr 2023 00:53:53 +0200 Subject: build: refactor frontend build * Always write ESLint output to a file in addition to the console to make the lintFrontend task cacheable in Gradle (according to the output file). * Make sure frontend task inputs are declared properly for caching. * Make sure Typescript type checking is incremental. * Do not use @tsconfig, because both Vite and SonarScanner have problems with extending tsconfig files from Yarn PnP modules. --- subprojects/frontend/.eslintrc.cjs | 3 ++ subprojects/frontend/build.gradle.kts | 75 +++++++++++++++------------- subprojects/frontend/config/eslintReport.cjs | 52 +++++++++++++++++++ subprojects/frontend/package.json | 4 +- subprojects/frontend/tsconfig.base.json | 24 +++++++-- subprojects/frontend/tsconfig.json | 1 - subprojects/frontend/tsconfig.node.json | 1 + subprojects/frontend/tsconfig.shared.json | 2 - 8 files changed, 119 insertions(+), 43 deletions(-) create mode 100644 subprojects/frontend/config/eslintReport.cjs (limited to 'subprojects/frontend') diff --git a/subprojects/frontend/.eslintrc.cjs b/subprojects/frontend/.eslintrc.cjs index 8a7b474a..dc03d721 100644 --- a/subprojects/frontend/.eslintrc.cjs +++ b/subprojects/frontend/.eslintrc.cjs @@ -90,6 +90,7 @@ module.exports = { files: [ '.eslintrc.cjs', 'config/*.ts', + 'config/*.cjs', 'prettier.config.cjs', 'vite.config.ts', ], @@ -103,6 +104,8 @@ module.exports = { 'error', { devDependencies: true }, ], + // Allow writing to the console in ad-hoc scripts. + 'no-console': 'off', // Access to the environment in configuration files. 'no-process-env': 'off', }, diff --git a/subprojects/frontend/build.gradle.kts b/subprojects/frontend/build.gradle.kts index 4a51c74e..8d2a8631 100644 --- a/subprojects/frontend/build.gradle.kts +++ b/subprojects/frontend/build.gradle.kts @@ -18,15 +18,45 @@ val productionAssets: Configuration by configurations.creating { isCanBeResolved = false } -val sourcesWithoutTypeGen = fileTree("src") { +val sourcesWithoutTypes = fileTree("src") { exclude("**/*.typegen.ts") } +val sourcesWithTypes = fileTree("src") + fileTree("types") + +val buildScripts = fileTree("config") + files( + ".eslintrc.cjs", + "prettier.config.cjs", + "vite.config.ts", +) + +val installationState = files( + rootProject.file("yarn.lock"), + rootProject.file("package.json"), + "package.json", +) + +val sharedConfigFiles = installationState + files( + "tsconfig.json", + "tsconfig.base.json", + "tsconfig.node.json", + "tsconfig.shared.json", +) + +val assembleConfigFiles = sharedConfigFiles + file("vite.config.ts") + fileTree("config") { + include("**/*.ts") +} + +val assembleSources = sourcesWithTypes + fileTree("public") + file("index.html") + +val assembleFiles = assembleSources + assembleConfigFiles + +val lintingFiles = sourcesWithTypes + buildScripts + sharedConfigFiles + val generateXStateTypes by tasks.registering(RunYarn::class) { dependsOn(tasks.installFrontend) - inputs.files(sourcesWithoutTypeGen) - inputs.file("package.json") - inputs.file(rootProject.file("yarn.lock")) + inputs.files(sourcesWithoutTypes) + inputs.files(installationState) outputs.dir("src") script.set("run typegen") description = "Generate TypeScript typings for XState state machines." @@ -34,11 +64,7 @@ val generateXStateTypes by tasks.registering(RunYarn::class) { tasks.assembleFrontend { dependsOn(generateXStateTypes) - inputs.dir("public") - inputs.files(sourcesWithoutTypeGen) - inputs.file("index.html") - inputs.files("package.json", "tsconfig.json", "tsconfig.base.json", "vite.config.ts") - inputs.file(rootProject.file("yarn.lock")) + inputs.files(assembleFiles) outputs.dir(productionResources) } @@ -51,10 +77,7 @@ artifacts { val typeCheckFrontend by tasks.registering(RunYarn::class) { dependsOn(tasks.installFrontend) dependsOn(generateXStateTypes) - inputs.dir("src") - inputs.dir("types") - inputs.files("package.json", "tsconfig.json", "tsconfig.base.json", "tsconfig.node.json") - inputs.file(rootProject.file("yarn.lock")) + inputs.files(lintingFiles) outputs.dir("$buildDir/typescript") script.set("run typecheck") group = "verification" @@ -65,17 +88,9 @@ val lintFrontend by tasks.registering(RunYarn::class) { dependsOn(tasks.installFrontend) dependsOn(generateXStateTypes) dependsOn(typeCheckFrontend) - inputs.dir("src") - inputs.dir("types") - inputs.files(".eslintrc.cjs", "prettier.config.cjs") - inputs.files("package.json", "tsconfig.json", "tsconfig.base.json", "tsconfig.node.json") - inputs.file(rootProject.file("yarn.lock")) - if (project.hasProperty("ci")) { - outputs.file("$buildDir/eslint.json") - script.set("run lint:ci") - } else { - script.set("run lint") - } + inputs.files(lintingFiles) + outputs.file("$buildDir/eslint.json") + script.set("run lint") group = "verification" description = "Check for TypeScript lint errors and warnings." } @@ -84,11 +99,7 @@ val fixFrontend by tasks.registering(RunYarn::class) { dependsOn(tasks.installFrontend) dependsOn(generateXStateTypes) dependsOn(typeCheckFrontend) - inputs.dir("src") - inputs.dir("types") - inputs.files(".eslintrc.cjs", "prettier.config.cjs") - inputs.files("package.json", "tsconfig.json", "tsconfig.base.json", "tsconfig.node.json") - inputs.file(rootProject.file("yarn.lock")) + inputs.files(lintingFiles) script.set("run lint:fix") group = "verification" description = "Fix TypeScript lint errors and warnings." @@ -102,11 +113,7 @@ tasks.check { tasks.register("serveFrontend", RunYarn::class) { dependsOn(tasks.installFrontend) dependsOn(generateXStateTypes) - inputs.dir("public") - inputs.files(sourcesWithoutTypeGen) - inputs.file("index.html") - inputs.files("package.json", "tsconfig.json", "tsconfig.base.json", "vite.config.ts") - inputs.file(rootProject.file("yarn.lock")) + inputs.files(assembleFiles) outputs.dir("$viteOutputDir/development") script.set("run serve") group = "run" diff --git a/subprojects/frontend/config/eslintReport.cjs b/subprojects/frontend/config/eslintReport.cjs new file mode 100644 index 00000000..5bf6a041 --- /dev/null +++ b/subprojects/frontend/config/eslintReport.cjs @@ -0,0 +1,52 @@ +const { writeFile } = require('node:fs/promises'); +const path = require('node:path'); +const { Readable } = require('node:stream'); +const { pipeline } = require('node:stream/promises'); + +const { ESLint } = require('eslint'); + +const rootDir = path.join(__dirname, '..'); + +/** + * Write ESLint report to console. + * + * @param cli {import('eslint').ESLint} The ESLint CLI. + * @param report {import('eslint').ESLint.LintResult[]} The ESLint report. + * @return {Promise} A promise that resolves when the report is finished. + */ +async function reportToConsole(cli, report) { + const stylishFormatter = await cli.loadFormatter('stylish'); + const output = new Readable(); + output.push(await stylishFormatter.format(report)); + output.push(null); + return pipeline(output, process.stdout); +} + +/** + * Write ESLint report to the build directory. + * + * @param cli {import('eslint').ESLint} The ESLint CLI. + * @param report {import('eslint').ESLint.LintResult[]} The ESLint report. + * @return {Promise} A promise that resolves when the report is finished. + */ +async function reportToJson(cli, report) { + const jsonFormatter = await cli.loadFormatter('json'); + const json = await jsonFormatter.format(report); + const reportPath = path.join(rootDir, 'build', 'eslint.json'); + return writeFile(reportPath, json, 'utf-8'); +} + +async function createReport() { + const cli = new ESLint({ + useEslintrc: true, + cwd: rootDir, + }); + const report = await cli.lintFiles('.'); + await Promise.all([reportToConsole(cli, report), reportToJson(cli, report)]); + + if (report.some((entry) => entry.errorCount > 0)) { + process.exitCode = 1; + } +} + +createReport().catch(console.error); diff --git a/subprojects/frontend/package.json b/subprojects/frontend/package.json index 8b4a3f71..e6bcc89e 100644 --- a/subprojects/frontend/package.json +++ b/subprojects/frontend/package.json @@ -9,8 +9,7 @@ "serve": "cross-env MODE=development vite serve", "typegen": "xstate typegen \"src/**/*.ts?(x)\"", "typecheck": "tsc -p tsconfig.shared.json && tsc -p tsconfig.node.json && tsc -p tsconfig.json", - "lint": "eslint .", - "lint:ci": "eslint -f json -o build/eslint.json .", + "lint": "node config/eslintReport.cjs", "lint:fix": "yarn run lint --fix" }, "repository": { @@ -60,7 +59,6 @@ }, "devDependencies": { "@lezer/generator": "^1.2.2", - "@tsconfig/strictest": "^2.0.0", "@types/eslint": "^8.37.0", "@types/html-minifier-terser": "^7.0.0", "@types/lodash-es": "^4.17.7", diff --git a/subprojects/frontend/tsconfig.base.json b/subprojects/frontend/tsconfig.base.json index b960e93c..30e707ae 100644 --- a/subprojects/frontend/tsconfig.base.json +++ b/subprojects/frontend/tsconfig.base.json @@ -1,10 +1,28 @@ { - "extends": "@tsconfig/strictest", "compilerOptions": { - "useDefineForClassFields": true, + "strict": true, + "allowUnusedLabels": false, + "allowUnreachableCode": false, + "exactOptionalPropertyTypes": true, + "noFallthroughCasesInSwitch": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noPropertyAccessFromIndexSignature": true, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": true, + "noUnusedParameters": true, "verbatimModuleSyntax": false, "isolatedModules": true, + "checkJs": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "useDefineForClassFields": true, "module": "es2022", - "moduleResolution": "node" + "moduleResolution": "node", + "incremental": true, + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "build/typescript" } } diff --git a/subprojects/frontend/tsconfig.json b/subprojects/frontend/tsconfig.json index 35abd789..35d0d164 100644 --- a/subprojects/frontend/tsconfig.json +++ b/subprojects/frontend/tsconfig.json @@ -2,7 +2,6 @@ "extends": "./tsconfig.base.json", "compilerOptions": { "jsx": "react-jsx", - "noEmit": true, "lib": ["DOM", "DOM.Iterable", "ES2022"], "types": ["vite/client", "vite-plugin-pwa/client"] }, diff --git a/subprojects/frontend/tsconfig.node.json b/subprojects/frontend/tsconfig.node.json index f4908bcb..cfa2da13 100644 --- a/subprojects/frontend/tsconfig.node.json +++ b/subprojects/frontend/tsconfig.node.json @@ -10,6 +10,7 @@ "include": [ ".eslintrc.cjs", "config/*.ts", + "config/*.cjs", "prettier.config.cjs", "types/node", "vite.config.ts" diff --git a/subprojects/frontend/tsconfig.shared.json b/subprojects/frontend/tsconfig.shared.json index b7e1de55..f7b56a1d 100644 --- a/subprojects/frontend/tsconfig.shared.json +++ b/subprojects/frontend/tsconfig.shared.json @@ -4,8 +4,6 @@ "composite": true, "lib": ["ES2022"], "types": [], - "emitDeclarationOnly": true, - "outDir": "build/typescript" }, "include": [ "src/xtext/BackendConfig.ts", -- cgit v1.2.3-54-g00ecf