mirror of
https://github.com/facebook/docusaurus.git
synced 2025-04-29 02:08:36 +02:00
refactor(core): refactor routes generation logic (#7054)
* refactor(core): refactor routes generation logic * fixes
This commit is contained in:
parent
e31e91ef47
commit
77662260f8
19 changed files with 551 additions and 506 deletions
|
@ -27,24 +27,26 @@ declare module '@generated/site-metadata' {
|
|||
}
|
||||
|
||||
declare module '@generated/registry' {
|
||||
const registry: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
readonly [key: string]: [() => Promise<any>, string, string];
|
||||
};
|
||||
import type {Registry} from '@docusaurus/types';
|
||||
|
||||
const registry: Registry;
|
||||
export default registry;
|
||||
}
|
||||
|
||||
declare module '@generated/routes' {
|
||||
import type {Route} from '@docusaurus/types';
|
||||
import type {RouteConfig as RRRouteConfig} from 'react-router-config';
|
||||
|
||||
const routes: Route[];
|
||||
type RouteConfig = RRRouteConfig & {
|
||||
path: string;
|
||||
};
|
||||
const routes: RouteConfig[];
|
||||
export default routes;
|
||||
}
|
||||
|
||||
declare module '@generated/routesChunkNames' {
|
||||
import type {RouteChunksTree} from '@docusaurus/types';
|
||||
import type {RouteChunkNames} from '@docusaurus/types';
|
||||
|
||||
const routesChunkNames: {[route: string]: RouteChunksTree};
|
||||
const routesChunkNames: RouteChunkNames;
|
||||
export = routesChunkNames;
|
||||
}
|
||||
|
||||
|
|
|
@ -292,19 +292,15 @@ export default async function pluginContentBlog(
|
|||
exact: true,
|
||||
modules: {
|
||||
sidebar: aliasedSource(sidebarProp),
|
||||
items: items.map((postID) =>
|
||||
// To tell routes.js this is an import and not a nested object
|
||||
// to recurse.
|
||||
({
|
||||
content: {
|
||||
__import: true,
|
||||
path: blogItemsToMetadata[postID]!.source,
|
||||
query: {
|
||||
truncated: true,
|
||||
},
|
||||
items: items.map((postID) => ({
|
||||
content: {
|
||||
__import: true,
|
||||
path: blogItemsToMetadata[postID]!.source,
|
||||
query: {
|
||||
truncated: true,
|
||||
},
|
||||
}),
|
||||
),
|
||||
},
|
||||
})),
|
||||
metadata: aliasedSource(pageMetadataPath),
|
||||
},
|
||||
});
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import type {Route} from '@docusaurus/types';
|
||||
import type {RouteConfig} from 'react-router-config';
|
||||
import {findHomePageRoute, isSamePath} from '../routesUtils';
|
||||
|
||||
describe('isSamePath', () => {
|
||||
|
@ -41,7 +41,7 @@ describe('isSamePath', () => {
|
|||
});
|
||||
|
||||
describe('findHomePageRoute', () => {
|
||||
const homePage: Route = {
|
||||
const homePage: RouteConfig = {
|
||||
path: '/',
|
||||
exact: true,
|
||||
};
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import {useMemo} from 'react';
|
||||
import generatedRoutes from '@generated/routes';
|
||||
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
||||
import type {Route} from '@docusaurus/types';
|
||||
import type {RouteConfig} from 'react-router-config';
|
||||
|
||||
/**
|
||||
* Compare the 2 paths, case insensitive and ignoring trailing slash
|
||||
|
@ -34,18 +34,18 @@ export function findHomePageRoute({
|
|||
baseUrl,
|
||||
routes: initialRoutes,
|
||||
}: {
|
||||
routes: Route[];
|
||||
routes: RouteConfig[];
|
||||
baseUrl: string;
|
||||
}): Route | undefined {
|
||||
function isHomePageRoute(route: Route): boolean {
|
||||
}): RouteConfig | undefined {
|
||||
function isHomePageRoute(route: RouteConfig): boolean {
|
||||
return route.path === baseUrl && route.exact === true;
|
||||
}
|
||||
|
||||
function isHomeParentRoute(route: Route): boolean {
|
||||
function isHomeParentRoute(route: RouteConfig): boolean {
|
||||
return route.path === baseUrl && !route.exact;
|
||||
}
|
||||
|
||||
function doFindHomePageRoute(routes: Route[]): Route | undefined {
|
||||
function doFindHomePageRoute(routes: RouteConfig[]): RouteConfig | undefined {
|
||||
if (routes.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
@ -66,7 +66,7 @@ export function findHomePageRoute({
|
|||
* Fetches the route that points to "/". Use this instead of the naive "/",
|
||||
* because the homepage may not exist.
|
||||
*/
|
||||
export function useHomePageRoute(): Route | undefined {
|
||||
export function useHomePageRoute(): RouteConfig | undefined {
|
||||
const {baseUrl} = useDocusaurusContext().siteConfig;
|
||||
return useMemo(
|
||||
() => findHomePageRoute({routes: generatedRoutes, baseUrl}),
|
||||
|
|
317
packages/docusaurus-types/src/index.d.ts
vendored
317
packages/docusaurus-types/src/index.d.ts
vendored
|
@ -11,19 +11,42 @@ import type {CommanderStatic} from 'commander';
|
|||
import type {ParsedUrlQueryInput} from 'querystring';
|
||||
import type Joi from 'joi';
|
||||
import type {
|
||||
DeepRequired,
|
||||
Required as RequireKeys,
|
||||
DeepPartial,
|
||||
DeepRequired,
|
||||
} from 'utility-types';
|
||||
import type {Location} from 'history';
|
||||
import type Loadable from 'react-loadable';
|
||||
|
||||
// === Configuration ===
|
||||
|
||||
export type ReportingSeverity = 'ignore' | 'log' | 'warn' | 'error' | 'throw';
|
||||
|
||||
export type PluginOptions = {id?: string} & {[key: string]: unknown};
|
||||
|
||||
export type PluginConfig =
|
||||
| string
|
||||
| [string, PluginOptions]
|
||||
| [PluginModule, PluginOptions]
|
||||
| PluginModule;
|
||||
|
||||
export type PresetConfig = string | [string, {[key: string]: unknown}];
|
||||
|
||||
export type ThemeConfig = {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export type I18nLocaleConfig = {
|
||||
label: string;
|
||||
htmlLang: string;
|
||||
direction: string;
|
||||
};
|
||||
|
||||
export type I18nConfig = {
|
||||
defaultLocale: string;
|
||||
locales: [string, ...string[]];
|
||||
localeConfigs: {[locale: string]: Partial<I18nLocaleConfig>};
|
||||
};
|
||||
|
||||
/**
|
||||
* Docusaurus config, after validation/normalization.
|
||||
*/
|
||||
|
@ -92,6 +115,8 @@ export type Config = RequireKeys<
|
|||
'title' | 'url' | 'baseUrl'
|
||||
>;
|
||||
|
||||
// === Data loading ===
|
||||
|
||||
/**
|
||||
* - `type: 'package'`, plugin is in a different package.
|
||||
* - `type: 'project'`, plugin is in the same docusaurus project.
|
||||
|
@ -115,26 +140,29 @@ export type SiteMetadata = {
|
|||
readonly pluginVersions: {[pluginName: string]: PluginVersionInformation};
|
||||
};
|
||||
|
||||
// Inspired by Chrome JSON, because it's a widely supported i18n format
|
||||
// https://developer.chrome.com/apps/i18n-messages
|
||||
// https://support.crowdin.com/file-formats/chrome-json/
|
||||
// https://www.applanga.com/docs/formats/chrome_i18n_json
|
||||
// https://docs.transifex.com/formats/chrome-json
|
||||
// https://help.phrase.com/help/chrome-json-messages
|
||||
/**
|
||||
* Inspired by Chrome JSON, because it's a widely supported i18n format
|
||||
* @see https://developer.chrome.com/apps/i18n-messages
|
||||
* @see https://support.crowdin.com/file-formats/chrome-json/
|
||||
* @see https://www.applanga.com/docs/formats/chrome_i18n_json
|
||||
* @see https://docs.transifex.com/formats/chrome-json
|
||||
* @see https://help.phrase.com/help/chrome-json-messages
|
||||
*/
|
||||
export type TranslationMessage = {message: string; description?: string};
|
||||
export type TranslationFileContent = {[key: string]: TranslationMessage};
|
||||
export type TranslationFile = {path: string; content: TranslationFileContent};
|
||||
|
||||
export type I18nLocaleConfig = {
|
||||
label: string;
|
||||
htmlLang: string;
|
||||
direction: string;
|
||||
};
|
||||
|
||||
export type I18nConfig = {
|
||||
defaultLocale: string;
|
||||
locales: [string, ...string[]];
|
||||
localeConfigs: {[locale: string]: Partial<I18nLocaleConfig>};
|
||||
/**
|
||||
* An abstract representation of how a translation file exists on disk. The core
|
||||
* would handle the file reading/writing; plugins just need to deal with
|
||||
* translations in-memory.
|
||||
*/
|
||||
export type TranslationFile = {
|
||||
/**
|
||||
* Relative to the directory where it's expected to be found. For plugin
|
||||
* files, it's relative to `i18n/<locale>/<pluginName>/<pluginId>`. Should NOT
|
||||
* have any extension.
|
||||
*/
|
||||
path: string;
|
||||
content: TranslationFileContent;
|
||||
};
|
||||
|
||||
export type I18n = DeepRequired<I18nConfig> & {currentLocale: string};
|
||||
|
@ -153,21 +181,6 @@ export type DocusaurusContext = {
|
|||
// isBrowser: boolean; // Not here on purpose!
|
||||
};
|
||||
|
||||
export type Preset = {
|
||||
plugins?: PluginConfig[];
|
||||
themes?: PluginConfig[];
|
||||
};
|
||||
|
||||
export type PresetModule = {
|
||||
<T>(context: LoadContext, presetOptions: T): Preset;
|
||||
};
|
||||
|
||||
export type ImportedPresetModule = PresetModule & {
|
||||
default?: PresetModule;
|
||||
};
|
||||
|
||||
export type PresetConfig = string | [string, {[key: string]: unknown}];
|
||||
|
||||
export type HostPortCLIOptions = {
|
||||
host?: string;
|
||||
port?: string;
|
||||
|
@ -191,14 +204,11 @@ export type ServeCLIOptions = HostPortCLIOptions &
|
|||
build: boolean;
|
||||
};
|
||||
|
||||
export type BuildOptions = ConfigOptions & {
|
||||
export type BuildCLIOptions = ConfigOptions & {
|
||||
bundleAnalyzer: boolean;
|
||||
outDir: string;
|
||||
minify: boolean;
|
||||
skipBuild: boolean;
|
||||
};
|
||||
|
||||
export type BuildCLIOptions = BuildOptions & {
|
||||
locale?: string;
|
||||
};
|
||||
|
||||
|
@ -219,8 +229,6 @@ export type LoadContext = {
|
|||
codeTranslations: {[msgId: string]: string};
|
||||
};
|
||||
|
||||
export type HtmlTags = string | HtmlTagObject | (string | HtmlTagObject)[];
|
||||
|
||||
export type Props = LoadContext & {
|
||||
readonly headTags: string;
|
||||
readonly preBodyTags: string;
|
||||
|
@ -231,12 +239,27 @@ export type Props = LoadContext & {
|
|||
readonly plugins: LoadedPlugin[];
|
||||
};
|
||||
|
||||
// === Plugin ===
|
||||
|
||||
export type PluginContentLoadedActions = {
|
||||
addRoute: (config: RouteConfig) => void;
|
||||
createData: (name: string, data: string) => Promise<string>;
|
||||
setGlobalData: (data: unknown) => void;
|
||||
};
|
||||
|
||||
export type ConfigureWebpackUtils = {
|
||||
getStyleLoaders: (
|
||||
isServer: boolean,
|
||||
cssOptions: {
|
||||
[key: string]: unknown;
|
||||
},
|
||||
) => RuleSetRule[];
|
||||
getJSLoader: (options: {
|
||||
isServer: boolean;
|
||||
babelOptions?: {[key: string]: unknown};
|
||||
}) => RuleSetRule;
|
||||
};
|
||||
|
||||
export type AllContent = {
|
||||
[pluginName: string]: {
|
||||
[pluginID: string]: unknown;
|
||||
|
@ -246,6 +269,37 @@ export type AllContent = {
|
|||
// TODO improve type (not exposed by postcss-loader)
|
||||
export type PostCssOptions = {[key: string]: unknown} & {plugins: unknown[]};
|
||||
|
||||
type HtmlTagObject = {
|
||||
/**
|
||||
* Attributes of the html tag.
|
||||
* E.g. `{ disabled: true, value: "demo", rel: "preconnect" }`
|
||||
*/
|
||||
attributes?: Partial<{[key: string]: string | boolean}>;
|
||||
/** The tag name, e.g. `div`, `script`, `link`, `meta` */
|
||||
tagName: string;
|
||||
/** The inner HTML */
|
||||
innerHTML?: string;
|
||||
};
|
||||
|
||||
export type HtmlTags = string | HtmlTagObject | (string | HtmlTagObject)[];
|
||||
|
||||
export type ValidationSchema<T> = Joi.ObjectSchema<T>;
|
||||
|
||||
export type Validate<T, U> = (
|
||||
validationSchema: ValidationSchema<U>,
|
||||
options: T,
|
||||
) => U;
|
||||
|
||||
export type OptionValidationContext<T, U> = {
|
||||
validate: Validate<T, U>;
|
||||
options: T;
|
||||
};
|
||||
|
||||
export type ThemeConfigValidationContext<T> = {
|
||||
validate: Validate<T, T>;
|
||||
themeConfig: Partial<T>;
|
||||
};
|
||||
|
||||
export type Plugin<Content = unknown> = {
|
||||
name: string;
|
||||
loadContent?: () => Promise<Content>;
|
||||
|
@ -266,7 +320,9 @@ export type Plugin<Content = unknown> = {
|
|||
utils: ConfigureWebpackUtils,
|
||||
content: Content,
|
||||
) => WebpackConfiguration & {
|
||||
mergeStrategy?: ConfigureWebpackFnMergeStrategy;
|
||||
mergeStrategy?: {
|
||||
[key: string]: CustomizeRuleString;
|
||||
};
|
||||
};
|
||||
configurePostCss?: (options: PostCssOptions) => PostCssOptions;
|
||||
getThemePath?: () => string;
|
||||
|
@ -334,9 +390,7 @@ export type NormalizedPluginConfig = {
|
|||
export type InitializedPlugin = Plugin & {
|
||||
readonly options: Required<PluginOptions>;
|
||||
readonly version: PluginVersionInformation;
|
||||
/**
|
||||
* The absolute path to the folder containing the entry point file.
|
||||
*/
|
||||
/** The absolute path to the folder containing the entry point file. */
|
||||
readonly path: string;
|
||||
};
|
||||
|
||||
|
@ -372,48 +426,71 @@ export type ImportedPluginModule = PluginModule & {
|
|||
default?: PluginModule;
|
||||
};
|
||||
|
||||
export type ConfigureWebpackFn = Plugin['configureWebpack'];
|
||||
export type ConfigureWebpackFnMergeStrategy = {
|
||||
[key: string]: CustomizeRuleString;
|
||||
};
|
||||
export type ConfigurePostCssFn = Plugin['configurePostCss'];
|
||||
|
||||
export type PluginOptions = {id?: string} & {[key: string]: unknown};
|
||||
|
||||
export type PluginConfig =
|
||||
| string
|
||||
| [string, PluginOptions]
|
||||
| [PluginModule, PluginOptions]
|
||||
| PluginModule;
|
||||
|
||||
export type ChunkRegistry = {
|
||||
loader: string;
|
||||
modulePath: string;
|
||||
export type Preset = {
|
||||
plugins?: PluginConfig[];
|
||||
themes?: PluginConfig[];
|
||||
};
|
||||
|
||||
export type PresetModule = {
|
||||
<T>(context: LoadContext, presetOptions: T): Preset;
|
||||
};
|
||||
|
||||
export type ImportedPresetModule = PresetModule & {
|
||||
default?: PresetModule;
|
||||
};
|
||||
|
||||
// === Route registry ===
|
||||
|
||||
/**
|
||||
* A "module" represents a unit of serialized data emitted from the plugin. It
|
||||
* will be imported on client-side and passed as props, context, etc.
|
||||
*
|
||||
* If it's a string, it's a file path that Webpack can `require`; if it's
|
||||
* an object, it can also contain `query` or other metadata.
|
||||
*/
|
||||
export type Module =
|
||||
| {
|
||||
path: string;
|
||||
/**
|
||||
* A marker that tells the route generator this is an import and not a
|
||||
* nested object to recurse.
|
||||
*/
|
||||
__import?: boolean;
|
||||
path: string;
|
||||
query?: ParsedUrlQueryInput;
|
||||
}
|
||||
| string;
|
||||
|
||||
export type RouteModule = {
|
||||
[module: string]: Module | RouteModule | RouteModule[];
|
||||
};
|
||||
|
||||
export type ChunkNames = {
|
||||
[name: string]: string | null | ChunkNames | ChunkNames[];
|
||||
/**
|
||||
* Represents the data attached to each route. Since the routes.js is a
|
||||
* monolithic data file, any data (like props) should be serialized separately
|
||||
* and registered here as file paths (a {@link Module}), so that we could
|
||||
* code-split.
|
||||
*/
|
||||
export type RouteModules = {
|
||||
[propName: string]: Module | RouteModules | RouteModules[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents a "slice" of the final route structure returned from the plugin
|
||||
* `addRoute` action.
|
||||
*/
|
||||
export type RouteConfig = {
|
||||
/** With leading slash. Trailing slash will be normalized by config. */
|
||||
path: string;
|
||||
/** Component used to render this route, a path that Webpack can `require`. */
|
||||
component: string;
|
||||
modules?: RouteModule;
|
||||
/**
|
||||
* Props. Each entry should be `[propName]: pathToPropModule` (created with
|
||||
* `createData`)
|
||||
*/
|
||||
modules?: RouteModules;
|
||||
/** Nested routes config. */
|
||||
routes?: RouteConfig[];
|
||||
/** React router config option: `exact` routes would not match subroutes. */
|
||||
exact?: boolean;
|
||||
/** Used to sort routes. Higher-priority routes will be placed first. */
|
||||
priority?: number;
|
||||
/** Extra props; will be copied to routes.js. */
|
||||
[propName: string]: unknown;
|
||||
};
|
||||
|
||||
|
@ -435,11 +512,57 @@ export type PluginRouteContext = RouteContext & {
|
|||
};
|
||||
};
|
||||
|
||||
export type Route = {
|
||||
readonly path: string;
|
||||
readonly component: ReturnType<typeof Loadable>;
|
||||
readonly exact?: boolean;
|
||||
readonly routes?: Route[];
|
||||
/**
|
||||
* The shape would be isomorphic to {@link RouteModules}:
|
||||
* {@link Module} -> `string`, `RouteModules[]` -> `ChunkNames[]`.
|
||||
*
|
||||
* Each `string` chunk name will correlate with one key in the {@link Registry}.
|
||||
*/
|
||||
export type ChunkNames = {
|
||||
[propName: string]: string | ChunkNames | ChunkNames[];
|
||||
};
|
||||
|
||||
/**
|
||||
* A map from route paths (with a hash) to the chunk names of each module, which
|
||||
* the bundler will collect.
|
||||
*
|
||||
* Chunk keys are routes with a hash, because 2 routes can conflict with each
|
||||
* other if they have the same path, e.g.: parent=/docs, child=/docs
|
||||
*
|
||||
* @see https://github.com/facebook/docusaurus/issues/2917
|
||||
*/
|
||||
export type RouteChunkNames = {
|
||||
[routePathHashed: string]: ChunkNames;
|
||||
};
|
||||
|
||||
/**
|
||||
* Each key is the chunk name, which you can get from `routeChunkNames` (see
|
||||
* {@link RouteChunkNames}). The values are the opts data that react-loadable
|
||||
* needs. For example:
|
||||
*
|
||||
* ```js
|
||||
* const options = {
|
||||
* optsLoader: {
|
||||
* component: () => import('./Pages.js'),
|
||||
* content.foo: () => import('./doc1.md'),
|
||||
* },
|
||||
* optsModules: ['./Pages.js', './doc1.md'],
|
||||
* optsWebpack: [
|
||||
* require.resolveWeak('./Pages.js'),
|
||||
* require.resolveWeak('./doc1.md'),
|
||||
* ],
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @see https://github.com/jamiebuilds/react-loadable#declaring-which-modules-are-being-loaded
|
||||
*/
|
||||
export type Registry = {
|
||||
readonly [chunkName: string]: [
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
Loader: () => Promise<any>,
|
||||
ModuleName: string,
|
||||
ResolvedModuleName: string,
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -449,56 +572,12 @@ export type ThemeAliases = {
|
|||
[alias: string]: string;
|
||||
};
|
||||
|
||||
export type ConfigureWebpackUtils = {
|
||||
getStyleLoaders: (
|
||||
isServer: boolean,
|
||||
cssOptions: {
|
||||
[key: string]: unknown;
|
||||
},
|
||||
) => RuleSetRule[];
|
||||
getJSLoader: (options: {
|
||||
isServer: boolean;
|
||||
babelOptions?: {[key: string]: unknown};
|
||||
}) => RuleSetRule;
|
||||
};
|
||||
|
||||
type HtmlTagObject = {
|
||||
/**
|
||||
* Attributes of the html tag.
|
||||
* E.g. `{ disabled: true, value: "demo", rel: "preconnect" }`
|
||||
*/
|
||||
attributes?: Partial<{[key: string]: string | boolean}>;
|
||||
/** The tag name, e.g. `div`, `script`, `link`, `meta` */
|
||||
tagName: string;
|
||||
/** The inner HTML */
|
||||
innerHTML?: string;
|
||||
};
|
||||
|
||||
export type ValidationSchema<T> = Joi.ObjectSchema<T>;
|
||||
|
||||
export type Validate<T, U> = (
|
||||
validationSchema: ValidationSchema<U>,
|
||||
options: T,
|
||||
) => U;
|
||||
|
||||
export type OptionValidationContext<T, U> = {
|
||||
validate: Validate<T, U>;
|
||||
options: T;
|
||||
};
|
||||
|
||||
export type ThemeConfigValidationContext<T> = {
|
||||
validate: Validate<T, T>;
|
||||
themeConfig: Partial<T>;
|
||||
};
|
||||
|
||||
export type TOCItem = {
|
||||
readonly value: string;
|
||||
readonly id: string;
|
||||
readonly level: number;
|
||||
};
|
||||
|
||||
export type RouteChunksTree = {[x: string | number]: string | RouteChunksTree};
|
||||
|
||||
export type ClientModule = {
|
||||
onRouteUpdate?: (args: {
|
||||
previousLocation: Location | null;
|
||||
|
|
|
@ -6,59 +6,10 @@
|
|||
*/
|
||||
|
||||
import {jest} from '@jest/globals';
|
||||
import {genChunkName, readOutputHTMLFile, generate} from '../emitUtils';
|
||||
import {readOutputHTMLFile, generate} from '../emitUtils';
|
||||
import path from 'path';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
describe('genChunkName', () => {
|
||||
it('works', () => {
|
||||
const firstAssert: {[key: string]: string} = {
|
||||
'/docs/adding-blog': 'docs-adding-blog-062',
|
||||
'/docs/versioning': 'docs-versioning-8a8',
|
||||
'/': 'index',
|
||||
'/blog/2018/04/30/How-I-Converted-Profilo-To-Docusaurus':
|
||||
'blog-2018-04-30-how-i-converted-profilo-to-docusaurus-4f2',
|
||||
'/youtube': 'youtube-429',
|
||||
'/users/en/': 'users-en-f7a',
|
||||
'/blog': 'blog-c06',
|
||||
};
|
||||
Object.keys(firstAssert).forEach((str) => {
|
||||
expect(genChunkName(str)).toBe(firstAssert[str]);
|
||||
});
|
||||
});
|
||||
|
||||
it("doesn't allow different chunk name for same path", () => {
|
||||
expect(genChunkName('path/is/similar', 'oldPrefix')).toEqual(
|
||||
genChunkName('path/is/similar', 'newPrefix'),
|
||||
);
|
||||
});
|
||||
|
||||
it('emits different chunk names for different paths even with same preferred name', () => {
|
||||
const secondAssert: {[key: string]: string} = {
|
||||
'/blog/1': 'blog-85-f-089',
|
||||
'/blog/2': 'blog-353-489',
|
||||
};
|
||||
Object.keys(secondAssert).forEach((str) => {
|
||||
expect(genChunkName(str, undefined, 'blog')).toBe(secondAssert[str]);
|
||||
});
|
||||
});
|
||||
|
||||
it('only generates short unique IDs', () => {
|
||||
const thirdAssert: {[key: string]: string} = {
|
||||
a: '0cc175b9',
|
||||
b: '92eb5ffe',
|
||||
c: '4a8a08f0',
|
||||
d: '8277e091',
|
||||
};
|
||||
Object.keys(thirdAssert).forEach((str) => {
|
||||
expect(genChunkName(str, undefined, undefined, true)).toBe(
|
||||
thirdAssert[str],
|
||||
);
|
||||
});
|
||||
expect(genChunkName('d', undefined, undefined, true)).toBe('8277e091');
|
||||
});
|
||||
});
|
||||
|
||||
describe('readOutputHTMLFile', () => {
|
||||
it('trailing slash undefined', async () => {
|
||||
await expect(
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
import path from 'path';
|
||||
import fs from 'fs-extra';
|
||||
import {createHash} from 'crypto';
|
||||
import {simpleHash, docuHash} from './hashUtils';
|
||||
import {findAsyncSequential} from './jsUtils';
|
||||
|
||||
const fileHash = new Map<string, string>();
|
||||
|
@ -18,7 +17,8 @@ const fileHash = new Map<string, string>();
|
|||
* differs from cache (for hot reload performance).
|
||||
*
|
||||
* @param generatedFilesDir Absolute path.
|
||||
* @param file Path relative to `generatedFilesDir`.
|
||||
* @param file Path relative to `generatedFilesDir`. File will always be
|
||||
* outputted; no need to ensure directory exists.
|
||||
* @param content String content to write.
|
||||
* @param skipCache If `true` (defaults as `true` for production), file is
|
||||
* force-rewritten, skipping cache.
|
||||
|
@ -29,7 +29,7 @@ export async function generate(
|
|||
content: string,
|
||||
skipCache: boolean = process.env.NODE_ENV === 'production',
|
||||
): Promise<void> {
|
||||
const filepath = path.join(generatedFilesDir, file);
|
||||
const filepath = path.resolve(generatedFilesDir, file);
|
||||
|
||||
if (skipCache) {
|
||||
await fs.outputFile(filepath, content);
|
||||
|
@ -62,35 +62,6 @@ export async function generate(
|
|||
}
|
||||
}
|
||||
|
||||
const chunkNameCache = new Map<string, string>();
|
||||
|
||||
/**
|
||||
* Generate unique chunk name given a module path.
|
||||
*/
|
||||
export function genChunkName(
|
||||
modulePath: string,
|
||||
prefix?: string,
|
||||
preferredName?: string,
|
||||
shortId: boolean = process.env.NODE_ENV === 'production',
|
||||
): string {
|
||||
let chunkName = chunkNameCache.get(modulePath);
|
||||
if (!chunkName) {
|
||||
if (shortId) {
|
||||
chunkName = simpleHash(modulePath, 8);
|
||||
} else {
|
||||
let str = modulePath;
|
||||
if (preferredName) {
|
||||
const shortHash = simpleHash(modulePath, 3);
|
||||
str = `${preferredName}${shortHash}`;
|
||||
}
|
||||
const name = str === '/' ? 'index' : docuHash(str);
|
||||
chunkName = prefix ? `${prefix}---${name}` : name;
|
||||
}
|
||||
chunkNameCache.set(modulePath, chunkName);
|
||||
}
|
||||
return chunkName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param permalink The URL that the HTML file corresponds to, without base URL
|
||||
* @param outDir Full path to the output directory
|
||||
|
|
|
@ -22,7 +22,7 @@ export {
|
|||
DEFAULT_PLUGIN_ID,
|
||||
WEBPACK_URL_LOADER_LIMIT,
|
||||
} from './constants';
|
||||
export {generate, genChunkName, readOutputHTMLFile} from './emitUtils';
|
||||
export {generate, readOutputHTMLFile} from './emitUtils';
|
||||
export {
|
||||
getFileCommitDate,
|
||||
FileNotTrackedError,
|
||||
|
|
|
@ -34,20 +34,16 @@ const canPrefetch = (routePath: string) =>
|
|||
const canPreload = (routePath: string) =>
|
||||
!isSlowConnection() && !loaded[routePath];
|
||||
|
||||
// Remove the last part containing the route hash
|
||||
// input: /blog/2018/12/14/Happy-First-Birthday-Slash-fe9
|
||||
// output: /blog/2018/12/14/Happy-First-Birthday-Slash
|
||||
const removeRouteNameHash = (str: string) => str.replace(/-[^-]+$/, '');
|
||||
|
||||
const getChunkNamesToLoad = (path: string): string[] =>
|
||||
Object.entries(routesChunkNames)
|
||||
.filter(
|
||||
([routeNameWithHash]) => removeRouteNameHash(routeNameWithHash) === path,
|
||||
// Remove the last part containing the route hash
|
||||
// input: /blog/2018/12/14/Happy-First-Birthday-Slash-fe9
|
||||
// output: /blog/2018/12/14/Happy-First-Birthday-Slash
|
||||
([routeNameWithHash]) =>
|
||||
routeNameWithHash.replace(/-[^-]+$/, '') === path,
|
||||
)
|
||||
.flatMap(([, routeChunks]) =>
|
||||
// flat() is useful for nested chunk names, it's not like array.flat()
|
||||
Object.values(flat(routeChunks)),
|
||||
);
|
||||
.flatMap(([, routeChunks]) => Object.values(flat(routeChunks)));
|
||||
|
||||
const docusaurus = {
|
||||
prefetch: (routePath: string): boolean => {
|
||||
|
|
|
@ -34,27 +34,12 @@ export default function ComponentCreator(
|
|||
});
|
||||
}
|
||||
|
||||
const chunkNamesKey = `${path}-${hash}`;
|
||||
const chunkNames = routesChunkNames[chunkNamesKey]!;
|
||||
const optsModules: string[] = [];
|
||||
const optsWebpack: string[] = [];
|
||||
const chunkNames = routesChunkNames[`${path}-${hash}`]!;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const optsLoader: {[key: string]: () => Promise<any>} = {};
|
||||
const optsModules: string[] = [];
|
||||
const optsWebpack: string[] = [];
|
||||
|
||||
/* Prepare opts data that react-loadable needs
|
||||
https://github.com/jamiebuilds/react-loadable#declaring-which-modules-are-being-loaded
|
||||
Example:
|
||||
- optsLoader:
|
||||
{
|
||||
component: () => import('./Pages.js'),
|
||||
content.foo: () => import('./doc1.md'),
|
||||
}
|
||||
- optsModules: ['./Pages.js', './doc1.md']
|
||||
- optsWebpack: [
|
||||
require.resolveWeak('./Pages.js'),
|
||||
require.resolveWeak('./doc1.md'),
|
||||
]
|
||||
*/
|
||||
const flatChunkNames = flat(chunkNames);
|
||||
Object.entries(flatChunkNames).forEach(([key, chunkName]) => {
|
||||
const chunkRegistry = registry[chunkName];
|
||||
|
|
|
@ -5,18 +5,27 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import type {RouteChunksTree} from '@docusaurus/types';
|
||||
import type {ChunkNames} from '@docusaurus/types';
|
||||
|
||||
const isTree = (x: string | RouteChunksTree): x is RouteChunksTree =>
|
||||
type Chunk = ChunkNames[string];
|
||||
type Tree = Exclude<Chunk, string>;
|
||||
|
||||
const isTree = (x: Chunk): x is Tree =>
|
||||
typeof x === 'object' && !!x && Object.keys(x).length > 0;
|
||||
|
||||
export default function flat(target: RouteChunksTree): {
|
||||
[keyPath: string]: string;
|
||||
} {
|
||||
/**
|
||||
* Takes a tree, and flattens it into a map of keyPath -> value.
|
||||
*
|
||||
* ```js
|
||||
* flat({ a: { b: 1 } }) === { "a.b": 1 };
|
||||
* flat({ a: [1, 2] }) === { "a.0": 1, "a.1": 2 };
|
||||
* ```
|
||||
*/
|
||||
export default function flat(target: ChunkNames): {[keyPath: string]: string} {
|
||||
const delimiter = '.';
|
||||
const output: {[keyPath: string]: string} = {};
|
||||
|
||||
function step(object: RouteChunksTree, prefix?: string | number) {
|
||||
function step(object: Tree, prefix?: string | number) {
|
||||
Object.entries(object).forEach(([key, value]) => {
|
||||
const newKey = prefix ? `${prefix}${delimiter}${key}` : key;
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@ exports[`loadRoutes loads flat route config 1`] = `
|
|||
},
|
||||
},
|
||||
"routesChunkNames": {
|
||||
"/blog-1e7": {
|
||||
"/blog-599": {
|
||||
"component": "component---theme-blog-list-pagea-6-a-7ba",
|
||||
"items": [
|
||||
{
|
||||
|
@ -39,29 +39,26 @@ exports[`loadRoutes loads flat route config 1`] = `
|
|||
},
|
||||
{
|
||||
"content": "content---blog-7-b-8-fd9",
|
||||
"metadata": null,
|
||||
},
|
||||
{
|
||||
"content": "content---blog-7-b-8-fd9",
|
||||
"metadata": null,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"routesConfig": "
|
||||
import React from 'react';
|
||||
"routesConfig": "import React from 'react';
|
||||
import ComponentCreator from '@docusaurus/ComponentCreator';
|
||||
|
||||
export default [
|
||||
{
|
||||
path: '/blog',
|
||||
component: ComponentCreator('/blog','1e7'),
|
||||
component: ComponentCreator('/blog', '599'),
|
||||
exact: true
|
||||
},
|
||||
{
|
||||
path: '*',
|
||||
component: ComponentCreator('*')
|
||||
}
|
||||
component: ComponentCreator('*'),
|
||||
},
|
||||
];
|
||||
",
|
||||
"routesPaths": [
|
||||
|
@ -119,24 +116,23 @@ exports[`loadRoutes loads nested route config 1`] = `
|
|||
"metadata": "metadata---docs-foo-baz-2-cf-fa7",
|
||||
},
|
||||
},
|
||||
"routesConfig": "
|
||||
import React from 'react';
|
||||
"routesConfig": "import React from 'react';
|
||||
import ComponentCreator from '@docusaurus/ComponentCreator';
|
||||
|
||||
export default [
|
||||
{
|
||||
path: '/docs:route',
|
||||
component: ComponentCreator('/docs:route','52d'),
|
||||
component: ComponentCreator('/docs:route', '52d'),
|
||||
routes: [
|
||||
{
|
||||
path: '/docs/hello',
|
||||
component: ComponentCreator('/docs/hello','44b'),
|
||||
component: ComponentCreator('/docs/hello', '44b'),
|
||||
exact: true,
|
||||
sidebar: \\"main\\"
|
||||
},
|
||||
{
|
||||
path: 'docs/foo/baz',
|
||||
component: ComponentCreator('docs/foo/baz','070'),
|
||||
component: ComponentCreator('docs/foo/baz', '070'),
|
||||
sidebar: \\"secondary\\",
|
||||
\\"key:a\\": \\"containing colon\\",
|
||||
\\"key'b\\": \\"containing quote\\",
|
||||
|
@ -148,8 +144,8 @@ export default [
|
|||
},
|
||||
{
|
||||
path: '*',
|
||||
component: ComponentCreator('*')
|
||||
}
|
||||
component: ComponentCreator('*'),
|
||||
},
|
||||
];
|
||||
",
|
||||
"routesPaths": [
|
||||
|
@ -173,19 +169,18 @@ exports[`loadRoutes loads route config with empty (but valid) path string 1`] =
|
|||
"component": "component---hello-world-jse-0-f-b6c",
|
||||
},
|
||||
},
|
||||
"routesConfig": "
|
||||
import React from 'react';
|
||||
"routesConfig": "import React from 'react';
|
||||
import ComponentCreator from '@docusaurus/ComponentCreator';
|
||||
|
||||
export default [
|
||||
{
|
||||
path: '',
|
||||
component: ComponentCreator('','b2a')
|
||||
component: ComponentCreator('', 'b2a')
|
||||
},
|
||||
{
|
||||
path: '*',
|
||||
component: ComponentCreator('*')
|
||||
}
|
||||
component: ComponentCreator('*'),
|
||||
},
|
||||
];
|
||||
",
|
||||
"routesPaths": [
|
||||
|
|
|
@ -6,9 +6,58 @@
|
|||
*/
|
||||
|
||||
import {jest} from '@jest/globals';
|
||||
import {loadRoutes, handleDuplicateRoutes} from '../routes';
|
||||
import {loadRoutes, handleDuplicateRoutes, genChunkName} from '../routes';
|
||||
import type {RouteConfig} from '@docusaurus/types';
|
||||
|
||||
describe('genChunkName', () => {
|
||||
it('works', () => {
|
||||
const firstAssert: {[key: string]: string} = {
|
||||
'/docs/adding-blog': 'docs-adding-blog-062',
|
||||
'/docs/versioning': 'docs-versioning-8a8',
|
||||
'/': 'index',
|
||||
'/blog/2018/04/30/How-I-Converted-Profilo-To-Docusaurus':
|
||||
'blog-2018-04-30-how-i-converted-profilo-to-docusaurus-4f2',
|
||||
'/youtube': 'youtube-429',
|
||||
'/users/en/': 'users-en-f7a',
|
||||
'/blog': 'blog-c06',
|
||||
};
|
||||
Object.keys(firstAssert).forEach((str) => {
|
||||
expect(genChunkName(str)).toBe(firstAssert[str]);
|
||||
});
|
||||
});
|
||||
|
||||
it("doesn't allow different chunk name for same path", () => {
|
||||
expect(genChunkName('path/is/similar', 'oldPrefix')).toEqual(
|
||||
genChunkName('path/is/similar', 'newPrefix'),
|
||||
);
|
||||
});
|
||||
|
||||
it('emits different chunk names for different paths even with same preferred name', () => {
|
||||
const secondAssert: {[key: string]: string} = {
|
||||
'/blog/1': 'blog-85-f-089',
|
||||
'/blog/2': 'blog-353-489',
|
||||
};
|
||||
Object.keys(secondAssert).forEach((str) => {
|
||||
expect(genChunkName(str, undefined, 'blog')).toBe(secondAssert[str]);
|
||||
});
|
||||
});
|
||||
|
||||
it('only generates short unique IDs', () => {
|
||||
const thirdAssert: {[key: string]: string} = {
|
||||
a: '0cc175b9',
|
||||
b: '92eb5ffe',
|
||||
c: '4a8a08f0',
|
||||
d: '8277e091',
|
||||
};
|
||||
Object.keys(thirdAssert).forEach((str) => {
|
||||
expect(genChunkName(str, undefined, undefined, true)).toBe(
|
||||
thirdAssert[str],
|
||||
);
|
||||
});
|
||||
expect(genChunkName('d', undefined, undefined, true)).toBe('8277e091');
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleDuplicateRoutes', () => {
|
||||
const routes: RouteConfig[] = [
|
||||
{
|
||||
|
@ -110,14 +159,12 @@ describe('loadRoutes', () => {
|
|||
},
|
||||
{
|
||||
content: 'blog/2018-12-14-Happy-First-Birthday-Slash.md',
|
||||
metadata: null,
|
||||
},
|
||||
{
|
||||
content: {
|
||||
__import: true,
|
||||
path: 'blog/2018-12-14-Happy-First-Birthday-Slash.md',
|
||||
},
|
||||
metadata: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
*/
|
||||
|
||||
import {docuHash, generate} from '@docusaurus/utils';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import type {
|
||||
LoadContext,
|
||||
|
@ -28,7 +27,8 @@ import {applyRouteTrailingSlash, sortConfig} from './routeConfig';
|
|||
|
||||
/**
|
||||
* Initializes the plugins, runs `loadContent`, `translateContent`,
|
||||
* `contentLoaded`, and `translateThemeConfig`.
|
||||
* `contentLoaded`, and `translateThemeConfig`. Because `contentLoaded` is
|
||||
* side-effect-ful (it generates temp files), so is this function.
|
||||
*/
|
||||
export async function loadPlugins(context: LoadContext): Promise<{
|
||||
plugins: LoadedPlugin[];
|
||||
|
@ -99,65 +99,53 @@ export async function loadPlugins(context: LoadContext): Promise<{
|
|||
if (!plugin.contentLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pluginId = plugin.options.id;
|
||||
|
||||
// plugins data files are namespaced by pluginName/pluginId
|
||||
const dataDirRoot = path.join(context.generatedFilesDir, plugin.name);
|
||||
const dataDir = path.join(dataDirRoot, pluginId);
|
||||
|
||||
const createData: PluginContentLoadedActions['createData'] = async (
|
||||
name,
|
||||
data,
|
||||
) => {
|
||||
const modulePath = path.join(dataDir, name);
|
||||
await fs.ensureDir(path.dirname(modulePath));
|
||||
await generate(dataDir, name, data);
|
||||
return modulePath;
|
||||
};
|
||||
|
||||
const dataDir = path.join(
|
||||
context.generatedFilesDir,
|
||||
plugin.name,
|
||||
pluginId,
|
||||
);
|
||||
// TODO this would be better to do all that in the codegen phase
|
||||
// TODO handle context for nested routes
|
||||
const pluginRouteContext: PluginRouteContext = {
|
||||
plugin: {name: plugin.name, id: pluginId},
|
||||
data: undefined, // TODO allow plugins to provide context data
|
||||
};
|
||||
const pluginRouteContextModulePath = await createData(
|
||||
const pluginRouteContextModulePath = path.join(
|
||||
dataDir,
|
||||
`${docuHash('pluginRouteContextModule')}.json`,
|
||||
);
|
||||
await generate(
|
||||
'/',
|
||||
pluginRouteContextModulePath,
|
||||
JSON.stringify(pluginRouteContext, null, 2),
|
||||
);
|
||||
|
||||
const addRoute: PluginContentLoadedActions['addRoute'] = (
|
||||
initialRouteConfig,
|
||||
) => {
|
||||
// Trailing slash behavior is handled in a generic way for all plugins
|
||||
const finalRouteConfig = applyRouteTrailingSlash(initialRouteConfig, {
|
||||
trailingSlash: context.siteConfig.trailingSlash,
|
||||
baseUrl: context.siteConfig.baseUrl,
|
||||
});
|
||||
pluginsRouteConfigs.push({
|
||||
...finalRouteConfig,
|
||||
modules: {
|
||||
...finalRouteConfig.modules,
|
||||
__routeContextModule: pluginRouteContextModulePath,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// the plugins global data are namespaced to avoid data conflicts:
|
||||
// - by plugin name
|
||||
// - by plugin id (allow using multiple instances of the same plugin)
|
||||
const setGlobalData: PluginContentLoadedActions['setGlobalData'] = (
|
||||
data,
|
||||
) => {
|
||||
globalData[plugin.name] = globalData[plugin.name] ?? {};
|
||||
globalData[plugin.name]![pluginId] = data;
|
||||
};
|
||||
|
||||
const actions: PluginContentLoadedActions = {
|
||||
addRoute,
|
||||
createData,
|
||||
setGlobalData,
|
||||
addRoute(initialRouteConfig) {
|
||||
// Trailing slash behavior is handled generically for all plugins
|
||||
const finalRouteConfig = applyRouteTrailingSlash(
|
||||
initialRouteConfig,
|
||||
context.siteConfig,
|
||||
);
|
||||
pluginsRouteConfigs.push({
|
||||
...finalRouteConfig,
|
||||
modules: {
|
||||
...finalRouteConfig.modules,
|
||||
__routeContextModule: pluginRouteContextModulePath,
|
||||
},
|
||||
});
|
||||
},
|
||||
async createData(name, data) {
|
||||
const modulePath = path.join(dataDir, name);
|
||||
await generate(dataDir, name, data);
|
||||
return modulePath;
|
||||
},
|
||||
setGlobalData(data) {
|
||||
globalData[plugin.name] ??= {};
|
||||
globalData[plugin.name]![pluginId] = data;
|
||||
},
|
||||
};
|
||||
|
||||
const translatedContent =
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
type ApplyTrailingSlashParams,
|
||||
} from '@docusaurus/utils-common';
|
||||
|
||||
/** Recursively applies trailing slash config to all nested routes. */
|
||||
export function applyRouteTrailingSlash(
|
||||
route: RouteConfig,
|
||||
params: ApplyTrailingSlashParams,
|
||||
|
|
|
@ -6,34 +6,90 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
genChunkName,
|
||||
docuHash,
|
||||
normalizeUrl,
|
||||
removeSuffix,
|
||||
simpleHash,
|
||||
escapePath,
|
||||
reportMessage,
|
||||
} from '@docusaurus/utils';
|
||||
import {stringify} from 'querystring';
|
||||
import _ from 'lodash';
|
||||
import query from 'querystring';
|
||||
import {getAllFinalRoutes} from './utils';
|
||||
import type {
|
||||
ChunkRegistry,
|
||||
Module,
|
||||
RouteConfig,
|
||||
RouteModule,
|
||||
RouteModules,
|
||||
ChunkNames,
|
||||
RouteChunkNames,
|
||||
ReportingSeverity,
|
||||
} from '@docusaurus/types';
|
||||
|
||||
type RegistryMap = {
|
||||
[chunkName: string]: ChunkRegistry;
|
||||
type LoadedRoutes = {
|
||||
/** Serialized routes config that can be directly emitted into temp file. */
|
||||
routesConfig: string;
|
||||
/** @see {ChunkNames} */
|
||||
routesChunkNames: RouteChunkNames;
|
||||
/** A map from chunk name to module loaders. */
|
||||
registry: {
|
||||
[chunkName: string]: {loader: string; modulePath: string};
|
||||
};
|
||||
/**
|
||||
* Collect all page paths for injecting it later in the plugin lifecycle.
|
||||
* This is useful for plugins like sitemaps, redirects etc... Only collects
|
||||
* "actual" pages, i.e. those without subroutes, because if a route has
|
||||
* subroutes, it is probably a wrapper.
|
||||
*/
|
||||
routesPaths: string[];
|
||||
};
|
||||
|
||||
/** Indents every line of `str` by one level. */
|
||||
function indent(str: string) {
|
||||
const spaces = ' ';
|
||||
return `${spaces}${str.replace(/\n/g, `\n${spaces}`)}`;
|
||||
return ` ${str.replace(/\n/g, `\n `)}`;
|
||||
}
|
||||
|
||||
function createRouteCodeString({
|
||||
const chunkNameCache = new Map<string, string>();
|
||||
|
||||
/**
|
||||
* Generates a unique chunk name that can be used in the chunk registry.
|
||||
*
|
||||
* @param modulePath A path to generate chunk name from. The actual value has no
|
||||
* semantic significance.
|
||||
* @param prefix A prefix to append to the chunk name, to avoid name clash.
|
||||
* @param preferredName Chunk names default to `modulePath`, and this can supply
|
||||
* a more human-readable name.
|
||||
* @param shortId When `true`, the chunk name would only be a hash without any
|
||||
* other characters. Useful for bundle size. Defaults to `true` in production.
|
||||
*/
|
||||
export function genChunkName(
|
||||
modulePath: string,
|
||||
prefix?: string,
|
||||
preferredName?: string,
|
||||
shortId: boolean = process.env.NODE_ENV === 'production',
|
||||
): string {
|
||||
let chunkName = chunkNameCache.get(modulePath);
|
||||
if (!chunkName) {
|
||||
if (shortId) {
|
||||
chunkName = simpleHash(modulePath, 8);
|
||||
} else {
|
||||
let str = modulePath;
|
||||
if (preferredName) {
|
||||
const shortHash = simpleHash(modulePath, 3);
|
||||
str = `${preferredName}${shortHash}`;
|
||||
}
|
||||
const name = str === '/' ? 'index' : docuHash(str);
|
||||
chunkName = prefix ? `${prefix}---${name}` : name;
|
||||
}
|
||||
chunkNameCache.set(modulePath, chunkName);
|
||||
}
|
||||
return chunkName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a piece of route config, and serializes it into raw JS code. The shape
|
||||
* is the same as react-router's `RouteConfig`. Formatting is similar to
|
||||
* `JSON.stringify` but without all the quotes.
|
||||
*/
|
||||
function serializeRouteConfig({
|
||||
routePath,
|
||||
routeHash,
|
||||
exact,
|
||||
|
@ -48,7 +104,7 @@ function createRouteCodeString({
|
|||
}) {
|
||||
const parts = [
|
||||
`path: '${routePath}'`,
|
||||
`component: ComponentCreator('${routePath}','${routeHash}')`,
|
||||
`component: ComponentCreator('${routePath}', '${routeHash}')`,
|
||||
];
|
||||
|
||||
if (exact) {
|
||||
|
@ -58,7 +114,7 @@ function createRouteCodeString({
|
|||
if (subroutesCodeStrings) {
|
||||
parts.push(
|
||||
`routes: [
|
||||
${indent(removeSuffix(subroutesCodeStrings.join(',\n'), ',\n'))}
|
||||
${indent(subroutesCodeStrings.join(',\n'))}
|
||||
]`,
|
||||
);
|
||||
}
|
||||
|
@ -89,96 +145,67 @@ ${indent(parts.join(',\n'))}
|
|||
}`;
|
||||
}
|
||||
|
||||
const NotFoundRouteCode = `{
|
||||
path: '*',
|
||||
component: ComponentCreator('*')
|
||||
}`;
|
||||
|
||||
const RoutesImportsCode = [
|
||||
`import React from 'react';`,
|
||||
`import ComponentCreator from '@docusaurus/ComponentCreator';`,
|
||||
].join('\n');
|
||||
|
||||
function isModule(value: unknown): value is Module {
|
||||
if (typeof value === 'string') {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
typeof value === 'object' &&
|
||||
const isModule = (value: unknown): value is Module =>
|
||||
typeof value === 'string' ||
|
||||
(typeof value === 'object' &&
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
(value as {[key: string]: unknown})?.__import &&
|
||||
(value as {[key: string]: unknown})?.path
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
!!(value as {[key: string]: unknown})?.__import);
|
||||
|
||||
/** Takes a {@link Module} and returns the string path it represents. */
|
||||
function getModulePath(target: Module): string {
|
||||
if (typeof target === 'string') {
|
||||
return target;
|
||||
}
|
||||
const queryStr = target.query ? `?${stringify(target.query)}` : '';
|
||||
const queryStr = target.query ? `?${query.stringify(target.query)}` : '';
|
||||
return `${target.path}${queryStr}`;
|
||||
}
|
||||
|
||||
function genRouteChunkNames(
|
||||
registry: RegistryMap,
|
||||
value: Module,
|
||||
prefix?: string,
|
||||
name?: string,
|
||||
): string;
|
||||
function genRouteChunkNames(
|
||||
registry: RegistryMap,
|
||||
value: RouteModule,
|
||||
prefix?: string,
|
||||
name?: string,
|
||||
/**
|
||||
* Takes a route module (which is a tree of modules), and transforms each module
|
||||
* into a chunk name. It also mutates `res.registry` and registers the loaders
|
||||
* for each chunk.
|
||||
*
|
||||
* @param routeModule One route module to be transformed.
|
||||
* @param prefix Prefix passed to {@link genChunkName}.
|
||||
* @param name Preferred name passed to {@link genChunkName}.
|
||||
* @param res The route structures being loaded.
|
||||
*/
|
||||
function genChunkNames(
|
||||
routeModule: RouteModules,
|
||||
prefix: string,
|
||||
name: string,
|
||||
res: LoadedRoutes,
|
||||
): ChunkNames;
|
||||
function genRouteChunkNames(
|
||||
registry: RegistryMap,
|
||||
value: RouteModule[],
|
||||
prefix?: string,
|
||||
name?: string,
|
||||
): ChunkNames[];
|
||||
function genRouteChunkNames(
|
||||
registry: RegistryMap,
|
||||
value: RouteModule | RouteModule[] | Module,
|
||||
prefix?: string,
|
||||
name?: string,
|
||||
function genChunkNames(
|
||||
routeModule: RouteModules | RouteModules[] | Module,
|
||||
prefix: string,
|
||||
name: string,
|
||||
res: LoadedRoutes,
|
||||
): ChunkNames | ChunkNames[] | string;
|
||||
function genRouteChunkNames(
|
||||
// TODO instead of passing a mutating the registry, return a registry slice?
|
||||
registry: RegistryMap,
|
||||
value: RouteModule | RouteModule[] | Module | null | undefined,
|
||||
prefix?: string,
|
||||
name?: string,
|
||||
): null | string | ChunkNames | ChunkNames[] {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((val, index) =>
|
||||
genRouteChunkNames(registry, val, `${index}`, name),
|
||||
);
|
||||
}
|
||||
|
||||
if (isModule(value)) {
|
||||
const modulePath = getModulePath(value);
|
||||
function genChunkNames(
|
||||
routeModule: RouteModules | RouteModules[] | Module,
|
||||
prefix: string,
|
||||
name: string,
|
||||
res: LoadedRoutes,
|
||||
): string | ChunkNames | ChunkNames[] {
|
||||
if (isModule(routeModule)) {
|
||||
// This is a leaf node, no need to recurse
|
||||
const modulePath = getModulePath(routeModule);
|
||||
const chunkName = genChunkName(modulePath, prefix, name);
|
||||
const loader = `() => import(/* webpackChunkName: '${chunkName}' */ '${escapePath(
|
||||
res.registry[chunkName] = {
|
||||
loader: `() => import(/* webpackChunkName: '${chunkName}' */ '${escapePath(
|
||||
modulePath,
|
||||
)}')`,
|
||||
modulePath,
|
||||
)}')`;
|
||||
|
||||
registry[chunkName] = {loader, modulePath};
|
||||
};
|
||||
return chunkName;
|
||||
}
|
||||
|
||||
const newValue: ChunkNames = {};
|
||||
Object.entries(value).forEach(([key, v]) => {
|
||||
newValue[key] = genRouteChunkNames(registry, v, key, name);
|
||||
});
|
||||
return newValue;
|
||||
if (Array.isArray(routeModule)) {
|
||||
return routeModule.map((val, index) =>
|
||||
genChunkNames(val, `${index}`, name, res),
|
||||
);
|
||||
}
|
||||
return _.mapValues(routeModule, (v, key) => genChunkNames(v, key, name, res));
|
||||
}
|
||||
|
||||
export function handleDuplicateRoutes(
|
||||
|
@ -212,80 +239,82 @@ This could lead to non-deterministic routing behavior.`;
|
|||
}
|
||||
}
|
||||
|
||||
export async function loadRoutes(
|
||||
pluginsRouteConfigs: RouteConfig[],
|
||||
baseUrl: string,
|
||||
onDuplicateRoutes: ReportingSeverity,
|
||||
): Promise<{
|
||||
registry: {[chunkName: string]: ChunkRegistry};
|
||||
routesConfig: string;
|
||||
routesChunkNames: {[routePath: string]: ChunkNames};
|
||||
routesPaths: string[];
|
||||
}> {
|
||||
handleDuplicateRoutes(pluginsRouteConfigs, onDuplicateRoutes);
|
||||
const registry: {[chunkName: string]: ChunkRegistry} = {};
|
||||
const routesPaths: string[] = [normalizeUrl([baseUrl, '404.html'])];
|
||||
const routesChunkNames: {[routePath: string]: ChunkNames} = {};
|
||||
/**
|
||||
* This is the higher level overview of route code generation. For each route
|
||||
* config node, it return the node's serialized form, and mutate `registry`,
|
||||
* `routesPaths`, and `routesChunkNames` accordingly.
|
||||
*/
|
||||
function genRouteCode(routeConfig: RouteConfig, res: LoadedRoutes): string {
|
||||
const {
|
||||
path: routePath,
|
||||
component,
|
||||
modules = {},
|
||||
routes: subroutes,
|
||||
priority,
|
||||
exact,
|
||||
...props
|
||||
} = routeConfig;
|
||||
|
||||
// This is the higher level overview of route code generation.
|
||||
function generateRouteCode(routeConfig: RouteConfig): string {
|
||||
const {
|
||||
path: routePath,
|
||||
component,
|
||||
modules = {},
|
||||
routes: subroutes,
|
||||
exact,
|
||||
priority,
|
||||
...props
|
||||
} = routeConfig;
|
||||
|
||||
if (typeof routePath !== 'string' || !component) {
|
||||
throw new Error(
|
||||
`Invalid route config: path must be a string and component is required.
|
||||
if (typeof routePath !== 'string' || !component) {
|
||||
throw new Error(
|
||||
`Invalid route config: path must be a string and component is required.
|
||||
${JSON.stringify(routeConfig)}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Collect all page paths for injecting it later in the plugin lifecycle
|
||||
// This is useful for plugins like sitemaps, redirects etc...
|
||||
// If a route has subroutes, it is not necessarily a valid page path (more
|
||||
// likely to be a wrapper)
|
||||
if (!subroutes) {
|
||||
routesPaths.push(routePath);
|
||||
}
|
||||
|
||||
// We hash the route to generate the key, because 2 routes can conflict with
|
||||
// each others if they have the same path, ex: parent=/docs, child=/docs
|
||||
// see https://github.com/facebook/docusaurus/issues/2917
|
||||
const routeHash = simpleHash(JSON.stringify(routeConfig), 3);
|
||||
const chunkNamesKey = `${routePath}-${routeHash}`;
|
||||
routesChunkNames[chunkNamesKey] = {
|
||||
...genRouteChunkNames(registry, {component}, 'component', component),
|
||||
...genRouteChunkNames(registry, modules, 'module', routePath),
|
||||
};
|
||||
|
||||
return createRouteCodeString({
|
||||
routePath: routeConfig.path.replace(/'/g, "\\'"),
|
||||
routeHash,
|
||||
exact,
|
||||
subroutesCodeStrings: subroutes?.map(generateRouteCode),
|
||||
props,
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
const routesConfig = `
|
||||
${RoutesImportsCode}
|
||||
if (!subroutes) {
|
||||
res.routesPaths.push(routePath);
|
||||
}
|
||||
|
||||
const routeHash = simpleHash(JSON.stringify(routeConfig), 3);
|
||||
res.routesChunkNames[`${routePath}-${routeHash}`] = {
|
||||
...genChunkNames({component}, 'component', component, res),
|
||||
...genChunkNames(modules, 'module', routePath, res),
|
||||
};
|
||||
|
||||
return serializeRouteConfig({
|
||||
routePath: routePath.replace(/'/g, "\\'"),
|
||||
routeHash,
|
||||
subroutesCodeStrings: subroutes?.map((r) => genRouteCode(r, res)),
|
||||
exact,
|
||||
props,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Routes are prepared into three temp files:
|
||||
*
|
||||
* - `routesConfig`, the route config passed to react-router. This file is kept
|
||||
* minimal, because it can't be code-splitted.
|
||||
* - `routesChunkNames`, a mapping from route paths (hashed) to code-splitted
|
||||
* chunk names.
|
||||
* - `registry`, a mapping from chunk names to options for react-loadable.
|
||||
*/
|
||||
export async function loadRoutes(
|
||||
routeConfigs: RouteConfig[],
|
||||
baseUrl: string,
|
||||
onDuplicateRoutes: ReportingSeverity,
|
||||
): Promise<LoadedRoutes> {
|
||||
handleDuplicateRoutes(routeConfigs, onDuplicateRoutes);
|
||||
const res: LoadedRoutes = {
|
||||
// To be written
|
||||
routesConfig: '',
|
||||
routesChunkNames: {},
|
||||
registry: {},
|
||||
routesPaths: [normalizeUrl([baseUrl, '404.html'])],
|
||||
};
|
||||
|
||||
res.routesConfig = `import React from 'react';
|
||||
import ComponentCreator from '@docusaurus/ComponentCreator';
|
||||
|
||||
export default [
|
||||
${indent(`${pluginsRouteConfigs.map(generateRouteCode).join(',\n')},`)}
|
||||
${indent(NotFoundRouteCode)}
|
||||
${indent(`${routeConfigs.map((r) => genRouteCode(r, res)).join(',\n')},`)}
|
||||
{
|
||||
path: '*',
|
||||
component: ComponentCreator('*'),
|
||||
},
|
||||
];
|
||||
`;
|
||||
|
||||
return {
|
||||
registry,
|
||||
routesConfig,
|
||||
routesChunkNames,
|
||||
routesPaths,
|
||||
};
|
||||
return res;
|
||||
}
|
||||
|
|
|
@ -14,10 +14,7 @@ import {
|
|||
applyConfigurePostCss,
|
||||
getHttpsConfig,
|
||||
} from '../utils';
|
||||
import type {
|
||||
ConfigureWebpackFn,
|
||||
ConfigureWebpackFnMergeStrategy,
|
||||
} from '@docusaurus/types';
|
||||
import type {Plugin} from '@docusaurus/types';
|
||||
|
||||
describe('customize JS loader', () => {
|
||||
it('getCustomizableJSLoader defaults to babel loader', () => {
|
||||
|
@ -63,7 +60,7 @@ describe('extending generated webpack config', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const configureWebpack: ConfigureWebpackFn = (
|
||||
const configureWebpack: Plugin['configureWebpack'] = (
|
||||
generatedConfig,
|
||||
isServer,
|
||||
) => {
|
||||
|
@ -99,7 +96,7 @@ describe('extending generated webpack config', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const configureWebpack: ConfigureWebpackFn = () => ({
|
||||
const configureWebpack: Plugin['configureWebpack'] = () => ({
|
||||
entry: 'entry.js',
|
||||
output: {
|
||||
path: path.join(__dirname, 'dist'),
|
||||
|
@ -128,9 +125,9 @@ describe('extending generated webpack config', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const createConfigureWebpack: (
|
||||
mergeStrategy?: ConfigureWebpackFnMergeStrategy,
|
||||
) => ConfigureWebpackFn = (mergeStrategy) => () => ({
|
||||
const createConfigureWebpack: (mergeStrategy?: {
|
||||
[key: string]: 'prepend' | 'append';
|
||||
}) => Plugin['configureWebpack'] = (mergeStrategy) => () => ({
|
||||
module: {
|
||||
rules: [{use: 'zzz'}],
|
||||
},
|
||||
|
|
|
@ -25,8 +25,7 @@ import crypto from 'crypto';
|
|||
import logger from '@docusaurus/logger';
|
||||
import type {TransformOptions} from '@babel/core';
|
||||
import type {
|
||||
ConfigureWebpackFn,
|
||||
ConfigurePostCssFn,
|
||||
Plugin,
|
||||
PostCssOptions,
|
||||
ConfigureWebpackUtils,
|
||||
} from '@docusaurus/types';
|
||||
|
@ -172,7 +171,7 @@ export const getCustomizableJSLoader =
|
|||
* @returns final/ modified webpack config
|
||||
*/
|
||||
export function applyConfigureWebpack(
|
||||
configureWebpack: ConfigureWebpackFn,
|
||||
configureWebpack: NonNullable<Plugin['configureWebpack']>,
|
||||
config: Configuration,
|
||||
isServer: boolean,
|
||||
jsLoader: 'babel' | ((isServer: boolean) => RuleSetRule) | undefined,
|
||||
|
@ -198,7 +197,7 @@ export function applyConfigureWebpack(
|
|||
}
|
||||
|
||||
export function applyConfigurePostCss(
|
||||
configurePostCss: NonNullable<ConfigurePostCssFn>,
|
||||
configurePostCss: NonNullable<Plugin['configurePostCss']>,
|
||||
config: Configuration,
|
||||
): Configuration {
|
||||
type LocalPostCSSLoader = unknown & {
|
||||
|
|
|
@ -46,13 +46,13 @@ Create a route to add to the website.
|
|||
interface RouteConfig {
|
||||
path: string;
|
||||
component: string;
|
||||
modules?: RouteModule;
|
||||
modules?: RouteModules;
|
||||
routes?: RouteConfig[];
|
||||
exact?: boolean;
|
||||
priority?: number;
|
||||
}
|
||||
interface RouteModule {
|
||||
[module: string]: Module | RouteModule | RouteModule[];
|
||||
interface RouteModules {
|
||||
[module: string]: Module | RouteModules | RouteModules[];
|
||||
}
|
||||
type Module =
|
||||
| {
|
||||
|
|
Loading…
Add table
Reference in a new issue