├── .npmrc ├── types └── global.d.ts ├── src ├── vite-env.d.ts ├── utils │ ├── sdk.ts │ └── render.ts ├── dev.tsx ├── interface │ └── event.ts ├── App.module.less ├── App.tsx ├── zustand │ └── useConfigStore.ts ├── Entry.tsx └── logo.svg ├── .editorconfig ├── tsconfig.node.json ├── .gitignore ├── index.html ├── tsconfig.json ├── LICENSE ├── package.json ├── vite.config.ts └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | legacy-peer-deps=true -------------------------------------------------------------------------------- /types/global.d.ts: -------------------------------------------------------------------------------- 1 | // import {SDKMessage} from '../src/interface/event'; 2 | 3 | declare interface SdkConfig { 4 | onMessage?: (message: any) => void; 5 | } 6 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line spaced-comment 2 | /// 3 | 4 | declare module 'lodash'; 5 | declare module '*.less'; 6 | 7 | interface Window { 8 | } 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /src/utils/sdk.ts: -------------------------------------------------------------------------------- 1 | import AiChat from '@/Entry'; 2 | 3 | const sdk: any = {}; 4 | export const setSdkInstance = (instance: AiChat) => { 5 | sdk.instance = instance; 6 | }; 7 | 8 | export const getSdkInstance = (): AiChat => { 9 | return sdk.instance; 10 | }; -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "es2015", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["./vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /src/dev.tsx: -------------------------------------------------------------------------------- 1 | import SDK from './Entry'; 2 | import { SDKEventType } from '@/interface/event'; 3 | 4 | const sdk = new SDK({}); 5 | sdk.render(document.getElementById('root') as HTMLElement); 6 | 7 | sdk.on(SDKEventType.MESSAGE, data => { 8 | window.alert(`accept message: ${data}`); 9 | }); -------------------------------------------------------------------------------- /src/interface/event.ts: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'eventemitter3'; 2 | 3 | export enum SDKEventType { 4 | MESSAGE = 'message' 5 | }; 6 | 7 | export interface SDKMessage extends EventEmitter { 8 | data?: any; 9 | error?: { 10 | code: number; 11 | message: string; 12 | }; 13 | } 14 | 15 | export interface SDKEventPayloadType { 16 | [SDKEventType.MESSAGE]: SDKMessage; 17 | } 18 | -------------------------------------------------------------------------------- /.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 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Vite + React + TS 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "es2015" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "module": "es2015", 15 | /* Bundler mode */ 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "react-jsx", 20 | 21 | /* Linting */ 22 | "strict": true, 23 | "noUnusedLocals": true, 24 | "noUnusedParameters": true, 25 | "noFallthroughCasesInSwitch": true, 26 | 27 | "emitDeclarationOnly": true, 28 | "declaration": true, 29 | "declarationDir": "dist/types", 30 | 31 | "baseUrl": "./", 32 | "paths": { 33 | "@/*": ["src/*"] 34 | }, 35 | "typeRoots": ["types"] 36 | }, 37 | "include": ["src", "types/global.d.ts"], 38 | "references": [{ "path": "./tsconfig.node.json" }], 39 | } 40 | -------------------------------------------------------------------------------- /src/App.module.less: -------------------------------------------------------------------------------- 1 | .app { 2 | text-align: center; 3 | } 4 | 5 | .appLogo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .appLogo { 12 | animation: appLogoSpin infinite 20s linear; 13 | } 14 | } 15 | 16 | .appHeader { 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 | button { 27 | margin-top: 10px; 28 | padding: 8px; 29 | font-size: 14px; 30 | color: blue; 31 | background-color: #fff; 32 | border-radius: 4px; 33 | border: 1px solid #ddd; 34 | 35 | &:hover { 36 | cursor: pointer; 37 | } 38 | } 39 | } 40 | 41 | .appLink { 42 | color: #61dafb; 43 | } 44 | 45 | @keyframes appLogoSpin { 46 | from { 47 | transform: rotate(0deg); 48 | } 49 | to { 50 | transform: rotate(360deg); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Harvey Delaney 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. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-sdk", 3 | "version": "0.1.0", 4 | "private": true, 5 | "type": "module", 6 | "files": [ 7 | "dist" 8 | ], 9 | "module": "./dist/index.js", 10 | "dependencies": { 11 | "eventemitter3": "^5.0.1", 12 | "lodash": "^4.17.21", 13 | "zustand": "^4.5.0" 14 | }, 15 | "scripts": { 16 | "serve": "vite", 17 | "build": "tsc && vite build", 18 | "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 19 | "preview": "vite preview" 20 | }, 21 | "eslintConfig": { 22 | "extends": [ 23 | "react-app", 24 | "react-app/jest" 25 | ] 26 | }, 27 | "browserslist": { 28 | "production": [ 29 | ">0.2%", 30 | "not dead", 31 | "not op_mini all" 32 | ] 33 | }, 34 | "peerDependencies": { 35 | "react": ">=17.0.2", 36 | "react-dom": ">=17.0.2" 37 | }, 38 | "devDependencies": { 39 | "@types/react": "^18.2.48", 40 | "@types/react-dom": "^18.2.18", 41 | "@vitejs/plugin-react": "^4.2.1", 42 | "less": "^4.2.0", 43 | "vite": "^5.0.12", 44 | "react": "^18.2.0", 45 | "react-dom": "^18.2.0", 46 | "typescript": "^4.9.5" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | import { getSdkInstance } from '@/utils/sdk'; 3 | import { SDKEventType } from '@/interface/event'; 4 | import logo from './logo.svg'; 5 | import style from './App.module.less'; 6 | 7 | function App() { 8 | const onClick = useCallback(() => { 9 | const sdk = getSdkInstance(); 10 | sdk.emit(SDKEventType.MESSAGE, 'hello'); 11 | }, []); 12 | 13 | return ( 14 |
15 |
16 | logo 17 |

18 | Edit src/App.tsx and save to reload. 19 |

20 | 26 | Learn React 27 | 28 | 29 | 30 |
31 |
32 | ); 33 | } 34 | 35 | export default App; 36 | -------------------------------------------------------------------------------- /src/zustand/useConfigStore.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand'; 2 | import { get, merge, noop } from 'lodash'; 3 | 4 | export interface ConfigState { 5 | config: SdkConfig; 6 | setConfig: (config: SdkConfig) => void; 7 | }; 8 | 9 | 10 | const useConfigStore = create((set, get) => ({ 11 | config: { 12 | onMessage: noop, 13 | logger: noop 14 | }, 15 | setConfig: config => { 16 | const newConfig = merge({}, get().config, config); 17 | set({ 18 | config: newConfig 19 | }); 20 | }, 21 | onMessage: noop 22 | })); 23 | 24 | export function getConfig(): SdkConfig; 25 | export function getConfig(key: K): SdkConfig[K]; 26 | export function getConfig(key?: K): SdkConfig | SdkConfig[K]; 27 | export function getConfig(key?: K): SdkConfig | SdkConfig[K] { 28 | const config = useConfigStore.getState().config; 29 | if (key) { 30 | return get(config, key); 31 | } 32 | return config; 33 | }; 34 | 35 | export const setConfig = (config: SdkConfig) => { 36 | return useConfigStore.getState().setConfig(config); 37 | }; 38 | 39 | export default useConfigStore; -------------------------------------------------------------------------------- /src/Entry.tsx: -------------------------------------------------------------------------------- 1 | import React, { createRef } from 'react'; 2 | import EventEmitter from 'eventemitter3'; 3 | import { setConfig, getConfig } from '@/zustand/useConfigStore'; 4 | import { SDKEventType } from '@/interface/event'; 5 | import { setSdkInstance } from '@/utils/sdk'; 6 | import { render, unmount } from '@/utils/render'; 7 | import App from './App'; 8 | 9 | 10 | export default class ReactLibSDK extends EventEmitter { 11 | sdkRef: React.RefObject; 12 | container: HTMLElement; 13 | 14 | constructor(config: SdkConfig) { 15 | super(); 16 | setConfig(config); 17 | setSdkInstance(this); 18 | 19 | this.sdkRef = createRef(); 20 | this.container = document.createElement('div'); 21 | } 22 | getConfig(key?: keyof SdkConfig) { 23 | return getConfig(key); 24 | } 25 | setConfig(config: SdkConfig) { 26 | setConfig(config); 27 | } 28 | render(target: HTMLElement) { 29 | this.container = target; 30 | render( 31 | 32 | 33 | , 34 | target 35 | ); 36 | } 37 | unmount() { 38 | unmount(this.container); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig, loadEnv} from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | // @ts-ignore 4 | import path from 'path'; 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig(({command, mode}) => { 8 | // @ts-ignore 9 | const env = loadEnv(mode, process.cwd(), ''); 10 | const isDev = env.NODE_ENV === 'development'; 11 | 12 | return { 13 | resolve: { 14 | alias: { 15 | // @ts-ignore 16 | '@': path.resolve(__dirname, 'src'), 17 | } 18 | }, 19 | plugins: [react()], 20 | css: { 21 | modules: { 22 | scopeBehaviour: 'local', 23 | localsConvention: 'dashesOnly', 24 | generateScopedName: 'sdk-[local]_[hash:base64:5]' 25 | }, 26 | preprocessorOptions: { 27 | less: { 28 | javascriptEnabled: true, 29 | }, 30 | } 31 | }, 32 | build: { 33 | target: 'es2015', 34 | lib: { 35 | entry: 'src/Entry.tsx', 36 | name: 'react-sdk', 37 | fileName: 'index', 38 | }, 39 | rollupOptions: { 40 | external: ['react', 'react-dom'], 41 | output: { 42 | globals: { 43 | react: 'React', 44 | 'react-dom': 'ReactDOM', 45 | } 46 | } 47 | } 48 | } 49 | }; 50 | }) 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-library-skeleton 2 | This project provide skeleton or template aim to help people quickly started with creating their own React library (not app),using: 3 | - build tool: [vite](https://vitejs.dev/guide/) 4 | - language: [typescript](https://www.typescriptlang.org/) 5 | - css preprocessor: [less](https://lesscss.org/) 6 | - zustand: [zustand](https://github.com/pmndrs/zustand). 7 | 8 | ## Usage 9 | ### build output 10 | it will build output to `dist` folder. Also make react, react-dom as peer dependencies to the bundle. **If you want add another dependencies, you can add it to `peerDependencies` in package.json and add external to `vite.config.ts`.** 11 | 12 | ### state management 13 | if you want to use redux, please check [react-redux](https://github.com/reduxjs/react-redux). **if don't want to use any state management, you can remove zustand and replace it with your own state management.** This project just give a example on [zustand/useConfigStore](https://github.com/codeDebugTest/react-library-skeleton/blob/main/src/zustand/useConfigStore.ts). 14 | 15 | ### react compatible 16 | this project is **compatible with react 17.0.2**, although it use react 18 as the development version. The implementation references to [rc-util](https://github.com/react-component/util/blob/master/src/React/render.ts). 17 | 18 | 19 | ## Development 20 | 21 | ### local development 22 | `npm run serve` 23 | 24 | Runs the app in the development mode.\ 25 | Open [http://localhost:5173/](http://localhost:5173/) to view it in the browser. 26 | 27 | The page will reload if you make edits.\ 28 | You will also see any lint errors in the console. 29 | 30 | ### building 31 | `npm run build` 32 | 33 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/utils/render.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description render.js 兼容17和18版本的react render 3 | * @author zhanghui33 4 | */ 5 | import {ReactElement} from 'react'; 6 | import * as ReactDOM from 'react-dom'; 7 | import type {Root} from 'react-dom/client'; 8 | 9 | // rc-util: https://github.com/react-component/util/blob/master/src/React/render.ts 10 | 11 | type CreateRoot = (container: ContainerType) => Root; 12 | 13 | // Let compiler not to search module usage 14 | const fullClone = { 15 | ...ReactDOM 16 | } as typeof ReactDOM & { 17 | __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED?: { 18 | usingClientEntryPoint?: boolean; 19 | }; 20 | createRoot?: CreateRoot; 21 | }; 22 | 23 | const {version, render: reactRender, unmountComponentAtNode} = fullClone; 24 | 25 | let createRoot: CreateRoot; 26 | try { 27 | const mainVersion = Number((version || '').split('.')[0]); 28 | if (mainVersion >= 18 && fullClone.createRoot) { 29 | // eslint-disable-next-line @typescript-eslint/no-var-requires 30 | createRoot = fullClone.createRoot; 31 | } 32 | } 33 | catch (e) { 34 | // Do nothing; 35 | } 36 | 37 | function toggleWarning(skip: boolean) { 38 | const {__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED} = fullClone; 39 | 40 | if ( 41 | __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED 42 | && typeof __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED === 'object' 43 | ) { 44 | __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.usingClientEntryPoint 45 | = skip; 46 | } 47 | } 48 | 49 | const MARK = '__antd_mobile_root__'; 50 | 51 | // ========================== Render ========================== 52 | type ContainerType = (Element | DocumentFragment) & { 53 | [MARK]?: Root; 54 | }; 55 | 56 | function legacyRender(node: ReactElement, container: ContainerType) { 57 | reactRender(node, container); 58 | } 59 | 60 | function concurrentRender(node: ReactElement, container: ContainerType) { 61 | toggleWarning(true); 62 | const root = container[MARK] || createRoot(container); 63 | toggleWarning(false); 64 | root.render(node); 65 | container[MARK] = root; 66 | } 67 | 68 | export function render(node: ReactElement, container: ContainerType) { 69 | if (createRoot as unknown) { 70 | concurrentRender(node, container); 71 | return; 72 | } 73 | legacyRender(node, container); 74 | } 75 | 76 | // ========================== Unmount ========================= 77 | function legacyUnmount(container: ContainerType) { 78 | return unmountComponentAtNode(container); 79 | } 80 | 81 | async function concurrentUnmount(container: ContainerType) { 82 | // Delay to unmount to avoid React 18 sync warning 83 | return Promise.resolve().then(() => { 84 | container[MARK]?.unmount(); 85 | delete container[MARK]; 86 | }); 87 | } 88 | 89 | export function unmount(container: ContainerType) { 90 | if (createRoot as unknown) { 91 | return concurrentUnmount(container); 92 | } 93 | 94 | return legacyUnmount(container); 95 | } --------------------------------------------------------------------------------