├── hello-mobile ├── public │ └── .npmignore ├── src │ ├── app │ │ ├── shared │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── environment.ts │ │ ├── hello-mobile.component.spec.ts │ │ └── hello-mobile.component.ts │ ├── typings.d.ts │ ├── system-import.js │ ├── favicon.ico │ ├── icons │ │ ├── icon.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon-96x96.png │ │ ├── mstile-70x70.png │ │ ├── mstile-144x144.png │ │ ├── mstile-150x150.png │ │ ├── mstile-310x150.png │ │ ├── mstile-310x310.png │ │ ├── apple-touch-icon.png │ │ ├── android-chrome-36x36.png │ │ ├── android-chrome-48x48.png │ │ ├── android-chrome-72x72.png │ │ ├── android-chrome-96x96.png │ │ ├── android-chrome-144x144.png │ │ ├── android-chrome-192x192.png │ │ ├── apple-touch-icon-57x57.png │ │ ├── apple-touch-icon-60x60.png │ │ ├── apple-touch-icon-72x72.png │ │ ├── apple-touch-icon-76x76.png │ │ ├── apple-touch-icon-114x114.png │ │ ├── apple-touch-icon-120x120.png │ │ ├── apple-touch-icon-144x144.png │ │ ├── apple-touch-icon-152x152.png │ │ ├── apple-touch-icon-180x180.png │ │ ├── apple-touch-icon-precomposed.png │ │ └── safari-pinned-tab.svg │ ├── main.ts │ ├── main-app-shell.ts │ ├── manifest.webapp │ ├── index.html │ └── system-config.ts ├── e2e │ ├── typings.d.ts │ ├── app.po.ts │ ├── app.e2e.ts │ └── tsconfig.json ├── config │ ├── environment.dev.ts │ ├── environment.prod.ts │ ├── environment.js │ ├── protractor.conf.js │ ├── karma-test-shim.js │ └── karma.conf.js ├── .clang-format ├── README.md ├── .editorconfig ├── typings.json ├── .gitignore ├── angular-cli-build.js ├── angular-cli.json ├── package.json └── tslint.json ├── app-shell ├── src │ ├── index.ts │ ├── experimental │ │ └── shell-parser │ │ │ ├── ast │ │ │ ├── index.ts │ │ │ └── ast-node.ts │ │ │ ├── node-matcher │ │ │ ├── css-selector │ │ │ │ ├── index.ts │ │ │ │ ├── css-selector.ts │ │ │ │ └── css-selector.spec.ts │ │ │ ├── index.ts │ │ │ ├── node-matcher.ts │ │ │ ├── css-node-matcher.ts │ │ │ └── css-node-matcher.spec.ts │ │ │ ├── template-parser │ │ │ ├── index.ts │ │ │ ├── template-parser.ts │ │ │ └── parse5 │ │ │ │ ├── parse5-template-parser.ts │ │ │ │ ├── tokenizer-case-sensitivity-patch.spec.ts │ │ │ │ ├── drop-named-entities-patch.spec.ts │ │ │ │ ├── tokenizer-case-sensitivity-patch.ts │ │ │ │ ├── parse5-template-parser.spec.ts │ │ │ │ └── drop-named-entities-patch.ts │ │ │ ├── testing │ │ │ ├── index.ts │ │ │ ├── context-mock.ts │ │ │ ├── mock-requests.ts │ │ │ └── mock-caches.ts │ │ │ ├── node-visitor │ │ │ ├── index.ts │ │ │ ├── resource-inline │ │ │ │ ├── index.ts │ │ │ │ ├── stylesheet-resource-inline-visitor.ts │ │ │ │ ├── inline-style-resource-inline-visitor.ts │ │ │ │ ├── resource-inline-visitor.ts │ │ │ │ ├── inline-style-resource-inline-visitor.spec.ts │ │ │ │ └── stylesheet-resource-inline-visitor.spec.ts │ │ │ ├── node-visitor.ts │ │ │ ├── template-strip-visitor.ts │ │ │ └── template-strip-visitor.spec.ts │ │ │ ├── index.ts │ │ │ ├── context.ts │ │ │ ├── config.ts │ │ │ ├── shell-parser-factory.ts │ │ │ └── shell-parser.ts │ ├── app │ │ ├── index.ts │ │ ├── prerender.ts │ │ ├── prerender.spec.ts │ │ ├── module.ts │ │ ├── shell.ts │ │ └── shell.spec.ts │ └── unit_tests.ts ├── .clang-format ├── .vscode │ └── settings.json ├── .editorconfig ├── tsconfig.json ├── .gitignore ├── tsconfig.es5.json ├── tsconfig.esm.json ├── package.json ├── tslint.json ├── README.md └── gulpfile.ts ├── service-worker ├── worker │ ├── src │ │ ├── index.ts │ │ ├── test │ │ │ ├── e2e │ │ │ │ ├── harness │ │ │ │ │ ├── client │ │ │ │ │ │ ├── debug │ │ │ │ │ │ │ ├── hello.txt │ │ │ │ │ │ │ ├── full.txt │ │ │ │ │ │ │ ├── ngsw-manifest.json.js │ │ │ │ │ │ │ └── ngsw-manifest.json │ │ │ │ │ │ ├── manifest.webapp │ │ │ │ │ │ ├── main.ts │ │ │ │ │ │ └── index.html │ │ │ │ │ └── server │ │ │ │ │ │ ├── push.ts │ │ │ │ │ │ ├── server.ts │ │ │ │ │ │ └── page-object.ts │ │ │ │ └── spec │ │ │ │ │ └── protractor.config.js │ │ │ └── webpack │ │ │ │ ├── ignored.js │ │ │ │ ├── other.js │ │ │ │ ├── index.js │ │ │ │ └── ngsw-manifest.json │ │ ├── build │ │ │ ├── index.ts │ │ │ ├── assets │ │ │ │ ├── register-basic.js │ │ │ │ └── register-basic.min.js │ │ │ ├── gulp.ts │ │ │ └── webpack.ts │ │ ├── testing │ │ │ ├── index.ts │ │ │ └── mock.spec.ts │ │ ├── webpack.ts │ │ ├── companion │ │ │ ├── index.ts │ │ │ └── module.ts │ │ ├── typings │ │ │ ├── jshashes.d.ts │ │ │ └── service-worker.d.ts │ │ ├── worker │ │ │ ├── facade │ │ │ │ ├── index.ts │ │ │ │ ├── adapter.ts │ │ │ │ ├── events.ts │ │ │ │ ├── cache.ts │ │ │ │ └── fetch.ts │ │ │ ├── index.ts │ │ │ ├── manifest.ts │ │ │ ├── builds │ │ │ │ ├── basic.ts │ │ │ │ └── test.ts │ │ │ ├── url.ts │ │ │ ├── cache.ts │ │ │ ├── api.ts │ │ │ ├── bootstrap.ts │ │ │ ├── logging.ts │ │ │ ├── common.ts │ │ │ └── worker.ts │ │ └── plugins │ │ │ ├── dynamic │ │ │ ├── index.ts │ │ │ ├── manifest.ts │ │ │ ├── strategy │ │ │ │ ├── performance.ts │ │ │ │ └── freshness.ts │ │ │ ├── linked.spec.ts │ │ │ └── dynamic.ts │ │ │ ├── external │ │ │ └── index.ts │ │ │ ├── push │ │ │ └── index.ts │ │ │ ├── routes │ │ │ └── index.ts │ │ │ └── static │ │ │ └── index.ts │ ├── .gitignore │ ├── .npmignore │ ├── .vscode │ │ └── settings.json │ ├── tsconfig.json │ ├── tsconfig.esm.json │ ├── tsconfig.es5.json │ └── package.json └── example │ ├── typings.json │ ├── webroot │ ├── manifest.appcache │ └── index.html │ ├── gulpfile.js │ ├── tsconfig.json │ ├── src │ └── index.html │ ├── package.json │ └── gulpfile.ts ├── test.sh ├── guides ├── img │ ├── app-shell-spinner.png │ └── app-shell-pre-bootstrap.png ├── cli-setup.md ├── service-worker.md └── web-app-manifest.md ├── install.sh ├── .travis.yml ├── README.md ├── .gitignore ├── LICENSE └── ROADMAP.md /hello-mobile/public/.npmignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hello-mobile/src/app/shared/index.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app-shell/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './app'; 2 | -------------------------------------------------------------------------------- /service-worker/worker/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './companion'; -------------------------------------------------------------------------------- /service-worker/worker/src/test/e2e/harness/client/debug/hello.txt: -------------------------------------------------------------------------------- 1 | Hello world! -------------------------------------------------------------------------------- /hello-mobile/e2e/typings.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /service-worker/worker/src/test/e2e/harness/client/debug/full.txt: -------------------------------------------------------------------------------- 1 | This is full text. -------------------------------------------------------------------------------- /hello-mobile/src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | -------------------------------------------------------------------------------- /service-worker/worker/src/test/webpack/ignored.js: -------------------------------------------------------------------------------- 1 | console.log('i should not make it'); -------------------------------------------------------------------------------- /app-shell/src/experimental/shell-parser/ast/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ast-node'; 2 | 3 | -------------------------------------------------------------------------------- /service-worker/worker/.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | /typings 4 | /ngsw-config.json 5 | -------------------------------------------------------------------------------- /hello-mobile/config/environment.dev.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: false 3 | }; 4 | -------------------------------------------------------------------------------- /hello-mobile/config/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /hello-mobile/src/system-import.js: -------------------------------------------------------------------------------- 1 | System.import('main') 2 | .catch(console.error.bind(console)); 3 | -------------------------------------------------------------------------------- /service-worker/worker/src/build/index.ts: -------------------------------------------------------------------------------- 1 | export * from './gulp'; 2 | export * from './webpack'; 3 | -------------------------------------------------------------------------------- /service-worker/worker/src/test/e2e/harness/client/debug/ngsw-manifest.json.js: -------------------------------------------------------------------------------- 1 | /* needed for SW to run */ -------------------------------------------------------------------------------- /service-worker/worker/src/testing/index.ts: -------------------------------------------------------------------------------- 1 | export * from './mock'; 2 | export * from './mock_cache'; -------------------------------------------------------------------------------- /app-shell/.clang-format: -------------------------------------------------------------------------------- 1 | Language: JavaScript 2 | BasedOnStyle: Google 3 | ColumnLimit: 100 4 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd ./service-worker/worker 3 | gulp test 4 | cd ../../app-shell 5 | gulp test 6 | -------------------------------------------------------------------------------- /hello-mobile/.clang-format: -------------------------------------------------------------------------------- 1 | Language: JavaScript 2 | BasedOnStyle: Google 3 | ColumnLimit: 100 4 | -------------------------------------------------------------------------------- /service-worker/worker/.npmignore: -------------------------------------------------------------------------------- 1 | build/ 2 | node_modules/ 3 | typings/ 4 | dist/src/ 5 | /ngsw-config.json 6 | -------------------------------------------------------------------------------- /service-worker/worker/src/test/webpack/other.js: -------------------------------------------------------------------------------- 1 | // This is another file. 2 | console.log('this is another file'); -------------------------------------------------------------------------------- /app-shell/src/app/index.ts: -------------------------------------------------------------------------------- 1 | export * from './module'; 2 | export * from './prerender'; 3 | export * from './shell'; 4 | -------------------------------------------------------------------------------- /app-shell/src/experimental/shell-parser/node-matcher/css-selector/index.ts: -------------------------------------------------------------------------------- 1 | export * from './css-selector'; 2 | 3 | -------------------------------------------------------------------------------- /hello-mobile/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/mobile-toolkit/HEAD/hello-mobile/src/favicon.ico -------------------------------------------------------------------------------- /guides/img/app-shell-spinner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/mobile-toolkit/HEAD/guides/img/app-shell-spinner.png -------------------------------------------------------------------------------- /hello-mobile/src/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/mobile-toolkit/HEAD/hello-mobile/src/icons/icon.png -------------------------------------------------------------------------------- /service-worker/worker/src/test/webpack/index.js: -------------------------------------------------------------------------------- 1 | // This is a test. 2 | require('./other'); 3 | 4 | console.log('testing'); -------------------------------------------------------------------------------- /guides/img/app-shell-pre-bootstrap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/mobile-toolkit/HEAD/guides/img/app-shell-pre-bootstrap.png -------------------------------------------------------------------------------- /hello-mobile/src/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/mobile-toolkit/HEAD/hello-mobile/src/icons/favicon-16x16.png -------------------------------------------------------------------------------- /hello-mobile/src/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/mobile-toolkit/HEAD/hello-mobile/src/icons/favicon-32x32.png -------------------------------------------------------------------------------- /hello-mobile/src/icons/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/mobile-toolkit/HEAD/hello-mobile/src/icons/favicon-96x96.png -------------------------------------------------------------------------------- /hello-mobile/src/icons/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/mobile-toolkit/HEAD/hello-mobile/src/icons/mstile-70x70.png -------------------------------------------------------------------------------- /hello-mobile/src/icons/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/mobile-toolkit/HEAD/hello-mobile/src/icons/mstile-144x144.png -------------------------------------------------------------------------------- /hello-mobile/src/icons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/mobile-toolkit/HEAD/hello-mobile/src/icons/mstile-150x150.png -------------------------------------------------------------------------------- /hello-mobile/src/icons/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/mobile-toolkit/HEAD/hello-mobile/src/icons/mstile-310x150.png -------------------------------------------------------------------------------- /hello-mobile/src/icons/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/mobile-toolkit/HEAD/hello-mobile/src/icons/mstile-310x310.png -------------------------------------------------------------------------------- /service-worker/worker/src/test/webpack/ngsw-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "other": true, 3 | "static.ignore": [ 4 | "^\/test\/ig.*$" 5 | ] 6 | } -------------------------------------------------------------------------------- /service-worker/worker/src/webpack.ts: -------------------------------------------------------------------------------- 1 | import {AngularServiceWorkerPlugin} from './build'; 2 | export default AngularServiceWorkerPlugin; 3 | -------------------------------------------------------------------------------- /app-shell/src/app/prerender.ts: -------------------------------------------------------------------------------- 1 | import {OpaqueToken} from '@angular/core'; 2 | 3 | export const IS_PRERENDER = new OpaqueToken('IsPrerender'); 4 | -------------------------------------------------------------------------------- /hello-mobile/src/app/index.ts: -------------------------------------------------------------------------------- 1 | export {environment} from './environment'; 2 | export {HelloMobileAppComponent} from './hello-mobile.component'; 3 | -------------------------------------------------------------------------------- /hello-mobile/src/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/mobile-toolkit/HEAD/hello-mobile/src/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /service-worker/example/typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@angular/service-worker-example", 3 | "version": false, 4 | "dependencies": {} 5 | } 6 | -------------------------------------------------------------------------------- /hello-mobile/src/icons/android-chrome-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/mobile-toolkit/HEAD/hello-mobile/src/icons/android-chrome-36x36.png -------------------------------------------------------------------------------- /hello-mobile/src/icons/android-chrome-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/mobile-toolkit/HEAD/hello-mobile/src/icons/android-chrome-48x48.png -------------------------------------------------------------------------------- /hello-mobile/src/icons/android-chrome-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/mobile-toolkit/HEAD/hello-mobile/src/icons/android-chrome-72x72.png -------------------------------------------------------------------------------- /hello-mobile/src/icons/android-chrome-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/mobile-toolkit/HEAD/hello-mobile/src/icons/android-chrome-96x96.png -------------------------------------------------------------------------------- /hello-mobile/src/icons/android-chrome-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/mobile-toolkit/HEAD/hello-mobile/src/icons/android-chrome-144x144.png -------------------------------------------------------------------------------- /hello-mobile/src/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/mobile-toolkit/HEAD/hello-mobile/src/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /hello-mobile/src/icons/apple-touch-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/mobile-toolkit/HEAD/hello-mobile/src/icons/apple-touch-icon-57x57.png -------------------------------------------------------------------------------- /hello-mobile/src/icons/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/mobile-toolkit/HEAD/hello-mobile/src/icons/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /hello-mobile/src/icons/apple-touch-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/mobile-toolkit/HEAD/hello-mobile/src/icons/apple-touch-icon-72x72.png -------------------------------------------------------------------------------- /hello-mobile/src/icons/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/mobile-toolkit/HEAD/hello-mobile/src/icons/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /service-worker/worker/src/companion/index.ts: -------------------------------------------------------------------------------- 1 | export {NgServiceWorker, NgPushRegistration} from './comm'; 2 | export {ServiceWorkerModule} from './module'; 3 | -------------------------------------------------------------------------------- /service-worker/worker/src/typings/jshashes.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'jshashes' { 2 | export class SHA1 { 3 | hex(data: string): string; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /app-shell/src/experimental/shell-parser/template-parser/index.ts: -------------------------------------------------------------------------------- 1 | export * from './template-parser'; 2 | export * from './parse5/parse5-template-parser'; 3 | 4 | -------------------------------------------------------------------------------- /hello-mobile/src/icons/apple-touch-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/mobile-toolkit/HEAD/hello-mobile/src/icons/apple-touch-icon-114x114.png -------------------------------------------------------------------------------- /hello-mobile/src/icons/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/mobile-toolkit/HEAD/hello-mobile/src/icons/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /hello-mobile/src/icons/apple-touch-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/mobile-toolkit/HEAD/hello-mobile/src/icons/apple-touch-icon-144x144.png -------------------------------------------------------------------------------- /hello-mobile/src/icons/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/mobile-toolkit/HEAD/hello-mobile/src/icons/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /hello-mobile/src/icons/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/mobile-toolkit/HEAD/hello-mobile/src/icons/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /service-worker/worker/src/worker/facade/index.ts: -------------------------------------------------------------------------------- 1 | export * from './adapter'; 2 | export * from './cache'; 3 | export * from './events'; 4 | export * from './fetch'; -------------------------------------------------------------------------------- /hello-mobile/src/icons/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/mobile-toolkit/HEAD/hello-mobile/src/icons/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /app-shell/src/experimental/shell-parser/testing/index.ts: -------------------------------------------------------------------------------- 1 | export * from './context-mock'; 2 | export * from './mock-requests'; 3 | export * from './mock-caches'; 4 | 5 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | npm config set registry http://registry.npmjs.org/ 3 | cd ./service-worker/worker 4 | npm install 5 | cd ../../app-shell 6 | npm install 7 | 8 | -------------------------------------------------------------------------------- /app-shell/src/experimental/shell-parser/node-matcher/index.ts: -------------------------------------------------------------------------------- 1 | export * from './css-selector'; 2 | export * from './node-matcher'; 3 | export * from './css-node-matcher'; 4 | 5 | -------------------------------------------------------------------------------- /app-shell/src/experimental/shell-parser/node-visitor/index.ts: -------------------------------------------------------------------------------- 1 | export * from './node-visitor'; 2 | export * from './resource-inline'; 3 | export * from './template-strip-visitor'; 4 | 5 | -------------------------------------------------------------------------------- /hello-mobile/README.md: -------------------------------------------------------------------------------- 1 | This directory is an example app created by [Angular CLI](https://cli.angular.io) 2 | to correspond with the [Progressive Web App guides](../guides/cli-setup.md). 3 | -------------------------------------------------------------------------------- /service-worker/example/webroot/manifest.appcache: -------------------------------------------------------------------------------- 1 | CACHE MANIFEST 2 | CACHE: 3 | # sw.group: html 4 | # sw.hash: f0c9884014ec6ff4496d44810713969c2f1a9710 5 | /index.html 6 | NETWORK: 7 | * 8 | -------------------------------------------------------------------------------- /app-shell/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "**/.git": true, 5 | "**/.DS_Store": true 6 | } 7 | } -------------------------------------------------------------------------------- /app-shell/src/experimental/shell-parser/node-matcher/node-matcher.ts: -------------------------------------------------------------------------------- 1 | import {ASTNode} from '../ast'; 2 | 3 | export abstract class NodeMatcher { 4 | abstract match(node: ASTNode): boolean; 5 | } 6 | 7 | -------------------------------------------------------------------------------- /service-worker/example/gulpfile.js: -------------------------------------------------------------------------------- 1 | // Set up ts-node to enable loading of TypeScript files. 2 | require('ts-node').register({ 3 | noProject: true, 4 | }); 5 | 6 | // Trampoline into gulpfile.ts. 7 | require('./gulpfile.ts'); -------------------------------------------------------------------------------- /service-worker/worker/src/plugins/dynamic/index.ts: -------------------------------------------------------------------------------- 1 | export {Dynamic, DynamicImpl} from './dynamic'; 2 | export {FreshnessStrategy} from './strategy/freshness'; 3 | export {PerformanceStrategy} from './strategy/performance'; 4 | -------------------------------------------------------------------------------- /service-worker/worker/src/test/e2e/harness/client/manifest.webapp: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Angular Service Worker Test Harness", 3 | "short_name": "ngsw harness", 4 | "start_url": "/index.html", 5 | "display": "standalone" 6 | } -------------------------------------------------------------------------------- /service-worker/worker/src/companion/module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {NgServiceWorker} from './comm'; 3 | 4 | @NgModule({ 5 | providers: [NgServiceWorker], 6 | }) 7 | export class ServiceWorkerModule {} 8 | -------------------------------------------------------------------------------- /app-shell/src/experimental/shell-parser/node-visitor/resource-inline/index.ts: -------------------------------------------------------------------------------- 1 | export * from './resource-inline-visitor'; 2 | export * from './stylesheet-resource-inline-visitor'; 3 | export * from './inline-style-resource-inline-visitor'; 4 | 5 | -------------------------------------------------------------------------------- /hello-mobile/config/environment.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 3 | module.exports = function(environment) { 4 | return { 5 | environment: environment, 6 | baseURL: '/', 7 | locationType: 'auto' 8 | }; 9 | }; 10 | 11 | -------------------------------------------------------------------------------- /hello-mobile/e2e/app.po.ts: -------------------------------------------------------------------------------- 1 | export class HelloMobilePage { 2 | navigateTo() { 3 | return browser.get('/'); 4 | } 5 | 6 | getParagraphText() { 7 | return element(by.css('hello-mobile-app h1')).getText(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /app-shell/src/experimental/shell-parser/template-parser/template-parser.ts: -------------------------------------------------------------------------------- 1 | import {ASTNode} from '../ast'; 2 | 3 | export abstract class TemplateParser { 4 | abstract parse(template: string): ASTNode; 5 | abstract serialize(node: ASTNode): string; 6 | } 7 | 8 | -------------------------------------------------------------------------------- /service-worker/worker/src/build/assets/register-basic.js: -------------------------------------------------------------------------------- 1 | if ('serviceWorker' in navigator) { 2 | navigator 3 | .serviceWorker 4 | .register('worker-basic.js') 5 | .catch(function(err) { 6 | console.error('Error registering service worker:', err); 7 | }); 8 | } 9 | -------------------------------------------------------------------------------- /service-worker/worker/src/test/e2e/harness/client/debug/ngsw-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "static": { 3 | "urls": { 4 | "/hello.txt": "test" 5 | } 6 | }, 7 | "external": { 8 | "urls": [ 9 | {"url": "http://localhost:8080/full.txt"} 10 | ] 11 | } 12 | } -------------------------------------------------------------------------------- /service-worker/worker/src/build/assets/register-basic.min.js: -------------------------------------------------------------------------------- 1 | if ('serviceWorker' in navigator) { 2 | navigator 3 | .serviceWorker 4 | .register('worker-basic.min.js') 5 | .catch(function(err) { 6 | console.error('Error registering service worker:', err); 7 | }); 8 | } 9 | -------------------------------------------------------------------------------- /hello-mobile/src/app/environment.ts: -------------------------------------------------------------------------------- 1 | // The file for the current environment will overwrite this one during build 2 | // Different environments can be found in config/environment.{dev|prod}.ts 3 | // The build system defaults to the dev environment 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | -------------------------------------------------------------------------------- /app-shell/.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | max_line_length = 0 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /app-shell/src/experimental/shell-parser/ast/ast-node.ts: -------------------------------------------------------------------------------- 1 | export interface ASTAttribute { 2 | name: string; 3 | value: string; 4 | } 5 | 6 | export interface ASTNode { 7 | attrs: ASTAttribute[]; 8 | childNodes?: ASTNode[]; 9 | parentNode?: ASTNode; 10 | nodeName: string; 11 | value?: string; 12 | } 13 | 14 | -------------------------------------------------------------------------------- /hello-mobile/.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | max_line_length = 0 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /service-worker/worker/src/worker/index.ts: -------------------------------------------------------------------------------- 1 | export * from './api'; 2 | export * from './bootstrap'; 3 | export * from './cache'; 4 | export * from './common'; 5 | export * from './driver'; 6 | export * from './facade'; 7 | export * from './manifest'; 8 | export * from './logging'; 9 | export * from './url'; 10 | export * from './worker'; 11 | -------------------------------------------------------------------------------- /app-shell/src/experimental/shell-parser/index.ts: -------------------------------------------------------------------------------- 1 | import {ShellParser} from './shell-parser'; 2 | import {shellParserFactory} from './shell-parser-factory'; 3 | import {RouteDefinition, ShellParserConfig} from './config'; 4 | 5 | export { 6 | shellParserFactory, 7 | ShellParser, 8 | RouteDefinition, 9 | ShellParserConfig 10 | }; 11 | 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: "4.2.1" 3 | 4 | before_install: 5 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then export CHROME_BIN=chromium-browser; fi # Karma CI 6 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then export DISPLAY=:99.0; fi 7 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sh -e /etc/init.d/xvfb start; fi 8 | 9 | install: ./install.sh 10 | script: ./test.sh 11 | 12 | -------------------------------------------------------------------------------- /hello-mobile/e2e/app.e2e.ts: -------------------------------------------------------------------------------- 1 | import { HelloMobilePage } from './app.po'; 2 | 3 | describe('hello-mobile App', function() { 4 | let page: HelloMobilePage; 5 | 6 | beforeEach(() => { 7 | page = new HelloMobilePage(); 8 | }); 9 | 10 | it('should display message saying app works', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('hello-mobile works!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /service-worker/worker/src/worker/manifest.ts: -------------------------------------------------------------------------------- 1 | import {SHA1} from 'jshashes'; 2 | 3 | export interface Manifest { 4 | [key: string]: any; 5 | _hash: string; 6 | _json: string; 7 | } 8 | 9 | export function parseManifest(data: string): Manifest { 10 | const manifest: Manifest = JSON.parse(data) as Manifest; 11 | manifest._json = data; 12 | manifest._hash = new SHA1().hex(data); 13 | return manifest; 14 | } 15 | -------------------------------------------------------------------------------- /hello-mobile/src/main.ts: -------------------------------------------------------------------------------- 1 | import { bootstrap } from '@angular/platform-browser-dynamic'; 2 | import { enableProdMode } from '@angular/core'; 3 | import { HelloMobileAppComponent, environment } from './app/'; 4 | import { APP_SHELL_RUNTIME_PROVIDERS } from '@angular/app-shell'; 5 | 6 | if (environment.production) { 7 | enableProdMode(); 8 | } 9 | 10 | bootstrap(HelloMobileAppComponent, [ APP_SHELL_RUNTIME_PROVIDERS ]); 11 | 12 | -------------------------------------------------------------------------------- /service-worker/worker/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "typescript.tsdk": "node_modules/typescript/lib", 4 | "editor.insertSpaces": true, 5 | "editor.tabSize": 2, 6 | "files.exclude": { 7 | "**/.git": true, 8 | "**/.svn": true, 9 | "**/.hg": true, 10 | "**/.DS_Store": true, 11 | "tmp": true, 12 | "node_modules": true 13 | } 14 | } -------------------------------------------------------------------------------- /service-worker/worker/src/testing/mock.spec.ts: -------------------------------------------------------------------------------- 1 | import {TestWorkerDriver} from './mock'; 2 | 3 | class TestWorker {} 4 | 5 | describe('TestWorkerDriver (mock)', () => { 6 | let driver: TestWorkerDriver; 7 | beforeEach(() => { 8 | driver = new TestWorkerDriver(() => new TestWorker); 9 | }); 10 | it('properly passes install events through', (done) => { 11 | driver.triggerInstall().then(() => done()); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /app-shell/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "noImplicitAny": false, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "declaration": true, 9 | "outDir": "dist", 10 | "sourceMap": false, 11 | "moduleResolution": "node", 12 | "rootDir": ".", 13 | "baseUrl": "." 14 | }, 15 | "exclude": [ 16 | "node_modules" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /service-worker/example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "system", 4 | "target": "es5", 5 | "noImplicitAny": false, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "outDir": "dist", 9 | "sourceMap": false, 10 | "moduleResolution": "node", 11 | "rootDir": "." 12 | }, 13 | "exclude": [ 14 | "node_modules", 15 | "typings/main.d.ts", 16 | "typings/main" 17 | ] 18 | } -------------------------------------------------------------------------------- /hello-mobile/e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "declaration": false, 5 | "emitDecoratorMetadata": true, 6 | "experimentalDecorators": true, 7 | "mapRoot": "", 8 | "module": "commonjs", 9 | "moduleResolution": "node", 10 | "noEmitOnError": true, 11 | "noImplicitAny": false, 12 | "rootDir": ".", 13 | "sourceMap": true, 14 | "sourceRoot": "/", 15 | "target": "es5" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /hello-mobile/typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "globalDevDependencies": { 3 | "angular-protractor": "registry:dt/angular-protractor#1.5.0+20160425143459", 4 | "jasmine": "registry:dt/jasmine#2.2.0+20160412134438", 5 | "selenium-webdriver": "registry:dt/selenium-webdriver#2.44.0+20160317120654" 6 | }, 7 | "globalDependencies": { 8 | "es6-shim": "registry:dt/es6-shim#0.31.2+20160317120654", 9 | "node": "registry:dt/node#4.0.0+20160509154515" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app-shell/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | 7 | # dependencies 8 | /node_modules 9 | /bower_components 10 | 11 | # IDEs and editors 12 | /.idea 13 | 14 | # misc 15 | /.sass-cache 16 | /connect.lock 17 | /coverage/* 18 | /libpeerconnection.log 19 | npm-debug.log 20 | testem.log 21 | /typings 22 | 23 | # e2e 24 | /e2e/*.js 25 | /e2e/*.map 26 | 27 | #System Files 28 | .DS_Store 29 | -------------------------------------------------------------------------------- /service-worker/example/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Test Service Worker Page 5 | 14 | 15 | 16 |

Service Worker Test

17 | 18 | 19 | -------------------------------------------------------------------------------- /hello-mobile/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | 7 | # dependencies 8 | /node_modules 9 | /bower_components 10 | 11 | # IDEs and editors 12 | /.idea 13 | 14 | # misc 15 | /.sass-cache 16 | /connect.lock 17 | /coverage/* 18 | /libpeerconnection.log 19 | npm-debug.log 20 | testem.log 21 | /typings 22 | 23 | # e2e 24 | /e2e/*.js 25 | /e2e/*.map 26 | 27 | #System Files 28 | .DS_Store 29 | Thumbs.db 30 | -------------------------------------------------------------------------------- /service-worker/example/webroot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Test Service Worker Page 5 | 14 | 15 | 16 |

Service Worker Test

17 | 18 | 19 | -------------------------------------------------------------------------------- /service-worker/example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@angular/service-worker-example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "@angular/service-worker": "^0.1.0", 13 | "gulp": "^3.9.1", 14 | "jshashes": "^1.0.5", 15 | "ts-node": "^0.7.2", 16 | "typings": "^0.7.12" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app-shell/tsconfig.es5.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "noImplicitAny": false, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "declaration": false, 9 | "outDir": "tmp/es5", 10 | "sourceMap": false, 11 | "moduleResolution": "node", 12 | "rootDir": ".", 13 | "baseUrl": ".", 14 | "lib": ["es2015", "dom"] 15 | }, 16 | "files": [ 17 | "src/index.ts", 18 | "src/unit_tests.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /app-shell/tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "es2015", 4 | "target": "es5", 5 | "noImplicitAny": false, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "declaration": true, 9 | "outDir": "tmp/esm", 10 | "sourceMap": true, 11 | "moduleResolution": "node", 12 | "rootDir": ".", 13 | "baseUrl": ".", 14 | "lib": ["es2015", "dom"] 15 | }, 16 | "files": [ 17 | "src/index.ts" 18 | ], 19 | "angularCompilerOptions": { 20 | "skipTemplateCodegen": true 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /hello-mobile/angular-cli-build.js: -------------------------------------------------------------------------------- 1 | /* global require, module */ 2 | 3 | var Angular2App = require('angular-cli/lib/broccoli/angular2-app'); 4 | 5 | module.exports = function(defaults) { 6 | return new Angular2App(defaults, { 7 | vendorNpmFiles: [ 8 | 'systemjs/dist/system-polyfills.js', 9 | 'systemjs/dist/system.src.js', 10 | 'zone.js/dist/*.js', 11 | 'es6-shim/es6-shim.js', 12 | 'reflect-metadata/*.js', 13 | 'rxjs/**/*.js', 14 | '@angular/**/*.js', 15 | '@angular2-material/**/*.+(js|css|map)' 16 | ] 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /service-worker/worker/src/worker/facade/adapter.ts: -------------------------------------------------------------------------------- 1 | export interface NgSwAdapter { 2 | newRequest(req: string | Request, init?: Object): Request; 3 | newResponse(body: string | Blob, init?: Object): Response; 4 | readonly scope: string; 5 | } 6 | 7 | export interface Clock { 8 | dateNow(): number; 9 | setTimeout(fn: Function, delay: number); 10 | } 11 | 12 | export class BrowserClock implements Clock { 13 | dateNow(): number { 14 | return Date.now(); 15 | } 16 | 17 | setTimeout(fn: Function, delay: number) { 18 | return setTimeout(fn, delay); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /service-worker/worker/src/plugins/dynamic/manifest.ts: -------------------------------------------------------------------------------- 1 | import {UrlConfig} from '../../worker'; 2 | 3 | export interface DynamicManifest { 4 | group: GroupManifest[]; 5 | } 6 | 7 | export type GroupStrategy = "backup" | "cache" | "staleWhileRefresh"; 8 | 9 | export type UrlConfigMap = {[url: string]: UrlConfig}; 10 | 11 | export interface CacheConfig { 12 | optimizeFor: string; 13 | 14 | strategy: "lru" | "lfu" | "fifo"; 15 | maxAgeMs?: number; 16 | maxSizeBytes?: number; 17 | maxEntries: number; 18 | } 19 | 20 | export interface GroupManifest { 21 | name: string; 22 | urls: UrlConfigMap; 23 | cache: CacheConfig; 24 | } 25 | -------------------------------------------------------------------------------- /hello-mobile/angular-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "project": { 3 | "version": "1.0.0-beta.4", 4 | "name": "hello-mobile" 5 | }, 6 | "apps": [ 7 | { 8 | "main": "src/main.ts", 9 | "tsconfig": "src/tsconfig.json", 10 | "mobile": true 11 | } 12 | ], 13 | "addons": [], 14 | "packages": [], 15 | "e2e": { 16 | "protractor": { 17 | "config": "config/protractor.conf.js" 18 | } 19 | }, 20 | "test": { 21 | "karma": { 22 | "config": "config/karma.conf.js" 23 | } 24 | }, 25 | "defaults": { 26 | "prefix": "app", 27 | "sourceDir": "src", 28 | "styleExt": "css" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app-shell/src/experimental/shell-parser/node-visitor/node-visitor.ts: -------------------------------------------------------------------------------- 1 | import {ASTNode} from '../ast'; 2 | 3 | export abstract class NodeVisitor { 4 | 5 | abstract process(node: ASTNode): Promise; 6 | 7 | visit(currentNode: ASTNode): Promise { 8 | return this.process(currentNode) 9 | .then((node: ASTNode) => { 10 | if (node) { 11 | return Promise 12 | .all((node.childNodes || []) 13 | .slice() 14 | .map(this.visit.bind(this))) 15 | .then(() => node); 16 | } else { 17 | return null; 18 | } 19 | }) 20 | } 21 | 22 | } 23 | 24 | -------------------------------------------------------------------------------- /app-shell/src/unit_tests.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import 'zone.js'; 3 | import 'zone.js/dist/long-stack-trace-zone.js'; 4 | import 'zone.js/dist/proxy.js'; 5 | import 'zone.js/dist/sync-test.js'; 6 | import 'zone.js/dist/jasmine-patch.js'; 7 | import 'zone.js/dist/async-test.js'; 8 | import 'zone.js/dist/fake-async-test.js'; 9 | 10 | import {TestBed} from '@angular/core/testing'; 11 | import {platformServerTesting, ServerTestingModule} from '@angular/platform-server/testing'; 12 | 13 | import prerenderTests from './app/prerender.spec'; 14 | import shellTests from './app/shell.spec'; 15 | 16 | TestBed.initTestEnvironment(ServerTestingModule, platformServerTesting()); 17 | 18 | prerenderTests(); 19 | shellTests(); 20 | -------------------------------------------------------------------------------- /app-shell/src/experimental/shell-parser/node-visitor/template-strip-visitor.ts: -------------------------------------------------------------------------------- 1 | import {ASTNode} from '../ast'; 2 | import {NodeVisitor} from './node-visitor'; 3 | import {WorkerScope} from '../context'; 4 | import {CssNodeMatcher} from '../node-matcher'; 5 | 6 | export class TemplateStripVisitor extends NodeVisitor { 7 | 8 | constructor(private matcher: CssNodeMatcher) { 9 | super(); 10 | } 11 | 12 | process(node: ASTNode) { 13 | if (this.matcher.match(node)) { 14 | if (node.parentNode) { 15 | const c = node.parentNode.childNodes; 16 | c.splice(c.indexOf(node), 1); 17 | } 18 | return Promise.resolve(null); 19 | } 20 | return Promise.resolve(node); 21 | } 22 | 23 | } 24 | 25 | -------------------------------------------------------------------------------- /hello-mobile/src/app/hello-mobile.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | beforeEachProviders, 3 | describe, 4 | expect, 5 | it, 6 | inject 7 | } from '@angular/core/testing'; 8 | import { HelloMobileAppComponent } from '../app/hello-mobile.component'; 9 | 10 | beforeEachProviders(() => [HelloMobileAppComponent]); 11 | 12 | describe('App: HelloMobile', () => { 13 | it('should create the app', 14 | inject([HelloMobileAppComponent], (app: HelloMobileAppComponent) => { 15 | expect(app).toBeTruthy(); 16 | })); 17 | 18 | it('should have as title \'hello-mobile works!\'', 19 | inject([HelloMobileAppComponent], (app: HelloMobileAppComponent) => { 20 | expect(app.title).toEqual('hello-mobile works!'); 21 | })); 22 | }); 23 | -------------------------------------------------------------------------------- /service-worker/worker/src/worker/builds/basic.ts: -------------------------------------------------------------------------------- 1 | import {bootstrapServiceWorker} from '../bootstrap'; 2 | import {Dynamic, FreshnessStrategy, PerformanceStrategy} from '../../plugins/dynamic'; 3 | import {ExternalContentCache} from '../../plugins/external'; 4 | import {RouteRedirection} from '../../plugins/routes'; 5 | import {StaticContentCache} from '../../plugins/static'; 6 | import {Push} from '../../plugins/push'; 7 | 8 | bootstrapServiceWorker({ 9 | manifestUrl: 'ngsw-manifest.json', 10 | plugins: [ 11 | StaticContentCache(), 12 | Dynamic([ 13 | new FreshnessStrategy(), 14 | new PerformanceStrategy(), 15 | ]), 16 | ExternalContentCache(), 17 | RouteRedirection(), 18 | Push(), 19 | ], 20 | }); 21 | -------------------------------------------------------------------------------- /app-shell/src/experimental/shell-parser/template-parser/parse5/parse5-template-parser.ts: -------------------------------------------------------------------------------- 1 | import {ASTNode} from '../../ast'; 2 | import {TemplateParser} from '../template-parser'; 3 | 4 | import './tokenizer-case-sensitivity-patch'; 5 | import './drop-named-entities-patch'; 6 | 7 | var Parser = require('../../../../vendor/parse5/lib/parser'); 8 | var Serializer = require('../../../../vendor/parse5/lib/serializer'); 9 | 10 | export class Parse5TemplateParser extends TemplateParser { 11 | parse(template: string): ASTNode { 12 | var parser = new Parser(); 13 | return parser.parse(template); 14 | } 15 | 16 | serialize(node: ASTNode): string { 17 | var serializer = new Serializer(node); 18 | return serializer.serialize(); 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /app-shell/src/app/prerender.spec.ts: -------------------------------------------------------------------------------- 1 | import {inject, TestBed} from '@angular/core/testing'; 2 | import {IS_PRERENDER} from './prerender'; 3 | import {AppShellModule} from './module'; 4 | 5 | export default function () { 6 | describe('IS_PRERENDER', () => { 7 | it('should be true at prerender time', () => { 8 | const prerender = TestBed 9 | .configureTestingModule({imports: [AppShellModule.prerender()]}) 10 | .get(IS_PRERENDER); 11 | expect(prerender).toBeTruthy(); 12 | }); 13 | it('should be false at runtime', () => { 14 | const prerender = TestBed 15 | .configureTestingModule({imports: [AppShellModule.runtime()]}) 16 | .get(IS_PRERENDER); 17 | expect(prerender).toBeFalsy(); 18 | }); 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /app-shell/src/app/module.ts: -------------------------------------------------------------------------------- 1 | import {ModuleWithProviders, NgModule} from '@angular/core'; 2 | import {IS_PRERENDER} from './prerender'; 3 | import {ShellNoRender, ShellRender} from './shell'; 4 | 5 | @NgModule({ 6 | declarations: [ 7 | ShellNoRender, 8 | ShellRender, 9 | ], 10 | exports: [ 11 | ShellNoRender, 12 | ShellRender, 13 | ], 14 | }) 15 | export class AppShellModule { 16 | static prerender(): ModuleWithProviders { 17 | return { 18 | ngModule: AppShellModule, 19 | providers: [{provide: IS_PRERENDER, useValue: true}] 20 | } 21 | } 22 | 23 | static runtime(): ModuleWithProviders { 24 | return { 25 | ngModule: AppShellModule, 26 | providers: [{provide: IS_PRERENDER, useValue: false}], 27 | }; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /hello-mobile/config/protractor.conf.js: -------------------------------------------------------------------------------- 1 | /*global jasmine */ 2 | var SpecReporter = require('jasmine-spec-reporter'); 3 | 4 | exports.config = { 5 | allScriptsTimeout: 11000, 6 | specs: [ 7 | '../e2e/**/*.e2e.ts' 8 | ], 9 | capabilities: { 10 | 'browserName': 'chrome' 11 | }, 12 | directConnect: true, 13 | baseUrl: 'http://localhost:4200/', 14 | framework: 'jasmine', 15 | jasmineNodeOpts: { 16 | showColors: true, 17 | defaultTimeoutInterval: 30000, 18 | print: function() {} 19 | }, 20 | useAllAngular2AppRoots: true, 21 | beforeLaunch: function() { 22 | require('ts-node').register({ 23 | project: 'e2e' 24 | }); 25 | }, 26 | onPrepare: function() { 27 | jasmine.getEnv().addReporter(new SpecReporter()); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /app-shell/src/experimental/shell-parser/context.ts: -------------------------------------------------------------------------------- 1 | export abstract class WorkerScope { 2 | abstract fetch(url: string | Request): Promise; 3 | abstract newRequest(input: string | Request, init?: RequestInit): Request; 4 | abstract newResponse(body?: BodyInit, init?: ResponseInit): Response; 5 | caches: CacheStorage; 6 | } 7 | 8 | export class BrowserWorkerScope { 9 | fetch(url: string | Request): Promise { 10 | return fetch(url); 11 | } 12 | 13 | get caches() { 14 | return caches; 15 | } 16 | 17 | newRequest(input: string | Request, init?: RequestInit): Request { 18 | return new Request(input, init); 19 | } 20 | 21 | newResponse(body?: BodyInit, init?: ResponseInit) { 22 | return new Response(body, init); 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /service-worker/worker/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "noImplicitAny": false, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "declaration": true, 9 | "outDir": "tmp/es5", 10 | "sourceMap": false, 11 | "moduleResolution": "node", 12 | "rootDir": ".", 13 | "baseUrl": ".", 14 | "paths": { 15 | "@angular/service-worker": ["src"], 16 | "@angular/service-worker/worker": ["src/worker"] 17 | }, 18 | "lib": [ 19 | "dom", 20 | "es2015" 21 | ], 22 | "types": [ 23 | "jasmine", 24 | "protractor", 25 | "node", 26 | "selenium-webdriver" 27 | ] 28 | }, 29 | "exclude": [ 30 | "node_modules" 31 | ] 32 | } -------------------------------------------------------------------------------- /hello-mobile/src/app/hello-mobile.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { APP_SHELL_DIRECTIVES } from '@angular/app-shell'; 3 | import { MdToolbar } from '@angular2-material/toolbar'; 4 | import { MdSpinner } from '@angular2-material/progress-circle'; 5 | 6 | @Component({ 7 | moduleId: module.id, 8 | selector: 'hello-mobile-app', 9 | template: ` 10 | 11 | {{title}} 12 | 13 | 14 |

App is Fully Rendered

15 | `, 16 | styles: [` 17 | md-spinner { 18 | margin: 24px auto 0; 19 | } 20 | `], 21 | directives: [APP_SHELL_DIRECTIVES, MdToolbar, MdSpinner] 22 | }) 23 | export class HelloMobileAppComponent { 24 | title = 'Hello Mobile'; 25 | } 26 | -------------------------------------------------------------------------------- /app-shell/src/experimental/shell-parser/node-visitor/resource-inline/stylesheet-resource-inline-visitor.ts: -------------------------------------------------------------------------------- 1 | import {ASTNode} from '../../ast'; 2 | import {ResourceInlineVisitor} from './resource-inline-visitor'; 3 | import {WorkerScope} from '../../context'; 4 | 5 | const URL_REGEXP = /:\s+url\(['"]?(.*?)['"]?\)/gmi; 6 | 7 | export class StylesheetResourceInlineVisitor extends ResourceInlineVisitor { 8 | 9 | process(node: ASTNode): Promise { 10 | if (node.nodeName.toLowerCase() === 'style') { 11 | const styleNode = node.childNodes[0]; 12 | return this.inlineAssets(styleNode.value) 13 | .then((content: string) => { 14 | styleNode.value = content; 15 | return node; 16 | }); 17 | } 18 | return Promise.resolve(node); 19 | } 20 | 21 | } 22 | 23 | -------------------------------------------------------------------------------- /service-worker/worker/src/test/e2e/harness/server/push.ts: -------------------------------------------------------------------------------- 1 | declare interface WebPushParams { 2 | TTL?: number; 3 | vapidDetails: { 4 | subject: string; 5 | publicKey: string; 6 | privateKey: string; 7 | }; 8 | } 9 | 10 | declare interface WebPush { 11 | sendNotification(subscription: any, message: any, options: WebPushParams): Promise; 12 | } 13 | 14 | let push: WebPush = require('web-push'); 15 | 16 | export function sendPush(subscription: any, payload?: Object): Promise { 17 | return push.sendNotification(subscription, JSON.stringify(payload), { 18 | vapidDetails: { 19 | subject: 'mailto:test@angular.io', 20 | publicKey: 'BLRl_fG1TCTc1D2JwzOpdZjaRcJucXtG8TAd5g9vuYjl6KUUDxgoRjQPCgjZfY-_Rusd_qtjNvanHXeFvOFlxH4', 21 | privateKey: 't3JtFOflouvPUxFKeSSmZdjuVidnD_0dNGFM1v-N4PI', 22 | }, 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/angular/mobile-toolkit.svg?branch=master)](https://travis-ci.org/angular/mobile-toolkit) 2 | 3 | # Angular Mobile Toolkit 4 | 5 | This repo is a series of tools and guides to help build Progressive 6 | Web Apps. All guides are currently based on Angular CLI, and all tools 7 | should be considered alpha quality. In the future, more guides and recipes 8 | to cover different tools and use cases will be added here and on 9 | [mobile.angular.io](https://mobile.angular.io). 10 | 11 | ## Guides 12 | 13 | 1. [Create an installable mobile web app with Angular CLI](./guides/cli-setup.md) 14 | 2. [Make the App Installable with Web App Manifest](./guides/web-app-manifest.md) 15 | 3. [Add an app shell component to the App](./guides/app-shell.md) 16 | 4. [Add basic offline capabilities with Service Worker](./guides/service-worker.md) 17 | -------------------------------------------------------------------------------- /app-shell/src/experimental/shell-parser/node-visitor/resource-inline/inline-style-resource-inline-visitor.ts: -------------------------------------------------------------------------------- 1 | import {ASTNode, ASTAttribute} from '../../ast'; 2 | import {ResourceInlineVisitor} from './resource-inline-visitor'; 3 | import {WorkerScope} from '../../context'; 4 | 5 | const URL_REGEXP = /:\s+url\(['"]?(.*?)['"]?\)/gmi; 6 | 7 | export class InlineStyleResourceInlineVisitor extends ResourceInlineVisitor { 8 | 9 | process(node: ASTNode): Promise { 10 | const styleAttr = (node.attrs || []) 11 | .filter((a: ASTAttribute) => a.name === 'style') 12 | .pop(); 13 | if (styleAttr) { 14 | return this.inlineAssets(styleAttr.value) 15 | .then((content: string) => { 16 | styleAttr.value = content; 17 | return node; 18 | }); 19 | } 20 | return Promise.resolve(node); 21 | } 22 | 23 | } 24 | 25 | -------------------------------------------------------------------------------- /hello-mobile/src/main-app-shell.ts: -------------------------------------------------------------------------------- 1 | import { provide } from '@angular/core'; 2 | import { APP_BASE_HREF } from '@angular/common'; 3 | import { APP_SHELL_BUILD_PROVIDERS } from '@angular/app-shell'; 4 | import { HelloMobileAppComponent } from './app/'; 5 | import { 6 | REQUEST_URL, 7 | ORIGIN_URL 8 | } from 'angular2-universal'; 9 | 10 | export const options = { 11 | directives: [ 12 | // The component that will become the main App Shell 13 | HelloMobileAppComponent 14 | ], 15 | platformProviders: [ 16 | APP_SHELL_BUILD_PROVIDERS, 17 | provide(ORIGIN_URL, { 18 | useValue: '' 19 | }) 20 | ], 21 | providers: [ 22 | // What URL should Angular be treating the app as if navigating 23 | provide(APP_BASE_HREF, {useValue: '/'}), 24 | provide(REQUEST_URL, {useValue: '/'}) 25 | ], 26 | async: false, 27 | preboot: false 28 | }; 29 | 30 | -------------------------------------------------------------------------------- /service-worker/example/gulpfile.ts: -------------------------------------------------------------------------------- 1 | declare var require; 2 | 3 | let gulp = require('gulp'); 4 | let ngsw = require('@angular/service-worker'); 5 | 6 | gulp.task('default', ['copy:static', 'copy:sw', 'build:manifest']) 7 | 8 | gulp.task('copy:sw/dev', () => gulp 9 | .src([ 10 | '../worker/dist/worker.js' 11 | ]) 12 | .pipe(gulp.dest('webroot'))); 13 | 14 | gulp.task('copy:static', () => gulp 15 | .src([ 16 | 'src/**/*.html' 17 | ]) 18 | .pipe(gulp.dest('webroot'))); 19 | 20 | gulp.task('copy:sw', () => gulp 21 | .src([ 22 | 'node_modules/@angular/service-worker/dist/worker.js' 23 | ]) 24 | .pipe(gulp.dest('webroot'))); 25 | 26 | gulp.task('build:manifest', () => ngsw 27 | .gulpGenManifest({ 28 | group: [ 29 | { 30 | name: 'html', 31 | sources: gulp.src('src/**/*.html') 32 | } 33 | ] 34 | }) 35 | .pipe(gulp.dest('webroot'))); -------------------------------------------------------------------------------- /service-worker/worker/src/worker/builds/test.ts: -------------------------------------------------------------------------------- 1 | import {bootstrapServiceWorker} from '../bootstrap'; 2 | import {Dynamic, FreshnessStrategy, PerformanceStrategy} from '../../plugins/dynamic'; 3 | import {ExternalContentCache} from '../../plugins/external'; 4 | import {RouteRedirection} from '../../plugins/routes'; 5 | import {StaticContentCache} from '../../plugins/static'; 6 | import {Push} from '../../plugins/push'; 7 | import {Verbosity, HttpHandler} from '../logging'; 8 | 9 | bootstrapServiceWorker({ 10 | manifestUrl: '/ngsw-manifest.json', 11 | plugins: [ 12 | StaticContentCache(), 13 | Dynamic([ 14 | new FreshnessStrategy(), 15 | new PerformanceStrategy(), 16 | ]), 17 | ExternalContentCache(), 18 | RouteRedirection(), 19 | Push(), 20 | ], 21 | logLevel: Verbosity.DEBUG, 22 | logHandlers: [ 23 | new HttpHandler('/ngsw-log'), 24 | ], 25 | }); 26 | -------------------------------------------------------------------------------- /service-worker/worker/src/worker/facade/events.ts: -------------------------------------------------------------------------------- 1 | export interface Callback { 2 | (event: T): void; 3 | } 4 | 5 | function nop(event: any): void {} 6 | 7 | export class NgSwEvents { 8 | install: Callback = nop; 9 | activate: Callback = nop; 10 | fetch: Callback = nop; 11 | message: Callback = nop; 12 | push: Callback = nop; 13 | 14 | constructor(scope: ServiceWorkerGlobalScope) { 15 | scope.addEventListener('install', (event: InstallEvent) => this.install(event)); 16 | scope.addEventListener('activate', (event: ActivateEvent) => this.activate(event)); 17 | scope.addEventListener('fetch', (event: FetchEvent) => this.fetch(event)); 18 | scope.addEventListener('message', (event: MessageEvent) => this.message(event)); 19 | scope.addEventListener('push', (event: PushEvent) => this.push(event)); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | 3 | # Don’t commit the following directories created by pub. 4 | packages 5 | pubspec.lock 6 | .pub 7 | .packages 8 | 9 | /dist/ 10 | .buildlog 11 | node_modules 12 | bower_components 13 | 14 | # Or broccoli working directory 15 | tmp 16 | 17 | # Or the files created by dart2js. 18 | *.dart.js 19 | *.dart.precompiled.js 20 | *.js_ 21 | *.js.deps 22 | *.js.map 23 | 24 | # Files created by the template compiler 25 | **/*.ngfactory.ts 26 | **/*.css.ts 27 | **/*.css.shim.ts 28 | 29 | # Or type definitions we mirror from github 30 | # (NB: these lines are removed in publish-build-artifacts.sh) 31 | **/typings/**/*.d.ts 32 | **/typings/tsd.cached.json 33 | !/service-worker/worker/src/typings 34 | 35 | # Include when developing application packages. 36 | pubspec.lock 37 | .c9 38 | .idea/ 39 | .settings/ 40 | *.swo 41 | .vscode 42 | 43 | # Don't check in secret files 44 | *secret.js 45 | 46 | # Ignore npm debug log 47 | npm-debug.log 48 | -------------------------------------------------------------------------------- /service-worker/worker/src/worker/url.ts: -------------------------------------------------------------------------------- 1 | export type UrlMatchType = "exact" | "prefix" | "regex"; 2 | 3 | export interface UrlConfig { 4 | match?: UrlMatchType; 5 | } 6 | 7 | export class UrlMatcher { 8 | match: UrlMatchType; 9 | 10 | private _regex: RegExp; 11 | 12 | constructor(public pattern: string, config: UrlConfig = {}, public scope: string) { 13 | this.match = config.match || "exact"; 14 | if (this.match === 'regex') { 15 | this._regex = new RegExp(pattern); 16 | } 17 | } 18 | 19 | matches(url: string): boolean { 20 | // Strip the scope from the URL if present. 21 | if (url.startsWith(this.scope)) { 22 | url = url.substr(this.scope.length); 23 | } 24 | switch (this.match) { 25 | case 'exact': 26 | return this.pattern === url; 27 | case 'prefix': 28 | return url.startsWith(this.pattern); 29 | case 'regex': 30 | return this._regex.test(url); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app-shell/src/experimental/shell-parser/config.ts: -------------------------------------------------------------------------------- 1 | export type RouteDefinition = string; 2 | 3 | const SHELL_PARSER_CACHE_NAME = 'mobile-toolkit:app-shell'; 4 | const APP_SHELL_URL = './app_shell.html'; 5 | const NO_RENDER_CSS_SELECTOR = '[shellNoRender]'; 6 | const ROUTE_DEFINITIONS: RouteDefinition[] = []; 7 | const INLINE_IMAGES: string[] = ['png', 'svg', 'jpg']; 8 | 9 | // TODO(mgechev): use if we decide to include @angular/core 10 | // export const SHELL_PARSER_CONFIG = new OpaqueToken('ShellRuntimeParserConfig'); 11 | 12 | export interface ShellParserConfig { 13 | APP_SHELL_URL?: string; 14 | SHELL_PARSER_CACHE_NAME?: string; 15 | NO_RENDER_CSS_SELECTOR?: string; 16 | ROUTE_DEFINITIONS?: RouteDefinition[]; 17 | INLINE_IMAGES?: string[]; 18 | } 19 | 20 | export const SHELL_PARSER_DEFAULT_CONFIG: ShellParserConfig = { 21 | SHELL_PARSER_CACHE_NAME, 22 | APP_SHELL_URL, 23 | NO_RENDER_CSS_SELECTOR, 24 | ROUTE_DEFINITIONS, 25 | INLINE_IMAGES 26 | }; 27 | 28 | -------------------------------------------------------------------------------- /service-worker/worker/tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "es2015", 4 | "target": "es5", 5 | "noImplicitAny": false, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "declaration": true, 9 | "outDir": "tmp/esm", 10 | "sourceMap": true, 11 | "moduleResolution": "node", 12 | "rootDir": ".", 13 | "baseUrl": ".", 14 | "paths": { 15 | "@angular/service-worker/worker": ["src/worker"] 16 | }, 17 | "lib": ["es2015", "dom"] 18 | }, 19 | "files": [ 20 | "src/index.ts", 21 | "src/plugins/push/index.ts", 22 | "src/plugins/routes/index.ts", 23 | "src/plugins/static/index.ts", 24 | "src/worker/builds/basic.ts", 25 | "src/worker/builds/test.ts", 26 | "src/worker/index.ts", 27 | "src/typings/jshashes.d.ts", 28 | "src/typings/service-worker.d.ts" 29 | ], 30 | "angularCompilerOptions": { 31 | "skipTemplateCodegen": true 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /service-worker/worker/src/test/e2e/harness/client/main.ts: -------------------------------------------------------------------------------- 1 | 2 | import {Component, NgModule} from '@angular/core'; 3 | import {FormsModule} from '@angular/forms'; 4 | import {BrowserModule} from '@angular/platform-browser'; 5 | import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; 6 | import {ControllerCmp} from './src/controller'; 7 | import {ServiceWorkerModule} from '@angular/service-worker'; 8 | 9 | 10 | @Component({ 11 | selector: 'sw-testing-harness', 12 | template: ` 13 |

Service Worker Testing Harness

14 | 15 | `, 16 | }) 17 | class SwTestingHarnessCmp {} 18 | 19 | @NgModule({ 20 | declarations: [ 21 | SwTestingHarnessCmp, 22 | ControllerCmp, 23 | ], 24 | imports: [ 25 | BrowserModule, 26 | FormsModule, 27 | ServiceWorkerModule, 28 | ], 29 | bootstrap: [SwTestingHarnessCmp], 30 | }) 31 | export class SwTestingHarnessModule {} 32 | 33 | platformBrowserDynamic().bootstrapModule(SwTestingHarnessModule); 34 | -------------------------------------------------------------------------------- /app-shell/src/experimental/shell-parser/testing/context-mock.ts: -------------------------------------------------------------------------------- 1 | import {MockRequest, MockResponse} from './mock-requests'; 2 | import {MockCacheStorage} from './mock-caches'; 3 | import {WorkerScope} from '../context'; 4 | 5 | export class MockWorkerScope { 6 | mockResponses: {[key: string]: Response} = {}; 7 | currentCaches: MockCacheStorage; 8 | 9 | fetch(url: string | Request): Promise { 10 | const requestUrl: string = url; 11 | if (this.mockResponses[requestUrl]) { 12 | return Promise.resolve(this.mockResponses[requestUrl]); 13 | } 14 | const resp = new MockResponse(''); 15 | resp.ok = false; 16 | resp.status = 404; 17 | resp.statusText = 'File Not Found'; 18 | return Promise.resolve(resp); 19 | } 20 | 21 | get caches(): CacheStorage { 22 | return this.currentCaches; 23 | } 24 | 25 | newRequest(input: string | Request, init?: RequestInit): Request { 26 | return new MockRequest(input, init); 27 | } 28 | 29 | newResponse(body?: BodyInit, init?: ResponseInit) { 30 | return new MockResponse(body, init); 31 | } 32 | } 33 | 34 | -------------------------------------------------------------------------------- /hello-mobile/src/manifest.webapp: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Hello Mobile", 3 | "short_name": "Hello Mobile", 4 | "icons": [ 5 | { 6 | "src": "/icons/android-chrome-36x36.png", 7 | "sizes": "36x36", 8 | "type": "image/png", 9 | "density": 0.75 10 | }, 11 | { 12 | "src": "/icons/android-chrome-48x48.png", 13 | "sizes": "48x48", 14 | "type": "image/png", 15 | "density": 1 16 | }, 17 | { 18 | "src": "/icons/android-chrome-72x72.png", 19 | "sizes": "72x72", 20 | "type": "image/png", 21 | "density": 1.5 22 | }, 23 | { 24 | "src": "/icons/android-chrome-96x96.png", 25 | "sizes": "96x96", 26 | "type": "image/png", 27 | "density": 2 28 | }, 29 | { 30 | "src": "/icons/android-chrome-144x144.png", 31 | "sizes": "144x144", 32 | "type": "image/png", 33 | "density": 3 34 | }, 35 | { 36 | "src": "/icons/android-chrome-192x192.png", 37 | "sizes": "192x192", 38 | "type": "image/png", 39 | "density": 4 40 | } 41 | ], 42 | "theme_color": "#000000", 43 | "background_color": "#e0e0e0", 44 | "start_url": "/index.html", 45 | "display": "standalone", 46 | "orientation": "portrait" 47 | } 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2014-2016 Google, Inc. http://angular.io 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /app-shell/src/app/shell.ts: -------------------------------------------------------------------------------- 1 | import { Directive, Inject, OnInit, ViewContainerRef, TemplateRef } from '@angular/core'; 2 | import {IS_PRERENDER} from './prerender'; 3 | 4 | @Directive({ 5 | selector: '[shellNoRender]' 6 | }) 7 | export class ShellNoRender implements OnInit { 8 | 9 | constructor( 10 | private _viewContainer: ViewContainerRef, 11 | private _templateRef: TemplateRef, 12 | @Inject(IS_PRERENDER) private _isPrerender: boolean) {} 13 | 14 | ngOnInit() { 15 | if (this._isPrerender) { 16 | this._viewContainer.clear(); 17 | } else { 18 | this._viewContainer.createEmbeddedView(this._templateRef); 19 | } 20 | } 21 | } 22 | 23 | @Directive({ 24 | selector: '[shellRender]' 25 | }) 26 | export class ShellRender implements OnInit { 27 | constructor( 28 | private _viewContainer: ViewContainerRef, 29 | private _templateRef: TemplateRef, 30 | @Inject(IS_PRERENDER) private _isPrerender: boolean) {} 31 | 32 | ngOnInit() { 33 | if (this._isPrerender) { 34 | this._viewContainer.createEmbeddedView(this._templateRef); 35 | } else { 36 | this._viewContainer.clear(); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /service-worker/worker/src/worker/cache.ts: -------------------------------------------------------------------------------- 1 | import {NgSwCache} from './facade'; 2 | 3 | export class ScopedCache implements NgSwCache { 4 | 5 | constructor(private delegate: NgSwCache, private prefix: string) {} 6 | 7 | load(cache: string, req: string | Request) { 8 | return this.delegate.load(this.prefix + cache, req); 9 | } 10 | 11 | store(cache: string, req: string | Request, resp: Response): Promise { 12 | return this.delegate.store(this.prefix + cache, req, resp); 13 | } 14 | 15 | remove(cache: string): Promise { 16 | return this.delegate.remove(this.prefix + cache); 17 | } 18 | 19 | invalidate(cache: string, req: string | Request): Promise { 20 | return this.delegate.invalidate(this.prefix + cache, req); 21 | } 22 | 23 | keys(): Promise { 24 | return this 25 | .delegate 26 | .keys() 27 | .then(keys => keys 28 | .filter(key => key.startsWith(this.prefix)) 29 | .map(key => key.substr(this.prefix.length)) 30 | ); 31 | } 32 | 33 | keysOf(cache: string): Promise { 34 | return this 35 | .delegate 36 | .keysOf(this.prefix + cache); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /service-worker/worker/tsconfig.es5.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "noImplicitAny": false, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "declaration": true, 9 | "outDir": "tmp/es5", 10 | "sourceMap": true, 11 | "moduleResolution": "node", 12 | "rootDir": ".", 13 | "baseUrl": ".", 14 | "paths": { 15 | "@angular/service-worker": ["src"], 16 | "@angular/service-worker/worker": ["src/worker"] 17 | }, 18 | "lib": [ 19 | "dom", 20 | "es2015" 21 | ], 22 | "types": [ 23 | "jasmine", 24 | "node", 25 | "protractor", 26 | "selenium-webdriver" 27 | ] 28 | }, 29 | "files": [ 30 | "src/build/index.ts", 31 | "src/webpack.ts", 32 | "src/worker/index.ts", 33 | "src/test/unit/worker.spec.ts", 34 | "src/plugins/dynamic/group.spec.ts", 35 | "src/plugins/dynamic/linked.spec.ts", 36 | "src/test/e2e/harness/client/main.ts", 37 | "src/test/e2e/harness/server/server.ts", 38 | "src/test/e2e/spec/sanity.e2e.ts", 39 | "src/testing/index.ts", 40 | "src/testing/mock.spec.ts", 41 | "src/typings/jshashes.d.ts", 42 | "src/typings/service-worker.d.ts" 43 | ] 44 | } -------------------------------------------------------------------------------- /app-shell/src/experimental/shell-parser/shell-parser-factory.ts: -------------------------------------------------------------------------------- 1 | import {Parse5TemplateParser} from './template-parser'; 2 | import {ShellParserImpl} from './shell-parser'; 3 | import {cssNodeMatcherFactory} from './node-matcher'; 4 | import {StylesheetResourceInlineVisitor, InlineStyleResourceInlineVisitor, TemplateStripVisitor, NodeVisitor} from './node-visitor'; 5 | import {BrowserWorkerScope} from './context'; 6 | import {ShellParserConfig, SHELL_PARSER_DEFAULT_CONFIG} from './config'; 7 | 8 | export const normalizeConfig = (config: ShellParserConfig) => { 9 | return Object.assign(Object.assign({}, SHELL_PARSER_DEFAULT_CONFIG), config); 10 | }; 11 | 12 | export const shellParserFactory = (config: ShellParserConfig = {}) => { 13 | const parserConfig = normalizeConfig(config); 14 | const scope = new BrowserWorkerScope(); 15 | const visitors: NodeVisitor[] = []; 16 | if (config.INLINE_IMAGES) { 17 | visitors.push(new TemplateStripVisitor(cssNodeMatcherFactory(parserConfig.NO_RENDER_CSS_SELECTOR))); 18 | visitors.push(new StylesheetResourceInlineVisitor(scope, config.INLINE_IMAGES)); 19 | visitors.push(new InlineStyleResourceInlineVisitor(scope, config.INLINE_IMAGES)); 20 | } 21 | return new ShellParserImpl( 22 | parserConfig, 23 | new Parse5TemplateParser(), 24 | visitors, 25 | scope); 26 | }; 27 | 28 | -------------------------------------------------------------------------------- /hello-mobile/src/icons/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /service-worker/worker/src/test/e2e/spec/protractor.config.js: -------------------------------------------------------------------------------- 1 | exports.config = { 2 | baseUrl: 'http://localhost:8080/dev/src/', 3 | specs: ['*.e2e.js'], 4 | directConnect: true, 5 | exclude: [], 6 | multiCapabilities: [{ 7 | browserName: 'chrome', 8 | chromeOptions: { 9 | prefs: { 10 | 'profile.content_settings.exceptions.push_messaging': { 11 | 'http://localhost:8080,http://localhost:8080': { 'setting': 1 } 12 | }, 13 | 'profile.content_settings.exceptions.notifications': { 14 | 'http://localhost:8080,*': { 15 | 'setting': 1, 16 | 'last_used': '1467058850.735089' 17 | } 18 | } 19 | } 20 | } 21 | }], 22 | allScriptsTimeout: 110000, 23 | getPageTimeout: 100000, 24 | framework: 'jasmine2', 25 | jasmineNodeOpts: { 26 | isVerbose: false, 27 | showColors: true, 28 | includeStackTrace: false, 29 | defaultTimeoutInterval: 400000 30 | }, 31 | 32 | /** 33 | * ng2 related configuration 34 | * 35 | * useAllAngular2AppRoots: tells Protractor to wait for any angular2 apps on the page instead of just the one matching 36 | * `rootEl` 37 | * 38 | */ 39 | useAllAngular2AppRoots: true, 40 | }; -------------------------------------------------------------------------------- /service-worker/worker/src/worker/api.ts: -------------------------------------------------------------------------------- 1 | import {NgSwAdapter, NgSwCache} from './facade'; 2 | import {Manifest} from './manifest'; 3 | 4 | export type FetchDelegate = () => Promise; 5 | 6 | export interface FetchInstruction { 7 | (next: FetchDelegate): Promise; 8 | desc?: Object; 9 | } 10 | 11 | export interface Operation { 12 | (): Promise; 13 | desc?: Object; 14 | } 15 | 16 | export interface VersionWorker extends StreamController { 17 | readonly manifest: Manifest; 18 | readonly cache: NgSwCache; 19 | readonly adapter: NgSwAdapter; 20 | 21 | refresh(req: Request, cacheBust?: boolean): Promise; 22 | fetch(req: Request): Promise; 23 | showNotification(title: string, options?: Object): void; 24 | sendToStream(id: number, message: Object): void; 25 | closeStream(id: number): void; 26 | } 27 | 28 | export interface StreamController { 29 | sendToStream(id: number, message: Object): void; 30 | closeStream(id: number): void; 31 | } 32 | 33 | export interface Plugin> { 34 | setup(operations: Operation[]): void; 35 | update?(operations: Operation[], previous: T): void; 36 | fetch?(req: Request): FetchInstruction; 37 | cleanup?(operations: Operation[]): void; 38 | message?(message: any, id: number): void; 39 | messageClosed?(id: number); 40 | push?(data: any): void; 41 | validate?(): Promise; 42 | } 43 | 44 | export interface PluginFactory> { 45 | (worker: VersionWorker): Plugin; 46 | } 47 | -------------------------------------------------------------------------------- /app-shell/src/experimental/shell-parser/template-parser/parse5/tokenizer-case-sensitivity-patch.spec.ts: -------------------------------------------------------------------------------- 1 | import './tokenizer-case-sensitivity-patch'; 2 | 3 | import { 4 | inject 5 | } from '@angular/core/testing'; 6 | 7 | var Tokenizer = require('../../../../vendor/parse5/lib/tokenizer'); 8 | 9 | describe('tokenizer\'s patch', () => { 10 | 11 | let lexer: any; 12 | beforeEach(() => { 13 | lexer = new Tokenizer(); 14 | }); 15 | 16 | it('should keep case sensntivity of elements', () => { 17 | lexer.write('
'); 18 | const openDiv = lexer.getNextToken(); 19 | expect(openDiv.type).toBe('START_TAG_TOKEN'); 20 | expect(openDiv.tagName).toBe('DiV'); 21 | expect(openDiv.selfClosing).toBe(false); 22 | 23 | const closeDiv = lexer.getNextToken(); 24 | expect(closeDiv.type).toBe('END_TAG_TOKEN'); 25 | expect(closeDiv.tagName).toBe('DiV'); 26 | }); 27 | 28 | it('should preserve case sensitivity of complex elements', () => { 29 | lexer.write(''); 30 | const open = lexer.getNextToken(); 31 | expect(open.tagName).toBe('mY-ApP'); 32 | const close = lexer.getNextToken(); 33 | expect(close.tagName).toBe('mY-ApP'); 34 | }); 35 | 36 | it('should keep case sensitivity of attrs', () => { 37 | lexer.write('
'); 38 | const div = lexer.getNextToken(); 39 | expect(div.tagName).toBe('dIV'); 40 | expect(div.attrs[0].name).toBe('StYlE'); 41 | expect(div.attrs[0].value).toBe('color: red;'); 42 | }); 43 | 44 | }); 45 | 46 | -------------------------------------------------------------------------------- /hello-mobile/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | HelloMobile 6 | 7 | 8 | {{#unless environment.production}} 9 | 10 | {{/unless}} 11 | 12 | 13 | 14 | 15 | 16 | {{#each mobile.icons}} 17 | 18 | {{/each}} 19 | 20 | {{#if environment.production}} 21 | 28 | {{/if}} 29 | 30 | 31 | 32 | Loading... 33 | 34 | 35 | 36 | {{#if environment.production}} 37 | 38 | {{else}} 39 | {{#each scripts.polyfills}}{{/each}} 40 | 45 | {{/if}} 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /hello-mobile/config/karma-test-shim.js: -------------------------------------------------------------------------------- 1 | /*global jasmine, __karma__, window*/ 2 | Error.stackTraceLimit = Infinity; 3 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000; 4 | 5 | __karma__.loaded = function () { 6 | }; 7 | 8 | var distPath = '/base/dist/'; 9 | var appPath = distPath + 'app/'; 10 | 11 | function isJsFile(path) { 12 | return path.slice(-3) == '.js'; 13 | } 14 | 15 | function isSpecFile(path) { 16 | return path.slice(-8) == '.spec.js'; 17 | } 18 | 19 | function isAppFile(path) { 20 | return isJsFile(path) && (path.substr(0, appPath.length) == appPath); 21 | } 22 | 23 | var allSpecFiles = Object.keys(window.__karma__.files) 24 | .filter(isSpecFile) 25 | .filter(isAppFile); 26 | 27 | // Load our SystemJS configuration. 28 | System.config({ 29 | baseURL: distPath 30 | }); 31 | 32 | System.import('system-config.js').then(function() { 33 | // Load and configure the TestComponentBuilder. 34 | return Promise.all([ 35 | System.import('@angular/core/testing'), 36 | System.import('@angular/platform-browser-dynamic/testing') 37 | ]).then(function (providers) { 38 | var testing = providers[0]; 39 | var testingBrowser = providers[1]; 40 | 41 | testing.setBaseTestProviders(testingBrowser.TEST_BROWSER_DYNAMIC_PLATFORM_PROVIDERS, 42 | testingBrowser.TEST_BROWSER_DYNAMIC_APPLICATION_PROVIDERS); 43 | }); 44 | }).then(function() { 45 | // Finally, load all spec files. 46 | // This will run the tests directly. 47 | return Promise.all( 48 | allSpecFiles.map(function (moduleName) { 49 | return System.import(moduleName); 50 | })); 51 | }).then(__karma__.start, __karma__.error); -------------------------------------------------------------------------------- /app-shell/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@angular/app-shell", 3 | "description": "App Shell runtime library for Angular 2 Progressive Web Apps.", 4 | "version": "0.1.0", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/angular/mobile-toolkit.git" 8 | }, 9 | "contributors": [ 10 | "Jeff Cross ", 11 | "Minko Gechev ", 12 | "Alex Rickabaugh " 13 | ], 14 | "keywords": [ 15 | "angular2", 16 | "pwa", 17 | "progressive web apps", 18 | "app shell" 19 | ], 20 | "license": "MIT", 21 | "angular-cli": {}, 22 | "scripts": {}, 23 | "main": "bundles/app-shell.umd.js", 24 | "module": "index.js", 25 | "private": true, 26 | "dependencies": {}, 27 | "devDependencies": { 28 | "@angular/common": "^2.0.0", 29 | "@angular/compiler": "^2.0.0", 30 | "@angular/compiler-cli": "^0.6.2", 31 | "@angular/core": "^2.0.0", 32 | "@angular/platform-browser": "^2.0.0", 33 | "@angular/platform-browser-dynamic": "^2.0.0", 34 | "@angular/platform-server": "^2.0.0", 35 | "@types/jasmine": "^2.2.34", 36 | "gulp": "^3.9.1", 37 | "gulp-jasmine": "^2.4.1", 38 | "reflect-metadata": "^0.1.8", 39 | "rimraf": "^2.5.4", 40 | "rollup": "^0.36.0", 41 | "rollup-plugin-commonjs": "^5.0.4", 42 | "rollup-plugin-node-resolve": "^2.0.0", 43 | "run-sequence": "^1.2.2", 44 | "rxjs": "^5.0.0-beta.12", 45 | "ts-node": "^1.3.0", 46 | "typescript": "^2.0.3", 47 | "zone.js": "^0.6.25" 48 | }, 49 | "peerDependencies": { 50 | "@angular/core": "^2.0.0" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /hello-mobile/config/karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config) { 2 | config.set({ 3 | basePath: '..', 4 | frameworks: ['jasmine'], 5 | plugins: [ 6 | require('karma-jasmine'), 7 | require('karma-chrome-launcher') 8 | ], 9 | customLaunchers: { 10 | // chrome setup for travis CI using chromium 11 | Chrome_travis_ci: { 12 | base: 'Chrome', 13 | flags: ['--no-sandbox'] 14 | } 15 | }, 16 | files: [ 17 | { pattern: 'dist/vendor/es6-shim/es6-shim.js', included: true, watched: false }, 18 | { pattern: 'dist/vendor/zone.js/dist/zone.js', included: true, watched: false }, 19 | { pattern: 'dist/vendor/reflect-metadata/Reflect.js', included: true, watched: false }, 20 | { pattern: 'dist/vendor/systemjs/dist/system-polyfills.js', included: true, watched: false }, 21 | { pattern: 'dist/vendor/systemjs/dist/system.src.js', included: true, watched: false }, 22 | { pattern: 'dist/vendor/zone.js/dist/async-test.js', included: true, watched: false }, 23 | 24 | { pattern: 'config/karma-test-shim.js', included: true, watched: true }, 25 | 26 | // Distribution folder. 27 | { pattern: 'dist/**/*', included: false, watched: true } 28 | ], 29 | exclude: [ 30 | // Vendor packages might include spec files. We don't want to use those. 31 | 'dist/vendor/**/*.spec.js' 32 | ], 33 | preprocessors: {}, 34 | reporters: ['progress'], 35 | port: 9876, 36 | colors: true, 37 | logLevel: config.LOG_INFO, 38 | autoWatch: true, 39 | browsers: ['Chrome'], 40 | singleRun: false 41 | }); 42 | }; 43 | -------------------------------------------------------------------------------- /service-worker/worker/src/plugins/external/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FetchInstruction, 3 | Operation, 4 | Plugin, 5 | PluginFactory, 6 | VersionWorker, 7 | VersionWorkerImpl, 8 | cacheFromNetworkOp, 9 | fetchFromCacheInstruction, 10 | } from '@angular/service-worker/worker'; 11 | 12 | interface UrlConfig { 13 | url: string; 14 | } 15 | 16 | interface ExternalManifest { 17 | urls: UrlConfig[]; 18 | } 19 | 20 | export interface ExternalContentCacheOptions { 21 | manifestKey?: string; 22 | } 23 | 24 | export function ExternalContentCache(options?: ExternalContentCacheOptions): PluginFactory { 25 | const manifestKey = (options && options.manifestKey) || 'external'; 26 | return (worker: VersionWorker) => new ExternalPlugin(worker as VersionWorkerImpl, manifestKey); 27 | } 28 | 29 | export class ExternalPlugin implements Plugin { 30 | private cacheKey: string; 31 | 32 | constructor(public worker: VersionWorkerImpl, public key: string) { 33 | this.cacheKey = key === 'external' ? key : `external:${key}`; 34 | } 35 | 36 | private get externalManifest(): ExternalManifest { 37 | return this.worker.manifest[this.key]; 38 | } 39 | 40 | setup(operations: Operation[]) { 41 | if (!this.externalManifest || !this.externalManifest.urls) { 42 | return; 43 | } 44 | operations.push(...this 45 | .externalManifest 46 | .urls 47 | .map(url => cacheFromNetworkOp(this.worker, url.url, this.cacheKey))); 48 | } 49 | 50 | fetch(req: Request): FetchInstruction { 51 | return fetchFromCacheInstruction(this.worker, req, this.cacheKey); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app-shell/src/experimental/shell-parser/template-parser/parse5/drop-named-entities-patch.spec.ts: -------------------------------------------------------------------------------- 1 | declare var require: any; 2 | import { 3 | inject 4 | } from '@angular/core/testing'; 5 | 6 | import { Parse5TemplateParser } from './parse5-template-parser'; 7 | 8 | describe('dropped named entities patch', () => { 9 | 10 | let parser: Parse5TemplateParser; 11 | beforeEach(() => { 12 | parser = new Parse5TemplateParser(); 13 | }); 14 | 15 | describe('parse', () => { 16 | 17 | it('should not modify character references', () => { 18 | const tree = parser.parse(' '); 19 | const body = (tree.childNodes[0].childNodes[1]); 20 | expect(body.childNodes[0].value).toBe(' '); 21 | }); 22 | 23 | it('should not modify character references in attribute values', () => { 24 | const tree = parser.parse(''); 25 | const body = (tree.childNodes[0].childNodes[1]); 26 | expect(body.attrs[0].value).toBe('"e;'); 27 | }); 28 | 29 | }); 30 | 31 | describe('serialize', () => { 32 | 33 | it('should serialize named entities properly', () => { 34 | const template = parser.serialize(parser.parse(' ')); 35 | expect(template).toBe(' '); 36 | }); 37 | 38 | it('should serialize named entities in attributes properly', () => { 39 | const template = parser.serialize(parser.parse(' ')); 40 | expect(template).toBe(' '); 41 | }); 42 | 43 | }); 44 | 45 | }); 46 | -------------------------------------------------------------------------------- /service-worker/worker/src/worker/facade/cache.ts: -------------------------------------------------------------------------------- 1 | import {NgSwAdapter} from './adapter'; 2 | 3 | export interface NgSwCache { 4 | 5 | load(cache: string, req: string | Request): Promise; 6 | store(cache: string, req: string | Request, resp: Response): Promise; 7 | remove(cache: string): Promise; 8 | invalidate(cache: string, req: string | Request): Promise; 9 | keys(): Promise; 10 | keysOf(cache: string): Promise; 11 | } 12 | 13 | export class NgSwCacheImpl implements NgSwCache { 14 | constructor(private caches: CacheStorage, private adapter: NgSwAdapter) {} 15 | 16 | private normalize(req: string | Request): Request { 17 | if (typeof req == 'string') { 18 | return this.adapter.newRequest(req); 19 | } 20 | return req; 21 | } 22 | 23 | load(cache: string, req: string | Request): Promise { 24 | return this.caches.open(cache) 25 | .then(cache => cache.match(this.normalize(req))); 26 | } 27 | 28 | store(cache: string, req: string | Request, resp: Response): Promise { 29 | return this.caches.open(cache) 30 | .then(cache => cache.put(this.normalize(req), resp)); 31 | } 32 | 33 | invalidate(cache: string, req: string | Request): Promise { 34 | return this.caches.open(cache) 35 | .then(cache => cache.delete(this.normalize(req))); 36 | } 37 | 38 | remove(cache: string): Promise { 39 | return this.caches.delete(cache); 40 | } 41 | 42 | keys(): Promise { 43 | return this.caches.keys(); 44 | } 45 | 46 | keysOf(cache: string): Promise { 47 | return this.caches.open(cache) 48 | .then(cache => cache.keys()); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app-shell/src/experimental/shell-parser/template-parser/parse5/tokenizer-case-sensitivity-patch.ts: -------------------------------------------------------------------------------- 1 | var Tokenizer = require('../../../../vendor/parse5/lib/tokenizer'); 2 | 3 | // Monkey patching the lexer in order to establish 4 | // case sensitive parsing of the input templates. 5 | // This way we'll be able to use case sensitive attribute 6 | // and element selectors for stripping the content that 7 | // is not required for the App Shell. 8 | // 9 | // Since we're patching module's internals we cannot 10 | // use parse5 as dependency of the App Shell since we 11 | // won't have access to the tokenizer in order to patch 12 | // it runtime. Because of that we distribute the entire 13 | // Runtime Parser as a single bundle which includes parse5. 14 | Tokenizer.prototype.getNextToken = function () { 15 | function replaceLastWithUppercase(token: any, prop: string, cp: number) { 16 | if (token) { 17 | let char = String.fromCharCode(cp); 18 | let val = token[prop]; 19 | let last = val[val.length - 1]; 20 | if (last && last !== char) { 21 | token[prop] = val.substring(0, val.length - 1) + last.toUpperCase(); 22 | } 23 | } 24 | } 25 | while (!this.tokenQueue.length && this.active) { 26 | this._hibernationSnapshot(); 27 | const cp = this._consume(); 28 | if (!this._ensureHibernation()) { 29 | this[this.state](cp); 30 | } 31 | switch (this.state) { 32 | case 'TAG_NAME_STATE': 33 | replaceLastWithUppercase(this.currentToken, 'tagName', cp); 34 | break; 35 | case 'ATTRIBUTE_NAME_STATE': 36 | replaceLastWithUppercase(this.currentAttr, 'name', cp); 37 | break; 38 | } 39 | } 40 | return this.tokenQueue.shift(); 41 | }; 42 | 43 | -------------------------------------------------------------------------------- /app-shell/src/experimental/shell-parser/node-matcher/css-selector/css-selector.ts: -------------------------------------------------------------------------------- 1 | const _EMPTY_ATTR_VALUE = ''; 2 | const _SELECTOR_REGEXP = new RegExp( 3 | '([-\\w]+)|' + // "tag" 4 | '(?:\\.([-\\w]+))|' + // ".class" 5 | '(?:\\#([-\\w]+))|' + // "#id" 6 | '(?:\\[([-\\w]+)(?:=[\'"]?([^\\]]*)[\'"]?)?\\])', // "[name]" or "[name=value]" 7 | 'g'); 8 | 9 | export class CssSelector { 10 | element: string = null; 11 | elementId: string = null; 12 | classNames: string[] = []; 13 | attrs: {[key: string]: string} = {}; 14 | 15 | static parse(selector: string): CssSelector { 16 | var cssSelector = new CssSelector(); 17 | var match: RegExpExecArray; 18 | _SELECTOR_REGEXP.lastIndex = 0; 19 | while ((match = _SELECTOR_REGEXP.exec(selector)) !== null) { 20 | if (match[1]) { 21 | cssSelector.setElement(match[1]); 22 | } 23 | if (match[2]) { 24 | cssSelector.addClassName(match[2]); 25 | } 26 | if (match[3]) { 27 | cssSelector.addId(match[3]); 28 | } 29 | if (match[4]) { 30 | cssSelector.addAttribute(match[4], match[5]); 31 | } 32 | } 33 | return cssSelector; 34 | } 35 | 36 | setElement(element: string = null) { 37 | this.element = element; 38 | } 39 | 40 | addId(name: string) { 41 | this.elementId = name; 42 | } 43 | 44 | addAttribute(name: string, value: string) { 45 | if (value === undefined) { 46 | value = _EMPTY_ATTR_VALUE; 47 | } 48 | this.attrs[name] = value; 49 | } 50 | 51 | addClassName(name: string) { 52 | this.classNames.push(name.toLowerCase()); 53 | } 54 | } 55 | 56 | -------------------------------------------------------------------------------- /app-shell/src/experimental/shell-parser/template-parser/parse5/parse5-template-parser.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | inject 3 | } from '@angular/core/testing'; 4 | import { Parse5TemplateParser } from './parse5-template-parser'; 5 | 6 | const caseSensitiveTemplate = 7 | ` 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | Content 17 |
18 | 19 | 20 | `; 21 | 22 | const normalize = (template: string) => 23 | template 24 | .replace(/^\s+/gm, '') 25 | .replace(/\s+$/gm, '') 26 | .replace(/\n/gm, ''); 27 | 28 | describe('Parse5TemplateParser', () => { 29 | 30 | let parser = new Parse5TemplateParser(); 31 | 32 | describe('parse', () => { 33 | 34 | it('should handle capital letters', () => { 35 | const ast = parser.parse(caseSensitiveTemplate); 36 | const div = ast.childNodes[1].childNodes[2].childNodes[1]; 37 | expect(div.nodeName).toBe('Div'); 38 | expect(div.childNodes[1].attrs[0].name).toBe('Title'); 39 | }); 40 | 41 | it('should perform case sensitive parsing', () => { 42 | const ast = parser.parse(caseSensitiveTemplate); 43 | const section = ast.childNodes[1].childNodes[2].childNodes[3]; 44 | expect(section.nodeName).toBe('sEctiON'); 45 | expect(section.attrs[0].name).toBe('aTTrIbUtE'); 46 | }); 47 | 48 | }); 49 | 50 | describe('serialize', () => { 51 | 52 | it('should serialize the template keeping case sensitivity', () => { 53 | const ast = parser.parse(caseSensitiveTemplate); 54 | expect(normalize(parser.serialize(ast))) 55 | .toBe(normalize(caseSensitiveTemplate)); 56 | }); 57 | 58 | }); 59 | 60 | }); 61 | 62 | -------------------------------------------------------------------------------- /service-worker/worker/src/worker/facade/fetch.ts: -------------------------------------------------------------------------------- 1 | import {NgSwAdapter} from './adapter'; 2 | 3 | export class NgSwFetch { 4 | 5 | constructor(private scope: ServiceWorkerGlobalScope, private adapter: NgSwAdapter) {} 6 | 7 | private _request(req: Request): Promise { 8 | return this 9 | .scope 10 | .fetch(req) 11 | .catch(err => this.adapter.newResponse('', {status: 503})) 12 | } 13 | 14 | private _followRedirectIfAny(resp: Response, limit: number, origUrl: string): Response|Promise { 15 | if (!!resp['redirected']) { 16 | if (limit <= 0) { 17 | return Promise.reject(`Hit redirect limit when attempting to fetch ${origUrl}.`); 18 | } 19 | if (!resp.url) { 20 | return resp; 21 | } 22 | return this 23 | ._request(this.adapter.newRequest(resp.url)) 24 | .then(newResp => this._followRedirectIfAny(newResp, limit - 1, origUrl)); 25 | } 26 | return resp; 27 | } 28 | 29 | request(req: Request, redirectSafe: boolean = false): Promise { 30 | if (!redirectSafe) { 31 | return this._request(req); 32 | } 33 | return this._request(req).then(resp => this._followRedirectIfAny(resp, 3, req.url)); 34 | } 35 | 36 | refresh(req: string | Request): Promise { 37 | let request: Request; 38 | if (typeof req == 'string') { 39 | request = this.adapter.newRequest(this._cacheBust(req)); 40 | } else { 41 | request = this.adapter.newRequest(this._cacheBust((req).url), req); 42 | } 43 | return this.request(request, false); 44 | } 45 | 46 | private _cacheBust(url: string): string { 47 | var bust = Math.random(); 48 | if (url.indexOf('?') == -1) { 49 | return `${url}?ngsw-cache-bust=${bust}`; 50 | } 51 | return `${url}&ngsw-cache-bust=${bust}`; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /hello-mobile/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello-mobile", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "angular-cli": {}, 6 | "scripts": { 7 | "start": "ng server", 8 | "postinstall": "typings install", 9 | "lint": "tslint \"src/**/*.ts\"", 10 | "test": "ng test", 11 | "pree2e": "webdriver-manager update", 12 | "e2e": "protractor" 13 | }, 14 | "private": true, 15 | "dependencies": { 16 | "@angular/common": "2.0.0-rc.1", 17 | "@angular/compiler": "2.0.0-rc.1", 18 | "@angular/core": "2.0.0-rc.1", 19 | "@angular/http": "2.0.0-rc.1", 20 | "@angular/platform-browser": "2.0.0-rc.1", 21 | "@angular/platform-browser-dynamic": "2.0.0-rc.1", 22 | "@angular/router": "2.0.0-rc.1", 23 | "@angular2-material/core": "^2.0.0-alpha.4", 24 | "@angular2-material/progress-circle": "^2.0.0-alpha.4", 25 | "@angular2-material/toolbar": "^2.0.0-alpha.4", 26 | "es6-shim": "^0.35.0", 27 | "reflect-metadata": "0.1.3", 28 | "rxjs": "5.0.0-beta.6", 29 | "systemjs": "0.19.26", 30 | "zone.js": "^0.6.12" 31 | }, 32 | "devDependencies": { 33 | "@angular/platform-server": "2.0.0-rc.1", 34 | "@angular/router-deprecated": "2.0.0-rc.1", 35 | "@angular/service-worker": "^0.2.0", 36 | "@angular/app-shell": "^0.0.0", 37 | "angular2-broccoli-prerender": "^0.11.0", 38 | "angular2-universal": "^0.100.3", 39 | "angular2-universal-polyfills": "^0.4.1", 40 | "preboot": "^2.0.10", 41 | "angular-cli": "^1.0.0-beta.4", 42 | "codelyzer": "0.0.14", 43 | "ember-cli-inject-live-reload": "^1.4.0", 44 | "jasmine-core": "^2.4.1", 45 | "jasmine-spec-reporter": "^2.4.0", 46 | "karma": "^0.13.15", 47 | "karma-chrome-launcher": "^0.2.3", 48 | "karma-jasmine": "^0.3.8", 49 | "protractor": "^3.3.0", 50 | "ts-node": "^0.5.5", 51 | "tslint": "^3.6.0", 52 | "typescript": "^1.8.10", 53 | "typings": "^0.8.1" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /service-worker/worker/src/typings/service-worker.d.ts: -------------------------------------------------------------------------------- 1 | declare interface ServiceWorkerGlobalScope { 2 | fetch(url: string | Request); 3 | caches: CacheStorage; 4 | clients: Clients; 5 | addEventListener(type: string, listener: Function, useCapture?: boolean): void; 6 | removeEventListener(type: string, listener: Function, last?: any): void; 7 | registration: ServiceWorkerRegistration; 8 | importScripts(scripts: string): void; 9 | skipWaiting(): Promise; 10 | } 11 | 12 | declare interface InstallEvent extends ExtendableEvent {} 13 | 14 | declare interface ActivateEvent extends ExtendableEvent {} 15 | 16 | declare interface FetchEvent extends ExtendableEvent { 17 | request: Request; 18 | isReload: boolean; 19 | clientId: string; 20 | respondWith(response: Promise); 21 | } 22 | 23 | declare interface PushMessageData { 24 | arrayBuffer(): ArrayBuffer; 25 | blob(): Blob; 26 | json(): Object; 27 | text(): string; 28 | } 29 | 30 | declare interface PushEvent extends ExtendableEvent { 31 | data: PushMessageData; 32 | } 33 | 34 | declare interface CacheOptions { 35 | ignoreSearch?: boolean; 36 | ignoreMethod?: boolean; 37 | ignoreVary?: boolean; 38 | cacheName?: string; 39 | } 40 | 41 | declare interface ExtendableEvent { 42 | waitUntil(promise: Promise); 43 | } 44 | 45 | declare interface Client { 46 | postMessage(message: any, transfer?: any[]): void; 47 | readonly id: string; 48 | readonly url: string; 49 | readonly frameType: "auxiliary" | "top-level" | "nested" | "none"; 50 | } 51 | 52 | declare interface Clients { 53 | get(id: string): Promise; 54 | matchAll(options?: ClientsMatchAllOptions): Promise; 55 | openWindow(url: string): Promise; 56 | claim(): Promise; 57 | } 58 | 59 | declare interface ClientsMatchAllOptions { 60 | includeUncontrolled?: boolean; 61 | type?: "window" | "worker" | "sharedworker" | "all"; 62 | } 63 | -------------------------------------------------------------------------------- /app-shell/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": ["node_modules/codelyzer"], 3 | "rules": { 4 | "max-line-length": [true, 100], 5 | "no-inferrable-types": true, 6 | "class-name": true, 7 | "comment-format": [ 8 | true, 9 | "check-space" 10 | ], 11 | "indent": [ 12 | true, 13 | "spaces" 14 | ], 15 | "eofline": true, 16 | "no-duplicate-variable": true, 17 | "no-eval": true, 18 | "no-arg": true, 19 | "no-internal-module": true, 20 | "no-trailing-whitespace": true, 21 | "no-bitwise": true, 22 | "no-shadowed-variable": true, 23 | "no-unused-expression": true, 24 | "no-unused-variable": true, 25 | "one-line": [ 26 | true, 27 | "check-catch", 28 | "check-else", 29 | "check-open-brace", 30 | "check-whitespace" 31 | ], 32 | "quotemark": [ 33 | true, 34 | "single", 35 | "avoid-escape" 36 | ], 37 | "semicolon": [true, "always"], 38 | "typedef-whitespace": [ 39 | true, 40 | { 41 | "call-signature": "nospace", 42 | "index-signature": "nospace", 43 | "parameter": "nospace", 44 | "property-declaration": "nospace", 45 | "variable-declaration": "nospace" 46 | } 47 | ], 48 | "curly": true, 49 | "variable-name": [ 50 | true, 51 | "ban-keywords", 52 | "check-format", 53 | "allow-trailing-underscore" 54 | ], 55 | "whitespace": [ 56 | true, 57 | "check-branch", 58 | "check-decl", 59 | "check-operator", 60 | "check-separator", 61 | "check-type" 62 | ], 63 | "component-selector-name": [true, "kebab-case"], 64 | "component-selector-type": [true, "element"], 65 | "host-parameter-decorator": true, 66 | "input-parameter-decorator": true, 67 | "output-parameter-decorator": true, 68 | "attribute-parameter-decorator": true, 69 | "input-property-directive": true, 70 | "output-property-directive": true 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /hello-mobile/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": ["node_modules/codelyzer"], 3 | "rules": { 4 | "max-line-length": [true, 100], 5 | "no-inferrable-types": true, 6 | "class-name": true, 7 | "comment-format": [ 8 | true, 9 | "check-space" 10 | ], 11 | "indent": [ 12 | true, 13 | "spaces" 14 | ], 15 | "eofline": true, 16 | "no-duplicate-variable": true, 17 | "no-eval": true, 18 | "no-arg": true, 19 | "no-internal-module": true, 20 | "no-trailing-whitespace": true, 21 | "no-bitwise": true, 22 | "no-shadowed-variable": true, 23 | "no-unused-expression": true, 24 | "no-unused-variable": true, 25 | "one-line": [ 26 | true, 27 | "check-catch", 28 | "check-else", 29 | "check-open-brace", 30 | "check-whitespace" 31 | ], 32 | "quotemark": [ 33 | true, 34 | "single", 35 | "avoid-escape" 36 | ], 37 | "semicolon": [true, "always"], 38 | "typedef-whitespace": [ 39 | true, 40 | { 41 | "call-signature": "nospace", 42 | "index-signature": "nospace", 43 | "parameter": "nospace", 44 | "property-declaration": "nospace", 45 | "variable-declaration": "nospace" 46 | } 47 | ], 48 | "curly": true, 49 | "variable-name": [ 50 | true, 51 | "ban-keywords", 52 | "check-format", 53 | "allow-trailing-underscore" 54 | ], 55 | "whitespace": [ 56 | true, 57 | "check-branch", 58 | "check-decl", 59 | "check-operator", 60 | "check-separator", 61 | "check-type" 62 | ], 63 | "component-selector-name": [true, "kebab-case"], 64 | "component-selector-type": [true, "element"], 65 | "host-parameter-decorator": true, 66 | "input-parameter-decorator": true, 67 | "output-parameter-decorator": true, 68 | "attribute-parameter-decorator": true, 69 | "input-property-directive": true, 70 | "output-property-directive": true 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /app-shell/src/experimental/shell-parser/node-matcher/css-selector/css-selector.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | inject 3 | } from '@angular/core/testing'; 4 | import { CssSelector } from './css-selector'; 5 | 6 | describe('CssSelector', () => { 7 | 8 | it('should support id selectors', () => { 9 | const result = CssSelector.parse('#elemId'); 10 | expect(result.elementId).toBe('elemId'); 11 | }); 12 | 13 | it('should support element selectors', () => { 14 | const result = CssSelector.parse('div'); 15 | expect(result.element).toBe('div'); 16 | }); 17 | 18 | it('should support class selectors', () => { 19 | const result = CssSelector.parse('.foo'); 20 | expect(result.classNames).toBeTruthy(); 21 | expect(result.classNames.length).toBe(1); 22 | expect(result.classNames[0]).toBe('foo'); 23 | }); 24 | 25 | describe('attribute selectors', () => { 26 | 27 | it('should support attributes with no values', () => { 28 | const result = CssSelector.parse('[title]'); 29 | expect(result.attrs['title']).toBe(''); 30 | }); 31 | 32 | it('should support attributes with values', () => { 33 | const result = CssSelector.parse('[title=random title]'); 34 | expect(result.attrs['title']).toBe('random title'); 35 | }); 36 | 37 | }); 38 | 39 | describe('complex selectors', () => { 40 | 41 | it('should support complex selectors', () => { 42 | const result = CssSelector.parse('div.foo[attr]'); 43 | expect(result.element).toBe('div'); 44 | expect(result.classNames[0]).toBe('foo'); 45 | expect(result.attrs['attr']).toBe(''); 46 | }); 47 | 48 | it('should support complex selectors with terminals in random order', () => { 49 | const result = CssSelector.parse('.foo[attr=bar].qux#baz'); 50 | expect(result.element).toBe(null); 51 | expect(result.attrs['attr']).toBe('bar'); 52 | expect(result.elementId).toBe('baz'); 53 | expect(result.classNames).toEqual(['foo', 'qux']); 54 | }); 55 | 56 | }); 57 | }); 58 | 59 | -------------------------------------------------------------------------------- /hello-mobile/src/system-config.ts: -------------------------------------------------------------------------------- 1 | /*********************************************************************************************** 2 | * User Configuration. 3 | **********************************************************************************************/ 4 | /** Map relative paths to URLs. */ 5 | const map: any = { 6 | '@angular2-material': 'vendor/@angular2-material' 7 | }; 8 | 9 | /** User packages configuration. */ 10 | const packages: any = { 11 | '@angular2-material/toolbar': { 12 | defaultExtension: 'js', 13 | main: 'toolbar.js' 14 | }, 15 | '@angular2-material/progress-circle': { 16 | defaultExtension: 'js', 17 | main: 'progress-circle.js' 18 | } 19 | }; 20 | 21 | //////////////////////////////////////////////////////////////////////////////////////////////// 22 | /*********************************************************************************************** 23 | * Everything underneath this line is managed by the CLI. 24 | **********************************************************************************************/ 25 | const barrels: string[] = [ 26 | // Angular specific barrels. 27 | '@angular/core', 28 | '@angular/common', 29 | '@angular/compiler', 30 | '@angular/http', 31 | '@angular/router', 32 | '@angular/platform-browser', 33 | '@angular/platform-browser-dynamic', 34 | '@angular/app-shell', 35 | 36 | // Thirdparty barrels. 37 | 'rxjs', 38 | 39 | // App specific barrels. 40 | 'app', 41 | 'app/shared', 42 | 'app/+hello', 43 | /** @cli-barrel */ 44 | ]; 45 | 46 | const cliSystemConfigPackages: any = {}; 47 | barrels.forEach((barrelName: string) => { 48 | cliSystemConfigPackages[barrelName] = { main: 'index' }; 49 | }); 50 | 51 | /** Type declaration for ambient System. */ 52 | declare var System: any; 53 | 54 | // Apply the CLI SystemJS configuration. 55 | System.config({ 56 | map: { 57 | '@angular': 'vendor/@angular', 58 | 'rxjs': 'vendor/rxjs', 59 | 'main': 'main.js' 60 | }, 61 | packages: cliSystemConfigPackages 62 | }); 63 | 64 | // Apply the user's configuration. 65 | System.config({ map, packages }); 66 | -------------------------------------------------------------------------------- /service-worker/worker/src/test/e2e/harness/server/server.ts: -------------------------------------------------------------------------------- 1 | import express = require('express'); 2 | 3 | export function create(port: number, harnessPath: string): Promise { 4 | return new Promise((resolve, reject) => { 5 | let server; 6 | server = new Server(port, harnessPath, () => { 7 | console.log('SERVER RUNNING'); 8 | resolve(server); 9 | }); 10 | }); 11 | } 12 | 13 | export class Server { 14 | app: any; 15 | server: any; 16 | 17 | responses: Object = {}; 18 | delays: Object = {}; 19 | 20 | constructor(port: number, harnessPath: string, readyCallback: Function) { 21 | this.app = express(); 22 | this.app.use(express.static(harnessPath)); 23 | this.server = this.app.listen(port, () => readyCallback()); 24 | this.app.get('/index2.html', (req, resp) => { 25 | resp.redirect(301, '/index.html'); 26 | }); 27 | this.app.post('/ngsw-log', (req, resp) => { 28 | let content = ''; 29 | req.on('data', data => content += data); 30 | req.on('end', () => console.log('SW: ' + content)); 31 | resp.send('ok'); 32 | }); 33 | } 34 | 35 | addResponse(url: string, response: string, delayMs?: number) { 36 | let urlExisted = this.responses.hasOwnProperty(url); 37 | 38 | // Add the response. 39 | this.responses[url] = response; 40 | this.delays[url] = (!!delayMs ? delayMs : undefined); 41 | 42 | if (urlExisted) { 43 | // A handler for this URL is already registered. 44 | return; 45 | } 46 | 47 | // Register a handler for the URL, that doesn't use the response 48 | // passed but instead return 49 | this.app.get(url, (req, resp) => { 50 | let response = this.responses[url]; 51 | let delay = this.delays[url]; 52 | if (!response) { 53 | return; 54 | } 55 | if (!!delay) { 56 | setTimeout(() => resp.send(response), delay); 57 | } else { 58 | resp.send(response); 59 | } 60 | }); 61 | } 62 | 63 | clearResponses() { 64 | this.responses = {}; 65 | } 66 | 67 | shutdown() { 68 | this.server.close(); 69 | } 70 | } -------------------------------------------------------------------------------- /guides/cli-setup.md: -------------------------------------------------------------------------------- 1 | # Create a Progressive Web App with [Angular CLI](https://cli.angular.io) 2 | 3 | Progressive Web Apps are web apps that combine the benefits of the Web 4 | with the capabilities and performance of native Apps. Angular makes it 5 | easy to get started building progressive Web apps with our Angular Mobile 6 | Toolkit, which is integrated with [Angular CLI](https://cli.angular.io). 7 | 8 | These guides will help get started on the right foot building Progressive 9 | Web Apps, so you can focus more on building a great user experience, and 10 | less on getting the underlying tooling and technology set up. 11 | 12 | To get started, install Angular CLI from [npm](https://www.npmjs.com/). 13 | 14 | ``` 15 | $ npm install -g angular-cli 16 | ``` 17 | 18 | (Note: this recipe is based on version 1.0.0-beta.4 of Angular CLI.) 19 | 20 | Then create a new project: 21 | 22 | ``` 23 | $ ng new hello-mobile --mobile 24 | $ cd hello-mobile 25 | ``` 26 | 27 | Then serve the app: 28 | 29 | ``` 30 | $ ng serve 31 | ``` 32 | 33 | Navigate to [localhost:4200](http://localhost:4200) in your browser, and you should see a simple page that says "hello-mobile works!". 34 | 35 | ## --mobile 36 | 37 | Passing the `--mobile` flag when creating a new app will set up a few extra things 38 | to help get your [Progressive Web App](https://developers.google.com/web/progressive-web-apps?hl=en) 39 | started on the right foot: 40 | * A **Web Application Manifest** to give browsers information to properly install your app 41 | to the home screen. 42 | * A build step to generate an **App Shell** from your app's root component. 43 | * A **Service Worker** script to automatically cache your app for fast loading, 44 | with or without an internet connection. Note: the Service Worker is only installed in production mode, i.e. via `ng serve --prod` or `ng build --prod`. 45 | 46 | We'll go deeper into these concepts in subsequent guides. 47 | 48 | For reference, see the example app created by Angular CLI in this repository at [/hello-mobile](../hello-mobile) 49 | 50 | --- 51 | 52 | ## [Next, let's learn how to take advantage of the Web App Manifest.](./web-app-manifest.md) 53 | -------------------------------------------------------------------------------- /service-worker/worker/src/build/gulp.ts: -------------------------------------------------------------------------------- 1 | declare var require, Buffer; 2 | 3 | const stream = require('stream'); 4 | const Vinyl = require('vinyl'); 5 | const crypto = require('crypto'); 6 | 7 | export interface GulpAddStaticFileOptions { 8 | manifestKey?: string; 9 | } 10 | 11 | export function gulpGenerateManifest() { 12 | let readable = new stream.Readable({objectMode: true}); 13 | readable._read = () => { 14 | readable.push(new Vinyl({ 15 | cwd: '/', 16 | base: '/', 17 | path: '/ngsw-manifest.json', 18 | contents: new Buffer('{}'), 19 | })); 20 | readable.push(null); 21 | }; 22 | return readable; 23 | } 24 | 25 | export function gulpAddStaticFiles(files: any, options: GulpAddStaticFileOptions = {}) { 26 | let manifestTransform = new stream.Transform({objectMode: true}); 27 | let singleFile = true; 28 | 29 | manifestTransform._transform = (manifestFile, _, callback) => { 30 | if (!singleFile) { 31 | throw new Error('Only one manifest allowed.'); 32 | } 33 | let manifest = JSON.parse(manifestFile.contents.toString('utf8')); 34 | let staticConfig = { 35 | urls: {} 36 | }; 37 | let property = options.manifestKey || 'static'; 38 | manifest[property] = staticConfig; 39 | 40 | // Look for ignored patterns in the manifest. 41 | let ignored: RegExp[] = []; 42 | const ignoreKey = `${options.manifestKey}.ignore`; 43 | if (manifest.hasOwnProperty(ignoreKey)) { 44 | ignored.push(...(manifest[ignoreKey] as string[]) 45 | .map(regex => new RegExp(regex))); 46 | delete manifest[ignoreKey]; 47 | } 48 | 49 | files.on('data', file => { 50 | const url = '/' + file.relative; 51 | if (ignored.some(regex => regex.test(url))) { 52 | return; 53 | } 54 | staticConfig.urls['/' + file.relative] = sha1(file.contents); 55 | }); 56 | files.on('end', () => { 57 | manifestFile.contents = new Buffer(JSON.stringify(manifest, null, 2)); 58 | callback(null, manifestFile); 59 | }); 60 | 61 | singleFile = false; 62 | }; 63 | 64 | return manifestTransform; 65 | } 66 | 67 | function sha1(buffer): string { 68 | const hash = crypto.createHash('sha1'); 69 | hash.update(buffer); 70 | return hash.digest('hex'); 71 | } -------------------------------------------------------------------------------- /service-worker/worker/src/test/e2e/harness/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Service Worker Test Harness 5 | 6 | 7 | 8 | 9 | 60 | 61 | 62 | 63 | 66 | 67 | -------------------------------------------------------------------------------- /service-worker/worker/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@angular/service-worker", 3 | "version": "1.0.0-beta.15", 4 | "description": "Experimental service worker by the Angular Mobile team", 5 | "main": "bundles/service-worker.umd.js", 6 | "module": "index.js", 7 | "typings": "index.d.ts", 8 | "scripts": { 9 | "build": "gulp build", 10 | "test": "gulp test" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/angular/mobile-toolkit.git" 15 | }, 16 | "author": "", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/angular/mobile-toolkit/issues" 20 | }, 21 | "homepage": "https://mobile.angular.io", 22 | "devDependencies": { 23 | "@angular/common": "^4.1.3", 24 | "@angular/compiler": "^4.1.3", 25 | "@angular/compiler-cli": "^4.1.3", 26 | "@angular/core": "^4.1.3", 27 | "@angular/forms": "^4.1.3", 28 | "@angular/platform-browser": "^4.1.3", 29 | "@angular/platform-browser-dynamic": "^4.1.3", 30 | "@types/base64-js": "^1.1.4", 31 | "@types/express": "^4.0.33", 32 | "@types/jasmine": "^2.2.34", 33 | "@types/node": "^6.0.42", 34 | "@types/protractor": "^1.5.20", 35 | "@types/selenium-webdriver": "2.44.26", 36 | "copy-webpack-plugin": "^3.0.1", 37 | "express": "^4.13.4", 38 | "gulp": "^3.9.0", 39 | "gulp-clean": "^0.3.1", 40 | "gulp-jasmine": "^2.4.0", 41 | "gulp-rename": "^1.2.2", 42 | "gulp-replace": "^0.5.4", 43 | "gulp-rimraf": "^0.2.0", 44 | "gulp-uglify": "^2.0.0", 45 | "jasmine": "~2.4.1", 46 | "protractor": "^4.0.10", 47 | "reflect-metadata": "0.1.2", 48 | "rimraf": "^2.5.2", 49 | "rollup": "^0.34.13", 50 | "rollup-plugin-commonjs": "^4.1.0", 51 | "rollup-plugin-node-resolve": "^2.0.0", 52 | "run-sequence": "^1.1.5", 53 | "rxjs": "^5.4.0", 54 | "systemjs": "^0.19.17", 55 | "ts-node": "^0.7.3", 56 | "typescript": "^2.2.2", 57 | "uglify-js": "^2.7.3", 58 | "web-push": "^3.2.2", 59 | "webdriver-manager": "^10.2.6", 60 | "webpack": "^2.1.0-beta.22", 61 | "zone.js": "^0.8.12" 62 | }, 63 | "dependencies": { 64 | "base64-js": "^1.1.2", 65 | "jshashes": "^1.0.5" 66 | }, 67 | "peerDependencies": { 68 | "@angular/core": "^2.3.1 || >=4.0.0-beta <5.0.0", 69 | "rxjs": ">=5.0.0-beta.12 <6.0.0" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /app-shell/src/experimental/shell-parser/node-visitor/resource-inline/resource-inline-visitor.ts: -------------------------------------------------------------------------------- 1 | import {ASTNode} from '../../ast'; 2 | import {NodeVisitor} from '../node-visitor'; 3 | import {WorkerScope} from '../../context'; 4 | 5 | const URL_REGEXP = /:\s+url\(['"]?(.*?)['"]?\)/gmi; 6 | 7 | export abstract class ResourceInlineVisitor extends NodeVisitor { 8 | 9 | constructor(private scope: WorkerScope, private inlineExtensions: string[]) { 10 | super(); 11 | } 12 | 13 | inlineAssets(style: string) { 14 | let urls = this.getImagesUrls(style); 15 | urls = urls.filter((url: string, idx: number) => urls.indexOf(url) === idx); 16 | return this.processInline(urls, style) 17 | .then((content: string) => content); 18 | } 19 | 20 | protected getImagesUrls(styles: string): string[] { 21 | URL_REGEXP.lastIndex = 0; 22 | let match: string[]; 23 | const result: string[] = []; 24 | while ((match = URL_REGEXP.exec(styles)) !== null) { 25 | const url = match[1]; 26 | if (this.supportedExtension(url)) { 27 | result.push(url); 28 | } 29 | } 30 | return result; 31 | } 32 | 33 | private supportedExtension(url: string) { 34 | return this.inlineExtensions.some((ext: string) => new RegExp(`${ext}$`).test(url)); 35 | } 36 | 37 | protected processInline(urls: string[], styles: string): Promise { 38 | const processResponse = (response: Response): Promise => { 39 | if (response && response.ok) { 40 | return response.arrayBuffer() 41 | .then((arr: ArrayBuffer) => [ 42 | btoa(String.fromCharCode.apply(null, new Uint8Array(arr))), 43 | // Can contain whitespace: 'image/jpg; charset=utf-8' 44 | response.headers.get('content-type').replace(/\s/g, '') 45 | ]); 46 | } else { 47 | return null; 48 | } 49 | }; 50 | return Promise.all(urls.map((url: string) => this.scope.fetch(url).catch(() => null))) 51 | .then((responses: any[]) => Promise.all(responses.map(processResponse))) 52 | .then((images: string[][]) => { 53 | return images.map((img: string[]) => img ? `data:${img[1]};base64,${img[0]}` : null) 54 | .reduce((content: string, img: string, idx: number) => 55 | img ? content.replace(new RegExp(urls[idx], 'g'), img) : content, styles); 56 | }); 57 | } 58 | 59 | } 60 | 61 | -------------------------------------------------------------------------------- /app-shell/src/experimental/shell-parser/template-parser/parse5/drop-named-entities-patch.ts: -------------------------------------------------------------------------------- 1 | var Tokenizer = require('../../../../vendor/parse5/lib/tokenizer'); 2 | var Serializer = require('../../../../vendor/parse5/lib/serializer'); 3 | var CP = require('../../../../vendor/parse5/lib/common/unicode').CODE_POINTS; 4 | var STATES_MAP: {[key: string]: string} = { 5 | CHARACTER_REFERENCE_IN_DATA_STATE: 'DATA_STATE', 6 | CHARACTER_REFERENCE_IN_RCDATA_STATE: 'RCDATA_STATE', 7 | CHARACTER_REFERENCE_IN_ATTRIBUTE_VALUE_STATE: 'DATA_STATE' 8 | }; 9 | 10 | function isAsciiDigit(cp: number) { 11 | return cp >= CP.DIGIT_0 && cp <= CP.DIGIT_9; 12 | } 13 | 14 | function isWhitespace(cp: number) { 15 | return cp === CP.SPACE || cp === CP.LINE_FEED || cp === CP.TABULATION || cp === CP.FORM_FEED; 16 | } 17 | 18 | function isAsciiUpper(cp: number) { 19 | return cp >= CP.LATIN_CAPITAL_A && cp <= CP.LATIN_CAPITAL_Z; 20 | } 21 | 22 | function isAsciiLower(cp: number) { 23 | return cp >= CP.LATIN_SMALL_A && cp <= CP.LATIN_SMALL_Z; 24 | } 25 | 26 | function isAsciiAlphaNumeric(cp: number) { 27 | return isAsciiDigit(cp) || isAsciiUpper(cp) || isAsciiLower(cp); 28 | } 29 | 30 | function isDigit(cp: number, isHex: boolean) { 31 | return isAsciiDigit(cp) || isHex && (cp >= CP.LATIN_CAPITAL_A && cp <= CP.LATIN_CAPITAL_F || 32 | cp >= CP.LATIN_SMALL_A && cp <= CP.LATIN_SMALL_F); 33 | } 34 | 35 | Serializer.escapeString = function (str: string) { 36 | return str; 37 | }; 38 | 39 | // Monkey patching this method intents to decrease the bundle size 40 | // of the runtime parser by allowing us to drop the "named_entity_trie". 41 | Tokenizer.prototype._consumeCharacterReference = function (startCp: number, inAttr: boolean) { 42 | if (isWhitespace(startCp) || startCp === CP.GREATER_THAN_SIGN || 43 | startCp === CP.AMPERSAND || startCp === this.additionalAllowedCp || startCp === CP.EOF) { 44 | this._unconsume(); 45 | return null; 46 | } 47 | if (startCp === CP.NUMBER_SIGN) { 48 | var isHex = false; 49 | var nextCp = this._lookahead(); 50 | 51 | if (nextCp === CP.LATIN_SMALL_X || nextCp === CP.LATIN_CAPITAL_X) { 52 | this._consume(); 53 | isHex = true; 54 | } 55 | nextCp = this._lookahead(); 56 | if (nextCp !== CP.EOF && isDigit(nextCp, isHex)) 57 | return [this._consumeNumericEntity(isHex)]; 58 | this._unconsumeSeveral(isHex ? 2 : 1); 59 | return null; 60 | } 61 | return this._reconsumeInState(STATES_MAP[this.state]); 62 | }; 63 | -------------------------------------------------------------------------------- /service-worker/worker/src/worker/bootstrap.ts: -------------------------------------------------------------------------------- 1 | import {PluginFactory} from './api'; 2 | import {NgSwAdapter, NgSwCacheImpl, NgSwEvents, NgSwFetch, BrowserClock} from './facade'; 3 | import {Driver} from './driver'; 4 | import {Verbosity, LogHandler, LOGGER} from './logging'; 5 | 6 | declare var global; 7 | 8 | const PAGE_SCOPE_FROM_SW_SCOPE = /^(https?:\/\/[^/]+)(\/.*)?$/; 9 | 10 | // The scope is the global object. 11 | const scope: ServiceWorkerGlobalScope = ((typeof self !== 'undefined') ? self : global as any) as ServiceWorkerGlobalScope; 12 | 13 | function copyRequest(req: Request): Object { 14 | let copy = { 15 | method: req.method, 16 | headers: req.headers, 17 | credentials: req.credentials, 18 | cache: req.cache, 19 | redirect: req.redirect, 20 | referrer: req.referrer, 21 | }; 22 | if (req.mode.toString() !== 'navigate') { 23 | copy['mode'] = req.mode; 24 | } 25 | return copy; 26 | } 27 | 28 | class NgSwBrowserAdapter implements NgSwAdapter { 29 | private _scope: string; 30 | 31 | constructor() { 32 | this._scope = PAGE_SCOPE_FROM_SW_SCOPE.exec(scope.registration.scope)[1]; 33 | } 34 | newRequest(req: string | Request, init?: Object): Request { 35 | if (init && init instanceof Request) { 36 | init = copyRequest(init); 37 | } 38 | return new Request(req, init); 39 | } 40 | 41 | newResponse(body: string | Blob, init?: ResponseInit): Response { 42 | return new Response(body, init); 43 | } 44 | 45 | get scope(): string { 46 | return this._scope; 47 | } 48 | } 49 | 50 | export interface BootstrapOptions { 51 | manifestUrl?: string; 52 | plugins?: PluginFactory[]; 53 | logLevel?: Verbosity; 54 | logHandlers?: LogHandler[]; 55 | } 56 | 57 | export function bootstrapServiceWorker(options?: BootstrapOptions): Driver { 58 | const manifestUrl = (options && options.manifestUrl) || '/ngsw-manifest.json'; 59 | const plugins = (options && options.plugins) || []; 60 | 61 | const adapter = new NgSwBrowserAdapter(); 62 | const cache = new NgSwCacheImpl(scope.caches, adapter); 63 | const events = new NgSwEvents(scope); 64 | const fetch = new NgSwFetch(scope, adapter); 65 | const clock = new BrowserClock(); 66 | LOGGER.setVerbosity(options.logLevel); 67 | if (!!options.logHandlers) { 68 | LOGGER.messages = (entry => options.logHandlers.forEach(handler => handler.handle(entry))); 69 | } 70 | LOGGER.release(); 71 | return new Driver(manifestUrl, plugins, scope, adapter, cache, events, fetch, clock); 72 | } 73 | -------------------------------------------------------------------------------- /app-shell/src/experimental/shell-parser/node-visitor/template-strip-visitor.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | inject 3 | } from '@angular/core/testing'; 4 | 5 | import {ASTNode} from '../ast'; 6 | import {cssNodeMatcherFactory} from '../node-matcher'; 7 | import {MockWorkerScope, MockResponse} from '../testing'; 8 | import {TemplateStripVisitor} from './'; 9 | 10 | describe('TemplateStripVisitor', () => { 11 | 12 | let astRoot: ASTNode; 13 | 14 | beforeEach(() => { 15 | astRoot = { 16 | nodeName: 'div', 17 | attrs: [], 18 | childNodes: [] 19 | }; 20 | const div1: ASTNode = { nodeName: 'div', attrs: [{ name: 'class', value: 'foo' }] }; 21 | const section: ASTNode = { 22 | nodeName: 'section', 23 | attrs: [], 24 | parentNode: astRoot, 25 | childNodes: [ 26 | div1 27 | ] 28 | }; 29 | div1.parentNode = section; 30 | const span1: ASTNode = { nodeName: 'span', childNodes: [], attrs: [], parentNode: astRoot }; 31 | const div2: ASTNode = { 32 | nodeName: 'div', 33 | parentNode: span1, 34 | attrs: [{ name: 'class', value: 'foo' }] 35 | }; 36 | span1.childNodes.push(div2); 37 | const span2: ASTNode = { nodeName: 'span', attrs: [], parentNode: astRoot } 38 | astRoot.childNodes.push(section); 39 | astRoot.childNodes.push(span1); 40 | astRoot.childNodes.push(span2); 41 | }); 42 | 43 | it('should strip the root when match', (done: any) => { 44 | let stripper = new TemplateStripVisitor(cssNodeMatcherFactory('div')); 45 | stripper.visit(astRoot) 46 | .then((astNode: ASTNode) => { 47 | expect(astNode).toBe(null); 48 | }); 49 | done(); 50 | }); 51 | 52 | it('should strip all nodes matching a selector', (done: any) => { 53 | let stripper = new TemplateStripVisitor(cssNodeMatcherFactory('span')); 54 | stripper.visit(astRoot) 55 | .then((astNode: ASTNode) => { 56 | expect(astNode.childNodes.length).toBe(1); 57 | done(); 58 | }); 59 | }); 60 | 61 | it('should strip nodes in different areas of the tree', (done: any) => { 62 | let stripper = new TemplateStripVisitor(cssNodeMatcherFactory('.foo')); 63 | stripper.visit(astRoot) 64 | .then((astNode: ASTNode) => { 65 | // div > section > div.foo to be removed 66 | expect(astNode.childNodes[0].childNodes.length).toBe(0); 67 | // div > span > div.foo to be removed 68 | expect(astNode.childNodes[1].childNodes.length).toBe(0); 69 | done(); 70 | }); 71 | }); 72 | 73 | }); 74 | 75 | -------------------------------------------------------------------------------- /app-shell/README.md: -------------------------------------------------------------------------------- 1 | # @angular/app-shell 2 | 3 | This is a simple library to make it easy to show/hide certain elements 4 | in a pre-rendered component. See the [App Shell Guide](../guides/app-shell.md) 5 | for more information about how it's used to build App Shell components. 6 | 7 | # Install 8 | 9 | `$ npm install @angular/app-shell` 10 | 11 | # Usage 12 | 13 | In the providers setup for the prerender environment, import and add 14 | the `APP_SHELL_BUILD_PROVIDERS`. For now, this provides a single `boolean` 15 | provider, `IS_PRERENDER`, which can be injected anywhere in the application 16 | to change JavaScript logic depending on the execution context. 17 | 18 | `main-app-shell.ts` (or other entry point): 19 | ```typescript 20 | // In pre-render context, such as main-app-shell.ts 21 | import { APP_SHELL_BUILD_PROVIDERS } from '@angular/app-shell'; 22 | // ... 23 | providers: [ 24 | APP_SHELL_BUILD_PROVIDERS, 25 | //... 26 | ] 27 | ``` 28 | 29 | There's a similar set of providers for the runtime environment, which 30 | should be added to your providers in main.ts: `APP_SHELL_RUNTIME_PROVIDERS`. 31 | 32 | `main.ts`: 33 | ```typescript 34 | import { APP_SHELL_RUNTIME_PROVIDERS } from '@angular/app-shell'; 35 | //... 36 | bootstrap(AppComponent, APP_SHELL_RUNTIME_PROVIDERS); 37 | ``` 38 | 39 | Then in the component(s) that will be shared between pre-render and runtime, 40 | add the `APP_SHELL_DIRECTIVES` and `IS_PRERENDER`. `APP_SHELL_DIRECTIVES` 41 | comes with two structural directives: 42 | 43 | * `*shellRender` designates a component that should **only** be rendered in the App Shell, 44 | and **not** rendered at runtime. 45 | * `*shellNoRender` designates a component that should **not** be rendered in the App Shell, 46 | and **only** rendered at runtime. 47 | 48 | The directives could be thought of as `*ngIf="isPrerender"` and `*ngIf="!isPrerender"`. 49 | 50 | `app.component.ts`: 51 | ```typescript 52 | import { Inject, Component } from '@angular/core'; 53 | import { Routes } from '@angular/router'; 54 | import { APP_SHELL_DIRECTIVES, IS_PRERENDER } from '@angular/app-shell'; 55 | //... 56 | @Component({ 57 | selector: 'app-component', 58 | template: ` 59 | 60 | Hello 61 | 62 | 63 | 64 | `, 65 | directives: [ APP_SHELL_DIRECTIVES, ROUTER_DIRECTIVES] 66 | }) 67 | @Routes([{ 68 | // ... 69 | }]) 70 | class AppComponent { 71 | constructor(@Inject(IS_PRERENDER) isPrerender:boolean) { 72 | if (!isPrerender) { 73 | // fetch some data 74 | } 75 | } 76 | } 77 | ``` 78 | 79 | Kudos to @mgechev for pairing on this library, and letting @jeffbcross get the git commit credit. 80 | -------------------------------------------------------------------------------- /app-shell/src/experimental/shell-parser/node-matcher/css-node-matcher.ts: -------------------------------------------------------------------------------- 1 | import {NodeMatcher} from './node-matcher'; 2 | import {ASTNode, ASTAttribute} from '../ast'; 3 | import {CssSelector} from './css-selector'; 4 | 5 | export const cssNodeMatcherFactory = (selector: string) => { 6 | return new CssNodeMatcher(CssSelector.parse(selector)); 7 | }; 8 | 9 | export class CssNodeMatcher extends NodeMatcher { 10 | constructor(private selector: CssSelector) { 11 | super(); 12 | } 13 | 14 | match(node: ASTNode): boolean { 15 | return this.matchElement(node) && this.matchAttributes(node) && 16 | this.matchId(node) && this.matchClassNames(node); 17 | } 18 | 19 | private matchElement(node: ASTNode) { 20 | return !this.selector.element || this.selector.element === node.nodeName; 21 | } 22 | 23 | private matchAttributes(node: ASTNode) { 24 | const selectorAttrs = this.selector.attrs; 25 | const nodeAttrs = (node.attrs || []).reduce((accum: any, attr: ASTAttribute) => { 26 | accum[attr.name] = attr.value; 27 | return accum; 28 | }, {}); 29 | const selectorAttrNames = Object.keys(selectorAttrs); 30 | if (!selectorAttrNames.length) { 31 | return true; 32 | } 33 | return selectorAttrNames.reduce((accum: boolean, name: string) => { 34 | return accum && (selectorAttrs[name] === nodeAttrs[name] || 35 | // nodeAttrs[name] cannot be undefined after parsing 36 | // since it'll be normalized to name="" if empty 37 | (nodeAttrs[name] !== undefined && selectorAttrs[name] === '')); 38 | }, true); 39 | } 40 | 41 | private matchClassNames(node: ASTNode) { 42 | const selectorClasses = this.selector.classNames; 43 | if (!selectorClasses.length) { 44 | return true; 45 | } 46 | const classAttr = this.getAttribute(node, 'class'); 47 | // We have selector by class but we don't have class attribute of the node 48 | if (!classAttr) { 49 | return false; 50 | } 51 | const classMap = classAttr.value 52 | .toLowerCase().split(' ') 53 | .reduce((accum: any, val: string) => { 54 | accum[val] = true; 55 | return accum; 56 | }, {}); 57 | return selectorClasses.reduce((accum: boolean, val: string) => { 58 | return accum && !!classMap[val]; 59 | }, true); 60 | } 61 | 62 | private matchId(node: ASTNode) { 63 | const id = this.selector.elementId; 64 | if (!id) { 65 | return true; 66 | } 67 | const idAttr = this.getAttribute(node, 'id'); 68 | if (idAttr && idAttr.value === this.selector.elementId) { 69 | return true; 70 | } else { 71 | return false; 72 | } 73 | } 74 | 75 | private getAttribute(node: ASTNode, attrName: string) { 76 | return (node.attrs || []) 77 | .filter((attr: ASTAttribute) => 78 | attr.name === attrName).pop(); 79 | } 80 | } 81 | 82 | -------------------------------------------------------------------------------- /app-shell/src/app/shell.spec.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | import {TestBed} from '@angular/core/testing'; 3 | import {AppShellModule} from './module'; 4 | import {ShellNoRender, ShellRender} from './shell'; 5 | 6 | export default function () { 7 | describe('ShellNoRender Directive', () => { 8 | @Component({ 9 | selector: 'test-component', 10 | template: `
Rendered
`, 11 | }) 12 | class NoRenderTestComponent {} 13 | 14 | it('should NOT render the element at prerender time', () => { 15 | const fixture = TestBed 16 | .configureTestingModule({ 17 | declarations: [NoRenderTestComponent], 18 | imports: [AppShellModule.prerender()] 19 | }) 20 | .createComponent(NoRenderTestComponent); 21 | fixture.detectChanges(); 22 | expect(fixture.debugElement.childNodes.length).toBe(1); 23 | expect(fixture.debugElement.childNodes[0].nativeNode.data).toBe('template bindings={}'); 24 | }); 25 | it('should render the element at runtime', () => { 26 | const fixture = TestBed 27 | .configureTestingModule({ 28 | declarations: [NoRenderTestComponent], 29 | imports: [AppShellModule.runtime()] 30 | }) 31 | .createComponent(NoRenderTestComponent); 32 | fixture.detectChanges(); 33 | expect(fixture.debugElement.childNodes.length).toBe(2); 34 | expect(fixture.debugElement.childNodes[0].nativeNode.data).toBe('template bindings={}'); 35 | expect(fixture.debugElement.childNodes[1].nativeNode.name).toBe('div'); 36 | }); 37 | }); 38 | 39 | describe('ShellRender Directive', () => { 40 | @Component({ 41 | selector: 'test-component', 42 | template: `
Rendered
`, 43 | }) 44 | class RenderTestComponent {} 45 | 46 | it('should render the element at prerender time', () => { 47 | const fixture = TestBed 48 | .configureTestingModule({ 49 | declarations: [RenderTestComponent], 50 | imports: [AppShellModule.prerender()], 51 | }) 52 | .createComponent(RenderTestComponent); 53 | fixture.detectChanges(); 54 | expect(fixture.debugElement.childNodes.length).toBe(2); 55 | expect(fixture.debugElement.childNodes[0].nativeNode.data).toBe('template bindings={}'); 56 | expect(fixture.debugElement.childNodes[1].nativeNode.name).toBe('div'); 57 | }); 58 | it('should NOT render the element at runtime', () => { 59 | const fixture = TestBed 60 | .configureTestingModule({ 61 | declarations: [RenderTestComponent], 62 | imports: [AppShellModule.runtime()], 63 | }) 64 | .createComponent(RenderTestComponent); 65 | fixture.detectChanges(); 66 | expect(fixture.debugElement.childNodes.length).toBe(1); 67 | expect(fixture.debugElement.childNodes[0].nativeNode.data).toBe('template bindings={}'); 68 | }); 69 | }); 70 | } 71 | -------------------------------------------------------------------------------- /service-worker/worker/src/plugins/push/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Operation, 3 | Plugin, 4 | PluginFactory, 5 | VersionWorker, 6 | } from '@angular/service-worker/worker'; 7 | 8 | interface PushManifest { 9 | showNotifications?: boolean; 10 | backgroundOnly?: boolean; 11 | } 12 | const EMPTY_MANIFEST: PushManifest = {}; 13 | 14 | const NOTIFICATION_OPTION_NAMES = [ 15 | 'actions', 16 | 'body', 17 | 'dir', 18 | 'icon', 19 | 'lang', 20 | 'renotify', 21 | 'requireInteraction', 22 | 'tag', 23 | 'vibrate', 24 | 'data' 25 | ]; 26 | 27 | export function Push(): PluginFactory { 28 | return (worker: VersionWorker) => new PushImpl(worker); 29 | } 30 | 31 | export class PushImpl implements Plugin { 32 | private streams: number[] = []; 33 | private buffer: Object[] = []; 34 | 35 | private get pushManifest(): PushManifest { 36 | return this.worker.manifest['push'] as PushManifest || EMPTY_MANIFEST; 37 | } 38 | 39 | constructor(private worker: VersionWorker) {} 40 | 41 | setup(ops: Operation[]): void {} 42 | 43 | message(message: any, id: number): void { 44 | switch (message['cmd']) { 45 | case 'push': 46 | this.streams.push(id); 47 | if (this.buffer !== null) { 48 | this.buffer.forEach(message => this.worker.sendToStream(id, message)); 49 | this.buffer = null; 50 | } 51 | break; 52 | } 53 | } 54 | 55 | messageClosed(id: number): void { 56 | const index = this.streams.indexOf(id); 57 | if (index === -1) { 58 | return; 59 | } 60 | this.streams.splice(index, 1); 61 | if (this.streams.length === 0) { 62 | this.buffer = []; 63 | } 64 | } 65 | 66 | push(data: any): void { 67 | let message: any; 68 | try { 69 | message = JSON.parse(data); 70 | } catch (e) { 71 | // If the string can't be parsed, display it verbatim. 72 | message = { 73 | notification: { 74 | title: data, 75 | }, 76 | }; 77 | } 78 | this.maybeShowNotification(message); 79 | if (this.buffer !== null) { 80 | this.buffer.push(message); 81 | } else { 82 | this.streams.forEach(id => { 83 | this.worker.sendToStream(id, message); 84 | }) 85 | } 86 | } 87 | 88 | maybeShowNotification(data: any) { 89 | if (!data.notification || !data.notification.title) { 90 | return; 91 | } 92 | const manifest = this.pushManifest; 93 | if (!manifest.showNotifications || (!!manifest.backgroundOnly && this.buffer === null)) { 94 | return; 95 | } 96 | const desc = data.notification as Object; 97 | let options = {}; 98 | NOTIFICATION_OPTION_NAMES 99 | .filter(name => desc.hasOwnProperty(name)) 100 | .forEach(name => options[name] = desc[name]); 101 | this.worker.showNotification(desc['title'], options); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /service-worker/worker/src/worker/logging.ts: -------------------------------------------------------------------------------- 1 | export enum Verbosity { 2 | DEBUG = 1, 3 | TECHNICAL = 2, 4 | INFO = 3, 5 | STATUS = 4, 6 | DISABLED = 1000, 7 | } 8 | 9 | export interface LogEntry { 10 | message: string; 11 | verbosity: Verbosity; 12 | } 13 | 14 | export interface Logging { 15 | debug(message: string, ...args: any[]): void; 16 | technical(message: string, ...args: any[]): void; 17 | info(message: string, ...args: any[]): void; 18 | status(message: string, ...args: any[]): void; 19 | log(verbosity: Verbosity, message: string, ...args: any[]): void; 20 | } 21 | 22 | export interface LogHandler { 23 | handle(msg: LogEntry); 24 | } 25 | 26 | export class Logger implements Logging { 27 | 28 | private buffer: LogEntry[] = []; 29 | private verbosity = Verbosity.DISABLED; 30 | 31 | constructor() {} 32 | 33 | messages: Function = () => null; 34 | 35 | debug(message: string, ...args: any[]): void { 36 | this._log(Verbosity.DEBUG, message, args); 37 | } 38 | 39 | technical(message: string, ...args: any[]): void { 40 | this._log(Verbosity.TECHNICAL, message, args); 41 | } 42 | 43 | info(message: string, ...args: any[]): void { 44 | this._log(Verbosity.INFO, message, args); 45 | } 46 | 47 | status(message: string, ...args: any[]): void { 48 | this._log(Verbosity.STATUS, message, args); 49 | } 50 | 51 | log(verbosity: Verbosity, message: string, ...args: any[]): void { 52 | this._log(verbosity, message, args); 53 | } 54 | 55 | setVerbosity(verbosity: Verbosity): void { 56 | this.verbosity = verbosity; 57 | } 58 | 59 | release(): void { 60 | this.buffer.forEach(entry => this.messages(entry)); 61 | this.buffer = null; 62 | } 63 | 64 | private _log(verbosity: Verbosity, start: string, args: any[]) { 65 | let message = start; 66 | if (args.length > 0) { 67 | message = `${start} ${args.map(v => this._serialize(v)).join(' ')}` 68 | } 69 | if (verbosity < this.verbosity) { 70 | // Skip this message. 71 | return; 72 | } 73 | 74 | if (this.buffer !== null) { 75 | this.buffer.push({verbosity, message}); 76 | } else { 77 | this.messages({verbosity, message}); 78 | } 79 | } 80 | 81 | private _serialize(v: any) { 82 | if (typeof v !== 'object') { 83 | return `${v}`; 84 | } 85 | return JSON.stringify(v); 86 | } 87 | } 88 | 89 | export class ConsoleHandler implements LogHandler { 90 | handle(entry: LogEntry) { 91 | console.log(`${Verbosity[entry.verbosity].toString()}: ${entry.message}`); 92 | } 93 | } 94 | 95 | export class HttpHandler implements LogHandler { 96 | constructor(private url: string) {} 97 | 98 | handle(entry: LogEntry) { 99 | fetch(this.url, {body: `${Verbosity[entry.verbosity].toString()}: ${entry.message}`, method: 'POST'}); 100 | } 101 | } 102 | 103 | export const LOGGER = new Logger(); 104 | export const LOG = LOGGER as Logging; 105 | -------------------------------------------------------------------------------- /service-worker/worker/src/plugins/routes/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | rewriteUrlInstruction, 3 | FetchInstruction, 4 | Operation, 5 | Plugin, 6 | PluginFactory, 7 | VersionWorker, 8 | UrlConfig, 9 | UrlMatcher, 10 | } from '@angular/service-worker/worker'; 11 | 12 | interface RouteMap { 13 | [url: string]: RouteConfig|UrlConfig; 14 | } 15 | 16 | interface RouteConfig { 17 | prefix?: boolean; 18 | onlyWithoutExtension?: boolean; 19 | } 20 | 21 | interface RouteRedirectionManifest { 22 | index: string; 23 | routes?: RouteMap; 24 | } 25 | 26 | export function RouteRedirection(): PluginFactory { 27 | return (worker: VersionWorker) => new RouteRedirectionImpl(worker); 28 | } 29 | 30 | export class RouteRedirectionImpl implements Plugin { 31 | constructor(public worker: VersionWorker) {} 32 | 33 | private get routeManifest(): RouteRedirectionManifest { 34 | return this.worker.manifest['routing'] as RouteRedirectionManifest; 35 | } 36 | 37 | private hasExtension(path: string): boolean { 38 | const lastSegment = path.substr(path.lastIndexOf('/') + 1); 39 | return lastSegment.indexOf('.') !== -1; 40 | } 41 | 42 | setup(operations: Operation[]): void { 43 | // No setup needed. 44 | } 45 | 46 | fetch(req: Request): FetchInstruction { 47 | const manifest = this.routeManifest; 48 | if (!manifest || !manifest.routes) { 49 | return; 50 | } 51 | let [base, path] = parseUrl(req.url); 52 | const matchesRoutingTable = Object.keys(manifest.routes).some(route => { 53 | const config = manifest.routes[route]; 54 | if (config['match']) { 55 | const matcher = new UrlMatcher(route, config as UrlConfig, this.worker.adapter.scope); 56 | return matcher.matches(req.url); 57 | } else { 58 | const oldConfig = config as RouteConfig; 59 | const matchesPath = oldConfig.prefix 60 | ? path.indexOf(route) === 0 61 | : path === route; 62 | const matchesPathAndExtension = matchesPath && 63 | (!oldConfig.onlyWithoutExtension || !this.hasExtension(path)); 64 | return matchesPathAndExtension; 65 | } 66 | }); 67 | if (matchesRoutingTable) { 68 | return rewriteUrlInstruction(this.worker, req, base + manifest.index); 69 | } else { 70 | return null; 71 | } 72 | } 73 | } 74 | 75 | function parseUrl(full: string) { 76 | let isHttp = full.toLowerCase().startsWith('http://'); 77 | let isHttps = full.toLowerCase().startsWith('https://'); 78 | if (!isHttp && !isHttps) { 79 | // Relative url. 80 | return ['', full]; 81 | } 82 | 83 | let protocol = 'http://'; 84 | let protocolSuffix = full.substr('http://'.length); 85 | if (isHttps) { 86 | protocol = 'https://'; 87 | protocolSuffix = full.substr('https://'.length); 88 | } 89 | let rootSlash = protocolSuffix.indexOf('/'); 90 | if (rootSlash === -1) { 91 | return [full, '/']; 92 | } 93 | return [full.substr(0, protocol.length + rootSlash), protocolSuffix.substr(rootSlash)]; 94 | } -------------------------------------------------------------------------------- /app-shell/gulpfile.ts: -------------------------------------------------------------------------------- 1 | declare var require; 2 | 3 | const childProcess = require('child_process'); 4 | const commonjs = require('rollup-plugin-commonjs'); 5 | const gulp = require('gulp'); 6 | const jsmn = require('gulp-jasmine'); 7 | const rimraf = require('rimraf'); 8 | const rollup = require('rollup'); 9 | const nodeResolve = require('rollup-plugin-node-resolve'); 10 | const runSequence = require('run-sequence'); 11 | 12 | 13 | gulp.task('clean', done => { 14 | rimraf('./tmp', () => { 15 | rimraf('./dist', done); 16 | }); 17 | }); 18 | 19 | gulp.task('build', done => runSequence( 20 | 'clean', 21 | 'task:build', 22 | 'task:deploy', 23 | done)); 24 | 25 | gulp.task('test', done => runSequence( 26 | 'clean', 27 | 'task:test', 28 | done)); 29 | 30 | gulp.task('task:build', done => runSequence( 31 | 'task:app:compile_esm', 32 | 'task:app:bundle', 33 | done)); 34 | 35 | gulp.task('task:test', done => runSequence( 36 | 'task:app:compile_es5', 37 | 'task:app:test', 38 | done)) 39 | 40 | gulp.task('task:deploy', done => runSequence( 41 | [ 42 | 'task:app:deploy', 43 | 'task:bundles:deploy', 44 | 'task:package:deploy', 45 | ], 46 | done)); 47 | 48 | gulp.task('task:app:compile_es5', () => { 49 | childProcess.execSync('node_modules/.bin/tsc -p tsconfig.es5.json'); 50 | }); 51 | 52 | gulp.task('task:app:compile_esm', () => { 53 | childProcess.execSync('node_modules/.bin/ngc -p tsconfig.esm.json'); 54 | }); 55 | 56 | gulp.task('task:app:bundle', done => rollup 57 | .rollup({ 58 | entry: 'tmp/esm/src/index.js', 59 | plugins: [ 60 | nodeResolve({jsnext: true, main: true}), 61 | commonjs({ 62 | include: 'node_modules/**', 63 | }), 64 | ], 65 | external: [ 66 | '@angular/core', 67 | ] 68 | }) 69 | .then(bundle => bundle.write({ 70 | format: 'umd', 71 | moduleName: 'ng.appShell', 72 | dest: 'tmp/es5/bundles/app-shell.umd.js', 73 | globals: { 74 | '@angular/core': 'ng.core', 75 | }, 76 | }))); 77 | 78 | gulp.task('task:app:deploy', () => gulp 79 | .src([ 80 | 'tmp/esm/src/index.d.ts', 81 | 'tmp/esm/src/index.js', 82 | 'tmp/esm/src/index.js.map', 83 | 'tmp/esm/src/index.metadata.json', 84 | 'tmp/esm/src/app/**/*.d.ts', 85 | 'tmp/esm/src/app/**/*.js', 86 | 'tmp/esm/src/app/**/*.js.map', 87 | 'tmp/esm/src/app/**/*.metadata.json', 88 | ], {base: 'tmp/esm/src'}) 89 | .pipe(gulp.dest('dist'))); 90 | 91 | gulp.task('task:app:test', () => gulp 92 | .src([ 93 | 'tmp/es5/src/unit_tests.js', 94 | ], {base: '.'}) 95 | .pipe(jsmn({ 96 | verbose: true, 97 | }))); 98 | 99 | gulp.task('task:bundles:deploy', () => gulp 100 | .src([ 101 | 'tmp/es5/bundles/**/*.js', 102 | 'tmp/es5/bundles/**/*.js.map', 103 | ], {base: 'tmp/es5'}) 104 | .pipe(gulp.dest('dist'))); 105 | 106 | gulp.task('task:package:deploy', () => gulp 107 | .src([ 108 | 'package.json' 109 | ]) 110 | .pipe(gulp.dest('dist'))); 111 | -------------------------------------------------------------------------------- /app-shell/src/experimental/shell-parser/shell-parser.ts: -------------------------------------------------------------------------------- 1 | import {RouteDefinition, ShellParserConfig} from './config'; 2 | import {ASTNode} from './ast'; 3 | import {NodeVisitor} from './node-visitor'; 4 | import {NodeMatcher} from './node-matcher'; 5 | import {TemplateParser} from './template-parser'; 6 | import {WorkerScope} from './context'; 7 | 8 | export interface ShellParser { 9 | fetchDoc(url?: string): Promise; 10 | parseDoc(res: Response): Promise; 11 | match(req: Request): Promise; 12 | } 13 | 14 | export class ShellParserImpl implements ShellParser { 15 | constructor(private config: ShellParserConfig, 16 | private parser: TemplateParser, 17 | private visitors: NodeVisitor[], 18 | private scope: WorkerScope) {} 19 | 20 | fetchDoc(url: string = this.config.APP_SHELL_URL): Promise { 21 | return this.scope.fetch(url); 22 | } 23 | 24 | parseDoc(res: Response): Promise { 25 | return res.text() 26 | .then((template: string) => { 27 | const headers: any = { 28 | 'content-type': 'text/html' 29 | }; 30 | return this.processDoc(template) 31 | .then((template: string) => { 32 | return this.scope.newResponse(template, { 33 | status: 200, 34 | headers 35 | }); 36 | }); 37 | }); 38 | } 39 | 40 | match(req: Request): Promise { 41 | if (req.method !== 'GET') { 42 | return Promise.resolve(null); 43 | } 44 | const matchedRoute = this.routeMatcher(this.config.ROUTE_DEFINITIONS, req.url).pop(); 45 | if (!matchedRoute) { 46 | return Promise.resolve(null); 47 | } 48 | return this.scope.caches.open(this.config.SHELL_PARSER_CACHE_NAME) 49 | .then((cache: Cache) => 50 | cache.match(this.scope.newRequest(this.config.APP_SHELL_URL))); 51 | } 52 | 53 | private processDoc(template: string): Promise { 54 | const root = this.parser.parse(template); 55 | return this.visitTemplate(root) 56 | .then((root: ASTNode) => { 57 | return this.parser.serialize(root); 58 | }); 59 | } 60 | 61 | private visitTemplate(node: ASTNode, visitorIdx: number = 0): Promise { 62 | if (visitorIdx >= this.visitors.length) { 63 | return Promise.resolve(node); 64 | } 65 | return this.visitors[visitorIdx].visit(node) 66 | .then((node: ASTNode) => { 67 | return this.visitTemplate(node, visitorIdx + 1); 68 | }); 69 | } 70 | 71 | private routeMatcher(definitions: RouteDefinition[], url: string) { 72 | const urlParts = url.split('/'); 73 | let definitionsParts = definitions.map(def => this.scope.newRequest(def).url.split('/')) 74 | .filter(def => urlParts.length === def.length); 75 | let currentIdx = 0; 76 | while (definitionsParts.length > 0 && urlParts.length > currentIdx) { 77 | definitionsParts = definitionsParts.filter(defParts => { 78 | if (defParts[currentIdx][0] === ':') { 79 | return true; 80 | } 81 | return defParts[currentIdx] === urlParts[currentIdx]; 82 | }); 83 | currentIdx += 1; 84 | } 85 | return definitionsParts.map(parts => parts.join('/')); 86 | } 87 | } 88 | 89 | -------------------------------------------------------------------------------- /service-worker/worker/src/worker/common.ts: -------------------------------------------------------------------------------- 1 | import {FetchDelegate, FetchInstruction, Operation, VersionWorker} from './api'; 2 | import {LOG} from './logging'; 3 | import {VersionWorkerImpl} from './worker'; 4 | 5 | export function cacheFromNetworkOp(worker: VersionWorker, url: string, cache: string, cacheBust = true): Operation { 6 | let limit = 3; 7 | const helper = (url: string): Promise => { 8 | if (limit-- === 0) { 9 | return Promise.reject(`Hit redirect limit when attempting to fetch ${url}.`); 10 | } 11 | const req = worker.adapter.newRequest(url); 12 | let reqPromise: Promise = null; 13 | return worker.refresh(req, cacheBust).then(res => { 14 | if (res['redirected'] as boolean && res.url && res.url !== '') { 15 | return helper(res.url); 16 | } 17 | return res; 18 | }); 19 | }; 20 | const op: Operation = () => helper(url) 21 | .then(resp => worker.cache.store(cache, url, resp)); 22 | op.desc = {type: 'cacheFromNetworkOp', worker, url, cache}; 23 | return op; 24 | } 25 | 26 | export function copyExistingCacheOp(oldWorker: VersionWorker, newWorker: VersionWorker, url: string, cache: string): Operation { 27 | const op: Operation = () => oldWorker 28 | .cache 29 | .load(cache, url) 30 | .then(resp => !!resp 31 | ? newWorker.cache.store(cache, url, resp).then(() => true) 32 | : null); 33 | op.desc = {type: 'copyExistingCacheOp', oldWorker, newWorker, url, cache}; 34 | return op; 35 | } 36 | 37 | export function copyExistingOrFetchOp(oldWorker: VersionWorker, newWorker: VersionWorker, url: string, cache: string): Operation { 38 | const op: Operation = () => copyExistingCacheOp(oldWorker, newWorker, url, cache)() 39 | .then(res => { 40 | if (!res) { 41 | return cacheFromNetworkOp(newWorker, url, cache)(); 42 | } 43 | return res; 44 | }); 45 | op.desc = {type: 'copyExistingOrFetchOp', oldWorker, newWorker, url, cache}; 46 | return op; 47 | } 48 | 49 | export function deleteCacheOp(worker: VersionWorker, key: string): Operation { 50 | const op: Operation = () => worker.cache.remove(key); 51 | op.desc = {type: 'deleteCacheOp', worker, key}; 52 | return op; 53 | } 54 | 55 | export function fetchFromCacheInstruction(worker: VersionWorker, req: string | Request, cache: string): FetchInstruction { 56 | const op: FetchInstruction = (next: FetchDelegate) => worker.cache.load(cache, req) 57 | .then(res => !!res ? res : next()); 58 | op.desc = {type: 'fetchFromCacheInstruction', worker, req, cache}; 59 | return op; 60 | } 61 | 62 | export function fetchFromNetworkInstruction(worker: VersionWorker, req: Request, shouldRefresh: boolean = true): FetchInstruction { 63 | const op: FetchInstruction = (next: FetchDelegate) => shouldRefresh ? worker.refresh(req) : (worker as any as VersionWorkerImpl).scope.fetch(req); 64 | op.desc = {type: 'fetchFromNetworkInstruction', worker, req}; 65 | return op; 66 | } 67 | 68 | export function rewriteUrlInstruction(worker: VersionWorker, req: Request, destUrl: string): FetchInstruction { 69 | const newReq = worker.adapter.newRequest(destUrl); 70 | const op: FetchInstruction = (next: FetchDelegate) => worker.fetch(newReq); 71 | op.desc = {type: 'rewriteUrlInstruction', worker, req, destUrl}; 72 | return op; 73 | } 74 | -------------------------------------------------------------------------------- /app-shell/src/experimental/shell-parser/testing/mock-requests.ts: -------------------------------------------------------------------------------- 1 | export class MockBody { 2 | bodyUsed: boolean = false; 3 | 4 | constructor(private _body: string) {} 5 | 6 | arrayBuffer(): Promise { 7 | var buf = new ArrayBuffer(this._body.length * 2); 8 | var bufView = new Uint16Array(buf); 9 | for (var i=0, strLen = this._body.length; i < strLen; i++) { 10 | bufView[i] = this._body.charCodeAt(i); 11 | } 12 | return Promise.resolve(buf); 13 | } 14 | 15 | blob(): Promise { 16 | throw 'Unimplemented'; 17 | } 18 | 19 | formData(): Promise { 20 | throw 'Unimplemented'; 21 | } 22 | 23 | json(): Promise { 24 | throw 'Unimplemented'; 25 | } 26 | 27 | text(): Promise { 28 | return Promise.resolve(this._body); 29 | } 30 | 31 | get _mockBody(): string { 32 | return this._body; 33 | } 34 | } 35 | 36 | export class MockRequest extends MockBody implements Request { 37 | url: string; 38 | method: string = "GET"; 39 | cache: RequestCache = "default"; 40 | 41 | headers: any; 42 | redirect: RequestRedirect; 43 | get body(): any { 44 | return this; 45 | } 46 | 47 | mode: RequestMode; 48 | context: RequestContext; 49 | referrer: string; 50 | credentials: RequestCredentials; 51 | 52 | constructor(req: string | Request, init?: {[key: string]: any}) { 53 | super(null); 54 | if (typeof req == 'string') { 55 | this.url = req; 56 | } else { 57 | let other = req; 58 | this.url = init['url'] || other.url; 59 | this.method = other.method; 60 | this.cache = other.cache; 61 | this.headers = other.headers; 62 | //this.body = other.body; 63 | this.mode = other.mode; 64 | this.context = other.context; 65 | this.referrer = other.referrer; 66 | this.credentials = other.credentials; 67 | } 68 | ['method', 'cache', 'headers', 'mode', 'context', 'referrer', 'credentials'] 69 | .forEach(prop => this._copyProperty(prop, init)); 70 | } 71 | 72 | _copyProperty(prop: string, from: Object) { 73 | if (from && from.hasOwnProperty(prop)) { 74 | (this)[prop] = (from)[prop]; 75 | } 76 | } 77 | 78 | matches(req: Request): boolean { 79 | return req.url === this.url && req.method === this.method; 80 | } 81 | } 82 | 83 | export class MockResponse extends MockBody implements Response { 84 | ok: boolean = true; 85 | statusText: string = 'OK'; 86 | status: number = 200; 87 | url: string; 88 | headers: any; 89 | type: ResponseType = "default"; 90 | 91 | constructor(body: string | Blob | BodyInit, init?: ResponseInit) { 92 | super(body); 93 | if ((init || { headers: null }).headers) { 94 | this.headers = init.headers; 95 | } 96 | } 97 | 98 | clone(): MockResponse { 99 | if (this.bodyUsed) { 100 | throw 'Body already consumed.'; 101 | } 102 | var resp = new MockResponse(this._mockBody); 103 | resp.ok = this.ok; 104 | resp.statusText = this.statusText; 105 | resp.status = this.status; 106 | resp.headers = this.headers; 107 | resp.url = this.url; 108 | return resp; 109 | } 110 | 111 | error(): Response { 112 | throw 'Unimplemented'; 113 | } 114 | 115 | redirect(url: string, status: number): Response { 116 | throw 'Unimplemented'; 117 | } 118 | } 119 | 120 | -------------------------------------------------------------------------------- /service-worker/worker/src/build/webpack.ts: -------------------------------------------------------------------------------- 1 | declare var require, Buffer; 2 | const crypto = require('crypto'); 3 | 4 | export interface SwPluginConfig { 5 | manifestFile?: string; 6 | manifestKey?: string; 7 | baseHref?: string; 8 | } 9 | 10 | /** 11 | * Webpack plugin that generates a basic Angular service worker manifest. 12 | */ 13 | export class AngularServiceWorkerPlugin { 14 | 15 | public manifestFile: string; 16 | public manifestKey: string; 17 | public baseHref: string; 18 | 19 | constructor(config?: SwPluginConfig) { 20 | this.manifestFile = (config && config.manifestFile) || 'ngsw-manifest.json'; 21 | this.manifestKey = (config && config.manifestKey) || 'static'; 22 | this.baseHref = (config && config.baseHref) || '/'; 23 | if (!this.baseHref.endsWith('/')) { 24 | this.baseHref += '/'; 25 | } 26 | } 27 | 28 | apply(compiler) { 29 | // Determine the URL prefix under which all files will be served. 30 | compiler.plugin('emit', (compilation, callback) => { 31 | // Manifest into which assets to be fetched will be recorded. This will either 32 | // be read from the existing template or created fresh. 33 | let manifest: any = {}; 34 | 35 | // Look for an existing manifest. If there is one, parse it. 36 | try { 37 | if (compilation.assets.hasOwnProperty(this.manifestFile)) { 38 | manifest = JSON.parse(compilation.assets[this.manifestFile].source().toString()); 39 | } 40 | } catch (err) { 41 | throw new Error(`Error reading existing service worker manifest: ${err}`); 42 | } 43 | 44 | // Throw if the manifest already has this particular key. 45 | if (manifest.hasOwnProperty(this.manifestKey) && 46 | !manifest[this.manifestKey].hasOwnProperty('_generatedFromWebpack')) { 47 | throw new Error(`Manifest already contains key: ${this.manifestKey}`); 48 | } 49 | 50 | // Look for ignored patterns in the manifest. 51 | let ignored: RegExp[] = []; 52 | const ignoreKey = `${this.manifestKey}.ignore`; 53 | if (manifest.hasOwnProperty(ignoreKey)) { 54 | ignored.push(...(manifest[ignoreKey] as string[]) 55 | .map(regex => new RegExp(regex))); 56 | delete manifest[ignoreKey]; 57 | } 58 | 59 | // Map of urls to hashes. 60 | let urls = {}; 61 | manifest[this.manifestKey] = {urls, _generatedFromWebpack: true}; 62 | // Go through every asset in the compilation and include it in the manifest, 63 | // computing a hash for proper versioning. 64 | Object 65 | .keys(compilation.assets) 66 | .filter(key => key !== this.manifestFile) 67 | .forEach(key => { 68 | let url = `${this.baseHref}${key}`; 69 | if (ignored.some(regex => regex.test(url))) { 70 | return; 71 | } 72 | urls[url] = sha1(compilation.assets[key].source()); 73 | }); 74 | 75 | // Serialize the manifest to a buffer, and include (or overwrite) it in the assets. 76 | let serialized = new Buffer(JSON.stringify(manifest, null, 2)); 77 | compilation.assets[this.manifestFile] = { 78 | source: () => serialized, 79 | size: () => serialized.length, 80 | }; 81 | 82 | callback(); 83 | }); 84 | } 85 | } 86 | 87 | function sha1(buffer: any): string { 88 | let hash = crypto.createHash('sha1'); 89 | hash.update(buffer); 90 | return hash.digest('hex'); 91 | } 92 | -------------------------------------------------------------------------------- /service-worker/worker/src/plugins/dynamic/strategy/performance.ts: -------------------------------------------------------------------------------- 1 | import {DynamicGroup, DynamicStrategy, ResponseWithSideEffect, maybeRun} from '../group'; 2 | import {CacheConfig} from '../manifest'; 3 | 4 | export interface PerformanceCacheConfig extends CacheConfig { 5 | optimizeFor: "performance"; 6 | 7 | refreshAheadMs?: number; 8 | } 9 | 10 | /** 11 | * A dynamic caching strategy which optimizes for the performance of requests 12 | * it serves, by placing the cache before the network. 13 | * 14 | * In the performance strategy, requests always hit the cache first. If cached 15 | * data is available it is returned immediately, and the network is not (usually) 16 | * consulted. 17 | * 18 | * An exception to this rule is if the user configures a `refreshAheadMs` age. 19 | * If cached responses are older than this configured age, a network request will 20 | * be made in the background to update them, even though the cached value is 21 | * returned to the consumer anyway. This allows caches to still be effective while 22 | * not letting them become too stale. 23 | * 24 | * If data is not available in the cache, it is fetched from the network and 25 | * cached. 26 | */ 27 | export class PerformanceStrategy implements DynamicStrategy { 28 | /** 29 | * Name of the strategy (matched to the value in `optimizeFor`). 30 | */ 31 | get name(): string { 32 | return 'performance'; 33 | } 34 | 35 | /** 36 | * Reads the cache configuration from the group's config. 37 | */ 38 | config(group: DynamicGroup): PerformanceCacheConfig { 39 | return group.config.cache as PerformanceCacheConfig; 40 | } 41 | 42 | /** 43 | * Makes a request using this strategy, falling back on the `delegate` if 44 | * the cache is not being used. 45 | */ 46 | fetch(group: DynamicGroup, req: Request, delegate: () => Promise): Promise { 47 | // Firstly, read the configuration. 48 | const config = this.config(group); 49 | 50 | return group 51 | // Attempt to load the data from the cache. 52 | .fetchFromCache(req) 53 | .then(rse => { 54 | // Check whether the cache had data. 55 | if (rse.response === null) { 56 | // No response found, fall back on the network. 57 | return group.fetchAndCache(req, delegate); 58 | } else if (!!rse.cacheAge && config.refreshAheadMs !== undefined && rse.cacheAge >= config.refreshAheadMs) { 59 | // Response found, but it's old enough to trigger refresh ahead. 60 | // The side affect in rse.sideEffect is to update the metadata for the cache, 61 | // but that can be ignored since a fresh fetch will also update the metadata. 62 | // So return the cached response, but with a side effect that fetches from 63 | // the network and ignores the result, but runs that side effect instead 64 | // (which will update the cache to contain the new, fresh data). 65 | return { 66 | response: rse.response, 67 | cacheAge: rse.cacheAge, 68 | sideEffect: () => group 69 | // Fetch from the network again. 70 | .fetchAndCache(req, delegate) 71 | // And run the side effect if given. 72 | .then(raRse => maybeRun(raRse.sideEffect)), 73 | }; 74 | } else { 75 | // Response found, and refresh ahead behavior was not triggered. Just return 76 | // the response directly. 77 | return rse; 78 | } 79 | }); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /service-worker/worker/src/worker/worker.ts: -------------------------------------------------------------------------------- 1 | 2 | import {Operation, Plugin, VersionWorker, FetchDelegate, FetchInstruction, StreamController} from './api'; 3 | import {fetchFromNetworkInstruction} from './common'; 4 | import {ScopedCache} from './cache'; 5 | import {NgSwAdapter, NgSwFetch, Clock} from './facade'; 6 | import {Manifest} from './manifest'; 7 | 8 | export class VersionWorkerImpl implements VersionWorker { 9 | 10 | constructor( 11 | public streamController: StreamController, 12 | public scope: ServiceWorkerGlobalScope, 13 | public manifest: Manifest, 14 | public adapter: NgSwAdapter, 15 | public cache: ScopedCache, 16 | public clock: Clock, 17 | private fetcher: NgSwFetch, 18 | private plugins: Plugin[]) {} 19 | 20 | refresh(req: Request, cacheBust: boolean = true): Promise { 21 | if (cacheBust) { 22 | return this.fetcher.refresh(req); 23 | } else { 24 | return this.fetcher.request(req); 25 | } 26 | } 27 | 28 | fetch(req: Request): Promise { 29 | const fromNetwork = fetchFromNetworkInstruction(this, req, false); 30 | return this 31 | .plugins 32 | .filter(plugin => !!plugin.fetch) 33 | .map(plugin => plugin.fetch(req)) 34 | .filter(instruction => !!instruction) 35 | .reduceRight( 36 | (delegate: FetchDelegate, curr: FetchInstruction) => () => curr(delegate), 37 | () => this.fetcher.request(req, true) 38 | ) 39 | (); 40 | } 41 | 42 | validate(): Promise { 43 | return Promise 44 | .all(this 45 | .plugins 46 | .filter(plugin => !!plugin.validate) 47 | .map(plugin => plugin.validate()) 48 | ) 49 | .then(results => results.every(v => v)); 50 | } 51 | 52 | setup(previous: VersionWorkerImpl): Promise { 53 | let operations: Operation[] = []; 54 | for (let i = 0; i < this.plugins.length; i++) { 55 | const plugin: Plugin = this.plugins[i]; 56 | if (plugin.update && previous) { 57 | plugin.update(operations, previous.plugins[i]); 58 | } else { 59 | plugin.setup(operations); 60 | } 61 | } 62 | return operations.reduce>( 63 | (prev, curr) => prev.then(() => curr()), 64 | Promise.resolve(null), 65 | ); 66 | } 67 | 68 | cleanup(): Operation[] { 69 | return this.plugins.reduce((ops, plugin) => { 70 | if (plugin.cleanup) { 71 | plugin.cleanup(ops); 72 | } 73 | return ops; 74 | }, []); 75 | } 76 | 77 | message(message: any, id: number): void { 78 | this 79 | .plugins 80 | .filter(plugin => !!plugin.message) 81 | .forEach(plugin => plugin.message(message, id)); 82 | } 83 | 84 | messageClosed(id: number): void { 85 | this 86 | .plugins 87 | .filter(plugin => !!plugin.messageClosed) 88 | .forEach(plugin => plugin.messageClosed(id)); 89 | } 90 | 91 | sendToStream(id: number, message: Object): void { 92 | this.streamController.sendToStream(id, message); 93 | } 94 | 95 | closeStream(id: number): void { 96 | this.streamController.closeStream(id); 97 | } 98 | 99 | push(data: any): void { 100 | this 101 | .plugins 102 | .filter(plugin => !!plugin.push) 103 | .forEach(plugin => plugin.push(data)); 104 | } 105 | 106 | showNotification(title: string, options?: Object): void { 107 | this.scope.registration.showNotification(title, options); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /service-worker/worker/src/plugins/dynamic/linked.spec.ts: -------------------------------------------------------------------------------- 1 | import {SortedLinkedList} from './linked'; 2 | 3 | function compareNumbers(a: number, b: number): number { 4 | if (a < b) { 5 | return -1; 6 | } else if (a === b) { 7 | return 0; 8 | } else { 9 | return 1; 10 | } 11 | } 12 | 13 | function expectSequence(list: SortedLinkedList, sequence: number[]) { 14 | expect(list.length).toBe(sequence.length); 15 | if (sequence.length === 0) { 16 | expect(list.head).toBeNull(); 17 | expect(list.tail).toBeNull(); 18 | return; 19 | } 20 | let prevNode = null; 21 | let node = list.head; 22 | for (let i = 0; i < sequence.length; i++) { 23 | expect(node).not.toBeNull(); 24 | if (node === null) { 25 | return; 26 | } 27 | expect(node.value).toBe(sequence[i]); 28 | expect(node.prev).toBe(prevNode); 29 | prevNode = node; 30 | node = node.next; 31 | } 32 | expect(list.tail).toBe(prevNode); 33 | expect(node).toBeNull(); 34 | } 35 | 36 | function insertAll(list: SortedLinkedList, sequence: number[]) { 37 | sequence.forEach(value => list.insert(value)); 38 | } 39 | 40 | describe('dynamic sorted linked list', () => { 41 | let list: SortedLinkedList; 42 | beforeEach(() => { 43 | list = new SortedLinkedList(compareNumbers); 44 | }); 45 | it('starts off correctly allocated', () => { 46 | expectSequence(list, []); 47 | }); 48 | describe('inserts', () => { 49 | it('one element', () => { 50 | list.insert(1); 51 | expectSequence(list, [1]); 52 | }); 53 | it('2nd element at tail', () => { 54 | insertAll(list, [2, 1]); 55 | expectSequence(list, [1, 2]); 56 | }); 57 | it('3rd element at tail', () => { 58 | insertAll(list, [3, 2, 1]); 59 | expectSequence(list, [1, 2, 3]); 60 | }); 61 | it('an element in the middle', () => { 62 | insertAll(list, [3, 1, 2]); 63 | expectSequence(list, [1, 2, 3]); 64 | }); 65 | }); 66 | describe('removes', () => { 67 | it('the head', () => { 68 | insertAll(list, [1, 2, 3]); 69 | list.remove(1); 70 | expectSequence(list, [2, 3]); 71 | }); 72 | it('the middle', () => { 73 | insertAll(list, [1, 2, 3]); 74 | list.remove(2); 75 | expectSequence(list, [1, 3]); 76 | }); 77 | it('the tail', () => { 78 | insertAll(list, [1, 2, 3]); 79 | list.remove(3); 80 | expectSequence(list, [1, 2]); 81 | }); 82 | it('a non-existent element', () => { 83 | insertAll(list, [1, 2, 3]); 84 | list.remove(4); 85 | expectSequence(list, [1, 2, 3]); 86 | }); 87 | it('the only element', () => { 88 | list.insert(1); 89 | list.remove(1); 90 | expectSequence(list, []); 91 | }); 92 | }) 93 | it('pops the head element', () => { 94 | insertAll(list, [1, 2, 3]); 95 | expect(list.pop()).toBe(1); 96 | expectSequence(list, [2, 3]); 97 | expect(list.pop()).toBe(2); 98 | expectSequence(list, [3]); 99 | expect(list.pop()).toBe(3); 100 | expectSequence(list, []); 101 | expect(list.pop()).toBeNull(); 102 | }); 103 | describe('handles a duplicate element', () => { 104 | it('at the head', () => { 105 | insertAll(list, [1, 2, 3, 1]); 106 | expectSequence(list, [1, 1, 2, 3]); 107 | }); 108 | it('at the middle', () => { 109 | insertAll(list, [1, 2, 3, 2]); 110 | expectSequence(list, [1, 2, 2, 3]); 111 | }); 112 | it('at the tail', () => { 113 | insertAll(list, [1, 2, 3, 3]); 114 | expectSequence(list, [1, 2, 3, 3]); 115 | }); 116 | }); 117 | }); 118 | -------------------------------------------------------------------------------- /guides/service-worker.md: -------------------------------------------------------------------------------- 1 | # Service Worker 2 | 3 | This guide assumes you've already completed the [Setup](./cli-setup.md) guide. 4 | 5 | The `hello-mobile` app by default comes with a fully-functional 6 | [ServiceWorker](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API) script, which is automatically installed when the 7 | app is built in production mode (`ng build --prod` or `ng serve --prod`). 8 | 9 | Service Worker is a relatively new addition to the Web Platform, 10 | and is a critical component to building true Progressive Web Apps. 11 | Not only does Service Worker make it possible to make apps load without an internet connection, it also makes it possible to push notifications and updates to a user's device while the app isn't even running. 12 | 13 | The Angular Mobile Toolkit comes with support for generating a service worker that will automatically pre-fetch and cache all 14 | static assets for an application, making it possible for the app 15 | to work without an internet connection. Even if an internet connection is available, the app will load more quickly because 16 | all of its assets are available in cache next time the app loads. 17 | 18 | At this point in time, there aren't any additional configuration options to control how the worker script works. 19 | 20 | To see the files that the Service Worker will be pre-fetching and caching to make 21 | available offline, run a prod build of the `hello-mobile` app: 22 | 23 | `$ ng build --prod` 24 | 25 | Then open dist/ngsw-manifest.json: 26 | 27 | `dist/ngsw-manifest.json`: 28 | 29 | ```json 30 | "{ 31 | "group": { 32 | "app": { 33 | "url": { 34 | "/app-concat.js": { 35 | "hash": "2431d95f572a2a23ee6df7d619d2b68ad65f1084" 36 | }, 37 | "/favicon.ico": { 38 | "hash": "164f9754ba7b676197a4974992da8fc3d3606dbf" 39 | }, 40 | "/icons/android-chrome-144x144.png": { 41 | "hash": "2eb2986d6c5050612d99e6a0fb93815942c63b02" 42 | }, 43 | 44 | 45 | "and-many": "more-icons", 46 | 47 | 48 | "/index.html": { 49 | "hash": "c536103ca1836310c60f7cc94b6fa14debcf2ddf" 50 | }, 51 | "/manifest.webapp": { 52 | "hash": "a020306797abb92fe29c90bb2832b6f5783e2487" 53 | } 54 | } 55 | } 56 | }, 57 | "routing": { 58 | "index": "/index.html" 59 | } 60 | }" 61 | ``` 62 | 63 | The `group.app.url` objects are the configuration the Service Worker script uses to pre-fetch 64 | and cache assets when the application is loaded. If a file's hash ever changes, the Service Worker 65 | knows it needs to fetch the latest version of that file. 66 | 67 | The `routing` config tells the service worker which URLs map to application routes, and should be 68 | served with index.html. 69 | 70 | Now to check that the Service Worker is installed correctly, open Chrome Developer Tools, click the Resources tab, and then click Service Workers. You should see our installed Service Worker! Now to really test that it works, go to the Network tab in Chrome Developer Tools, and change the Throttling dropdown to select Offline. Now refresh the page, and it should still load. 71 | 72 | ## The End 73 | 74 | This is the end of the guides for now. Our work is in an alpha state, and we'd love feedback. 75 | Please open issues on [angular/mobile-toolkit](https://github.com/angular/mobile-toolkit), 76 | and tweet at the Angular Mobile Team [@jeffbcross](https://twitter.com/jeffbcross), 77 | [@robwormald](https://twitter.com/robwormald), [@synalx](https://twitter.com/synalx). 78 | 79 | And to dive deeper into Progresive Web Apps, check out [Progressive Web Apps](https://developers.google.com/web/progressive-web-apps/?hl=en) 80 | on Google Developers. -------------------------------------------------------------------------------- /service-worker/worker/src/plugins/static/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | cacheFromNetworkOp, 3 | copyExistingOrFetchOp, 4 | deleteCacheOp, 5 | fetchFromCacheInstruction, 6 | FetchInstruction, 7 | Operation, 8 | Plugin, 9 | PluginFactory, 10 | VersionWorker, 11 | LOG, 12 | Verbosity 13 | } from '@angular/service-worker/worker'; 14 | 15 | interface UrlToHashMap { 16 | [url: string]: string; 17 | } 18 | 19 | interface StaticManifest { 20 | urls: UrlToHashMap; 21 | versioned?: string[]; 22 | } 23 | 24 | export interface StaticContentCacheOptions { 25 | manifestKey?: string; 26 | } 27 | 28 | export function StaticContentCache(options?: StaticContentCacheOptions): PluginFactory { 29 | const manifestKey = (options && options.manifestKey) || 'static'; 30 | return (worker: VersionWorker) => new StaticContentCacheImpl(worker, manifestKey); 31 | } 32 | 33 | export class StaticContentCacheImpl implements Plugin { 34 | private cacheKey: string; 35 | 36 | constructor(public worker: VersionWorker, public key: string) { 37 | this.cacheKey = key === 'static' ? key : `static:${key}`; 38 | } 39 | 40 | private get staticManifest(): StaticManifest { 41 | return this.worker.manifest[this.key]; 42 | } 43 | 44 | private shouldCacheBustFn(): (url: string) => boolean { 45 | let shouldCacheBust = (url: string): boolean => true; 46 | if (!!this.staticManifest.versioned && Array.isArray(this.staticManifest.versioned)) { 47 | const regexes = this.staticManifest.versioned.map(expr => new RegExp(expr)); 48 | shouldCacheBust = url => !regexes.some(regex => regex.test(url)); 49 | } 50 | return shouldCacheBust; 51 | } 52 | 53 | setup(operations: Operation[]): void { 54 | const shouldCacheBust = this.shouldCacheBustFn(); 55 | operations.push(...Object 56 | .keys(this.staticManifest.urls) 57 | .map(url => () => { 58 | return this 59 | .worker 60 | .cache 61 | .load(this.cacheKey, url) 62 | .then(resp => { 63 | if (!!resp) { 64 | LOG.technical(`setup(${this.cacheKey}, ${url}): no need to refresh ${url} in the cache`); 65 | return null; 66 | } 67 | LOG.technical(`setup(${this.cacheKey}, ${url}): caching from network`); 68 | return cacheFromNetworkOp(this.worker, url, this.cacheKey, shouldCacheBust(url))(); 69 | }) 70 | } 71 | )); 72 | } 73 | 74 | update(operations: Operation[], previous: StaticContentCacheImpl): void { 75 | const shouldCacheBust = this.shouldCacheBustFn(); 76 | operations.push(...Object 77 | .keys(this.staticManifest.urls) 78 | .map(url => { 79 | const hash = this.staticManifest.urls[url]; 80 | const previousHash = previous.staticManifest.urls[url]; 81 | if (previousHash === hash) { 82 | LOG.technical(`update(${this.cacheKey}, ${url}): no need to refresh ${url} in the cache`); 83 | return copyExistingOrFetchOp(previous.worker, this.worker, url, this.cacheKey); 84 | } else { 85 | LOG.technical(`update(${this.cacheKey}, ${url}): caching from network`); 86 | return cacheFromNetworkOp(this.worker, url, this.cacheKey, shouldCacheBust(url)); 87 | } 88 | }) 89 | ); 90 | } 91 | 92 | fetch(req: Request): FetchInstruction { 93 | return fetchFromCacheInstruction(this.worker, req, this.cacheKey); 94 | } 95 | 96 | cleanup(operations: Operation[]): void { 97 | operations.push(deleteCacheOp(this.worker, this.cacheKey)); 98 | } 99 | 100 | validate(): Promise { 101 | return Promise 102 | .all(Object 103 | .keys(this.staticManifest.urls) 104 | .map(url => this.worker.cache.load(this.cacheKey, url)) 105 | ) 106 | .then(resps => resps.every(resp => !!resp && resp.ok)); 107 | } 108 | } -------------------------------------------------------------------------------- /service-worker/worker/src/test/e2e/harness/server/page-object.ts: -------------------------------------------------------------------------------- 1 | export class HarnessPageObject { 2 | 3 | sendKeysSlow(el, keys) { 4 | keys.split('').forEach(char => el.sendKeys(char)); 5 | } 6 | 7 | selectAction(action: string) { 8 | this.sendKeysSlow(element(by.css('#actionInput')).clear(), action); 9 | element(by.css('#actionExec')) 10 | .click(); 11 | } 12 | 13 | setTextOn(id: string, text: string) { 14 | this.sendKeysSlow(element(by.css(`#${id}`)).clear(), text); 15 | } 16 | 17 | clickButton(id: string) { 18 | element(by.css(`#${id}`)) 19 | .click(); 20 | } 21 | 22 | get result(): Promise { 23 | return element(by.css('#result')).getText() as any as Promise; 24 | } 25 | 26 | get updates(): Promise { 27 | browser.wait(protractor.ExpectedConditions.presenceOf(element(by.id('updateAlert')))); 28 | return element(by.id('updates')).getText() as any as Promise; 29 | } 30 | 31 | get asyncResult(): Promise { 32 | browser.wait(protractor.ExpectedConditions.presenceOf(element(by.id('alert')))); 33 | return this.result; 34 | } 35 | 36 | request(url: string): Promise { 37 | this.reset(); 38 | this.selectAction('MAKE_REQUEST'); 39 | this.setTextOn('requestUrl', url); 40 | this.clickButton('requestAction'); 41 | return this.asyncResult; 42 | } 43 | 44 | installServiceWorker(url: string): void { 45 | this.selectAction('SW_INSTALL'); 46 | this.setTextOn('workerUrl', url); 47 | this.clickButton('installAction'); 48 | } 49 | 50 | forceUpdate(version: string): Promise { 51 | this.selectAction('FORCE_UPDATE'); 52 | this.setTextOn('updateVersion', version); 53 | this.clickButton('updateAction'); 54 | return this.result; 55 | } 56 | 57 | hasActiveWorker(): Promise { 58 | this.selectAction('SW_CHECK'); 59 | return this 60 | .result 61 | .then(JSON.parse) 62 | .then(res => res.some(worker => worker.active)); 63 | } 64 | 65 | hasServiceWorker(): Promise { 66 | this.selectAction('SW_CHECK'); 67 | browser.waitForAngular(); 68 | return this 69 | .result 70 | .then(value => { 71 | return value; 72 | }) 73 | .then(value => value !== '[]'); 74 | } 75 | 76 | cacheKeys(): Promise { 77 | this.selectAction('CACHE_KEYS'); 78 | return this 79 | .result 80 | .then(JSON.parse); 81 | } 82 | 83 | ping(): Promise { 84 | this.reset(); 85 | this.selectAction('COMPANION_PING'); 86 | return this.asyncResult; 87 | } 88 | 89 | waitForPush(): Promise { 90 | this.reset(); 91 | this.selectAction('COMPANION_WAIT_FOR_PUSH'); 92 | return this.result; 93 | } 94 | 95 | log(): Promise { 96 | return (element(by.css('#log')) 97 | .getText() as any as Promise) 98 | .then(v => JSON.parse(v)) 99 | .then(log => { 100 | this.selectAction('RESET'); 101 | return log; 102 | }); 103 | } 104 | 105 | reset() { 106 | this.selectAction('RESET'); 107 | browser.wait(protractor.ExpectedConditions.not( 108 | protractor.ExpectedConditions.presenceOf(element(by.id('alert'))))); 109 | browser.wait(protractor.ExpectedConditions.not( 110 | protractor.ExpectedConditions.presenceOf(element(by.id('updateAlert'))))); 111 | } 112 | 113 | registerForPush(): Promise { 114 | this.reset(); 115 | this.selectAction('COMPANION_REG_PUSH'); 116 | return this.asyncResult; 117 | } 118 | 119 | checkForUpdate(): Promise { 120 | this.reset(); 121 | this.selectAction('CHECK_FOR_UPDATES'); 122 | return this 123 | .asyncResult 124 | .then(JSON.parse); 125 | } 126 | 127 | subscribeToUpdates(): void { 128 | this.reset(); 129 | this.selectAction('COMPANION_SUBSCRIBE_TO_UPDATES'); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /ROADMAP.md: -------------------------------------------------------------------------------- 1 | # Angular Mobile Roadmap 2 | 3 | This is a rough view of the roadmap for Angular Mobile Toolkit. 4 | Angular Mobile Toolkit is an unusual project, in that its tools 5 | span several projects, implementations, and partners. 6 | It's better to consider Angular Mobile as an initiative to help drive 7 | the state of the art of mobile development with Angular 2, with 8 | some of its own first-class tools. 9 | 10 | This roadmap is primarily concerned with the tools under the 11 | Angular Mobile Toolkit, with some information about corresponding 12 | efforts in other projects like Universal, Angular Core, Ionic, NgRx, 13 | AngularFire etc. 14 | 15 | ## Stages 16 | 17 | Our roadmap is composed of milestones that are focused on some cohesive goal, 18 | with a list of deliverables for each milestone. For the active milestone, 19 | each deliverable includes a label indicating its current stage of development. 20 | 21 | Here are what the stages mean: 22 | 23 | * **Stage 0:** Not started 24 | * **Stage 1:** Design has consensus 25 | * **Stage 2:** Started 26 | * **Stage 3:** Feature implemented and published. Ideally integrated with Angular CLI. 27 | * **Stage 4:** Good e2e+unit test coverage. Feature has been dogfooded by one developer not on the Angular Mobile team. Feature is testable and debuggable by developer users. 28 | * **Stage 5:** Released to npm or equivalent. Well-documented on mobile.angular.io or equivalent. Verified/accepted by one stakeholder in the feature as adequately solving a real problem. 29 | * **Stage 6:** Used in production in a real, non-demo app. 30 | 31 | A milestone can be considered complete when all of its deliverables are at at least stage 5. 32 | 33 | ## Milestones 34 | 35 | ### Milestone 1: Progressive Web Apps Foundation 36 | 37 | Estimated Completion: July 2016 38 | 39 | * App Shell universal plugin to generate App Shell with CLI with --mobile 40 | * Stage 3 41 | * [Guide](https://mobile.angular.io/guides/app-shell.html) 42 | * [Implementation](https://github.com/angular/universal/tree/master/modules/broccoli-prerender) 43 | * App Shell library provides directives and providers to show/hide content 44 | * Stage 5 45 | * [Design](https://github.com/angular/mobile-toolkit/issues/12) 46 | * [Guide](https://mobile.angular.io/guides/app-shell.html) 47 | * [Implementation](https://github.com/angular/mobile-toolkit/tree/master/app-shell/src/app) 48 | * App Shell library parsing fully pre-rendered page to extrapolate app shell 49 | * Stage 2 50 | * [Design](https://github.com/angular/mobile-toolkit/issues/12) 51 | * [Implementation](https://github.com/angular/mobile-toolkit/tree/master/app-shell/src/app/shell-parser) 52 | * Web App Manifest generation incorporated into CLI with --mobile 53 | * Stage 4 54 | * [Guide](https://mobile.angular.io/guides/web-app-manifest.html) 55 | * [Implementation](https://github.com/angular/angular-cli/tree/master/addon/ng2/blueprints/mobile) 56 | * Service Worker static asset pre-caching and offline support 57 | * Stage 3 58 | * [Guide](https://mobile.angular.io/guides/service-worker.html) 59 | * [Implementation](https://github.com/angular/mobile-toolkit/tree/master/service-worker/worker) 60 | * Service Worker script automatically generated by CLI with --mobile 61 | * Stage 4 62 | * [Implementation](https://github.com/angular/angular-cli/blob/master/lib/broccoli/angular2-app.js) 63 | * Service Worker user-visible push notifications 64 | * Stage 0 65 | * End-to-end testing tooling for Service Worker 66 | * Stage 2 67 | 68 | 69 | ### Milestone 2: More Caching! 70 | * Self-updating Service-Worker 71 | * Service Worker Debugging 72 | * Service Worker dynamic data caching w/various strategies 73 | * General-purpose cache hydration protocol in Universal 74 | * Easy to write Web Worker services that interact with DOM 75 | * Explore: make touch event bindings simple and native-like 76 | 77 | 78 | ### Milestone 3: Plumbing 79 | * Service Worker HTTP/2-push-aware code loading protocol 80 | * Service Worker HTTP/-delta pushing and expanding to urls+modules 81 | * App serving and loading guidance (HTTP/2, http2-push, sw+http2, compression, route sharding) 82 | 83 | -------------------------------------------------------------------------------- /app-shell/src/experimental/shell-parser/testing/mock-caches.ts: -------------------------------------------------------------------------------- 1 | function findIndex(array: any[], matcher: Function): number { 2 | for (var i = 0; i < array.length; i++) { 3 | if (matcher(array[i])) { 4 | return i; 5 | } 6 | } 7 | return -1; 8 | } 9 | 10 | export class MockCacheStorage implements CacheStorage { 11 | caches: {[key: string]: MockCache} = {}; 12 | constructor() {} 13 | 14 | delete(cacheName: string): Promise { 15 | if (this.caches.hasOwnProperty(cacheName)) { 16 | delete (this).caches[cacheName]; 17 | return Promise.resolve(true); 18 | } 19 | return Promise.resolve(false); 20 | } 21 | 22 | has(cacheName: string): Promise { 23 | return Promise.resolve(this.caches.hasOwnProperty(cacheName)); 24 | } 25 | 26 | keys(): Promise { 27 | var keys: any[] = []; 28 | for (var cacheName in this.caches) { 29 | keys.push(cacheName); 30 | } 31 | return Promise.resolve(keys); 32 | } 33 | 34 | match(request: Request, options?: CacheOptions): Promise { 35 | if (options !== undefined && options !== null) { 36 | throw 'CacheOptions are unsupported'; 37 | } 38 | var promises: any[] = []; 39 | for (var cacheName in this.caches) { 40 | promises.push((this).caches[cacheName].match(request)); 41 | } 42 | promises.push(Promise.resolve(undefined)); 43 | 44 | var valueOrNextPromiseFn: Function = (value: any) => { 45 | if (value !== undefined || promises.length === 0) { 46 | return value; 47 | } 48 | return promises.shift().then(valueOrNextPromiseFn); 49 | }; 50 | 51 | return promises.shift().then(valueOrNextPromiseFn); 52 | } 53 | 54 | open(cacheName: string): Promise { 55 | if (!this.caches.hasOwnProperty(cacheName)) { 56 | (this).caches[cacheName] = new MockCache(); 57 | } 58 | return Promise.resolve((this).caches[cacheName]); 59 | } 60 | } 61 | 62 | export class MockCache implements Cache { 63 | 64 | entries: MockCacheEntry[] = []; 65 | 66 | add(request: Request): Promise { 67 | throw 'Unimplemented'; 68 | } 69 | 70 | addAll(requests: Request[]): Promise { 71 | return Promise 72 | .all(requests.map((req) => this.add(req))) 73 | .then(() => undefined); 74 | } 75 | 76 | delete(request: Request, options?: CacheOptions): Promise { 77 | if (options !== undefined) { 78 | throw 'CacheOptions are unsupported'; 79 | } 80 | var idx = findIndex(this.entries, (entry: any) => entry.match(request)); 81 | if (idx !== -1) { 82 | this.entries.splice(idx, 1); 83 | } 84 | return Promise.resolve(undefined); 85 | } 86 | 87 | keys(request?: Request, options?: CacheOptions): Promise { 88 | throw 'Unimplemented'; 89 | } 90 | 91 | match(request: Request, options?: CacheOptions): Promise { 92 | if (options !== undefined) { 93 | throw 'CacheOptions are unsupported'; 94 | } 95 | var idx = findIndex(this.entries, (entry: any) => entry.match(request)); 96 | if (idx === -1) { 97 | return Promise.resolve(undefined); 98 | } 99 | return Promise.resolve(this.entries[idx].response.clone()); 100 | } 101 | 102 | matchAll(request: Request, options?: CacheOptions): Promise { 103 | if (options !== undefined) { 104 | throw 'CacheOptions are unsupported'; 105 | } 106 | return Promise.resolve(this 107 | .entries 108 | .filter((entry) => entry.match(request)) 109 | .map((entry) => entry.response.clone())); 110 | } 111 | 112 | put(request: Request, response: Response): Promise { 113 | this.entries.unshift(new MockCacheEntry(request, response)); 114 | return Promise.resolve(undefined); 115 | } 116 | } 117 | 118 | export class MockCacheEntry { 119 | constructor(public request: Request, public response: Response) {} 120 | 121 | match(req: Request): boolean { 122 | return req.url === this.request.url && req.method === this.request.method; 123 | } 124 | } 125 | 126 | -------------------------------------------------------------------------------- /app-shell/src/experimental/shell-parser/node-visitor/resource-inline/inline-style-resource-inline-visitor.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | inject 3 | } from '@angular/core/testing'; 4 | 5 | import {ASTNode} from '../../ast'; 6 | import {MockWorkerScope, MockResponse} from '../../testing'; 7 | import {InlineStyleResourceInlineVisitor} from './'; 8 | 9 | const createResponseHelper = (body: string, contentType: string) => { 10 | const response = new MockResponse(body); 11 | response.headers = { 12 | get(name: string) { 13 | if (name === 'content-type') { 14 | return contentType; 15 | } 16 | } 17 | }; 18 | return response; 19 | }; 20 | 21 | describe('ResourceInlineVisitor', () => { 22 | 23 | let simpleNode: ASTNode; 24 | let nestedAst: ASTNode; 25 | let multiStyles: ASTNode; 26 | let jpgInlineVisitor: InlineStyleResourceInlineVisitor; 27 | let pngJpgInlineVisitor: InlineStyleResourceInlineVisitor; 28 | let scope: MockWorkerScope; 29 | 30 | beforeEach(() => { 31 | scope = new MockWorkerScope(); 32 | jpgInlineVisitor = new InlineStyleResourceInlineVisitor(scope, ['jpg']); 33 | pngJpgInlineVisitor = new InlineStyleResourceInlineVisitor(scope, ['png', 'jpg']); 34 | simpleNode = { 35 | attrs: [{ 36 | name: 'style', 37 | value: 'color: #fff; background-image: url(bar.jpg);' 38 | }], 39 | nodeName: 'div', 40 | }; 41 | multiStyles = { 42 | attrs: [{ 43 | name: 'style', 44 | value: 'color: #fff; background-image: url(bar.jpg);' 45 | }, { 46 | name: 'style', 47 | value: 'color: #fff; background-image: url(baz.jpg);' 48 | }], 49 | nodeName: 'div', 50 | }; 51 | nestedAst = { 52 | nodeName: 'body', 53 | attrs: null, 54 | childNodes: [ 55 | { 56 | nodeName: 'div', 57 | attrs: null, 58 | childNodes: [ 59 | { 60 | nodeName: 'p', 61 | attrs: null, 62 | childNodes: [ 63 | { 64 | nodeName: 'span', 65 | attrs: [{ 66 | name: 'style', 67 | value: 'color: #fff; background-image: url(bar.jpg);' 68 | }], 69 | } 70 | ] 71 | } 72 | ] 73 | }, 74 | { 75 | nodeName: 'section', 76 | attrs: null, 77 | childNodes: [ 78 | { 79 | nodeName: 'span', 80 | attrs: [{ 81 | name: 'style', 82 | value: 'font-size: 42px; background-image: url(bar.png);' 83 | }] 84 | } 85 | ] 86 | } 87 | ] 88 | }; 89 | }); 90 | 91 | it('should inline assets in style attribute', (done: any) => { 92 | scope.mockResponses['bar.jpg'] = createResponseHelper('bar', 'image/jpg'); 93 | jpgInlineVisitor.visit(simpleNode) 94 | .then(() => { 95 | expect(simpleNode.attrs[0].value).toBe('color: #fff; background-image: url(data:image/jpg;base64,YgBhAHIA);'); 96 | done(); 97 | }); 98 | }); 99 | 100 | it('should work with nested elements', (done: any) => { 101 | scope.mockResponses['bar.jpg'] = createResponseHelper('bar', 'image/jpg'); 102 | scope.mockResponses['bar.png'] = createResponseHelper('bar', 'image/png'); 103 | pngJpgInlineVisitor.visit(nestedAst) 104 | .then(() => { 105 | expect(nestedAst.childNodes[0].childNodes[0].childNodes[0].attrs[0].value).toBe('color: #fff; background-image: url(data:image/jpg;base64,YgBhAHIA);'); 106 | expect(nestedAst.childNodes[1].childNodes[0].attrs[0].value).toBe('font-size: 42px; background-image: url(data:image/png;base64,YgBhAHIA);'); 107 | done(); 108 | }); 109 | }); 110 | 111 | it('should always pick the last occurence of the style attribute', (done: any) => { 112 | scope.mockResponses['bar.jpg'] = createResponseHelper('bar', 'image/jpg'); 113 | scope.mockResponses['baz.jpg'] = createResponseHelper('baz', 'image/jpg'); 114 | jpgInlineVisitor.visit(multiStyles) 115 | .then(() => { 116 | expect(multiStyles.attrs[0].value).toBe('color: #fff; background-image: url(bar.jpg);'); 117 | expect(multiStyles.attrs[1].value).toBe('color: #fff; background-image: url(data:image/jpg;base64,YgBhAHoA);'); 118 | done(); 119 | }); 120 | }); 121 | 122 | }); 123 | 124 | -------------------------------------------------------------------------------- /app-shell/src/experimental/shell-parser/node-matcher/css-node-matcher.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | inject 3 | } from '@angular/core/testing'; 4 | import { ASTNode } from '../ast'; 5 | import { CssSelector } from './css-selector'; 6 | import { CssNodeMatcher } from './css-node-matcher'; 7 | 8 | describe('CssNodeMatcher', () => { 9 | 10 | var node: ASTNode; 11 | beforeEach(() => { 12 | node = { 13 | attrs: [ 14 | { name: 'foo', value: 'bar' }, 15 | { name: 'class', value: 'dialog modal--drop' }, 16 | { name: 'id', value: 'dialog-id' } 17 | ], 18 | nodeName: 'div', 19 | parentNode: null 20 | }; 21 | }); 22 | 23 | describe('successful match', () => { 24 | 25 | it('should match any node with empty selector', () => { 26 | const emptySelector = CssSelector.parse(''); 27 | const selector = new CssNodeMatcher(emptySelector); 28 | expect(selector.match(node)).toBe(true); 29 | }); 30 | 31 | it('should match basic element selector', () => { 32 | const elementSelector = CssSelector.parse('div'); 33 | const selector = new CssNodeMatcher(elementSelector); 34 | expect(selector.match(node)).toBe(true); 35 | }); 36 | 37 | it('should match attribute selector', () => { 38 | const attrSelector = CssSelector.parse('[foo=bar]'); 39 | const selector = new CssNodeMatcher(attrSelector); 40 | expect(selector.match(node)).toBe(true); 41 | }); 42 | 43 | it('should match attribute selector when no value is provided', () => { 44 | const attrSelector = CssSelector.parse('[foo]'); 45 | const selector = new CssNodeMatcher(attrSelector); 46 | expect(selector.match(node)).toBe(true); 47 | }); 48 | 49 | it('should match class selector', () => { 50 | const classSelector = CssSelector.parse('.dialog'); 51 | const selector = new CssNodeMatcher(classSelector); 52 | expect(selector.match(node)).toBe(true); 53 | const complexClassSelector = CssSelector.parse('.dialog.modal--drop'); 54 | const complexSelector = new CssNodeMatcher(complexClassSelector); 55 | expect(complexSelector.match(node)).toBe(true); 56 | }); 57 | 58 | it('should match case insensitive class selector', () => { 59 | const classSelector = CssSelector.parse('.DIALOG'); 60 | const selector = new CssNodeMatcher(classSelector); 61 | expect(selector.match(node)).toBe(true); 62 | const complexClassSelector = CssSelector.parse('.dialog.modal--drop'); 63 | const complexSelector = new CssNodeMatcher(complexClassSelector); 64 | expect(complexSelector.match(node)).toBe(true); 65 | }); 66 | 67 | it('should match element by id', () => { 68 | const idSelector = CssSelector.parse('#dialog-id'); 69 | const selector = new CssNodeMatcher(idSelector); 70 | expect(selector.match(node)).toBe(true); 71 | }); 72 | 73 | }); 74 | 75 | describe('unsuccessful match', () => { 76 | 77 | it('should fail when different element is used', () => { 78 | const elementSelector = CssSelector.parse('span'); 79 | const selector = new CssNodeMatcher(elementSelector); 80 | expect(selector.match(node)).toBe(false); 81 | }); 82 | 83 | it('should fail when different attribute selector is provided', () => { 84 | const attrSelector = CssSelector.parse('[foo=qux]'); 85 | const selector = new CssNodeMatcher(attrSelector); 86 | expect(selector.match(node)).toBe(false); 87 | }); 88 | 89 | it('should fail when non-matching class selector is used', () => { 90 | const classSelector = CssSelector.parse('.modal'); 91 | const selector = new CssNodeMatcher(classSelector); 92 | expect(selector.match(node)).toBe(false); 93 | const complexClassSelector = CssSelector.parse('.dialog.modal-drop'); 94 | const complexSelector = new CssNodeMatcher(complexClassSelector); 95 | expect(complexSelector.match(node)).toBe(false); 96 | }); 97 | 98 | it('should fail when superset of attributes is used in selector', () => { 99 | const cssSelector = CssSelector.parse('[foo=bar][baz=bar]'); 100 | const selector = new CssNodeMatcher(cssSelector); 101 | expect(selector.match(node)).toBe(false); 102 | }); 103 | 104 | it('should fail when superset of attributes is used in selector', () => { 105 | const cssSelector = CssSelector.parse('[no-render]'); 106 | const selector = new CssNodeMatcher(cssSelector); 107 | expect(selector.match(node)).toBe(false); 108 | }) 109 | 110 | it('should fail match by id when element has no id', () => { 111 | const cssSelector = CssSelector.parse('#foo'); 112 | const selector = new CssNodeMatcher(cssSelector); 113 | const node1: ASTNode = { 114 | nodeName: 'div', 115 | parentNode: null, 116 | attrs: [] 117 | }; 118 | const node2: ASTNode = { 119 | attrs: [ 120 | { name: 'not-id', value: '' } 121 | ], 122 | nodeName: 'div', 123 | parentNode: null 124 | }; 125 | expect(selector.match(node1)).toBe(false); 126 | expect(selector.match(node2)).toBe(false); 127 | }); 128 | 129 | }); 130 | }); 131 | 132 | -------------------------------------------------------------------------------- /guides/web-app-manifest.md: -------------------------------------------------------------------------------- 1 | # Web App Manifest 2 | 3 | (This guide assumes that the [CLI setup guide](./cli-setup.md) has been completed.) 4 | 5 | Web App Manifest is a new standard feature of the Web Platform, which 6 | allows applications to provide some metadata to help browsers know how 7 | to install the application to the device's home screen. 8 | 9 | The example app we created with Angular CLI in the [previous guide](./cli-setup.md) 10 | already contains a Web App Manifest. Feel free to edit it; the file will be 11 | automatically moved over as Angular CLI builds the app. 12 | 13 | `src/manifest.webapp:` 14 | 15 | ```json 16 | { 17 | "name": "Hello Mobile", 18 | "short_name": "Hello Mobile", 19 | "icons": [ 20 | { 21 | "src": "/android-chrome-36x36.png", 22 | "sizes": "36x36", 23 | "type": "image/png", 24 | "density": 0.75 25 | }, 26 | { 27 | "src": "/android-chrome-48x48.png", 28 | "sizes": "48x48", 29 | "type": "image/png", 30 | "density": 1 31 | }, 32 | { 33 | "src": "/android-chrome-72x72.png", 34 | "sizes": "72x72", 35 | "type": "image/png", 36 | "density": 1.5 37 | }, 38 | { 39 | "src": "/android-chrome-96x96.png", 40 | "sizes": "96x96", 41 | "type": "image/png", 42 | "density": 2 43 | }, 44 | { 45 | "src": "/android-chrome-144x144.png", 46 | "sizes": "144x144", 47 | "type": "image/png", 48 | "density": 3 49 | }, 50 | { 51 | "src": "/android-chrome-192x192.png", 52 | "sizes": "192x192", 53 | "type": "image/png", 54 | "density": 4 55 | } 56 | ], 57 | "theme_color": "#000000", 58 | "background_color": "#e0e0e0", 59 | "start_url": "/index.html", 60 | "display": "standalone", 61 | "orientation": "portrait" 62 | } 63 | ``` 64 | 65 | ## Customization 66 | 67 | All of the required icons are also present in the project automatically. Though, 68 | it's the Angular logo, and you'll probably want to update it with your own. 69 | 70 | You'll also want to make sure the `short_name` property **12 characters** or less so 71 | it will fit nicely under the app icon on the home screen, without being truncated. 72 | 73 | Tip: Use [http://realfavicongenerator.net/](http://realfavicongenerator.net/) to easily 74 | create correctly-sized icons from your app's logo. 75 | 76 | Since this file is already referenced by `src/index.html`, there's no additional 77 | work needed to help browsers know how to install your app. 78 | 79 | `src/index.html` 80 | 81 | ``` 82 | ... 83 | 84 | ... 85 | ``` 86 | 87 | ## How Users Install the App 88 | 89 | Browsers support installation in various ways. In **Chrome on Android**, 90 | an app can be installed by selecting "Add to Home Screen" from the 91 | menu. Chrome on Android also will automatically prompt a user to install 92 | an app to home screen if the user meets certain usage criteria[1](#footnote1). 93 | 94 | **Opera on Android** allows users to install apps by clicking a "+" icon in the browser url 95 | bar, and will also prompt users to install the app, using the same criteria 96 | as Chrome[2](#footnote2). 97 | 98 | **Firefox on Android** has support for adding to home screen, and will use icons and short_name 99 | from the Web App Manifest. Work is planned to support automatic prompts to install[3](#footnote3). 100 | 101 | **Safari on iOS** doesn't yet support Web App Manifest, but does allow for proprietary meta tags to 102 | be added to a page to let users add it to the home screen with proper icons, title, and display 103 | mode[4](#footnote4). It's up to the user to install the app to home screen from the 104 | browser menu. The app generated by Angular CLI automatically includes the correct 105 | icons and meta tags to allow installing to home screen on Safari. 106 | 107 | 108 | ## Further Reading 109 | 110 | * [Web App Manifest](https://developer.mozilla.org/en-US/docs/Web/Manifest) (Mozilla Developer Network) 111 | * [Installable Web Apps with the Web App Manifest in Chrome for Android](https://developers.google.com/web/updates/2014/11/Support-for-installable-web-apps-with-webapp-manifest-in-chrome-38-for-Android?hl=en) (Google Developers) 112 | * [Installable Web Apps and Add to Home screen](https://dev.opera.com/articles/installable-web-apps/) (Dev.Opera) 113 | 114 | --- 115 | 116 | ## [Next, let's build our App Shell.](./app-shell.md) 117 | 118 | --- 119 | 120 | ### Footnotes 121 | 122 | 1. [Increasing Engagement with Web App Install Banners 123 | ](https://developers.google.com/web/updates/2015/03/increasing-engagement-with-app-install-banners-in-chrome-for-android?hl=en) (Google Developers) 124 | 2. [Progressive Web App install banners come to Opera for Android](https://dev.opera.com/blog/web-app-install-banners/) (Dev.Opera) 125 | 3. [Bug 1212648 - Progressive Web Apps Support](https://bugzilla.mozilla.org/show_bug.cgi?id=1212648) (Bugzilla@Mozilla) 126 | 4. [Configuring Web Applications](https://developer.apple.com/library/ios/documentation/AppleApplications/Reference/SafariWebContent/ConfiguringWebApplications/ConfiguringWebApplications.html) (iOS Developer Library) -------------------------------------------------------------------------------- /service-worker/worker/src/plugins/dynamic/strategy/freshness.ts: -------------------------------------------------------------------------------- 1 | import {DynamicGroup, DynamicStrategy, ResponseWithSideEffect, maybeRun} from '../group'; 2 | import {CacheConfig} from '../manifest'; 3 | import {Clock} from '../../../worker'; 4 | 5 | /** 6 | * Extension of the caching configuration specifically for freshness-optimized 7 | * caching. 8 | */ 9 | export interface FreshnessCacheConfig extends CacheConfig { 10 | optimizeFor: "freshness"; 11 | 12 | /** 13 | * A timeout that if provided, will cause network requests to be abandoned 14 | * in favor of cached values if they take longer than the provided timeout. 15 | */ 16 | networkTimeoutMs?: number; 17 | } 18 | 19 | /** 20 | * A dynamic caching strategy which optimizes for the freshness of data it 21 | * returns, by always attempting a server fetch first. 22 | * 23 | * In the freshness strategy, requests are always sent to the server first. 24 | * If the network request times out (according to the timeout value passed 25 | * in the configuration), cached values are used instead, if available. 26 | * 27 | * If the network request times out but the cache does not contain data, 28 | * the network value will still be returned eventually. 29 | * 30 | * Regardless of whether the request times out or not, if the network fetch 31 | * eventually completes then the result is cached for future use. 32 | */ 33 | export class FreshnessStrategy implements DynamicStrategy { 34 | /** 35 | * Name of the strategy (matched to the value in `optimizeFor`). 36 | */ 37 | get name() { 38 | return 'freshness'; 39 | } 40 | 41 | /** 42 | * Reads the cache configuration from the group's config. 43 | */ 44 | config(group: DynamicGroup): FreshnessCacheConfig { 45 | return group.config.cache as FreshnessCacheConfig; 46 | } 47 | 48 | /** 49 | * Makes a request using this strategy, falling back on the `delegate` if 50 | * the cache is not being used. 51 | */ 52 | fetch(group: DynamicGroup, req: Request, delegate: () => Promise): Promise { 53 | // Firstly, read the configuration. 54 | const config = this.config(group); 55 | const unrestrictedFetch = group 56 | // Make a request to the network and cache the result, irrespective of the 57 | // timeout. 58 | .fetchAndCache(req, delegate) 59 | // If this operation fails (note that a failed HTTP status code is still 60 | // counted as success, treat it as an unavailable response. 61 | // TODO: allow more control over what constitutes request failure and 62 | // what happens in the case of failure. 63 | .catch(() => ({response: null} as ResponseWithSideEffect)); 64 | 65 | // By default, wait for the network request indefinitely. 66 | let networkFetch = unrestrictedFetch; 67 | 68 | // If a timeout is defined, then only wait that long before reverting to 69 | // the cache. 70 | if (!!config.networkTimeoutMs) { 71 | // Race the indefinite fetch operation with a timer that returns a null 72 | // response after the configured network timeout. 73 | networkFetch = Promise.race([ 74 | unrestrictedFetch, 75 | this 76 | .timeout(config.networkTimeoutMs, group.clock) 77 | .then(() => ({response: null})), 78 | ]); 79 | } 80 | 81 | return networkFetch 82 | .then(rse => { 83 | if (rse.response === null) { 84 | // Network request failed or timed out. Check the cache to see if 85 | // this request is available there. 86 | return group 87 | .fetchFromCache(req) 88 | .then(cacheRse => { 89 | // Regardless of whether the cache hit, the network request may 90 | // still be going, so set up a side effect that runs the cache 91 | // effect first and the network effect following. This ensures 92 | // the network result will be cached if/when it comes back. 93 | const sideEffect = () => maybeRun(cacheRse.sideEffect) 94 | .then(() => unrestrictedFetch) 95 | .then(netRse => maybeRun(netRse.sideEffect)); 96 | 97 | // Check whether the cache had the data or not. 98 | if (cacheRse.response !== null) { 99 | // Cache hit, the response is available in the cache. 100 | return { 101 | response: cacheRse.response, 102 | cacheAge: cacheRse.cacheAge, 103 | sideEffect, 104 | } as ResponseWithSideEffect; 105 | } else { 106 | // The cache was missing the data. Right now, just fall back 107 | // on the indefinite fetch from the network. 108 | return unrestrictedFetch; 109 | } 110 | }); 111 | } else { 112 | // The network returned in time, no need to consult the cache. 113 | return rse; 114 | } 115 | }); 116 | } 117 | 118 | /** 119 | * Constructs a promise that resolves after a delay. 120 | */ 121 | private timeout(delay: number, clock: Clock): Promise { 122 | return new Promise(resolve => clock.setTimeout(resolve, delay)); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /app-shell/src/experimental/shell-parser/node-visitor/resource-inline/stylesheet-resource-inline-visitor.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | inject 3 | } from '@angular/core/testing'; 4 | 5 | import {ASTNode} from '../../ast'; 6 | import {MockWorkerScope, MockResponse} from '../../testing'; 7 | import {StylesheetResourceInlineVisitor} from './'; 8 | 9 | const createResponseHelper = (body: string, contentType: string) => { 10 | const response = new MockResponse(body); 11 | response.headers = { 12 | get(name: string) { 13 | if (name === 'content-type') { 14 | return contentType; 15 | } 16 | } 17 | }; 18 | return response; 19 | }; 20 | 21 | describe('ResourceInlineVisitor', () => { 22 | 23 | let astRoot: ASTNode; 24 | let nestedAst: ASTNode; 25 | let simpleNode: ASTNode; 26 | let jpgInlineVisitor: StylesheetResourceInlineVisitor; 27 | let pngJpgInlineVisitor: StylesheetResourceInlineVisitor; 28 | let scope: MockWorkerScope; 29 | 30 | beforeEach(() => { 31 | scope = new MockWorkerScope(); 32 | jpgInlineVisitor = new StylesheetResourceInlineVisitor(scope, ['jpg']); 33 | pngJpgInlineVisitor = new StylesheetResourceInlineVisitor(scope, ['png', 'jpg']); 34 | simpleNode = { 35 | attrs: null, 36 | nodeName: 'style', 37 | childNodes: [ 38 | { 39 | nodeName: '#text', 40 | attrs: null, 41 | value: ` 42 | .bar { 43 | background-image: url(bar.jpg); 44 | } 45 | ` 46 | } 47 | ] 48 | }; 49 | 50 | astRoot = { 51 | attrs: null, 52 | nodeName: 'style', 53 | childNodes: [ 54 | { 55 | nodeName: '#text', 56 | attrs: null, 57 | value: ` 58 | .bar { 59 | background-image: url('bar.jpg'); 60 | background-color: #ccc; 61 | } 62 | .baz { 63 | background-image: url(bar.jpg); 64 | } 65 | .baz { 66 | background-image: url("foo.png"); 67 | } 68 | ` 69 | } 70 | ] 71 | }; 72 | 73 | nestedAst = { 74 | nodeName: 'body', 75 | attrs: null, 76 | childNodes: [ 77 | { 78 | nodeName: 'div', 79 | attrs: null, 80 | childNodes: [ 81 | { 82 | nodeName: 'style', 83 | attrs: null, 84 | childNodes: [ 85 | { 86 | nodeName: '#text', 87 | attrs: null, 88 | value: ` 89 | bar { 90 | background-image: url('bar.jpg'); 91 | } 92 | ` 93 | } 94 | ] 95 | } 96 | ] 97 | }, 98 | { 99 | nodeName: 'style', 100 | attrs: null, 101 | childNodes: [ 102 | { 103 | nodeName: '#text', 104 | attrs: null, 105 | value: ` 106 | baz { 107 | background-image: url(bar.jpg); 108 | } 109 | foo { 110 | background-image: url('bar.svg'); 111 | } 112 | ` 113 | } 114 | ] 115 | } 116 | ] 117 | }; 118 | }); 119 | 120 | it('should replace image with base64 representation', (done: any) => { 121 | scope.mockResponses['bar.jpg'] = createResponseHelper('image', 'image/jpg'); 122 | jpgInlineVisitor.visit(simpleNode) 123 | .then(() => { 124 | expect(simpleNode.childNodes[0]).not.toBeFalsy(); 125 | expect(simpleNode.childNodes[0].value).toBe( 126 | ` 127 | .bar { 128 | background-image: url(data:image/jpg;base64,aQBtAGEAZwBlAA==); 129 | } 130 | `); 131 | done(); 132 | }); 133 | }); 134 | 135 | it('should replace images in multiple styles', (done: any) => { 136 | scope.mockResponses['foo.png'] = createResponseHelper('foo', 'image/png'); 137 | scope.mockResponses['bar.jpg'] = createResponseHelper('bar', 'image/jpg'); 138 | pngJpgInlineVisitor.visit(astRoot) 139 | .then(() => { 140 | const styles = astRoot.childNodes[0].value; 141 | expect(styles).not.toBeFalsy(); 142 | expect(styles).toBe(` 143 | .bar { 144 | background-image: url('data:image/jpg;base64,YgBhAHIA'); 145 | background-color: #ccc; 146 | } 147 | .baz { 148 | background-image: url(data:image/jpg;base64,YgBhAHIA); 149 | } 150 | .baz { 151 | background-image: url("data:image/png;base64,ZgBvAG8A"); 152 | } 153 | ` 154 | ); 155 | done(); 156 | }); 157 | }); 158 | 159 | it('should handle invalid requests', (done: any) => { 160 | jpgInlineVisitor.visit(simpleNode) 161 | .then(() => { 162 | expect(simpleNode.childNodes[0].value).toBe( 163 | ` 164 | .bar { 165 | background-image: url(bar.jpg); 166 | } 167 | `); 168 | done(); 169 | }); 170 | }); 171 | 172 | it('should work with nested elements', (done: any) => { 173 | scope.mockResponses['bar.jpg'] = createResponseHelper('bar', 'image/jpg'); 174 | pngJpgInlineVisitor.visit(nestedAst) 175 | .then(() => { 176 | let s1 = nestedAst.childNodes[0].childNodes[0].childNodes[0].value; 177 | let s2 = nestedAst.childNodes[1].childNodes[0].value; 178 | expect(s1).toBe(` 179 | bar { 180 | background-image: url('data:image/jpg;base64,YgBhAHIA'); 181 | } 182 | `); 183 | expect(s2).toBe(` 184 | baz { 185 | background-image: url(data:image/jpg;base64,YgBhAHIA); 186 | } 187 | foo { 188 | background-image: url('bar.svg'); 189 | } 190 | `); 191 | done(); 192 | }); 193 | }); 194 | 195 | }); 196 | 197 | -------------------------------------------------------------------------------- /service-worker/worker/src/plugins/dynamic/dynamic.ts: -------------------------------------------------------------------------------- 1 | import {FetchDelegate, FetchInstruction, Operation, Plugin, PluginFactory, UrlMatcher, NgSwCache, ScopedCache, VersionWorker, VersionWorkerImpl} from '@angular/service-worker/worker'; 2 | import {DynamicGroup, ResponseWithSideEffect, DynamicStrategy, DynamicStrategyMap} from './group'; 3 | import {DynamicManifest} from './manifest'; 4 | 5 | export function Dynamic(strategies: DynamicStrategy[]): PluginFactory { 6 | return (worker: VersionWorker) => new DynamicImpl(worker as VersionWorkerImpl, strategies); 7 | } 8 | 9 | /** 10 | * A plugin which implements dynamic content caching - the caching of requests to 11 | * arbitrary URLs. 12 | */ 13 | export class DynamicImpl implements Plugin { 14 | 15 | /** 16 | * The manifest configured by the user. 17 | */ 18 | private manifest: DynamicManifest; 19 | 20 | /** 21 | * All `DynamicGroup`s configured, each one representing a group defined in 22 | * the manifest. 23 | */ 24 | private group: DynamicGroup[]; 25 | 26 | /** 27 | * Map of `optimizeFor` strategies to their implementations. 28 | */ 29 | private strategies: DynamicStrategyMap = {}; 30 | 31 | /** 32 | * `Promise` that tracks side effect application after requests have completed. This 33 | * is used to serialize application of side effects, even if requests are executed 34 | * in parallel. 35 | */ 36 | private sideEffectQueue: Promise; 37 | 38 | constructor(public worker: VersionWorkerImpl, strategies: DynamicStrategy[]) { 39 | // Extract the dynamic section of the manifest. 40 | this.manifest = worker.manifest['dynamic']; 41 | 42 | // Initially there are no side effects. 43 | this.sideEffectQueue = Promise.resolve(); 44 | 45 | // Build the `strategies` map from all configured strategies. 46 | strategies.forEach(strategy => this.strategies[strategy.name] = strategy); 47 | } 48 | 49 | /** 50 | * After installation, setup the group array for immediate use. On 51 | * subsequent startups, this step is performed by `validate()`. 52 | */ 53 | setup(ops: Operation[]): void { 54 | // If no dynamic caching configuration is provided, skip this plugin. 55 | if (!this.manifest) { 56 | return; 57 | } 58 | // Ensure even on first installation, the cache groups are loaded and 59 | // ready to serve traffic. 60 | ops.push(() => this._setupGroups()); 61 | } 62 | 63 | fetch(req: Request): FetchInstruction|null { 64 | // If no dynamic caching configuration is provided, skip this plugin. 65 | if (!this.manifest) { 66 | return null; 67 | } 68 | 69 | // Return an instruction that applies dynamic content caching. 70 | const instruction: FetchInstruction = (next: FetchDelegate): Promise => { 71 | // There may be multiple groups configured. Check whether the request matches any 72 | // of them. 73 | const groups = this.group.filter(group => group.matches(req)); 74 | if (groups.length === 0) { 75 | // It doesn't match any groups - continue down the chain. 76 | return next(); 77 | } 78 | 79 | // It has matched at least one group. Only the first group is considered. 80 | return this 81 | // First, wait for any pending side effects to finish. This is precautionary, 82 | // more testing and design work is required to verify that multiple requests 83 | // can be processed before their side effects are fully resolved. 84 | .sideEffectQueue 85 | // After any pending side effects, route the fetch to the group. The group 86 | // will handle the request according to its configuration and return the 87 | // chosen response, along with an optional side effect (side effects are 88 | // things like updating the persisted cache state). 89 | .then(() => groups[0].fetch(req, next)) 90 | // Primarily extract the response from the result and return it, but also 91 | // queue the side effect to run after sideEffectQueue resolves. Because of 92 | // the .sideEffectQueue in the chain above, this will likely run effect() 93 | // immediately, but the chain will not wait on effect() to fully resolve 94 | // before moving on. 95 | .then(result => { 96 | if (!!result.sideEffect) { 97 | // If there is a side effect, queue it to happen asynchronously. 98 | const effect = result.sideEffect; 99 | this.sideEffectQueue = this 100 | // Wait for the last effect to finish. 101 | .sideEffectQueue 102 | // Apply the new effect. 103 | .then(() => effect()) 104 | // Errors shouldn't crash the side effect chain. 105 | // TODO: log errors somewhere 106 | .catch(() => {}); 107 | } 108 | // Extract the response and return it. 109 | return result.response; 110 | }); 111 | }; 112 | return instruction; 113 | } 114 | 115 | /** 116 | * Ensure all configuration is valid and the Dynamic plugin is ready to serve 117 | * traffic. 118 | */ 119 | validate(): Promise { 120 | // If no configuration was provided, this plugin is not active. 121 | if (!this.manifest) { 122 | return Promise.resolve(true); 123 | } 124 | 125 | return this 126 | ._setupGroups() 127 | // Success or failure depends on the error state. 128 | .then(() => true) 129 | .catch(() => false); 130 | } 131 | 132 | /* 133 | * For every group configured in the manifest, instantiate the DynamicGroup 134 | * associated with it, which will validate the configuration. This is an async 135 | * operation as initializing the DynamicGroup involves loading stored state 136 | * from the cache. 137 | */ 138 | private _setupGroups(): Promise { 139 | return Promise 140 | // Open a DynamicGroup for each configured group. 141 | .all(this.manifest.group.map(config => 142 | DynamicGroup.open(config, this.worker.adapter, this.worker.cache, this.worker.clock, this.strategies))) 143 | // Once all groups are active, assign the array to this.group which is needed 144 | // to serve requests to the groups. 145 | .then(groups => this.group = groups); 146 | } 147 | } 148 | --------------------------------------------------------------------------------