mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-02 11:47:23 +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' {
|
declare module '@generated/registry' {
|
||||||
const registry: {
|
import type {Registry} from '@docusaurus/types';
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
readonly [key: string]: [() => Promise<any>, string, string];
|
const registry: Registry;
|
||||||
};
|
|
||||||
export default registry;
|
export default registry;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@generated/routes' {
|
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;
|
export default routes;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@generated/routesChunkNames' {
|
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;
|
export = routesChunkNames;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -292,10 +292,7 @@ export default async function pluginContentBlog(
|
||||||
exact: true,
|
exact: true,
|
||||||
modules: {
|
modules: {
|
||||||
sidebar: aliasedSource(sidebarProp),
|
sidebar: aliasedSource(sidebarProp),
|
||||||
items: items.map((postID) =>
|
items: items.map((postID) => ({
|
||||||
// To tell routes.js this is an import and not a nested object
|
|
||||||
// to recurse.
|
|
||||||
({
|
|
||||||
content: {
|
content: {
|
||||||
__import: true,
|
__import: true,
|
||||||
path: blogItemsToMetadata[postID]!.source,
|
path: blogItemsToMetadata[postID]!.source,
|
||||||
|
@ -303,8 +300,7 @@ export default async function pluginContentBlog(
|
||||||
truncated: true,
|
truncated: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
})),
|
||||||
),
|
|
||||||
metadata: aliasedSource(pageMetadataPath),
|
metadata: aliasedSource(pageMetadataPath),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* 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';
|
import {findHomePageRoute, isSamePath} from '../routesUtils';
|
||||||
|
|
||||||
describe('isSamePath', () => {
|
describe('isSamePath', () => {
|
||||||
|
@ -41,7 +41,7 @@ describe('isSamePath', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('findHomePageRoute', () => {
|
describe('findHomePageRoute', () => {
|
||||||
const homePage: Route = {
|
const homePage: RouteConfig = {
|
||||||
path: '/',
|
path: '/',
|
||||||
exact: true,
|
exact: true,
|
||||||
};
|
};
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
import {useMemo} from 'react';
|
import {useMemo} from 'react';
|
||||||
import generatedRoutes from '@generated/routes';
|
import generatedRoutes from '@generated/routes';
|
||||||
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
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
|
* Compare the 2 paths, case insensitive and ignoring trailing slash
|
||||||
|
@ -34,18 +34,18 @@ export function findHomePageRoute({
|
||||||
baseUrl,
|
baseUrl,
|
||||||
routes: initialRoutes,
|
routes: initialRoutes,
|
||||||
}: {
|
}: {
|
||||||
routes: Route[];
|
routes: RouteConfig[];
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
}): Route | undefined {
|
}): RouteConfig | undefined {
|
||||||
function isHomePageRoute(route: Route): boolean {
|
function isHomePageRoute(route: RouteConfig): boolean {
|
||||||
return route.path === baseUrl && route.exact === true;
|
return route.path === baseUrl && route.exact === true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isHomeParentRoute(route: Route): boolean {
|
function isHomeParentRoute(route: RouteConfig): boolean {
|
||||||
return route.path === baseUrl && !route.exact;
|
return route.path === baseUrl && !route.exact;
|
||||||
}
|
}
|
||||||
|
|
||||||
function doFindHomePageRoute(routes: Route[]): Route | undefined {
|
function doFindHomePageRoute(routes: RouteConfig[]): RouteConfig | undefined {
|
||||||
if (routes.length === 0) {
|
if (routes.length === 0) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
@ -66,7 +66,7 @@ export function findHomePageRoute({
|
||||||
* Fetches the route that points to "/". Use this instead of the naive "/",
|
* Fetches the route that points to "/". Use this instead of the naive "/",
|
||||||
* because the homepage may not exist.
|
* because the homepage may not exist.
|
||||||
*/
|
*/
|
||||||
export function useHomePageRoute(): Route | undefined {
|
export function useHomePageRoute(): RouteConfig | undefined {
|
||||||
const {baseUrl} = useDocusaurusContext().siteConfig;
|
const {baseUrl} = useDocusaurusContext().siteConfig;
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() => findHomePageRoute({routes: generatedRoutes, baseUrl}),
|
() => 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 {ParsedUrlQueryInput} from 'querystring';
|
||||||
import type Joi from 'joi';
|
import type Joi from 'joi';
|
||||||
import type {
|
import type {
|
||||||
|
DeepRequired,
|
||||||
Required as RequireKeys,
|
Required as RequireKeys,
|
||||||
DeepPartial,
|
DeepPartial,
|
||||||
DeepRequired,
|
|
||||||
} from 'utility-types';
|
} from 'utility-types';
|
||||||
import type {Location} from 'history';
|
import type {Location} from 'history';
|
||||||
import type Loadable from 'react-loadable';
|
|
||||||
|
// === Configuration ===
|
||||||
|
|
||||||
export type ReportingSeverity = 'ignore' | 'log' | 'warn' | 'error' | 'throw';
|
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 = {
|
export type ThemeConfig = {
|
||||||
[key: string]: unknown;
|
[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.
|
* Docusaurus config, after validation/normalization.
|
||||||
*/
|
*/
|
||||||
|
@ -92,6 +115,8 @@ export type Config = RequireKeys<
|
||||||
'title' | 'url' | 'baseUrl'
|
'title' | 'url' | 'baseUrl'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
// === Data loading ===
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* - `type: 'package'`, plugin is in a different package.
|
* - `type: 'package'`, plugin is in a different package.
|
||||||
* - `type: 'project'`, plugin is in the same docusaurus project.
|
* - `type: 'project'`, plugin is in the same docusaurus project.
|
||||||
|
@ -115,26 +140,29 @@ export type SiteMetadata = {
|
||||||
readonly pluginVersions: {[pluginName: string]: PluginVersionInformation};
|
readonly pluginVersions: {[pluginName: string]: PluginVersionInformation};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Inspired by Chrome JSON, because it's a widely supported i18n format
|
/**
|
||||||
// https://developer.chrome.com/apps/i18n-messages
|
* Inspired by Chrome JSON, because it's a widely supported i18n format
|
||||||
// https://support.crowdin.com/file-formats/chrome-json/
|
* @see https://developer.chrome.com/apps/i18n-messages
|
||||||
// https://www.applanga.com/docs/formats/chrome_i18n_json
|
* @see https://support.crowdin.com/file-formats/chrome-json/
|
||||||
// https://docs.transifex.com/formats/chrome-json
|
* @see https://www.applanga.com/docs/formats/chrome_i18n_json
|
||||||
// https://help.phrase.com/help/chrome-json-messages
|
* @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 TranslationMessage = {message: string; description?: string};
|
||||||
export type TranslationFileContent = {[key: string]: TranslationMessage};
|
export type TranslationFileContent = {[key: string]: TranslationMessage};
|
||||||
export type TranslationFile = {path: string; content: TranslationFileContent};
|
/**
|
||||||
|
* An abstract representation of how a translation file exists on disk. The core
|
||||||
export type I18nLocaleConfig = {
|
* would handle the file reading/writing; plugins just need to deal with
|
||||||
label: string;
|
* translations in-memory.
|
||||||
htmlLang: string;
|
*/
|
||||||
direction: string;
|
export type TranslationFile = {
|
||||||
};
|
/**
|
||||||
|
* Relative to the directory where it's expected to be found. For plugin
|
||||||
export type I18nConfig = {
|
* files, it's relative to `i18n/<locale>/<pluginName>/<pluginId>`. Should NOT
|
||||||
defaultLocale: string;
|
* have any extension.
|
||||||
locales: [string, ...string[]];
|
*/
|
||||||
localeConfigs: {[locale: string]: Partial<I18nLocaleConfig>};
|
path: string;
|
||||||
|
content: TranslationFileContent;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type I18n = DeepRequired<I18nConfig> & {currentLocale: string};
|
export type I18n = DeepRequired<I18nConfig> & {currentLocale: string};
|
||||||
|
@ -153,21 +181,6 @@ export type DocusaurusContext = {
|
||||||
// isBrowser: boolean; // Not here on purpose!
|
// 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 = {
|
export type HostPortCLIOptions = {
|
||||||
host?: string;
|
host?: string;
|
||||||
port?: string;
|
port?: string;
|
||||||
|
@ -191,14 +204,11 @@ export type ServeCLIOptions = HostPortCLIOptions &
|
||||||
build: boolean;
|
build: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type BuildOptions = ConfigOptions & {
|
export type BuildCLIOptions = ConfigOptions & {
|
||||||
bundleAnalyzer: boolean;
|
bundleAnalyzer: boolean;
|
||||||
outDir: string;
|
outDir: string;
|
||||||
minify: boolean;
|
minify: boolean;
|
||||||
skipBuild: boolean;
|
skipBuild: boolean;
|
||||||
};
|
|
||||||
|
|
||||||
export type BuildCLIOptions = BuildOptions & {
|
|
||||||
locale?: string;
|
locale?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -219,8 +229,6 @@ export type LoadContext = {
|
||||||
codeTranslations: {[msgId: string]: string};
|
codeTranslations: {[msgId: string]: string};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type HtmlTags = string | HtmlTagObject | (string | HtmlTagObject)[];
|
|
||||||
|
|
||||||
export type Props = LoadContext & {
|
export type Props = LoadContext & {
|
||||||
readonly headTags: string;
|
readonly headTags: string;
|
||||||
readonly preBodyTags: string;
|
readonly preBodyTags: string;
|
||||||
|
@ -231,12 +239,27 @@ export type Props = LoadContext & {
|
||||||
readonly plugins: LoadedPlugin[];
|
readonly plugins: LoadedPlugin[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// === Plugin ===
|
||||||
|
|
||||||
export type PluginContentLoadedActions = {
|
export type PluginContentLoadedActions = {
|
||||||
addRoute: (config: RouteConfig) => void;
|
addRoute: (config: RouteConfig) => void;
|
||||||
createData: (name: string, data: string) => Promise<string>;
|
createData: (name: string, data: string) => Promise<string>;
|
||||||
setGlobalData: (data: unknown) => void;
|
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 = {
|
export type AllContent = {
|
||||||
[pluginName: string]: {
|
[pluginName: string]: {
|
||||||
[pluginID: string]: unknown;
|
[pluginID: string]: unknown;
|
||||||
|
@ -246,6 +269,37 @@ export type AllContent = {
|
||||||
// TODO improve type (not exposed by postcss-loader)
|
// TODO improve type (not exposed by postcss-loader)
|
||||||
export type PostCssOptions = {[key: string]: unknown} & {plugins: unknown[]};
|
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> = {
|
export type Plugin<Content = unknown> = {
|
||||||
name: string;
|
name: string;
|
||||||
loadContent?: () => Promise<Content>;
|
loadContent?: () => Promise<Content>;
|
||||||
|
@ -266,7 +320,9 @@ export type Plugin<Content = unknown> = {
|
||||||
utils: ConfigureWebpackUtils,
|
utils: ConfigureWebpackUtils,
|
||||||
content: Content,
|
content: Content,
|
||||||
) => WebpackConfiguration & {
|
) => WebpackConfiguration & {
|
||||||
mergeStrategy?: ConfigureWebpackFnMergeStrategy;
|
mergeStrategy?: {
|
||||||
|
[key: string]: CustomizeRuleString;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
configurePostCss?: (options: PostCssOptions) => PostCssOptions;
|
configurePostCss?: (options: PostCssOptions) => PostCssOptions;
|
||||||
getThemePath?: () => string;
|
getThemePath?: () => string;
|
||||||
|
@ -334,9 +390,7 @@ export type NormalizedPluginConfig = {
|
||||||
export type InitializedPlugin = Plugin & {
|
export type InitializedPlugin = Plugin & {
|
||||||
readonly options: Required<PluginOptions>;
|
readonly options: Required<PluginOptions>;
|
||||||
readonly version: PluginVersionInformation;
|
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;
|
readonly path: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -372,48 +426,71 @@ export type ImportedPluginModule = PluginModule & {
|
||||||
default?: PluginModule;
|
default?: PluginModule;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ConfigureWebpackFn = Plugin['configureWebpack'];
|
export type Preset = {
|
||||||
export type ConfigureWebpackFnMergeStrategy = {
|
plugins?: PluginConfig[];
|
||||||
[key: string]: CustomizeRuleString;
|
themes?: PluginConfig[];
|
||||||
};
|
|
||||||
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 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 =
|
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;
|
__import?: boolean;
|
||||||
|
path: string;
|
||||||
query?: ParsedUrlQueryInput;
|
query?: ParsedUrlQueryInput;
|
||||||
}
|
}
|
||||||
| string;
|
| string;
|
||||||
|
|
||||||
export type RouteModule = {
|
/**
|
||||||
[module: string]: Module | RouteModule | RouteModule[];
|
* 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
|
||||||
export type ChunkNames = {
|
* code-split.
|
||||||
[name: string]: string | null | ChunkNames | ChunkNames[];
|
*/
|
||||||
|
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 = {
|
export type RouteConfig = {
|
||||||
|
/** With leading slash. Trailing slash will be normalized by config. */
|
||||||
path: string;
|
path: string;
|
||||||
|
/** Component used to render this route, a path that Webpack can `require`. */
|
||||||
component: string;
|
component: string;
|
||||||
modules?: RouteModule;
|
/**
|
||||||
|
* Props. Each entry should be `[propName]: pathToPropModule` (created with
|
||||||
|
* `createData`)
|
||||||
|
*/
|
||||||
|
modules?: RouteModules;
|
||||||
|
/** Nested routes config. */
|
||||||
routes?: RouteConfig[];
|
routes?: RouteConfig[];
|
||||||
|
/** React router config option: `exact` routes would not match subroutes. */
|
||||||
exact?: boolean;
|
exact?: boolean;
|
||||||
|
/** Used to sort routes. Higher-priority routes will be placed first. */
|
||||||
priority?: number;
|
priority?: number;
|
||||||
|
/** Extra props; will be copied to routes.js. */
|
||||||
[propName: string]: unknown;
|
[propName: string]: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -435,11 +512,57 @@ export type PluginRouteContext = RouteContext & {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Route = {
|
/**
|
||||||
readonly path: string;
|
* The shape would be isomorphic to {@link RouteModules}:
|
||||||
readonly component: ReturnType<typeof Loadable>;
|
* {@link Module} -> `string`, `RouteModules[]` -> `ChunkNames[]`.
|
||||||
readonly exact?: boolean;
|
*
|
||||||
readonly routes?: Route[];
|
* 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;
|
[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 = {
|
export type TOCItem = {
|
||||||
readonly value: string;
|
readonly value: string;
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
readonly level: number;
|
readonly level: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RouteChunksTree = {[x: string | number]: string | RouteChunksTree};
|
|
||||||
|
|
||||||
export type ClientModule = {
|
export type ClientModule = {
|
||||||
onRouteUpdate?: (args: {
|
onRouteUpdate?: (args: {
|
||||||
previousLocation: Location | null;
|
previousLocation: Location | null;
|
||||||
|
|
|
@ -6,59 +6,10 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {jest} from '@jest/globals';
|
import {jest} from '@jest/globals';
|
||||||
import {genChunkName, readOutputHTMLFile, generate} from '../emitUtils';
|
import {readOutputHTMLFile, generate} from '../emitUtils';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import fs from 'fs-extra';
|
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', () => {
|
describe('readOutputHTMLFile', () => {
|
||||||
it('trailing slash undefined', async () => {
|
it('trailing slash undefined', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
|
|
|
@ -8,7 +8,6 @@
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import {createHash} from 'crypto';
|
import {createHash} from 'crypto';
|
||||||
import {simpleHash, docuHash} from './hashUtils';
|
|
||||||
import {findAsyncSequential} from './jsUtils';
|
import {findAsyncSequential} from './jsUtils';
|
||||||
|
|
||||||
const fileHash = new Map<string, string>();
|
const fileHash = new Map<string, string>();
|
||||||
|
@ -18,7 +17,8 @@ const fileHash = new Map<string, string>();
|
||||||
* differs from cache (for hot reload performance).
|
* differs from cache (for hot reload performance).
|
||||||
*
|
*
|
||||||
* @param generatedFilesDir Absolute path.
|
* @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 content String content to write.
|
||||||
* @param skipCache If `true` (defaults as `true` for production), file is
|
* @param skipCache If `true` (defaults as `true` for production), file is
|
||||||
* force-rewritten, skipping cache.
|
* force-rewritten, skipping cache.
|
||||||
|
@ -29,7 +29,7 @@ export async function generate(
|
||||||
content: string,
|
content: string,
|
||||||
skipCache: boolean = process.env.NODE_ENV === 'production',
|
skipCache: boolean = process.env.NODE_ENV === 'production',
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const filepath = path.join(generatedFilesDir, file);
|
const filepath = path.resolve(generatedFilesDir, file);
|
||||||
|
|
||||||
if (skipCache) {
|
if (skipCache) {
|
||||||
await fs.outputFile(filepath, content);
|
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 permalink The URL that the HTML file corresponds to, without base URL
|
||||||
* @param outDir Full path to the output directory
|
* @param outDir Full path to the output directory
|
||||||
|
|
|
@ -22,7 +22,7 @@ export {
|
||||||
DEFAULT_PLUGIN_ID,
|
DEFAULT_PLUGIN_ID,
|
||||||
WEBPACK_URL_LOADER_LIMIT,
|
WEBPACK_URL_LOADER_LIMIT,
|
||||||
} from './constants';
|
} from './constants';
|
||||||
export {generate, genChunkName, readOutputHTMLFile} from './emitUtils';
|
export {generate, readOutputHTMLFile} from './emitUtils';
|
||||||
export {
|
export {
|
||||||
getFileCommitDate,
|
getFileCommitDate,
|
||||||
FileNotTrackedError,
|
FileNotTrackedError,
|
||||||
|
|
|
@ -34,20 +34,16 @@ const canPrefetch = (routePath: string) =>
|
||||||
const canPreload = (routePath: string) =>
|
const canPreload = (routePath: string) =>
|
||||||
!isSlowConnection() && !loaded[routePath];
|
!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[] =>
|
const getChunkNamesToLoad = (path: string): string[] =>
|
||||||
Object.entries(routesChunkNames)
|
Object.entries(routesChunkNames)
|
||||||
.filter(
|
.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]) =>
|
.flatMap(([, routeChunks]) => Object.values(flat(routeChunks)));
|
||||||
// flat() is useful for nested chunk names, it's not like array.flat()
|
|
||||||
Object.values(flat(routeChunks)),
|
|
||||||
);
|
|
||||||
|
|
||||||
const docusaurus = {
|
const docusaurus = {
|
||||||
prefetch: (routePath: string): boolean => {
|
prefetch: (routePath: string): boolean => {
|
||||||
|
|
|
@ -34,27 +34,12 @@ export default function ComponentCreator(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const chunkNamesKey = `${path}-${hash}`;
|
const chunkNames = routesChunkNames[`${path}-${hash}`]!;
|
||||||
const chunkNames = routesChunkNames[chunkNamesKey]!;
|
|
||||||
const optsModules: string[] = [];
|
|
||||||
const optsWebpack: string[] = [];
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const optsLoader: {[key: string]: () => Promise<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);
|
const flatChunkNames = flat(chunkNames);
|
||||||
Object.entries(flatChunkNames).forEach(([key, chunkName]) => {
|
Object.entries(flatChunkNames).forEach(([key, chunkName]) => {
|
||||||
const chunkRegistry = registry[chunkName];
|
const chunkRegistry = registry[chunkName];
|
||||||
|
|
|
@ -5,18 +5,27 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* 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;
|
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 delimiter = '.';
|
||||||
const output: {[keyPath: string]: string} = {};
|
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]) => {
|
Object.entries(object).forEach(([key, value]) => {
|
||||||
const newKey = prefix ? `${prefix}${delimiter}${key}` : key;
|
const newKey = prefix ? `${prefix}${delimiter}${key}` : key;
|
||||||
|
|
||||||
|
|
|
@ -30,7 +30,7 @@ exports[`loadRoutes loads flat route config 1`] = `
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"routesChunkNames": {
|
"routesChunkNames": {
|
||||||
"/blog-1e7": {
|
"/blog-599": {
|
||||||
"component": "component---theme-blog-list-pagea-6-a-7ba",
|
"component": "component---theme-blog-list-pagea-6-a-7ba",
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
|
@ -39,29 +39,26 @@ exports[`loadRoutes loads flat route config 1`] = `
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"content": "content---blog-7-b-8-fd9",
|
"content": "content---blog-7-b-8-fd9",
|
||||||
"metadata": null,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"content": "content---blog-7-b-8-fd9",
|
"content": "content---blog-7-b-8-fd9",
|
||||||
"metadata": null,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"routesConfig": "
|
"routesConfig": "import React from 'react';
|
||||||
import React from 'react';
|
|
||||||
import ComponentCreator from '@docusaurus/ComponentCreator';
|
import ComponentCreator from '@docusaurus/ComponentCreator';
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
{
|
{
|
||||||
path: '/blog',
|
path: '/blog',
|
||||||
component: ComponentCreator('/blog','1e7'),
|
component: ComponentCreator('/blog', '599'),
|
||||||
exact: true
|
exact: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '*',
|
path: '*',
|
||||||
component: ComponentCreator('*')
|
component: ComponentCreator('*'),
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
",
|
",
|
||||||
"routesPaths": [
|
"routesPaths": [
|
||||||
|
@ -119,8 +116,7 @@ exports[`loadRoutes loads nested route config 1`] = `
|
||||||
"metadata": "metadata---docs-foo-baz-2-cf-fa7",
|
"metadata": "metadata---docs-foo-baz-2-cf-fa7",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"routesConfig": "
|
"routesConfig": "import React from 'react';
|
||||||
import React from 'react';
|
|
||||||
import ComponentCreator from '@docusaurus/ComponentCreator';
|
import ComponentCreator from '@docusaurus/ComponentCreator';
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
|
@ -148,8 +144,8 @@ export default [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '*',
|
path: '*',
|
||||||
component: ComponentCreator('*')
|
component: ComponentCreator('*'),
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
",
|
",
|
||||||
"routesPaths": [
|
"routesPaths": [
|
||||||
|
@ -173,8 +169,7 @@ exports[`loadRoutes loads route config with empty (but valid) path string 1`] =
|
||||||
"component": "component---hello-world-jse-0-f-b6c",
|
"component": "component---hello-world-jse-0-f-b6c",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"routesConfig": "
|
"routesConfig": "import React from 'react';
|
||||||
import React from 'react';
|
|
||||||
import ComponentCreator from '@docusaurus/ComponentCreator';
|
import ComponentCreator from '@docusaurus/ComponentCreator';
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
|
@ -184,8 +179,8 @@ export default [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '*',
|
path: '*',
|
||||||
component: ComponentCreator('*')
|
component: ComponentCreator('*'),
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
",
|
",
|
||||||
"routesPaths": [
|
"routesPaths": [
|
||||||
|
|
|
@ -6,9 +6,58 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {jest} from '@jest/globals';
|
import {jest} from '@jest/globals';
|
||||||
import {loadRoutes, handleDuplicateRoutes} from '../routes';
|
import {loadRoutes, handleDuplicateRoutes, genChunkName} from '../routes';
|
||||||
import type {RouteConfig} from '@docusaurus/types';
|
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', () => {
|
describe('handleDuplicateRoutes', () => {
|
||||||
const routes: RouteConfig[] = [
|
const routes: RouteConfig[] = [
|
||||||
{
|
{
|
||||||
|
@ -110,14 +159,12 @@ describe('loadRoutes', () => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
content: 'blog/2018-12-14-Happy-First-Birthday-Slash.md',
|
content: 'blog/2018-12-14-Happy-First-Birthday-Slash.md',
|
||||||
metadata: null,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
content: {
|
content: {
|
||||||
__import: true,
|
__import: true,
|
||||||
path: 'blog/2018-12-14-Happy-First-Birthday-Slash.md',
|
path: 'blog/2018-12-14-Happy-First-Birthday-Slash.md',
|
||||||
},
|
},
|
||||||
metadata: null,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {docuHash, generate} from '@docusaurus/utils';
|
import {docuHash, generate} from '@docusaurus/utils';
|
||||||
import fs from 'fs-extra';
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import type {
|
import type {
|
||||||
LoadContext,
|
LoadContext,
|
||||||
|
@ -28,7 +27,8 @@ import {applyRouteTrailingSlash, sortConfig} from './routeConfig';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the plugins, runs `loadContent`, `translateContent`,
|
* 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<{
|
export async function loadPlugins(context: LoadContext): Promise<{
|
||||||
plugins: LoadedPlugin[];
|
plugins: LoadedPlugin[];
|
||||||
|
@ -99,42 +99,36 @@ export async function loadPlugins(context: LoadContext): Promise<{
|
||||||
if (!plugin.contentLoaded) {
|
if (!plugin.contentLoaded) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pluginId = plugin.options.id;
|
const pluginId = plugin.options.id;
|
||||||
|
|
||||||
// plugins data files are namespaced by pluginName/pluginId
|
// plugins data files are namespaced by pluginName/pluginId
|
||||||
const dataDirRoot = path.join(context.generatedFilesDir, plugin.name);
|
const dataDir = path.join(
|
||||||
const dataDir = path.join(dataDirRoot, pluginId);
|
context.generatedFilesDir,
|
||||||
|
plugin.name,
|
||||||
const createData: PluginContentLoadedActions['createData'] = async (
|
pluginId,
|
||||||
name,
|
);
|
||||||
data,
|
|
||||||
) => {
|
|
||||||
const modulePath = path.join(dataDir, name);
|
|
||||||
await fs.ensureDir(path.dirname(modulePath));
|
|
||||||
await generate(dataDir, name, data);
|
|
||||||
return modulePath;
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO this would be better to do all that in the codegen phase
|
// TODO this would be better to do all that in the codegen phase
|
||||||
// TODO handle context for nested routes
|
// TODO handle context for nested routes
|
||||||
const pluginRouteContext: PluginRouteContext = {
|
const pluginRouteContext: PluginRouteContext = {
|
||||||
plugin: {name: plugin.name, id: pluginId},
|
plugin: {name: plugin.name, id: pluginId},
|
||||||
data: undefined, // TODO allow plugins to provide context data
|
data: undefined, // TODO allow plugins to provide context data
|
||||||
};
|
};
|
||||||
const pluginRouteContextModulePath = await createData(
|
const pluginRouteContextModulePath = path.join(
|
||||||
|
dataDir,
|
||||||
`${docuHash('pluginRouteContextModule')}.json`,
|
`${docuHash('pluginRouteContextModule')}.json`,
|
||||||
|
);
|
||||||
|
await generate(
|
||||||
|
'/',
|
||||||
|
pluginRouteContextModulePath,
|
||||||
JSON.stringify(pluginRouteContext, null, 2),
|
JSON.stringify(pluginRouteContext, null, 2),
|
||||||
);
|
);
|
||||||
|
|
||||||
const addRoute: PluginContentLoadedActions['addRoute'] = (
|
const actions: PluginContentLoadedActions = {
|
||||||
|
addRoute(initialRouteConfig) {
|
||||||
|
// Trailing slash behavior is handled generically for all plugins
|
||||||
|
const finalRouteConfig = applyRouteTrailingSlash(
|
||||||
initialRouteConfig,
|
initialRouteConfig,
|
||||||
) => {
|
context.siteConfig,
|
||||||
// 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({
|
pluginsRouteConfigs.push({
|
||||||
...finalRouteConfig,
|
...finalRouteConfig,
|
||||||
modules: {
|
modules: {
|
||||||
|
@ -142,22 +136,16 @@ export async function loadPlugins(context: LoadContext): Promise<{
|
||||||
__routeContextModule: pluginRouteContextModulePath,
|
__routeContextModule: pluginRouteContextModulePath,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
},
|
||||||
|
async createData(name, data) {
|
||||||
// the plugins global data are namespaced to avoid data conflicts:
|
const modulePath = path.join(dataDir, name);
|
||||||
// - by plugin name
|
await generate(dataDir, name, data);
|
||||||
// - by plugin id (allow using multiple instances of the same plugin)
|
return modulePath;
|
||||||
const setGlobalData: PluginContentLoadedActions['setGlobalData'] = (
|
},
|
||||||
data,
|
setGlobalData(data) {
|
||||||
) => {
|
globalData[plugin.name] ??= {};
|
||||||
globalData[plugin.name] = globalData[plugin.name] ?? {};
|
|
||||||
globalData[plugin.name]![pluginId] = data;
|
globalData[plugin.name]![pluginId] = data;
|
||||||
};
|
},
|
||||||
|
|
||||||
const actions: PluginContentLoadedActions = {
|
|
||||||
addRoute,
|
|
||||||
createData,
|
|
||||||
setGlobalData,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const translatedContent =
|
const translatedContent =
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {
|
||||||
type ApplyTrailingSlashParams,
|
type ApplyTrailingSlashParams,
|
||||||
} from '@docusaurus/utils-common';
|
} from '@docusaurus/utils-common';
|
||||||
|
|
||||||
|
/** Recursively applies trailing slash config to all nested routes. */
|
||||||
export function applyRouteTrailingSlash(
|
export function applyRouteTrailingSlash(
|
||||||
route: RouteConfig,
|
route: RouteConfig,
|
||||||
params: ApplyTrailingSlashParams,
|
params: ApplyTrailingSlashParams,
|
||||||
|
|
|
@ -6,34 +6,90 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
genChunkName,
|
docuHash,
|
||||||
normalizeUrl,
|
normalizeUrl,
|
||||||
removeSuffix,
|
|
||||||
simpleHash,
|
simpleHash,
|
||||||
escapePath,
|
escapePath,
|
||||||
reportMessage,
|
reportMessage,
|
||||||
} from '@docusaurus/utils';
|
} from '@docusaurus/utils';
|
||||||
import {stringify} from 'querystring';
|
import _ from 'lodash';
|
||||||
|
import query from 'querystring';
|
||||||
import {getAllFinalRoutes} from './utils';
|
import {getAllFinalRoutes} from './utils';
|
||||||
import type {
|
import type {
|
||||||
ChunkRegistry,
|
|
||||||
Module,
|
Module,
|
||||||
RouteConfig,
|
RouteConfig,
|
||||||
RouteModule,
|
RouteModules,
|
||||||
ChunkNames,
|
ChunkNames,
|
||||||
|
RouteChunkNames,
|
||||||
ReportingSeverity,
|
ReportingSeverity,
|
||||||
} from '@docusaurus/types';
|
} from '@docusaurus/types';
|
||||||
|
|
||||||
type RegistryMap = {
|
type LoadedRoutes = {
|
||||||
[chunkName: string]: ChunkRegistry;
|
/** 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) {
|
function indent(str: string) {
|
||||||
const spaces = ' ';
|
return ` ${str.replace(/\n/g, `\n `)}`;
|
||||||
return `${spaces}${str.replace(/\n/g, `\n${spaces}`)}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
routePath,
|
||||||
routeHash,
|
routeHash,
|
||||||
exact,
|
exact,
|
||||||
|
@ -58,7 +114,7 @@ function createRouteCodeString({
|
||||||
if (subroutesCodeStrings) {
|
if (subroutesCodeStrings) {
|
||||||
parts.push(
|
parts.push(
|
||||||
`routes: [
|
`routes: [
|
||||||
${indent(removeSuffix(subroutesCodeStrings.join(',\n'), ',\n'))}
|
${indent(subroutesCodeStrings.join(',\n'))}
|
||||||
]`,
|
]`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -89,96 +145,67 @@ ${indent(parts.join(',\n'))}
|
||||||
}`;
|
}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const NotFoundRouteCode = `{
|
const isModule = (value: unknown): value is Module =>
|
||||||
path: '*',
|
typeof value === 'string' ||
|
||||||
component: ComponentCreator('*')
|
(typeof value === 'object' &&
|
||||||
}`;
|
|
||||||
|
|
||||||
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' &&
|
|
||||||
// eslint-disable-next-line no-underscore-dangle
|
// eslint-disable-next-line no-underscore-dangle
|
||||||
(value as {[key: string]: unknown})?.__import &&
|
!!(value as {[key: string]: unknown})?.__import);
|
||||||
(value as {[key: string]: unknown})?.path
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/** Takes a {@link Module} and returns the string path it represents. */
|
||||||
function getModulePath(target: Module): string {
|
function getModulePath(target: Module): string {
|
||||||
if (typeof target === 'string') {
|
if (typeof target === 'string') {
|
||||||
return target;
|
return target;
|
||||||
}
|
}
|
||||||
const queryStr = target.query ? `?${stringify(target.query)}` : '';
|
const queryStr = target.query ? `?${query.stringify(target.query)}` : '';
|
||||||
return `${target.path}${queryStr}`;
|
return `${target.path}${queryStr}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function genRouteChunkNames(
|
/**
|
||||||
registry: RegistryMap,
|
* Takes a route module (which is a tree of modules), and transforms each module
|
||||||
value: Module,
|
* into a chunk name. It also mutates `res.registry` and registers the loaders
|
||||||
prefix?: string,
|
* for each chunk.
|
||||||
name?: string,
|
*
|
||||||
): string;
|
* @param routeModule One route module to be transformed.
|
||||||
function genRouteChunkNames(
|
* @param prefix Prefix passed to {@link genChunkName}.
|
||||||
registry: RegistryMap,
|
* @param name Preferred name passed to {@link genChunkName}.
|
||||||
value: RouteModule,
|
* @param res The route structures being loaded.
|
||||||
prefix?: string,
|
*/
|
||||||
name?: string,
|
function genChunkNames(
|
||||||
|
routeModule: RouteModules,
|
||||||
|
prefix: string,
|
||||||
|
name: string,
|
||||||
|
res: LoadedRoutes,
|
||||||
): ChunkNames;
|
): ChunkNames;
|
||||||
function genRouteChunkNames(
|
function genChunkNames(
|
||||||
registry: RegistryMap,
|
routeModule: RouteModules | RouteModules[] | Module,
|
||||||
value: RouteModule[],
|
prefix: string,
|
||||||
prefix?: string,
|
name: string,
|
||||||
name?: string,
|
res: LoadedRoutes,
|
||||||
): ChunkNames[];
|
|
||||||
function genRouteChunkNames(
|
|
||||||
registry: RegistryMap,
|
|
||||||
value: RouteModule | RouteModule[] | Module,
|
|
||||||
prefix?: string,
|
|
||||||
name?: string,
|
|
||||||
): ChunkNames | ChunkNames[] | string;
|
): ChunkNames | ChunkNames[] | string;
|
||||||
function genRouteChunkNames(
|
function genChunkNames(
|
||||||
// TODO instead of passing a mutating the registry, return a registry slice?
|
routeModule: RouteModules | RouteModules[] | Module,
|
||||||
registry: RegistryMap,
|
prefix: string,
|
||||||
value: RouteModule | RouteModule[] | Module | null | undefined,
|
name: string,
|
||||||
prefix?: string,
|
res: LoadedRoutes,
|
||||||
name?: string,
|
): string | ChunkNames | ChunkNames[] {
|
||||||
): null | string | ChunkNames | ChunkNames[] {
|
if (isModule(routeModule)) {
|
||||||
if (!value) {
|
// This is a leaf node, no need to recurse
|
||||||
return null;
|
const modulePath = getModulePath(routeModule);
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
return value.map((val, index) =>
|
|
||||||
genRouteChunkNames(registry, val, `${index}`, name),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isModule(value)) {
|
|
||||||
const modulePath = getModulePath(value);
|
|
||||||
const chunkName = genChunkName(modulePath, prefix, name);
|
const chunkName = genChunkName(modulePath, prefix, name);
|
||||||
const loader = `() => import(/* webpackChunkName: '${chunkName}' */ '${escapePath(
|
res.registry[chunkName] = {
|
||||||
|
loader: `() => import(/* webpackChunkName: '${chunkName}' */ '${escapePath(
|
||||||
modulePath,
|
modulePath,
|
||||||
)}')`;
|
)}')`,
|
||||||
|
modulePath,
|
||||||
registry[chunkName] = {loader, modulePath};
|
};
|
||||||
return chunkName;
|
return chunkName;
|
||||||
}
|
}
|
||||||
|
if (Array.isArray(routeModule)) {
|
||||||
const newValue: ChunkNames = {};
|
return routeModule.map((val, index) =>
|
||||||
Object.entries(value).forEach(([key, v]) => {
|
genChunkNames(val, `${index}`, name, res),
|
||||||
newValue[key] = genRouteChunkNames(registry, v, key, name);
|
);
|
||||||
});
|
}
|
||||||
return newValue;
|
return _.mapValues(routeModule, (v, key) => genChunkNames(v, key, name, res));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleDuplicateRoutes(
|
export function handleDuplicateRoutes(
|
||||||
|
@ -212,30 +239,19 @@ This could lead to non-deterministic routing behavior.`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadRoutes(
|
/**
|
||||||
pluginsRouteConfigs: RouteConfig[],
|
* This is the higher level overview of route code generation. For each route
|
||||||
baseUrl: string,
|
* config node, it return the node's serialized form, and mutate `registry`,
|
||||||
onDuplicateRoutes: ReportingSeverity,
|
* `routesPaths`, and `routesChunkNames` accordingly.
|
||||||
): Promise<{
|
*/
|
||||||
registry: {[chunkName: string]: ChunkRegistry};
|
function genRouteCode(routeConfig: RouteConfig, res: LoadedRoutes): string {
|
||||||
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.
|
|
||||||
function generateRouteCode(routeConfig: RouteConfig): string {
|
|
||||||
const {
|
const {
|
||||||
path: routePath,
|
path: routePath,
|
||||||
component,
|
component,
|
||||||
modules = {},
|
modules = {},
|
||||||
routes: subroutes,
|
routes: subroutes,
|
||||||
exact,
|
|
||||||
priority,
|
priority,
|
||||||
|
exact,
|
||||||
...props
|
...props
|
||||||
} = routeConfig;
|
} = routeConfig;
|
||||||
|
|
||||||
|
@ -246,46 +262,59 @@ ${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) {
|
if (!subroutes) {
|
||||||
routesPaths.push(routePath);
|
res.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 routeHash = simpleHash(JSON.stringify(routeConfig), 3);
|
||||||
const chunkNamesKey = `${routePath}-${routeHash}`;
|
res.routesChunkNames[`${routePath}-${routeHash}`] = {
|
||||||
routesChunkNames[chunkNamesKey] = {
|
...genChunkNames({component}, 'component', component, res),
|
||||||
...genRouteChunkNames(registry, {component}, 'component', component),
|
...genChunkNames(modules, 'module', routePath, res),
|
||||||
...genRouteChunkNames(registry, modules, 'module', routePath),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return createRouteCodeString({
|
return serializeRouteConfig({
|
||||||
routePath: routeConfig.path.replace(/'/g, "\\'"),
|
routePath: routePath.replace(/'/g, "\\'"),
|
||||||
routeHash,
|
routeHash,
|
||||||
|
subroutesCodeStrings: subroutes?.map((r) => genRouteCode(r, res)),
|
||||||
exact,
|
exact,
|
||||||
subroutesCodeStrings: subroutes?.map(generateRouteCode),
|
|
||||||
props,
|
props,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const routesConfig = `
|
/**
|
||||||
${RoutesImportsCode}
|
* 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 [
|
export default [
|
||||||
${indent(`${pluginsRouteConfigs.map(generateRouteCode).join(',\n')},`)}
|
${indent(`${routeConfigs.map((r) => genRouteCode(r, res)).join(',\n')},`)}
|
||||||
${indent(NotFoundRouteCode)}
|
{
|
||||||
|
path: '*',
|
||||||
|
component: ComponentCreator('*'),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
`;
|
`;
|
||||||
|
|
||||||
return {
|
return res;
|
||||||
registry,
|
|
||||||
routesConfig,
|
|
||||||
routesChunkNames,
|
|
||||||
routesPaths,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,10 +14,7 @@ import {
|
||||||
applyConfigurePostCss,
|
applyConfigurePostCss,
|
||||||
getHttpsConfig,
|
getHttpsConfig,
|
||||||
} from '../utils';
|
} from '../utils';
|
||||||
import type {
|
import type {Plugin} from '@docusaurus/types';
|
||||||
ConfigureWebpackFn,
|
|
||||||
ConfigureWebpackFnMergeStrategy,
|
|
||||||
} from '@docusaurus/types';
|
|
||||||
|
|
||||||
describe('customize JS loader', () => {
|
describe('customize JS loader', () => {
|
||||||
it('getCustomizableJSLoader defaults to babel loader', () => {
|
it('getCustomizableJSLoader defaults to babel loader', () => {
|
||||||
|
@ -63,7 +60,7 @@ describe('extending generated webpack config', () => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const configureWebpack: ConfigureWebpackFn = (
|
const configureWebpack: Plugin['configureWebpack'] = (
|
||||||
generatedConfig,
|
generatedConfig,
|
||||||
isServer,
|
isServer,
|
||||||
) => {
|
) => {
|
||||||
|
@ -99,7 +96,7 @@ describe('extending generated webpack config', () => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const configureWebpack: ConfigureWebpackFn = () => ({
|
const configureWebpack: Plugin['configureWebpack'] = () => ({
|
||||||
entry: 'entry.js',
|
entry: 'entry.js',
|
||||||
output: {
|
output: {
|
||||||
path: path.join(__dirname, 'dist'),
|
path: path.join(__dirname, 'dist'),
|
||||||
|
@ -128,9 +125,9 @@ describe('extending generated webpack config', () => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const createConfigureWebpack: (
|
const createConfigureWebpack: (mergeStrategy?: {
|
||||||
mergeStrategy?: ConfigureWebpackFnMergeStrategy,
|
[key: string]: 'prepend' | 'append';
|
||||||
) => ConfigureWebpackFn = (mergeStrategy) => () => ({
|
}) => Plugin['configureWebpack'] = (mergeStrategy) => () => ({
|
||||||
module: {
|
module: {
|
||||||
rules: [{use: 'zzz'}],
|
rules: [{use: 'zzz'}],
|
||||||
},
|
},
|
||||||
|
|
|
@ -25,8 +25,7 @@ import crypto from 'crypto';
|
||||||
import logger from '@docusaurus/logger';
|
import logger from '@docusaurus/logger';
|
||||||
import type {TransformOptions} from '@babel/core';
|
import type {TransformOptions} from '@babel/core';
|
||||||
import type {
|
import type {
|
||||||
ConfigureWebpackFn,
|
Plugin,
|
||||||
ConfigurePostCssFn,
|
|
||||||
PostCssOptions,
|
PostCssOptions,
|
||||||
ConfigureWebpackUtils,
|
ConfigureWebpackUtils,
|
||||||
} from '@docusaurus/types';
|
} from '@docusaurus/types';
|
||||||
|
@ -172,7 +171,7 @@ export const getCustomizableJSLoader =
|
||||||
* @returns final/ modified webpack config
|
* @returns final/ modified webpack config
|
||||||
*/
|
*/
|
||||||
export function applyConfigureWebpack(
|
export function applyConfigureWebpack(
|
||||||
configureWebpack: ConfigureWebpackFn,
|
configureWebpack: NonNullable<Plugin['configureWebpack']>,
|
||||||
config: Configuration,
|
config: Configuration,
|
||||||
isServer: boolean,
|
isServer: boolean,
|
||||||
jsLoader: 'babel' | ((isServer: boolean) => RuleSetRule) | undefined,
|
jsLoader: 'babel' | ((isServer: boolean) => RuleSetRule) | undefined,
|
||||||
|
@ -198,7 +197,7 @@ export function applyConfigureWebpack(
|
||||||
}
|
}
|
||||||
|
|
||||||
export function applyConfigurePostCss(
|
export function applyConfigurePostCss(
|
||||||
configurePostCss: NonNullable<ConfigurePostCssFn>,
|
configurePostCss: NonNullable<Plugin['configurePostCss']>,
|
||||||
config: Configuration,
|
config: Configuration,
|
||||||
): Configuration {
|
): Configuration {
|
||||||
type LocalPostCSSLoader = unknown & {
|
type LocalPostCSSLoader = unknown & {
|
||||||
|
|
|
@ -46,13 +46,13 @@ Create a route to add to the website.
|
||||||
interface RouteConfig {
|
interface RouteConfig {
|
||||||
path: string;
|
path: string;
|
||||||
component: string;
|
component: string;
|
||||||
modules?: RouteModule;
|
modules?: RouteModules;
|
||||||
routes?: RouteConfig[];
|
routes?: RouteConfig[];
|
||||||
exact?: boolean;
|
exact?: boolean;
|
||||||
priority?: number;
|
priority?: number;
|
||||||
}
|
}
|
||||||
interface RouteModule {
|
interface RouteModules {
|
||||||
[module: string]: Module | RouteModule | RouteModule[];
|
[module: string]: Module | RouteModules | RouteModules[];
|
||||||
}
|
}
|
||||||
type Module =
|
type Module =
|
||||||
| {
|
| {
|
||||||
|
|
Loading…
Add table
Reference in a new issue