diff --git a/.eslintrc.js b/.eslintrc.js index 24869c41ec0b..dbf43124daaf 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -87,6 +87,7 @@ module.exports = { {functions: false, classes: false, variables: true}, ], 'no-unused-vars': OFF, + 'no-nested-ternary': WARNING, '@typescript-eslint/no-unused-vars': [ERROR, {argsIgnorePattern: '^_'}], '@typescript-eslint/ban-ts-comment': [ ERROR, diff --git a/packages/docusaurus-plugin-content-docs/package.json b/packages/docusaurus-plugin-content-docs/package.json index b009540e134c..b24a95022e48 100644 --- a/packages/docusaurus-plugin-content-docs/package.json +++ b/packages/docusaurus-plugin-content-docs/package.json @@ -14,6 +14,7 @@ "devDependencies": { "@docusaurus/module-type-aliases": "^2.0.0-alpha.61", "@types/hapi__joi": "^17.1.2", + "@types/picomatch": "^2.2.1", "commander": "^5.0.0", "picomatch": "^2.1.1" }, @@ -37,7 +38,9 @@ "lodash.pickby": "^4.6.0", "lodash.sortby": "^4.6.0", "remark-admonitions": "^1.2.1", - "shelljs": "^0.8.4" + "shelljs": "^0.8.4", + "utility-types": "^3.10.0", + "webpack": "^4.41.2" }, "peerDependencies": { "react": "^16.8.4", diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/empty-site/sidebars.json b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/empty-site/sidebars.json new file mode 100644 index 000000000000..0967ef424bce --- /dev/null +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/empty-site/sidebars.json @@ -0,0 +1 @@ +{} diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/versioned-site/versioned_sidebars/version-withSlugs-sidebars.json b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/versioned-site/versioned_sidebars/version-withSlugs-sidebars.json new file mode 100644 index 000000000000..ca469922c09c --- /dev/null +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/versioned-site/versioned_sidebars/version-withSlugs-sidebars.json @@ -0,0 +1,5 @@ +{ + "version-1.0.1/docs": { + "Test": ["version-withSlugs/rootAbsoluteSlug"] + } +} diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/version.test.ts.snap b/packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/cli.test.ts.snap similarity index 100% rename from packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/version.test.ts.snap rename to packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/cli.test.ts.snap diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/index.test.ts.snap b/packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/index.test.ts.snap index 93ef7be61ea0..8a93f780e515 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/index.test.ts.snap +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/index.test.ts.snap @@ -10,14 +10,12 @@ Object { "collapsed": true, "items": Array [ Object { - "href": "/docs/foo/bar", - "label": "Bar", - "type": "link", + "id": "foo/bar", + "type": "doc", }, Object { - "href": "/docs/foo/bazSlug.html", - "label": "baz", - "type": "link", + "id": "foo/baz", + "type": "doc", }, ], "label": "foo", @@ -29,9 +27,8 @@ Object { "type": "link", }, Object { - "href": "/docs/", - "label": "Hello, World !", - "type": "link", + "id": "hello", + "type": "ref", }, ], "label": "Test", @@ -41,9 +38,8 @@ Object { "collapsed": true, "items": Array [ Object { - "href": "/docs/", - "label": "Hello, World !", - "type": "link", + "id": "hello", + "type": "doc", }, ], "label": "Guides", @@ -57,7 +53,6 @@ exports[`simple website content 2`] = ` Object { "pluginName": Object { "pluginId": Object { - "latestVersionName": null, "path": "/docs", "versions": Array [ Object { @@ -115,8 +110,10 @@ Object { "path": "/docs/tryToEscapeSlug", }, ], + "isLast": true, + "label": "Next", "mainDocId": "hello", - "name": null, + "name": "current", "path": "/docs", }, ], @@ -127,64 +124,6 @@ Object { exports[`simple website content: data 1`] = ` Object { - "docs-route-ff2.json": "{ - \\"docsSidebars\\": { - \\"docs\\": [ - { - \\"collapsed\\": true, - \\"type\\": \\"category\\", - \\"label\\": \\"Test\\", - \\"items\\": [ - { - \\"collapsed\\": true, - \\"type\\": \\"category\\", - \\"label\\": \\"foo\\", - \\"items\\": [ - { - \\"type\\": \\"link\\", - \\"label\\": \\"Bar\\", - \\"href\\": \\"/docs/foo/bar\\" - }, - { - \\"type\\": \\"link\\", - \\"label\\": \\"baz\\", - \\"href\\": \\"/docs/foo/bazSlug.html\\" - } - ] - }, - { - \\"type\\": \\"link\\", - \\"label\\": \\"Github\\", - \\"href\\": \\"https://github.com\\" - }, - { - \\"type\\": \\"link\\", - \\"label\\": \\"Hello, World !\\", - \\"href\\": \\"/docs/\\" - } - ] - }, - { - \\"collapsed\\": true, - \\"type\\": \\"category\\", - \\"label\\": \\"Guides\\", - \\"items\\": [ - { - \\"type\\": \\"link\\", - \\"label\\": \\"Hello, World !\\", - \\"href\\": \\"/docs/\\" - } - ] - } - ] - }, - \\"permalinkToSidebar\\": { - \\"/docs/\\": \\"docs\\", - \\"/docs/foo/bar\\": \\"docs\\", - \\"/docs/foo/bazSlug.html\\": \\"docs\\" - }, - \\"version\\": null -}", "site-docs-foo-bar-md-8c2.json": "{ \\"unversionedId\\": \\"foo/bar\\", \\"id\\": \\"foo/bar\\", @@ -194,6 +133,7 @@ Object { \\"source\\": \\"@site/docs/foo/bar.md\\", \\"slug\\": \\"/foo/bar\\", \\"permalink\\": \\"/docs/foo/bar\\", + \\"version\\": \\"current\\", \\"sidebar\\": \\"docs\\", \\"next\\": { \\"title\\": \\"baz\\", @@ -209,6 +149,7 @@ Object { \\"source\\": \\"@site/docs/foo/baz.md\\", \\"slug\\": \\"/foo/bazSlug.html\\", \\"permalink\\": \\"/docs/foo/bazSlug.html\\", + \\"version\\": \\"current\\", \\"sidebar\\": \\"docs\\", \\"previous\\": { \\"title\\": \\"Bar\\", @@ -228,6 +169,7 @@ Object { \\"source\\": \\"@site/docs/hello.md\\", \\"slug\\": \\"/\\", \\"permalink\\": \\"/docs/\\", + \\"version\\": \\"current\\", \\"sidebar\\": \\"docs\\", \\"previous\\": { \\"title\\": \\"baz\\", @@ -243,7 +185,8 @@ Object { \\"source\\": \\"@site/docs/ipsum.md\\", \\"slug\\": \\"/ipsum\\", \\"permalink\\": \\"/docs/ipsum\\", - \\"editUrl\\": null + \\"editUrl\\": null, + \\"version\\": \\"current\\" }", "site-docs-lorem-md-b27.json": "{ \\"unversionedId\\": \\"lorem\\", @@ -254,7 +197,8 @@ Object { \\"source\\": \\"@site/docs/lorem.md\\", \\"slug\\": \\"/lorem\\", \\"permalink\\": \\"/docs/lorem\\", - \\"editUrl\\": \\"https://github.com/customUrl/docs/lorem.md\\" + \\"editUrl\\": \\"https://github.com/customUrl/docs/lorem.md\\", + \\"version\\": \\"current\\" }", "site-docs-root-absolute-slug-md-db5.json": "{ \\"unversionedId\\": \\"rootAbsoluteSlug\\", @@ -264,7 +208,8 @@ Object { \\"description\\": \\"Lorem\\", \\"source\\": \\"@site/docs/rootAbsoluteSlug.md\\", \\"slug\\": \\"/rootAbsoluteSlug\\", - \\"permalink\\": \\"/docs/rootAbsoluteSlug\\" + \\"permalink\\": \\"/docs/rootAbsoluteSlug\\", + \\"version\\": \\"current\\" }", "site-docs-root-relative-slug-md-3dd.json": "{ \\"unversionedId\\": \\"rootRelativeSlug\\", @@ -274,7 +219,8 @@ Object { \\"description\\": \\"Lorem\\", \\"source\\": \\"@site/docs/rootRelativeSlug.md\\", \\"slug\\": \\"/rootRelativeSlug\\", - \\"permalink\\": \\"/docs/rootRelativeSlug\\" + \\"permalink\\": \\"/docs/rootRelativeSlug\\", + \\"version\\": \\"current\\" }", "site-docs-root-resolved-slug-md-4d1.json": "{ \\"unversionedId\\": \\"rootResolvedSlug\\", @@ -284,7 +230,8 @@ Object { \\"description\\": \\"Lorem\\", \\"source\\": \\"@site/docs/rootResolvedSlug.md\\", \\"slug\\": \\"/hey/rootResolvedSlug\\", - \\"permalink\\": \\"/docs/hey/rootResolvedSlug\\" + \\"permalink\\": \\"/docs/hey/rootResolvedSlug\\", + \\"version\\": \\"current\\" }", "site-docs-root-try-to-escape-slug-md-9ee.json": "{ \\"unversionedId\\": \\"rootTryToEscapeSlug\\", @@ -294,7 +241,8 @@ Object { \\"description\\": \\"Lorem\\", \\"source\\": \\"@site/docs/rootTryToEscapeSlug.md\\", \\"slug\\": \\"/rootTryToEscapeSlug\\", - \\"permalink\\": \\"/docs/rootTryToEscapeSlug\\" + \\"permalink\\": \\"/docs/rootTryToEscapeSlug\\", + \\"version\\": \\"current\\" }", "site-docs-slugs-absolute-slug-md-4e8.json": "{ \\"unversionedId\\": \\"slugs/absoluteSlug\\", @@ -304,7 +252,8 @@ Object { \\"description\\": \\"Lorem\\", \\"source\\": \\"@site/docs/slugs/absoluteSlug.md\\", \\"slug\\": \\"/absoluteSlug\\", - \\"permalink\\": \\"/docs/absoluteSlug\\" + \\"permalink\\": \\"/docs/absoluteSlug\\", + \\"version\\": \\"current\\" }", "site-docs-slugs-relative-slug-md-d1c.json": "{ \\"unversionedId\\": \\"slugs/relativeSlug\\", @@ -314,7 +263,8 @@ Object { \\"description\\": \\"Lorem\\", \\"source\\": \\"@site/docs/slugs/relativeSlug.md\\", \\"slug\\": \\"/slugs/relativeSlug\\", - \\"permalink\\": \\"/docs/slugs/relativeSlug\\" + \\"permalink\\": \\"/docs/slugs/relativeSlug\\", + \\"version\\": \\"current\\" }", "site-docs-slugs-resolved-slug-md-02b.json": "{ \\"unversionedId\\": \\"slugs/resolvedSlug\\", @@ -324,7 +274,8 @@ Object { \\"description\\": \\"Lorem\\", \\"source\\": \\"@site/docs/slugs/resolvedSlug.md\\", \\"slug\\": \\"/slugs/hey/resolvedSlug\\", - \\"permalink\\": \\"/docs/slugs/hey/resolvedSlug\\" + \\"permalink\\": \\"/docs/slugs/hey/resolvedSlug\\", + \\"version\\": \\"current\\" }", "site-docs-slugs-try-to-escape-slug-md-70d.json": "{ \\"unversionedId\\": \\"slugs/tryToEscapeSlug\\", @@ -334,7 +285,66 @@ Object { \\"description\\": \\"Lorem\\", \\"source\\": \\"@site/docs/slugs/tryToEscapeSlug.md\\", \\"slug\\": \\"/tryToEscapeSlug\\", - \\"permalink\\": \\"/docs/tryToEscapeSlug\\" + \\"permalink\\": \\"/docs/tryToEscapeSlug\\", + \\"version\\": \\"current\\" +}", + "version-current-metadata-prop-751.json": "{ + \\"version\\": \\"current\\", + \\"docsSidebars\\": { + \\"docs\\": [ + { + \\"collapsed\\": true, + \\"type\\": \\"category\\", + \\"label\\": \\"Test\\", + \\"items\\": [ + { + \\"collapsed\\": true, + \\"type\\": \\"category\\", + \\"label\\": \\"foo\\", + \\"items\\": [ + { + \\"type\\": \\"link\\", + \\"label\\": \\"Bar\\", + \\"href\\": \\"/docs/foo/bar\\" + }, + { + \\"type\\": \\"link\\", + \\"label\\": \\"baz\\", + \\"href\\": \\"/docs/foo/bazSlug.html\\" + } + ] + }, + { + \\"type\\": \\"link\\", + \\"label\\": \\"Github\\", + \\"href\\": \\"https://github.com\\" + }, + { + \\"type\\": \\"link\\", + \\"label\\": \\"Hello, World !\\", + \\"href\\": \\"/docs/\\" + } + ] + }, + { + \\"collapsed\\": true, + \\"type\\": \\"category\\", + \\"label\\": \\"Guides\\", + \\"items\\": [ + { + \\"type\\": \\"link\\", + \\"label\\": \\"Hello, World !\\", + \\"href\\": \\"/docs/\\" + } + ] + } + ] + }, + \\"permalinkToSidebar\\": { + \\"/docs/foo/bar\\": \\"docs\\", + \\"/docs/foo/bazSlug.html\\": \\"docs\\", + \\"/docs/\\": \\"docs\\" + } }", } `; @@ -343,7 +353,6 @@ exports[`simple website content: global data 1`] = ` Object { "pluginName": Object { "pluginId": Object { - "latestVersionName": null, "path": "/docs", "versions": Array [ Object { @@ -401,8 +410,10 @@ Object { "path": "/docs/tryToEscapeSlug", }, ], + "isLast": true, + "label": "Next", "mainDocId": "hello", - "name": null, + "name": "current", "path": "/docs", }, ], @@ -417,10 +428,10 @@ Array [ "component": "@theme/DocPage", "exact": false, "modules": Object { - "docsMetadata": "~docs/docs-route-ff2.json", + "versionMetadata": "~docs/version-current-metadata-prop-751.json", }, "path": "/docs", - "priority": undefined, + "priority": -1, "routes": Array [ Object { "component": "@theme/DocItem", @@ -532,7 +543,10 @@ Array [ `; exports[`site with wrong sidebar file 1`] = ` -"Bad sidebars file. The document id 'goku' was used in the sidebar, but no document with this id could be found. +"Bad sidebars file. +These sidebar document ids do not exist: +- goku\`, + Available document ids= - foo/bar - foo/baz @@ -549,93 +563,30 @@ Available document ids= - slugs/tryToEscapeSlug" `; -exports[`versioned website (community) content: all sidebars 1`] = ` +exports[`versioned website (community) content: 100 version sidebars 1`] = ` Object { - "community": Array [ - Object { - "href": "/community/next/team", - "label": "team", - "type": "link", - }, - ], "version-1.0.0/community": Array [ Object { - "href": "/community/team", - "label": "team", - "type": "link", + "id": "version-1.0.0/team", + "type": "doc", }, ], } `; -exports[`versioned website (community) content: base metadata for latest version 1`] = ` -Object { - "docsSidebars": Object { - "version-1.0.0/community": Array [ - Object { - "href": "/community/team", - "label": "team", - "type": "link", - }, - ], - }, - "permalinkToSidebar": Object { - "/community/team": "version-1.0.0/community", - }, - "version": "1.0.0", -} -`; - -exports[`versioned website (community) content: base metadata for next version 1`] = ` +exports[`versioned website (community) content: current version sidebars 1`] = ` Object { - "docsSidebars": Object { - "community": Array [ - Object { - "href": "/community/next/team", - "label": "team", - "type": "link", - }, - ], - }, - "permalinkToSidebar": Object { - "/community/next/team": "community", - }, - "version": "next", + "community": Array [ + Object { + "id": "team", + "type": "doc", + }, + ], } `; exports[`versioned website (community) content: data 1`] = ` Object { - "community-next-route-f11.json": "{ - \\"docsSidebars\\": { - \\"community\\": [ - { - \\"type\\": \\"link\\", - \\"label\\": \\"team\\", - \\"href\\": \\"/community/next/team\\" - } - ] - }, - \\"permalinkToSidebar\\": { - \\"/community/next/team\\": \\"community\\" - }, - \\"version\\": \\"next\\" -}", - "community-route-aa2.json": "{ - \\"docsSidebars\\": { - \\"version-1.0.0/community\\": [ - { - \\"type\\": \\"link\\", - \\"label\\": \\"team\\", - \\"href\\": \\"/community/team\\" - } - ] - }, - \\"permalinkToSidebar\\": { - \\"/community/team\\": \\"version-1.0.0/community\\" - }, - \\"version\\": \\"1.0.0\\" -}", "site-community-team-md-9d8.json": "{ \\"unversionedId\\": \\"team\\", \\"id\\": \\"team\\", @@ -645,7 +596,7 @@ Object { \\"source\\": \\"@site/community/team.md\\", \\"slug\\": \\"/team\\", \\"permalink\\": \\"/community/next/team\\", - \\"version\\": \\"next\\", + \\"version\\": \\"current\\", \\"sidebar\\": \\"community\\" }", "site-community-versioned-docs-version-1-0-0-team-md-359.json": "{ @@ -659,6 +610,36 @@ Object { \\"permalink\\": \\"/community/team\\", \\"version\\": \\"1.0.0\\", \\"sidebar\\": \\"version-1.0.0/community\\" +}", + "version-1-0-0-metadata-prop-608.json": "{ + \\"version\\": \\"1.0.0\\", + \\"docsSidebars\\": { + \\"version-1.0.0/community\\": [ + { + \\"type\\": \\"link\\", + \\"label\\": \\"team\\", + \\"href\\": \\"/community/team\\" + } + ] + }, + \\"permalinkToSidebar\\": { + \\"/community/team\\": \\"version-1.0.0/community\\" + } +}", + "version-current-metadata-prop-751.json": "{ + \\"version\\": \\"current\\", + \\"docsSidebars\\": { + \\"community\\": [ + { + \\"type\\": \\"link\\", + \\"label\\": \\"team\\", + \\"href\\": \\"/community/next/team\\" + } + ] + }, + \\"permalinkToSidebar\\": { + \\"/community/next/team\\": \\"community\\" + } }", } `; @@ -667,7 +648,6 @@ exports[`versioned website (community) content: global data 1`] = ` Object { "pluginName": Object { "pluginId": Object { - "latestVersionName": "1.0.0", "path": "/community", "versions": Array [ Object { @@ -677,8 +657,10 @@ Object { "path": "/community/next/team", }, ], + "isLast": false, + "label": "Next", "mainDocId": "team", - "name": "next", + "name": "current", "path": "/community/next", }, Object { @@ -688,6 +670,8 @@ Object { "path": "/community/team", }, ], + "isLast": true, + "label": "1.0.0", "mainDocId": "team", "name": "1.0.0", "path": "/community", @@ -704,7 +688,7 @@ Array [ "component": "@theme/DocPage", "exact": false, "modules": Object { - "docsMetadata": "~docs/community-next-route-f11.json", + "versionMetadata": "~docs/version-current-metadata-prop-751.json", }, "path": "/community/next", "priority": undefined, @@ -723,7 +707,7 @@ Array [ "component": "@theme/DocPage", "exact": false, "modules": Object { - "docsMetadata": "~docs/community-route-aa2.json", + "versionMetadata": "~docs/version-1-0-0-metadata-prop-608.json", }, "path": "/community", "priority": -1, @@ -741,27 +725,19 @@ Array [ ] `; -exports[`versioned website (community) content: sidebars needed for each version 1`] = ` -Object { - "1.0.0": Set { - "version-1.0.0/community", - }, - "next": Set { - "community", - }, -} -`; - -exports[`versioned website content: all sidebars 1`] = ` +exports[`versioned website content: 100 version sidebars 1`] = ` Object { - "docs": Array [ + "version-1.0.0/docs": Array [ Object { "collapsed": true, "items": Array [ Object { - "href": "/docs/next/foo/barSlug", - "label": "bar", - "type": "link", + "id": "version-1.0.0/foo/bar", + "type": "doc", + }, + Object { + "id": "version-1.0.0/foo/baz", + "type": "doc", }, ], "label": "Test", @@ -771,28 +747,26 @@ Object { "collapsed": true, "items": Array [ Object { - "href": "/docs/next/", - "label": "hello", - "type": "link", + "id": "version-1.0.0/hello", + "type": "doc", }, ], "label": "Guides", "type": "category", }, ], - "version-1.0.0/docs": Array [ +} +`; + +exports[`versioned website content: 101 version sidebars 1`] = ` +Object { + "version-1.0.1/docs": Array [ Object { "collapsed": true, "items": Array [ Object { - "href": "/docs/1.0.0/foo/barSlug", - "label": "bar", - "type": "link", - }, - Object { - "href": "/docs/1.0.0/foo/baz", - "label": "baz", - "type": "link", + "id": "version-1.0.1/foo/bar", + "type": "doc", }, ], "label": "Test", @@ -802,23 +776,26 @@ Object { "collapsed": true, "items": Array [ Object { - "href": "/docs/1.0.0/", - "label": "hello", - "type": "link", + "id": "version-1.0.1/hello", + "type": "doc", }, ], "label": "Guides", "type": "category", }, ], - "version-1.0.1/docs": Array [ +} +`; + +exports[`versioned website content: current version sidebars 1`] = ` +Object { + "docs": Array [ Object { "collapsed": true, "items": Array [ Object { - "href": "/docs/foo/bar", - "label": "bar", - "type": "link", + "id": "foo/bar", + "type": "doc", }, ], "label": "Test", @@ -828,9 +805,8 @@ Object { "collapsed": true, "items": Array [ Object { - "href": "/docs/", - "label": "hello", - "type": "link", + "id": "hello", + "type": "doc", }, ], "label": "Guides", @@ -840,244 +816,8 @@ Object { } `; -exports[`versioned website content: base metadata for first version 1`] = ` -Object { - "docsSidebars": Object { - "version-1.0.0/docs": Array [ - Object { - "collapsed": true, - "items": Array [ - Object { - "href": "/docs/1.0.0/foo/barSlug", - "label": "bar", - "type": "link", - }, - Object { - "href": "/docs/1.0.0/foo/baz", - "label": "baz", - "type": "link", - }, - ], - "label": "Test", - "type": "category", - }, - Object { - "collapsed": true, - "items": Array [ - Object { - "href": "/docs/1.0.0/", - "label": "hello", - "type": "link", - }, - ], - "label": "Guides", - "type": "category", - }, - ], - }, - "permalinkToSidebar": Object { - "/docs/1.0.0/": "version-1.0.0/docs", - "/docs/1.0.0/foo/barSlug": "version-1.0.0/docs", - "/docs/1.0.0/foo/baz": "version-1.0.0/docs", - }, - "version": "1.0.0", -} -`; - -exports[`versioned website content: base metadata for latest version 1`] = ` -Object { - "docsSidebars": Object { - "version-1.0.1/docs": Array [ - Object { - "collapsed": true, - "items": Array [ - Object { - "href": "/docs/foo/bar", - "label": "bar", - "type": "link", - }, - ], - "label": "Test", - "type": "category", - }, - Object { - "collapsed": true, - "items": Array [ - Object { - "href": "/docs/", - "label": "hello", - "type": "link", - }, - ], - "label": "Guides", - "type": "category", - }, - ], - }, - "permalinkToSidebar": Object { - "/docs/": "version-1.0.1/docs", - "/docs/foo/bar": "version-1.0.1/docs", - }, - "version": "1.0.1", -} -`; - -exports[`versioned website content: base metadata for next version 1`] = ` -Object { - "docsSidebars": Object { - "docs": Array [ - Object { - "collapsed": true, - "items": Array [ - Object { - "href": "/docs/next/foo/barSlug", - "label": "bar", - "type": "link", - }, - ], - "label": "Test", - "type": "category", - }, - Object { - "collapsed": true, - "items": Array [ - Object { - "href": "/docs/next/", - "label": "hello", - "type": "link", - }, - ], - "label": "Guides", - "type": "category", - }, - ], - }, - "permalinkToSidebar": Object { - "/docs/next/": "docs", - "/docs/next/foo/barSlug": "docs", - }, - "version": "next", -} -`; - exports[`versioned website content: data 1`] = ` Object { - "docs-1-0-0-route-660.json": "{ - \\"docsSidebars\\": { - \\"version-1.0.0/docs\\": [ - { - \\"collapsed\\": true, - \\"type\\": \\"category\\", - \\"label\\": \\"Test\\", - \\"items\\": [ - { - \\"type\\": \\"link\\", - \\"label\\": \\"bar\\", - \\"href\\": \\"/docs/1.0.0/foo/barSlug\\" - }, - { - \\"type\\": \\"link\\", - \\"label\\": \\"baz\\", - \\"href\\": \\"/docs/1.0.0/foo/baz\\" - } - ] - }, - { - \\"collapsed\\": true, - \\"type\\": \\"category\\", - \\"label\\": \\"Guides\\", - \\"items\\": [ - { - \\"type\\": \\"link\\", - \\"label\\": \\"hello\\", - \\"href\\": \\"/docs/1.0.0/\\" - } - ] - } - ] - }, - \\"permalinkToSidebar\\": { - \\"/docs/1.0.0/\\": \\"version-1.0.0/docs\\", - \\"/docs/1.0.0/foo/barSlug\\": \\"version-1.0.0/docs\\", - \\"/docs/1.0.0/foo/baz\\": \\"version-1.0.0/docs\\" - }, - \\"version\\": \\"1.0.0\\" -}", - "docs-next-route-1c8.json": "{ - \\"docsSidebars\\": { - \\"docs\\": [ - { - \\"collapsed\\": true, - \\"type\\": \\"category\\", - \\"label\\": \\"Test\\", - \\"items\\": [ - { - \\"type\\": \\"link\\", - \\"label\\": \\"bar\\", - \\"href\\": \\"/docs/next/foo/barSlug\\" - } - ] - }, - { - \\"collapsed\\": true, - \\"type\\": \\"category\\", - \\"label\\": \\"Guides\\", - \\"items\\": [ - { - \\"type\\": \\"link\\", - \\"label\\": \\"hello\\", - \\"href\\": \\"/docs/next/\\" - } - ] - } - ] - }, - \\"permalinkToSidebar\\": { - \\"/docs/next/\\": \\"docs\\", - \\"/docs/next/foo/barSlug\\": \\"docs\\" - }, - \\"version\\": \\"next\\" -}", - "docs-route-ff2.json": "{ - \\"docsSidebars\\": { - \\"version-1.0.1/docs\\": [ - { - \\"collapsed\\": true, - \\"type\\": \\"category\\", - \\"label\\": \\"Test\\", - \\"items\\": [ - { - \\"type\\": \\"link\\", - \\"label\\": \\"bar\\", - \\"href\\": \\"/docs/foo/bar\\" - } - ] - }, - { - \\"collapsed\\": true, - \\"type\\": \\"category\\", - \\"label\\": \\"Guides\\", - \\"items\\": [ - { - \\"type\\": \\"link\\", - \\"label\\": \\"hello\\", - \\"href\\": \\"/docs/\\" - } - ] - } - ] - }, - \\"permalinkToSidebar\\": { - \\"/docs/\\": \\"version-1.0.1/docs\\", - \\"/docs/foo/bar\\": \\"version-1.0.1/docs\\" - }, - \\"version\\": \\"1.0.1\\" -}", - "docs-with-slugs-route-335.json": "{ - \\"docsSidebars\\": {}, - \\"permalinkToSidebar\\": {}, - \\"version\\": \\"withSlugs\\" -}", "site-docs-foo-bar-md-8c2.json": "{ \\"unversionedId\\": \\"foo/bar\\", \\"id\\": \\"foo/bar\\", @@ -1087,7 +827,7 @@ Object { \\"source\\": \\"@site/docs/foo/bar.md\\", \\"slug\\": \\"/foo/barSlug\\", \\"permalink\\": \\"/docs/next/foo/barSlug\\", - \\"version\\": \\"next\\", + \\"version\\": \\"current\\", \\"sidebar\\": \\"docs\\", \\"next\\": { \\"title\\": \\"hello\\", @@ -1103,7 +843,7 @@ Object { \\"source\\": \\"@site/docs/hello.md\\", \\"slug\\": \\"/\\", \\"permalink\\": \\"/docs/next/\\", - \\"version\\": \\"next\\", + \\"version\\": \\"current\\", \\"sidebar\\": \\"docs\\", \\"previous\\": { \\"title\\": \\"bar\\", @@ -1119,7 +859,7 @@ Object { \\"source\\": \\"@site/docs/slugs/absoluteSlug.md\\", \\"slug\\": \\"/absoluteSlug\\", \\"permalink\\": \\"/docs/next/absoluteSlug\\", - \\"version\\": \\"next\\" + \\"version\\": \\"current\\" }", "site-docs-slugs-relative-slug-md-d1c.json": "{ \\"unversionedId\\": \\"slugs/relativeSlug\\", @@ -1130,7 +870,7 @@ Object { \\"source\\": \\"@site/docs/slugs/relativeSlug.md\\", \\"slug\\": \\"/slugs/relativeSlug\\", \\"permalink\\": \\"/docs/next/slugs/relativeSlug\\", - \\"version\\": \\"next\\" + \\"version\\": \\"current\\" }", "site-docs-slugs-resolved-slug-md-02b.json": "{ \\"unversionedId\\": \\"slugs/resolvedSlug\\", @@ -1141,7 +881,7 @@ Object { \\"source\\": \\"@site/docs/slugs/resolvedSlug.md\\", \\"slug\\": \\"/slugs/hey/resolvedSlug\\", \\"permalink\\": \\"/docs/next/slugs/hey/resolvedSlug\\", - \\"version\\": \\"next\\" + \\"version\\": \\"current\\" }", "site-docs-slugs-try-to-escape-slug-md-70d.json": "{ \\"unversionedId\\": \\"slugs/tryToEscapeSlug\\", @@ -1152,7 +892,7 @@ Object { \\"source\\": \\"@site/docs/slugs/tryToEscapeSlug.md\\", \\"slug\\": \\"/tryToEscapeSlug\\", \\"permalink\\": \\"/docs/next/tryToEscapeSlug\\", - \\"version\\": \\"next\\" + \\"version\\": \\"current\\" }", "site-versioned-docs-version-1-0-0-foo-bar-md-7a6.json": "{ \\"unversionedId\\": \\"foo/bar\\", @@ -1247,7 +987,8 @@ Object { \\"source\\": \\"@site/versioned_docs/version-withSlugs/rootAbsoluteSlug.md\\", \\"slug\\": \\"/rootAbsoluteSlug\\", \\"permalink\\": \\"/docs/withSlugs/rootAbsoluteSlug\\", - \\"version\\": \\"withSlugs\\" + \\"version\\": \\"withSlugs\\", + \\"sidebar\\": \\"version-1.0.1/docs\\" }", "site-versioned-docs-version-with-slugs-root-relative-slug-md-32a.json": "{ \\"unversionedId\\": \\"rootRelativeSlug\\", @@ -1325,6 +1066,139 @@ Object { \\"slug\\": \\"/tryToEscapeSlug\\", \\"permalink\\": \\"/docs/withSlugs/tryToEscapeSlug\\", \\"version\\": \\"withSlugs\\" +}", + "version-1-0-0-metadata-prop-608.json": "{ + \\"version\\": \\"1.0.0\\", + \\"docsSidebars\\": { + \\"version-1.0.0/docs\\": [ + { + \\"collapsed\\": true, + \\"type\\": \\"category\\", + \\"label\\": \\"Test\\", + \\"items\\": [ + { + \\"type\\": \\"link\\", + \\"label\\": \\"bar\\", + \\"href\\": \\"/docs/1.0.0/foo/barSlug\\" + }, + { + \\"type\\": \\"link\\", + \\"label\\": \\"baz\\", + \\"href\\": \\"/docs/1.0.0/foo/baz\\" + } + ] + }, + { + \\"collapsed\\": true, + \\"type\\": \\"category\\", + \\"label\\": \\"Guides\\", + \\"items\\": [ + { + \\"type\\": \\"link\\", + \\"label\\": \\"hello\\", + \\"href\\": \\"/docs/1.0.0/\\" + } + ] + } + ] + }, + \\"permalinkToSidebar\\": { + \\"/docs/1.0.0/foo/barSlug\\": \\"version-1.0.0/docs\\", + \\"/docs/1.0.0/foo/baz\\": \\"version-1.0.0/docs\\", + \\"/docs/1.0.0/\\": \\"version-1.0.0/docs\\" + } +}", + "version-1-0-1-metadata-prop-e87.json": "{ + \\"version\\": \\"1.0.1\\", + \\"docsSidebars\\": { + \\"version-1.0.1/docs\\": [ + { + \\"collapsed\\": true, + \\"type\\": \\"category\\", + \\"label\\": \\"Test\\", + \\"items\\": [ + { + \\"type\\": \\"link\\", + \\"label\\": \\"bar\\", + \\"href\\": \\"/docs/foo/bar\\" + } + ] + }, + { + \\"collapsed\\": true, + \\"type\\": \\"category\\", + \\"label\\": \\"Guides\\", + \\"items\\": [ + { + \\"type\\": \\"link\\", + \\"label\\": \\"hello\\", + \\"href\\": \\"/docs/\\" + } + ] + } + ] + }, + \\"permalinkToSidebar\\": { + \\"/docs/foo/bar\\": \\"version-1.0.1/docs\\", + \\"/docs/\\": \\"version-1.0.1/docs\\" + } +}", + "version-current-metadata-prop-751.json": "{ + \\"version\\": \\"current\\", + \\"docsSidebars\\": { + \\"docs\\": [ + { + \\"collapsed\\": true, + \\"type\\": \\"category\\", + \\"label\\": \\"Test\\", + \\"items\\": [ + { + \\"type\\": \\"link\\", + \\"label\\": \\"bar\\", + \\"href\\": \\"/docs/next/foo/barSlug\\" + } + ] + }, + { + \\"collapsed\\": true, + \\"type\\": \\"category\\", + \\"label\\": \\"Guides\\", + \\"items\\": [ + { + \\"type\\": \\"link\\", + \\"label\\": \\"hello\\", + \\"href\\": \\"/docs/next/\\" + } + ] + } + ] + }, + \\"permalinkToSidebar\\": { + \\"/docs/next/foo/barSlug\\": \\"docs\\", + \\"/docs/next/\\": \\"docs\\" + } +}", + "version-with-slugs-metadata-prop-2bf.json": "{ + \\"version\\": \\"withSlugs\\", + \\"docsSidebars\\": { + \\"version-1.0.1/docs\\": [ + { + \\"collapsed\\": true, + \\"type\\": \\"category\\", + \\"label\\": \\"Test\\", + \\"items\\": [ + { + \\"type\\": \\"link\\", + \\"label\\": \\"rootAbsoluteSlug\\", + \\"href\\": \\"/docs/withSlugs/rootAbsoluteSlug\\" + } + ] + } + ] + }, + \\"permalinkToSidebar\\": { + \\"/docs/withSlugs/rootAbsoluteSlug\\": \\"version-1.0.1/docs\\" + } }", } `; @@ -1333,7 +1207,6 @@ exports[`versioned website content: global data 1`] = ` Object { "pluginName": Object { "pluginId": Object { - "latestVersionName": "1.0.1", "path": "/docs", "versions": Array [ Object { @@ -1363,8 +1236,10 @@ Object { "path": "/docs/next/tryToEscapeSlug", }, ], + "isLast": false, + "label": "Next", "mainDocId": "hello", - "name": "next", + "name": "current", "path": "/docs/next", }, Object { @@ -1378,6 +1253,8 @@ Object { "path": "/docs/", }, ], + "isLast": true, + "label": "1.0.1", "mainDocId": "hello", "name": "1.0.1", "path": "/docs", @@ -1397,6 +1274,8 @@ Object { "path": "/docs/1.0.0/", }, ], + "isLast": false, + "label": "1.0.0", "mainDocId": "hello", "name": "1.0.0", "path": "/docs/1.0.0", @@ -1436,6 +1315,8 @@ Object { "path": "/docs/withSlugs/tryToEscapeSlug", }, ], + "isLast": false, + "label": "withSlugs", "mainDocId": "rootAbsoluteSlug", "name": "withSlugs", "path": "/docs/withSlugs", @@ -1452,7 +1333,7 @@ Array [ "component": "@theme/DocPage", "exact": false, "modules": Object { - "docsMetadata": "~docs/docs-1-0-0-route-660.json", + "versionMetadata": "~docs/version-1-0-0-metadata-prop-608.json", }, "path": "/docs/1.0.0", "priority": undefined, @@ -1487,7 +1368,7 @@ Array [ "component": "@theme/DocPage", "exact": false, "modules": Object { - "docsMetadata": "~docs/docs-next-route-1c8.json", + "versionMetadata": "~docs/version-current-metadata-prop-751.json", }, "path": "/docs/next", "priority": undefined, @@ -1546,7 +1427,7 @@ Array [ "component": "@theme/DocPage", "exact": false, "modules": Object { - "docsMetadata": "~docs/docs-with-slugs-route-335.json", + "versionMetadata": "~docs/version-with-slugs-metadata-prop-2bf.json", }, "path": "/docs/withSlugs", "priority": undefined, @@ -1621,7 +1502,7 @@ Array [ "component": "@theme/DocPage", "exact": false, "modules": Object { - "docsMetadata": "~docs/docs-route-ff2.json", + "versionMetadata": "~docs/version-1-0-1-metadata-prop-e87.json", }, "path": "/docs", "priority": -1, @@ -1647,16 +1528,20 @@ Array [ ] `; -exports[`versioned website content: sidebars needed for each version 1`] = ` +exports[`versioned website content: withSlugs version sidebars 1`] = ` Object { - "1.0.0": Set { - "version-1.0.0/docs", - }, - "1.0.1": Set { - "version-1.0.1/docs", - }, - "next": Set { - "docs", - }, + "version-1.0.1/docs": Array [ + Object { + "collapsed": true, + "items": Array [ + Object { + "id": "version-withSlugs/rootAbsoluteSlug", + "type": "doc", + }, + ], + "label": "Test", + "type": "category", + }, + ], } `; diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/version.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/cli.test.ts similarity index 79% rename from packages/docusaurus-plugin-content-docs/src/__tests__/version.test.ts rename to packages/docusaurus-plugin-content-docs/src/__tests__/cli.test.ts index a7a70fc1c6fb..b296aa17a34f 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/version.test.ts +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/cli.test.ts @@ -6,14 +6,14 @@ */ import path from 'path'; -import {docsVersion} from '../version'; +import {cliDocsVersionCommand} from '../cli'; import {PathOptions} from '../types'; import fs from 'fs-extra'; import { - getVersionedDocsDir, - getVersionsJSONFile, - getVersionedSidebarsDir, -} from '../env'; + getVersionedDocsDirPath, + getVersionsFilePath, + getVersionedSidebarsDirPath, +} from '../versions'; import {DEFAULT_PLUGIN_ID} from '@docusaurus/core/lib/constants'; const fixtureDir = path.join(__dirname, '__fixtures__'); @@ -28,17 +28,32 @@ describe('docsVersion', () => { test('no version tag provided', () => { expect(() => - docsVersion(null, simpleSiteDir, DEFAULT_PLUGIN_ID, DEFAULT_OPTIONS), + cliDocsVersionCommand( + null, + simpleSiteDir, + DEFAULT_PLUGIN_ID, + DEFAULT_OPTIONS, + ), ).toThrowErrorMatchingInlineSnapshot( `"[docs] No version tag specified!. Pass the version you wish to create as an argument. Ex: 1.0.0"`, ); expect(() => - docsVersion(undefined, simpleSiteDir, DEFAULT_PLUGIN_ID, DEFAULT_OPTIONS), + cliDocsVersionCommand( + undefined, + simpleSiteDir, + DEFAULT_PLUGIN_ID, + DEFAULT_OPTIONS, + ), ).toThrowErrorMatchingInlineSnapshot( `"[docs] No version tag specified!. Pass the version you wish to create as an argument. Ex: 1.0.0"`, ); expect(() => - docsVersion('', simpleSiteDir, DEFAULT_PLUGIN_ID, DEFAULT_OPTIONS), + cliDocsVersionCommand( + '', + simpleSiteDir, + DEFAULT_PLUGIN_ID, + DEFAULT_OPTIONS, + ), ).toThrowErrorMatchingInlineSnapshot( `"[docs] No version tag specified!. Pass the version you wish to create as an argument. Ex: 1.0.0"`, ); @@ -46,12 +61,17 @@ describe('docsVersion', () => { test('version tag should not have slash', () => { expect(() => - docsVersion('foo/bar', simpleSiteDir, DEFAULT_PLUGIN_ID, DEFAULT_OPTIONS), + cliDocsVersionCommand( + 'foo/bar', + simpleSiteDir, + DEFAULT_PLUGIN_ID, + DEFAULT_OPTIONS, + ), ).toThrowErrorMatchingInlineSnapshot( `"[docs] Invalid version tag specified! Do not include slash (/) or (\\\\). Try something like: 1.0.0"`, ); expect(() => - docsVersion( + cliDocsVersionCommand( 'foo\\bar', simpleSiteDir, DEFAULT_PLUGIN_ID, @@ -64,7 +84,7 @@ describe('docsVersion', () => { test('version tag should not be too long', () => { expect(() => - docsVersion( + cliDocsVersionCommand( 'a'.repeat(255), simpleSiteDir, DEFAULT_PLUGIN_ID, @@ -77,12 +97,22 @@ describe('docsVersion', () => { test('version tag should not be a dot or two dots', () => { expect(() => - docsVersion('..', simpleSiteDir, DEFAULT_PLUGIN_ID, DEFAULT_OPTIONS), + cliDocsVersionCommand( + '..', + simpleSiteDir, + DEFAULT_PLUGIN_ID, + DEFAULT_OPTIONS, + ), ).toThrowErrorMatchingInlineSnapshot( `"[docs] Invalid version tag specified! Do not name your version \\".\\" or \\"..\\". Try something like: 1.0.0"`, ); expect(() => - docsVersion('.', simpleSiteDir, DEFAULT_PLUGIN_ID, DEFAULT_OPTIONS), + cliDocsVersionCommand( + '.', + simpleSiteDir, + DEFAULT_PLUGIN_ID, + DEFAULT_OPTIONS, + ), ).toThrowErrorMatchingInlineSnapshot( `"[docs] Invalid version tag specified! Do not name your version \\".\\" or \\"..\\". Try something like: 1.0.0"`, ); @@ -90,7 +120,7 @@ describe('docsVersion', () => { test('version tag should be a valid pathname', () => { expect(() => - docsVersion( + cliDocsVersionCommand( '', simpleSiteDir, DEFAULT_PLUGIN_ID, @@ -100,7 +130,7 @@ describe('docsVersion', () => { `"[docs] Invalid version tag specified! Please ensure its a valid pathname too. Try something like: 1.0.0"`, ); expect(() => - docsVersion( + cliDocsVersionCommand( 'foo\x00bar', simpleSiteDir, DEFAULT_PLUGIN_ID, @@ -110,7 +140,12 @@ describe('docsVersion', () => { `"[docs] Invalid version tag specified! Please ensure its a valid pathname too. Try something like: 1.0.0"`, ); expect(() => - docsVersion('foo:bar', simpleSiteDir, DEFAULT_PLUGIN_ID, DEFAULT_OPTIONS), + cliDocsVersionCommand( + 'foo:bar', + simpleSiteDir, + DEFAULT_PLUGIN_ID, + DEFAULT_OPTIONS, + ), ).toThrowErrorMatchingInlineSnapshot( `"[docs] Invalid version tag specified! Please ensure its a valid pathname too. Try something like: 1.0.0"`, ); @@ -118,7 +153,7 @@ describe('docsVersion', () => { test('version tag already exist', () => { expect(() => - docsVersion( + cliDocsVersionCommand( '1.0.0', versionedSiteDir, DEFAULT_PLUGIN_ID, @@ -132,7 +167,12 @@ describe('docsVersion', () => { test('no docs file to version', () => { const emptySiteDir = path.join(fixtureDir, 'empty-site'); expect(() => - docsVersion('1.0.0', emptySiteDir, DEFAULT_PLUGIN_ID, DEFAULT_OPTIONS), + cliDocsVersionCommand( + '1.0.0', + emptySiteDir, + DEFAULT_PLUGIN_ID, + DEFAULT_OPTIONS, + ), ).toThrowErrorMatchingInlineSnapshot( `"[docs] There is no docs to version !"`, ); @@ -159,23 +199,23 @@ describe('docsVersion', () => { path: 'docs', sidebarPath: path.join(simpleSiteDir, 'sidebars.json'), }; - docsVersion('1.0.0', simpleSiteDir, DEFAULT_PLUGIN_ID, options); + cliDocsVersionCommand('1.0.0', simpleSiteDir, DEFAULT_PLUGIN_ID, options); expect(copyMock).toHaveBeenCalledWith( path.join(simpleSiteDir, options.path), path.join( - getVersionedDocsDir(simpleSiteDir, DEFAULT_PLUGIN_ID), + getVersionedDocsDirPath(simpleSiteDir, DEFAULT_PLUGIN_ID), 'version-1.0.0', ), ); expect(versionedSidebar).toMatchSnapshot(); expect(versionedSidebarPath).toEqual( path.join( - getVersionedSidebarsDir(simpleSiteDir, DEFAULT_PLUGIN_ID), + getVersionedSidebarsDirPath(simpleSiteDir, DEFAULT_PLUGIN_ID), 'version-1.0.0-sidebars.json', ), ); expect(versionsPath).toEqual( - getVersionsJSONFile(simpleSiteDir, DEFAULT_PLUGIN_ID), + getVersionsFilePath(simpleSiteDir, DEFAULT_PLUGIN_ID), ); expect(versions).toEqual(['1.0.0']); expect(consoleMock).toHaveBeenCalledWith('[docs] Version 1.0.0 created!'); @@ -207,23 +247,28 @@ describe('docsVersion', () => { path: 'docs', sidebarPath: path.join(versionedSiteDir, 'sidebars.json'), }; - docsVersion('2.0.0', versionedSiteDir, DEFAULT_PLUGIN_ID, options); + cliDocsVersionCommand( + '2.0.0', + versionedSiteDir, + DEFAULT_PLUGIN_ID, + options, + ); expect(copyMock).toHaveBeenCalledWith( path.join(versionedSiteDir, options.path), path.join( - getVersionedDocsDir(versionedSiteDir, DEFAULT_PLUGIN_ID), + getVersionedDocsDirPath(versionedSiteDir, DEFAULT_PLUGIN_ID), 'version-2.0.0', ), ); expect(versionedSidebar).toMatchSnapshot(); expect(versionedSidebarPath).toEqual( path.join( - getVersionedSidebarsDir(versionedSiteDir, DEFAULT_PLUGIN_ID), + getVersionedSidebarsDirPath(versionedSiteDir, DEFAULT_PLUGIN_ID), 'version-2.0.0-sidebars.json', ), ); expect(versionsPath).toEqual( - getVersionsJSONFile(versionedSiteDir, DEFAULT_PLUGIN_ID), + 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!'); @@ -257,23 +302,23 @@ describe('docsVersion', () => { path: 'community', sidebarPath: path.join(versionedSiteDir, 'community_sidebars.json'), }; - docsVersion('2.0.0', versionedSiteDir, pluginId, options); + cliDocsVersionCommand('2.0.0', versionedSiteDir, pluginId, options); expect(copyMock).toHaveBeenCalledWith( path.join(versionedSiteDir, options.path), path.join( - getVersionedDocsDir(versionedSiteDir, pluginId), + getVersionedDocsDirPath(versionedSiteDir, pluginId), 'version-2.0.0', ), ); expect(versionedSidebar).toMatchSnapshot(); expect(versionedSidebarPath).toEqual( path.join( - getVersionedSidebarsDir(versionedSiteDir, pluginId), + getVersionedSidebarsDirPath(versionedSiteDir, pluginId), 'version-2.0.0-sidebars.json', ), ); expect(versionsPath).toEqual( - getVersionsJSONFile(versionedSiteDir, pluginId), + getVersionsFilePath(versionedSiteDir, pluginId), ); expect(versions).toEqual(['2.0.0', '1.0.0']); expect(consoleMock).toHaveBeenCalledWith( diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/docs.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/docs.test.ts new file mode 100644 index 000000000000..df1028a97f17 --- /dev/null +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/docs.test.ts @@ -0,0 +1,529 @@ +/** + * 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 path from 'path'; +import {loadContext} from '@docusaurus/core/src/server/index'; +import {processDocMetadata, readVersionDocs, readDocFile} from '../docs'; +import {readVersionsMetadata} from '../versions'; +import { + DocFile, + DocMetadataBase, + MetadataOptions, + VersionMetadata, +} from '../types'; +import {LoadContext} from '@docusaurus/types'; +import {DEFAULT_PLUGIN_ID} from '@docusaurus/core/lib/constants'; +import {DEFAULT_OPTIONS} from '../options'; +import {Optional} from 'utility-types'; + +const fixtureDir = path.join(__dirname, '__fixtures__'); + +const createFakeDocFile = ({ + source, + frontmatter = {}, + markdown = 'some markdown content', +}: { + source: string; + frontmatter?: Record; + markdown?: string; +}): DocFile => { + const content = `--- +${Object.entries(frontmatter) + .map(([key, value]) => `${key}: ${value}`) + .join('\n')} +--- +${markdown} +`; + return { + source, + content, + lastUpdate: {}, + }; +}; + +function createTestUtils({ + siteDir, + context, + versionMetadata, + options, +}: { + siteDir: string; + context: LoadContext; + versionMetadata: VersionMetadata; + options: MetadataOptions; +}) { + async function readDoc(docFileSource: string) { + return readDocFile(versionMetadata.docsDirPath, docFileSource, options); + } + function processDocFile(docFile: DocFile) { + return processDocMetadata({ + docFile, + versionMetadata, + options, + context, + }); + } + async function testMeta( + docFileSource: string, + expectedMetadata: Optional< + DocMetadataBase, + 'source' | 'lastUpdatedBy' | 'lastUpdatedAt' | 'sidebar_label' | 'editUrl' + >, + ) { + const docFile = await readDoc(docFileSource); + const metadata = await processDocMetadata({ + docFile, + versionMetadata, + context, + options, + }); + expect(metadata).toEqual({ + lastUpdatedBy: undefined, + lastUpdatedAt: undefined, + sidebar_label: undefined, + editUrl: undefined, + source: path.join( + '@site', + path.relative(siteDir, versionMetadata.docsDirPath), + docFileSource, + ), + ...expectedMetadata, + }); + } + + async function testSlug(docFileSource: string, expectedPermalink: string) { + const docFile = await readDoc(docFileSource); + const metadata = await processDocMetadata({ + docFile, + versionMetadata, + context, + options, + }); + expect(metadata.permalink).toEqual(expectedPermalink); + } + + return {processDocFile, testMeta, testSlug}; +} + +describe('simple site', () => { + const siteDir = path.join(fixtureDir, 'simple-site'); + const context = loadContext(siteDir); + const options = { + id: DEFAULT_PLUGIN_ID, + ...DEFAULT_OPTIONS, + }; + const versionsMetadata = readVersionsMetadata({ + context, + options: { + id: DEFAULT_PLUGIN_ID, + ...DEFAULT_OPTIONS, + }, + }); + expect(versionsMetadata.length).toEqual(1); + const [currentVersion] = versionsMetadata; + + const defaultTestUtils = createTestUtils({ + siteDir, + context, + options, + versionMetadata: currentVersion, + }); + + test('readVersionDocs', async () => { + const docs = await readVersionDocs(currentVersion, options); + expect(docs.map((doc) => doc.source)).toMatchObject([ + 'hello.md', + 'ipsum.md', + 'lorem.md', + 'rootAbsoluteSlug.md', + 'rootRelativeSlug.md', + 'rootResolvedSlug.md', + 'rootTryToEscapeSlug.md', + 'foo/bar.md', + 'foo/baz.md', + 'slugs/absoluteSlug.md', + 'slugs/relativeSlug.md', + 'slugs/resolvedSlug.md', + 'slugs/tryToEscapeSlug.md', + ]); + }); + + test('normal docs', async () => { + await defaultTestUtils.testMeta(path.join('foo', 'bar.md'), { + version: 'current', + id: 'foo/bar', + unversionedId: 'foo/bar', + isDocsHomePage: false, + permalink: '/docs/foo/bar', + slug: '/foo/bar', + title: 'Bar', + description: 'This is custom description', + }); + await defaultTestUtils.testMeta(path.join('hello.md'), { + version: 'current', + id: 'hello', + unversionedId: 'hello', + isDocsHomePage: false, + permalink: '/docs/hello', + slug: '/hello', + title: 'Hello, World !', + description: `Hi, Endilie here :)`, + }); + }); + + test('homePageId doc', async () => { + const testUtilsLocal = createTestUtils({ + siteDir, + context, + options: {...options, homePageId: 'hello'}, + versionMetadata: currentVersion, + }); + + await testUtilsLocal.testMeta(path.join('hello.md'), { + version: 'current', + id: 'hello', + unversionedId: 'hello', + isDocsHomePage: true, + permalink: '/docs/', + slug: '/', + title: 'Hello, World !', + description: `Hi, Endilie here :)`, + }); + }); + + test('homePageId doc nested', async () => { + const testUtilsLocal = createTestUtils({ + siteDir, + context, + options: {...options, homePageId: 'foo/bar'}, + versionMetadata: currentVersion, + }); + + await testUtilsLocal.testMeta(path.join('foo', 'bar.md'), { + version: 'current', + id: 'foo/bar', + unversionedId: 'foo/bar', + isDocsHomePage: true, + permalink: '/docs/', + slug: '/', + title: 'Bar', + description: 'This is custom description', + }); + }); + + test('docs with editUrl', async () => { + const testUtilsLocal = createTestUtils({ + siteDir, + context, + options: { + ...options, + editUrl: 'https://github.com/facebook/docusaurus/edit/master/website', + }, + versionMetadata: currentVersion, + }); + + await testUtilsLocal.testMeta(path.join('foo', 'baz.md'), { + version: 'current', + id: 'foo/baz', + unversionedId: 'foo/baz', + isDocsHomePage: false, + permalink: '/docs/foo/bazSlug.html', + slug: '/foo/bazSlug.html', + title: 'baz', + editUrl: + 'https://github.com/facebook/docusaurus/edit/master/website/docs/foo/baz.md', + description: 'Images', + }); + }); + + test('docs with custom editUrl & unrelated frontmatter', async () => { + await defaultTestUtils.testMeta('lorem.md', { + version: 'current', + id: 'lorem', + unversionedId: 'lorem', + isDocsHomePage: false, + permalink: '/docs/lorem', + slug: '/lorem', + title: 'lorem', + editUrl: 'https://github.com/customUrl/docs/lorem.md', + description: 'Lorem ipsum.', + }); + }); + + test('docs with last update time and author', async () => { + const testUtilsLocal = createTestUtils({ + siteDir, + context, + options: { + ...options, + showLastUpdateAuthor: true, + showLastUpdateTime: true, + }, + versionMetadata: currentVersion, + }); + + await testUtilsLocal.testMeta('lorem.md', { + version: 'current', + id: 'lorem', + unversionedId: 'lorem', + isDocsHomePage: false, + permalink: '/docs/lorem', + slug: '/lorem', + title: 'lorem', + editUrl: 'https://github.com/customUrl/docs/lorem.md', + description: 'Lorem ipsum.', + lastUpdatedAt: 1539502055, + lastUpdatedBy: 'Author', + }); + }); + + test('docs with slugs', async () => { + await defaultTestUtils.testSlug( + path.join('rootRelativeSlug.md'), + '/docs/rootRelativeSlug', + ); + await defaultTestUtils.testSlug( + path.join('rootAbsoluteSlug.md'), + '/docs/rootAbsoluteSlug', + ); + await defaultTestUtils.testSlug( + path.join('rootResolvedSlug.md'), + '/docs/hey/rootResolvedSlug', + ); + await defaultTestUtils.testSlug( + path.join('rootTryToEscapeSlug.md'), + '/docs/rootTryToEscapeSlug', + ); + + await defaultTestUtils.testSlug( + path.join('slugs', 'absoluteSlug.md'), + '/docs/absoluteSlug', + ); + await defaultTestUtils.testSlug( + path.join('slugs', 'relativeSlug.md'), + '/docs/slugs/relativeSlug', + ); + await defaultTestUtils.testSlug( + path.join('slugs', 'resolvedSlug.md'), + '/docs/slugs/hey/resolvedSlug', + ); + await defaultTestUtils.testSlug( + path.join('slugs', 'tryToEscapeSlug.md'), + '/docs/tryToEscapeSlug', + ); + }); + + test('docs with invalid id', () => { + expect(() => { + defaultTestUtils.processDocFile( + createFakeDocFile({ + source: 'some/fake/path', + frontmatter: { + id: 'Hello/world', + }, + }), + ); + }).toThrowErrorMatchingInlineSnapshot( + `"Document id [Hello/world] cannot include \\"/\\"."`, + ); + }); + + test('docs with slug on doc home', async () => { + const testUtilsLocal = createTestUtils({ + siteDir, + context, + options: { + ...options, + homePageId: 'homePageId', + }, + versionMetadata: currentVersion, + }); + expect(() => { + testUtilsLocal.processDocFile( + createFakeDocFile({ + source: 'homePageId', + frontmatter: { + slug: '/x/y', + }, + }), + ); + }).toThrowErrorMatchingInlineSnapshot( + `"The docs homepage (homePageId=homePageId) is not allowed to have a frontmatter slug=/x/y => you have to chooser either homePageId or slug, not both"`, + ); + }); +}); + +describe('versioned site', () => { + const siteDir = path.join(fixtureDir, 'versioned-site'); + const context = loadContext(siteDir); + const options = { + id: DEFAULT_PLUGIN_ID, + ...DEFAULT_OPTIONS, + }; + const versionsMetadata = readVersionsMetadata({ + context, + options: { + id: DEFAULT_PLUGIN_ID, + ...DEFAULT_OPTIONS, + }, + }); + expect(versionsMetadata.length).toEqual(4); + const [ + currentVersion, + version101, + version100, + versionWithSlugs, + ] = versionsMetadata; + + const currentVersionTestUtils = createTestUtils({ + siteDir, + context, + options, + versionMetadata: currentVersion, + }); + const version101TestUtils = createTestUtils({ + siteDir, + context, + options, + versionMetadata: version101, + }); + + const version100TestUtils = createTestUtils({ + siteDir, + context, + options, + versionMetadata: version100, + }); + + const versionWithSlugsTestUtils = createTestUtils({ + siteDir, + context, + options, + versionMetadata: versionWithSlugs, + }); + + test('next docs', async () => { + await currentVersionTestUtils.testMeta(path.join('foo', 'bar.md'), { + id: 'foo/bar', + unversionedId: 'foo/bar', + isDocsHomePage: false, + permalink: '/docs/next/foo/barSlug', + slug: '/foo/barSlug', + title: 'bar', + description: 'This is next version of bar.', + version: 'current', + }); + await currentVersionTestUtils.testMeta(path.join('hello.md'), { + id: 'hello', + unversionedId: 'hello', + isDocsHomePage: false, + permalink: '/docs/next/hello', + slug: '/hello', + title: 'hello', + description: 'Hello next !', + version: 'current', + }); + }); + + test('versioned docs', async () => { + await version100TestUtils.testMeta(path.join('foo', 'bar.md'), { + id: 'version-1.0.0/foo/bar', + unversionedId: 'foo/bar', + isDocsHomePage: false, + permalink: '/docs/1.0.0/foo/barSlug', + slug: '/foo/barSlug', + title: 'bar', + description: 'Bar 1.0.0 !', + version: '1.0.0', + }); + await version100TestUtils.testMeta(path.join('hello.md'), { + id: 'version-1.0.0/hello', + unversionedId: 'hello', + isDocsHomePage: false, + permalink: '/docs/1.0.0/hello', + slug: '/hello', + title: 'hello', + description: 'Hello 1.0.0 !', + version: '1.0.0', + }); + await version101TestUtils.testMeta(path.join('foo', 'bar.md'), { + id: 'version-1.0.1/foo/bar', + unversionedId: 'foo/bar', + isDocsHomePage: false, + permalink: '/docs/foo/bar', + slug: '/foo/bar', + title: 'bar', + description: 'Bar 1.0.1 !', + version: '1.0.1', + }); + await version101TestUtils.testMeta(path.join('hello.md'), { + id: 'version-1.0.1/hello', + unversionedId: 'hello', + isDocsHomePage: false, + permalink: '/docs/hello', + slug: '/hello', + title: 'hello', + description: 'Hello 1.0.1 !', + version: '1.0.1', + }); + }); + + test('next doc slugs', async () => { + await currentVersionTestUtils.testSlug( + path.join('slugs', 'absoluteSlug.md'), + '/docs/next/absoluteSlug', + ); + await currentVersionTestUtils.testSlug( + path.join('slugs', 'relativeSlug.md'), + '/docs/next/slugs/relativeSlug', + ); + await currentVersionTestUtils.testSlug( + path.join('slugs', 'resolvedSlug.md'), + '/docs/next/slugs/hey/resolvedSlug', + ); + await currentVersionTestUtils.testSlug( + path.join('slugs', 'tryToEscapeSlug.md'), + '/docs/next/tryToEscapeSlug', + ); + }); + + test('versioned doc slugs', async () => { + await versionWithSlugsTestUtils.testSlug( + path.join('rootAbsoluteSlug.md'), + '/docs/withSlugs/rootAbsoluteSlug', + ); + await versionWithSlugsTestUtils.testSlug( + path.join('rootRelativeSlug.md'), + '/docs/withSlugs/rootRelativeSlug', + ); + await versionWithSlugsTestUtils.testSlug( + path.join('rootResolvedSlug.md'), + '/docs/withSlugs/hey/rootResolvedSlug', + ); + await versionWithSlugsTestUtils.testSlug( + path.join('rootTryToEscapeSlug.md'), + '/docs/withSlugs/rootTryToEscapeSlug', + ); + + await versionWithSlugsTestUtils.testSlug( + path.join('slugs', 'absoluteSlug.md'), + '/docs/withSlugs/absoluteSlug', + ); + await versionWithSlugsTestUtils.testSlug( + path.join('slugs', 'relativeSlug.md'), + '/docs/withSlugs/slugs/relativeSlug', + ); + await versionWithSlugsTestUtils.testSlug( + path.join('slugs', 'resolvedSlug.md'), + '/docs/withSlugs/slugs/hey/resolvedSlug', + ); + await versionWithSlugsTestUtils.testSlug( + path.join('slugs', 'tryToEscapeSlug.md'), + '/docs/withSlugs/tryToEscapeSlug', + ); + }); +}); diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/env.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/env.test.ts deleted file mode 100644 index 017ab47bab4e..000000000000 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/env.test.ts +++ /dev/null @@ -1,58 +0,0 @@ -/** - * 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 path from 'path'; -import loadEnv from '../env'; -import {DEFAULT_PLUGIN_ID} from '@docusaurus/core/lib/constants'; - -describe('loadEnv', () => { - test('website with versioning disabled', () => { - const siteDir = path.join(__dirname, '__fixtures__', 'simple-site'); - const env = loadEnv(siteDir, DEFAULT_PLUGIN_ID); - expect(env.versioning.enabled).toBe(false); - expect(env.versioning.versions).toStrictEqual([]); - }); - - test('website with versioning enabled', () => { - const siteDir = path.join(__dirname, '__fixtures__', 'versioned-site'); - const env = loadEnv(siteDir, DEFAULT_PLUGIN_ID); - expect(env.versioning.enabled).toBe(true); - expect(env.versioning.latestVersion).toBe('1.0.1'); - expect(env.versioning.versions).toStrictEqual([ - '1.0.1', - '1.0.0', - 'withSlugs', - ]); - }); - - test('website with versioning enabled, 2nd docs plugin instance', () => { - const siteDir = path.join(__dirname, '__fixtures__', 'versioned-site'); - const env = loadEnv(siteDir, 'community'); - expect(env.versioning.enabled).toBe(true); - expect(env.versioning.latestVersion).toBe('1.0.0'); - expect(env.versioning.versions).toStrictEqual(['1.0.0']); - }); - - test('website with versioning but disabled', () => { - const siteDir = path.join(__dirname, '__fixtures__', 'versioned-site'); - const env = loadEnv(siteDir, DEFAULT_PLUGIN_ID, {disableVersioning: true}); - expect(env.versioning.enabled).toBe(false); - expect(env.versioning.versions).toStrictEqual([]); - }); - - test('website with invalid versions.json file', () => { - const siteDir = path.join(__dirname, '__fixtures__', 'versioned-site'); - const mock = jest.spyOn(JSON, 'parse').mockImplementationOnce(() => { - return { - invalid: 'json', - }; - }); - const env = loadEnv(siteDir, DEFAULT_PLUGIN_ID); - expect(env.versioning.enabled).toBe(false); - mock.mockRestore(); - }); -}); diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts index ffa0d92cf3d9..fa1b1b71fa2b 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts @@ -6,12 +6,12 @@ */ import path from 'path'; -import {validate} from 'webpack'; import {isMatch} from 'picomatch'; import commander from 'commander'; +import {kebabCase} from 'lodash'; + import fs from 'fs-extra'; import pluginContentDocs from '../index'; -import loadEnv from '../env'; import {loadContext} from '@docusaurus/core/src/server/index'; import {applyConfigureWebpack} from '@docusaurus/core/src/webpack/utils'; import {RouteConfig} from '@docusaurus/types'; @@ -19,9 +19,26 @@ import {posixPath} from '@docusaurus/utils'; import {sortConfig} from '@docusaurus/core/src/server/plugins'; import {DEFAULT_PLUGIN_ID} from '@docusaurus/core/lib/constants'; -import * as version from '../version'; -import {PluginOptionSchema} from '../pluginOptionSchema'; +import * as cliDocs from '../cli'; +import {OptionsSchema} from '../options'; import {normalizePluginOptions} from '@docusaurus/utils-validation'; +import {DocMetadata, LoadedVersion} from '../types'; +import {toSidebarsProp} from '../props'; + +// @ts-expect-error: TODO typedefs missing? +import {validate} from 'webpack'; + +function findDocById(version: LoadedVersion, unversionedId: string) { + return version.docs.find((item) => item.unversionedId === unversionedId); +} +const defaultDocMetadata: Partial = { + next: undefined, + previous: undefined, + editUrl: undefined, + lastUpdatedAt: undefined, + lastUpdatedBy: undefined, + sidebar_label: undefined, +}; const createFakeActions = (contentDir: string) => { const routeConfigs: RouteConfig[] = []; @@ -41,20 +58,34 @@ const createFakeActions = (contentDir: string) => { }, }; + // query by prefix, because files have a hash at the end + // so it's not convenient to query by full filename + const getCreatedDataByPrefix = (prefix: string) => { + const entry = Object.entries(dataContainer).find(([key]) => + key.startsWith(prefix), + ); + if (!entry) { + throw new Error(`No created entry found for prefix=[${prefix}] +Entries created: +- ${Object.keys(dataContainer).join('\n- ')} + `); + } + return JSON.parse(entry[1] as string); + }; + // Extra fns useful for tests! const utils = { getGlobalData: () => globalDataContainer, getRouteConfigs: () => routeConfigs, - // query by prefix, because files have a hash at the end - // so it's not convenient to query by full filename - getCreatedDataByPrefix: (prefix: string) => { - const entry = Object.entries(dataContainer).find(([key]) => - key.startsWith(prefix), + + checkVersionMetadataPropCreated: (version: LoadedVersion) => { + const versionMetadataProp = getCreatedDataByPrefix( + `version-${kebabCase(version.versionName)}-metadata-prop`, + ); + expect(versionMetadataProp.docsSidebars).toEqual(toSidebarsProp(version)); + expect(versionMetadataProp.permalinkToSidebar).toEqual( + version.permalinkToSidebar, ); - if (!entry) { - throw new Error(`No entry found for prefix=${prefix}`); - } - return JSON.parse(entry[1] as string); }, expectSnapshot: () => { @@ -79,11 +110,11 @@ test('site with wrong sidebar file', async () => { const sidebarPath = path.join(siteDir, 'wrong-sidebars.json'); const plugin = pluginContentDocs( context, - normalizePluginOptions(PluginOptionSchema, { + normalizePluginOptions(OptionsSchema, { sidebarPath, }), ); - await expect(plugin.loadContent()).rejects.toThrowErrorMatchingSnapshot(); + await expect(plugin.loadContent!()).rejects.toThrowErrorMatchingSnapshot(); }); describe('empty/no docs website', () => { @@ -94,33 +125,26 @@ describe('empty/no docs website', () => { await fs.ensureDir(path.join(siteDir, 'docs')); const plugin = pluginContentDocs( context, - normalizePluginOptions(PluginOptionSchema, {}), + normalizePluginOptions(OptionsSchema, {}), + ); + await expect( + plugin.loadContent!(), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Docs version current has no docs! At least one doc should exist at path=[docs]"`, ); - const content = await plugin.loadContent(); - const {docsMetadata, docsSidebars} = content; - expect(docsMetadata).toMatchInlineSnapshot(`Object {}`); - expect(docsSidebars).toMatchInlineSnapshot(`Object {}`); - - const pluginContentDir = path.join(context.generatedFilesDir, plugin.name); - const {actions, utils} = createFakeActions(pluginContentDir); - - await plugin.contentLoaded({ - content, - actions, - }); - - expect(utils.getRouteConfigs()).toEqual([]); }); test('docs folder does not exist', async () => { - const plugin = pluginContentDocs( - context, - normalizePluginOptions(PluginOptionSchema, { - path: '/path/does/not/exist/', - }), + expect(() => + pluginContentDocs( + context, + normalizePluginOptions(OptionsSchema, { + path: '/path/does/not/exist/', + }), + ), + ).toThrowErrorMatchingInlineSnapshot( + `"The docs folder does not exist for version [current]. A docs folder is expected to be found at /path/does/not/exist"`, ); - const content = await plugin.loadContent(); - expect(content).toBeNull(); }); }); @@ -128,11 +152,10 @@ describe('simple website', () => { const siteDir = path.join(__dirname, '__fixtures__', 'simple-site'); const context = loadContext(siteDir); const sidebarPath = path.join(siteDir, 'sidebars.json'); - const pluginPath = 'docs'; const plugin = pluginContentDocs( context, - normalizePluginOptions(PluginOptionSchema, { - path: pluginPath, + normalizePluginOptions(OptionsSchema, { + path: 'docs', sidebarPath, homePageId: 'hello', }), @@ -140,27 +163,31 @@ describe('simple website', () => { const pluginContentDir = path.join(context.generatedFilesDir, plugin.name); test('extendCli - docsVersion', () => { - const mock = jest.spyOn(version, 'docsVersion').mockImplementation(); + const mock = jest + .spyOn(cliDocs, 'cliDocsVersionCommand') + .mockImplementation(); const cli = new commander.Command(); - plugin.extendCli(cli); + // @ts-expect-error: TODO annoying type incompatibility + plugin.extendCli!(cli); cli.parse(['node', 'test', 'docs:version', '1.0.0']); + expect(mock).toHaveBeenCalledTimes(1); expect(mock).toHaveBeenCalledWith('1.0.0', siteDir, DEFAULT_PLUGIN_ID, { - path: pluginPath, + path: 'docs', sidebarPath, }); mock.mockRestore(); }); test('getPathToWatch', () => { - const pathToWatch = plugin.getPathsToWatch(); + const pathToWatch = plugin.getPathsToWatch!(); const matchPattern = pathToWatch.map((filepath) => posixPath(path.relative(siteDir, filepath)), ); expect(matchPattern).not.toEqual([]); expect(matchPattern).toMatchInlineSnapshot(` Array [ - "docs/**/*.{md,mdx}", "sidebars.json", + "docs/**/*.{md,mdx}", ] `); expect(isMatch('docs/hello.md', matchPattern)).toEqual(true); @@ -192,15 +219,13 @@ describe('simple website', () => { }); test('content', async () => { - const content = await plugin.loadContent(); - const { - docsMetadata, - docsSidebars, - versionToSidebars, - permalinkToSidebar, - } = content; - expect(versionToSidebars).toEqual({}); - expect(docsMetadata.hello).toEqual({ + const content = await plugin.loadContent!(); + expect(content.loadedVersions.length).toEqual(1); + const [currentVersion] = content.loadedVersions; + + expect(findDocById(currentVersion, 'hello')).toEqual({ + ...defaultDocMetadata, + version: 'current', id: 'hello', unversionedId: 'hello', isDocsHomePage: true, @@ -211,12 +236,18 @@ describe('simple website', () => { permalink: '/docs/foo/bazSlug.html', }, sidebar: 'docs', - source: path.join('@site', pluginPath, 'hello.md'), + source: path.join( + '@site', + path.relative(siteDir, currentVersion.docsDirPath), + 'hello.md', + ), title: 'Hello, World !', description: 'Hi, Endilie here :)', }); - expect(docsMetadata['foo/bar']).toEqual({ + expect(findDocById(currentVersion, 'foo/bar')).toEqual({ + ...defaultDocMetadata, + version: 'current', id: 'foo/bar', unversionedId: 'foo/bar', isDocsHomePage: false, @@ -227,26 +258,30 @@ describe('simple website', () => { permalink: '/docs/foo/bar', slug: '/foo/bar', sidebar: 'docs', - source: path.join('@site', pluginPath, 'foo', 'bar.md'), + source: path.join( + '@site', + path.relative(siteDir, currentVersion.docsDirPath), + 'foo', + 'bar.md', + ), title: 'Bar', description: 'This is custom description', }); - expect(docsSidebars).toMatchSnapshot(); + expect(currentVersion.sidebars).toMatchSnapshot(); const {actions, utils} = createFakeActions(pluginContentDir); - await plugin.contentLoaded({ + await plugin.contentLoaded!({ content, actions, + allContent: {}, }); - // There is only one nested docs route for simple site - const baseMetadata = utils.getCreatedDataByPrefix('docs-route-'); - expect(baseMetadata.docsSidebars).toEqual(docsSidebars); - expect(baseMetadata.permalinkToSidebar).toEqual(permalinkToSidebar); + utils.checkVersionMetadataPropCreated(currentVersion); utils.expectSnapshot(); + expect(utils.getGlobalData()).toMatchSnapshot(); }); }); @@ -258,25 +293,24 @@ describe('versioned website', () => { const routeBasePath = 'docs'; const plugin = pluginContentDocs( context, - normalizePluginOptions(PluginOptionSchema, { + normalizePluginOptions(OptionsSchema, { routeBasePath, sidebarPath, homePageId: 'hello', }), ); - const env = loadEnv(siteDir, DEFAULT_PLUGIN_ID); - const {docsDir: versionedDir} = env.versioning; - const pluginContentDir = path.join(context.generatedFilesDir, plugin.name); - test('isVersioned', () => { - expect(env.versioning.enabled).toEqual(true); - }); + const pluginContentDir = path.join(context.generatedFilesDir, plugin.name); test('extendCli - docsVersion', () => { - const mock = jest.spyOn(version, 'docsVersion').mockImplementation(); + const mock = jest + .spyOn(cliDocs, 'cliDocsVersionCommand') + .mockImplementation(); const cli = new commander.Command(); - plugin.extendCli(cli); + // @ts-expect-error: TODO annoying type incompatibility + plugin.extendCli!(cli); cli.parse(['node', 'test', 'docs:version', '2.0.0']); + expect(mock).toHaveBeenCalledTimes(1); expect(mock).toHaveBeenCalledWith('2.0.0', siteDir, DEFAULT_PLUGIN_ID, { path: routeBasePath, sidebarPath, @@ -285,21 +319,21 @@ describe('versioned website', () => { }); test('getPathToWatch', () => { - const pathToWatch = plugin.getPathsToWatch(); + const pathToWatch = plugin.getPathsToWatch!(); const matchPattern = pathToWatch.map((filepath) => posixPath(path.relative(siteDir, filepath)), ); expect(matchPattern).not.toEqual([]); expect(matchPattern).toMatchInlineSnapshot(` Array [ + "sidebars.json", "docs/**/*.{md,mdx}", "versioned_sidebars/version-1.0.1-sidebars.json", - "versioned_sidebars/version-1.0.0-sidebars.json", - "versioned_sidebars/version-withSlugs-sidebars.json", "versioned_docs/version-1.0.1/**/*.{md,mdx}", + "versioned_sidebars/version-1.0.0-sidebars.json", "versioned_docs/version-1.0.0/**/*.{md,mdx}", + "versioned_sidebars/version-withSlugs-sidebars.json", "versioned_docs/version-withSlugs/**/*.{md,mdx}", - "sidebars.json", ] `); expect(isMatch('docs/hello.md', matchPattern)).toEqual(true); @@ -335,50 +369,65 @@ describe('versioned website', () => { }); test('content', async () => { - const content = await plugin.loadContent(); - const { - docsMetadata, - docsSidebars, - versionToSidebars, - permalinkToSidebar, - } = content; + const content = await plugin.loadContent!(); + expect(content.loadedVersions.length).toEqual(4); + const [ + currentVersion, + version101, + version100, + versionWithSlugs, + ] = content.loadedVersions; // foo/baz.md only exists in version -1.0.0 - expect(docsMetadata['foo/baz']).toBeUndefined(); - expect(docsMetadata['version-1.0.1/foo/baz']).toBeUndefined(); - expect(docsMetadata['foo/bar']).toEqual({ + expect(findDocById(currentVersion, 'foo/baz')).toBeUndefined(); + expect(findDocById(version101, 'foo/baz')).toBeUndefined(); + expect(findDocById(versionWithSlugs, 'foo/baz')).toBeUndefined(); + + expect(findDocById(currentVersion, 'foo/bar')).toEqual({ + ...defaultDocMetadata, id: 'foo/bar', unversionedId: 'foo/bar', isDocsHomePage: false, permalink: '/docs/next/foo/barSlug', slug: '/foo/barSlug', - source: path.join('@site', routeBasePath, 'foo', 'bar.md'), + source: path.join( + '@site', + path.relative(siteDir, currentVersion.docsDirPath), + 'foo', + 'bar.md', + ), title: 'bar', description: 'This is next version of bar.', - version: 'next', + version: 'current', sidebar: 'docs', next: { title: 'hello', permalink: '/docs/next/', }, }); - expect(docsMetadata.hello).toEqual({ + expect(findDocById(currentVersion, 'hello')).toEqual({ + ...defaultDocMetadata, id: 'hello', unversionedId: 'hello', isDocsHomePage: true, permalink: '/docs/next/', slug: '/', - source: path.join('@site', routeBasePath, 'hello.md'), + source: path.join( + '@site', + path.relative(siteDir, currentVersion.docsDirPath), + 'hello.md', + ), title: 'hello', description: 'Hello next !', - version: 'next', + version: 'current', sidebar: 'docs', previous: { title: 'bar', permalink: '/docs/next/foo/barSlug', }, }); - expect(docsMetadata['version-1.0.1/hello']).toEqual({ + expect(findDocById(version101, 'hello')).toEqual({ + ...defaultDocMetadata, id: 'version-1.0.1/hello', unversionedId: 'hello', isDocsHomePage: true, @@ -386,8 +435,7 @@ describe('versioned website', () => { slug: '/', source: path.join( '@site', - path.relative(siteDir, versionedDir), - 'version-1.0.1', + path.relative(siteDir, version101.docsDirPath), 'hello.md', ), title: 'hello', @@ -399,7 +447,8 @@ describe('versioned website', () => { permalink: '/docs/foo/bar', }, }); - expect(docsMetadata['version-1.0.0/foo/baz']).toEqual({ + expect(findDocById(version100, 'foo/baz')).toEqual({ + ...defaultDocMetadata, id: 'version-1.0.0/foo/baz', unversionedId: 'foo/baz', isDocsHomePage: false, @@ -407,8 +456,7 @@ describe('versioned website', () => { slug: '/foo/baz', source: path.join( '@site', - path.relative(siteDir, versionedDir), - 'version-1.0.0', + path.relative(siteDir, version100.docsDirPath), 'foo', 'baz.md', ), @@ -427,47 +475,24 @@ describe('versioned website', () => { }, }); - expect(docsSidebars).toMatchSnapshot('all sidebars'); - expect(versionToSidebars).toMatchSnapshot( - 'sidebars needed for each version', + expect(currentVersion.sidebars).toMatchSnapshot('current version sidebars'); + expect(version101.sidebars).toMatchSnapshot('101 version sidebars'); + expect(version100.sidebars).toMatchSnapshot('100 version sidebars'); + expect(versionWithSlugs.sidebars).toMatchSnapshot( + 'withSlugs version sidebars', ); + const {actions, utils} = createFakeActions(pluginContentDir); - await plugin.contentLoaded({ + await plugin.contentLoaded!({ content, actions, + allContent: {}, }); - // The created base metadata for each nested docs route is smartly chunked/ splitted across version - const latestVersionBaseMetadata = utils.getCreatedDataByPrefix( - 'docs-route-', - ); - expect(latestVersionBaseMetadata).toMatchSnapshot( - 'base metadata for latest version', - ); - expect(latestVersionBaseMetadata.docsSidebars).not.toEqual(docsSidebars); - expect(latestVersionBaseMetadata.permalinkToSidebar).not.toEqual( - permalinkToSidebar, - ); - const nextVersionBaseMetadata = utils.getCreatedDataByPrefix( - 'docs-next-route-', - ); - expect(nextVersionBaseMetadata).toMatchSnapshot( - 'base metadata for next version', - ); - expect(nextVersionBaseMetadata.docsSidebars).not.toEqual(docsSidebars); - expect(nextVersionBaseMetadata.permalinkToSidebar).not.toEqual( - permalinkToSidebar, - ); - const firstVersionBaseMetadata = utils.getCreatedDataByPrefix( - 'docs-1-0-0-route-', - ); - expect(firstVersionBaseMetadata).toMatchSnapshot( - 'base metadata for first version', - ); - expect(nextVersionBaseMetadata.docsSidebars).not.toEqual(docsSidebars); - expect(nextVersionBaseMetadata.permalinkToSidebar).not.toEqual( - permalinkToSidebar, - ); + utils.checkVersionMetadataPropCreated(currentVersion); + utils.checkVersionMetadataPropCreated(version101); + utils.checkVersionMetadataPropCreated(version100); + utils.checkVersionMetadataPropCreated(versionWithSlugs); utils.expectSnapshot(); }); @@ -481,26 +506,24 @@ describe('versioned website (community)', () => { const pluginId = 'community'; const plugin = pluginContentDocs( context, - normalizePluginOptions(PluginOptionSchema, { + normalizePluginOptions(OptionsSchema, { id: 'community', path: 'community', routeBasePath, sidebarPath, }), ); - const env = loadEnv(siteDir, pluginId); - const {docsDir: versionedDir} = env.versioning; const pluginContentDir = path.join(context.generatedFilesDir, plugin.name); - test('isVersioned', () => { - expect(env.versioning.enabled).toEqual(true); - }); - test('extendCli - docsVersion', () => { - const mock = jest.spyOn(version, 'docsVersion').mockImplementation(); + const mock = jest + .spyOn(cliDocs, 'cliDocsVersionCommand') + .mockImplementation(); const cli = new commander.Command(); - plugin.extendCli(cli); + // @ts-expect-error: TODO annoying type incompatibility + plugin.extendCli!(cli); cli.parse(['node', 'test', `docs:version:${pluginId}`, '2.0.0']); + expect(mock).toHaveBeenCalledTimes(1); expect(mock).toHaveBeenCalledWith('2.0.0', siteDir, pluginId, { path: routeBasePath, sidebarPath, @@ -509,17 +532,17 @@ describe('versioned website (community)', () => { }); test('getPathToWatch', () => { - const pathToWatch = plugin.getPathsToWatch(); + const pathToWatch = plugin.getPathsToWatch!(); const matchPattern = pathToWatch.map((filepath) => posixPath(path.relative(siteDir, filepath)), ); expect(matchPattern).not.toEqual([]); expect(matchPattern).toMatchInlineSnapshot(` Array [ + "community_sidebars.json", "community/**/*.{md,mdx}", "community_versioned_sidebars/version-1.0.0-sidebars.json", "community_versioned_docs/version-1.0.0/**/*.{md,mdx}", - "community_sidebars.json", ] `); expect(isMatch('community/team.md', matchPattern)).toEqual(true); @@ -545,27 +568,29 @@ describe('versioned website (community)', () => { }); test('content', async () => { - const content = await plugin.loadContent(); - const { - docsMetadata, - docsSidebars, - versionToSidebars, - permalinkToSidebar, - } = content; - - expect(docsMetadata.team).toEqual({ + const content = await plugin.loadContent!(); + expect(content.loadedVersions.length).toEqual(2); + const [currentVersion, version100] = content.loadedVersions; + + expect(findDocById(currentVersion, 'team')).toEqual({ + ...defaultDocMetadata, id: 'team', unversionedId: 'team', isDocsHomePage: false, permalink: '/community/next/team', slug: '/team', - source: path.join('@site', routeBasePath, 'team.md'), + source: path.join( + '@site', + path.relative(siteDir, currentVersion.docsDirPath), + 'team.md', + ), title: 'team', description: 'Team current version', - version: 'next', + version: 'current', sidebar: 'community', }); - expect(docsMetadata['version-1.0.0/team']).toEqual({ + expect(findDocById(version100, 'team')).toEqual({ + ...defaultDocMetadata, id: 'version-1.0.0/team', unversionedId: 'team', isDocsHomePage: false, @@ -573,8 +598,7 @@ describe('versioned website (community)', () => { slug: '/team', source: path.join( '@site', - path.relative(siteDir, versionedDir), - 'version-1.0.0', + path.relative(siteDir, version100.docsDirPath), 'team.md', ), title: 'team', @@ -583,38 +607,18 @@ describe('versioned website (community)', () => { sidebar: 'version-1.0.0/community', }); - expect(docsSidebars).toMatchSnapshot('all sidebars'); - expect(versionToSidebars).toMatchSnapshot( - 'sidebars needed for each version', - ); + expect(currentVersion.sidebars).toMatchSnapshot('current version sidebars'); + expect(version100.sidebars).toMatchSnapshot('100 version sidebars'); const {actions, utils} = createFakeActions(pluginContentDir); - await plugin.contentLoaded({ + await plugin.contentLoaded!({ content, actions, + allContent: {}, }); - // The created base metadata for each nested docs route is smartly chunked/ splitted across version - const latestVersionBaseMetadata = utils.getCreatedDataByPrefix( - 'community-route-', - ); - expect(latestVersionBaseMetadata).toMatchSnapshot( - 'base metadata for latest version', - ); - expect(latestVersionBaseMetadata.docsSidebars).not.toEqual(docsSidebars); - expect(latestVersionBaseMetadata.permalinkToSidebar).not.toEqual( - permalinkToSidebar, - ); - const nextVersionBaseMetadata = utils.getCreatedDataByPrefix( - 'community-next-route-', - ); - expect(nextVersionBaseMetadata).toMatchSnapshot( - 'base metadata for next version', - ); - expect(nextVersionBaseMetadata.docsSidebars).not.toEqual(docsSidebars); - expect(nextVersionBaseMetadata.permalinkToSidebar).not.toEqual( - permalinkToSidebar, - ); + utils.checkVersionMetadataPropCreated(currentVersion); + utils.checkVersionMetadataPropCreated(version100); utils.expectSnapshot(); }); 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 6c66b5da9c84..6c106ded9a49 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/lastUpdate.test.ts +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/lastUpdate.test.ts @@ -9,7 +9,7 @@ import fs from 'fs'; import path from 'path'; import shell from 'shelljs'; -import lastUpdate from '../lastUpdate'; +import {getFileLastUpdate} from '../lastUpdate'; describe('lastUpdate', () => { const existingFilePath = path.join( @@ -17,7 +17,7 @@ describe('lastUpdate', () => { '__fixtures__/simple-site/docs/hello.md', ); test('existing test file in repository with Git timestamp', async () => { - const lastUpdateData = await lastUpdate(existingFilePath); + const lastUpdateData = await getFileLastUpdate(existingFilePath); expect(lastUpdateData).not.toBeNull(); const {author, timestamp} = lastUpdateData; @@ -36,29 +36,29 @@ describe('lastUpdate', () => { '__fixtures__', '.nonExisting', ); - expect(await lastUpdate(nonExistingFilePath)).toBeNull(); + expect(await getFileLastUpdate(nonExistingFilePath)).toBeNull(); expect(consoleMock).toHaveBeenCalledTimes(1); expect(consoleMock).toHaveBeenCalledWith( new Error( `Command failed with exit code 128: git log -1 --format=%ct, %an ${nonExistingFilePath}`, ), ); - expect(await lastUpdate(null)).toBeNull(); - expect(await lastUpdate(undefined)).toBeNull(); + expect(await getFileLastUpdate(null)).toBeNull(); + expect(await getFileLastUpdate(undefined)).toBeNull(); consoleMock.mockRestore(); }); test('temporary created file that has no git timestamp', async () => { const tempFilePath = path.join(__dirname, '__fixtures__', '.temp'); fs.writeFileSync(tempFilePath, 'Lorem ipsum :)'); - expect(await lastUpdate(tempFilePath)).toBeNull(); + expect(await getFileLastUpdate(tempFilePath)).toBeNull(); fs.unlinkSync(tempFilePath); }); test('Git does not exist', async () => { const mock = jest.spyOn(shell, 'which').mockImplementationOnce(() => null); const consoleMock = jest.spyOn(console, 'warn').mockImplementation(); - const lastUpdateData = await lastUpdate(existingFilePath); + const lastUpdateData = await getFileLastUpdate(existingFilePath); expect(lastUpdateData).toBeNull(); expect(consoleMock).toHaveBeenLastCalledWith( 'Sorry, the docs plugin last update options require Git.', diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/metadata.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/metadata.test.ts deleted file mode 100644 index 52ffc0afea23..000000000000 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/metadata.test.ts +++ /dev/null @@ -1,464 +0,0 @@ -/** - * 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 path from 'path'; -import {loadContext} from '@docusaurus/core/src/server/index'; -import processMetadata from '../metadata'; -import loadEnv from '../env'; -import {MetadataRaw, Env, MetadataOptions} from '../types'; -import {LoadContext} from '@docusaurus/types'; -import {DEFAULT_PLUGIN_ID} from '@docusaurus/core/lib/constants'; - -const fixtureDir = path.join(__dirname, '__fixtures__'); - -function createTestHelpers({ - siteDir, - context, - env, - options, -}: { - siteDir: string; - context: LoadContext; - env: Env; - options: MetadataOptions; -}) { - async function testMeta( - refDir: string, - source: string, - expectedMetadata: Omit, - ) { - const metadata = await processMetadata({ - source, - refDir, - context, - options, - env, - }); - expect(metadata).toEqual({ - ...expectedMetadata, - source: path.join('@site', path.relative(siteDir, refDir), source), - }); - } - - async function testSlug( - refDir: string, - source: string, - expectedPermalink: string, - ) { - const metadata = await processMetadata({ - source, - refDir, - context, - options, - env, - }); - expect(metadata.permalink).toEqual(expectedPermalink); - } - - return {testMeta, testSlug}; -} - -describe('simple site', () => { - const siteDir = path.join(fixtureDir, 'simple-site'); - const context = loadContext(siteDir); - const routeBasePath = 'docs'; - const docsDir = path.resolve(siteDir, routeBasePath); - const env = loadEnv(siteDir, DEFAULT_PLUGIN_ID); - const options = {routeBasePath}; - - const {testMeta, testSlug} = createTestHelpers({ - siteDir, - context, - options, - env, - }); - - test('normal docs', async () => { - await testMeta(docsDir, path.join('foo', 'bar.md'), { - id: 'foo/bar', - unversionedId: 'foo/bar', - isDocsHomePage: false, - permalink: '/docs/foo/bar', - slug: '/foo/bar', - title: 'Bar', - description: 'This is custom description', - }); - await testMeta(docsDir, path.join('hello.md'), { - id: 'hello', - unversionedId: 'hello', - isDocsHomePage: false, - permalink: '/docs/hello', - slug: '/hello', - title: 'Hello, World !', - description: `Hi, Endilie here :)`, - }); - }); - - test('homePageId doc', async () => { - const {testMeta: testMetaLocal} = createTestHelpers({ - siteDir, - options: { - routeBasePath, - homePageId: 'hello', - }, - context, - env, - }); - - await testMetaLocal(docsDir, path.join('hello.md'), { - id: 'hello', - unversionedId: 'hello', - isDocsHomePage: true, - permalink: '/docs/', - slug: '/', - title: 'Hello, World !', - description: `Hi, Endilie here :)`, - }); - }); - - test('homePageId doc nested', async () => { - const {testMeta: testMetaLocal} = createTestHelpers({ - siteDir, - options: { - routeBasePath, - homePageId: 'foo/bar', - }, - context, - env, - }); - - await testMetaLocal(docsDir, path.join('foo', 'bar.md'), { - id: 'foo/bar', - unversionedId: 'foo/bar', - isDocsHomePage: true, - permalink: '/docs/', - slug: '/', - title: 'Bar', - description: 'This is custom description', - }); - }); - - test('docs with editUrl', async () => { - const {testMeta: testMetaLocal} = createTestHelpers({ - siteDir, - options: { - routeBasePath, - editUrl: 'https://github.com/facebook/docusaurus/edit/master/website', - }, - context, - env, - }); - - await testMetaLocal(docsDir, path.join('foo', 'baz.md'), { - id: 'foo/baz', - unversionedId: 'foo/baz', - isDocsHomePage: false, - permalink: '/docs/foo/bazSlug.html', - slug: '/foo/bazSlug.html', - title: 'baz', - editUrl: - 'https://github.com/facebook/docusaurus/edit/master/website/docs/foo/baz.md', - description: 'Images', - }); - }); - - test('docs with custom editUrl & unrelated frontmatter', async () => { - await testMeta(docsDir, 'lorem.md', { - id: 'lorem', - unversionedId: 'lorem', - isDocsHomePage: false, - permalink: '/docs/lorem', - slug: '/lorem', - title: 'lorem', - editUrl: 'https://github.com/customUrl/docs/lorem.md', - description: 'Lorem ipsum.', - }); - }); - - test('docs with last update time and author', async () => { - const {testMeta: testMetaLocal} = createTestHelpers({ - siteDir, - options: { - routeBasePath, - showLastUpdateAuthor: true, - showLastUpdateTime: true, - }, - context, - env, - }); - - await testMetaLocal(docsDir, 'lorem.md', { - id: 'lorem', - unversionedId: 'lorem', - isDocsHomePage: false, - permalink: '/docs/lorem', - slug: '/lorem', - title: 'lorem', - editUrl: 'https://github.com/customUrl/docs/lorem.md', - description: 'Lorem ipsum.', - lastUpdatedAt: 1539502055, - lastUpdatedBy: 'Author', - }); - }); - - test('docs with null custom_edit_url', async () => { - const {testMeta: testMetaLocal} = createTestHelpers({ - siteDir, - options: { - routeBasePath, - showLastUpdateAuthor: true, - showLastUpdateTime: true, - }, - context, - env, - }); - - await testMetaLocal(docsDir, 'ipsum.md', { - id: 'ipsum', - unversionedId: 'ipsum', - isDocsHomePage: false, - permalink: '/docs/ipsum', - slug: '/ipsum', - title: 'ipsum', - editUrl: null, - description: 'Lorem ipsum.', - lastUpdatedAt: 1539502055, - lastUpdatedBy: 'Author', - }); - }); - - test('docs with slugs', async () => { - await testSlug( - docsDir, - path.join('rootRelativeSlug.md'), - '/docs/rootRelativeSlug', - ); - await testSlug( - docsDir, - path.join('rootAbsoluteSlug.md'), - '/docs/rootAbsoluteSlug', - ); - await testSlug( - docsDir, - path.join('rootResolvedSlug.md'), - '/docs/hey/rootResolvedSlug', - ); - await testSlug( - docsDir, - path.join('rootTryToEscapeSlug.md'), - '/docs/rootTryToEscapeSlug', - ); - - await testSlug( - docsDir, - path.join('slugs', 'absoluteSlug.md'), - '/docs/absoluteSlug', - ); - await testSlug( - docsDir, - path.join('slugs', 'relativeSlug.md'), - '/docs/slugs/relativeSlug', - ); - await testSlug( - docsDir, - path.join('slugs', 'resolvedSlug.md'), - '/docs/slugs/hey/resolvedSlug', - ); - await testSlug( - docsDir, - path.join('slugs', 'tryToEscapeSlug.md'), - '/docs/tryToEscapeSlug', - ); - }); - - test('docs with invalid id', async () => { - const badSiteDir = path.join(fixtureDir, 'bad-id-site'); - - await expect( - processMetadata({ - source: 'invalid-id.md', - refDir: path.join(badSiteDir, 'docs'), - context, - options: { - routeBasePath, - }, - env, - }), - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Document id cannot include \\"/\\"."`, - ); - }); - - test('docs with slug on doc home', async () => { - const badSiteDir = path.join(fixtureDir, 'bad-slug-on-doc-home-site'); - - await expect( - processMetadata({ - source: 'docWithSlug.md', - refDir: path.join(badSiteDir, 'docs'), - context, - options: { - routeBasePath, - homePageId: 'docWithSlug', - }, - env, - }), - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"The docs homepage (homePageId=docWithSlug) is not allowed to have a frontmatter slug=docWithSlug.html => you have to chooser either homePageId or slug, not both"`, - ); - }); -}); - -describe('versioned site', () => { - const siteDir = path.join(fixtureDir, 'versioned-site'); - const context = loadContext(siteDir); - const routeBasePath = 'docs'; - const docsDir = path.resolve(siteDir, routeBasePath); - const env = loadEnv(siteDir, DEFAULT_PLUGIN_ID); - const {docsDir: versionedDir} = env.versioning; - const options = {routeBasePath}; - - const {testMeta, testSlug} = createTestHelpers({ - siteDir, - context, - options, - env, - }); - - test('next docs', async () => { - await testMeta(docsDir, path.join('foo', 'bar.md'), { - id: 'foo/bar', - unversionedId: 'foo/bar', - isDocsHomePage: false, - permalink: '/docs/next/foo/barSlug', - slug: '/foo/barSlug', - title: 'bar', - description: 'This is next version of bar.', - version: 'next', - }); - await testMeta(docsDir, path.join('hello.md'), { - id: 'hello', - unversionedId: 'hello', - isDocsHomePage: false, - permalink: '/docs/next/hello', - slug: '/hello', - title: 'hello', - description: 'Hello next !', - version: 'next', - }); - }); - - test('versioned docs', async () => { - await testMeta(versionedDir, path.join('version-1.0.0', 'foo', 'bar.md'), { - id: 'version-1.0.0/foo/bar', - unversionedId: 'foo/bar', - isDocsHomePage: false, - permalink: '/docs/1.0.0/foo/barSlug', - slug: '/foo/barSlug', - title: 'bar', - description: 'Bar 1.0.0 !', - version: '1.0.0', - }); - await testMeta(versionedDir, path.join('version-1.0.0', 'hello.md'), { - id: 'version-1.0.0/hello', - unversionedId: 'hello', - isDocsHomePage: false, - permalink: '/docs/1.0.0/hello', - slug: '/hello', - title: 'hello', - description: 'Hello 1.0.0 !', - version: '1.0.0', - }); - await testMeta(versionedDir, path.join('version-1.0.1', 'foo', 'bar.md'), { - id: 'version-1.0.1/foo/bar', - unversionedId: 'foo/bar', - isDocsHomePage: false, - permalink: '/docs/foo/bar', - slug: '/foo/bar', - title: 'bar', - description: 'Bar 1.0.1 !', - version: '1.0.1', - }); - await testMeta(versionedDir, path.join('version-1.0.1', 'hello.md'), { - id: 'version-1.0.1/hello', - unversionedId: 'hello', - isDocsHomePage: false, - permalink: '/docs/hello', - slug: '/hello', - title: 'hello', - description: 'Hello 1.0.1 !', - version: '1.0.1', - }); - }); - - test('next doc slugs', async () => { - await testSlug( - docsDir, - path.join('slugs', 'absoluteSlug.md'), - '/docs/next/absoluteSlug', - ); - await testSlug( - docsDir, - path.join('slugs', 'relativeSlug.md'), - '/docs/next/slugs/relativeSlug', - ); - await testSlug( - docsDir, - path.join('slugs', 'resolvedSlug.md'), - '/docs/next/slugs/hey/resolvedSlug', - ); - await testSlug( - docsDir, - path.join('slugs', 'tryToEscapeSlug.md'), - '/docs/next/tryToEscapeSlug', - ); - }); - - test('versioned doc slugs', async () => { - await testSlug( - versionedDir, - path.join('version-withSlugs', 'rootAbsoluteSlug.md'), - '/docs/withSlugs/rootAbsoluteSlug', - ); - await testSlug( - versionedDir, - path.join('version-withSlugs', 'rootRelativeSlug.md'), - '/docs/withSlugs/rootRelativeSlug', - ); - await testSlug( - versionedDir, - path.join('version-withSlugs', 'rootResolvedSlug.md'), - '/docs/withSlugs/hey/rootResolvedSlug', - ); - await testSlug( - versionedDir, - path.join('version-withSlugs', 'rootTryToEscapeSlug.md'), - '/docs/withSlugs/rootTryToEscapeSlug', - ); - - await testSlug( - versionedDir, - path.join('version-withSlugs', 'slugs', 'absoluteSlug.md'), - '/docs/withSlugs/absoluteSlug', - ); - await testSlug( - versionedDir, - path.join('version-withSlugs', 'slugs', 'relativeSlug.md'), - '/docs/withSlugs/slugs/relativeSlug', - ); - await testSlug( - versionedDir, - path.join('version-withSlugs', 'slugs', 'resolvedSlug.md'), - '/docs/withSlugs/slugs/hey/resolvedSlug', - ); - await testSlug( - versionedDir, - path.join('version-withSlugs', 'slugs', 'tryToEscapeSlug.md'), - '/docs/withSlugs/tryToEscapeSlug', - ); - }); -}); diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/pluginOptionSchema.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/options.test.ts similarity index 85% rename from packages/docusaurus-plugin-content-docs/src/__tests__/pluginOptionSchema.test.ts rename to packages/docusaurus-plugin-content-docs/src/__tests__/options.test.ts index 4fb4304ff01d..d065896f1c32 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/pluginOptionSchema.test.ts +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/options.test.ts @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import {PluginOptionSchema, DEFAULT_OPTIONS} from '../pluginOptionSchema'; +import {OptionsSchema, DEFAULT_OPTIONS} from '../options'; import {normalizePluginOptions} from '@docusaurus/utils-validation'; // the type of remark/rehype plugins is function @@ -14,7 +14,7 @@ const markdownPluginsObjectStub = {}; describe('normalizeDocsPluginOptions', () => { test('should return default options for undefined user options', async () => { - const {value, error} = await PluginOptionSchema.validate({}); + const {value, error} = await OptionsSchema.validate({}); expect(value).toEqual(DEFAULT_OPTIONS); expect(error).toBe(undefined); }); @@ -34,9 +34,10 @@ describe('normalizeDocsPluginOptions', () => { showLastUpdateAuthor: true, admonitions: {}, excludeNextVersionDocs: true, + includeCurrentVersion: false, disableVersioning: true, }; - const {value, error} = await PluginOptionSchema.validate(userOptions); + const {value, error} = await OptionsSchema.validate(userOptions); expect(value).toEqual(userOptions); expect(error).toBe(undefined); }); @@ -50,14 +51,14 @@ describe('normalizeDocsPluginOptions', () => { [markdownPluginsFunctionStub, {option1: '42'}], ], }; - const {value, error} = await PluginOptionSchema.validate(userOptions); + const {value, error} = await OptionsSchema.validate(userOptions); expect(value).toEqual(userOptions); expect(error).toBe(undefined); }); test('should reject invalid remark plugin options', () => { expect(() => { - normalizePluginOptions(PluginOptionSchema, { + normalizePluginOptions(OptionsSchema, { remarkPlugins: [[{option1: '42'}, markdownPluginsFunctionStub]], }); }).toThrowErrorMatchingInlineSnapshot( @@ -67,7 +68,7 @@ describe('normalizeDocsPluginOptions', () => { test('should reject invalid rehype plugin options', () => { expect(() => { - normalizePluginOptions(PluginOptionSchema, { + normalizePluginOptions(OptionsSchema, { rehypePlugins: [ [ markdownPluginsFunctionStub, @@ -83,7 +84,7 @@ describe('normalizeDocsPluginOptions', () => { test('should reject bad path inputs', () => { expect(() => { - normalizePluginOptions(PluginOptionSchema, { + normalizePluginOptions(OptionsSchema, { path: 2, }); }).toThrowErrorMatchingInlineSnapshot(`"\\"path\\" must be a string"`); @@ -91,7 +92,7 @@ describe('normalizeDocsPluginOptions', () => { test('should reject bad include inputs', () => { expect(() => { - normalizePluginOptions(PluginOptionSchema, { + normalizePluginOptions(OptionsSchema, { include: '**/*.{md,mdx}', }); }).toThrowErrorMatchingInlineSnapshot(`"\\"include\\" must be an array"`); @@ -99,7 +100,7 @@ describe('normalizeDocsPluginOptions', () => { test('should reject bad showLastUpdateTime inputs', () => { expect(() => { - normalizePluginOptions(PluginOptionSchema, { + normalizePluginOptions(OptionsSchema, { showLastUpdateTime: 'true', }); }).toThrowErrorMatchingInlineSnapshot( @@ -109,7 +110,7 @@ describe('normalizeDocsPluginOptions', () => { test('should reject bad remarkPlugins input', () => { expect(() => { - normalizePluginOptions(PluginOptionSchema, { + normalizePluginOptions(OptionsSchema, { remarkPlugins: 'remark-math', }); }).toThrowErrorMatchingInlineSnapshot( diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/order.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/order.test.ts deleted file mode 100644 index 1f6bc057c29d..000000000000 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/order.test.ts +++ /dev/null @@ -1,240 +0,0 @@ -/** - * 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 createOrder from '../order'; - -describe('createOrder', () => { - test('multiple sidebars with subcategory', () => { - const result = createOrder({ - docs: [ - { - type: 'category', - label: 'Category1', - items: [ - { - type: 'category', - label: 'Subcategory 1', - items: [{type: 'doc', id: 'doc1'}], - }, - { - type: 'category', - label: 'Subcategory 2', - items: [{type: 'doc', id: 'doc2'}], - }, - ], - }, - { - type: 'category', - label: 'Category2', - items: [ - {type: 'doc', id: 'doc3'}, - {type: 'doc', id: 'doc4'}, - ], - }, - ], - otherDocs: [ - { - type: 'category', - label: 'Category1', - items: [{type: 'doc', id: 'doc5'}], - }, - ], - }); - expect(result).toEqual({ - doc1: { - next: 'doc2', - previous: undefined, - sidebar: 'docs', - }, - doc2: { - next: 'doc3', - previous: 'doc1', - sidebar: 'docs', - }, - doc3: { - next: 'doc4', - previous: 'doc2', - sidebar: 'docs', - }, - doc4: { - next: undefined, - previous: 'doc3', - sidebar: 'docs', - }, - doc5: { - next: undefined, - previous: undefined, - sidebar: 'otherDocs', - }, - }); - }); - test('multiple sidebars without subcategory', () => { - const result = createOrder({ - docs: [ - { - type: 'category', - label: 'Category1', - items: [ - {type: 'doc', id: 'doc1'}, - {type: 'doc', id: 'doc2'}, - ], - }, - { - type: 'category', - label: 'Category2', - items: [ - {type: 'doc', id: 'doc3'}, - {type: 'doc', id: 'doc4'}, - ], - }, - ], - otherDocs: [ - { - type: 'category', - label: 'Category1', - items: [{type: 'doc', id: 'doc5'}], - }, - ], - }); - expect(result).toEqual({ - doc1: { - next: 'doc2', - previous: undefined, - sidebar: 'docs', - }, - doc2: { - next: 'doc3', - previous: 'doc1', - sidebar: 'docs', - }, - doc3: { - next: 'doc4', - previous: 'doc2', - sidebar: 'docs', - }, - doc4: { - next: undefined, - previous: 'doc3', - sidebar: 'docs', - }, - doc5: { - next: undefined, - previous: undefined, - sidebar: 'otherDocs', - }, - }); - }); - - test('versioned sidebars', () => { - const result = createOrder({ - docs: [ - { - type: 'category', - label: 'Category1', - items: [{type: 'doc', id: 'doc1'}], - }, - ], - 'version-1.2.3-docs': [ - { - type: 'category', - label: 'Category1', - items: [{type: 'doc', id: 'version-1.2.3-doc2'}], - }, - { - type: 'category', - label: 'Category2', - items: [{type: 'doc', id: 'version-1.2.3-doc1'}], - }, - ], - }); - expect(result).toEqual({ - doc1: { - next: undefined, - previous: undefined, - sidebar: 'docs', - }, - 'version-1.2.3-doc1': { - next: undefined, - previous: 'version-1.2.3-doc2', - sidebar: 'version-1.2.3-docs', - }, - 'version-1.2.3-doc2': { - next: 'version-1.2.3-doc1', - previous: undefined, - sidebar: 'version-1.2.3-docs', - }, - }); - }); - - test('multiple sidebars with subcategories, refs and external links', () => { - const result = createOrder({ - docs: [ - { - type: 'category', - label: 'Category1', - items: [ - { - type: 'category', - label: 'Subcategory 1', - items: [{type: 'link', href: '//example.com', label: 'bar'}], - }, - { - type: 'category', - label: 'Subcategory 2', - items: [{type: 'doc', id: 'doc2'}], - }, - { - type: 'category', - label: 'Subcategory 1', - items: [{type: 'link', href: '//example2.com', label: 'baz'}], - }, - ], - }, - { - type: 'category', - label: 'Category2', - items: [ - {type: 'doc', id: 'doc3'}, - {type: 'ref', id: 'doc4'}, - ], - }, - ], - otherDocs: [ - { - type: 'category', - label: 'Category1', - items: [{type: 'doc', id: 'doc5'}], - }, - ], - }); - expect(result).toEqual({ - doc2: { - next: 'doc3', - previous: undefined, - sidebar: 'docs', - }, - doc3: { - next: undefined, - previous: 'doc2', - sidebar: 'docs', - }, - doc5: { - next: undefined, - previous: undefined, - sidebar: 'otherDocs', - }, - }); - }); - - test('edge cases', () => { - expect(createOrder({})).toEqual({}); - expect(createOrder(undefined)).toEqual({}); - expect(() => createOrder(null)).toThrowErrorMatchingInlineSnapshot( - `"Cannot convert undefined or null to object"`, - ); - }); -}); diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/sidebars.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/sidebars.test.ts index 1d322bba4463..ec8f6fb4c36b 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/sidebars.test.ts +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/sidebars.test.ts @@ -6,7 +6,13 @@ */ import path from 'path'; -import loadSidebars from '../sidebars'; +import { + loadSidebars, + collectSidebarDocItems, + collectSidebarsDocIds, + createSidebarsUtils, +} from '../sidebars'; +import {Sidebar, Sidebars} from '../types'; /* eslint-disable global-require, import/no-dynamic-require */ @@ -14,13 +20,13 @@ describe('loadSidebars', () => { const fixtureDir = path.join(__dirname, '__fixtures__', 'sidebars'); test('sidebars with known sidebar item type', async () => { const sidebarPath = path.join(fixtureDir, 'sidebars.json'); - const result = loadSidebars([sidebarPath]); + const result = loadSidebars(sidebarPath); expect(result).toMatchSnapshot(); }); test('sidebars with deep level of category', async () => { const sidebarPath = path.join(fixtureDir, 'sidebars-category.js'); - const result = loadSidebars([sidebarPath]); + const result = loadSidebars(sidebarPath); expect(result).toMatchSnapshot(); }); @@ -30,8 +36,8 @@ describe('loadSidebars', () => { fixtureDir, 'sidebars-category-shorthand.js', ); - const sidebar1 = loadSidebars([sidebarPath1]); - const sidebar2 = loadSidebars([sidebarPath2]); + const sidebar1 = loadSidebars(sidebarPath1); + const sidebar2 = loadSidebars(sidebarPath2); expect(sidebar1).toEqual(sidebar2); }); @@ -40,9 +46,7 @@ describe('loadSidebars', () => { fixtureDir, 'sidebars-category-wrong-items.json', ); - expect(() => - loadSidebars([sidebarPath]), - ).toThrowErrorMatchingInlineSnapshot( + expect(() => loadSidebars(sidebarPath)).toThrowErrorMatchingInlineSnapshot( `"Error loading {\\"type\\":\\"category\\",\\"label\\":\\"Category Label\\",\\"items\\":\\"doc1\\"}. \\"items\\" must be an array."`, ); }); @@ -52,9 +56,7 @@ describe('loadSidebars', () => { fixtureDir, 'sidebars-category-wrong-label.json', ); - expect(() => - loadSidebars([sidebarPath]), - ).toThrowErrorMatchingInlineSnapshot( + expect(() => loadSidebars(sidebarPath)).toThrowErrorMatchingInlineSnapshot( `"Error loading {\\"type\\":\\"category\\",\\"label\\":true,\\"items\\":[\\"doc1\\"]}. \\"label\\" must be a string."`, ); }); @@ -64,9 +66,7 @@ describe('loadSidebars', () => { fixtureDir, 'sidebars-doc-id-not-string.json', ); - expect(() => - loadSidebars([sidebarPath]), - ).toThrowErrorMatchingInlineSnapshot( + expect(() => loadSidebars(sidebarPath)).toThrowErrorMatchingInlineSnapshot( `"Error loading {\\"type\\":\\"doc\\",\\"id\\":[\\"doc1\\"]}. \\"id\\" must be a string."`, ); }); @@ -76,60 +76,75 @@ describe('loadSidebars', () => { fixtureDir, 'sidebars-first-level-not-category.js', ); - const result = loadSidebars([sidebarPath]); + const result = loadSidebars(sidebarPath); expect(result).toMatchSnapshot(); }); test('sidebars link', async () => { const sidebarPath = path.join(fixtureDir, 'sidebars-link.json'); - const result = loadSidebars([sidebarPath]); + const result = loadSidebars(sidebarPath); expect(result).toMatchSnapshot(); }); test('sidebars link wrong label', async () => { const sidebarPath = path.join(fixtureDir, 'sidebars-link-wrong-label.json'); - expect(() => - loadSidebars([sidebarPath]), - ).toThrowErrorMatchingInlineSnapshot( + expect(() => loadSidebars(sidebarPath)).toThrowErrorMatchingInlineSnapshot( `"Error loading {\\"type\\":\\"link\\",\\"label\\":false,\\"href\\":\\"https://github.com\\"}. \\"label\\" must be a string."`, ); }); test('sidebars link wrong href', async () => { const sidebarPath = path.join(fixtureDir, 'sidebars-link-wrong-href.json'); - expect(() => - loadSidebars([sidebarPath]), - ).toThrowErrorMatchingInlineSnapshot( + expect(() => loadSidebars(sidebarPath)).toThrowErrorMatchingInlineSnapshot( `"Error loading {\\"type\\":\\"link\\",\\"label\\":\\"GitHub\\",\\"href\\":[\\"example.com\\"]}. \\"href\\" must be a string."`, ); }); test('sidebars with unknown sidebar item type', async () => { const sidebarPath = path.join(fixtureDir, 'sidebars-unknown-type.json'); - expect(() => - loadSidebars([sidebarPath]), - ).toThrowErrorMatchingInlineSnapshot( + expect(() => loadSidebars(sidebarPath)).toThrowErrorMatchingInlineSnapshot( `"Unknown sidebar item type [superman]. Sidebar item={\\"type\\":\\"superman\\"} "`, ); }); test('sidebars with known sidebar item type but wrong field', async () => { const sidebarPath = path.join(fixtureDir, 'sidebars-wrong-field.json'); + expect(() => loadSidebars(sidebarPath)).toThrowErrorMatchingInlineSnapshot( + `"Unknown sidebar item keys: href. Item: {\\"type\\":\\"category\\",\\"label\\":\\"category\\",\\"href\\":\\"https://github.com\\"}"`, + ); + }); + + test('unexisting path', () => { + expect(() => loadSidebars('badpath')).toThrowErrorMatchingInlineSnapshot( + `"No sidebar file exist at path: badpath"`, + ); + }); + + test('undefined path', () => { expect(() => - loadSidebars([sidebarPath]), + loadSidebars( + // @ts-expect-error: bad arg + undefined, + ), ).toThrowErrorMatchingInlineSnapshot( - `"Unknown sidebar item keys: href. Item: {\\"type\\":\\"category\\",\\"label\\":\\"category\\",\\"href\\":\\"https://github.com\\"}"`, + `"sidebarFilePath not provided: undefined"`, ); }); - test('no sidebars', () => { - const result = loadSidebars(null); - expect(result).toEqual({}); + test('null path', () => { + expect(() => + loadSidebars( + // @ts-expect-error: bad arg + null, + ), + ).toThrowErrorMatchingInlineSnapshot( + `"sidebarFilePath not provided: null"`, + ); }); test('sidebars with category.collapsed property', async () => { const sidebarPath = path.join(fixtureDir, 'sidebars-collapsed.json'); - const result = loadSidebars([sidebarPath]); + const result = loadSidebars(sidebarPath); expect(result).toMatchSnapshot(); }); @@ -138,7 +153,177 @@ describe('loadSidebars', () => { fixtureDir, 'sidebars-collapsed-first-level.json', ); - const result = loadSidebars([sidebarPath]); + const result = loadSidebars(sidebarPath); expect(result).toMatchSnapshot(); }); }); + +describe('collectSidebarDocItems', () => { + test('can collect recursively', async () => { + const sidebar: Sidebar = [ + { + type: 'category', + collapsed: false, + label: 'Category1', + items: [ + { + type: 'category', + collapsed: false, + label: 'Subcategory 1', + items: [{type: 'doc', id: 'doc1'}], + }, + { + type: 'category', + collapsed: false, + label: 'Subcategory 2', + items: [ + {type: 'doc', id: 'doc2'}, + { + type: 'category', + collapsed: false, + label: 'Sub sub category 1', + items: [{type: 'doc', id: 'doc3'}], + }, + ], + }, + ], + }, + { + type: 'category', + collapsed: false, + label: 'Category2', + items: [ + {type: 'doc', id: 'doc4'}, + {type: 'doc', id: 'doc5'}, + ], + }, + ]; + + expect(collectSidebarDocItems(sidebar).map((doc) => doc.id)).toEqual([ + 'doc1', + 'doc2', + 'doc3', + 'doc4', + 'doc5', + ]); + }); +}); + +describe('collectSidebarsDocItems', () => { + test('can collect sidebars doc items', async () => { + const sidebar1: Sidebar = [ + { + type: 'category', + collapsed: false, + label: 'Category1', + items: [ + { + type: 'category', + collapsed: false, + label: 'Subcategory 1', + items: [{type: 'doc', id: 'doc1'}], + }, + {type: 'doc', id: 'doc2'}, + ], + }, + ]; + + const sidebar2: Sidebar = [ + { + type: 'category', + collapsed: false, + label: 'Category2', + items: [ + {type: 'doc', id: 'doc3'}, + {type: 'doc', id: 'doc4'}, + ], + }, + ]; + + const sidebar3: Sidebar = [ + {type: 'doc', id: 'doc5'}, + {type: 'doc', id: 'doc6'}, + ]; + expect(collectSidebarsDocIds({sidebar1, sidebar2, sidebar3})).toEqual({ + sidebar1: ['doc1', 'doc2'], + sidebar2: ['doc3', 'doc4'], + sidebar3: ['doc5', 'doc6'], + }); + }); +}); + +describe('createSidebarsUtils', () => { + const sidebar1: Sidebar = [ + { + type: 'category', + collapsed: false, + label: 'Category1', + items: [ + { + type: 'category', + collapsed: false, + label: 'Subcategory 1', + items: [{type: 'doc', id: 'doc1'}], + }, + {type: 'doc', id: 'doc2'}, + ], + }, + ]; + + const sidebar2: Sidebar = [ + { + type: 'category', + collapsed: false, + label: 'Category2', + items: [ + {type: 'doc', id: 'doc3'}, + {type: 'doc', id: 'doc4'}, + ], + }, + ]; + + const sidebars: Sidebars = {sidebar1, sidebar2}; + + const { + getFirstDocIdOfFirstSidebar, + getSidebarNameByDocId, + getDocNavigation, + } = createSidebarsUtils(sidebars); + + test('getSidebarNameByDocId', async () => { + expect(getFirstDocIdOfFirstSidebar()).toEqual('doc1'); + }); + + test('getSidebarNameByDocId', async () => { + expect(getSidebarNameByDocId('doc1')).toEqual('sidebar1'); + expect(getSidebarNameByDocId('doc2')).toEqual('sidebar1'); + expect(getSidebarNameByDocId('doc3')).toEqual('sidebar2'); + expect(getSidebarNameByDocId('doc4')).toEqual('sidebar2'); + expect(getSidebarNameByDocId('doc5')).toEqual(undefined); + expect(getSidebarNameByDocId('doc6')).toEqual(undefined); + }); + + test('getDocNavigation', async () => { + expect(getDocNavigation('doc1')).toEqual({ + sidebarName: 'sidebar1', + previousId: undefined, + nextId: 'doc2', + }); + expect(getDocNavigation('doc2')).toEqual({ + sidebarName: 'sidebar1', + previousId: 'doc1', + nextId: undefined, + }); + + expect(getDocNavigation('doc3')).toEqual({ + sidebarName: 'sidebar2', + previousId: undefined, + nextId: 'doc4', + }); + expect(getDocNavigation('doc4')).toEqual({ + sidebarName: 'sidebar2', + previousId: 'doc3', + nextId: undefined, + }); + }); +}); diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/versions.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/versions.test.ts new file mode 100644 index 000000000000..bba60d89f2e8 --- /dev/null +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/versions.test.ts @@ -0,0 +1,345 @@ +/** + * 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 path from 'path'; +import { + getVersionsFilePath, + getVersionedDocsDirPath, + getVersionedSidebarsDirPath, + readVersionsMetadata, +} from '../versions'; +import {DEFAULT_OPTIONS} from '../options'; +import {DEFAULT_PLUGIN_ID} from '@docusaurus/core/lib/constants'; +import {VersionMetadata} from '../types'; + +describe('version paths', () => { + test('getVersionedDocsDirPath', () => { + expect(getVersionsFilePath('someSiteDir', DEFAULT_PLUGIN_ID)).toBe( + 'someSiteDir/versions.json', + ); + expect(getVersionsFilePath('otherSite/dir', 'pluginId')).toBe( + 'otherSite/dir/pluginId_versions.json', + ); + }); + + test('getVersionedDocsDirPath', () => { + expect(getVersionedDocsDirPath('someSiteDir', DEFAULT_PLUGIN_ID)).toBe( + 'someSiteDir/versioned_docs', + ); + expect(getVersionedDocsDirPath('otherSite/dir', 'pluginId')).toBe( + 'otherSite/dir/pluginId_versioned_docs', + ); + }); + + test('getVersionedSidebarsDirPath', () => { + expect(getVersionedSidebarsDirPath('someSiteDir', DEFAULT_PLUGIN_ID)).toBe( + 'someSiteDir/versioned_sidebars', + ); + expect(getVersionedSidebarsDirPath('otherSite/dir', 'pluginId')).toBe( + 'otherSite/dir/pluginId_versioned_sidebars', + ); + }); +}); + +describe('simple site', () => { + const simpleSiteDir = path.resolve( + path.join(__dirname, '__fixtures__', 'simple-site'), + ); + const defaultOptions = { + id: DEFAULT_PLUGIN_ID, + ...DEFAULT_OPTIONS, + }; + const defaultContext = { + siteDir: simpleSiteDir, + baseUrl: '/', + }; + + const vCurrent: VersionMetadata = { + docsDirPath: path.join(simpleSiteDir, 'docs'), + isLast: true, + routePriority: -1, + sidebarFilePath: path.join(simpleSiteDir, 'sidebars.json'), + versionLabel: 'Next', + versionName: 'current', + versionPath: '/docs', + }; + + test('readVersionsMetadata simple site', () => { + const versionsMetadata = readVersionsMetadata({ + options: defaultOptions, + context: defaultContext, + }); + + expect(versionsMetadata).toEqual([vCurrent]); + }); + + test('readVersionsMetadata simple site with base url', () => { + const versionsMetadata = readVersionsMetadata({ + options: defaultOptions, + context: { + ...defaultContext, + baseUrl: '/myBaseUrl', + }, + }); + + expect(versionsMetadata).toEqual([ + { + ...vCurrent, + versionPath: '/myBaseUrl/docs', + }, + ]); + }); + + test('readVersionsMetadata simple site with base url', () => { + expect(() => + readVersionsMetadata({ + options: {...defaultOptions, disableVersioning: true}, + context: defaultContext, + }), + ).toThrowErrorMatchingInlineSnapshot( + `"Docs: using disableVersioning=true option on a non-versioned site does not make sense"`, + ); + }); + + test('readVersionsMetadata simple site with base url', () => { + expect(() => + readVersionsMetadata({ + options: {...defaultOptions, includeCurrentVersion: false}, + context: defaultContext, + }), + ).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', () => { + const versionedSiteDir = path.resolve( + path.join(__dirname, '__fixtures__', 'versioned-site'), + ); + const defaultOptions = { + id: DEFAULT_PLUGIN_ID, + ...DEFAULT_OPTIONS, + }; + const defaultContext = { + siteDir: versionedSiteDir, + baseUrl: '/', + }; + + const vCurrent: VersionMetadata = { + docsDirPath: path.join(versionedSiteDir, 'docs'), + isLast: false, + routePriority: undefined, + sidebarFilePath: path.join(versionedSiteDir, 'sidebars.json'), + versionLabel: 'Next', + versionName: 'current', + versionPath: '/docs/next', + }; + + const v101: VersionMetadata = { + docsDirPath: path.join(versionedSiteDir, 'versioned_docs/version-1.0.1'), + isLast: true, + routePriority: -1, + sidebarFilePath: path.join( + versionedSiteDir, + 'versioned_sidebars/version-1.0.1-sidebars.json', + ), + versionLabel: '1.0.1', + versionName: '1.0.1', + versionPath: '/docs', + }; + + const v100: VersionMetadata = { + docsDirPath: path.join(versionedSiteDir, 'versioned_docs/version-1.0.0'), + isLast: false, + routePriority: undefined, + sidebarFilePath: path.join( + versionedSiteDir, + 'versioned_sidebars/version-1.0.0-sidebars.json', + ), + versionLabel: '1.0.0', + versionName: '1.0.0', + versionPath: '/docs/1.0.0', + }; + + const vwithSlugs: VersionMetadata = { + docsDirPath: path.join( + versionedSiteDir, + 'versioned_docs/version-withSlugs', + ), + isLast: false, + routePriority: undefined, + sidebarFilePath: path.join( + versionedSiteDir, + 'versioned_sidebars/version-withSlugs-sidebars.json', + ), + versionLabel: 'withSlugs', + versionName: 'withSlugs', + versionPath: '/docs/withSlugs', + }; + + test('readVersionsMetadata versioned site', () => { + const versionsMetadata = readVersionsMetadata({ + options: defaultOptions, + context: defaultContext, + }); + + expect(versionsMetadata).toEqual([vCurrent, v101, v100, vwithSlugs]); + }); + + test('readVersionsMetadata versioned site with includeCurrentVersion=false', () => { + const versionsMetadata = readVersionsMetadata({ + options: {...defaultOptions, includeCurrentVersion: false}, + context: defaultContext, + }); + + expect(versionsMetadata).toEqual([ + // vCurrent removed + v101, + v100, + vwithSlugs, + ]); + }); + + test('readVersionsMetadata versioned site with disableVersioning', () => { + const versionsMetadata = readVersionsMetadata({ + options: {...defaultOptions, disableVersioning: true}, + context: defaultContext, + }); + + expect(versionsMetadata).toEqual([ + {...vCurrent, isLast: true, routePriority: -1, versionPath: '/docs'}, + ]); + }); + + test('readVersionsMetadata versioned site with all versions disabled', () => { + expect(() => + readVersionsMetadata({ + options: { + ...defaultOptions, + includeCurrentVersion: false, + disableVersioning: true, + }, + context: defaultContext, + }), + ).toThrowErrorMatchingInlineSnapshot( + `"It is not possible to use docs without any version. Please check the configuration of these options: includeCurrentVersion=false disableVersioning=true"`, + ); + }); + + test('readVersionsMetadata versioned site with invalid versions.json file', () => { + const mock = jest.spyOn(JSON, 'parse').mockImplementationOnce(() => { + return { + invalid: 'json', + }; + }); + + expect(() => { + readVersionsMetadata({ + options: defaultOptions, + context: defaultContext, + }); + }).toThrowErrorMatchingInlineSnapshot( + `"The versions file should contain an array of versions! Found content={\\"invalid\\":\\"json\\"}"`, + ); + mock.mockRestore(); + }); +}); + +describe('versioned site, pluginId=community', () => { + const versionedSiteDir = path.resolve( + path.join(__dirname, '__fixtures__', 'versioned-site'), + ); + const defaultOptions = { + ...DEFAULT_OPTIONS, + id: 'community', + path: 'community', + routeBasePath: 'communityBasePath', + }; + const defaultContext = { + siteDir: versionedSiteDir, + baseUrl: '/', + }; + + const vCurrent: VersionMetadata = { + docsDirPath: path.join(versionedSiteDir, 'community'), + isLast: false, + routePriority: undefined, + sidebarFilePath: path.join(versionedSiteDir, 'sidebars.json'), + versionLabel: 'Next', + versionName: 'current', + versionPath: '/communityBasePath/next', + }; + + const v100: VersionMetadata = { + docsDirPath: path.join( + versionedSiteDir, + 'community_versioned_docs/version-1.0.0', + ), + isLast: true, + routePriority: -1, + sidebarFilePath: path.join( + versionedSiteDir, + 'community_versioned_sidebars/version-1.0.0-sidebars.json', + ), + versionLabel: '1.0.0', + versionName: '1.0.0', + versionPath: '/communityBasePath', + }; + + test('readVersionsMetadata versioned site (community)', () => { + const versionsMetadata = readVersionsMetadata({ + options: defaultOptions, + context: defaultContext, + }); + + expect(versionsMetadata).toEqual([vCurrent, v100]); + }); + + test('readVersionsMetadata versioned site (community) with includeCurrentVersion=false', () => { + const versionsMetadata = readVersionsMetadata({ + options: {...defaultOptions, includeCurrentVersion: false}, + context: defaultContext, + }); + + expect(versionsMetadata).toEqual([ + // vCurrent removed + v100, + ]); + }); + + test('readVersionsMetadata versioned site (community) with disableVersioning', () => { + const versionsMetadata = readVersionsMetadata({ + options: {...defaultOptions, disableVersioning: true}, + context: defaultContext, + }); + + expect(versionsMetadata).toEqual([ + { + ...vCurrent, + isLast: true, + routePriority: -1, + versionPath: '/communityBasePath', + }, + ]); + }); + + test('readVersionsMetadata versioned site (community) with all versions disabled', () => { + expect(() => + readVersionsMetadata({ + options: { + ...defaultOptions, + includeCurrentVersion: false, + disableVersioning: true, + }, + context: defaultContext, + }), + ).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/version.ts b/packages/docusaurus-plugin-content-docs/src/cli.ts similarity index 85% rename from packages/docusaurus-plugin-content-docs/src/version.ts rename to packages/docusaurus-plugin-content-docs/src/cli.ts index 860beaffb53b..75b734c04b0d 100644 --- a/packages/docusaurus-plugin-content-docs/src/version.ts +++ b/packages/docusaurus-plugin-content-docs/src/cli.ts @@ -6,19 +6,19 @@ */ import { - getVersionsJSONFile, - getVersionedDocsDir, - getVersionedSidebarsDir, -} from './env'; + getVersionsFilePath, + getVersionedDocsDirPath, + getVersionedSidebarsDirPath, +} from './versions'; import fs from 'fs-extra'; import path from 'path'; -import {Sidebar, PathOptions, SidebarItem} from './types'; -import loadSidebars from './sidebars'; +import {Sidebars, PathOptions, SidebarItem} from './types'; +import {loadSidebars} from './sidebars'; import {DEFAULT_PLUGIN_ID} from '@docusaurus/core/lib/constants'; // Tests depend on non-default export for mocking. // eslint-disable-next-line import/prefer-default-export -export function docsVersion( +export function cliDocsVersionCommand( version: string | null | undefined, siteDir: string, pluginId: string, @@ -63,7 +63,7 @@ export function docsVersion( // Load existing versions. let versions = []; - const versionsJSONFile = getVersionsJSONFile(siteDir, pluginId); + const versionsJSONFile = getVersionsFilePath(siteDir, pluginId); if (fs.existsSync(versionsJSONFile)) { versions = JSON.parse(fs.readFileSync(versionsJSONFile, 'utf8')); } @@ -80,7 +80,7 @@ export function docsVersion( // Copy docs files. const docsDir = path.join(siteDir, docsPath); if (fs.existsSync(docsDir) && fs.readdirSync(docsDir).length > 0) { - const versionedDir = getVersionedDocsDir(siteDir, pluginId); + const versionedDir = getVersionedDocsDirPath(siteDir, pluginId); const newVersionDir = path.join(versionedDir, `version-${version}`); fs.copySync(docsDir, newVersionDir); } else { @@ -89,7 +89,7 @@ export function docsVersion( // Load current sidebar and create a new versioned sidebars file. if (fs.existsSync(sidebarPath)) { - const loadedSidebars: Sidebar = loadSidebars([sidebarPath]); + const loadedSidebars: Sidebars = loadSidebars(sidebarPath); // Transform id in original sidebar to versioned id. const normalizeItem = (item: SidebarItem): SidebarItem => { @@ -107,8 +107,8 @@ export function docsVersion( } }; - const versionedSidebar: Sidebar = Object.entries(loadedSidebars).reduce( - (acc: Sidebar, [sidebarId, sidebarItems]) => { + const versionedSidebar: Sidebars = Object.entries(loadedSidebars).reduce( + (acc: Sidebars, [sidebarId, sidebarItems]) => { const newVersionedSidebarId = `version-${version}/${sidebarId}`; acc[newVersionedSidebarId] = sidebarItems.map(normalizeItem); return acc; @@ -116,7 +116,7 @@ export function docsVersion( {}, ); - const versionedSidebarsDir = getVersionedSidebarsDir(siteDir, pluginId); + const versionedSidebarsDir = getVersionedSidebarsDirPath(siteDir, pluginId); const newSidebarFile = path.join( versionedSidebarsDir, `version-${version}-sidebars.json`, diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/client/docsClientUtils.test.ts b/packages/docusaurus-plugin-content-docs/src/client/__tests__/docsClientUtils.test.ts similarity index 91% rename from packages/docusaurus-plugin-content-docs/src/__tests__/client/docsClientUtils.test.ts rename to packages/docusaurus-plugin-content-docs/src/client/__tests__/docsClientUtils.test.ts index 08129b9fea70..cf12adcd49c2 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/client/docsClientUtils.test.ts +++ b/packages/docusaurus-plugin-content-docs/src/client/__tests__/docsClientUtils.test.ts @@ -12,7 +12,7 @@ import { getActiveDocContext, getActiveVersion, getDocVersionSuggestions, -} from '../../client/docsClientUtils'; +} from '../docsClientUtils'; import {GlobalPluginData, GlobalVersion} from '../../types'; import {shuffle} from 'lodash'; @@ -21,12 +21,10 @@ describe('docsClientUtils', () => { const data: Record = { pluginIosId: { path: '/ios', - latestVersionName: 'xyz', versions: [], }, pluginAndroidId: { path: '/android', - latestVersionName: 'xyz', versions: [], }, }; @@ -55,19 +53,25 @@ describe('docsClientUtils', () => { const versions: GlobalVersion[] = [ { name: 'version1', + label: 'version1', path: '/???', + isLast: false, docs: [], mainDocId: '???', }, { name: 'version2', + label: 'version2', path: '/???', + isLast: true, docs: [], mainDocId: '???', }, { name: 'version3', + label: 'version3', path: '/???', + isLast: false, docs: [], mainDocId: '???', }, @@ -76,52 +80,35 @@ describe('docsClientUtils', () => { expect( getLatestVersion({ path: '???', - latestVersionName: 'does not exist', versions, }), - ).toEqual(undefined); - expect( - getLatestVersion({ - path: '???', - latestVersionName: 'version1', - versions, - })?.name, - ).toEqual('version1'); - expect( - getLatestVersion({ - path: '???', - latestVersionName: 'version2', - versions, - })?.name, - ).toEqual('version2'); - expect( - getLatestVersion({ - path: '???', - latestVersionName: 'version3', - versions, - })?.name, - ).toEqual('version3'); + ).toEqual(versions[1]); }); test('getActiveVersion', () => { const data: GlobalPluginData = { path: 'docs', - latestVersionName: 'version2', versions: [ { name: 'next', + label: 'next', + isLast: false, path: '/docs/next', docs: [], mainDocId: '???', }, { name: 'version2', + label: 'version2', + isLast: true, path: '/docs', docs: [], mainDocId: '???', }, { name: 'version1', + label: 'version1', + isLast: false, path: '/docs/version1', docs: [], mainDocId: '???', @@ -146,7 +133,9 @@ describe('docsClientUtils', () => { test('getActiveDocContext', () => { const versionNext: GlobalVersion = { name: 'next', + label: 'next', path: '/docs/next', + isLast: false, mainDocId: 'doc1', docs: [ { @@ -162,6 +151,8 @@ describe('docsClientUtils', () => { const version2: GlobalVersion = { name: 'version2', + label: 'version2', + isLast: true, path: '/docs', mainDocId: 'doc1', docs: [ @@ -178,7 +169,9 @@ describe('docsClientUtils', () => { const version1: GlobalVersion = { name: 'version1', + label: 'version1', path: '/docs/version1', + isLast: false, mainDocId: 'doc1', docs: [ { @@ -197,7 +190,6 @@ describe('docsClientUtils', () => { const data: GlobalPluginData = { path: 'docs', - latestVersionName: 'version2', versions, }; @@ -270,6 +262,8 @@ describe('docsClientUtils', () => { test('getDocVersionSuggestions', () => { const versionNext: GlobalVersion = { name: 'next', + label: 'next', + isLast: false, path: '/docs/next', mainDocId: 'doc1', docs: [ @@ -286,7 +280,9 @@ describe('docsClientUtils', () => { const version2: GlobalVersion = { name: 'version2', + label: 'version2', path: '/docs', + isLast: true, mainDocId: 'doc1', docs: [ { @@ -302,6 +298,8 @@ describe('docsClientUtils', () => { const version1: GlobalVersion = { name: 'version1', + label: 'version1', + isLast: false, path: '/docs/version1', mainDocId: 'doc1', docs: [ @@ -321,7 +319,6 @@ describe('docsClientUtils', () => { const data: GlobalPluginData = { path: 'docs', - latestVersionName: 'version2', versions, }; diff --git a/packages/docusaurus-plugin-content-docs/src/client/docsClientUtils.ts b/packages/docusaurus-plugin-content-docs/src/client/docsClientUtils.ts index 1692169f64d1..6adbc0d98511 100644 --- a/packages/docusaurus-plugin-content-docs/src/client/docsClientUtils.ts +++ b/packages/docusaurus-plugin-content-docs/src/client/docsClientUtils.ts @@ -49,9 +49,7 @@ export type ActiveDocContext = { }; export const getLatestVersion = (data: GlobalPluginData): Version => { - return data.versions.find( - (version) => version.name === data.latestVersionName, - )!; + return data.versions.find((version) => version.isLast)!; }; // Note: return undefined on doc-unrelated pages, diff --git a/packages/docusaurus-plugin-content-docs/src/constants.ts b/packages/docusaurus-plugin-content-docs/src/constants.ts index ca7d99f27cf1..822b8e600e88 100644 --- a/packages/docusaurus-plugin-content-docs/src/constants.ts +++ b/packages/docusaurus-plugin-content-docs/src/constants.ts @@ -5,6 +5,9 @@ * LICENSE file in the root directory of this source tree. */ +// The name of the version at the root of your site (website/docs) +export const CURRENT_VERSION_NAME = 'current'; + export const VERSIONED_DOCS_DIR = 'versioned_docs'; export const VERSIONED_SIDEBARS_DIR = 'versioned_sidebars'; export const VERSIONS_JSON_FILE = 'versions.json'; diff --git a/packages/docusaurus-plugin-content-docs/src/docs.ts b/packages/docusaurus-plugin-content-docs/src/docs.ts new file mode 100644 index 000000000000..e10e19bc7a52 --- /dev/null +++ b/packages/docusaurus-plugin-content-docs/src/docs.ts @@ -0,0 +1,185 @@ +/** + * 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 path from 'path'; +import fs from 'fs-extra'; +import { + aliasedSitePath, + normalizeUrl, + getEditUrl, + parseMarkdownString, +} from '@docusaurus/utils'; +import {LoadContext} from '@docusaurus/types'; + +import {getFileLastUpdate} from './lastUpdate'; +import { + DocMetadataBase, + LastUpdateData, + MetadataOptions, + VersionMetadata, + DocFile, + PluginOptions, +} from './types'; +import getSlug from './slug'; +import {CURRENT_VERSION_NAME} from './constants'; +import globby from 'globby'; + +type LastUpdateOptions = Pick< + PluginOptions, + 'showLastUpdateAuthor' | 'showLastUpdateTime' +>; + +async function readLastUpdateData( + filePath: string, + options: LastUpdateOptions, +): Promise { + const {showLastUpdateAuthor, showLastUpdateTime} = options; + if (showLastUpdateAuthor || showLastUpdateTime) { + // Use fake data in dev for faster development. + const fileLastUpdateData = + process.env.NODE_ENV === 'production' + ? await getFileLastUpdate(filePath) + : { + author: 'Author', + timestamp: 1539502055, + }; + + if (fileLastUpdateData) { + const {author, timestamp} = fileLastUpdateData; + return { + lastUpdatedAt: showLastUpdateTime ? timestamp : undefined, + lastUpdatedBy: showLastUpdateAuthor ? author : undefined, + }; + } + } + + return {}; +} + +export async function readDocFile( + docsDirPath: string, + source: string, + options: LastUpdateOptions, +): Promise { + const filePath = path.join(docsDirPath, source); + const [content, lastUpdate] = await Promise.all([ + fs.readFile(filePath, 'utf-8'), + readLastUpdateData(filePath, options), + ]); + return {source, content, lastUpdate}; +} + +export async function readVersionDocs( + versionMetadata: VersionMetadata, + options: Pick< + PluginOptions, + 'include' | 'showLastUpdateAuthor' | 'showLastUpdateTime' + >, +): Promise { + const sources = await globby(options.include, { + cwd: versionMetadata.docsDirPath, + }); + return Promise.all( + sources.map((source) => + readDocFile(versionMetadata.docsDirPath, source, options), + ), + ); +} + +export function processDocMetadata({ + docFile, + versionMetadata, + context, + options, +}: { + docFile: DocFile; + versionMetadata: VersionMetadata; + context: LoadContext; + options: MetadataOptions; +}): DocMetadataBase { + const {source, content, lastUpdate} = docFile; + const {editUrl, homePageId} = options; + const {siteDir} = context; + const filePath = path.join(versionMetadata.docsDirPath, source); + + // ex: api/myDoc -> api + // ex: myDoc -> . + const docsFileDirName = path.dirname(source); + + const docsEditUrl = getEditUrl(path.relative(siteDir, filePath), editUrl); + + const {frontMatter = {}, excerpt} = parseMarkdownString(content); + const {sidebar_label, custom_edit_url} = frontMatter; + + const baseID: string = + frontMatter.id || path.basename(source, path.extname(source)); + if (baseID.includes('/')) { + throw new Error(`Document id [${baseID}] cannot include "/".`); + } + + // TODO legacy retrocompatibility + // The same doc in 2 distinct version could keep the same id, + // we just need to namespace the data by version + const versionIdPart = + versionMetadata.versionName === CURRENT_VERSION_NAME + ? '' + : `version-${versionMetadata.versionName}/`; + + // TODO legacy retrocompatibility + // I think it's bad to affect the frontmatter id with the dirname + const dirNameIdPart = docsFileDirName === '.' ? '' : `${docsFileDirName}/`; + + // TODO legacy composite id, requires a breaking change to modify this + const id = `${versionIdPart}${dirNameIdPart}${baseID}`; + + const unversionedId = `${dirNameIdPart}${baseID}`; + + // TODO remove soon, deprecated homePageId + const isDocsHomePage = unversionedId === (homePageId ?? '_index'); + if (frontMatter.slug && isDocsHomePage) { + throw new Error( + `The docs homepage (homePageId=${homePageId}) is not allowed to have a frontmatter slug=${frontMatter.slug} => you have to chooser either homePageId or slug, not both`, + ); + } + + const docSlug = isDocsHomePage + ? '/' + : getSlug({ + baseID, + dirName: docsFileDirName, + frontmatterSlug: frontMatter.slug, + }); + + // Default title is the id. + const title: string = frontMatter.title || baseID; + + const description: string = frontMatter.description || excerpt; + + const permalink = normalizeUrl([versionMetadata.versionPath, docSlug]); + + // Assign all of object properties during instantiation (if possible) for + // NodeJS optimization. + // Adding properties to object after instantiation will cause hidden + // class transitions. + const metadata: DocMetadataBase = { + unversionedId, + id, + isDocsHomePage, + title, + description, + source: aliasedSitePath(filePath, siteDir), + slug: docSlug, + permalink, + editUrl: custom_edit_url !== undefined ? custom_edit_url : docsEditUrl, + version: versionMetadata.versionName, + lastUpdatedBy: lastUpdate.lastUpdatedBy, + lastUpdatedAt: lastUpdate.lastUpdatedAt, + sidebar_label, + }; + + return metadata; +} diff --git a/packages/docusaurus-plugin-content-docs/src/env.ts b/packages/docusaurus-plugin-content-docs/src/env.ts deleted file mode 100644 index a5062ae0f471..000000000000 --- a/packages/docusaurus-plugin-content-docs/src/env.ts +++ /dev/null @@ -1,88 +0,0 @@ -/** - * 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 path from 'path'; -import fs from 'fs-extra'; -import {VersioningEnv, Env} from './types'; -import { - VERSIONS_JSON_FILE, - VERSIONED_DOCS_DIR, - VERSIONED_SIDEBARS_DIR, -} from './constants'; - -import {DEFAULT_PLUGIN_ID} from '@docusaurus/core/lib/constants'; - -// retro-compatibility: no prefix for the default plugin id -function addPluginIdPrefix(fileOrDir: string, pluginId: string): string { - if (pluginId === DEFAULT_PLUGIN_ID) { - return fileOrDir; - } else { - return `${pluginId}_${fileOrDir}`; - } -} - -export function getVersionedDocsDir(siteDir: string, pluginId: string): string { - return path.join(siteDir, addPluginIdPrefix(VERSIONED_DOCS_DIR, pluginId)); -} - -export function getVersionedSidebarsDir( - siteDir: string, - pluginId: string, -): string { - return path.join( - siteDir, - addPluginIdPrefix(VERSIONED_SIDEBARS_DIR, pluginId), - ); -} - -export function getVersionsJSONFile(siteDir: string, pluginId: string): string { - return path.join(siteDir, addPluginIdPrefix(VERSIONS_JSON_FILE, pluginId)); -} - -type EnvOptions = Partial<{disableVersioning: boolean}>; - -export default function ( - siteDir: string, - pluginId: string, - options: EnvOptions = {disableVersioning: false}, -): Env { - if (!siteDir) { - throw new Error('unexpected, missing siteDir'); - } - if (!pluginId) { - throw new Error('unexpected, missing pluginId'); - } - - const versioning: VersioningEnv = { - enabled: false, - versions: [], - latestVersion: null, - docsDir: '', - sidebarsDir: '', - }; - - const versionsJSONFile = getVersionsJSONFile(siteDir, pluginId); - if (fs.existsSync(versionsJSONFile)) { - if (!options.disableVersioning) { - const parsedVersions = JSON.parse( - fs.readFileSync(versionsJSONFile, 'utf8'), - ); - if (parsedVersions && parsedVersions.length > 0) { - // eslint-disable-next-line prefer-destructuring - versioning.latestVersion = parsedVersions[0]; - versioning.enabled = true; - versioning.versions = parsedVersions; - versioning.docsDir = getVersionedDocsDir(siteDir, pluginId); - versioning.sidebarsDir = getVersionedSidebarsDir(siteDir, pluginId); - } - } - } - - return { - versioning, - }; -} diff --git a/packages/docusaurus-plugin-content-docs/src/globalData.ts b/packages/docusaurus-plugin-content-docs/src/globalData.ts new file mode 100644 index 000000000000..80d9bc8e9301 --- /dev/null +++ b/packages/docusaurus-plugin-content-docs/src/globalData.ts @@ -0,0 +1,26 @@ +/** + * 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 {DocMetadata, GlobalDoc, LoadedVersion, GlobalVersion} from './types'; + +export function toGlobalDataDoc(doc: DocMetadata): GlobalDoc { + return { + id: doc.unversionedId, + path: doc.permalink, + }; +} + +export function toGlobalDataVersion(version: LoadedVersion): GlobalVersion { + return { + name: version.versionName, + label: version.versionLabel, + isLast: version.isLast, + path: version.versionPath, + mainDocId: version.mainDocId, + docs: version.docs.map(toGlobalDataDoc), + }; +} diff --git a/packages/docusaurus-plugin-content-docs/src/index.ts b/packages/docusaurus-plugin-content-docs/src/index.ts index f0dfbcfda381..6d451b2b0306 100644 --- a/packages/docusaurus-plugin-content-docs/src/index.ts +++ b/packages/docusaurus-plugin-content-docs/src/index.ts @@ -5,93 +5,52 @@ * LICENSE file in the root directory of this source tree. */ -import groupBy from 'lodash.groupby'; -import pick from 'lodash.pick'; -import pickBy from 'lodash.pickby'; -import sortBy from 'lodash.sortby'; -import globby from 'globby'; -import fs from 'fs-extra'; import path from 'path'; -import chalk from 'chalk'; -import admonitions from 'remark-admonitions'; import { STATIC_DIR_NAME, DEFAULT_PLUGIN_ID, } from '@docusaurus/core/lib/constants'; -import { - normalizeUrl, - docuHash, - objectWithKeySorted, - aliasedSitePath, -} from '@docusaurus/utils'; -import { - LoadContext, - Plugin, - RouteConfig, - OptionValidationContext, - ValidationResult, -} from '@docusaurus/types'; - -import createOrder from './order'; -import loadSidebars from './sidebars'; -import processMetadata from './metadata'; -import loadEnv from './env'; +import {normalizeUrl, docuHash, aliasedSitePath} from '@docusaurus/utils'; +import {LoadContext, Plugin, RouteConfig} from '@docusaurus/types'; + +import {loadSidebars, createSidebarsUtils} from './sidebars'; +import {readVersionDocs, processDocMetadata} from './docs'; +import {readVersionsMetadata} from './versions'; import { PluginOptions, - Sidebar, - Order, - DocsMetadata, LoadedContent, SourceToPermalink, PermalinkToSidebar, - SidebarItemLink, - SidebarItemDoc, - DocsSidebar, - DocsBaseMetadata, - MetadataRaw, - DocsMetadataRaw, - Metadata, - VersionToSidebars, - SidebarItem, - DocsSidebarItem, + DocMetadataBase, + DocMetadata, GlobalPluginData, - DocsVersion, - GlobalVersion, - GlobalDoc, + VersionMetadata, + DocNavLink, + LoadedVersion, + DocFile, + DocsMarkdownOption, } from './types'; -import {Configuration} from 'webpack'; -import {docsVersion} from './version'; +import {RuleSetRule} from 'webpack'; +import {cliDocsVersionCommand} from './cli'; import {VERSIONS_JSON_FILE} from './constants'; -import {PluginOptionSchema} from './pluginOptionSchema'; -import {ValidationError} from '@hapi/joi'; +import {OptionsSchema} from './options'; +import {flatten, keyBy, compact} from 'lodash'; +import {toGlobalDataVersion} from './globalData'; +import {toVersionMetadataProp} from './props'; +import chalk from 'chalk'; export default function pluginContentDocs( context: LoadContext, options: PluginOptions, -): Plugin { - // TODO remove homePageId before end of 2020 - // "slug: /" is better because the home doc can be different across versions - if (options.homePageId) { - console.log( - chalk.red( - `The docs plugin option homePageId=${options.homePageId} is deprecated. To make a doc the "home", prefer frontmatter: "slug: /"`, - ), - ); - } - - if (options.admonitions) { - options.remarkPlugins = options.remarkPlugins.concat([ - [admonitions, options.admonitions], - ]); - } - +): Plugin { const {siteDir, generatedFilesDir, baseUrl} = context; - const docsDir = path.resolve(siteDir, options.path); + + const versionsMetadata = readVersionsMetadata({context, options}); + const sourceToPermalink: SourceToPermalink = {}; const pluginId = options.id ?? DEFAULT_PLUGIN_ID; - const isDefaultPluginId = pluginId === DEFAULT_PLUGIN_ID; const pluginDataDirRoot = path.join( generatedFilesDir, @@ -101,18 +60,6 @@ export default function pluginContentDocs( const aliasedSource = (source: string) => `~docs/${path.relative(pluginDataDirRoot, source)}`; - // Versioning. - const env = loadEnv(siteDir, pluginId, { - disableVersioning: options.disableVersioning, - }); - const {versioning} = env; - const { - versions, - docsDir: versionedDir, - sidebarsDir: versionedSidebarsDir, - } = versioning; - const versionsNames = versions.map((version) => `version-${version}`); - return { name: 'docusaurus-plugin-content-docs', @@ -125,6 +72,10 @@ export default function pluginContentDocs( }, extendCli(cli) { + const isDefaultPluginId = pluginId === DEFAULT_PLUGIN_ID; + + // Need to create one distinct command per plugin instance + // otherwise 2 instances would try to execute the command! const command = isDefaultPluginId ? 'docs:version' : `docs:version:${pluginId}`; @@ -137,259 +88,159 @@ export default function pluginContentDocs( .arguments('') .description(commandDescription) .action((version) => { - docsVersion(version, siteDir, pluginId, { + cliDocsVersionCommand(version, siteDir, pluginId, { path: options.path, sidebarPath: options.sidebarPath, }); }); }, - getPathsToWatch() { - const {include} = options; - let globPattern = include.map((pattern) => `${docsDir}/${pattern}`); - if (versioning.enabled) { - const docsGlob = include - .map((pattern) => - versionsNames.map( - (versionName) => `${versionedDir}/${versionName}/${pattern}`, - ), - ) - .reduce((a, b) => a.concat(b), []); - const sidebarsGlob = versionsNames.map( - (versionName) => - `${versionedSidebarsDir}/${versionName}-sidebars.json`, - ); - globPattern = [...globPattern, ...sidebarsGlob, ...docsGlob]; - } - return [...globPattern, options.sidebarPath]; - }, - getClientModules() { const modules = []; - if (options.admonitions) { modules.push(require.resolve('remark-admonitions/styles/infima.css')); } - return modules; }, - // Fetches blog contents and returns metadata for the contents. - async loadContent() { - const {include, sidebarPath} = options; - - if (!fs.existsSync(docsDir)) { - console.error( - chalk.red( - `No docs directory found for the docs plugin at: ${docsDir}`, + getPathsToWatch() { + function getVersionPathsToWatch(version: VersionMetadata): string[] { + return [ + version.sidebarFilePath, + ...options.include.map( + (pattern) => `${version.docsDirPath}/${pattern}`, ), - ); - return null; + ]; } - // Prepare metadata container. - const docsMetadataRaw: DocsMetadataRaw = {}; - const docsPromises = []; - const includeDefaultDocs = !( - options.excludeNextVersionDocs && process.argv[2] === 'build' - ); - - // Metadata for default/master docs files. - if (includeDefaultDocs) { - const docsFiles = await globby(include, { - cwd: docsDir, - }); - docsPromises.push( - Promise.all( - docsFiles.map(async (source) => { - const metadata: MetadataRaw = await processMetadata({ - source, - refDir: docsDir, - context, - options, - env, - }); - docsMetadataRaw[metadata.id] = metadata; - }), - ), - ); + return flatten(versionsMetadata.map(getVersionPathsToWatch)); + }, + + async loadContent() { + async function loadVersionDocsBase( + versionMetadata: VersionMetadata, + ): Promise { + const docFiles = await readVersionDocs(versionMetadata, options); + if (docFiles.length === 0) { + throw new Error( + `Docs version ${ + versionMetadata.versionName + } has no docs! At least one doc should exist at path=[${path.relative( + siteDir, + versionMetadata.docsDirPath, + )}]`, + ); + } + async function processVersionDoc(docFile: DocFile) { + return processDocMetadata({ + docFile, + versionMetadata, + context, + options, + }); + } + return Promise.all(docFiles.map(processVersionDoc)); } - // Metadata for versioned docs. - if (versioning.enabled) { - const versionedGlob = include - .map((pattern) => - versionsNames.map((versionName) => `${versionName}/${pattern}`), - ) - .reduce((a, b) => a.concat(b), []); - const versionedFiles = await globby(versionedGlob, { - cwd: versionedDir, - }); - docsPromises.push( - Promise.all( - versionedFiles.map(async (source) => { - const metadata = await processMetadata({ - source, - refDir: versionedDir, - context, - options, - env, - }); - docsMetadataRaw[metadata.id] = metadata; - }), - ), + async function loadVersion( + versionMetadata: VersionMetadata, + ): Promise { + const sidebars = loadSidebars(versionMetadata.sidebarFilePath); + const sidebarsUtils = createSidebarsUtils(sidebars); + + const docsBase: DocMetadataBase[] = await loadVersionDocsBase( + versionMetadata, + ); + const docsBaseById: Record = keyBy( + docsBase, + (doc) => doc.id, ); - } - // Load the sidebars and create docs ordering. - const sidebarPaths = versionsNames.map( - (versionName) => `${versionedSidebarsDir}/${versionName}-sidebars.json`, - ); + const validDocIds = Object.keys(docsBaseById); + sidebarsUtils.checkSidebarsDocIds(validDocIds); + + // Add sidebar/next/previous to the docs + function addNavData(doc: DocMetadataBase): DocMetadata { + const { + sidebarName, + previousId, + nextId, + } = sidebarsUtils.getDocNavigation(doc.id); + const toDocNavLink = (navDocId: string): DocNavLink => ({ + title: docsBaseById[navDocId].title, + permalink: docsBaseById[navDocId].permalink, + }); + return { + ...doc, + sidebar: sidebarName, + previous: previousId ? toDocNavLink(previousId) : undefined, + next: nextId ? toDocNavLink(nextId) : undefined, + }; + } - if (includeDefaultDocs) { - sidebarPaths.unshift(sidebarPath); - } + const docs = docsBase.map(addNavData); - const loadedSidebars: Sidebar = loadSidebars(sidebarPaths); - const order: Order = createOrder(loadedSidebars); - - await Promise.all(docsPromises); - - // Construct inter-metadata relationship in docsMetadata. - const docsMetadata: DocsMetadata = {}; - const permalinkToSidebar: PermalinkToSidebar = {}; - const versionToSidebars: VersionToSidebars = {}; - Object.keys(docsMetadataRaw).forEach((currentID) => { - const {next: nextID, previous: previousID, sidebar} = - order[currentID] || {}; - const previous = previousID - ? { - title: docsMetadataRaw[previousID]?.title ?? 'Previous', - permalink: docsMetadataRaw[previousID]?.permalink, - } - : undefined; - const next = nextID - ? { - title: docsMetadataRaw[nextID]?.title ?? 'Next', - permalink: docsMetadataRaw[nextID]?.permalink, - } - : undefined; - docsMetadata[currentID] = { - ...docsMetadataRaw[currentID], - sidebar, - previous, - next, - }; + // sort to ensure consistent output for tests + docs.sort((a, b) => a.id.localeCompare(b.id)); - // sourceToPermalink and permalinkToSidebar mapping. - const {source, permalink, version} = docsMetadataRaw[currentID]; - sourceToPermalink[source] = permalink; - if (sidebar) { - permalinkToSidebar[permalink] = sidebar; - if (versioning.enabled && version) { - if (!versionToSidebars[version]) { - versionToSidebars[version] = new Set(); - } - versionToSidebars[version].add(sidebar); - } - } - }); + // TODO annoying side effect! + Object.values(docs).forEach((loadedDoc) => { + const {source, permalink} = loadedDoc; + sourceToPermalink[source] = permalink; + }); - const convertDocLink = (item: SidebarItemDoc): SidebarItemLink => { - const docId = item.id; - const docMetadata = docsMetadataRaw[docId]; + // TODO really useful? replace with global state logic? + const permalinkToSidebar: PermalinkToSidebar = {}; + Object.values(docs).forEach((doc) => { + if (doc.sidebar) { + permalinkToSidebar[doc.permalink] = doc.sidebar; + } + }); - if (!docMetadata) { - throw new Error( - `Bad sidebars file. The document id '${docId}' was used in the sidebar, but no document with this id could be found. -Available document ids= -- ${Object.keys(docsMetadataRaw).sort().join('\n- ')}`, + // The "main doc" is the "version entry point" + // We browse this doc by clicking on a version: + // - the "home" doc (at '/docs/') + // - the first doc of the first sidebar + // - a random doc (if no docs are in any sidebar... edge case) + function getMainDoc(): DocMetadata { + const versionHomeDoc = docs.find( + (doc) => + doc.unversionedId === options.homePageId || doc.slug === '/', ); + const firstDocIdOfFirstSidebar = sidebarsUtils.getFirstDocIdOfFirstSidebar(); + if (versionHomeDoc) { + return versionHomeDoc; + } else if (firstDocIdOfFirstSidebar) { + return docs.find((doc) => doc.id === firstDocIdOfFirstSidebar)!; + } else { + return docs[0]; + } } - const {title, permalink, sidebar_label} = docMetadata; - return { - type: 'link', - label: sidebar_label || title, - href: permalink, + ...versionMetadata, + mainDocId: getMainDoc().unversionedId, + sidebars, + permalinkToSidebar, + docs: docs.map(addNavData), }; - }; - - const normalizeItem = (item: SidebarItem): DocsSidebarItem => { - switch (item.type) { - case 'category': - return {...item, items: item.items.map(normalizeItem)}; - case 'ref': - case 'doc': - return convertDocLink(item); - case 'link': - default: - return item; - } - }; + } - // Transform the sidebar so that all sidebar item will be in the - // form of 'link' or 'category' only. - // This is what will be passed as props to the UI component. - const docsSidebars: DocsSidebar = Object.entries(loadedSidebars).reduce( - (acc: DocsSidebar, [sidebarId, sidebarItems]) => { - acc[sidebarId] = sidebarItems.map(normalizeItem); - return acc; - }, - {}, - ); return { - docsMetadata, - docsDir, - docsSidebars, - permalinkToSidebar: objectWithKeySorted(permalinkToSidebar), - versionToSidebars, + loadedVersions: await Promise.all(versionsMetadata.map(loadVersion)), }; }, async contentLoaded({content, actions}) { - if (!content || Object.keys(content.docsMetadata).length === 0) { - return; - } - - const {docLayoutComponent, docItemComponent, routeBasePath} = options; + const {loadedVersions} = content; + const {docLayoutComponent, docItemComponent} = options; const {addRoute, createData, setGlobalData} = actions; - const pluginInstanceGlobalData: GlobalPluginData = { - path: normalizeUrl([baseUrl, options.routeBasePath]), - latestVersionName: versioning.latestVersion, - // Initialized empty, will be mutated - versions: [], - }; - - setGlobalData(pluginInstanceGlobalData); - - const createDocsBaseMetadata = ( - version: DocsVersion, - ): DocsBaseMetadata => { - const {docsSidebars, permalinkToSidebar, versionToSidebars} = content; - const neededSidebars: Set = - versionToSidebars[version!] || new Set(); - - return { - docsSidebars: version - ? pick(docsSidebars, Array.from(neededSidebars)) - : docsSidebars, - permalinkToSidebar: version - ? pickBy(permalinkToSidebar, (sidebar) => - neededSidebars.has(sidebar), - ) - : permalinkToSidebar, - version, - }; - }; - - const genRoutes = async ( - metadataItems: Metadata[], + const createDocRoutes = async ( + docs: DocMetadata[], ): Promise => { const routes = await Promise.all( - metadataItems.map(async (metadataItem) => { + docs.map(async (metadataItem) => { await createData( // Note that this created data path must be in sync with // metadataPath provided to mdx-loader. @@ -411,111 +262,84 @@ Available document ids= return routes.sort((a, b) => a.path.localeCompare(b.path)); }; - // We want latest version route to have lower priority - // Otherwise `/docs/next/foo` would match - // `/docs/:route` instead of `/docs/next/:route`. - const getVersionRoutePriority = (version: DocsVersion) => - version === versioning.latestVersion ? -1 : undefined; - - // This is the base route of the document root (for a doc given version) - // (/docs, /docs/next, /docs/1.0 etc...) - // The component applies the layout and renders the appropriate doc - const addVersionRoute = async ( - docsBasePath: string, - docsBaseMetadata: DocsBaseMetadata, - docs: Metadata[], - priority?: number, - ) => { - const docsBaseMetadataPath = await createData( - `${docuHash(normalizeUrl([docsBasePath, ':route']))}.json`, - JSON.stringify(docsBaseMetadata, null, 2), + async function handleVersion(loadedVersion: LoadedVersion) { + const versionMetadataPropPath = await createData( + `${docuHash( + `version-${loadedVersion.versionName}-metadata-prop`, + )}.json`, + JSON.stringify(toVersionMetadataProp(loadedVersion), null, 2), ); - const docsRoutes = await genRoutes(docs); - - const mainDoc: Metadata = - docs.find( - (doc) => - doc.unversionedId === options.homePageId || doc.slug === '/', - ) ?? docs[0]; - - const toGlobalDataDoc = (doc: Metadata): GlobalDoc => ({ - id: doc.unversionedId, - path: doc.permalink, - }); - - pluginInstanceGlobalData.versions.push({ - name: docsBaseMetadata.version, - path: docsBasePath, - mainDocId: mainDoc.unversionedId, - docs: docs - .map(toGlobalDataDoc) - // stable ordering, useful for tests - .sort((a, b) => a.id.localeCompare(b.id)), - }); - addRoute({ - path: docsBasePath, - exact: false, // allow matching /docs/* as well - component: docLayoutComponent, // main docs component (DocPage) - routes: docsRoutes, // subroute for each doc + path: loadedVersion.versionPath, + // allow matching /docs/* as well + exact: false, + // main docs component (DocPage) + component: docLayoutComponent, + // sub-routes for each doc + routes: await createDocRoutes(loadedVersion.docs), modules: { - docsMetadata: aliasedSource(docsBaseMetadataPath), + versionMetadata: aliasedSource(versionMetadataPropPath), }, - priority, + priority: loadedVersion.routePriority, }); - }; - // If versioning is enabled, we cleverly chunk the generated routes - // to be by version and pick only needed base metadata. - if (versioning.enabled) { - const docsMetadataByVersion = groupBy( - // sort to ensure consistent output for tests - Object.values(content.docsMetadata).sort((a, b) => - a.id.localeCompare(b.id), - ), - 'version', - ); - - await Promise.all( - Object.keys(docsMetadataByVersion).map(async (version) => { - const docsMetadata = docsMetadataByVersion[version]; - - const isLatestVersion = version === versioning.latestVersion; - const docsBaseRoute = normalizeUrl([ - baseUrl, - routeBasePath, - isLatestVersion ? '' : version, - ]); - const docsBaseMetadata = createDocsBaseMetadata(version); - - await addVersionRoute( - docsBaseRoute, - docsBaseMetadata, - docsMetadata, - getVersionRoutePriority(version), - ); - }), - ); - } else { - const docsMetadata = Object.values(content.docsMetadata); - const docsBaseMetadata = createDocsBaseMetadata(null); - const docsBaseRoute = normalizeUrl([baseUrl, routeBasePath]); - await addVersionRoute(docsBaseRoute, docsBaseMetadata, docsMetadata); } - // ensure version ordering on the global data (latest first) - pluginInstanceGlobalData.versions = sortBy( - pluginInstanceGlobalData.versions, - (versionMetadata: GlobalVersion) => { - const orderedVersionNames = ['next', ...versions]; - return orderedVersionNames.indexOf(versionMetadata.name!); - }, - ); + await Promise.all(loadedVersions.map(handleVersion)); + + setGlobalData({ + path: normalizeUrl([baseUrl, options.routeBasePath]), + versions: loadedVersions.map(toGlobalDataVersion), + }); }, configureWebpack(_config, isServer, utils) { const {getBabelLoader, getCacheLoader} = utils; const {rehypePlugins, remarkPlugins} = options; + + const docsMarkdownOptions: DocsMarkdownOption = { + siteDir, + sourceToPermalink, + versionsMetadata, + onBrokenMarkdownLink: (brokenMarkdownLink) => { + // TODO make this warning configurable? + console.warn( + chalk.yellow( + `Docs markdown link couldn't be resolved: (${brokenMarkdownLink.link}) in ${brokenMarkdownLink.filePath} for version ${brokenMarkdownLink.version.versionName}`, + ), + ); + }, + }; + + function createMDXLoaderRule(): RuleSetRule { + return { + test: /(\.mdx?)$/, + include: versionsMetadata.map((vmd) => vmd.docsDirPath), + use: compact([ + getCacheLoader(isServer), + getBabelLoader(isServer), + { + loader: require.resolve('@docusaurus/mdx-loader'), + options: { + remarkPlugins, + rehypePlugins, + staticDir: path.join(siteDir, STATIC_DIR_NAME), + metadataPath: (mdxPath: string) => { + // Note that metadataPath must be the same/in-sync as + // the path from createData for each MDX. + const aliasedPath = aliasedSitePath(mdxPath, siteDir); + return path.join(dataDir, `${docuHash(aliasedPath)}.json`); + }, + }, + }, + { + loader: path.resolve(__dirname, './markdown/index.js'), + options: docsMarkdownOptions, + }, + ]), + }; + } + // Suppress warnings about non-existing of versions file. const stats = { warningsFilter: [VERSIONS_JSON_FILE], @@ -532,55 +356,11 @@ Available document ids= }, }, module: { - rules: [ - { - test: /(\.mdx?)$/, - include: [docsDir, versionedDir].filter(Boolean), - use: [ - getCacheLoader(isServer), - getBabelLoader(isServer), - { - loader: require.resolve('@docusaurus/mdx-loader'), - options: { - remarkPlugins, - rehypePlugins, - staticDir: path.join(siteDir, STATIC_DIR_NAME), - metadataPath: (mdxPath: string) => { - // Note that metadataPath must be the same/in-sync as - // the path from createData for each MDX. - const aliasedPath = aliasedSitePath(mdxPath, siteDir); - return path.join( - dataDir, - `${docuHash(aliasedPath)}.json`, - ); - }, - }, - }, - { - loader: path.resolve(__dirname, './markdown/index.js'), - options: { - siteDir, - docsDir, - sourceToPermalink, - versionedDir, - }, - }, - ].filter(Boolean), - }, - ], + rules: [createMDXLoaderRule()], }, - } as Configuration; + }; }, }; } -export function validateOptions({ - validate, - options, -}: OptionValidationContext): ValidationResult< - PluginOptions, - ValidationError -> { - const validatedOptions = validate(PluginOptionSchema, options); - return validatedOptions; -} +export {validateOptions} from './options'; diff --git a/packages/docusaurus-plugin-content-docs/src/lastUpdate.ts b/packages/docusaurus-plugin-content-docs/src/lastUpdate.ts index 8c5a982c46f7..2ce8e71ffb2f 100644 --- a/packages/docusaurus-plugin-content-docs/src/lastUpdate.ts +++ b/packages/docusaurus-plugin-content-docs/src/lastUpdate.ts @@ -14,7 +14,7 @@ const GIT_COMMIT_TIMESTAMP_AUTHOR_REGEX = /^(\d+), (.+)$/; let showedGitRequirementError = false; -export default async function getFileLastUpdate( +export async function getFileLastUpdate( filePath?: string, ): Promise { if (!filePath) { diff --git a/packages/docusaurus-plugin-content-docs/src/markdown/__tests__/__fixtures__/docs/doc5.md b/packages/docusaurus-plugin-content-docs/src/markdown/__tests__/__fixtures__/docs/doc5.md new file mode 100644 index 000000000000..cea1e3ade8f9 --- /dev/null +++ b/packages/docusaurus-plugin-content-docs/src/markdown/__tests__/__fixtures__/docs/doc5.md @@ -0,0 +1,6 @@ +### Not Existing Docs + +- [docNotExist1](docNotExist1.md) +- [docNotExist2](./docNotExist2.mdx) +- [docNotExist3](../docNotExist3.mdx) +- [docNotExist4](./subdir/docNotExist4.md) diff --git a/packages/docusaurus-plugin-content-docs/src/markdown/__tests__/linkify.test.ts b/packages/docusaurus-plugin-content-docs/src/markdown/__tests__/linkify.test.ts index 9147a683c572..1e12fc36dfba 100644 --- a/packages/docusaurus-plugin-content-docs/src/markdown/__tests__/linkify.test.ts +++ b/packages/docusaurus-plugin-content-docs/src/markdown/__tests__/linkify.test.ts @@ -7,13 +7,41 @@ import fs from 'fs-extra'; import path from 'path'; -import linkify from '../linkify'; -import {SourceToPermalink} from '../../types'; -import {VERSIONED_DOCS_DIR} from '../../constants'; +import {linkify} from '../linkify'; +import { + DocsMarkdownOption, + SourceToPermalink, + VersionMetadata, + BrokenMarkdownLink, +} from '../../types'; +import {VERSIONED_DOCS_DIR, CURRENT_VERSION_NAME} from '../../constants'; + +function createFakeVersion( + versionName: string, + docsDirPath: string, +): VersionMetadata { + return { + versionName, + versionLabel: 'Any', + versionPath: 'any', + docsDirPath, + sidebarFilePath: 'any', + routePriority: undefined, + isLast: false, + }; +} const siteDir = path.join(__dirname, '__fixtures__'); -const docsDir = path.join(siteDir, 'docs'); -const versionedDir = path.join(siteDir, VERSIONED_DOCS_DIR); + +const versionCurrent = createFakeVersion( + CURRENT_VERSION_NAME, + path.join(siteDir, 'docs'), +); +const version100 = createFakeVersion( + CURRENT_VERSION_NAME, + path.join(siteDir, VERSIONED_DOCS_DIR, 'version-1.0.0'), +); + const sourceToPermalink: SourceToPermalink = { '@site/docs/doc1.md': '/docs/doc1', '@site/docs/doc2.md': '/docs/doc2', @@ -24,28 +52,34 @@ const sourceToPermalink: SourceToPermalink = { '/docs/1.0.0/subdir/doc1', }; -const transform = (filepath) => { - const content = fs.readFileSync(filepath, 'utf-8'); - const transformedContent = linkify( - content, - filepath, - docsDir, - siteDir, +function createMarkdownOptions( + options?: Partial, +): DocsMarkdownOption { + return { sourceToPermalink, - versionedDir, - ); + onBrokenMarkdownLink: () => {}, + versionsMetadata: [versionCurrent, version100], + siteDir, + ...options, + }; +} + +const transform = (filepath: string, options?: Partial) => { + const markdownOptions = createMarkdownOptions(options); + const content = fs.readFileSync(filepath, 'utf-8'); + const transformedContent = linkify(content, filepath, markdownOptions); return [content, transformedContent]; }; test('transform nothing', () => { - const doc1 = path.join(docsDir, 'doc1.md'); + const doc1 = path.join(versionCurrent.docsDirPath, 'doc1.md'); const [content, transformedContent] = transform(doc1); expect(transformedContent).toMatchSnapshot(); expect(content).toEqual(transformedContent); }); test('transform to correct links', () => { - const doc2 = path.join(docsDir, 'doc2.md'); + const doc2 = path.join(versionCurrent.docsDirPath, 'doc2.md'); const [content, transformedContent] = transform(doc2); expect(transformedContent).toMatchSnapshot(); expect(transformedContent).toContain('](/docs/doc1'); @@ -58,7 +92,8 @@ test('transform to correct links', () => { }); test('transform relative links', () => { - const doc3 = path.join(docsDir, 'subdir', 'doc3.md'); + const doc3 = path.join(versionCurrent.docsDirPath, 'subdir', 'doc3.md'); + const [content, transformedContent] = transform(doc3); expect(transformedContent).toMatchSnapshot(); expect(transformedContent).toContain('](/docs/doc2'); @@ -67,7 +102,7 @@ test('transform relative links', () => { }); test('transforms reference links', () => { - const doc4 = path.join(docsDir, 'doc4.md'); + const doc4 = path.join(versionCurrent.docsDirPath, 'doc4.md'); const [content, transformedContent] = transform(doc4); expect(transformedContent).toMatchSnapshot(); expect(transformedContent).toContain('[doc1]: /docs/doc1'); @@ -77,8 +112,38 @@ test('transforms reference links', () => { expect(content).not.toEqual(transformedContent); }); +test('report broken markdown links', () => { + const doc5 = path.join(versionCurrent.docsDirPath, 'doc5.md'); + const onBrokenMarkdownLink = jest.fn(); + const [content, transformedContent] = transform(doc5, { + onBrokenMarkdownLink, + }); + expect(transformedContent).toEqual(content); + expect(onBrokenMarkdownLink).toHaveBeenCalledTimes(4); + expect(onBrokenMarkdownLink).toHaveBeenNthCalledWith(1, { + filePath: doc5, + link: 'docNotExist1.md', + version: versionCurrent, + } as BrokenMarkdownLink); + expect(onBrokenMarkdownLink).toHaveBeenNthCalledWith(2, { + filePath: doc5, + link: './docNotExist2.mdx', + version: versionCurrent, + } as BrokenMarkdownLink); + expect(onBrokenMarkdownLink).toHaveBeenNthCalledWith(3, { + filePath: doc5, + link: '../docNotExist3.mdx', + version: versionCurrent, + } as BrokenMarkdownLink); + expect(onBrokenMarkdownLink).toHaveBeenNthCalledWith(4, { + filePath: doc5, + link: './subdir/docNotExist4.md', + version: versionCurrent, + } as BrokenMarkdownLink); +}); + test('transforms absolute links in versioned docs', () => { - const doc2 = path.join(versionedDir, 'version-1.0.0', 'doc2.md'); + const doc2 = path.join(version100.docsDirPath, 'doc2.md'); const [content, transformedContent] = transform(doc2); expect(transformedContent).toMatchSnapshot(); expect(transformedContent).toContain('](/docs/1.0.0/subdir/doc1'); @@ -89,7 +154,7 @@ test('transforms absolute links in versioned docs', () => { }); test('transforms relative links in versioned docs', () => { - const doc1 = path.join(versionedDir, 'version-1.0.0', 'subdir', 'doc1.md'); + const doc1 = path.join(version100.docsDirPath, 'subdir', 'doc1.md'); const [content, transformedContent] = transform(doc1); expect(transformedContent).toMatchSnapshot(); expect(transformedContent).toContain('](/docs/1.0.0/doc2'); diff --git a/packages/docusaurus-plugin-content-docs/src/markdown/index.ts b/packages/docusaurus-plugin-content-docs/src/markdown/index.ts index 6c54199a8eb8..d02c03f6bb9a 100644 --- a/packages/docusaurus-plugin-content-docs/src/markdown/index.ts +++ b/packages/docusaurus-plugin-content-docs/src/markdown/index.ts @@ -7,26 +7,15 @@ import {getOptions} from 'loader-utils'; import {loader} from 'webpack'; -import linkify from './linkify'; +import {linkify} from './linkify'; +import {DocsMarkdownOption} from '../types'; const markdownLoader: loader.Loader = function (source) { const fileString = source as string; const callback = this.async(); - const {docsDir, siteDir, versionedDir, sourceToPermalink} = getOptions(this); - + const options = getOptions(this) as DocsMarkdownOption; return ( - callback && - callback( - null, - linkify( - fileString, - this.resourcePath, - docsDir, - siteDir, - sourceToPermalink, - versionedDir, - ), - ) + callback && callback(null, linkify(fileString, this.resourcePath, options)) ); }; diff --git a/packages/docusaurus-plugin-content-docs/src/markdown/linkify.ts b/packages/docusaurus-plugin-content-docs/src/markdown/linkify.ts index 66250aac26a8..5d48335ba86e 100644 --- a/packages/docusaurus-plugin-content-docs/src/markdown/linkify.ts +++ b/packages/docusaurus-plugin-content-docs/src/markdown/linkify.ts @@ -7,68 +7,81 @@ import path from 'path'; import {resolve} from 'url'; -import {getSubFolder} from '@docusaurus/utils'; -import {SourceToPermalink} from '../types'; +import { + DocsMarkdownOption, + VersionMetadata, + BrokenMarkdownLink, +} from '../types'; -export default function ( - fileString: string, - filePath: string, - docsDir: string, - siteDir: string, - sourceToPermalink: SourceToPermalink, - versionedDir?: string, -): string { - // Determine the source dir. e.g: /website/docs, /website/versioned_docs/version-1.0.0 - let sourceDir: string | undefined; - const thisSource = filePath; - if (thisSource.startsWith(docsDir)) { - sourceDir = docsDir; - } else if (versionedDir && thisSource.startsWith(versionedDir)) { - const specificVersionDir = getSubFolder(thisSource, versionedDir); - // e.g: specificVersionDir = version-1.0.0 - if (specificVersionDir) { - sourceDir = path.join(versionedDir, specificVersionDir); - } +function getVersion(filePath: string, options: DocsMarkdownOption) { + const versionFound = options.versionsMetadata.find((version) => + filePath.startsWith(version.docsDirPath), + ); + if (!versionFound) { + throw new Error( + `Unexpected, markdown file does not belong to any docs version! file=${filePath}`, + ); } + return versionFound; +} - let content = fileString; +function replaceMarkdownLinks( + fileString: string, + filePath: string, + version: VersionMetadata, + options: DocsMarkdownOption, +) { + const {siteDir, sourceToPermalink, onBrokenMarkdownLink} = options; + const {docsDirPath} = version; // Replace internal markdown linking (except in fenced blocks). - if (sourceDir) { - let fencedBlock = false; - const lines = content.split('\n').map((line) => { - if (line.trim().startsWith('```')) { - fencedBlock = !fencedBlock; - } - if (fencedBlock) { - return line; - } + let fencedBlock = false; + const lines = fileString.split('\n').map((line) => { + if (line.trim().startsWith('```')) { + fencedBlock = !fencedBlock; + } + if (fencedBlock) { + return line; + } - let modifiedLine = line; - // Replace inline-style links or reference-style links e.g: - // This is [Document 1](doc1.md) -> we replace this doc1.md with correct link - // [doc1]: doc1.md -> we replace this doc1.md with correct link - const mdRegex = /(?:(?:\]\()|(?:\]:\s?))(?!https)([^'")\]\s>]+\.mdx?)/g; - let mdMatch = mdRegex.exec(modifiedLine); - while (mdMatch !== null) { - // Replace it to correct html link. - const mdLink = mdMatch[1]; - const targetSource = `${sourceDir}/${mdLink}`; - const aliasedSource = (source: string) => - `@site/${path.relative(siteDir, source)}`; - const permalink = - sourceToPermalink[aliasedSource(resolve(thisSource, mdLink))] || - sourceToPermalink[aliasedSource(targetSource)]; - if (permalink) { - modifiedLine = modifiedLine.replace(mdLink, permalink); - } - mdMatch = mdRegex.exec(modifiedLine); + let modifiedLine = line; + // Replace inline-style links or reference-style links e.g: + // This is [Document 1](doc1.md) -> we replace this doc1.md with correct link + // [doc1]: doc1.md -> we replace this doc1.md with correct link + const mdRegex = /(?:(?:\]\()|(?:\]:\s?))(?!https)([^'")\]\s>]+\.mdx?)/g; + let mdMatch = mdRegex.exec(modifiedLine); + while (mdMatch !== null) { + // Replace it to correct html link. + const mdLink = mdMatch[1]; + const targetSource = `${docsDirPath}/${mdLink}`; + const aliasedSource = (source: string) => + `@site/${path.relative(siteDir, source)}`; + const permalink = + sourceToPermalink[aliasedSource(resolve(filePath, mdLink))] || + sourceToPermalink[aliasedSource(targetSource)]; + if (permalink) { + modifiedLine = modifiedLine.replace(mdLink, permalink); + } else { + const brokenMarkdownLink: BrokenMarkdownLink = { + version, + filePath, + link: mdLink, + }; + onBrokenMarkdownLink(brokenMarkdownLink); } - return modifiedLine; - }); + mdMatch = mdRegex.exec(modifiedLine); + } + return modifiedLine; + }); - content = lines.join('\n'); - } + return lines.join('\n'); +} - return content; +export function linkify( + fileString: string, + filePath: string, + options: DocsMarkdownOption, +): string { + const version = getVersion(filePath, options); + return replaceMarkdownLinks(fileString, filePath, version, options); } diff --git a/packages/docusaurus-plugin-content-docs/src/metadata.ts b/packages/docusaurus-plugin-content-docs/src/metadata.ts deleted file mode 100644 index 74159f3c1414..000000000000 --- a/packages/docusaurus-plugin-content-docs/src/metadata.ts +++ /dev/null @@ -1,188 +0,0 @@ -/** - * 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 path from 'path'; -import { - parseMarkdownFile, - aliasedSitePath, - normalizeUrl, - getEditUrl, -} from '@docusaurus/utils'; -import {LoadContext} from '@docusaurus/types'; - -import lastUpdate from './lastUpdate'; -import { - MetadataRaw, - LastUpdateData, - MetadataOptions, - Env, - VersioningEnv, -} from './types'; -import getSlug from './slug'; -import {escapeRegExp} from 'lodash'; - -function removeVersionPrefix(str: string, version: string): string { - return str.replace(new RegExp(`^version-${escapeRegExp(version)}/?`), ''); -} - -function inferVersion( - dirName: string, - versioning: VersioningEnv, -): string | undefined { - if (!versioning.enabled) { - return undefined; - } - if (/^version-/.test(dirName)) { - const inferredVersion = dirName - .split('/', 1) - .shift()! - .replace(/^version-/, ''); - if (inferredVersion && versioning.versions.includes(inferredVersion)) { - return inferredVersion; - } - throw new Error( - `Can't infer version from folder=${dirName} -Expected versions: -- ${versioning.versions.join('- ')}`, - ); - } else { - return 'next'; - } -} - -type Args = { - source: string; - refDir: string; - context: LoadContext; - options: MetadataOptions; - env: Env; -}; - -async function lastUpdated( - filePath: string, - options: MetadataOptions, -): Promise { - const {showLastUpdateAuthor, showLastUpdateTime} = options; - if (showLastUpdateAuthor || showLastUpdateTime) { - // Use fake data in dev for faster development. - const fileLastUpdateData = - process.env.NODE_ENV === 'production' - ? await lastUpdate(filePath) - : { - author: 'Author', - timestamp: 1539502055, - }; - - if (fileLastUpdateData) { - const {author, timestamp} = fileLastUpdateData; - return { - lastUpdatedAt: showLastUpdateTime ? timestamp : undefined, - lastUpdatedBy: showLastUpdateAuthor ? author : undefined, - }; - } - } - - return {}; -} - -export default async function processMetadata({ - source, - refDir, - context, - options, - env, -}: Args): Promise { - const {routeBasePath, editUrl, homePageId} = options; - const {siteDir, baseUrl} = context; - const {versioning} = env; - const filePath = path.join(refDir, source); - - const fileMarkdownPromise = parseMarkdownFile(filePath); - const lastUpdatedPromise = lastUpdated(filePath, options); - - const dirNameWithVersion = path.dirname(source); // ex: version-1.0.0/foo - const version = inferVersion(dirNameWithVersion, versioning); // ex: 1.0.0 - const dirNameWithoutVersion = // ex: foo - version && version !== 'next' - ? removeVersionPrefix(dirNameWithVersion, version) - : dirNameWithVersion; - - // The version portion of the url path. Eg: 'next', '1.0.0', and ''. - const versionPath = - version && version !== versioning.latestVersion ? version : ''; - - const relativePath = path.relative(siteDir, filePath); - - const docsEditUrl = getEditUrl(relativePath, editUrl); - - const {frontMatter = {}, excerpt} = await fileMarkdownPromise; - const {sidebar_label, custom_edit_url} = frontMatter; - - // Default base id is the file name. - const baseID: string = - frontMatter.id || path.basename(source, path.extname(source)); - if (baseID.includes('/')) { - throw new Error('Document id cannot include "/".'); - } - - // test for website/docs folder, not a versioned folder - // TODO legacy test, looks bad - const isCurrrentDocs = dirNameWithVersion === '.'; - const id = isCurrrentDocs ? baseID : `${dirNameWithVersion}/${baseID}`; - const unversionedId = version ? removeVersionPrefix(id, version) : id; - - const isDocsHomePage = unversionedId === (homePageId ?? '_index'); - if (frontMatter.slug && isDocsHomePage) { - throw new Error( - `The docs homepage (homePageId=${homePageId}) is not allowed to have a frontmatter slug=${frontMatter.slug} => you have to chooser either homePageId or slug, not both`, - ); - } - - const docSlug = isDocsHomePage - ? '/' - : getSlug({ - baseID, - dirName: dirNameWithoutVersion, - frontmatterSlug: frontMatter.slug, - }); - - // Default title is the id. - const title: string = frontMatter.title || baseID; - - const description: string = frontMatter.description || excerpt; - - const permalink = normalizeUrl([ - baseUrl, - routeBasePath, - versionPath, - docSlug, - ]); - - const {lastUpdatedAt, lastUpdatedBy} = await lastUpdatedPromise; - - // Assign all of object properties during instantiation (if possible) for - // NodeJS optimization. - // Adding properties to object after instantiation will cause hidden - // class transitions. - const metadata: MetadataRaw = { - unversionedId, - id, - isDocsHomePage, - title, - description, - source: aliasedSitePath(filePath, siteDir), - slug: docSlug, - permalink, - editUrl: custom_edit_url !== undefined ? custom_edit_url : docsEditUrl, - version, - lastUpdatedBy, - lastUpdatedAt, - sidebar_label, - }; - - return metadata; -} diff --git a/packages/docusaurus-plugin-content-docs/src/pluginOptionSchema.ts b/packages/docusaurus-plugin-content-docs/src/options.ts similarity index 50% rename from packages/docusaurus-plugin-content-docs/src/pluginOptionSchema.ts rename to packages/docusaurus-plugin-content-docs/src/options.ts index 7e3a983bd25c..e036651dd700 100644 --- a/packages/docusaurus-plugin-content-docs/src/pluginOptionSchema.ts +++ b/packages/docusaurus-plugin-content-docs/src/options.ts @@ -12,13 +12,17 @@ import { AdmonitionsSchema, URISchema, } from '@docusaurus/utils-validation'; +import {OptionValidationContext, ValidationResult} from '@docusaurus/types'; +import {ValidationError} from '@hapi/joi'; +import chalk from 'chalk'; +import admonitions from 'remark-admonitions'; -export const DEFAULT_OPTIONS: PluginOptions = { +export const DEFAULT_OPTIONS: Omit = { path: 'docs', // Path to data on filesystem, relative to site dir. routeBasePath: 'docs', // URL Route. homePageId: undefined, // TODO remove soon, deprecated include: ['**/*.{md,mdx}'], // Extensions to include. - sidebarPath: '', // Path to sidebar configuration for showing a list of markdown pages. + sidebarPath: 'sidebars.json', // Path to sidebar configuration for showing a list of markdown pages. docLayoutComponent: '@theme/DocPage', docItemComponent: '@theme/DocItem', remarkPlugins: [], @@ -27,10 +31,11 @@ export const DEFAULT_OPTIONS: PluginOptions = { showLastUpdateAuthor: false, admonitions: {}, excludeNextVersionDocs: false, + includeCurrentVersion: true, disableVersioning: false, }; -export const PluginOptionSchema = Joi.object({ +export const OptionsSchema = Joi.object({ path: Joi.string().default(DEFAULT_OPTIONS.path), editUrl: URISchema, routeBasePath: Joi.string().allow('').default(DEFAULT_OPTIONS.routeBasePath), @@ -49,5 +54,50 @@ export const PluginOptionSchema = Joi.object({ excludeNextVersionDocs: Joi.bool().default( DEFAULT_OPTIONS.excludeNextVersionDocs, ), + includeCurrentVersion: Joi.bool().default( + DEFAULT_OPTIONS.includeCurrentVersion, + ), disableVersioning: Joi.bool().default(DEFAULT_OPTIONS.disableVersioning), }); + +// TODO bad validation function types +export function validateOptions({ + validate, + options, +}: OptionValidationContext): ValidationResult< + PluginOptions, + ValidationError +> { + // TODO remove homePageId before end of 2020 + // "slug: /" is better because the home doc can be different across versions + if (options.homePageId) { + console.log( + chalk.red( + `The docs plugin option homePageId=${options.homePageId} is deprecated. To make a doc the "home", prefer frontmatter: "slug: /"`, + ), + ); + } + + if (typeof options.excludeNextVersionDocs !== 'undefined') { + console.log( + chalk.red( + `The docs plugin option excludeNextVersionDocs=${ + options.excludeNextVersionDocs + } is deprecated. Use the includeCurrentVersion=${!options.excludeNextVersionDocs} option instead!"`, + ), + ); + options.includeCurrentVersion = !options.excludeNextVersionDocs; + } + + // @ts-expect-error: TODO bad OptionValidationContext, need refactor + const normalizedOptions: PluginOptions = validate(OptionsSchema, options); + + if (normalizedOptions.admonitions) { + normalizedOptions.remarkPlugins = normalizedOptions.remarkPlugins.concat([ + [admonitions, normalizedOptions.admonitions], + ]); + } + + // @ts-expect-error: TODO bad OptionValidationContext, need refactor + return normalizedOptions; +} diff --git a/packages/docusaurus-plugin-content-docs/src/order.ts b/packages/docusaurus-plugin-content-docs/src/order.ts deleted file mode 100644 index 1073335f1144..000000000000 --- a/packages/docusaurus-plugin-content-docs/src/order.ts +++ /dev/null @@ -1,63 +0,0 @@ -/** - * 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 {Sidebar, SidebarItem, Order} from './types'; - -// Build the docs meta such as next, previous, category and sidebar. -export default function createOrder(allSidebars: Sidebar = {}): Order { - const order: Order = {}; - - Object.keys(allSidebars).forEach((sidebarId) => { - const sidebar = allSidebars[sidebarId]; - - const ids: string[] = []; - const indexItems = ({items}: {items: SidebarItem[]}) => { - items.forEach((item) => { - switch (item.type) { - case 'category': - indexItems({ - items: item.items, - }); - break; - case 'ref': - case 'link': - // Refs and links should not be shown in navigation. - break; - case 'doc': - ids.push(item.id); - break; - default: - } - }); - }; - - indexItems({items: sidebar}); - - // eslint-disable-next-line - for (let i = 0; i < ids.length; i++) { - const id = ids[i]; - let previous; - let next; - - if (i > 0) { - previous = ids[i - 1]; - } - - if (i < ids.length - 1) { - next = ids[i + 1]; - } - - order[id] = { - previous, - next, - sidebar: sidebarId, - }; - } - }); - - return order; -} diff --git a/packages/docusaurus-plugin-content-docs/src/props.ts b/packages/docusaurus-plugin-content-docs/src/props.ts new file mode 100644 index 000000000000..f194648db697 --- /dev/null +++ b/packages/docusaurus-plugin-content-docs/src/props.ts @@ -0,0 +1,70 @@ +/** + * 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 { + LoadedVersion, + PropSidebars, + SidebarItemDoc, + SidebarItemLink, + PropVersionMetadata, + SidebarItem, + PropSidebarItem, +} from './types'; +import {keyBy, mapValues} from 'lodash'; + +export function toSidebarsProp(loadedVersion: LoadedVersion): PropSidebars { + const docsById = keyBy(loadedVersion.docs, (doc) => doc.id); + + const convertDocLink = (item: SidebarItemDoc): SidebarItemLink => { + const docId = item.id; + const docMetadata = docsById[docId]; + + if (!docMetadata) { + throw new Error( + `Bad sidebars file. The document id '${docId}' was used in the sidebar, but no document with this id could be found. +Available document ids= +- ${Object.keys(docsById).sort().join('\n- ')}`, + ); + } + + const {title, permalink, sidebar_label} = docMetadata; + + return { + type: 'link', + label: sidebar_label || title, + href: permalink, + }; + }; + + const normalizeItem = (item: SidebarItem): PropSidebarItem => { + switch (item.type) { + case 'category': + return {...item, items: item.items.map(normalizeItem)}; + case 'ref': + case 'doc': + return convertDocLink(item); + case 'link': + default: + return item; + } + }; + + // Transform the sidebar so that all sidebar item will be in the + // form of 'link' or 'category' only. + // This is what will be passed as props to the UI component. + return mapValues(loadedVersion.sidebars, (items) => items.map(normalizeItem)); +} + +export function toVersionMetadataProp( + loadedVersion: LoadedVersion, +): PropVersionMetadata { + return { + version: loadedVersion.versionName, + docsSidebars: toSidebarsProp(loadedVersion), + permalinkToSidebar: loadedVersion.permalinkToSidebar, + }; +} diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars.ts b/packages/docusaurus-plugin-content-docs/src/sidebars.ts index 7afb576e5546..e2cbd02f6d6f 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebars.ts @@ -9,19 +9,47 @@ import flatMap from 'lodash.flatmap'; import fs from 'fs-extra'; import importFresh from 'import-fresh'; import { - Sidebar, - SidebarRaw, + Sidebars, SidebarItem, - SidebarItemCategoryRaw, - SidebarItemRaw, SidebarItemLink, SidebarItemDoc, - SidebarCategoryShorthandRaw, + Sidebar, } from './types'; +import {mapValues, flatten, difference} from 'lodash'; +import {getElementsAround} from '@docusaurus/utils'; + +type SidebarItemCategoryJSON = { + type: 'category'; + label: string; + items: SidebarItemJSON[]; + collapsed?: boolean; +}; + +type SidebarItemJSON = + | string + | SidebarCategoryShorthandJSON + | SidebarItemDoc + | SidebarItemLink + | SidebarItemCategoryJSON + | { + type: string; + [key: string]: unknown; + }; + +type SidebarCategoryShorthandJSON = { + [sidebarCategory: string]: SidebarItemJSON[]; +}; + +type SidebarJSON = SidebarCategoryShorthandJSON | SidebarItemJSON[]; + +// Sidebar given by user that is not normalized yet. e.g: sidebars.json +type SidebarsJSON = { + [sidebarId: string]: SidebarJSON; +}; function isCategoryShorthand( - item: SidebarItemRaw, -): item is SidebarCategoryShorthandRaw { + item: SidebarItemJSON, +): item is SidebarCategoryShorthandJSON { return typeof item !== 'string' && !item.type; } @@ -32,8 +60,8 @@ const defaultCategoryCollapsedValue = true; * Convert {category1: [item1,item2]} shorthand syntax to long-form syntax */ function normalizeCategoryShorthand( - sidebar: SidebarCategoryShorthandRaw, -): SidebarItemCategoryRaw[] { + sidebar: SidebarCategoryShorthandJSON, +): SidebarItemCategoryJSON[] { return Object.entries(sidebar).map(([label, items]) => ({ type: 'category', collapsed: defaultCategoryCollapsedValue, @@ -65,7 +93,7 @@ function assertItem( function assertIsCategory( item: unknown, -): asserts item is SidebarItemCategoryRaw { +): asserts item is SidebarItemCategoryJSON { assertItem(item, ['items', 'label', 'collapsed']); if (typeof item.label !== 'string') { throw new Error( @@ -112,7 +140,7 @@ function assertIsLink(item: unknown): asserts item is SidebarItemLink { * Normalizes recursively item and all its children. Ensures that at the end * each item will be an object with the corresponding type. */ -function normalizeItem(item: SidebarItemRaw): SidebarItem[] { +function normalizeItem(item: SidebarItemJSON): SidebarItem[] { if (typeof item === 'string') { return [ { @@ -155,38 +183,119 @@ function normalizeItem(item: SidebarItemRaw): SidebarItem[] { } } -/** - * Converts sidebars object to mapping to arrays of sidebar item objects. - */ -function normalizeSidebar(sidebars: SidebarRaw): Sidebar { - return Object.entries(sidebars).reduce( - (acc: Sidebar, [sidebarId, sidebar]) => { - const normalizedSidebar: SidebarItemRaw[] = Array.isArray(sidebar) - ? sidebar - : normalizeCategoryShorthand(sidebar); - - acc[sidebarId] = flatMap(normalizedSidebar, normalizeItem); - - return acc; - }, - {}, - ); +function normalizeSidebar(sidebar: SidebarJSON) { + const normalizedSidebar: SidebarItemJSON[] = Array.isArray(sidebar) + ? sidebar + : normalizeCategoryShorthand(sidebar); + + return flatMap(normalizedSidebar, normalizeItem); } -export default function loadSidebars(sidebarPaths?: string[]): Sidebar { - // We don't want sidebars to be cached because of hot reloading. - const allSidebars: SidebarRaw = {}; +function normalizeSidebars(sidebars: SidebarsJSON): Sidebars { + return mapValues(sidebars, normalizeSidebar); +} - if (!sidebarPaths || !sidebarPaths.length) { - return {} as Sidebar; +// TODO refactor: make async +export function loadSidebars(sidebarFilePath: string): Sidebars { + if (!sidebarFilePath) { + throw new Error(`sidebarFilePath not provided: ${sidebarFilePath}`); } + if (!fs.existsSync(sidebarFilePath)) { + throw new Error(`No sidebar file exist at path: ${sidebarFilePath}`); + } + // We don't want sidebars to be cached because of hot reloading. + const sidebarJson = importFresh(sidebarFilePath) as SidebarsJSON; + return normalizeSidebars(sidebarJson); +} - sidebarPaths.forEach((sidebarPath) => { - if (sidebarPath && fs.existsSync(sidebarPath)) { - const sidebar = importFresh(sidebarPath) as SidebarRaw; - Object.assign(allSidebars, sidebar); +// traverse the sidebar tree in depth to find all doc items, in correct order +export function collectSidebarDocItems(sidebar: Sidebar): SidebarItemDoc[] { + function collectRecursive(item: SidebarItem): SidebarItemDoc[] { + if (item.type === 'doc') { + return [item]; + } + if (item.type === 'category') { + return flatten(item.items.map(collectRecursive)); } + // Refs and links should not be shown in navigation. + if (item.type === 'ref' || item.type === 'link') { + return []; + } + throw new Error(`unknown sidebar item type = ${item.type}`); + } + + return flatten(sidebar.map(collectRecursive)); +} + +export function collectSidebarsDocIds( + sidebars: Sidebars, +): Record { + return mapValues(sidebars, (sidebar) => { + return collectSidebarDocItems(sidebar).map((docItem) => docItem.id); }); +} + +export function createSidebarsUtils(sidebars: Sidebars) { + const sidebarNameToDocIds = collectSidebarsDocIds(sidebars); + + function getFirstDocIdOfFirstSidebar(): string | undefined { + return Object.values(sidebarNameToDocIds)[0]?.[0]; + } + + function getSidebarNameByDocId(docId: string): string | undefined { + // TODO lookup speed can be optimized + const entry = Object.entries( + sidebarNameToDocIds, + ).find(([_sidebarName, docIds]) => docIds.includes(docId)); + + return entry?.[0]; + } + + function getDocNavigation( + docId: string, + ): { + sidebarName: string | undefined; + previousId: string | undefined; + nextId: string | undefined; + } { + const sidebarName = getSidebarNameByDocId(docId); + if (sidebarName) { + const docIds = sidebarNameToDocIds[sidebarName]; + const currentIndex = docIds.indexOf(docId); + const {previous, next} = getElementsAround(docIds, currentIndex); + return { + sidebarName, + previousId: previous, + nextId: next, + }; + } else { + return { + sidebarName: undefined, + previousId: undefined, + nextId: undefined, + }; + } + } + + function checkSidebarsDocIds(validDocIds: string[]) { + const allSidebarDocIds = flatten(Object.values(sidebarNameToDocIds)); + const invalidSidebarDocIds = difference(allSidebarDocIds, validDocIds); + if (invalidSidebarDocIds.length > 0) { + throw new Error( + `Bad sidebars file. +These sidebar document ids do not exist: +- ${invalidSidebarDocIds.sort().join('\n- ')}\`, + +Available document ids= +- ${validDocIds.sort().join('\n- ')}`, + ); + } + } - return normalizeSidebar(allSidebars); + return { + getFirstDocIdOfFirstSidebar, + getSidebarNameByDocId, + getDocNavigation, + checkSidebarsDocIds, + }; } diff --git a/packages/docusaurus-plugin-content-docs/src/theme/hooks/useVersioning.ts b/packages/docusaurus-plugin-content-docs/src/theme/hooks/useVersioning.ts index 419ee5585706..afc278336ca6 100644 --- a/packages/docusaurus-plugin-content-docs/src/theme/hooks/useVersioning.ts +++ b/packages/docusaurus-plugin-content-docs/src/theme/hooks/useVersioning.ts @@ -14,6 +14,7 @@ try { versions = []; } +// TODO deprecate in favor of useDocs.ts instead function useVersioning(): { versioningEnabled: boolean; versions: string[]; diff --git a/packages/docusaurus-plugin-content-docs/src/types.ts b/packages/docusaurus-plugin-content-docs/src/types.ts index 7dc2f1636770..9528f5758b51 100644 --- a/packages/docusaurus-plugin-content-docs/src/types.ts +++ b/packages/docusaurus-plugin-content-docs/src/types.ts @@ -8,116 +8,91 @@ // eslint-disable-next-line spaced-comment /// -export type DocsVersion = string | null; // null = unversioned sites +export type DocFile = { + source: string; + content: string; + lastUpdate: LastUpdateData; +}; -export interface MetadataOptions { +export type VersionName = string; + +export type VersionMetadata = { + versionName: VersionName; // 1.0.0 + versionLabel: string; // Version 1.0.0 + versionPath: string; // /baseUrl/docs/1.0.0 + isLast: boolean; + docsDirPath: string; // versioned_docs/1.0.0 + sidebarFilePath: string; // versioned_sidebars/1.0.0.json + routePriority: number | undefined; // -1 for the latest docs +}; + +export type MetadataOptions = { routeBasePath: string; homePageId?: string; editUrl?: string; showLastUpdateTime?: boolean; showLastUpdateAuthor?: boolean; -} +}; -export interface PathOptions { +export type PathOptions = { path: string; sidebarPath: string; -} - -export interface PluginOptions extends MetadataOptions, PathOptions { - id?: string; - include: string[]; - docLayoutComponent: string; - docItemComponent: string; - remarkPlugins: ([Function, object] | Function)[]; - rehypePlugins: string[]; - admonitions: any; - disableVersioning: boolean; - excludeNextVersionDocs: boolean; -} +}; + +export type PluginOptions = MetadataOptions & + PathOptions & { + id: string; + include: string[]; + docLayoutComponent: string; + docItemComponent: string; + remarkPlugins: ([Function, object] | Function)[]; + rehypePlugins: string[]; + admonitions: any; + disableVersioning: boolean; + excludeNextVersionDocs?: boolean; + includeCurrentVersion: boolean; + }; export type SidebarItemDoc = { type: 'doc' | 'ref'; id: string; }; -export interface SidebarItemLink { +export type SidebarItemLink = { type: 'link'; href: string; label: string; -} +}; -export interface SidebarItemCategory { +export type SidebarItemCategory = { type: 'category'; label: string; items: SidebarItem[]; collapsed: boolean; -} - -export interface SidebarItemCategoryRaw { - type: 'category'; - label: string; - items: SidebarItemRaw[]; - collapsed?: boolean; -} +}; export type SidebarItem = | SidebarItemDoc | SidebarItemLink | SidebarItemCategory; -export type SidebarItemRaw = - | string - | SidebarCategoryShorthandRaw - | SidebarItemDoc - | SidebarItemLink - | SidebarItemCategoryRaw - | { - type: string; - [key: string]: unknown; - }; - -export interface SidebarCategoryShorthandRaw { - [sidebarCategory: string]: SidebarItemRaw[]; -} - -// Sidebar given by user that is not normalized yet. e.g: sidebars.json -export interface SidebarRaw { - [sidebarId: string]: SidebarCategoryShorthandRaw | SidebarItemRaw[]; -} - -export interface Sidebar { - [sidebarId: string]: SidebarItem[]; -} - -export interface DocsSidebarItemCategory { - type: 'category'; - label: string; - items: DocsSidebarItem[]; - collapsed?: boolean; -} - -export type DocsSidebarItem = SidebarItemLink | DocsSidebarItemCategory; +export type Sidebar = SidebarItem[]; -export interface DocsSidebar { - [sidebarId: string]: DocsSidebarItem[]; -} +export type Sidebars = Record; -export interface OrderMetadata { +export type OrderMetadata = { previous?: string; next?: string; sidebar?: string; -} - -export interface Order { - [id: string]: OrderMetadata; -} +}; -export interface LastUpdateData { +export type LastUpdateData = { lastUpdatedAt?: number; lastUpdatedBy?: string; -} +}; -export interface MetadataRaw extends LastUpdateData { +export type DocMetadataBase = LastUpdateData & { + version: VersionName; unversionedId: string; id: string; isDocsHomePage: boolean; @@ -128,67 +103,38 @@ export interface MetadataRaw extends LastUpdateData { permalink: string; sidebar_label?: string; editUrl?: string | null; - version?: string; -} +}; -export interface Paginator { +export type DocNavLink = { title: string; permalink: string; -} +}; -export interface Metadata extends MetadataRaw { +export type DocMetadata = DocMetadataBase & { sidebar?: string; - previous?: Paginator; - next?: Paginator; -} - -export interface DocsMetadata { - [id: string]: Metadata; -} - -export interface DocsMetadataRaw { - [id: string]: MetadataRaw; -} + previous?: DocNavLink; + next?: DocNavLink; +}; -export interface SourceToPermalink { +export type SourceToPermalink = { [source: string]: string; -} +}; -export interface PermalinkToSidebar { +export type PermalinkToSidebar = { [permalink: string]: string; -} - -export interface VersionToSidebars { - [version: string]: Set; -} - -export interface LoadedContent { - docsMetadata: DocsMetadata; - docsDir: string; - docsSidebars: DocsSidebar; - permalinkToSidebar: PermalinkToSidebar; - versionToSidebars: VersionToSidebars; -} - -export type DocsBaseMetadata = Pick< - LoadedContent, - 'docsSidebars' | 'permalinkToSidebar' -> & { - version: string | null; }; -export type VersioningEnv = { - enabled: boolean; - latestVersion: string | null; - versions: string[]; - docsDir: string; - sidebarsDir: string; +export type LoadedVersion = VersionMetadata & { + versionPath: string; + mainDocId: string; + docs: DocMetadata[]; + sidebars: Sidebars; + permalinkToSidebar: Record; }; -export interface Env { - versioning: VersioningEnv; - // TODO: translation -} +export type LoadedContent = { + loadedVersions: LoadedVersion[]; +}; export type GlobalDoc = { id: string; @@ -196,7 +142,9 @@ export type GlobalDoc = { }; export type GlobalVersion = { - name: DocsVersion; + name: VersionName; + label: string; + isLast: boolean; path: string; mainDocId: string; // home doc (if docs homepage configured), or first doc docs: GlobalDoc[]; @@ -204,6 +152,39 @@ export type GlobalVersion = { export type GlobalPluginData = { path: string; - latestVersionName: DocsVersion; versions: GlobalVersion[]; }; + +export type PropVersionMetadata = { + version: VersionName; + docsSidebars: PropSidebars; + permalinkToSidebar: PermalinkToSidebar; +}; + +export type PropSidebarItemLink = SidebarItemLink; // same + +export type PropSidebarItemCategory = { + type: 'category'; + label: string; + items: PropSidebarItem[]; + collapsed?: boolean; +}; + +export type PropSidebarItem = PropSidebarItemLink | PropSidebarItemCategory; + +export type PropSidebars = { + [sidebarId: string]: PropSidebarItem[]; +}; + +export type BrokenMarkdownLink = { + filePath: string; + version: VersionMetadata; + link: string; +}; + +export type DocsMarkdownOption = { + versionsMetadata: VersionMetadata[]; + siteDir: string; + sourceToPermalink: SourceToPermalink; + onBrokenMarkdownLink: (brokenMarkdownLink: BrokenMarkdownLink) => void; +}; diff --git a/packages/docusaurus-plugin-content-docs/src/versions.ts b/packages/docusaurus-plugin-content-docs/src/versions.ts new file mode 100644 index 000000000000..cc61ab3b30e8 --- /dev/null +++ b/packages/docusaurus-plugin-content-docs/src/versions.ts @@ -0,0 +1,259 @@ +/** + * 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 path from 'path'; +import fs from 'fs-extra'; +import {PluginOptions, VersionMetadata} from './types'; +import { + VERSIONS_JSON_FILE, + VERSIONED_DOCS_DIR, + VERSIONED_SIDEBARS_DIR, + CURRENT_VERSION_NAME, +} from './constants'; + +import {DEFAULT_PLUGIN_ID} from '@docusaurus/core/lib/constants'; +import {LoadContext} from '@docusaurus/types'; +import {normalizeUrl} from '@docusaurus/utils'; + +// retro-compatibility: no prefix for the default plugin id +function addPluginIdPrefix(fileOrDir: string, pluginId: string): string { + if (pluginId === DEFAULT_PLUGIN_ID) { + return fileOrDir; + } else { + return `${pluginId}_${fileOrDir}`; + } +} + +export function getVersionedDocsDirPath( + siteDir: string, + pluginId: string, +): string { + return path.join(siteDir, addPluginIdPrefix(VERSIONED_DOCS_DIR, pluginId)); +} + +export function getVersionedSidebarsDirPath( + siteDir: string, + pluginId: string, +): string { + return path.join( + siteDir, + addPluginIdPrefix(VERSIONED_SIDEBARS_DIR, pluginId), + ); +} + +export function getVersionsFilePath(siteDir: string, pluginId: string): string { + return path.join(siteDir, addPluginIdPrefix(VERSIONS_JSON_FILE, pluginId)); +} + +function ensureValidVersionString(version: unknown): asserts version is string { + if (typeof version !== 'string') { + throw new Error( + `versions should be strings. Found type=[${typeof version}] for version=[${version}]`, + ); + } + // Should we forbid versions with special chars like / ? + if (version.trim().length === 0) { + throw new Error(`Invalid version=[${version}]`); + } +} + +function ensureValidVersionArray( + versionArray: unknown, +): asserts versionArray is string[] { + if (!(versionArray instanceof Array)) { + throw new Error( + `The versions file should contain an array of versions! Found content=${JSON.stringify( + versionArray, + )}`, + ); + } + + versionArray.forEach(ensureValidVersionString); +} + +// TODO not easy to make async due to many deps +function readVersionsFile(siteDir: string, pluginId: string): string[] | null { + const versionsFilePath = getVersionsFilePath(siteDir, pluginId); + if (fs.existsSync(versionsFilePath)) { + const content = JSON.parse(fs.readFileSync(versionsFilePath, 'utf8')); + ensureValidVersionArray(content); + return content; + } else { + return null; + } +} + +// TODO not easy to make async due to many deps +function readVersionNames( + siteDir: string, + options: Pick< + PluginOptions, + 'id' | 'disableVersioning' | 'includeCurrentVersion' + >, +): string[] { + const versionFileContent = readVersionsFile(siteDir, options.id); + + if (!versionFileContent && options.disableVersioning) { + throw new Error( + `Docs: using disableVersioning=${options.disableVersioning} option on a non-versioned site does not make sense`, + ); + } + + const versions = options.disableVersioning ? [] : versionFileContent ?? []; + + // We add the current version at the beginning, unless + // - user don't want to + // - it's been explicitly added to versions.json + if ( + options.includeCurrentVersion && + !versions.includes(CURRENT_VERSION_NAME) + ) { + versions.unshift(CURRENT_VERSION_NAME); + } + + if (versions.length === 0) { + throw new Error( + `It is not possible to use docs without any version. Please check the configuration of these options: includeCurrentVersion=${options.includeCurrentVersion} disableVersioning=${options.disableVersioning}`, + ); + } + + return versions; +} + +function getVersionMetadataPaths({ + versionName, + context, + options, +}: { + versionName: string; + context: Pick; + options: Pick; +}): Pick { + const isCurrentVersion = versionName === CURRENT_VERSION_NAME; + + const docsDirPath = isCurrentVersion + ? path.resolve(context.siteDir, options.path) + : path.join( + getVersionedDocsDirPath(context.siteDir, options.id), + `version-${versionName}`, + ); + + const sidebarFilePath = isCurrentVersion + ? path.resolve(context.siteDir, options.sidebarPath) + : path.join( + getVersionedSidebarsDirPath(context.siteDir, options.id), + `version-${versionName}-sidebars.json`, + ); + + return {docsDirPath, sidebarFilePath}; +} + +function createVersionMetadata({ + versionName, + isLast, + context, + options, +}: { + versionName: string; + isLast: boolean; + context: Pick; + options: Pick; +}): VersionMetadata { + const {sidebarFilePath, docsDirPath} = getVersionMetadataPaths({ + versionName, + context, + options, + }); + + // TODO hardcoded for retro-compatibility + // TODO Need to make this configurable + const versionLabel = + versionName === CURRENT_VERSION_NAME ? 'Next' : versionName; + const versionPathPart = isLast + ? '' + : versionName === CURRENT_VERSION_NAME + ? 'next' + : versionName; + + const versionPath = normalizeUrl([ + context.baseUrl, + options.routeBasePath, + versionPathPart, + ]); + + // Because /docs/:route` should always be after `/docs/versionName/:route`. + const routePriority = versionPathPart === '' ? -1 : undefined; + + return { + versionName, + versionLabel, + versionPath, + isLast, + routePriority, + sidebarFilePath, + docsDirPath, + }; +} + +function checkVersionMetadataPaths({ + versionName, + docsDirPath, + sidebarFilePath, +}: VersionMetadata) { + if (!fs.existsSync(docsDirPath)) { + throw new Error( + `The docs folder does not exist for version [${versionName}]. A docs folder is expected to be found at ${docsDirPath}`, + ); + } + if (!fs.existsSync(sidebarFilePath)) { + throw new Error( + `The sidebar file does not exist for version [${versionName}]. A sidebar file is expected to be found at ${sidebarFilePath}`, + ); + } +} + +// TODO for retrocompatibility with existing behavior +// We should make this configurable +// "last version" is not a very good concept nor api surface +function getLastVersionName(versionNames: string[]) { + if (versionNames.length === 1) { + return versionNames[0]; + } else { + return versionNames.filter( + (versionName) => versionName !== CURRENT_VERSION_NAME, + )[0]; + } +} + +export function readVersionsMetadata({ + context, + options, +}: { + context: Pick; + options: Pick< + PluginOptions, + | 'id' + | 'path' + | 'sidebarPath' + | 'routeBasePath' + | 'includeCurrentVersion' + | 'disableVersioning' + >; +}): VersionMetadata[] { + const versionNames = readVersionNames(context.siteDir, options); + const lastVersionName = getLastVersionName(versionNames); + const versionsMetadata = versionNames.map((versionName) => + createVersionMetadata({ + versionName, + isLast: versionName === lastVersionName, + context, + options, + }), + ); + versionsMetadata.forEach(checkVersionMetadataPaths); + return versionsMetadata; +} diff --git a/packages/docusaurus-theme-classic/src/theme/DocPage/index.tsx b/packages/docusaurus-theme-classic/src/theme/DocPage/index.tsx index 2ee000fb4da2..64ac1a1d7350 100644 --- a/packages/docusaurus-theme-classic/src/theme/DocPage/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/DocPage/index.tsx @@ -20,11 +20,11 @@ import styles from './styles.module.css'; function DocPageContent({ currentDocRoute, - docsMetadata, + versionMetadata, children, }): JSX.Element { const {siteConfig, isClient} = useDocusaurusContext(); - const {permalinkToSidebar, docsSidebars, version} = docsMetadata; + const {permalinkToSidebar, docsSidebars, version} = versionMetadata; const sidebarName = permalinkToSidebar[currentDocRoute.path]; const sidebar = docsSidebars[sidebarName]; return ( @@ -52,7 +52,7 @@ function DocPageContent({ function DocPage(props) { const { route: {routes: docRoutes}, - docsMetadata, + versionMetadata, location, } = props; const currentDocRoute = docRoutes.find((docRoute) => @@ -64,7 +64,7 @@ function DocPage(props) { return ( + versionMetadata={versionMetadata}> {renderRoutes(docRoutes)} ); diff --git a/packages/docusaurus-theme-classic/src/theme/DocVersionSuggestions/index.tsx b/packages/docusaurus-theme-classic/src/theme/DocVersionSuggestions/index.tsx index 7654f22aeb22..f047b60b250d 100644 --- a/packages/docusaurus-theme-classic/src/theme/DocVersionSuggestions/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/DocVersionSuggestions/index.tsx @@ -52,18 +52,21 @@ function DocVersionSuggestions(): JSX.Element { return (
- {activeVersionName === 'next' ? ( -
- This is unreleased documentation for {siteTitle}{' '} - {activeVersionName} version. -
- ) : ( -
- This is documentation for {siteTitle}{' '} - v{activeVersionName}, which is no longer actively - maintained. -
- )} + { + // TODO need refactoring + activeVersionName === 'current' ? ( +
+ This is unreleased documentation for {siteTitle}{' '} + {activeVersionName} version. +
+ ) : ( +
+ This is documentation for {siteTitle}{' '} + v{activeVersionName}, which is no longer actively + maintained. +
+ ) + }
For up-to-date documentation, see the{' '} diff --git a/packages/docusaurus-theme-classic/src/theme/NavbarItem/DocsVersionDropdownNavbarItem.tsx b/packages/docusaurus-theme-classic/src/theme/NavbarItem/DocsVersionDropdownNavbarItem.tsx index 275d6cfe9cd7..a9a6efd56e00 100644 --- a/packages/docusaurus-theme-classic/src/theme/NavbarItem/DocsVersionDropdownNavbarItem.tsx +++ b/packages/docusaurus-theme-classic/src/theme/NavbarItem/DocsVersionDropdownNavbarItem.tsx @@ -13,16 +13,13 @@ import { useActiveDocContext, } from '@theme/hooks/useDocs'; -const versionLabel = (version, nextVersionLabel) => - version.name === 'next' ? nextVersionLabel : version.name; - const getVersionMainDoc = (version) => version.docs.find((doc) => doc.id === version.mainDocId); export default function DocsVersionDropdownNavbarItem({ mobile, docsPluginId, - nextVersionLabel, + nextVersionLabel: _unused, // TODO legacy, remove asap ...props }) { const activeDocContext = useActiveDocContext(docsPluginId); @@ -37,7 +34,7 @@ export default function DocsVersionDropdownNavbarItem({ getVersionMainDoc(version); return { isNavLink: true, - label: versionLabel(version, nextVersionLabel), + label: version.label, to: versionDoc.path, isActive: () => version === activeDocContext?.activeVersion, }; @@ -46,9 +43,7 @@ export default function DocsVersionDropdownNavbarItem({ const dropdownVersion = activeDocContext.activeVersion ?? latestVersion; // Mobile is handled a bit differently - const dropdownLabel = mobile - ? 'Versions' - : versionLabel(dropdownVersion, nextVersionLabel); + const dropdownLabel = mobile ? 'Versions' : dropdownVersion.label; const dropdownTo = mobile ? undefined : getVersionMainDoc(dropdownVersion).path; diff --git a/packages/docusaurus-theme-classic/src/theme/NavbarItem/DocsVersionNavbarItem.tsx b/packages/docusaurus-theme-classic/src/theme/NavbarItem/DocsVersionNavbarItem.tsx index 11bd3e037f29..5d6a0f33bf95 100644 --- a/packages/docusaurus-theme-classic/src/theme/NavbarItem/DocsVersionNavbarItem.tsx +++ b/packages/docusaurus-theme-classic/src/theme/NavbarItem/DocsVersionNavbarItem.tsx @@ -12,20 +12,17 @@ import {useActiveVersion, useLatestVersion} from '@theme/hooks/useDocs'; const getVersionMainDoc = (version) => version.docs.find((doc) => doc.id === version.mainDocId); -const versionLabel = (version, nextVersionLabel) => - version.name === 'next' ? nextVersionLabel : version.name; - export default function DocsVersionNavbarItem({ label: staticLabel, to: staticTo, docsPluginId, - nextVersionLabel, + nextVersionLabel: _unused, // TODO legacy, remove asap ...props }) { const activeVersion = useActiveVersion(docsPluginId); const latestVersion = useLatestVersion(docsPluginId); const version = activeVersion ?? latestVersion; - const label = staticLabel ?? versionLabel(version, nextVersionLabel); + const label = staticLabel ?? version.label; const path = staticTo ?? getVersionMainDoc(version).path; return ; } diff --git a/packages/docusaurus-theme-classic/src/validateThemeConfig.js b/packages/docusaurus-theme-classic/src/validateThemeConfig.js index 1267db7c56dc..5d6e919dc977 100644 --- a/packages/docusaurus-theme-classic/src/validateThemeConfig.js +++ b/packages/docusaurus-theme-classic/src/validateThemeConfig.js @@ -50,14 +50,13 @@ const DocsVersionNavbarItemSchema = Joi.object({ label: Joi.string(), to: Joi.string(), docsPluginId: Joi.string(), - nextVersionLabel: Joi.string().default('Next'), }); const DocsVersionDropdownNavbarItemSchema = Joi.object({ type: Joi.string().equal('docsVersionDropdown').required(), position: NavbarItemPosition, docsPluginId: Joi.string(), - nextVersionLabel: Joi.string().default('Next'), + nextVersionLabel: Joi.string().default('Next'), // TODO remove soon }); // Can this be made easier? :/ diff --git a/packages/docusaurus-utils/src/__tests__/index.test.ts b/packages/docusaurus-utils/src/__tests__/index.test.ts index 1861b72d97fa..9a14101a53ae 100644 --- a/packages/docusaurus-utils/src/__tests__/index.test.ts +++ b/packages/docusaurus-utils/src/__tests__/index.test.ts @@ -26,6 +26,7 @@ import { removePrefix, getFilePathForRoutePath, addLeadingSlash, + getElementsAround, } from '../index'; describe('load utils', () => { @@ -477,3 +478,37 @@ describe('getFilePathForRoutePath', () => { ); }); }); + +describe('getElementsAround', () => { + test('can return elements around', () => { + expect(getElementsAround(['a', 'b', 'c', 'd'], 0)).toEqual({ + previous: undefined, + next: 'b', + }); + expect(getElementsAround(['a', 'b', 'c', 'd'], 1)).toEqual({ + previous: 'a', + next: 'c', + }); + expect(getElementsAround(['a', 'b', 'c', 'd'], 2)).toEqual({ + previous: 'b', + next: 'd', + }); + expect(getElementsAround(['a', 'b', 'c', 'd'], 3)).toEqual({ + previous: 'c', + next: undefined, + }); + }); + + test('throws if bad index is provided', () => { + expect(() => + getElementsAround(['a', 'b', 'c', 'd'], -1), + ).toThrowErrorMatchingInlineSnapshot( + `"Valid aroundIndex for array (of size 4) are between 0 and 3, but you provided aroundIndex=-1"`, + ); + expect(() => + getElementsAround(['a', 'b', 'c', 'd'], 4), + ).toThrowErrorMatchingInlineSnapshot( + `"Valid aroundIndex for array (of size 4) are between 0 and 3, but you provided aroundIndex=4"`, + ); + }); +}); diff --git a/packages/docusaurus-utils/src/index.ts b/packages/docusaurus-utils/src/index.ts index baaba7e9824f..6b85cd1408d5 100644 --- a/packages/docusaurus-utils/src/index.ts +++ b/packages/docusaurus-utils/src/index.ts @@ -403,3 +403,22 @@ export function getFilePathForRoutePath(routePath: string): string { const filePath = path.dirname(routePath); return path.join(filePath, `${fileName}/index.html`); } + +export function getElementsAround( + array: T[], + aroundIndex: number, +): { + next: T | undefined; + previous: T | undefined; +} { + const min = 0; + const max = array.length - 1; + if (aroundIndex < min || aroundIndex > max) { + throw new Error( + `Valid aroundIndex for array (of size ${array.length}) are between ${min} and ${max}, but you provided aroundIndex=${aroundIndex}`, + ); + } + const previous = aroundIndex === min ? undefined : array[aroundIndex - 1]; + const next = aroundIndex === max ? undefined : array[aroundIndex + 1]; + return {previous, next}; +} diff --git a/packages/docusaurus/src/server/__tests__/__fixtures__/custom-site/sidebars.json b/packages/docusaurus/src/server/__tests__/__fixtures__/custom-site/sidebars.json new file mode 100644 index 000000000000..0967ef424bce --- /dev/null +++ b/packages/docusaurus/src/server/__tests__/__fixtures__/custom-site/sidebars.json @@ -0,0 +1 @@ +{} diff --git a/packages/docusaurus/src/server/__tests__/__fixtures__/simple-site/sidebars.json b/packages/docusaurus/src/server/__tests__/__fixtures__/simple-site/sidebars.json new file mode 100644 index 000000000000..0967ef424bce --- /dev/null +++ b/packages/docusaurus/src/server/__tests__/__fixtures__/simple-site/sidebars.json @@ -0,0 +1 @@ +{} diff --git a/website/docs/using-themes.md b/website/docs/using-themes.md index 5f66df46cad1..ad542fd028f2 100644 --- a/website/docs/using-themes.md +++ b/website/docs/using-themes.md @@ -189,7 +189,7 @@ High-level overview about themes: Related pieces --- -- [Advanced Guides – Themes](advanced-themes.md) +- [Advanced Guides – Themes](using-themes.md) - [Lifecycle APIs](lifecycle-apis.md) References diff --git a/website/versioned_docs/version-2.0.0-alpha.54/using-themes.md b/website/versioned_docs/version-2.0.0-alpha.54/using-themes.md index 544f93293a66..0aded7b80931 100644 --- a/website/versioned_docs/version-2.0.0-alpha.54/using-themes.md +++ b/website/versioned_docs/version-2.0.0-alpha.54/using-themes.md @@ -182,7 +182,7 @@ High-level overview about themes: Related pieces --- -- [Advanced Guides – Themes](advanced-themes.md) +- [Advanced Guides – Themes](using-themes.md) - [Lifecycle APIs](lifecycle-apis.md) References diff --git a/website/versioned_docs/version-2.0.0-alpha.55/using-themes.md b/website/versioned_docs/version-2.0.0-alpha.55/using-themes.md index 56e706ab9f84..f6701b9a3585 100644 --- a/website/versioned_docs/version-2.0.0-alpha.55/using-themes.md +++ b/website/versioned_docs/version-2.0.0-alpha.55/using-themes.md @@ -184,7 +184,7 @@ High-level overview about themes: Related pieces --- -- [Advanced Guides – Themes](advanced-themes.md) +- [Advanced Guides – Themes](using-themes.md) - [Lifecycle APIs](lifecycle-apis.md) References diff --git a/website/versioned_docs/version-2.0.0-alpha.56/using-themes.md b/website/versioned_docs/version-2.0.0-alpha.56/using-themes.md index e09dd1e6f384..f330a9a35b7d 100644 --- a/website/versioned_docs/version-2.0.0-alpha.56/using-themes.md +++ b/website/versioned_docs/version-2.0.0-alpha.56/using-themes.md @@ -184,7 +184,7 @@ High-level overview about themes: Related pieces --- -- [Advanced Guides – Themes](advanced-themes.md) +- [Advanced Guides – Themes](using-themes.md) - [Lifecycle APIs](lifecycle-apis.md) References diff --git a/website/versioned_docs/version-2.0.0-alpha.58/using-themes.md b/website/versioned_docs/version-2.0.0-alpha.58/using-themes.md index 56e706ab9f84..f6701b9a3585 100644 --- a/website/versioned_docs/version-2.0.0-alpha.58/using-themes.md +++ b/website/versioned_docs/version-2.0.0-alpha.58/using-themes.md @@ -184,7 +184,7 @@ High-level overview about themes: Related pieces --- -- [Advanced Guides – Themes](advanced-themes.md) +- [Advanced Guides – Themes](using-themes.md) - [Lifecycle APIs](lifecycle-apis.md) References diff --git a/website/versioned_docs/version-2.0.0-alpha.60/using-themes.md b/website/versioned_docs/version-2.0.0-alpha.60/using-themes.md index 5f66df46cad1..ad542fd028f2 100644 --- a/website/versioned_docs/version-2.0.0-alpha.60/using-themes.md +++ b/website/versioned_docs/version-2.0.0-alpha.60/using-themes.md @@ -189,7 +189,7 @@ High-level overview about themes: Related pieces --- -- [Advanced Guides – Themes](advanced-themes.md) +- [Advanced Guides – Themes](using-themes.md) - [Lifecycle APIs](lifecycle-apis.md) References diff --git a/website/versioned_docs/version-2.0.0-alpha.61/using-themes.md b/website/versioned_docs/version-2.0.0-alpha.61/using-themes.md index 5f66df46cad1..ad542fd028f2 100644 --- a/website/versioned_docs/version-2.0.0-alpha.61/using-themes.md +++ b/website/versioned_docs/version-2.0.0-alpha.61/using-themes.md @@ -189,7 +189,7 @@ High-level overview about themes: Related pieces --- -- [Advanced Guides – Themes](advanced-themes.md) +- [Advanced Guides – Themes](using-themes.md) - [Lifecycle APIs](lifecycle-apis.md) References diff --git a/yarn.lock b/yarn.lock index 0164f627c4b8..f2e174408aa9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4074,6 +4074,11 @@ resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-5.0.3.tgz#e7b5aebbac150f8b5fdd4a46e7f0bd8e65e19109" integrity sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw== +"@types/picomatch@^2.2.1": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@types/picomatch/-/picomatch-2.2.1.tgz#f9e5a5e6ad03996832975ab7eadfa35791ca2a8f" + integrity sha512-26/tQcDmJXYHiaWAAIjnTVL5nwrT+IVaqFZIbBImAuKk/r/j1r/1hmZ7uaOzG6IknqP3QHcNNQ6QO8Vp28lUoA== + "@types/prettier@^1.19.0": version "1.19.1" resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-1.19.1.tgz#33509849f8e679e4add158959fdb086440e9553f" @@ -21279,6 +21284,11 @@ utila@^0.4.0, utila@~0.4: resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c" integrity sha1-ihagXURWV6Oupe7MWxKk+lN5dyw= +utility-types@^3.10.0: + version "3.10.0" + resolved "https://registry.yarnpkg.com/utility-types/-/utility-types-3.10.0.tgz#ea4148f9a741015f05ed74fd615e1d20e6bed82b" + integrity sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg== + utils-merge@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"