feat(v2): blog + docs multi-instance plugins (#3204)

* stable createData namespacing + second-blog dogfooding

* Docs: support multi-instance + make community docs a separate instance

* tests: add 2nd docs instance to versioned site

* fix docs version cli tests

* fix docs versioning cli

* typo

* team: add link to my site

* better extendCli integration

* fix metadata tests

* tests for versioned site with second docs instance

* move some validation code to utils-validation

* fix missing dependency

* fix bad compiled output due to importing constants in ./client folder

* make docs tests easier to maintain

* refactors

* prevent lodash imports in client bundle

* redirect old community docs to new urls
This commit is contained in:
Sébastien Lorber 2020-08-05 18:27:55 +02:00 committed by GitHub
parent e944f35640
commit 59f705ee66
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
67 changed files with 2025 additions and 2059 deletions

View file

@ -0,0 +1,3 @@
{
"version-1.0.0/community": ["version-1.0.0/team"]
}

View file

@ -77,3 +77,14 @@ Object {
],
}
`;
exports[`docsVersion second docs instance versioning 1`] = `
Object {
"version-2.0.0/community": Array [
Object {
"id": "version-2.0.0/team",
"type": "doc",
},
],
}
`;

View file

@ -7,18 +7,19 @@
import path from 'path';
import loadEnv from '../env';
import {DEFAULT_PLUGIN_ID} from '@docusaurus/core/lib/constants';
describe('loadEnv', () => {
test('website with versioning disabled', () => {
const siteDir = path.join(__dirname, '__fixtures__', 'simple-site');
const env = loadEnv(siteDir);
const env = loadEnv(siteDir, DEFAULT_PLUGIN_ID);
expect(env.versioning.enabled).toBe(false);
expect(env.versioning.versions).toStrictEqual([]);
});
test('website with versioning enabled', () => {
const siteDir = path.join(__dirname, '__fixtures__', 'versioned-site');
const env = loadEnv(siteDir);
const env = loadEnv(siteDir, DEFAULT_PLUGIN_ID);
expect(env.versioning.enabled).toBe(true);
expect(env.versioning.latestVersion).toBe('1.0.1');
expect(env.versioning.versions).toStrictEqual([
@ -28,9 +29,17 @@ describe('loadEnv', () => {
]);
});
test('website with versioning enabled, 2nd docs plugin instance', () => {
const siteDir = path.join(__dirname, '__fixtures__', 'versioned-site');
const env = loadEnv(siteDir, 'community');
expect(env.versioning.enabled).toBe(true);
expect(env.versioning.latestVersion).toBe('1.0.0');
expect(env.versioning.versions).toStrictEqual(['1.0.0']);
});
test('website with versioning but disabled', () => {
const siteDir = path.join(__dirname, '__fixtures__', 'versioned-site');
const env = loadEnv(siteDir, {disableVersioning: true});
const env = loadEnv(siteDir, DEFAULT_PLUGIN_ID, {disableVersioning: true});
expect(env.versioning.enabled).toBe(false);
expect(env.versioning.versions).toStrictEqual([]);
});
@ -42,7 +51,7 @@ describe('loadEnv', () => {
invalid: 'json',
};
});
const env = loadEnv(siteDir);
const env = loadEnv(siteDir, DEFAULT_PLUGIN_ID);
expect(env.versioning.enabled).toBe(false);
mock.mockRestore();
});

View file

@ -12,35 +12,65 @@ import commander from 'commander';
import fs from 'fs-extra';
import pluginContentDocs from '../index';
import loadEnv from '../env';
import normalizePluginOptions from './pluginOptionSchema.test';
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';
import {DEFAULT_PLUGIN_ID} from '@docusaurus/core/lib/constants';
import * as version from '../version';
import {PluginOptionSchema} from '../pluginOptionSchema';
import {normalizePluginOptions} from '@docusaurus/utils-validation';
const createFakeActions = (
routeConfigs: RouteConfig[],
contentDir,
dataContainer?,
globalDataContainer?,
) => {
return {
const createFakeActions = (contentDir: string) => {
const routeConfigs: RouteConfig[] = [];
const dataContainer: any = {};
const globalDataContainer: any = {};
const actions = {
addRoute: (config: RouteConfig) => {
routeConfigs.push(config);
},
createData: async (name, content) => {
if (dataContainer) {
dataContainer[name] = content;
}
createData: async (name: string, content: unknown) => {
dataContainer[name] = content;
return path.join(contentDir, name);
},
setGlobalData: (data) => {
setGlobalData: (data: any) => {
globalDataContainer.pluginName = {pluginId: data};
},
};
// Extra fns useful for tests!
const utils = {
getGlobalData: () => globalDataContainer,
getRouteConfigs: () => routeConfigs,
// query by prefix, because files have a hash at the end
// so it's not convenient to query by full filename
getCreatedDataByPrefix: (prefix: string) => {
const entry = Object.entries(dataContainer).find(([key]) =>
key.startsWith(prefix),
);
if (!entry) {
throw new Error(`No entry found for prefix=${prefix}`);
}
return JSON.parse(entry[1] as string);
},
expectSnapshot: () => {
// Sort the route config like in src/server/plugins/index.ts for consistent snapshot ordering
sortConfig(routeConfigs);
expect(routeConfigs).not.toEqual([]);
expect(routeConfigs).toMatchSnapshot('route config');
expect(dataContainer).toMatchSnapshot('data');
expect(globalDataContainer).toMatchSnapshot('global data');
},
};
return {
actions,
utils,
};
};
test('site with wrong sidebar file', async () => {
@ -49,7 +79,7 @@ test('site with wrong sidebar file', async () => {
const sidebarPath = path.join(siteDir, 'wrong-sidebars.json');
const plugin = pluginContentDocs(
context,
normalizePluginOptions({
normalizePluginOptions(PluginOptionSchema, {
sidebarPath,
}),
);
@ -62,28 +92,30 @@ describe('empty/no docs website', () => {
test('no files in docs folder', async () => {
await fs.ensureDir(path.join(siteDir, 'docs'));
const plugin = pluginContentDocs(context, normalizePluginOptions({}));
const plugin = pluginContentDocs(
context,
normalizePluginOptions(PluginOptionSchema, {}),
);
const content = await plugin.loadContent();
const {docsMetadata, docsSidebars} = content;
expect(docsMetadata).toMatchInlineSnapshot(`Object {}`);
expect(docsSidebars).toMatchInlineSnapshot(`Object {}`);
const routeConfigs = [];
const pluginContentDir = path.join(context.generatedFilesDir, plugin.name);
const actions = createFakeActions(routeConfigs, pluginContentDir);
const {actions, utils} = createFakeActions(pluginContentDir);
await plugin.contentLoaded({
content,
actions,
});
expect(routeConfigs).toEqual([]);
expect(utils.getRouteConfigs()).toEqual([]);
});
test('docs folder does not exist', async () => {
const plugin = pluginContentDocs(
context,
normalizePluginOptions({
normalizePluginOptions(PluginOptionSchema, {
path: '/path/does/not/exist/',
}),
);
@ -99,7 +131,7 @@ describe('simple website', () => {
const pluginPath = 'docs';
const plugin = pluginContentDocs(
context,
normalizePluginOptions({
normalizePluginOptions(PluginOptionSchema, {
path: pluginPath,
sidebarPath,
homePageId: 'hello',
@ -112,7 +144,7 @@ describe('simple website', () => {
const cli = new commander.Command();
plugin.extendCli(cli);
cli.parse(['node', 'test', 'docs:version', '1.0.0']);
expect(mock).toHaveBeenCalledWith('1.0.0', siteDir, {
expect(mock).toHaveBeenCalledWith('1.0.0', siteDir, DEFAULT_PLUGIN_ID, {
path: pluginPath,
sidebarPath,
});
@ -200,15 +232,7 @@ describe('simple website', () => {
expect(docsSidebars).toMatchSnapshot();
const routeConfigs = [];
const dataContainer = {};
const globalDataContainer = {};
const actions = createFakeActions(
routeConfigs,
pluginContentDir,
dataContainer,
globalDataContainer,
);
const {actions, utils} = createFakeActions(pluginContentDir);
await plugin.contentLoaded({
content,
@ -216,16 +240,12 @@ describe('simple website', () => {
});
// There is only one nested docs route for simple site
const baseMetadata = JSON.parse(dataContainer['docs-route-ff2.json']);
const baseMetadata = utils.getCreatedDataByPrefix('docs-route-');
expect(baseMetadata.docsSidebars).toEqual(docsSidebars);
expect(baseMetadata.permalinkToSidebar).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();
expect(globalDataContainer).toMatchSnapshot();
utils.expectSnapshot();
expect(utils.getGlobalData()).toMatchSnapshot();
});
});
@ -236,22 +256,26 @@ describe('versioned website', () => {
const routeBasePath = 'docs';
const plugin = pluginContentDocs(
context,
normalizePluginOptions({
normalizePluginOptions(PluginOptionSchema, {
routeBasePath,
sidebarPath,
homePageId: 'hello',
}),
);
const env = loadEnv(siteDir);
const env = loadEnv(siteDir, DEFAULT_PLUGIN_ID);
const {docsDir: versionedDir} = env.versioning;
const pluginContentDir = path.join(context.generatedFilesDir, plugin.name);
test('isVersioned', () => {
expect(env.versioning.enabled).toEqual(true);
});
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, {
expect(mock).toHaveBeenCalledWith('2.0.0', siteDir, DEFAULT_PLUGIN_ID, {
path: routeBasePath,
sidebarPath,
});
@ -401,23 +425,15 @@ describe('versioned website', () => {
expect(versionToSidebars).toMatchSnapshot(
'sidebars needed for each version',
);
const routeConfigs = [];
const dataContainer = {};
const globalDataContainer = {};
const actions = createFakeActions(
routeConfigs,
pluginContentDir,
dataContainer,
globalDataContainer,
);
const {actions, utils} = createFakeActions(pluginContentDir);
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'],
const latestVersionBaseMetadata = utils.getCreatedDataByPrefix(
'docs-route-',
);
expect(latestVersionBaseMetadata).toMatchSnapshot(
'base metadata for latest version',
@ -426,8 +442,8 @@ describe('versioned website', () => {
expect(latestVersionBaseMetadata.permalinkToSidebar).not.toEqual(
permalinkToSidebar,
);
const nextVersionBaseMetadata = JSON.parse(
dataContainer['docs-next-route-1c8.json'],
const nextVersionBaseMetadata = utils.getCreatedDataByPrefix(
'docs-next-route-',
);
expect(nextVersionBaseMetadata).toMatchSnapshot(
'base metadata for next version',
@ -436,8 +452,8 @@ describe('versioned website', () => {
expect(nextVersionBaseMetadata.permalinkToSidebar).not.toEqual(
permalinkToSidebar,
);
const firstVersionBaseMetadata = JSON.parse(
dataContainer['docs-1-0-0-route-660.json'],
const firstVersionBaseMetadata = utils.getCreatedDataByPrefix(
'docs-1-0-0-route-',
);
expect(firstVersionBaseMetadata).toMatchSnapshot(
'base metadata for first version',
@ -447,11 +463,151 @@ describe('versioned website', () => {
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();
expect(globalDataContainer).toMatchSnapshot();
utils.expectSnapshot();
});
});
describe('versioned website (community)', () => {
const siteDir = path.join(__dirname, '__fixtures__', 'versioned-site');
const context = loadContext(siteDir);
const sidebarPath = path.join(siteDir, 'community_sidebars.json');
const routeBasePath = 'community';
const pluginId = 'community';
const plugin = pluginContentDocs(
context,
normalizePluginOptions(PluginOptionSchema, {
id: 'community',
path: 'community',
routeBasePath,
sidebarPath,
}),
);
const env = loadEnv(siteDir, pluginId);
const {docsDir: versionedDir} = env.versioning;
const pluginContentDir = path.join(context.generatedFilesDir, plugin.name);
test('isVersioned', () => {
expect(env.versioning.enabled).toEqual(true);
});
test('extendCli - docsVersion', () => {
const mock = jest.spyOn(version, 'docsVersion').mockImplementation();
const cli = new commander.Command();
plugin.extendCli(cli);
cli.parse(['node', 'test', `docs:version:${pluginId}`, '2.0.0']);
expect(mock).toHaveBeenCalledWith('2.0.0', siteDir, pluginId, {
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 [
"community/**/*.{md,mdx}",
"community_versioned_sidebars/version-1.0.0-sidebars.json",
"community_versioned_docs/version-1.0.0/**/*.{md,mdx}",
"community_sidebars.json",
]
`);
expect(isMatch('community/team.md', matchPattern)).toEqual(true);
expect(
isMatch('community_versioned_docs/version-1.0.0/team.md', matchPattern),
).toEqual(true);
// Non existing version
expect(
isMatch('community_versioned_docs/version-2.0.0/team.md', matchPattern),
).toEqual(false);
expect(
isMatch(
'community_versioned_sidebars/version-2.0.0-sidebars.json',
matchPattern,
),
).toEqual(false);
expect(isMatch('community/team.js', matchPattern)).toEqual(false);
expect(
isMatch('community_versioned_docs/version-1.0.0/team.js', matchPattern),
).toEqual(false);
});
test('content', async () => {
const content = await plugin.loadContent();
const {
docsMetadata,
docsSidebars,
versionToSidebars,
permalinkToSidebar,
} = content;
expect(docsMetadata.team).toEqual({
id: 'team',
unversionedId: 'team',
isDocsHomePage: false,
permalink: '/community/next/team',
source: path.join('@site', routeBasePath, 'team.md'),
title: 'team',
description: 'Team current version',
version: 'next',
sidebar: 'community',
});
expect(docsMetadata['version-1.0.0/team']).toEqual({
id: 'version-1.0.0/team',
unversionedId: 'team',
isDocsHomePage: false,
permalink: '/community/team',
source: path.join(
'@site',
path.relative(siteDir, versionedDir),
'version-1.0.0',
'team.md',
),
title: 'team',
description: 'Team 1.0.0',
version: '1.0.0',
sidebar: 'version-1.0.0/community',
});
expect(docsSidebars).toMatchSnapshot('all sidebars');
expect(versionToSidebars).toMatchSnapshot(
'sidebars needed for each version',
);
const {actions, utils} = createFakeActions(pluginContentDir);
await plugin.contentLoaded({
content,
actions,
});
// The created base metadata for each nested docs route is smartly chunked/ splitted across version
const latestVersionBaseMetadata = utils.getCreatedDataByPrefix(
'community-route-',
);
expect(latestVersionBaseMetadata).toMatchSnapshot(
'base metadata for latest version',
);
expect(latestVersionBaseMetadata.docsSidebars).not.toEqual(docsSidebars);
expect(latestVersionBaseMetadata.permalinkToSidebar).not.toEqual(
permalinkToSidebar,
);
const nextVersionBaseMetadata = utils.getCreatedDataByPrefix(
'community-next-route-',
);
expect(nextVersionBaseMetadata).toMatchSnapshot(
'base metadata for next version',
);
expect(nextVersionBaseMetadata.docsSidebars).not.toEqual(docsSidebars);
expect(nextVersionBaseMetadata.permalinkToSidebar).not.toEqual(
permalinkToSidebar,
);
utils.expectSnapshot();
});
});

View file

@ -11,6 +11,7 @@ import processMetadata from '../metadata';
import loadEnv from '../env';
import {MetadataRaw, Env, MetadataOptions} from '../types';
import {LoadContext} from '@docusaurus/types';
import {DEFAULT_PLUGIN_ID} from '@docusaurus/core/lib/constants';
const fixtureDir = path.join(__dirname, '__fixtures__');
@ -66,7 +67,7 @@ describe('simple site', () => {
const context = loadContext(siteDir);
const routeBasePath = 'docs';
const docsDir = path.resolve(siteDir, routeBasePath);
const env = loadEnv(siteDir);
const env = loadEnv(siteDir, DEFAULT_PLUGIN_ID);
const options = {routeBasePath};
const {testMeta, testSlug} = createTestHelpers({
@ -309,7 +310,7 @@ describe('versioned site', () => {
const context = loadContext(siteDir);
const routeBasePath = 'docs';
const docsDir = path.resolve(siteDir, routeBasePath);
const env = loadEnv(siteDir);
const env = loadEnv(siteDir, DEFAULT_PLUGIN_ID);
const {docsDir: versionedDir} = env.versioning;
const options = {routeBasePath};

View file

@ -6,17 +6,7 @@
*/
import {PluginOptionSchema, DEFAULT_OPTIONS} from '../pluginOptionSchema';
export default function normalizePluginOptions(options) {
const {value, error} = PluginOptionSchema.validate(options, {
convert: false,
});
if (error) {
throw error;
} else {
return value;
}
}
import {normalizePluginOptions} from '@docusaurus/utils-validation';
// the type of remark/rehype plugins is function
const remarkRehypePluginStub = () => {};
@ -63,7 +53,7 @@ describe('normalizeDocsPluginOptions', () => {
test('should reject bad path inputs', () => {
expect(() => {
normalizePluginOptions({
normalizePluginOptions(PluginOptionSchema, {
path: 2,
});
}).toThrowErrorMatchingInlineSnapshot(`"\\"path\\" must be a string"`);
@ -71,7 +61,7 @@ describe('normalizeDocsPluginOptions', () => {
test('should reject bad include inputs', () => {
expect(() => {
normalizePluginOptions({
normalizePluginOptions(PluginOptionSchema, {
include: '**/*.{md,mdx}',
});
}).toThrowErrorMatchingInlineSnapshot(`"\\"include\\" must be an array"`);
@ -79,7 +69,7 @@ describe('normalizeDocsPluginOptions', () => {
test('should reject bad showLastUpdateTime inputs', () => {
expect(() => {
normalizePluginOptions({
normalizePluginOptions(PluginOptionSchema, {
showLastUpdateTime: 'true',
});
}).toThrowErrorMatchingInlineSnapshot(
@ -89,7 +79,7 @@ describe('normalizeDocsPluginOptions', () => {
test('should reject bad remarkPlugins input', () => {
expect(() => {
normalizePluginOptions({
normalizePluginOptions(PluginOptionSchema, {
remarkPlugins: 'remark-math',
});
}).toThrowErrorMatchingInlineSnapshot(

View file

@ -14,6 +14,7 @@ import {
getVersionsJSONFile,
getVersionedSidebarsDir,
} from '../env';
import {DEFAULT_PLUGIN_ID} from '@docusaurus/core/lib/constants';
const fixtureDir = path.join(__dirname, '__fixtures__');
@ -27,87 +28,114 @@ describe('docsVersion', () => {
test('no version tag provided', () => {
expect(() =>
docsVersion(null, simpleSiteDir, DEFAULT_OPTIONS),
docsVersion(null, simpleSiteDir, DEFAULT_PLUGIN_ID, DEFAULT_OPTIONS),
).toThrowErrorMatchingInlineSnapshot(
`"No version tag specified!. Pass the version you wish to create as an argument. Ex: 1.0.0"`,
`"[docs] No version tag specified!. Pass the version you wish to create as an argument. Ex: 1.0.0"`,
);
expect(() =>
docsVersion(undefined, simpleSiteDir, DEFAULT_OPTIONS),
docsVersion(undefined, simpleSiteDir, DEFAULT_PLUGIN_ID, DEFAULT_OPTIONS),
).toThrowErrorMatchingInlineSnapshot(
`"No version tag specified!. Pass the version you wish to create as an argument. Ex: 1.0.0"`,
`"[docs] No version tag specified!. Pass the version you wish to create as an argument. Ex: 1.0.0"`,
);
expect(() =>
docsVersion('', simpleSiteDir, DEFAULT_OPTIONS),
docsVersion('', simpleSiteDir, DEFAULT_PLUGIN_ID, DEFAULT_OPTIONS),
).toThrowErrorMatchingInlineSnapshot(
`"No version tag specified!. Pass the version you wish to create as an argument. Ex: 1.0.0"`,
`"[docs] 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),
docsVersion('foo/bar', simpleSiteDir, DEFAULT_PLUGIN_ID, DEFAULT_OPTIONS),
).toThrowErrorMatchingInlineSnapshot(
`"Invalid version tag specified! Do not include slash (/) or (\\\\). Try something like: 1.0.0"`,
`"[docs] Invalid version tag specified! Do not include slash (/) or (\\\\). Try something like: 1.0.0"`,
);
expect(() =>
docsVersion('foo\\bar', simpleSiteDir, DEFAULT_OPTIONS),
docsVersion(
'foo\\bar',
simpleSiteDir,
DEFAULT_PLUGIN_ID,
DEFAULT_OPTIONS,
),
).toThrowErrorMatchingInlineSnapshot(
`"Invalid version tag specified! Do not include slash (/) or (\\\\). Try something like: 1.0.0"`,
`"[docs] 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),
docsVersion(
'a'.repeat(255),
simpleSiteDir,
DEFAULT_PLUGIN_ID,
DEFAULT_OPTIONS,
),
).toThrowErrorMatchingInlineSnapshot(
`"Invalid version tag specified! Length must <= 32 characters. Try something like: 1.0.0"`,
`"[docs] 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),
docsVersion('..', simpleSiteDir, DEFAULT_PLUGIN_ID, DEFAULT_OPTIONS),
).toThrowErrorMatchingInlineSnapshot(
`"Invalid version tag specified! Do not name your version \\".\\" or \\"..\\". Try something like: 1.0.0"`,
`"[docs] Invalid version tag specified! Do not name your version \\".\\" or \\"..\\". Try something like: 1.0.0"`,
);
expect(() =>
docsVersion('.', simpleSiteDir, DEFAULT_OPTIONS),
docsVersion('.', simpleSiteDir, DEFAULT_PLUGIN_ID, DEFAULT_OPTIONS),
).toThrowErrorMatchingInlineSnapshot(
`"Invalid version tag specified! Do not name your version \\".\\" or \\"..\\". Try something like: 1.0.0"`,
`"[docs] 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),
docsVersion(
'<foo|bar>',
simpleSiteDir,
DEFAULT_PLUGIN_ID,
DEFAULT_OPTIONS,
),
).toThrowErrorMatchingInlineSnapshot(
`"Invalid version tag specified! Please ensure its a valid pathname too. Try something like: 1.0.0"`,
`"[docs] Invalid version tag specified! Please ensure its a valid pathname too. Try something like: 1.0.0"`,
);
expect(() =>
docsVersion('foo\x00bar', simpleSiteDir, DEFAULT_OPTIONS),
docsVersion(
'foo\x00bar',
simpleSiteDir,
DEFAULT_PLUGIN_ID,
DEFAULT_OPTIONS,
),
).toThrowErrorMatchingInlineSnapshot(
`"Invalid version tag specified! Please ensure its a valid pathname too. Try something like: 1.0.0"`,
`"[docs] Invalid version tag specified! Please ensure its a valid pathname too. Try something like: 1.0.0"`,
);
expect(() =>
docsVersion('foo:bar', simpleSiteDir, DEFAULT_OPTIONS),
docsVersion('foo:bar', simpleSiteDir, DEFAULT_PLUGIN_ID, DEFAULT_OPTIONS),
).toThrowErrorMatchingInlineSnapshot(
`"Invalid version tag specified! Please ensure its a valid pathname too. Try something like: 1.0.0"`,
`"[docs] 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),
docsVersion(
'1.0.0',
versionedSiteDir,
DEFAULT_PLUGIN_ID,
DEFAULT_OPTIONS,
),
).toThrowErrorMatchingInlineSnapshot(
`"This version already exists!. Use a version tag that does not already exist."`,
`"[docs] 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 !"`);
docsVersion('1.0.0', emptySiteDir, DEFAULT_PLUGIN_ID, DEFAULT_OPTIONS),
).toThrowErrorMatchingInlineSnapshot(
`"[docs] There is no docs to version !"`,
);
});
test('first time versioning', () => {
@ -131,21 +159,26 @@ describe('docsVersion', () => {
path: 'docs',
sidebarPath: path.join(simpleSiteDir, 'sidebars.json'),
};
docsVersion('1.0.0', simpleSiteDir, options);
docsVersion('1.0.0', simpleSiteDir, DEFAULT_PLUGIN_ID, options);
expect(copyMock).toHaveBeenCalledWith(
path.join(simpleSiteDir, options.path),
path.join(getVersionedDocsDir(simpleSiteDir), 'version-1.0.0'),
path.join(
getVersionedDocsDir(simpleSiteDir, DEFAULT_PLUGIN_ID),
'version-1.0.0',
),
);
expect(versionedSidebar).toMatchSnapshot();
expect(versionedSidebarPath).toEqual(
path.join(
getVersionedSidebarsDir(simpleSiteDir),
getVersionedSidebarsDir(simpleSiteDir, DEFAULT_PLUGIN_ID),
'version-1.0.0-sidebars.json',
),
);
expect(versionsPath).toEqual(getVersionsJSONFile(simpleSiteDir));
expect(versionsPath).toEqual(
getVersionsJSONFile(simpleSiteDir, DEFAULT_PLUGIN_ID),
);
expect(versions).toEqual(['1.0.0']);
expect(consoleMock).toHaveBeenCalledWith('Version 1.0.0 created!');
expect(consoleMock).toHaveBeenCalledWith('[docs] Version 1.0.0 created!');
copyMock.mockRestore();
writeMock.mockRestore();
@ -174,21 +207,78 @@ describe('docsVersion', () => {
path: 'docs',
sidebarPath: path.join(versionedSiteDir, 'sidebars.json'),
};
docsVersion('2.0.0', versionedSiteDir, options);
docsVersion('2.0.0', versionedSiteDir, DEFAULT_PLUGIN_ID, options);
expect(copyMock).toHaveBeenCalledWith(
path.join(versionedSiteDir, options.path),
path.join(getVersionedDocsDir(versionedSiteDir), 'version-2.0.0'),
path.join(
getVersionedDocsDir(versionedSiteDir, DEFAULT_PLUGIN_ID),
'version-2.0.0',
),
);
expect(versionedSidebar).toMatchSnapshot();
expect(versionedSidebarPath).toEqual(
path.join(
getVersionedSidebarsDir(versionedSiteDir),
getVersionedSidebarsDir(versionedSiteDir, DEFAULT_PLUGIN_ID),
'version-2.0.0-sidebars.json',
),
);
expect(versionsPath).toEqual(getVersionsJSONFile(versionedSiteDir));
expect(versionsPath).toEqual(
getVersionsJSONFile(versionedSiteDir, DEFAULT_PLUGIN_ID),
);
expect(versions).toEqual(['2.0.0', '1.0.1', '1.0.0', 'withSlugs']);
expect(consoleMock).toHaveBeenCalledWith('Version 2.0.0 created!');
expect(consoleMock).toHaveBeenCalledWith('[docs] Version 2.0.0 created!');
copyMock.mockRestore();
writeMock.mockRestore();
consoleMock.mockRestore();
ensureMock.mockRestore();
});
test('second docs instance versioning', () => {
const pluginId = 'community';
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: 'community',
sidebarPath: path.join(versionedSiteDir, 'community_sidebars.json'),
};
docsVersion('2.0.0', versionedSiteDir, pluginId, options);
expect(copyMock).toHaveBeenCalledWith(
path.join(versionedSiteDir, options.path),
path.join(
getVersionedDocsDir(versionedSiteDir, pluginId),
'version-2.0.0',
),
);
expect(versionedSidebar).toMatchSnapshot();
expect(versionedSidebarPath).toEqual(
path.join(
getVersionedSidebarsDir(versionedSiteDir, pluginId),
'version-2.0.0-sidebars.json',
),
);
expect(versionsPath).toEqual(
getVersionsJSONFile(versionedSiteDir, pluginId),
);
expect(versions).toEqual(['2.0.0', '1.0.0']);
expect(consoleMock).toHaveBeenCalledWith(
'[community] Version 2.0.0 created!',
);
copyMock.mockRestore();
writeMock.mockRestore();

View file

@ -14,24 +14,49 @@ import {
VERSIONED_SIDEBARS_DIR,
} from './constants';
export function getVersionedDocsDir(siteDir: string): string {
return path.join(siteDir, VERSIONED_DOCS_DIR);
import {DEFAULT_PLUGIN_ID} from '@docusaurus/core/lib/constants';
// retro-compatibility: no prefix for the default plugin id
function addPluginIdPrefix(fileOrDir: string, pluginId: string): string {
if (pluginId === DEFAULT_PLUGIN_ID) {
return fileOrDir;
} else {
return `${pluginId}_${fileOrDir}`;
}
}
export function getVersionedSidebarsDir(siteDir: string): string {
return path.join(siteDir, VERSIONED_SIDEBARS_DIR);
export function getVersionedDocsDir(siteDir: string, pluginId: string): string {
return path.join(siteDir, addPluginIdPrefix(VERSIONED_DOCS_DIR, pluginId));
}
export function getVersionsJSONFile(siteDir: string): string {
return path.join(siteDir, VERSIONS_JSON_FILE);
export function getVersionedSidebarsDir(
siteDir: string,
pluginId: string,
): string {
return path.join(
siteDir,
addPluginIdPrefix(VERSIONED_SIDEBARS_DIR, pluginId),
);
}
export function getVersionsJSONFile(siteDir: string, pluginId: string): string {
return path.join(siteDir, addPluginIdPrefix(VERSIONS_JSON_FILE, pluginId));
}
type EnvOptions = Partial<{disableVersioning: boolean}>;
export default function (
siteDir: string,
pluginId: string,
options: EnvOptions = {disableVersioning: false},
): Env {
if (!siteDir) {
throw new Error('unexpected, missing siteDir');
}
if (!pluginId) {
throw new Error('unexpected, missing pluginId');
}
const versioning: VersioningEnv = {
enabled: false,
versions: [],
@ -40,7 +65,7 @@ export default function (
sidebarsDir: '',
};
const versionsJSONFile = getVersionsJSONFile(siteDir);
const versionsJSONFile = getVersionsJSONFile(siteDir, pluginId);
if (fs.existsSync(versionsJSONFile)) {
if (!options.disableVersioning) {
const parsedVersions = JSON.parse(
@ -51,8 +76,8 @@ export default function (
versioning.latestVersion = parsedVersions[0];
versioning.enabled = true;
versioning.versions = parsedVersions;
versioning.docsDir = getVersionedDocsDir(siteDir);
versioning.sidebarsDir = getVersionedSidebarsDir(siteDir);
versioning.docsDir = getVersionedDocsDir(siteDir, pluginId);
versioning.sidebarsDir = getVersionedSidebarsDir(siteDir, pluginId);
}
}
}

View file

@ -15,7 +15,10 @@ import path from 'path';
import chalk from 'chalk';
import admonitions from 'remark-admonitions';
import {STATIC_DIR_NAME} from '@docusaurus/core/lib/constants';
import {
STATIC_DIR_NAME,
DEFAULT_PLUGIN_ID,
} from '@docusaurus/core/lib/constants';
import {
normalizeUrl,
docuHash,
@ -77,15 +80,21 @@ export default function pluginContentDocs(
const {siteDir, generatedFilesDir, baseUrl} = context;
const docsDir = path.resolve(siteDir, options.path);
const sourceToPermalink: SourceToPermalink = {};
const pluginId = options.id ?? DEFAULT_PLUGIN_ID;
const isDefaultPluginId = pluginId === DEFAULT_PLUGIN_ID;
const dataDir = path.join(
const pluginDataDirRoot = path.join(
generatedFilesDir,
'docusaurus-plugin-content-docs',
// options.id ?? 'default', // TODO support multi-instance
);
const dataDir = path.join(pluginDataDirRoot, pluginId);
const aliasedSource = (source: string) =>
`~docs/${path.relative(pluginDataDirRoot, source)}`;
// Versioning.
const env = loadEnv(siteDir, {disableVersioning: options.disableVersioning});
const env = loadEnv(siteDir, pluginId, {
disableVersioning: options.disableVersioning,
});
const {versioning} = env;
const {
versions,
@ -102,12 +111,19 @@ export default function pluginContentDocs(
},
extendCli(cli) {
const command = isDefaultPluginId
? 'docs:version'
: `docs:version:${pluginId}`;
const commandDescription = isDefaultPluginId
? 'Tag a new docs version'
: `Tag a new docs version (${pluginId})`;
cli
.command('docs:version')
.command(command)
.arguments('<version>')
.description('Tag a new version for docs')
.description(commandDescription)
.action((version) => {
docsVersion(version, siteDir, {
docsVersion(version, siteDir, pluginId, {
path: options.path,
sidebarPath: options.sidebarPath,
});
@ -335,9 +351,6 @@ Available document ids=
setGlobalData<GlobalPluginData>(pluginInstanceGlobalData);
const aliasedSource = (source: string) =>
`~docs/${path.relative(dataDir, source)}`;
const createDocsBaseMetadata = (
version: DocsVersion,
): DocsBaseMetadata => {
@ -499,7 +512,7 @@ Available document ids=
},
resolve: {
alias: {
'~docs': dataDir,
'~docs': pluginDataDirRoot,
},
},
module: {
@ -519,10 +532,10 @@ Available document ids=
metadataPath: (mdxPath: string) => {
// Note that metadataPath must be the same/in-sync as
// the path from createData for each MDX.
const aliasedSource = aliasedSitePath(mdxPath, siteDir);
const aliasedPath = aliasedSitePath(mdxPath, siteDir);
return path.join(
dataDir,
`${docuHash(aliasedSource)}.json`,
`${docuHash(aliasedPath)}.json`,
);
},
},

View file

@ -14,29 +14,36 @@ import fs from 'fs-extra';
import path from 'path';
import {Sidebar, PathOptions, SidebarItem} from './types';
import loadSidebars from './sidebars';
import {DEFAULT_PLUGIN_ID} from '@docusaurus/core/lib/constants';
// Tests depend on non-default export for mocking.
// eslint-disable-next-line import/prefer-default-export
export function docsVersion(
version: string | null | undefined,
siteDir: string,
pluginId: string,
options: PathOptions,
): void {
// It wouldn't be very user-friendly to show a [default] log prefix,
// so we use [docs] instead of [default]
const pluginIdLogPrefix =
pluginId === DEFAULT_PLUGIN_ID ? '[docs] ' : `[${pluginId}] `;
if (!version) {
throw new Error(
'No version tag specified!. Pass the version you wish to create as an argument. Ex: 1.0.0',
`${pluginIdLogPrefix}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`,
`${pluginIdLogPrefix}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',
`${pluginIdLogPrefix}Invalid version tag specified! Length must <= 32 characters. Try something like: 1.0.0`,
);
}
@ -44,19 +51,19 @@ export function docsVersion(
// sure it's a valid pathname.
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',
`${pluginIdLogPrefix}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',
`${pluginIdLogPrefix}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);
const versionsJSONFile = getVersionsJSONFile(siteDir, pluginId);
if (fs.existsSync(versionsJSONFile)) {
versions = JSON.parse(fs.readFileSync(versionsJSONFile, 'utf8'));
}
@ -64,7 +71,7 @@ export function docsVersion(
// Check if version already exists.
if (versions.includes(version)) {
throw new Error(
'This version already exists!. Use a version tag that does not already exist.',
`${pluginIdLogPrefix}This version already exists!. Use a version tag that does not already exist.`,
);
}
@ -73,11 +80,11 @@ export function docsVersion(
// Copy docs files.
const docsDir = path.join(siteDir, docsPath);
if (fs.existsSync(docsDir) && fs.readdirSync(docsDir).length > 0) {
const versionedDir = getVersionedDocsDir(siteDir);
const versionedDir = getVersionedDocsDir(siteDir, pluginId);
const newVersionDir = path.join(versionedDir, `version-${version}`);
fs.copySync(docsDir, newVersionDir);
} else {
throw new Error('There is no docs to version !');
throw new Error(`${pluginIdLogPrefix}There is no docs to version !`);
}
// Load current sidebar and create a new versioned sidebars file.
@ -109,7 +116,7 @@ export function docsVersion(
{},
);
const versionedSidebarsDir = getVersionedSidebarsDir(siteDir);
const versionedSidebarsDir = getVersionedSidebarsDir(siteDir, pluginId);
const newSidebarFile = path.join(
versionedSidebarsDir,
`version-${version}-sidebars.json`,
@ -127,5 +134,5 @@ export function docsVersion(
fs.ensureDirSync(path.dirname(versionsJSONFile));
fs.writeFileSync(versionsJSONFile, `${JSON.stringify(versions, null, 2)}\n`);
console.log(`Version ${version} created!`);
console.log(`${pluginIdLogPrefix}Version ${version} created!`);
}