diff --git a/CHANGELOG.md b/CHANGELOG.md index 69c5fa1..614a082 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- Make dead link report configurable (#49) - Remove internal dependency upon slugify (#48) - Add support for custom rendering functions (#47) - Add support for referencing files by path (#44) diff --git a/README.md b/README.md index ded9572..2e9f82d 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,9 @@ type EleventyPluginInterlinkOptions = { // that returns [UNABLE TO LOCATE EMBED]. unableToLocateEmbedFn?: ErrorRenderFn, + // deadLinkReport is the desired output format of the dead link report, by default its set to 'console' + deadLinkReport?: 'console' | 'json' | 'none', + // resolvingFns contains functions used for resolving a wikilinks output. // see the Custom Resolving Functions section below resolvingFns?: Map Promise> @@ -191,6 +194,12 @@ You can then display this information in any way you would like, I use the below {% endif %} ``` +### Dead link Report + +The default behaviour of this plugin is to report to the console every broken Wikilink and internal link. This behaviour is configurable via the `deadLinkReport` config option. This option accepts three values: `none`, `console` and `json` with `console` being the default. + +Setting the value to `none` will disable the dead link report while setting it to `json` will silence console output instead writing to `.dead-links.json` within the project root folder. + ### Page lookup logic This plugin will attempt to identify the page being linked using the following steps in order: diff --git a/index.d.ts b/index.d.ts index 80d428e..5456c17 100644 --- a/index.d.ts +++ b/index.d.ts @@ -49,6 +49,9 @@ type EleventyPluginInterlinkOptions = { // slug that you are using. This defaults to a function that returns [UNABLE TO LOCATE EMBED]. unableToLocateEmbedFn?: ErrorRenderFn, + // deadLinkReport is the desired output format of the dead link report, by default its set to 'console' + deadLinkReport?: 'console' | 'json' | 'none', + // resolvingFns is a list of resolving functions. These are invoked by a wikilink containing a `:` character // prefixed by the fn name. The page in this case is the linking page. resolvingFns?: Map Promise>, diff --git a/index.js b/index.js index 263faf3..a97b47d 100644 --- a/index.js +++ b/index.js @@ -18,6 +18,7 @@ module.exports = function (eleventyConfig, options = {}) { layoutKey: 'embedLayout', layoutTemplateLangKey: 'embedLayoutLanguage', resolvingFns: new Map(), + deadLinkReport: 'console', }, options); // TODO: deprecate usage of unableToLocateEmbedFn in preference of using resolving fn @@ -47,7 +48,9 @@ module.exports = function (eleventyConfig, options = {}) { // After 11ty has finished generating the site output a list of wikilinks that do not link to // anything. // TODO: 1.1.0 have this clear the interlinker cache so that next time 11ty builds its starting from fresh data! (#24) - eleventyConfig.on('eleventy.after', () => interlinker.deadLinks.report()); + eleventyConfig.on('eleventy.after', () => { + if (opts.deadLinkReport !== 'none') interlinker.deadLinks.report(opts.deadLinkReport) + }); // Teach Markdown-It how to display MediaWiki Links. eleventyConfig.amendLibrary('md', (md) => { diff --git a/src/dead-links.js b/src/dead-links.js index 34e4ebe..0d099b5 100644 --- a/src/dead-links.js +++ b/src/dead-links.js @@ -1,6 +1,8 @@ +const path = require('node:path'); const chalk = require("chalk"); -module.exports = class DeadLinks { +const fs = require('node:fs'); +module.exports = class DeadLinks { constructor() { this.gravestones = new Map; this.fileSrc = 'unknown'; @@ -25,17 +27,33 @@ module.exports = class DeadLinks { this.gravestones.set(link, names); } - report() { - for (const [link, files] of this.gravestones.entries()) { - console.warn( - chalk.blue('[@photogabble/wikilinks]'), - chalk.yellow('WARNING'), - `${(link.includes('href') ? 'Link' : 'Wikilink')} (${link}) found pointing to to non-existent page in:` - ); - - for (const file of files) { - console.warn(`\t- ${file}`); + /** + * @param {'console'|'json'} format + */ + report(format) { + if (format === 'console') { + for (const [link, files] of this.gravestones.entries()) { + console.warn( + chalk.blue('[@photogabble/wikilinks]'), + chalk.yellow('WARNING'), + `${(link.includes('href') ? 'Link' : 'Wikilink')} (${link}) found pointing to to non-existent page in:` + ); + + for (const file of files) { + console.warn(`\t- ${file}`); + } } + return; } + + let obj = {}; + for (const [link, files] of this.gravestones.entries()) { + obj[link] = files; + } + + fs.writeFileSync( + path.join(process.env.ELEVENTY_ROOT, '.dead-links.json'), + JSON.stringify(obj) + ); } } diff --git a/tests/eleventy.test.js b/tests/eleventy.test.js index 89798db..547246f 100644 --- a/tests/eleventy.test.js +++ b/tests/eleventy.test.js @@ -1,13 +1,21 @@ const Eleventy = require("@11ty/eleventy"); const {normalize, consoleMockMessages, findResultByUrl, fixturePath} = require('./helpers'); -const test = require("ava"); +const fs = require('node:fs'); const sinon = require("sinon"); +const test = require("ava"); // NOTE: Tests using sinon to mock console.warn need to be run with // `test.serial` so that they don't run at the same time as one another to // avoid the "Attempted to wrap warn which is already wrapped" error. // @see https://stackoverflow.com/a/37900956/1225977 +test.afterEach(() => { + const deadLinksPathname = fixturePath('website-with-custom-resolving-fn/.dead-links.json'); + if (fs.existsSync(deadLinksPathname)) { + fs.rmSync(deadLinksPathname); + } +}) + test("Sample small Website (wikilinks and regular links)", async t => { let elev = new Eleventy(fixturePath('sample-small-website'), fixturePath('sample-small-website/_site'), { configPath: fixturePath('sample-small-website/eleventy.config.js'), @@ -217,10 +225,14 @@ test("Custom resolving functions are invoked", async t => { configPath: fixturePath('website-with-custom-resolving-fn/eleventy.config.js'), }); + t.false(fs.existsSync(fixturePath('website-with-custom-resolving-fn/.dead-links.json'))); + let results = await elev.toJSON(); t.is( normalize(findResultByUrl(results, '/').content), `

These wikilinks use custom resolving functions:

` ); + + t.true(fs.existsSync(fixturePath('website-with-custom-resolving-fn/.dead-links.json'))); }); diff --git a/tests/fixtures/sample-with-hash-in-title/eleventy.config.js b/tests/fixtures/sample-with-hash-in-title/eleventy.config.js index 9b10b5c..fd8d97b 100644 --- a/tests/fixtures/sample-with-hash-in-title/eleventy.config.js +++ b/tests/fixtures/sample-with-hash-in-title/eleventy.config.js @@ -1,6 +1,9 @@ module.exports = function (eleventyConfig) { eleventyConfig.addPlugin( require('../../../index.js'), + { + deadLinkReport: 'none' + } ); return { diff --git a/tests/fixtures/website-with-custom-resolving-fn/eleventy.config.js b/tests/fixtures/website-with-custom-resolving-fn/eleventy.config.js index bef6a17..06f226f 100644 --- a/tests/fixtures/website-with-custom-resolving-fn/eleventy.config.js +++ b/tests/fixtures/website-with-custom-resolving-fn/eleventy.config.js @@ -6,6 +6,7 @@ module.exports = function (eleventyConfig) { ['howdy', (link, page) => `Hello ${link.name}!`], ['issue', (link, page) => `#${link.name}`], ]), + deadLinkReport: 'json', } );