build(aio): test Firebase hosting redirection configuration (#21763)

PR Close #21763
This commit is contained in:
Pete Bacon Darwin 2018-01-25 10:13:30 +00:00 committed by Alex Rickabaugh
parent 339ca83f9d
commit bf29936af9
13 changed files with 752 additions and 7 deletions

View File

@ -15,7 +15,7 @@
"build": "yarn ~~build", "build": "yarn ~~build",
"prebuild-local": "yarn setup-local", "prebuild-local": "yarn setup-local",
"build-local": "yarn ~~build", "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", "test": "yarn check-env && ng test",
"pree2e": "yarn check-env && yarn ~~update-webdriver", "pree2e": "yarn check-env && yarn ~~update-webdriver",
"e2e": "ng e2e --no-webdriver-update", "e2e": "ng e2e --no-webdriver-update",
@ -44,7 +44,10 @@
"docs-watch": "node tools/transforms/authors-package/watchr.js", "docs-watch": "node tools/transforms/authors-package/watchr.js",
"docs-lint": "eslint --ignore-path=\"tools/transforms/.eslintignore\" tools/transforms", "docs-lint": "eslint --ignore-path=\"tools/transforms/.eslintignore\" tools/transforms",
"docs-test": "node tools/transforms/test.js", "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", "preserve-and-sync": "yarn docs",
"serve-and-sync": "concurrently --kill-others \"yarn docs-watch --watch-only\" \"yarn start\"", "serve-and-sync": "concurrently --kill-others \"yarn docs-watch --watch-only\" \"yarn start\"",
"boilerplate:add": "node ./tools/examples/example-boilerplate add", "boilerplate:add": "node ./tools/examples/example-boilerplate add",
@ -98,6 +101,7 @@
"archiver": "^1.3.0", "archiver": "^1.3.0",
"canonical-path": "^0.0.2", "canonical-path": "^0.0.2",
"chalk": "^2.1.0", "chalk": "^2.1.0",
"cjson": "^0.5.0",
"codelyzer": "~2.0.0", "codelyzer": "~2.0.0",
"concurrently": "^3.4.0", "concurrently": "^3.4.0",
"cross-spawn": "^5.1.0", "cross-spawn": "^5.1.0",
@ -118,6 +122,7 @@
"image-size": "^0.5.1", "image-size": "^0.5.1",
"jasmine-core": "^2.8.0", "jasmine-core": "^2.8.0",
"jasmine-spec-reporter": "^4.1.0", "jasmine-spec-reporter": "^4.1.0",
"jasmine-ts": "^0.2.1",
"jsdom": "^9.12.0", "jsdom": "^9.12.0",
"karma": "^1.7.0", "karma": "^1.7.0",
"karma-chrome-launcher": "^2.1.1", "karma-chrome-launcher": "^2.1.1",
@ -147,6 +152,7 @@
"unist-util-visit-parents": "^1.1.1", "unist-util-visit-parents": "^1.1.1",
"vrsource-tslint-rules": "^4.0.1", "vrsource-tslint-rules": "^4.0.1",
"watchr": "^3.0.1", "watchr": "^3.0.1",
"xregexp": "^4.0.0",
"yargs": "^7.0.2" "yargs": "^7.0.2"
} }
} }

View File

@ -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

View File

@ -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>([^<]+)<\/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;
}

View File

@ -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?:\/)?\/.*/);
});
});
});
});
});

View File

@ -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]));
}

View File

@ -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 "${_}"`);
}
}

View File

@ -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('<common><NgClass>');
});
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');
});
});
});

View File

@ -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]);
}
}
}

View File

@ -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');
});
});

View File

@ -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;
}
}

88
aio/tools/tslint.json Normal file
View File

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

View File

@ -1426,6 +1426,12 @@ cjson@^0.3.1:
dependencies: dependencies:
json-parse-helpfulerror "^1.0.3" 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: clap@^1.0.9:
version "1.2.3" version "1.2.3"
resolved "https://registry.yarnpkg.com/clap/-/clap-1.2.3.tgz#4f36745b32008492557f46412d66d50cb99bce51" resolved "https://registry.yarnpkg.com/clap/-/clap-1.2.3.tgz#4f36745b32008492557f46412d66d50cb99bce51"
@ -4635,6 +4641,15 @@ jasmine-spec-reporter@^4.1.0:
dependencies: dependencies:
colors "1.1.2" 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: jasmine@^2.5.3, jasmine@^2.6.0:
version "2.8.0" version "2.8.0"
resolved "https://registry.yarnpkg.com/jasmine/-/jasmine-2.8.0.tgz#6b089c0a11576b1f16df11b80146d91d4e8b8a3e" resolved "https://registry.yarnpkg.com/jasmine/-/jasmine-2.8.0.tgz#6b089c0a11576b1f16df11b80146d91d4e8b8a3e"
@ -8352,7 +8367,7 @@ tryit@^1.0.1:
version "1.0.3" version "1.0.3"
resolved "https://registry.yarnpkg.com/tryit/-/tryit-1.0.3.tgz#393be730a9446fd1ead6da59a014308f36c289cb" 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" version "3.3.0"
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-3.3.0.tgz#c13c6a3024e30be1180dd53038fc209289d4bf69" resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-3.3.0.tgz#c13c6a3024e30be1180dd53038fc209289d4bf69"
dependencies: dependencies:
@ -8446,14 +8461,14 @@ typescript@2.4:
version "2.4.2" version "2.4.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.4.2.tgz#f8395f85d459276067c988aa41837a8f82870844" 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: typescript@^2.5.3:
version "2.5.3" version "2.5.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.5.3.tgz#df3dcdc38f3beb800d4bc322646b04a3f6ca7f0d" 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: uglify-es@^3.3.4:
version "3.3.5" version "3.3.5"
resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.5.tgz#cf7e695da81999f85196b15e2978862f13212f88" 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" version "1.5.3"
resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.3.tgz#185a888c04eca46c3e4070d99f7b49de3528992d" 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: xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.0, xtend@~4.0.1:
version "4.0.1" version "4.0.1"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"

View File

@ -37,6 +37,10 @@ source ${thisDir}/_travis-fold.sh
yarn e2e-prod yarn e2e-prod
travisFoldEnd "test.aio.e2e" 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 # Run unit tests for aio/aio-builds-setup
travisFoldStart "test.aio.aio-builds-setup" travisFoldStart "test.aio.aio-builds-setup"