mirror of
https://github.com/oxc-project/oxc.git
synced 2026-09-14 19:36:11 +08:00
1193c9dbaf
dogfood
157 lines
4.9 KiB
TypeScript
157 lines
4.9 KiB
TypeScript
import { importJsConfig, loadViteConfigField } from "@oxapps/shared";
|
|
import { getErrorMessage } from "./utils/utils.ts";
|
|
|
|
interface JsConfigResult {
|
|
path: string;
|
|
config: unknown; // Will be validated as Oxlintrc on Rust side, `null` means "skip this config"
|
|
}
|
|
|
|
const isObject = (v: unknown) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
|
|
type LoadJsConfigsResult =
|
|
| { Success: JsConfigResult[] }
|
|
| { Failures: { path: string; error: string }[] }
|
|
| { Error: string };
|
|
|
|
function validateConfigExtends(root: object): void {
|
|
const visited = new WeakSet<object>();
|
|
const inStack = new WeakSet<object>();
|
|
const stackObjects: object[] = [];
|
|
const stackPaths: string[] = [];
|
|
|
|
const formatCycleError = (refPath: string, cycleStart: string, idx: number): string => {
|
|
const cycle =
|
|
idx === -1
|
|
? `${cycleStart} -> ${cycleStart}`
|
|
: [...stackPaths.slice(idx), cycleStart].join(" -> ");
|
|
|
|
return (
|
|
"`extends` contains a circular reference.\n\n"
|
|
+ `${refPath} points back to ${cycleStart}\n`
|
|
+ `Cycle: ${cycle}`
|
|
);
|
|
};
|
|
|
|
const visit = (config: object, path: string): void => {
|
|
if (visited.has(config)) return;
|
|
if (inStack.has(config)) {
|
|
const idx = stackObjects.indexOf(config);
|
|
const cycleStart = idx === -1 ? "<unknown>" : stackPaths[idx];
|
|
throw new Error(formatCycleError(path, cycleStart, idx));
|
|
}
|
|
|
|
inStack.add(config);
|
|
stackObjects.push(config);
|
|
stackPaths.push(path);
|
|
|
|
const maybeExtends = (config as Record<string, unknown>).extends;
|
|
if (maybeExtends !== undefined) {
|
|
if (!Array.isArray(maybeExtends)) {
|
|
throw new Error(
|
|
"`extends` must be an array of config objects (strings/paths are not supported).",
|
|
);
|
|
}
|
|
for (let i = 0; i < maybeExtends.length; i++) {
|
|
const item = maybeExtends[i];
|
|
if (!isObject(item)) {
|
|
throw new Error(
|
|
`\`extends[${i}]\` must be a config object (strings/paths are not supported).`,
|
|
);
|
|
}
|
|
|
|
const itemPath = `${path}.extends[${i}]`;
|
|
if (inStack.has(item)) {
|
|
const idx = stackObjects.indexOf(item);
|
|
const cycleStart = idx === -1 ? "<unknown>" : stackPaths[idx];
|
|
throw new Error(formatCycleError(itemPath, cycleStart, idx));
|
|
}
|
|
|
|
visit(item, itemPath);
|
|
}
|
|
}
|
|
|
|
inStack.delete(config);
|
|
stackObjects.pop();
|
|
stackPaths.pop();
|
|
visited.add(config);
|
|
};
|
|
|
|
visit(root, "<root>");
|
|
}
|
|
|
|
/**
|
|
* Resolve a single config path to a `JsConfigResult`.
|
|
* Standard mode: default export must be a plain object.
|
|
*/
|
|
async function resolveJsConfig(path: string, cacheKey: number): Promise<JsConfigResult> {
|
|
const config = await importJsConfig(path, cacheKey);
|
|
validateConfigExtends(config);
|
|
return { path, config };
|
|
}
|
|
|
|
/**
|
|
* Resolve a single Vite+ config path to a `JsConfigResult`.
|
|
* Extracts the `.lint` field via `vite-plus`. Returns `null` config when missing (signals "skip").
|
|
*/
|
|
async function resolveVitePlusConfig(path: string): Promise<JsConfigResult> {
|
|
const lintConfig = await loadViteConfigField(path, "lint");
|
|
if (lintConfig === null) return { path, config: null };
|
|
validateConfigExtends(lintConfig);
|
|
return { path, config: lintConfig };
|
|
}
|
|
|
|
/**
|
|
* Load config files in parallel using the given resolver, and return JSON-stringified result.
|
|
*/
|
|
async function loadConfigs(
|
|
paths: string[],
|
|
resolver: (path: string) => Promise<JsConfigResult>,
|
|
): Promise<string> {
|
|
try {
|
|
const results = await Promise.allSettled(paths.map(resolver));
|
|
|
|
const successes: JsConfigResult[] = [];
|
|
const errors: { path: string; error: string }[] = [];
|
|
|
|
for (let i = 0; i < results.length; i++) {
|
|
const result = results[i];
|
|
if (result.status === "fulfilled") {
|
|
successes.push(result.value);
|
|
} else {
|
|
errors.push({
|
|
path: paths[i],
|
|
error: getErrorMessage(result.reason),
|
|
});
|
|
}
|
|
}
|
|
|
|
if (errors.length > 0) {
|
|
return JSON.stringify({ Failures: errors } satisfies LoadJsConfigsResult);
|
|
}
|
|
|
|
return JSON.stringify({ Success: successes } satisfies LoadJsConfigsResult);
|
|
} catch (err) {
|
|
return JSON.stringify({
|
|
Error: getErrorMessage(err),
|
|
} satisfies LoadJsConfigsResult);
|
|
}
|
|
}
|
|
|
|
export type ConfigLoader = (paths: string[]) => Promise<string>;
|
|
|
|
/**
|
|
* Load standard oxlint JS/TS config files in parallel.
|
|
*/
|
|
export const loadJsConfigs: ConfigLoader = (paths) => {
|
|
// Share one cache-busting key across the batch so that `?cache=<key>` is identical
|
|
// for every path resolved in this call (consistent reload semantics for LSP).
|
|
const cacheKey = Date.now();
|
|
return loadConfigs(paths, (path) => resolveJsConfig(path, cacheKey));
|
|
};
|
|
|
|
/**
|
|
* Load Vite+ config files in parallel, extracting the `.lint` field from each.
|
|
*/
|
|
export const loadVitePlusConfigs: ConfigLoader = (paths) =>
|
|
loadConfigs(paths, resolveVitePlusConfig);
|