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
Compact output mode #2151
Compact output mode #2151
Conversation
@@ -0,0 +1,3 @@ | |||
varfoo=(function(x){'use strict';x=x&&x.hasOwnProperty('default')?x['default']:x;function foo () { |
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.
varfoo seems as a bug
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.
Thanks!
aa72b97
to
b42bbba
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.
An interesting feature though I am not sure how much it will actually be used. There are a few things I would love to see tested + some other minor comments.
@@ -47,11 +47,14 @@ export default class NamespaceVariable extends Variable { | |||
} | |||
|
|||
renderBlock(options: RenderOptions) { | |||
const _ = options.compact ? '' : ' '; | |||
const nl = options.compact ? '' : '\n'; |
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.
Just as t
represents a \t
, maybe this should actually be an n
(You probably know I am not a huge fan of abbreviated variable names but this way it is actually somewhat consistent).
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.
Also, even though it is not 100% certain it will be used, it would help readability to define t
here as well and replace other usages of options.indent
.
@@ -61,27 +64,28 @@ export default class NamespaceVariable extends Variable { | |||
const name = this.getName(); | |||
|
|||
const callee = options.freeze | |||
? `/*#__PURE__*/${options.legacy ? `(Object.freeze || Object)` : `Object.freeze`}` | |||
? `${options.compact ? '' : '/*#__PURE__*/'}${ |
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 do not think compact output should remove this annotation as this would mean that this is not only a visual change but it removes functionality, at least for some users, which I do not think is the intention here.
src/Chunk.ts
Outdated
if (addons.banner) magicString.prepend(addons.banner + '\n'); | ||
if (addons.footer) magicString.append('\n' + addons.footer); | ||
if (addons.banner) magicString.prepend(addons.banner + nl); | ||
if (addons.footer) magicString.append(nl + addons.footer); |
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 if NOT adding new lines after banners or before footers will not break things for some i.e. if the banner is a line-comment for headers or specific types of code for footers. Maybe addons.ts should take care there is always a line-break?
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.
Good catch.
src/Chunk.ts
Outdated
private prepareDynamicImports({ format }: OutputOptions) { | ||
private prepareDynamicImports({ format, compact }: OutputOptions) { | ||
const _ = compact ? '' : ' '; | ||
const nl = compact ? '' : '\n'; |
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.
Again I would just call this n
. Also, I would very much love to see a test for dynamic imports in compact mode.
options.indent | ||
}Object.defineProperty(${name}, 'toString', { value: function () { return '[object Module]' } }); | ||
${callee}(${name});`; | ||
const t = options.indent; |
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?
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.
BTW is there a reason why this is not merged with the previous if statement?
Thanks for the review, I've updated the 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.
Thanks for the updates from the initial review. There are a few more things I stumbled upon which is mainly wrong code in the form
test that went unnoticed. Maybe we can make this test a function test as well?
Also I would still like to see a test involving dynamic imports as the compactification of the dynamic import mechanism is still completely untested.
src/Chunk.ts
Outdated
private prepareDynamicImports({ format }: OutputOptions) { | ||
private prepareDynamicImports({ format, compact }: OutputOptions) { | ||
const _ = compact ? '' : ' '; | ||
const n = compact ? '' : '\n'; |
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 one is actually unused
@@ -0,0 +1,4 @@ | |||
define(['external'],function(x){'use strict';x=x&&x.hasOwnProperty('default')?x['default']:x;var self = {get default(){returnfoo$$1}};if(typeof Symbol!=='undefined'&&Symbol.toStringTag)Object.defineProperty(self,Symbol.toStringTag,{value:'Module'});elseObject.defineProperty(self,'toString',{value:function(){return'[object Module]';}});/*#__PURE__*/Object.freeze(self);console.log(self); |
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.
Now this is why I think we need functional tests and should not stop writing them! There are at least two things wrong here that a functional test could probably have found but a human easily overlooks:
- space missing in
returnfoo$$1
- space missing in
elseObject
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 caught - will add these.
define(['external'],function(x){'use strict';x=x&&x.hasOwnProperty('default')?x['default']:x;var self = {get default(){returnfoo$$1}};if(typeof Symbol!=='undefined'&&Symbol.toStringTag)Object.defineProperty(self,Symbol.toStringTag,{value:'Module'});elseObject.defineProperty(self,'toString',{value:function(){return'[object Module]';}});/*#__PURE__*/Object.freeze(self);console.log(self); | ||
function foo$$1 () { | ||
console.log( x ); | ||
}return foo$$1;}); |
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.
Also it would be nice to separate the user code from the wrapper via new-lines. Not only is it more consistent with the concept of only compactifying the auto-generated code, it will also break down if the user's code ends with a line comment (would not hurt to add this to the test 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.
Good point. Let me rather add the ability to detect and remove trailing line comments.
The intention here is that a plugin can perform minification in the transform hook to get an overall minification build running over per-module minify workers.
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.
But trailing line-comments could also be a purposefully placed footer comment. It might be ok to remove it in some situations but we actually have logic to NOT do this as it is still one goal for rollup to preserve all comments a user added as long as we think they do not describe removed code. If a minifier is doing its thing, it will remove those comments anyway so the presence of such a comment would signal that minification was NOT performed and is NOT the intended goal here.
And line comments are just an example. If you throw a line such as const x = 3 return foo;
into acorn, it will tell you it is invalid syntax. So you also need to make sure there is a semicolon. Etc. etc.
All in all I am not happy about this approach and would prefer the line-break.
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.
On the other hand, there is no reason to keep the semicolon after return foo$$1
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.
Oh, reading the actual code you wrote, you actually DO preserve the trailing comments. So I am cool with that.
Nevertheless, we should also detect for the situation where the last line is non-empty, does not contain a line-comment and where the last non-whitespace character is not a semicolon in which cases I would suggest to append a semicolon instead.
collectAddon(graph, options.banner, 'banner'), | ||
collectAddon(graph, options.footer, 'footer'), | ||
collectAddon(graph, options.banner, 'banner', '\n'), | ||
collectAddon(graph, options.footer, 'footer', '\n'), |
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 much safer 👍
@@ -0,0 +1,6 @@ | |||
import x from 'external'; | |||
import * as self from './main.js'; |
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.
Nice adding this!
Thanks again for the thorough review here and catching the edge cases. I've addressed the changes and included the functional tests you mentioned. For checking a last line comment on a source I created Rich-Harris/magic-string#142 and added that check here, so that should be merged first. |
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.
Thanks for checking my comments, I am nearly good with this except the issue I outlined in some places where I would prefer to see a semicolon. Might be worth checking out if there could be issues even in the non-compact case that we might fix here as well. #2110 is a prime candidate for this as the issue manifests due to the module concatenation.
src/Chunk.ts
Outdated
@@ -802,6 +808,7 @@ export default class Chunk { | |||
for (const module of this.orderedModules) { | |||
const source = module.render(renderOptions); | |||
source.trim(); | |||
if (options.compact && source.lastLine().indexOf('//') !== -1) source.append('\n'); |
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 already stated, we should also add a semi-colon if there is neither a line-comment nor a trailing semi-colon.
@@ -0,0 +1,6 @@ | |||
define(['exports'],function(exports){'use strict';function fn () { | |||
console.log('lib2 fn'); | |||
}function fn$1 () { |
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 only works because function declarations do not require semicolons to be on the same line as other statements. The same does not hold for e.g. other declarations—
const x =3function y(){}
would NOT be valid code, even if you add a space in between. A semicolon would be the proper solution here.
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.
Note that in the const x = 3
case, the VariableDeclaration render function will ensure a trailing semicolon already for us.
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.
Oh, you are right! For declarations and expression statements, this is taken care of 👍
}function fn$1 () { | ||
fn(); | ||
console.log('dep2 fn'); | ||
}exports.a=fn$1;}); |
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.
and again we should consider adding semicolons even if it means adding empty statements in some situations. Alternatively, line-breaks might work as well but would not solve issues where the succeeding code starts with an opening bracket.
@@ -0,0 +1,23 @@ | |||
module.exports = { |
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 adding this test
I've added a fix here for #2110. I think we shouldn't add a semicolon for the compact case actually - since these cases are already handled by the renderer itself in the individual node renders, which seems exactly how this should be handled to me. |
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.
Looks good! Now we just need the magic-string PR to be released.
With the magic string update, the tests are now passing here. @lukastaegert happy to keep this experimental for a while, and just let me know if you want to rename to |
What's the advantage to having rollup do this itself (though only for the rollup-added wrapping code) as opposed to doing whole-app compaction once the bundle is generated? |
@tivac it's an experimental step along the path to turning Rollup into a minifier. For example, mangling is an easy feature for Rollup to add, which when combined with this gets us there. I'm then experimenting with per-source minification for a fast build workflow. Has to be weighed up against whole-graph minification optimizations, but so far there doesn't seem to be much loss. 2/3 build time feels like a win to me so far, but as I say it's experimental. |
@guybedford I do not think we need an |
This implements a compact output mode for Rollup wrappers and injections.
It doesn't remove whitespace in user code, only for the wrappers etc in the Rollup generation itself.
It will be interesting to see where this goes - perhaps whitespace removal in Rollup itself would be useful, perhaps it won't be great for performance, it's hard to tell.
But this gives some starting point that when combined with translate functions doing per-module minification gives an overall minified output that looks pretty good.