UNPKG

@storm-stack/core

Version:

A build toolkit and runtime used by Storm Software in TypeScript applications

555 lines (537 loc) 19.9 kB
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 };