@eclipse-scout/cli 25.1.14 → 25.2.0-beta.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/README.md +2 -2
- package/package.json +5 -4
- package/scripts/DataObjectTransformer.js +293 -0
- package/scripts/ModuleDetector.js +84 -0
- package/scripts/ModuleNamespaceResolver.js +183 -0
- package/scripts/webpack-defaults.js +44 -11
package/README.md
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
6
|
-
<a href="https://ci.eclipse.org/scout/job/scout-integration-25.
|
|
7
|
-
<a href="https://ci.eclipse.org/scout/job/scout-integration-25.
|
|
6
|
+
<a href="https://ci.eclipse.org/scout/job/scout-integration-25.2-RT-nightly_pipeline/" target="_blank" rel="noopener noreferrer"><img alt="Jenkins" src="https://img.shields.io/jenkins/build?jobUrl=https%3A%2F%2Fci.eclipse.org%2Fscout%2Fjob%2Fscout-integration-25.2-RT-nightly_pipeline%2F"></a>
|
|
7
|
+
<a href="https://ci.eclipse.org/scout/job/scout-integration-25.2-RT-nightly_pipeline/" target="_blank" rel="noopener noreferrer"><img alt="Jenkins tests" src="https://img.shields.io/jenkins/tests?jobUrl=https%3A%2F%2Fci.eclipse.org%2Fscout%2Fjob%2Fscout-integration-25.2-RT-nightly_pipeline%2F"></a>
|
|
8
8
|
<a href="https://www.npmjs.com/package/@eclipse-scout/cli" target="_blank" rel="noopener noreferrer"><img alt="npm" src="https://img.shields.io/npm/dm/@eclipse-scout/cli"></a>
|
|
9
9
|
<a href="https://www.eclipse.org/legal/epl-2.0/" target="_blank" rel="noopener noreferrer"><img alt="NPM" src="https://img.shields.io/npm/l/@eclipse-scout/cli"></a>
|
|
10
10
|
<a href="https://www.npmjs.com/package/@eclipse-scout/cli" target="_blank" rel="noopener noreferrer"><img alt="npm (scoped)" src="https://img.shields.io/npm/v/@eclipse-scout/cli"></a>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eclipse-scout/cli",
|
|
3
|
-
"version": "25.
|
|
3
|
+
"version": "25.2.0-beta.0",
|
|
4
4
|
"description": "CLI for Eclipse Scout",
|
|
5
5
|
"author": "BSI Business Systems Integration AG",
|
|
6
6
|
"homepage": "https://eclipse.dev/scout/",
|
|
@@ -45,8 +45,8 @@
|
|
|
45
45
|
"karma-jasmine": "5.1.0",
|
|
46
46
|
"karma-jasmine-ajax": "0.1.13",
|
|
47
47
|
"@metahub/karma-jasmine-jquery": "4.0.1",
|
|
48
|
-
"@eclipse-scout/karma-jasmine-scout": "25.
|
|
49
|
-
"@eclipse-scout/tsconfig": "25.
|
|
48
|
+
"@eclipse-scout/karma-jasmine-scout": "25.2.0-beta.0",
|
|
49
|
+
"@eclipse-scout/tsconfig": "25.2.0-beta.0",
|
|
50
50
|
"karma-jasmine-html-reporter": "2.1.0",
|
|
51
51
|
"karma-junit-reporter": "2.0.1",
|
|
52
52
|
"karma-webpack": "5.0.1",
|
|
@@ -72,6 +72,7 @@
|
|
|
72
72
|
"version:snapshot:dependencies": "releng-scripts version:snapshot:dependencies",
|
|
73
73
|
"version:snapshot": "releng-scripts version:snapshot",
|
|
74
74
|
"version:release:dependencies": "releng-scripts version:release:dependencies",
|
|
75
|
-
"version:release": "releng-scripts version:release"
|
|
75
|
+
"version:release": "releng-scripts version:release",
|
|
76
|
+
"test": "node test/runTests.js"
|
|
76
77
|
}
|
|
77
78
|
}
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2010, 2025 BSI Business Systems Integration AG
|
|
3
|
+
*
|
|
4
|
+
* This program and the accompanying materials are made
|
|
5
|
+
* available under the terms of the Eclipse Public License 2.0
|
|
6
|
+
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
|
7
|
+
*
|
|
8
|
+
* SPDX-License-Identifier: EPL-2.0
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const ts = require('typescript');
|
|
12
|
+
const ModuleDetector = require('./ModuleDetector');
|
|
13
|
+
const CONSTANT_PATTERN = new RegExp('^[A-Z_0-9]+$');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* TypeScript transformer for DataObject which injects @Reflect.metadata('scout.m.t', dataType) decorators to all DO attributes.
|
|
17
|
+
* These decorators store the data type information from the TS property declaration.
|
|
18
|
+
* This allows to transfer the TypeScript data type information to the runtime which is used when (de)serializing a data object.
|
|
19
|
+
*
|
|
20
|
+
* See https://github.com/itsdouges/typescript-transformer-handbook
|
|
21
|
+
*/
|
|
22
|
+
module.exports = class DataObjectTransformer {
|
|
23
|
+
|
|
24
|
+
constructor(program, context, namespaceResolver) {
|
|
25
|
+
this.program = program;
|
|
26
|
+
this.context = context;
|
|
27
|
+
this.moduleDetector = null; // created on first use
|
|
28
|
+
this.doInventoryAddStatements = [];
|
|
29
|
+
this.namespaceResolver = namespaceResolver;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
transform(node) {
|
|
33
|
+
if (ts.isSourceFile(node)) {
|
|
34
|
+
let transformedFile = this._visitChildren(node); // step into top level source files
|
|
35
|
+
if (this.doInventoryAddStatements.length) {
|
|
36
|
+
// add auto-register DO statements to the source file end
|
|
37
|
+
const statements = [...transformedFile.statements, ...this.doInventoryAddStatements];
|
|
38
|
+
transformedFile = ts.factory.updateSourceFile(node, statements,
|
|
39
|
+
transformedFile.isDeclarationFile, transformedFile.referencedFiles, transformedFile.typeReferenceDirectives, transformedFile.hasNoDefaultLib, transformedFile.libReferenceDirectives);
|
|
40
|
+
this.doInventoryAddStatements = []; // clear added statements
|
|
41
|
+
}
|
|
42
|
+
this.moduleDetector = null; // forget cached types for this file
|
|
43
|
+
return transformedFile;
|
|
44
|
+
}
|
|
45
|
+
if (ts.isClassDeclaration(node)) {
|
|
46
|
+
const typeNameDecorator = node.modifiers?.find(m => ts.isDecorator(m) && m.expression?.expression?.escapedText === 'typeName');
|
|
47
|
+
if (typeNameDecorator) {
|
|
48
|
+
// it is a data object: remember DataObjectInventory.add statement to add at the end to the source file
|
|
49
|
+
const className = node.localSymbol.escapedName;
|
|
50
|
+
const typeName = this._getTypeNameFromDecorator(typeNameDecorator);
|
|
51
|
+
const namespace = this._detectExportInfoFor(node).namespace; // detectExportInfoOf() will not find anything because a ClassDeclaration node is passed here. But this is fine as the namespace of the own module is required here.
|
|
52
|
+
this.doInventoryAddStatements.push(this._createDoInventoryAddStatement(className, typeName, namespace));
|
|
53
|
+
return this._visitChildren(node); // step into DO with typeName decorator
|
|
54
|
+
}
|
|
55
|
+
return node; // no need to step into
|
|
56
|
+
}
|
|
57
|
+
if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node) || ts.isVariableStatement(node) || ts.isIdentifier(node) || ts.isTypeReferenceNode(node)
|
|
58
|
+
|| ts.isPropertySignature(node) || ts.isStringLiteral(node) || ts.isInterfaceDeclaration(node) || ts.isPropertyAssignment(node) || ts.isObjectLiteralExpression(node)
|
|
59
|
+
|| ts.isPropertyAccessExpression(node) || ts.isTypeAliasDeclaration(node) || ts.isParameter(node) || ts.isEnumDeclaration(node)
|
|
60
|
+
|| ts.isCallExpression(node) || ts.isExpressionStatement(node) || ts.isDecorator(node) || node.kind === ts.SyntaxKind.ExportKeyword) {
|
|
61
|
+
return node; // no need to step into
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (ts.isPropertyDeclaration(node) && !this._isSkipProperty(node)) {
|
|
65
|
+
const newModifiers = [
|
|
66
|
+
...(node.modifiers || []), // existing
|
|
67
|
+
...this._createMetaDataDecoratorsFor(node) // newly added
|
|
68
|
+
];
|
|
69
|
+
return ts.factory.replaceDecoratorsAndModifiers(node, newModifiers);
|
|
70
|
+
}
|
|
71
|
+
return node; // no need to step into
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Reads the value passed to the typeName decorator. Supports direct string literals and references to constants
|
|
76
|
+
* @param typeNameDecorator {ts.Decorator}
|
|
77
|
+
* @returns {string|null}
|
|
78
|
+
*/
|
|
79
|
+
_getTypeNameFromDecorator(typeNameDecorator) {
|
|
80
|
+
const decoratorArgument = typeNameDecorator.expression?.arguments?.[0];
|
|
81
|
+
if (decoratorArgument && ts.isStringLiteral(decoratorArgument)) {
|
|
82
|
+
return decoratorArgument.text;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// might be a reference to a constant
|
|
86
|
+
let constant = decoratorArgument?.flowNode?.node;
|
|
87
|
+
if (constant && ts.isVariableDeclaration(constant)) {
|
|
88
|
+
const initializer = constant.initializer;
|
|
89
|
+
if (initializer && ts.isStringLiteral(initializer)) {
|
|
90
|
+
return initializer.text;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return null; // rely on the auto-detection of the Scout RT.
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @param node {ts.PropertyDeclaration} the node for the DO property.
|
|
99
|
+
* @returns {ts.Decorator[]}
|
|
100
|
+
*/
|
|
101
|
+
_createMetaDataDecoratorsFor(node) {
|
|
102
|
+
if (!node.type) {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
return [this._createMetaDataDecorator(this._createTypeNode(node.type))];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Creates the value node to be added to the @Reflect.metadata('scout.m.t', valueNode) decorator.
|
|
110
|
+
* @param typeNode {ts.TypeNode} The data type node of a DO property.
|
|
111
|
+
* @returns {ts.StringLiteral|ts.Identifier|ts.ObjectLiteralExpression}
|
|
112
|
+
* A constructor reference identifier for primitives like number, string, boolean, object, unknown, any, void, etc.
|
|
113
|
+
* A constructor reference identifier for built-in types like Map, Set, Date.
|
|
114
|
+
* A RawFieldMetaData object literal for arrays (having Array type and the element type as type arguments: Array<ElementType>).
|
|
115
|
+
* A string literal for string literal types as e.g. used in IDs: UuId<'scout.SimpleUuid'>.
|
|
116
|
+
* A string literal for custom type references not having type arguments in the form of a Scout objectType: 'myApp.MySpecialDo'.
|
|
117
|
+
* A RawFieldMetaData object literal for type references having type arguments.
|
|
118
|
+
*/
|
|
119
|
+
_createTypeNode(typeNode) {
|
|
120
|
+
if (typeNode.kind === ts.SyntaxKind.NumberKeyword) {
|
|
121
|
+
// primitive number
|
|
122
|
+
return ts.factory.createIdentifier('Number');
|
|
123
|
+
}
|
|
124
|
+
if (typeNode.kind === ts.SyntaxKind.StringKeyword) {
|
|
125
|
+
// primitive string
|
|
126
|
+
return ts.factory.createIdentifier('String');
|
|
127
|
+
}
|
|
128
|
+
if (typeNode.kind === ts.SyntaxKind.BooleanKeyword) {
|
|
129
|
+
// primitive boolean
|
|
130
|
+
return ts.factory.createIdentifier('Boolean');
|
|
131
|
+
}
|
|
132
|
+
// bigint is not yet supported as it is only part of ES2020 while Scout still uses ES2019
|
|
133
|
+
|
|
134
|
+
if (ts.isArrayTypeNode(typeNode)) {
|
|
135
|
+
// treat Obj[] like Array<Obj>
|
|
136
|
+
const objectType = ts.factory.createIdentifier('Array');
|
|
137
|
+
const elementType = this._createTypeNode(typeNode.elementType);
|
|
138
|
+
return this._createFieldMetaData(objectType, [elementType]);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (ts.isTypeReferenceNode(typeNode)) {
|
|
142
|
+
const objectType = this._createTypeReferenceNode(typeNode);
|
|
143
|
+
if (!typeNode.typeArguments?.length) {
|
|
144
|
+
// no type arguments: directly use the type reference (constructor ref or objectType string)
|
|
145
|
+
return objectType;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// types with typeArguments like Map<string, number> or Array<MyObject>
|
|
149
|
+
const typeArgsNodes = typeNode.typeArguments.map(typeArg => this._createTypeNode(typeArg));
|
|
150
|
+
return this._createFieldMetaData(objectType, typeArgsNodes);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// // literal types like e.g. in IDs: UuId<'scout.SimpleUuid'>
|
|
154
|
+
if (ts.isLiteralTypeNode(typeNode)) {
|
|
155
|
+
if (ts.isStringLiteral(typeNode.literal)) {
|
|
156
|
+
return ts.factory.createStringLiteral(typeNode.literal.text);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return ts.factory.createIdentifier('Object'); // e.g. any, void, unknown
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Creates a field metadata object. See RawFieldMetaData type.
|
|
165
|
+
* <pre>
|
|
166
|
+
* {
|
|
167
|
+
* objectType: 'myApp.MySpecialDo',
|
|
168
|
+
* typeArgs: [...]
|
|
169
|
+
* }
|
|
170
|
+
* </pre>
|
|
171
|
+
* @returns {ts.ObjectLiteralExpression}
|
|
172
|
+
*/
|
|
173
|
+
_createFieldMetaData(objectType, typeArgsNodes) {
|
|
174
|
+
return ts.factory.createObjectLiteralExpression([
|
|
175
|
+
ts.factory.createPropertyAssignment('objectType', objectType),
|
|
176
|
+
ts.factory.createPropertyAssignment('typeArgs', ts.factory.createArrayLiteralExpression(typeArgsNodes, false))
|
|
177
|
+
], false);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Creates a node for a type reference.
|
|
182
|
+
*
|
|
183
|
+
* This is directly the constructor reference for built-in types like Date, Number, String, Map, Set, etc.
|
|
184
|
+
* Or a string with the Scout objectType for the type (e.g. 'myApp.MySpecialDo').
|
|
185
|
+
*
|
|
186
|
+
* @param node {ts.TypeReferenceNode}
|
|
187
|
+
* @returns {ts.StringLiteral|ts.Identifier}
|
|
188
|
+
*/
|
|
189
|
+
_createTypeReferenceNode(node) {
|
|
190
|
+
const name = node.typeName.escapedText;
|
|
191
|
+
if (global[name]) {
|
|
192
|
+
return ts.factory.createIdentifier(name); // Use directly the constructor for known types like Date, Number, String, Boolean, Map, Set, Array
|
|
193
|
+
}
|
|
194
|
+
if ('Record' === name) {
|
|
195
|
+
return ts.factory.createStringLiteral(name);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const exportInfo = this._detectExportInfoFor(node);
|
|
199
|
+
const qualifiedName = this._createObjectType(exportInfo.namespace, exportInfo.exportName);
|
|
200
|
+
// use objectType as string because e.g. of TS interfaces (which do not exist at RT) and that overwrites in ObjectFactory are taken into account.
|
|
201
|
+
return ts.factory.createStringLiteral(qualifiedName);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Creates the qualified object type (e.g. 'myApp.MyDataObject').
|
|
206
|
+
* @param namespace {string} the namespace of the class
|
|
207
|
+
* @param className {string} the name of the class
|
|
208
|
+
* @returns {string} The objectType
|
|
209
|
+
*/
|
|
210
|
+
_createObjectType(namespace, className) {
|
|
211
|
+
return (!namespace || namespace === 'scout') ? className : namespace + '.' + className;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* 1. Checks in which Node module the type referenced by typeNode is declared.
|
|
216
|
+
* 2. Resolves the Scout JS namespace and exported name of the referenced type.
|
|
217
|
+
*
|
|
218
|
+
* @param typeNode {ts.TypeReferenceNode} The type reference for which the namespace it is declared in should be resolved.
|
|
219
|
+
* @returns {{namespace: string|null;exportName: string|null}} The namespace of the module that contains the given type.
|
|
220
|
+
*/
|
|
221
|
+
_detectExportInfoFor(typeNode) {
|
|
222
|
+
if (!this.moduleDetector) {
|
|
223
|
+
this.moduleDetector = new ModuleDetector(typeNode);
|
|
224
|
+
}
|
|
225
|
+
const exportInfo = this.moduleDetector.detectExportInfoOf(typeNode);
|
|
226
|
+
const namespace = this.namespaceResolver.resolveNamespace(exportInfo?.module, this.moduleDetector.sourceFile.fileName);
|
|
227
|
+
return {namespace, exportName: exportInfo?.exportName || typeNode?.typeName?.escapedText};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Creates a metadata decorator:
|
|
232
|
+
* <pre>
|
|
233
|
+
* @Reflect.metadata('scout.m.t', valueNode)
|
|
234
|
+
* </pre>
|
|
235
|
+
* @param valueNode The value of the decorator.
|
|
236
|
+
* @returns {ts.Decorator}
|
|
237
|
+
*/
|
|
238
|
+
_createMetaDataDecorator(valueNode) {
|
|
239
|
+
const reflect = ts.factory.createIdentifier('Reflect');
|
|
240
|
+
const reflectMetaData = ts.factory.createPropertyAccessExpression(reflect, ts.factory.createIdentifier('metadata'));
|
|
241
|
+
const keyNode = ts.factory.createStringLiteral('scout.m.t');
|
|
242
|
+
const call = ts.factory.createCallExpression(reflectMetaData, undefined, [keyNode, valueNode]);
|
|
243
|
+
return ts.factory.createDecorator(call);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Skips properties which are static, protected or the name starts with '$' or '_'.
|
|
248
|
+
* Furthermore, constant like properties (consisting of uppercase chars, numbers and underscore) are skipped as well.
|
|
249
|
+
* @returns {boolean}
|
|
250
|
+
*/
|
|
251
|
+
_isSkipProperty(node) {
|
|
252
|
+
const propertyName = node.symbol?.escapedName;
|
|
253
|
+
if (!propertyName || propertyName.startsWith('_') || propertyName.startsWith('$') || CONSTANT_PATTERN.test(propertyName)) {
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
return !!node.modifiers?.some(n => n.kind === ts.SyntaxKind.StaticKeyword || n.kind === ts.SyntaxKind.ProtectedKeyword);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
_visitChildren(node) {
|
|
260
|
+
return ts.visitEachChild(node, n => this.transform(n), this.context);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Creates an ExpressionStatement calling DataObjectInventory.add for the given data object name.
|
|
265
|
+
* Creates the following call:
|
|
266
|
+
* <pre>
|
|
267
|
+
* window['scout']['DataObjectInventory'].get().add(className, typeName, objectType);
|
|
268
|
+
* </pre>
|
|
269
|
+
* @param className {string} The local class name of the data object.
|
|
270
|
+
* @param typeName {string|null} The typeName value of the data object (as detected from the @typeName decorator)
|
|
271
|
+
* @param namespace {string|null} The namespace of the current module. Used to build the objectType of the class.
|
|
272
|
+
* @returns {ts.ExpressionStatement}
|
|
273
|
+
*/
|
|
274
|
+
_createDoInventoryAddStatement(className, typeName, namespace) {
|
|
275
|
+
if (!className) {
|
|
276
|
+
throw new Error('DataObjectInventory.add not supported for anonymous data objects.');
|
|
277
|
+
}
|
|
278
|
+
const win = ts.factory.createIdentifier('window');
|
|
279
|
+
const scout = ts.factory.createElementAccessExpression(win, ts.factory.createStringLiteral('scout'));
|
|
280
|
+
const doInventory = ts.factory.createElementAccessExpression(scout, ts.factory.createStringLiteral('DataObjectInventory'));
|
|
281
|
+
const get = ts.factory.createPropertyAccessExpression(doInventory, 'get');
|
|
282
|
+
const getCall = ts.factory.createCallExpression(get, undefined, undefined);
|
|
283
|
+
const add = ts.factory.createPropertyAccessExpression(getCall, 'add');
|
|
284
|
+
|
|
285
|
+
const objectType = this._createObjectType(namespace, className);
|
|
286
|
+
const addCall = ts.factory.createCallExpression(add, undefined, [
|
|
287
|
+
ts.factory.createIdentifier(className),
|
|
288
|
+
typeName ? ts.factory.createStringLiteral(typeName) : ts.factory.createNull(),
|
|
289
|
+
ts.factory.createStringLiteral(objectType)
|
|
290
|
+
]);
|
|
291
|
+
return ts.factory.createExpressionStatement(addCall);
|
|
292
|
+
}
|
|
293
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2010, 2025 BSI Business Systems Integration AG
|
|
3
|
+
*
|
|
4
|
+
* This program and the accompanying materials are made
|
|
5
|
+
* available under the terms of the Eclipse Public License 2.0
|
|
6
|
+
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
|
7
|
+
*
|
|
8
|
+
* SPDX-License-Identifier: EPL-2.0
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const ts = require('typescript');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Class to compute the external module name (e.g. '@eclipse-scout/core') of types used in the TS file given.
|
|
15
|
+
*
|
|
16
|
+
* It parses all imports in the file and stores all names imported from external modules together with the module the name is declared in.
|
|
17
|
+
* External module means an import from a module different from the one this files belongs to.
|
|
18
|
+
*/
|
|
19
|
+
module.exports = class ModuleDetector {
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param node A node in the TS file for which the module detector should be created. All external modules used (imported) in this file are stored.
|
|
23
|
+
*/
|
|
24
|
+
constructor(node) {
|
|
25
|
+
this.sourceFile = this._findSourceFile(node);
|
|
26
|
+
const imports = this._findImportDeclarations(this.sourceFile);
|
|
27
|
+
this._moduleByTypeMap = this._computeImportMap(imports); // Map only contains types in 'other' modules (modules different from the one of the source file)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param typeNode {ts.TypeReferenceNode}
|
|
32
|
+
* @returns {{module: string;exportName:string}} The name of the module (e.g. '@eclipse-scout/core') the given type is declared in and the name it was exported in this module.
|
|
33
|
+
* Only returns a value if it is a different module than the one this detector was created in (only external modules).
|
|
34
|
+
* For imports in the same module, this function returns undefined.
|
|
35
|
+
*/
|
|
36
|
+
detectExportInfoOf(typeNode) {
|
|
37
|
+
const name = typeNode?.typeName?.escapedText;
|
|
38
|
+
return this._moduleByTypeMap.get(name);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
_computeImportMap(imports) {
|
|
42
|
+
const moduleByTypeMap = new Map();
|
|
43
|
+
for (let imp of imports) {
|
|
44
|
+
const moduleName = imp.moduleSpecifier?.text; // e.g. '@eclipse-scout/core' or './index'
|
|
45
|
+
const isExternalModule = !moduleName?.startsWith('.'); // only store imports to other modules
|
|
46
|
+
if (isExternalModule) {
|
|
47
|
+
this._putExternalNamedBindings(imp.importClause?.namedBindings, moduleByTypeMap, moduleName);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return moduleByTypeMap;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
_putExternalNamedBindings(namedBindings, moduleByTypeMap, moduleName) {
|
|
54
|
+
const importElements = namedBindings?.elements;
|
|
55
|
+
if (Array.isArray(importElements)) {
|
|
56
|
+
// multi import e.g.: import {a, b, c as d} from 'whatever'
|
|
57
|
+
for (let importElement of importElements) {
|
|
58
|
+
const name = importElement?.name?.escapedText;
|
|
59
|
+
if (!name) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const exportedName = importElement?.propertyName?.escapedText || name;
|
|
63
|
+
moduleByTypeMap.set(name, {module: moduleName, exportName: exportedName});
|
|
64
|
+
}
|
|
65
|
+
} else {
|
|
66
|
+
// single import e.g.: import * as self from './index';
|
|
67
|
+
const name = namedBindings?.name?.escapedText;
|
|
68
|
+
if (name) {
|
|
69
|
+
moduleByTypeMap.set(name, {module: moduleName, exportName: name});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
_findImportDeclarations(sourceFile) {
|
|
75
|
+
return sourceFile.statements.filter(s => ts.isImportDeclaration(s));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
_findSourceFile(node) {
|
|
79
|
+
while (!ts.isSourceFile(node)) {
|
|
80
|
+
node = node.parent;
|
|
81
|
+
}
|
|
82
|
+
return node;
|
|
83
|
+
}
|
|
84
|
+
};
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2010, 2025 BSI Business Systems Integration AG
|
|
3
|
+
*
|
|
4
|
+
* This program and the accompanying materials are made
|
|
5
|
+
* available under the terms of the Eclipse Public License 2.0
|
|
6
|
+
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
|
7
|
+
*
|
|
8
|
+
* SPDX-License-Identifier: EPL-2.0
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const REGISTER_NS_PATTERN = new RegExp('\\.registerNamespace\\s*\\(\'(\\w+)\'\\s*,');
|
|
14
|
+
const JS_COMMENTS_PATTERN = new RegExp('\\/\\*[\\s\\S]*?\\*\\/|(?<=[^:])\\/\\/.*|^\\/\\/.*', 'g');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Class to resolve the Scout JS namespace of a module (e.g. module 'eclipse-scout/core' uses namespace 'scout').
|
|
18
|
+
*/
|
|
19
|
+
module.exports = class ModuleNamespaceResolver {
|
|
20
|
+
|
|
21
|
+
constructor() {
|
|
22
|
+
this._rootsByFileDir = new Map();
|
|
23
|
+
this._namespaceByModuleRoot = new Map();
|
|
24
|
+
this._dependencyRootsByModuleRoot = new Set();
|
|
25
|
+
this.ownModuleNamespace = null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Resolves the Scout JS namespace for the given module name.
|
|
30
|
+
*
|
|
31
|
+
* Algorithm:
|
|
32
|
+
* 1. It resolves the module root of the sourceFilePath given (the first parent directory containing the package.json file).
|
|
33
|
+
* 2. If a moduleName is given searches for a dependency in node_modules with given moduleName and uses its module root.
|
|
34
|
+
* 3. Searches in the module for a file *.js or *.ts file calling the Scout method 'registerNamespace' and uses the value passed as namespace.
|
|
35
|
+
*
|
|
36
|
+
* @param moduleName {string} The name of an external dependency module (e.g. '@eclipse-scout/core') or null if the namespace of the module containing the sourceFilePath should be returned.
|
|
37
|
+
* @param sourceFilePath {string} A file path inside a module that has a dependency to the moduleName (if available).
|
|
38
|
+
* @returns {string} The namespace or undefined.
|
|
39
|
+
*/
|
|
40
|
+
resolveNamespace(moduleName, sourceFilePath) {
|
|
41
|
+
const moduleRoot = this.resolveModuleRoot(moduleName, sourceFilePath);
|
|
42
|
+
let namespace = this._namespaceByModuleRoot.get(moduleRoot);
|
|
43
|
+
if (!namespace) {
|
|
44
|
+
if (!moduleName && this.ownModuleNamespace) {
|
|
45
|
+
// use given namespace for own module if known
|
|
46
|
+
namespace = this.ownModuleNamespace;
|
|
47
|
+
} else {
|
|
48
|
+
namespace = this._resolveNamespace(moduleRoot);
|
|
49
|
+
}
|
|
50
|
+
this._namespaceByModuleRoot.set(moduleRoot, namespace);
|
|
51
|
+
}
|
|
52
|
+
return namespace;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @param moduleRoot {string}
|
|
57
|
+
* @returns {string}
|
|
58
|
+
*/
|
|
59
|
+
_resolveNamespace(moduleRoot) {
|
|
60
|
+
let searchRoot = moduleRoot;
|
|
61
|
+
const srcFolder = path.join(moduleRoot, 'src');
|
|
62
|
+
if (fs.existsSync(srcFolder)) {
|
|
63
|
+
searchRoot = srcFolder;
|
|
64
|
+
const mavenSrc = path.join(searchRoot, 'main/js');
|
|
65
|
+
if (fs.existsSync(mavenSrc)) {
|
|
66
|
+
searchRoot = mavenSrc;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return this._parseFromRegister(searchRoot);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @param root {string}
|
|
74
|
+
* @returns {string} The namespace parsed from the 'registerNamespace' function.
|
|
75
|
+
*/
|
|
76
|
+
_parseFromRegister(root) {
|
|
77
|
+
let namespace = null;
|
|
78
|
+
this._visitFiles(root, filePath => {
|
|
79
|
+
const content = fs.readFileSync(filePath, 'utf-8').toString();
|
|
80
|
+
const result = REGISTER_NS_PATTERN.exec(content.replaceAll(JS_COMMENTS_PATTERN, ''));
|
|
81
|
+
if (result?.length === 2) {
|
|
82
|
+
namespace = result[1];
|
|
83
|
+
return false; // abort
|
|
84
|
+
}
|
|
85
|
+
return true;
|
|
86
|
+
});
|
|
87
|
+
return namespace;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Breadth-first visit of all *.ts or *.ts files within the given root directory excluding node_modules sub folders.
|
|
92
|
+
* @param root {string} The root directory path
|
|
93
|
+
* @param callback {(string) => boolean} Callback for all *.ts or *.js file paths in given directory. Visiting is aborted as soon as the callback returns false.
|
|
94
|
+
*/
|
|
95
|
+
_visitFiles(root, callback) {
|
|
96
|
+
const buf = this._readDir(root);
|
|
97
|
+
while (buf.length) {
|
|
98
|
+
const dirent = buf.shift(); // remove first
|
|
99
|
+
const filePath = path.join(dirent.parentPath || dirent.path, dirent.name);
|
|
100
|
+
if (dirent.isFile()) {
|
|
101
|
+
const cont = callback(filePath);
|
|
102
|
+
if (!cont) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
buf.push(...this._readDir(filePath));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* @param directory {string} The directory to get the contents from
|
|
113
|
+
* @returns {Dirent[]} Array of child files (only *.js or *.ts) or folders (excluding node_modules)
|
|
114
|
+
*/
|
|
115
|
+
_readDir(directory) {
|
|
116
|
+
return fs.readdirSync(directory, {withFileTypes: true})
|
|
117
|
+
.filter(dirent => (dirent.isDirectory() && dirent.name !== 'node_modules') || dirent.name.endsWith('.js') || dirent.name.endsWith('.ts'))
|
|
118
|
+
.sort((a, b) => { // files first
|
|
119
|
+
if (a.isDirectory() && !b.isDirectory()) {
|
|
120
|
+
return 1;
|
|
121
|
+
}
|
|
122
|
+
if (!a.isDirectory() && b.isDirectory()) {
|
|
123
|
+
return -1;
|
|
124
|
+
}
|
|
125
|
+
return a.name.localeCompare(b.name);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
resolveModuleRoot(moduleName, sourceFilePath) {
|
|
130
|
+
const directory = path.dirname(sourceFilePath);
|
|
131
|
+
const moduleRoot = this._getModuleRoot(directory);
|
|
132
|
+
if (!moduleName) {
|
|
133
|
+
return moduleRoot; // no external module name: own module
|
|
134
|
+
}
|
|
135
|
+
return this._resolveExternalModule(moduleRoot, moduleName);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* @param moduleRoot {string}
|
|
140
|
+
* @param moduleName {string}
|
|
141
|
+
* @returns {string}
|
|
142
|
+
*/
|
|
143
|
+
_resolveExternalModule(moduleRoot, moduleName) {
|
|
144
|
+
const dependencyRoot = path.join(moduleRoot, 'node_modules', moduleName);
|
|
145
|
+
if (this._dependencyRootsByModuleRoot.has(dependencyRoot)) {
|
|
146
|
+
return dependencyRoot; // existence already verified
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (!fs.existsSync(path.join(dependencyRoot, 'package.json'))) {
|
|
150
|
+
throw new Error(`Dependency ${moduleName} not found in ${moduleRoot}.`);
|
|
151
|
+
}
|
|
152
|
+
this._dependencyRootsByModuleRoot.add(dependencyRoot);
|
|
153
|
+
return dependencyRoot;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
*
|
|
158
|
+
* @param sourceFileDir {string}
|
|
159
|
+
* @returns {string}
|
|
160
|
+
*/
|
|
161
|
+
_getModuleRoot(sourceFileDir) {
|
|
162
|
+
let root = this._rootsByFileDir.get(sourceFileDir);
|
|
163
|
+
if (!root) {
|
|
164
|
+
root = this._findModuleRoot(sourceFileDir);
|
|
165
|
+
if (!root) {
|
|
166
|
+
throw new Error(`${sourceFileDir} is not within any Node module.`);
|
|
167
|
+
}
|
|
168
|
+
this._rootsByFileDir.set(sourceFileDir, root); // remember for next files
|
|
169
|
+
}
|
|
170
|
+
return root;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* @param sourceFileDir {string}
|
|
175
|
+
* @returns {string}
|
|
176
|
+
*/
|
|
177
|
+
_findModuleRoot(sourceFileDir) {
|
|
178
|
+
while (sourceFileDir && !fs.existsSync(path.join(sourceFileDir, 'package.json'))) {
|
|
179
|
+
sourceFileDir = path.dirname(sourceFileDir);
|
|
180
|
+
}
|
|
181
|
+
return sourceFileDir.replaceAll('\\', '/');
|
|
182
|
+
}
|
|
183
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* Copyright (c) 2010,
|
|
2
|
+
* Copyright (c) 2010, 2025 BSI Business Systems Integration AG
|
|
3
3
|
*
|
|
4
4
|
* This program and the accompanying materials are made
|
|
5
5
|
* available under the terms of the Eclipse Public License 2.0
|
|
@@ -10,11 +10,14 @@
|
|
|
10
10
|
const fs = require('fs');
|
|
11
11
|
const path = require('path');
|
|
12
12
|
const scoutBuildConstants = require('./constants');
|
|
13
|
+
const DataObjectTransformer = require('./DataObjectTransformer');
|
|
14
|
+
const ModuleNamespaceResolver = require('./ModuleNamespaceResolver');
|
|
13
15
|
const CopyPlugin = require('copy-webpack-plugin');
|
|
14
16
|
const {CycloneDxWebpackPlugin} = require('@cyclonedx/webpack-plugin');
|
|
15
17
|
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
|
16
18
|
const AfterEmitWebpackPlugin = require('./AfterEmitWebpackPlugin');
|
|
17
19
|
const {SourceMapDevToolPlugin, WatchIgnorePlugin, ProgressPlugin} = require('webpack');
|
|
20
|
+
const ts = require('typescript');
|
|
18
21
|
|
|
19
22
|
/**
|
|
20
23
|
* @param {string} args.mode development or production
|
|
@@ -26,6 +29,7 @@ const {SourceMapDevToolPlugin, WatchIgnorePlugin, ProgressPlugin} = require('web
|
|
|
26
29
|
* @param {string} args.cyclonedxVersion CycloneDX version to use. Default is '1.5'.
|
|
27
30
|
* @param {[]} args.resDirArray an array containing directories which should be copied to dist/res
|
|
28
31
|
* @param {object} args.tsOptions a config object to be passed to the ts-loader
|
|
32
|
+
* @param {object} args.forkTypeCheckOptions a config object to be passed to the ForkTsCheckerWebpackPlugin
|
|
29
33
|
* @param {boolean|'fork'} args.typeCheck
|
|
30
34
|
* true: let the TypeScript compiler check the types.
|
|
31
35
|
* false: let the TypeScript compiler only transpile the TypeScript code without checking types, which makes it faster.
|
|
@@ -61,7 +65,10 @@ module.exports = (env, args) => {
|
|
|
61
65
|
});
|
|
62
66
|
}
|
|
63
67
|
|
|
64
|
-
|
|
68
|
+
// browser min. requirements for full ES2022 feature set
|
|
69
|
+
// Exception: static initialization blocks (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Static_initialization_blocks) which would require Safari 16
|
|
70
|
+
// But static initialization blocks can be transpiled by Babel to static fields which are supported in all these browsers. Therefore, in the code full ES2022 feature set can be used.
|
|
71
|
+
const minimizerTarget = ['firefox92', 'chrome93', 'safari15.4'];
|
|
65
72
|
const babelOptions = {
|
|
66
73
|
compact: false,
|
|
67
74
|
cacheDirectory: true,
|
|
@@ -70,22 +77,31 @@ module.exports = (env, args) => {
|
|
|
70
77
|
[require.resolve('@babel/preset-env'), {
|
|
71
78
|
debug: false,
|
|
72
79
|
targets: {
|
|
73
|
-
firefox: '
|
|
74
|
-
chrome: '
|
|
75
|
-
safari: '
|
|
80
|
+
firefox: '92',
|
|
81
|
+
chrome: '93',
|
|
82
|
+
safari: '15.4'
|
|
76
83
|
}
|
|
77
84
|
}]
|
|
78
85
|
]
|
|
79
86
|
};
|
|
80
87
|
|
|
81
88
|
const transpileOnly = typeCheck === 'fork' ? true : !typeCheck;
|
|
89
|
+
const namespaceResolver = new ModuleNamespaceResolver();
|
|
90
|
+
const getCustomTransformers = program => ({
|
|
91
|
+
before: [ctx => {
|
|
92
|
+
const doTransformer = new DataObjectTransformer(program, ctx, namespaceResolver);
|
|
93
|
+
return node => ts.visitNode(node, node => doTransformer.transform(node));
|
|
94
|
+
}]
|
|
95
|
+
});
|
|
96
|
+
getCustomTransformers.namespaceResolver = namespaceResolver; // for setDoTransformerOwnModuleNamespace below
|
|
82
97
|
const tsOptions = {
|
|
83
98
|
...args.tsOptions,
|
|
84
99
|
transpileOnly: transpileOnly,
|
|
85
100
|
compilerOptions: {
|
|
86
101
|
noEmit: false,
|
|
87
102
|
...args.tsOptions?.compilerOptions
|
|
88
|
-
}
|
|
103
|
+
},
|
|
104
|
+
getCustomTransformers
|
|
89
105
|
};
|
|
90
106
|
|
|
91
107
|
const config = {
|
|
@@ -177,7 +193,7 @@ module.exports = (env, args) => {
|
|
|
177
193
|
}
|
|
178
194
|
}]
|
|
179
195
|
}, {
|
|
180
|
-
test: /\.tsx?$/,
|
|
196
|
+
test: /\.[c|m]?tsx?$/,
|
|
181
197
|
exclude: /node_modules/,
|
|
182
198
|
use: [{
|
|
183
199
|
loader: require.resolve('babel-loader'),
|
|
@@ -187,13 +203,13 @@ module.exports = (env, args) => {
|
|
|
187
203
|
options: tsOptions
|
|
188
204
|
}]
|
|
189
205
|
}, {
|
|
190
|
-
test: /\.jsx?$/,
|
|
206
|
+
test: /\.[c|m]?jsx?$/,
|
|
191
207
|
use: [{
|
|
192
208
|
loader: require.resolve('babel-loader'),
|
|
193
209
|
options: babelOptions
|
|
194
210
|
}]
|
|
195
211
|
}, {
|
|
196
|
-
test: /\.jsx?$/,
|
|
212
|
+
test: /\.[c|m]?jsx?$/,
|
|
197
213
|
enforce: 'pre',
|
|
198
214
|
use: [{
|
|
199
215
|
loader: require.resolve('source-map-loader')
|
|
@@ -245,13 +261,16 @@ module.exports = (env, args) => {
|
|
|
245
261
|
|
|
246
262
|
let forkTsCheckerConfig = {
|
|
247
263
|
typescript: {
|
|
248
|
-
memoryLimit: 4096
|
|
249
|
-
|
|
264
|
+
memoryLimit: 4096,
|
|
265
|
+
...args.forkTypeCheckOptions?.typescript
|
|
266
|
+
},
|
|
267
|
+
...args.forkTypeCheckOptions
|
|
250
268
|
};
|
|
251
269
|
if (!fs.existsSync('./tsconfig.json')) {
|
|
252
270
|
// if the module has no tsconfig: use default from Scout.
|
|
253
271
|
// Otherwise, each module would need to provide a tsconfig even if there is no typescript code in the module.
|
|
254
272
|
forkTsCheckerConfig = {
|
|
273
|
+
...forkTsCheckerConfig,
|
|
255
274
|
typescript: {
|
|
256
275
|
...forkTsCheckerConfig.typescript,
|
|
257
276
|
configFile: require.resolve('@eclipse-scout/tsconfig'),
|
|
@@ -335,6 +354,19 @@ function toExternals(src, dest) {
|
|
|
335
354
|
}, dest);
|
|
336
355
|
}
|
|
337
356
|
|
|
357
|
+
/**
|
|
358
|
+
* Sets the Scout JS module namespace for the root module currently built.
|
|
359
|
+
* @param config The build config to modify.
|
|
360
|
+
* @param namespace The namespace of this module.
|
|
361
|
+
*/
|
|
362
|
+
function setDoTransformerOwnModuleNamespace(config, namespace) {
|
|
363
|
+
config.module.rules
|
|
364
|
+
.flatMap(r => r.use || [])
|
|
365
|
+
.find(l => l.loader?.indexOf('ts-loader') >= 0)
|
|
366
|
+
.options.getCustomTransformers.namespaceResolver
|
|
367
|
+
.ownModuleNamespace = namespace;
|
|
368
|
+
}
|
|
369
|
+
|
|
338
370
|
/**
|
|
339
371
|
* Converts the given base config to a library config meaning that all dependencies declared in the package.json are externalized by default.
|
|
340
372
|
*
|
|
@@ -624,4 +656,5 @@ function computeTypeCheck(typeCheck, devMode, watchMode) {
|
|
|
624
656
|
module.exports.addThemes = addThemes;
|
|
625
657
|
module.exports.libraryConfig = libraryConfig;
|
|
626
658
|
module.exports.markExternals = markExternals;
|
|
659
|
+
module.exports.setDoTransformerOwnModuleNamespace = setDoTransformerOwnModuleNamespace;
|
|
627
660
|
module.exports.rewriteIndexImports = rewriteIndexImports;
|