From a48dcd849bd217c1d56cbdd5b73af38c7b4fe79a Mon Sep 17 00:00:00 2001 From: Thomas Allmer Date: Sun, 21 Aug 2022 18:35:27 +0200 Subject: [PATCH] feat(cli): introduce "rocket lint" --- .changeset/bright-emus-design.md | 15 ++ packages/cli/package.json | 2 +- packages/cli/src/RocketBuild.js | 203 +++--------------- packages/cli/src/RocketCli.js | 7 +- packages/cli/src/RocketLint.js | 144 +++++++------ packages/cli/src/build/buildHtml.js | 28 +++ .../build/buildJavaScriptOptimizedOutput.js | 88 ++++++++ .../cli/src/build/buildOpenGraphImages.js | 95 ++++++++ packages/cli/types/main.d.ts | 5 + .../10--docs/30--guides/50--go-live.rocket.md | 34 +++ site/pages/pageTreeData.rocketGenerated.json | 10 + yarn.lock | 7 - 12 files changed, 385 insertions(+), 253 deletions(-) create mode 100644 .changeset/bright-emus-design.md create mode 100644 packages/cli/src/build/buildHtml.js create mode 100644 packages/cli/src/build/buildJavaScriptOptimizedOutput.js create mode 100644 packages/cli/src/build/buildOpenGraphImages.js diff --git a/.changeset/bright-emus-design.md b/.changeset/bright-emus-design.md new file mode 100644 index 0000000..d97c84b --- /dev/null +++ b/.changeset/bright-emus-design.md @@ -0,0 +1,15 @@ +--- +'@rocket/cli': patch +--- + +Introducing `rocket lint` to verify if all your links are correct. + +There are two modes: + +```bash +# check existing production build in _site (need to execute "rocket build" before) +rocket lint + +# run a fast html only build and then check it +rocket lint --build-html +``` diff --git a/packages/cli/package.json b/packages/cli/package.json index 056fa5a..956b7ab 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -54,7 +54,7 @@ "dependencies": { "@rocket/building-rollup": "^0.4.0", "@rocket/engine": "^0.2.6", - "@web/rollup-plugin-copy": "^0.3.0", + "check-html-links": "^0.2.3", "colorette": "^2.0.16", "commander": "^9.0.0", "fs-extra": "^9.0.1", diff --git a/packages/cli/src/RocketBuild.js b/packages/cli/src/RocketBuild.js index f959fd5..8d75154 100755 --- a/packages/cli/src/RocketBuild.js +++ b/packages/cli/src/RocketBuild.js @@ -1,78 +1,18 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ -// @ts-nocheck - -import { Engine } from '@rocket/engine/server'; -import { gatherFiles } from '@rocket/engine'; - -import { fromRollup } from '@web/dev-server-rollup'; - -import { rollup } from 'rollup'; import path from 'path'; -import { rollupPluginHTML } from '@web/rollup-plugin-html'; -import { createMpaConfig, createServiceWorkerConfig } from '@rocket/building-rollup'; -import { adjustPluginOptions } from 'plugins-manager'; import { existsSync } from 'fs'; -import { readFile, unlink, writeFile } from 'fs/promises'; +import { readFile, writeFile } from 'fs/promises'; -import puppeteer from 'puppeteer'; - -/** - * @param {object} config - */ -async function buildAndWrite(config) { - const bundle = await rollup(config); - - if (Array.isArray(config.output)) { - await bundle.write(config.output[0]); - await bundle.write(config.output[1]); - } else { - await bundle.write(config.output); - } -} - -async function productionBuild(config) { - const defaultSetupPlugins = []; - if (config.pathPrefix) { - defaultSetupPlugins.push( - adjustPluginOptions(rollupPluginHTML, { absolutePathPrefix: config.pathPrefix }), - ); - } - - const mpaConfig = createMpaConfig({ - input: '**/*.html', - output: { - dir: config.outputDir, - }, - // custom - rootDir: path.resolve(config.outputDevDir), - absoluteBaseUrl: config.absoluteBaseUrl, - setupPlugins: [ - ...defaultSetupPlugins, - ...config.setupDevServerAndBuildPlugins, - ...config.setupBuildPlugins, - ], - }); - const finalConfig = - typeof config.adjustBuildOptions === 'function' - ? config.adjustBuildOptions(mpaConfig) - : mpaConfig; - await buildAndWrite(finalConfig); - - const { serviceWorkerSourcePath } = config; - if (existsSync(serviceWorkerSourcePath)) { - const serviceWorkerConfig = createServiceWorkerConfig({ - input: serviceWorkerSourcePath, - output: { - file: path.join(path.resolve(config.outputDir), config.serviceWorkerName), - }, - }); - - await buildAndWrite(serviceWorkerConfig); - } -} +import { buildHtml } from './build/buildHtml.js'; +import { buildOpenGraphImages } from './build/buildOpenGraphImages.js'; +import { buildJavaScriptOptimizedOutput } from './build/buildJavaScriptOptimizedOutput.js'; export class RocketBuild { + /** + * @param {import('commander').Command} program + * @param {import('./RocketCli.js').RocketCli} cli + */ async setupCommand(program, cli) { this.cli = cli; @@ -87,32 +27,30 @@ export class RocketBuild { } async build() { - await this.cli.events.dispatchEventDone('build-start'); - await this.cli.clearOutputDir(); - await this.cli.clearOutputDevDir(); - - this.engine = new Engine(); - this.engine.setOptions({ - docsDir: this.cli.options.inputDir, - outputDir: this.cli.options.outputDevDir, - setupPlugins: this.cli.options.setupEnginePlugins, - longFileHeaderWidth: this.cli.options.longFileHeaderWidth, - longFileHeaderComment: this.cli.options.longFileHeaderComment, - renderMode: 'production', - clearOutputDir: this.cli.options.clearOutputDir, - }); - console.log('Engine building...'); - await this.engine.build({ autoStop: this.cli.options.buildAutoStop }); - - if (this.cli.options.buildOpenGraphImages) { - console.log('Generating Open Graph Images...'); - await this.buildOpenGraphImages(); + if (!this.cli) { + return; + } + // for typescript as `this.cli.options.outputDir` supports other inputs as well + // but the cli will normalize it to a string before calling plugins + if (typeof this.cli.options.outputDir !== 'string') { + return; } - if (this.cli.options.buildOptimize) { + await this.cli.events.dispatchEventDone('build-start'); + + // 1. build html + this.engine = await buildHtml(this.cli); + + // 2. build open graph images + if (this.cli.options.buildOpenGraphImages) { + console.log('Generating Open Graph Images...'); + await buildOpenGraphImages(this.cli); + } + + // 3. build optimized output + if (this.cli.options.buildOptimize && this.engine) { console.log('Optimize Production Build...'); - await productionBuild(this.cli.options); - await this.engine.copyPublicFilesTo(this.cli.options.outputDir); + await buildJavaScriptOptimizedOutput(this.cli, this.engine); } // hackfix 404.html by making all asset urls absolute (rollup always makes them relative) which will break if netlify serves the content form a different url @@ -130,87 +68,4 @@ export class RocketBuild { await this.cli.events.dispatchEventDone('build-end'); } - - async buildOpenGraphImages() { - const openGraphFiles = await gatherFiles(this.cli.options.outputDevDir, { - fileEndings: ['.opengraph.html'], - }); - if (openGraphFiles.length === 0) { - return; - } - - // TODO: enable URL support in the Engine and remove this "workaround" - if ( - typeof this.cli.options.inputDir !== 'string' || - typeof this.cli.options.outputDevDir !== 'string' - ) { - return; - } - - const withWrap = this.cli.options.setupDevServerAndBuildPlugins - ? this.cli.options.setupDevServerAndBuildPlugins.map(modFunction => { - modFunction.wrapPlugin = fromRollup; - return modFunction; - }) - : []; - - this.engine = new Engine(); - this.engine.setOptions({ - docsDir: this.cli.options.inputDir, - outputDir: this.cli.options.outputDevDir, - setupPlugins: this.cli.options.setupEnginePlugins, - open: false, - clearOutputDir: false, - adjustDevServerOptions: this.cli.options.adjustDevServerOptions, - setupDevServerMiddleware: this.cli.options.setupDevServerMiddleware, - setupDevServerPlugins: [...this.cli.options.setupDevServerPlugins, ...withWrap], - }); - try { - await this.engine.start(); - - const browser = await puppeteer.launch(); - const page = await browser.newPage(); - - // In 2022 Twitter & Facebook recommend a size of 1200x628 - we capture with 2 dpr for retina displays - await page.setViewport({ - width: 1200, - height: 628, - deviceScaleFactor: 2, - }); - - for (const openGraphFile of openGraphFiles) { - const relUrl = path.relative(this.cli.options.outputDevDir, openGraphFile); - const imagePath = openGraphFile.replace('.opengraph.html', '.opengraph.png'); - const htmlPath = openGraphFile.replace('.opengraph.html', '.html'); - const relImageUrl = path.basename(imagePath); - - let htmlString = await readFile(htmlPath, 'utf8'); - if (!htmlString.includes('')) { - htmlString = htmlString.replace( - '', - [ - ' ', - ' ', - ` `, - ' ', - ].join('\n'), - ); - } - } - const url = `http://localhost:${this.engine.devServer.config.port}/${relUrl}`; - await page.goto(url, { waitUntil: 'networkidle0' }); - await page.screenshot({ path: imagePath }); - - await unlink(openGraphFile); - await writeFile(htmlPath, htmlString); - } - await browser.close(); - - await this.engine.stop(); - } catch (e) { - console.log('Could not start dev server to generate open graph images'); - console.error(e); - } - } } diff --git a/packages/cli/src/RocketCli.js b/packages/cli/src/RocketCli.js index b0e6d95..8a5813c 100644 --- a/packages/cli/src/RocketCli.js +++ b/packages/cli/src/RocketCli.js @@ -2,6 +2,7 @@ import { Command } from 'commander'; import { RocketStart } from './RocketStart.js'; import { RocketBuild } from './RocketBuild.js'; +import { RocketLint } from './RocketLint.js'; import { RocketUpgrade } from './RocketUpgrade.js'; import { RocketPreview } from './RocketPreview.js'; // import { ignore } from './images/ignore.js'; @@ -53,6 +54,10 @@ export class RocketCli { absoluteBaseUrl: '', clearOutputDir: true, + lint: { + buildHtml: false, + }, + // /** @type {{[key: string]: ImagePreset}} */ // imagePresets: { // responsive: { @@ -179,7 +184,7 @@ export class RocketCli { let pluginsMeta = [ { plugin: RocketStart, options: {} }, { plugin: RocketBuild, options: {} }, - // { plugin: RocketLint }, + { plugin: RocketLint, options: {} }, { plugin: RocketUpgrade, options: {} }, { plugin: RocketPreview, options: {} }, ]; diff --git a/packages/cli/src/RocketLint.js b/packages/cli/src/RocketLint.js index e6c34d1..6f20af7 100755 --- a/packages/cli/src/RocketLint.js +++ b/packages/cli/src/RocketLint.js @@ -1,81 +1,85 @@ -// /* eslint-disable */ -// // @ts-nocheck +/* eslint-disable @typescript-eslint/ban-ts-comment */ -// /** @typedef {import('../types/main').RocketCliOptions} RocketCliOptions */ +// @ts-ignore +import { CheckHtmlLinksCli } from 'check-html-links'; +import { bold, gray } from 'colorette'; +import { existsSync } from 'fs'; +import path from 'path'; +import { buildHtml } from './build/buildHtml.js'; -// import { CheckHtmlLinksCli } from 'check-html-links'; +export class RocketLint { + options = { + buildHtml: false, + }; -// export class RocketLint { -// static pluginName = 'RocketLint'; -// commands = ['start', 'build', 'lint']; + /** + * @param {import('commander').Command} program + * @param {import('./RocketCli.js').RocketCli} cli + */ + async setupCommand(program, cli) { + this.cli = cli; + this.active = true; -// /** -// * @param {RocketCliOptions} config -// */ -// setupCommand(config) { -// if (config.command === 'lint') { -// config.watch = false; -// } -// return config; -// } + program + .command('lint') + .option('-i, --input-dir ', 'path to where to search for source files') + .option('-b, --build-html', 'do a quick html only build and then check links') + .action(async options => { + const { cliOptions, ...lintOptions } = options; + cli.setOptions({ + ...cliOptions, + lint: lintOptions, + }); + this.options = { ...this.options, ...cli.options.lint }; + cli.activePlugin = this; -// /** -// * @param {object} options -// * @param {RocketCliOptions} options.config -// * @param {any} options.argv -// */ -// async setup({ config, argv, eleventy }) { -// this.__argv = argv; -// this.config = { -// lintInputDir: config.outputDevDir, -// lintExecutesEleventyBefore: true, -// ...config, -// }; -// this.eleventy = eleventy; -// } + await this.lint(); + }); + } -// async lintCommand() { -// if (this.config.lintExecutesEleventyBefore) { -// await this.eleventy.write(); -// // updated will trigger linting -// } else { -// await this.__lint(); -// } -// } + async lint() { + if (!this.cli) { + return; + } -// async __lint() { -// if (this.config?.pathPrefix) { -// console.log('INFO: RocketLint currently does not support being used with a pathPrefix'); -// return; -// } + // for typescript as `this.cli.options.outputDir` supports other inputs as well + // but the cli will normalize it to a string before calling plugins + if ( + typeof this.cli.options.outputDevDir !== 'string' || + typeof this.cli.options.outputDir !== 'string' + ) { + return; + } -// const checkLinks = new CheckHtmlLinksCli(); -// checkLinks.setOptions({ -// ...this.config.checkLinks, -// rootDir: this.config.lintInputDir, -// printOnError: false, -// continueOnError: true, -// }); + if (this.options.buildHtml) { + await buildHtml(this.cli); + } -// const { errors, message } = await checkLinks.run(); -// if (errors.length > 0) { -// if (this.config.command === 'start') { -// console.log(message); -// } else { -// throw new Error(message); -// } -// } -// } + const folderToCheck = this.options.buildHtml + ? this.cli.options.outputDevDir + : this.cli.options.outputDir; -// async postCommand() { -// if (this.config.watch === false) { -// await this.__lint(); -// } -// } + const rootIndexHtml = path.join(folderToCheck, 'index.html'); + if (!existsSync(rootIndexHtml)) { + console.log(`${bold(`👀 Linting Production Build`)}`); + console.log(''); + console.log(` 🛑 No index.html found in the build directory ${gray(`${rootIndexHtml}`)}`); + console.log(' 🤔 Did you forget to run `rocket build` before?'); + console.log(''); + return; + } -// async updated() { -// if (this.config.watch === true) { -// await this.__lint(); -// } -// } -// } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { buildHtml: _drop, ...userCheckHtmlLinksOptions } = this.options; + + const checkLinks = new CheckHtmlLinksCli(); + checkLinks.setOptions({ + rootDir: folderToCheck, + printOnError: true, + continueOnError: false, + ...userCheckHtmlLinksOptions, + }); + + await checkLinks.run(); + } +} diff --git a/packages/cli/src/build/buildHtml.js b/packages/cli/src/build/buildHtml.js new file mode 100644 index 0000000..93ffc77 --- /dev/null +++ b/packages/cli/src/build/buildHtml.js @@ -0,0 +1,28 @@ +import { Engine } from '@rocket/engine/server'; + +/** + * @param {import('../RocketCli.js').RocketCli} cli + * @returns + */ +export async function buildHtml(cli) { + // TODO: enable URL support in the Engine and remove this typescript "workaround" + if (typeof cli.options.inputDir !== 'string' || typeof cli.options.outputDevDir !== 'string') { + return; + } + + await cli.clearOutputDevDir(); + const engine = new Engine(); + engine.setOptions({ + docsDir: cli.options.inputDir, + outputDir: cli.options.outputDevDir, + setupPlugins: cli.options.setupEnginePlugins, + longFileHeaderWidth: cli.options.longFileHeaderWidth, + longFileHeaderComment: cli.options.longFileHeaderComment, + renderMode: 'production', + clearOutputDir: cli.options.clearOutputDir, + }); + console.log('Engine building...'); + await engine.build({ autoStop: cli.options.buildAutoStop }); + + return engine; +} diff --git a/packages/cli/src/build/buildJavaScriptOptimizedOutput.js b/packages/cli/src/build/buildJavaScriptOptimizedOutput.js new file mode 100644 index 0000000..706f2d0 --- /dev/null +++ b/packages/cli/src/build/buildJavaScriptOptimizedOutput.js @@ -0,0 +1,88 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +import path from 'path'; +import { existsSync } from 'fs'; +import { rollup } from 'rollup'; + +// @ts-ignore +import { createMpaConfig, createServiceWorkerConfig } from '@rocket/building-rollup'; + +// import { rollupPluginHTML } from '@web/rollup-plugin-html'; +// import { adjustPluginOptions } from 'plugins-manager'; + +/** + * @param {import('rollup').RollupOptions} config + */ +async function buildAndWrite(config) { + if (!config.output) { + return; + } + + const bundle = await rollup(config); + + if (Array.isArray(config.output)) { + await bundle.write(config.output[0]); + await bundle.write(config.output[1]); + } else { + await bundle.write(config.output); + } +} + +/** + * @param {import('../RocketCli.js').RocketCli} cli + * @param {import('@rocket/engine/server').Engine} engine + * @returns + */ +export async function buildJavaScriptOptimizedOutput(cli, engine) { + const config = cli.options; + + // for typescript as `this.cli.options.outputDir` supports other inputs as well + // but the cli will normalize it to a string before calling plugins + if (typeof config.outputDir !== 'string' || typeof config.outputDevDir !== 'string') { + return; + } + + await cli.clearOutputDir(); + + // TODO: pathPrefix is currently not supported + // const defaultSetupPlugins = []; + // if (config.pathPrefix) { + // defaultSetupPlugins.push( + // adjustPluginOptions(rollupPluginHTML, { absolutePathPrefix: config.pathPrefix }), + // ); + // } + + const mpaConfig = createMpaConfig({ + input: '**/*.html', + output: { + dir: config.outputDir, + }, + // custom + rootDir: path.resolve(config.outputDevDir), + absoluteBaseUrl: config.absoluteBaseUrl, + setupPlugins: [ + // ...defaultSetupPlugins, + ...config.setupDevServerAndBuildPlugins, + ...config.setupBuildPlugins, + ], + }); + const finalConfig = + typeof config.adjustBuildOptions === 'function' + ? config.adjustBuildOptions(mpaConfig) + : mpaConfig; + await buildAndWrite(finalConfig); + + const { serviceWorkerSourcePath } = config; + if (existsSync(serviceWorkerSourcePath)) { + const serviceWorkerConfig = createServiceWorkerConfig({ + input: serviceWorkerSourcePath, + output: { + file: path.join(path.resolve(config.outputDir), config.serviceWorkerName), + }, + }); + + await buildAndWrite(serviceWorkerConfig); + } + + // copy static files over + await engine.copyPublicFilesTo(config.outputDir); +} diff --git a/packages/cli/src/build/buildOpenGraphImages.js b/packages/cli/src/build/buildOpenGraphImages.js new file mode 100644 index 0000000..6b4919b --- /dev/null +++ b/packages/cli/src/build/buildOpenGraphImages.js @@ -0,0 +1,95 @@ +import { gatherFiles } from '@rocket/engine'; +import { Engine } from '@rocket/engine/server'; +import { fromRollup } from '@web/dev-server-rollup'; + +import { readFile, unlink, writeFile } from 'fs/promises'; + +import puppeteer from 'puppeteer'; +import path from 'path'; + +/** + * @param {import('../RocketCli.js').RocketCli} cli + * @returns + */ +export async function buildOpenGraphImages(cli) { + const openGraphFiles = await gatherFiles(cli.options.outputDevDir, { + fileEndings: ['.opengraph.html'], + }); + if (openGraphFiles.length === 0) { + return; + } + + // TODO: enable URL support in the Engine and remove this typescript "workaround" + if (typeof cli.options.inputDir !== 'string' || typeof cli.options.outputDevDir !== 'string') { + return; + } + + const withWrap = cli.options.setupDevServerAndBuildPlugins + ? cli.options.setupDevServerAndBuildPlugins.map(modFunction => { + modFunction.wrapPlugin = fromRollup; + return modFunction; + }) + : []; + + const engine = new Engine(); + engine.setOptions({ + docsDir: cli.options.inputDir, + outputDir: cli.options.outputDevDir, + setupPlugins: cli.options.setupEnginePlugins, + open: false, + clearOutputDir: false, + adjustDevServerOptions: cli.options.adjustDevServerOptions, + setupDevServerMiddleware: cli.options.setupDevServerMiddleware, + setupDevServerPlugins: [...cli.options.setupDevServerPlugins, ...withWrap], + }); + try { + await engine.start(); + if (!engine?.devServer?.config.port) { + return; + } + + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + + // In 2022 Twitter & Facebook recommend a size of 1200x628 - we capture with 2 dpr for retina displays + await page.setViewport({ + width: 1200, + height: 628, + deviceScaleFactor: 2, + }); + + for (const openGraphFile of openGraphFiles) { + const relUrl = path.relative(cli.options.outputDevDir, openGraphFile); + const imagePath = openGraphFile.replace('.opengraph.html', '.opengraph.png'); + const htmlPath = openGraphFile.replace('.opengraph.html', '.html'); + const relImageUrl = path.basename(imagePath); + + let htmlString = await readFile(htmlPath, 'utf8'); + if (!htmlString.includes('')) { + htmlString = htmlString.replace( + '', + [ + ' ', + ' ', + ` `, + ' ', + ].join('\n'), + ); + } + } + const url = `http://localhost:${engine.devServer.config.port}/${relUrl}`; + await page.goto(url, { waitUntil: 'networkidle0' }); + await page.screenshot({ path: imagePath }); + + await unlink(openGraphFile); + await writeFile(htmlPath, htmlString); + } + await browser.close(); + + await engine.stop(); + } catch (e) { + console.log('Could not start dev server to generate open graph images'); + console.error(e); + } +} diff --git a/packages/cli/types/main.d.ts b/packages/cli/types/main.d.ts index ab63c1e..8e4d2c0 100644 --- a/packages/cli/types/main.d.ts +++ b/packages/cli/types/main.d.ts @@ -47,6 +47,11 @@ export interface FullRocketCliOptions extends Pick // rarely used configFile: string; outputDevDir: URL | string; + + lint: { + buildHtml: boolean; + [key: string]: any; + }; } export type RocketCliOptions = Partial; diff --git a/site/pages/10--docs/30--guides/50--go-live.rocket.md b/site/pages/10--docs/30--guides/50--go-live.rocket.md index 5aee953..5166c32 100644 --- a/site/pages/10--docs/30--guides/50--go-live.rocket.md +++ b/site/pages/10--docs/30--guides/50--go-live.rocket.md @@ -34,6 +34,40 @@ const { resolve } = createRequire(new URL('.', import.meta.url)); A few things are usually needed before going live "for real". +## Make sure all links are correct + +When you launch a website you don't want the first feedback to be "that link doesn't work". + +To prevent this we want to execute `rocket lint` before going live. +It will make sure all internal links are correct by using [check-html-links](../../30--tools/40--check-html-links/10--overview.rocket.md). +Typically we deploy via a Continuous Integration system like GitHub Actions or Netlify Deploy. +We can also integrate the lint command into that process. + +``` +rocket build +rocket lint +``` + +### Fixing broken links + +If found a couple of broken links on your page and you want to fix them and verify that they are now correct it might be a little time consuming to create a full production build every time. +The reason is that a production build is doing a lot of things + +1. Generate HTML +2. Generate & Inject Open Graph Images +3. Optimize Images (not available yet) +4. Optimize JavaScript + +But there is a way around this. We can use an optional flag `--build-html` which means it will run only (1) and then lint that (non-optimized) HTML output. + +So for a more time efficient way of validating link use + +```bash +rocket lint --build-html +``` + +Note: We can do this as 2-4 generally does not impact links/references (as long as the optimizations scripts do not have related bugs) + ## Add a Not Found Page When a user enters a URL that does not exist, a "famous" 404 Page Not Found error occurs. diff --git a/site/pages/pageTreeData.rocketGenerated.json b/site/pages/pageTreeData.rocketGenerated.json index af041fe..3c14b29 100644 --- a/site/pages/pageTreeData.rocketGenerated.json +++ b/site/pages/pageTreeData.rocketGenerated.json @@ -1363,6 +1363,16 @@ "id": "go-live", "level": 1 }, + { + "text": "Make sure all links are correct", + "id": "make-sure-all-links-are-correct", + "level": 2 + }, + { + "text": "Fixing broken links", + "id": "fixing-broken-links", + "level": 3 + }, { "text": "Add a Not Found Page", "id": "add-a-not-found-page", diff --git a/yarn.lock b/yarn.lock index 48f95ea..1f1350e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2084,13 +2084,6 @@ terser "^5.14.2" whatwg-fetch "^3.5.0" -"@web/rollup-plugin-copy@^0.3.0": - version "0.3.0" - resolved "https://registry.yarnpkg.com/@web/rollup-plugin-copy/-/rollup-plugin-copy-0.3.0.tgz#fe999b2ea3dd71c8e663e6947fc2eb92a221e8e8" - integrity sha512-QNNtE7Svhk0/p21etaR0JQXYhlMgTAg/HmRXDMmQHMf3uOUWsWMGiJa96P49RRVJut1ECB5FDFeBUgFEmegysQ== - dependencies: - glob "^7.1.6" - "@web/rollup-plugin-html@^1.8.0": version "1.11.0" resolved "https://registry.yarnpkg.com/@web/rollup-plugin-html/-/rollup-plugin-html-1.11.0.tgz#46c2bcb3a3b9db55d53b897ffc7e7ef2f618a052"