diff --git a/package.json b/package.json index a855732d5..7182ab59d 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,8 @@ "lint:fix": "eslint \"**/*.{js,mjs,jsx,ts,tsx}\" --fix", "pretty:check": "prettier --check ./", "pretty:fix": "prettier --write ./", - "format": "npm run pretty:fix && npm run lint:fix", + "format": "node tests/format.mjs", + "format:all": "node tests/format.mjs --all", "test:chrome": "node tests/run-unittests.mjs --browser chrome", "test:firefox": "node tests/run-unittests.mjs --browser firefox", "test:safari": "node tests/run-unittests.mjs --browser safari", @@ -46,16 +47,16 @@ "eslint-plugin-react": "^7.32.2", "eslint-plugin-vue": "^9.10.0", "expect.js": "^0.3.1", - "mocha": "^10.2.0", - "prettier": "^2.8.3", - "selenium-webdriver": "^4.27.0", - "sinon": "^17.0.1", - "typescript": "^5.0.4", "lws": "^4.2.0", "lws-cors": "^4.2.1", "lws-index": "^3.1.1", "lws-log": "^3.0.0", "lws-static": "^3.1.1", - "prettier-plugin-tailwindcss": "^0.4.1" + "mocha": "^10.2.0", + "prettier": "^2.8.3", + "prettier-plugin-tailwindcss": "^0.4.1", + "selenium-webdriver": "^4.27.0", + "sinon": "^17.0.1", + "typescript": "^5.0.4" } } diff --git a/tests/format.mjs b/tests/format.mjs new file mode 100644 index 000000000..82b0b1004 --- /dev/null +++ b/tests/format.mjs @@ -0,0 +1,97 @@ +import { execFileSync } from "child_process"; +import { parseArgs } from "util"; +import fs from "fs"; + +function formatAll() { + console.log("Formatting all files..."); + execFileSync("npm", ["run", "pretty:fix"], { stdio: "inherit" }); + execFileSync("npm", ["run", "lint:fix"], { stdio: "inherit" }); +} + +// Chunk files into batches to avoid "Argument list too long" errors on large changelists. +function execFileSyncInBatches(command, args, files, batchSize = 100) { + for (let i = 0; i < files.length; i += batchSize) { + const batch = files.slice(i, i + batchSize); + execFileSync("npx", [command, ...args, ...batch], { stdio: "inherit" }); + } +} + +function runPrettier(files) { + try { + console.log(`Running prettier on ${files.length} file(s)`); + execFileSyncInBatches("prettier", ["--write", "--ignore-unknown"], files); + } catch (e) { + console.error("Prettier formatting failed"); + process.exit(1); + } +} + +function runEslint(files) { + const jsTsFiles = files.filter((f) => /\.(js|mjs|jsx|ts|tsx)$/.test(f)); + if (jsTsFiles.length === 0) + return; + try { + console.log(`Running eslint on ${jsTsFiles.length} file(s)`); + execFileSyncInBatches("eslint", ["--fix"], jsTsFiles); + } catch (e) { + console.error("ESLint formatting failed"); + process.exit(1); + } +} + +function getChangedFiles() { + // "--diff-filter=ACMR" => ignore deleted files. + const diffOut = execFileSync("git", ["diff", "--name-only", "--diff-filter=ACMR", "@{upstream}"], { encoding: "utf8" }); + const files = diffOut + .split("\n") + .map((f) => f.trim()) + .filter((f) => f.length > 0 && fs.existsSync(f)); + return [...new Set(files)]; +} + +let values; +try { + ({ values } = parseArgs({ + options: { + all: { type: "boolean" }, + help: { type: "boolean", short: "h" }, + }, + strict: true, + })); +} catch (err) { + console.error(err.message); + console.error("Run 'node tests/format.mjs --help' for usage."); + process.exit(1); +} + +if (values.help) { + console.log(`Usage: node tests/format.mjs [options] + +Options: + --all Format all files across the repository instead of just changed files + -h, --help Show this help message`); + process.exit(0); +} + +if (values.all) { + formatAll(); + process.exit(0); +} + +let changedFiles = []; +try { + changedFiles = getChangedFiles(); +} catch (e) { + console.error("Failed to get changed files from git. Falling back to formatting all files."); + formatAll(); + process.exit(0); +} + +if (changedFiles.length === 0) { + console.log("No files changed compared to upstream."); + process.exit(0); +} +console.log(`Formatting ${changedFiles.length} changed files compared to upstream`); + +runPrettier(changedFiles); +runEslint(changedFiles);