-
Notifications
You must be signed in to change notification settings - Fork 97
feat: add mechanism to load internal module files to patch #52
feat: add mechanism to load internal module files to patch #52
Conversation
fc173ef
to
f81e135
Compare
756415f
to
9c3ceed
Compare
this.setPluginContext(moduleExports, tracer, version); | ||
applyPatch( | ||
moduleExports: any, tracer: modelTypes.Tracer, version: string, | ||
basedir?: string): any { |
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.
basedir
is optional here but is passed to functions that require it to have a value later. I would suggest making it required here as well.
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.
It will not be used by all implementations, just the ones that need to patch internal files that are not exported. The parameter as optional does not affect current implementations.
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.
TS optionality doesn't refer to whether it is required to be used, but whether it is required to be given a value. In the latter case it is not optional because some plugins just won't work if you don't pass it.
I understand that you'd have to update the plugins to add a basedir
argument -- if you agree that it should be required then it should be done in this PR.
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.
Method loadPlugins from PluginLoader is always passing this parameter value.
result = plugin.applyPatch(exports, this.tracer, version, basedir);
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.
The implementations that will require this parameter will have it available.
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 think you might be confounding optional assignment vs. optional usage:
optional assignment
// it is OK for us not to pass a value for `basedir`.
plugin.applyPatch(exports, this.tracer, version);
optional usage
class MyPlugin extends BasePlugin {
applyPatch(exports, tracer, version, basedir) {
// it is OK for us not to use `basedir`.
}
}
The syntax basedir?
suggests that basedir
's assignment is optional. But as we have both said, the real intent that we want to convey is that its usage is optional. (There is no syntax for this; the only way to show this intent is to document it.)
/** | ||
* Maps a name (key) representing a internal file module and its exports | ||
*/ | ||
export type ModuleExports = { |
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.
The type name here should be more descriptive. Something like ModuleExportsMapping
or something like that. But I wonder if you really need this type to be defined explicitly, because it's so broad. (Why not just define it locally in base-plugin.ts
.)
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.
Agree. Done !
if (this.internalFileList) { | ||
Object.keys(this.internalFileList).map(versionRange => { | ||
if (semver.satisfies(this.version, versionRange)) { | ||
result = this.loadInternalModuleFiles( |
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.
Should you warn if multiple versions match? It sounds like users would be depending on implementation-private behavior.
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.
If multiple versions match, the last one will prevail.
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.
Can you add a logger warning? It should be brought to the user's attention because it is mostly a bug for someone to user overlapping version ranges.
let result: types.ModuleExports = null; | ||
this.logger.debug('loadInternalFiles %o', this.internalFileList); | ||
if (this.internalFileList) { | ||
Object.keys(this.internalFileList).map(versionRange => { |
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.
Prefer forEach
over map
if you are not using the return value.
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.
Done!
/** a logger */ | ||
protected logger: Logger; | ||
/** list of internal files that need patch and are not exported by default */ | ||
protected internalFileList: types.PluginInternalFiles; |
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 is readonly
, right?
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.
Done!
basedir: string): types.ModuleExports { | ||
const extraModules: types.ModuleExports = {}; | ||
if (extraModulesList) { | ||
Object.keys(extraModulesList).map(modulename => { |
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.
Use a camelcase variable name (modulename
).
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.
Done!
try { | ||
this.logger.debug('loading File %s', extraModulesList[modulename]); | ||
extraModules[modulename] = | ||
require(path.join(basedir, extraModulesList[modulename])); |
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 am not certain that this will work in all cases. What does basedir
usually look like -- is it absolute or relative?
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.
The basedir parameter is passed to the callback of the hook function from lib "require-in-the-middle", it is an absolute path.
this.moduleExports = moduleExports; | ||
this.tracer = tracer; | ||
this.version = version; | ||
this.basedir = basedir; | ||
this.logger = tracer.logger || logger.logger(); |
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.
Why not just tracer.logger
?
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.
Agree.
@@ -20,18 +20,36 @@ import * as shimmer from 'shimmer'; | |||
|
|||
|
|||
export class SimpleModulePlugin extends classes.BasePlugin { | |||
protected internalFileList: types.PluginInternalFiles = { |
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 module doesn't seem very simple anymore. Can you have a separate instrumentation fixture for something that requires an internal module to be patched.
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.
A new one was created.
const extraFile = 'src/extra-module'; | ||
const extraModule = require(path.join(indexPath, extraFile)); | ||
assert.strictEqual(extraModule.name(), 'patched-' + extraModuleName); | ||
assert.strictEqual(extraModule.value(), 121); |
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.
extraModule
is already not user-visible. I would rather see a test based around a module that uses the module exports of an internal file.
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 think if the internal file export is accessible, whats the point to load it this way?
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.
It does not simulate a real user-visible behavior. Based on this test alone, it is not clear that if the internal module's exports are patched, it will be visible to the user through behavior changes in simple-module
.
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.
Test improved.
Overall, this is much better than before. I think it's good to not expose details about module internals to users. |
2c06ae1
to
7d20f2c
Compare
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 think this will be good to land after these comments. I still think moduleExports
does not belong as a field on Plugin
... let's address that in a separate PR.
* applyPatch() and applyUnpatch() | ||
* | ||
* GoF Template Method Pattern - this is the variant part of the pattern | ||
* Each plugin should implement his own version |
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 wasn't aware that this pattern had a specific name, TIL. However, I don't think this necessary belongs in docs, it may be confusing to users. Not everyone has read GoF.
I would just mention that this method will be called when applyPluginPatch
is called.
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.
Design Patterns are meant to be a kind of universal language, I think it is not confusing at all, it adds more information and makes things clear. I think our audience is pretty much aware of them.
if (this.internalFileList) { | ||
Object.keys(this.internalFileList).map(versionRange => { | ||
if (semver.satisfies(this.version, versionRange)) { | ||
result = this.loadInternalModuleFiles( |
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.
Can you add a logger warning? It should be brought to the user's attention because it is mostly a bug for someone to user overlapping version ranges.
*/ | ||
// tslint:disable:no-any | ||
applyPatch( | ||
applyPluginPatch( |
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.
Suggestion: enable
and disable
to distinguish this from applyPatch
and applyUnpatch
.
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.
For the public interface [enable/disable]PluginPatch ? or the protected methods [enable/disable]Patch ?
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.
Re: naming, I meant just call the functions enable
and disable
.
if (this.internalFileList) { | ||
this.logger.debug('loadInternalFiles %o', this.internalFileList); | ||
Object.keys(this.internalFileList).forEach(versionRange => { | ||
if (semver.satisfies(this.version, versionRange)) { | ||
if (result) { |
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.
Not to be pedantic, but this can yield strange results if result
just happens to be a falsey value (0
or ''
). For correctness' sake I would just make overlap
a counter (or otherwise just print the warning right in this function).
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.
Refactory done.
* applyPatch() and applyUnpatch() | ||
* | ||
* GoF Template Method Pattern - this is the variant part of the pattern | ||
* Each plugin should implement his own version. |
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 would not consider this universal language. I did not know what this was referring to and had to look it up. It might be true that I am not the common case, but it is better not to assume something like this about the audience. After all, it is possible to understand the concept without knowing its name.
Feel free to document the design pattern at the top of the file, but I do not think it belongs here.
Another note, we should not use gender pronouns in code, especially when referring to impersonal objects. Please use its
.
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.
... document the design pattern at the top of the file ...
At the top, it loses context. I have improved the message.
48e95b5
to
c082458
Compare
c082458
to
5d442ac
Compare
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. I will file a separate issue about the concern related to loading multiple modules with different versions.
…strumentation#52) * feat: add mechanism to load internal module files to patch * refactor: change Plugin Interface and BasePlugin to implement GoF Template Method * refactor: change http plugin to new interface * refactor: change https plugin to new interface * refactor: change http2 plugin to new interface * refactor: change mongodb plugin to new interface * refactor: improve load internal file test case * refactor: rename methods of Plugin interface
…strumentation#52) * feat: add mechanism to load internal module files to patch * refactor: change Plugin Interface and BasePlugin to implement GoF Template Method * refactor: change http plugin to new interface * refactor: change https plugin to new interface * refactor: change http2 plugin to new interface * refactor: change mongodb plugin to new interface * refactor: improve load internal file test case * refactor: rename methods of Plugin interface
…strumentation#52) * feat: add mechanism to load internal module files to patch * refactor: change Plugin Interface and BasePlugin to implement GoF Template Method * refactor: change http plugin to new interface * refactor: change https plugin to new interface * refactor: change http2 plugin to new interface * refactor: change mongodb plugin to new interface * refactor: improve load internal file test case * refactor: rename methods of Plugin interface
Sometimes, there is a need to patch internal module files that are not exported by default. This PR aims to add a mechanism to make it possible to load internal files from a module and make it available to a plugin.