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

IgniteUI / igniteui-cli / 8358408476

20 Mar 2024 11:52AM UTC coverage: 66.781% (-0.09%) from 66.871%
8358408476

push

github

web-flow
Generate default route redirect in React (#1227)

* fix(react): add index for default path
* fix(react): generate default route redirect
* fix(react): add import for redirect

* release: v13.1.12-beta.2

---------

Co-authored-by: Milko Venkov <MVenkov@infragistics.com>

395 of 629 branches covered (62.8%)

Branch coverage included in aggregate %.

0 of 15 new or added lines in 2 files covered. (0.0%)

2 existing lines in 1 file now uncovered.

4076 of 6066 relevant lines covered (67.19%)

76.22 hits per line

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

0.0
/packages/cli/templates/react/ReactTypeScriptFileUpdate.ts
1
import { App, FS_TOKEN, IFileSystem, TypeScriptUtils, Util } from "@igniteui/cli-core";
×
2
import * as ts from "typescript";
×
3

4
const DEFAULT_ROUTES_VARIABLE = "routes";
×
5
/**
6
 * Apply various updates to typescript files using AST
7
 */
8
export class ReactTypeScriptFileUpdate {
×
9

10
        protected formatOptions = { spaces: false, indentSize: 4, singleQuotes: false };
×
11
        private fileSystem: IFileSystem;
12
        private targetSource: ts.SourceFile;
13
        private importsMeta: { lastIndex: number, modulePaths: string[] };
14

15
        private requestedImports: Array<{
16
                as: string | undefined,
17
                from: string,
18
                component: string,
19
                edit: boolean,
20
                namedImport: boolean
21
        }>;
22

23
        private createdStringLiterals: string[];
24

25
        /** Create updates for a file. Use `add<X>` methods to add transformations and `finalize` to apply and save them. */
26
        constructor(private targetPath: string) {
×
27
                this.fileSystem = App.container.get<IFileSystem>(FS_TOKEN);
×
28
                this.initState();
×
29
        }
30

31
        /** Applies accumulated transforms, saves and formats the file */
32
        public finalize() {
33
                // add new import statements after visitor walks:
34
                this.addNewFileImports();
×
35

36
                TypeScriptUtils.saveFile(this.targetPath, this.targetSource);
×
37
                this.formatFile(this.targetPath);
×
38
                // reset state in case of further updates
39
                this.initState();
×
40
        }
41

42
        public addRoute(
43
                path: string,
44
                component: string,
45
                name: string,
46
                filePath: string,
47
                routerChildren: string,
48
                importAlias: string,
49
                defaultRoute = false
×
50
        ) {
NEW
51
                this.addRouteModuleEntry(path, component, name, filePath, routerChildren, importAlias, defaultRoute);
×
52
        }
53

54
        //#region File state
55

56
        /** Initializes existing imports info, [re]sets import and `NgModule` edits */
57
        protected initState() {
58
                this.targetSource = TypeScriptUtils.getFileSource(this.targetPath);
×
59
                this.importsMeta = this.loadImportsMeta();
×
60
                this.requestedImports = [];
×
61
                this.createdStringLiterals = [];
×
62
        }
63

64
        /* load some metadata about imports */
65
        protected loadImportsMeta() {
66
                const meta = { lastIndex: 0, modulePaths: [] };
×
67

68
                for (let i = 0; i < this.targetSource.statements.length; i++) {
×
69
                        const statement = this.targetSource.statements[i];
×
70
                        switch (statement.kind) {
71
                                case ts.SyntaxKind.ImportDeclaration:
72
                                        const importStmt = (statement as ts.ImportDeclaration);
×
73

74
                                        if (importStmt.importClause && importStmt.importClause.namedBindings &&
×
75
                                                importStmt.importClause.namedBindings.kind !== ts.SyntaxKind.NamespaceImport) {
76
                                                // don't add imports without named (e.g. `import $ from "JQuery"` or `import "./my-module.js";`)
77
                                                // don't add namespace imports (`import * as fs`) as available for editing, maybe in the future
78
                                                meta.modulePaths.push((importStmt.moduleSpecifier as ts.StringLiteral).text);
×
79
                                        }
80

81
                                // don't add equals imports (`import url = require("url")`) as available for editing, maybe in the future
82
                                case ts.SyntaxKind.ImportEqualsDeclaration:
×
83
                                        meta.lastIndex = i + 1;
×
84
                                        break;
×
85
                                default:
86
                                        break;
×
87
                        }
88
                }
89

90
                return meta;
×
91
        }
92

93
        //#endregion File state
94

95
        protected addRouteModuleEntry(
96
                path: string,
97
                component: string,
98
                name: string,
99
                filePath: string,
100
                routerChildren: string,
101
                importAlias: string,
102
                defaultRoute = false
×
103
        ) {
NEW
104
                const isRouting: boolean = path.indexOf(DEFAULT_ROUTES_VARIABLE) >= 0;
×
105

106
                if (isRouting && this.targetSource.text.indexOf(path.slice(0, -4)) > 0) {
×
107
                        return;
×
108
                }
109

110
                if (defaultRoute) {
NEW
111
                        this.requestImport("react-router-dom", undefined, "redirect", true);
×
112
                } else {
UNCOV
113
                        const relativePath: string = Util.relativePath(this.targetPath, filePath, true, true);
×
UNCOV
114
                        this.requestImport(relativePath, importAlias, component);
×
115
                }
116

117
                // https://github.com/Microsoft/TypeScript/issues/14419#issuecomment-307256171
118
                const transformer: ts.TransformerFactory<ts.Node> = <T extends ts.Node>(context: ts.TransformationContext) =>
×
119
                        (rootNode: T) => {
×
120
                                // the visitor that should be used when adding routes to the main route array
121
                                const conditionalVisitor: ts.Visitor = (node: ts.Node): ts.Node => {
×
122
                                        if (node.kind === ts.SyntaxKind.ArrayLiteralExpression) {
NEW
123
                                                const newObject = this.createRouteEntry(path, component, name, routerChildren, defaultRoute);
×
124
                                                const array = (node as ts.ArrayLiteralExpression);
×
125
                                                this.createdStringLiterals.push(path, name);
×
126
                                                const notFoundWildCard = ".*";
×
127
                                                const nodes = ts.visitNodes(array.elements, visitor);
×
128
                                                const errorRouteNode = nodes.filter(element => element.getText().includes(notFoundWildCard))[0];
×
129
                                                let resultNodes = null;
×
130
                                                if (errorRouteNode) {
131
                                                        resultNodes = nodes
×
132
                                                                .slice(0, nodes.indexOf(errorRouteNode))
133
                                                                .concat(newObject)
134
                                                                .concat(errorRouteNode);
135
                                                } else {
136
                                                        resultNodes = nodes
×
137
                                                                .concat(newObject);
138
                                                }
139

140
                                                const elements = ts.factory.createNodeArray([
×
141
                                                        ...resultNodes
142
                                                ]);
143

144
                                                return ts.factory.updateArrayLiteralExpression(array, elements);
×
145
                                        } else {
146
                                                return ts.visitEachChild(node, conditionalVisitor, context);
×
147
                                        }
148
                                };
149

150
                                let visitCondition;
151

152
                                if (!isRouting) {
153
                                        visitCondition = (node: ts.Node): boolean => {
×
154
                                                return node.kind === ts.SyntaxKind.VariableDeclaration &&
×
155
                                                        (node as ts.VariableDeclaration).name.getText() === DEFAULT_ROUTES_VARIABLE;
156
                                                // no type currently
157
                                                //(node as ts.VariableDeclaration).type.getText() === "Route[]";
158
                                        };
159
                                } else {
160
                                        visitCondition = (node: ts.Node): boolean => {
×
161
                                                return undefined;
×
162
                                        };
163
                                }
164

165
                                const visitor: ts.Visitor = this.createVisitor(conditionalVisitor, visitCondition, context);
×
166
                                context.enableSubstitution(ts.SyntaxKind.ClassDeclaration);
×
167
                                return ts.visitNode(rootNode, visitor);
×
168
                        };
169

170
                this.targetSource = ts.transform(this.targetSource, [transformer], {
×
171
                        pretty: true // oh well..
172
                }).transformed[0] as ts.SourceFile;
173

174
                this.finalize();
×
175
        }
176

177
        protected requestImport(modulePath: string, routerAlias: string, componentName: string, namedImport = false) {
×
178
                const existing = this.requestedImports.find(x => x.from === modulePath);
×
179
                // TODO: better check for named imports. There could be several named imports from same modulePath
180
                if (!existing) {
181
                        // new imports, check if already exists in file
182
                        this.requestedImports.push({
×
183
                                as: routerAlias,
184
                                from: modulePath,
185
                                component: componentName,
186
                                edit: this.importsMeta.modulePaths.indexOf(modulePath) !== -1,
187
                                namedImport
188
                        });
189
                }
190
        }
191

192
        /** Add `import` statements not previously found in the file  */
193
        protected addNewFileImports() {
194
                const newImports = this.requestedImports.filter(x => !x.edit);
×
195
                if (!newImports.length) {
196
                        return;
×
197
                }
198

199
                const newStatements = ts.factory.createNodeArray([
×
200
                        ...this.targetSource.statements.slice(0, this.importsMeta.lastIndex),
NEW
201
                        ...newImports.map(x => this.createIdentifierImport(x.from, x.as, x.component, x.namedImport)),
×
202
                        ...this.targetSource.statements.slice(this.importsMeta.lastIndex)
203
                ]);
204
                newImports.forEach(x => this.createdStringLiterals.push(x.from));
×
205

206
                this.targetSource = ts.factory.updateSourceFile(this.targetSource, newStatements);
×
207
        }
208

209
        protected createIdentifierImport(
210
                importPath: string, as: string, component: string, namedImport: boolean): ts.ImportDeclaration {
211
                let exportedObject: string | undefined;
212
                let exportedObjectName: string | undefined;
213
                let importClause: ts.ImportClause | undefined;
214
                if (as) {
215
                        exportedObject = "routes";
×
216
                        exportedObjectName = as.replace(/\s/g, "");
×
217
                        importClause = ts.factory.createImportClause(
×
218
                                false,
219
                                undefined,
220
                                ts.factory.createNamedImports([
221
                                        ts.factory.createImportSpecifier(false, ts.factory.createIdentifier(exportedObject),
222
                                                ts.factory.createIdentifier(exportedObjectName))
223
                                ])
224
                        );
225
                } else {
226
                        if (namedImport) {
NEW
227
                                const importSpecifier = ts.factory.createImportSpecifier(
×
228
                                        false, undefined, ts.factory.createIdentifier(component));
NEW
229
                                const imports = ts.factory.createNamedImports([importSpecifier]);
×
NEW
230
                                importClause = ts.factory.createImportClause(false, undefined, imports);
×
231
                        } else {
NEW
232
                                importClause = ts.factory.createImportClause(
×
233
                                        false,
234
                                        ts.factory.createIdentifier(component),
235
                                        undefined
236
                                );
237
                        }
238
                }
239
                const importDeclaration = ts.factory.createImportDeclaration(
×
240
                        undefined,
241
                        undefined,
242
                        importClause,
243
                        ts.factory.createStringLiteral(importPath, true));
244
                return importDeclaration;
×
245
        }
246

247
        //#region ts.TransformerFactory
248

249
        /** Transformation to apply edits to existing named import declarations */
250
        protected importsTransformer: ts.TransformerFactory<ts.Node> =
×
251
                <T extends ts.Node>(context: ts.TransformationContext) => (rootNode: T) => {
×
252
                        const editImports = this.requestedImports.filter(x => x.edit);
×
253

254
                        // https://github.com/Microsoft/TypeScript/issues/14419#issuecomment-307256171
255
                        const visitor = (node: ts.Node): ts.Node => {
×
256
                                if (node.kind === ts.SyntaxKind.ImportDeclaration &&
×
257
                                        editImports.find(x => x.from === ((node as ts.ImportDeclaration).moduleSpecifier as ts.StringLiteral).text)
×
258
                                ) {
259
                                        // visit just the source file main array (second visit)
260
                                        return visitImport(node as ts.ImportDeclaration);
×
261
                                } else {
262
                                        node = ts.visitEachChild(node, visitor, context);
×
263
                                }
264
                                return node;
×
265
                        };
266
                        function visitImport(node: ts.Node) {
267
                                node = ts.visitEachChild(node, visitImport, context);
×
268
                                return node;
×
269
                        }
270
                        return ts.visitNode(rootNode, visitor);
×
271
                }
272

273
        //#endregion ts.TransformerFactory
274

275
        //#region Formatting
276

277
        /** Format a TS source file, very TBD */
278
        protected formatFile(filePath: string) {
279
                // formatting via LanguageService https://github.com/Microsoft/TypeScript/wiki/Using-the-Compiler-API
280
                // https://github.com/Microsoft/TypeScript/issues/1651
281

282
                let text = this.fileSystem.readFile(filePath);
×
283
                // create the language service files
284
                const services = ts.createLanguageService(this.getLanguageHost(filePath), ts.createDocumentRegistry());
×
285

286
                this.readFormatConfigs();
×
287
                const textChanges = services.getFormattingEditsForDocument(filePath, this.getFormattingOptions());
×
288
                text = this.applyChanges(text, textChanges);
×
289

290
                if (this.formatOptions.singleQuotes) {
291
                        for (const str of this.createdStringLiterals) {
292
                                // there shouldn't be duplicate strings of these
293
                                text = text.replace(`"${str}"`, `'${str}'`);
×
294
                        }
295
                }
296

297
                this.fileSystem.writeFile(filePath, text);
×
298
        }
299

300
        /**  Try and parse formatting from project `.editorconfig` / `tslint.json` */
301
        protected readFormatConfigs() {
302
                if (this.fileSystem.fileExists(".editorconfig")) {
303
                        // very basic parsing support
304
                        const text = this.fileSystem.readFile(".editorconfig", "utf-8");
×
305
                        const options = text
×
306
                                .replace(/\s*[#;].*([\r\n])/g, "$1") //remove comments
307
                                .replace(/\[(?!\*\]|\*.ts).+\][^\[]+/g, "") // leave [*]/[*.ts] sections
308
                                .split(/\r\n|\r|\n/)
309
                                .reduce((obj, x) => {
310
                                        if (x.indexOf("=") !== -1) {
311
                                                const pair = x.split("=");
×
312
                                                obj[pair[0].trim()] = pair[1].trim();
×
313
                                        }
314
                                        return obj;
×
315
                                }, {});
316

317
                        this.formatOptions.spaces = options["indent_style"] === "space";
×
318
                        if (options["indent_size"]) {
319
                                this.formatOptions.indentSize = parseInt(options["indent_size"], 10) || this.formatOptions.indentSize;
×
320
                        }
321

322
                        if (options["quote_type"]) {
323
                                this.formatOptions.singleQuotes = options["quote_type"] === "single";
×
324
                        }
325
                }
326
                if (this.fileSystem.fileExists("tslint.json")) {
327
                        // tslint prio - overrides other settings
328
                        const options = JSON.parse(this.fileSystem.readFile("tslint.json", "utf-8"));
×
329
                        if (options.rules && options.rules.indent && options.rules.indent[0]) {
×
330
                                this.formatOptions.spaces = options.rules.indent[1] === "spaces";
×
331
                                if (options.rules.indent[2]) {
332
                                        this.formatOptions.indentSize = parseInt(options.rules.indent[2], 10);
×
333
                                }
334
                        }
335
                        if (options.rules && options.rules.quotemark && options.rules.quotemark[0]) {
×
336
                                this.formatOptions.singleQuotes = options.rules.quotemark.indexOf("single") !== -1;
×
337
                        }
338
                }
339
        }
340

341
        /**
342
         * Apply formatting changes (position based) in reverse
343
         * from https://github.com/Microsoft/TypeScript/issues/1651#issuecomment-69877863
344
         */
345
        private applyChanges(orig: string, changes: ts.TextChange[]): string {
346
                let result = orig;
×
347
                for (let i = changes.length - 1; i >= 0; i--) {
×
348
                        const change = changes[i];
×
349
                        const head = result.slice(0, change.span.start);
×
350
                        const tail = result.slice(change.span.start + change.span.length);
×
351
                        result = head + change.newText + tail;
×
352
                }
353
                return result;
×
354
        }
355

356
        /** Return source file formatting options */
357
        private getFormattingOptions(): ts.FormatCodeSettings {
358
                const formatOptions: ts.FormatCodeSettings = {
359
                        // tslint:disable:object-literal-sort-keys
360
                        indentSize: this.formatOptions.indentSize,
361
                        tabSize: 4,
362
                        newLineCharacter: ts.sys.newLine,
363
                        convertTabsToSpaces: this.formatOptions.spaces,
364
                        indentStyle: ts.IndentStyle.Smart,
365
                        insertSpaceAfterCommaDelimiter: true,
366
                        insertSpaceAfterSemicolonInForStatements: true,
367
                        insertSpaceBeforeAndAfterBinaryOperators: true,
368
                        insertSpaceAfterKeywordsInControlFlowStatements: true,
369
                        insertSpaceAfterTypeAssertion: true
370
                        // tslint:enable:object-literal-sort-keys
371
                };
372

373
                return formatOptions;
×
374
        }
375

376
        /** Get language service host, sloppily */
377
        private getLanguageHost(filePath: string): ts.LanguageServiceHost {
378
                const files = {};
×
379
                files[filePath] = { version: 0 };
×
380
                // create the language service host to allow the LS to communicate with the host
381
                const servicesHost: ts.LanguageServiceHost = {
382
                        getCompilationSettings: () => ({}),
×
383
                        getScriptFileNames: () => Object.keys(files),
×
384
                        getScriptVersion: fileName => files[fileName] && files[fileName].version.toString(),
×
385
                        getScriptSnapshot: fileName => {
386
                                if (!this.fileSystem.fileExists(fileName)) {
387
                                        return undefined;
×
388
                                }
389
                                return ts.ScriptSnapshot.fromString(this.fileSystem.readFile(fileName));
×
390
                        },
391
                        getCurrentDirectory: () => process.cwd(),
×
392
                        getDefaultLibFileName: options => ts.getDefaultLibFilePath(options),
×
393
                        readDirectory: ts.sys.readDirectory,
394
                        readFile: ts.sys.readFile,
395
                        fileExists: ts.sys.fileExists
396
                };
397
                return servicesHost;
×
398
        }
399

400
        //#endregion Formatting
401

402
        private createVisitor(
403
                conditionalVisitor: ts.Visitor,
404
                visitCondition: (node: ts.Node) => boolean,
405
                nodeContext: ts.TransformationContext
406
        ): ts.Visitor {
407
                return function visitor(node: ts.Node): ts.Node {
×
408
                        if (visitCondition(node)) {
409
                                node = ts.visitEachChild(node, conditionalVisitor, nodeContext);
×
410
                        } else {
411
                                node = ts.visitEachChild(node, visitor, nodeContext);
×
412
                        }
413
                        return node;
×
414
                };
415
        }
416

417
        private createRouteEntry(
418
                path: string,
419
                component: string,
420
                name: string,
421
                routerAlias: string,
422
                defaultRoute: boolean = false
×
423
        ): ts.ObjectLiteralExpression {
424
                if (defaultRoute) {
425
                        // for default route in React we should generate index: true, loader: () => redirect(path)
NEW
426
                        const index = ts.factory.createPropertyAssignment("index", ts.factory.createTrue());
×
NEW
427
                        const loader = ts.factory.createArrowFunction(
×
428
                                undefined,
429
                                undefined,
430
                                [],
431
                                undefined,
432
                                undefined,
433
                                ts.factory.createCallExpression(
434
                                        ts.factory.createIdentifier("redirect"),
435
                                        [],
436
                                        [ts.factory.createStringLiteral(path, true)]
437
                                ));
NEW
438
                        const redirect = ts.factory.createPropertyAssignment("loader", loader);
×
NEW
439
                        return ts.factory.createObjectLiteralExpression([index, redirect]);
×
440
                }
441
                const routePath = ts.factory.createPropertyAssignment("path", ts.factory.createStringLiteral(path, true));
×
442
                const jsxElement = ts.factory.createJsxSelfClosingElement(
×
443
                        ts.factory.createIdentifier(component), [], undefined
444
                );
445
                const routeComponent =
446
                        ts.factory.createPropertyAssignment("element", jsxElement);
×
447
                const routeData = ts.factory.createPropertyAssignment("text", ts.factory.createStringLiteral(name, true));
×
448
                if (routerAlias) {
449
                        const childrenData = ts.factory.createPropertyAssignment("children", ts.factory.createIdentifier(routerAlias));
×
450
                        return ts.factory.createObjectLiteralExpression([routePath, routeComponent, routeData, childrenData]);
×
451
                }
NEW
452
                return ts.factory.createObjectLiteralExpression([routePath, routeComponent, routeData]);
×
453
        }
454
}
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

© 2026 Coveralls, Inc