Use rollup to package ts+react cache components and publish them to npm

Create a new project directory, for example, called root, and create a rollup configuration file below: rollup.config.ts Because rollup supports ts and esmodule well, use the ts configuration file.

Setup

Generate a package.json file, generated here using pnpm:

pnpm init

Install rollup and Typescript:

pnpm add rollup
pnpm add typescript

Configure the pnpm build command of package.json:

{
    
    
  "scripts": {
    
    
    "build": "rollup --c --configPlugin typescript2 --bundleConfigAsCjs",
    "build:dev": "cross-env NODE_ENV=development pnpm build",
    "build:prod": "cross-env NODE_ENV=production pnpm build"
  },
  "type": "module"
}

Rollup runs in the node environment. The modularity of node is that commonjs does not support esmodule, so you need to configure a rollup command in the running script of package.json -bundleConfigAsCjs to parse the esmodule code into commonjs so that nodejs can recognize it, and then package.json needs to be Add type:module to support esmodule

build command explanation:

  1. –c specifies that rollup reads the rollup.config configuration file in the project root directory for construction.
  2. –configPlugin specifies the plug-in to be used when building rollup, including the processing of the rollup configuration file. Here, the typescript2 plug-in is specified to process the ts configuration file to prevent errors when reading the rollup configuration file.
  3. –bundleConfigAsCjs is a command of rollup used to convert esmodule into commonjs for use in the node environment.
  4. cross-env is a plug-in used to smooth out the way to set environment variables in different operating systems. The way to set environment variables in different operating systems is different. We cannot do it one by one, so it is used to achieve cross-platform setting of environment variables.
  5. build:dev and build:prod are used to perform different operations based on obtaining the value process.env.NODE_ENV injected by the environment variable.
  6. type:module is also a step to configure to support esmodule.

rollup.config.ts configuration file

The file requires exporting a RollupOptions object/RollupOptions[] object array. An object is the packaging configuration of a file. You can package as many configuration objects as you want. Here I will specify an entry file to expose three interfaces to the outside. Rollup will go according to the import reference of the entry file. Find the file to configure the build
rollup configuration file:

import nodeResolve from '@rollup/plugin-node-resolve'
import typescript2 from 'rollup-plugin-typescript2'
// @ts-ignore
import babel from 'rollup-plugin-babel'
import commonjs from '@rollup/plugin-commonjs'
import {
    
     join, resolve } from 'path'
import {
    
     readdir } from 'fs/promises'
import {
    
     RollupOptions, defineConfig } from 'rollup'
import {
    
     IOptions } from 'rollup-plugin-typescript2/dist/ioptions'
import {
    
     existsSync } from 'fs'
import {
    
     unlink, rmdir, lstat } from 'fs/promises'
const commonPlugins = [
  nodeResolve({
    
    
    extensions: ['.ts', '.tsx'], // 告诉node要解析的文件扩展名
  }),
  typescript2({
    
    
    tsConfig: resolve(__dirname, 'tsconfig.json'), // 指定ts配置文件位置
    // useTsconfigDeclarationDir: true, // 使用配置文件里的DeclarationDir 不开启默认强制生成在和文件同级目录同名文件
  } as Partial<IOptions>),
  babel({
    
    
    babelrc: true, // 使用.babelrc配置文件
  }),
  commonjs(), // 这个插件比如加 用来转换成commonjs 然后注入react17新的jsx组件转换函数_JSX react17+不再用createElement 不用这个插件只用babel处理会报错
]
/**
 * @description 根据路径删除目录
 * @param dirs 删除的目录路径
 */
const removeDir = async (...dirs: string[]) => {
    
    
  for (const dir of dirs) {
    
    
    const absolutePath = resolve(__dirname, dir)
    if (existsSync(absolutePath)) {
    
    
      const dirStack = [absolutePath]
      while (dirStack.length > 0) {
    
    
        const initPath = dirStack[dirStack.length - 1]
        const fileStat = await lstat(initPath)
        if (fileStat.isDirectory()) {
    
    
          const files = await readdir(initPath)
          if (files.length > 0) {
    
    
            dirStack.push(...files.map((e) => join(initPath, e)))
          } else {
    
    
            await rmdir(initPath)
            dirStack.pop()
          }
        } else if (fileStat.isFile()) {
    
    
          await unlink(initPath)
          dirStack.pop()
        }
      }
    }
  }
}
const resolveRollupOptions = async () => {
    
    
  const results: RollupOptions[] = []
  const dirStack = [resolve(__dirname, 'src')]
  while (dirStack.length > 0) {
    
    
    const initPath = dirStack.shift()!
    const fileStat = await lstat(initPath)
    if (fileStat.isDirectory()) {
    
    
      const files = await readdir(initPath)
      if (files.length > 0) {
    
    
        dirStack.push(...files.map((e) => join(initPath, e)))
      }
    } else if (fileStat.isFile()) {
    
    
      const rollupOption: RollupOptions =
        process.env.NODE_ENV === 'development'
          ? {
    
    
              input: initPath,
              treeshake: false,
              external: ['react', 'react-dom'],
              output: {
    
    
                file: initPath
                  .replace(/src/, 'lib')
                  .replace(/\.(tsx|ts)/, '.js'),
                format: 'esm',
                sourcemap: true,
              },
              plugins: commonPlugins,
            }
          : {
    
    
              input: initPath,
              treeshake: true,
              external: ['react', 'react-dom'],
              output: {
    
    
                file: initPath
                  .replace(/src/, 'lib')
                  .replace(/\.(tsx|ts)/, '.min.js'),
                format: 'esm',
                sourcemap: false,
              },
              plugins: [...commonPlugins],
            }
      results.push(rollupOption)
    }
  }
  return results
}
export default defineConfig(async (/* commandLineArgs */) => {
    
    
  // 每次构建前先删除上一次的产物
  await removeDir('es', 'lib')
  // 生成两个产物 一个esmodule模块 一个umd通用模块
  return [
    {
    
    
      input: resolve(__dirname, 'src/index.ts'), // 指定入口文件
      treeshake: true, // 开启treeshaking
      external: ['react', 'react-dom'], // 第三方库使用外部依赖
      output: {
    
    
        name: 'ReactAlive', // 这个name用于打包成umd/iife模块时模块挂到全局对象上的key
        file: resolve(__dirname, 'es/index.js'), // 构建的产物输出位置和文件名
        format: 'esm', // 构建产物的模块化类型
        sourcemap: false, // 关闭sourcemap
        // 指定被排除掉的外部依赖在全局对象上的key
        globals: {
    
    
          react: 'React',
          'react-dom': 'ReactDOM',
        },
      },
      plugins: commonPlugins,
    },
    {
    
    
      input: resolve(__dirname, 'src/index.ts'),
      treeshake: true,
      external: ['react', 'react-dom'],
      output: {
    
    
        name: 'ReactAlive',
        file: resolve(__dirname, 'lib/index.js'),
        format: 'umd',
        sourcemap: false,
        globals: {
    
    
          react: 'React',
          'react-dom': 'ReactDOM',
        },
      },
      plugins: commonPlugins,
    },
  ] as RollupOptions[]
})

root/src/index.ts: The entry file declares the interface that this library will expose to the outside world:

import KeepAliveScope from './components/keepalive-scope'
import KeepAliveItem, {
    
     useCacheDestroy } from './components/keepalive-item'
export {
    
     KeepAliveItem, KeepAliveScope, useCacheDestroy }

root/global.d.ts: Provide a type declaration for process.env.NODE_ENV so that there are code prompts:

declare namespace NodeJS {
    
    
  interface ProcessEnv {
    
    
    NODE_ENV: 'development' | 'production'
  }
}

root/tsconfig.json:

{
    
    
  "compilerOptions": {
    
    
    "target": "ESNext",
    "lib": ["DOM", "DOM.Iterable", "ESNext"],
    "allowJs": false,
    "skipLibCheck": true, // 跳过第三方库类型声明文件的检查
    "esModuleInterop": true, // 开启将esm代码编译成cjs
    "allowSyntheticDefaultImports": true, // 启用默认导出 .default访问默认导出的module.exports内容
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "module": "ESNext",
    "sourceMap": true,
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "types": ["node"],
    "experimentalDecorators": true, // 开启装饰器语法
    "jsx": "react-jsx", // react17+这里可以改成react-jsx 17+后会自动引入一个编译jsx函数 配置babel的automatic
    "baseUrl": ".",
    // "paths": {
    
    
    //   "@/*": ["src/*"]
    // },
    "declaration": true // 是否生成类型声明文件
    // "declarationDir": "lib/types" // 类型声明文件默认生成在对应ts文件同级目录 指定一个目录统一生成
  },
  "exclude": ["node_modules"],
  "include": ["src"]
}

root/.babelrc: Provide configuration for babel plug-in:

{
    
    
  "presets": [
    "@babel/preset-env",
    [
      "@babel/preset-react",
      {
    
    
        "runtime": "automatic"
      }
    ]
  ],
  "extensions": [".ts", ".tsx"],
  "include": ["src"],
  "exclude": ["node_modules"]
}

.babelrc file explanation:

  1. @babel/preset-env: Babel adjusts Babel’s own configuration presets according to our environment.
  2. @babel/preset-react: Babel uses presets to adjust its own configuration in the react project environment. It is used to convert configuration items in presets such as jsx propstype checking. runtime: automatic refers to automatically injecting into react components at runtime. jsx conversion function Before react17, we needed to import React at the beginning of the component to create a component using createElement on the React object. New features after v17 allow us to automatically inject the jsx conversion function without importing React. This matter was finally brought up by @babel/plugin -transform-react-jsx does it
  3. extensions: tells babel the file suffix of the file to be processed. My components only have tsx and ts files.
    npmignore file: tells npm which files to ignore when publishing and do not need to publish them to the npm package.
    This is the checklist to be ignored in my project:
node_modules
src
.babelrc
.gitignore
.npmignore
.prettierrc
rollup.config.ts
test
pnpm-lock.yaml
global.d.ts
tsconfig.json
.DS_Store
test-project

Except for files in .npmignore, they will be uploaded to npm.

Publish npm package

First register an npm account and then go to the home page to search for the package header. There will be a prompt asking you to bind the 2FA security verification policy. Just follow the prompts and complete the process. It probably means binding an otp (one-time-password) one-time password. Download a google authentication app and you can generate a code by scanning the webpage to bind the account and then follow the prompts. This one-time password will ask you to enter otp when you first npm login and npm publish, and then it will directly let you go to the browser. Just verify

Before each release, you must modify the version number of package.json. If you cannot release the same version, an error will be reported.

Before publishing, it is best to search for the package name (the name of package.json is the package name of the npm package) to see if it already exists. Otherwise, if you use the existing name, you will be told that you have no permission to update other people's packages. It is best to use @your own name/package name. Adding a user name prefix to this format is equivalent to a namespace, and the chance of duplication will be much smaller.

root/package.json complete code example:

{
    
    
  "name": "@williamyi74/react-keepalive",
  "version": "1.1.2",
  "description": "基于react18+的缓存组件,拥有类似vue的KeepAlive组件功能效果",
  "main": "index.js",
  "scripts": {
    
    
    "build": "rollup --c --configPlugin typescript2 --bundleConfigAsCjs",
    "build:dev": "cross-env NODE_ENV=development pnpm build",
    "build:prod": "cross-env NODE_ENV=production pnpm build"
  },
  "keywords": [
    "alive",
    "keep alive",
    "react",
    "react keep alive",
    "react keep alive item",
    "react keep alive scope",
    "react keep scope",
    "react hooks keep alive"
  ],
  "author": "williamyi",
  "license": "ISC",
  "peerDependencies": {
    
    
    "react": ">=18.2.0",
    "react-dom": ">=18.2.0",
    "typescript": ">=5.0.4"
  },
  "devDependencies": {
    
    
    "@babel/core": "^7.21.4",
    "@babel/preset-env": "^7.21.4",
    "@babel/preset-react": "^7.18.6",
    "@rollup/plugin-commonjs": "^24.1.0",
    "@rollup/plugin-node-resolve": "^15.0.2",
    "@types/react": "^18.2.0",
    "@types/react-dom": "^18.2.1",
    "cross-env": "^7.0.3",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "rollup": "^3.21.0",
    "rollup-plugin-babel": "^4.4.0",
    "rollup-plugin-typescript2": "^0.34.1",
    "typescript": "^5.0.4"
  },
  "type": "module"
}

root/package.json explanation:

  1. keywords: Keywords to search for your package
  2. description: A brief description under the thumbnail when searching for a package
  3. license: the agreement of the project. ISC is a simplified version of MIT. This means that others can freely modify and use your project.
  4. peerDependencies: The external dependency requirements used in the project tell the project using this package that these dependency versions must meet this requirement, otherwise an error will be reported. When the project using this package downloads dependencies, it will download peerDep instead of downloading all the packages in external in our project. come here

First, switch to the npm source before executing npm login. Do not use the Taobao source, otherwise an error will be reported. Enter the information and follow the steps.

After successful login, execute npm publish --access public publish

–access public tells npm to publish public packages because private packages cannot be published by default. Forgot whether private packages are charged or something? If you specify this command parameter, permission errors will not be reported.

After no errors are reported along the way, you can look at the npm package and see the package you published:
Publish successful npm package

document

Just use the README.md file in the root directory to complete this file.

Guess you like

Origin blog.csdn.net/Suk__/article/details/130511222