Skip to content

Plugin system refactoring - #2382

Merged
lukastaegert merged 10 commits into
masterfrom
plugin-driver
Aug 12, 2018
Merged

Plugin system refactoring#2382
lukastaegert merged 10 commits into
masterfrom
plugin-driver

Conversation

@guybedford

Copy link
Copy Markdown
Contributor

This should be an almost-backwards-compatible refactoring, with some minor error differences that will be fine to release under a new 0.x version here.

Changes:

  • We create a single plugin driver interface that is used to call the plugin hooks.
  • In the process we also allow plugins to now have their own individual contexts (opening up the door for each plugin having a unique .cache
  • Some error handling has been added for plugin name collisions
  • One big benefit is that now all plugin hook errors get "hook" and "plugin" added to them so that we can always track which plugin caused the original error. This is the user-facing breaking change, and will likely not even be noticed at all.

We can certainly work on improving the output further for coming majors, and hopefully by unifying the code paths this will now be easier.

Closes #2250.

@shellscape should recognise the base plugin approach from Webpack well :)

@guybedford

Copy link
Copy Markdown
Contributor Author

Note this is based to the asset-refactoring PR branch because that had some needed refactorings for the plugin context interface that were used here.

@guybedford

Copy link
Copy Markdown
Contributor Author

This also happens to include #2375.

@TrySound

TrySound commented Aug 6, 2018

Copy link
Copy Markdown
Member

Cool! However it's hard to read with big tabs. Could you also ignore \0 paths or do you consider using driver to handle these cases?

Comment thread src/Graph.ts Outdated
this.isExternal = options.external;
} else {
const ids = ensureArray(options.external);
this.isExternal = id => ids.indexOf(id) !== -1;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

i realize that this is out of scope of this PR, but maybe this function could be memoized? could get some minor perf boost for bigger projects with a lot of externals

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The indexOf operation shouldn't be costly here at all, so the main optimization case would be user-provided isExternal functions. In those cases I think custom memoization might be best in the hook itself?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

An alternative would be to change this to ensureSet and call ids.has(id) - if we're concerned with speed.

Comment thread src/utils/error.ts

const err = new Error(props.message);
export default function error(base: Error | RollupError, props?: RollupError) {
if (base instanceof Error === false) base = Object.assign(new Error(base.message), base);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

personally I think if (!(base instanceof Error)) { is less confusing, but ofc its just a personal taste of mine

Comment thread src/utils/pluginDriver.ts Outdated
if (existingPluginNames.indexOf(plugin.name) !== -1)
error({
code: 'DUPLICATE_PLUGIN_NAME',
message: `The plugin name ${

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

while this is true - plugins should have unique names, I'm not sure if you should fail the build in this case - this might cause trouble for rollup users because of a rollup plugin author's mistake

also - it seems reasonable to i.e. use rollup-plugin-babel multiple times (with different babel configs on different paths - using exclude/include options)

@Andarist

Andarist commented Aug 6, 2018

Copy link
Copy Markdown
Member

Could u also address not always reporting error.stack to the CLI?

Love errors getting more contextual info ❤️

@eight04

eight04 commented Aug 6, 2018

Copy link
Copy Markdown

Does it fix #2308?

@keithamus

Copy link
Copy Markdown
Contributor

👍 from me! Looks like this addresses most of #2308 as well, as errors/warnings will get consistent code and plugin values.

@shellscape shellscape left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

While I'm not a fan of the patterns that webpack chose in declaring hooks, I think the differences here work well. I'm also a huge fan of this being backwards compatible - not having to programmatically declare which hooks you want to use is a massive plus and good thing™️ for the developer experience.

This may be slight bikeshedding, but food for thought. The use of "hook" to define these is technically accurate by definition for most of the plugin hooks. However, the documentation up to this point has referred to them only as "properties". To provide distinction between Rollup and Webpack, and for clarity about what is technically a hook and what is not technically a hook (hooks seem to be by definition a function that can modify up/downstream), perhaps a different term could be considered. Nothing close to a serious concern, but wanted to toss that bit in there for posterity.

@guybedford

Copy link
Copy Markdown
Contributor Author

Yes this does fix #2308 as well I believe.

@guybedford

Copy link
Copy Markdown
Contributor Author

@Andarist help with pinpointing the exact issue you're referring to there would be appreciated.

@Andarist

Andarist commented Aug 7, 2018

Copy link
Copy Markdown
Member

The issue was that CLI swallows error.stack if error.frame is present. I believe while most users dont care about the stack its very useful for fixing problems upstream and should always be reported. As it being less useful for majority of users im OK with reporting it in grey color or smth (so it doesnt catch ur eye as easily) and keep red for message and frame

@guybedford

Copy link
Copy Markdown
Contributor Author

@Andarist is that exactly what #2307 is tracking? It should be possible to add the change to the CLI logging here. Would you be interested in making a PR?

@guybedford
guybedford changed the base branch from asset-refactoring to master August 8, 2018 10:14
@guybedford

Copy link
Copy Markdown
Contributor Author

I've rebased this to master, and included a Set fix for ensureArray.

@Andarist

Andarist commented Aug 8, 2018

Copy link
Copy Markdown
Member

@Andarist is that exactly what #2307 is tracking?

Among other things (some of them are resolved by this PR) - yes! Once this gets published I'm going to check the CLI error outputs, close my issue & open more focused ones (if needed) for stuff that didnt get addressed

It should be possible to add the change to the CLI logging here. Would you be interested in making a PR?

Should I create a PR to this PR? 😄

@guybedford

Copy link
Copy Markdown
Contributor Author

@Andarist that would be a great help.

@Andarist

Andarist commented Aug 8, 2018

Copy link
Copy Markdown
Member

I'll probably do it (wanted to do it anyway), not sure if I can manage to do it before this PR gets merged in though. Anyway - it's not blocking anything here, so don't want for me and I'll just do my thing when I get time to do it.

@lukastaegert lukastaegert left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Now this is a PR that I just love! Especially the abstractions to how to run hooks in a unified way are just awesome!

Just some minor comments here and there.

Comment thread bin/src/run/build.ts Outdated
let writePromise: Promise<any> = Promise.resolve();
for (const output of outputOptions)
writePromise = writePromise.then(() => <Promise<any>>bundle.write(output));
return writePromise.then(() => bundle);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I like the simplification but I wonder if it is possible to run the writes in parallel via Promise.all or if this may break things. Changing the code did not create any red tests for me but if these are potential ordering issues for hooks, it may be hard to reproduce them in tests.
If there are, we should at least add a comment why this cannot be changed, otherwise I would change to a Promise.all for the performance benefit.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That would be a perf benefit with no observable difference, well spotted, will add.

Comment thread src/rollup/types.d.ts
parentId: string,
isResolved: boolean
) => Promise<boolean | void> | boolean | void;
export type IsExternal = (id: string, parentId: string, isResolved: boolean) => boolean | void;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is potentially breaking in that previously, it seems it was possible that users returned a Promise for the external option which would have propagated here while this is now prevented. However since I could find no documentation for this, I prefer this simplification.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Previously we said in the type that it would permit a Promise, but the implementation didn't actually handle the promise. So this actually corrects the type to match what happens.

Comment thread src/utils/addons.ts Outdated

return { intro, outro, banner, footer, hash };
});
function strOrFn(strOrFn: string | (() => string | Promise<string>)) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Maybe normalizeStrOfFn or evaluateIfFn or similar as otherwise, it only describes the argument but not what we do with it. We could also explicitly add the return type to the signature so that it is immediately visible this generates either strings or Promises of string.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good suggestion.

Comment thread src/utils/addons.ts Outdated
].map(({ pluginName, source }, idx) => {
if (!source) return;
const reduceSep = (out: string, next: string) => (next ? `${out}\n${next}` : out);
const reduceDblSep = (out: string, next: string) => (next ? `${out}\n\n${next}` : out);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I know the name implies it is meant for a reduce but maybe concatSep and concatDblSep would work as well but tell us at a glance what the result is?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Sure

if (typeof specifier === 'string')
return <Promise<string>>this.resolveId(specifier, parentId);
}
};

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Really nice idea handling it like this 👍

Comment thread src/utils/default-plugin.ts Outdated
export function getRollupDefaultPlugin(options: InputOptions): Plugin {
return {
name: 'Rollup Core',
resolveId: resolveId(options),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I wonder if we should rename the rhs function to getResolveId or createResolveId to distinguish it from the actual hook which is the return value of this function?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Would probably trigger some renames in Graph as well, though.

Comment thread src/utils/pluginDriver.ts
if (err.code) err.pluginCode = err.code;
err.code = 'PLUGIN_ERROR';
err.plugin = plugin.name || '(anonymous plugin)';
error(err);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Maybe the code to normalize warnings and errors should actually be extracted into a shared function so that this can become something like
error(normalizePluginError('PLUGIN_ERROR')) and `graph.warn(normalizePluginError('PLUGIN_WARNING'))?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed this should be a next step handling error centralization. Would prefer to do that in successive PR work.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

👍

Comment thread src/utils/transform.ts
warning.plugin = plugin.name || '(anonymous plugin)';
warning.hook = 'transform';
graph.warn(warning);
},

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Again, the code for normalizing warnings and errors looks nearly identical. Maybe it could even be a central utility function to normalize warnings and errors from plugins with parameters to customize it for the respective hook and if it is a warning or error?
That way we ensure the warning/error handling is always consistent across hooks and have a single place to do changes?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Again, I didn't want to get tooo deep into the errors right now. There's a lot that can be done here.

Comment thread src/Graph.ts
} else {
const external = options.external;
const ids = new Set(Array.isArray(external) ? external : external ? [external] : []);
this.isExternal = id => ids.has(id);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Definitely an improvement! As all the ids are strings, we could also put them as keys into an object {[id: string]: true} for another very slight performance boost? Might be a micro-optimization, though.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

JS sets should be equal or better in performance according to theory.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Definitely looks a little cleaner so let's follow the theory. Googling shows that results are definitely inconclusive which is not necessarily what I would have expected, so the cleaner solution should win here 👍

@guybedford

Copy link
Copy Markdown
Contributor Author

Thanks for the review! Yes this has been on the list for a while, was glad to have a reason to get to it.

}

export function createResolveId(options: InputOptions) {
function createResolveId(options: InputOptions) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I removed the unnecessary export here which was probably an artefact.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants