@fluidframework/eslint-config-fluid
Version:
Shareable ESLint config for the Fluid Framework
173 lines (148 loc) • 5.93 kB
text/typescript
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/
/**
* Script to print the resolved ESLint configurations for various configurations and source file types.
*
* To add new configurations to print, add them to the `configsToPrint` array.
*
* For clarity, all the async file operations are done sequentially rather than collecting promises and using
* `Promise.all`. This makes the code easier to read and is acceptable as this script is not performance critical.
*/
import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { ESLint } from "eslint";
import sortJson from "sort-json";
// Import flat configs directly from flat.mjs
import { recommended, strict } from "../flat.mjs";
import type { FlatConfigArray } from "../library/configs/base.mjs";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
interface ConfigToPrint {
name: string;
config: FlatConfigArray;
sourceFilePath: string;
}
const configsToPrint = [
{
name: "default",
config: recommended,
sourceFilePath: path.join(__dirname, "..", "src", "file.ts"),
},
{
name: "react",
config: recommended,
sourceFilePath: path.join(__dirname, "..", "src", "file.tsx"),
},
{
name: "recommended",
config: recommended,
sourceFilePath: path.join(__dirname, "..", "src", "file.ts"),
},
{
name: "strict",
config: strict,
sourceFilePath: path.join(__dirname, "..", "src", "file.ts"),
},
{
name: "strict-biome",
// strict-biome uses the same flat config as strict; biome integration is handled separately
config: strict,
sourceFilePath: path.join(__dirname, "..", "src", "file.ts"),
},
{
name: "test",
config: recommended,
sourceFilePath: path.join(__dirname, "..", "src", "test", "file.ts"),
},
] as const satisfies readonly ConfigToPrint[];
/**
* Generates the applied ESLint config for a specific file and config.
*/
async function generateConfig(filePath: string, config: FlatConfigArray): Promise<string> {
console.log(`Generating config for ${filePath}`);
// ESLint 9's default ESLint class uses flat config format.
// Use overrideConfigFile: true to prevent loading eslint.config.js,
// and pass the config directly via overrideConfig.
const eslint = new ESLint({
overrideConfigFile: true,
overrideConfig: [...config],
});
const resolvedConfig = (await eslint.calculateConfigForFile(filePath)) as unknown;
if (!resolvedConfig) {
console.warn("Warning: ESLint returned undefined config for " + filePath);
return "{}\n";
}
// Serialize and parse to create a clean copy without any circular references or non-serializable values
const cleanConfig = JSON.parse(JSON.stringify(resolvedConfig));
// Remove globals from languageOptions (very large) but keep the rest (parserOptions, etc.)
if (cleanConfig.languageOptions?.globals) {
delete cleanConfig.languageOptions.globals;
}
// Remove tsconfigRootDir since it varies by environment (it's set to process.cwd())
if (typeof cleanConfig.languageOptions?.parserOptions === "object") {
delete cleanConfig.languageOptions.parserOptions.tsconfigRootDir;
}
// Convert numeric severities to string equivalents in rules
if (cleanConfig.rules) {
for (const [ruleName, ruleConfig] of Object.entries(cleanConfig.rules)) {
if (Array.isArray(ruleConfig) && ruleConfig.length > 0) {
const severity = ruleConfig[0];
if (severity === 0) ruleConfig[0] = "off";
else if (severity === 1) ruleConfig[0] = "warn";
else if (severity === 2) ruleConfig[0] = "error";
} else if (ruleConfig === 0 || ruleConfig === 1 || ruleConfig === 2) {
// Handle standalone severity values
const stringValue = ruleConfig === 0 ? "off" : ruleConfig === 1 ? "warn" : "error";
cleanConfig.rules[ruleName] = stringValue;
}
}
}
// Generate the new content with sorting applied
// Sorting at all is desirable as otherwise changes in the order of common config references may cause large diffs
// with little semantic meaning.
// On the other hand, fully sorting the json can be misleading:
// some eslint settings depend on object key order ("import-x/resolver" being a known one, see
// https://github.com/un-ts/eslint-plugin-import-x/blob/master/src/utils/resolve.ts).
// Using depth 2 is a nice compromise.
const sortedConfig = sortJson(cleanConfig, { depth: 2 });
const finalConfig = JSON.stringify(sortedConfig, null, "\t");
// Add a trailing newline to match preferred output formatting
return finalConfig + "\n";
}
(async () => {
const args = process.argv.slice(2);
if (args.length !== 1) {
console.error("Usage: jiti scripts/print-configs.ts <output-directory>");
process.exit(1);
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- validated by the args.length check above
const outputPath = args[0]!;
await fs.mkdir(outputPath, { recursive: true });
const expectedFiles = new Set<string>();
for (const { name, config, sourceFilePath } of configsToPrint) {
const outputFilePath = path.join(outputPath, `${name}.json`);
expectedFiles.add(`${name}.json`);
let originalContent = "";
try {
originalContent = await fs.readFile(outputFilePath, "utf8");
} catch (err) {
// File doesn't exist yet, which is OK - we'll create it
}
const newContent = await generateConfig(sourceFilePath, config);
// Only write the file if the content has changed
if (newContent !== originalContent) {
await fs.writeFile(outputFilePath, newContent);
}
}
// Remove any files in the output directory that aren't in the expected list
const existingFiles = await fs.readdir(outputPath);
for (const file of existingFiles) {
if (file.endsWith(".json") && !expectedFiles.has(file)) {
console.log(`Removing unexpected file: ${file}`);
await fs.unlink(path.join(outputPath, file));
}
}
})();