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
9 changes: 2 additions & 7 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,9 @@ repos:
# language: system because the config packages aren't published yet.
# Consumers should use language: node with additional_dependencies.
- hooks:
# oxfmt must run before markdownlint so tables are formatted first.
- entry: pnpm exec oxfmt --write
files: \.(json[5c]?|md|ya?ml)$
id: oxfmt
language: system
name: oxfmt
# ESLint handles both linting and formatting (via eslint-plugin-format).
# Must run before markdownlint so tables are formatted first.
- entry: pnpm exec eslint --fix --max-warnings=0
files: \.(ts|mts|cts|js|mjs|cjs|json|ya?ml)$
id: eslint
language: system
name: eslint
Expand Down
41 changes: 24 additions & 17 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# @gtbuchanan/tooling

Shared build configuration monorepo. Individual packages for ESLint, oxfmt,
Shared build configuration monorepo. Individual packages for ESLint,
TypeScript, Vitest configuration, and a shared build CLI.

## Structure
Expand All @@ -22,7 +22,6 @@ packages/
cli/ — @gtbuchanan/cli (gtb build CLI for consumers)
eslint-config/ — @gtbuchanan/eslint-config (ESLint configure())
markdownlint-config/ — @gtbuchanan/markdownlint-config (markdownlint configure())
oxfmt-config/ — @gtbuchanan/oxfmt-config (oxfmt configure())
tsconfig/ — @gtbuchanan/tsconfig (shared base tsconfig.json)
vitest-config/ — @gtbuchanan/vitest-config (configurePackage, configureGlobal, + e2e variants)
test-utils/ — private shared E2E fixture utilities
Expand Down Expand Up @@ -108,36 +107,44 @@ globs for monorepos, or falls back to single-package mode.

### Linter

- **ESLint** — Primary linter. `typescript-eslint` strictTypeChecked +
stylisticTypeChecked presets, `eslint-plugin-unicorn` (recommended),
`eslint-plugin-promise`, `@stylistic/eslint-plugin` (formatting),
`@eslint-community/eslint-plugin-eslint-comments`, `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), `@vitest/eslint-plugin` (test rules), and
`eslint-plugin-only-warn` (downgrades errors to warnings).
- **ESLint** — Primary linter and formatter. `typescript-eslint`
strictTypeChecked + stylisticTypeChecked presets, `eslint-plugin-unicorn`
(recommended), `eslint-plugin-promise`, `eslint-plugin-regexp` (regex
safety), `eslint-plugin-jsdoc` (JSDoc/TSDoc validation),
`@stylistic/eslint-plugin` (JS/TS formatting), `eslint-plugin-format`
(Prettier formatting for JSON, Markdown, YAML, CSS, XML via ESLint
rules), `@eslint-community/eslint-plugin-eslint-comments`,
`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),
`@vitest/eslint-plugin` (test rules), and `eslint-plugin-only-warn`
(downgrades errors to warnings).

### Formatter

- **oxfmt** — Formats non-JS/TS files (JSON, Markdown, YAML, etc.).
JS/TS files are ignored via `ignorePatterns` because `@stylistic` handles
formatting through ESLint.
- **Prettier (via eslint-plugin-format)** — Formats non-JS/TS files
(JSON, Markdown, YAML, CSS/SCSS/Less, XML) through ESLint rules.
JS/TS formatting is handled by `@stylistic/eslint-plugin`. Prettier
plugins (`prettier-plugin-sort-json`, `prettier-plugin-multiline-arrays`,
`prettier-plugin-packagejson`, `prettier-plugin-css-order`,
`@prettier/plugin-xml`) are resolved as `file://` URLs from this
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 oxfmt formatting.
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` — JS/TS linting with `--fix`
- `markdownlint-cli2` — Markdown linting with `--fix`
- `oxfmt` — JSON/Markdown/YAML formatting (local system hook)
- `eslint` — linting and formatting with `--fix` (JS/TS/JSON/Markdown/YAML)
- `markdownlint-cli2` — Markdown structural linting with `--fix`

### CI/CD workflows

Expand Down
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ Shared build configuration monorepo for JavaScript/TypeScript projects.
| [@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/oxfmt-config](packages/oxfmt-config) | Shared oxfmt configuration |
| [@gtbuchanan/tsconfig](packages/tsconfig) | Shared TypeScript base configuration |
| [@gtbuchanan/vitest-config](packages/vitest-config) | Shared Vitest configuration |

Expand Down
8 changes: 0 additions & 8 deletions codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,6 @@ component_management:
name: test-utils
paths:
- packages/test-utils/src/**
- component_id: oxfmt-config
name: oxfmt-config
paths:
- packages/oxfmt-config/src/**
- component_id: markdownlint-config
name: markdownlint-config
paths:
Expand Down Expand Up @@ -57,10 +53,6 @@ flags:
carryforward: true
paths:
- packages/markdownlint-config/
oxfmt-config:
carryforward: true
paths:
- packages/oxfmt-config/
test-utils:
carryforward: true
paths:
Expand Down
3 changes: 0 additions & 3 deletions oxfmt.config.ts

This file was deleted.

7 changes: 3 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
"@changesets/cli": "catalog:",
"@gtbuchanan/cli": "workspace:*",
"@gtbuchanan/eslint-config": "workspace:*",
"@gtbuchanan/oxfmt-config": "workspace:*",
"@gtbuchanan/tsconfig": "workspace:*",
"@gtbuchanan/vitest-config": "workspace:*",
"@j178/prek": "catalog:",
Expand All @@ -39,14 +38,14 @@
"find-up-simple": "catalog:",
"jiti": "catalog:",
"markdownlint-cli2": "catalog:",
"oxfmt": "catalog:",
"prettier": "catalog:",
"turbo": "catalog:",
"typescript": "catalog:",
"valibot": "catalog:",
"vitest": "catalog:"
},
"packageManager": "pnpm@10.32.1",
"engines": {
"node": ">=22.17"
},
"packageManager": "pnpm@10.32.1"
}
}
30 changes: 15 additions & 15 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,15 @@
"name": "@gtbuchanan/cli",
"version": "0.1.0",
"description": "Shared build CLI",
"bin": {
"gtb": "./dist/source/bin/gtb.js"
},
"type": "module",
"imports": {
"#src/*": "./src/*"
},
"exports": {
".": "./src/index.ts"
},
"publishConfig": {
"bin": {
"gtb": "./bin/gtb.js"
},
"directory": "dist/source",
"exports": {
".": "./src/index.js"
},
"imports": {
"#src/*.ts": "./src/*.js"
},
"linkDirectory": false
"bin": {
"gtb": "./dist/source/bin/gtb.js"
},
"scripts": {
"compile:ts": "pnpm run gtb compile:ts",
Expand Down Expand Up @@ -54,5 +41,18 @@
},
"engines": {
"node": ">=22.17"
},
"publishConfig": {
"bin": {
"gtb": "./bin/gtb.js"
},
"directory": "dist/source",
"exports": {
".": "./src/index.js"
},
"imports": {
"#src/*.ts": "./src/*.js"
},
"linkDirectory": false
}
}
4 changes: 2 additions & 2 deletions packages/cli/src/lib/file-writer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@ export const mergePackageScripts = (

/*
* Write back to the raw parsed object instead of the valibot output.
* Valibot reorders keys (schema-defined first, rest after), but oxfmt
* expects conventional package.json key order to be preserved.
* Valibot reorders keys (schema-defined first, rest after), but
* Prettier expects conventional package.json key order to be preserved.
*/
raw['scripts'] = sortKeysDeep(merged);
writeJsonFile(path, raw);
Expand Down
5 changes: 4 additions & 1 deletion packages/cli/src/lib/turbo-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,10 @@ export interface ToolFlags {
readonly hasVitest: boolean;
}

/** @internal Exported for script generation. */
/**
* Exported for script generation.
* @internal
*/
export const resolveToolFlags = (discovery: WorkspaceDiscovery): ToolFlags => {
const hasEslint = discovery.packages.some(pkg => pkg.hasEslint);
const hasLint = hasEslint;
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/test/file-writer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ describe(mergeCodecovSections, () => {
mergeCodecovSections(filePath, sections);

const content = readFileSync(filePath, 'utf8');
const keys = [...content.matchAll(/^(?<key>\w[\w_]*):/gmv)]
const keys = [...content.matchAll(/^(?<key>\w+):/gmv)]
.map(match => match.groups?.['key'] ?? '');

expect(keys).toStrictEqual([...keys].toSorted((left, right) => left.localeCompare(right)));
Expand Down
4 changes: 3 additions & 1 deletion packages/eslint-config/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ interface ModuleMap {
// createRequire bridges ESM→CJS resolution, which respects NODE_PATH (set by pre-commit)
const { resolve } = createRequire(import.meta.url);

async function importModule<S extends keyof ModuleMap>(specifier: S): Promise<ModuleMap[S]> {
async function importModule<S extends keyof ModuleMap>(
specifier: S,
): Promise<ModuleMap[S]> {
const { href } = pathToFileURL(resolve(specifier));
const module: ModuleMap[S] = await import(href);
return module;
Expand Down
58 changes: 55 additions & 3 deletions packages/eslint-config/e2e/cli.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { mkdirSync, writeFileSync } from 'node:fs';
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import path from 'node:path';
import { createIsolatedFixture, runCommand } from '@gtbuchanan/test-utils';
import { it as base, describe } from 'vitest';
Expand Down Expand Up @@ -45,6 +45,7 @@ interface RunOptions {
config?: string;
env?: Record<string, string | undefined>;
files: Record<string, string>;
flags?: readonly string[];
}

const createFixture = () => {
Expand All @@ -57,7 +58,7 @@ const createFixture = () => {

const eslint = path.join(fixture.hookDir, 'node_modules/.bin/eslint');

const run = ({ config, env, files }: RunOptions) => {
const run = ({ config, env, files, flags = [] }: RunOptions) => {
writeFileSync(path.join(fixture.projectDir, 'eslint.config.ts'), config ?? createRequireConfig);
writeFileSync(path.join(fixture.projectDir, 'tsconfig.json'), tsconfig);
writeFileSync(path.join(fixture.projectDir, 'tsconfig.root.json'), tsconfigRoot);
Expand All @@ -69,7 +70,7 @@ const createFixture = () => {
writeFileSync(filePath, content);
}

return runCommand(eslint, fileNames, {
return runCommand(eslint, [...flags, ...fileNames], {
cwd: fixture.projectDir,
env: {
...process.env,
Expand All @@ -79,10 +80,14 @@ const createFixture = () => {
});
};

const readFile = (name: string): string =>
readFileSync(path.join(fixture.projectDir, name), 'utf8');

return {
eslint,
nodePath: fixture.nodePath,
projectDir: fixture.projectDir,
readFile,
run,
[Symbol.dispose]() {
fixture[Symbol.dispose]();
Expand Down Expand Up @@ -212,4 +217,51 @@ describe.concurrent('eslint CLI integration', () => {
expect(exitCode).toBe(0);
expect(stdout).toContain('n/no-process-exit');
});

it('formats JSON, Markdown, YAML, and CSS via Prettier plugins', ({ fixture, expect }) => {
const unsortedJson = '{\n "z": 1,\n "a": [1, 2]\n}\n';
const longMarkdown = `# Title\n\n${'word '.repeat(40).trim()}\n`;
const doubleQuotedYaml = 'key: "value"\n';
const unsortedCss = '.box {\n display: flex;\n color: red;\n}\n';

const result = fixture.run({
files: {
'config.yml': doubleQuotedYaml,
'data.json': unsortedJson,
'doc.md': longMarkdown,
'style.css': unsortedCss,
},
flags: ['--fix'],
});

expect(result).toMatchObject({ exitCode: 0 });

// JSON: sort-json sorts keys, multiline-arrays expands arrays
expect(fixture.readFile('data.json')).toBe(
['{', ' "a": [', ' 1,', ' 2', ' ],', ' "z": 1', '}', ''].join('\n'),
);

// Markdown: proseWrap 'preserve' keeps long lines unwrapped
expect(fixture.readFile('doc.md')).toBe(longMarkdown);

// YAML: singleQuote from prettierDefaults converts double quotes
expect(fixture.readFile('config.yml')).toBe("key: 'value'\n");

// CSS: alphabetical property sorting (color before display)
expect(fixture.readFile('style.css')).toBe(
'.box {\n color: red;\n display: flex;\n}\n',
);
});

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

const result = fixture.run({
files: { 'app.csproj': uglyXml },
flags: ['--fix'],
});

expect(result).toMatchObject({ exitCode: 0 });
expect(fixture.readFile('app.csproj')).toContain('<PropertyGroup>\n');
});
});
23 changes: 16 additions & 7 deletions packages/eslint-config/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,6 @@
"exports": {
".": "./src/index.ts"
},
"publishConfig": {
"directory": "dist/source",
"exports": {
".": "./src/index.js"
},
"linkDirectory": false
},
"scripts": {
"compile:ts": "pnpm run gtb compile:ts",
"coverage:codecov:upload": "pnpm run gtb coverage:codecov:upload",
Expand All @@ -31,15 +24,24 @@
"dependencies": {
"@eslint-community/eslint-plugin-eslint-comments": "catalog:",
"@eslint/json": "catalog:",
"@prettier/plugin-xml": "catalog:",
"@stylistic/eslint-plugin": "catalog:",
"@vitest/eslint-plugin": "catalog:",
"eslint-plugin-format": "catalog:",
"eslint-plugin-import-x": "catalog:",
"eslint-plugin-jsdoc": "catalog:",
"eslint-plugin-n": "catalog:",
"eslint-plugin-only-warn": "catalog:",
"eslint-plugin-pnpm": "catalog:",
"eslint-plugin-promise": "catalog:",
"eslint-plugin-regexp": "catalog:",
"eslint-plugin-unicorn": "catalog:",
"eslint-plugin-yml": "catalog:",
"prettier": "catalog:",
"prettier-plugin-css-order": "catalog:",
"prettier-plugin-multiline-arrays": "catalog:",
"prettier-plugin-packagejson": "catalog:",
"prettier-plugin-sort-json": "catalog:",
"typescript-eslint": "catalog:"
},
"devDependencies": {
Expand All @@ -51,5 +53,12 @@
},
"engines": {
"node": ">=22.17"
},
"publishConfig": {
"directory": "dist/source",
"exports": {
".": "./src/index.js"
},
"linkDirectory": false
}
}
Loading
Loading