├── packages ├── assets │ ├── playground │ │ └── .gitkeep │ ├── .gitignore │ ├── src │ │ ├── templates │ │ │ ├── micro-frontend │ │ │ │ ├── base-app │ │ │ │ │ ├── src │ │ │ │ │ │ ├── index.tsx.hbs │ │ │ │ │ │ ├── types │ │ │ │ │ │ │ ├── global.d.ts.hbs │ │ │ │ │ │ │ └── assets.d.ts.hbs │ │ │ │ │ │ ├── utils │ │ │ │ │ │ │ ├── shared-utils.ts.hbs │ │ │ │ │ │ │ ├── init-common.ts.hbs │ │ │ │ │ │ │ └── create-micro-app.tsx.hbs │ │ │ │ │ │ ├── routes.ts.hbs │ │ │ │ │ │ ├── pages │ │ │ │ │ │ │ ├── home.less.hbs │ │ │ │ │ │ │ └── home.tsx.hbs │ │ │ │ │ │ ├── common │ │ │ │ │ │ │ └── const.ts.hbs │ │ │ │ │ │ └── app.tsx.hbs │ │ │ │ │ ├── public │ │ │ │ │ │ └── index.html.hbs │ │ │ │ │ ├── scripts │ │ │ │ │ │ └── proxy.ts.hbs │ │ │ │ │ └── app-config.ts.hbs │ │ │ │ ├── micro-app │ │ │ │ │ ├── src │ │ │ │ │ │ ├── pages │ │ │ │ │ │ │ ├── home.less.hbs │ │ │ │ │ │ │ ├── page-one.tsx.hbs │ │ │ │ │ │ │ ├── page-two.tsx.hbs │ │ │ │ │ │ │ └── home.tsx.hbs │ │ │ │ │ │ ├── types │ │ │ │ │ │ │ ├── global.d.ts.hbs │ │ │ │ │ │ │ └── assets.d.ts.hbs │ │ │ │ │ │ ├── init-common.ts.hbs │ │ │ │ │ │ ├── routes.ts.hbs │ │ │ │ │ │ ├── app.tsx.hbs │ │ │ │ │ │ └── index.tsx.hbs │ │ │ │ │ ├── public │ │ │ │ │ │ └── index.html.hbs │ │ │ │ │ ├── scripts │ │ │ │ │ │ └── proxy.ts.hbs │ │ │ │ │ └── app-config.ts.hbs │ │ │ │ └── shared │ │ │ │ │ ├── gitignore.hbs │ │ │ │ │ ├── tsconfig.json.hbs │ │ │ │ │ └── package.json.hbs │ │ │ ├── typescript-project │ │ │ │ ├── src │ │ │ │ │ └── index.ts.hbs │ │ │ │ ├── __tests__ │ │ │ │ │ └── index.spec.ts.hbs │ │ │ │ ├── README.md.hbs │ │ │ │ ├── tsconfig.json.hbs │ │ │ │ └── package.json.hbs │ │ │ └── common │ │ │ │ ├── prettierrc.json.hbs │ │ │ │ ├── jest.config.js.hbs │ │ │ │ └── eslintrc.js.hbs │ │ ├── index.ts │ │ ├── attachments.ts │ │ ├── configurations │ │ │ └── node-plop │ │ │ │ ├── ts-project-generator.ts │ │ │ │ └── micro-fe-generator.ts │ │ └── utils.ts │ ├── README.md │ ├── tsconfig.json │ ├── LICENSE │ └── package.json ├── monitor │ └── monitor-sdk-browser │ │ ├── .gitignore │ │ ├── README.md │ │ ├── .nyc_output │ │ └── out.json │ │ ├── examples │ │ ├── react-app │ │ │ ├── src │ │ │ │ ├── react-app-env.d.ts │ │ │ │ ├── setupTests.ts │ │ │ │ ├── App.test.tsx │ │ │ │ ├── index.css │ │ │ │ ├── reportWebVitals.ts │ │ │ │ ├── App.css │ │ │ │ ├── index.tsx │ │ │ │ ├── App.tsx │ │ │ │ └── logo.svg │ │ │ ├── public │ │ │ │ ├── robots.txt │ │ │ │ ├── favicon.ico │ │ │ │ ├── logo192.png │ │ │ │ ├── logo512.png │ │ │ │ ├── manifest.json │ │ │ │ └── index.html │ │ │ ├── .gitignore │ │ │ ├── tsconfig.json │ │ │ ├── package.json │ │ │ └── README.md │ │ ├── cross-origin-script-error.js │ │ ├── js-error-cross-origin.html │ │ └── performance-timing-for-fetch.html │ │ ├── global.d.ts │ │ ├── src │ │ ├── utils │ │ │ ├── instance-of.ts │ │ │ ├── get-first-and-last.ts │ │ │ ├── assign-keys-between-objects.ts │ │ │ ├── format-plain-headers-string.ts │ │ │ ├── on-page-load.ts │ │ │ ├── on-page-unload.ts │ │ │ ├── patch-method.ts │ │ │ ├── performance-entry.ts │ │ │ ├── get-url-data.ts │ │ │ ├── use-request-animation-frame.ts │ │ │ ├── format-error.ts │ │ │ ├── observe-performance.ts │ │ │ ├── browser-interfaces.ts │ │ │ ├── create-scheduler.ts │ │ │ ├── get-request-report-data.ts │ │ │ ├── observe-long-task-and-resources.ts │ │ │ ├── calculate-tti.ts │ │ │ ├── compute-last-known-network-2-busy.ts │ │ │ ├── get-dom-layout-score.ts │ │ │ └── observe-incoming-requests.ts │ │ ├── fmp │ │ │ ├── types.ts │ │ │ └── fmp-monitor.ts │ │ ├── common-timing │ │ │ ├── types.ts │ │ │ └── common-timing-monitor.ts │ │ ├── fetch │ │ │ ├── types.ts │ │ │ └── fetch-monitor.ts │ │ ├── assets │ │ │ ├── types.ts │ │ │ └── assets-monitor.ts │ │ ├── assets-error │ │ │ ├── types.ts │ │ │ └── assets-error-monitor.ts │ │ ├── js-error │ │ │ ├── types.ts │ │ │ └── js-error-monitor.ts │ │ ├── fid │ │ │ ├── types.ts │ │ │ └── fid-monitor.ts │ │ ├── mpfid │ │ │ ├── types.ts │ │ │ └── mpfid-monitor.ts │ │ ├── tti │ │ │ ├── types.ts │ │ │ └── tti-monitor.ts │ │ ├── constants.ts │ │ ├── cls │ │ │ ├── types.ts │ │ │ └── cls-monitor.ts │ │ ├── index.ts │ │ ├── paint │ │ │ └── types.ts │ │ ├── xhr │ │ │ └── types.ts │ │ └── types.ts │ │ ├── cypress.json │ │ ├── cypress │ │ ├── fixtures │ │ │ └── example.json │ │ ├── tsconfig.json │ │ ├── plugins │ │ │ └── index.js │ │ ├── integration │ │ │ ├── fid.spec.tsx │ │ │ ├── mpfid.spec.tsx │ │ │ ├── cls.spec.tsx │ │ │ ├── assets.spec.ts │ │ │ ├── paint.spec.ts │ │ │ ├── xhr.spec.ts │ │ │ └── fetch.spec.ts │ │ ├── support │ │ │ ├── index.js │ │ │ └── commands.js │ │ └── utils │ │ │ └── test-utils.ts │ │ ├── tsconfig.json │ │ ├── LICENSE │ │ ├── webpack.config.ts │ │ └── package.json ├── i18n │ ├── i18n-webpack-plugin │ │ ├── examples │ │ │ └── my-app │ │ │ │ ├── src │ │ │ │ ├── index.tsx │ │ │ │ ├── i18n │ │ │ │ │ └── zh-cn.js │ │ │ │ ├── types │ │ │ │ │ ├── global.d.ts │ │ │ │ │ └── assets.d.ts │ │ │ │ ├── routes.ts │ │ │ │ ├── pages │ │ │ │ │ ├── home.less │ │ │ │ │ └── home.tsx │ │ │ │ ├── common │ │ │ │ │ └── const.ts │ │ │ │ ├── utils │ │ │ │ │ ├── shared-utils.ts │ │ │ │ │ ├── init-common.ts │ │ │ │ │ └── create-micro-app.tsx │ │ │ │ └── app.tsx │ │ │ │ ├── .prettierrc.json │ │ │ │ ├── public │ │ │ │ ├── index.html │ │ │ │ └── mf-expose-types │ │ │ │ │ └── exposes.d.ts │ │ │ │ ├── .eslintrc.js │ │ │ │ ├── babel.config.js │ │ │ │ ├── scripts │ │ │ │ └── proxy.ts │ │ │ │ ├── .gitignore │ │ │ │ ├── tsconfig.json │ │ │ │ ├── app-config.ts │ │ │ │ └── package.json │ │ ├── src │ │ │ └── index.ts │ │ ├── README.md │ │ ├── tsconfig.json │ │ ├── package.json │ │ └── LICENSE │ ├── i18n-core │ │ ├── __tests__ │ │ │ └── data │ │ │ │ ├── zh-cn-part-2.json │ │ │ │ ├── en-us-part-2.json │ │ │ │ ├── zh-cn.json │ │ │ │ └── en-us.json │ │ ├── README.md │ │ ├── tsconfig.json │ │ ├── src │ │ │ ├── index.ts │ │ │ ├── common.ts │ │ │ ├── create-intl.ts │ │ │ └── types.ts │ │ ├── webpack.config.ts │ │ ├── LICENSE │ │ └── package.json │ └── babel-plugin-i18n │ │ ├── src │ │ ├── index.ts │ │ └── types.ts │ │ ├── README.md │ │ ├── jest.config.ts │ │ ├── tsconfig.json │ │ ├── package.json │ │ └── LICENSE ├── utils │ ├── src │ │ ├── public │ │ │ ├── index.ts │ │ │ └── remove-white-space.ts │ │ ├── index.ts │ │ ├── browser │ │ │ ├── index.ts │ │ │ ├── delay.ts │ │ │ ├── css │ │ │ │ └── common.css │ │ │ └── console-tag.ts │ │ └── node │ │ │ ├── index.ts │ │ │ ├── interop-require-default.ts │ │ │ ├── run-command.ts │ │ │ └── load-ts-config-file.ts │ ├── README.md │ ├── jest.config.ts │ ├── tsconfig.json │ ├── __tests__ │ │ └── public.spec.ts │ ├── package.json │ └── LICENSE ├── eslint-config │ ├── README.md │ ├── tsconfig.json │ ├── package.json │ └── src │ │ └── index.ts ├── github-trending │ ├── jest.config.ts │ ├── README.md │ ├── src │ │ ├── index.ts │ │ ├── types.ts │ │ ├── get-trending-by-more-language.ts │ │ └── get-github-trending.ts │ ├── tsconfig.json │ ├── __tests__ │ │ └── index.spec.ts │ └── package.json ├── proxy │ ├── src │ │ ├── index.ts │ │ ├── types.ts │ │ ├── middlewares │ │ │ ├── proxy-rule-middleware.ts │ │ │ ├── url-middleware.ts │ │ │ └── proxy-pass-middleware.ts │ │ ├── certification-manager.ts │ │ └── utils.ts │ ├── tsconfig.json │ ├── jest.config.ts │ ├── __tests__ │ │ ├── index.spec.ts │ │ ├── utils.spec.ts │ │ └── rule-manager.spec.ts │ ├── example │ │ ├── http │ │ │ ├── http.ts │ │ │ └── proxy.ts │ │ └── websocket │ │ │ ├── proxy.ts │ │ │ └── server.ts │ ├── LICENSE │ ├── package.json │ └── README.md └── hooks │ ├── src │ └── index.ts │ ├── tsconfig.json │ ├── package.json │ └── LICENSE ├── .eslintrc.js ├── stylelint.config.js ├── pnpm-workspace.yaml ├── lage.config.js ├── lerna.json ├── .gitignore ├── README.md ├── tsconfig.json ├── scripts └── add-extension.ts ├── LICENSE └── package.json /packages/assets/playground/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/.gitignore: -------------------------------------------------------------------------------- 1 | !dist 2 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/README.md: -------------------------------------------------------------------------------- 1 | TODO 2 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/.nyc_output/out.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /packages/assets/.gitignore: -------------------------------------------------------------------------------- 1 | !playground/.gitkeep 2 | playground/ 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: '@attachments/eslint-config', 3 | }; 4 | -------------------------------------------------------------------------------- /packages/i18n/i18n-webpack-plugin/examples/my-app/src/index.tsx: -------------------------------------------------------------------------------- 1 | import ('./app'); 2 | 3 | -------------------------------------------------------------------------------- /stylelint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'stylelint-config-standard', 3 | }; 4 | -------------------------------------------------------------------------------- /packages/assets/src/templates/micro-frontend/base-app/src/index.tsx.hbs: -------------------------------------------------------------------------------- 1 | import ('./app'); 2 | 3 | -------------------------------------------------------------------------------- /packages/i18n/i18n-webpack-plugin/src/index.ts: -------------------------------------------------------------------------------- 1 | export { I18nWebpackPlugin } from './plugin'; 2 | -------------------------------------------------------------------------------- /packages/utils/src/public/index.ts: -------------------------------------------------------------------------------- 1 | export { removeWhiteSpace } from './remove-white-space'; 2 | -------------------------------------------------------------------------------- /packages/i18n/i18n-core/__tests__/data/zh-cn-part-2.json: -------------------------------------------------------------------------------- 1 | { 2 | "Yzl_test_Hobby": "爱好: {hobby}" 3 | } 4 | -------------------------------------------------------------------------------- /packages/utils/src/index.ts: -------------------------------------------------------------------------------- 1 | console.warn('[@attachments/utils] 请单独从 esm/browser 或者 esm/node 目录下导入本项目模块!'); 2 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/i18n/*' 3 | - 'packages/monitor/*' 4 | - 'packages/*' 5 | -------------------------------------------------------------------------------- /packages/i18n/i18n-core/__tests__/data/en-us-part-2.json: -------------------------------------------------------------------------------- 1 | { 2 | "Yzl_test_Hobby": "hobby: {hobby}" 3 | } 4 | 5 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/examples/react-app/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/utils/src/browser/index.ts: -------------------------------------------------------------------------------- 1 | export { delay } from './delay'; 2 | export { consoleTag } from './console-tag'; 3 | -------------------------------------------------------------------------------- /packages/assets/src/templates/typescript-project/src/index.ts.hbs: -------------------------------------------------------------------------------- 1 | console.log('The Project is {{package project-name}}'); 2 | -------------------------------------------------------------------------------- /packages/assets/src/index.ts: -------------------------------------------------------------------------------- 1 | export { createAddConfigAction, createAddManyTemplatesAction, launchPlopByConfig } from './utils'; 2 | -------------------------------------------------------------------------------- /packages/i18n/babel-plugin-i18n/src/index.ts: -------------------------------------------------------------------------------- 1 | import { BabelPluginI18n } from './plugin'; 2 | 3 | export default BabelPluginI18n; 4 | -------------------------------------------------------------------------------- /packages/i18n/i18n-core/__tests__/data/zh-cn.json: -------------------------------------------------------------------------------- 1 | { 2 | "Yzl_test_Name": "姓名: {name}", 3 | "Yzl_test_Age": "年龄: {age}" 4 | } 5 | -------------------------------------------------------------------------------- /packages/i18n/i18n-core/__tests__/data/en-us.json: -------------------------------------------------------------------------------- 1 | { 2 | "Yzl_test_Name": "name: {name}", 3 | "Yzl_test_Age": "age: {age}" 4 | } 5 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/global.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | const Monitor = require('./src'); 3 | } 4 | 5 | export {}; 6 | -------------------------------------------------------------------------------- /packages/i18n/i18n-webpack-plugin/examples/my-app/src/i18n/zh-cn.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "Yzl_Test_Name": "1", 3 | "Yzl_Test_Age": "2" 4 | }; 5 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/examples/react-app/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/examples/cross-origin-script-error.js: -------------------------------------------------------------------------------- 1 | console.log('I am cross origin script!'); 2 | throw new Error('hello world!'); 3 | -------------------------------------------------------------------------------- /packages/utils/README.md: -------------------------------------------------------------------------------- 1 | # @attachments/utils 2 | 3 | 实用的前端开发工具库 4 | 5 | ## Usage 6 | 7 | ```bash 8 | yarn add @attachments/utils 9 | ``` 10 | -------------------------------------------------------------------------------- /packages/assets/README.md: -------------------------------------------------------------------------------- 1 | # @attachments/assets 2 | 3 | 常用资源实用工具包,如 css、项目模板等 4 | 5 | ## Usage 6 | 7 | ```bash 8 | yarn add @attachments/assets 9 | ``` 10 | -------------------------------------------------------------------------------- /packages/assets/src/templates/common/prettierrc.json.hbs: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "semi": false, 4 | "singleQuote": true, 5 | "jsxBracketSameLine": true 6 | } 7 | -------------------------------------------------------------------------------- /packages/i18n/i18n-webpack-plugin/examples/my-app/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "semi": false, 4 | "singleQuote": true, 5 | "jsxBracketSameLine": true 6 | } 7 | -------------------------------------------------------------------------------- /packages/i18n/babel-plugin-i18n/README.md: -------------------------------------------------------------------------------- 1 | # @attachments/babel-plugin-i18n 2 | 3 | > TODO: description 4 | 5 | ## Usage 6 | 7 | ``` 8 | // TODO: DEMONSTRATE API 9 | ``` 10 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/src/utils/instance-of.ts: -------------------------------------------------------------------------------- 1 | export const instanceOf = (a: any, b: any) => { 2 | if (b) 3 | return a instanceof b; 4 | 5 | return false; 6 | }; 7 | -------------------------------------------------------------------------------- /packages/i18n/i18n-core/README.md: -------------------------------------------------------------------------------- 1 | # `i18n` 2 | 3 | > TODO: description 4 | 5 | ## Usage 6 | 7 | ``` 8 | const i18n = require('i18n'); 9 | 10 | // TODO: DEMONSTRATE API 11 | ``` 12 | -------------------------------------------------------------------------------- /packages/utils/jest.config.ts: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | collectCoverage: false, 4 | testRegex: '(/__tests__/.*\\.(test|spec))\\.ts$', 5 | testEnvironment: 'node', 6 | }; 7 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultCommandTimeout": 30000, 3 | "componentFolder": "cypress/integration", 4 | "viewportWidth": 1000, 5 | "viewportHeight": 1000 6 | } 7 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/examples/react-app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuzhanglong/attachments/HEAD/packages/monitor/monitor-sdk-browser/examples/react-app/public/favicon.ico -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/examples/react-app/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuzhanglong/attachments/HEAD/packages/monitor/monitor-sdk-browser/examples/react-app/public/logo192.png -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/examples/react-app/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuzhanglong/attachments/HEAD/packages/monitor/monitor-sdk-browser/examples/react-app/public/logo512.png -------------------------------------------------------------------------------- /packages/eslint-config/README.md: -------------------------------------------------------------------------------- 1 | # @attachments/eslint-plugin 2 | 3 | Typescript App @attachments/eslint-plugin 4 | 5 | ## Usage 6 | 7 | ```bash 8 | yarn add @attachments/eslint-config 9 | ``` 10 | -------------------------------------------------------------------------------- /packages/github-trending/jest.config.ts: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | collectCoverage: false, 4 | testRegex: '(/__tests__/.*\\.(test|spec))\\.ts$', 5 | testEnvironment: 'node', 6 | }; 7 | -------------------------------------------------------------------------------- /packages/utils/src/node/index.ts: -------------------------------------------------------------------------------- 1 | export { interopRequireDefault } from './interop-require-default'; 2 | export { loadTsConfigFile } from './load-ts-config-file'; 3 | export { runCommand } from './run-command'; 4 | -------------------------------------------------------------------------------- /lage.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | pipeline: { 3 | build: [ 4 | '^build' 5 | ], 6 | test: [ 7 | 'build' 8 | ], 9 | lint: [] 10 | }, 11 | npmClient: 'pnpm' 12 | }; 13 | -------------------------------------------------------------------------------- /packages/github-trending/README.md: -------------------------------------------------------------------------------- 1 | # @attachments/github-trending 2 | 3 | Typescript App @attachments/github-trending 4 | 5 | ## Usage 6 | 7 | ```bash 8 | yarn add @attachments/github-trending 9 | ``` 10 | -------------------------------------------------------------------------------- /packages/i18n/babel-plugin-i18n/jest.config.ts: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | collectCoverage: false, 4 | testRegex: '(/__tests__/.*\\.(test|spec))\\.ts$', 5 | testEnvironment: 'node', 6 | }; 7 | -------------------------------------------------------------------------------- /packages/assets/src/templates/typescript-project/__tests__/index.spec.ts.hbs: -------------------------------------------------------------------------------- 1 | describe('index test', () => { 2 | test('assert package name', () => { 3 | expect('{{package project-name}}').toBeTruthy(); 4 | }); 5 | }); 6 | -------------------------------------------------------------------------------- /packages/proxy/src/index.ts: -------------------------------------------------------------------------------- 1 | export { ProxyServer } from './proxy-server'; 2 | export { CertificationManager } from './certification-manager'; 3 | 4 | export type { ProxyServerContext, ProxyServerMiddleware } from './types'; 5 | -------------------------------------------------------------------------------- /packages/assets/src/templates/typescript-project/README.md.hbs: -------------------------------------------------------------------------------- 1 | # {{package project-name}} 2 | 3 | Typescript App {{package project-name}} 4 | 5 | ## Usage 6 | 7 | ```bash 8 | yarn add {{package project-name}} 9 | ``` 10 | -------------------------------------------------------------------------------- /packages/assets/src/templates/micro-frontend/micro-app/src/pages/home.less.hbs: -------------------------------------------------------------------------------- 1 | .react-app-home { 2 | .button-wrapper { 3 | margin-bottom: 20px; 4 | } 5 | 6 | .home-content { 7 | font-size: 20px; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } 6 | -------------------------------------------------------------------------------- /packages/i18n/i18n-webpack-plugin/README.md: -------------------------------------------------------------------------------- 1 | # `i18n-webpack-plugin` 2 | 3 | > TODO: description 4 | 5 | ## Usage 6 | 7 | ``` 8 | const i18nWebpackPlugin = require('i18n-webpack-plugin'); 9 | 10 | // TODO: DEMONSTRATE API 11 | ``` 12 | -------------------------------------------------------------------------------- /packages/assets/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "lib", 6 | "rootDir": "./src" 7 | }, 8 | "include": [ 9 | "src/**/*" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /packages/hooks/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * File: index.ts 3 | * Description: 入口 4 | * Created: 2021-08-13 22:41:12 5 | * Author: yuzhanglong 6 | * Email: yuzl1123@163.com 7 | */ 8 | 9 | export { useServerTask } from './use-server-task'; 10 | -------------------------------------------------------------------------------- /packages/hooks/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "lib", 6 | "rootDir": "./src" 7 | }, 8 | "include": [ 9 | "src/**/*" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /packages/i18n/i18n-webpack-plugin/examples/my-app/src/types/global.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | interface Window { 3 | __POWERED_BY_QIANKUN__: boolean; 4 | __REACT_DEVTOOLS_GLOBAL_HOOK__: any; 5 | } 6 | } 7 | 8 | 9 | export {}; 10 | -------------------------------------------------------------------------------- /packages/proxy/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "lib", 6 | "rootDir": "./src" 7 | }, 8 | "include": [ 9 | "src/**/*" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /packages/github-trending/src/index.ts: -------------------------------------------------------------------------------- 1 | export { getTrendingByMoreLanguage } from './get-trending-by-more-language'; 2 | export { getGithubTrending } from './get-github-trending'; 3 | export type { GetGithubTrendingOptions, Repository } from './types'; 4 | -------------------------------------------------------------------------------- /packages/assets/src/templates/micro-frontend/base-app/src/types/global.d.ts.hbs: -------------------------------------------------------------------------------- 1 | declare global { 2 | interface Window { 3 | __POWERED_BY_QIANKUN__: boolean; 4 | __REACT_DEVTOOLS_GLOBAL_HOOK__: any; 5 | } 6 | } 7 | 8 | 9 | export {}; 10 | -------------------------------------------------------------------------------- /packages/assets/src/templates/micro-frontend/micro-app/src/types/global.d.ts.hbs: -------------------------------------------------------------------------------- 1 | declare global { 2 | interface Window { 3 | __POWERED_BY_QIANKUN__: boolean; 4 | __REACT_DEVTOOLS_GLOBAL_HOOK__: any; 5 | } 6 | } 7 | 8 | 9 | export {}; 10 | -------------------------------------------------------------------------------- /packages/i18n/i18n-core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "lib", 6 | "rootDir": "./src" 7 | }, 8 | "include": [ 9 | "src/**/*" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /packages/i18n/i18n-webpack-plugin/examples/my-app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | my-app 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/utils/src/browser/delay.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 延迟 time 时间 3 | * 4 | * @author yuzhanglong 5 | * @date 2021-11-14 00:52:40 6 | */ 7 | export const delay = (time: number) => { 8 | return new Promise(resolve => setTimeout(resolve, time)); 9 | }; 10 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/src/utils/get-first-and-last.ts: -------------------------------------------------------------------------------- 1 | export const getFirstAndLast = (arr: T[]) => { 2 | if (Array.isArray(arr) && arr.length > 0) 3 | return [arr[0], arr[arr.length - 1]]; 4 | 5 | return [undefined, undefined]; 6 | }; 7 | -------------------------------------------------------------------------------- /packages/i18n/babel-plugin-i18n/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "lib", 6 | "rootDir": "./src" 7 | }, 8 | "include": [ 9 | "src/**/*" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /packages/i18n/i18n-webpack-plugin/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "lib", 6 | "rootDir": "./src" 7 | }, 8 | "include": [ 9 | "src/**/*" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/src/fmp/types.ts: -------------------------------------------------------------------------------- 1 | import type { MonitorOptions } from '../types'; 2 | 3 | interface FMPReportData { 4 | fmp: number 5 | } 6 | 7 | export type FMPMonitorOptions = MonitorOptions & { 8 | exact?: boolean 9 | }; 10 | -------------------------------------------------------------------------------- /packages/proxy/jest.config.ts: -------------------------------------------------------------------------------- 1 | const base = require('../../jest.config'); 2 | // @ts-expect-error 3 | const { name } = require('./package.json'); 4 | 5 | module.exports = { 6 | ...base, 7 | name, 8 | displayName: name, 9 | collectCoverage: true, 10 | }; 11 | -------------------------------------------------------------------------------- /packages/utils/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "lib", 6 | "rootDir": "./src", 7 | "jsx": "react" 8 | }, 9 | "include": [ 10 | "src/**/*" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /packages/assets/src/templates/micro-frontend/base-app/src/utils/shared-utils.ts.hbs: -------------------------------------------------------------------------------- 1 | export const add = (a: number, b: number) => { 2 | return a + b; 3 | }; 4 | 5 | export const sayHello = () => { 6 | console.log('hello world!'); 7 | }; 8 | 9 | export default add; 10 | -------------------------------------------------------------------------------- /packages/i18n/i18n-webpack-plugin/examples/my-app/src/routes.ts: -------------------------------------------------------------------------------- 1 | import { RouteConfig } from 'react-router-config'; 2 | import Home from './pages/home'; 3 | 4 | export const routes: RouteConfig[] = [ 5 | { 6 | component: Home, 7 | routes: [], 8 | }, 9 | ]; 10 | -------------------------------------------------------------------------------- /packages/assets/src/templates/micro-frontend/base-app/public/index.html.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{projectName}} 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/assets/src/templates/micro-frontend/micro-app/src/init-common.ts.hbs: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import { injectBaseReactRefresh } from '@mf-lite/core/esm/browser/inject-base-react-refresh'; 3 | 4 | if (process.env.NODE_ENV === 'development') { 5 | injectBaseReactRefresh(); 6 | } 7 | -------------------------------------------------------------------------------- /packages/eslint-config/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "lib", 6 | "rootDir": "./src", 7 | "jsx": "react" 8 | }, 9 | "include": [ 10 | "src/**/*" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/src/common-timing/types.ts: -------------------------------------------------------------------------------- 1 | import type { MonitorOptions } from '../types'; 2 | 3 | interface CommonTimingReportData { 4 | timing: PerformanceTiming 5 | } 6 | 7 | export type CommonTimingMonitorOptions = MonitorOptions; 8 | -------------------------------------------------------------------------------- /packages/assets/src/templates/micro-frontend/base-app/src/routes.ts.hbs: -------------------------------------------------------------------------------- 1 | import { RouteConfig } from 'react-router-config'; 2 | import Home from './pages/home'; 3 | 4 | export const routes: RouteConfig[] = [ 5 | { 6 | component: Home, 7 | routes: [], 8 | }, 9 | ]; 10 | -------------------------------------------------------------------------------- /packages/assets/src/templates/micro-frontend/micro-app/public/index.html.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{projectName}} 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/i18n/i18n-webpack-plugin/examples/my-app/src/pages/home.less: -------------------------------------------------------------------------------- 1 | .base-app-home { 2 | .title { 3 | font-size: 16px; 4 | margin-bottom: 20px; 5 | } 6 | 7 | .content { 8 | border: 1px solid; 9 | padding: 10px; 10 | min-height: 100px; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/src/fetch/types.ts: -------------------------------------------------------------------------------- 1 | import type { MonitorOptions } from '../types'; 2 | import type { XHRReportData } from '../xhr/types'; 3 | 4 | export type FetchReportData = XHRReportData; 5 | 6 | export type FetchMonitorOptions = MonitorOptions; 7 | -------------------------------------------------------------------------------- /packages/assets/src/templates/micro-frontend/base-app/src/pages/home.less.hbs: -------------------------------------------------------------------------------- 1 | .base-app-home { 2 | .title { 3 | font-size: 16px; 4 | margin-bottom: 20px; 5 | } 6 | 7 | .content { 8 | border: 1px solid; 9 | padding: 10px; 10 | min-height: 100px; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/src/assets/types.ts: -------------------------------------------------------------------------------- 1 | import type { MonitorOptions } from '../types'; 2 | 3 | export interface AssetsReportData { 4 | timeStamp: number 5 | performance: Record 6 | } 7 | 8 | export type AssetsMonitorOptions = MonitorOptions; 9 | -------------------------------------------------------------------------------- /packages/i18n/i18n-webpack-plugin/examples/my-app/src/common/const.ts: -------------------------------------------------------------------------------- 1 | export interface MicroAppConfig { 2 | name: string; 3 | url: string; 4 | } 5 | 6 | export const MICRO_APPS: MicroAppConfig[] = [ 7 | { 8 | name: 'micro-app', 9 | url: 'http://localhost:10000/', 10 | }, 11 | ]; 12 | -------------------------------------------------------------------------------- /packages/utils/src/public/remove-white-space.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 移除多余的空格 3 | * 4 | * @author yuzhanglong 5 | * @date 2021-11-23 23:17:02 6 | */ 7 | export const removeWhiteSpace = (data: string) => { 8 | if (!data) { 9 | return data; 10 | } 11 | 12 | return data.replace(/\s/g, ''); 13 | }; 14 | -------------------------------------------------------------------------------- /packages/assets/src/templates/micro-frontend/base-app/src/common/const.ts.hbs: -------------------------------------------------------------------------------- 1 | export interface MicroAppConfig { 2 | name: string; 3 | url: string; 4 | } 5 | 6 | export const MICRO_APPS: MicroAppConfig[] = [ 7 | { 8 | name: 'micro-app', 9 | url: 'http://localhost:10000/', 10 | }, 11 | ]; 12 | -------------------------------------------------------------------------------- /packages/assets/src/templates/micro-frontend/micro-app/src/pages/page-one.tsx.hbs: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface ProfileProps { 4 | 5 | } 6 | 7 | const PageOne: React.FC = () => { 8 | return ( 9 |
Page One
10 | ); 11 | }; 12 | 13 | export default PageOne; 14 | -------------------------------------------------------------------------------- /packages/assets/src/templates/micro-frontend/micro-app/src/pages/page-two.tsx.hbs: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface PageTwoProps { 4 | 5 | } 6 | 7 | const PageTwo: React.FC = () => { 8 | return ( 9 |
Page Two
10 | ); 11 | }; 12 | 13 | export default PageTwo; 14 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/i18n/*", 4 | "packages/*" 5 | ], 6 | "command": { 7 | "publish": { 8 | "message": "chore(release): publish %s", 9 | "registry": "https://registry.npmjs.org" 10 | } 11 | }, 12 | "version": "0.4.0", 13 | "npmClient": "pnpm" 14 | } 15 | -------------------------------------------------------------------------------- /packages/i18n/i18n-core/src/index.ts: -------------------------------------------------------------------------------- 1 | export { IntlPoolExecutor } from './intl-pool-executor'; 2 | export { IntlGroup } from './intl-group'; 3 | export { createIntl } from './create-intl'; 4 | export { LANGUAGE_MAP } from './common'; 5 | export type { GlobalIntl, MessageMap, IntlSources, IIntlGroupExecutor } from './types'; 6 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/cypress/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../../tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": true, 5 | "types": [ 6 | "cypress" 7 | ] 8 | }, 9 | "include": [ 10 | "../../../../node_modules/cypress", 11 | "./**/*.ts" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/examples/react-app/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /packages/assets/src/templates/common/jest.config.js.hbs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | collectCoverage: false, 4 | testRegex: '(src/__tests__/.*\\.(test|spec))\\.ts$', 5 | collectCoverageFrom: [ 6 | 'src/**/*.ts', 7 | ], 8 | coverageDirectory: '/coverage/', 9 | testEnvironment: 'node', 10 | }; 11 | -------------------------------------------------------------------------------- /packages/i18n/babel-plugin-i18n/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { NodePath } from '@babel/core'; 2 | import type { StringLiteral } from '@babel/types'; 3 | 4 | export interface PluginOptions { 5 | intlKeyPrefix: string 6 | include?: RegExp 7 | intlCallee?: string 8 | } 9 | 10 | export type StringLiteralPath = NodePath; 11 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "target": "ES5", 6 | "outDir": "lib", 7 | "rootDir": "./src", 8 | "lib": [ 9 | "es5", 10 | "dom" 11 | ] 12 | }, 13 | "include": [ 14 | "src/**/*" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /packages/utils/__tests__/public.spec.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { removeWhiteSpace } from '../src/public'; 4 | 5 | describe('public tools tests', () => { 6 | test('remove white space', () => { 7 | expect(removeWhiteSpace('hello world \n\n yuzhanglong!')).toStrictEqual('helloworldyuzhanglong!'); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /packages/assets/src/templates/common/eslintrc.js.hbs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['plugin:@attachments/eslint-plugin/recommended'], 4 | plugins: ['@attachments/eslint-plugin'], 5 | ignorePatterns: ['lib', 'esm', 'cjs'], 6 | rules: { 7 | // if you use prettier, open it 8 | 'prettier/prettier': 'off', 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/src/assets-error/types.ts: -------------------------------------------------------------------------------- 1 | import type { MonitorOptions, UrlData } from '../types'; 2 | 3 | export type AssetsErrorReportData = { 4 | tagName: string 5 | timestamp: number 6 | performance: Record 7 | } & UrlData; 8 | 9 | export type AssetsErrorMonitorOptions = MonitorOptions; 10 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/src/js-error/types.ts: -------------------------------------------------------------------------------- 1 | import type { MonitorOptions } from '../types'; 2 | 3 | export interface JsErrorReportData { 4 | timestamp: number 5 | error: { 6 | name: string 7 | message: string 8 | stack: string 9 | } 10 | } 11 | 12 | export type JsErrorMonitorOptions = MonitorOptions; 13 | -------------------------------------------------------------------------------- /packages/i18n/i18n-core/src/common.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * File: common.ts 3 | * Description: 用到的常量 4 | * Created: 2021-07-29 16:30:59 5 | * Author: yuzhanglong 6 | * Email: yuzl1123@163.com 7 | */ 8 | 9 | export enum LANGUAGE_MAP { 10 | en = 'en-US', 11 | zh = 'zh-CN', 12 | } 13 | 14 | export const INTL_KEY_NOT_EXIST_DEFAULT_MESSAGE = 'the message is empty...'; 15 | -------------------------------------------------------------------------------- /packages/i18n/i18n-webpack-plugin/examples/my-app/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['plugin:@attachments/eslint-config/recommended'], 4 | plugins: ['@attachments/eslint-config'], 5 | ignorePatterns: ['lib', 'esm', 'cjs'], 6 | rules: { 7 | // if you use prettier, open it 8 | 'prettier/prettier': 'off', 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /packages/i18n/i18n-webpack-plugin/examples/my-app/src/types/assets.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.module.css'; 2 | declare module '*.module.sass'; 3 | declare module '*.module.scss'; 4 | declare module '*.svg' 5 | declare module '*.png' 6 | declare module '*.jpg' 7 | declare module '*.jpeg' 8 | declare module '*.gif' 9 | declare module '*.bmp' 10 | declare module '*.tiff' 11 | -------------------------------------------------------------------------------- /packages/assets/src/templates/micro-frontend/base-app/src/types/assets.d.ts.hbs: -------------------------------------------------------------------------------- 1 | declare module '*.module.css'; 2 | declare module '*.module.sass'; 3 | declare module '*.module.scss'; 4 | declare module '*.svg' 5 | declare module '*.png' 6 | declare module '*.jpg' 7 | declare module '*.jpeg' 8 | declare module '*.gif' 9 | declare module '*.bmp' 10 | declare module '*.tiff' 11 | -------------------------------------------------------------------------------- /packages/i18n/i18n-webpack-plugin/examples/my-app/src/utils/shared-utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 说 hello 3 | * 4 | * @author yuzhanglong 5 | * @date 2021-11-24 12:05:36 6 | */ 7 | 8 | export const add = (a: number, b: number) => { 9 | return a + b; 10 | }; 11 | 12 | export const sayHello = () => { 13 | console.log('hello world!'); 14 | }; 15 | 16 | export default add; 17 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/src/fid/types.ts: -------------------------------------------------------------------------------- 1 | import type { MonitorOptions } from '../types'; 2 | 3 | export interface FIDEntry extends PerformanceEntry { 4 | processingStart: number 5 | processingEnd: number 6 | } 7 | 8 | export interface FIDReportData { 9 | fid: FIDEntry 10 | } 11 | 12 | export type FIDMonitorOptions = MonitorOptions; 13 | -------------------------------------------------------------------------------- /packages/assets/src/templates/micro-frontend/micro-app/src/types/assets.d.ts.hbs: -------------------------------------------------------------------------------- 1 | declare module '*.module.css'; 2 | declare module '*.module.sass'; 3 | declare module '*.module.scss'; 4 | declare module '*.svg' 5 | declare module '*.png' 6 | declare module '*.jpg' 7 | declare module '*.jpeg' 8 | declare module '*.gif' 9 | declare module '*.bmp' 10 | declare module '*.tiff' 11 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/examples/react-app/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | render(); 7 | const linkElement = screen.getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | import webpackConfig from '../../webpack.config'; 2 | const { startDevServer } = require('@cypress/webpack-dev-server'); 3 | 4 | /** 5 | * @type {Cypress.PluginConfig} 6 | */ 7 | module.exports = (on, config) => { 8 | on('dev-server:start', options => startDevServer({ options, webpackConfig })); 9 | 10 | return config; 11 | }; 12 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/src/mpfid/types.ts: -------------------------------------------------------------------------------- 1 | import type { MonitorOptions } from '../types'; 2 | 3 | export interface MPFIDReportData { 4 | // noinspection SpellCheckingInspection 5 | mpfid: number 6 | } 7 | 8 | export type MPFIDMonitorOptions = MonitorOptions & { 9 | // 在 onload 事件之后多久结束监听,默认为200ms,一般不需要改,增加这个 option 的重要目的是方便单测 10 | timeout?: number 11 | }; 12 | -------------------------------------------------------------------------------- /packages/utils/src/node/interop-require-default.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 处理 require 结果 3 | * 4 | * @date 2021-10-10 22:24:18 5 | */ 6 | 7 | // copied from https://github.com/babel/babel/blob/56044c7851d583d498f919e9546caddf8f80a72f/packages/babel-helpers/src/helpers.js#L558-L562 8 | export const interopRequireDefault = (obj: any) => { 9 | return obj && obj.__esModule ? obj : { default: obj }; 10 | }; 11 | -------------------------------------------------------------------------------- /packages/github-trending/src/types.ts: -------------------------------------------------------------------------------- 1 | export interface Repository { 2 | author: string 3 | name: string 4 | href: string 5 | description: string | null 6 | language: string 7 | stars: number 8 | forks: number 9 | starsInPeriod: number | null 10 | } 11 | 12 | export interface GetGithubTrendingOptions { 13 | period?: string 14 | language?: string 15 | spokenLanguage: string 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | 16 | .idea 17 | 18 | dist 19 | esm 20 | lib 21 | 22 | yarn.lock 23 | package.lock.json 24 | 25 | coverage 26 | 27 | playground 28 | 29 | 30 | .pnpm-debug.log 31 | -------------------------------------------------------------------------------- /packages/i18n/i18n-webpack-plugin/examples/my-app/babel.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = function(apis) { 4 | apis.cache(false); 5 | 6 | return { 7 | plugins: [ 8 | [ 9 | path.resolve(__dirname, "../../../babel-plugin-i18n/lib/index.js"), 10 | { 11 | intlKeyPrefix: "Yzl_Test", 12 | include: "src/i18n" 13 | } 14 | ] 15 | ] 16 | }; 17 | }; 18 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/src/tti/types.ts: -------------------------------------------------------------------------------- 1 | import type { MonitorOptions } from '../types'; 2 | 3 | export interface TTIReportData { 4 | tti: number 5 | } 6 | 7 | export type TTIMonitorOptions = MonitorOptions; 8 | 9 | export interface PatchedXMLHttpRequest extends XMLHttpRequest { 10 | taggedMethod: string 11 | } 12 | 13 | export interface TaskTimeInfo { 14 | startTime: number 15 | endTime: number 16 | } 17 | -------------------------------------------------------------------------------- /packages/i18n/i18n-webpack-plugin/examples/my-app/public/mf-expose-types/exposes.d.ts: -------------------------------------------------------------------------------- 1 | // module name: my_app/shared-utils 2 | 3 | declare module 'my_app/shared-utils' { 4 | /** 5 | * 说 hello 6 | * 7 | * @author yuzhanglong 8 | * @date 2021-11-24 12:05:36 9 | */ 10 | export const add: (a: number, b: number) => number; 11 | export const sayHello: () => void; 12 | export default add; 13 | export { }; 14 | } 15 | 16 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/examples/react-app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/src/constants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * File: common.ts 3 | * Description: 静态常量 4 | * Created: 2021-08-26 11:44:30 5 | * Author: yuzhanglong 6 | * Email: yuzl1123@163.com 7 | */ 8 | 9 | export enum PERFORMANCE_ENTRY_TYPES { 10 | RESOURCE = 'resource', 11 | PAINT = 'paint', 12 | LARGEST_CONTENTFUL_PAINT = 'largest-contentful-paint', 13 | LAYOUT_SHIFT = 'layout-shift', 14 | LONG_TASK = 'longtask', 15 | FIRST_INPUT = 'first-input', 16 | } 17 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/examples/react-app/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/src/utils/assign-keys-between-objects.ts: -------------------------------------------------------------------------------- 1 | import type { BaseObject } from '../types'; 2 | 3 | /** 4 | * 传入多个 key, 将 object1 对应的值赋值到 object2 对应的值 5 | * 6 | * @author yuzhanglong 7 | * @date 2021-08-23 00:47:02 8 | */ 9 | export const assignKeysBetweenObjects = (obj1: BaseObject, obj2: BaseObject, keys: string[]) => { 10 | for (let i = 0; i < keys.length; i += 1) { 11 | const k = keys[i]; 12 | 13 | obj2[k] = obj1[k]; 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /packages/proxy/__tests__/index.spec.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import * as path from 'path'; 3 | import { pathCert, pathCertKey } from '../src/const'; 4 | 5 | describe('test utils', () => { 6 | test('cert path', () => { 7 | const p = path.resolve(process.cwd(), 'packages', 'proxy', 'certificate'); 8 | expect(pathCert).toStrictEqual(path.resolve(p, 'rootCA.pem')); 9 | expect(pathCertKey).toStrictEqual(path.resolve(p, 'rootCA-key.pem')); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 |
6 | 7 |
8 | 9 |
10 | 11 | **I hear and I forget. I see and I remember. I do and I understand.** 12 | 13 |
14 | 15 | ## 介绍 16 | 17 | 平时造的小轮子,用于练手或个人使用。 18 | 19 | ## License 20 | 21 | MIT [@yuzhanglong](https://github.com/yuzhanglong) 22 | -------------------------------------------------------------------------------- /packages/proxy/example/http/http.ts: -------------------------------------------------------------------------------- 1 | import Koa from 'koa'; 2 | import Router from 'koa-router'; 3 | 4 | const app = new Koa(); 5 | const router = new Router(); 6 | 7 | // 指定一个url匹配 8 | router.get('/', async (ctx) => { 9 | ctx.body = 'base!'; 10 | }); 11 | 12 | // 指定一个url匹配 13 | router.get('/hello_world', async (ctx) => { 14 | ctx.body = 'hello world!'; 15 | }); 16 | 17 | app.use(router.routes()); 18 | 19 | app.listen(8001, () => { 20 | console.log('server is running on http://localhost:8001'); 21 | }); 22 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/src/cls/types.ts: -------------------------------------------------------------------------------- 1 | import type { MonitorOptions } from '../types'; 2 | 3 | export interface LayoutShift extends PerformanceEntry { 4 | duration: number 5 | entryType: 'layout-shift' 6 | hadRecentInput: boolean 7 | lastInputTime: number 8 | startTime: number 9 | value: number 10 | name: string 11 | } 12 | 13 | export interface ClsReportData { 14 | clsValue: number 15 | entries: LayoutShift[] 16 | } 17 | 18 | export type ClsMonitorOptions = MonitorOptions; 19 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/src/common-timing/common-timing-monitor.ts: -------------------------------------------------------------------------------- 1 | import { onPageLoad } from '../utils/on-page-load'; 2 | import { EventType } from '../types'; 3 | import type { CommonTimingMonitorOptions } from './types'; 4 | 5 | export const createCommonTimingMonitor = (options: CommonTimingMonitorOptions) => { 6 | onPageLoad(() => { 7 | options.onReport({ 8 | data: { 9 | timing: performance.timing, 10 | }, 11 | eventType: EventType.COMMON_PERFORMANCE_TIMING, 12 | }); 13 | }); 14 | }; 15 | -------------------------------------------------------------------------------- /packages/assets/src/templates/micro-frontend/micro-app/scripts/proxy.ts.hbs: -------------------------------------------------------------------------------- 1 | import { ProxyServer } from '@attachments/proxy'; 2 | 3 | const runProxy = async () => { 4 | const server = new ProxyServer(); 5 | 6 | // micro app 代理 7 | server.addRule( 8 | 'mf-lite-quick-start-micro-app.vercel.app', 9 | { 10 | location: '/', 11 | proxyPass: 'http://localhost:10000', 12 | } 13 | ); 14 | 15 | await server.initServers(); 16 | await server.listen(); 17 | }; 18 | 19 | runProxy().catch(e => { 20 | console.log(e); 21 | }); 22 | -------------------------------------------------------------------------------- /packages/proxy/example/websocket/proxy.ts: -------------------------------------------------------------------------------- 1 | import { ProxyServer } from '../../src'; 2 | 3 | async function runApp() { 4 | const server = new ProxyServer(); 5 | // 当访问 ws://proxy.yuzzl.top 时,代理到 https://localhost:10000 6 | server.addRule('proxy.yuzzl.top', { 7 | location: '/', 8 | // 即使这里写了 http 或者 https 对于 websocket 请求,我们会将其转换成 ws 或者 wss 9 | proxyPass: 'https://localhost:10000', 10 | }); 11 | 12 | await server.initServers(); 13 | await server.listen(); 14 | } 15 | 16 | runApp().catch((e) => { 17 | console.log(e); 18 | }); 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "CommonJS", 5 | "declaration": true, 6 | "moduleResolution": "node", 7 | "resolveJsonModule": true, 8 | "types": [ 9 | "node" 10 | ], 11 | "emitDecoratorMetadata": true, 12 | "sourceMap": true, 13 | "allowJs": true, 14 | "skipLibCheck": true, 15 | "esModuleInterop": true, 16 | "isolatedModules": false, 17 | "experimentalDecorators": true, 18 | "jsx": "preserve", 19 | "downlevelIteration": true 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/examples/react-app/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /packages/github-trending/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "target": "ESNext", 5 | "outDir": "lib", 6 | "rootDir": "./src", 7 | "module": "CommonJS", 8 | "declaration": true, 9 | "moduleResolution": "Node", 10 | "resolveJsonModule": true, 11 | "types": [ 12 | "jest", 13 | "node" 14 | ], 15 | "allowJs": true, 16 | "experimentalDecorators": true, 17 | "emitDecoratorMetadata": true, 18 | "sourceMap": true 19 | }, 20 | "include": [ 21 | "src/**/*" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /packages/i18n/i18n-webpack-plugin/examples/my-app/scripts/proxy.ts: -------------------------------------------------------------------------------- 1 | import { ProxyServer } from '@attachments/proxy'; 2 | 3 | const runProxy = async () => { 4 | const server = new ProxyServer(); 5 | 6 | // 基座代理,将 base-app.vercel.app 的所有请求代理到 http://localhost:8080 7 | server.addRule( 8 | 'base-app.vercel.app', 9 | { 10 | location: '/', 11 | proxyPass: 'http://localhost:8080', 12 | } 13 | ); 14 | 15 | await server.initServers(); 16 | await server.listen(); 17 | }; 18 | 19 | runProxy().catch(e => { 20 | console.log(e); 21 | }); 22 | -------------------------------------------------------------------------------- /packages/assets/src/templates/micro-frontend/base-app/scripts/proxy.ts.hbs: -------------------------------------------------------------------------------- 1 | import { ProxyServer } from '@attachments/proxy'; 2 | 3 | const runProxy = async () => { 4 | const server = new ProxyServer(); 5 | 6 | // 基座代理,将 base-app.vercel.app 的所有请求代理到 http://localhost:8080 7 | server.addRule( 8 | 'base-app.vercel.app', 9 | { 10 | location: '/', 11 | proxyPass: 'http://localhost:8080', 12 | } 13 | ); 14 | 15 | await server.initServers(); 16 | await server.listen(); 17 | }; 18 | 19 | runProxy().catch(e => { 20 | console.log(e); 21 | }); 22 | -------------------------------------------------------------------------------- /packages/hooks/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@attachments/hooks", 3 | "version": "0.4.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "@types/react": "^17.0.17", 8 | "react": "^17.0.2" 9 | }, 10 | "files": [ 11 | "src", 12 | "esm", 13 | "lib" 14 | ], 15 | "scripts": { 16 | "dev:start": "tsc -w", 17 | "build:cjs": "rimraf ./lib && tsc --module commonjs --outDir lib", 18 | "build:esm": "rimraf ./esm && tsc --module ESNext --outDir esm", 19 | "build": "npm-run-all --parallel build:*" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/assets/src/templates/typescript-project/tsconfig.json.hbs: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "target": "ESNext", 5 | "outDir": "lib", 6 | "rootDir": "./src", 7 | "module": "CommonJS", 8 | "declaration": true, 9 | "moduleResolution": "Node", 10 | "resolveJsonModule": true, 11 | "types": [ 12 | "jest", 13 | "node" 14 | ], 15 | "allowJs": true, 16 | "experimentalDecorators": true, 17 | "emitDecoratorMetadata": true, 18 | "sourceMap": true 19 | }, 20 | "include": [ 21 | "src/**/*" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/src/utils/format-plain-headers-string.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 格式化 headers 字符串,返回一个对象 3 | * 4 | * @author yuzhanglong 5 | * @date 2021-08-24 00:26:43 6 | */ 7 | export const formatPlainHeadersString = (headerStr: string) => { 8 | const headers = headerStr.trim().split(/[\r\n]+/); 9 | 10 | const headerMap = {}; 11 | for (let i = 0; i < headers.length; i += 1) { 12 | const line = headers[i]; 13 | const parts = line.split(': '); 14 | const header = parts.shift(); 15 | headerMap[header] = parts.join(': '); 16 | } 17 | return headerMap; 18 | }; 19 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/src/utils/on-page-load.ts: -------------------------------------------------------------------------------- 1 | import { getBrowserWindow, getDocument } from './browser-interfaces'; 2 | 3 | export const onPageLoad = (callback: () => void) => { 4 | const window = getBrowserWindow(); 5 | const document = getDocument(); 6 | if (!window || !document) 7 | return; 8 | 9 | if (document.readyState === 'complete') { 10 | callback(); 11 | return; 12 | } 13 | 14 | window.addEventListener( 15 | 'load', 16 | () => { 17 | setTimeout(() => { 18 | callback(); 19 | }, 0); 20 | }, 21 | false, 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /packages/proxy/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { IncomingMessage, ServerResponse } from 'http'; 2 | import type { Socket } from 'net'; 3 | import type { Middleware } from 'koa-compose'; 4 | 5 | export interface ProxyServerContext { 6 | // for http(s) 7 | incomingRequestData: IncomingMessage 8 | proxyServerResponse?: ServerResponse 9 | protocol?: string 10 | urlInstance?: URL 11 | // for websocket 12 | socketBetweenClientAndProxyServer?: Socket 13 | head?: Buffer 14 | } 15 | 16 | export interface RuleConfig { 17 | location: string 18 | proxyPass: string 19 | } 20 | 21 | export type ProxyServerMiddleware = Middleware; 22 | -------------------------------------------------------------------------------- /packages/assets/src/templates/micro-frontend/base-app/src/pages/home.tsx.hbs: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './home.less'; 3 | import { createMicroApp } from '~src/utils/create-micro-app'; 4 | 5 | interface HomeProps { 6 | 7 | } 8 | 9 | const Home: React.FC = (props) => { 10 | return ( 11 |
12 |
13 | 🎉 This is Base App home page, the micro app will be rendered below! 🎉 14 |
15 |
16 | {createMicroApp('micro-app')()} 17 |
18 |
19 | ); 20 | }; 21 | 22 | export default Home; 23 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/cypress/integration/fid.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createFIDMonitor } from '../../src'; 3 | 4 | describe('test fid monitor', () => { 5 | it('fid', () => { 6 | createFIDMonitor({ 7 | onReport: (e) => { 8 | console.log(e); 9 | }, 10 | }); 11 | document.body.innerHTML = ``; 16 | 17 | setTimeout(() => { 18 | document.getElementById('btn').click(); 19 | }, 1000); 20 | 21 | // TODO:尝试发现 fid 不能直接通过 .click() 捕获,必须是真实用户的点击,能否改进? 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /packages/i18n/i18n-webpack-plugin/examples/my-app/src/utils/init-common.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * File: init-common.ts 3 | * Description: 项目全局初始化 4 | * Created: 2021-09-28 21:51:34 5 | * Author: yuzhanglong 6 | * Email: yuzl1123@163.com 7 | */ 8 | 9 | // patch 基座的 react-refresh 10 | import { consoleTag } from '@attachments/utils/esm/browser/console-tag'; 11 | 12 | consoleTag( 13 | { 14 | key: 'MODE', 15 | value: __MODE__, 16 | }, 17 | { 18 | key: 'VERSION', 19 | value: __APP_VERSION__, 20 | valueColor: '#409eff', 21 | }, 22 | { 23 | key: 'BUILD_TIME', 24 | value: __BUILD_TIME__, 25 | valueColor: '#ea7b27', 26 | }, 27 | ); 28 | -------------------------------------------------------------------------------- /packages/utils/src/node/run-command.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * File: run-command.ts 3 | * Description: 执行命令 4 | * Created: 2021-10-01 19:56:40 5 | * Author: yuzhanglong 6 | * Email: yuzl1123@163.com 7 | */ 8 | 9 | import execa from 'execa'; 10 | 11 | export const runCommand = async (command: string, args?: string[], path?: string): Promise => { 12 | let p = path; 13 | if (!p) 14 | p = process.cwd(); 15 | 16 | if (!args) { 17 | // \s 匹配任何空白字符,包括空格、制表符、换页符 18 | 19 | [command, ...args] = command.split(/\s+/); 20 | } 21 | 22 | return execa(command, args, { 23 | cwd: p, 24 | stdio: 'inherit', 25 | }); 26 | }; 27 | -------------------------------------------------------------------------------- /packages/assets/src/templates/micro-frontend/micro-app/src/routes.ts.hbs: -------------------------------------------------------------------------------- 1 | import { RouteConfig } from 'react-router-config'; 2 | import PageOne from './pages/page-one'; 3 | import Home from './pages/home'; 4 | import PageTwo from './pages/page-two'; 5 | 6 | export const routes: RouteConfig[] = [ 7 | { 8 | component: Home, 9 | routes: [ 10 | { 11 | path: '/', 12 | exact: true, 13 | component: PageOne, 14 | }, 15 | { 16 | path: '/page-one', 17 | component: PageOne, 18 | }, 19 | { 20 | path: '/page-two', 21 | component: PageTwo, 22 | }, 23 | ], 24 | }, 25 | ]; 26 | -------------------------------------------------------------------------------- /packages/assets/src/templates/micro-frontend/base-app/src/utils/init-common.ts.hbs: -------------------------------------------------------------------------------- 1 | /** 2 | * File: init-common.ts 3 | * Description: 项目全局初始化 4 | * Created: 2021-09-28 21:51:34 5 | * Author: yuzhanglong 6 | * Email: yuzl1123@163.com 7 | */ 8 | 9 | // patch 基座的 react-refresh 10 | import { consoleTag } from '@attachments/utils/esm/browser/console-tag'; 11 | 12 | consoleTag( 13 | { 14 | key: 'MODE', 15 | value: __MODE__, 16 | }, 17 | { 18 | key: 'VERSION', 19 | value: __APP_VERSION__, 20 | valueColor: '#409eff', 21 | }, 22 | { 23 | key: 'BUILD_TIME', 24 | value: __BUILD_TIME__, 25 | valueColor: '#ea7b27', 26 | }, 27 | ); 28 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/examples/react-app/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/src/utils/on-page-unload.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 在页面即将销毁时做些什么 3 | * 4 | * @author yuzhanglong 5 | * @date 2021-11-10 00:44:39 6 | */ 7 | import { isFunction } from 'lodash'; 8 | import type { CallBack } from 'src/types'; 9 | import { getBrowserWindow } from './browser-interfaces'; 10 | 11 | export const onPageUnload = (callback: CallBack) => { 12 | const window = getBrowserWindow(); 13 | if (!window || !isFunction(window.addEventListener)) 14 | return; 15 | 16 | ['beforeunload', 'pagehide', 'unload'].forEach((event) => { 17 | window.addEventListener(event, (e) => { 18 | callback(e); 19 | }); 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /packages/assets/src/templates/micro-frontend/micro-app/app-config.ts.hbs: -------------------------------------------------------------------------------- 1 | import { MicroAppConfig } from '@mf-lite/core/lib/node/micro-fe-app-config'; 2 | 3 | const config: MicroAppConfig = { 4 | name: '{{underlinedProjectName projectName}}', 5 | url: 'http://localhost:10000/', 6 | exposes: [], 7 | remotes: [ 8 | { 9 | name: 'base_app', 10 | url: 'http://localhost:8080/', 11 | sharedLibraries: [ 12 | 'react', 13 | 'react-dom', 14 | 'react/jsx-dev-runtime', 15 | 'react-router', 16 | 'react-router-dom', 17 | 'react-router-config' 18 | ] 19 | } 20 | ] 21 | }; 22 | 23 | export default config; 24 | -------------------------------------------------------------------------------- /packages/i18n/i18n-webpack-plugin/examples/my-app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | dist 14 | 15 | 16 | # IDE Config 17 | .idea 18 | 19 | # misc 20 | .DS_Store 21 | .env.local 22 | .env.development.local 23 | .env.test.local 24 | .env.production.local 25 | 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | 30 | # tmp files 31 | public/static 32 | 33 | .cache 34 | 35 | # 本地生成的类型定义文件 36 | src/types/mf-remotes 37 | 38 | # 模块提供者打包生成的类型定义 39 | public/mf-expose/types 40 | -------------------------------------------------------------------------------- /packages/assets/src/templates/micro-frontend/shared/gitignore.hbs: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | dist 14 | 15 | 16 | # IDE Config 17 | .idea 18 | 19 | # misc 20 | .DS_Store 21 | .env.local 22 | .env.development.local 23 | .env.test.local 24 | .env.production.local 25 | 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | 30 | # tmp files 31 | public/static 32 | 33 | .cache 34 | 35 | # 本地生成的类型定义文件 36 | src/types/mf-remotes 37 | 38 | # 模块提供者打包生成的类型定义 39 | public/mf-expose/types 40 | -------------------------------------------------------------------------------- /packages/assets/src/attachments.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { program } from 'commander'; 4 | import { launchPlopByConfig } from './utils'; 5 | 6 | // 版本信息 7 | program.version(`attachments ${require('../package').version}`); 8 | 9 | // serendipity create,创建一个由 serendipity 管理的项目 10 | program 11 | .command('generate [template-name]') 12 | .description('初始化一个项目模板') 13 | .action(async (name: string) => { 14 | if (name === 'ts') 15 | await launchPlopByConfig('ts-project-generator'); 16 | else if (name === 'micro-fe') 17 | await launchPlopByConfig('micro-fe-generator'); 18 | else 19 | console.log('没有这个模板呜呜呜~'); 20 | }); 21 | 22 | program.parse(process.argv); 23 | -------------------------------------------------------------------------------- /packages/github-trending/src/get-trending-by-more-language.ts: -------------------------------------------------------------------------------- 1 | import type { GetGithubTrendingOptions, Repository } from './types'; 2 | import { getGithubTrending } from './get-github-trending'; 3 | 4 | /** 5 | * 获取多个语言的 GitHub 趋势 6 | * 7 | * @author yuzhanglong 8 | * @date 2022-01-06 21:17:53 9 | */ 10 | export const getTrendingByMoreLanguage = async (languages: string[], options: GetGithubTrendingOptions) => { 11 | const res: Record = {}; 12 | await Promise.all( 13 | languages.map(async (language) => { 14 | res[language] = await getGithubTrending({ 15 | ...options, 16 | language, 17 | }); 18 | }), 19 | ); 20 | return res; 21 | }; 22 | -------------------------------------------------------------------------------- /packages/proxy/example/http/proxy.ts: -------------------------------------------------------------------------------- 1 | import { ProxyServer } from '../../src'; 2 | 3 | async function runApp() { 4 | const server = new ProxyServer(); 5 | // 当访问 proxy.yuzzl.top 时,代理到 http://localhost:8001 6 | // 当访问 proxy.yuzzl.top/hello/world/xxx 时,代理到 http://localhost:8001/hello_world/xxx 7 | server.addRule( 8 | 'proxy.yuzzl.top', 9 | { 10 | location: '/', 11 | proxyPass: 'http://localhost:8001', 12 | }, 13 | { 14 | location: '/hello/world', 15 | proxyPass: 'http://localhost:8001/hello_world', 16 | }, 17 | ); 18 | 19 | await server.initServers(); 20 | await server.listen(); 21 | } 22 | 23 | runApp().catch((e) => { 24 | console.log(e); 25 | }); 26 | -------------------------------------------------------------------------------- /scripts/add-extension.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as fs from 'fs-extra'; 3 | 4 | /** 5 | * 批量添加后缀名 6 | * 7 | * @author YuZhanglong 8 | */ 9 | export const addExtension = (baseDir: string, extension: string) => { 10 | const res = fs.readdirSync(baseDir); 11 | for (const re of res) { 12 | const fileOrDir = path.resolve(baseDir, re); 13 | if (fs.lstatSync(fileOrDir).isDirectory()) 14 | addExtension(fileOrDir, extension); 15 | else if (!re.endsWith('.hbs')) 16 | fs.renameSync(fileOrDir, path.resolve(baseDir, `${re}.${extension}`)); 17 | } 18 | }; 19 | 20 | addExtension(path.resolve(process.cwd(), 'packages/assets/src/templates'), 'hbs'); 21 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/examples/react-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/src/utils/patch-method.ts: -------------------------------------------------------------------------------- 1 | import type { MethodKeys } from '../types'; 2 | 3 | /** 4 | * patch 一个方法(方法劫持) 5 | * 6 | * @author yuzhanglong 7 | * @date 2021-08-22 20:41:45 8 | * @param obj 被劫持的对象 9 | * @param key 需要劫持的 key 10 | * @param patchFn 劫持回调,劫持回调返回一个函数(即覆盖后的函数),其中: 11 | * 回调的第一个参数为将要被覆盖的对象 12 | * 第二个参数为携带的参数(可选) 13 | * @return function 返回一个函数,当这个函数被调用后,劫持工作将被执行 14 | */ 15 | export const patchMethod = , P extends any[]>( 16 | obj: O, 17 | key: K, 18 | patchFn: (origin: O[K], ...params: P) => O[K] & Function, 19 | ) => { 20 | return (...params: P) => { 21 | obj[key] = patchFn(obj[key], ...params); 22 | }; 23 | }; 24 | -------------------------------------------------------------------------------- /packages/i18n/i18n-core/src/create-intl.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * File: create-intl.ts 3 | * Description: create-intl 函数,再一次封装 intl-pool-executor 以方便调用 4 | * Created: 2021-07-31 10:40:59 5 | * Author: yuzhanglong 6 | * Email: yuzl1123@163.com 7 | */ 8 | import { IntlPoolExecutor } from './intl-pool-executor'; 9 | import type { GlobalIntl } from './types'; 10 | 11 | export function createIntl() { 12 | const executor = new IntlPoolExecutor(); 13 | 14 | const i: GlobalIntl = executor.getMessage.bind(executor); 15 | 16 | i.setLocal = executor.setLocal.bind(executor); 17 | 18 | i.register = executor.register.bind(executor); 19 | 20 | i.unregister = executor.unregister.bind(executor); 21 | 22 | return i; 23 | } 24 | -------------------------------------------------------------------------------- /packages/i18n/i18n-webpack-plugin/examples/my-app/src/pages/home.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./home.less"; 3 | 4 | const data = import("../i18n/zh-cn"); 5 | 6 | interface HomeProps { 7 | 8 | } 9 | 10 | const Home: React.FC = (props) => { 11 | console.log(data); 12 | return ( 13 |
14 |
15 | 🎉 This is Base App home page, the micro app will be rendered below! 🎉 16 |
17 |
18 | {/*// @ts-ignore*/} 19 | {intl("Yzl_Test_Ok!!", { 20 | a: 11, 21 | b: 2 22 | })} 23 |
24 |
25 | ); 26 | }; 27 | 28 | export default Home; 29 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/examples/js-error-cross-origin.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | js-error-cross-origin 6 | 8 | 15 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /packages/assets/src/templates/micro-frontend/base-app/app-config.ts.hbs: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import { MicroAppConfig } from '@mf-lite/core/lib/node/micro-fe-app-config'; 3 | import { sourcePath } from '@mf-lite/core/lib/common/paths'; 4 | 5 | const config: MicroAppConfig = { 6 | remotes: [], 7 | name: '{{underlinedProjectName projectName}}', 8 | url: 'http://localhost:8080/', 9 | exposes: [ 10 | 'react', 11 | 'react-dom', 12 | 'react-router', 13 | 'react-router-dom', 14 | 'react-router-config', 15 | 'react/jsx-dev-runtime', 16 | { 17 | name: 'shared-utils', 18 | path: path.resolve(sourcePath, 'utils', 'shared-utils.ts'), 19 | type: 'module', 20 | }, 21 | ], 22 | }; 23 | 24 | export default config; 25 | -------------------------------------------------------------------------------- /packages/i18n/i18n-webpack-plugin/examples/my-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES5", 4 | "module": "CommonJS", 5 | "baseUrl": "src", 6 | "lib": [ 7 | "dom", 8 | "dom.iterable", 9 | "esnext" 10 | ], 11 | "paths": { 12 | "~src/*": [ 13 | "./*" 14 | ] 15 | }, 16 | "types": [ 17 | "node" 18 | ], 19 | "allowJs": true, 20 | "skipLibCheck": true, 21 | "esModuleInterop": true, 22 | "moduleResolution": "node", 23 | "resolveJsonModule": true, 24 | "isolatedModules": false, 25 | "noEmit": true, 26 | "experimentalDecorators": true, 27 | "jsx": "preserve", 28 | "downlevelIteration": true 29 | }, 30 | "include": [ 31 | "src" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /packages/assets/src/templates/micro-frontend/shared/tsconfig.json.hbs: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES5", 4 | "module": "CommonJS", 5 | "baseUrl": "src", 6 | "lib": [ 7 | "dom", 8 | "dom.iterable", 9 | "esnext" 10 | ], 11 | "paths": { 12 | "~src/*": [ 13 | "./*" 14 | ] 15 | }, 16 | "types": [ 17 | "node" 18 | ], 19 | "allowJs": true, 20 | "skipLibCheck": true, 21 | "esModuleInterop": true, 22 | "moduleResolution": "node", 23 | "resolveJsonModule": true, 24 | "isolatedModules": false, 25 | "noEmit": true, 26 | "experimentalDecorators": true, 27 | "jsx": "preserve", 28 | "downlevelIteration": true 29 | }, 30 | "include": [ 31 | "src" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/examples/react-app/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/examples/react-app/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | setTimeout(() => { 8 | testThrowError(); 9 | }) 10 | const testThrowError = async () => { 11 | throw new Error("😭😭 React App 的入口文件产生了一个错误 😭😭"); 12 | }; 13 | 14 | ReactDOM.render( 15 | 16 | 17 | , 18 | document.getElementById('root') 19 | ); 20 | 21 | // If you want to start measuring performance in your app, pass a function 22 | // to log results (for example: reportWebVitals(console.log)) 23 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 24 | reportWebVitals(); 25 | -------------------------------------------------------------------------------- /packages/utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@attachments/utils", 3 | "version": "0.4.0", 4 | "license": "MIT", 5 | "description": "useful front-end development tool library", 6 | "author": "yuzhanglong ", 7 | "homepage": "https://github.com/yuzhanglong/attachments", 8 | "main": "lib/index.js", 9 | "module": "esm/index.js", 10 | "files": [ 11 | "src", 12 | "esm", 13 | "lib", 14 | "dist" 15 | ], 16 | "scripts": { 17 | "dev:start": "tsc -w", 18 | "build": "npm-run-all --parallel build:*", 19 | "build:cjs": "rimraf ./lib && tsc --module commonjs --outDir lib", 20 | "build:esm": "rimraf ./esm && tsc --module ESNext --outDir esm" 21 | }, 22 | "dependencies": { 23 | "execa": "^5.1.1", 24 | "ts-node": "^10.2.1" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/assets/src/templates/micro-frontend/micro-app/src/app.tsx.hbs: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { BrowserRouter } from 'react-router-dom'; 4 | import { renderRoutes } from 'react-router-config'; 5 | import './init-common'; 6 | import { routes } from './routes'; 7 | 8 | 9 | const App: React.FC = () => { 10 | return ( 11 | 12 | {renderRoutes(routes)} 13 | 14 | ); 15 | }; 16 | 17 | export const render = () => { 18 | const el = document.getElementById('{{projectName}}'); 19 | if (el) { 20 | ReactDOM.render(, el); 21 | } 22 | }; 23 | 24 | export const destroy = () => { 25 | const el = document.getElementById('{{projectName}}'); 26 | if (el) { 27 | ReactDOM.unmountComponentAtNode(el); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /packages/utils/src/browser/css/common.css: -------------------------------------------------------------------------------- 1 | .g-flexbox { 2 | display: flex; 3 | } 4 | 5 | .g-flexbox-column { 6 | display: flex; 7 | flex-direction: column; 8 | } 9 | 10 | .g-full { 11 | width: 100%; 12 | height: 100%; 13 | } 14 | 15 | .g-flex { 16 | flex: 1; 17 | } 18 | 19 | .g-flex-none { 20 | flex: none; 21 | } 22 | 23 | .g-justify-center { 24 | justify-content: center; 25 | } 26 | 27 | .g-justify-space-between { 28 | justify-content: space-between; 29 | } 30 | 31 | .g-align-center { 32 | align-items: center; 33 | } 34 | 35 | .g-flex-center { 36 | display: flex; 37 | justify-content: center; 38 | align-items: center; 39 | } 40 | 41 | .g-flex-right { 42 | display: flex; 43 | justify-content: flex-end; 44 | align-items: center; 45 | } 46 | 47 | .g-scroll { 48 | overflow: auto; 49 | } 50 | -------------------------------------------------------------------------------- /packages/proxy/src/middlewares/proxy-rule-middleware.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * File: proxy-rule-middleware.ts 3 | * Description: 代理规则中间件 4 | * Created: 2021-08-12 10:57:34 5 | * Author: yuzhanglong 6 | * Email: yuzl1123@163.com 7 | */ 8 | 9 | import type { ProxyServerMiddleware } from '../types'; 10 | import type { RuleManager } from '../rule-manager'; 11 | 12 | export function createProxyRuleMiddleware(ruleManager: RuleManager): ProxyServerMiddleware { 13 | return async (context, next) => { 14 | const { urlInstance } = context; 15 | 16 | const resUrl = ruleManager.getProxyPassUrl(urlInstance); 17 | 18 | if (resUrl) { 19 | console.log(`[@attachments/proxy] proxy-make-effect: ${urlInstance.toString()} => ${resUrl.toString()}`); 20 | context.urlInstance = resUrl; 21 | } 22 | return next(); 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /packages/utils/src/browser/console-tag.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * File: console-tag.ts 3 | * Description: 打印在浏览器终端的小标签,用来展示版本及构建信息 4 | * Created: 2021-09-28 01:28:44 5 | * Author: yuzhanglong 6 | * Email: yuzl1123@163.com 7 | */ 8 | interface ConsoleTagConfig { 9 | key: string 10 | value: string 11 | keyColor?: string 12 | valueColor?: string 13 | } 14 | 15 | export const consoleTag = (...config: ConsoleTagConfig[]) => { 16 | for (const { keyColor, valueColor, value, key } of config) { 17 | const data = [ 18 | `%c ${key} %c ${value} `, 19 | `padding: 1px; border-radius: 3px 0 0 3px; color: #fff; background: ${keyColor || '#606060'};`, 20 | `padding: 1px; border-radius: 0 3px 3px 0; color: #fff; background: ${valueColor || '#42c02e'}`, 21 | ]; 22 | 23 | console.log(data[0], data[1], data[2]); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/cypress/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands'; 18 | // import '@cypress/code-coverage/support'; 19 | 20 | // Alternatively you can use CommonJS syntax: 21 | // require('./commands') 22 | -------------------------------------------------------------------------------- /packages/github-trending/__tests__/index.spec.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { getGithubTrending } from '../src'; 3 | import { getTrendingByMoreLanguage } from '../src/get-trending-by-more-language'; 4 | 5 | describe('index test', () => { 6 | test('get trending', async () => { 7 | const res = await getGithubTrending({ 8 | language: 'javascript', 9 | spokenLanguage: 'zh', 10 | period: 'daily', 11 | }); 12 | expect(res.length).toBeTruthy(); 13 | }); 14 | 15 | test('get trending by more languages', async () => { 16 | const res = await getTrendingByMoreLanguage(['javascript', 'java'], { 17 | language: 'javascript', 18 | spokenLanguage: 'zh', 19 | period: 'daily', 20 | }); 21 | expect(res.javascript.length).toBeTruthy(); 22 | expect(res.java.length).toBeTruthy(); 23 | }, 20000); 24 | }); 25 | -------------------------------------------------------------------------------- /packages/assets/src/templates/micro-frontend/micro-app/src/index.tsx.hbs: -------------------------------------------------------------------------------- 1 | const App = import('./app'); 2 | 3 | const render = () => { 4 | App.then(res => res.render()); 5 | }; 6 | 7 | if (!window.__POWERED_BY_QIANKUN__) { 8 | render(); 9 | } 10 | 11 | /** 12 | * bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。 13 | * 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。 14 | */ 15 | export async function bootstrap() { 16 | return 0; 17 | } 18 | 19 | 20 | /** 21 | * 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法 22 | */ 23 | export async function mount() { 24 | render(); 25 | } 26 | 27 | /** 28 | * 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例 29 | */ 30 | export async function unmount() { 31 | App.then(res => res.destroy()); 32 | } 33 | 34 | /** 35 | * 可选生命周期钩子,仅使用 loadMicroApp 方式加载微应用时生效 36 | */ 37 | export async function update() { 38 | render(); 39 | } 40 | -------------------------------------------------------------------------------- /packages/i18n/babel-plugin-i18n/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@attachments/babel-plugin-i18n", 3 | "version": "0.4.0", 4 | "description": "> TODO: description", 5 | "author": "yuzhanglong ", 6 | "homepage": "", 7 | "license": "MIT", 8 | "main": "lib/index.js", 9 | "module": "esm/index.js", 10 | "files": [ 11 | "src", 12 | "esm", 13 | "lib" 14 | ], 15 | "scripts": { 16 | "dev:start": "tsc -w", 17 | "build:cjs": "rimraf ./lib && tsc --module commonjs --outDir lib", 18 | "build:esm": "rimraf ./esm && tsc --module ESNext --outDir esm", 19 | "build": "npm-run-all --parallel build:*" 20 | }, 21 | "devDependencies": { 22 | "@babel/core": "^7.14.8", 23 | "@babel/preset-typescript": "^7.15.0" 24 | }, 25 | "dependencies": { 26 | "@attachments/utils": "^0.4.0", 27 | "@babel/types": "^7.20.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/eslint-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@attachments/eslint-config", 3 | "version": "0.4.0", 4 | "description": "useful eslint config", 5 | "author": "yuzhanglong ", 6 | "homepage": "", 7 | "license": "MIT", 8 | "main": "lib/index.js", 9 | "module": "esm/index.js", 10 | "files": [ 11 | "src", 12 | "esm", 13 | "lib" 14 | ], 15 | "scripts": { 16 | "dev:start": "tsc -w", 17 | "build:cjs": "rimraf ./lib && tsc --module commonjs --outDir lib", 18 | "build:esm": "rimraf ./esm && tsc --module ESNext --outDir esm", 19 | "build": "npm-run-all --parallel build:*", 20 | "lint": "eslint --ext .ts --max-warnings 0 ./src" 21 | }, 22 | "peerDependencies": { 23 | "eslint": "^7 || ^8" 24 | }, 25 | "dependencies": { 26 | "@antfu/eslint-config": "^0.38.4", 27 | "@antfu/eslint-config-react": "^0.38.4" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/eslint-config/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { Linter } from 'eslint'; 2 | 3 | module.exports = { 4 | extends: [ 5 | '@antfu', 6 | '@antfu/eslint-config-react', 7 | ], 8 | ignorePatterns: ['lib', 'esm', 'cjs'], 9 | rules: { 10 | '@typescript-eslint/semi': ['error', 'always'], 11 | '@typescript-eslint/ban-ts-comment': 'off', 12 | '@typescript-eslint/comma-dangle': 'off', 13 | 'react/jsx-tag-spacing': 'error', 14 | 'curly': 'off', 15 | '@typescript-eslint/no-var-requires': 'off', 16 | 'no-console': 'warn', 17 | '@typescript-eslint/brace-style': 'off', 18 | '@typescript-eslint/member-delimiter-style': ['error', { 19 | multiline: { 20 | delimiter: 'semi', 21 | requireLast: true 22 | }, 23 | singleline: { 24 | delimiter: 'semi', 25 | requireLast: false 26 | } 27 | }], 28 | }, 29 | } as Linter.Config; 30 | -------------------------------------------------------------------------------- /packages/i18n/i18n-core/src/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * File: common.ts 3 | * Description: intl package 类型定义 4 | * Created: 2021-07-29 16:29:55 5 | * Author: yuzhanglong 6 | * Email: yuzl1123@163.com 7 | */ 8 | 9 | export type MessageMap = Record; 10 | 11 | export type IntlSources = Record MessageMap) | (() => Promise)>; 12 | 13 | export interface IIntlGroupExecutor { 14 | // 修改当前语言 15 | setLocal: (local: string) => Promise 16 | 17 | // 激活某个文案组 18 | activate: (name: string) => Promise 19 | 20 | // 取消激活某个文案组 21 | deactivate: (name: string) => void 22 | 23 | // 注册一个 intl group 24 | register: (name: string, sources: IntlSources) => IIntlGroupExecutor 25 | 26 | // 移除一个 intl group 27 | unregister: (name: string) => IIntlGroupExecutor 28 | } 29 | 30 | export type GlobalIntl = IIntlGroupExecutor & { 31 | (key: string, args: any): string 32 | }; 33 | -------------------------------------------------------------------------------- /packages/github-trending/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@attachments/github-trending", 3 | "version": "0.4.0", 4 | "description": "github trending API wrapper", 5 | "author": "yuzhanglong ", 6 | "homepage": "", 7 | "license": "MIT", 8 | "main": "lib/index.js", 9 | "module": "esm/index.js", 10 | "files": [ 11 | "src", 12 | "esm", 13 | "lib" 14 | ], 15 | "scripts": { 16 | "dev:start": "tsc -w", 17 | "build:cjs": "rimraf ./lib && tsc --module commonjs --outDir lib", 18 | "build:esm": "rimraf ./esm && tsc --module ESNext --outDir esm", 19 | "build": "npm-run-all --parallel build:*", 20 | "lint": "eslint --ext js,jsx,ts,tsx src", 21 | "test": "jest" 22 | }, 23 | "devDependencies": { 24 | "npm-run-all": "^4.1.5", 25 | "rimraf": "^3.0.2" 26 | }, 27 | "dependencies": { 28 | "axios": "^0.24.0", 29 | "cheerio": "^1.0.0-rc.10" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/i18n/i18n-webpack-plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@attachments/i18n-webpack-plugin", 3 | "version": "0.4.0", 4 | "description": "> TODO: description", 5 | "author": "yuzhanglong ", 6 | "homepage": "", 7 | "license": "MIT", 8 | "main": "lib/index.js", 9 | "module": "esm/index.js", 10 | "files": [ 11 | "src", 12 | "esm", 13 | "lib" 14 | ], 15 | "scripts": { 16 | "dev:start": "tsc -w", 17 | "build": "npm-run-all --parallel build:*", 18 | "build:cjs": "rimraf ./lib && tsc --module commonjs --outDir lib", 19 | "build:esm": "rimraf ./esm && tsc --module ESNext --outDir esm" 20 | }, 21 | "devDependencies": { 22 | "@attachments/babel-plugin-i18n": "^0.4.0", 23 | "webpack": "^5.61.0", 24 | "webpack-cli": "^4.8.0" 25 | }, 26 | "peerDependencies": { 27 | "@attachments/babel-plugin-i18n": "*", 28 | "webpack": "^5 || ^4" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/i18n/i18n-webpack-plugin/examples/my-app/app-config.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import { MicroAppConfig } from "@mf-lite/core/lib/node/micro-fe-app-config"; 3 | import { sourcePath } from "@mf-lite/core/lib/common/paths"; 4 | import { I18nWebpackPlugin } from "../../lib"; 5 | 6 | const config: MicroAppConfig = { 7 | remotes: [], 8 | name: "my_app", 9 | url: "http://localhost:8080/", 10 | exposes: [ 11 | "react", 12 | "react-dom", 13 | "react-router", 14 | "react-router-dom", 15 | "react-router-config", 16 | "react/jsx-dev-runtime", 17 | { 18 | name: "shared-utils", 19 | path: path.resolve(sourcePath, "utils", "shared-utils.ts"), 20 | type: "module" 21 | } 22 | ], 23 | webpackConfig: { 24 | devtool: "source-map", 25 | plugins: [ 26 | // @ts-ignore 27 | new I18nWebpackPlugin() 28 | ] 29 | } 30 | }; 31 | 32 | export default config; 33 | -------------------------------------------------------------------------------- /packages/assets/src/templates/micro-frontend/base-app/src/app.tsx.hbs: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom'; 2 | import { BrowserRouter } from 'react-router-dom'; 3 | import React, { Suspense } from 'react'; 4 | import { renderRoutes } from 'react-router-config'; 5 | import { routes } from '~src/routes'; 6 | import './utils/init-common'; 7 | import '@attachments/utils/src/browser/css/common.css'; 8 | 9 | const App = () => { 10 | return ( 11 | 12 | 13 | {renderRoutes(routes)} 14 | 15 | 16 | ); 17 | }; 18 | 19 | 20 | const reactRenderer = () => { 21 | ReactDOM.render(( 22 | 23 | 24 | 25 | ), document.getElementById('{{projectName}}')); 26 | }; 27 | 28 | const beforeAppStart = async () => { 29 | return true; 30 | }; 31 | 32 | beforeAppStart() 33 | .then(() => { 34 | reactRenderer(); 35 | }); 36 | 37 | 38 | -------------------------------------------------------------------------------- /packages/i18n/i18n-core/webpack.config.ts: -------------------------------------------------------------------------------- 1 | import type * as webpack from 'webpack'; 2 | 3 | const env = process.env.NODE_ENV; 4 | const isDev = env === 'development'; 5 | 6 | const config: webpack.Configuration = { 7 | entry: './src/index.ts', 8 | mode: isDev ? 'development' : 'production', 9 | output: { 10 | library: { 11 | name: 'intl', 12 | type: 'umd', 13 | }, 14 | filename: `intl.${isDev ? 'development' : 'production'}.js`, 15 | }, 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.ts$/, 20 | exclude: /node_modules/, 21 | use: { 22 | loader: 'babel-loader', 23 | options: { 24 | presets: ['@babel/preset-env', '@babel/preset-typescript'], 25 | plugins: [['@babel/plugin-transform-runtime']], 26 | }, 27 | }, 28 | }, 29 | ], 30 | }, 31 | resolve: { 32 | extensions: ['.ts', '.tsx', '.js', '.jsx'], 33 | }, 34 | }; 35 | 36 | export default config; 37 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add('login', (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This will overwrite an existing command -- 25 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) 26 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * File: index.ts 3 | * Description: entry 4 | * Created: 2021-08-23 22:06:48 5 | * Author: yuzhanglong 6 | * Email: yuzl1123@163.com 7 | */ 8 | 9 | export { getDomLayoutScore } from './utils/get-dom-layout-score'; 10 | export { createFMPMonitor } from './fmp/fmp-monitor'; 11 | export { createClsMonitor } from './cls/cls-monitor'; 12 | export { createXHRMonitor } from './xhr/xhr-monitor'; 13 | export { createJsErrorMonitor } from './js-error/js-error-monitor'; 14 | export { createAssetsMonitor } from './assets/assets-monitor'; 15 | export { createAssetsErrorMonitor } from './assets-error/assets-error-monitor'; 16 | export { createPaintMonitor } from './paint/paint-monitor'; 17 | export { createTTIMonitor } from './tti/tti-monitor'; 18 | export { createMPFIDMonitor } from './mpfid/mpfid-monitor'; 19 | export { createFIDMonitor } from './fid/fid-monitor'; 20 | export { createCommonTimingMonitor } from './common-timing/common-timing-monitor'; 21 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/src/utils/performance-entry.ts: -------------------------------------------------------------------------------- 1 | import { isFunction } from 'lodash'; 2 | import { getPerformance } from './browser-interfaces'; 3 | 4 | /** 5 | * 根据 entry 名称获取 entry,如果浏览器不兼容,返回一个空数组 6 | * 7 | * @author yuzhanglong 8 | * @date 2021-09-12 11:12:21 9 | * @param name entry 名称 10 | */ 11 | export const getPerformanceEntriesByName = (name: string) => { 12 | const performance = getPerformance(); 13 | 14 | if (performance && isFunction(performance.getEntriesByName)) 15 | return performance.getEntriesByName(name); 16 | 17 | return []; 18 | }; 19 | 20 | /** 21 | * 根据 entry 类型获取 entry,如果浏览器不兼容,返回一个空数组 22 | * 23 | * @author yuzhanglong 24 | * @date 2021-09-12 11:12:21 25 | * @param type entry 名称 26 | */ 27 | export const getPerformanceEntriesByType = (type: string) => { 28 | const performance = getPerformance(); 29 | if (performance && isFunction(performance.getEntriesByType)) 30 | return performance.getEntriesByType(type); 31 | 32 | return []; 33 | }; 34 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/examples/react-app/src/App.tsx: -------------------------------------------------------------------------------- 1 | // noinspection JSIgnoredPromiseFromCall 2 | 3 | import React, { useEffect } from "react"; 4 | import logo from "./logo.svg"; 5 | import "./App.css"; 6 | 7 | function App() { 8 | useEffect(() => { 9 | setTimeout(() => { 10 | testThrowError(); 11 | }) 12 | }); 13 | 14 | const testThrowError = async () => { 15 | throw new Error("😭😭 React App 的 组件又产生了一个错误 😭😭"); 16 | }; 17 | return ( 18 |
19 |
20 | logo 21 |

22 | Edit src/App.tsx and save to reload. 23 |

24 | 30 | Learn React 31 | 32 |
33 |
34 | ); 35 | } 36 | 37 | export default App; 38 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/src/paint/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * File: paint.ts 3 | * Description: 绘制相关类型定义 4 | * Created: 2021-08-26 22:22:32 5 | * Author: yuzhanglong 6 | * Email: yuzl1123@163.com 7 | */ 8 | import type { MonitorOptions } from '../types'; 9 | 10 | export interface PaintReportData { 11 | // FP 12 | firstPaint: Record 13 | // FCP 14 | firstContentfulPaint: Record 15 | // 汇报时间戳 16 | timeStamp: number 17 | } 18 | 19 | export interface LargestContentfulPaint { 20 | duration: number 21 | element: Element 22 | entryType: string 23 | id: string 24 | loadTime: number 25 | name: string 26 | renderTime: number 27 | size: number 28 | startTime: number 29 | url: string 30 | } 31 | 32 | export interface LargestContentfulPaintReportData { 33 | // LCP 34 | largestContentfulPaint: LargestContentfulPaint 35 | // 汇报时间戳 36 | timeStamp: number 37 | } 38 | 39 | export type PaintMonitorOptions = MonitorOptions; 40 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/src/utils/get-url-data.ts: -------------------------------------------------------------------------------- 1 | import type { UrlData } from '../types'; 2 | import { getBrowserWindow } from './browser-interfaces'; 3 | import { assignKeysBetweenObjects } from './assign-keys-between-objects'; 4 | 5 | export const getUrlData = (url: string): UrlData => { 6 | // 支持 url 7 | const keys: string[] = ['hash', 'host', 'hostname', 'href', 'origin', 'pathname', 'port', 'protocol', 'search']; 8 | 9 | const res = { 10 | url, 11 | hash: '', 12 | host: '', 13 | hostname: '', 14 | href: '', 15 | origin: '', 16 | pathname: '', 17 | port: '', 18 | protocol: '', 19 | search: '', 20 | }; 21 | 22 | const w = getBrowserWindow(); 23 | 24 | // 这里不推荐用浏览器内置的 URL 实例,而是利用原生 a 标签的特性来实现 25 | // 因为像下面 img 标签这样的错误,拿到的 "url" 是不规范的,使用 new URL() 会抛出异常 26 | // 27 | if (w && w.document) { 28 | const a = document.createElement('a'); 29 | a.href = url; 30 | assignKeysBetweenObjects(a, res, keys); 31 | } 32 | return res; 33 | }; 34 | -------------------------------------------------------------------------------- /packages/i18n/i18n-webpack-plugin/examples/my-app/src/app.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from "react-dom"; 2 | import { BrowserRouter } from "react-router-dom"; 3 | import React, { Suspense } from "react"; 4 | import { renderRoutes } from "react-router-config"; 5 | import { routes } from "~src/routes"; 6 | import "./utils/init-common"; 7 | import "@attachments/utils/src/browser/css/common.css"; 8 | 9 | // @ts-ignore 10 | window.intl = (a, b) => { 11 | return a + JSON.stringify(b); 12 | }; 13 | 14 | const App = () => { 15 | return ( 16 | 17 | 18 | {renderRoutes(routes)} 19 | 20 | 21 | ); 22 | }; 23 | 24 | 25 | const reactRenderer = () => { 26 | ReactDOM.render(( 27 | 28 | 29 | 30 | ), document.getElementById("my-app")); 31 | }; 32 | 33 | const beforeAppStart = async () => { 34 | return true; 35 | }; 36 | 37 | beforeAppStart() 38 | .then(() => { 39 | reactRenderer(); 40 | }); 41 | 42 | 43 | -------------------------------------------------------------------------------- /packages/proxy/src/middlewares/url-middleware.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * File: resolve-req-url.ts 3 | * Description: req 参数的 url 属性不包含主机名,我们要进行额外的加工,注意,后面关于 url 的一切参数都从这边获取 4 | * Created: 2021-08-11 15:04:19 5 | * Author: yuzhanglong 6 | * Email: yuzl1123@163.com 7 | */ 8 | import type { ProxyServerContext, ProxyServerMiddleware } from '../types'; 9 | 10 | export function createUrlMiddleWare(): ProxyServerMiddleware { 11 | return async (ctx: ProxyServerContext, next) => { 12 | const { incomingRequestData, protocol } = ctx; 13 | 14 | if (!protocol) 15 | throw new Error('[@attachments/proxy] please give us a protocol!'); 16 | 17 | const host = incomingRequestData.headers.host || ''; 18 | // origin 指 协议 + 主机的形式 19 | const origin = `${protocol}://${host}`; 20 | // req.url 为 nodejs 的 API,指的是 url 串除了 origin 一部分(search、params 所在的地方) 21 | ctx.urlInstance = new URL(incomingRequestData.url, origin); 22 | 23 | // 略去 80 端口 24 | if (ctx.urlInstance.port === '80') 25 | ctx.urlInstance.port = ''; 26 | 27 | await next(); 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/src/xhr/types.ts: -------------------------------------------------------------------------------- 1 | import type { BaseObject, MonitorOptions, UrlData } from '../types'; 2 | 3 | // xhr 监控结果记录数据 4 | export interface XHRReportData { 5 | // 请求数据,包括 url 相关参数 6 | request: UrlData & { 7 | // 请求方法 8 | method: string 9 | // 通过 setRequestHeaders 添加的请求头 10 | headers: Record 11 | body: string 12 | } 13 | performance: Record 14 | duration: number 15 | response: { 16 | status: number 17 | timestamp: number 18 | headers: Record 19 | body: string 20 | } 21 | } 22 | 23 | // xhr 监控选项 24 | export type XHRMonitorOptions = MonitorOptions; 25 | 26 | // xhr 监控实例暂存数据记录,挂在在用户初始化的 XMLHttpRequest 上 27 | interface XHRMonitorRecode { 28 | url?: string 29 | method?: string 30 | startTime?: number 31 | requestHeaders?: BaseObject 32 | requestData?: Parameters[0] 33 | } 34 | 35 | export interface PatchedXMLHttpRequest extends XMLHttpRequest { 36 | // 记录信息 37 | monitorRecords: XHRMonitorRecode 38 | } 39 | -------------------------------------------------------------------------------- /packages/assets/src/templates/typescript-project/package.json.hbs: -------------------------------------------------------------------------------- 1 | { 2 | "name": "{{package project-name}}", 3 | "version": "0.0.0", 4 | "description": "> TODO: add description", 5 | "author": "> TODO: add author", 6 | "homepage": "", 7 | "license": "MIT", 8 | "main": "lib/index.js", 9 | "module": "esm/index.js", 10 | "files": [ 11 | "src", 12 | "esm", 13 | "lib" 14 | ], 15 | "scripts": { 16 | "dev:start": "tsc -w", 17 | "build:cjs": "rimraf ./lib && tsc --module commonjs --outDir lib", 18 | "build:esm": "rimraf ./esm && tsc --module ESNext --outDir esm", 19 | "build": "npm-run-all --parallel build:*", 20 | "lint": "eslint --ext js,jsx,ts,tsx src", 21 | "test": "jest" 22 | }, 23 | "devDependencies": { 24 | "@attachments/eslint-plugin": "^0.1.0", 25 | "@types/jest": "^26.0.24", 26 | "@types/node": "^16.4.6", 27 | "eslint": "^7.31.0", 28 | "jest": "^27.0.6", 29 | "npm-run-all": "^4.1.5", 30 | "prettier": "^2.4.1", 31 | "rimraf": "^3.0.2", 32 | "ts-jest": "^27.0.4", 33 | "ts-node": "^10.1.0", 34 | "typescript": "^4.3.5" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 YuZhanglong 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 | -------------------------------------------------------------------------------- /packages/assets/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 YuZhanglong 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 | -------------------------------------------------------------------------------- /packages/hooks/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 YuZhanglong 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 | -------------------------------------------------------------------------------- /packages/proxy/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 YuZhanglong 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 | -------------------------------------------------------------------------------- /packages/utils/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 YuZhanglong 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 | -------------------------------------------------------------------------------- /packages/i18n/i18n-core/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 YuZhanglong 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 | -------------------------------------------------------------------------------- /packages/i18n/babel-plugin-i18n/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 YuZhanglong 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 | -------------------------------------------------------------------------------- /packages/i18n/i18n-webpack-plugin/examples/my-app/src/utils/create-micro-app.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from 'react'; 2 | import { MicroApp, MicroAppRef } from '@mf-lite/core/esm/browser/micro-app'; 3 | import { MICRO_APPS, MicroAppConfig } from '~src/common/const'; 4 | 5 | 6 | export const getMicroApp = (name: string): MicroAppConfig => { 7 | const item = MICRO_APPS.find(res => res.name === name); 8 | if (!item) { 9 | throw new Error(`the micro app ${name} is not exist!`); 10 | } 11 | return item; 12 | }; 13 | 14 | 15 | export const createMicroApp = (name: string) => { 16 | return () => { 17 | const ref = useRef(null); 18 | 19 | const microAppConfig = { 20 | name: name, 21 | entry: getMicroApp(name).url, 22 | } 23 | 24 | useEffect(() => { 25 | if (ref.current?.appStore) { 26 | ref.current?.appStore.microApp?.loadPromise 27 | .then(() => { 28 | console.log('micro app loaded!'); 29 | }); 30 | } 31 | }, []); 32 | 33 | return ( 34 | 37 | ); 38 | }; 39 | }; 40 | -------------------------------------------------------------------------------- /packages/assets/src/configurations/node-plop/ts-project-generator.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import type * as plop from 'node-plop'; 3 | import { createAddConfigAction, createAddManyTemplatesAction } from '../../utils'; 4 | 5 | const project = function (plop: plop.NodePlopAPI) { 6 | plop.setGenerator('typescript package', { 7 | description: 'generate a typescript package', 8 | prompts: [ 9 | { 10 | type: 'input', 11 | name: 'project-name', 12 | message: 'Please enter the name of the package:', 13 | }, 14 | ], 15 | actions: [ 16 | // ts basic template 17 | createAddManyTemplatesAction('typescript-project', path.resolve(process.cwd(), '{{project-name}}')), 18 | // eslint config 19 | createAddConfigAction('eslintrc.js.hbs', path.resolve(process.cwd(), '{{project-name}}', '.eslintrc.js')), 20 | // jest config 21 | createAddConfigAction('jest.config.js.hbs', path.resolve(process.cwd(), '{{project-name}}', 'jest.config.js')), 22 | ], 23 | }); 24 | 25 | plop.setHelper('package', (name) => { 26 | return `@attachments/${name}`; 27 | }); 28 | }; 29 | 30 | export default project; 31 | -------------------------------------------------------------------------------- /packages/assets/src/templates/micro-frontend/base-app/src/utils/create-micro-app.tsx.hbs: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from 'react'; 2 | import { MicroApp, MicroAppRef } from '@mf-lite/core/esm/browser/micro-app'; 3 | import { MICRO_APPS, MicroAppConfig } from '~src/common/const'; 4 | 5 | 6 | export const getMicroApp = (name: string): MicroAppConfig => { 7 | const item = MICRO_APPS.find(res => res.name === name); 8 | if (!item) { 9 | throw new Error(`the micro app ${name} is not exist!`); 10 | } 11 | return item; 12 | }; 13 | 14 | 15 | export const createMicroApp = (name: string) => { 16 | return () => { 17 | const ref = useRef(null); 18 | 19 | const microAppConfig = { 20 | name: name, 21 | entry: getMicroApp(name).url, 22 | } 23 | 24 | useEffect(() => { 25 | if (ref.current?.appStore) { 26 | ref.current?.appStore.microApp?.loadPromise 27 | .then(() => { 28 | console.log('micro app loaded!'); 29 | }); 30 | } 31 | }, []); 32 | 33 | return ( 34 | 37 | ); 38 | }; 39 | }; 40 | -------------------------------------------------------------------------------- /packages/i18n/i18n-webpack-plugin/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 YuZhanglong 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 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 YuZhanglong 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 | -------------------------------------------------------------------------------- /packages/utils/src/node/load-ts-config-file.ts: -------------------------------------------------------------------------------- 1 | import type { Service } from 'ts-node'; 2 | import { interopRequireDefault } from './interop-require-default'; 3 | 4 | /** 5 | * 加载 typescript 文件,常用于加载一些配置文件 6 | * 7 | * @author yuzhanglong 8 | * @date 2021-10-10 22:21:31 9 | */ 10 | export const loadTsConfigFile = async (configPath: string): Promise => { 11 | let tsNodeService: Service; 12 | 13 | // Register TypeScript compiler instance 14 | try { 15 | tsNodeService = require('ts-node').register({ 16 | compilerOptions: { 17 | module: 'CommonJS', 18 | }, 19 | }); 20 | } 21 | catch (e) { 22 | if (e.code === 'MODULE_NOT_FOUND') { 23 | throw new Error( 24 | `'ts-node' is required for the TypeScript configuration files. Make sure it is installed\nError: ${e.message}`, 25 | ); 26 | } 27 | 28 | throw e; 29 | } 30 | 31 | tsNodeService.enabled(true); 32 | 33 | let configObject = interopRequireDefault(require(configPath)).default; 34 | 35 | // 配置文件是一个函数,调用之 36 | if (typeof configObject === 'function') 37 | configObject = await configObject(); 38 | 39 | tsNodeService.enabled(false); 40 | 41 | return configObject; 42 | }; 43 | -------------------------------------------------------------------------------- /packages/assets/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@attachments/assets", 3 | "version": "0.4.0", 4 | "license": "MIT", 5 | "description": "common resource tool packages, such as the CSS and project templates", 6 | "author": "yuzhanglong ", 7 | "homepage": "https://github.com/yuzhanglong/attachments", 8 | "files": [ 9 | "src", 10 | "lib", 11 | "esm" 12 | ], 13 | "bin": { 14 | "attachments": "./lib/attachments.js" 15 | }, 16 | "scripts": { 17 | "test": "attachments generate ts", 18 | "dev:start": "tsc -w", 19 | "build:cjs": "rimraf ./lib && tsc --module commonjs --outDir lib", 20 | "build:esm": "rimraf ./esm && tsc --module ESNext --outDir esm", 21 | "build": "npm-run-all --parallel build:*", 22 | "typescript-template": "cd playground && plop --plopfile ../lib/configurations/node-plop/ts-project-generator.js", 23 | "micro-app-template": "cd playground && plop --plopfile ../lib/configurations/node-plop/micro-fe-generator.js" 24 | }, 25 | "dependencies": { 26 | "@attachments/utils": "^0.4.0", 27 | "commander": "^8.2.0", 28 | "execa": "^5.1.1", 29 | "minimist": "^1.2.5", 30 | "node-plop": "^0.26.3", 31 | "plop": "^2.7.6" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/assets/src/templates/micro-frontend/micro-app/src/pages/home.tsx.hbs: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { renderRoutes } from 'react-router-config'; 3 | import { RouteComponentProps } from 'react-router'; 4 | import './home.less'; 5 | 6 | interface HomeProps extends RouteComponentProps { 7 | 8 | } 9 | 10 | const Home: React.FC = (props) => { 11 | // @ts-ignore 12 | const { route, history, location } = props; 13 | const [currentPage, setCurrentPage] = useState<'one' | 'two'>( 14 | location.pathname === '/page-two' ? 'two' : 'one' 15 | ); 16 | 17 | const handleButtonClick = () => { 18 | const nextPage = currentPage === 'one' ? 'two' : 'one'; 19 | setCurrentPage(nextPage); 20 | history.push(`page-${nextPage}`); 21 | }; 22 | 23 | return ( 24 |
25 |
26 | 29 |
30 |
31 | {renderRoutes(route.routes)} 32 |
33 |
34 | ); 35 | }; 36 | 37 | export default Home; 38 | -------------------------------------------------------------------------------- /packages/i18n/i18n-core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@attachments/i18n", 3 | "version": "0.4.0", 4 | "description": "> TODO: description", 5 | "author": "yuzhanglong ", 6 | "homepage": "", 7 | "license": "MIT", 8 | "main": "lib/index.js", 9 | "module": "esm/index.js", 10 | "files": [ 11 | "src", 12 | "esm", 13 | "lib", 14 | "dist" 15 | ], 16 | "scripts": { 17 | "dev:start": "tsc -w", 18 | "build": "npm-run-all --parallel build:* build-umd:*", 19 | "build:cjs": "rimraf ./lib && tsc --module commonjs --outDir lib", 20 | "build:esm": "rimraf ./esm && tsc --module ESNext --outDir esm", 21 | "build-umd:development": "webpack --node-env development", 22 | "build-umd:production": "webpack --node-env production", 23 | "build:umd": "rimraf ./dist && npm-run-all --parallel build-umd:*" 24 | }, 25 | "dependencies": { 26 | "intl-messageformat": "^9.8.1" 27 | }, 28 | "devDependencies": { 29 | "@babel/plugin-transform-runtime": "^7.15.8", 30 | "@babel/preset-env": "^7.15.8", 31 | "@babel/preset-typescript": "^7.15.0", 32 | "@babel/runtime": "^7.16.0", 33 | "babel-loader": "^8.2.2", 34 | "webpack": "^5.51.1", 35 | "webpack-cli": "^4.8.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/src/utils/use-request-animation-frame.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 使用 request animation frame 调度某个回调函数 3 | * 4 | * @author yuzhanglong 5 | * @date 2021-11-06 23:18:15 6 | */ 7 | import { isFunction } from 'lodash'; 8 | import { getAnimationFrame } from './browser-interfaces'; 9 | 10 | export const useRequestAnimationFrame = (callback: FrameRequestCallback) => { 11 | const apis = getAnimationFrame(); 12 | 13 | if (!apis) 14 | return; 15 | 16 | const { raf, caf } = apis; 17 | 18 | if (!isFunction(raf) || !isFunction(caf)) 19 | return; 20 | 21 | // raf 的返回值为非 0 数字 22 | let rafTimer = 0; 23 | 24 | const runCallback = () => { 25 | if (rafTimer) { 26 | // requestAnimationFrame 不管理回调函数 27 | // 在回调被执行前,多次调用带有同一回调函数的 requestAnimationFrame,会导致回调在同一帧中执行多次 28 | // 常见的情况是一些事件机制导致多次触发 29 | // 设定一个 timer,如果接下来回调再次被调度,那么撤销上一个 30 | // https://www.w3.org/TR/animation-timing/#dom-windowanimationtiming-requestanimationframe 31 | caf(rafTimer); 32 | } 33 | else { 34 | rafTimer = raf(callback); 35 | } 36 | }; 37 | 38 | const cancelCallback = () => { 39 | if (rafTimer) 40 | caf(rafTimer); 41 | }; 42 | 43 | return { 44 | runCallback, 45 | cancelCallback, 46 | }; 47 | }; 48 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/examples/react-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.16.1", 7 | "@testing-library/react": "^12.1.2", 8 | "@testing-library/user-event": "^13.5.0", 9 | "@types/jest": "^27.4.0", 10 | "@types/node": "^16.11.17", 11 | "@types/react": "^17.0.38", 12 | "@types/react-dom": "^17.0.11", 13 | "cross-env": "^7.0.3", 14 | "react": "^17.0.2", 15 | "react-dom": "^17.0.2", 16 | "react-scripts": "5.0.0", 17 | "typescript": "^4.5.4", 18 | "web-vitals": "^2.1.2" 19 | }, 20 | "scripts": { 21 | "start": "react-scripts start", 22 | "build": "cross-env PUBLIC_URL=https://qch624.web.cloudendpoint.cn react-scripts build", 23 | "test": "react-scripts test", 24 | "eject": "react-scripts eject" 25 | }, 26 | "eslintConfig": { 27 | "extends": [ 28 | "react-app", 29 | "react-app/jest" 30 | ] 31 | }, 32 | "browserslist": { 33 | "production": [ 34 | ">0.2%", 35 | "not dead", 36 | "not op_mini all" 37 | ], 38 | "development": [ 39 | "last 1 chrome version", 40 | "last 1 firefox version", 41 | "last 1 safari version" 42 | ] 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/src/utils/format-error.ts: -------------------------------------------------------------------------------- 1 | import type { JsErrorReportData } from '../js-error/types'; 2 | import { instanceOf } from './instance-of'; 3 | 4 | /** 5 | * 格式化错误信息,将其转换为简单对象 6 | * 7 | * @author yuzhanglong 8 | * @date 2021-08-24 22:59:27 9 | * @see https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Error 10 | * @param e 异常 ErrorEvent, 或者 PromiseRejectionEvent 实例 11 | * 前者来自 window.addEventListener('error') 的回调 12 | * 后者来自 window.addEventListener('unhandledrejection') 的回调 13 | */ 14 | export function formatError(e: ErrorEvent | PromiseRejectionEvent): JsErrorReportData { 15 | let error: Error; 16 | 17 | if (instanceOf(e, PromiseRejectionEvent)) { 18 | error = (e as PromiseRejectionEvent).reason; 19 | } 20 | else if (instanceOf(e, ErrorEvent)) { 21 | error = (e as ErrorEvent).error; 22 | } 23 | else { 24 | // @ts-expect-error 25 | error = e.reason || e.error; 26 | } 27 | 28 | if (!error) { 29 | // 这是为了处理跨域脚本内部异常详细信息无法获取的边界情况 30 | // 此类错误我们忽略之 31 | return; 32 | } 33 | 34 | return { 35 | // 发生异常的时间戳 36 | timestamp: Date.now(), 37 | // 核心内容,包括堆栈错误信息 38 | error: { 39 | name: error.name, 40 | message: error.message, 41 | stack: error.stack, 42 | }, 43 | }; 44 | } 45 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/webpack.config.ts: -------------------------------------------------------------------------------- 1 | import type webpack from 'webpack'; 2 | import TerserWebpackPlugin from 'terser-webpack-plugin'; 3 | 4 | const env = process.env.NODE_ENV; 5 | const isDev = env === 'development'; 6 | 7 | const config: webpack.Configuration = { 8 | entry: './src/index.ts', 9 | mode: isDev ? 'development' : 'production', 10 | devtool: false, 11 | output: { 12 | library: { 13 | name: 'Monitor', 14 | type: 'umd', 15 | }, 16 | filename: isDev ? 'monitor.js' : 'monitor.min.js', 17 | }, 18 | module: { 19 | rules: [ 20 | { 21 | test: /\.(ts|tsx)$/, 22 | use: { 23 | loader: 'babel-loader', 24 | options: { 25 | presets: ['@babel/preset-env', '@babel/preset-typescript', '@babel/preset-react'], 26 | plugins: ['@babel/plugin-transform-runtime', 'lodash'], 27 | }, 28 | }, 29 | }, 30 | ], 31 | }, 32 | resolve: { 33 | extensions: ['.ts', '.tsx', '.js', '.jsx'], 34 | }, 35 | optimization: { 36 | minimizer: [ 37 | new TerserWebpackPlugin({ 38 | terserOptions: { 39 | format: { 40 | comments: false, 41 | }, 42 | }, 43 | extractComments: false, 44 | }), 45 | ], 46 | }, 47 | }; 48 | 49 | export default config; 50 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/src/js-error/js-error-monitor.ts: -------------------------------------------------------------------------------- 1 | import { EventType } from '../types'; 2 | import { getBrowserWindow } from '../utils/browser-interfaces'; 3 | import { formatError } from '../utils/format-error'; 4 | import type { JsErrorMonitorOptions } from './types'; 5 | 6 | /** 7 | * javaScript 异常监控能力 8 | * 9 | * @author yuzhanglong 10 | * @date 2021-08-24 17:08:10 11 | */ 12 | export function createJsErrorMonitor(options: JsErrorMonitorOptions) { 13 | const window = getBrowserWindow(); 14 | 15 | if (!window) 16 | return; 17 | 18 | const handleError = (e: ErrorEvent) => { 19 | const data = formatError(e); 20 | if (data) { 21 | options.onReport({ 22 | eventType: EventType.JS_ERROR, 23 | data, 24 | }); 25 | } 26 | }; 27 | 28 | const handleRejection = (e: PromiseRejectionEvent) => { 29 | options.onReport({ 30 | eventType: EventType.JS_ERROR, 31 | data: formatError(e), 32 | }); 33 | }; 34 | 35 | const destroyListeners = () => { 36 | window.removeEventListener('error', handleError); 37 | window.removeEventListener('unhandledrejection', handleRejection); 38 | }; 39 | 40 | // 捕获异步 error 41 | window.addEventListener('error', handleError); 42 | window.addEventListener('unhandledrejection', handleRejection); 43 | 44 | return { 45 | destroy: destroyListeners, 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/cypress/integration/mpfid.spec.tsx: -------------------------------------------------------------------------------- 1 | import { createMPFIDMonitor } from '../../src'; 2 | import type { CallBack } from '../../src/types'; 3 | import { EventType } from '../../src/types'; 4 | import { promisifyCounterMonitorReport } from '../utils/test-utils'; 5 | import type { MPFIDReportData } from '../../src/mpfid/types'; 6 | 7 | const runMonitor = async (cb: CallBack) => 8 | promisifyCounterMonitorReport( 9 | { 10 | afterCreateMonitorCallback: cb, 11 | monitorFactory: createMPFIDMonitor, 12 | }, 13 | { 14 | timeout: 2000, 15 | }, 16 | ); 17 | 18 | describe('test mpfid monitor', () => { 19 | it('mpfid', async () => { 20 | const data = await runMonitor(async () => { 21 | // 递归版的斐波那契数列,比较耗时的一个任务,可以作为 long task 22 | const fib = (n: number) => { 23 | if (n <= 2) 24 | return 1; 25 | 26 | return fib(n - 1) + fib(n - 2); 27 | }; 28 | await Promise.all([ 29 | new Promise(resolve => 30 | setTimeout(() => { 31 | console.log(`fib(40) = ${fib(40)}`); 32 | resolve(true); 33 | }, 1000), 34 | ), 35 | ]); 36 | }); 37 | 38 | expect(data.length).to.equal(1); 39 | const res = data.pop(); 40 | expect(res.eventType).to.equal(EventType.MPFID); 41 | expect(res.data.mpfid > 0).to.be.true; 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /packages/assets/src/utils.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import { runCommand } from '@attachments/utils/lib/node/run-command'; 3 | 4 | export const getBasePath = () => path.resolve(__dirname, '..'); 5 | 6 | export const getSourcePath = () => path.resolve(getBasePath(), 'src'); 7 | 8 | export const getLibPath = () => path.resolve(getBasePath(), 'lib'); 9 | 10 | export const getTemplatePath = () => path.resolve(getSourcePath(), 'templates'); 11 | 12 | export const getTemplatePathByName = (name: string) => path.resolve(getTemplatePath(), name); 13 | 14 | export const createAddConfigAction = (name: string, p: string) => { 15 | return { 16 | type: 'add', 17 | path: p, 18 | templateFile: path.resolve(getTemplatePath(), 'common', name), 19 | }; 20 | }; 21 | 22 | export const createAddManyTemplatesAction = (name: string, destination: string) => { 23 | const tsTemplatePath = getTemplatePathByName(name); 24 | 25 | return { 26 | type: 'addMany', 27 | destination, 28 | templateFiles: `${tsTemplatePath}/**/*`, 29 | base: tsTemplatePath, 30 | }; 31 | }; 32 | 33 | export const launchPlopByConfig = async (generator: string) => { 34 | const configPath = path.resolve(getLibPath(), 'configurations', 'node-plop', `${generator}.js`); 35 | const plopPath = path.resolve(require.resolve('plop'), '../../bin/plop.js'); 36 | await runCommand('node', [plopPath, '--plopfile', configPath]); 37 | }; 38 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/src/utils/observe-performance.ts: -------------------------------------------------------------------------------- 1 | import { noop } from 'lodash'; 2 | import { getPerformanceObserver } from './browser-interfaces'; 3 | 4 | /** 5 | * 监听 performance 性能指标, 当不支持 performance API 时,我们不进行任何动作 6 | * 7 | * @author yuzhanglong 8 | * @date 2021-08-26 16:38:12 9 | * @param options 监听的选项,可以在这里配置监听目标 10 | * @param callback 监听回调 11 | * @param once 仅监听一次 12 | * @return Function 一个销毁监听器的函数,如果 performance API 不存在,则返回 noop 13 | * 14 | */ 15 | export const observePerformance = ( 16 | options: PerformanceObserverInit, 17 | callback: (entryList: PerformanceEntry[]) => void, 18 | once = false, 19 | ) => { 20 | let destroy = noop; 21 | 22 | let isExecuted = false; 23 | 24 | // 通过 observer 监听 25 | const PerformanceObserver = getPerformanceObserver(); 26 | if (PerformanceObserver) { 27 | const observerInstance = new PerformanceObserver((list) => { 28 | // 用户配置了只回调一次,并且已经执行过,我们不再执行 29 | if (once && isExecuted) 30 | return; 31 | 32 | // performanceEntries 是【某小一段时间】得到的性能结果 33 | // 我们再遍历他们,并逐一调用 callback, 这样上层调用者无需再额外处理 34 | const performanceEntries = list.getEntries(); 35 | callback(performanceEntries); 36 | isExecuted = true; 37 | }); 38 | 39 | observerInstance.observe(options); 40 | 41 | // 覆写销毁函数 42 | destroy = () => { 43 | observerInstance.disconnect(); 44 | }; 45 | } 46 | 47 | return destroy; 48 | }; 49 | -------------------------------------------------------------------------------- /packages/proxy/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@attachments/proxy", 3 | "version": "0.4.0", 4 | "description": "proxy server for front-end developers", 5 | "author": "yuzhanglong ", 6 | "homepage": "https://github.com/yuzhanglong/attachments", 7 | "license": "MIT", 8 | "main": "lib/index.js", 9 | "module": "esm/index.js", 10 | "files": [ 11 | "src", 12 | "esm", 13 | "lib", 14 | "certificate", 15 | "example" 16 | ], 17 | "scripts": { 18 | "dev:start": "tsc -w", 19 | "build": "npm-run-all --parallel build:*", 20 | "build:cjs": "rimraf ./lib && tsc --module commonjs --outDir lib", 21 | "build:esm": "rimraf ./esm && tsc --module ESNext --outDir esm", 22 | "example-http:proxy": "ts-node example/http/proxy.ts", 23 | "example-http:server": "ts-node example/http/http.ts", 24 | "example-ws:proxy": "ts-node example/websocket/proxy.ts", 25 | "example-ws:server": "ts-node example/websocket/server.ts", 26 | "dev:example-http": "npm-run-all --parallel example-http:*", 27 | "dev:example-ws": "npm-run-all --parallel example-ws:*" 28 | }, 29 | "dependencies": { 30 | "koa-compose": "^4.1.0", 31 | "pem": "^1.14.4", 32 | "winston": "^3.3.3" 33 | }, 34 | "devDependencies": { 35 | "@types/koa-compose": "^3.2.5", 36 | "@types/pem": "^1.9.6", 37 | "koa": "^2.13.1", 38 | "koa-router": "^10.0.0", 39 | "ws": "^8.1.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/src/utils/browser-interfaces.ts: -------------------------------------------------------------------------------- 1 | import { isFunction, isObject } from 'lodash'; 2 | 3 | export const getBrowserWindow = () => { 4 | if (isObject(window)) 5 | return window; 6 | 7 | return null; 8 | }; 9 | 10 | export const getPerformance = () => { 11 | if (getBrowserWindow() && isObject(window.performance)) 12 | return window.performance; 13 | 14 | return null; 15 | }; 16 | 17 | export const getPerformanceObserver = () => { 18 | if (getBrowserWindow() && isFunction(window.PerformanceObserver)) 19 | return window.PerformanceObserver; 20 | 21 | return null; 22 | }; 23 | 24 | export const getXMLHttpRequest = () => { 25 | if (getBrowserWindow() && isFunction(window.XMLHttpRequest)) 26 | return window.XMLHttpRequest; 27 | 28 | return null; 29 | }; 30 | 31 | export const getDocument = () => { 32 | const window = getBrowserWindow(); 33 | if (!window || !window.document) 34 | return null; 35 | 36 | return window.document; 37 | }; 38 | 39 | export const getAnimationFrame = () => { 40 | const window = getBrowserWindow(); 41 | 42 | if (!window) 43 | return null; 44 | 45 | return { 46 | raf: window.requestAnimationFrame, 47 | caf: window.cancelAnimationFrame, 48 | }; 49 | }; 50 | 51 | export const getMutationObserver = () => { 52 | const window = getBrowserWindow(); 53 | 54 | if (!window) 55 | return undefined; 56 | 57 | return window.MutationObserver; 58 | }; 59 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/src/utils/create-scheduler.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * File: create-scheduler.ts 3 | * Description: 定时调度器工具函数 4 | * Created: 2021-09-05 21:06:44 5 | * Author: yuzhanglong 6 | * Email: yuzl1123@163.com 7 | */ 8 | 9 | export const createScheduler = () => { 10 | // 任务 id 11 | let taskId: number | undefined; 12 | 13 | // 执行调度的回调函数 14 | let callback: () => any | undefined; 15 | 16 | // 即将调度的时间 17 | let scheduleTime = -Infinity; 18 | 19 | const getCurrentTime = () => { 20 | return performance.now(); 21 | }; 22 | 23 | const clearCurrentScheduleTimer = () => { 24 | window.clearTimeout(taskId); 25 | }; 26 | 27 | const resetScheduler = (newScheduleTime: number) => { 28 | if (!callback) 29 | return; 30 | 31 | // 新配置的时间早于即将调度的时间,不处理 32 | if (newScheduleTime < scheduleTime) 33 | return; 34 | 35 | clearCurrentScheduleTimer(); 36 | 37 | const newTime = newScheduleTime - getCurrentTime(); 38 | taskId = window.setTimeout(() => { 39 | callback(); 40 | }, newTime); 41 | scheduleTime = newScheduleTime; 42 | }; 43 | 44 | const startSchedule = (cb: () => void, time: number) => { 45 | callback = cb; 46 | resetScheduler(time); 47 | }; 48 | 49 | const stopSchedule = () => { 50 | clearCurrentScheduleTimer(); 51 | callback = undefined; 52 | }; 53 | 54 | return { 55 | resetScheduler, 56 | startSchedule, 57 | stopSchedule, 58 | }; 59 | }; 60 | -------------------------------------------------------------------------------- /packages/proxy/example/websocket/server.ts: -------------------------------------------------------------------------------- 1 | import https from 'https'; 2 | import { Server } from 'ws'; 3 | import Koa from 'koa'; 4 | import { CertificationManager } from '../../src'; 5 | 6 | const app = new Koa(); 7 | const cm = new CertificationManager(); 8 | 9 | const run = async () => { 10 | const cfg = await cm.createCertificationByDomain('localhost'); 11 | const server = https.createServer( 12 | { 13 | cert: cfg.cert, 14 | key: cfg.key, 15 | }, 16 | app.callback(), 17 | ); 18 | 19 | // 指定一个url匹配 20 | app.use(async (ctx) => { 21 | ctx.body 22 | = '\n' 23 | + '\n' 24 | + '\n' 25 | + ' \n' 26 | + ' Use Proxy For WebSocket\n' 27 | + '\n' 28 | + '\n' 29 | + '\n' 37 | + '\n' 38 | + '\n' 39 | + '\n'; 40 | }); 41 | 42 | const wss = new Server({ 43 | server, 44 | }); 45 | 46 | wss.on('connection', (ws) => { 47 | ws.on('message', (message) => { 48 | console.log('received: %s', message); 49 | }); 50 | 51 | ws.send('something'); 52 | }); 53 | 54 | server.listen(10000); 55 | }; 56 | 57 | run(); 58 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/src/types.ts: -------------------------------------------------------------------------------- 1 | // 推断某个对象的所有 Functional keys 2 | export type MethodKeys = { 3 | [key in keyof O]: O[key] extends Function ? key : never; 4 | }[keyof O]; 5 | 6 | // 推断某个对象所有非 Functional key 7 | export type CommonKeys = { 8 | [key in keyof O]: O[key] extends Function ? never : key; 9 | }[keyof O]; 10 | 11 | // 回调 12 | export type CallBack

= (params: P) => void; 13 | 14 | export type BaseObject = Record; 15 | 16 | export interface ReportBase { 17 | // 上报事件的类型 18 | eventType: EventType 19 | data: Data 20 | } 21 | 22 | export enum EventType { 23 | // noinspection SpellCheckingInspection 24 | FETCH = 'FETCH', 25 | XHR = 'XHR', 26 | JS_ERROR = 'JS_ERROR', 27 | ASSETS = 'ASSETS', 28 | ASSETS_ERROR = 'ASSETS_ERROR', 29 | PAINT = 'PAINT', 30 | LARGEST_CONTENTFUL_PAINT = 'LARGEST_CONTENTFUL_PAINT', 31 | CUMULATIVE_LAYOUT_SHIFT = 'CUMULATIVE_LAYOUT_SHIFT', 32 | TTI = 'TTI', 33 | MPFID = 'MPFID', 34 | FID = 'FID', 35 | COMMON_PERFORMANCE_TIMING = 'COMMON_PERFORMANCE_TIMING', 36 | FMP = 'FMP', 37 | } 38 | 39 | export interface MonitorOptions { 40 | // 发送报告回调 41 | onReport: CallBack> 42 | // window 实例 43 | window?: Window 44 | } 45 | 46 | export interface UrlData { 47 | url: string 48 | hash: string 49 | host: string 50 | hostname: string 51 | href: string 52 | origin: string 53 | pathname: string 54 | port: string 55 | protocol: string 56 | search: string 57 | } 58 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@attachments/monitor-sdk-browser", 3 | "version": "0.1.26", 4 | "main": "lib/index.js", 5 | "module": "esm/index.js", 6 | "license": "MIT", 7 | "scripts": { 8 | "dev:start": "node_modules/.bin/webpack --node-env development -w", 9 | "build": "npm-run-all --parallel build:*", 10 | "build:cjs": "rimraf lib && tsc --module commonjs --outDir lib", 11 | "build:esm": "rimraf esm && tsc --module ESNext --outDir esm", 12 | "build:umd": "rimraf dist && webpack --node-env production", 13 | "build:dev-umd": "rimraf dist && webpack --node-env development", 14 | "test:cypress": "cypress open-ct" 15 | }, 16 | "files": [ 17 | "src", 18 | "esm", 19 | "lib", 20 | "dist" 21 | ], 22 | "devDependencies": { 23 | "@attachments/utils": "^0.1.26", 24 | "@babel/plugin-transform-runtime": "^7.15.8", 25 | "@babel/preset-env": "^7.15.8", 26 | "@babel/preset-react": "^7.16.0", 27 | "@babel/preset-typescript": "^7.15.0", 28 | "@babel/runtime": "^7.16.0", 29 | "@types/lodash": "^4.14.172", 30 | "axios": "^0.24.0", 31 | "babel-loader": "^8.2.2", 32 | "babel-plugin-lodash": "^3.3.4", 33 | "html-webpack-plugin": "^5.3.2", 34 | "react": "^17.0.2", 35 | "react-dom": "^17.0.2", 36 | "terser-webpack-plugin": "^5.2.4", 37 | "webpack": "^5.61.0", 38 | "webpack-bundle-analyzer": "^4.5.0", 39 | "webpack-dev-server": "^4.4.0" 40 | }, 41 | "dependencies": { 42 | "lodash": "^4.17.21" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/src/fid/fid-monitor.ts: -------------------------------------------------------------------------------- 1 | import { first } from 'lodash'; 2 | import { getPerformanceObserver } from '../utils/browser-interfaces'; 3 | import { observePerformance } from '../utils/observe-performance'; 4 | import { PERFORMANCE_ENTRY_TYPES } from '../constants'; 5 | import { getPerformanceEntriesByType } from '../utils/performance-entry'; 6 | import { EventType } from '../types'; 7 | import type { FIDEntry, FIDMonitorOptions } from './types'; 8 | 9 | /** 10 | * 创建 fid 监听器 11 | * 12 | * @author yuzhanglong 13 | * @date 2021-11-10 23:39:55 14 | */ 15 | export const createFIDMonitor = (options: FIDMonitorOptions) => { 16 | if (!getPerformanceObserver()) 17 | return; 18 | 19 | const reportData = (entry: FIDEntry) => { 20 | if (!entry) 21 | return; 22 | 23 | options.onReport({ 24 | data: { 25 | fid: entry, 26 | }, 27 | eventType: EventType.FID, 28 | }); 29 | }; 30 | 31 | const observeFID = () => { 32 | observePerformance( 33 | { 34 | entryTypes: [PERFORMANCE_ENTRY_TYPES.FIRST_INPUT], 35 | }, 36 | (entryList) => { 37 | const entry = first(entryList); 38 | reportData(entry as FIDEntry); 39 | }, 40 | true, 41 | ); 42 | }; 43 | 44 | const getFIDDirectly = () => { 45 | return first(getPerformanceEntriesByType(PERFORMANCE_ENTRY_TYPES.FIRST_INPUT)); 46 | }; 47 | 48 | // 先尝试直接获取,如果没有再开启监听器 49 | const entry = getFIDDirectly(); 50 | if (!entry) 51 | observeFID(); 52 | else 53 | reportData(entry as FIDEntry); 54 | }; 55 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/cypress/integration/cls.spec.tsx: -------------------------------------------------------------------------------- 1 | import { noop } from 'lodash'; 2 | import { mount } from '@cypress/react'; 3 | import React, { useEffect, useState } from 'react'; 4 | import { createClsMonitor } from '../../src'; 5 | import type { ReportBase } from '../../src/types'; 6 | import { EventType } from '../../src/types'; 7 | import type { ClsReportData } from '../../src/cls/types'; 8 | 9 | describe('test cls monitor', () => { 10 | it('test cls monitor', async () => { 11 | const m = createClsMonitor({ 12 | onReport: noop, 13 | }); 14 | 15 | const Cmp = () => { 16 | const [count, setCount] = useState(0); 17 | useEffect(() => { 18 | setTimeout(() => { 19 | setCount(10); 20 | }, 1000); 21 | setTimeout(() => { 22 | setCount(100); 23 | }, 2000); 24 | }, []); 25 | 26 | return ( 27 |

28 | {new Array(count).fill('').map((item, index) => ( 29 |
Added DOM
30 | ))} 31 |
hello world
32 |
33 | ); 34 | }; 35 | 36 | mount(); 37 | 38 | const res = await new Promise>((resolve, reject) => { 39 | setTimeout(() => { 40 | resolve(m.getReportData()); 41 | }, 4000); 42 | }); 43 | 44 | expect(res.eventType === EventType.CUMULATIVE_LAYOUT_SHIFT); 45 | expect(res.data.clsValue > 0).to.be.true; 46 | expect(res.data.entries.length).to.equal(2); 47 | 48 | console.log(res); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/examples/performance-timing-for-fetch.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /packages/i18n/i18n-webpack-plugin/examples/my-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-app", 3 | "version": "0.0.1", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "devDependencies": { 7 | "@attachments/proxy": "^0.1.0", 8 | "@types/fs-extra": "^9.0.12", 9 | "@types/node": "^16.4.6", 10 | "@types/react": "^17.0.18", 11 | "@types/react-dom": "^17.0.9", 12 | "@types/react-router": "^5.1.16", 13 | "@types/react-router-config": "^5.0.3", 14 | "@types/react-router-dom": "^5.1.8", 15 | "autoprefixer": "^10.3.6", 16 | "concurrently": "^6.3.0", 17 | "cross-env": "^7.0.3", 18 | "eslint": "^7.31.0", 19 | "react-router-config": "^5.1.1", 20 | "rimraf": "^3.0.2", 21 | "ts-node": "^10.2.1", 22 | "typescript": "^4.3.5", 23 | "webpack-merge": "^5.8.0" 24 | }, 25 | "scripts": { 26 | "dev:proxy": "ts-node scripts/proxy.ts", 27 | "dev:serve": "cross-env NODE_ENV=development mf-lite serve --port=8080 --app-type=base-app", 28 | "build:dev": "rimraf dist && cross-env NODE_ENV=development mf-lite build --app-type=base-app", 29 | "build:prod": "rimraf dist && cross-env NODE_ENV=production mf-lite build --app-type=base-app", 30 | "lint": "eslint --ext js,jsx,ts,tsx src", 31 | "generate": "mf-lite generate", 32 | "upgrade": "mf-lite upgrade" 33 | }, 34 | "dependencies": { 35 | "@mf-lite/core": "^0.1.0", 36 | "@mf-lite/cli": "^0.1.0", 37 | "@attachments/utils": "^0.1.0", 38 | "@attachments/eslint-plugin": "^0.1.0", 39 | "react": "^17.0.2", 40 | "react-dom": "^17.0.2", 41 | "react-router": "^5.2.1", 42 | "react-router-dom": "^5.2.1" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/src/utils/get-request-report-data.ts: -------------------------------------------------------------------------------- 1 | // 生成请求报告 2 | import type { XHRReportData } from '../xhr/types'; 3 | import { PatchedXMLHttpRequest } from '../xhr/types'; 4 | import { getUrlData } from './get-url-data'; 5 | import { formatPlainHeadersString } from './format-plain-headers-string'; 6 | import { getPerformanceEntriesByName } from './performance-entry'; 7 | 8 | export interface GetRequestReportDataOptions { 9 | url: string 10 | method: string 11 | status: number 12 | startTime: number 13 | requestHeaders: Record 14 | responseHeaders: Record 15 | requestData: any 16 | responseData: any 17 | responseUrl: string 18 | } 19 | 20 | /** 21 | * 格式化请求上报数据 22 | * 23 | * @author yuzhanglong 24 | * @date 2021-11-14 15:03:53 25 | */ 26 | export const getRequestReportData = (options: GetRequestReportDataOptions): XHRReportData => { 27 | const { url, method, status, startTime, requestHeaders, responseUrl, responseHeaders, responseData, requestData } 28 | = options; 29 | const current = Date.now(); 30 | const isError = status >= 400; 31 | 32 | return { 33 | request: { 34 | ...getUrlData(url), 35 | method: method.toUpperCase(), 36 | headers: requestHeaders, 37 | body: isError ? `${requestData}` : null, 38 | }, 39 | response: { 40 | status: status || -1, 41 | timestamp: current, 42 | headers: responseHeaders, 43 | body: isError ? `${responseData}` : null, 44 | }, 45 | // 如果URL有重定向, responseURL 的值会是经过多次重定向后的最终 URL 46 | performance: getPerformanceEntriesByName(responseUrl).pop(), 47 | duration: current - startTime, 48 | }; 49 | }; 50 | -------------------------------------------------------------------------------- /packages/proxy/__tests__/utils.spec.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { comparePathAndGetDivision, getUrlPaths, removeWWWAndProtocol } from '../src/utils'; 3 | 4 | describe('test utils', () => { 5 | test('getUrlPaths()', () => { 6 | expect(getUrlPaths('www.baidu.com')).toStrictEqual([]); 7 | expect(getUrlPaths('www.baidu.com/foo/bar/baz')).toStrictEqual(['foo', 'bar', 'baz']); 8 | expect(getUrlPaths('www.baidu.com/foo/bar/baz/')).toStrictEqual(['foo', 'bar', 'baz']); 9 | expect(getUrlPaths('www.baidu.com/')).toStrictEqual([]); 10 | }); 11 | 12 | test('removeWWWAndProtocol()', () => { 13 | expect(removeWWWAndProtocol('www.baidu.com')).toStrictEqual('baidu.com'); 14 | expect(removeWWWAndProtocol('google.com')).toStrictEqual('google.com'); 15 | expect(removeWWWAndProtocol('https://www.google.com')).toStrictEqual('google.com'); 16 | expect(removeWWWAndProtocol('http://www.google.com')).toStrictEqual('google.com'); 17 | }); 18 | 19 | test('comparePathAndGetDivision()', () => { 20 | const cmp1 = comparePathAndGetDivision(['foo', 'bar', 'baz'], ['foo', 'bar']); 21 | expect(cmp1).toStrictEqual({ 22 | samePaths: ['foo', 'bar'], 23 | otherPaths: ['baz'], 24 | dividedPos: 2, 25 | }); 26 | }); 27 | 28 | const cmp2 = comparePathAndGetDivision(['aaa', 'bbb'], ['ccc', 'ddd']); 29 | 30 | expect(cmp2).toStrictEqual({ 31 | samePaths: [], 32 | otherPaths: [], 33 | dividedPos: -1, 34 | }); 35 | 36 | const cmp3 = comparePathAndGetDivision(['aaa', 'bbb'], []); 37 | 38 | expect(cmp3).toStrictEqual({ 39 | samePaths: [], 40 | otherPaths: ['aaa', 'bbb'], 41 | dividedPos: 0, 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/src/utils/observe-long-task-and-resources.ts: -------------------------------------------------------------------------------- 1 | import { PERFORMANCE_ENTRY_TYPES } from '../constants'; 2 | import type { TaskTimeInfo } from '../tti/types'; 3 | import { observePerformance } from './observe-performance'; 4 | 5 | /** 6 | * 监听长任务和资源请求 7 | * 8 | * @author yuzhanglong 9 | * @date 2021-09-06 17:37:20 10 | * @param onLongTask 在监听到长任务时做些什么,第一个回调参数为本次长任务的开始时间和结束时间,第二个为对应的 performanceEntry 对象 11 | * @param onNetworkRequest 在监听到网络请求后做些什么,参数同上 12 | */ 13 | export const observeLongTaskAndResources = ( 14 | onLongTask: (timeInfo: TaskTimeInfo, entry: PerformanceEntry) => void, 15 | onNetworkRequest: (timeInfo: TaskTimeInfo, resourceEntry: PerformanceResourceTiming) => void, 16 | ) => { 17 | observePerformance( 18 | { 19 | entryTypes: [PERFORMANCE_ENTRY_TYPES.LONG_TASK, PERFORMANCE_ENTRY_TYPES.RESOURCE], 20 | }, 21 | (entryList) => { 22 | for (const entry of entryList) { 23 | const { startTime, duration, entryType } = entry; 24 | 25 | if (entryType === PERFORMANCE_ENTRY_TYPES.LONG_TASK) { 26 | onLongTask( 27 | { 28 | startTime, 29 | endTime: startTime + duration, 30 | }, 31 | entry, 32 | ); 33 | } 34 | else if (entry.entryType === PERFORMANCE_ENTRY_TYPES.RESOURCE) { 35 | const { fetchStart, responseEnd } = entry as PerformanceResourceTiming; 36 | 37 | onNetworkRequest( 38 | { 39 | startTime: fetchStart, 40 | endTime: responseEnd, 41 | }, 42 | entry as PerformanceResourceTiming, 43 | ); 44 | } 45 | } 46 | }, 47 | ); 48 | }; 49 | -------------------------------------------------------------------------------- /packages/assets/src/templates/micro-frontend/shared/package.json.hbs: -------------------------------------------------------------------------------- 1 | { 2 | "name": "{{projectName}}", 3 | "version": "0.0.1", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "devDependencies": { 7 | "@attachments/proxy": "^0.1.0", 8 | "@types/fs-extra": "^9.0.12", 9 | "@types/node": "^16.4.6", 10 | "@types/react": "^17.0.18", 11 | "@types/react-dom": "^17.0.9", 12 | "@types/react-router": "^5.1.16", 13 | "@types/react-router-config": "^5.0.3", 14 | "@types/react-router-dom": "^5.1.8", 15 | "autoprefixer": "^10.3.6", 16 | "concurrently": "^6.3.0", 17 | "cross-env": "^7.0.3", 18 | "eslint": "^7.31.0", 19 | "react-router-config": "^5.1.1", 20 | "rimraf": "^3.0.2", 21 | "ts-node": "^10.2.1", 22 | "typescript": "^4.3.5", 23 | "webpack-merge": "^5.8.0" 24 | }, 25 | "scripts": { 26 | "dev:proxy": "ts-node scripts/proxy.ts", 27 | "dev:serve": "cross-env NODE_ENV=development mf-lite serve --port={{devServerPort appType}} --app-type={{cmdAppType appType}}", 28 | "build:dev": "rimraf dist && cross-env NODE_ENV=development mf-lite build --app-type={{cmdAppType appType}}", 29 | "build:prod": "rimraf dist && cross-env NODE_ENV=production mf-lite build --app-type={{cmdAppType appType}}", 30 | "lint": "eslint --ext js,jsx,ts,tsx src", 31 | "generate": "mf-lite generate", 32 | "upgrade": "mf-lite upgrade" 33 | }, 34 | "dependencies": { 35 | "@mf-lite/core": "^{{coreVersion}}", 36 | "@mf-lite/cli": "^{{coreVersion}}", 37 | "@attachments/utils": "^0.1.0", 38 | "@attachments/eslint-plugin": "^0.1.0", 39 | "react": "^17.0.2", 40 | "react-dom": "^17.0.2", 41 | "react-router": "^5.2.1", 42 | "react-router-dom": "^5.2.1" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/proxy/src/middlewares/proxy-pass-middleware.ts: -------------------------------------------------------------------------------- 1 | import * as https from 'https'; 2 | import * as http from 'http'; 3 | import type { RequestOptions } from 'https'; 4 | import type { ProxyServerContext, ProxyServerMiddleware } from '../types'; 5 | 6 | export function createProxyPassMiddleware(): ProxyServerMiddleware { 7 | return (ctx: ProxyServerContext) => { 8 | const { incomingRequestData, proxyServerResponse, urlInstance } = ctx; 9 | 10 | const client = urlInstance.protocol === 'https:' ? https : http; 11 | // 向真正的服务器发送消息 12 | 13 | const options: RequestOptions = { 14 | headers: incomingRequestData.headers, 15 | method: incomingRequestData.method, 16 | port: urlInstance.port, 17 | timeout: 3000, 18 | rejectUnauthorized: false, 19 | }; 20 | 21 | const requestToRealServer = client.request(urlInstance.toString(), options, (realServerResponse) => { 22 | // 覆写 proxyServerResponse, 即用户实际接收到的内容 23 | proxyServerResponse.statusCode = realServerResponse.statusCode; 24 | proxyServerResponse.statusMessage = realServerResponse.statusMessage; 25 | 26 | // 添加 proxy 相关的请求头 27 | proxyServerResponse.setHeader('x-response-from', '@attachments/proxy'); 28 | 29 | const headers = Object.entries(realServerResponse.headers); 30 | for (const [k, v] of headers) 31 | proxyServerResponse.setHeader(k, v); 32 | 33 | // 连接真实服务器的响应流(可读流)和代理服务器的响应流(可写流) 34 | realServerResponse.pipe(proxyServerResponse); 35 | 36 | realServerResponse.on('error', (e) => { 37 | console.log(e); 38 | }); 39 | }); 40 | 41 | requestToRealServer.end(); 42 | 43 | requestToRealServer.on('error', (e) => { 44 | console.log(e); 45 | }); 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/src/assets/assets-monitor.ts: -------------------------------------------------------------------------------- 1 | import { noop } from 'lodash'; 2 | import { EventType } from '../types'; 3 | import { observePerformance } from '../utils/observe-performance'; 4 | import { PERFORMANCE_ENTRY_TYPES } from '../constants'; 5 | import { onPageLoad } from '../utils/on-page-load'; 6 | import { getPerformanceEntriesByType } from '../utils/performance-entry'; 7 | import type { AssetsMonitorOptions } from './types'; 8 | 9 | /** 10 | * 资源性能相关监控 11 | * 12 | * @author yuzhanglong 13 | * @date 2021-10-29 23:19:38 14 | */ 15 | export function createAssetsMonitor(options: AssetsMonitorOptions) { 16 | const observerOptions: PerformanceObserverInit = { 17 | entryTypes: [PERFORMANCE_ENTRY_TYPES.RESOURCE], 18 | }; 19 | 20 | const reportAll = (entryList: PerformanceEntry[]) => { 21 | entryList.forEach((entry) => { 22 | options.onReport({ 23 | data: { 24 | timeStamp: Date.now(), 25 | performance: entry, 26 | }, 27 | eventType: EventType.ASSETS, 28 | }); 29 | }); 30 | }; 31 | 32 | let destroy = noop; 33 | 34 | const reportResourceInfoInitiative = () => { 35 | const res = getPerformanceEntriesByType(PERFORMANCE_ENTRY_TYPES.RESOURCE); 36 | reportAll(res); 37 | }; 38 | 39 | // 为什么要基于 onload 后的资源监听 + onload 之前的资源主动获取的模式,而不是直接监听? 40 | // 第一:大量资源加载的发生时机一般都是网页的首屏加载 41 | // 第二:onload 事件会在 DOM 和所有的资源加载完成后触发,如果直接开启监听器必然会影响首屏性能 42 | // 对于 onload 之前的内容,在 onload 回调开始时直接使用 performance.getEntriesByType 获取 43 | onPageLoad(() => { 44 | reportResourceInfoInitiative(); 45 | destroy = observePerformance(observerOptions, (entryList) => { 46 | reportAll(entryList); 47 | }); 48 | }); 49 | 50 | return { 51 | destroy, 52 | }; 53 | } 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "attachments", 3 | "version": "1.0.0", 4 | "private": true, 5 | "workspaces": [ 6 | "packages/*" 7 | ], 8 | "main": "index.js", 9 | "license": "MIT", 10 | "scripts": { 11 | "lint": "eslint --ext .ts,.tsx ./ --fix", 12 | "test": "jest --no-cache", 13 | "build-all": "pnpm -r build", 14 | "clean-all": "lerna clean", 15 | "publish-all": "pnpm clean-all && pnpm i && pnpm build-all && lerna publish --no-push", 16 | "rename-template": "ts-node scripts/add-extension.ts", 17 | "lage": "lage build --profile=./playground/test.json" 18 | }, 19 | "devDependencies": { 20 | "@attachments/eslint-config": "^0.3.1", 21 | "@babel/core": "^7.14.8", 22 | "@babel/preset-env": "^7.15.8", 23 | "@cypress/code-coverage": "^3.9.11", 24 | "@cypress/react": "^5.10.3", 25 | "@cypress/webpack-dev-server": "^1.7.0", 26 | "@cypress/webpack-preprocessor": "^5.9.1", 27 | "@types/faker": "^5.5.9", 28 | "@types/jest": "^27.0.0", 29 | "@types/node": "^16.4.6", 30 | "babel-loader": "^8.2.2", 31 | "cypress": "9.5.1", 32 | "eslint": "^8.26.0", 33 | "faker": "^5.5.3", 34 | "fs-extra": "^10.0.0", 35 | "html-webpack-plugin": "^5.3.2", 36 | "jest": "^27.3.1", 37 | "lerna": "^4.0.0", 38 | "nodemon": "^2.0.12", 39 | "npm-run-all": "^4.1.5", 40 | "react": "^17.0.2", 41 | "react-dom": "^17.0.2", 42 | "rimraf": "^3.0.2", 43 | "stylelint": "^13.13.1", 44 | "stylelint-config-standard": "^22.0.0", 45 | "ts-jest": "^27.0.4", 46 | "ts-node": "^10.1.0", 47 | "typescript": "4.3.5", 48 | "webpack": "^5.65.0", 49 | "webpack-dev-server": "^4.4.0", 50 | "lage": "1.9.5" 51 | }, 52 | "publishConfig": { 53 | "registry": "https://registry.npmjs.org" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/src/utils/calculate-tti.ts: -------------------------------------------------------------------------------- 1 | import { last } from 'lodash'; 2 | import { getPerformance } from './browser-interfaces'; 3 | 4 | interface TTICalculateOptions { 5 | // 起始点 6 | searchStart: number 7 | // 距离最近的,具有至少 2 个网络请求的时间点 8 | lastKnownNetwork2Busy: number 9 | // 检测时间点,该时间位于静默窗口期内 10 | checkTimeInQuiteWindow: number 11 | // 长任务集合 12 | longTasks: { startTime: number; endTime: number }[] 13 | } 14 | 15 | const getDomContentLoadedEventEndTime = () => { 16 | const { timing } = getPerformance(); 17 | 18 | if (timing && timing.domContentLoadedEventEnd) { 19 | const { domContentLoadedEventEnd, navigationStart } = timing; 20 | return domContentLoadedEventEnd - navigationStart; 21 | } 22 | 23 | return null; 24 | }; 25 | 26 | /** 27 | * 计算 TTI 28 | * 29 | * TTI 算法描述如下: 30 | * - 从起始点(一般是 FCP 或者 FMP),向前搜索一个不小于 5s 的静默窗口期 31 | * - 所谓静默窗口期,就是该窗口所对应的时间没有 long task 并且进行中的网络请求数目不超过 2 个 32 | * - 如果没有找到 long task,则起始点就是 TTI 33 | * - 如果 TTI < DOMContentLoadedEventEnd, 则以 DOMContentLoadedEventEnd 作为 TTI 34 | * 35 | * @author yuzhanglong 36 | * @date 2021-09-06 15:31:41 37 | * @param options 见 TTICalculateOptions 38 | * @see TTICalculateOptions 39 | */ 40 | export const calculateTTI = (options: TTICalculateOptions) => { 41 | const { searchStart, longTasks, checkTimeInQuiteWindow, lastKnownNetwork2Busy } = options; 42 | // 确保静默窗口期中没有请求数超过 2 的时刻 43 | if (checkTimeInQuiteWindow - lastKnownNetwork2Busy < 5000) 44 | return null; 45 | 46 | // 如果没有 long task,那么 FCP 时间就是 TTI 时间 47 | const maybeTTI = longTasks.length === 0 ? searchStart : last(longTasks).endTime; 48 | 49 | // 确保窗口期没有 long task 50 | if (checkTimeInQuiteWindow - maybeTTI < 5000) 51 | return null; 52 | 53 | return Math.max(maybeTTI, getDomContentLoadedEventEndTime()); 54 | }; 55 | -------------------------------------------------------------------------------- /packages/proxy/src/certification-manager.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import type { CertificateCreationOptions, CertificateCreationResult } from 'pem'; 3 | import { createCertificate } from 'pem'; 4 | import { CERT, KEY, MAX_DAYS, pathCert, pathCertKey } from './const'; 5 | 6 | interface Certification { 7 | key: string 8 | cert: string 9 | } 10 | 11 | export class CertificationManager { 12 | private rootCertification: Certification; 13 | 14 | private certificationCachedMap = new Map(); 15 | 16 | /** 17 | * 创建 CA 证书 18 | * 19 | * @author yuzhanglong 20 | * @date 2021-08-11 00:28:42 21 | * @param option pem 库关于 ca 的配置 22 | */ 23 | static createCertification(option: CertificateCreationOptions): Promise { 24 | return new Promise((resolve, reject) => { 25 | createCertificate(option, (error, result) => { 26 | if (error) 27 | reject(error); 28 | 29 | resolve(result); 30 | }); 31 | }); 32 | } 33 | 34 | /** 35 | * 基于域名创建一个 CA 证书 36 | * 37 | * @author yuzhanglong 38 | * @date 2021-08-11 00:43:52 39 | * @param domain 域名 40 | */ 41 | async createCertificationByDomain(domain: string): Promise { 42 | if (!this.rootCertification) { 43 | this.rootCertification = { 44 | key: KEY, 45 | cert: CERT, 46 | }; 47 | } 48 | 49 | const certification = await CertificationManager.createCertification({ 50 | altNames: [domain], 51 | commonName: domain, 52 | days: MAX_DAYS, 53 | serviceCertificate: this.rootCertification.cert, 54 | serviceKey: this.rootCertification.key, 55 | }); 56 | 57 | const res = { 58 | key: certification.clientKey, 59 | cert: certification.certificate, 60 | }; 61 | 62 | this.certificationCachedMap.set(domain, res); 63 | 64 | return res; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/src/cls/cls-monitor.ts: -------------------------------------------------------------------------------- 1 | import { PERFORMANCE_ENTRY_TYPES } from '../constants'; 2 | import { EventType } from '../types'; 3 | import { observePerformance } from '../utils/observe-performance'; 4 | import { onPageUnload } from '../utils/on-page-unload'; 5 | import type { ClsMonitorOptions, LayoutShift } from './types'; 6 | 7 | /** 8 | * 累计布局偏移监控 9 | * 10 | * @author yuzhanglong 11 | * @date 2021-08-27 23:17:43 12 | * @param clsMonitorOptions 相关选项 13 | */ 14 | export function createClsMonitor(clsMonitorOptions: ClsMonitorOptions) { 15 | // CLS 是衡量偏移频率的一个指标,它代表整个页面生命周期内发生的所有意外布局偏移中最大连续布局偏移分数。 16 | // 可以参考: 17 | // https://web.dev/cls/#what-is-cls 18 | // https://github.com/mmocny/web-vitals/blob/master/src/getCLS.ts 19 | // https://wicg.github.io/layout-instability/#sec-layout-shift 20 | const observerOptions: PerformanceObserverInit = { 21 | type: PERFORMANCE_ENTRY_TYPES.LAYOUT_SHIFT, 22 | }; 23 | 24 | let clsValue = 0; 25 | const entries: LayoutShift[] = []; 26 | 27 | const destroy = observePerformance(observerOptions, (entryList: LayoutShift[]) => { 28 | for (const entry of entryList) { 29 | // 在用户输入 500 毫秒内发生的布局偏移会带有 hadRecentInput 标志,便于在计算中排除这些偏移。 30 | // 适用于不连续输入事件,如轻触、点击或按键操作。滚动、拖动或捏拉缩放手势等连续性交互操作不算作"最近输入" 31 | // 相关介绍:https://wicg.github.io/layout-instability/#dom-layoutshift-hadrecentinput 32 | if (!entry.hadRecentInput) { 33 | clsValue += entry.value; 34 | entries.push(entry); 35 | } 36 | } 37 | }); 38 | 39 | const getReportData = () => { 40 | return { 41 | data: { 42 | clsValue, 43 | entries, 44 | }, 45 | eventType: EventType.CUMULATIVE_LAYOUT_SHIFT, 46 | }; 47 | }; 48 | 49 | const reportData = () => { 50 | clsMonitorOptions.onReport(getReportData()); 51 | }; 52 | 53 | onPageUnload(() => { 54 | reportData(); 55 | }); 56 | 57 | return { 58 | destroy, 59 | getReportData, 60 | }; 61 | } 62 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/cypress/integration/assets.spec.ts: -------------------------------------------------------------------------------- 1 | import faker from 'faker'; 2 | import { createAssetsMonitor } from '../../src'; 3 | import type { ReportBase } from '../../src/types'; 4 | import type { AssetsReportData } from '../../src/assets/types'; 5 | 6 | const createAndInsertImageTag = () => { 7 | const img = document.createElement('img'); 8 | img.src = faker.image.imageUrl(1, 1, undefined, true); 9 | document.body.appendChild(img); 10 | }; 11 | 12 | describe('test assets monitor', () => { 13 | beforeEach(() => { 14 | // cypress 单测不方便执行 reload,所以两个 it 共享了同一个 window 实例, 15 | // 导致上一个单测的结果也被统计进来了,所以要把所有的 performance 信息全部清空 16 | performance.clearResourceTimings(); 17 | }); 18 | 19 | it('test create images before monitor creation', async () => { 20 | const res = await new Promise[]>((resolve) => { 21 | createAndInsertImageTag(); 22 | createAndInsertImageTag(); 23 | createAndInsertImageTag(); 24 | 25 | const data = []; 26 | createAssetsMonitor({ 27 | onReport: (e) => { 28 | if (e.data.performance.name.includes('placeimg.com')) 29 | data.push(e); 30 | 31 | if (data.length === 3) 32 | resolve(data); 33 | }, 34 | }); 35 | }); 36 | 37 | expect(res.every(item => item.eventType === 'ASSETS')).to.be.true; 38 | }); 39 | 40 | it('test create images after monitor creation', async () => { 41 | const res = await new Promise[]>((resolve) => { 42 | const data = []; 43 | createAssetsMonitor({ 44 | onReport: (e) => { 45 | if (e.data.performance.name.includes('placeimg.com')) 46 | data.push(e); 47 | 48 | if (data.length === 3) 49 | resolve(data); 50 | }, 51 | }); 52 | createAndInsertImageTag(); 53 | createAndInsertImageTag(); 54 | createAndInsertImageTag(); 55 | }); 56 | 57 | expect(res.length).to.equal(3); 58 | 59 | expect(res.every(item => item.eventType === 'ASSETS')).to.be.true; 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/src/utils/compute-last-known-network-2-busy.ts: -------------------------------------------------------------------------------- 1 | import type { TaskTimeInfo } from '../tti/types'; 2 | 3 | interface Endpoint { 4 | timestamp: number 5 | type: 'request-start' | 'request-end' 6 | } 7 | 8 | /** 9 | * 计算当前时间点之前,距离最近的、同时进行的网络请求数量 >= 2 的时机,单位为毫秒 10 | * 11 | * @author yuzhanglong 12 | * @date 2021-09-05 23:49:27 13 | * @param incompleteRequestStarts 已经开始但未结束的网络请求(你可以通过 劫持 XMLHttpRequest 来得到) 14 | * @param observedResourceRequests 已经监听到的网络(资源)请求(你可以通过 Performance API 来得到) 15 | */ 16 | export const computeLastKnownNetwork2Busy = ( 17 | incompleteRequestStarts: number[], 18 | observedResourceRequests: TaskTimeInfo[], 19 | ) => { 20 | // 当前进行的请求超过 2 个,直接返回当前时间 21 | if (incompleteRequestStarts.length > 2) 22 | return performance.now(); 23 | 24 | // endpoints 包含了每个请求的开始时间点和结束时间点,最后会按时间先后顺序排序 25 | const endpoints: Endpoint[] = []; 26 | 27 | for (const { startTime, endTime } of observedResourceRequests) { 28 | endpoints.push({ 29 | timestamp: startTime, 30 | type: 'request-start', 31 | }); 32 | 33 | endpoints.push({ 34 | timestamp: endTime, 35 | type: 'request-end', 36 | }); 37 | } 38 | 39 | for (const incompleteRequestStart of incompleteRequestStarts) { 40 | endpoints.push({ 41 | timestamp: incompleteRequestStart, 42 | type: 'request-start', 43 | }); 44 | } 45 | 46 | endpoints.sort((a, b) => a.timestamp - b.timestamp); 47 | 48 | // 开始【时间倒推】,首先初始化当前活跃的请求数目 49 | // 接下来,如果是请求开始阶段,则将此时活跃的请求数 - 1,反之 + 1 50 | // 如果在某一个时刻 currentActive 的值大于 2,那么这个时候就是当前时间点之前,距离最近的、同时进行的网络请求数量 >= 2 的时机 51 | let currentActive = incompleteRequestStarts.length; 52 | 53 | for (let i = endpoints.length - 1; i >= 0; i -= 1) { 54 | const endpoint = endpoints[i]; 55 | switch (endpoint.type) { 56 | case 'request-start': 57 | currentActive -= 1; 58 | break; 59 | case 'request-end': 60 | currentActive += 1; 61 | if (currentActive > 2) 62 | return endpoint.timestamp; 63 | 64 | break; 65 | default: 66 | return 0; 67 | } 68 | } 69 | return 0; 70 | }; 71 | -------------------------------------------------------------------------------- /packages/github-trending/src/get-github-trending.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import * as cheerio from 'cheerio'; 3 | import type { GetGithubTrendingOptions, Repository } from './types'; 4 | 5 | /** 6 | * 获取 GitHub 趋势 7 | * 8 | * @author https://github.com/ecrmnn/trending-github/blob/master/src/index.ts 9 | * @date 2022-01-06 21:17:24 10 | */ 11 | export const getGithubTrending = async (options: GetGithubTrendingOptions) => { 12 | const { period = 'daily', language, spokenLanguage = 'zh' } = options; 13 | const response = await axios.get( 14 | `https://github.com/trending/${encodeURIComponent( 15 | language, 16 | )}?since=${period}&spoken_language_code=${spokenLanguage}`, 17 | { 18 | headers: { 19 | Accept: 'text/html', 20 | }, 21 | }, 22 | ); 23 | const $ = cheerio.load(response.data); 24 | const repositories: Repository[] = []; 25 | 26 | $('article').each((index, repo) => { 27 | const title = $(repo).find('h1.h3 a').text().replace(/\s/g, ''); 28 | 29 | const author = title.split('/')[0]; 30 | const name = title.split('/')[1]; 31 | 32 | const starLink = `/${title.replace(/ /g, '')}/stargazers`; 33 | const forkLink = `/${title.replace(/ /g, '')}/network/members.${name}`; 34 | 35 | let text: string; 36 | if (period === 'daily') 37 | text = 'stars today'; 38 | else if (period === 'weekly') 39 | text = 'stars this week'; 40 | else 41 | text = 'stars this month'; 42 | 43 | const indexRepo: Repository = { 44 | author, 45 | name, 46 | href: `https://github.com/${author}/${name}`, 47 | description: $(repo).find('p').text().trim() || null, 48 | language: $(repo).find('[itemprop=programmingLanguage]').text().trim(), 49 | stars: parseInt($(repo).find(`[href="${starLink}"]`).text().trim().replace(',', '') || '0', 0), 50 | forks: parseInt($(repo).find(`[href="${forkLink}"]`).text().trim().replace(',', '') || '0', 0), 51 | starsInPeriod: parseInt( 52 | $(repo).find(`span.float-sm-right:contains('${text}')`).text().trim().replace(text, '').replace(',', '') || '0', 53 | 0, 54 | ), 55 | }; 56 | 57 | repositories.push(indexRepo); 58 | }); 59 | return repositories; 60 | }; 61 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/examples/react-app/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/src/assets-error/assets-error-monitor.ts: -------------------------------------------------------------------------------- 1 | import { EventType } from '../types'; 2 | import { getBrowserWindow } from '../utils/browser-interfaces'; 3 | import { getUrlData } from '../utils/get-url-data'; 4 | import { getPerformanceEntriesByName } from '../utils/performance-entry'; 5 | import type { AssetsErrorMonitorOptions } from './types'; 6 | 7 | /** 8 | * 初始化资源异常监听器 9 | * 10 | * @author yuzhanglong 11 | * @date 2021-08-25 21:55:26 12 | * @param options 初始化选项 13 | */ 14 | export const createAssetsErrorMonitor = (options: AssetsErrorMonitorOptions) => { 15 | // 保证 window 存在 16 | const window = getBrowserWindow(); 17 | 18 | if (!window) 19 | return; 20 | 21 | // 从捕获到的 error 时间中筛选出有用的异常信息,如果这个 error 和资源异常无关,我们返回 undefined 22 | const getErrorInfoFromErrorEvent = (e: ErrorEvent) => { 23 | const target = e.target as HTMLElement; 24 | 25 | const tagName = target.tagName; 26 | 27 | const isSourceErrorEvent = target && tagName; 28 | 29 | if (!isSourceErrorEvent) { 30 | // 在通过 window.addEventListener('error') 捕获的同时 31 | // 一些非静态资源造成的异常也被捕获进来 32 | // 而这些异常应该在 js-error-monitor 处理 33 | return; 34 | } 35 | 36 | // 一般情况下资源属性为 src 37 | const srcAttr = target?.getAttribute('src'); 38 | // link 标签(常用于加载 css)的 href 属性会指向资源路径 39 | const hrefAttr = target?.getAttribute('href'); 40 | 41 | return { 42 | tagName, 43 | url: srcAttr || hrefAttr, 44 | }; 45 | }; 46 | 47 | const errorListener = (e: ErrorEvent) => { 48 | const res = getErrorInfoFromErrorEvent(e); 49 | if (!res) 50 | return; 51 | 52 | const { url, tagName } = res; 53 | const urlData = getUrlData(url); 54 | const performance = getPerformanceEntriesByName(urlData.href).pop(); 55 | 56 | const data = { 57 | tagName: tagName.toLowerCase(), 58 | timestamp: Date.now(), 59 | performance, 60 | ...urlData, 61 | }; 62 | 63 | // 上报 64 | options.onReport({ 65 | data, 66 | eventType: EventType.ASSETS_ERROR, 67 | }); 68 | }; 69 | 70 | // 静态资源的异常加载不会冒泡到 window, 要在捕获阶段处理 71 | window.addEventListener('error', errorListener, true); 72 | 73 | const destroy = () => { 74 | window.removeEventListener('error', errorListener, false); 75 | }; 76 | 77 | return { 78 | destroy, 79 | }; 80 | }; 81 | -------------------------------------------------------------------------------- /packages/proxy/src/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * File: utils.ts 3 | * Description: 常用工具函数 4 | * Created: 2021-08-12 15:55:26 5 | * Author: yuzhanglong 6 | * Email: yuzl1123@163.com 7 | */ 8 | 9 | export function removeWWWAndProtocol(url: string) { 10 | const data = url.split(/^https?:\/\/www\.|^www\.|^https?:\/\//); 11 | return data.pop(); 12 | } 13 | 14 | export function getUrlPaths(url: string) { 15 | let u = url; 16 | if (u.endsWith('/')) { 17 | // www.baidu.com/foo/bar/ => www.baidu.com/foo/bar 18 | u = u.slice(0, -1); 19 | } 20 | // www.baidu.com/foo/bar => [www.baidu.com, foo, bar] 21 | const tmp = u.split('/'); 22 | // [www.baidu.com, foo, bar] => [foo, bar] 23 | tmp.splice(0, 1); 24 | return tmp; 25 | } 26 | 27 | /** 28 | * 给定一个 basePath 和一个 providedPath, 判断 providePath 的全段是否为 base 的子集(起始 position 要相同) 29 | * base ['foo', 'bar', 'baz', 'quz'] 30 | * provide ['foo', 'bar', 'baz'] 31 | * return {samePaths:['foo', 'bar', 'baz'], otherPaths: ['quz']} 32 | * @author yuzhanglong 33 | * @date 2021-08-13 00:19:11 34 | */ 35 | export function comparePathAndGetDivision(basePath: string[], providedPath: string[]) { 36 | // provide 的长度大于 base, 肯定匹配不到 37 | if (providedPath.length > basePath.length) { 38 | return { 39 | samePaths: [], 40 | otherPaths: [], 41 | dividedPos: -1, 42 | }; 43 | } 44 | 45 | // provide 的长度为 0,必然可以匹配的到,例如: 46 | // www.baidu.com/foo/bar => [foo, bar] => base path 47 | // / => [] 48 | if (providedPath.length === 0) { 49 | return { 50 | samePaths: [], 51 | otherPaths: basePath.slice(), 52 | dividedPos: 0, 53 | }; 54 | } 55 | 56 | let dividedPos = -1; 57 | for (let i = 0; i < providedPath.length; i += 1) { 58 | if (providedPath[i] === basePath[i]) 59 | dividedPos = i + 1; 60 | else 61 | break; 62 | } 63 | 64 | return { 65 | samePaths: dividedPos === -1 ? [] : basePath.slice(0, dividedPos), 66 | otherPaths: dividedPos === -1 ? [] : basePath.slice(dividedPos), 67 | dividedPos, 68 | }; 69 | } 70 | 71 | /** 72 | * 分离 connect 方法的 url 73 | * 74 | * @author yuzhanglong 75 | * @date 2021-10-18 00:16:10 76 | */ 77 | export const divideConnectMethodReqUrl = (url: string) => { 78 | const [domain, port] = url.split(':'); 79 | return { 80 | domain, 81 | port: port ? parseInt(port, 10) : 443, 82 | }; 83 | }; 84 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/src/mpfid/mpfid-monitor.ts: -------------------------------------------------------------------------------- 1 | import { first } from 'lodash'; 2 | import { observePerformance } from '../utils/observe-performance'; 3 | import { getPerformance, getPerformanceObserver } from '../utils/browser-interfaces'; 4 | import { PERFORMANCE_ENTRY_TYPES } from '../constants'; 5 | import { getPerformanceEntriesByName } from '../utils/performance-entry'; 6 | import { EventType } from '../types'; 7 | import { onPageLoad } from '../utils/on-page-load'; 8 | import type { MPFIDMonitorOptions } from './types'; 9 | 10 | export const MPFID_REPORT_TIMEOUT_AFTER_ONLOAD = 200; 11 | 12 | /** 13 | * 初始化 MPFID 监听器 14 | * 15 | * @author yuzhanglong 16 | * @date 2021-11-11 00:55:37 17 | */ 18 | export const createMPFIDMonitor = (options: MPFIDMonitorOptions) => { 19 | if (!getPerformance() || !getPerformanceObserver()) 20 | return; 21 | 22 | const longTaskEntries: PerformanceEntry[] = []; 23 | 24 | const performanceOptions: PerformanceObserverInit = { 25 | entryTypes: [PERFORMANCE_ENTRY_TYPES.LONG_TASK], 26 | }; 27 | 28 | const destroy = observePerformance(performanceOptions, (entryList) => { 29 | entryList.forEach((entry) => { 30 | longTaskEntries.push(entry); 31 | }); 32 | }); 33 | 34 | // MPFID 衡量从用户首次与您的网站交互(例如单击按钮)到浏览器实际能够响应该交互的时间。 35 | // 通过查找 First Contentful Paint 之后最长任务的持续时间来计算 Max Potential FID。 36 | // First Contentful Paint 之前的任务被排除在外, 37 | // 因为用户不太可能在任何内容呈现到屏幕之前尝试与您的页面进行交互 38 | // 而这正是 First Contentful Paint 所衡量的。 39 | const getMPFID = () => { 40 | destroy(); 41 | 42 | const fcp = first(getPerformanceEntriesByName(PERFORMANCE_ENTRY_TYPES.PAINT)) || 0; 43 | 44 | return longTaskEntries.reduce((res, entry) => { 45 | const { duration, startTime } = entry; 46 | const isBeforeFCP = startTime < fcp; 47 | return res < duration && !isBeforeFCP ? duration : res; 48 | }, 0); 49 | }; 50 | 51 | onPageLoad(async () => { 52 | await new Promise((resolve) => { 53 | // 为提高性能,如果 onload 事件后 0.2s 还没有 FID, 54 | // 终止监听, 并将此时作为一个粗略的 FID 55 | // 因为在绝大部分情况下 onload 事件结束之前用户应该已经开始和页面交互 56 | onPageLoad(() => { 57 | setTimeout(() => { 58 | resolve(true); 59 | }, options.timeout || MPFID_REPORT_TIMEOUT_AFTER_ONLOAD); 60 | }); 61 | }); 62 | 63 | options.onReport({ 64 | eventType: EventType.MPFID, 65 | data: { 66 | mpfid: getMPFID(), 67 | }, 68 | }); 69 | }); 70 | }; 71 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/src/fmp/fmp-monitor.ts: -------------------------------------------------------------------------------- 1 | import { EventType } from '../types'; 2 | import { useRequestAnimationFrame } from '../utils/use-request-animation-frame'; 3 | import { getDomLayoutScore } from '../utils/get-dom-layout-score'; 4 | import { getMutationObserver } from '../utils/browser-interfaces'; 5 | import { onPageLoad } from '../utils/on-page-load'; 6 | import type { FMPMonitorOptions } from './types'; 7 | 8 | interface FMPRecodeData { 9 | time: number 10 | domScore: number 11 | } 12 | 13 | export const calculateFMP = (scoredData: FMPRecodeData[]) => { 14 | // 首先将打分结果基于时间排序(时间戳从小到大) 15 | scoredData.sort((a, b) => a.time - b.time); 16 | 17 | // 计算每两个时间戳之间的得分差值,变动最大的即为最终结果 18 | const initInfoValue = { 19 | maxDelta: -1, 20 | time: -1, 21 | prev: { 22 | time: 0, 23 | domScore: 0, 24 | } as FMPRecodeData, 25 | }; 26 | 27 | const res = scoredData.reduce((info, curr) => { 28 | const delta = curr.domScore - info.prev.domScore; 29 | if (delta > info.maxDelta) { 30 | info.maxDelta = delta; 31 | info.time = curr.time; 32 | } 33 | info.prev = curr; 34 | return info; 35 | }, initInfoValue); 36 | 37 | return res; 38 | }; 39 | 40 | export const createFMPMonitor = (options: FMPMonitorOptions) => { 41 | const MutationObserver = getMutationObserver(); 42 | 43 | if (!MutationObserver) 44 | return; 45 | 46 | const startTime = Date.now(); 47 | 48 | const scoredData: FMPRecodeData[] = []; 49 | 50 | const observeFMP = () => { 51 | const callback = useRequestAnimationFrame(() => { 52 | const bodyScore = getDomLayoutScore(document.body, 1, false, options.exact); 53 | scoredData.push({ 54 | domScore: bodyScore, 55 | time: Date.now() - startTime, 56 | }); 57 | }); 58 | 59 | const observer = new MutationObserver(() => { 60 | callback.runCallback(); 61 | }); 62 | 63 | observer.observe(document.body, { 64 | subtree: true, 65 | childList: true, 66 | }); 67 | 68 | return observer; 69 | }; 70 | 71 | const observer = observeFMP(); 72 | 73 | const reportData = () => { 74 | options.onReport({ 75 | data: { 76 | fmp: calculateFMP(scoredData).time, 77 | }, 78 | eventType: EventType.FMP, 79 | }); 80 | observer.disconnect(); 81 | }; 82 | 83 | // FMP 和 onload 事件并不密切相关,但它很可能在 onload 事件附近,所以我们延时一小段时间再报告 84 | onPageLoad(() => { 85 | setTimeout(() => { 86 | reportData(); 87 | }, 1000); 88 | }); 89 | }; 90 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/cypress/integration/paint.spec.ts: -------------------------------------------------------------------------------- 1 | import { createPaintMonitor } from '../../src'; 2 | import { promisifyCounterMonitorReport } from '../utils/test-utils'; 3 | import type { LargestContentfulPaintReportData, PaintReportData } from '../../src/paint/types'; 4 | import type { CallBack } from '../../src/types'; 5 | 6 | const createMonitor = (cb: CallBack, times: number) => 7 | promisifyCounterMonitorReport({ 8 | monitorFactory: createPaintMonitor, 9 | afterCreateMonitorCallback: cb, 10 | reportTimesBeforeResolve: times, 11 | }); 12 | 13 | createPaintMonitor({ 14 | onReport: (e) => { 15 | console.log(e); 16 | }, 17 | }); 18 | describe('test assets monitor', () => { 19 | it('test FP、FCP and LCP', async () => { 20 | const el = document.createElement('div'); 21 | document.body.appendChild(el); 22 | 23 | let smallElement = null; 24 | let largestElement = null; 25 | 26 | const insertSmallElement = () => { 27 | smallElement = document.createElement('div'); 28 | smallElement.style.width = '100px'; 29 | smallElement.style.height = '100px'; 30 | smallElement.style.backgroundColor = '#9375de'; 31 | smallElement.innerHTML = 'I\'m the small element'; 32 | el.appendChild(smallElement); 33 | }; 34 | 35 | const insertLargestElement = () => { 36 | largestElement = document.createElement('div'); 37 | largestElement.style.width = '300px'; 38 | largestElement.style.height = '300px'; 39 | largestElement.style.backgroundColor = '#409eff'; 40 | largestElement.innerHTML = 'I\'m the largest element!I\'m the largest element!I\'m the largest element!I\'m the largest element!'; 41 | el.appendChild(largestElement); 42 | }; 43 | 44 | // 首先插入一个小的 element,尝试触发第一次 LCP 45 | insertSmallElement(); 46 | 47 | const [p, lcp, lcp2] = await createMonitor(() => { 48 | // 在插入一个大的 element,触发第一次 LCP 49 | setTimeout(() => { 50 | insertLargestElement(); 51 | }, 2000); 52 | }, 3); 53 | expect(p.eventType).to.equal('PAINT'); 54 | expect(lcp.eventType).to.equal('LARGEST_CONTENTFUL_PAINT'); 55 | expect(lcp2.eventType).to.equal('LARGEST_CONTENTFUL_PAINT'); 56 | 57 | const lcp1Data = lcp.data.largestContentfulPaint; 58 | const lcp2Data = lcp2.data.largestContentfulPaint; 59 | 60 | // 第一次触发 lcp 的元素 61 | expect(lcp1Data.element).to.equal(smallElement); 62 | 63 | // 第二次触发 lcp 的元素 64 | expect(lcp2Data.element).to.equal(largestElement); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /packages/proxy/README.md: -------------------------------------------------------------------------------- 1 | # @attachments/proxy 2 | 3 | 为前端开发者准备的代理服务器 4 | 5 | ## 快速开始 6 | 7 | **安装并配置浏览器插件** 8 | 9 | SwitchyOmega 是一个知名的浏览器代理工具,它可以将浏览器的 http 请求全部重定向到你本地的代理工具上(只负责请求重定向,其本身没有代理功能)。 10 | 11 | [下载地址](https://github.com/FelisCatus/SwitchyOmega/releases/tag/v2.5.20) (GitHub Release) 12 | 13 | 插件安装完成之后,打开设置页面,新建情景模式,按照如下的方法配置,确认无误后保存,不代理的地址列表保持默认即可: 14 | 15 | ![插件使用](https://user-images.githubusercontent.com/56540811/137702382-1ec43265-4b35-4d48-8154-09bb0b65bef4.png) 16 | 17 | **安装依赖** 18 | 19 | ```bash 20 | yarn add @attachments/proxy 21 | 22 | # or npm install @attachments/proxy 23 | ``` 24 | 25 | **编写规则** 26 | 27 | 在工作目录下新建一个 `proxy.js` 文件(名称随意),使用下面的代码: 28 | 29 | ```typescript 30 | import { ProxyServer } from '@attachments/proxy'; 31 | 32 | async function runApp() { 33 | const server = new ProxyServer(); 34 | server.addRule( 35 | 'proxy.yuzzl.top', 36 | { 37 | location: '/', 38 | // 当访问 proxy.yuzzl.top 时,代理到 http://localhost:8001 39 | proxyPass: 'http://localhost:8001', 40 | }, 41 | { 42 | // 当访问 proxy.yuzzl.top/hello/world/xxx 时,代理到 http://localhost:8001/hello_world/xxx 43 | location: '/hello/world', 44 | proxyPass: 'http://localhost:8001/hello_world', 45 | }, 46 | ); 47 | 48 | await server.initServers(); 49 | await server.listen(); 50 | } 51 | 52 | runApp().catch(e => { 53 | console.log(e); 54 | }); 55 | ``` 56 | 57 | 启动项目 58 | 59 | ```bash 60 | node proxy.js 61 | ``` 62 | 63 | 接下来: 64 | 65 | - 当你访问 `proxy.yuzzl.top`,会将请求代理到 `http://localhost:8001` 66 | 67 | - 当你访问 `proxy.yuzzl.top/hello/world/xxx`,会将请求代理到 `http://localhost:8001/hello_world/xxx` 68 | 69 | 70 | ## 常见问题 71 | 72 | ### HTTPS 环境下浏览器提示不安全 73 | 74 | proxy 内置了一个根证书,位于 package 的 `certificate` 目录下,你需要让你的计算机信任 `rootCA.pem`: 75 | 76 | #### macOS 系统 77 | 78 | - 请在 Finder 中打开该目录,可以进入 node_modules 找到它。 79 | 80 | ![image](https://user-images.githubusercontent.com/56540811/137639025-b333694d-5980-4aaf-a069-46a68ec4e46a.png) 81 | 82 | 83 | - 双击 `rootCA.pem`,将证书导入到钥匙串,如图所示: 84 | 85 | ![image](https://user-images.githubusercontent.com/56540811/137638859-a3b8c2d3-9f72-4a1e-ad0d-019cc7100375.png) 86 | 87 | - 双击该证书,打开信任选项,设置为始终信任。 88 | 89 | ![image](https://user-images.githubusercontent.com/56540811/137638923-d2cbe734-68c0-4505-917e-428e71469976.png) 90 | 91 | #### window 系统 92 | 93 | 本人无 window 电脑,待补充... 94 | 95 | ### 它是如何工作的? 96 | 97 | 请参考《HTTP 权威指南》的如下内容: 98 | 99 | - 第十四章第九节:通过代理以隧道形式传输安全流量 100 | - 中间人攻击 101 | - 第六章:代理 102 | 103 | 具体实现原理不久我会写一篇文章来讲解 104 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/examples/react-app/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/cypress/integration/xhr.spec.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import faker from 'faker'; 3 | import type { CallBack } from '../../src/types'; 4 | import { promisifyCounterMonitorReport } from '../utils/test-utils'; 5 | import { createXHRMonitor } from '../../src'; 6 | import type { XHRReportData } from '../../src/xhr/types'; 7 | 8 | const runMonitor = async (cb: CallBack, t?: number) => 9 | promisifyCounterMonitorReport({ 10 | afterCreateMonitorCallback: cb, 11 | monitorFactory: createXHRMonitor, 12 | reportTimesBeforeResolve: t, 13 | }); 14 | describe('xhr monitor', () => { 15 | it('test xhr error request (GET)', async () => { 16 | const fakeUrl = faker.image.imageUrl(); 17 | 18 | // 一个不支持直接通过 xhr 完成请求的网址 19 | const [res] = await runMonitor(async () => { 20 | try { 21 | await axios.get(fakeUrl, { 22 | timeout: 2000, 23 | }); 24 | } 25 | catch (e) { 26 | console.log(e); 27 | } 28 | }); 29 | 30 | expect(res.eventType).to.equal('XHR'); 31 | expect(res.data.performance).to.be.undefined; 32 | expect(res.data.duration >= 0).to.be.true; 33 | expect(res.data.request.href).to.equal(fakeUrl); 34 | expect(res.data.response.status).to.equal(-1); 35 | }); 36 | 37 | it('test xhr success request', async () => { 38 | const fakeUrl = 'https://disease.sh/v3/covid-19/historical/all?lastdays=all'; 39 | 40 | // 一个不支持直接通过 xhr 完成请求的网址 41 | const [res] = await runMonitor(async () => { 42 | try { 43 | await axios.get(fakeUrl); 44 | } 45 | catch (e) { 46 | console.log(e); 47 | } 48 | }); 49 | 50 | expect(res.eventType).to.equal('XHR'); 51 | expect(!!res.data.performance).to.be.true; 52 | expect(res.data.duration >= 0).to.be.true; 53 | expect(res.data.request.href).to.equal(fakeUrl); 54 | }); 55 | 56 | it('test xhr success request more than one', async () => { 57 | const fakeUrl = 'https://disease.sh/v3/covid-19/historical/all?lastdays=all'; 58 | 59 | // 一个不支持直接通过 xhr 完成请求的网址 60 | const res = await runMonitor(async () => { 61 | try { 62 | await Promise.all([axios.get(fakeUrl), axios.get(fakeUrl), axios.get(fakeUrl)]); 63 | } 64 | catch (e) { 65 | console.log(e); 66 | } 67 | }, 2); 68 | 69 | expect(res.length).to.equal(2); 70 | expect(res.every(item => item.eventType === 'XHR')).to.be.true; 71 | expect(res.every(item => item.data.request.href === fakeUrl)).to.be.true; 72 | expect(res.every(item => item.data.response.status === 200)).to.be.true; 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/cypress/integration/fetch.spec.ts: -------------------------------------------------------------------------------- 1 | import type { CallBack } from '../../src/types'; 2 | import { promisifyCounterMonitorReport } from '../utils/test-utils'; 3 | import { createFetchMonitor } from '../../src/fetch/fetch-monitor'; 4 | import type { FetchReportData } from '../../src/fetch/types'; 5 | 6 | const runMonitor = async (cb: CallBack, times?: number) => 7 | promisifyCounterMonitorReport({ 8 | afterCreateMonitorCallback: cb, 9 | monitorFactory: createFetchMonitor, 10 | reportTimesBeforeResolve: times, 11 | }); 12 | 13 | describe('test fetch API(200 code)', () => { 14 | it('test fetch (GET)', async () => { 15 | const [res] = await runMonitor(() => { 16 | fetch('https://disease.sh/v3/covid-19/historical/all?lastdays=all', { 17 | method: 'get', 18 | headers: { 19 | 'Content-type': 'application/x-www-form-urlencoded; charset=UTF-8', 20 | }, 21 | }) 22 | .then((res) => { 23 | return res.json(); 24 | }) 25 | .then((res) => { 26 | console.log(res); 27 | }); 28 | }); 29 | 30 | expect(res.eventType).to.equal('FETCH'); 31 | expect(res.data.response.status).to.equal(200); 32 | expect(!!res.data.performance).to.be.true; 33 | // 只有400+的请求才会触发 34 | expect(res.data.response.body).to.be.null; 35 | }); 36 | 37 | it('test fetch error(404 code)', async () => { 38 | const [res] = await runMonitor(() => { 39 | fetch('https://disease.sh/v3/covid-19/historical/all?lastdays=all', { 40 | method: 'post', 41 | body: 'a=1&b=2', 42 | }); 43 | }); 44 | 45 | expect(res.eventType).to.equal('FETCH'); 46 | expect(res.data.response.status).to.equal(404); 47 | expect(!!res.data.performance).to.be.true; 48 | // 只有400+的请求才会触发 49 | expect(!!res.data.response.body).to.be.true; 50 | }); 51 | 52 | it('test success request more than one', async () => { 53 | const res = await runMonitor(() => { 54 | Promise.all([ 55 | fetch('https://disease.sh/v3/covid-19/historical/all?lastdays=all', { 56 | method: 'get', 57 | }), 58 | fetch('https://disease.sh/v3/covid-19/historical/all?lastdays=all', { 59 | method: 'get', 60 | }), 61 | fetch('https://disease.sh/v3/covid-19/historical/all?lastdays=all', { 62 | method: 'get', 63 | }), 64 | ]); 65 | }, 3); 66 | 67 | expect(res.length).to.equal(3); 68 | expect(res.every(item => item.eventType === 'FETCH')).to.be.true; 69 | expect(res.every(item => item.data.response.status === 200)).to.be.true; 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/src/fetch/fetch-monitor.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 初始化 fetch 监听器 3 | * 4 | * @author yuzhanglong 5 | * @date 2021-11-14 12:08:25 6 | */ 7 | import { isString } from 'lodash'; 8 | import { patchMethod } from '../utils/patch-method'; 9 | import { getBrowserWindow } from '../utils/browser-interfaces'; 10 | import { EventType } from '../types'; 11 | import { getRequestReportData } from '../utils/get-request-report-data'; 12 | import type { FetchMonitorOptions } from './types'; 13 | 14 | export const createFetchMonitor = (options: FetchMonitorOptions) => { 15 | const browserWindow = getBrowserWindow(); 16 | if (!browserWindow) 17 | return; 18 | 19 | // 获取 fetch 入参的 url 字符串 20 | const parseFetchUrl = (requestInfo: RequestInfo) => { 21 | if (isString(requestInfo)) 22 | return requestInfo; 23 | 24 | return requestInfo.url; 25 | }; 26 | 27 | patchMethod(browserWindow, 'fetch', (origin: typeof window['fetch']) => { 28 | return async (requestInfo, requestInit) => { 29 | // 先让浏览器开启线程去执行网络请求,提高性能 30 | const fetchPromise = origin(requestInfo, requestInit); 31 | 32 | // 即将被上报的数据 33 | const initData = { 34 | // REQUEST OPTIONS 35 | url: parseFetchUrl(requestInfo), 36 | method: requestInit?.method || 'GET', 37 | requestData: requestInit?.body, 38 | requestHeaders: requestInit?.headers as Record, 39 | startTime: Date.now(), 40 | 41 | // RESPONSE OPTIONS 42 | responseData: undefined, 43 | responseHeaders: undefined, 44 | responseUrl: '', 45 | status: -1, 46 | }; 47 | 48 | const reportData = () => { 49 | // fetch 的 Performance API 收集存在一定延迟,这里需要延迟一秒再上报 50 | setTimeout(() => { 51 | options.onReport({ 52 | eventType: EventType.FETCH, 53 | data: getRequestReportData(initData), 54 | }); 55 | }, 1000); 56 | }; 57 | 58 | const onResolve = async (res: Response) => { 59 | try { 60 | // 只有 4xx 以上的错误才会收集响应体 61 | let data = ''; 62 | if (res.status >= 400) 63 | data = await res.clone().text(); 64 | 65 | initData.responseUrl = res.url; 66 | initData.responseHeaders = res.headers; 67 | initData.status = res.status || -1; 68 | initData.responseData = data; 69 | reportData(); 70 | } 71 | catch (e) { 72 | console.error(e); 73 | } 74 | }; 75 | 76 | const onReject = () => { 77 | reportData(); 78 | }; 79 | 80 | fetchPromise.then(onResolve, onReject); 81 | 82 | return fetchPromise; 83 | }; 84 | })(); 85 | }; 86 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/src/utils/get-dom-layout-score.ts: -------------------------------------------------------------------------------- 1 | import { isFunction } from 'lodash'; 2 | 3 | type HTMLElementWithCss = HTMLElement & { 4 | readonly style?: CSSStyleDeclaration 5 | }; 6 | 7 | // 需要忽略的功能性标签 8 | export const IGNORE_TAGS = ['SCRIPT', 'STYLE', 'META', 'HEAD']; 9 | 10 | /** 11 | * 递归地获取 DOM 布局分数, 该分数体现了某个节点的复杂程度 12 | * 注:不在视口中的子元素不会被考虑 13 | * 14 | * @author yuzhanglong 15 | * @date 2021-11-07 11:58:16 16 | * @param element 根 dom 元素 17 | * @param depth 当前元素的深度 18 | * @param isSiblingExists 符合标准的(在视口中)的兄弟节点是否存在 19 | * @param exact 是否开启精确模式,如果开启,则还会验证元素的宽度和 css 样式属性,确保不在视口内,这可能会影响性能,默认为 false 20 | * @param onGetScore 在获取得分之后做些什么(使用者可忽略此 API,主要用于单测方便查看效果) 21 | */ 22 | export const getDomLayoutScore = ( 23 | element: HTMLElementWithCss, 24 | depth: number, 25 | isSiblingExists: boolean, 26 | exact?: boolean, 27 | onGetScore?: (element: HTMLElementWithCss, score: number, depth: number, isPositionCheckNeeded: boolean) => void, 28 | ) => { 29 | const { tagName, children } = element; 30 | 31 | if (!element || IGNORE_TAGS.includes(tagName)) 32 | return 0; 33 | 34 | const childNodes = Array.from(children || []) as HTMLElementWithCss[]; 35 | 36 | const childrenScore = childNodes.reduceRight((siblingScore, currentNode) => { 37 | // 如果它的右子树兄弟分数存在,则无需计算 dom 位置 38 | const score = getDomLayoutScore(currentNode, depth + 1, siblingScore > 0, exact, onGetScore); 39 | return siblingScore + score; 40 | }, 0); 41 | 42 | // 如果有必要的话,会对该元素的位置进行 check 43 | // 需要满足的条件:1. 它的相邻兄弟节点没有分数 2. 它不是叶子节点 44 | const isPositionCheckNeeded = childrenScore <= 0 && !isSiblingExists; 45 | 46 | if (isPositionCheckNeeded) { 47 | if (!isFunction(element.getBoundingClientRect)) 48 | return 0; 49 | 50 | const { top, height, width } = element.getBoundingClientRect(); 51 | 52 | // 这个 dom 元素是否可见,如果不可见那么这个元素对我们的 fmp 没有影响 53 | // https://docs.google.com/document/d/1BR94tJdZLsin5poeet0XoTW60M0SjvOJQttKT-JK8HI/view# 54 | // 主要包括:元素顶部位置是否在视口之下 55 | // 宽度是否小于 0,visibility 是否为 hidden 按理说也应该被考虑进去 56 | // 但是基于性能考虑默认尽可能忽略它们(在实际应用中这样的 DOM 元素应该也很少碰到,但也提供了 exact 选项进行判断) 57 | const isUnderView = top > window.innerHeight; 58 | let isNotVisible: boolean; 59 | 60 | if (!exact) 61 | isNotVisible = height <= 0; 62 | else 63 | isNotVisible = height <= 0 || width <= 0 || element.style.visibility === 'hidden'; 64 | 65 | const isElementOutOfView = isUnderView || isNotVisible; 66 | if (isElementOutOfView) 67 | return 0; 68 | } 69 | 70 | const score = childrenScore + 1 + 0.5 * depth; 71 | 72 | if (isFunction(onGetScore)) 73 | onGetScore(element, score, depth, isPositionCheckNeeded); 74 | 75 | return score; 76 | }; 77 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/cypress/utils/test-utils.ts: -------------------------------------------------------------------------------- 1 | import { noop } from 'lodash'; 2 | import type { CallBack, ReportBase } from '../../src/types'; 3 | import { MonitorOptions } from '../../src/types'; 4 | import { MPFIDMonitorOptions } from '../../src/mpfid/types'; 5 | 6 | interface PromisifyMonitorReportOptions { 7 | // monitor 的工厂函数 8 | monitorFactory: CallBack 9 | 10 | // 在 monitor 创建后做些什么 11 | afterCreateMonitorCallback?: CallBack 12 | 13 | // 在 onReport 方法调用多少次后将 promise resolve 14 | reportTimesBeforeResolve?: number 15 | } 16 | 17 | /** 18 | * 将一个 monitor 实例 promisify 19 | * 20 | * @author yuzhanglong 21 | * @date 2021-10-30 20:57:11 22 | * @param options 相关选项,见上面代码 23 | * @param monitorOptions 监控程序配置 24 | */ 25 | export const promisifyCounterMonitorReport = ( 26 | options: PromisifyMonitorReportOptions, 27 | monitorOptions?: Record, 28 | ) => { 29 | const { monitorFactory, afterCreateMonitorCallback = noop, reportTimesBeforeResolve = 1 } = options; 30 | return new Promise[]>((resolve) => { 31 | const reportData = []; 32 | 33 | // 如果次数为 0 直接 resolve 即可 34 | if (reportTimesBeforeResolve === 0) 35 | resolve([]); 36 | 37 | monitorFactory({ 38 | onReport: (e) => { 39 | reportData.push(e); 40 | if (reportData.length >= reportTimesBeforeResolve) 41 | resolve(reportData.slice()); 42 | }, 43 | ...(monitorOptions || {}), 44 | }); 45 | afterCreateMonitorCallback(); 46 | }); 47 | }; 48 | 49 | /** 50 | * cypress 在一个 spec 中有两个 window(即两个 iframe),一个被称为 spec window,一个被称为 app window 51 | * 52 | * 在 spec 中的代码的 window.xxx 会指向 spec window,而这个 window 是不可见的,在我们计算 fp/fcp 是会拿不到相应的值 53 | * 所以我们要修改对应 iframe 的样式,让其可见。 54 | * 55 | * @author yuzhanglong 56 | * @date 2021-10-31 21:02:48 57 | * @deprecated 由于使用了 component 模式,废弃之 58 | */ 59 | export const initSpecWindow = () => { 60 | // 由于不是跨域的,可以直接通过 parent 拿到父亲节点 window 61 | const data = parent.window.document; 62 | // 插入一个 style 标签,给予 visibility 属性为 initial 63 | const el = document.createElement('style'); 64 | el.setAttribute('type', 'text/css'); 65 | el.innerHTML = '.spec-iframe{height: 100% !important;width: 100% !important;visibility: initial !important;background-color: #ffffff !important}'; 66 | data.body.appendChild(el); 67 | 68 | const testContainer = document.createElement('div'); 69 | testContainer.innerHTML = ''; 70 | testContainer.id = 'spec-container'; 71 | document.body.appendChild(testContainer); 72 | 73 | const resetContainer = () => { 74 | testContainer.innerHTML = ''; 75 | }; 76 | 77 | const getContainer = () => { 78 | return testContainer; 79 | }; 80 | 81 | return { 82 | resetContainer, 83 | getContainer, 84 | }; 85 | }; 86 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/src/utils/observe-incoming-requests.ts: -------------------------------------------------------------------------------- 1 | import type { PatchedXMLHttpRequest } from '../tti/types'; 2 | import { patchMethod } from './patch-method'; 3 | 4 | /** 5 | * 使用劫持方式监听正在进行中的请求 6 | * 7 | * @author yuzhanglong 8 | * @date 2021-09-06 17:59:14 9 | * @return incomingRequests 一个对象,唯一的 key 代表某个进行中的请求 10 | * 如果是 **XMLHttpRequest**, 为偶数,如果是 **fetch** 则为奇数,对应的 value 为这次请求开始的时间戳 11 | * @return getIncomingRequestsTimes 基于所有 incomingRequests 的 value 合并得到的一个数组 12 | */ 13 | export const observeIncomingRequests = () => { 14 | const incomingRequests: Record = {}; 15 | 16 | // 监听 XMLHttpRequest open 方法 17 | patchMethod(XMLHttpRequest.prototype, 'open', (origin: XMLHttpRequest['open']) => { 18 | return function (this: PatchedXMLHttpRequest, ...args: Parameters) { 19 | const [method] = args; 20 | this.taggedMethod = method; 21 | return origin.apply(this, args); 22 | } as any; 23 | })(); 24 | 25 | // 监听 XMLHttpRequest send 方法 26 | patchMethod(XMLHttpRequest.prototype, 'send', (origin) => { 27 | let uniqueId = 0; 28 | return function (this: PatchedXMLHttpRequest, ...args: Parameters) { 29 | if (this.taggedMethod !== 'GET') 30 | return origin.apply(this, args); 31 | 32 | // uniqueId 为偶数 33 | uniqueId += 2; 34 | const reqId = uniqueId; 35 | 36 | incomingRequests[reqId] = Date.now(); 37 | 38 | patchMethod(this, 'onreadystatechange', (origin) => { 39 | return function (e) { 40 | origin.call(this, e); 41 | if (this.readyState === XMLHttpRequest.DONE) { 42 | // 从【进行中】表中移除 43 | delete incomingRequests[reqId]; 44 | } 45 | }; 46 | })(); 47 | 48 | return origin.apply(this, args); 49 | }; 50 | })(); 51 | 52 | // 监听 window.fetch 方法 53 | patchMethod(window, 'fetch', (origin: typeof window['fetch']) => { 54 | let uniqueId = 0; 55 | return function (...args: Parameters) { 56 | const [request, init] = args; 57 | const method = (request as Request)?.method || init.method; 58 | if (method !== 'GET') 59 | return origin(...args); 60 | 61 | return new Promise((resolve, reject) => { 62 | uniqueId += 2; 63 | const reqId = uniqueId; 64 | incomingRequests[reqId] = Date.now(); 65 | 66 | origin(...args) 67 | .then((value) => { 68 | delete incomingRequests[reqId]; 69 | resolve(value); 70 | }) 71 | .catch((e) => { 72 | delete incomingRequests[reqId]; 73 | reject(e); 74 | }); 75 | }); 76 | }; 77 | })(); 78 | 79 | const getIncomingRequestsTimes = (): number[] => { 80 | const entries = Object.entries(incomingRequests); 81 | return entries.map((res) => { 82 | return res[1]; 83 | }); 84 | }; 85 | 86 | return { 87 | incomingRequests, 88 | getIncomingRequestsTimes, 89 | }; 90 | }; 91 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/examples/react-app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 | 57 | 58 | 59 | 60 |
61 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /packages/monitor/monitor-sdk-browser/src/tti/tti-monitor.ts: -------------------------------------------------------------------------------- 1 | import { last } from 'lodash'; 2 | import { getPerformance, getPerformanceObserver, getXMLHttpRequest } from '../utils/browser-interfaces'; 3 | import { getPerformanceEntriesByName } from '../utils/performance-entry'; 4 | import { EventType } from '../types'; 5 | import { createScheduler } from '../utils/create-scheduler'; 6 | import { calculateTTI } from '../utils/calculate-tti'; 7 | import { computeLastKnownNetwork2Busy } from '../utils/compute-last-known-network-2-busy'; 8 | import { observeIncomingRequests } from '../utils/observe-incoming-requests'; 9 | import { observeLongTaskAndResources } from '../utils/observe-long-task-and-resources'; 10 | import type { TTIMonitorOptions, TaskTimeInfo } from './types'; 11 | 12 | const TIME_GAP = 5000; 13 | 14 | export const getFCP = () => { 15 | const fcp = getPerformanceEntriesByName('first-contentful-paint')[0]; 16 | return fcp ? fcp.startTime : 0; 17 | }; 18 | 19 | /** 20 | * 上报 tti 21 | * 22 | * @author yuzhanglong 23 | * @date 2021-09-06 17:56:44 24 | * @param options 选项 25 | * @param lastKnownNetwork2Busy 26 | * @param longTasks 27 | */ 28 | const checkAndReportTTI = (options: TTIMonitorOptions, lastKnownNetwork2Busy: number, longTasks: TaskTimeInfo[]) => { 29 | const searchStartTime = getFCP(); 30 | const tti = calculateTTI({ 31 | searchStart: searchStartTime, 32 | checkTimeInQuiteWindow: performance.now(), 33 | longTasks, 34 | lastKnownNetwork2Busy, 35 | }); 36 | 37 | options.onReport({ 38 | eventType: EventType.TTI, 39 | data: { 40 | tti, 41 | }, 42 | }); 43 | }; 44 | 45 | export const createTTIMonitor = (options: TTIMonitorOptions) => { 46 | const XMLHttpRequest = getXMLHttpRequest(); 47 | const performanceObserver = getPerformanceObserver(); 48 | const performance = getPerformance(); 49 | 50 | if (!XMLHttpRequest || !performanceObserver || !performance) 51 | return; 52 | 53 | const longTasks: TaskTimeInfo[] = []; 54 | const networkRequests: TaskTimeInfo[] = []; 55 | 56 | const ttiCalculatorScheduler = createScheduler(); 57 | const { getIncomingRequestsTimes } = observeIncomingRequests(); 58 | 59 | const getLastKnownNetworkBusy = () => { 60 | return computeLastKnownNetwork2Busy(getIncomingRequestsTimes(), networkRequests); 61 | }; 62 | 63 | // 监听 long task 和 network resource 64 | observeLongTaskAndResources( 65 | (timeInfo) => { 66 | // 在 long task 5 秒 后尝试获取 tti 67 | longTasks.push(timeInfo); 68 | ttiCalculatorScheduler.resetScheduler(timeInfo.endTime + TIME_GAP); 69 | }, 70 | (timeInfo) => { 71 | networkRequests.push(timeInfo); 72 | // 遇到资源请求,在最后一次请求数大于 2 的时刻五秒后尝试获取 tti 73 | ttiCalculatorScheduler.resetScheduler(getLastKnownNetworkBusy() + TIME_GAP); 74 | }, 75 | ); 76 | 77 | const checkAndReport = () => { 78 | checkAndReportTTI(options, getLastKnownNetworkBusy(), longTasks); 79 | }; 80 | 81 | const lastLongTask = last(longTasks)?.endTime || 0; 82 | 83 | ttiCalculatorScheduler.startSchedule(checkAndReport, Math.max(getLastKnownNetworkBusy() + TIME_GAP, lastLongTask)); 84 | }; 85 | -------------------------------------------------------------------------------- /packages/proxy/__tests__/rule-manager.spec.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { RuleManager } from '../src/rule-manager'; 3 | 4 | describe('test rule manager', () => { 5 | const ruleManager = new RuleManager(); 6 | 7 | beforeEach(() => { 8 | ruleManager.clear(); 9 | }); 10 | 11 | test('rule configurations match', () => { 12 | ruleManager.addRule('baidu.com', { 13 | proxyPass: 'yzl.top', 14 | location: '/', 15 | }); 16 | 17 | expect(ruleManager.matchRuleConfigurations('www.baidu.com')).toBeTruthy(); 18 | expect(ruleManager.matchRuleConfigurations('baidu.com')).toBeTruthy(); 19 | // 二级域名 20 | expect(ruleManager.matchRuleConfigurations('foo.baidu.com')).toBeFalsy(); 21 | expect(ruleManager.matchRuleConfigurations('google.com')).toBeFalsy(); 22 | expect(ruleManager.matchRuleConfigurations('www.baidu.com.cn')).toBeFalsy(); 23 | }); 24 | 25 | test('rule match that domain start with www', () => { 26 | ruleManager.addRule('www.baidu.com', { 27 | proxyPass: 'foo.com', 28 | location: '/', 29 | }); 30 | 31 | expect(ruleManager.matchRuleConfigurations('www.baidu.com')).toBeTruthy(); 32 | expect(ruleManager.matchRuleConfigurations('baidu.com')).toBeTruthy(); 33 | // 二级域名 34 | expect(ruleManager.matchRuleConfigurations('foo.baidu.com')).toBeFalsy(); 35 | expect(ruleManager.matchRuleConfigurations('google.com')).toBeFalsy(); 36 | expect(ruleManager.matchRuleConfigurations('www.baidu.com.cn')).toBeFalsy(); 37 | }); 38 | 39 | test('mapDomainToRules match same domain, we only match the first one', () => { 40 | ruleManager.addRule('www.baidu.com', { 41 | proxyPass: 'foo.com', 42 | location: '/', 43 | }); 44 | 45 | ruleManager.addRule('baidu.com', { 46 | proxyPass: 'bar.com', 47 | location: '/', 48 | }); 49 | 50 | const expectedProxyPass = ruleManager.matchRuleConfigurations('www.baidu.com')[0].proxyPass; 51 | expect(expectedProxyPass).toStrictEqual('foo.com'); 52 | }); 53 | 54 | test('getProxyPassUrl', () => { 55 | ruleManager.addRule('www.baidu.com', { 56 | proxyPass: 'http://foo.com', 57 | location: '/', 58 | }); 59 | 60 | ruleManager.addRule('www.google.com', { 61 | proxyPass: 'https://hello.com', 62 | location: '/foo/bar', 63 | }); 64 | 65 | const str = ruleManager.getProxyPassUrl(new URL('http://www.baidu.com/hello')); 66 | expect(str?.toString()).toStrictEqual('http://foo.com/hello'); 67 | 68 | const str2 = ruleManager.getProxyPassUrl(new URL('http://www.google.com/foo/bar/baz')); 69 | expect(str2?.toString()).toStrictEqual('https://hello.com/baz'); 70 | }); 71 | 72 | test('match domain', () => { 73 | ruleManager.addRule('www.baidu.com', { 74 | proxyPass: 'http://foo.com', 75 | location: '/', 76 | }); 77 | 78 | ruleManager.addRule('www.google.com', { 79 | proxyPass: 'https://hello.com', 80 | location: '/foo/bar', 81 | }); 82 | 83 | expect(ruleManager.matchDomain('baidu.com')).toBeTruthy(); 84 | expect(ruleManager.matchDomain('google.com')).toBeTruthy(); 85 | 86 | expect(ruleManager.matchDomain('www.baidu.com')).toBeTruthy(); 87 | expect(ruleManager.matchDomain('https://www.baidu.com')).toBeTruthy(); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /packages/assets/src/configurations/node-plop/micro-fe-generator.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import type * as plop from 'node-plop'; 3 | import { createAddConfigAction, createAddManyTemplatesAction, getTemplatePath } from '../../utils'; 4 | 5 | export enum MICRO_FE_TYPE { 6 | BASE_APP = 'Base App', 7 | MICRO_APP = 'Micro App', 8 | } 9 | 10 | const project = function (plop: plop.NodePlopAPI) { 11 | const basePath = path.resolve(process.cwd(), '{{projectName}}'); 12 | const templatePath = path.resolve(getTemplatePath(), 'micro-frontend'); 13 | 14 | // ^0.3.6 匹配:0.3.6 <= v <0.4.0 15 | const MF_LITE_CORE_VERSION = '0.1.0'; 16 | 17 | plop.setGenerator('micro frontend generator', { 18 | description: 'generate a micro app or base app by micro frontend generator', 19 | prompts: [ 20 | { 21 | type: 'input', 22 | name: 'projectName', 23 | message: 'Please enter the name of the package:', 24 | validate: (v: any) => { 25 | if (!v || typeof v !== 'string') 26 | return 'invalid project name, the name cannot be empty!'; 27 | 28 | if (!v.match(/^[^\s]*$/)) 29 | return 'invalid project name, the name cannot not contain space!'; 30 | 31 | return true; 32 | }, 33 | }, 34 | { 35 | type: 'list', 36 | name: 'appType', 37 | message: 'Please select the type of project (base-app or micro-app):', 38 | choices: [ 39 | { 40 | value: MICRO_FE_TYPE.BASE_APP, 41 | }, 42 | { 43 | value: MICRO_FE_TYPE.MICRO_APP, 44 | }, 45 | ], 46 | }, 47 | ], 48 | actions(data) { 49 | const isBaseApp = data.appType === MICRO_FE_TYPE.BASE_APP; 50 | return [ 51 | // 基本代码模板 52 | createAddManyTemplatesAction(`micro-frontend/${isBaseApp ? 'base-app' : 'micro-app'}`, basePath), 53 | 54 | // eslint config 55 | createAddConfigAction('eslintrc.js.hbs', path.resolve(basePath, '.eslintrc.js')), 56 | 57 | // prettier config 58 | createAddConfigAction('prettierrc.json.hbs', path.resolve(basePath, '.prettierrc.json')), 59 | 60 | // shared 61 | { 62 | type: 'add', 63 | path: path.resolve(basePath, '.gitignore'), 64 | templateFile: path.resolve(templatePath, 'shared', 'gitignore.hbs'), 65 | }, 66 | { 67 | type: 'add', 68 | path: path.resolve(basePath, 'package.json'), 69 | templateFile: path.resolve(templatePath, 'shared', 'package.json.hbs'), 70 | }, 71 | { 72 | type: 'add', 73 | path: path.resolve(basePath, 'tsconfig.json'), 74 | templateFile: path.resolve(templatePath, 'shared', 'tsconfig.json.hbs'), 75 | }, 76 | ]; 77 | }, 78 | }); 79 | 80 | plop.setHelper('coreVersion', () => { 81 | return MF_LITE_CORE_VERSION; 82 | }); 83 | 84 | plop.setHelper('cmdAppType', (type) => { 85 | return type === MICRO_FE_TYPE.BASE_APP ? 'base-app' : 'micro-app'; 86 | }); 87 | 88 | plop.setHelper('devServerPort', (type) => { 89 | return type === MICRO_FE_TYPE.BASE_APP ? '8080' : '10000'; 90 | }); 91 | 92 | plop.setHelper('underlinedProjectName', (name: string) => { 93 | return name.replace(/-/g, '_'); 94 | }); 95 | }; 96 | 97 | export default project; 98 | --------------------------------------------------------------------------------