From 08a1ed24e6f07c6331916b441192a8efb79a00d5 Mon Sep 17 00:00:00 2001 From: Bryan Mishkin <698306+bmish@users.noreply.github.com> Date: Wed, 23 Nov 2022 09:03:07 -0600 Subject: [PATCH] feat: allow content above rule doc title --- lib/generator.ts | 15 +- lib/markdown.ts | 32 +++- .../comment-markers-test.ts.snap | 51 ++++++ test/lib/generate/comment-markers-test.ts | 159 ++++++++++++++++-- test/lib/generate/configs-test.ts | 7 +- 5 files changed, 232 insertions(+), 32 deletions(-) diff --git a/lib/generator.ts b/lib/generator.ts index 35f43887..67903076 100644 --- a/lib/generator.ts +++ b/lib/generator.ts @@ -162,9 +162,8 @@ export async function generate(path: string, options?: GenerateOptions) { (details) => !ignoreDeprecatedRules || !details.deprecated ); - let initializedRuleDoc = false; - // Update rule doc for each rule. + let initializedRuleDoc = false; for (const { name, description, schema } of details) { const pathToDoc = join(path, pathRuleDoc).replace(/{name}/g, name); @@ -243,6 +242,12 @@ export async function generate(path: string, options?: GenerateOptions) { } } + if (initRuleDocs && !initializedRuleDoc) { + throw new Error( + '--init-rule-docs was enabled, but no rule doc file needed to be created.' + ); + } + // Find the README. const pathToReadme = getPathWithExactFileNameCasing(join(path, pathRuleList)); if (!pathToReadme || !existsSync(pathToReadme)) { @@ -282,10 +287,4 @@ export async function generate(path: string, options?: GenerateOptions) { } else { writeFileSync(pathToReadme, readmeContentsNew, 'utf8'); } - - if (initRuleDocs && !initializedRuleDoc) { - throw new Error( - '--init-rule-docs was enabled, but no rule doc file needed to be created.' - ); - } } diff --git a/lib/markdown.ts b/lib/markdown.ts index 7368e9b1..cb8a6ace 100644 --- a/lib/markdown.ts +++ b/lib/markdown.ts @@ -14,14 +14,36 @@ export function replaceOrCreateHeader( ) { const lines = markdown.split('\n'); + const titleLineIndex = lines.findIndex((line) => line.startsWith('# ')); const markerLineIndex = lines.indexOf(marker); + const dashesLineIndex1 = lines.indexOf('---'); + const dashesLineIndex2 = lines.indexOf('---', dashesLineIndex1 + 1); + const hasTitle = titleLineIndex !== -1; + const hasMarker = markerLineIndex !== -1; + const hasYamlFrontMatter = dashesLineIndex1 === 0 && dashesLineIndex2 !== -1; - if (markerLineIndex === -1 && lines.length > 0 && lines[0].startsWith('# ')) { - // No marker present so delete any existing title before we add the new one. - lines.splice(0, 1); - } + // Any YAML front matter or anything else above the title should be kept as-is ahead of the new header. + const preHeader = lines + .slice( + 0, + hasTitle ? titleLineIndex : hasYamlFrontMatter ? dashesLineIndex2 + 1 : 0 + ) + .join('\n'); + + // Anything after the marker comment, title, or YAML front matter should be kept as-is after the new header. + const postHeader = lines + .slice( + hasMarker + ? markerLineIndex + 1 + : hasTitle + ? titleLineIndex + 1 + : hasYamlFrontMatter + ? dashesLineIndex2 + 1 + : 0 + ) + .join('\n'); - return `${newHeader}\n${lines.slice(markerLineIndex + 1).join('\n')}`; + return `${preHeader ? `${preHeader}\n` : ''}${newHeader}\n${postHeader}`; } /** diff --git a/test/lib/generate/__snapshots__/comment-markers-test.ts.snap b/test/lib/generate/__snapshots__/comment-markers-test.ts.snap index f634deaf..7495249d 100644 --- a/test/lib/generate/__snapshots__/comment-markers-test.ts.snap +++ b/test/lib/generate/__snapshots__/comment-markers-test.ts.snap @@ -46,6 +46,24 @@ exports[`generate (comment markers) no existing comment markers - minimal doc co " `; +exports[`generate (comment markers) no existing comment markers - rule doc with YAML-formatted metadata (front matter) above title updates the documentation 1`] = ` +"--- +pageClass: "rule-details" +sidebarDepth: 0 +title: "plugin/rule-name" +description: "disallow foo" +since: "v0.12.0" +--- +# Description (\`test/no-foo\`) + +💼 This rule is enabled in the ✅ \`recommended\` config. + + +Pre-existing notice about the rule being recommended. +## Rule details +Details." +`; + exports[`generate (comment markers) no existing comment markers - with no blank lines in existing content generates the documentation 1`] = ` "## Rules @@ -94,6 +112,39 @@ exports[`generate (comment markers) no existing comment markers - with one blank Existing rule doc content." `; +exports[`generate (comment markers) rule doc with YAML-formatted metadata (front matter) above title and comment marker updates the documentation 1`] = ` +"--- +pageClass: "rule-details" +sidebarDepth: 0 +title: "plugin/rule-name" +description: "disallow foo" +since: "v0.12.0" +--- +# Description (\`test/no-foo\`) + +💼 This rule is enabled in the ✅ \`recommended\` config. + + +## Rule details +Details." +`; + +exports[`generate (comment markers) rule doc with YAML-formatted metadata (front matter) and nothing else updates the documentation 1`] = ` +"--- +pageClass: "rule-details" +sidebarDepth: 0 +title: "plugin/rule-name" +description: "disallow foo" +since: "v0.12.0" +--- +# Description (\`test/no-foo\`) + +💼 This rule is enabled in the ✅ \`recommended\` config. + + +" +`; + exports[`generate (comment markers) rule doc without header marker but pre-existing header updates the documentation 1`] = ` "# Description (\`test/no-foo\`) diff --git a/test/lib/generate/comment-markers-test.ts b/test/lib/generate/comment-markers-test.ts index 5de2c9ed..95abd30d 100644 --- a/test/lib/generate/comment-markers-test.ts +++ b/test/lib/generate/comment-markers-test.ts @@ -260,6 +260,55 @@ describe('generate (comment markers)', function () { }); }); + describe('no existing comment markers - rule doc with YAML-formatted metadata (front matter) above title', function () { + beforeEach(function () { + mockFs({ + 'package.json': JSON.stringify({ + name: 'eslint-plugin-test', + exports: 'index.js', + type: 'module', + }), + + 'index.js': ` + export default { + rules: { 'no-foo': { meta: { docs: { description: 'Description.' }, }, create(context) {} }, }, + configs: { recommended: { rules: { 'test/no-foo': 'error', } } } + };`, + + 'README.md': '## Rules\n', + + // YAML-formatted metadata (front matter) content above title. + 'docs/rules/no-foo.md': outdent` + --- + pageClass: "rule-details" + sidebarDepth: 0 + title: "plugin/rule-name" + description: "disallow foo" + since: "v0.12.0" + --- + # Some pre-existing title. + Pre-existing notice about the rule being recommended. + ## Rule details + Details. + `, + + // Needed for some of the test infrastructure to work. + node_modules: mockFs.load(PATH_NODE_MODULES), + }); + }); + + afterEach(function () { + mockFs.restore(); + jest.resetModules(); + }); + + it('updates the documentation', async function () { + await generate('.'); + + expect(readFileSync('docs/rules/no-foo.md', 'utf8')).toMatchSnapshot(); + }); + }); + describe('README missing rule list markers but with rules section', function () { beforeEach(function () { mockFs({ @@ -357,19 +406,8 @@ describe('generate (comment markers)', function () { 'index.js': ` export default { - rules: { - 'no-foo': { - meta: { docs: { description: 'Description.' }, }, - create(context) {} - }, - }, - configs: { - recommended: { - rules: { - 'test/no-foo': 'error', - } - } - } + rules: { 'no-foo': { meta: { docs: { description: 'Description.' }, }, create(context) {} }, }, + configs: { recommended: { rules: { 'test/no-foo': 'error', } } } };`, 'README.md': @@ -398,4 +436,99 @@ describe('generate (comment markers)', function () { expect(readFileSync('docs/rules/no-foo.md', 'utf8')).toMatchSnapshot(); }); }); + + describe('rule doc with YAML-formatted metadata (front matter) above title and comment marker', function () { + beforeEach(function () { + mockFs({ + 'package.json': JSON.stringify({ + name: 'eslint-plugin-test', + exports: 'index.js', + type: 'module', + }), + + 'index.js': ` + export default { + rules: { 'no-foo': { meta: { docs: { description: 'Description.' }, }, create(context) {} }, }, + configs: { recommended: { rules: { 'test/no-foo': 'error', } } } + };`, + + 'README.md': '## Rules\n', + + // YAML-formatted metadata (front matter) above title. + 'docs/rules/no-foo.md': outdent` + --- + pageClass: "rule-details" + sidebarDepth: 0 + title: "plugin/rule-name" + description: "disallow foo" + since: "v0.12.0" + --- + # Outdated title. + Outdated content. + + ## Rule details + Details. + `, + + // Needed for some of the test infrastructure to work. + node_modules: mockFs.load(PATH_NODE_MODULES), + }); + }); + + afterEach(function () { + mockFs.restore(); + jest.resetModules(); + }); + + it('updates the documentation', async function () { + await generate('.'); + + expect(readFileSync('docs/rules/no-foo.md', 'utf8')).toMatchSnapshot(); + }); + }); + + describe('rule doc with YAML-formatted metadata (front matter) and nothing else', function () { + beforeEach(function () { + mockFs({ + 'package.json': JSON.stringify({ + name: 'eslint-plugin-test', + exports: 'index.js', + type: 'module', + }), + + 'index.js': ` + export default { + rules: { 'no-foo': { meta: { docs: { description: 'Description.' }, }, create(context) {} }, }, + configs: { recommended: { rules: { 'test/no-foo': 'error', } } } + };`, + + 'README.md': '## Rules\n', + + // YAML-formatted metadata (front matter) only. + 'docs/rules/no-foo.md': outdent` + --- + pageClass: "rule-details" + sidebarDepth: 0 + title: "plugin/rule-name" + description: "disallow foo" + since: "v0.12.0" + --- + `, + + // Needed for some of the test infrastructure to work. + node_modules: mockFs.load(PATH_NODE_MODULES), + }); + }); + + afterEach(function () { + mockFs.restore(); + jest.resetModules(); + }); + + it('updates the documentation', async function () { + await generate('.'); + + expect(readFileSync('docs/rules/no-foo.md', 'utf8')).toMatchSnapshot(); + }); + }); }); diff --git a/test/lib/generate/configs-test.ts b/test/lib/generate/configs-test.ts index 26ab09f7..917ae8f6 100644 --- a/test/lib/generate/configs-test.ts +++ b/test/lib/generate/configs-test.ts @@ -407,12 +407,7 @@ describe('generate (configs)', function () { 'index.js': ` export default { - rules: { - 'no-foo': { - meta: { docs: { description: 'Description.' }, }, - create(context) {} - }, - }, + rules: { 'no-foo': { meta: { docs: { description: 'Description.' }, }, create(context) {} }, }, configs: { recommended: { rules: {