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

IgniteUI / igniteui-cli / 9957168548

16 Jul 2024 12:49PM UTC coverage: 66.802% (-0.09%) from 66.896%
9957168548

push

github

web-flow
Adding schema to template dependency (#1269)

1161 of 1855 branches covered (62.59%)

Branch coverage included in aggregate %.

11 of 30 new or added lines in 1 file covered. (36.67%)

1 existing line in 1 file now uncovered.

4765 of 7016 relevant lines covered (67.92%)

149.64 hits per line

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

83.09
/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
                schemas: Array<{ name: string, standalone?: boolean }>,
29
                exports: string[]
30
        };
31

32
        private createdStringLiterals: string[];
33

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

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

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

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

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

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

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

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

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

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

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

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

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

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

168
                const schemas = copy.schema
52✔
NEW
169
                        .map(x => ({ name: x, root: dep.root }))
×
NEW
170
                        .filter(x => !this.ngMetaEdits.schemas.find(s => s.name === x.name));
×
171
                this.ngMetaEdits.schemas.push(...schemas);
52✔
172

173
                const exportsArr = copy.export
52✔
174
                        .filter(x => !this.ngMetaEdits.exports.find(p => p === x));
×
175
                this.ngMetaEdits.exports.push(...exportsArr);
52✔
176
        }
177

178
        /**
179
         * Updates a standalone component's imports metadata.
180
         */
181
        public addStandaloneImport(dep: TemplateDependency, variables?: { [key: string]: string }) {
182
                const copy = {
183
                        import: this.asArray(dep.import, variables),
184
                        provide: this.asArray(dep.provide, variables),
185
                        schema: this.asArray(dep.schema, variables)
186
                };
187
                if (dep.from) {
×
188
                        // request import
NEW
189
                        const identifiers = [...copy.import, ...copy.provide, ...copy.schema];
×
190
                        this.requestImport(identifiers, Util.applyConfigTransformation(dep.from, variables));
×
191
                }
192

193
                const imports = copy.import
×
194
                        .map(x => ({ name: x, root: dep.root, standalone: true }))
×
195
                        .filter(x => !this.ngMetaEdits.imports.find(i => i.name === x.name));
×
196
                this.ngMetaEdits.imports.push(...imports);
×
197

NEW
198
                const schemas = copy.schema
×
NEW
199
                        .map(x => ({ name: x, root: dep.root, standalone: true}))
×
NEW
200
                        .filter(x => !this.ngMetaEdits.schemas.find(i => i.name === x.name));
×
NEW
201
                this.ngMetaEdits.schemas.push(...schemas);
×
202
        }
203

204
        /**
205
         * Create a CallExpression for dep and add it to the `ApplicationConfig` providers array.
206
         * @param dep The dependency to provide. TODO: Use different type to describe CallExpression, possible parameters, etc
207
         * @param configVariable The name of the app config variable to edit
208
         */
209
        public addAppConfigProvider(
210
                dep: Pick<TemplateDependency, "provide" | "from">,
211
                configVariable = DEFAULT_APPCONFIG_VARIABLE) {
4✔
212
                let providers = this.asArray(dep.provide, {});
4✔
213

214
                const transformer: ts.TransformerFactory<ts.SourceFile> = <T extends ts.SourceFile>(context: ts.TransformationContext) =>
4✔
215
                (rootNode: T) => {
4✔
216
                        const conditionalVisitor: ts.Visitor = (node: ts.Node): ts.Node => {
4✔
217
                                if (node.kind === ts.SyntaxKind.ArrayLiteralExpression &&
28✔
218
                                        node.parent.kind === ts.SyntaxKind.PropertyAssignment &&
219
                                        (node.parent as ts.PropertyAssignment).name.getText() === "providers") {
220
                                        const array = (node as ts.ArrayLiteralExpression);
4✔
221
                                        const nodes = ts.visitNodes(array.elements, visitor, ts.isExpression);
4✔
222
                                        const alreadyProvided = nodes.map(x => TsUtils.getIdentifierName(x));
6✔
223

224
                                        providers =  providers.filter(x => alreadyProvided.indexOf(x) === -1);
4✔
225
                                        this.requestImport(providers, dep.from);
4✔
226

227
                                        const newProvides = providers
4✔
228
                                                .map(x => ts.factory.createCallExpression(ts.factory.createIdentifier(x), undefined, undefined));
2✔
229
                                        const elements = ts.factory.createNodeArray<ts.Expression>([
4✔
230
                                                ...nodes,
231
                                                ...newProvides
232
                                        ]);
233

234
                                        return ts.factory.updateArrayLiteralExpression(array, elements);
4✔
235
                                } else {
236
                                        return ts.visitEachChild(node, conditionalVisitor, context);
24✔
237
                                }
238
                        };
239

240
                        const visitCondition = (node: ts.Node): boolean => {
4✔
241
                                return node.kind === ts.SyntaxKind.VariableDeclaration &&
120✔
242
                                        (node as ts.VariableDeclaration).name.getText() === configVariable &&
243
                                        (node as ts.VariableDeclaration).type.getText() === "ApplicationConfig";
244
                        };
245
                        const visitor: ts.Visitor = this.createVisitor(conditionalVisitor, visitCondition, context);
4✔
246
                        context.enableSubstitution(ts.SyntaxKind.ArrayLiteralExpression);
4✔
247
                        return ts.visitNode(rootNode, visitor, ts.isSourceFile);
4✔
248
                };
249
                this.targetSource = this.transform(this.targetSource, [transformer], {
4✔
250
                        pretty: true // oh well..
251
                }).transformed[0];
252
        }
253

254
        //#region File state
255

256
        /** Initializes existing imports info, [re]sets import and `NgModule` edits */
257
        protected initState() {
258
                this.targetSource = TsUtils.getFileSource(this.targetPath);
104✔
259
                this.importsMeta = this.loadImportsMeta();
104✔
260
                this.requestedImports = [];
104✔
261
                this.ngMetaEdits = {
104✔
262
                        declarations: [],
263
                        imports: [],
264
                        providers: [],
265
                        schemas: [],
266
                        exports: []
267
                };
268
                this.createdStringLiterals = [];
104✔
269
        }
270

271
        /* load some metadata about imports */
272
        protected loadImportsMeta() {
273
                const meta = { lastIndex: 0, modulePaths: [] };
102✔
274

275
                for (let i = 0; i < this.targetSource.statements.length; i++) {
102✔
276
                        const statement = this.targetSource.statements[i];
200✔
277
                        switch (statement.kind) {
200✔
278
                                case ts.SyntaxKind.ImportDeclaration:
314✔
279
                                        const importStmt = (statement as ts.ImportDeclaration);
114✔
280

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

288
                                // don't add equals imports (`import url = require("url")`) as available for editing, maybe in the future
289
                                case ts.SyntaxKind.ImportEqualsDeclaration:
290
                                        meta.lastIndex = i + 1;
116✔
291
                                        break;
116✔
292
                                default:
293
                                        break;
84✔
294
                        }
295
                }
296

297
                return meta;
102✔
298
        }
299

300
        //#endregion File state
301

302
        protected addRouteModuleEntry(
303
                filePath: string,
304
                linkPath: string,
305
                linkText: string,
306
                routesVariable = DEFAULT_ROUTES_VARIABLE,
×
307
                parentRoutePath?: string,
308
                lazyload = false,
×
309
                routesPath = "",
×
310
                root = false,
×
311
                isDefault = false
×
312
        ) {
313
                let className: string;
314
                const fileSource = TsUtils.getFileSource(filePath);
16✔
315
                const relativePath: string = Util.relativePath(this.targetPath, filePath, true, true);
16✔
316
                className = TsUtils.getClassName(fileSource.getChildren());
16✔
317

318
                if (!lazyload) {
16!
319
                        this.requestImport([className], relativePath);
16✔
320
                }
321

322
                // https://github.com/Microsoft/TypeScript/issues/14419#issuecomment-307256171
323
                const transformer: ts.TransformerFactory<ts.Node> = <T extends ts.Node>(context: ts.TransformationContext) =>
16✔
324
                        (rootNode: T) => {
16✔
325
                                let conditionalVisitor: ts.Visitor;
326
                                // the visitor that should be used when adding routes to the main route array
327
                                const routeArrayVisitor = (node: ts.Node): ts.Node => {
16✔
328
                                        if (node.kind === ts.SyntaxKind.ArrayLiteralExpression) {
48✔
329
                                                const newObject = this.createRouteEntry(linkPath, className, linkText, lazyload, routesPath, root);
12✔
330
                                                const array = (node as ts.ArrayLiteralExpression);
12✔
331
                                                this.createdStringLiterals.push(linkPath, linkText);
12✔
332
                                                const notFoundWildCard = "**";
12✔
333
                                                const nodes = ts.visitNodes(array.elements, visitor);
12✔
334
                                                const errorRouteNode = nodes.filter(element => element.getText().includes(notFoundWildCard))[0];
12✔
335
                                                let resultNodes = null;
12✔
336
                                                if (errorRouteNode) {
12!
337
                                                        resultNodes = nodes
×
338
                                                                .slice(0, nodes.indexOf(errorRouteNode))
339
                                                                .concat(newObject)
340
                                                                .concat(errorRouteNode);
341
                                                } else {
342
                                                        resultNodes = nodes
12✔
343
                                                                .concat(newObject);
344
                                                }
345

346
                                                const elements = ts.factory.createNodeArray([
12✔
347
                                                        ...resultNodes
348
                                                ]);
349

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

391
                                                // if the target parent route already has child routes - get them
392
                                                // if not - create an empty 'chuldren' array
393
                                                if (childrenProperty) {
4✔
394
                                                        childrenArray = childrenProperty.getChildren()
2!
395
                                                                .filter(element => element.kind === ts.SyntaxKind.ArrayLiteralExpression)[0] as ts.ArrayLiteralExpression
6✔
396
                                                                || ts.factory.createArrayLiteralExpression();
397
                                                } else {
398
                                                        childrenArray = ts.factory.createArrayLiteralExpression();
2✔
399
                                                }
400

401
                                                let existingProperties = syntaxList.getChildren()
4✔
402
                                                        .filter(element => element.kind !== ts.SyntaxKind["CommaToken"]) as ts.ObjectLiteralElementLike[];
16✔
403
                                                let newArrayValues: ts.Expression[];
404
                                                if (isDefault) {
4!
405
                                                        newArrayValues = childrenArray.elements.concat(defaultRoute, newObject);
×
406
                                                } else {
407
                                                        newArrayValues = childrenArray.elements.concat(newObject);
4✔
408
                                                }
409
                                                if (!childrenProperty) {
4✔
410
                                                        const propertyName = "children";
2✔
411
                                                        const propertyValue = ts.factory.createArrayLiteralExpression([...newArrayValues]);
2✔
412
                                                        childrenProperty = ts.factory.createPropertyAssignment(propertyName, propertyValue);
2✔
413
                                                        existingProperties = existingProperties
2✔
414
                                                                .concat(childrenProperty);
415
                                                } else {
416
                                                        const index = existingProperties.indexOf(childrenProperty);
2✔
417
                                                        const childrenPropertyName = childrenProperty.name;
2✔
418
                                                        childrenProperty =
2✔
419
                                                                ts.factory.updatePropertyAssignment(
420
                                                                        childrenProperty,
421
                                                                        childrenPropertyName,
422
                                                                        ts.factory.createArrayLiteralExpression([...newArrayValues])
423
                                                                );
424
                                                        existingProperties
2✔
425
                                                                .splice(index, 1, childrenProperty);
426
                                                }
427
                                                return ts.factory.updateObjectLiteralExpression(currentNode, existingProperties) as ts.Node;
4✔
428
                                        } else {
429
                                                return ts.visitEachChild(node, conditionalVisitor, context);
16✔
430
                                        }
431
                                };
432

433
                                if (parentRoutePath === null) {
16✔
434
                                        conditionalVisitor = routeArrayVisitor;
12✔
435
                                } else {
436
                                        conditionalVisitor = parentRouteVisitor;
4✔
437
                                }
438
                                const visitCondition = (node: ts.Node): boolean => {
16✔
439
                                        return node.kind === ts.SyntaxKind.VariableDeclaration &&
154✔
440
                                                (node as ts.VariableDeclaration).name.getText() === routesVariable &&
441
                                                (node as ts.VariableDeclaration).type.getText() === "Routes";
442
                                };
443
                                const visitor: ts.Visitor = this.createVisitor(conditionalVisitor, visitCondition, context);
16✔
444
                                context.enableSubstitution(ts.SyntaxKind.ClassDeclaration);
16✔
445
                                return ts.visitNode(rootNode, visitor);
16✔
446
                        };
447

448
                this.targetSource = this.transform(this.targetSource, [transformer], {
16✔
449
                        pretty: true // oh well..
450
                }).transformed[0] as ts.SourceFile;
451

452
                this.finalize();
16✔
453
        }
454

455
        /**
456
         * Add named imports from a path/package.
457
         * @param identifiers Strings to create named import from ("Module" => `import { Module }`)
458
         * @param modulePath Module specifier - can be path to file or npm package, etc
459
         */
460
        protected requestImport(identifiers: string[], modulePath: string) {
461
                const existing = this.requestedImports.find(x => x.from === modulePath);
70✔
462
                if (!existing) {
70✔
463
                        // new imports, check if already exists in file
464
                        this.requestedImports.push({
56✔
465
                                from: modulePath, imports: identifiers,
466
                                edit: this.importsMeta.modulePaths.indexOf(modulePath) !== -1
467
                        });
468
                } else {
469
                        const newNamedImports = identifiers.filter(x => existing.imports.indexOf(x) === -1);
20✔
470
                        existing.imports.push(...newNamedImports);
14✔
471
                }
472
        }
473

474
        /** Add `import` statements not previously found in the file  */
475
        protected addNewFileImports() {
476
                const newImports = this.requestedImports.filter(x => !x.edit);
54✔
477
                if (!newImports.length) {
44✔
478
                        return;
12✔
479
                }
480

481
                const newStatements = ts.factory.createNodeArray([
32✔
482
                        ...this.targetSource.statements.slice(0, this.importsMeta.lastIndex),
483
                        ...newImports.map(x => TsUtils.createIdentifierImport(x.imports, x.from)),
40✔
484
                        ...this.targetSource.statements.slice(this.importsMeta.lastIndex)
485
                ]);
486
                newImports.forEach(x => this.createdStringLiterals.push(x.from));
40✔
487

488
                this.targetSource = ts.factory.updateSourceFile(this.targetSource, newStatements);
32✔
489
        }
490

491
        //#region ts.TransformerFactory
492

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

498
                        // https://github.com/Microsoft/TypeScript/issues/14419#issuecomment-307256171
499
                        const visitor = (node: ts.Node): ts.Node => {
10✔
500
                                if (node.kind === ts.SyntaxKind.ImportDeclaration &&
216✔
501
                                        editImports.find(x => x.from === ((node as ts.ImportDeclaration).moduleSpecifier as ts.StringLiteral).text)
26✔
502
                                ) {
503
                                        // visit just the source file main array (second visit)
504
                                        return visitImport(node as ts.ImportDeclaration);
14✔
505
                                } else {
506
                                        node = ts.visitEachChild(node, visitor, context);
202✔
507
                                }
508
                                return node;
202✔
509
                        };
510
                        function visitImport(node: ts.Node) {
511
                                if (node.kind === ts.SyntaxKind.NamedImports) {
56✔
512
                                        const namedImports = node as ts.NamedImports;
14✔
513
                                        const moduleSpecifier = (namedImports.parent.parent.moduleSpecifier as ts.StringLiteral).text;
14✔
514

515
                                        const existing = ts.visitNodes(namedImports.elements, visitor, ts.isImportSpecifier);
14✔
516
                                        const alreadyImported = existing.map(x => ts.isImportSpecifier(x) && x.name.text);
14✔
517

518
                                        const editImport = editImports.find(x => x.from === moduleSpecifier);
18✔
519
                                        const newImports = editImport.imports.filter(x => alreadyImported.indexOf(x) === -1);
14✔
520

521
                                        node = ts.factory.updateNamedImports(namedImports, [
14✔
522
                                                ...existing,
523
                                                ...newImports.map(x => ts.factory.createImportSpecifier(false, undefined, ts.factory.createIdentifier(x)))
12✔
524
                                        ]);
525
                                } else {
526
                                        node = ts.visitEachChild(node, visitImport, context);
42✔
527
                                }
528
                                return node;
56✔
529
                        }
530
                        return ts.visitNode(rootNode, visitor);
10✔
531
                }
532

533
        /** Transformation to apply `this.ngMetaEdits` to `NgModule` metadata properties */
534
        protected ngModuleTransformer: ts.TransformerFactory<ts.Node> =
60✔
535
                <T extends ts.Node>(context: ts.TransformationContext) => (rootNode: T) => {
18✔
536
                        const visitNgModule: ts.Visitor = (node: ts.Node): ts.Node => {
18✔
537
                                const properties: string[] = []; // "declarations", "imports", "providers"
160✔
538
                                for (const key in this.ngMetaEdits) {
160✔
539
                                        if (this.ngMetaEdits[key].length) {
800✔
540
                                                properties.push(key);
296✔
541
                                        }
542
                                }
543
                                if (node.kind === ts.SyntaxKind.ObjectLiteralExpression &&
160✔
544
                                        node.parent &&
545
                                        node.parent.kind === ts.SyntaxKind.CallExpression) {
546

547
                                        let obj = (node as ts.ObjectLiteralExpression);
18✔
548

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

552
                                        // skip visiting if no declaration/imports/providers arrays exist:
553
                                        if (missingProperties.length !== properties.length) {
18!
554
                                                obj = ts.visitEachChild(node, visitNgModule, context) as ts.ObjectLiteralExpression;
18✔
555
                                        }
556

557
                                        if (!missingProperties.length) {
18✔
558
                                                return obj;
16✔
559
                                        }
560

561
                                        const objProperties = ts.visitNodes(obj.properties, visitor);
2✔
562
                                        const newProps = [];
2✔
563
                                        for (const prop of missingProperties) {
2✔
564
                                                if (this.ngMetaEdits[prop].length) {
4!
565
                                                        const expr = ts.factory.createArrayLiteralExpression(
4✔
566
                                                                this.ngMetaEdits[prop].map(x => {
567
                                                                        if (typeof x === "string") {
6✔
568
                                                                                return TsUtils.createIdentifier(x);
2✔
569
                                                                        }
570
                                                                        if (typeof x === "object" && "name" in x) {
4!
571
                                                                                return TsUtils.createIdentifier(x.name, x.root ? "forRoot" : "")
4!
572
                                                                        }
573
                                                                })
574
                                                        );
575
                                                        newProps.push(ts.factory.createPropertyAssignment(prop, expr));
4✔
576
                                                }
577
                                        }
578

579
                                        return ts.factory.updateObjectLiteralExpression(obj, [
2✔
580
                                                ...objProperties,
581
                                                ...newProps
582
                                        ]);
583
                                } else if (node.kind === ts.SyntaxKind.ArrayLiteralExpression &&
142✔
584
                                        node.parent.kind === ts.SyntaxKind.PropertyAssignment &&
585
                                        properties.indexOf((node.parent as ts.PropertyAssignment).name.getText()) !== -1) {
586
                                        const initializer = (node as ts.ArrayLiteralExpression);
28✔
587
                                        const props = ts.visitNodes(initializer.elements, visitor);
28✔
588
                                        const alreadyImported = props.map(x => TsUtils.getIdentifierName(x));
34✔
589
                                        const prop = properties.find(x => x === (node.parent as ts.PropertyAssignment).name.getText());
40✔
590

591
                                        let identifiers = [];
28✔
592
                                        switch (prop) {
28✔
593
                                                case "imports":
54!
594
                                                        identifiers = this.ngMetaEdits.imports
14✔
595
                                                                .filter(x => alreadyImported.indexOf(x.name) === -1)
18✔
596
                                                                .map(x => TsUtils.createIdentifier(x.name, x.root ? "forRoot" : ""));
18✔
597
                                                        break;
14✔
598
                                                case "schemas":
NEW
599
                                                        identifiers = this.ngMetaEdits.schemas
×
NEW
600
                                                                .filter(x => alreadyImported.indexOf(x.name) === -1)
×
NEW
601
                                                                .map(x => TsUtils.createIdentifier(x.name));
×
NEW
602
                                                        break;
×
603
                                                case "declarations":
604
                                                case "providers":
605
                                                case "exports":
606
                                                        identifiers = this.ngMetaEdits[prop]
14✔
607
                                                                .filter(x => alreadyImported.indexOf(x) === -1)
18✔
608
                                                                .map(x => ts.factory.createIdentifier(x));
18✔
609
                                                        break;
14✔
610
                                        }
611
                                        const elements = ts.factory.createNodeArray([
28✔
612
                                                ...props,
613
                                                ...identifiers
614
                                        ]);
615

616
                                        return ts.factory.updateArrayLiteralExpression(initializer, elements);
28✔
617
                                } else {
618
                                        node = ts.visitEachChild(node, visitNgModule, context);
114✔
619
                                }
620
                                return node;
114✔
621
                        };
622
                        const visitCondition: (node: ts.Node) => boolean = (node: ts.Node) => {
18✔
623
                                return node.kind === ts.SyntaxKind.CallExpression &&
240✔
624
                                        node.parent && node.parent.kind === ts.SyntaxKind.Decorator &&
625
                                        (node as ts.CallExpression).expression.getText() === "NgModule";
626
                        };
627
                        const visitor = this.createVisitor(visitNgModule, visitCondition, context);
18✔
628
                        return ts.visitNode(rootNode, visitor);
18✔
629
                }
630

631
        // TODO: extend to allow the modification of multiple metadata properties
632
        /** Transformation to apply `this.ngMetaEdits` to a standalone `Component` metadata imports */
633
        protected componentMetaTransformer: ts.TransformerFactory<ts.Node> =
60✔
634
                <T extends ts.Node>(context: ts.TransformationContext) => (rootNode: T) => {
×
635
                        const visitComponent: ts.Visitor = (node: ts.Node): ts.Node => {
×
NEW
636
                                const properties = Object.keys(this.ngMetaEdits);
×
637
                                if (node.kind === ts.SyntaxKind.ObjectLiteralExpression &&
×
638
                                        node.parent &&
639
                                        node.parent.kind === ts.SyntaxKind.CallExpression) {
640
                                                const obj = (node as ts.ObjectLiteralExpression);
×
641
                                                const objProperties = ts.visitNodes(obj.properties, visitor);
×
642
                                                const newProps = [];
×
NEW
643
                                                const missingProperties = properties.filter(x => !obj.properties.find(o => o.name.getText() === x));
×
NEW
644
                                                for (const prop of missingProperties) {
×
NEW
645
                                                        if (this.ngMetaEdits[prop].length) {
×
NEW
646
                                                                const expr = ts.factory.createArrayLiteralExpression(
×
NEW
647
                                                                        this.ngMetaEdits[prop].map(x => TsUtils.createIdentifier(x.name))
×
648
                                                                );
NEW
649
                                                                newProps.push(ts.factory.createPropertyAssignment(prop, expr));
×
650
                                                        }
651
                                                }
UNCOV
652
                                                return context.factory.updateObjectLiteralExpression(obj, [
×
653
                                                        ...objProperties,
654
                                                        ...newProps
655
                                                ]);
656
                                } else {
657
                                        node = ts.visitEachChild(node, visitComponent, context);
×
658
                                }
659

660
                                return node;
×
661
                        };
662
                        const visitCondition: (node: ts.Node) => boolean = (node: ts.Node) => {
×
663
                                return node.kind === ts.SyntaxKind.CallExpression &&
×
664
                                        node.parent && node.parent.kind === ts.SyntaxKind.Decorator &&
665
                                        (node as ts.CallExpression).expression.getText() === "Component";
666
                        };
667
                        const visitor = this.createVisitor(visitComponent, visitCondition, context);
×
668
                        return ts.visitNode(rootNode, visitor);
×
669
                }
670

671
        //#endregion ts.TransformerFactory
672

673
        //#region Formatting
674

675
        /** Format a TS source file, very TBD */
676
        protected formatFile(filePath: string) {
677
                // formatting via LanguageService https://github.com/Microsoft/TypeScript/wiki/Using-the-Compiler-API
678
                // https://github.com/Microsoft/TypeScript/issues/1651
679

680
                let text = this.fileSystem.readFile(filePath);
26✔
681
                // create the language service files
682
                const services = ts.createLanguageService(this.getLanguageHost(filePath), ts.createDocumentRegistry());
26✔
683

684
                this.readFormatConfigs();
26✔
685
                const textChanges = services.getFormattingEditsForDocument(filePath, this.getFormattingOptions());
26✔
686
                text = this.applyChanges(text, textChanges);
26✔
687

688
                if (this.formatOptions.singleQuotes) {
26✔
689
                        for (const str of this.createdStringLiterals) {
14✔
690
                                // there shouldn't be duplicate strings of these
691
                                text = text.replace(`"${str}"`, `'${str}'`);
30✔
692
                        }
693
                        text = text.replace(/["]/g, "'");
14✔
694
                }
695

696
                this.fileSystem.writeFile(filePath, text);
26✔
697
        }
698

699
        /**  Try and parse formatting from project `.editorconfig` / `tslint.json` */
700
        protected readFormatConfigs() {
701
                if (this.fileSystem.fileExists(".editorconfig")) {
36✔
702
                        // very basic parsing support
703
                        const text = this.fileSystem.readFile(".editorconfig", "utf-8");
14✔
704
                        const options = text
14✔
705
                                .replace(/\s*[#;].*([\r\n])/g, "$1") //remove comments
706
                                .replace(/\[(?!\*\]|\*.ts).+\][^\[]+/g, "") // leave [*]/[*.ts] sections
707
                                .split(/\r\n|\r|\n/)
708
                                .reduce((obj, x) => {
709
                                        if (x.indexOf("=") !== -1) {
130✔
710
                                                const pair = x.split("=");
66✔
711
                                                obj[pair[0].trim()] = pair[1].trim();
66✔
712
                                        }
713
                                        return obj;
130✔
714
                                }, {});
715

716
                        this.formatOptions.spaces = options["indent_style"] === "space";
14✔
717
                        if (options["indent_size"]) {
14✔
718
                                this.formatOptions.indentSize = parseInt(options["indent_size"], 10) || this.formatOptions.indentSize;
12!
719
                        }
720
                        if (options["quote_type"]) {
14✔
721
                                this.formatOptions.singleQuotes = options["quote_type"] === "single";
10✔
722
                        }
723
                }
724
                if (this.fileSystem.fileExists("tslint.json")) {
36✔
725
                        // tslint prio - overrides other settings
726
                        const options = JSON.parse(this.fileSystem.readFile("tslint.json", "utf-8"));
18✔
727
                        if (options.rules && options.rules.indent && options.rules.indent[0]) {
18✔
728
                                this.formatOptions.spaces = options.rules.indent[1] === "spaces";
16✔
729
                                if (options.rules.indent[2]) {
16!
730
                                        this.formatOptions.indentSize = parseInt(options.rules.indent[2], 10);
16✔
731
                                }
732
                        }
733
                        if (options.rules && options.rules.quotemark && options.rules.quotemark[0]) {
18!
734
                                this.formatOptions.singleQuotes = options.rules.quotemark.indexOf("single") !== -1;
18✔
735
                        }
736
                }
737
        }
738

739
        /**
740
         * Apply formatting changes (position based) in reverse
741
         * from https://github.com/Microsoft/TypeScript/issues/1651#issuecomment-69877863
742
         */
743
        private applyChanges(orig: string, changes: ts.TextChange[]): string {
744
                let result = orig;
26✔
745
                for (let i = changes.length - 1; i >= 0; i--) {
26✔
746
                        const change = changes[i];
102✔
747
                        const head = result.slice(0, change.span.start);
102✔
748
                        const tail = result.slice(change.span.start + change.span.length);
102✔
749
                        result = head + change.newText + tail;
102✔
750
                }
751
                return result;
26✔
752
        }
753

754
        /** Return source file formatting options */
755
        private getFormattingOptions(): ts.FormatCodeSettings {
756
                const formatOptions: ts.FormatCodeSettings = {
757
                        // tslint:disable:object-literal-sort-keys
758
                        indentSize: this.formatOptions.indentSize,
759
                        tabSize: 4,
760
                        newLineCharacter: ts.sys.newLine,
761
                        convertTabsToSpaces: this.formatOptions.spaces,
762
                        indentStyle: ts.IndentStyle.Smart,
763
                        insertSpaceAfterCommaDelimiter: true,
764
                        insertSpaceAfterSemicolonInForStatements: true,
765
                        insertSpaceBeforeAndAfterBinaryOperators: true,
766
                        insertSpaceAfterKeywordsInControlFlowStatements: true,
767
                        insertSpaceAfterTypeAssertion: true
768
                        // tslint:enable:object-literal-sort-keys
769
                };
770

771
                return formatOptions;
26✔
772
        }
773

774
        /** Get language service host, sloppily */
775
        private getLanguageHost(filePath: string): ts.LanguageServiceHost {
776
                const files = {};
26✔
777
                files[filePath] = { version: 0 };
26✔
778
                // create the language service host to allow the LS to communicate with the host
779
                const servicesHost: ts.LanguageServiceHost = {
780
                        getCompilationSettings: () => ({}),
52✔
781
                        getScriptFileNames: () => Object.keys(files),
×
782
                        getScriptVersion: fileName => files[fileName] && files[fileName].version.toString(),
26✔
783
                        getScriptSnapshot: fileName => {
784
                                if (!this.fileSystem.fileExists(fileName)) {
26!
785
                                        return undefined;
×
786
                                }
787
                                return ts.ScriptSnapshot.fromString(this.fileSystem.readFile(fileName));
26✔
788
                        },
789
                        getCurrentDirectory: () => process.cwd(),
52✔
790
                        getDefaultLibFileName: options => ts.getDefaultLibFilePath(options),
×
791
                        readDirectory: ts.sys.readDirectory,
792
                        readFile: ts.sys.readFile,
793
                        fileExists: ts.sys.fileExists
794
                };
795
                return servicesHost;
26✔
796
        }
797

798
        //#endregion Formatting
799

800
        /** Convert a string or string array union to array. Splits strings as comma delimited */
801
        private asArray(value: string | string[], variables: { [key: string]: string }): string[] {
802
                let result: string[] = [];
264✔
803
                if (value) {
264✔
804
                        result = typeof value === "string" ? value.split(/\s*,\s*/) : value;
74✔
805
                        result = result.map(x => Util.applyConfigTransformation(x, variables));
80✔
806
                }
807
                return result;
264✔
808
        }
809

810
        private createVisitor(
811
                conditionalVisitor: ts.Visitor,
812
                visitCondition: (node: ts.Node) => boolean,
813
                nodeContext: ts.TransformationContext
814
        ): ts.Visitor {
815
                return function visitor(node: ts.Node): ts.Node {
38✔
816
                        if (visitCondition(node)) {
514✔
817
                                node = ts.visitEachChild(node, conditionalVisitor, nodeContext);
38✔
818
                        } else {
819
                                node = ts.visitEachChild(node, visitor, nodeContext);
476✔
820
                        }
821
                        return node;
514✔
822
                };
823
        }
824

825
        private createRouteEntry(
826
                linkPath: string,
827
                className: string,
828
                linkText: string,
829
                lazyload = false,
4✔
830
                routesPath = "",
4✔
831
                root = false,
4✔
832
                isDefault = false
16✔
833
        ): ts.ObjectLiteralExpression {
834
                const routePath = ts.factory.createPropertyAssignment("path", ts.factory.createStringLiteral(linkPath));
16✔
835
                if (isDefault) {
16!
836
                        const routeRedirectTo = ts.factory.createPropertyAssignment("redirectTo",
×
837
                                ts.factory.createStringLiteral(routesPath));
838
                        const routePathMatch = ts.factory.createPropertyAssignment("pathMatch",
×
839
                                ts.factory.createStringLiteral("full"));
840
                        return ts.factory.createObjectLiteralExpression([routePath, routeRedirectTo, routePathMatch]);
×
841
                }
842
                let routeComponent;
843
                // TODO: we should consider using the ts.factory instead of string interpolations
844
                if (lazyload) {
16!
845
                        if (root) {
×
846
                                routeComponent = ts
×
847
                                .factory
848
                                .createPropertyAssignment("loadChildren",
849
                                ts.factory.createIdentifier(`() => import('${routesPath}').then(m => m.routes)`));
850
                        } else {
851
                                routeComponent = ts
×
852
                                .factory
853
                                .createPropertyAssignment("loadComponent",
854
                                ts.factory.createIdentifier(`() => import('./${linkPath}/${linkPath}.component').then(m => m.${className})`));
855
                        }
856
                } else {
857
                        routeComponent = ts.factory.createPropertyAssignment("component", ts.factory.createIdentifier(className));
16✔
858
                }
859
                const routeDataInner = ts.factory.createPropertyAssignment("text", ts.factory.createStringLiteral(linkText));
16✔
860
                const routeData = ts.factory.createPropertyAssignment(
16✔
861
                        "data", ts.factory.createObjectLiteralExpression([routeDataInner]));
862
                return ts.factory.createObjectLiteralExpression([routePath, routeComponent, routeData]);
16✔
863
        }
864

865
}
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