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';
|
import { defineAppConfig } from 'ice';
|
||||||
|
|
||||||
|
if (process.env.ICE_RUNTIME_ERROR_BOUNDARY) {
|
||||||
|
console.log('__REMOVED__');
|
||||||
|
}
|
||||||
|
|
||||||
export default defineAppConfig({
|
export default defineAppConfig({
|
||||||
app: {
|
app: {
|
||||||
// @ts-expect-error loss tslib dependency
|
// @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 { Config } from '@ice/types';
|
||||||
import type { CommandArgs } from 'build-scripts';
|
import type { CommandArgs } from 'build-scripts';
|
||||||
import { createUnplugin } from 'unplugin';
|
import { createUnplugin } from 'unplugin';
|
||||||
|
import { getRuntimeEnvironment } from './clientEnv.js';
|
||||||
import AssetsManifestPlugin from './webpackPlugins/AssetsManifestPlugin.js';
|
import AssetsManifestPlugin from './webpackPlugins/AssetsManifestPlugin.js';
|
||||||
import getTransformPlugins from './unPlugins/index.js';
|
import getTransformPlugins from './unPlugins/index.js';
|
||||||
|
|
||||||
|
@ -42,11 +43,19 @@ const getWebpackConfig: GetWebpackConfig = ({ rootDir, config, commandArgs = {}
|
||||||
} = config;
|
} = config;
|
||||||
|
|
||||||
const dev = mode !== 'production';
|
const dev = mode !== 'production';
|
||||||
const defineVariables = {
|
const defineStaticVariables = {
|
||||||
'process.env.NODE_ENV': JSON.stringify(mode || 'development'),
|
'process.env.NODE_ENV': JSON.stringify(mode || 'development'),
|
||||||
'process.env.SERVER_PORT': JSON.stringify(commandArgs.port),
|
'process.env.SERVER_PORT': JSON.stringify(commandArgs.port),
|
||||||
'process.env.__IS_SERVER__': false,
|
'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
|
// create plugins
|
||||||
const webpackPlugins = getTransformPlugins(rootDir, config).map((plugin) => createUnplugin(() => plugin).webpack());
|
const webpackPlugins = getTransformPlugins(rootDir, config).map((plugin) => createUnplugin(() => plugin).webpack());
|
||||||
|
|
||||||
|
@ -147,7 +156,10 @@ const getWebpackConfig: GetWebpackConfig = ({ rootDir, config, commandArgs = {}
|
||||||
new MiniCssExtractPlugin({
|
new MiniCssExtractPlugin({
|
||||||
filename: '[name].css',
|
filename: '[name].css',
|
||||||
}),
|
}),
|
||||||
new webpack.DefinePlugin(defineVariables),
|
new webpack.DefinePlugin({
|
||||||
|
...defineStaticVariables,
|
||||||
|
...defineRuntimeVariables,
|
||||||
|
}),
|
||||||
new webpack.ProvidePlugin({ process: 'process/browser' }),
|
new webpack.ProvidePlugin({ process: 'process/browser' }),
|
||||||
new AssetsManifestPlugin({
|
new AssetsManifestPlugin({
|
||||||
fileName: 'assets-manifest.json',
|
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 = [
|
export const builtInPlugins = [
|
||||||
'@ice/plugin-app',
|
'@ice/plugin-app',
|
||||||
'@ice/plugin-auth',
|
'@ice/plugin-auth',
|
||||||
];
|
];
|
||||||
|
|
|
@ -11,6 +11,8 @@ import createWatch from './service/watchSource.js';
|
||||||
import start from './commands/start.js';
|
import start from './commands/start.js';
|
||||||
import build from './commands/build.js';
|
import build from './commands/build.js';
|
||||||
import getContextConfig from './utils/getContextConfig.js';
|
import getContextConfig from './utils/getContextConfig.js';
|
||||||
|
import { getAppConfig } from './analyzeRuntime.js';
|
||||||
|
import { defineRuntimeEnv, updateRuntimeEnv } from './utils/runtimeEnv.js';
|
||||||
import { generateRoutesInfo } from './routes.js';
|
import { generateRoutesInfo } from './routes.js';
|
||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
@ -28,7 +30,7 @@ async function createService({ rootDir, command, commandArgs, getBuiltInPlugins
|
||||||
const tmpDirName = '.ice';
|
const tmpDirName = '.ice';
|
||||||
const tmpDir = path.join(rootDir, tmpDirName);
|
const tmpDir = path.join(rootDir, tmpDirName);
|
||||||
|
|
||||||
const { routes, routesStr } = generateRoutesInfo(rootDir);
|
const { routeManifest, routes, routesStr } = generateRoutesInfo(rootDir);
|
||||||
const generator = new Generator({
|
const generator = new Generator({
|
||||||
rootDir,
|
rootDir,
|
||||||
targetDir: tmpDirName,
|
targetDir: tmpDirName,
|
||||||
|
@ -96,7 +98,8 @@ async function createService({ rootDir, command, commandArgs, getBuiltInPlugins
|
||||||
const renderStart = new Date().getTime();
|
const renderStart = new Date().getTime();
|
||||||
generator.render();
|
generator.render();
|
||||||
consola.debug('template render cost:', new Date().getTime() - renderStart);
|
consola.debug('template render cost:', new Date().getTime() - renderStart);
|
||||||
|
// define runtime env before get webpack config
|
||||||
|
defineRuntimeEnv();
|
||||||
const contextConfig = getContextConfig(ctx);
|
const contextConfig = getContextConfig(ctx);
|
||||||
const webTask = contextConfig.find(({ name }) => name === 'web');
|
const webTask = contextConfig.find(({ name }) => name === 'web');
|
||||||
const esbuildCompile = createEsbuildCompiler({
|
const esbuildCompile = createEsbuildCompiler({
|
||||||
|
@ -109,6 +112,8 @@ async function createService({ rootDir, command, commandArgs, getBuiltInPlugins
|
||||||
if (command === 'start') {
|
if (command === 'start') {
|
||||||
return await start(ctx, contextConfig, esbuildCompile);
|
return await start(ctx, contextConfig, esbuildCompile);
|
||||||
} else if (command === 'build') {
|
} else if (command === 'build') {
|
||||||
|
const appConfig = await getAppConfig({ esbuildCompile, rootDir });
|
||||||
|
updateRuntimeEnv(appConfig, routeManifest);
|
||||||
return await build(ctx, contextConfig, esbuildCompile);
|
return await build(ctx, contextConfig, esbuildCompile);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -8,6 +8,7 @@ export function generateRoutesInfo(rootDir: string) {
|
||||||
const str = generateNestRoutesStr(routes);
|
const str = generateNestRoutesStr(routes);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
routeManifest,
|
||||||
routesStr: `[${str}]`,
|
routesStr: `[${str}]`,
|
||||||
routes,
|
routes,
|
||||||
};
|
};
|
||||||
|
|
|
@ -8,6 +8,7 @@ import consola from 'consola';
|
||||||
|
|
||||||
interface Options {
|
interface Options {
|
||||||
parallel?: number;
|
parallel?: number;
|
||||||
|
analyzeRelativeImport?: boolean;
|
||||||
alias?: Alias;
|
alias?: Alias;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,7 +80,7 @@ export function getImportPath(
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function analyzeImports(files: string[], options: Options) {
|
export async function analyzeImports(files: string[], options: Options) {
|
||||||
const { parallel, alias = {} } = options;
|
const { parallel, analyzeRelativeImport, alias = {} } = options;
|
||||||
const parallelNum = parallel ?? 10;
|
const parallelNum = parallel ?? 10;
|
||||||
const entries = [...files];
|
const entries = [...files];
|
||||||
const analyzedSet = new Set<string>();
|
const analyzedSet = new Set<string>();
|
||||||
|
@ -113,7 +114,7 @@ export async function analyzeImports(files: string[], options: Options) {
|
||||||
if (!importSet.has(importStr)) importSet.add(importStr);
|
if (!importSet.has(importStr)) importSet.add(importStr);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else if (analyzeRelativeImport) {
|
||||||
let importPath = importName;
|
let importPath = importName;
|
||||||
if (!path.isAbsolute(importPath)) {
|
if (!path.isAbsolute(importPath)) {
|
||||||
importPath = getImportPath(importPath, filePath, alias);
|
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', () => {
|
describe('analyzeImports', () => {
|
||||||
it('basic usage', async () => {
|
it('basic usage', async () => {
|
||||||
const entryFile = path.join(__dirname, './fixtures/preAnalyze/app.ts');
|
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']);
|
expect([...(analyzeSet || [])]).toStrictEqual(['runApp', 'request', 'store']);
|
||||||
})
|
})
|
||||||
});
|
});
|
|
@ -18,6 +18,7 @@
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2"
|
"react-dom": "^17.0.2"
|
||||||
},
|
},
|
||||||
|
"sideEffects": false,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"history": "^5.3.0",
|
"history": "^5.3.0",
|
||||||
"react-router-dom": "^6.2.2",
|
"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 React, { useLayoutEffect, useReducer } from 'react';
|
||||||
import type { Update } from 'history';
|
import type { Update } from 'history';
|
||||||
import { createHashHistory, createBrowserHistory } from 'history';
|
import { createHashHistory, createBrowserHistory } from 'history';
|
||||||
import { createSearchParams } from 'react-router-dom';
|
|
||||||
import Runtime from './runtime.js';
|
import Runtime from './runtime.js';
|
||||||
import App from './App.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 { loadRouteModules, loadPageData, matchRoutes } from './routes.js';
|
||||||
|
import getInitialData from './getInitialData.js';
|
||||||
|
|
||||||
export default async function runBrowserApp(
|
export default async function runBrowserApp(
|
||||||
appConfig: AppConfig,
|
appConfig: AppConfig,
|
||||||
|
@ -21,6 +21,9 @@ export default async function runBrowserApp(
|
||||||
routeModules,
|
routeModules,
|
||||||
pageData,
|
pageData,
|
||||||
};
|
};
|
||||||
|
if (process.env.ICE_RUNTIME_INITIAL_DATA) {
|
||||||
|
appContext.initialData = await getInitialData(appConfig);
|
||||||
|
}
|
||||||
|
|
||||||
const runtime = new Runtime(appContext);
|
const runtime = new Runtime(appContext);
|
||||||
runtimeModules.forEach(m => {
|
runtimeModules.forEach(m => {
|
||||||
|
|
|
@ -122,6 +122,8 @@ export interface CommonJsRuntime {
|
||||||
|
|
||||||
export type GetWrapperPageRegistration = () => PageWrapper<any>[];
|
export type GetWrapperPageRegistration = () => PageWrapper<any>[];
|
||||||
|
|
||||||
|
export type RuntimeModules = (RuntimePlugin | CommonJsRuntime)[];
|
||||||
|
|
||||||
export interface AppRouterProps {
|
export interface AppRouterProps {
|
||||||
action: Action;
|
action: Action;
|
||||||
location: Location;
|
location: Location;
|
||||||
|
@ -152,4 +154,4 @@ export interface RouteMatch {
|
||||||
* The route object that was used to match.
|
* The route object that was used to match.
|
||||||
*/
|
*/
|
||||||
route: RouteItem;
|
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 { buildFixture, setupBrowser } from '../utils/build';
|
||||||
import { startFixture, setupStartBrowser } from '../utils/start';
|
import { startFixture, setupStartBrowser } from '../utils/start';
|
||||||
import { Page } from '../utils/browser';
|
import { Page } from '../utils/browser';
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
const example = 'basic-project';
|
const example = 'basic-project';
|
||||||
|
|
||||||
describe(`build ${example}`, () => {
|
describe(`build ${example}`, () => {
|
||||||
|
@ -15,6 +20,8 @@ describe(`build ${example}`, () => {
|
||||||
page = res.page;
|
page = res.page;
|
||||||
browser = res.browser;
|
browser = res.browser;
|
||||||
expect(await page.$$text('h2')).toStrictEqual(['Home Page']);
|
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 () => {
|
afterAll(async () => {
|
||||||
|
|
Loading…
Reference in New Issue