Open timmfin opened 8 years ago
To answer this properly, I should probably explain/document the new __broccoliGetInfo__
API. That API would be the correct way to interact with nodes and wrap them. There's some other stuff I want to do first, but think I can get to it within a week.
Sounds good to me, thanks!
@timmfin Although this wont be an immediate thing for me to work on, I believe exploring this line of thought will be good. There are many cases, especially while discovering new files or projects to import based on contents of existing files, where building the tree dynamically would be beneficial.
@stefanpenner ok, sounds good.
Though I haven't been using Broccoli for over a year now, so please feel free to close, move to a different issue, or whatever you think is best (but I appreciate the revisit nonetheless!)
Recently, I got the time again to dig back into my work on revamping HubSpot’s front end build system. I hadn’t done much with it for a while, and figured I’d start by updating all my dependencies to the latest, the main of which was taking broccoli from v0.14.x to v1.0.0-beta.5.
Overall the changes in v1 seem great, and will hopefully lead to plugins that are easier to write and more performant builds, but it seems like new API made it harder to write utility plugins that compose nodes/trees (as I originally mentioned in this twitter conversation).
tl;dr (because yeah, this is long)
this._inputNodes
)_inputNode
manipulation methods on broccoli-plugin (example gist)/cc @stefanpenner @joliss
So what actually is the concern?
Before v1, broccoli did not build the graph ahead of time. And as a side-effect of how that traversal used to work, plugins had the ability to create a Promise on the completion of any particular input node (via the readTree function). So you could compose input nodes at will, like this silly, hypothetical (ES2015-ful) example:
That example is way over the top, but it shows how a plugin could compose a various number of input nodes and call any code/hooks (whether it was debugging statements or actual plugin manipulation logic) as it desired.
However, in v1 I see no straightforward way to write a plugin like this. You don’t have access to your
inputNode
’s promises or any of the builder’sbeginNode
orendNode
hooks. Yes it is totally true that thereadTree
-style code is darn hard to follow, and the v1 API makes most plugin implementations much clearer; nonetheless, I’d argue losing this compositional flexibility is a bummer. I don't think missing it is a "show stopper", but I do think broccoli would be less powerful and miss out on a whole set interesting/useful (edge-case?) plugins without it.What can we do with v1 beta as is
Alrighty, time to be constructive. Let’s experiment and see what we can do with v1 before trying to change it. The only two real avenues I saw as possibilities were:
inputNode
s before the builder creates its topologically sorted list.build
method on otherinputNode
s to hook into their execution.Originally, I started to mess with the latter, but it wasn’t working very well. It was too easy to goof things up, like not apply the original build method with the correct context. And from a debugging perspective, having other code replace your function was a pain in the butt.
So I gave manipulating inputNodes, more specifically manipulating the “private”
passedInputNode._inputNodes
array created via broccoli-plugin, a shot. You can see a realistic example of this in before-build.js on the experimental move-to-broccoli-v1.0.0 branch of my broccoli-stew fork. Here is the core of what is going on with extraneous stuff removed and plus some comments:This still is pretty hacky, since we are messing with someone else’s “private”
_inputNodes
array. Though it is less hacky than overwriting the build method on other plugins. So if we had no other option, this seems to be best route to doing this sort of thing in the v1 beta.Hypothesizing changes to v1 beta
Next, let's think about what broccoli core changes we could make to enable easier plugin composition/hooks. Here are some "straightforward" considerations:
RSVP.EventTarget
(or similar event system). And while iterating over nodeWrappers during the build, do anw.originalNode.trigger('begin/endNode')
if the trigger method exists.RSVP.EventTarget
to broccoli-plugin, add a few more things to the object returned via__broccoliGetInfo__
. For example,beginBuild
andendBuild
keys that can be a list of functions (that the builder will call later).originalNode
acurrentBuildPromise
method. It would return a promise that is resolved when its build is finished. And the start of every build would need to clear those promises out and create new ones.However, for any of those to fully work, we'd also need a hook for each plugin to know that a another rebuild has started (or an existing build finished). Because we may need to re-create event handlers or promise chains for every new build.
Here is a rough thought experiment of a plugin using #1 (likely similar to 2 and 3 above anyway). But it still feels a bit off to me. Forcing the plugin to have to worry about reseting state/handlers every rebuild feels problematic.
Yet another approach would be to go back to input node manipulation, but officially condone it with new methods on broccoli-plugin. For example:
Plugin.prototype.appendChildNode
that will append a new node to the end ofthis._inputNodes
for you (possibly automatically using something like broccoli-quick-plugin if you only give it a function instead of a actual node instance?)Plugin.prototype.wrapChildNode
that will replace node at index X ofthis._inputNodes
with a new wrapperinsertChildNodeBefore(childNode)
Plugin.prototype.wrap
that will return a new node that wrapsthis
for you (or maybe better to keep that separate function/plugin,wrap(plugin, ...)
kinda like broccoli-quick-plugin)?nodeType
to the builder. Right now there aresource
andtransform
types...hook
orfunction
node type whose sole purpose to execute a bit of code.inputNodes
or the filesystem at all, making them easier to add without disrupting the existing build graph.Here's a thought experiment using those methods. That code seems less complicated, since you don't need to worry about resetting things every build, but it still has some complexity—namely understanding how the order/composition of
_inputNodes
affects the order of execution during the whole build.And at this point, I'm getting really handwavy, so I'll stop my ramblings.