diff --git a/content/3.resources/1.learn/3.unbuild-101-first-hand.md b/content/3.resources/1.learn/3.unbuild-101-first-hand.md new file mode 100644 index 00000000..b8d47939 --- /dev/null +++ b/content/3.resources/1.learn/3.unbuild-101-first-hand.md @@ -0,0 +1,568 @@ +--- +title: unbuild 101 - first hand +description: A zero-config utility that simplifies npm package builds by automatically generating commonjs, esm, and declaration files from TypeScript, with security checks and customization hooks, based on rollup and mkdist. +authors: + - name: Estéban Soubiran + picture: https://esteban-soubiran.site/esteban.webp + twitter: soubiran_ +packages: + - unbuild +project: # https://github.com/unjs/unbuild/tree/main/examples +publishedAt: 2023-10-05 +modifiedAt: 2023-10-05 +layout: article +--- + +[unbuild](https://unbuild.unjs.io) is a utility package to build packages for npm with zero configuration by default by inferring the configuration from your `package.json` file. It takes your TypeScript code to generate commonjs, esm and declaration files. + +It's built on top of [rollup](https://rollupjs.org/) and [mkdist](https://mkdist.unjs.io/), have a passive watcher and a lot of hooks to customize the build. At the same time, it create secure builds by checking various build issues such as potential missing and unused dependencies. + +## Installation + +First, let's create a new project: + +```bash +mkdir unbuild-101 +cd unbuild-101 +``` + +::alert{type="info"} +Do not initialize a new project with `npm init` or `yarn init` since we will explore how `unbuild` infers the configuration from `package.json`. +:: + +Then, install [unjs/unbuild](https://github.com/unjs/unbuild): + +```bash +npm install unbuild --save-dev +``` + +We now have a `package.json` file that looks like this: + +```json [package.json] +{ + "devDependencies": { + "unbuild": "^2.0.0" // or the latest version + } +} +``` + +::alert{type="info"} +We can use the package manager of our choice like `npm`, `yarn`, `pnpm` or `bun`. +:: + +## Our First Build + +By default, [unjs/unbuild](https://github.com/unjs/unbuild) works out of the box without configuration by inferring the configuration found in the `package.json` used by `npm` to publish our package. + +Let's add a little script to our `package.json`: + +```json [package.json] +{ + "scripts": { + "build": "unbuild" + } +} +``` + +And create a `src/index.js` file: + +```js [src/index.js] +export function sayFoo() { + console.log('foo') +} +``` + +::alert{type="info"} +It's important to note that we use a `src` folder to store our source code. This is a default behavior of [unjs/unbuild](https://github.com/unjs/unbuild) to search for an entrypoint in this folder. +:: + +Now, let's run our build: + +```bash +npm run build +``` + +We expect to have a `dist/index.js` file with the contents of `src/index.js` but we got nothing and the output shows a dist size of 0 bytes. :thinking: + +::alert{type="info"} +By default, [unjs/unbuild](https://github.com/unjs/unbuild) will build the package in the `dist` folder, even if we specify another location in our `package.json` file. +:: + +```bash +ℹ Automatically detected entries: [esm] +ℹ Building default +✔ Build succeeded for default +Σ Total dist size (byte size): 0 B +``` + +**And that's totally normal!** + +### Setup `package.json` + +[unjs/unbuild](https://github.com/unjs/unbuild) exists to allow us to build our package to publish it to npm. So, if our `package.json` is not configured to be then usable by other people, it's useless to build it so [unjs/unbuild](https://github.com/unjs/unbuild) does not build it. + +This behavior is very interesting because it allows us to have build warnings when exported files are not present in the build. + +First of all, we need to tell [unjs/unbuild](https://github.com/unjs/unbuild) that our package is an ES module: + +```json [package.json] +{ + "type": "module" +} +``` + +Then, we need to tell him what is exported by our package. In our case, we want to export the `sayFoo` function from `src/index.js`. For better compatibility, we will build our project to CommonJS and ES module: + +```json [package.json] +{ + "exports": { + ".": { + "import": "./dist/index.mjs", + "require": "./dist/index.cjs" + } + } +} +``` + +We will add a `main` entry point as a fallback for CommonJS users: + +```json [package.json] +{ + "main": "./dist/index.cjs" +} +``` + +Finally, we will tell `npm` which files we want to publish: + +```json [package.json] +{ + "files": ["dist"] +} +``` + +Now, we can run our build again: + +```bash +npm run build +``` + +And we got our `dist/index.js` file with the content of `src/index.js`: + +```bash +ℹ Automatically detected entries: src/index [esm] [cjs] +ℹ Building default +ℹ Cleaning dist directory: ./dist +✔ Build succeeded for default + dist/index.cjs (total size: 85 B, chunk size: 85 B, exports: sayFoo) + + dist/index.mjs (total size: 64 B, chunk size: 64 B, exports: sayFoo) + +Σ Total dist size (byte size): 149 B +``` + +As expected, we've got our exported files with content from `src/index.js`. :tada: + +::alert{type="info"} +Read more about [package.json exports](https://nodejs.org/api/packages.html#packages_exports). +:: + +### TypeScript Support + +Out of the box, [unjs/unbuild](https://github.com/unjs/unbuild) will support TypeScript. + +Let's rename our `src/index.js` file to `src/index.ts` and add some TypeScript code: + +```ts [src/index.ts] +export function sayFoo() { + console.log('foo') +} +``` + +If we run our build again, everything works as expected but because we're using TypeScript, we want to export the types of our package too. + +To do that, we need to add a `types` entry point to our `package.json` before the key `main`: + +```json [package.json] +{ + "types": "./dist/index.d.ts" // Note that we use `.d.ts` instead of `.mts` +} +``` + +Then, we also need to add `types` to the `import` and `require` keys: + +```json [package.json] +{ + "exports": { + ".": { + "import": { + "types": "./dist/index.d.mts", // Note that we use `.d.mts` + "default": "./dist/index.mjs" + }, + "require": { + "types": "./dist/index.d.cts", // Note that we use `.d.cts` + "default": "./dist/index.cjs" + } + } + } +} +``` + +::alert{type="warning"} +The `types` condition should always come first in `exports`. +:: + +Now, everything is well configured and we can run our build again: + +```bash +npm run build +``` + +And we've got a `dist` folder like this: + +```bash +dist +├── index.cjs +├── index.d.cts +├── index.d.mts +├── index.d.ts +└── index.mjs +``` + +Perfect and we're now sure that our package is well configured to be used by other people once published! :ok_hand: + +::alert{type="info"} +Read more about the [TypeScript usage](https://www.typescriptlang.org/docs/handbook/esm-node.html#packagejson-exports-imports-and-self-referencing). +:: + +## Configuration + +Of course, [unjs/unbuild](https://github.com/unjs/unbuild) is fully configurable and we will see how to do that. But in most cases, we don't need to configure it or we just need to write some lines! + +To do that, we've just to write a `build.config.ts` at the root of the folder: + +```ts [build.config.ts] +import { defineBuildConfig } from 'unbuild' + +export default defineBuildConfig({}) +``` + +Starting from now, we must be careful. Adding content to the configuration file will break the infer configuration and it may happen some unexpected behavior. + +### Changing the Output Directory + +Imagine we do not want to publish a `dist` directory but an `output` one, we can simply specify it in our config file: + +```ts [build.config.ts] +import { defineBuildConfig } from 'unbuild' + +export default defineBuildConfig({ + outDir: 'output' +}) +``` + +The default value is `dist` whatever is the value defined in our `package.json` file. + +::alert{type="warning"} +We absolutely need to update our paths in our `package.json` file. +:: + +### Adding entries + +`entries` refers to the entrypoint of the compiler. For example, imagine we have a `plugins` folder with `vite.ts` and `webpack.ts` file in it. + +```ts [./src/plugins/vite.ts] +export default function defineVitePlugin() { + console.log('vite') +} +``` + +```ts [./src/plugins/webpack.ts] +export default function defineWebpackPlugin() { + console.log('webpack') +} +``` + +If we build our package, the compiler will totally ignore these files. + +First, we need to add an export option to our `package.json`: + +```json [package.json] +{ + "exports": { + "./plugins/*": { + "import": { + "types": "./dist/*.d.mts", + "default": "./dist/*.mjs" + }, + "require": { + "types": "./dist/*.d.cts", + "default": "./dist/*.cjs" + } + } + } +} +``` + +Then, we will need to tell to [unjs/unbuild](https://github.com/unjs/unbuild) to build our plugins. + +```ts [build.config.ts] +import { defineBuildConfig } from 'unbuild' + +export default defineBuildConfig({ + entries: [ + './src/index.ts', // We need to add the default value + './src/plugins/' + ], + declaration: true, // Create `.d.*ts` files. Must be set since we overwrite default configuration. + rollup: { + emitCJS: true, // Create `.cjs` file. Must be set since we overwrite default configuration. + } +}) +``` + +When building our project, we got this structure: + +```bash +dist +├── index.cjs +├── index.d.cts +├── index.d.mts +├── index.d.ts +├── index.mjs +├── vite.d.ts +├── vite.mjs +├── webpack.d.ts +└── webpack.mjs +``` + +Clearly, that's far from ideal since the folder structure is not respected. This can be an issue if some files have the same name. Also, we do not generate Common JS file. + +To fix that, we can personalize the builder used: + +```ts [build.config.ts] +import { defineBuildConfig } from 'unbuild' + +export default defineBuildConfig({ + entries: [ + './src/index.ts', + { + builder: 'mkdist', + input: './src/plugins/', + outDir: './dist/plugins/', + } + ], + declaration: true, + rollup: { + emitCJS: true, + } +}) +``` + +And now, we've got this structure: + +```bash +dist +├── index.cjs +├── index.d.cts +├── index.d.mts +├── index.d.ts +├── index.mjs +└── plugins + ├── vite.cjs + ├── vite.d.cts + ├── vite.d.mts + ├── vite.d.ts + ├── vite.mjs + ├── webpack.cjs + ├── webpack.d.cts + ├── webpack.d.mts + ├── webpack.d.ts + └── webpack.mjs +``` + + + +::alert{type="info"} +[`unjs/mkdist`](https://github.com/unjs/mkdist) is a file-to-file transpiler. +:: + +## Using Externals Dependencies + +[unjs/unbuild](https://github.com/unjs/unbuild) will use our `package.json` to infer the dependencies to include or not in the build. + +### Dependencies + +By default, all dependencies will stay as external dependencies. This means that they will not be included in the build and the user will need to install them to use our package. + +For example, we can try to use [`unjs/consola`](https://github.com/unjs/consola). First, we can instlal it: + +```bash +npm install consola +``` + +Then, we can use it in our `src/index.ts` file: + +```ts [src/index.ts] +import consola from 'consola' + +export function sayFoo() { + consola.info('foo') +} +``` + +If we run our build and take a look at `dist/index.mjs`, we can see that `consola` is not included in the build: + +```js [dist/index.mjs] +import consola from 'consola' + +function sayFoo() { + consola.info('foo') +} + +export { sayFoo } +``` + +### Dev Dependencies + +By default, all dev dependencies will be included in the build. This means that they will be included in the build and the user will not need to install them to use our package. + +In the same time, [unjs/unbuild](https://github.com/unjs/unbuild) will warn us that we should not include dev dependencies in our build and fail to finish the build. + +::alert{type="info"} +We can disable `failOnWarn` to allow the build to finish but it's **not recommended**. +:: + +We can try this behavior by installing [`unjs/scule`](https://github.com/unjs/scule) as a dev dependency: + +```bash +npm install scule --save-dev +``` + +Then, we can use it in our `src/index.ts` file: + +```ts [src/index.ts] +import { camelCase } from 'scule' + +export function sayFoo() { + console.log(camelCase('foo')) +} +``` + +If we build our package, we get this warning: + +```bash +WARN Build is done with some warnings: + +- Inlined implicit external scule +``` + +To deal with that, we have two options: + +- We can explicitly tell [unjs/unbuild](https://github.com/unjs/unbuild) to treat a dev dependency as an external dependency. + +```ts [build.config.ts] +import { defineBuildConfig } from 'unbuild' + +export default defineBuildConfig({ + externals: ['scule'] +}) +``` + +- We can inline dependencies. + +```ts [build.config.ts] +import { defineBuildConfig } from 'unbuild' + +export default defineBuildConfig({ + rollup: { + inlineDependencies: true + } +}) +``` + +Now, if we take a look at `dist/index.mjs`, we can see that `scule` is included in the build. We can search for the function `camelCase`, and we can see that it's inlined: + +```js [dist/index.mjs] +function camelCase(string_) { + return lowerFirst(pascalCase(string_)) +} +``` + +At the same time, we have a line in the build log to indicate that `scule` is inlined: + +```bash +📦 node_modules/.pnpm/scule@1.0.0/node_modules/scule/dist/index.mjs (1.74 kB) +``` + +::alert{type="info"} +Inlining dependencies is a tradeoff since it increases the size of the build and slows down the update (users can manually update a dependency), but it could result in a faster runtime. +:: + +## Stub + +[unjs/unbuild](https://github.com/unjs/unbuild) allows us to use a stub to build our package. A stub is a link to the project with [`unjs/jiti`](https://github.com/unjs/jiti) in the middle to compile on the fly. + +It's very useful when we want to test our package or when we work in a monorepo. + +To create a stub, we can pass the option `--stub` to the build command: + +```bash +npm run build -- --stub +``` + +Or we can add it to our `build.config.ts` file: + +```ts [build.config.ts] +import { defineBuildConfig } from 'unbuild' + +export default defineBuildConfig({ + stub: true +}) +``` + +To understand what a stub is, we can take a look at the `dist/index.mjs` file. We can see that the entry file is loaded by [`unjs/jiti`](https://github.com/unjs/jiti) and exported from the file. + +## Hooks + +We can easily access different steps of the build process by using hooks. + +For example, we can use the `build:done` hook to log a message when the build is done: + +```ts [build.config.ts] +import { defineBuildConfig } from 'unbuild' + +export default defineBuildConfig({ + hooks: { + 'build:done': (ctx) => { + console.log('Build is done!') + } + } +}) +``` + +There are a lot of hooks available: + +- `build:prepare`: Called when the build is prepared before entries are resolved. +- `build:before`: Called before the build starts. +- `build:done`: Called when the build is done. + +- `rollup:options`: Called when the rollup options are prepared. +- `rollup:build`: Called when the rollup build is done. +- `rollup:dts:options`: Called when the rollup options for the declaration file are prepared. +- `rollup:dts:build`: Called when the rollup build for the declaration file is done. +- `rollup:done`: Called when the rollup build is done. + +- `mkdist:entries`: Called when the entries for `mkdist` are prepared. +- `mkdist:entry:options`: Called when the options for `mkdist` are prepared. +- `mkdist:entry:build`: Called when the build for `mkdist` is done. +- `mkdist:done`: Called when the build for `mkdist` is done. + +- `untyped:entries`: Called when the entries for `untyped` are prepared. +- `untyped:entry:options`: Called when the options for `untyped` are prepared. +- `untyped:entry:schema`: Called when the schema for `untyped` is prepared. +- `untyped:entry:outputs`: Called when the outputs for `untyped` are prepared. +- `untyped:done`: Called when the build for `untyped` is done. + +## Conclusion + +Finally, unbuild offers a versatile and efficient solution for building npm packages with minimal configuration. By leveraging rollup and mkdist, it streamlines the process of transpiling TypeScript into usable formats for npm distribution, ensuring a smooth workflow for developers. It automatically generates commonjs, esm, and declaration files, and provides security checks to prevent build issues. Its passive watcher and customizable hooks further enhance the developer experience, making it an excellent tool for modern package development. With its ability to infer settings from `package.json` and support for TypeScript out of the box, unbuild simplifies the build process, allowing developers to focus on their code rather than build configurations.