diff --git a/src/deep-linking/util.spec.ts b/src/deep-linking/util.spec.ts index 2bc2e04a..5b64f700 100644 --- a/src/deep-linking/util.spec.ts +++ b/src/deep-linking/util.spec.ts @@ -3,7 +3,7 @@ import * as util from './util'; describe('util', () => { describe('extractDeepLinkPathData', () => { - /*it('should return the deep link metadata', () => { + it('should return the parsed deep link metadata', () => { const fileContent = ` import { NgModule } from '@angular/core'; import { IonicApp, IonicModule } from 'ionic-angular'; @@ -33,27 +33,92 @@ export function getSharedIonicModule() { return IonicModule.forRoot(MyApp, {}, { links: [ { loadChildren: '../pages/home/home.module#HomePageModule', name: 'Home' }, - { loadChildren: '../pages/page-one/page-one.module#PageOneModule', name: 'PageOne' }, - { loadChildren: '../pages/page-two/page-two.module#PageTwoModule', name: 'PageTwo' } + { name: "PageOne", loadChildren: "../pages/page-one/page-one.module#PageOneModule" }, + { loadChildren: \`../pages/page-two/page-two.module#PageTwoModule\`, name: \`PageTwo\` }, + { Component: MyComponent, name: 'SomePage'}, + { name: 'SomePage2', Component: MyComponent2 } ] }); } `; const results = util.extractDeepLinkPathData(fileContent); - expect(results).toBeTruthy(); - expect(Array.isArray(results)).toBeTruthy(); - expect(results[0].modulePath).toEqual('../pages/home/home.module'); - expect(results[0].namedExport).toEqual('HomePageModule'); - expect(results[0].name).toEqual('Home'); - expect(results[1].modulePath).toEqual('../pages/page-one/page-one.module'); - expect(results[1].namedExport).toEqual('PageOneModule'); - expect(results[1].name).toEqual('PageOne'); - expect(results[2].modulePath).toEqual('../pages/page-two/page-two.module'); - expect(results[2].namedExport).toEqual('PageTwoModule'); - expect(results[2].name).toEqual('PageTwo'); + + expect(results[0].component).toEqual(null); + expect(results[0].name).toBe('Home'); + expect(results[0].modulePath).toBe('../pages/home/home.module'); + expect(results[0].namedExport).toBe('HomePageModule'); + + expect(results[1].component).toEqual(null); + expect(results[1].name).toBe('PageOne'); + expect(results[1].modulePath).toBe('../pages/page-one/page-one.module'); + expect(results[1].namedExport).toBe('PageOneModule'); + + expect(results[2].component).toEqual(null); + expect(results[2].name).toBe('PageTwo'); + expect(results[2].modulePath).toBe('../pages/page-two/page-two.module'); + expect(results[2].namedExport).toBe('PageTwoModule'); + + expect(results[3].component).toEqual('MyComponent'); + expect(results[3].name).toBe('SomePage'); + expect(results[3].modulePath).toBe(null); + expect(results[3].namedExport).toBe(null); + + expect(results[4].component).toEqual('MyComponent2'); + expect(results[4].name).toBe('SomePage2'); + expect(results[4].modulePath).toBe(null); + expect(results[4].namedExport).toBe(null); + }); + + it('should throw an exception when there is an invalid deep link config', () => { + // arrange + const fileContent = ` +import { NgModule } from '@angular/core'; +import { IonicApp, IonicModule } from 'ionic-angular'; +import { MyApp } from './app.component'; +import { HomePage } from '../pages/home/home'; + +import * as Constants from '../util/constants'; + +@NgModule({ + declarations: [ + MyApp, + HomePage + ], + imports: [ + getSharedIonicModule() + ], + bootstrap: [IonicApp], + entryComponents: [ + MyApp, + HomePage + ], + providers: [] +}) +export class AppModule {} + +export function getSharedIonicModule() { + return IonicModule.forRoot(MyApp, {}, { + links: [ + { loadChildren: '../pages/home/home.module#HomePageModule'}, + { name: "PageOne", loadChildren: "../pages/page-one/page-one.module#PageOneModule" }, + { loadChildren: \`../pages/page-two/page-two.module#PageTwoModule\`, name: \`PageTwo\` }, + { Component: MyComponent, name: 'SomePage'}, + { name: 'SomePage2', Component: MyComponent2 } + ] + }); +} + `; + // act + const knownMessage = 'Should never get here'; + try { + util.extractDeepLinkPathData(fileContent); + throw new Error(knownMessage); + } catch (ex) { + // assert + expect(ex.message).not.toEqual(knownMessage); + } }); - */ }); describe('getDeepLinkData', () => { @@ -126,8 +191,9 @@ export function getSharedIonicModule() { return IonicModule.forRoot(MyApp, {}, { links: [ { loadChildren: '../pages/home/home.module#HomePageModule', name: 'Home' }, - { loadChildren: '../pages/page-one/page-one.module#PageOneModule', name: 'PageOne' }, - { loadChildren: '../pages/page-two/page-two.module#PageTwoModule', name: 'PageTwo' } + { name: "PageOne", loadChildren: "../pages/page-one/page-one.module#PageOneModule" }, + { loadChildren: \`../pages/page-two/page-two.module#PageTwoModule\`, name: \`PageTwo\` }, + { Component: MyComponent, name: 'SomePage'}, ] }); } @@ -136,20 +202,31 @@ export function getSharedIonicModule() { const srcDir = '/Users/dan/Dev/myApp/src'; const result = util.getDeepLinkData(join(srcDir, 'app/app.module.ts'), fileContent, false); expect(result[0].modulePath).toEqual('../pages/home/home.module'); + expect(result[0].namedExport).toEqual('HomePageModule'); expect(result[0].name).toEqual('Home'); + expect(result[0].component).toEqual(null); expect(result[0].absolutePath).toEqual('/Users/dan/Dev/myApp/src/pages/home/home.module.ts'); expect(result[1].modulePath).toEqual('../pages/page-one/page-one.module'); + expect(result[1].namedExport).toEqual('PageOneModule'); expect(result[1].name).toEqual('PageOne'); + expect(result[1].component).toEqual(null); expect(result[1].absolutePath).toEqual('/Users/dan/Dev/myApp/src/pages/page-one/page-one.module.ts'); expect(result[2].modulePath).toEqual('../pages/page-two/page-two.module'); + expect(result[2].namedExport).toEqual('PageTwoModule'); expect(result[2].name).toEqual('PageTwo'); + expect(result[2].component).toEqual(null); expect(result[2].absolutePath).toEqual('/Users/dan/Dev/myApp/src/pages/page-two/page-two.module.ts'); + expect(result[3].modulePath).toEqual(null); + expect(result[3].namedExport).toEqual(null); + expect(result[3].name).toEqual('SomePage'); + expect(result[3].component).toEqual('MyComponent'); + expect(result[3].absolutePath).toEqual(null); }); - /*it('should return a deep link data adjusted for AoT', () => { + it('should return a deep link data adjusted for AoT', () => { const fileContent = ` import { NgModule } from '@angular/core'; @@ -180,9 +257,9 @@ export function getSharedIonicModule() { return IonicModule.forRoot(MyApp, {}, { links: [ { loadChildren: '../pages/home/home.module#HomePageModule', name: 'Home' }, - { loadChildren: '../pages/page-one/page-one.module#PageOneModule', name: 'PageOne' }, - { loadChildren: '../pages/page-two/page-two.module#PageTwoModule', name: 'PageTwo' }, - { loadChildren: '../pages/page-three/page-three.module#PageThreeModule', name: 'PageThree' } + { name: "PageOne", loadChildren: "../pages/page-one/page-one.module#PageOneModule" }, + { loadChildren: \`../pages/page-two/page-two.module#PageTwoModule\`, name: \`PageTwo\` }, + { Component: MyComponent, name: 'SomePage'}, ] }); } @@ -193,18 +270,132 @@ export function getSharedIonicModule() { expect(result[0].modulePath).toEqual('../pages/home/home.module.ngfactory'); expect(result[0].namedExport).toEqual('HomePageModuleNgFactory'); expect(result[0].name).toEqual('Home'); + expect(result[0].component).toEqual(null); expect(result[0].absolutePath).toEqual('/Users/dan/Dev/myApp/src/pages/home/home.module.ngfactory.ts'); expect(result[1].modulePath).toEqual('../pages/page-one/page-one.module.ngfactory'); expect(result[1].namedExport).toEqual('PageOneModuleNgFactory'); expect(result[1].name).toEqual('PageOne'); + expect(result[1].component).toEqual(null); expect(result[1].absolutePath).toEqual('/Users/dan/Dev/myApp/src/pages/page-one/page-one.module.ngfactory.ts'); expect(result[2].modulePath).toEqual('../pages/page-two/page-two.module.ngfactory'); expect(result[2].namedExport).toEqual('PageTwoModuleNgFactory'); expect(result[2].name).toEqual('PageTwo'); + expect(result[2].component).toEqual(null); expect(result[2].absolutePath).toEqual('/Users/dan/Dev/myApp/src/pages/page-two/page-two.module.ngfactory.ts'); + + expect(result[3].modulePath).toEqual(null); + expect(result[3].namedExport).toEqual(null); + expect(result[3].name).toEqual('SomePage'); + expect(result[3].component).toEqual('MyComponent'); + expect(result[3].absolutePath).toEqual(null); + }); + }); + + describe('validateDeepLinks', () => { + it('should return false when one entry is missing name', () => { + // arrange + const invalidDeepLinkConfig: any = { + name: null, + component: {} + }; + // act + const result = util.validateDeepLinks([invalidDeepLinkConfig]); + + // assert + expect(result).toEqual(false); + }); + + it('should return false when one entry has empty name', () => { + // arrange + const invalidDeepLinkConfig: any = { + name: '', + component: {} + }; + // act + const result = util.validateDeepLinks([invalidDeepLinkConfig]); + + // assert + expect(result).toEqual(false); + }); + + it('should return false when missing component and (modulePath or namedExport)', () => { + // arrange + const invalidDeepLinkConfig: any = { + name: 'someName', + component: null, + modulePath: null + }; + + // act + const result = util.validateDeepLinks([invalidDeepLinkConfig]); + + // assert + expect(result).toEqual(false); + }); + + it('should return false when missing component and (modulePath or namedExport)', () => { + // arrange + const invalidDeepLinkConfig: any = { + name: 'someName', + component: '', + modulePath: '' + }; + + // act + const result = util.validateDeepLinks([invalidDeepLinkConfig]); + + // assert + expect(result).toEqual(false); + }); + + it('should return false when missing component and has valid modulePath but missing namedExport', () => { + // arrange + const invalidDeepLinkConfig: any = { + name: 'someName', + component: '', + modulePath: 'somePath', + namedExport: '' + }; + + // act + const result = util.validateDeepLinks([invalidDeepLinkConfig]); + + // assert + expect(result).toEqual(false); + }); + + it('should return true when it has a valid modulePath and namedExport', () => { + // arrange + const invalidDeepLinkConfig: any = { + name: 'someName', + component: '', + modulePath: 'somePath', + namedExport: 'someNamedExport' + }; + + // act + const result = util.validateDeepLinks([invalidDeepLinkConfig]); + + // assert + expect(result).toEqual(true); + }); + + it('should return true when it has a valid component', () => { + // arrange + const invalidDeepLinkConfig: any = { + name: 'someName', + component: 'MyComponent', + modulePath: null, + namedExport: null + }; + + // act + const result = util.validateDeepLinks([invalidDeepLinkConfig]); + + // assert + expect(result).toEqual(true); }); - */ }); }); diff --git a/src/deep-linking/util.ts b/src/deep-linking/util.ts index 9e0b4705..985fa1e3 100644 --- a/src/deep-linking/util.ts +++ b/src/deep-linking/util.ts @@ -1,9 +1,12 @@ import { dirname, join } from 'path'; import { DeepLinkConfigEntry, HydratedDeepLinkConfigEntry } from '../util/interfaces'; +const LOAD_CHILDREN_SPLIT_TOKEN = '#'; + /* this is a very temporary approach to extracting deeplink data since the Angular compiler API has changed a bit */ function getLinksArrayContent(appNgModuleFileContent: string) { + const LINKS_REGEX = /links\s*?:\s*\[([\s|\S]*?)\]/igm; const deepLinksContentMatches = LINKS_REGEX.exec(appNgModuleFileContent.toString()); if (deepLinksContentMatches && deepLinksContentMatches.length === 2) { return deepLinksContentMatches[1]; @@ -11,40 +14,82 @@ function getLinksArrayContent(appNgModuleFileContent: string) { return null; } -export function extractDeepLinkPathData(appNgModuleFileContent: string) { +export function extractDeepLinkPathData(appNgModuleFileContent: string): DeepLinkConfigEntry[] { const linksInternalContent = getLinksArrayContent(appNgModuleFileContent); if (!linksInternalContent) { return null; } - const pathsList = extractRegexContent(appNgModuleFileContent, LOAD_CHILDREN_REGEX); - const nameList = extractRegexContent(appNgModuleFileContent, NAME_REGEX); + // parse into individual entries + const results = getIndividualConfigEntries(linksInternalContent); + // convert each long, multi-element string into it's proper fields + const deepLinks = results.map(result => convertRawContentStringToParsedDeepLink(result)); + const valid = validateDeepLinks(deepLinks); + if (!valid) { + throw new Error('Each deep link entry must contain a "name" entry, and a "component" or "loadChildren" entry'); + } + + + return deepLinks; +} + +export function validateDeepLinks(deepLinks: DeepLinkConfigEntry[]) { + for (const deepLink of deepLinks) { + if (!deepLink.name || deepLink.name.length === 0) { + return false; + } + const missingComponent = !deepLink.component || deepLink.component.length === 0; + const missingModulePath = !deepLink.modulePath || deepLink.modulePath.length === 0; + const missingNamedExport = !deepLink.namedExport || deepLink.namedExport.length === 0; - const expectedLength = pathsList.length; + if (missingComponent && (missingModulePath || missingNamedExport)) { + return false; + } + } + return true; +} - if (nameList.length !== expectedLength) { - throw new Error(`Expected ${expectedLength} names in deep link config, found the following: ${nameList.join(',')}`); +function convertRawContentStringToParsedDeepLink(input: string): DeepLinkConfigEntry { + const LOAD_CHILDREN_REGEX = /loadChildren\s*?:\s*?['"`]\s*?(.*?)['"`]/igm; + const NAME_REGEX = /name\s*?:\s*?['"`]\s*?(.*?)['"`]/igm; + const COMPONENT_REGEX = /component\s*?:(.*?)[,}]/igm; + const loadChildrenValue = extractContentWithKnownMatch(input, LOAD_CHILDREN_REGEX); + const nameValue = extractContentWithKnownMatch(input, NAME_REGEX); + const componentValue = extractContentWithKnownMatch(input, COMPONENT_REGEX); + let modulePath = null; + let namedExport = null; + if (loadChildrenValue) { + const tokens = loadChildrenValue.split(LOAD_CHILDREN_SPLIT_TOKEN); + if (tokens.length === 2) { + modulePath = tokens[0]; + namedExport = tokens[1]; + } } - // metadata looks legit, let's do some looping shall we - const deepLinkConfig: DeepLinkConfigEntry[] = []; - for (let i = 0; i < expectedLength; i++) { - const moduleAndExport = pathsList[i].split('#'); - const path = moduleAndExport[0]; - const namedExport = moduleAndExport[1]; - const name = nameList[i]; - deepLinkConfig.push({modulePath: path, namedExport: namedExport, name: name}); + return { + component: componentValue, + name: nameValue, + modulePath: modulePath, + namedExport: namedExport + }; +} + +function extractContentWithKnownMatch(input: string, regex: RegExp) { + const result = regex.exec(input); + if (result && result.length > 1) { + return result[1].trim(); } - return deepLinkConfig; + return null; } -function extractRegexContent(content: string, regex: RegExp) { +function getIndividualConfigEntries(content: string) { let match: RegExpExecArray = null; const results: string[] = []; - while ((match = regex.exec(content))) { + const INDIVIDUAL_ENTRIES_REGEX = /({.*?})/igm; + while ((match = INDIVIDUAL_ENTRIES_REGEX.exec(content))) { if (!match) { break; } - results.push(match[1]); + results.push(match[1].trim()); } return results; } @@ -58,16 +103,18 @@ export function getDeepLinkData(appNgModuleFilePath: string, appNgModuleFileCont const absolutePathSuffix = isAot ? '.ngfactory.ts' : '.ts'; const modulePathSuffix = isAot ? '.ngfactory' : ''; const namedExportSuffix = isAot ? 'NgFactory' : ''; - const hydratedDeepLinks = deepLinkConfigList.map(deepLinkConfigEntry => { + const hydratedDeepLinks = deepLinkConfigList.map((deepLinkConfigEntry: DeepLinkConfigEntry) => { return Object.assign({}, deepLinkConfigEntry, { - modulePath: deepLinkConfigEntry.modulePath + modulePathSuffix, - namedExport: deepLinkConfigEntry.namedExport + namedExportSuffix, - absolutePath: join(appDirectory, deepLinkConfigEntry.modulePath + absolutePathSuffix) + modulePath: deepLinkConfigEntry.modulePath ? deepLinkConfigEntry.modulePath + modulePathSuffix : null, + namedExport: deepLinkConfigEntry.namedExport ? deepLinkConfigEntry.namedExport + namedExportSuffix : null, + absolutePath: deepLinkConfigEntry.modulePath ? join(appDirectory, deepLinkConfigEntry.modulePath + absolutePathSuffix) : null }) as HydratedDeepLinkConfigEntry; }); return hydratedDeepLinks; } -const LINKS_REGEX = /links\s*?:\s*\[([\s|\S]*)\]/igm; -const LOAD_CHILDREN_REGEX = /loadChildren\s*?:\s*?['"`]\s*?(.*?)['"`]/igm; -const NAME_REGEX = /name\s*?:\s*?['"`]\s*?(.*?)['"`]/igm; +interface ParsedDeepLink { + component: string; + name: string; + loadChildren: string; +}; diff --git a/src/util/interfaces.ts b/src/util/interfaces.ts index 432877fb..991b8787 100644 --- a/src/util/interfaces.ts +++ b/src/util/interfaces.ts @@ -148,6 +148,7 @@ export interface DeepLinkConfigEntry { modulePath: string; namedExport: string; name: string; + component: string; }; export interface HydratedDeepLinkConfigEntry extends DeepLinkConfigEntry {