@eclipse-scout/cli 25.1.16-alpha.1 → 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 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.1-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.1-RT-nightly_pipeline%2F"></a>
7
- <a href="https://ci.eclipse.org/scout/job/scout-integration-25.1-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.1-RT-nightly_pipeline%2F"></a>
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.1.16-alpha.1",
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.1.16-alpha.1",
49
- "@eclipse-scout/tsconfig": "25.1.16-alpha.1",
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, 2024 BSI Business Systems Integration AG
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
- const minimizerTarget = ['firefox69', 'chrome71', 'safari13'];
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: '69',
74
- chrome: '71',
75
- safari: '13'
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;