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: add htmlGenerating mode option #7032

Merged
merged 1 commit into from
Dec 12, 2024
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
5 changes: 5 additions & 0 deletions .changeset/angry-carrots-occur.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@ice/app': patch
---

feat: add htmlGenerating `mode` option
9 changes: 9 additions & 0 deletions examples/basic-project/compatHtml.config.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { defineConfig } from '@ice/app';
import defaultConfig from './ice.config.mjs';

export default defineConfig(() => ({
...defaultConfig,
htmlGenerating: {
mode: 'compat'
}
}));
6 changes: 5 additions & 1 deletion packages/ice/src/bundler/config/output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}
Expand All @@ -37,6 +39,7 @@ async function buildCustomOutputs(
outputDir: string,
serverEntry: string,
bundleOptions: Pick<BundlerOptions, 'userConfig' | 'appConfig' | 'routeManifest'>,
generatingMode?: HtmlGeneratingMode,
) {
const { userConfig, appConfig, routeManifest } = bundleOptions;
const { ssg } = userConfig;
Expand All @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion packages/ice/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -508,7 +508,7 @@ const userConfig = [
},
{
name: 'htmlGenerating',
validation: 'boolean',
validation: 'boolean|object',
defaultValue: true,
},
];
Expand Down
15 changes: 14 additions & 1 deletion packages/ice/src/types/userConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
19 changes: 15 additions & 4 deletions packages/ice/src/utils/generateEntry.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -12,6 +13,7 @@ interface Options {
documentOnly: boolean;
routeType: AppConfig['router']['type'];
renderMode?: RenderMode;
generatingMode?: HtmlGeneratingMode;
routeManifest: RouteManifest;
}

Expand All @@ -28,6 +30,7 @@ export default async function generateEntry(options: Options): Promise<EntryResu
renderMode,
routeType,
routeManifest,
generatingMode,
} = options;

let serverEntry: string;
Expand All @@ -48,7 +51,7 @@ export default async function generateEntry(options: Options): Promise<EntryResu
} = await renderEntry({ routePath, serverEntry, documentOnly, renderMode });
const generateOptions = { rootDir, routePath, outputDir };
if (htmlOutput) {
const path = await generateFilePath({ ...generateOptions, type: 'html' });
const path = await generateFilePath({ ...generateOptions, type: 'html', generatingMode });
await writeFile(
path,
htmlOutput,
Expand All @@ -72,8 +75,14 @@ export function escapeRoutePath(str: string) {
return str.replace(/\/(:|\*)/g, '/$');
}

function formatFilePath(routePath: string, type: 'js' | 'html' | 'js.map'): string {
return routePath === '/' ? `index.${type}` : `${escapeRoutePath(routePath)}.${type}`;
function formatFilePath(routePath: string, type: 'js' | 'html' | 'js.map', generatingMode?: HtmlGeneratingMode): string {
if (routePath === '/') {
return `index.${type}`;
}
if (type === 'html' && generatingMode === 'compat') {
return `${escapeRoutePath(routePath)}/index.${type}`;
}
return `${escapeRoutePath(routePath)}.${type}`;
}

async function generateFilePath(
Expand All @@ -82,14 +91,16 @@ async function generateFilePath(
routePath,
outputDir,
type,
generatingMode,
}: {
rootDir: string;
routePath: string;
outputDir: string;
type: 'js' | 'html' | 'js.map' ;
generatingMode?: HtmlGeneratingMode;
},
) {
const fileName = formatFilePath(routePath, type);
const fileName = formatFilePath(routePath, type, generatingMode);
if (fse.existsSync(path.join(rootDir, 'public', fileName))) {
logger.warn(`${fileName} is overwrite by framework, rename file name if it is necessary.`);
}
Expand Down
10 changes: 10 additions & 0 deletions tests/integration/basic-project.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ describe(`build ${example}`, () => {
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`);
Expand Down Expand Up @@ -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();
});
Expand Down
21 changes: 20 additions & 1 deletion website/docs/guide/basic/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<Config, OverwritePluginAPI>`
Expand Down
Loading