在2023年建立NPM包装
#javascript #react #开源 #npm

构建NPM软件包需要许多需要考虑的决定。从选择默认的捆绑器到选择合适的转板器到了解工具或软件包的用例,或者我敢说,您正在创建库。

与编写代码的实际过程相比,这一过程中有很多思考。是的,有时候,整个“深思熟虑” charade甚至可能没有必要。但是,重要的是要把它放在脑海。

webpack,老人

几年前,构建这些包裹的一种方法完全取决于使用webpack作为首选bundler,它还带有许多插件,可用于捆绑您的JavaScript代码。

但是,WebPack附加了许多复杂性。当我尝试使用几次时,我总是会对在配置文件I even wrote an article about my travails中飞来的事物数量感到沮丧。

注意:我在这里的目的不是要对webpack进行大声疾呼,而是为构建这些包裹的更微妙的方法和发布。

很多人可能会在这里提出我的意见,并提出著名的报价“ webpack用于构建应用程序,其他人用于构建图书馆”

但是,我认为这里有很多人错过的是,与WebPack相比,“其他”捆绑工具具有的良好DX开发人员体验。

是的,WebPack为您提供了很多功能。好的。但是不利于我的理智?

正如我之前提到的,本文的目的某种程度上围绕着我犯的错误以及如何绕过它们。

已经有几天了,我一直在处理一个React Tab component,该React Tab component在单击时保留每个选项卡的状态(或NAV项目),当您从和返回到安装该组件的位置时。

处理它肯定了一个我一直在思考很长时间的问题。 “我们可以将组件状态保持在浏览器URL中吗?”

您应该尝试使用包裹,让我知道您的想法。

我写了一篇关于为什么you might not need a state-management library的简短文章,因为它为该组件建立了概念概念。在业余时间看看。

现在,在手头的问题上,NPM软件包。

从我过去所做的事情中,我认为我可以得出结论,当您决定构建不在任何不在任何中类似于依赖CSS某种样式的组件库或软件包。

一个示例是这个helper function,它从任何一段时间以前从任何smarkdown文件中提取标题文本。您可以查看编译器here

的配置

让我介绍您完成此配置的本质。它与仓库中的一个相同。

{
 "include": ["src"],
 "exclude": ["node_modules", "dist"],
 "compilerOptions": {
 "lib": ["ESNext"],
 "module": "ESNext",
 "sourceMap": true,
 "importHelpers": true,
 "declaration": true,
 "rootDir": "./src",
 "outDir": "./dist/esm",
 "strict": true,
 "noImplicitReturns": true,
 "noUnusedLocals": true,
 "noUnusedParameters": true,
 "moduleResolution": "node",
 "jsx": "react",
 "esModuleInterop": true,
 "skipLibCheck": true,
 "forceConsistentCasingInFileNames": true
 }
}

include指定编译打字稿代码时应包含的文件或目录。在这种情况下,对于该辅助功能,它包括src目录。您的可能是不同的。

exclude指定了应从汇编过程中排除的文件或目录。在这里,它不包括node_modulesdist目录。

compilerOptions部分中,有lib属性可确保应包含在编译过程中的特定库。

在这里,它包括“ esnext”库,该库提供了最新的ecmascript功能。

module属性确定要使用的模块系统。在这种情况下,它设置为“ esnext”,它可以使用现代JavaScript模块。

sourceMap启用了生成源地图,这对于在浏览器或开发工具中调试打字条代码很有用。

importHelpers使打字稿助手函数的进口能够帮助某些功能,例如装饰器和异步/等待。

declaration与编译的JavaScript文件一起生成相应的.d.ts声明文件,允许在消耗此代码的其他打字稿项目中进行键入检查和代码完成。

rootDir为打字稿源文件指定根目录。在这里,它设置为src目录。同样,您的可能是完全不同的。

在最近的软件包中,我一直在工作。我将我的矿山设置为packages,因为库将有许多不同组件的用例,据推测。

outDir确定编译JavaScript文件的输出目录。在这种情况下,它设置为dist/esm目录。

strict启用严格的类型检查和更严格的编译器选项。

noImplicitReturns报告函数缺少返回语句时的错误。

noUnusedLocals此报告当声明本地变量但未使用本地变量时发生错误。

noUnusedParameters在声明但未使用函数参数时报告错误。

moduleResolution指定了打字稿如何解决模块导入。在这里,它设置为使用node.js模块分辨率的“ node”。

有时这部分往往非常令人沮丧。

jsx确定用于JSX的语法。在这种情况下,它设置为“反应”以支持React JSX语法。

esModuleInterop可以在commonjs和es ES模块之间互操作性,从而更轻松地在打字稿中导入commonjs模块。

skipLibCheck跳过依赖性的声明文件(*.d.ts)的样式检查,可以提高编译速度。

forceConsistentCasingInFileNames强制执行一致的文件名外壳,这可以帮助防止在不同操作系统上工作时的问题。

如果您不想,则不必严格使用此配置。您可以自由设置对您有效的任何一个。

输入,rollup.js

lolup是用于JavaScript应用程序的捆绑器,如果您想构建组件库或任何NPM软件包,它是迄今为止使用的最建议的工具,因为与WebPack相比,它具有直观而直接的启动开发过程。<<<<<<<<<<<<<<<<<<<<<<<<<<<< /p>

这里的好处是,当您想构建NPM软件包时,您可以一起使用Typescript和汇总。尤其是一个取决于样式的人。

您需要做的就是找到合适的插件。当我着手构建react-tab软件包时,我从打字稿的编译器tsc作为我的Bundler开始。

但是,它带来了很多局限性,因为它无法正确提取和处理CSS输出。由于Rollup具有我可以使用的插件生态系统,因此进行研究并发现合适的插件并不太艰难。

还记得我还如何谈论创建理想软件包的“思考”吗?

如果您要构建的NPM软件包以一种或另一种方式取决于使用CSS,则可能需要考虑选择您采用的方法。

有些人喜欢使用CSS模块,一些带有SCS或SASS的模块,有些则具有普通的CSS。我为react-tab

使用了样式的组件

“你为什么要这样做?样式组件有很多限制”

我稍后再解决。我的观点围绕着那些小边缘的案例展开。例如,现在,如果您决定将CSS模块用于主要最终用户可能会在Next.js项目中使用它的软件包,那么您将有一些问题要处理。

使用样式组件对我来说是一个明智的决定,因为Tab组件本来可以直观且完全可以自定义。我需要暴露某些样式道具。

,使用样式组件时可以扩展组件的道具是不是新事物。

// component.styled.ts
import styled from "styled-components"

type themeProps = {
 theme: string
}

export const Container = styled.div<themeProps>`
 background: ${({theme}) => theme ? theme : "brown"}
`
然后,

Container变成类似于下面的摘要的东西。这只是通过此CSS组件图
可以实现的冰山的尖端

<Container theme="purple">
 // children
</Container>

但是,限制仍然存在。因为,正如我之前提到的,打字稿的编译器无法正确处理将捆绑在一起的样式的输出,请滚动以保存一天。

真相的来源 - config.js

对于大多数捆绑器来说,拥有一个配置文件是一种常见的做法,该文件可以控制应用程序的块,处理,编译,以及在幕后进行的任何其他操作。

可以在下面看到配置文件的示例。这将导出应用程序和输出目录的入口点。

export default {
 input: "packages/index.ts",
 output: {
   file: "dist/index.js",
   format: "cjs"
 }
}

由于react-tab使用外部依赖关系,因此,next/router是确切的,当我尝试使用rollup -c命令构建包装时,将记录“未解决的依赖关系”警告。

汇总根据其导入路径以不同的方式对待依赖性。当它遇到以./开头的导入时,它假定它是本地文件并相应地解决它。

但是,当导入以模块名称(例如next/router)开头时,它将其视为外部依赖项,应通过使用包装的人来解决。

要绕过此警告,我必须指定next/router是我的汇总配置中的外部依赖性。

同样的方法适用于您,一旦您确定了您的应用程序/库依赖的外部软件包。

export default {
 input: "packages/index.ts",
 output: {
   file: "dist/index.js",
   format: "cjs",
 },
 external: ["react", "react-dom", "next/router"],
}

回想一下,当我提到与样式组件的局限性有关的东西时?这是它的问题。

在共享组件或库中使用样式组件时,您应该牢记三件事,以确保在其他项目消耗软件包时正确应用样式。

将其列为对等依赖关系:我必须确保styled-components在我的软件包文件中列为同行依赖。

这通知消费者,他们需要在自己的项目中安装styled-components才能正确利用您的组件。

如果您在package.json文件中没有这样的方式,则可以通过使用generatePackageJSON插件来修改peerDependencies属性来这样做:

generatePackageJSON({
 outputFolder: "dist",
 baseContents: (pkg) => ({
   name: pkg.name,
   main: "/dist/index.js",
   peerDependencies: {
     react: "^18.2.0",
     "styled-components": "^6.0.0-rc.3",
   },
  }),
})

CSS提取:默认情况下,样式组件通过在运行时将其注入DOM来应用样式。

但是,当构建共享组件库或软件包时,最好在构建过程中提取样式,因此消费项目可以捆绑并使用它们。

为了实现这一目标,我必须在我的Rollup Config
中使用像babel-plugin-styled-components这样的工具以及Babel插件

babel({
 extensions: [".ts", ".tsx"],
 exclude: "node_modules/**",
 presets: ["@babel/preset-react", "@babel/preset-typescript"],
 plugins: ["styled-components"],
}),

css output :只要您编写任何被转移到CSS的内容,都应包含一个croup插件rollup-plugin-styles。 P>

插件可确保正确提取您要编写的样式。

export default {
 input: "packages/index.ts",
 output: {
   file: "dist/index.js",
   format: "cjs",
 },
 external: ["react", "react-dom", "next/router"],
 styles(),
}

peerDependencies属性中,依赖项的确切版本是硬编码的。但是,有一种方法可以确保同行依赖性自动更新到最新兼容版本而不手动硬编码。

您可以在同行依赖项的版本范围内使用latest标签。

peerDependencies: {
 react: "latest",
 "styled-components": "latest",
},

通过使用latest标签,它将自动获取满足指定版本范围的同行依赖的最新版本。

这样,您无需每次兼容版本时都需要手动更新版本。

当用户安装您的软件包或运行npm installyarn install时,软件包管理器将根据您的package.json中指定的版本范围获取同伴依赖项的最新兼容版本。

但总会有一个,但请记住,如果发布新的同行依赖性新的主要版本,并且包括破坏变化,则使用latest标签也可以引入潜在的破坏更改。

为了减轻这种情况,在发布新版本之前,最好用最新版本的同行依赖项彻底测试您的软件包。

这是完整的汇总配置的样子:

import babel from "rollup-plugin-babel";
import commonjs from "rollup-plugin-commonjs";
import generatePackageJSON from "rollup-plugin-generate-package-json";
import { nodeResolve } from "@rollup/plugin-node-resolve";
import styles from "rollup-plugin-styles";
import { terser } from "rollup-plugin-terser";

const dev = process.env.NODE_ENV !== "production";

export default {
  input: "packages/index.ts",
  output: {
    file: "dist/index.js",
    format: "cjs",
  },
  external: ["react", "react-dom"],
  plugins: [
    nodeResolve({
      extensions: [".ts", ".tsx"],
    }),
    babel({
      extensions: [".ts", ".tsx"],
      exclude: "node_modules/**",
      presets: ["@babel/preset-react", "@babel/preset-typescript"],
    }),
    generatePackageJSON({
      outputFolder: "dist",
      baseContents: (pkg) => ({
        name: pkg.name,
        main: "/dist/index.js",
        peerDependencies: {
          react: "^18.2.0",
          "styled-components": "^6.0.0-rc.3",
        },
      }),
    }),
    terser({
      ecma: 2015,
      mangle: { toplevel: true },
      compress: {
        toplevel: true,
        drop_console: !dev,
        drop_debugger: !dev,
      },
      output: { quote_style: 1 },
    }),
    commonjs(),
    styles(),
  ],
};

这是package.json文件的重要部分所需要的。它为react-tab软件包指定了构建配置和脚本。

您可以采用它适合您的用例。

{
 "main": "./dist/cjs/index.js",
 "module": "./dist/esm/index.js",
 "types": "./dist/esm/index.d.ts",
 "scripts": {
   "build:esm": "tsc",
   "build": "yarn build:esm && yarn build:cjs",
   "build:cjs": "rollup -c"
 },
}

这是摘要的分解;

"main": "./dist/cjs/index.js"指定COMPORJS(CJS)模块的入口点。当某人使用require()或在commonjs环境中导入您的软件包时,这是将加载的文件。

"module": "./dist/esm/index.js"指定了eCmascript模块(ESM)的入口点。当某人使用在ESM支持的环境中导入您的软件包时,这是将加载的文件。

"types": "./dist/esm/index.d.ts"指定了打字稿声明文件的位置(.d.ts)。

此文件为您的软件包提供类型信息,允许打字稿用户在使用软件包时获得类型检查和自动完成。

"build:esm": "tsc"运行打字稿编译器tsc,以构建软件包的ESM版本。它将打字稿代码编译到JavaScript中,并在./dist/esm目录中输出文件。

"build": "yarn build:esm && yarn build:cjs"首先运行build:esm脚本,然后执行build:cjs脚本。

它可以确保构建包装的ESM和CJS版本。

"build:cjs": "rollup -c"使用配置文件(-c)运行汇总bundler rollup

汇总读取配置文件以捆绑代码,应用任何指定的转换或优化。输出是在./dist/cjs目录中生成的。

出版前在本地进行测试。

此部分有些棘手,因为它可能会以某种方式影响工具的版本控制过程。

您不必随时随地运行npm publish,而应与npm link

链接该软件包

如果您使用的是任何Linux发行版OS,则必须将sudo附加到npm link才能授予权限。这样可以确保为软件包创建符号链接。

然后,您可以继续在您想这样使用的另一个项目中链接软件包。

npm link package-name

包起来。

因为本文的意图并不完全是通过遵循一些步骤来引导您完成构建组件库的过程,以下是一些资源可以帮助您实现目标。

How to build a component library with React and TypeScript

Component library setup with React, TypeScript and Rollup

Easily build and publish a React / Typescript component library package to Npm using Storybook and Rollup.