-
Notifications
You must be signed in to change notification settings - Fork 2.1k
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
Modules #1177
Modules #1177
Conversation
Will update this pull-request with external module support using |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As discussed let's incorporate rollup
and plugins to achieve this functionality while preserving & standardizing on es6 module syntax, also to minimize the required changes to adapters, tests, etc.
I've added a commit (5d7ade4) showing an example of splitting modules only using webpack code-splitting. I didn't update the express module as we'll still need to figure out the best way to get config to modules, but the rubiconAdapter module has been updated. |
28ce904
to
16a9f4d
Compare
I've updated this pull-request with the To test you can clone the repo and checkout the You can also use
Besides searching for modules in the Starting tomorrow I will begin the process of converting the rest of the adapters into modules. Note: One final thing probably needed before being production ready is to create the webpack plugin to exclude the manifest and jsonp loading code from the webpack runtime. These bundles work fine as is implemented here, but excluding that code will reduce the bundle size. |
Another thing to note: in order to support |
@matthewlane @jaiminpanchal27 @dbemiller added all of you as reviewers, please have a look as this is working towards final and will need to be understood by all of us. |
I looked this over... but I must admit, I think I'm too late to the party to give much serious feedback. A build task which creates a bundle with only the adapters you need looks like a great contribution. The spec was also super clear and well-written. I'm getting hung up on the claim: "we want dynamic runtime inclusion without async loading." The main benefit of for dynamic runtime inclusion which I can think of is the fact that you can download only the parts of the javascript which you need. Since it sounds like you still expect publishers to have downloaded it all, then what benefit is there to loading things at runtime? On the other hand... I don't actually see anything in the diff which adds APIs to allow that. So am I missing something, or did that part of the spec get dropped? The rest is mostly details. Will leave some comments on individual files about them. |
@@ -0,0 +1,200 @@ | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you leave comments at the top of new files describing (at a high level) what their purpose is?
It's helpful for a few reasons... but those are even more pronounced now that we're making Modules.
- It helps newcomers get their bearings on the codebase
- It helps users decide whether or not they should include the module (at a glance) in their bundle
- It (theoretically) improves code quality over time, because some future engineer tasked with implementing "feature X" can tell at a glance which (if any) code makes sense in this module.
"gulp-header": "^1.7.1", | ||
"gulp-if": "^2.0.2", | ||
"gulp-jscs": "^3.0.2", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks accidental... are you using jscs anywhere? I thought we got rid of that with the move to eslint.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added the gulp-if
but am not sure what gulp-jscs
s deal is. If it was removed I might have accidentally re-added it after a merge conflict, I can remove it if its no longer needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
src/adapter.js
Outdated
@@ -0,0 +1,24 @@ | |||
function Adapter(code) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this replace src/adapters/adapter.js
? I don't see that file being deleted... but they're duplicates.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, this replaces src/adpaters/adapter.js
. I haven't removed the old file yet because I haven't finished converting all the adapters to modules yet, so the non-converted adapters still look for this file. As soon as I'm done converting the adapters to modules I'll make sure this is moved properly in git.
src/config.js
Outdated
configHandlers[property] = []; | ||
} | ||
configHandlers[property].push(handler); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens if getConfig
is called after the last call to setConfig
? If I were calling these APIs, I would still expect my callback to be executed.
Actually... a related problem: I would expect my callback to be executed exactly once. It doesn't affect your use-cases here... but looking at this file in isolation, I'd still consider it a bug.
Related: are unit tests coming soon?
modules/rubiconBidAdapter.js
Outdated
@@ -22,6 +24,10 @@ const VIDEO_ENDPOINT = '//fastlane-adv.rubiconproject.com/v1/auction/video'; | |||
|
|||
const TIMEOUT_BUFFER = 500; | |||
|
|||
var ACCOUNT_ID; | |||
getConfig('rubiconAdapter', config => ACCOUNT_ID = config.accountId); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this looks..... well, very dangerous. By setting a variable in an async callback, you've inherently created a race condition. This works (I would guess?) because a user will (should?) call setConfig
sometime between the time that this code runs, and when they kick off an auction... but if someone tried to use ACCOUNT_ID
in this file outside of those auction-related functions, you could have some really tricky bugs to track down.
Part of me wants to say "your adapter, your business :p" but.... given that you're prototyping an architecture here, I'm a bit concerned because I can't really see a way to do it safely.
It seems to me like it would have to look something like:
getConfig('myNamespace', config => {
var property = config.myParam;
module.exports = stuffWhichUses(property);
});
...which I'm pretty sure modules don't actually let you do.
Can you write an example of a very small module which reads a value from the config, and then exports something useful once the param has been read? Without assuming that callers follow that implicit import files, setConfig, callUsefulMethod
order.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know if I'd call it a race condition, but there is definitely a dependency problem. However, I didn't create it. The problem is inherent in the way we execute pbjs.que
blocks and when module code is ran by webpack. I detailed this in a diagram I pasted to slack awhile back to describe our problem:
We effectively have a circular dependency based on the current architecture of Prebid.js. This isn't a new problem, it's just now more obvious because this is the first time someone has attempted to apply global config (config across adUnits and auctions) to an adapter (or in this case now, a module). Currently configuration can only come in through bid.params
which makes this condition less obvious, but is also not an ideal solution for handling global config.
Using a callback was the solution I found to solve this. By registering a callback that sets the config it allows us to defer configuration until after setConfig
is called. I think the dependency is intuitive (that you must setConfig
for a module before you can use that config), so I'm not too worried about the implications.
I originally tried to solve the problem with promises, but then I ran into an actual race condition when the scheduled config callback wasn't called until after the auction had ran.
It's possible to not register the adapter to Prebid.js until the config has been received (the registration being the real export that Prebid cares about, module.exports
is only used for testing), but then you're just swapping a set of config error messages, for a more cryptic and general error message. i.e. "Rubcion Adapter missing required config Account ID" vs "Rubicon Adapter was not found".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, that makes sense. I also agree that there's an architectural problem with prebid right now, and your diagram distills it down well.
One obvious fix that jumps out at me is if we moved towards an API like requestBids(config)
instead, and then implement write requestBids
to forward the config through to any adapters. That would also move us one step closer to multiple auctions, since it's clear what the config for each particular auction is.
I know fixing all that is outside the scope of this PR... but I wonder if we could get things a little closer to that ideal, without too much effort?
For example, right now your modules look like:
getConfig();
adapter = makeAdapter(config);
adaptermanager.registerBidAdapter(adapter, ...);
Instead, we could do something like:
function adapterLoader(config) {
return makeAdapter(config);
}
adaptermanager.registerBidAdapterLoader(adapterLoader, ...);
Then load the config inside registerBidAdapterLoader
, and forward the call through to registerBidAdapter
in the config callback.
Main benefits being:
- The circular dependence stays within prebid-core, and out of the modules
- Adapters would be easier to unit test (can send through mock configs)
Another benefit (disjoint from the first... so take it as an independent suggestion) is that you could namespace the global config. For example, if the bidder code is rubicon
, then registerAdapterLoader
function could decide (universally) that rubiconAdapter
is where your config data goes. Then when it actually executes your load
function, it can send that data right back to you.
Benefits there being:
- Lower publisher learning curve (config naming conventions consistent across all adapters)
- Adapters can't trample each others' config values accidentally
@dbemiller I wrote a longer response, but for whatever reason github posted it twice and when I deleted the extra one github decided to delete both of them.... It might still be in your e-mail, but if not, here's the gist of it: The requirement of dynamic runtime inclusion was modified in a follow-up meeting concerning modules that happened after the spec was written. It was stressed that in an effort to keep es6 module syntax we should get rid of the runtime requirement. We still want quick dynamic inclusion though. And by that I mean, concatenating built files together to include required functionality (a process which is fast and could be part of a server-side request), or including them individually like: <script src="./prebid.js"></script>
<script src="./rubiconBidAdapter.js"></script>
<script src="./appnexusBidAdapter.js"></script>
<script>
pbjs.processQueue();
pbjs.que.push(function() {
... stuff here
});
</script> |
@snapwich The latter (individual scripts) approach would likely see benefits in maintenance and performance in an HTTP2 environment if managed properly. In terms of caching and config and in terms of delivering from a central CDN while also allowing modularity with a known, off the shelf for everyone, mechanism: HTML. |
@GLStephen I think it's a feasible approach. However, I don't think it'll be used often due to the rampant use of tag managers within the ad space and the hard requirement that the For that reason I think the server-side concatenation of the bundle will be more common. I think that is still a very useful addition to Prebid.js though. It'll allow us to do things like: <script src="./prebid.js?modules=rubiconBidAdapter,appnexusBidAdapter"></script> and get exact bundles of only the code we want on request. |
I agree that server-side or build-time concatenation generally makes much more sense than multiple script tags on the page. If you do want to leave that pattern open as a possibility, then the relevant parts of each module in this code (e.g. the But that's pretty convoluted and icky if loading individual modules is a non-goal. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM!
Please note that this document http://prebid.org/dev-docs/bidder-adaptor.html now contains many broken link. |
Thanks @navneetpandey - covered in prebid/prebid.github.io#275. May have missed some -- review welcome. |
Seem consistent now @bretg . Thanks. |
Hi, as of this day, the |
@jeremiegirault what do you mean, it has no effect? I just pulled the latest and built prebid and it still seems to be successfully stubbing the jsonp loader code. |
Hi, In my tests I had troubles and the plugin was not active indeed. But after using the good tag/master looking at the generated code it indeed seem to replace the
However Looking at the source without the plugin :
So I question the usefulness of this webpack plugin which seem outdated in the context ? |
@jeremiegirault yeah that code is probably useless. It was an attempt to fail nicely if JSONP loading was attempted, but I don't think it's ever running. I added a pull-request to just remove that code block entirely. |
Type of change
Description of change
The module system as described in #1086
For an example of converting an existing adapter to a module look at the adaption in file
src/adapters/rubicon.js
tomodules/rubiconAdapter.js
and tests fromtest/spec/adapters/rubicon_spec.js
totest/spec/modules/rubiconAdapter_spec.js
.New
express
module was added as well. With the newexpress
module and the conversion of the rubcion adapter to a module the build currently spits out 3 files:prebid.js
,express.js
, andrubiconAdapter.js
. These can then being bundled for usage aftergulp
is run usinggulp bundle --modules=express,rubiconAdapter
. The--modules
flag is optional if you just want to bundle all modules found.