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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
persist-credentials: false
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: 1.3.14
bun-version: 1.4.2
- run: bun install --frozen-lockfile
# Build first: plugins resolve @noctcore/eslint-utils via its emitted
# dist/*.d.ts, so typecheck needs the build to have run.
Expand All @@ -41,7 +41,7 @@ jobs:
persist-credentials: false
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: 1.3.14
bun-version: 1.4.2
- run: bun install --frozen-lockfile
# Tests read built dist/ too: plugins import @noctcore/eslint-utils, and
# lint-meta-rules runs against the workspace plugins' builds.
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ jobs:
persist-credentials: false
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: 1.3.14
bun-version: 1.4.2
# Astro 7 needs Node 22.12 or newer; the runner image ships an older one.
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ jobs:
persist-credentials: false
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: 1.3.14
bun-version: 1.4.2
# Node 24 ships npm 11. Publishing uses npm trusted publishing (OIDC) only,
# which needs npm 11.5.1 or later; there is no npm token to fall back to.
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
Expand Down
6 changes: 4 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ exactly what is needed to act.

## Setup and the commands that matter

Requirements: [Bun](https://bun.sh) 1.3.14 (`packageManager` in the root `package.json`) and
Requirements: [Bun](https://bun.sh) 1.4.2 (`packageManager` in the root `package.json`) and
Node 22 or newer (`engines`; the docs site needs 22.12 or newer).

```sh
Expand Down Expand Up @@ -90,7 +90,9 @@ and each package's `recommended` preset:
`lint-meta-rules` gets a table of rule id, factory, entry point and category instead;
- a one-line status header in every `docs/rules/<rule>.md`, right after the title and blockquote
summary, between `<!-- begin generated rule header -->` and `<!-- end generated rule header -->`.
The site page drops it and shows its own metadata line.
The site page drops it and shows the same facts as its own facts strip, built by `factsStrip` in
`site/scripts/sync.ts` (an ESLint rule: package, preset, autofix, suggestions, options, type
information; a lint-meta rule: its package, factory, entry point and category).

Never edit between the markers by hand. Run the command after adding a rule, changing a
description, or moving a rule in or out of the preset, and commit what it writes. Running it twice
Expand Down
26 changes: 13 additions & 13 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@noctcore/eslint-plugins",
"private": true,
"type": "module",
"packageManager": "bun@1.3.14",
"packageManager": "bun@1.4.2",
"engines": {
"node": ">=22"
},
Expand Down
12 changes: 10 additions & 2 deletions site/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,17 @@ export default defineConfig({
'Focused ESLint plugins for architecture and correctness conventions generic linters cannot see: cross-file boundaries, IO contracts, and code that compiles but bites in production.',
favicon: '/favicon.svg',
logo: { src: './src/assets/mark.svg', alt: '' },
customCss: ['./src/styles/theme.css'],
// The noctcore docs theme in its load order (tokens, pieces, preset), then
// this site's own rules. The Nocturne preset imports its fonts itself.
customCss: [
'./src/styles/noctcore/base.css',
'./src/styles/noctcore/components.css',
'./src/styles/noctcore/presets/nocturne.css',
'./src/styles/site.css',
],
components: {
Head: './src/components/Head.astro',
SiteTitle: './src/components/SiteTitle.astro',
PageTitle: './src/components/PageTitle.astro',
Footer: './src/components/Footer.astro',
},
social: [
Expand Down
5 changes: 4 additions & 1 deletion site/ec.config.mjs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { defineEcConfig } from '@astrojs/starlight/expressive-code';

import { noctcoreCodeConfig } from './src/styles/noctcore/expressive-code.mjs';

/**
* The rule docs mark every example ` ```ts bad ` or ` ```ts good `, and
* scripts/sync.ts carries that through as `verdict=bad|good` fence meta. This
* plugin turns it into a class on the rendered block so theme.css can give the
* plugin turns it into a class on the rendered block so components.css can give the
* two a different frame, label colour and marker. Rendered identically, a reader
* cannot tell which snippet is the one not to write.
*/
Expand All @@ -24,5 +26,6 @@ function pluginVerdict() {
}

export default defineEcConfig({
...noctcoreCodeConfig('nocturne'),
plugins: [pluginVerdict()],
});
8 changes: 4 additions & 4 deletions site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
"type": "module",
"scripts": {
"sync": "bun scripts/sync.ts",
"dev": "bun run sync && astro dev",
"docs:build": "bun run sync && astro build && bun scripts/check-build.ts",
"preview": "astro preview",
"typecheck": "bun run sync && astro sync && tsc --noEmit",
"dev": "bun run sync && ASTRO_TELEMETRY_DISABLED=1 astro dev",
"docs:build": "bun run sync && ASTRO_TELEMETRY_DISABLED=1 astro build && bun scripts/check-build.ts",
"preview": "ASTRO_TELEMETRY_DISABLED=1 astro preview",
"typecheck": "bun run sync && ASTRO_TELEMETRY_DISABLED=1 astro sync && tsc --noEmit",
"test": "bun test scripts"
},
"dependencies": {
Expand Down
3 changes: 3 additions & 0 deletions site/scripts/deprecation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,8 @@ describe('rendering a deprecated rule', () => {
'client. Use [`noctcore-fixture/new-rule`](../../fixture/new-rule/) instead.\n:::',
);
expect(page).toContain(' badge:\n text: Deprecated\n variant: caution');
expect(page).toContain('<div><dt>Status</dt><dd><span class="nc-badge nc-badge--deprecated">deprecated</span></dd></div>');
expect(page).not.toContain('nc-optin');
expect(page).not.toContain('<!-- begin generated rule header -->');
});

Expand All @@ -181,6 +183,7 @@ describe('rendering a deprecated rule', () => {
const page = renderRuleDoc(doc, '/repo/packages/eslint-plugin-fixture/docs/rules/new-rule.md', pkg, newRule);
expect(page).not.toContain(':::caution');
expect(page).not.toContain('badge:');
expect(page).not.toContain('<dt>Status</dt>');
});

test('llms.txt lists deprecated rules with their replacement, and nothing when there are none', () => {
Expand Down
128 changes: 128 additions & 0 deletions site/scripts/facts.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/**
* The header a rule page gets from sync.ts: the doc's summary as the lead
* paragraph, then the facts strip. Same cells in the same order on every page,
* a badge where a value is a state, plain text for a yes or no, and `is-no` on
* a negative. Proven on fixtures so every branch is covered, then on the real
* inventory so no rule falls outside them.
*/
import { describe, expect, test } from 'bun:test';

import { SITE_BASE, loadInventory, type PackageEntry, type RuleEntry } from './inventory';
import { factsStrip, renderRuleDoc } from './sync';

const inventory = await loadInventory('src');

function rule(overrides: Partial<RuleEntry> = {}): RuleEntry {
return {
name: 'some-rule',
id: 'noctcore-fixture/some-rule',
description: 'Fixture rule.',
recommended: 'error',
fixable: false,
hasSuggestions: false,
typeInfo: 'none',
requiresOptions: false,
hasOptions: false,
deprecated: false,
replacedBy: [],
deprecatedSince: null,
deprecationMessage: null,
docsUrl: null,
...overrides,
};
}

const plugin: PackageEntry = {
short: 'fixture',
npmName: '@noctcore/eslint-plugin-fixture',
kind: 'eslint-plugin',
namespace: 'noctcore-fixture',
version: '1.2.3',
description: 'Fixture plugin.',
dir: '/repo/packages/eslint-plugin-fixture',
rules: [],
};

/** The strip as `label: value` pairs, with ` (no)` after a quiet value. */
function cells(html: string): string[] {
return [...html.matchAll(/<div><dt>([^<]+)<\/dt><dd( class="is-no")?>(.*?)<\/dd><\/div>/g)].map(
([, label, quiet, value]) => `${label}: ${value}${quiet ? ' (no)' : ''}`,
);
}

describe('facts strip', () => {
test('a rule on in the preset, with nothing else set', () => {
expect(cells(factsStrip(plugin, rule()))).toEqual([
`Package: <a href="${SITE_BASE}/packages/fixture/"><code>fixture</code></a><wbr><span class="nc-facts-note">v1.2.3</span>`,
'Recommended preset: <span class="nc-badge nc-badge--on">error</span>',
'Autofix: No (no)',
'Suggestions: No (no)',
'Options: None (no)',
'Type information: Not needed (no)',
]);
});

test('an opt-in rule with every capability', () => {
const html = factsStrip(
plugin,
rule({ recommended: 'off', fixable: true, hasSuggestions: true, requiresOptions: true, hasOptions: true, typeInfo: 'required' }),
);
expect(cells(html).slice(1)).toEqual([
'Recommended preset: <span class="nc-badge nc-badge--off">off</span><span class="nc-optin">opt-in</span>',
'Autofix: Yes',
'Suggestions: Yes',
'Options: <span class="nc-badge nc-badge--options">Required</span>',
'Type information: <span class="nc-badge nc-badge--types">required</span>',
]);
});

test('a rule left out of the preset, with optional options and optional types', () => {
const html = factsStrip(plugin, rule({ recommended: null, hasOptions: true, typeInfo: 'optional' }));
expect(cells(html)).toContain(
'Recommended preset: <span class="nc-badge nc-badge--out">not listed</span><span class="nc-optin">opt-in</span>',
);
expect(cells(html)).toContain('Options: Optional');
expect(cells(html)).toContain('Type information: <span class="nc-badge nc-badge--types">optional</span>');
});

test('a lint-meta rule gets the harness cells', () => {
const lintMeta: PackageEntry = { ...plugin, short: 'lint-meta-rules', kind: 'lint-meta', namespace: null };
const html = factsStrip(
lintMeta,
rule({ factory: 'createThingRule', entry: '@noctcore/lint-meta-rules/i18n', category: 'source-text', ciCritical: false }),
);
expect(cells(html).map((cell) => cell.split(':')[0])).toEqual([
'Package',
'Runs under',
'Factory',
'Entry point',
'Category',
'Fails CI by default',
]);
expect(cells(html)).toContain('Fails CI by default: No (no)');
});

test('every real rule gets a complete strip', () => {
for (const pkg of inventory) {
for (const entry of pkg.rules) {
const html = factsStrip(pkg, entry);
expect(html.startsWith('<dl class="nc-facts">')).toBe(true);
expect(cells(html)).toHaveLength(entry.deprecated ? 7 : 6);
}
}
});
});

describe('rule page header', () => {
test('the summary becomes the lead, above the facts strip, and the blockquote is gone', () => {
const sections = ['Why', 'What it flags', 'What it does not flag', 'When not to use it'];
const body = sections.map((section) => `## ${section}\n\nText.\n`).join('\n');
const doc = `# \`noctcore-fixture/some-rule\`\n\n> Checks \`x\` twice.\n> **Opt-in.**\n\n${body}`;
const page = renderRuleDoc(doc, '/repo/packages/eslint-plugin-fixture/docs/rules/some-rule.md', plugin, rule());
const rendered = page.slice(page.indexOf('---\n', 4) + 4);
expect(rendered.startsWith('\n<div class="nc-lead">\n\nChecks `x` twice.\n**Opt-in.**\n\n</div>\n\n<dl class="nc-facts">')).toBe(
true,
);
expect(rendered).not.toMatch(/^>/m);
});
});
Loading
Loading