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..6a20c845ed --- /dev/null +++ b/examples/custom-runtime/ice.config.mts @@ -0,0 +1,34 @@ +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', + router: { + source: '@/routes', + }, + }; + }) + }, + }, + ], +})); + 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..828cab673a --- /dev/null +++ b/examples/custom-runtime/runtime.tsx @@ -0,0 +1,24 @@ +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) => { + const { basename = '', createRoutes } = options; + // Normalize pathname with leading slash + const pathname = `/${window.location.pathname.replace(basename, '').replace(/^\/+/, '')}`; + + const routes = createRoutes?.({ renderMode: 'CSR' }); + const Component = routes?.find(route => route.path === pathname)?.component; + + ReactDOM.render( + Component ? :
404
, + 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/routes.tsx b/examples/custom-runtime/src/routes.tsx new file mode 100644 index 0000000000..76c5344c0f --- /dev/null +++ b/examples/custom-runtime/src/routes.tsx @@ -0,0 +1,13 @@ +import Index from './pages/index'; +import Home from './pages/home'; + +export default () => [ + { + path: '/', + component: Index, + }, + { + path: '/home', + component: Home, + }, +]; 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/examples/with-antd-mobile/src/store.tsx b/examples/with-antd-mobile/src/store.tsx index c2a334f6b7..983366b766 100644 --- a/examples/with-antd-mobile/src/store.tsx +++ b/examples/with-antd-mobile/src/store.tsx @@ -1,4 +1,3 @@ -import type { ComponentWithChildren } from '@ice/runtime/types'; import { useState } from 'react'; import constate from 'constate'; @@ -12,7 +11,7 @@ function useCounter() { const [CounterProvider, useCounterContext] = constate(useCounter); -export const StoreProvider: ComponentWithChildren = ({ children }) => { +export const StoreProvider = ({ children }) => { return <CounterProvider>{ children } </CounterProvider>; }; diff --git a/package.json b/package.json index d66ef5646c..b6e85a9c0d 100644 --- a/package.json +++ b/package.json @@ -74,5 +74,8 @@ "@rspack/core@0.5.7": "patches/@rspack__core@0.5.7.patch", "unplugin@1.6.0": "patches/unplugin@1.6.0.patch" } - } + }, + "workspaces": [ + "packages/runtime-kit" + ] } diff --git a/packages/ice/package.json b/packages/ice/package.json index baf4d45dfb..f4211d3ca9 100644 --- a/packages/ice/package.json +++ b/packages/ice/package.json @@ -50,6 +50,7 @@ "@ice/bundles": "workspace:*", "@ice/route-manifest": "workspace:*", "@ice/runtime": "workspace:^", + "@ice/runtime-kit": "workspace:^", "@ice/shared-config": "workspace:*", "@ice/webpack-config": "workspace:*", "@ice/rspack-config": "workspace:*", diff --git a/packages/ice/src/bundler/config/getUrls.ts b/packages/ice/src/bundler/config/getUrls.ts index 5faa89342c..1cf97a7df7 100644 --- a/packages/ice/src/bundler/config/getUrls.ts +++ b/packages/ice/src/bundler/config/getUrls.ts @@ -1,6 +1,6 @@ import type { TaskConfig } from 'build-scripts'; import type { Config } from '@ice/shared-config/types'; -import type { AppConfig } from '@ice/runtime/types'; +import type { AppConfig } from '@ice/runtime-kit'; import type { Configuration as DevServerConfiguration } from 'webpack-dev-server'; import type { Configuration as RSPackDevServerConfiguration } from '@rspack/dev-server'; diff --git a/packages/ice/src/bundler/types.ts b/packages/ice/src/bundler/types.ts index 0cf4f110ed..61459b67b2 100644 --- a/packages/ice/src/bundler/types.ts +++ b/packages/ice/src/bundler/types.ts @@ -1,7 +1,7 @@ import type { Config } from '@ice/shared-config/types'; import type ora from '@ice/bundles/compiled/ora/index.js'; import type { Stats as WebpackStats } from '@ice/bundles/compiled/webpack/index.js'; -import type { AppConfig } from '@ice/runtime/types'; +import type { AppConfig } from '@ice/runtime-kit'; import type { Configuration, MultiCompiler, MultiStats } from '@rspack/core'; import type { Context as DefaultContext, TaskConfig } from 'build-scripts'; import type { ServerCompiler, GetAppConfig, GetRoutesConfig, GetDataloaderConfig, ExtendsPluginAPI } from '../types/plugin.js'; diff --git a/packages/ice/src/createService.ts b/packages/ice/src/createService.ts index 4a97092a1d..ed839545df 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 type { AppConfig } from '@ice/runtime-kit'; +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 = '@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/esbuild/assets.ts b/packages/ice/src/esbuild/assets.ts index ea594ffd4e..aca0ac8059 100644 --- a/packages/ice/src/esbuild/assets.ts +++ b/packages/ice/src/esbuild/assets.ts @@ -2,7 +2,7 @@ import * as path from 'path'; import * as mrmime from 'mrmime'; import fs from 'fs-extra'; import type { PluginBuild } from 'esbuild'; -import type { AssetsManifest } from '@ice/runtime/types'; +import type { AssetsManifest } from '@ice/runtime-kit'; export const ASSET_TYPES = [ // images 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 3dd7e80e5a..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,6 +34,16 @@ const getDefaultTaskConfig = ({ rootDir, command }): Config => { logging: process.env.WEBPACK_LOGGING || defaultLogging, minify: command === 'build', useDevServer: true, + 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/types/plugin.ts b/packages/ice/src/types/plugin.ts index 174e1cbc86..0fd4688152 100644 --- a/packages/ice/src/types/plugin.ts +++ b/packages/ice/src/types/plugin.ts @@ -4,7 +4,7 @@ import type { Configuration, Stats, WebpackOptionsNormalized } from '@ice/bundle import type { esbuild } from '@ice/bundles'; import type { DefineExtraRoutes, NestedRouteManifest } from '@ice/route-manifest'; import type { Config } from '@ice/shared-config/types'; -import type { AppConfig, AssetsManifest } from '@ice/runtime/types'; +import type { AppConfig, AssetsManifest } from '@ice/runtime-kit'; import type ServerCompileTask from '../utils/ServerCompileTask.js'; import type { CreateLogger } from '../utils/logger.js'; import type { DeclarationData, AddRenderFile, AddTemplateFiles, ModifyRenderData, AddDataLoaderImport, Render } from './generator.js'; diff --git a/packages/ice/src/utils/getRouterBasename.ts b/packages/ice/src/utils/getRouterBasename.ts index 4ad0e90ec5..482bf8591d 100644 --- a/packages/ice/src/utils/getRouterBasename.ts +++ b/packages/ice/src/utils/getRouterBasename.ts @@ -1,4 +1,4 @@ -import type { AppConfig } from '@ice/runtime'; +import type { AppConfig } from '@ice/runtime-kit'; import type { Config } from '@ice/shared-config/types'; import type { TaskConfig } from 'build-scripts'; diff --git a/packages/ice/src/utils/injectInitialEntry.ts b/packages/ice/src/utils/injectInitialEntry.ts index a8375983b0..175da9682d 100644 --- a/packages/ice/src/utils/injectInitialEntry.ts +++ b/packages/ice/src/utils/injectInitialEntry.ts @@ -1,6 +1,6 @@ import path from 'path'; import fse from 'fs-extra'; -import type { RouteItem } from '@ice/runtime/types'; +import type { RouteItem } from '@ice/runtime'; import matchRoutes from '@ice/runtime/matchRoutes'; import { logger } from './logger.js'; import type RouteManifest from './routeManifest.js'; 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/src/utils/runtimeEnv.ts b/packages/ice/src/utils/runtimeEnv.ts index 08337167b6..78f9cdbbcb 100644 --- a/packages/ice/src/utils/runtimeEnv.ts +++ b/packages/ice/src/utils/runtimeEnv.ts @@ -3,7 +3,7 @@ import * as fs from 'fs'; import * as dotenv from 'dotenv'; import { expand as dotenvExpand } from 'dotenv-expand'; import type { CommandArgs } from 'build-scripts'; -import type { AppConfig } from '@ice/runtime/types'; +import type { AppConfig } from '@ice/runtime-kit'; export interface Envs { [key: string]: string; diff --git a/packages/ice/templates/core/entry.client.tsx.ejs b/packages/ice/templates/core/entry.client.tsx.ejs index 0e52e5a325..de4074f294 100644 --- a/packages/ice/templates/core/entry.client.tsx.ejs +++ b/packages/ice/templates/core/entry.client.tsx.ejs @@ -1,11 +1,10 @@ <% if (importCoreJs) { -%>import 'core-js';<% } %> <%- entry.imports %> -import { createElement, Fragment } from 'react'; 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%><% } -%> @@ -15,19 +14,6 @@ const getRouterBasename = () => { const appConfig = getAppConfig(app); return appConfig?.router?.basename ?? <%- basename %> ?? ''; } -// Add react fragment for split chunks of app. -// Otherwise chunk of route component will pack @ice/jsx-runtime and depend on framework bundle. -const App = <></>; - -<% if (!dataLoaderImport.imports && hasDataLoader) {-%> -let dataLoaderFetcher = (options) => { - return window.fetch(options.url, options); -} - -let dataLoaderDecorator = (dataLoader) => { - return dataLoader; -} -<% } -%> const renderOptions: RunClientAppOptions = { app, @@ -35,11 +21,11 @@ const renderOptions: RunClientAppOptions = { commons, statics, }, - <% if (enableRoutes) { %>createRoutes,<% } %> + <% if (routesFile) { %>createRoutes,<% } %> basename: getRouterBasename(), hydrate: <%- hydrate %>, memoryRouter: <%- memoryRouter || false %>, -<% if (hasDataLoader) { -%> +<% if (dataLoaderImport.imports && hasDataLoader) { -%> dataLoaderFetcher, dataLoaderDecorator,<% } -%> runtimeOptions: { @@ -50,27 +36,23 @@ const renderOptions: RunClientAppOptions = { }, }; -const defaultRender = (customOptions: Partial<RunClientAppOptions> = {}) => { - return runClientApp({ - ...renderOptions, - ...customOptions, - runtimeOptions: { - ...(renderOptions.runtimeOptions || {}), - ...customOptions.runtimeOptions, - }, - }); -}; - -const renderApp = (appExport: any, customOptions: Partial<RunClientAppOptions>) => { - if (appExport.runApp) { - return appExport.runApp(defaultRender, renderOptions); - } else { - return defaultRender(customOptions); - } -}; +const mergeOptions = (customOptions: Partial<RunClientAppOptions> = {}): RunClientAppOptions => ({ + ...renderOptions, + ...customOptions, + runtimeOptions: { + ...renderOptions.runtimeOptions, + ...customOptions.runtimeOptions, + }, +}); -const render = (customOptions: Partial<RunClientAppOptions> = {}) => { - return renderApp(app, customOptions); +const render = () => { + return app.runApp?.( + (customOptions: Partial<RunClientAppOptions> = {}) => { + const options = mergeOptions(customOptions); + return runClientApp(options); + }, + renderOptions + ) ?? runClientApp(renderOptions); }; <%- entryCode %> 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-auth/src/runtime/index.tsx b/packages/plugin-auth/src/runtime/index.tsx index cf0fda848d..5e44e6d6f7 100644 --- a/packages/plugin-auth/src/runtime/index.tsx +++ b/packages/plugin-auth/src/runtime/index.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import type { RuntimePlugin, AppProvider, RouteWrapper } from '@ice/runtime/types'; +import type { RuntimePlugin, AppProvider, RouteWrapper } from '@ice/runtime'; import type { AuthConfig, AuthType, Auth } from '../types.js'; import { AuthProvider, useAuth, withAuth } from './Auth.js'; import type { InjectProps } from './Auth.js'; diff --git a/packages/plugin-auth/src/types.ts b/packages/plugin-auth/src/types.ts index 318a0a670d..7fc292a80c 100644 --- a/packages/plugin-auth/src/types.ts +++ b/packages/plugin-auth/src/types.ts @@ -1,5 +1,5 @@ import type * as React from 'react'; -import type { RouteConfig } from '@ice/runtime/types'; +import type { RouteConfig } from '@ice/runtime'; export interface AuthConfig { initialAuth: { diff --git a/packages/plugin-i18n/src/runtime/index.tsx b/packages/plugin-i18n/src/runtime/index.tsx index dee5e6a12c..420759cf2f 100644 --- a/packages/plugin-i18n/src/runtime/index.tsx +++ b/packages/plugin-i18n/src/runtime/index.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import type { RuntimePlugin } from '@ice/runtime/types'; +import type { RuntimePlugin } from '@ice/runtime'; import detectLocale from '../utils/detectLocale.js'; import type { I18nAppConfig, I18nConfig } from '../types.js'; import getLocaleRedirectPath from '../utils/getLocaleRedirectPath.js'; @@ -86,4 +86,4 @@ const runtime: RuntimePlugin<{ i18nConfig: I18nConfig }> = async ( export default runtime; -export { useLocale, withLocale }; \ No newline at end of file +export { useLocale, withLocale }; diff --git a/packages/plugin-icestark/src/runtime/child.tsx b/packages/plugin-icestark/src/runtime/child.tsx index 11dc169362..ed62c64569 100644 --- a/packages/plugin-icestark/src/runtime/child.tsx +++ b/packages/plugin-icestark/src/runtime/child.tsx @@ -1,5 +1,5 @@ import * as ReactDOM from 'react-dom/client'; -import type { RuntimePlugin } from '@ice/runtime/types'; +import type { RuntimePlugin } from '@ice/runtime'; import type { LifecycleOptions } from '../types'; const runtime: RuntimePlugin<LifecycleOptions> = ({ setRender }, runtimeOptions) => { diff --git a/packages/plugin-icestark/src/runtime/framework.tsx b/packages/plugin-icestark/src/runtime/framework.tsx index e8743b9905..2be519208a 100644 --- a/packages/plugin-icestark/src/runtime/framework.tsx +++ b/packages/plugin-icestark/src/runtime/framework.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { AppRouter, AppRoute } from '@ice/stark'; -import type { RuntimePlugin, ClientAppRouterProps } from '@ice/runtime/types'; +import type { RuntimePlugin, ClientAppRouterProps } from '@ice/runtime'; import type { RouteInfo, AppConfig } from '../types'; const { useState, useEffect } = React; diff --git a/packages/plugin-intl/src/runtime-simple.ts b/packages/plugin-intl/src/runtime-simple.ts index 504e18cf09..0084423eac 100644 --- a/packages/plugin-intl/src/runtime-simple.ts +++ b/packages/plugin-intl/src/runtime-simple.ts @@ -1,4 +1,4 @@ -import type { RuntimePlugin } from '@ice/runtime/types'; +import type { RuntimePlugin } from '@ice/runtime'; import { getDefaultLocale, getLocaleMessages, EXPORT_NAME } from './intl-until.js'; let currentLocale = getDefaultLocale(); diff --git a/packages/plugin-intl/src/runtime.tsx b/packages/plugin-intl/src/runtime.tsx index d8c47f5f39..82a48a980f 100644 --- a/packages/plugin-intl/src/runtime.tsx +++ b/packages/plugin-intl/src/runtime.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { createIntl, createIntlCache, RawIntlProvider, useIntl } from 'react-intl'; import type { IntlShape } from 'react-intl'; -import type { RuntimePlugin } from '@ice/runtime/types'; +import type { RuntimePlugin } from '@ice/runtime'; import { getDefaultLocale, getLocaleMessages, EXPORT_NAME } from './intl-until.js'; import type { LocaleConfig } from './types.js'; 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/plugin-miniapp/src/runtime/index.ts b/packages/plugin-miniapp/src/runtime/index.ts index 90ec50aab2..8848958e31 100644 --- a/packages/plugin-miniapp/src/runtime/index.ts +++ b/packages/plugin-miniapp/src/runtime/index.ts @@ -1,4 +1,4 @@ -import type { RuntimePlugin } from '@ice/runtime/types'; +import type { RuntimePlugin } from '@ice/runtime'; import type { MiniappLifecycles } from '@ice/miniapp-runtime/esm/types'; export function defineMiniappConfig( diff --git a/packages/plugin-request/src/runtime.ts b/packages/plugin-request/src/runtime.ts index 5c4c8800f3..d47e6b8f03 100644 --- a/packages/plugin-request/src/runtime.ts +++ b/packages/plugin-request/src/runtime.ts @@ -1,4 +1,4 @@ -import type { StaticRuntimePlugin } from '@ice/runtime/types'; +import type { StaticRuntimePlugin } from '@ice/runtime'; import { createAxiosInstance, setAxiosInstance } from './request.js'; import type { RequestConfig } from './types'; diff --git a/packages/plugin-store/src/runtime.tsx b/packages/plugin-store/src/runtime.tsx index cfb6fc9b80..d75bfd4741 100644 --- a/packages/plugin-store/src/runtime.tsx +++ b/packages/plugin-store/src/runtime.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import type { RuntimePlugin, AppProvider, RouteWrapper } from '@ice/runtime/types'; +import type { RuntimePlugin, AppProvider, RouteWrapper } from '@ice/runtime'; import { PAGE_STORE_INITIAL_STATES, PAGE_STORE_PROVIDER } from './constants.js'; import type { StoreConfig } from './types.js'; diff --git a/packages/runtime-kit/package.json b/packages/runtime-kit/package.json new file mode 100644 index 0000000000..fa7387e86e --- /dev/null +++ b/packages/runtime-kit/package.json @@ -0,0 +1,36 @@ +{ + "name": "@ice/runtime-kit", + "version": "0.1.0", + "description": "Runtime utilities and tools for ICE framework", + "main": "./esm/index.js", + "module": "./esm/index.js", + "types": "./esm/index.d.ts", + "type": "module", + "files": [ + "esm" + ], + "sideEffects": false, + "scripts": { + "watch": "tsc -w --sourceMap", + "build": "tsc" + }, + "dependencies": {}, + "devDependencies": { + "@types/react": "^18.0.8", + "@types/react-dom": "^18.0.3", + "react": "^18.0.0", + "react-dom": "^18.0.0" + }, + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/alibaba/ice.git" + }, + "bugs": { + "url": "https://github.com/alibaba/ice/issues" + }, + "homepage": "https://ice.work", + "license": "MIT" +} diff --git a/packages/runtime-kit/src/appConfig.ts b/packages/runtime-kit/src/appConfig.ts new file mode 100644 index 0000000000..a260595543 --- /dev/null +++ b/packages/runtime-kit/src/appConfig.ts @@ -0,0 +1,29 @@ +import type { AppConfig, AppExport } from './types.js'; + +const defaultAppConfig: AppConfig = { + app: { + strict: false, + rootId: 'ice-container', + }, + router: { + type: 'browser', + }, +} as const; + +export function getAppConfig(appExport: AppExport): AppConfig { + const { default: appConfig = {} } = appExport || {}; + const { app, router, ...others } = appConfig; + + return { + app: { ...defaultAppConfig.app, ...app }, + router: { ...defaultAppConfig.router, ...router }, + ...others, + }; +} + +export const defineAppConfig = ( + appConfigOrDefineAppConfig: AppConfig | (() => AppConfig), +): AppConfig => + (typeof appConfigOrDefineAppConfig === 'function' + ? appConfigOrDefineAppConfig() + : appConfigOrDefineAppConfig); diff --git a/packages/runtime-kit/src/dataLoader.ts b/packages/runtime-kit/src/dataLoader.ts new file mode 100644 index 0000000000..84c5ac3a37 --- /dev/null +++ b/packages/runtime-kit/src/dataLoader.ts @@ -0,0 +1,239 @@ +import { getRequestContext } from './requestContext.js'; +import type { + RequestContext, RenderMode, AppExport, + RuntimeModules, StaticRuntimePlugin, CommonJsRuntime, + Loader, DataLoaderResult, StaticDataLoader, DataLoaderConfig, DataLoaderOptions, + RunClientAppOptions, +} from './types.js'; + +interface Loaders { + [routeId: string]: DataLoaderConfig; +} + +interface CachedResult { + value: any; +} + +interface Options { + fetcher: RunClientAppOptions['dataLoaderFetcher']; + decorator: RunClientAppOptions['dataLoaderDecorator']; + runtimeModules: RuntimeModules['statics']; + appExport: AppExport; +} + +export interface LoadRoutesDataOptions { + renderMode: RenderMode; + requestContext?: RequestContext; +} + +export function defineDataLoader(dataLoader: Loader, options?: DataLoaderOptions): DataLoaderConfig { + return { + loader: dataLoader, + options, + }; +} + +export function defineServerDataLoader(dataLoader: Loader, options?: DataLoaderOptions): DataLoaderConfig { + return { + loader: dataLoader, + options, + }; +} + +export function defineStaticDataLoader(dataLoader: Loader): DataLoaderConfig { + return { + loader: dataLoader, + }; +} + +/** + * Custom fetcher for load static data loader config. + * Set globally to avoid passing this fetcher too deep. + */ +let dataLoaderFetcher: RunClientAppOptions['dataLoaderFetcher']; +export function setFetcher(customFetcher: RunClientAppOptions['dataLoaderFetcher']): void { + dataLoaderFetcher = customFetcher; +} + +/** + * Custom decorator for deal with data loader. + */ +// @ts-ignore +let dataLoaderDecorator: RunClientAppOptions['dataLoaderDecorator'] = (dataLoader) => { + return dataLoader; +}; +export function setDecorator(customDecorator: RunClientAppOptions['dataLoaderDecorator']): void { + dataLoaderDecorator = customDecorator; +} + +/** + * Parse template for static dataLoader. + */ +export function parseTemplate(config: StaticDataLoader): StaticDataLoader { + const queryParams: Record<string, string> = {}; + const getQueryParams = (): Record<string, string> => { + if (Object.keys(queryParams).length === 0 && location.search.includes('?')) { + location.search.substring(1).split('&').forEach(query => { + const [key, value] = query.split('='); + if (key && value) { + queryParams[key] = value; + } + }); + } + return queryParams; + }; + + const cookie: Record<string, string> = {}; + const getCookie = (): Record<string, string> => { + if (Object.keys(cookie).length === 0) { + document.cookie.split(';').forEach(c => { + const [key, value] = c.split('='); + if (key && value) { + cookie[key.trim()] = value.trim(); + } + }); + } + return cookie; + }; + + let strConfig = JSON.stringify(config) || ''; + const regexp = /\$\{(queryParams|cookie|storage)(\.(\w|-)+)?}/g; + const matches = Array.from(strConfig.matchAll(regexp)); + + matches.forEach(([origin, key, value]) => { + if (origin && key && value?.startsWith('.')) { + const param = value.substring(1); + if (key === 'queryParams') { + strConfig = strConfig.replace(origin, getQueryParams()[param] || ''); + } else if (key === 'cookie') { + strConfig = strConfig.replace(origin, getCookie()[param] || ''); + } else if (key === 'storage') { + strConfig = strConfig.replace(origin, localStorage.getItem(param) || ''); + } + } + }); + + strConfig = strConfig.replace('${url}', location.href); + + return JSON.parse(strConfig); +} + +export function loadDataByCustomFetcher(config: StaticDataLoader): Promise<any> { + let parsedConfig = config; + try { + if (import.meta.renderer === 'client') { + parsedConfig = parseTemplate(config); + } + } catch (error) { + console.error('parse template error: ', error); + } + return dataLoaderFetcher?.(parsedConfig); +} + +/** + * Handle for different dataLoader. + */ +export function callDataLoader(dataLoader: Loader, requestContext: RequestContext): DataLoaderResult { + if (Array.isArray(dataLoader)) { + return dataLoader.map((loader, index) => + (typeof loader === 'object' ? loadDataByCustomFetcher(loader) : dataLoaderDecorator(loader, index)(requestContext)), + ); + } + + if (typeof dataLoader === 'object') { + return loadDataByCustomFetcher(dataLoader); + } + + return dataLoaderDecorator?.(dataLoader)?.(requestContext); +} + +const cache = new Map<string, CachedResult>(); + +/** + * Start getData once data-loader.js is ready in client, and set to cache. + */ +function loadInitialDataInClient(loaders: Loaders): void { + const context = (window as any).__ICE_APP_CONTEXT__ || {}; + const matchedIds = context.matchedIds || []; + const loaderData = context.loaderData || {}; + const { renderMode } = context; + + const ids = ['__app', ...matchedIds]; + ids.forEach(id => { + const dataFromSSR = loaderData[id]?.data; + if (dataFromSSR) { + cache.set(renderMode === 'SSG' ? `${id}_ssg` : id, { + value: dataFromSSR, + }); + + if (renderMode === 'SSR') { + return; + } + } + + const dataLoaderConfig = loaders[id]; + if (dataLoaderConfig) { + const requestContext = getRequestContext(window.location); + const { loader } = dataLoaderConfig; + const promise = callDataLoader(loader, requestContext); + + cache.set(id, { + value: promise, + }); + } + }); +} + +/** + * Init data loader in client side. + */ +async function init(loaders: Loaders, options: Options): Promise<void> { + const { fetcher, decorator, runtimeModules, appExport } = options; + + const runtimeApi = { + appContext: { appExport }, + }; + + if (runtimeModules) { + await Promise.all( + runtimeModules + .map(module => { + const runtimeModule = ((module as CommonJsRuntime).default || module) as StaticRuntimePlugin; + return runtimeModule(runtimeApi); + }) + .filter(Boolean), + ); + } + + if (fetcher) setFetcher(fetcher); + if (decorator) setDecorator(decorator); + + try { + loadInitialDataInClient(loaders); + } catch (error) { + console.error('Load initial data error: ', error); + } + + (window as any).__ICE_DATA_LOADER__ = { + getLoader: (id: string): DataLoaderConfig => loaders[id], + getData: (id: string, options: LoadRoutesDataOptions): DataLoaderResult => { + const cacheKey = `${id}${options?.renderMode === 'SSG' ? '_ssg' : ''}`; + const result = cache.get(cacheKey); + cache.delete(cacheKey); + + if (result) return result.value; + + const dataLoaderConfig = loaders[id]; + if (!dataLoaderConfig) return null; + + const { loader } = dataLoaderConfig; + return callDataLoader(loader, options?.requestContext || getRequestContext(window.location)); + }, + }; +} + +export const dataLoader = { + init, +}; + +export default dataLoader; diff --git a/packages/runtime-kit/src/index.ts b/packages/runtime-kit/src/index.ts new file mode 100644 index 0000000000..b001336370 --- /dev/null +++ b/packages/runtime-kit/src/index.ts @@ -0,0 +1,4 @@ +export * from './appConfig.js'; +export * from './dataLoader.js'; +export * from './requestContext.js'; +export * from './types.js'; diff --git a/packages/runtime/src/requestContext.ts b/packages/runtime-kit/src/requestContext.ts similarity index 88% rename from packages/runtime/src/requestContext.ts rename to packages/runtime-kit/src/requestContext.ts index aff80f144d..72482f5455 100644 --- a/packages/runtime/src/requestContext.ts +++ b/packages/runtime-kit/src/requestContext.ts @@ -1,6 +1,6 @@ import type { ServerContext, RequestContext } from './types.js'; -export interface Location { +interface Location { pathname: string; search: string; } @@ -8,7 +8,7 @@ export interface Location { /** * context for getData both in server and client side. */ -export default function getRequestContext(location: Location, serverContext: ServerContext = {}): RequestContext { +export function getRequestContext(location: Location, serverContext: ServerContext = {}): RequestContext { const { pathname, search } = location; // Use query form server context first to avoid unnecessary parsing. // @ts-ignore diff --git a/packages/runtime-kit/src/types.ts b/packages/runtime-kit/src/types.ts new file mode 100644 index 0000000000..4149fe76a2 --- /dev/null +++ b/packages/runtime-kit/src/types.ts @@ -0,0 +1,267 @@ +import type { IncomingMessage, ServerResponse } from 'http'; +import type { ComponentType, PropsWithChildren } from 'react'; +import type { HydrationOptions, Root } from 'react-dom/client'; + +// Basic Types +export type AppData = any; +export type RouteData = any; +export type RenderMode = 'SSR' | 'SSG' | 'CSR'; + +// Core Interfaces +export interface Path { + pathname: string; + search: string; + hash: string; +} + +export interface Location<State = any> extends Path { + state: State; + key: string; +} + +export interface ErrorStack { + componentStack?: string; + digest?: string; +} + +export interface ServerContext { + req?: IncomingMessage; + res?: ServerResponse; +} + +export interface RequestContext extends ServerContext { + pathname: string; + query: Record<string, any>; +} + +// App Configuration Types +export type App = Partial<{ + rootId: string; + strict: boolean; + errorBoundary: boolean; + onRecoverableError: (error: unknown, errorInfo: ErrorStack) => void; + onBeforeHydrate: () => void; +}>; + +export interface AppConfig { + app?: App; + router?: { + type?: 'hash' | 'browser' | 'memory'; + basename?: string; + initialEntries?: InitialEntry[]; + }; +} + +export interface AppExport { + default?: AppConfig; + dataLoader?: DataLoaderConfig; + [key: string]: any; +} + +// Route & Component Types +export type ComponentWithChildren<P = {}> = ComponentType<PropsWithChildren<P>>; +export type AppProvider = ComponentWithChildren<any>; +export type RouteWrapper = ComponentType<any>; + +export type InitialEntry = string | Partial<Location>; +export type Params<Key extends string = string> = { + readonly [key in Key]: string | undefined; +}; + +// Data Loading Types +export interface DataLoaderOptions { + defer?: boolean; +} + +export interface StaticDataLoader { + key?: string; + prefetch_type?: string; + api: string; + v: string; + data: any; + ext_headers: Object; +} + +export type DataLoaderResult = (Promise<RouteData> | RouteData) | RouteData; +export type DataLoader = (ctx: RequestContext) => DataLoaderResult; +export type Loader = DataLoader | StaticDataLoader | Array<DataLoader | StaticDataLoader>; + +export interface DataLoaderConfig { + loader: Loader; + options?: DataLoaderOptions; +} + +// Route Configuration Types +export type RouteConfig<T = {}> = T & { + title?: string; + meta?: React.MetaHTMLAttributes<HTMLMetaElement>[]; + links?: React.LinkHTMLAttributes<HTMLLinkElement>[]; + scripts?: React.ScriptHTMLAttributes<HTMLScriptElement>[]; +}; + +export type PageConfig = (args: { data?: RouteData }) => RouteConfig; + +export interface LoaderData { + data?: RouteData; + pageConfig?: RouteConfig; +} + +export interface LoadersData { + [routeId: string]: LoaderData; +} + +// Component & Module Types +export type ComponentModule = { + default?: ComponentType<any>; + Component?: ComponentType<any>; + staticDataLoader?: DataLoaderConfig; + serverDataLoader?: DataLoaderConfig; + dataLoader?: DataLoaderConfig; + pageConfig?: PageConfig; + [key: string]: any; +}; + +export interface RouteModules { + [routeId: string]: ComponentModule; +} + +export interface RouteWrapperConfig { + Wrapper: RouteWrapper; + layout?: boolean; +} + +// Runtime Types +export interface AppContext<T = any> { + appConfig: AppConfig; + appData: any; + documentData?: any; + serverData?: any; + assetsManifest?: AssetsManifest; + loaderData?: LoadersData; + routeModules?: RouteModules; + RouteWrappers?: RouteWrapperConfig[]; + routePath?: string; + matches?: { + params: Params; + pathname: string; + pathnameBase: string; + route: T; + }[]; + routes?: T[]; + documentOnly?: boolean; + matchedIds?: string[]; + appExport?: AppExport; + basename?: string; + downgrade?: boolean; + renderMode?: RenderMode; + requestContext?: RequestContext; + revalidate?: boolean; +} + +// Runtime API Types +export type Renderer = ( + container: Element | Document, + initialChildren: React.ReactNode, + options?: HydrationOptions, +) => Root; + +export type ResponseHandler = ( + req: IncomingMessage, + res: ServerResponse, +) => any | Promise<any>; + +export type SetAppRouter = <T>(AppRouter: ComponentType<T>) => void; +export type GetAppRouter = () => AppProvider; +export type AddProvider = (Provider: AppProvider) => void; +export type SetRender = (render: Renderer) => void; +export type AddWrapper = (wrapper: RouteWrapper, forLayout?: boolean) => void; +export type AddResponseHandler = (handler: ResponseHandler) => void; +export type GetResponseHandlers = () => ResponseHandler[]; + +type UseConfig = () => RouteConfig<Record<string, any>>; +type UseData = () => RouteData; +type UseAppContext = () => AppContext; + +export interface RuntimeAPI<T = History> { + setAppRouter?: SetAppRouter; + getAppRouter: GetAppRouter; + addProvider: AddProvider; + addResponseHandler: AddResponseHandler; + getResponseHandlers: GetResponseHandlers; + setRender: SetRender; + addWrapper: AddWrapper; + appContext: AppContext; + useData: UseData; + useConfig: UseConfig; + useAppContext: UseAppContext; + history: T; +} + +// Plugin Types +export interface RuntimePlugin<T = Record<string, any>, H = History> { + (apis: RuntimeAPI<H>, runtimeOptions?: T): Promise<void> | void; +} + +export interface StaticRuntimeAPI { + appContext: { + appExport: AppExport; + }; +} + +export interface StaticRuntimePlugin<T = Record<string, any>> { + (apis: StaticRuntimeAPI, runtimeOptions?: T): Promise<void> | void; +} + +export interface CommonJsRuntime { + default: RuntimePlugin | StaticRuntimePlugin; +} + +// Assets & Runtime Modules +export interface AssetsManifest { + dataLoader?: string; + publicPath: string; + entries: { + [assetPath: string]: string[]; + }; + pages: { + [assetPath: string]: string[]; + }; + assets?: { + [assetPath: string]: string; + }; +} + +export interface RuntimeModules { + statics?: (StaticRuntimePlugin | CommonJsRuntime)[]; + commons?: (RuntimePlugin | CommonJsRuntime)[]; +} + +// Loader & Routes Types +export interface RouteLoaderOptions { + routeId: string; + requestContext?: RequestContext; + module: ComponentModule; + renderMode: RenderMode; +} + +export type CreateRoutes<T> = (options: Pick<RouteLoaderOptions, 'renderMode' | 'requestContext'>) => T[]; + +export interface RunClientAppOptions<T = any> { + app: AppExport; + runtimeModules: RuntimeModules; + createRoutes?: CreateRoutes<T>; + hydrate?: boolean; + basename?: string; + memoryRouter?: boolean; + runtimeOptions?: Record<string, any>; + dataLoaderFetcher?: (config: StaticDataLoader) => any; + dataLoaderDecorator?: (loader: Loader, index?: number) => (requestContext: RequestContext) => DataLoaderResult; +} + +declare global { + interface ImportMeta { + target: string; + renderer: 'client' | 'server'; + env: Record<string, string>; + } +} diff --git a/packages/runtime-kit/tsconfig.json b/packages/runtime-kit/tsconfig.json new file mode 100644 index 0000000000..79cf8a2f2d --- /dev/null +++ b/packages/runtime-kit/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "baseUrl": "./", + "rootDir": "src", + "outDir": "esm", + "module": "ES2020", + "moduleResolution": "NodeNext", + }, + "include": ["src"] +} diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 8a18717a50..998eb47608 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -60,7 +60,8 @@ "abortcontroller-polyfill": "1.7.5", "history": "^5.3.0", "react-router-dom": "6.21.3", - "semver": "^7.4.0" + "semver": "^7.4.0", + "@ice/runtime-kit": "^0.1.0" }, "peerDependencies": { "react": "^18.1.0", diff --git a/packages/runtime/src/AppContext.tsx b/packages/runtime/src/AppContext.tsx index 4aff665a9c..79c16f2c0e 100644 --- a/packages/runtime/src/AppContext.tsx +++ b/packages/runtime/src/AppContext.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import type { AppContext } from './types.js'; +import type { AppContext } from '@ice/runtime-kit'; const Context = React.createContext<AppContext | undefined>(undefined); diff --git a/packages/runtime/src/Document.tsx b/packages/runtime/src/Document.tsx index 53827764d6..0dd06af6b5 100644 --- a/packages/runtime/src/Document.tsx +++ b/packages/runtime/src/Document.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; -import type { WindowContext, RouteMatch, AssetsManifest } from './types.js'; +import type { AssetsManifest } from '@ice/runtime-kit'; +import type { WindowContext, RouteMatch } from './types.js'; import { useAppContext, useAppData } from './AppContext.js'; import { getMeta, getTitle, getLinks, getScripts } from './routesConfig.js'; import getCurrentRoutePath from './utils/getCurrentRoutePath.js'; diff --git a/packages/runtime/src/RouteContext.ts b/packages/runtime/src/RouteContext.ts index 0165af32e5..1b424c5b0e 100644 --- a/packages/runtime/src/RouteContext.ts +++ b/packages/runtime/src/RouteContext.ts @@ -1,5 +1,5 @@ import { useLoaderData } from 'react-router-dom'; -import type { RouteConfig } from './types.js'; +import type { RouteConfig } from '@ice/runtime-kit'; function useData<T = any>(): T { return (useLoaderData() as any)?.data; diff --git a/packages/runtime/src/RouteWrapper.tsx b/packages/runtime/src/RouteWrapper.tsx index adacd13289..1e42ee3585 100644 --- a/packages/runtime/src/RouteWrapper.tsx +++ b/packages/runtime/src/RouteWrapper.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import type { RouteWrapperConfig, ComponentModule } from './types.js'; +import type { RouteWrapperConfig, ComponentModule } from '@ice/runtime-kit'; interface Props { id: string; diff --git a/packages/runtime/src/Suspense.tsx b/packages/runtime/src/Suspense.tsx index 5323436308..5865d93193 100644 --- a/packages/runtime/src/Suspense.tsx +++ b/packages/runtime/src/Suspense.tsx @@ -1,8 +1,8 @@ import * as React from 'react'; import type { ReactNode } from 'react'; +import type { RequestContext } from '@ice/runtime-kit'; import { useAppContext } from './AppContext.js'; import proxyData from './proxyData.js'; -import type { RequestContext } from './types.js'; const LOADER = '__ICE_SUSPENSE_LOADER__'; const isClient = typeof window !== 'undefined' && 'onload' in window; diff --git a/packages/runtime/src/appConfig.ts b/packages/runtime/src/appConfig.ts deleted file mode 100644 index be66c3212c..0000000000 --- a/packages/runtime/src/appConfig.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { AppConfig, AppExport } from './types.js'; - -const defaultAppConfig: AppConfig = { - app: { - strict: false, - rootId: 'ice-container', - }, - router: { - type: 'browser', - }, -}; - -export default function getAppConfig(appExport: AppExport): AppConfig { - const appConfig = appExport?.default || {}; - - const { app, router, ...others } = appConfig; - - return { - app: { - ...defaultAppConfig.app, - ...(app || {}), - }, - router: { - ...defaultAppConfig.router, - ...(router || {}), - }, - ...others, - }; -} - -export function defineAppConfig(appConfigOrDefineAppConfig: AppConfig | (() => AppConfig)): AppConfig { - if (typeof appConfigOrDefineAppConfig === 'function') { - return appConfigOrDefineAppConfig(); - } else { - return appConfigOrDefineAppConfig; - } -} diff --git a/packages/runtime/src/appData.ts b/packages/runtime/src/appData.ts index de9a3cea37..25714742a7 100644 --- a/packages/runtime/src/appData.ts +++ b/packages/runtime/src/appData.ts @@ -1,5 +1,5 @@ -import type { AppExport, AppData, RequestContext, Loader } from './types.js'; -import { callDataLoader } from './dataLoader.js'; +import type { AppExport, AppData, RequestContext, Loader } from '@ice/runtime-kit'; +import { callDataLoader } from '@ice/runtime-kit'; /** * Call the getData of app config. diff --git a/packages/runtime/src/dataLoader.ts b/packages/runtime/src/dataLoader.ts index c37b3b178e..2d7ca42621 100644 --- a/packages/runtime/src/dataLoader.ts +++ b/packages/runtime/src/dataLoader.ts @@ -1,281 +1 @@ -import getRequestContext from './requestContext.js'; -import type { - RequestContext, RenderMode, AppExport, - RuntimeModules, StaticRuntimePlugin, CommonJsRuntime, - Loader, DataLoaderResult, StaticDataLoader, DataLoaderConfig, DataLoaderOptions, -} from './types.js'; -interface Loaders { - [routeId: string]: DataLoaderConfig; -} - -interface CachedResult { - value: any; -} - -interface Options { - fetcher: Function; - decorator: Function; - runtimeModules: RuntimeModules['statics']; - appExport: AppExport; -} - -export interface LoadRoutesDataOptions { - renderMode: RenderMode; - requestContext?: RequestContext; -} - -export function defineDataLoader(dataLoader: Loader, options?: DataLoaderOptions): DataLoaderConfig { - return { - loader: dataLoader, - options, - }; -} - -export function defineServerDataLoader(dataLoader: Loader, options?: DataLoaderOptions): DataLoaderConfig { - return { - loader: dataLoader, - options, - }; -} - -export function defineStaticDataLoader(dataLoader: Loader): DataLoaderConfig { - return { - loader: dataLoader, - }; -} - -/** - * Custom fetcher for load static data loader config. - * Set globally to avoid passing this fetcher too deep. - */ -let dataLoaderFetcher; -export function setFetcher(customFetcher) { - dataLoaderFetcher = customFetcher; -} - -/** - * Custom decorator for deal with data loader. - */ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -let dataLoaderDecorator = (dataLoader: Function, id?: number) => { - return dataLoader; -}; -export function setDecorator(customDecorator) { - dataLoaderDecorator = customDecorator; -} - -/** - * Parse template for static dataLoader. - */ -export function parseTemplate(config: StaticDataLoader) { - const queryParams = {}; - const getQueryParams = () => { - if (Object.keys(queryParams).length === 0) { - if (location.search.includes('?')) { - location.search.substring(1).split('&').forEach(query => { - const res = query.split('='); - // ?test=1&hello=world - if (res[0] !== undefined && res[1] !== undefined) { - queryParams[res[0]] = res[1]; - } - }); - } - } - - return queryParams; - }; - - const cookie = {}; - const getCookie = () => { - if (Object.keys(cookie).length === 0) { - document.cookie.split(';').forEach(c => { - const [key, value] = c.split('='); - if (key !== undefined && value !== undefined) { - cookie[key.trim()] = value.trim(); - } - }); - } - - return cookie; - }; - - // Match all template of query cookie and storage. - let strConfig = JSON.stringify(config) || ''; - const regexp = /\$\{(queryParams|cookie|storage)(\.(\w|-)+)?}/g; - let cap = []; - let matched = []; - while ((cap = regexp.exec(strConfig)) !== null) { - matched.push(cap); - } - - matched.forEach(item => { - const [origin, key, value] = item; - if (item && origin && key && value && value.startsWith('.')) { - if (key === 'queryParams') { - // Replace query params. - strConfig = strConfig.replace(origin, getQueryParams()[value.substring(1)] || ''); - } else if (key === 'cookie') { - // Replace cookie. - strConfig = strConfig.replace(origin, getCookie()[value.substring(1)] || ''); - } else if (key === 'storage') { - // Replace storage. - strConfig = strConfig.replace(origin, localStorage.getItem(value.substring(1)) || ''); - } - } - }); - - // Replace url. - strConfig = strConfig.replace('${url}', location.href); - - return JSON.parse(strConfig); -} - -export function loadDataByCustomFetcher(config: StaticDataLoader) { - let parsedConfig = config; - try { - // Not parse template in SSG/SSR. - if (import.meta.renderer === 'client') { - parsedConfig = parseTemplate(config); - } - } catch (error) { - console.error('parse template error: ', error); - } - return dataLoaderFetcher(parsedConfig); -} - -/** - * Handle for different dataLoader. - */ -export function callDataLoader(dataLoader: Loader, requestContext: RequestContext): DataLoaderResult { - if (Array.isArray(dataLoader)) { - const loaders = dataLoader.map((loader, index) => { - return typeof loader === 'object' ? loadDataByCustomFetcher(loader) : dataLoaderDecorator(loader, index)(requestContext); - }); - - return loaders; - } - - if (typeof dataLoader === 'object') { - return loadDataByCustomFetcher(dataLoader); - } - - return dataLoaderDecorator(dataLoader)(requestContext); -} - -const cache = new Map<string, CachedResult>(); - -/** - * Start getData once data-loader.js is ready in client, and set to cache. - */ -function loadInitialDataInClient(loaders: Loaders) { - const context = (window as any).__ICE_APP_CONTEXT__ || {}; - const matchedIds = context.matchedIds || []; - const loaderData = context.loaderData || {}; - const { renderMode } = context; - - const ids = ['__app'].concat(matchedIds); - ids.forEach(id => { - const dataFromSSR = loaderData[id]?.data; - if (dataFromSSR) { - cache.set(renderMode === 'SSG' ? `${id}_ssg` : id, { - value: dataFromSSR, - }); - - if (renderMode === 'SSR') { - return; - } - } - - const dataLoaderConfig = loaders[id]; - - if (dataLoaderConfig) { - const requestContext = getRequestContext(window.location); - const { loader } = dataLoaderConfig; - const promise = callDataLoader(loader, requestContext); - - cache.set(id, { - value: promise, - }); - } - }); -} - -/** - * Init data loader in client side. - * Load initial data and register global loader. - * In order to load data, JavaScript modules, CSS and other assets in parallel. - */ -async function init(loaders: Loaders, options: Options) { - const { - fetcher, - decorator, - runtimeModules, - appExport, - } = options; - - const runtimeApi = { - appContext: { - appExport, - }, - }; - - if (runtimeModules) { - await Promise.all(runtimeModules.map(module => { - const runtimeModule = ((module as CommonJsRuntime).default || module) as StaticRuntimePlugin; - return runtimeModule(runtimeApi); - }).filter(Boolean)); - } - - if (fetcher) { - setFetcher(fetcher); - } - - if (decorator) { - setDecorator(decorator); - } - - try { - loadInitialDataInClient(loaders); - } catch (error) { - console.error('Load initial data error: ', error); - } - - (window as any).__ICE_DATA_LOADER__ = { - getLoader: (id) => { - return loaders[id]; - }, - getData: (id, options: LoadRoutesDataOptions) => { - let result; - - // First render for ssg use data from build time, second render for ssg will use data from data loader. - const cacheKey = `${id}${options?.renderMode === 'SSG' ? '_ssg' : ''}`; - - // In CSR, all dataLoader is called by global data loader to avoid bundle dataLoader in page bundle duplicate. - result = cache.get(cacheKey); - // Always fetch new data after cache is been used. - cache.delete(cacheKey); - - // Already send data request. - if (result) { - return result.value; - } - - const dataLoaderConfig = loaders[id]; - - // No data loader. - if (!dataLoaderConfig) { - return null; - } - - // Call dataLoader. - const { loader } = dataLoaderConfig; - return callDataLoader(loader, options?.requestContext || getRequestContext(window.location)); - }, - }; -} - -export const dataLoader = { - init, -}; - -export default dataLoader; +export { defineDataLoader, defineServerDataLoader, defineStaticDataLoader } from '@ice/runtime-kit'; diff --git a/packages/runtime/src/index.ts b/packages/runtime/src/index.ts index 7371d5cefa..f59bf85084 100644 --- a/packages/runtime/src/index.ts +++ b/packages/runtime/src/index.ts @@ -1,20 +1,27 @@ import type { + RunClientAppOptions, + CreateRoutes, RuntimePlugin, AppContext, - PublicAppContext, AppConfig, RouteConfig, - RouteItem, - ServerContext, - AppProvider, + RouteWrapperConfig, RouteWrapper, RenderMode, Loader, - RouteWrapperConfig, + ServerContext, + AppProvider, + StaticRuntimePlugin, +} from '@ice/runtime-kit'; +import { dataLoader, defineDataLoader, defineServerDataLoader, defineStaticDataLoader, callDataLoader, getRequestContext } from '@ice/runtime-kit'; +import { getAppConfig, defineAppConfig } from '@ice/runtime-kit'; +import type { + PublicAppContext, + RouteItem, + ClientAppRouterProps, } from './types.js'; import Runtime from './runtime.js'; import runClientApp from './runClientApp.js'; -import type { RunClientAppOptions, CreateRoutes } from './runClientApp.js'; import { useAppContext as useInternalAppContext, useAppData, AppContextProvider } from './AppContext.js'; import { getAppData } from './appData.js'; import { useData, useConfig } from './RouteContext.js'; @@ -37,10 +44,7 @@ import type { DataType, MainType, } from './Document.js'; -import dataLoader, { defineDataLoader, defineServerDataLoader, defineStaticDataLoader, callDataLoader } from './dataLoader.js'; -import getRequestContext from './requestContext.js'; import AppErrorBoundary from './AppErrorBoundary.js'; -import getAppConfig, { defineAppConfig } from './appConfig.js'; import { routerHistory as history } from './history.js'; import KeepAliveOutlet from './KeepAliveOutlet.js'; import { useActive } from './Activity.js'; @@ -150,6 +154,7 @@ export { } from 'react-router-dom'; export type { + StaticRuntimePlugin, RuntimePlugin, AppContext, AppConfig, @@ -170,4 +175,5 @@ export type { DataType, MainType, CreateRoutes, + ClientAppRouterProps, }; diff --git a/packages/runtime/src/renderDocument.tsx b/packages/runtime/src/renderDocument.tsx index 87eb3f8bb8..fcb5ebe237 100644 --- a/packages/runtime/src/renderDocument.tsx +++ b/packages/runtime/src/renderDocument.tsx @@ -1,24 +1,23 @@ import * as React from 'react'; import * as ReactDOMServer from 'react-dom/server'; -import getAppConfig from './appConfig.js'; +import { getRequestContext, getAppConfig } from '@ice/runtime-kit'; +import type { ServerContext, AppContext } from '@ice/runtime-kit'; import { AppContextProvider } from './AppContext.js'; import { DocumentContextProvider } from './Document.js'; import addLeadingSlash from './utils/addLeadingSlash.js'; -import getRequestContext from './requestContext.js'; import matchRoutes from './matchRoutes.js'; import getDocumentData from './server/getDocumentData.js'; import getCurrentRoutePath from './utils/getCurrentRoutePath.js'; import { sendResponse, getLocation } from './server/response.js'; import type { - AppContext, RouteItem, RouteMatch, RenderOptions, Response, - ServerContext, } from './types.js'; + interface RenderDocumentOptions { matches: RouteMatch[]; renderOptions: RenderOptions; diff --git a/packages/runtime/src/reportRecoverableError.ts b/packages/runtime/src/reportRecoverableError.ts index 5bb9968fa0..9b18648db8 100644 --- a/packages/runtime/src/reportRecoverableError.ts +++ b/packages/runtime/src/reportRecoverableError.ts @@ -1,4 +1,4 @@ -import type { ErrorStack } from './types.js'; +import type { ErrorStack } from '@ice/runtime-kit'; interface ErrorOptions { ignoreRuntimeWarning?: boolean; diff --git a/packages/runtime/src/routes.tsx b/packages/runtime/src/routes.tsx index fe95deb476..f2855ce865 100644 --- a/packages/runtime/src/routes.tsx +++ b/packages/runtime/src/routes.tsx @@ -3,21 +3,13 @@ import { useRouteError, defer, Await as ReactRouterAwait } from 'react-router-do import type { AwaitProps } from 'react-router-dom'; // eslint-disable-next-line camelcase import type { UNSAFE_DeferredData, LoaderFunctionArgs } from '@remix-run/router'; -import type { - RouteItem, - RouteModules, - RenderMode, - RequestContext, - ComponentModule, - DataLoaderConfig, - DataLoaderOptions, - Loader, -} from './types.js'; +import type { RouteModules, RenderMode, RequestContext, ComponentModule, DataLoaderConfig, DataLoaderOptions, Loader } from '@ice/runtime-kit'; +import { callDataLoader } from '@ice/runtime-kit'; +import { parseSearch } from '@ice/runtime-kit'; +import type { RouteItem } from './types.js'; import RouteWrapper from './RouteWrapper.js'; import { useAppContext } from './AppContext.js'; -import { callDataLoader } from './dataLoader.js'; import { updateRoutesConfig } from './routesConfig.js'; -import { parseSearch } from './requestContext.js'; type RouteModule = Pick<RouteItem, 'id' | 'lazy'>; diff --git a/packages/runtime/src/routesConfig.ts b/packages/runtime/src/routesConfig.ts index ceba4f3aba..a2c8335d40 100644 --- a/packages/runtime/src/routesConfig.ts +++ b/packages/runtime/src/routesConfig.ts @@ -1,4 +1,5 @@ -import type { RouteMatch, LoadersData, LoaderData, RouteConfig } from './types.js'; +import type { RouteConfig, LoadersData, LoaderData } from '@ice/runtime-kit'; +import type { RouteMatch } from './types.js'; export function getMeta( matches: RouteMatch[], diff --git a/packages/runtime/src/runClientApp.tsx b/packages/runtime/src/runClientApp.tsx index f268cb0d2e..18ee466837 100644 --- a/packages/runtime/src/runClientApp.tsx +++ b/packages/runtime/src/runClientApp.tsx @@ -1,43 +1,40 @@ import React from 'react'; import * as ReactDOM from 'react-dom/client'; -import { createHashHistory, createBrowserHistory, createMemoryHistory } from '@remix-run/router'; +import { + createHashHistory, + createBrowserHistory, + createMemoryHistory, +} from '@remix-run/router'; import type { History } from '@remix-run/router'; import type { - AppContext, WindowContext, AppExport, RouteItem, RuntimeModules, AppConfig, AssetsManifest, ClientAppRouterProps, + AppContext, + AppConfig, + AssetsManifest, + RunClientAppOptions, ErrorStack, +} from '@ice/runtime-kit'; +import { setFetcher, setDecorator, getRequestContext, getAppConfig } from '@ice/runtime-kit'; +import type { + WindowContext, + RouteItem, + ClientAppRouterProps, } from './types.js'; -import { createHistory as createHistorySingle, getSingleRoute } from './singleRouter.js'; -import { setHistory } from './history.js'; import Runtime from './runtime.js'; +import { + createHistory as createHistorySingle, + getSingleRoute, +} from './singleRouter.js'; +import { setHistory } from './history.js'; import { getAppData } from './appData.js'; import { getRoutesPath, loadRouteModule } from './routes.js'; -import type { RouteLoaderOptions } from './routes.js'; -import getRequestContext from './requestContext.js'; -import getAppConfig from './appConfig.js'; import matchRoutes from './matchRoutes.js'; -import { setFetcher, setDecorator } from './dataLoader.js'; import ClientRouter from './ClientRouter.js'; -import addLeadingSlash from './utils/addLeadingSlash.js'; import { AppContextProvider } from './AppContext.js'; +import addLeadingSlash from './utils/addLeadingSlash.js'; import { deprecatedHistory } from './utils/deprecatedHistory.js'; import reportRecoverableError from './reportRecoverableError.js'; -export type CreateRoutes = (options: Pick<RouteLoaderOptions, 'renderMode' | 'requestContext'>) => RouteItem[]; - -export interface RunClientAppOptions { - app: AppExport; - runtimeModules: RuntimeModules; - createRoutes?: CreateRoutes; - hydrate?: boolean; - basename?: string; - memoryRouter?: boolean; - runtimeOptions?: Record<string, any>; - dataLoaderFetcher?: Function; - dataLoaderDecorator?: Function; -} - - -export default async function runClientApp(options: RunClientAppOptions) { +export default async function runClientApp(options: RunClientAppOptions<RouteItem>) { const { app, createRoutes, diff --git a/packages/runtime/src/runServerApp.tsx b/packages/runtime/src/runServerApp.tsx index 8205e20487..3515d4b876 100644 --- a/packages/runtime/src/runServerApp.tsx +++ b/packages/runtime/src/runServerApp.tsx @@ -1,11 +1,10 @@ import * as React from 'react'; import type { Location } from 'history'; +import type { AppContext, ServerContext, AppData } from '@ice/runtime-kit'; +import { getAppConfig, getRequestContext } from '@ice/runtime-kit'; import type { OnAllReadyParams, OnShellReadyParams } from './server/streamRender.js'; import type { - AppContext, - ServerContext, RouteMatch, - AppData, ServerAppRouterProps, RenderOptions, Response, @@ -13,11 +12,9 @@ import type { import Runtime from './runtime.js'; import { AppContextProvider } from './AppContext.js'; import { getAppData } from './appData.js'; -import getAppConfig from './appConfig.js'; import { DocumentContextProvider } from './Document.js'; import { loadRouteModules } from './routes.js'; import { pipeToString, renderToNodeStream } from './server/streamRender.js'; -import getRequestContext from './requestContext.js'; import matchRoutes from './matchRoutes.js'; import getCurrentRoutePath from './utils/getCurrentRoutePath.js'; import ServerRouter from './ServerRouter.js'; diff --git a/packages/runtime/src/runtime.tsx b/packages/runtime/src/runtime.tsx index 0c102aa610..c4a506bb92 100644 --- a/packages/runtime/src/runtime.tsx +++ b/packages/runtime/src/runtime.tsx @@ -1,7 +1,6 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom/client'; import type { ComponentType } from 'react'; -import { routerHistory as history } from './history.js'; import type { Renderer, AppContext, @@ -14,14 +13,15 @@ import type { AddWrapper, RouteWrapperConfig, SetRender, - AppRouterProps, ComponentWithChildren, ResponseHandler, -} from './types.js'; +} from '@ice/runtime-kit'; +import type { History } from '@remix-run/router'; +import { routerHistory as history } from './history.js'; +import type { AppRouterProps } from './types.js'; import { useData, useConfig } from './RouteContext.js'; import { useData as useSingleRouterData, useConfig as useSingleRouterConfig } from './singleRouter.js'; import { useAppContext } from './AppContext.js'; - class Runtime { private appContext: AppContext; @@ -73,7 +73,7 @@ class Runtime { public getWrappers = () => this.RouteWrappers; public loadModule(module: RuntimePlugin | StaticRuntimePlugin | CommonJsRuntime) { - let runtimeAPI: RuntimeAPI = { + let runtimeAPI: RuntimeAPI<History> = { addProvider: this.addProvider, addResponseHandler: this.addResponseHandler, getResponseHandlers: this.getResponseHandlers, @@ -88,7 +88,7 @@ class Runtime { history, }; - const runtimeModule = ((module as CommonJsRuntime).default || module) as RuntimePlugin; + const runtimeModule = ((module as CommonJsRuntime).default || module) as RuntimePlugin<any, History>; if (module) { return runtimeModule(runtimeAPI, this.runtimeOptions); } diff --git a/packages/runtime/src/server/getDocumentData.ts b/packages/runtime/src/server/getDocumentData.ts index 875eceaf39..dfc691a45f 100644 --- a/packages/runtime/src/server/getDocumentData.ts +++ b/packages/runtime/src/server/getDocumentData.ts @@ -1,4 +1,5 @@ -import type { DocumentDataLoaderConfig, RequestContext } from '../types.js'; +import type { RequestContext } from '@ice/runtime-kit'; +import type { DocumentDataLoaderConfig } from '../types.js'; interface Options { loaderConfig: DocumentDataLoaderConfig; diff --git a/packages/runtime/src/singleRouter.tsx b/packages/runtime/src/singleRouter.tsx index 06d4b30783..1f6242f373 100644 --- a/packages/runtime/src/singleRouter.tsx +++ b/packages/runtime/src/singleRouter.tsx @@ -5,7 +5,8 @@ import * as React from 'react'; import type { History } from '@remix-run/router'; import type { RouteObject } from 'react-router-dom'; -import type { LoaderData, RouteItem } from './types.js'; +import type { LoaderData } from '@ice/runtime-kit'; +import type { RouteItem } from './types.js'; import { loadRouteModules } from './routes.js'; const Context = React.createContext<LoaderData>(undefined); diff --git a/packages/runtime/src/types.ts b/packages/runtime/src/types.ts index 80f2adeeee..38ebfe009f 100644 --- a/packages/runtime/src/types.ts +++ b/packages/runtime/src/types.ts @@ -1,93 +1,20 @@ -import type { IncomingMessage, ServerResponse } from 'http'; -import type { InitialEntry, AgnosticRouteObject, Location, History, RouterInit, StaticHandlerContext } from '@remix-run/router'; -import type { ComponentType, PropsWithChildren } from 'react'; -import type { HydrationOptions, Root } from 'react-dom/client'; +import type { ComponentType } from 'react'; +import type { AgnosticRouteObject, Location, RouterInit, StaticHandlerContext } from '@remix-run/router'; import type { Params, RouteObject } from 'react-router-dom'; +import type { + AppContext, + AppExport, + ComponentWithChildren, + DataLoaderResult, + LoaderData, + PageConfig, + RenderMode, + RequestContext, + RuntimeModules, + AssetsManifest, +} from '@ice/runtime-kit'; import type { RouteLoaderOptions } from './routes.js'; -import type { RenderToPipeableStreamOptions, NodeWritablePiper } from './server/streamRender.js'; - -type UseConfig = () => RouteConfig<Record<string, any>>; -type UseData = () => RouteData; -type UseAppContext = () => AppContext; - -type VoidFunction = () => void; -type AppLifecycle = 'onShow' | 'onHide' | 'onPageNotFound' | 'onShareAppMessage' | 'onUnhandledRejection' | 'onLaunch' | 'onError' | 'onTabItemClick'; -type App = Partial<{ - rootId: string; - strict: boolean; - errorBoundary: boolean; - onRecoverableError: (error: unknown, errorInfo: ErrorStack) => void; - onBeforeHydrate: () => void; -} & Record<AppLifecycle, VoidFunction>>; - -export interface ErrorStack { - componentStack?: string; - digest?: string; -} - -export type AppData = any; -export type RouteData = any; - -// route.pageConfig return value -export type RouteConfig<T = {}> = T & { - // Support for extends config. - title?: string; - meta?: React.MetaHTMLAttributes<HTMLMetaElement>[]; - links?: React.LinkHTMLAttributes<HTMLLinkElement>[]; - scripts?: React.ScriptHTMLAttributes<HTMLScriptElement>[]; -}; - -export interface AppExport { - default?: AppConfig; - [key: string]: any; - dataLoader?: DataLoaderConfig; -} - -export type DataLoaderResult = (Promise<RouteData> | RouteData) | RouteData; -export type DataLoader = (ctx: RequestContext) => DataLoaderResult; - -export interface StaticDataLoader { - key?: string; - prefetch_type?: string; - api: string; - v: string; - data: any; - ext_headers: Object; -} - -// route.defineDataLoader -// route.defineServerDataLoader -// route.defineStaticDataLoader -export type Loader = DataLoader | StaticDataLoader | Array<DataLoader | StaticDataLoader>; - -// route.pageConfig -export type PageConfig = (args: { data?: RouteData }) => RouteConfig; - -export interface AppConfig { - app?: App; - router?: { - type?: 'hash' | 'browser' | 'memory'; - basename?: string; - initialEntries?: InitialEntry[]; - }; -} - -export interface RoutesConfig { - [routeId: string]: RouteConfig; -} - -export interface RoutesData { - [routeId: string]: RouteData; -} - -export interface DataLoaderOptions { - defer?: boolean; -} - -export interface DataLoaderConfig { - loader: Loader; - options?: DataLoaderOptions; -} +import type { NodeWritablePiper, RenderToPipeableStreamOptions } from './server/streamRender.js'; interface DocumentLoaderOptions { documentOnly?: boolean; @@ -98,38 +25,6 @@ export interface DocumentDataLoaderConfig { loader: DocumentDataLoader; } -export interface LoadersData { - [routeId: string]: LoaderData; -} - -export interface LoaderData { - data?: RouteData; - pageConfig?: RouteConfig; -} - -// useAppContext -export interface AppContext { - appConfig: AppConfig; - appData: any; - documentData?: any; - serverData?: any; - assetsManifest?: AssetsManifest; - loaderData?: LoadersData; - routeModules?: RouteModules; - RouteWrappers?: RouteWrapperConfig[]; - routePath?: string; - matches?: RouteMatch[]; - routes?: RouteItem[]; - documentOnly?: boolean; - matchedIds?: string[]; - appExport?: AppExport; - basename?: string; - downgrade?: boolean; - renderMode?: RenderMode; - requestContext?: RequestContext; - revalidate?: boolean; -} - export type PublicAppContext = Pick< AppContext, 'appConfig' | 'routePath' | 'downgrade' | 'documentOnly' | 'renderMode' @@ -140,31 +35,6 @@ AppContext, 'appData' | 'loaderData' | 'routePath' | 'downgrade' | 'matchedIds' | 'documentOnly' | 'renderMode' | 'serverData' | 'revalidate' >; -export type Renderer = ( - container: Element | Document, - initialChildren: React.ReactNode, - options?: HydrationOptions, -) => Root; - -export interface ServerContext { - req?: IncomingMessage; - res?: ServerResponse; -} - -export interface RequestContext extends ServerContext { - pathname: string; - query: Record<string, any>; -} - -export type ComponentModule = { - default?: ComponentType<any>; - Component?: ComponentType<any>; - staticDataLoader?: DataLoaderConfig; - serverDataLoader?: DataLoaderConfig; - dataLoader?: DataLoaderConfig; - pageConfig?: PageConfig; - [key: string]: any; -}; export type RouteItem = AgnosticRouteObject & { componentName: string; @@ -174,94 +44,10 @@ export type RouteItem = AgnosticRouteObject & { children?: RouteItem[]; }; -export type ComponentWithChildren<P = {}> = ComponentType<PropsWithChildren<P>>; - export type DocumentComponent = ComponentWithChildren<{ pagePath: string; }>; -export interface RouteWrapperConfig { - Wrapper: RouteWrapper; - layout?: boolean; -} - -export type AppProvider = ComponentWithChildren<any>; -export type RouteWrapper = ComponentType<any>; -export type ResponseHandler = ( - req: IncomingMessage, - res: ServerResponse, -) => any | Promise<any>; - -export type SetAppRouter = <T>(AppRouter: ComponentType<T>) => void; -export type GetAppRouter = () => AppProvider; -export type AddProvider = (Provider: AppProvider) => void; -export type SetRender = (render: Renderer) => void; -export type AddWrapper = (wrapper: RouteWrapper, forLayout?: boolean) => void; -export type AddResponseHandler = (handler: ResponseHandler) => void; -export type GetResponseHandlers = () => ResponseHandler[]; - -export interface RouteModules { - [routeId: string]: ComponentModule; -} - -export interface AssetsManifest { - dataLoader?: string; - publicPath: string; - entries: { - [assetPath: string]: string[]; - }; - pages: { - [assetPath: string]: string[]; - }; - assets?: { - [assetPath: string]: string; - }; -} - -export interface RuntimeAPI { - setAppRouter?: SetAppRouter; - getAppRouter: GetAppRouter; - addProvider: AddProvider; - addResponseHandler: AddResponseHandler; - getResponseHandlers: GetResponseHandlers; - setRender: SetRender; - addWrapper: AddWrapper; - appContext: AppContext; - useData: UseData; - useConfig: UseConfig; - useAppContext: UseAppContext; - history: History; -} - -export interface StaticRuntimeAPI { - appContext: { - appExport: AppExport; - }; -} - -export interface RuntimePlugin<T = Record<string, any>> { - ( - apis: RuntimeAPI, - runtimeOptions?: T, - ): Promise<void> | void; -} - -export interface StaticRuntimePlugin<T = Record<string, any>> { - ( - apis: StaticRuntimeAPI, - runtimeOptions?: T, - ): Promise<void> | void; -} - -export interface CommonJsRuntime { - default: RuntimePlugin | StaticRuntimePlugin; -} - -export interface RuntimeModules { - statics?: (StaticRuntimePlugin | CommonJsRuntime)[]; - commons?: (RuntimePlugin | CommonJsRuntime)[]; -} - export interface AppRouterProps { routes?: RouteObject[]; location?: Location; @@ -301,8 +87,6 @@ export interface RouteMatch { route: RouteItem; } -export type RenderMode = 'SSR' | 'SSG' | 'CSR'; - interface Piper { pipe: NodeWritablePiper; fallback: Function; diff --git a/packages/runtime/tests/appConfig.test.ts b/packages/runtime/tests/appConfig.test.ts index bc3c9daee3..3c664467f6 100644 --- a/packages/runtime/tests/appConfig.test.ts +++ b/packages/runtime/tests/appConfig.test.ts @@ -1,5 +1,5 @@ import { expect, it, describe } from 'vitest'; -import getAppConfig, { defineAppConfig } from '../src/appConfig.js'; +import { getAppConfig, defineAppConfig } from '@ice/runtime-kit'; describe('AppConfig', () => { it('getAppConfig', () => { diff --git a/packages/runtime/tests/routes.test.tsx b/packages/runtime/tests/routes.test.tsx index 066360c16a..9c9c02f17e 100644 --- a/packages/runtime/tests/routes.test.tsx +++ b/packages/runtime/tests/routes.test.tsx @@ -155,6 +155,7 @@ describe('routes', () => { }); it('load async route', async () => { + process.env.ICE_CORE_ROUTER = 'true'; const { data: deferredResult } = await createRouteLoader({ routeId: 'home', module: InfoItem, diff --git a/packages/runtime/tests/templateParse.test.ts b/packages/runtime/tests/templateParse.test.ts index 84a4847301..b1f4d9300c 100644 --- a/packages/runtime/tests/templateParse.test.ts +++ b/packages/runtime/tests/templateParse.test.ts @@ -3,7 +3,7 @@ */ import { expect, it, describe, beforeEach, afterEach, vi } from 'vitest'; -import { parseTemplate } from '../src/dataLoader'; +import { parseTemplate } from '@ice/runtime-kit'; describe('parseTemplate', () => { let locationSpy; @@ -128,4 +128,4 @@ describe('parseTemplate', () => { ext_headers: {}, }); }); -}); \ No newline at end of file +}); 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 26d469c334..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; @@ -233,4 +245,19 @@ export interface Config { useDataLoader?: boolean; optimizePackageImports?: 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 de3814851c..4693ffc435 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': @@ -1665,6 +1690,9 @@ importers: '@ice/runtime': specifier: workspace:^ version: link:../runtime + '@ice/runtime-kit': + specifier: workspace:^ + version: link:../runtime-kit '@ice/shared-config': specifier: workspace:* version: link:../shared-config @@ -2403,6 +2431,9 @@ importers: '@ice/jsx-runtime': specifier: ^0.3.1 version: link:../jsx-runtime + '@ice/runtime-kit': + specifier: ^0.1.0 + version: link:../runtime-kit '@ice/shared': specifier: ^1.1.0 version: link:../shared @@ -2441,6 +2472,21 @@ importers: specifier: ^0.13.9 version: 0.13.11 + packages/runtime-kit: + devDependencies: + '@types/react': + specifier: ^18.0.8 + version: 18.0.34 + '@types/react-dom': + specifier: ^18.0.3 + version: 18.0.11 + react: + specifier: ^18.0.0 + version: 18.2.0 + react-dom: + specifier: ^18.0.0 + version: 18.2.0(react@18.2.0) + packages/shared: devDependencies: typescript: @@ -2468,6 +2514,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 @@ -3096,7 +3145,7 @@ packages: '@babel/traverse': 7.23.9 '@babel/types': 7.23.9 convert-source-map: 2.0.0 - debug: 4.3.4 + debug: 4.4.0 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -4520,7 +4569,7 @@ packages: '@babel/helper-split-export-declaration': 7.22.6 '@babel/parser': 7.23.9 '@babel/types': 7.23.9 - debug: 4.3.4 + debug: 4.4.0 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -12077,6 +12126,18 @@ packages: dependencies: ms: 2.1.2 + /debug@4.4.0: + resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.3 + dev: true + /decamelize-keys@1.1.1: resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} engines: {node: '>=0.10.0'} diff --git a/tests/utils/browser.ts b/tests/utils/browser.ts index 5591e9921d..be37c830ce 100644 --- a/tests/utils/browser.ts +++ b/tests/utils/browser.ts @@ -84,7 +84,11 @@ export default class Browser { } async start() { - this.browser = await puppeteer.launch(); + this.browser = await puppeteer.launch( + { + args: ['--no-sandbox', '--disable-setuid-sandbox'], + }, + ); } async close() {