developit / microbundle

📦 Zero-configuration bundler for tiny modules.

Home Page:https://npm.im/microbundle

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Support for multiple "entries"

Andarist opened this issue · comments

Having this implemented would resolve this issue.

The idea is that we need to output private package.json files to support appropriate es/cjs resolving, I believe that @kentcdodds got it "right" here. Although you might prefer having some convention for creating additional "entries" over additional config flag.

I will work on implementing this, hopefully some time soon~

Indeed - right now I have to run microbundle multiple times in order to handle disparate input-output flows.

So I was thinking how this thing can be handled, I think there are 2 fundamental ways of providing this feature (I'll talk about them later)

At the moment there is a possibility to pass multiple entry points, even with a glob pattern and for each of the entry points microbundle creates several files (1 for each format) - they all land in the single, specified directory. I think that there is actually no need for this in its current form. I mean - there is a need for a single entry point configured with standard package.json fields (main, module) and there is a need for creating additional, alternative, entry points which need to get "proxy" directories with single package.json in them (example of such usage would be stockroom/inline).

Actually we could also support additional targets, but how to do that have to be figured out. What I mean by additional targets? React Native (react-native field in package.json) and better differentiating between node & browser builds (leveraging browser field in the root package.json). That is just another problem though, but I'll discuss it later - at the end of the post.

Also ideally we could run a single command of microbundle and have it all done in a single pass.

Configuration

At the moment when configuring multiple entries we can specify both input files and output directory, but it cannot quite work like that - when providing additional/alternative entries we need to reflect entry name in the package's directory structure. So actually we have 2 options for this:
a) output created files to the dist (output) directory, like normal, and create "proxy" directories somehow. But how? Their names have to be specified (we could come up with some convention based on the structure in the source directory) - if we provide a config option for this it seems that it will be quite complicated to use this feature, if we go with convention it could work
b) output created files to created "proxy" directories - again we have a problem of specifying input files/entries names pairs, but if we ignore output option entirely for those it seems reasonable to rely on convention and be done with it

Convention

We could support special name.entry.js convention and do all of the fancy stuff mentioned above based on it.

My choice would be...

A mix of configuration and convention approaches - I wouldn't ignore output option ever and create "proxy" directories based on the convention (structure of the source directory). That way all of the created files would be in a single directory (clarity) and we would have a nice way of specifying additional entries (opt-in when options gets used) with automatic creation of "proxy" directories.

Additional notes
Would be cool to warn people that they have to update their files in package.json (or that they cannot ignore those in .npmignore) with created directories.

Additional "main" entries - react-native, browser
As to react-native - metro bundler understands .native.js so it should be possible to provide "overrides" for each file with a sibling file with that extension (support for this can be easily added by providing extensions: ['.native.js', '.js'] to the rollup-plugin-node-resolve). Before bundling we can check if for the specified entry point there is an additional .native.js file and if there is we can create additional output bundle for it. We should also check for existance of react-native key in the root package.json - in such case we should create additional output bundle and add .native.js to possible extensions (in this case we wouldnt check if there is entry_name.native.js file, because we know that the package wants to have react-native alternative, as it specifies it in the package.json, but it might want to "override" only some modules and not the entry point itself). For react-native it's possible to output only a cjs format as it doesnt support anything else.

Similar thing could be done for browsers, but we would have to "invent" (?) .browser.js extension and automatically treat main as the entry point targeting node. About this I'm not yet sure how exactly it would work, and I need to wrap this up (wife looking at me dangerously 😉), so I'll leave this thought for now.

I think I'd prefer to have Microbundle generate directories with derived package.json files like Kent's solution. It would be difficult to infer when a file needs to be treated as an entry versus just a second bundle, we'll likely have to resort to using configuration for that (or the filename convention you mentioned, but I am wary of dynamically changing names - that breaks src / dist parity which makes Node a pain).

but I am wary of dynamically changing names - that breaks src / dist parity which makes Node a pain).

Could you elaborate on this? There is no parity at the moment (w are flat bundling) between src and dist.

I think convention files are more error-prone, so I'd suggest configuration is a better choice for this. Anyhow - I've started working on this slowly, hopefully I'll present a PR soon-ish, so we can discuss used approach then.

Ah but there is, just it's only for multiple entries: their names and locations are mirrored in dist and they are automatically treated as externals of eachother.

Would be great to add a test for this to see whats the expected result. By quickly inspecting unistore and stockroom dist files I cant see any correlation between built files, all seem to be completely different bundles without sharing deps between each other (except probably some externals from package.json)

OK, i think I've found example use of what you were talking about (which is microbundle itself 😄 ):

Ah but there is, just it's only for multiple entries: their names and locations are mirrored in dist and they are automatically treated as externals of eachother.

Need to look later how unistore/stockroom are different (maybe they just do not refer to each other? i suspect now that they do not refer to their "entries" directly, but probably just to some other imported internal modules and thats why other modules get duplicated - which also doesnt seem ok) or if Ive missed something when inspecting them.

I'm not sure yet, because I haven't experimented with this freshness in rollup yet, that ultimately we should use code-splitting (which is available already). This would simplify the microbundle's code, make those things more automatic and we could end up with no duplication between outputted files.

Seriously, i still can't understand for what you are talking about 😆

Rollup support multiple entry points anyway, even without the code splitting and etc, just pass array to input option, which array even can hold not only strings but inputOptions.

Regarding input:

String/ String[] The bundle's entry point (e.g. your main.js or app.js or index.js). If you enable experimentalCodeSplitting, you can provide an array of entry points which will be bundled to separate chunks.

I think what you mean is that multiple configuration can be passed to rollup in array form, i.e.

export default [cjsConfig, esConfig, umdConfig]

Those configs would be completely separate though and we'd ideally would like to end up with 0 repetition between output bundles.

Oh yea yea, I was sleepy 😪

Yup, finally got it. And recently i backed to the @rolldown, so i quickly implemented it in few lines, of course plus bonus features as it was dreamt.

rolldown-carbon

Support rich config files, multiple entry points and basic placeholders.

rolldown-config-carbon

Creates the following bundles

  • ESM: dist/es-bundle-foo.mjs for the src/foo.js
  • CJS: dist/foo.cjs.js for the src/foo.js
  • ESM: dist/es-bundle-bar.mjs for the src/bar.js
  • CJS: dist/bar.cjs.js for the src/bar.js
  • CJS: dist/cli-cli.common.js for src/cli.js

Is this feature still being worked on? Keen to see it implemented!

I find it confusing that's cited as a feature in the README.md.
or am I missing something? should we remove that line?

- Supports multiple entry modules _(`cli.js` + `index.js`, etc)_

Same @leonardodino Is there already a way to support multiple entry modules? Or is the README wrong?

If there is already a way to support multiple non-disparate entry modules, it should probably be documented otherwise it's just confusing to say it exists but not say how to use it

Same @leonardodino Is there already a way to support multiple entry modules? Or is the README wrong?

If there is already a way to support multiple non-disparate entry modules, it should probably be documented otherwise it's just confusing to say it exists but not say how to use it

I'm trying to do the same thing, I want to create a single package for a collection of React components but make it possible for users to access sub components without having to import everything else. Struggling to find a way to do this at the moment.

@gregrafferty No need for separate bundles. Just use named exports and all popular bundlers will take care of the rest via tree-shaking.

@gregrafferty No need for separate bundles. Just use named exports and all popular bundlers will take care of the rest via tree-shaking.

Ah that's great, thanks for the info. I've had a few problems with this in the past so sometimes I get a bit paranoid about it!

FYI: We're going to support package exports.

Being able to specify multiple input > output files, would be most welcome.

I'm using microbundle to bundle my serverless functions. I currently have 1 npm script per function, and need to maintain a mapping when adding / removing functions.

A glob pattern, or even just multiple entries that map to a output file in a folder, would be of big help.

I'm trying to conditionally include logic or dependencies based on platform (node.js vs web), with Microbundle. My package supports both platforms but needs extra dependencies in Node.js. The article How to write a JavaScript package for both Node and the browser suggests doing this with the pkg.browser field and multiple entries. Is this the right issue to watch, or have I misunderstood? Multiple entries would help my case, package exports might not. 🤔

@donmccurdy Why do you think exports might not help? Have you read https://nodejs.org/api/packages.html#packages_conditions_definitions and https://nodejs.org/api/packages.html#packages_conditional_exports? There it is explained that a "browser" key can be used.

I'm not sure what to make of a "browser" exports key being endorsed by Node.js — it's the bundlers for web that I need to support that field. 🙂 There's also a root browser key in package.json, which I think bundlers do support, but can I connect that to a different source entry with microbundle?

@donmccurdy Ah! For multiple sources I believe you have to run microbundle multiple times. The same way you have to run it multiples times if you want to export multiple subpaths. See for instance how preact does it. Then you can combine that with either the broswer key or the browser conditional, depending on what targets support.

This is my most wanted feature 😄

Hey, what's the status of this feature? Or even, what's the right way to do this at the moment? Is it multiple package.json? Multiple CLI runs? With which package fields?

Hmmmm, this seems to work already or am I missing something?

microbundle src/{a,b}.js

Hmmmm, this seems to work already or am I missing something?

microbundle src/{a,b}.js

But can they have different outputs?

Example: i have entry src/A.js and src/B.js, can they be emmitted to A/index.js and B/index.js?

I stopped having this use case, but yes, as I remember that worked using the exports (this is from the README):

{
	"name": "foo",
	"exports": {
		".": "./dist/foo.modern.js", // import "foo" (the default)
		"./lite": "./dist/lite.modern.js", // import "foo/lite"
		"./full": "./dist/full.modern.js" // import "foo/full"
	},
	"scripts": {
		"build": "microbundle src/*.js" // build foo.js, lite.js and full.js
	}
}

with {a,b}.js that would be

{
	"name": "foo",
	"exports": {
		".": "./dist/foo.modern.js",
		"./a": "./dist/a.modern.js", 
		"./b": "./dist/b.modern.js" 
	},
	"scripts": {
		"build": "microbundle src/{a,b}.js"
	}
}

but I am not 100% sure, sorry

Keep in mind that this makes microbundle generate all that, it does not mean that you are restricted to accessing the generated files through monder exports fields. You can additionally add a files field in your package json to expose the generated a and b files

Tried here and didn't worked. I did with an script file.

If anyone in the future wants:

#!/usr/bin/env bash

echo Starting build...

packages=(
  a
  b
)

rm -rf ${packages[@]}

echo Target cleared...

for package in "${packages[@]}"; do
  yarn microbundle -i src/$package.ts -o $package/index.js --tsconfig 'tsconfig.build.json' --name ${package^} &
done

wait

echo Build complete!

And in my package.json

{
 "scripts": {
   "build": "bash build.sh"
 },
 "exports": {
    "./b": {
      "import": "./b/index.esm.js",
      "node": "./b/index.js",
      "require": "./b/index.js",
      "default": "./b/index.modern.js"
    },
    "./a": {
      "import": "./a/index.esm.js",
      "node": "./a/index.js",
      "require": "./a/index.js",
      "default": "./a/index.modern.js"
    }
  }
}

EDIT: Added --name with the uppercased package name because in umd builds, the global object name was beeing the name field in the package.json

@arthurfiorette Just a heads up: having "default" first means the rest of those keys do absolutely nothing. Keys are ordered, and as "default" will always match, "import", "node", and "require" will never be used.

Within the "exports" object, key order is significant. During condition matching, earlier entries have higher priority and take precedence over later entries. The general rule is that conditions should be from most specific to least specific in object order.

https://nodejs.org/api/packages.html#conditional-exports

Thanks @arthurfiorette . In addition to your code, I had to use this as my project was using an older version of typescript https://stackoverflow.com/a/69791012/9406951.

  "typesVersions": {
    "*": {
      "*": [
        "dist/types/index.d.ts"
      ],
      "module2": [
        "dist/types/module2.d.ts"
      ]
    }
  }

And for those looking to export a default module, use the exports as mentioned by @LukasBombach here #50 (comment). Thanks @LukasBombach

 "exports": {
    ".": {
      "import": "./dist/index.esm.js",
      "node": "./dist/index.js",
      "require": "./dist/index.js",
      "default": "./dist/index.modern.js"
    },
    "./module2": {...}
  }