├── .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 | [![NPM version](https://img.shields.io/npm/v/@ice/stark-app.svg?style=flat)](https://npmjs.org/package/@ice/stark-app) [![Package Quality](https://npm.packagequality.com/shield/@ice%2Fstark-app.svg)](https://packagequality.com/#?package=@ice%2Fstark-app) [![build status](https://img.shields.io/travis/ice-lab/icestark.svg?style=flat-square)](https://travis-ci.org/ice-lab/icestark) [![Test coverage](https://img.shields.io/codecov/c/github/ice-lab/icestark.svg?style=flat-square)](https://codecov.io/gh/ice-lab/icestark) [![NPM downloads](http://img.shields.io/npm/dm/@ice/stark-app.svg?style=flat)](https://npmjs.org/package/@ice/stark-app) [![David deps](https://img.shields.io/david/ice-lab/icestark.svg?style=flat-square)](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 | [![NPM version](https://img.shields.io/npm/v/@ice/stark-data.svg?style=flat)](https://npmjs.org/package/@ice/stark-data) [![Package Quality](https://npm.packagequality.com/shield/@ice%2Fstark-data.svg)](https://packagequality.com/#?package=@ice%2Fstark-data) [![build status](https://img.shields.io/travis/ice-lab/icestark.svg?style=flat-square)](https://travis-ci.org/ice-lab/icestark) [![Test coverage](https://img.shields.io/codecov/c/github/ice-lab/icestark.svg?style=flat-square)](https://codecov.io/gh/ice-lab/icestark) [![NPM downloads](http://img.shields.io/npm/dm/@ice/stark-data.svg?style=flat)](https://npmjs.org/package/@ice/stark-data) [![David deps](https://img.shields.io/david/ice-lab/icestark.svg?style=flat-square)](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 | [![NPM version](https://img.shields.io/npm/v/@ice/sandbox.svg?style=flat)](https://npmjs.org/package/@ice/sandbox) [![Package Quality](https://npm.packagequality.com/shield/@ice%2Fsandbox.svg)](https://packagequality.com/#?package=@ice%2Fsandbox) [![build status](https://img.shields.io/travis/ice-lab/icestark.svg?style=flat-square)](https://travis-ci.org/ice-lab/icestark) [![Test coverage](https://img.shields.io/codecov/c/github/ice-lab/icestark.svg?style=flat-square)](https://codecov.io/gh/ice-lab/icestark) [![NPM downloads](http://img.shields.io/npm/dm/@ice/sandbox.svg?style=flat)](https://npmjs.org/package/@ice/sandbox) [![David deps](https://img.shields.io/david/ice-lab/icestark.svg?style=flat-square)](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 | ![](https://img.alicdn.com/imgextra/i3/O1CN01Fzwbb31LpQb0uqoHy_!!6000000001348-0-tps-2996-396.jpg) 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. 如果是通过 `