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 +83 -0
- package/dist/classes/helpers/classes.js +57 -14
- package/dist/classes/helpers/decorators.js +23 -1
- package/dist/classes/helpers/imports.js +783 -6
- package/dist/classes/helpers/jsdoc.js +10 -0
- package/dist/classes/pre.js +7 -6
- package/dist/classes/visitor.js +197 -8
- package/dist/index.js +52 -2
- package/dist/modules/helpers/exports.js +11 -2
- package/dist/modules/helpers/wrapper.js +9 -3
- package/dist/modules/visitor.js +32 -13
- package/dist/utils/ast.js +16 -3
- package/package.json +2 -2
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
31
|
-
//
|
|
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
|
}
|