-
-
Notifications
You must be signed in to change notification settings - Fork 48
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(treeshaking): allowing tree-shaking with terser #74
base: main
Are you sure you want to change the base?
Conversation
I've been trying to find a good way to demonstrate the issue, and the best I've come up with is to past the following into https://try.terser.org/ and then swap the comments on the last two lines. The same behavior reproduces when using esbuild as the minifier: link function getExports(component) {
return component.exports
}
function normalizeComponent (
scriptExports,
render,
staticRenderFns,
functionalTemplate,
injectStyles,
scopeId,
moduleIdentifier, /* server only */
shadowMode /* vue-cli only */
) {
// Vue.extend constructor export interop
var options = typeof scriptExports === 'function'
? scriptExports.options
: scriptExports
// render functions
if (render) {
options.render = render
options.staticRenderFns = staticRenderFns
options._compiled = true
}
// functional template
if (functionalTemplate) {
options.functional = true
}
// scopedId
if (scopeId) {
options._scopeId = 'data-v-' + scopeId
}
var hook
if (moduleIdentifier) { // server build
hook = function (context) {
// 2.3 injection
context =
context || // cached call
(this.$vnode && this.$vnode.ssrContext) || // stateful
(this.parent && this.parent.$vnode && this.parent.$vnode.ssrContext) // functional
// 2.2 with runInNewContext: true
if (!context && typeof __VUE_SSR_CONTEXT__ !== 'undefined') {
context = __VUE_SSR_CONTEXT__
}
// inject component styles
if (injectStyles) {
injectStyles.call(this, context)
}
// register component module identifier for async chunk inference
if (context && context._registeredComponents) {
context._registeredComponents.add(moduleIdentifier)
}
}
// used by ssr in case component is cached and beforeCreate
// never gets called
options._ssrRegister = hook
} else if (injectStyles) {
hook = shadowMode
? function () {
injectStyles.call(
this,
(options.functional ? this.parent : this).$root.$options.shadowRoot
)
}
: injectStyles
}
if (hook) {
if (options.functional) {
// for template-only hot-reload because in that case the render fn doesn't
// go through the normalizer
options._injectStyles = hook
// register for functional component in vue file
var originalRender = options.render
options.render = function renderWithStyleInjection (h, context) {
hook.call(context)
return originalRender(h, context)
}
} else {
// inject component registration as beforeCreate hook
var existing = options.beforeCreate
options.beforeCreate = existing
? [].concat(existing, hook)
: [hook]
}
}
return {
exports: scriptExports,
options: options
}
}
const icon = "_icon_5y8ds_1";
const style0 = {
icon,
};
const _sfc_main = /* @__PURE__ */ Vue.extend({
computed: {
value() {
return Date.now();
},
},
});
var _sfc_render = function render() {
var _vm = this, _c = _vm._self._c;
_vm._self._setupProxy;
return _c("div", { class: _vm.iconClasses }, [_vm._t("default")], 2);
};
var _sfc_staticRenderFns = [];
const __cssModules = {
"$style": style0
};
function _sfc_injectStyles(ctx) {
for (var key in __cssModules) {
this[key] = __cssModules[key];
}
}
var __component__ = /*#__PURE__*/ normalizeComponent(
_sfc_main,
_sfc_render,
_sfc_staticRenderFns,
false,
_sfc_injectStyles,
null,
null,
null
)
// swap between the commented lines below:
var Foo = __component__.exports
// var Foo = /*#__PURE__*/ getExports(__component__) The version of this code represented by this fix ( You can also reproduce the fully tree-shaken result with the existing code by using the following terser options to assert that all getters are pure, but it isn't generally correct or safe to do so: {
"compress": {
"pure_getters": true
}
} |
The code currently generated by this plugin looks something like the following when building a library: src/index.js ```js export {default as Foo} from './component.vue'; ``` ```js var __component__ = /*#__PURE__*/ normalizeComponent(...) var Foo = __component__.exports ``` In the event you aren't actually using the Foo export, this code is unable to be dropped because Terser assumes that property access (__component__.exports) is not side-effect free and as a result it decides it can not drop it, and thus it can't drop the rest of the component code. To work around this, I have wrapped the `__component__.exports` statement in a function so that we can mark the function invokation as `#__PURE__` and thus allow for unused components to be dropped fully. The resulting code looks like: ```js var __component__ = /*#__PURE__*/ normalizeComponent(...) var Foo = /*#__PURE__*/ getExports(__component__) ```
2f53050
to
3c2f6b9
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.
@sodatea let me know if you have any questions about this PR.
@@ -137,7 +137,7 @@ var __component__ = /*#__PURE__*/__normalizer( | |||
|
|||
let resolvedMap: RawSourceMap | undefined = scriptMap | |||
|
|||
output.push(`export default __component__.exports`) | |||
output.push(`export default /*#__PURE__*/ __getExports(__component__)`) |
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.
We could also have not exported the getExports
function from the normalizer module and instead just used an IIFE:
output.push(`export default /*#__PURE__*/ (function() { return __component__.exports })()`)
But I thought the exported function was more clear. That said, the IIFE approach wouldn't require touching the NORMALIZER_ID module at all, so it might be preferable for that reason.
return component.exports | ||
} | ||
|
||
export function normalizeComponent ( |
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 switched this to be a named export instead out of habit since I've run into issues in the past with mixing named and default exports for commonjs modules.
I can switch this back to the default export if you'd prefer.
You are absolutely right. I also encountered the same issue, and it seems that this merge request will have no negative impact. please look about this merge request @sodatea |
The code currently generated by this plugin looks something like the following when building a library:
src/index.js
In the event you aren't actually using the Foo export, using a minifier like Terser, this code is unable to be dropped. This is because Terser assumes that the property property access (
__component__.exports
) is not side-effect free, and as a result it decides it can not drop it, and by extension it can not drop the rest of the component code. For more context, see thepure_getters
documentation here: https://terser.org/docs/api-reference#compress-optionsTo work around this, I have wrapped the
__component__.exports
statement in a function so that we can mark the function invokation as#__PURE__
and thus allow for unused components bundled into a single library to be dropped fully.The resulting code looks like:
Additional Context about the problem
I am using vite & vite-plugin-vue2 to build a component library. In this library there are a couple hundred icons which are each vue components. The library is shipped as a single .js file with named exports for each icon component.
When consuming this library from a vite application with the default minification settings (esbuild), tree-shaking works correctly and only the icons that are used are present in the bundled application code. I believe rollup may be just handling things better here since terser and esbuild both seem to behave identically in this regard. (see comment below for reproducers)
However, when consuming this library from webpack and using terser as the minification engine, the bundled application has the code for every component is being included even though only a couple are being used. When I make the change in this PR, I am no longer seeing the unused components in the resulting bundle.