From 770418f8d262c56cd3de5ba1087f972ab20da617 Mon Sep 17 00:00:00 2001 From: Joshua Chen Date: Tue, 21 Dec 2021 00:24:59 +0800 Subject: [PATCH] refactor: unify log format with new logger utility (#5994) Co-authored-by: sebastienlorber --- .eslintrc.js | 2 +- packages/create-docusaurus/bin/index.js | 17 +- packages/create-docusaurus/package.json | 2 +- packages/create-docusaurus/src/index.ts | 77 ++++----- packages/docusaurus-logger/.npmignore | 4 + packages/docusaurus-logger/README.md | 44 +++++ packages/docusaurus-logger/demo.png | Bin 0 -> 94456 bytes packages/docusaurus-logger/package.json | 32 ++++ .../docusaurus-logger/src/__mocks__/chalk.js | 11 ++ .../src/__tests__/index.test.ts | 73 ++++++++ packages/docusaurus-logger/src/index.ts | 134 ++++++++++++++ packages/docusaurus-logger/tsconfig.json | 11 ++ packages/docusaurus-mdx-loader/package.json | 2 +- packages/docusaurus-mdx-loader/src/index.ts | 4 +- packages/docusaurus-migrate/bin/index.js | 14 +- packages/docusaurus-migrate/package.json | 2 +- packages/docusaurus-migrate/src/index.ts | 163 +++++++----------- .../package.json | 1 + .../src/collectRedirects.ts | 24 +-- .../package.json | 2 +- .../src/authors.ts | 8 +- .../src/blogUtils.ts | 14 +- .../package.json | 2 +- .../src/__tests__/cli.test.ts | 16 +- .../src/__tests__/lastUpdate.test.ts | 11 +- .../docusaurus-plugin-content-docs/src/cli.ts | 3 +- .../src/docs.ts | 8 +- .../src/index.ts | 8 +- .../src/lastUpdate.ts | 7 +- .../src/options.ts | 8 +- .../src/routes.ts | 8 +- .../src/sidebars/__tests__/generator.test.ts | 2 +- .../src/sidebars/generator.ts | 14 +- .../docusaurus-theme-classic/package.json | 1 - .../package.json | 1 + .../src/index.ts | 7 +- .../package.json | 3 +- .../docusaurus-theme-translations/update.js | 90 ++++------ .../docusaurus-utils-validation/package.json | 2 +- .../src/validationUtils.ts | 40 ++--- packages/docusaurus-utils/package.json | 5 +- packages/docusaurus-utils/src/index.ts | 8 +- .../docusaurus-utils/src/markdownParser.ts | 8 +- packages/docusaurus/bin/beforeCli.js | 29 ++-- packages/docusaurus/bin/docusaurus.js | 10 +- packages/docusaurus/package.json | 2 +- packages/docusaurus/src/choosePort.ts | 26 ++- packages/docusaurus/src/client/serverEntry.js | 26 ++- packages/docusaurus/src/commands/build.ts | 31 +--- packages/docusaurus/src/commands/clear.ts | 20 +-- packages/docusaurus/src/commands/deploy.ts | 32 ++-- packages/docusaurus/src/commands/serve.ts | 19 +- packages/docusaurus/src/commands/start.ts | 20 +-- packages/docusaurus/src/commands/swizzle.ts | 92 ++++------ .../src/commands/writeHeadingIds.ts | 21 +-- .../server/__tests__/configValidation.test.ts | 2 +- .../src/server/__tests__/i18n.test.ts | 3 +- .../docusaurus/src/server/configValidation.ts | 3 +- packages/docusaurus/src/server/i18n.ts | 22 +-- packages/docusaurus/src/server/index.ts | 17 +- .../docusaurus/src/server/plugins/index.ts | 8 +- .../src/server/translations/translations.ts | 32 ++-- .../translations/translationsExtractor.ts | 11 +- packages/docusaurus/src/webpack/client.ts | 9 +- packages/docusaurus/src/webpack/utils.ts | 38 ++-- packages/docusaurus/tsconfig.json | 1 - 66 files changed, 717 insertions(+), 650 deletions(-) create mode 100644 packages/docusaurus-logger/.npmignore create mode 100644 packages/docusaurus-logger/README.md create mode 100644 packages/docusaurus-logger/demo.png create mode 100644 packages/docusaurus-logger/package.json create mode 100644 packages/docusaurus-logger/src/__mocks__/chalk.js create mode 100644 packages/docusaurus-logger/src/__tests__/index.test.ts create mode 100644 packages/docusaurus-logger/src/index.ts create mode 100644 packages/docusaurus-logger/tsconfig.json diff --git a/.eslintrc.js b/.eslintrc.js index 67999a3a2c..383b94b2f1 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -122,7 +122,7 @@ module.exports = { 'array-callback-return': WARNING, camelcase: WARNING, 'no-restricted-syntax': WARNING, - 'no-unused-expressions': WARNING, + 'no-unused-expressions': [WARNING, {allowTaggedTemplates: true}], 'global-require': WARNING, 'prefer-destructuring': WARNING, yoda: WARNING, diff --git a/packages/create-docusaurus/bin/index.js b/packages/create-docusaurus/bin/index.js index 5598fd7486..bd608096f0 100755 --- a/packages/create-docusaurus/bin/index.js +++ b/packages/create-docusaurus/bin/index.js @@ -6,7 +6,9 @@ * LICENSE file in the root directory of this source tree. */ -const chalk = require('chalk'); +// @ts-check + +const logger = require('@docusaurus/logger').default; const semver = require('semver'); const path = require('path'); const program = require('commander'); @@ -14,19 +16,15 @@ const {default: init} = require('../lib'); const requiredVersion = require('../package.json').engines.node; if (!semver.satisfies(process.version, requiredVersion)) { - console.log( - chalk.red(`\nMinimum Node.js version not met :)`) + - chalk.yellow( - `\nYou are using Node.js ${process.version}, Requirement: Node.js ${requiredVersion}.\n`, - ), - ); + logger.error('Minimum Node.js version not met :('); + logger.info`You are using Node.js number=${process.version}, Requirement: Node.js number=${requiredVersion}.`; process.exit(1); } function wrapCommand(fn) { return (...args) => fn(...args).catch((err) => { - console.error(chalk.red(err.stack)); + logger.error(err.stack); process.exitCode = 1; }); } @@ -58,8 +56,7 @@ program program.arguments('').action((cmd) => { program.outputHelp(); - console.log(` ${chalk.red(`\n Unknown command ${chalk.yellow(cmd)}.`)}`); - console.log(); + logger.error`Unknown command code=${cmd}.`; }); program.parse(process.argv); diff --git a/packages/create-docusaurus/package.json b/packages/create-docusaurus/package.json index f4da621ddd..fc927edc2a 100755 --- a/packages/create-docusaurus/package.json +++ b/packages/create-docusaurus/package.json @@ -23,7 +23,7 @@ }, "license": "MIT", "dependencies": { - "chalk": "^4.1.2", + "@docusaurus/logger": "2.0.0-beta.13", "commander": "^5.1.0", "fs-extra": "^10.0.0", "lodash": "^4.17.20", diff --git a/packages/create-docusaurus/src/index.ts b/packages/create-docusaurus/src/index.ts index c7bb9cd57a..b51683c571 100755 --- a/packages/create-docusaurus/src/index.ts +++ b/packages/create-docusaurus/src/index.ts @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import chalk from 'chalk'; +import logger from '@docusaurus/logger'; import fs from 'fs-extra'; import {execSync} from 'child_process'; import prompts, {Choice} from 'prompts'; @@ -131,12 +131,14 @@ export default async function init( } if (!name) { - throw new Error(chalk.red('A website name is required.')); + logger.error('A website name is required.'); + process.exit(1); } const dest = path.resolve(rootDir, name); if (fs.existsSync(dest)) { - throw new Error(`Directory already exists at "${dest}"!`); + logger.error`Directory already exists at path=${dest}!`; + process.exit(1); } let template = reqTemplate; @@ -171,10 +173,10 @@ export default async function init( if (url && isValidGitRepoUrl(url)) { return true; } - return chalk.red(`Invalid repository URL`); + return logger.red('Invalid repository URL'); }, - message: - 'Enter a repository URL from GitHub, Bitbucket, GitLab, or any other public repo.\n(e.g: https://github.com/ownerName/repoName.git)', + message: logger.interpolate`Enter a repository URL from GitHub, Bitbucket, GitLab, or any other public repo. +(e.g: path=${'https://github.com/ownerName/repoName.git'})`, }); template = repoPrompt.gitRepoUrl; } else if (template === 'Local template') { @@ -187,11 +189,11 @@ export default async function init( if (fs.existsSync(fullDir)) { return true; } - return chalk.red( - `The path ${chalk.magenta(fullDir)} does not exist.`, + return logger.red( + logger.interpolate`path=${fullDir} does not exist.`, ); } - return chalk.red('Please enter a valid path.'); + return logger.red('Please enter a valid path.'); }, message: 'Enter a local folder path, relative to the current working directory.', @@ -200,37 +202,34 @@ export default async function init( } if (!template) { - throw new Error('Template should not be empty'); + logger.error('Template should not be empty'); + process.exit(1); } - console.log(` -${chalk.cyan('Creating new Docusaurus project...')} -`); + logger.info('Creating new Docusaurus project...'); if (isValidGitRepoUrl(template)) { - console.log(`Cloning Git template ${chalk.cyan(template)}...`); + logger.info`Cloning Git template path=${template}...`; if ( shell.exec(`git clone --recursive ${template} ${dest}`, {silent: true}) .code !== 0 ) { - throw new Error(chalk.red(`Cloning Git template ${template} failed!`)); + logger.error`Cloning Git template name=${template} failed!`; + process.exit(1); } } else if (templates.includes(template)) { // Docusaurus templates. if (useTS) { if (!hasTS(template)) { - throw new Error( - `Template ${template} doesn't provide the Typescript variant.`, - ); + logger.error`Template name=${template} doesn't provide the Typescript variant.`; + process.exit(1); } template = `${template}${TypeScriptTemplateSuffix}`; } try { await copyTemplate(templatesDir, template, dest); } catch (err) { - console.log( - `Copying Docusaurus template ${chalk.cyan(template)} failed!`, - ); + logger.error`Copying Docusaurus template name=${template} failed!`; throw err; } } else if (fs.existsSync(path.resolve(process.cwd(), template))) { @@ -238,11 +237,12 @@ ${chalk.cyan('Creating new Docusaurus project...')} try { await fs.copy(templateDir, dest); } catch (err) { - console.log(`Copying local template ${templateDir} failed!`); + logger.error`Copying local template path=${templateDir} failed!`; throw err; } } else { - throw new Error('Invalid template.'); + logger.error('Invalid template.'); + process.exit(1); } // Update package.json info. @@ -253,7 +253,7 @@ ${chalk.cyan('Creating new Docusaurus project...')} private: true, }); } catch (err) { - console.log(chalk.red('Failed to update package.json.')); + logger.error('Failed to update package.json.'); throw err; } @@ -275,7 +275,7 @@ ${chalk.cyan('Creating new Docusaurus project...')} ? name : path.relative(process.cwd(), name); if (!cliOptions.skipInstall) { - console.log(`Installing dependencies with ${chalk.cyan(pkgManager)}...`); + logger.info`Installing dependencies with name=${pkgManager}...`; if ( shell.exec( `cd "${name}" && ${useYarn ? 'yarn' : 'npm install --color always'}`, @@ -288,36 +288,35 @@ ${chalk.cyan('Creating new Docusaurus project...')} }, ).code !== 0 ) { - console.error(chalk.red('Dependency installation failed.')); - console.log(`The site directory has already been created, and you can retry by typing: + logger.error('Dependency installation failed.'); + logger.info`The site directory has already been created, and you can retry by typing: - ${chalk.cyan('cd')} ${cdpath} - ${chalk.cyan(`${pkgManager} install`)}`); + code=${`cd ${cdpath}`} + code=${`${pkgManager} install`}`; process.exit(0); } } - console.log(` -Successfully created "${chalk.cyan(cdpath)}". -Inside that directory, you can run several commands: + logger.success`Created path=${cdpath}.`; + logger.info`Inside that directory, you can run several commands: - ${chalk.cyan(`${pkgManager} start`)} + code=${`${pkgManager} start`} Starts the development server. - ${chalk.cyan(`${pkgManager} ${useYarn ? '' : 'run '}build`)} + code=${`${pkgManager} ${useYarn ? '' : 'run '}build`} Bundles your website into static files for production. - ${chalk.cyan(`${pkgManager} ${useYarn ? '' : 'run '}serve`)} + code=${`${pkgManager} ${useYarn ? '' : 'run '}serve`} Serves the built website locally. - ${chalk.cyan(`${pkgManager} deploy`)} + code=${`${pkgManager} deploy`} Publishes the website to GitHub pages. We recommend that you begin by typing: - ${chalk.cyan('cd')} ${cdpath} - ${chalk.cyan(`${pkgManager} start`)} + code=${`cd ${cdpath}`} + code=${`${pkgManager} start`} Happy building awesome websites! -`); +`; } diff --git a/packages/docusaurus-logger/.npmignore b/packages/docusaurus-logger/.npmignore new file mode 100644 index 0000000000..3eabd2dd8d --- /dev/null +++ b/packages/docusaurus-logger/.npmignore @@ -0,0 +1,4 @@ +copyUntypedFiles.js +.tsbuildinfo +tsconfig* +__tests__ diff --git a/packages/docusaurus-logger/README.md b/packages/docusaurus-logger/README.md new file mode 100644 index 0000000000..2c4cea3bf5 --- /dev/null +++ b/packages/docusaurus-logger/README.md @@ -0,0 +1,44 @@ +# `@docusaurus/logger` + +An encapsulated logger for semantically formatting console messages. + +## APIs + +It exports a single object as default export: `logger`. `logger` has the following properties: + +- Some useful colors. +- Formatters. These functions have the same signature as the formatters of `picocolors`. Note that their implementations are not guaranteed. You should only care about their semantics. + - `path`: formats a file path or URL. + - `id`: formats an identifier. + - `code`: formats a code snippet. + - `subdue`: subdues the text. + - `num`: formats a number. +- The `interpolate` function. It is a template literal tag. +- Logging functions. All logging functions can both be used as functions (in which it has the same usage as `console.log`) or template literal tags. + - `info`: prints information. + - `warn`: prints a warning that should be payed attention to. + - `error`: prints an error (not necessarily halting the program) that signals significant problems. + - `success`: prints a success message. + +### Using the template literal tag + +The template literal tag evaluates the template and expressions embedded. `interpolate` returns a new string, while other logging functions prints it. Below is a typical usage: + +```js +logger.info`Hello name=${name}! You have number=${money} dollars. Here are the ${ + items.length > 1 ? 'items' : 'item' +} on the shelf: ${items} +To buy anything, enter code=${'buy x'} where code=${'x'} is the item's name; to quit, press code=${'Ctrl + C'}.`; +``` + +An embedded expression is optionally preceded by a flag in the form `%[a-z]+` (a percentage sign followed by a few lowercase letters). If it's not preceded by any flag, it's printed out as-is. Otherwise, it's formatted with one of the formatters: + +- `path=`: `path` +- `name=`: `id` +- `code=`: `code` +- `subdue=`: `subdue` +- `number=`: `num` + +If the expression is an array, it's formatted by `` `\n- ${array.join('\n- ')}\n` `` (note it automatically gets a leading line end). Each member is formatted by itself and the bullet is not formatted. So you would see the above message printed as: + +![demo](./demo.png) diff --git a/packages/docusaurus-logger/demo.png b/packages/docusaurus-logger/demo.png new file mode 100644 index 0000000000000000000000000000000000000000..f3877552104f4c8c3500610950cc0c3de15e2012 GIT binary patch literal 94456 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

DVa5CEE(UaSFYJiLv>c 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ୁ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�sUPx zl$|HSYt55h_|sClqllYl{=@`Vpm1%K2anwVv3HpBlWdSaUfyk59T8Kx7}3{q1|IQ! z={eI%J_#3dzG9Q`?0tJgNARBblLqkSZ26nd>nU+JbM?{amBI2@iq0>D0q}r+Ps#5< zoJ!NEYU-{dZ`fvzV4DuC-A%;h;=lvtg%=nDbZ@NT0)2$*aYGju`b!Su^U9KNiBkLV z-zgjk(N2n=eux>09*5epC2*NW!P6_ zk~-LDZYK|`D&tO4pilZfAuwstPcTfBpzpx%??Pn3P7=!RZpTnJX~C7gN28C6^wrr! zGKOlKi@3VNIyigN79&jvbE&0AN?&!6ZS^8noB6?SutPTnqJNOu03RqM&8|1ip$C9c z`DY2^JinGXsjp-jW;{&9BkTxS`w?Pv-r8xs$ex{<7uJWizb+CU6r5D*#o933-vhC6 zi47w9S~wuQ(;(5S@;wz*l1s=7Gzf}@3^uU5Bzrm2f(NJbQHib_Iy~N?uS1D-Pz7_W zB?Klwfe{aUVL;!l&_U_EM)uPFn zqQxb=hanc)ibLY+D z{̪qP|;TX;AhZx7W8gdmI1**R2a(g5?U1F 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 literal 0 HcmV?d00001 diff --git a/packages/docusaurus-logger/package.json b/packages/docusaurus-logger/package.json new file mode 100644 index 0000000000..51907b08ac --- /dev/null +++ b/packages/docusaurus-logger/package.json @@ -0,0 +1,32 @@ +{ + "name": "@docusaurus/logger", + "version": "2.0.0-beta.13", + "description": "An encapsulated logger for semantically formatting console messages.", + "main": "./lib/index.js", + "repository": { + "type": "git", + "url": "https://github.com/facebook/docusaurus.git", + "directory": "packages/docusaurus-logger" + }, + "bugs": { + "url": "https://github.com/facebook/docusaurus/issues" + }, + "scripts": { + "build": "tsc", + "watch": "tsc --watch" + }, + "publishConfig": { + "access": "public" + }, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14" + }, + "devDependencies": { + "@types/supports-color": "^8.1.1" + } +} diff --git a/packages/docusaurus-logger/src/__mocks__/chalk.js b/packages/docusaurus-logger/src/__mocks__/chalk.js new file mode 100644 index 0000000000..361c56faf9 --- /dev/null +++ b/packages/docusaurus-logger/src/__mocks__/chalk.js @@ -0,0 +1,11 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +const chalk = require('chalk'); + +// Force coloring the output even in CI +module.exports = new chalk.Instance({level: 3}); diff --git a/packages/docusaurus-logger/src/__tests__/index.test.ts b/packages/docusaurus-logger/src/__tests__/index.test.ts new file mode 100644 index 0000000000..8436de916c --- /dev/null +++ b/packages/docusaurus-logger/src/__tests__/index.test.ts @@ -0,0 +1,73 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import logger from '../index'; + +describe('formatters', () => { + test('path', () => { + expect(logger.path('hey')).toMatchInlineSnapshot(`"hey"`); + }); + test('id', () => { + expect(logger.name('hey')).toMatchInlineSnapshot(`"hey"`); + }); + test('code', () => { + expect(logger.code('hey')).toMatchInlineSnapshot(`"\`hey\`"`); + }); + test('subdue', () => { + expect(logger.subdue('hey')).toMatchInlineSnapshot(`"hey"`); + }); +}); + +describe('interpolate', () => { + test('should format text with variables & arrays', () => { + const name = 'Josh'; + const items = [1, 'hi', 'Hmmm']; + expect(logger.interpolate`Hello ${name}! Here are your goodies:${items}`) + .toMatchInlineSnapshot(` + "Hello Josh! Here are your goodies: + - 1 + - hi + - Hmmm" + `); + }); + test('should recognize valid flags', () => { + expect( + logger.interpolate`The package at path=${'packages/docusaurus'} has number=${10} files. name=${'Babel'} is exported here subdue=${'(as a preset)'} that you can with code=${"require.resolve('@docusaurus/core/lib/babel/preset')"}`, + ).toMatchInlineSnapshot( + `"The package at packages/docusaurus has 10 files. Babel is exported here (as a preset) that you can with \`require.resolve('@docusaurus/core/lib/babel/preset')\`"`, + ); + }); + test('should interpolate arrays with flags', () => { + expect( + logger.interpolate`The following commands are available:code=${[ + 'docusaurus start', + 'docusaurus build', + 'docusaurus deploy', + ]}`, + ).toMatchInlineSnapshot(` + "The following commands are available: + - \`docusaurus start\` + - \`docusaurus build\` + - \`docusaurus deploy\`" + `); + }); + test('should print detached flags as-is', () => { + expect( + logger.interpolate`You can use placeholders like code= ${'and it will'} be replaced with the succeeding arguments`, + ).toMatchInlineSnapshot( + `"You can use placeholders like code= and it will be replaced with the succeeding arguments"`, + ); + }); + test('should throw with bad flags', () => { + expect( + () => + logger.interpolate`I mistyped this: cde=${'this code'} and I will be damned`, + ).toThrowErrorMatchingInlineSnapshot( + `"Bad Docusaurus logging message. This is likely an internal bug, please report it."`, + ); + }); +}); diff --git a/packages/docusaurus-logger/src/index.ts b/packages/docusaurus-logger/src/index.ts new file mode 100644 index 0000000000..65273995ce --- /dev/null +++ b/packages/docusaurus-logger/src/index.ts @@ -0,0 +1,134 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import chalk, {Chalk} from 'chalk'; + +type InterpolatableValue = string | number | (string | number)[]; + +const path = (msg: unknown): string => chalk.cyan(chalk.underline(msg)); +const name = (msg: unknown): string => chalk.blue(chalk.bold(msg)); +const code = (msg: unknown): string => chalk.cyan(`\`${msg}\``); +const subdue: Chalk = chalk.gray; +const num: Chalk = chalk.yellow; + +function interpolate( + msgs: TemplateStringsArray, + ...values: InterpolatableValue[] +): string { + let res = ''; + values.forEach((value, idx) => { + const flag = msgs[idx].match(/[a-z]+=$/); + res += msgs[idx].replace(/[a-z]+=$/, ''); + const format = (function () { + if (!flag) { + return (a: string | number) => a; + } + switch (flag[0]) { + case 'path=': + return path; + case 'number=': + return num; + case 'name=': + return name; + case 'subdue=': + return subdue; + case 'code=': + return code; + default: + throw new Error( + 'Bad Docusaurus logging message. This is likely an internal bug, please report it.', + ); + } + })(); + res += Array.isArray(value) + ? `\n- ${value.map((v) => format(v)).join('\n- ')}` + : format(value); + }); + res += msgs.slice(-1)[0]; + return res; +} + +function info(msg: unknown): void; +function info( + msg: TemplateStringsArray, + ...values: [InterpolatableValue, ...InterpolatableValue[]] +): void; +function info(msg: unknown, ...values: InterpolatableValue[]): void { + console.info( + `${chalk.cyan(chalk.bold('[INFO]'))} ${ + values.length === 0 + ? msg + : interpolate(msg as TemplateStringsArray, ...values) + }`, + ); +} +function warn(msg: unknown): void; +function warn( + msg: TemplateStringsArray, + ...values: [InterpolatableValue, ...InterpolatableValue[]] +): void; +function warn(msg: unknown, ...values: InterpolatableValue[]): void { + console.warn( + chalk.yellow( + `${chalk.bold('[WARNING]')} ${ + values.length === 0 + ? msg + : interpolate(msg as TemplateStringsArray, ...values) + }`, + ), + ); +} +function error(msg: unknown): void; +function error( + msg: TemplateStringsArray, + ...values: [InterpolatableValue, ...InterpolatableValue[]] +): void; +function error(msg: unknown, ...values: InterpolatableValue[]): void { + console.error( + chalk.red( + `${chalk.bold('[ERROR]')} ${ + values.length === 0 + ? msg + : interpolate(msg as TemplateStringsArray, ...values) + }`, + ), + ); +} +function success(msg: unknown): void; +function success( + msg: TemplateStringsArray, + ...values: [InterpolatableValue, ...InterpolatableValue[]] +): void; +function success(msg: unknown, ...values: InterpolatableValue[]): void { + console.log( + `${chalk.green(chalk.bold('[SUCCESS]'))} ${ + values.length === 0 + ? msg + : interpolate(msg as TemplateStringsArray, ...values) + }`, + ); +} + +const logger = { + red: chalk.red, + yellow: chalk.yellow, + green: chalk.green, + bold: chalk.bold, + dim: chalk.dim, + path, + name, + code, + subdue, + num, + interpolate, + info, + warn, + error, + success, +}; + +export default logger; diff --git a/packages/docusaurus-logger/tsconfig.json b/packages/docusaurus-logger/tsconfig.json new file mode 100644 index 0000000000..aee99fc0f3 --- /dev/null +++ b/packages/docusaurus-logger/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "incremental": true, + "tsBuildInfoFile": "./lib/.tsbuildinfo", + "sourceMap": true, + "declarationMap": true, + "rootDir": "src", + "outDir": "lib" + } +} diff --git a/packages/docusaurus-mdx-loader/package.json b/packages/docusaurus-mdx-loader/package.json index 603e13a65a..36615b5704 100644 --- a/packages/docusaurus-mdx-loader/package.json +++ b/packages/docusaurus-mdx-loader/package.json @@ -20,10 +20,10 @@ "dependencies": { "@babel/parser": "^7.16.4", "@babel/traverse": "^7.16.3", + "@docusaurus/logger": "2.0.0-beta.13", "@docusaurus/utils": "2.0.0-beta.13", "@mdx-js/mdx": "^1.6.21", "@mdx-js/react": "^1.6.21", - "chalk": "^4.1.2", "escape-html": "^1.0.3", "file-loader": "^6.2.0", "fs-extra": "^10.0.0", diff --git a/packages/docusaurus-mdx-loader/src/index.ts b/packages/docusaurus-mdx-loader/src/index.ts index a37256db8a..c12c5dc938 100644 --- a/packages/docusaurus-mdx-loader/src/index.ts +++ b/packages/docusaurus-mdx-loader/src/index.ts @@ -7,7 +7,7 @@ import {readFile} from 'fs-extra'; import mdx from '@mdx-js/mdx'; -import chalk from 'chalk'; +import logger from '@docusaurus/logger'; import emoji from 'remark-emoji'; import { parseFrontMatter, @@ -164,7 +164,7 @@ ${JSON.stringify(frontMatter, null, 2)}`; if (shouldError) { return callback(new Error(errorMessage)); } else { - console.warn(chalk.yellow(errorMessage)); + logger.warn(errorMessage); } } } diff --git a/packages/docusaurus-migrate/bin/index.js b/packages/docusaurus-migrate/bin/index.js index 79145ede74..2b79c4d521 100755 --- a/packages/docusaurus-migrate/bin/index.js +++ b/packages/docusaurus-migrate/bin/index.js @@ -6,7 +6,9 @@ * LICENSE file in the root directory of this source tree. */ -const chalk = require('chalk'); +// @ts-check + +const logger = require('@docusaurus/logger').default; const semver = require('semver'); const cli = require('commander'); const path = require('path'); @@ -18,18 +20,14 @@ const {migrateDocusaurusProject, migrateMDToMDX} = require('../lib'); function wrapCommand(fn) { return (...args) => fn(...args).catch((err) => { - console.error(chalk.red(err.stack)); + logger.error(err.stack); process.exitCode = 1; }); } if (!semver.satisfies(process.version, requiredVersion)) { - console.log( - chalk.red(`\nMinimum Node.js version not met :(`) + - chalk.yellow( - `\n\nYou are using Node ${process.version}. We require Node.js ${requiredVersion} or up!\n`, - ), - ); + logger.error('Minimum Node.js version not met :('); + logger.info`You are using Node.js number=${process.version}, Requirement: Node.js number=${requiredVersion}.`; process.exit(1); } diff --git a/packages/docusaurus-migrate/package.json b/packages/docusaurus-migrate/package.json index 330cc84893..ddf80933c5 100644 --- a/packages/docusaurus-migrate/package.json +++ b/packages/docusaurus-migrate/package.json @@ -24,8 +24,8 @@ }, "dependencies": { "@babel/preset-env": "^7.16.4", + "@docusaurus/logger": "2.0.0-beta.13", "@mapbox/hast-util-to-jsx": "^1.0.0", - "chalk": "^4.1.2", "color": "^4.0.1", "commander": "^5.1.0", "fs-extra": "^10.0.0", diff --git a/packages/docusaurus-migrate/src/index.ts b/packages/docusaurus-migrate/src/index.ts index d4c5449a23..880454e5c1 100644 --- a/packages/docusaurus-migrate/src/index.ts +++ b/packages/docusaurus-migrate/src/index.ts @@ -5,9 +5,9 @@ * LICENSE file in the root directory of this source tree. */ -import * as fs from 'fs-extra'; +import fs from 'fs-extra'; import importFresh from 'import-fresh'; -import chalk from 'chalk'; +import logger from '@docusaurus/logger'; import glob from 'glob'; import Color from 'color'; @@ -79,7 +79,7 @@ export async function migrateDocusaurusProject( ): Promise { function createMigrationContext(): MigrationContext { const v1Config = importFresh(`${siteDir}/siteConfig`) as VersionOneConfig; - console.log('Starting migration from v1 to v2...'); + logger.info('Starting migration from v1 to v2...'); const partialMigrationContext = { siteDir, newDir, @@ -109,85 +109,68 @@ export async function migrateDocusaurusProject( react: '^17.0.1', 'react-dom': '^17.0.1', }; + let errorCount = 0; try { createClientRedirects(siteConfig, deps, config); - console.log( - chalk.green('Successfully created client redirect for non clean URL'), - ); - } catch (errorInClientRedirect) { - console.log( - chalk.red(`Error while creating redirects: ${errorInClientRedirect}`), - ); + logger.success('Created client redirect for non clean URL'); + } catch (e) { + logger.error(`Failed to creating redirects: ${e}`); + errorCount += 1; } if (shouldMigratePages) { try { createPages(newDir, siteDir); - console.log( - chalk.green( - 'Successfully created pages (check migration page for more details)', - ), - ); - } catch (errorInMigratingPages) { - console.log( - chalk.red( - `Error occurred while creating pages: ${errorInMigratingPages}`, - ), + logger.success( + 'Created new doc pages (check migration page for more details)', ); + } catch (e) { + logger.error(`Failed to create new doc pages: ${e}`); + errorCount += 1; } } else { try { createDefaultLandingPage(newDir); - console.log( - chalk.green( - 'Successfully created landing page (check migration page for more details)', - ), - ); - } catch (errorInLandingPage) { - console.log( - chalk.red( - `Error occurred while creating landing page: ${errorInLandingPage}`, - ), + logger.success( + 'Created landing page (check migration page for more details)', ); + } catch (e) { + logger.error(`Failed to create landing page: ${e}`); + errorCount += 1; } } try { migrateStaticFiles(siteDir, newDir); - console.log(chalk.green('Successfully migrated static folder')); - } catch (errorInStatic) { - console.log( - chalk.red(`Error occurred while copying static folder: ${errorInStatic}`), - ); + logger.success('Migrated static folder'); + } catch (e) { + logger.error(`Failed to copy static folder: ${e}`); + errorCount += 1; } try { migrateBlogFiles(siteDir, newDir, classicPreset, shouldMigrateMdFiles); - } catch (errorInMigratingBlogs) { - console.log( - chalk.red( - `Error occurred while migrating blogs: ${errorInMigratingBlogs}`, - ), - ); + } catch (e) { + logger.error(`Failed to migrate blogs: ${e}`); + errorCount += 1; } try { handleVersioning(siteDir, siteConfig, newDir, config, shouldMigrateMdFiles); - } catch (errorInVersion) { - console.log( - chalk.red( - `Error occurred while migrating versioned docs: ${errorInVersion}`, - ), - ); + } catch (e) { + logger.error(`Failed to migrate versioned docs: ${e}`); + errorCount += 1; } try { migrateLatestDocs(siteDir, newDir, shouldMigrateMdFiles, classicPreset); - } catch (errorInDoc) { - chalk.red(`Error occurred while migrating docs: ${errorInDoc}`); + } catch (e) { + logger.error(`Failed to migrate docs: ${e}`); + errorCount += 1; } try { migrateLatestSidebar(siteDir, newDir, classicPreset, siteConfig); - } catch (error) { - console.log(chalk.red(`Error occurred while migrating sidebar: ${error}`)); + } catch (e) { + logger.error(`Failed to migrate sidebar: ${e}`); + errorCount += 1; } try { @@ -195,26 +178,26 @@ export async function migrateDocusaurusProject( path.join(newDir, 'docusaurus.config.js'), `module.exports=${JSON.stringify(config, null, 2)}`, ); - console.log( - chalk.green( - `Successfully created a new config file with new navbar and footer config`, - ), - ); - } catch (error) { - console.log( - chalk.red(`Error occurred while creating config file: ${error}`), + logger.success( + `Created a new config file with new navbar and footer config`, ); + } catch (e) { + logger.error(`Failed to create config file: ${e}`); + errorCount += 1; } try { migratePackageFile(siteDir, deps, newDir); - } catch (error) { - console.log( - chalk.red( - `Error occurred while creating package.json file for project: ${error}`, - ), + } catch (e) { + logger.error( + `Error occurred while creating package.json file for project: ${e}`, ); + errorCount += 1; + } + if (errorCount) { + logger.warn`Migration from v1 to v2 failed with number=${errorCount} errors: please check the log above`; + } else { + logger.success('Completed migration from v1 to v2'); } - console.log('Completed migration from v1 to v2'); } export function createConfigFile({ @@ -270,11 +253,9 @@ export function createConfigFile({ customConfigFields[key] = value; } }); - console.log( - `${chalk.yellow( - 'Following Fields from siteConfig.js will be added to docusaurus.config.js in `customFields`', - )}\n${chalk.yellow(Object.keys(customConfigFields).join('\n'))}`, - ); + logger.info`Following Fields from path=${'siteConfig.js'} will be added to path=${'docusaurus.config.js'} in code=${'customFields'}: ${Object.keys( + customConfigFields, + )}`; let v2DocsPath: string | undefined; if (siteConfig.customDocsPath) { @@ -409,12 +390,12 @@ function createPages(newDir: string, siteDir: string): void { const content = String(fs.readFileSync(filePath)); fs.writeFileSync(filePath, migratePage(content)); }); - } catch (error) { - console.log(chalk.red(`Unable to migrate Pages : ${error}`)); + } catch (e) { + logger.error(`Unable to migrate Pages: ${e}`); createDefaultLandingPage(newDir); } } else { - console.log('Ignoring Pages'); + logger.info('Ignoring Pages'); } } @@ -452,13 +433,9 @@ function migrateBlogFiles( fs.writeFileSync(file, sanitizedFileContent(content, migrateMDFiles)); }); classicPreset.blog.path = 'blog'; - console.log( - chalk.green( - `Successfully migrated blogs to version 2 with change in frontmatter`, - ), - ); + logger.success('Migrated blogs to version 2 with change in front matter'); } else { - console.log(chalk.yellow(`Blog not found. Skipping migration for blog`)); + logger.warn('Blog not found. Skipping migration for blog'); } } @@ -489,18 +466,10 @@ function handleVersioning( versionRegex, migrateMDFiles, ); - console.log( - chalk.green( - `Successfully migrated version docs and sidebar. The following doc versions have been created: \n${loadedVersions.join( - '\n', - )}`, - ), - ); + logger.success`Migrated version docs and sidebar. The following doc versions have been created:name=${loadedVersions}`; } else { - console.log( - chalk.yellow( - 'Versioned docs not found. Skipping migration for versioned docs', - ), + logger.warn( + 'Versioned docs not found. Skipping migration for versioned docs', ); } } @@ -689,9 +658,7 @@ function migrateLatestSidebar( 'sidebars.json', ); } catch { - console.log( - chalk.yellow(`Sidebar not found. Skipping migration for sidebar`), - ); + logger.warn('Sidebar not found. Skipping migration for sidebar'); } if (siteConfig.colors) { const primaryColor = Color(siteConfig.colors.primaryColor); @@ -732,11 +699,9 @@ function migrateLatestDocs( const content = String(fs.readFileSync(file)); fs.writeFileSync(file, sanitizedFileContent(content, migrateMDFiles)); }); - console.log(chalk.green(`Successfully migrated docs to version 2`)); + logger.success('Migrated docs to version 2'); } else { - console.log( - chalk.yellow(`Docs folder not found. Skipping migration for docs`), - ); + logger.warn('Docs folder not found. Skipping migration for docs'); } } @@ -774,7 +739,7 @@ function migratePackageFile( path.join(newDir, 'package.json'), JSON.stringify(packageFile, null, 2), ); - console.log(chalk.green(`Successfully migrated package.json file`)); + logger.success('Migrated package.json file'); } export async function migrateMDToMDX( @@ -790,5 +755,5 @@ export async function migrateMDToMDX( sanitizedFileContent(String(fs.readFileSync(file)), true), ); }); - console.log(`Successfully migrated ${siteDir} to ${newDir}`); + logger.success`Successfully migrated path=${siteDir} to path=${newDir}`; } diff --git a/packages/docusaurus-plugin-client-redirects/package.json b/packages/docusaurus-plugin-client-redirects/package.json index b80303d8af..1ae51f1c96 100644 --- a/packages/docusaurus-plugin-client-redirects/package.json +++ b/packages/docusaurus-plugin-client-redirects/package.json @@ -19,6 +19,7 @@ "license": "MIT", "dependencies": { "@docusaurus/core": "2.0.0-beta.13", + "@docusaurus/logger": "2.0.0-beta.13", "@docusaurus/utils": "2.0.0-beta.13", "@docusaurus/utils-common": "2.0.0-beta.13", "@docusaurus/utils-validation": "2.0.0-beta.13", diff --git a/packages/docusaurus-plugin-client-redirects/src/collectRedirects.ts b/packages/docusaurus-plugin-client-redirects/src/collectRedirects.ts index 327d608bd9..e650705d51 100644 --- a/packages/docusaurus-plugin-client-redirects/src/collectRedirects.ts +++ b/packages/docusaurus-plugin-client-redirects/src/collectRedirects.ts @@ -22,7 +22,7 @@ import { ApplyTrailingSlashParams, } from '@docusaurus/utils-common'; -import chalk from 'chalk'; +import logger from '@docusaurus/logger'; export default function collectRedirects( pluginContext: PluginContext, @@ -99,14 +99,10 @@ function filterUnwantedRedirects( Object.entries(groupBy(redirects, (redirect) => redirect.from)).forEach( ([from, groupedFromRedirects]) => { if (groupedFromRedirects.length > 1) { - console.error( - chalk.red( - `@docusaurus/plugin-client-redirects: multiple redirects are created with the same "from" pathname=${from} -It is not possible to redirect the same pathname to multiple destinations: -- ${groupedFromRedirects.map((r) => JSON.stringify(r)).join('\n- ')} -`, - ), - ); + logger.error`name=${'@docusaurus/plugin-client-redirects'}: multiple redirects are created with the same "from" pathname: path=${from} +It is not possible to redirect the same pathname to multiple destinations: ${groupedFromRedirects.map( + (r) => JSON.stringify(r), + )}`; } }, ); @@ -117,13 +113,9 @@ It is not possible to redirect the same pathname to multiple destinations: (redirect) => pluginContext.relativeRoutesPaths.includes(redirect.from), ); if (redirectsOverridingExistingPath.length > 0) { - console.error( - chalk.red( - `@docusaurus/plugin-client-redirects: some redirects would override existing paths, and will be ignored: -- ${redirectsOverridingExistingPath.map((r) => JSON.stringify(r)).join('\n- ')} -`, - ), - ); + logger.error`name=${'@docusaurus/plugin-client-redirects'}: some redirects would override existing paths, and will be ignored: ${redirectsOverridingExistingPath.map( + (r) => JSON.stringify(r), + )}`; } return collectedRedirects.filter( (redirect) => !pluginContext.relativeRoutesPaths.includes(redirect.from), diff --git a/packages/docusaurus-plugin-content-blog/package.json b/packages/docusaurus-plugin-content-blog/package.json index b8c4556d24..d0af144395 100644 --- a/packages/docusaurus-plugin-content-blog/package.json +++ b/packages/docusaurus-plugin-content-blog/package.json @@ -19,10 +19,10 @@ "license": "MIT", "dependencies": { "@docusaurus/core": "2.0.0-beta.13", + "@docusaurus/logger": "2.0.0-beta.13", "@docusaurus/mdx-loader": "2.0.0-beta.13", "@docusaurus/utils": "2.0.0-beta.13", "@docusaurus/utils-validation": "2.0.0-beta.13", - "chalk": "^4.1.2", "escape-string-regexp": "^4.0.0", "feed": "^4.2.2", "fs-extra": "^10.0.0", diff --git a/packages/docusaurus-plugin-content-blog/src/authors.ts b/packages/docusaurus-plugin-content-blog/src/authors.ts index ab77371e3d..dddd98f288 100644 --- a/packages/docusaurus-plugin-content-blog/src/authors.ts +++ b/packages/docusaurus-plugin-content-blog/src/authors.ts @@ -6,7 +6,7 @@ */ import fs from 'fs-extra'; -import chalk from 'chalk'; +import logger from '@docusaurus/logger'; import path from 'path'; import {Author, BlogContentPaths} from './types'; import {findFolderContainingFile} from '@docusaurus/utils'; @@ -48,7 +48,7 @@ export async function readAuthorsMapFile( return validateAuthorsMapFile(unsafeContent); } catch (e) { // TODO replace later by error cause: see https://v8.dev/features/error-cause - console.error(chalk.red('The author list file looks invalid!')); + logger.error('The author list file looks invalid!'); throw e; } } @@ -88,9 +88,7 @@ export async function getAuthorsMap( return await readAuthorsMapFile(filePath); } catch (e) { // TODO replace later by error cause, see https://v8.dev/features/error-cause - console.error( - chalk.red(`Couldn't read blog authors map at path ${filePath}`), - ); + logger.error`Couldn't read blog authors map at path=${filePath}`; throw e; } } diff --git a/packages/docusaurus-plugin-content-blog/src/blogUtils.ts b/packages/docusaurus-plugin-content-blog/src/blogUtils.ts index 167bc7bbce..fb32ddc9ab 100644 --- a/packages/docusaurus-plugin-content-blog/src/blogUtils.ts +++ b/packages/docusaurus-plugin-content-blog/src/blogUtils.ts @@ -6,7 +6,6 @@ */ import fs from 'fs-extra'; -import chalk from 'chalk'; import path from 'path'; import readingTime from 'reading-time'; import {keyBy, mapValues} from 'lodash'; @@ -33,6 +32,7 @@ import { import {LoadContext} from '@docusaurus/types'; import {validateBlogPostFrontMatter} from './blogFrontMatter'; import {AuthorsMap, getAuthorsMap, getBlogPostAuthors} from './authors'; +import logger from '@docusaurus/logger'; export function truncate(fileString: string, truncateMarker: RegExp): string { return fileString.split(truncateMarker, 1).shift()!; @@ -151,11 +151,7 @@ async function processBlogSourceFile( } if (frontMatter.id) { - console.warn( - chalk.yellow( - `"id" header option is deprecated in ${blogSourceRelative} file. Please use "slug" option instead.`, - ), - ); + logger.warn`name=${'id'} header option is deprecated in path=${blogSourceRelative} file. Please use name=${'slug'} option instead.`; } const parsedBlogFileName = parseBlogFileName(blogSourceRelative); @@ -276,11 +272,7 @@ export async function generateBlogPosts( authorsMap, ); } catch (e) { - console.error( - chalk.red( - `Processing of blog source file failed for path "${blogSourceFile}"`, - ), - ); + logger.error`Processing of blog source file failed for path path=${blogSourceFile}.`; throw e; } }), diff --git a/packages/docusaurus-plugin-content-docs/package.json b/packages/docusaurus-plugin-content-docs/package.json index dfc10ad90a..c3f3f105fa 100644 --- a/packages/docusaurus-plugin-content-docs/package.json +++ b/packages/docusaurus-plugin-content-docs/package.json @@ -19,10 +19,10 @@ "license": "MIT", "dependencies": { "@docusaurus/core": "2.0.0-beta.13", + "@docusaurus/logger": "2.0.0-beta.13", "@docusaurus/mdx-loader": "2.0.0-beta.13", "@docusaurus/utils": "2.0.0-beta.13", "@docusaurus/utils-validation": "2.0.0-beta.13", - "chalk": "^4.1.2", "combine-promises": "^1.1.0", "escape-string-regexp": "^4.0.0", "fs-extra": "^10.0.0", diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/cli.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/cli.test.ts index a96cdf4c18..eae4ea6d6b 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/cli.test.ts +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/cli.test.ts @@ -221,7 +221,11 @@ describe('docsVersion', () => { getVersionsFilePath(simpleSiteDir, DEFAULT_PLUGIN_ID), ); expect(versions).toEqual(['1.0.0']); - expect(consoleMock).toHaveBeenCalledWith('[docs]: version 1.0.0 created!'); + expect(consoleMock).toHaveBeenCalledWith( + expect.stringMatching( + /.*\[SUCCESS\].* .*\[docs\].*: version .*1\.0\.0.* created!.*/, + ), + ); copyMock.mockRestore(); writeMock.mockRestore(); @@ -274,7 +278,11 @@ describe('docsVersion', () => { getVersionsFilePath(versionedSiteDir, DEFAULT_PLUGIN_ID), ); expect(versions).toEqual(['2.0.0', '1.0.1', '1.0.0', 'withSlugs']); - expect(consoleMock).toHaveBeenCalledWith('[docs]: version 2.0.0 created!'); + expect(consoleMock).toHaveBeenCalledWith( + expect.stringMatching( + /.*\[SUCCESS\].* .*\[docs\].*: version .*2\.0\.0.* created!.*/, + ), + ); copyMock.mockRestore(); writeMock.mockRestore(); @@ -326,7 +334,9 @@ describe('docsVersion', () => { ); expect(versions).toEqual(['2.0.0', '1.0.0']); expect(consoleMock).toHaveBeenCalledWith( - '[community]: version 2.0.0 created!', + expect.stringMatching( + /.*\[SUCCESS\].* .*\[community\].*: version .*2.0.0.* created!.*/, + ), ); copyMock.mockRestore(); diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/lastUpdate.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/lastUpdate.test.ts index 36324d16c0..53ffc3a27f 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/lastUpdate.test.ts +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/lastUpdate.test.ts @@ -29,8 +29,7 @@ describe('lastUpdate', () => { }); test('non-existing file', async () => { - const consoleMock = jest.spyOn(console, 'error'); - consoleMock.mockImplementation(); + const consoleMock = jest.spyOn(console, 'error').mockImplementation(); const nonExistingFileName = '.nonExisting'; const nonExistingFilePath = path.join( __dirname, @@ -39,8 +38,8 @@ describe('lastUpdate', () => { ); expect(await getFileLastUpdate(nonExistingFilePath)).toBeNull(); expect(consoleMock).toHaveBeenCalledTimes(1); - expect(consoleMock.mock.calls[0][0].message).toContain( - ' with exit code 128', + expect(consoleMock).toHaveBeenLastCalledWith( + expect.stringMatching(/with exit code 128/), ); expect(await getFileLastUpdate(null)).toBeNull(); expect(await getFileLastUpdate(undefined)).toBeNull(); @@ -60,7 +59,9 @@ describe('lastUpdate', () => { const lastUpdateData = await getFileLastUpdate(existingFilePath); expect(lastUpdateData).toBeNull(); expect(consoleMock).toHaveBeenLastCalledWith( - 'Sorry, the docs plugin last update options require Git.', + expect.stringMatching( + /.*\[WARNING\].* Sorry, the docs plugin last update options require Git\..*/, + ), ); consoleMock.mockRestore(); diff --git a/packages/docusaurus-plugin-content-docs/src/cli.ts b/packages/docusaurus-plugin-content-docs/src/cli.ts index 633ee74a8d..3cc109b431 100644 --- a/packages/docusaurus-plugin-content-docs/src/cli.ts +++ b/packages/docusaurus-plugin-content-docs/src/cli.ts @@ -15,6 +15,7 @@ import path from 'path'; import type {PathOptions, SidebarOptions} from './types'; import {loadSidebarsFile, resolveSidebarPathOption} from './sidebars'; import {DEFAULT_PLUGIN_ID} from '@docusaurus/utils'; +import logger from '@docusaurus/logger'; function createVersionedSidebarFile({ siteDir, @@ -133,5 +134,5 @@ export function cliDocsVersionCommand( fs.ensureDirSync(path.dirname(versionsJSONFile)); fs.writeFileSync(versionsJSONFile, `${JSON.stringify(versions, null, 2)}\n`); - console.log(`${pluginIdLogPrefix}: version ${version} created!`); + logger.success`name=${pluginIdLogPrefix}: version name=${version} created!`; } diff --git a/packages/docusaurus-plugin-content-docs/src/docs.ts b/packages/docusaurus-plugin-content-docs/src/docs.ts index cab18d8471..43ab48ca59 100644 --- a/packages/docusaurus-plugin-content-docs/src/docs.ts +++ b/packages/docusaurus-plugin-content-docs/src/docs.ts @@ -7,7 +7,7 @@ import path from 'path'; import fs from 'fs-extra'; -import chalk from 'chalk'; +import logger from '@docusaurus/logger'; import {keyBy, last} from 'lodash'; import { aliasedSitePath, @@ -274,11 +274,7 @@ export function processDocMetadata(args: { try { return doProcessDocMetadata(args); } catch (e) { - console.error( - chalk.red( - `Can't process doc metadata for doc at path "${args.docFile.filePath}" in version "${args.versionMetadata.versionName}"`, - ), - ); + logger.error`Can't process doc metadata for doc at path path=${args.docFile.filePath} in version name=${args.versionMetadata.versionName}`; throw e; } } diff --git a/packages/docusaurus-plugin-content-docs/src/index.ts b/packages/docusaurus-plugin-content-docs/src/index.ts index d5f6734cdf..bb549d3e70 100644 --- a/packages/docusaurus-plugin-content-docs/src/index.ts +++ b/packages/docusaurus-plugin-content-docs/src/index.ts @@ -51,7 +51,7 @@ import { translateLoadedContent, getLoadedContentTranslationFiles, } from './translations'; -import chalk from 'chalk'; +import logger from '@docusaurus/logger'; import {getVersionTags} from './tags'; import {createVersionRoutes} from './routes'; import type {PropTagsListPage} from '@docusaurus/plugin-content-docs'; @@ -203,11 +203,7 @@ export default function pluginContentDocs( try { return await doLoadVersion(versionMetadata); } catch (e) { - console.error( - chalk.red( - `Loading of version failed for version "${versionMetadata.versionName}"`, - ), - ); + logger.error`Loading of version failed for version name=${versionMetadata.versionName}`; throw e; } } diff --git a/packages/docusaurus-plugin-content-docs/src/lastUpdate.ts b/packages/docusaurus-plugin-content-docs/src/lastUpdate.ts index c36d99efb1..c37e55bdb6 100644 --- a/packages/docusaurus-plugin-content-docs/src/lastUpdate.ts +++ b/packages/docusaurus-plugin-content-docs/src/lastUpdate.ts @@ -6,6 +6,7 @@ */ import shell from 'shelljs'; +import logger from '@docusaurus/logger'; type FileLastUpdateData = {timestamp?: number; author?: string}; @@ -36,7 +37,7 @@ export async function getFileLastUpdate( if (!shell.which('git')) { if (!showedGitRequirementError) { showedGitRequirementError = true; - console.warn('Sorry, the docs plugin last update options require Git.'); + logger.warn('Sorry, the docs plugin last update options require Git.'); } return null; @@ -51,8 +52,8 @@ export async function getFileLastUpdate( ); } return getTimestampAndAuthor(result.stdout.trim()); - } catch (error) { - console.error(error); + } catch (e) { + logger.error(e); } return null; diff --git a/packages/docusaurus-plugin-content-docs/src/options.ts b/packages/docusaurus-plugin-content-docs/src/options.ts index 8fd50adec0..7dd8f4342b 100644 --- a/packages/docusaurus-plugin-content-docs/src/options.ts +++ b/packages/docusaurus-plugin-content-docs/src/options.ts @@ -19,7 +19,7 @@ import type { OptionValidationContext, ValidationResult, } from '@docusaurus/types'; -import chalk from 'chalk'; +import logger from '@docusaurus/logger'; import admonitions from 'remark-admonitions'; import {DefaultSidebarItemsGenerator} from './sidebars/generator'; import { @@ -157,11 +157,7 @@ export function validateOptions({ }; } if (options.sidebarCollapsed) { - console.warn( - chalk.yellow( - 'The docs plugin config is inconsistent. It does not make sense to use sidebarCollapsible=false and sidebarCollapsed=true at the same time. sidebarCollapsed=false will be ignored.', - ), - ); + logger.warn`The docs plugin config is inconsistent. It does not make sense to use code=${'sidebarCollapsible: false'} and code=${'sidebarCollapsed: true'} at the same time. code=${'sidebarCollapsed: true'} will be ignored.`; options = { ...options, sidebarCollapsed: false, diff --git a/packages/docusaurus-plugin-content-docs/src/routes.ts b/packages/docusaurus-plugin-content-docs/src/routes.ts index c4f35b87c0..28bfdff372 100644 --- a/packages/docusaurus-plugin-content-docs/src/routes.ts +++ b/packages/docusaurus-plugin-content-docs/src/routes.ts @@ -14,7 +14,7 @@ import { } from './types'; import type {PropCategoryGeneratedIndex} from '@docusaurus/plugin-content-docs'; import {toVersionMetadataProp} from './props'; -import chalk from 'chalk'; +import logger from '@docusaurus/logger'; export async function createCategoryGeneratedIndexRoutes({ version, @@ -163,11 +163,7 @@ export async function createVersionRoutes({ try { return await doCreateVersionRoutes(loadedVersion); } catch (e) { - console.error( - chalk.red( - `Can't create version routes for version "${loadedVersion.versionName}"`, - ), - ); + logger.error`Can't create version routes for version name=${loadedVersion.versionName}`; throw e; } } diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/generator.test.ts b/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/generator.test.ts index 2c6fea0f66..c1fae21550 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/generator.test.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/generator.test.ts @@ -57,7 +57,7 @@ describe('DefaultSidebarItemsGenerator', () => { expect(sidebarSlice).toEqual([]); expect(consoleWarn).toHaveBeenCalledWith( expect.stringMatching( - /No docs found in dir .: can't auto-generate a sidebar/, + /.*\[WARNING\].* No docs found in .*\..*: can't auto-generate a sidebar\..*/, ), ); }); diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/generator.ts b/packages/docusaurus-plugin-content-docs/src/sidebars/generator.ts index 061a93af35..e06e1d752b 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/generator.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/generator.ts @@ -16,7 +16,7 @@ import type { } from './types'; import {sortBy, last} from 'lodash'; import {addTrailingSlash, posixPath} from '@docusaurus/utils'; -import chalk from 'chalk'; +import logger from '@docusaurus/logger'; import path from 'path'; import fs from 'fs-extra'; import Yaml from 'js-yaml'; @@ -72,11 +72,7 @@ async function readCategoryMetadataFile( try { return validateCategoryMetadataFile(unsafeContent); } catch (e) { - console.error( - chalk.red( - `The docs sidebar category metadata file looks invalid!\nPath: ${filePath}`, - ), - ); + logger.error`The docs sidebar category metadata file path=${filePath} looks invalid!`; throw e; } } @@ -134,11 +130,7 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({ const docs = allDocs.filter(isInAutogeneratedDir); if (docs.length === 0) { - console.warn( - chalk.yellow( - `No docs found in dir ${autogenDir}: can't auto-generate a sidebar.`, - ), - ); + logger.warn`No docs found in path=${autogenDir}: can't auto-generate a sidebar.`; } return docs; } diff --git a/packages/docusaurus-theme-classic/package.json b/packages/docusaurus-theme-classic/package.json index 23877e758f..5a0b364a5f 100644 --- a/packages/docusaurus-theme-classic/package.json +++ b/packages/docusaurus-theme-classic/package.json @@ -33,7 +33,6 @@ "@docusaurus/utils-validation": "2.0.0-beta.13", "@mdx-js/mdx": "^1.6.21", "@mdx-js/react": "^1.6.21", - "chalk": "^4.1.2", "clsx": "^1.1.1", "copy-text-to-clipboard": "^3.0.1", "globby": "^11.0.2", diff --git a/packages/docusaurus-theme-search-algolia/package.json b/packages/docusaurus-theme-search-algolia/package.json index 71ccc1d589..7a1ac95629 100644 --- a/packages/docusaurus-theme-search-algolia/package.json +++ b/packages/docusaurus-theme-search-algolia/package.json @@ -22,6 +22,7 @@ "dependencies": { "@docsearch/react": "^3.0.0-alpha.39", "@docusaurus/core": "2.0.0-beta.13", + "@docusaurus/logger": "2.0.0-beta.13", "@docusaurus/theme-common": "2.0.0-beta.13", "@docusaurus/theme-translations": "2.0.0-beta.13", "@docusaurus/utils": "2.0.0-beta.13", diff --git a/packages/docusaurus-theme-search-algolia/src/index.ts b/packages/docusaurus-theme-search-algolia/src/index.ts index ad1e5a49c0..0b1be918ff 100644 --- a/packages/docusaurus-theme-search-algolia/src/index.ts +++ b/packages/docusaurus-theme-search-algolia/src/index.ts @@ -10,6 +10,7 @@ import fs from 'fs'; import {defaultConfig, compile} from 'eta'; import {normalizeUrl, getSwizzledComponent} from '@docusaurus/utils'; import {readDefaultCodeTranslationMessages} from '@docusaurus/theme-translations'; +import logger from '@docusaurus/logger'; import openSearchTemplate from './templates/opensearch'; import {memoize} from 'lodash'; @@ -83,9 +84,9 @@ export default function theme( favicon: favicon ? normalizeUrl([url, baseUrl, favicon]) : null, }), ); - } catch (err) { - console.error(err); - throw new Error(`Generating OpenSearch file failed: ${err}`); + } catch (e) { + logger.error('Generating OpenSearch file failed.'); + throw e; } }, diff --git a/packages/docusaurus-theme-translations/package.json b/packages/docusaurus-theme-translations/package.json index 53df98becd..063def6b05 100644 --- a/packages/docusaurus-theme-translations/package.json +++ b/packages/docusaurus-theme-translations/package.json @@ -23,7 +23,8 @@ "tslib": "^2.3.1" }, "devDependencies": { - "chalk": "^4.1.2", + "@docusaurus/core": "2.0.0-beta.13", + "@docusaurus/logger": "2.0.0-beta.13", "lodash": "^4.17.20" }, "engines": { diff --git a/packages/docusaurus-theme-translations/update.js b/packages/docusaurus-theme-translations/update.js index c17716d2ec..215d0ea27d 100644 --- a/packages/docusaurus-theme-translations/update.js +++ b/packages/docusaurus-theme-translations/update.js @@ -5,9 +5,10 @@ * LICENSE file in the root directory of this source tree. */ +// @ts-check /* eslint-disable import/no-extraneous-dependencies */ -const chalk = require('chalk'); +const logger = require('@docusaurus/logger').default; const path = require('path'); const fs = require('fs-extra'); const {mapValues, pickBy, difference, orderBy} = require('lodash'); @@ -69,17 +70,10 @@ function sortObjectKeys(obj) { }, {}); } -function logSection(title) { - console.log(``); - console.log(``); - console.log(`##############################`); - console.log(`## ${chalk.blue(title)}`); -} - -function logKeys(keys) { - return `Keys:\n- ${keys.join('\n- ')}`; -} - +/** + * @param {string[]} targetDirs + * @returns {Promise} + */ async function extractThemeCodeMessages(targetDirs = AllThemesSrcDirs) { // Unsafe import, should we create a package for the translationsExtractor ? const { @@ -122,7 +116,7 @@ ${warning} } async function readMessagesFile(filePath) { - return JSON.parse(await fs.readFile(filePath)); + return JSON.parse((await fs.readFile(filePath)).toString()); } async function writeMessagesFile(filePath, messages) { @@ -130,11 +124,11 @@ async function writeMessagesFile(filePath, messages) { const content = `${JSON.stringify(sortedMessages, null, 2)}\n`; // \n makes prettier happy await fs.outputFile(filePath, content); - console.log( - `${path.basename(filePath)} updated (${ - Object.keys(sortedMessages).length - } messages)`, - ); + logger.info`path=${path.basename( + filePath, + )} updated subdue=${logger.interpolate`(number=${ + Object.keys(sortedMessages).length + } messages)`}\n`; } async function getCodeTranslationFiles(themeName) { @@ -166,11 +160,8 @@ async function updateBaseFile(baseFile, targetDirs) { ); if (unknownMessages.length) { - console.log( - chalk.red(`Some messages exist in base locale but were not found by the code extractor! -They won't be removed automatically, so do the cleanup manually if necessary! -${logKeys(unknownMessages)}`), - ); + logger.error`Some messages exist in base locale but were not found by the code extractor! +They won't be removed automatically, so do the cleanup manually if necessary! code=${unknownMessages}`; } const newBaseMessages = { @@ -210,11 +201,8 @@ async function updateLocaleCodeTranslations(localeFile, baseFileMessages) { ); if (unknownMessages.length) { - console.log( - chalk.red(`Some localized messages do not exist in base.json! -You may want to delete these! -${logKeys(unknownMessages)}`), - ); + logger.error`Some localized messages do not exist in base.json! +You may want to delete these! code=${unknownMessages}`; } const newLocaleFileMessages = { @@ -227,10 +215,7 @@ ${logKeys(unknownMessages)}`), .map(([key]) => key); if (untranslatedKeys.length) { - console.warn( - chalk.yellow(`Some messages do not seem to be translated! -${logKeys(untranslatedKeys)}`), - ); + logger.warn`Some messages do not seem to be translated! code=${untranslatedKeys}`; } await writeMessagesFile(localeFile, newLocaleFileMessages); @@ -241,7 +226,7 @@ async function updateCodeTranslations() { // eslint-disable-next-line no-restricted-syntax for (const theme of Themes) { const {baseFile, localesFiles} = await getCodeTranslationFiles(theme.name); - logSection(`Will update base file for ${theme.name}`); + logger.info`Will update base file for name=${theme.name}\n`; const baseFileMessages = await updateBaseFile(baseFile, theme.src); const [, newLocale] = process.argv; @@ -250,26 +235,23 @@ async function updateCodeTranslations() { if (!fs.existsSync(newLocalePath)) { await writeMessagesFile(newLocalePath, baseFileMessages); - console.error( - chalk.green( - `Locale file ${path.basename(newLocalePath)} have been created.`, - ), - ); + logger.success`Locale file path=${path.basename( + newLocalePath, + )} have been created.`; } else { - console.error( - chalk.red( - `Locale file ${path.basename(newLocalePath)} was already created!`, - ), - ); + logger.warn`Locale file path=${path.basename( + newLocalePath, + )} was already created!`; } } else { // eslint-disable-next-line no-restricted-syntax for (const localeFile of localesFiles) { - logSection( - `Will update ${path.basename( - path.dirname(localeFile), - )} locale in ${path.basename(localeFile, path.extname(localeFile))}`, - ); + logger.info`Will update name=${path.basename( + path.dirname(localeFile), + )} locale in name=${path.basename( + localeFile, + path.extname(localeFile), + )}`; await updateLocaleCodeTranslations(localeFile, baseFileMessages); } @@ -280,16 +262,12 @@ async function updateCodeTranslations() { function run() { updateCodeTranslations().then( () => { - console.log(''); - console.log(chalk.green('updateCodeTranslations end')); - console.log(''); + logger.success('updateCodeTranslations end\n'); }, (e) => { - console.log(''); - console.error(chalk.red(`updateCodeTranslations failure: ${e.message}`)); - console.log(''); - console.error(e.stack); - console.log(''); + logger.error( + `\nupdateCodeTranslations failure: ${e.message}\n${e.stack}\n`, + ); process.exit(1); }, ); diff --git a/packages/docusaurus-utils-validation/package.json b/packages/docusaurus-utils-validation/package.json index 380cf49974..160b491749 100644 --- a/packages/docusaurus-utils-validation/package.json +++ b/packages/docusaurus-utils-validation/package.json @@ -18,8 +18,8 @@ }, "license": "MIT", "dependencies": { + "@docusaurus/logger": "2.0.0-beta.13", "@docusaurus/utils": "2.0.0-beta.13", - "chalk": "^4.1.2", "joi": "^17.4.2", "tslib": "^2.3.1" }, diff --git a/packages/docusaurus-utils-validation/src/validationUtils.ts b/packages/docusaurus-utils-validation/src/validationUtils.ts index b76f8fc726..0666a3a7a9 100644 --- a/packages/docusaurus-utils-validation/src/validationUtils.ts +++ b/packages/docusaurus-utils-validation/src/validationUtils.ts @@ -6,7 +6,7 @@ */ import Joi from './Joi'; -import chalk from 'chalk'; +import logger from '@docusaurus/logger'; import {PluginIdSchema} from './validationSchemas'; // TODO temporary escape hatch for alpha-60: to be removed soon @@ -19,21 +19,14 @@ export const isValidationDisabledEscapeHatch = process.env.DISABLE_DOCUSAURUS_VALIDATION === 'true'; if (isValidationDisabledEscapeHatch) { - console.error( - chalk.red( - 'You should avoid using DISABLE_DOCUSAURUS_VALIDATION escape hatch, this will be removed.', - ), - ); + logger.error`You should avoid using code=${'DISABLE_DOCUSAURUS_VALIDATION'} escape hatch, this will be removed.`; } export const logValidationBugReportHint = (): void => { - console.log( - `\n${chalk.red('A validation error occurred.')}${chalk.cyanBright( - '\nThe validation system was added recently to Docusaurus as an attempt to avoid user configuration errors.' + - '\nWe may have made some mistakes.' + - '\nIf you think your configuration is valid and should keep working, please open a bug report.', - )}\n`, - ); + logger.error('A validation error occurred.'); + logger.info(`The validation system was added recently to Docusaurus as an attempt to avoid user configuration errors. +We may have made some mistakes. +If you think your configuration is valid and should keep working, please open a bug report.`); }; export function printWarning(warning?: Joi.ValidationError): void { @@ -41,7 +34,7 @@ export function printWarning(warning?: Joi.ValidationError): void { const warningMessages = warning.details .map(({message}) => message) .join('\n'); - console.log(chalk.yellow(warningMessages)); + logger.warn(warningMessages); } } @@ -63,7 +56,7 @@ export function normalizePluginOptions( if (error) { logValidationBugReportHint(); if (isValidationDisabledEscapeHatch) { - console.error(error); + logger.error(error); return options as T; } else { throw error; @@ -91,7 +84,7 @@ export function normalizeThemeConfig( if (error) { logValidationBugReportHint(); if (isValidationDisabledEscapeHatch) { - console.error(error); + logger.error(error); return themeConfig as T; } else { throw error; @@ -116,19 +109,14 @@ export function validateFrontMatter( const frontMatterString = JSON.stringify(frontMatter, null, 2); const errorDetails = error.details; const invalidFields = errorDetails.map(({path}) => path).join(', '); - const errorMessages = errorDetails - .map(({message}) => ` - ${message}`) - .join('\n'); logValidationBugReportHint(); - console.error( - chalk.red( - `The following frontmatter:\n${chalk.yellow( - frontMatterString, - )}\ncontains invalid values for field(s): ${invalidFields}.\n${errorMessages}\n`, - ), - ); + logger.error`The following frontmatter: +${logger.yellow(frontMatterString)} +contains invalid values for field(s): ${logger.yellow(invalidFields)}. +${errorDetails.map(({message}) => message)} +`; throw error; } diff --git a/packages/docusaurus-utils/package.json b/packages/docusaurus-utils/package.json index f95ef9704f..04e22d753a 100644 --- a/packages/docusaurus-utils/package.json +++ b/packages/docusaurus-utils/package.json @@ -18,9 +18,9 @@ }, "license": "MIT", "dependencies": { + "@docusaurus/logger": "2.0.0-beta.13", "@mdx-js/runtime": "^1.6.22", "@svgr/webpack": "^6.0.0", - "chalk": "^4.1.2", "escape-string-regexp": "^4.0.0", "file-loader": "^6.2.0", "fs-extra": "^10.0.0", @@ -44,7 +44,8 @@ "@types/github-slugger": "^1.3.0", "@types/micromatch": "^4.0.2", "@types/react-dom": "^17.0.1", - "dedent": "^0.7.0" + "dedent": "^0.7.0", + "tslib": "^2.3.1" }, "peerDependencies": { "react": "*", diff --git a/packages/docusaurus-utils/src/index.ts b/packages/docusaurus-utils/src/index.ts index 45f216f648..14ec8d1b74 100644 --- a/packages/docusaurus-utils/src/index.ts +++ b/packages/docusaurus-utils/src/index.ts @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import chalk from 'chalk'; +import logger from '@docusaurus/logger'; import path from 'path'; import {createHash} from 'crypto'; import {camelCase, mapValues} from 'lodash'; @@ -369,13 +369,13 @@ export function reportMessage( case 'ignore': break; case 'log': - console.log(chalk.bold.blue('info ') + chalk.blue(message)); + logger.info(message); break; case 'warn': - console.warn(chalk.bold.yellow('warn ') + chalk.yellow(message)); + logger.warn(message); break; case 'error': - console.error(chalk.bold.red('error ') + chalk.red(message)); + logger.error(message); break; case 'throw': throw new Error(message); diff --git a/packages/docusaurus-utils/src/markdownParser.ts b/packages/docusaurus-utils/src/markdownParser.ts index 3649b123e8..a0577f2b84 100644 --- a/packages/docusaurus-utils/src/markdownParser.ts +++ b/packages/docusaurus-utils/src/markdownParser.ts @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import chalk from 'chalk'; +import logger from '@docusaurus/logger'; import fs from 'fs-extra'; import matter from 'gray-matter'; @@ -166,10 +166,8 @@ export function parseMarkdownString( excerpt, }; } catch (e) { - console.error( - chalk.red(`Error while parsing Markdown frontmatter. -This can happen if you use special characters in frontmatter values (try using double quotes around that value).`), - ); + logger.error(`Error while parsing Markdown frontmatter. +This can happen if you use special characters in frontmatter values (try using double quotes around that value).`); throw e; } } diff --git a/packages/docusaurus/bin/beforeCli.js b/packages/docusaurus/bin/beforeCli.js index 91a265a4c3..4c0cf28055 100644 --- a/packages/docusaurus/bin/beforeCli.js +++ b/packages/docusaurus/bin/beforeCli.js @@ -5,7 +5,9 @@ * LICENSE file in the root directory of this source tree. */ -const chalk = require('chalk'); +// @ts-check + +const logger = require('@docusaurus/logger').default; const fs = require('fs-extra'); const semver = require('semver'); const path = require('path'); @@ -53,7 +55,7 @@ try { } } catch (e) { // Do not stop cli if this fails, see https://github.com/facebook/docusaurus/issues/5400 - console.error(e); + logger.error(e); } // We don't want to display update message for canary releases @@ -74,6 +76,7 @@ if ( notifier.config.set('update', notifier.update); if (ignoreUpdate(notifier.update)) { + // @ts-expect-error: it works return; } @@ -91,6 +94,7 @@ if ( ? `yarn upgrade ${siteDocusaurusPackagesForUpdate}` : `npm i ${siteDocusaurusPackagesForUpdate}`; + /** @type {import('boxen').Options} */ const boxenOptions = { padding: 1, margin: 1, @@ -100,13 +104,12 @@ if ( }; const docusaurusUpdateMessage = boxen( - `Update available ${chalk.dim(`${notifier.update.current}`)}${chalk.reset( - ' → ', - )}${chalk.green( - `${notifier.update.latest}`, - )}\n\nTo upgrade Docusaurus packages with the latest version, run the following command:\n${chalk.cyan( - `${upgradeCommand}`, - )}`, + `Update available ${logger.dim( + `${notifier.update.current}`, + )} → ${logger.green(`${notifier.update.latest}`)} + +To upgrade Docusaurus packages with the latest version, run the following command: +${logger.code(upgradeCommand)}`, boxenOptions, ); @@ -115,11 +118,7 @@ if ( // notify user if node version needs to be updated if (!semver.satisfies(process.version, requiredVersion)) { - console.log( - chalk.red(`\nMinimum Node version not met :(`) + - chalk.yellow( - `\n\nYou are using Node ${process.version}. We require Node ${requiredVersion} or up!\n`, - ), - ); + logger.error('Minimum Node.js version not met :('); + logger.info`You are using Node.js number=${process.version}, Requirement: Node.js number=${requiredVersion}.`; process.exit(1); } diff --git a/packages/docusaurus/bin/docusaurus.js b/packages/docusaurus/bin/docusaurus.js index 279623514a..ffe7bf5837 100755 --- a/packages/docusaurus/bin/docusaurus.js +++ b/packages/docusaurus/bin/docusaurus.js @@ -6,7 +6,9 @@ * LICENSE file in the root directory of this source tree. */ -const chalk = require('chalk'); +// @ts-check + +const logger = require('@docusaurus/logger').default; const fs = require('fs'); const cli = require('commander'); const { @@ -219,8 +221,7 @@ cli cli.arguments('').action((cmd) => { cli.outputHelp(); - console.log(` ${chalk.red(`\n Unknown command ${chalk.yellow(cmd)}.`)}.`); - console.log(); + logger.error` Unknown command name=${cmd}.`; }); function isInternalCommand(command) { @@ -238,6 +239,7 @@ function isInternalCommand(command) { async function run() { if (!isInternalCommand(process.argv.slice(2)[0])) { + // @ts-expect-error: Hmmm await externalCommand(cli, resolveDir('.')); } @@ -251,6 +253,6 @@ async function run() { run(); process.on('unhandledRejection', (err) => { - console.error(chalk.red(err.stack)); + logger.error(err.stack); process.exit(1); }); diff --git a/packages/docusaurus/package.json b/packages/docusaurus/package.json index a38412ab4e..a4d27e44a8 100644 --- a/packages/docusaurus/package.json +++ b/packages/docusaurus/package.json @@ -42,6 +42,7 @@ "@babel/runtime-corejs3": "^7.16.3", "@babel/traverse": "^7.16.3", "@docusaurus/cssnano-preset": "2.0.0-beta.13", + "@docusaurus/logger": "2.0.0-beta.13", "@docusaurus/mdx-loader": "2.0.0-beta.13", "@docusaurus/react-loadable": "5.5.2", "@docusaurus/utils": "2.0.0-beta.13", @@ -53,7 +54,6 @@ "babel-loader": "^8.2.2", "babel-plugin-dynamic-import-node": "2.3.0", "boxen": "^5.0.1", - "chalk": "^4.1.2", "chokidar": "^3.5.2", "clean-css": "^5.1.5", "commander": "^5.1.0", diff --git a/packages/docusaurus/src/choosePort.ts b/packages/docusaurus/src/choosePort.ts index 7e430d9909..b18f875517 100644 --- a/packages/docusaurus/src/choosePort.ts +++ b/packages/docusaurus/src/choosePort.ts @@ -13,7 +13,7 @@ import {execSync} from 'child_process'; import detect from 'detect-port'; import isRoot from 'is-root'; -import chalk from 'chalk'; +import logger from '@docusaurus/logger'; import prompts from 'prompts'; const isInteractive = process.stdout.isTTY; @@ -68,12 +68,7 @@ function getProcessForPort(port: number): string | null { const processId = getProcessIdOnPort(port); const directory = getDirectoryOfProcessById(processId); const command = getProcessCommand(processId); - return ( - chalk.cyan(command) + - chalk.grey(` (pid ${processId})\n`) + - chalk.blue(' in ') + - chalk.cyan(directory) - ); + return logger.interpolate`code=${command} subdue=${`(pid ${processId})`} in path=${directory}`; } catch (e) { return null; } @@ -104,11 +99,11 @@ export default async function choosePort( const question: prompts.PromptObject = { type: 'confirm', name: 'shouldChangePort', - message: `${chalk.yellow( - `${message}${ - existingProcess ? ` Probably:\n ${existingProcess}` : '' - }`, - )}\n\nWould you like to run the app on another port instead?`, + message: logger.yellow(`${logger.bold('[WARNING]')} ${message}${ + existingProcess ? ` Probably:\n ${existingProcess}` : '' + } + +Would you like to run the app on another port instead?`), initial: true, }; prompts(question).then((answer) => { @@ -119,15 +114,14 @@ export default async function choosePort( } }); } else { - console.log(chalk.red(message)); + logger.error(message); resolve(null); } }), (err) => { throw new Error( - `${chalk.red(`Could not find an open port at ${chalk.bold(host)}.`)}\n${ - `Network error message: "${err.message}".` || err - }\n`, + `Could not find an open port at ${host}. +${`Network error message: "${err.message || err}".`}`, ); }, ); diff --git a/packages/docusaurus/src/client/serverEntry.js b/packages/docusaurus/src/client/serverEntry.js index 6954b0250c..3f2d893323 100644 --- a/packages/docusaurus/src/client/serverEntry.js +++ b/packages/docusaurus/src/client/serverEntry.js @@ -5,6 +5,8 @@ * LICENSE file in the root directory of this source tree. */ +// @ts-check + import * as eta from 'eta'; import React from 'react'; import {StaticRouter} from 'react-router-dom'; @@ -24,7 +26,7 @@ import { createStatefulLinksCollector, ProvideLinksCollector, } from './LinksCollector'; -import chalk from 'chalk'; +import logger from '@docusaurus/logger'; // eslint-disable-next-line no-restricted-imports import {memoize} from 'lodash'; @@ -43,21 +45,16 @@ export default async function render(locals) { try { return await doRender(locals); } catch (e) { - console.error( - chalk.red( - `Docusaurus Node/SSR could not render static page with path "${locals.path}" because of following error:\n\n${e.stack}\n`, - ), - ); + logger.error`Docusaurus Node/SSR could not render static page with path path=${locals.path} because of following error: +${e.stack}`; const isNotDefinedErrorRegex = /(window|document|localStorage|navigator|alert|location|buffer|self) is not defined/i; if (isNotDefinedErrorRegex.test(e.message)) { - console.error( - chalk.green( - 'Pro tip: It looks like you are using code that should run on the client-side only.\nTo get around it, try using (https://docusaurus.io/docs/docusaurus-core/#browseronly) or ExecutionEnvironment (https://docusaurus.io/docs/docusaurus-core/#executionenvironment).\nIt might also require to wrap your client code in useEffect hook and/or import a third-party library dynamically (if any).', - ), - ); + logger.info`It looks like you are using code that should run on the client-side only. +To get around it, try using code=${''} (path=${'https://docusaurus.io/docs/docusaurus-core/#browseronly'}) or code=${'ExecutionEnvironment'} (path=${'https://docusaurus.io/docs/docusaurus-core/#executionenvironment'}). +It might also require to wrap your client code in code=${'useEffect'} hook and/or import a third-party library dynamically (if any).`; } throw new Error('Server-side rendering fails due to the error above.'); @@ -142,11 +139,8 @@ async function doRender(locals) { minifyJS: true, }); } catch (e) { - console.error( - chalk.red( - `Minification page with path "${locals.path}" failed because of following error:\n\n${e.stack}\n`, - ), - ); + logger.error`Minification of page path=${locals.path} failed because of following error: +${e.stack}`; throw e; } } diff --git a/packages/docusaurus/src/commands/build.ts b/packages/docusaurus/src/commands/build.ts index cd5830dc46..6604975f5e 100644 --- a/packages/docusaurus/src/commands/build.ts +++ b/packages/docusaurus/src/commands/build.ts @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import chalk from 'chalk'; +import logger from '@docusaurus/logger'; import CopyWebpackPlugin from 'copy-webpack-plugin'; import fs from 'fs-extra'; import path from 'path'; @@ -47,7 +47,6 @@ export default async function build( isLastLocale: boolean; }) { try { - // console.log(chalk.green(`Site successfully built in locale=${locale}`)); return await buildLocale({ siteDir, locale, @@ -56,7 +55,7 @@ export default async function build( isLastLocale, }); } catch (e) { - console.error(`Unable to build website for locale "${locale}".`); + logger.error`Unable to build website for locale name=${locale}.`; throw e; } } @@ -73,12 +72,7 @@ export default async function build( return tryToBuildLocale({locale: cliOptions.locale, isLastLocale: true}); } else { if (i18n.locales.length > 1) { - console.log( - chalk.yellow( - `\nWebsite will be built for all these locales: -- ${i18n.locales.join('\n- ')}`, - ), - ); + logger.info`Website will be built for all these locales: ${i18n.locales}`; } // We need the default locale to always be the 1st in the list @@ -112,9 +106,7 @@ async function buildLocale({ }): Promise { process.env.BABEL_ENV = 'production'; process.env.NODE_ENV = 'production'; - console.log( - chalk.blue(`\n[${locale}] Creating an optimized production build...`), - ); + logger.info`name=${`[${locale}]`} Creating an optimized production build...`; const props: Props = await load(siteDir, { customOutDir: cliOptions.outDir, @@ -238,18 +230,13 @@ async function buildLocale({ baseUrl, }); - console.log( - `${chalk.green(`Success!`)} Generated static files in "${chalk.cyan( - path.relative(process.cwd(), outDir), - )}".`, - ); + logger.success`Generated static files in path=${path.relative( + process.cwd(), + outDir, + )}.`; if (isLastLocale) { - console.log( - `\nUse ${chalk.greenBright( - '`npm run serve`', - )} command to test your build locally.\n`, - ); + logger.info`Use code=${'npm run serve'} command to test your build locally.`; } if (forceTerminate && isLastLocale && !cliOptions.bundleAnalyzer) { diff --git a/packages/docusaurus/src/commands/clear.ts b/packages/docusaurus/src/commands/clear.ts index bd9326e19a..327db8c767 100644 --- a/packages/docusaurus/src/commands/clear.ts +++ b/packages/docusaurus/src/commands/clear.ts @@ -7,22 +7,20 @@ import fs from 'fs-extra'; import path from 'path'; -import chalk from 'chalk'; +import logger from '@docusaurus/logger'; import { DEFAULT_BUILD_DIR_NAME, GENERATED_FILES_DIR_NAME, } from '@docusaurus/utils'; -function removePath(fsPath: string) { - return fs - .remove(path.join(fsPath)) - .then(() => { - console.log(chalk.green(`Successfully removed "${fsPath}" directory.`)); - }) - .catch((err) => { - console.error(`Could not remove ${fsPath} directory.`); - console.error(err); - }); +async function removePath(fsPath: string) { + try { + fs.remove(path.join(fsPath)); + logger.success`Removed the path=${fsPath} directory.`; + } catch (e) { + logger.error`Could not remove path=${fsPath} directory. +${e as string}`; + } } export default async function clear(siteDir: string): Promise { diff --git a/packages/docusaurus/src/commands/deploy.ts b/packages/docusaurus/src/commands/deploy.ts index e98f01bdec..70c1f5ecfd 100644 --- a/packages/docusaurus/src/commands/deploy.ts +++ b/packages/docusaurus/src/commands/deploy.ts @@ -7,7 +7,7 @@ import fs from 'fs-extra'; import shell from 'shelljs'; -import chalk from 'chalk'; +import logger from '@docusaurus/logger'; import {loadContext} from '../server'; import build from './build'; import {BuildCLIOptions} from '@docusaurus/types'; @@ -25,14 +25,10 @@ function obfuscateGitPass(str: string) { function shellExecLog(cmd: string) { try { const result = shell.exec(cmd); - console.log( - `${chalk.cyan('CMD:')} ${obfuscateGitPass(cmd)} ${chalk.cyan( - `(code: ${result.code})`, - )}`, - ); + logger.info`code=${obfuscateGitPass(cmd)} subdue=${`code: ${result.code}`}`; return result; } catch (e) { - console.log(`${chalk.red('CMD:')} ${obfuscateGitPass(cmd)}`); + logger.error`code=${obfuscateGitPass(cmd)}`; throw e; } } @@ -84,17 +80,13 @@ export default async function deploy( }); if (typeof siteConfig.trailingSlash === 'undefined') { - console.warn( - chalk.yellow(` -Docusaurus recommendation: -When deploying to GitHub Pages, it is better to use an explicit "trailingSlash" site config. + logger.warn(`When deploying to GitHub Pages, it is better to use an explicit "trailingSlash" site config. Otherwise, GitHub Pages will add an extra trailing slash to your site urls only on direct-access (not when navigation) with a server redirect. This behavior can have SEO impacts and create relative link issues. -`), - ); +`); } - console.log('Deploy command invoked...'); + logger.info('Deploy command invoked...'); if (!shell.which('git')) { throw new Error('Git not installed or on the PATH!'); } @@ -135,7 +127,7 @@ This behavior can have SEO impacts and create relative link issues. `Missing project organization name. Did you forget to define "organizationName" in ${siteConfigPath}? You may also export it via the ORGANIZATION_NAME environment variable.`, ); } - console.log(`${chalk.cyan('organizationName:')} ${organizationName}`); + logger.info`organizationName: name=${organizationName}`; const projectName = process.env.PROJECT_NAME || @@ -146,7 +138,7 @@ This behavior can have SEO impacts and create relative link issues. `Missing project name. Did you forget to define "projectName" in ${siteConfigPath}? You may also export it via the PROJECT_NAME environment variable.`, ); } - console.log(`${chalk.cyan('projectName:')} ${projectName}`); + logger.info`projectName: name=${projectName}`; // We never deploy on pull request. const isPullRequest = @@ -173,7 +165,7 @@ You can also set the deploymentBranch property in docusaurus.config.js .`); const deploymentBranch = process.env.DEPLOYMENT_BRANCH || siteConfig.deploymentBranch || 'gh-pages'; - console.log(`${chalk.cyan('deploymentBranch:')} ${deploymentBranch}`); + logger.info`deploymentBranch: name=${deploymentBranch}`; const githubHost = process.env.GITHUB_HOST || siteConfig.githubHost || 'github.com'; @@ -199,9 +191,7 @@ You can also set the deploymentBranch property in docusaurus.config.js .`); ); } - console.log( - `${chalk.cyan('Remote repo URL:')} ${obfuscateGitPass(deploymentRepoURL)}`, - ); + logger.info`Remote repo URL: name=${obfuscateGitPass(deploymentRepoURL)}`; // Check if this is a cross-repo publish. const crossRepoPublish = !sourceRepoUrl.endsWith( @@ -283,7 +273,7 @@ You can also set the deploymentBranch property in docusaurus.config.js .`); try { await runDeploy(await build(siteDir, cliOptions, false)); } catch (buildError) { - console.error(buildError); + logger.error((buildError as Error).message); process.exit(1); } } else { diff --git a/packages/docusaurus/src/commands/serve.ts b/packages/docusaurus/src/commands/serve.ts index 037b1899ac..f366c4a675 100644 --- a/packages/docusaurus/src/commands/serve.ts +++ b/packages/docusaurus/src/commands/serve.ts @@ -7,8 +7,7 @@ import http from 'http'; import serveHandler from 'serve-handler'; -import boxen from 'boxen'; -import chalk from 'chalk'; +import logger from '@docusaurus/logger'; import path from 'path'; import {loadSiteConfig} from '../server'; import build from './build'; @@ -71,18 +70,8 @@ export default async function serve( }); }); - console.log( - boxen( - chalk.green( - `Serving "${cliOptions.dir}" directory at "${servingUrl + baseUrl}".`, - ), - { - borderColor: 'green', - padding: 1, - margin: 1, - align: 'center', - }, - ), - ); + logger.success`Serving path=${cliOptions.dir} directory at path=${ + servingUrl + baseUrl + }.`; server.listen(port); } diff --git a/packages/docusaurus/src/commands/start.ts b/packages/docusaurus/src/commands/start.ts index ce6f003ab9..b7bc71fe7b 100644 --- a/packages/docusaurus/src/commands/start.ts +++ b/packages/docusaurus/src/commands/start.ts @@ -6,7 +6,7 @@ */ import {normalizeUrl, posixPath} from '@docusaurus/utils'; -import chalk = require('chalk'); +import logger from '@docusaurus/logger'; import chokidar from 'chokidar'; import HtmlWebpackPlugin from 'html-webpack-plugin'; import path from 'path'; @@ -34,7 +34,7 @@ export default async function start( ): Promise { process.env.NODE_ENV = 'development'; process.env.BABEL_ENV = 'development'; - console.log(chalk.blue('Starting the development server...')); + logger.info('Starting the development server...'); function loadSite() { return load(siteDir, { @@ -60,9 +60,7 @@ export default async function start( const urls = prepareUrls(protocol, host, port); const openUrl = normalizeUrl([urls.localUrlForBrowser, baseUrl]); - console.log( - chalk.cyanBright(`Docusaurus website is running at "${openUrl}".`), - ); + logger.success`Docusaurus website is running at path=${openUrl}.`; // Reload files processing. const reload = debounce(() => { @@ -70,15 +68,11 @@ export default async function start( .then(({baseUrl: newBaseUrl}) => { const newOpenUrl = normalizeUrl([urls.localUrlForBrowser, newBaseUrl]); if (newOpenUrl !== openUrl) { - console.log( - chalk.cyanBright( - `Docusaurus website is running at "${newOpenUrl}".`, - ), - ); + logger.success`Docusaurus website is running at path=${newOpenUrl}.`; } }) .catch((err) => { - console.error(chalk.red(err.stack)); + logger.error(err.stack); }); }, 500); const {siteConfig, plugins = []} = props; @@ -170,10 +164,10 @@ export default async function start( if (process.env.E2E_TEST) { compiler.hooks.done.tap('done', (stats) => { if (stats.hasErrors()) { - console.log('E2E_TEST: Project has compiler errors.'); + logger.error('E2E_TEST: Project has compiler errors.'); process.exit(1); } - console.log('E2E_TEST: Project can compile.'); + logger.success('E2E_TEST: Project can compile.'); process.exit(0); }); } diff --git a/packages/docusaurus/src/commands/swizzle.ts b/packages/docusaurus/src/commands/swizzle.ts index ef101d37c3..32c422de43 100644 --- a/packages/docusaurus/src/commands/swizzle.ts +++ b/packages/docusaurus/src/commands/swizzle.ts @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import chalk from 'chalk'; +import logger from '@docusaurus/logger'; import fs from 'fs-extra'; import importFresh from 'import-fresh'; import path from 'path'; @@ -100,23 +100,18 @@ function themeComponents( const components = colorCode(themePath, plugin); if (components.length === 0) { - return `${chalk.red('No component to swizzle.')}`; + return 'No component to swizzle.'; } - return ` -${chalk.cyan('Theme components available for swizzle.')} + return `Theme components available for swizzle. -${chalk.green('green =>')} safe: lower breaking change risk -${chalk.red('red =>')} unsafe: higher breaking change risk +${logger.green(logger.bold('green =>'))} safe: lower breaking change risk +${logger.red(logger.bold('red =>'))} unsafe: higher breaking change risk ${components.join('\n')} `; } -function formattedThemeNames(themeNames: string[]): string { - return `Themes available for swizzle:\n- ${themeNames.join('\n- ')}`; -} - function colorCode( themePath: string, plugin: ImportedPluginModule, @@ -135,8 +130,12 @@ function colorCode( ); return [ - ...greenComponents.map((component) => chalk.green(`safe: ${component}`)), - ...redComponents.map((component) => chalk.red(`unsafe: ${component}`)), + ...greenComponents.map( + (component) => `${logger.green(logger.bold('safe:'))} ${component}`, + ), + ...redComponents.map( + (component) => `${logger.red(logger.bold('unsafe:'))} ${component}`, + ), ]; } @@ -161,27 +160,25 @@ export default async function swizzle( ); if (!themeName) { - console.log(formattedThemeNames(themeNames)); - process.exit(1); + logger.info`Themes available for swizzle: name=${themeNames}`; + return; } let pluginModule: ImportedPluginModule; try { pluginModule = importFresh(themeName); } catch { - let suggestion; + let suggestion: string | undefined; themeNames.forEach((name) => { if (leven(name, themeName) < 4) { suggestion = name; } }); - chalk.red( - `Theme ${themeName} not found. ${ - suggestion - ? `Did you mean "${suggestion}" ?` - : formattedThemeNames(themeNames) - }`, - ); + logger.error`Theme name=${themeName} not found. ${ + suggestion + ? logger.interpolate`Did you mean name=${suggestion}?` + : logger.interpolate`Themes available for swizzle: ${themeNames}` + }`; process.exit(1); } @@ -218,19 +215,17 @@ export default async function swizzle( : pluginInstance.getThemePath?.(); if (!themePath) { - console.warn( - chalk.yellow( - typescript - ? `${themeName} does not provide TypeScript theme code via "getTypeScriptThemePath()".` - : `${themeName} does not provide any theme code.`, - ), + logger.warn( + typescript + ? logger.interpolate`name=${themeName} does not provide TypeScript theme code via ${'getTypeScriptThemePath()'}.` + : logger.interpolate`name=${themeName} does not provide any theme code.`, ); process.exit(1); } if (!componentName) { - console.warn(themeComponents(themePath, pluginModule)); - process.exit(1); + logger.info(themeComponents(themePath, pluginModule)); + return; } const components = getComponentName(themePath, pluginModule, Boolean(danger)); @@ -256,12 +251,8 @@ export default async function swizzle( if (mostSuitableMatch !== componentName) { mostSuitableComponent = mostSuitableMatch; - console.log( - chalk.red(`Component "${componentName}" doesn't exist.`), - chalk.yellow( - `"${mostSuitableComponent}" is swizzled instead of "${componentName}".`, - ), - ); + logger.error`Component name=${componentName} doesn't exist.`; + logger.info`name=${mostSuitableComponent} is swizzled instead of name=${componentName}.`; } } @@ -277,40 +268,29 @@ export default async function swizzle( } else if (fs.existsSync(`${fromPath}.js`)) { [fromPath, toPath] = [`${fromPath}.js`, `${toPath}.js`]; } else { - let suggestion; + let suggestion: string | undefined; components.forEach((name) => { if (leven(name, mostSuitableComponent) < 3) { suggestion = name; } }); - console.warn(chalk.red(`Component ${mostSuitableComponent} not found.`)); - console.warn( + logger.error`Component name=${mostSuitableComponent} not found. ${ suggestion - ? `Did you mean "${suggestion}"?` - : `${themeComponents(themePath, pluginModule)}`, - ); + ? logger.interpolate`Did you mean name=${suggestion} ?` + : themeComponents(themePath, pluginModule) + }`; process.exit(1); } } if (!components.includes(mostSuitableComponent) && !danger) { - console.warn( - chalk.red( - `${mostSuitableComponent} is an internal component and has a higher breaking change probability. If you want to swizzle it, use the "--danger" flag.`, - ), - ); + logger.error`name=${mostSuitableComponent} is an internal component and has a higher breaking change probability. If you want to swizzle it, use the code=${'--danger'} flag.`; process.exit(1); } await fs.copy(fromPath, toPath); - const relativeDir = path.relative(process.cwd(), toPath); - const fromMsg = chalk.blue( - mostSuitableComponent - ? `${themeName} ${chalk.yellow(mostSuitableComponent)}` - : themeName, - ); - const toMsg = chalk.cyan(relativeDir); - - console.log(`\n${chalk.green('Success!')} Copied ${fromMsg} to ${toMsg}.\n`); + logger.success`Copied code=${ + mostSuitableComponent ? `${themeName} ${mostSuitableComponent}` : themeName + } to path=${path.relative(process.cwd(), toPath)}.`; } diff --git a/packages/docusaurus/src/commands/writeHeadingIds.ts b/packages/docusaurus/src/commands/writeHeadingIds.ts index ff7b6d2557..20c6563d1a 100644 --- a/packages/docusaurus/src/commands/writeHeadingIds.ts +++ b/packages/docusaurus/src/commands/writeHeadingIds.ts @@ -6,7 +6,7 @@ */ import fs from 'fs-extra'; -import chalk from 'chalk'; +import logger from '@docusaurus/logger'; import {loadContext, loadPluginConfigs} from '../server'; import initPlugins from '../server/plugins/init'; @@ -150,21 +150,10 @@ export default async function writeHeadingIds( const pathsModified = result.filter(Boolean) as string[]; if (pathsModified.length) { - console.log( - chalk.green(`Heading ids added to Markdown files (${ - pathsModified.length - }/${markdownFiles.length} files): -- ${pathsModified.join('\n- ')}`), - ); + logger.success`Heading ids added to Markdown files (number=${`${pathsModified.length}/${markdownFiles.length}`} files): ${pathsModified}`; } else { - console.log( - chalk.yellow( - `${ - markdownFiles.length - } Markdown files already have explicit heading IDs. If you intend to overwrite the existing heading IDs, use the ${chalk.cyan( - '--overwrite', - )} option.`, - ), - ); + logger.warn`number=${ + markdownFiles.length + } Markdown files already have explicit heading IDs. If you intend to overwrite the existing heading IDs, use the code=${'--overwrite'} option.`; } } diff --git a/packages/docusaurus/src/server/__tests__/configValidation.test.ts b/packages/docusaurus/src/server/__tests__/configValidation.test.ts index 17973023f6..9a00ad3587 100644 --- a/packages/docusaurus/src/server/__tests__/configValidation.test.ts +++ b/packages/docusaurus/src/server/__tests__/configValidation.test.ts @@ -147,7 +147,7 @@ describe('normalizeConfig', () => { 'should accept [function, object] for plugin', [[function (_context, _options) {}, {it: 'should work'}]], ], - ])(`%s for the input of: %p`, (_message, plugins) => { + ])(`subdue= for the input of: path=`, (_message, plugins) => { expect(() => { normalizeConfig({ plugins, diff --git a/packages/docusaurus/src/server/__tests__/i18n.test.ts b/packages/docusaurus/src/server/__tests__/i18n.test.ts index 645435e4dc..64fd17e674 100644 --- a/packages/docusaurus/src/server/__tests__/i18n.test.ts +++ b/packages/docusaurus/src/server/__tests__/i18n.test.ts @@ -34,7 +34,6 @@ function loadI18nTest(i18nConfig: I18nConfig, locale?: string) { } describe('defaultLocaleConfig', () => { - // @ts-expect-error: wait for TS support of ES2021 feature const canComputeLabel = typeof Intl.DisplayNames !== 'undefined'; test('returns correct labels', () => { @@ -174,7 +173,7 @@ describe('loadI18n', () => { 'it', ); expect(consoleSpy.mock.calls[0][0]).toMatch( - /The locale "it" was not found in your site configuration/, + /The locale .*it.* was not found in your site configuration/, ); }); }); diff --git a/packages/docusaurus/src/server/configValidation.ts b/packages/docusaurus/src/server/configValidation.ts index 8c123acbb9..0763f47722 100644 --- a/packages/docusaurus/src/server/configValidation.ts +++ b/packages/docusaurus/src/server/configValidation.ts @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. */ +import logger from '@docusaurus/logger'; import {DocusaurusConfig, I18nConfig} from '@docusaurus/types'; import {DEFAULT_CONFIG_FILE_NAME, STATIC_DIR_NAME} from '@docusaurus/utils'; import { @@ -200,7 +201,7 @@ export function validateConfig( if (error) { logValidationBugReportHint(); if (isValidationDisabledEscapeHatch) { - console.error(error); + logger.error(error.message); return config as DocusaurusConfig; } diff --git a/packages/docusaurus/src/server/i18n.ts b/packages/docusaurus/src/server/i18n.ts index f86fe40038..5799d0a927 100644 --- a/packages/docusaurus/src/server/i18n.ts +++ b/packages/docusaurus/src/server/i18n.ts @@ -7,9 +7,9 @@ import {I18n, DocusaurusConfig, I18nLocaleConfig} from '@docusaurus/types'; import path from 'path'; -import {normalizeUrl, NODE_MAJOR_VERSION} from '@docusaurus/utils'; +import {normalizeUrl} from '@docusaurus/utils'; import {getLangDir} from 'rtl-detect'; -import chalk from 'chalk'; +import logger from '@docusaurus/logger'; function getDefaultLocaleLabel(locale: string) { // Intl.DisplayNames is ES2021 - Node14+ @@ -51,28 +51,14 @@ export async function loadI18n( const currentLocale = options.locale ?? i18nConfig.defaultLocale; if (!i18nConfig.locales.includes(currentLocale)) { - console.warn( - chalk.yellow( - `The locale "${currentLocale}" was not found in your site configuration: Available locales are: ${i18nConfig.locales.join( - ',', - )}. -Note: Docusaurus only support running one locale at a time.`, - ), - ); + logger.warn`The locale name=${currentLocale} was not found in your site configuration: Available locales are: ${i18nConfig.locales} +Note: Docusaurus only support running one locale at a time.`; } const locales = i18nConfig.locales.includes(currentLocale) ? i18nConfig.locales : (i18nConfig.locales.concat(currentLocale) as [string, ...string[]]); - if (shouldWarnAboutNodeVersion(NODE_MAJOR_VERSION, locales)) { - console.warn( - chalk.yellow( - `To use Docusaurus i18n, it is strongly advised to use Node.js 14 or later (instead of ${NODE_MAJOR_VERSION}).`, - ), - ); - } - function getLocaleConfig(locale: string): I18nLocaleConfig { return { ...getDefaultLocaleConfig(locale), diff --git a/packages/docusaurus/src/server/index.ts b/packages/docusaurus/src/server/index.ts index beee3c3134..c8bca3ddd8 100644 --- a/packages/docusaurus/src/server/index.ts +++ b/packages/docusaurus/src/server/index.ts @@ -11,8 +11,8 @@ import { DEFAULT_CONFIG_FILE_NAME, GENERATED_FILES_DIR_NAME, } from '@docusaurus/utils'; -import path, {join} from 'path'; -import chalk from 'chalk'; +import path from 'path'; +import logger from '@docusaurus/logger'; import ssrDefaultTemplate from '../client/templates/ssr.html.template'; import loadClientModules from './client-modules'; import loadConfig from './config'; @@ -385,9 +385,9 @@ ${Object.keys(registry) // Version metadata. const siteMetadata: DocusaurusSiteMetadata = { docusaurusVersion: getPackageJsonVersion( - join(__dirname, '../../package.json'), + path.join(__dirname, '../../package.json'), )!, - siteVersion: getPackageJsonVersion(join(siteDir, 'package.json')), + siteVersion: getPackageJsonVersion(path.join(siteDir, 'package.json')), pluginVersions: {}, }; plugins @@ -446,15 +446,14 @@ function checkDocusaurusPackagesVersion(siteMetadata: DocusaurusSiteMetadata) { if ( versionInfo.type === 'package' && versionInfo.name?.startsWith('@docusaurus/') && + versionInfo.version && versionInfo.version !== docusaurusVersion ) { // should we throw instead? // It still could work with different versions - console.warn( - chalk.red( - `Invalid ${plugin} version ${versionInfo.version}.\nAll official @docusaurus/* packages should have the exact same version as @docusaurus/core (${docusaurusVersion}).\nMaybe you want to check, or regenerate your yarn.lock or package-lock.json file?`, - ), - ); + logger.error`Invalid name=${plugin} version number=${versionInfo.version}. +All official @docusaurus/* packages should have the exact same version as @docusaurus/core (number=${docusaurusVersion}). +Maybe you want to check, or regenerate your yarn.lock or package-lock.json file?`; } }, ); diff --git a/packages/docusaurus/src/server/plugins/index.ts b/packages/docusaurus/src/server/plugins/index.ts index 4be498014f..beaa4dc9f8 100644 --- a/packages/docusaurus/src/server/plugins/index.ts +++ b/packages/docusaurus/src/server/plugins/index.ts @@ -20,7 +20,7 @@ import { InitializedPlugin, } from '@docusaurus/types'; import initPlugins from './init'; -import chalk from 'chalk'; +import logger from '@docusaurus/logger'; import {chain} from 'lodash'; import {localizePluginTranslationFile} from '../translations/translations'; import applyRouteTrailingSlash from './applyRouteTrailingSlash'; @@ -211,11 +211,7 @@ export async function loadPlugins({ // TODO remove this deprecated lifecycle soon // deprecated since alpha-60 // TODO, 1 user reported usage of this lifecycle! https://github.com/facebook/docusaurus/issues/3918 - console.error( - chalk.red( - 'Plugin routesLoaded lifecycle is deprecated. If you think we should keep this lifecycle, please report here: https://github.com/facebook/docusaurus/issues/3918', - ), - ); + logger.error`Plugin code=${'routesLoaded'} lifecycle is deprecated. If you think we should keep this lifecycle, please report here: path=${'https://github.com/facebook/docusaurus/issues/3918'}`; return plugin.routesLoaded(pluginsRouteConfigs); }), diff --git a/packages/docusaurus/src/server/translations/translations.ts b/packages/docusaurus/src/server/translations/translations.ts index 37ebec30a7..cd0d32c282 100644 --- a/packages/docusaurus/src/server/translations/translations.ts +++ b/packages/docusaurus/src/server/translations/translations.ts @@ -16,7 +16,7 @@ import { } from '@docusaurus/types'; import {getPluginI18nPath, toMessageRelativeFilePath} from '@docusaurus/utils'; import {Joi} from '@docusaurus/utils-validation'; -import chalk from 'chalk'; +import logger from '@docusaurus/logger'; export type WriteTranslationsOptions = { override?: boolean; @@ -115,11 +115,8 @@ export async function writeTranslationFileContent({ Object.keys(newContent), ); if (unknownKeys.length > 0) { - console.warn( - chalk.yellow(`Some translation keys looks unknown to us in file ${filePath} -Maybe you should remove them? -- ${unknownKeys.join('\n- ')}`), - ); + logger.warn`Some translation keys looks unknown to us in file path=${filePath}. +Maybe you should remove them? ${unknownKeys}`; } const mergedContent = mergeTranslationFileContent({ @@ -130,16 +127,11 @@ Maybe you should remove them? // Avoid creating empty translation files if (Object.keys(mergedContent).length > 0) { - console.log( - `${Object.keys(mergedContent) - .length.toString() - .padStart( - 3, - ' ', - )} translations will be written at "${toMessageRelativeFilePath( - filePath, - )}".`, - ); + logger.info`number=${ + Object.keys(mergedContent).length + } translations will be written at path=${toMessageRelativeFilePath( + filePath, + )}.`; await fs.ensureDir(path.dirname(filePath)); await fs.writeFile(filePath, JSON.stringify(mergedContent, null, 2)); } @@ -290,12 +282,8 @@ export function applyDefaultCodeTranslations({ Object.keys(extractedCodeTranslations), ); if (unusedDefaultCodeMessages.length > 0) { - console.warn( - chalk.yellow(`Unused default message codes found. -Please report this Docusaurus issue. -- ${unusedDefaultCodeMessages.join('\n- ')} -`), - ); + logger.warn`Unused default message codes found. +Please report this Docusaurus issue. name=${unusedDefaultCodeMessages}`; } return mapValues( diff --git a/packages/docusaurus/src/server/translations/translationsExtractor.ts b/packages/docusaurus/src/server/translations/translationsExtractor.ts index 6ea5efb1f4..749e9ec8f0 100644 --- a/packages/docusaurus/src/server/translations/translationsExtractor.ts +++ b/packages/docusaurus/src/server/translations/translationsExtractor.ts @@ -8,7 +8,7 @@ import fs from 'fs-extra'; import traverse, {Node} from '@babel/traverse'; import generate from '@babel/generator'; -import chalk from 'chalk'; +import logger from '@docusaurus/logger'; import {parse, types as t, NodePath, TransformOptions} from '@babel/core'; import { InitializedPlugin, @@ -115,11 +115,7 @@ function logSourceCodeFileTranslationsWarnings( ) { sourceCodeFilesTranslations.forEach(({sourceCodeFilePath, warnings}) => { if (warnings.length > 0) { - console.warn( - `Translation extraction warnings for file path=${sourceCodeFilePath}:\n- ${chalk.yellow( - warnings.join('\n\n- '), - )}`, - ); + logger.warn`Translation extraction warnings for file path=${sourceCodeFilePath}: ${warnings}`; } }); } @@ -302,7 +298,6 @@ function extractSourceCodeAstTranslations( return; } - // console.log('CallExpression', path.node); const args = path.get('arguments'); if (args.length === 1 || args.length === 2) { const firstArgPath = args[0]; @@ -310,8 +305,6 @@ function extractSourceCodeAstTranslations( // evaluation allows translate("x" + "y"); to be considered as translate("xy"); const firstArgEvaluated = firstArgPath.evaluate(); - // console.log('firstArgEvaluated', firstArgEvaluated); - if ( firstArgEvaluated.confident && typeof firstArgEvaluated.value === 'object' diff --git a/packages/docusaurus/src/webpack/client.ts b/packages/docusaurus/src/webpack/client.ts index 40fb507c22..6c19133680 100644 --- a/packages/docusaurus/src/webpack/client.ts +++ b/packages/docusaurus/src/webpack/client.ts @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import chalk from 'chalk'; +import logger from '@docusaurus/logger'; import path from 'path'; import {Configuration} from 'webpack'; import merge from 'webpack-merge'; @@ -45,12 +45,9 @@ export default function createClientConfig( apply: (compiler) => { compiler.hooks.done.tap('client:done', (stats) => { if (stats.hasErrors()) { - console.log( - chalk.red( - 'Client bundle compiled with errors therefore further build is impossible.', - ), + logger.error( + 'Client bundle compiled with errors therefore further build is impossible.', ); - process.exit(1); } }); diff --git a/packages/docusaurus/src/webpack/utils.ts b/packages/docusaurus/src/webpack/utils.ts index 5cf5fdb3e8..6ff4c9d1b5 100644 --- a/packages/docusaurus/src/webpack/utils.ts +++ b/packages/docusaurus/src/webpack/utils.ts @@ -21,7 +21,7 @@ import TerserPlugin from 'terser-webpack-plugin'; import CssMinimizerPlugin from 'css-minimizer-webpack-plugin'; import path from 'path'; import crypto from 'crypto'; -import chalk from 'chalk'; +import logger from '@docusaurus/logger'; import {TransformOptions} from '@babel/core'; import { ConfigureWebpackFn, @@ -161,11 +161,7 @@ export const getCustomizableJSLoader = // TODO remove this before end of 2021? const warnBabelLoaderOnce = memoize(() => { - console.warn( - chalk.yellow( - 'Docusaurus plans to support multiple JS loader strategies (Babel, esbuild...): "getBabelLoader(isServer)" is now deprecated in favor of "getJSLoader({isServer})".', - ), - ); + logger.warn`Docusaurus plans to support multiple JS loader strategies (Babel, esbuild...): code=${'getBabelLoader(isServer)'} is now deprecated in favor of code=${'getJSLoader(isServer)'}.`; }); const getBabelLoaderDeprecated = function getBabelLoaderDeprecated( isServer: boolean, @@ -177,11 +173,7 @@ const getBabelLoaderDeprecated = function getBabelLoaderDeprecated( // TODO remove this before end of 2021 ? const warnCacheLoaderOnce = memoize(() => { - console.warn( - chalk.yellow( - 'Docusaurus uses Webpack 5 and getCacheLoader() usage is now deprecated.', - ), - ); + logger.warn`Docusaurus uses Webpack 5 and code=${'getCacheLoader()'} usage is now deprecated.`; }); function getCacheLoaderDeprecated() { warnCacheLoaderOnce(); @@ -269,11 +261,11 @@ export function compile(config: Configuration[]): Promise { const compiler = webpack(config); compiler.run((err, stats) => { if (err) { - console.error(err.stack || err); + logger.error(err.stack || err); // @ts-expect-error: see https://webpack.js.org/api/node/#error-handling if (err.details) { // @ts-expect-error: see https://webpack.js.org/api/node/#error-handling - console.error(err.details); + logger.error(err.details); } reject(err); } @@ -284,16 +276,14 @@ export function compile(config: Configuration[]): Promise { } if (errorsWarnings && stats?.hasWarnings()) { errorsWarnings.warnings?.forEach((warning) => { - console.warn(warning); + logger.warn(`${warning}`); }); } // Webpack 5 requires calling close() so that persistent caching works // See https://github.com/webpack/webpack.js.org/pull/4775 compiler.close((errClose) => { if (errClose) { - console.error( - chalk.red('Error while closing Webpack compiler:', errClose), - ); + logger.error(`Error while closing Webpack compiler: ${errClose}`); reject(errClose); } else { resolve(); @@ -322,9 +312,8 @@ function validateKeyAndCerts({ encrypted = crypto.publicEncrypt(cert, Buffer.from('test')); } catch (err) { throw new Error( - `The certificate "${chalk.yellow(crtFile)}" is invalid.\n${ - (err as Error).message - }`, + `The certificate ${crtFile} is invalid. +${err}`, ); } @@ -333,9 +322,8 @@ function validateKeyAndCerts({ crypto.privateDecrypt(key, encrypted); } catch (err) { throw new Error( - `The certificate key "${chalk.yellow(keyFile)}" is invalid.\n${ - (err as Error).message - }`, + `The certificate key ${keyFile} is invalid. +${err}`, ); } } @@ -344,9 +332,7 @@ function validateKeyAndCerts({ function readEnvFile(file: string, type: string) { if (!fs.existsSync(file)) { throw new Error( - `You specified ${chalk.cyan( - type, - )} in your env, but the file "${chalk.yellow(file)}" can't be found.`, + `You specified ${type} in your env, but the file "${file}" can't be found.`, ); } return fs.readFileSync(file); diff --git a/packages/docusaurus/tsconfig.json b/packages/docusaurus/tsconfig.json index 105a516d42..af81dee451 100644 --- a/packages/docusaurus/tsconfig.json +++ b/packages/docusaurus/tsconfig.json @@ -2,7 +2,6 @@ "extends": "../../tsconfig.json", "compilerOptions": { "incremental": true, - "lib": ["DOM", "ES2019"], "tsBuildInfoFile": "./lib/.tsbuildinfo", "rootDir": "src", "outDir": "lib",