diff --git a/packages/babel-plugin-class-features/.npmignore b/packages/babel-plugin-class-features/.npmignore new file mode 100644 index 000000000000..f9806945836e --- /dev/null +++ b/packages/babel-plugin-class-features/.npmignore @@ -0,0 +1,3 @@ +src +test +*.log diff --git a/packages/babel-plugin-class-features/README.md b/packages/babel-plugin-class-features/README.md new file mode 100644 index 000000000000..567c8ae908a2 --- /dev/null +++ b/packages/babel-plugin-class-features/README.md @@ -0,0 +1,19 @@ +# @babel/plugin-class-features + +> Compile class public and private fields, private methods and decorators to ES6 + +See our website [@babel/plugin-class-features](https://babeljs.io/docs/en/next/babel-plugin-class-features.html) for more information. + +## Install + +Using npm: + +```sh +npm install --save-dev @babel/plugin-class-features +``` + +or using yarn: + +```sh +yarn add @babel/plugin-class-features --dev +``` diff --git a/packages/babel-plugin-class-features/package.json b/packages/babel-plugin-class-features/package.json new file mode 100644 index 000000000000..1d55b96bd6fc --- /dev/null +++ b/packages/babel-plugin-class-features/package.json @@ -0,0 +1,27 @@ +{ + "name": "@babel/plugin-class-features", + "version": "7.1.4", + "author": "The Babel Team (https://babeljs.io/team)", + "license": "MIT", + "description": "Compile class public and private fields, private methods and decorators to ES6", + "repository": "https://github.com/babel/babel/tree/master/packages/babel-plugin-class-features", + "main": "lib/index.js", + "keywords": [ + "babel", + "babel-plugin" + ], + "dependencies": { + "@babel/helper-function-name": "^7.1.0", + "@babel/helper-member-expression-to-functions": "^7.0.0", + "@babel/helper-optimise-call-expression": "^7.0.0", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-replace-supers": "^7.1.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + }, + "devDependencies": { + "@babel/core": "^7.1.0", + "@babel/helper-plugin-test-runner": "^7.0.0" + } +} diff --git a/packages/babel-plugin-class-features/src/decorators.js b/packages/babel-plugin-class-features/src/decorators.js new file mode 100644 index 000000000000..e0f66f2c6a51 --- /dev/null +++ b/packages/babel-plugin-class-features/src/decorators.js @@ -0,0 +1,3 @@ +export function hasDecorators(path) { + return !!(path.node.decorators && path.node.decorators.length); +} diff --git a/packages/babel-plugin-class-features/src/features.js b/packages/babel-plugin-class-features/src/features.js new file mode 100644 index 000000000000..d777182852d7 --- /dev/null +++ b/packages/babel-plugin-class-features/src/features.js @@ -0,0 +1,62 @@ +import { hasDecorators } from "./decorators"; + +export const FEATURES = Object.freeze({ + //classes: 1 << 0, + fields: 1 << 1, + privateMethods: 1 << 2, + decorators: 1 << 3, +}); + +// We can't use a symbol because this needs to always be the same, even if +// this package isn't deduped by npm. e.g. +// - node_modules/ +// - @babel/plugin-class-features +// - @babel/plugin-proposal-decorators +// - node_modules +// - @babel-plugin-class-features +const featuresKey = "@babel/plugin-class-features/featuresKey"; +const looseKey = "@babel/plugin-class-features/looseKey"; + +export function enableFeature(file, feature, loose) { + // We can't blindly enable the feature because, if it was already set, + // "loose" can't be changed, so that + // @babel/plugin-class-properties { loose: true } + // @babel/plugin-class-properties { loose: false } + // is transformed in loose mode. + // We only enabled the feature if it was previously disabled. + if (!hasFeature(file, feature)) { + file.set(featuresKey, file.get(featuresKey) | feature); + if (loose) file.set(looseKey, file.get(looseKey) | feature); + } +} + +function hasFeature(file, feature) { + return !!(file.get(featuresKey) & feature); +} + +export function isLoose(file, feature) { + return !!(file.get(looseKey) & feature); +} + +export function verifyUsedFeatures(path, file) { + if (hasFeature(file, FEATURES.decorators)) { + throw new Error( + "@babel/plugin-class-features doesn't support decorators yet.", + ); + } + if (hasFeature(file, FEATURES.privateMethods)) { + throw new Error( + "@babel/plugin-class-features doesn't support private methods yet.", + ); + } + + if (hasDecorators(path) && !hasFeature(file, FEATURES.decorators)) { + throw path.buildCodeFrameError("Decorators are not enabled."); + } + + if (path.isProperty()) { + if (!hasFeature(file, FEATURES.fields)) { + throw path.buildCodeFrameError("Class fields are not enabled."); + } + } +} diff --git a/packages/babel-plugin-class-features/src/fields.js b/packages/babel-plugin-class-features/src/fields.js new file mode 100644 index 000000000000..0ed9329cb5d4 --- /dev/null +++ b/packages/babel-plugin-class-features/src/fields.js @@ -0,0 +1,322 @@ +import { template, traverse, types as t } from "@babel/core"; +import { environmentVisitor } from "@babel/helper-replace-supers"; +import memberExpressionToFunctions from "@babel/helper-member-expression-to-functions"; +import optimiseCall from "@babel/helper-optimise-call-expression"; + +export function buildPrivateNamesMap(props) { + const privateNamesMap = new Map(); + for (const prop of props) { + if (prop.isPrivate()) { + const { name } = prop.node.key.id; + privateNamesMap.set(name, { + id: prop.scope.generateUidIdentifier(name), + static: !!prop.node.static, + }); + } + } + return privateNamesMap; +} + +export function buildPrivateNamesNodes(privateNamesMap, loose, state) { + const initNodes = []; + + for (const [name, { id, static: isStatic }] of privateNamesMap) { + // In loose mode, both static and instance fields hare transpiled using a + // secret non-enumerable property. Hence, we also need to generate that + // key (using the classPrivateFieldLooseKey helper) in loose mode. + // In spec mode, only instance fields need a "private name" initializer + // (the WeakMap), becase static fields are directly assigned to a variable + // in the buildPrivateStaticFieldInitSpec function. + + if (loose) { + initNodes.push( + template.statement.ast` + var ${id} = ${state.addHelper("classPrivateFieldLooseKey")}("${name}") + `, + ); + } else if (!isStatic) { + initNodes.push(template.statement.ast`var ${id} = new WeakMap();`); + } + } + + return initNodes; +} + +// Traverses the class scope, handling private name references. If an inner +// class redeclares the same private name, it will hand off traversal to the +// restricted visitor (which doesn't traverse the inner class's inner scope). +const privateNameVisitor = { + PrivateName(path) { + const { privateNamesMap } = this; + const { node, parentPath } = path; + + if (!parentPath.isMemberExpression({ property: node })) return; + if (!privateNamesMap.has(node.id.name)) return; + + this.handle(parentPath); + }, + + Class(path) { + const { privateNamesMap } = this; + const body = path.get("body.body"); + + for (const prop of body) { + if (!prop.isClassPrivateProperty()) continue; + if (!privateNamesMap.has(prop.node.key.id.name)) continue; + + // This class redeclares the private name. + // So, we can only evaluate the things in the outer scope. + path.traverse(privateNameInnerVisitor, this); + path.skip(); + break; + } + }, +}; + +// Traverses the outer portion of a class, without touching the class's inner +// scope, for private names. +const privateNameInnerVisitor = traverse.visitors.merge([ + { + PrivateName: privateNameVisitor.PrivateName, + }, + environmentVisitor, +]); + +const privateNameHandlerSpec = { + memoise(member, count) { + const { scope } = member; + const { object } = member.node; + + const memo = scope.maybeGenerateMemoised(object); + if (!memo) { + return; + } + + this.memoiser.set(object, memo, count); + }, + + receiver(member) { + const { object } = member.node; + + if (this.memoiser.has(object)) { + return t.cloneNode(this.memoiser.get(object)); + } + + return t.cloneNode(object); + }, + + get(member) { + const { classRef, privateNamesMap, file } = this; + const { name } = member.node.property.id; + const { id, static: isStatic } = privateNamesMap.get(name); + + if (isStatic) { + return t.callExpression( + file.addHelper("classStaticPrivateFieldSpecGet"), + [this.receiver(member), t.cloneNode(classRef), t.cloneNode(id)], + ); + } else { + return t.callExpression(file.addHelper("classPrivateFieldGet"), [ + this.receiver(member), + t.cloneNode(id), + ]); + } + }, + + set(member, value) { + const { classRef, privateNamesMap, file } = this; + const { name } = member.node.property.id; + const { id, static: isStatic } = privateNamesMap.get(name); + + if (isStatic) { + return t.callExpression( + file.addHelper("classStaticPrivateFieldSpecSet"), + [this.receiver(member), t.cloneNode(classRef), t.cloneNode(id), value], + ); + } else { + return t.callExpression(file.addHelper("classPrivateFieldSet"), [ + this.receiver(member), + t.cloneNode(id), + value, + ]); + } + }, + + call(member, args) { + // The first access (the get) should do the memo assignment. + this.memoise(member, 1); + + return optimiseCall(this.get(member), this.receiver(member), args); + }, +}; + +const privateNameHandlerLoose = { + handle(member) { + const { privateNamesMap, file } = this; + const { object } = member.node; + const { name } = member.node.property.id; + + member.replaceWith( + template.expression`BASE(REF, PROP)[PROP]`({ + BASE: file.addHelper("classPrivateFieldLooseBase"), + REF: object, + PROP: privateNamesMap.get(name).id, + }), + ); + }, +}; + +export function transformPrivateNamesUsage( + ref, + path, + privateNamesMap, + loose, + state, +) { + const body = path.get("body"); + + if (loose) { + body.traverse(privateNameVisitor, { + privateNamesMap, + file: state, + ...privateNameHandlerLoose, + }); + } else { + memberExpressionToFunctions(body, privateNameVisitor, { + privateNamesMap, + classRef: ref, + file: state, + ...privateNameHandlerSpec, + }); + } +} + +function buildPrivateFieldInitLoose(ref, prop, privateNamesMap) { + const { id } = privateNamesMap.get(prop.node.key.id.name); + const value = prop.node.value || prop.scope.buildUndefinedNode(); + + return template.statement.ast` + Object.defineProperty(${ref}, ${id}, { + // configurable is false by default + // enumerable is false by default + writable: true, + value: ${value} + }); + `; +} + +function buildPrivateInstanceFieldInitSpec(ref, prop, privateNamesMap) { + const { id } = privateNamesMap.get(prop.node.key.id.name); + const value = prop.node.value || prop.scope.buildUndefinedNode(); + + return template.statement.ast`${id}.set(${ref}, { + // configurable is always false for private elements + // enumerable is always false for private elements + writable: true, + value: ${value}, + })`; +} + +function buildPrivateStaticFieldInitSpec(prop, privateNamesMap) { + const { id } = privateNamesMap.get(prop.node.key.id.name); + const value = prop.node.value || prop.scope.buildUndefinedNode(); + + return template.statement.ast` + var ${id} = { + // configurable is false by default + // enumerable is false by default + writable: true, + value: ${value} + }; + `; +} + +function buildPublicFieldInitLoose(ref, prop) { + const { key, computed } = prop.node; + const value = prop.node.value || prop.scope.buildUndefinedNode(); + + return t.expressionStatement( + t.assignmentExpression( + "=", + t.memberExpression(ref, key, computed || t.isLiteral(key)), + value, + ), + ); +} + +function buildPublicFieldInitSpec(ref, prop, state) { + const { key, computed } = prop.node; + const value = prop.node.value || prop.scope.buildUndefinedNode(); + + return t.expressionStatement( + t.callExpression(state.addHelper("defineProperty"), [ + ref, + computed || t.isLiteral(key) ? key : t.stringLiteral(key.name), + value, + ]), + ); +} + +export function buildFieldsInitNodes( + ref, + props, + privateNamesMap, + state, + loose, +) { + const staticNodes = []; + const instanceNodes = []; + + for (const prop of props) { + const isStatic = prop.node.static; + const isPrivate = prop.isPrivate(); + + // Pattern matching please + switch (true) { + case isStatic && isPrivate && loose: + staticNodes.push( + buildPrivateFieldInitLoose(t.cloneNode(ref), prop, privateNamesMap), + ); + break; + case isStatic && isPrivate && !loose: + staticNodes.push( + buildPrivateStaticFieldInitSpec(prop, privateNamesMap), + ); + break; + case isStatic && !isPrivate && loose: + staticNodes.push(buildPublicFieldInitLoose(t.cloneNode(ref), prop)); + break; + case isStatic && !isPrivate && !loose: + staticNodes.push( + buildPublicFieldInitSpec(t.cloneNode(ref), prop, state), + ); + break; + case !isStatic && isPrivate && loose: + instanceNodes.push( + buildPrivateFieldInitLoose(t.thisExpression(), prop, privateNamesMap), + ); + break; + case !isStatic && isPrivate && !loose: + instanceNodes.push( + buildPrivateInstanceFieldInitSpec( + t.thisExpression(), + prop, + privateNamesMap, + ), + ); + break; + case !isStatic && !isPrivate && loose: + instanceNodes.push(buildPublicFieldInitLoose(t.thisExpression(), prop)); + break; + case !isStatic && !isPrivate && !loose: + instanceNodes.push( + buildPublicFieldInitSpec(t.thisExpression(), prop, state), + ); + break; + default: + throw new Error("Unreachable."); + } + } + + return { staticNodes, instanceNodes }; +} diff --git a/packages/babel-plugin-class-features/src/index.js b/packages/babel-plugin-class-features/src/index.js new file mode 100644 index 000000000000..fefa3b41a185 --- /dev/null +++ b/packages/babel-plugin-class-features/src/index.js @@ -0,0 +1,208 @@ +import { declare } from "@babel/helper-plugin-utils"; +import nameFunction from "@babel/helper-function-name"; +import { types as t } from "@babel/core"; +import { + buildPrivateNamesNodes, + buildPrivateNamesMap, + transformPrivateNamesUsage, + buildFieldsInitNodes, +} from "./fields"; +import { injectInitialization, extractComputedKeys } from "./misc"; +import { + enableFeature, + verifyUsedFeatures, + FEATURES, + setLoose, + isLoose, +} from "./features"; + +import pkg from "../package.json"; + +export { enableFeature, FEATURES, setLoose }; + +// Note: Versions are represented as an integer. e.g. 7.1.5 is represented +// as 70000100005. This method is easier than using a semver-parsing +// package, but it breaks if we relese x.y.z where x, y or z are +// greater than 99_999. +const version = pkg.version.split(".").reduce((v, x) => v * 1e5 + +x, 0); +const versionKey = "@babel/plugin-class-features/version"; + +const getFeatureOptions = (options, name) => { + const value = options[name]; + + if (value === undefined || value === false) return { enabled: false }; + if (value === true) return { enabled: true, loose: false }; + + if (typeof value === "object") { + if ( + typeof value.loose !== "undefined" && + typeof value.loose !== "boolean" + ) { + throw new Error(`.${name}.loose must be a boolean or undefined.`); + } + + return { enabled: true, loose: !!value.loose }; + } + + throw new Error( + `.${name} must be a boolean, an object with a 'loose'` + + ` property or undefined.`, + ); +}; + +export default declare((api, options) => { + api.assertVersion(7); + + const fields = getFeatureOptions(options, "fields"); + const privateMethods = getFeatureOptions(options, "privateMethods"); + const decorators = getFeatureOptions(options, "decorators"); + + return { + name: "class-features", + + manipulateOptions(opts, parserOpts) { + if (fields) { + parserOpts.plugins.push("classProperties", "classPrivateProperties"); + } + }, + + pre() { + if (!this.file.get(versionKey) || this.file.get(versionKey) < version) { + this.file.set(versionKey, version); + } + + if (fields.enabled) { + enableFeature(this.file, FEATURES.fields, fields.loose); + } + if (privateMethods.enabled) { + throw new Error("Private methods are not supported yet"); + enableFeature(this.file, FEATURES.privateMethods); + } + if (decorators.enabled) { + throw new Error("Decorators are not supported yet"); + enableFeature(this.file, FEATURES.decorators); + } + }, + + visitor: { + Class(path, state) { + if (this.file.get(versionKey) !== version) return; + + verifyUsedFeatures(path, this.file); + + // Only fields are currently supported, this needs to be moved somewhere + // else when other features are added. + const loose = isLoose(this.file, FEATURES.fields); + + let constructor; + const props = []; + const computedPaths = []; + const privateNames = new Set(); + const body = path.get("body"); + + for (const path of body.get("body")) { + verifyUsedFeatures(path, this.file); + + if (path.node.computed) { + computedPaths.push(path); + } + + if (path.isClassPrivateProperty()) { + const { name } = path.node.key.id; + + if (privateNames.has(name)) { + throw path.buildCodeFrameError("Duplicate private field"); + } + privateNames.add(name); + } + + if (path.isProperty()) { + props.push(path); + } else if (path.isClassMethod({ kind: "constructor" })) { + constructor = path; + } + } + + if (!props.length) return; + + let ref; + + if (path.isClassExpression() || !path.node.id) { + nameFunction(path); + ref = path.scope.generateUidIdentifier("class"); + } else { + // path.isClassDeclaration() && path.node.id + ref = path.node.id; + } + + const keysNodes = extractComputedKeys( + ref, + path, + computedPaths, + this.file, + ); + + const privateNamesMap = buildPrivateNamesMap(props); + const privateNamesNodes = buildPrivateNamesNodes( + privateNamesMap, + loose, + state, + ); + + transformPrivateNamesUsage(ref, path, privateNamesMap, loose, state); + + const { staticNodes, instanceNodes } = buildFieldsInitNodes( + ref, + props, + privateNamesMap, + state, + loose, + ); + if (instanceNodes.length > 0) { + injectInitialization( + path, + constructor, + instanceNodes, + (referenceVisitor, state) => { + for (const prop of props) { + if (prop.node.static) continue; + prop.traverse(referenceVisitor, state); + } + }, + ); + } + + for (const prop of props) { + prop.remove(); + } + + if ( + keysNodes.length === 0 && + staticNodes.length === 0 && + privateNamesNodes.length === 0 + ) { + return; + } + + if (path.isClassExpression()) { + path.scope.push({ id: ref }); + path.replaceWith( + t.assignmentExpression("=", t.cloneNode(ref), path.node), + ); + } else if (!path.node.id) { + // Anonymous class declaration + path.node.id = ref; + } + + path.insertBefore(keysNodes); + path.insertAfter([...privateNamesNodes, ...staticNodes]); + }, + + PrivateName(path) { + if (this.file.get(versionKey) !== version) return; + + throw path.buildCodeFrameError(`Unknown PrivateName "${path}"`); + }, + }, + }; +}); diff --git a/packages/babel-plugin-class-features/src/misc.js b/packages/babel-plugin-class-features/src/misc.js new file mode 100644 index 000000000000..2468167fe940 --- /dev/null +++ b/packages/babel-plugin-class-features/src/misc.js @@ -0,0 +1,111 @@ +import { template, traverse, types as t } from "@babel/core"; +import { environmentVisitor } from "@babel/helper-replace-supers"; + +const findBareSupers = traverse.visitors.merge([ + { + Super(path) { + const { node, parentPath } = path; + if (parentPath.isCallExpression({ callee: node })) { + this.push(parentPath); + } + }, + }, + environmentVisitor, +]); + +const referenceVisitor = { + "TSTypeAnnotation|TypeAnnotation"(path) { + path.skip(); + }, + + ReferencedIdentifier(path) { + if (this.scope.hasOwnBinding(path.node.name)) { + this.scope.rename(path.node.name); + path.skip(); + } + }, +}; + +const classFieldDefinitionEvaluationTDZVisitor = traverse.visitors.merge([ + { + ReferencedIdentifier(path) { + if ( + this.classBinding && + this.classBinding === path.scope.getBinding(path.node.name) + ) { + const classNameTDZError = this.file.addHelper("classNameTDZError"); + const throwNode = t.callExpression(classNameTDZError, [ + t.stringLiteral(path.node.name), + ]); + + path.replaceWith(t.sequenceExpression([throwNode, path.node])); + path.skip(); + } + }, + }, + environmentVisitor, +]); + +export function injectInitialization(path, constructor, nodes, renamer) { + if (!nodes.length) return; + + const isDerived = !!path.node.superClass; + + if (!constructor) { + const newConstructor = t.classMethod( + "constructor", + t.identifier("constructor"), + [], + t.blockStatement([]), + ); + + if (isDerived) { + newConstructor.params = [t.restElement(t.identifier("args"))]; + newConstructor.body.body.push(template.statement.ast`super(...args)`); + } + + [constructor] = path.get("body").unshiftContainer("body", newConstructor); + } + + if (renamer) { + renamer(referenceVisitor, { scope: constructor.scope }); + } + + if (isDerived) { + const bareSupers = []; + constructor.traverse(findBareSupers, bareSupers); + for (const bareSuper of bareSupers) { + bareSuper.insertAfter(nodes); + } + } else { + constructor.get("body").unshiftContainer("body", nodes); + } +} + +export function extractComputedKeys(ref, path, computedPaths, file) { + const declarations = []; + + for (const computedPath of computedPaths) { + computedPath.traverse(classFieldDefinitionEvaluationTDZVisitor, { + classBinding: path.node.id && path.scope.getBinding(path.node.id.name), + file, + }); + + const computedNode = computedPath.node; + // Make sure computed property names are only evaluated once (upon class definition) + // and in the right order in combination with static properties + if (!computedPath.get("key").isConstantExpression()) { + const ident = path.scope.generateUidIdentifierBasedOnNode( + computedNode.key, + ); + declarations.push( + t.variableDeclaration("var", [ + t.variableDeclarator(ident, computedNode.key), + ]), + ); + computedNode.key = t.cloneNode(ident); + } + } + + return declarations; +} diff --git a/packages/babel-plugin-class-features/test/fixtures/plugin-proposal-class-properties/loose-not-overwritten/input.js b/packages/babel-plugin-class-features/test/fixtures/plugin-proposal-class-properties/loose-not-overwritten/input.js new file mode 100644 index 000000000000..eb0eb350654f --- /dev/null +++ b/packages/babel-plugin-class-features/test/fixtures/plugin-proposal-class-properties/loose-not-overwritten/input.js @@ -0,0 +1,3 @@ +class A { + foo; +} diff --git a/packages/babel-plugin-class-features/test/fixtures/plugin-proposal-class-properties/loose-not-overwritten/options.json b/packages/babel-plugin-class-features/test/fixtures/plugin-proposal-class-properties/loose-not-overwritten/options.json new file mode 100644 index 000000000000..60fe9fc3ffdf --- /dev/null +++ b/packages/babel-plugin-class-features/test/fixtures/plugin-proposal-class-properties/loose-not-overwritten/options.json @@ -0,0 +1,6 @@ +{ + "plugins": [ + ["proposal-class-properties", { "loose": true }, "name 1"], + ["proposal-class-properties", { "loose": false }, "name 2"] + ] +} diff --git a/packages/babel-plugin-class-features/test/fixtures/plugin-proposal-class-properties/loose-not-overwritten/output.js b/packages/babel-plugin-class-features/test/fixtures/plugin-proposal-class-properties/loose-not-overwritten/output.js new file mode 100644 index 000000000000..063a057f52c0 --- /dev/null +++ b/packages/babel-plugin-class-features/test/fixtures/plugin-proposal-class-properties/loose-not-overwritten/output.js @@ -0,0 +1,6 @@ +class A { + constructor() { + this.foo = void 0; + } + +} diff --git a/packages/babel-plugin-class-features/test/index.js b/packages/babel-plugin-class-features/test/index.js new file mode 100644 index 000000000000..1b534b8fc64a --- /dev/null +++ b/packages/babel-plugin-class-features/test/index.js @@ -0,0 +1,3 @@ +import runner from "@babel/helper-plugin-test-runner"; + +runner(__dirname); diff --git a/packages/babel-plugin-proposal-class-properties/package.json b/packages/babel-plugin-proposal-class-properties/package.json index 02d1fe8f257e..876711b7c0e0 100644 --- a/packages/babel-plugin-proposal-class-properties/package.json +++ b/packages/babel-plugin-proposal-class-properties/package.json @@ -12,12 +12,8 @@ "babel-plugin" ], "dependencies": { - "@babel/helper-function-name": "^7.1.0", - "@babel/helper-member-expression-to-functions": "^7.0.0", - "@babel/helper-optimise-call-expression": "^7.0.0", "@babel/helper-plugin-utils": "^7.0.0", - "@babel/helper-replace-supers": "^7.1.0", - "@babel/plugin-syntax-class-properties": "^7.0.0" + "@babel/plugin-class-features": "^7.1.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" diff --git a/packages/babel-plugin-proposal-class-properties/src/index.js b/packages/babel-plugin-proposal-class-properties/src/index.js index 247fce7609a5..96b66020a1ef 100644 --- a/packages/babel-plugin-proposal-class-properties/src/index.js +++ b/packages/babel-plugin-proposal-class-properties/src/index.js @@ -1,525 +1,25 @@ import { declare } from "@babel/helper-plugin-utils"; -import nameFunction from "@babel/helper-function-name"; -import syntaxClassProperties from "@babel/plugin-syntax-class-properties"; -import { template, traverse, types as t } from "@babel/core"; -import { environmentVisitor } from "@babel/helper-replace-supers"; -import memberExpressionToFunctions from "@babel/helper-member-expression-to-functions"; -import optimiseCall from "@babel/helper-optimise-call-expression"; +import pluginClassFeatures, { + enableFeature, + FEATURES, +} from "@babel/plugin-class-features"; export default declare((api, options) => { api.assertVersion(7); const { loose } = options; - const findBareSupers = traverse.visitors.merge([ - { - Super(path) { - const { node, parentPath } = path; - if (parentPath.isCallExpression({ callee: node })) { - this.push(parentPath); - } - }, - }, - environmentVisitor, - ]); - - const referenceVisitor = { - "TSTypeAnnotation|TypeAnnotation"(path) { - path.skip(); - }, - - ReferencedIdentifier(path) { - if (this.scope.hasOwnBinding(path.node.name)) { - this.scope.rename(path.node.name); - path.skip(); - } - }, - }; - - const classFieldDefinitionEvaluationTDZVisitor = traverse.visitors.merge([ - { - ReferencedIdentifier(path) { - if ( - this.classBinding && - this.classBinding === path.scope.getBinding(path.node.name) - ) { - const classNameTDZError = this.file.addHelper("classNameTDZError"); - const throwNode = t.callExpression(classNameTDZError, [ - t.stringLiteral(path.node.name), - ]); - - path.replaceWith(t.sequenceExpression([throwNode, path.node])); - path.skip(); - } - }, - }, - environmentVisitor, - ]); - - // Traverses the class scope, handling private name references. If an inner - // class redeclares the same private name, it will hand off traversal to the - // restricted visitor (which doesn't traverse the inner class's inner scope). - const privateNameVisitor = { - PrivateName(path) { - const { name } = this; - const { node, parentPath } = path; - - if (!parentPath.isMemberExpression({ property: node })) return; - if (node.id.name !== name) return; - this.handle(parentPath); - }, - - Class(path) { - const { name } = this; - const body = path.get("body.body"); - - for (const prop of body) { - if (!prop.isClassPrivateProperty()) continue; - if (prop.node.key.id.name !== name) continue; - - // This class redeclares the private name. - // So, we can only evaluate the things in the outer scope. - path.traverse(privateNameInnerVisitor, this); - path.skip(); - break; - } - }, - }; - - // Traverses the outer portion of a class, without touching the class's inner - // scope, for private names. - const privateNameInnerVisitor = traverse.visitors.merge([ - { - PrivateName: privateNameVisitor.PrivateName, - }, - environmentVisitor, - ]); - - const privateNameHandlerSpec = { - memoise(member, count) { - const { scope } = member; - const { object } = member.node; - - const memo = scope.maybeGenerateMemoised(object); - if (!memo) { - return; - } - - this.memoiser.set(object, memo, count); - }, - - receiver(member) { - const { object } = member.node; - - if (this.memoiser.has(object)) { - return t.cloneNode(this.memoiser.get(object)); - } - - return t.cloneNode(object); - }, - - get(member) { - const { map, file } = this; - - return t.callExpression(file.addHelper("classPrivateFieldGet"), [ - this.receiver(member), - t.cloneNode(map), - ]); - }, - - set(member, value) { - const { map, file } = this; - - return t.callExpression(file.addHelper("classPrivateFieldSet"), [ - this.receiver(member), - t.cloneNode(map), - value, - ]); - }, - - call(member, args) { - // The first access (the get) should do the memo assignment. - this.memoise(member, 1); - - return optimiseCall(this.get(member), this.receiver(member), args); - }, - }; - - const privateNameHandlerLoose = { - handle(member) { - const { prop, file } = this; - const { object } = member.node; - - member.replaceWith( - template.expression`BASE(REF, PROP)[PROP]`({ - BASE: file.addHelper("classPrivateFieldLooseBase"), - REF: object, - PROP: prop, - }), - ); - }, - }; - - const staticPrivatePropertyHandlerSpec = { - ...privateNameHandlerSpec, - - get(member) { - const { file, privateId, classRef } = this; - - return t.callExpression( - file.addHelper("classStaticPrivateFieldSpecGet"), - [this.receiver(member), t.cloneNode(classRef), t.cloneNode(privateId)], - ); - }, - - set(member, value) { - const { file, privateId, classRef } = this; - - return t.callExpression( - file.addHelper("classStaticPrivateFieldSpecSet"), - [ - this.receiver(member), - t.cloneNode(classRef), - t.cloneNode(privateId), - value, - ], - ); - }, - }; - - function buildClassPropertySpec(ref, path, state) { - const { scope } = path; - const { key, value, computed } = path.node; - return t.expressionStatement( - t.callExpression(state.addHelper("defineProperty"), [ - ref, - computed || t.isLiteral(key) ? key : t.stringLiteral(key.name), - value || scope.buildUndefinedNode(), - ]), - ); - } - - function buildClassPropertyLoose(ref, path) { - const { scope } = path; - const { key, value, computed } = path.node; - return t.expressionStatement( - t.assignmentExpression( - "=", - t.memberExpression(ref, key, computed || t.isLiteral(key)), - value || scope.buildUndefinedNode(), - ), - ); - } - - function buildClassPrivatePropertySpec(ref, path, initNodes, state) { - const { parentPath, scope } = path; - const { name } = path.node.key.id; - - const map = scope.generateUidIdentifier(name); - memberExpressionToFunctions(parentPath, privateNameVisitor, { - name, - map, - file: state, - ...privateNameHandlerSpec, - }); - - initNodes.push( - template.statement`var MAP = new WeakMap();`({ - MAP: map, - }), - ); - - // Must be late evaluated in case it references another private field. - return () => - template.statement` - MAP.set(REF, { - // configurable is always false for private elements - // enumerable is always false for private elements - writable: true, - value: VALUE - }); - `({ - MAP: map, - REF: ref, - VALUE: path.node.value || scope.buildUndefinedNode(), - }); - } - - function buildClassPrivatePropertyLooseHelper(ref, path, state) { - const { parentPath, scope } = path; - const { name } = path.node.key.id; - - const prop = scope.generateUidIdentifier(name); - - parentPath.traverse(privateNameVisitor, { - name, - prop, - file: state, - ...privateNameHandlerLoose, - }); - - return { - keyDecl: template.statement`var PROP = HELPER(NAME);`({ - PROP: prop, - HELPER: state.addHelper("classPrivateFieldLooseKey"), - NAME: t.stringLiteral(name), - }), - // Must be late evaluated in case it references another private field. - buildInit: () => - template.statement.ast` - Object.defineProperty(${ref}, ${prop}, { - // configurable is false by default - // enumerable is false by default - writable: true, - value: ${path.node.value || scope.buildUndefinedNode()} - }); - `, - }; - } - - function buildClassInstancePrivatePropertyLoose(ref, path, initNodes, state) { - const { keyDecl, buildInit } = buildClassPrivatePropertyLooseHelper( - ref, - path, - state, - ); - - initNodes.push(keyDecl); - return buildInit; - } - - function buildClassStaticPrivatePropertyLoose(ref, path, state) { - const { keyDecl, buildInit } = buildClassPrivatePropertyLooseHelper( - ref, - path, - state, - ); - - return [keyDecl, buildInit()]; - } - - function buildClassStaticPrivatePropertySpec(ref, path, state) { - const { parentPath, scope } = path; - const { name } = path.node.key.id; - - const privateId = scope.generateUidIdentifier(name); - memberExpressionToFunctions(parentPath, privateNameVisitor, { - name, - privateId, - classRef: ref, - file: state, - ...staticPrivatePropertyHandlerSpec, - }); - - return [ - template.statement.ast` - var ${privateId} = { - // configurable is always false for private elements - // enumerable is always false for private elements - writable: true, - value: ${path.node.value || scope.buildUndefinedNode()} - } - `, - ]; - } - - const buildClassProperty = loose - ? buildClassPropertyLoose - : buildClassPropertySpec; - - const buildClassPrivateProperty = loose - ? buildClassInstancePrivatePropertyLoose - : buildClassPrivatePropertySpec; - - const buildClassStaticPrivateProperty = loose - ? buildClassStaticPrivatePropertyLoose - : buildClassStaticPrivatePropertySpec; - return { name: "proposal-class-properties", - inherits: syntaxClassProperties, - - visitor: { - Class(path, state) { - const isDerived = !!path.node.superClass; - let constructor; - const props = []; - const computedPaths = []; - const privateNames = new Set(); - const body = path.get("body"); - - for (const path of body.get("body")) { - const { computed, decorators } = path.node; - if (computed) { - computedPaths.push(path); - } - if (decorators && decorators.length > 0) { - throw path.buildCodeFrameError( - "Decorators transform is necessary.", - ); - } - - if (path.isClassPrivateProperty()) { - const { - key: { - id: { name }, - }, - } = path.node; - - if (privateNames.has(name)) { - throw path.buildCodeFrameError("Duplicate private field"); - } - privateNames.add(name); - } - - if (path.isProperty()) { - props.push(path); - } else if (path.isClassMethod({ kind: "constructor" })) { - constructor = path; - } - } - - if (!props.length) return; - - let ref; - if (path.isClassExpression() || !path.node.id) { - nameFunction(path); - ref = path.scope.generateUidIdentifier("class"); - } else { - // path.isClassDeclaration() && path.node.id - ref = path.node.id; - } - - const computedNodes = []; - const staticNodes = []; - const instanceBody = []; - for (const computedPath of computedPaths) { - computedPath.traverse(classFieldDefinitionEvaluationTDZVisitor, { - classBinding: - path.node.id && path.scope.getBinding(path.node.id.name), - file: this.file, - }); + inherits: pluginClassFeatures, - const computedNode = computedPath.node; - // Make sure computed property names are only evaluated once (upon class definition) - // and in the right order in combination with static properties - if (!computedPath.get("key").isConstantExpression()) { - const ident = path.scope.generateUidIdentifierBasedOnNode( - computedNode.key, - ); - computedNodes.push( - t.variableDeclaration("var", [ - t.variableDeclarator(ident, computedNode.key), - ]), - ); - computedNode.key = t.cloneNode(ident); - } - } - - // Transform private props before publics. - const privateMaps = []; - const privateMapInits = []; - for (const prop of props) { - if (prop.isPrivate() && !prop.node.static) { - const inits = []; - privateMapInits.push(inits); - - privateMaps.push( - buildClassPrivateProperty(t.thisExpression(), prop, inits, state), - ); - } - } - let p = 0; - for (const prop of props) { - if (prop.node.static) { - if (prop.isPrivate()) { - staticNodes.push( - ...buildClassStaticPrivateProperty( - t.cloneNode(ref), - prop, - state, - ), - ); - } else { - staticNodes.push( - buildClassProperty(t.cloneNode(ref), prop, state), - ); - } - } else if (prop.isPrivate()) { - instanceBody.push(privateMaps[p]()); - staticNodes.push(...privateMapInits[p]); - p++; - } else { - instanceBody.push( - buildClassProperty(t.thisExpression(), prop, state), - ); - } - } - - if (instanceBody.length) { - if (!constructor) { - const newConstructor = t.classMethod( - "constructor", - t.identifier("constructor"), - [], - t.blockStatement([]), - ); - if (isDerived) { - newConstructor.params = [t.restElement(t.identifier("args"))]; - newConstructor.body.body.push( - t.expressionStatement( - t.callExpression(t.super(), [ - t.spreadElement(t.identifier("args")), - ]), - ), - ); - } - [constructor] = body.unshiftContainer("body", newConstructor); - } - - const state = { scope: constructor.scope }; - for (const prop of props) { - if (prop.node.static) continue; - prop.traverse(referenceVisitor, state); - } - - // - - if (isDerived) { - const bareSupers = []; - constructor.traverse(findBareSupers, bareSupers); - for (const bareSuper of bareSupers) { - bareSuper.insertAfter(instanceBody); - } - } else { - constructor.get("body").unshiftContainer("body", instanceBody); - } - } - - for (const prop of props) { - prop.remove(); - } - - if (computedNodes.length === 0 && staticNodes.length === 0) return; - - if (path.isClassExpression()) { - path.scope.push({ id: ref }); - path.replaceWith( - t.assignmentExpression("=", t.cloneNode(ref), path.node), - ); - } else if (!path.node.id) { - // Anonymous class declaration - path.node.id = ref; - } - - path.insertBefore(computedNodes); - path.insertAfter(staticNodes); - }, + manipulateOptions(opts, parserOpts) { + parserOpts.plugins.push("classProperties", "classPrivateProperties"); + }, - PrivateName(path) { - throw path.buildCodeFrameError(`Unknown PrivateName "${path}"`); - }, + pre() { + enableFeature(this.file, FEATURES.fields, loose); }, }; }); diff --git a/packages/babel-plugin-proposal-class-properties/test/fixtures/private-loose/native-classes/output.js b/packages/babel-plugin-proposal-class-properties/test/fixtures/private-loose/native-classes/output.js index 33bfbfeff1b6..fb965472f6d7 100644 --- a/packages/babel-plugin-proposal-class-properties/test/fixtures/private-loose/native-classes/output.js +++ b/packages/babel-plugin-proposal-class-properties/test/fixtures/private-loose/native-classes/output.js @@ -18,13 +18,12 @@ class Foo { var _foo = babelHelpers.classPrivateFieldLooseKey("foo"); +var _bar = babelHelpers.classPrivateFieldLooseKey("bar"); + Object.defineProperty(Foo, _foo, { writable: true, value: "foo" }); - -var _bar = babelHelpers.classPrivateFieldLooseKey("bar"); - var f = new Foo(); expect("foo" in Foo).toBe(false); expect("bar" in f).toBe(false); diff --git a/packages/babel-plugin-proposal-class-properties/test/fixtures/private/native-classes/output.js b/packages/babel-plugin-proposal-class-properties/test/fixtures/private/native-classes/output.js index 72ca92a92bbf..fc70fa14fb8a 100644 --- a/packages/babel-plugin-proposal-class-properties/test/fixtures/private/native-classes/output.js +++ b/packages/babel-plugin-proposal-class-properties/test/fixtures/private/native-classes/output.js @@ -16,9 +16,9 @@ class Foo { } +var _bar = new WeakMap(); + var _foo = { writable: true, value: "foo" }; - -var _bar = new WeakMap();