@vue/eslint-config-typescript
Version:
ESLint config for TypeScript + Vue.js projects
844 lines (837 loc) • 26 kB
JavaScript
import process from 'node:process';
import fs from 'node:fs/promises';
import path from 'node:path';
import { createInterface } from 'node:readline/promises';
import fg from 'fast-glob';
import { Linter } from 'eslint';
import tseslint from 'typescript-eslint';
function isIdentifier(node) {
return !!node && typeof node === "object" && node.type === "Identifier";
}
function isCallExpression(node) {
return !!node && typeof node === "object" && node.type === "CallExpression";
}
function getRequiredRange(node) {
if (!node.range) {
throw new Error("Expected node to have a range.");
}
return node.range;
}
function isAstNode(node) {
return !!node && typeof node === "object" && typeof node.type === "string";
}
function unwrapExpression(node) {
let current = node;
while (isAstNode(current)) {
switch (current.type) {
case "ParenthesizedExpression":
case "TSAsExpression":
case "TSSatisfiesExpression":
case "TSNonNullExpression":
current = current.expression;
continue;
default:
return current;
}
}
return current;
}
function getCallToLocalNames(node, localNames) {
const unwrappedNode = unwrapExpression(node);
return isCallExpression(unwrappedNode) && isIdentifier(unwrappedNode.callee) && localNames.has(unwrappedNode.callee.name) ? unwrappedNode : void 0;
}
function visitAst(node, visitor, parent = null) {
if (!isAstNode(node)) {
return;
}
visitor(node, parent);
for (const [key, value] of Object.entries(node)) {
if (key === "parent" || key === "range" || key === "loc") {
continue;
}
if (Array.isArray(value)) {
for (const item of value) {
visitAst(item, visitor, node);
}
continue;
}
visitAst(value, visitor, node);
}
}
function getNodeText(sourceCode, node) {
return sourceCode.getText(node);
}
function getStatementRemovalRange(sourceCode, statement) {
const text = sourceCode.getText();
const range = getRequiredRange(statement);
let start = range[0];
while (start > 0 && text[start - 1] !== "\n") {
start--;
}
let end = range[1];
while (end < text.length && text[end] !== "\n") {
end++;
}
if (text[end] === "\n") {
end++;
}
let nextLineEnd = end;
while (nextLineEnd < text.length && text[nextLineEnd] !== "\n") {
nextLineEnd++;
}
if (text.slice(end, nextLineEnd).trim() === "") {
end = nextLineEnd;
if (text[end] === "\n") {
end++;
}
}
return [start, end];
}
const MIGRATION_PLUGIN_NAME = "vue-ts-migrate";
const MIGRATION_RULE_NAME = "to-with-vue-ts";
const MIGRATION_RULE_ID = `${MIGRATION_PLUGIN_NAME}/${MIGRATION_RULE_NAME}`;
const PACKAGE_NAME = "@vue/eslint-config-typescript";
const LEGACY_HELPER_IMPORTS = /* @__PURE__ */ new Set(["defineConfigWithVueTs", "defineConfig"]);
const PROJECT_OPTIONS_KEYS = /* @__PURE__ */ new Set([
"tsSyntaxInTemplates",
"scriptLangs",
"allowComponentTypeUnsafety",
"includeDotFolders",
"rootDir"
]);
function isTsConfigFile(filename) {
return [".ts", ".mts"].includes(path.extname(filename));
}
function getLintFilename(filename) {
return path.basename(filename);
}
function getBaseConfig(filename) {
return [
{
files: [
"**/*.js",
"**/*.mjs",
"**/*.ts",
"**/*.mts",
"eslint.config.js",
"eslint.config.mjs",
"eslint.config.ts",
"eslint.config.mts",
"**/eslint.config.js",
"**/eslint.config.mjs",
"**/eslint.config.ts",
"**/eslint.config.mts"
],
languageOptions: {
ecmaVersion: "latest",
sourceType: "module",
...isTsConfigFile(filename) ? { parser: tseslint.parser } : {}
}
}
];
}
function parseSourceCode(text, filename) {
const lintFilename = getLintFilename(filename);
const linter = new Linter({ configType: "flat" });
const messages = linter.verify(text, getBaseConfig(lintFilename), {
filename: lintFilename
});
const sourceCode = linter.getSourceCode();
if (!sourceCode) {
return {
error: messages.find((message) => message.fatal)?.message ?? messages[0]?.message
};
}
return {
sourceCode
};
}
function getImportSpecifierImportedName(specifier) {
if (specifier.type !== "ImportSpecifier") {
return;
}
if (typeof specifier.imported === "string") {
return specifier.imported;
}
const imported = specifier.imported;
return imported.name ?? imported.value;
}
function collectVueImports(sourceCode) {
const declarations = [];
const legacyHelperLocalNames = /* @__PURE__ */ new Set();
const configureVueProjectLocalNames = /* @__PURE__ */ new Set();
let withVueTsLocalName;
const unsupportedReasons = [];
for (const statement of sourceCode.ast.body) {
if (statement.type !== "ImportDeclaration") {
continue;
}
if (statement.source.value !== PACKAGE_NAME) {
continue;
}
declarations.push(statement);
for (const specifier of statement.specifiers) {
if (specifier.type === "ImportDefaultSpecifier") {
unsupportedReasons.push(
"Default `createConfig()` imports are not supported by this migration yet."
);
continue;
}
if (specifier.type === "ImportNamespaceSpecifier") {
unsupportedReasons.push(
"Namespace imports from `@vue/eslint-config-typescript` are not supported by this migration yet."
);
continue;
}
const importedName = getImportSpecifierImportedName(specifier);
if (!importedName) {
continue;
}
if (LEGACY_HELPER_IMPORTS.has(importedName)) {
legacyHelperLocalNames.add(specifier.local.name);
}
if (importedName === "configureVueProject") {
configureVueProjectLocalNames.add(specifier.local.name);
}
if (importedName === "withVueTs") {
withVueTsLocalName = specifier.local.name;
}
if (importedName === "createConfig") {
unsupportedReasons.push(
"`createConfig()` uses the older array-spread API and is not supported by this migration yet."
);
}
}
}
return {
declarations,
legacyHelperLocalNames,
configureVueProjectLocalNames,
withVueTsLocalName,
unsupportedReasons
};
}
function findConfigureVueProjectStatements(sourceCode, localNames) {
if (localNames.size === 0) {
return [];
}
return sourceCode.ast.body.flatMap((statement) => {
if (statement.type !== "ExpressionStatement") {
return [];
}
const call = getCallToLocalNames(statement.expression, localNames);
return call ? [{ statement, call }] : [];
});
}
function findLocalNameUsages(sourceCode, localNames) {
const usages = [];
visitAst(sourceCode.ast, (node, parent) => {
if (node.type !== "Identifier" || !localNames.has(node.name)) {
return;
}
usages.push({
identifier: node,
parent
});
});
return usages;
}
function isIgnoredConfigureUsage(usage, supportedCalls) {
const { identifier, parent } = usage;
if (!parent) {
return false;
}
if ((parent.type === "ImportSpecifier" || parent.type === "ImportDefaultSpecifier" || parent.type === "ImportNamespaceSpecifier") && (parent.local === identifier || parent.imported === identifier)) {
return true;
}
if ((parent.type === "Property" || parent.type === "PropertyDefinition" || parent.type === "MethodDefinition") && parent.key === identifier && !parent.computed) {
return true;
}
if (parent.type === "MemberExpression" && parent.property === identifier && !parent.computed) {
return true;
}
if (parent.type === "CallExpression" && parent.callee === identifier && supportedCalls.has(parent)) {
return true;
}
return false;
}
function findLegacyTarget(sourceCode, localNames) {
if (localNames.size === 0) {
return;
}
const topLevelVariables = /* @__PURE__ */ new Map();
for (const statement of sourceCode.ast.body) {
if (statement.type !== "VariableDeclaration") {
continue;
}
if (statement.kind !== "const" && statement.kind !== "let" && statement.kind !== "var") {
continue;
}
for (const declaration of statement.declarations) {
if (!isIdentifier(declaration.id)) {
continue;
}
const call = getCallToLocalNames(declaration.init, localNames);
if (!call) {
continue;
}
topLevelVariables.set(declaration.id.name, { call, kind: statement.kind });
}
}
const exportDefault = sourceCode.ast.body.find(
(statement) => statement.type === "ExportDefaultDeclaration"
);
if (!exportDefault) {
return;
}
const directCall = getCallToLocalNames(exportDefault.declaration, localNames);
if (directCall) {
const callee = directCall.callee;
return {
call: directCall,
helperLocalName: callee.name
};
}
const unwrappedDeclaration = unwrapExpression(exportDefault.declaration);
if (!isIdentifier(unwrappedDeclaration)) {
return;
}
const target = topLevelVariables.get(unwrappedDeclaration.name);
if (!target || target.kind !== "const" || !isIdentifier(target.call.callee)) {
return;
}
return {
call: target.call,
helperLocalName: target.call.callee.name
};
}
function formatMultilineCall(calleeText, argsText) {
if (argsText.length === 0) {
return `${calleeText}()`;
}
const formattedArgs = argsText.map(
(arg) => arg.split("\n").map((line) => ` ${line}`).join("\n")
).join(",\n");
return `${calleeText}(
${formattedArgs},
)`;
}
function buildImportEdits(sourceCode, imports, withVueTsLocalName) {
const primaryImport = imports.find(
(statement) => statement.specifiers.some(
(specifier) => specifier.type === "ImportSpecifier" && getImportSpecifierImportedName(specifier) === "withVueTs"
)
) ?? imports[0];
return imports.map((statement) => {
const keptNamedSpecifiers = statement.specifiers.filter(
(specifier) => specifier.type === "ImportSpecifier" && !LEGACY_HELPER_IMPORTS.has(
getImportSpecifierImportedName(specifier) ?? ""
) && getImportSpecifierImportedName(specifier) !== "configureVueProject"
).map((specifier) => sourceCode.getText(specifier));
const hasWithVueTsSpecifier = statement.specifiers.some(
(specifier) => specifier.type === "ImportSpecifier" && getImportSpecifierImportedName(specifier) === "withVueTs"
);
if (statement === primaryImport && !hasWithVueTsSpecifier) {
keptNamedSpecifiers.unshift(
withVueTsLocalName === "withVueTs" ? "withVueTs" : `withVueTs as ${withVueTsLocalName}`
);
}
if (keptNamedSpecifiers.length === 0) {
return {
range: getStatementRemovalRange(sourceCode, statement),
text: ""
};
}
const statementText = sourceCode.getText(statement);
const semicolon = statementText.trimEnd().endsWith(";") ? ";" : "";
return {
range: getRequiredRange(statement),
text: `import { ${keptNamedSpecifiers.join(", ")} } from ${sourceCode.getText(statement.source)}${semicolon}`
};
}).filter((edit) => !!edit);
}
function classifyFirstArgument(call) {
const firstArg = unwrapExpression(call.arguments[0]);
if (!firstArg || typeof firstArg !== "object" || Array.isArray(firstArg) || firstArg.type !== "ObjectExpression") {
return "config";
}
const properties = firstArg.properties;
let sawProjectOptionKey = false;
let sawNonProjectOptionKey = false;
let sawAmbiguousProperty = false;
for (const property of properties) {
if (property.type === "SpreadElement") {
sawAmbiguousProperty = true;
continue;
}
if (property.type !== "Property" || property.computed) {
sawAmbiguousProperty = true;
continue;
}
let propertyName;
if (property.key.type === "Identifier") {
propertyName = property.key.name;
} else if (property.key.type === "Literal" && typeof property.key.value === "string") {
propertyName = property.key.value;
}
if (!propertyName) {
sawAmbiguousProperty = true;
continue;
}
if (PROJECT_OPTIONS_KEYS.has(propertyName)) {
sawProjectOptionKey = true;
} else {
sawNonProjectOptionKey = true;
}
}
if (sawProjectOptionKey && !sawNonProjectOptionKey && !sawAmbiguousProperty) {
return "options";
}
if (sawProjectOptionKey || sawAmbiguousProperty) {
return "ambiguous-options";
}
return "config";
}
function analyzeToWithVueTsMigrationText(text, filename) {
const parsed = parseSourceCode(text, filename);
if (!parsed.sourceCode) {
return {
status: "parse-error",
reason: parsed.error ?? "Unknown parser error."
};
}
return analyzeToWithVueTsMigrationSourceCode(parsed.sourceCode);
}
function analyzeToWithVueTsMigrationSourceCode(sourceCode) {
const facts = collectMigrationFacts(sourceCode);
if (facts.imports.declarations.length === 0) {
return { status: "noop" };
}
const structuralUnsupportedReasons = getStructuralUnsupportedReasons(
sourceCode,
facts
);
if (structuralUnsupportedReasons.length > 0) {
return {
status: "unsupported",
reasons: structuralUnsupportedReasons
};
}
if (!facts.legacyTarget) {
return { status: "noop" };
}
if (isAlreadyUsingWithVueTs(facts)) {
return { status: "noop" };
}
const callUnsupportedReason = getLegacyCallUnsupportedReason(
facts.legacyTarget.call
);
if (callUnsupportedReason) {
return {
status: "unsupported",
reasons: [callUnsupportedReason]
};
}
return buildFixableMigrationAnalysis(sourceCode, {
...facts,
legacyTarget: facts.legacyTarget
});
}
function collectMigrationFacts(sourceCode) {
const imports = collectVueImports(sourceCode);
return {
imports,
configureStatements: findConfigureVueProjectStatements(
sourceCode,
imports.configureVueProjectLocalNames
),
legacyTarget: findLegacyTarget(sourceCode, imports.legacyHelperLocalNames)
};
}
function getStructuralUnsupportedReasons(sourceCode, facts) {
return [
.../* @__PURE__ */ new Set([
...facts.imports.unsupportedReasons,
...getConfigureVueProjectUnsupportedReasons(sourceCode, facts),
...getLegacyTargetUnsupportedReasons(facts)
])
];
}
function getConfigureVueProjectUnsupportedReasons(sourceCode, facts) {
const reasons = [];
const configureUsages = findLocalNameUsages(
sourceCode,
facts.imports.configureVueProjectLocalNames
);
const supportedConfigureCalls = new Set(
facts.configureStatements.map(
(configureStatement) => configureStatement.call
)
);
if (configureUsages.some(
(usage) => !isIgnoredConfigureUsage(usage, supportedConfigureCalls)
)) {
reasons.push(
"Only top-level `configureVueProject(...)` calls are supported by this migration."
);
}
if (facts.configureStatements.length > 1) {
reasons.push(
"Multiple `configureVueProject()` calls are not supported by this migration."
);
}
return reasons;
}
function getLegacyTargetUnsupportedReasons(facts) {
const reasons = [];
if (facts.imports.legacyHelperLocalNames.size > 0 && !facts.legacyTarget) {
reasons.push(
"`defineConfigWithVueTs()` must be exported directly, or assigned to a top-level variable that is exported as default, for this migration to rewrite it safely."
);
}
if (facts.configureStatements.length > 0 && !facts.legacyTarget) {
reasons.push(
"`configureVueProject()` was found, but this file does not export `defineConfigWithVueTs()`. Manual migration is required."
);
}
return reasons;
}
function isAlreadyUsingWithVueTs(facts) {
return !!facts.legacyTarget && !!facts.imports.withVueTsLocalName && facts.imports.withVueTsLocalName === facts.legacyTarget.helperLocalName;
}
function getLegacyCallUnsupportedReason(call) {
const firstArgumentKind = classifyFirstArgument(call);
if (firstArgumentKind === "options") {
return "`defineConfigWithVueTs()` does not accept project options. This file appears to be partially migrated already.";
}
if (firstArgumentKind === "ambiguous-options") {
return "The first argument to `defineConfigWithVueTs()` could be reinterpreted as `withVueTs()` project options. Please migrate this case manually.";
}
}
function buildFixableMigrationAnalysis(sourceCode, facts) {
const withVueTsLocalName = facts.imports.withVueTsLocalName ?? "withVueTs";
const configureArgText = getConfigureVueProjectOptionsText(
sourceCode,
facts.configureStatements[0]
);
const replacementArgs = getWithVueTsReplacementArgs(
sourceCode,
facts.legacyTarget,
configureArgText
);
const edits = buildMigrationEdits(
sourceCode,
facts,
withVueTsLocalName,
replacementArgs
);
const changes = getMigrationChanges(
facts.legacyTarget.helperLocalName,
withVueTsLocalName,
configureArgText
);
return {
status: "fixable",
changes,
edits
};
}
function getConfigureVueProjectOptionsText(sourceCode, configureStatement) {
const firstArg = configureStatement?.call.arguments[0];
return firstArg === void 0 ? void 0 : getNodeText(sourceCode, firstArg);
}
function getWithVueTsReplacementArgs(sourceCode, legacyTarget, configureArgText) {
return [
...configureArgText ? [configureArgText] : [],
...legacyTarget.call.arguments.map(
(argument) => getNodeText(sourceCode, argument)
)
];
}
function buildMigrationEdits(sourceCode, facts, withVueTsLocalName, replacementArgs) {
const edits = [
{
range: getRequiredRange(facts.legacyTarget.call),
text: formatMultilineCall(withVueTsLocalName, replacementArgs)
},
...buildImportEdits(
sourceCode,
facts.imports.declarations,
withVueTsLocalName
)
];
if (facts.configureStatements[0]) {
edits.push({
range: getStatementRemovalRange(
sourceCode,
facts.configureStatements[0].statement
),
text: ""
});
}
return edits.sort((a, b) => a.range[0] - b.range[0]);
}
function getMigrationChanges(legacyHelperLocalName, withVueTsLocalName, configureArgText) {
return [
`replace \`${legacyHelperLocalName}(...)\` with \`${withVueTsLocalName}(...)\``,
"rewrite imports from `@vue/eslint-config-typescript`",
...configureArgText ? ["inline `configureVueProject(...)` options into `withVueTs(...)`"] : []
];
}
const migrateToWithVueTsRule = {
meta: {
type: "suggestion",
fixable: "code",
docs: {
description: "migrate legacy @vue/eslint-config-typescript helper usage to withVueTs"
},
schema: [],
messages: {
migrate: "Legacy @vue/eslint-config-typescript helper usage can be migrated to `withVueTs(...)`."
}
},
create(context) {
return {
Program(node) {
const analysis = analyzeToWithVueTsMigrationSourceCode(
context.sourceCode
);
if (analysis.status !== "fixable") {
return;
}
context.report({
node,
messageId: "migrate",
fix(fixer) {
return analysis.edits.map(
(edit) => fixer.replaceTextRange(edit.range, edit.text)
);
}
});
}
};
}
};
function getFixConfig(filename) {
return [
{
...getBaseConfig(filename)[0],
plugins: {
[MIGRATION_PLUGIN_NAME]: {
rules: {
[MIGRATION_RULE_NAME]: migrateToWithVueTsRule
}
}
},
rules: {
[MIGRATION_RULE_ID]: "error"
}
}
];
}
function applyToWithVueTsMigrationText(text, filename) {
const analysis = analyzeToWithVueTsMigrationText(text, filename);
if (analysis.status !== "fixable") {
return {
analysis,
changed: false,
output: text
};
}
const linter = new Linter({ configType: "flat" });
const lintFilename = getLintFilename(filename);
const report = linter.verifyAndFix(text, getFixConfig(lintFilename), {
filename: lintFilename
});
return {
analysis,
changed: report.fixed,
output: report.output ?? text
};
}
const DEFAULT_PATTERNS = ["**/eslint.config.{js,mjs,ts,mts}"];
const DEFAULT_IGNORE = ["**/node_modules/**", "**/.git/**", "**/dist/**"];
function isFixableCandidate(candidate) {
return candidate.analysis.status === "fixable";
}
function isUnsupportedCandidate(candidate) {
return candidate.analysis.status === "unsupported" || candidate.analysis.status === "parse-error";
}
function parseArgs(argv) {
return {
yes: argv.includes("--yes") || argv.includes("-y"),
help: argv.includes("--help") || argv.includes("-h"),
patterns: argv.filter(
(arg) => !["--yes", "-y", "--help", "-h"].includes(arg)
)
};
}
function printHelp$1() {
console.log(`Usage:
vue-eslint-config-typescript migrate-to-with-vue-ts [paths...] [--yes]
Examples:
npx @vue/eslint-config-typescript migrate-to-with-vue-ts
npx @vue/eslint-config-typescript migrate-to-with-vue-ts eslint.config.ts
npx @vue/eslint-config-typescript migrate-to-with-vue-ts "packages/*/eslint.config.ts" --yes
`);
}
async function resolveTargetFiles(patterns) {
const targetPatterns = patterns.length > 0 ? patterns : DEFAULT_PATTERNS;
const explicitFiles = [];
const globPatterns = [];
for (const pattern of targetPatterns) {
const filename = path.resolve(pattern);
const stat = await fs.stat(filename).catch(() => void 0);
if (stat?.isFile()) {
explicitFiles.push(filename);
continue;
}
globPatterns.push(pattern);
}
const entries = await fg(globPatterns, {
cwd: process.cwd(),
absolute: true,
onlyFiles: true,
unique: true,
ignore: DEFAULT_IGNORE
});
return [.../* @__PURE__ */ new Set([...explicitFiles, ...entries])].sort();
}
async function loadCandidates(filenames) {
return Promise.all(
filenames.map(async (filename) => {
const text = await fs.readFile(filename, "utf8");
return {
filename,
relativeFilename: path.relative(process.cwd(), filename) || path.basename(filename),
analysis: analyzeToWithVueTsMigrationText(text, filename)
};
})
);
}
function printSummary(candidates) {
const fixable = candidates.filter(isFixableCandidate);
const unsupported = candidates.filter(isUnsupportedCandidate);
console.log(`Found ${candidates.length} ESLint config file(s).
`);
if (fixable.length > 0) {
console.log("Auto-migratable:");
for (const candidate of fixable) {
console.log(`- ${candidate.relativeFilename}`);
for (const change of candidate.analysis.changes) {
console.log(` - ${change}`);
}
}
console.log("");
}
if (unsupported.length > 0) {
console.log("Needs manual review:");
for (const candidate of unsupported) {
console.log(`- ${candidate.relativeFilename}`);
if (candidate.analysis.status === "parse-error") {
console.log(` - parser error: ${candidate.analysis.reason}`);
continue;
}
for (const reason of candidate.analysis.reasons) {
console.log(` - ${reason}`);
}
}
console.log("");
}
if (fixable.length === 0 && unsupported.length === 0) {
console.log("No migration needed.\n");
}
}
async function confirmApply() {
const readline = createInterface({
input: process.stdin,
output: process.stdout
});
try {
const answer = (await readline.question("Apply automatic changes? [Y/n] ")).trim().toLowerCase();
return answer === "" || answer === "y" || answer === "yes";
} finally {
readline.close();
}
}
async function applyFixes(candidates) {
let updatedFiles = 0;
for (const candidate of candidates) {
if (candidate.analysis.status !== "fixable") {
continue;
}
const input = await fs.readFile(candidate.filename, "utf8");
const result = applyToWithVueTsMigrationText(input, candidate.filename);
if (!result.changed) {
continue;
}
await fs.writeFile(candidate.filename, result.output);
updatedFiles += 1;
}
return updatedFiles;
}
async function runMigrateToWithVueTsCommand(argv) {
const options = parseArgs(argv);
if (options.help) {
printHelp$1();
return;
}
const filenames = await resolveTargetFiles(options.patterns);
if (filenames.length === 0) {
console.log("No matching ESLint config files found.");
return;
}
const candidates = await loadCandidates(filenames);
printSummary(candidates);
const fixableCount = candidates.filter(isFixableCandidate).length;
if (fixableCount === 0) {
return;
}
if (!options.yes && !process.stdin.isTTY) {
console.log(
"Cannot prompt in a non-interactive terminal. Re-run with `--yes` to apply the automatic changes."
);
return;
}
const shouldApply = options.yes ? true : await confirmApply();
if (!shouldApply) {
console.log("No files were changed.");
return;
}
const updatedFiles = await applyFixes(candidates);
console.log(`Updated ${updatedFiles} file(s).`);
}
const COMMANDS = {
"migrate-to-with-vue-ts": runMigrateToWithVueTsCommand,
"migrate-14.9": runMigrateToWithVueTsCommand
};
function printHelp() {
console.log(`Usage:
vue-eslint-config-typescript <command> [args]
Commands:
migrate-to-with-vue-ts Primary command. Migrate legacy helper-based configs to withVueTs(...)
migrate-14.9 Versioned alias for migrate-to-with-vue-ts
`);
}
async function main() {
const [command, ...args] = process.argv.slice(2);
if (!command || command === "--help" || command === "-h") {
printHelp();
return;
}
const runCommand = COMMANDS[command];
if (!runCommand) {
console.error(`Unknown command: ${command}
`);
printHelp();
process.exitCode = 1;
return;
}
await runCommand(args);
}
main().catch((error) => {
const message = error instanceof Error ? error.stack ?? error.message : String(error);
console.error(message);
process.exitCode = 1;
});