mirror of https://github.com/alibaba/ice.git
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:
parent
e357724703
commit
701a2190a5
|
|
@ -46,4 +46,6 @@ compiled
|
|||
|
||||
# website
|
||||
.docusaurus
|
||||
.cache-loader
|
||||
.cache-loader
|
||||
|
||||
.env*.local
|
||||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
ICE_A=env
|
||||
ICE_VERSION=$npm_package_version
|
||||
|
|
@ -0,0 +1 @@
|
|||
ICE_A=env-development
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import { defineConfig } from '@ice/app';
|
||||
|
||||
export default defineConfig({
|
||||
publicPath: '/',
|
||||
removeHistoryDeadCode: true,
|
||||
sourceMap: true,
|
||||
});
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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: {},
|
||||
});
|
||||
|
|
@ -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;
|
||||
|
|
@ -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>;
|
||||
}
|
||||
|
|
@ -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"]
|
||||
}
|
||||
|
|
@ -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 }) => {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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 => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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'];
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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 %>
|
||||
<% } %>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 }) => ({
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ export interface RouteItem {
|
|||
path: string;
|
||||
element?: ReactNode;
|
||||
componentName: string;
|
||||
index?: false;
|
||||
index?: boolean;
|
||||
exact?: boolean;
|
||||
strict?: boolean;
|
||||
load?: () => Promise<RouteComponent>;
|
||||
|
|
|
|||
|
|
@ -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]]);
|
||||
}, []));
|
||||
};
|
||||
|
|
@ -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 [{}, () => {}];
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -24,4 +24,5 @@ export interface UserConfig {
|
|||
sourceMap?: string | boolean;
|
||||
tsChecker?: boolean;
|
||||
eslint?: Config['eslintOptions'] | boolean;
|
||||
removeHistoryDeadCode?: boolean;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
823
pnpm-lock.yaml
823
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
|
@ -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)}'`;
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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']);
|
||||
|
|
|
|||
Loading…
Reference in New Issue