babel-plugin-transform-modules-ui5 7.9.0 → 7.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,88 @@
1
1
  # Change Log
2
2
 
3
+ ## 7.11.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#162](https://github.com/ui5-community/babel-plugin-transform-modules-ui5/pull/162) [`db2ad98`](https://github.com/ui5-community/babel-plugin-transform-modules-ui5/commit/db2ad98648ac7971d7b6cc1264b8d7b32d4b0b56) Thanks [@petermuessig](https://github.com/petermuessig)! - feat: auto-detect UI5 extend-classes by filename suffix and known base-class module paths
8
+
9
+ ## What's new
10
+ - **Auto-detection by known UI5 base classes**: Classes extending from known UI5 base-class module paths (e.g. `sap/ui/core/mvc/Controller`, `sap/ui/core/Control`, `sap/ui/base/ManagedObject`) are automatically converted to `.extend()` syntax without requiring explicit `@alias` or `@namespace` annotations. Controlled by new `autoConvertKnownUI5Classes` option (defaults to true).
11
+
12
+ - **Transitive inheritance resolution**: Auto-detection walks the superclass chain through local files (order-independent via on-demand ancestor reading from disk) and detects UI5 ancestry even through multiple levels of custom base classes.
13
+
14
+ - **File-type suffix detection**: New `autoConvertUI5FileTypes` option detects and converts classes based on filename patterns (`.controller.js|ts` and `.view.js|ts|jsx|tsx`). Controllers preserve legacy behavior (no namespace required). Views require a resolvable namespace.
15
+
16
+ - **Custom base-class paths**: New `additionalUI5Classes` option allows registering project-specific base-class module paths for auto-detection, supplementing the built-in UI5 class list.
17
+
18
+ - **Cycle detection**: Inheritance chain walking correctly handles cycles (A extends B extends A) via visited-set tracking instead of depth limits.
19
+
20
+ - **Watch-mode cache fixes**: Fixed stale cache entries persisting across rebuilds when files are renamed or deleted mid-watch. The `importDeclarations` cache entry is now cleared (not just overwritten) during re-processing.
21
+
22
+ - **Unmatched path warnings**: Added `console.warn` diagnostic when `additionalUI5Classes` paths are configured but never matched by any import in the build, helping catch typos and misconfigurations.
23
+
24
+ ## Breaking changes
25
+
26
+ None. All new features are opt-in or preserve existing behavior.
27
+
28
+ **Internal API note:** `getImportDeclaration` (in `src/classes/helpers/imports.js`) now matches
29
+ `import { Foo as Bar }` by the _local_ binding name (`Bar`) rather than the _imported_ name (`Foo`).
30
+ This is the correct semantics — callers always pass the name as it appears at the use site — but if
31
+ you maintain a fork or plugin that calls this helper directly by the imported name, update those call
32
+ sites to pass the local name instead.
33
+
34
+ - [#168](https://github.com/ui5-community/babel-plugin-transform-modules-ui5/pull/168) [`e31c07f`](https://github.com/ui5-community/babel-plugin-transform-modules-ui5/commit/e31c07f2e47478ae77e00e135d60c4c8fb896c88) Thanks [@petermuessig](https://github.com/petermuessig)! - feat: add `@movePropsToConstructor` per-property annotation to opt out of `moveControllerPropsToOnInit` ([#116](https://github.com/ui5-community/babel-plugin-transform-modules-ui5/issues/116))
35
+
36
+ When `moveControllerPropsToOnInit` is enabled, individual class properties can be kept in the
37
+ constructor by annotating them with `@movePropsToConstructor` (JSDoc block comment or decorator):
38
+
39
+ ```ts
40
+ export default class MyController extends Controller {
41
+ /** Normal prop — moves to onInit as usual */
42
+ prop = 1;
43
+
44
+ /**
45
+ * @movePropsToConstructor
46
+ */
47
+ formatter = { formatValue: (v: string) => v }; // stays in constructor
48
+
49
+ handler = () => true; // moves to onInit as usual
50
+ }
51
+ ```
52
+
53
+ ### Patch Changes
54
+
55
+ - [#167](https://github.com/ui5-community/babel-plugin-transform-modules-ui5/pull/167) [`d021d2e`](https://github.com/ui5-community/babel-plugin-transform-modules-ui5/commit/d021d2e6422494931ddeb8944ac443facc2c5e4b) Thanks [@petermuessig](https://github.com/petermuessig)! - fix: avoid crash when a default export uses `Object.assign(Object.create(null), …)` alongside named exports ([#148](https://github.com/ui5-community/babel-plugin-transform-modules-ui5/issues/148))
56
+
57
+ `getPropertiesOfObjectAssignOrExtendHelper` no longer leaks `undefined` entries for call-expression arguments
58
+ (e.g. `Object.create(null)`), which previously caused `Cannot read properties of undefined (reading 'key')` during
59
+ export collapsing.
60
+
61
+ - [#166](https://github.com/ui5-community/babel-plugin-transform-modules-ui5/pull/166) [`506985d`](https://github.com/ui5-community/babel-plugin-transform-modules-ui5/commit/506985d2d5f9004faad9f4cbd9b5e4c04b4bd052) Thanks [@petermuessig](https://github.com/petermuessig)! - fix: handle string-literal import names and object property keys; escape regex metacharacters in noImportInteropPrefixes
62
+
63
+ Picks up the still-relevant fixes from PR [#163](https://github.com/ui5-community/babel-plugin-transform-modules-ui5/issues/163) (thanks @mauriciolauffer):
64
+
65
+ - Named imports using string-literal specifiers (`import { "some-export" as local } from "mod"`) now produce correct destructure output instead of `undefined`.
66
+ - `noImportInteropPrefixes` entries are now regex-escaped before being embedded in the regexp pattern, preventing metacharacters from acting as wildcards.
67
+ - `getPropNames` and `groupPropertiesByName` in `utils/ast.js` now handle `StringLiteral` property keys in addition to `Identifier` keys.
68
+
69
+ ## 7.10.0
70
+
71
+ ### Minor Changes
72
+
73
+ - [#154](https://github.com/ui5-community/babel-plugin-transform-modules-ui5/pull/154) [`f62f9fb`](https://github.com/ui5-community/babel-plugin-transform-modules-ui5/commit/f62f9fba1a64dea2673662dd163fcc1eb5cff540) Thanks [@bbogovich-sap](https://github.com/bbogovich-sap)! - Add two comment pragmas to opt out of module transformation:
74
+
75
+ - `/* @ui5-no-wrap */` (file level) leaves a module completely unwrapped —
76
+ no `sap.ui.define`/`sap.ui.require` and no import/export rewriting — while
77
+ still stripping TypeScript types. Intended for modules consumed as native
78
+ ES modules, e.g. web workers.
79
+ - `/* @ui5-ignore-import */` (statement level) excludes a single static
80
+ `import` or dynamic `import()` from being converted into a UI5 dependency.
81
+
82
+ Note: `@ui5-ignore-import` on a static import only yields valid output in an
83
+ unwrapped module (no exports, or together with `@ui5-no-wrap`); a leftover
84
+ ES `import` inside a `sap.ui.define` factory is not valid at runtime.
85
+
3
86
  ## 7.9.0
4
87
 
5
88
  ### Minor Changes
@@ -30,6 +30,7 @@ function convertClassToUI5Extend(path, node, classInfo, extraStaticProps, import
30
30
  return node;
31
31
  }
32
32
  const CONTROLLER_EXTENSION_TAG = "transformControllerExtension";
33
+ const KEEP_IN_CONSTRUCTOR_TAG = "movePropsToConstructor";
33
34
  const staticMembers = [];
34
35
  const classNameIdentifier = node.id;
35
36
  const className = classNameIdentifier.name;
@@ -42,6 +43,8 @@ function convertClassToUI5Extend(path, node, classInfo, extraStaticProps, import
42
43
  const alwaysMoveInstanceProps = !opts.onlyMoveClassPropsUsingThis;
43
44
  const extendProps = [];
44
45
  const boundProps = [];
46
+ const constructorBoundProps = []; // props that must stay in constructor even when moveControllerPropsToOnInit is true
47
+
45
48
  const CONSTRUCTOR = "constructor";
46
49
  const propsByName = {};
47
50
  let constructor;
@@ -187,6 +190,7 @@ function convertClassToUI5Extend(path, node, classInfo, extraStaticProps, import
187
190
  staticMembers.push(buildMemberAssignmentStatement(classNameIdentifier, member));
188
191
  }
189
192
  } else {
193
+ var _member$leadingCommen2, _member$decorators2;
190
194
  propsByName[memberName] = member.value;
191
195
  if (memberName === "constructor") {
192
196
  constructorComments = member.leadingComments;
@@ -195,8 +199,19 @@ function convertClassToUI5Extend(path, node, classInfo, extraStaticProps, import
195
199
  continue; // don't push to props yet
196
200
  }
197
201
  }
202
+ // When moveControllerPropsToOnInit is active, a per-property @movePropsToConstructor
203
+ // annotation (JSDoc block comment or decorator) opts that specific property out of the
204
+ // onInit move and keeps it in the constructor instead.
205
+ const keepInConstructor = moveControllerPropsToOnInit && (((_member$leadingCommen2 = member.leadingComments) === null || _member$leadingCommen2 === void 0 ? void 0 : _member$leadingCommen2.some(comment => comment.value.includes("@" + KEEP_IN_CONSTRUCTOR_TAG))) || ((_member$decorators2 = member.decorators) === null || _member$decorators2 === void 0 ? void 0 : _member$decorators2.some(decorator => {
206
+ var _decorator$expression2;
207
+ return ((_decorator$expression2 = decorator.expression) === null || _decorator$expression2 === void 0 ? void 0 : _decorator$expression2.name) === KEEP_IN_CONSTRUCTOR_TAG;
208
+ })));
198
209
  if (alwaysMoveInstanceProps || _core.types.isArrowFunctionExpression(member.value) || ast.isThisExpressionUsed(member.value)) {
199
- boundProps.push(member);
210
+ if (keepInConstructor) {
211
+ constructorBoundProps.push(member);
212
+ } else {
213
+ boundProps.push(member);
214
+ }
200
215
  } else {
201
216
  extendProps.push(buildObjectProperty(member));
202
217
  }
@@ -249,6 +264,10 @@ function convertClassToUI5Extend(path, node, classInfo, extraStaticProps, import
249
264
  // See if we need either constructor or onInit
250
265
  const needsBindingMethod = boundProps.length || moveControllerPropsToOnInit && constructor && !keepConstructor;
251
266
 
267
+ // See if we need to create a constructor separately for constructorBoundProps
268
+ // (only relevant when moveControllerPropsToOnInit is on and some props opted out).
269
+ const needsConstructorForOverrides = moveControllerPropsToOnInit && constructorBoundProps.length;
270
+
252
271
  // See if we need to create a new 'constructor' or 'onInit' method, depending which one we'll bind to.
253
272
  if (needsBindingMethod && !bindMethod) {
254
273
  const bindToId = _core.types.identifier(bindToMethodName);
@@ -284,6 +303,32 @@ function convertClassToUI5Extend(path, node, classInfo, extraStaticProps, import
284
303
  bindMethod.body.body = [...upToSuper, ...mappedProps, ...afterSuper];
285
304
  }
286
305
  }
306
+ if (needsConstructorForOverrides) {
307
+ // Some props were annotated @movePropsToConstructor — inject them into the
308
+ // constructor even though moveControllerPropsToOnInit is active.
309
+ let constructorMethod = constructor;
310
+ if (!constructorMethod) {
311
+ // No explicit constructor: create an inheriting one and register it.
312
+ const constructorDeclaration = th.buildInheritingConstructor({
313
+ SUPER: _core.types.identifier(superClassName)
314
+ });
315
+ constructorMethod = ast.convertFunctionDeclarationToExpression(constructorDeclaration);
316
+ extendProps.unshift(_core.types.objectProperty(_core.types.identifier(CONSTRUCTOR), constructorMethod));
317
+ } else if (!keepConstructor) {
318
+ // Constructor exists but was merged into onInit — we need to keep it in
319
+ // extend() as well so the constructor-bound props have somewhere to live.
320
+ extendProps.unshift(_core.types.objectProperty(_core.types.identifier(CONSTRUCTOR), constructorMethod));
321
+ }
322
+ const mappedOverrideProps = constructorBoundProps.map(member => buildThisMemberAssignmentStatement(member));
323
+ const superIndex = constructorMethod.body.body.findIndex(node => ast.isSuperCallExpression(node.expression) || ast.isSuperPrototypeCallOf(node.expression, superClassName, CONSTRUCTOR));
324
+ if (superIndex === -1) {
325
+ constructorMethod.body.body.unshift(...mappedOverrideProps);
326
+ } else {
327
+ const upToSuper = constructorMethod.body.body.slice(0, superIndex + 1);
328
+ const afterSuper = constructorMethod.body.body.slice(superIndex + 1);
329
+ constructorMethod.body.body = [...upToSuper, ...mappedOverrideProps, ...afterSuper];
330
+ }
331
+ }
287
332
  const extendAssign = th.buildExtendAssign({
288
333
  NAME: classNameIdentifier,
289
334
  SUPER: superClass,
@@ -293,6 +338,16 @@ function convertClassToUI5Extend(path, node, classInfo, extraStaticProps, import
293
338
  });
294
339
  return [extendAssign, ...staticMembers];
295
340
  }
341
+
342
+ /**
343
+ * Derives the fully-qualified name (FQN) passed to SAPClass.extend("FQN", ...).
344
+ *
345
+ * Precedence:
346
+ * 1. @alias — full FQN set directly; use for named extend-classes (recommended)
347
+ * 2. @name — full FQN set directly; use for anonymous class expressions where
348
+ * @alias cannot be used (there is no local name to alias)
349
+ * 3. @namespace / fileNamespace — namespace prefix; the ES class name is appended
350
+ */
296
351
  function getFullyQualifiedName(classInfo) {
297
352
  if (classInfo.alias) return classInfo.alias;
298
353
  if (classInfo.name) return classInfo.name;
@@ -336,19 +391,7 @@ function getFileBaseNamespace(path, pluginOpts) {
336
391
  const opts = path.hub.file.opts;
337
392
  const filename = _path.default.resolve(opts.filename);
338
393
  const sourceRoot = opts.sourceRoot ? _path.default.resolve(process.cwd(), opts.sourceRoot) : process.cwd();
339
- if (filename.startsWith(sourceRoot)) {
340
- const filenameRelative = _path.default.relative(sourceRoot, filename);
341
- const {
342
- dir
343
- } = _path.default.parse(filenameRelative);
344
- const namespaceParts = dir.split(_path.default.sep);
345
- if (pluginOpts.namespacePrefix) {
346
- namespaceParts.unshift(pluginOpts.namespacePrefix);
347
- }
348
- return namespaceParts.join(".");
349
- } else {
350
- return undefined;
351
- }
394
+ return (0, _imports.deriveNamespaceForFile)(filename, sourceRoot, pluginOpts);
352
395
  }
353
396
  const getQualifiedName = node => {
354
397
  let {
@@ -4,6 +4,17 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.getDecoratorClassInfo = getDecoratorClassInfo;
7
+ // Tags that provide a value for the class info via decorators.
8
+ // The naming tags follow the same conventions as their JSDoc equivalents:
9
+ // @alias — recommended for named UI5 extend-classes; sets the fully-qualified
10
+ // class name passed to .extend().
11
+ // @name — use for anonymous (unnamed) class expressions where @alias cannot
12
+ // be used.
13
+ // @namespace — sets the namespace prefix; the ES class name is appended automatically
14
+ // to form the full FQN passed to .extend().
15
+ // The following two are parsed but their values are not used by the class transformer;
16
+ // metadata and renderer are handled via static class properties instead:
17
+ // @metadata, @renderer
7
18
  const classInfoValueTags = ["alias", "name", "namespace", "metadata", "renderer"];
8
19
  const classInfoBoolTags = ["nonUI5"];
9
20
  function getDecoratorClassInfo(node) {
@@ -31,10 +42,21 @@ function groupByName(decorators) {
31
42
  return decorators.reduce((accumulator, decorator) => {
32
43
  const expression = decorator.expression;
33
44
  const name = expression.name || expression.callee && expression.callee.name;
45
+ // Skip decorators with no resolvable name — e.g. member-expression decorators
46
+ // like `@foo.bar` / `@foo.bar()` (callee is a MemberExpression, so .name is
47
+ // undefined). These are never one of the recognised UI5 tags, and indexing with
48
+ // an undefined name would throw on .toLowerCase().
49
+ if (!name) {
50
+ return accumulator;
51
+ }
34
52
  accumulator[name.toLowerCase()] = decorator;
35
53
  return accumulator;
36
54
  }, {});
37
55
  }
38
56
  function getDecoratorValue(decorator) {
39
- return (decorator && decorator.expression.arguments[0] || {}).value;
57
+ // A recognised value tag may be used without a call (e.g. `@alias` instead of
58
+ // `@alias("x")`). In that case decorator.expression is an Identifier with no
59
+ // `.arguments`, so guard before indexing to avoid a crash.
60
+ const args = decorator && decorator.expression && decorator.expression.arguments || [];
61
+ return (args[0] || {}).value;
40
62
  }