@linter-spec/cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +114 -0
  3. package/dist/actions/init/index.d.ts +2 -0
  4. package/dist/actions/init/index.js +67 -0
  5. package/dist/actions/init/install-deps.d.ts +9 -0
  6. package/dist/actions/init/install-deps.js +36 -0
  7. package/dist/actions/init/prompts.d.ts +4 -0
  8. package/dist/actions/init/prompts.js +10 -0
  9. package/dist/actions/init/setup-husky.d.ts +7 -0
  10. package/dist/actions/init/setup-husky.js +18 -0
  11. package/dist/actions/init/write-vscode.d.ts +4 -0
  12. package/dist/actions/init/write-vscode.js +7 -0
  13. package/dist/actions/scan/index.d.ts +5 -0
  14. package/dist/actions/scan/index.js +20 -0
  15. package/dist/actions/scan/orchestrate.d.ts +6 -0
  16. package/dist/actions/scan/orchestrate.js +49 -0
  17. package/dist/actions/update.d.ts +5 -0
  18. package/dist/actions/update.js +61 -0
  19. package/dist/cli.d.ts +2 -0
  20. package/dist/cli.js +9 -0
  21. package/dist/commands/commit-file-scan.d.ts +2 -0
  22. package/dist/commands/commit-file-scan.js +35 -0
  23. package/dist/commands/commit-msg-scan.d.ts +2 -0
  24. package/dist/commands/commit-msg-scan.js +24 -0
  25. package/dist/commands/fix.d.ts +2 -0
  26. package/dist/commands/fix.js +26 -0
  27. package/dist/commands/index.d.ts +3 -0
  28. package/dist/commands/index.js +15 -0
  29. package/dist/commands/init.d.ts +2 -0
  30. package/dist/commands/init.js +31 -0
  31. package/dist/commands/scan.d.ts +2 -0
  32. package/dist/commands/scan.js +36 -0
  33. package/dist/commands/update.d.ts +2 -0
  34. package/dist/commands/update.js +8 -0
  35. package/dist/config/_editorconfig.ejs +13 -0
  36. package/dist/config/_markdownlint-cli2.cjs.ejs +13 -0
  37. package/dist/config/_stylelintignore.ejs +5 -0
  38. package/dist/config/_vscode/extensions.json.ejs +9 -0
  39. package/dist/config/_vscode/settings.json.ejs +26 -0
  40. package/dist/config/commitlint.config.mjs.ejs +3 -0
  41. package/dist/config/eslint.config.mjs.ejs +16 -0
  42. package/dist/config/linter-spec.config.mjs.ejs +6 -0
  43. package/dist/config/prettier.config.mjs.ejs +10 -0
  44. package/dist/config/stylelint.config.mjs.ejs +5 -0
  45. package/dist/index.d.ts +6 -0
  46. package/dist/index.js +23 -0
  47. package/dist/lints/eslint/do-eslint.d.ts +7 -0
  48. package/dist/lints/eslint/do-eslint.js +31 -0
  49. package/dist/lints/eslint/format-results.d.ts +6 -0
  50. package/dist/lints/eslint/format-results.js +25 -0
  51. package/dist/lints/eslint/get-config-type.d.ts +12 -0
  52. package/dist/lints/eslint/get-config-type.js +30 -0
  53. package/dist/lints/eslint/get-config.d.ts +10 -0
  54. package/dist/lints/eslint/get-config.js +39 -0
  55. package/dist/lints/eslint/index.d.ts +4 -0
  56. package/dist/lints/eslint/index.js +4 -0
  57. package/dist/lints/index.d.ts +4 -0
  58. package/dist/lints/index.js +4 -0
  59. package/dist/lints/markdownlint/do-markdownlint.d.ts +7 -0
  60. package/dist/lints/markdownlint/do-markdownlint.js +33 -0
  61. package/dist/lints/markdownlint/format-results.d.ts +7 -0
  62. package/dist/lints/markdownlint/format-results.js +38 -0
  63. package/dist/lints/markdownlint/get-config.d.ts +10 -0
  64. package/dist/lints/markdownlint/get-config.js +31 -0
  65. package/dist/lints/markdownlint/index.d.ts +3 -0
  66. package/dist/lints/markdownlint/index.js +3 -0
  67. package/dist/lints/prettier/do-prettier.d.ts +2 -0
  68. package/dist/lints/prettier/do-prettier.js +31 -0
  69. package/dist/lints/prettier/index.d.ts +1 -0
  70. package/dist/lints/prettier/index.js +1 -0
  71. package/dist/lints/resolve-files.d.ts +10 -0
  72. package/dist/lints/resolve-files.js +20 -0
  73. package/dist/lints/stylelint/do-stylelint.d.ts +7 -0
  74. package/dist/lints/stylelint/do-stylelint.js +15 -0
  75. package/dist/lints/stylelint/format-results.d.ts +6 -0
  76. package/dist/lints/stylelint/format-results.js +39 -0
  77. package/dist/lints/stylelint/get-config.d.ts +7 -0
  78. package/dist/lints/stylelint/get-config.js +33 -0
  79. package/dist/lints/stylelint/get-doc-url.d.ts +4 -0
  80. package/dist/lints/stylelint/get-doc-url.js +18 -0
  81. package/dist/lints/stylelint/index.d.ts +4 -0
  82. package/dist/lints/stylelint/index.js +4 -0
  83. package/dist/types.d.ts +85 -0
  84. package/dist/types.js +1 -0
  85. package/dist/utils/conflict-resolve.d.ts +2 -0
  86. package/dist/utils/conflict-resolve.js +103 -0
  87. package/dist/utils/constants.d.ts +43 -0
  88. package/dist/utils/constants.js +91 -0
  89. package/dist/utils/errors.d.ts +9 -0
  90. package/dist/utils/errors.js +12 -0
  91. package/dist/utils/generate-template.d.ts +7 -0
  92. package/dist/utils/generate-template.js +67 -0
  93. package/dist/utils/git.d.ts +9 -0
  94. package/dist/utils/git.js +28 -0
  95. package/dist/utils/log.d.ts +8 -0
  96. package/dist/utils/log.js +23 -0
  97. package/dist/utils/messages.d.ts +47 -0
  98. package/dist/utils/messages.js +54 -0
  99. package/dist/utils/npm.d.ts +20 -0
  100. package/dist/utils/npm.js +76 -0
  101. package/dist/utils/print-report.d.ts +5 -0
  102. package/dist/utils/print-report.js +63 -0
  103. package/dist/utils/read-config.d.ts +3 -0
  104. package/dist/utils/read-config.js +15 -0
  105. package/package.json +88 -0
@@ -0,0 +1,36 @@
1
+ import ora from 'ora';
2
+ import scan from '../actions/scan/index.js';
3
+ import printReport from '../utils/print-report.js';
4
+ import { hasLocalLintConfig, installProjectDepsIfMissing } from '../actions/init/install-deps.js';
5
+ import { messages } from '../utils/messages.js';
6
+ export function registerScan(program, cwd) {
7
+ program
8
+ .command('scan')
9
+ .description(messages.scanDescription)
10
+ .option('-q, --quiet', messages.optQuiet)
11
+ .option('-o, --output-report', messages.optOutputReport)
12
+ .option('-i, --include <dirpath>', messages.optInclude)
13
+ .option('--no-ignore', messages.optNoIgnore)
14
+ .action(async (cmd) => {
15
+ await installProjectDepsIfMissing(cwd, hasLocalLintConfig(cwd));
16
+ const checking = ora();
17
+ checking.start(messages.runChecking);
18
+ const { results, errorCount, warningCount, runErrors } = await scan({
19
+ cwd,
20
+ fix: false,
21
+ include: cmd.include || cwd,
22
+ quiet: Boolean(cmd.quiet),
23
+ outputReport: Boolean(cmd.outputReport),
24
+ ignore: cmd.ignore,
25
+ });
26
+ if (runErrors.length > 0 || errorCount > 0)
27
+ checking.fail();
28
+ else if (warningCount > 0)
29
+ checking.warn();
30
+ else
31
+ checking.succeed();
32
+ if (results.length > 0)
33
+ printReport(results, false);
34
+ runErrors.forEach((e) => console.log(e));
35
+ });
36
+ }
@@ -0,0 +1,2 @@
1
+ import type { Command } from 'commander';
2
+ export declare function registerUpdate(program: Command): void;
@@ -0,0 +1,8 @@
1
+ import update from '../actions/update.js';
2
+ import { messages } from '../utils/messages.js';
3
+ export function registerUpdate(program) {
4
+ program
5
+ .command('update')
6
+ .description(messages.updateDescription)
7
+ .action(() => update(true));
8
+ }
@@ -0,0 +1,13 @@
1
+ root = true
2
+
3
+ [*]
4
+ indent_style = space
5
+ indent_size = 2
6
+ end_of_line = lf
7
+ charset = utf-8
8
+ trim_trailing_whitespace = true
9
+ insert_final_newline = true
10
+ quote_type = single
11
+
12
+ [*.md]
13
+ trim_trailing_whitespace = false
@@ -0,0 +1,13 @@
1
+ <%_ if (enableMarkdownlint) { _%>
2
+ // markdownlint-cli2 config. The shared rule set is an ESM-only package,
3
+ // so we require its default export and pass it as cli2's `config` option.
4
+ // Read by markdownlint-cli2 (CLI) and the VS Code markdownlint extension —
5
+ // per the cli2 README, `.markdownlint-cli2.cjs` outranks `.markdownlint.cjs`,
6
+ // so both tools pick up the `ignores` field here.
7
+ const config = require('@linter-spec/markdownlint-config').default;
8
+
9
+ module.exports = {
10
+ config,
11
+ ignores: [<%- markdownLintIgnores.map((txt) => `'${txt}'`).join(', ') %>],
12
+ };
13
+ <%_ } _%>
@@ -0,0 +1,5 @@
1
+ <%_ if (enableStylelint) { _%>
2
+ <%_ stylelintIgnores.forEach((txt) => { _%>
3
+ <%= txt %>
4
+ <%_ }); _%>
5
+ <%_ } _%>
@@ -0,0 +1,9 @@
1
+ {
2
+ "recommendations": [
3
+ "dbaeumer.vscode-eslint",
4
+ "stylelint.vscode-stylelint",
5
+ "esbenp.prettier-vscode",
6
+ "DavidAnson.vscode-markdownlint",
7
+ "EditorConfig.EditorConfig"
8
+ ]
9
+ }
@@ -0,0 +1,26 @@
1
+ <%
2
+ const settings = {};
3
+ if (enableESLint) {
4
+ settings['eslint.validate'] = [
5
+ 'javascript', 'javascriptreact', 'typescript', 'typescriptreact', 'vue',
6
+ ];
7
+ }
8
+ if (enableStylelint) {
9
+ settings['stylelint.validate'] = stylelintExt.map((ext) => ext.replace(/^\./, ''));
10
+ }
11
+ settings['editor.codeActionsOnSave'] = {
12
+ 'source.fixAll.eslint': 'explicit',
13
+ ...(enableStylelint ? { 'source.fixAll.stylelint': 'explicit' } : {}),
14
+ ...(enableMarkdownlint ? { 'source.fixAll.markdownlint': 'explicit' } : {}),
15
+ };
16
+ if (enablePrettier) {
17
+ settings['editor.defaultFormatter'] = 'esbenp.prettier-vscode';
18
+ for (const lang of [
19
+ 'javascript', 'javascriptreact', 'typescript', 'typescriptreact', 'vue',
20
+ 'css', 'less', 'scss', 'html', 'json', 'jsonc',
21
+ ]) {
22
+ settings[`[${lang}]`] = { 'editor.defaultFormatter': 'esbenp.prettier-vscode' };
23
+ }
24
+ }
25
+ settings['editor.formatOnSave'] = Boolean(enablePrettier);
26
+ %><%- JSON.stringify(settings, null, 2) %>
@@ -0,0 +1,3 @@
1
+ export default {
2
+ extends: ['@linter-spec/commitlint-config'],
3
+ };
@@ -0,0 +1,16 @@
1
+ <%
2
+ const spec = eslintType === 'index'
3
+ ? '@linter-spec/eslint-config'
4
+ : `@linter-spec/eslint-config/${eslintType}`;
5
+ %>import config from '<%= spec %>';
6
+
7
+ export default [
8
+ ...config,
9
+ {
10
+ ignores: [
11
+ <%_ eslintIgnores.forEach((pattern) => { _%>
12
+ '<%= pattern %>',
13
+ <%_ }); _%>
14
+ ],
15
+ },
16
+ ];
@@ -0,0 +1,6 @@
1
+ export default {
2
+ enableESLint: <%= enableESLint %>,
3
+ enableStylelint: <%= enableStylelint %>,
4
+ enableMarkdownlint: <%= enableMarkdownlint %>,
5
+ enablePrettier: <%= enablePrettier %>,
6
+ };
@@ -0,0 +1,10 @@
1
+ <%_ if (enablePrettier) { _%>
2
+ export default {
3
+ printWidth: 100,
4
+ tabWidth: 2,
5
+ semi: true,
6
+ singleQuote: true,
7
+ trailingComma: 'all',
8
+ arrowParens: 'always',
9
+ };
10
+ <%_ } _%>
@@ -0,0 +1,5 @@
1
+ <%_ if (enableStylelint) { _%>
2
+ export default {
3
+ extends: ['@linter-spec/stylelint-config'],
4
+ };
5
+ <%_ } _%>
@@ -0,0 +1,6 @@
1
+ import type { InitOptions, ScanOptions, ScanReport } from './types.js';
2
+ /** Programmatically initialise a project (never self-updates the CLI). */
3
+ export declare const init: (options: Omit<InitOptions, "checkVersionUpdate">) => Promise<void>;
4
+ /** Programmatically scan a project, printing a report. */
5
+ export declare const scan: (options: ScanOptions) => Promise<ScanReport>;
6
+ export type { Config, ScanOptions, ScanReport, ScanResult, ScanMessage, InitOptions, } from './types.js';
package/dist/index.js ADDED
@@ -0,0 +1,23 @@
1
+ import ora from 'ora';
2
+ import scanAction from './actions/scan/index.js';
3
+ import initAction from './actions/init/index.js';
4
+ import printReport from './utils/print-report.js';
5
+ import { messages } from './utils/messages.js';
6
+ /** Programmatically initialise a project (never self-updates the CLI). */
7
+ export const init = (options) => initAction({ ...options, checkVersionUpdate: false });
8
+ /** Programmatically scan a project, printing a report. */
9
+ export const scan = async (options) => {
10
+ const checking = ora();
11
+ checking.start(messages.runChecking);
12
+ const report = await scanAction(options);
13
+ const { results, errorCount, warningCount } = report;
14
+ if (errorCount > 0)
15
+ checking.fail();
16
+ else if (warningCount > 0)
17
+ checking.warn();
18
+ else
19
+ checking.succeed();
20
+ if (results.length > 0)
21
+ printReport(results, false);
22
+ return report;
23
+ };
@@ -0,0 +1,7 @@
1
+ import type { Config, PKG, ScanOptions } from '../../types.js';
2
+ import { formatESLintResults } from './format-results.js';
3
+ export interface DoESLintOptions extends ScanOptions {
4
+ pkg: PKG;
5
+ config: Config;
6
+ }
7
+ export declare function doESLint(options: DoESLintOptions): Promise<ReturnType<typeof formatESLintResults>>;
@@ -0,0 +1,31 @@
1
+ import path from 'node:path';
2
+ import { ESLint } from 'eslint';
3
+ import { ESLINT_FILE_EXT } from '../../utils/constants.js';
4
+ import { formatESLintResults } from './format-results.js';
5
+ import { getESLintConfig } from './get-config.js';
6
+ /** Glob (relative to cwd) covering every ESLint-handled extension. */
7
+ function buildPattern(cwd, include) {
8
+ const exts = ESLINT_FILE_EXT.map((ext) => ext.replace(/^\./, '')).join(',');
9
+ const rel = path.relative(cwd, include).replace(/\\/g, '/');
10
+ const prefix = rel && rel !== '.' ? `${rel}/` : '';
11
+ return `${prefix}**/*.{${exts}}`;
12
+ }
13
+ export async function doESLint(options) {
14
+ let patterns;
15
+ if (options.files) {
16
+ const files = options.files.filter((name) => ESLINT_FILE_EXT.includes(path.extname(name)));
17
+ if (files.length === 0)
18
+ return [];
19
+ patterns = files;
20
+ }
21
+ else {
22
+ patterns = [buildPattern(options.cwd, options.include || options.cwd)];
23
+ }
24
+ const eslintOptions = await getESLintConfig(options, options.pkg, options.config);
25
+ const eslint = new ESLint(eslintOptions);
26
+ const reports = await eslint.lintFiles(patterns);
27
+ if (options.fix) {
28
+ await ESLint.outputFixes(reports);
29
+ }
30
+ return formatESLintResults(reports, options.quiet, eslint);
31
+ }
@@ -0,0 +1,6 @@
1
+ import type { ESLint } from 'eslint';
2
+ import type { ScanResult } from '../../types.js';
3
+ /**
4
+ * Normalise ESLint results into the shared `ScanResult` shape.
5
+ */
6
+ export declare function formatESLintResults(results: ESLint.LintResult[], quiet: boolean | undefined, eslint: ESLint): ScanResult[];
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Normalise ESLint results into the shared `ScanResult` shape.
3
+ */
4
+ export function formatESLintResults(results, quiet, eslint) {
5
+ const rulesMeta = eslint.getRulesMetaForResults(results);
6
+ return results
7
+ .filter(({ warningCount, errorCount }) => errorCount || warningCount)
8
+ .map(({ filePath, messages, errorCount, warningCount, fixableErrorCount, fixableWarningCount, }) => ({
9
+ filePath,
10
+ errorCount,
11
+ warningCount: quiet ? 0 : warningCount,
12
+ fixableErrorCount,
13
+ fixableWarningCount: quiet ? 0 : fixableWarningCount,
14
+ messages: messages
15
+ .map(({ line = 0, column = 0, ruleId, message, fatal, severity }) => ({
16
+ line,
17
+ column,
18
+ rule: ruleId,
19
+ url: (ruleId && rulesMeta[ruleId]?.docs?.url) || '',
20
+ message: message.replace(/([^ ])\.$/u, '$1'),
21
+ errored: Boolean(fatal) || severity === 2,
22
+ }))
23
+ .filter(({ errored }) => (quiet ? errored : true)),
24
+ }));
25
+ }
@@ -0,0 +1,12 @@
1
+ import type { PKG } from '../../types.js';
2
+ /**
3
+ * Detect the project type from its files and dependencies.
4
+ * The returned value is a `PROJECT_TYPES` value (e.g. `index`, `typescript`,
5
+ * `typescript/react`) which maps directly onto a `@linter-spec/eslint-config`
6
+ * export subpath.
7
+ */
8
+ export declare function getESLintConfigType(cwd: string, pkg: PKG): string;
9
+ /**
10
+ * Resolve a project type to the `@linter-spec/eslint-config` import specifier.
11
+ */
12
+ export declare function eslintConfigSpecifier(type: string): string;
@@ -0,0 +1,30 @@
1
+ import fg from 'fast-glob';
2
+ /**
3
+ * Detect the project type from its files and dependencies.
4
+ * The returned value is a `PROJECT_TYPES` value (e.g. `index`, `typescript`,
5
+ * `typescript/react`) which maps directly onto a `@linter-spec/eslint-config`
6
+ * export subpath.
7
+ */
8
+ export function getESLintConfigType(cwd, pkg) {
9
+ const tsFiles = fg.sync('./!(node_modules)/**/*.@(ts|tsx)', { cwd });
10
+ const reactFiles = fg.sync('./!(node_modules)/**/*.@(jsx|tsx)', { cwd });
11
+ const vueFiles = fg.sync('./!(node_modules)/**/*.vue', { cwd });
12
+ const dependencies = Object.keys(pkg.dependencies || {});
13
+ const language = tsFiles.length > 0 ? 'typescript' : '';
14
+ let dsl = '';
15
+ if (reactFiles.length > 0 || dependencies.some((name) => /^react(-|$)/.test(name))) {
16
+ dsl = 'react';
17
+ }
18
+ else if (vueFiles.length > 0 || dependencies.some((name) => /^vue(-|$)/.test(name))) {
19
+ dsl = 'vue';
20
+ }
21
+ // `''` -> `index`, `typescript/` -> `typescript`, `/react` -> `react`, etc.
22
+ return `${language}/${dsl}`.replace(/\/$/, '').replace(/^\//, '') || 'index';
23
+ }
24
+ /**
25
+ * Resolve a project type to the `@linter-spec/eslint-config` import specifier.
26
+ */
27
+ export function eslintConfigSpecifier(type) {
28
+ const base = '@linter-spec/eslint-config';
29
+ return type === 'index' ? base : `${base}/${type}`;
30
+ }
@@ -0,0 +1,10 @@
1
+ import type { ESLint } from 'eslint';
2
+ import type { Config, PKG, ScanOptions } from '../../types.js';
3
+ /**
4
+ * Build the ESLint 9 constructor options.
5
+ *
6
+ * - If the project has its own flat config, let ESLint discover it.
7
+ * - Otherwise synthesise a `baseConfig` from the matching
8
+ * `@linter-spec/eslint-config` export (resolved relative to this package).
9
+ */
10
+ export declare function getESLintConfig(opts: ScanOptions, pkg: PKG, config: Config): Promise<ESLint.Options>;
@@ -0,0 +1,39 @@
1
+ import fg from 'fast-glob';
2
+ import { ESLINT_IGNORE_GLOBS } from '../../utils/constants.js';
3
+ import { eslintConfigSpecifier, getESLintConfigType } from './get-config-type.js';
4
+ /** True when the project ships its own ESLint flat config / inline config. */
5
+ function hasUserConfig(cwd, pkg) {
6
+ const flatConfig = fg.sync('eslint.config.@(js|mjs|cjs|ts|mts|cts)', { cwd, dot: true });
7
+ return flatConfig.length > 0 || Boolean(pkg.eslintConfig);
8
+ }
9
+ /**
10
+ * Build the ESLint 9 constructor options.
11
+ *
12
+ * - If the project has its own flat config, let ESLint discover it.
13
+ * - Otherwise synthesise a `baseConfig` from the matching
14
+ * `@linter-spec/eslint-config` export (resolved relative to this package).
15
+ */
16
+ export async function getESLintConfig(opts, pkg, config) {
17
+ const { cwd, fix, ignore } = opts;
18
+ const lintConfig = {
19
+ cwd,
20
+ fix,
21
+ ignore: ignore !== false,
22
+ errorOnUnmatchedPattern: false,
23
+ };
24
+ if (config.eslintOptions) {
25
+ Object.assign(lintConfig, config.eslintOptions);
26
+ return lintConfig;
27
+ }
28
+ if (hasUserConfig(cwd, pkg)) {
29
+ // ESLint will auto-discover the project's own flat config.
30
+ return lintConfig;
31
+ }
32
+ const type = getESLintConfigType(cwd, pkg);
33
+ const specifier = eslintConfigSpecifier(type);
34
+ const mod = (await import(specifier));
35
+ const baseConfig = (mod.default ?? mod);
36
+ lintConfig.overrideConfigFile = true;
37
+ lintConfig.baseConfig = [...baseConfig, { ignores: ESLINT_IGNORE_GLOBS }];
38
+ return lintConfig;
39
+ }
@@ -0,0 +1,4 @@
1
+ export * from './get-config-type.js';
2
+ export * from './get-config.js';
3
+ export * from './format-results.js';
4
+ export * from './do-eslint.js';
@@ -0,0 +1,4 @@
1
+ export * from './get-config-type.js';
2
+ export * from './get-config.js';
3
+ export * from './format-results.js';
4
+ export * from './do-eslint.js';
@@ -0,0 +1,4 @@
1
+ export * from './eslint/index.js';
2
+ export * from './stylelint/index.js';
3
+ export * from './markdownlint/index.js';
4
+ export * from './prettier/index.js';
@@ -0,0 +1,4 @@
1
+ export * from './eslint/index.js';
2
+ export * from './stylelint/index.js';
3
+ export * from './markdownlint/index.js';
4
+ export * from './prettier/index.js';
@@ -0,0 +1,7 @@
1
+ import type { Config, PKG, ScanOptions } from '../../types.js';
2
+ import { formatMarkdownlintResults } from './format-results.js';
3
+ export interface DoMarkdownlintOptions extends ScanOptions {
4
+ pkg: PKG;
5
+ config: Config;
6
+ }
7
+ export declare function doMarkdownlint(options: DoMarkdownlintOptions): Promise<ReturnType<typeof formatMarkdownlintResults>>;
@@ -0,0 +1,33 @@
1
+ import fsExtra from 'fs-extra';
2
+ import markdownlint from 'markdownlint';
3
+ import { applyFixes } from 'markdownlint-rule-helpers';
4
+ import { MARKDOWN_LINT_FILE_EXT, MARKDOWN_LINT_IGNORE_PATTERN } from '../../utils/constants.js';
5
+ import { resolveScanFiles } from '../resolve-files.js';
6
+ import { formatMarkdownlintResults } from './format-results.js';
7
+ import { getMarkdownlintConfig } from './get-config.js';
8
+ const { readFile, writeFile } = fsExtra;
9
+ export async function doMarkdownlint(options) {
10
+ const files = await resolveScanFiles(options, MARKDOWN_LINT_FILE_EXT, MARKDOWN_LINT_IGNORE_PATTERN);
11
+ if (files.length === 0)
12
+ return [];
13
+ const results = await markdownlint.promises.markdownlint({
14
+ ...getMarkdownlintConfig(options, options.pkg, options.config),
15
+ files,
16
+ });
17
+ if (options.fix) {
18
+ await Promise.all(Object.keys(results).map((filename) => fixMarkdownFile(filename, results[filename] ?? [])));
19
+ }
20
+ return formatMarkdownlintResults(results, options.quiet);
21
+ }
22
+ async function fixMarkdownFile(filename, errors) {
23
+ const fixes = errors?.filter((error) => error.fixInfo);
24
+ if (fixes?.length > 0) {
25
+ const originalText = await readFile(filename, 'utf8');
26
+ const fixedText = applyFixes(originalText, fixes);
27
+ if (originalText !== fixedText) {
28
+ await writeFile(filename, fixedText, 'utf8');
29
+ return errors.filter((error) => !error.fixInfo);
30
+ }
31
+ }
32
+ return errors;
33
+ }
@@ -0,0 +1,7 @@
1
+ import type markdownlint from 'markdownlint';
2
+ import type { ScanResult } from '../../types.js';
3
+ /**
4
+ * Normalise markdownlint results into the shared `ScanResult` shape.
5
+ * markdownlint findings are always warnings.
6
+ */
7
+ export declare function formatMarkdownlintResults(results: markdownlint.LintResults, quiet: boolean | undefined): ScanResult[];
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Normalise markdownlint results into the shared `ScanResult` shape.
3
+ * markdownlint findings are always warnings.
4
+ */
5
+ export function formatMarkdownlintResults(results, quiet) {
6
+ const parsedResults = [];
7
+ for (const file in results) {
8
+ if (!Object.prototype.hasOwnProperty.call(results, file) || quiet)
9
+ continue;
10
+ const fileErrors = results[file];
11
+ if (!fileErrors)
12
+ continue;
13
+ let warningCount = 0;
14
+ let fixableWarningCount = 0;
15
+ const messages = fileErrors.map(({ lineNumber, ruleNames, ruleDescription, ruleInformation, errorRange, fixInfo }) => {
16
+ if (fixInfo)
17
+ fixableWarningCount++;
18
+ warningCount++;
19
+ return {
20
+ line: lineNumber,
21
+ column: Array.isArray(errorRange) ? (errorRange[0] ?? 1) : 1,
22
+ rule: ruleNames[0] ?? null,
23
+ url: ruleInformation ?? '',
24
+ message: ruleDescription,
25
+ errored: false,
26
+ };
27
+ });
28
+ parsedResults.push({
29
+ filePath: file,
30
+ messages,
31
+ errorCount: 0,
32
+ warningCount,
33
+ fixableErrorCount: 0,
34
+ fixableWarningCount,
35
+ });
36
+ }
37
+ return parsedResults;
38
+ }
@@ -0,0 +1,10 @@
1
+ import markdownlint from 'markdownlint';
2
+ import type { Config, PKG, ScanOptions } from '../../types.js';
3
+ export type MarkdownlintOptions = markdownlint.Options & {
4
+ fix?: boolean;
5
+ };
6
+ /**
7
+ * Build the markdownlint options. Uses the shared config unless the project
8
+ * provides its own `.markdownlint.*`.
9
+ */
10
+ export declare function getMarkdownlintConfig(opts: ScanOptions, _pkg: PKG, config: Config): MarkdownlintOptions;
@@ -0,0 +1,31 @@
1
+ import path from 'node:path';
2
+ import fg from 'fast-glob';
3
+ import markdownlint from 'markdownlint';
4
+ import markdownlintConfig from '@linter-spec/markdownlint-config';
5
+ /**
6
+ * Build the markdownlint options. Uses the shared config unless the project
7
+ * provides its own `.markdownlint.*`.
8
+ */
9
+ export function getMarkdownlintConfig(opts, _pkg, config) {
10
+ const { cwd } = opts;
11
+ const lintConfig = {
12
+ fix: Boolean(opts.fix),
13
+ resultVersion: 3,
14
+ };
15
+ if (config.markdownlintOptions) {
16
+ Object.assign(lintConfig, config.markdownlintOptions);
17
+ return lintConfig;
18
+ }
19
+ const userConfigFiles = fg.sync('.markdownlint?(-cli2).@(jsonc|json|yaml|yml)', {
20
+ cwd,
21
+ dot: true,
22
+ });
23
+ const [userConfigFile] = userConfigFiles;
24
+ if (!userConfigFile) {
25
+ lintConfig.config = markdownlintConfig;
26
+ }
27
+ else {
28
+ lintConfig.config = markdownlint.readConfigSync(path.resolve(cwd, userConfigFile));
29
+ }
30
+ return lintConfig;
31
+ }
@@ -0,0 +1,3 @@
1
+ export * from './get-config.js';
2
+ export * from './format-results.js';
3
+ export * from './do-markdownlint.js';
@@ -0,0 +1,3 @@
1
+ export * from './get-config.js';
2
+ export * from './format-results.js';
3
+ export * from './do-markdownlint.js';
@@ -0,0 +1,2 @@
1
+ import type { ScanOptions } from '../../types.js';
2
+ export declare function doPrettier(options: ScanOptions): Promise<void>;
@@ -0,0 +1,31 @@
1
+ import os from 'node:os';
2
+ import fsExtra from 'fs-extra';
3
+ import prettier from 'prettier';
4
+ import { PRETTIER_FILE_EXT, PRETTIER_IGNORE_PATTERN } from '../../utils/constants.js';
5
+ import { resolveScanFiles } from '../resolve-files.js';
6
+ const { readFile, writeFile } = fsExtra;
7
+ // Cap how many files Prettier formats at once: an unbounded `Promise.all` over
8
+ // thousands of files would spawn that many concurrent reads + format passes and
9
+ // can exhaust file handles / memory on large projects.
10
+ const CONCURRENCY = Math.max(4, os.cpus().length);
11
+ export async function doPrettier(options) {
12
+ const files = await resolveScanFiles(options, PRETTIER_FILE_EXT, PRETTIER_IGNORE_PATTERN);
13
+ for (let i = 0; i < files.length; i += CONCURRENCY) {
14
+ await Promise.all(files.slice(i, i + CONCURRENCY).map(formatFile));
15
+ }
16
+ }
17
+ async function formatFile(filepath) {
18
+ // Skip files Prettier can't parse / is configured to ignore.
19
+ const info = await prettier.getFileInfo(filepath, { resolveConfig: true });
20
+ if (info.ignored || !info.inferredParser)
21
+ return;
22
+ const text = await readFile(filepath, 'utf8');
23
+ const config = await prettier.resolveConfig(filepath);
24
+ // Prettier 3: `format` is async. Default `endOfLine` to `auto` so a file keeps
25
+ // its existing line endings (a CRLF file stays CRLF) when the user's resolved
26
+ // config doesn't pin `endOfLine`; their config still wins via the spread order.
27
+ const formatted = await prettier.format(text, { endOfLine: 'auto', ...config, filepath });
28
+ if (formatted !== text) {
29
+ await writeFile(filepath, formatted, 'utf8');
30
+ }
31
+ }
@@ -0,0 +1 @@
1
+ export * from './do-prettier.js';
@@ -0,0 +1 @@
1
+ export * from './do-prettier.js';
@@ -0,0 +1,10 @@
1
+ import type { ScanOptions } from '../types.js';
2
+ /**
3
+ * Resolve the absolute file list a linter should process: either the explicitly
4
+ * passed `options.files` (filtered to `exts`, resolved against `cwd`) or a
5
+ * fast-glob sweep of `options.include` for those extensions, honoring `ignore`.
6
+ *
7
+ * ESLint does its own globbing and works in relative patterns, so it does not
8
+ * use this helper — see `do-eslint.ts`.
9
+ */
10
+ export declare function resolveScanFiles(options: ScanOptions, exts: string[], ignore: string[]): Promise<string[]>;
@@ -0,0 +1,20 @@
1
+ import path from 'node:path';
2
+ import fg from 'fast-glob';
3
+ /**
4
+ * Resolve the absolute file list a linter should process: either the explicitly
5
+ * passed `options.files` (filtered to `exts`, resolved against `cwd`) or a
6
+ * fast-glob sweep of `options.include` for those extensions, honoring `ignore`.
7
+ *
8
+ * ESLint does its own globbing and works in relative patterns, so it does not
9
+ * use this helper — see `do-eslint.ts`.
10
+ */
11
+ export async function resolveScanFiles(options, exts, ignore) {
12
+ if (options.files) {
13
+ return options.files
14
+ .filter((name) => exts.includes(path.extname(name)))
15
+ .map((name) => path.resolve(options.cwd, name));
16
+ }
17
+ const globExts = exts.map((ext) => ext.replace(/^\./, '')).join(',');
18
+ const include = options.include || options.cwd;
19
+ return fg(`**/*.{${globExts}}`, { cwd: include, ignore, absolute: true });
20
+ }
@@ -0,0 +1,7 @@
1
+ import type { Config, PKG, ScanOptions } from '../../types.js';
2
+ import { formatStylelintResults } from './format-results.js';
3
+ export interface DoStylelintOptions extends ScanOptions {
4
+ pkg: PKG;
5
+ config: Config;
6
+ }
7
+ export declare function doStylelint(options: DoStylelintOptions): Promise<ReturnType<typeof formatStylelintResults>>;