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: 支持htmlSuffix、dynamicRoot #12496

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
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
14 changes: 14 additions & 0 deletions examples/export-static/.umirc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export default {
runtimePublicPath: {},
exportStatic: {
htmlSuffix: true,
dynamicRoot: true,
},
hash: true,
// 配置式路由
// routes: [
// { path: '/', component: 'index'},
// { path: '/page1', component: 'page1/index'},
// { path: '/page1/page1_1', component: 'page1/page1_1/index'},
// ],
};
13 changes: 13 additions & 0 deletions examples/export-static/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "@example/export-static",
"private": true,
"scripts": {
"build": "umi build",
"dev": "umi dev",
"setup": "umi setup",
"start": "npm run dev"
},
"dependencies": {
"umi": "workspace:*"
}
}
5 changes: 5 additions & 0 deletions examples/export-static/src/pages/404.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import React from 'react';

export default function Page1() {
return <h1>404</h1>;
}
3 changes: 3 additions & 0 deletions examples/export-static/src/pages/bar.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.bar {
background: green;
}
10 changes: 10 additions & 0 deletions examples/export-static/src/pages/foo.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@

.foo {
color: red;
}
.foo2 {
font-size: 40px;
}
.foo3 {
font-weight: bold;
}
34 changes: 34 additions & 0 deletions examples/export-static/src/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React from 'react';
// @ts-ignore
import { Helmet, Link } from 'umi';
// @ts-ignore
import fooStyles from './foo.less';
// @ts-ignore
import barStyles from './bar.css';

export default function HomePage() {
return (
<div>
<Helmet>
<title>helmet title</title>
</Helmet>
<div
className={`${fooStyles.foo} ${fooStyles.foo2} ${fooStyles.foo3} ${barStyles.bar}`}
>
Home Page
</div>
<Link to="/page1">
<h2>Page 1(/page1)</h2>
</Link>
<Link to="/page1.html">
<h2>Page 1(/page1.html)</h2>
</Link>
<Link to="/page1/page1_1">
<h2>Page 1-1(/page1/page1_1)</h2>
</Link>
<Link to="/page1/page1_1.html">
<h2>Page 1-1(/page1/page1_1.html)</h2>
</Link>
</div>
);
}
5 changes: 5 additions & 0 deletions examples/export-static/src/pages/page1/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import React from 'react';

export default function Page1() {
return <h1>Page 1</h1>;
}
5 changes: 5 additions & 0 deletions examples/export-static/src/pages/page1/page1_1/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import React from 'react';

export default function Page1() {
return <h1>Page 1-1</h1>;
}
216 changes: 146 additions & 70 deletions packages/preset-umi/src/features/exportStatic/exportStatic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,26 +19,42 @@ interface IExportHtmlItem {

type IUserExtraRoute = string | { path: string; prerender: boolean };

function isHtmlRoute(route: IRoute): boolean {
return (
// skip layout
!route.isLayout &&
// skip duplicate route
!route.noHtml &&
// skip dynamic route for win, because `:` is not allowed in file name
(!IS_WIN || !route.path.includes('/:')) &&
// skip `*` route, because `*` is not working for most site serve services
(!route.path.includes('*') ||
// skip `404.html`
route.absPath === '/*')
);
}
function getHtmlPath(path: string, htmlSuffix: boolean): string {
if (!path) return path;
if (path === '/*') return '/404.html';
if (path === '/') return '/index.html';

if (path.endsWith('/')) path = path.slice(0, -1);
return htmlSuffix ? `${path}.html` : `${path}/index.html`;
}
/**
* get export html data from routes
*/
function getExportHtmlData(routes: Record<string, IRoute>): IExportHtmlItem[] {
function getExportHtmlData(
routes: Record<string, IRoute>,
htmlSuffix: boolean,
): IExportHtmlItem[] {
const map = new Map<string, IExportHtmlItem>();

Object.values(routes).forEach((route) => {
for (const route of Object.values(routes)) {
const is404 = route.absPath === '/*';

if (
// skip layout
!route.isLayout &&
// skip dynamic route for win, because `:` is not allowed in file name
(!IS_WIN || !route.path.includes('/:')) &&
// skip `*` route, because `*` is not working for most site serve services
(!route.path.includes('*') ||
// except `404.html`
is404)
) {
const file = is404 ? '404.html' : join('.', route.absPath, 'index.html');
if (isHtmlRoute(route)) {
const file = join('.', getHtmlPath(route.absPath, htmlSuffix));

map.set(file, {
route: {
Expand All @@ -49,7 +65,7 @@ function getExportHtmlData(routes: Record<string, IRoute>): IExportHtmlItem[] {
file,
});
}
});
}

return Array.from(map.values());
}
Expand Down Expand Up @@ -112,6 +128,8 @@ export default (api: IApi) => {
schema: ({ zod }) =>
zod
.object({
htmlSuffix: zod.boolean(),
dynamicRoot: zod.boolean(),
extraRoutePaths: zod.union([
zod.function(),
zod.array(zod.string()),
Expand All @@ -129,72 +147,105 @@ export default (api: IApi) => {

// export routes to html files
api.modifyExportHTMLFiles(async (_defaultFiles, opts) => {
const { publicPath } = api.config;
const {
publicPath,
base,
exportStatic: { htmlSuffix, dynamicRoot },
} = api.config;
const htmlData = api.appData.exportHtmlData;
const htmlFiles: { path: string; content: string }[] = [];
const { markupArgs: defaultMarkupArgs } = opts;

for (const { file, route, prerender } of htmlData) {
let markupArgs = defaultMarkupArgs;

// handle relative publicPath, such as `./`
if (publicPath.startsWith('.')) {
let routerBaseStr = JSON.stringify(base || '/');
let publicPathStr = JSON.stringify(publicPath || '/');
// handle relative publicPath, such as `./`, same with dynamicRoot
if (publicPath.startsWith('.') || dynamicRoot) {
assert(
api.config.runtimePublicPath,
'`runtimePublicPath` should be enable when `publicPath` is relative!',
'`runtimePublicPath` should be enable when `publicPath` is relative or `exportStatic.dynamicRoot` is true!',
);

const rltPrefix = relative(dirname(file), '.');
let pathS = route.path;
const isSlash = pathS.endsWith('/');
if (pathS === '/404') {
//do nothing
}
// keep the relative path same for route /xxx and /xxx.html
else if (htmlSuffix && isSlash) {
pathS = pathS.slice(0, -1);
}
// keep the relative path same for route /xxx/ and /xxx/index.html
else if (!htmlSuffix && !isSlash) {
pathS = pathS + '/';
}

const pathN = Math.max(pathS.split('/').length - 1, 1);
routerBaseStr = `location.pathname.split('/').slice(0, -${pathN}).concat('').join('/')`;
publicPathStr = `location.protocol + '//' + location.hostname + (location.port ? ':' + location.port : '') + window.routerBase`;

const rltPrefix = relative(dirname(file), '.');
const joinRltPrefix = (path: string) => {
if (!rltPrefix || rltPrefix == '.') {
return `.${path.startsWith('/') ? '' : '/'}${path}`;
}
return winPath(join(rltPrefix, path));
};
// prefix for all assets
if (rltPrefix) {
// HINT: clone for keep original markupArgs unmodified
const picked = lodash.cloneDeep(
lodash.pick(markupArgs, [
'favicons',
'links',
'styles',
'headScripts',
'scripts',
]),
);

// handle favicons
picked.favicons.forEach((item: string, i: number) => {
if (item.startsWith(publicPath)) {
picked.favicons[i] = winPath(join(rltPrefix, item));
}
});

// handle links
picked.links.forEach((link: { href: string }) => {
if (link.href?.startsWith(publicPath)) {
link.href = winPath(join(rltPrefix, link.href));
}
});

// handle scripts
[picked.headScripts, picked.scripts, picked.styles].forEach(
(group: ({ src: string } | string)[]) => {
group.forEach((script, i) => {
if (
typeof script === 'string' &&
script.startsWith(publicPath)
) {
group[i] = winPath(join(rltPrefix, script));
} else if (
typeof script === 'object' &&
script.src?.startsWith(publicPath)
) {
script.src = winPath(join(rltPrefix, script.src));
}
});
},
);

// update markupArgs
markupArgs = Object.assign({}, markupArgs, picked);
}
// HINT: clone for keep original markupArgs unmodified
const picked = lodash.cloneDeep(
lodash.pick(markupArgs, [
'favicons',
'links',
'styles',
'headScripts',
'scripts',
]),
);

// handle favicons
picked.favicons.forEach((item: string, i: number) => {
if (item.startsWith(publicPath)) {
picked.favicons[i] = joinRltPrefix(item);
}
});

// handle links
picked.links.forEach((link: { href: string }) => {
if (link.href?.startsWith(publicPath)) {
link.href = joinRltPrefix(link.href);
}
});

// handle scripts
[picked.headScripts, picked.scripts, picked.styles].forEach(
(group: ({ src: string } | string)[]) => {
group.forEach((script, i) => {
if (typeof script === 'string' && script.startsWith(publicPath)) {
group[i] = joinRltPrefix(script);
} else if (
typeof script === 'object' &&
script.src?.startsWith(publicPath)
) {
script.src = joinRltPrefix(script.src);
}
});
},
);

picked.headScripts.unshift(
`window.routerBase = ${routerBaseStr};`,
`
if(!window.publicPath) {
window.publicPath = ${publicPathStr};
}
`,
);

// update markupArgs
markupArgs = Object.assign({}, markupArgs, picked);
}

// append html file
Expand All @@ -218,12 +269,13 @@ export default (api: IApi) => {

api.onGenerateFiles(async () => {
const {
exportStatic: { extraRoutePaths = [] },
exportStatic: { extraRoutePaths = [], htmlSuffix },
} = api.config;
const extraHtmlData = getExportHtmlData(
await getRoutesFromUserExtraPaths(extraRoutePaths),
htmlSuffix,
);
const htmlData = getExportHtmlData(api.appData.routes).concat(
const htmlData = getExportHtmlData(api.appData.routes, htmlSuffix).concat(
extraHtmlData,
);

Expand All @@ -241,6 +293,13 @@ export function modifyClientRenderOpts(memo: any) {
hydrate: hydrate && !{{{ ignorePaths }}}.includes(history.location.pathname),
};
}

export function modifyContextOpts(memo: any) {
return {
...memo,
basename: window.routerBase || memo.basename,
}
}
`.trim(),
{
ignorePaths: JSON.stringify(
Expand All @@ -253,7 +312,24 @@ export function modifyClientRenderOpts(memo: any) {
noPluginDir: true,
});
});

api.modifyRoutes((routes: Record<string, IRoute>) => {
const {
exportStatic: { htmlSuffix },
} = api.config;
// copy / to /index.html and /xxx to /xxx.html or /xxx/index.html
for (let key of Object.keys(routes)) {
const route = routes[key];
if (isHtmlRoute(route)) {
key = `${key}.html`;
flgame marked this conversation as resolved.
Show resolved Hide resolved
routes[key] = {
...route,
path: getHtmlPath(route.path, htmlSuffix),
noHtml: true,
};
}
}
return routes;
});
api.addRuntimePlugin(() => {
return [`@@/core/exportStaticRuntimePlugin.ts`];
});
Expand Down
6 changes: 6 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.