Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expose the partial Babel config for callers to load and mutate. #7472

Merged
merged 2 commits into from
Mar 8, 2018

Conversation

loganfsmyth
Copy link
Member

Q                       A
Fixed Issues? Not necessarily fixes, but related to #5541
Patch: Bug Fix? N
Major: Breaking Change? N
Minor: New Feature? Y
Tests Added + Pass? Yes
Documentation PR
Any Dependency Changes?
License MIT

This PR exposes .loadPartialConfig as a way to load the Babel configuration up to the point where the plugins and presets are all known, but have not yet loaded. It also exposes the path of the .babelrc and .babelignore files that Babel found, since a lot of tooling likes to toggle behavior based on whether the user has created a config file or not.

The goal of this PR is to expose enough of Babel's config that we can avoid tooling feeling like they have to implement it themselves. I think this achieves that, but if there are cases where that isn't the case I'd love to hear them.

See the README in this PR for more information, but the core of this PR is that it exposes a new first-class instance type ConfigItem that is allowed in plugins and presets lists, and the results of .loadPartialConfig ensures that each plugin and preset is a ConfigItem. From the README:

Each ConfigItem exposes all of the information Babel knows. The fields are:

  • value: {} | Function - The resolved value of the plugin.
  • options: {} | void - The options object passed to the plugin.
  • dirname: string - The path that the options are relative to.
  • name: string | void - The name that the user gave the plugin instance, e.g. plugins: [ ['env', {}, 'my-env'] ]
  • file: Object | void - Information about the plugin's file, if Babel knows it.
    • request: string - The file that the user requested, e.g. "@babel/env"
    • resolved: string - The full path of the resolved file, e.g. "/tmp/node_modules/@babel/preset-env/lib/index.js"

Beyond that, tooling can manipulate the config however they'd like before passing the config back off to Babel again.

@loganfsmyth loganfsmyth added the PR: New Feature 🚀 A type of pull request used for our changelog categories label Mar 3, 2018
@loganfsmyth loganfsmyth added this to the Babel 7.next milestone Mar 3, 2018
@loganfsmyth
Copy link
Member Author

cc @benjamn @KyleAMathews since I've talked to you two about this a little

@babel-bot
Copy link
Collaborator

babel-bot commented Mar 3, 2018

Build successful! You can test your changes in the REPL here: https://babeljs.io/repl/build/7145/

@KyleAMathews
Copy link

Looks great! /cc @jquense

@jquense
Copy link
Contributor

jquense commented Mar 3, 2018

Neat!

@@ -123,6 +123,78 @@ const parsedAst = babylon.parse(sourceCode, { allowReturnOutsideFunction: true }
const { code, map, ast } = babel.transformFromAstSync(parsedAst, sourceCode, options);
```

## babel.parse(code: string, [options?](#options): Object)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this return the AST or something else? Maybe we can add return types so I don't have to look up the code to see what this does. :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I'll add a note. Just realized we didn't document it when we actually landed that PR.

* `name: string | void` - The name that the user gave the plugin instance, e.g. `plugins: [ ['env', {}, 'my-env'] ]`
* `file: Object | void` - Information about the plugin's file, if Babel knows it.
* `request: string` - The file that the user requested, e.g. `"@babel/env"`
* `resolved: string` - The full path of the resolved file, e.g. `"/tmp/node_modules/@babel/preset-env/lib/index.js"`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is awesome, so in theory babel-loader can add every single plugin/preset as dependency (if coming from a file) so that webpack watch will trigger a change when one of them changes.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potentially, but since the presets aren't flattened, there are other plugins that aren't included here, so it wouldn't cover everything. Also since they are CommonJS, they'll still be cached by the module loader unless something also cleared that. I mostly exposed this since we already knew it and it seemed helpful for users. I'm figuring this will be good because they we can dump out a user's config in a human-readable format mostly.

* `babelrc: string | void` - The path of the `.babelrc` file, if there was one.
* `babelignore: string | void` - The path of the `.babelignore` file, if there was one.
* `options: ValidatedOptions` - The partially resolved options, which can be manipulated and passed back to Babel again.
* `plugins: Array<ConfigItem>` - See below.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

presets are not resolved to their plugins in this case?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If they are not resolved (which I guess might be useful for some use cases) is there a way to "resolve" presets to plugins?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is babel.loadOptions to get the fully flattened options/plugin list, but honestly I don't think manipulating that list is a good idea since it means that you're potentially changing the behavior of a preset that wouldn't otherwise be changeable.


export type { PartialConfig };

class PartialConfig {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there any value in this being a class?

also I havent ever understood the point of hiding "private" fields under public getters - would love to hear out the reasoning behind this structure, always seemed to me that it complicates a simple thing for no reason, especially that those getters do not have any additiona logic behind them

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly because I wasn't sure if we might want to add other helper methods and stuff on here in the future, but you're totally right it isn't required right now.

Copy link
Member Author

@loganfsmyth loganfsmyth Mar 3, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For instance I thought we might want a .hasFilesystemConfig() to do this.babelrc !== undefined || this.babelConfig !== undefined once we add the babel.config.js file.

@Andarist
Copy link
Member

Andarist commented Mar 3, 2018

This seems super useful for other tools 👏

that callers will take the config's `.options`, manipulate it as then see fit
and pass it back to Babel again.

* `babelrc: string | void` - The path of the `.babelrc` file, if there was one.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does loadPartialConfig only load the first .babelrc or it also loads its parents (when needed)? Also, what about babel.config.js?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It basically does everything to know what the configured plugins and presets will be, without executing them, so that includes loading all config files, and any options passed in programmatically, and merging them all together into a single config object. It just skips actually calling the plugin/presets function, and flattening the presets themselves.

@loganfsmyth
Copy link
Member Author

Alright, I updated the PartialConfig and ConfigItem classes to have public properties instead of getters, and I added the .hasFilesystemConfig function that I mentioned.

I also updated the babel.createConfigItem to just take the same input argument format that .babelrc items have, e.g. an array if you want to pass options. I think at the end of the day the consistency is nicer, and we could always expand it later if we really wanted.

@donaldpipowitch
Copy link

donaldpipowitch commented May 7, 2018

Sorry to capture this thread, but I'm a little confused what it the idiomatic way to load a config with the Node API.

So the README for babel.transform shows this example:

babel.transform(code, options, function(err, result) {
  result; // => { code, map, ast }
});

options is linked to all available options to Babel CLI. So clearly there must be a mapping from the CLI options to the Node API options.

We have loadOptions, loadPartialConfig and createConfigItem exposed (as well as (new OptionManager().init()) which just calls loadOptions() and was the Babel 6 way to load configs). While loadOptions() seems to mimic the old behavior they actually seem to behave differently.

Say I have a .babelrc.js using preset-env and a src/foo.js containing some code. Is this difference in behavior intended?

const opts = { filename: './src/foo.js' };

const { OptionManager } = require('babel-core');
console.log(new OptionManager().init(opts).plugins.length); // 0

const { loadOptions } = require('@babel/core');
// loadOptions(opts) same as (new require('@babel/core').OptionManager().init(opts))
console.log(loadOptions(opts).plugins.length); // 23

It looks like the plugins are only resolved in Babel 7 (and only when I give it a filename).

Is the idiomatic way to load a config (e.g. is this return value of loadOptions the actual value which should be passed to transform as the second param)? Should it be done for every file?

Should I use loadOptions() at all? E.g. transformSync(data, { filename }) returns the same as transformSync(data, loadOptions({ filename })). What is the use case of loadOptions? Load options once and re-use it across multiple transformSync calls, if you know they havent' changed?

Thank you :)

Update

Or is the use case to make in-place changes to existing configs before the transformation (e.g. like here)?

@lock lock bot added the outdated A closed issue/PR that is archived due to age. Recommended to make a new issue label Oct 5, 2019
@lock lock bot locked as resolved and limited conversation to collaborators Oct 5, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
outdated A closed issue/PR that is archived due to age. Recommended to make a new issue PR: New Feature 🚀 A type of pull request used for our changelog categories
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

9 participants