From bf29936af930f1848b5a85e32327edcba1694fe2 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Thu, 25 Jan 2018 10:13:30 +0000 Subject: [PATCH] build(aio): test Firebase hosting redirection configuration (#21763) PR Close #21763 --- aio/package.json | 10 +- aio/tests/deployment/URLS_TO_REDIRECT.txt | 181 +++++++++++++++++ aio/tests/deployment/helpers.ts | 29 +++ .../testFirebaseRedirection.spec.ts | 31 +++ .../firebase-test-utils/FirebaseGlob.spec.ts | 189 ++++++++++++++++++ aio/tools/firebase-test-utils/FirebaseGlob.ts | 74 +++++++ .../FirebaseRedirect.spec.ts | 30 +++ .../firebase-test-utils/FirebaseRedirect.ts | 16 ++ .../FirebaseRedirector.spec.ts | 42 ++++ .../firebase-test-utils/FirebaseRedirector.ts | 36 ++++ aio/tools/tslint.json | 88 ++++++++ aio/yarn.lock | 29 ++- scripts/ci/test-aio.sh | 4 + 13 files changed, 752 insertions(+), 7 deletions(-) create mode 100644 aio/tests/deployment/URLS_TO_REDIRECT.txt create mode 100644 aio/tests/deployment/helpers.ts create mode 100644 aio/tests/deployment/testFirebaseRedirection.spec.ts create mode 100644 aio/tools/firebase-test-utils/FirebaseGlob.spec.ts create mode 100644 aio/tools/firebase-test-utils/FirebaseGlob.ts create mode 100644 aio/tools/firebase-test-utils/FirebaseRedirect.spec.ts create mode 100644 aio/tools/firebase-test-utils/FirebaseRedirect.ts create mode 100644 aio/tools/firebase-test-utils/FirebaseRedirector.spec.ts create mode 100644 aio/tools/firebase-test-utils/FirebaseRedirector.ts create mode 100644 aio/tools/tslint.json diff --git a/aio/package.json b/aio/package.json index 328a7234b9..fa5d9b0ada 100644 --- a/aio/package.json +++ b/aio/package.json @@ -15,7 +15,7 @@ "build": "yarn ~~build", "prebuild-local": "yarn setup-local", "build-local": "yarn ~~build", - "lint": "yarn check-env && yarn docs-lint && ng lint && yarn example-lint", + "lint": "yarn check-env && yarn docs-lint && ng lint && yarn example-lint && yarn tools-lint", "test": "yarn check-env && ng test", "pree2e": "yarn check-env && yarn ~~update-webdriver", "e2e": "ng e2e --no-webdriver-update", @@ -44,7 +44,10 @@ "docs-watch": "node tools/transforms/authors-package/watchr.js", "docs-lint": "eslint --ignore-path=\"tools/transforms/.eslintignore\" tools/transforms", "docs-test": "node tools/transforms/test.js", - "tools-test": "./scripts/deploy-to-firebase.test.sh && yarn docs-test && yarn boilerplate:test && jasmine tools/ng-packages-installer/index.spec.js", + "deployment-config-test": "jasmine-ts tests/deployment/*.spec.ts", + "firebase-utils-test": "jasmine-ts tools/firebase-test-utils/*.spec.ts", + "tools-lint": "tslint -c \"tools/tslint.json\" \"tools/firebase-test-utils/**/*.ts\"", + "tools-test": "./scripts/deploy-to-firebase.test.sh && yarn docs-test && yarn boilerplate:test && jasmine tools/ng-packages-installer/index.spec.js && yarn firebase-utils-test", "preserve-and-sync": "yarn docs", "serve-and-sync": "concurrently --kill-others \"yarn docs-watch --watch-only\" \"yarn start\"", "boilerplate:add": "node ./tools/examples/example-boilerplate add", @@ -98,6 +101,7 @@ "archiver": "^1.3.0", "canonical-path": "^0.0.2", "chalk": "^2.1.0", + "cjson": "^0.5.0", "codelyzer": "~2.0.0", "concurrently": "^3.4.0", "cross-spawn": "^5.1.0", @@ -118,6 +122,7 @@ "image-size": "^0.5.1", "jasmine-core": "^2.8.0", "jasmine-spec-reporter": "^4.1.0", + "jasmine-ts": "^0.2.1", "jsdom": "^9.12.0", "karma": "^1.7.0", "karma-chrome-launcher": "^2.1.1", @@ -147,6 +152,7 @@ "unist-util-visit-parents": "^1.1.1", "vrsource-tslint-rules": "^4.0.1", "watchr": "^3.0.1", + "xregexp": "^4.0.0", "yargs": "^7.0.2" } } diff --git a/aio/tests/deployment/URLS_TO_REDIRECT.txt b/aio/tests/deployment/URLS_TO_REDIRECT.txt new file mode 100644 index 0000000000..b8df3bd6c0 --- /dev/null +++ b/aio/tests/deployment/URLS_TO_REDIRECT.txt @@ -0,0 +1,181 @@ +/api/api/core/ElementRef /api/core/ElementRef +/api/common/Control-class /api/forms/FormControl +/api/common/CORE_DIRECTIVES /api/common/CommonModule +/api/common/DatePipe-class /api/common/DatePipe +/api/common/NgClass-directive.html /api/common/NgClass +/api/common/NgFor-directive.html /api/common/NgForOf +/api/common/NgModel-directive /api/forms/NgModel +/api/common/SelectControlValueAccessor-directive /api/forms/SelectControlValueAccessor +/api/common/SlicePipe-class.html /api/common/SlicePipe +/api/core/AnimationStateDeclarationMetadata /api/animations +/api/core/DoCheck-interface.html /api/core/DoCheck +/api/core/HostBinding-var /api/core/HostBinding +/api/core/OpaqueToken-class /api/core/OpaqueToken +/api/core/OptionalMetadata-class /api/core/Optional +/api/core/PLATFORM_PIPES /api/common/CommonModule +/api/core/Provider-class.html /api/core/Provider +/api/core/Renderer-class /api/core/Renderer +/api/core/testing/async-function /api/core/testing/async +/api/core/testing/index/async-function /api/core/testing/async +/api/core/testing/index/TestBed-class.html /api/core/testing/TestBed +/api/core/testing/inject-function /api/core/testing/inject +/api/core/testing/inject-function.html /api/core/testing/inject +/api/http/Headers-class /api/http/Headers +/api/http/Headers-class.html /api/http/Headers +/api/http/HTTP_PROVIDERS-let /api/http/HttpModule +/api/http/testing/index/MockBackend-class /api/http/testing/MockBackend +/api/http/testing/index/MockBackend-class.html /api/http/testing/MockBackend +/api/platform-browser-dynamic/testing/index/platformBrowserDynamicTesting-let.html /api/platform-browser-dynamic/testing/platformBrowserDynamicTesting +/api/platform-browser/AnimationDriver /api/animations/browser/AnimationDriver +/api/router/Route-class /api/router/Route +/api/router/RouterLink-directive /api/router/RouterLink +/api/testing/tick-function /api/core/testing/tick +/api/upgrade/static/downgradeComponent-function /api/upgrade/static/downgradeComponent +/api/upgrade/static/downgradeComponent-function.html /api/upgrade/static/downgradeComponent +/api/upgrade/static/index/downgradeComponent-function /api/upgrade/static/downgradeComponent +/api/upgrade/static/UpgradeModule-class /api/upgrade/static/UpgradeModule +/docs/js/latest/api/ /api +/docs/js/latest/api/animate/AnimationBuilder-class /api/animations/AnimationBuilder +/docs/js/latest/api/animate/CssAnimationBuilder-class.html /api/animations/CssAnimationBuilder +/docs/js/latest/api/animations/index/trigger-function /api/animations/trigger +/docs/js/latest/api/common/ControlGroup-class.html /api/forms/FormGroup +/docs/js/latest/api/common/index/DatePipe-pipe /api/common/DatePipe +/docs/js/latest/api/common/index/Location-class.html /api/common/Location +/docs/js/latest/api/common/index/MaxLengthValidator-directive.html /api/forms/MaxLengthValidator +/docs/js/latest/api/common/index/NgFor-directive /api/common/NgForOf +/docs/js/latest/api/common/index/NgFor-directive.html /api/common/NgForOf +/docs/js/latest/api/common/index/NgFor-type-alias /api/common/NgForOf +/docs/js/latest/api/common/index/NgFor-type-alias.html /api/common/NgForOf +/docs/js/latest/api/common/index/NgTemplateOutlet-directive /api/common/NgTemplateOutlet +/docs/js/latest/api/common/NgStyle-directive /api/common/NgStyle +/docs/js/latest/api/core/DynamicComponentLoader-class.html /api/core/DynamicComponentLoader +/docs/js/latest/api/core/HostListener-var /api/core/HostListener +/docs/js/latest/api/core/index/AfterViewChecked-class /api/core/AfterViewChecked +/docs/js/latest/api/core/index/AnimationStateTransitionMetadata-class.html /api/core/AnimationStateTransitionMetadata +/docs/js/latest/api/core/index/AnimationStateTransitionMetadata-type-alias /api/core/AnimationStateTransitionMetadata +/docs/js/latest/api/core/index/ApplicationModule-class /api/core/ApplicationModule +/docs/js/latest/api/core/index/ApplicationRef-class /api/core/ApplicationRef +/docs/js/latest/api/core/index/ChangeDetectorRef-class /api/core/ChangeDetectorRef +/docs/js/latest/api/core/index/ComponentFactory-class /api/core/ComponentFactory +/docs/js/latest/api/core/index/DebugNode-class /api/core/DebugNode +/docs/js/latest/api/core/index/destroyPlatform-function /api/core/destroyPlatform +/docs/js/latest/api/core/index/DirectiveMetadata-class /api/core/Directive +/docs/js/latest/api/core/index/ErrorHandler-class /api/core/ErrorHandler +/docs/js/latest/api/core/index/EventEmitter-class /api/core/EventEmitter +/docs/js/latest/api/core/index/EventEmitter-class.html /api/core/EventEmitter +/docs/js/latest/api/core/index/getPlatform-function /api/core/getPlatform +/docs/js/latest/api/core/index/NgModule-interface /api/core/NgModule +/docs/js/latest/api/core/index/NgModuleRef-class /api/core/NgModuleRef +/docs/js/latest/api/core/index/NgModuleRef-class.html /api/core/NgModuleRef +/docs/js/latest/api/core/index/OnInit-class /api/core/OnInit +/docs/js/latest/api/core/index/OnInit-class.html /api/core/OnInit +/docs/js/latest/api/core/index/OnInit-interface /api/core/OnInit +/docs/js/latest/api/core/index/PlatformRef-class /api/core/PlatformRef +/docs/js/latest/api/core/index/QueryList-class.html /api/core/QueryList +/docs/js/latest/api/core/index/RenderComponentType-class /api/core/RenderComponentType +/docs/js/latest/api/core/index/Renderer2-class.html /api/core/Renderer2 +/docs/js/latest/api/core/index/state-function.html /api/core/state +/docs/js/latest/api/core/index/transition-function.html /api/core/transition +/docs/js/latest/api/core/index/trigger-function /api/core/trigger +/docs/js/latest/api/core/index/trigger-function.html /api/core/trigger +/docs/js/latest/api/core/index/ViewChild-decorator /api/core/ViewChild +/docs/js/latest/api/core/index/wtfLeave-let /api/core/wtfLeave +/docs/js/latest/api/core/Injector-class.html /api/core/Injector +/docs/js/latest/api/core/Query-var /api/core/Query +/docs/js/latest/api/core/ResolvedProvider-interface.html /api/core/ResolvedProvider +/docs/js/latest/api/core/testing/index/TestBed-class /api/core/testing/TestBed +/docs/js/latest/api/core/testing/index/tick-function /api/core/testing/tick +/docs/js/latest/api/core/testing/index/tick-function.html /api/core/testing/tick +/docs/js/latest/api/core/ViewContainerRef-class.html /api/core/ViewContainerRef +/docs/js/latest/api/forms/index/AbstractControl-class /api/forms/AbstractControl +/docs/js/latest/api/forms/index/AbstractControl-class.html /api/forms/AbstractControl +/docs/js/latest/api/forms/index/FormArray-class.html /api/forms/FormArray +/docs/js/latest/api/forms/index/FormBuilder-class.html /api/forms/FormBuilder +/docs/js/latest/api/forms/index/NG_VALIDATORS-let /api/forms/NG_VALIDATORS +/docs/js/latest/api/forms/index/Validator-interface.html /api/forms/Validator +/docs/js/latest/api/http/ConnectionBackend-class /api/http/ConnectionBackend +/docs/js/latest/api/http/index/Http-class.html /api/http/Http +/docs/js/latest/api/http/index/Jsonp-class.html /api/http/Jsonp +/docs/js/latest/api/http/index/ResponseOptions-class.html /api/http/ResponseOptions +/docs/js/latest/api/http/index/URLSearchParams-class /api/http/URLSearchParams +/docs/js/latest/api/http/index/XHRConnection-class /api/http/XHRConnection +/docs/js/latest/api/http/index/XHRConnection-class.html /api/http/XHRConnection +/docs/js/latest/api/http/testing/index/MockConnection-class.html /api/http/testing/MockConnection +/docs/js/latest/api/http/testing/MockBackend-class /api/http/testing/MockBackend +/docs/js/latest/api/platform-browser-dynamic/index/platformBrowserDynamic-let.html /api/platform-browser-dynamic/platformBrowserDynamic +/docs/js/latest/api/platform-browser-dynamic/testing/index/BrowserDynamicTestingModule-class.html /api/platform-browser-dynamic/testing/BrowserDynamicTestingModule +/docs/js/latest/api/platform-browser/animations/index/BrowserAnimationsModule-class /api/platform-browser/animations/BrowserAnimationsModule +/docs/js/latest/api/platform-browser/animations/index/BrowserAnimationsModule-class.html /api/platform-browser/animations/BrowserAnimationsModule +/docs/js/latest/api/platform-browser/animations/index/NoopAnimationsModule-class.html /api/platform-browser/animations/NoopAnimationsModule +/docs/js/latest/api/platform-browser/index/DomSanitizer-class /api/platform-browser/DomSanitizer +/docs/js/latest/api/platform-browser/index/Meta-class /api/platform-browser/Meta +/docs/js/latest/api/platform-browser/index/MetaDefinition-type-alias /api/platform-browser/MetaDefinition +/docs/js/latest/api/platform-browser/index/SafeUrl-interface /api/platform-browser/SafeUrl +/docs/js/latest/api/platform-browser/index/Title-class /api/platform-browser/Title +/docs/js/latest/api/platform-browser/index/Title-class.html /api/platform-browser/Title +/docs/js/latest/api/platform-server/index/PlatformState-class /api/platform-server/PlatformState +/docs/js/latest/api/platform-server/index/PlatformState-class.html /api/platform-server/PlatformState +/docs/js/latest/api/platform-webworker /api/platform-webworker +/docs/js/latest/api/platform-webworker/index/MessageBus-class /api/platform-webworker/MessageBus +/docs/js/latest/api/router/index/ActivatedRoute-interface /api/router/ActivatedRoute +/docs/js/latest/api/router/index/CanActivate-interface.html /api/router/CanActivate +/docs/js/latest/api/router/index/CanActivateChild-interface /api/router/CanActivateChild +/docs/js/latest/api/router/index/CanDeactivate-interface.html /api/router/CanDeactivate +/docs/js/latest/api/router/index/CanLoad-interface /api/router/CanLoad +/docs/js/latest/api/router/index/CanLoad-interface.html /api/router/CanLoad +/docs/js/latest/api/router/index/NavigationEnd-class /api/router/NavigationEnd +/docs/js/latest/api/router/index/provideRoutes-function /api/router/provideRoutes +/docs/js/latest/api/router/index/provideRoutes-function.html /api/router/provideRoutes +/docs/js/latest/api/router/index/Router-class /api/router/Router +/docs/js/latest/api/router/index/RouterLink-directive /api/router/RouterLink +/docs/js/latest/api/router/index/Routes-type-alias /api/router/Routes +/docs/js/latest/api/router/index/UrlSegment-class /api/router/UrlSegment +/docs/js/latest/api/router/index/UrlSerializer-class.html /api/router/UrlSerializer +/docs/js/latest/api/router/Instruction-class /api/router/Instruction +/docs/js/latest/api/router/Location-class /api/router/Location +/docs/js/latest/api/router/OnActivate-interface /api/router/OnActivate +/docs/js/latest/api/router/Redirect-class.html /api/router/Redirect +/docs/js/latest/api/router/RouteDefinition-interface.html /api/router/RouteDefinition +/docs/js/latest/api/router/RouteParams-class /api/router/RouteParams +/docs/js/latest/api/router/RouteParams-class.html /api/router/RouteParams +/docs/js/latest/api/router/Router-class.html /api/router/Router +/docs/js/latest/api/router/testing /api/router/testing +/docs/js/latest/api/upgrade/index/UpgradeAdapter-class.html /api/upgrade/UpgradeAdapter +/docs/js/latest/api/upgrade/static/UpgradeModule-class /api/upgrade/static/UpgradeModule +/docs/js/latest/api/upgrade/static/UpgradeModule-class.html /api/upgrade/static/UpgradeModule +/docs/js/latest/cookbook/ts-to-js.html https://github.com/angular/angular/blob/master/aio/content/guide/change-log.md#es6--described-in-typescript-to-javascript-2016-11-14 +/docs/js/latest/glossary /guide/glossary +/docs/js/latest/guide/ /docs +/docs/js/latest/guide/lifecycle-hooks /guide/lifecycle-hooks +/docs/js/latest/guide/ngmodule /guide/ngmodules +/docs/js/latest/resources /resources +/docs/latest/tutorial /tutorial +/docs/styleguide /guide/styleguide +/docs/styleguide.html /guide/styleguide +/docs/ts/latest/api/core/HostBinding-var.html /api/core/HostBinding +/docs/ts/latest/api/core/index/BaseException-class.html /api/core/BaseException +/docs/ts/latest/api/core/index/PLATFORM_PIPES-let.html /api/common/CommonModule +/docs/ts/latest/api/core/OnInit-interface.html /api/core/OnInit +/docs/ts/latest/api/core/OpaqueToken-class.html /api/core/OpaqueToken +/docs/ts/latest/api/core/OptionalMetadata-class.html /api/core/Optional +/docs/ts/latest/api/core/testing/index/async-function.html /api/core/testing/async +/docs/ts/latest/api/core/testing/index/fakeAsync-function.html /api/core/testing/fakeAsync +/docs/ts/latest/api/core/testing/index/TestComponentRenderer-class.html /api/core/testing/TestComponentRenderer +/docs/ts/latest/api/core/testing/index/tick-function.html /api/core/testing/tick +/docs/ts/latest/api/http/Connection-class.html /api/http/Connection +/docs/ts/latest/api/http/testing/index/MockBackend-class.html /api/http/testing/MockBackend +/docs/ts/latest/api/http/testing/index/MockConnection-class.html /api/http/testing/MockConnection +/docs/ts/latest/api/platform-browser-dynamic/index/workerAppDynamicPlatform-let.html /api/platform-browser-dynamic/workerAppDynamicPlatform +/docs/ts/latest/api/testing/fakeAsync-function.html /api/core/testing/fakeAsync +/docs/ts/latest/cookbook/ts-to-js.html https://github.com/angular/angular/blob/master/aio/content/guide/change-log.md#es6--described-in-typescript-to-javascript-2016-11-14 +/guide/cli-quickstart /guide/quickstart +/guide/learning-angular /guide/quickstart +/guide/learning-angular.html /guide/quickstart +/guide/metadata /guide/aot-compiler +/guide/service-worker-getstart /guide/service-worker-getting-started +/guide/service-worker-comm /guide/service-worker-communications +/guide/service-worker-configref /guide/service-worker-config +/news https://blog.angular.io/ +/news.html https://blog.angular.io/ +/testing /guide/testing +/testing/first-app-tests.html /guide/testing \ No newline at end of file diff --git a/aio/tests/deployment/helpers.ts b/aio/tests/deployment/helpers.ts new file mode 100644 index 0000000000..6193740e3c --- /dev/null +++ b/aio/tests/deployment/helpers.ts @@ -0,0 +1,29 @@ +const { readFileSync } = require('fs'); +const path = require('canonical-path'); +const cjson = require('cjson'); + +import { FirebaseRedirector, FirebaseRedirectConfig } from '../../tools/firebase-test-utils/FirebaseRedirector'; + +export function getRedirector() { + return new FirebaseRedirector(loadRedirects()); +} + +export function loadRedirects(): FirebaseRedirectConfig[] { + const pathToFirebaseJSON = path.resolve(__dirname, '../../firebase.json'); + const contents = cjson.load(pathToFirebaseJSON); + return contents.hosting.redirects; +} + +export function loadSitemapUrls() { + const pathToSiteMap = path.resolve(__dirname, '../../src/generated/sitemap.xml'); + const xml = readFileSync(pathToSiteMap, 'utf8'); + const urls: string[] = []; + xml.replace(/([^<]+)<\/loc>/g, (_, loc) => urls.push(loc.replace('%%DEPLOYMENT_HOST%%', ''))); + return urls; +} + +export function loadLegacyUrls() { + const pathToLegacyUrls = path.resolve(__dirname, 'URLS_TO_REDIRECT.txt'); + const urls = readFileSync(pathToLegacyUrls, 'utf8').split('\n').map(line => line.split('\t')); + return urls; +} diff --git a/aio/tests/deployment/testFirebaseRedirection.spec.ts b/aio/tests/deployment/testFirebaseRedirection.spec.ts new file mode 100644 index 0000000000..19b687456d --- /dev/null +++ b/aio/tests/deployment/testFirebaseRedirection.spec.ts @@ -0,0 +1,31 @@ +import { getRedirector, loadLegacyUrls, loadRedirects, loadSitemapUrls } from './helpers'; + +describe('firebase.json redirect config', () => { + describe('with sitemap urls', () => { + loadSitemapUrls().forEach(url => { + it('should not redirect any urls in the sitemap', () => { + expect(getRedirector().redirect(url)).toEqual(url); + }); + }); + }); + + describe('with legacy urls', () => { + loadLegacyUrls().forEach(urlPair => { + it('should redirect the legacy urls', () => { + const redirector = getRedirector(); + expect(redirector.redirect(urlPair[0])).not.toEqual(urlPair[0]); + if (urlPair[1]) { + expect(redirector.redirect(urlPair[0])).toEqual(urlPair[1]); + } + }); + }); + + describe('destinations', () => { + loadRedirects().forEach(redirect => { + it('should match pattern "^(https?:/)?/.*"', () => { + expect(redirect.destination).toMatch(/^(https?:\/)?\/.*/); + }); + }); + }); + }); +}); diff --git a/aio/tools/firebase-test-utils/FirebaseGlob.spec.ts b/aio/tools/firebase-test-utils/FirebaseGlob.spec.ts new file mode 100644 index 0000000000..447ec98c92 --- /dev/null +++ b/aio/tools/firebase-test-utils/FirebaseGlob.spec.ts @@ -0,0 +1,189 @@ +import { FirebaseGlob } from './FirebaseGlob'; +describe('FirebaseGlob', () => { + + describe('test', () => { + it('should match * parts', () => { + testGlob('asdf/*.jpg', + ['asdf/.jpg', 'asdf/asdf.jpg', 'asdf/asdf_asdf.jpg'], + ['asdf/asdf/asdf.jpg', 'xxxasdf/asdf.jpgxxx']); + }); + + it('should match ** parts', () => { + testGlob('asdf/**.jpg', // treated like two consecutive single `*`s + ['asdf/.jpg', 'asdf/asdf.jpg', 'asdf/asdf_asdf.jpg'], + ['asdf/a/.jpg', 'asdf/a/b.jpg', '/asdf/asdf.jpg', 'asdff/asdf.jpg', 'xxxasdf/asdf.jpg', 'asdf/asdf.jpgxxx']); + }); + + it('should match **/ and /**/', () => { + testGlob('**/*.js', + ['asdf.js', 'asdf/asdf.js', 'asdf/asdf/asdfasdf_asdf.js', '/asdf/asdf.js', '/asdf/aasdf-asdf.2.1.4.js'], + ['asdf/asdf.jpg', '/asdf/asdf.jpg']); + testGlob('aaa/**/bbb', + ['aaa/xxx/bbb', 'aaa/xxx/yyy/bbb', 'aaa/bbb'], + ['/aaa/xxx/bbb', 'aaa/x/bbb/', 'aaa/bbb/ccc']); + }); + + it('should match choice groups', () => { + testGlob('aaa/*.@(bbb|ccc)', + ['aaa/aaa.bbb', 'aaa/aaa_aaa.ccc'], + ['/aaa/aaa.bbb', 'aaaf/aaa.bbb', 'aaa/aaa.ddd']); + + testGlob('aaa/*(bbb|ccc)', + ['aaa/', 'aaa/bbb', 'aaa/ccc', 'aaa/bbbbbb', 'aaa/bbbccc', 'aaa/cccbbb', 'aaa/bbbcccbbb'], + ['aaa/aaa', 'aaa/bbbb']); + + testGlob('aaa/+(bbb|ccc)', + ['aaa/bbb', 'aaa/ccc', 'aaa/bbbbbb', 'aaa/bbbccc', 'aaa/cccbbb', 'aaa/bbbcccbbb'], + ['aaa/', 'aaa/aaa', 'aaa/bbbb']); + + testGlob('aaa/?(bbb|ccc)', + ['aaa/', 'aaa/bbb', 'aaa/ccc'], + ['aaa/aaa', 'aaa/bbbb', 'aaa/bbbbbb', 'aaa/bbbccc', 'aaa/cccbbb', 'aaa/bbbcccbbb']); + }); + + it('should error on non-supported choice groups', () => { + expect(() => new FirebaseGlob('/!(a|b)/c')) + .toThrowError('Error in FirebaseGlob: "/!(a|b)/c" - "not" expansions are not supported: "!(a|b)"'); + expect(() => new FirebaseGlob('/(a|b)/c')) + .toThrowError('Error in FirebaseGlob: "/(a|b)/c" - unknown expansion type: "/" in "/(a|b)"'); + expect(() => new FirebaseGlob('/&(a|b)/c')) + .toThrowError('Error in FirebaseGlob: "/&(a|b)/c" - unknown expansion type: "&" in "&(a|b)"'); + }); + + // Globs that contain params tested via the match tests below + }); + + describe('match', () => { + it('should match patterns with no parameters', () => { + testMatch('/abc/def/*', { + }, { + '/abc/def/': {}, + '/abc/def/ghi': {}, + '/': undefined, + '/abc': undefined, + '/abc/def/ghi/jk;': undefined, + }); + }); + + it('should capture a simple named param', () => { + testMatch('/:abc', { + named: ['abc'] + }, { + '/a': {abc: 'a'}, + '/abc': {abc: 'abc'}, + '/': undefined, + '/a/': undefined, + '/a/b/': undefined, + '/a/a/b': undefined, + '/a/a/b/': undefined, + }); + testMatch('/a/:b', { + named: ['b'] + }, { + '/a/b': {b: 'b'}, + '/a/bcd': {b: 'bcd'}, + '/a/': undefined, + '/a/b/': undefined, + '/a': undefined, + '/a//': undefined, + '/a/a/b': undefined, + '/a/a/b/': undefined, + }); + }); + + it('should capture a named param followed by non-word chars', () => { + testMatch('/a/:x-', { + named: ['x'] + }, { + '/a/b-': {x: 'b'}, + '/a/bcd-': {x: 'bcd'}, + '/a/--': {x: '-'}, + '/a': undefined, + '/a/-': undefined, + '/a/-/': undefined, + '/a/': undefined, + '/a/b/-': undefined, + '/a/b-c': undefined, + }); + }); + + it('should capture multiple named params', () => { + testMatch('/a/:b/:c', { + named: ['b', 'c'] + }, { + '/a/b/c': {b: 'b', c: 'c'}, + '/a/bcd/efg': {b: 'bcd', c: 'efg'}, + '/a/b/c-': {b: 'b', c: 'c-'}, + '/a/': undefined, + '/a/b/': undefined, + '/a/b/c/': undefined, + }); + testMatch('/:a/b/:c', { + named: ['a', 'c'] + }, { + '/a/b/c': {a: 'a', c: 'c'}, + '/abc/b/efg': {a: 'abc', c: 'efg'}, + '/a/b/c-': {a: 'a', c: 'c-'}, + '/a/': undefined, + '/a/b/': undefined, + '/a/b/c/': undefined, + }); + }); + + it('should capture a simple rest param', () => { + testMatch('/:abc*', { + rest: ['abc'] + }, { + '/a': {abc: 'a'}, + '/a/b': {abc: 'a/b'}, + '/a/bcd': {abc: 'a/bcd'}, + '/a/': {abc: 'a/'}, + '/a/b/': {abc: 'a/b/'}, + '/a//': {abc: 'a//'}, + '/a/b/c': {abc: 'a/b/c'}, + '/a/b/c/': {abc: 'a/b/c/'}, + }); + testMatch('/a/:b*', { + rest: ['b'] + }, { + '/a/b': {b: 'b'}, + '/a/bcd': {b: 'bcd'}, + '/a/': {b: ''}, + '/a/b/': {b: 'b/'}, + '/a': {b: undefined}, + '/a//': {b: '/'}, + '/a/a/b': {b: 'a/b'}, + '/a/a/b/': {b: 'a/b/'}, + }); + }); + + it('should capture a rest param mixed with a named param', () => { + testMatch('/:abc/:rest*', { + named: ['abc'], + rest: ['rest'] + }, { + '/a': {abc: 'a', rest: undefined}, + '/a/b': {abc: 'a', rest: 'b'}, + '/a/bcd': {abc: 'a', rest: 'bcd'}, + '/a/': {abc: 'a', rest: ''}, + '/a/b/': {abc: 'a', rest: 'b/'}, + '/a//': {abc: 'a', rest: '/'}, + '/a/b/c': {abc: 'a', rest: 'b/c'}, + '/a/b/c/': {abc: 'a', rest: 'b/c/'}, + }); + }); + }); +}); + +function testGlob(pattern: string, matches: string[], nonMatches: string[]) { + const glob = new FirebaseGlob(pattern); + matches.forEach(url => expect(glob.test(url)).toBe(true, url)); + nonMatches.forEach(url => expect(glob.test(url)).toBe(false, url)); +} + +function testMatch(pattern: string, captures: { named?: string[], rest?: string[] }, matches: { [url: string]: object|undefined }) { + const glob = new FirebaseGlob(pattern); + expect(Object.keys(glob.namedParams)).toEqual(captures.named || []); + expect(Object.keys(glob.restParams)).toEqual(captures.rest || []); + Object.keys(matches).forEach(url => expect(glob.match(url)).toEqual(matches[url])); +} diff --git a/aio/tools/firebase-test-utils/FirebaseGlob.ts b/aio/tools/firebase-test-utils/FirebaseGlob.ts new file mode 100644 index 0000000000..1e26d668ce --- /dev/null +++ b/aio/tools/firebase-test-utils/FirebaseGlob.ts @@ -0,0 +1,74 @@ +import * as XRegExp from 'xregexp'; + +const dot = /\./g; +const star = /\*/g; +const doubleStar = /(^|\/)\*\*($|\/)/g; // e.g. a/**/b or **/b or a/** but not a**b +const modifiedPatterns = /(.)\(([^)]+)\)/g; // e.g. `@(a|b) +const restParam = /\/:([A-Za-z]+)\*/g; // e.g. `:rest*` +const namedParam = /\/:([A-Za-z]+)/g; // e.g. `:api` +const possiblyEmptyInitialSegments = /^\.🐷\//g; // e.g. `**/a` can also match `a` +const possiblyEmptySegments = /\/\.🐷\//g; // e.g. `a/**/b` can also match `a/b` +const willBeStar = /🐷/g; // e.g. `a**b` not matched by previous rule + +export class FirebaseGlob { + pattern: string; + regex: XRegExp; + namedParams: { [key: string]: boolean } = {}; + restParams: { [key: string]: boolean } = {}; + constructor(glob: string) { + try { + const pattern = glob + .replace(dot, '\\.') + .replace(modifiedPatterns, replaceModifiedPattern) + .replace(restParam, (_, param) => { + // capture the rest of the string + this.restParams[param] = true; + return `(?:/(?<${param}>.🐷))?`; + }) + .replace(namedParam, (_, param) => { + // capture the named parameter + this.namedParams[param] = true; + return `/(?<${param}>[^/]+)`; + }) + .replace(doubleStar, '$1.🐷$2') // use the pig to avoid replacing ** in next rule + .replace(star, '[^/]*') // match a single segment + .replace(possiblyEmptyInitialSegments, '(?:.*)')// deal with **/ special cases + .replace(possiblyEmptySegments, '(?:/|/.*/)') // deal with /**/ special cases + .replace(willBeStar, '*'); // other ** matches + this.pattern = `^${pattern}$`; + this.regex = XRegExp(this.pattern); + } catch (e) { + throw new Error(`Error in FirebaseGlob: "${glob}" - ${e.message}`); + } + } + + test(url: string) { + return XRegExp.test(url, this.regex); + } + + match(url: string) { + const match = XRegExp.exec(url, this.regex); + if (match) { + const result = {}; + const names = (this.regex as any).xregexp.captureNames || []; + names.forEach(name => result[name] = match[name]); + return result; + } + } +} + +function replaceModifiedPattern(_, modifier, pattern) { + switch (modifier) { + case '!': + throw new Error(`"not" expansions are not supported: "${_}"`); + case '?': + case '+': + return `(${pattern})${modifier}`; + case '*': + return `(${pattern})🐷`; // it will become a star + case '@': + return `(${pattern})`; + default: + throw new Error(`unknown expansion type: "${modifier}" in "${_}"`); + } +} diff --git a/aio/tools/firebase-test-utils/FirebaseRedirect.spec.ts b/aio/tools/firebase-test-utils/FirebaseRedirect.spec.ts new file mode 100644 index 0000000000..db652cc1aa --- /dev/null +++ b/aio/tools/firebase-test-utils/FirebaseRedirect.spec.ts @@ -0,0 +1,30 @@ +import { FirebaseRedirect } from './FirebaseRedirect'; + +describe('FirebaseRedirect', () => { + describe('replace', () => { + it('should return undefined if the redirect does not match the url', () => { + const redirect = new FirebaseRedirect('/a/b/c', '/x/y/z'); + expect(redirect.replace('/1/2/3')).toBe(undefined); + }); + + it('should return the destination if there is a match', () => { + const redirect = new FirebaseRedirect('/a/b/c', '/x/y/z'); + expect(redirect.replace('/a/b/c')).toBe('/x/y/z'); + }); + + it('should inject name params into the destination', () => { + const redirect = new FirebaseRedirect('/api/:package/:api-*', '<:package><:api>'); + expect(redirect.replace('/api/common/NgClass-directive')).toEqual(''); + }); + + it('should inject rest params into the destination', () => { + const redirect = new FirebaseRedirect('/a/:rest*', '/x/:rest*/y'); + expect(redirect.replace('/a/b/c')).toEqual('/x/b/c/y'); + }); + + it('should inject both named and rest parameters into the destination', () => { + const redirect = new FirebaseRedirect('/:a/:rest*', '/x/:a/y/:rest*/z'); + expect(redirect.replace('/a/b/c')).toEqual('/x/a/y/b/c/z'); + }); + }); +}); diff --git a/aio/tools/firebase-test-utils/FirebaseRedirect.ts b/aio/tools/firebase-test-utils/FirebaseRedirect.ts new file mode 100644 index 0000000000..9dee3bd011 --- /dev/null +++ b/aio/tools/firebase-test-utils/FirebaseRedirect.ts @@ -0,0 +1,16 @@ +import * as XRegExp from 'xregexp'; +import { FirebaseGlob } from './FirebaseGlob'; + +export class FirebaseRedirect { + glob = new FirebaseGlob(this.source); + constructor(public source: string, public destination: string) {} + + replace(url: string) { + const match = this.glob.match(url); + if (match) { + const paramReplacers = Object.keys(this.glob.namedParams).map(name => [ XRegExp(`:${name}`, 'g'), match[name] ]); + const restReplacers = Object.keys(this.glob.restParams).map(name => [ XRegExp(`:${name}\\*`, 'g'), match[name] ]); + return XRegExp.replaceEach(this.destination, [...paramReplacers, ...restReplacers]); + } + } +} diff --git a/aio/tools/firebase-test-utils/FirebaseRedirector.spec.ts b/aio/tools/firebase-test-utils/FirebaseRedirector.spec.ts new file mode 100644 index 0000000000..1833e525f7 --- /dev/null +++ b/aio/tools/firebase-test-utils/FirebaseRedirector.spec.ts @@ -0,0 +1,42 @@ +import { FirebaseRedirector } from './FirebaseRedirector'; + +describe('FirebaseRedirector', () => { + it('should replace with the first matching redirect', () => { + const redirector = new FirebaseRedirector([ + { source: '/a/b/c', destination: '/X/Y/Z' }, + { source: '/a/:foo/c', destination: '/X/:foo/Z' }, + { source: '/**/:foo/c', destination: '/A/:foo/zzz' }, + ]); + expect(redirector.redirect('/a/b/c')).toEqual('/X/Y/Z'); + expect(redirector.redirect('/a/moo/c')).toEqual('/X/moo/Z'); + expect(redirector.redirect('/x/y/a/b/c')).toEqual('/A/b/zzz'); + expect(redirector.redirect('/x/y/c')).toEqual('/A/y/zzz'); + }); + + it('should return the original url if no redirect matches', () => { + const redirector = new FirebaseRedirector([ + { source: 'x', destination: 'X' }, + { source: 'y', destination: 'Y' }, + { source: 'z', destination: 'Z' }, + ]); + expect(redirector.redirect('a')).toEqual('a'); + }); + + it('should recursively redirect', () => { + const redirector = new FirebaseRedirector([ + { source: 'a', destination: 'b' }, + { source: 'b', destination: 'c' }, + { source: 'c', destination: 'd' }, + ]); + expect(redirector.redirect('a')).toEqual('d'); + }); + + it('should throw if stuck in an infinite loop', () => { + const redirector = new FirebaseRedirector([ + { source: 'a', destination: 'b' }, + { source: 'b', destination: 'c' }, + { source: 'c', destination: 'a' }, + ]); + expect(() => redirector.redirect('a')).toThrowError('infinite redirect loop'); + }); +}); diff --git a/aio/tools/firebase-test-utils/FirebaseRedirector.ts b/aio/tools/firebase-test-utils/FirebaseRedirector.ts new file mode 100644 index 0000000000..cd8a44e75a --- /dev/null +++ b/aio/tools/firebase-test-utils/FirebaseRedirector.ts @@ -0,0 +1,36 @@ +import { FirebaseRedirect } from './FirebaseRedirect'; + +export interface FirebaseRedirectConfig { + source: string; + destination: string; +} + +export class FirebaseRedirector { + private redirects: FirebaseRedirect[]; + constructor(redirects: FirebaseRedirectConfig[]) { + this.redirects = redirects.map(redirect => new FirebaseRedirect(redirect.source, redirect.destination)); + } + + redirect(url: string) { + let ttl = 50; + while (ttl > 0) { + const newUrl = this.doRedirect(url); + if (newUrl === url) { + return url; + } else { + url = newUrl; + ttl--; + } + } + throw new Error('infinite redirect loop'); + } + private doRedirect(url: string) { + for (let i = 0; i < this.redirects.length; i++) { + const newUrl = this.redirects[i].replace(url); + if (newUrl !== undefined) { + return newUrl; + } + } + return url; + } +} diff --git a/aio/tools/tslint.json b/aio/tools/tslint.json new file mode 100644 index 0000000000..dc43816374 --- /dev/null +++ b/aio/tools/tslint.json @@ -0,0 +1,88 @@ +{ + "rulesDirectory": [ + "../node_modules/codelyzer" + ], + "rules": { + "class-name": true, + "comment-format": [ + true, + "check-space" + ], + "curly": true, + "eofline": true, + "forin": true, + "indent": [ + true, + "spaces" + ], + "label-position": true, + "max-line-length": [ + true, + 140 + ], + "member-access": false, + "no-arg": true, + "no-bitwise": true, + "no-console": [ + true, + "debug", + "info", + "time", + "timeEnd", + "trace" + ], + "no-construct": true, + "no-debugger": true, + "no-duplicate-variable": true, + "no-empty": false, + "no-eval": true, + "no-inferrable-types": true, + "no-string-literal": false, + "no-switch-case-fall-through": true, + "no-trailing-whitespace": true, + "no-unused-expression": true, + "no-use-before-declare": true, + "no-var-keyword": true, + "object-literal-sort-keys": false, + "one-line": [ + true, + "check-open-brace", + "check-catch", + "check-else", + "check-whitespace" + ], + "quotemark": [ + true, + "single" + ], + "radix": true, + "semicolon": [ + true + ], + "triple-equals": [ + true, + "allow-null-check" + ], + "typedef-whitespace": [ + true, + { + "call-signature": "nospace", + "index-signature": "nospace", + "parameter": "nospace", + "property-declaration": "nospace", + "variable-declaration": "nospace" + } + ], + "variable-name": false, + "whitespace": [ + true, + "check-branch", + "check-decl", + "check-operator", + "check-separator", + "check-type" + ], + + "import-destructuring-spacing": true + } + } diff --git a/aio/yarn.lock b/aio/yarn.lock index 81be1ebf58..4acead46b1 100644 --- a/aio/yarn.lock +++ b/aio/yarn.lock @@ -1426,6 +1426,12 @@ cjson@^0.3.1: dependencies: json-parse-helpfulerror "^1.0.3" +cjson@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/cjson/-/cjson-0.5.0.tgz#a0f48601e016164dfb2c6d891e380c96cada9839" + dependencies: + json-parse-helpfulerror "^1.0.3" + clap@^1.0.9: version "1.2.3" resolved "https://registry.yarnpkg.com/clap/-/clap-1.2.3.tgz#4f36745b32008492557f46412d66d50cb99bce51" @@ -4635,6 +4641,15 @@ jasmine-spec-reporter@^4.1.0: dependencies: colors "1.1.2" +jasmine-ts@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/jasmine-ts/-/jasmine-ts-0.2.1.tgz#7fe5911d98ea22cc257efe98f9728ab66ac0e90e" + dependencies: + jasmine "^2.6.0" + ts-node "^3.2.0" + typescript "^2.4.1" + yargs "^8.0.2" + jasmine@^2.5.3, jasmine@^2.6.0: version "2.8.0" resolved "https://registry.yarnpkg.com/jasmine/-/jasmine-2.8.0.tgz#6b089c0a11576b1f16df11b80146d91d4e8b8a3e" @@ -8352,7 +8367,7 @@ tryit@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/tryit/-/tryit-1.0.3.tgz#393be730a9446fd1ead6da59a014308f36c289cb" -ts-node@^3.0.2, ts-node@^3.3.0: +ts-node@^3.0.2, ts-node@^3.2.0, ts-node@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-3.3.0.tgz#c13c6a3024e30be1180dd53038fc209289d4bf69" dependencies: @@ -8446,14 +8461,14 @@ typescript@2.4: version "2.4.2" resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.4.2.tgz#f8395f85d459276067c988aa41837a8f82870844" +typescript@^2.4.1, typescript@~2.6.1: + version "2.6.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.6.2.tgz#3c5b6fd7f6de0914269027f03c0946758f7673a4" + typescript@^2.5.3: version "2.5.3" resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.5.3.tgz#df3dcdc38f3beb800d4bc322646b04a3f6ca7f0d" -typescript@~2.6.1: - version "2.6.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.6.2.tgz#3c5b6fd7f6de0914269027f03c0946758f7673a4" - uglify-es@^3.3.4: version "3.3.5" resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.5.tgz#cf7e695da81999f85196b15e2978862f13212f88" @@ -9301,6 +9316,10 @@ xmlhttprequest-ssl@1.5.3: version "1.5.3" resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.3.tgz#185a888c04eca46c3e4070d99f7b49de3528992d" +xregexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-4.0.0.tgz#e698189de49dd2a18cc5687b05e17c8e43943020" + xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.0, xtend@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" diff --git a/scripts/ci/test-aio.sh b/scripts/ci/test-aio.sh index 033cdba4bc..4dc95fd7ff 100755 --- a/scripts/ci/test-aio.sh +++ b/scripts/ci/test-aio.sh @@ -37,6 +37,10 @@ source ${thisDir}/_travis-fold.sh yarn e2e-prod travisFoldEnd "test.aio.e2e" + # Test Firebase redirects + travisFoldStart "test.aio.deployment-config" + yarn deployment-config-test + travisFoldEnd "test.aio.deployment-config" # Run unit tests for aio/aio-builds-setup travisFoldStart "test.aio.aio-builds-setup"