-
-
Notifications
You must be signed in to change notification settings - Fork 35
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
fix(webpack): Aliased module paths now properly map to the correct aurelia-loader module id [SIMPLE] #139
base: master
Are you sure you want to change the base?
Conversation
Thanks for creating this PR! I feel bad about what I'm going to say but the timing is unfortunate... Could you please rebase your changes on top of |
@jods4 I went ahead and rebased but I have yet to test it as I have not yet upgraded my own build to webpack 4. If you have a working webpack 4 build, would you be able to test on your build for a reference against my webpack 3 build while I work on getting my build on webpack 4. |
@pat841 Hi Pat. This still shows open. Where are we at with this? Thanks. |
@pat841 have you had time to test your rebase? |
@rek72 and @Alexander-Taran I have verified that this now works on both Windows, MacOS, and Linux using Webpack 4. |
Are you testing with the fixed version or what's been committed? |
So I wanted to review this and possibly merge but it seems that it wasn't rebased properly. Can this be rebased on |
15eb138
to
568dc37
Compare
@jods4 I went ahead and re-based on |
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 a huge PR to the most complex part of this plugin so it'll take some time for me to review entirely.
First pass is mostly coding style...
I'm trying to get a good grasp of the modifications. Can you provide a quick summary of what behavior you intended to change with this PR? What specific use-case works differently than before?
src/PreserveModuleNamePlugin.ts
Outdated
return !!value; | ||
|
||
// Preserve the module if its dependencies are also preserved | ||
const reasons = (m.reasons && Array.isArray(m.reasons)) ? m.reasons : []; |
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.
Is there a reason for this defensive code?
It was not in my code and I've never had an issue filed about m.reasons
not being an array.
Is there a case where it happens?
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 unsure, I usually add safety checks and assume to worst. If you need me to I can remove it.
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 problem I have with defensive code is that it creates headache for future maintainers.
When I see this code it tells me "m.reasons
could be something else than an array, we better be careful" and I immediately ask myself: "how so? what am I missing?".
If this m.reasons
can be null
we should document how / why, it's useful information.
If it cannot, then it's misleading bloat that I'd rather remove.
When unsure, someone needs to figure this out and it's courteous that you do it rather than deferring the analysis to future maintainers.
src/PreserveModuleNamePlugin.ts
Outdated
|
||
// Preserve the module if its dependencies are also preserved | ||
const reasons = (m.reasons && Array.isArray(m.reasons)) ? m.reasons : []; | ||
return reasons.some((reason) => Boolean(reason.dependency && reason.dependency[preserveModuleName])); |
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.
Nit: no need for parenthesis when there's a single argument. No need to convert to bool explicitely as it's done by some
.
src/PreserveModuleNamePlugin.ts
Outdated
@@ -57,11 +147,13 @@ export class PreserveModuleNamePlugin { | |||
if (/^async[?!]/.test(realModule.rawRequest)) | |||
id = "async!" + id; | |||
|
|||
id = id.replace(/\\/g, "/"); | |||
id = id.replace(/\\/g, '/'); |
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.
All strings use double-quotes, why did you change this one to single-quote?
Also: consistency is good, make sure your new code uses double-quotes (I spotted some single-quotes).
src/PreserveModuleNamePlugin.ts
Outdated
* @return {string|null} The module id if available, null otherwise | ||
*/ | ||
function getModuleId(module: Webpack.Module, paths: string[], aliases: { [key: string]: string } | null): string | null { | ||
if (!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.
Why? This is an internal API and you've typed module: Webpack.Module
.
We compile with strict null checks enabled, so this is not possible.
aliasRelative(alias, realModule.resource); | ||
|
||
// Get the module id | ||
let id = getModuleId(realModule, roots, alias); |
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 we factor out getModuleId
, we might as well take the if (!id) throw
below with it.
That changes the return type to a non-null 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.
What do you mean by this?
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 mean on next line (140) you throw if the id is null.
You can make getModuleId
throw if it can't find an id
; and have a non-nullable return.
Non-nulls are better coding style and it makes getModuleId
easier to re-use by not having to check its result. Mainline code is simpler as well.
// Get the module id, fallback to using the module request | ||
let moduleId: string = dep.request; | ||
if (dep.module && typeof dep.module[preserveModuleName] === 'string') { | ||
moduleId = dep.module[preserveModuleName]; |
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 sure what the impact of this change is.
Can you explain briefly, maybe with an example that behaves differently before/after this change?
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.
AureliaDependenciesPlugin
uses the preserveModuleName
symbol to set the generated moduleId. We then check that it is set here and use that for the webpack moduleId, otherwise fallback to the default usage.
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.
OK, I see.
I had this idea of rewriting user code with Webpack generated id but it breaks quite a few things (such as conventions).
You take a middle ground where you rewrite user's code with our normalized id.
That seems safe and would solve a few issue, I think, as aurelia-loader
will do the same normalization anyway.., and in the rare cases where it doesn't (there are a few) it would break at runtime anyway.
Looks like a good change to me 👍
Out of curiosity: does this fix a specific problem you have?
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 it does see #121
src/AureliaDependenciesPlugin.ts
Outdated
@@ -41,7 +48,7 @@ class ParserPlugin { | |||
hooks.evaluateIdentifier.tap("imported var.moduleName", TAP_NAME, (expr: Webpack.MemberExpression) => { | |||
if (expr.property.name === "moduleName" && | |||
expr.object.name === "PLATFORM" && | |||
expr.object.type === "Identifier") { | |||
String(expr.object.type) === "Identifier") { |
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? What can expr.object.type
be that is not a string but would convert to "Identifier"
?
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.
OK, I see now that newer TS shows an error here.
A cast is a hack, the real problem lies in my poor type definition of expr.object
.
Would be nice to fix it there instead.
Same for the later occurances of String(expr.object.type)
src/AureliaDependenciesPlugin.ts
Outdated
@@ -55,8 +62,8 @@ class ParserPlugin { | |||
// PLATFORM.moduleName("id"); | |||
hooks.evaluate.tap("MemberExpression", TAP_NAME, expr => { | |||
if (expr.property.name === "moduleName" && | |||
(expr.object.type === "MemberExpression" && expr.object.property.name === "PLATFORM" || | |||
expr.object.type === "Identifier" && expr.object.name === "PLATFORM")) { | |||
(String(expr.object.type) === "MemberExpression" && expr.object.property.name === "PLATFORM" || |
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.
Same question here and on next line: what can expr.object.type
be that is not a string and would convert to "MemberExpression"
?
src/PreserveModuleNamePlugin.ts
Outdated
* @return {string|null} The escaped string | ||
*/ | ||
function escapeString(str: string): string | null { | ||
if (typeof str !== 'string') { |
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.
internal API with type string
...
src/PreserveModuleNamePlugin.ts
Outdated
* | ||
* @return {string|null} The module id if available, null otherwise | ||
*/ | ||
function getModuleId(module: Webpack.Module, paths: string[], aliases: { [key: string]: string } | null): string | null { |
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.
When extracting this function, we lost one bit of functionality.
If module[preserveModuleNode]
was defined it would be used without looking at anything else.
This can be set by some plugins when creating the dependency, if I recall correctly.
Is this intentionnal?
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 was unaware that other modules could set it. The AureliaDependenciesPlugin
uses it to explicitly set the generated moduleId.
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.
See for example (with some comments on why) ConventionDependenciesPlugin
I went ahead and updated the PR, apologies on the delay. I commented with any questions I had and updated some things to conform to your style guide. |
@jods4 Have you had a chance to review this yet? |
Any update on this @jods4 @EisenbergEffect |
@jods4 Can you look into this? |
src/webpack.d.ts
Outdated
@@ -58,7 +58,7 @@ declare namespace Webpack { | |||
// Those types are not correct, but that's enough to compile this project | |||
property: IdentifierExpression; | |||
object: { name: string; type: string; } & MemberExpression; | |||
type: "MemberExpression"; | |||
type: "MemberExpression" | "Identifier"; |
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 seems very wrong MemberExpression
is defined by type: "MemberExpression"
, it can't be Identifier
.
The problem is rather on previous line where object: { ... } & MemberExpression;
I noticed that newer TS version create error because of this, we need to fix it.
I'm not sure why I wrote & MemberExpression
in the first place here, because it's obviously not 100% correct.
src/PreserveModuleNamePlugin.ts
Outdated
export const preserveModuleName = Symbol(); | ||
|
||
// node_module maps | ||
const _nodeModuleResourcesMap: NodeModule.ResourcesMap = {}; | ||
const _nodeModuleResourceIdMap: NodeModule.ResourceIdMap = {}; |
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.
nit (style): other private members are not _
prefixed in this project.
src/PreserveModuleNamePlugin.ts
Outdated
@@ -30,6 +123,10 @@ export class PreserveModuleNamePlugin { | |||
modulesBeforeConcat.splice(i--, 1, ...m["modules"]); | |||
} | |||
|
|||
// Map and parse the modules if needed | |||
modulesBeforeConcat.forEach((m) => mapNodeModule(m)); |
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.
nit: modulesBeforeConcat.forEach(mapNodeModule)
, no need for lambda.
return true; | ||
} | ||
if (m[preserveModuleName]) { | ||
return true; |
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 seems to me that you have significantly changed the behavior of this.
We did not just check if some dependency had preserveModuleName
set.
If the dependency used an absolute module id, we would use it as a module name, as this is more robust than trying to figure out the normalized id ourselves. See line 82 before PR.
Unless you have a good reason to remove that behavior, I would like to revert this change.
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 confused by this. Why would we set a modules id based on one of its dependencies? ModuleA could require ModuleB but we dont want to set ModuleA's id based on ModuleB which might or might not live in the same module directory.
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.
Well I said it here:
If the dependency used an absolute module id, we would use it as a module name, as this is more robust than trying to figure out the normalized id ourselves.
Not sure how to say it better.
Normalizing the module names for aurelia-loader is a can of worms. It works rather well but there are lots of edge cases, this PR is proof of that.
So when we are handed out an absolute module id, that's great because it means we don't have to make guesses at normalization and it's guaranteed to work.
It's also marginally faster.
I'm sorry I did not come back to this earlier. It needs a lot of focus and I've been working a little relentlessly those past months. Thanks for the changes you've done so far, it's already easier to look at for me (less modifications). The general changes I am fine with, modulo:
I like the change where you modify user code to inject the normalized module id instead of original one. I think that's a good change 👍 Then there's the It would really help me a lot if you could summarize in a few word what use case you're trying to improve here. Can you provide an example of something that didn't work before and works with the new code? I'm sorry it takes so long and is so hard to merge but that's a big change to the most tricky parts of this plugin. |
1e634ff
to
103d9b7
Compare
@jods4 The issue was aliases were never handled properly when mixed with relative/absolute paths. This new approach creates a mapping of all normalized paths and replaces the alias/relative/absolute path in code with the normalized path. Maybe we could tag this as beta/possible breaking and pre-release until were certain its stable? I have been using this PR in multiple projects of mine without issue . |
@pat841 We appreciate the awesome work you've put into this PR. One of the things that is holding this up is it's raw size. We usually like to work in smaller increments when we can. I wonder if there's a way that we could break this up into a set of successive, smaller PR, each addressing one issue. That would make the process go faster, it would be easier to review, test, etc. Any thoughts on this? @bigopon and @fkleuver have done a good job of this on a few of our other libraries, so maybe they have some specific recommendations in this case. |
I must admit that I often fail at breaking things up into smaller chunks as well. However, what I do to mitigate risks is add lots and lots of tests. Tests are usually easier to review than code. You can look at simple inputs and outputs and verify whether they are appropriate, instead of having to reverse-engineer logic and guessing whether it might work or not. So my recommendation would be to add tests. Whatever makes sense. A few unit tests could be good where possible (e.g. there's no need for a full project structure) and for the large areas it could be a full "e2e" tests on some real physical project structures where you have an input project and compare the processed output to a pre-generated one. Just to give an example. That way you can cover a lot of ground with relatively little work |
…relia-loader module id [SIMPLE]
Reference: #122
@jods4 @EisenbergEffect
Information: