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

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