in packages/ice/src/createService.ts [53:422]
async function createService({ rootDir, command, commandArgs }: CreateServiceOptions) {
const buildSpinner = createSpinner('loading config...');
const templateDir = path.join(__dirname, '../templates/');
const coreTemplate = path.join(templateDir, 'core/');
const configFile = commandArgs.config || 'ice.config.(mts|mjs|ts|js|cjs|json)';
const dataCache = new Map<string, string>();
const builtinPlugin = commandArgs.plugin as string;
const generator = new Generator({
// Directory of templates includes `core` and `exports`.
templateDir,
rootDir,
targetDir: RUNTIME_TMP_DIR,
// add default template of ice
templates: [coreTemplate],
});
const { addWatchEvent, removeWatchEvent } = createWatch({
watchDir: rootDir,
command,
});
let entryCode = 'render();';
const generatorAPI = {
addExport: (declarationData: DeclarationData) => {
generator.addDeclaration('framework', declarationData);
},
addExportTypes: (declarationData: DeclarationData) => {
generator.addDeclaration('frameworkTypes', declarationData);
},
addRuntimeOptions: (declarationData: DeclarationData) => {
generator.addDeclaration('runtimeOptions', declarationData);
},
removeRuntimeOptions: (removeSource: string | string[]) => {
generator.removeDeclaration('runtimeOptions', removeSource);
},
addRouteTypes: (declarationData: DeclarationData) => {
generator.addDeclaration('routeConfigTypes', declarationData);
},
addRenderFile: generator.addRenderFile,
addRenderTemplate: generator.addTemplateFiles,
addEntryCode: (callback: (originalCode: string) => string) => {
entryCode = callback(entryCode);
},
addEntryImportAhead: (declarationData: Pick<DeclarationData, 'source'>, type = 'client') => {
if (type === 'both' || type === 'server') {
generator.addDeclaration('entryServer', declarationData);
}
if (type === 'both' || type === 'client') {
generator.addDeclaration('entry', declarationData);
}
},
modifyRenderData: generator.modifyRenderData,
addDataLoaderImport: (declarationData: DeclarationData) => {
generator.addDeclaration('dataLoaderImport', declarationData);
},
getExportList: (registerKey: string) => {
return generator.getExportList(registerKey);
},
render: generator.render,
};
// Store server runner for plugins.
let serverRunner: ServerRunner;
const serverCompileTask = new ServerCompileTask();
async function excuteServerEntry() {
try {
if (serverRunner) {
return serverRunner.run(SERVER_ENTRY);
} else {
const { error, serverEntry } = await serverCompileTask.get();
if (error) {
logger.error('Server compile error:', error);
return;
}
delete require.cache[serverEntry];
return await dynamicImport(serverEntry, true);
}
} catch (error) {
// make error clearly, notice typeof err === 'string'
logger.error('Execute server entry error:', error);
return;
}
}
const { target = WEB } = commandArgs;
const plugins = [];
// Add default web plugin.
if (target === WEB) {
plugins.push(pluginWeb());
}
if (builtinPlugin) {
try {
const pluginModule = await dynamicImport(builtinPlugin.startsWith('.') ? path.join(rootDir, builtinPlugin) : builtinPlugin);
const plugin = pluginModule.default || pluginModule;
plugins.push(plugin());
} catch (err) {
logger.error(`Load builtin plugin error, Faild to import plugin "${builtinPlugin}".`);
throw err;
}
}
// Register framework level API.
RUNTIME_EXPORTS.forEach(exports => {
generatorAPI.addExport(exports);
});
const routeManifest = new RouteManifest();
const ctx = new Context<Config, ExtendsPluginAPI>({
rootDir,
command,
commandArgs,
configFile,
plugins,
extendsPluginAPI: {
generator: generatorAPI,
watch: {
addEvent: addWatchEvent,
removeEvent: removeWatchEvent,
},
getRouteManifest: () => routeManifest.getNestedRoute(),
getFlattenRoutes: () => routeManifest.getFlattenRoute(),
getRoutesFile: () => routeManifest.getRoutesFile(),
addRoutesDefinition: routeManifest.addRoutesDefinition.bind(routeManifest),
excuteServerEntry,
context: {
webpack,
},
serverCompileTask,
dataCache,
createLogger,
// Override registerTask to merge default config.
registerTask: (target: string, config: Partial<Config>) => {
// Merge task config with default config, so developer should not care about the config built-in of framework.
const defaultTaskConfig = getDefaultTaskConfig({ rootDir, command });
return ctx.registerTask(target, mergeConfig(defaultTaskConfig, config));
},
},
});
// Load .env before resolve user config, so we can access env variables defined in .env files.
await setEnv(rootDir, commandArgs);
// resolve userConfig from ice.config.ts before registerConfig
await ctx.resolveUserConfig();
// get plugins include built-in plugins and custom plugins
const resolvedPlugins = await ctx.resolvePlugins() as PluginData[];
const runtimeModules = getRuntimeModules(resolvedPlugins, rootDir);
const { getAppConfig, init: initAppConfigCompiler } = getAppExportConfig(rootDir);
const { getRoutesConfig, getDataloaderConfig, init: initRouteConfigCompiler } = getRouteExportConfig(rootDir);
// register config
['userConfig', 'cliOption'].forEach((configType) => {
// Support getDefaultValue for config, make easier for get default value in different mode.
const configData = config[configType].map(({ getDefaultValue, ...resetConfig }) => {
if (getDefaultValue && typeof getDefaultValue === 'function') {
return {
...resetConfig,
defaultValue: getDefaultValue(),
};
}
return resetConfig;
});
ctx.registerConfig(configType, configData);
});
let taskConfigs: TaskConfig<Config>[] = await ctx.setup();
// get userConfig after setup because of userConfig maybe modified by plugins
const { userConfig } = ctx;
const { routes: routesConfig, server, syntaxFeatures, polyfill } = userConfig;
const coreEnvKeys = getCoreEnvKeys();
const routesInfo = await generateRoutesInfo(rootDir, routesConfig, routeManifest.getRoutesDefinitions());
routeManifest.setRoutes(routesInfo.routes);
const hasExportAppData = (await getFileExports({ rootDir, file: 'src/app' })).includes('dataLoader');
const csr = !userConfig.ssr && !userConfig.ssg;
const disableRouter = (userConfig?.optimization?.router && routesInfo.routesCount <= 1) ||
userConfig?.optimization?.disableRouter;
if (disableRouter) {
logger.info('`optimization.router` is enabled, ice build will remove react-router and history which is unnecessary.');
taskConfigs = mergeTaskConfig(taskConfigs, {
alias: {
'@ice/runtime/router': '@ice/runtime/single-router',
},
});
} else {
// Only when router is enabled, we will add router polyfills.
addPolyfills(generatorAPI, userConfig.featurePolyfill, rootDir, command === 'start');
}
// Get first task config as default platform config.
const platformTaskConfig = taskConfigs[0];
const iceRuntimePath = '@ice/runtime';
// Only when code splitting use the default strategy or set to `router`, the router will be lazy loaded.
const lazy = [true, 'chunks', 'page', 'page-vendors'].includes(userConfig.codeSplitting);
const { routeImports, routeDefinition } = getRoutesDefinition({
manifest: routesInfo.routes,
lazy,
});
const loaderExports = hasExportAppData || Boolean(routesInfo.loaders);
const hasDataLoader = Boolean(userConfig.dataLoader) && loaderExports;
// add render data
generator.setRenderData({
...routesInfo,
target,
iceRuntimePath,
hasExportAppData,
runtimeModules,
coreEnvKeys,
// Stringify basename because `config` basename in task config only support type string.
basename: JSON.stringify(platformTaskConfig.config.basename || '/'),
memoryRouter: platformTaskConfig.config.memoryRouter,
hydrate: !csr,
importCoreJs: polyfill === 'entry',
// Enable react-router for web as default.
enableRoutes: true,
entryCode,
hasDocument: hasDocument(rootDir),
dataLoader: userConfig.dataLoader,
hasDataLoader,
routeImports,
routeDefinition,
routesFile: './routes',
});
dataCache.set('routes', JSON.stringify(routesInfo));
dataCache.set('hasExportAppData', hasExportAppData ? 'true' : '');
// Render exports files if route component export dataLoader / pageConfig.
renderExportsTemplate(
{
...routesInfo,
hasExportAppData,
},
generator.addRenderFile,
{
rootDir,
runtimeDir: RUNTIME_TMP_DIR,
templateDir: path.join(templateDir, 'exports'),
dataLoader: Boolean(userConfig.dataLoader),
},
);
if (platformTaskConfig.config.server?.fallbackEntry) {
// Add fallback entry for server side rendering.
generator.addRenderFile('core/entry.server.ts.ejs', FALLBACK_ENTRY, { hydrate: false });
}
if (typeof userConfig.dataLoader === 'object' && userConfig.dataLoader.fetcher) {
const {
packageName,
method,
} = userConfig.dataLoader.fetcher;
generatorAPI.addDataLoaderImport(method ? {
source: packageName,
alias: {
[method]: 'dataLoaderFetcher',
},
specifier: [method],
} : {
source: packageName,
specifier: '',
});
}
if (multipleServerEntry(userConfig, command)) {
renderMultiEntry({
generator,
renderRoutes: routeManifest.getFlattenRoute(),
routesManifest: routesInfo.routes,
lazy,
});
}
// render template before webpack compile
const renderStart = new Date().getTime();
generator.render();
logger.debug('template render cost:', new Date().getTime() - renderStart);
if (server.onDemand && command === 'start') {
serverRunner = new ServerRunner({
speedup: commandArgs.speedup,
rootDir,
task: platformTaskConfig,
server,
csr,
getRoutesFile: () => routeManifest.getRoutesFile(),
});
addWatchEvent([
// Files in .ice directory will update when routes changed.
/(src|.ice)\/?[\w*-:.$]+$/,
async (eventName: string, filePath: string) => {
if (eventName === 'change' || eventName === 'add') {
serverRunner.fileChanged(filePath);
}
}],
);
}
// create serverCompiler with task config
const serverCompiler = createServerCompiler({
rootDir,
task: platformTaskConfig,
command,
speedup: commandArgs.speedup,
server,
syntaxFeatures,
getRoutesFile: () => routeManifest.getRoutesFile(),
});
initAppConfigCompiler(serverCompiler);
initRouteConfigCompiler(serverCompiler);
addWatchEvent(
...getWatchEvents({
generator,
targetDir: RUNTIME_TMP_DIR,
templateDir: coreTemplate,
cache: dataCache,
routeManifest,
lazyRoutes: lazy,
ctx,
}),
);
const appConfig: AppConfig = (await getAppConfig()).default;
updateRuntimeEnv(appConfig, {
disableRouter,
// The optimization for runtime size should only be enabled in production mode.
routesConfig: command !== 'build' || routesInfo.routesExports.length > 0,
dataLoader: command !== 'build' || loaderExports,
});
return {
run: async () => {
const bundlerConfig = {
taskConfigs,
spinner: buildSpinner,
routeManifest,
appConfig,
hooksAPI: {
getAppConfig,
getRoutesConfig,
getDataloaderConfig,
serverRunner,
serverCompiler,
},
userConfig,
configFile,
hasDataLoader,
};
try {
if (command === 'test') {
return test(ctx, {
taskConfigs,
spinner: buildSpinner,
});
} else {
return commandArgs.speedup
? await rspackBundler(ctx, bundlerConfig)
: await webpackBundler(ctx, bundlerConfig);
}
} catch (error) {
buildSpinner.stop();
throw error;
}
},
};
}