babel-plugin-transform-modules-ui5
Version:
An unofficial babel plugin for SAP UI5.
375 lines (357 loc) • 22.2 kB
JavaScript
;
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());