Merge branch 'release/4.0' into feat/environments-api

This commit is contained in:
Keith 2025-02-19 10:24:20 +08:00 committed by GitHub
commit a9341f44f0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
98 changed files with 9310 additions and 8365 deletions

View File

@ -11,34 +11,21 @@ jobs:
strategy:
matrix:
node-version: [16.x, 18.x]
os: [ubuntu-latest, windows-latest]
os: [windows-latest, ubuntu-latest]
fail-fast: false
steps:
- name: Checkout Branch
uses: actions/checkout@v3
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
registry-url: https://registry.npmjs.org/
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
run_install: false
- name: Get pnpm store directory
id: pnpm-cache
run: |
echo "pnpm_cache_dir=$(pnpm store path)" >> "$GITHUB_OUTPUT"
- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: |
${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
.cache
key: ${{ runner.os }}-pnpm-store-node-${{ matrix.node-version }}-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-node-${{ matrix.node-version }}
cache: 'pnpm'
- run: npm run setup
- run: npm run puppeteer:install
- run: npm run dependency:check
- run: npm run lint
- run: npm run test

View File

@ -6,4 +6,4 @@ const { join } = require('path');
module.exports = {
// Changes the cache location for Puppeteer.
cacheDirectory: join(__dirname, '.cache', 'puppeteer'),
};
};

View File

@ -0,0 +1 @@
chrome 55

View File

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

View File

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

View File

@ -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 ? <Component /> : <div>404</div>,
document.getElementById('ice-container'),
);
};
export {
getAppConfig,
runClientApp,
};

View File

@ -0,0 +1,5 @@
import { defineAppConfig } from 'ice';
export default defineAppConfig(() => ({
}));

View File

@ -0,0 +1,22 @@
import { Meta, Title, Links, Main, Scripts } from 'ice';
function Document() {
return (
<html>
<head>
<meta charSet="utf-8" />
<meta name="description" content="ICE Demo" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Title />
<Links />
</head>
<body>
<Main />
<Scripts />
</body>
</html>
);
}
export default Document;

View File

@ -0,0 +1,3 @@
export default function Home() {
return <h1>home</h1>;
}

View File

@ -0,0 +1,3 @@
export default function Index() {
return <h1>index</h1>;
}

View File

@ -0,0 +1,13 @@
import Index from './pages/index';
import Home from './pages/home';
export default () => [
{
path: '/',
component: Index,
},
{
path: '/home',
component: Home,
},
];

View File

@ -0,0 +1 @@
/// <reference types="@ice/app/types" />

View File

@ -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"]
}

View File

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

View File

@ -21,6 +21,6 @@
"@types/react-dom": "^18.0.2",
"express": "^4.19.2",
"tslib": "^2.5.0",
"tsx": "^3.12.1"
"tsx": "3.12.1"
}
}
}

View File

@ -19,8 +19,9 @@
"dependency:check": "tsx ./scripts/dependencyCheck.ts",
"lint": "eslint --cache --ext .js,.jsx,.ts,.tsx ./",
"cov": "vitest run --coverage",
"test": "vitest",
"test": "vitest run",
"changeset": "changeset",
"puppeteer:install": "tsx ./scripts/puppeteer.ts",
"install:frozen": "pnpm install --frozen-lockfile false",
"version": "changeset version && pnpm install:frozen",
"release": "changeset publish",
@ -61,7 +62,7 @@
"react-dom": "^18.2.0",
"rimraf": "^3.0.2",
"stylelint": "^15.10.1",
"tsx": "^3.12.1",
"tsx": "3.12.1",
"typescript": "^4.7.4",
"vitest": "^0.15.2"
},
@ -71,8 +72,11 @@
"packageManager": "pnpm@8.9.2",
"pnpm": {
"patchedDependencies": {
"@rspack/core@0.5.7": "patches/@rspack__core@0.5.7.patch",
"unplugin@1.6.0": "patches/unplugin@1.6.0.patch"
"unplugin@1.6.0": "patches/unplugin@1.6.0.patch",
"@rspack/core@1.2.2": "patches/@rspack__core@1.2.2.patch"
}
}
},
"workspaces": [
"packages/runtime-kit"
]
}

View File

@ -1,5 +1,15 @@
/**
* The following code is modified based on
* https://github.com/pmmmwh/react-refresh-webpack-plugin/blob/0b960573797bf38926937994c481e4fec9ed8aa6/lib/runtime/RefreshUtils.js
*
* MIT Licensed
* Author Michael Mok
* Copyright (c) 2019 Michael Mok
* https://github.com/pmmmwh/react-refresh-webpack-plugin/blob/0b960573797bf38926937994c481e4fec9ed8aa6/LICENSE
*/
/* global __webpack_require__ */
let Refresh = require('react-refresh/runtime');
var Refresh = require('react-refresh/runtime');
/**
* Extracts exports from a webpack module object.
@ -14,19 +24,21 @@ function getModuleExports(moduleId) {
// These are likely runtime or dynamically generated modules.
return {};
}
// eslint-disable-next-line
let maybeModule = __webpack_require__.c[moduleId];
var maybeModule = __webpack_require__.c[moduleId];
if (typeof maybeModule === 'undefined') {
// `moduleId` is available but the module in cache is unavailable,
// which indicates the module is somehow corrupted (e.g. broken Webpacak `module` globals).
// We will warn the user (as this is likely a mistake) and assume they cannot be refreshed.
console.warn(`[React Refresh] Failed to get exports for module: ${moduleId}.`);
console.warn(
'[React Refresh] Failed to get exports for module: ' + moduleId + '.',
);
return {};
}
let exportsOrPromise = maybeModule.exports;
var exportsOrPromise = maybeModule.exports;
if (typeof Promise !== 'undefined' && exportsOrPromise instanceof Promise) {
return exportsOrPromise.then((exports) => {
return exportsOrPromise.then(function (exports) {
return exports;
});
}
@ -42,7 +54,7 @@ function getModuleExports(moduleId) {
* @returns {string[]} A React refresh boundary signature array.
*/
function getReactRefreshBoundarySignature(moduleExports) {
let signature = [];
var signature = [];
signature.push(Refresh.getFamilyByType(moduleExports));
if (moduleExports == null || typeof moduleExports !== 'object') {
@ -50,7 +62,7 @@ function getReactRefreshBoundarySignature(moduleExports) {
return signature;
}
for (let key in moduleExports) {
for (var key in moduleExports) {
if (key === '__esModule') {
continue;
}
@ -71,7 +83,7 @@ function createDebounceUpdate() {
* A cached setTimeout handler.
* @type {number | undefined}
*/
let refreshTimeout;
var refreshTimeout;
/**
* Performs react refresh on a delay and clears the error overlay.
@ -80,7 +92,7 @@ function createDebounceUpdate() {
*/
function enqueueUpdate(callback) {
if (typeof refreshTimeout === 'undefined') {
refreshTimeout = setTimeout(() => {
refreshTimeout = setTimeout(function () {
refreshTimeout = undefined;
Refresh.performReactRefresh();
callback();
@ -110,27 +122,34 @@ function isSafeExport(key) {
key === 'staticDataLoader'
);
}
function isReactRefreshBoundary(moduleExports) {
if (Refresh.isLikelyComponentType(moduleExports)) {
return true;
}
if (moduleExports === undefined || moduleExports === null || typeof moduleExports !== 'object') {
if (
moduleExports === undefined ||
moduleExports === null ||
typeof moduleExports !== 'object'
) {
// Exit if we can't iterate over exports.
return false;
}
let hasExports = false;
let areAllExportsComponents = true;
for (let key in moduleExports) {
var hasExports = false;
var areAllExportsComponents = true;
for (var key in moduleExports) {
hasExports = true;
if (isSafeExport(key)) {
continue;
}
// We can (and have to) safely execute getters here,
// as Webpack manually assigns harmony exports to getters,
// without any side-effects attached.
// Ref: https://github.com/webpack/webpack/blob/b93048643fe74de2a6931755911da1212df55897/lib/MainTemplate.js#L281
let exportValue = moduleExports[key];
var exportValue = moduleExports[key];
if (!Refresh.isLikelyComponentType(exportValue)) {
areAllExportsComponents = false;
}
@ -150,23 +169,27 @@ function isReactRefreshBoundary(moduleExports) {
function registerExportsForReactRefresh(moduleExports, moduleId) {
if (Refresh.isLikelyComponentType(moduleExports)) {
// Register module.exports if it is likely a component
Refresh.register(moduleExports, `${moduleId} %exports%`);
Refresh.register(moduleExports, moduleId + ' %exports%');
}
if (moduleExports === undefined || moduleExports === null || typeof moduleExports !== 'object') {
if (
moduleExports === undefined ||
moduleExports === null ||
typeof moduleExports !== 'object'
) {
// Exit if we can't iterate over the exports.
return;
}
for (let key in moduleExports) {
for (var key in moduleExports) {
// Skip registering the ES Module indicator
if (key === '__esModule') {
continue;
}
let exportValue = moduleExports[key];
var exportValue = moduleExports[key];
if (Refresh.isLikelyComponentType(exportValue)) {
let typeID = `${moduleId} %exports% ${key}`;
var typeID = moduleId + ' %exports% ' + key;
Refresh.register(exportValue, typeID);
}
}
@ -181,14 +204,14 @@ function registerExportsForReactRefresh(moduleExports, moduleId) {
* @returns {boolean} Whether the React refresh boundary should be invalidated.
*/
function shouldInvalidateReactRefreshBoundary(prevExports, nextExports) {
let prevSignature = getReactRefreshBoundarySignature(prevExports);
let nextSignature = getReactRefreshBoundarySignature(nextExports);
var prevSignature = getReactRefreshBoundarySignature(prevExports);
var nextSignature = getReactRefreshBoundarySignature(nextExports);
if (prevSignature.length !== nextSignature.length) {
return true;
}
for (let i = 0; i < nextSignature.length; i += 1) {
for (var i = 0; i < nextSignature.length; i += 1) {
if (prevSignature[i] !== nextSignature[i]) {
return true;
}
@ -197,13 +220,19 @@ function shouldInvalidateReactRefreshBoundary(prevExports, nextExports) {
return false;
}
let enqueueUpdate = createDebounceUpdate();
function executeRuntime(moduleExports, moduleId, webpackHot, refreshOverlay, isTest) {
var enqueueUpdate = createDebounceUpdate();
function executeRuntime(
moduleExports,
moduleId,
webpackHot,
refreshOverlay,
isTest,
) {
registerExportsForReactRefresh(moduleExports, moduleId);
if (webpackHot) {
let isHotUpdate = !!webpackHot.data;
let prevExports;
var isHotUpdate = !!webpackHot.data;
var prevExports;
if (isHotUpdate) {
prevExports = webpackHot.data.prevExports;
}
@ -216,7 +245,7 @@ function executeRuntime(moduleExports, moduleId, webpackHot, refreshOverlay, isT
* @param {*} data A hot module data object from Webpack HMR.
* @returns {void}
*/
(data) => {
function hotDisposeCallback(data) {
// We have to mutate the data object to get data registered and cached
data.prevExports = moduleExports;
},
@ -237,7 +266,7 @@ function executeRuntime(moduleExports, moduleId, webpackHot, refreshOverlay, isT
window.onHotAcceptError(error.message);
}
}
// eslint-disable-next-line
__webpack_require__.c[moduleId].hot.accept(hotErrorHandler);
},
);
@ -254,7 +283,7 @@ function executeRuntime(moduleExports, moduleId, webpackHot, refreshOverlay, isT
* A function to dismiss the error overlay after performing React refresh.
* @returns {void}
*/
() => {
function updateCallback() {
if (typeof refreshOverlay !== 'undefined' && refreshOverlay) {
refreshOverlay.clearRuntimeErrors();
}

View File

@ -1,6 +0,0 @@
"use strict";
const checkVersion = function() {
// Skip binding version check, framework will lock the binding version.
return null;
}
exports.checkVersion = checkVersion;

View File

@ -23,7 +23,7 @@
"html-entities": "^2.3.2",
"core-js": "3.32.0",
"caniuse-lite": "^1.0.30001561",
"chokidar": "3.5.3",
"chokidar": "3.6.0",
"esbuild": "^0.17.16",
"events": "3.3.0",
"jest-worker": "27.5.1",
@ -45,13 +45,16 @@
"zod": "^3.22.3",
"zod-validation-error": "1.2.0",
"terminal-link": "^2.1.1",
"@ice/pack-binding": "0.0.13",
"mime-types": "2.1.35"
"@ice/pack-binding": "1.2.2",
"mime-types": "2.1.35",
"p-retry": "^6.2.0",
"open": "^10.0.3",
"@rspack/lite-tapable": "1.0.1"
},
"devDependencies": {
"@rspack/plugin-react-refresh": "0.5.7",
"@rspack/dev-server": "0.5.7",
"@rspack/core": "0.5.7",
"@rspack/plugin-react-refresh": "1.0.1",
"@rspack/dev-server": "1.0.10",
"@rspack/core": "1.2.2",
"@types/less": "^3.0.3",
"@types/lodash": "^4.14.181",
"@types/webpack-bundle-analyzer": "^4.4.1",
@ -84,9 +87,9 @@
"trusted-cert": "1.1.3",
"webpack": "5.88.2",
"webpack-bundle-analyzer": "4.5.0",
"webpack-dev-server": "4.15.0",
"webpack-dev-server": "5.0.4",
"unplugin": "1.6.0",
"bonjour-service": "^1.0.13",
"bonjour-service": "^1.2.1",
"colorette": "^2.0.10",
"compression": "^1.7.4",
"connect-history-api-fallback": "2.0.0",
@ -95,19 +98,17 @@
"graceful-fs": "4.2.10",
"http-proxy-middleware": "^2.0.3",
"ipaddr.js": "^2.0.1",
"open": "^8.0.9",
"p-retry": "^4.5.0",
"portfinder": "^1.0.28",
"rimraf": "^3.0.2",
"schema-utils": "^4.0.0",
"selfsigned": "^2.0.1",
"rimraf": "^5.0.5",
"schema-utils": "^4.2.0",
"selfsigned": "^2.4.1",
"serve-index": "^1.9.1",
"sockjs": "^0.3.21",
"sockjs": "^0.3.24",
"spdy": "^4.0.2",
"webpack-dev-middleware": "^5.3.4",
"webpack-dev-middleware": "^7.1.0",
"ws": "^8.4.2",
"globby": "13.1.2",
"@pmmmwh/react-refresh-webpack-plugin": "0.5.10",
"@pmmmwh/react-refresh-webpack-plugin": "0.5.15",
"loader-utils": "^2.0.0",
"source-map": "0.8.0-beta.0",
"find-up": "5.0.0",

View File

@ -25,10 +25,26 @@ export const taskExternals = {
const commonDeps = ['terser', 'tapable', 'cssnano', 'terser-webpack-plugin', 'webpack', 'schema-utils',
'lodash', 'postcss-preset-env', 'loader-utils', 'find-up', 'common-path-prefix', 'magic-string'];
const webpackDevServerDeps = ['bonjour-service', 'colorette', 'compression', 'connect-history-api-fallback',
'default-gateway', 'express', 'graceful-fs', 'http-proxy-middleware',
'ipaddr.js', 'open', 'p-retry', 'portfinder', 'rimraf', 'selfsigned', 'serve-index',
'sockjs', 'spdy', 'webpack-dev-middleware', 'ws'];
const webpackDevServerDeps = [
'bonjour-service',
'colorette',
'compression',
'connect-history-api-fallback',
'default-gateway',
'express',
'graceful-fs',
'http-proxy-middleware',
'ipaddr.js',
'portfinder',
'rimraf',
'selfsigned',
'serve-index',
'sockjs',
'spdy',
'webpack-dev-middleware',
'ws',
];
commonDeps.concat(webpackDevServerDeps).forEach(dep => taskExternals[dep] = `@ice/bundles/compiled/${dep}`);
@ -230,29 +246,17 @@ const tasks = [
// Copy the entire directory.
// filter out js files and replace with compiled files.
const filePaths = globbySync(['**/*'], { cwd: pkgPath, ignore: ['node_modules'] });
const filesAddOverwrite = [
'dist/util/bindingVersionCheck.js',
];
filePaths.forEach((filePath) => {
const sourcePath = path.join(pkgPath, filePath);
const targetFilePath = path.join(targetPath, filePath);
fs.ensureDirSync(path.dirname(targetFilePath));
if (path.extname(filePath) === '.js') {
const matched = filesAddOverwrite.some(filePath => {
const matched = sourcePath.split(path.sep).join('/').includes(filePath);
if (matched) {
fs.copyFileSync(path.join(__dirname, `../override/rspack/${path.basename(filePath)}`), targetFilePath);
}
return matched;
});
if (!matched) {
const fileContent = fs.readFileSync(sourcePath, 'utf8');
fs.writeFileSync(
targetFilePath,
replaceDeps(fileContent, ['tapable', 'schema-utils', 'graceful-fs'])
.replace(new RegExp('require\\(["\']@rspack/binding["\']\\)', 'g'), 'require("@ice/pack-binding")'),
);
}
const fileContent = fs.readFileSync(sourcePath, 'utf8');
fs.writeFileSync(
targetFilePath,
fileContent
.replace(new RegExp('require\\(["\']@rspack/binding["\']\\)', 'g'), 'require("@ice/pack-binding")'),
);
} else {
fs.copyFileSync(sourcePath, targetFilePath);
}
@ -276,7 +280,8 @@ const tasks = [
replaceDeps(fileContent, webpackDevServerDeps.concat([...commonDeps, '@rspack/core', 'webpack-dev-server']))
.replace(/webpack-dev-server\//g, '@ice/bundles/compiled/webpack-dev-server/')
.replace(/@rspack\/core\//g, '@ice/bundles/compiled/@rspack/core/')
.replace(/@rspack\/dev-server\//g, '@ice/bundles/compiled/@rspack/dev-server/'),
.replace(/@rspack\/dev-server\//g, '@ice/bundles/compiled/@rspack/dev-server/')
.replace(/"webpack-dev-server"/g, '"@ice/bundles/compiled/webpack-dev-server"'),
);
} else {
fs.copyFileSync(sourcePath, targetPath);

View File

@ -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:*",
@ -97,9 +98,9 @@
"sass": "^1.50.0",
"unplugin": "^1.6.0",
"webpack": "^5.88.0",
"webpack-dev-server": "4.15.0",
"@rspack/core": "0.5.7",
"@rspack/dev-server": "0.5.7"
"webpack-dev-server": "5.0.4",
"@rspack/core": "1.2.2",
"@rspack/dev-server": "1.0.10"
},
"peerDependencies": {
"react": ">=18.0.0",

View File

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

View File

@ -6,7 +6,7 @@ import formatWebpackMessages from '../../utils/formatWebpackMessages.js';
function formatStats(stats: Stats | MultiStats, showWarnings = true) {
const statsData = stats.toJson({
preset: 'errors-warnings',
}) as StatsCompilation;
}) as unknown as StatsCompilation;
const { errors, warnings } = formatWebpackMessages(statsData);

View File

@ -23,6 +23,7 @@ async function bundler(
let dataLoaderCompiler: Compiler;
let devServer: RspackDevServer;
const { rspack } = await import('@ice/bundles/esm/rspack.js');
// Override the type of rspack, because of rspack is imported from pre-compiled bundle.
const rspackConfigs = await getConfig(context, options, rspack as unknown as typeof Rspack);
try {

View File

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

View File

@ -32,7 +32,6 @@ export async function startDevServer(
// Sort by length, shortest path first.
a.split('/').filter(Boolean).length - b.split('/').filter(Boolean).length);
const webTaskConfig = taskConfigs.find(({ name }) => name === WEB);
// @ts-expect-error webpack-dev-server types in Configuration is missing.
const originalDevServer: DevServerConfiguration = webpackConfigs[0].devServer;
const customMiddlewares = originalDevServer?.setupMiddlewares;
const defaultDevServerConfig = await getDefaultServerConfig(originalDevServer, commandArgs);

View File

@ -1,47 +1,46 @@
// 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 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 createSpinner from './utils/createSpinner.js';
import dynamicImport from './utils/dynamicImport.js';
import getRuntimeModules from './utils/getRuntimeModules.js';
import hasDocument from './utils/hasDocument.js';
import { onGetBundlerConfig } from './service/onGetBundlerConfig.js';
import { onGetEnvironmentConfig, environmentConfigContext } from './service/onGetEnvironmentConfig.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));
@ -73,46 +72,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();
@ -154,10 +114,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,
@ -250,17 +207,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,
@ -272,70 +237,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),
},
);
// Render template to runtime directory.
renderTemplate({
ctx,
taskConfig: platformTaskConfig,
routeManifest,
generator,
generatorAPI,
renderData,
runtimeExports,
templateDir,
});
if (platformTaskConfig.config.server?.fallbackEntry) {
// Add fallback entry for server side rendering.
generator.addRenderFile('core/entry.server.ts.ejs', FALLBACK_ENTRY, { hydrate: false });
}
if (typeof userConfig.dataLoader === 'object' && userConfig.dataLoader.fetcher) {
const {
packageName,
method,
} = userConfig.dataLoader.fetcher;
generatorAPI.addDataLoaderImport(method ? {
source: packageName,
alias: {
[method]: 'dataLoaderFetcher',
},
specifier: [method],
} : {
source: packageName,
specifier: '',
});
}
if (multipleServerEntry(userConfig, command)) {
renderMultiEntry({
generator,
renderRoutes: routeManifest.getFlattenRoute(),
routesManifest: routesInfo.routes,
lazy,
});
}
// render template before webpack compile
const renderStart = new Date().getTime();
generator.render();
logger.debug('template render cost:', new Date().getTime() - renderStart);
if (server.onDemand && command === 'start') {
serverRunner = new ServerRunner({
speedup: commandArgs.speedup,
@ -377,6 +303,7 @@ async function createService({ rootDir, command, commandArgs }: CreateServiceOpt
routeManifest,
lazyRoutes: lazy,
ctx,
router: runtimeRouter,
}),
);

View File

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

View File

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

View File

@ -39,27 +39,28 @@ export default function createDataLoaderMiddleware(compiler: Compiler): Middlewa
return next();
}
const publicPath = compiler.options.output?.publicPath
? `${compiler.options.output.publicPath.replace(/\/$/, '')}/`
// Only support string publicPath config for now
? `${(compiler.options.output.publicPath as unknown as string)?.replace(/\/$/, '')}/`
: '/';
const filePath = parse(url || '').pathname;
const filename = filePath?.startsWith(publicPath) ? filePath.slice(publicPath.length) : filePath.slice(1);
// Mark sure the compiler is ready.
await compileTask;
const buffer = compiler.getAsset(filename);
if (!buffer) {
return next();
}
const calcEtag = etag(buffer);
const oldEtag = req.headers['if-none-match'];
// Only data-loader.js will be matched.
res.setHeader('Content-Type', 'text/javascript');
res.setHeader('ETag', calcEtag);
if (calcEtag === oldEtag) {
res.status(304).send();
} else {
res.send(buffer);
}
compiler.outputFileSystem.readFile(filename, (err, data) => {
if (err) {
return next();
}
const calcEtag = etag(data as Buffer);
const oldEtag = req.headers['if-none-match'];
// Only data-loader.js will be matched.
res.setHeader('Content-Type', 'text/javascript');
res.setHeader('ETag', calcEtag);
if (calcEtag === oldEtag) {
res.status(304).send();
} else {
res.send(data);
}
});
};
return {
name: 'data-loader-middleware',

View File

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

View File

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

View File

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

View File

@ -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 { OnGetEnvironmentConfig } from '../service/onGetEnvironmentConfig.js';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -60,7 +60,6 @@ export default class ServerCompilerPlugin {
compiler.hooks.watchRun.tap(pluginName, () => {
this.isCompiling = true;
});
// @ts-expect-error webpack hooks type not match.
compiler.hooks.emit.tapPromise(pluginName, async (compilation: Compilation) => {
this.isCompiling = false;
await this.compileTask(compilation);

View File

@ -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 mergeOptions = (customOptions: Partial<RunClientAppOptions> = {}): RunClientAppOptions => ({
...renderOptions,
...customOptions,
runtimeOptions: {
...renderOptions.runtimeOptions,
...customOptions.runtimeOptions,
},
});
const render = () => {
return app.runApp?.(
(customOptions: Partial<RunClientAppOptions> = {}) => {
const options = mergeOptions(customOptions);
return runClientApp(options);
},
});
};
const renderApp = (appExport: any, customOptions: Partial<RunClientAppOptions>) => {
if (appExport.runApp) {
return appExport.runApp(defaultRender, renderOptions);
} else {
return defaultRender(customOptions);
}
};
const render = (customOptions: Partial<RunClientAppOptions> = {}) => {
return renderApp(app, customOptions);
renderOptions
) ?? runClientApp(renderOptions);
};
<%- entryCode %>

View File

@ -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 = [];

View File

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

View File

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

View File

@ -53,7 +53,7 @@
"@types/accept-language-parser": "^1.5.3",
"@types/react": "^18.0.33",
"cross-env": "^7.0.3",
"webpack-dev-server": "4.15.0"
"webpack-dev-server": "5.0.4"
},
"peerDependencies": {
"@ice/app": "^3.5.1",

View File

@ -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 };
export { useLocale, withLocale };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -92,10 +92,11 @@ export default class NormalModulesPlugin {
const [type, prop] = node.arguments;
if (!type) return;
// @ts-ignore
const componentName = type.name;
// @ts-ignore
if (type.value) {
// @ts-ignore
this.onParseCreateElement?.(type.value, componentConfig);
// @ts-ignore
currentModule.elementNameSet.add(type.value);

View File

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

View File

@ -29,7 +29,7 @@
"build-scripts": "^2.1.2-0",
"esbuild": "^0.17.16",
"webpack": "^5.88.0",
"webpack-dev-server": "4.15.0"
"webpack-dev-server": "5.0.4"
},
"repository": {
"type": "http",

View File

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

View File

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

View File

@ -19,7 +19,7 @@
"@ice/shared-config": "1.3.0"
},
"devDependencies": {
"@rspack/core": "0.5.7"
"@rspack/core": "1.2.2"
},
"scripts": {
"watch": "tsc -w --sourceMap",

View File

@ -115,8 +115,8 @@ const getConfig: GetConfig = async (options) => {
const isDev = mode === 'development';
const absoluteOutputDir = path.isAbsolute(outputDir) ? outputDir : path.join(rootDir, outputDir);
const hashKey = hash === true ? 'hash:8' : (hash || '');
const { rspack: { DefinePlugin, ProvidePlugin, SwcJsMinimizerRspackPlugin, CopyRspackPlugin } } = await import('@ice/bundles/esm/rspack.js');
// @ts-expect-error ManifestPlugin is an custom plugin.
const { rspack: { DefinePlugin, ProvidePlugin, SwcJsMinimizerRspackPlugin, CopyRspackPlugin, ManifestPlugin } } = await import('@ice/bundles/esm/rspack.js');
const cssFilename = `css/${hashKey ? `[name]-[${hashKey}].css` : '[name].css'}`;
// get compile plugins
const compilerWebpackPlugins = getCompilerPlugins(rootDir, {
@ -264,6 +264,11 @@ const getConfig: GetConfig = async (options) => {
extensionAlias: cssExtensionAlias ?? [],
}),
],
generator: {
'css/auto': {
localIdentName,
},
},
},
resolve: {
extensions: ['...', '.ts', '.tsx', '.jsx'],
@ -281,7 +286,6 @@ const getConfig: GetConfig = async (options) => {
minimize: !!minify,
...(splitChunksStrategy ? { splitChunks: splitChunksStrategy } : {}),
},
// @ts-expect-error plugin instance defined by default in not compatible with rspack.
plugins: [
...plugins,
// Unplugin should be compatible with rspack.
@ -291,6 +295,7 @@ const getConfig: GetConfig = async (options) => {
new ProvidePlugin({
process: [require.resolve('process/browser')],
}),
new ManifestPlugin({}),
!!minify && new SwcJsMinimizerRspackPlugin(jsMinimizerPluginOptions),
(enableCopyPlugin || !isDev) && new CopyRspackPlugin({
patterns: [{
@ -305,16 +310,11 @@ const getConfig: GetConfig = async (options) => {
},
globOptions: {
dot: true,
gitignore: true,
ignore: ['.gitignore'],
},
}],
}),
].filter(Boolean),
builtins: {
css: {
modules: { localIdentName },
},
},
stats: 'none',
infrastructureLogging: {
level: 'warn',
@ -336,11 +336,16 @@ const getConfig: GetConfig = async (options) => {
client: {
logging: 'info',
},
https,
server: https ? {
type: 'https',
options: typeof https === 'object' ? https : {},
} : undefined,
...devServer,
setupMiddlewares: middlewares,
},
features: builtinFeatures,
experiments: {
css: true,
},
};
// Compatible with API configureWebpack.
const ctx = {

View File

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

View File

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

View File

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

View File

@ -0,0 +1,4 @@
export * from './appConfig.js';
export * from './dataLoader.js';
export * from './requestContext.js';
export * from './types.js';

View File

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

View File

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

View File

@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"baseUrl": "./",
"rootDir": "src",
"outDir": "esm",
"module": "ES2020",
"moduleResolution": "NodeNext",
},
"include": ["src"]
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
import type { ErrorStack } from './types.js';
import type { ErrorStack } from '@ice/runtime-kit';
interface ErrorOptions {
ignoreRuntimeWarning?: boolean;

View File

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

View File

@ -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[],

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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": "5.0.4",
"@ice/route-manifest": "workspace:*"
},
"scripts": {
"watch": "tsc -w --sourceMap",

View File

@ -3,7 +3,6 @@ import type { RuleSetRule, Configuration, Compiler, WebpackPluginInstance } from
import type {
ProxyConfigArray,
ProxyConfigArrayItem,
ProxyConfigMap,
Middleware,
ServerOptions,
} from 'webpack-dev-server';
@ -14,6 +13,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 +90,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;
@ -137,7 +148,7 @@ export interface Config {
| ((middlewares: Middleware[], devServer: Server) => Middleware[])
| undefined;
proxy?: ProxyConfigArrayItem | ProxyConfigMap | ProxyConfigArray | undefined;
proxy?: ProxyConfigArray;
polyfill?: 'usage' | 'entry' | false;
// You can use `browserslist` to automatically configure supported browsers if set to be true.
@ -233,4 +244,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;
};
};
}

View File

@ -352,7 +352,10 @@ export function getWebpackConfig(options: GetWebpackConfigOptions): Configuratio
logging: 'info',
},
setupMiddlewares: middlewares,
https,
server: https ? {
type: 'https',
options: typeof https === 'object' ? https : {},
} : undefined,
}, devServer || {}) as Config['devServer'],
} as Configuration;
// tnpm / cnpm 安装时webpack 5 的持久缓存无法生成,长时间将导致 OOM

View File

@ -1,66 +0,0 @@
diff --git a/dist/config/adapter.js b/dist/config/adapter.js
index 4eebbcf79cba29acbc0a36d565acc1c08eaf790b..1d87c9be33f69b8dda1436d8e3b970ce0b571112 100644
--- a/dist/config/adapter.js
+++ b/dist/config/adapter.js
@@ -15,6 +15,7 @@ const getRawOptions = (options, compiler) => {
const mode = options.mode;
const experiments = getRawExperiments(options.experiments);
return {
+ features: options.features,
mode,
target: getRawTarget(options.target),
context: options.context,
diff --git a/dist/config/defaults.js b/dist/config/defaults.js
index 1f9f61ff680b6db026c43eb95fe2d78c5f5d8195..56ce90247fd920717d42bc16864e6025fe6dca66 100644
--- a/dist/config/defaults.js
+++ b/dist/config/defaults.js
@@ -53,6 +53,11 @@ const applyRspackOptionsDefaults = (options) => {
applyExperimentsDefaults(options.experiments, {
cache: options.cache
});
+ if (options.features) {
+ applyFeaturesDefaults(options.features);
+ } else {
+ D(options, 'features', {});
+ }
applySnapshotDefaults(options.snapshot, { production });
applyModuleDefaults(options.module, {
// syncWebAssembly: options.experiments.syncWebAssembly,
@@ -103,6 +108,13 @@ const applyInfrastructureLoggingDefaults = (infrastructureLogging) => {
D(infrastructureLogging, "colors", tty);
D(infrastructureLogging, "appendOnly", !tty);
};
+const applyFeaturesDefaults = (features) => {
+ D(features, 'split_chunks_strategy', {});
+ if (typeof features.split_chunks_strategy === 'object') {
+ D(features.split_chunks_strategy, 'name', '');
+ D(features.split_chunks_strategy, 'topLevelFrameworks', []);
+ }
+};
const applyExperimentsDefaults = (experiments, { cache }) => {
D(experiments, "lazyCompilation", false);
D(experiments, "asyncWebAssembly", false);
diff --git a/dist/config/normalization.js b/dist/config/normalization.js
index 696eddf849f8a2f2c66237cb37db767f4dfe20ca..7e89b6091471de8287ce0785042676873141cfbe 100644
--- a/dist/config/normalization.js
+++ b/dist/config/normalization.js
@@ -12,6 +12,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
exports.getNormalizedRspackOptions = void 0;
const getNormalizedRspackOptions = (config) => {
return {
+ features: config.features,
ignoreWarnings: config.ignoreWarnings !== undefined
? config.ignoreWarnings.map(ignore => {
if (typeof ignore === "function") {
diff --git a/dist/config/zod.js b/dist/config/zod.js
index a81260f08e4e7de64ff3c1f8769a658db4c73883..df3184bad831922f64f3c41b64bce08fcdf5b3cd 100644
--- a/dist/config/zod.js
+++ b/dist/config/zod.js
@@ -775,5 +775,6 @@ exports.rspackOptions = zod_1.z.strictObject({
builtins: builtins.optional(),
module: moduleOptions.optional(),
profile: profile.optional(),
- bail: bail.optional()
+ bail: bail.optional(),
+ features: zod_1.z.custom().optional(),
});

View File

@ -0,0 +1,41 @@
diff --git a/dist/index.js b/dist/index.js
index 57955f085ab0ccf9cb514390a6d2472531a1e6c1..d333b82dcbc299c4580288955df54f1d77ff625b 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -694,6 +694,7 @@ __export(src_exports, {
Compiler: () => Compiler,
ContextReplacementPlugin: () => ContextReplacementPlugin,
CopyRspackPlugin: () => CopyRspackPlugin,
+ ManifestPlugin: () => ManifestPlugin,
CssExtractRspackPlugin: () => CssExtractRspackPlugin,
DefinePlugin: () => DefinePlugin,
DllPlugin: () => DllPlugin,
@@ -760,6 +761,7 @@ __export(exports_exports, {
Compiler: () => Compiler,
ContextReplacementPlugin: () => ContextReplacementPlugin,
CopyRspackPlugin: () => CopyRspackPlugin,
+ ManifestPlugin: () => ManifestPlugin,
CssExtractRspackPlugin: () => CssExtractRspackPlugin,
DefinePlugin: () => DefinePlugin,
DllPlugin: () => DllPlugin,
@@ -5015,6 +5017,12 @@ var CopyRspackPlugin = create2(
}
);
+// Customize builtin plugin.
+var ManifestPlugin = create2(
+ import_binding10.BuiltinPluginName.ManifestPlugin,
+ () => {}
+);
+
// src/builtin-plugin/css-extract/index.ts
var import_binding11 = require("@rspack/binding");
var import_node_path3 = require("path");
@@ -20545,6 +20553,7 @@ module.exports = rspack;
Compiler,
ContextReplacementPlugin,
CopyRspackPlugin,
+ ManifestPlugin,
CssExtractRspackPlugin,
DefinePlugin,
DllPlugin,

File diff suppressed because it is too large Load Diff

11
scripts/puppeteer.ts Normal file
View File

@ -0,0 +1,11 @@
import { exec } from '@actions/exec';
const installPuppeteer = async () => {
await exec('node', [
require.resolve('puppeteer/install.js'),
]);
};
(async () => {
await installPuppeteer();
})();

View File

@ -41,7 +41,8 @@ describe(`start ${example} in speedup mode`, () => {
let page: Page;
let browser: Browser;
test('open /', async () => {
const { devServer, port } = await startFixture(example, { speedup: true });
// Close speed up mode is win32 system for now.
const { devServer, port } = await startFixture(example, { speedup: process.platform !== 'win32' });
const res = await setupStartBrowser({ server: devServer, port });
page = res.page;
browser = res.browser;
@ -55,6 +56,6 @@ describe(`start ${example} in speedup mode`, () => {
describe(`build ${example} in speedup mode`, () => {
test('open /', async () => {
await buildFixture(example, { speedup: true });
await buildFixture(example, { speedup: process.platform !== 'win32' });
});
});

View File

@ -97,9 +97,9 @@ export default class Browser {
// @ts-ignore
if (this.server.stop) {
// @ts-ignore
this.server.stop();
await this.server.stop();
} else {
this.server.close();
await this.server.close();
}
}