GithubHelp home page GithubHelp logo

privatenumber / pkgroll Goto Github PK

View Code? Open in Web Editor NEW
804.0 6.0 16.0 2.34 MB

๐Ÿ“ฆ Zero-config package bundler for Node.js + TypeScript

License: MIT License

TypeScript 98.55% JavaScript 1.45%
javascript bundler typescript commonjs rollup dts exports esm cjs declaration

pkgroll's Introduction

pkgroll

pkgroll is a JavaScript package bundler powered by Rollup that automatically builds your package from entry-points defined in package.json. No config necessary!

Write your code in TypeScript/ESM and run pkgroll to get ESM + CommonJS + .d.ts outputs!

Features

  • โœ… package.json#exports to define entry-points
  • โœ… Dependency externalization
  • โœ… Minification
  • โœ… TypeScript support + .d.ts bundling
  • โœ… Watch mode
  • โœ… CLI outputs (auto hashbang insertion)

Premium sponsor banner

Install

npm install --save-dev pkgroll

Quick setup

  1. Setup your project with source files in src and output in dist (configurable).

  2. Define package entry-files in package.json.

    These configurations are for Node.js to determine how to import the package.

    Pkgroll leverages the same configuration to determine how to build the package.

    {
        "name": "my-package",
    
        // Set "module" or "commonjs" (https://nodejs.org/api/packages.html#type)
        // "type": "module",
    
        // Define the output files
        "main": "./dist/index.cjs",
        "module": "./dist/index.mjs",
        "types": "./dist/index.d.cts",
    
        // Define output files for Node.js export maps (https://nodejs.org/api/packages.html#exports)
        "exports": {
            "require": {
                "types": "./dist/index.d.cts",
                "default": "./dist/index.cjs"
            },
            "import": {
                "types": "./dist/index.d.mts",
                "default": "./dist/index.mjs"
            }
        },
    
        // bin files will be compiled to be executable with the Node.js hashbang
        "bin": "./dist/cli.js",
    
        // (Optional) Add a build script referencing `pkgroll`
        "scripts": {
            "build": "pkgroll"
        }
    
        // ...
    }

    Paths that start with ./dist/ are automatically mapped to files in the ./src/ directory.

  3. Package roll!

    npm run build # or npx pkgroll

Premium sponsor banner

Usage

Entry-points

Pkgroll parses package entry-points from package.json by reading properties main, module, types, and exports.

The paths in ./dist are mapped to paths in ./src (configurable with --src and --dist flags) to determine bundle entry-points.

Output formats

Pkgroll detects the format for each entry-point based on the file extension or the package.json property it's placed in, using the same lookup logic as Node.js.

package.json property Output format
main Auto-detect
module ESM
Note: This unofficial property is not supported by Node.js and is mainly used by bundlers.
types TypeScript declaration
exports Auto-detect
exports.require CommonJS
exports.import Auto-detect
exports.types TypeScript declaration
bin Auto-detect
Also patched to be executable with the Node.js hashbang.

Auto-detect infers the type by extension or package.json#type:

Extension Output format
.cjs CommonJS
.mjs ECMAScript Modules
.js Determined by package.json#type, defaulting to CommonJS

Dependency bundling & externalization

Packages to externalize are detected by reading dependency types in package.json. Only dependencies listed in devDependencies are bundled in.

When generating type declarations (.d.ts files), this also bundles and tree-shakes type dependencies declared in devDependencies as well.

// package.json
{
    // ...

    "peerDependencies": {
        // Externalized
    },
    "dependencies": {
        // Externalized
    },
    "optionalDependencies": {
        // Externalized
    },
    "devDependencies": {
        // Bundled
    },
}

Aliases

Aliases can be configured in the import map, defined in package.json#imports.

For native Node.js import mapping, all entries must be prefixed with # to indicate an internal subpath import. Pkgroll takes advantage of this behavior to define entries that are not prefixed with # as an alias.

Native Node.js import mapping supports conditional imports (eg. resolving different paths for Node.js and browser), but Pkgroll does not.

โš ๏ธ Aliases are not supported in type declaration generation. If you need type support, do not use aliases.

{
    // ...

    "imports": {
        // Mapping '~utils' to './src/utils.js'
        "~utils": "./src/utils.js",

        // Native Node.js import mapping (can't reference ./src)
        "#internal-package": "./vendors/package/index.js",
    }
}

Target

Pkgroll uses esbuild to handle TypeScript and JavaScript transformation and minification.

The target specifies the environments the output should support. Depending on how new the target is, it can generate less code using newer syntax. Read more about it in the esbuild docs.

By default, the target is set to the version of Node.js used. It can be overwritten with the --target flag:

pkgroll --target=es2020 --target=node14.18.0

It will also automatically detect and include the target specified in tsconfig.json#compilerOptions.

Strip node: protocol

Node.js builtin modules can be prefixed with the node: protocol for explicitness:

import fs from 'node:fs/promises'

This is a new feature and may not work in older versions of Node.js. While you can opt out of using it, your dependencies may still be using it (example package using node:: path-exists).

Pass in a Node.js target that that doesn't support it to strip the node: protocol from imports:

pkgroll --target=node12.19

Export condition

Similarly to the target, the export condition specifies which fields to read from when evaluating export and import maps.

For example, to simulate import resolutions in Node.js, pass in node as the export condition:

pkgroll --export-condition=node

ESM โ‡„ CJS interoperability

Node.js ESM offers interoperability with CommonJS via static analysis. However, not all bundlers compile ESM to CJS syntax in a way that is statically analyzable.

Because pkgroll uses Rollup, it's able to produce CJS modules that are minimal and interoperable with Node.js ESM.

This means you can technically output in CommonJS to get ESM and CommonJS support.

require() in ESM

Sometimes it's useful to use require() or require.resolve() in ESM. ESM code that uses require() can be seamlessly compiled to CommonJS, but when compiling to ESM, Node.js will error because require doesn't exist in the module scope.

When compiling to ESM, Pkgroll detects require() usages and shims it with createRequire(import.meta.url).

Environment variables

Pass in compile-time environment variables with the --env flag.

This will replace all instances of process.env.NODE_ENV with 'production' and remove unused code:

pkgroll --env.NODE_ENV=production

Minification

Pass in the --minify flag to minify assets.

pkgroll --minify

Watch mode

Run the bundler in watch mode during development:

pkgroll --watch

FAQ

Why bundle with Rollup?

Rollup has the best tree-shaking performance, outputs simpler code, and produces seamless CommonJS and ESM formats (minimal interop code). Notably, CJS outputs generated by Rollup supports named exports so it can be parsed by Node.js ESM. TypeScript & minification transformations are handled by esbuild for speed.

Why bundle Node.js packages?

  • ESM and CommonJS outputs

    As the Node.js ecosystem migrates to ESM, there will be both ESM and CommonJS users. A bundler helps accommodate both distribution types.

  • Dependency bundling yields smaller and faster installation.

    Tree-shaking only pulls in used code from dependencies, preventing unused code and unnecessary files (eg. README.md, package.json, etc.) from getting downloaded.

    Removing dependencies also eliminates dependency tree traversal, which is one of the biggest bottlenecks.

  • Inadvertent breaking changes

    Dependencies can introduce breaking changes due to a discrepancy in environment support criteria, by accident, or in rare circumstances, maliciously.

    Compiling dependencies will make sure new syntax & features are downgraded to support the same environments. And also prevent any unexpected changes from sneaking in during installation.

  • Type dependencies must be declared in the dependencies object in package.json, instead of devDependencies, to be resolved by the consumer.

    This may seem counterintuitive because types are a development enhancement. By bundling them in with your package, you remove the need for an external type dependency. Additionally, bundling only keeps the types that are actually used which helps minimize unnecessary bloat.

  • Minification strips dead-code, comments, white-space, and shortens variable names.

Sponsors

Premium sponsor banner Premium sponsor banner

pkgroll's People

Contributors

adaliszk avatar ayuhito avatar logotip4ik avatar privatenumber avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

pkgroll's Issues

Hashbang not included in CLIs

I think the current version doesn't work to include the hashbang for any files included in the bin field of package.json. Or more accurately, I don't think the bundler even recognises bin field at all, considering the cli.ts file is compiled to both cli.js and cli.mjs when pkgroll is run, rather than just cli.js with just the hashbang.

Here's a simple repro, although there really isn't anything special to it. I've copied the instructions on the README which leads to this bug. The dist files are shown so you can see the result.

I wonder if #7 is related to this?

Support rollup plugins or use @rollup/plugin-strip to remove debugger statements.

Feature request

Customize the use of the rollup plugins or use parameters (such as --strip) when executing the command to enable the @rollup/plugin-strip package.

Motivations

Debugger statements used in development are not used in production, so I want to use the plugins to remove these debugger statements.

Whether to support the use of additional configuration files to customize the rollup plugins?

Alternatives

No response

Additional context

No response

Contributions

  • I plan to open a pull request for this issue
  • I plan to make a financial contribution to this project

feat: tsconfig paths support

Feature request

I understand that pkgroll is based off of package.json config - however, it would be really nice to have a build system that operates exactly like tsx. I use tsx for dev, since the watch/file execution is the fastest i've found by far. However, this means that I must use something like tsup, or ts-node for actual production building.

This is a concern for me, as tsx auto-configures, but these other libraries do not. I want as few deviations as possible between my dev environment with tsx and the production build for obvious reasons - I want a similar output format, build process, etc to reduce the chance of production-build-only bugs.

One major way to close this gap is to support tsconfig paths. Today, wildcard matching paths isn't possible with package.json.

Motivations

Closing the gap between my dev and full build process.

Alternatives

No response

Additional context

No response

Contributions

  • I plan to open a pull request for this issue
  • I plan to make a financial contribution to this project

Type definition generated for CommonJS output using `module.exports = ` is not usable

Problem

The type definition file generated for CommonJS output using module.exports = syntax is not consumable by TS projects without esModuleInterop enabled or JS projects with type checking or editors with support for type hints on JavaScript files.

For example, given this TypeScript input:

// src/index.ts
export default function Foo() {}

The JavaScript file generated when using pkgroll v2.0.1 will be:

// dist/index.cjs
module.exports = function Foo() {}

And this will be created along with the following type definition:

// dist/index.d.cts
declare function Foo(): void;

export { Foo as default };

However, when a project using CommonJS modules consumes the compiled files like so:

// @ts-check
const Foo = require('./dist/index.cjs');

new Foo()

Then they will receive the following error when running a type check on their code or attempting to use type hints in their editor because these are expecting the implementor to reference a property named "default":

This expression is not constructable.
  Type 'typeof import("/Users/matthinchliffe/Code/test-case/dist/index")' has no construct signatures.ts(2351)

Because there's no equivalent of module.exports = with ESM the Rollup documentation suggests always using named export mode to avoid creating two different interfaces:

In other words for those tools, you cannot create a package interface where const lib = require("your-lib") yields the same as import lib from "your-lib". With named export mode however, const {lib} = require("your-lib") will be equivalent to import {lib} from "your-lib".

So whilst neither of these may be ideal I understand there are two potential solutions for this problem:

  1. Manipulate the type definition to output export = syntax.
  2. Use Rollup's named export mode to output exports.default =

(p.s. apart from this hiccup, the project has been A+, thank you)

Expected behavior

The module imported using require should have the correct type definition applied and editor type hints enabled. The following type definition would work:

declare function Foo(): void;

export = Foo;

Alternatively, the following generated JS would work:

exports.default = function Foo() {}

Minimal reproduction URL

https://gist.github.com/i-like-robots/29b8662210f6898626f40d49e8e39e68

Version

v2.0.1

Node.js version

v18.18

Package manager

npm

Operating system

macOS

Contributions

  • I plan to open a pull request for this issue
  • I plan to make a financial contribution to this project

support iife export format

Feature request

Provide an option for an iife type of bundle.

Motivations

Libraries that target browser environments usually need an iife export to be injectible by html script tag.

Alternatives

The best thing to do right now, is to use rollup to have iife exports.

Additional context

I understand how this bundler works and loved it. I don't want it to be complicated by config files or more more dependencies. Still I'm curious what the developer would think about this.

Contributions

  • I plan to open a pull request for this issue
  • I plan to make a financial contribution to this project

Inaccurate type externalization recommendations

Problem


Recommendation: "@types/eslint" is externalized because "eslint" is in "peerDependencies". Place "@types/eslint" in "peerDependencies" as well so users don't have missing types.
Recommendation: "@types/eslint__eslintrc" is externalized because "@eslint/eslintrc" is in "dependencies". Place "@types/eslint__eslintrc" in "dependencies" as well so users don't have missing types.
Recommendation: "@types/eslint__js" is externalized because "@eslint/js" is in "dependencies". Place "@types/eslint__js" in "dependencies" as well so users don't have missing types.
Recommendation: "@types/confusing-browser-globals" is externalized because "confusing-browser-globals" is in "dependencies". Place "@types/confusing-browser-globals" in "dependencies" as well so users don't have missing types.

However, my type file only imports from eslint:

import { Linter } from 'eslint';

/**
 * These specific signatures are needed to make sure that the return type is
 * narrowed to the input type.
 */
declare function defineConfig<T extends Linter.FlatConfig>(config: T): T;
declare function defineConfig<T extends Linter.FlatConfig[]>(config: T): T;

type Options = {
    node?: boolean;
};
declare const pvtnbr: (options?: Options) => Linter.FlatConfig[];

export { type Options, defineConfig, pvtnbr };

Expected behavior

To only recommend externalizing types that are imported/bundled

Minimal reproduction URL

N/A

Version

N/A

Node.js version

N/A

Package manager

npm

Operating system

macOS

Contributions

  • I plan to open a pull request for this issue
  • I plan to make a financial contribution to this project

Recommendation to move type to dependency

The recommendation to move type to dependency should only happen when bundling types:

Recommendation: "@types/ini" is externalized because "ini" is in "dependencies". Place "@types/ini" in "dependencies" as well so users don't have missing types.

And ideally, if the type dependency is imported.

Include rm -rf to dist folder

The dist folder end up becoming very polluted at times, especially when you delete a source file which still leaves its bundled-up counterpart in the dist folder. Either cleaning up the dist folder by default or including a --clean flag would be a nice QOL addition.

(Btw, I am happy to make some PRs in the near future for some of these issues I'm making - imo this bundler is the most ideal one out of others I've seen and I'm working on integrating it into the Fontsource project).

Support/Allow Typescript 5 as a Peer Dependency

Great work on this tool! I was looking for something simpler than tsup and came across your simple yet powerful solution!

I have been playing around a bit, and it seems that Typescript 5 works just fine, so could we add that to the peer dependencies (like ^4.1 || ^5.0) so that we would not get warnings about that?

I ran into this specifically with PNPM, but I assume the same warning happens in other tools.

Possible bug related to --dist

While writing some tests for #11, I was a little confused about this test for the --dist flag.

Is it intended for the command to be used as pkgroll --dist ./nested or as written in the test pkgroll --dist .? I'm not sure if I agree with the test in this case as being intended behaviour.

I was originally intending to use distPath for #11, but that wouldn't work with the current --dist flag setup since we'd be cleaning the . dir instead of ./nested.

Switching it pkgroll --dist ./nested in the test results in the following error:

/home/lotus/pkgroll/node_modules/.pnpm/[email protected]/node_modules/execa/lib/error.js:59
                error = new Error(message);
                        ^

Error: Command failed with exit code 1: /home/lotus/.cache/nve/12.22.9/x64/bin/node /home/lotus/pkgroll/dist/cli.js --dist ./nested
Error: Could not find mathing source file for export path "./nested/index.d.ts"
    at tD (/home/lotus/pkgroll/dist/cli-0d8a4c1f.js:21:5041)
    at async /home/lotus/pkgroll/dist/cli-0d8a4c1f.js:37:19127
    at async Promise.all (index 2)
    at async /home/lotus/pkgroll/dist/cli-0d8a4c1f.js:37:19086
    at null.makeError (/home/lotus/pkgroll/node_modules/.pnpm/[email protected]/node_modules/execa/lib/error.js:59:11)
    at null.handlePromise (/home/lotus/pkgroll/node_modules/.pnpm/[email protected]/node_modules/execa/index.js:119:26)
    at processTicksAndRejections (node:internal/process/task_queues:96:5)
    at null.pkgroll (/home/lotus/pkgroll/tests/utils/pkgroll.ts:18:6)
    at null.<anonymous> (/home/lotus/pkgroll/tests/specs/builds/src-dist.ts:88:27)
    at null.<anonymous> (/home/lotus/pkgroll/node_modules/.pnpm/[email protected]/node_modules/manten/dist/index.js:36:9) {
  shortMessage: 'Command failed with exit code 1: /home/lotus/.cache/nve/12.22.9/x64/bin/node /home/lotus/pkgroll/dist/cli.js --dist ./nested',
  command: '/home/lotus/.cache/nve/12.22.9/x64/bin/node /home/lotus/pkgroll/dist/cli.js --dist ./nested',
  escapedCommand: '"/home/lotus/.cache/nve/12.22.9/x64/bin/node" "/home/lotus/pkgroll/dist/cli.js" --dist "./nested"',
  exitCode: 1,
  signal: undefined,
  signalDescription: undefined,
  stdout: '',
  stderr: 'Error: Could not find mathing source file for export path "./nested/index.d.ts"\n' +
    '    at tD (/home/lotus/pkgroll/dist/cli-0d8a4c1f.js:21:5041)\n' +
    '    at async /home/lotus/pkgroll/dist/cli-0d8a4c1f.js:37:19127\n' +
    '    at async Promise.all (index 2)\n' +
    '    at async /home/lotus/pkgroll/dist/cli-0d8a4c1f.js:37:19086',
  failed: true,
  timedOut: false,
  isCanceled: false,
  killed: false
}

Possibly related to the dts plugin not taking in the dist arg properly? I could dig around a little if you have any clues which file may be related.

Ignore unresolvable entry paths

Problem

for the following exports:

  "exports": {
    "./modules/dayjs.js": {
      "import": {
        "types": "./dist/modules/dayjs.d.ts",
        "default": "./dist/modules/dayjs.js"
      }
    },
    "./svelte/store/device.js": {
      "import": {
        "types": "./dist/svelte/store/device.d.ts",
        "default": "./dist/svelte/store/device.js"
      }
    },
    "./Banner.svelte": {
      "types": "./dist/svelte/components/elements/LoadingBar.svelte.d.ts",
      "svelte": "./dist/svelte/components/elements/LoadingBar.svelte"
    },
  },

the lib is trying to read .svelte files too which is not recognized in node env:

Error: Could not find matching source file for export path "./dist/svelte/components/elements/Banner.svelte"

Expected behavior

i package svelte files with a different bundler (@sveltejs/package) therefore i would expect an option to ignore some files in exports.

Minimal reproduction URL

https://stackblitz.com/edit/vitejs-vite-kw52rd?file=package.json

Version

v2.0.2

Node.js version

v20.9

Package manager

npm

Operating system

macOS

Contributions

  • I plan to open a pull request for this issue
  • I plan to make a financial contribution to this project

Add an option to use a different tsconfig.json file

It's quite common for projects to use environmentally specific file such as tsconfig.prod.json and tsconfig.dev.json instead of the default tsconfig.json.

Would be great to have a CLI option to specify which file should be considered.

Love this tool BTW.

shims for __dirname so one code can output to both esm and cjs

Feature request

shim for _dirname or other features like this

This is something tsup supports (and esbuild) : https://github.com/egoist/tsup/blob/00188a0dc848c48fac45de245d1e021f370a84a3/src/cli-main.ts#L91

Motivations

My project export to both cjs and esm and being able to use the same code for both is needed

Alternatives

No response

Additional context

No response

Contributions

  • I plan to open a pull request for this issue
  • I plan to make a financial contribution to this project

Change default target to LTS

I think having the default target to be reliant on process.versions.node creates a lot of inconsistency especially if you use a multitude of environments such as local and CI.

To fit closer to the paradigm of zero-config, it might be better for the bundler to default to Node LTS instead for best practices.

I'm not sure if this should be considered a breaking change considering the existing setup already led to inconsistent bundles.

Detect Vitest for in-source testing

Love this bundler and I appreciate the peace of mind having not to configure anything for things to just work.

Vitest does support in-source testing which I think is great when writing unit tests, however, since this bundler isn't extensible, it isn't possible for us to modify the Rollup config to tree-shake the testing code out as specified in their docs.

Would it be possible to merge the following replace config into the default Rollup config of this project? Whilst the addition of this shouldn't affect non-Vitest users, we can still include a check just incase to only include it if Vitest is present in the package.json.

// rollup.config.js
+ import replace from '@rollup/plugin-replace'

export default {
  plugins: [
+   replace({
+     'import.meta.vitest': 'undefined',
+   })
  ],
  // other options
}

Support for subpath patterns

Currently, subpath patterns defined in package.json raise an exception. Not sure if it should be considered a bug or a feature request.

package.json

...
        "./*.js": {
            "import": "./dist/*.mjs",
            "require": "./dist/*.cjs",
            "default": "./dist/*.mjs"
        }
...

Error: Could not find matching source file for export path "./dist/*.cjs

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.