babel-plugin-transform-modules-ui5 7.10.0 → 7.11.1

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.
@@ -3,35 +3,812 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
+ exports.clearFileFromCache = clearFileFromCache;
7
+ exports.deriveNamespaceForFile = deriveNamespaceForFile;
8
+ exports.getEffectiveNamespacePrefix = getEffectiveNamespacePrefix;
6
9
  exports.getImportDeclaration = getImportDeclaration;
10
+ exports.getUnmatchedAdditionalUI5Classes = getUnmatchedAdditionalUI5Classes;
11
+ exports.isLocalChainConvertible = isLocalChainConvertible;
12
+ exports.isSuperclassTransitivelyKnown = isSuperclassTransitivelyKnown;
13
+ exports.recordAdditionalUI5ClassMatch = recordAdditionalUI5ClassMatch;
14
+ exports.resetAdditionalUI5ClassMatchTracking = resetAdditionalUI5ClassMatchTracking;
15
+ exports.resetAllCaches = resetAllCaches;
16
+ exports.resetOnDemandEvictionCycle = resetOnDemandEvictionCycle;
17
+ exports.resolveImportSourceToFilename = resolveImportSourceToFilename;
7
18
  exports.saveImports = void 0;
19
+ exports.saveSuperclassImportSource = saveSuperclassImportSource;
8
20
  var _core = require("@babel/core");
21
+ var Path = _interopRequireWildcard(require("path"));
22
+ var _fs = require("fs");
23
+ 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); }
9
24
  let importDeclarations = {};
10
- const saveImports = file => {
25
+
26
+ // Maps "filename\0superclassLocalName" → the import source path of that superclass.
27
+ // Used to check whether a class's superclass comes from a known UI5 base-class path.
28
+ let superclassImportSources = {};
29
+
30
+ // Maps "filename\0className" → the local binding name of that class's superclass in the
31
+ // same file. Allows transitive lookup: given a descendant that imports class Foo from a
32
+ // file, look up file\0Foo to get Foo's superclass binding name, then use that binding
33
+ // name to look up superclassImportSources. This correctly follows the exported class's
34
+ // superclass chain rather than any sibling class's chain.
35
+ let classSuperclassNames = {};
36
+
37
+ // PM1: Per-file key index for O(1) cache invalidation.
38
+ // Maps filename → Set of keys currently stored in superclassImportSources and
39
+ // classSuperclassNames for that file. Allows clearFileFromCache to visit only
40
+ // the relevant keys rather than scanning all entries (which was O(N²) across N files).
41
+ // Using a Set gives O(1) dedup and membership checks vs. the prior Array.includes approach.
42
+ let ancestryCacheKeys = {};
43
+
44
+ // PM2: Tracks which files were populated by readAndRecordFile (on-demand disk reads)
45
+ // rather than by saveImports (Babel-opened files). On-demand entries are evicted once
46
+ // at the start of the first saveImports call in each rebuild cycle, so that a changed
47
+ // ancestor is re-read next time a descendant needs it rather than serving stale data.
48
+ // onDemandEvictedThisCycle prevents re-evicting the same set on every file in the cycle.
49
+ // It must be reset to false after each file (in post()) so the next watch cycle re-evicts.
50
+ const onDemandCachedFiles = new Set();
51
+ let onDemandEvictedThisCycle = false;
52
+
53
+ // N1: Tracks files that have any explicit `export default` declaration. Used to decide
54
+ // whether by-name fallback is safe in isSuperclassTransitivelyKnown for default imports:
55
+ // the fallback is only allowed when no default export exists (i.e. the class is consumed
56
+ // via module interop, and the import alias matches the class name). When a file HAS a
57
+ // default export, the "\0default" sentinel is authoritative — if it's absent, the default
58
+ // export has no convertible UI5 ancestry and the by-name fallback must not fire.
59
+ let filesWithDefaultExport = new Set();
60
+
61
+ // Tracks which additionalUI5Classes module paths were actually matched during this
62
+ // Babel run. Used by getUnmatchedAdditionalUI5Classes() to warn about misconfigured paths.
63
+ //
64
+ // Lifetime note: like allConfiguredUI5ClassPaths in index.js, this accumulates for the
65
+ // whole Node process. The unmatched-path warning fires once at process exit and reports
66
+ // the union across every build, so "configured" and "matched" are intentionally tracked
67
+ // on the same process-lifetime basis — they stay consistent with each other. A long-lived
68
+ // watch process never reaches process exit, so the warning is effectively a one-shot-CLI
69
+ // feature. resetAdditionalUI5ClassMatchTracking() is provided for programmatic callers and
70
+ // tests that drive multiple independent runs in one process and want a clean slate.
71
+ const matchedAdditionalUI5ClassPaths = new Set();
72
+
73
+ /**
74
+ * Derive the full namespace string for `filename` given `sourceRoot` and plugin options.
75
+ *
76
+ * This is the pure, path-only version of the namespace computation — it does NOT append
77
+ * the class name. Returns "" when the file has no namespace (e.g. unmapped sourceFolders
78
+ * entry), and `undefined` when the file is outside both sourceRoot and all dependencies.
79
+ *
80
+ * Used by:
81
+ * - getFileBaseNamespace (classes.js) — production path via Babel path object
82
+ * - N2 ancestor-namespace check (visitor.js) — operates on resolved absolute paths
83
+ *
84
+ * @param {string} filename Absolute path of the file.
85
+ * @param {string} sourceRoot Absolute source-root path.
86
+ * @param {object} opts Plugin options (sourceFolders, namespacePrefix, dependencies).
87
+ * @returns {string|undefined} Namespace string (may be ""), or undefined when outside all roots.
88
+ */
89
+ function deriveNamespaceForFile(filename, sourceRoot, opts) {
90
+ const filenameRelative = Path.relative(sourceRoot, filename);
91
+ const insideSourceRoot = filenameRelative === "" || !filenameRelative.startsWith("..") && !Path.isAbsolute(filenameRelative);
92
+ if (!insideSourceRoot) {
93
+ const deps = opts && Array.isArray(opts.dependencies) ? opts.dependencies : [];
94
+ for (const dep of deps) {
95
+ if (!dep.sourceRoot) continue;
96
+ const depRoot = Path.resolve(dep.sourceRoot);
97
+ const depRel = Path.relative(depRoot, filename);
98
+ const depInside = depRel === "" || !depRel.startsWith("..") && !Path.isAbsolute(depRel);
99
+ if (!depInside) continue;
100
+ const sf = dep.sourceFolders;
101
+ if (sf && typeof sf === "object") {
102
+ const parts = Path.parse(depRel).dir.split(Path.sep);
103
+ const leadingFolder = parts[0];
104
+ if (leadingFolder && Object.prototype.hasOwnProperty.call(sf, leadingFolder)) {
105
+ const mappedPrefix = sf[leadingFolder];
106
+ const namespaceParts = parts.slice(1).filter(Boolean);
107
+ if (mappedPrefix) namespaceParts.unshift(mappedPrefix);
108
+ return namespaceParts.join(".");
109
+ } else if (leadingFolder) {
110
+ return "";
111
+ }
112
+ // File directly at dep root with no sub-folder — fall through to namespacePrefix
113
+ }
114
+ const depDirParts = Path.parse(depRel).dir.split(Path.sep).filter(Boolean);
115
+ if (dep.namespacePrefix) depDirParts.unshift(dep.namespacePrefix);
116
+ return depDirParts.join(".");
117
+ }
118
+ return undefined;
119
+ }
120
+ const sourceFolders = opts && opts.sourceFolders;
121
+ const parts = Path.parse(filenameRelative).dir.split(Path.sep);
122
+ const leadingFolder = parts[0];
123
+ if (sourceFolders && typeof sourceFolders === "object") {
124
+ if (leadingFolder && Object.prototype.hasOwnProperty.call(sourceFolders, leadingFolder)) {
125
+ const mappedPrefix = sourceFolders[leadingFolder];
126
+ const namespaceParts = parts.slice(1).filter(Boolean);
127
+ if (mappedPrefix) namespaceParts.unshift(mappedPrefix);
128
+ return namespaceParts.join(".");
129
+ } else if (leadingFolder) {
130
+ return "";
131
+ }
132
+ // File directly at sourceRoot with no sub-folder — fall through to namespacePrefix
133
+ }
134
+ const namespaceParts = parts.filter(Boolean);
135
+ if (opts && opts.namespacePrefix) namespaceParts.unshift(opts.namespacePrefix);
136
+ return namespaceParts.join(".");
137
+ }
138
+
139
+ /**
140
+ * Resolve the effective namespace prefix for a given file path, respecting `sourceFolders`.
141
+ *
142
+ * When `opts.sourceFolders` (a `{ folderName: "prefix" }` map) is set:
143
+ * - The file's leading folder segment (relative to `sourceRoot`) is looked up in the map.
144
+ * - If matched, the mapped prefix is returned (may be an empty string).
145
+ * - If unmatched but a leading folder exists, `null` is returned (signals: no namespace).
146
+ * Falls back to `opts.namespacePrefix` when `sourceFolders` is absent.
147
+ *
148
+ * @param {string} filename Absolute path of the file.
149
+ * @param {string} sourceRoot Absolute source-root path.
150
+ * @param {object} opts Plugin options (sourceFolders, namespacePrefix).
151
+ * @returns {string|null} The effective prefix string, "" for an empty-string mapping,
152
+ * or null when the file's folder is present but unmapped.
153
+ */
154
+ function getEffectiveNamespacePrefix(filename, sourceRoot, opts) {
155
+ const sourceFolders = opts && opts.sourceFolders;
156
+ if (!sourceFolders || typeof sourceFolders !== "object") {
157
+ return opts && opts.namespacePrefix != null ? opts.namespacePrefix : null;
158
+ }
159
+ const filenameRelative = Path.relative(sourceRoot, filename);
160
+ const {
161
+ dir
162
+ } = Path.parse(filenameRelative);
163
+ const parts = dir.split(Path.sep);
164
+ const leadingFolder = parts[0];
165
+ if (leadingFolder && Object.prototype.hasOwnProperty.call(sourceFolders, leadingFolder)) {
166
+ return sourceFolders[leadingFolder]; // "" or a namespace string
167
+ }
168
+ if (leadingFolder) {
169
+ // Present but not mapped in primary sourceFolders — check dependencies
170
+ const deps = opts && opts.dependencies;
171
+ if (Array.isArray(deps)) {
172
+ for (const dep of deps) {
173
+ if (!dep.sourceRoot) continue;
174
+ const depRoot = Path.resolve(dep.sourceRoot);
175
+ const depRel = Path.relative(depRoot, filename);
176
+ const depInside = depRel === "" || !depRel.startsWith("..") && !Path.isAbsolute(depRel);
177
+ if (!depInside) continue;
178
+ const sf = dep.sourceFolders;
179
+ if (sf && typeof sf === "object") {
180
+ const {
181
+ dir: depDir
182
+ } = Path.parse(depRel);
183
+ const depParts = depDir.split(Path.sep);
184
+ const depLeading = depParts[0];
185
+ if (depLeading && Object.prototype.hasOwnProperty.call(sf, depLeading)) {
186
+ return sf[depLeading];
187
+ }
188
+ if (depLeading) {
189
+ // File is inside this dep root but its leading folder is not mapped → no namespace
190
+ return null;
191
+ }
192
+ // File directly at dep root — fall through to namespacePrefix
193
+ }
194
+ // No sourceFolders on this dep — use dep.namespacePrefix if provided.
195
+ // If neither sourceFolders nor namespacePrefix is set on this dep entry,
196
+ // null is returned and auto-conversion is suppressed for files in this dep.
197
+ // Set dep.namespacePrefix to enable conversion for flat (non-folder-mapped) deps.
198
+ return dep.namespacePrefix != null ? dep.namespacePrefix : null;
199
+ }
200
+ }
201
+ // Present but not mapped → caller should treat as empty namespace
202
+ return null;
203
+ }
204
+ // File is directly inside sourceRoot with no sub-folder — use namespacePrefix fallback
205
+ return opts && opts.namespacePrefix != null ? opts.namespacePrefix : null;
206
+ }
207
+
208
+ /**
209
+ * Record that `modulePath` (from additionalUI5Classes) was matched by an import in this run.
210
+ */
211
+ function recordAdditionalUI5ClassMatch(modulePath) {
212
+ matchedAdditionalUI5ClassPaths.add(modulePath);
213
+ }
214
+
215
+ /**
216
+ * Clear the matched-path tracking. Intended for programmatic/test callers that run
217
+ * several independent transforms in a single process and need the next run's
218
+ * unmatched-path check to start fresh. Not called during a normal CLI build (where
219
+ * process-lifetime accumulation is the desired behaviour — see the note above).
220
+ */
221
+ function resetAdditionalUI5ClassMatchTracking() {
222
+ matchedAdditionalUI5ClassPaths.clear();
223
+ }
224
+
225
+ /**
226
+ * Return any paths from `additionalUI5Classes` that were never matched by an import
227
+ * during this Babel run. Call once in the plugin's post hook per file; unmatched entries
228
+ * that remain at the end of the run indicate a likely misconfiguration (e.g. a typo).
229
+ */
230
+ function getUnmatchedAdditionalUI5Classes(additionalUI5Classes) {
231
+ if (!(additionalUI5Classes !== null && additionalUI5Classes !== void 0 && additionalUI5Classes.length)) return [];
232
+ return additionalUI5Classes.filter(path => !matchedAdditionalUI5ClassPaths.has(path));
233
+ }
234
+
235
+ /**
236
+ * Populate the three ancestry caches for `filename` from a parsed program body.
237
+ * Called from saveImports (eager path) and readAndRecordFile (on-demand path).
238
+ */
239
+ function recordAncestry(filename, programBody) {
11
240
  // save all import declarations before "unneeded" ones are removed by the TypeScript plugin
12
- importDeclarations[file.opts.filename] = file.ast.program.body.filter(_core.types.isImportDeclaration); // right now even the removed import still exists later and can be re-added. Otherwise do: .map(decl => t.cloneNode(decl));
241
+ const imports = programBody.filter(_core.types.isImportDeclaration);
242
+ // right now even the removed import still exists later and can be re-added. Otherwise do: .map(decl => t.cloneNode(decl));
243
+ importDeclarations[filename] = imports;
244
+
245
+ // N1: Mark the file as having a default export unconditionally — before filtering
246
+ // by class type or class name. This covers every form of `export default …`:
247
+ // named classes, anonymous classes (`export default class {}`), function declarations,
248
+ // expressions, and literals. Also covers `export { Foo as default }` (ExportNamedDeclaration
249
+ // with a default-named specifier), which is NOT an ExportDefaultDeclaration.
250
+ // The by-name fallback in isSuperclassTransitivelyKnown is suppressed whenever this file
251
+ // is in filesWithDefaultExport.
252
+ if (programBody.some(_core.types.isExportDefaultDeclaration) || programBody.some(s => {
253
+ var _s$specifiers;
254
+ return _core.types.isExportNamedDeclaration(s) && ((_s$specifiers = s.specifiers) === null || _s$specifiers === void 0 ? void 0 : _s$specifiers.some(sp => _core.types.isExportSpecifier(sp) && sp.exported.name === "default"));
255
+ })) {
256
+ filesWithDefaultExport.add(filename);
257
+ }
258
+
259
+ // Eagerly record class→superclass relationships from the AST so that transitive
260
+ // detection works regardless of file-processing order. The class visitor also calls
261
+ // saveSuperclassImportSource() for classes it converts, but that happens too late
262
+ // when a descendant file is transformed before this ancestor file in the Babel run.
263
+ const importBindings = new Map(); // local binding name → import source
264
+ for (const decl of imports) {
265
+ for (const specifier of decl.specifiers) {
266
+ if (_core.types.isImportDefaultSpecifier(specifier) || _core.types.isImportNamespaceSpecifier(specifier) || _core.types.isImportSpecifier(specifier)) {
267
+ importBindings.set(specifier.local.name, decl.source.value);
268
+ }
269
+ }
270
+ }
271
+
272
+ // Maps className → superclassLocalName for classes defined in this file.
273
+ // Used below to resolve identifier-style default exports to their superclass.
274
+ const localClassSuperMap = new Map();
275
+ for (const stmt of programBody) {
276
+ var _classNode$id, _classNode$superClass;
277
+ // Handle: class Foo extends Bar {} and export default class Foo extends Bar {}
278
+ // Also handle: export class Foo extends Bar {} (ExportNamedDeclaration wrapping a class).
279
+ // ExportNamedDeclaration must be unwrapped here so that a subsequent identifier-style
280
+ // `export default Foo` can find Foo in localClassSuperMap.
281
+ const isDefaultExport = _core.types.isExportDefaultDeclaration(stmt) && _core.types.isClassDeclaration(stmt.declaration);
282
+ const isNamedExport = _core.types.isExportNamedDeclaration(stmt) && _core.types.isClassDeclaration(stmt.declaration);
283
+ const classNode = _core.types.isClassDeclaration(stmt) ? stmt : isDefaultExport || isNamedExport ? stmt.declaration : null;
284
+ if (!classNode) continue;
285
+ const className = (_classNode$id = classNode.id) === null || _classNode$id === void 0 ? void 0 : _classNode$id.name;
286
+ const superName = (_classNode$superClass = classNode.superClass) === null || _classNode$superClass === void 0 ? void 0 : _classNode$superClass.name;
287
+ if (!className) continue;
288
+ // N1: Record which classes are declared in this file, even if they have no imported
289
+ // superclass, so localClassSuperMap contains every class name for the default-export pass.
290
+ localClassSuperMap.set(className, superName || null);
291
+ if (!superName) continue;
292
+ const importSource = importBindings.get(superName);
293
+ if (importSource) {
294
+ const srcKey = filename + "\0" + superName;
295
+ const clsKey = filename + "\0" + className;
296
+ superclassImportSources[srcKey] = importSource;
297
+ classSuperclassNames[clsKey] = superName;
298
+ // PM1: track keys for O(1) invalidation
299
+ ancestryCacheKeys[filename] = ancestryCacheKeys[filename] || new Set();
300
+ ancestryCacheKeys[filename].add(srcKey);
301
+ ancestryCacheKeys[filename].add(clsKey);
302
+ // For inline `export default class Foo extends Bar {}`, record the sentinel
303
+ // immediately. This will be overwritten below if an identifier-style default
304
+ // export names a different class (identifier-style takes precedence).
305
+ if (isDefaultExport) {
306
+ const defKey = filename + "\0default";
307
+ classSuperclassNames[defKey] = superName;
308
+ ancestryCacheKeys[filename].add(defKey);
309
+ }
310
+ }
311
+ }
312
+
313
+ // PM3: handle identifier-style default exports: `export default ActualBase`.
314
+ // These are a separate statement from the class declaration, so the loop above misses them.
315
+ // Also fixes the sibling-collision issue: the sentinel must point to the class that is
316
+ // actually exported as default, not to whichever inline export default class appeared last.
317
+ // Also handles `export { Foo as default }` (ExportNamedDeclaration with a default specifier).
318
+ for (const stmt of programBody) {
319
+ let exportedName = null;
320
+ if (_core.types.isExportDefaultDeclaration(stmt) && _core.types.isIdentifier(stmt.declaration)) {
321
+ // `export default ActualBase`
322
+ exportedName = stmt.declaration.name;
323
+ } else if (_core.types.isExportNamedDeclaration(stmt)) {
324
+ // `export { Foo as default }` — specifier form
325
+ for (const sp of (_stmt$specifiers = stmt.specifiers) !== null && _stmt$specifiers !== void 0 ? _stmt$specifiers : []) {
326
+ var _stmt$specifiers;
327
+ if (_core.types.isExportSpecifier(sp) && sp.exported.name === "default") {
328
+ exportedName = sp.local.name;
329
+ break;
330
+ }
331
+ }
332
+ }
333
+ if (!exportedName) continue;
334
+ // N1: filesWithDefaultExport was already populated by the upfront scan above.
335
+ const superName = localClassSuperMap.get(exportedName);
336
+ // localClassSuperMap stores null for classes with no imported superclass (set above).
337
+ // Only record the "\0default" sentinel when there IS a superclass to follow.
338
+ if (superName) {
339
+ const defKey = filename + "\0default";
340
+ // Overwrite any sentinel from an inline export default class above.
341
+ classSuperclassNames[defKey] = superName;
342
+ // PM1: Set.add is idempotent — no need to guard against duplicates.
343
+ ancestryCacheKeys[filename] = ancestryCacheKeys[filename] || new Set();
344
+ ancestryCacheKeys[filename].add(defKey);
345
+ }
346
+ }
347
+ }
348
+
349
+ /**
350
+ * Read `absPath` from disk, parse it minimally, and populate the ancestry caches.
351
+ * Returns true if the file was read and recorded (or was already cached), false otherwise.
352
+ * Degrades silently on missing or unparseable files — never throws.
353
+ */
354
+ function readAndRecordFile(absPath) {
355
+ if (Object.prototype.hasOwnProperty.call(importDeclarations, absPath)) return true; // already cached
356
+ try {
357
+ const code = (0, _fs.readFileSync)(absPath, "utf8");
358
+ // Don't enable JSX for plain .ts files: Babel's JSX parser treats the legacy
359
+ // TypeScript type-assertion syntax `<SomeType>expr` as JSX and fails to parse it.
360
+ // Only enable JSX for .jsx/.tsx files where it is expected.
361
+ const isTS = /\.tsx?$/.test(absPath);
362
+ const isJSX = /\.[jt]sx$/.test(absPath);
363
+ const ast = (0, _core.parse)(code, {
364
+ sourceType: "module",
365
+ babelrc: false,
366
+ configFile: false,
367
+ parserOpts: {
368
+ errorRecovery: true,
369
+ plugins: [...(isTS ? ["typescript"] : []), ...(isJSX ? ["jsx"] : []), ["decorators", {
370
+ legacy: true
371
+ }], "classProperties"]
372
+ }
373
+ });
374
+ recordAncestry(absPath, ast.program.body);
375
+ // PM2: mark as on-demand so saveImports evicts it on the next rebuild,
376
+ // preventing a changed ancestor from serving stale cached data indefinitely.
377
+ onDemandCachedFiles.add(absPath);
378
+ return true;
379
+ } catch (err) {
380
+ if (err.code !== "ENOENT") {
381
+ // Swallow ENOENT silently (file missing or deleted before the read).
382
+ // Re-surface unexpected errors (permissions, parse failures, etc.).
383
+ console.warn(`[babel-plugin-ui5] Could not read ancestor file ${absPath}: ${err.message}`);
384
+ }
385
+ return false; // unparseable or missing ancestor → degrade, never throw
386
+ }
387
+ }
388
+ const saveImports = (file, additionalUI5Classes) => {
389
+ const filename = file.opts.filename;
390
+ // PM2: evict all on-demand (disk-read) ancestor cache entries once per rebuild cycle.
391
+ // Doing it once (guarded by onDemandEvictedThisCycle) rather than on every saveImports
392
+ // call avoids re-reading every shared ancestor file once per source file per rebuild.
393
+ // clearFileFromCache also removes the entry from onDemandCachedFiles, so a snapshot
394
+ // of the keys is taken first to avoid mutating the Set while iterating it.
395
+ if (!onDemandEvictedThisCycle) {
396
+ onDemandEvictedThisCycle = true;
397
+ for (const f of [...onDemandCachedFiles]) {
398
+ clearFileFromCache(f);
399
+ }
400
+ }
401
+ // Clear ALL stale cache entries for this file before re-populating, so watch-mode
402
+ // rebuilds never carry over data from a previous version of the file.
403
+ clearFileFromCache(filename);
404
+ recordAncestry(filename, file.ast.program.body);
405
+ // PM1: record matches for any import whose source is in additionalUI5Classes.
406
+ // This runs unconditionally — before any class-conversion decision — so opt-outs like
407
+ // autoConvertControllerClass:false don't suppress valid match recording and produce
408
+ // false "never matched" warnings.
409
+ if (additionalUI5Classes !== null && additionalUI5Classes !== void 0 && additionalUI5Classes.length) {
410
+ const additionalSet = new Set(additionalUI5Classes);
411
+ for (const decl of (_importDeclarations$f = importDeclarations[filename]) !== null && _importDeclarations$f !== void 0 ? _importDeclarations$f : []) {
412
+ var _importDeclarations$f;
413
+ if (additionalSet.has(decl.source.value)) {
414
+ recordAdditionalUI5ClassMatch(decl.source.value);
415
+ }
416
+ }
417
+ }
13
418
  };
14
419
 
15
- // can be called from visitor to access previously present declarations
420
+ /**
421
+ * Reset all ancestry caches to their initial state.
422
+ * Intended for programmatic/test callers that run several independent transforms
423
+ * in a single process and need a completely clean slate between runs.
424
+ * (The per-file `clearFileFromCache` is sufficient for watch-mode incremental eviction.)
425
+ */
16
426
  exports.saveImports = saveImports;
427
+ function resetAllCaches() {
428
+ importDeclarations = {};
429
+ superclassImportSources = {};
430
+ classSuperclassNames = {};
431
+ ancestryCacheKeys = {};
432
+ onDemandCachedFiles.clear();
433
+ onDemandEvictedThisCycle = false;
434
+ filesWithDefaultExport.clear();
435
+ matchedAdditionalUI5ClassPaths.clear();
436
+ }
437
+
438
+ /**
439
+ * Reset the per-cycle eviction flag after each file's post() hook.
440
+ * This allows the next watch cycle's first saveImports call to re-evict on-demand
441
+ * ancestor files that may have changed between builds.
442
+ */
443
+ function resetOnDemandEvictionCycle() {
444
+ onDemandEvictedThisCycle = false;
445
+ }
446
+
447
+ /**
448
+ * Remove all cache entries associated with `filename`.
449
+ * Call this when a file is renamed or deleted mid-watch to prevent stale ancestry
450
+ * data from persisting for the lifetime of the Babel process.
451
+ */
452
+ function clearFileFromCache(filename) {
453
+ delete importDeclarations[filename];
454
+ // PM1: use the per-file key index for O(own-keys) cleanup instead of scanning all entries.
455
+ for (const key of (_ancestryCacheKeys$fi = ancestryCacheKeys[filename]) !== null && _ancestryCacheKeys$fi !== void 0 ? _ancestryCacheKeys$fi : new Set()) {
456
+ var _ancestryCacheKeys$fi;
457
+ delete superclassImportSources[key];
458
+ delete classSuperclassNames[key];
459
+ }
460
+ delete ancestryCacheKeys[filename];
461
+ filesWithDefaultExport.delete(filename);
462
+ onDemandCachedFiles.delete(filename);
463
+ }
464
+
465
+ /**
466
+ * Record the superclass import source and superclass name for a class in a file.
467
+ * Called from the visitor when a class extending an import is encountered.
468
+ * Used later for transitive resolution.
469
+ *
470
+ * @param {string} filename Absolute path of the file being processed.
471
+ * @param {string} className The class's own local name (node.id.name).
472
+ * @param {string} superclassLocalName Local binding name of the superclass in this file.
473
+ * @param {string} superclassImportSource Module specifier the superclass was imported from.
474
+ */
475
+ function saveSuperclassImportSource(filename, className, superclassLocalName, superclassImportSource) {
476
+ if (filename && className && superclassLocalName && superclassImportSource) {
477
+ const srcKey = filename + "\0" + superclassLocalName;
478
+ const clsKey = filename + "\0" + className;
479
+ superclassImportSources[srcKey] = superclassImportSource;
480
+ classSuperclassNames[clsKey] = superclassLocalName;
481
+ // PM1: register keys so clearFileFromCache can find them without a full scan.
482
+ // Set.add is idempotent, so no explicit membership check is needed.
483
+ ancestryCacheKeys[filename] = ancestryCacheKeys[filename] || new Set();
484
+ ancestryCacheKeys[filename].add(srcKey);
485
+ ancestryCacheKeys[filename].add(clsKey);
486
+ }
487
+ }
488
+
489
+ // can be called from visitor to access previously present declarations
17
490
  function getImportDeclaration(filename, typeName) {
491
+ var _importDeclarations$f2;
18
492
  if (!filename || !typeName) {
19
493
  return null;
20
494
  }
21
495
  const typeNameParts = typeName.split(".");
22
496
 
23
497
  // find the declaration importing the typeName among the collected import declarations in this file
24
- const filteredDeclarations = importDeclarations[filename].filter(importDeclaration => {
498
+ const filteredDeclarations = ((_importDeclarations$f2 = importDeclarations[filename]) !== null && _importDeclarations$f2 !== void 0 ? _importDeclarations$f2 : []).filter(importDeclaration => {
25
499
  // each import declaration can import several entities, so let's check all of them
26
500
  for (let specifier of importDeclaration.specifiers) {
27
501
  if ((_core.types.isImportDefaultSpecifier(specifier) || _core.types.isImportNamespaceSpecifier(specifier)) && specifier.local.name === typeNameParts[0]) {
28
502
  // if the import is default, then the typeName should only have one part (the import name)
29
503
  return true;
30
- } else if (_core.types.isImportSpecifier(specifier) && specifier.imported.name === typeNameParts[typeNameParts.length - 1]) {
31
- // If the import is named, then the last part of the typeName should match the imported name
504
+ } else if (_core.types.isImportSpecifier(specifier) && specifier.local.name === typeNameParts[typeNameParts.length - 1]) {
505
+ // Match on the local binding name (what the code actually uses).
506
+ // For `import { Foo as Bar }`, local.name is "Bar" and imported.name is "Foo";
507
+ // callers always pass the name as it appears in the source (the local name).
32
508
  return true;
33
509
  }
34
510
  }
35
511
  });
36
512
  return filteredDeclarations[0]; // should be exactly one
513
+ }
514
+
515
+ /**
516
+ * Try to resolve a module import source (as written in the source, e.g. "my/app/BaseController")
517
+ * to the absolute filename key used in importDeclarations. Returns the key if a matching
518
+ * file was already processed in this Babel run or can be read from disk on demand, or null
519
+ * otherwise.
520
+ *
521
+ * Handles:
522
+ * - Relative imports ("./BaseController", "../controllers/Base") — resolved from currentFilename's dir
523
+ * - Module-path imports ("my/app/BaseController") — resolved from sourceRoot
524
+ * When namespacePrefix is set (e.g. "my.app"), module-path imports are first tried with
525
+ * the prefix stripped (dots→slashes), so "my/app/BaseController" → "BaseController"
526
+ * is resolved as sourceRoot + "/BaseController" rather than sourceRoot + "/my/app/BaseController".
527
+ * - Module-path imports into dependency sourceRoots (e.g. "my/dep/base/DepBase") — resolved
528
+ * by walking each entry in `dependencies`, inverting its sourceFolders map, and trying
529
+ * both the raw path and the prefix-stripped path under each dep folder's root.
530
+ *
531
+ * Third-party / SAP module paths like "sap/ui/core/mvc/Controller" are never on disk
532
+ * and will always return null (no key in importDeclarations, existsSync false).
533
+ */
534
+ function resolveImportSourceToFilename(importSource, currentFilename, {
535
+ sourceRoot,
536
+ namespacePrefix,
537
+ sourceFolders,
538
+ dependencies
539
+ } = {}) {
540
+ if (!importSource || !currentFilename) return null;
541
+ const extensions = [".js", ".ts", ".jsx", ".tsx", ".mjs"];
542
+ const bases = [];
543
+ if (importSource.startsWith("./") || importSource.startsWith("../")) {
544
+ bases.push(Path.resolve(Path.dirname(currentFilename), importSource));
545
+ } else if (sourceRoot) {
546
+ bases.push(Path.resolve(sourceRoot, importSource));
547
+ // When namespacePrefix is configured, module paths include the prefix as a leading
548
+ // path segment. Strip it so "my/app/controller/Base" resolves to
549
+ // sourceRoot/controller/Base instead of sourceRoot/my/app/controller/Base.
550
+ if (namespacePrefix) {
551
+ const prefixPath = namespacePrefix.replace(/\./g, "/");
552
+ if (importSource.startsWith(prefixPath + "/")) {
553
+ const stripped = importSource.slice(prefixPath.length + 1);
554
+ bases.push(Path.resolve(sourceRoot, stripped));
555
+ }
556
+ }
557
+ // When sourceFolders is configured (e.g. { "src": "my.app" }), each folder maps
558
+ // module paths through its own prefix. Try both the raw path and the prefix-stripped
559
+ // path under each folder's root so transitive resolution works without namespacePrefix.
560
+ if (sourceFolders && typeof sourceFolders === "object") {
561
+ for (const [folder, prefix] of Object.entries(sourceFolders)) {
562
+ const folderRoot = Path.resolve(sourceRoot, folder);
563
+ bases.push(Path.resolve(folderRoot, importSource));
564
+ if (prefix) {
565
+ const prefixPath = prefix.replace(/\./g, "/");
566
+ if (importSource.startsWith(prefixPath + "/")) {
567
+ const stripped = importSource.slice(prefixPath.length + 1);
568
+ bases.push(Path.resolve(folderRoot, stripped));
569
+ }
570
+ }
571
+ }
572
+ }
573
+ // Walk dependency sourceRoots: a module-path import like "my/dep/base/DepBase" may
574
+ // refer to a file in a dep project whose folder is mapped to a prefix in dep.sourceFolders.
575
+ // Invert each dep's sourceFolders map (prefix → folder) to strip the prefix and build
576
+ // the candidate path under the dep's physical folder.
577
+ if (Array.isArray(dependencies)) {
578
+ for (const dep of dependencies) {
579
+ if (!dep.sourceRoot) continue;
580
+ const depRoot = Path.resolve(dep.sourceRoot);
581
+ bases.push(Path.resolve(depRoot, importSource));
582
+ if (dep.namespacePrefix) {
583
+ const prefixPath = dep.namespacePrefix.replace(/\./g, "/");
584
+ if (importSource.startsWith(prefixPath + "/")) {
585
+ const stripped = importSource.slice(prefixPath.length + 1);
586
+ bases.push(Path.resolve(depRoot, stripped));
587
+ }
588
+ }
589
+ const depSf = dep.sourceFolders;
590
+ if (depSf && typeof depSf === "object") {
591
+ for (const [folder, prefix] of Object.entries(depSf)) {
592
+ const folderRoot = Path.resolve(depRoot, folder);
593
+ bases.push(Path.resolve(folderRoot, importSource));
594
+ if (prefix) {
595
+ const prefixPath = prefix.replace(/\./g, "/");
596
+ if (importSource.startsWith(prefixPath + "/")) {
597
+ const stripped = importSource.slice(prefixPath.length + 1);
598
+ bases.push(Path.resolve(folderRoot, stripped));
599
+ }
600
+ }
601
+ }
602
+ }
603
+ }
604
+ }
605
+ }
606
+ const has = p => Object.prototype.hasOwnProperty.call(importDeclarations, p);
607
+ for (const base of bases) {
608
+ if (has(base)) return base;
609
+ for (const ext of extensions) {
610
+ const candidate = base + ext;
611
+ if (has(candidate)) return candidate;
612
+ }
613
+ for (const ext of extensions) {
614
+ const candidate = base + "/index" + ext;
615
+ if (has(candidate)) return candidate;
616
+ }
617
+ }
618
+ // Nothing cached — try reading candidate files from disk on demand.
619
+ // This makes transitive detection order-independent: a descendant processed before
620
+ // its ancestor can still discover the ancestor's superclass chain via a disk read.
621
+ // Bare module specifiers (e.g. "sap/ui/core/mvc/Controller") never reach here because
622
+ // isSuperclassTransitivelyKnown checks knownPaths.has(importSource) first,
623
+ // before resolveImportSourceToFilename is ever called.
624
+ for (const base of bases) {
625
+ // F1: try the exact path first — handles explicit-extension imports like "./Base.js"
626
+ // where `base` is already the full resolved path and appending an extension would be wrong.
627
+ if (readAndRecordFile(base)) return base;
628
+ for (const ext of extensions) {
629
+ const candidate = base + ext;
630
+ if (readAndRecordFile(candidate)) return candidate;
631
+ }
632
+ // F2: directory index — handles `import Base from "./controllers"` → controllers/index.js
633
+ for (const ext of extensions) {
634
+ const candidate = base + "/index" + ext;
635
+ if (readAndRecordFile(candidate)) return candidate;
636
+ }
637
+ }
638
+ return null;
639
+ }
640
+
641
+ /**
642
+ * Walk every intermediate local ancestor in the superclass chain and return false if any
643
+ * would not itself be convertible to .extend() — i.e. has an empty namespace and is not
644
+ * a .controller.(js|ts) file. Stops when a known SAP/UI5 path is reached.
645
+ *
646
+ * Fixes N2 for chains of depth > 1: a chain
647
+ * MyCtrl → LocalA (has namespace) → LocalMid (empty namespace) → SAP Controller
648
+ * is rejected because LocalMid would remain a native ES class at runtime.
649
+ *
650
+ * @param {string} startImportSource Import source of the direct ancestor.
651
+ * @param {string} currentFile Absolute path of the file being converted.
652
+ * @param {string} sourceRoot Resolved absolute source root.
653
+ * @param {string|null} namespacePrefix Effective namespace prefix for currentFile.
654
+ * @param {Set<string>} knownPaths SAP/UI5 known module paths (stop condition).
655
+ * @param {object} opts Full plugin options.
656
+ * @returns {boolean} true if all intermediate locals are convertible, false otherwise.
657
+ */
658
+ function isLocalChainConvertible(startImportSource, currentFile, sourceRoot, namespacePrefix, knownPaths, opts) {
659
+ let importSource = startImportSource;
660
+ let fromFile = currentFile;
661
+ const visited = new Set();
662
+ while (true) {
663
+ var _opts$sourceFolders, _opts$dependencies;
664
+ if (knownPaths.has(importSource)) return true;
665
+ const ancestorFile = resolveImportSourceToFilename(importSource, fromFile, {
666
+ sourceRoot,
667
+ namespacePrefix,
668
+ sourceFolders: (_opts$sourceFolders = opts.sourceFolders) !== null && _opts$sourceFolders !== void 0 ? _opts$sourceFolders : null,
669
+ dependencies: (_opts$dependencies = opts.dependencies) !== null && _opts$dependencies !== void 0 ? _opts$dependencies : null
670
+ });
671
+ if (!ancestorFile) return true; // can't resolve → allow
672
+
673
+ if (visited.has(ancestorFile)) return true;
674
+ visited.add(ancestorFile);
675
+ const ancestorNamespace = deriveNamespaceForFile(ancestorFile, sourceRoot, opts);
676
+ const isControllerFile = opts.autoConvertUI5FileTypes !== false && opts.autoConvertControllerClass !== false && /[.]controller[.](js|ts)$/.test(Path.basename(ancestorFile));
677
+ if (!ancestorNamespace && !isControllerFile) {
678
+ return false;
679
+ }
680
+
681
+ // Find what this ancestor's exported class extends by looking in the cache.
682
+ // Try "\0default" sentinel first. If the file has an explicit default export but no
683
+ // sentinel, the default-exported class has no UI5 superclass — stop here (return true:
684
+ // we cannot verify further, but the namespace check above already passed for this hop).
685
+ // Only fall back to the key scan when the file has NO explicit default export at all
686
+ // (bare class / module-interop), to avoid picking a sibling class's ancestry chain.
687
+ // This mirrors the filesWithDefaultExport guard in isSuperclassTransitivelyKnown.
688
+ const defSuperName = classSuperclassNames[ancestorFile + "\0default"];
689
+ if (defSuperName) {
690
+ const nextSrc = superclassImportSources[ancestorFile + "\0" + defSuperName];
691
+ if (!nextSrc) return true;
692
+ importSource = nextSrc;
693
+ fromFile = ancestorFile;
694
+ continue;
695
+ }
696
+ if (filesWithDefaultExport.has(ancestorFile)) return true;
697
+ // No explicit default export — key-scan fallback is safe (interop / bare class declaration).
698
+ let nextSrc = null;
699
+ for (const key of (_ancestryCacheKeys$an = ancestryCacheKeys[ancestorFile]) !== null && _ancestryCacheKeys$an !== void 0 ? _ancestryCacheKeys$an : []) {
700
+ var _ancestryCacheKeys$an;
701
+ const src = superclassImportSources[key];
702
+ if (src !== undefined) {
703
+ nextSrc = src;
704
+ break;
705
+ }
706
+ }
707
+ if (!nextSrc) return true;
708
+ importSource = nextSrc;
709
+ fromFile = ancestorFile;
710
+ }
711
+ }
712
+
713
+ /**
714
+ * Walk the superclass import chain transitively to check whether any ancestor class
715
+ * is imported from a path in knownPaths. Cycle detection is handled by the `_visited`
716
+ * Set (keyed by "filename\0superclassLocalName"), so circular inheritance chains never
717
+ * cause infinite recursion.
718
+ *
719
+ * Ancestor files are populated via two paths:
720
+ * 1. Eagerly, when Babel opens the file: the `pre` hook calls saveImports, which calls
721
+ * recordAncestry. This is the fast path; file-processing order doesn't matter here.
722
+ * 2. On demand, when the ancestor hasn't been seen yet: resolveImportSourceToFilename
723
+ * tries to read and parse the ancestor file from disk (readAndRecordFile). This
724
+ * makes transitive detection fully order-independent.
725
+ *
726
+ * Example chain:
727
+ * MyController.js extends BaseController → import "my/app/BaseController"
728
+ * BaseController.js extends Controller → import "sap/ui/core/mvc/Controller" ✓ known
729
+ *
730
+ * Called with:
731
+ * isSuperclassTransitivelyKnown("MyController.js", "BaseController", knownPaths, sourceRoot, opts)
732
+ * → returns true
733
+ *
734
+ * @param {string} filename Absolute path of the file being checked.
735
+ * @param {string} superclassLocalName Local binding name of the superclass in `filename`.
736
+ * @param {Set<string>} knownPaths Module paths that count as "known UI5 base classes".
737
+ * @param {string} sourceRoot Resolved absolute source root for the project.
738
+ * @param {object} opts Plugin options subset: { namespacePrefix, sourceFolders, dependencies }.
739
+ * @param {Set} [_visited] Internal cycle-detection set; callers omit this.
740
+ */
741
+ function isSuperclassTransitivelyKnown(filename, superclassLocalName, knownPaths, sourceRoot, opts = {}, _visited = null) {
742
+ const {
743
+ namespacePrefix = null,
744
+ sourceFolders = null,
745
+ dependencies = null
746
+ } = opts;
747
+ if (!filename || !superclassLocalName || !(knownPaths !== null && knownPaths !== void 0 && knownPaths.size)) {
748
+ return null;
749
+ }
750
+ const visited = _visited !== null && _visited !== void 0 ? _visited : new Set();
751
+ const key = filename + "\0" + superclassLocalName;
752
+ if (visited.has(key)) return null; // cycle detected
753
+ visited.add(key);
754
+ const importDecl = getImportDeclaration(filename, superclassLocalName);
755
+ if (!importDecl) return null;
756
+ const importSource = importDecl.source.value;
757
+
758
+ // Direct hit — immediate superclass is in the known list; return the matched path
759
+ if (knownPaths.has(importSource)) return importSource;
760
+
761
+ // Try to resolve to a local file processed earlier in this Babel run
762
+ const resolvedFile = resolveImportSourceToFilename(importSource, filename, {
763
+ sourceRoot,
764
+ namespacePrefix,
765
+ sourceFolders,
766
+ dependencies
767
+ });
768
+ if (!resolvedFile) return null;
769
+
770
+ // Look up what that file's class itself extends (stored by saveSuperclassImportSource).
771
+ //
772
+ // N1: For default imports the descendant's local alias (superclassLocalName) is irrelevant
773
+ // to the ancestry stored in the resolved file — prefer the "\0default" sentinel, which
774
+ // always points to the class actually exported as default. Only fall back to by-name lookup
775
+ // when the ancestor file has NO explicit default export at all (bare class declaration
776
+ // without any export default, consumed via module interop). When a file HAS an export
777
+ // default the sentinel is authoritative: if it's absent, the exported class has no
778
+ // convertible UI5 ancestry. This prevents a local sibling class sharing the import alias
779
+ // from being matched instead of the actual default export (including the case where the
780
+ // default-exported class has no superclass — which previously left no sentinel, causing
781
+ // the by-name fallback to fire against the wrong sibling).
782
+ // For named imports the by-name lookup is always correct.
783
+ const isDefaultImport = importDecl.specifiers.some(s => _core.types.isImportDefaultSpecifier(s));
784
+ let ancestorSuperLocalName;
785
+ if (isDefaultImport) {
786
+ // Prefer the "\0default" sentinel.
787
+ // Fall back to by-name ONLY when no explicit default export exists (interop case).
788
+ const hasSentinel = resolvedFile + "\0default" in classSuperclassNames;
789
+ const hasDefaultExport = filesWithDefaultExport.has(resolvedFile);
790
+ if (hasDefaultExport) {
791
+ // File has an explicit default export — sentinel is authoritative.
792
+ // If the sentinel is absent it means the default-exported class has no UI5 ancestry.
793
+ ancestorSuperLocalName = classSuperclassNames[resolvedFile + "\0default"];
794
+ } else {
795
+ // No explicit default export — by-name fallback is safe (interop / bare class).
796
+ ancestorSuperLocalName = hasSentinel ? classSuperclassNames[resolvedFile + "\0default"] : classSuperclassNames[resolvedFile + "\0" + superclassLocalName];
797
+ }
798
+ } else {
799
+ var _namedSpecifier$impor, _namedSpecifier$impor2;
800
+ // Named import: the class name in the ancestor file is the *imported* name, not the
801
+ // local alias. For `import { Foo as Bar }`, the cache key was stored as "Foo" but
802
+ // superclassLocalName is "Bar". Extract the imported name from the specifier.
803
+ const namedSpecifier = importDecl.specifiers.find(s => _core.types.isImportSpecifier(s) && s.local.name === superclassLocalName);
804
+ const ancestorClassName = (_namedSpecifier$impor = namedSpecifier === null || namedSpecifier === void 0 || (_namedSpecifier$impor2 = namedSpecifier.imported) === null || _namedSpecifier$impor2 === void 0 ? void 0 : _namedSpecifier$impor2.name) !== null && _namedSpecifier$impor !== void 0 ? _namedSpecifier$impor : superclassLocalName;
805
+ ancestorSuperLocalName = classSuperclassNames[resolvedFile + "\0" + ancestorClassName];
806
+ }
807
+ if (!ancestorSuperLocalName) return null;
808
+ const ancestorSuperImportSource = superclassImportSources[resolvedFile + "\0" + ancestorSuperLocalName];
809
+ if (!ancestorSuperImportSource) return null;
810
+
811
+ // One-hop hit: ancestor's superclass is directly known; return the matched path
812
+ if (knownPaths.has(ancestorSuperImportSource)) return ancestorSuperImportSource;
813
+ return isSuperclassTransitivelyKnown(resolvedFile, ancestorSuperLocalName, knownPaths, sourceRoot, opts, visited);
37
814
  }