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

Moving CJS to ES6 Modules #3379

Closed
wants to merge 5 commits into
base: master
from

Conversation

Projects
None yet
7 participants
@Cmdv
Copy link
Contributor

Cmdv commented Jun 7, 2018

I've just been experimenting with generating ES6 modules, but before I went any further (add tests etc) wanted some opinions πŸ˜„

Think rather than a 1 to 1 conversion it would be great for users to opt in with say a flag -es6?
If this is a good idea could someone point me to an example of something using a flag.

Also if you look below at the output of the JS I ended up using:

import name from '../file/name.js'

instead of:

import * as name from '../file/name.js'

When I tested this * as format with Webpack 4 it just came up with lots of crazy warnings:

WARNING in ./output/Data.Foldable/index.js 182:33-51
"export 'Nothing' (imported as 'Data_Maybe') was not found in '../Data.Maybe/index.js'
 @ ./output/Effect.Aff/index.js
 @ ./output/Main/index.js

The second issue was that foreign exports needed to be an object literal with key values and I wasn't able to use ES6 version of just:
export { a, b , c} because I'm not importing like import {a, b, c} from '../foreign.js'
but I got around this by keeping the current functionality and adding default keyword to the export.

So far the output is as such:

"use strict";
import Button from "../Button/index.js";
import Control_Bind from "../Control.Bind/index.js";
import Data_Unit from "../Data.Unit/index.js";
import Effect from "../Effect/index.js";
import Effect_Aff from "../Effect.Aff/index.js";
import Halogen_Aff from "../Halogen.Aff/index.js";
import Halogen_Aff_Util from "../Halogen.Aff.Util/index.js";
import Halogen_VDom_Driver from "../Halogen.VDom.Driver/index.js";
import Prelude from "../Prelude/index.js";
var main = Halogen_Aff_Util.runHalogenAff(Control_Bind.bind(Effect_Aff.bindAff)(Halogen_Aff_Util.awaitBody)(function (v) {
    return Halogen_VDom_Driver.runUI(Button.myButton)(Data_Unit.unit)(v);
}));
export default{
    main: main
};

Then foreign:

// Generated by purs version 0.12.0
"use strict";
import $foreign from "./foreign.js";
export default {
    abs: $foreign.abs,
    acos: $foreign.acos,
    asin: $foreign.asin,
    ...
};

I tested this against Halogen Basic Demo with Webpack 4 with > webpack --mode production
and the output file was still fairly large at 651 KiB so I'm not sure it's doing all of the DCE / Tree shacking properly.

related to #1206 Β―\(ツ)/Β―

@Cmdv

This comment has been minimized.

Copy link
Contributor

Cmdv commented Jun 7, 2018

Ok going to change things a little as the DCE isn't working with how I have things currently:

Normal export will be:

var cube =  function (x) {
  return x * x * x;
}
export { cube };

Foreign exports will need to go be a little more explicit:

export const abs = $foreign.abs
@spicydonuts

This comment has been minimized.

Copy link

spicydonuts commented Jun 7, 2018

TypeScript has an option for choosing the output target (es2015, es5, etc). If it's just changing the export style es-modules might be more clear.

@paf31

This comment has been minimized.

Copy link
Member

paf31 commented Jun 7, 2018

I'm glad you started looking at ES6 imports and exports, but the real problem is DCE and bundling, as you've found out. There has been a lot of discussion about this in the past, and we decided that someone should go prototype a backend for ES6 first (so it's great that you have!). This can exist as a fork and be useful regardless of whether we can solve the DCE problem in the short term.

Fixing DCE will involve changing purs bundle, and (the hard bit), language-javascript to support ES6 syntax.

@chexxor

This comment has been minimized.

Copy link
Contributor

chexxor commented Jun 8, 2018

Looks like the language-javascript has a PR (erikd/language-javascript#64) which adds partial ES6 support. I wonder if it's ok to use a branch of that project for this project's needs.

@michaelficarra

This comment has been minimized.

Copy link
Contributor

michaelficarra commented Jun 11, 2018

@chexxor There's also the possibility that I proposed long ago of implementing just an import/export extractor for JS modules. That's all we care about, right? This would allow us to be much lazier about parsing. We don't care to actually parse the full module as long as we can accurately skip the parts we are uninterested in.

@spicydonuts

This comment has been minimized.

Copy link

spicydonuts commented Jun 11, 2018

Which turns out to be simpler and safer with es modules than cjs πŸ™‚

@Cmdv

This comment has been minimized.

Copy link
Contributor

Cmdv commented Jun 12, 2018

I'm now pretty close but have a couple of niggly errors:

// foreign.js
exports["insertCell'"] = function () {}

// index.js
import $foreign from "./foreign.js";

export const insertCell' = $foreign["insertCell'"];

doesn't like insertCell' because ' is an invalid identifier according to ECMAScript 6.

and the other is a hanging ; which I'm sure could be sorted out with correct output for this:

match (ExportStandard _ []) = return $ emit ""

I've tried various solutions with the way you can export + import but it still seems that I'm not getting DCE to work. I'm hopeful that maybe if I can get around the above it might fix it but I'm not too certain that is going to be the case.

@spicydonuts

This comment has been minimized.

Copy link

spicydonuts commented Jun 12, 2018

Should the ffi file use modules as well? Either way, I've seen ' renamed to $prime elsewhere in the generated code.

// foreign.js
export const insertCell$prime = function () {};

// index.js
import { insertCell$prime } from "./foreign.js";

export const insertCell$prime = insertCell$prime;
@michaelficarra

This comment has been minimized.

Copy link
Contributor

michaelficarra commented Jun 12, 2018

I'd be fine with just disallowing foreign imports of invalid JS identifiers when you're backed by a JS implementation.

@Cmdv

This comment has been minimized.

Copy link
Contributor

Cmdv commented Jun 12, 2018

@spicydonuts that would be a good idea, been giving that a go.

@michaelficarra just thinking of backwards compatibility really, because using prime is quite a common thing in PS / Haskell.

I've managed to change the export but just searching for it's use like:
Data_Lens_Prism["prism'"] as I would need to change these too.

@Cmdv

This comment has been minimized.

Copy link
Contributor

Cmdv commented Aug 17, 2018

I'm going to park this for the moment as not finding the time to finish it. Don't think I was too far away but will re attempt in the future if no one else has already done it πŸ˜„

@Cmdv Cmdv closed this Aug 17, 2018

@utkarshkukreti

This comment has been minimized.

Copy link
Contributor

utkarshkukreti commented Sep 5, 2018

Hi @Cmdv,

Thanks for the work on this! I just tried out your branch (with a tiny patch to fix a bug, mentioned at the end) on the Halogen basic example. After running webpack -p on the main module, the bundle output was 341K, compared to pulp browserify --optimise which produced a 354K file. I believe the size should go down even further if the foreign files start using ES6 modules (I don't think webpack is able to remove any dead code in foreign files). Could you tell me what things need to be done before this is considered ready? I might be able to help!

Tiny patch:

--- a/src/Language/PureScript/CodeGen/JS.hs
+++ b/src/Language/PureScript/CodeGen/JS.hs
@@ -166,7 +166,7 @@ moduleToJs (Module _ coms mn _ imps exps foreigns decls) foreign_ =
   -- a PureScript identifier. If the name is not valid in JavaScript (symbol based, reserved name) an
   -- indexer is returned.
   accessor :: Ident -> AST -> AST
-  accessor (Ident prop) = accessorString $ mkString prop
+  accessor (Ident prop) = accessorString $ mkString $ properToJs prop
   accessor (GenIdent _ _) = internalError "GenIdent in accessor"
   accessor UnusedIdent = internalError "UnusedIdent in accessor"

(Your code exports a function named const as $$const but the code to access that property outputs ["const"] without this patch.)

@Cmdv

This comment has been minimized.

Copy link
Contributor

Cmdv commented Sep 6, 2018

@utkarshkukreti thanks I was literally thinking about starting this off again today.

Think I went towards converting all keywords to using $$ as I still had a discrepancy between how things were being Imported + Exported.

I can't recall if I did more work on this locally and broke things even further haha. I'll have a look and respond as this would be a cool feature πŸ˜„

Right so just been having a look, re you're last comment:

(Your code exports a function named const as $$const but the code to access that property outputs ["const"] without this patch.)

I've just checked and it does seem to output ["$$const"] though technically that's not required as it could quite easily do FOO.$$const()

The other part that I have an issue with is before it used to do 2 different types of exports but now it only does one and I couldn't work out where this trailing ; came from:

export {
    main
};
;

just feels like with this working it should be able to be a lot more performant than this, do you feel it's more down to how the FFI's are not using ES6? I might try it out see if it does make a difference πŸ˜„

@Cmdv

This comment has been minimized.

Copy link
Contributor

Cmdv commented Sep 6, 2018

right so having experimented with converting an FFI to using ES6 export instead of CJS and it fails. This is probably done by the parser too so will need the ability to handle ES6 too, yet still being backwards compatible otherwise nothing will work unless all FFI code is converted to ES6!! that for sure doesn't sound fun.

FutureToken {tokenSpan = TokenPn 31760 1140 1, tokenLiteral = "export", tokenComment = [WhiteSpace (TokenPn 31758 1138 27) "\n\n"]}
@utkarshkukreti

This comment has been minimized.

Copy link
Contributor

utkarshkukreti commented Sep 6, 2018

Yeah, looks like this is gonna require some more work. The language-javascript package recently got support for ES6 import/export statements. The compiler needs to be modified to support that.

I was able to get webpack to work transparently with both ES6 and CommonJS modules:

➜ cat Main.js
import * as First from './First';
import * as Second from './Second';

console.log(First.first, Second.second);
➜ cat First.js
module.exports = { first: 1 };
➜ cat Second.js
export const second = 2;
➜ webpack Main.js
...
➜ node dist/main.js
1 2
@gabejohnson

This comment has been minimized.

Copy link

gabejohnson commented Sep 6, 2018

@utkarshkukreti looks like that merge was reverted in favor of implementing the features individually erikd/language-javascript#71

@utkarshkukreti

This comment has been minimized.

Copy link
Contributor

utkarshkukreti commented Sep 6, 2018

Ah, you're right. No support for import/export yet. 😞

@Cmdv

This comment has been minimized.

Copy link
Contributor

Cmdv commented Sep 6, 2018

that's cool though I might do it as the work has already been done just needs an individual PR πŸ˜„

@Cmdv

This comment has been minimized.

Copy link
Contributor

Cmdv commented Sep 7, 2018

@utkarshkukreti not sure if you or anyone else would be keen but now have 2 PR's on language-javascript for import + export but need a bit of help as getting a bit stuck!

@Cmdv Cmdv referenced this pull request Sep 8, 2018

Open

Common JS to AS6/7 modules - WIP #3427

2 of 5 tasks complete

@gasi gasi referenced this pull request Nov 18, 2018

Closed

Parse ES2015 (aka ES6) `export` Declarations (partial support) #79

2 of 8 tasks complete
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment