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:
Endi 2019-11-22 16:17:40 +07:00 committed by GitHub
parent c413cff212
commit 9829f56b1e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
45 changed files with 1852 additions and 395 deletions

View file

@ -1,5 +1,6 @@
---
custom_edit_url: https://github.com/customUrl/docs/lorem.md
unrelated_frontmatter: won't be part of metadata
---
Lorem ipsum.

View file

@ -1,7 +0,0 @@
---
id: permalink
title: Permalink
permalink: :baseUrl:docsUrl/endiliey/:id
---
This has a different permalink

View file

@ -11,6 +11,4 @@ module.exports = {
url: 'https://your-docusaurus-test-site.com',
baseUrl: '/',
favicon: 'img/favicon.ico',
organizationName: 'facebook', // Usually your GitHub org/user name.
projectName: 'docusaurus', // Usually your repo name.
};

View file

@ -0,0 +1 @@
This is `next` version of bar.

View file

@ -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',
};

View file

@ -0,0 +1,10 @@
{
"docs": {
"Test": [
"foo/bar"
],
"Guides": [
"hello"
]
}
}

View file

@ -0,0 +1 @@
Baz `1.0.0` ! This will be deleted in next subsequent versions.

View file

@ -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"
]
}
}

View file

@ -0,0 +1,10 @@
{
"version-1.0.1/docs": {
"Test": [
"version-1.0.1/foo/bar"
],
"Guides": [
"version-1.0.1/hello"
]
}
}

View file

@ -58,16 +58,8 @@ Array [
"docsMetadata": "@docusaurus-plugin-content-docs/docs-route-ff2.json",
},
"path": "/docs/:route",
"priority": undefined,
"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 {
"component": "@theme/DocItem",
"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",
},
}
`;

View file

@ -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",
},
],
}
`;

View file

@ -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();
});
});

View file

@ -8,19 +8,31 @@
import path from 'path';
import {validate} from 'webpack';
import {isMatch} from 'picomatch';
import commander from 'commander';
import fs from 'fs-extra';
import pluginContentDocs from '../index';
import loadEnv from '../env';
import {loadContext} from '@docusaurus/core/src/server/index';
import {applyConfigureWebpack} from '@docusaurus/core/src/webpack/utils';
import {RouteConfig} from '@docusaurus/types';
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 {
addRoute: (config: RouteConfig) => {
routeConfigs.push(config);
},
createData: async (name, _content) => {
createData: async (name, content) => {
if (dataContainer) {
dataContainer[name] = content;
}
return path.join(contentDir, name);
},
};
@ -84,6 +96,18 @@ describe('simple website', () => {
});
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', () => {
const pathToWatch = plugin.getPathsToWatch();
const matchPattern = pathToWatch.map(filepath =>
@ -126,7 +150,13 @@ describe('simple website', () => {
test('content', async () => {
const content = await plugin.loadContent();
const {docsMetadata, docsSidebars} = content;
const {
docsMetadata,
docsSidebars,
versionToSidebars,
permalinkToSidebar,
} = content;
expect(versionToSidebars).toEqual({});
expect(docsMetadata.hello).toEqual({
id: 'hello',
permalink: '/docs/hello',
@ -156,13 +186,233 @@ describe('simple website', () => {
expect(docsSidebars).toMatchSnapshot();
const routeConfigs = [];
const actions = createFakeActions(routeConfigs, pluginContentDir);
const dataContainer = {};
const actions = createFakeActions(
routeConfigs,
pluginContentDir,
dataContainer,
);
await plugin.contentLoaded({
content,
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).toMatchSnapshot();
});

View file

@ -6,96 +6,81 @@
*/
import path from 'path';
import {loadContext} from '@docusaurus/core/src/server/index';
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 siteConfig = {
title: 'Hello',
baseUrl: '/',
url: 'https://docusaurus.io',
};
const pluginPath = 'docs';
const docsDir = path.resolve(simpleSiteDir, pluginPath);
const context = loadContext(simpleSiteDir);
const routeBasePath = 'docs';
const docsDir = path.resolve(simpleSiteDir, routeBasePath);
const env = loadEnv(simpleSiteDir);
test('normal 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,
docsDir,
order: {},
siteConfig,
docsBasePath: pluginPath,
siteDir: simpleSiteDir,
refDir: docsDir,
context,
options,
env,
}),
processMetadata({
source: sourceB,
docsDir,
order: {},
siteConfig,
docsBasePath: pluginPath,
siteDir: simpleSiteDir,
refDir: docsDir,
context,
options,
env,
}),
]);
expect(dataA).toEqual({
id: 'foo/bar',
permalink: '/docs/foo/bar',
source: path.join('@site', pluginPath, sourceA),
source: path.join('@site', routeBasePath, sourceA),
title: 'Bar',
description: 'This is custom description',
});
expect(dataB).toEqual({
id: 'hello',
permalink: '/docs/hello',
source: path.join('@site', pluginPath, sourceB),
source: path.join('@site', routeBasePath, sourceB),
title: 'Hello, World !',
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 () => {
const editUrl =
'https://github.com/facebook/docusaurus/edit/master/website';
const source = path.join('foo', 'baz.md');
const options = {
routeBasePath,
editUrl,
};
const data = await processMetadata({
source,
docsDir,
order: {},
siteConfig,
docsBasePath: pluginPath,
siteDir: simpleSiteDir,
editUrl,
refDir: docsDir,
context,
options,
env,
});
expect(data).toEqual({
id: 'foo/baz',
permalink: '/docs/foo/baz',
source: path.join('@site', pluginPath, source),
source: path.join('@site', routeBasePath, source),
title: 'baz',
editUrl:
'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 options = {
routeBasePath,
};
const data = await processMetadata({
source,
docsDir,
order: {},
siteConfig,
docsBasePath: pluginPath,
siteDir: simpleSiteDir,
refDir: docsDir,
context,
options,
env,
});
expect(data).toEqual({
id: 'lorem',
permalink: '/docs/lorem',
source: path.join('@site', pluginPath, source),
source: path.join('@site', routeBasePath, source),
title: 'lorem',
editUrl: 'https://github.com/customUrl/docs/lorem.md',
description: 'Lorem ipsum.',
});
// unrelated frontmatter is not part of metadata
expect(data['unrelated_frontmatter']).toBeUndefined();
});
test('docs with last update time and author', async () => {
const source = 'lorem.md';
const data = await processMetadata({
source,
docsDir,
order: {},
siteConfig,
docsBasePath: pluginPath,
siteDir: simpleSiteDir,
const options = {
routeBasePath,
showLastUpdateAuthor: true,
showLastUpdateTime: true,
};
const data = await processMetadata({
source,
refDir: docsDir,
context,
options,
env,
});
expect(data).toEqual({
id: 'lorem',
permalink: '/docs/lorem',
source: path.join('@site', pluginPath, source),
source: path.join('@site', routeBasePath, source),
title: 'lorem',
editUrl: 'https://github.com/customUrl/docs/lorem.md',
description: 'Lorem ipsum.',
lastUpdatedAt: '1539502055',
lastUpdatedAt: 1539502055,
lastUpdatedBy: 'Author',
});
});
test('docs with invalid id', async () => {
const badSiteDir = path.join(fixtureDir, 'bad-site');
const options = {
routeBasePath,
};
return processMetadata({
source: 'invalid-id.md',
docsDir: path.join(badSiteDir, 'docs'),
order: {},
siteConfig,
docsBasePath: 'docs',
siteDir: simpleSiteDir,
refDir: path.join(badSiteDir, 'docs'),
context,
options,
env,
}).catch(e =>
expect(e).toMatchInlineSnapshot(
`[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',
});
});
});

View file

@ -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', () => {
expect(createOrder({})).toEqual({});
expect(createOrder(undefined)).toEqual({});

View file

@ -14,13 +14,13 @@ describe('loadSidebars', () => {
const fixtureDir = path.join(__dirname, '__fixtures__', 'sidebars');
test('sidebars with known sidebar item type', async () => {
const sidebarPath = path.join(fixtureDir, 'sidebars.json');
const result = loadSidebars(sidebarPath);
const result = loadSidebars([sidebarPath]);
expect(result).toMatchSnapshot();
});
test('sidebars with deep level of category', async () => {
const sidebarPath = path.join(fixtureDir, 'sidebars-category.js');
const result = loadSidebars(sidebarPath);
const result = loadSidebars([sidebarPath]);
expect(result).toMatchSnapshot();
});
@ -29,7 +29,9 @@ describe('loadSidebars', () => {
fixtureDir,
'sidebars-category-wrong-items.json',
);
expect(() => loadSidebars(sidebarPath)).toThrowErrorMatchingInlineSnapshot(
expect(() =>
loadSidebars([sidebarPath]),
).toThrowErrorMatchingInlineSnapshot(
`"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 () => {
const sidebarPath = path.join(
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"`,
);
});
test('sidebars with unknown sidebar item type', async () => {
const sidebarPath = path.join(fixtureDir, 'sidebars-unknown-type.json');
expect(() => loadSidebars(sidebarPath)).toThrowErrorMatchingInlineSnapshot(
expect(() =>
loadSidebars([sidebarPath]),
).toThrowErrorMatchingInlineSnapshot(
`"Unknown sidebar item type: superman"`,
);
});
test('sidebars with known sidebar item type but wrong field', async () => {
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\\"}"`,
);
});
@ -62,10 +70,4 @@ describe('loadSidebars', () => {
const result = loadSidebars(null);
expect(result).toEqual({});
});
test('fake sidebars path', () => {
expect(() => {
loadSidebars('/fake/path');
}).toThrowError();
});
});

View file

@ -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();
});
});

View 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';

View 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,
};
}

View file

@ -5,20 +5,17 @@
* LICENSE file in the root directory of this source tree.
*/
import _ from 'lodash';
import globby from 'globby';
import fs from 'fs-extra';
import path from 'path';
import {
idx,
normalizeUrl,
docuHash,
objectWithKeySorted,
} from '@docusaurus/utils';
import {LoadContext, Plugin} from '@docusaurus/types';
import {normalizeUrl, docuHash, objectWithKeySorted} from '@docusaurus/utils';
import {LoadContext, Plugin, RouteConfig} from '@docusaurus/types';
import createOrder from './order';
import loadSidebars from './sidebars';
import processMetadata from './metadata';
import loadEnv from './env';
import {
PluginOptions,
@ -35,8 +32,12 @@ import {
DocsSidebar,
DocsBaseMetadata,
MetadataRaw,
DocsMetadataRaw,
Metadata,
VersionToSidebars,
} from './types';
import {Configuration} from 'webpack';
import {docsVersion} from './version';
const DEFAULT_OPTIONS: PluginOptions = {
path: 'docs', // Path to data on filesystem, relative to site dir.
@ -56,101 +57,166 @@ export default function pluginContentDocs(
opts: Partial<PluginOptions>,
): Plugin<LoadedContent | null> {
const options = {...DEFAULT_OPTIONS, ...opts};
const contentPath = path.resolve(context.siteDir, options.path);
let sourceToPermalink: SourceToPermalink = {};
const {siteDir, generatedFilesDir, baseUrl} = context;
const docsDir = path.resolve(siteDir, options.path);
const sourceToPermalink: SourceToPermalink = {};
const dataDir = path.join(
context.generatedFilesDir,
generatedFilesDir,
'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 {
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() {
const {include = []} = options;
const globPattern = include.map(pattern => `${contentPath}/${pattern}`);
const {include} = options;
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];
},
// Fetches blog contents and returns metadata for the contents.
async loadContent() {
const {
include,
routeBasePath,
sidebarPath,
editUrl,
showLastUpdateAuthor,
showLastUpdateTime,
} = options;
const {siteConfig, siteDir} = context;
const docsDir = contentPath;
const {include, sidebarPath} = options;
if (!fs.existsSync(docsDir)) {
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.
const docsMetadataRaw: {
[id: string]: MetadataRaw;
} = {};
const docsMetadataRaw: DocsMetadataRaw = {};
const docsPromises = [];
// Metadata for default docs files.
// Metadata for default/ master docs files.
const docsFiles = await globby(include, {
cwd: docsDir,
});
await Promise.all(
docsFiles.map(async source => {
const metadata: MetadataRaw = await processMetadata({
source,
docsDir,
order,
siteConfig,
docsBasePath: routeBasePath,
siteDir,
editUrl,
showLastUpdateAuthor,
showLastUpdateTime,
});
docsMetadataRaw[metadata.id] = metadata;
}),
docsPromises.push(
Promise.all(
docsFiles.map(async source => {
const metadata: MetadataRaw = await processMetadata({
source,
refDir: docsDir,
context,
options,
env,
});
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 permalinkToSidebar: PermalinkToSidebar = {};
const versionToSidebars: VersionToSidebars = {};
Object.keys(docsMetadataRaw).forEach(currentID => {
let previous;
let next;
const previousID = idx(docsMetadataRaw, [currentID, 'previous']);
if (previousID) {
previous = {
title: idx(docsMetadataRaw, [previousID, 'title']) || 'Previous',
permalink: idx(docsMetadataRaw, [previousID, 'permalink']),
};
}
const nextID = idx(docsMetadataRaw, [currentID, 'next']);
if (nextID) {
next = {
title: idx(docsMetadataRaw, [nextID, 'title']) || 'Next',
permalink: idx(docsMetadataRaw, [nextID, 'permalink']),
};
}
const {next: nextID, previous: previousID, sidebar} =
order[currentID] || {};
const previous = previousID
? {
title: docsMetadataRaw[previousID]?.title ?? 'Previous',
permalink: docsMetadataRaw[previousID]?.permalink,
}
: undefined;
const next = nextID
? {
title: docsMetadataRaw[nextID]?.title ?? 'Next',
permalink: docsMetadataRaw[nextID]?.permalink,
}
: undefined;
docsMetadata[currentID] = {
...docsMetadataRaw[currentID],
sidebar,
previous,
next,
};
// sourceToPermalink and permalinkToSidebar mapping
const {source, permalink, sidebar} = docsMetadataRaw[currentID];
const {source, permalink, version} = docsMetadataRaw[currentID];
sourceToPermalink[source] = permalink;
if (sidebar) {
permalinkToSidebar[permalink] = sidebar;
if (versioning.enabled && version) {
if (!versionToSidebars[version]) {
versionToSidebars[version] = new Set();
}
versionToSidebars[version].add(sidebar);
}
}
});
@ -206,8 +272,8 @@ export default function pluginContentDocs(
docsMetadata,
docsDir,
docsSidebars,
sourceToPermalink,
permalinkToSidebar: objectWithKeySorted(permalinkToSidebar),
versionToSidebars,
};
},
@ -221,49 +287,107 @@ export default function pluginContentDocs(
const aliasedSource = (source: string) =>
`@docusaurus-plugin-content-docs/${path.relative(dataDir, source)}`;
const routes = await Promise.all(
Object.values(content.docsMetadata).map(async metadataItem => {
const metadataPath = await createData(
`${docuHash(metadataItem.permalink)}.json`,
JSON.stringify(metadataItem, null, 2),
);
return {
path: metadataItem.permalink,
component: docItemComponent,
exact: true,
modules: {
content: metadataItem.source,
metadata: aliasedSource(metadataPath),
},
};
}),
);
const docsBaseMetadata: DocsBaseMetadata = {
docsSidebars: content.docsSidebars,
permalinkToSidebar: content.permalinkToSidebar,
const genRoutes = async (
metadataItems: Metadata[],
): Promise<RouteConfig[]> => {
const routes = await Promise.all(
metadataItems.map(async metadataItem => {
const metadataPath = await createData(
`${docuHash(metadataItem.permalink)}.json`,
JSON.stringify(metadataItem, null, 2),
);
return {
path: metadataItem.permalink,
component: docItemComponent,
exact: true,
modules: {
content: metadataItem.source,
metadata: aliasedSource(metadataPath),
},
};
}),
);
return routes.sort((a, b) =>
a.path > b.path ? 1 : b.path > a.path ? -1 : 0,
);
};
const docsBaseRoute = normalizeUrl([
context.baseUrl,
routeBasePath,
':route',
]);
const docsBaseMetadataPath = await createData(
`${docuHash(docsBaseRoute)}.json`,
JSON.stringify(docsBaseMetadata, null, 2),
);
const addBaseRoute = async (
docsBaseRoute: string,
docsBaseMetadata: DocsBaseMetadata,
routes: RouteConfig[],
priority?: number,
) => {
const docsBaseMetadataPath = await createData(
`${docuHash(docsBaseRoute)}.json`,
JSON.stringify(docsBaseMetadata, null, 2),
);
addRoute({
path: docsBaseRoute,
component: docLayoutComponent,
routes: routes.sort((a, b) =>
a.path > b.path ? 1 : b.path > a.path ? -1 : 0,
),
modules: {
docsMetadata: aliasedSource(docsBaseMetadataPath),
},
});
addRoute({
path: docsBaseRoute,
component: docLayoutComponent,
routes,
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) {
@ -279,7 +403,7 @@ export default function pluginContentDocs(
rules: [
{
test: /(\.mdx?)$/,
include: [contentPath],
include: [docsDir, versionedDir].filter(Boolean),
use: [
getCacheLoader(isServer),
getBabelLoader(isServer),
@ -293,9 +417,10 @@ export default function pluginContentDocs(
{
loader: path.resolve(__dirname, './markdown/index.js'),
options: {
siteDir: context.siteDir,
docsDir: contentPath,
siteDir,
docsDir,
sourceToPermalink: sourceToPermalink,
versionedDir,
},
},
].filter(Boolean),

View file

@ -0,0 +1,6 @@
### Existing Docs
- [doc1](subdir/doc1.md)
### With hash
- [doc2](doc2.md#existing-docs)

View file

@ -0,0 +1,2 @@
### Relative linking
- [doc1](../doc2.md)

View file

@ -34,6 +34,15 @@ exports[`transform to correct links 1`] = `
- [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`] = `
"### Existing Docs
@ -55,3 +64,9 @@ exports[`transforms reference links 1`] = `
[image1]: assets/image1.png
"
`;
exports[`transforms relative links in versioned docs 1`] = `
"### Relative linking
- [doc1](/docs/1.0.0/doc2)
"
`;

View file

@ -9,14 +9,19 @@ import fs from 'fs-extra';
import path from 'path';
import linkify from '../linkify';
import {SourceToPermalink} from '../../types';
import {VERSIONED_DOCS_DIR} from '../../constants';
const siteDir = path.join(__dirname, '__fixtures__');
const docsDir = path.join(siteDir, 'docs');
const versionedDir = path.join(siteDir, VERSIONED_DOCS_DIR);
const sourceToPermalink: SourceToPermalink = {
'@site/docs/doc1.md': '/docs/doc1',
'@site/docs/doc2.md': '/docs/doc2',
'@site/docs/subdir/doc3.md': '/docs/subdir/doc3',
'@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 => {
@ -27,6 +32,7 @@ const transform = filepath => {
docsDir,
siteDir,
sourceToPermalink,
versionedDir,
);
return [content, transformedContent];
};
@ -70,3 +76,23 @@ test('transforms reference links', () => {
expect(transformedContent).not.toContain('[doc2]: ./doc2.md');
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);
});

View file

@ -11,7 +11,7 @@ import linkify from './linkify';
export = function(fileString: string) {
const callback = this.async();
const {docsDir, siteDir, sourceToPermalink} = getOptions(this);
const {docsDir, siteDir, versionedDir, sourceToPermalink} = getOptions(this);
return (
callback &&
callback(
@ -22,6 +22,7 @@ export = function(fileString: string) {
docsDir,
siteDir,
sourceToPermalink,
versionedDir,
),
)
);

View file

@ -7,6 +7,7 @@
import path from 'path';
import {resolve} from 'url';
import {getSubFolder} from '@docusaurus/utils';
import {SourceToPermalink} from '../types';
export default function(
@ -15,12 +16,19 @@ export default function(
docsDir: string,
siteDir: string,
sourceToPermalink: SourceToPermalink,
versionedDir?: string,
) {
// Determine the source dir. e.g: /website/docs, /website/versioned_docs/version-1.0.0
let sourceDir: string | undefined;
const thisSource = filePath;
if (thisSource.startsWith(docsDir)) {
sourceDir = docsDir;
} else if (versionedDir && thisSource.startsWith(versionedDir)) {
const specificVersionDir = getSubFolder(thisSource, versionedDir);
// e.g: specificVersionDir = version-1.0.0
if (specificVersionDir) {
sourceDir = path.join(versionedDir, specificVersionDir);
}
}
let content = fileString;

View file

@ -8,109 +8,24 @@
import fs from 'fs-extra';
import path from 'path';
import {parse, normalizeUrl, posixPath} from '@docusaurus/utils';
import {DocusaurusConfig} from '@docusaurus/types';
import {LoadContext} from '@docusaurus/types';
import lastUpdate from './lastUpdate';
import {Order, MetadataRaw} from './types';
import {MetadataRaw, LastUpdateData, MetadataOptions, Env} from './types';
type Args = {
source: string;
docsDir: string;
order: Order;
siteConfig: Partial<DocusaurusConfig>;
docsBasePath: string;
siteDir: string;
editUrl?: string;
showLastUpdateAuthor?: boolean;
showLastUpdateTime?: boolean;
refDir: string;
context: LoadContext;
options: MetadataOptions;
env: Env;
};
export default async function processMetadata({
source,
docsDir,
order,
siteConfig,
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;
}
async function lastUpdated(
filePath: string,
options: MetadataOptions,
): Promise<LastUpdateData> {
const {showLastUpdateAuthor, showLastUpdateTime} = options;
if (showLastUpdateAuthor || showLastUpdateTime) {
// Use fake data in dev for faster development
const fileLastUpdateData =
@ -118,20 +33,113 @@ export default async function processMetadata({
? await lastUpdate(filePath)
: {
author: 'Author',
timestamp: '1539502055',
timestamp: 1539502055,
};
if (fileLastUpdateData) {
const {author, timestamp} = fileLastUpdateData;
if (showLastUpdateAuthor && author) {
metadata.lastUpdatedBy = author;
}
return {
lastUpdatedAt: showLastUpdateTime ? timestamp : undefined,
lastUpdatedBy: showLastUpdateAuthor ? author : undefined,
};
}
}
return {};
}
if (showLastUpdateTime && timestamp) {
metadata.lastUpdatedAt = timestamp;
export default async function processMetadata({
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;
}

View file

@ -5,13 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {
Sidebar,
SidebarItem,
SidebarItemDoc,
SidebarItemCategory,
Order,
} from './types';
import {Sidebar, SidebarItem, Order} from './types';
// Build the docs meta such as next, previous, category and sidebar.
export default function createOrder(allSidebars: Sidebar = {}): Order {
@ -26,7 +20,7 @@ export default function createOrder(allSidebars: Sidebar = {}): Order {
switch (item.type) {
case 'category':
indexItems({
items: (item as SidebarItemCategory).items,
items: item.items,
});
break;
case 'ref':
@ -34,12 +28,8 @@ export default function createOrder(allSidebars: Sidebar = {}): Order {
// Refs and links should not be shown in navigation.
break;
case 'doc':
ids.push((item as SidebarItemDoc).id);
ids.push(item.id);
break;
default:
throw new Error(
`Unknown item type: ${item.type}. Item: ${JSON.stringify(item)}`,
);
}
});
};

View file

@ -5,6 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import fs from 'fs-extra';
import importFresh from 'import-fresh';
import {
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.
let allSidebars: SidebarRaw = {};
if (sidebarPath) {
allSidebars = importFresh(sidebarPath) as SidebarRaw;
if (!sidebarPaths || !sidebarPaths.length) {
return {} as Sidebar;
}
sidebarPaths.map(sidebarPath => {
if (sidebarPath && fs.existsSync(sidebarPath)) {
const sidebar = importFresh(sidebarPath) as SidebarRaw;
Object.assign(allSidebars, sidebar);
}
});
return normalizeSidebar(allSidebars);
}

View file

@ -5,39 +5,45 @@
* LICENSE file in the root directory of this source tree.
*/
export interface PluginOptions {
path: string;
export interface MetadataOptions {
routeBasePath: string;
include: string[];
sidebarPath: string;
docLayoutComponent: string;
docItemComponent: string;
remarkPlugins: string[];
rehypePlugins: string[];
editUrl?: string;
showLastUpdateTime?: 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 = {
type: string;
type: 'doc' | 'ref';
id: string;
};
export interface SidebarItemLink {
type: string;
type: 'link';
href: string;
label: string;
}
export interface SidebarItemCategory {
type: string;
type: 'category';
label: string;
items: SidebarItem[];
}
export interface SidebarItemCategoryRaw {
type: string;
type: 'category';
label: string;
items: SidebarItemRaw[];
}
@ -51,7 +57,11 @@ export type SidebarItemRaw =
| string
| SidebarItemDoc
| SidebarItemLink
| SidebarItemCategoryRaw;
| SidebarItemCategoryRaw
| {
type: string;
[key: string]: any;
};
// Sidebar given by user that is not normalized yet. e.g: sidebars.json
export interface SidebarRaw {
@ -65,7 +75,7 @@ export interface Sidebar {
}
export interface DocsSidebarItemCategory {
type: string;
type: 'category';
label: string;
items: (SidebarItemLink | DocsSidebarItemCategory)[];
}
@ -84,7 +94,12 @@ export interface Order {
[id: string]: OrderMetadata;
}
export interface MetadataRaw extends OrderMetadata {
export interface LastUpdateData {
lastUpdatedAt?: number;
lastUpdatedBy?: string;
}
export interface MetadataRaw extends LastUpdateData {
id: string;
title: string;
description: string;
@ -92,9 +107,7 @@ export interface MetadataRaw extends OrderMetadata {
permalink: string;
sidebar_label?: string;
editUrl?: string;
lastUpdatedAt?: number;
lastUpdatedBy?: string;
[key: string]: any;
version?: string;
}
export interface Paginator {
@ -102,7 +115,8 @@ export interface Paginator {
permalink: string;
}
export interface Metadata extends Omit<MetadataRaw, 'previous' | 'next'> {
export interface Metadata extends MetadataRaw {
sidebar?: string;
previous?: Paginator;
next?: Paginator;
}
@ -111,6 +125,10 @@ export interface DocsMetadata {
[id: string]: Metadata;
}
export interface DocsMetadataRaw {
[id: string]: MetadataRaw;
}
export interface SourceToPermalink {
[source: string]: string;
}
@ -119,15 +137,34 @@ export interface PermalinkToSidebar {
[permalink: string]: string;
}
export interface VersionToSidebars {
[version: string]: Set<string>;
}
export interface LoadedContent {
docsMetadata: DocsMetadata;
docsDir: string;
docsSidebars: Sidebar;
sourceToPermalink: SourceToPermalink;
docsSidebars: DocsSidebar;
permalinkToSidebar: PermalinkToSidebar;
versionToSidebars: VersionToSidebars;
}
export type DocsBaseMetadata = Pick<
LoadedContent,
'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
}

View 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!`);
}