Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(doc-core): support extendPageData hook & last updated time #3480

Merged
merged 5 commits into from
Apr 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/violet-humans-bow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@modern-js/doc-tools-doc': minor
'@modern-js/utils': minor
'@modern-js/doc-core': minor
---

feat: support extendPageData hook and last updated time

feat: 支持 extendPageData 钩子和最后更新时间功能
4 changes: 0 additions & 4 deletions packages/cli/doc-core/src/node/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,23 +126,19 @@ export async function renderPages(config: UserConfig) {
}

export async function build(rootDir: string, config: UserConfig) {
const docPlugins = [...(config.doc?.plugins ?? [])];
const isProd = true;
const modifiedConfig = await modifyConfig({
config,
docPlugins,
});

await beforeBuild({
config: modifiedConfig,
docPlugins,
isProd,
});
await bundle(rootDir, modifiedConfig);
await renderPages(modifiedConfig);
await afterBuild({
config: modifiedConfig,
docPlugins,
isProd,
});
}
4 changes: 0 additions & 4 deletions packages/cli/doc-core/src/node/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,15 @@ export async function dev(
rootDir: string,
config: UserConfig,
): Promise<ServerInstance> {
const docPlugins = [...(config.doc?.plugins ?? [])];
const base = config.doc?.base ?? '';
const isProd = false;

try {
const modifiedConfig = await modifyConfig({
config,
docPlugins,
});
await beforeBuild({
config: modifiedConfig,
docPlugins,
isProd,
});

Expand All @@ -40,7 +37,6 @@ export async function dev(

await afterBuild({
config: modifiedConfig,
docPlugins,
isProd,
});
return server;
Expand Down
39 changes: 32 additions & 7 deletions packages/cli/doc-core/src/node/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
import { UserConfig, DocPlugin } from 'shared/types';
import { UserConfig, PageIndexInfo, DocPlugin } from 'shared/types';
import { pluginLastUpdated } from './plugins/lastUpdated';

type HookOptions = {
config: UserConfig;
docPlugins: DocPlugin[];
isProd?: boolean;
pageData?: PageIndexInfo;
};

const DEFAULT_PLUGINS = [pluginLastUpdated() as DocPlugin];

function getPlugins(config: UserConfig) {
const plugins = config.doc?.plugins || [];
return [...DEFAULT_PLUGINS, ...plugins];
}

export async function modifyConfig(hookOptions: HookOptions) {
const { config, docPlugins } = hookOptions;
const { config } = hookOptions;
const docPlugins = getPlugins(config);

// config hooks
for (const plugin of docPlugins) {
Expand All @@ -20,27 +29,43 @@ export async function modifyConfig(hookOptions: HookOptions) {
}

export async function beforeBuild(hookOptions: HookOptions) {
const { config, docPlugins, isProd = true } = hookOptions;
const { config, isProd = true } = hookOptions;
const docPlugins = getPlugins(config);

// beforeBuild hooks
return await Promise.all(
docPlugins
.filter(plugin => typeof plugin.beforeBuild === 'function')
.map(plugin => {
return plugin.beforeBuild!(config.doc || {}, isProd);
return plugin.beforeBuild(config.doc || {}, isProd);
}),
);
}

export async function afterBuild(hookOptions: HookOptions) {
const { config, docPlugins, isProd = true } = hookOptions;
const { config, isProd = true } = hookOptions;
const docPlugins = getPlugins(config);

// afterBuild hooks
return await Promise.all(
docPlugins
.filter(plugin => typeof plugin.afterBuild === 'function')
.map(plugin => {
return plugin.afterBuild!(config.doc || {}, isProd);
return plugin.afterBuild(config.doc || {}, isProd);
}),
);
}

export async function extendPageData(hookOptions: HookOptions): Promise<void> {
const { pageData } = hookOptions;
const docPlugins = getPlugins(hookOptions.config);

// extendPageData hooks
await Promise.all(
docPlugins
.filter(plugin => typeof plugin.extendPageData === 'function')
.map(plugin => {
return plugin.extendPageData(pageData);
}),
);
}
34 changes: 34 additions & 0 deletions packages/cli/doc-core/src/node/plugins/lastUpdated/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import execa from '@modern-js/utils/execa';

function transform(timestamp: number, lang: string) {
return new Date(timestamp).toLocaleString(lang);
}

async function getGitLastUpdatedTimeStamp(filePath: string) {
let lastUpdated;
try {
const { stdout } = await execa('git', [
'log',
'-1',
'--format=%at',
filePath,
]);
lastUpdated = Number(stdout) * 1000;
} catch (e) {
/* noop */
}
return lastUpdated;
}

export function pluginLastUpdated() {
return {
name: 'last-updated',
async extendPageData(pageData: any) {
const { _filepath, lang } = pageData;
const lastUpdated = await getGitLastUpdatedTimeStamp(_filepath);
if (lastUpdated) {
pageData.lastUpdatedTime = transform(lastUpdated, lang);
}
},
};
}
2 changes: 1 addition & 1 deletion packages/cli/doc-core/src/node/runtimeModule/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { UserConfig } from 'shared/types/index';
import { BuilderPlugin } from '@modern-js/builder-webpack-provider';
import { BuilderPlugin } from '@modern-js/builder';
import RuntimeModulesPlugin from './RuntimeModulePlugin';
import { routeVMPlugin } from './routeData';
import { siteDataVMPlugin } from './siteData';
Expand Down
123 changes: 64 additions & 59 deletions packages/cli/doc-core/src/node/runtimeModule/siteData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,36 +17,32 @@ import { importStatementRegex, TEMP_DIR } from '../constants';
import { applyReplaceRules } from '../utils/applyReplaceRules';
import { logger, createHash } from '../utils';
import { flattenMdxContent } from '../utils/flattenMdxContent';
import { extendPageData } from '../hooks';
import RuntimeModulesPlugin from './RuntimeModulePlugin';
import { routeService } from './routeData';
import { RuntimeModuleID } from '.';
import { withBase, MDX_REGEXP, SEARCH_INDEX_NAME } from '@/shared/utils';

let pages: PageIndexInfo[] | undefined;

// The concern about future architecture:
// The `indexHash` will be generated before Rspack build so we can wrap it with virtual module in rspack to ensure that client runtime can access it.The process will be like this:
// | ........................ process ........................... |
//
// Input -> | Compute index | -> Rspack build ->- Output -> | Append index file to output dir |

// However, if we generate the search index at internal Rspack build process in the future, like this:
// | ........................ process ........................... |
//
// Input ->- Rspack build ->- Output ->- | Write Index file to output dir |
// |
// +---------------+
// | Compute index |
// +---------------+
// In this way, we can compute index in a custom mdx loader instead of `@mdx-js/loader` and reuse the ast info of mdx files and cache all the compile result of unified processor.In other words, we won't need to compile mdx files twice for search index generation.

// Then there will be a problem: how can we let the client runtime access the `indexHash`?
// As far as I know, we can only do something after the Rspack build process becuase the index hash is generated within Rspack build process.There are two ways to do this:
// How can we let the client runtime access the `indexHash`?
// We can only do something after the Rspack build process becuase the index hash is generated within Rspack build process.There are two ways to do this:
// 1. insert window.__INDEX_HASH__ = 'xxx' into the html template manually
// 2. replace the `__INDEX_HASH__` placeholder in the html template with the real index hash after Rspack build

export const indexHash = '';

function deletePriviteKey<T>(obj: T): T {
if (typeof obj !== 'object' || obj === null) {
return obj;
}
Object.keys(obj).forEach(key => {
if (key.startsWith('_')) {
delete obj[key];
}
});
return obj;
}

export function normalizeThemeConfig(
themeConfig: DefaultThemeConfig,
pages: PageIndexInfo[] = [],
Expand Down Expand Up @@ -126,7 +122,6 @@ async function extractPageData(
replaceRules: ReplaceRule[],
alias: Record<string, string | string[]>,
domain: string,
disableSearch: boolean,
root: string,
): Promise<(PageIndexInfo | null)[]> {
return Promise.all(
Expand All @@ -139,9 +134,7 @@ async function extractPageData(
// eslint-disable-next-line import/no-named-as-default-member
...yamlFront.loadFront(content),
};
if (frontmatter.pageType === 'home') {
return null;
}

// 1. Replace rules for frontmatter & content
Object.keys(frontmatter).forEach(key => {
if (typeof frontmatter[key] === 'string') {
Expand Down Expand Up @@ -175,7 +168,7 @@ async function extractPageData(
root,
});

if (!title?.length && !frontmatter.title?.length) {
if (!title?.length && !frontmatter && !frontmatter.title?.length) {
return null;
}
content = htmlToText(String(html), {
Expand Down Expand Up @@ -236,6 +229,7 @@ async function extractPageData(
...frontmatter,
__content: undefined,
},
_filepath: route.absolutePath,
};
}),
);
Expand Down Expand Up @@ -266,44 +260,24 @@ export async function siteDataVMPlugin(
? userConfig?.search.domain ?? ''
: '';
pages = (
await extractPageData(
replaceRules,
alias,
domain,
userConfig?.search === false,
userRoot,
)
await extractPageData(replaceRules, alias, domain, userRoot)
).filter(Boolean);
}

const siteData = {
title: userConfig?.title || '',
description: userConfig?.description || '',
icon: userConfig?.icon || '',
themeConfig: normalizeThemeConfig(
userConfig?.themeConfig || {},
pages,
config.doc?.base,
config.doc?.replaceRules || [],
),
base: userConfig?.base || '/',
root: userRoot,
lang: userConfig?.lang || '',
logo: userConfig?.logo || '',
search: userConfig?.search ?? { mode: 'local' },
pages: pages.map(({ routePath, toc }) => ({
routePath,
toc,
})),
};
// Categorize pages, sorted by language
const pagesByLang = pages.reduce((acc, page) => {
if (!acc[page.lang]) {
acc[page.lang] = [];
}
acc[page.lang].push(page);
return acc;
}, {} as Record<string, PageIndexInfo[]>);
// Categorize pages, sorted by language, and write search index to file
const pagesByLang = deletePriviteKey<PageIndexInfo[]>(pages).reduce(
(acc, page) => {
if (!acc[page.lang]) {
acc[page.lang] = [];
}
if (page.frontmatter?.pageType === 'home') {
return acc;
}
acc[page.lang].push(page);
return acc;
},
{} as Record<string, PageIndexInfo[]>,
);

const indexHashByLang = {} as Record<string, string>;

Expand All @@ -321,6 +295,37 @@ export async function siteDataVMPlugin(
}),
);

// Run extendPageData hook in plugins
await Promise.all(
pages.map(async pageData =>
extendPageData({
pageData,
config,
}),
),
);

const siteData = {
title: userConfig?.title || '',
description: userConfig?.description || '',
icon: userConfig?.icon || '',
themeConfig: normalizeThemeConfig(
userConfig?.themeConfig || {},
pages,
config.doc?.base,
config.doc?.replaceRules || [],
),
base: userConfig?.base || '/',
root: userRoot,
lang: userConfig?.lang || '',
logo: userConfig?.logo || '',
search: userConfig?.search ?? { mode: 'local' },
pages: pages.map(page => {
const { content, id, domain, ...rest } = page;
return rest;
}),
};

const plugin = new RuntimeModulesPlugin({
[entryPath]: `export default ${JSON.stringify(siteData)}`,
[searchIndexHashPath]: `export default ${JSON.stringify(indexHashByLang)}`,
Expand Down
Loading