feat: support tree-shaking react-router deps when only one route (#86)

* feat: support tree-shaking react-router deps when only one route

* chore: optimize history API

* fix: typo error

* chore: make esbuild compile @ice/runtime add inject env vars

* chore: make esbuild compile @ice/runtime add inject env vars

* chore: resolve conflict

* feat: mock react-router api when disable router

* feat: env

* fix: test

* feat: support dotenv

* chore: add stringify

* chore: lock build-scripts

* feat: add removeHistoryDeadCode userConfig

* fix: duplicate register of routes

* fix: default value of define

* chore: optimize code

* fix: typo

* chore: upgrade react

* fix: code

* chore: optimize matchRoutes

Co-authored-by: ClarkXia <xiawenwu41@gmail.com>
This commit is contained in:
大果 2022-04-29 16:16:00 +08:00 committed by GitHub
parent e357724703
commit 701a2190a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 929 additions and 531 deletions

4
.gitignore vendored
View File

@ -46,4 +46,6 @@ compiled
# website
.docusaurus
.cache-loader
.cache-loader
.env*.local

View File

@ -3,6 +3,10 @@ import SpeedMeasurePlugin from 'speed-measure-webpack-plugin';
export default defineConfig({
publicPath: '/',
define: {
HAHA: JSON.stringify(true),
'process.env.HAHA': JSON.stringify(true),
},
webpack: (webpackConfig) => {
if (process.env.NODE_ENV !== 'test') {
webpackConfig.plugins?.push(new SpeedMeasurePlugin());

View File

@ -17,7 +17,7 @@
},
"devDependencies": {
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"@types/react-dom": "^18.0.2",
"browserslist": "^4.19.3",
"regenerator-runtime": "^0.13.9",
"speed-measure-webpack-plugin": "^1.5.0"

View File

@ -1,12 +1,13 @@
import { defineAppConfig } from 'ice';
if (process.env.ICE_RUNTIME_ERROR_BOUNDARY) {
if (process.env.ICE_CORE_ERROR_BOUNDARY === 'true') {
console.error('__REMOVED__');
}
console.log('__LOG__');
console.warn('__WARN__');
console.error('__ERROR__');
console.log('process.env.HAHA', process.env.HAHA);
export default defineAppConfig({
app: {

View File

@ -0,0 +1,2 @@
ICE_A=env
ICE_VERSION=$npm_package_version

View File

@ -0,0 +1 @@
ICE_A=env-development

View File

@ -0,0 +1,7 @@
import { defineConfig } from '@ice/app';
export default defineConfig({
publicPath: '/',
removeHistoryDeadCode: true,
sourceMap: true,
});

View File

@ -0,0 +1,23 @@
{
"name": "basic-project",
"version": "1.0.0",
"scripts": {
"start": "ice start",
"build": "ice build"
},
"description": "",
"author": "",
"license": "MIT",
"dependencies": {
"@ice/app": "file:../../packages/ice",
"@ice/runtime": "^1.0.0",
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"devDependencies": {
"browserslist": "^4.19.3",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"webpack-bundle-analyzer": "^4.5.0"
}
}

View File

@ -0,0 +1,11 @@
import { defineAppConfig } from 'ice';
if (process.env.ICE_CORE_ERROR_BOUNDARY) {
console.log('__REMOVED__');
}
console.log('ICE_VERSION', process.env.ICE_VERSION);
export default defineAppConfig({
app: {},
});

View File

@ -0,0 +1,26 @@
/* eslint-disable react/self-closing-comp */
import React from 'react';
import { Meta, Title, Links, Main, Scripts } from 'ice';
function Document(props) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="description" content="ICE 3.0 Demo" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Title />
<Links />
</head>
<body>
<Main>
{props.children}
</Main>
<Scripts />
</body>
</html>
);
}
export default Document;

View File

@ -0,0 +1,8 @@
import { Link } from 'ice';
console.log('process.env.ICE_CORE_ROUTER', process.env.ICE_CORE_ROUTER);
console.log('Link', Link);
export default function Home() {
return <div>home <h2>Home Page</h2></div>;
}

View File

@ -0,0 +1,32 @@
{
"compileOnSave": false,
"buildOnSave": false,
"compilerOptions": {
"baseUrl": ".",
"outDir": "build",
"module": "esnext",
"target": "es6",
"jsx": "react-jsx",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"lib": ["es6", "dom"],
"sourceMap": true,
"allowJs": true,
"rootDir": "./",
"forceConsistentCasingInFileNames": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noImplicitAny": false,
"importHelpers": true,
"strictNullChecks": true,
"suppressImplicitAnyIndexErrors": true,
"noUnusedLocals": true,
"skipLibCheck": true,
"paths": {
"@/*": ["./src/*"],
"ice": [".ice"]
}
},
"include": ["src", ".ice", "ice.config.*"],
"exclude": ["node_modules", "build", "public"]
}

View File

@ -25,6 +25,8 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
.command('build')
.description('build project')
.allowUnknownOption()
.option('--mode <mode>', 'set mode', 'production')
.option('--analyzer', 'visualize size of output files', false)
.option('--config <config>', 'use custom config')
.option('--rootDir <rootDir>', 'project root directory', cwd)
.action(async ({ rootDir, ...commandArgs }) => {
@ -36,6 +38,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
.command('start')
.description('start server')
.allowUnknownOption()
.option('--mode <mode>', 'set mode', 'development')
.option('--config <config>', 'custom config path')
.option('-h, --host <host>', 'dev server host', '0.0.0.0')
.option('-p, --port <port>', 'dev server port', 3333)
@ -54,6 +57,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
.command('test')
.description('run tests with jest')
.allowUnknownOption() // allow jest config
.option('--mode <mode>', 'set mode', 'test')
.option('--config <config>', 'use custom config')
.option('--rootDir <rootDir>', 'project root directory', cwd)
.action(async ({ rootDir, ...commandArgs }) => {

View File

@ -21,15 +21,16 @@
"dependencies": {
"@ice/bundles": "^0.1.0",
"@ice/route-manifest": "^1.0.0",
"@ice/runtime": "^1.0.0",
"@ice/webpack-config": "^1.0.0",
"address": "^1.1.2",
"build-scripts": "2.0.0-17",
"chalk": "^4.0.0",
"commander": "^9.0.0",
"consola": "^2.15.3",
"detect-port": "^1.3.0",
"cross-spawn": "^7.0.3",
"detect-port": "^1.3.0",
"dotenv": "^16.0.0",
"dotenv-expand": "^8.0.3",
"ejs": "^3.1.6",
"esbuild": "^0.14.23",
"fast-glob": "^3.2.11",
@ -39,6 +40,7 @@
"open": "^8.4.0",
"mrmime": "^1.0.0",
"prettier": "^2.5.1",
"react-router": "^6.3.0",
"sass": "^1.49.9",
"semver": "^7.3.5",
"temp": "^0.9.4",

View File

@ -11,18 +11,15 @@ interface Options {
export const getAppConfig = async (options: Options): Promise<AppConfig> => {
const { esbuildCompile, rootDir } = options;
const outfile = path.join(rootDir, 'node_modules', 'entry.mjs');
try {
await esbuildCompile({
// TODO: detect src/app if it is exists
entryPoints: [path.join(rootDir, 'src/app')],
outfile,
format: 'esm',
});
const appConfig = (await import(outfile)).default;
consola.debug('app config:', appConfig);
return appConfig;
} catch (err) {
consola.error('[ERROR]', 'Fail to analyze app config', err);
}
await esbuildCompile({
// TODO: detect src/app if it is exists
entryPoints: [path.join(rootDir, 'src/app')],
outfile,
format: 'esm',
});
const appConfig = (await import(outfile)).default;
consola.debug('get app config by esbuild:', appConfig);
return appConfig;
};

View File

@ -14,11 +14,12 @@ import build from './commands/build.js';
import getContextConfig from './utils/getContextConfig.js';
import getWatchEvents from './getWatchEvents.js';
import { getAppConfig } from './analyzeRuntime.js';
import { defineRuntimeEnv, updateRuntimeEnv } from './utils/runtimeEnv.js';
import { initProcessEnv, updateRuntimeEnv, getCoreEnvKeys } from './utils/runtimeEnv.js';
import getRuntimeModules from './utils/getRuntimeModules.js';
import { generateRoutesInfo } from './routes.js';
import webPlugin from './plugins/web/index.js';
import configPlugin from './plugins/config.js';
import type { AppConfig } from './utils/runtimeEnv.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
@ -40,6 +41,10 @@ async function createService({ rootDir, command, commandArgs }: CreateServiceOpt
templates: [templateDir],
});
// load dotenv, set to process.env
await initProcessEnv(rootDir, command, commandArgs);
const coreEnvKeys = getCoreEnvKeys();
const { addWatchEvent, removeWatchEvent } = createWatch({
watchDir: rootDir,
command,
@ -80,49 +85,80 @@ async function createService({ rootDir, command, commandArgs }: CreateServiceOpt
},
});
await ctx.resolveConfig();
const { userConfig: { routes: routesConfig } } = ctx;
const { userConfig } = ctx;
const { routes: routesConfig } = userConfig;
const routesRenderData = generateRoutesInfo(rootDir, routesConfig);
const { routeManifest } = routesRenderData;
generator.modifyRenderData((renderData) => ({
...renderData,
...routesRenderData,
coreEnvKeys,
}));
dataCache.set('routes', JSON.stringify(routesRenderData.routeManifest));
dataCache.set('routes', JSON.stringify(routeManifest));
const runtimeModules = getRuntimeModules(ctx.getAllPlugin());
generator.modifyRenderData((renderData) => ({
...renderData,
runtimeModules,
}));
await ctx.setup();
// render template before webpack compile
const renderStart = new Date().getTime();
generator.render();
addWatchEvent(
...getWatchEvents({ generator, targetDir, templateDir, cache: dataCache, ctx }),
);
const compileIncludes = runtimeModules.map(({ name }) => `${name}/runtime`);
const contextConfig = getContextConfig(ctx, {
compileIncludes,
port: commandArgs.port,
});
const webTask = contextConfig.find(({ name }) => name === 'web');
// render template before webpack compile
const renderStart = new Date().getTime();
generator.modifyRenderData((renderData) => ({
...renderData,
runtimeModules,
}));
generator.render();
consola.debug('template render cost:', new Date().getTime() - renderStart);
// define runtime env before get webpack config
defineRuntimeEnv();
const compileIncludes = runtimeModules.map(({ name }) => `${name}/runtime`);
const contextConfig = getContextConfig(ctx, { compileIncludes, port: commandArgs.port });
const webTask = contextConfig.find(({ name }) => name === 'web');
const esbuildCompile = createEsbuildCompiler({
rootDir,
task: webTask,
});
let appConfig: AppConfig;
if (command === 'build') {
try {
// should after generator, otherwise it will compile error
appConfig = await getAppConfig({ esbuildCompile, rootDir });
} catch (err) {
consola.warn('Failed to get app config:', err.message);
consola.debug(err);
}
}
let disableRouter = false;
if (userConfig.removeHistoryDeadCode) {
let routesCount = 0;
routeManifest && Object.keys(routeManifest).forEach((key) => {
const routeItem = routeManifest[key];
if (!routeItem.layout) {
routesCount += 1;
}
});
if (routesCount <= 1) {
consola.info('[ice] removeHistoryDeadCode is enabled and only have one route, ice build will remove history and react-router dead code.');
disableRouter = true;
}
}
updateRuntimeEnv(appConfig, disableRouter);
return {
run: async () => {
if (command === 'start') {
return await start(ctx, contextConfig, esbuildCompile);
} else if (command === 'build') {
const appConfig = await getAppConfig({ esbuildCompile, rootDir });
updateRuntimeEnv(appConfig, routesRenderData.routeManifest);
return await build(ctx, contextConfig, esbuildCompile);
}
},

View File

@ -250,6 +250,10 @@ const userConfig = [
}
},
},
{
name: 'removeHistoryDeadCode',
validation: 'boolean',
},
];
const cliOptions = [
@ -257,9 +261,13 @@ const cliOptions = [
name: 'open',
commands: ['start'],
},
{
name: 'mode',
commands: ['start', 'build', 'test'],
},
{
name: 'analyzer',
commands: ['start'],
commands: ['start', 'build'],
setConfig: (config: Config, analyzer: boolean) => {
return mergeDefaultValue(config, 'analyzer', analyzer);
},

View File

@ -22,6 +22,17 @@ const webPlugin: Plugin = ({ registerTask, context, onHook }) => {
onHook(`before.${command as 'start' | 'build'}.run`, async ({ esbuildCompile }) => {
await emptyDir(outputDir);
// same as webpack define runtimeEnvs in build-webpack-config
const runtimeDefineVars = {};
Object.keys(process.env).forEach((key) => {
if (/^ICE_CORE_/i.test(key)) {
// in server.entry
runtimeDefineVars[`__process.env.${key}__`] = JSON.stringify(process.env[key]);
} else if (/^ICE_/i.test(key)) {
runtimeDefineVars[`process.env.${key}`] = JSON.stringify(process.env[key]);
}
});
serverCompiler = async () => {
await esbuildCompile({
entryPoints: [path.join(rootDir, '.ice/entry.server')],
@ -29,6 +40,7 @@ const webPlugin: Plugin = ({ registerTask, context, onHook }) => {
// platform: 'node',
format: 'esm',
outExtension: { '.js': '.mjs' },
define: runtimeDefineVars,
plugins: [
createAssetsPlugin(assetsManifest, rootDir),
],

View File

@ -1,6 +1,6 @@
import * as path from 'path';
import type { RouteObject } from 'react-router';
import fse from 'fs-extra';
import type { RouteItem } from '@ice/runtime';
interface Options {
entry: string;
@ -19,7 +19,15 @@ export default async function generateHTML(options: Options) {
ssr,
} = options;
const serverEntry = await import(entry);
let serverEntry;
try {
serverEntry = await import(entry);
} catch (err) {
// make error clearly, notice typeof err === 'string'
throw new Error(`import ${entry} error: ${err}`);
}
const routes = JSON.parse(fse.readFileSync(routeManifest, 'utf8'));
const paths = getPaths(routes);
@ -47,7 +55,7 @@ export default async function generateHTML(options: Options) {
* @param routes
* @returns
*/
function getPaths(routes: RouteItem[], parentPath = ''): string[] {
function getPaths(routes: RouteObject[], parentPath = ''): string[] {
let pathList = [];
routes.forEach(route => {

View File

@ -1,5 +1,5 @@
import * as fs from 'fs';
import { matchRoutes } from '@ice/runtime';
import matchRoutes from '../../../utils/matchRoutes.js';
import type { Request, Response } from 'express';
interface Options {
@ -26,7 +26,15 @@ export function setupRenderServer(options: Options) {
if (matches.length === 0) return;
const entry = await serverCompiler();
const serverEntry = await import(entry);
let serverEntry;
try {
serverEntry = await import(entry);
} catch (err) {
// make error clearly, notice typeof err === 'string'
res.send(`import ${entry} error: ${err}`);
}
const requestContext = {
req,
res,

View File

@ -25,21 +25,30 @@ export function createEsbuildCompiler(options: Options) {
const { taskConfig, webpackConfig } = task;
const transformPlugins = getTransformPlugins(taskConfig);
const alias = (webpackConfig.resolve?.alias || {}) as Record<string, string | false>;
const { define = {} } = taskConfig;
// auto stringify define value
const defineVars = {};
Object.keys(define).forEach((key) => {
defineVars[key] = JSON.stringify(define[key]);
});
const esbuildCompile: EsbuildCompile = async (buildOptions) => {
const startTime = new Date().getTime();
consola.debug('[esbuild]', `start compile for: ${buildOptions.entryPoints}`);
const define = {
// ref: https://github.com/evanw/esbuild/blob/master/CHANGELOG.md#01117
// in esm, this in the global should be undefined. Set the following config to avoid warning
this: undefined,
...defineVars,
...buildOptions.define,
};
const buildResult = await esbuild.build({
bundle: true,
target: 'node12.19.0',
...buildOptions,
define: {
// ref: https://github.com/evanw/esbuild/blob/master/CHANGELOG.md#01117
// in esm, this in the global should be undefined. Set the following config to avoid warning
this: undefined,
// TODO: sync ice runtime env
'process.env.ICE_RUNTIME_SERVER': 'true',
},
define,
inject: [path.resolve(__dirname, '../polyfills/react.js')],
plugins: [
emptyCSSPlugin(),
@ -69,3 +78,5 @@ export function createEsbuildCompiler(options: Options) {
};
return esbuildCompile;
}

View File

@ -0,0 +1,15 @@
import { matchRoutes as originMatchRoutes } from 'react-router';
const matchRoutes: typeof originMatchRoutes = function (routes, location) {
let matches = originMatchRoutes(routes, location);
if (!matches) return [];
return matches.map(({ params, pathname, pathnameBase, route }) => ({
params,
pathname,
route,
pathnameBase,
}));
};
export default matchRoutes;

View File

@ -1,27 +1,66 @@
import type { RouteManifest } from '@ice/route-manifest';
import * as path from 'path';
import * as fs from 'fs';
import * as dotenv from 'dotenv';
import { expand as dotenvExpand } from 'dotenv-expand';
import type { CommandArgs, CommandName } from 'build-scripts';
export type AppConfig = Record<string, any>;
export interface Envs {
[key: string]: string;
}
export const defineRuntimeEnv = () => {
const runtimeEnvironment = {
ROUTER: 'true',
ERROR_BOUNDARY: 'true',
AUTH: 'true',
INITIAL_DATA: 'true',
};
Object.keys(runtimeEnvironment).forEach((key) => {
process.env[`ICE_RUNTIME_${key}`] = runtimeEnvironment[key];
export async function initProcessEnv(rootDir: string, command: CommandName, commandArgs: CommandArgs): Promise<void> {
const { mode } = commandArgs;
// .env.${mode}.local is the highest priority
const dotenvFiles = [
`.env.${mode}.local`,
`.env.${mode}`,
'.env.local',
'.env',
];
dotenvFiles.forEach(dotenvFile => {
const filepath = path.join(rootDir, dotenvFile);
if (fs.existsSync(dotenvFile)) {
dotenvExpand(
dotenv.config({
path: filepath,
}),
);
}
});
};
export const updateRuntimeEnv = (appConfig?: AppConfig, routeManifest?: RouteManifest) => {
process.env.ICE_CORE_MODE = mode;
process.env.ICE_CORE_DEV_PORT = commandArgs.port;
if (command === 'start') {
process.env.NODE_ENV = 'development';
} else if (command === 'test') {
process.env.NODE_ENV = 'test';
} else {
// build
process.env.NODE_ENV = 'production';
}
// set runtime initial env
process.env.ICE_CORE_ROUTER = 'true';
process.env.ICE_CORE_ERROR_BOUNDARY = 'true';
process.env.ICE_CORE_INITIAL_DATA = 'true';
}
export const updateRuntimeEnv = (appConfig: AppConfig, disableRouter: boolean) => {
if (!appConfig?.app?.getInitialData) {
process.env['ICE_RUNTIME_INITIAL_DATA'] = 'false';
process.env['ICE_CORE_INITIAL_DATA'] = 'false';
}
if (!appConfig?.app?.errorBoundary) {
process.env['ICE_RUNTIME_ERROR_BOUNDARY'] = 'false';
process.env['ICE_CORE_ERROR_BOUNDARY'] = 'false';
}
if (routeManifest && Object.keys(routeManifest).length <= 1) {
process.env['ICE_RUNTIME_ROUTER'] = 'false';
if (disableRouter) {
process.env['ICE_CORE_ROUTER'] = 'false';
}
};
export function getCoreEnvKeys() {
return ['ICE_CORE_MODE', 'ICE_CORE_ROUTER', 'ICE_CORE_ERROR_BOUNDARY', 'ICE_CORE_INITIAL_DATA', 'ICE_CORE_DEV_PORT'];
}

View File

@ -1,3 +1,9 @@
// Define process.env in top make it possible to use ICE_CORE_* in @ice/runtime, esbuild define options doesn't have the ability
// The runtime value such as __process.env.ICE_CORE_*__ will be replaced by esbuild define, so the value is real-time
<% coreEnvKeys.forEach((key) => { %>
process.env.<%= key %> = __process.env.<%= key %>__;
<% }) %>
import * as runtime from '@ice/runtime/server';
import appConfig from '@/app';
import runtimeModules from './runtimeModules';

View File

@ -1,34 +1,49 @@
import {
defineAppConfig,
useAppData,
useData,
useConfig,
Link as OriginLink,
Outlet as OriginOutlet,
useParams as useOriginParams,
useSearchParams as useOriginSearchParams,
LinkSingle,
OutletSingle,
useParamsSingle,
useSearchParamsSingle,
} from '@ice/runtime';
let Link, Outlet, useParams, useSearchParams;
if (process.env.ICE_CORE_ROUTER === 'true') {
Link = OriginLink;
Outlet = OriginOutlet;
useParams = useOriginParams;
useSearchParams = useOriginSearchParams;
} else {
Link = LinkSingle;
Outlet = OutletSingle;
useParams = useParamsSingle;
useSearchParams = useSearchParamsSingle;
}
export {
Link,
Outlet,
useParams,
useSearchParams,
Meta,
Title,
Links,
Scripts,
Main,
} from '@ice/runtime';
<%- framework.imports %>
};
export {
defineAppConfig,
useAppData,
useData,
useConfig,
Link,
Outlet,
useParams,
useSearchParams,
Meta,
Title,
Links,
Scripts,
Main,
} from '@ice/runtime';
<%- framework.imports %>
export {
<% if (framework.exports) { %>
<%- framework.exports %>
<% } %>

View File

@ -42,21 +42,15 @@ export default function App(props: Props) {
[],
);
let element: React.ReactNode;
if (routes.length === 1 && !routes[0].children) {
// TODO: 去除 react-router-dom history 等依赖
element = routes[0].element;
} else {
element = (
<AppRouter
action={action}
location={location}
navigator={navigator}
static={staticProp}
routes={routes}
/>
);
}
let element: React.ReactNode = (
<AppRouter
action={action}
location={location}
navigator={navigator}
static={staticProp}
routes={routes}
/>
);
return (
<StrictMode>

View File

@ -1,19 +1,22 @@
import * as React from 'react';
import type { RouteObject } from 'react-router-dom';
import { Router, useRoutes } from 'react-router-dom';
import { RouterSingle, useRoutesSingle } from './utils/history-single.js';
import type { AppRouterProps } from './types.js';
const AppRouter: React.ComponentType<AppRouterProps> = (props) => {
const { action, location, navigator, static: staticProps, routes } = props;
const IceRouter = process.env.ICE_CORE_ROUTER === 'true' ? Router : RouterSingle;
return (
<Router
<IceRouter
navigationType={action}
location={location}
navigator={navigator}
static={staticProps}
>
<Routes routes={routes} />
</Router>
</IceRouter>
);
};
@ -22,7 +25,8 @@ interface RoutesProps {
}
function Routes({ routes }: RoutesProps) {
const element = useRoutes(routes);
const useIceRoutes = process.env.ICE_CORE_ROUTER === 'true' ? useRoutes : useRoutesSingle;
const element = useIceRoutes(routes);
return element;
}

View File

@ -4,6 +4,12 @@ import {
useParams,
useSearchParams,
} from 'react-router-dom';
import {
LinkSingle,
OutletSingle,
useParamsSingle,
useSearchParamsSingle,
} from './utils/history-single.js';
import Runtime from './runtime.js';
import App from './App.js';
import runClientApp from './runClientApp.js';
@ -47,6 +53,10 @@ export {
Outlet,
useParams,
useSearchParams,
LinkSingle,
OutletSingle,
useParamsSingle,
useSearchParamsSingle,
};
export type {

View File

@ -2,6 +2,7 @@ import React from 'react';
import type { Location } from 'history';
import type { RouteObject } from 'react-router-dom';
import { matchRoutes as originMatchRoutes } from 'react-router-dom';
import { matchRoutesSingle } from './utils/history-single.js';
import RouteWrapper from './RouteWrapper.js';
import type { RouteItem, RouteModules, RouteWrapper as IRouteWrapper, RouteMatch, InitialContext, RoutesConfig, RoutesData } from './types';
@ -126,7 +127,8 @@ export function matchRoutes(
location: Partial<Location> | string,
basename?: string,
): RouteMatch[] {
let matches = originMatchRoutes(routes as unknown as RouteObject[], location, basename);
const matchRoutesFn = process.env.ICE_CORE_ROUTER === 'true' ? originMatchRoutes : matchRoutesSingle;
let matches = matchRoutesFn(routes as unknown as RouteObject[], location, basename);
if (!matches) return [];
return matches.map(({ params, pathname, pathnameBase, route }) => ({

View File

@ -1,7 +1,8 @@
import React, { useLayoutEffect, useState } from 'react';
import { createHashHistory, createBrowserHistory } from 'history';
import type { HashHistory, BrowserHistory, Action, Location } from 'history';
import { createSearchParams } from 'react-router-dom';
import { createHistorySingle } from './utils/history-single.js';
import { createSearchParams } from './utils/createSearchParams.js';
import Runtime from './runtime.js';
import App from './App.js';
import { AppContextProvider } from './AppContext.js';
@ -73,7 +74,10 @@ async function render(runtime: Runtime, Document: ComponentWithChildren<{}>) {
const RouteWrappers = runtime.getWrappers();
const AppRouter = runtime.getAppRouter();
const history = (appContext.appConfig?.router?.type === 'hash' ? createHashHistory : createBrowserHistory)({ window });
const createHistory = process.env.ICE_CORE_ROUTER === 'true'
? (appContext.appConfig?.router?.type === 'hash' ? createHashHistory : createBrowserHistory)
: createHistorySingle;
const history = createHistory({ window });
render(
document,
@ -89,7 +93,7 @@ async function render(runtime: Runtime, Document: ComponentWithChildren<{}>) {
}
interface BrowserEntryProps {
history: HashHistory | BrowserHistory;
history: HashHistory | BrowserHistory | null;
appContext: AppContext;
AppProvider: React.ComponentType<any>;
RouteWrappers: RouteWrapper[];
@ -123,22 +127,26 @@ function BrowserEntry({ history, appContext, Document, ...rest }: BrowserEntryPr
// listen the history change and update the state which including the latest action and location
useLayoutEffect(() => {
history.listen(({ action, location }) => {
const currentMatches = matchRoutes(routes, location);
if (!currentMatches.length) {
throw new Error(`Routes not found in location ${location.pathname}.`);
}
if (history) {
history.listen(({ action, location }) => {
const currentMatches = matchRoutes(routes, location);
if (!currentMatches.length) {
throw new Error(`Routes not found in location ${location.pathname}.`);
}
loadNextPage(currentMatches, historyState).then(({ routesData, routesConfig }) => {
setHistoryState({
action,
location,
routesData,
routesConfig,
matches: currentMatches,
loadNextPage(currentMatches, historyState).then(({ routesData, routesConfig }) => {
setHistoryState({
action,
location,
routesData,
routesConfig,
matches: currentMatches,
});
});
});
});
}
// just trigger once
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// update app context for the current route.

View File

@ -3,7 +3,7 @@ import * as React from 'react';
import * as ReactDOMServer from 'react-dom/server';
import { Action, parsePath } from 'history';
import type { Location } from 'history';
import { createSearchParams } from 'react-router-dom';
import { createSearchParams } from './utils/createSearchParams.js';
import Runtime from './runtime.js';
import App from './App.js';
import { AppContextProvider } from './AppContext.js';

View File

@ -90,7 +90,7 @@ export interface RouteItem {
path: string;
element?: ReactNode;
componentName: string;
index?: false;
index?: boolean;
exact?: boolean;
strict?: boolean;
load?: () => Promise<RouteComponent>;

View File

@ -0,0 +1,11 @@
// copy from react-router-dom, very simple, make size smaller
export const createSearchParams = (init) => {
if (init === void 0) {
init = '';
}
return new URLSearchParams(typeof init === 'string' || Array.isArray(init) || init instanceof URLSearchParams ? init : Object.keys(init).reduce((memo, key) => {
let value = init[key];
return memo.concat(Array.isArray(value) ? value.map(v => [key, v]) : [[key, value]]);
}, []));
};

View File

@ -0,0 +1,46 @@
/**
* react-router-dom tree-shaking react-router
*/
import * as React from 'react';
import type { History } from 'history';
export const useRoutesSingle = (routes) => {
return <>{routes[0].element}</>;
};
export const RouterSingle = (props) => {
return <>{props.children}</>;
};
export const createHistorySingle = (): History => {
return {
// @ts-expect-error
listen: () => {},
// @ts-expect-error
action: 'POP',
// @ts-expect-error
location: '',
};
};
export const matchRoutesSingle = (routes) => {
return routes.map(item => {
return {
params: {},
pathname: '',
pathnameBase: '',
route: item,
};
});
};
export const LinkSingle = () => null;
export const OutletSingle = () => {
return <></>;
};
export const useParamsSingle = () => {
return {};
};
export const useSearchParamsSingle = () => {
return [{}, () => {}];
};

View File

@ -27,11 +27,12 @@ interface ConfigurationCtx extends Config {
type Experimental = Pick<Configuration, 'experiments'>;
export type ModifyWebpackConfig = (config: Configuration, ctx: ConfigurationCtx) => Configuration;
export interface Config {
mode: 'none' | 'development' | 'production';
define?: Record<string, string | boolean>;
define?: {
[key: string]: string | boolean;
};
experimental?: Experimental;

View File

@ -24,4 +24,5 @@ export interface UserConfig {
sourceMap?: string | boolean;
tsChecker?: boolean;
eslint?: Config['eslintOptions'] | boolean;
removeHistoryDeadCode?: boolean;
}

View File

@ -18,7 +18,6 @@ import { createUnplugin } from 'unplugin';
import browserslist from 'browserslist';
import configAssets from './config/assets.js';
import configCss from './config/css.js';
import { getRuntimeEnvironment } from './clientEnv.js';
import AssetsManifestPlugin from './webpackPlugins/AssetsManifestPlugin.js';
import getTransformPlugins from './unPlugins/index.js';
@ -57,7 +56,7 @@ function getEntry(rootDir: string) {
const getWebpackConfig: GetWebpackConfig = ({ rootDir, config, webpack }) => {
const {
mode,
define,
define = {},
externals = {},
publicPath = '/',
outputDir = path.join(rootDir, 'build'),
@ -71,7 +70,6 @@ const getWebpackConfig: GetWebpackConfig = ({ rootDir, config, webpack }) => {
hash,
minify,
minimizerOptions = {},
port,
cacheDirectory,
https,
analyzer,
@ -88,23 +86,22 @@ const getWebpackConfig: GetWebpackConfig = ({ rootDir, config, webpack }) => {
aliasWithRoot[key] = alias[key].startsWith('.') ? path.join(rootDir, alias[key]) : alias[key];
});
const defineStaticVariables = {
...define || {},
'process.env.NODE_ENV': mode || 'development',
'process.env.SERVER_PORT': port,
};
// formate define variables
Object.keys(defineStaticVariables).forEach((key) => {
defineStaticVariables[key] = typeof defineStaticVariables[key] === 'boolean'
? defineStaticVariables[key]
: JSON.stringify(defineStaticVariables[key]);
// auto stringify define value
const defineVars = {};
Object.keys(define).forEach((key) => {
defineVars[key] = JSON.stringify(define[key]);
});
const runtimeEnv = getRuntimeEnvironment();
const defineRuntimeVariables = {};
Object.keys(runtimeEnv).forEach((key) => {
const runtimeValue = runtimeEnv[key];
// set true to flag the module as uncacheable
defineRuntimeVariables[key] = webpack.DefinePlugin.runtimeValue(runtimeValue, true);
const runtimeDefineVars = {};
const RUNTIME_PREFIX = /^ICE_/i;
Object.keys(process.env).filter((key) => {
return RUNTIME_PREFIX.test(key) || ['NODE_ENV'].includes(key);
}).forEach((key) => {
runtimeDefineVars[`process.env.${key}`] =
/^ICE_CORE_/i.test(key)
// ICE_CORE_* will be updated dynamically, so we need to make it effectively
? webpack.DefinePlugin.runtimeValue(() => JSON.stringify(process.env[key]), true)
: JSON.stringify(process.env[key]);
});
// create plugins
const webpackPlugins = getTransformPlugins(config).map((plugin) => createUnplugin(() => plugin).webpack());
@ -214,8 +211,8 @@ const getWebpackConfig: GetWebpackConfig = ({ rootDir, config, webpack }) => {
exclude: [/node_modules/, /bundles\/compiled/],
}),
new webpack.DefinePlugin({
...defineStaticVariables,
...defineRuntimeVariables,
...defineVars,
...runtimeDefineVars,
}),
new AssetsManifestPlugin({
fileName: 'assets-manifest.json',

File diff suppressed because it is too large Load Diff

View File

@ -60,6 +60,7 @@ export async function packDependency(options: Options): Promise<void> {
filesToCopy.push(require.resolve(filePath, {
paths: [path.dirname(id)],
}));
return `'./${path.basename(filePath)}'`;
}
},

View File

@ -7,6 +7,7 @@ import { startFixture, setupStartBrowser } from '../utils/start';
import { Page } from '../utils/browser';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
const example = 'basic-project';
@ -17,6 +18,7 @@ describe(`build ${example}`, () => {
test('open /', async () => {
await buildFixture(example);
const res = await setupBrowser({ example });
page = res.page;
browser = res.browser;
expect(await page.$$text('h2')).toStrictEqual(['Home Page']);