From 763be3492725057208375e7ca78be70ed89752e2 Mon Sep 17 00:00:00 2001 From: Deep <50181-dDeepLb@users.noreply.gitgud.io> Date: Wed, 3 Jun 2026 02:45:23 +0300 Subject: [PATCH] CI: Pretty parallel CI --- .gitignore | 1 + .gitlab-ci.yml | 9 +- BondageClub/Tools/Node/Checks.js | 201 +++++++++ BondageClub/Tools/Node/ChecksFormat.js | 392 ++++++++++++++++++ BondageClub/Tools/Node/Common.js | 2 +- .../Tools/Node/tsconfig-assetcheck.json | 2 + .../Tools/Node/tsconfig-csvtypescript.json | 2 + .../Node/tsconfig-typecheckexpensive.json | 2 + BondageClub/package.json | 27 +- BondageClub/tsconfig.json | 2 + 10 files changed, 627 insertions(+), 13 deletions(-) create mode 100644 BondageClub/Tools/Node/Checks.js create mode 100644 BondageClub/Tools/Node/ChecksFormat.js diff --git a/.gitignore b/.gitignore index 37f985a109..c2c1dc6724 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ .DS_Store node_modules +BondageClub/.cache/ web.config /BondageClub/Tools/Node/.imagemin-cache BondageClub/1 - prepare.bat diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 9aeb6b0b31..3232e2907b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -6,6 +6,13 @@ check: only: - merge_requests image: node:20 + cache: + key: + files: + - BondageClub/package-lock.json + paths: + - BondageClub/node_modules/ + - BondageClub/.cache/ script: - - npm --prefix $NPM_PREFIX ci + - npm --prefix $NPM_PREFIX ci --no-fund --no-audit --loglevel=error - npm --prefix $NPM_PREFIX run checks diff --git a/BondageClub/Tools/Node/Checks.js b/BondageClub/Tools/Node/Checks.js new file mode 100644 index 0000000000..ff3ee7b362 --- /dev/null +++ b/BondageClub/Tools/Node/Checks.js @@ -0,0 +1,201 @@ +import { spawn } from "node:child_process"; +import { fileURLToPath } from "node:url"; +import path from "node:path"; +import { + colorizeOutput, + formatDivider, + formatFailureHeading, + formatRunningHeader, + formatStatusLine, + formatSummaryCounts, + formatWarningHeading, + outputHasWarnings, +} from "./ChecksFormat.js"; + +// #region Configuration + +const ROOT = path.resolve(fileURLToPath(import.meta.url), "../.."); + +/** @typedef {{ name: string, code: number, output: string, duration?: number }} CheckResult */ + +/** @type {readonly string[]} */ +const CHEAP_CHECKS = [ + "assets:typecheck", + "assets:check", + "assets:prettier", + "assets:eslint", + "scripts:typecheck:tsc", + "scripts:typecheck:strict", + "scripts:lint", + "files:case", + "files:csvtypescript", +]; + +/** @type {readonly string[]} */ +const FULL_CHECKS = [ + ...CHEAP_CHECKS, + "scripts:typecheck-expensive", + "styles:prettier", +]; + +// #endregion + +// #region Check execution + +/** + * Creates a child process, buffering the output and resolving with the result. + * @param {string} name + * @returns {Promise} + */ +function runCheck(name) { + return new Promise((resolve) => { + /** @type {string[]} */ + const output = []; + const npmExecPath = process.env.npm_execpath; + const useNodeForNpm = typeof npmExecPath === "string" && /\.m?js$/i.test(npmExecPath); + const command = useNodeForNpm ? process.execPath : "npm"; + /** @type {string[]} */ + const args = useNodeForNpm + ? [npmExecPath, "run", "--silent", name] + : ["run", "--silent", name]; + + const env = { ...process.env }; + if (name === "scripts:typecheck:strict") { + // kill tsc-strict spinner + env.CI = "1"; + } + + const startTime = performance.now(); + const child = spawn(command, args, { + cwd: ROOT, + env, + stdio: ["ignore", "pipe", "pipe"], + }); + + child.stdout.setEncoding("utf8"); + child.stderr.setEncoding("utf8"); + child.stdout.on("data", (chunk) => output.push(chunk)); + child.stderr.on("data", (chunk) => output.push(chunk)); + + child.on("close", (code) => { + resolve({ + name, + code: code ?? 1, + output: output.join("").trimEnd(), + duration: performance.now() - startTime, + }); + }); + + child.on("error", (err) => { + resolve({ + name, + code: 1, + output: String(err), + duration: performance.now() - startTime, + }); + }); + }); +} + +/** + * Runs all checks in parallel and returns the results. + * @param {readonly string[]} names + * @returns {Promise} + */ +async function runChecks(names) { + /** @type {Map} */ + const resultsByName = new Map(); + + await Promise.all( + names.map(async (name) => { + resultsByName.set(name, await runCheck(name)); + }), + ); + + return names.map((name) => resultsByName.get(name) ?? { + name, + code: 1, + output: "Check did not run", + }); +} + +// #endregion + +// #region Console output + +/** + * Prints the failure output for a check result. + * @param {CheckResult} result + */ +function printFailureOutput(result) { + console.log("\u200B"); + console.log(formatFailureHeading(result.name)); + console.log(colorizeOutput(result.output)); +} + +/** + * Prints the warning output for a check that passed but reported warnings. + * @param {CheckResult} result + */ +function printWarningOutput(result) { + console.log("\u200B"); + console.log(formatWarningHeading(result.name)); + console.log(colorizeOutput(result.output)); +} + +/** + * Prints the summary of the check results. + * @param {readonly CheckResult[]} results + */ +function printSummary(results) { + const failed = results.filter((result) => result.code !== 0); + const passed = results.length - failed.length; + + console.log("\u200B"); + console.log(formatDivider()); + console.log(formatSummaryCounts(passed, failed.length)); + console.log("\u200B"); + + for (const result of results) { + console.log(formatStatusLine(result)); + } + + console.log(formatDivider()); +} + +// #endregion + +// #region Entry point + +/** + * @param {readonly string[]} names + * @param {boolean} silent + * @returns {Promise} + */ +async function main(names, silent) { + if (!silent) { + console.log(formatRunningHeader(names.length)); + } + + const results = await runChecks(names); + + for (const result of results) { + if (result.code !== 0) { + printFailureOutput(result); + } else if (outputHasWarnings(result.output)) { + printWarningOutput(result); + } + } + + if (!silent) { + printSummary(results); + } + + return results.some((result) => result.code !== 0) ? 1 : 0; +} + +const cheap = process.argv.includes("--cheap") || process.argv.includes("-c"); +const isSilent = process.argv.includes("--silent") || process.argv.includes("-s"); +process.exitCode = await main(cheap ? CHEAP_CHECKS : FULL_CHECKS, isSilent); + +// #endregion diff --git a/BondageClub/Tools/Node/ChecksFormat.js b/BondageClub/Tools/Node/ChecksFormat.js new file mode 100644 index 0000000000..1b1bd34d59 --- /dev/null +++ b/BondageClub/Tools/Node/ChecksFormat.js @@ -0,0 +1,392 @@ +import colors from "ansi-colors"; + +/** @typedef {{ name: string, code: number, output: string, duration?: number }} CheckResult */ + +// #region Patterns + +/** @type {RegExp} */ +const QUOTED_STRING_RE = /"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/g; + +/** @type {RegExp} */ +const FILE_PATH_RE = + /(?:\.\.?\/|\/|[A-Za-z]:[\\/])?[\w./\\-]+\.(?:js|ts|tsx|mts|cts|css|png|csv|json|md|yml|yaml|txt|html|svg|jpe?g|gif|webp)\b/g; + +const DIVIDER = "─".repeat(60); + +/** Placeholder byte used while stashing quoted strings during colorization. */ +const STASH = String.fromCharCode(0); + +// #endregion + +// #region Primitives + +/** + * Returns whether the output has color. + * @param {string} output + * @returns {boolean} + */ +export function outputHasColor(output) { + return output.includes("\x1b["); +} + +/** + * Returns the style for a severity. + * @param {string} severity + * @returns {(value: string) => string} + */ +function severityStyle(severity) { + return severity.toLowerCase() === "error" ? colors.red : colors.yellow; +} + +/** + * Formats the duration of a check. + * @param {number | undefined} durationMs + * @returns {string} + */ +export function formatDuration(durationMs) { + if (durationMs === undefined) { + return colors.gray("(—)"); + } + return colors.gray(`(${durationMs.toFixed(0)}ms)`); +} + +/** + * Temporarily replaces quoted strings so other patterns do not match inside them. + * @param {string} text + * @returns {{ text: string, restore: (value: string) => string }} + */ +function stashQuotedStrings(text) { + /** @type {string[]} */ + const quoted = []; + const stashed = text.replace(QUOTED_STRING_RE, (match) => { + quoted.push(match); + return `${STASH}${quoted.length - 1}${STASH}`; + }); + return { + text: stashed, + restore: (value) => + value.replace(new RegExp(`${STASH}(\\d+)${STASH}`, "g"), (_, index) => colors.green(quoted[Number(index)] ?? "")), + }; +} + +/** + * Colorizes the output of an identifier. + * @param {string} part + * @returns {string} + */ +function colorizeIdentifier(part) { + return part === "null" ? colors.dim("null") : colors.cyan(part); +} + +/** + * Applies generic inline highlights to plain text. + * @param {string} text + * @returns {string} + */ +function colorizeInline(text) { + const { text: stashed, restore } = stashQuotedStrings(text); + let result = stashed.replace(FILE_PATH_RE, (match) => colors.cyan(match)); + result = result.replace(/\b(TS\d+)\b/g, (_, code) => colors.red.bold(code)); + result = result.replace(/\bnull\b/g, (match) => colors.dim(match)); + return restore(result); +} + +/** + * Colorizes the output of a prefixed line. + * @param {string} line + * @param {string} prefix + * @param {(value: string) => string} stylePrefix + * @returns {string} + */ +function colorizePrefixedLine(line, prefix, stylePrefix) { + const match = line.match(/^(\s*)(.+)$/); + if (!match) { + return line; + } + + const [, indent, body] = match; + const prefixMatch = body.match(new RegExp(`^(${prefix})(\\s*)(.*)$`, "i")); + if (!prefixMatch) { + return line; + } + + const [, label, spacing, rest] = prefixMatch; + return `${indent}${stylePrefix(label)}${spacing}${colorizeInline(rest)}`; +} + +// #endregion + +// #region Tool-specific line formatters + +/** + * Colorizes the output of an AssetCheck error line. + * @param {string} line + * @returns {string} + */ +function colorizeAssetErrorLine(line) { + const match = line.match( + /^(\s*)ERROR:\s*(.+):\s*(Missing asset)\s+("(?:[^"\\]|\\.)*")\s*$/i, + ); + if (!match) { + return colorizePrefixedLine(line, "ERROR:", colors.red.bold); + } + + const [, indent, ids, label, quotedPath] = match; + const coloredIds = ids.split(":").map(colorizeIdentifier).join(colors.dim(":")); + return `${indent}${colors.red.bold("ERROR:")} ${coloredIds}${colors.dim(":")} ${colors.dim(label)} ${colors.green(quotedPath)}`; +} + +/** + * Colorizes the output of a TypeScript check result. + * @param {string} line + * @returns {string} + */ +function colorizeTypeScriptLine(line) { + const parenMatch = line.match(/^(.+?\(\d+,\d+\)):\s*(error|warning)\s+(TS\d+):\s*(.*)$/i); + if (parenMatch) { + const [, location, severity, code, message] = parenMatch; + const style = severityStyle(severity); + return `${colors.cyan(location)}: ${style(severity)} ${colors.red.bold(code)}: ${colorizeInline(message)}`; + } + + const colonMatch = line.match(/^(.+?):(\d+):(\d+)\s*-\s*(error|warning)\s+(TS\d+):\s*(.*)$/i); + if (colonMatch) { + const [, file, lineNo, column, severity, code, message] = colonMatch; + const style = severityStyle(severity); + return `${colors.cyan(file)}:${colors.yellow(`${lineNo}:${column}`)} - ${style(severity)} ${colors.red.bold(code)}: ${colorizeInline(message)}`; + } + + if (/error TS\d+:/i.test(line)) { + return colorizePrefixedLine(line, "error TS\\d+:", colors.red); + } + + return line; +} + +/** + * Colorizes the output of an ESLint check result. + * @param {string} line + * @returns {string} + */ +function colorizeEslintLine(line) { + const detailMatch = line.match( + /^(\s*)(\d+):(\d+)\s+(error|warning)\s+(.+?)\s+([@\w-]+(?:\/[\w-]+)?)\s*$/, + ); + if (detailMatch) { + const [, indent, lineNo, column, severity, message, rule] = detailMatch; + const style = severityStyle(severity); + return `${indent}${colors.yellow(`${lineNo}:${column}`)} ${style(severity)} ${colorizeInline(message)} ${colors.magenta.dim(rule)}`; + } + + const trimmed = line.trim(); + if (/^(?:\.\.?\/|\/|[A-Za-z]:)/.test(trimmed) && FILE_PATH_RE.test(trimmed)) { + return colors.cyan.underline(trimmed); + } + + return line; +} + +/** + * Colorizes the output of a Prettier check result. + * @param {string} line + * @returns {string} + */ +function colorizePrettierLine(line) { + const match = line.match(/^(\[(?:warn|error)\])\s+(.+)$/i); + if (match) { + const [, tag, file] = match; + const tagColor = /error/i.test(tag) ? colors.red : colors.yellow; + return `${tagColor(tag)} ${colors.cyan(file)}`; + } + + if (/^Checking formatting\.\.\.$/i.test(line.trim())) { + return colors.dim(line); + } + + return line; +} + +// #endregion + +// #region Output colorization + +/** + * Colorizes a single line of check output. + * @param {string} line + * @returns {string} + */ +function colorizeLine(line) { + // we don't want to colorize colored output (e.g. AssetCheck, FileCase) + if (outputHasColor(line)) { + return line; + } + + if (/^\s*ERROR:/i.test(line)) { + return colorizeAssetErrorLine(line); + } + + if (/^\s*(WARNING|WARN):/i.test(line)) { + return colorizePrefixedLine(line, "(WARNING|WARN):", colors.yellow.bold); + } + + if (/\berror TS\d+:/i.test(line) || /\(\d+,\d+\):/.test(line)) { + const colored = colorizeTypeScriptLine(line); + if (colored !== line) { + return colored; + } + } + + if (/^\s*\d+:\d+\s+(error|warning)\s+/i.test(line)) { + const colored = colorizeEslintLine(line); + if (colored !== line) { + return colored; + } + } + + if (/^\[(?:warn|error)\]/i.test(line.trim()) || /^Checking formatting/i.test(line.trim())) { + const colored = colorizePrettierLine(line); + if (colored !== line) { + return colored; + } + } + + const trimmed = line.trim(); + if (trimmed && FILE_PATH_RE.test(trimmed) && !/[:]\s/.test(trimmed)) { + return colors.cyan.underline(trimmed); + } + + return colorizeInline(line); +} + +/** + * Colorizes the output of a check result. + * @param {string} output + * @returns {string} + */ +export function colorizeOutput(output) { + // not sure if this can happen, but anyway + if (!output) { + return colors.dim("(no output)"); + } + + return output.split("\n").map(colorizeLine).join("\n"); +} + +/** + * Returns whether a line of check output is a warning (not an error). + * @param {string} line + * @returns {boolean} + */ +export function lineHasWarning(line) { + if (!line.trim()) { + return false; + } + + if (/^\s*(WARNING|WARN):/i.test(line)) { + return true; + } + + if (/:\s*warning\s+TS\d+:/i.test(line) || / - warning TS\d+:/i.test(line)) { + return true; + } + + if (/^\s*\d+:\d+\s+warning\s+/i.test(line)) { + return true; + } + + if (/^\[warn\]/i.test(line.trim())) { + return true; + } + + if (/\(\d+ errors?, \d+ warnings?\)/i.test(line) || /^\s*✖.*\b\d+\s+warning/i.test(line)) { + return true; + } + + return false; +} + +/** + * Returns whether check output contains any warnings. + * @param {string} output + * @returns {boolean} + */ +export function outputHasWarnings(output) { + return output.split("\n").some(lineHasWarning); +} + +// #endregion + +// #region Summary & status formatting + +/** + * Formats the name of a check. + * @param {string} name + * @returns {string} + */ +export function formatCheckName(name) { + const colon = name.indexOf(":"); + if (colon === -1) { + return colors.bold(name); + } + return colors.dim(name.slice(0, colon + 1)) + colors.bold(name.slice(colon + 1)); +} + +/** + * Formats the status line for a check result. + * @param {CheckResult} result + * @returns {string} + */ +export function formatStatusLine(result) { + /** @type {string} */ + const icon = result.code !== 0 + ? colors.red("✗") + : outputHasWarnings(result.output) + ? colors.yellow("⚠") + : colors.green("✓"); + return ` ${icon} ${formatCheckName(result.name)} ${formatDuration(result.duration)}`; +} + +/** + * Formats the summary counts for the check results. + * @param {number} passed + * @param {number} failed + * @returns {string} + */ +export function formatSummaryCounts(passed, failed) { + const failedText = failed ? colors.red(String(failed)) : colors.green("0"); + return colors.bold(`Summary: ${colors.green(String(passed))} passed, ${failedText} failed`); +} + +/** + * Formats the header for the running checks. + * @param {number} count + * @returns {string} + */ +export function formatRunningHeader(count) { + return colors.bold(`Running ${colors.cyan(String(count))} checks…`); +} + +/** + * Formats the heading for a failed check. + * @param {string} name + * @returns {string} + */ +export function formatFailureHeading(name) { + return colors.red.bold.underline(name); +} + +/** + * Formats the heading for a passed check that reported warnings. + * @param {string} name + * @returns {string} + */ +export function formatWarningHeading(name) { + return colors.yellow.bold.underline(name); +} + +/** @returns {string} */ +export function formatDivider() { + return colors.bold(DIVIDER); +} + +// #endregion diff --git a/BondageClub/Tools/Node/Common.js b/BondageClub/Tools/Node/Common.js index 0779beb5d4..fc6c128224 100644 --- a/BondageClub/Tools/Node/Common.js +++ b/BondageClub/Tools/Node/Common.js @@ -108,7 +108,7 @@ export const errorState = { * @param {string} text The error */ export function error(text) { - console.log("ERROR:", text); + console.error("ERROR:", text); process.exitCode = 1; errorState.local = true; errorState.global = true; diff --git a/BondageClub/Tools/Node/tsconfig-assetcheck.json b/BondageClub/Tools/Node/tsconfig-assetcheck.json index d52dbac3bb..8d5bc88e7c 100644 --- a/BondageClub/Tools/Node/tsconfig-assetcheck.json +++ b/BondageClub/Tools/Node/tsconfig-assetcheck.json @@ -1,6 +1,8 @@ { "compilerOptions": { "noEmit": true, + "incremental": true, + "tsBuildInfoFile": "../../.cache/tsc-asset.tsbuildinfo", "module": "commonjs", "target": "ES2021", "lib": ["es2021", "dom", "DOM.Iterable"], diff --git a/BondageClub/Tools/Node/tsconfig-csvtypescript.json b/BondageClub/Tools/Node/tsconfig-csvtypescript.json index 84cf1f44a7..bc122324ad 100644 --- a/BondageClub/Tools/Node/tsconfig-csvtypescript.json +++ b/BondageClub/Tools/Node/tsconfig-csvtypescript.json @@ -8,6 +8,8 @@ "types": [], "strict": false, "skipLibCheck": true, + "incremental": true, + "tsBuildInfoFile": "../../.cache/tsc-csvtypescript.tsbuildinfo", "plugins": [ { "name": "typescript-strict-plugin" diff --git a/BondageClub/Tools/Node/tsconfig-typecheckexpensive.json b/BondageClub/Tools/Node/tsconfig-typecheckexpensive.json index fe840def7f..b5072c2b2d 100644 --- a/BondageClub/Tools/Node/tsconfig-typecheckexpensive.json +++ b/BondageClub/Tools/Node/tsconfig-typecheckexpensive.json @@ -1,6 +1,8 @@ { "compilerOptions": { "noEmit": true, + "incremental": true, + "tsBuildInfoFile": "../../.cache/tsc-expensive.tsbuildinfo", "module": "commonjs", "target": "ES2021", "lib": ["es2021", "dom", "DOM.Iterable"], diff --git a/BondageClub/package.json b/BondageClub/package.json index 5b4831d38a..ed5996da34 100644 --- a/BondageClub/package.json +++ b/BondageClub/package.json @@ -8,22 +8,27 @@ "scripts": { "changelog:generate": "cd Tools/Node && node GenerateChangelog generate", "changelog:prepare": "cd Tools/Node && node GenerateChangelog prepare", - "checks": "npm-run-all -c assets:typecheck scripts:typecheck scripts:typecheck-expensive assets:check scripts:lint styles:prettier assets:lint files:case files:csvtypescript", - "checks:cheap": "npm-run-all -c assets:typecheck scripts:typecheck assets:check scripts:lint assets:lint files:case files:csvtypescript", - "checks:expensive": "npm-run-all -c checks", + "checks": "node --unhandled-rejections=strict Tools/Node/Checks.js", + "checks:cheap": "node --unhandled-rejections=strict Tools/Node/Checks.js --cheap", + "checks:expensive": "node --unhandled-rejections=strict Tools/Node/Checks.js", + "assets:checks": "run-p -cs --aggregate-output assets:typecheck assets:check assets:lint", + "assets:typecheck-check": "run-p -cs --aggregate-output assets:typecheck assets:check", "assets:clean": "gulp --gulpfile Tools/Node/gulpfile.js clean", "assets:minify": "gulp --gulpfile Tools/Node/gulpfile.js assetMinify --max-old-space-size=8192", - "assets:prettier": "prettier --write Assets/Female3DCG/Female3DCG.js Assets/Female3DCG/Female3DCGExtended.js && git diff --exit-code Assets/Female3DCG/Female3DCG.js Assets/Female3DCG/Female3DCGExtended.js", - "assets:eslint": "eslint Assets/Female3DCG/Female3DCG.js Assets/Female3DCG/Female3DCGExtended.js", - "assets:lint": "npm run assets:prettier && npm run assets:eslint", + "assets:prettier": "prettier --check Assets/Female3DCG/Female3DCG.js Assets/Female3DCG/Female3DCGExtended.js", + "assets:prettier:fix": "prettier --write Assets/Female3DCG/Female3DCG.js Assets/Female3DCG/Female3DCGExtended.js", + "assets:eslint": "eslint --cache --cache-location .cache/eslint Assets/Female3DCG/Female3DCG.js Assets/Female3DCG/Female3DCGExtended.js", + "assets:lint": "run-s -cn assets:prettier assets:eslint", "assets:lint:fix": "prettier-eslint --write Assets/Female3DCG/Female3DCG.js Assets/Female3DCG/Female3DCGExtended.js", "assets:check": "cd Tools/Node && node --unhandled-rejections=strict AssetCheck", - "assets:typecheck": "cd ../ && tsc -p BondageClub/Tools/Node/tsconfig-assetcheck.json", + "assets:typecheck": "tsc -p Tools/Node/tsconfig-assetcheck.json", "assets:posemapping": "cd Tools/Node && node --unhandled-rejections=strict PoseMapping", - "scripts:lint": "eslint \"Scripts/**/*.js\" \"Screens/**/*.js\" \"Tools/**/*.js\" \"Backgrounds/Backgrounds.js\"", - "scripts:lint:fix": "eslint --fix \"Scripts/**/*.js\" \"Screens/**/*.js\" \"Tools/**/*.js\" \"Backgrounds/Backgrounds.js\"", - "scripts:typecheck": "cd ../ && tsc -p BondageClub/tsconfig.json && tsc-strict -p BondageClub/tsconfig.json", - "scripts:typecheck-expensive": "cd ../ && tsc -p BondageClub/Tools/Node/tsconfig-typecheckexpensive.json", + "scripts:lint": "eslint --cache --cache-location .cache/eslint \"Scripts/**/*.js\" \"Screens/**/*.js\" \"Tools/**/*.js\" \"Backgrounds/Backgrounds.js\"", + "scripts:lint:fix": "eslint --cache --cache-location .cache/eslint --fix \"Scripts/**/*.js\" \"Screens/**/*.js\" \"Tools/**/*.js\" \"Backgrounds/Backgrounds.js\"", + "scripts:typecheck": "run-p -cs --aggregate-output scripts:typecheck:tsc scripts:typecheck:strict", + "scripts:typecheck:tsc": "tsc -p tsconfig.json", + "scripts:typecheck:strict": "CI=1 tsc-strict -p tsconfig.json", + "scripts:typecheck-expensive": "tsc -p Tools/Node/tsconfig-typecheckexpensive.json", "styles:prettier": "prettier --check --print-width 120 CSS/", "styles:prettier:fix": "prettier --write --print-width 120 CSS/", "files:case": "cd Tools/Node && node --unhandled-rejections=strict FileCase", diff --git a/BondageClub/tsconfig.json b/BondageClub/tsconfig.json index 6b09218b57..73c0216cb5 100644 --- a/BondageClub/tsconfig.json +++ b/BondageClub/tsconfig.json @@ -5,6 +5,8 @@ "lib": ["es2021", "dom", "DOM.Iterable"], "checkJs": true, "noEmit": true, + "incremental": true, + "tsBuildInfoFile": ".cache/tsc-main.tsbuildinfo", "types": [], "strict": false, "skipLibCheck": true,