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

IgniteUI / igniteui-cli / 7165317954

11 Dec 2023 09:18AM UTC coverage: 65.923% (-0.4%) from 66.277%
7165317954

push

github

web-flow
Merge pull request #1174 from IgniteUI/bpenkov/standalone-components

Update to standalone components

354 of 568 branches covered (0.0%)

Branch coverage included in aggregate %.

4 of 29 new or added lines in 2 files covered. (13.79%)

2 existing lines in 1 file now uncovered.

3842 of 5797 relevant lines covered (66.28%)

79.2 hits per line

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

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

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

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

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

30
        private createdStringLiterals: string[];
31

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

38
        /** Applies accumulated transforms, saves and formats the file */
39
        public finalize() {
40
                const transforms = [];
20✔
41
                // walk AST for modifications.
42
                if (this.requestedImports.filter(x => x.edit).length) {
25✔
43
                        transforms.push(this.importsTransformer);
4✔
44
                }
45

46
                // should we support both standalone and module-based components in the same app?
47
                if (this.ngMetaEdits.imports.some(x => x.standalone)) {
11✔
NEW
48
                        transforms.push(this.componentMetaTransformer);
×
49
                } else if (Object.keys(this.ngMetaEdits).filter(x => this.ngMetaEdits[x].length).length) {
80✔
50
                        transforms.push(this.ngModuleTransformer);
9✔
51
                }
52

53
                if (transforms.length) {
54
                        this.targetSource = ts.transform(this.targetSource, transforms).transformed[0];
10✔
55
                }
56

57
                // add new import statements after visitor walks:
58
                this.addNewFileImports();
20✔
59

60
                TsUtils.saveFile(this.targetPath, this.targetSource);
20✔
61
                this.formatFile(this.targetPath);
20✔
62
                // reset state in case of further updates
63
                this.initState();
20✔
64
        }
65

66
        /**
67
         * Create configuration object for a component and add it to the `Routes` array variable.
68
         * Imports the first exported class and finalizes the file update (see `.finalize()`).
69
         * @param filePath Path to the component file to import
70
         * @param linkPath Routing `path` to add
71
         * @param linkText Text of the route to add as `data.text`
72
         * @param parentRoutePath Will include the new route as a **child** of the specified route path
73
         * @param routesVariable Name of the array variable holding routes
74
         */
75
        public addChildRoute(
76
                filePath: string, linkPath: string, linkText: string, parentRoutePath: string,
77
                routesVariable = DEFAULT_ROUTES_VARIABLE) {
2✔
78
                this.addRouteModuleEntry(filePath, linkPath, linkText, routesVariable, parentRoutePath);
2✔
79
        }
80

81
        /**
82
         * Create configuration object for a component and add it to the `Routes` array variable.
83
         * Imports the first exported class and finalizes the file update (see `.finalize()`).
84
         * @param filePath Path to the component file to import
85
         * @param linkPath Routing `path` to add
86
         * @param linkText Text of the route to add as `data.text`
87
         * @param routesVariable Name of the array variable holding routes
88
         */
89
        public addRoute(filePath: string, linkPath: string, linkText: string, routesVariable = DEFAULT_ROUTES_VARIABLE) {
5✔
90
                this.addRouteModuleEntry(filePath, linkPath, linkText, routesVariable);
6✔
91
        }
92

93
        /**
94
         * Import class and add it to `NgModule` declarations.
95
         * Creates `declarations` array if one is not present already.
96
         * @param filePath Path to the file to import
97
         */
98
        public addDeclaration(filePath: string, addToExport?: boolean) {
99
                let className: string;
100
                const fileSource = TsUtils.getFileSource(filePath);
7✔
101
                const relativePath: string = Util.relativePath(this.targetPath, filePath, true, true);
7✔
102
                className = TsUtils.getClassName(fileSource.getChildren());
7✔
103
                if (addToExport) {
104
                        this.addNgModuleMeta({ declare: className, from: relativePath, export: className });
2✔
105
                } else {
106
                        this.addNgModuleMeta({ declare: className, from: relativePath });
5✔
107
                }
108
        }
109

110
        /**
111
         * Add a metadata update to the file's `NgModule`. Will also import identifiers.
112
         */
113
        public addNgModuleMeta(dep: TemplateDependency, variables?: { [key: string]: string }) {
114
                const copy = {
115
                        declare: this.asArray(dep.declare, variables),
116
                        import: this.asArray(dep.import, variables),
117
                        provide: this.asArray(dep.provide, variables),
118
                        // tslint:disable-next-line:object-literal-sort-keys
119
                        export: this.asArray(dep.export, variables)
120
                };
121

122
                if (dep.from) {
123
                        // request import
124
                        const identifiers = [...copy.import, ...copy.declare, ...copy.provide];
21✔
125
                        this.requestImport(identifiers, Util.applyConfigTransformation(dep.from, variables));
21✔
126
                }
127
                const imports = copy.import
26✔
128
                        .map(x => ({ name: x, root: dep.root }))
19✔
129
                        .filter(x => !this.ngMetaEdits.imports.find(i => i.name === x.name));
19✔
130
                this.ngMetaEdits.imports.push(...imports);
26✔
131

132
                const declarations = copy.declare
26✔
133
                        .filter(x => !this.ngMetaEdits.declarations.find(d => d === x));
14✔
134
                this.ngMetaEdits.declarations.push(...declarations);
26✔
135

136
                const providers = copy.provide
26✔
137
                        .filter(x => !this.ngMetaEdits.providers.find(p => p === x));
5✔
138
                this.ngMetaEdits.providers.push(...providers);
26✔
139

140
                const exportsArr = copy.export
26✔
141
                        .filter(x => !this.ngMetaEdits.exports.find(p => p === x));
×
142
                this.ngMetaEdits.exports.push(...exportsArr);
26✔
143
        }
144

145
        /**
146
         * Updates a standalone component's imports metadata.
147
         */
148
        public addStandaloneImport(dep: TemplateDependency, variables?: { [key: string]: string }) {
149
                const copy = {
150
                        import: this.asArray(dep.import, variables),
151
                        provide: this.asArray(dep.provide, variables)
152
                };
153
                if (dep.from) {
154
                        // request import
NEW
155
                        const identifiers = [...copy.import, ...copy.provide];
×
NEW
156
                        this.requestImport(identifiers, Util.applyConfigTransformation(dep.from, variables));
×
157
                }
158

NEW
159
                const imports = copy.import
×
NEW
160
                        .map(x => ({ name: x, root: dep.root, standalone: true }))
×
NEW
161
                        .filter(x => !this.ngMetaEdits.imports.find(i => i.name === x.name));
×
NEW
162
                this.ngMetaEdits.imports.push(...imports);
×
163
        }
164

165
        //#region File state
166

167
        /** Initializes existing imports info, [re]sets import and `NgModule` edits */
168
        protected initState() {
169
                this.targetSource = TsUtils.getFileSource(this.targetPath);
48✔
170
                this.importsMeta = this.loadImportsMeta();
48✔
171
                this.requestedImports = [];
48✔
172
                this.ngMetaEdits = {
48✔
173
                        declarations: [],
174
                        imports: [],
175
                        providers: [],
176
                        exports: []
177
                };
178
                this.createdStringLiterals = [];
48✔
179
        }
180

181
        /* load some metadata about imports */
182
        protected loadImportsMeta() {
183
                const meta = { lastIndex: 0, modulePaths: [] };
47✔
184

185
                for (let i = 0; i < this.targetSource.statements.length; i++) {
47✔
186
                        const statement = this.targetSource.statements[i];
81✔
187
                        switch (statement.kind) {
188
                                case ts.SyntaxKind.ImportDeclaration:
189
                                        const importStmt = (statement as ts.ImportDeclaration);
42✔
190

191
                                        if (importStmt.importClause && importStmt.importClause.namedBindings &&
123✔
192
                                                importStmt.importClause.namedBindings.kind !== ts.SyntaxKind.NamespaceImport) {
193
                                                // don't add imports without named (e.g. `import $ from "JQuery"` or `import "./my-module.js";`)
194
                                                // don't add namespace imports (`import * as fs`) as available for editing, maybe in the future
195
                                                meta.modulePaths.push((importStmt.moduleSpecifier as ts.StringLiteral).text);
39✔
196
                                        }
197

198
                                // don't add equals imports (`import url = require("url")`) as available for editing, maybe in the future
199
                                case ts.SyntaxKind.ImportEqualsDeclaration:
81✔
200
                                        meta.lastIndex = i + 1;
43✔
201
                                        break;
43✔
202
                                default:
203
                                        break;
38✔
204
                        }
205
                }
206

207
                return meta;
47✔
208
        }
209

210
        //#endregion File state
211

212
        protected addRouteModuleEntry(
213
                filePath: string,
214
                linkPath: string,
215
                linkText: string,
216
                routesVariable = DEFAULT_ROUTES_VARIABLE,
×
217
                parentRoutePath?: string
218
        ) {
219
                let className: string;
220
                const fileSource = TsUtils.getFileSource(filePath);
8✔
221
                const relativePath: string = Util.relativePath(this.targetPath, filePath, true, true);
8✔
222
                className = TsUtils.getClassName(fileSource.getChildren());
8✔
223
                this.requestImport([className], relativePath);
8✔
224

225
                // https://github.com/Microsoft/TypeScript/issues/14419#issuecomment-307256171
226
                const transformer: ts.TransformerFactory<ts.Node> = <T extends ts.Node>(context: ts.TransformationContext) =>
8✔
227
                        (rootNode: T) => {
8✔
228
                                let conditionalVisitor: ts.Visitor;
229
                                // the visitor that should be used when adding routes to the main route array
230
                                const routeArrayVisitor = (node: ts.Node): ts.Node => {
8✔
231
                                        if (node.kind === ts.SyntaxKind.ArrayLiteralExpression) {
232
                                                const newObject = this.createRouteEntry(linkPath, className, linkText);
6✔
233
                                                const array = (node as ts.ArrayLiteralExpression);
6✔
234
                                                this.createdStringLiterals.push(linkPath, linkText);
6✔
235
                                                const notFoundWildCard = "**";
6✔
236
                                                const nodes = ts.visitNodes(array.elements, visitor);
6✔
237
                                                const errorRouteNode = nodes.filter(element => element.getText().includes(notFoundWildCard))[0];
6✔
238
                                                let resultNodes = null;
6✔
239
                                                if (errorRouteNode) {
240
                                                        resultNodes = nodes
×
241
                                                                .slice(0, nodes.indexOf(errorRouteNode))
242
                                                                .concat(newObject)
243
                                                                .concat(errorRouteNode);
244
                                                } else {
245
                                                        resultNodes = nodes
6✔
246
                                                                .concat(newObject);
247
                                                }
248

249
                                                const elements = ts.factory.createNodeArray([
6✔
250
                                                        ...resultNodes
251
                                                ]);
252

253
                                                return ts.factory.updateArrayLiteralExpression(array, elements);
6✔
254
                                        } else {
255
                                                return ts.visitEachChild(node, conditionalVisitor, context);
18✔
256
                                        }
257
                                };
258
                                // the visitor that should be used when adding child routes to a specified parent
259
                                const parentRouteVisitor = (node: ts.Node): ts.Node => {
8✔
260
                                        if (node.kind === ts.SyntaxKind.ObjectLiteralExpression) {
261
                                                if (!node.getText().includes(parentRoutePath)) {
262
                                                        return node;
×
263
                                                }
264
                                                const nodeProperties = (node as ts.ObjectLiteralExpression).properties;
2✔
265
                                                const parentPropertyCheck = (element: ts.PropertyAssignment) => {
2✔
266
                                                        return element.name.kind === ts.SyntaxKind.Identifier && element.name.text === "path"
5✔
267
                                                                && element.initializer.kind === ts.SyntaxKind.StringLiteral
268
                                                                && (element.initializer as ts.StringLiteral).text === parentRoutePath;
269
                                                };
270
                                                const parentProperty = nodeProperties.filter(parentPropertyCheck)[0];
2✔
271
                                                if (!parentProperty) {
272
                                                        return node;
×
273
                                                }
274
                                                function filterForChildren(element: ts.Node): boolean {
275
                                                        if (element.kind === ts.SyntaxKind.PropertyAssignment) {
276
                                                                const identifier = element.getChildren()[0];
3✔
277
                                                                return identifier.kind === ts.SyntaxKind.Identifier && identifier.getText().trim() === "children";
3✔
278
                                                        }
279
                                                        return false;
5✔
280
                                                }
281
                                                const newObject = this.createRouteEntry(linkPath, className, linkText);
2✔
282
                                                const currentNode = node as ts.ObjectLiteralExpression;
2✔
283
                                                this.createdStringLiterals.push(linkPath, linkText);
2✔
284
                                                const syntaxList: ts.SyntaxList = node.getChildren()
2✔
285
                                                        .filter(element => element.kind === ts.SyntaxKind.SyntaxList)[0] as ts.SyntaxList;
6✔
286
                                                let childrenProperty: ts.PropertyAssignment = syntaxList
2✔
287
                                                        .getChildren().filter(filterForChildren)[0] as ts.PropertyAssignment;
288
                                                let childrenArray: ts.ArrayLiteralExpression = null;
2✔
289

290
                                                // if the target parent route already has child routes - get them
291
                                                // if not - create an empty 'chuldren' array
292
                                                if (childrenProperty) {
293
                                                        childrenArray = childrenProperty.getChildren()
1!
294
                                                                .filter(element => element.kind === ts.SyntaxKind.ArrayLiteralExpression)[0] as ts.ArrayLiteralExpression
3✔
295
                                                                || ts.factory.createArrayLiteralExpression();
296
                                                } else {
297
                                                        childrenArray = ts.factory.createArrayLiteralExpression();
1✔
298
                                                }
299

300
                                                let existingProperties = syntaxList.getChildren()
2✔
301
                                                        .filter(element => element.kind !== ts.SyntaxKind["CommaToken"]) as ts.ObjectLiteralElementLike[];
8✔
302
                                                const newArrayValues = childrenArray.elements.concat(newObject);
2✔
303
                                                if (!childrenProperty) {
304
                                                        const propertyName = "children";
1✔
305
                                                        const propertyValue = ts.factory.createArrayLiteralExpression([...newArrayValues]);
1✔
306
                                                        childrenProperty = ts.factory.createPropertyAssignment(propertyName, propertyValue);
1✔
307
                                                        existingProperties = existingProperties
1✔
308
                                                                .concat(childrenProperty);
309
                                                } else {
310
                                                        const index = existingProperties.indexOf(childrenProperty);
1✔
311
                                                        const childrenPropertyName = childrenProperty.name;
1✔
312
                                                        childrenProperty =
1✔
313
                                                                ts.updatePropertyAssignment(
314
                                                                        childrenProperty,
315
                                                                        childrenPropertyName,
316
                                                                        ts.factory.createArrayLiteralExpression([...newArrayValues])
317
                                                                );
318
                                                        existingProperties
1✔
319
                                                                .splice(index, 1, childrenProperty);
320
                                                }
321
                                                return ts.updateObjectLiteral(currentNode, existingProperties) as ts.Node;
2✔
322
                                        } else {
323
                                                return ts.visitEachChild(node, conditionalVisitor, context);
8✔
324
                                        }
325
                                };
326

327
                                if (parentRoutePath === undefined) {
328
                                        conditionalVisitor = routeArrayVisitor;
6✔
329
                                } else {
330
                                        conditionalVisitor = parentRouteVisitor;
2✔
331
                                }
332
                                const visitCondition = (node: ts.Node): boolean => {
8✔
333
                                        return node.kind === ts.SyntaxKind.VariableDeclaration &&
77✔
334
                                                (node as ts.VariableDeclaration).name.getText() === routesVariable &&
335
                                                (node as ts.VariableDeclaration).type.getText() === "Routes";
336
                                };
337
                                const visitor: ts.Visitor = this.createVisitor(conditionalVisitor, visitCondition, context);
8✔
338
                                context.enableSubstitution(ts.SyntaxKind.ClassDeclaration);
8✔
339
                                return ts.visitNode(rootNode, visitor);
8✔
340
                        };
341

342
                this.targetSource = ts.transform(this.targetSource, [transformer], {
8✔
343
                        pretty: true // oh well..
344
                }).transformed[0] as ts.SourceFile;
345

346
                this.finalize();
8✔
347
        }
348

349
        /**
350
         * Add named imports from a path/package.
351
         * @param identifiers Strings to create named import from ("Module" => `import { Module }`)
352
         * @param modulePath Module specifier - can be path to file or npm package, etc
353
         */
354
        protected requestImport(identifiers: string[], modulePath: string) {
355
                const existing = this.requestedImports.find(x => x.from === modulePath);
33✔
356
                if (!existing) {
357
                        // new imports, check if already exists in file
358
                        this.requestedImports.push({
26✔
359
                                from: modulePath, imports: identifiers,
360
                                edit: this.importsMeta.modulePaths.indexOf(modulePath) !== -1
361
                        });
362
                } else {
363
                        const newNamedImports = identifiers.filter(x => existing.imports.indexOf(x) === -1);
10✔
364
                        existing.imports.push(...newNamedImports);
7✔
365
                }
366
        }
367

368
        /** Add `import` statements not previously found in the file  */
369
        protected addNewFileImports() {
370
                const newImports = this.requestedImports.filter(x => !x.edit);
25✔
371
                if (!newImports.length) {
372
                        return;
5✔
373
                }
374

375
                const newStatements = ts.factory.createNodeArray([
15✔
376
                        ...this.targetSource.statements.slice(0, this.importsMeta.lastIndex),
377
                        ...newImports.map(x => TsUtils.createIdentifierImport(x.imports, x.from)),
19✔
378
                        ...this.targetSource.statements.slice(this.importsMeta.lastIndex)
379
                ]);
380
                newImports.forEach(x => this.createdStringLiterals.push(x.from));
19✔
381

382
                this.targetSource = ts.factory.updateSourceFile(this.targetSource, newStatements);
15✔
383
        }
384

385
        //#region ts.TransformerFactory
386

387
        /** Transformation to apply edits to existing named import declarations */
388
        protected importsTransformer: ts.TransformerFactory<ts.Node> =
28✔
389
                <T extends ts.Node>(context: ts.TransformationContext) => (rootNode: T) => {
4✔
390
                        const editImports = this.requestedImports.filter(x => x.edit);
7✔
391

392
                        // https://github.com/Microsoft/TypeScript/issues/14419#issuecomment-307256171
393
                        const visitor = (node: ts.Node): ts.Node => {
4✔
394
                                if (node.kind === ts.SyntaxKind.ImportDeclaration &&
77✔
395
                                        editImports.find(x => x.from === ((node as ts.ImportDeclaration).moduleSpecifier as ts.StringLiteral).text)
9✔
396
                                ) {
397
                                        // visit just the source file main array (second visit)
398
                                        return visitImport(node as ts.ImportDeclaration);
6✔
399
                                } else {
400
                                        node = ts.visitEachChild(node, visitor, context);
64✔
401
                                }
402
                                return node;
64✔
403
                        };
404
                        function visitImport(node: ts.Node) {
405
                                if (node.kind === ts.SyntaxKind.NamedImports) {
406
                                        const namedImports = node as ts.NamedImports;
6✔
407
                                        const moduleSpecifier = (namedImports.parent.parent.moduleSpecifier as ts.StringLiteral).text;
6✔
408

409
                                        const existing = ts.visitNodes(namedImports.elements, visitor);
6✔
410
                                        const alreadyImported = existing.map(x => x.name.text);
6✔
411

412
                                        const editImport = editImports.find(x => x.from === moduleSpecifier);
8✔
413
                                        const newImports = editImport.imports.filter(x => alreadyImported.indexOf(x) === -1);
7✔
414

415
                                        node = ts.factory.updateNamedImports(namedImports, [
6✔
416
                                                ...existing,
417
                                                ...newImports.map(x => ts.factory.createImportSpecifier(false, undefined, ts.factory.createIdentifier(x)))
6✔
418
                                        ]);
419
                                } else {
420
                                        node = ts.visitEachChild(node, visitImport, context);
18✔
421
                                }
422
                                return node;
24✔
423
                        }
424
                        return ts.visitNode(rootNode, visitor);
4✔
425
                }
426

427
        /** Transformation to apply `this.ngMetaEdits` to `NgModule` metadata properties */
428
        protected ngModuleTransformer: ts.TransformerFactory<ts.Node> =
28✔
429
                <T extends ts.Node>(context: ts.TransformationContext) => (rootNode: T) => {
9✔
430
                        const visitNgModule: ts.Visitor = (node: ts.Node): ts.Node => {
9✔
431
                                const properties: string[] = []; // "declarations", "imports", "providers"
80✔
432
                                for (const key in this.ngMetaEdits) {
433
                                        if (this.ngMetaEdits[key].length) {
434
                                                properties.push(key);
148✔
435
                                        }
436
                                }
437
                                if (node.kind === ts.SyntaxKind.ObjectLiteralExpression &&
98✔
438
                                        node.parent &&
439
                                        node.parent.kind === ts.SyntaxKind.CallExpression) {
440

441
                                        let obj = (node as ts.ObjectLiteralExpression);
9✔
442

443
                                        //TODO: test node.parent for ts.CallExpression NgModule
444
                                        const missingProperties = properties.filter(x => !obj.properties.find(o => o.name.getText() === x));
22✔
445

446
                                        // skip visiting if no declaration/imports/providers arrays exist:
447
                                        if (missingProperties.length !== properties.length) {
448
                                                obj = ts.visitEachChild(node, visitNgModule, context) as ts.ObjectLiteralExpression;
9✔
449
                                        }
450

451
                                        if (!missingProperties.length) {
452
                                                return obj;
8✔
453
                                        }
454

455
                                        const objProperties = ts.visitNodes(obj.properties, visitor);
1✔
456
                                        const newProps = [];
1✔
457
                                        for (const prop of missingProperties) {
458
                                                let arrayExpr;
459
                                                switch (prop) {
460
                                                        case "imports":
3!
461
                                                                const importDeps = this.ngMetaEdits.imports;
1✔
462
                                                                arrayExpr = ts.factory.createArrayLiteralExpression(
1✔
463
                                                                        importDeps.map(x => TsUtils.createIdentifier(x.name, x.root ? "forRoot" : ""))
2!
464
                                                                );
465
                                                                break;
1✔
466
                                                        case "declarations":
467
                                                        case "providers":
468
                                                        case "exports":
469
                                                                arrayExpr = ts.factory.createArrayLiteralExpression(
1✔
470
                                                                        this.ngMetaEdits[prop].map(x => ts.factory.createIdentifier(x))
1✔
471
                                                                );
472
                                                                break;
1✔
473
                                                }
474
                                                newProps.push(ts.factory.createPropertyAssignment(prop, arrayExpr));
2✔
475
                                        }
476

477
                                        return ts.updateObjectLiteral(obj, [
1✔
478
                                                ...objProperties,
479
                                                ...newProps
480
                                        ]);
481
                                } else if (node.kind === ts.SyntaxKind.ArrayLiteralExpression &&
109✔
482
                                        node.parent.kind === ts.SyntaxKind.PropertyAssignment &&
483
                                        properties.indexOf((node.parent as ts.PropertyAssignment).name.getText()) !== -1) {
484
                                        const initializer = (node as ts.ArrayLiteralExpression);
14✔
485
                                        const props = ts.visitNodes(initializer.elements, visitor);
14✔
486
                                        const alreadyImported = props.map(x => TsUtils.getIdentifierName(x));
17✔
487
                                        const prop = properties.find(x => x === (node.parent as ts.PropertyAssignment).name.getText());
20✔
488

489
                                        let identifiers = [];
14✔
490
                                        switch (prop) {
491
                                                case "imports":
27✔
492
                                                        identifiers = this.ngMetaEdits.imports
7✔
493
                                                                .filter(x => alreadyImported.indexOf(x.name) === -1)
9✔
494
                                                                .map(x => TsUtils.createIdentifier(x.name, x.root ? "forRoot" : ""));
9✔
495
                                                        break;
7✔
496
                                                case "declarations":
497
                                                case "providers":
498
                                                case "exports":
499
                                                        identifiers = this.ngMetaEdits[prop]
7✔
500
                                                                .filter(x => alreadyImported.indexOf(x) === -1)
9✔
501
                                                                .map(x => ts.factory.createIdentifier(x));
9✔
502
                                                        break;
7✔
503
                                        }
504
                                        const elements = ts.factory.createNodeArray([
14✔
505
                                                ...props,
506
                                                ...identifiers
507
                                        ]);
508

509
                                        return ts.factory.updateArrayLiteralExpression(initializer, elements);
14✔
510
                                } else {
511
                                        node = ts.visitEachChild(node, visitNgModule, context);
57✔
512
                                }
513
                                return node;
57✔
514
                        };
515
                        const visitCondition: (node: ts.Node) => boolean = (node: ts.Node) => {
9✔
516
                                return node.kind === ts.SyntaxKind.CallExpression &&
120✔
517
                                        node.parent && node.parent.kind === ts.SyntaxKind.Decorator &&
518
                                        (node as ts.CallExpression).expression.getText() === "NgModule";
519
                        };
520
                        const visitor = this.createVisitor(visitNgModule, visitCondition, context);
9✔
521
                        return ts.visitNode(rootNode, visitor);
9✔
522
                }
523

524
        // TODO: extend to allow the modification of multiple metadata properties
525
        /** Transformation to apply `this.ngMetaEdits` to a standalone `Component` metadata imports */
526
        protected componentMetaTransformer: ts.TransformerFactory<ts.Node> =
28✔
NEW
527
                <T extends ts.Node>(context: ts.TransformationContext) => (rootNode: T) => {
×
NEW
528
                        const visitComponent: ts.Visitor = (node: ts.Node): ts.Node => {
×
NEW
529
                                let importsExpr = null;
×
NEW
530
                                const prop = "imports";
×
531
                                if (node.kind === ts.SyntaxKind.ObjectLiteralExpression &&
×
532
                                        node.parent &&
533
                                        node.parent.kind === ts.SyntaxKind.CallExpression) {
NEW
534
                                                const obj = (node as ts.ObjectLiteralExpression);
×
NEW
535
                                                const objProperties = ts.visitNodes(obj.properties, visitor);
×
NEW
536
                                                const newProps = [];
×
NEW
537
                                                const importDeps = this.ngMetaEdits.imports;
×
NEW
538
                                                importsExpr = ts.factory.createArrayLiteralExpression(
×
NEW
539
                                                        importDeps.map(x => TsUtils.createIdentifier(x.name))
×
540
                                                );
NEW
541
                                                newProps.push(ts.factory.createPropertyAssignment(prop, importsExpr));
×
NEW
542
                                                return context.factory.updateObjectLiteralExpression(obj, [
×
543
                                                        ...objProperties,
544
                                                        ...newProps
545
                                                ]);
546
                                } else {
NEW
547
                                        node = ts.visitEachChild(node, visitComponent, context);
×
548
                                }
549

NEW
550
                                return node;
×
551
                        };
NEW
552
                        const visitCondition: (node: ts.Node) => boolean = (node: ts.Node) => {
×
NEW
553
                                return node.kind === ts.SyntaxKind.CallExpression &&
×
554
                                        node.parent && node.parent.kind === ts.SyntaxKind.Decorator &&
555
                                        (node as ts.CallExpression).expression.getText() === "Component";
556
                        };
NEW
557
                        const visitor = this.createVisitor(visitComponent, visitCondition, context);
×
NEW
558
                        return ts.visitNode(rootNode, visitor);
×
559
                }
560

561
        //#endregion ts.TransformerFactory
562

563
        //#region Formatting
564

565
        /** Format a TS source file, very TBD */
566
        protected formatFile(filePath: string) {
567
                // formatting via LanguageService https://github.com/Microsoft/TypeScript/wiki/Using-the-Compiler-API
568
                // https://github.com/Microsoft/TypeScript/issues/1651
569

570
                let text = this.fileSystem.readFile(filePath);
11✔
571
                // create the language service files
572
                const services = ts.createLanguageService(this.getLanguageHost(filePath), ts.createDocumentRegistry());
11✔
573

574
                this.readFormatConfigs();
11✔
575
                const textChanges = services.getFormattingEditsForDocument(filePath, this.getFormattingOptions());
11✔
576
                text = this.applyChanges(text, textChanges);
11✔
577

578
                if (this.formatOptions.singleQuotes) {
579
                        for (const str of this.createdStringLiterals) {
580
                                // there shouldn't be duplicate strings of these
581
                                text = text.replace(`"${str}"`, `'${str}'`);
15✔
582
                        }
583
                }
584

585
                this.fileSystem.writeFile(filePath, text);
11✔
586
        }
587

588
        /**  Try and parse formatting from project `.editorconfig` / `tslint.json` */
589
        protected readFormatConfigs() {
590
                if (this.fileSystem.fileExists(".editorconfig")) {
591
                        // very basic parsing support
592
                        const text = this.fileSystem.readFile(".editorconfig", "utf-8");
7✔
593
                        const options = text
7✔
594
                                .replace(/\s*[#;].*([\r\n])/g, "$1") //remove comments
595
                                .replace(/\[(?!\*\]|\*.ts).+\][^\[]+/g, "") // leave [*]/[*.ts] sections
596
                                .split(/\r\n|\r|\n/)
597
                                .reduce((obj, x) => {
598
                                        if (x.indexOf("=") !== -1) {
599
                                                const pair = x.split("=");
33✔
600
                                                obj[pair[0].trim()] = pair[1].trim();
33✔
601
                                        }
602
                                        return obj;
65✔
603
                                }, {});
604

605
                        this.formatOptions.spaces = options["indent_style"] === "space";
7✔
606
                        if (options["indent_size"]) {
607
                                this.formatOptions.indentSize = parseInt(options["indent_size"], 10) || this.formatOptions.indentSize;
6!
608
                        }
609
                        if (options["quote_type"]) {
610
                                this.formatOptions.singleQuotes = options["quote_type"] === "single";
5✔
611
                        }
612
                }
613
                if (this.fileSystem.fileExists("tslint.json")) {
614
                        // tslint prio - overrides other settings
615
                        const options = JSON.parse(this.fileSystem.readFile("tslint.json", "utf-8"));
10✔
616
                        if (options.rules && options.rules.indent && options.rules.indent[0]) {
29✔
617
                                this.formatOptions.spaces = options.rules.indent[1] === "spaces";
9✔
618
                                if (options.rules.indent[2]) {
619
                                        this.formatOptions.indentSize = parseInt(options.rules.indent[2], 10);
8✔
620
                                }
621
                        }
622
                        if (options.rules && options.rules.quotemark && options.rules.quotemark[0]) {
29✔
623
                                this.formatOptions.singleQuotes = options.rules.quotemark.indexOf("single") !== -1;
9✔
624
                        }
625
                }
626
        }
627

628
        /**
629
         * Apply formatting changes (position based) in reverse
630
         * from https://github.com/Microsoft/TypeScript/issues/1651#issuecomment-69877863
631
         */
632
        private applyChanges(orig: string, changes: ts.TextChange[]): string {
633
                let result = orig;
11✔
634
                for (let i = changes.length - 1; i >= 0; i--) {
11✔
635
                        const change = changes[i];
49✔
636
                        const head = result.slice(0, change.span.start);
49✔
637
                        const tail = result.slice(change.span.start + change.span.length);
49✔
638
                        result = head + change.newText + tail;
49✔
639
                }
640
                return result;
11✔
641
        }
642

643
        /** Return source file formatting options */
644
        private getFormattingOptions(): ts.FormatCodeSettings {
645
                const formatOptions: ts.FormatCodeSettings = {
646
                        // tslint:disable:object-literal-sort-keys
647
                        indentSize: this.formatOptions.indentSize,
648
                        tabSize: 4,
649
                        newLineCharacter: ts.sys.newLine,
650
                        convertTabsToSpaces: this.formatOptions.spaces,
651
                        indentStyle: ts.IndentStyle.Smart,
652
                        insertSpaceAfterCommaDelimiter: true,
653
                        insertSpaceAfterSemicolonInForStatements: true,
654
                        insertSpaceBeforeAndAfterBinaryOperators: true,
655
                        insertSpaceAfterKeywordsInControlFlowStatements: true,
656
                        insertSpaceAfterTypeAssertion: true
657
                        // tslint:enable:object-literal-sort-keys
658
                };
659

660
                return formatOptions;
11✔
661
        }
662

663
        /** Get language service host, sloppily */
664
        private getLanguageHost(filePath: string): ts.LanguageServiceHost {
665
                const files = {};
11✔
666
                files[filePath] = { version: 0 };
11✔
667
                // create the language service host to allow the LS to communicate with the host
668
                const servicesHost: ts.LanguageServiceHost = {
669
                        getCompilationSettings: () => ({}),
×
670
                        getScriptFileNames: () => Object.keys(files),
×
671
                        getScriptVersion: fileName => files[fileName] && files[fileName].version.toString(),
11✔
672
                        getScriptSnapshot: fileName => {
673
                                if (!this.fileSystem.fileExists(fileName)) {
674
                                        return undefined;
×
675
                                }
676
                                return ts.ScriptSnapshot.fromString(this.fileSystem.readFile(fileName));
11✔
677
                        },
678
                        getCurrentDirectory: () => process.cwd(),
11✔
679
                        getDefaultLibFileName: options => ts.getDefaultLibFilePath(options),
×
680
                        readDirectory: ts.sys.readDirectory,
681
                        readFile: ts.sys.readFile,
682
                        fileExists: ts.sys.fileExists
683
                };
684
                return servicesHost;
11✔
685
        }
686

687
        //#endregion Formatting
688

689
        /** Convert a string or string array union to array. Splits strings as comma delimited */
690
        private asArray(value: string | string[], variables: { [key: string]: string }): string[] {
691
                let result: string[] = [];
104✔
692
                if (value) {
693
                        result = typeof value === "string" ? value.split(/\s*,\s*/) : value;
35✔
694
                        result = result.map(x => Util.applyConfigTransformation(x, variables));
38✔
695
                }
696
                return result;
104✔
697
        }
698

699
        private createVisitor(
700
                conditionalVisitor: ts.Visitor,
701
                visitCondition: (node: ts.Node) => boolean,
702
                nodeContext: ts.TransformationContext
703
        ): ts.Visitor {
704
                return function visitor(node: ts.Node): ts.Node {
17✔
705
                        if (visitCondition(node)) {
706
                                node = ts.visitEachChild(node, conditionalVisitor, nodeContext);
17✔
707
                        } else {
708
                                node = ts.visitEachChild(node, visitor, nodeContext);
180✔
709
                        }
710
                        return node;
197✔
711
                };
712
        }
713

714
        private createRouteEntry(linkPath: string, className: string, linkText: string): ts.ObjectLiteralExpression {
715
                const routePath = ts.factory.createPropertyAssignment("path", ts.factory.createStringLiteral(linkPath));
8✔
716
                const routeComponent = ts.factory.createPropertyAssignment("component", ts.factory.createIdentifier(className));
8✔
717
                const routeDataInner = ts.factory.createPropertyAssignment("text", ts.factory.createStringLiteral(linkText));
8✔
718
                const routeData = ts.factory.createPropertyAssignment(
8✔
719
                        "data", ts.factory.createObjectLiteralExpression([routeDataInner]));
720
                return ts.factory.createObjectLiteralExpression([routePath, routeComponent, routeData]);
8✔
721
        }
722

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