Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .markdownlint-cli2.mjs

This file was deleted.

3 changes: 0 additions & 3 deletions .markdownlint.mjs

This file was deleted.

10 changes: 2 additions & 8 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,10 @@ repos:
# language: system because the config packages aren't published yet.
# Consumers should use language: node with additional_dependencies.
- hooks:
# ESLint handles both linting and formatting (via eslint-plugin-format).
# Must run before markdownlint so tables are formatted first.
# ESLint handles linting, formatting (via eslint-plugin-format),
# and markdown structural checks (via eslint-plugin-markdownlint).
- entry: pnpm exec eslint --fix --max-warnings=0
id: eslint
language: system
name: eslint
repo: local
- hooks:
- args: [--fix]
id: markdownlint-cli2
name: '[Markdown] Lint/Fix'
repo: https://github.com/DavidAnson/markdownlint-cli2
rev: v0.22.0
26 changes: 12 additions & 14 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ README.md — Consumer-facing documentation
pre-commit.yml — Run prek hooks on PR changed files
pre-commit-seed.yml — Seed prek cache on push to main
packages/
cli/ — @gtbuchanan/cli (gtb build CLI for consumers)
eslint-config/ — @gtbuchanan/eslint-config (ESLint configure())
markdownlint-config/ — @gtbuchanan/markdownlint-config (markdownlint configure())
tsconfig/ — @gtbuchanan/tsconfig (shared base tsconfig.json)
vitest-config/ — @gtbuchanan/vitest-config (configurePackage, configureGlobal, + e2e variants)
test-utils/ — private shared E2E fixture utilities
cli/ — @gtbuchanan/cli (gtb build CLI for consumers)
eslint-config/ — @gtbuchanan/eslint-config (ESLint configure())
eslint-plugin-markdownlint/ — @gtbuchanan/eslint-plugin-markdownlint (markdownlint via ESLint)
tsconfig/ — @gtbuchanan/tsconfig (shared base tsconfig.json)
vitest-config/ — @gtbuchanan/vitest-config (configurePackage, configureGlobal, + e2e variants)
test-utils/ — private shared E2E fixture utilities
```

## Architecture
Expand Down Expand Up @@ -117,6 +117,7 @@ globs for monorepos, or falls back to single-package mode.
`eslint-plugin-import-x` (ordering), `@eslint/json` (JSON linting),
`eslint-plugin-pnpm` (workspace validation), `eslint-plugin-n` (Node.js
rules), `eslint-plugin-yml` (YAML linting + key sorting),
`eslint-plugin-markdownlint` (Markdown structural linting),
`@vitest/eslint-plugin` (test rules), and `eslint-plugin-only-warn`
(downgrades errors to warnings).

Expand All @@ -131,20 +132,13 @@ globs for monorepos, or falls back to single-package mode.
package's dependencies for reliable resolution under pnpm strict
hoisting.

### Markdown linter

- **markdownlint-cli2** — Structural linting for Markdown files.
`@gtbuchanan/markdownlint-config` extends `markdownlint/style/prettier` to
disable rules that conflict with Prettier formatting.

### Pre-commit hooks

- **prek** — Rust-based pre-commit hook manager (drop-in replacement for
Python pre-commit). Installed automatically via `prepare` script on
`pnpm install`. Hooks defined in `.pre-commit-config.yaml`:
- `pre-commit-hooks` — file hygiene (large files, EOF newlines, BOM, trailing whitespace, no commit to branch)
- `eslint` — linting and formatting with `--fix` (JS/TS/JSON/Markdown/YAML)
- `markdownlint-cli2` — Markdown structural linting with `--fix`
- `eslint` — linting, formatting, and Markdown structural checks with `--fix`

### CI/CD workflows

Expand Down Expand Up @@ -262,6 +256,10 @@ Consumer guidance:
- All lint violations report as warnings in IDEs (not errors) so TypeScript
diagnostics stand out. CI enforces via `--max-warnings=0`.
- Inline suppressions require a `--` reason suffix.
- Markdown structural rules (`markdownlint/lint`) use markdownlint's
own comment syntax for per-rule suppression, not ESLint comments:
`<!-- markdownlint-disable-next-line MD024 -->`. This keeps the
plugin compatible with standalone markdownlint usage.
- All exported functions, types, interfaces, and constants must have JSDoc comments.
- When asserting on `CommandResult` (exit code, stdout, stderr), use
`expect(result).toMatchObject({ exitCode: 0 })` instead of
Expand Down
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ Shared build configuration monorepo for JavaScript/TypeScript projects.

## Packages

| Package | Description |
| --------------------------------------------------------------- | ------------------------------------ |
| [@gtbuchanan/cli](packages/cli) | Shared build CLI (`gtb`) |
| [@gtbuchanan/eslint-config](packages/eslint-config) | Shared ESLint configuration |
| [@gtbuchanan/markdownlint-config](packages/markdownlint-config) | Shared markdownlint configuration |
| [@gtbuchanan/tsconfig](packages/tsconfig) | Shared TypeScript base configuration |
| [@gtbuchanan/vitest-config](packages/vitest-config) | Shared Vitest configuration |
| Package | Description |
| ----------------------------------------------------------------------------- | ------------------------------------ |
| [@gtbuchanan/cli](packages/cli) | Shared build CLI (`gtb`) |
| [@gtbuchanan/eslint-config](packages/eslint-config) | Shared ESLint configuration |
| [@gtbuchanan/eslint-plugin-markdownlint](packages/eslint-plugin-markdownlint) | ESLint plugin wrapping markdownlint |
| [@gtbuchanan/tsconfig](packages/tsconfig) | Shared TypeScript base configuration |
| [@gtbuchanan/vitest-config](packages/vitest-config) | Shared Vitest configuration |

## Reusable Workflows

Expand Down
10 changes: 5 additions & 5 deletions codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ component_management:
name: test-utils
paths:
- packages/test-utils/src/**
- component_id: markdownlint-config
name: markdownlint-config
- component_id: eslint-plugin-markdownlint
name: eslint-plugin-markdownlint
paths:
- packages/markdownlint-config/src/**
- packages/eslint-plugin-markdownlint/src/**
- component_id: eslint-config
name: eslint-config
paths:
Expand Down Expand Up @@ -49,10 +49,10 @@ flags:
carryforward: true
paths:
- packages/eslint-config/
markdownlint-config:
eslint-plugin-markdownlint:
carryforward: true
paths:
- packages/markdownlint-config/
- packages/eslint-plugin-markdownlint/
test-utils:
carryforward: true
paths:
Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@
"eslint": "catalog:",
"find-up-simple": "catalog:",
"jiti": "catalog:",
"markdownlint-cli2": "catalog:",
"prettier": "catalog:",
"turbo": "catalog:",
"typescript": "catalog:",
Expand Down
19 changes: 19 additions & 0 deletions packages/eslint-config/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,23 @@ export default configure({
- `eslint-plugin-n` — Node.js best practices
- `eslint-plugin-pnpm` — pnpm workspace validation (opt-in)
- `eslint-plugin-yml` — YAML linting and key sorting
- `@gtbuchanan/eslint-plugin-markdownlint` — Markdown structural linting via markdownlint
- `eslint-plugin-only-warn` — Downgrades errors to warnings (opt-in)

## Markdown suppression

Markdown structural rules run as a single `markdownlint/lint` ESLint rule.
To suppress a specific markdownlint rule, use markdownlint's own comment
syntax (not ESLint comments):

```markdown
<!-- markdownlint-disable-next-line MD024 -->

# Duplicate heading allowed here
```

ESLint's `<!-- eslint-disable markdownlint/lint -->` suppresses all
markdownlint rules at once. Use markdownlint directives for per-rule
control. See the
[markdownlint docs](https://github.com/DavidAnson/markdownlint#configuration)
for the full inline directive syntax.
63 changes: 62 additions & 1 deletion packages/eslint-config/e2e/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ const createFixture = () => {
depsPackages: ['typescript'],
hookPackages: ['eslint', 'jiti'],
packageName: '@gtbuchanan/eslint-config',
workspaceDeps: [],
workspaceDeps: ['@gtbuchanan/eslint-plugin-markdownlint'],
});

const eslint = path.join(fixture.hookDir, 'node_modules/.bin/eslint');
Expand Down Expand Up @@ -254,6 +254,67 @@ describe.concurrent('eslint CLI integration', () => {
);
});

it('detects markdownlint violations in markdown files', async ({ fixture, expect }) => {
const result = await fixture.run({
files: { 'doc.md': '# Title\n\n# Title\n' },
});

expect(result.stdout).toContain('markdownlint/lint');
expect(result.stdout).toMatch(/MD024/v);
});

it('suppresses markdownlint rules disabled by prettier style', async ({ fixture, expect }) => {
// heading-style (md003) is disabled — mixed ATX/setext should pass
const result = await fixture.run({
files: { 'doc.md': '# ATX heading\n\nSetext heading\n---\n' },
});

expect(result.stdout).not.toMatch(/MD003/v);
});

it('runs both Prettier formatting and markdownlint on markdown', async ({ fixture, expect }) => {
/*
* Prettier (format/prettier) and markdownlint (markdownlint/lint)
* both target *.md. The markdownlint parser must override
* format.parserPlain so both rule sets work on the same file.
* Misformatted table triggers Prettier, duplicate heading triggers MD024.
*/
const result = await fixture.run({
files: {
'doc.md': '# Title\n\n| a|b |\n|---|---|\n| 1|2 |\n\n# Title\n',
},
});

expect(result.stdout).toContain('format/prettier');
expect(result.stdout).toContain('markdownlint/lint');
});

it('applies markdownlint autofix via --fix', async ({ fixture, expect }) => {
// MD034: no-bare-urls (fixable, not disabled by prettier conflicts)
const result = await fixture.run({
config: createRequireOnlyWarnConfig,
files: { 'doc.md': '# Title\n\nVisit https://example.com today.\n' },
flags: ['--fix'],
});

expect(result).toMatchObject({ exitCode: 0 });
expect(result.readFile('doc.md')).toBe(
'# Title\n\nVisit <https://example.com> today.\n',
);
});

it('ignores .changeset/ files for markdownlint', async ({ fixture, expect }) => {
// Changeset file without a heading — would fail MD041 if not ignored
const result = await fixture.run({
files: {
'.changeset/test-changeset.md': 'No heading here\n',
'doc.md': '# Valid\n\nContent.\n',
},
});

expect(result.stdout).not.toMatch(/MD041/v);
});

it('formats XML with whitespace-insensitive mode', async ({ fixture, expect }) => {
const uglyXml = '<Project><PropertyGroup><Version>1.0</Version></PropertyGroup></Project>';

Expand Down
1 change: 1 addition & 0 deletions packages/eslint-config/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"dependencies": {
"@eslint-community/eslint-plugin-eslint-comments": "catalog:",
"@eslint/json": "catalog:",
"@gtbuchanan/eslint-plugin-markdownlint": "workspace:*",
"@prettier/plugin-xml": "catalog:",
"@stylistic/eslint-plugin": "catalog:",
"@vitest/eslint-plugin": "catalog:",
Expand Down
3 changes: 2 additions & 1 deletion packages/eslint-config/src/plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import format from './format.ts';
import importX from './import-x.ts';
import jsdoc from './jsdoc.ts';
import json from './json.ts';
import markdownlint from './markdownlint.ts';
import node from './node.ts';
import pnpm from './pnpm.ts';
import promise from './promise.ts';
Expand All @@ -18,6 +19,6 @@ import yaml from './yaml.ts';
/** Ordered plugin factories. Later entries override earlier ones for the same file. */
export const plugins: readonly PluginFactory[] = [
typescript, unicorn, promise, regexp, jsdoc, json, yaml, pnpm, node,
format, stylistic, eslintComments, importX,
format, markdownlint, stylistic, eslintComments, importX,
core, vitest,
];
60 changes: 60 additions & 0 deletions packages/eslint-config/src/plugins/markdownlint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import markdownlint from '@gtbuchanan/eslint-plugin-markdownlint';
import { parser as markdownlintParser } from '@gtbuchanan/eslint-plugin-markdownlint';
import type { PluginFactory } from '../index.ts';

/*
* Rules from markdownlint/style/prettier (markdownlint@0.40.0) that
* conflict with Prettier formatting (handled by eslint-plugin-format).
* md047 (single-trailing-newline) is also disabled because Prettier
* and pre-commit hooks handle trailing newlines.
*/
const prettierConflicts = {
'blanks-around-fences': false, // md031
'blanks-around-headings': false, // md022
'blanks-around-lists': false, // md032
'code-fence-style': false, // md046
'emphasis-style': false, // md049
'heading-start-left': false, // md023
'heading-style': false, // md003
'hr-style': false, // md035
'line-length': false, // md013
'list-indent': false, // md005
'list-marker-space': false, // md030
'no-blanks-blockquote': false, // md028
'no-hard-tabs': false, // md010
'no-missing-space-atx': false, // md018
'no-missing-space-closed-atx': false, // md020
'no-multiple-blanks': false, // md012
'no-multiple-space-atx': false, // md019
'no-multiple-space-blockquote': false, // md027
'no-multiple-space-closed-atx': false, // md021
'no-trailing-spaces': false, // md009
'ol-prefix': false, // md029
'single-trailing-newline': false, // md047
'strong-style': false, // md050
'ul-indent': false, // md007
} as const;

/**
* markdownlint structural linting via `@gtbuchanan/eslint-plugin-markdownlint`.
* Must be registered after the format plugin so the markdownlint
* parser overrides format.parserPlain for `*.md` files — the
* format/prettier rule only needs source text access, which the
* markdownlint parser provides.
*/
const plugin: PluginFactory = () => [
{
files: ['**/*.md'],
ignores: ['.changeset/**'],
languageOptions: { parser: markdownlintParser },
plugins: { markdownlint },
rules: {
'markdownlint/lint': ['warn', {
default: true,
...prettierConflicts,
}],
},
},
];

export default plugin;
33 changes: 33 additions & 0 deletions packages/eslint-config/test/configure.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,39 @@ describe(configure, () => {
expect(ignoresConfig?.ignores).toStrictEqual(['vendor/**']);
});

it('includes markdownlint/lint for markdown files', async ({ expect }) => {
const configs = await configure({ onlyWarn: false });

const mdlConfig = configs.find(
cfg => cfg.rules?.['markdownlint/lint'] !== undefined,
);

expect(mdlConfig?.files).toStrictEqual(['**/*.md']);
});

it('disables Prettier-conflicting markdownlint rules', async ({ expect }) => {
const configs = await configure({ onlyWarn: false });

const mdlConfig = configs.find(
cfg => cfg.rules?.['markdownlint/lint'] !== undefined,
);
const [, ruleConfig] = mdlConfig?.rules?.['markdownlint/lint'] as
[string, Record<string, unknown>];

expect(ruleConfig['line-length']).toBe(false);
expect(ruleConfig['single-trailing-newline']).toBe(false);
});

it('ignores .changeset files for markdownlint', async ({ expect }) => {
const configs = await configure({ onlyWarn: false });

const mdlConfig = configs.find(
cfg => cfg.rules?.['markdownlint/lint'] !== undefined,
);

expect(mdlConfig?.ignores).toContain('.changeset/**');
});

it('includes format/prettier for all supported file types', async ({ expect }) => {
const configs = await configure({ onlyWarn: false });

Expand Down
7 changes: 7 additions & 0 deletions packages/eslint-plugin-markdownlint/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# @gtbuchanan/eslint-plugin-markdownlint

## 0.1.0

### Minor Changes

- Initial release
Loading
Loading