diff --git a/.eslintignore b/.eslintignore index d52a4dd288..03ed7e494d 100644 --- a/.eslintignore +++ b/.eslintignore @@ -12,6 +12,7 @@ packages/docusaurus-1.x/lib/core/metadata.js packages/docusaurus-1.x/lib/core/MetadataBlog.js packages/docusaurus-1.x/lib/core/__tests__/split-tab.test.js packages/docusaurus-utils/lib/ +packages/docusaurus-utils-validation/lib/ packages/docusaurus/lib/ packages/docusaurus-init/lib/ packages/docusaurus-plugin-client-redirects/lib/ diff --git a/.gitignore b/.gitignore index 63dd065106..00ec5b8ba6 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ coverage types test-website packages/docusaurus-utils/lib/ +packages/docusaurus-utils-validation/lib/ packages/docusaurus/lib/ packages/docusaurus-init/lib/ packages/docusaurus-plugin-client-redirects/lib/ diff --git a/.prettierignore b/.prettierignore index 5aad71a5d9..05dc4d5f53 100644 --- a/.prettierignore +++ b/.prettierignore @@ -4,6 +4,7 @@ build coverage .docusaurus packages/docusaurus-utils/lib/ +packages/docusaurus-utils-validation/lib/ packages/docusaurus/lib/ packages/docusaurus-init/lib/ packages/docusaurus-plugin-client-redirects/lib/ diff --git a/jest.config.js b/jest.config.js index ce95d2bb67..152c5a3fad 100644 --- a/jest.config.js +++ b/jest.config.js @@ -12,6 +12,7 @@ const ignorePatterns = [ '__fixtures__', '/packages/docusaurus/lib', '/packages/docusaurus-utils/lib', + '/packages/docusaurus-utils-validation/lib', '/packages/docusaurus-plugin-content-blog/lib', '/packages/docusaurus-plugin-content-docs/lib', '/packages/docusaurus-plugin-content-pages/lib', diff --git a/packages/docusaurus-plugin-content-blog/package.json b/packages/docusaurus-plugin-content-blog/package.json index 1be7ea9f96..03a260d94a 100644 --- a/packages/docusaurus-plugin-content-blog/package.json +++ b/packages/docusaurus-plugin-content-blog/package.json @@ -19,6 +19,7 @@ "@docusaurus/mdx-loader": "2.0.0-alpha.60", "@docusaurus/types": "2.0.0-alpha.60", "@docusaurus/utils": "2.0.0-alpha.60", + "@docusaurus/utils-validation": "2.0.0-alpha.60", "@hapi/joi": "^17.1.1", "feed": "^4.1.0", "fs-extra": "^8.1.0", diff --git a/packages/docusaurus-plugin-content-blog/src/pluginOptionSchema.ts b/packages/docusaurus-plugin-content-blog/src/pluginOptionSchema.ts index 53ef3c6261..757a50b026 100644 --- a/packages/docusaurus-plugin-content-blog/src/pluginOptionSchema.ts +++ b/packages/docusaurus-plugin-content-blog/src/pluginOptionSchema.ts @@ -6,6 +6,11 @@ */ import * as Joi from '@hapi/joi'; +import { + RemarkPluginsSchema, + RehypePluginsSchema, + AdmonitionsSchema, +} from '@docusaurus/utils-validation'; export const DEFAULT_OPTIONS = { feedOptions: {}, @@ -47,25 +52,11 @@ export const PluginOptionSchema = Joi.object({ .allow('') .default(DEFAULT_OPTIONS.blogDescription), showReadingTime: Joi.bool().default(DEFAULT_OPTIONS.showReadingTime), - remarkPlugins: Joi.array() - .items( - Joi.array() - .items(Joi.function().required(), Joi.object().required()) - .length(2), - Joi.function(), - ) - .default(DEFAULT_OPTIONS.remarkPlugins), - rehypePlugins: Joi.array() - .items( - Joi.array() - .items(Joi.function().required(), Joi.object().required()) - .length(2), - Joi.function(), - ) - .default(DEFAULT_OPTIONS.rehypePlugins), + remarkPlugins: RemarkPluginsSchema.default(DEFAULT_OPTIONS.remarkPlugins), + rehypePlugins: RehypePluginsSchema.default(DEFAULT_OPTIONS.rehypePlugins), + admonitions: AdmonitionsSchema.default(DEFAULT_OPTIONS.admonitions), editUrl: Joi.string().uri(), truncateMarker: Joi.object().default(DEFAULT_OPTIONS.truncateMarker), - admonitions: Joi.object().default(DEFAULT_OPTIONS.admonitions), beforeDefaultRemarkPlugins: Joi.array() .items( Joi.array() diff --git a/packages/docusaurus-plugin-content-docs/package.json b/packages/docusaurus-plugin-content-docs/package.json index b2c2b914bd..598a4cecad 100644 --- a/packages/docusaurus-plugin-content-docs/package.json +++ b/packages/docusaurus-plugin-content-docs/package.json @@ -22,6 +22,7 @@ "@docusaurus/mdx-loader": "2.0.0-alpha.60", "@docusaurus/types": "2.0.0-alpha.60", "@docusaurus/utils": "2.0.0-alpha.60", + "@docusaurus/utils-validation": "2.0.0-alpha.60", "@hapi/joi": "17.1.1", "execa": "^3.4.0", "fs-extra": "^8.1.0", diff --git a/packages/docusaurus-plugin-content-docs/src/pluginOptionSchema.ts b/packages/docusaurus-plugin-content-docs/src/pluginOptionSchema.ts index 93739252b3..e72783e73f 100644 --- a/packages/docusaurus-plugin-content-docs/src/pluginOptionSchema.ts +++ b/packages/docusaurus-plugin-content-docs/src/pluginOptionSchema.ts @@ -6,6 +6,11 @@ */ import * as Joi from '@hapi/joi'; import {PluginOptions} from './types'; +import { + RemarkPluginsSchema, + RehypePluginsSchema, + AdmonitionsSchema, +} from '@docusaurus/utils-validation'; const REVERSED_DOCS_HOME_PAGE_ID = '_index'; @@ -35,27 +40,13 @@ export const PluginOptionSchema = Joi.object({ sidebarPath: Joi.string().default(DEFAULT_OPTIONS.sidebarPath), docLayoutComponent: Joi.string().default(DEFAULT_OPTIONS.docLayoutComponent), docItemComponent: Joi.string().default(DEFAULT_OPTIONS.docItemComponent), - remarkPlugins: Joi.array() - .items( - Joi.array() - .items(Joi.function().required(), Joi.object().required()) - .length(2), - Joi.function(), - ) - .default(DEFAULT_OPTIONS.remarkPlugins), - rehypePlugins: Joi.array() - .items( - Joi.array() - .items(Joi.function().required(), Joi.object().required()) - .length(2), - Joi.function(), - ) - .default(DEFAULT_OPTIONS.rehypePlugins), + remarkPlugins: RemarkPluginsSchema.default(DEFAULT_OPTIONS.remarkPlugins), + rehypePlugins: RehypePluginsSchema.default(DEFAULT_OPTIONS.rehypePlugins), + admonitions: AdmonitionsSchema.default(DEFAULT_OPTIONS.admonitions), showLastUpdateTime: Joi.bool().default(DEFAULT_OPTIONS.showLastUpdateTime), showLastUpdateAuthor: Joi.bool().default( DEFAULT_OPTIONS.showLastUpdateAuthor, ), - admonitions: Joi.object().default(DEFAULT_OPTIONS.admonitions), excludeNextVersionDocs: Joi.bool().default( DEFAULT_OPTIONS.excludeNextVersionDocs, ), diff --git a/packages/docusaurus-plugin-content-pages/package.json b/packages/docusaurus-plugin-content-pages/package.json index 846ccdeb7b..1c8b03f81a 100644 --- a/packages/docusaurus-plugin-content-pages/package.json +++ b/packages/docusaurus-plugin-content-pages/package.json @@ -15,10 +15,14 @@ "@types/hapi__joi": "^17.1.2" }, "dependencies": { + "@docusaurus/mdx-loader": "2.0.0-alpha.60", "@docusaurus/types": "2.0.0-alpha.60", "@docusaurus/utils": "2.0.0-alpha.60", + "@docusaurus/utils-validation": "2.0.0-alpha.60", "@hapi/joi": "17.1.1", - "globby": "^10.0.1" + "loader-utils": "^1.2.3", + "globby": "^10.0.1", + "remark-admonitions": "^1.2.1" }, "peerDependencies": { "@docusaurus/core": "^2.0.0", diff --git a/packages/docusaurus-plugin-content-pages/src/__tests__/__fixtures__/website/docusaurus.config.js b/packages/docusaurus-plugin-content-pages/src/__tests__/__fixtures__/website/docusaurus.config.js new file mode 100644 index 0000000000..6fa02ca102 --- /dev/null +++ b/packages/docusaurus-plugin-content-pages/src/__tests__/__fixtures__/website/docusaurus.config.js @@ -0,0 +1,14 @@ +/** + * 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. + */ + +module.exports = { + title: 'My Site', + tagline: 'The tagline of my site', + url: 'https://your-docusaurus-test-site.com', + baseUrl: '/', + favicon: 'img/favicon.ico', +}; diff --git a/packages/docusaurus-plugin-content-pages/src/__tests__/__fixtures__/website/src/pages/hello/index.md b/packages/docusaurus-plugin-content-pages/src/__tests__/__fixtures__/website/src/pages/hello/index.md new file mode 100644 index 0000000000..3d83ddb74e --- /dev/null +++ b/packages/docusaurus-plugin-content-pages/src/__tests__/__fixtures__/website/src/pages/hello/index.md @@ -0,0 +1,2 @@ + +Markdown index page \ No newline at end of file diff --git a/packages/docusaurus-plugin-content-pages/src/__tests__/__fixtures__/website/src/pages/hello/mdxPage.mdx b/packages/docusaurus-plugin-content-pages/src/__tests__/__fixtures__/website/src/pages/hello/mdxPage.mdx new file mode 100644 index 0000000000..54ce38beb5 --- /dev/null +++ b/packages/docusaurus-plugin-content-pages/src/__tests__/__fixtures__/website/src/pages/hello/mdxPage.mdx @@ -0,0 +1,5 @@ +--- +title: mdx page +description: my mdx page +--- +MDX page diff --git a/packages/docusaurus-plugin-content-pages/src/__tests__/index.test.ts b/packages/docusaurus-plugin-content-pages/src/__tests__/index.test.ts index b5d34c9eaf..60ff61bc0c 100644 --- a/packages/docusaurus-plugin-content-pages/src/__tests__/index.test.ts +++ b/packages/docusaurus-plugin-content-pages/src/__tests__/index.test.ts @@ -6,23 +6,15 @@ */ import path from 'path'; +import {loadContext} from '@docusaurus/core/lib/server'; import pluginContentPages from '../index'; -import {LoadContext} from '@docusaurus/types'; import normalizePluginOptions from './pluginOptionSchema.test'; describe('docusaurus-plugin-content-pages', () => { test('simple pages', async () => { const siteDir = path.join(__dirname, '__fixtures__', 'website'); - const siteConfig = { - title: 'Hello', - baseUrl: '/', - url: 'https://docusaurus.io', - }; - const context = { - siteDir, - siteConfig, - } as LoadContext; + const context = loadContext(siteDir); const pluginPath = 'src/pages'; const plugin = pluginContentPages( context, @@ -34,14 +26,27 @@ describe('docusaurus-plugin-content-pages', () => { expect(pagesMetadatas).toEqual([ { + type: 'jsx', permalink: '/', source: path.join('@site', pluginPath, 'index.js'), }, { + type: 'jsx', permalink: '/typescript', source: path.join('@site', pluginPath, 'typescript.tsx'), }, { + type: 'mdx', + permalink: '/hello/', + source: path.join('@site', pluginPath, 'hello', 'index.md'), + }, + { + type: 'mdx', + permalink: '/hello/mdxPage', + source: path.join('@site', pluginPath, 'hello', 'mdxPage.mdx'), + }, + { + type: 'jsx', permalink: '/hello/world', source: path.join('@site', pluginPath, 'hello', 'world.js'), }, diff --git a/packages/docusaurus-plugin-content-pages/src/__tests__/pluginOptionSchema.test.ts b/packages/docusaurus-plugin-content-pages/src/__tests__/pluginOptionSchema.test.ts index b117e6b057..dd93ec0a00 100644 --- a/packages/docusaurus-plugin-content-pages/src/__tests__/pluginOptionSchema.test.ts +++ b/packages/docusaurus-plugin-content-pages/src/__tests__/pluginOptionSchema.test.ts @@ -6,8 +6,11 @@ */ import {PluginOptionSchema, DEFAULT_OPTIONS} from '../pluginOptionSchema'; +import {PluginOptions} from '../types'; -export default function normalizePluginOptions(options) { +export default function normalizePluginOptions( + options: Partial, +) { const {value, error} = PluginOptionSchema.validate(options, { convert: false, }); @@ -19,29 +22,30 @@ export default function normalizePluginOptions(options) { } describe('normalizePagesPluginOptions', () => { - test('should return default options for undefined user options', async () => { - const {value} = await PluginOptionSchema.validate({}); + test('should return default options for undefined user options', () => { + const value = normalizePluginOptions({}); expect(value).toEqual(DEFAULT_OPTIONS); }); - test('should fill in default options for partially defined user options', async () => { - const {value} = await PluginOptionSchema.validate({path: 'src/pages'}); + test('should fill in default options for partially defined user options', () => { + const value = normalizePluginOptions({path: 'src/pages'}); expect(value).toEqual(DEFAULT_OPTIONS); }); - test('should accept correctly defined user options', async () => { + test('should accept correctly defined user options', () => { const userOptions = { path: 'src/my-pages', routeBasePath: 'my-pages', include: ['**/*.{js,jsx,ts,tsx}'], }; - const {value} = await PluginOptionSchema.validate(userOptions); - expect(value).toEqual(userOptions); + const value = normalizePluginOptions(userOptions); + expect(value).toEqual({...DEFAULT_OPTIONS, ...userOptions}); }); test('should reject bad path inputs', () => { expect(() => { normalizePluginOptions({ + // @ts-expect-error: bad attribute path: 42, }); }).toThrowErrorMatchingInlineSnapshot(`"\\"path\\" must be a string"`); diff --git a/packages/docusaurus-plugin-content-pages/src/index.ts b/packages/docusaurus-plugin-content-pages/src/index.ts index 00e40eea0a..f20e4fa3e5 100644 --- a/packages/docusaurus-plugin-content-pages/src/index.ts +++ b/packages/docusaurus-plugin-content-pages/src/index.ts @@ -8,23 +8,46 @@ import globby from 'globby'; import fs from 'fs'; import path from 'path'; -import {encodePath, fileToPath, aliasedSitePath} from '@docusaurus/utils'; +import { + encodePath, + fileToPath, + aliasedSitePath, + docuHash, +} from '@docusaurus/utils'; import { LoadContext, Plugin, OptionValidationContext, ValidationResult, + ConfigureWebpackUtils, } from '@docusaurus/types'; - -import {PluginOptions, LoadedContent} from './types'; +import {Configuration, Loader} from 'webpack'; +import admonitions from 'remark-admonitions'; import {PluginOptionSchema} from './pluginOptionSchema'; import {ValidationError} from '@hapi/joi'; +import {PluginOptions, LoadedContent, Metadata} from './types'; + +const isMarkdownSource = (source: string) => + source.endsWith('.md') || source.endsWith('.mdx'); + export default function pluginContentPages( context: LoadContext, options: PluginOptions, ): Plugin { - const contentPath = path.resolve(context.siteDir, options.path); + if (options.admonitions) { + options.remarkPlugins = options.remarkPlugins.concat([ + [admonitions, options.admonitions || {}], + ]); + } + const {siteConfig, siteDir, generatedFilesDir} = context; + + const contentPath = path.resolve(siteDir, options.path); + + const dataDir = path.join( + generatedFilesDir, + 'docusaurus-plugin-content-pages', + ); return { name: 'docusaurus-plugin-content-pages', @@ -35,9 +58,18 @@ export default function pluginContentPages( return [...globPattern]; }, + getClientModules() { + const modules = []; + + if (options.admonitions) { + modules.push(require.resolve('remark-admonitions/styles/infima.css')); + } + + return modules; + }, + async loadContent() { const {include} = options; - const {siteConfig, siteDir} = context; const pagesDir = contentPath; if (!fs.existsSync(pagesDir)) { @@ -49,16 +81,27 @@ export default function pluginContentPages( cwd: pagesDir, }); - return pagesFiles.map((relativeSource) => { + function toMetadata(relativeSource: string): Metadata { const source = path.join(pagesDir, relativeSource); const aliasedSource = aliasedSitePath(source, siteDir); const pathName = encodePath(fileToPath(relativeSource)); - // Default Language. - return { - permalink: pathName.replace(/^\//, baseUrl || ''), - source: aliasedSource, - }; - }); + const permalink = pathName.replace(/^\//, baseUrl || ''); + if (isMarkdownSource(relativeSource)) { + return { + type: 'mdx', + permalink, + source: aliasedSource, + }; + } else { + return { + type: 'jsx', + permalink, + source: aliasedSource, + }; + } + } + + return pagesFiles.map(toMetadata); }, async contentLoaded({content, actions}) { @@ -66,22 +109,89 @@ export default function pluginContentPages( return; } - const {addRoute} = actions; + const {addRoute, createData} = actions; await Promise.all( - content.map(async (metadataItem) => { - const {permalink, source} = metadataItem; - addRoute({ - path: permalink, - component: source, - exact: true, - modules: { - config: `@generated/docusaurus.config`, - }, - }); + content.map(async (metadata) => { + const {permalink, source} = metadata; + if (metadata.type === 'mdx') { + await createData( + // Note that this created data path must be in sync with + // metadataPath provided to mdx-loader. + `${docuHash(metadata.source)}.json`, + JSON.stringify(metadata, null, 2), + ); + addRoute({ + path: permalink, + component: options.mdxPageComponent, + exact: true, + modules: { + content: source, + }, + }); + } else { + addRoute({ + path: permalink, + component: source, + exact: true, + modules: { + config: `@generated/docusaurus.config`, + }, + }); + } }), ); }, + + configureWebpack( + _config: Configuration, + isServer: boolean, + {getBabelLoader, getCacheLoader}: ConfigureWebpackUtils, + ) { + const {rehypePlugins, remarkPlugins} = options; + return { + resolve: { + alias: { + '~pages': dataDir, + }, + }, + module: { + rules: [ + { + test: /(\.mdx?)$/, + include: [contentPath], + use: [ + getCacheLoader(isServer), + getBabelLoader(isServer), + { + loader: require.resolve('@docusaurus/mdx-loader'), + options: { + remarkPlugins, + rehypePlugins, + // Note that metadataPath must be the same/in-sync as + // the path from createData for each MDX. + metadataPath: (mdxPath: string) => { + const aliasedSource = aliasedSitePath(mdxPath, siteDir); + return path.join( + dataDir, + `${docuHash(aliasedSource)}.json`, + ); + }, + }, + }, + { + loader: path.resolve(__dirname, './markdownLoader.js'), + options: { + // siteDir, + // contentPath, + }, + }, + ].filter(Boolean) as Loader[], + }, + ], + }, + }; + }, }; } diff --git a/packages/docusaurus-plugin-content-pages/src/markdownLoader.ts b/packages/docusaurus-plugin-content-pages/src/markdownLoader.ts new file mode 100644 index 0000000000..e6cbe55978 --- /dev/null +++ b/packages/docusaurus-plugin-content-pages/src/markdownLoader.ts @@ -0,0 +1,22 @@ +/** + * 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 {loader} from 'webpack'; +// import {getOptions} from 'loader-utils'; + +const markdownLoader: loader.Loader = function (fileString) { + const callback = this.async(); + + // const options = getOptions(this); + + // TODO provide additinal md processing here? like interlinking pages? + // fileString = linkify(fileString) + + return callback && callback(null, fileString); +}; + +export default markdownLoader; diff --git a/packages/docusaurus-plugin-content-pages/src/pluginOptionSchema.ts b/packages/docusaurus-plugin-content-pages/src/pluginOptionSchema.ts index 6d20134f00..b5d3dd29ef 100644 --- a/packages/docusaurus-plugin-content-pages/src/pluginOptionSchema.ts +++ b/packages/docusaurus-plugin-content-pages/src/pluginOptionSchema.ts @@ -6,15 +6,28 @@ */ import * as Joi from '@hapi/joi'; import {PluginOptions} from './types'; +import { + RemarkPluginsSchema, + RehypePluginsSchema, + AdmonitionsSchema, +} from '@docusaurus/utils-validation'; export const DEFAULT_OPTIONS: PluginOptions = { path: 'src/pages', // Path to data on filesystem, relative to site dir. routeBasePath: '', // URL Route. - include: ['**/*.{js,jsx,ts,tsx}'], // Extensions to include. + include: ['**/*.{js,jsx,ts,tsx,md,mdx}'], // Extensions to include. + mdxPageComponent: '@theme/MDXPage', + remarkPlugins: [], + rehypePlugins: [], + admonitions: {}, }; export const PluginOptionSchema = Joi.object({ path: Joi.string().default(DEFAULT_OPTIONS.path), routeBasePath: Joi.string().default(DEFAULT_OPTIONS.routeBasePath), include: Joi.array().items(Joi.string()).default(DEFAULT_OPTIONS.include), + mdxPageComponent: Joi.string().default(DEFAULT_OPTIONS.mdxPageComponent), + remarkPlugins: RemarkPluginsSchema.default(DEFAULT_OPTIONS.remarkPlugins), + rehypePlugins: RehypePluginsSchema.default(DEFAULT_OPTIONS.rehypePlugins), + admonitions: AdmonitionsSchema.default(DEFAULT_OPTIONS.admonitions), }); diff --git a/packages/docusaurus-plugin-content-pages/src/types.ts b/packages/docusaurus-plugin-content-pages/src/types.ts index 74e47d1707..9c9e2706d9 100644 --- a/packages/docusaurus-plugin-content-pages/src/types.ts +++ b/packages/docusaurus-plugin-content-pages/src/types.ts @@ -9,11 +9,24 @@ export interface PluginOptions { path: string; routeBasePath: string; include: string[]; + mdxPageComponent: string; + remarkPlugins: ([Function, object] | Function)[]; + rehypePlugins: string[]; + admonitions: any; } -export interface Metadata { +export type JSXPageMetadata = { + type: 'jsx'; permalink: string; source: string; -} +}; + +export type MDXPageMetadata = { + type: 'mdx'; + permalink: string; + source: string; +}; + +export type Metadata = JSXPageMetadata | MDXPageMetadata; export type LoadedContent = Metadata[]; diff --git a/packages/docusaurus-plugin-content-pages/types.d.ts b/packages/docusaurus-plugin-content-pages/types.d.ts new file mode 100644 index 0000000000..c0c9f3a8ec --- /dev/null +++ b/packages/docusaurus-plugin-content-pages/types.d.ts @@ -0,0 +1,13 @@ +/** + * 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. + */ + +declare module 'remark-admonitions' { + type Options = any; + + const plugin: (options?: Options) => void; + export = plugin; +} diff --git a/packages/docusaurus-theme-classic/src/theme/MDXPage/index.js b/packages/docusaurus-theme-classic/src/theme/MDXPage/index.js new file mode 100644 index 0000000000..e58723a01a --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/MDXPage/index.js @@ -0,0 +1,32 @@ +/** + * 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 React from 'react'; +import Layout from '@theme/Layout'; +import {MDXProvider} from '@mdx-js/react'; +import MDXComponents from '@theme/MDXComponents'; + +function MDXPage(props) { + const {content: MDXPageContent} = props; + const {frontMatter, metadata} = MDXPageContent; + const {title, description} = frontMatter; + const {permalink} = metadata; + + return ( + +
+
+ + + +
+
+
+ ); +} + +export default MDXPage; diff --git a/packages/docusaurus-utils-validation/package.json b/packages/docusaurus-utils-validation/package.json new file mode 100644 index 0000000000..1ca47d0516 --- /dev/null +++ b/packages/docusaurus-utils-validation/package.json @@ -0,0 +1,24 @@ +{ + "name": "@docusaurus/utils-validation", + "version": "2.0.0-alpha.60", + "description": "Node validation utility functions for Docusaurus packages", + "main": "./lib/index.js", + "types": "./lib/index.d.ts", + "scripts": { + "build": "tsc", + "watch": "tsc --watch" + }, + "publishConfig": { + "access": "public" + }, + "license": "MIT", + "devDependencies": { + "@types/hapi__joi": "^17.1.2" + }, + "dependencies": { + "@hapi/joi": "17.1.1" + }, + "engines": { + "node": ">=10.15.1" + } +} diff --git a/packages/docusaurus-utils-validation/src/__tests__/__snapshots__/index.test.ts.snap b/packages/docusaurus-utils-validation/src/__tests__/__snapshots__/index.test.ts.snap new file mode 100644 index 0000000000..a6d659ab1f --- /dev/null +++ b/packages/docusaurus-utils-validation/src/__tests__/__snapshots__/index.test.ts.snap @@ -0,0 +1,45 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`validation schemas AdmonitionsSchema 1`] = `"\\"value\\" must be of type object"`; + +exports[`validation schemas AdmonitionsSchema 2`] = `"\\"value\\" must be of type object"`; + +exports[`validation schemas AdmonitionsSchema 3`] = `"\\"value\\" must be of type object"`; + +exports[`validation schemas AdmonitionsSchema 4`] = `"\\"value\\" must be of type object"`; + +exports[`validation schemas RehypePluginsSchema 1`] = `"\\"value\\" must be an array"`; + +exports[`validation schemas RehypePluginsSchema 2`] = `"\\"value\\" must be an array"`; + +exports[`validation schemas RehypePluginsSchema 3`] = `"\\"value\\" must be an array"`; + +exports[`validation schemas RehypePluginsSchema 4`] = `"\\"[0]\\" does not match any of the allowed types"`; + +exports[`validation schemas RehypePluginsSchema 5`] = `"\\"[0]\\" does not match any of the allowed types"`; + +exports[`validation schemas RehypePluginsSchema 6`] = `"\\"[0]\\" does not match any of the allowed types"`; + +exports[`validation schemas RehypePluginsSchema 7`] = `"\\"[0]\\" does not match any of the allowed types"`; + +exports[`validation schemas RehypePluginsSchema 8`] = `"\\"[0]\\" does not match any of the allowed types"`; + +exports[`validation schemas RehypePluginsSchema 9`] = `"\\"[0]\\" does not match any of the allowed types"`; + +exports[`validation schemas RemarkPluginsSchema 1`] = `"\\"value\\" must be an array"`; + +exports[`validation schemas RemarkPluginsSchema 2`] = `"\\"value\\" must be an array"`; + +exports[`validation schemas RemarkPluginsSchema 3`] = `"\\"value\\" must be an array"`; + +exports[`validation schemas RemarkPluginsSchema 4`] = `"\\"[0]\\" does not match any of the allowed types"`; + +exports[`validation schemas RemarkPluginsSchema 5`] = `"\\"[0]\\" does not match any of the allowed types"`; + +exports[`validation schemas RemarkPluginsSchema 6`] = `"\\"[0]\\" does not match any of the allowed types"`; + +exports[`validation schemas RemarkPluginsSchema 7`] = `"\\"[0]\\" does not match any of the allowed types"`; + +exports[`validation schemas RemarkPluginsSchema 8`] = `"\\"[0]\\" does not match any of the allowed types"`; + +exports[`validation schemas RemarkPluginsSchema 9`] = `"\\"[0]\\" does not match any of the allowed types"`; diff --git a/packages/docusaurus-utils-validation/src/__tests__/index.test.ts b/packages/docusaurus-utils-validation/src/__tests__/index.test.ts new file mode 100644 index 0000000000..8812686d40 --- /dev/null +++ b/packages/docusaurus-utils-validation/src/__tests__/index.test.ts @@ -0,0 +1,84 @@ +/** + * 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 * as Joi from '@hapi/joi'; + +import { + AdmonitionsSchema, + RehypePluginsSchema, + RemarkPluginsSchema, +} from '../index'; + +function createTestHelpers({ + schema, + defaultValue, +}: { + schema: Joi.SchemaLike; + defaultValue?: unknown; +}) { + function testOK(value: unknown) { + expect(Joi.attempt(value, schema)).toEqual(value ?? defaultValue); + } + + function testFail(value: unknown) { + expect(() => Joi.attempt(value, schema)).toThrowErrorMatchingSnapshot(); + } + + return {testOK, testFail}; +} + +function testMarkdownPluginSchemas(schema: Joi.SchemaLike) { + const {testOK, testFail} = createTestHelpers({ + schema, + defaultValue: [], + }); + + testOK(undefined); + testOK([function () {}]); + testOK([[function () {}, {attr: 'val'}]]); + testOK([ + [function () {}, {attr: 'val'}], + function () {}, + [function () {}, {attr: 'val'}], + ]); + + testFail(null); + testFail(false); + testFail(3); + testFail([null]); + testFail([false]); + testFail([3]); + testFail([[]]); + testFail([[function () {}, undefined]]); + testFail([[function () {}, true]]); +} + +describe('validation schemas', () => { + test('AdmonitionsSchema', () => { + const {testOK, testFail} = createTestHelpers({ + schema: AdmonitionsSchema, + defaultValue: {}, + }); + + testOK(undefined); + testOK({}); + testOK({attr: 'val'}); + + testFail(null); + testFail(3); + testFail(true); + testFail([]); + }); + + test('RemarkPluginsSchema', () => { + testMarkdownPluginSchemas(RemarkPluginsSchema); + }); + + test('RehypePluginsSchema', () => { + testMarkdownPluginSchemas(RehypePluginsSchema); + }); +}); diff --git a/packages/docusaurus-utils-validation/src/index.ts b/packages/docusaurus-utils-validation/src/index.ts new file mode 100644 index 0000000000..f918e5cf9e --- /dev/null +++ b/packages/docusaurus-utils-validation/src/index.ts @@ -0,0 +1,22 @@ +/** + * 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 * as Joi from '@hapi/joi'; + +const MarkdownPluginsSchema = Joi.array() + .items( + Joi.array() + // TODO, this allows [config,fn] too? + .items(Joi.function().required(), Joi.object().required()) + .length(2), + Joi.function(), + ) + .default([]); + +export const RemarkPluginsSchema = MarkdownPluginsSchema; +export const RehypePluginsSchema = MarkdownPluginsSchema; + +export const AdmonitionsSchema = Joi.object().default({}); diff --git a/packages/docusaurus-utils-validation/tsconfig.json b/packages/docusaurus-utils-validation/tsconfig.json new file mode 100644 index 0000000000..f5902ba108 --- /dev/null +++ b/packages/docusaurus-utils-validation/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "incremental": true, + "tsBuildInfoFile": "./lib/.tsbuildinfo", + "rootDir": "src", + "outDir": "lib" + } +} diff --git a/packages/docusaurus-utils/src/index.ts b/packages/docusaurus-utils/src/index.ts index 8c29343cc0..fab73e6ae1 100644 --- a/packages/docusaurus-utils/src/index.ts +++ b/packages/docusaurus-utils/src/index.ts @@ -62,8 +62,8 @@ export function objectWithKeySorted(obj: {[index: string]: any}) { }, {}); } -const indexRE = /(^|.*\/)index\.(md|js|jsx|ts|tsx)$/i; -const extRE = /\.(md|js|ts|tsx)$/; +const indexRE = /(^|.*\/)index\.(md|mdx|js|jsx|ts|tsx)$/i; +const extRE = /\.(md|mdx|js|jsx|ts|tsx)$/; /** * Convert filepath to url path. diff --git a/website/docs/creating-pages.md b/website/docs/creating-pages.md index 7306a1b06d..9b1bef9c89 100644 --- a/website/docs/creating-pages.md +++ b/website/docs/creating-pages.md @@ -7,13 +7,13 @@ In this section, we will learn about creating ad-hoc pages in Docusaurus using R The functionality of pages is powered by `@docusaurus/plugin-content-pages`. -## Adding a new page +You can use React components, or Markdown. - +## Add a React page -In the `/src/pages/` directory, create a file called `hello.js` with the following contents: +Create a file `/src/pages/helloReact.js`: -```jsx title="/src/pages/hello.js" +```jsx title="/src/pages/helloReact.js" import React from 'react'; import Layout from '@theme/Layout'; @@ -39,13 +39,40 @@ function Hello() { export default Hello; ``` -Once you save the file, the development server will automatically reload the changes. Now open http://localhost:3000/hello, you will see the new page you just created. +Once you save the file, the development server will automatically reload the changes. Now open `http://localhost:3000/helloReact`, you will see the new page you just created. Each page doesn't come with any styling. You will need to import the `Layout` component from `@theme/Layout` and wrap your contents within that component if you want the navbar and/or footer to appear. :::tip -You can also create a page in TypeScript, in which case the file name should use the `.tsx` extension, eg. `hello.tsx`. +You can also create TypeScript pages with the `.tsx` extension (`helloReact.tsx`). + +::: + +## Add a Markdown page + +Create a file `/src/pages/helloMarkdown.md`: + +```mdx title="/src/pages/helloMarkdown.md" +--- +title: my hello page title +description: my hello page description +--- + +# Hello + +How are you? +``` + +In the same way, a page will be created at `http://localhost:3000/helloMarkdown`. + +Markdown pages are less flexible than React pages, because it always uses the theme layout. + +Here's an [example markdown page](/examples/markdownPageExample). + +:::tip + +You can use the full power of React in Markdown pages too, refer to the [MDX](https://mdxjs.com/) documentation. ::: @@ -85,9 +112,4 @@ All JavaScript/TypeScript files within the `src/pages/` directory will have corr ## Using React -React is used as the UI library to create pages. Every page component should export a React component and you can leverage on the expressiveness of React to build rich and interactive content. - - +React is used as the UI library to create pages. Every page component should export a React component, and you can leverage on the expressiveness of React to build rich and interactive content. diff --git a/website/docs/using-plugins.md b/website/docs/using-plugins.md index 3650aca79c..d020528293 100644 --- a/website/docs/using-plugins.md +++ b/website/docs/using-plugins.md @@ -376,6 +376,15 @@ module.exports = { */ routeBasePath: '', include: ['**/*.{js,jsx}'], + /** + * Theme component used by markdown pages. + */ + mdxPageComponent: '@theme/MDXPage', + /** + * Remark and Rehype plugins passed to MDX + */ + remarkPlugins: [], + rehypePlugins: [], }, ], ], diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js index 2a8b5ae15a..afa2b1c2d8 100644 --- a/website/docusaurus.config.js +++ b/website/docusaurus.config.js @@ -136,6 +136,9 @@ module.exports = { copyright: `Copyright © ${new Date().getFullYear()} Facebook, Inc.`, }, }, + pages: { + remarkPlugins: [require('./src/plugins/remark-npm2yarn')], + }, theme: { customCss: require.resolve('./src/css/custom.css'), }, diff --git a/website/src/pages/examples/markdownPageExample.md b/website/src/pages/examples/markdownPageExample.md new file mode 100644 index 0000000000..9cbb1bf1aa --- /dev/null +++ b/website/src/pages/examples/markdownPageExample.md @@ -0,0 +1,34 @@ +--- +title: Markdown Page example title +description: Markdown Page example description +--- + +# Markdown page + +This is a page generated from markdown to illustrate the markdown page feature. + +It supports all the regular MDX features, as you can see: + +:::info + +Useful information. + +::: + +```jsx live +function Button() { + return ( + + ); +} +``` + +![](../../../static/img/docusaurus.png) + +import Tabs from '@theme/Tabs'; + +import TabItem from '@theme/TabItem'; + +This is an apple 🍎This is an orange 🍊This is a banana 🍌