refactor(v2): add typing for docs plugin (#1811)

* refactor(v2): add typing for docs plugin

* nits
This commit is contained in:
Endi 2019-10-07 18:28:33 +07:00 committed by GitHub
parent f671e6b437
commit 1591128cdd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 304 additions and 131 deletions

View file

@ -14,3 +14,4 @@ packages/docusaurus-utils/lib/
packages/docusaurus/lib/
packages/docusaurus-init/lib/
packages/docusaurus-plugin-content-blog/lib/
packages/docusaurus-plugin-content-docs-legacy/lib/

1
.gitignore vendored
View file

@ -16,3 +16,4 @@ packages/docusaurus-utils/lib/
packages/docusaurus/lib/
packages/docusaurus-init/lib/
packages/docusaurus-plugin-content-blog/lib/
packages/docusaurus-plugin-content-docs-legacy/lib/

View file

@ -6,3 +6,4 @@ packages/docusaurus-utils/lib/
packages/docusaurus/lib/
packages/docusaurus-init/lib/
packages/docusaurus-plugin-content-blog/lib/
packages/docusaurus-plugin-content-docs-legacy/lib/

View file

@ -18,6 +18,7 @@ module.exports = {
'/packages/docusaurus/lib',
'/packages/docusaurus-utils/lib',
'/packages/docusaurus-plugin-content-blog/lib',
'/packages/docusaurus-plugin-content-docs-legacy/lib',
],
transform: {
'^.+\\.[jt]sx?$': 'babel-jest',

View file

@ -28,6 +28,7 @@
"@types/fs-extra": "8.0.0",
"@types/inquirer": "^6.0.3",
"@types/jest": "^24.0.15",
"@types/loader-utils": "^1.1.3",
"@types/lodash": "^4.14.136",
"@types/lodash.kebabcase": "^4.1.6",
"@types/node": "^12.0.2",

View file

@ -2,17 +2,23 @@
"name": "@docusaurus/plugin-content-docs-legacy",
"version": "2.0.0-alpha.25",
"description": "Documentation plugin for legacy v1 Docusaurus docs",
"main": "src/index.js",
"main": "lib/index.js",
"scripts": {
"tsc": "tsc"
},
"publishConfig": {
"access": "public"
},
"license": "MIT",
"devDependencies": {
"@docusaurus/types": "^2.0.0-alpha.25"
},
"dependencies": {
"@docusaurus/mdx-loader": "^2.0.0-alpha.25",
"@docusaurus/utils": "^2.0.0-alpha.25",
"fs-extra": "^8.1.0",
"globby": "^10.0.1",
"import-fresh": "^3.0.0",
"import-fresh": "^3.1.0",
"loader-utils": "^1.2.3"
},
"peerDependencies": {

View file

@ -7,6 +7,7 @@
import path from 'path';
import pluginContentDocs from '../index';
import {LoadContext} from '@docusaurus/types';
describe('loadDocs', () => {
test('simple website', async () => {
@ -16,18 +17,16 @@ describe('loadDocs', () => {
baseUrl: '/',
url: 'https://docusaurus.io',
};
const context = {
siteDir,
siteConfig,
} as LoadContext;
const sidebarPath = path.join(siteDir, 'sidebars.json');
const pluginPath = 'docs';
const plugin = pluginContentDocs(
{
siteDir,
siteConfig,
},
{
path: 'docs',
sidebarPath,
},
);
const plugin = pluginContentDocs(context, {
path: 'docs',
sidebarPath,
});
const {docs: docsMetadata} = await plugin.loadContent();
expect(docsMetadata.hello).toEqual({

View file

@ -5,38 +5,49 @@
* LICENSE file in the root directory of this source tree.
*/
const globby = require('globby');
const fs = require('fs');
const path = require('path');
const {idx, normalizeUrl, docuHash} = require('@docusaurus/utils');
import globby from 'globby';
import fs from 'fs-extra';
import path from 'path';
import {idx, normalizeUrl, docuHash} from '@docusaurus/utils';
const createOrder = require('./order');
const loadSidebars = require('./sidebars');
const processMetadata = require('./metadata');
import createOrder from './order';
import loadSidebars from './sidebars';
import processMetadata from './metadata';
import {LoadContext, Plugin, DocusaurusConfig} from '@docusaurus/types';
import {
PluginOptions,
Sidebar,
Order,
Metadata,
DocsMetadata,
LoadedContent,
SourceToPermalink,
PermalinkToId,
} from './types';
import {Configuration} from 'webpack';
const DEFAULT_OPTIONS = {
const DEFAULT_OPTIONS: PluginOptions = {
path: 'docs', // Path to data on filesystem, relative to site dir.
routeBasePath: 'docs', // URL Route.
include: ['**/*.md', '**/*.mdx'], // Extensions to include.
// TODO: Change format to array.
sidebarPath: '', // Path to sidebar configuration for showing a list of markdown pages.
// TODO: Settle themeing.
docLayoutComponent: '@theme/DocLegacyPage',
docItemComponent: '@theme/DocLegacyItem',
remarkPlugins: [],
rehypePlugins: [],
};
module.exports = function(context, opts) {
export default function pluginContentDocs(
context: LoadContext,
opts: Partial<PluginOptions>,
): Plugin<LoadedContent | null> {
const options = {...DEFAULT_OPTIONS, ...opts};
const contentPath = path.resolve(context.siteDir, options.path);
let globalContents = {};
let sourceToPermalink: SourceToPermalink = {};
return {
name: 'docusaurus-plugin-content-docs',
contentPath,
getPathsToWatch() {
const {include = []} = options;
const globPattern = include.map(pattern => `${contentPath}/${pattern}`);
@ -53,13 +64,13 @@ module.exports = function(context, opts) {
return null;
}
const docsSidebars = loadSidebars(sidebarPath);
const docsSidebars: Sidebar = loadSidebars(sidebarPath);
// Build the docs ordering such as next, previous, category and sidebar
const order = createOrder(docsSidebars);
const order: Order = createOrder(docsSidebars);
// Prepare metadata container.
const docs = {};
const docs: DocsMetadata = {};
// Metadata for default docs files.
const docsFiles = await globby(include, {
@ -67,7 +78,7 @@ module.exports = function(context, opts) {
});
await Promise.all(
docsFiles.map(async source => {
const metadata = await processMetadata(
const metadata: Metadata = await processMetadata(
source,
docsDir,
order,
@ -93,22 +104,19 @@ module.exports = function(context, opts) {
}
});
const sourceToPermalink = {};
const permalinkToId = {};
const permalinkToId: PermalinkToId = {};
Object.values(docs).forEach(({id, source, permalink}) => {
sourceToPermalink[source] = permalink;
permalinkToId[permalink] = id;
});
globalContents = {
return {
docs,
docsDir,
docsSidebars,
sourceToPermalink,
permalinkToId,
};
return globalContents;
},
async contentLoaded({content, actions}) {
@ -138,7 +146,7 @@ module.exports = function(context, opts) {
);
const docsBaseRoute = normalizeUrl([
context.siteConfig.baseUrl,
(context.siteConfig as DocusaurusConfig).baseUrl,
routeBasePath,
]);
const docsMetadataPath = await createData(
@ -156,7 +164,8 @@ module.exports = function(context, opts) {
});
},
configureWebpack(config, isServer, {getBabelLoader, getCacheLoader}) {
configureWebpack(_, isServer, utils) {
const {getBabelLoader, getCacheLoader} = utils;
const {rehypePlugins, remarkPlugins} = options;
return {
module: {
@ -179,15 +188,15 @@ module.exports = function(context, opts) {
options: {
siteConfig: context.siteConfig,
siteDir: context.siteDir,
docsDir: globalContents.docsDir,
sourceToPermalink: globalContents.sourceToPermalink,
docsDir: contentPath,
sourceToPermalink: sourceToPermalink,
},
},
].filter(Boolean),
},
],
},
};
} as Configuration;
},
};
};
}

View file

@ -5,11 +5,12 @@
* LICENSE file in the root directory of this source tree.
*/
const path = require('path');
const {getOptions} = require('loader-utils');
const {resolve} = require('url');
import path from 'path';
import {getOptions} from 'loader-utils';
import {resolve} from 'url';
import {loader} from 'webpack';
module.exports = async function(fileString) {
export = function(fileString: string) {
const callback = this.async();
const options = Object.assign({}, getOptions(this), {
filepath: this.resourcePath,
@ -17,7 +18,7 @@ module.exports = async function(fileString) {
const {docsDir, siteDir, sourceToPermalink} = options;
// Determine the source dir. e.g: /docs, /website/versioned_docs/version-1.0.0
let sourceDir;
let sourceDir: string | undefined;
const thisSource = this.resourcePath;
if (thisSource.startsWith(docsDir)) {
sourceDir = docsDir;
@ -44,7 +45,7 @@ module.exports = async function(fileString) {
// Replace it to correct html link.
const mdLink = mdMatch[1];
const targetSource = `${sourceDir}/${mdLink}`;
const aliasedSource = source =>
const aliasedSource = (source: string) =>
`@site/${path.relative(siteDir, source)}`;
const permalink =
sourceToPermalink[aliasedSource(resolve(thisSource, mdLink))] ||
@ -59,5 +60,5 @@ module.exports = async function(fileString) {
content = lines.join('\n');
}
return callback(null, content);
};
return callback && callback(null, content);
} as loader.Loader;

View file

@ -5,18 +5,20 @@
* LICENSE file in the root directory of this source tree.
*/
const fs = require('fs-extra');
const path = require('path');
const {parse, normalizeUrl} = require('@docusaurus/utils');
import fs from 'fs-extra';
import path from 'path';
import {parse, normalizeUrl} from '@docusaurus/utils';
import {Order, MetadataRaw} from './types';
import {DocusaurusConfig} from '@docusaurus/types';
module.exports = async function processMetadata(
source,
docsDir,
order,
siteConfig,
docsBasePath,
siteDir,
) {
export default async function processMetadata(
source: string,
docsDir: string,
order: Order,
siteConfig: Partial<DocusaurusConfig>,
docsBasePath: string,
siteDir: string,
): Promise<MetadataRaw> {
const filepath = path.join(docsDir, source);
const fileString = await fs.readFile(filepath, 'utf-8');
@ -82,5 +84,5 @@ module.exports = async function processMetadata(
}
}
return metadata;
};
return metadata as MetadataRaw;
}

View file

@ -5,24 +5,42 @@
* LICENSE file in the root directory of this source tree.
*/
import {
Sidebar,
SidebarItem,
SidebarItemDoc,
SidebarItemCategory,
Order,
} from './types';
// Build the docs meta such as next, previous, category and sidebar.
module.exports = function createOrder(allSidebars = {}) {
const order = {};
export default function createOrder(allSidebars: Sidebar = {}): Order {
const order: Order = {};
Object.keys(allSidebars).forEach(sidebarId => {
const sidebar = allSidebars[sidebarId];
const ids = [];
const categoryOrder = [];
const subCategoryOrder = [];
const indexItems = ({items, categoryLabel, subCategoryLabel}) => {
const ids: string[] = [];
const categoryOrder: (string | undefined)[] = [];
const subCategoryOrder: (string | undefined)[] = [];
const indexItems = ({
items,
categoryLabel,
subCategoryLabel,
}: {
items: SidebarItem[];
categoryLabel?: string;
subCategoryLabel?: string;
}) => {
items.forEach(item => {
switch (item.type) {
case 'category':
indexItems({
items: item.items,
categoryLabel: categoryLabel || item.label,
subCategoryLabel: categoryLabel && item.label,
items: (item as SidebarItemCategory).items,
categoryLabel:
categoryLabel || (item as SidebarItemCategory).label,
subCategoryLabel:
categoryLabel && (item as SidebarItemCategory).label,
});
break;
case 'ref':
@ -30,7 +48,7 @@ module.exports = function createOrder(allSidebars = {}) {
// Refs and links should not be shown in navigation.
break;
case 'doc':
ids.push(item.id);
ids.push((item as SidebarItemDoc).id);
categoryOrder.push(categoryLabel);
subCategoryOrder.push(subCategoryLabel);
break;
@ -69,4 +87,4 @@ module.exports = function createOrder(allSidebars = {}) {
});
return order;
};
}

View file

@ -5,16 +5,20 @@
* LICENSE file in the root directory of this source tree.
*/
const fs = require('fs');
const importFresh = require('import-fresh');
import fs from 'fs';
import importFresh from 'import-fresh';
import {
SidebarItemCategory,
Sidebar,
SidebarRaw,
SidebarItem,
SidebarItemCategoryRaw,
} from './types';
/**
* Check that item contains only allowed keys
*
* @param {Object} item
* @param {Array<string>} keys
*/
function assertItem(item, keys) {
function assertItem(item: Object, keys: string[]): void {
const unknownKeys = Object.keys(item).filter(
key => !keys.includes(key) && key !== 'type',
);
@ -31,13 +35,11 @@ function assertItem(item, keys) {
/**
* Normalizes recursively category and all its children. Ensures, that at the end
* each item will be an object with the corresponding type
*
* @param {Array<Object>} category
* @param {number} [level=0]
*
* @return {Array<Object>}
*/
function normalizeCategory(category, level = 0) {
function normalizeCategory(
category: SidebarItemCategoryRaw,
level = 0,
): SidebarItemCategory {
if (level === 2) {
throw new Error(
`Can not process ${
@ -54,33 +56,32 @@ function normalizeCategory(category, level = 0) {
);
}
const items = category.items.map(item => {
const items: SidebarItem[] = category.items.map(item => {
if (typeof item === 'string') {
return {
type: 'doc',
id: item,
};
}
switch (item.type) {
case 'category':
return normalizeCategory(item, level + 1);
return normalizeCategory(item as SidebarItemCategoryRaw, level + 1);
case 'link':
assertItem(item, ['href', 'label']);
break;
case 'ref':
assertItem(item, ['id', 'label']);
assertItem(item, ['id']);
break;
default:
if (typeof item === 'string') {
return {
type: 'doc',
id: item,
};
}
if (item.type !== 'doc') {
throw new Error(`Unknown sidebar item type: ${item.type}`);
}
assertItem(item, ['id', 'label']);
assertItem(item, ['id']);
break;
}
return item;
return item as SidebarItem;
});
return {...category, items};
@ -88,35 +89,36 @@ function normalizeCategory(category, level = 0) {
/**
* Converts sidebars object to mapping to arrays of sidebar item objects
*
* @param {{[key: string]: Object}} sidebars
*
* @return {{[key: string]: Array<Object>}}
*/
function normalizeSidebar(sidebars) {
return Object.entries(sidebars).reduce((acc, [sidebarId, sidebar]) => {
let normalizedSidebar = sidebar;
function normalizeSidebar(sidebars: SidebarRaw): Sidebar {
return Object.entries(sidebars).reduce(
(acc: Sidebar, [sidebarId, sidebar]) => {
let normalizedSidebar: SidebarItemCategoryRaw[];
if (!Array.isArray(sidebar)) {
// convert sidebar to a more generic structure
normalizedSidebar = Object.entries(sidebar).map(([label, items]) => ({
type: 'category',
label,
items,
}));
}
if (!Array.isArray(sidebar)) {
// convert sidebar to a more generic structure
normalizedSidebar = Object.entries(sidebar).map(([label, items]) => ({
type: 'category',
label,
items,
}));
} else {
normalizedSidebar = sidebar;
}
acc[sidebarId] = normalizedSidebar.map(item => normalizeCategory(item));
acc[sidebarId] = normalizedSidebar.map(item => normalizeCategory(item));
return acc;
}, {});
return acc;
},
{},
);
}
module.exports = function loadSidebars(sidebarPath) {
export default function loadSidebars(sidebarPath: string): Sidebar {
// We don't want sidebars to be cached because of hotreloading.
let allSidebars = {};
let allSidebars: SidebarRaw = {};
if (sidebarPath && fs.existsSync(sidebarPath)) {
allSidebars = importFresh(sidebarPath);
allSidebars = importFresh(sidebarPath) as SidebarRaw;
}
return normalizeSidebar(allSidebars);
};
}

View file

@ -0,0 +1,105 @@
/**
* 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 interface PluginOptions {
path: string;
routeBasePath: string;
include: string[];
sidebarPath: string;
docLayoutComponent: string;
docItemComponent: string;
remarkPlugins: string[];
rehypePlugins: string[];
}
export type SidebarItemDoc = {
type: string;
id: string;
};
export interface SidebarItemLink {
type: string;
href: string;
label: string;
}
export interface SidebarItemCategory {
type: string;
label: string;
items: SidebarItem[];
}
export interface SidebarItemCategoryRaw {
type: string;
label: string;
items: SidebarItemRaw[];
}
export type SidebarItem =
| SidebarItemDoc
| SidebarItemLink
| SidebarItemCategory;
export type SidebarItemRaw =
| string
| SidebarItemDoc
| SidebarItemLink
| SidebarItemCategoryRaw;
// Sidebar given by user that is not normalized yet. e.g: sidebars.json
export interface SidebarRaw {
[sidebarId: string]: {
[sidebarCategory: string]: SidebarItemRaw[];
};
}
export interface Sidebar {
[sidebarId: string]: SidebarItemCategory[];
}
export interface OrderMetadata {
previous?: string;
next?: string;
sidebar?: string;
category?: string;
subCategory?: string;
}
export interface Order {
[id: string]: OrderMetadata;
}
export interface MetadataRaw extends OrderMetadata {
id: string;
title: string;
description: string;
source: string;
permalink: string;
}
export interface Metadata extends MetadataRaw {
previous_title?: string;
next_title?: string;
}
export interface DocsMetadata {
[id: string]: Metadata;
}
export interface SourceToPermalink {
[source: string]: string;
}
export interface PermalinkToId {
[permalink: string]: string;
}
export interface LoadedContent {
docs: DocsMetadata;
docsDir: string;
docsSidebars: Sidebar;
sourceToPermalink: SourceToPermalink;
permalinkToId: PermalinkToId;
}

View file

@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"incremental": true,
"tsBuildInfoFile": "./lib/.tsbuildinfo",
"rootDir": "src",
"outDir": "lib",
}
}

View file

@ -57,7 +57,7 @@ export interface PluginContentLoadedActions {
export interface Plugin<T> {
name: string;
loadContent?(): T;
loadContent?(): Promise<T>;
contentLoaded?({
content,
actions,
@ -67,7 +67,11 @@ export interface Plugin<T> {
}): void;
postBuild?(props: Props): void;
postStart?(props: Props): void;
configureWebpack?(config: Configuration, isServer: boolean): Configuration;
configureWebpack?(
config: Configuration,
isServer: boolean,
utils: ConfigureWebpackUtils,
): Configuration;
getThemePath?(): string;
getPathsToWatch?(): string[];
getClientModules?(): string[];

View file

@ -52,7 +52,7 @@
"fs-extra": "^8.1.0",
"globby": "^10.0.1",
"html-webpack-plugin": "^4.0.0-beta.8",
"import-fresh": "^3.0.0",
"import-fresh": "^3.1.0",
"lodash": "^4.17.15",
"mini-css-extract-plugin": "^0.8.0",
"nprogress": "^0.2.0",

View file

@ -17,7 +17,7 @@ export async function swizzle(
themeName: string,
componentName?: string,
): Promise<void> {
const plugin = importFresh(themeName);
const plugin: any = importFresh(themeName);
const pluginInstance = plugin({siteDir});
let fromPath = pluginInstance.getThemePath();

View file

@ -51,7 +51,7 @@ export function loadConfig(siteDir: string): DocusaurusConfig {
if (!fs.existsSync(configPath)) {
throw new Error(`${CONFIG_FILE_NAME} not found`);
}
const loadedConfig = importFresh(configPath);
const loadedConfig = importFresh(configPath) as Partial<DocusaurusConfig>;
const missingFields = REQUIRED_FIELDS.filter(
field => !_.has(loadedConfig, field),
);
@ -64,7 +64,10 @@ export function loadConfig(siteDir: string): DocusaurusConfig {
}
// Merge default config with loaded config.
const config: DocusaurusConfig = {...DEFAULT_CONFIG, ...loadedConfig};
const config: DocusaurusConfig = {
...DEFAULT_CONFIG,
...loadedConfig,
} as DocusaurusConfig;
// Don't allow unrecognized fields.
const allowedFields = [...REQUIRED_FIELDS, ...OPTIONAL_FIELDS];

View file

@ -49,7 +49,7 @@ export async function loadPlugins({
}
// module is any valid module identifier - npm package or locally-resolved path.
const pluginModule = importFresh(pluginModuleImport);
const pluginModule: any = importFresh(pluginModuleImport);
return (pluginModule.default || pluginModule)(context, pluginOptions);
}),
);

View file

@ -32,9 +32,11 @@ export function loadPresets(
} else if (Array.isArray(presetItem)) {
presetModuleImport = presetItem[0];
presetOptions = presetItem[1] || {};
} else {
throw new Error('Invalid presets format detected in config.');
}
const presetModule = importFresh(presetModuleImport);
const presetModule: any = importFresh(presetModuleImport);
const preset: Preset = (presetModule.default || presetModule)(
context,
presetOptions,

View file

@ -2494,6 +2494,14 @@
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.3.tgz#bdfd69d61e464dcc81b25159c270d75a73c1a636"
integrity sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A==
"@types/loader-utils@^1.1.3":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@types/loader-utils/-/loader-utils-1.1.3.tgz#82b9163f2ead596c68a8c03e450fbd6e089df401"
integrity sha512-euKGFr2oCB3ASBwG39CYJMR3N9T0nanVqXdiH7Zu/Nqddt6SmFRxytq/i2w9LQYNQekEtGBz+pE3qG6fQTNvRg==
dependencies:
"@types/node" "*"
"@types/webpack" "*"
"@types/lodash.kebabcase@^4.1.6":
version "4.1.6"
resolved "https://registry.yarnpkg.com/@types/lodash.kebabcase/-/lodash.kebabcase-4.1.6.tgz#07b07aeca6c0647836de46f87a3cdfff72166c8e"
@ -8126,10 +8134,10 @@ import-fresh@^2.0.0:
caller-path "^2.0.0"
resolve-from "^3.0.0"
import-fresh@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.0.0.tgz#a3d897f420cab0e671236897f75bc14b4885c390"
integrity sha512-pOnA9tfM3Uwics+SaBLCNyZZZbK+4PTu0OPZtLlMIrv17EdBoC15S9Kn8ckJ9TZTyKb3ywNE5y1yeDxxGA7nTQ==
import-fresh@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.1.0.tgz#6d33fa1dcef6df930fae003446f33415af905118"
integrity sha512-PpuksHKGt8rXfWEr9m9EHIpgyyaltBy8+eF6GJM0QCAxMgxCfucMF3mjecK2QsJr0amJW7gTqh5/wht0z2UhEQ==
dependencies:
parent-module "^1.0.0"
resolve-from "^4.0.0"