UNPKG

babel-plugin-transform-modules-ui5

Version:
375 lines (357 loc) 22.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.convertClassToUI5Extend = convertClassToUI5Extend; exports.getClassInfo = getClassInfo; exports.getTypeName = void 0; var _core = require("@babel/core"); var _path = _interopRequireDefault(require("path")); var _objectAssignDefined = _interopRequireDefault(require("object-assign-defined")); var th = _interopRequireWildcard(require("../../utils/templates")); var ast = _interopRequireWildcard(require("../../utils/ast")); var _jsdoc = require("./jsdoc"); var _decorators = require("./decorators"); var _imports = require("./imports"); function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); } function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; } function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; } function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } /** * Converts an ES6 class to a UI5 extend. * Any static methods or properties will be moved outside the class body. * The path will be updated with the new AST. */ function convertClassToUI5Extend(path, node, classInfo, extraStaticProps, importDeclarationPaths, opts) { if (!(_core.types.isClassDeclaration(node) || _core.types.isClassExpression(node))) { return node; } const CONTROLLER_EXTENSION_TAG = "transformControllerExtension"; const staticMembers = []; const classNameIdentifier = node.id; const className = classNameIdentifier.name; const superClass = node.superClass; // Identifier node. const superClassName = superClass.name; const isController = className.includes("Controller") || !!classInfo.controller; const moveControllerConstructorToOnInit = isController && !!opts.moveControllerConstructorToOnInit; const moveControllerPropsToOnInit = isController && (!!opts.moveControllerPropsToOnInit || !!opts.moveControllerConstructorToOnInit); const moveStaticStaticPropsToExtend = isController && !!opts.addControllerStaticPropsToExtend; const alwaysMoveInstanceProps = !opts.onlyMoveClassPropsUsingThis; const extendProps = []; const boundProps = []; const CONSTRUCTOR = "constructor"; const propsByName = {}; let constructor; let constructorComments; const staticPropsToAdd = moveStaticStaticPropsToExtend ? Object.keys(extraStaticProps) : ["metadata", "renderer", "overrides"]; for (const propName of staticPropsToAdd) { if (extraStaticProps[propName]) { extendProps.push(_core.types.objectProperty(_core.types.identifier(propName), extraStaticProps[propName])); } } for (const memberPath of path.get("body.body")) { const member = memberPath.node; const memberName = member.key.name; if (_core.types.isClassMethod(member)) { var _member$params; const isConstructor = member.kind === "constructor"; const membersToAssign = []; const params = isConstructor ? (_member$params = member.params) === null || _member$params === void 0 ? void 0 : _member$params.map(param => { // handling of parameter properties for constructors (TypeScript): // https://www.typescriptlang.org/docs/handbook/2/classes.html#parameter-properties // -> extracting the real parameters and store the members to assign if (param.type === "TSParameterProperty") { membersToAssign.push(param.parameter); return param.parameter; } return param; }) : member.params; const func = _core.types.functionExpression(member.key, params, member.body, member.generator, member.async); if (isConstructor && membersToAssign.length > 0) { // handling of parameter properties for constructors (TypeScript): // -> assigning parameter properties as members to the instance const newMembers = membersToAssign.map(member => buildMemberAssignmentStatement(_core.types.thisExpression(), { key: member, computed: false, value: member })); const superIndex = member.body.body.findIndex(node => ast.isSuperCallExpression(node.expression) || ast.isSuperPrototypeCallOf(node.expression, superClassName, "constructor")); member.body.body.splice(superIndex === -1 ? member.body.body.length : superIndex + 1, 0, ...newMembers); } if (member.static) { staticMembers.push(buildMemberAssignmentStatement(classNameIdentifier, _objectSpread(_objectSpread({}, member), {}, { value: func }))); } else { propsByName[memberName] = func; if (member.kind === "get" || member.kind === "set") { extendProps.push(_core.types.objectMethod(member.kind, member.key, member.params, member.body, member.computed)); } else { // method if (memberName === CONSTRUCTOR) { constructorComments = member.leadingComments; constructor = func; if (moveControllerPropsToOnInit) { continue; // don't push to props yet } } func.id = path.scope.generateUidIdentifier(func.id.name); // Give the function a unique name extendProps.push(buildObjectProperty(_objectSpread(_objectSpread({}, member), {}, { value: func }))); } } } else if (_core.types.isClassProperty(member)) { var _member$leadingCommen, _member$decorators; // For class properties annotated to represent controller extensions, replace the pure declaration with an assignment (that's what the runtime expects) // and keep them at the initialization object as properties (don't move into constructor). if ((_member$leadingCommen = member.leadingComments) !== null && _member$leadingCommen !== void 0 && _member$leadingCommen.some(comment => { return comment.value.includes("@" + CONTROLLER_EXTENSION_TAG); }) || (_member$decorators = member.decorators) !== null && _member$decorators !== void 0 && _member$decorators.some(decorator => { var _decorator$expression; return ((_decorator$expression = decorator.expression) === null || _decorator$expression === void 0 ? void 0 : _decorator$expression.name) === CONTROLLER_EXTENSION_TAG; })) { var _member$typeAnnotatio; const typeAnnotation = (_member$typeAnnotatio = member.typeAnnotation) === null || _member$typeAnnotatio === void 0 ? void 0 : _member$typeAnnotatio.typeAnnotation; // double-check that it is a valid node for a controller extension if (_core.types.isTSTypeReference(typeAnnotation) || _core.types.isTSQualifiedName(typeAnnotation)) { var _memberPath$hub; const typeName = getTypeName(typeAnnotation); // 1. transform the property from being typed as instance and un-initialized to a property where the controller extension *class* is assigned as value const valueIdentifier = _core.types.identifier(typeName); member.value = valueIdentifier; member.typeAnnotation = null; extendProps.unshift(buildObjectProperty(member)); // add it to the properties of the extend() config object // 2. add a binding reference to the value, so in case the TS transpiler runs later it recognizes that the import is still needed const typeNameFirstPart = typeName.split(".")[0]; // e.g. when "myExtension: someBundle.MyExtension" if (memberPath.scope.hasBinding(typeNameFirstPart)) { const binding = path.scope.getBinding(typeNameFirstPart); binding.referencePaths.push(memberPath.get("value")); } // 3. restore the import in case it was run already and removed the import const neededImportDeclaration = (0, _imports.getImportDeclaration)(memberPath === null || memberPath === void 0 || (_memberPath$hub = memberPath.hub) === null || _memberPath$hub === void 0 || (_memberPath$hub = _memberPath$hub.file) === null || _memberPath$hub === void 0 || (_memberPath$hub = _memberPath$hub.opts) === null || _memberPath$hub === void 0 ? void 0 : _memberPath$hub.filename, typeName); if (!importDeclarationPaths.some(path => path.node === neededImportDeclaration)) { // TODO: import might be there but with the specifier removed; we can clone, but then other specifiers are duplicate // if import is no longer there, re-add it importDeclarationPaths[importDeclarationPaths.length - 1].insertAfter(neededImportDeclaration); } // 4. prevent the member from also being added to the constructor (member does have a value now and initializer would be added below) continue; } } if (!member.value) continue; // remove all other un-initialized static class props (typescript) // Now handle the case where an extended controller extension - not just e.g. "Routing", but // "Routing.override({...})" - is assigned to the class property. To make this work in TypeScript code // (Routing.override({...}) returns a class, but the "routing" member property needs to be typed as an instance), // we require the following notation, using a dummy function ControllerExtension.use() that does actually // not exist at runtime, but does the required class-to-instance conversion from TypeScript perspective: // routing: ControllerExtension.use(Routing.override({...})) // In this case, the code in the "if" clause above was not executed, regardless of the presence of the // @transformControllerExtension marker, because it is not a type assignment. In the resulting code, the // "ControllerExtension.use(...)" part should be removed and the content of the brackets should be assigned // directly to the member property. const rightSide = member.value; if (isCallToControllerExtensionUse(rightSide, memberPath)) { member.value = rightSide.arguments[0]; extendProps.unshift(buildObjectProperty(member)); // add it to the properties of the extend() config object continue; // prevent the member from also being added to the constructor } // code instrumentation sometimes wraps ControllerExtension.use() calls like: // this.routing = (cov_1uvvg22e7l().s[5]++, ControllerExtension.use(Routing.override({ … }))); if (_core.types.isSequenceExpression(rightSide) && isCallToControllerExtensionUse(rightSide.expressions[rightSide.expressions.length - 1], memberPath)) { rightSide.expressions[rightSide.expressions.length - 1] = rightSide.expressions[rightSide.expressions.length - 1].arguments[0]; member.value = rightSide; extendProps.unshift(buildObjectProperty(member)); // add it to the properties of the extend() config object continue; // prevent the member from also being added to the constructor } // Special handling for TypeScript limitation where metadata, renderer and overrides must be properties. if (["metadata", "renderer", "overrides"].includes(memberName)) { if (opts.overridesToOverride && member.key.name === "overrides") { member.key.name = "override"; } extendProps.unshift(buildObjectProperty(member)); } else if (member.static) { if (moveStaticStaticPropsToExtend) { extendProps.unshift(buildObjectProperty(member)); } else { staticMembers.push(buildMemberAssignmentStatement(classNameIdentifier, member)); } } else { propsByName[memberName] = member.value; if (memberName === "constructor") { constructorComments = member.leadingComments; constructor = member.value; if (moveControllerPropsToOnInit) { continue; // don't push to props yet } } if (alwaysMoveInstanceProps || _core.types.isArrowFunctionExpression(member.value) || ast.isThisExpressionUsed(member.value)) { boundProps.push(member); } else { extendProps.push(buildObjectProperty(member)); } } } } /** * Checks whether the given thing is a CallExpression that calls ControllerExtension.use(...) * * @param {*} expression the thing to check - does not need to be a CallExpression * @param {string} memberPath * @returns true if the given expression is a CallExpression that calls ControllerExtension.use(...) */ function isCallToControllerExtensionUse(expression, memberPath) { if (!_core.types.isCallExpression(expression)) { return false; } const callee = expression.callee; if (_core.types.isMemberExpression(callee) && _core.types.isIdentifier(callee.object) && _core.types.isIdentifier(callee.property) && callee.property.name === "use" // we are looking for "ControllerExtension.use(...)" ) { var _memberPath$hub2, _callee$object, _importDeclaration$so; const importDeclaration = (0, _imports.getImportDeclaration)(memberPath === null || memberPath === void 0 || (_memberPath$hub2 = memberPath.hub) === null || _memberPath$hub2 === void 0 || (_memberPath$hub2 = _memberPath$hub2.file) === null || _memberPath$hub2 === void 0 || (_memberPath$hub2 = _memberPath$hub2.opts) === null || _memberPath$hub2 === void 0 ? void 0 : _memberPath$hub2.filename, callee === null || callee === void 0 || (_callee$object = callee.object) === null || _callee$object === void 0 ? void 0 : _callee$object.name // usually, but not necessarily always: "ControllerExtension"... ); // ...hence we rather look at the imported module name to be sure if ((importDeclaration === null || importDeclaration === void 0 || (_importDeclaration$so = importDeclaration.source) === null || _importDeclaration$so === void 0 ? void 0 : _importDeclaration$so.value) === "sap/ui/core/mvc/ControllerExtension") { if (!expression.arguments || expression.arguments.length !== 1) { // exactly one argument must be there throw memberPath.buildCodeFrameError(`ControllerExtension.use() must be called with exactly one argument but has ${expression.arguments ? expression.arguments.length : 0}`); } return true; } } return false; // return false if not a match } // Arrow function properties need to get moved to the constructor so that // they're bound properly to the class instance, to align with the spec. // For controllers, use onInit rather than constructor, since controller constructors don't work. // Also move the constructor's statements to the onInit. const bindToConstructor = !moveControllerPropsToOnInit; const bindToMethodName = moveControllerPropsToOnInit ? "onInit" : "constructor"; // avoid getting a prop named constructor as it may return {}'s let bindMethod = moveControllerPropsToOnInit ? propsByName[bindToMethodName] : constructor; const constructorJsdoc = (0, _jsdoc.getTags)(constructorComments); const keepConstructor = !moveControllerConstructorToOnInit || classInfo.keepConstructor || constructorJsdoc.keep; // See if we need either constructor or onInit const needsBindingMethod = boundProps.length || moveControllerPropsToOnInit && constructor && !keepConstructor; // See if we need to create a new 'constructor' or 'onInit' method, depending which one we'll bind to. if (needsBindingMethod && !bindMethod) { const bindToId = _core.types.identifier(bindToMethodName); const bindMethodDeclaration = bindToConstructor ? th.buildInheritingConstructor({ SUPER: _core.types.identifier(superClassName) }) : th.buildInheritingFunction({ NAME: bindToId, SUPER: _core.types.identifier(superClassName) }); bindMethod = ast.convertFunctionDeclarationToExpression(bindMethodDeclaration); extendProps.unshift(_core.types.objectProperty(bindToId, bindMethod)); } if (constructor && moveControllerPropsToOnInit) { if (keepConstructor) { extendProps.unshift(_core.types.objectProperty(_core.types.identifier(CONSTRUCTOR), constructor)); } else { // Copy all except the super call from the constructor to the bindMethod (i.e. onInit) bindMethod.body.body.unshift(...constructor.body.body.filter(node => !(ast.isSuperCallExpression(node.expression) || ast.isSuperPrototypeCallOf(node.expression, superClassName, "constructor")))); } } if (boundProps.length) { // We need to inject the bound props into the bind method (constructor or onInit), // but not until after the super call (if applicable) const mappedProps = boundProps.map(member => buildThisMemberAssignmentStatement(member)); const superIndex = bindMethod.body.body.findIndex(node => ast.isSuperCallExpression(node.expression) || ast.isSuperPrototypeCallOf(node.expression, superClassName, bindToMethodName)); if (superIndex === -1) { // If there's no super, just add the bound props at the start bindMethod.body.body.unshift(...mappedProps); } else { const upToSuper = bindMethod.body.body.slice(0, superIndex + 1); const afterSuper = bindMethod.body.body.slice(superIndex + 1); bindMethod.body.body = [...upToSuper, ...mappedProps, ...afterSuper]; } } const extendAssign = th.buildExtendAssign({ NAME: classNameIdentifier, SUPER: superClass, // Needs Identifier node FQN: _core.types.stringLiteral(getFullyQualifiedName(classInfo)), OBJECT: _core.types.objectExpression(extendProps) }); return [extendAssign, ...staticMembers]; } function getFullyQualifiedName(classInfo) { if (classInfo.alias) return classInfo.alias; if (classInfo.name) return classInfo.name; const namespace = classInfo.namespace || classInfo.fileNamespace; const separator = namespace ? "." : ""; return `${namespace}${separator}${classInfo.localName}`; } function getClassInfo(path, node, parent, pluginOpts) { const defaults = { localName: node.id.name, superClassName: node.superClass && node.superClass.name, fileNamespace: getFileBaseNamespace(path, pluginOpts) || "" }; const decoratorInfo = (0, _decorators.getDecoratorClassInfo)(node); const jsDocInfo = (0, _jsdoc.getJsDocClassInfo)(node, parent); // like Object.assign, but ignoring undefined values. return (0, _objectAssignDefined.default)(defaults, decoratorInfo, jsDocInfo); } /** * Reads the namespace from the file path (but not the name). */ function getFileBaseNamespace(path, pluginOpts) { const opts = path.hub.file.opts; const filename = _path.default.resolve(opts.filename); const sourceRoot = opts.sourceRoot ? _path.default.resolve(process.cwd(), opts.sourceRoot) : process.cwd(); if (filename.startsWith(sourceRoot)) { const filenameRelative = _path.default.relative(sourceRoot, filename); const { dir } = _path.default.parse(filenameRelative); const namespaceParts = dir.split(_path.default.sep); if (pluginOpts.namespacePrefix) { namespaceParts.unshift(pluginOpts.namespacePrefix); } return namespaceParts.join("."); } else { return undefined; } } const getQualifiedName = node => { let { left, right } = node; // if left is TSQualifiedName, recursive call to get full namespace if (_core.types.isTSQualifiedName(left)) { left = getQualifiedName(left); } else { // if left is an Identifier left = left.name; } return `${left}.${right.name}`; }; const getTypeName = typeAnnotation => { if (_core.types.isTSTypeReference(typeAnnotation)) { // for TSTypeReference, typeName can be an Identifier or a TSQualifiedName return typeAnnotation.typeName.name || getQualifiedName(typeAnnotation.typeName); } if (_core.types.isTSQualifiedName(typeAnnotation)) { // for TSQualifiedName return getQualifiedName(typeAnnotation); } return null; }; exports.getTypeName = getTypeName; const buildObjectProperty = member => { const newObjectProperty = _core.types.objectProperty(member.key, member.value, member.computed); newObjectProperty.leadingComments = member.leadingComments; return newObjectProperty; }; const buildMemberAssignmentStatement = (objectIdentifier, member) => { const newMember = _core.types.expressionStatement(_core.types.assignmentExpression("=", _core.types.memberExpression(objectIdentifier, member.key, member.computed), member.value)); newMember.leadingComments = member.leadingComments; return newMember; }; const buildThisMemberAssignmentStatement = buildMemberAssignmentStatement.bind(null, _core.types.thisExpression());