Skip to content
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

Merged
merged 14 commits into from May 22, 2018
Merged

Compact output mode #2151

merged 14 commits into from May 22, 2018

Conversation

guybedford
Copy link
Contributor

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.

@@ -0,0 +1,3 @@
varfoo=(function(x){'use strict';x=x&&x.hasOwnProperty('default')?x['default']:x;function foo () {

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

Copy link
Member

@lukastaegert lukastaegert left a 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';
Copy link
Member

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).

Copy link
Member

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__*/'}${
Copy link
Member

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);
Copy link
Member

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?

Copy link
Contributor Author

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';
Copy link
Member

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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test?

Copy link
Member

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?

@guybedford
Copy link
Contributor Author

Thanks for the review, I've updated the PR.

Copy link
Member

@lukastaegert lukastaegert left a 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';
Copy link
Member

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);
Copy link
Member

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

Copy link
Contributor Author

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;});
Copy link
Member

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)

Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Member

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

Copy link
Member

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'),
Copy link
Member

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';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice adding this!

@guybedford
Copy link
Contributor Author

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.

Copy link
Member

@lukastaegert lukastaegert left a 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');
Copy link
Member

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 () {
Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

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;});
Copy link
Member

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 = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 for adding this test

@guybedford
Copy link
Contributor Author

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.

Copy link
Member

@lukastaegert lukastaegert left a 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.

@guybedford
Copy link
Contributor Author

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 experimentalCompact for example.

@tivac
Copy link
Contributor

tivac commented May 15, 2018

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?

@guybedford
Copy link
Contributor Author

@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.

@lukastaegert
Copy link
Member

@guybedford I do not think we need an experimental option for this. Even though the exact results of this option may change, I would expect it to remain valid at least for a while.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants