From 872b7c71a79ad2770c51757bbfd832f456d9c927 Mon Sep 17 00:00:00 2001 From: XGHeaven Date: Thu, 5 Dec 2024 16:25:49 +0800 Subject: [PATCH] feat: support htmlGeneratingMode option --- .changeset/angry-carrots-occur.md | 5 +++++ examples/basic-project/compatHtml.config.mts | 9 +++++++++ packages/ice/src/bundler/config/output.ts | 6 +++++- packages/ice/src/config.ts | 2 +- packages/ice/src/types/userConfig.ts | 15 +++++++++++++- packages/ice/src/utils/generateEntry.ts | 19 ++++++++++++++---- tests/integration/basic-project.test.ts | 10 ++++++++++ website/docs/guide/basic/config.md | 21 +++++++++++++++++++- 8 files changed, 79 insertions(+), 8 deletions(-) create mode 100644 .changeset/angry-carrots-occur.md create mode 100644 examples/basic-project/compatHtml.config.mts diff --git a/.changeset/angry-carrots-occur.md b/.changeset/angry-carrots-occur.md new file mode 100644 index 0000000000..6790e4df52 --- /dev/null +++ b/.changeset/angry-carrots-occur.md @@ -0,0 +1,5 @@ +--- +'@ice/app': patch +--- + +feat: add htmlGenerating `mode` option diff --git a/examples/basic-project/compatHtml.config.mts b/examples/basic-project/compatHtml.config.mts new file mode 100644 index 0000000000..e039dcaadd --- /dev/null +++ b/examples/basic-project/compatHtml.config.mts @@ -0,0 +1,9 @@ +import { defineConfig } from '@ice/app'; +import defaultConfig from './ice.config.mjs'; + +export default defineConfig(() => ({ + ...defaultConfig, + htmlGenerating: { + mode: 'compat' + } +})); diff --git a/packages/ice/src/bundler/config/output.ts b/packages/ice/src/bundler/config/output.ts index 56ecbd9393..a393f82798 100644 --- a/packages/ice/src/bundler/config/output.ts +++ b/packages/ice/src/bundler/config/output.ts @@ -5,6 +5,7 @@ import injectInitialEntry from '../../utils/injectInitialEntry.js'; import { SERVER_OUTPUT_DIR } from '../../constant.js'; import { logger } from '../../utils/logger.js'; import type { BundlerOptions } from '../types.js'; +import type { HtmlGeneratingMode } from '../../types/index.js'; export async function getOutputPaths(options: { rootDir: string; @@ -21,7 +22,8 @@ export async function getOutputPaths(options: { } } if (serverEntry && userConfig.htmlGenerating) { - outputPaths = await buildCustomOutputs(rootDir, outputDir, serverEntry, bundleOptions); + const htmlGeneratingMode = typeof userConfig.htmlGenerating === 'boolean' ? undefined : userConfig.htmlGenerating?.mode; + outputPaths = await buildCustomOutputs(rootDir, outputDir, serverEntry, bundleOptions, htmlGeneratingMode); } return outputPaths; } @@ -37,6 +39,7 @@ async function buildCustomOutputs( outputDir: string, serverEntry: string, bundleOptions: Pick, + generatingMode?: HtmlGeneratingMode, ) { const { userConfig, appConfig, routeManifest } = bundleOptions; const { ssg } = userConfig; @@ -52,6 +55,7 @@ async function buildCustomOutputs( renderMode: ssg ? 'SSG' : undefined, routeType: appConfig?.router?.type, routeManifest, + generatingMode, }); if (routeType === 'memory' && userConfig?.routes?.injectInitialEntry) { injectInitialEntry(routeManifest, outputDir); diff --git a/packages/ice/src/config.ts b/packages/ice/src/config.ts index 0dd14ca72b..c754ce74a5 100644 --- a/packages/ice/src/config.ts +++ b/packages/ice/src/config.ts @@ -508,7 +508,7 @@ const userConfig = [ }, { name: 'htmlGenerating', - validation: 'boolean', + validation: 'boolean|object', defaultValue: true, }, ]; diff --git a/packages/ice/src/types/userConfig.ts b/packages/ice/src/types/userConfig.ts index ae252c00a9..f5fb319391 100644 --- a/packages/ice/src/types/userConfig.ts +++ b/packages/ice/src/types/userConfig.ts @@ -49,6 +49,19 @@ interface Fetcher { method?: string; } +export type HtmlGeneratingMode = 'cleanUrl' | 'compat'; + +export interface HtmlGeneratingConfig { + /** + * Control how file structure to generation html. + * Route: '/' '/foo' '/foo/bar' + * `cleanUrl`: '/index.html' '/foo.html' '/foo/bar.html' + * `compat`: '/index.html' '/foo/index.html' '/foo/bar/index.html' + * @default 'cleanUrl' + */ + mode?: HtmlGeneratingMode; +} + export interface UserConfig { /** * Feature polyfill for legacy browsers, which can not polyfilled by core-js. @@ -178,7 +191,7 @@ export interface UserConfig { * HTML will not be generated when build, If it is false. * @see https://v3.ice.work/docs/guide/basic/config#htmlgenerating */ - htmlGenerating?: boolean; + htmlGenerating?: boolean | HtmlGeneratingConfig; /** * Choose a style of souce mapping to enhance the debugging process. * @see https://v3.ice.work/docs/guide/basic/config#sourcemap diff --git a/packages/ice/src/utils/generateEntry.ts b/packages/ice/src/utils/generateEntry.ts index 198e59d8c1..5e9038fe51 100644 --- a/packages/ice/src/utils/generateEntry.ts +++ b/packages/ice/src/utils/generateEntry.ts @@ -1,6 +1,7 @@ import * as path from 'path'; import fse from 'fs-extra'; import type { ServerContext, RenderMode, AppConfig } from '@ice/runtime'; +import type { HtmlGeneratingMode } from '../types/index.js'; import dynamicImport from './dynamicImport.js'; import { logger } from './logger.js'; import type RouteManifest from './routeManifest.js'; @@ -12,6 +13,7 @@ interface Options { documentOnly: boolean; routeType: AppConfig['router']['type']; renderMode?: RenderMode; + generatingMode?: HtmlGeneratingMode; routeManifest: RouteManifest; } @@ -28,6 +30,7 @@ export default async function generateEntry(options: Options): Promise { expect(bundleContent.includes('__IS_NODE__')).toBe(false); expect(fs.existsSync(path.join(__dirname, `../../examples/${example}/build/favicon.ico`))).toBe(true); expect(fs.existsSync(path.join(__dirname, `../../examples/${example}/build/js/data-loader.js`))).toBe(true); + expect(fs.existsSync(path.join(__dirname, `../../examples/${example}/build/index.html`))).toBe(true); + expect(fs.existsSync(path.join(__dirname, `../../examples/${example}/build/blog.html`))).toBe(true); const jsonContent = fs.readFileSync(path.join(__dirname, `../../examples/${example}/build/assets-manifest.json`), 'utf-8'); expect(JSON.parse(jsonContent).pages.about.includes('js/framework.js')).toBeFalsy(); const dataLoaderPath = path.join(__dirname, `../../examples/${example}/build/js/data-loader.js`); @@ -69,6 +71,14 @@ describe(`build ${example}`, () => { expect((await page.$$text('h2')).length).toEqual(0); }); + test('using "compat" html generating mode', async () => { + await buildFixture(example, { + config: 'compatHtml.config.mts', + }); + expect(fs.existsSync(path.join(__dirname, `../../examples/${example}/build/index.html`))).toBeTruthy(); + expect(fs.existsSync(path.join(__dirname, `../../examples/${example}/build/blog/index.html`))).toBeTruthy(); + }); + afterAll(async () => { await browser.close(); }); diff --git a/website/docs/guide/basic/config.md b/website/docs/guide/basic/config.md index 25ad5ed6fa..4bc7366dd7 100644 --- a/website/docs/guide/basic/config.md +++ b/website/docs/guide/basic/config.md @@ -620,11 +620,30 @@ export default defineConfig(() => ({ ### htmlGenerating -- 类型:`boolean` +- 类型:`boolean | object` - 默认值:`true` 如果产物不想生成 html,可以设置为 `false`,在 SSG 开启的情况下,强制关闭 html 生成,将导致 SSG 失效。 +传入 `true` 则与 `{}` 效果一致。 + +#### htmlGenerating.mode + +- 类型: `'cleanUrl' | 'compat'` +- 默认值 `'cleanUrl'` + +配置 HTML 生成文件的规则,避免在某些服务器下出现非首页内容刷新后 404 的情况。目前主要由两种,分别是: + +- `cleanUrl` 生成的文件路径和路由一致。通常用于支持此模式的现代服务器,即自动省略 `.html` 后缀 +- `compat` 生成兼容模式的路径文件,通常用于一些只能省略 `index.html` 的服务器 + +具体区别可以参照下表: + +| Route | `/` | `/foo` | `/foo/bar` | +|------------|---------------|-------------------|-----------------------| +| `cleanUrl` | `/index.html` | `/foo.html` | `/foo/bar.html` | +| `compat` | `/index.html` | `/foo/index.html` | `/foo/bar/index.html` | + ### plugins - 类型:`PluginList`