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"; 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, }), ); }, }; 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 buildClassPrivatePropertyLoose(ref, path, initNodes, state) { const { parentPath, scope } = path; const { name } = path.node.key.id; const prop = scope.generateUidIdentifier(name); parentPath.traverse(privateNameVisitor, { name, prop, file: state, ...privateNameHandlerLoose, }); initNodes.push( 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. return () => template.statement` Object.defineProperty(REF, PROP, { // configurable is false by default // enumerable is false by default writable: true, value: VALUE }); `({ REF: ref, PROP: prop, VALUE: path.node.value || scope.buildUndefinedNode(), }); } const buildClassProperty = loose ? buildClassPropertyLoose : buildClassPropertySpec; const buildClassPrivateProperty = loose ? buildClassPrivatePropertyLoose : buildClassPrivatePropertySpec; return { 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 { static: isStatic, key: { id: { name }, }, } = path.node; if (isStatic) { throw path.buildCodeFrameError( "Static class fields are not spec'ed yet.", ); } 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, }); 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()) { 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) { 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); }, PrivateName(path) { throw path.buildCodeFrameError(`Unknown PrivateName "${path}"`); }, }, }; });