Skip to content

Commit

Permalink
feat: 支持htmlSuffix、dynamicRoot
Browse files Browse the repository at this point in the history
htmlSuffix 会生成 xxx.html,非xxx/index.html
dynamicRoot可不用做任何修改部署在任意目录下
  • Loading branch information
flgame committed Jun 26, 2024
1 parent 3a92682 commit b04143c
Show file tree
Hide file tree
Showing 10 changed files with 238 additions and 71 deletions.
8 changes: 8 additions & 0 deletions examples/export-static/.umirc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export default {
runtimePublicPath: {},
exportStatic: {
htmlSuffix: true,
dynamicRoot: true,
},
hash: true,
};
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>;
}
220 changes: 149 additions & 71 deletions packages/preset-umi/src/features/exportStatic/exportStatic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,26 +19,47 @@ interface IExportHtmlItem {

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

function isHtmlRoute(route: IRoute): boolean {
const is404 = route.absPath === '/*';

if (
// skip layout
!route.isLayout &&
// skip duplicate route
!route.path.endsWith('.html') &&
// 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)
) {
return true;
}
return false;
}
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 +70,7 @@ function getExportHtmlData(routes: Record<string, IRoute>): IExportHtmlItem[] {
file,
});
}
});
}

return Array.from(map.values());
}
Expand Down Expand Up @@ -128,6 +149,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 @@ -145,7 +168,11 @@ 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;
Expand All @@ -164,65 +191,93 @@ export default (api: IApi) => {
),
};
}

// 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 @@ -246,12 +301,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 @@ -269,6 +325,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 @@ -281,7 +344,22 @@ 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 (const key of Object.keys(routes)) {
const route = routes[key];
if (isHtmlRoute(route)) {
key = `${key}.html`;
routes[key] = {
...route,
path: getHtmlPath(route.path, htmlSuffix),
};
}
}
});
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.

0 comments on commit b04143c

Please sign in to comment.