feat(v2): absolute slugs and slug resolution system (#3084)

* rework slug to allow absolute slugs and slug resolution

* add slug metadata tests

* refactor docs metadata test + fix slug bugs

* fix tests

* fix docs tests failing due to randomness + update snapshot

* add test for addLeadingSlash
This commit is contained in:
Sébastien Lorber 2020-07-21 18:26:30 +02:00 committed by GitHub
parent 6730590c1e
commit f4434b2e42
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 791 additions and 255 deletions

View file

@ -0,0 +1,5 @@
---
slug: /rootAbsoluteSlug
---
Lorem

View file

@ -0,0 +1,5 @@
---
slug: rootRelativeSlug
---
Lorem

View file

@ -0,0 +1,5 @@
---
slug: ./hey/ho/../rootResolvedSlug
---
Lorem

View file

@ -0,0 +1,5 @@
---
slug: ../../../../../../../../rootTryToEscapeSlug
---
Lorem

View file

@ -0,0 +1,5 @@
---
slug: /absoluteSlug
---
Lorem

View file

@ -0,0 +1,5 @@
---
slug: relativeSlug
---
Lorem

View file

@ -0,0 +1,5 @@
---
slug: ./hey/ho/../resolvedSlug
---
Lorem

View file

@ -0,0 +1,5 @@
---
slug: ../../../../../../../../tryToEscapeSlug
---
Lorem

View file

@ -0,0 +1,5 @@
---
slug: /absoluteSlug
---
Lorem

View file

@ -0,0 +1,5 @@
---
slug: relativeSlug
---
Lorem

View file

@ -0,0 +1,5 @@
---
slug: ./hey/ho/../resolvedSlug
---
Lorem

View file

@ -0,0 +1,5 @@
---
slug: ../../../../../../../../tryToEscapeSlug
---
Lorem

View file

@ -0,0 +1,5 @@
---
slug: ./hey/ho/../rootResolvedSlug
---
Lorem

View file

@ -0,0 +1,5 @@
---
slug: ../../../../../../../../rootTryToEscapeSlug
---
Lorem

View file

@ -0,0 +1,5 @@
---
slug: ../../../../../../../../tryToEscapeSlug
---
Lorem

View file

@ -1,4 +1,5 @@
[
"1.0.1",
"1.0.0"
"1.0.0",
"withSlugs"
]

View file

@ -72,6 +72,14 @@ Array [
},
"path": "/docs/",
},
Object {
"component": "@theme/DocItem",
"exact": true,
"modules": Object {
"content": "@site/docs/slugs/absoluteSlug.md",
},
"path": "/docs/absoluteSlug",
},
Object {
"component": "@theme/DocItem",
"exact": true,
@ -88,6 +96,14 @@ Array [
},
"path": "/docs/foo/bazSlug.html",
},
Object {
"component": "@theme/DocItem",
"exact": true,
"modules": Object {
"content": "@site/docs/rootResolvedSlug.md",
},
"path": "/docs/hey/rootResolvedSlug",
},
Object {
"component": "@theme/DocItem",
"exact": true,
@ -104,6 +120,54 @@ Array [
},
"path": "/docs/lorem",
},
Object {
"component": "@theme/DocItem",
"exact": true,
"modules": Object {
"content": "@site/docs/rootAbsoluteSlug.md",
},
"path": "/docs/rootAbsoluteSlug",
},
Object {
"component": "@theme/DocItem",
"exact": true,
"modules": Object {
"content": "@site/docs/rootRelativeSlug.md",
},
"path": "/docs/rootRelativeSlug",
},
Object {
"component": "@theme/DocItem",
"exact": true,
"modules": Object {
"content": "@site/docs/rootTryToEscapeSlug.md",
},
"path": "/docs/rootTryToEscapeSlug",
},
Object {
"component": "@theme/DocItem",
"exact": true,
"modules": Object {
"content": "@site/docs/slugs/resolvedSlug.md",
},
"path": "/docs/slugs/hey/resolvedSlug",
},
Object {
"component": "@theme/DocItem",
"exact": true,
"modules": Object {
"content": "@site/docs/slugs/relativeSlug.md",
},
"path": "/docs/slugs/relativeSlug",
},
Object {
"component": "@theme/DocItem",
"exact": true,
"modules": Object {
"content": "@site/docs/slugs/tryToEscapeSlug.md",
},
"path": "/docs/tryToEscapeSlug",
},
],
},
]
@ -138,6 +202,38 @@ Object {
"id": "lorem",
"path": "/docs/lorem",
},
Object {
"id": "rootAbsoluteSlug",
"path": "/docs/rootAbsoluteSlug",
},
Object {
"id": "rootRelativeSlug",
"path": "/docs/rootRelativeSlug",
},
Object {
"id": "rootResolvedSlug",
"path": "/docs/hey/rootResolvedSlug",
},
Object {
"id": "rootTryToEscapeSlug",
"path": "/docs/rootTryToEscapeSlug",
},
Object {
"id": "slugs/absoluteSlug",
"path": "/docs/absoluteSlug",
},
Object {
"id": "slugs/relativeSlug",
"path": "/docs/slugs/relativeSlug",
},
Object {
"id": "slugs/resolvedSlug",
"path": "/docs/slugs/hey/resolvedSlug",
},
Object {
"id": "slugs/tryToEscapeSlug",
"path": "/docs/tryToEscapeSlug",
},
],
"mainDocId": "hello",
"name": null,
@ -156,7 +252,15 @@ Available document ids=
- foo/baz
- hello
- ipsum
- lorem"
- lorem
- rootAbsoluteSlug
- rootRelativeSlug
- rootResolvedSlug
- rootTryToEscapeSlug
- slugs/absoluteSlug
- slugs/relativeSlug
- slugs/resolvedSlug
- slugs/tryToEscapeSlug"
`;
exports[`versioned website content 1`] = `
@ -213,6 +317,14 @@ Array [
},
"path": "/docs/next/",
},
Object {
"component": "@theme/DocItem",
"exact": true,
"modules": Object {
"content": "@site/docs/slugs/absoluteSlug.md",
},
"path": "/docs/next/absoluteSlug",
},
Object {
"component": "@theme/DocItem",
"exact": true,
@ -221,6 +333,105 @@ Array [
},
"path": "/docs/next/foo/barSlug",
},
Object {
"component": "@theme/DocItem",
"exact": true,
"modules": Object {
"content": "@site/docs/slugs/resolvedSlug.md",
},
"path": "/docs/next/slugs/hey/resolvedSlug",
},
Object {
"component": "@theme/DocItem",
"exact": true,
"modules": Object {
"content": "@site/docs/slugs/relativeSlug.md",
},
"path": "/docs/next/slugs/relativeSlug",
},
Object {
"component": "@theme/DocItem",
"exact": true,
"modules": Object {
"content": "@site/docs/slugs/tryToEscapeSlug.md",
},
"path": "/docs/next/tryToEscapeSlug",
},
],
},
Object {
"component": "@theme/DocPage",
"exact": false,
"modules": Object {
"docsMetadata": "~docs/docs-with-slugs-route-335.json",
},
"path": "/docs/withSlugs",
"priority": undefined,
"routes": Array [
Object {
"component": "@theme/DocItem",
"exact": true,
"modules": Object {
"content": "@site/versioned_docs/version-withSlugs/slugs/absoluteSlug.md",
},
"path": "/docs/withSlugs/absoluteSlug",
},
Object {
"component": "@theme/DocItem",
"exact": true,
"modules": Object {
"content": "@site/versioned_docs/version-withSlugs/rootResolvedSlug.md",
},
"path": "/docs/withSlugs/hey/rootResolvedSlug",
},
Object {
"component": "@theme/DocItem",
"exact": true,
"modules": Object {
"content": "@site/versioned_docs/version-withSlugs/rootAbsoluteSlug.md",
},
"path": "/docs/withSlugs/rootAbsoluteSlug",
},
Object {
"component": "@theme/DocItem",
"exact": true,
"modules": Object {
"content": "@site/versioned_docs/version-withSlugs/rootRelativeSlug.md",
},
"path": "/docs/withSlugs/rootRelativeSlug",
},
Object {
"component": "@theme/DocItem",
"exact": true,
"modules": Object {
"content": "@site/versioned_docs/version-withSlugs/rootTryToEscapeSlug.md",
},
"path": "/docs/withSlugs/rootTryToEscapeSlug",
},
Object {
"component": "@theme/DocItem",
"exact": true,
"modules": Object {
"content": "@site/versioned_docs/version-withSlugs/slugs/resolvedSlug.md",
},
"path": "/docs/withSlugs/slugs/hey/resolvedSlug",
},
Object {
"component": "@theme/DocItem",
"exact": true,
"modules": Object {
"content": "@site/versioned_docs/version-withSlugs/slugs/relativeSlug.md",
},
"path": "/docs/withSlugs/slugs/relativeSlug",
},
Object {
"component": "@theme/DocItem",
"exact": true,
"modules": Object {
"content": "@site/versioned_docs/version-withSlugs/slugs/tryToEscapeSlug.md",
},
"path": "/docs/withSlugs/tryToEscapeSlug",
},
],
},
Object {
@ -270,6 +481,22 @@ Object {
"id": "hello",
"path": "/docs/next/",
},
Object {
"id": "slugs/absoluteSlug",
"path": "/docs/next/absoluteSlug",
},
Object {
"id": "slugs/relativeSlug",
"path": "/docs/next/slugs/relativeSlug",
},
Object {
"id": "slugs/resolvedSlug",
"path": "/docs/next/slugs/hey/resolvedSlug",
},
Object {
"id": "slugs/tryToEscapeSlug",
"path": "/docs/next/tryToEscapeSlug",
},
],
"mainDocId": "hello",
"name": "next",
@ -309,6 +536,45 @@ Object {
"name": "1.0.0",
"path": "/docs/1.0.0",
},
Object {
"docs": Array [
Object {
"id": "rootAbsoluteSlug",
"path": "/docs/withSlugs/rootAbsoluteSlug",
},
Object {
"id": "rootRelativeSlug",
"path": "/docs/withSlugs/rootRelativeSlug",
},
Object {
"id": "rootResolvedSlug",
"path": "/docs/withSlugs/hey/rootResolvedSlug",
},
Object {
"id": "rootTryToEscapeSlug",
"path": "/docs/withSlugs/rootTryToEscapeSlug",
},
Object {
"id": "slugs/absoluteSlug",
"path": "/docs/withSlugs/absoluteSlug",
},
Object {
"id": "slugs/relativeSlug",
"path": "/docs/withSlugs/slugs/relativeSlug",
},
Object {
"id": "slugs/resolvedSlug",
"path": "/docs/withSlugs/slugs/hey/resolvedSlug",
},
Object {
"id": "slugs/tryToEscapeSlug",
"path": "/docs/withSlugs/tryToEscapeSlug",
},
],
"mainDocId": "rootAbsoluteSlug",
"name": "withSlugs",
"path": "/docs/withSlugs",
},
],
},
},

View file

@ -21,7 +21,11 @@ describe('loadEnv', () => {
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']);
expect(env.versioning.versions).toStrictEqual([
'1.0.1',
'1.0.0',
'withSlugs',
]);
});
test('website with versioning but disabled', () => {

View file

@ -269,8 +269,10 @@ describe('versioned website', () => {
"docs/**/*.{md,mdx}",
"versioned_sidebars/version-1.0.1-sidebars.json",
"versioned_sidebars/version-1.0.0-sidebars.json",
"versioned_sidebars/version-withSlugs-sidebars.json",
"versioned_docs/version-1.0.1/**/*.{md,mdx}",
"versioned_docs/version-1.0.0/**/*.{md,mdx}",
"versioned_docs/version-withSlugs/**/*.{md,mdx}",
"sidebars.json",
]
`);

View file

@ -9,136 +9,150 @@ import path from 'path';
import {loadContext} from '@docusaurus/core/src/server/index';
import processMetadata from '../metadata';
import loadEnv from '../env';
import {MetadataRaw, Env, MetadataOptions} from '../types';
import {LoadContext} from '@docusaurus/types';
const fixtureDir = path.join(__dirname, '__fixtures__');
describe('simple site', () => {
const simpleSiteDir = path.join(fixtureDir, 'simple-site');
const context = loadContext(simpleSiteDir);
const routeBasePath = 'docs';
const docsDir = path.resolve(simpleSiteDir, routeBasePath);
function createTestHelpers({
siteDir,
context,
env,
options,
}: {
siteDir: string;
context: LoadContext;
env: Env;
options: MetadataOptions;
}) {
async function testMeta(
refDir: string,
source: string,
expectedMetadata: Omit<MetadataRaw, 'source'>,
) {
const metadata = await processMetadata({
source,
refDir,
context,
options,
env,
});
expect(metadata).toEqual({
...expectedMetadata,
source: path.join('@site', path.relative(siteDir, refDir), source),
});
}
const env = loadEnv(simpleSiteDir);
async function testSlug(
refDir: string,
source: string,
expectedPermalink: string,
) {
const metadata = await processMetadata({
source,
refDir,
context,
options,
env,
});
expect(metadata.permalink).toEqual(expectedPermalink);
}
return {testMeta, testSlug};
}
describe('simple site', () => {
const siteDir = path.join(fixtureDir, 'simple-site');
const context = loadContext(siteDir);
const routeBasePath = 'docs';
const docsDir = path.resolve(siteDir, routeBasePath);
const env = loadEnv(siteDir);
const options = {routeBasePath};
const {testMeta, testSlug} = createTestHelpers({
siteDir,
context,
options,
env,
});
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,
refDir: docsDir,
context,
options,
env,
}),
processMetadata({
source: sourceB,
refDir: docsDir,
context,
options,
env,
}),
]);
expect(dataA).toEqual({
await testMeta(docsDir, path.join('foo', 'bar.md'), {
id: 'foo/bar',
unversionedId: 'foo/bar',
isDocsHomePage: false,
permalink: '/docs/foo/bar',
source: path.join('@site', routeBasePath, sourceA),
title: 'Bar',
description: 'This is custom description',
});
expect(dataB).toEqual({
await testMeta(docsDir, path.join('hello.md'), {
id: 'hello',
unversionedId: 'hello',
isDocsHomePage: false,
permalink: '/docs/hello',
source: path.join('@site', routeBasePath, sourceB),
title: 'Hello, World !',
description: `Hi, Endilie here :)`,
});
});
test('homePageId doc', async () => {
const source = path.join('hello.md');
const options = {
routeBasePath,
homePageId: 'hello',
};
const data = await processMetadata({
source,
refDir: docsDir,
const {testMeta: testMetaLocal} = createTestHelpers({
siteDir,
options: {
routeBasePath,
homePageId: 'hello',
},
context,
options,
env,
});
expect(data).toEqual({
await testMetaLocal(docsDir, path.join('hello.md'), {
id: 'hello',
unversionedId: 'hello',
isDocsHomePage: true,
permalink: '/docs/',
source: path.join('@site', routeBasePath, source),
title: 'Hello, World !',
description: `Hi, Endilie here :)`,
});
});
test('homePageId doc nested', async () => {
const source = path.join('foo', 'bar.md');
const options = {
routeBasePath,
homePageId: 'foo/bar',
};
const data = await processMetadata({
source,
refDir: docsDir,
const {testMeta: testMetaLocal} = createTestHelpers({
siteDir,
options: {
routeBasePath,
homePageId: 'foo/bar',
},
context,
options,
env,
});
expect(data).toEqual({
await testMetaLocal(docsDir, path.join('foo', 'bar.md'), {
id: 'foo/bar',
unversionedId: 'foo/bar',
isDocsHomePage: true,
permalink: '/docs/',
source: path.join('@site', routeBasePath, source),
title: 'Bar',
description: 'This is custom description',
});
});
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,
refDir: docsDir,
const {testMeta: testMetaLocal} = createTestHelpers({
siteDir,
options: {
routeBasePath,
editUrl: 'https://github.com/facebook/docusaurus/edit/master/website',
},
context,
options,
env,
});
expect(data).toEqual({
await testMetaLocal(docsDir, path.join('foo', 'baz.md'), {
id: 'foo/baz',
unversionedId: 'foo/baz',
isDocsHomePage: false,
permalink: '/docs/foo/bazSlug.html',
source: path.join('@site', routeBasePath, source),
title: 'baz',
editUrl:
'https://github.com/facebook/docusaurus/edit/master/website/docs/foo/baz.md',
@ -147,57 +161,34 @@ describe('simple site', () => {
});
test('docs with custom editUrl & unrelated frontmatter', async () => {
const source = 'lorem.md';
const options = {
routeBasePath,
};
const data = await processMetadata({
source,
refDir: docsDir,
context,
options,
env,
});
expect(data).toEqual({
await testMeta(docsDir, 'lorem.md', {
id: 'lorem',
unversionedId: 'lorem',
isDocsHomePage: false,
permalink: '/docs/lorem',
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
// @ts-expect-error: It doesn't exist, so the test will show it's undefined.
expect(data.unrelated_frontmatter).toBeUndefined();
});
test('docs with last update time and author', async () => {
const source = 'lorem.md';
const options = {
routeBasePath,
showLastUpdateAuthor: true,
showLastUpdateTime: true,
};
const data = await processMetadata({
source,
refDir: docsDir,
const {testMeta: testMetaLocal} = createTestHelpers({
siteDir,
options: {
routeBasePath,
showLastUpdateAuthor: true,
showLastUpdateTime: true,
},
context,
options,
env,
});
expect(data).toEqual({
await testMetaLocal(docsDir, 'lorem.md', {
id: 'lorem',
unversionedId: 'lorem',
isDocsHomePage: false,
permalink: '/docs/lorem',
source: path.join('@site', routeBasePath, source),
title: 'lorem',
editUrl: 'https://github.com/customUrl/docs/lorem.md',
description: 'Lorem ipsum.',
@ -207,27 +198,22 @@ describe('simple site', () => {
});
test('docs with null custom_edit_url', async () => {
const source = 'ipsum.md';
const options = {
routeBasePath,
showLastUpdateAuthor: true,
showLastUpdateTime: true,
};
const data = await processMetadata({
source,
refDir: docsDir,
const {testMeta: testMetaLocal} = createTestHelpers({
siteDir,
options: {
routeBasePath,
showLastUpdateAuthor: true,
showLastUpdateTime: true,
},
context,
options,
env,
});
expect(data).toEqual({
await testMetaLocal(docsDir, 'ipsum.md', {
id: 'ipsum',
unversionedId: 'ipsum',
isDocsHomePage: false,
permalink: '/docs/ipsum',
source: path.join('@site', routeBasePath, source),
title: 'ipsum',
editUrl: null,
description: 'Lorem ipsum.',
@ -236,18 +222,61 @@ describe('simple site', () => {
});
});
test('docs with slugs', async () => {
await testSlug(
docsDir,
path.join('rootRelativeSlug.md'),
'/docs/rootRelativeSlug',
);
await testSlug(
docsDir,
path.join('rootAbsoluteSlug.md'),
'/docs/rootAbsoluteSlug',
);
await testSlug(
docsDir,
path.join('rootResolvedSlug.md'),
'/docs/hey/rootResolvedSlug',
);
await testSlug(
docsDir,
path.join('rootTryToEscapeSlug.md'),
'/docs/rootTryToEscapeSlug',
);
await testSlug(
docsDir,
path.join('slugs', 'absoluteSlug.md'),
'/docs/absoluteSlug',
);
await testSlug(
docsDir,
path.join('slugs', 'relativeSlug.md'),
'/docs/slugs/relativeSlug',
);
await testSlug(
docsDir,
path.join('slugs', 'resolvedSlug.md'),
'/docs/slugs/hey/resolvedSlug',
);
await testSlug(
docsDir,
path.join('slugs', 'tryToEscapeSlug.md'),
'/docs/tryToEscapeSlug',
);
});
test('docs with invalid id', async () => {
const badSiteDir = path.join(fixtureDir, 'bad-id-site');
const options = {
routeBasePath,
};
await expect(
processMetadata({
source: 'invalid-id.md',
refDir: path.join(badSiteDir, 'docs'),
context,
options,
options: {
routeBasePath,
},
env,
}),
).rejects.toThrowErrorMatchingInlineSnapshot(
@ -255,38 +284,18 @@ describe('simple site', () => {
);
});
test('docs with invalid slug', async () => {
const badSiteDir = path.join(fixtureDir, 'bad-slug-site');
const options = {
routeBasePath,
};
await expect(
processMetadata({
source: 'invalid-slug.md',
refDir: path.join(badSiteDir, 'docs'),
context,
options,
env,
}),
).rejects.toThrowErrorMatchingInlineSnapshot(
`"Document slug cannot include \\"/\\"."`,
);
});
test('docs with slug on doc home', async () => {
const badSiteDir = path.join(fixtureDir, 'bad-slug-on-doc-home-site');
const options = {
routeBasePath,
homePageId: 'docWithSlug',
};
await expect(
processMetadata({
source: 'docWithSlug.md',
refDir: path.join(badSiteDir, 'docs'),
context,
options,
options: {
routeBasePath,
homePageId: 'docWithSlug',
},
env,
}),
).rejects.toThrowErrorMatchingInlineSnapshot(
@ -302,47 +311,30 @@ describe('versioned site', () => {
const docsDir = path.resolve(siteDir, routeBasePath);
const env = loadEnv(siteDir);
const {docsDir: versionedDir} = env.versioning;
const options = {routeBasePath};
test('master/next docs', async () => {
const sourceA = path.join('foo', 'bar.md');
const sourceB = path.join('hello.md');
const options = {
routeBasePath,
};
const {testMeta, testSlug} = createTestHelpers({
siteDir,
context,
options,
env,
});
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({
test('next docs', async () => {
await testMeta(docsDir, path.join('foo', 'bar.md'), {
id: 'foo/bar',
unversionedId: 'foo/bar',
isDocsHomePage: false,
permalink: '/docs/next/foo/barSlug',
source: path.join('@site', routeBasePath, sourceA),
title: 'bar',
description: 'This is next version of bar.',
version: 'next',
});
expect(dataB).toEqual({
await testMeta(docsDir, path.join('hello.md'), {
id: 'hello',
unversionedId: 'hello',
isDocsHomePage: false,
permalink: '/docs/next/hello',
source: path.join('@site', routeBasePath, sourceB),
title: 'hello',
description: 'Hello next !',
version: 'next',
@ -350,84 +342,108 @@ describe('versioned site', () => {
});
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({
await testMeta(versionedDir, path.join('version-1.0.0', 'foo', 'bar.md'), {
id: 'version-1.0.0/foo/bar',
unversionedId: 'foo/bar',
isDocsHomePage: false,
permalink: '/docs/1.0.0/foo/barSlug',
source: path.join('@site', path.relative(siteDir, versionedDir), sourceA),
title: 'bar',
description: 'Bar 1.0.0 !',
version: '1.0.0',
});
expect(dataB).toEqual({
await testMeta(versionedDir, path.join('version-1.0.0', 'hello.md'), {
id: 'version-1.0.0/hello',
unversionedId: 'hello',
isDocsHomePage: false,
permalink: '/docs/1.0.0/hello',
source: path.join('@site', path.relative(siteDir, versionedDir), sourceB),
title: 'hello',
description: 'Hello 1.0.0 !',
version: '1.0.0',
});
expect(dataC).toEqual({
await testMeta(versionedDir, path.join('version-1.0.1', 'foo', 'bar.md'), {
id: 'version-1.0.1/foo/bar',
unversionedId: 'foo/bar',
isDocsHomePage: false,
permalink: '/docs/foo/bar',
source: path.join('@site', path.relative(siteDir, versionedDir), sourceC),
title: 'bar',
description: 'Bar 1.0.1 !',
version: '1.0.1',
});
expect(dataD).toEqual({
await testMeta(versionedDir, path.join('version-1.0.1', 'hello.md'), {
id: 'version-1.0.1/hello',
unversionedId: 'hello',
isDocsHomePage: false,
permalink: '/docs/hello',
source: path.join('@site', path.relative(siteDir, versionedDir), sourceD),
title: 'hello',
description: 'Hello 1.0.1 !',
version: '1.0.1',
});
});
test('next doc slugs', async () => {
await testSlug(
docsDir,
path.join('slugs', 'absoluteSlug.md'),
'/docs/next/absoluteSlug',
);
await testSlug(
docsDir,
path.join('slugs', 'relativeSlug.md'),
'/docs/next/slugs/relativeSlug',
);
await testSlug(
docsDir,
path.join('slugs', 'resolvedSlug.md'),
'/docs/next/slugs/hey/resolvedSlug',
);
await testSlug(
docsDir,
path.join('slugs', 'tryToEscapeSlug.md'),
'/docs/next/tryToEscapeSlug',
);
});
test('versioned doc slugs', async () => {
await testSlug(
versionedDir,
path.join('version-withSlugs', 'rootAbsoluteSlug.md'),
'/docs/withSlugs/rootAbsoluteSlug',
);
await testSlug(
versionedDir,
path.join('version-withSlugs', 'rootRelativeSlug.md'),
'/docs/withSlugs/rootRelativeSlug',
);
await testSlug(
versionedDir,
path.join('version-withSlugs', 'rootResolvedSlug.md'),
'/docs/withSlugs/hey/rootResolvedSlug',
);
await testSlug(
versionedDir,
path.join('version-withSlugs', 'rootTryToEscapeSlug.md'),
'/docs/withSlugs/rootTryToEscapeSlug',
);
await testSlug(
versionedDir,
path.join('version-withSlugs', 'slugs', 'absoluteSlug.md'),
'/docs/withSlugs/absoluteSlug',
);
await testSlug(
versionedDir,
path.join('version-withSlugs', 'slugs', 'relativeSlug.md'),
'/docs/withSlugs/slugs/relativeSlug',
);
await testSlug(
versionedDir,
path.join('version-withSlugs', 'slugs', 'resolvedSlug.md'),
'/docs/withSlugs/slugs/hey/resolvedSlug',
);
await testSlug(
versionedDir,
path.join('version-withSlugs', 'slugs', 'tryToEscapeSlug.md'),
'/docs/withSlugs/tryToEscapeSlug',
);
});
});

View file

@ -0,0 +1,85 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import getSlug from '../slug';
describe('getSlug', () => {
test('should default to dirname/id', () => {
expect(getSlug({baseID: 'doc', dirName: '/dir'})).toEqual('/dir/doc');
expect(getSlug({baseID: 'doc', dirName: '/dir/subdir'})).toEqual(
'/dir/subdir/doc',
);
});
test('should handle current dir', () => {
expect(getSlug({baseID: 'doc', dirName: '.'})).toEqual('/doc');
expect(getSlug({baseID: 'doc', dirName: '/'})).toEqual('/doc');
});
test('should resolve absolute slug frontmatter', () => {
expect(
getSlug({baseID: 'any', dirName: '.', frontmatterSlug: '/abc/def'}),
).toEqual('/abc/def');
expect(
getSlug({baseID: 'any', dirName: './any', frontmatterSlug: '/abc/def'}),
).toEqual('/abc/def');
expect(
getSlug({
baseID: 'any',
dirName: './any/any',
frontmatterSlug: '/abc/def',
}),
).toEqual('/abc/def');
});
test('should resolve relative slug frontmatter', () => {
expect(
getSlug({baseID: 'any', dirName: '.', frontmatterSlug: 'abc/def'}),
).toEqual('/abc/def');
expect(
getSlug({baseID: 'any', dirName: '/dir', frontmatterSlug: 'abc/def'}),
).toEqual('/dir/abc/def');
expect(
getSlug({
baseID: 'any',
dirName: 'unslashedDir',
frontmatterSlug: 'abc/def',
}),
).toEqual('/unslashedDir/abc/def');
expect(
getSlug({
baseID: 'any',
dirName: 'dir/subdir',
frontmatterSlug: 'abc/def',
}),
).toEqual('/dir/subdir/abc/def');
expect(
getSlug({baseID: 'any', dirName: '/dir', frontmatterSlug: './abc/def'}),
).toEqual('/dir/abc/def');
expect(
getSlug({
baseID: 'any',
dirName: '/dir',
frontmatterSlug: './abc/../def',
}),
).toEqual('/dir/def');
expect(
getSlug({
baseID: 'any',
dirName: '/dir/subdir',
frontmatterSlug: '../abc/def',
}),
).toEqual('/dir/abc/def');
expect(
getSlug({
baseID: 'any',
dirName: '/dir/subdir',
frontmatterSlug: '../../../../../abc/../def',
}),
).toEqual('/def');
});
});

View file

@ -187,7 +187,7 @@ describe('docsVersion', () => {
),
);
expect(versionsPath).toEqual(getVersionsJSONFile(versionedSiteDir));
expect(versions).toEqual(['2.0.0', '1.0.1', '1.0.0']);
expect(versions).toEqual(['2.0.0', '1.0.1', '1.0.0', 'withSlugs']);
expect(consoleMock).toHaveBeenCalledWith('Version 2.0.0 created!');
copyMock.mockRestore();

View file

@ -436,7 +436,10 @@ Available document ids=
// to be by version and pick only needed base metadata.
if (versioning.enabled) {
const docsMetadataByVersion = groupBy(
Object.values(content.docsMetadata),
// sort to ensure consistent output for tests
Object.values(content.docsMetadata).sort((a, b) =>
a.id.localeCompare(b.id),
),
'version',
);

View file

@ -22,9 +22,11 @@ import {
Env,
VersioningEnv,
} from './types';
import getSlug from './slug';
import {escapeRegExp} from 'lodash';
function removeVersionPrefix(str: string, version: string): string {
return str.replace(new RegExp(`^version-${version}/`), '');
return str.replace(new RegExp(`^version-${escapeRegExp(version)}/?`), '');
}
function inferVersion(
@ -102,8 +104,12 @@ export default async function processMetadata({
const fileMarkdownPromise = parseMarkdownFile(filePath);
const lastUpdatedPromise = lastUpdated(filePath, options);
const dirName = path.dirname(source);
const version = inferVersion(dirName, versioning);
const dirNameWithVersion = path.dirname(source); // ex: version-1.0.0/foo
const version = inferVersion(dirNameWithVersion, versioning); // ex: 1.0.0
const dirNameWithoutVersion = // ex: foo
version && version !== 'next'
? removeVersionPrefix(dirNameWithVersion, version)
: dirNameWithVersion;
// The version portion of the url path. Eg: 'next', '1.0.0', and ''.
const versionPath =
@ -122,7 +128,9 @@ export default async function processMetadata({
if (baseID.includes('/')) {
throw new Error('Document id cannot include "/".');
}
const id = dirName !== '.' ? `${dirName}/${baseID}` : baseID;
const id =
dirNameWithVersion !== '.' ? `${dirNameWithVersion}/${baseID}` : baseID;
const unversionedId = version ? removeVersionPrefix(id, version) : id;
const isDocsHomePage = unversionedId === homePageId;
@ -132,34 +140,24 @@ export default async function processMetadata({
);
}
const baseSlug: string = frontMatter.slug || baseID;
if (baseSlug.includes('/')) {
throw new Error('Document slug cannot include "/".');
}
const slug = dirName !== '.' ? `${dirName}/${baseSlug}` : baseSlug;
const docSlug = isDocsHomePage
? '/'
: getSlug({
baseID,
dirName: dirNameWithoutVersion,
frontmatterSlug: frontMatter.slug,
});
// Default title is the id.
const title: string = frontMatter.title || baseID;
const description: string = frontMatter.description || excerpt;
// The last portion of the url path. Eg: 'foo/bar', 'bar'.
let routePath;
if (isDocsHomePage) {
// TODO can we remove this trailing / ?
// Seems it's not that easy...
// Related to https://github.com/facebook/docusaurus/issues/2917
routePath = '/';
} else {
routePath =
version && version !== 'next' ? removeVersionPrefix(slug, version) : slug;
}
const permalink = normalizeUrl([
baseUrl,
routeBasePath,
versionPath,
routePath,
docSlug,
]);
const {lastUpdatedAt, lastUpdatedBy} = await lastUpdatedPromise;

View file

@ -0,0 +1,41 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {
addLeadingSlash,
addTrailingSlash,
isValidPathname,
resolvePathname,
} from '@docusaurus/utils';
export default function getSlug({
baseID,
frontmatterSlug,
dirName,
}: {
baseID: string;
frontmatterSlug?: string;
dirName: string;
}) {
const baseSlug: string = frontmatterSlug || baseID;
let slug: string;
if (baseSlug.startsWith('/')) {
slug = baseSlug;
} else {
const resolveDirname =
dirName === '.' ? '/' : addLeadingSlash(addTrailingSlash(dirName));
slug = resolvePathname(baseSlug, resolveDirname);
}
if (!isValidPathname(slug)) {
throw new Error(
`Unable to resolve valid document slug. Maybe your slug frontmatter is incorrect? Doc id=${baseID} / dirName=${dirName} / frontmatterSlug=${frontmatterSlug} => bad result slug=${slug}`,
);
}
return slug;
}

View file

@ -126,7 +126,7 @@ export interface MetadataRaw extends LastUpdateData {
source: string;
permalink: string;
sidebar_label?: string;
editUrl?: string;
editUrl?: string | null;
version?: string;
}