mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-20 20:46:58 +02:00
feat(v2): docs versioning ❄️🔥 (#1983)
* wip: versioning * wip again * nits lint * refactor metadata code so that we can have inobject properties optimization, fix typing * remove buggy permalink code * modify versioned docs fixture such that foo/baz only exists in v1.0.0 * refactor metadata.ts so that there is less transformon object * more refactoring * reduce test fixtures, refactoring * refactoring readability * finish metadata part * refactor with readdir * first pass of implementation * fix mdx laoder * split generated routes by version for performance & smaller bundle * test data for demo * refactor with set * more tests * typo * fix typo * better temporary ui * stronger typing & docsVersion command * add 100% test coverage for docsVersion command * more test and delete manual docs cut * cut 2.0.0-alpha.35 docs * cut alpha.36 instead * copyright * delete versioned docs * stronger test on metadata * update typo
This commit is contained in:
parent
c413cff212
commit
9829f56b1e
45 changed files with 1852 additions and 395 deletions
|
@ -12,6 +12,7 @@
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@docusaurus/types": "^2.0.0-alpha.36",
|
"@docusaurus/types": "^2.0.0-alpha.36",
|
||||||
|
"commander": "^4.0.1",
|
||||||
"picomatch": "^2.1.0"
|
"picomatch": "^2.1.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -22,6 +23,7 @@
|
||||||
"globby": "^10.0.1",
|
"globby": "^10.0.1",
|
||||||
"import-fresh": "^3.1.0",
|
"import-fresh": "^3.1.0",
|
||||||
"loader-utils": "^1.2.3",
|
"loader-utils": "^1.2.3",
|
||||||
|
"lodash": "^4.17.15",
|
||||||
"shelljs": "^0.8.3"
|
"shelljs": "^0.8.3"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
---
|
---
|
||||||
custom_edit_url: https://github.com/customUrl/docs/lorem.md
|
custom_edit_url: https://github.com/customUrl/docs/lorem.md
|
||||||
|
unrelated_frontmatter: won't be part of metadata
|
||||||
---
|
---
|
||||||
|
|
||||||
Lorem ipsum.
|
Lorem ipsum.
|
|
@ -1,7 +0,0 @@
|
||||||
---
|
|
||||||
id: permalink
|
|
||||||
title: Permalink
|
|
||||||
permalink: :baseUrl:docsUrl/endiliey/:id
|
|
||||||
---
|
|
||||||
|
|
||||||
This has a different permalink
|
|
|
@ -11,6 +11,4 @@ module.exports = {
|
||||||
url: 'https://your-docusaurus-test-site.com',
|
url: 'https://your-docusaurus-test-site.com',
|
||||||
baseUrl: '/',
|
baseUrl: '/',
|
||||||
favicon: 'img/favicon.ico',
|
favicon: 'img/favicon.ico',
|
||||||
organizationName: 'facebook', // Usually your GitHub org/user name.
|
|
||||||
projectName: 'docusaurus', // Usually your repo name.
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
This is `next` version of bar.
|
|
@ -0,0 +1 @@
|
||||||
|
Hello `next` !
|
|
@ -0,0 +1,14 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2017-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
title: 'Versioned Site',
|
||||||
|
tagline: 'The tagline of my site',
|
||||||
|
url: 'https://your-docusaurus-test-site.com',
|
||||||
|
baseUrl: '/',
|
||||||
|
favicon: 'img/favicon.ico',
|
||||||
|
};
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"docs": {
|
||||||
|
"Test": [
|
||||||
|
"foo/bar"
|
||||||
|
],
|
||||||
|
"Guides": [
|
||||||
|
"hello"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
Bar `1.0.0` !
|
|
@ -0,0 +1 @@
|
||||||
|
Baz `1.0.0` ! This will be deleted in next subsequent versions.
|
|
@ -0,0 +1 @@
|
||||||
|
Hello `1.0.0` !
|
|
@ -0,0 +1 @@
|
||||||
|
Bar `1.0.1` !
|
|
@ -0,0 +1 @@
|
||||||
|
Hello `1.0.1` !
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"version-1.0.0/docs": {
|
||||||
|
"Test": [
|
||||||
|
"version-1.0.0/foo/bar",
|
||||||
|
"version-1.0.0/foo/baz"
|
||||||
|
],
|
||||||
|
"Guides": [
|
||||||
|
"version-1.0.0/hello"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"version-1.0.1/docs": {
|
||||||
|
"Test": [
|
||||||
|
"version-1.0.1/foo/bar"
|
||||||
|
],
|
||||||
|
"Guides": [
|
||||||
|
"version-1.0.1/hello"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
[
|
||||||
|
"1.0.1",
|
||||||
|
"1.0.0"
|
||||||
|
]
|
|
@ -58,16 +58,8 @@ Array [
|
||||||
"docsMetadata": "@docusaurus-plugin-content-docs/docs-route-ff2.json",
|
"docsMetadata": "@docusaurus-plugin-content-docs/docs-route-ff2.json",
|
||||||
},
|
},
|
||||||
"path": "/docs/:route",
|
"path": "/docs/:route",
|
||||||
|
"priority": undefined,
|
||||||
"routes": Array [
|
"routes": Array [
|
||||||
Object {
|
|
||||||
"component": "@theme/DocItem",
|
|
||||||
"exact": true,
|
|
||||||
"modules": Object {
|
|
||||||
"content": "@site/docs/permalink.md",
|
|
||||||
"metadata": "@docusaurus-plugin-content-docs/docs-endiliey-permalink-086.json",
|
|
||||||
},
|
|
||||||
"path": "/docs/endiliey/permalink",
|
|
||||||
},
|
|
||||||
Object {
|
Object {
|
||||||
"component": "@theme/DocItem",
|
"component": "@theme/DocItem",
|
||||||
"exact": true,
|
"exact": true,
|
||||||
|
@ -108,3 +100,311 @@ Array [
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`versioned website content 1`] = `
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"component": "@theme/DocPage",
|
||||||
|
"modules": Object {
|
||||||
|
"docsMetadata": "@docusaurus-plugin-content-docs/docs-1-0-0-route-660.json",
|
||||||
|
},
|
||||||
|
"path": "/docs/1.0.0/:route",
|
||||||
|
"priority": undefined,
|
||||||
|
"routes": Array [
|
||||||
|
Object {
|
||||||
|
"component": "@theme/DocItem",
|
||||||
|
"exact": true,
|
||||||
|
"modules": Object {
|
||||||
|
"content": "@site/versioned_docs/version-1.0.0/foo/bar.md",
|
||||||
|
"metadata": "@docusaurus-plugin-content-docs/docs-1-0-0-foo-bar-568.json",
|
||||||
|
},
|
||||||
|
"path": "/docs/1.0.0/foo/bar",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"component": "@theme/DocItem",
|
||||||
|
"exact": true,
|
||||||
|
"modules": Object {
|
||||||
|
"content": "@site/versioned_docs/version-1.0.0/foo/baz.md",
|
||||||
|
"metadata": "@docusaurus-plugin-content-docs/docs-1-0-0-foo-baz-5e1.json",
|
||||||
|
},
|
||||||
|
"path": "/docs/1.0.0/foo/baz",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"component": "@theme/DocItem",
|
||||||
|
"exact": true,
|
||||||
|
"modules": Object {
|
||||||
|
"content": "@site/versioned_docs/version-1.0.0/hello.md",
|
||||||
|
"metadata": "@docusaurus-plugin-content-docs/docs-1-0-0-hello-1d0.json",
|
||||||
|
},
|
||||||
|
"path": "/docs/1.0.0/hello",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"component": "@theme/DocPage",
|
||||||
|
"modules": Object {
|
||||||
|
"docsMetadata": "@docusaurus-plugin-content-docs/docs-next-route-1c8.json",
|
||||||
|
},
|
||||||
|
"path": "/docs/next/:route",
|
||||||
|
"priority": undefined,
|
||||||
|
"routes": Array [
|
||||||
|
Object {
|
||||||
|
"component": "@theme/DocItem",
|
||||||
|
"exact": true,
|
||||||
|
"modules": Object {
|
||||||
|
"content": "@site/docs/foo/bar.md",
|
||||||
|
"metadata": "@docusaurus-plugin-content-docs/docs-next-foo-bar-09c.json",
|
||||||
|
},
|
||||||
|
"path": "/docs/next/foo/bar",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"component": "@theme/DocItem",
|
||||||
|
"exact": true,
|
||||||
|
"modules": Object {
|
||||||
|
"content": "@site/docs/hello.md",
|
||||||
|
"metadata": "@docusaurus-plugin-content-docs/docs-next-hello-64c.json",
|
||||||
|
},
|
||||||
|
"path": "/docs/next/hello",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"component": "@theme/DocPage",
|
||||||
|
"modules": Object {
|
||||||
|
"docsMetadata": "@docusaurus-plugin-content-docs/docs-route-ff2.json",
|
||||||
|
},
|
||||||
|
"path": "/docs/:route",
|
||||||
|
"priority": -1,
|
||||||
|
"routes": Array [
|
||||||
|
Object {
|
||||||
|
"component": "@theme/DocItem",
|
||||||
|
"exact": true,
|
||||||
|
"modules": Object {
|
||||||
|
"content": "@site/versioned_docs/version-1.0.1/foo/bar.md",
|
||||||
|
"metadata": "@docusaurus-plugin-content-docs/docs-foo-bar-cef.json",
|
||||||
|
},
|
||||||
|
"path": "/docs/foo/bar",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"component": "@theme/DocItem",
|
||||||
|
"exact": true,
|
||||||
|
"modules": Object {
|
||||||
|
"content": "@site/versioned_docs/version-1.0.1/hello.md",
|
||||||
|
"metadata": "@docusaurus-plugin-content-docs/docs-hello-da2.json",
|
||||||
|
},
|
||||||
|
"path": "/docs/hello",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`versioned website content: all sidebars 1`] = `
|
||||||
|
Object {
|
||||||
|
"docs": Array [
|
||||||
|
Object {
|
||||||
|
"items": Array [
|
||||||
|
Object {
|
||||||
|
"href": "/docs/next/foo/bar",
|
||||||
|
"label": "bar",
|
||||||
|
"type": "link",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"label": "Test",
|
||||||
|
"type": "category",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"items": Array [
|
||||||
|
Object {
|
||||||
|
"href": "/docs/next/hello",
|
||||||
|
"label": "hello",
|
||||||
|
"type": "link",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"label": "Guides",
|
||||||
|
"type": "category",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"version-1.0.0/docs": Array [
|
||||||
|
Object {
|
||||||
|
"items": Array [
|
||||||
|
Object {
|
||||||
|
"href": "/docs/1.0.0/foo/bar",
|
||||||
|
"label": "bar",
|
||||||
|
"type": "link",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"href": "/docs/1.0.0/foo/baz",
|
||||||
|
"label": "baz",
|
||||||
|
"type": "link",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"label": "Test",
|
||||||
|
"type": "category",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"items": Array [
|
||||||
|
Object {
|
||||||
|
"href": "/docs/1.0.0/hello",
|
||||||
|
"label": "hello",
|
||||||
|
"type": "link",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"label": "Guides",
|
||||||
|
"type": "category",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"version-1.0.1/docs": Array [
|
||||||
|
Object {
|
||||||
|
"items": Array [
|
||||||
|
Object {
|
||||||
|
"href": "/docs/foo/bar",
|
||||||
|
"label": "bar",
|
||||||
|
"type": "link",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"label": "Test",
|
||||||
|
"type": "category",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"items": Array [
|
||||||
|
Object {
|
||||||
|
"href": "/docs/hello",
|
||||||
|
"label": "hello",
|
||||||
|
"type": "link",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"label": "Guides",
|
||||||
|
"type": "category",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`versioned website content: base metadata for first version 1`] = `
|
||||||
|
Object {
|
||||||
|
"docsSidebars": Object {
|
||||||
|
"version-1.0.0/docs": Array [
|
||||||
|
Object {
|
||||||
|
"items": Array [
|
||||||
|
Object {
|
||||||
|
"href": "/docs/1.0.0/foo/bar",
|
||||||
|
"label": "bar",
|
||||||
|
"type": "link",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"href": "/docs/1.0.0/foo/baz",
|
||||||
|
"label": "baz",
|
||||||
|
"type": "link",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"label": "Test",
|
||||||
|
"type": "category",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"items": Array [
|
||||||
|
Object {
|
||||||
|
"href": "/docs/1.0.0/hello",
|
||||||
|
"label": "hello",
|
||||||
|
"type": "link",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"label": "Guides",
|
||||||
|
"type": "category",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"permalinkToSidebar": Object {
|
||||||
|
"/docs/1.0.0/foo/bar": "version-1.0.0/docs",
|
||||||
|
"/docs/1.0.0/foo/baz": "version-1.0.0/docs",
|
||||||
|
"/docs/1.0.0/hello": "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 {
|
||||||
|
"items": Array [
|
||||||
|
Object {
|
||||||
|
"href": "/docs/foo/bar",
|
||||||
|
"label": "bar",
|
||||||
|
"type": "link",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"label": "Test",
|
||||||
|
"type": "category",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"items": Array [
|
||||||
|
Object {
|
||||||
|
"href": "/docs/hello",
|
||||||
|
"label": "hello",
|
||||||
|
"type": "link",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"label": "Guides",
|
||||||
|
"type": "category",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"permalinkToSidebar": Object {
|
||||||
|
"/docs/foo/bar": "version-1.0.1/docs",
|
||||||
|
"/docs/hello": "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 {
|
||||||
|
"items": Array [
|
||||||
|
Object {
|
||||||
|
"href": "/docs/next/foo/bar",
|
||||||
|
"label": "bar",
|
||||||
|
"type": "link",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"label": "Test",
|
||||||
|
"type": "category",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"items": Array [
|
||||||
|
Object {
|
||||||
|
"href": "/docs/next/hello",
|
||||||
|
"label": "hello",
|
||||||
|
"type": "link",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"label": "Guides",
|
||||||
|
"type": "category",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"permalinkToSidebar": Object {
|
||||||
|
"/docs/next/foo/bar": "docs",
|
||||||
|
"/docs/next/hello": "docs",
|
||||||
|
},
|
||||||
|
"version": "next",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`versioned website content: sidebars needed for each version 1`] = `
|
||||||
|
Object {
|
||||||
|
"1.0.0": Set {
|
||||||
|
"version-1.0.0/docs",
|
||||||
|
},
|
||||||
|
"1.0.1": Set {
|
||||||
|
"version-1.0.1/docs",
|
||||||
|
},
|
||||||
|
"next": Set {
|
||||||
|
"docs",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
|
@ -0,0 +1,74 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`docsVersion first time versioning 1`] = `
|
||||||
|
Object {
|
||||||
|
"version-1.0.0/docs": Array [
|
||||||
|
Object {
|
||||||
|
"items": Array [
|
||||||
|
Object {
|
||||||
|
"items": Array [
|
||||||
|
Object {
|
||||||
|
"id": "version-1.0.0/foo/bar",
|
||||||
|
"type": "doc",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"id": "version-1.0.0/foo/baz",
|
||||||
|
"type": "doc",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"label": "foo",
|
||||||
|
"type": "category",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"href": "https://github.com",
|
||||||
|
"label": "Github",
|
||||||
|
"type": "link",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"id": "version-1.0.0/hello",
|
||||||
|
"type": "ref",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"label": "Test",
|
||||||
|
"type": "category",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"items": Array [
|
||||||
|
Object {
|
||||||
|
"id": "version-1.0.0/hello",
|
||||||
|
"type": "doc",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"label": "Guides",
|
||||||
|
"type": "category",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`docsVersion not the first time versioning 1`] = `
|
||||||
|
Object {
|
||||||
|
"version-2.0.0/docs": Array [
|
||||||
|
Object {
|
||||||
|
"items": Array [
|
||||||
|
Object {
|
||||||
|
"id": "version-2.0.0/foo/bar",
|
||||||
|
"type": "doc",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"label": "Test",
|
||||||
|
"type": "category",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"items": Array [
|
||||||
|
Object {
|
||||||
|
"id": "version-2.0.0/hello",
|
||||||
|
"type": "doc",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"label": "Guides",
|
||||||
|
"type": "category",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
|
@ -0,0 +1,38 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2017-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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';
|
||||||
|
|
||||||
|
describe('loadEnv', () => {
|
||||||
|
test('website with versioning disabled', () => {
|
||||||
|
const siteDir = path.join(__dirname, '__fixtures__', 'simple-site');
|
||||||
|
const env = loadEnv(siteDir);
|
||||||
|
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);
|
||||||
|
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']);
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
expect(env.versioning.enabled).toBe(false);
|
||||||
|
mock.mockRestore();
|
||||||
|
});
|
||||||
|
});
|
|
@ -8,19 +8,31 @@
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import {validate} from 'webpack';
|
import {validate} from 'webpack';
|
||||||
import {isMatch} from 'picomatch';
|
import {isMatch} from 'picomatch';
|
||||||
|
import commander from 'commander';
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import pluginContentDocs from '../index';
|
import pluginContentDocs from '../index';
|
||||||
|
import loadEnv from '../env';
|
||||||
import {loadContext} from '@docusaurus/core/src/server/index';
|
import {loadContext} from '@docusaurus/core/src/server/index';
|
||||||
import {applyConfigureWebpack} from '@docusaurus/core/src/webpack/utils';
|
import {applyConfigureWebpack} from '@docusaurus/core/src/webpack/utils';
|
||||||
import {RouteConfig} from '@docusaurus/types';
|
import {RouteConfig} from '@docusaurus/types';
|
||||||
import {posixPath} from '@docusaurus/utils';
|
import {posixPath} from '@docusaurus/utils';
|
||||||
|
import {sortConfig} from '@docusaurus/core/src/server/plugins';
|
||||||
|
|
||||||
const createFakeActions = (routeConfigs: RouteConfig[], contentDir) => {
|
import * as version from '../version';
|
||||||
|
|
||||||
|
const createFakeActions = (
|
||||||
|
routeConfigs: RouteConfig[],
|
||||||
|
contentDir,
|
||||||
|
dataContainer?,
|
||||||
|
) => {
|
||||||
return {
|
return {
|
||||||
addRoute: (config: RouteConfig) => {
|
addRoute: (config: RouteConfig) => {
|
||||||
routeConfigs.push(config);
|
routeConfigs.push(config);
|
||||||
},
|
},
|
||||||
createData: async (name, _content) => {
|
createData: async (name, content) => {
|
||||||
|
if (dataContainer) {
|
||||||
|
dataContainer[name] = content;
|
||||||
|
}
|
||||||
return path.join(contentDir, name);
|
return path.join(contentDir, name);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -84,6 +96,18 @@ describe('simple website', () => {
|
||||||
});
|
});
|
||||||
const pluginContentDir = path.join(context.generatedFilesDir, plugin.name);
|
const pluginContentDir = path.join(context.generatedFilesDir, plugin.name);
|
||||||
|
|
||||||
|
test('extendCli - docsVersion', () => {
|
||||||
|
const mock = jest.spyOn(version, 'docsVersion').mockImplementation();
|
||||||
|
const cli = new commander.Command();
|
||||||
|
plugin.extendCli(cli);
|
||||||
|
cli.parse(['node', 'test', 'docs:version', '1.0.0']);
|
||||||
|
expect(mock).toHaveBeenCalledWith('1.0.0', siteDir, {
|
||||||
|
path: pluginPath,
|
||||||
|
sidebarPath,
|
||||||
|
});
|
||||||
|
mock.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
test('getPathToWatch', () => {
|
test('getPathToWatch', () => {
|
||||||
const pathToWatch = plugin.getPathsToWatch();
|
const pathToWatch = plugin.getPathsToWatch();
|
||||||
const matchPattern = pathToWatch.map(filepath =>
|
const matchPattern = pathToWatch.map(filepath =>
|
||||||
|
@ -126,7 +150,13 @@ describe('simple website', () => {
|
||||||
|
|
||||||
test('content', async () => {
|
test('content', async () => {
|
||||||
const content = await plugin.loadContent();
|
const content = await plugin.loadContent();
|
||||||
const {docsMetadata, docsSidebars} = content;
|
const {
|
||||||
|
docsMetadata,
|
||||||
|
docsSidebars,
|
||||||
|
versionToSidebars,
|
||||||
|
permalinkToSidebar,
|
||||||
|
} = content;
|
||||||
|
expect(versionToSidebars).toEqual({});
|
||||||
expect(docsMetadata.hello).toEqual({
|
expect(docsMetadata.hello).toEqual({
|
||||||
id: 'hello',
|
id: 'hello',
|
||||||
permalink: '/docs/hello',
|
permalink: '/docs/hello',
|
||||||
|
@ -156,13 +186,233 @@ describe('simple website', () => {
|
||||||
expect(docsSidebars).toMatchSnapshot();
|
expect(docsSidebars).toMatchSnapshot();
|
||||||
|
|
||||||
const routeConfigs = [];
|
const routeConfigs = [];
|
||||||
const actions = createFakeActions(routeConfigs, pluginContentDir);
|
const dataContainer = {};
|
||||||
|
const actions = createFakeActions(
|
||||||
|
routeConfigs,
|
||||||
|
pluginContentDir,
|
||||||
|
dataContainer,
|
||||||
|
);
|
||||||
|
|
||||||
await plugin.contentLoaded({
|
await plugin.contentLoaded({
|
||||||
content,
|
content,
|
||||||
actions,
|
actions,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// There is only one nested docs route for simple site
|
||||||
|
const baseMetadata = JSON.parse(dataContainer['docs-route-ff2.json']);
|
||||||
|
expect(baseMetadata.docsSidebars).toEqual(docsSidebars);
|
||||||
|
expect(baseMetadata.permalinkToSidebar).toEqual(permalinkToSidebar);
|
||||||
|
|
||||||
|
expect(routeConfigs).not.toEqual([]);
|
||||||
|
expect(routeConfigs).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('versioned website', () => {
|
||||||
|
const siteDir = path.join(__dirname, '__fixtures__', 'versioned-site');
|
||||||
|
const context = loadContext(siteDir);
|
||||||
|
const sidebarPath = path.join(siteDir, 'sidebars.json');
|
||||||
|
const routeBasePath = 'docs';
|
||||||
|
const plugin = pluginContentDocs(context, {
|
||||||
|
routeBasePath,
|
||||||
|
sidebarPath,
|
||||||
|
});
|
||||||
|
const env = loadEnv(siteDir);
|
||||||
|
const {docsDir: versionedDir} = env.versioning;
|
||||||
|
const pluginContentDir = path.join(context.generatedFilesDir, plugin.name);
|
||||||
|
|
||||||
|
test('extendCli - docsVersion', () => {
|
||||||
|
const mock = jest.spyOn(version, 'docsVersion').mockImplementation();
|
||||||
|
const cli = new commander.Command();
|
||||||
|
plugin.extendCli(cli);
|
||||||
|
cli.parse(['node', 'test', 'docs:version', '2.0.0']);
|
||||||
|
expect(mock).toHaveBeenCalledWith('2.0.0', siteDir, {
|
||||||
|
path: routeBasePath,
|
||||||
|
sidebarPath,
|
||||||
|
});
|
||||||
|
mock.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getPathToWatch', () => {
|
||||||
|
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}",
|
||||||
|
"versioned_sidebars/version-1.0.1-sidebars.json",
|
||||||
|
"versioned_sidebars/version-1.0.0-sidebars.json",
|
||||||
|
"versioned_docs/version-1.0.1/**/*.{md,mdx}",
|
||||||
|
"versioned_docs/version-1.0.0/**/*.{md,mdx}",
|
||||||
|
"sidebars.json",
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
expect(isMatch('docs/hello.md', matchPattern)).toEqual(true);
|
||||||
|
expect(isMatch('docs/hello.mdx', matchPattern)).toEqual(true);
|
||||||
|
expect(isMatch('docs/foo/bar.md', matchPattern)).toEqual(true);
|
||||||
|
expect(isMatch('sidebars.json', matchPattern)).toEqual(true);
|
||||||
|
expect(
|
||||||
|
isMatch('versioned_docs/version-1.0.0/hello.md', matchPattern),
|
||||||
|
).toEqual(true);
|
||||||
|
expect(
|
||||||
|
isMatch('versioned_docs/version-1.0.0/foo/bar.md', matchPattern),
|
||||||
|
).toEqual(true);
|
||||||
|
expect(
|
||||||
|
isMatch('versioned_sidebars/version-1.0.0-sidebars.json', matchPattern),
|
||||||
|
).toEqual(true);
|
||||||
|
|
||||||
|
// Non existing version
|
||||||
|
expect(
|
||||||
|
isMatch('versioned_docs/version-2.0.0/foo/bar.md', matchPattern),
|
||||||
|
).toEqual(false);
|
||||||
|
expect(
|
||||||
|
isMatch('versioned_docs/version-2.0.0/hello.md', matchPattern),
|
||||||
|
).toEqual(false);
|
||||||
|
expect(
|
||||||
|
isMatch('versioned_sidebars/version-2.0.0-sidebars.json', matchPattern),
|
||||||
|
).toEqual(false);
|
||||||
|
|
||||||
|
expect(isMatch('docs/hello.js', matchPattern)).toEqual(false);
|
||||||
|
expect(isMatch('docs/super.mdl', matchPattern)).toEqual(false);
|
||||||
|
expect(isMatch('docs/mdx', matchPattern)).toEqual(false);
|
||||||
|
expect(isMatch('hello.md', matchPattern)).toEqual(false);
|
||||||
|
expect(isMatch('super/docs/hello.md', matchPattern)).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('content', async () => {
|
||||||
|
const content = await plugin.loadContent();
|
||||||
|
const {
|
||||||
|
docsMetadata,
|
||||||
|
docsSidebars,
|
||||||
|
versionToSidebars,
|
||||||
|
permalinkToSidebar,
|
||||||
|
} = content;
|
||||||
|
|
||||||
|
// 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({
|
||||||
|
id: 'foo/bar',
|
||||||
|
permalink: '/docs/next/foo/bar',
|
||||||
|
source: path.join('@site', routeBasePath, 'foo', 'bar.md'),
|
||||||
|
title: 'bar',
|
||||||
|
description: 'This is `next` version of bar.',
|
||||||
|
version: 'next',
|
||||||
|
sidebar: 'docs',
|
||||||
|
next: {
|
||||||
|
title: 'hello',
|
||||||
|
permalink: '/docs/next/hello',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(docsMetadata['hello']).toEqual({
|
||||||
|
id: 'hello',
|
||||||
|
permalink: '/docs/next/hello',
|
||||||
|
source: path.join('@site', routeBasePath, 'hello.md'),
|
||||||
|
title: 'hello',
|
||||||
|
description: 'Hello `next` !',
|
||||||
|
version: 'next',
|
||||||
|
sidebar: 'docs',
|
||||||
|
previous: {
|
||||||
|
title: 'bar',
|
||||||
|
permalink: '/docs/next/foo/bar',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(docsMetadata['version-1.0.1/hello']).toEqual({
|
||||||
|
id: 'version-1.0.1/hello',
|
||||||
|
permalink: '/docs/hello',
|
||||||
|
source: path.join(
|
||||||
|
'@site',
|
||||||
|
path.relative(siteDir, versionedDir),
|
||||||
|
'version-1.0.1',
|
||||||
|
'hello.md',
|
||||||
|
),
|
||||||
|
title: 'hello',
|
||||||
|
description: 'Hello `1.0.1` !',
|
||||||
|
version: '1.0.1',
|
||||||
|
sidebar: 'version-1.0.1/docs',
|
||||||
|
previous: {
|
||||||
|
title: 'bar',
|
||||||
|
permalink: '/docs/foo/bar',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(docsMetadata['version-1.0.0/foo/baz']).toEqual({
|
||||||
|
id: 'version-1.0.0/foo/baz',
|
||||||
|
permalink: '/docs/1.0.0/foo/baz',
|
||||||
|
source: path.join(
|
||||||
|
'@site',
|
||||||
|
path.relative(siteDir, versionedDir),
|
||||||
|
'version-1.0.0',
|
||||||
|
'foo',
|
||||||
|
'baz.md',
|
||||||
|
),
|
||||||
|
title: 'baz',
|
||||||
|
description:
|
||||||
|
'Baz `1.0.0` ! This will be deleted in next subsequent versions.',
|
||||||
|
version: '1.0.0',
|
||||||
|
sidebar: 'version-1.0.0/docs',
|
||||||
|
next: {
|
||||||
|
title: 'hello',
|
||||||
|
permalink: '/docs/1.0.0/hello',
|
||||||
|
},
|
||||||
|
previous: {
|
||||||
|
title: 'bar',
|
||||||
|
permalink: '/docs/1.0.0/foo/bar',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(docsSidebars).toMatchSnapshot('all sidebars');
|
||||||
|
expect(versionToSidebars).toMatchSnapshot(
|
||||||
|
'sidebars needed for each version',
|
||||||
|
);
|
||||||
|
const routeConfigs = [];
|
||||||
|
const dataContainer = {};
|
||||||
|
const actions = createFakeActions(
|
||||||
|
routeConfigs,
|
||||||
|
pluginContentDir,
|
||||||
|
dataContainer,
|
||||||
|
);
|
||||||
|
await plugin.contentLoaded({
|
||||||
|
content,
|
||||||
|
actions,
|
||||||
|
});
|
||||||
|
|
||||||
|
// The created base metadata for each nested docs route is smartly chunked/ splitted across version
|
||||||
|
const latestVersionBaseMetadata = JSON.parse(
|
||||||
|
dataContainer['docs-route-ff2.json'],
|
||||||
|
);
|
||||||
|
expect(latestVersionBaseMetadata).toMatchSnapshot(
|
||||||
|
'base metadata for latest version',
|
||||||
|
);
|
||||||
|
expect(latestVersionBaseMetadata.docsSidebars).not.toEqual(docsSidebars);
|
||||||
|
expect(latestVersionBaseMetadata.permalinkToSidebar).not.toEqual(
|
||||||
|
permalinkToSidebar,
|
||||||
|
);
|
||||||
|
const nextVersionBaseMetadata = JSON.parse(
|
||||||
|
dataContainer['docs-next-route-1c8.json'],
|
||||||
|
);
|
||||||
|
expect(nextVersionBaseMetadata).toMatchSnapshot(
|
||||||
|
'base metadata for next version',
|
||||||
|
);
|
||||||
|
expect(nextVersionBaseMetadata.docsSidebars).not.toEqual(docsSidebars);
|
||||||
|
expect(nextVersionBaseMetadata.permalinkToSidebar).not.toEqual(
|
||||||
|
permalinkToSidebar,
|
||||||
|
);
|
||||||
|
const firstVersionBaseMetadata = JSON.parse(
|
||||||
|
dataContainer['docs-1-0-0-route-660.json'],
|
||||||
|
);
|
||||||
|
expect(firstVersionBaseMetadata).toMatchSnapshot(
|
||||||
|
'base metadata for first version',
|
||||||
|
);
|
||||||
|
expect(nextVersionBaseMetadata.docsSidebars).not.toEqual(docsSidebars);
|
||||||
|
expect(nextVersionBaseMetadata.permalinkToSidebar).not.toEqual(
|
||||||
|
permalinkToSidebar,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Sort the route config like in src/server/plugins/index.ts for consistent snapshot ordering
|
||||||
|
sortConfig(routeConfigs);
|
||||||
|
|
||||||
expect(routeConfigs).not.toEqual([]);
|
expect(routeConfigs).not.toEqual([]);
|
||||||
expect(routeConfigs).toMatchSnapshot();
|
expect(routeConfigs).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,96 +6,81 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import {loadContext} from '@docusaurus/core/src/server/index';
|
||||||
import processMetadata from '../metadata';
|
import processMetadata from '../metadata';
|
||||||
|
import loadEnv from '../env';
|
||||||
|
|
||||||
describe('processMetadata', () => {
|
const fixtureDir = path.join(__dirname, '__fixtures__');
|
||||||
const fixtureDir = path.join(__dirname, '__fixtures__');
|
|
||||||
|
describe('simple site', () => {
|
||||||
const simpleSiteDir = path.join(fixtureDir, 'simple-site');
|
const simpleSiteDir = path.join(fixtureDir, 'simple-site');
|
||||||
const siteConfig = {
|
const context = loadContext(simpleSiteDir);
|
||||||
title: 'Hello',
|
const routeBasePath = 'docs';
|
||||||
baseUrl: '/',
|
const docsDir = path.resolve(simpleSiteDir, routeBasePath);
|
||||||
url: 'https://docusaurus.io',
|
|
||||||
};
|
const env = loadEnv(simpleSiteDir);
|
||||||
const pluginPath = 'docs';
|
|
||||||
const docsDir = path.resolve(simpleSiteDir, pluginPath);
|
|
||||||
|
|
||||||
test('normal docs', async () => {
|
test('normal docs', async () => {
|
||||||
const sourceA = path.join('foo', 'bar.md');
|
const sourceA = path.join('foo', 'bar.md');
|
||||||
const sourceB = path.join('hello.md');
|
const sourceB = path.join('hello.md');
|
||||||
|
const options = {
|
||||||
|
routeBasePath,
|
||||||
|
};
|
||||||
|
|
||||||
const [dataA, dataB] = await Promise.all([
|
const [dataA, dataB] = await Promise.all([
|
||||||
processMetadata({
|
processMetadata({
|
||||||
source: sourceA,
|
source: sourceA,
|
||||||
docsDir,
|
refDir: docsDir,
|
||||||
order: {},
|
context,
|
||||||
siteConfig,
|
options,
|
||||||
docsBasePath: pluginPath,
|
env,
|
||||||
siteDir: simpleSiteDir,
|
|
||||||
}),
|
}),
|
||||||
processMetadata({
|
processMetadata({
|
||||||
source: sourceB,
|
source: sourceB,
|
||||||
docsDir,
|
refDir: docsDir,
|
||||||
order: {},
|
context,
|
||||||
siteConfig,
|
options,
|
||||||
docsBasePath: pluginPath,
|
env,
|
||||||
siteDir: simpleSiteDir,
|
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(dataA).toEqual({
|
expect(dataA).toEqual({
|
||||||
id: 'foo/bar',
|
id: 'foo/bar',
|
||||||
permalink: '/docs/foo/bar',
|
permalink: '/docs/foo/bar',
|
||||||
source: path.join('@site', pluginPath, sourceA),
|
source: path.join('@site', routeBasePath, sourceA),
|
||||||
title: 'Bar',
|
title: 'Bar',
|
||||||
description: 'This is custom description',
|
description: 'This is custom description',
|
||||||
});
|
});
|
||||||
expect(dataB).toEqual({
|
expect(dataB).toEqual({
|
||||||
id: 'hello',
|
id: 'hello',
|
||||||
permalink: '/docs/hello',
|
permalink: '/docs/hello',
|
||||||
source: path.join('@site', pluginPath, sourceB),
|
source: path.join('@site', routeBasePath, sourceB),
|
||||||
title: 'Hello, World !',
|
title: 'Hello, World !',
|
||||||
description: `Hi, Endilie here :)`,
|
description: `Hi, Endilie here :)`,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('docs with custom permalink', async () => {
|
|
||||||
const source = path.join('permalink.md');
|
|
||||||
const data = await processMetadata({
|
|
||||||
source,
|
|
||||||
docsDir,
|
|
||||||
order: {},
|
|
||||||
siteConfig,
|
|
||||||
docsBasePath: pluginPath,
|
|
||||||
siteDir: simpleSiteDir,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(data).toEqual({
|
|
||||||
id: 'permalink',
|
|
||||||
permalink: '/docs/endiliey/permalink',
|
|
||||||
source: path.join('@site', pluginPath, source),
|
|
||||||
title: 'Permalink',
|
|
||||||
description: 'This has a different permalink',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('docs with editUrl', async () => {
|
test('docs with editUrl', async () => {
|
||||||
const editUrl =
|
const editUrl =
|
||||||
'https://github.com/facebook/docusaurus/edit/master/website';
|
'https://github.com/facebook/docusaurus/edit/master/website';
|
||||||
const source = path.join('foo', 'baz.md');
|
const source = path.join('foo', 'baz.md');
|
||||||
|
const options = {
|
||||||
|
routeBasePath,
|
||||||
|
editUrl,
|
||||||
|
};
|
||||||
|
|
||||||
const data = await processMetadata({
|
const data = await processMetadata({
|
||||||
source,
|
source,
|
||||||
docsDir,
|
refDir: docsDir,
|
||||||
order: {},
|
context,
|
||||||
siteConfig,
|
options,
|
||||||
docsBasePath: pluginPath,
|
env,
|
||||||
siteDir: simpleSiteDir,
|
|
||||||
editUrl,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(data).toEqual({
|
expect(data).toEqual({
|
||||||
id: 'foo/baz',
|
id: 'foo/baz',
|
||||||
permalink: '/docs/foo/baz',
|
permalink: '/docs/foo/baz',
|
||||||
source: path.join('@site', pluginPath, source),
|
source: path.join('@site', routeBasePath, source),
|
||||||
title: 'baz',
|
title: 'baz',
|
||||||
editUrl:
|
editUrl:
|
||||||
'https://github.com/facebook/docusaurus/edit/master/website/docs/foo/baz.md',
|
'https://github.com/facebook/docusaurus/edit/master/website/docs/foo/baz.md',
|
||||||
|
@ -103,62 +88,73 @@ describe('processMetadata', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('docs with custom editUrl', async () => {
|
test('docs with custom editUrl & unrelated frontmatter', async () => {
|
||||||
const source = 'lorem.md';
|
const source = 'lorem.md';
|
||||||
|
const options = {
|
||||||
|
routeBasePath,
|
||||||
|
};
|
||||||
|
|
||||||
const data = await processMetadata({
|
const data = await processMetadata({
|
||||||
source,
|
source,
|
||||||
docsDir,
|
refDir: docsDir,
|
||||||
order: {},
|
context,
|
||||||
siteConfig,
|
options,
|
||||||
docsBasePath: pluginPath,
|
env,
|
||||||
siteDir: simpleSiteDir,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(data).toEqual({
|
expect(data).toEqual({
|
||||||
id: 'lorem',
|
id: 'lorem',
|
||||||
permalink: '/docs/lorem',
|
permalink: '/docs/lorem',
|
||||||
source: path.join('@site', pluginPath, source),
|
source: path.join('@site', routeBasePath, source),
|
||||||
title: 'lorem',
|
title: 'lorem',
|
||||||
editUrl: 'https://github.com/customUrl/docs/lorem.md',
|
editUrl: 'https://github.com/customUrl/docs/lorem.md',
|
||||||
description: 'Lorem ipsum.',
|
description: 'Lorem ipsum.',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// unrelated frontmatter is not part of metadata
|
||||||
|
expect(data['unrelated_frontmatter']).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('docs with last update time and author', async () => {
|
test('docs with last update time and author', async () => {
|
||||||
const source = 'lorem.md';
|
const source = 'lorem.md';
|
||||||
const data = await processMetadata({
|
const options = {
|
||||||
source,
|
routeBasePath,
|
||||||
docsDir,
|
|
||||||
order: {},
|
|
||||||
siteConfig,
|
|
||||||
docsBasePath: pluginPath,
|
|
||||||
siteDir: simpleSiteDir,
|
|
||||||
showLastUpdateAuthor: true,
|
showLastUpdateAuthor: true,
|
||||||
showLastUpdateTime: true,
|
showLastUpdateTime: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const data = await processMetadata({
|
||||||
|
source,
|
||||||
|
refDir: docsDir,
|
||||||
|
context,
|
||||||
|
options,
|
||||||
|
env,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(data).toEqual({
|
expect(data).toEqual({
|
||||||
id: 'lorem',
|
id: 'lorem',
|
||||||
permalink: '/docs/lorem',
|
permalink: '/docs/lorem',
|
||||||
source: path.join('@site', pluginPath, source),
|
source: path.join('@site', routeBasePath, source),
|
||||||
title: 'lorem',
|
title: 'lorem',
|
||||||
editUrl: 'https://github.com/customUrl/docs/lorem.md',
|
editUrl: 'https://github.com/customUrl/docs/lorem.md',
|
||||||
description: 'Lorem ipsum.',
|
description: 'Lorem ipsum.',
|
||||||
lastUpdatedAt: '1539502055',
|
lastUpdatedAt: 1539502055,
|
||||||
lastUpdatedBy: 'Author',
|
lastUpdatedBy: 'Author',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('docs with invalid id', async () => {
|
test('docs with invalid id', async () => {
|
||||||
const badSiteDir = path.join(fixtureDir, 'bad-site');
|
const badSiteDir = path.join(fixtureDir, 'bad-site');
|
||||||
|
const options = {
|
||||||
|
routeBasePath,
|
||||||
|
};
|
||||||
|
|
||||||
return processMetadata({
|
return processMetadata({
|
||||||
source: 'invalid-id.md',
|
source: 'invalid-id.md',
|
||||||
docsDir: path.join(badSiteDir, 'docs'),
|
refDir: path.join(badSiteDir, 'docs'),
|
||||||
order: {},
|
context,
|
||||||
siteConfig,
|
options,
|
||||||
docsBasePath: 'docs',
|
env,
|
||||||
siteDir: simpleSiteDir,
|
|
||||||
}).catch(e =>
|
}).catch(e =>
|
||||||
expect(e).toMatchInlineSnapshot(
|
expect(e).toMatchInlineSnapshot(
|
||||||
`[Error: Document id cannot include "/".]`,
|
`[Error: Document id cannot include "/".]`,
|
||||||
|
@ -166,3 +162,128 @@ describe('processMetadata', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
const {docsDir: versionedDir} = env.versioning;
|
||||||
|
|
||||||
|
test('master/next docs', async () => {
|
||||||
|
const sourceA = path.join('foo', 'bar.md');
|
||||||
|
const sourceB = path.join('hello.md');
|
||||||
|
const options = {
|
||||||
|
routeBasePath,
|
||||||
|
};
|
||||||
|
|
||||||
|
const [dataA, dataB] = await Promise.all([
|
||||||
|
processMetadata({
|
||||||
|
source: sourceA,
|
||||||
|
refDir: docsDir,
|
||||||
|
context,
|
||||||
|
options,
|
||||||
|
env,
|
||||||
|
}),
|
||||||
|
processMetadata({
|
||||||
|
source: sourceB,
|
||||||
|
refDir: docsDir,
|
||||||
|
context,
|
||||||
|
options,
|
||||||
|
env,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(dataA).toEqual({
|
||||||
|
id: 'foo/bar',
|
||||||
|
permalink: '/docs/next/foo/bar',
|
||||||
|
source: path.join('@site', routeBasePath, sourceA),
|
||||||
|
title: 'bar',
|
||||||
|
description: 'This is `next` version of bar.',
|
||||||
|
version: 'next',
|
||||||
|
});
|
||||||
|
expect(dataB).toEqual({
|
||||||
|
id: 'hello',
|
||||||
|
permalink: '/docs/next/hello',
|
||||||
|
source: path.join('@site', routeBasePath, sourceB),
|
||||||
|
title: 'hello',
|
||||||
|
description: 'Hello `next` !',
|
||||||
|
version: 'next',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('versioned docs', async () => {
|
||||||
|
const sourceA = path.join('version-1.0.0', 'foo', 'bar.md');
|
||||||
|
const sourceB = path.join('version-1.0.0', 'hello.md');
|
||||||
|
const sourceC = path.join('version-1.0.1', 'foo', 'bar.md');
|
||||||
|
const sourceD = path.join('version-1.0.1', 'hello.md');
|
||||||
|
const options = {
|
||||||
|
routeBasePath,
|
||||||
|
};
|
||||||
|
|
||||||
|
const [dataA, dataB, dataC, dataD] = await Promise.all([
|
||||||
|
processMetadata({
|
||||||
|
source: sourceA,
|
||||||
|
refDir: versionedDir,
|
||||||
|
context,
|
||||||
|
options,
|
||||||
|
env,
|
||||||
|
}),
|
||||||
|
processMetadata({
|
||||||
|
source: sourceB,
|
||||||
|
refDir: versionedDir,
|
||||||
|
context,
|
||||||
|
options,
|
||||||
|
env,
|
||||||
|
}),
|
||||||
|
processMetadata({
|
||||||
|
source: sourceC,
|
||||||
|
refDir: versionedDir,
|
||||||
|
context,
|
||||||
|
options,
|
||||||
|
env,
|
||||||
|
}),
|
||||||
|
processMetadata({
|
||||||
|
source: sourceD,
|
||||||
|
refDir: versionedDir,
|
||||||
|
context,
|
||||||
|
options,
|
||||||
|
env,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(dataA).toEqual({
|
||||||
|
id: 'version-1.0.0/foo/bar',
|
||||||
|
permalink: '/docs/1.0.0/foo/bar',
|
||||||
|
source: path.join('@site', path.relative(siteDir, versionedDir), sourceA),
|
||||||
|
title: 'bar',
|
||||||
|
description: 'Bar `1.0.0` !',
|
||||||
|
version: '1.0.0',
|
||||||
|
});
|
||||||
|
expect(dataB).toEqual({
|
||||||
|
id: 'version-1.0.0/hello',
|
||||||
|
permalink: '/docs/1.0.0/hello',
|
||||||
|
source: path.join('@site', path.relative(siteDir, versionedDir), sourceB),
|
||||||
|
title: 'hello',
|
||||||
|
description: 'Hello `1.0.0` !',
|
||||||
|
version: '1.0.0',
|
||||||
|
});
|
||||||
|
expect(dataC).toEqual({
|
||||||
|
id: 'version-1.0.1/foo/bar',
|
||||||
|
permalink: '/docs/foo/bar',
|
||||||
|
source: path.join('@site', path.relative(siteDir, versionedDir), sourceC),
|
||||||
|
title: 'bar',
|
||||||
|
description: 'Bar `1.0.1` !',
|
||||||
|
version: '1.0.1',
|
||||||
|
});
|
||||||
|
expect(dataD).toEqual({
|
||||||
|
id: 'version-1.0.1/hello',
|
||||||
|
permalink: '/docs/hello',
|
||||||
|
source: path.join('@site', path.relative(siteDir, versionedDir), sourceD),
|
||||||
|
title: 'hello',
|
||||||
|
description: 'Hello `1.0.1` !',
|
||||||
|
version: '1.0.1',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -230,32 +230,6 @@ describe('createOrder', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('multiple sidebars with unknown sidebar item type', () => {
|
|
||||||
expect(() =>
|
|
||||||
createOrder({
|
|
||||||
docs: [
|
|
||||||
{
|
|
||||||
type: 'category',
|
|
||||||
label: 'Category1',
|
|
||||||
items: [
|
|
||||||
{type: 'endi', id: 'doc1'},
|
|
||||||
{type: 'doc', id: 'doc2'},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
otherDocs: [
|
|
||||||
{
|
|
||||||
type: 'category',
|
|
||||||
label: 'Category1',
|
|
||||||
items: [{type: 'doc', id: 'doc5'}],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
).toThrowErrorMatchingInlineSnapshot(
|
|
||||||
`"Unknown item type: endi. Item: {\\"type\\":\\"endi\\",\\"id\\":\\"doc1\\"}"`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('edge cases', () => {
|
test('edge cases', () => {
|
||||||
expect(createOrder({})).toEqual({});
|
expect(createOrder({})).toEqual({});
|
||||||
expect(createOrder(undefined)).toEqual({});
|
expect(createOrder(undefined)).toEqual({});
|
||||||
|
|
|
@ -14,13 +14,13 @@ describe('loadSidebars', () => {
|
||||||
const fixtureDir = path.join(__dirname, '__fixtures__', 'sidebars');
|
const fixtureDir = path.join(__dirname, '__fixtures__', 'sidebars');
|
||||||
test('sidebars with known sidebar item type', async () => {
|
test('sidebars with known sidebar item type', async () => {
|
||||||
const sidebarPath = path.join(fixtureDir, 'sidebars.json');
|
const sidebarPath = path.join(fixtureDir, 'sidebars.json');
|
||||||
const result = loadSidebars(sidebarPath);
|
const result = loadSidebars([sidebarPath]);
|
||||||
expect(result).toMatchSnapshot();
|
expect(result).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('sidebars with deep level of category', async () => {
|
test('sidebars with deep level of category', async () => {
|
||||||
const sidebarPath = path.join(fixtureDir, 'sidebars-category.js');
|
const sidebarPath = path.join(fixtureDir, 'sidebars-category.js');
|
||||||
const result = loadSidebars(sidebarPath);
|
const result = loadSidebars([sidebarPath]);
|
||||||
expect(result).toMatchSnapshot();
|
expect(result).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -29,7 +29,9 @@ describe('loadSidebars', () => {
|
||||||
fixtureDir,
|
fixtureDir,
|
||||||
'sidebars-category-wrong-items.json',
|
'sidebars-category-wrong-items.json',
|
||||||
);
|
);
|
||||||
expect(() => loadSidebars(sidebarPath)).toThrowErrorMatchingInlineSnapshot(
|
expect(() =>
|
||||||
|
loadSidebars([sidebarPath]),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
`"Error loading \\"Category Label\\" category. Category items must be array."`,
|
`"Error loading \\"Category Label\\" category. Category items must be array."`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -37,23 +39,29 @@ describe('loadSidebars', () => {
|
||||||
test('sidebars with first level not a category', async () => {
|
test('sidebars with first level not a category', async () => {
|
||||||
const sidebarPath = path.join(
|
const sidebarPath = path.join(
|
||||||
fixtureDir,
|
fixtureDir,
|
||||||
'sidebars-first-level-not-category',
|
'sidebars-first-level-not-category.js',
|
||||||
);
|
);
|
||||||
expect(() => loadSidebars(sidebarPath)).toThrowErrorMatchingInlineSnapshot(
|
expect(() =>
|
||||||
|
loadSidebars([sidebarPath]),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
`"Error loading {\\"type\\":\\"doc\\",\\"id\\":\\"api\\"}. First level item of a sidebar must be a category"`,
|
`"Error loading {\\"type\\":\\"doc\\",\\"id\\":\\"api\\"}. First level item of a sidebar must be a category"`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('sidebars with unknown sidebar item type', async () => {
|
test('sidebars with unknown sidebar item type', async () => {
|
||||||
const sidebarPath = path.join(fixtureDir, 'sidebars-unknown-type.json');
|
const sidebarPath = path.join(fixtureDir, 'sidebars-unknown-type.json');
|
||||||
expect(() => loadSidebars(sidebarPath)).toThrowErrorMatchingInlineSnapshot(
|
expect(() =>
|
||||||
|
loadSidebars([sidebarPath]),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
`"Unknown sidebar item type: superman"`,
|
`"Unknown sidebar item type: superman"`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('sidebars with known sidebar item type but wrong field', async () => {
|
test('sidebars with known sidebar item type but wrong field', async () => {
|
||||||
const sidebarPath = path.join(fixtureDir, 'sidebars-wrong-field.json');
|
const sidebarPath = path.join(fixtureDir, 'sidebars-wrong-field.json');
|
||||||
expect(() => loadSidebars(sidebarPath)).toThrowErrorMatchingInlineSnapshot(
|
expect(() =>
|
||||||
|
loadSidebars([sidebarPath]),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
`"Unknown sidebar item keys: href. Item: {\\"type\\":\\"category\\",\\"label\\":\\"category\\",\\"href\\":\\"https://github.com\\"}"`,
|
`"Unknown sidebar item keys: href. Item: {\\"type\\":\\"category\\",\\"label\\":\\"category\\",\\"href\\":\\"https://github.com\\"}"`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -62,10 +70,4 @@ describe('loadSidebars', () => {
|
||||||
const result = loadSidebars(null);
|
const result = loadSidebars(null);
|
||||||
expect(result).toEqual({});
|
expect(result).toEqual({});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('fake sidebars path', () => {
|
|
||||||
expect(() => {
|
|
||||||
loadSidebars('/fake/path');
|
|
||||||
}).toThrowError();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,198 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2017-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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 {docsVersion} from '../version';
|
||||||
|
import {PathOptions} from '../types';
|
||||||
|
import fs from 'fs-extra';
|
||||||
|
import {
|
||||||
|
getVersionedDocsDir,
|
||||||
|
getVersionsJSONFile,
|
||||||
|
getVersionedSidebarsDir,
|
||||||
|
} from '../env';
|
||||||
|
|
||||||
|
const fixtureDir = path.join(__dirname, '__fixtures__');
|
||||||
|
|
||||||
|
describe('docsVersion', () => {
|
||||||
|
const simpleSiteDir = path.join(fixtureDir, 'simple-site');
|
||||||
|
const versionedSiteDir = path.join(fixtureDir, 'versioned-site');
|
||||||
|
const DEFAULT_OPTIONS: PathOptions = {
|
||||||
|
path: 'docs',
|
||||||
|
sidebarPath: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
test('no version tag provided', () => {
|
||||||
|
expect(() =>
|
||||||
|
docsVersion(null, simpleSiteDir, DEFAULT_OPTIONS),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`"No version tag specified!. Pass the version you wish to create as an argument. Ex: 1.0.0"`,
|
||||||
|
);
|
||||||
|
expect(() =>
|
||||||
|
docsVersion(undefined, simpleSiteDir, DEFAULT_OPTIONS),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`"No version tag specified!. Pass the version you wish to create as an argument. Ex: 1.0.0"`,
|
||||||
|
);
|
||||||
|
expect(() =>
|
||||||
|
docsVersion('', simpleSiteDir, DEFAULT_OPTIONS),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`"No version tag specified!. Pass the version you wish to create as an argument. Ex: 1.0.0"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('version tag should not have slash', () => {
|
||||||
|
expect(() =>
|
||||||
|
docsVersion('foo/bar', simpleSiteDir, DEFAULT_OPTIONS),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`"Invalid version tag specified! Do not include slash (/) or (\\\\). Try something like: 1.0.0"`,
|
||||||
|
);
|
||||||
|
expect(() =>
|
||||||
|
docsVersion('foo\\bar', simpleSiteDir, DEFAULT_OPTIONS),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`"Invalid version tag specified! Do not include slash (/) or (\\\\). Try something like: 1.0.0"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('version tag should not be too long', () => {
|
||||||
|
expect(() =>
|
||||||
|
docsVersion('a'.repeat(255), simpleSiteDir, DEFAULT_OPTIONS),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`"Invalid version tag specified! Length must <= 32 characters. Try something like: 1.0.0"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('version tag should not be a dot or two dots', () => {
|
||||||
|
expect(() =>
|
||||||
|
docsVersion('..', simpleSiteDir, DEFAULT_OPTIONS),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`"Invalid version tag specified! Do not name your version \\".\\" or \\"..\\". Try something like: 1.0.0"`,
|
||||||
|
);
|
||||||
|
expect(() =>
|
||||||
|
docsVersion('.', simpleSiteDir, DEFAULT_OPTIONS),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`"Invalid version tag specified! Do not name your version \\".\\" or \\"..\\". Try something like: 1.0.0"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('version tag should be a valid pathname', () => {
|
||||||
|
expect(() =>
|
||||||
|
docsVersion('<foo|bar>', simpleSiteDir, DEFAULT_OPTIONS),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`"Invalid version tag specified! Please ensure its a valid pathname too. Try something like: 1.0.0"`,
|
||||||
|
);
|
||||||
|
expect(() =>
|
||||||
|
docsVersion('foo\x00bar', simpleSiteDir, DEFAULT_OPTIONS),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`"Invalid version tag specified! Please ensure its a valid pathname too. Try something like: 1.0.0"`,
|
||||||
|
);
|
||||||
|
expect(() =>
|
||||||
|
docsVersion('foo:bar', simpleSiteDir, DEFAULT_OPTIONS),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`"Invalid version tag specified! Please ensure its a valid pathname too. Try something like: 1.0.0"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('version tag already exist', () => {
|
||||||
|
expect(() =>
|
||||||
|
docsVersion('1.0.0', versionedSiteDir, DEFAULT_OPTIONS),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`"This version already exists!. Use a version tag that does not already exist."`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('no docs file to version', () => {
|
||||||
|
const emptySiteDir = path.join(fixtureDir, 'empty-site');
|
||||||
|
expect(() =>
|
||||||
|
docsVersion('1.0.0', emptySiteDir, DEFAULT_OPTIONS),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(`"There is no docs to version !"`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('first time versioning', () => {
|
||||||
|
const copyMock = jest.spyOn(fs, 'copySync').mockImplementation();
|
||||||
|
const ensureMock = jest.spyOn(fs, 'ensureDirSync').mockImplementation();
|
||||||
|
const writeMock = jest.spyOn(fs, 'writeFileSync');
|
||||||
|
let versionedSidebar;
|
||||||
|
let versionedSidebarPath;
|
||||||
|
writeMock.mockImplementationOnce((filepath, content) => {
|
||||||
|
versionedSidebarPath = filepath;
|
||||||
|
versionedSidebar = JSON.parse(content);
|
||||||
|
});
|
||||||
|
let versionsPath;
|
||||||
|
let versions;
|
||||||
|
writeMock.mockImplementationOnce((filepath, content) => {
|
||||||
|
versionsPath = filepath;
|
||||||
|
versions = JSON.parse(content);
|
||||||
|
});
|
||||||
|
const consoleMock = jest.spyOn(console, 'log').mockImplementation();
|
||||||
|
const options = {
|
||||||
|
path: 'docs',
|
||||||
|
sidebarPath: path.join(simpleSiteDir, 'sidebars.json'),
|
||||||
|
};
|
||||||
|
docsVersion('1.0.0', simpleSiteDir, options);
|
||||||
|
expect(copyMock).toHaveBeenCalledWith(
|
||||||
|
path.join(simpleSiteDir, options.path),
|
||||||
|
path.join(getVersionedDocsDir(simpleSiteDir), 'version-1.0.0'),
|
||||||
|
);
|
||||||
|
expect(versionedSidebar).toMatchSnapshot();
|
||||||
|
expect(versionedSidebarPath).toEqual(
|
||||||
|
path.join(
|
||||||
|
getVersionedSidebarsDir(simpleSiteDir),
|
||||||
|
'version-1.0.0-sidebars.json',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(versionsPath).toEqual(getVersionsJSONFile(simpleSiteDir));
|
||||||
|
expect(versions).toEqual(['1.0.0']);
|
||||||
|
expect(consoleMock).toHaveBeenCalledWith('Version 1.0.0 created!');
|
||||||
|
|
||||||
|
copyMock.mockRestore();
|
||||||
|
writeMock.mockRestore();
|
||||||
|
consoleMock.mockRestore();
|
||||||
|
ensureMock.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('not the first time versioning', () => {
|
||||||
|
const copyMock = jest.spyOn(fs, 'copySync').mockImplementation();
|
||||||
|
const ensureMock = jest.spyOn(fs, 'ensureDirSync').mockImplementation();
|
||||||
|
const writeMock = jest.spyOn(fs, 'writeFileSync');
|
||||||
|
let versionedSidebar;
|
||||||
|
let versionedSidebarPath;
|
||||||
|
writeMock.mockImplementationOnce((filepath, content) => {
|
||||||
|
versionedSidebarPath = filepath;
|
||||||
|
versionedSidebar = JSON.parse(content);
|
||||||
|
});
|
||||||
|
let versionsPath;
|
||||||
|
let versions;
|
||||||
|
writeMock.mockImplementationOnce((filepath, content) => {
|
||||||
|
versionsPath = filepath;
|
||||||
|
versions = JSON.parse(content);
|
||||||
|
});
|
||||||
|
const consoleMock = jest.spyOn(console, 'log').mockImplementation();
|
||||||
|
const options = {
|
||||||
|
path: 'docs',
|
||||||
|
sidebarPath: path.join(versionedSiteDir, 'sidebars.json'),
|
||||||
|
};
|
||||||
|
docsVersion('2.0.0', versionedSiteDir, options);
|
||||||
|
expect(copyMock).toHaveBeenCalledWith(
|
||||||
|
path.join(versionedSiteDir, options.path),
|
||||||
|
path.join(getVersionedDocsDir(versionedSiteDir), 'version-2.0.0'),
|
||||||
|
);
|
||||||
|
expect(versionedSidebar).toMatchSnapshot();
|
||||||
|
expect(versionedSidebarPath).toEqual(
|
||||||
|
path.join(
|
||||||
|
getVersionedSidebarsDir(versionedSiteDir),
|
||||||
|
'version-2.0.0-sidebars.json',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(versionsPath).toEqual(getVersionsJSONFile(versionedSiteDir));
|
||||||
|
expect(versions).toEqual(['2.0.0', '1.0.1', '1.0.0']);
|
||||||
|
expect(consoleMock).toHaveBeenCalledWith('Version 2.0.0 created!');
|
||||||
|
|
||||||
|
copyMock.mockRestore();
|
||||||
|
writeMock.mockRestore();
|
||||||
|
consoleMock.mockRestore();
|
||||||
|
ensureMock.mockRestore();
|
||||||
|
});
|
||||||
|
});
|
10
packages/docusaurus-plugin-content-docs/src/constants.ts
Normal file
10
packages/docusaurus-plugin-content-docs/src/constants.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2017-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const VERSIONED_DOCS_DIR = 'versioned_docs';
|
||||||
|
export const VERSIONED_SIDEBARS_DIR = 'versioned_sidebars';
|
||||||
|
export const VERSIONS_JSON_FILE = 'versions.json';
|
55
packages/docusaurus-plugin-content-docs/src/env.ts
Normal file
55
packages/docusaurus-plugin-content-docs/src/env.ts
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2017-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* 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';
|
||||||
|
|
||||||
|
export function getVersionedDocsDir(siteDir: string) {
|
||||||
|
return path.join(siteDir, VERSIONED_DOCS_DIR);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getVersionedSidebarsDir(siteDir: string) {
|
||||||
|
return path.join(siteDir, VERSIONED_SIDEBARS_DIR);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getVersionsJSONFile(siteDir: string) {
|
||||||
|
return path.join(siteDir, VERSIONS_JSON_FILE);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function(siteDir: string): Env {
|
||||||
|
const versioning: VersioningEnv = {
|
||||||
|
enabled: false,
|
||||||
|
versions: [],
|
||||||
|
latestVersion: null,
|
||||||
|
docsDir: '',
|
||||||
|
sidebarsDir: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const versionsJSONFile = getVersionsJSONFile(siteDir);
|
||||||
|
if (fs.existsSync(versionsJSONFile)) {
|
||||||
|
const parsedVersions = JSON.parse(
|
||||||
|
fs.readFileSync(versionsJSONFile, 'utf8'),
|
||||||
|
);
|
||||||
|
if (parsedVersions && parsedVersions.length > 0) {
|
||||||
|
versioning.latestVersion = parsedVersions[0];
|
||||||
|
versioning.enabled = true;
|
||||||
|
versioning.versions = parsedVersions;
|
||||||
|
versioning.docsDir = getVersionedDocsDir(siteDir);
|
||||||
|
versioning.sidebarsDir = getVersionedSidebarsDir(siteDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
versioning,
|
||||||
|
};
|
||||||
|
}
|
|
@ -5,20 +5,17 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import _ from 'lodash';
|
||||||
import globby from 'globby';
|
import globby from 'globby';
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import {
|
import {normalizeUrl, docuHash, objectWithKeySorted} from '@docusaurus/utils';
|
||||||
idx,
|
import {LoadContext, Plugin, RouteConfig} from '@docusaurus/types';
|
||||||
normalizeUrl,
|
|
||||||
docuHash,
|
|
||||||
objectWithKeySorted,
|
|
||||||
} from '@docusaurus/utils';
|
|
||||||
import {LoadContext, Plugin} from '@docusaurus/types';
|
|
||||||
|
|
||||||
import createOrder from './order';
|
import createOrder from './order';
|
||||||
import loadSidebars from './sidebars';
|
import loadSidebars from './sidebars';
|
||||||
import processMetadata from './metadata';
|
import processMetadata from './metadata';
|
||||||
|
import loadEnv from './env';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
PluginOptions,
|
PluginOptions,
|
||||||
|
@ -35,8 +32,12 @@ import {
|
||||||
DocsSidebar,
|
DocsSidebar,
|
||||||
DocsBaseMetadata,
|
DocsBaseMetadata,
|
||||||
MetadataRaw,
|
MetadataRaw,
|
||||||
|
DocsMetadataRaw,
|
||||||
|
Metadata,
|
||||||
|
VersionToSidebars,
|
||||||
} from './types';
|
} from './types';
|
||||||
import {Configuration} from 'webpack';
|
import {Configuration} from 'webpack';
|
||||||
|
import {docsVersion} from './version';
|
||||||
|
|
||||||
const DEFAULT_OPTIONS: PluginOptions = {
|
const DEFAULT_OPTIONS: PluginOptions = {
|
||||||
path: 'docs', // Path to data on filesystem, relative to site dir.
|
path: 'docs', // Path to data on filesystem, relative to site dir.
|
||||||
|
@ -56,101 +57,166 @@ export default function pluginContentDocs(
|
||||||
opts: Partial<PluginOptions>,
|
opts: Partial<PluginOptions>,
|
||||||
): Plugin<LoadedContent | null> {
|
): Plugin<LoadedContent | null> {
|
||||||
const options = {...DEFAULT_OPTIONS, ...opts};
|
const options = {...DEFAULT_OPTIONS, ...opts};
|
||||||
const contentPath = path.resolve(context.siteDir, options.path);
|
const {siteDir, generatedFilesDir, baseUrl} = context;
|
||||||
let sourceToPermalink: SourceToPermalink = {};
|
const docsDir = path.resolve(siteDir, options.path);
|
||||||
|
const sourceToPermalink: SourceToPermalink = {};
|
||||||
|
|
||||||
const dataDir = path.join(
|
const dataDir = path.join(
|
||||||
context.generatedFilesDir,
|
generatedFilesDir,
|
||||||
'docusaurus-plugin-content-docs',
|
'docusaurus-plugin-content-docs',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Versioning
|
||||||
|
const env = loadEnv(siteDir);
|
||||||
|
const {versioning} = env;
|
||||||
|
const {
|
||||||
|
versions,
|
||||||
|
docsDir: versionedDir,
|
||||||
|
sidebarsDir: versionedSidebarsDir,
|
||||||
|
} = versioning;
|
||||||
|
const versionsNames = versions.map(version => `version-${version}`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: 'docusaurus-plugin-content-docs',
|
name: 'docusaurus-plugin-content-docs',
|
||||||
|
|
||||||
|
extendCli(cli) {
|
||||||
|
cli
|
||||||
|
.command('docs:version')
|
||||||
|
.arguments('<version>')
|
||||||
|
.description('Tag a new version for docs')
|
||||||
|
.action(version => {
|
||||||
|
docsVersion(version, siteDir, {
|
||||||
|
path: options.path,
|
||||||
|
sidebarPath: options.sidebarPath,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
getPathsToWatch() {
|
getPathsToWatch() {
|
||||||
const {include = []} = options;
|
const {include} = options;
|
||||||
const globPattern = include.map(pattern => `${contentPath}/${pattern}`);
|
let globPattern = include.map(pattern => `${docsDir}/${pattern}`);
|
||||||
|
if (versioning.enabled) {
|
||||||
|
const docsGlob = _.flatten(
|
||||||
|
include.map(pattern =>
|
||||||
|
versionsNames.map(
|
||||||
|
versionName => `${versionedDir}/${versionName}/${pattern}`,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const sidebarsGlob = versionsNames.map(
|
||||||
|
versionName => `${versionedSidebarsDir}/${versionName}-sidebars.json`,
|
||||||
|
);
|
||||||
|
globPattern = [...globPattern, ...sidebarsGlob, ...docsGlob];
|
||||||
|
}
|
||||||
return [...globPattern, options.sidebarPath];
|
return [...globPattern, options.sidebarPath];
|
||||||
},
|
},
|
||||||
|
|
||||||
// Fetches blog contents and returns metadata for the contents.
|
// Fetches blog contents and returns metadata for the contents.
|
||||||
async loadContent() {
|
async loadContent() {
|
||||||
const {
|
const {include, sidebarPath} = options;
|
||||||
include,
|
|
||||||
routeBasePath,
|
|
||||||
sidebarPath,
|
|
||||||
editUrl,
|
|
||||||
showLastUpdateAuthor,
|
|
||||||
showLastUpdateTime,
|
|
||||||
} = options;
|
|
||||||
const {siteConfig, siteDir} = context;
|
|
||||||
const docsDir = contentPath;
|
|
||||||
|
|
||||||
if (!fs.existsSync(docsDir)) {
|
if (!fs.existsSync(docsDir)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadedSidebars: Sidebar = loadSidebars(sidebarPath);
|
|
||||||
|
|
||||||
// Build the docs ordering such as next, previous, category and sidebar.
|
|
||||||
const order: Order = createOrder(loadedSidebars);
|
|
||||||
|
|
||||||
// Prepare metadata container.
|
// Prepare metadata container.
|
||||||
const docsMetadataRaw: {
|
const docsMetadataRaw: DocsMetadataRaw = {};
|
||||||
[id: string]: MetadataRaw;
|
const docsPromises = [];
|
||||||
} = {};
|
|
||||||
|
|
||||||
// Metadata for default docs files.
|
// Metadata for default/ master docs files.
|
||||||
const docsFiles = await globby(include, {
|
const docsFiles = await globby(include, {
|
||||||
cwd: docsDir,
|
cwd: docsDir,
|
||||||
});
|
});
|
||||||
await Promise.all(
|
docsPromises.push(
|
||||||
docsFiles.map(async source => {
|
Promise.all(
|
||||||
const metadata: MetadataRaw = await processMetadata({
|
docsFiles.map(async source => {
|
||||||
source,
|
const metadata: MetadataRaw = await processMetadata({
|
||||||
docsDir,
|
source,
|
||||||
order,
|
refDir: docsDir,
|
||||||
siteConfig,
|
context,
|
||||||
docsBasePath: routeBasePath,
|
options,
|
||||||
siteDir,
|
env,
|
||||||
editUrl,
|
});
|
||||||
showLastUpdateAuthor,
|
docsMetadataRaw[metadata.id] = metadata;
|
||||||
showLastUpdateTime,
|
}),
|
||||||
});
|
),
|
||||||
docsMetadataRaw[metadata.id] = metadata;
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Construct docsMetadata
|
// Metadata for versioned docs
|
||||||
|
if (versioning.enabled) {
|
||||||
|
const versionedGlob = _.flatten(
|
||||||
|
include.map(pattern =>
|
||||||
|
versionsNames.map(versionName => `${versionName}/${pattern}`),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
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;
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the sidebars & create docs ordering
|
||||||
|
const sidebarPaths = [
|
||||||
|
sidebarPath,
|
||||||
|
...versionsNames.map(
|
||||||
|
versionName => `${versionedSidebarsDir}/${versionName}-sidebars.json`,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
const loadedSidebars: Sidebar = loadSidebars(sidebarPaths);
|
||||||
|
const order: Order = createOrder(loadedSidebars);
|
||||||
|
|
||||||
|
await Promise.all(docsPromises);
|
||||||
|
|
||||||
|
// Construct inter-metadata relationship in docsMetadata
|
||||||
const docsMetadata: DocsMetadata = {};
|
const docsMetadata: DocsMetadata = {};
|
||||||
const permalinkToSidebar: PermalinkToSidebar = {};
|
const permalinkToSidebar: PermalinkToSidebar = {};
|
||||||
|
const versionToSidebars: VersionToSidebars = {};
|
||||||
Object.keys(docsMetadataRaw).forEach(currentID => {
|
Object.keys(docsMetadataRaw).forEach(currentID => {
|
||||||
let previous;
|
const {next: nextID, previous: previousID, sidebar} =
|
||||||
let next;
|
order[currentID] || {};
|
||||||
const previousID = idx(docsMetadataRaw, [currentID, 'previous']);
|
const previous = previousID
|
||||||
if (previousID) {
|
? {
|
||||||
previous = {
|
title: docsMetadataRaw[previousID]?.title ?? 'Previous',
|
||||||
title: idx(docsMetadataRaw, [previousID, 'title']) || 'Previous',
|
permalink: docsMetadataRaw[previousID]?.permalink,
|
||||||
permalink: idx(docsMetadataRaw, [previousID, 'permalink']),
|
}
|
||||||
};
|
: undefined;
|
||||||
}
|
const next = nextID
|
||||||
const nextID = idx(docsMetadataRaw, [currentID, 'next']);
|
? {
|
||||||
if (nextID) {
|
title: docsMetadataRaw[nextID]?.title ?? 'Next',
|
||||||
next = {
|
permalink: docsMetadataRaw[nextID]?.permalink,
|
||||||
title: idx(docsMetadataRaw, [nextID, 'title']) || 'Next',
|
}
|
||||||
permalink: idx(docsMetadataRaw, [nextID, 'permalink']),
|
: undefined;
|
||||||
};
|
|
||||||
}
|
|
||||||
docsMetadata[currentID] = {
|
docsMetadata[currentID] = {
|
||||||
...docsMetadataRaw[currentID],
|
...docsMetadataRaw[currentID],
|
||||||
|
sidebar,
|
||||||
previous,
|
previous,
|
||||||
next,
|
next,
|
||||||
};
|
};
|
||||||
|
|
||||||
// sourceToPermalink and permalinkToSidebar mapping
|
// sourceToPermalink and permalinkToSidebar mapping
|
||||||
const {source, permalink, sidebar} = docsMetadataRaw[currentID];
|
const {source, permalink, version} = docsMetadataRaw[currentID];
|
||||||
sourceToPermalink[source] = permalink;
|
sourceToPermalink[source] = permalink;
|
||||||
if (sidebar) {
|
if (sidebar) {
|
||||||
permalinkToSidebar[permalink] = sidebar;
|
permalinkToSidebar[permalink] = sidebar;
|
||||||
|
if (versioning.enabled && version) {
|
||||||
|
if (!versionToSidebars[version]) {
|
||||||
|
versionToSidebars[version] = new Set();
|
||||||
|
}
|
||||||
|
versionToSidebars[version].add(sidebar);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -206,8 +272,8 @@ export default function pluginContentDocs(
|
||||||
docsMetadata,
|
docsMetadata,
|
||||||
docsDir,
|
docsDir,
|
||||||
docsSidebars,
|
docsSidebars,
|
||||||
sourceToPermalink,
|
|
||||||
permalinkToSidebar: objectWithKeySorted(permalinkToSidebar),
|
permalinkToSidebar: objectWithKeySorted(permalinkToSidebar),
|
||||||
|
versionToSidebars,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -221,49 +287,107 @@ export default function pluginContentDocs(
|
||||||
const aliasedSource = (source: string) =>
|
const aliasedSource = (source: string) =>
|
||||||
`@docusaurus-plugin-content-docs/${path.relative(dataDir, source)}`;
|
`@docusaurus-plugin-content-docs/${path.relative(dataDir, source)}`;
|
||||||
|
|
||||||
const routes = await Promise.all(
|
const genRoutes = async (
|
||||||
Object.values(content.docsMetadata).map(async metadataItem => {
|
metadataItems: Metadata[],
|
||||||
const metadataPath = await createData(
|
): Promise<RouteConfig[]> => {
|
||||||
`${docuHash(metadataItem.permalink)}.json`,
|
const routes = await Promise.all(
|
||||||
JSON.stringify(metadataItem, null, 2),
|
metadataItems.map(async metadataItem => {
|
||||||
);
|
const metadataPath = await createData(
|
||||||
return {
|
`${docuHash(metadataItem.permalink)}.json`,
|
||||||
path: metadataItem.permalink,
|
JSON.stringify(metadataItem, null, 2),
|
||||||
component: docItemComponent,
|
);
|
||||||
exact: true,
|
return {
|
||||||
modules: {
|
path: metadataItem.permalink,
|
||||||
content: metadataItem.source,
|
component: docItemComponent,
|
||||||
metadata: aliasedSource(metadataPath),
|
exact: true,
|
||||||
},
|
modules: {
|
||||||
};
|
content: metadataItem.source,
|
||||||
}),
|
metadata: aliasedSource(metadataPath),
|
||||||
);
|
},
|
||||||
|
};
|
||||||
const docsBaseMetadata: DocsBaseMetadata = {
|
}),
|
||||||
docsSidebars: content.docsSidebars,
|
);
|
||||||
permalinkToSidebar: content.permalinkToSidebar,
|
return routes.sort((a, b) =>
|
||||||
|
a.path > b.path ? 1 : b.path > a.path ? -1 : 0,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const docsBaseRoute = normalizeUrl([
|
const addBaseRoute = async (
|
||||||
context.baseUrl,
|
docsBaseRoute: string,
|
||||||
routeBasePath,
|
docsBaseMetadata: DocsBaseMetadata,
|
||||||
':route',
|
routes: RouteConfig[],
|
||||||
]);
|
priority?: number,
|
||||||
const docsBaseMetadataPath = await createData(
|
) => {
|
||||||
`${docuHash(docsBaseRoute)}.json`,
|
const docsBaseMetadataPath = await createData(
|
||||||
JSON.stringify(docsBaseMetadata, null, 2),
|
`${docuHash(docsBaseRoute)}.json`,
|
||||||
);
|
JSON.stringify(docsBaseMetadata, null, 2),
|
||||||
|
);
|
||||||
|
|
||||||
addRoute({
|
addRoute({
|
||||||
path: docsBaseRoute,
|
path: docsBaseRoute,
|
||||||
component: docLayoutComponent,
|
component: docLayoutComponent,
|
||||||
routes: routes.sort((a, b) =>
|
routes,
|
||||||
a.path > b.path ? 1 : b.path > a.path ? -1 : 0,
|
modules: {
|
||||||
),
|
docsMetadata: aliasedSource(docsBaseMetadataPath),
|
||||||
modules: {
|
},
|
||||||
docsMetadata: aliasedSource(docsBaseMetadataPath),
|
priority,
|
||||||
},
|
});
|
||||||
});
|
};
|
||||||
|
|
||||||
|
// 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(
|
||||||
|
Object.values(content.docsMetadata),
|
||||||
|
'version',
|
||||||
|
);
|
||||||
|
await Promise.all(
|
||||||
|
Object.keys(docsMetadataByVersion).map(async version => {
|
||||||
|
const routes: RouteConfig[] = await genRoutes(
|
||||||
|
docsMetadataByVersion[version],
|
||||||
|
);
|
||||||
|
|
||||||
|
const isLatestVersion = version === versioning.latestVersion;
|
||||||
|
const docsBasePermalink = normalizeUrl([
|
||||||
|
baseUrl,
|
||||||
|
routeBasePath,
|
||||||
|
isLatestVersion ? '' : version,
|
||||||
|
]);
|
||||||
|
const docsBaseRoute = normalizeUrl([docsBasePermalink, ':route']);
|
||||||
|
const neededSidebars: Set<string> =
|
||||||
|
content.versionToSidebars[version] || new Set();
|
||||||
|
const docsBaseMetadata: DocsBaseMetadata = {
|
||||||
|
docsSidebars: _.pick(
|
||||||
|
content.docsSidebars,
|
||||||
|
Array.from(neededSidebars),
|
||||||
|
),
|
||||||
|
permalinkToSidebar: _.pickBy(
|
||||||
|
content.permalinkToSidebar,
|
||||||
|
sidebar => neededSidebars.has(sidebar),
|
||||||
|
),
|
||||||
|
version,
|
||||||
|
};
|
||||||
|
|
||||||
|
// We want latest version route config to be placed last in the generated routeconfig.
|
||||||
|
// Otherwise, `/docs/next/foo` will match `/docs/:route` instead of `/docs/next/:route`
|
||||||
|
return addBaseRoute(
|
||||||
|
docsBaseRoute,
|
||||||
|
docsBaseMetadata,
|
||||||
|
routes,
|
||||||
|
isLatestVersion ? -1 : undefined,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const routes = await genRoutes(Object.values(content.docsMetadata));
|
||||||
|
const docsBaseMetadata: DocsBaseMetadata = {
|
||||||
|
docsSidebars: content.docsSidebars,
|
||||||
|
permalinkToSidebar: content.permalinkToSidebar,
|
||||||
|
};
|
||||||
|
|
||||||
|
const docsBaseRoute = normalizeUrl([baseUrl, routeBasePath, ':route']);
|
||||||
|
return addBaseRoute(docsBaseRoute, docsBaseMetadata, routes);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
configureWebpack(_config, isServer, utils) {
|
configureWebpack(_config, isServer, utils) {
|
||||||
|
@ -279,7 +403,7 @@ export default function pluginContentDocs(
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
test: /(\.mdx?)$/,
|
test: /(\.mdx?)$/,
|
||||||
include: [contentPath],
|
include: [docsDir, versionedDir].filter(Boolean),
|
||||||
use: [
|
use: [
|
||||||
getCacheLoader(isServer),
|
getCacheLoader(isServer),
|
||||||
getBabelLoader(isServer),
|
getBabelLoader(isServer),
|
||||||
|
@ -293,9 +417,10 @@ export default function pluginContentDocs(
|
||||||
{
|
{
|
||||||
loader: path.resolve(__dirname, './markdown/index.js'),
|
loader: path.resolve(__dirname, './markdown/index.js'),
|
||||||
options: {
|
options: {
|
||||||
siteDir: context.siteDir,
|
siteDir,
|
||||||
docsDir: contentPath,
|
docsDir,
|
||||||
sourceToPermalink: sourceToPermalink,
|
sourceToPermalink: sourceToPermalink,
|
||||||
|
versionedDir,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
].filter(Boolean),
|
].filter(Boolean),
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
### Existing Docs
|
||||||
|
|
||||||
|
- [doc1](subdir/doc1.md)
|
||||||
|
|
||||||
|
### With hash
|
||||||
|
- [doc2](doc2.md#existing-docs)
|
|
@ -0,0 +1,2 @@
|
||||||
|
### Relative linking
|
||||||
|
- [doc1](../doc2.md)
|
|
@ -34,6 +34,15 @@ exports[`transform to correct links 1`] = `
|
||||||
- [doc2](/docs/doc2)"
|
- [doc2](/docs/doc2)"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`transforms absolute links in versioned docs 1`] = `
|
||||||
|
"### Existing Docs
|
||||||
|
|
||||||
|
- [doc1](/docs/1.0.0/subdir/doc1)
|
||||||
|
|
||||||
|
### With hash
|
||||||
|
- [doc2](/docs/1.0.0/doc2#existing-docs)"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`transforms reference links 1`] = `
|
exports[`transforms reference links 1`] = `
|
||||||
"### Existing Docs
|
"### Existing Docs
|
||||||
|
|
||||||
|
@ -55,3 +64,9 @@ exports[`transforms reference links 1`] = `
|
||||||
[image1]: assets/image1.png
|
[image1]: assets/image1.png
|
||||||
"
|
"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`transforms relative links in versioned docs 1`] = `
|
||||||
|
"### Relative linking
|
||||||
|
- [doc1](/docs/1.0.0/doc2)
|
||||||
|
"
|
||||||
|
`;
|
||||||
|
|
|
@ -9,14 +9,19 @@ import fs from 'fs-extra';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import linkify from '../linkify';
|
import linkify from '../linkify';
|
||||||
import {SourceToPermalink} from '../../types';
|
import {SourceToPermalink} from '../../types';
|
||||||
|
import {VERSIONED_DOCS_DIR} from '../../constants';
|
||||||
|
|
||||||
const siteDir = path.join(__dirname, '__fixtures__');
|
const siteDir = path.join(__dirname, '__fixtures__');
|
||||||
const docsDir = path.join(siteDir, 'docs');
|
const docsDir = path.join(siteDir, 'docs');
|
||||||
|
const versionedDir = path.join(siteDir, VERSIONED_DOCS_DIR);
|
||||||
const sourceToPermalink: SourceToPermalink = {
|
const sourceToPermalink: SourceToPermalink = {
|
||||||
'@site/docs/doc1.md': '/docs/doc1',
|
'@site/docs/doc1.md': '/docs/doc1',
|
||||||
'@site/docs/doc2.md': '/docs/doc2',
|
'@site/docs/doc2.md': '/docs/doc2',
|
||||||
'@site/docs/subdir/doc3.md': '/docs/subdir/doc3',
|
'@site/docs/subdir/doc3.md': '/docs/subdir/doc3',
|
||||||
'@site/docs/doc4.md': '/docs/doc4',
|
'@site/docs/doc4.md': '/docs/doc4',
|
||||||
|
'@site/versioned_docs/version-1.0.0/doc2.md': '/docs/1.0.0/doc2',
|
||||||
|
'@site/versioned_docs/version-1.0.0/subdir/doc1.md':
|
||||||
|
'/docs/1.0.0/subdir/doc1',
|
||||||
};
|
};
|
||||||
|
|
||||||
const transform = filepath => {
|
const transform = filepath => {
|
||||||
|
@ -27,6 +32,7 @@ const transform = filepath => {
|
||||||
docsDir,
|
docsDir,
|
||||||
siteDir,
|
siteDir,
|
||||||
sourceToPermalink,
|
sourceToPermalink,
|
||||||
|
versionedDir,
|
||||||
);
|
);
|
||||||
return [content, transformedContent];
|
return [content, transformedContent];
|
||||||
};
|
};
|
||||||
|
@ -70,3 +76,23 @@ test('transforms reference links', () => {
|
||||||
expect(transformedContent).not.toContain('[doc2]: ./doc2.md');
|
expect(transformedContent).not.toContain('[doc2]: ./doc2.md');
|
||||||
expect(content).not.toEqual(transformedContent);
|
expect(content).not.toEqual(transformedContent);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('transforms absolute links in versioned docs', () => {
|
||||||
|
const doc2 = path.join(versionedDir, 'version-1.0.0', 'doc2.md');
|
||||||
|
const [content, transformedContent] = transform(doc2);
|
||||||
|
expect(transformedContent).toMatchSnapshot();
|
||||||
|
expect(transformedContent).toContain('](/docs/1.0.0/subdir/doc1');
|
||||||
|
expect(transformedContent).toContain('](/docs/1.0.0/doc2#existing-docs');
|
||||||
|
expect(transformedContent).not.toContain('](subdir/doc1.md)');
|
||||||
|
expect(transformedContent).not.toContain('](doc2.md#existing-docs)');
|
||||||
|
expect(content).not.toEqual(transformedContent);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('transforms relative links in versioned docs', () => {
|
||||||
|
const doc1 = path.join(versionedDir, 'version-1.0.0', 'subdir', 'doc1.md');
|
||||||
|
const [content, transformedContent] = transform(doc1);
|
||||||
|
expect(transformedContent).toMatchSnapshot();
|
||||||
|
expect(transformedContent).toContain('](/docs/1.0.0/doc2');
|
||||||
|
expect(transformedContent).not.toContain('](../doc2.md)');
|
||||||
|
expect(content).not.toEqual(transformedContent);
|
||||||
|
});
|
||||||
|
|
|
@ -11,7 +11,7 @@ import linkify from './linkify';
|
||||||
|
|
||||||
export = function(fileString: string) {
|
export = function(fileString: string) {
|
||||||
const callback = this.async();
|
const callback = this.async();
|
||||||
const {docsDir, siteDir, sourceToPermalink} = getOptions(this);
|
const {docsDir, siteDir, versionedDir, sourceToPermalink} = getOptions(this);
|
||||||
return (
|
return (
|
||||||
callback &&
|
callback &&
|
||||||
callback(
|
callback(
|
||||||
|
@ -22,6 +22,7 @@ export = function(fileString: string) {
|
||||||
docsDir,
|
docsDir,
|
||||||
siteDir,
|
siteDir,
|
||||||
sourceToPermalink,
|
sourceToPermalink,
|
||||||
|
versionedDir,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import {resolve} from 'url';
|
import {resolve} from 'url';
|
||||||
|
import {getSubFolder} from '@docusaurus/utils';
|
||||||
import {SourceToPermalink} from '../types';
|
import {SourceToPermalink} from '../types';
|
||||||
|
|
||||||
export default function(
|
export default function(
|
||||||
|
@ -15,12 +16,19 @@ export default function(
|
||||||
docsDir: string,
|
docsDir: string,
|
||||||
siteDir: string,
|
siteDir: string,
|
||||||
sourceToPermalink: SourceToPermalink,
|
sourceToPermalink: SourceToPermalink,
|
||||||
|
versionedDir?: string,
|
||||||
) {
|
) {
|
||||||
// Determine the source dir. e.g: /website/docs, /website/versioned_docs/version-1.0.0
|
// Determine the source dir. e.g: /website/docs, /website/versioned_docs/version-1.0.0
|
||||||
let sourceDir: string | undefined;
|
let sourceDir: string | undefined;
|
||||||
const thisSource = filePath;
|
const thisSource = filePath;
|
||||||
if (thisSource.startsWith(docsDir)) {
|
if (thisSource.startsWith(docsDir)) {
|
||||||
sourceDir = 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let content = fileString;
|
let content = fileString;
|
||||||
|
|
|
@ -8,109 +8,24 @@
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import {parse, normalizeUrl, posixPath} from '@docusaurus/utils';
|
import {parse, normalizeUrl, posixPath} from '@docusaurus/utils';
|
||||||
import {DocusaurusConfig} from '@docusaurus/types';
|
import {LoadContext} from '@docusaurus/types';
|
||||||
|
|
||||||
import lastUpdate from './lastUpdate';
|
import lastUpdate from './lastUpdate';
|
||||||
import {Order, MetadataRaw} from './types';
|
import {MetadataRaw, LastUpdateData, MetadataOptions, Env} from './types';
|
||||||
|
|
||||||
type Args = {
|
type Args = {
|
||||||
source: string;
|
source: string;
|
||||||
docsDir: string;
|
refDir: string;
|
||||||
order: Order;
|
context: LoadContext;
|
||||||
siteConfig: Partial<DocusaurusConfig>;
|
options: MetadataOptions;
|
||||||
docsBasePath: string;
|
env: Env;
|
||||||
siteDir: string;
|
|
||||||
editUrl?: string;
|
|
||||||
showLastUpdateAuthor?: boolean;
|
|
||||||
showLastUpdateTime?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function processMetadata({
|
async function lastUpdated(
|
||||||
source,
|
filePath: string,
|
||||||
docsDir,
|
options: MetadataOptions,
|
||||||
order,
|
): Promise<LastUpdateData> {
|
||||||
siteConfig,
|
const {showLastUpdateAuthor, showLastUpdateTime} = options;
|
||||||
docsBasePath,
|
|
||||||
siteDir,
|
|
||||||
editUrl,
|
|
||||||
showLastUpdateAuthor,
|
|
||||||
showLastUpdateTime,
|
|
||||||
}: Args): Promise<MetadataRaw> {
|
|
||||||
const filePath = path.join(docsDir, source);
|
|
||||||
|
|
||||||
const fileString = await fs.readFile(filePath, 'utf-8');
|
|
||||||
const {frontMatter: metadata = {}, excerpt} = parse(fileString);
|
|
||||||
|
|
||||||
// Default id is the file name.
|
|
||||||
if (!metadata.id) {
|
|
||||||
metadata.id = path.basename(source, path.extname(source));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (metadata.id.includes('/')) {
|
|
||||||
throw new Error('Document id cannot include "/".');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default title is the id.
|
|
||||||
if (!metadata.title) {
|
|
||||||
metadata.title = metadata.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!metadata.description) {
|
|
||||||
metadata.description = excerpt;
|
|
||||||
}
|
|
||||||
|
|
||||||
const dirName = path.dirname(source);
|
|
||||||
if (dirName !== '.') {
|
|
||||||
const prefix = dirName;
|
|
||||||
if (prefix) {
|
|
||||||
metadata.id = `${prefix}/${metadata.id}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cannot use path.join() as it resolves '../' and removes the '@site'. Let webpack loader resolve it.
|
|
||||||
const aliasedPath = `@site/${path.relative(siteDir, filePath)}`;
|
|
||||||
metadata.source = aliasedPath;
|
|
||||||
|
|
||||||
// Build the permalink.
|
|
||||||
const {baseUrl} = siteConfig;
|
|
||||||
|
|
||||||
// If user has own custom permalink defined in frontmatter
|
|
||||||
// e.g: :baseUrl:docsUrl/:langPart/:versionPart/endiliey/:id
|
|
||||||
if (metadata.permalink) {
|
|
||||||
metadata.permalink = path.resolve(
|
|
||||||
metadata.permalink
|
|
||||||
.replace(/:baseUrl/, baseUrl)
|
|
||||||
.replace(/:docsUrl/, docsBasePath)
|
|
||||||
.replace(/:id/, metadata.id),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
metadata.permalink = normalizeUrl([baseUrl, docsBasePath, metadata.id]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine order.
|
|
||||||
const {id} = metadata;
|
|
||||||
if (order[id]) {
|
|
||||||
metadata.sidebar = order[id].sidebar;
|
|
||||||
if (order[id].next) {
|
|
||||||
metadata.next = order[id].next;
|
|
||||||
}
|
|
||||||
if (order[id].previous) {
|
|
||||||
metadata.previous = order[id].previous;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (editUrl) {
|
|
||||||
metadata.editUrl = normalizeUrl([
|
|
||||||
editUrl,
|
|
||||||
posixPath(path.relative(siteDir, filePath)),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (metadata.custom_edit_url) {
|
|
||||||
metadata.editUrl = metadata.custom_edit_url;
|
|
||||||
delete metadata.custom_edit_url;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showLastUpdateAuthor || showLastUpdateTime) {
|
if (showLastUpdateAuthor || showLastUpdateTime) {
|
||||||
// Use fake data in dev for faster development
|
// Use fake data in dev for faster development
|
||||||
const fileLastUpdateData =
|
const fileLastUpdateData =
|
||||||
|
@ -118,20 +33,113 @@ export default async function processMetadata({
|
||||||
? await lastUpdate(filePath)
|
? await lastUpdate(filePath)
|
||||||
: {
|
: {
|
||||||
author: 'Author',
|
author: 'Author',
|
||||||
timestamp: '1539502055',
|
timestamp: 1539502055,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (fileLastUpdateData) {
|
if (fileLastUpdateData) {
|
||||||
const {author, timestamp} = fileLastUpdateData;
|
const {author, timestamp} = fileLastUpdateData;
|
||||||
if (showLastUpdateAuthor && author) {
|
return {
|
||||||
metadata.lastUpdatedBy = author;
|
lastUpdatedAt: showLastUpdateTime ? timestamp : undefined,
|
||||||
}
|
lastUpdatedBy: showLastUpdateAuthor ? author : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
if (showLastUpdateTime && timestamp) {
|
export default async function processMetadata({
|
||||||
metadata.lastUpdatedAt = timestamp;
|
source,
|
||||||
|
refDir,
|
||||||
|
context,
|
||||||
|
options,
|
||||||
|
env,
|
||||||
|
}: Args): Promise<MetadataRaw> {
|
||||||
|
const {routeBasePath, editUrl} = options;
|
||||||
|
const {siteDir, baseUrl} = context;
|
||||||
|
const {versioning} = env;
|
||||||
|
const filePath = path.join(refDir, source);
|
||||||
|
|
||||||
|
const fileString = await fs.readFile(filePath, 'utf-8');
|
||||||
|
const {frontMatter = {}, excerpt} = parse(fileString);
|
||||||
|
|
||||||
|
// Default 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 "/".');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default title is the id.
|
||||||
|
const title: string = frontMatter.title || baseID;
|
||||||
|
|
||||||
|
const description: string = frontMatter.description || excerpt;
|
||||||
|
|
||||||
|
let version;
|
||||||
|
let id = baseID;
|
||||||
|
|
||||||
|
// Append subdirectory as part of id.
|
||||||
|
const dirName = path.dirname(source);
|
||||||
|
if (dirName !== '.') {
|
||||||
|
id = `${dirName}/${baseID}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (versioning.enabled) {
|
||||||
|
if (/^version-/.test(dirName)) {
|
||||||
|
const inferredVersion = dirName
|
||||||
|
.split('/', 1)
|
||||||
|
.shift()!
|
||||||
|
.replace(/^version-/, '');
|
||||||
|
if (inferredVersion && versioning.versions.includes(inferredVersion)) {
|
||||||
|
version = inferredVersion;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
version = 'next';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return metadata as MetadataRaw;
|
// The version portion of the url path. Eg: 'next', '1.0.0', and ''
|
||||||
|
const versionPath =
|
||||||
|
version && version !== versioning.latestVersion ? version : '';
|
||||||
|
|
||||||
|
// The last portion of the url path. Eg: 'foo/bar', 'bar'
|
||||||
|
const routePath =
|
||||||
|
version && version !== 'next'
|
||||||
|
? id.replace(new RegExp(`^version-${version}/`), '')
|
||||||
|
: id;
|
||||||
|
const permalink = normalizeUrl([
|
||||||
|
baseUrl,
|
||||||
|
routeBasePath,
|
||||||
|
versionPath,
|
||||||
|
routePath,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const {sidebar_label, custom_edit_url} = frontMatter;
|
||||||
|
|
||||||
|
const relativePath = path.relative(siteDir, filePath);
|
||||||
|
|
||||||
|
// Cannot use path.join() as it resolves '../' and removes the '@site'. Let webpack loader resolve it.
|
||||||
|
const aliasedPath = `@site/${relativePath}`;
|
||||||
|
|
||||||
|
const docsEditUrl = editUrl
|
||||||
|
? normalizeUrl([editUrl, posixPath(relativePath)])
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const {lastUpdatedAt, lastUpdatedBy} = await lastUpdated(filePath, options);
|
||||||
|
|
||||||
|
// 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 = {
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
source: aliasedPath,
|
||||||
|
permalink,
|
||||||
|
editUrl: custom_edit_url || docsEditUrl,
|
||||||
|
version,
|
||||||
|
lastUpdatedBy,
|
||||||
|
lastUpdatedAt,
|
||||||
|
sidebar_label,
|
||||||
|
};
|
||||||
|
|
||||||
|
return metadata;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,13 +5,7 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {Sidebar, SidebarItem, Order} from './types';
|
||||||
Sidebar,
|
|
||||||
SidebarItem,
|
|
||||||
SidebarItemDoc,
|
|
||||||
SidebarItemCategory,
|
|
||||||
Order,
|
|
||||||
} from './types';
|
|
||||||
|
|
||||||
// Build the docs meta such as next, previous, category and sidebar.
|
// Build the docs meta such as next, previous, category and sidebar.
|
||||||
export default function createOrder(allSidebars: Sidebar = {}): Order {
|
export default function createOrder(allSidebars: Sidebar = {}): Order {
|
||||||
|
@ -26,7 +20,7 @@ export default function createOrder(allSidebars: Sidebar = {}): Order {
|
||||||
switch (item.type) {
|
switch (item.type) {
|
||||||
case 'category':
|
case 'category':
|
||||||
indexItems({
|
indexItems({
|
||||||
items: (item as SidebarItemCategory).items,
|
items: item.items,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'ref':
|
case 'ref':
|
||||||
|
@ -34,12 +28,8 @@ export default function createOrder(allSidebars: Sidebar = {}): Order {
|
||||||
// Refs and links should not be shown in navigation.
|
// Refs and links should not be shown in navigation.
|
||||||
break;
|
break;
|
||||||
case 'doc':
|
case 'doc':
|
||||||
ids.push((item as SidebarItemDoc).id);
|
ids.push(item.id);
|
||||||
break;
|
break;
|
||||||
default:
|
|
||||||
throw new Error(
|
|
||||||
`Unknown item type: ${item.type}. Item: ${JSON.stringify(item)}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import fs from 'fs-extra';
|
||||||
import importFresh from 'import-fresh';
|
import importFresh from 'import-fresh';
|
||||||
import {
|
import {
|
||||||
SidebarItemCategory,
|
SidebarItemCategory,
|
||||||
|
@ -108,11 +109,18 @@ function normalizeSidebar(sidebars: SidebarRaw): Sidebar {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function loadSidebars(sidebarPath: string): Sidebar {
|
export default function loadSidebars(sidebarPaths?: string[]): Sidebar {
|
||||||
// We don't want sidebars to be cached because of hotreloading.
|
// We don't want sidebars to be cached because of hotreloading.
|
||||||
let allSidebars: SidebarRaw = {};
|
let allSidebars: SidebarRaw = {};
|
||||||
if (sidebarPath) {
|
if (!sidebarPaths || !sidebarPaths.length) {
|
||||||
allSidebars = importFresh(sidebarPath) as SidebarRaw;
|
return {} as Sidebar;
|
||||||
}
|
}
|
||||||
|
sidebarPaths.map(sidebarPath => {
|
||||||
|
if (sidebarPath && fs.existsSync(sidebarPath)) {
|
||||||
|
const sidebar = importFresh(sidebarPath) as SidebarRaw;
|
||||||
|
Object.assign(allSidebars, sidebar);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return normalizeSidebar(allSidebars);
|
return normalizeSidebar(allSidebars);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,39 +5,45 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export interface PluginOptions {
|
export interface MetadataOptions {
|
||||||
path: string;
|
|
||||||
routeBasePath: string;
|
routeBasePath: string;
|
||||||
include: string[];
|
|
||||||
sidebarPath: string;
|
|
||||||
docLayoutComponent: string;
|
|
||||||
docItemComponent: string;
|
|
||||||
remarkPlugins: string[];
|
|
||||||
rehypePlugins: string[];
|
|
||||||
editUrl?: string;
|
editUrl?: string;
|
||||||
showLastUpdateTime?: boolean;
|
showLastUpdateTime?: boolean;
|
||||||
showLastUpdateAuthor?: boolean;
|
showLastUpdateAuthor?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PathOptions {
|
||||||
|
path: string;
|
||||||
|
sidebarPath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PluginOptions extends MetadataOptions, PathOptions {
|
||||||
|
include: string[];
|
||||||
|
docLayoutComponent: string;
|
||||||
|
docItemComponent: string;
|
||||||
|
remarkPlugins: string[];
|
||||||
|
rehypePlugins: string[];
|
||||||
|
}
|
||||||
|
|
||||||
export type SidebarItemDoc = {
|
export type SidebarItemDoc = {
|
||||||
type: string;
|
type: 'doc' | 'ref';
|
||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface SidebarItemLink {
|
export interface SidebarItemLink {
|
||||||
type: string;
|
type: 'link';
|
||||||
href: string;
|
href: string;
|
||||||
label: string;
|
label: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SidebarItemCategory {
|
export interface SidebarItemCategory {
|
||||||
type: string;
|
type: 'category';
|
||||||
label: string;
|
label: string;
|
||||||
items: SidebarItem[];
|
items: SidebarItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SidebarItemCategoryRaw {
|
export interface SidebarItemCategoryRaw {
|
||||||
type: string;
|
type: 'category';
|
||||||
label: string;
|
label: string;
|
||||||
items: SidebarItemRaw[];
|
items: SidebarItemRaw[];
|
||||||
}
|
}
|
||||||
|
@ -51,7 +57,11 @@ export type SidebarItemRaw =
|
||||||
| string
|
| string
|
||||||
| SidebarItemDoc
|
| SidebarItemDoc
|
||||||
| SidebarItemLink
|
| SidebarItemLink
|
||||||
| SidebarItemCategoryRaw;
|
| SidebarItemCategoryRaw
|
||||||
|
| {
|
||||||
|
type: string;
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
|
||||||
// Sidebar given by user that is not normalized yet. e.g: sidebars.json
|
// Sidebar given by user that is not normalized yet. e.g: sidebars.json
|
||||||
export interface SidebarRaw {
|
export interface SidebarRaw {
|
||||||
|
@ -65,7 +75,7 @@ export interface Sidebar {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DocsSidebarItemCategory {
|
export interface DocsSidebarItemCategory {
|
||||||
type: string;
|
type: 'category';
|
||||||
label: string;
|
label: string;
|
||||||
items: (SidebarItemLink | DocsSidebarItemCategory)[];
|
items: (SidebarItemLink | DocsSidebarItemCategory)[];
|
||||||
}
|
}
|
||||||
|
@ -84,7 +94,12 @@ export interface Order {
|
||||||
[id: string]: OrderMetadata;
|
[id: string]: OrderMetadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MetadataRaw extends OrderMetadata {
|
export interface LastUpdateData {
|
||||||
|
lastUpdatedAt?: number;
|
||||||
|
lastUpdatedBy?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MetadataRaw extends LastUpdateData {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
@ -92,9 +107,7 @@ export interface MetadataRaw extends OrderMetadata {
|
||||||
permalink: string;
|
permalink: string;
|
||||||
sidebar_label?: string;
|
sidebar_label?: string;
|
||||||
editUrl?: string;
|
editUrl?: string;
|
||||||
lastUpdatedAt?: number;
|
version?: string;
|
||||||
lastUpdatedBy?: string;
|
|
||||||
[key: string]: any;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Paginator {
|
export interface Paginator {
|
||||||
|
@ -102,7 +115,8 @@ export interface Paginator {
|
||||||
permalink: string;
|
permalink: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Metadata extends Omit<MetadataRaw, 'previous' | 'next'> {
|
export interface Metadata extends MetadataRaw {
|
||||||
|
sidebar?: string;
|
||||||
previous?: Paginator;
|
previous?: Paginator;
|
||||||
next?: Paginator;
|
next?: Paginator;
|
||||||
}
|
}
|
||||||
|
@ -111,6 +125,10 @@ export interface DocsMetadata {
|
||||||
[id: string]: Metadata;
|
[id: string]: Metadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DocsMetadataRaw {
|
||||||
|
[id: string]: MetadataRaw;
|
||||||
|
}
|
||||||
|
|
||||||
export interface SourceToPermalink {
|
export interface SourceToPermalink {
|
||||||
[source: string]: string;
|
[source: string]: string;
|
||||||
}
|
}
|
||||||
|
@ -119,15 +137,34 @@ export interface PermalinkToSidebar {
|
||||||
[permalink: string]: string;
|
[permalink: string]: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface VersionToSidebars {
|
||||||
|
[version: string]: Set<string>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface LoadedContent {
|
export interface LoadedContent {
|
||||||
docsMetadata: DocsMetadata;
|
docsMetadata: DocsMetadata;
|
||||||
docsDir: string;
|
docsDir: string;
|
||||||
docsSidebars: Sidebar;
|
docsSidebars: DocsSidebar;
|
||||||
sourceToPermalink: SourceToPermalink;
|
|
||||||
permalinkToSidebar: PermalinkToSidebar;
|
permalinkToSidebar: PermalinkToSidebar;
|
||||||
|
versionToSidebars: VersionToSidebars;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DocsBaseMetadata = Pick<
|
export type DocsBaseMetadata = Pick<
|
||||||
LoadedContent,
|
LoadedContent,
|
||||||
'docsSidebars' | 'permalinkToSidebar'
|
'docsSidebars' | 'permalinkToSidebar'
|
||||||
>;
|
> & {
|
||||||
|
version?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type VersioningEnv = {
|
||||||
|
enabled: boolean;
|
||||||
|
latestVersion: string | null;
|
||||||
|
versions: string[];
|
||||||
|
docsDir: string;
|
||||||
|
sidebarsDir: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface Env {
|
||||||
|
versioning: VersioningEnv;
|
||||||
|
// TODO: translation
|
||||||
|
}
|
||||||
|
|
134
packages/docusaurus-plugin-content-docs/src/version.ts
Normal file
134
packages/docusaurus-plugin-content-docs/src/version.ts
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2017-present, Facebook, Inc.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
getVersionsJSONFile,
|
||||||
|
getVersionedDocsDir,
|
||||||
|
getVersionedSidebarsDir,
|
||||||
|
} from './env';
|
||||||
|
import fs from 'fs-extra';
|
||||||
|
import path from 'path';
|
||||||
|
import {Sidebar, SidebarItemCategory, PathOptions} from './types';
|
||||||
|
import loadSidebars from './sidebars';
|
||||||
|
|
||||||
|
export function docsVersion(
|
||||||
|
version: string | null | undefined,
|
||||||
|
siteDir: string,
|
||||||
|
options: PathOptions,
|
||||||
|
) {
|
||||||
|
if (!version) {
|
||||||
|
throw new Error(
|
||||||
|
'No version tag specified!. Pass the version you wish to create as an argument. Ex: 1.0.0',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (version.includes('/') || version.includes('\\')) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid version tag specified! Do not include slash (/) or (\\). Try something like: 1.0.0`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (version.length > 32) {
|
||||||
|
throw new Error(
|
||||||
|
'Invalid version tag specified! Length must <= 32 characters. Try something like: 1.0.0',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since we are going to create `version-${version}` folder, we need to make sure its a valid path name
|
||||||
|
if (/[<>:"\/\\|?*\x00-\x1F]/g.test(version)) {
|
||||||
|
throw new Error(
|
||||||
|
'Invalid version tag specified! Please ensure its a valid pathname too. Try something like: 1.0.0',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/^\.\.?$/.test(version)) {
|
||||||
|
throw new Error(
|
||||||
|
'Invalid version tag specified! Do not name your version "." or "..". Try something like: 1.0.0',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load existing versions
|
||||||
|
let versions = [];
|
||||||
|
const versionsJSONFile = getVersionsJSONFile(siteDir);
|
||||||
|
if (fs.existsSync(versionsJSONFile)) {
|
||||||
|
versions = JSON.parse(fs.readFileSync(versionsJSONFile, 'utf8'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if version already exist
|
||||||
|
if (versions.includes(version)) {
|
||||||
|
throw new Error(
|
||||||
|
'This version already exists!. Use a version tag that does not already exist.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const {path: docsPath, sidebarPath} = options;
|
||||||
|
|
||||||
|
// Copy docs files
|
||||||
|
const docsDir = path.join(siteDir, docsPath);
|
||||||
|
if (fs.existsSync(docsDir) && fs.readdirSync(docsDir).length > 0) {
|
||||||
|
const versionedDir = getVersionedDocsDir(siteDir);
|
||||||
|
const newVersionDir = path.join(versionedDir, `version-${version}`);
|
||||||
|
fs.copySync(docsDir, newVersionDir);
|
||||||
|
} else {
|
||||||
|
throw new Error('There is no docs to version !');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load current sidebar and create a new versioned sidebars file
|
||||||
|
if (fs.existsSync(sidebarPath)) {
|
||||||
|
const loadedSidebars: Sidebar = loadSidebars([sidebarPath]);
|
||||||
|
|
||||||
|
// Transform id in original sidebar to versioned id
|
||||||
|
const normalizeCategory = (
|
||||||
|
category: SidebarItemCategory,
|
||||||
|
): SidebarItemCategory => {
|
||||||
|
const items = category.items.map(item => {
|
||||||
|
switch (item.type) {
|
||||||
|
case 'category':
|
||||||
|
return normalizeCategory(item);
|
||||||
|
case 'ref':
|
||||||
|
case 'doc':
|
||||||
|
return {
|
||||||
|
type: item.type,
|
||||||
|
id: `version-${version}/${item.id}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
return {...category, items};
|
||||||
|
};
|
||||||
|
|
||||||
|
const versionedSidebar: Sidebar = Object.entries(loadedSidebars).reduce(
|
||||||
|
(acc: Sidebar, [sidebarId, sidebarItemCategories]) => {
|
||||||
|
const newVersionedSidebarId = `version-${version}/${sidebarId}`;
|
||||||
|
acc[
|
||||||
|
newVersionedSidebarId
|
||||||
|
] = sidebarItemCategories.map(sidebarItemCategory =>
|
||||||
|
normalizeCategory(sidebarItemCategory),
|
||||||
|
);
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
const versionedSidebarsDir = getVersionedSidebarsDir(siteDir);
|
||||||
|
const newSidebarFile = path.join(
|
||||||
|
versionedSidebarsDir,
|
||||||
|
`version-${version}-sidebars.json`,
|
||||||
|
);
|
||||||
|
fs.ensureDirSync(path.dirname(newSidebarFile));
|
||||||
|
fs.writeFileSync(
|
||||||
|
newSidebarFile,
|
||||||
|
`${JSON.stringify(versionedSidebar, null, 2)}\n`,
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// update versions.json file
|
||||||
|
versions.unshift(version);
|
||||||
|
fs.ensureDirSync(path.dirname(versionsJSONFile));
|
||||||
|
fs.writeFileSync(versionsJSONFile, `${JSON.stringify(versions, null, 2)}\n`);
|
||||||
|
|
||||||
|
console.log(`Version ${version} created!`);
|
||||||
|
}
|
|
@ -62,6 +62,7 @@ function DocItem(props) {
|
||||||
lastUpdatedAt,
|
lastUpdatedAt,
|
||||||
lastUpdatedBy,
|
lastUpdatedBy,
|
||||||
keywords,
|
keywords,
|
||||||
|
version,
|
||||||
} = metadata;
|
} = metadata;
|
||||||
const {
|
const {
|
||||||
frontMatter: {
|
frontMatter: {
|
||||||
|
@ -96,6 +97,13 @@ function DocItem(props) {
|
||||||
<div className="col">
|
<div className="col">
|
||||||
<div className={styles.docItemContainer}>
|
<div className={styles.docItemContainer}>
|
||||||
<article>
|
<article>
|
||||||
|
{version && (
|
||||||
|
<span
|
||||||
|
style={{verticalAlign: 'top'}}
|
||||||
|
className="badge badge--info">
|
||||||
|
Version: {version}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{!hideTitle && (
|
{!hideTitle && (
|
||||||
<header>
|
<header>
|
||||||
<h1 className={styles.docTitle}>{metadata.title}</h1>
|
<h1 className={styles.docTitle}>{metadata.title}</h1>
|
||||||
|
|
|
@ -24,7 +24,7 @@ function matchingRouteExist(routes, pathname) {
|
||||||
|
|
||||||
function DocPage(props) {
|
function DocPage(props) {
|
||||||
const {route, docsMetadata, location} = props;
|
const {route, docsMetadata, location} = props;
|
||||||
const {permalinkToSidebar, docsSidebars} = docsMetadata;
|
const {permalinkToSidebar, docsSidebars, version} = docsMetadata;
|
||||||
const sidebar = permalinkToSidebar[location.pathname.replace(/\/$/, '')];
|
const sidebar = permalinkToSidebar[location.pathname.replace(/\/$/, '')];
|
||||||
const {siteConfig: {themeConfig = {}} = {}} = useDocusaurusContext();
|
const {siteConfig: {themeConfig = {}} = {}} = useDocusaurusContext();
|
||||||
const {sidebarCollapsible = true} = themeConfig;
|
const {sidebarCollapsible = true} = themeConfig;
|
||||||
|
@ -34,7 +34,7 @@ function DocPage(props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout version={version}>
|
||||||
<div className={styles.docPage}>
|
<div className={styles.docPage}>
|
||||||
{sidebar && (
|
{sidebar && (
|
||||||
<div className={styles.docSidebarContainer}>
|
<div className={styles.docSidebarContainer}>
|
||||||
|
|
|
@ -31,6 +31,7 @@ function Layout(props) {
|
||||||
image,
|
image,
|
||||||
keywords,
|
keywords,
|
||||||
permalink,
|
permalink,
|
||||||
|
version,
|
||||||
} = props;
|
} = props;
|
||||||
const metaTitle = title || `${defaultTitle} · ${tagline}`;
|
const metaTitle = title || `${defaultTitle} · ${tagline}`;
|
||||||
const metaImage = image || defaultImage;
|
const metaImage = image || defaultImage;
|
||||||
|
@ -47,6 +48,7 @@ function Layout(props) {
|
||||||
{description && (
|
{description && (
|
||||||
<meta property="og:description" content={description} />
|
<meta property="og:description" content={description} />
|
||||||
)}
|
)}
|
||||||
|
{version && <meta name="docsearch:version" content={version} />}
|
||||||
{keywords && keywords.length && (
|
{keywords && keywords.length && (
|
||||||
<meta name="keywords" content={keywords.join(',')} />
|
<meta name="keywords" content={keywords.join(',')} />
|
||||||
)}
|
)}
|
||||||
|
|
5
packages/docusaurus-types/src/index.d.ts
vendored
5
packages/docusaurus-types/src/index.d.ts
vendored
|
@ -1,5 +1,5 @@
|
||||||
import {Loader, Configuration} from 'webpack';
|
import {Loader, Configuration} from 'webpack';
|
||||||
import {CommanderStatic} from 'commander';
|
import {Command} from 'commander';
|
||||||
import {ParsedUrlQueryInput} from 'querystring';
|
import {ParsedUrlQueryInput} from 'querystring';
|
||||||
|
|
||||||
export interface DocusaurusConfig {
|
export interface DocusaurusConfig {
|
||||||
|
@ -96,7 +96,7 @@ export interface Plugin<T> {
|
||||||
getThemePath?(): string;
|
getThemePath?(): string;
|
||||||
getPathsToWatch?(): string[];
|
getPathsToWatch?(): string[];
|
||||||
getClientModules?(): string[];
|
getClientModules?(): string[];
|
||||||
extendCli?(cli: CommanderStatic): void;
|
extendCli?(cli: Command): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PluginConfig = [string, Object] | [string] | string;
|
export type PluginConfig = [string, Object] | [string] | string;
|
||||||
|
@ -124,6 +124,7 @@ export interface RouteConfig {
|
||||||
modules?: RouteModule;
|
modules?: RouteModule;
|
||||||
routes?: RouteConfig[];
|
routes?: RouteConfig[];
|
||||||
exact?: boolean;
|
exact?: boolean;
|
||||||
|
priority?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ThemeAlias {
|
export interface ThemeAlias {
|
||||||
|
|
|
@ -17,6 +17,28 @@ import {
|
||||||
} from '@docusaurus/types';
|
} from '@docusaurus/types';
|
||||||
import {initPlugins} from './init';
|
import {initPlugins} from './init';
|
||||||
|
|
||||||
|
export function sortConfig(routeConfigs: RouteConfig[]) {
|
||||||
|
// Sort the route config. This ensures that route with nested routes is always placed last
|
||||||
|
routeConfigs.sort((a, b) => {
|
||||||
|
if (a.routes && !b.routes) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (!a.routes && b.routes) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
// Higher priority get placed first
|
||||||
|
if (a.priority || b.priority) {
|
||||||
|
const priorityA = a.priority || 0;
|
||||||
|
const priorityB = b.priority || 0;
|
||||||
|
const score = priorityA > priorityB ? -1 : priorityB > priorityA ? 1 : 0;
|
||||||
|
if (score !== 0) {
|
||||||
|
return score;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return a.path > b.path ? 1 : b.path > a.path ? -1 : 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function loadPlugins({
|
export async function loadPlugins({
|
||||||
pluginConfigs,
|
pluginConfigs,
|
||||||
context,
|
context,
|
||||||
|
@ -76,15 +98,7 @@ export async function loadPlugins({
|
||||||
);
|
);
|
||||||
|
|
||||||
// Sort the route config. This ensures that route with nested routes is always placed last
|
// Sort the route config. This ensures that route with nested routes is always placed last
|
||||||
pluginsRouteConfigs.sort((a, b) => {
|
sortConfig(pluginsRouteConfigs);
|
||||||
if (a.routes && !b.routes) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if (!a.routes && b.routes) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
return a.path > b.path ? 1 : b.path > a.path ? -1 : 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
plugins,
|
plugins,
|
||||||
|
|
|
@ -133,7 +133,7 @@ module.exports = {
|
||||||
/**
|
/**
|
||||||
* URL for editing website repo, example: 'https://github.com/facebook/docusaurus/edit/master/website/'
|
* URL for editing website repo, example: 'https://github.com/facebook/docusaurus/edit/master/website/'
|
||||||
*/
|
*/
|
||||||
editUrl: 'https://github.com/repo/project/website/',
|
editUrl: 'https://github.com/facebook/docusaurus/edit/master/website/',
|
||||||
/**
|
/**
|
||||||
* URL route for the blog section of your site
|
* URL route for the blog section of your site
|
||||||
* do not include trailing slash
|
* do not include trailing slash
|
||||||
|
|
|
@ -67,7 +67,7 @@ The headers are well-spaced so that the hierarchy is clear.
|
||||||
|
|
||||||
This will render in the browser as follows:
|
This will render in the browser as follows:
|
||||||
|
|
||||||
import BrowserWindow from '../src/components/BrowserWindow';
|
import BrowserWindow from '@site/src/components/BrowserWindow';
|
||||||
|
|
||||||
<BrowserWindow url="http://localhost:3000">
|
<BrowserWindow url="http://localhost:3000">
|
||||||
<h2>Hello from Docusaurus</h2>
|
<h2>Hello from Docusaurus</h2>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue