From cd41e38071e4bff182736d61d61580bcc2427594 Mon Sep 17 00:00:00 2001 From: ClarkXia Date: Mon, 20 Jan 2025 18:15:45 +0800 Subject: [PATCH] feat: support custom runtime --- examples/custom-runtime/.browserslistrc | 1 + examples/custom-runtime/ice.config.mts | 31 +++ examples/custom-runtime/package.json | 23 +++ examples/custom-runtime/runtime.tsx | 15 ++ examples/custom-runtime/runtimeServer.tsx | 0 examples/custom-runtime/src/app.tsx | 5 + examples/custom-runtime/src/document.tsx | 22 ++ examples/custom-runtime/src/pages/home.tsx | 3 + examples/custom-runtime/src/pages/index.tsx | 3 + examples/custom-runtime/src/typings.d.ts | 1 + examples/custom-runtime/tsconfig.json | 32 +++ packages/ice/src/createService.ts | 189 ++++++------------ packages/ice/src/getWatchEvents.ts | 25 ++- packages/ice/src/plugins/task.ts | 14 +- packages/ice/src/service/generatorAPI.ts | 73 +++++++ packages/ice/src/service/renderTemplate.ts | 100 +++++++++ packages/ice/src/utils/multipleEntry.ts | 10 +- .../ice/src/utils/renderExportsTemplate.ts | 5 +- .../ice/templates/core/entry.client.tsx.ejs | 6 +- .../ice/templates/core/entry.server.ts.ejs | 8 +- packages/plugin-miniapp/src/index.ts | 2 +- packages/runtime-kit/package.json | 2 +- packages/runtime-kit/src/appConfig.ts | 2 +- packages/runtime/src/runClientApp.tsx | 1 - packages/shared-config/package.json | 3 +- packages/shared-config/src/types.ts | 27 ++- pnpm-lock.yaml | 28 +++ 27 files changed, 467 insertions(+), 164 deletions(-) create mode 100644 examples/custom-runtime/.browserslistrc create mode 100644 examples/custom-runtime/ice.config.mts create mode 100644 examples/custom-runtime/package.json create mode 100644 examples/custom-runtime/runtime.tsx create mode 100644 examples/custom-runtime/runtimeServer.tsx create mode 100644 examples/custom-runtime/src/app.tsx create mode 100644 examples/custom-runtime/src/document.tsx create mode 100644 examples/custom-runtime/src/pages/home.tsx create mode 100644 examples/custom-runtime/src/pages/index.tsx create mode 100644 examples/custom-runtime/src/typings.d.ts create mode 100644 examples/custom-runtime/tsconfig.json create mode 100644 packages/ice/src/service/generatorAPI.ts create mode 100644 packages/ice/src/service/renderTemplate.ts diff --git a/examples/custom-runtime/.browserslistrc b/examples/custom-runtime/.browserslistrc new file mode 100644 index 0000000000..7637baddc3 --- /dev/null +++ b/examples/custom-runtime/.browserslistrc @@ -0,0 +1 @@ +chrome 55 \ No newline at end of file diff --git a/examples/custom-runtime/ice.config.mts b/examples/custom-runtime/ice.config.mts new file mode 100644 index 0000000000..26d2dd805b --- /dev/null +++ b/examples/custom-runtime/ice.config.mts @@ -0,0 +1,31 @@ +import { defineConfig } from '@ice/app'; + +export default defineConfig(() => ({ + ssg: false, + plugins: [ + { + name: 'custom-runtime', + setup: (api) => { + // Customize the runtime + api.onGetConfig((config) => { + // Override the runtime config + config.runtime = { + exports: [ + { + specifier: ['Meta', 'Title', 'Links', 'Main', 'Scripts'], + source: '@ice/runtime', + }, + { + specifier: ['defineAppConfig'], + source: '@ice/runtime-kit', + }, + ], + source: '../runtime', + server: '@ice/runtime/server', + }; + }) + }, + }, + ], +})); + diff --git a/examples/custom-runtime/package.json b/examples/custom-runtime/package.json new file mode 100644 index 0000000000..5f3de1f9b0 --- /dev/null +++ b/examples/custom-runtime/package.json @@ -0,0 +1,23 @@ +{ + "name": "@examples/custom-runtime", + "version": "1.0.0", + "private": true, + "scripts": { + "start": "ice start", + "build": "ice build" + }, + "description": "", + "author": "", + "license": "MIT", + "dependencies": { + "@ice/app": "workspace:*", + "@ice/runtime": "workspace:*", + "@ice/runtime-kit": "workspace:*", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.2" + } +} diff --git a/examples/custom-runtime/runtime.tsx b/examples/custom-runtime/runtime.tsx new file mode 100644 index 0000000000..49f495de95 --- /dev/null +++ b/examples/custom-runtime/runtime.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import type { RunClientAppOptions } from '@ice/runtime-kit'; +import { getAppConfig } from '@ice/runtime-kit'; + +import ReactDOM from 'react-dom'; + +const runClientApp = (options: RunClientAppOptions) => { + console.log('runClientApp', options); + ReactDOM.render(
Hello World
, document.getElementById('ice-container')); +}; + +export { + getAppConfig, + runClientApp, +}; diff --git a/examples/custom-runtime/runtimeServer.tsx b/examples/custom-runtime/runtimeServer.tsx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/custom-runtime/src/app.tsx b/examples/custom-runtime/src/app.tsx new file mode 100644 index 0000000000..4e915ff399 --- /dev/null +++ b/examples/custom-runtime/src/app.tsx @@ -0,0 +1,5 @@ +import { defineAppConfig } from 'ice'; + +export default defineAppConfig(() => ({ + +})); diff --git a/examples/custom-runtime/src/document.tsx b/examples/custom-runtime/src/document.tsx new file mode 100644 index 0000000000..1e7b99c49d --- /dev/null +++ b/examples/custom-runtime/src/document.tsx @@ -0,0 +1,22 @@ +import { Meta, Title, Links, Main, Scripts } from 'ice'; + +function Document() { + return ( + + + + + + + + <Links /> + </head> + <body> + <Main /> + <Scripts /> + </body> + </html> + ); +} + +export default Document; diff --git a/examples/custom-runtime/src/pages/home.tsx b/examples/custom-runtime/src/pages/home.tsx new file mode 100644 index 0000000000..bb72815361 --- /dev/null +++ b/examples/custom-runtime/src/pages/home.tsx @@ -0,0 +1,3 @@ +export default function Home() { + return <h1>home</h1>; +} diff --git a/examples/custom-runtime/src/pages/index.tsx b/examples/custom-runtime/src/pages/index.tsx new file mode 100644 index 0000000000..5b3753e70e --- /dev/null +++ b/examples/custom-runtime/src/pages/index.tsx @@ -0,0 +1,3 @@ +export default function Index() { + return <h1>index</h1>; +} diff --git a/examples/custom-runtime/src/typings.d.ts b/examples/custom-runtime/src/typings.d.ts new file mode 100644 index 0000000000..1f6ba4ffa6 --- /dev/null +++ b/examples/custom-runtime/src/typings.d.ts @@ -0,0 +1 @@ +/// <reference types="@ice/app/types" /> diff --git a/examples/custom-runtime/tsconfig.json b/examples/custom-runtime/tsconfig.json new file mode 100644 index 0000000000..26fd9ec799 --- /dev/null +++ b/examples/custom-runtime/tsconfig.json @@ -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": ["build", "public"] +} \ No newline at end of file diff --git a/packages/ice/src/createService.ts b/packages/ice/src/createService.ts index 818770e0c5..d42ad23127 100644 --- a/packages/ice/src/createService.ts +++ b/packages/ice/src/createService.ts @@ -1,45 +1,44 @@ // hijack webpack before import other modules import './requireHook.js'; +import { createRequire } from 'module'; import * as path from 'path'; import { fileURLToPath } from 'url'; -import { createRequire } from 'module'; +import webpack from '@ice/bundles/compiled/webpack/index.js'; import { Context } from 'build-scripts'; import type { CommandArgs, CommandName, TaskConfig } from 'build-scripts'; import type { Config } from '@ice/shared-config/types'; import type { AppConfig } from '@ice/runtime/types'; -import webpack from '@ice/bundles/compiled/webpack/index.js'; +import * as config from './config.js'; +import test from './commands/test.js'; +import webpackBundler from './bundler/webpack/index.js'; +import rspackBundler from './bundler/rspack/index.js'; +import { RUNTIME_TMP_DIR, WEB, RUNTIME_EXPORTS, SERVER_ENTRY } from './constant.js'; +import getWatchEvents from './getWatchEvents.js'; +import pluginWeb from './plugins/web/index.js'; +import getDefaultTaskConfig from './plugins/task.js'; +import { getFileExports } from './service/analyze.js'; +import { getAppExportConfig, getRouteExportConfig } from './service/config.js'; +import Generator from './service/runtimeGenerator.js'; +import ServerRunner from './service/ServerRunner.js'; +import { createServerCompiler } from './service/serverCompiler.js'; +import createWatch from './service/watchSource.js'; import type { - DeclarationData, PluginData, ExtendsPluginAPI, } from './types/index.js'; -import Generator from './service/runtimeGenerator.js'; -import { createServerCompiler } from './service/serverCompiler.js'; -import createWatch from './service/watchSource.js'; -import pluginWeb from './plugins/web/index.js'; -import test from './commands/test.js'; -import getWatchEvents from './getWatchEvents.js'; -import { setEnv, updateRuntimeEnv, getCoreEnvKeys } from './utils/runtimeEnv.js'; -import getRuntimeModules from './utils/getRuntimeModules.js'; -import { generateRoutesInfo, getRoutesDefinition } from './routes.js'; -import * as config from './config.js'; -import { RUNTIME_TMP_DIR, WEB, RUNTIME_EXPORTS, SERVER_ENTRY, FALLBACK_ENTRY } from './constant.js'; +import addPolyfills from './utils/runtimePolyfill.js'; import createSpinner from './utils/createSpinner.js'; -import ServerCompileTask from './utils/ServerCompileTask.js'; -import { getAppExportConfig, getRouteExportConfig } from './service/config.js'; -import renderExportsTemplate from './utils/renderExportsTemplate.js'; -import { getFileExports } from './service/analyze.js'; -import { logger, createLogger } from './utils/logger.js'; -import ServerRunner from './service/ServerRunner.js'; -import RouteManifest from './utils/routeManifest.js'; import dynamicImport from './utils/dynamicImport.js'; -import mergeTaskConfig, { mergeConfig } from './utils/mergeTaskConfig.js'; -import addPolyfills from './utils/runtimePolyfill.js'; -import webpackBundler from './bundler/webpack/index.js'; -import rspackBundler from './bundler/rspack/index.js'; -import getDefaultTaskConfig from './plugins/task.js'; -import { multipleServerEntry, renderMultiEntry } from './utils/multipleEntry.js'; +import getRuntimeModules from './utils/getRuntimeModules.js'; import hasDocument from './utils/hasDocument.js'; +import { logger, createLogger } from './utils/logger.js'; +import mergeTaskConfig, { mergeConfig } from './utils/mergeTaskConfig.js'; +import RouteManifest from './utils/routeManifest.js'; +import { setEnv, updateRuntimeEnv, getCoreEnvKeys } from './utils/runtimeEnv.js'; +import ServerCompileTask from './utils/ServerCompileTask.js'; +import { generateRoutesInfo } from './routes.js'; +import GeneratorAPI from './service/generatorAPI.js'; +import renderTemplate from './service/renderTemplate.js'; const require = createRequire(import.meta.url); const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -71,46 +70,7 @@ async function createService({ rootDir, command, commandArgs }: CreateServiceOpt 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, - }; + const generatorAPI = new GeneratorAPI(generator); // Store server runner for plugins. let serverRunner: ServerRunner; const serverCompileTask = new ServerCompileTask(); @@ -152,10 +112,7 @@ async function createService({ rootDir, command, commandArgs }: CreateServiceOpt throw err; } } - // Register framework level API. - RUNTIME_EXPORTS.forEach(exports => { - generatorAPI.addExport(exports); - }); + const routeManifest = new RouteManifest(); const ctx = new Context<Config, ExtendsPluginAPI>({ rootDir, @@ -246,17 +203,25 @@ async function createService({ rootDir, command, commandArgs }: CreateServiceOpt // Get first task config as default platform config. const platformTaskConfig = taskConfigs[0]; - const iceRuntimePath = platformTaskConfig.config.runtimeSource || '@ice/runtime'; + const runtimeConfig = platformTaskConfig.config?.runtime; + const iceRuntimePath = runtimeConfig?.source || '@ice/runtime'; + const runtimeExports = runtimeConfig?.exports || RUNTIME_EXPORTS; // 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({ + const runtimeRouter = runtimeConfig?.router; + const { routeImports, routeDefinition } = runtimeRouter?.routesDefinition?.({ manifest: routesInfo.routes, lazy, - }); + }) || { + routeImports: [], + routeDefinition: '', + }; + + const routesFile = runtimeRouter?.source; + const loaderExports = hasExportAppData || Boolean(routesInfo.loaders); const hasDataLoader = Boolean(userConfig.dataLoader) && loaderExports; - // add render data - generator.setRenderData({ + const renderData = { ...routesInfo, target, iceRuntimePath, @@ -268,70 +233,31 @@ async function createService({ rootDir, command, commandArgs }: CreateServiceOpt memoryRouter: platformTaskConfig.config.memoryRouter, hydrate: !csr, importCoreJs: polyfill === 'entry', - // Enable react-router for web as default. - enableRoutes: true, - entryCode, + entryCode: generatorAPI.getEntryCode(), hasDocument: hasDocument(rootDir), dataLoader: userConfig.dataLoader, hasDataLoader, routeImports, routeDefinition, - routesFile: './routes', - }); + routesFile: routesFile?.replace(/\.[^.]+$/, ''), + lazy, + runtimeServer: runtimeConfig?.server, + }; 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 to runtime directory. + renderTemplate({ + ctx, + taskConfig: platformTaskConfig, + routeManifest, + generator, + generatorAPI, + renderData, + runtimeExports, + templateDir, + }); - // 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, @@ -373,6 +299,7 @@ async function createService({ rootDir, command, commandArgs }: CreateServiceOpt routeManifest, lazyRoutes: lazy, ctx, + router: runtimeRouter, }), ); diff --git a/packages/ice/src/getWatchEvents.ts b/packages/ice/src/getWatchEvents.ts index ec697bccfe..728cf5e250 100644 --- a/packages/ice/src/getWatchEvents.ts +++ b/packages/ice/src/getWatchEvents.ts @@ -2,7 +2,7 @@ import * as path from 'path'; import type { Context } from 'build-scripts'; import type { Config } from '@ice/shared-config/types'; import type { WatchEvent } from './types/plugin.js'; -import { generateRoutesInfo, getRoutesDefinition } from './routes.js'; +import { generateRoutesInfo } from './routes.js'; import type Generator from './service/runtimeGenerator'; import getGlobalStyleGlobPattern from './utils/getGlobalStyleGlobPattern.js'; import renderExportsTemplate from './utils/renderExportsTemplate.js'; @@ -20,31 +20,38 @@ interface Options { ctx: Context<Config>; routeManifest: RouteManifest; lazyRoutes: boolean; + router: { + source?: string; + template?: string; + routesDefinition?: Config['runtime']['router']['routesDefinition']; + }; } const getWatchEvents = (options: Options): WatchEvent[] => { - const { generator, targetDir, templateDir, cache, ctx, routeManifest, lazyRoutes } = options; + const { generator, targetDir, templateDir, cache, ctx, routeManifest, lazyRoutes, router } = options; const { userConfig: { routes: routesConfig, dataLoader }, configFile, rootDir } = ctx; const watchRoutes: WatchEvent = [ /src\/pages\/?[\w*-:.$]+$/, async (eventName: string) => { if (eventName === 'add' || eventName === 'unlink' || eventName === 'change') { const routesRenderData = await generateRoutesInfo(rootDir, routesConfig); - const { routeImports, routeDefinition } = getRoutesDefinition({ + const { routeImports, routeDefinition } = router?.routesDefinition?.({ manifest: routesRenderData.routes, lazy: lazyRoutes, - }); + }) || {}; const stringifiedData = JSON.stringify(routesRenderData); if (cache.get('routes') !== stringifiedData) { cache.set('routes', stringifiedData); logger.debug(`routes data regenerated: ${stringifiedData}`); if (eventName !== 'change') { // Specify the route files to re-render. - generator.renderFile( - path.join(templateDir, 'routes.tsx.ejs'), - path.join(rootDir, targetDir, 'routes.tsx'), - { routeImports, routeDefinition }, - ); + if (router.source && router.template) { + generator.renderFile( + router.template, + router.source, + { routeImports, routeDefinition }, + ); + } // Keep generate route manifest for avoid breaking change. generator.renderFile( path.join(templateDir, 'route-manifest.json.ejs'), diff --git a/packages/ice/src/plugins/task.ts b/packages/ice/src/plugins/task.ts index 892d146a90..c829d9744c 100644 --- a/packages/ice/src/plugins/task.ts +++ b/packages/ice/src/plugins/task.ts @@ -1,7 +1,8 @@ import * as path from 'path'; import { createRequire } from 'module'; import type { Config } from '@ice/shared-config/types'; -import { CACHE_DIR, RUNTIME_TMP_DIR } from '../constant.js'; +import { CACHE_DIR, RUNTIME_TMP_DIR, RUNTIME_EXPORTS } from '../constant.js'; +import { getRoutesDefinition } from '../routes.js'; const require = createRequire(import.meta.url); const getDefaultTaskConfig = ({ rootDir, command }): Config => { @@ -33,7 +34,16 @@ const getDefaultTaskConfig = ({ rootDir, command }): Config => { logging: process.env.WEBPACK_LOGGING || defaultLogging, minify: command === 'build', useDevServer: true, - runtimeSource: '@ice/runtime', + runtime: { + exports: RUNTIME_EXPORTS, + source: '@ice/runtime', + server: '@ice/runtime/server', + router: { + routesDefinition: getRoutesDefinition, + source: './routes.tsx', + template: 'core/routes.tsx.ejs', + }, + }, }; }; diff --git a/packages/ice/src/service/generatorAPI.ts b/packages/ice/src/service/generatorAPI.ts new file mode 100644 index 0000000000..761b7399eb --- /dev/null +++ b/packages/ice/src/service/generatorAPI.ts @@ -0,0 +1,73 @@ +import type { DeclarationData } from '../types/index.js'; +import type Generator from './runtimeGenerator.js'; + +class GeneratorAPI { + private readonly generator: Generator; + private entryCode: string; + constructor(generator: Generator) { + this.generator = generator; + this.entryCode = 'render();'; + } + addExport = (declarationData: DeclarationData): void => { + this.generator.addDeclaration('framework', declarationData); + }; + + addExportTypes = (declarationData: DeclarationData): void => { + this.generator.addDeclaration('frameworkTypes', declarationData); + }; + + addRuntimeOptions = (declarationData: DeclarationData): void => { + this.generator.addDeclaration('runtimeOptions', declarationData); + }; + + removeRuntimeOptions = (removeSource: string | string[]): void => { + this.generator.removeDeclaration('runtimeOptions', removeSource); + }; + + addRouteTypes = (declarationData: DeclarationData): void => { + this.generator.addDeclaration('routeConfigTypes', declarationData); + }; + + addEntryCode = (callback: (originalCode: string) => string): void => { + this.entryCode = callback(this.entryCode); + }; + + addEntryImportAhead = (declarationData: Pick<DeclarationData, 'source'>, type = 'client'): void => { + if (type === 'both' || type === 'server') { + this.generator.addDeclaration('entryServer', declarationData); + } + if (type === 'both' || type === 'client') { + this.generator.addDeclaration('entry', declarationData); + } + }; + + addRenderFile = (...args: Parameters<Generator['addRenderFile']>): ReturnType<Generator['addRenderFile']> => { + return this.generator.addRenderFile(...args); + }; + + addRenderTemplate = (...args: Parameters<Generator['addTemplateFiles']>): ReturnType<Generator['addTemplateFiles']> => { + return this.generator.addTemplateFiles(...args); + }; + + modifyRenderData = (...args: Parameters<Generator['modifyRenderData']>): ReturnType<Generator['modifyRenderData']> => { + return this.generator.modifyRenderData(...args); + }; + + addDataLoaderImport = (declarationData: DeclarationData): void => { + this.generator.addDeclaration('dataLoaderImport', declarationData); + }; + + getExportList = (registerKey: string) => { + return this.generator.getExportList(registerKey); + }; + + render = (): void => { + this.generator.render(); + }; + + getEntryCode = (): string => { + return this.entryCode; + }; +} + +export default GeneratorAPI; diff --git a/packages/ice/src/service/renderTemplate.ts b/packages/ice/src/service/renderTemplate.ts new file mode 100644 index 0000000000..5f35d9481a --- /dev/null +++ b/packages/ice/src/service/renderTemplate.ts @@ -0,0 +1,100 @@ +import path from 'path'; +import type { Context, TaskConfig } from 'build-scripts'; +import type { Config } from '@ice/shared-config/types'; +import { FALLBACK_ENTRY, RUNTIME_TMP_DIR } from '../constant.js'; +import type { RenderData } from '../types/generator.js'; +import type { ExtendsPluginAPI } from '../types/plugin.js'; +import renderExportsTemplate from '../utils/renderExportsTemplate.js'; +import { logger } from '../utils/logger.js'; +import { multipleServerEntry, renderMultiEntry } from '../utils/multipleEntry.js'; +import type RouteManifest from '../utils/routeManifest.js'; +import type GeneratorAPI from './generatorAPI.js'; +import type Generator from './runtimeGenerator.js'; + +interface RenderTemplateOptions { + ctx: Context<Config, ExtendsPluginAPI>; + taskConfig: TaskConfig<Config>; + routeManifest: RouteManifest; + generator: Generator; + generatorAPI: GeneratorAPI; + renderData: RenderData; + runtimeExports: Config['runtime']['exports']; + templateDir: string; +} + +function renderTemplate({ + ctx, + taskConfig, + routeManifest, + generator, + generatorAPI, + renderData, + runtimeExports, + templateDir, +}: RenderTemplateOptions): void { + // Record start time for performance tracking. + const renderStart = performance.now(); + + const { rootDir, userConfig, command } = ctx; + generator.setRenderData(renderData); + + // Register framework level exports. + runtimeExports.forEach(generatorAPI.addExport); + + // Render exports for routes with dataLoader/pageConfig. + renderExportsTemplate( + renderData, + generator.addRenderFile, + { + rootDir, + runtimeDir: RUNTIME_TMP_DIR, + templateDir: path.join(templateDir, 'exports'), + dataLoader: Boolean(userConfig.dataLoader), + }, + ); + + // Handle server-side fallback entry. + if (taskConfig.config.server?.fallbackEntry) { + generator.addRenderFile('core/entry.server.ts.ejs', FALLBACK_ENTRY, { hydrate: false }); + } + + // Handle custom router template. + const customRouter = taskConfig.config.runtime?.router; + + if (customRouter?.source && customRouter?.template) { + generator.addRenderFile(customRouter.template, customRouter.source); + } + + // Configure data loader if specified. + const dataLoaderFetcher = userConfig.dataLoader?.fetcher; + if (typeof userConfig.dataLoader === 'object' && dataLoaderFetcher) { + const { packageName, method } = dataLoaderFetcher; + + const importConfig = method ? { + source: packageName, + alias: { [method]: 'dataLoaderFetcher' }, + specifier: [method], + } : { + source: packageName, + specifier: '', + }; + + generatorAPI.addDataLoaderImport(importConfig); + } + + // Handle multiple server entries. + if (multipleServerEntry(userConfig, command)) { + renderMultiEntry({ + generator, + renderRoutes: routeManifest.getFlattenRoute(), + routesManifest: routeManifest.getNestedRoute(), + lazy: renderData.lazy, + }); + } + + generator.render(); + logger.debug('template render cost:', performance.now() - renderStart); +} + + +export default renderTemplate; diff --git a/packages/ice/src/utils/multipleEntry.ts b/packages/ice/src/utils/multipleEntry.ts index 31b7ce631d..8804bb3279 100644 --- a/packages/ice/src/utils/multipleEntry.ts +++ b/packages/ice/src/utils/multipleEntry.ts @@ -1,16 +1,16 @@ import matchRoutes from '@ice/runtime/matchRoutes'; import type { NestedRouteManifest } from '@ice/route-manifest'; import type { CommandName } from 'build-scripts'; -import { getRoutesDefinition } from '../routes.js'; +import type { Config } from '@ice/shared-config/types'; import type Generator from '../service/runtimeGenerator.js'; import type { UserConfig } from '../types/userConfig.js'; import { escapeRoutePath } from './generateEntry.js'; - interface Options { renderRoutes: string[]; routesManifest: NestedRouteManifest[]; generator: Generator; lazy: boolean; + routesDefinition?: Config['runtime']['router']['routesDefinition']; } export const multipleServerEntry = (userConfig: UserConfig, command: CommandName): boolean => { @@ -29,7 +29,7 @@ export const formatServerEntry = (route: string) => { }; export function renderMultiEntry(options: Options) { - const { renderRoutes, routesManifest, generator, lazy } = options; + const { renderRoutes, routesManifest, generator, lazy, routesDefinition } = options; renderRoutes.forEach((route) => { const routeId = formatRoutePath(route); generator.addRenderFile( @@ -41,13 +41,13 @@ export function renderMultiEntry(options: Options) { ); // Generate route file for each route. const matches = matchRoutes(routesManifest, route); - const { routeImports, routeDefinition } = getRoutesDefinition({ + const { routeImports, routeDefinition } = routesDefinition?.({ manifest: routesManifest, lazy, matchRoute: (routeItem) => { return matches.some((match) => match.route.id === routeItem.id); }, - }); + }) || {}; generator.addRenderFile('core/routes.tsx.ejs', `routes.${routeId}.tsx`, { routeImports, routeDefinition, diff --git a/packages/ice/src/utils/renderExportsTemplate.ts b/packages/ice/src/utils/renderExportsTemplate.ts index a02d20bb46..d4ed339d23 100644 --- a/packages/ice/src/utils/renderExportsTemplate.ts +++ b/packages/ice/src/utils/renderExportsTemplate.ts @@ -1,10 +1,7 @@ import * as path from 'path'; import fse from 'fs-extra'; import type Generator from '../service/runtimeGenerator.js'; - -type RenderData = { - loaders: string; -} & Record<string, any>; +import type { RenderData } from '../types/generator.js'; function renderExportsTemplate( renderData: RenderData, diff --git a/packages/ice/templates/core/entry.client.tsx.ejs b/packages/ice/templates/core/entry.client.tsx.ejs index ee9f31bbd4..de4074f294 100644 --- a/packages/ice/templates/core/entry.client.tsx.ejs +++ b/packages/ice/templates/core/entry.client.tsx.ejs @@ -3,8 +3,8 @@ import { runClientApp, getAppConfig } from '<%- iceRuntimePath %>'; import { commons, statics } from './runtime-modules'; import * as app from '@/app'; -<% if (enableRoutes) { -%> -import createRoutes from './routes'; +<% if (routesFile) { -%> +import createRoutes from '<%- routesFile %>'; <% } -%> <%- runtimeOptions.imports %> <% if (dataLoaderImport.imports && hasDataLoader) {-%><%-dataLoaderImport.imports%><% } -%> @@ -21,7 +21,7 @@ const renderOptions: RunClientAppOptions = { commons, statics, }, - <% if (enableRoutes) { %>createRoutes,<% } %> + <% if (routesFile) { %>createRoutes,<% } %> basename: getRouterBasename(), hydrate: <%- hydrate %>, memoryRouter: <%- memoryRouter || false %>, diff --git a/packages/ice/templates/core/entry.server.ts.ejs b/packages/ice/templates/core/entry.server.ts.ejs index 2fd517eac9..41a915639b 100644 --- a/packages/ice/templates/core/entry.server.ts.ejs +++ b/packages/ice/templates/core/entry.server.ts.ejs @@ -1,8 +1,8 @@ import './env.server'; <% if (hydrate) {-%> -import { getAppConfig, renderToHTML as renderAppToHTML, renderToResponse as renderAppToResponse } from '@ice/runtime/server'; +import { getAppConfig, renderToHTML as renderAppToHTML, renderToResponse as renderAppToResponse } from '<%- runtimeServer %>'; <% } else { -%> -import { getAppConfig, getDocumentResponse as renderAppToHTML, renderDocumentToResponse as renderAppToResponse } from '@ice/runtime/server'; +import { getAppConfig, getDocumentResponse as renderAppToHTML, renderDocumentToResponse as renderAppToResponse } from '<%- runtimeServer %>'; <% }-%> <%- entryServer.imports %> <% if (hydrate) {-%> @@ -18,7 +18,7 @@ import type { RenderMode } from '@ice/runtime'; import type { RenderToPipeableStreamOptions } from 'react-dom/server'; // @ts-ignore import assetsManifest from 'virtual:assets-manifest.json'; -<% if (hydrate) {-%> +<% if (hydrate && routesFile) {-%> import createRoutes from '<%- routesFile %>'; <% } else { -%> import routesManifest from './route-manifest.json'; @@ -26,7 +26,7 @@ import routesManifest from './route-manifest.json'; <% if (dataLoaderImport.imports) {-%><%-dataLoaderImport.imports%><% } -%> <% if (hydrate) {-%><%- runtimeOptions.imports %><% } -%> -<% if (!hydrate) {-%> +<% if (!hydrate || !routesFile) {-%> // Do not inject runtime modules when render mode is document only. const commons = []; const statics = []; diff --git a/packages/plugin-miniapp/src/index.ts b/packages/plugin-miniapp/src/index.ts index e4fa119e81..ae692bc836 100644 --- a/packages/plugin-miniapp/src/index.ts +++ b/packages/plugin-miniapp/src/index.ts @@ -44,7 +44,7 @@ const plugin: Plugin<MiniappOptions> = (miniappOptions = {}) => ({ ]; generator.addRenderFile('core/entry.client.tsx.ejs', 'entry.miniapp.tsx', { iceRuntimePath: miniappRuntime, - enableRoutes: false, + routesFile: '', }); generator.addRenderFile('core/index.ts.ejs', 'index.miniapp.ts', { diff --git a/packages/runtime-kit/package.json b/packages/runtime-kit/package.json index 4c55e8576d..bf26a7156c 100644 --- a/packages/runtime-kit/package.json +++ b/packages/runtime-kit/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "description": "Runtime utilities and tools for ICE framework", "main": "./esm/index.js", - "module": "./esm/index.mjs", + "module": "./esm/index.js", "types": "./esm/index.d.ts", "files": [ "esm" diff --git a/packages/runtime-kit/src/appConfig.ts b/packages/runtime-kit/src/appConfig.ts index 88dc6fd743..a260595543 100644 --- a/packages/runtime-kit/src/appConfig.ts +++ b/packages/runtime-kit/src/appConfig.ts @@ -10,7 +10,7 @@ const defaultAppConfig: AppConfig = { }, } as const; -export default function getAppConfig(appExport: AppExport): AppConfig { +export function getAppConfig(appExport: AppExport): AppConfig { const { default: appConfig = {} } = appExport || {}; const { app, router, ...others } = appConfig; diff --git a/packages/runtime/src/runClientApp.tsx b/packages/runtime/src/runClientApp.tsx index f268cb0d2e..d03ed992ea 100644 --- a/packages/runtime/src/runClientApp.tsx +++ b/packages/runtime/src/runClientApp.tsx @@ -36,7 +36,6 @@ export interface RunClientAppOptions { dataLoaderDecorator?: Function; } - export default async function runClientApp(options: RunClientAppOptions) { const { app, diff --git a/packages/shared-config/package.json b/packages/shared-config/package.json index 8556e56cce..eeaa677261 100644 --- a/packages/shared-config/package.json +++ b/packages/shared-config/package.json @@ -28,7 +28,8 @@ "esbuild": "^0.17.16", "postcss": "^8.4.31", "webpack": "^5.86.0", - "webpack-dev-server": "4.15.0" + "webpack-dev-server": "4.15.0", + "@ice/route-manifest": "workspace:*" }, "scripts": { "watch": "tsc -w --sourceMap", diff --git a/packages/shared-config/src/types.ts b/packages/shared-config/src/types.ts index 158c7de1e4..feba32c3ce 100644 --- a/packages/shared-config/src/types.ts +++ b/packages/shared-config/src/types.ts @@ -14,6 +14,7 @@ import type Server from 'webpack-dev-server'; import type { SwcCompilationConfig } from '@ice/bundles'; import type { BuildOptions } from 'esbuild'; import type { ProcessOptions } from 'postcss'; +import type { NestedRouteManifest } from '@ice/route-manifest'; export type ECMA = 5 | 2015 | 2016 | 2017 | 2018 | 2019 | 2020; @@ -90,6 +91,17 @@ export type { webpack }; type PluginFunction = (this: Compiler, compiler: Compiler) => void; +export interface RouteDefinitionOptions { + manifest: NestedRouteManifest[]; + lazy?: boolean; + depth?: number; + matchRoute?: (route: NestedRouteManifest) => boolean; +} +export interface RouteDefinition { + routeImports: string[]; + routeDefinition: string; +} + export interface Config { // The name of the task, used for the output log. name?: string; @@ -234,5 +246,18 @@ export interface Config { optimizePackageImports?: string[]; - runtimeSource?: string; + runtime?: { + source?: string; + server?: string; + exports?: { + specifier: string[]; + source: string; + alias?: Record<string, string>; + }[]; + router?: { + routesDefinition?: (options: RouteDefinitionOptions) => RouteDefinition; + source?: string; + template?: string; + }; + }; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 128c676300..115b1131e4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -252,6 +252,31 @@ importers: specifier: ^5.88.0 version: 5.88.2 + examples/custom-runtime: + dependencies: + '@ice/app': + specifier: workspace:* + version: link:../../packages/ice + '@ice/runtime': + specifier: workspace:* + version: link:../../packages/runtime + '@ice/runtime-kit': + specifier: workspace:* + version: link:../../packages/runtime-kit + react: + specifier: ^18.2.0 + version: 18.2.0 + react-dom: + specifier: ^18.2.0 + version: 18.2.0(react@18.2.0) + devDependencies: + '@types/react': + specifier: ^18.0.0 + version: 18.0.34 + '@types/react-dom': + specifier: ^18.0.2 + version: 18.0.11 + examples/disable-data-loader: dependencies: '@ice/app': @@ -2486,6 +2511,9 @@ importers: specifier: ^0.11.10 version: 0.11.10 devDependencies: + '@ice/route-manifest': + specifier: workspace:* + version: link:../route-manifest esbuild: specifier: ^0.17.16 version: 0.17.16