Create static class properties in a class IIFE#6963
Create static class properties in a class IIFE#6963Andarist wants to merge 1 commit intobabel:masterfrom
Conversation
| inherits: syntaxClassProperties, | ||
|
|
||
| visitor: { | ||
| ExportDefaultDeclaration(path) { |
There was a problem hiding this comment.
this is literally copied from here any suggestions what's the best way to dedupe this?
There was a problem hiding this comment.
You can create a path.splitExportDeclaration() function (or a @babel/helper-...)
There was a problem hiding this comment.
was thinking about both, weren't sure which one to pick :D I'd probably go with latter, thanks
There was a problem hiding this comment.
babel/packages/babel-traverse/src/scope/lib/renamer.js
Lines 42 to 85 in a2aabbd
| return iife; | ||
| } | ||
|
|
||
| if (this.path.isClassExpression()) { |
There was a problem hiding this comment.
hating this IfStatement here, kinda a hack to insert a correct return for mentioned double IIFEs case - need to figure out how to flatten those IIFEs (in case of ClassExpresion with static properties) and keep all surrounding references correct
|
Build successful! You can test your changes in the REPL here: https://babeljs.io/repl/build/6877/ |
| var _class; | ||
|
|
||
| return _temp = _class = | ||
| return _class = |
There was a problem hiding this comment.
Is it possible to remove this unused _class = assignment?
There was a problem hiding this comment.
it probably is, added _class is caused by ClassExpression being replaced by multiple statements. The path#replaceWithMultiple creates an IIFE when the conversion is made so the single expression can be maintained in place.
This is certainly something I'd like to revisit, but for another month (till Jan 16th) I have very little time and I couldn't manage to do this properly at the moment because of the complexity and probably my insufficient experience with AST manipulation.
Ideally I would like to "inline" the ClassExpression here if possible to skip second IIFE creation, but I'm not sure how safe it is because scope references have to be synced and stuff.
There was a problem hiding this comment.
This case (and other similar) are caused by simple insertion here. To remove this unused variable the logic here would have to be expanded. I might revisit this in a followup PR, but would like to keep this one as simple as possible.
| @@ -6,7 +6,7 @@ var four = 4; | |||
|
|
|||
| var MyClass = | |||
| /*#__PURE__*/ | |||
There was a problem hiding this comment.
Isn't this iife pure only if computed properties are pure?
There was a problem hiding this comment.
Yes, you are right. They can also be impure in other cases, such as impure property initializers, or even super class being an impure expression.
I've wanted to keep marking those class IIFEs as pure, because it is what transform-classes do at the moment in all cases (and classes without properties can be impure too). That's quite my fault, adding those annotations was one of my 1st PRs to babel.
I'd like to revisit (as another PR) annotating classes as pure as a whole and not just in case of proposal-class-properties plugin. While checking if class is pure should be quite easy, I'm not sure what we should do about subclasses - we won't be able to tell if a super class is pure or not.
| }); | ||
|
|
||
| export default class MyClass3 { | ||
| class MyClass3 { |
There was a problem hiding this comment.
Can we split the class and the export statement only if it is actually needed? (Only if it doesn't add too much complexity to the plugin)
There was a problem hiding this comment.
It was easier to split those for now in all cases, need to check how much complexity it would add to keep them together (and the same should be done in transform-classes as it is splitting those every time)
|
@nicolo-ribaudo thanks for the initial review! |
75fb3a4 to
2925450
Compare
| declaration.isFunctionDeclaration() || declaration.isClassDeclaration(); | ||
|
|
||
| let id = declaration.node.id; | ||
| const liveBinding = !!id; |
There was a problem hiding this comment.
export default function() {}is transformed into
function _default() {}
export default _default;The first export statement is hoisted, but is it also the second?
There was a problem hiding this comment.
could u elaborate? I've thought about hoisting briefly in this context, but came to conclusion that no extra work needs to be done for export statements, they are evaluated in place, right? and as function declaration will get hoisted by javascript this identifier will be available by the time it gets exported
There was a problem hiding this comment.
After playing a bit with it, I understood that the way you implemented it is correct 👍
a314c95 to
7fec1b7
Compare
|
@nicolo-ribaudo |
nicolo-ribaudo
left a comment
There was a problem hiding this comment.
Are the changes to the module transformer required by the split-export-declaration helper? If not, I'd prefer them to be moved to another PR, to keep the changes simpler.
PS. This review is not complete yet (I don't have time to look at the tests now).
|
|
||
| const classIIFEs = new WeakMap(); | ||
|
|
||
| export function getClassIIFE(path, assumePure) { |
There was a problem hiding this comment.
Nit: can you use a default export? It seems that most helpers do that.
| localData.delete(localName); | ||
|
|
||
| reexport.names.forEach(name => { | ||
| reexport.bindings.map(({ name }) => name).forEach(name => { |
There was a problem hiding this comment.
Can you use reexport.bindings.forEach({ name } => { and avoid an unnecessary loop? Also on lines 186 and 200
| for (const data of metadata.local.values()) { | ||
| for (const name of data.names) { | ||
| const names = data.bindings.map(({ name }) => name); | ||
| for (const name of names) { |
There was a problem hiding this comment.
Here you can avoid looping twice:
for (const { name } of data.bindings) {| const declaration = child.get("declaration"); | ||
| if ( | ||
|
|
||
| if (declaration.isIdentifier()) { |
There was a problem hiding this comment.
Aren't identifiers normal expressions? e.g., what is the difference between
var a = 2;
export default a;and
export default 2;?
There was a problem hiding this comment.
There is no difference when it comes to the functionality.
The containing function here (getLocalExportMetadata) gathers export names for local bindings so every future assignment to those local bindings can be transformed into a live reference.
You can note that this function here expects only a FunctionDeclaration, ClassDeclarations and now Identifiers in position of default export. Everything else in this position gets splitted before by nameAnonymousExports into
var *local* = expression
export { *local* as default }
I omit this splitting for local bindings here to produce a nicer output, if I'd keep splitting them we would end up with things like:
var _default = function () { /* some class */ }
var _default2 = _default
module.exports = _default2| export default function(api, options) { | ||
| const { loose } = options; | ||
| const { loose, assumePure: _assumePure } = options; | ||
| const assumePure = typeof _assumePure === "boolean" ? _assumePure : !!loose; |
There was a problem hiding this comment.
I'd prefer this to throw if assumePure is specified and it is neither a boolean nor null/undefined.
There was a problem hiding this comment.
Gonna go with throwing on nulls too, it doesn't give any extra meaning for the option, and one can just skip providing it (thus making it undefined). If you feel we should allow null here and treat is as undefined, please raise your concern.
| path.parentPath.insertAfter(nodes); | ||
| } else { | ||
| path.insertAfter(nodes); | ||
| } |
There was a problem hiding this comment.
You can just use path.insertAfter(nodes), thanks to #7040
| if (isClassExpression || !path.node.id) { | ||
| nameFunction(path); | ||
| ref = path.scope.generateUidIdentifier("class"); | ||
| ref = path.node.id || path.scope.generateUidIdentifier("class"); |
There was a problem hiding this comment.
You should always generate a new identifier, otherwise it won't work in cases like https://babeljs.io/repl/build/6407/#?babili=false&browsers=&build=&builtIns=false&code_lz=DYUwLgBAghC8ECYDcAoFAzAdgCgMbAEMBnI6CAbxQgiLALAEtcIAHAJwHsW5FUBfAJRIgA&debug=false&circleciRepo=&evaluate=false&lineWrap=false&presets=stage-0&prettier=false&targets=&version=7.0.0-beta.36%2Bpr.6963 (A gets declared twice).
You can do something like
ref = path.scope.generateUidIdentifier(path.node.id ? path.node.id.name : "class");There was a problem hiding this comment.
Good catch. Need to dig into this after work, as I have a feeling that it will break inlining ClassExpressions into a class IIFE here, so I will have to check this and probably fix.
There was a problem hiding this comment.
Made a quick test and as suspected - it breaks some cases. Maybe you would have a possible solution/approach in mind to fix this.
The problem is that this:
() => class Bar {
method(){}
static fn = () => 'static';
};gets transformed into with class-properties transform
() => {
var _Bar;
return _Bar =
/*#__PURE__*/
(() => {
let _Bar = class Bar {
method() {}
};
_Bar.fn = () => 'static';
return _Bar;
})();
}which gets later transformed with transform-classes into:
() => {
var _Bar;
return _Bar =
/*#__PURE__*/
(() => {
function Bar() {}
var _proto = Bar.prototype;
_proto.method = function method() {};
_Bar.fn = () => 'static';
return _Bar;
})();
};The problem lies in the fact that the mentioned inlining doesn't know that the local class ID is other than that class ID itself.
What comes to my mind is to:
- retrieve this ID - we know for sure in this place that the
parentStatement(the one that is going to be replaced) is aVariableDeclarationcontaining the correct ID - rename references to the old ID (constructor's ID) with the retrieved ID (Im not sure how to do this right now, as I'd like to update identifiers in array of nodes and not a path)
- actually do not remove
parentStatement, but just replace it'sinitwith a constructor (from the nodes that we want to insert here), to keep a local ID and a class proper name
There was a problem hiding this comment.
Probably the right fix is avoiding adding var [name] for class expressions.
Input:
(class A {
static foo = 2;
})Expected output
/*#__PURE__*/
(() => {
let A = class A {};
Object.defineProperty(A, "foo", {
configurable: true,
enumerable: true,
writable: true,
value: 2
});
return A;
})();
// Which then becomes
/*#__PURE__*/
(function () {
var A = function A() {
_classCallCheck(this, A);
};
Object.defineProperty(A, "foo", {
configurable: true,
enumerable: true,
writable: true,
value: 2
});
return A;
})();Actual output
var A;
A =
/*#__PURE__*/
(() => {
let A = class A {};
Object.defineProperty(A, "foo", {
configurable: true,
enumerable: true,
writable: true,
value: 2
});
return A;
})();
// Which then becomes
var A;
A =
/*#__PURE__*/
function () {
var A = function A() {
_classCallCheck(this, A);
};
Object.defineProperty(A, "foo", {
configurable: true,
enumerable: true,
writable: true,
value: 2
});
return A;
}();
Somewhat, with the changes it produces nicer output without extra temporary variables.
Sure, at first I thought this module might contain more than a single export, but it didnt come to that. Gonna change in a minute. |
cb4efac to
c17aa0c
Compare
|
@nicolo-ribaudo Fixed discussed issue by omitting inner var for ClassExpressions and converting them into ClassDeclarations (can be certain about this conversion because we are always inside a class IIFE by this time) + Im generating outer uid for outer ref (as suggested). Probably gonna add some test or 2 to be sure that I won't shadow existing bindings with freshly inserted one, this is somewhat tested already, but want to add a more focused test for this + maybe an exec test. Anyway - I think this is ready for another round of code review. |
| if (isClassExpression) { | ||
| const outerRef = path.scope.generateUidIdentifier(ref.name); | ||
| path.scope.push({ id: outerRef }); | ||
| path.replaceWith(t.assignmentExpression("=", outerRef, path.node)); |
There was a problem hiding this comment.
Q: What is outer ref needed for? It doesn't conflict now so it works, but I still don't understand what it is for.
There was a problem hiding this comment.
Im pretty confident it's not needed in 100% cases, it was here before the PR - didnt want to optimize the output more than needed. Gonna revisit this in followup PR.
Is this the only part that you do not understand? Or is something else needing a clarification too?
There was a problem hiding this comment.
I think it was needed because static properties where defined after the iife: https://babeljs.io/repl/build/master#?babili=false&browsers=&build=&builtIns=false&code_lz=BQYwNghgzlAECCsDeAoWsoBcKYJYlgDMB7Y2AXlgCYBuNWAWwFNMALYgE2AEpkBfFH240gA&debug=false&circleciRepo=&evaluate=false&fileSize=false&lineWrap=false&presets=es2015%2Cstage-0%2Ctypescript&prettier=false&targets=&version=7.0.0-beta.36 (_class is used in Object.defineProperty)
Is this the only part that you do not understand? Or is something else needing a clarification too?
Thank you, the other parts are clear enough 😊
2507647 to
30b9811
Compare
|
@nicolo-ribaudo I think you were right, removed creating |
|
@loganfsmyth @existentialism @xtuc would love any of yours feedback on this before merging :) |
30b9811 to
8eba015
Compare
8922dd0 to
2b959c0
Compare
|
@Andarist Can you confirm with this PR to have unreferenced classes with static class properties with side effects dropped the following must be specified? "plugins": [
["transform-classes", { "assumePure": true }],
]By default such otherwise unreferenced classes with static properties with side effects will be retained, correct? |
|
@kzc it's a little bit more complicated that this. I try to detect if the class is pure and if I it is I annotate it no matter what the value of When left unspecified it uses an "auto" value - This means that by default when using WDYT about used approach? |
|
I personally think all down-leveled classes should be pure by default regardless of Rationale for pure classes by default: |
|
This linked rationale lines up perfectly with my reasoning (I've also extended the same reasoning to things other than just classes in babel-plugin-annotate-pure-calls). I just ain't sure what is others opinion and I've went with the safer option by default for now. Happy to change it, I'd like to reach an agreement on this though with the rest of the team. |
|
@Andarist If not pure annotated by default, what would a |
|
IIUC, it would look like this: {
"plugins": [
["@babel/plugin-proposal-class-properties", { "assumePure": true }]
]
} |
|
Yeah, exactly - although I feel that we need to introduce option passing through presets to underlaying plugins. |
2b959c0 to
5dd49ed
Compare
| ? t.exportNamedDeclaration(null, [ | ||
| t.exportSpecifier(t.cloneNode(id), t.identifier("default")), | ||
| ]) | ||
| : t.exportDefaultDeclaration(t.cloneNode(id)); |
There was a problem hiding this comment.
Even if it doesn't have an ID, cyclic dependencies can still rely on it being hoisted. It's never safe to do export default foo; in this case.
There was a problem hiding this comment.
could you back it up with a sample code? not sure if I understand how the problem can be created
| import { types as t } from "@babel/core"; | ||
| import annotateAsPure from "@babel/helper-annotate-as-pure"; | ||
|
|
||
| const classIIFEs = new WeakMap(); |
There was a problem hiding this comment.
I generally haven't been super comfortable with things that rely on being singletons like this because it relies on them being the same version and npm flattening things. Usually we've relied on flattening for better performance, but making flattening affect the actual transform output worries me a lot.
There was a problem hiding this comment.
good point, but without merging both class transforms (which I think @hzoo proposed numerous times) i have no idea how to achieve the desired goal of "pulling" stuff into a single IIFE here
There was a problem hiding this comment.
@loganfsmyth do you have an idea how I could tackle this one differently?
The only thing that comes to my mind is to wrap a class with IIFE in class-properties transform and hardcode some detection logic in transform-classes to check if it should create IIFE or if it should just inline output nodes in place (when it would know that it's already in the IIFE).
It would create indirect dependency on what class-properties does, but even if the detection logic fails it would just create an extra IIFE (so the output still would be valid, just not "optimized").
5dd49ed to
1e70cd1
Compare
|
Hey @Andarist do you need help getting this PR updated? Would love to see it land. |
|
This is probably blocked until we merge the base class transform with the other classes plugins. I'll probably do that after we have finished adding all the new class features. |
|
@nicolo-ribaudo is there a tracking ticket / PR I can subscribe to? Thanks! |
|
I suggest just watching this PR |
I must say I'm not completely satisfied with the implementation. I've found that handling
ClassExpressions was troublesome and I've left them at the moment unpolished - in combination with static properties they can produce 2 nested IIFEs now - I'd like to revisit this, but possibly as a followup PR as I don't have much time lately to work on OSS and I wouldn't like to postpone merging this IMHO important PR.