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

Scope hoisted ES6 classes fail if minified #2513

Closed
postspectacular opened this issue Jan 5, 2019 · 8 comments
Closed

Scope hoisted ES6 classes fail if minified #2513

postspectacular opened this issue Jan 5, 2019 · 8 comments

Comments

@postspectacular
Copy link

🐛 bug report

I think this is related to #2041, but showing the same symptom in a slightly different context. I.e. It seems that when using scope hoisting and minification, any class method cannot refer to its own class, i.e. to create a copy of itself:

export class Foo {
    constructor(x) {
        this.x = x;
    }
    copy() {
        // this call fails currently...
        return new Foo(this.x);
    }
}

🎛 Configuration (.babelrc, package.json, cli command)

// in package.json
"browserslist": ["since 2018"]

🤔 Expected Behavior

The above code should execute as expected (and DOES work if minification is disabled), but throws a runtime error due to wrong naming resolution in the minified version:

😯 Current Behavior

The relevant parts of the built, non-minified version are:

var $wwEC$exports = {};

class $wwEC$export$Foo {
    constructor(x) {
        this.x = x;
    }

    copy() {
        return new $wwEC$export$Foo(this.x);
    }

}

$wwEC$exports.Foo = $wwEC$export$Foo;
const $wwEC$var$a = new $wwEC$export$Foo(42).copy();

However, once minified, this turns into and the generated $wwEC$export$Foo does NOT get properly replaced:

var a = {};
class b {
    constructor(o) {
        this.x = o
    }
    copy() {
        return new $wwEC$export$Foo(this.x)
    }
}
a.Foo = b;
const c = new b(42).copy();

💁 Possible Solution

Not sure if this is an issue w/ Terser or Parcel... Right now the only workaround for some cases is to delegate to separate functions, e.g. to implement the above .copy() operation:

export class Foo {
    constructor(x) {
        this.x = x;
    }
    copy() {
        // this works!
        return foo(this.x);
    }
}

export const foo = (x) => new Foo(x);

🔦 Context

It's a pretty common scenario for class methods to refer to its own class by name and right now this issue prevents any package containing such cases from using scope hoisting, which is a real shame!

💻 Code Sample

See above.

🌍 Your Environment

Software Version(s)
Parcel 1.11.0
Node 11.5.0
npm/Yarn 1.12.3
Operating System OSX
@mischnic
Copy link
Member

mischnic commented Jan 5, 2019

It works without "browserslist": ["since 2018"], so Parcel doesn't handle the ES6 classes correctly (which would otherwise be transpiled by Babel)

@postspectacular
Copy link
Author

@mischnic That's interesting, though I really don't want Babel to transpile down to ES5. So it'd be great to have this working w/ ES6 outputs too... Happy to take a look myself, just not quite sure where to start. Any pointers?

@mischnic
Copy link
Member

mischnic commented Jan 5, 2019

Happy to take a look myself, just not quite sure where to start. Any pointers?

This call here


renames the variables. It seems to miss some occurences.
Minimal example:

class Foo {
    copy() {
        return new Foo();
    }
}

new Foo();

@mischnic
Copy link
Member

mischnic commented Jan 5, 2019

For some reason, the reference to the class inside of copy isn't listed in referencePaths

for (let path of binding.referencePaths) {

Replacing

with scope.rename(name, newName) fixes the issue. (There was a similar issue before)

Babel works correctly:

const core = require('@babel/core');
const traverse = require('@babel/traverse').default;

const code = `
class Foo {
    copy() {
        return new Foo();
    }
}

new Foo();
`;

const { ast } = core.transformSync(code, {ast: true});

traverse(ast, {
  Program(path) {
    console.log(path.scope.getBinding('Foo').referencePaths.length);
  }
});

postspectacular added a commit to thi-ng/umbrella that referenced this issue Jan 10, 2019
* feature/multi-output: (28 commits)
  docs(examples): add troubleshooting notes
  fix(examples): add quote escaping in xml-converter
  build(examples): update tsconfig for commit-table-ssr
  docs(examples): update readmes to refer to wiki build instructions
  build: update project tpl scripts, ignore files
  build: update make-module script
  build: update bundle-module outputs, emit meta data, update .gitignore
  fix(transducers): update juxt re-export
  build: temporarily add webpack until parcel-bundler/parcel#2513 is fixed
  feat(examples): add package-stats charting example
  minor(examples): cleanup
  build(examples): update dev deps
  build(examples): enable scope hoisting in `yarn build` scripts
  minor: update imports in readme's / examples
  build: update .gitignore
  refactor(examples): update imports & deps of all examples
  fix(csp): disable __State reverse enum lookup
  fix(rstream-log): remove __Level reverse enum lookup, update Level (non const)
  fix(rstream-gestures): disable __GestureType reverse enum export
  fix(rstream): disable __State reverse enum lookups
  ...
postspectacular added a commit to thi-ng/umbrella that referenced this issue Jan 10, 2019
* develop: (65 commits)
  test(api): fix import in mixin test
  build: add test-only yarn script
  build: fix cjs output path in bundle-module script
  docs(examples): add troubleshooting notes
  fix(examples): add quote escaping in xml-converter
  build(examples): update tsconfig for commit-table-ssr
  docs(examples): update readmes to refer to wiki build instructions
  build: update project tpl scripts, ignore files
  build: update make-module script
  build: update bundle-module outputs, emit meta data, update .gitignore
  fix(transducers): update juxt re-export
  build: temporarily add webpack until parcel-bundler/parcel#2513 is fixed
  feat(examples): add package-stats charting example
  minor(examples): cleanup
  build(examples): update dev deps
  build(examples): enable scope hoisting in `yarn build` scripts
  minor: update imports in readme's / examples
  build: update .gitignore
  refactor(examples): update imports & deps of all examples
  fix(csp): disable __State reverse enum lookup
  ...
@mischnic
Copy link
Member

mischnic commented Jan 12, 2019

@devongovett The babel bugs become more even more obscure:
The babel code below outputs (the inner Foo isn't renamed because it's missing in referencePaths):

class abc {
  copy() {
    return new Foo();
  }

}

new abc();

Remove scope.crawl() and it works just fine.
babel's scope.rename() also works properly in both cases.

const core = require("@babel/core");
const traverse = require("@babel/traverse").default;
const generator = require("@babel/generator").default;

const code = `
class Foo {
    copy() {
        return new Foo();
    }
}
new Foo();
`;

const { ast } = core.transformSync(code, { ast: true });

traverse(ast, {
	Program(path) {
		const { scope } = path;

		scope.crawl();

		// parcel's rename algorithm:
		// ------------------------------
		let binding = scope.getBinding("Foo");

		// Rename all references
		for (let path of binding.referencePaths) {
			if (path.node.name === "Foo") {
				path.node.name = "abc";
			}
		}

		// Rename binding identifier, and update scope.
		scope.removeOwnBinding("Foo");
		scope.bindings["abc"] = binding;
		binding.identifier.name = "abc";
	}
});

console.log(generator(ast).code);

@devongovett
Copy link
Member

This is possibly fixed by #2986.

@mischnic
Copy link
Member

mischnic commented May 7, 2019

This is possibly fixed by #2986.

Yes. The other issue #2981 is actually a duplicate of this one.

@postspectacular
Copy link
Author

Hi @mischnic @devongovett - thanks for this!!! Is that fix going to be released anytime soon or will be part of v2?

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

No branches or pull requests

4 participants