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:
ClarkXia 2022-03-28 18:04:44 +08:00
parent 75ea6fe113
commit 647996e63e
15 changed files with 154 additions and 11 deletions

View File

@ -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

View File

@ -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;
};

View File

@ -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',

View File

@ -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);
}
};

View File

@ -1,4 +1,4 @@
export const builtInPlugins = [
'@ice/plugin-app',
'@ice/plugin-auth',
];
];

View File

@ -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);
}
},

View File

@ -8,6 +8,7 @@ export function generateRoutesInfo(rootDir: string) {
const str = generateNestRoutesStr(routes);
return {
routeManifest,
routesStr: `[${str}]`,
routes,
};

View File

@ -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);

View File

@ -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';
}
};

View File

@ -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']);
})
});

View File

@ -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",

View File

@ -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;

View File

@ -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 => {

View File

@ -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;
}
}

View File

@ -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 () => {