mirror of https://github.com/alibaba/ice.git
feat: define runtime value (#30)
* feat: define runtime value * chore: remove console * test: fix test option * fix: remove process fallback for client * fix: optimize code * feat: optimize code * fix: optimize code * fix: types * chore: optimize code * test: add test case * fix: remove useless code * chore: optimize code * chore: optimize code
This commit is contained in:
parent
75ea6fe113
commit
647996e63e
|
@ -1,5 +1,9 @@
|
|||
import { defineAppConfig } from 'ice';
|
||||
|
||||
if (process.env.ICE_RUNTIME_ERROR_BOUNDARY) {
|
||||
console.log('__REMOVED__');
|
||||
}
|
||||
|
||||
export default defineAppConfig({
|
||||
app: {
|
||||
// @ts-expect-error loss tslib dependency
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
import type { NormalModule } from 'webpack';
|
||||
|
||||
interface RuntimeEnvironment {
|
||||
[x: string]: (args: {module: NormalModule; key: string; version?: string}) => string;
|
||||
}
|
||||
|
||||
const RUNTIME_PREFIX = /^ICE_RUNTIME_/i;
|
||||
const isBooleanString = (str: string) => ['true', 'false'].includes(str);
|
||||
export const getRuntimeEnvironment = (customEnv?: Record<string, string>): RuntimeEnvironment => {
|
||||
// Grab ICE_RUNTIME_* environment variables and prepare them to be
|
||||
// injected into the application via DefinePlugin in webpack configuration.
|
||||
const raw = Object.keys({ ...process.env, ...(customEnv || {}) }).filter(key => RUNTIME_PREFIX.test(key))
|
||||
.reduce((env, key) => {
|
||||
env[key.startsWith('process.env.') ? key : `process.env.${key}`] = () => {
|
||||
const envValue = process.env[key] ?? customEnv[key];
|
||||
return isBooleanString(envValue) ? envValue : JSON.stringify(envValue);
|
||||
};
|
||||
return env;
|
||||
}, {});
|
||||
return raw;
|
||||
};
|
|
@ -11,6 +11,7 @@ import type { Configuration as DevServerConfiguration } from 'webpack-dev-server
|
|||
import type { Config } from '@ice/types';
|
||||
import type { CommandArgs } from 'build-scripts';
|
||||
import { createUnplugin } from 'unplugin';
|
||||
import { getRuntimeEnvironment } from './clientEnv.js';
|
||||
import AssetsManifestPlugin from './webpackPlugins/AssetsManifestPlugin.js';
|
||||
import getTransformPlugins from './unPlugins/index.js';
|
||||
|
||||
|
@ -42,11 +43,19 @@ const getWebpackConfig: GetWebpackConfig = ({ rootDir, config, commandArgs = {}
|
|||
} = config;
|
||||
|
||||
const dev = mode !== 'production';
|
||||
const defineVariables = {
|
||||
const defineStaticVariables = {
|
||||
'process.env.NODE_ENV': JSON.stringify(mode || 'development'),
|
||||
'process.env.SERVER_PORT': JSON.stringify(commandArgs.port),
|
||||
'process.env.__IS_SERVER__': false,
|
||||
};
|
||||
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);
|
||||
});
|
||||
|
||||
// create plugins
|
||||
const webpackPlugins = getTransformPlugins(rootDir, config).map((plugin) => createUnplugin(() => plugin).webpack());
|
||||
|
||||
|
@ -147,7 +156,10 @@ const getWebpackConfig: GetWebpackConfig = ({ rootDir, config, commandArgs = {}
|
|||
new MiniCssExtractPlugin({
|
||||
filename: '[name].css',
|
||||
}),
|
||||
new webpack.DefinePlugin(defineVariables),
|
||||
new webpack.DefinePlugin({
|
||||
...defineStaticVariables,
|
||||
...defineRuntimeVariables,
|
||||
}),
|
||||
new webpack.ProvidePlugin({ process: 'process/browser' }),
|
||||
new AssetsManifestPlugin({
|
||||
fileName: 'assets-manifest.json',
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
import * as path from 'path';
|
||||
import consola from 'consola';
|
||||
import type { EsbuildCompile } from '@ice/types/esm/plugin.js';
|
||||
import type { AppConfig } from './utils/runtimeEnv.js';
|
||||
|
||||
interface Options {
|
||||
esbuildCompile: EsbuildCompile;
|
||||
rootDir: string;
|
||||
}
|
||||
|
||||
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',
|
||||
external: ['./node_modules/*'],
|
||||
}, { isServer: true });
|
||||
|
||||
const appConfig = (await import(outfile)).default;
|
||||
consola.debug('app config:', appConfig);
|
||||
return appConfig;
|
||||
} catch (err) {
|
||||
consola.error('[ERROR]', 'Fail to analyze app config', err);
|
||||
}
|
||||
};
|
|
@ -1,4 +1,4 @@
|
|||
export const builtInPlugins = [
|
||||
'@ice/plugin-app',
|
||||
'@ice/plugin-auth',
|
||||
];
|
||||
];
|
||||
|
|
|
@ -11,6 +11,8 @@ import createWatch from './service/watchSource.js';
|
|||
import start from './commands/start.js';
|
||||
import build from './commands/build.js';
|
||||
import getContextConfig from './utils/getContextConfig.js';
|
||||
import { getAppConfig } from './analyzeRuntime.js';
|
||||
import { defineRuntimeEnv, updateRuntimeEnv } from './utils/runtimeEnv.js';
|
||||
import { generateRoutesInfo } from './routes.js';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
@ -28,7 +30,7 @@ async function createService({ rootDir, command, commandArgs, getBuiltInPlugins
|
|||
const tmpDirName = '.ice';
|
||||
const tmpDir = path.join(rootDir, tmpDirName);
|
||||
|
||||
const { routes, routesStr } = generateRoutesInfo(rootDir);
|
||||
const { routeManifest, routes, routesStr } = generateRoutesInfo(rootDir);
|
||||
const generator = new Generator({
|
||||
rootDir,
|
||||
targetDir: tmpDirName,
|
||||
|
@ -96,7 +98,8 @@ async function createService({ rootDir, command, commandArgs, getBuiltInPlugins
|
|||
const renderStart = new Date().getTime();
|
||||
generator.render();
|
||||
consola.debug('template render cost:', new Date().getTime() - renderStart);
|
||||
|
||||
// define runtime env before get webpack config
|
||||
defineRuntimeEnv();
|
||||
const contextConfig = getContextConfig(ctx);
|
||||
const webTask = contextConfig.find(({ name }) => name === 'web');
|
||||
const esbuildCompile = createEsbuildCompiler({
|
||||
|
@ -109,6 +112,8 @@ async function createService({ rootDir, command, commandArgs, getBuiltInPlugins
|
|||
if (command === 'start') {
|
||||
return await start(ctx, contextConfig, esbuildCompile);
|
||||
} else if (command === 'build') {
|
||||
const appConfig = await getAppConfig({ esbuildCompile, rootDir });
|
||||
updateRuntimeEnv(appConfig, routeManifest);
|
||||
return await build(ctx, contextConfig, esbuildCompile);
|
||||
}
|
||||
},
|
||||
|
|
|
@ -8,6 +8,7 @@ export function generateRoutesInfo(rootDir: string) {
|
|||
const str = generateNestRoutesStr(routes);
|
||||
|
||||
return {
|
||||
routeManifest,
|
||||
routesStr: `[${str}]`,
|
||||
routes,
|
||||
};
|
||||
|
|
|
@ -8,6 +8,7 @@ import consola from 'consola';
|
|||
|
||||
interface Options {
|
||||
parallel?: number;
|
||||
analyzeRelativeImport?: boolean;
|
||||
alias?: Alias;
|
||||
}
|
||||
|
||||
|
@ -79,7 +80,7 @@ export function getImportPath(
|
|||
}
|
||||
|
||||
export async function analyzeImports(files: string[], options: Options) {
|
||||
const { parallel, alias = {} } = options;
|
||||
const { parallel, analyzeRelativeImport, alias = {} } = options;
|
||||
const parallelNum = parallel ?? 10;
|
||||
const entries = [...files];
|
||||
const analyzedSet = new Set<string>();
|
||||
|
@ -113,7 +114,7 @@ export async function analyzeImports(files: string[], options: Options) {
|
|||
if (!importSet.has(importStr)) importSet.add(importStr);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
} else if (analyzeRelativeImport) {
|
||||
let importPath = importName;
|
||||
if (!path.isAbsolute(importPath)) {
|
||||
importPath = getImportPath(importPath, filePath, alias);
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
import type { RouteManifest } from '@ice/route-manifest';
|
||||
|
||||
export type AppConfig = Record<string, any>;
|
||||
|
||||
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 const updateRuntimeEnv = (appConfig?: AppConfig, routeManifest?: RouteManifest) => {
|
||||
if (!appConfig?.app?.getInitialData) {
|
||||
process.env['ICE_RUNTIME_INITIAL_DATA'] = 'false';
|
||||
}
|
||||
if (!appConfig?.app?.errorBoundary) {
|
||||
process.env['ICE_RUNTIME_ERROR_BOUNDARY'] = 'false';
|
||||
}
|
||||
if (routeManifest && Object.keys(routeManifest).length <= 1) {
|
||||
process.env['ICE_RUNTIME_ROUTER'] = 'false';
|
||||
}
|
||||
};
|
|
@ -45,7 +45,12 @@ describe('getImportPath', () => {
|
|||
describe('analyzeImports', () => {
|
||||
it('basic usage', async () => {
|
||||
const entryFile = path.join(__dirname, './fixtures/preAnalyze/app.ts');
|
||||
const analyzeSet = await analyzeImports([entryFile], { alias: {'@': path.join(__dirname, './fixtures/preAnalyze')}})
|
||||
const analyzeSet = await analyzeImports([entryFile], {
|
||||
analyzeRelativeImport: true,
|
||||
alias: {
|
||||
'@': path.join(__dirname, './fixtures/preAnalyze'),
|
||||
},
|
||||
});
|
||||
expect([...(analyzeSet || [])]).toStrictEqual(['runApp', 'request', 'store']);
|
||||
})
|
||||
});
|
|
@ -18,6 +18,7 @@
|
|||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2"
|
||||
},
|
||||
"sideEffects": false,
|
||||
"dependencies": {
|
||||
"history": "^5.3.0",
|
||||
"react-router-dom": "^6.2.2",
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
import { createSearchParams } from 'react-router-dom';
|
||||
import type { AppContext, InitialContext, AppConfig } from './types';
|
||||
|
||||
const getInitialData = async (appConfig: AppConfig): Promise<AppContext['initialData']> => {
|
||||
// ssr enabled and the server has returned data
|
||||
if ((window as any).__ICE_APP_DATA__) {
|
||||
return (window as any).__ICE_APP_DATA__;
|
||||
// context.pageInitialProps = (window as any).__ICE_PAGE_PROPS__;
|
||||
} else if (appConfig?.app?.getInitialData) {
|
||||
const { href, origin, pathname, search } = window.location;
|
||||
const path = href.replace(origin, '');
|
||||
const query = Object.fromEntries(createSearchParams(search));
|
||||
const ssrError = (window as any).__ICE_SSR_ERROR__;
|
||||
const initialContext: InitialContext = {
|
||||
pathname,
|
||||
path,
|
||||
query,
|
||||
ssrError,
|
||||
};
|
||||
return await appConfig.app.getInitialData(initialContext);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export default getInitialData;
|
|
@ -1,11 +1,11 @@
|
|||
import React, { useLayoutEffect, useReducer } from 'react';
|
||||
import type { Update } from 'history';
|
||||
import { createHashHistory, createBrowserHistory } from 'history';
|
||||
import { createSearchParams } from 'react-router-dom';
|
||||
import Runtime from './runtime.js';
|
||||
import App from './App.js';
|
||||
import type { AppContext, InitialContext, AppConfig, RouteItem } from './types';
|
||||
import type { AppContext, AppConfig, RouteItem } from './types';
|
||||
import { loadRouteModules, loadPageData, matchRoutes } from './routes.js';
|
||||
import getInitialData from './getInitialData.js';
|
||||
|
||||
export default async function runBrowserApp(
|
||||
appConfig: AppConfig,
|
||||
|
@ -21,6 +21,9 @@ export default async function runBrowserApp(
|
|||
routeModules,
|
||||
pageData,
|
||||
};
|
||||
if (process.env.ICE_RUNTIME_INITIAL_DATA) {
|
||||
appContext.initialData = await getInitialData(appConfig);
|
||||
}
|
||||
|
||||
const runtime = new Runtime(appContext);
|
||||
runtimeModules.forEach(m => {
|
||||
|
|
|
@ -122,6 +122,8 @@ export interface CommonJsRuntime {
|
|||
|
||||
export type GetWrapperPageRegistration = () => PageWrapper<any>[];
|
||||
|
||||
export type RuntimeModules = (RuntimePlugin | CommonJsRuntime)[];
|
||||
|
||||
export interface AppRouterProps {
|
||||
action: Action;
|
||||
location: Location;
|
||||
|
@ -152,4 +154,4 @@ export interface RouteMatch {
|
|||
* The route object that was used to match.
|
||||
*/
|
||||
route: RouteItem;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { buildFixture, setupBrowser } from '../utils/build';
|
||||
import { startFixture, setupStartBrowser } from '../utils/start';
|
||||
import { Page } from '../utils/browser';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const example = 'basic-project';
|
||||
|
||||
describe(`build ${example}`, () => {
|
||||
|
@ -15,6 +20,8 @@ describe(`build ${example}`, () => {
|
|||
page = res.page;
|
||||
browser = res.browser;
|
||||
expect(await page.$$text('h2')).toStrictEqual(['Home Page']);
|
||||
const bundleContent = fs.readFileSync(path.join(__dirname, `../../examples/${example}/build/main.js`), 'utf-8');
|
||||
expect(bundleContent.includes('__REMOVED__')).toBe(false);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
|
|
Loading…
Reference in New Issue