├── .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 |
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 | }
--------------------------------------------------------------------------------