diff --git a/jest.config.mjs b/jest.config.mjs index 4c21dede96..2b0342501b 100644 --- a/jest.config.mjs +++ b/jest.config.mjs @@ -36,18 +36,18 @@ export default { // Jest can't resolve CSS or asset imports '^.+\\.(css|jpe?g|png|svg)$': '/jest/emptyModule.js', - // TODO we need to allow Jest to resolve core Webpack aliases automatically + // Using src instead of lib, so we always get fresh source '@docusaurus/(browserContext|BrowserOnly|ComponentCreator|constants|docusaurusContext|ExecutionEnvironment|Head|Interpolate|isInternalUrl|Link|Noop|renderRoutes|router|Translate|use.*)': - '@docusaurus/core/lib/client/exports/$1', + '@docusaurus/core/src/client/exports/$1', // Maybe point to a fixture? '@generated/.*': '/jest/emptyModule.js', // TODO use "projects" + multiple configs if we work on another theme? '@theme/(.*)': '@docusaurus/theme-classic/src/theme/$1', '@site/(.*)': 'website/$1', - // TODO why Jest can't figure node package entry points? + // Using src instead of lib, so we always get fresh source '@docusaurus/plugin-content-docs/client': - '@docusaurus/plugin-content-docs/lib/client/index.js', + '@docusaurus/plugin-content-docs/src/client/index.ts', }, globals: { window: { diff --git a/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__fixtures__/just-content.md b/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__fixtures__/just-content.md index 982f2df656..7f9152f5b8 100644 --- a/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__fixtures__/just-content.md +++ b/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__fixtures__/just-content.md @@ -13,3 +13,5 @@ Lorem ipsum Some content here ## I ♥ unicode. + +export const c = 1; diff --git a/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__snapshots__/index.test.ts.snap b/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__snapshots__/index.test.ts.snap index bb2f9b0723..fbd3fcc125 100644 --- a/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__snapshots__/index.test.ts.snap +++ b/packages/docusaurus-mdx-loader/src/remark/toc/__tests__/__snapshots__/index.test.ts.snap @@ -114,6 +114,8 @@ Lorem ipsum Some content here ## I ♥ unicode. + +export const c = 1; " `; @@ -241,5 +243,7 @@ Lorem ipsum Some content here ## I ♥ unicode. + +export const c = 1; " `; diff --git a/packages/docusaurus-mdx-loader/src/remark/utils/index.ts b/packages/docusaurus-mdx-loader/src/remark/utils/index.ts index e735eea28e..b0314d7a23 100644 --- a/packages/docusaurus-mdx-loader/src/remark/utils/index.ts +++ b/packages/docusaurus-mdx-loader/src/remark/utils/index.ts @@ -31,7 +31,6 @@ export function toValue(node: PhrasingContent | Heading): string { case 'link': return stringifyContent(node); default: + return toString(node); } - - return toString(node); } diff --git a/packages/docusaurus-plugin-client-redirects/src/normalizePluginOptions.ts b/packages/docusaurus-plugin-client-redirects/src/normalizePluginOptions.ts index f3d2cfb82a..b838746e1e 100644 --- a/packages/docusaurus-plugin-client-redirects/src/normalizePluginOptions.ts +++ b/packages/docusaurus-plugin-client-redirects/src/normalizePluginOptions.ts @@ -31,7 +31,7 @@ const RedirectPluginOptionValidation = Joi.object({ const isString = Joi.string().required().not(null); const UserOptionsSchema = Joi.object({ - id: Joi.string().optional(), // TODO remove once validation migrated to new system + id: Joi.string().optional(), // TODO remove once validation migrated to new system fromExtensions: Joi.array().items(isString), toExtensions: Joi.array().items(isString), redirects: Joi.array().items(RedirectPluginOptionValidation), diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/globalData.test.ts.snap b/packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/globalData.test.ts.snap new file mode 100644 index 0000000000..95ed78b3ff --- /dev/null +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/globalData.test.ts.snap @@ -0,0 +1,43 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`toGlobalDataVersion generates the right docs, sidebars, and metadata 1`] = ` +Object { + "docs": Array [ + Object { + "id": "main", + "path": "/current/main", + "sidebar": "tutorial", + }, + Object { + "id": "doc", + "path": "/current/doc", + "sidebar": "tutorial", + }, + Object { + "id": "/current/generated", + "path": "/current/generated", + "sidebar": "tutorial", + }, + ], + "isLast": true, + "label": "Label", + "mainDocId": "main", + "name": "current", + "path": "/current", + "sidebars": Object { + "another": Object { + "link": Object { + "label": "Generated", + "path": "/current/generated", + }, + }, + "links": Object {}, + "tutorial": Object { + "link": Object { + "label": "main", + "path": "/current/main", + }, + }, + }, +} +`; diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/globalData.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/globalData.test.ts new file mode 100644 index 0000000000..4c1cac6ecd --- /dev/null +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/globalData.test.ts @@ -0,0 +1,100 @@ +/** + * 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 {toGlobalDataVersion} from '../globalData'; + +describe('toGlobalDataVersion', () => { + it('generates the right docs, sidebars, and metadata', () => { + expect( + toGlobalDataVersion({ + versionName: 'current', + versionLabel: 'Label', + isLast: true, + versionPath: '/current', + mainDocId: 'main', + docs: [ + { + unversionedId: 'main', + permalink: '/current/main', + sidebar: 'tutorial', + }, + { + unversionedId: 'doc', + permalink: '/current/doc', + sidebar: 'tutorial', + }, + ], + sidebars: { + another: [ + { + type: 'category', + label: 'Generated', + link: { + type: 'generated-index', + permalink: '/current/generated', + }, + items: [ + { + type: 'doc', + id: 'doc', + }, + ], + }, + ], + tutorial: [ + { + type: 'doc', + id: 'main', + }, + { + type: 'category', + label: 'Generated', + link: { + type: 'generated-index', + permalink: '/current/generated', + }, + items: [ + { + type: 'doc', + id: 'doc', + }, + ], + }, + ], + links: [ + { + type: 'link', + href: 'foo', + label: 'Foo', + }, + { + type: 'link', + href: 'bar', + label: 'Bar', + }, + ], + }, + categoryGeneratedIndices: [ + { + title: 'Generated', + slug: '/current/generated', + permalink: '/current/generated', + sidebar: 'tutorial', + }, + ], + versionBanner: 'unreleased', + versionBadge: true, + versionClassName: 'current-cls', + tagsPath: '/current/tags', + contentPath: '', + contentPathLocalized: '', + sidebarFilePath: '', + routePriority: 0.5, + }), + ).toMatchSnapshot(); + }); +}); diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/slug.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/slug.test.ts index f9f774b693..25ee077dad 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/slug.test.ts +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/slug.test.ts @@ -111,6 +111,27 @@ describe('getSlug', () => { ).toBe('/dir with spâce/hey $hello/my dôc'); }); + it('throws for invalid routes', () => { + expect(() => + getSlug({ + baseID: 'my dôc', + source: '@site/docs/dir with spâce/hey $hello/doc.md', + sourceDirName: '/dir with spâce/hey $hello', + frontMatterSlug: '//', + }), + ).toThrowErrorMatchingInlineSnapshot(` + "We couldn't compute a valid slug for document with ID \\"my dôc\\" in \\"/dir with spâce/hey $hello\\" directory. + The slug we computed looks invalid: //. + Maybe your slug front matter is incorrect or there are special characters in the file path? + By using front matter to set a custom slug, you should be able to fix this error: + + --- + slug: /my/customDocPath + --- + " + `); + }); + it('handles current dir', () => { expect( getSlug({baseID: 'doc', source: '@site/docs/doc.md', sourceDirName: '.'}), diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/versions.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/versions.test.ts index 9a4aacb873..9e5f2e4831 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/versions.test.ts +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/versions.test.ts @@ -26,8 +26,8 @@ const DefaultI18N: I18n = { localeConfigs: {}, }; -describe('version paths', () => { - it('getVersionsFilePath', () => { +describe('getVersionsFilePath', () => { + it('works', () => { expect(getVersionsFilePath('someSiteDir', DEFAULT_PLUGIN_ID)).toBe( `someSiteDir${path.sep}versions.json`, ); @@ -35,8 +35,10 @@ describe('version paths', () => { `otherSite${path.sep}dir${path.sep}pluginId_versions.json`, ); }); +}); - it('getVersionedDocsDirPath', () => { +describe('getVersionedDocsDirPath', () => { + it('works', () => { expect(getVersionedDocsDirPath('someSiteDir', DEFAULT_PLUGIN_ID)).toBe( `someSiteDir${path.sep}versioned_docs`, ); @@ -44,8 +46,10 @@ describe('version paths', () => { `otherSite${path.sep}dir${path.sep}pluginId_versioned_docs`, ); }); +}); - it('getVersionedSidebarsDirPath', () => { +describe('getVersionedSidebarsDirPath', () => { + it('works', () => { expect(getVersionedSidebarsDirPath('someSiteDir', DEFAULT_PLUGIN_ID)).toBe( `someSiteDir${path.sep}versioned_sidebars`, ); @@ -55,692 +59,722 @@ describe('version paths', () => { }); }); -describe('simple site', () => { - async function loadSite() { - const simpleSiteDir = path.resolve( - path.join(__dirname, '__fixtures__', 'simple-site'), - ); - const defaultOptions: PluginOptions = { - id: DEFAULT_PLUGIN_ID, - ...DEFAULT_OPTIONS, - }; - const defaultContext = { - siteDir: simpleSiteDir, - baseUrl: '/', - i18n: DefaultI18N, - }; +describe('readVersionsMetadata', () => { + describe('simple site', () => { + async function loadSite() { + const simpleSiteDir = path.resolve( + path.join(__dirname, '__fixtures__', 'simple-site'), + ); + const defaultOptions: PluginOptions = { + id: DEFAULT_PLUGIN_ID, + ...DEFAULT_OPTIONS, + }; + const defaultContext = { + siteDir: simpleSiteDir, + baseUrl: '/', + i18n: DefaultI18N, + }; - const vCurrent: VersionMetadata = { - contentPath: path.join(simpleSiteDir, 'docs'), - contentPathLocalized: path.join( - simpleSiteDir, - 'i18n/en/docusaurus-plugin-content-docs/current', - ), - isLast: true, - routePriority: -1, - sidebarFilePath: undefined, - tagsPath: '/docs/tags', - versionLabel: 'Next', - versionName: 'current', - versionPath: '/docs', - versionBanner: null, - versionBadge: false, - versionClassName: 'docs-version-current', - }; - return {simpleSiteDir, defaultOptions, defaultContext, vCurrent}; - } - - it('readVersionsMetadata simple site', async () => { - const {defaultOptions, defaultContext, vCurrent} = await loadSite(); - - const versionsMetadata = await readVersionsMetadata({ - options: defaultOptions, - context: defaultContext, - }); - - expect(versionsMetadata).toEqual([vCurrent]); - }); - - it('readVersionsMetadata simple site with base url', async () => { - const {defaultOptions, defaultContext, vCurrent} = await loadSite(); - - const versionsMetadata = await readVersionsMetadata({ - options: defaultOptions, - context: { - ...defaultContext, - baseUrl: '/myBaseUrl', - }, - }); - - expect(versionsMetadata).toEqual([ - { - ...vCurrent, - versionPath: '/myBaseUrl/docs', - tagsPath: '/myBaseUrl/docs/tags', - }, - ]); - }); - - it('readVersionsMetadata simple site with current version config', async () => { - const {defaultOptions, defaultContext, vCurrent} = await loadSite(); - - const versionsMetadata = await readVersionsMetadata({ - options: { - ...defaultOptions, - versions: { - current: { - label: 'current-label', - path: 'current-path', - }, - }, - }, - context: { - ...defaultContext, - baseUrl: '/myBaseUrl', - }, - }); - - expect(versionsMetadata).toEqual([ - { - ...vCurrent, - versionPath: '/myBaseUrl/docs/current-path', - versionLabel: 'current-label', - routePriority: undefined, + const vCurrent: VersionMetadata = { + contentPath: path.join(simpleSiteDir, 'docs'), + contentPathLocalized: path.join( + simpleSiteDir, + 'i18n/en/docusaurus-plugin-content-docs/current', + ), + isLast: true, + routePriority: -1, sidebarFilePath: undefined, - tagsPath: '/myBaseUrl/docs/current-path/tags', - versionEditUrl: undefined, - versionEditUrlLocalized: undefined, - }, - ]); - }); + tagsPath: '/docs/tags', + versionLabel: 'Next', + versionName: 'current', + versionPath: '/docs', + versionBanner: null, + versionBadge: false, + versionClassName: 'docs-version-current', + }; + return {simpleSiteDir, defaultOptions, defaultContext, vCurrent}; + } - it('readVersionsMetadata simple site with unknown lastVersion should throw', async () => { - const {defaultOptions, defaultContext} = await loadSite(); + it('works', async () => { + const {defaultOptions, defaultContext, vCurrent} = await loadSite(); - await expect( - readVersionsMetadata({ - options: {...defaultOptions, lastVersion: 'unknownVersionName'}, + const versionsMetadata = await readVersionsMetadata({ + options: defaultOptions, context: defaultContext, - }), - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Docs option lastVersion: unknownVersionName is invalid. Available version names are: current"`, - ); - }); + }); - it('readVersionsMetadata simple site with unknown version configurations should throw', async () => { - const {defaultOptions, defaultContext} = await loadSite(); + expect(versionsMetadata).toEqual([vCurrent]); + }); - await expect( - readVersionsMetadata({ + it('works with base url', async () => { + const {defaultOptions, defaultContext, vCurrent} = await loadSite(); + + const versionsMetadata = await readVersionsMetadata({ + options: defaultOptions, + context: { + ...defaultContext, + baseUrl: '/myBaseUrl', + }, + }); + + expect(versionsMetadata).toEqual([ + { + ...vCurrent, + versionPath: '/myBaseUrl/docs', + tagsPath: '/myBaseUrl/docs/tags', + }, + ]); + }); + + it('works with current version config', async () => { + const {defaultOptions, defaultContext, vCurrent} = await loadSite(); + + const versionsMetadata = await readVersionsMetadata({ options: { ...defaultOptions, versions: { - current: {label: 'current'}, - unknownVersionName1: {label: 'unknownVersionName1'}, - unknownVersionName2: {label: 'unknownVersionName2'}, + current: { + label: 'current-label', + path: 'current-path', + }, }, }, - context: defaultContext, - }), - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Invalid docs option \\"versions\\": unknown versions (unknownVersionName1,unknownVersionName2) found. Available version names are: current"`, - ); - }); - - it('readVersionsMetadata simple site with disableVersioning while single version should throw', async () => { - const {defaultOptions, defaultContext} = await loadSite(); - - await expect( - readVersionsMetadata({ - options: {...defaultOptions, disableVersioning: true}, - context: defaultContext, - }), - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Docs: using \\"disableVersioning: true\\" option on a non-versioned site does not make sense."`, - ); - }); - - it('readVersionsMetadata simple site without including current version should throw', async () => { - const {defaultOptions, defaultContext} = await loadSite(); - - await expect( - readVersionsMetadata({ - options: {...defaultOptions, includeCurrentVersion: false}, - context: defaultContext, - }), - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"It is not possible to use docs without any version. Please check the configuration of these options: \\"includeCurrentVersion: false\\", \\"disableVersioning: false\\"."`, - ); - }); -}); - -describe('versioned site, pluginId=default', () => { - async function loadSite() { - const versionedSiteDir = path.resolve( - path.join(__dirname, '__fixtures__', 'versioned-site'), - ); - const defaultOptions: PluginOptions = { - id: DEFAULT_PLUGIN_ID, - ...DEFAULT_OPTIONS, - sidebarPath: 'sidebars.json', - }; - const defaultContext = { - siteDir: versionedSiteDir, - baseUrl: '/', - i18n: DefaultI18N, - }; - - const vCurrent: VersionMetadata = { - contentPath: path.join(versionedSiteDir, 'docs'), - contentPathLocalized: path.join( - versionedSiteDir, - 'i18n/en/docusaurus-plugin-content-docs/current', - ), - isLast: false, - routePriority: undefined, - sidebarFilePath: path.join(versionedSiteDir, 'sidebars.json'), - tagsPath: '/docs/next/tags', - versionLabel: 'Next', - versionName: 'current', - versionPath: '/docs/next', - versionBanner: 'unreleased', - versionBadge: true, - versionClassName: 'docs-version-current', - }; - - const v101: VersionMetadata = { - contentPath: path.join(versionedSiteDir, 'versioned_docs/version-1.0.1'), - contentPathLocalized: path.join( - versionedSiteDir, - 'i18n/en/docusaurus-plugin-content-docs/version-1.0.1', - ), - isLast: true, - routePriority: -1, - sidebarFilePath: path.join( - versionedSiteDir, - 'versioned_sidebars/version-1.0.1-sidebars.json', - ), - tagsPath: '/docs/tags', - versionLabel: '1.0.1', - versionName: '1.0.1', - versionPath: '/docs', - versionBanner: null, - versionBadge: true, - versionClassName: 'docs-version-1.0.1', - }; - - const v100: VersionMetadata = { - contentPath: path.join(versionedSiteDir, 'versioned_docs/version-1.0.0'), - contentPathLocalized: path.join( - versionedSiteDir, - 'i18n/en/docusaurus-plugin-content-docs/version-1.0.0', - ), - isLast: false, - routePriority: undefined, - sidebarFilePath: path.join( - versionedSiteDir, - 'versioned_sidebars/version-1.0.0-sidebars.json', - ), - tagsPath: '/docs/1.0.0/tags', - versionLabel: '1.0.0', - versionName: '1.0.0', - versionPath: '/docs/1.0.0', - versionBanner: 'unmaintained', - versionBadge: true, - versionClassName: 'docs-version-1.0.0', - }; - - const vWithSlugs: VersionMetadata = { - contentPath: path.join( - versionedSiteDir, - 'versioned_docs/version-withSlugs', - ), - contentPathLocalized: path.join( - versionedSiteDir, - 'i18n/en/docusaurus-plugin-content-docs/version-withSlugs', - ), - isLast: false, - routePriority: undefined, - sidebarFilePath: path.join( - versionedSiteDir, - 'versioned_sidebars/version-withSlugs-sidebars.json', - ), - tagsPath: '/docs/withSlugs/tags', - versionLabel: 'withSlugs', - versionName: 'withSlugs', - versionPath: '/docs/withSlugs', - versionBanner: 'unmaintained', - versionBadge: true, - versionClassName: 'docs-version-withSlugs', - }; - - return { - versionedSiteDir, - defaultOptions, - defaultContext, - vCurrent, - v101, - v100, - vWithSlugs, - }; - } - - it('readVersionsMetadata versioned site', async () => { - const {defaultOptions, defaultContext, vCurrent, v101, v100, vWithSlugs} = - await loadSite(); - - const versionsMetadata = await readVersionsMetadata({ - options: defaultOptions, - context: defaultContext, - }); - - expect(versionsMetadata).toEqual([vCurrent, v101, v100, vWithSlugs]); - }); - - it('readVersionsMetadata versioned site with includeCurrentVersion=false', async () => { - const {defaultOptions, defaultContext, v101, v100, vWithSlugs} = - await loadSite(); - - const versionsMetadata = await readVersionsMetadata({ - options: {...defaultOptions, includeCurrentVersion: false}, - context: defaultContext, - }); - - expect(versionsMetadata).toEqual([ - // vCurrent removed - v101, - v100, - vWithSlugs, - ]); - }); - - it('readVersionsMetadata versioned site with version options', async () => { - const {defaultOptions, defaultContext, vCurrent, v101, v100, vWithSlugs} = - await loadSite(); - - const versionsMetadata = await readVersionsMetadata({ - options: { - ...defaultOptions, - lastVersion: '1.0.0', - versions: { - current: { - path: 'current-path', - banner: 'unmaintained', - badge: false, - className: 'custom-current-className', - }, - '1.0.0': { - label: '1.0.0-label', - banner: 'unreleased', - }, + context: { + ...defaultContext, + baseUrl: '/myBaseUrl', }, - }, - context: defaultContext, + }); + + expect(versionsMetadata).toEqual([ + { + ...vCurrent, + versionPath: '/myBaseUrl/docs/current-path', + versionLabel: 'current-label', + routePriority: undefined, + sidebarFilePath: undefined, + tagsPath: '/myBaseUrl/docs/current-path/tags', + versionEditUrl: undefined, + versionEditUrlLocalized: undefined, + }, + ]); }); - expect(versionsMetadata).toEqual([ - { - ...vCurrent, - tagsPath: '/docs/current-path/tags', - versionPath: '/docs/current-path', - versionBanner: 'unmaintained', - versionBadge: false, - versionClassName: 'custom-current-className', - }, - { - ...v101, + it('throws with unknown lastVersion', async () => { + const {defaultOptions, defaultContext} = await loadSite(); + + await expect( + readVersionsMetadata({ + options: {...defaultOptions, lastVersion: 'unknownVersionName'}, + context: defaultContext, + }), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Docs option lastVersion: unknownVersionName is invalid. Available version names are: current"`, + ); + }); + + it('throws with unknown version configurations', async () => { + const {defaultOptions, defaultContext} = await loadSite(); + + await expect( + readVersionsMetadata({ + options: { + ...defaultOptions, + versions: { + current: {label: 'current'}, + unknownVersionName1: {label: 'unknownVersionName1'}, + unknownVersionName2: {label: 'unknownVersionName2'}, + }, + }, + context: defaultContext, + }), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Invalid docs option \\"versions\\": unknown versions (unknownVersionName1,unknownVersionName2) found. Available version names are: current"`, + ); + }); + + it('throws with disableVersioning while single version', async () => { + const {defaultOptions, defaultContext} = await loadSite(); + + await expect( + readVersionsMetadata({ + options: {...defaultOptions, disableVersioning: true}, + context: defaultContext, + }), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Docs: using \\"disableVersioning: true\\" option on a non-versioned site does not make sense."`, + ); + }); + + it('throws without including current version', async () => { + const {defaultOptions, defaultContext} = await loadSite(); + + await expect( + readVersionsMetadata({ + options: {...defaultOptions, includeCurrentVersion: false}, + context: defaultContext, + }), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"It is not possible to use docs without any version. Please check the configuration of these options: \\"includeCurrentVersion: false\\", \\"disableVersioning: false\\"."`, + ); + }); + }); + + describe('versioned site, pluginId=default', () => { + async function loadSite() { + const versionedSiteDir = path.resolve( + path.join(__dirname, '__fixtures__', 'versioned-site'), + ); + const defaultOptions: PluginOptions = { + id: DEFAULT_PLUGIN_ID, + ...DEFAULT_OPTIONS, + sidebarPath: 'sidebars.json', + }; + const defaultContext = { + siteDir: versionedSiteDir, + baseUrl: '/', + i18n: DefaultI18N, + }; + + const vCurrent: VersionMetadata = { + contentPath: path.join(versionedSiteDir, 'docs'), + contentPathLocalized: path.join( + versionedSiteDir, + 'i18n/en/docusaurus-plugin-content-docs/current', + ), isLast: false, routePriority: undefined, - tagsPath: '/docs/1.0.1/tags', - versionPath: '/docs/1.0.1', + sidebarFilePath: path.join(versionedSiteDir, 'sidebars.json'), + tagsPath: '/docs/next/tags', + versionLabel: 'Next', + versionName: 'current', + versionPath: '/docs/next', versionBanner: 'unreleased', - }, - { - ...v100, - isLast: true, - routePriority: -1, - tagsPath: '/docs/tags', - versionLabel: '1.0.0-label', - versionPath: '/docs', - versionBanner: 'unreleased', - }, - vWithSlugs, - ]); - }); - - it('readVersionsMetadata versioned site with editUrl', async () => { - const {defaultOptions, defaultContext, vCurrent, v101, v100, vWithSlugs} = - await loadSite(); - - const versionsMetadata = await readVersionsMetadata({ - options: { - ...defaultOptions, - editUrl: 'https://github.com/facebook/docusaurus/edit/main/website/', - }, - context: defaultContext, - }); - - expect(versionsMetadata).toEqual([ - { - ...vCurrent, - versionEditUrl: - 'https://github.com/facebook/docusaurus/edit/main/website/docs', - versionEditUrlLocalized: - 'https://github.com/facebook/docusaurus/edit/main/website/i18n/en/docusaurus-plugin-content-docs/current', - }, - { - ...v101, - versionEditUrl: - 'https://github.com/facebook/docusaurus/edit/main/website/versioned_docs/version-1.0.1', - versionEditUrlLocalized: - 'https://github.com/facebook/docusaurus/edit/main/website/i18n/en/docusaurus-plugin-content-docs/version-1.0.1', - }, - { - ...v100, - versionEditUrl: - 'https://github.com/facebook/docusaurus/edit/main/website/versioned_docs/version-1.0.0', - versionEditUrlLocalized: - 'https://github.com/facebook/docusaurus/edit/main/website/i18n/en/docusaurus-plugin-content-docs/version-1.0.0', - }, - { - ...vWithSlugs, - versionEditUrl: - 'https://github.com/facebook/docusaurus/edit/main/website/versioned_docs/version-withSlugs', - versionEditUrlLocalized: - 'https://github.com/facebook/docusaurus/edit/main/website/i18n/en/docusaurus-plugin-content-docs/version-withSlugs', - }, - ]); - }); - - it('readVersionsMetadata versioned site with editUrl and editCurrentVersion=true', async () => { - const {defaultOptions, defaultContext, vCurrent, v101, v100, vWithSlugs} = - await loadSite(); - - const versionsMetadata = await readVersionsMetadata({ - options: { - ...defaultOptions, - editUrl: 'https://github.com/facebook/docusaurus/edit/main/website/', - editCurrentVersion: true, - }, - context: defaultContext, - }); - - expect(versionsMetadata).toEqual([ - { - ...vCurrent, - versionEditUrl: - 'https://github.com/facebook/docusaurus/edit/main/website/docs', - versionEditUrlLocalized: - 'https://github.com/facebook/docusaurus/edit/main/website/i18n/en/docusaurus-plugin-content-docs/current', - }, - { - ...v101, - versionEditUrl: - 'https://github.com/facebook/docusaurus/edit/main/website/docs', - versionEditUrlLocalized: - 'https://github.com/facebook/docusaurus/edit/main/website/i18n/en/docusaurus-plugin-content-docs/current', - }, - { - ...v100, - versionEditUrl: - 'https://github.com/facebook/docusaurus/edit/main/website/docs', - versionEditUrlLocalized: - 'https://github.com/facebook/docusaurus/edit/main/website/i18n/en/docusaurus-plugin-content-docs/current', - }, - { - ...vWithSlugs, - versionEditUrl: - 'https://github.com/facebook/docusaurus/edit/main/website/docs', - versionEditUrlLocalized: - 'https://github.com/facebook/docusaurus/edit/main/website/i18n/en/docusaurus-plugin-content-docs/current', - }, - ]); - }); - - it('readVersionsMetadata versioned site with onlyIncludeVersions option', async () => { - const {defaultOptions, defaultContext, v101, vWithSlugs} = await loadSite(); - - const versionsMetadata = await readVersionsMetadata({ - options: { - ...defaultOptions, - // Order reversed on purpose: should not have any impact - onlyIncludeVersions: [vWithSlugs.versionName, v101.versionName], - }, - context: defaultContext, - }); - - expect(versionsMetadata).toEqual([v101, vWithSlugs]); - }); - - it('readVersionsMetadata versioned site with disableVersioning', async () => { - const {defaultOptions, defaultContext, vCurrent} = await loadSite(); - - const versionsMetadata = await readVersionsMetadata({ - options: {...defaultOptions, disableVersioning: true}, - context: defaultContext, - }); - - expect(versionsMetadata).toEqual([ - { - ...vCurrent, + versionBadge: true, + versionClassName: 'docs-version-current', + }; + + const v101: VersionMetadata = { + contentPath: path.join( + versionedSiteDir, + 'versioned_docs/version-1.0.1', + ), + contentPathLocalized: path.join( + versionedSiteDir, + 'i18n/en/docusaurus-plugin-content-docs/version-1.0.1', + ), isLast: true, routePriority: -1, + sidebarFilePath: path.join( + versionedSiteDir, + 'versioned_sidebars/version-1.0.1-sidebars.json', + ), tagsPath: '/docs/tags', + versionLabel: '1.0.1', + versionName: '1.0.1', versionPath: '/docs', versionBanner: null, - versionBadge: false, - }, - ]); - }); + versionBadge: true, + versionClassName: 'docs-version-1.0.1', + }; - it('readVersionsMetadata versioned site with all versions disabled', async () => { - const {defaultOptions, defaultContext} = await loadSite(); + const v100: VersionMetadata = { + contentPath: path.join( + versionedSiteDir, + 'versioned_docs/version-1.0.0', + ), + contentPathLocalized: path.join( + versionedSiteDir, + 'i18n/en/docusaurus-plugin-content-docs/version-1.0.0', + ), + isLast: false, + routePriority: undefined, + sidebarFilePath: path.join( + versionedSiteDir, + 'versioned_sidebars/version-1.0.0-sidebars.json', + ), + tagsPath: '/docs/1.0.0/tags', + versionLabel: '1.0.0', + versionName: '1.0.0', + versionPath: '/docs/1.0.0', + versionBanner: 'unmaintained', + versionBadge: true, + versionClassName: 'docs-version-1.0.0', + }; - await expect( - readVersionsMetadata({ - options: { - ...defaultOptions, - includeCurrentVersion: false, - disableVersioning: true, - }, - context: defaultContext, - }), - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"It is not possible to use docs without any version. Please check the configuration of these options: \\"includeCurrentVersion: false\\", \\"disableVersioning: true\\"."`, - ); - }); + const vWithSlugs: VersionMetadata = { + contentPath: path.join( + versionedSiteDir, + 'versioned_docs/version-withSlugs', + ), + contentPathLocalized: path.join( + versionedSiteDir, + 'i18n/en/docusaurus-plugin-content-docs/version-withSlugs', + ), + isLast: false, + routePriority: undefined, + sidebarFilePath: path.join( + versionedSiteDir, + 'versioned_sidebars/version-withSlugs-sidebars.json', + ), + tagsPath: '/docs/withSlugs/tags', + versionLabel: 'withSlugs', + versionName: 'withSlugs', + versionPath: '/docs/withSlugs', + versionBanner: 'unmaintained', + versionBadge: true, + versionClassName: 'docs-version-withSlugs', + }; - it('readVersionsMetadata versioned site with empty onlyIncludeVersions', async () => { - const {defaultOptions, defaultContext} = await loadSite(); + return { + versionedSiteDir, + defaultOptions, + defaultContext, + vCurrent, + v101, + v100, + vWithSlugs, + }; + } - await expect( - readVersionsMetadata({ - options: { - ...defaultOptions, - onlyIncludeVersions: [], - }, - context: defaultContext, - }), - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Invalid docs option \\"onlyIncludeVersions\\": an empty array is not allowed, at least one version is needed."`, - ); - }); + it('works', async () => { + const {defaultOptions, defaultContext, vCurrent, v101, v100, vWithSlugs} = + await loadSite(); - it('readVersionsMetadata versioned site with unknown versions in onlyIncludeVersions', async () => { - const {defaultOptions, defaultContext} = await loadSite(); - - await expect( - readVersionsMetadata({ - options: { - ...defaultOptions, - onlyIncludeVersions: ['unknownVersion1', 'unknownVersion2'], - }, - context: defaultContext, - }), - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Invalid docs option \\"onlyIncludeVersions\\": unknown versions (unknownVersion1,unknownVersion2) found. Available version names are: current, 1.0.1, 1.0.0, withSlugs"`, - ); - }); - - it('readVersionsMetadata versioned site with lastVersion not in onlyIncludeVersions', async () => { - const {defaultOptions, defaultContext} = await loadSite(); - - await expect( - readVersionsMetadata({ - options: { - ...defaultOptions, - lastVersion: '1.0.1', - onlyIncludeVersions: ['current', '1.0.0'], - }, - context: defaultContext, - }), - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Invalid docs option \\"lastVersion\\": if you use both the \\"onlyIncludeVersions\\" and \\"lastVersion\\" options, then \\"lastVersion\\" must be present in the provided \\"onlyIncludeVersions\\" array."`, - ); - }); - - it('readVersionsMetadata versioned site with invalid versions.json file', async () => { - const {defaultOptions, defaultContext} = await loadSite(); - - const mock = jest.spyOn(JSON, 'parse').mockImplementationOnce(() => ({ - invalid: 'json', - })); - - await expect( - readVersionsMetadata({ + const versionsMetadata = await readVersionsMetadata({ options: defaultOptions, context: defaultContext, - }), - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"The versions file should contain an array of versions! Found content: {\\"invalid\\":\\"json\\"}"`, - ); - mock.mockRestore(); - }); -}); + }); -describe('versioned site, pluginId=community', () => { - async function loadSite() { - const versionedSiteDir = path.resolve( - path.join(__dirname, '__fixtures__', 'versioned-site'), - ); - const defaultOptions: PluginOptions = { - ...DEFAULT_OPTIONS, - id: 'community', - path: 'community', - routeBasePath: 'communityBasePath', - sidebarPath: 'sidebars.json', - }; - const defaultContext = { - siteDir: versionedSiteDir, - baseUrl: '/', - i18n: DefaultI18N, - }; - - const vCurrent: VersionMetadata = { - contentPath: path.join(versionedSiteDir, 'community'), - contentPathLocalized: path.join( - versionedSiteDir, - 'i18n/en/docusaurus-plugin-content-docs-community/current', - ), - isLast: false, - routePriority: undefined, - sidebarFilePath: path.join(versionedSiteDir, 'sidebars.json'), - tagsPath: '/communityBasePath/next/tags', - versionLabel: 'Next', - versionName: 'current', - versionPath: '/communityBasePath/next', - versionBanner: 'unreleased', - versionBadge: true, - versionClassName: 'docs-version-current', - }; - - const v100: VersionMetadata = { - contentPath: path.join( - versionedSiteDir, - 'community_versioned_docs/version-1.0.0', - ), - contentPathLocalized: path.join( - versionedSiteDir, - 'i18n/en/docusaurus-plugin-content-docs-community/version-1.0.0', - ), - isLast: true, - routePriority: -1, - sidebarFilePath: path.join( - versionedSiteDir, - 'community_versioned_sidebars/version-1.0.0-sidebars.json', - ), - tagsPath: '/communityBasePath/tags', - versionLabel: '1.0.0', - versionName: '1.0.0', - versionPath: '/communityBasePath', - versionBanner: null, - versionBadge: true, - versionClassName: 'docs-version-1.0.0', - }; - - return {versionedSiteDir, defaultOptions, defaultContext, vCurrent, v100}; - } - - it('readVersionsMetadata versioned site (community)', async () => { - const {defaultOptions, defaultContext, vCurrent, v100} = await loadSite(); - - const versionsMetadata = await readVersionsMetadata({ - options: defaultOptions, - context: defaultContext, + expect(versionsMetadata).toEqual([vCurrent, v101, v100, vWithSlugs]); }); - expect(versionsMetadata).toEqual([vCurrent, v100]); - }); + it('works with includeCurrentVersion=false', async () => { + const {defaultOptions, defaultContext, v101, v100, vWithSlugs} = + await loadSite(); - it('readVersionsMetadata versioned site (community) with includeCurrentVersion=false', async () => { - const {defaultOptions, defaultContext, v100} = await loadSite(); + const versionsMetadata = await readVersionsMetadata({ + options: {...defaultOptions, includeCurrentVersion: false}, + context: defaultContext, + }); - const versionsMetadata = await readVersionsMetadata({ - options: {...defaultOptions, includeCurrentVersion: false}, - context: defaultContext, + expect(versionsMetadata).toEqual([ + // vCurrent removed + v101, + v100, + vWithSlugs, + ]); }); - expect(versionsMetadata).toEqual([ - // vCurrent removed - {...v100, versionBadge: false}, - ]); - }); + it('works with version options', async () => { + const {defaultOptions, defaultContext, vCurrent, v101, v100, vWithSlugs} = + await loadSite(); - it('readVersionsMetadata versioned site (community) with disableVersioning', async () => { - const {defaultOptions, defaultContext, vCurrent} = await loadSite(); - - const versionsMetadata = await readVersionsMetadata({ - options: {...defaultOptions, disableVersioning: true}, - context: defaultContext, - }); - - expect(versionsMetadata).toEqual([ - { - ...vCurrent, - isLast: true, - routePriority: -1, - tagsPath: '/communityBasePath/tags', - versionPath: '/communityBasePath', - versionBanner: null, - versionBadge: false, - }, - ]); - }); - - it('readVersionsMetadata versioned site (community) with all versions disabled', async () => { - const {defaultOptions, defaultContext} = await loadSite(); - - await expect( - readVersionsMetadata({ + const versionsMetadata = await readVersionsMetadata({ options: { ...defaultOptions, - includeCurrentVersion: false, - disableVersioning: true, + lastVersion: '1.0.0', + versions: { + current: { + path: 'current-path', + banner: 'unmaintained', + badge: false, + className: 'custom-current-className', + }, + '1.0.0': { + label: '1.0.0-label', + banner: 'unreleased', + }, + }, }, context: defaultContext, - }), - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"It is not possible to use docs without any version. Please check the configuration of these options: \\"includeCurrentVersion: false\\", \\"disableVersioning: true\\"."`, - ); + }); + + expect(versionsMetadata).toEqual([ + { + ...vCurrent, + tagsPath: '/docs/current-path/tags', + versionPath: '/docs/current-path', + versionBanner: 'unmaintained', + versionBadge: false, + versionClassName: 'custom-current-className', + }, + { + ...v101, + isLast: false, + routePriority: undefined, + tagsPath: '/docs/1.0.1/tags', + versionPath: '/docs/1.0.1', + versionBanner: 'unreleased', + }, + { + ...v100, + isLast: true, + routePriority: -1, + tagsPath: '/docs/tags', + versionLabel: '1.0.0-label', + versionPath: '/docs', + versionBanner: 'unreleased', + }, + vWithSlugs, + ]); + }); + + it('works with editUrl', async () => { + const {defaultOptions, defaultContext, vCurrent, v101, v100, vWithSlugs} = + await loadSite(); + + const versionsMetadata = await readVersionsMetadata({ + options: { + ...defaultOptions, + editUrl: 'https://github.com/facebook/docusaurus/edit/main/website/', + }, + context: defaultContext, + }); + + expect(versionsMetadata).toEqual([ + { + ...vCurrent, + versionEditUrl: + 'https://github.com/facebook/docusaurus/edit/main/website/docs', + versionEditUrlLocalized: + 'https://github.com/facebook/docusaurus/edit/main/website/i18n/en/docusaurus-plugin-content-docs/current', + }, + { + ...v101, + versionEditUrl: + 'https://github.com/facebook/docusaurus/edit/main/website/versioned_docs/version-1.0.1', + versionEditUrlLocalized: + 'https://github.com/facebook/docusaurus/edit/main/website/i18n/en/docusaurus-plugin-content-docs/version-1.0.1', + }, + { + ...v100, + versionEditUrl: + 'https://github.com/facebook/docusaurus/edit/main/website/versioned_docs/version-1.0.0', + versionEditUrlLocalized: + 'https://github.com/facebook/docusaurus/edit/main/website/i18n/en/docusaurus-plugin-content-docs/version-1.0.0', + }, + { + ...vWithSlugs, + versionEditUrl: + 'https://github.com/facebook/docusaurus/edit/main/website/versioned_docs/version-withSlugs', + versionEditUrlLocalized: + 'https://github.com/facebook/docusaurus/edit/main/website/i18n/en/docusaurus-plugin-content-docs/version-withSlugs', + }, + ]); + }); + + it('works with editUrl and editCurrentVersion=true', async () => { + const {defaultOptions, defaultContext, vCurrent, v101, v100, vWithSlugs} = + await loadSite(); + + const versionsMetadata = await readVersionsMetadata({ + options: { + ...defaultOptions, + editUrl: 'https://github.com/facebook/docusaurus/edit/main/website/', + editCurrentVersion: true, + }, + context: defaultContext, + }); + + expect(versionsMetadata).toEqual([ + { + ...vCurrent, + versionEditUrl: + 'https://github.com/facebook/docusaurus/edit/main/website/docs', + versionEditUrlLocalized: + 'https://github.com/facebook/docusaurus/edit/main/website/i18n/en/docusaurus-plugin-content-docs/current', + }, + { + ...v101, + versionEditUrl: + 'https://github.com/facebook/docusaurus/edit/main/website/docs', + versionEditUrlLocalized: + 'https://github.com/facebook/docusaurus/edit/main/website/i18n/en/docusaurus-plugin-content-docs/current', + }, + { + ...v100, + versionEditUrl: + 'https://github.com/facebook/docusaurus/edit/main/website/docs', + versionEditUrlLocalized: + 'https://github.com/facebook/docusaurus/edit/main/website/i18n/en/docusaurus-plugin-content-docs/current', + }, + { + ...vWithSlugs, + versionEditUrl: + 'https://github.com/facebook/docusaurus/edit/main/website/docs', + versionEditUrlLocalized: + 'https://github.com/facebook/docusaurus/edit/main/website/i18n/en/docusaurus-plugin-content-docs/current', + }, + ]); + }); + + it('works with onlyIncludeVersions option', async () => { + const {defaultOptions, defaultContext, v101, vWithSlugs} = + await loadSite(); + + const versionsMetadata = await readVersionsMetadata({ + options: { + ...defaultOptions, + // Order reversed on purpose: should not have any impact + onlyIncludeVersions: [vWithSlugs.versionName, v101.versionName], + }, + context: defaultContext, + }); + + expect(versionsMetadata).toEqual([v101, vWithSlugs]); + }); + + it('works with disableVersioning', async () => { + const {defaultOptions, defaultContext, vCurrent} = await loadSite(); + + const versionsMetadata = await readVersionsMetadata({ + options: {...defaultOptions, disableVersioning: true}, + context: defaultContext, + }); + + expect(versionsMetadata).toEqual([ + { + ...vCurrent, + isLast: true, + routePriority: -1, + tagsPath: '/docs/tags', + versionPath: '/docs', + versionBanner: null, + versionBadge: false, + }, + ]); + }); + + it('throws with all versions disabled', async () => { + const {defaultOptions, defaultContext} = await loadSite(); + + await expect( + readVersionsMetadata({ + options: { + ...defaultOptions, + includeCurrentVersion: false, + disableVersioning: true, + }, + context: defaultContext, + }), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"It is not possible to use docs without any version. Please check the configuration of these options: \\"includeCurrentVersion: false\\", \\"disableVersioning: true\\"."`, + ); + }); + + it('throws with empty onlyIncludeVersions', async () => { + const {defaultOptions, defaultContext} = await loadSite(); + + await expect( + readVersionsMetadata({ + options: { + ...defaultOptions, + onlyIncludeVersions: [], + }, + context: defaultContext, + }), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Invalid docs option \\"onlyIncludeVersions\\": an empty array is not allowed, at least one version is needed."`, + ); + }); + + it('throws with unknown versions in onlyIncludeVersions', async () => { + const {defaultOptions, defaultContext} = await loadSite(); + + await expect( + readVersionsMetadata({ + options: { + ...defaultOptions, + onlyIncludeVersions: ['unknownVersion1', 'unknownVersion2'], + }, + context: defaultContext, + }), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Invalid docs option \\"onlyIncludeVersions\\": unknown versions (unknownVersion1,unknownVersion2) found. Available version names are: current, 1.0.1, 1.0.0, withSlugs"`, + ); + }); + + it('throws with lastVersion not in onlyIncludeVersions', async () => { + const {defaultOptions, defaultContext} = await loadSite(); + + await expect( + readVersionsMetadata({ + options: { + ...defaultOptions, + lastVersion: '1.0.1', + onlyIncludeVersions: ['current', '1.0.0'], + }, + context: defaultContext, + }), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Invalid docs option \\"lastVersion\\": if you use both the \\"onlyIncludeVersions\\" and \\"lastVersion\\" options, then \\"lastVersion\\" must be present in the provided \\"onlyIncludeVersions\\" array."`, + ); + }); + + it('throws with invalid versions.json file', async () => { + const {defaultOptions, defaultContext} = await loadSite(); + + const jsonMock = jest.spyOn(JSON, 'parse'); + jsonMock.mockImplementationOnce(() => ({ + invalid: 'json', + })); + + await expect( + readVersionsMetadata({ + options: defaultOptions, + context: defaultContext, + }), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"The versions file should contain an array of version names! Found content: {\\"invalid\\":\\"json\\"}"`, + ); + jsonMock.mockImplementationOnce(() => [1.1]); + + await expect( + readVersionsMetadata({ + options: defaultOptions, + context: defaultContext, + }), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Versions should be strings. Found type \\"number\\" for version \\"1.1\\"."`, + ); + jsonMock.mockImplementationOnce(() => [' ']); + + await expect( + readVersionsMetadata({ + options: defaultOptions, + context: defaultContext, + }), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Invalid version \\" \\"."`, + ); + jsonMock.mockRestore(); + }); + }); + + describe('versioned site, pluginId=community', () => { + async function loadSite() { + const versionedSiteDir = path.resolve( + path.join(__dirname, '__fixtures__', 'versioned-site'), + ); + const defaultOptions: PluginOptions = { + ...DEFAULT_OPTIONS, + id: 'community', + path: 'community', + routeBasePath: 'communityBasePath', + sidebarPath: 'sidebars.json', + }; + const defaultContext = { + siteDir: versionedSiteDir, + baseUrl: '/', + i18n: DefaultI18N, + }; + + const vCurrent: VersionMetadata = { + contentPath: path.join(versionedSiteDir, 'community'), + contentPathLocalized: path.join( + versionedSiteDir, + 'i18n/en/docusaurus-plugin-content-docs-community/current', + ), + isLast: false, + routePriority: undefined, + sidebarFilePath: path.join(versionedSiteDir, 'sidebars.json'), + tagsPath: '/communityBasePath/next/tags', + versionLabel: 'Next', + versionName: 'current', + versionPath: '/communityBasePath/next', + versionBanner: 'unreleased', + versionBadge: true, + versionClassName: 'docs-version-current', + }; + + const v100: VersionMetadata = { + contentPath: path.join( + versionedSiteDir, + 'community_versioned_docs/version-1.0.0', + ), + contentPathLocalized: path.join( + versionedSiteDir, + 'i18n/en/docusaurus-plugin-content-docs-community/version-1.0.0', + ), + isLast: true, + routePriority: -1, + sidebarFilePath: path.join( + versionedSiteDir, + 'community_versioned_sidebars/version-1.0.0-sidebars.json', + ), + tagsPath: '/communityBasePath/tags', + versionLabel: '1.0.0', + versionName: '1.0.0', + versionPath: '/communityBasePath', + versionBanner: null, + versionBadge: true, + versionClassName: 'docs-version-1.0.0', + }; + + return {versionedSiteDir, defaultOptions, defaultContext, vCurrent, v100}; + } + + it('works', async () => { + const {defaultOptions, defaultContext, vCurrent, v100} = await loadSite(); + + const versionsMetadata = await readVersionsMetadata({ + options: defaultOptions, + context: defaultContext, + }); + + expect(versionsMetadata).toEqual([vCurrent, v100]); + }); + + it('works with includeCurrentVersion=false', async () => { + const {defaultOptions, defaultContext, v100} = await loadSite(); + + const versionsMetadata = await readVersionsMetadata({ + options: {...defaultOptions, includeCurrentVersion: false}, + context: defaultContext, + }); + + expect(versionsMetadata).toEqual([ + // vCurrent removed + {...v100, versionBadge: false}, + ]); + }); + + it('works with disableVersioning', async () => { + const {defaultOptions, defaultContext, vCurrent} = await loadSite(); + + const versionsMetadata = await readVersionsMetadata({ + options: {...defaultOptions, disableVersioning: true}, + context: defaultContext, + }); + + expect(versionsMetadata).toEqual([ + { + ...vCurrent, + isLast: true, + routePriority: -1, + tagsPath: '/communityBasePath/tags', + versionPath: '/communityBasePath', + versionBanner: null, + versionBadge: false, + }, + ]); + }); + + it('throws with all versions disabled', async () => { + const {defaultOptions, defaultContext} = await loadSite(); + + await expect( + readVersionsMetadata({ + options: { + ...defaultOptions, + includeCurrentVersion: false, + disableVersioning: true, + }, + context: defaultContext, + }), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"It is not possible to use docs without any version. Please check the configuration of these options: \\"includeCurrentVersion: false\\", \\"disableVersioning: true\\"."`, + ); + }); }); }); diff --git a/packages/docusaurus-plugin-content-docs/src/docFrontMatter.ts b/packages/docusaurus-plugin-content-docs/src/docFrontMatter.ts index 72e085b070..da4b86ca37 100644 --- a/packages/docusaurus-plugin-content-docs/src/docFrontMatter.ts +++ b/packages/docusaurus-plugin-content-docs/src/docFrontMatter.ts @@ -25,7 +25,7 @@ const DocFrontMatterSchema = Joi.object({ hide_table_of_contents: Joi.boolean(), keywords: Joi.array().items(Joi.string().required()), image: URISchema, - description: Joi.string().allow(''), // see https://github.com/facebook/docusaurus/issues/4591#issuecomment-822372398 + description: Joi.string().allow(''), // see https://github.com/facebook/docusaurus/issues/4591#issuecomment-822372398 slug: Joi.string(), sidebar_label: Joi.string(), sidebar_position: Joi.number(), diff --git a/packages/docusaurus-plugin-content-docs/src/docs.ts b/packages/docusaurus-plugin-content-docs/src/docs.ts index b58c4c2353..9fcfa8ee32 100644 --- a/packages/docusaurus-plugin-content-docs/src/docs.ts +++ b/packages/docusaurus-plugin-content-docs/src/docs.ts @@ -391,6 +391,10 @@ export const isCategoryIndex: CategoryIndexMatcher = ({ return eligibleDocIndexNames.includes(fileName.toLowerCase()); }; +/** + * `guides/sidebar/autogenerated.md` -> + * `'autogenerated', '.md', ['sidebar', 'guides']` + */ export function toCategoryIndexMatcherParam({ source, sourceDirName, @@ -406,28 +410,6 @@ export function toCategoryIndexMatcherParam({ }; } -/** - * `guides/sidebar/autogenerated.md` -> - * `'autogenerated', '.md', ['sidebar', 'guides']` - */ -export function splitPath(str: string): { - /** - * The list of directories, from lowest level to highest. - * If there's no dir name, directories is ['.'] - */ - directories: string[]; - /** The file name, without extension */ - fileName: string; - /** The extension, with a leading dot */ - extension: string; -} { - return { - fileName: path.parse(str).name, - extension: path.parse(str).ext, - directories: path.dirname(str).split(path.sep).reverse(), - }; -} - // Return both doc ids // TODO legacy retro-compatibility due to old versioned sidebars using // versioned doc ids ("id" should be removed & "versionedId" should be renamed diff --git a/packages/docusaurus-plugin-content-docs/src/globalData.ts b/packages/docusaurus-plugin-content-docs/src/globalData.ts index 9939fb7fca..bc974d527c 100644 --- a/packages/docusaurus-plugin-content-docs/src/globalData.ts +++ b/packages/docusaurus-plugin-content-docs/src/globalData.ts @@ -6,7 +6,6 @@ */ import _ from 'lodash'; -import {normalizeUrl} from '@docusaurus/utils'; import type {Sidebars} from './sidebars/types'; import {createSidebarsUtils} from './sidebars/utils'; import type { @@ -20,7 +19,7 @@ import type { GlobalDoc, } from '@docusaurus/plugin-content-docs/client'; -export function toGlobalDataDoc(doc: DocMetadata): GlobalDoc { +function toGlobalDataDoc(doc: DocMetadata): GlobalDoc { return { id: doc.unversionedId, path: doc.permalink, @@ -28,7 +27,7 @@ export function toGlobalDataDoc(doc: DocMetadata): GlobalDoc { }; } -export function toGlobalDataGeneratedIndex( +function toGlobalDataGeneratedIndex( doc: CategoryGeneratedIndexMetadata, ): GlobalDoc { return { @@ -38,7 +37,7 @@ export function toGlobalDataGeneratedIndex( }; } -export function toGlobalSidebars( +function toGlobalSidebars( sidebars: Sidebars, version: LoadedVersion, ): Record { @@ -52,7 +51,7 @@ export function toGlobalSidebars( link: { path: firstLink.type === 'generated-index' - ? normalizeUrl([version.versionPath, firstLink.slug]) + ? firstLink.permalink : version.docs.find( (doc) => doc.id === firstLink.id || doc.unversionedId === firstLink.id, diff --git a/packages/docusaurus-plugin-content-docs/src/plugin-content-docs.d.ts b/packages/docusaurus-plugin-content-docs/src/plugin-content-docs.d.ts index 95ec7b3635..f14be0f462 100644 --- a/packages/docusaurus-plugin-content-docs/src/plugin-content-docs.d.ts +++ b/packages/docusaurus-plugin-content-docs/src/plugin-content-docs.d.ts @@ -18,8 +18,14 @@ declare module '@docusaurus/plugin-content-docs' { }; export type CategoryIndexMatcherParam = { + /** The file name, without extension */ fileName: string; + /** + * The list of directories, from lowest level to highest. + * If there's no dir name, directories is ['.'] + */ directories: string[]; + /** The extension, with a leading dot */ extension: string; }; export type CategoryIndexMatcher = ( diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/utils.test.ts b/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/utils.test.ts index c6a740fb84..1b75e8e65e 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/utils.test.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/__tests__/utils.test.ts @@ -111,7 +111,7 @@ describe('createSidebarsUtils', () => { link: { type: 'generated-index', slug: '/s4-category-slug', - permalink: '/s4-category-permalink', + permalink: '/s4-category-slug', }, items: [ {type: 'doc', id: 'doc8'}, @@ -291,7 +291,7 @@ describe('createSidebarsUtils', () => { }); expect(getFirstLink('sidebar4')).toEqual({ type: 'generated-index', - slug: '/s4-category-slug', + permalink: '/s4-category-slug', label: 'S4 Category', }); }); diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/utils.ts b/packages/docusaurus-plugin-content-docs/src/sidebars/utils.ts index 20013e5208..da095918d5 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/utils.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/utils.ts @@ -139,6 +139,11 @@ export type SidebarsUtils = { getCategoryGeneratedIndexNavigation: ( categoryGeneratedIndexPermalink: string, ) => SidebarNavigation; + /** + * This function may return undefined. This is usually a user mistake, because + * it means this sidebar will never be displayed; however, we can still use + * `displayed_sidebar` to make it displayed. Pretty weird but valid use-case + */ getFirstLink: (sidebarId: string) => | { type: 'doc'; @@ -147,7 +152,7 @@ export type SidebarsUtils = { } | { type: 'generated-index'; - slug: string; + permalink: string; label: string; } | undefined; @@ -295,7 +300,7 @@ Available document ids are: } | { type: 'generated-index'; - slug: string; + permalink: string; label: string; } | undefined { @@ -316,7 +321,7 @@ Available document ids are: } else if (item.link?.type === 'generated-index') { return { type: 'generated-index', - slug: item.link.slug, + permalink: item.link.permalink, label: item.label, }; } diff --git a/packages/docusaurus-plugin-content-docs/src/slug.ts b/packages/docusaurus-plugin-content-docs/src/slug.ts index 14c99925dc..9de48aa13f 100644 --- a/packages/docusaurus-plugin-content-docs/src/slug.ts +++ b/packages/docusaurus-plugin-content-docs/src/slug.ts @@ -63,12 +63,11 @@ export default function getSlug({ function ensureValidSlug(slug: string): string { if (!isValidPathname(slug)) { throw new Error( - `We couldn't compute a valid slug for document with id "${baseID}" in "${sourceDirName}" directory. + `We couldn't compute a valid slug for document with ID "${baseID}" in "${sourceDirName}" directory. The slug we computed looks invalid: ${slug}. -Maybe your slug front matter is incorrect or you use weird chars in the file path? -By using the slug front matter, you should be able to fix this error, by using the slug of your choice: +Maybe your slug front matter is incorrect or there are special characters in the file path? +By using front matter to set a custom slug, you should be able to fix this error: -Example => --- slug: /my/customDocPath --- diff --git a/packages/docusaurus-plugin-content-docs/src/translations.ts b/packages/docusaurus-plugin-content-docs/src/translations.ts index 49681b01ea..c6929f795e 100644 --- a/packages/docusaurus-plugin-content-docs/src/translations.ts +++ b/packages/docusaurus-plugin-content-docs/src/translations.ts @@ -274,10 +274,7 @@ function translateVersion( translationFiles: Record, ): LoadedVersion { const versionTranslations = - translationFiles[getVersionFileName(version.versionName)]?.content; - if (!versionTranslations) { - return version; - } + translationFiles[getVersionFileName(version.versionName)]!.content; return { ...version, versionLabel: diff --git a/packages/docusaurus-plugin-content-docs/src/versions.ts b/packages/docusaurus-plugin-content-docs/src/versions.ts index 08dbb0a796..6641184981 100644 --- a/packages/docusaurus-plugin-content-docs/src/versions.ts +++ b/packages/docusaurus-plugin-content-docs/src/versions.ts @@ -74,9 +74,9 @@ function ensureValidVersionString(version: unknown): asserts version is string { function ensureValidVersionArray( versionArray: unknown, ): asserts versionArray is string[] { - if (!(versionArray instanceof Array)) { + if (!Array.isArray(versionArray)) { throw new Error( - `The versions file should contain an array of versions! Found content: ${JSON.stringify( + `The versions file should contain an array of version names! Found content: ${JSON.stringify( versionArray, )}`, ); diff --git a/packages/docusaurus-theme-common/src/utils/docsPreferredVersion/DocsPreferredVersionProvider.tsx b/packages/docusaurus-theme-common/src/utils/docsPreferredVersion/DocsPreferredVersionProvider.tsx index 49c74aa7b0..e425d86d53 100644 --- a/packages/docusaurus-theme-common/src/utils/docsPreferredVersion/DocsPreferredVersionProvider.tsx +++ b/packages/docusaurus-theme-common/src/utils/docsPreferredVersion/DocsPreferredVersionProvider.tsx @@ -92,13 +92,13 @@ function useVersionPersistence(): DocsVersionPersistence { return useThemeConfig().docs.versionPersistence; } -// Value that will be accessible through context: [state,api] +// Value that will be accessible through context: [state,api] function useContextValue() { const allDocsData = useAllDocsData(); const versionPersistence = useVersionPersistence(); const pluginIds = useMemo(() => Object.keys(allDocsData), [allDocsData]); - // Initial state is empty, as we can't read browser storage in node/SSR + // Initial state is empty, as we can't read browser storage in node/SSR const [state, setState] = useState(() => getInitialState(pluginIds)); // On mount, we set the state read from browser storage diff --git a/packages/docusaurus-theme-search-algolia/src/theme/SearchPage/index.tsx b/packages/docusaurus-theme-search-algolia/src/theme/SearchPage/index.tsx index 61daf2266c..a9227bdff4 100644 --- a/packages/docusaurus-theme-search-algolia/src/theme/SearchPage/index.tsx +++ b/packages/docusaurus-theme-search-algolia/src/theme/SearchPage/index.tsx @@ -218,7 +218,7 @@ export default function SearchPage(): JSX.Element { algoliaHelper.on( 'result', ({results: {query, hits, page, nbHits, nbPages}}) => { - if (query === '' || !(hits instanceof Array)) { + if (query === '' || !Array.isArray(hits)) { searchResultStateDispatcher({type: 'reset'}); return; } diff --git a/packages/docusaurus-utils/src/__tests__/markdownParser.test.ts b/packages/docusaurus-utils/src/__tests__/markdownParser.test.ts index bc565f7550..01999c82c7 100644 --- a/packages/docusaurus-utils/src/__tests__/markdownParser.test.ts +++ b/packages/docusaurus-utils/src/__tests__/markdownParser.test.ts @@ -250,7 +250,7 @@ Lorem Ipsum }); }); - it('parses markdown h1 title at the top followed by h2 title', () => { + it('parses markdown h1 title at the top followed by h2 title', () => { const markdown = dedent` # Markdown Title diff --git a/packages/docusaurus/src/client/__tests__/normalizeLocation.test.ts b/packages/docusaurus/src/client/__tests__/normalizeLocation.test.ts index df6294b60c..9517a5b0f1 100644 --- a/packages/docusaurus/src/client/__tests__/normalizeLocation.test.ts +++ b/packages/docusaurus/src/client/__tests__/normalizeLocation.test.ts @@ -9,7 +9,15 @@ import {jest} from '@jest/globals'; import normalizeLocation from '../normalizeLocation'; describe('normalizeLocation', () => { - it('rewrite locations with index.html', () => { + it('rewrites locations with index.html', () => { + expect( + normalizeLocation({ + pathname: '/index.html', + }), + ).toEqual({ + pathname: '/', + }); + expect( normalizeLocation({ pathname: '/docs/introduction/index.html', @@ -35,7 +43,7 @@ describe('normalizeLocation', () => { }); }); - it('untouched pathnames', () => { + it('leaves pathnames untouched', () => { const replaceMock = jest.spyOn(String.prototype, 'replace'); expect( diff --git a/packages/docusaurus/src/client/exports/Interpolate.tsx b/packages/docusaurus/src/client/exports/Interpolate.tsx index 8b894f7032..5b290d6054 100644 --- a/packages/docusaurus/src/client/exports/Interpolate.tsx +++ b/packages/docusaurus/src/client/exports/Interpolate.tsx @@ -89,12 +89,12 @@ export function interpolate( export default function Interpolate({ children, values, -}: InterpolateProps): ReactNode { +}: InterpolateProps): JSX.Element { if (typeof children !== 'string') { console.warn('Illegal children', children); throw new Error( 'The Docusaurus component only accept simple string values', ); } - return interpolate(children, values); + return <>{interpolate(children, values)}; } diff --git a/packages/docusaurus/src/client/exports/Translate.tsx b/packages/docusaurus/src/client/exports/Translate.tsx index 8e9c0f8fcc..5cfbef7edb 100644 --- a/packages/docusaurus/src/client/exports/Translate.tsx +++ b/packages/docusaurus/src/client/exports/Translate.tsx @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import type {ReactNode} from 'react'; +import React from 'react'; import {interpolate, type InterpolateValues} from '@docusaurus/Interpolate'; import type {TranslateParam, TranslateProps} from '@docusaurus/Translate'; @@ -46,7 +46,7 @@ export default function Translate({ children, id, values, -}: TranslateProps): ReactNode { +}: TranslateProps): JSX.Element { if (children && typeof children !== 'string') { console.warn('Illegal children', children); throw new Error( @@ -55,5 +55,5 @@ export default function Translate({ } const localizedMessage: string = getLocalizedMessage({message: children, id}); - return interpolate(localizedMessage, values); + return <>{interpolate(localizedMessage, values)}; } diff --git a/packages/docusaurus/src/client/exports/__tests__/BrowserOnly.test.tsx b/packages/docusaurus/src/client/exports/__tests__/BrowserOnly.test.tsx index 0de095ad04..7ab1c09720 100644 --- a/packages/docusaurus/src/client/exports/__tests__/BrowserOnly.test.tsx +++ b/packages/docusaurus/src/client/exports/__tests__/BrowserOnly.test.tsx @@ -5,46 +5,55 @@ * LICENSE file in the root directory of this source tree. */ -import {jest} from '@jest/globals'; import React from 'react'; import renderer from 'react-test-renderer'; import BrowserOnly from '../BrowserOnly'; +import {Context} from '../browserContext'; -jest.mock('@docusaurus/useIsBrowser', () => () => true); - -describe('BrowserOnly', () => { +describe('', () => { it('rejects react element children', () => { process.env.NODE_ENV = 'development'; - expect(() => { - renderer.create( - - {/* @ts-expect-error test */} - {window.location.href} - , - ); - }).toThrowErrorMatchingInlineSnapshot(` + expect(() => + renderer + .create( + + + {/* @ts-expect-error test */} + {window.location.href} + + , + ) + .toJSON(), + ).toThrowErrorMatchingInlineSnapshot(` "Docusaurus error: The children of must be a \\"render function\\", e.g. {() => {window.location.href}}. Current type: React element" `); }); + it('rejects string children', () => { + process.env.NODE_ENV = 'development'; expect(() => { renderer.create( - // @ts-expect-error test - , + + {/* @ts-expect-error test */} + + , ); }).toThrowErrorMatchingInlineSnapshot(` "Docusaurus error: The children of must be a \\"render function\\", e.g. {() => {window.location.href}}. Current type: string" `); }); + it('accepts valid children', () => { expect( renderer .create( - Loading}> - {() => {window.location.href}} - , + + Loading}> + {() => {window.location.href}} + + , ) .toJSON(), ).toMatchInlineSnapshot(` @@ -53,4 +62,36 @@ describe('BrowserOnly', () => { `); }); + + it('returns fallback when not in browser', () => { + expect( + renderer + .create( + + Loading}> + {() => {window.location.href}} + + , + ) + .toJSON(), + ).toMatchInlineSnapshot(` + + Loading + + `); + }); + + it('gracefully falls back', () => { + expect( + renderer + .create( + + + {() => {window.location.href}} + + , + ) + .toJSON(), + ).toMatchInlineSnapshot(`null`); + }); }); diff --git a/packages/docusaurus/src/client/exports/__tests__/Interpolate.test.tsx b/packages/docusaurus/src/client/exports/__tests__/Interpolate.test.tsx index 7cd3155722..fdf1033103 100644 --- a/packages/docusaurus/src/client/exports/__tests__/Interpolate.test.tsx +++ b/packages/docusaurus/src/client/exports/__tests__/Interpolate.test.tsx @@ -6,9 +6,10 @@ */ import React from 'react'; -import {interpolate} from '../Interpolate'; +import renderer from 'react-test-renderer'; +import Interpolate, {interpolate} from '../Interpolate'; -describe('Interpolate', () => { +describe('interpolate', () => { it('without placeholders', () => { const text = 'Hello how are you?'; expect(interpolate(text)).toEqual(text); @@ -86,3 +87,50 @@ describe('Interpolate', () => { expect(interpolate(text, values)).toMatchSnapshot(); }); }); + +describe('', () => { + it('without placeholders', () => { + const text = 'Hello how are you?'; + expect(renderer.create({text}).toJSON()).toEqual( + text, + ); + }); + + it('placeholders with string values', () => { + const text = 'Hello {name} how are you {day}?'; + const values = {name: 'Sébastien', day: 'today'}; + expect( + renderer + .create({text}) + .toJSON(), + ).toMatchInlineSnapshot(`"Hello Sébastien how are you today?"`); + }); + + it('acceptance test', () => { + const text = 'Hello {name} how are you {day}? Another {unprovidedValue}!'; + const values = { + name: 'Sébastien', + day: today, + extraUselessValue1:
test
, + extraUselessValue2: 'hi', + }; + expect( + renderer + .create({text}) + .toJSON(), + ).toMatchSnapshot(); + }); + + it('rejects when children is not string', () => { + expect(() => + renderer.create( + + {/* @ts-expect-error: for test */} + aaa + , + ), + ).toThrowErrorMatchingInlineSnapshot( + `"The Docusaurus component only accept simple string values"`, + ); + }); +}); diff --git a/packages/docusaurus/src/client/exports/__tests__/Translate.test.tsx b/packages/docusaurus/src/client/exports/__tests__/Translate.test.tsx index b1e8efb76a..28a8f90fe6 100644 --- a/packages/docusaurus/src/client/exports/__tests__/Translate.test.tsx +++ b/packages/docusaurus/src/client/exports/__tests__/Translate.test.tsx @@ -5,28 +5,75 @@ * LICENSE file in the root directory of this source tree. */ -import {translate} from '../Translate'; +import React from 'react'; +import renderer from 'react-test-renderer'; +import Translate, {translate} from '../Translate'; describe('translate', () => { - it('accept id and use it as fallback', () => { + it('accepts id and uses it as fallback', () => { expect(translate({id: 'some-id'})).toBe('some-id'); }); - it('accept message and use it as fallback', () => { + it('accepts message and uses it as fallback', () => { expect(translate({message: 'some-message'})).toBe('some-message'); }); - it('accept id+message and use message as fallback', () => { + it('accepts id+message and uses message as fallback', () => { expect(translate({id: 'some-id', message: 'some-message'})).toBe( 'some-message', ); }); - it('reject when no id or message', () => { - // TODO tests are not resolving type defs correctly + it('rejects when no id or message', () => { + // TODO tests are not resolving type defs correctly. We need to include test + // files in a tsconfig file // @ts-expect-error: TS should protect when both id/message are missing expect(() => translate({})).toThrowErrorMatchingInlineSnapshot( `"Docusaurus translation declarations must have at least a translation id or a default translation message"`, ); }); }); + +describe('', () => { + it('accepts id and uses it as fallback', () => { + expect(renderer.create().toJSON()).toBe( + 'some-id', + ); + }); + + it('accepts message and uses it as fallback', () => { + expect(renderer.create(some-message).toJSON()).toBe( + 'some-message', + ); + }); + + it('accepts id+message and uses message as fallback', () => { + expect( + renderer + .create(some-message) + .toJSON(), + ).toBe('some-message'); + }); + + it('rejects when no id or message', () => { + expect(() => + // @ts-expect-error: TS should protect when both id/message are missing + renderer.create(), + ).toThrowErrorMatchingInlineSnapshot( + `"Docusaurus translation declarations must have at least a translation id or a default translation message"`, + ); + }); + + it('rejects when children is not a string', () => { + expect(() => + renderer.create( + + {/* @ts-expect-error: for test */} + aaa + , + ), + ).toThrowErrorMatchingInlineSnapshot( + `"The Docusaurus component only accept simple string values"`, + ); + }); +}); diff --git a/packages/docusaurus/src/client/exports/__tests__/__snapshots__/Interpolate.test.tsx.snap b/packages/docusaurus/src/client/exports/__tests__/__snapshots__/Interpolate.test.tsx.snap index a42c4388ab..09d0dcadd9 100644 --- a/packages/docusaurus/src/client/exports/__tests__/__snapshots__/Interpolate.test.tsx.snap +++ b/packages/docusaurus/src/client/exports/__tests__/__snapshots__/Interpolate.test.tsx.snap @@ -1,6 +1,18 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Interpolate acceptance test 1`] = ` +exports[` acceptance test 1`] = ` +Array [ + "Hello ", + "Sébastien", + " how are you ", + + today + , + "? Another {unprovidedValue}!", +] +`; + +exports[`interpolate acceptance test 1`] = ` Array [ Hello @@ -18,7 +30,7 @@ Array [ ] `; -exports[`Interpolate placeholders with JSX values 1`] = ` +exports[`interpolate placeholders with JSX values 1`] = ` Array [ Hello @@ -38,7 +50,7 @@ Array [ ] `; -exports[`Interpolate placeholders with mixed vales 1`] = ` +exports[`interpolate placeholders with mixed vales 1`] = ` Array [ Hello diff --git a/packages/docusaurus/src/client/exports/__tests__/browserContext.test.tsx b/packages/docusaurus/src/client/exports/__tests__/browserContext.test.tsx new file mode 100644 index 0000000000..2802f91532 --- /dev/null +++ b/packages/docusaurus/src/client/exports/__tests__/browserContext.test.tsx @@ -0,0 +1,30 @@ +/** + * 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. + * + * @jest-environment jsdom + */ + +// Jest doesn't allow pragma below other comments. https://github.com/facebook/jest/issues/12573 +// eslint-disable-next-line header/header +import React from 'react'; +import {renderHook} from '@testing-library/react-hooks/server'; +import {BrowserContextProvider} from '../browserContext'; +import useIsBrowser from '../useIsBrowser'; + +describe('BrowserContextProvider', () => { + const {result, hydrate} = renderHook(() => useIsBrowser(), { + wrapper: ({children}) => ( + {children} + ), + }); + it('has value false on first render', () => { + expect(result.current).toBe(false); + }); + it('has value true on hydration', () => { + hydrate(); + expect(result.current).toBe(true); + }); +}); diff --git a/packages/docusaurus/src/client/exports/__tests__/docusaurusContext.test.tsx b/packages/docusaurus/src/client/exports/__tests__/docusaurusContext.test.tsx new file mode 100644 index 0000000000..062591f3b4 --- /dev/null +++ b/packages/docusaurus/src/client/exports/__tests__/docusaurusContext.test.tsx @@ -0,0 +1,41 @@ +/** + * 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. + * + * @jest-environment jsdom + */ + +// Jest doesn't allow pragma below other comments. https://github.com/facebook/jest/issues/12573 +// eslint-disable-next-line header/header +import React from 'react'; +import {renderHook} from '@testing-library/react-hooks/server'; +import {DocusaurusContextProvider} from '../docusaurusContext'; +import useDocusaurusContext from '../useDocusaurusContext'; + +// This test currently isn't quite useful because the @generated aliases point +// to the empty modules. Maybe we can point that to fixtures in the future. +describe('DocusaurusContextProvider', () => { + const {result, hydrate} = renderHook(() => useDocusaurusContext(), { + wrapper: ({children}) => ( + {children} + ), + }); + const value = result.current; + it('returns right value', () => { + expect(value).toMatchInlineSnapshot(` + Object { + "codeTranslations": Object {}, + "globalData": Object {}, + "i18n": Object {}, + "siteConfig": Object {}, + "siteMetadata": Object {}, + } + `); + }); + it('has reference-equal value on hydration', () => { + hydrate(); + expect(result.current).toBe(value); + }); +}); diff --git a/packages/docusaurus/src/client/exports/__tests__/useGlobalData.test.tsx b/packages/docusaurus/src/client/exports/__tests__/useGlobalData.test.tsx new file mode 100644 index 0000000000..670a7e3bcc --- /dev/null +++ b/packages/docusaurus/src/client/exports/__tests__/useGlobalData.test.tsx @@ -0,0 +1,122 @@ +/** + * 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 React from 'react'; +import {renderHook} from '@testing-library/react-hooks'; +import useGlobalData, { + useAllPluginInstancesData, + usePluginData, +} from '../useGlobalData'; +import {Context} from '../docusaurusContext'; + +describe('useGlobalData', () => { + it('returns global data from context', () => { + expect( + renderHook(() => useGlobalData(), { + wrapper: ({children}) => ( + // eslint-disable-next-line react/jsx-no-constructed-context-values + + {children} + + ), + }).result.current, + ).toEqual({foo: 'bar'}); + }); + + it('throws when global data not found', () => { + // Can it actually happen? + expect( + () => + renderHook(() => useGlobalData(), { + wrapper: ({children}) => ( + // eslint-disable-next-line react/jsx-no-constructed-context-values + {children} + ), + }).result.current, + ).toThrowErrorMatchingInlineSnapshot(`"Docusaurus global data not found."`); + }); +}); + +describe('useAllPluginInstancesData', () => { + it('returns plugin data namespace', () => { + expect( + renderHook(() => useAllPluginInstancesData('foo'), { + wrapper: ({children}) => ( + + {children} + + ), + }).result.current, + ).toEqual({default: 'default', bar: 'bar'}); + }); + + it('throws when plugin data not found', () => { + expect( + () => + renderHook(() => useAllPluginInstancesData('bar'), { + wrapper: ({children}) => ( + + {children} + + ), + }).result.current, + ).toThrowErrorMatchingInlineSnapshot( + `"Docusaurus plugin global data not found for \\"bar\\" plugin."`, + ); + }); +}); + +describe('usePluginData', () => { + it('returns plugin instance data', () => { + expect( + renderHook(() => usePluginData('foo', 'bar'), { + wrapper: ({children}) => ( + + {children} + + ), + }).result.current, + ).toBe('bar'); + }); + + it('defaults to default ID', () => { + expect( + renderHook(() => usePluginData('foo'), { + wrapper: ({children}) => ( + + {children} + + ), + }).result.current, + ).toBe('default'); + }); + + it('throws when plugin instance data not found', () => { + expect( + () => + renderHook(() => usePluginData('foo', 'baz'), { + wrapper: ({children}) => ( + + {children} + + ), + }).result.current, + ).toThrowErrorMatchingInlineSnapshot( + `"Docusaurus plugin global data not found for \\"foo\\" plugin with id \\"baz\\"."`, + ); + }); +}); diff --git a/packages/docusaurus/src/client/normalizeLocation.ts b/packages/docusaurus/src/client/normalizeLocation.ts index 18b408c33b..6d443c316e 100644 --- a/packages/docusaurus/src/client/normalizeLocation.ts +++ b/packages/docusaurus/src/client/normalizeLocation.ts @@ -18,12 +18,8 @@ export default function normalizeLocation(location: T): T { }; } - let pathname = location.pathname || '/'; - pathname = pathname.trim().replace(/\/index\.html$/, ''); - - if (pathname === '') { - pathname = '/'; - } + const pathname = + location.pathname.trim().replace(/\/index\.html$/, '') || '/'; pathnames[location.pathname] = pathname; diff --git a/packages/docusaurus/src/server/config.ts b/packages/docusaurus/src/server/config.ts index 5f6d0ef01f..94e8656acf 100644 --- a/packages/docusaurus/src/server/config.ts +++ b/packages/docusaurus/src/server/config.ts @@ -24,7 +24,7 @@ export default async function loadConfig( | (() => Promise>); const loadedConfig = - importedConfig instanceof Function + typeof importedConfig === 'function' ? await importedConfig() : await importedConfig; diff --git a/packages/docusaurus/src/server/index.ts b/packages/docusaurus/src/server/index.ts index 414125b416..d603c46c2d 100644 --- a/packages/docusaurus/src/server/index.ts +++ b/packages/docusaurus/src/server/index.ts @@ -464,7 +464,7 @@ ${Object.entries(registry) return props; } -// We want all @docusaurus/* packages to have the exact same version! +// We want all @docusaurus/* packages to have the exact same version! // See https://github.com/facebook/docusaurus/issues/3371 // See https://github.com/facebook/docusaurus/pull/3386 function checkDocusaurusPackagesVersion(siteMetadata: DocusaurusSiteMetadata) { diff --git a/packages/docusaurus/src/webpack/__tests__/__fixtures__/host.crt b/packages/docusaurus/src/webpack/__tests__/__fixtures__/host.crt new file mode 100644 index 0000000000..81a7dc73bb --- /dev/null +++ b/packages/docusaurus/src/webpack/__tests__/__fixtures__/host.crt @@ -0,0 +1,24 @@ +-----BEGIN CERTIFICATE----- +MIID8zCCAtugAwIBAgIUK1U7Oje+GjLlzxNryMDUT72qJZ0wDQYJKoZIhvcNAQEL +BQAwgYgxCzAJBgNVBAYTAkNOMREwDwYDVQQIDAhTaGFuZ2hhaTERMA8GA1UEBwwI +U2hhbmdoYWkxGDAWBgNVBAoMD0NvbXB1dGVyaXphdGlvbjESMBAGA1UEAwwJSm9z +aC1DZW5hMSUwIwYJKoZIhvcNAQkBFhZzaWRhY2hlbjIwMDNAZ21haWwuY29tMB4X +DTIyMDMxMjE0MzI0N1oXDTIzMDMxMjE0MzI0N1owgYgxCzAJBgNVBAYTAkNOMREw +DwYDVQQIDAhTaGFuZ2hhaTERMA8GA1UEBwwIU2hhbmdoYWkxGDAWBgNVBAoMD0Nv +bXB1dGVyaXphdGlvbjESMBAGA1UEAwwJSm9zaC1DZW5hMSUwIwYJKoZIhvcNAQkB +FhZzaWRhY2hlbjIwMDNAZ21haWwuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEA7Cq2QW6rcZAm6MMo97aqkFi9dkXx97fW6vPEt2bgS9O6E+M/wXBI +q1Dh3ud8sGP+CiEWa+7uIEwX9pRGyQo0Lkr7qZWSbsDh+RmdkiKUCiIUUTBopBjM +jo7XF9KBM609GYoGlKYxv4adPbOMJcK/9VdJPz3NprIA1PHEqInJNnuKMMjBMhNu +1MZ7JwING/LYBOJ/Mve08XKAcyDdWBVPe2TOfcKhEmtBTKhnOuUicuAdVtDkN34Z +e4ZlifLo7wlQU7NNh7YDOYZz3JXB5QotuqtWkUgfpMSCWG90p4P4LExLzS+2sb7O +C/jO0qYcKjaKAKjrA9IIyClF6VP1yFRZywIDAQABo1MwUTAdBgNVHQ4EFgQUNy2X ++cLPh17QdR6raPKeoKLIm2QwHwYDVR0jBBgwFoAUNy2X+cLPh17QdR6raPKeoKLI +m2QwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAzvyP1QxKR8Ik +k7v3dzRl1gKdu6BtRL1zoFXeOjFOCVX9ORxcpCJItuTM4kEbJLhC0uFxn+zQ/Urs +JAc56gic4fCIcxlTNPr4VtAEUQKhfGG7XTRs8Cl2Rm7E7FwNiGjdLuiPI+G+ZZbl +TYmB5ILGzvI8LAOii17s5wFX84PehZ9gYgcgEvVBaU7lWF3WakR53Msf2bHkjk/r +NfaINeBltOwijhzb8pWf0XG2z4olJjg1qTOgr1gNseyTwMAFwFmeXQAoidoZfKya +DD+hY1/IgiUXi2pdmO+sMHtRBG5JdOi2cjSOcTx1xkWyb60PpW4uxKhduQPAiZRO +266P7J962Q== +-----END CERTIFICATE----- diff --git a/packages/docusaurus/src/webpack/__tests__/__fixtures__/host.key b/packages/docusaurus/src/webpack/__tests__/__fixtures__/host.key new file mode 100644 index 0000000000..1b47cc35e4 --- /dev/null +++ b/packages/docusaurus/src/webpack/__tests__/__fixtures__/host.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA7Cq2QW6rcZAm6MMo97aqkFi9dkXx97fW6vPEt2bgS9O6E+M/ +wXBIq1Dh3ud8sGP+CiEWa+7uIEwX9pRGyQo0Lkr7qZWSbsDh+RmdkiKUCiIUUTBo +pBjMjo7XF9KBM609GYoGlKYxv4adPbOMJcK/9VdJPz3NprIA1PHEqInJNnuKMMjB +MhNu1MZ7JwING/LYBOJ/Mve08XKAcyDdWBVPe2TOfcKhEmtBTKhnOuUicuAdVtDk +N34Ze4ZlifLo7wlQU7NNh7YDOYZz3JXB5QotuqtWkUgfpMSCWG90p4P4LExLzS+2 +sb7OC/jO0qYcKjaKAKjrA9IIyClF6VP1yFRZywIDAQABAoIBAHiHR+LW/2qC3ki2 +qWba8+udTnxZMCdzzJy2cjQDrf8k/Hd/6B7qFjxQmCXx0GIZdiJnRpEpLKCRFT3D +6Ohba8wgepXO/x/FEs7VsuRM/264e9P/t7ff7C3pWn8O8N+Vz3QETF17ADK2GfPO +eX0gCmXE+V3sRdOITwJerTYys904bo5CQsDQQENpcuYbZU2IYt9dw9XrTexaFwP1 +3ssOXCwpaW4kS95a6WQlwCqNTq49zqf3VGA3QG3JEdPPWhG+jEG2L4RxSosvo4wt +MYFqeXcS5sz7WOH1gtleGL2i6WKYuLl7Bo/CLokn1tgrXjGvNpeBFvZucC+L246f +e7iG+gkCgYEA+CcISFav/uwKNv3Sdp87kVpBAno8cZTiYvB15zAGaXuLyI/OuJNh +lcJBhtZSN94T/mgj+gXDafjmRr4i7Q4Pu+KG95JTk1FfWv/974NxbRNrrp+4PFKb +wxcM1cHuqq88mUPUX+k0eKPqDcuY6vHBPAV4ji1Wl+VXpREDvhKgAEUCgYEA86Kl +xnOf3TWbEaQRJx2mMnRYLyrEEPqEMgHWlzXdWl2E9LJDGGmOEbZLv6uNcx1uWJVP +AaoitmQNTl+rSsJY0TwqooX5zvT8po9MXUt8FvButJyYUOJZFTuLtLxFJqAzFipz +SaiYTrEBC76uqe/87AVm0wCdJN4ajcptyibaus8CgYEAnXSm3L+kjKxZDuufT4VZ +1rDd7ySAldFSlFTfewIOD4BFAc297YAWu1+3FEeJg8l2BkcuDMb7Z5J3Cww6PRBf +C2iBGzXNsfw/9Q3ZotBUeFGKUhMmY6BHFVLa4gdb2RG38cgISZM/qAzZxkcZkHo1 +klAmXpCGEXuEUUiqh0BqJcECgYEAv42Gt0QbUeoetL0BO3blP9AXsWX3Z73/h+3I +EXUpRy42JcmuVRhQuf5RCi7QdMyUAJPL3WwuBKcfixpO6+VnvYKHpuadZSlbJ32N +NeDufH6nG9vvKdD852O80OohmF/mKqxPnn8u2Nf0EY7ndvcYLV2F3aoi42S5Dfg1 +X/YyjSMCgYAg2fEisapxje98KZ4TPvOffJRF5PRG4H6UBQvxaWw9oUjVkGM6t10U +D6uOCYPkb+l3wBFTNAfScr22EnpW33Q5JOAfHBeE1oEoWGdMgp1C1V9ZQTIkjXyj +YE+lrsTFVoyY+dnLcZ4U7syVkzINk10GaAKjGXD0gtrqC+cQy8z1XQ== +-----END RSA PRIVATE KEY----- diff --git a/packages/docusaurus/src/webpack/__tests__/__fixtures__/invalid.crt b/packages/docusaurus/src/webpack/__tests__/__fixtures__/invalid.crt new file mode 100644 index 0000000000..bc56c4d894 --- /dev/null +++ b/packages/docusaurus/src/webpack/__tests__/__fixtures__/invalid.crt @@ -0,0 +1 @@ +Foo diff --git a/packages/docusaurus/src/webpack/__tests__/__fixtures__/invalid.key b/packages/docusaurus/src/webpack/__tests__/__fixtures__/invalid.key new file mode 100644 index 0000000000..bc56c4d894 --- /dev/null +++ b/packages/docusaurus/src/webpack/__tests__/__fixtures__/invalid.key @@ -0,0 +1 @@ +Foo diff --git a/packages/docusaurus/src/webpack/__tests__/server.test.ts b/packages/docusaurus/src/webpack/__tests__/server.test.ts index fd4254258f..b722f46f7d 100644 --- a/packages/docusaurus/src/webpack/__tests__/server.test.ts +++ b/packages/docusaurus/src/webpack/__tests__/server.test.ts @@ -13,7 +13,7 @@ import loadSetup from '../../server/__tests__/testUtils'; describe('webpack production config', () => { it('simple', async () => { - jest.spyOn(console, 'log').mockImplementation(); + jest.spyOn(console, 'log').mockImplementation(() => {}); const props = await loadSetup('simple'); const config = await createServerConfig({props}); const errors = webpack.validate(config); @@ -21,7 +21,7 @@ describe('webpack production config', () => { }); it('custom', async () => { - jest.spyOn(console, 'log').mockImplementation(); + jest.spyOn(console, 'log').mockImplementation(() => {}); const props = await loadSetup('custom'); const config = await createServerConfig({props}); const errors = webpack.validate(config); diff --git a/packages/docusaurus/src/webpack/__tests__/utils.test.ts b/packages/docusaurus/src/webpack/__tests__/utils.test.ts index 2b29331dbd..3b58b57d22 100644 --- a/packages/docusaurus/src/webpack/__tests__/utils.test.ts +++ b/packages/docusaurus/src/webpack/__tests__/utils.test.ts @@ -12,6 +12,7 @@ import { getCustomizableJSLoader, applyConfigureWebpack, applyConfigurePostCss, + getHttpsConfig, } from '../utils'; import type { ConfigureWebpackFn, @@ -297,3 +298,65 @@ describe('extending PostCSS', () => { ]); }); }); + +describe('getHttpsConfig', () => { + const originalEnv = process.env; + + beforeEach(() => { + jest.resetModules(); + process.env = {...originalEnv}; + }); + + afterAll(() => { + process.env = originalEnv; + }); + + it('returns true for HTTPS not env', async () => { + await expect(getHttpsConfig()).resolves.toBe(false); + }); + + it('returns true for HTTPS in env', async () => { + process.env.HTTPS = 'true'; + await expect(getHttpsConfig()).resolves.toBe(true); + }); + + it('returns custom certs if they are in env', async () => { + process.env.HTTPS = 'true'; + process.env.SSL_CRT_FILE = path.join(__dirname, '__fixtures__/host.crt'); + process.env.SSL_KEY_FILE = path.join(__dirname, '__fixtures__/host.key'); + await expect(getHttpsConfig()).resolves.toEqual({ + key: expect.any(Buffer), + cert: expect.any(Buffer), + }); + }); + + it("throws if file doesn't exist", async () => { + process.env.HTTPS = 'true'; + process.env.SSL_CRT_FILE = path.join( + __dirname, + '__fixtures__/nonexistent.crt', + ); + process.env.SSL_KEY_FILE = path.join(__dirname, '__fixtures__/host.key'); + await expect(getHttpsConfig()).rejects.toThrowErrorMatchingInlineSnapshot( + `"You specified SSL_CRT_FILE in your env, but the file \\"/packages/docusaurus/src/webpack/__tests__/__fixtures__/nonexistent.crt\\" can't be found."`, + ); + }); + + it('throws for invalid key', async () => { + process.env.HTTPS = 'true'; + process.env.SSL_CRT_FILE = path.join(__dirname, '__fixtures__/host.crt'); + process.env.SSL_KEY_FILE = path.join(__dirname, '__fixtures__/invalid.key'); + await expect(getHttpsConfig()).rejects.toThrowError( + /The certificate key .*[/\\]__fixtures__[/\\]invalid\.key is invalid/, + ); + }); + + it('throws for invalid cert', async () => { + process.env.HTTPS = 'true'; + process.env.SSL_CRT_FILE = path.join(__dirname, '__fixtures__/invalid.crt'); + process.env.SSL_KEY_FILE = path.join(__dirname, '__fixtures__/host.key'); + await expect(getHttpsConfig()).rejects.toThrowError( + /The certificate .*[/\\]__fixtures__[/\\]invalid\.crt is invalid/, + ); + }); +}); diff --git a/website/src/utils/jsUtils.ts b/website/src/utils/jsUtils.ts index c389b4e397..868a32d061 100644 --- a/website/src/utils/jsUtils.ts +++ b/website/src/utils/jsUtils.ts @@ -5,11 +5,6 @@ * LICENSE file in the root directory of this source tree. */ -// Inspired by https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore#_difference -export function difference(...arrays: T[][]): T[] { - return arrays.reduce((a, b) => a.filter((c) => !b.includes(c))); -} - // Inspired by https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore#_sortby-and-_orderby export function sortBy( array: T[],