├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .github
├── ISSUE_TEMPLATE.md
└── workflows
│ ├── ci.yml
│ ├── contributors.yml
│ └── website.yml
├── .gitignore
├── .prettierrc
├── CHANGELOG.md
├── LICENSE
├── README.md
├── codecov.yml
├── commitlint.config.js
├── package.json
├── packages
├── icestark-app
│ ├── .eslintignore
│ ├── .eslintrc.js
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── __tests__
│ │ └── index.spec.tsx
│ ├── package.json
│ ├── src
│ │ ├── AppLink.tsx
│ │ ├── appHistory.ts
│ │ ├── cache.ts
│ │ ├── getBasename.ts
│ │ ├── getMountNode.ts
│ │ ├── index.ts
│ │ ├── isInIcestark.ts
│ │ ├── registerAppEnter.ts
│ │ ├── registerAppLeave.ts
│ │ ├── renderNotFound.ts
│ │ ├── setBasename.ts
│ │ ├── setLibraryName.ts
│ │ └── util
│ │ │ ├── formatUrl.ts
│ │ │ └── normalizeArgs.ts
│ └── tsconfig.json
├── icestark-data
│ ├── .eslintignore
│ ├── .eslintrc.js
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── __tests__
│ │ ├── cache.spec.tsx
│ │ ├── event.spec.tsx
│ │ ├── index.spec.tsx
│ │ └── store.spec.tsx
│ ├── package.json
│ ├── src
│ │ ├── cache.ts
│ │ ├── event.ts
│ │ ├── index.ts
│ │ ├── store.ts
│ │ └── utils.ts
│ └── tsconfig.json
├── icestark-module
│ ├── .eslintignore
│ ├── .eslintrc.js
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── __tests__
│ │ ├── component.js
│ │ ├── index.spec.tsx
│ │ └── loader.spec.ts
│ ├── package.json
│ ├── src
│ │ ├── MicroModule.tsx
│ │ ├── assist.ts
│ │ ├── global.ts
│ │ ├── index.ts
│ │ ├── loader.ts
│ │ ├── modules.tsx
│ │ ├── runtimeHelper.ts
│ │ └── utils.ts
│ └── tsconfig.json
├── icestark
│ ├── .eslintignore
│ ├── .eslintrc.js
│ ├── README.md
│ ├── __tests__
│ │ ├── AppRoute.spec.tsx
│ │ ├── AppRouter.spec.tsx
│ │ ├── app.spec.tsx
│ │ ├── appLifeCycle.spec.tsx
│ │ ├── cache.spec.tsx
│ │ ├── capturedListeners.spec.tsx
│ │ ├── checkActive.spec.tsx
│ │ ├── createMicroApp.spec.tsx
│ │ ├── error.spec.ts
│ │ ├── getLifecycle.spec.tsx
│ │ ├── global-umd-sample.js
│ │ ├── handleAssets.spec.tsx
│ │ ├── index.spec.tsx
│ │ ├── js-bundle-sample.js
│ │ ├── loader.bundle.spec.ts
│ │ ├── loader.runtime.spec.ts
│ │ ├── loader.umd.setLibrary.spec.ts
│ │ ├── loader.umd.spec.ts
│ │ ├── umd-not-setlibrary-sample.js
│ │ └── umd-setlibrary-sample.js
│ ├── package.json
│ ├── src
│ │ ├── AppLink.tsx
│ │ ├── AppRoute.tsx
│ │ ├── AppRouter.tsx
│ │ ├── appHistory.ts
│ │ ├── apps.ts
│ │ ├── index.ts
│ │ ├── start.ts
│ │ └── util
│ │ │ ├── appLifeCycle.ts
│ │ │ ├── cache.ts
│ │ │ ├── capturedListeners.ts
│ │ │ ├── checkActive.ts
│ │ │ ├── constant.ts
│ │ │ ├── error.ts
│ │ │ ├── getLifecycle.ts
│ │ │ ├── global.ts
│ │ │ ├── globalConfiguration.ts
│ │ │ ├── handleAssets.ts
│ │ │ ├── helpers.ts
│ │ │ ├── loaders.ts
│ │ │ ├── prefetch.ts
│ │ │ └── renderComponent.tsx
│ └── tsconfig.json
└── sandbox
│ ├── .eslintignore
│ ├── .eslintrc.js
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── __tests__
│ └── index.spec.ts
│ ├── package.json
│ ├── src
│ └── index.ts
│ └── tsconfig.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── scripts
├── beta.ts
├── getPackageInfos.ts
└── publish.ts
├── setupJest.ts
├── tsconfig.json
└── website
├── .prettierrc.js
├── README.md
├── babel.config.js
├── blog
├── 00-icestark-2-4-0.md
├── 01-icestark-2-5-0.md
├── 02-icestark-2-6-0.md
└── 03-icestark-2-7-0.md
├── config
├── footer.js
├── navbar.js
└── sidebars.js
├── docs
├── api
│ ├── ice-stark-app.md
│ ├── ice-stark.md
│ └── stark-module.md
├── faq.md
├── guide.md
└── guide
│ ├── advanced
│ ├── communication.md
│ ├── performance.md
│ └── sandbox.md
│ ├── concept
│ ├── child.md
│ ├── layout.md
│ └── workflow.md
│ ├── ecosystem.md
│ ├── micro-module.md
│ ├── upgrade.md
│ ├── use-child
│ ├── others.md
│ ├── react.md
│ └── vue.md
│ └── use-layout
│ ├── react.md
│ └── vue.md
├── docusaurus.config.js
├── package.json
├── scripts
└── getDocsFromDir.js
├── src
├── codes
│ ├── 1.jsx
│ ├── 2.jsx
│ ├── 3.jsx
│ ├── 4.jsx
│ ├── 5.jsx
│ ├── 6.jsx
│ ├── 7.jsx
│ └── CodeSnippet.js
├── components
│ ├── AreaWrapper
│ │ ├── area.module.css
│ │ └── index.jsx
│ ├── Badge
│ │ ├── Badge.module.css
│ │ └── index.jsx
│ ├── Button
│ │ ├── button.module.css
│ │ └── index.jsx
│ ├── Feature
│ │ ├── feature.module.css
│ │ └── index.jsx
│ ├── Splash
│ │ ├── index.jsx
│ │ └── splash.module.css
│ └── Users
│ │ ├── index.jsx
│ │ └── users.module.css
├── css
│ └── custom.css
└── pages
│ ├── error.jsx
│ └── index.jsx
├── static
├── .nojekyll
└── img
│ ├── favicon.ico
│ └── logo.png
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 2
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 | [*.md]
12 | trim_trailing_whitespace = false
13 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | lib
2 | node_modules
3 | tests
4 | __tests__
5 | website
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | const { getESLintConfig } = require('@iceworks/spec');
2 |
3 | module.exports = getESLintConfig('react-ts', {
4 | env: {
5 | jest: true,
6 | },
7 | rules: {
8 | '@typescript-eslint/explicit-member-accessibility': 'off',
9 | '@typescript-eslint/explicit-function-return-type': 'off',
10 | 'no-unused-expressions': 'off',
11 | 'no-async-promise-executor': 'off',
12 | 'no-mixed-operators': 'off',
13 | 'react/static-property-placement': 'off',
14 | '@iceworks/best-practices/recommend-functional-component': 'off',
15 | '@iceworks/best-practices/recommend-polyfill': 'off',
16 | 'no-nested-ternary': 'warn',
17 | },
18 | });
19 |
20 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | **Do you want to request a *feature* or report a *bug*?**
2 |
3 | **What is the current behavior?**
4 |
5 | **If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem.
6 |
7 | **What is the expected behavior?**
8 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build:
7 |
8 | runs-on: ubuntu-latest
9 |
10 | strategy:
11 | matrix:
12 | node-version: [16.x]
13 |
14 | steps:
15 | - uses: actions/checkout@v2
16 | - name: Set branch name
17 | run: echo >>$GITHUB_ENV BRANCH_NAME=${GITHUB_REF#refs/heads/}
18 |
19 | - name: Echo branch name
20 | run: echo ${BRANCH_NAME}
21 |
22 | - name: Use Node.js ${{ matrix.node-version }}
23 | uses: actions/setup-node@v1
24 | with:
25 | node-version: ${{ matrix.node-version }}
26 | registry-url: https://registry.npmjs.org/
27 |
28 | - name: Install pnpm
29 | uses: pnpm/action-setup@v2
30 | with:
31 | version: 8
32 |
33 | - run: pnpm run setup
34 | - run: pnpm run lint
35 | - run: pnpm run test
36 | - run: pnpm run publish:packages
37 | env:
38 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
39 |
40 |
--------------------------------------------------------------------------------
/.github/workflows/contributors.yml:
--------------------------------------------------------------------------------
1 | name: Add contributors
2 | on:
3 | push:
4 | branches:
5 | - master
6 |
7 | jobs:
8 | add-contributors:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v2
12 | - uses: BobAnkh/add-contributors@master
13 | with:
14 | REPO_NAME: "ice-lab/icestark"
15 | CONTRIBUTOR: "## Contributors"
16 | COLUMN_PER_ROW: "10"
17 | ACCESS_TOKEN: ${{secrets.PERSONAL_GITHUB_TOKEN}}
18 | IMG_WIDTH: "60"
19 | FONT_SIZE: "14"
20 | PATH: "/README.md"
21 | COMMIT_MESSAGE: "docs(README): update contributors"
22 | AVATAR_SHAPE: "round"
23 |
--------------------------------------------------------------------------------
/.github/workflows/website.yml:
--------------------------------------------------------------------------------
1 | name: Build docs
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 |
8 | jobs:
9 | build-docs-and-deploy:
10 | # 服务器环境:最新版 Ubuntu
11 | runs-on: ubuntu-latest
12 | steps:
13 | # 拉取代码
14 | - uses: actions/checkout@v2
15 |
16 | - name: Use Node.js 16.x
17 | uses: actions/setup-node@v2
18 | with:
19 | node-version: 16.x
20 |
21 | - name: Get yarn cache directory path
22 | id: yarn-cache-dir-path
23 | run: echo "::set-output name=dir::$(yarn cache dir)"
24 |
25 | - uses: actions/cache@v2
26 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
27 | with:
28 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
29 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
30 | restore-keys: |
31 | ${{ runner.os }}-yarn-
32 |
33 | - run: cd website && yarn
34 | - run: cd website && npm run build
35 |
36 | # 部署到 GitHub Pages
37 | - name: Deploy
38 | uses: JamesIves/github-pages-deploy-action@4.1.0
39 | if: github.ref == 'refs/heads/master'
40 | with:
41 | BRANCH: gh-pages
42 | FOLDER: website/build
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.swp
2 | *.dia~
3 | .idea/
4 | .DS_Store
5 | .eslintcache
6 |
7 | package-lock.json
8 |
9 | npm-debug.log
10 | yarn-error.log
11 | node_modules/
12 | coverage/
13 |
14 | # 这是编译后的代码, 用于提交到 tnpm, git 仓库不需要此份代码
15 | lib/
16 |
17 | build/
18 |
19 | # Docusaurus Generated files
20 | .docusaurus
21 | .cache-loader
22 | .vscode/launch.json
23 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": true,
3 | "singleQuote": true,
4 | "trailingComma": "all",
5 | "printWidth": 100,
6 | "proseWrap": "never"
7 | }
8 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | See [https://github.com/ice-lab/icestark/releases](https://github.com/ice-lab/icestark/releases) for what has changed in each version of icestark.
4 |
5 | # 2.8.3
6 |
7 | - [feat] support `runtime.url` as an array for loading multiple types of resources.
8 | - [fix] refactor fetchScripts for improved runtime asset handling.
9 |
10 | # 2.8.2
11 |
12 | - [fix] fix the issue that the UMD library is overwritten by undefined value.
13 |
14 | # 2.8.1
15 |
16 | - [fix] support unique cache keys for JavaScript resources.
17 |
18 | ## 2.8.0
19 |
20 | - [feat] support `runtime` option to reuse the UMD library.
21 |
22 | ## 2.7.5
23 |
24 | - [fix] avoid to set undefined url when state change.
25 |
26 | ## 2.7.4
27 |
28 | - [feat] support custom `AppRoute` location by component props.
29 |
30 | ## 2.7.3
31 |
32 | - [fix] empty value or `undefined` in `activePath` array will be ignored. ([#558](https://github.com/ice-lab/icestark/issues/558))
33 | - [fix] append missing styles in vite developing mode. ([#555](https://github.com/ice-lab/icestark/issues/555))
34 |
35 | ## 2.7.2
36 |
37 | - [fix] set actual basename when `activePath` is an array. ([#526](https://github.com/ice-lab/icestark/issues/526))
38 |
39 | ## 2.7.1
40 |
41 | - [feat] improve DX a lot.
42 |
43 | ## 2.7.0
44 |
45 | - [feat] cache css by default. ([#373](https://github.com/ice-lab/icestark/issues/373))
46 | - [feat] appHistory and can both take state. ([#477](https://github.com/ice-lab/icestark/issues/477))
47 | - [chore] narrow scope of `import`'s error. ([#466](https://github.com/ice-lab/icestark/issues/466))
48 | - [chore] add missing props for lifecycles. ([#440](https://github.com/ice-lab/icestark/issues/440))
49 |
50 | ## 2.6.2
51 |
52 | - [fix] avoid to append duplicate assets. ([#331](https://github.com/ice-lab/icestark/issues/331))
53 | - [fix] bind `pushState` to global. ([#426](https://github.com/ice-lab/icestark/issues/426))
54 | - [fix] prefetch apps using `window.fetch` by default.
55 |
56 | ## 2.6.1
57 |
58 | - [fix] wrap `import` using `new Function` to avoid compiler error under chrome61 & ie. ([#404](https://github.com/ice-lab/icestark/issues/404))
59 |
60 | ## 2.6.0
61 |
62 | - [feat] support native es module micro-applications. ([#346, #260](https://github.com/ice-lab/icestark/issues/346))
63 | - [feat] handle `` element and re-execute DOM Parser to enable `` of Angular.([#368](https://github.com/ice-lab/icestark/pull/368))
64 | - [refact] change `module: commonjs` to `module: esnext`.
65 | - [fix] assign to `location.hash` never trigger `onRouteChange` twice. ([#353](https://github.com/ice-lab/icestark/issues/353))
66 |
67 | ## 2.5.3
68 |
69 | - [fix] `setBasename` before `createMicroApp` may be covered for empty activePath.
70 |
71 | ## 2.5.2
72 |
73 | - [fix] `createMicroApp` without `activePath` cause error.
74 |
75 | ## 2.5.1
76 |
77 | - [hotfix] keep the misunderstanding `basename=''` working fine with ``.
78 |
79 | ## 2.5.0
80 |
81 | - [feat] `path` is deprecated and using the more powerful `activePath` instead. ([#299, #297, #209](https://github.com/ice-lab/icestark/issues/299))
82 | - [feat] debug mirco-frontends are accessiable by using source maps, even in sandbox. ([#259](https://github.com/ice-lab/icestark/issues/259))
83 | - [fix] call callCapturedEventListeners later to prevent double Prompt. ([#325](https://github.com/ice-lab/icestark/issues/325))
84 | - [refact] refactor url-matching algorithm.
85 |
86 |
87 | ## 2.4.0
88 |
89 | - [feat] support appending extra attributes for scripts when using `loadScriptMode = script`. ([#276](https://github.com/ice-lab/icestark/issues/276))
90 | - [fix] unexpectable sandbox's cleaning up when load modules. ([#293](https://github.com/ice-lab/icestark/issues/293))
91 | - [fix] missing `ErrorComponent` causes React rendering's error. ([#312](https://github.com/ice-lab/icestark/issues/312))
92 |
93 | ## 2.3.2
94 |
95 | - [refact] compatible with sandbox spell error.
96 |
97 | ## 2.3.1
98 |
99 | - [fix] parse `library` the right way if `library` is an array. ([#287](https://github.com/ice-lab/icestark/issues/287))
100 |
101 | ## 2.3.0
102 |
103 | - [feat] support `prefetch` sub-application, which let your micro application fly. ([#188](https://github.com/ice-lab/icestark/issues/188))
104 |
105 | ## 2.2.2
106 |
107 | - [fix] `basename` of `AppRouter` makes effect. ([#241](https://github.com/ice-lab/icestark/issues/241))
108 | - [fix] alter baseURI when using `DOMParser`. ([#233](https://github.com/ice-lab/icestark/issues/233), [#4040](https://github.com/alibaba/ice/issues/4040))
109 |
110 | ## 2.2.1
111 |
112 | - [fix] css assets are unable to load when remove `umd` from sub-application.
113 | ## 2.2.0
114 |
115 | - [feat] no need to use `umd` anymore. Migrate to `loadScriptMode` or use `setLibraryName` in sub-application. ([#240](https://github.com/ice-lab/icestark/issues/240))
116 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 ICE Team
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/codecov.yml:
--------------------------------------------------------------------------------
1 | comment: off
2 | coverage:
3 | status:
4 | project:
5 | default:
6 | threshold: 90%
7 | patch: off
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = { extends: ['@commitlint/config-conventional'] };
2 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "icestark-monorepo",
3 | "version": "2.7.3",
4 | "private": true,
5 | "description": "Icestark is a JavaScript library for multiple projects, Ice workbench solution.",
6 | "scripts": {
7 | "preinstall": "npx only-allow pnpm",
8 | "setup": "rm -rf node_modules && rm -rf ./packages/*/node_modules && pnpm i --registry=https://registry.npmmirror.com && npm run build",
9 | "build": "pnpm run clean && pnpm -r build",
10 | "watch": "pnpm -r watch",
11 | "clean": "rimraf packages/*/lib",
12 | "publish:packages": "ts-node ./scripts/publish.ts",
13 | "publish:beta": "ts-node ./scripts/beta.ts",
14 | "lint": "eslint --cache --ext .js,.jsx,.ts,.tsx ./",
15 | "test": "NODE_ENV=unittest jest"
16 | },
17 | "license": "MIT",
18 | "bugs": {
19 | "url": "https://github.com/ice-lab/icestark/issues"
20 | },
21 | "homepage": "https://github.com/ice-lab/icestark",
22 | "husky": {
23 | "hooks": {
24 | "pre-commit": "tsc --noEmit && lint-staged",
25 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
26 | }
27 | },
28 | "lint-staged": {
29 | "*.{ts,tsx,js,jsx}": [
30 | "eslint --fix",
31 | "git add"
32 | ]
33 | },
34 | "repository": {
35 | "type": "git",
36 | "url": "git+https://github.com/ice-lab/icestark.git"
37 | },
38 | "devDependencies": {
39 | "@commitlint/cli": "^7.5.2",
40 | "@commitlint/config-conventional": "^7.5.0",
41 | "@iceworks/spec": "^1.3.2",
42 | "@ice/stark": "workspace:*",
43 | "@ice/sandbox": "workspace:*",
44 | "@ice/stark-data": "workspace:*",
45 | "@ice/stark-app": "workspace:*",
46 | "@ice/stark-module": "workspace:*",
47 | "@testing-library/jest-dom": "^4.2.3",
48 | "@testing-library/react": "^9.3.2",
49 | "@types/jest": "^24.0.12",
50 | "@types/node": "^12.0.0",
51 | "@types/fs-extra": "^9.0.13",
52 | "codecov": "^3.4.0",
53 | "eslint": "^7.31.0",
54 | "fs-extra": "^10.0.0",
55 | "husky": "^2.2.0",
56 | "jest": "^24.7.1",
57 | "jest-fetch-mock": "^2.1.2",
58 | "lint-staged": "^10.5.3",
59 | "npm-run-all": "^4.1.5",
60 | "rimraf": "^3.0.2",
61 | "stylelint": "^10.1.0",
62 | "ts-jest": "^24.0.2",
63 | "ts-node": "^10.2.1",
64 | "typescript": "^4.3.5",
65 | "urllib": "^2.38.0"
66 | },
67 | "jest": {
68 | "coverageDirectory": "./coverage/",
69 | "collectCoverage": true,
70 | "preset": "ts-jest",
71 | "automock": false,
72 | "testMatch": [ "**/__tests__/**/*.ts?(x)", "**/?(*.)+(spec|test).ts?(x)" ],
73 | "setupFiles": [
74 | "./setupJest.ts"
75 | ]
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/packages/icestark-app/.eslintignore:
--------------------------------------------------------------------------------
1 | lib
2 | node_modules
3 | tests
4 | __tests__
--------------------------------------------------------------------------------
/packages/icestark-app/.eslintrc.js:
--------------------------------------------------------------------------------
1 |
2 | module.exports = {
3 | extends: '../../.eslintrc.js',
4 | };
5 |
--------------------------------------------------------------------------------
/packages/icestark-app/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## 1.5.0
4 |
5 | - [feat] `appHistory` and `` can both take state. ([#478](https://github.com/ice-lab/icestark/pull/428))
6 | - [fix] add missing props for `registerAppEnter` & `registerAppLeave`.
7 |
8 | ## 1.4.2
9 |
10 | - [fix] bind history to window when using `AppLink`. ([#428](https://github.com/ice-lab/icestark/pull/428))
11 |
12 | ## 1.4.1
13 |
14 | - [feat] correct types of `setLibraryName`. ([#287](https://github.com/ice-lab/icestark/issues/287))
15 |
16 | ## 1.4.0
17 |
18 | - [feat] add function `setBasename`.
19 |
20 | ## 1.3.0
21 |
22 | - [feat] add function `setLibraryName`.
23 |
--------------------------------------------------------------------------------
/packages/icestark-app/README.md:
--------------------------------------------------------------------------------
1 | # icestark-app
2 |
3 | > icestark micro-frontends solution, APIs used by sub-application. [icestark docs](https://ice-lab.github.io/icestark/).
4 |
5 | [](https://npmjs.org/package/@ice/stark-app) [](https://packagequality.com/#?package=@ice%2Fstark-app) [](https://travis-ci.org/ice-lab/icestark) [](https://codecov.io/gh/ice-lab/icestark) [](https://npmjs.org/package/@ice/stark-app) [](https://david-dm.org/ice-lab/icestark)
6 |
7 | ## Installation
8 |
9 | ```bash
10 | npm install @ice/stark-app --save
11 | ```
12 |
13 | #### Why is the sub-application API to be drawn separately `@ice/stark-app`?
14 |
15 | - The APIs used by sub-applications are very stable and do not have to be updated frequently with the main package
16 | - Sub-application APIs are more compatible and support non-relay systems
17 |
18 | #### Compatibility
19 |
20 | `icestark-app` supports all major browsers and supports all popular frameworks such as react, vue, angular, jQuery, etc.
21 |
22 | ## Contributors
23 |
24 | Feel free to report any questions as an [issue](https://github.com/ice-lab/icestark/issues/new), we'd love to have your helping hand on `icestark`.
25 |
26 | If you're interested in `icestark`, see [CONTRIBUTING.md](https://github.com/alibaba/ice/blob/master/.github/CONTRIBUTING.md) for more information to learn how to get started.
27 |
28 | ## License
29 |
30 | [MIT](LICENSE)
31 |
--------------------------------------------------------------------------------
/packages/icestark-app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@ice/stark-app",
3 | "version": "1.5.0",
4 | "description": "icestark-app is a JavaScript library for icestark, used by sub-application.",
5 | "scripts": {
6 | "build": "rm -rf lib && tsc",
7 | "watch": "tsc -w",
8 | "test": "NODE_ENV=unittest pnpm jest",
9 | "lint": "pnpm eslint --ext .js,.jsx,.ts,.tsx ./",
10 | "prepublishOnly": "pnpm run-s lint test build"
11 | },
12 | "main": "lib/index.js",
13 | "types": "lib/index.d.ts",
14 | "files": [
15 | "lib"
16 | ],
17 | "license": "MIT",
18 | "keywords": [
19 | "ice",
20 | "spa",
21 | "micro-frontends",
22 | "microfrontends"
23 | ],
24 | "bugs": {
25 | "url": "https://github.com/ice-lab/icestark/issues"
26 | },
27 | "homepage": "https://github.com/ice-lab/icestark",
28 | "repository": {
29 | "type": "git",
30 | "url": "git+https://github.com/ice-lab/icestark.git"
31 | },
32 | "devDependencies": {
33 | "typescript": "^3.4.4",
34 | "react": "^16.7.0",
35 | "@types/react": "^16.14.57"
36 | },
37 | "jest": {
38 | "coverageDirectory": "./coverage/",
39 | "collectCoverage": true,
40 | "preset": "ts-jest",
41 | "automock": false,
42 | "testMatch": [ "**/__tests__/**/*.ts?(x)", "**/?(*.)+(spec|test).ts?(x)" ],
43 | "setupFiles": [
44 | "../../setupJest.ts"
45 | ]
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/packages/icestark-app/src/AppLink.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import formatUrl from './util/formatUrl';
3 |
4 | interface To {
5 | /**
6 | * A string representing the path link to
7 | */
8 | pathname: string;
9 | /**
10 | * A string representing the url search to
11 | */
12 | search?: string;
13 | /**
14 | * A string representing the url state to
15 | */
16 | state?: object;
17 | }
18 | export type AppLinkProps = {
19 | to: string | To;
20 | hashType?: boolean;
21 | replace?: boolean;
22 | message?: string;
23 | children?: React.ReactNode;
24 | } & Pick, 'onClick' | 'href' | 'className' | 'style' | 'target'>;
25 |
26 | const AppLink = (props: AppLinkProps) => {
27 | const { to, hashType, replace, message, children, ...rest } = props;
28 |
29 | const _to = typeof to === 'object' ? `${to.pathname}${to.search ?? ''}` : to;
30 | const _state = typeof to === 'object' ? to.state : {};
31 |
32 | const linkTo = formatUrl(_to, hashType);
33 | return (
34 | {
38 | e.preventDefault();
39 | // eslint-disable-next-line no-alert
40 | if (message && window.confirm(message) === false) {
41 | return false;
42 | }
43 |
44 | /*
45 | * Bind `replaceState` and `pushState` to window to avoid illegal invocation error
46 | */
47 | const changeState = window.history[replace ? 'replaceState' : 'pushState'].bind(window);
48 |
49 | changeState(_state ?? {}, null, linkTo);
50 | }}
51 | >
52 | {children}
53 |
54 | );
55 | };
56 |
57 | export default AppLink;
58 |
--------------------------------------------------------------------------------
/packages/icestark-app/src/appHistory.ts:
--------------------------------------------------------------------------------
1 | import formatUrl from './util/formatUrl';
2 | import normalizeArgs from './util/normalizeArgs';
3 |
4 | const appHistory = {
5 | push: (url: string, state?: object | boolean, hashType?: boolean) => {
6 | const [_state, _hashType] = normalizeArgs(state, hashType);
7 | window.history.pushState(
8 | _state ?? {},
9 | null,
10 | formatUrl(url, _hashType),
11 | );
12 | },
13 | replace: (url: string, state?: object | boolean, hashType?: boolean) => {
14 | const [_state, _hashType] = normalizeArgs(state, hashType);
15 | window.history.replaceState(
16 | _state ?? {},
17 | null,
18 | formatUrl(url, _hashType),
19 | );
20 | },
21 | };
22 |
23 | export default appHistory;
24 |
--------------------------------------------------------------------------------
/packages/icestark-app/src/cache.ts:
--------------------------------------------------------------------------------
1 | const namespace = 'ICESTARK';
2 |
3 | export const setCache = (key: string, value: any): void => {
4 | if (!(window as any)[namespace]) {
5 | (window as any)[namespace] = {};
6 | }
7 | (window as any)[namespace][key] = value;
8 | };
9 |
10 | export const getCache = (key: string): any => {
11 | const icestark: any = (window as any)[namespace];
12 | return icestark && icestark[key] ? icestark[key] : null;
13 | };
14 |
--------------------------------------------------------------------------------
/packages/icestark-app/src/getBasename.ts:
--------------------------------------------------------------------------------
1 | import { getCache } from './cache';
2 |
3 | export default (): string => (getCache('basename') ? getCache('basename') : '/');
4 |
--------------------------------------------------------------------------------
/packages/icestark-app/src/getMountNode.ts:
--------------------------------------------------------------------------------
1 | import { getCache } from './cache';
2 |
3 | export default function getMountNode(element?: any): any {
4 | if (getCache('root')) {
5 | return getCache('root');
6 | }
7 |
8 | if (element) {
9 | // string treated as 'id'
10 | if (typeof element === 'string') {
11 | return document.querySelector(`#${element}`);
12 | }
13 |
14 | // function, return value
15 | if (typeof element === 'function') {
16 | return element();
17 | }
18 |
19 | return element;
20 | }
21 |
22 | const ICE_CONTAINER = document.querySelector('#ice-container');
23 | if (!ICE_CONTAINER) {
24 | throw new Error('Current page does not exist element.');
25 | }
26 |
27 | return ICE_CONTAINER;
28 | }
29 |
--------------------------------------------------------------------------------
/packages/icestark-app/src/index.ts:
--------------------------------------------------------------------------------
1 | export { default as getMountNode } from './getMountNode';
2 | export { default as renderNotFound } from './renderNotFound';
3 | export { default as getBasename } from './getBasename';
4 | export { default as setBasename } from './setBasename';
5 | export { default as registerAppEnter } from './registerAppEnter';
6 | export { default as registerAppLeave } from './registerAppLeave';
7 | export { default as appHistory } from './appHistory';
8 | export { default as isInIcestark } from './isInIcestark';
9 | export { default as AppLink } from './AppLink';
10 | export { default as setLibraryName } from './setLibraryName';
11 |
12 | export type { LifecycleProps } from './registerAppEnter';
13 |
--------------------------------------------------------------------------------
/packages/icestark-app/src/isInIcestark.ts:
--------------------------------------------------------------------------------
1 | import { getCache } from './cache';
2 |
3 | const isInIcestark = () => !!getCache('root');
4 |
5 | export default isInIcestark;
6 |
--------------------------------------------------------------------------------
/packages/icestark-app/src/registerAppEnter.ts:
--------------------------------------------------------------------------------
1 | import { setCache } from './cache';
2 |
3 | export interface LifecycleProps {
4 | container: HTMLElement | string;
5 | customProps?: object;
6 | }
7 |
8 | export default (callback?: (props: LifecycleProps) => void): void => {
9 | if (!callback) return;
10 |
11 | if (typeof callback !== 'function') {
12 | throw new Error('registerAppEnter must be function.');
13 | }
14 |
15 | setCache('appEnter', callback);
16 | };
17 |
--------------------------------------------------------------------------------
/packages/icestark-app/src/registerAppLeave.ts:
--------------------------------------------------------------------------------
1 | import { setCache } from './cache';
2 | import type { LifecycleProps } from './registerAppEnter';
3 |
4 | export default (callback?: (props: LifecycleProps) => void): void => {
5 | if (!callback) return;
6 |
7 | if (typeof callback !== 'function') {
8 | throw new Error('registerAppLeave must be function.');
9 | }
10 |
11 | setCache('appLeave', callback);
12 | };
13 |
--------------------------------------------------------------------------------
/packages/icestark-app/src/renderNotFound.ts:
--------------------------------------------------------------------------------
1 | import { getCache } from './cache';
2 |
3 | /**
4 | * CustomEvent Polyfill for IE
5 | */
6 | (function () {
7 | if (typeof (window as any).CustomEvent === 'function') return false;
8 |
9 | function CustomEvent(event, params) {
10 | params = params || { bubbles: false, cancelable: false, detail: null };
11 | const evt = document.createEvent('CustomEvent');
12 | evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
13 | return evt;
14 | }
15 |
16 | (window as any).CustomEvent = CustomEvent;
17 | })();
18 |
19 | /**
20 | * Trigger customEvent icestark:not-found
21 | */
22 | export default () => {
23 | if (getCache('root')) {
24 | window.dispatchEvent(new CustomEvent('icestark:not-found'));
25 |
26 | // Compatible processing return renderNotFound();
27 | return null;
28 | }
29 |
30 | return 'Current sub-application is running independently';
31 | };
32 |
--------------------------------------------------------------------------------
/packages/icestark-app/src/setBasename.ts:
--------------------------------------------------------------------------------
1 | import { setCache } from './cache';
2 |
3 | function setBasename(base: string) {
4 | return setCache('basename', base);
5 | }
6 |
7 | export default setBasename;
8 |
--------------------------------------------------------------------------------
/packages/icestark-app/src/setLibraryName.ts:
--------------------------------------------------------------------------------
1 | import { setCache } from './cache';
2 |
3 | const setLibraryName = (library: string | string[]): void => {
4 | if (!library) {
5 | console.error('[@ice/stark-app] setLibraryName: params can not be empty!');
6 | return;
7 | }
8 | setCache('library', library);
9 | };
10 |
11 | export default setLibraryName;
12 |
--------------------------------------------------------------------------------
/packages/icestark-app/src/util/formatUrl.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * format url
3 | * @param url
4 | * @param hashType
5 | */
6 | const formatUrl = (url: string, hashType?: boolean) => {
7 | return (hashType && url.indexOf('#') === -1) ? `#${url}` : url;
8 | };
9 |
10 | export default formatUrl;
11 |
--------------------------------------------------------------------------------
/packages/icestark-app/src/util/normalizeArgs.ts:
--------------------------------------------------------------------------------
1 | // `hashType' was relocated to the third argument.
2 | const isDev = process.env.NODE_ENV === 'development';
3 |
4 | const normalizeArgs = (state?: object | boolean, hashType?: boolean): [object, boolean] => {
5 | if (typeof state === 'boolean') {
6 | isDev && console.warn('[icestark]: hashType was relocated to the third argument.');
7 | return [{}, hashType ?? state];
8 | }
9 | if (typeof state === 'object') {
10 | return [state, hashType];
11 | }
12 |
13 | return [{}, hashType];
14 | };
15 |
16 | export default normalizeArgs;
17 |
--------------------------------------------------------------------------------
/packages/icestark-app/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "jsx": "react",
5 | "experimentalDecorators": true,
6 | "declaration": true,
7 | "skipLibCheck": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "outDir": "lib"
10 | },
11 | "include": ["src/*"],
12 | "exclude": ["node_modules"]
13 | }
14 |
--------------------------------------------------------------------------------
/packages/icestark-data/.eslintignore:
--------------------------------------------------------------------------------
1 | lib
2 | node_modules
3 | tests
4 | __tests__
--------------------------------------------------------------------------------
/packages/icestark-data/.eslintrc.js:
--------------------------------------------------------------------------------
1 |
2 | module.exports = {
3 | extends: '../../.eslintrc.js',
4 | };
5 |
--------------------------------------------------------------------------------
/packages/icestark-data/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## 0.1.3
4 |
5 | - [feat] support `Symbol` key for index. ([#298](https://github.com/ice-lab/icestark/issues/298))
--------------------------------------------------------------------------------
/packages/icestark-data/README.md:
--------------------------------------------------------------------------------
1 | # icestark-data
2 |
3 | > icestark sommunication solution. [icestark docs](https://ice-lab.github.io/icestark/).
4 |
5 | [](https://npmjs.org/package/@ice/stark-data) [](https://packagequality.com/#?package=@ice%2Fstark-data) [](https://travis-ci.org/ice-lab/icestark) [](https://codecov.io/gh/ice-lab/icestark) [](https://npmjs.org/package/@ice/stark-data) [](https://david-dm.org/ice-lab/icestark)
6 |
7 | ## Installation
8 |
9 | ```bash
10 | npm install @ice/stark-data --save
11 | ```
12 |
13 | ## API
14 |
15 | ### Store
16 |
17 | Global Store, unified management of all variables
18 |
19 | - get(key)
20 | - set(key, value)
21 | - on(key, callback, force), when `force` is true, callback will be called immediately when initializing
22 | - off(key, callback)
23 |
24 | #### example
25 |
26 | ```javascript
27 | // Framework
28 | import { store } from '@ice/stark-data';
29 |
30 | const userInfo = { name: 'Tom', age: 18 };
31 | store.set('user', userInfo); // set UserInfo
32 | store.set('language', 'CH');
33 |
34 | // Sub-application A
35 | import { store } from '@ice/stark-data';
36 |
37 | const userInfo = store.get('user'); // get UserInfo
38 |
39 | function showLang(lang) {
40 | console.log(`current language is ${lang}`);
41 | }
42 |
43 | store.on('language', showLang, true); // add callback for 'language', callback will be called whenever 'language' is changed
44 |
45 | store.off('language', showLang); // remove callback for 'language'
46 | ```
47 |
48 |
49 | ### Event
50 |
51 | Global Event, unified management of all events
52 |
53 | - on(key, callback) `callback` will be called with (...rest)
54 | - off(key, callback)
55 | - emit(key, ...rest)
56 |
57 | #### example
58 |
59 | ```javascript
60 | // Framework
61 | import { event } from '@ice/stark-data';
62 |
63 | function fresh(needFresh) {
64 | if (!needFresh) return;
65 |
66 | fetch('/api/fresh/message').then(res => {
67 | // ...
68 | });
69 | }
70 |
71 | event.on('freshMessage', fresh);
72 |
73 | // Sub-application A
74 | import { event } from '@ice/stark-data';
75 |
76 | event.emit('freshMessage', false);
77 | // ...
78 | event.emit('freshMessage', true);
79 | ```
80 |
81 | ## Contributors
82 |
83 | Feel free to report any questions as an [issue](https://github.com/ice-lab/icestark/issues/new), we'd love to have your helping hand on `icestark`.
84 |
85 | If you're interested in `icestark`, see [CONTRIBUTING.md](https://github.com/alibaba/ice/blob/master/.github/CONTRIBUTING.md) for more information to learn how to get started.
86 |
87 | ## License
88 |
89 | [MIT](LICENSE)
90 |
--------------------------------------------------------------------------------
/packages/icestark-data/__tests__/cache.spec.tsx:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom/extend-expect';
2 |
3 | import { getCache, setCache } from '../src/cache';
4 |
5 | const namespace = 'ICESTARK';
6 |
7 | describe('cache', () => {
8 | test('getCache', () => {
9 | window[namespace] = null;
10 | expect(getCache('name')).toBeNull();
11 |
12 | window[namespace] = {};
13 | expect(getCache('name')).toBeNull();
14 |
15 | window[namespace] = { name: 'TOM' };
16 | expect(getCache('name')).toBe('TOM');
17 | });
18 |
19 | test('getCache', () => {
20 | window[namespace] = null;
21 |
22 | setCache('testSet', {});
23 | expect(window[namespace].testSet).toStrictEqual({});
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/packages/icestark-data/__tests__/event.spec.tsx:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom/extend-expect';
2 |
3 | import event from '../src/event';
4 |
5 | const namespace = 'ICESTARK';
6 | const eventNamespace = 'event';
7 |
8 | describe('event', () => {
9 | test('event.on', () => {
10 | const warnMockFn = jest.fn();
11 | (global as any).console = {
12 | warn: warnMockFn,
13 | };
14 |
15 | event.on([]);
16 | expect(warnMockFn).toBeCalledWith('event.on: key should be string / symbol');
17 |
18 | event.on('testOn');
19 | expect(warnMockFn).toBeCalledWith('event.on: callback is required, should be function');
20 | event.on('testOn', {});
21 | expect(warnMockFn).toBeCalledWith('event.on: callback is required, should be function');
22 |
23 | const testFunc = jest.fn();
24 | event.on('testOn', testFunc);
25 | expect(testFunc).toBeCalledTimes(0);
26 | expect(window[namespace][eventNamespace].eventEmitter.testOn[0]).toBe(testFunc);
27 | });
28 |
29 | test('event.off', () => {
30 | const warnMockFn = jest.fn();
31 | (global as any).console = {
32 | warn: warnMockFn,
33 | };
34 |
35 | event.off([]);
36 | expect(warnMockFn).toBeCalledWith('event.off: key should be string / symbol');
37 |
38 | event.off('testOff');
39 | expect(warnMockFn).toBeCalledWith('event.off: testOff has no callback');
40 |
41 | const testFunc = jest.fn();
42 | window[namespace][eventNamespace].eventEmitter.testOff = [testFunc];
43 | event.off('testOff');
44 | expect(window[namespace][eventNamespace].eventEmitter.testOff).toBeUndefined();
45 |
46 | window[namespace][eventNamespace].eventEmitter.testOff = [testFunc];
47 | event.off('testOff', testFunc);
48 | expect(window[namespace][eventNamespace].eventEmitter.testOff).toStrictEqual([]);
49 | });
50 |
51 | test('event.emit', () => {
52 | const warnMockFn = jest.fn();
53 | (global as any).console = {
54 | warn: warnMockFn,
55 | };
56 |
57 | const testFunc = jest.fn();
58 | const testFunc2 = jest.fn();
59 | window[namespace][eventNamespace].eventEmitter.testEmit = [testFunc, testFunc2];
60 |
61 | event.emit('testEmit', 'testData');
62 | expect(testFunc).toBeCalledWith('testData');
63 | expect(testFunc2).toBeCalledWith('testData');
64 |
65 | event.emit('testEmit', 'testData2');
66 | expect(testFunc).toBeCalledWith('testData2');
67 | expect(testFunc2).toBeCalledWith('testData2');
68 |
69 | window[namespace][eventNamespace].eventEmitter.testEmit = [];
70 | event.emit('testEmit', 'testData2');
71 | expect(warnMockFn).toBeCalledWith('event.emit: no callback is called for testEmit');
72 |
73 | window[namespace][eventNamespace].eventEmitter.testEmit = undefined;
74 | event.emit('testEmit', 'testData2');
75 | expect(warnMockFn).toBeCalledWith('event.emit: no callback is called for testEmit');
76 | });
77 |
78 | test('event.has', () => {
79 | window[namespace][eventNamespace].eventEmitter.testHas = undefined;
80 | expect(event.has('testHas')).toBe(false);
81 |
82 | const testFunc = jest.fn();
83 | const testFunc2 = jest.fn();
84 | window[namespace][eventNamespace].eventEmitter.testHas = [testFunc, testFunc2];
85 | expect(event.has('testHas')).toBe(true);
86 | });
87 | });
88 |
--------------------------------------------------------------------------------
/packages/icestark-data/__tests__/index.spec.tsx:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom/extend-expect';
2 |
3 | import { store, event } from '../src/index';
4 |
5 | describe('store', () => {
6 | test('store', () => {
7 | const warnMockFn = jest.fn();
8 | (global as any).console = {
9 | warn: warnMockFn,
10 | };
11 |
12 | store.set('user', { name: 'Tom', age: 11 });
13 | const userInfo = store.get('user');
14 | expect(userInfo.name).toBe('Tom');
15 | expect(userInfo.age).toBe(11);
16 |
17 | expect(store.has('language')).toBe(false);
18 |
19 | const changeLang = jest.fn();
20 | store.on('language', changeLang, true);
21 | expect(changeLang).toBeCalledWith(undefined);
22 |
23 | store.set('language', 'CH');
24 | expect(changeLang).toBeCalledWith('CH');
25 | store.set('language', 'EN');
26 | expect(changeLang).toBeCalledWith('EN');
27 | expect(changeLang).toBeCalledTimes(3);
28 |
29 | expect(store.has('language')).toBe(true);
30 |
31 | store.off('language', changeLang);
32 | expect(changeLang).toBeCalledTimes(3);
33 | expect(store.has('language')).toBe(false);
34 |
35 | store.on('language', changeLang);
36 | const changeLang2 = jest.fn();
37 | store.on('language', changeLang2, true);
38 | expect(changeLang2).toBeCalledTimes(1);
39 | expect(store.has('language')).toBe(true);
40 | });
41 | });
42 |
43 | describe('event', () => {
44 | test('event', () => {
45 | const warnMockFn = jest.fn();
46 | (global as any).console = {
47 | warn: warnMockFn,
48 | };
49 |
50 | const testFunc = jest.fn();
51 | const testFunc2 = jest.fn();
52 |
53 | expect(event.has('testEvent')).toBe(false);
54 |
55 | event.on('testEvent', testFunc);
56 | expect(event.has('testEvent')).toBe(true);
57 |
58 | event.emit('testEvent', 'testData');
59 | expect(testFunc).toBeCalledWith('testData');
60 |
61 | event.on('testEvent', testFunc2);
62 | expect(event.has('testEvent')).toBe(true);
63 |
64 | event.emit('testEvent', 'testData');
65 | expect(testFunc).toBeCalledWith('testData');
66 | expect(testFunc).toBeCalledTimes(2);
67 | expect(testFunc2).toBeCalledWith('testData');
68 |
69 | event.off('testEvent', testFunc);
70 | expect(event.has('testEvent')).toBe(true);
71 |
72 | event.emit('testEvent', 'testData');
73 | expect(testFunc).toBeCalledTimes(2);
74 | expect(testFunc2).toBeCalledWith('testData');
75 | expect(testFunc2).toBeCalledTimes(2);
76 |
77 | event.off('testEvent', testFunc2);
78 | expect(event.has('testEvent')).toBe(false);
79 | });
80 | });
81 |
--------------------------------------------------------------------------------
/packages/icestark-data/__tests__/store.spec.tsx:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom/extend-expect';
2 |
3 | import store from '../src/store';
4 |
5 | const namespace = 'ICESTARK';
6 | const storeNamespace = 'store';
7 |
8 | describe('store', () => {
9 | test('store.get', () => {
10 | const warnMockFn = jest.fn();
11 | (global as any).console = {
12 | warn: warnMockFn,
13 | };
14 |
15 | expect(store.get()).toStrictEqual({});
16 |
17 | store.get([]);
18 | expect(warnMockFn).toBeCalledWith('store.get: key should be string / symbol');
19 |
20 | expect(store.get('test')).toBeUndefined();
21 | });
22 |
23 | test('store.set', () => {
24 | const warnMockFn = jest.fn();
25 | (global as any).console = {
26 | warn: warnMockFn,
27 | };
28 |
29 | store.set([]);
30 | expect(warnMockFn).toBeCalledWith('store.set: key should be string / symbol / object');
31 |
32 | const testArray = [];
33 | const testObj = {};
34 | const testFunc = () => {};
35 | store.set({ name: 'TOM', age: 18, testArray, testObj, testFunc });
36 | expect(window[namespace][storeNamespace].store.name).toBe('TOM');
37 | expect(window[namespace][storeNamespace].store.age).toBe(18);
38 | expect(window[namespace][storeNamespace].store.testArray).toBe(testArray);
39 | expect(window[namespace][storeNamespace].store.testObj).toBe(testObj);
40 | expect(window[namespace][storeNamespace].store.testFunc).toBe(testFunc);
41 |
42 | store.set('name', 'LUCY');
43 | expect(window[namespace][storeNamespace].store.name).toBe('LUCY');
44 | });
45 |
46 | test('store.on', () => {
47 | const warnMockFn = jest.fn();
48 | (global as any).console = {
49 | warn: warnMockFn,
50 | };
51 |
52 | store.on([]);
53 | expect(warnMockFn).toBeCalledWith('store.on: key should be string / symbol');
54 |
55 | store.on('testOn');
56 | expect(warnMockFn).toBeCalledWith('store.on: callback is required, should be function');
57 | store.on('testOn', {});
58 | expect(warnMockFn).toBeCalledWith('store.on: callback is required, should be function');
59 |
60 | const testFunc = jest.fn();
61 | store.on('testOn', testFunc);
62 | expect(testFunc).toBeCalledTimes(0);
63 | expect(window[namespace][storeNamespace].storeEmitter.testOn[0]).toBe(testFunc);
64 |
65 | const testFuncForce = jest.fn();
66 | store.on('testOnForce', testFuncForce, true);
67 | expect(testFuncForce).toBeCalledTimes(1);
68 | expect(window[namespace][storeNamespace].storeEmitter.testOnForce[0]).toBe(testFuncForce);
69 | });
70 |
71 | test('store.off', () => {
72 | const warnMockFn = jest.fn();
73 | (global as any).console = {
74 | warn: warnMockFn,
75 | };
76 |
77 | store.off([]);
78 | expect(warnMockFn).toBeCalledWith('store.off: key should be string / symbol');
79 |
80 | store.off('testOff');
81 | expect(warnMockFn).toBeCalledWith('store.off: testOff has no callback');
82 |
83 | const testFunc = jest.fn();
84 | window[namespace][storeNamespace].storeEmitter.testOff = [testFunc];
85 | store.off('testOff');
86 | expect(window[namespace][storeNamespace].storeEmitter.testOff).toBeUndefined();
87 |
88 | window[namespace][storeNamespace].storeEmitter.testOff = [testFunc];
89 | store.off('testOff', testFunc);
90 | expect(window[namespace][storeNamespace].storeEmitter.testOff).toStrictEqual([]);
91 | });
92 | });
93 |
--------------------------------------------------------------------------------
/packages/icestark-data/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@ice/stark-data",
3 | "version": "0.1.3",
4 | "description": "icestark-data is a JavaScript library for icestark, used for communication.",
5 | "scripts": {
6 | "build": "rm -rf lib && tsc",
7 | "watch": "tsc -w",
8 | "test": "NODE_ENV=unittest pnpm jest",
9 | "lint": "pnpm eslint --ext .js,.jsx,.ts,.tsx ./",
10 | "prepublishOnly": "pnpm run-s lint test build"
11 | },
12 | "main": "lib/index.js",
13 | "types": "lib/index.d.ts",
14 | "files": [
15 | "lib"
16 | ],
17 | "license": "MIT",
18 | "keywords": [
19 | "ice",
20 | "spa",
21 | "micro-frontends",
22 | "microfrontends"
23 | ],
24 | "bugs": {
25 | "url": "https://github.com/ice-lab/icestark/issues"
26 | },
27 | "homepage": "https://github.com/ice-lab/icestark",
28 | "repository": {
29 | "type": "git",
30 | "url": "git+https://github.com/ice-lab/icestark.git"
31 | },
32 | "devDependencies": {
33 | "typescript": "^3.4.4"
34 | },
35 | "jest": {
36 | "coverageDirectory": "./coverage/",
37 | "collectCoverage": true,
38 | "preset": "ts-jest",
39 | "automock": false,
40 | "testMatch": [ "**/__tests__/**/*.ts?(x)", "**/?(*.)+(spec|test).ts?(x)" ],
41 | "setupFiles": [
42 | "../../setupJest.ts"
43 | ]
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/packages/icestark-data/src/cache.ts:
--------------------------------------------------------------------------------
1 | const namespace = 'ICESTARK';
2 |
3 | export const setCache = (key: string, value: any): void => {
4 | if (!(window as any)[namespace]) {
5 | (window as any)[namespace] = {};
6 | }
7 | (window as any)[namespace][key] = value;
8 | };
9 |
10 | export const getCache = (key: string): any => {
11 | const icestark: any = (window as any)[namespace];
12 | return icestark && icestark[key] ? icestark[key] : null;
13 | };
14 |
--------------------------------------------------------------------------------
/packages/icestark-data/src/event.ts:
--------------------------------------------------------------------------------
1 | /* eslint import/no-mutable-exports: 'off' */
2 |
3 | import { isArray, warn } from './utils';
4 | import { setCache, getCache } from './cache';
5 |
6 | const eventNameSpace = 'event';
7 |
8 | type StringSymbolUnion = string | symbol;
9 |
10 | interface Hooks {
11 | emit(key: StringSymbolUnion, value: any): void;
12 | on(key: StringSymbolUnion, callback: (value: any) => void): void;
13 | off(key: StringSymbolUnion, callback?: (value: any) => void): void;
14 | has(key: StringSymbolUnion): boolean;
15 | }
16 |
17 | class Event implements Hooks {
18 | eventEmitter: object;
19 |
20 | constructor() {
21 | this.eventEmitter = {};
22 | }
23 |
24 | emit(key: StringSymbolUnion, ...args) {
25 | const keyEmitter = this.eventEmitter[key];
26 |
27 | if (!isArray(keyEmitter) || (isArray(keyEmitter) && keyEmitter.length === 0)) {
28 | warn(`event.emit: no callback is called for ${String(key)}`);
29 | return;
30 | }
31 |
32 | keyEmitter.forEach(cb => {
33 | cb(...args);
34 | });
35 | }
36 |
37 | on(key: StringSymbolUnion, callback: (value: any) => void) {
38 | if (typeof key !== 'string' && typeof key !== 'symbol') {
39 | warn('event.on: key should be string / symbol');
40 | return;
41 | }
42 | if (callback === undefined || typeof callback !== 'function') {
43 | warn('event.on: callback is required, should be function');
44 | return;
45 | }
46 |
47 | if (!this.eventEmitter[key]) {
48 | this.eventEmitter[key] = [];
49 | }
50 |
51 | this.eventEmitter[key].push(callback);
52 | }
53 |
54 | off(key: StringSymbolUnion, callback?: (value: any) => void) {
55 | if (typeof key !== 'string' && typeof key !== 'symbol') {
56 | warn('event.off: key should be string / symbol');
57 | return;
58 |
59 | }
60 |
61 | if (!isArray(this.eventEmitter[key])) {
62 | warn(`event.off: ${String(key)} has no callback`);
63 | return;
64 | }
65 |
66 | if (callback === undefined) {
67 | this.eventEmitter[key] = undefined;
68 | return;
69 | }
70 |
71 | this.eventEmitter[key] = this.eventEmitter[key].filter(cb => cb !== callback);
72 | }
73 |
74 | has(key: StringSymbolUnion) {
75 | const keyEmitter = this.eventEmitter[key];
76 | return isArray(keyEmitter) && keyEmitter.length > 0;
77 | }
78 | }
79 |
80 | let event = getCache(eventNameSpace);
81 | if (!event) {
82 | event = new Event();
83 | setCache(eventNameSpace, event);
84 | }
85 |
86 | export default event;
87 |
--------------------------------------------------------------------------------
/packages/icestark-data/src/index.ts:
--------------------------------------------------------------------------------
1 | export { default as store } from './store';
2 | export { default as event } from './event';
3 |
--------------------------------------------------------------------------------
/packages/icestark-data/src/store.ts:
--------------------------------------------------------------------------------
1 | /* eslint no-underscore-dangle: ["error", { "allow": ["foo_", "_bar"], "allowAfterThis": true }] */
2 | /* eslint import/no-mutable-exports: 'off' */
3 |
4 | import { isObject, isArray, warn } from './utils';
5 | import { setCache, getCache } from './cache';
6 |
7 | const storeNameSpace = 'store';
8 |
9 | type StringSymbolUnion = string | symbol;
10 |
11 | interface IO {
12 | set(key: string | symbol | object, value?: any): void;
13 | get(key?: StringSymbolUnion): void;
14 | }
15 |
16 | interface Hooks {
17 | on(key: StringSymbolUnion, callback: (value: any) => void, force?: boolean): void;
18 | off(key: StringSymbolUnion, callback?: (value: any) => void): void;
19 | has(key: StringSymbolUnion): boolean;
20 | }
21 |
22 | class Store implements IO, Hooks {
23 | store: object;
24 |
25 | storeEmitter: object;
26 |
27 | constructor() {
28 | this.store = {};
29 | this.storeEmitter = {};
30 | }
31 |
32 | _getValue(key: StringSymbolUnion) {
33 | return this.store[key];
34 | }
35 |
36 | _setValue(key: StringSymbolUnion, value: any) {
37 | this.store[key] = value;
38 | this._emit(key);
39 | }
40 |
41 | _emit(key: StringSymbolUnion) {
42 | const keyEmitter = this.storeEmitter[key];
43 |
44 | if (!isArray(keyEmitter) || (isArray(keyEmitter) && keyEmitter.length === 0)) {
45 | return;
46 | }
47 |
48 | const value = this._getValue(key);
49 | keyEmitter.forEach(cb => {
50 | cb(value);
51 | });
52 | }
53 |
54 | get(key?: StringSymbolUnion) {
55 | if (key === undefined) {
56 | return this.store;
57 | }
58 |
59 | if (typeof key !== 'string' && typeof key !== 'symbol') {
60 | warn('store.get: key should be string / symbol');
61 | return null;
62 | }
63 |
64 | return this._getValue(key);
65 | }
66 |
67 | set(key: string | symbol | object, value?: T) {
68 | if (typeof key !== 'string'
69 | && typeof key !== 'symbol'
70 | && !isObject(key)) {
71 | warn('store.set: key should be string / symbol / object');
72 | return;
73 | }
74 |
75 | if (isObject(key)) {
76 | Object.keys(key).forEach(k => {
77 | const v = key[k];
78 |
79 | this._setValue(k, v);
80 | });
81 | } else {
82 | this._setValue(key as StringSymbolUnion, value);
83 | }
84 | }
85 |
86 | on(key: StringSymbolUnion, callback: (value: any) => void, force?: boolean) {
87 | if (typeof key !== 'string' && typeof key !== 'symbol') {
88 | warn('store.on: key should be string / symbol');
89 | return;
90 | }
91 |
92 | if (callback === undefined || typeof callback !== 'function') {
93 | warn('store.on: callback is required, should be function');
94 | return;
95 | }
96 |
97 | if (!this.storeEmitter[key]) {
98 | this.storeEmitter[key] = [];
99 | }
100 |
101 | this.storeEmitter[key].push(callback);
102 |
103 | if (force) {
104 | callback(this._getValue(key));
105 | }
106 | }
107 |
108 | off(key: StringSymbolUnion, callback?: (value: any) => void) {
109 | if (typeof key !== 'string' && typeof key !== 'symbol') {
110 | warn('store.off: key should be string / symbol');
111 | return;
112 | }
113 |
114 | if (!isArray(this.storeEmitter[key])) {
115 | warn(`store.off: ${String(key)} has no callback`);
116 | return;
117 | }
118 |
119 | if (callback === undefined) {
120 | this.storeEmitter[key] = undefined;
121 | return;
122 | }
123 |
124 | this.storeEmitter[key] = this.storeEmitter[key].filter(cb => cb !== callback);
125 | }
126 |
127 | has(key: StringSymbolUnion) {
128 | const keyEmitter = this.storeEmitter[key];
129 | return isArray(keyEmitter) && keyEmitter.length > 0;
130 | }
131 | }
132 |
133 | let store = getCache(storeNameSpace);
134 | if (!store) {
135 | store = new Store();
136 | setCache(storeNameSpace, store);
137 | }
138 |
139 | export default store;
140 |
--------------------------------------------------------------------------------
/packages/icestark-data/src/utils.ts:
--------------------------------------------------------------------------------
1 | export function isObject(o: any): boolean {
2 | return Object.prototype.toString.call(o) === '[object Object]';
3 | }
4 |
5 | export function isArray(a: any): boolean {
6 | return Object.prototype.toString.call(a) === '[object Array]';
7 | }
8 |
9 | export function warn(message: string): void {
10 | return console && console.warn(message);
11 | }
12 |
--------------------------------------------------------------------------------
/packages/icestark-data/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "jsx": "react",
5 | "experimentalDecorators": true,
6 | "declaration": true,
7 | "skipLibCheck": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "outDir": "lib"
10 | },
11 | "include": ["src/*"],
12 | "exclude": ["node_modules"]
13 | }
14 |
--------------------------------------------------------------------------------
/packages/icestark-module/.eslintignore:
--------------------------------------------------------------------------------
1 | lib
2 | node_modules
3 | tests
4 | __tests__
--------------------------------------------------------------------------------
/packages/icestark-module/.eslintrc.js:
--------------------------------------------------------------------------------
1 |
2 | module.exports = {
3 | extends: '../../.eslintrc.js',
4 | };
5 |
--------------------------------------------------------------------------------
/packages/icestark-module/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## 1.5.0
4 |
5 | - [fix] exports all library exports. ([#469](https://github.com/ice-lab/icestark/pull/469))
6 |
7 | ## 1.4.3
8 |
9 | - [fix] avoid unmounting when `` has not yet been mounted.
10 |
11 | ## 1.4.2
12 |
13 | - [feat] append sourceURL to js resources to make sourcemaps work.
14 |
15 | ## 1.4.1
16 |
17 | - [refact] compatible with sandbox spell error.
18 |
19 | ## 1.4.0
20 |
21 | - [feat] support local component when using ``. ([#205](https://github.com/ice-lab/icestark/issues/205))
22 | - [feat] moudles can be registerd more then once, but only the last will be kept.
23 | ## 1.3.1
24 |
25 | - [fix] eliminate race condition when using `parseRuntime`. ([#257](https://github.com/ice-lab/icestark/issues/257))
26 |
27 | ## 1.3.0
28 |
29 | - [feat] provide a built-in strategy to handle externals. ([#252](https://github.com/ice-lab/icestark/issues/252))
30 |
--------------------------------------------------------------------------------
/packages/icestark-module/__tests__/loader.spec.ts:
--------------------------------------------------------------------------------
1 | import * as fs from 'fs';
2 | import * as path from 'path';
3 | import Sandbox from '@ice/sandbox';
4 | import ModuleLoader from '../src/loader';
5 |
6 | declare global {
7 | interface Window {
8 | fetch?: any;
9 | }
10 | }
11 |
12 | describe('module loader', () => {
13 | const source = fs.readFileSync(path.resolve(__dirname, './component.js'));
14 | beforeEach(() => {
15 | // mock fetch
16 | window.fetch = (url) => {
17 | const isSource = url.indexOf('source') > 0;
18 | return Promise.resolve({
19 | text: () => isSource ? source.toString() : url,
20 | });
21 | };
22 | });
23 | const moduleLoader = new ModuleLoader();
24 |
25 | test('load module', async () => {
26 | const task = moduleLoader.load({
27 | url: '//localhost',
28 | name: 'test',
29 | });
30 | const res = await task;
31 | expect(res).toEqual(['//localhost \n //# sourceURL=//localhost']);
32 | });
33 |
34 | test('load cache', async () => {
35 | const task = moduleLoader.load({ name: 'test', url: '//localhost2' });
36 | const res = await task;
37 | expect(res).toEqual(['//localhost \n //# sourceURL=//localhost']);
38 | });
39 |
40 | test('load source', async () => {
41 | const task = moduleLoader.load({ name: 'testsource', url: '//source' });
42 | const res = await task;
43 | expect(res).toEqual([`${source.toString()} \n //# sourceURL=//source`]);
44 | });
45 |
46 | test('execute module', async () => {
47 | const moduleInfo = await moduleLoader.execModule({ name: 'modulename', url: '//source' });
48 | expect(!!moduleInfo.default).toBe(true);
49 | });
50 |
51 | test('excute module in sandbox', async () => {
52 | const sandbox = new Sandbox({ multiMode: true });
53 | const moduleInfo = await moduleLoader.execModule({ name: 'modulename', url: '//source' }, sandbox);
54 | expect(!!moduleInfo.default).toBe(true);
55 | });
56 | });
--------------------------------------------------------------------------------
/packages/icestark-module/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@ice/stark-module",
3 | "version": "1.5.0",
4 | "description": "toolkit for load standard micro-module",
5 | "main": "lib/index.js",
6 | "scripts": {
7 | "build": "rm -rf lib && tsc",
8 | "watch": "tsc -w",
9 | "test": "NODE_ENV=unittest pnpm jest",
10 | "lint": "pnpm eslint --ext .js,.jsx,.ts,.tsx ./",
11 | "prepublishOnly": "pnpm run-s lint test build"
12 | },
13 | "repository": {
14 | "type": "git",
15 | "url": "git+https://github.com/ice-lab/icestark.git"
16 | },
17 | "files": [
18 | "lib"
19 | ],
20 | "keywords": [
21 | "modules",
22 | "icestark"
23 | ],
24 | "author": "",
25 | "license": "MIT",
26 | "bugs": {
27 | "url": "https://github.com/ice-lab/icestark/issues"
28 | },
29 | "homepage": "https://github.com/ice-lab/icestark#readme",
30 | "peerDependencies": {
31 | "react": ">=15.0.0",
32 | "react-dom": ">=15.0.0"
33 | },
34 | "devDependencies": {
35 | "react": "^16.13.1",
36 | "react-dom": "^16.13.1",
37 | "typescript": "^3.8.3",
38 | "@types/react": "^16.14.57",
39 | "@types/react-dom": "^16.9.25"
40 | },
41 | "dependencies": {
42 | "@ice/sandbox": "^1.1.0"
43 | },
44 | "jest": {
45 | "coverageDirectory": "./coverage/",
46 | "collectCoverage": true,
47 | "preset": "ts-jest",
48 | "automock": false,
49 | "testMatch": [ "**/__tests__/**/*.ts?(x)", "**/?(*.)+(spec|test).ts?(x)" ],
50 | "setupFiles": [
51 | "../../setupJest.ts"
52 | ]
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/packages/icestark-module/src/assist.ts:
--------------------------------------------------------------------------------
1 | export function shallowCompare(source: T, target: T) {
2 | return Object.keys(source).every((key) => source[key] === target[key]);
3 | }
4 |
--------------------------------------------------------------------------------
/packages/icestark-module/src/global.ts:
--------------------------------------------------------------------------------
1 | // fork: https://github.com/systemjs/systemjs/blob/master/src/extras/global.js
2 |
3 | // safari unpredictably lists some new globals first or second in object order
4 | let firstGlobalProp;
5 | let secondGlobalProp;
6 | let lastGlobalProp;
7 | let noteGlobalKeys = [];
8 | const isIE11 = typeof navigator !== 'undefined' && navigator.userAgent.indexOf('Trident') !== -1;
9 |
10 | function shouldSkipProperty(p, globalWindow) {
11 | // eslint-disable-next-line no-prototype-builtins
12 | return !globalWindow.hasOwnProperty(p)
13 | || !isNaN(p) && p < (globalWindow as any).length
14 | || isIE11 && globalWindow[p] && typeof window !== 'undefined' && globalWindow[p].parent === window;
15 | }
16 |
17 | export function getGlobalProp(globalWindow) {
18 | let cnt = 0;
19 | let lastProp;
20 | // eslint-disable-next-line no-restricted-syntax
21 | for (const p in globalWindow) {
22 | // do not check frames cause it could be removed during import
23 | if (shouldSkipProperty(p, globalWindow)) { continue; }
24 | if (cnt === 0 && p !== firstGlobalProp || cnt === 1 && p !== secondGlobalProp) { return p; }
25 | cnt++;
26 | lastProp = p;
27 | }
28 | if (lastProp !== lastGlobalProp) {
29 | return lastProp;
30 | } else {
31 | // polyfill for UC browser which lastprops will alway be window
32 | // eslint-disable-next-line no-restricted-syntax
33 | for (const p in globalWindow) {
34 | if (!noteGlobalKeys.includes(p)) {
35 | lastProp = p;
36 | }
37 | }
38 | return lastProp;
39 | }
40 | }
41 |
42 | export function noteGlobalProps(globalWindow) {
43 | // alternatively Object.keys(global).pop()
44 | // but this may be faster (pending benchmarks)
45 | firstGlobalProp = undefined;
46 | secondGlobalProp = undefined;
47 | noteGlobalKeys = Object.keys(globalWindow);
48 | // eslint-disable-next-line no-restricted-syntax
49 | for (const p in globalWindow) {
50 | // do not check frames cause it could be removed during import
51 | if (shouldSkipProperty(p, globalWindow)) { continue; }
52 | if (!firstGlobalProp) { firstGlobalProp = p; } else if (!secondGlobalProp) { secondGlobalProp = p; }
53 | lastGlobalProp = p;
54 | }
55 | return lastGlobalProp;
56 | }
57 |
--------------------------------------------------------------------------------
/packages/icestark-module/src/index.ts:
--------------------------------------------------------------------------------
1 | import {
2 | StarkModule,
3 | registerModule,
4 | registerModules,
5 | getModules,
6 | mountModule,
7 | unmoutModule,
8 | clearModules,
9 | } from './modules';
10 | import MicroModule from './MicroModule';
11 |
12 | export {
13 | StarkModule,
14 | MicroModule,
15 | registerModule,
16 | registerModules,
17 | clearModules,
18 | getModules,
19 | mountModule,
20 | unmoutModule,
21 | };
--------------------------------------------------------------------------------
/packages/icestark-module/src/loader.ts:
--------------------------------------------------------------------------------
1 | import Sandbox from '@ice/sandbox';
2 | import { getGlobalProp, noteGlobalProps } from './global';
3 | import { StarkModule } from './modules';
4 |
5 | export interface ImportTask {
6 | [name: string]: Promise;
7 | }
8 |
9 | export type PromiseModule = Promise;
10 |
11 | export interface Fetch {
12 | (input: RequestInfo, init?: RequestInit): Promise;
13 | }
14 |
15 | export default class ModuleLoader {
16 | private importTask: ImportTask = {};
17 |
18 | load(starkModule: StarkModule, fetch: Fetch = window.fetch): Promise {
19 | const { url, name } = starkModule;
20 | if (this.importTask[name]) {
21 | // return promise if current module is pending or resolved
22 | return this.importTask[name];
23 | }
24 | const urls = Array.isArray(url) ? url : [url];
25 |
26 | const task = Promise.all(
27 | urls.map(
28 | (scriptUrl) => fetch(scriptUrl)
29 | .then((res) => res.text())
30 | .then((res) => `${res} \n //# sourceURL=${scriptUrl}`),
31 | ),
32 | );
33 | this.importTask[name] = task;
34 | return task;
35 | }
36 |
37 | removeTask(name: string) {
38 | delete this.importTask[name];
39 | }
40 |
41 | clearTask() {
42 | this.importTask = {};
43 | }
44 |
45 | execModule(starkModule: StarkModule, sandbox?: Sandbox, deps?: object) {
46 | return this.load(starkModule).then((sources) => {
47 | let globalWindow = null;
48 | if (sandbox?.getSandbox) {
49 | sandbox.createProxySandbox(deps);
50 | globalWindow = sandbox.getSandbox();
51 | } else {
52 | globalWindow = window;
53 | }
54 | const { name } = starkModule;
55 | let libraryExport = '';
56 | // excute script in order
57 | try {
58 | sources.forEach((source, index) => {
59 | const lastScript = index === sources.length - 1;
60 | if (lastScript) {
61 | noteGlobalProps(globalWindow);
62 | }
63 | // check sandbox
64 | if (sandbox?.execScriptInSandbox) {
65 | sandbox.execScriptInSandbox(source);
66 | } else {
67 | // https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/eval
68 | // eslint-disable-next-line no-eval
69 | (0, eval)(source);
70 | }
71 | if (lastScript) {
72 | libraryExport = getGlobalProp(globalWindow);
73 | }
74 | });
75 | } catch (err) {
76 | console.error(err);
77 | }
78 | const moduleInfo = libraryExport ? (globalWindow as any)[libraryExport] : ((globalWindow as any)[name] || {});
79 | // remove moduleInfo from globalWindow in case of excute multi module in globalWindow
80 | if ((globalWindow as any)[libraryExport]) {
81 | delete globalWindow[libraryExport];
82 | }
83 | return moduleInfo;
84 | });
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/packages/icestark-module/src/runtimeHelper.ts:
--------------------------------------------------------------------------------
1 | import Sandbox from '@ice/sandbox';
2 | import { any2AnyArray } from './utils';
3 | import { parseUrlAssets, appendCSS } from './modules';
4 |
5 | /**
6 | * CustomEvent Polyfill for IE.
7 | * See https://gist.github.com/gt3/787767e8cbf0451716a189cdcb2a0d08.
8 | */
9 | (function () {
10 | if (typeof (window as any).CustomEvent === 'function') return false;
11 |
12 | function CustomEvent(event, params) {
13 | params = params || { bubbles: false, cancelable: false, detail: null };
14 | const evt = document.createEvent('CustomEvent');
15 | evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
16 | return evt;
17 | }
18 |
19 | (window as any).CustomEvent = CustomEvent;
20 | })();
21 |
22 | export interface RuntimeInstance {
23 | id: string;
24 | url: string;
25 | }
26 |
27 | type CombineRuntime = Pick & { url?: string | string[] };
28 |
29 | export type Runtime = boolean | string | RuntimeInstance[];
30 |
31 | export type AssetState = 'INIT' | 'LOADING' | 'LOAD_ERROR' | 'LOADED';
32 |
33 | interface Json {
34 | [id: string]: T;
35 | }
36 |
37 | interface RuntimeCache {
38 | deps: object;
39 | state: AssetState;
40 | }
41 |
42 | const runtimeCache: Json = {};
43 |
44 | /**
45 | * excute one or multi runtime in serial.
46 | */
47 | export function execute(codes: string | string[], deps: object, sandbox = new Sandbox({ multiMode: true }) as Sandbox) {
48 | sandbox.createProxySandbox(deps);
49 |
50 | any2AnyArray(codes).forEach((code) => sandbox.execScriptInSandbox(code));
51 |
52 | const addedProperties = sandbox.getAddedProperties();
53 | sandbox.clear();
54 | return addedProperties;
55 | }
56 |
57 | export function updateRuntimeState(mark: string, state: AssetState) {
58 | if (!runtimeCache[mark]) {
59 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
60 | runtimeCache[mark] = {} as any;
61 | }
62 | runtimeCache[mark].state = state;
63 | }
64 |
65 | /**
66 | * fetch, excute then cache runtime info.
67 | */
68 | export async function cacheDeps(runtime: CombineRuntime, deps: object, fetch = window.fetch) {
69 | const { id, url } = runtime;
70 | const mark = id;
71 |
72 | if (runtimeCache[mark]?.state === 'LOADING') {
73 | // await util resource loaded or error
74 | await new Promise((resolve) => window.addEventListener(mark, resolve));
75 | }
76 |
77 | if (runtimeCache[mark]?.state === 'LOADED') {
78 | return runtimeCache[mark]?.deps;
79 | }
80 |
81 | updateRuntimeState(mark, 'LOADING');
82 |
83 | const { cssList, jsList } = parseUrlAssets(url);
84 |
85 | // append css
86 | Promise.all(cssList.map((css: string) => appendCSS(`runtime-${id}`, css)));
87 |
88 | // execute in sandbox
89 | try {
90 | runtimeCache[mark].deps = await Promise.all(
91 | jsList
92 | .map(
93 | (u) => fetch(u).then((res) => res.text()),
94 | ),
95 | ).then((codes) => execute(codes, deps));
96 |
97 | updateRuntimeState(mark, 'LOADED');
98 | window.dispatchEvent(new CustomEvent(mark, { detail: { state: 'LOADED' } }));
99 |
100 | return runtimeCache[mark].deps;
101 | } catch (e) {
102 | updateRuntimeState(mark, 'LOAD_ERROR');
103 | window.dispatchEvent(new CustomEvent(mark, { detail: { state: 'LOAD_ERROR' } }));
104 | console.error(`[icestark module] ${id} fetch or excute js assets error`, e);
105 | return Promise.reject(e);
106 | }
107 | }
108 |
109 | export function fetchRuntimeJson(url: string, fetch = window.fetch) {
110 | if (!/.json/.test(url)) {
111 | console.warn('[icestark-module] runtime url should be a json file.');
112 | }
113 | return fetch(url).then((res) => res.json());
114 | }
115 |
116 | export async function parseImmediately(runtimes: RuntimeInstance[], fetch = window.fetch) {
117 | return await runtimes.reduce(async (pre, next) => {
118 | const preProps = await pre;
119 | return {
120 | ...preProps,
121 | ...(await cacheDeps(next, preProps, fetch)),
122 | };
123 | }, Promise.resolve({}));
124 | }
125 |
126 | export async function parseRuntime(runtime: Runtime, fetch = window.fetch) {
127 | // if runtime is `undefined`/`false`
128 | if (!runtime) {
129 | return null;
130 | }
131 |
132 | /*
133 | * runtime info provided by url.
134 | */
135 | if (typeof runtime === 'string') {
136 | const runtimeConfigs = await fetchRuntimeJson(runtime, fetch);
137 | return parseImmediately(runtimeConfigs);
138 | }
139 |
140 | /*
141 | * runtime info provided in detail.
142 | */
143 | if (Array.isArray(runtime)) {
144 | return parseImmediately(runtime);
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/packages/icestark-module/src/utils.ts:
--------------------------------------------------------------------------------
1 | export const any2AnyArray = (any: T | T[]): T[] => (Array.isArray(any) ? any : [any]);
2 |
--------------------------------------------------------------------------------
/packages/icestark-module/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "jsx": "react",
5 | "experimentalDecorators": true,
6 | "declaration": true,
7 | "skipLibCheck": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "outDir": "lib"
10 | },
11 | "include": ["src/*"],
12 | "exclude": ["node_modules"]
13 | }
14 |
--------------------------------------------------------------------------------
/packages/icestark/.eslintignore:
--------------------------------------------------------------------------------
1 | lib
2 | node_modules
3 | tests
4 | __tests__
--------------------------------------------------------------------------------
/packages/icestark/.eslintrc.js:
--------------------------------------------------------------------------------
1 |
2 | module.exports = {
3 | extends: '../../.eslintrc.js',
4 | };
5 |
--------------------------------------------------------------------------------
/packages/icestark/__tests__/AppRoute.spec.tsx:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom/extend-expect';
2 |
3 | import { converArray2String } from '../src/util/helpers';
4 |
5 | describe('converArray2String', () => {
6 | test('converArray2String', () => {
7 | expect(converArray2String(['this', 'is', 'a', 'test'])).toBe('this,is,a,test');
8 | expect(converArray2String('this is a test')).toBe('this is a test');
9 | expect(converArray2String(1 as any)).toBe('1');
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/packages/icestark/__tests__/AppRouter.spec.tsx:
--------------------------------------------------------------------------------
1 | import * as fs from 'fs';
2 | import * as path from 'path';
3 | import '@testing-library/jest-dom/extend-expect';
4 | import { FetchMock } from 'jest-fetch-mock';
5 | import { getCache, setCache } from '../src/util/cache';
6 | import * as React from 'react';
7 | import { render } from '@testing-library/react';
8 | import { AppRouter, AppRoute } from '../src/index';
9 |
10 | const delay = (milliscond: number) => new Promise(resolve => setTimeout(resolve, milliscond));
11 |
12 | describe('AppRouter', () => {
13 | const umdSourceWithSetLibrary = fs.readFileSync(path.resolve(__dirname, './umd-setlibrary-sample.js'));
14 | beforeEach(() => {
15 | (fetch as FetchMock).resetMocks();
16 | setCache('basename', '');
17 | });
18 |
19 | test('app-basename-default', async () => {
20 | (fetch as FetchMock).mockResponseOnce(umdSourceWithSetLibrary.toString());
21 | const { container, unmount } = render(
22 |
23 |
32 |
33 | );
34 | window.history.pushState({}, 'test', '/seller');
35 | expect(container.innerHTML).toContain('Loading')
36 | expect(getCache('basename')).toBe('/seller');
37 |
38 | await delay(1000);
39 | expect(container.innerHTML).toContain('商家平台')
40 |
41 | window.history.pushState({}, 'test', '/seller/detail');
42 | await delay(1000);
43 | expect(container.innerHTML).toContain('商家详情')
44 |
45 | unmount();
46 | })
47 |
48 | test('app-basename-custom', async () => {
49 | (fetch as FetchMock).mockResponseOnce(umdSourceWithSetLibrary.toString());
50 | const { container, unmount } = render(
51 |
52 |
62 |
63 | );
64 | window.history.pushState({}, 'test', '/seller');
65 | expect(container.innerHTML).toContain('Loading')
66 | expect(getCache('basename')).toBe('/seller-b');
67 |
68 | await delay(1000);
69 | expect(container.innerHTML).toContain('NotFound')
70 |
71 | unmount();
72 | })
73 |
74 | test('app-basename-empty', async () => {
75 | (fetch as FetchMock).mockResponseOnce(umdSourceWithSetLibrary.toString());
76 | const { container, unmount } = render(
77 |
78 |
88 |
89 | );
90 | window.history.pushState({}, 'test', '/seller');
91 | expect(container.innerHTML).toContain('Loading')
92 | expect(getCache('basename')).toBe('/seller');
93 |
94 | await delay(1000);
95 | expect(container.innerHTML).toContain('商家平台')
96 | unmount();
97 | })
98 |
99 | test('app-basename-frameworkBasename', async () => {
100 | (fetch as FetchMock).mockResponseOnce(umdSourceWithSetLibrary.toString());
101 | const { container, unmount } = render(
102 |
105 |
113 |
114 | );
115 | window.history.pushState({}, 'test', '/micro/seller');
116 | expect(getCache('basename')).toBe('/micro/seller');
117 |
118 | await delay(1000);
119 | expect(container.innerHTML).toContain('商家平台')
120 |
121 | unmount();
122 | })
123 |
124 | test('app-multi-paths', async () => {
125 | (fetch as FetchMock).mockResponseOnce(umdSourceWithSetLibrary.toString());
126 | const { container, unmount } = render(
127 |
128 |
137 |
138 | );
139 | window.history.pushState({}, 'test', '/seller2');
140 | expect(container.innerHTML).toContain('Loading')
141 | expect(getCache('basename')).toBe('/seller2');
142 |
143 | await delay(1000);
144 | expect(container.innerHTML).toContain('商家平台')
145 |
146 | unmount();
147 | })
148 | })
--------------------------------------------------------------------------------
/packages/icestark/__tests__/app.spec.tsx:
--------------------------------------------------------------------------------
1 | import { registerMicroApps } from '../src';
2 | import start, { unload } from '../src/start';
3 | import { AppConfig, getMicroApps, mountMicroApp, removeMicroApp, removeMicroApps, unmountMicroApp, createMicroApp } from '../src/apps';
4 | import { LOADING_ASSETS, MOUNTED, NOT_LOADED, UNMOUNTED } from '../src/util/constant';
5 |
6 | describe('app start', () => {
7 | test('start hijack', () => {
8 | const routerChange = jest.fn();
9 | start({
10 | onRouteChange: routerChange
11 | });
12 | // first call when init
13 | expect(routerChange.mock.calls.length).toBe(1);
14 | window.history.pushState({}, 'test', '/props-render');
15 | expect(routerChange.mock.calls.length).toBe(2);
16 | unload();
17 | });
18 |
19 | test('active micro app', () => {
20 | window.history.pushState({}, 'test', '/');
21 | registerMicroApps([
22 | {
23 | name: 'app1',
24 | activePath: '/test',
25 | url: ['//icestark.com/index.js']
26 | },
27 | {
28 | name: 'app2',
29 | activePath: '/test2',
30 | hashType: true,
31 | url: ['//icestark.com/index.js']
32 | },
33 | {
34 | name: 'app3',
35 | activePath: '/',
36 | exact: true,
37 | url: ['//icestark.com/index.js']
38 | },
39 | {
40 | name: 'app4',
41 | activePath: (url) => {
42 | return url.indexOf('/test4') > -1
43 | },
44 | url: ['//icestark.com/index.js']
45 | },
46 | {
47 | name: 'app5',
48 | activePath: ['/test5', '/test'],
49 | url: ['//icestark.com/index.js'],
50 | },
51 | {
52 | name: 'app6',
53 | activePath: [{ value: '/test6', exact: true}],
54 | url: ['//icestark.com/index.js'],
55 | }
56 | ]);
57 | let activeApps = [];
58 | start({
59 | onActiveApps: (apps => {
60 | activeApps = apps;
61 | }),
62 | });
63 | expect(getMicroApps().length).toBe(6);
64 | expect(getMicroApps().find(item => item.name === 'app3').status).toBe(LOADING_ASSETS);
65 | expect(getMicroApps().find(item => item.name === 'app1').status).toBe(NOT_LOADED);
66 | window.history.pushState({}, 'test', '/test');
67 | expect(getMicroApps().find(item => item.name === 'app1').status).toBe(LOADING_ASSETS);
68 | expect(getMicroApps().find(item => item.name === 'app5').status).toBe(LOADING_ASSETS);
69 | window.history.pushState({}, 'test', '/#/test2');
70 | expect(getMicroApps().find(item => item.name === 'app2').status).toBe(LOADING_ASSETS);
71 | window.history.pushState({}, 'test', '/test4');
72 | expect(getMicroApps().find(item => item.name === 'app4').status).toBe(LOADING_ASSETS);
73 | window.history.pushState({}, 'test', '/test6/a');
74 | expect(getMicroApps().find(item => item.name === 'app6').status).toBe(NOT_LOADED);
75 | expect(activeApps).toStrictEqual([]);
76 |
77 | unload();
78 | });
79 |
80 | test('remove micro app', () => {
81 | registerMicroApps([
82 | {
83 | name: 'app1',
84 | activePath: '/test',
85 | url: ['//icestark.com/index.js']
86 | },
87 | ]);
88 | expect(getMicroApps().length).toBe(1);
89 | removeMicroApp('app2');
90 | expect(getMicroApps().length).toBe(1);
91 | removeMicroApps(['app1']);
92 | expect(getMicroApps().length).toBe(0);
93 | });
94 |
95 | test('mount app', async () => {
96 | let status = '';
97 | registerMicroApps([
98 | {
99 | name: 'app',
100 | activePath: '/testapp',
101 | url: ['//icestark.com/index.js'],
102 | mount: () => {
103 | status = MOUNTED;
104 | },
105 | unmount: () => {
106 | status = UNMOUNTED;
107 | },
108 | } as AppConfig,
109 | ]);
110 | window.history.pushState({}, 'test', '/testapp');
111 | await mountMicroApp('app');
112 | expect(status).toBe(MOUNTED);
113 | await unmountMicroApp('app');
114 | expect(status).toBe(UNMOUNTED);
115 | await createMicroApp('app');
116 | expect(status).toBe(MOUNTED);
117 |
118 | const errorApp = await createMicroApp('app-error');
119 | expect(errorApp).toBe(null);
120 | })
121 | });
--------------------------------------------------------------------------------
/packages/icestark/__tests__/appLifeCycle.spec.tsx:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom/extend-expect';
2 |
3 | import { AppLifeCycleEnum, cacheApp, callAppEnter, callAppLeave, isCached, deleteCache } from '../src/util/appLifeCycle';
4 | import { setCache } from '../src/util/cache';
5 |
6 | describe('appLifeCycle', () => {
7 | test('callAppEnter', () => {
8 | const appEnterMockFn = jest.fn();
9 |
10 | setCache(AppLifeCycleEnum.AppEnter, appEnterMockFn);
11 |
12 | callAppEnter();
13 | expect(appEnterMockFn).toBeCalledTimes(1);
14 |
15 | setCache(AppLifeCycleEnum.AppEnter, null);
16 | callAppEnter();
17 | expect(appEnterMockFn).toBeCalledTimes(1);
18 | });
19 |
20 | test('callAppLeave', () => {
21 | const appLeaveMockFn = jest.fn();
22 |
23 | setCache(AppLifeCycleEnum.AppLeave, appLeaveMockFn);
24 |
25 | callAppLeave();
26 | expect(appLeaveMockFn).toBeCalledTimes(1);
27 |
28 | setCache(AppLifeCycleEnum.AppLeave, null);
29 | callAppLeave();
30 | expect(appLeaveMockFn).toBeCalledTimes(1);
31 | });
32 |
33 | test('cache app', () => {
34 | const appEnterMockFn = jest.fn();
35 | const appLeaveMockFn = jest.fn();
36 | const appKey = 'appKey';
37 | setCache(AppLifeCycleEnum.AppEnter, appEnterMockFn);
38 | setCache(AppLifeCycleEnum.AppLeave, appLeaveMockFn);
39 | cacheApp(appKey);
40 | expect(isCached(appKey)).toBe(true);
41 | deleteCache(appKey);
42 | expect(isCached(appKey)).toBe(false);
43 | })
44 | });
45 |
--------------------------------------------------------------------------------
/packages/icestark/__tests__/cache.spec.tsx:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom/extend-expect';
2 |
3 | import { setCache, getCache } from '../src/util/cache';
4 |
5 | describe('setCache', () => {
6 | test('setCache', () => {
7 | expect(setCache('testKey', 123)).toBeUndefined;
8 |
9 | setCache('testKey', 123);
10 | expect(getCache('testKey')).toBe(123);
11 | });
12 | });
13 |
--------------------------------------------------------------------------------
/packages/icestark/__tests__/capturedListeners.spec.tsx:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom/extend-expect';
2 | import { create } from 'domain';
3 |
4 | import {
5 | CapturedEventNameEnum,
6 | find,
7 | addCapturedEventListeners,
8 | removeCapturedEventListeners,
9 | isInCapturedEventListeners,
10 | callCapturedEventListeners,
11 | setHistoryEvent,
12 | resetCapturedEventListeners,
13 | createPopStateEvent,
14 | } from '../src/util/capturedListeners';
15 |
16 | describe('capturedListeners', () => {
17 | test('find', () => {
18 | const testMockFn = jest.fn();
19 |
20 | expect(find({}, 1)).toBe(false);
21 |
22 | expect(find([], 1)).toBe(false);
23 |
24 | expect(find([testMockFn], () => {})).toBe(false);
25 |
26 | expect(find([testMockFn], testMockFn)).toBe(true);
27 | });
28 |
29 | test('capturedListeners', () => {
30 | const popStateMockFn = jest.fn();
31 | const hashChangeMockFn = jest.fn();
32 |
33 | // add
34 | addCapturedEventListeners(CapturedEventNameEnum.POPSTATE, popStateMockFn);
35 | addCapturedEventListeners(CapturedEventNameEnum.HASHCHANGE, hashChangeMockFn);
36 |
37 | expect(isInCapturedEventListeners(CapturedEventNameEnum.HASHCHANGE, hashChangeMockFn)).toBe(
38 | true,
39 | );
40 | expect(isInCapturedEventListeners(CapturedEventNameEnum.POPSTATE, popStateMockFn)).toBe(true);
41 |
42 | // call
43 | setHistoryEvent(createPopStateEvent({}, 'popstate'));
44 | callCapturedEventListeners();
45 |
46 | expect(popStateMockFn).toBeCalledTimes(1);
47 | expect(hashChangeMockFn).toBeCalledTimes(1);
48 |
49 | // remove
50 | removeCapturedEventListeners(CapturedEventNameEnum.HASHCHANGE, hashChangeMockFn);
51 |
52 | expect(isInCapturedEventListeners(CapturedEventNameEnum.HASHCHANGE, hashChangeMockFn)).toBe(
53 | false,
54 | );
55 |
56 | // call
57 | setHistoryEvent(createPopStateEvent({}, 'popstate'));
58 | callCapturedEventListeners();
59 |
60 | expect(popStateMockFn).toBeCalledTimes(2);
61 | expect(hashChangeMockFn).toBeCalledTimes(1);
62 |
63 | // reset
64 | resetCapturedEventListeners();
65 |
66 | // call
67 | setHistoryEvent(createPopStateEvent({}, 'popstate'));
68 | callCapturedEventListeners();
69 |
70 | expect(popStateMockFn).toBeCalledTimes(2);
71 | expect(hashChangeMockFn).toBeCalledTimes(1);
72 | });
73 | });
74 |
--------------------------------------------------------------------------------
/packages/icestark/__tests__/checkActive.spec.tsx:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom/extend-expect';
2 | import findActivePath, { matchPath, getPathname, formatPath } from '../src/util/checkActive';
3 |
4 | describe('checkActive', () => {
5 | test('matchPath - path options', () => {
6 | let match = matchPath('/test/123', { value: '/test' });
7 | expect(match && match.url).toBe('/test');
8 |
9 | match = matchPath('/test/123', { value: '/test/:id' });
10 | expect(match && match.url).toBe('/test/123');
11 | expect(match && match.params.id).toBe('123');
12 |
13 | // test exact
14 | match = matchPath('/test/123', { value: '/test', exact: true });
15 | expect(match).toBeFalsy();
16 |
17 | match = matchPath('/test/123', { value: '/test', exact: true });
18 | expect(match).toBeFalsy();
19 |
20 | // test sensitive
21 | match = matchPath('/Test/123', { value: '/test', sensitive: false });
22 | expect(match && match.url) .toBe('/Test');
23 |
24 | match = matchPath('/Test/123', { value: '/test', sensitive: true });
25 | expect(match).toBeFalsy();
26 |
27 | // test strict
28 | match = matchPath('/test', { value: '/test/', strict: false });
29 | expect(match && match.url) .toBe('/test');
30 |
31 | match = matchPath('/test', { value: '/test/', strict: true });
32 | expect(match).toBeFalsy();
33 |
34 | });
35 |
36 | test('getPathname', () => {
37 | let pathname = getPathname('/test/123');
38 | expect(pathname).toBe('/test/123');
39 |
40 | pathname = getPathname('/test/#home')
41 | expect(pathname).toBe('/test/');
42 |
43 | pathname = getPathname('/test/#home', true)
44 | expect(pathname).toBe('/home');
45 | })
46 |
47 | test('matchPath - hashType', () => {
48 | let match = matchPath('/test/123', { value: '/test', hashType: true });
49 | expect(match).toBeFalsy();
50 |
51 | match = matchPath('/test/#home', { value: '/home', hashType: true });
52 | expect(match && match.url).toBe('/home');
53 | })
54 |
55 | test('checkActive', () => {
56 | // empty activePath
57 | let checkFnc = findActivePath();
58 | expect(checkFnc('/test/123')).toBeTruthy();
59 |
60 | // type `string`
61 | checkFnc = findActivePath(formatPath('/test', {}));
62 | expect(checkFnc('/test/123')).toEqual('/test');
63 |
64 | checkFnc = findActivePath(formatPath('/test', { exact: true }));
65 | expect(checkFnc('/test/123')).toBeFalsy();
66 |
67 | // type `string[]`
68 | checkFnc = findActivePath(formatPath(['/test', '/seller'], {}));
69 | expect(checkFnc('/test/123')).toEqual('/test');
70 |
71 | checkFnc = findActivePath(formatPath(['/test', '/seller'], { exact: true }));
72 | expect(checkFnc('/test/123')).toBeFalsy();
73 |
74 | // type `PathData`
75 | checkFnc = findActivePath(formatPath({ value: '/test' }, {}));
76 | expect(checkFnc('/test/123')).toEqual('/test');
77 |
78 | checkFnc = findActivePath(formatPath({ value: '/test', exact: true }, {}));
79 | expect(checkFnc('/test/123')).toBeFalsy();
80 |
81 | // type `PathData[]`
82 | checkFnc = findActivePath([{ value: '/test' }, { value: '/seller' }]);
83 | expect(checkFnc('/test/123')).toEqual('/test');
84 |
85 | // type `MixedPathData`
86 | checkFnc = findActivePath(formatPath(['/test', { value: '/seller' }]));
87 | expect(checkFnc('/test/123')).toEqual('/test');
88 |
89 | // type `ActiveFn`
90 | checkFnc = findActivePath((url: string) => url.includes('/test'));
91 | expect(checkFnc('/test/123')).toBeTruthy();
92 |
93 | // `undefined`
94 | checkFnc = findActivePath(formatPath());
95 | expect(checkFnc('/test/123')).toBeTruthy();
96 |
97 | // matched idx
98 | checkFnc = findActivePath(formatPath(['/test', '/seller'], {}));
99 | expect(checkFnc('/seller')).toEqual('/seller');
100 |
101 | // undefined array
102 | checkFnc = findActivePath(formatPath([undefined, '/seller'], {}));
103 | expect(checkFnc('/')).toBeFalsy();
104 |
105 | // nuallable array
106 | checkFnc = findActivePath(formatPath([null, '/seller'], {}));
107 | expect(checkFnc('/')).toBeFalsy();
108 |
109 | // empty array
110 | checkFnc = findActivePath(formatPath(['', '/seller'], {}));
111 | expect(checkFnc('/')).toBeFalsy();
112 | })
113 | });
114 |
--------------------------------------------------------------------------------
/packages/icestark/__tests__/createMicroApp.spec.tsx:
--------------------------------------------------------------------------------
1 | import * as fs from 'fs';
2 | import * as path from 'path';
3 | import { FetchMock } from 'jest-fetch-mock';
4 | import * as React from 'react';
5 | import { render } from '@testing-library/react';
6 | import { createMicroApp, unmountMicroApp } from '../src/apps';
7 | import { setCache, getCache } from '../src/util/cache';
8 |
9 | describe('createMicroApp', () => {
10 | const umdSourceWithSetLibrary = fs.readFileSync(path.resolve(__dirname, './umd-setlibrary-sample.js'));
11 | beforeEach(() => {
12 | (fetch as FetchMock).resetMocks();
13 | setCache('basename', '');
14 | });
15 |
16 | // before 2.5.0, createMicroApp did not support basename
17 | test('old-app-basename', async () => {
18 | (fetch as FetchMock).mockResponseOnce(umdSourceWithSetLibrary.toString());
19 |
20 | const { getByTestId, container, unmount } = render()
21 |
22 | // use setBasename instead
23 | setCache('basename', '/seller');
24 |
25 | await createMicroApp({
26 | name: 'waiter',
27 | url:[
28 | '//icestark.com/index.js'
29 | ],
30 | container: getByTestId('container'),
31 | loadScriptMode: "fetch"
32 | })
33 |
34 | console.log('fsdff', getCache('basename'))
35 | expect(getCache('basename')).toEqual('/seller');
36 | expect(!!getCache('root')).toBeTruthy();
37 | expect(container.innerHTML).toContain('商家列表')
38 |
39 | unmountMicroApp('waiter')
40 | unmount();
41 | })
42 |
43 | test('app-basename', async () => {
44 | (fetch as FetchMock).mockResponseOnce(umdSourceWithSetLibrary.toString());
45 |
46 | const { getByTestId, container, unmount } = render()
47 | await createMicroApp({
48 | name: 'seller',
49 | basename: "seller",
50 | url:[
51 | '//icestark.com/index.js'
52 | ],
53 | container: getByTestId('container'),
54 | loadScriptMode: "fetch"
55 | })
56 |
57 | expect(getCache('basename')).toEqual('/seller');
58 | expect(!!getCache('root')).toBeTruthy();
59 | expect(container.innerHTML).toContain('商家列表');
60 |
61 | unmountMicroApp('seller');
62 | unmount();
63 | })
64 | })
65 |
--------------------------------------------------------------------------------
/packages/icestark/__tests__/error.spec.ts:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom/extend-expect';
2 | import { formatErrMessage } from '../src/util/error';
3 |
4 | describe('helpers', () => {
5 | test('formatErrMessage', () => {
6 | expect(
7 | formatErrMessage(0)
8 | ).toEqual('icestark minified message #0: See https://micro-frontends.ice.work/error?code=0');
9 |
10 | expect(
11 | formatErrMessage(0, 'Unable to load app')
12 | ).toEqual('icestark minified message #0: Unable to load app. See https://micro-frontends.ice.work/error?code=0');
13 |
14 | expect(
15 | formatErrMessage(0, 'Unable to load app {0}')
16 | ).toEqual('icestark minified message #0: Unable to load app {0}. See https://micro-frontends.ice.work/error?code=0');
17 |
18 | expect(
19 | formatErrMessage(0, 'Unable to load app {0}', 'seller')
20 | ).toEqual('icestark minified message #0: Unable to load app seller. See https://micro-frontends.ice.work/error?code=0&arg=seller');
21 | });
22 | })
23 |
--------------------------------------------------------------------------------
/packages/icestark/__tests__/getLifecycle.spec.tsx:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom/extend-expect';
2 |
3 | import { setCache } from '../src/util/cache';
4 | import { getLifecyleByLibrary } from '../src/util/getLifecycle';
5 |
6 | describe('getLifecycle', () => {
7 | test('getLifecyleByLibrary - string', () => {
8 | // @ts-ignore
9 | window.mockFn = {
10 | mount: jest.fn(),
11 | unmount: jest.fn(),
12 | }
13 | setCache('library', 'mockFn');
14 |
15 | expect(getLifecyleByLibrary()).not.toBe(null);
16 | })
17 |
18 | test('getLifecyleByLibrary - string[]', () => {
19 | // @ts-ignore
20 | (window.scope = window.scope || {}).mockFn = {
21 | mount: jest.fn(),
22 | unmount: jest.fn(),
23 | }
24 | setCache('library', ['scope', 'mockFn']);
25 |
26 | expect(getLifecyleByLibrary()).not.toBe(null);
27 | })
28 |
29 | test('getLifecyleByLibrary - undefined', () => {
30 |
31 | setCache('library', 'mockData');
32 |
33 | expect(getLifecyleByLibrary()).toBe(null);
34 | })
35 | });
36 |
37 |
38 |
--------------------------------------------------------------------------------
/packages/icestark/__tests__/global-umd-sample.js:
--------------------------------------------------------------------------------
1 | window.React = { version: '16.14.0' };
--------------------------------------------------------------------------------
/packages/icestark/__tests__/loader.bundle.spec.ts:
--------------------------------------------------------------------------------
1 | import * as fs from 'fs';
2 | import * as path from 'path';
3 | import { FetchMock } from 'jest-fetch-mock';
4 | import Sandbox from '@ice/sandbox';
5 | import { AssetTypeEnum } from '../src/util/handleAssets';
6 | import { loadScriptByFetch } from '../src/util/loaders';
7 | import { setCache } from '../src/util/cache';
8 |
9 | describe('loader', () => {
10 | const jsBundleSource = fs.readFileSync(path.resolve(__dirname, './js-bundle-sample.js'));
11 | beforeEach(() => {
12 | (fetch as FetchMock).resetMocks();
13 | setCache('root', true);
14 | });
15 |
16 | test('load js bundle', async () => {
17 | (fetch as FetchMock).mockResponseOnce(jsBundleSource.toString());
18 | const lifecycle: any = await loadScriptByFetch([{
19 | content: '//icesk.com/index.js',
20 | type: AssetTypeEnum.EXTERNAL,
21 | }]);
22 | expect((!!lifecycle.mount && !!lifecycle.unmount)).toBe(true);
23 | });
24 |
25 | test('load js bundle with sandbox', async () => {
26 | (fetch as FetchMock).mockResponseOnce(jsBundleSource.toString());
27 | const lifecycle: any = await loadScriptByFetch([{
28 | content: '//icesk.com/index.js',
29 | type: AssetTypeEnum.EXTERNAL,
30 | }], new Sandbox());
31 | expect((!!lifecycle.mount && !!lifecycle.unmount)).toBe(true);
32 | });
33 | });
34 |
--------------------------------------------------------------------------------
/packages/icestark/__tests__/loader.runtime.spec.ts:
--------------------------------------------------------------------------------
1 | import * as fs from 'fs';
2 | import * as path from 'path';
3 | import { FetchMock } from 'jest-fetch-mock';
4 | import { AssetTypeEnum } from '../src/util/handleAssets';
5 | import { loadScriptByFetch } from '../src/util/loaders';
6 | import { setCache } from '../src/util/cache';
7 |
8 | describe('loader', () => {
9 | const reactUMD = fs.readFileSync(path.resolve(__dirname, './global-umd-sample.js'));
10 | beforeEach(() => {
11 | (fetch as FetchMock).resetMocks();
12 | setCache('root', true);
13 | });
14 |
15 | test('load js bundle with runtime', async () => {
16 | (fetch as FetchMock).mockResponseOnce(reactUMD.toString());
17 | window['React@16.14.0'] = {};
18 | await loadScriptByFetch([{
19 | content: '//icesk.com/index.js',
20 | type: AssetTypeEnum.RUNTIME,
21 | library: 'React',
22 | version: '16.14.0',
23 | }]);
24 | expect(window['React@16.14.0']?.version).toBe('16.14.0');
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/packages/icestark/__tests__/loader.umd.setLibrary.spec.ts:
--------------------------------------------------------------------------------
1 | import * as fs from 'fs';
2 | import * as path from 'path';
3 | import { FetchMock } from 'jest-fetch-mock';
4 | import Sandbox from '@ice/sandbox';
5 | import { AssetTypeEnum } from '../src/util/handleAssets';
6 | import { loadScriptByFetch } from '../src/util/loaders';
7 | import { setCache } from '../src/util/cache';
8 |
9 | describe('loader', () => {
10 | const umdSourceWithSetLibrary = fs.readFileSync(path.resolve(__dirname, './umd-setlibrary-sample.js'));
11 | beforeEach(() => {
12 | (fetch as FetchMock).resetMocks();
13 | setCache('root', true);
14 | });
15 |
16 | test('load umd module - setLibraryName', async () => {
17 | (fetch as FetchMock).mockResponseOnce(umdSourceWithSetLibrary.toString());
18 | const lifecycle: any = await loadScriptByFetch([{
19 | content: '//icestark.com/index.js',
20 | type: AssetTypeEnum.EXTERNAL,
21 | }]);
22 | expect(!!lifecycle.mount && !!lifecycle.unmount).toBe(true);
23 | });
24 |
25 | test('load umd module with sandbox - setLibraryName', async () => {
26 | (fetch as FetchMock).mockResponseOnce(umdSourceWithSetLibrary.toString());
27 | const lifecycle: any = await loadScriptByFetch([{
28 | content: '//icestark.com/index.js',
29 | type: AssetTypeEnum.EXTERNAL,
30 | }], new Sandbox());
31 | expect(!!lifecycle.mount && !!lifecycle.unmount).toBe(true);
32 | });
33 | });
34 |
--------------------------------------------------------------------------------
/packages/icestark/__tests__/loader.umd.spec.ts:
--------------------------------------------------------------------------------
1 | import * as fs from 'fs';
2 | import * as path from 'path';
3 | import { FetchMock } from 'jest-fetch-mock';
4 | import Sandbox from '@ice/sandbox';
5 | import { AssetTypeEnum } from '../src/util/handleAssets';
6 | import { loadScriptByFetch } from '../src/util/loaders';
7 | import { setCache } from '../src/util/cache';
8 |
9 | describe('loader', () => {
10 | const umdSource = fs.readFileSync(path.resolve(__dirname, './umd-not-setlibrary-sample.js'));
11 | beforeEach(() => {
12 | (fetch as FetchMock).resetMocks();
13 | setCache('root', true);
14 | });
15 |
16 | test('load normal umd module', async () => {
17 | (fetch as FetchMock).mockResponseOnce(umdSource.toString());
18 | const lifecycle: any = await loadScriptByFetch([{
19 | content: '//icestark.com/index.js',
20 | type: AssetTypeEnum.EXTERNAL,
21 | }]);
22 |
23 | expect(!!lifecycle.mount && !!lifecycle.unmount).toBe(true);
24 | });
25 |
26 | test('load normal umd module with sandbox', async () => {
27 | (fetch as FetchMock).mockResponseOnce(umdSource.toString());
28 | const lifecycle: any = await loadScriptByFetch([{
29 | content: '//icestark.com/index.js',
30 | type: AssetTypeEnum.EXTERNAL,
31 | }], new Sandbox());
32 |
33 | expect(!!lifecycle.mount && !!lifecycle.unmount).toBe(true);
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/packages/icestark/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@ice/stark",
3 | "version": "2.8.3",
4 | "description": "Icestark is a JavaScript library for multiple projects, Ice workbench solution.",
5 | "scripts": {
6 | "build": "rm -rf lib && tsc",
7 | "watch": "tsc -w",
8 | "test": "NODE_ENV=unittest pnpm jest",
9 | "lint": "pnpm eslint --ext .js,.jsx,.ts,.tsx ./",
10 | "prepublishOnly": "pnpm run-s lint test build"
11 | },
12 | "main": "lib/index.js",
13 | "module": "lib/index.js",
14 | "types": "lib/index.d.ts",
15 | "files": [
16 | "lib"
17 | ],
18 | "license": "MIT",
19 | "keywords": [
20 | "ice",
21 | "react",
22 | "microfrontends"
23 | ],
24 | "bugs": {
25 | "url": "https://github.com/alibaba/ice/issues"
26 | },
27 | "homepage": "https://github.com/ice-lab/icestark",
28 | "repository": {
29 | "type": "git",
30 | "url": "git+https://github.com/ice-lab/icestark.git"
31 | },
32 | "peerDependencies": {
33 | "react": ">=15.0.0"
34 | },
35 | "dependencies": {
36 | "@ice/sandbox": "^1.0.4",
37 | "lodash.isempty": "^4.4.0",
38 | "lodash.isequal": "^4.5.0",
39 | "path-to-regexp": "^1.7.0",
40 | "url-parse": "^1.1.9"
41 | },
42 | "devDependencies": {
43 | "@types/path-to-regexp": "^1.7.0",
44 | "@types/react": "^16.8.19",
45 | "@types/url-parse": "^1.4.3",
46 | "react": "^16.7.0",
47 | "react-dom": "^16.7.0",
48 | "typescript": "^4.3.5"
49 | },
50 | "jest": {
51 | "coverageDirectory": "./coverage/",
52 | "collectCoverage": true,
53 | "preset": "ts-jest",
54 | "automock": false,
55 | "testMatch": [
56 | "**/__tests__/**/*.ts?(x)",
57 | "**/?(*.)+(spec|test).ts?(x)"
58 | ],
59 | "setupFiles": [
60 | "../../setupJest.ts"
61 | ]
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/packages/icestark/src/AppLink.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | export type AppLinkProps = {
4 | to: string;
5 | hashType?: boolean;
6 | replace?: boolean;
7 | message?: string;
8 | children?: React.ReactNode;
9 | } & React.AnchorHTMLAttributes;
10 |
11 | const AppLink: React.SFC = (props: AppLinkProps) => {
12 | const { to, hashType, replace, message, children, ...rest } = props;
13 | const linkTo = hashType && to.indexOf('#') === -1 ? `/#${to}` : to;
14 | return (
15 | {
19 | e.preventDefault();
20 | if (message && window.confirm(message) === false) {
21 | return false;
22 | }
23 |
24 | /*
25 | * Bind `replaceState` and `pushState` to window to avoid illegal invocation error
26 | */
27 | const changeState = window.history[replace ? 'replaceState' : 'pushState'].bind(window);
28 |
29 | changeState({}, null, linkTo);
30 | }}
31 | >
32 | {children}
33 |
34 | );
35 | };
36 |
37 | export default AppLink;
38 |
--------------------------------------------------------------------------------
/packages/icestark/src/appHistory.ts:
--------------------------------------------------------------------------------
1 | export interface AppHistory {
2 | push: (path: string) => void;
3 | replace: (path: string) => void;
4 | }
5 |
6 | const appHistory: AppHistory = {
7 | push: (path: string) => {
8 | window.history.pushState({}, null, path);
9 | },
10 | replace: (path: string) => {
11 | window.history.replaceState({}, null, path);
12 | },
13 | };
14 |
15 | export default appHistory;
16 |
--------------------------------------------------------------------------------
/packages/icestark/src/index.ts:
--------------------------------------------------------------------------------
1 | export { default as AppRouter } from './AppRouter';
2 | export { default as AppRoute } from './AppRoute';
3 | export { default as AppLink } from './AppLink';
4 | export { default as appHistory } from './appHistory';
5 | export { createMicroApp, registerMicroApps, unmountMicroApp, unloadMicroApp, removeMicroApps, AppConfig } from './apps';
6 | export { prefetchApps } from './util/prefetch';
7 | export { default as start } from './start';
8 |
--------------------------------------------------------------------------------
/packages/icestark/src/util/appLifeCycle.ts:
--------------------------------------------------------------------------------
1 | import { setCache, getCache } from './cache';
2 | import { resetCapturedEventListeners } from './capturedListeners';
3 |
4 | export enum AppLifeCycleEnum {
5 | AppEnter = 'appEnter',
6 | AppLeave = 'appLeave',
7 | }
8 |
9 | export function cacheApp(cacheKey: string) {
10 | [AppLifeCycleEnum.AppEnter, AppLifeCycleEnum.AppLeave].forEach((lifeCycle) => {
11 | const lifeCycleCacheKey = `cache_${cacheKey}_${lifeCycle}`;
12 | if (getCache(lifeCycle)) {
13 | setCache(lifeCycleCacheKey, getCache(lifeCycle));
14 | } else if (getCache(lifeCycleCacheKey)) {
15 | // set cache to current lifeCycle
16 | setCache(lifeCycle, getCache(lifeCycleCacheKey));
17 | }
18 | });
19 | }
20 |
21 | export function deleteCache(cacheKey: string) {
22 | [AppLifeCycleEnum.AppEnter, AppLifeCycleEnum.AppLeave].forEach((lifeCycle) => {
23 | setCache(`cache_${cacheKey}_${lifeCycle}`, null);
24 | });
25 | }
26 |
27 | export function isCached(cacheKey: string) {
28 | return !!getCache(`cache_${cacheKey}_${AppLifeCycleEnum.AppEnter}`);
29 | }
30 |
31 | export function callAppEnter() {
32 | const appEnterKey = AppLifeCycleEnum.AppEnter;
33 | const registerAppEnterCallback = getCache(appEnterKey);
34 |
35 | if (registerAppEnterCallback) {
36 | registerAppEnterCallback();
37 | setCache(appEnterKey, null);
38 | }
39 | }
40 |
41 | export function callAppLeave() {
42 | // resetCapturedEventListeners when app change, remove react-router/vue-router listeners
43 | resetCapturedEventListeners();
44 |
45 | const appLeaveKey = AppLifeCycleEnum.AppLeave;
46 | const registerAppLeaveCallback = getCache(appLeaveKey);
47 |
48 | if (registerAppLeaveCallback) {
49 | registerAppLeaveCallback();
50 | setCache(appLeaveKey, null);
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/packages/icestark/src/util/cache.ts:
--------------------------------------------------------------------------------
1 | const namespace = 'ICESTARK';
2 |
3 | export const setCache = (key: string, value: any): void => {
4 | if (!(window as any)[namespace]) {
5 | (window as any)[namespace] = {};
6 | }
7 | (window as any)[namespace][key] = value;
8 | };
9 |
10 | export const getCache = (key: string): any => {
11 | const icestark: any = (window as any)[namespace];
12 | return icestark && icestark[key] ? icestark[key] : null;
13 | };
14 |
--------------------------------------------------------------------------------
/packages/icestark/src/util/capturedListeners.ts:
--------------------------------------------------------------------------------
1 | // Reference https://github.com/CanopyTax/single-spa/blob/master/src/navigation/navigation-events.js
2 | export enum CapturedEventNameEnum {
3 | POPSTATE = 'popstate',
4 | HASHCHANGE = 'hashchange',
5 | }
6 |
7 | export const routingEventsListeningTo = [
8 | CapturedEventNameEnum.HASHCHANGE,
9 | CapturedEventNameEnum.POPSTATE,
10 | ];
11 |
12 | const capturedEventListeners = {
13 | [CapturedEventNameEnum.POPSTATE]: [],
14 | [CapturedEventNameEnum.HASHCHANGE]: [],
15 | };
16 |
17 | let historyEvent = null;
18 |
19 | export function find(list, element) {
20 | if (!Array.isArray(list)) {
21 | return false;
22 | }
23 |
24 | return list.filter((item) => item === element).length > 0;
25 | }
26 |
27 | // inspired by https://github.com/single-spa/single-spa/blob/master/src/navigation/navigation-events.js#L107
28 | export function createPopStateEvent(state, originalMethodName) {
29 | // We need a popstate event even though the browser doesn't do one by default when you call replaceState, so that
30 | // all the applications can reroute.
31 | let evt;
32 | try {
33 | evt = new PopStateEvent('popstate', { state });
34 | } catch (err) {
35 | // IE 11 compatibility
36 | // https://docs.microsoft.com/en-us/openspecs/ie_standards/ms-html5e/bd560f47-b349-4d2c-baa8-f1560fb489dd
37 | evt = document.createEvent('PopStateEvent');
38 | evt.initPopStateEvent('popstate', false, false, state);
39 | }
40 | evt.icestark = true;
41 | evt.icestarkTrigger = originalMethodName;
42 | return evt;
43 | }
44 |
45 | /**
46 | * For micro apps don't share the same history instance, it's need
47 | * to hajack popstate eventListeners and trigger it when routes change.
48 | * As a side effect,micro app's popstate eventLister would execute twice,
49 | * which works as expected.
50 | */
51 | export function callCapturedEventListeners() {
52 | if (historyEvent) {
53 | Object.keys(capturedEventListeners).forEach((eventName) => {
54 | const capturedListeners = capturedEventListeners[eventName];
55 | if (capturedListeners.length) {
56 | capturedListeners.forEach((listener) => {
57 | listener.call(this, historyEvent);
58 | });
59 | }
60 | });
61 | historyEvent = null;
62 | }
63 | }
64 |
65 | export function setHistoryEvent(evt: PopStateEvent | HashChangeEvent) {
66 | historyEvent = evt;
67 | }
68 |
69 | export function isInCapturedEventListeners(eventName, fn) {
70 | return find(capturedEventListeners[eventName], fn);
71 | }
72 |
73 | export function addCapturedEventListeners(eventName, fn) {
74 | capturedEventListeners[eventName].push(fn);
75 | }
76 |
77 | export function removeCapturedEventListeners(eventName, listenerFn) {
78 | capturedEventListeners[eventName] = capturedEventListeners[eventName].filter(
79 | (fn) => fn !== listenerFn,
80 | );
81 | }
82 |
83 | export function resetCapturedEventListeners() {
84 | capturedEventListeners[CapturedEventNameEnum.POPSTATE] = [];
85 | capturedEventListeners[CapturedEventNameEnum.HASHCHANGE] = [];
86 | }
87 |
--------------------------------------------------------------------------------
/packages/icestark/src/util/constant.ts:
--------------------------------------------------------------------------------
1 | export const PREFIX = 'icestark';
2 |
3 | export const DYNAMIC = 'dynamic';
4 |
5 | export const STATIC = 'static';
6 |
7 | export const ICESTSRK_NOT_FOUND = `/${PREFIX}_404`;
8 |
9 | export const ICESTSRK_ERROR = `/${PREFIX}_error`;
10 |
11 | export const IS_CSS_REGEX = /\.css(\?((?!\.js$).)+)?$/;
12 |
13 | // app status
14 |
15 | export const NOT_LOADED = 'NOT_LOADED';
16 |
17 | export const LOADING_ASSETS = 'LOADING_ASSETS';
18 |
19 | export const LOAD_ERROR = 'LOAD_ERROR';
20 |
21 | export const NOT_MOUNTED = 'NOT_MOUNTED';
22 |
23 | export const MOUNTED = 'MOUNTED';
24 |
25 | export const UNMOUNTED = 'UNMOUNTED';
26 |
--------------------------------------------------------------------------------
/packages/icestark/src/util/error.ts:
--------------------------------------------------------------------------------
1 | export enum ErrorCode {
2 | 'EMPTY_LIFECYCLES' = 1,
3 | 'UNSUPPORTED_IMPORT_BROWSER' = 2,
4 | 'UNSUPPORTED_FETCH' = 3,
5 | 'CANNOT_FIND_APP' = 4,
6 | 'JS_LOAD_ERROR' = 5,
7 | 'CSS_LOAD_ERROR' = 6,
8 | 'ACTIVE_PATH_ITEM_CAN_NOT_BE_EMPTY' = 7,
9 | }
10 |
11 | export function normalizeMsg(msg: string, args: string[]) {
12 | if (args.length === 0) {
13 | return msg;
14 | }
15 |
16 | return msg.replace(/\{(\d+)\}/g, (match, p1) => {
17 | const idx = p1[0];
18 | if (typeof args[idx] === 'string' || typeof args[idx] === 'number') {
19 | return args[idx];
20 | }
21 | return match;
22 | });
23 | }
24 |
25 | export function formatErrMessage(code: string | number, msg?: string, ...args: string[]) {
26 | return `icestark minified message #${code}: ${
27 | normalizeMsg(
28 | msg ? `${msg}. ` : '',
29 | args,
30 | )}See https://micro-frontends.ice.work/error?code=${code}${
31 | args.length ? `&arg=${args.join('&arg=')}` : ''
32 | }`;
33 | }
34 |
--------------------------------------------------------------------------------
/packages/icestark/src/util/getLifecycle.ts:
--------------------------------------------------------------------------------
1 | import { getCache, setCache } from './cache';
2 | import { AppLifeCycleEnum } from './appLifeCycle';
3 | import type { ModuleLifeCycle } from '../apps';
4 |
5 | export function getLifecyleByLibrary() {
6 | const libraryName = getCache('library');
7 |
8 | /**
9 | * if `libraryName` is array, iterate it util a deepest value found.
10 | */
11 | const moduleInfo = (Array.isArray(libraryName)
12 | ? libraryName.reduce((pre, next) => pre[next], window)
13 | : window[libraryName]) as ModuleLifeCycle;
14 |
15 | if (moduleInfo && moduleInfo.mount && moduleInfo.unmount) {
16 | const lifecycle = moduleInfo;
17 |
18 | delete window[libraryName];
19 | setCache('library', null);
20 |
21 | return lifecycle;
22 | }
23 | return null;
24 | }
25 |
26 | export function getLifecyleByRegister() {
27 | const mount = getCache(AppLifeCycleEnum.AppEnter);
28 | const unmount = getCache(AppLifeCycleEnum.AppLeave);
29 |
30 | if (mount && unmount) {
31 | const lifecycle = {
32 | mount,
33 | unmount,
34 | };
35 |
36 | setCache(AppLifeCycleEnum.AppEnter, null);
37 | setCache(AppLifeCycleEnum.AppLeave, null);
38 |
39 | return lifecycle;
40 | }
41 | return null;
42 | }
43 |
--------------------------------------------------------------------------------
/packages/icestark/src/util/global.ts:
--------------------------------------------------------------------------------
1 | // fork: https://github.com/systemjs/systemjs/blob/master/src/extras/global.js
2 |
3 | // safari unpredictably lists some new globals first or second in object order
4 | let firstGlobalProp;
5 | let secondGlobalProp;
6 | let lastGlobalProp;
7 | const isIE11 = typeof navigator !== 'undefined' && navigator.userAgent.indexOf('Trident') !== -1;
8 |
9 | function shouldSkipProperty(p, globalWindow) {
10 | // eslint-disable-next-line no-prototype-builtins
11 | return !globalWindow.hasOwnProperty(p)
12 | || !isNaN(p) && p < (globalWindow as any).length
13 | || isIE11 && globalWindow[p] && typeof window !== 'undefined' && globalWindow[p].parent === window;
14 | }
15 |
16 | export function getGlobalProp(globalWindow) {
17 | let cnt = 0;
18 | let lastProp;
19 | // eslint-disable-next-line no-restricted-syntax
20 | for (const p in globalWindow) {
21 | // do not check frames cause it could be removed during import
22 | if (shouldSkipProperty(p, globalWindow)) { continue; }
23 | if (cnt === 0 && p !== firstGlobalProp || cnt === 1 && p !== secondGlobalProp) { return p; }
24 | cnt++;
25 | lastProp = p;
26 | }
27 | if (lastProp !== lastGlobalProp) { return lastProp; }
28 | }
29 |
30 | export function noteGlobalProps(globalWindow) {
31 | // alternatively Object.keys(global).pop()
32 | // but this may be faster (pending benchmarks)
33 | firstGlobalProp = undefined;
34 | secondGlobalProp = undefined;
35 | // eslint-disable-next-line no-restricted-syntax
36 | for (const p in globalWindow) {
37 | // do not check frames cause it could be removed during import
38 | if (shouldSkipProperty(p, globalWindow)) { continue; }
39 | if (!firstGlobalProp) { firstGlobalProp = p; } else if (!secondGlobalProp) { secondGlobalProp = p; }
40 | lastGlobalProp = p;
41 | }
42 | return lastGlobalProp;
43 | }
44 |
--------------------------------------------------------------------------------
/packages/icestark/src/util/globalConfiguration.ts:
--------------------------------------------------------------------------------
1 | import type { AppConfig } from '../apps';
2 | import type { Prefetch } from './prefetch';
3 |
4 | export type Fetch = typeof window.fetch | ((url: string) => Promise);
5 |
6 | export type RouteType = 'pushState' | 'replaceState';
7 |
8 | export interface StartConfiguration {
9 | shouldAssetsRemove?: (
10 | assetUrl?: string,
11 | element?: HTMLElement | HTMLLinkElement | HTMLStyleElement | HTMLScriptElement,
12 | ) => boolean;
13 | onRouteChange?: (
14 | url: string,
15 | pathname: string,
16 | query: object,
17 | hash?: string,
18 | type?: RouteType | 'init' | 'popstate' | 'hashchange',
19 | ) => void;
20 | onAppEnter?: (appConfig: AppConfig) => void;
21 | onAppLeave?: (appConfig: AppConfig) => void;
22 | onLoadingApp?: (appConfig: AppConfig) => void;
23 | onFinishLoading?: (appConfig: AppConfig) => void;
24 | onError?: (err: Error) => void;
25 | onActiveApps?: (appConfigs: AppConfig[]) => void;
26 | reroute?: (url: string, type: RouteType | 'init' | 'popstate'| 'hashchange') => void;
27 | fetch?: Fetch;
28 | prefetch?: Prefetch;
29 | basename?: string;
30 | }
31 |
32 | const globalConfiguration: StartConfiguration = {
33 | shouldAssetsRemove: () => true,
34 | onRouteChange: () => {},
35 | onAppEnter: () => {},
36 | onAppLeave: () => {},
37 | onLoadingApp: () => {},
38 | onFinishLoading: () => {},
39 | onError: () => {},
40 | onActiveApps: () => {},
41 | reroute: () => {},
42 | fetch: window.fetch,
43 | prefetch: false,
44 | basename: '',
45 | };
46 |
47 | export default globalConfiguration;
48 |
49 | // todos: remove it from 3.x
50 | export const temporaryState = {
51 | shouldAssetsRemoveConfigured: false,
52 | };
53 |
--------------------------------------------------------------------------------
/packages/icestark/src/util/loaders.ts:
--------------------------------------------------------------------------------
1 | import Sandbox from '@ice/sandbox';
2 | import { getGlobalProp, noteGlobalProps } from './global';
3 | import { Asset, fetchScripts, AssetTypeEnum, appendExternalScript } from './handleAssets';
4 | import { getLifecyleByLibrary, getLifecyleByRegister } from './getLifecycle';
5 | import { asyncForEach, isDev } from './helpers';
6 | import { ErrorCode, formatErrMessage } from './error';
7 | import { PREFIX } from './constant';
8 |
9 | import type { ModuleLifeCycle } from '../apps';
10 |
11 | function executeScripts(scripts: string[], sandbox?: Sandbox, globalwindow: Window = window) {
12 | let libraryExport = null;
13 |
14 | for (let idx = 0; idx < scripts.length; ++idx) {
15 | const lastScript = idx === scripts.length - 1;
16 | if (lastScript) {
17 | noteGlobalProps(globalwindow);
18 | }
19 |
20 | if (sandbox?.execScriptInSandbox) {
21 | sandbox.execScriptInSandbox(scripts[idx]);
22 | } else {
23 | // https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/eval
24 | // eslint-disable-next-line no-eval
25 | (0, eval)(scripts[idx]);
26 | }
27 |
28 | if (lastScript) {
29 | libraryExport = getGlobalProp(globalwindow);
30 | }
31 | }
32 |
33 | return libraryExport;
34 | }
35 |
36 | /**
37 | * load bundle
38 | */
39 | export async function loadScriptByFetch(jsList: Asset[], sandbox?: Sandbox, fetch = window.fetch) {
40 | const scriptTexts = await fetchScripts(jsList, fetch);
41 | const globalwindow = getGobalWindow(sandbox);
42 | const libraryExport = executeScripts(scriptTexts, sandbox, globalwindow);
43 | let moduleInfo = getLifecyleByLibrary() || getLifecyleByRegister();
44 | if (!moduleInfo) {
45 | moduleInfo = (libraryExport ? globalwindow[libraryExport] : {}) as ModuleLifeCycle;
46 | if (globalwindow[libraryExport]) {
47 | delete globalwindow[libraryExport];
48 | }
49 | }
50 |
51 | return moduleInfo;
52 | }
53 |
54 | /**
55 | * Get globalwindow
56 | *
57 | * @export
58 | * @param {Sandbox} [sandbox]
59 | * @returns
60 | */
61 | export function getGobalWindow(sandbox?: Sandbox) {
62 | if (sandbox?.getSandbox) {
63 | sandbox.createProxySandbox();
64 | return sandbox.getSandbox();
65 | }
66 | // FIXME: If run in Node environment
67 | return window;
68 | }
69 |
70 | /**
71 | * Load es modules and get lifecycles sequentially.
72 | * `import` returns a promise for the module namespace object of the requested module which means
73 | * + non-export returns empty object
74 | * + default export return object with `default` key
75 | */
76 | export async function loadScriptByImport(jsList: Asset[]): Promise {
77 | let mount = null;
78 | let unmount = null;
79 | await asyncForEach(jsList, async (js, index) => {
80 | if (js.type === AssetTypeEnum.INLINE) {
81 | await appendExternalScript(js, {
82 | id: `${PREFIX}-js-module-${index}`,
83 | });
84 | } else {
85 | let dynamicImport = null;
86 | try {
87 | /**
88 | * `import` will cause error under chrome 61 and ie.
89 | * Then use `new Function` to escape compile error.
90 | * Inspired by [dynamic-import-polyfill](https://github.com/GoogleChromeLabs/dynamic-import-polyfill)
91 | */
92 | // eslint-disable-next-line no-new-func
93 | dynamicImport = new Function('url', 'return import(url)');
94 | } catch (e) {
95 | return Promise.reject(
96 | new Error(
97 | formatErrMessage(
98 | ErrorCode.UNSUPPORTED_IMPORT_BROWSER,
99 | isDev && 'You can not use loadScriptMode = import where dynamic import is not supported by browsers.',
100 | ),
101 | ),
102 | );
103 | }
104 |
105 | try {
106 | if (dynamicImport) {
107 | const { mount: maybeMount, unmount: maybeUnmount } = await dynamicImport(js.content);
108 |
109 | if (maybeMount && maybeUnmount) {
110 | mount = maybeMount;
111 | unmount = maybeUnmount;
112 | }
113 | }
114 | } catch (e) {
115 | return Promise.reject(e);
116 | }
117 | }
118 | });
119 |
120 | if (mount && unmount) {
121 | return {
122 | mount,
123 | unmount,
124 | };
125 | }
126 |
127 | return null;
128 | }
129 |
--------------------------------------------------------------------------------
/packages/icestark/src/util/prefetch.ts:
--------------------------------------------------------------------------------
1 | import { fetchScripts, fetchStyles, getUrlAssets, getEntryAssets } from './handleAssets';
2 | import { NOT_LOADED } from './constant';
3 | import type { Fetch } from './globalConfiguration';
4 | import type { MicroApp, AppConfig } from '../apps';
5 |
6 | export type Prefetch =
7 | | boolean
8 | | string[]
9 | | ((app: AppConfig) => boolean);
10 |
11 | /**
12 | * https://github.com/microsoft/TypeScript/issues/21309#issuecomment-376338415
13 | */
14 | type RequestIdleCallbackHandle = any;
15 | interface RequestIdleCallbackOptions {
16 | timeout: number;
17 | }
18 | interface RequestIdleCallbackDeadline {
19 | readonly didTimeout: boolean;
20 | timeRemaining: (() => number);
21 | }
22 |
23 | declare global {
24 | interface Window {
25 | // @ts-ignore
26 | requestIdleCallback: ((
27 | callback: ((deadline: RequestIdleCallbackDeadline) => void),
28 | opts?: RequestIdleCallbackOptions,
29 | ) => RequestIdleCallbackHandle);
30 | // @ts-ignore
31 | cancelIdleCallback: ((handle: RequestIdleCallbackHandle) => void);
32 | }
33 | }
34 |
35 | /**
36 | * polyfill/shim for the `requestIdleCallback` and `cancelIdleCallback`.
37 | * https://github.com/pladaria/requestidlecallback-polyfill/blob/master/index.js
38 | */
39 | // @ts-ignore
40 | window.requestIdleCallback =
41 | window.requestIdleCallback ||
42 | function (cb) {
43 | const start = Date.now();
44 | return setTimeout(() => {
45 | cb({
46 | didTimeout: false,
47 | timeRemaining() {
48 | return Math.max(0, 50 - (Date.now() - start));
49 | },
50 | });
51 | }, 1);
52 | };
53 |
54 | window.cancelIdleCallback =
55 | window.cancelIdleCallback ||
56 | function (id) {
57 | clearTimeout(id);
58 | };
59 |
60 | function prefetchIdleTask(fetch = window.fetch) {
61 | return (app: MicroApp) => {
62 | window.requestIdleCallback(async () => {
63 | const { url, entry, entryContent, name } = app;
64 | const { jsList, cssList } = url ? getUrlAssets(url) : await getEntryAssets({
65 | entry,
66 | entryContent,
67 | assetsCacheKey: name,
68 | fetch,
69 | });
70 | window.requestIdleCallback(() => fetchScripts(jsList, fetch));
71 | window.requestIdleCallback(() => fetchStyles(cssList, fetch));
72 | });
73 | };
74 | }
75 |
76 | const names2PrefetchingApps = (names: string[]) => (app: MicroApp) => names.includes(app.name) && (app.status === NOT_LOADED || !app.status);
77 |
78 | /**
79 | * get prefetching apps by strategy
80 | * @param apps
81 | * @returns
82 | */
83 | const getPrefetchingApps = (apps: MicroApp[]) => (strategy: (app: MicroApp) => boolean) => apps.filter(strategy);
84 |
85 | export function doPrefetch(
86 | apps: MicroApp[],
87 | prefetchStrategy: Prefetch,
88 | fetch: Fetch,
89 | ) {
90 | const executeAllPrefetchTasks = (strategy: (app: MicroApp) => boolean) => {
91 | getPrefetchingApps(apps)(strategy)
92 | .forEach(prefetchIdleTask(fetch));
93 | };
94 |
95 | if (Array.isArray(prefetchStrategy)) {
96 | executeAllPrefetchTasks(names2PrefetchingApps(prefetchStrategy));
97 | return;
98 | }
99 | if (typeof prefetchStrategy === 'function') {
100 | executeAllPrefetchTasks(prefetchStrategy);
101 | return;
102 | }
103 | if (prefetchStrategy) {
104 | executeAllPrefetchTasks((app) => app.status === NOT_LOADED || !app.status);
105 | }
106 | }
107 |
108 | export function prefetchApps(apps: AppConfig[], fetch: Fetch = window.fetch) {
109 | if (apps && Array.isArray(apps)) {
110 | apps.forEach(prefetchIdleTask(fetch));
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/packages/icestark/src/util/renderComponent.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | /**
4 | * Render Component, compatible with Component and
5 | */
6 | export default function renderComponent(Component: any, props = {}): React.ReactElement {
7 | return React.isValidElement(Component) ? (
8 | React.cloneElement(Component, props)
9 | ) : (
10 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/packages/icestark/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "module": "esnext",
5 | "moduleResolution": "Node",
6 | "jsx": "react",
7 | "experimentalDecorators": true,
8 | "declaration": true,
9 | "skipLibCheck": true,
10 | "forceConsistentCasingInFileNames": true,
11 | "outDir": "lib",
12 | "allowSyntheticDefaultImports": true,
13 | "esModuleInterop": true
14 | },
15 | "include": ["src/*"],
16 | "exclude": ["node_modules"],
17 | "ts-node": {
18 | // these options are overrides used only by ts-node
19 | // same as our --compilerOptions flag and our TS_NODE_COMPILER_OPTIONS environment variable
20 | // https://github.com/TypeStrong/ts-node/issues/922#issuecomment-673155000
21 | "compilerOptions": {
22 | "module": "commonjs"
23 | }
24 | },
25 | }
26 |
--------------------------------------------------------------------------------
/packages/sandbox/.eslintignore:
--------------------------------------------------------------------------------
1 | lib
2 | node_modules
3 | tests
4 | __tests__
--------------------------------------------------------------------------------
/packages/sandbox/.eslintrc.js:
--------------------------------------------------------------------------------
1 |
2 | module.exports = {
3 | extends: '../../.eslintrc.js',
4 | };
5 |
--------------------------------------------------------------------------------
/packages/sandbox/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## 1.1.4
4 |
5 | - [fix] simply copy callable funtions's extra properties.
6 |
7 | ## 1.1.2
8 |
9 | - [fix] hijacked eventListener were not been removed after sandbox unload. ([#295](https://github.com/ice-lab/icestark/issues/295))
10 | - [fix] never bind `eval` in sandbox. ([#4294](https://github.com/alibaba/ice/issues/4294))
11 | - [refact] misspelling of Sandbox types.
12 |
13 | ## 1.1.1
14 |
15 | - [fix] falsy values except `undefined` would be trapped by proxy window. ([#156](https://github.com/ice-lab/icestark/issues/156))
16 |
17 | ## 1.1.0
18 |
19 | - [feat] mark access to all properties added to local window by using method `getAddedProperties`.
20 | - [feat] support injecting properties to sandbox.
21 |
--------------------------------------------------------------------------------
/packages/sandbox/README.md:
--------------------------------------------------------------------------------
1 | # @ice/sandbox
2 |
3 | > icestark sandbox solution. [icestark docs](https://ice-lab.github.io/icestark/).
4 |
5 | [](https://npmjs.org/package/@ice/sandbox) [](https://packagequality.com/#?package=@ice%2Fsandbox) [](https://travis-ci.org/ice-lab/icestark) [](https://codecov.io/gh/ice-lab/icestark) [](https://npmjs.org/package/@ice/sandbox) [](https://david-dm.org/ice-lab/icestark)
6 |
7 | ## Installation
8 |
9 | ```bash
10 | $ npm install @ice/sandbox --save
11 | ```
12 |
13 | ## Usage
14 |
15 | ```js
16 | import Sandbox from '@ice/sandbox';
17 |
18 | const sandbox = new Sandbox();
19 |
20 | // execute scripts in sandbox
21 | sandbox.execScriptInSandbox('window.a = 1;console.log(window.a);');
22 |
23 | // clear side effects added by sandbox, such as addEventListener, setInterval
24 | sandbox.clear();
25 | ```
26 |
27 | ## Inspiration
28 |
29 | `@ice/sandbox` is inspired by [tc39/proposal-realms](https://github.com/tc39/proposal-realms), [realms-shim](https://github.com/Agoric/realms-shim) and [qiankun sandbox](https://github.com/umijs/qiankun).
30 |
31 | ## Contributors
32 |
33 | Feel free to report any questions as an [issue](https://github.com/ice-lab/icestark/issues/new), we'd love to have your helping hand on `icestark`.
34 |
35 | If you're interested in `icestark`, see [CONTRIBUTING.md](https://github.com/alibaba/ice/blob/master/.github/CONTRIBUTING.md) for more information to learn how to get started.
36 |
37 | ## License
38 |
39 | [MIT](LICENSE)
40 |
--------------------------------------------------------------------------------
/packages/sandbox/__tests__/index.spec.ts:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom/extend-expect';
2 | import Sandbox from '../src/index';
3 |
4 | const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
5 |
6 | describe('sandbox: excapeSandbox', () => {
7 | const sandbox = new Sandbox({});
8 |
9 | test('execute script in sandbox', () => {
10 | sandbox.execScriptInSandbox('window.a = 1;expect(window.a).toBe(1);');
11 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
12 | expect((window as any).a).toBe(1);
13 | sandbox.clear();
14 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
15 | expect((window as any).a).toBe(undefined);
16 | });
17 |
18 | test('capture global event', async () => {
19 | sandbox.execScriptInSandbox(`
20 | setInterval(() => {expect(1).toBe(2)}, 100);
21 | setTimeout(() => { expect(1).toBe(2)}, 100)`);
22 | sandbox.clear();
23 | // delay 1000 ms for timeout
24 | await delay(1000);
25 | expect(true).toBe(true);
26 | });
27 | });
28 |
29 | describe('sandbox: default props', () => {
30 | const sandbox = new Sandbox({ multiMode: true });
31 |
32 | test('execute script in sandbox', () => {
33 | sandbox.execScriptInSandbox('window.a = 1;expect(window.a).toBe(1);');
34 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
35 | expect((window as any).a).toBe(undefined);
36 | });
37 | });
38 |
39 | describe('sandbox: access constructor', () => {
40 | const sandbox = new Sandbox();
41 |
42 | test('execute global functions', () => {
43 | sandbox.execScriptInSandbox('window.error = new Error("errmsg");Error.toString();');
44 | const globalWindow = sandbox.getSandbox();
45 | expect((globalWindow as any).error.toString()).toBe('Error: errmsg');
46 | });
47 | });
48 |
49 | describe('sanbox: binding this', () => {
50 | const sandbox = new Sandbox();
51 | test('bind this to proxy', () => {
52 | sandbox.execScriptInSandbox('expect(window === this).toBe(true);');
53 | });
54 | });
55 |
56 | describe('sandbox: falsy values should be trapped.', () => {
57 | const sandbox = new Sandbox({ multiMode: true });
58 |
59 | test('Falsy value - 0', () => {
60 | sandbox.execScriptInSandbox('window.a = 0;expect(window.a).toBe(0);');
61 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
62 | expect((window as any).a).toBe(undefined);
63 | });
64 |
65 | test('Falsy value - false', () => {
66 | sandbox.execScriptInSandbox('window.b = false;expect(window.b).toBe(false);');
67 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
68 | expect((window as any).a).toBe(undefined);
69 | });
70 |
71 | test('Falsy value - void 0', () => {
72 | sandbox.execScriptInSandbox('window.c = void 0;expect(window.c).toBe(undefined);');
73 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
74 | expect((window as any).a).toBe(undefined);
75 | });
76 | });
77 |
78 | describe('sandbox: eventListener and setTimeout should be trapped', () => {
79 | /**
80 | * for some reason, set `setTimeout: false` to enable communicate with global.
81 | */
82 | const sandbox = new Sandbox({ multiMode: false });
83 |
84 | test('trap eventListener and setTimeout', async () => {
85 | sandbox.execScriptInSandbox(`
86 | window.count = 0;
87 | window.addEventListener('popstate', (event) => {
88 | console.warn('sandbox: onPopState count', count);
89 | count += 1;
90 | });
91 | history.pushState({page: 1}, "title 1", "?page=1");
92 | history.pushState({page: 2}, "title 2", "?page=2");
93 | history.pushState({page: 3}, "title 3", "?page=3");
94 | history.back();
95 |
96 | window.id = setTimeout(() => {
97 | expect(count).toEqual(1);
98 | }, 100)
99 | `);
100 |
101 | await delay(1000);
102 | expect((window as any).count).toEqual(1);
103 | sandbox.clear();
104 | history.back();
105 | await delay(1000);
106 | expect((window as any).count).toEqual(undefined);
107 | });
108 | });
109 |
110 | describe('eval in sandbox', () => {
111 | const sandbox = new Sandbox({ multiMode: true });
112 |
113 | test('execution context is not global execution context', () => {
114 | let error = null;
115 | try {
116 | sandbox.execScriptInSandbox(
117 | `
118 | function bar (value) {
119 | eval('console.log(value);');
120 | }
121 | bar(1);
122 | `,
123 | );
124 | } catch (e) {
125 | error = e.message;
126 | }
127 |
128 | expect(error).toBe(null);
129 | });
130 | });
131 |
132 | describe('callable functions in sandbox', () => {
133 | const sandbox = new Sandbox({ multiMode: true });
134 |
135 | test('callable function with extra properties', () => {
136 | // @ts-ignore
137 | window.axios = function(){};
138 | // @ts-ignore
139 | axios.create = function(){};
140 | let error = null;
141 | try {
142 | sandbox.execScriptInSandbox(
143 | `
144 | axios.create();
145 | `,
146 | );
147 | } catch (e) {
148 | error = e.message;
149 | }
150 |
151 | expect(error).toBe(null);
152 | });
153 | });
154 |
--------------------------------------------------------------------------------
/packages/sandbox/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@ice/sandbox",
3 | "version": "1.1.4",
4 | "description": "sandbox for execute scripts",
5 | "main": "lib/index.js",
6 | "scripts": {
7 | "build": "rm -rf lib && tsc",
8 | "watch": "tsc -w",
9 | "test": "NODE_ENV=unittest pnpm jest",
10 | "lint": "pnpm eslint --ext .js,.jsx,.ts,.tsx ./",
11 | "prepublishOnly": "pnpm run-s lint test build"
12 | },
13 | "repository": {
14 | "type": "git",
15 | "url": "git+https://github.com/ice-lab/icestark.git"
16 | },
17 | "files": [
18 | "lib"
19 | ],
20 | "keywords": [
21 | "sandbox",
22 | "icestark"
23 | ],
24 | "license": "MIT",
25 | "bugs": {
26 | "url": "https://github.com/ice-lab/icestark/issues"
27 | },
28 | "homepage": "https://github.com/ice-lab/icestark#readme",
29 | "devDependencies": {
30 | "typescript": "^3.8.3"
31 | },
32 | "jest": {
33 | "coverageDirectory": "./coverage/",
34 | "collectCoverage": true,
35 | "preset": "ts-jest",
36 | "automock": false,
37 | "testMatch": [ "**/__tests__/**/*.ts?(x)", "**/?(*.)+(spec|test).ts?(x)" ],
38 | "setupFiles": [
39 | "../../setupJest.ts"
40 | ]
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/packages/sandbox/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "jsx": "react",
5 | "experimentalDecorators": true,
6 | "declaration": true,
7 | "skipLibCheck": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "outDir": "lib"
10 | },
11 | "include": ["src/*"],
12 | "exclude": ["node_modules"]
13 | }
14 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - 'packages/*'
--------------------------------------------------------------------------------
/scripts/beta.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Scripts to check unpublished version and run publish
3 | */
4 | import { join } from 'path';
5 | import { spawnSync } from 'child_process';
6 | import { IPackageInfo, getPackageInfos } from './getPackageInfos';
7 |
8 | const semverReg = /^\d+\.\d+\.\d+$/;
9 |
10 | if (process.env.BRANCH_NAME === 'master') {
11 | console.log('The current branch forbids publishing.', process.env.BRANCH_NAME);
12 | process.exit(0);
13 | }
14 |
15 | function publishBeta(pkg: string, version: string, directory: string): void {
16 | console.log('[PUBLISH BETA]', `${pkg}@${version}`);
17 | console.log('[PUBLISH BETA]', directory);
18 | spawnSync('npm', [
19 | 'publish',
20 | " --tag='beta'",
21 | ], {
22 | stdio: 'inherit',
23 | cwd: directory,
24 | shell: true,
25 | });
26 | }
27 |
28 | // Entry
29 | console.log('[PUBLISH BETA] Start:');
30 |
31 | getPackageInfos(join(__dirname, '../packages'), true)
32 | .then((packageInfos: IPackageInfo[]) => {
33 | let publishedCount = 0;
34 | // Publish
35 | for (let j = 0; j < packageInfos.length; j++) {
36 | const { name, directory, localVersion, shouldPublish } = packageInfos[j];
37 | const conformedSemver = semverReg.test(localVersion);
38 | if (shouldPublish) {
39 | if (conformedSemver) {
40 | console.log(`Package ${name} expects to provide a semver version., instead of ${localVersion}`);
41 | continue;
42 | }
43 |
44 | publishedCount++;
45 | console.log(`--- ${name}@${localVersion} ---`);
46 | publishBeta(name, localVersion, directory);
47 | }
48 | }
49 | console.log(`[PUBLISH BETA] Complete (count=${publishedCount}).`);
50 | });
51 |
52 |
--------------------------------------------------------------------------------
/scripts/getPackageInfos.ts:
--------------------------------------------------------------------------------
1 | import { existsSync, readdirSync, readFileSync } from 'fs';
2 | import { join } from 'path';
3 | import * as urllib from 'urllib';
4 |
5 | const TIMEOUT = 8000; // ms
6 |
7 | export interface IPackageInfo {
8 | name: string;
9 | directory: string;
10 | localVersion: string;
11 | shouldPublish: boolean;
12 | }
13 |
14 | function checkVersionExists(pkg: string, version: string): Promise {
15 | return urllib.request(
16 | `https://registry.npmjs.com/${pkg}/${version}`,
17 | { dataType: 'json', timeout: TIMEOUT },
18 | ).then((res) => res.status === 200).catch(() => false);
19 | }
20 |
21 | export async function getPackageInfos(targetDir: string, isMonorepos: boolean): Promise {
22 | console.log('[targetDir]', targetDir);
23 | const packageInfos: IPackageInfo[] = [];
24 | if (!existsSync(targetDir)) {
25 | console.log(`[ERROR] Directory ${targetDir} not exist!`);
26 | } else {
27 | const packageFolders: string[] = isMonorepos ? readdirSync(targetDir).filter((filename) => filename[0] !== '.') : [''];
28 | console.log('[PUBLISH] Start check with following packages:');
29 | await Promise.all(packageFolders.map(async (packageFolder) => {
30 | const directory = join(targetDir, packageFolder);
31 | const packageInfoPath = join(directory, 'package.json');
32 |
33 | // Process package info.
34 | if (existsSync(packageInfoPath)) {
35 | const packageInfo = JSON.parse(readFileSync(packageInfoPath, 'utf8'));
36 | const packageName = packageInfo.name || packageFolder;
37 |
38 | console.log(`- ${packageName}`);
39 |
40 | try {
41 | packageInfos.push({
42 | name: packageName,
43 | directory,
44 | localVersion: packageInfo.version,
45 | // If localVersion not exist, publish it
46 | shouldPublish: !await checkVersionExists(packageName, packageInfo.version),
47 | });
48 | } catch (e) {
49 | console.log(`[ERROR] get ${packageName} information failed: `, e);
50 | }
51 | } else {
52 | console.log(`[ERROR] ${packageFolder}'s package.json not found.`);
53 | }
54 | }));
55 | }
56 | return packageInfos;
57 | }
58 |
--------------------------------------------------------------------------------
/scripts/publish.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Scripts to check unpublished version and run publish
3 | */
4 | import { join } from 'path';
5 | import { spawnSync } from 'child_process';
6 | import { IPackageInfo, getPackageInfos } from './getPackageInfos';
7 |
8 | if (process.env.BRANCH_NAME !== 'master') {
9 | console.log('The current branch forbids publishing.', process.env.BRANCH_NAME);
10 | process.exit(0);
11 | }
12 |
13 | function publish(pkg: string, version: string, directory: string): void {
14 | console.log('[PUBLISH]', `${pkg}@${version}`);
15 | console.log('[PUBLISH]', directory);
16 | spawnSync('npm', [
17 | 'publish',
18 | // use default registry
19 | ], {
20 | stdio: 'inherit',
21 | cwd: directory,
22 | });
23 | }
24 |
25 | // Entry
26 | console.log('[PUBLISH] Start:');
27 |
28 | getPackageInfos(join(__dirname, '../packages'), true)
29 | .then((packageInfos: IPackageInfo[]) => {
30 | let publishedCount = 0;
31 | // Publish
32 | for (let j = 0; j < packageInfos.length; j++) {
33 | const { name, directory, localVersion, shouldPublish } = packageInfos[j];
34 | if (shouldPublish) {
35 | publishedCount++;
36 | console.log(`--- ${name}@${localVersion} ---`);
37 | publish(name, localVersion, directory);
38 | }
39 | }
40 | console.log(`[PUBLISH] Complete (count=${publishedCount}).`);
41 | });
42 |
--------------------------------------------------------------------------------
/setupJest.ts:
--------------------------------------------------------------------------------
1 | import { GlobalWithFetchMock } from 'jest-fetch-mock';
2 |
3 | const customGlobal: GlobalWithFetchMock = global as any;
4 | customGlobal.fetch = require('jest-fetch-mock');
5 |
6 | customGlobal.fetchMock = customGlobal.fetch;
7 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "module": "esnext",
5 | "moduleResolution": "Node",
6 | "jsx": "react",
7 | "experimentalDecorators": true,
8 | "declaration": true,
9 | "skipLibCheck": true,
10 | "forceConsistentCasingInFileNames": true,
11 | "outDir": "lib",
12 | "allowSyntheticDefaultImports": true,
13 | "esModuleInterop": true
14 | },
15 | "exclude": ["node_modules"],
16 | "ts-node": {
17 | // these options are overrides used only by ts-node
18 | // same as our --compilerOptions flag and our TS_NODE_COMPILER_OPTIONS environment variable
19 | "compilerOptions": {
20 | "module": "commonjs"
21 | }
22 | },
23 | }
24 |
--------------------------------------------------------------------------------
/website/.prettierrc.js:
--------------------------------------------------------------------------------
1 | const { getPrettierConfig } = require('@iceworks/spec');
2 |
3 | // getPrettierConfig(rule: 'rax'|'react'|'vue', customConfig?);
4 | module.exports = getPrettierConfig('react');
--------------------------------------------------------------------------------
/website/README.md:
--------------------------------------------------------------------------------
1 | # Website for icestark
2 |
3 | This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator.
4 |
5 | ## Installation
6 |
7 | ```console
8 | yarn install
9 | ```
10 |
11 | ## Local Development
12 |
13 | ```console
14 | yarn start
15 | ```
16 |
17 | This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
18 |
19 | ## Deployment
20 |
21 | Just push.
22 |
23 |
--------------------------------------------------------------------------------
/website/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
3 | };
4 |
--------------------------------------------------------------------------------
/website/blog/01-icestark-2-5-0.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: icestark 2.5.0
3 | author: 那吒
4 | author_url: https://github.com/maoxiaoke
5 | author_image_url: https://avatars.githubusercontent.com/u/13417006?v=4
6 | tags: [v2, announcement]
7 | description: Announcing icestark 2.5.0
8 | hide_table_of_contents: false
9 | ---
10 |
11 | ## Announcing icestark 2.5.0
12 |
13 | 在 2.5.0 版本中,我们主要统一了在使用 [API](/../docs/api/ice-stark#核心-api) 和 [React Component](/../docs/api/ice-stark#react-%E7%BB%84%E4%BB%B6) 接入的一些字段使用方式,并集中修复了一些常见问题。本次更新主要包括:
14 |
15 | + [对齐 API 和 React Component 使用字段](#对齐-api-和-react-component-使用字段)
16 | + [重构路由匹配算法](#重构路由匹配算法)
17 | + [优化 icestark 本地开发以及沙箱的调试体验](#优化-icestark-本地开发以及沙箱的调试体验)
18 | + [修复使用 Prompt 组件二次弹框的问题](#修复使用-prompt-组件二次弹框的问题)
19 |
20 |
21 |
22 | ### 对齐 API 和 React Component 使用字段
23 |
24 | 在之前的版本中,[AppConfig](/../docs/api/ice-stark#appconfig) 有些字段和 `` 接收的 props 存在一定的差异,对用户的使用造成了困恼。因此,我们整体梳理了相关字段,主要的变更如下:
25 |
26 | #### 1. `` 的 [path](/../docs/api/ice-stark#path) 字段废弃,请使用 [activePath](/../docs/api/ice-stark#activepath) 字段。
27 |
28 | `activePath` 是 [appConfig](/../docs/api/ice-stark#appconfig) 定义的基础字段,除了支持 [path](/../docs/api/ice-stark#path) 所有配置能力外,还支持函数写法,可自定义路由匹配逻辑:
29 |
30 | ```ts
31 | {
32 | name: 'app',
33 | activePath: (url) => {
34 | return url.includes('/seller'); // 当路由匹配上 /seller,则激活应用
35 | }
36 | }
37 | ```
38 |
39 | 值得注意的是,当使用函数写法的 `activePath` 是,需要有 `name` 字段标识,否则会有 Error 提示;对于其他非函数写法,icestark 会默认根据 `activePath` 的配置生成一个 `name` 值。
40 |
41 | :::info
42 | 建议在 [配置微应用](/../docs/api/ice-stark#appconfig) 时,添加 `name` 字段。
43 | :::
44 |
45 | #### 2. AppConfig 提供 basename 字段
46 |
47 | [basename](/../docs/api/ice-stark#basename) 字段可以方便地自定义微应用路由的 [basename](https://reactrouter.com/web/api/BrowserRouter/basename-string)。我们在 `` 的 Props 支持了这一字段,但是在 AppConfig 并没有支持这一字段。比如,当使用 [createMicroApp](/../docs/api/ice-stark#createmicroapp) 加载微应用时,需要通过主动调用 [setBasename](/../docs/api/ice-stark-app#getbasename) 来设置 `basename`。
48 |
49 | ```js
50 | import { createMicroApp } from '@ice/stark';
51 | import { setBasename } from '@ice/stark-app';
52 |
53 | setBasename('/seller');
54 |
55 | createMicroApp({
56 | name: 'app',
57 | activePath: '/seller',
58 | url: ['/js/index.js'],
59 | container: ref.current;
60 | });
61 | ```
62 |
63 | 现在,你可以使用 `basename` 字段:
64 |
65 | ```js
66 | import { createMicroApp } from '@ice/stark';
67 |
68 | createMicroApp({
69 | name: 'app',
70 | activePath: '/seller',
71 | basename: '/seller',
72 | url: ['/js/index.js'],
73 | container: ref.current;
74 | });
75 | ```
76 |
77 | #### 3. 在 `` 中使用 onLoadingApp、onFinishLoading、onError 等 Hooks
78 |
79 | 在之前的版本中,使用 `` 不太方便对微应用执行的各个阶段进行监控或埋点(虽然这些能力均在 [API](/../docs/api/ice-stark#核心-api) 中支持)。因此,我们也在 `` 中透出了这些 Hooks。
80 |
81 | > 更多有关 [对齐 API 和 React Component 使用字段](#对齐-api-和-react-component-使用字段) 可参见 [RFC](https://github.com/ice-lab/icestark/issues/299)。
82 |
83 |
84 | ### 重构路由匹配算法
85 |
86 | 在新版本中,我们对 [路由匹配算法](https://github.com/ice-lab/icestark/blob/release/2.5.0/src/util/checkActive.ts) 进行了重构。重构之后的版本并不会影响现有的代码功能,对于这一功能的变更,我们进行了充分的测试。但如果您的应用因此受到了一些影响,欢迎通过 [issue](https://github.com/ice-lab/icestark/issues) 来告知我们。
87 |
88 | ### 优化 icestark 本地开发以及沙箱的调试体验
89 |
90 | 当我们本地开发时候,source map 对定位源码非常有效。但是使用 [fetch](/../docs/api/ice-stark/#loadscriptmode) 来加载 js 资源时候,由于当前 origin 是主应用的 origin,导致 source map 文件加载失败。如图:
91 |
92 | 
93 |
94 | 在 [Source Map Revision 3 Proposal](https://docs.google.com/document/u/0/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/mobilebasic) 中,我们找到解决这个问题的一些蛛丝马迹,通过在代码末尾添加 `//# sourceURL= */` 解决这个问题。
95 |
96 | 有两点需要注意:
97 |
98 | 1. 该方式对 sandbox 同样有效,很好地缓解了 sandbox 的定位难题
99 | 2. 如果是通过 `` 标签加载的资源,不受该问题限制
100 |
101 | ### 修复使用 Prompt 组件二次弹框的问题
102 |
103 | 当微应用使用 ReactRouterDOM 的 [Prompt](https://reactrouter.com/ice-stark/api/Prompt) 组件时,会出现两次 confirm 框确认,如图:
104 |
105 | 
106 |
107 | 新版本中,我们修复了这一异常行为。可以访问 [从 Prompt 来看微前端路由劫持原理](https://zhuanlan.zhihu.com/p/394624654) 了解我们对一问题的追溯。
108 |
109 |
110 | ## 后续的版本计划
111 |
112 | 我们会持续扩展 icestark 的能力,提升微前端体验。在接下来的版本中,我们会:
113 |
114 | + 提供更优的方式接入 es module 微应用 ([rfc](https://github.com/ice-lab/icestark/issues/346))
115 | + 优化 icestark 的错误提示信息 ([issue](https://github.com/ice-lab/icestark/issues/308))
116 | + 给新手用户提供更简单的接入指导
117 | + 提供多页签的微应用模板
118 |
--------------------------------------------------------------------------------
/website/blog/03-icestark-2-7-0.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: icestark 2.7.0
3 | author: 那吒
4 | author_url: https://github.com/maoxiaoke
5 | author_image_url: https://avatars.githubusercontent.com/u/13417006?v=4
6 | tags: [v2, announcement]
7 | description: Announcing icestark 2.7.0
8 | hide_table_of_contents: false
9 | ---
10 |
11 | ## Announcing icestark 2.7.0
12 |
13 | 本次更新为大家带来了应用的细节优化,更新主要包括:
14 |
15 | - [缓存 css 资源](#缓存-css-资源)
16 | - [为 Vite 应用的开发者提供便捷的接入插件](#为-vite-应用的开发者提供便捷的接入插件)
17 | - [appHistory 支持传递 state](#apphistory-支持传递-state)
18 | - [常规的错误修复](#常规的错误修复)
19 |
20 |
21 |
22 | ### 缓存 css 资源
23 |
24 | 持续优化微前端应用的加载体验是我们一直追求的事情。在这边版本中,在微应用的以下配置:
25 |
26 | + [loadScriptMode](/../docs/api/ice-stark#loadscriptmode) 配置为 `fetch` 或 `import`
27 | + 开启脚本沙箱,即 [sandbox](/../docs/api/ice-stark#sandbox) 设置为 `true` 或自定义沙箱
28 | + 开启 [umd: true](/../docs/api/ice-stark#umd)
29 |
30 | 我们会默认缓存样式资源,以提升微应用二次加载的体验。对比如下(前图未缓存样式,二次加载有明显加载 Loading;后图为缓存样式):
31 |
32 |
33 |

34 |

35 |
36 |
37 | 为了保证用户尽可能地不产生 Break Change,以下场景不会默认缓存 css 资源:
38 |
39 | + 配置了 [shouldAssetsRemove](/../docs/api/ice-stark#shouldassetsremove)
40 | + fetch 样式资源失败,使用原方式处理样式资源
41 |
42 | ### 为 Vite 应用的开发者提供便捷的接入插件
43 |
44 | 满足使用 Vite 官方应用的用户便捷地接入 icestark,我们提供了 [vite-plugin-index-html](https://github.com/alibaba/ice/tree/master/packages/vite-plugin-index-html) Vite 插件。该插件提供了类似 [webpack-html-plugin](https://github.com/jantimon/html-webpack-plugin) 的能力,会将 Vite 生成的虚拟入口,替换成用户指定的入口。
45 |
46 | 用户可按照我们的 [教程](/../docs/guide/use-child/others#vite-应用) 接入。
47 |
48 | 该插件的简单用法如下:
49 |
50 | ```diff
51 | import { defineConfig } from 'vite';
52 | import vue from '@vitejs/plugin-vue';
53 | + import htmlPlugin from 'vite-plugin-index-html';
54 | export default defineConfig({
55 | plugins: [
56 | vue(),
57 | + htmlPlugin({
58 | + input: './src/main.ts', // 指定确定的入口文件
59 | + preserveEntrySignatures: "exports-only", // 确保入口文件导出生命周期函数
60 | + })
61 | ],
62 | })
63 | ```
64 |
65 | ### appHistory 支持传递 state
66 |
67 | 为满足用户通过 [history state](https://developer.mozilla.org/en-US/docs/Web/API/History/state) 传参的诉求,[appHistory](/../docs/api/ice-stark-app#apphistory) 和 [``](/../docs/api/ice-stark-app#applink) 可通过第二个参数传递 state。用法如下:
68 |
69 | ```js
70 | appHistory.push('/home?name=ice', { framework: 'icestark' });
71 |
72 |
81 | 使用 AppLink 进行页面跳转
82 |
83 | ```
84 |
85 | ### 常规的错误修复
86 |
87 | - [x] 修复 [registerAppEnter](/../docs/api/ice-stark-app/#registerappenter) 以及 [registerAppLeave](/../docs/api/ice-stark-app/#registerAppLeave) 类型问题
88 | - [x] 加载 esm 应用时,提供更精确的错误提示,[#466](https://github.com/ice-lab/icestark/issues/466)
89 | - [x] 修复 AppLink 丢失 global 绑定可能导致的 Illegal invocation 问题,[#426](https://github.com/ice-lab/icestark/issues/426)
90 | - [x] 修复使用 ice.js 插件 [build-plugin-icestark](https://ice.work/docs/guide/advanced/icestark/) 导致切换主应用路由是,造成重复渲染,[#427](https://github.com/ice-lab/icestark/issues/427)
91 |
92 | ## 后续的版本计划
93 |
94 | 我们会持续扩展 icestark 的能力,提升微前端体验。在接下来的版本中,我们会:
95 |
96 | + 我们会结合官网,提供详尽的报错指引
97 | + 提供官方的权限控制实践 ([rfcs](https://github.com/ice-lab/icestark/issues/396))
98 | + 以及样式隔离方案 ([rfcs](https://github.com/ice-lab/icestark/issues/413))
99 |
--------------------------------------------------------------------------------
/website/config/footer.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | style: 'dark',
3 | links: [
4 | {
5 | title: '社区',
6 | items: [
7 | {
8 | label: '社区钉钉群',
9 | href: 'https://iceworks.oss-cn-hangzhou.aliyuncs.com/assets/images/ice-group.png',
10 | },
11 | {
12 | label: '阿里内部钉钉群',
13 | href: 'https://iceworks.oss-cn-hangzhou.aliyuncs.com/assets/images/ice-group-inside.JPG',
14 | },
15 | {
16 | label: 'GitHub Issue',
17 | href: 'https://github.com/ice-lab/icestark/issues',
18 | },
19 | ],
20 | },
21 | {
22 | title: '帮助',
23 | items: [
24 | {
25 | label: '反馈问题',
26 | href: 'https://github.com/ice-lab/icestark/issues/new?assignees=&labels=bug&template=bug_report.md',
27 | },
28 | {
29 | label: '提交需求',
30 | href: 'https://github.com/ice-lab/icestark/issues/new',
31 | },
32 | ],
33 | },
34 | {
35 | title: '更多',
36 | items: [
37 | {
38 | label: '飞冰(ICE)',
39 | href: 'https://ice.work/',
40 | },
41 | {
42 | label: 'Rax',
43 | href: 'https://rax.js.org',
44 | },
45 | {
46 | label: 'AppWorks',
47 | href: 'https://appworks.site',
48 | },
49 | {
50 | label: 'Kraken',
51 | href: 'https://openkraken.com/',
52 | },
53 | ],
54 | },
55 | ],
56 | copyright: `Copyright © ${new Date().getFullYear()} 飞冰(ICE). Built with Docusaurus.`,
57 | };
58 |
--------------------------------------------------------------------------------
/website/config/navbar.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | title: 'ICESTARK',
3 | logo: {
4 | alt: 'icestark',
5 | src: 'img/logo.png',
6 | },
7 | items: [
8 | {
9 | type: 'search',
10 | position: 'right',
11 | },
12 | {
13 | to: '/docs/guide',
14 | position: 'right',
15 | label: '文档',
16 | },
17 | {
18 | to: '/docs/api/ice-stark',
19 | position: 'right',
20 | label: 'API',
21 | },
22 | {
23 | to: '/docs/faq',
24 | position: 'right',
25 | label: '常见问题',
26 | },
27 | // {
28 | // position: 'right',
29 | // label: 'Changelog',
30 | // href: 'https://github.com/ice-lab/icestark/releases',
31 | // },
32 | // currently hide blog
33 | {
34 | label: '博客',
35 | to: 'blog',
36 | position: 'right',
37 | },
38 | {
39 | href: 'https://github.com/ice-lab/icestark',
40 | // label: 'GitHub',
41 | className: 'header-github-link',
42 | position: 'right',
43 | },
44 | ],
45 | };
46 |
--------------------------------------------------------------------------------
/website/config/sidebars.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) Facebook, Inc. and its affiliates.
3 | *
4 | * This source code is licensed under the MIT license found in the
5 | * LICENSE file in the root directory of this source tree.
6 | */
7 | const getDocsFromDir = require('../scripts/getDocsFromDir');
8 |
9 | module.exports = {
10 | guide: [
11 | 'guide',
12 | 'guide/upgrade',
13 | {
14 | type: 'category',
15 | label: '概念',
16 | collapsed: false,
17 | items: getDocsFromDir('guide/concept'),
18 | },
19 | {
20 | type: 'category',
21 | label: '主应用接入',
22 | collapsed: false,
23 | items: getDocsFromDir('guide/use-layout'),
24 | },
25 | {
26 | type: 'category',
27 | label: '微应用接入',
28 | collapsed: false,
29 | items: getDocsFromDir('guide/use-child'),
30 | },
31 | {
32 | type: 'category',
33 | label: '进阶',
34 | collapsed: false,
35 | items: getDocsFromDir('guide/advanced'),
36 | },
37 | 'guide/ecosystem',
38 | 'guide/micro-module',
39 | ],
40 | api: [
41 | 'api/ice-stark',
42 | 'api/ice-stark-app',
43 | 'api/stark-module',
44 | ],
45 | };
46 |
--------------------------------------------------------------------------------
/website/docs/api/stark-module.md:
--------------------------------------------------------------------------------
1 | ---
2 | toc: menu
3 | ---
4 |
5 | # @ice/stark-module
6 |
7 | 以下 api 均从 `@ice/stark-module` 导出。使用方式如下:
8 |
9 | ```js
10 | import { MicroModule, registerModule } from '@ice/stark-module';
11 | ```
12 |
13 | 对于使用非 React 的用户,我们建议您直接从 `lib` 目录下导入这些方法。
14 |
15 | ```js
16 | // 对于使用 Vue、Angular 的用户
17 | import { mountModule, unmountModule } from '@ice/stark-module/lib/modules';
18 | ```
19 |
20 | ## ModuleInfo
21 |
22 | `ModuleInfo` 是一个类型接口,用于定义一个微模块结构。接口定义如下:
23 |
24 | ```ts
25 | name: string;
26 | url: string | string[];
27 | render?: (props: StarkModule) => any;
28 | runtime?: Runtime;
29 | mount?: (Component: any, targetNode: HTMLElement, props?: any) => void;
30 | unmount?: (targetNode: HTMLElement) => void;
31 | ```
32 |
33 | 各字段介绍如下:
34 |
35 | ### name
36 |
37 | 微模块唯一标识(必填)。
38 |
39 | - 类型: `string`
40 | - 默认值:`-`
41 |
42 | ### url
43 |
44 | 微模块静态资源对应的 CDN 地址,当渲染微模块时,会主动加载该资源(必填)。
45 |
46 | - 类型:`string | string[]`
47 | - 默认值:`-`
48 |
49 |
50 | ### render
51 |
52 | 用于渲染一个本地模块(选填),参考 [注册本地模块](../guide/micro-module#注册本地模块)。
53 |
54 |
55 | ### runtime
56 |
57 | 用于加载公共依赖库(选填),参考 [性能优化](../guide/micro-module#性能优化)。
58 |
59 | ### mount
60 |
61 | 用于自定义生命周期(选填),参考 [自定义生命周期](../guide/micro-module#自定义生命周期)。
62 |
63 | ### unmount
64 |
65 | 用于自定义生命周期(选填),参考 [自定义生命周期](../guide/micro-module#自定义生命周期)。
66 |
67 | ## MicroModule
68 |
69 | 封装底层能力的 React Component,用于 React 框架模块快速接入。
70 |
71 | ```js
72 | import { MicroModule } from '@ice/stark-module';
73 |
74 | const App = () => {
75 | const moduleInfo = {
76 | name: 'moduleName',
77 | url: 'https://localhost/module.js',
78 | }
79 | return ;
80 | }
81 | ```
82 |
83 | ## registerModule
84 |
85 | 注册单个模块。
86 |
87 | ```js
88 | registerModule({
89 | url: 'https://localhost/module-a.js',
90 | name: 'module-a',
91 | });
92 | ```
93 |
94 | ## registerModules
95 |
96 | 同时注册多个模块。
97 |
98 | ```js
99 | registerModules([
100 | {
101 | url: 'https://localhost/module-b.js',
102 | name: 'module-b',
103 | },
104 | {
105 | url: 'https://localhost/module-c.js',
106 | name: 'module-c',
107 | },
108 | ]);
109 | ```
110 |
111 | ## clearModules
112 |
113 | 移除已注册的所有模块。
114 |
115 | ```js
116 | import { registerModules, clearModules } from '@ice/stark-module';
117 |
118 | registerModules([
119 | {
120 | url: 'https://localhost/module-a.js',
121 | name: 'module-a',
122 | },
123 | {
124 | url: 'https://localhost/module-b.js',
125 | name: 'module-b',
126 | },
127 | ]);
128 |
129 | clearModules();
130 | ```
131 |
132 | ## mountModule
133 |
134 | 挂载模块,提供自定义组件能力。
135 |
136 | ```js
137 | import { mountModule, unmoutModule } from '@ice/stark-module';
138 |
139 | const moduleInfo = {
140 | name: 'moduleName',
141 | url: 'https://localhost/module.js',
142 | };
143 |
144 | const ModuleComponent = () => {
145 | const renderNode = useRef(null);
146 | useEffect(() => {
147 | mountModule(moduleInfo, renderNode, {});
148 | return () => {
149 | unmoutModule(moduleInfo, renderNode);
150 | }
151 | }, []);
152 | return ();
153 | };
154 | ```
155 |
156 | ## unmoutModule
157 |
158 | 卸载模块,提供自定义组件能力,见 [mountModule](#mountModule)。
159 |
160 |
--------------------------------------------------------------------------------
/website/docs/guide.md:
--------------------------------------------------------------------------------
1 | # 快速起步
2 |
3 | ## 介绍
4 |
5 | [icestark](https://github.com/ice-lab/icestark) 是一个面向大型系统的微前端解决方案,适用于以下业务场景:
6 |
7 | - 后台比较分散,体验差别大,因为要频繁跳转导致操作效率低,希望能统一收口的一个系统内
8 | - 单页面应用非常庞大,多人协作成本高,开发/构建时间长,依赖升级回归成本高
9 | - 系统有二方/三方接入的需求
10 |
11 | icestark 在保证一个系统的操作体验基础上,实现各个微应用的独立开发和发版,主应用通过 icestark 管理微应用的注册和渲染,将整个系统彻底解耦。
12 |
13 | ## 项目初始化
14 |
15 | icestark 可以通过简单的命令行,生成主应用和微应用模板。无论您是使用 React 还是 Vue,都可以便捷的创建符合 icestark 微前端规范的项目。这些项目均由 icestark 团队官方维护。
16 |
17 | :::tip
18 | 如果您想将正在开发中或已开发完成的项目接入 icestark,请移步[主应用接入](guide/use-layout/react)和[微应用接入](guide/use-child/react)。如果您使用的是 [create-react-app](https://github.com/facebook/create-react-app) 、umi 等框架开发的应用,亦可参考[其它框架的接入指南](guide/use-child/others)。
19 | :::
20 |
21 | ### 初始化主应用
22 |
23 | ```bash
24 | # 基于 React 的主应用
25 | $ npm init ice icestark-layout @icedesign/stark-layout-scaffold
26 | # 或者基于 Vue 的主应用
27 | $ npm init ice icestark-layout @vue-materials/icestark-layout-app
28 |
29 | $ cd icestark-layout
30 | $ npm install
31 | $ npm start
32 | ```
33 |
34 | ### 初始化微应用
35 |
36 | ```bash
37 | # 基于 React 的微应用
38 | $ npm init ice icestark-child @icedesign/stark-child-scaffold
39 | # 基于 Vue 的微应用
40 | $ npm init ice icestark-child @vue-materials/icestark-child-app
41 |
42 | $ cd icestark-child
43 | $ npm install
44 | $ npm run start
45 | ```
46 |
47 | ## 兼容性
48 |
49 | + 现代浏览器和 IE11。
50 |
51 | :::tip
52 | 对于 IE 系列浏览器,需要提供相应的 polyfill 支持。详细介绍,请参考[常见问题 -> 兼容 IE 浏览器](faq#兼容-ie-浏览器)
53 | :::
54 |
--------------------------------------------------------------------------------
/website/docs/guide/advanced/communication.md:
--------------------------------------------------------------------------------
1 | import Badge from '../../../src/components/Badge'
2 |
3 | # 应用间通信
4 |
5 | 通过 `@ice/stark-data` 这个包可以很简单的实现应用间通信,比如全局切换语言微应用响应的场景。`@ice/stark-data` 支持状态共享和事件监听响应两种方式。
6 |
7 | ## store
8 |
9 | ### API
10 |
11 | - `get(key)` **从 store 中获取变量**
12 | - `set(key, value)` **设置/初始化 store 中的变量**
13 | - `on(key, callback, force)` **注册变量监听事件,其中 force 为 boolean 类型,true 则表示初始化注册过程中,会强制执行一次**
14 | - `off(key, callback)` **删除已经注册的变量监听事件**
15 |
16 | ### 示例
17 |
18 | 使用场景:
19 |
20 | 1. 中英文切换,切换按钮在主应用,监听事件在微应用
21 | 2. 获取全局的登录用户信息
22 |
23 | 在主应用存储/设置信息:
24 |
25 | ```js
26 | // 主应用
27 | import { store } from '@ice/stark-data';
28 |
29 | const userInfo = { name: 'Tom', age: 18 };
30 | store.set('language', 'CH'); // 设置语言
31 | store.set('user', userInfo); // 设置登录后当前用户信息
32 |
33 | setTimeout(() => {
34 | store.set('language', 'EN');
35 | }, 3000);
36 | ```
37 |
38 | 在微应用中响应/获取数据:
39 |
40 | ```js
41 | // 微应用
42 | import { store } from '@ice/stark-data';
43 |
44 | // 监听语言变化
45 | store.on('language', (lang) => {
46 | console.log(`current language is ${lang}`);
47 | }, true);
48 |
49 | // 获取当前用户
50 | const userInfo = store.get('user');
51 | ```
52 |
53 | ## event
54 |
55 | ### api
56 |
57 | - `on(key, callback)` **注册回调函数,回调函数的入参通过 emit 注入,如 ...rest**
58 | - `off(key, callback)` **删除已经注册的回调函数**
59 | - `emit(key, ...rest)` **触发已经注册的函数,支持入参**
60 |
61 | ### 示例
62 |
63 | 主应用顶部有**消息**展示入口,微应用内有阅读消息的能力,阅读完消息后需要通知主应用刷新**消息**展示信息。
64 |
65 | 在主应用中监听事件:
66 |
67 | ```js
68 | // 主应用
69 | import { event } from '@ice/stark-data';
70 |
71 | event.on('freshMessage', () => {
72 | // 重新获取消息数
73 | });
74 | ```
75 |
76 | 在微应用中触发事件:
77 |
78 | ```js
79 | // 微应用
80 | import { event } from '@ice/stark-data';
81 |
82 | event.emit('freshMessage');
83 | ```
84 |
85 | ## props
86 |
87 | icestark 还支持通过 [props](../../api/ice-stark#props) 将主应用数据传递给微应用。
88 |
89 | ### 示例
90 |
91 | 主应用向微应用统一注入用户信息。
92 |
93 | 在主应用中通过 [props](../../api/ice-stark#props) 配置用户信息。
94 |
95 | ```js
96 | // src/App.jsx
97 | import { AppRouter, AppRoute } from '@ice/stark';
98 |
99 | const App = () => {
100 | return (
101 |
102 |
114 | ...
115 |
116 | );
117 | }
118 | ```
119 |
120 | 微应用可以通过[生命周期函数](../concept/child#生命周期)获取到该数据:
121 |
122 | ```js
123 | export function mount({ container, customProps }) {
124 | ReactDOM.render(, props.container);
125 | }
126 | ```
127 |
128 |
129 | ## 其他
130 |
131 | 对于主应用和微应用,运行时都共享了当前页面的 location、Cookie、LocalStorage、window 等全局信息,因此应用间的通信,也可以通过这些方案很简单的实现。
132 |
--------------------------------------------------------------------------------
/website/docs/guide/advanced/sandbox.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 样式和脚本隔离
3 | order: 8
4 | ---
5 |
6 | icestark 当下的方案里,无论是主应用还是微应用都是直接在页面里执行,本质上不存在隔离机制,针对这个问题我们一方面通过一些规范来保证污染问题,一方面也在尝试更加彻底的沙箱机制,如果你的微应用都是二方接入,那我们推荐直接通过规范约束即可,如果存在三方接入这种不可控的场景,建议还是通过 iframe 的方式嵌入。
7 |
8 | ## 样式污染
9 |
10 | 页面运行时同时只会存在一个微应用,因此多个微应用不存在样式相互污染的问题,但是主应用和微应用是同时运行的,因此这两者中间可能会存在一些样式相互污染,针对这个问题,我们目前推荐「通过约定避免微应用与主应用的样式相互污染」的方案,同时也在尝试 Shadow DOM 的方案。
11 |
12 | ### 规范
13 |
14 | #### 使用 CSS Modules 方案管理样式
15 |
16 | 无论是主应用还是微应用,直接通过 CSS Modules 的方案管理自身可控的样式,这样基本杜绝了两者样式冲突的问题。
17 |
18 | #### 主应用自定义基础组件 prefix
19 |
20 | 除了自身可控的样式,应用中还会有一些全局样式,比较典型的就是类似 next 这种基础组件的样式,如果主应用和微应用使用了不同版本的 next,则很容易造成样式相互污染,这种场景推荐在主应用中将基础组件的前缀统一改掉,比如将 `next-` 改为 `next-icestark-`,这个能力已在主应用模板中内置,具体可参考相关代码。
21 |
22 | #### 微应用避免产生全局样式
23 |
24 | 对于类似 `normalize.css` 这种全局重置样式,推荐统一通过主应用引入,微应用尽量避免产生全局性质的样式,因为这样在切换微应用时可能会因为全局样式差异产生一些抖动。
25 |
26 | ### Shadow DOM(方案试验中)
27 |
28 | 如果将微应用渲染到 Shadow DOM 中,那么微应用产生的所有样式都不会污染到全局,事实上在我们试验的过程中的确是这样的。但是我们遇到一个当下无法解决的问题,大部分类似 Dialog 组件的实现都是在 body 下创建一个容器节点,但是 Shadow DOM 里 Dialog 的样式无法作用到全局,因此展示出来 Dialog 就是无样式的,在这个问题上我们还在尝试,比如类似 Dialog 组件的实现能够进行优化:判断自身是否在 Shadow DOM 里,如果是的话则将容器节点创建到 Shadow DOM 里,否则创建到 body 节点下。
29 |
30 | ## JS 污染
31 |
32 | 相对于样式污染,JS 污染的危害性更高,在目前的方案下,如果微应用想要恶意污染的话基本是无法杜绝的,因此针对这种不可控的微应用建议还是通过 iframe 的方式接入。针对可控的二方应用,正常书写代码是不会有问题的,针对一些特殊情况我们也总结了一些规范。
33 |
34 | ### 规范
35 |
36 | #### 微应用避免改变全局状态
37 |
38 | 比如改变全局变量 `window/location` 的默认行为,通过 `document` 操作 Layout 的 DOM,这些本身都是一些不推荐的做法。
39 |
40 | #### 主应用通过钩子记录并恢复全局状态
41 |
42 | ```js
43 | {
45 | // 按需记录全局状态
46 | }}
47 | onAppLeave={(appConfig) => {
48 | // 按需恢复全局状态
49 | }}
50 | >
51 | // {...}
52 |
53 | ```
54 |
55 | ### 基于 Proxy 的运行沙箱
56 |
57 | 通过 `with + new Function` 的形式,为微应用脚本创建沙箱运行环境,并通过 Proxy 代理阻断沙箱内对 `window` 全局变量的访问和修改。
58 |
59 | icestark 内置了基于 `@ice/sandbox` 的沙箱隔离,通过 `sandbox` 属性开启:
60 |
61 | ```js
62 |
71 | ```
72 |
--------------------------------------------------------------------------------
/website/docs/guide/concept/child.md:
--------------------------------------------------------------------------------
1 | ---
2 | order: 2
3 | ---
4 |
5 | import Badge from '../../../src/components/Badge'
6 |
7 | # 微应用
8 |
9 | 又称子应用,微应用通常是一个单页面应用(SPA),可能包含一个或多个路由页面,一般情况下不存在多个微应用同时运行的场景。有以下特点:
10 |
11 | + 本身是普通的前端应用,负责具体的业务逻辑;
12 | + 可以独立交付(开发、部署、运行),但是一般会集成到主应用中运行;
13 | + 如有必要,甚至能集成到不同的主应用中。
14 |
15 | ## 生命周期
16 |
17 | 在 icestark 中,微应用是一个 **具有生命周期** 的前端资源。icestark 中有两个生命周期,分别是:
18 |
19 | + 微应用挂载到主应用
20 | + 微应用从主应用卸载
21 |
22 | icestark 支持两种声明生命周期的方式。分别通过全局注册的 `registerAppEnter/registerAppLeave` 以及 UMD 格式下导出的 `mount/unmount` 方法。为了跟社区的 [single-spa](https://single-spa.js.org/) 方案更好兼容,我们推荐使用后者。
23 |
24 | #### 1. registerAppEnter/registerAppLeave
25 |
26 | ```js
27 | import ReactDOM from 'react-dom';
28 | import { registerAppEnter, registerAppLeave } from '@ice/stark-app';
29 | import App from './App';
30 |
31 | registerAppEnter((props) => {
32 | ReactDOM.render(, props.container);
33 | });
34 |
35 | registerAppLeave((props) => {
36 | ReactDOM.unmountComponentAtNode(props.container);
37 | });
38 | ```
39 |
40 | #### 2. mount/unmount
41 |
42 | ```js
43 | import ReactDOM from 'react-dom';
44 | import App from './App';
45 |
46 | export function mount(props) {
47 | ReactDOM.render(, props.container);
48 | }
49 |
50 | export function unmount(props) {
51 | ReactDOM.unmountComponentAtNode(props.container);
52 | }
53 | ```
54 |
55 | ## 入口规范
56 |
57 | icestark 通过微应用入口字段的配置进行应用的渲染,因此这个字段 **非常重要**。针对不同的场景,icestark 也支持了多种入口配置形式。
58 |
59 | ### 1. url
60 |
61 | 适用于微应用入口资源比较确定,此时将这些资源地址按顺序拼成数组传给 icestark 即可。
62 |
63 | ```js
64 | const apps = [{
65 | url: ['https://example.com/a.js', 'https://example.com/a.css'],
66 | activePath: '/foo'
67 | // ...
68 | }]
69 | ```
70 |
71 | ### 2. entry
72 |
73 | 使用场景:
74 |
75 | - 应用依赖的入口资源不确定:比如需要引入 vendor、或者不确定的 externals 资源、资源地址带 hash 等场景
76 | - 应用默认需要依赖很多 DOM 节点:比如 `jQuery`/`Kissy`/`Angular` 等框架
77 |
78 | ```js
79 | const apps = [{
80 | entry: 'https://example.com/a.html',
81 | activePath: '/foo'
82 | // ...
83 | }]
84 | ```
85 |
86 | entry 对应 html url, icestark 对 `entry` 的处理包含以下步骤:
87 |
88 | - 通过 `window.fetch` 获取 entry 属性对应的 html 内容
89 | - 解析 html 内容,框架将会进行解析处理:提取 js 信息,如果资源路径为相对地址,将根据 entry 地址进行补齐
90 | - 将处理后的 html 内容插入 icestark 动态创建的节点
91 | - 依次通过创建 `script` 标签按顺序引入 js 资源
92 |
93 | ### 3. entryContent
94 |
95 | 当需要使用 entry 能力但是 html url 不支持前端跨域访问的情况,可以自行将 html 内容拿到,然后通过 entryContent 属性传递给 icestark。
96 |
97 | ```js
98 | const apps = [{
99 | entryContent: '