• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

IgniteUI / igniteui-cli / 8631556716

10 Apr 2024 12:52PM UTC coverage: 67.144% (+0.4%) from 66.741%
8631556716

push

github

web-flow
chore: update typescript; migrate to eslint (#1235)

997 of 1601 branches covered (62.27%)

Branch coverage included in aggregate %.

17 of 17 new or added lines in 4 files covered. (100.0%)

117 existing lines in 26 files now uncovered.

4541 of 6647 relevant lines covered (68.32%)

156.03 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.31
/packages/core/typescript/TypeScriptFileUpdate.ts
1
import * as ts from "typescript";
12✔
2
import { App } from "..";
12✔
3
import { TemplateDependency } from "../types";
4
import { FS_TOKEN, IFileSystem } from "../types/FileSystem";
12✔
5
import { Util } from "../util/Util";
12✔
6
import { TypeScriptUtils as TsUtils } from "./TypeScriptUtils";
12✔
7

8
const DEFAULT_ROUTES_VARIABLE = "routes";
12✔
9
const DEFAULT_APPCONFIG_VARIABLE = "appConfig";
12✔
10
/**
11
 * Apply various updates to typescript files using AST
12
 */
13
export class TypeScriptFileUpdate {
12✔
14
        // https://github.com/Microsoft/TypeScript/wiki/Using-the-Compiler-API for general source parsing
15
        // http://blog.scottlogic.com/2017/05/02/typescript-compiler-api-revisited.html
16
        // for AST transformation API List: https://github.com/Microsoft/TypeScript/pull/13940
17

18
        protected formatOptions = { spaces: false, indentSize: 4, singleQuotes: false };
60✔
19
        private fileSystem: IFileSystem;
20
        private targetSource: ts.SourceFile;
21
        private importsMeta: { lastIndex: number, modulePaths: string[] };
22

23
        private requestedImports: Array<{ from: string, imports: string[], edit: boolean }>;
24
        private ngMetaEdits: {
25
                declarations: string[],
26
                imports: Array<{ name: string, root: boolean, standalone?: boolean }>,
27
                providers: string[],
28
                exports: string[]
29
        };
30

31
        private createdStringLiterals: string[];
32

33
        /** Create updates for a file. Use `add<X>` methods to add transformations and `finalize` to apply and save them. */
34
        constructor(private targetPath: string) {
60✔
35
                this.fileSystem = App.container.get<IFileSystem>(FS_TOKEN);
60✔
36
                this.initState();
60✔
37
        }
38

39
        public transform<T extends ts.Node>(source: T | T[],
40
                transformers: ts.TransformerFactory<T>[],
41
                compilerOptions?: ts.CompilerOptions)
42
                : ts.TransformationResult<T> {
43
                return ts.transform(source, transformers, compilerOptions);
42✔
44
        }
45

46
        /** Applies accumulated transforms, saves and formats the file */
47
        public finalize() {
48
                const transforms = [];
44✔
49
                // walk AST for modifications.
50
                if (this.requestedImports.filter(x => x.edit).length) {
54✔
51
                        transforms.push(this.importsTransformer);
10✔
52
                }
53

54
                // should we support both standalone and module-based components in the same app?
55
                if (this.ngMetaEdits.imports.some(x => x.standalone)) {
44!
56
                        transforms.push(this.componentMetaTransformer);
×
57
                } else if (Object.keys(this.ngMetaEdits).filter(x => this.ngMetaEdits[x].length).length) {
176✔
58
                        transforms.push(this.ngModuleTransformer);
18✔
59
                }
60

61
                if (transforms.length) {
44✔
62
                        this.targetSource = this.transform(this.targetSource, transforms).transformed[0];
22✔
63
                }
64

65
                // add new import statements after visitor walks:
66
                this.addNewFileImports();
44✔
67

68
                TsUtils.saveFile(this.targetPath, this.targetSource);
44✔
69
                this.formatFile(this.targetPath);
44✔
70
                // reset state in case of further updates
71
                this.initState();
44✔
72
        }
73

74
        /**
75
         * Create configuration object for a component and add it to the `Routes` array variable.
76
         * Imports the first exported class and finalizes the file update (see `.finalize()`).
77
         * @param filePath Path to the component file to import
78
         * @param linkPath Routing `path` to add
79
         * @param linkText Text of the route to add as `data.text`
80
         * @param parentRoutePath Will include the new route as a **child** of the specified route path
81
         * @param routesVariable Name of the array variable holding routes
82
         * @param lazyload Whether to use lazy loading for the route
83
         * @param routesPath Path to the routing module
84
         * @param root Whether the route is a root route
85
         * @param isDefault Whether the route is the default route for the view
86
         */
87
        public addChildRoute(
88
                filePath: string,
89
                linkPath: string,
90
                linkText: string,
91
                parentRoutePath: string,
92
                routesVariable = DEFAULT_ROUTES_VARIABLE,
4✔
93
                lazyload = false,
4✔
94
                routesPath = "",
4✔
95
                root = false,
4✔
96
                isDefault = false
4✔
97
        ) {
98
                this.addRouteModuleEntry(filePath, linkPath, linkText, routesVariable,
4✔
99
                        parentRoutePath, lazyload, routesPath, root, isDefault);
100
        }
101

102
        /**
103
         * Create configuration object for a component and add it to the `Routes` array variable.
104
         * Imports the first exported class and finalizes the file update (see `.finalize()`).
105
         * @param filePath Path to the component file to import
106
         * @param linkPath Routing `path` to add
107
         * @param linkText Text of the route to add as `data.text`
108
         * @param routesVariable Name of the array variable holding routes
109
         */
110
        public addRoute(
111
                filePath: string,
112
                linkPath: string,
113
                linkText: string,
114
                routesVariable = DEFAULT_ROUTES_VARIABLE,
10✔
115
                lazyload = false, routesPath = "", root = false, isDefault = false) {
48✔
116
                this.addRouteModuleEntry(filePath, linkPath, linkText, routesVariable, null, lazyload, routesPath, root, isDefault);
12✔
117
        }
118

119
        /**
120
         * Import class and add it to `NgModule` declarations.
121
         * Creates `declarations` array if one is not present already.
122
         * @param filePath Path to the file to import
123
         */
124
        public addDeclaration(filePath: string, addToExport?: boolean) {
125
                let className: string;
126
                const fileSource = TsUtils.getFileSource(filePath);
14✔
127
                const relativePath: string = Util.relativePath(this.targetPath, filePath, true, true);
14✔
128
                className = TsUtils.getClassName(fileSource.getChildren());
14✔
129
                if (addToExport) {
14✔
130
                        this.addNgModuleMeta({ declare: className, from: relativePath, export: className });
4✔
131
                } else {
132
                        this.addNgModuleMeta({ declare: className, from: relativePath });
10✔
133
                }
134
        }
135

136
        /**
137
         * Add a metadata update to the file's `NgModule`. Will also import identifiers.
138
         */
139
        public addNgModuleMeta(dep: TemplateDependency, variables?: { [key: string]: string }) {
140
                const copy = {
141
                        declare: this.asArray(dep.declare, variables),
142
                        import: this.asArray(dep.import, variables),
143
                        provide: this.asArray(dep.provide, variables),
144
                        // tslint:disable-next-line:object-literal-sort-keys
145
                        export: this.asArray(dep.export, variables)
146
                };
147

148
                if (dep.from) {
52✔
149
                        // request import
150
                        const identifiers = [...copy.import, ...copy.declare, ...copy.provide];
42✔
151
                        this.requestImport(identifiers, Util.applyConfigTransformation(dep.from, variables));
42✔
152
                }
153
                const imports = copy.import
52✔
154
                        .map(x => ({ name: x, root: dep.root }))
38✔
155
                        .filter(x => !this.ngMetaEdits.imports.find(i => i.name === x.name));
38✔
156
                this.ngMetaEdits.imports.push(...imports);
52✔
157

158
                const declarations = copy.declare
52✔
159
                        .filter(x => !this.ngMetaEdits.declarations.find(d => d === x));
28✔
160
                this.ngMetaEdits.declarations.push(...declarations);
52✔
161

162
                const providers = copy.provide
52✔
163
                        .filter(x => !this.ngMetaEdits.providers.find(p => p === x));
10✔
164
                this.ngMetaEdits.providers.push(...providers);
52✔
165

166
                const exportsArr = copy.export
52✔
167
                        .filter(x => !this.ngMetaEdits.exports.find(p => p === x));
×
168
                this.ngMetaEdits.exports.push(...exportsArr);
52✔
169
        }
170

171
        /**
172
         * Updates a standalone component's imports metadata.
173
         */
174
        public addStandaloneImport(dep: TemplateDependency, variables?: { [key: string]: string }) {
175
                const copy = {
176
                        import: this.asArray(dep.import, variables),
177
                        provide: this.asArray(dep.provide, variables)
178
                };
UNCOV
179
                if (dep.from) {
×
180
                        // request import
181
                        const identifiers = [...copy.import, ...copy.provide];
×
182
                        this.requestImport(identifiers, Util.applyConfigTransformation(dep.from, variables));
×
183
                }
184

185
                const imports = copy.import
×
186
                        .map(x => ({ name: x, root: dep.root, standalone: true }))
×
187
                        .filter(x => !this.ngMetaEdits.imports.find(i => i.name === x.name));
×
188
                this.ngMetaEdits.imports.push(...imports);
×
189
        }
190

191
        /**
192
         * Create a CallExpression for dep and add it to the `ApplicationConfig` providers array.
193
         * @param dep The dependency to provide. TODO: Use different type to describe CallExpression, possible parameters, etc
194
         * @param configVariable The name of the app config variable to edit
195
         */
196
        public addAppConfigProvider(
197
                dep: Pick<TemplateDependency, "provide" | "from">,
198
                configVariable = DEFAULT_APPCONFIG_VARIABLE) {
4✔
199
                let providers = this.asArray(dep.provide, {});
4✔
200

201
                const transformer: ts.TransformerFactory<ts.SourceFile> = <T extends ts.SourceFile>(context: ts.TransformationContext) =>
4✔
202
                (rootNode: T) => {
4✔
203
                        const conditionalVisitor: ts.Visitor = (node: ts.Node): ts.Node => {
4✔
204
                                if (node.kind === ts.SyntaxKind.ArrayLiteralExpression &&
28✔
205
                                        node.parent.kind === ts.SyntaxKind.PropertyAssignment &&
206
                                        (node.parent as ts.PropertyAssignment).name.getText() === "providers") {
207
                                        const array = (node as ts.ArrayLiteralExpression);
4✔
208
                                        const nodes = ts.visitNodes(array.elements, visitor, ts.isExpression);
4✔
209
                                        const alreadyProvided = nodes.map(x => TsUtils.getIdentifierName(x));
6✔
210

211
                                        providers =  providers.filter(x => alreadyProvided.indexOf(x) === -1);
4✔
212
                                        this.requestImport(providers, dep.from);
4✔
213

214
                                        const newProvides = providers
4✔
215
                                                .map(x => ts.factory.createCallExpression(ts.factory.createIdentifier(x), undefined, undefined));
2✔
216
                                        const elements = ts.factory.createNodeArray<ts.Expression>([
4✔
217
                                                ...nodes,
218
                                                ...newProvides
219
                                        ]);
220

221
                                        return ts.factory.updateArrayLiteralExpression(array, elements);
4✔
222
                                } else {
223
                                        return ts.visitEachChild(node, conditionalVisitor, context);
24✔
224
                                }
225
                        };
226

227
                        const visitCondition = (node: ts.Node): boolean => {
4✔
228
                                return node.kind === ts.SyntaxKind.VariableDeclaration &&
120✔
229
                                        (node as ts.VariableDeclaration).name.getText() === configVariable &&
230
                                        (node as ts.VariableDeclaration).type.getText() === "ApplicationConfig";
231
                        };
232
                        const visitor: ts.Visitor = this.createVisitor(conditionalVisitor, visitCondition, context);
4✔
233
                        context.enableSubstitution(ts.SyntaxKind.ArrayLiteralExpression);
4✔
234
                        return ts.visitNode(rootNode, visitor, ts.isSourceFile);
4✔
235
                };
236
                this.targetSource = this.transform(this.targetSource, [transformer], {
4✔
237
                        pretty: true // oh well..
238
                }).transformed[0];
239
        }
240

241
        //#region File state
242

243
        /** Initializes existing imports info, [re]sets import and `NgModule` edits */
244
        protected initState() {
245
                this.targetSource = TsUtils.getFileSource(this.targetPath);
104✔
246
                this.importsMeta = this.loadImportsMeta();
104✔
247
                this.requestedImports = [];
104✔
248
                this.ngMetaEdits = {
104✔
249
                        declarations: [],
250
                        imports: [],
251
                        providers: [],
252
                        exports: []
253
                };
254
                this.createdStringLiterals = [];
104✔
255
        }
256

257
        /* load some metadata about imports */
258
        protected loadImportsMeta() {
259
                const meta = { lastIndex: 0, modulePaths: [] };
102✔
260

261
                for (let i = 0; i < this.targetSource.statements.length; i++) {
102✔
262
                        const statement = this.targetSource.statements[i];
200✔
263
                        switch (statement.kind) {
200✔
264
                                case ts.SyntaxKind.ImportDeclaration:
314✔
265
                                        const importStmt = (statement as ts.ImportDeclaration);
114✔
266

267
                                        if (importStmt.importClause && importStmt.importClause.namedBindings &&
114✔
268
                                                importStmt.importClause.namedBindings.kind !== ts.SyntaxKind.NamespaceImport) {
269
                                                // don't add imports without named (e.g. `import $ from "JQuery"` or `import "./my-module.js";`)
270
                                                // don't add namespace imports (`import * as fs`) as available for editing, maybe in the future
271
                                                meta.modulePaths.push((importStmt.moduleSpecifier as ts.StringLiteral).text);
108✔
272
                                        }
273

274
                                // don't add equals imports (`import url = require("url")`) as available for editing, maybe in the future
275
                                case ts.SyntaxKind.ImportEqualsDeclaration:
276
                                        meta.lastIndex = i + 1;
116✔
277
                                        break;
116✔
278
                                default:
279
                                        break;
84✔
280
                        }
281
                }
282

283
                return meta;
102✔
284
        }
285

286
        //#endregion File state
287

288
        protected addRouteModuleEntry(
289
                filePath: string,
290
                linkPath: string,
291
                linkText: string,
292
                routesVariable = DEFAULT_ROUTES_VARIABLE,
×
293
                parentRoutePath?: string,
294
                lazyload = false,
×
295
                routesPath = "",
×
296
                root = false,
×
297
                isDefault = false
×
298
        ) {
299
                let className: string;
300
                const fileSource = TsUtils.getFileSource(filePath);
16✔
301
                const relativePath: string = Util.relativePath(this.targetPath, filePath, true, true);
16✔
302
                className = TsUtils.getClassName(fileSource.getChildren());
16✔
303

304
                if (!lazyload) {
16!
305
                        this.requestImport([className], relativePath);
16✔
306
                }
307

308
                // https://github.com/Microsoft/TypeScript/issues/14419#issuecomment-307256171
309
                const transformer: ts.TransformerFactory<ts.Node> = <T extends ts.Node>(context: ts.TransformationContext) =>
16✔
310
                        (rootNode: T) => {
16✔
311
                                let conditionalVisitor: ts.Visitor;
312
                                // the visitor that should be used when adding routes to the main route array
313
                                const routeArrayVisitor = (node: ts.Node): ts.Node => {
16✔
314
                                        if (node.kind === ts.SyntaxKind.ArrayLiteralExpression) {
48✔
315
                                                const newObject = this.createRouteEntry(linkPath, className, linkText, lazyload, routesPath, root);
12✔
316
                                                const array = (node as ts.ArrayLiteralExpression);
12✔
317
                                                this.createdStringLiterals.push(linkPath, linkText);
12✔
318
                                                const notFoundWildCard = "**";
12✔
319
                                                const nodes = ts.visitNodes(array.elements, visitor);
12✔
320
                                                const errorRouteNode = nodes.filter(element => element.getText().includes(notFoundWildCard))[0];
12✔
321
                                                let resultNodes = null;
12✔
322
                                                if (errorRouteNode) {
12!
323
                                                        resultNodes = nodes
×
324
                                                                .slice(0, nodes.indexOf(errorRouteNode))
325
                                                                .concat(newObject)
326
                                                                .concat(errorRouteNode);
327
                                                } else {
328
                                                        resultNodes = nodes
12✔
329
                                                                .concat(newObject);
330
                                                }
331

332
                                                const elements = ts.factory.createNodeArray([
12✔
333
                                                        ...resultNodes
334
                                                ]);
335

336
                                                return ts.factory.updateArrayLiteralExpression(array, elements);
12✔
337
                                        } else {
338
                                                return ts.visitEachChild(node, conditionalVisitor, context);
36✔
339
                                        }
340
                                };
341
                                // the visitor that should be used when adding child routes to a specified parent
342
                                const parentRouteVisitor = (node: ts.Node): ts.Node => {
16✔
343
                                        if (node.kind === ts.SyntaxKind.ObjectLiteralExpression) {
20✔
344
                                                if (!node.getText().includes(parentRoutePath)) {
4!
345
                                                        return node;
×
346
                                                }
347
                                                const nodeProperties = (node as ts.ObjectLiteralExpression).properties;
4✔
348
                                                const parentPropertyCheck = (element: ts.PropertyAssignment) => {
4✔
349
                                                        return element.name.kind === ts.SyntaxKind.Identifier && element.name.text === "path"
10✔
350
                                                                && element.initializer.kind === ts.SyntaxKind.StringLiteral
351
                                                                && (element.initializer as ts.StringLiteral).text === parentRoutePath;
352
                                                };
353
                                                const parentProperty = nodeProperties.filter(parentPropertyCheck)[0];
4✔
354
                                                if (!parentProperty) {
4!
355
                                                        return node;
×
356
                                                }
357
                                                function filterForChildren(element: ts.Node): boolean {
358
                                                        if (element.kind === ts.SyntaxKind.PropertyAssignment) {
16✔
359
                                                                const identifier = element.getChildren()[0];
6✔
360
                                                                return identifier.kind === ts.SyntaxKind.Identifier && identifier.getText().trim() === "children";
6✔
361
                                                        }
362
                                                        return false;
10✔
363
                                                }
364
                                                let defaultRoute: ts.ObjectLiteralExpression;
365
                                                if (isDefault) {
4!
366
                                                        defaultRoute = this.createRouteEntry("", null, null, false, routesPath, root, isDefault);
×
367
                                                }
368
                                                const newObject = this.createRouteEntry(linkPath, className, linkText);
4✔
369
                                                const currentNode = node as ts.ObjectLiteralExpression;
4✔
370
                                                this.createdStringLiterals.push(linkPath, linkText);
4✔
371
                                                const syntaxList: ts.SyntaxList = node.getChildren()
4✔
372
                                                        .filter(element => element.kind === ts.SyntaxKind.SyntaxList)[0] as ts.SyntaxList;
12✔
373
                                                let childrenProperty: ts.PropertyAssignment = syntaxList
4✔
374
                                                        .getChildren().filter(filterForChildren)[0] as ts.PropertyAssignment;
375
                                                let childrenArray: ts.ArrayLiteralExpression = null;
4✔
376

377
                                                // if the target parent route already has child routes - get them
378
                                                // if not - create an empty 'chuldren' array
379
                                                if (childrenProperty) {
4✔
380
                                                        childrenArray = childrenProperty.getChildren()
2!
381
                                                                .filter(element => element.kind === ts.SyntaxKind.ArrayLiteralExpression)[0] as ts.ArrayLiteralExpression
6✔
382
                                                                || ts.factory.createArrayLiteralExpression();
383
                                                } else {
384
                                                        childrenArray = ts.factory.createArrayLiteralExpression();
2✔
385
                                                }
386

387
                                                let existingProperties = syntaxList.getChildren()
4✔
388
                                                        .filter(element => element.kind !== ts.SyntaxKind["CommaToken"]) as ts.ObjectLiteralElementLike[];
16✔
389
                                                let newArrayValues: ts.Expression[];
390
                                                if (isDefault) {
4!
391
                                                        newArrayValues = childrenArray.elements.concat(defaultRoute, newObject);
×
392
                                                } else {
393
                                                        newArrayValues = childrenArray.elements.concat(newObject);
4✔
394
                                                }
395
                                                if (!childrenProperty) {
4✔
396
                                                        const propertyName = "children";
2✔
397
                                                        const propertyValue = ts.factory.createArrayLiteralExpression([...newArrayValues]);
2✔
398
                                                        childrenProperty = ts.factory.createPropertyAssignment(propertyName, propertyValue);
2✔
399
                                                        existingProperties = existingProperties
2✔
400
                                                                .concat(childrenProperty);
401
                                                } else {
402
                                                        const index = existingProperties.indexOf(childrenProperty);
2✔
403
                                                        const childrenPropertyName = childrenProperty.name;
2✔
404
                                                        childrenProperty =
2✔
405
                                                                ts.factory.updatePropertyAssignment(
406
                                                                        childrenProperty,
407
                                                                        childrenPropertyName,
408
                                                                        ts.factory.createArrayLiteralExpression([...newArrayValues])
409
                                                                );
410
                                                        existingProperties
2✔
411
                                                                .splice(index, 1, childrenProperty);
412
                                                }
413
                                                return ts.factory.updateObjectLiteralExpression(currentNode, existingProperties) as ts.Node;
4✔
414
                                        } else {
415
                                                return ts.visitEachChild(node, conditionalVisitor, context);
16✔
416
                                        }
417
                                };
418

419
                                if (parentRoutePath === null) {
16✔
420
                                        conditionalVisitor = routeArrayVisitor;
12✔
421
                                } else {
422
                                        conditionalVisitor = parentRouteVisitor;
4✔
423
                                }
424
                                const visitCondition = (node: ts.Node): boolean => {
16✔
425
                                        return node.kind === ts.SyntaxKind.VariableDeclaration &&
154✔
426
                                                (node as ts.VariableDeclaration).name.getText() === routesVariable &&
427
                                                (node as ts.VariableDeclaration).type.getText() === "Routes";
428
                                };
429
                                const visitor: ts.Visitor = this.createVisitor(conditionalVisitor, visitCondition, context);
16✔
430
                                context.enableSubstitution(ts.SyntaxKind.ClassDeclaration);
16✔
431
                                return ts.visitNode(rootNode, visitor);
16✔
432
                        };
433

434
                this.targetSource = this.transform(this.targetSource, [transformer], {
16✔
435
                        pretty: true // oh well..
436
                }).transformed[0] as ts.SourceFile;
437

438
                this.finalize();
16✔
439
        }
440

441
        /**
442
         * Add named imports from a path/package.
443
         * @param identifiers Strings to create named import from ("Module" => `import { Module }`)
444
         * @param modulePath Module specifier - can be path to file or npm package, etc
445
         */
446
        protected requestImport(identifiers: string[], modulePath: string) {
447
                const existing = this.requestedImports.find(x => x.from === modulePath);
70✔
448
                if (!existing) {
70✔
449
                        // new imports, check if already exists in file
450
                        this.requestedImports.push({
56✔
451
                                from: modulePath, imports: identifiers,
452
                                edit: this.importsMeta.modulePaths.indexOf(modulePath) !== -1
453
                        });
454
                } else {
455
                        const newNamedImports = identifiers.filter(x => existing.imports.indexOf(x) === -1);
20✔
456
                        existing.imports.push(...newNamedImports);
14✔
457
                }
458
        }
459

460
        /** Add `import` statements not previously found in the file  */
461
        protected addNewFileImports() {
462
                const newImports = this.requestedImports.filter(x => !x.edit);
54✔
463
                if (!newImports.length) {
44✔
464
                        return;
12✔
465
                }
466

467
                const newStatements = ts.factory.createNodeArray([
32✔
468
                        ...this.targetSource.statements.slice(0, this.importsMeta.lastIndex),
469
                        ...newImports.map(x => TsUtils.createIdentifierImport(x.imports, x.from)),
40✔
470
                        ...this.targetSource.statements.slice(this.importsMeta.lastIndex)
471
                ]);
472
                newImports.forEach(x => this.createdStringLiterals.push(x.from));
40✔
473

474
                this.targetSource = ts.factory.updateSourceFile(this.targetSource, newStatements);
32✔
475
        }
476

477
        //#region ts.TransformerFactory
478

479
        /** Transformation to apply edits to existing named import declarations */
480
        protected importsTransformer: ts.TransformerFactory<ts.Node> =
60✔
481
                <T extends ts.Node>(context: ts.TransformationContext) => (rootNode: T) => {
10✔
482
                        const editImports = this.requestedImports.filter(x => x.edit);
16✔
483

484
                        // https://github.com/Microsoft/TypeScript/issues/14419#issuecomment-307256171
485
                        const visitor = (node: ts.Node): ts.Node => {
10✔
486
                                if (node.kind === ts.SyntaxKind.ImportDeclaration &&
216✔
487
                                        editImports.find(x => x.from === ((node as ts.ImportDeclaration).moduleSpecifier as ts.StringLiteral).text)
26✔
488
                                ) {
489
                                        // visit just the source file main array (second visit)
490
                                        return visitImport(node as ts.ImportDeclaration);
14✔
491
                                } else {
492
                                        node = ts.visitEachChild(node, visitor, context);
202✔
493
                                }
494
                                return node;
202✔
495
                        };
496
                        function visitImport(node: ts.Node) {
497
                                if (node.kind === ts.SyntaxKind.NamedImports) {
56✔
498
                                        const namedImports = node as ts.NamedImports;
14✔
499
                                        const moduleSpecifier = (namedImports.parent.parent.moduleSpecifier as ts.StringLiteral).text;
14✔
500

501
                                        const existing = ts.visitNodes(namedImports.elements, visitor, ts.isImportSpecifier);
14✔
502
                                        const alreadyImported = existing.map(x => ts.isImportSpecifier(x) && x.name.text);
14✔
503

504
                                        const editImport = editImports.find(x => x.from === moduleSpecifier);
18✔
505
                                        const newImports = editImport.imports.filter(x => alreadyImported.indexOf(x) === -1);
14✔
506

507
                                        node = ts.factory.updateNamedImports(namedImports, [
14✔
508
                                                ...existing,
509
                                                ...newImports.map(x => ts.factory.createImportSpecifier(false, undefined, ts.factory.createIdentifier(x)))
12✔
510
                                        ]);
511
                                } else {
512
                                        node = ts.visitEachChild(node, visitImport, context);
42✔
513
                                }
514
                                return node;
56✔
515
                        }
516
                        return ts.visitNode(rootNode, visitor);
10✔
517
                }
518

519
        /** Transformation to apply `this.ngMetaEdits` to `NgModule` metadata properties */
520
        protected ngModuleTransformer: ts.TransformerFactory<ts.Node> =
60✔
521
                <T extends ts.Node>(context: ts.TransformationContext) => (rootNode: T) => {
18✔
522
                        const visitNgModule: ts.Visitor = (node: ts.Node): ts.Node => {
18✔
523
                                const properties: string[] = []; // "declarations", "imports", "providers"
160✔
524
                                for (const key in this.ngMetaEdits) {
160✔
525
                                        if (this.ngMetaEdits[key].length) {
640✔
526
                                                properties.push(key);
296✔
527
                                        }
528
                                }
529
                                if (node.kind === ts.SyntaxKind.ObjectLiteralExpression &&
160✔
530
                                        node.parent &&
531
                                        node.parent.kind === ts.SyntaxKind.CallExpression) {
532

533
                                        let obj = (node as ts.ObjectLiteralExpression);
18✔
534

535
                                        //TODO: test node.parent for ts.CallExpression NgModule
536
                                        const missingProperties = properties.filter(x => !obj.properties.find(o => o.name.getText() === x));
44✔
537

538
                                        // skip visiting if no declaration/imports/providers arrays exist:
539
                                        if (missingProperties.length !== properties.length) {
18!
540
                                                obj = ts.visitEachChild(node, visitNgModule, context) as ts.ObjectLiteralExpression;
18✔
541
                                        }
542

543
                                        if (!missingProperties.length) {
18✔
544
                                                return obj;
16✔
545
                                        }
546

547
                                        const objProperties = ts.visitNodes(obj.properties, visitor);
2✔
548
                                        const newProps = [];
2✔
549
                                        for (const prop of missingProperties) {
2✔
550
                                                let arrayExpr;
551
                                                switch (prop) {
4✔
552
                                                        case "imports":
6!
553
                                                                const importDeps = this.ngMetaEdits.imports;
2✔
554
                                                                arrayExpr = ts.factory.createArrayLiteralExpression(
2✔
555
                                                                        importDeps.map(x => TsUtils.createIdentifier(x.name, x.root ? "forRoot" : ""))
4!
556
                                                                );
557
                                                                break;
2✔
558
                                                        case "declarations":
559
                                                        case "providers":
560
                                                        case "exports":
561
                                                                arrayExpr = ts.factory.createArrayLiteralExpression(
2✔
562
                                                                        this.ngMetaEdits[prop].map(x => ts.factory.createIdentifier(x))
2✔
563
                                                                );
564
                                                                break;
2✔
565
                                                }
566
                                                newProps.push(ts.factory.createPropertyAssignment(prop, arrayExpr));
4✔
567
                                        }
568

569
                                        return ts.factory.updateObjectLiteralExpression(obj, [
2✔
570
                                                ...objProperties,
571
                                                ...newProps
572
                                        ]);
573
                                } else if (node.kind === ts.SyntaxKind.ArrayLiteralExpression &&
142✔
574
                                        node.parent.kind === ts.SyntaxKind.PropertyAssignment &&
575
                                        properties.indexOf((node.parent as ts.PropertyAssignment).name.getText()) !== -1) {
576
                                        const initializer = (node as ts.ArrayLiteralExpression);
28✔
577
                                        const props = ts.visitNodes(initializer.elements, visitor);
28✔
578
                                        const alreadyImported = props.map(x => TsUtils.getIdentifierName(x));
34✔
579
                                        const prop = properties.find(x => x === (node.parent as ts.PropertyAssignment).name.getText());
40✔
580

581
                                        let identifiers = [];
28✔
582
                                        switch (prop) {
28✔
583
                                                case "imports":
54✔
584
                                                        identifiers = this.ngMetaEdits.imports
14✔
585
                                                                .filter(x => alreadyImported.indexOf(x.name) === -1)
18✔
586
                                                                .map(x => TsUtils.createIdentifier(x.name, x.root ? "forRoot" : ""));
18✔
587
                                                        break;
14✔
588
                                                case "declarations":
589
                                                case "providers":
590
                                                case "exports":
591
                                                        identifiers = this.ngMetaEdits[prop]
14✔
592
                                                                .filter(x => alreadyImported.indexOf(x) === -1)
18✔
593
                                                                .map(x => ts.factory.createIdentifier(x));
18✔
594
                                                        break;
14✔
595
                                        }
596
                                        const elements = ts.factory.createNodeArray([
28✔
597
                                                ...props,
598
                                                ...identifiers
599
                                        ]);
600

601
                                        return ts.factory.updateArrayLiteralExpression(initializer, elements);
28✔
602
                                } else {
603
                                        node = ts.visitEachChild(node, visitNgModule, context);
114✔
604
                                }
605
                                return node;
114✔
606
                        };
607
                        const visitCondition: (node: ts.Node) => boolean = (node: ts.Node) => {
18✔
608
                                return node.kind === ts.SyntaxKind.CallExpression &&
240✔
609
                                        node.parent && node.parent.kind === ts.SyntaxKind.Decorator &&
610
                                        (node as ts.CallExpression).expression.getText() === "NgModule";
611
                        };
612
                        const visitor = this.createVisitor(visitNgModule, visitCondition, context);
18✔
613
                        return ts.visitNode(rootNode, visitor);
18✔
614
                }
615

616
        // TODO: extend to allow the modification of multiple metadata properties
617
        /** Transformation to apply `this.ngMetaEdits` to a standalone `Component` metadata imports */
618
        protected componentMetaTransformer: ts.TransformerFactory<ts.Node> =
60✔
619
                <T extends ts.Node>(context: ts.TransformationContext) => (rootNode: T) => {
×
620
                        const visitComponent: ts.Visitor = (node: ts.Node): ts.Node => {
×
621
                                let importsExpr = null;
×
622
                                const prop = "imports";
×
UNCOV
623
                                if (node.kind === ts.SyntaxKind.ObjectLiteralExpression &&
×
624
                                        node.parent &&
625
                                        node.parent.kind === ts.SyntaxKind.CallExpression) {
626
                                                const obj = (node as ts.ObjectLiteralExpression);
×
627
                                                const objProperties = ts.visitNodes(obj.properties, visitor);
×
628
                                                const newProps = [];
×
629
                                                const importDeps = this.ngMetaEdits.imports;
×
630
                                                importsExpr = ts.factory.createArrayLiteralExpression(
×
631
                                                        importDeps.map(x => TsUtils.createIdentifier(x.name))
×
632
                                                );
633
                                                newProps.push(ts.factory.createPropertyAssignment(prop, importsExpr));
×
634
                                                return context.factory.updateObjectLiteralExpression(obj, [
×
635
                                                        ...objProperties,
636
                                                        ...newProps
637
                                                ]);
638
                                } else {
639
                                        node = ts.visitEachChild(node, visitComponent, context);
×
640
                                }
641

642
                                return node;
×
643
                        };
644
                        const visitCondition: (node: ts.Node) => boolean = (node: ts.Node) => {
×
645
                                return node.kind === ts.SyntaxKind.CallExpression &&
×
646
                                        node.parent && node.parent.kind === ts.SyntaxKind.Decorator &&
647
                                        (node as ts.CallExpression).expression.getText() === "Component";
648
                        };
649
                        const visitor = this.createVisitor(visitComponent, visitCondition, context);
×
650
                        return ts.visitNode(rootNode, visitor);
×
651
                }
652

653
        //#endregion ts.TransformerFactory
654

655
        //#region Formatting
656

657
        /** Format a TS source file, very TBD */
658
        protected formatFile(filePath: string) {
659
                // formatting via LanguageService https://github.com/Microsoft/TypeScript/wiki/Using-the-Compiler-API
660
                // https://github.com/Microsoft/TypeScript/issues/1651
661

662
                let text = this.fileSystem.readFile(filePath);
26✔
663
                // create the language service files
664
                const services = ts.createLanguageService(this.getLanguageHost(filePath), ts.createDocumentRegistry());
26✔
665

666
                this.readFormatConfigs();
26✔
667
                const textChanges = services.getFormattingEditsForDocument(filePath, this.getFormattingOptions());
26✔
668
                text = this.applyChanges(text, textChanges);
26✔
669

670
                if (this.formatOptions.singleQuotes) {
26✔
671
                        for (const str of this.createdStringLiterals) {
14✔
672
                                // there shouldn't be duplicate strings of these
673
                                text = text.replace(`"${str}"`, `'${str}'`);
30✔
674
                        }
675
                        text = text.replace(/["]/g, "'");
14✔
676
                }
677

678
                this.fileSystem.writeFile(filePath, text);
26✔
679
        }
680

681
        /**  Try and parse formatting from project `.editorconfig` / `tslint.json` */
682
        protected readFormatConfigs() {
683
                if (this.fileSystem.fileExists(".editorconfig")) {
36✔
684
                        // very basic parsing support
685
                        const text = this.fileSystem.readFile(".editorconfig", "utf-8");
14✔
686
                        const options = text
14✔
687
                                .replace(/\s*[#;].*([\r\n])/g, "$1") //remove comments
688
                                .replace(/\[(?!\*\]|\*.ts).+\][^\[]+/g, "") // leave [*]/[*.ts] sections
689
                                .split(/\r\n|\r|\n/)
690
                                .reduce((obj, x) => {
691
                                        if (x.indexOf("=") !== -1) {
130✔
692
                                                const pair = x.split("=");
66✔
693
                                                obj[pair[0].trim()] = pair[1].trim();
66✔
694
                                        }
695
                                        return obj;
130✔
696
                                }, {});
697

698
                        this.formatOptions.spaces = options["indent_style"] === "space";
14✔
699
                        if (options["indent_size"]) {
14✔
700
                                this.formatOptions.indentSize = parseInt(options["indent_size"], 10) || this.formatOptions.indentSize;
12!
701
                        }
702
                        if (options["quote_type"]) {
14✔
703
                                this.formatOptions.singleQuotes = options["quote_type"] === "single";
10✔
704
                        }
705
                }
706
                if (this.fileSystem.fileExists("tslint.json")) {
36✔
707
                        // tslint prio - overrides other settings
708
                        const options = JSON.parse(this.fileSystem.readFile("tslint.json", "utf-8"));
18✔
709
                        if (options.rules && options.rules.indent && options.rules.indent[0]) {
18✔
710
                                this.formatOptions.spaces = options.rules.indent[1] === "spaces";
16✔
711
                                if (options.rules.indent[2]) {
16!
712
                                        this.formatOptions.indentSize = parseInt(options.rules.indent[2], 10);
16✔
713
                                }
714
                        }
715
                        if (options.rules && options.rules.quotemark && options.rules.quotemark[0]) {
18!
716
                                this.formatOptions.singleQuotes = options.rules.quotemark.indexOf("single") !== -1;
18✔
717
                        }
718
                }
719
        }
720

721
        /**
722
         * Apply formatting changes (position based) in reverse
723
         * from https://github.com/Microsoft/TypeScript/issues/1651#issuecomment-69877863
724
         */
725
        private applyChanges(orig: string, changes: ts.TextChange[]): string {
726
                let result = orig;
26✔
727
                for (let i = changes.length - 1; i >= 0; i--) {
26✔
728
                        const change = changes[i];
102✔
729
                        const head = result.slice(0, change.span.start);
102✔
730
                        const tail = result.slice(change.span.start + change.span.length);
102✔
731
                        result = head + change.newText + tail;
102✔
732
                }
733
                return result;
26✔
734
        }
735

736
        /** Return source file formatting options */
737
        private getFormattingOptions(): ts.FormatCodeSettings {
738
                const formatOptions: ts.FormatCodeSettings = {
739
                        // tslint:disable:object-literal-sort-keys
740
                        indentSize: this.formatOptions.indentSize,
741
                        tabSize: 4,
742
                        newLineCharacter: ts.sys.newLine,
743
                        convertTabsToSpaces: this.formatOptions.spaces,
744
                        indentStyle: ts.IndentStyle.Smart,
745
                        insertSpaceAfterCommaDelimiter: true,
746
                        insertSpaceAfterSemicolonInForStatements: true,
747
                        insertSpaceBeforeAndAfterBinaryOperators: true,
748
                        insertSpaceAfterKeywordsInControlFlowStatements: true,
749
                        insertSpaceAfterTypeAssertion: true
750
                        // tslint:enable:object-literal-sort-keys
751
                };
752

753
                return formatOptions;
26✔
754
        }
755

756
        /** Get language service host, sloppily */
757
        private getLanguageHost(filePath: string): ts.LanguageServiceHost {
758
                const files = {};
26✔
759
                files[filePath] = { version: 0 };
26✔
760
                // create the language service host to allow the LS to communicate with the host
761
                const servicesHost: ts.LanguageServiceHost = {
762
                        getCompilationSettings: () => ({}),
52✔
763
                        getScriptFileNames: () => Object.keys(files),
×
764
                        getScriptVersion: fileName => files[fileName] && files[fileName].version.toString(),
26✔
765
                        getScriptSnapshot: fileName => {
766
                                if (!this.fileSystem.fileExists(fileName)) {
26!
767
                                        return undefined;
×
768
                                }
769
                                return ts.ScriptSnapshot.fromString(this.fileSystem.readFile(fileName));
26✔
770
                        },
771
                        getCurrentDirectory: () => process.cwd(),
52✔
772
                        getDefaultLibFileName: options => ts.getDefaultLibFilePath(options),
×
773
                        readDirectory: ts.sys.readDirectory,
774
                        readFile: ts.sys.readFile,
775
                        fileExists: ts.sys.fileExists
776
                };
777
                return servicesHost;
26✔
778
        }
779

780
        //#endregion Formatting
781

782
        /** Convert a string or string array union to array. Splits strings as comma delimited */
783
        private asArray(value: string | string[], variables: { [key: string]: string }): string[] {
784
                let result: string[] = [];
212✔
785
                if (value) {
212✔
786
                        result = typeof value === "string" ? value.split(/\s*,\s*/) : value;
74✔
787
                        result = result.map(x => Util.applyConfigTransformation(x, variables));
80✔
788
                }
789
                return result;
212✔
790
        }
791

792
        private createVisitor(
793
                conditionalVisitor: ts.Visitor,
794
                visitCondition: (node: ts.Node) => boolean,
795
                nodeContext: ts.TransformationContext
796
        ): ts.Visitor {
797
                return function visitor(node: ts.Node): ts.Node {
38✔
798
                        if (visitCondition(node)) {
514✔
799
                                node = ts.visitEachChild(node, conditionalVisitor, nodeContext);
38✔
800
                        } else {
801
                                node = ts.visitEachChild(node, visitor, nodeContext);
476✔
802
                        }
803
                        return node;
514✔
804
                };
805
        }
806

807
        private createRouteEntry(
808
                linkPath: string,
809
                className: string,
810
                linkText: string,
811
                lazyload = false,
4✔
812
                routesPath = "",
4✔
813
                root = false,
4✔
814
                isDefault = false
16✔
815
        ): ts.ObjectLiteralExpression {
816
                const routePath = ts.factory.createPropertyAssignment("path", ts.factory.createStringLiteral(linkPath));
16✔
817
                if (isDefault) {
16!
818
                        const routeRedirectTo = ts.factory.createPropertyAssignment("redirectTo",
×
819
                                ts.factory.createStringLiteral(routesPath));
820
                        const routePathMatch = ts.factory.createPropertyAssignment("pathMatch",
×
821
                                ts.factory.createStringLiteral("full"));
822
                        return ts.factory.createObjectLiteralExpression([routePath, routeRedirectTo, routePathMatch]);
×
823
                }
824
                let routeComponent;
825
                // TODO: we should consider using the ts.factory instead of string interpolations
826
                if (lazyload) {
16!
UNCOV
827
                        if (root) {
×
828
                                routeComponent = ts
×
829
                                .factory
830
                                .createPropertyAssignment("loadChildren",
831
                                ts.factory.createIdentifier(`() => import('${routesPath}').then(m => m.routes)`));
832
                        } else {
833
                                routeComponent = ts
×
834
                                .factory
835
                                .createPropertyAssignment("loadComponent",
836
                                ts.factory.createIdentifier(`() => import('./${linkPath}/${linkPath}.component').then(m => m.${className})`));
837
                        }
838
                } else {
839
                        routeComponent = ts.factory.createPropertyAssignment("component", ts.factory.createIdentifier(className));
16✔
840
                }
841
                const routeDataInner = ts.factory.createPropertyAssignment("text", ts.factory.createStringLiteral(linkText));
16✔
842
                const routeData = ts.factory.createPropertyAssignment(
16✔
843
                        "data", ts.factory.createObjectLiteralExpression([routeDataInner]));
844
                return ts.factory.createObjectLiteralExpression([routePath, routeComponent, routeData]);
16✔
845
        }
846

847
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc