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

Make CanJS tree-shakable #4013

Closed
justinbmeyer opened this Issue Mar 7, 2018 · 14 comments

Comments

Projects
None yet
3 participants
@justinbmeyer
Contributor

justinbmeyer commented Mar 7, 2018

TLDR: Make it so the can package export is tree-shake-able. Allowing you to import just what you want like:

import {Component, observe} from "can"

This was discussed on a recent live stream (8:14).

The Problem

Currently, to use CanJS, you likely have to install at least 8 different packages:

npm install can-component can-connect can-define can-route can-route-pushstate can-set can-stache can-stache-bindings --save

While this is for good reason, it's annoying.

Furthermore, to build something like a model, you need to import a bunch of stuff:

var DefineMap = require("can-define/map/map");
var DefineList = require("can-define/list/list");
var superMap = require("can-connect/can/super-map/super-map")
var set = require("can-set");

The Solution

I'd like to make the can package's main export a tree-shake-able ES module. This would allow importing enough to make a model look like:

import {DefineMap, DefineList, superMap, set} from "can";

This module should be tree-shake-able by both webpack and stealjs. So if someone imports only, for example ajax like:

import {ajax} from "can";

They would only get ajax in their build.

Questions that need answering

What is the minimal viable example of this?

Ideally, we will not have to rewrite every existing project to ES modules. Instead, steal.export() should be able to do the necessary transformations.

Also, it would probably be nice if we don't have to move every module within the single export. The main module might look like:

// can.js 
exports Component from "can-component";
exports route from "can-route";

Essentially, we will export everything by it's can-namespace name. Perhaps there's a way to do this programmatically within the module, but doubt it. Something like:

import "moduleA";
import "moduleB";
...
import "moduleZ";
import can from "can-namespace";
for(var prop in can){
  exports [prop] as can[prop]
}

But I would be fine maintaining this list of modules and exports for now.

How do we document this?

There are a few places in the docs that might be affected by using tree-shaking as the primary way of using CanJS:

  • The sidebar
  • The title
  • Examples

I still think CanJS's sidebar should use the module-name listing. But we should show the alias, perhaps next to the module name in the title area like:

canjs_-_can-component

What should examples how?

Guides should almost certainly show using the tree-shaking module. This will make JSBin / script tag use very similar. But what about individual packages and modules?

@chasenlehara chasenlehara changed the title from Make CanJS tree-shake-able to Make CanJS Tree Shakable Mar 9, 2018

@chasenlehara chasenlehara changed the title from Make CanJS Tree Shakable to Make CanJS Tree-Shakable Mar 9, 2018

@chasenlehara chasenlehara changed the title from Make CanJS Tree-Shakable to Make CanJS tree-shakable Mar 9, 2018

@matthewp

This comment has been minimized.

Contributor

matthewp commented Mar 16, 2018

Import options:

import {DefineMap, DefineList, superMap, set} from "can";

vs.

import can from "can";

can.Component ...
@matthewp

This comment has been minimized.

Contributor

matthewp commented Mar 27, 2018

Tested this out and if you have a module like:

import { Component } from 'can/es';

console.log(Component);

Where can/es looks like:

export {
	default as connect
} from 'can-connect/all';

export {
	default as DefineMap
} from 'can-define/map/map';

export {
	default as Component
} from 'can-component/es';

It will tree-shake out can-connect. Notice that the can-component module is can-component/es. It looks like this:

import Component from './can-component';

export default Component;

We need to add these wrapper modules to all of the projects that we want to include in can/es. We also need to add "sideEffects": true to the package.json of these projects.

@justinbmeyer

This comment has been minimized.

Contributor

justinbmeyer commented Mar 27, 2018

@matthewp A few questions:

  1. Does it tree-shake out other packages? So can-set isn't included? What about modules in can-util that can-connect might use, but the others don't.
  2. Does this imply every module (not just package) needs an es counterpart?
  3. These are the requirements for Webpack I assume. Do you know:
    1. Are these technical/mathmatical requirements, or is it just Webpack needs things a certain way right now. Is webpack looking to make CJS modules work?
    2. What does sideEffects true do? What's it's purpose?
@matthewp

This comment has been minimized.

Contributor

matthewp commented Mar 27, 2018

  1. It doesn't include can-set. Not sure about can-util types of things, as I don't know what can-connect uses that others do not.
  2. No, only the modules that are imported by can/es would need the wrapper es module.
  3. below
    i. I don't know of a technical requirement at this time, I don't plan on making the wrapper a requirement but perhaps there is a good reason, will have to wait until I add it to steal-tools to know.
    ii. It's an opt-in that a package can be tree-shaken, since import { foo } from 'pkg' could cause code to break if tree-shaken if importing causes needed side-effects.
@justinbmeyer

This comment has been minimized.

Contributor

justinbmeyer commented Mar 27, 2018

To clarify:

No, only the modules that are imported by can/es would need the wrapper es module.

You mean only modules DIRECTLY imported by can/es would need the wrapper ... right? This surprises me, lets say that can-component uses can-util/foo, and it the only user of foo. How is it able to tree-shake can-util/foo correctly if not an ES module?

@matthewp

This comment has been minimized.

Contributor

matthewp commented Mar 27, 2018

I don't understand your question, can-component is also not an ES module. Why are you surprised by can-util/foo but not can-component?

@matthewp

This comment has been minimized.

Contributor

matthewp commented Mar 27, 2018

I just tested and the wrapper modules do not need be in the separate packages. So if we wanted we could add (within canjs) a es/component.js that acts as the wrapper:

import Component from 'can-component';

export default Component;

Or shortened to:

export * from 'can-component';
@justinbmeyer

This comment has been minimized.

Contributor

justinbmeyer commented Mar 27, 2018

I don't understand your question, can-component is also not an ES module. Why are you surprised by can-util/foo but not can-component?

Yeah, it's surprising to me that it's able to tree shake those CJS modules too.

@matthewp

This comment has been minimized.

Contributor

matthewp commented Mar 27, 2018

It doesn't actually tree-shake the CJS module, it tree shakes the ES wrapper. Think about it like this, you have code:

app.js

import { Component } from 'can/es';

can/es.js

export { default as Component } from 'can-component/es';
export { default as connect } from 'can-connect/es';

When the tree-shaking occurs the latter gets treated as if it were:

export { default as Component } from 'can-component/es';

At this point it doesn't need to understand the CJS modules of can-connect, it's like it was never even imported.

@matthewp

This comment has been minimized.

Contributor

matthewp commented Mar 27, 2018

This is actually really good news because it means we don't have to rewrite everything into ES modules. The question becomes should we:

  1. Create wrapper modules in each package, ie create a can-component/es that just reexports the CJS source.
  2. Create the wrapper modules in canjs/canjs. This prevents needing to spread this feature into each package.

Argument for (1) would be that it makes it easier for people that use the individual packages to still get benefits of tree-shaking.

@chasenlehara

This comment has been minimized.

Member

chasenlehara commented Mar 27, 2018

+1 on option 1, I think we will still encourage expert users to use the individual packages.

Unrelated question: I can across this blog post recently, and it explains how import { times } from 'lodash' is worse than import times from 'lodash/times' when bundling Lodash with webpack.

It sounds like we won’t have that issue, so my question is, do we avoid that by exporting the modules like this from can/es?

export {
	default as connect
} from 'can-connect/all';
@justinbmeyer

This comment has been minimized.

Contributor

justinbmeyer commented Mar 28, 2018

@matthewp why does #1 do that? Are you talking about tree-shaking within the individual modules? import {realTime} from "can-connect"? This is mostly just a problem for can-connect I think right? Nothing else really "needs" tree-shaking within a module.

I think we should start with #2. It is probably easier to accomplish, makes adding new packages much easier, and over-all less to worry about. Once ES has "taken over". Speaking of which, where is node ES support?

@matthewp

This comment has been minimized.

Contributor

matthewp commented Mar 28, 2018

@justinbmeyer No, I'm not talking about adding granular exports for packages. I'm just talking about enabling people to get the benefits of tree shaking even if they are not using can. For example, this module:

import Component from 'can-component';
import connect from 'can-connect';

console.log(Component);

Doesn't use connect, but it will still be included because it is a CommonJS module. However if the code was:

import Component from 'can-component/es';
import connect from 'can-connect/es';

console.log(Component);

Then connect would be removed. It's the same deal as this where the wrapper is needed to get tree-shaking.

@justinbmeyer

This comment has been minimized.

Contributor

justinbmeyer commented Mar 28, 2018

@matthewp gotcha. I'd still say most linters would pick up on that example. I still don't think it's worth the extra hassle. I'd prefer to wait to update everything to ES than keep supporting two module syntaxes everywhere.

matthewp added a commit that referenced this issue Mar 30, 2018

Adds the can/es module for tree-shaking bundlers
This change adds a new module, `can/es` which exports everything from
__core__ as named exports. This enables people to import only the parts
of CanJS they need:

```js
import { Component, DefineMap } from "can/es";
```

Provided they use a tree-shaking capable bundler, the bundle size will
not be too large.

Closes #4013

matthewp added a commit that referenced this issue Mar 30, 2018

Adds the can/es module for tree-shaking bundlers
This change adds a new module, `can/es` which exports everything from
__core__ as named exports. This enables people to import only the parts
of CanJS they need:

```js
import { Component, DefineMap } from "can/es";
```

Provided they use a tree-shaking capable bundler, the bundle size will
not be too large.

Closes #4013
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment