mirror of
https://github.com/alibaba/ice.git
synced 2024-10-23 07:04:52 +08:00
Feat: unnecessary to compile routes config for server render (#6856)
* fix: unnecessary to compile routes config for server render * chore: format ejs template * feat: support build fallback entry * chore: add test case for fallback entry * chore: update lock * chore: changeset * chore: lint * chore: lint * chore: remove console log * fix: merge conflict * fix: undefined assign
This commit is contained in:
parent
d073ee5ade
commit
15c8200f60
7
.changeset/green-files-watch.md
Normal file
7
.changeset/green-files-watch.md
Normal file
@ -0,0 +1,7 @@
|
||||
---
|
||||
'@ice/shared-config': patch
|
||||
'@ice/runtime': patch
|
||||
'@ice/app': patch
|
||||
---
|
||||
|
||||
feat: support build additional server entry for fallback
|
@ -29,4 +29,4 @@
|
||||
},
|
||||
"include": ["src", ".ice", "ice.config.*"],
|
||||
"exclude": ["build", "public"]
|
||||
}
|
||||
}
|
||||
|
@ -1,18 +0,0 @@
|
||||
import { defineConfig } from '@ice/app';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [],
|
||||
server: {
|
||||
onDemand: true,
|
||||
format: 'esm',
|
||||
},
|
||||
output: {
|
||||
distType: 'javascript'
|
||||
},
|
||||
sourceMap: true,
|
||||
routes: {
|
||||
defineRoutes: (route) => {
|
||||
route('/custom', 'Custom/index.tsx');
|
||||
},
|
||||
},
|
||||
});
|
@ -1,42 +0,0 @@
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { Meta, Title, Links, Main, Scripts } from 'ice';
|
||||
import fse from 'fs-extra';
|
||||
|
||||
let dirname;
|
||||
if (typeof __dirname === 'string') {
|
||||
dirname = __dirname;
|
||||
} else {
|
||||
dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
}
|
||||
|
||||
function Document() {
|
||||
return (
|
||||
<html>
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta name="description" content="ICE Demo" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<Meta />
|
||||
<Title />
|
||||
<Links />
|
||||
</head>
|
||||
<body>
|
||||
<Main />
|
||||
<Scripts ScriptElement={(props) => {
|
||||
if (props.src && !props.src.startsWith('http')) {
|
||||
const filePath = path.join(dirname, `..${props.src}`);
|
||||
const sourceMapFilePath = path.join(dirname, `..${props.src}.map`);
|
||||
const res = fse.readFileSync(filePath, 'utf-8');
|
||||
return <script data-sourcemap={sourceMapFilePath} dangerouslySetInnerHTML={{ __html: res }} {...props} />;
|
||||
} else {
|
||||
return <script {...props} />;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
export default Document;
|
@ -1,19 +0,0 @@
|
||||
import { Link, useData } from 'ice';
|
||||
|
||||
export default function Custom() {
|
||||
const data = useData();
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2>Custom Page</h2>
|
||||
<Link to="/home">home</Link>
|
||||
{data}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function pageConfig() {
|
||||
return {
|
||||
title: 'Custom',
|
||||
};
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
import { Link } from 'ice';
|
||||
|
||||
export default function About() {
|
||||
return (
|
||||
<>
|
||||
<h2>About Page</h2>
|
||||
<Link to="/">home</Link>
|
||||
<span className="mark">new</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function pageConfig() {
|
||||
return {
|
||||
title: 'About',
|
||||
meta: [
|
||||
{
|
||||
name: 'theme-color',
|
||||
content: '#eee',
|
||||
},
|
||||
],
|
||||
links: [{
|
||||
href: 'https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css',
|
||||
rel: 'stylesheet',
|
||||
}],
|
||||
scripts: [{
|
||||
src: 'https://cdn.jsdelivr.net/npm/lodash@2.4.1/dist/lodash.min.js',
|
||||
}],
|
||||
};
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
import { Link, useData, useConfig } from 'ice';
|
||||
|
||||
export default function Blog() {
|
||||
const data = useData();
|
||||
const config = useConfig();
|
||||
|
||||
console.log('render Blog', 'data', data, 'config', config);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2>Blog Page</h2>
|
||||
<Link to="/home">home</Link>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function pageConfig() {
|
||||
return {
|
||||
title: 'Blog',
|
||||
};
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
import { definePageConfig } from 'ice';
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<>
|
||||
<h2>Home Page</h2>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const pageConfig = definePageConfig(() => {
|
||||
return {
|
||||
queryParamsPassKeys: [
|
||||
'questionId',
|
||||
'source',
|
||||
'disableNav',
|
||||
],
|
||||
title: 'Home',
|
||||
};
|
||||
});
|
@ -1,25 +0,0 @@
|
||||
.title {
|
||||
color: red;
|
||||
margin-left: 10rpx;
|
||||
}
|
||||
|
||||
.data {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.homeContainer {
|
||||
align-items: center;
|
||||
margin-top: 200rpx;
|
||||
}
|
||||
|
||||
.homeTitle {
|
||||
font-size: 45rpx;
|
||||
font-weight: bold;
|
||||
margin: 20rpx 0;
|
||||
}
|
||||
|
||||
.homeInfo {
|
||||
font-size: 36rpx;
|
||||
margin: 8rpx 0;
|
||||
color: #555;
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
import { Outlet } from 'ice';
|
||||
|
||||
export default () => {
|
||||
return (
|
||||
<div>
|
||||
<h1>ICE 3.0 Layout</h1>
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export function pageConfig() {
|
||||
return {
|
||||
title: 'Layout',
|
||||
meta: [
|
||||
{
|
||||
name: 'layout-color',
|
||||
content: '#f00',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
10
examples/with-fallback-entry/ice.config.mts
Normal file
10
examples/with-fallback-entry/ice.config.mts
Normal file
@ -0,0 +1,10 @@
|
||||
import { defineConfig } from '@ice/app';
|
||||
import plugin from './plugin';
|
||||
|
||||
export default defineConfig(() => ({
|
||||
plugins: [plugin()],
|
||||
ssr: true,
|
||||
server: {
|
||||
format: 'cjs',
|
||||
}
|
||||
}));
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@examples/with-entry-type",
|
||||
"private": true,
|
||||
"name": "@examples/with-fallback-entry",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "ice start",
|
||||
"build": "ice build"
|
||||
@ -12,11 +12,10 @@
|
||||
"dependencies": {
|
||||
"@ice/app": "workspace:*",
|
||||
"@ice/runtime": "workspace:*",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"fs-extra": "^10.0.0",
|
||||
"@types/react": "^18.0.0",
|
||||
"@types/react-dom": "^18.0.2",
|
||||
"webpack": "^5.88.0"
|
12
examples/with-fallback-entry/plugin.ts
Normal file
12
examples/with-fallback-entry/plugin.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export default function createPlugin() {
|
||||
return {
|
||||
name: 'custom-plugin',
|
||||
setup({ onGetConfig }) {
|
||||
onGetConfig((config) => {
|
||||
config.server = {
|
||||
fallbackEntry: true,
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
22
examples/with-fallback-entry/src/document.tsx
Normal file
22
examples/with-fallback-entry/src/document.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import { Meta, Title, Links, Main, Scripts } from 'ice';
|
||||
|
||||
function Document() {
|
||||
return (
|
||||
<html>
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta name="description" content="ICE Demo" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<Meta />
|
||||
<Title />
|
||||
<Links />
|
||||
</head>
|
||||
<body>
|
||||
<Main />
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
export default Document;
|
3
examples/with-fallback-entry/src/global.css
Normal file
3
examples/with-fallback-entry/src/global.css
Normal file
@ -0,0 +1,3 @@
|
||||
body {
|
||||
font-size: 14px;
|
||||
}
|
3
examples/with-fallback-entry/src/pages/index.tsx
Normal file
3
examples/with-fallback-entry/src/pages/index.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
export default function Home() {
|
||||
return <h1>home</h1>;
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
import path from 'path';
|
||||
import type { CommandName } from 'build-scripts';
|
||||
import ServerRunnerPlugin from '../../webpack/ServerRunnerPlugin.js';
|
||||
import { IMPORT_META_RENDERER, IMPORT_META_TARGET, WEB } from '../../constant.js';
|
||||
import { IMPORT_META_RENDERER, IMPORT_META_TARGET, WEB, FALLBACK_ENTRY, RUNTIME_TMP_DIR } from '../../constant.js';
|
||||
import getServerCompilerPlugin from '../../utils/getServerCompilerPlugin.js';
|
||||
import ReCompilePlugin from '../../webpack/ReCompilePlugin.js';
|
||||
import getEntryPoints from '../../utils/getEntryPoints.js';
|
||||
@ -20,6 +22,18 @@ export const getSpinnerPlugin = (spinner, name?: string) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const getFallbackEntry = (options: {
|
||||
rootDir: string;
|
||||
command: CommandName;
|
||||
fallbackEntry: boolean;
|
||||
}): string => {
|
||||
const { command, fallbackEntry, rootDir } = options;
|
||||
if (command === 'build' && fallbackEntry) {
|
||||
return path.join(rootDir, RUNTIME_TMP_DIR, FALLBACK_ENTRY);
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
interface ServerPluginOptions {
|
||||
serverRunner?: ServerRunner;
|
||||
serverCompiler?: ServerCompiler;
|
||||
@ -30,6 +44,7 @@ interface ServerPluginOptions {
|
||||
serverEntry?: string;
|
||||
ensureRoutesConfig: () => Promise<void>;
|
||||
userConfig?: UserConfig;
|
||||
fallbackEntry?: string;
|
||||
getFlattenRoutes?: () => string[];
|
||||
command?: string;
|
||||
}
|
||||
@ -43,6 +58,7 @@ export const getServerPlugin = ({
|
||||
outputDir,
|
||||
serverCompileTask,
|
||||
userConfig,
|
||||
fallbackEntry,
|
||||
getFlattenRoutes,
|
||||
command,
|
||||
}: ServerPluginOptions) => {
|
||||
@ -53,6 +69,7 @@ export const getServerPlugin = ({
|
||||
return getServerCompilerPlugin(serverCompiler, {
|
||||
rootDir,
|
||||
serverEntry,
|
||||
fallbackEntry,
|
||||
outputDir,
|
||||
serverCompileTask,
|
||||
userConfig,
|
||||
|
@ -11,7 +11,7 @@ import {
|
||||
CSS_MODULES_LOCAL_IDENT_NAME,
|
||||
CSS_MODULES_LOCAL_IDENT_NAME_DEV,
|
||||
} from '../../constant.js';
|
||||
import { getReCompilePlugin, getServerPlugin, getSpinnerPlugin } from '../config/plugins.js';
|
||||
import { getFallbackEntry, getReCompilePlugin, getServerPlugin, getSpinnerPlugin } from '../config/plugins.js';
|
||||
import { getExpandedEnvs } from '../../utils/runtimeEnv.js';
|
||||
import type { BundlerOptions, Context } from '../types.js';
|
||||
import type { PluginData } from '../../types/plugin.js';
|
||||
@ -34,8 +34,8 @@ const getConfig: GetConfig = async (context, options, rspack) => {
|
||||
} = options;
|
||||
const {
|
||||
rootDir,
|
||||
userConfig,
|
||||
command,
|
||||
userConfig,
|
||||
extendsPluginAPI: {
|
||||
serverCompileTask,
|
||||
getRoutesFile,
|
||||
@ -50,6 +50,11 @@ const getConfig: GetConfig = async (context, options, rspack) => {
|
||||
getSpinnerPlugin(spinner),
|
||||
// Add Server runner plugin.
|
||||
getServerPlugin({
|
||||
fallbackEntry: getFallbackEntry({
|
||||
rootDir,
|
||||
command,
|
||||
fallbackEntry: server?.fallbackEntry,
|
||||
}),
|
||||
serverRunner,
|
||||
ensureRoutesConfig,
|
||||
serverCompiler,
|
||||
|
@ -7,7 +7,7 @@ import { getRouteExportConfig } from '../../service/config.js';
|
||||
import { getFileHash } from '../../utils/hash.js';
|
||||
import DataLoaderPlugin from '../../webpack/DataLoaderPlugin.js';
|
||||
import { IMPORT_META_RENDERER, IMPORT_META_TARGET, RUNTIME_TMP_DIR, WEB } from '../../constant.js';
|
||||
import { getReCompilePlugin, getServerPlugin, getSpinnerPlugin } from '../config/plugins.js';
|
||||
import { getFallbackEntry, getReCompilePlugin, getServerPlugin, getSpinnerPlugin } from '../config/plugins.js';
|
||||
import type RouteManifest from '../../utils/routeManifest.js';
|
||||
import type ServerRunnerPlugin from '../../webpack/ServerRunnerPlugin.js';
|
||||
import type ServerCompilerPlugin from '../../webpack/ServerCompilerPlugin.js';
|
||||
@ -93,6 +93,11 @@ const getWebpackConfig: GetWebpackConfig = async (context, options) => {
|
||||
serverCompiler,
|
||||
target,
|
||||
rootDir,
|
||||
fallbackEntry: getFallbackEntry({
|
||||
rootDir,
|
||||
command,
|
||||
fallbackEntry: server?.fallbackEntry,
|
||||
}),
|
||||
serverEntry: server?.entry,
|
||||
outputDir,
|
||||
serverCompileTask,
|
||||
|
@ -5,6 +5,7 @@ export const DEFAULT_HOST = '0.0.0.0';
|
||||
|
||||
export const RUNTIME_TMP_DIR = '.ice';
|
||||
export const SERVER_ENTRY = path.join(RUNTIME_TMP_DIR, 'entry.server.ts');
|
||||
export const FALLBACK_ENTRY = 'entry.document.ts';
|
||||
export const DATA_LOADER_ENTRY = path.join(RUNTIME_TMP_DIR, 'data-loader.ts');
|
||||
export const SERVER_OUTPUT_DIR = 'server';
|
||||
export const IMPORT_META_TARGET = 'import.meta.target';
|
||||
|
@ -4,7 +4,7 @@ import * as path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { createRequire } from 'module';
|
||||
import { Context } from 'build-scripts';
|
||||
import type { CommandArgs, CommandName } from 'build-scripts';
|
||||
import type { CommandArgs, CommandName, TaskConfig } from 'build-scripts';
|
||||
import type { Config } from '@ice/shared-config/types';
|
||||
import type { AppConfig } from '@ice/runtime/types';
|
||||
import webpack from '@ice/bundles/compiled/webpack/index.js';
|
||||
@ -23,7 +23,7 @@ import { setEnv, updateRuntimeEnv, getCoreEnvKeys } from './utils/runtimeEnv.js'
|
||||
import getRuntimeModules from './utils/getRuntimeModules.js';
|
||||
import { generateRoutesInfo, getRoutesDefinition } from './routes.js';
|
||||
import * as config from './config.js';
|
||||
import { RUNTIME_TMP_DIR, WEB, RUNTIME_EXPORTS, SERVER_ENTRY } from './constant.js';
|
||||
import { RUNTIME_TMP_DIR, WEB, RUNTIME_EXPORTS, SERVER_ENTRY, FALLBACK_ENTRY } from './constant.js';
|
||||
import createSpinner from './utils/createSpinner.js';
|
||||
import ServerCompileTask from './utils/ServerCompileTask.js';
|
||||
import { getAppExportConfig, getRouteExportConfig } from './service/config.js';
|
||||
@ -215,7 +215,7 @@ async function createService({ rootDir, command, commandArgs }: CreateServiceOpt
|
||||
|
||||
ctx.registerConfig(configType, configData);
|
||||
});
|
||||
let taskConfigs = await ctx.setup();
|
||||
let taskConfigs: TaskConfig<Config>[] = await ctx.setup();
|
||||
|
||||
// get userConfig after setup because of userConfig maybe modified by plugins
|
||||
const { userConfig } = ctx;
|
||||
@ -296,6 +296,11 @@ async function createService({ rootDir, command, commandArgs }: CreateServiceOpt
|
||||
},
|
||||
);
|
||||
|
||||
if (platformTaskConfig.config.server?.fallbackEntry) {
|
||||
// Add fallback entry for server side rendering.
|
||||
generator.addRenderFile('core/entry.server.ts.ejs', FALLBACK_ENTRY, { hydrate: false });
|
||||
}
|
||||
|
||||
if (typeof userConfig.dataLoader === 'object' && userConfig.dataLoader.fetcher) {
|
||||
const {
|
||||
packageName,
|
||||
|
@ -10,6 +10,7 @@ interface Options {
|
||||
userConfig: UserConfig;
|
||||
outputDir: string;
|
||||
serverEntry: string;
|
||||
fallbackEntry?: string;
|
||||
serverCompileTask: ExtendsPluginAPI['serverCompileTask'];
|
||||
ensureRoutesConfig: () => Promise<void>;
|
||||
runtimeDefineVars: Record<string, string>;
|
||||
@ -25,16 +26,24 @@ function getServerCompilerPlugin(serverCompiler: ServerCompiler, options: Option
|
||||
serverCompileTask,
|
||||
ensureRoutesConfig,
|
||||
runtimeDefineVars,
|
||||
fallbackEntry,
|
||||
entryPoints,
|
||||
} = options;
|
||||
const { ssg, ssr, server: { format } } = userConfig;
|
||||
const isEsm = userConfig?.server?.format === 'esm';
|
||||
const defaultEntryPoints = { index: getServerEntry(rootDir, serverEntry) };
|
||||
if (fallbackEntry) {
|
||||
if (entryPoints) {
|
||||
entryPoints['index.fallback'] = fallbackEntry;
|
||||
}
|
||||
defaultEntryPoints['index.fallback'] = fallbackEntry;
|
||||
}
|
||||
|
||||
return new ServerCompilerPlugin(
|
||||
serverCompiler,
|
||||
[
|
||||
{
|
||||
entryPoints: entryPoints || { index: getServerEntry(rootDir, serverEntry) },
|
||||
entryPoints: entryPoints || defaultEntryPoints,
|
||||
outdir: path.join(outputDir, SERVER_OUTPUT_DIR),
|
||||
splitting: isEsm,
|
||||
format,
|
||||
|
@ -36,7 +36,6 @@ export default class ServerCompilerPlugin {
|
||||
public compileTask = async (compilation?: Compilation) => {
|
||||
const [buildOptions] = this.serverCompilerOptions;
|
||||
if (!this.isCompiling) {
|
||||
await this.ensureRoutesConfig();
|
||||
if (compilation) {
|
||||
// Option of compilationInfo need to be object, while it may changed during multi-time compilation.
|
||||
this.compilerOptions.compilationInfo.assetsManifest =
|
||||
|
@ -19,7 +19,6 @@ export default class ServerRunnerPlugin {
|
||||
|
||||
public compileTask = async (compilation?: Compilation) => {
|
||||
if (!this.isCompiling) {
|
||||
await this.ensureRoutesConfig();
|
||||
if (compilation) {
|
||||
// Option of compilationInfo need to be object, while it may changed during multi-time compilation.
|
||||
this.serverRunner.addCompileData({
|
||||
|
@ -1,6 +1,10 @@
|
||||
import './env.server';
|
||||
<% if (hydrate) {-%>
|
||||
import { getAppConfig, renderToHTML as renderAppToHTML, renderToResponse as renderAppToResponse } from '@ice/runtime/server';
|
||||
<% } else { -%>
|
||||
import { getAppConfig, getDocumentResponse as renderAppToHTML, renderDocumentToResponse as renderAppToResponse } from '@ice/runtime/server';
|
||||
<% }-%>
|
||||
<%- entryServer.imports %>
|
||||
import * as runtime from '@ice/runtime/server';
|
||||
<% if (hydrate) {-%>
|
||||
import { commons, statics } from './runtime-modules';
|
||||
<% }-%>
|
||||
@ -19,7 +23,6 @@ import createRoutes from '<%- routesFile %>';
|
||||
<% } else { -%>
|
||||
import routesManifest from './route-manifest.json';
|
||||
<% } -%>
|
||||
import routesConfig from './routes-config.bundle.mjs';
|
||||
<% if (dataLoaderImport.imports) {-%><%-dataLoaderImport.imports%><% } -%>
|
||||
<% if (hydrate) {-%><%- runtimeOptions.imports %><% } -%>
|
||||
|
||||
@ -32,10 +35,11 @@ const createRoutes = () => routesManifest;
|
||||
const runtimeModules = { commons, statics };
|
||||
|
||||
const getRouterBasename = () => {
|
||||
const appConfig = runtime.getAppConfig(app);
|
||||
const appConfig = getAppConfig(app);
|
||||
return appConfig?.router?.basename ?? <%- basename %> ?? '';
|
||||
}
|
||||
|
||||
<% if (hydrate) {-%>
|
||||
const setRuntimeEnv = (renderMode) => {
|
||||
if (renderMode === 'SSG') {
|
||||
process.env.ICE_CORE_SSG = 'true';
|
||||
@ -43,6 +47,7 @@ const setRuntimeEnv = (renderMode) => {
|
||||
process.env.ICE_CORE_SSR = 'true';
|
||||
}
|
||||
}
|
||||
<% } -%>
|
||||
|
||||
interface RenderOptions {
|
||||
documentOnly?: boolean;
|
||||
@ -57,19 +62,21 @@ interface RenderOptions {
|
||||
}
|
||||
|
||||
export async function renderToHTML(requestContext, options: RenderOptions = {}) {
|
||||
<% if (hydrate) {-%>
|
||||
const { renderMode = 'SSR' } = options;
|
||||
setRuntimeEnv(renderMode);
|
||||
|
||||
<% }-%>
|
||||
const mergedOptions = mergeOptions(options);
|
||||
return await runtime.renderToHTML(requestContext, mergedOptions);
|
||||
return await renderAppToHTML(requestContext, mergedOptions);
|
||||
}
|
||||
|
||||
export async function renderToResponse(requestContext, options: RenderOptions = {}) {
|
||||
<% if (hydrate) {-%>
|
||||
const { renderMode = 'SSR' } = options;
|
||||
setRuntimeEnv(renderMode);
|
||||
|
||||
<% }-%>
|
||||
const mergedOptions = mergeOptions(options);
|
||||
return runtime.renderToResponse(requestContext, mergedOptions);
|
||||
return renderAppToResponse(requestContext, mergedOptions);
|
||||
}
|
||||
|
||||
function mergeOptions(options) {
|
||||
@ -89,16 +96,13 @@ function mergeOptions(options) {
|
||||
Document: Document.default,
|
||||
basename: basename || getRouterBasename(),
|
||||
renderMode,
|
||||
routesConfig,
|
||||
<% if (hydrate) {-%>
|
||||
runtimeOptions: {
|
||||
<% if (runtimeOptions.exports) { -%>
|
||||
<% if (hydrate) {-%>runtimeOptions: {
|
||||
<% if (runtimeOptions.exports) { -%>
|
||||
<%- runtimeOptions.exports %>
|
||||
<% } -%>
|
||||
<% if (locals.customRuntimeOptions) { _%>
|
||||
...<%- JSON.stringify(customRuntimeOptions) %>,
|
||||
<% } _%>
|
||||
<% } -%>
|
||||
<% if (locals.customRuntimeOptions) { -%>
|
||||
...<%- JSON.stringify(customRuntimeOptions) %>,
|
||||
<% } -%>
|
||||
},
|
||||
<% } -%>
|
||||
};
|
||||
<% } -%>};
|
||||
}
|
||||
|
1
packages/runtime/server.d.ts
vendored
Normal file
1
packages/runtime/server.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export * from './esm/index.server';
|
@ -10,7 +10,7 @@ function useAppContext() {
|
||||
return value;
|
||||
}
|
||||
|
||||
function useAppData() {
|
||||
function useAppData<T = any>(): T {
|
||||
const value = React.useContext(Context);
|
||||
return value.appData;
|
||||
}
|
||||
|
@ -1,2 +1,3 @@
|
||||
export { renderToResponse, renderToHTML } from './runServerApp.js';
|
||||
export { renderToResponse as renderDocumentToResponse, getDocumentResponse } from './renderDocument.js';
|
||||
export * from './index.js';
|
||||
|
140
packages/runtime/src/renderDocument.tsx
Normal file
140
packages/runtime/src/renderDocument.tsx
Normal file
@ -0,0 +1,140 @@
|
||||
import * as React from 'react';
|
||||
import * as ReactDOMServer from 'react-dom/server';
|
||||
import getAppConfig from './appConfig.js';
|
||||
import { AppContextProvider } from './AppContext.js';
|
||||
import { DocumentContextProvider } from './Document.js';
|
||||
import addLeadingSlash from './utils/addLeadingSlash.js';
|
||||
import getRequestContext from './requestContext.js';
|
||||
import matchRoutes from './matchRoutes.js';
|
||||
import getDocumentData from './server/getDocumentData.js';
|
||||
import getCurrentRoutePath from './utils/getCurrentRoutePath.js';
|
||||
import { sendResponse, getLocation } from './server/response.js';
|
||||
|
||||
import type {
|
||||
AppContext,
|
||||
RouteItem,
|
||||
RouteMatch,
|
||||
RenderOptions,
|
||||
Response,
|
||||
ServerContext,
|
||||
} from './types.js';
|
||||
|
||||
interface RenderDocumentOptions {
|
||||
matches: RouteMatch[];
|
||||
renderOptions: RenderOptions;
|
||||
routes: RouteItem[];
|
||||
documentData: any;
|
||||
routePath?: string;
|
||||
downgrade?: boolean;
|
||||
}
|
||||
|
||||
export function renderDocument(options: RenderDocumentOptions): Response {
|
||||
const {
|
||||
matches,
|
||||
renderOptions,
|
||||
routePath = '',
|
||||
downgrade,
|
||||
routes,
|
||||
documentData,
|
||||
}: RenderDocumentOptions = options;
|
||||
|
||||
const {
|
||||
assetsManifest,
|
||||
app,
|
||||
Document,
|
||||
basename,
|
||||
routesConfig = {},
|
||||
serverData,
|
||||
} = renderOptions;
|
||||
|
||||
const appData = null;
|
||||
const appConfig = getAppConfig(app);
|
||||
|
||||
const loaderData = {};
|
||||
matches.forEach(async (match) => {
|
||||
const { id } = match.route;
|
||||
const pageConfig = routesConfig[id];
|
||||
|
||||
loaderData[id] = {
|
||||
pageConfig: pageConfig ? pageConfig({}) : {},
|
||||
};
|
||||
});
|
||||
|
||||
const appContext: AppContext = {
|
||||
assetsManifest,
|
||||
appConfig,
|
||||
appData,
|
||||
loaderData,
|
||||
matches,
|
||||
routes,
|
||||
documentOnly: true,
|
||||
renderMode: 'CSR',
|
||||
routePath,
|
||||
basename,
|
||||
downgrade,
|
||||
serverData,
|
||||
documentData,
|
||||
};
|
||||
|
||||
const documentContext = {
|
||||
main: null,
|
||||
};
|
||||
|
||||
const htmlStr = ReactDOMServer.renderToString(
|
||||
<AppContextProvider value={appContext}>
|
||||
<DocumentContextProvider value={documentContext}>
|
||||
{
|
||||
Document && <Document pagePath={routePath} />
|
||||
}
|
||||
</DocumentContextProvider>
|
||||
</AppContextProvider>,
|
||||
);
|
||||
|
||||
return {
|
||||
value: `<!DOCTYPE html>${htmlStr}`,
|
||||
headers: {
|
||||
'Content-Type': 'text/html; charset=utf-8',
|
||||
},
|
||||
statusCode: 200,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getDocumentResponse(
|
||||
serverContext: ServerContext,
|
||||
renderOptions: RenderOptions,
|
||||
): Promise<Response> {
|
||||
const { req } = serverContext;
|
||||
const {
|
||||
app,
|
||||
basename,
|
||||
serverOnlyBasename,
|
||||
createRoutes,
|
||||
documentOnly,
|
||||
renderMode,
|
||||
} = renderOptions;
|
||||
const finalBasename = addLeadingSlash(serverOnlyBasename || basename);
|
||||
const location = getLocation(req.url);
|
||||
const requestContext = getRequestContext(location, serverContext);
|
||||
const appConfig = getAppConfig(app);
|
||||
const routes = createRoutes({
|
||||
requestContext,
|
||||
renderMode,
|
||||
});
|
||||
const documentData = await getDocumentData({
|
||||
loaderConfig: renderOptions.documentDataLoader,
|
||||
requestContext,
|
||||
documentOnly,
|
||||
});
|
||||
const matches = appConfig?.router?.type === 'hash' ? [] : matchRoutes(routes, location, finalBasename);
|
||||
const routePath = getCurrentRoutePath(matches);
|
||||
return renderDocument({ matches, routePath, routes, renderOptions, documentData });
|
||||
}
|
||||
|
||||
export async function renderToResponse(
|
||||
requestContext: ServerContext,
|
||||
renderOptions: RenderOptions,
|
||||
) {
|
||||
const { req, res } = requestContext;
|
||||
const documentResoponse = await getDocumentResponse(requestContext, renderOptions);
|
||||
sendResponse(req, res, documentResoponse);
|
||||
}
|
@ -1,22 +1,14 @@
|
||||
import type { ServerResponse, IncomingMessage } from 'http';
|
||||
import * as React from 'react';
|
||||
import * as ReactDOMServer from 'react-dom/server';
|
||||
import type { Location } from 'history';
|
||||
import { parsePath } from 'history';
|
||||
import { isFunction } from '@ice/shared';
|
||||
import type { RenderToPipeableStreamOptions, OnAllReadyParams, NodeWritablePiper } from './server/streamRender.js';
|
||||
import type { OnAllReadyParams } from './server/streamRender.js';
|
||||
import type {
|
||||
AppContext, RouteItem, ServerContext,
|
||||
AppExport,
|
||||
AssetsManifest,
|
||||
AppContext,
|
||||
ServerContext,
|
||||
RouteMatch,
|
||||
PageConfig,
|
||||
RenderMode,
|
||||
DocumentComponent,
|
||||
RuntimeModules,
|
||||
AppData,
|
||||
ServerAppRouterProps,
|
||||
DocumentDataLoaderConfig,
|
||||
RenderOptions,
|
||||
Response,
|
||||
} from './types.js';
|
||||
import Runtime from './runtime.js';
|
||||
import { AppContextProvider } from './AppContext.js';
|
||||
@ -24,48 +16,15 @@ import { getAppData } from './appData.js';
|
||||
import getAppConfig from './appConfig.js';
|
||||
import { DocumentContextProvider } from './Document.js';
|
||||
import { loadRouteModules } from './routes.js';
|
||||
import type { RouteLoaderOptions } from './routes.js';
|
||||
import { pipeToString, renderToNodeStream } from './server/streamRender.js';
|
||||
import getRequestContext from './requestContext.js';
|
||||
import matchRoutes from './matchRoutes.js';
|
||||
import getCurrentRoutePath from './utils/getCurrentRoutePath.js';
|
||||
import ServerRouter from './ServerRouter.js';
|
||||
import addLeadingSlash from './utils/addLeadingSlash.js';
|
||||
|
||||
export interface RenderOptions {
|
||||
app: AppExport;
|
||||
assetsManifest: AssetsManifest;
|
||||
createRoutes: (options: Pick<RouteLoaderOptions, 'requestContext' | 'renderMode'>) => RouteItem[];
|
||||
runtimeModules: RuntimeModules;
|
||||
documentDataLoader?: DocumentDataLoaderConfig;
|
||||
Document?: DocumentComponent;
|
||||
documentOnly?: boolean;
|
||||
preRender?: boolean;
|
||||
renderMode?: RenderMode;
|
||||
// basename is used both for server and client, once set, it will be sync to client.
|
||||
basename?: string;
|
||||
// serverOnlyBasename is used when just want to change basename for server.
|
||||
serverOnlyBasename?: string;
|
||||
routePath?: string;
|
||||
disableFallback?: boolean;
|
||||
routesConfig: {
|
||||
[key: string]: PageConfig;
|
||||
};
|
||||
runtimeOptions?: Record<string, any>;
|
||||
serverData?: any;
|
||||
streamOptions?: RenderToPipeableStreamOptions;
|
||||
}
|
||||
|
||||
interface Piper {
|
||||
pipe: NodeWritablePiper;
|
||||
fallback: Function;
|
||||
}
|
||||
interface Response {
|
||||
statusCode?: number;
|
||||
statusText?: string;
|
||||
value?: string | Piper;
|
||||
headers?: Record<string, string>;
|
||||
}
|
||||
import { renderDocument } from './renderDocument.js';
|
||||
import { sendResponse, getLocation } from './server/response.js';
|
||||
import getDocumentData from './server/getDocumentData.js';
|
||||
|
||||
/**
|
||||
* Render and return the result as html string.
|
||||
@ -161,23 +120,6 @@ export async function renderToResponse(requestContext: ServerContext, renderOpti
|
||||
}
|
||||
}
|
||||
|
||||
async function sendResponse(
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse,
|
||||
response: Response,
|
||||
) {
|
||||
res.statusCode = response.statusCode;
|
||||
res.statusMessage = response.statusText;
|
||||
Object.entries(response.headers || {}).forEach(([name, value]) => {
|
||||
res.setHeader(name, value);
|
||||
});
|
||||
if (response.value && req.method !== 'HEAD') {
|
||||
res.end(response.value);
|
||||
} else {
|
||||
res.end();
|
||||
}
|
||||
}
|
||||
|
||||
function needRevalidate(matchedRoutes: RouteMatch[]) {
|
||||
return matchedRoutes.some(({ route }) => route.exports.includes('dataLoader') && route.exports.includes('staticDataLoader'));
|
||||
}
|
||||
@ -227,18 +169,13 @@ async function doRender(serverContext: ServerContext, renderOptions: RenderOptio
|
||||
await Promise.all(runtimeModules.statics.map(m => runtime.loadModule(m)).filter(Boolean));
|
||||
}
|
||||
|
||||
// Execute document dataLoader.
|
||||
let documentData: any;
|
||||
if (renderOptions.documentDataLoader) {
|
||||
const { loader } = renderOptions.documentDataLoader;
|
||||
if (isFunction(loader)) {
|
||||
documentData = await loader(requestContext, { documentOnly });
|
||||
// @TODO: document should have it's own context, not shared with app.
|
||||
appContext.documentData = documentData;
|
||||
} else {
|
||||
console.warn('Document dataLoader only accepts function.');
|
||||
}
|
||||
}
|
||||
const documentData = await getDocumentData({
|
||||
loaderConfig: renderOptions.documentDataLoader,
|
||||
requestContext,
|
||||
documentOnly,
|
||||
});
|
||||
// @TODO: document should have it's own context, not shared with app.
|
||||
appContext.documentData = documentData;
|
||||
|
||||
// Not to execute [getAppData] when CSR.
|
||||
if (!documentOnly) {
|
||||
@ -400,105 +337,3 @@ async function renderServerEntry(
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
interface RenderDocumentOptions {
|
||||
matches: RouteMatch[];
|
||||
renderOptions: RenderOptions;
|
||||
routes: RouteItem[];
|
||||
documentData: any;
|
||||
routePath?: string;
|
||||
downgrade?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render Document for CSR.
|
||||
*/
|
||||
function renderDocument(options: RenderDocumentOptions): Response {
|
||||
const {
|
||||
matches,
|
||||
renderOptions,
|
||||
routePath = '',
|
||||
downgrade,
|
||||
routes,
|
||||
documentData,
|
||||
}: RenderDocumentOptions = options;
|
||||
|
||||
const {
|
||||
assetsManifest,
|
||||
app,
|
||||
Document,
|
||||
basename,
|
||||
routesConfig = {},
|
||||
serverData,
|
||||
} = renderOptions;
|
||||
|
||||
const appData = null;
|
||||
const appConfig = getAppConfig(app);
|
||||
|
||||
const loaderData = {};
|
||||
matches.forEach(async (match) => {
|
||||
const { id } = match.route;
|
||||
const pageConfig = routesConfig[id];
|
||||
|
||||
loaderData[id] = {
|
||||
pageConfig: pageConfig ? pageConfig({}) : {},
|
||||
};
|
||||
});
|
||||
|
||||
const appContext: AppContext = {
|
||||
assetsManifest,
|
||||
appConfig,
|
||||
appData,
|
||||
loaderData,
|
||||
matches,
|
||||
routes,
|
||||
documentOnly: true,
|
||||
renderMode: 'CSR',
|
||||
routePath,
|
||||
basename,
|
||||
downgrade,
|
||||
serverData,
|
||||
documentData,
|
||||
};
|
||||
|
||||
const documentContext = {
|
||||
main: null,
|
||||
};
|
||||
|
||||
const htmlStr = ReactDOMServer.renderToString(
|
||||
<AppContextProvider value={appContext}>
|
||||
<DocumentContextProvider value={documentContext}>
|
||||
{
|
||||
Document && <Document pagePath={routePath} />
|
||||
}
|
||||
</DocumentContextProvider>
|
||||
</AppContextProvider>,
|
||||
);
|
||||
|
||||
return {
|
||||
value: `<!DOCTYPE html>${htmlStr}`,
|
||||
headers: {
|
||||
'Content-Type': 'text/html; charset=utf-8',
|
||||
},
|
||||
statusCode: 200,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* ref: https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/server.tsx
|
||||
*/
|
||||
const REGEXP_WITH_HOSTNAME = /^https?:\/\/[^/]+/i;
|
||||
function getLocation(url: string) {
|
||||
// In case of invalid URL, provide a default base url.
|
||||
const locationPath = url.replace(REGEXP_WITH_HOSTNAME, '') || '/';
|
||||
const locationProps = parsePath(locationPath);
|
||||
const location: Location = {
|
||||
pathname: locationProps.pathname || '/',
|
||||
search: locationProps.search || '',
|
||||
hash: locationProps.hash || '',
|
||||
state: null,
|
||||
key: 'default',
|
||||
};
|
||||
|
||||
return location;
|
||||
}
|
||||
|
19
packages/runtime/src/server/getDocumentData.ts
Normal file
19
packages/runtime/src/server/getDocumentData.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import type { DocumentDataLoaderConfig, RequestContext } from '../types.js';
|
||||
|
||||
interface Options {
|
||||
loaderConfig: DocumentDataLoaderConfig;
|
||||
requestContext: RequestContext;
|
||||
documentOnly: boolean;
|
||||
}
|
||||
|
||||
export default async function getDocumentData(options: Options) {
|
||||
const { loaderConfig, requestContext, documentOnly } = options;
|
||||
if (loaderConfig) {
|
||||
const { loader } = loaderConfig;
|
||||
if (typeof loader === 'function') {
|
||||
return await loader(requestContext, { documentOnly });
|
||||
} else {
|
||||
console.warn('Document dataLoader only accepts function.');
|
||||
}
|
||||
}
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
import { createPath } from 'history';
|
||||
import type { To } from 'history';
|
||||
|
||||
export function createStaticNavigator() {
|
||||
return {
|
||||
createHref(to: To) {
|
||||
return typeof to === 'string' ? to : createPath(to);
|
||||
},
|
||||
push(to: To) {
|
||||
throw new Error(
|
||||
'You cannot use navigator.push() on the server because it is a stateless ' +
|
||||
'environment. This error was probably triggered when you did a ' +
|
||||
`\`navigate(${JSON.stringify(to)})\` somewhere in your app.`,
|
||||
);
|
||||
},
|
||||
replace(to: To) {
|
||||
throw new Error(
|
||||
'You cannot use navigator.replace() on the server because it is a stateless ' +
|
||||
'environment. This error was probably triggered when you did a ' +
|
||||
`\`navigate(${JSON.stringify(to)}, { replace: true })\` somewhere ` +
|
||||
'in your app.',
|
||||
);
|
||||
},
|
||||
go(delta: number) {
|
||||
throw new Error(
|
||||
'You cannot use navigator.go() on the server because it is a stateless ' +
|
||||
'environment. This error was probably triggered when you did a ' +
|
||||
`\`navigate(${delta})\` somewhere in your app.`,
|
||||
);
|
||||
},
|
||||
back() {
|
||||
throw new Error(
|
||||
'You cannot use navigator.back() on the server because it is a stateless ' +
|
||||
'environment.',
|
||||
);
|
||||
},
|
||||
forward() {
|
||||
throw new Error(
|
||||
'You cannot use navigator.forward() on the server because it is a stateless ' +
|
||||
'environment.',
|
||||
);
|
||||
},
|
||||
block() {
|
||||
throw new Error(
|
||||
'You cannot use navigator.block() on the server because it is a stateless ' +
|
||||
'environment.',
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
43
packages/runtime/src/server/response.ts
Normal file
43
packages/runtime/src/server/response.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import type { ServerResponse, IncomingMessage } from 'http';
|
||||
import { parsePath } from 'history';
|
||||
import type { Location } from 'history';
|
||||
import type {
|
||||
Response,
|
||||
} from '../types.js';
|
||||
|
||||
export async function sendResponse(
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse,
|
||||
response: Response,
|
||||
) {
|
||||
res.statusCode = response.statusCode;
|
||||
res.statusMessage = response.statusText;
|
||||
Object.entries(response.headers || {}).forEach(([name, value]) => {
|
||||
res.setHeader(name, value);
|
||||
});
|
||||
if (response.value && req.method !== 'HEAD') {
|
||||
res.end(response.value);
|
||||
} else {
|
||||
res.end();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ref: https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/server.tsx
|
||||
*/
|
||||
const REGEXP_WITH_HOSTNAME = /^https?:\/\/[^/]+/i;
|
||||
export function getLocation(url: string) {
|
||||
// In case of invalid URL, provide a default base url.
|
||||
const locationPath = url.replace(REGEXP_WITH_HOSTNAME, '') || '/';
|
||||
const locationProps = parsePath(locationPath);
|
||||
const location: Location = {
|
||||
pathname: locationProps.pathname || '/',
|
||||
search: locationProps.search || '',
|
||||
hash: locationProps.hash || '',
|
||||
state: null,
|
||||
key: 'default',
|
||||
};
|
||||
|
||||
return location;
|
||||
}
|
||||
|
@ -2,8 +2,7 @@ import * as Stream from 'stream';
|
||||
import type * as StreamType from 'stream';
|
||||
import * as ReactDOMServer from 'react-dom/server';
|
||||
import { getAllAssets } from '../Document.js';
|
||||
import type { RenderOptions } from '../runServerApp.js';
|
||||
import type { ServerAppRouterProps } from '../types.js';
|
||||
import type { ServerAppRouterProps, RenderOptions } from '../types.js';
|
||||
|
||||
const { Writable } = Stream;
|
||||
|
||||
@ -117,4 +116,4 @@ export function pipeToString(input): Promise<string> {
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,8 @@ import type { InitialEntry, AgnosticRouteObject, Location, History, RouterInit,
|
||||
import type { ComponentType, PropsWithChildren } from 'react';
|
||||
import type { HydrationOptions, Root } from 'react-dom/client';
|
||||
import type { Params, RouteObject } from 'react-router-dom';
|
||||
import type { RouteLoaderOptions } from './routes.js';
|
||||
import type { RenderToPipeableStreamOptions, NodeWritablePiper } from './server/streamRender.js';
|
||||
|
||||
type UseConfig = () => RouteConfig<Record<string, any>>;
|
||||
type UseData = () => RouteData;
|
||||
@ -300,6 +302,42 @@ export interface RouteMatch {
|
||||
|
||||
export type RenderMode = 'SSR' | 'SSG' | 'CSR';
|
||||
|
||||
interface Piper {
|
||||
pipe: NodeWritablePiper;
|
||||
fallback: Function;
|
||||
}
|
||||
|
||||
export interface Response {
|
||||
statusCode?: number;
|
||||
statusText?: string;
|
||||
value?: string | Piper;
|
||||
headers?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface RenderOptions {
|
||||
app: AppExport;
|
||||
assetsManifest: AssetsManifest;
|
||||
createRoutes: (options: Pick<RouteLoaderOptions, 'requestContext' | 'renderMode'>) => RouteItem[];
|
||||
runtimeModules: RuntimeModules;
|
||||
documentDataLoader?: DocumentDataLoaderConfig;
|
||||
Document?: DocumentComponent;
|
||||
documentOnly?: boolean;
|
||||
preRender?: boolean;
|
||||
renderMode?: RenderMode;
|
||||
// basename is used both for server and client, once set, it will be sync to client.
|
||||
basename?: string;
|
||||
// serverOnlyBasename is used when just want to change basename for server.
|
||||
serverOnlyBasename?: string;
|
||||
routePath?: string;
|
||||
disableFallback?: boolean;
|
||||
routesConfig?: {
|
||||
[key: string]: PageConfig;
|
||||
};
|
||||
runtimeOptions?: Record<string, any>;
|
||||
serverData?: any;
|
||||
streamOptions?: RenderToPipeableStreamOptions;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface ImportMeta {
|
||||
// The build target for ice.js
|
||||
|
@ -1,33 +0,0 @@
|
||||
import { expect, it, describe } from 'vitest';
|
||||
import { createStaticNavigator } from '../src/server/navigator';
|
||||
|
||||
describe('mock server navigator', () => {
|
||||
const staticNavigator = createStaticNavigator();
|
||||
it('createHref', () => {
|
||||
expect(staticNavigator.createHref('/')).toBe('/');
|
||||
});
|
||||
|
||||
it('push', () => {
|
||||
expect(() => staticNavigator.push('/')).toThrow();
|
||||
});
|
||||
|
||||
it('replace', () => {
|
||||
expect(() => staticNavigator.replace('/')).toThrow();
|
||||
});
|
||||
|
||||
it('go', () => {
|
||||
expect(() => staticNavigator.go(1)).toThrow();
|
||||
});
|
||||
|
||||
it('back', () => {
|
||||
expect(() => staticNavigator.back()).toThrow();
|
||||
});
|
||||
|
||||
it('forward', () => {
|
||||
expect(() => staticNavigator.forward()).toThrow();
|
||||
});
|
||||
|
||||
it('block', () => {
|
||||
expect(() => staticNavigator.block()).toThrow();
|
||||
});
|
||||
});
|
@ -192,6 +192,12 @@ export interface Config {
|
||||
memoryRouter?: boolean;
|
||||
|
||||
server?: {
|
||||
/**
|
||||
* Generate sperate bundle for fallback,
|
||||
* it only outputs document content.
|
||||
*/
|
||||
fallbackEntry?: boolean;
|
||||
|
||||
entry?: string;
|
||||
|
||||
buildOptions?: (options: BuildOptions) => BuildOptions;
|
||||
|
@ -802,7 +802,7 @@ importers:
|
||||
specifier: ^18.0.6
|
||||
version: 18.0.11
|
||||
|
||||
examples/with-entry-type:
|
||||
examples/with-fallback-entry:
|
||||
dependencies:
|
||||
'@ice/app':
|
||||
specifier: workspace:*
|
||||
@ -811,10 +811,10 @@ importers:
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/runtime
|
||||
react:
|
||||
specifier: ^18.0.0
|
||||
specifier: ^18.2.0
|
||||
version: 18.2.0
|
||||
react-dom:
|
||||
specifier: ^18.0.0
|
||||
specifier: ^18.2.0
|
||||
version: 18.2.0(react@18.2.0)
|
||||
devDependencies:
|
||||
'@types/react':
|
||||
@ -823,9 +823,6 @@ importers:
|
||||
'@types/react-dom':
|
||||
specifier: ^18.0.2
|
||||
version: 18.0.11
|
||||
fs-extra:
|
||||
specifier: ^10.0.0
|
||||
version: 10.1.0
|
||||
webpack:
|
||||
specifier: ^5.88.0
|
||||
version: 5.88.2
|
||||
|
@ -65,7 +65,7 @@ describe(`build ${example}`, () => {
|
||||
|
||||
test('render route config when downgrade to CSR.', async () => {
|
||||
await page.push('/downgrade.html');
|
||||
expect(await page.$$text('title')).toStrictEqual(['hello']);
|
||||
expect(await page.$$text('title')).toStrictEqual(['']);
|
||||
expect((await page.$$text('h2')).length).toEqual(0);
|
||||
});
|
||||
|
||||
|
26
tests/integration/with-fallback-entry.test.ts
Normal file
26
tests/integration/with-fallback-entry.test.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { expect, test, describe } from 'vitest';
|
||||
import { buildFixture } from '../utils/build';
|
||||
// @ts-ignore
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const example = 'with-fallback-entry';
|
||||
|
||||
describe(`build ${example}`, () => {
|
||||
let sizeServer = 0;
|
||||
let sizeFallback = 0;
|
||||
|
||||
test('build fallback entry', async () => {
|
||||
await buildFixture(example);
|
||||
const serverPath = path.join(__dirname, `../../examples/${example}/build/server/index.cjs`);
|
||||
sizeServer = fs.statSync(serverPath).size;
|
||||
const fallbackPath = path.join(__dirname, `../../examples/${example}/build/server/index.fallback.cjs`);
|
||||
sizeFallback = fs.statSync(fallbackPath).size;
|
||||
|
||||
expect(sizeFallback).toBeLessThan(sizeServer);
|
||||
// The Stat size of fallback entry will reduce more than 50kb.
|
||||
expect(sizeServer - sizeFallback).toBeGreaterThan(50 * 1024);
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user