From e513c5be7655755bfeb49806013a14233960121a Mon Sep 17 00:00:00 2001 From: Ryan Murphy Date: Fri, 8 Feb 2019 09:45:08 -0500 Subject: [PATCH 1/5] general readme updates --- LICENSE | 2 +- README.md | 51 ++++++++++++++++++++++++++------------------------- 2 files changed, 27 insertions(+), 26 deletions(-) diff --git a/LICENSE b/LICENSE index 46d3a18..e39b539 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2017 Ryan Murphy +Copyright (c) 2019 Ryan Murphy Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index dc427d0..89099c7 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,16 @@ -# babel-ui5 +# babel transform-ui5 An unofficial Babel transformer plugin for SAP/Open UI5. It allows you to develop SAP UI5 applications by using the latest [ECMAScript](http://babeljs.io/docs/learn-es2015/), including classes and modules, or even TypeScript. -**WARNING Currently not compatible with @babel/preset-typescript** +**WARNING Currently not fully compatible with @babel/preset-typescript** [![Build Status](https://travis-ci.org/r-murphy/babel-plugin-transform-modules-ui5.svg?branch=master)](https://travis-ci.org/r-murphy/babel-plugin-transform-modules-ui5) ## Install -This repo contains both a preset and a plugin. It is recommended to use the preset. +This repo contains both a preset and a plugin. It is recommended to use the preset, in case the plugin gets split up in the future. ```sh npm install babel-preset-transform-ui5 --save-dev @@ -52,22 +52,22 @@ It is also recommended to use [@babel/preset-env](https://babeljs.io/docs/en/nex ## Features -There are 2 main feature categories of the plugin, and you can use both or one without the other.: +There are 2 main features of the plugin, and you can use both or one without the other: 1. Converting ES modules (import/export) into sap.ui.define or sap.ui.require. 2. Converting ES classes into Control.extend(..) syntax. -This only transforms the UI5 relevant things. It does not transform everything to ES5 (for example it does not transform const/let to var). This makes it easier to use `babel-preset-env` to determine how to transform everything else. +This only transforms the UI5 relevant things. It does not transform everything to ES5 (for example it does not transform const/let to var). This makes it easier to use `@babel/preset-env` to transform things correctly. -A more detailed list includes: +A more detailed feature list includes: - ES2015 Imports (default, named, and dynamic) - ES2015 Exports (default and named) -- Class, using inheritance and `super` keyword +- Classes, using inheritance and `super` keyword - Static methods and fields - Class properties - Class property arrow functions are bound correctly in the constructor. -- Existing `sap.ui.define` calls don't get wrapped but can still be converted. +- Existing `sap.ui.define` calls don't get wrapped but classes within can still be converted. - Fixes `constructor` shorthand method, if used. - Various options to control the class name string used. - JSDoc (name, namespace, alias) @@ -76,7 +76,7 @@ A more detailed list includes: ### Converting ES modules (import/export) into sap.ui.define or sap.ui.require -The plugin will wrap any code having import/export statements in an sap.ui.define. If there is no import/export, it won't wrap. +The plugin will wrap any code having import/export statements in an sap.ui.define. If there is no import/export, it won't be wrapped. #### Static Import @@ -85,13 +85,13 @@ The plugin supports all of the ES import statements, and will convert them into ```js import Default from "module"; import Default, { Named } from "module"; -import { Named, Named2 } from "module"; -import * as Name from "module"; +import { Named1, Named2 } from "module"; +import * as Namespace from "module"; ``` -The plugin uses a temporary name (as needed) for the initial imported variable, and then extracts the properties from it as needed. -This allows importing ES Modules which have a 'default' value, and also non-ES modules which don't. -The plugin also allows for merged imports statements from the same source path into a single require and then deconstructs it accordingly. +The plugin uses a temporary name (as needed) for the initial imported variable, and then extracts the properties from it. +This allows importing ES Modules which have a 'default' value, and also non-ES modules which do not. +The plugin also merges imports statements from the same source path into a single require and then deconstructs it accordingly. This: @@ -144,7 +144,8 @@ export { X as default }; export let v; v = 'v'; // NOTE that the value here is currently not a live binding (http://2ality.com/2015/07/es6-module-exports.html) ``` -Export is a bit trickier if you want your exported module to be imported by code that does not include the import inter-op. If the importing code has the inter-op logic inserted by this plugin, then you don't need to worry, and can disable the export inter-op features if desired. +Export is a bit trickier if you want your exported module to be imported by code that does not include an import inter-op. +If the importing code has an inter-op logic inserted by this plugin, then you don't need to worry and can disable the export inter-op features if desired. Imagine a file like this: @@ -159,7 +160,7 @@ export default { }; ``` -Which might create an exported module that looks like: +Which might export a module that looks like: ```js { @@ -205,7 +206,7 @@ In order to determine which properties the default export already has, the plugi export default { prop: val, }; -// plugin knows about prop +// plugin knows about 'prop' ``` - In a variable declaration literal or assigned afterwards. @@ -216,10 +217,10 @@ const Module = { }; Module.prop2 = val2; export default Module; -// plugin knows about prop1 and prop2 +// plugin knows about 'prop1' and 'prop2' ``` -- In an Object.assign(..) or \_extends(..) +- In an `Object.assign(..)` or `_extends(..)` - \_extends is the named used by babel and typescript when compiling object spread. - This includes a recursive search for any additional objects used in the assign/extend which are defined in the upper block scope. @@ -231,10 +232,10 @@ const object2 = Object.assign({}, object1, { prop2: val, }); export default object2; -// plugin knows about prop1 and prop2 +// plugin knows about 'prop1' and 'prop2' ``` -**CAUTION**: The plugin cannot check the properties on imported modules. So if they are used in Object.assign() or \_extends(), the plugin will not be aware of its properties and may override them with a named export. +**CAUTION**: The plugin cannot check the properties on imported modules. So if they are used in `Object.assign()` or `_extends()`, the plugin will not be aware of its properties and may override them with a named export. ##### Example non-solvable issues @@ -254,7 +255,7 @@ function one_string() { } const MyUtil = { - // The plugin can't assign these to `exports` since the definition is not just a reference to the named export. + // The plugin can't assign 'one' or 'two' to `exports` since there is a collision with a different definition. one: one_string, two: () => "two", }; @@ -320,8 +321,8 @@ sap.ui.define(["./a"], A => { ### Converting ES classes into Control.extend(..) syntax -By default, the plugin converts ES classes to Control.extend(..) syntax if the class extends from a class which has been imported. -So a class without a parent will not be extended. +By default, the plugin converts ES classes to `Control.extend(..)` syntax if the class extends from a class which has been imported. +So a class without a parent will not be converted to .extend() syntax. There are a few options or some metadata you can use to control this. @@ -672,4 +673,4 @@ Issues also welcome for feature requests. ## License -MIT © 2017 Ryan Murphy +MIT © 2019 Ryan Murphy From e0e9a6de6da508f4fe5f38ba687f1afa7060ba74 Mon Sep 17 00:00:00 2001 From: Ryan Murphy Date: Tue, 12 Feb 2019 09:20:41 -0500 Subject: [PATCH 2/5] Split up plugin logic and move module transform out of Program enter --- README.md | 26 +- .../__test__/__snapshots__/test.js.snap | 24 +- .../preset-env-class.js} | 0 .../fixtures/preset-env/preset-env-usage.js | 15 + packages/plugin/__test__/test.js | 6 +- packages/plugin/package.json | 1 + .../src/{ => classes}/helpers/classes.js | 6 +- .../src/{ => classes}/helpers/decorators.js | 0 .../plugin/src/{ => classes}/helpers/jsdoc.js | 15 +- packages/plugin/src/classes/visitor.js | 189 ++++++ packages/plugin/src/helpers/imports.js | 9 - packages/plugin/src/helpers/utils.js | 3 - packages/plugin/src/index.js | 539 ++---------------- .../src/{ => modules}/helpers/exports.js | 4 +- .../src/{ => modules}/helpers/wrapper.js | 15 +- packages/plugin/src/modules/visitor.js | 273 +++++++++ packages/plugin/src/{helpers => utils}/ast.js | 0 .../src/{helpers => utils}/templates.js | 16 +- 18 files changed, 598 insertions(+), 543 deletions(-) rename packages/plugin/__test__/fixtures/{presets/with-preset-env.js => preset-env/preset-env-class.js} (100%) create mode 100644 packages/plugin/__test__/fixtures/preset-env/preset-env-usage.js rename packages/plugin/src/{ => classes}/helpers/classes.js (99%) rename packages/plugin/src/{ => classes}/helpers/decorators.js (100%) rename packages/plugin/src/{ => classes}/helpers/jsdoc.js (84%) create mode 100644 packages/plugin/src/classes/visitor.js delete mode 100644 packages/plugin/src/helpers/imports.js delete mode 100644 packages/plugin/src/helpers/utils.js rename packages/plugin/src/{ => modules}/helpers/exports.js (98%) rename packages/plugin/src/{ => modules}/helpers/wrapper.js (94%) create mode 100644 packages/plugin/src/modules/visitor.js rename packages/plugin/src/{helpers => utils}/ast.js (100%) rename packages/plugin/src/{helpers => utils}/templates.js (91%) diff --git a/README.md b/README.md index 89099c7..b65ebff 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,11 @@ An unofficial Babel transformer plugin for SAP/Open UI5. It allows you to develop SAP UI5 applications by using the latest [ECMAScript](http://babeljs.io/docs/learn-es2015/), including classes and modules, or even TypeScript. -**WARNING Currently not fully compatible with @babel/preset-typescript** - [![Build Status](https://travis-ci.org/r-murphy/babel-plugin-transform-modules-ui5.svg?branch=master)](https://travis-ci.org/r-murphy/babel-plugin-transform-modules-ui5) ## Install -This repo contains both a preset and a plugin. It is recommended to use the preset, in case the plugin gets split up in the future. +This repo contains both a preset and a plugin. It is recommended to use the preset since the plugin will get split into two in the future. ```sh npm install babel-preset-transform-ui5 --save-dev @@ -26,7 +24,7 @@ yarn add babel-preset-transform-ui5 --dev ### .babelrc -At a minimum, add `transform-modules-ui5` to the `plugins`. +At a minimum, add `transform-ui5` to the `presets`. ```js { @@ -46,10 +44,26 @@ Or if you want to supply plugin options, use the array syntax. } ``` -At the time of writing, the babel version is 7.0, which does not natively support class property syntax. To use that syntax also add the plugin `@babel/plugin-syntax-class-properties`. +At the time of writing the babel version is 7.3, which does not natively support class property syntax. To use that syntax also add the plugin `@babel/plugin-syntax-class-properties`. It is also recommended to use [@babel/preset-env](https://babeljs.io/docs/en/next/babel-preset-env.html) to control which ES version the final code is transformed to. +The order of presets is important and `@babel/preset-env` should be higher in the array than this one. Babel applies them in reverse order for legacy reasons, so this preset will be applied first. + +```js +{ + "presets": [ + ["@babel/preset-env", { // applied 3rd + ...presetEnvOpts + }], + ["transform-ui5", { // applied 2nd + ...pluginOpts + }], + "@babel/preset-typescript", // applied 1st + ] +} +``` + ## Features There are 2 main features of the plugin, and you can use both or one without the other: @@ -57,6 +71,8 @@ There are 2 main features of the plugin, and you can use both or one without the 1. Converting ES modules (import/export) into sap.ui.define or sap.ui.require. 2. Converting ES classes into Control.extend(..) syntax. +**NOTE:** The class transform will be split into its own plugin in the future. + This only transforms the UI5 relevant things. It does not transform everything to ES5 (for example it does not transform const/let to var). This makes it easier to use `@babel/preset-env` to transform things correctly. A more detailed feature list includes: diff --git a/packages/plugin/__test__/__snapshots__/test.js.snap b/packages/plugin/__test__/__snapshots__/test.js.snap index ef1e06d..08ced32 100644 --- a/packages/plugin/__test__/__snapshots__/test.js.snap +++ b/packages/plugin/__test__/__snapshots__/test.js.snap @@ -1173,10 +1173,8 @@ exports[`no-wrap skip-iffe.js 1`] = ` })();" `; -exports[`presets with-preset-env.js 1`] = ` -"\\"use strict\\"; - -sap.ui.define([\\"sap/class\\"], function (SAPClass) { +exports[`preset-env preset-env-class.js 1`] = ` +"sap.ui.define([\\"sap/class\\"], function (SAPClass) { var MyClass = SAPClass.extend(\\"my.MyClass\\", { x: 1, fn: function _fn() { @@ -1192,6 +1190,22 @@ sap.ui.define([\\"sap/class\\"], function (SAPClass) { });" `; +exports[`preset-env preset-env-usage.js 1`] = ` +"sap.ui.define([\\"sap/SAPClass\\", \\"core-js/modules/es6.promise\\", \\"core-js/modules/es6.string.includes\\", \\"core-js/modules/es7.array.includes\\"], function (SAPClass, __core_js_modules_es6promise, __core_js_modules_es6stringincludes, __core_js_modules_es7arrayincludes) { + var AClass = SAPClass.extend(\\"my.MyClass\\", { + delay: function _delay() { + return new Promise(function (resolve) { + setTimeout(resolve); + }); + }, + includes: function _includes(str) { + return str.includes(\\"thing\\"); + } + }); + return AClass; +});" +`; + exports[`typescript ts-class-props.ts 1`] = ` "sap.ui.define([\\"sap/Class\\"], function (SAPClass) { const MyClass = SAPClass.extend(\\"MyClass\\", { @@ -1208,7 +1222,7 @@ exports[`typescript ts-class-props.ts 1`] = ` exports[`typescript ts-export-interface.ts 1`] = ` "sap.ui.define([], function () { const MY_CONSTANT = \\"constant\\"; - let MyEnum; + var MyEnum; (function (MyEnum) { MyEnum[MyEnum[\\"A\\"] = 0] = \\"A\\"; diff --git a/packages/plugin/__test__/fixtures/presets/with-preset-env.js b/packages/plugin/__test__/fixtures/preset-env/preset-env-class.js similarity index 100% rename from packages/plugin/__test__/fixtures/presets/with-preset-env.js rename to packages/plugin/__test__/fixtures/preset-env/preset-env-class.js diff --git a/packages/plugin/__test__/fixtures/preset-env/preset-env-usage.js b/packages/plugin/__test__/fixtures/preset-env/preset-env-usage.js new file mode 100644 index 0000000..55abf0e --- /dev/null +++ b/packages/plugin/__test__/fixtures/preset-env/preset-env-usage.js @@ -0,0 +1,15 @@ +import SAPClass from "sap/SAPClass"; + +/** + * @name my.MyClass + */ +export default class AClass extends SAPClass { + delay() { + return new Promise(resolve => { + setTimeout(resolve); + }); + } + includes(str) { + return str.includes("thing"); + } +} diff --git a/packages/plugin/__test__/test.js b/packages/plugin/__test__/test.js index 741d65a..87e9b8d 100644 --- a/packages/plugin/__test__/test.js +++ b/packages/plugin/__test__/test.js @@ -41,7 +41,9 @@ function processDirectory(dir) { presets.push([ "@babel/preset-env", { - // default targets for preset-env is ES5 + targets: undefined, // default targets for preset-env is ES5 + modules: false, + useBuiltIns: "usage", }, ]); } @@ -50,7 +52,7 @@ function processDirectory(dir) { "@babel/plugin-syntax-dynamic-import", "@babel/plugin-syntax-object-rest-spread", ["@babel/plugin-syntax-decorators", { legacy: true }], - ["@babel/plugin-syntax-class-properties", { useBuiltIns: true }], + // ["@babel/plugin-syntax-class-properties", { useBuiltIns: true }], [plugin, opts], ], presets, diff --git a/packages/plugin/package.json b/packages/plugin/package.json index a6a91c8..910e821 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -10,6 +10,7 @@ "scripts": { "clean": "rimraf dist", "build": "babel src -d dist", + "build:watch": "babel src -d dist --watch", "lint": "eslint src", "lint:fix": "npm run lint -- --fix", "lint:staged": "lint-staged", diff --git a/packages/plugin/src/helpers/classes.js b/packages/plugin/src/classes/helpers/classes.js similarity index 99% rename from packages/plugin/src/helpers/classes.js rename to packages/plugin/src/classes/helpers/classes.js index c89b826..401e184 100644 --- a/packages/plugin/src/helpers/classes.js +++ b/packages/plugin/src/classes/helpers/classes.js @@ -1,9 +1,11 @@ import { types as t } from "@babel/core"; -import * as th from "./templates"; + import Path from "path"; import assignDefined from "object-assign-defined"; -import * as ast from "./ast"; +import * as th from "../../utils/templates"; +import * as ast from "../../utils/ast"; + import { getJsDocClassInfo, getTags } from "./jsdoc"; import { getDecoratorClassInfo } from "./decorators"; diff --git a/packages/plugin/src/helpers/decorators.js b/packages/plugin/src/classes/helpers/decorators.js similarity index 100% rename from packages/plugin/src/helpers/decorators.js rename to packages/plugin/src/classes/helpers/decorators.js diff --git a/packages/plugin/src/helpers/jsdoc.js b/packages/plugin/src/classes/helpers/jsdoc.js similarity index 84% rename from packages/plugin/src/helpers/jsdoc.js rename to packages/plugin/src/classes/helpers/jsdoc.js index 6ebb913..c24b8c1 100644 --- a/packages/plugin/src/helpers/jsdoc.js +++ b/packages/plugin/src/classes/helpers/jsdoc.js @@ -1,10 +1,8 @@ import { types as t } from "@babel/core"; import doctrine from "doctrine"; import ignoreCase from "ignore-case"; -import { isCommentBlock } from "./ast"; const classInfoValueTags = ["alias", "name", "namespace"]; - const classInfoBoolTags = ["nonUI5", "controller", "keepConstructor"]; export function getJsDocClassInfo(node, parent) { @@ -33,8 +31,12 @@ export function getJsDocClassInfo(node, parent) { }) .filter(notEmpty)[0]; } - // Else see if the JSDoc are on the return statement (i..e return class X extends SAPClass) - else if (t.isClassExpression(node) && t.isReturnStatement(parent)) { + // Else see if the JSDoc are on the return statement (eg. return class X extends SAPClass) + // or export statement (eg. export default class X extends SAPClass) + else if ( + (t.isClassExpression(node) && t.isReturnStatement(parent)) || + (t.isClassDeclaration(node) && t.isExportDefaultDeclaration(parent)) + ) { return getJsDocClassInfo(parent); } else { return {}; @@ -98,3 +100,8 @@ export function hasJsdocGlobalExportFlag(node) { ); }); } + +// This doesn't exist on babel-types +function isCommentBlock(node) { + return node && node.type === "CommentBlock"; +} diff --git a/packages/plugin/src/classes/visitor.js b/packages/plugin/src/classes/visitor.js new file mode 100644 index 0000000..a9b8295 --- /dev/null +++ b/packages/plugin/src/classes/visitor.js @@ -0,0 +1,189 @@ +import { types as t } from "@babel/core"; +import * as th from "../utils/templates"; +import * as ast from "../utils/ast"; +import * as classes from "./helpers/classes"; + +const CONSTRUCTOR = "constructor"; + +export const ClassTransformVisitor = { + ImportDefaultSpecifier(path) { + this.importNames.push(path.node.local.name); + }, + /*! + * Visits function calls. + */ + CallExpression(path) { + const { node } = path; + const { callee } = node; + // If the file already has sap.ui.define, get the names of variables it creates to use for the class logic. + if (ast.isCallExpressionCalling(node, "sap.ui.define")) { + this.importNames.push( + ...getRequiredParamsOfSAPUIDefine(path, node).map(req => req.name) + ); + return; + } else if (this.superClassName) { + if (t.isSuper(callee)) { + replaceConstructorSuperCall(path, node, this.superClassName); + } else if (t.isSuper(callee.object)) { + replaceObjectSuperCall(path, node, this.superClassName); + } + } + }, + /** + * ClassDeclaration visitor. + * Use both enter() and exit() to track when the visitor is inside a UI5 class, + * in order to convert the super calls. + * No changes for non-UI5 classes. + */ + Class: { + enter(path, { file, opts = {} }) { + const { node, parent, parentPath } = path; + + if (opts.neverConvertClass) { + return; + } + if (!doesClassExtendFromImport(node, [...this.importNames])) { + // If it doesn't extend from an import, treat it as plain ES2015 class. + return; + } + // If the super class is one of the imports, we'll assume it's a UI5 managed class, + // and therefore may need to be transformed to .extend() syntax. + const { name } = node.id; + const classInfo = classes.getClassInfo(path, node, path.parent, opts); + if (classInfo.nonUI5) { + return; + } + + let shouldConvert = false; + if (opts.autoConvertAllExtendClasses == true) { + shouldConvert = true; + } + if ( + classInfo.name || + classInfo.alias || + classInfo.controller || + classInfo.namespace + ) { + shouldConvert = true; + } + if ( + /.*[.]controller[.]js$/.test(file.opts.filename) && + opts.autoConvertControllerClass !== false + ) { + shouldConvert = true; + } + + if (!shouldConvert) { + return; + } + + this.superClassName = classInfo.superClassName; + + // Find the Block scoped parent (Program or Function body) and search for assigned properties within that. + const blockParent = path.findParent(path => path.isBlock()).node; + const staticProps = ast.groupPropertiesByName( + ast.getOtherPropertiesOfIdentifier(blockParent, name) + ); + // TODO: flag metadata and renderer for removal if applicable + const ui5ExtendClass = classes.convertClassToUI5Extend( + path, + node, + classInfo, + staticProps, + opts + ); + + if (path.isClassDeclaration()) { + if (t.isExportDefaultDeclaration(parent)) { + path.parentPath.replaceWithMultiple([ + ...ui5ExtendClass, + th.buildExportDefault({ + VALUE: node.id, + }), + ]); + } else { + // e.g. class X {} + path.replaceWithMultiple(ui5ExtendClass); + } + } else if (path.isClassExpression()) { + //e.g. return class X {} + if (t.isReturnStatement(parent)) { + // Add the return statement back before calling replace + ui5ExtendClass.push( + th.buildReturn({ + ID: t.identifier(classInfo.localName), + }) + ); + } + parentPath.replaceWithMultiple(ui5ExtendClass); + } + }, + exit() { + this.superClassName = null; + }, + }, + /** + * Convert object method constructor() to constructor: function constructor(), + * since a UI5 class is not a real class. + */ + ObjectMethod(path) { + const { node } = path; + if (node.key.name === CONSTRUCTOR) { + // The keyword 'constructor' should not be used as a shorthand + // method name in an object. It might(?) work on some objects, + // but it doesn't work with X.extend(...) inheritance. + path.replaceWith( + t.objectProperty( + t.identifier(CONSTRUCTOR), + t.functionExpression( + t.identifier(CONSTRUCTOR), + node.params, + node.body + ) + ) + ); + } + }, +}; + +function getRequiredParamsOfSAPUIDefine(path, node) { + const defineArgs = node.arguments; + const callbackNode = defineArgs.find(argNode => t.isFunction(argNode)); + return callbackNode.params; // Identifier +} + +/** + * Replace super() call + */ +function replaceConstructorSuperCall(path, node, superClassName) { + replaceSuperNamedCall(path, node, superClassName, CONSTRUCTOR); +} + +/** + * Replace super.method() call + */ +function replaceObjectSuperCall(path, node, superClassName) { + replaceSuperNamedCall(path, node, superClassName, node.callee.property.name); +} + +function replaceSuperNamedCall(path, node, superClassName, methodName) { + // .call() is better for simple args (or not args) but doesn't work right for spread args + // if it gets further transpiled by babel spread args transform (will be .call.apply(...). + const thisEx = t.thisExpression(); + const hasSpread = node.arguments.some(t.isSpreadElement); + const caller = hasSpread ? "apply" : "call"; + const callArgs = hasSpread + ? [thisEx, t.arrayExpression(node.arguments)] + : [thisEx, ...node.arguments]; + path.replaceWith( + t.callExpression( + t.identifier(`${superClassName}.prototype.${methodName}.${caller}`), + callArgs + ) + ); +} + +function doesClassExtendFromImport(node, imports) { + const superClass = node.superClass; + return superClass && imports.some(imported => imported === superClass.name); +} diff --git a/packages/plugin/src/helpers/imports.js b/packages/plugin/src/helpers/imports.js deleted file mode 100644 index 4459130..0000000 --- a/packages/plugin/src/helpers/imports.js +++ /dev/null @@ -1,9 +0,0 @@ -// export function create - -// export function setInteropFlag(imports, opts) { -// const { noImportInteropPrefixes = ['sap/'] } = opts -// const noImportInteropPrefixesRegexp = new RegExp(noImportInteropPrefixes.map(p => `(^${p}.*)`).join('|')) -// imports -// .filter(imp => imp.default) -// .forEach(imp => imp.interop = !noImportInteropPrefixesRegexp.test(imp.src)) -// } diff --git a/packages/plugin/src/helpers/utils.js b/packages/plugin/src/helpers/utils.js deleted file mode 100644 index f85a30d..0000000 --- a/packages/plugin/src/helpers/utils.js +++ /dev/null @@ -1,3 +0,0 @@ -// export function equalsIgnoreCase() { -// -// } diff --git a/packages/plugin/src/index.js b/packages/plugin/src/index.js index a71744e..c84bfc5 100644 --- a/packages/plugin/src/index.js +++ b/packages/plugin/src/index.js @@ -1,10 +1,7 @@ -import { types as t } from "@babel/core"; -import * as th from "./helpers/templates"; -import * as ast from "./helpers/ast"; +import { ClassTransformVisitor } from "./classes/visitor"; +import { ModuleTransformVisitor } from "./modules/visitor"; -import * as classes from "./helpers/classes"; -import { hasJsdocGlobalExportFlag } from "./helpers/jsdoc"; -import { wrap } from "./helpers/wrapper"; +import { wrap } from "./modules/helpers/wrapper"; /* References: @@ -14,506 +11,56 @@ Babel Types (t.*) https://github.com/babel/babel/tree/master/packages/babel-type AST Explorer: https://astexplorer.net/ */ -const CONSTRUCTOR = "constructor"; -const tempModuleName = name => `__${name}`; -const cleanImportSource = src => - src.replace(/(\/)|(-)|(@)/g, "_").replace(/\./g, ""); - module.exports = () => { - const ProgramVisitor = { - // Use a ProgramVisitor to efficiently avoid processing the same file twice if babel calls twice. - // The UI5ModuleVisitor will only be used if it hasn't ran. - Program(path, { opts }) { - if (this.ran) return; - this.ran = true; - let { node } = path; - - if (opts.onlyConvertNamedClass == false) { - throw new Error( - "ERROR: onlyConvertNamedClass=false is no longer supported. Use autoConvertAllExtendClassesByDefault=true" - ); - } - // TODO: enable this in a later version - // else if (opts.onlyConvertNamedClass == true) { - // console.warn('WARN: onlyConvertNamedClass=true is now the default behaviour') // eslint-disable-line no-console - // } - - this.defaultExport = null; - this.defaultExportNode = null; - this.exportGlobal = false; - this.ignoredImports = []; - this.imports = []; - this.namedExports = []; - this.requires = []; - this.injectDynamicImportHelper = false; - this.programNode = node; - - // Opts handling - this.namespacePrefix = opts.namespacePrefix; - this.noImportInteropPrefixes = opts.noImportInteropPrefixes || ["sap/"]; - this.noImportInteropPrefixesRegexp = new RegExp( - this.noImportInteropPrefixes.map(p => `(^${p}.*)`).join("|") - ); - - path.traverse(UI5ModuleVisitor, this); - - const needsWrap = !!( - this.defaultExport || - this.imports.length || - this.namedExports.length || - this.injectDynamicImportHelper - ); - if (needsWrap) { - wrap(this, node, opts); - } - }, - }; - - const UI5ModuleVisitor = { - /*! - * Removes the ES6 import and adds the details to the import array in our state. - */ - ImportDeclaration(path, { opts = {} }) { - const { node } = path; - - if (node.importKind === "type") return; // flow-type - - const { specifiers, source } = node; - const src = source.value.replace(/\\/g, "/"); - - // When 'libs' are used, only 'libs' will be converted to UI5 imports. - const { libs = [".*"] } = opts; - const isLibToConvert = new RegExp(`(${libs.join("|")})`).test(src); - if (!isLibToConvert) { - this.ignoredImports.push(node); - path.remove(); - return; - } - //const testSrc = (opts.libs || ["^sap/"]).concat(opts.files || []); - // const isUi5SrcRE = testSrc.length && new RegExp(`(${testSrc.join("|")})`); - // const isUi5Src = isUi5SrcRE.test(src); - - // Importing using an interop is the default behaviour but can be opt-out using regex. - const shouldInterop = !this.noImportInteropPrefixesRegexp.test(src); - - const name = cleanImportSource(src); // default to the src for import without named var - - const { modulesMap = {} } = opts; - const mappedSrc = - (typeof modulesMap === "function" - ? modulesMap(src) - : modulesMap[src]) || src; - - // Note that existingImport may get mutated if there are multiple import lines from the same module. - const existingImport = this.imports.find(imp => imp.src === mappedSrc); - - const imp = existingImport || { - src: mappedSrc, // url - name, - // isLib, // for future use separating UI5 imports from npm/webpack imports - // isUi5Src, // not used yet - tmpName: shouldInterop ? tempModuleName(name) : name, - deconstructors: [], - default: false, - interop: false, - path: path, - locked: false, - }; - - const deconstructors = []; - - for (const specifier of specifiers) { - if (t.isImportDefaultSpecifier(specifier)) { - // e.g. import X from 'X' - imp.default = true; - imp.interop = shouldInterop; - - // Shorten the imported-as name since it should be unique for default imports. - // The default import should always come first, - // so this new name will be used for destructuring the other too. - if (!imp.locked) { - imp.name = specifier.local.name; - imp.tmpName = shouldInterop ? tempModuleName(imp.name) : imp.name; - imp.locked = true; - } - - if (shouldInterop) { - deconstructors.push( - th.buildDefaultImportDeconstructor({ - MODULE: t.identifier(imp.tmpName), - LOCAL: specifier.local, - }) - ); - } - } else if (t.isImportNamespaceSpecifier(specifier)) { - if (specifiers.length === 1 && !imp.locked) { - // e.g. import * as X from 'X' - // If the namespace specifier is the only import, we can avoid the temp name and the destructor - imp.name = specifier.local.name; - imp.tmpName = specifier.local.name; - imp.locked = true; // Don't let another import line for the same module change the name. - } else { - // e.g. import X, * as X2 from 'X' - // Else it's probably combined with a default export. keep the tmpName and destructure it - deconstructors.push( - th.buildConstDeclaration({ - NAME: specifier.local, - VALUE: t.identifier(imp.tmpName), - }) - ); - } - } else if (t.isImportSpecifier(specifier)) { - // e.g. import { A } from 'X' - deconstructors.push( - th.buildNamedImportDestructor({ - MODULE: t.identifier(imp.tmpName), - LOCAL: specifier.local, - IMPORTED: t.stringLiteral(specifier.imported.name), - }) - ); - } else { - throw path.buildCodeFrameError( - `Unknown ImportDeclaration specifier type ${specifier.type}` - ); - } - } - - path.replaceWithMultiple(deconstructors); - - if (deconstructors.length) { - // Keep the same variable name if the same module is imported on another line. - imp.locked = true; - } - - imp.deconstructors = imp.deconstructors.concat(deconstructors); - - // TODO: now that we're saving deconstructors on the import, dynamically determine firstImport if needed. - if (!this.firstImport && imp.deconstructors[0]) { - this.firstImport = imp.deconstructors[0]; - } - - if (!existingImport) { - this.imports.push(imp); - } - }, - - /** - * Push all exports to an array. - * The reason we don't export in place is to handle the situation - * where a let or var can be defined, and the latest one should be exported. - */ - ExportNamedDeclaration(path) { - const { node } = path; - const { specifiers, declaration, source } = node; - - let fromSource = ""; - if (source) { - // e.g. export { one, two } from 'x' - const src = source.value; - const name = cleanImportSource(src); - const tmpName = tempModuleName(name); - this.imports.push({ src, name, tmpName }); - fromSource = tmpName + "."; - } - - if (specifiers && specifiers.length) { - // e.g. export { one, two } - for (const specifier of path.node.specifiers) { - this.namedExports.push({ - key: specifier.exported, - value: t.identifier(`${fromSource}${specifier.local.name}`), - }); - } - path.remove(); - } else if (declaration) { - // e.g. export const c = 1 | export function f() {} - if ( - [ - "TypeAlias", - "InterfaceDeclaration", - "TSInterfaceDeclaration", - ].includes(declaration.type) - ) - return; // TS or Flow-types - const name = ast.getIdName(declaration); - if (name) { - // e.g. export function f() {} - const id = t.identifier(declaration.id.name); - this.namedExports.push({ - key: id, - value: id, - declaration, - }); - } else if (declaration.declarations) { - // e.g. export const c = 1 - for (const subDeclaration of declaration.declarations) { - const id = t.identifier(subDeclaration.id.name); - this.namedExports.push({ - value: id, - key: id, - declaration: subDeclaration, - }); - } - } else { - throw path.buildCodeFrameError( - "Unknown ExportNamedDeclaration shape." + const UI5Visitor = { + Program: { + enter(path, { opts }) { + if (this.ran) return; + this.ran = true; + + if (opts.onlyConvertNamedClass == false) { + throw new Error( + "ERROR: onlyConvertNamedClass=false is no longer supported. Use autoConvertAllExtendClassesByDefault=true" ); } - path.replaceWith(declaration); - } else { - throw path.buildCodeFrameError("Unknown ExportNamedDeclaration shape."); - } - }, - - /*! - * Replaces the ES6 export with sap.ui.define by using the state.imports array built up when - * visiting ImportDeclaration. - * Only a single 'export default' is supported. - */ - ExportDefaultDeclaration(path) { - const { node } = path; - const { declaration } = node; - const declarationName = ast.getIdName(declaration); - if (hasGlobalExportFlag(node)) { - // check for jsdoc @export - this.exportGlobal = true; - } - if (declarationName) { - // ClassDeclaration or FunctionDeclaration with name. - // Leave the declaration in-line and preserve the identifier for the return statement. - path.replaceWith(declaration); - this.defaultExport = t.identifier(declarationName); - } else { - // Identifier, ObjectExpression or anonymous FunctionDeclaration - // Safe to move to the end and return directly - if (t.isFunctionDeclaration(declaration)) { - const { params, body, generator, async: isAsync } = declaration; - this.defaultExport = t.functionExpression( - null, - params, - body, - generator, - isAsync - ); - } else { - this.defaultExport = declaration; - } - path.remove(); - } - }, - - ExportAllDeclaration(path) { - const src = path.node.source.value; - const name = src.replace(/\//g, "_").replace(/\./g, ""); - const tmpName = tempModuleName(name); - - this.imports.push({ src, name, tmpName }); - - this.exportAllHelper = true; - - this.namedExports.push({ - all: true, - value: t.identifier(tmpName), - }); - - path.remove(); - }, - - /** - * ClassDeclaration visitor. - * Use both enter() and exit() to track when the visitor is inside a UI5 class, - * in order to convert the super calls. - * No changes for non-UI5 classes. - */ - Class: { - enter(path, { file, opts = {} }) { - const { node, parent, parentPath } = path; - - if (opts.neverConvertClass) { - return; - } - if ( - !doesClassExtendFromImport(node, [...this.imports, ...this.requires]) - ) { - // If it doesn't extend from an import, treat it as plain ES2015 class. - return; - } - // If the super class is one of the imports, we'll assume it's a UI5 managed class, - // and therefore may need to be transformed to .extend() syntax. - const { name } = node.id; - const classInfo = classes.getClassInfo(path, node, path.parent, opts); - if (classInfo.nonUI5) { - return; - } - - let shouldConvert = false; - if (opts.autoConvertAllExtendClasses == true) { - shouldConvert = true; - } - if ( - classInfo.name || - classInfo.alias || - classInfo.controller || - classInfo.namespace - ) { - shouldConvert = true; - } - if ( - /.*[.]controller[.]js$/.test(file.opts.filename) && - opts.autoConvertControllerClass !== false - ) { - shouldConvert = true; - } - - if (!shouldConvert) { - return; - } - - this.superClassName = classInfo.superClassName; - - // Find the Block scoped parent (Program or Function body) and search for assigned properties within that. - const blockParent = path.findParent(path => path.isBlock()).node; - const staticProps = ast.groupPropertiesByName( - ast.getOtherPropertiesOfIdentifier(blockParent, name) - ); - // TODO: flag metadata and renderer for removal if applicable - const ui5ExtendClass = classes.convertClassToUI5Extend( - path, - node, - classInfo, - staticProps, - opts + // TODO: enable this in a later version + // else if (opts.onlyConvertNamedClass == true) { + // console.warn('WARN: onlyConvertNamedClass=true is now the default behaviour') // eslint-disable-line no-console + // } + + // Opts + this.namespacePrefix = opts.namespacePrefix; + this.noImportInteropPrefixes = opts.noImportInteropPrefixes || ["sap/"]; + this.noImportInteropPrefixesRegexp = new RegExp( + this.noImportInteropPrefixes.map(p => `(^${p}.*)`).join("|") ); - if (path.isClassDeclaration()) { - // e.g. class X {} - path.replaceWithMultiple(ui5ExtendClass); - } else if (path.isClassExpression()) { - //e.g. return class X {} - if (t.isReturnStatement(parent)) { - // Add the return statement back before calling replace - ui5ExtendClass.push( - th.buildReturn({ - ID: t.identifier(classInfo.localName), - }) - ); - } - parentPath.replaceWithMultiple(ui5ExtendClass); - } + // Properties for Module Transform + this.programNode = path.node; + this.defaultExport = null; + this.defaultExportNode = null; + this.exportGlobal = false; + this.ignoredImports = []; + this.imports = []; + this.namedExports = []; + this.injectDynamicImportHelper = false; + + // Properties for Class Transform + this.importNames = []; + + // The classes must be converted right away before any other class transforms get a chance to run. + path.traverse(ClassTransformVisitor, this); }, - exit() { - this.superClassName = null; + exit(path, { opts }) { + wrap(this, path.node, opts); }, }, - - /*! - * Visits function calls. - */ - CallExpression(path) { - const { node } = path; - const { callee } = node; - if (ast.isImport(callee)) { - this.injectDynamicImportHelper = true; - path.replaceWith({ - ...node, - callee: t.identifier("__ui5_require_async"), - }); - } - // If the file already has sap.ui.define, get the names of variables it creates to use for the class logic. - else if (ast.isCallExpressionCalling(node, "sap.ui.define")) { - this.requires = getRequiredParamsOfSAPUIDefine(path, node); - return; - } else if (this.superClassName) { - if (t.isSuper(callee)) { - replaceConstructorSuperCall(path, node, this.superClassName); - } else if (t.isSuper(callee.object)) { - replaceObjectSuperCall(path, node, this.superClassName); - } - } - }, - - /** - * Convert object method constructor() to constructor: function constructor(), - * since a UI5 class is not a real class. - */ - ObjectMethod(path) { - const { node } = path; - if (node.key.name === CONSTRUCTOR) { - // The keyword 'constructor' should not be used as a shorthand - // method name in an object. It might(?) work on some objects, - // but it doesn't work with X.extend(...) inheritance. - path.replaceWith( - t.objectProperty( - t.identifier(CONSTRUCTOR), - t.functionExpression( - t.identifier(CONSTRUCTOR), - node.params, - node.body - ) - ) - ); - } - }, + // The module transform visitor uses normal traversal so we capture + // imports and helper code that get added by other transforms. + ...ModuleTransformVisitor, }; - function getRequiredParamsOfSAPUIDefine(path, node) { - const defineArgs = node.arguments; - const callbackNode = defineArgs.find(argNode => t.isFunction(argNode)); - return callbackNode.params; // Identifier - } - - /** - * Replace super() call - */ - function replaceConstructorSuperCall(path, node, superClassName) { - replaceSuperNamedCall(path, node, superClassName, CONSTRUCTOR); - } - - /** - * Replace super.method() call - */ - function replaceObjectSuperCall(path, node, superClassName) { - replaceSuperNamedCall( - path, - node, - superClassName, - node.callee.property.name - ); - } - - function replaceSuperNamedCall(path, node, superClassName, methodName) { - // .call() is better for simple args (or not args) but doesn't work right for spread args - // if it gets further transpiled by babel spread args transform (will be .call.apply(...). - const thisEx = t.thisExpression(); - const hasSpread = node.arguments.some(t.isSpreadElement); - const caller = hasSpread ? "apply" : "call"; - const callArgs = hasSpread - ? [thisEx, t.arrayExpression(node.arguments)] - : [thisEx, ...node.arguments]; - path.replaceWith( - t.callExpression( - t.identifier(`${superClassName}.prototype.${methodName}.${caller}`), - callArgs - ) - ); - } - - function doesClassExtendFromImport(node, imports) { - const superClass = node.superClass; - if (!superClass) { - return false; - } - const isImported = imports.some( - imported => imported.name === superClass.name - ); - return isImported; - } - - function hasGlobalExportFlag(node) { - return hasJsdocGlobalExportFlag(node); - } - return { - visitor: ProgramVisitor, + visitor: UI5Visitor, }; }; diff --git a/packages/plugin/src/helpers/exports.js b/packages/plugin/src/modules/helpers/exports.js similarity index 98% rename from packages/plugin/src/helpers/exports.js rename to packages/plugin/src/modules/helpers/exports.js index 59d90b4..9083e30 100644 --- a/packages/plugin/src/helpers/exports.js +++ b/packages/plugin/src/modules/helpers/exports.js @@ -1,6 +1,6 @@ import { types as t } from "@babel/core"; -import * as th from "./templates"; -import * as ast from "./ast"; +import * as th from "../../utils/templates"; +import * as ast from "../../utils/ast"; /** * Collapse named exports onto the default export so that the default export can be returned directly, diff --git a/packages/plugin/src/helpers/wrapper.js b/packages/plugin/src/modules/helpers/wrapper.js similarity index 94% rename from packages/plugin/src/helpers/wrapper.js rename to packages/plugin/src/modules/helpers/wrapper.js index 0c812c0..c6da85d 100644 --- a/packages/plugin/src/helpers/wrapper.js +++ b/packages/plugin/src/modules/helpers/wrapper.js @@ -1,19 +1,28 @@ import { types as t } from "@babel/core"; -import * as th from "./templates"; import * as eh from "./exports"; -import * as ast from "./ast"; +import * as th from "../../utils/templates"; +import * as ast from "../../utils/ast"; export function wrap(visitor, programNode, opts) { let { defaultExport, exportGlobal, firstImport, - injectDynamicImportHelper, imports, namedExports, ignoredImports, + injectDynamicImportHelper, } = visitor; + const needsWrap = !!( + defaultExport || + imports.length || + namedExports.length || + injectDynamicImportHelper + ); + + if (!needsWrap) return; + let { body } = programNode; let allExportHelperAdded = false; diff --git a/packages/plugin/src/modules/visitor.js b/packages/plugin/src/modules/visitor.js new file mode 100644 index 0000000..521151e --- /dev/null +++ b/packages/plugin/src/modules/visitor.js @@ -0,0 +1,273 @@ +import { types as t } from "@babel/core"; +import * as th from "../utils/templates"; +import * as ast from "../utils/ast"; + +import { hasJsdocGlobalExportFlag } from "../classes/helpers/jsdoc"; + +const tempModuleName = name => `__${name}`; +const cleanImportSource = src => + src.replace(/(\/)|(-)|(@)/g, "_").replace(/\./g, ""); +const hasGlobalExportFlag = node => hasJsdocGlobalExportFlag(node); + +export const ModuleTransformVisitor = { + /*! + * Removes the ES6 import and adds the details to the import array in our state. + */ + ImportDeclaration(path, { opts = {} }) { + const { node } = path; + + if (node.importKind === "type") return; // flow-type + + const { specifiers, source } = node; + const src = source.value.replace(/\\/g, "/"); + + // When 'libs' are used, only 'libs' will be converted to UI5 imports. + const { libs = [".*"] } = opts; + const isLibToConvert = new RegExp(`(${libs.join("|")})`).test(src); + if (!isLibToConvert) { + this.ignoredImports.push(node); + path.remove(); + return; + } + //const testSrc = (opts.libs || ["^sap/"]).concat(opts.files || []); + // const isUi5SrcRE = testSrc.length && new RegExp(`(${testSrc.join("|")})`); + // const isUi5Src = isUi5SrcRE.test(src); + + // Importing using an interop is the default behaviour but can be opt-out using regex. + const shouldInterop = !this.noImportInteropPrefixesRegexp.test(src); + + const name = cleanImportSource(src); // default to the src for import without named var + + const { modulesMap = {} } = opts; + const mappedSrc = + (typeof modulesMap === "function" ? modulesMap(src) : modulesMap[src]) || + src; + + // Note that existingImport may get mutated if there are multiple import lines from the same module. + const existingImport = this.imports.find(imp => imp.src === mappedSrc); + + const imp = existingImport || { + src: mappedSrc, // url + name, + // isLib, // for future use separating UI5 imports from npm/webpack imports + // isUi5Src, // not used yet + tmpName: shouldInterop ? tempModuleName(name) : name, + deconstructors: [], + default: false, + interop: false, + path: path, + locked: false, + }; + + const deconstructors = []; + + for (const specifier of specifiers) { + if (t.isImportDefaultSpecifier(specifier)) { + // e.g. import X from 'X' + imp.default = true; + imp.interop = shouldInterop; + + // Shorten the imported-as name since it should be unique for default imports. + // The default import should always come first, + // so this new name will be used for destructuring the other too. + if (!imp.locked) { + imp.name = specifier.local.name; + imp.tmpName = shouldInterop ? tempModuleName(imp.name) : imp.name; + imp.locked = true; + } + + if (shouldInterop) { + deconstructors.push( + th.buildDefaultImportDeconstructor({ + MODULE: t.identifier(imp.tmpName), + LOCAL: specifier.local, + }) + ); + } + } else if (t.isImportNamespaceSpecifier(specifier)) { + if (specifiers.length === 1 && !imp.locked) { + // e.g. import * as X from 'X' + // If the namespace specifier is the only import, we can avoid the temp name and the destructor + imp.name = specifier.local.name; + imp.tmpName = specifier.local.name; + imp.locked = true; // Don't let another import line for the same module change the name. + } else { + // e.g. import X, * as X2 from 'X' + // Else it's probably combined with a default export. keep the tmpName and destructure it + deconstructors.push( + th.buildConstDeclaration({ + NAME: specifier.local, + VALUE: t.identifier(imp.tmpName), + }) + ); + } + } else if (t.isImportSpecifier(specifier)) { + // e.g. import { A } from 'X' + deconstructors.push( + th.buildNamedImportDestructor({ + MODULE: t.identifier(imp.tmpName), + LOCAL: specifier.local, + IMPORTED: t.stringLiteral(specifier.imported.name), + }) + ); + } else { + throw path.buildCodeFrameError( + `Unknown ImportDeclaration specifier type ${specifier.type}` + ); + } + } + + path.replaceWithMultiple(deconstructors); + + if (deconstructors.length) { + // Keep the same variable name if the same module is imported on another line. + imp.locked = true; + } + + imp.deconstructors = imp.deconstructors.concat(deconstructors); + + // TODO: now that we're saving deconstructors on the import, dynamically determine firstImport if needed. + if (!this.firstImport && imp.deconstructors[0]) { + this.firstImport = imp.deconstructors[0]; + } + + if (!existingImport) { + this.imports.push(imp); + } + }, + + /** + * Push all exports to an array. + * The reason we don't export in place is to handle the situation + * where a let or var can be defined, and the latest one should be exported. + */ + ExportNamedDeclaration(path) { + const { node } = path; + const { specifiers, declaration, source } = node; + + let fromSource = ""; + if (source) { + // e.g. export { one, two } from 'x' + const src = source.value; + const name = cleanImportSource(src); + const tmpName = tempModuleName(name); + this.imports.push({ src, name, tmpName }); + fromSource = tmpName + "."; + } + + if (specifiers && specifiers.length) { + // e.g. export { one, two } + for (const specifier of path.node.specifiers) { + this.namedExports.push({ + key: specifier.exported, + value: t.identifier(`${fromSource}${specifier.local.name}`), + }); + } + path.remove(); + } else if (declaration) { + // e.g. export const c = 1 | export function f() {} + if ( + [ + "TypeAlias", + "InterfaceDeclaration", + "TSInterfaceDeclaration", + ].includes(declaration.type) + ) + return; // TS or Flow-types + const name = ast.getIdName(declaration); + if (name) { + // e.g. export function f() {} + const id = t.identifier(declaration.id.name); + this.namedExports.push({ + key: id, + value: id, + declaration, + }); + } else if (declaration.declarations) { + // e.g. export const c = 1 + for (const subDeclaration of declaration.declarations) { + const id = t.identifier(subDeclaration.id.name); + this.namedExports.push({ + value: id, + key: id, + declaration: subDeclaration, + }); + } + } else { + throw path.buildCodeFrameError("Unknown ExportNamedDeclaration shape."); + } + path.replaceWith(declaration); + } else { + throw path.buildCodeFrameError("Unknown ExportNamedDeclaration shape."); + } + }, + + /*! + * Replaces the ES6 export with sap.ui.define by using the state.imports array built up when + * visiting ImportDeclaration. + * Only a single 'export default' is supported. + */ + ExportDefaultDeclaration(path) { + const { node } = path; + const { declaration } = node; + const declarationName = ast.getIdName(declaration); + if (hasGlobalExportFlag(node)) { + // check for jsdoc @export + this.exportGlobal = true; + } + if (declarationName) { + // ClassDeclaration or FunctionDeclaration with name. + // Leave the declaration in-line and preserve the identifier for the return statement. + path.replaceWith(declaration); + this.defaultExport = t.identifier(declarationName); + } else { + // Identifier, ObjectExpression or anonymous FunctionDeclaration + // Safe to move to the end and return directly + if (t.isFunctionDeclaration(declaration)) { + const { params, body, generator, async: isAsync } = declaration; + this.defaultExport = t.functionExpression( + null, + params, + body, + generator, + isAsync + ); + } else { + this.defaultExport = declaration; + } + path.remove(); + } + }, + + ExportAllDeclaration(path) { + const src = path.node.source.value; + const name = src.replace(/\//g, "_").replace(/\./g, ""); + const tmpName = tempModuleName(name); + + this.imports.push({ src, name, tmpName }); + + this.exportAllHelper = true; + + this.namedExports.push({ + all: true, + value: t.identifier(tmpName), + }); + + path.remove(); + }, + + /*! + * Visits function calls. + */ + CallExpression(path) { + const { node } = path; + const { callee } = node; + if (ast.isImport(callee)) { + this.injectDynamicImportHelper = true; + path.replaceWith({ + ...node, + callee: t.identifier("__ui5_require_async"), + }); + } + }, +}; diff --git a/packages/plugin/src/helpers/ast.js b/packages/plugin/src/utils/ast.js similarity index 100% rename from packages/plugin/src/helpers/ast.js rename to packages/plugin/src/utils/ast.js diff --git a/packages/plugin/src/helpers/templates.js b/packages/plugin/src/utils/templates.js similarity index 91% rename from packages/plugin/src/helpers/templates.js rename to packages/plugin/src/utils/templates.js index b4ed2e5..d7b4f05 100644 --- a/packages/plugin/src/helpers/templates.js +++ b/packages/plugin/src/utils/templates.js @@ -30,16 +30,14 @@ export const buildTempExport = template(` const ${exportName} = VALUE; `); +export const buildExportDefault = template(` + export default VALUE; +`); + export const buildReturnExports = template(` return ${exportName}; `); -// export const buildExportsModuleDeclaration = template(` -// Object.defineProperty(${exportString}, "__esModule", { -// value: true -// }); -// `) - export function buildNamedExport(obj) { // console.log(obj); return buildAssign({ @@ -117,12 +115,6 @@ export const buildThisAssignment = template(` this.NAME = VALUE; `); -// export const buildDefaultConstructorFunction = template(` -// function constructor() { -// SUPER.prototype.constructor.apply(this, arguments); -// } -// `) - // This is use when there is not already the function, so always propagate arguments. export const buildInheritingFunction = template(` function NAME() { From 355835d9e62ea4e82ffef07d007730e66aefb48c Mon Sep 17 00:00:00 2001 From: Ryan Murphy Date: Wed, 13 Feb 2019 00:10:39 -0500 Subject: [PATCH 3/5] tests: re-enable class property syntax --- packages/plugin/__test__/test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugin/__test__/test.js b/packages/plugin/__test__/test.js index 87e9b8d..4f7a175 100644 --- a/packages/plugin/__test__/test.js +++ b/packages/plugin/__test__/test.js @@ -52,7 +52,7 @@ function processDirectory(dir) { "@babel/plugin-syntax-dynamic-import", "@babel/plugin-syntax-object-rest-spread", ["@babel/plugin-syntax-decorators", { legacy: true }], - // ["@babel/plugin-syntax-class-properties", { useBuiltIns: true }], + ["@babel/plugin-syntax-class-properties", { useBuiltIns: true }], [plugin, opts], ], presets, From fb9e5588b111777959125e97bd631bb243367cb5 Mon Sep 17 00:00:00 2001 From: Ryan Murphy Date: Wed, 13 Feb 2019 00:18:02 -0500 Subject: [PATCH 4/5] placeholder for classes plugin --- packages/plugin-transform-classes/README.md | 17 +++++++++++++++++ .../plugin-transform-classes/package.json | 19 +++++++++++++++++++ .../plugin-transform-classes/src/index.js | 3 +++ 3 files changed, 39 insertions(+) create mode 100644 packages/plugin-transform-classes/README.md create mode 100644 packages/plugin-transform-classes/package.json create mode 100644 packages/plugin-transform-classes/src/index.js diff --git a/packages/plugin-transform-classes/README.md b/packages/plugin-transform-classes/README.md new file mode 100644 index 0000000..840daf0 --- /dev/null +++ b/packages/plugin-transform-classes/README.md @@ -0,0 +1,17 @@ +# babel-plugin-transform-classes-ui5 + +[Docs](../../README.md) + +There is also a preset which should be used rather than using the plugin directly. + +## Install + +```sh +npm install babel-plugin-transform-classes-ui5 --save-dev +``` + +or + +```sh +yarn add babel-plugin-transform-classes-ui5 --dev +``` diff --git a/packages/plugin-transform-classes/package.json b/packages/plugin-transform-classes/package.json new file mode 100644 index 0000000..957a69f --- /dev/null +++ b/packages/plugin-transform-classes/package.json @@ -0,0 +1,19 @@ +{ + "name": "babel-plugin-transform-classes-ui5", + "version": "7.0.0-rc.5", + "description": "An unofficial babel plugin for SAP UI5.", + "main": "src/index.js", + "repository": "https://github.com/r-murphy/babel-plugin-ui5/tree/master/packages/plugin-transform-classes", + "engines": { + "node": ">=6" + }, + "scripts": {}, + "author": "Ryan Murphy", + "license": "MIT", + "bugs": { + "url": "https://github.com/r-murphy/babel-plugin-transform-modules-ui5/issues" + }, + "homepage": "https://github.com/r-murphy/babel-plugin-transform-modules-ui5#readme", + "dependencies": {}, + "devDependencies": {} +} diff --git a/packages/plugin-transform-classes/src/index.js b/packages/plugin-transform-classes/src/index.js new file mode 100644 index 0000000..7aad76b --- /dev/null +++ b/packages/plugin-transform-classes/src/index.js @@ -0,0 +1,3 @@ +return { + visitor: {}, +}; From 46ad4b59862771b57fdfdf53e303cc5cb1807cee Mon Sep 17 00:00:00 2001 From: Ryan Murphy Date: Wed, 13 Feb 2019 00:18:12 -0500 Subject: [PATCH 5/5] github link updates --- packages/plugin/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 910e821..e33ae1c 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -39,9 +39,9 @@ "author": "Ryan Murphy", "license": "MIT", "bugs": { - "url": "https://github.com/r-murphy/babel-plugin-ui5/issues" + "url": "https://github.com/r-murphy/babel-plugin-transform-modules-ui5/issues" }, - "homepage": "https://github.com/r-murphy/babel-plugin-ui5#readme", + "homepage": "https://github.com/r-murphy/babel-plugin-transform-modules-ui5#readme", "jest": { "collectCoverage": true },