Merge pull request #6148 from alibaba/release/next

Release 3.2.0
This commit is contained in:
ClarkXia 2023-05-17 10:38:05 +08:00 committed by GitHub
commit df7a474973
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
247 changed files with 6212 additions and 2940 deletions

View File

@ -7,65 +7,34 @@ find the full documentation for it [in our repository](https://github.com/change
We have a quick list of common questions to get you started engaging with this project in
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
## Add A Changeset
After you have completed a feature or fixed a bug, you need to do three things:
Read the [documentation](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md) for more detail.
- Select which packages should be released
- Bump released packages version
- Write Changelog for the released packages
## Publish Snapshot Version
You can follow these steps:
- Run the command line script `npm run changeset`
- Select the packages you want to include in the changeset using `↑` and `↓` to navigate to packages, and `space` to select a package. Hit enter when all desired packages are selected.
- You will be prompted to select a bump type for each selected package. Select an appropriate bump type for the changes made. See here for information on semver versioning
- Your final prompt will be to provide a message to go alongside the changeset. This will be written into the changelog when the next release occurs.
> NOTE: It is useful for you to release a version to test your package on your current branch.
After that, you should commit changes to the remote repository. For example:
Run the following command to publish the snapshot version:
```bash
$ git status
On branch test-3
Untracked files:
(use "git add <file>..." to include in what will be committed)
.changeset/curvy-jobs-fly.md
$ git commit -am "chore: add changeset"
$ git push
$ npm run release:snapshot
```
For more detail, please see [this documentation](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).
After that, you don't need to commit the change to the branch.
## Publish Beta Version
> NOTE: You must add a changeset first before publishing beta version.
> NOTE: Recommend you to publish a beta version on a new branch(such as `release-beta`) which is checked out from the `release*` branch.
Run the following command to publish the beta version
Run the following command to publish the beta version:
```bash
$ npm run release:beta
```
Then, we need to commit changes to the remote repository. For example:
```bash
$ git status
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: packages/a/CHANGELOG.md
modified: packages/a/package.json
modified: pnpm-workspace.yaml
Untracked files:
(use "git add <file>..." to include in what will be committed)
.changeset/pre.json
$ git commit -am "chore: beta version"
$ git push
```
Then, we need to commit the changes to the branch.
For more detail, please see this [documentation](https://github.com/changesets/changesets/blob/main/docs/prereleases.md).
@ -75,4 +44,4 @@ GitHub bot will automatically create a PR to update the latest versions for the
<img width="701" alt="image" src="https://user-images.githubusercontent.com/44047106/215980879-965da73d-317e-4576-81ee-118e11bcc2d4.png">
What we need to do is merge the PR to the `release*` branch.
What we need to do is to merge the PR to the `release*` branch. After the `release*` branch was merged to the master branch, the versions will be released automatically.

View File

@ -10,5 +10,9 @@
"ignore": [
"@examples/*",
"ice-website-v3"
]
],
"snapshot": {
"useCalculatedVersion": true,
"prereleaseTemplate": "{tag}-{commit}-{datetime}"
}
}

1
.gitignore vendored
View File

@ -40,6 +40,7 @@ yalc.lock
# Packages
packages/*/lib/
packages/*/esm/
packages/*/es2017/
# temp folder .ice
examples/*/.ice

3
.npmrc
View File

@ -1 +1,2 @@
strict-peer-dependencies=false
strict-peer-dependencies=false
registry=https://registry.npmjs.org/

View File

@ -24,6 +24,6 @@
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.2",
"speed-measure-webpack-plugin": "^1.5.0",
"webpack": "^5.73.0"
"webpack": "^5.80.0"
}
}

View File

@ -23,6 +23,6 @@
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.2",
"speed-measure-webpack-plugin": "^1.5.0",
"webpack": "^5.73.0"
"webpack": "^5.80.0"
}
}

View File

@ -21,6 +21,6 @@
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"speed-measure-webpack-plugin": "^1.5.0",
"webpack": "^5.73.0"
"webpack": "^5.80.0"
}
}

View File

@ -24,6 +24,6 @@
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.2",
"speed-measure-webpack-plugin": "^1.5.0",
"webpack": "^5.73.0"
"webpack": "^5.80.0"
}
}

View File

@ -13,4 +13,4 @@ export default function FrameworkLayout({ children }) {
</div>
</>
);
}
}

View File

@ -7,4 +7,4 @@ export default function About() {
<Link to="/">home</Link>
</>
);
}
}

View File

@ -1,10 +1,9 @@
import { defineConfig } from '@ice/app';
import miniapp from '@ice/plugin-miniapp';
console.log('process.env.FOO', process.env.FOO);
export default defineConfig({
ssg: false,
hash: true,
// minify: true,
minify: true,
dropLogLevel: 'trace',
outputDir: 'build/wechat',
alias: {
@ -12,11 +11,11 @@ export default defineConfig({
},
plugins: [miniapp({
nativeConfig: {
appid: 'tourist'
}
appid: 'tourist',
},
})],
// eslint: false,
define: {
ASSETS_VERSION: '1.0.1'
}
ASSETS_VERSION: JSON.stringify('1.0.1'),
},
});

View File

@ -7,7 +7,8 @@
"start:wechat": "ice start --target wechat-miniprogram",
"start:ali": "ice start --target ali-miniapp",
"build": "ice build",
"build:wechat": "ice build --target wechat-miniprogram"
"build:wechat": "ice build --target wechat-miniprogram",
"build:ali": "ice build --target ali-miniapp"
},
"description": "",
"author": "",
@ -27,6 +28,6 @@
"@types/react-dom": "^18.0.2",
"browserslist": "^4.19.3",
"speed-measure-webpack-plugin": "^1.5.0",
"webpack": "^5.73.0"
"webpack": "^5.80.0"
}
}

View File

@ -0,0 +1,29 @@
# ice.js example miniapp
## feature list
### App
- dataLoader
- miniappManifest
- miniappLifecycles
### Pages
- index
- Link component
- history
- useAppData
- useData
- second
- useSearchParams
- page config
- page lifecycles
- third/index
- html tags
- use json
- use png
- fourth
- define
- process env
- miniapp native components

View File

@ -1,4 +1,5 @@
import { defineAppConfig, defineDataLoader } from 'ice';
import { defineMiniappConfig } from '@ice/plugin-miniapp/runtime';
export const dataLoader = defineDataLoader(() => {
return new Promise((resolve) => {
@ -17,17 +18,35 @@ export const miniappManifest = {
},
routes: [
'index',
'about',
'second/profile',
'second',
'third/index',
'third/test',
'fourth',
],
};
export const miniappLifecycles = defineMiniappConfig(() => {
return {
onLaunch(options) {
console.log('[App] on launch', options);
},
onShow(options) {
console.log('[App] on show', options);
},
onShareAppMessage(options) {
// Only works in ali miniapp
console.log('[App] onShareAppMessage', options);
return {
title: 'test',
path: 'pages/index',
desc: '呵呵',
};
},
};
});
export default defineAppConfig(() => {
return {
app: {
strict: true,
errorBoundary: true,
},
};

View File

@ -1,27 +0,0 @@
import { defineDataLoader } from 'ice';
export default function Home() {
return (
<>
<view className="title">About</view>
</>
);
}
export function pageConfig() {
return {
title: 'About',
};
}
export const dataLoader = defineDataLoader((options) => {
// options comes from onLoad in miniapp page config
console.log('about page options.pathname', options.pathname);
console.log('about page options.query', options.query);
return new Promise((resolve) => {
setTimeout(() => {
resolve({
name: 'About',
});
}, 1 * 100);
});
});

View File

@ -0,0 +1,15 @@
declare const ASSETS_VERSION: string;
export default function Fourth() {
console.log('[Fourth] ASSETS_VERSION', ASSETS_VERSION);
return (
<>
<view>Fourth Page</view>
<button> button </button>
{/* @ts-ignore */}
<progress percent={50}> progress </progress>
<switch type={'switch'}> switch </switch>
</>
);
}

View File

@ -1,61 +1,28 @@
import './index.scss';
import { useAppData, useConfig, useData, Link, useSearchParams, history, defineDataLoader } from 'ice';
import json from '../test.json';
import url from './ice.png';
import { useAppData, useData, defineDataLoader, history, Link } from 'ice';
export default function Home() {
console.log('process.env', process.env.ICE_APP_ID);
export default function Index() {
const appData = useAppData();
console.log('🚀 ~ file: index.tsx ~ line 6 ~ Home ~ appData', appData);
const config = useConfig();
console.log('🚀 ~ file: index.tsx ~ line 8 ~ Home ~ config', config);
const data = useData();
console.log('🚀 ~ file: index.tsx ~ line 10 ~ Home ~ data', data);
console.log('json', json);
const [params] = useSearchParams();
console.log('🚀 ~ file: index.tsx ~ line 15 ~ Home ~ params', params);
// @ts-ignore
console.log('ASSETS_VERSION', ASSETS_VERSION);
function onClick() {
console.log('123');
}
console.log('[Index] use app data', appData);
console.log('[Index] use data', data);
return (
<>
<view className="title" onClick={() => { console.log(123123); }}>Home Page</view>
{/* @ts-ignore */}
<image mode="aspectFit" src="https://v3.ice.work/img/logo.png" />
<view className="data">
<view>foo: </view>
<view>users:</view>
<view>userInfo: </view>
{/* @ts-ignore */}
<image src="https://v3.ice.work/img/logo.png" />
{/* @ts-ignore */}
<image src={url} />
<view onClick={() => { history.push('/?hello=computer'); }}>history </view>
<view onClick={() => { history.push('/about?hello=computer'); }}>history about </view>
<view onClick={() => { history.push('/second/profile?hello=computer'); }}>history second/profile </view>
<view onClick={() => { history.push('/third'); }}>history third/index </view>
<view onClick={() => { history.push('/third/test'); }}>history third/test </view>
<Link to="/?hello=world">Link </Link>
<Link to="/about">Link about </Link>
<Link to="/third">Link third/index </Link>
<Link to="/third/test">Link third/test </Link>
<div onClick={onClick}> div </div>
<view onClick={onClick}> view </view>
<view>Index Page</view>
<view onClick={() => { history.push('/second?name=chris'); }}>history second </view>
<view onClick={() => { history.push('/third'); }}>history third/index </view>
<Link to="/fourth">Link fourth </Link>
</view>
</>
);
}
export function pageConfig() {
return {
title: 'Home',
};
}
export const dataLoader = defineDataLoader((options) => {
// options comes from onLoad in miniapp page config
console.log('index page options.pathname', options.pathname);
console.log('index page options.query', options.query);
console.log('[Index] data loader options', options);
return new Promise((resolve) => {
setTimeout(() => {
resolve({

View File

@ -0,0 +1,54 @@
// import './index.scss';
import { useState } from 'react';
import { useConfig, useSearchParams, usePageLifecycle } from 'ice';
export default function Second() {
const config = useConfig();
console.log('[Second] use config', config);
const [params] = useSearchParams();
console.log('[Second] use search params', params);
const [count, setCount] = useState(0);
usePageLifecycle('onLoad', (options) => {
console.log('[Second] page onLoad', options);
});
usePageLifecycle('onReady', () => {
console.log('[Second] page onReady');
});
usePageLifecycle('onShow', () => {
console.log('[Second] page onShow');
});
usePageLifecycle('onHide', () => {
console.log('[Second] page onHide');
});
usePageLifecycle('onShareAppMessage', (options) => {
console.log('[Second] onShareAppMessage', options);
return {
title: '123',
path: 'pages/index',
};
});
usePageLifecycle('onShareTimeline', (options) => {
console.log('[Second] onShareTimeline', options);
return {
title: '456',
};
});
return (<view>
<view>Second Page</view>
<view>{count}</view>
<view onClick={() => { setCount(count + 1); }}>+</view>
</view>
);
}
export function pageConfig() {
return {
title: 'Home',
nativeEvents: [
'onShareAppMessage',
'onShareTimeline',
],
};
}

View File

@ -1,4 +0,0 @@
.profile {
font-size: 50rpx;
color: red;
}

View File

@ -1,15 +0,0 @@
import styles from './profile.module.css';
export default function Profile() {
return (
<>
<view className={styles.profile}>second profile</view>
</>
);
}
export function pageConfig() {
return {
title: 'Second profile',
};
}

View File

@ -1,7 +1,14 @@
import url from '../ice.png';
import json from './test.json';
export default function Third() {
console.log('[Third] get json', json);
return (
<>
<view className="title">Third</view>
<view className="title">Third Page</view>
<div> div </div>
<span> span </span>
<img src={url} />
</>
);
}

View File

@ -1,8 +0,0 @@
export default function Test() {
return (
<>
<view className="title">Third test</view>
</>
);
}

View File

@ -12,3 +12,14 @@ declare module '*.module.scss' {
const classes: { [key: string]: string };
export default classes;
}
declare module '*.png' {
const value: string;
export default value;
}
declare module '*.json' {
const value: Record<string, any>;
export default value;
}

View File

@ -2,7 +2,6 @@ import { defineConfig } from '@ice/app';
export default defineConfig(() => ({
ssr: true,
ssg: true,
define: {
'process.env.NODE_ENV': JSON.stringify(true),
},

View File

@ -8,5 +8,6 @@ export default defineConfig(() => ({
})],
server: {
bundle: true,
format: 'cjs',
},
}));

View File

@ -24,6 +24,6 @@
"devDependencies": {
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.2",
"webpack": "^5.73.0"
"webpack": "^5.80.0"
}
}

View File

@ -26,6 +26,6 @@
"devDependencies": {
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.2",
"webpack": "^5.73.0"
"webpack": "^5.80.0"
}
}
}

View File

@ -0,0 +1,5 @@
import { defineConfig } from '@ice/app';
export default defineConfig(() => ({
ssg: false,
}));

View File

@ -0,0 +1,22 @@
{
"name": "@examples/with-antd5",
"private": true,
"version": "1.0.0",
"scripts": {
"start": "ice start",
"build": "ice build"
},
"dependencies": {
"@ice/app": "workspace:*",
"@ice/runtime": "workspace:*",
"antd": "^5.0.0",
"dayjs": "^1.11.7",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react-intl": "^6.3.2"
},
"devDependencies": {
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.2"
}
}

View File

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

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,10 @@
export const messages: Record<string, any> = {
en: {
changeLanguageTitle: 'Change locale:',
indexTitle: 'Index',
},
'zh-cn': {
changeLanguageTitle: '修改语言:',
indexTitle: '首页',
},
};

View File

@ -0,0 +1,12 @@
import { DatePicker, Pagination } from 'antd';
import { FormattedMessage } from 'react-intl';
export default function Index() {
return (
<div>
<h2><FormattedMessage id="indexTitle" /></h2>
<Pagination defaultCurrent={1} total={50} showSizeChanger />
<DatePicker />
</div>
);
}

View File

@ -0,0 +1,47 @@
import { Outlet } from 'ice';
import { useState } from 'react';
import { IntlProvider, FormattedMessage } from 'react-intl';
import { ConfigProvider, Radio } from 'antd';
import type { RadioChangeEvent } from 'antd';
import enUS from 'antd/locale/en_US';
import zhCN from 'antd/locale/zh_CN';
import type { Locale } from 'antd/es/locale';
import * as dayjs from 'dayjs';
import { messages } from '@/locales';
import 'dayjs/locale/zh-cn';
export default function Layout() {
const [locale, setLocale] = useState<Locale>(enUS);
const changeLocale = (e: RadioChangeEvent) => {
const localeValue = e.target.value;
setLocale(localeValue);
if (localeValue) {
dayjs.locale('zh-cn');
} else {
dayjs.locale('en');
}
};
return (
<main>
<IntlProvider locale={locale.locale} messages={messages[locale.locale]}>
<div style={{ marginBottom: 16 }}>
<span style={{ marginRight: 16 }}><FormattedMessage id="changeLanguageTitle" /></span>
<Radio.Group value={locale} onChange={changeLocale}>
<Radio.Button key="en" value={enUS}>
English
</Radio.Button>
<Radio.Button key="cn" value={zhCN}>
</Radio.Button>
</Radio.Group>
</div>
<ConfigProvider locale={locale}>
<Outlet />
</ConfigProvider>
</IntlProvider>
</main>
);
}

1
examples/with-antd5/src/typings.d.ts vendored Normal file
View File

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

View File

@ -0,0 +1,17 @@
{
"compilerOptions": {
"baseUrl": "./",
"module": "ESNext",
"target": "ESNext",
"lib": ["DOM", "ESNext", "DOM.Iterable"],
"jsx": "react-jsx",
"moduleResolution": "node",
"strict": true,
"skipLibCheck": true,
"paths": {
"@/*": ["./src/*"],
"ice": [".ice"]
}
},
"include": ["src", ".ice"],
}

View File

@ -0,0 +1,35 @@
import { useData, defineDataLoader, Await } from 'ice';
import styles from './index.module.css';
export default function Home() {
const data = useData();
return (
<>
<h2 className={styles.title}>With dataLoader</h2>
<Await resolve={data} fallback={<p>Loading item info...</p>} errorElement={<p>Error loading!</p>}>
{(itemInfo) => {
return <p>Item id is <span id="itemId">{itemInfo.id}</span></p>;
}}
</Await>
</>
);
}
export function pageConfig() {
return {
title: 'Home',
};
}
export const dataLoader = defineDataLoader(async () => {
const promise = new Promise((resolve) => {
setTimeout(() => {
resolve({
id: 1233,
});
}, 100);
});
return await promise;
}, { defer: true });

View File

@ -0,0 +1,52 @@
import { useData, defineDataLoader, Await } from 'ice';
import styles from './index.module.css';
export default function Home() {
const data = useData();
return (
<>
<h2 className={styles.title}>With dataLoader</h2>
<Await resolve={data[0]} fallback={<p>Loading item info...</p>} errorElement={<p>Error loading!</p>}>
{(itemInfo) => {
return <p>Item id is {itemInfo.id}</p>;
}}
</Await>
<Await resolve={data[1]} fallback={<p>Loading item info...</p>} errorElement={<p>Error loading!</p>}>
{(itemInfo) => {
return <p>Item price is {itemInfo.price}</p>;
}}
</Await>
</>
);
}
export function pageConfig() {
return {
title: 'Home',
};
}
export const dataLoader = defineDataLoader([
async () => {
const promise = new Promise((resolve) => {
setTimeout(() => {
resolve({
id: 1233,
});
}, 100);
});
return await promise;
},
async () => {
const promise = new Promise((resolve) => {
setTimeout(() => {
resolve({
price: 9.99,
});
}, 2000);
});
return await promise;
},
], { defer: true });

View File

@ -0,0 +1,45 @@
import { useData, defineDataLoader, defineServerDataLoader, Await } from 'ice';
import styles from './index.module.css';
export default function Home() {
const data = useData();
return (
<>
<h2 className={styles.title}>With dataLoader</h2>
<Await resolve={data} fallback={<p>Loading item info...</p>} errorElement={<p>Error loading!</p>}>
{(itemInfo) => {
return <p>Item id is {itemInfo.id}</p>;
}}
</Await>
</>
);
}
export function pageConfig() {
return {
title: 'Home',
};
}
export const dataLoader = defineDataLoader(async () => {
const promise = new Promise((resolve) => {
setTimeout(() => {
resolve({
id: 1233,
});
}, 100);
});
return await promise;
}, { defer: true });
export const serverDataLoader = defineServerDataLoader(async () => {
const promise = new Promise((resolve) => {
setTimeout(() => {
resolve({
id: 1233,
});
}, 100);
});
return await promise;
});

View File

@ -16,4 +16,5 @@ export default defineConfig({
locales: ['af'],
}),
],
ssg: false,
});

View File

@ -12,16 +12,17 @@
"dependencies": {
"@alifd/next": "^1.25.49",
"@ice/runtime": "workspace:*",
"moment": "^2.29.4",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"moment": "^2.29.4"
"react-intl": "^6.3.2"
},
"devDependencies": {
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.2",
"@ice/app": "workspace:*",
"@ice/plugin-css-assets-local": "workspace:*",
"@ice/plugin-fusion": "workspace:*",
"@ice/plugin-moment-locales": "workspace:*"
"@ice/plugin-moment-locales": "workspace:*",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.2"
}
}

View File

@ -0,0 +1,8 @@
export const messages: Record<string, any> = {
en: {
buttonText: 'Button',
},
'zh-cn': {
buttonText: '按钮',
},
};

View File

@ -1,11 +1,15 @@
import { Button } from '@alifd/next';
import { Button, DatePicker } from '@alifd/next';
import '@alifd/next/dist/next.css';
import { FormattedMessage } from 'react-intl';
export default function Home() {
return (
<div>
<h1>with fusion</h1>
<Button type="primary">Button</Button>
<DatePicker />
<Button type="primary">
<FormattedMessage id="buttonText" />
</Button>
</div>
);
}

View File

@ -0,0 +1,46 @@
import { Outlet } from 'ice';
import { useState } from 'react';
import { ConfigProvider, Radio } from '@alifd/next';
import enUS from '@alifd/next/lib/locale/en-us';
import zhCN from '@alifd/next/lib/locale/zh-cn';
import { IntlProvider } from 'react-intl';
import { messages } from '@/locales';
const localeMap = new Map([
['en', enUS],
['zh-cn', zhCN],
]);
export default function Layout() {
const [locale, setLocale] = useState('en');
const list = [
{
value: 'en',
label: 'English',
},
{
value: 'zh-cn',
label: '中文',
},
];
function changeLocale(value: string) {
setLocale(value);
}
return (
<main>
<IntlProvider locale={locale} messages={messages[locale]}>
<Radio.Group
dataSource={list}
shape="button"
value={locale}
onChange={changeLocale}
/>
<ConfigProvider locale={localeMap.get(locale)}>
<Outlet />
</ConfigProvider>
</IntlProvider>
</main>
);
}

View File

@ -0,0 +1,13 @@
import { defineConfig } from '@ice/app';
import i18n from '@ice/plugin-i18n';
export default defineConfig({
plugins: [
i18n({
locales: ['zh-CN', 'en-US'],
defaultLocale: 'zh-CN',
autoRedirect: true,
}),
],
ssr: true,
});

View File

@ -0,0 +1,26 @@
{
"name": "@examples/with-i18n",
"private": true,
"version": "1.0.0",
"scripts": {
"start": "ice start",
"build": "ice build",
"serve": "tsx server.mts"
},
"dependencies": {
"@ice/runtime": "workspace:*",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react-intl": "^6.3.2"
},
"devDependencies": {
"@ice/app": "workspace:*",
"@ice/plugin-i18n": "workspace:*",
"@types/express": "^4.17.14",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.2",
"express": "^4.17.3",
"tslib": "^2.5.0",
"tsx": "^3.12.1"
}
}

View File

@ -0,0 +1,27 @@
import express from 'express';
import { renderToHTML } from './build/server/index.mjs';
const app = express();
const port = 4000;
const basename = '/app';
app.use(express.static('build', {}));
app.use(async (req, res) => {
const { statusCode, statusText, headers, value: body } = await renderToHTML({ req, res }, { basename });
res.statusCode = statusCode;
res.statusMessage = statusText;
Object.entries((headers || {}) as Record<string, string>).forEach(([name, value]) => {
res.setHeader(name, value);
});
if (body && req.method !== 'HEAD') {
res.end(body);
} else {
res.end();
}
});
app.listen(port, () => {
console.log(`App listening on http://localhost:${port}${basename}`);
});

View File

@ -0,0 +1,12 @@
import { defineAppConfig } from 'ice';
import { defineI18nConfig } from '@ice/plugin-i18n/types';
export default defineAppConfig(() => ({
router: {
basename: '/app',
},
}));
export const i18nConfig = defineI18nConfig(() => ({
// disabledCookie: true,
}));

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,8 @@
export const messages: Record<string, any> = {
'en-US': {
buttonText: 'Normal Button',
},
'zh-CN': {
buttonText: '普通按钮',
},
};

View File

@ -0,0 +1,13 @@
import { Link } from 'ice';
export default function BlogA() {
return (
<>
<h2>Blog A</h2>
<ul>
<li><Link to="/">Index</Link></li>
<li><Link to="/blog">Blog</Link></li>
</ul>
</>
);
}

View File

@ -0,0 +1,13 @@
import { Link } from 'ice';
export default function Blog() {
return (
<>
<h2>Blog</h2>
<ul>
<li><Link to="/">Index</Link></li>
<li><Link to="/blog/a">Blog A</Link></li>
</ul>
</>
);
}

View File

@ -0,0 +1,15 @@
import { Link } from 'ice';
import { FormattedMessage } from 'react-intl';
export default function Home() {
return (
<div>
<h1>I18n Example</h1>
<Link to="/blog">Blog</Link>
<br />
<button style={{ marginTop: 20 }} id="button">
<FormattedMessage id="buttonText" />
</button>
</div>
);
}

View File

@ -0,0 +1,38 @@
import { Outlet, useLocale, getAllLocales, getDefaultLocale, Link, useLocation } from 'ice';
import { IntlProvider as ReactIntlProvider } from 'react-intl';
import { messages } from '@/locales';
export default function Layout() {
const location = useLocation();
const [activeLocale, setLocale] = useLocale();
return (
<main>
<p><b>Current locale: </b>{activeLocale}</p>
<p><b>Default locale: </b>{getDefaultLocale()}</p>
<p><b>Configured locales: </b>{JSON.stringify(getAllLocales())}</p>
<b>Choose language: </b>
<ul>
{
getAllLocales().map((locale: string) => {
return (
<li key={locale}>
<Link
to={location.pathname}
onClick={() => setLocale(locale)}
// state={{ locale }}
>
{locale}
</Link>
</li>
);
})
}
</ul>
<ReactIntlProvider locale={activeLocale} messages={messages[activeLocale]}>
<Outlet />
</ReactIntlProvider>
</main>
);
}

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

@ -19,6 +19,6 @@
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"speed-measure-webpack-plugin": "^1.5.0",
"webpack": "^5.73.0"
"webpack": "^5.80.0"
}
}

View File

@ -1,3 +0,0 @@
export default function A() {
return <div>111</div>;
}

View File

@ -19,6 +19,6 @@
"devDependencies": {
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.2",
"webpack": "^5.73.0"
"webpack": "^5.80.0"
}
}

View File

@ -39,4 +39,4 @@ export const staticDataLoader = defineStaticDataLoader(() => {
return {
price: '0.00',
};
});
});

View File

@ -2,6 +2,7 @@ import { defineConfig } from '@ice/app';
import store from '@ice/plugin-store';
export default defineConfig(() => ({
ssg: false,
plugins: [
store({
resetPageState: true,

View File

@ -19,4 +19,8 @@ export const dataLoader = defineDataLoader(() => {
});
});
export default defineAppConfig(() => ({}));
export default defineAppConfig(() => ({
router: {
type: 'hash',
},
}));

View File

@ -21,16 +21,18 @@
"cov": "vitest run --coverage",
"test": "vitest",
"changeset": "changeset",
"version": "changeset version && pnpm install --frozen-lockfile false",
"install:frozen": "pnpm install --frozen-lockfile false",
"version": "changeset version && pnpm install:frozen",
"release": "changeset publish",
"release:beta": "changeset pre enter beta && pnpm run version && pnpm build && pnpm release && changeset pre exit"
"release:beta": "changeset pre enter beta && pnpm run version && pnpm build && pnpm release && changeset pre exit",
"release:snapshot": "changeset version --snapshot canary && pnpm install:frozen && pnpm build && pnpm release --tag canary --no-git-tag --snapshot"
},
"devDependencies": {
"@actions/exec": "^1.1.1",
"@applint/spec": "^1.2.3",
"@changesets/cli": "^2.26.0",
"@commitlint/cli": "^16.3.0",
"@ice/bundles": "workspace:^0.1.0",
"@ice/bundles": "workspace:*",
"@testing-library/react": "^13.3.0",
"@types/eslint": "^8.4.5",
"@types/fs-extra": "^9.0.13",

View File

@ -1,5 +1,12 @@
# Changelog
## 0.1.10
### Patch Changes
- 57219848: update webpack version
- 5dd3c86e: fix: update version of @ice/swc-plugin-keep-export(0.1.4-2)
## 0.1.9
### Patch Changes

View File

@ -1,6 +1,6 @@
{
"name": "@ice/bundles",
"version": "0.1.9",
"version": "0.1.10",
"license": "MIT",
"author": "ICE",
"description": "Basic dependencies for ice.",
@ -17,14 +17,14 @@
"dependencies": {
"@swc/core": "1.3.19",
"@ice/swc-plugin-remove-export": "0.1.2",
"@ice/swc-plugin-keep-export": "0.1.4",
"@ice/swc-plugin-keep-export": "0.1.4-2",
"@ice/swc-plugin-node-transform": "0.1.0-8",
"ansi-html-community": "^0.0.8",
"html-entities": "^2.3.2",
"core-js": "3.29.1",
"caniuse-lite": "^1.0.30001431",
"chokidar": "3.5.3",
"esbuild": "^0.16.5",
"esbuild": "^0.17.16",
"events": "3.3.0",
"jest-worker": "27.5.1",
"less": "4.1.2",
@ -65,7 +65,7 @@
"terser-webpack-plugin": "5.3.5",
"typescript": "^4.6.4",
"trusted-cert": "1.1.3",
"webpack": "5.76.2",
"webpack": "5.80.0",
"webpack-bundle-analyzer": "4.5.0",
"webpack-dev-server": "4.11.1",
"unplugin": "0.9.5",

View File

@ -1,5 +1,31 @@
# Changelog
## 3.2.0
### Minor Changes
- f62560f9: feat: refactor runtime about router
### Patch Changes
- 8f97e538: chore: bump build-scripts version to unify esbuild version
- 1c3d3fec: feat: support add routes definition
- 9de09ee8: feat: support version check between @ice/app and @ice/runtime
- da7c733c: fix: wrap data loader in function expression to avoid rewrite global scope
- ddee1c3e: support miniapp native events
- 5dd3c86e: refactor: use swc plugin for remove code
- 467dc56b: fix: only create esbuild context when dev
- 1c3d3fec: fix: routeSpecifier is not unique
- 7b04ca7e: fix: remove all exports of components when render mode is csr
- 57219848: support miniapp native lifecycle events
- 13770d53: fix: compatible with runtime absolute path on win32 platform
- a878225f: fix: dataLoader is sent repeatedly in PHA
- Updated dependencies
- @ice/runtime@1.2.0
- @ice/bundles@0.1.10
- @ice/route-manifest@1.2.0
- @ice/webpack-config@1.0.14
## 3.1.7
### Patch Changes
@ -8,12 +34,7 @@
- 0d549779: fix: read the same task target
- b226e356: fix: bump @swc/helpers to 0.5.1
- 6e9c822a: fix: server compiler define config
- Updated dependencies [28de278f]
- Updated dependencies [6e9c822a]
- Updated dependencies [b226e356]
- Updated dependencies [5d5fb334]
- Updated dependencies [bb07fd91]
- Updated dependencies [0d549779]
- Updated dependencies
- @ice/webpack-config@1.0.13
- @ice/bundles@0.1.9
- @ice/runtime@1.1.7

View File

@ -1,6 +1,6 @@
{
"name": "@ice/app",
"version": "3.1.7",
"version": "3.2.0",
"description": "provide scripts and configuration used by web framework ice",
"type": "module",
"main": "./esm/index.js",
@ -18,7 +18,8 @@
"!esm/**/*.map",
"templates",
"openChrome.applescript",
"*.d.ts"
"*.d.ts",
"scripts"
],
"engines": {
"node": ">=14.19.0",
@ -26,7 +27,8 @@
},
"scripts": {
"watch": "tsc -w",
"build": "tsc"
"build": "tsc",
"postinstall": "node ./scripts/postinstall.mjs"
},
"author": "ice-admin",
"license": "MIT",
@ -34,18 +36,14 @@
"bugs": "https://github.com/alibaba/ice/issues",
"homepage": "https://v3.ice.work",
"dependencies": {
"@babel/generator": "7.18.10",
"@babel/parser": "7.18.10",
"@babel/traverse": "7.18.10",
"@babel/types": "7.18.10",
"@ice/bundles": "0.1.9",
"@ice/route-manifest": "1.1.1",
"@ice/runtime": "^1.1.7",
"@ice/webpack-config": "1.0.13",
"@ice/bundles": "0.1.10",
"@ice/route-manifest": "1.2.0",
"@ice/runtime": "^1.2.0",
"@ice/webpack-config": "1.0.14",
"@swc/helpers": "0.5.1",
"@types/express": "^4.17.14",
"address": "^1.1.2",
"build-scripts": "^2.1.0",
"build-scripts": "^2.1.1-0",
"chalk": "^4.0.0",
"commander": "^9.0.0",
"consola": "^2.15.3",
@ -54,7 +52,6 @@
"dotenv": "^16.0.0",
"dotenv-expand": "^8.0.3",
"ejs": "^3.1.6",
"estree-walker": "^3.0.2",
"fast-glob": "^3.2.11",
"find-up": "^5.0.0",
"fs-extra": "^10.0.0",
@ -79,13 +76,13 @@
"@types/multer": "^1.4.7",
"@types/temp": "^0.9.1",
"chokidar": "^3.5.3",
"esbuild": "^0.16.5",
"esbuild": "^0.17.16",
"jest": "^29.0.2",
"react": "^18.2.0",
"react-router": "^6.8.2",
"react-router": "6.11.1",
"sass": "^1.50.0",
"unplugin": "^0.9.0",
"webpack": "^5.76.2",
"webpack": "^5.80.0",
"webpack-dev-server": "^4.7.4"
},
"peerDependencies": {

View File

@ -0,0 +1,20 @@
import { createRequire } from 'module';
import fs from 'fs';
import semver from 'semver';
const require = createRequire(import.meta.url);
// Only change this when you release a version which break the usage of runtime generation.
const RUNTIME_VALID_VERSION = '>=1.2.0';
try {
// @ice/runtime has defined package.json exports, so we can use require.resolve to get the package.json path.
const packageJsonPath = require.resolve('@ice/runtime/package.json');
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
const version = semver.valid(semver.coerce(pkg.version));
if (!semver.satisfies(version, RUNTIME_VALID_VERSION)) {
console.log(`Detect @ice/runtime version is ${pkg.version}, Please update @ice/runtime to ${RUNTIME_VALID_VERSION}`);
// Break the process while @ice/runtime version is not valid.
process.exit(1);
}
} catch (e) {
// Ignore errors while @ice/runtime is not installed.
}

View File

@ -144,27 +144,28 @@ const build = async (
if (ssg) {
renderMode = 'SSG';
}
const { serverEntry } = await serverCompileTask.get();
serverEntryRef.current = serverEntry;
const routeType = appConfig?.router?.type;
const {
outputPaths = [],
} = await generateEntry({
rootDir,
outputDir,
entry: serverEntry,
// only ssg need to generate the whole page html when build time.
documentOnly: !ssg,
renderMode,
routeType: appConfig?.router?.type,
distType,
routeManifest,
});
// This depends on orders.
output.paths = [...outputPaths];
if (routeType === 'memory' && userConfig?.routes?.injectInitialEntry) {
injectInitialEntry(routeManifest, outputDir);
const { serverEntry } = await serverCompileTask.get() || {};
if (serverEntry) {
serverEntryRef.current = serverEntry;
const routeType = appConfig?.router?.type;
const {
outputPaths = [],
} = await generateEntry({
rootDir,
outputDir,
entry: serverEntry,
// only ssg need to generate the whole page html when build time.
documentOnly: !ssg,
renderMode,
routeType: appConfig?.router?.type,
distType,
routeManifest,
});
// This depends on orders.
output.paths = [...outputPaths];
if (routeType === 'memory' && userConfig?.routes?.injectInitialEntry) {
injectInitialEntry(routeManifest, outputDir);
}
}
await applyHook('after.build.compile', {

View File

@ -39,25 +39,34 @@ export const TARGETS = [
export const RUNTIME_EXPORTS = [
{
specifier: ['Link', 'Outlet', 'useParams', 'useSearchParams', 'useLocation', 'useNavigate'],
specifier: [
'Link',
'Outlet',
'useParams',
'useSearchParams',
'useLocation',
'useData',
'useConfig',
'useNavigate',
],
source: '@ice/runtime/router',
},
{
specifier: [
'defineAppConfig',
'useAppData',
'useData',
'useConfig',
'history',
'KeepAliveOutlet',
'useMounted',
'ClientOnly',
'withSuspense',
'useSuspenseData',
'Await',
'defineDataLoader',
'defineServerDataLoader',
'defineStaticDataLoader',
'usePageLifecycle',
],
source: '@ice/runtime',
},
];
];

View File

@ -7,7 +7,12 @@ import type { Config } from '@ice/webpack-config/types';
import type { AppConfig } from '@ice/runtime/types';
import webpack from '@ice/bundles/compiled/webpack/index.js';
import fg from 'fast-glob';
import type { DeclarationData, PluginData, ExtendsPluginAPI, TargetDeclarationData } from './types';
import type {
DeclarationData,
PluginData,
ExtendsPluginAPI,
TargetDeclarationData,
} from './types/index.js';
import { DeclarationType } from './types/index.js';
import Generator from './service/runtimeGenerator.js';
import { createServerCompiler } from './service/serverCompiler.js';
@ -19,7 +24,7 @@ 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, getRoutesDefination } from './routes.js';
import { generateRoutesInfo, getRoutesDefinition } from './routes.js';
import * as config from './config.js';
import { RUNTIME_TMP_DIR, WEB, RUNTIME_EXPORTS, SERVER_ENTRY } from './constant.js';
import createSpinner from './utils/createSpinner.js';
@ -167,6 +172,7 @@ async function createService({ rootDir, command, commandArgs }: CreateServiceOpt
getRouteManifest: () => routeManifest.getNestedRoute(),
getFlattenRoutes: () => routeManifest.getFlattenRoute(),
getRoutesFile: () => routeManifest.getRoutesFile(),
addRoutesDefinition: routeManifest.addRoutesDefinition.bind(routeManifest),
excuteServerEntry,
context: {
webpack,
@ -183,7 +189,7 @@ async function createService({ rootDir, command, commandArgs }: CreateServiceOpt
// get plugins include built-in plugins and custom plugins
const resolvedPlugins = await ctx.resolvePlugins() as PluginData[];
const runtimeModules = getRuntimeModules(resolvedPlugins);
const runtimeModules = getRuntimeModules(resolvedPlugins, rootDir);
const { getAppConfig, init: initAppConfigCompiler } = getAppExportConfig(rootDir);
const { getRoutesConfig, getDataloaderConfig, init: initRouteConfigCompiler } = getRouteExportConfig(rootDir);
@ -212,8 +218,9 @@ async function createService({ rootDir, command, commandArgs }: CreateServiceOpt
const coreEnvKeys = getCoreEnvKeys();
const routesInfo = await generateRoutesInfo(rootDir, routesConfig);
const routesInfo = await generateRoutesInfo(rootDir, routesConfig, routeManifest.getRoutesDefinitions());
routeManifest.setRoutes(routesInfo.routes);
const hasExportAppData = (await getFileExports({ rootDir, file: 'src/app' })).includes('dataLoader');
const csr = !userConfig.ssr && !userConfig.ssg;
@ -230,7 +237,7 @@ async function createService({ rootDir, command, commandArgs }: CreateServiceOpt
const iceRuntimePath = '@ice/runtime';
// Only when code splitting use the default strategy or set to `router`, the router will be lazy loaded.
const lazy = [true, 'chunks', 'page'].includes(userConfig.codeSplitting);
const { routeImports, routeDefination } = getRoutesDefination(routesInfo.routes, lazy);
const { routeImports, routeDefinition } = getRoutesDefinition(routesInfo.routes, lazy);
// add render data
generator.setRenderData({
...routesInfo,
@ -250,21 +257,25 @@ async function createService({ rootDir, command, commandArgs }: CreateServiceOpt
jsOutput: distType.includes('javascript'),
dataLoader: userConfig.dataLoader,
routeImports,
routeDefination,
routeDefinition,
});
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),
});
renderExportsTemplate(
{
...routesInfo,
hasExportAppData,
},
generator.addRenderFile,
{
rootDir,
runtimeDir: RUNTIME_TMP_DIR,
templateDir: path.join(templateDir, 'exports'),
dataLoader: Boolean(userConfig.dataLoader),
},
);
if (typeof userConfig.dataLoader === 'object' && userConfig.dataLoader.fetcher) {
const {
@ -293,6 +304,7 @@ async function createService({ rootDir, command, commandArgs }: CreateServiceOpt
rootDir,
task: platformTaskConfig,
server,
csr,
});
addWatchEvent([
/src\/?[\w*-:.$]+$/,

View File

@ -1,55 +0,0 @@
import * as fs from 'fs';
import type { Plugin } from 'esbuild';
import { parse, type ParserOptions } from '@babel/parser';
import babelTraverse from '@babel/traverse';
import babelGenerate from '@babel/generator';
import removeTopLevelCode from '../utils/babelPluginRemoveCode.js';
import formatPath from '../utils/formatPath.js';
import { logger } from '../utils/logger.js';
// @ts-ignore @babel/traverse is not a valid export in esm
const traverse = babelTraverse.default || babelTraverse;
// @ts-ignore @babel/generate is not a valid export in esm
const generate = babelGenerate.default || babelGenerate;
const removeCodePlugin = (keepExports: string[], transformInclude: (id: string) => boolean): Plugin => {
const parserOptions: ParserOptions = {
sourceType: 'module',
plugins: [
'jsx',
'importMeta',
'topLevelAwait',
'classProperties',
'classPrivateMethods',
],
};
return {
name: 'esbuild-remove-top-level-code',
setup(build) {
build.onLoad({ filter: /\.(js|jsx|ts|tsx)$/ }, async ({ path: id }) => {
if (!transformInclude(formatPath(id))) {
return;
}
const source = await fs.promises.readFile(id, 'utf-8');
let isTS = false;
if (id.match(/\.(ts|tsx)$/)) {
isTS = true;
parserOptions.plugins.push('typescript', 'decorators-legacy');
}
try {
const ast = parse(source, parserOptions);
traverse(ast, removeTopLevelCode(keepExports));
const contents = generate(ast).code;
return {
contents,
loader: isTS ? 'tsx' : 'jsx',
};
} catch (error) {
logger.debug('Remove top level code error.', `\nFile id: ${id}`, `\n${error.stack}`);
}
});
},
};
};
export default removeCodePlugin;

View File

@ -2,7 +2,7 @@ import * as path from 'path';
import type { Context } from 'build-scripts';
import type { Config } from '@ice/webpack-config/types';
import type { WatchEvent } from './types/plugin.js';
import { generateRoutesInfo, getRoutesDefination } from './routes.js';
import { generateRoutesInfo, getRoutesDefinition } from './routes.js';
import type Generator from './service/runtimeGenerator';
import getGlobalStyleGlobPattern from './utils/getGlobalStyleGlobPattern.js';
import renderExportsTemplate from './utils/renderExportsTemplate.js';
@ -30,7 +30,7 @@ const getWatchEvents = (options: Options): WatchEvent[] => {
async (eventName: string) => {
if (eventName === 'add' || eventName === 'unlink' || eventName === 'change') {
const routesRenderData = await generateRoutesInfo(rootDir, routesConfig);
const { routeImports, routeDefination } = getRoutesDefination(routesRenderData.routes, lazyRoutes);
const { routeImports, routeDefinition } = getRoutesDefinition(routesRenderData.routes, lazyRoutes);
const stringifiedData = JSON.stringify(routesRenderData);
if (cache.get('routes') !== stringifiedData) {
cache.set('routes', stringifiedData);
@ -38,9 +38,9 @@ const getWatchEvents = (options: Options): WatchEvent[] => {
if (eventName !== 'change') {
// Specify the route files to re-render.
generator.renderFile(
path.join(templateDir, 'routes.ts.ejs'),
path.join(rootDir, targetDir, 'routes.ts'),
{ routeImports, routeDefination },
path.join(templateDir, 'routes.tsx.ejs'),
path.join(rootDir, targetDir, 'routes.tsx'),
{ routeImports, routeDefinition },
);
// Keep generate route manifest for avoid breaking change.
generator.renderFile(

View File

@ -1,15 +1,19 @@
import * as path from 'path';
import { formatNestedRouteManifest, generateRouteManifest } from '@ice/route-manifest';
import type { NestedRouteManifest } from '@ice/route-manifest';
import type { NestedRouteManifest, DefineExtraRoutes } from '@ice/route-manifest';
import type { UserConfig } from './types/userConfig.js';
import { getFileExports } from './service/analyze.js';
import formatPath from './utils/formatPath.js';
export async function generateRoutesInfo(rootDir: string, routesConfig: UserConfig['routes'] = {}) {
export async function generateRoutesInfo(
rootDir: string,
routesConfig: UserConfig['routes'] = {},
routesDefinitions: DefineExtraRoutes[] = [],
) {
const routeManifest = generateRouteManifest(
rootDir,
routesConfig.ignoreFiles,
routesConfig.defineRoutes,
[routesConfig.defineRoutes, ...routesDefinitions],
routesConfig.config,
);
@ -51,9 +55,9 @@ export default {
};
}
export function getRoutesDefination(nestRouteManifest: NestedRouteManifest[], lazy = false, depth = 0) {
export function getRoutesDefinition(nestRouteManifest: NestedRouteManifest[], lazy = false, depth = 0) {
const routeImports: string[] = [];
const routeDefination = nestRouteManifest.reduce((prev, route, currentIndex) => {
const routeDefinition = nestRouteManifest.reduce((prev, route) => {
const { children, path: routePath, index, componentName, file, id, layout, exports } = route;
const componentPath = id.startsWith('__') ? file : `@/pages/${file}`.replace(new RegExp(`${path.extname(file)}$`), '');
@ -62,27 +66,47 @@ export function getRoutesDefination(nestRouteManifest: NestedRouteManifest[], la
if (lazy) {
loadStatement = `import(/* webpackChunkName: "p_${componentName}" */ '${formatPath(componentPath)}')`;
} else {
const routeSpecifier = `route_${depth}_${currentIndex}`;
const routeSpecifier = id.replace(/[./-]/g, '_').replace(/[:*]/g, '$');
routeImports.push(`import * as ${routeSpecifier} from '${formatPath(componentPath)}';`);
loadStatement = routeSpecifier;
}
const component = `Component: () => WrapRouteComponent({
routeId: '${id}',
isLayout: ${layout},
routeExports: ${lazy ? 'componentModule' : loadStatement},
})`;
const loader = `loader: createRouteLoader({
routeId: '${id}',
requestContext,
renderMode,
module: ${lazy ? 'componentModule' : loadStatement},
})`;
const routeProperties: string[] = [
`path: '${formatPath(routePath || '')}',`,
`load: () => ${loadStatement},`,
`async lazy() {
${lazy ? `const componentModule = await ${loadStatement}` : ''};
return {
${lazy ? '...componentModule' : `...${loadStatement}`},
${component},
${loader},
};
},`,
// Empty errorElement to avoid default ui provided by react-router.
'errorElement: <RouteErrorComponent />,',
`componentName: '${componentName}',`,
`index: ${index},`,
`id: '${id}',`,
'exact: true,',
`exports: ${JSON.stringify(exports)},`,
];
].filter(Boolean);
if (layout) {
routeProperties.push('layout: true,');
}
if (children) {
const res = getRoutesDefination(children, lazy, depth + 1);
const res = getRoutesDefinition(children, lazy, depth + 1);
routeImports.push(...res.routeImports);
routeProperties.push(`children: [${res.routeDefination}]`);
routeProperties.push(`children: [${res.routeDefinition}]`);
}
prev += formatRoutesStr(depth, routeProperties);
return prev;
@ -90,7 +114,7 @@ export function getRoutesDefination(nestRouteManifest: NestedRouteManifest[], la
return {
routeImports,
routeDefination,
routeDefinition,
};
}

View File

@ -25,6 +25,7 @@ import { filterAlias, getRuntimeDefination } from './serverCompiler.js';
interface InitOptions {
rootDir: string;
server: UserConfig['server'];
csr: boolean;
task: TaskConfig<Config>;
}
@ -86,6 +87,7 @@ class ServerRunner extends Runner {
task,
server,
rootDir,
csr,
}: InitOptions) {
const transformPlugins = getCompilerPlugins(rootDir, {
...task.config,
@ -94,6 +96,8 @@ class ServerRunner extends Runner {
polyfill: false,
swcOptions: {
nodeTransform: true,
// Remove all exports except pageConfig when ssr and ssg both are false.
keepExports: csr ? ['pageConfig'] : null,
compilationConfig: {
jsc: {
// https://node.green/#ES2020
@ -269,7 +273,6 @@ class ServerRunner extends Runner {
const bundlePath = await runtimeMeta.bundle(id);
return { externalize: bundlePath };
}
return {
code: await transformJsxRuntime(code),
};

View File

@ -1,7 +1,6 @@
import * as path from 'path';
import fs from 'fs-extra';
import type { ServerCompiler } from '../types/plugin.js';
import removeTopLevelCode from '../esbuild/removeTopLevelCode.js';
import { getCache, setCache } from '../utils/persistentCache.js';
import { getFileHash } from '../utils/hash.js';
import dynamicImport from '../utils/dynamicImport.js';
@ -15,7 +14,7 @@ type GetOutfile = (entry: string, exportNames: string[]) => string;
interface CompileConfig {
entry: string;
rootDir: string;
transformInclude: (id: string) => boolean;
transformInclude?: (id: string) => boolean;
needRecompile?: (entry: string, options: string[]) => Promise<boolean | string>;
getOutfile?: GetOutfile;
}
@ -48,13 +47,19 @@ class Config {
format: 'esm',
outfile,
plugins: [
removeTopLevelCode(keepExports, transformInclude),
// External node builtin modules, such as `fs`, it will be imported by weex document.
externalBuiltinPlugin(),
],
].filter(Boolean),
sourcemap: false,
logLevel: 'silent', // The main server compiler process will log it.
}, {});
}, {
swc: {
keepExports: {
value: keepExports,
include: transformInclude,
},
},
});
if (!error) {
this.status = 'RESOLVED';
return outfile;
@ -197,8 +202,6 @@ export const getRouteExportConfig = (rootDir: string) => {
entry: routeConfigFile,
rootDir,
getOutfile: getRouteConfigOutfile,
// Only remove top level code for route component file.
transformInclude: (id) => id.includes('src/pages'),
needRecompile: async (entry) => {
let cached = false;
try {
@ -218,8 +221,6 @@ export const getRouteExportConfig = (rootDir: string) => {
entry: loadersConfigFile,
rootDir,
getOutfile: getdataLoadersConfigOutfile,
// Only remove top level code for route component file.
transformInclude: (id) => id.includes('src/pages'),
needRecompile: async (entry) => {
let cached = false;
const cachedKey = `loader_config_file_${process.env.__ICE_VERSION__}`;

View File

@ -342,7 +342,7 @@ export default class Generator {
const renderExt = '.ejs';
const realTargetPath = path.isAbsolute(targetPath)
? targetPath : path.join(this.rootDir, this.targetDir, targetPath);
// example: templatePath = 'routes.ts.ejs'
// example: templatePath = 'routes.tsx.ejs'
const realTemplatePath = path.isAbsolute(templatePath)
? templatePath : path.join(this.templateDir, templatePath);
const { ext } = path.parse(templatePath);

View File

@ -224,7 +224,14 @@ export function createServerCompiler(options: Options) {
logger.debug('[esbuild]', `start compile for: ${JSON.stringify(buildOptions.entryPoints)}`);
try {
const esbuildResult = await esbuild.build(buildOptions);
let esbuildResult: esbuild.BuildResult;
let context: esbuild.BuildContext;
if (dev) {
context = await esbuild.context(buildOptions);
esbuildResult = await context.rebuild();
} else {
esbuildResult = await esbuild.build(buildOptions);
}
logger.debug('[esbuild]', `time cost: ${new Date().getTime() - startTime}ms`);
@ -243,6 +250,7 @@ export function createServerCompiler(options: Options) {
return {
...esbuildResult,
context,
serverEntry,
};
} catch (error) {

View File

@ -85,7 +85,6 @@ async function webpackCompiler(options: {
[IMPORT_META_TARGET]: JSON.stringify(target),
[IMPORT_META_RENDERER]: JSON.stringify('server'),
},
incremental: command === 'start',
});
webpackConfig.plugins.push(serverCompilerPlugin);
}
@ -104,7 +103,7 @@ async function webpackCompiler(options: {
}),
);
const debounceCompile = debounce(() => {
serverCompilerPlugin?.buildResult?.rebuild();
serverCompilerPlugin?.buildResult?.context.rebuild();
console.log('Document updated, try to reload page for latest html content.');
}, 200);
watch.addEvent([

View File

@ -19,7 +19,7 @@ export interface TargetDeclarationData {
declarationType?: DeclarationType;
}
export type RenderData = Record<string, unknown>;
export type RenderData = Record<string, any>;
type RenderDataFunction = (renderDataFunction: RenderData) => RenderData;
export interface TemplateOptions {
template: string;

View File

@ -2,7 +2,7 @@ import type webpack from '@ice/bundles/compiled/webpack';
import type { _Plugin, CommandArgs, TaskConfig } from 'build-scripts';
import type { Configuration, Stats, WebpackOptionsNormalized } from '@ice/bundles/compiled/webpack';
import type { esbuild } from '@ice/bundles';
import type { NestedRouteManifest } from '@ice/route-manifest';
import type { DefineExtraRoutes, NestedRouteManifest } from '@ice/route-manifest';
import type { Config } from '@ice/webpack-config/types';
import type { AppConfig, AssetsManifest } from '@ice/runtime/types';
import type ServerCompileTask from '../utils/ServerCompileTask.js';
@ -37,11 +37,14 @@ type ServerCompilerBuildOptions = Pick<
'plugins' |
'logLevel' |
'sourcemap' |
'metafile' |
'incremental'
'metafile'
>;
export type ServerBuildResult = Partial<esbuild.BuildResult & { serverEntry: string; error: any }>;
export type ServerBuildResult =
Partial<
esbuild.BuildResult &
{ serverEntry: string; error: any; context: esbuild.BuildContext<esbuild.BuildOptions> }
>;
export interface CompilerOptions {
swc?: Config['swcOptions'];
@ -158,6 +161,7 @@ export interface ExtendsPluginAPI {
getRouteManifest: () => Routes;
getFlattenRoutes: () => string[];
getRoutesFile: () => string[];
addRoutesDefinition: (defineRoutes: DefineExtraRoutes) => void;
dataCache: Map<string, string>;
createLogger: CreateLogger;
}

View File

@ -1,128 +0,0 @@
import type { NodePath } from '@babel/traverse';
import * as t from '@babel/types';
const removeUnreferencedCode = (nodePath: NodePath<t.Program>) => {
let hasRemoved = false;
// Update bindings removed in enter hooks.
nodePath.scope.crawl();
for (const [, binding] of Object.entries(nodePath.scope.bindings)) {
if (!binding.referenced && binding.path.node) {
const nodeType = binding.path.node.type;
if (['VariableDeclarator', 'ImportSpecifier', 'FunctionDeclaration'].includes(nodeType)) {
if (nodeType === 'ImportSpecifier' && (binding.path.parentPath.node as t.ImportDeclaration)?.specifiers.length === 1) {
binding.path.parentPath.remove();
} else if (nodeType === 'VariableDeclarator') {
if (binding.identifier === binding.path.node.id) {
binding.path.remove();
} else {
if (binding.path.node.id.type === 'ArrayPattern') {
binding.path.node.id.elements = binding.path.node.id.elements.filter((element) =>
(element !== binding.identifier && (element as t.RestElement)?.argument !== binding.identifier));
if (binding.path.node.id.elements.length === 0) {
binding.path.remove();
}
} else if (binding.path.node.id.type === 'ObjectPattern') {
binding.path.node.id.properties = binding.path.node.id.properties.filter((property) => {
const { value, key } = property as t.ObjectProperty;
const argument = (property as t.RestElement)?.argument;
return value !== binding.identifier && argument !== binding.identifier &&
(key?.type !== 'Identifier' || (key as t.Identifier)?.name !== binding.identifier.name);
});
if (binding.path.node.id.properties.length === 0) {
binding.path.remove();
}
}
}
} else {
binding.path.remove();
}
hasRemoved = true;
} else if (['ImportDefaultSpecifier'].includes(nodeType)) {
binding.path.parentPath.remove();
hasRemoved = true;
}
}
}
if (hasRemoved) {
// Remove code until there is no more to removed.
removeUnreferencedCode(nodePath);
}
};
const keepExportCode = (identifier: t.Identifier, keepExports: string[]) => {
return keepExports.some((exportString) => {
return t.isIdentifier(identifier, { name: exportString });
});
};
const removeTopLevelCode = (keepExports: string[] = []) => {
return {
ExportNamedDeclaration: {
enter(nodePath: NodePath<t.ExportNamedDeclaration>) {
const { node } = nodePath;
// Exp: export function pageConfig() {}
const isFunctionExport = t.isFunctionDeclaration(node.declaration) &&
keepExportCode(node.declaration.id, keepExports);
// Exp: export const pageConfig = () => {}
const isVariableExport = t.isVariableDeclaration(node.declaration) &&
keepExportCode(node.declaration.declarations![0]?.id as t.Identifier, keepExports);
// Exp: export { pageConfig };
if (node.specifiers && node.specifiers.length > 0) {
nodePath.traverse({
ExportSpecifier(nodePath: NodePath<t.ExportSpecifier>) {
if (!keepExportCode(nodePath.node.exported as t.Identifier, keepExports)) {
nodePath.remove();
}
},
});
node.specifiers = node.specifiers.filter(specifier =>
keepExportCode(specifier.exported as t.Identifier, keepExports));
} else if (!isFunctionExport && !isVariableExport) {
// Remove named export expect defined in keepExports.
nodePath.remove();
}
},
},
ExportDefaultDeclaration: {
enter(nodePath: NodePath<t.ExportDefaultDeclaration>) {
// Remove default export declaration.
if (!keepExports.includes('default')) {
nodePath.remove();
}
},
},
ExpressionStatement: {
enter(nodePath: NodePath<t.ExpressionStatement>) {
// Remove top level call expression.
if (nodePath.parentPath.isProgram()) {
if (t.isCallExpression(nodePath.node.expression) || t.isAssignmentExpression(nodePath.node.expression)) {
nodePath.remove();
}
}
},
},
ImportDeclaration: {
enter(nodePath: NodePath<t.ImportDeclaration>) {
// Remove import statement without specifiers.
if (nodePath.node.specifiers.length === 0) {
nodePath.remove();
}
},
},
'IfStatement|TryStatement|WhileStatement|DoWhileStatement': {
// Remove statement even if it's may cause variable changed.
enter(nodePath: NodePath<t.IfStatement | t.TryStatement | t.WhileStatement>) {
// TODO: check expression statement if it is changed top level variable referenced by pageConfig
nodePath.remove();
},
},
Program: {
exit(nodePath: NodePath<t.Program>) {
removeUnreferencedCode(nodePath);
},
},
};
};
export default removeTopLevelCode;

View File

@ -1,4 +1,7 @@
import * as path from 'path';
import { RUNTIME_TMP_DIR } from '../constant.js';
import type { PluginData } from '../types/plugin.js';
import formatPath from './formatPath.js';
export interface RuntimeModule {
staticRuntime: boolean;
@ -6,10 +9,18 @@ export interface RuntimeModule {
name: string;
}
function getRuntimeModules(plugins: PluginData[]) {
function getRuntimeModules(plugins: PluginData[], rootDir: string) {
return plugins
.filter(({ runtime }) => !!runtime)
.map(({ name, runtime, staticRuntime }) => ({ name, path: runtime, staticRuntime }));
.map(({ name, runtime, staticRuntime }) => {
return {
name,
path: path.isAbsolute(runtime)
? formatPath(path.relative(path.join(rootDir, RUNTIME_TMP_DIR), runtime)) // Be compatible with win32.
: runtime,
staticRuntime,
};
});
}
export default getRuntimeModules;

View File

@ -10,7 +10,6 @@ interface Options {
userConfig: UserConfig;
outputDir: string;
serverEntry: string;
incremental: boolean;
serverCompileTask: ExtendsPluginAPI['serverCompileTask'];
ensureRoutesConfig: () => Promise<void>;
runtimeDefineVars: Record<string, string>;
@ -25,7 +24,6 @@ function getServerCompilerPlugin(serverCompiler: ServerCompiler, options: Option
serverCompileTask,
ensureRoutesConfig,
runtimeDefineVars,
incremental,
} = options;
const entryPoint = getServerEntry(rootDir, serverEntry);
const { ssg, ssr, server: { format } } = userConfig;
@ -43,7 +41,6 @@ function getServerCompilerPlugin(serverCompiler: ServerCompiler, options: Option
outExtension: { '.js': isEsm ? '.mjs' : '.cjs' },
metafile: true,
logLevel: 'silent', // The server compiler process will log it in debug.
incremental,
},
{
// The server bundle will external all the dependencies when the format type is esm,

View File

@ -18,7 +18,7 @@ function injectInitialEntry(routeManifest: RouteManifest, outputDir: string) {
const routePaths = routeManifest.getFlattenRoute();
const routeItems = routeManifest.getNestedRoute();
routePaths.forEach((routePath) => {
const routeAsset = getRouteAsset(routeItems, routePath);
const routeAsset = getRouteAsset(routeItems as unknown as RouteItem[], routePath);
// Inject `initialPath` when router type is memory.
const routeAssetPath = path.join(outputDir, 'js', routeAsset);
if (fse.existsSync(routeAssetPath)) {

View File

@ -1,11 +1,13 @@
import type { NestedRouteManifest } from '@ice/route-manifest';
import type { NestedRouteManifest, DefineExtraRoutes } from '@ice/route-manifest';
import getRoutePath, { getRoutesFile } from './getRoutePaths.js';
export default class RouteManifest {
private routeManifest: NestedRouteManifest[];
private routesDefinitions: DefineExtraRoutes[];
constructor() {
this.routeManifest = null;
this.routesDefinitions = [];
}
getNestedRoute() {
@ -26,4 +28,12 @@ export default class RouteManifest {
getRoutesFile() {
return getRoutesFile(this.getNestedRoute());
}
}
public addRoutesDefinition(defineRoutes: DefineExtraRoutes) {
this.routesDefinitions.push(defineRoutes);
}
public getRoutesDefinitions() {
return this.routesDefinitions;
}
}

View File

@ -53,6 +53,7 @@ export default class DataLoaderPlugin {
const { outputFiles, error } = await this.serverCompiler(
{
target: 'es6', // should not set to esnext, https://github.com/alibaba/ice/issues/5830
format: 'iife',
entryPoints: [filePath],
write: false,
logLevel: 'silent', // The main server compile process will log it.

View File

@ -64,8 +64,8 @@ export default class ServerCompilerPlugin {
this.isCompiling = false;
await this.compileTask(compilation);
const compilerTask = this.buildResult?.rebuild
? this.buildResult.rebuild()
const compilerTask = this.buildResult?.context.rebuild
? this.buildResult.context.rebuild()
.then((result) => {
return {
// Pass original buildResult, because it's returned serverEntry.

View File

@ -4,7 +4,7 @@ import { runClientApp, getAppConfig } from '<%- iceRuntimePath %>';
import { commons, statics } from './runtimeModules';
import * as app from '@/app';
<% if (enableRoutes) { -%>
import routes from './routes';
import createRoutes from './routes';
<% } -%>
<%- runtimeOptions.imports %>
<% if(dataLoaderImport.imports) {-%><%-dataLoaderImport.imports%><% } -%>
@ -16,25 +16,39 @@ const getRouterBasename = () => {
// Otherwise chunk of route component will pack @ice/jsx-runtime and depend on framework bundle.
const App = <></>;
const render = (customOptions = {}) => {
<% if(!dataLoaderImport.imports) {-%>
let dataLoaderFetcher = (options) => {
window.fetch(options.url, options);
}
let dataLoaderDecorator = (dataLoader) => {
return dataLoader;
}
<% } -%>
const render = (customOptions: Record<string, any> = {}) => {
const appProps = {
app,
runtimeModules: {
commons,
statics,
},
<% if (enableRoutes) { %>routes,<% } %>
<% if (enableRoutes) { %>createRoutes,<% } %>
basename: getRouterBasename(),
hydrate: <%- hydrate %>,
memoryRouter: <%- memoryRouter || false %>,
<% if(dataLoaderImport.imports) {-%>dataLoaderFetcher,<% } -%>
dataLoaderFetcher,
dataLoaderDecorator,
...customOptions,
<% if (runtimeOptions.exports) { -%>
runtimeOptions: {
<%- runtimeOptions.exports %>
...(customOptions.runtimeOptions || {}),
<% if (runtimeOptions.exports) { -%>
<%- runtimeOptions.exports %>
<% } -%>
<% if (locals.customRuntimeOptions) { -%>
...<%- JSON.stringify(customRuntimeOptions) %>,
<% } -%>
...customOptions.runtimeOptions,
},
<% } -%>
};
return runClientApp(appProps);
};

View File

@ -8,7 +8,7 @@ import Document from '@/document';
import type { RenderMode, DistType } from '@ice/runtime';
// @ts-ignore
import assetsManifest from 'virtual:assets-manifest.json';
import routes from './routes';
import createRoutes from './routes';
import routesConfig from './routes-config.bundle.mjs';
<% if(dataLoaderImport.imports) {-%><%-dataLoaderImport.imports%><% } -%>
<%- runtimeOptions.imports %>
@ -47,7 +47,7 @@ interface RenderOptions {
export async function renderToHTML(requestContext, options: RenderOptions = {}) {
const { renderMode = 'SSR' } = options;
setRuntimeEnv(renderMode);
const mergedOptions = mergeOptions(options);
return await runtime.renderToHTML(requestContext, mergedOptions);
}
@ -55,7 +55,7 @@ export async function renderToHTML(requestContext, options: RenderOptions = {})
export async function renderToResponse(requestContext, options: RenderOptions = {}) {
const { renderMode = 'SSR' } = options;
setRuntimeEnv(renderMode);
const mergedOptions = mergeOptions(options);
return runtime.renderToResponse(requestContext, mergedOptions);
}
@ -80,7 +80,7 @@ function mergeOptions(options) {
return {
app,
assetsManifest,
routes,
createRoutes,
runtimeModules,
Document,
serverOnlyBasename,
@ -92,10 +92,13 @@ function mergeOptions(options) {
routesConfig,
distType,
serverData,
<% if (runtimeOptions.exports) { -%>
runtimeOptions: {
<% if (runtimeOptions.exports) { -%>
<%- runtimeOptions.exports %>
<% } -%>
<% if (locals.customRuntimeOptions) { _%>
...<%- JSON.stringify(customRuntimeOptions) %>,
<% } _%>
},
<% } -%>
};
}
}

View File

@ -1,4 +0,0 @@
<%- routeImports.length ? routeImports.join('\n') + '\n\n' : ''; -%>
export default [
<%- routeDefination %>
];

View File

@ -0,0 +1,8 @@
import { createRouteLoader, WrapRouteComponent, RouteErrorComponent } from '@ice/runtime';
<%- routeImports.length ? routeImports.join('\n') + '\n\n' : ''; -%>
export default ({
requestContext,
renderMode,
}) => ([
<%- routeDefinition %>
]);

View File

@ -20,7 +20,11 @@ const loaders = {
<% if(!dataLoaderImport.imports) {-%>
let dataLoaderFetcher = (options) => {
window.fetch(options.url, options);
}
}
let dataLoaderDecorator = (dataLoader) => {
return dataLoader;
}
<% } -%>
// Only init static runtime in data-loader.
@ -32,6 +36,7 @@ const staticRuntimeModules = [
dataLoader.init(loaders, {
fetcher: dataLoaderFetcher,
decorator: dataLoaderDecorator,
runtimeModules: staticRuntimeModules,
appExport: app,
});

View File

@ -1,2 +0,0 @@
const a = 1;
export default a;

View File

@ -1,6 +0,0 @@
const pageConfig = () => {};
const getData = () => {};
export {
pageConfig,
getData,
};

View File

@ -1,2 +0,0 @@
export const getData = () => {};
export const pageConfig = () => {};

View File

@ -1,2 +0,0 @@
const a = {};
a.test = 1;

View File

@ -1,3 +0,0 @@
export default function Bar() {}
export function pageConfig() {}
export function getData() {}

View File

@ -1,4 +0,0 @@
let a = 1;
if (true) {
a = 2;
}

View File

@ -1,5 +0,0 @@
function a() {}
a();
console.log('test', window.a);
const b = [];
b.map(() => {});

Some files were not shown because too many files have changed in this diff Show More