@storm-stack/core
Version:
A build toolkit and runtime used by Storm Software in TypeScript applications
555 lines (537 loc) • 19.9 kB
JavaScript
import { installPackages } from './chunk-RGKSWMYB.js';
import { writeFile } from './chunk-WEZ7ASIP.js';
import { init_esm_shims, __name } from './chunk-QH7NXH7H.js';
import { LogLevelLabel } from '@storm-software/config-tools/types';
import { joinPaths } from '@stryke/path/join-paths';
import { isDirectory } from '@stryke/fs/is-file';
import { isPackageExists } from '@stryke/fs/package-fns';
import { readFile } from '@stryke/fs/read-file';
import { parseVersion } from '@stryke/fs/semver-fns';
import { findFileName } from '@stryke/path/file-path-fns';
import { getParentPath } from '@stryke/path/get-parent-path';
import { isError } from '@stryke/type-checks/is-error';
import path from 'node:path';
import ts from 'typescript';
import '@stryke/fs/list-files';
import '@stryke/fs/resolve';
import defu from 'defu';
import { minimatch } from 'minimatch';
// src/commands/lint/index.ts
init_esm_shims();
// src/commands/lint/eslint/index.ts
init_esm_shims();
// src/commands/lint/eslint/lint.ts
init_esm_shims();
// src/commands/lint/eslint/format-result.ts
init_esm_shims();
function pluginCount(messages) {
let nextPluginWarningCount = 0;
let nextPluginErrorCount = 0;
for (let i = 0; i < messages.length; i++) {
const { severity, ruleId } = messages[i];
if (ruleId?.includes("storm-stack")) {
if (severity === 1) {
nextPluginWarningCount += 1;
} else {
nextPluginErrorCount += 1;
}
}
}
return {
nextPluginErrorCount,
nextPluginWarningCount
};
}
__name(pluginCount, "pluginCount");
function formatMessage(dir, messages, filePath) {
let fileName = path.posix.normalize(path.relative(dir, filePath).replace(/\\/g, "/"));
if (!fileName.startsWith(".")) {
fileName = `./${fileName}`;
}
let output = `
${fileName}`;
for (let i = 0; i < messages.length; i++) {
const { message, severity, line, column, ruleId } = messages[i];
output += "\n";
if (line && column) {
output = `${output + line.toString()}:${column.toString()} `;
}
if (severity === 1) {
output += `Warning: `;
} else {
output += `Error: `;
}
output += message;
if (ruleId) {
output += ` ${ruleId}`;
}
}
return output;
}
__name(formatMessage, "formatMessage");
async function formatResults(baseDir, results, format) {
let totalPluginErrorCount = 0;
let totalPluginWarningCount = 0;
const resultsWithMessages = results.filter(({ messages }) => messages?.length);
resultsWithMessages.forEach(({ messages }) => {
const res = pluginCount(messages);
totalPluginErrorCount += res.nextPluginErrorCount;
totalPluginWarningCount += res.nextPluginWarningCount;
});
const output = format ? await format(resultsWithMessages) : resultsWithMessages.map(({ messages, filePath }) => formatMessage(baseDir, messages, filePath)).join("\n");
return {
output,
outputWithMessages: resultsWithMessages.length > 0 ? `${output}
Info - Need to disable some ESLint rules? Learn more here: https://nextjs.org/docs/app/api-reference/config/eslint#disabling-rules` : "",
totalPluginErrorCount,
totalPluginWarningCount
};
}
__name(formatResults, "formatResults");
// src/commands/lint/eslint/write-default-config.ts
init_esm_shims();
async function writeDefaultEslintConfig(log, context, type = "recommended") {
const eslintConfigFile = joinPaths(context.options.workspaceRoot, "eslint.config.js");
const eslintConfig = `
import { getConfig } from "eslint-config-storm-stack";
Error.stackTraceLimit = Number.POSITIVE_INFINITY;
export default getConfig({
repositoryName: "${context.options.workspaceConfig.name || context.options.name || "storm-stack"}",
"storm-stack": "${type}",
});
`;
log(LogLevelLabel.INFO, `Writing a default ESLint config file to ${eslintConfigFile}`);
return writeFile(log, eslintConfigFile, eslintConfig);
}
__name(writeDefaultEslintConfig, "writeDefaultEslintConfig");
// src/commands/lint/eslint/lint.ts
var VALID_SEVERITY = [
"off",
"warn",
"error"
];
function isValidSeverity(severity) {
return VALID_SEVERITY.includes(severity);
}
__name(isValidSeverity, "isValidSeverity");
async function writeOutputFile(log, outputFile, outputData) {
const filePath = path.resolve(process.cwd(), outputFile);
if (isDirectory(filePath)) {
log(LogLevelLabel.ERROR, `Cannot write to output file path, it is a directory: ${filePath}`);
} else {
try {
await writeFile(log, filePath, outputData);
log(LogLevelLabel.INFO, `The output file has been created: ${filePath}`);
} catch (err) {
log(LogLevelLabel.ERROR, `There was a problem writing the output file: ${filePath}`);
console.error(err);
}
}
}
__name(writeOutputFile, "writeOutputFile");
async function hasEslintConfiguration(eslintFile, packageJsonConfig) {
const configObject = {
exists: false,
emptyEslint: false,
emptyPkgJsonConfig: false
};
if (eslintFile) {
const content = await readFile(eslintFile).then((txt) => txt.trim().replace(/\n/g, ""), () => null);
if (content === "" || content === "{}" || content === "---" || content === "module.exports = {}") {
configObject.emptyEslint = true;
} else {
configObject.exists = true;
}
} else if (packageJsonConfig?.eslintConfig) {
if (Object.keys(packageJsonConfig.eslintConfig).length) {
configObject.exists = true;
} else {
configObject.emptyPkgJsonConfig = true;
}
}
return configObject;
}
__name(hasEslintConfiguration, "hasEslintConfiguration");
var lint = /* @__PURE__ */ __name(async (log, context, eslintConfigPath, { lintDuringBuild = false, eslintOptions = null, reportErrorsOnly = false, maxWarnings = -1, formatter = null, outputFile = null }) => {
try {
await installPackages(context, [
{
name: "eslint",
dev: true
},
{
name: "eslint-config-storm-stack",
dev: true
}
]);
const isInstalled = isPackageExists("eslint", {
paths: [
context.options.workspaceRoot,
context.options.projectRoot
]
});
if (!isInstalled) {
log(LogLevelLabel.ERROR, `ESLint must be installed${lintDuringBuild ? " in order to run during builds:" : ":"} npm install --save-dev eslint`);
return null;
}
const module = await context.resolver.import(context.resolver.esmResolve("eslint"));
const useFlatConfig = eslintConfigPath ? findFileName(eslintConfigPath).startsWith("eslint.config.") : false;
let ESLint;
if ("loadESLint" in module) {
ESLint = await module.loadESLint({
useFlatConfig
});
}
const eslintVersion = parseVersion(ESLint?.version);
if (!eslintVersion || eslintVersion.compare("8.57.0") < 0) {
return `Error - Your project has an older version of ESLint installed${eslintVersion ? ` (${eslintVersion.major}.${eslintVersion.minor}.${eslintVersion.patch})` : ""}. Please upgrade to ESLint version 8.57.0 or above`;
}
const options = {
useEslintrc: true,
baseConfig: {},
errorOnUnmatchedPattern: false,
extensions: [
".js",
".jsx",
".ts",
".tsx"
],
cache: true,
...eslintOptions
};
if (eslintVersion?.compare("9.0.0") && useFlatConfig) {
for (const option of [
"useEslintrc",
"extensions",
"ignorePath",
"reportUnusedDisableDirectives",
"resolvePluginsRelativeTo",
"rulePaths",
"inlineConfig",
"maxWarnings"
]) {
if (option in options) {
delete options[option];
}
}
}
let eslint = new ESLint(options);
let stormStackEslintPluginIsEnabled = false;
const stormStackRulesEnabled = /* @__PURE__ */ new Map();
for (const configFile of [
eslintConfigPath,
joinPaths(context.options.projectRoot, "package.json")
]) {
if (!configFile) continue;
const completeConfig = await eslint.calculateConfigForFile(configFile);
if (!completeConfig) continue;
const plugins = completeConfig.plugins;
const hasStormStackPlugin = (
// in ESLint < 9, `plugins` value is string[]
Array.isArray(plugins) ? plugins.includes("storm-stack") : "storm-stack" in plugins
);
if (hasStormStackPlugin) {
stormStackEslintPluginIsEnabled = true;
for (const [name, [severity]] of Object.entries(completeConfig.rules)) {
if (!name.startsWith("storm-stack/")) {
continue;
}
if (typeof severity === "number" && severity >= 0 && severity < VALID_SEVERITY.length) {
stormStackRulesEnabled.set(name, VALID_SEVERITY[severity]);
} else if (typeof severity === "string" && isValidSeverity(severity)) {
stormStackRulesEnabled.set(name, severity);
}
}
break;
}
}
if (stormStackEslintPluginIsEnabled) {
eslint = new ESLint(options);
} else {
log(LogLevelLabel.WARN, "The Storm Stack plugin was not detected in your ESLint configuration. See https://nextjs.org/docs/app/api-reference/config/eslint#migrating-existing-config");
}
const lintStart = process.hrtime();
let results = await eslint.lintFiles(context.tsconfig.fileNames.filter((fileName) => !fileName.includes(context.artifactsPath) && !fileName.includes("node_modules")));
let selectedFormatter = null;
if (options.fix) {
await ESLint.outputFixes(results);
}
if (reportErrorsOnly) {
results = ESLint.getErrorResults(results);
}
if (formatter) {
selectedFormatter = await eslint.loadFormatter(formatter);
}
const formattedResult = await formatResults(context.options.projectRoot, results, selectedFormatter?.format?.bind(selectedFormatter));
const lintEnd = process.hrtime(lintStart);
const totalWarnings = results.reduce((sum, file) => sum + file.warningCount, 0);
if (outputFile) {
await writeOutputFile(log, outputFile, formattedResult.output);
}
return {
output: formattedResult.outputWithMessages,
isError: ESLint.getErrorResults(results)?.length > 0 || maxWarnings >= 0 && totalWarnings > maxWarnings,
eventInfo: {
durationInSeconds: lintEnd[0],
eslintVersion: eslintVersion.version,
lintedFilesCount: results.length,
lintFix: !!options.fix,
eslintPluginErrorsCount: formattedResult.totalPluginErrorCount,
eslintPluginWarningsCount: formattedResult.totalPluginWarningCount,
stormStackRulesEnabled: Object.fromEntries(stormStackRulesEnabled)
}
};
} catch (err) {
if (lintDuringBuild) {
log(LogLevelLabel.ERROR, `ESLint: ${isError(err) && err.message ? err.message.replace(/\n/g, " ") : String(err)}`);
return null;
} else {
throw err;
}
}
}, "lint");
async function runEslint(context, opts = {}) {
const { lintDuringBuild = false, eslintOptions = null, reportErrorsOnly = false, maxWarnings = -1, formatter = null, outputFile = null, type = "recommended" } = opts;
const eslintFile = getParentPath([
// eslint v9
"eslint.config.js",
"eslint.config.mjs",
"eslint.config.cjs",
// TS extensions require to install a separate package `jiti`.
// https://eslint.org/docs/latest/use/configure/configuration-files#typescript-configuration-files
"eslint.config.ts",
"eslint.config.mts",
"eslint.config.cts",
// eslint <= v8
".eslintrc.js",
".eslintrc.cjs",
".eslintrc.yaml",
".eslintrc.yml",
".eslintrc.json",
".eslintrc"
], context.options.projectRoot) ?? null;
const config = await hasEslintConfiguration(eslintFile, context.packageJson);
if (config.exists) {
return lint(context.log, context, eslintFile, {
lintDuringBuild,
eslintOptions,
reportErrorsOnly,
maxWarnings,
formatter,
outputFile
});
}
if (lintDuringBuild) {
if (config.emptyPkgJsonConfig || config.emptyEslint) {
context.log(LogLevelLabel.WARN, `No ESLint configuration detected. Run "storm-stack lint" to begin setup`);
}
return null;
} else {
const isEslintInstalled = isPackageExists("eslint", {
paths: [
context.options.workspaceRoot,
context.options.projectRoot
]
});
const isEslintPluginInstalled = isPackageExists("eslint-config-storm-stack", {
paths: [
context.options.workspaceRoot,
context.options.projectRoot
]
});
if (!isEslintInstalled || !isEslintPluginInstalled) {
await installPackages(context, [
{
name: "eslint",
dev: true
},
{
name: "eslint-config-storm-stack",
dev: true
}
]);
}
await writeDefaultEslintConfig(context.log, context, type);
}
context.log(LogLevelLabel.SUCCESS, `ESLint has successfully been configured. Run "storm-stack lint" again to view warnings and errors.`);
return null;
}
__name(runEslint, "runEslint");
// src/commands/lint/eslint/index.ts
async function lintEslint(context, hooks) {
context.log(LogLevelLabel.TRACE, `Running ESLint on the Storm Stack project.`);
await runEslint(context, {
lintDuringBuild: true,
eslintOptions: {
cacheLocation: joinPaths(context.envPaths.cache, "eslint")
}
});
await hooks.callHook("lint:eslint", context).catch((error) => {
context.log(LogLevelLabel.ERROR, `An error occured while running ESLint on the Storm Stack project: ${error.message}
${error.stack ?? ""}`);
throw new Error("An error occured while running ESLint on the Storm Stack project", {
cause: error
});
});
}
__name(lintEslint, "lintEslint");
// src/commands/lint/types/index.ts
init_esm_shims();
// src/commands/lint/types/type-checks.ts
init_esm_shims();
// src/lib/typescript/program.ts
init_esm_shims();
function getDefaultCompilerOptions() {
return {
...ts.getDefaultCompilerOptions(),
jsx: ts.JsxEmit.React,
strict: true,
esModuleInterop: true,
module: ts.ModuleKind.ESNext,
suppressOutputPathCheck: true,
skipLibCheck: true,
skipDefaultLibCheck: true,
moduleResolution: ts.ModuleResolutionKind.Node10
};
}
__name(getDefaultCompilerOptions, "getDefaultCompilerOptions");
async function createVirtualProgram(rootNames, context, compilerOptions = {}) {
const options = defu(compilerOptions, getDefaultCompilerOptions());
const host = {
name: "storm-vfs",
root: context.options.workspaceRoot,
...ts.sys,
realpath: /* @__PURE__ */ __name((path3) => {
if (context.vfs.existsSync(path3)) {
return context.vfs.resolvePath(path3);
}
return ts.sys.realpath?.(path3) ?? path3;
}, "realpath"),
getCurrentDirectory() {
return context.options.workspaceRoot;
},
getCanonicalFileName(fileName) {
return fileName;
},
getDefaultLibFileName(_options) {
return ts.getDefaultLibFileName(options);
},
getDefaultLibLocation() {
return "/";
},
getNewLine() {
return "\n";
},
useCaseSensitiveFileNames() {
return true;
},
fileExists(fileName) {
return context.vfs.existsSync(fileName);
},
readFile(fileName) {
if (context.vfs.existsSync(fileName)) {
return context.vfs.readFileSync(fileName);
}
return void 0;
},
readDirectory: /* @__PURE__ */ __name((path3, extensions = [], exclude = [], include = []) => {
let results = [];
if (context.vfs.existsSync(path3)) {
results = context.vfs.readdirSync(path3, {
encoding: "utf8",
recursive: true
});
if (extensions.length > 0) {
results = results.filter((file) => extensions.some((ext) => file.endsWith(ext.startsWith(".") ? ext : `.${ext}`)));
}
if (exclude.length > 0) {
results = results.filter((file) => !exclude.some((pattern) => minimatch(file, pattern)));
}
if (include.length > 0) {
results = results.filter((file) => include.some((pattern) => minimatch(file, pattern)));
}
}
return results;
}, "readDirectory"),
writeFile(fileName, data) {
context.vfs.writeFileSync(fileName, data);
},
resolvePath: /* @__PURE__ */ __name((fileName) => {
if (context.vfs.existsSync(fileName)) {
return context.vfs.resolvePath(fileName);
}
return ts.sys.resolvePath(fileName);
}, "resolvePath"),
getSourceFile(fileName, languageVersionOrOptions, _, shouldCreateNewSourceFile) {
if (context.vfs.existsSync(fileName)) {
return ts.createSourceFile(fileName, context.vfs.readFileSync(fileName), languageVersionOrOptions ?? compilerOptions.target ?? getDefaultCompilerOptions().target, false);
} else if (shouldCreateNewSourceFile) {
const sourceFile = ts.createSourceFile(fileName, "", languageVersionOrOptions ?? compilerOptions.target ?? getDefaultCompilerOptions().target, false);
context.vfs.writeFileSync(fileName, sourceFile.text);
return sourceFile;
}
return void 0;
}
};
return ts.createProgram(rootNames, options, host);
}
__name(createVirtualProgram, "createVirtualProgram");
// src/commands/lint/types/type-checks.ts
async function checkTypes(context, sources) {
if (sources) {
await Promise.all(sources.entries().map(async ([path3, content]) => context.vfs.writeFile(path3, content)));
}
const program = await createVirtualProgram(sources ? Array.from(sources.keys()) : [], context, {
noEmit: true,
lib: [
"lib.esnext.d.ts"
],
types: []
});
const emitResult = program.emit();
const allDiagnostics = ts.getPreEmitDiagnostics(program).concat(emitResult.diagnostics);
allDiagnostics.forEach((diagnostic) => {
if (diagnostic.file) {
const { line, character } = ts.getLineAndCharacterOfPosition(diagnostic.file, diagnostic.start);
const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
context.log(LogLevelLabel.ERROR, `${diagnostic.file.fileName}:${line + 1}:${character + 1} : ${message}`);
} else {
context.log(LogLevelLabel.ERROR, ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"));
}
});
}
__name(checkTypes, "checkTypes");
// src/commands/lint/types/index.ts
async function lintTypes(context, hooks) {
context.log(LogLevelLabel.TRACE, `Performing TypeScript type checks on the Storm Stack project.`);
await checkTypes(context);
await hooks.callHook("lint:types", context).catch((error) => {
context.log(LogLevelLabel.ERROR, `An error occured while running TypeScript type checks on the Storm Stack project: ${error.message}
${error.stack ?? ""}`);
throw new Error("An error occured while running TypeScript type checks on the Storm Stack project", {
cause: error
});
});
}
__name(lintTypes, "lintTypes");
// src/commands/lint/index.ts
async function lint2(context, hooks) {
await hooks.callHook("lint:begin", context).catch((error) => {
context.log(LogLevelLabel.ERROR, `An error occured while starting the lint process for the Storm Stack project: ${error.message}
${error.stack ?? ""}`);
throw new Error("An error occured while starting the lint process for the Storm Stack project", {
cause: error
});
});
await lintTypes(context, hooks);
await lintEslint(context, hooks);
await hooks.callHook("lint:complete", context).catch((error) => {
context.log(LogLevelLabel.ERROR, `An error occured while finishing the lint process for the Storm Stack project: ${error.message}
${error.stack ?? ""}`);
throw new Error("An error occured while finishing the lint process for the Storm Stack project", {
cause: error
});
});
}
__name(lint2, "lint");
export { lint2 as lint };