├── src └── lib │ ├── module.spec.ts │ ├── ng-package.json │ ├── typings.d.ts │ ├── index.ts │ ├── public-api.ts │ ├── common │ ├── index.ts │ ├── tokens.ts │ ├── preboot.mocks.ts │ ├── get-node-key.ts │ ├── get-node-key.spec.ts │ └── preboot.interfaces.ts │ ├── api │ ├── index.ts │ ├── event.replayer.spec.ts │ ├── inline.preboot.code.spec.ts │ ├── event.recorder.spec.ts │ ├── inline.preboot.code.ts │ ├── event.replayer.ts │ └── event.recorder.ts │ ├── tsconfig.spec.json │ ├── module.ts │ ├── tsconfig.lib.json │ ├── package.json │ └── provider.ts ├── integration ├── src │ ├── main.ts │ ├── index.html │ ├── postrender.ts │ ├── prerender.ts │ ├── webpack.config.js │ ├── tsconfig.prerender.json │ ├── tsconfig.postrender.json │ ├── tsconfig.json │ └── app │ │ └── app.module.ts ├── .gitignore ├── bs-config.json ├── e2e │ ├── tsconfig.json │ ├── e2e.utils.ts │ └── e2e.browser.spec.ts ├── protractor.conf.js ├── build.js └── package.json ├── prettier.config.js ├── bs-config.json ├── .editorconfig ├── tools ├── tsconfig.json ├── tslint-rules │ ├── tsLoaderRule.js │ ├── noExposedTodoRule.ts │ ├── noHostDecoratorInConcreteRule.ts │ ├── noRxjsPatchImportsRule.ts │ ├── requireLicenseBannerRule.ts │ └── validateDecoratorsRule.ts └── package-tools │ ├── find-build-config.ts │ └── build-config.ts ├── .gitignore ├── tsconfig.json ├── .github ├── PULL_REQUEST_TEMPLATE.md └── ISSUE_TEMPLATE.md ├── LICENSE ├── karma.conf.js ├── package.json ├── tslint.json ├── .circleci └── config.yml ├── karma-test-shim.js ├── CONTRIBUTING.md └── README.md /src/lib/module.spec.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /integration/src/main.ts: -------------------------------------------------------------------------------- 1 | export * from './app/app.module'; 2 | -------------------------------------------------------------------------------- /integration/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | src/**/*.js 4 | *.js.map 5 | e2e/**/*.js 6 | e2e/**/*.js.map 7 | out-tsc/* 8 | dist/* -------------------------------------------------------------------------------- /integration/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Preboot Test 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: 'typescript', 3 | printWidth: 140, 4 | tabWidth: 2, 5 | singleQuote: true, 6 | semi: true 7 | }; 8 | -------------------------------------------------------------------------------- /bs-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "server": { 3 | "baseDir": "src/demo", 4 | "routes": { 5 | "/node_modules": "node_modules", 6 | "/preboot": "src/lib" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/lib/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist", 4 | "assets": [], 5 | "lib": { 6 | "entryFile": "public-api.ts" 7 | } 8 | } -------------------------------------------------------------------------------- /integration/bs-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "open": false, 3 | "logLevel": "silent", 4 | "port": 9393, 5 | "server": { 6 | "baseDir": "dist", 7 | "middleware": { 8 | "0": null 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/lib/typings.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google LLC All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | -------------------------------------------------------------------------------- /src/lib/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google LLC All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | export * from './public-api'; 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | 12 | [*.md] 13 | max_line_length = 0 14 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /src/lib/public-api.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google LLC All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | export * from './common/index'; 9 | export * from './api/index'; 10 | export * from './module'; 11 | -------------------------------------------------------------------------------- /integration/src/postrender.ts: -------------------------------------------------------------------------------- 1 | import 'zone.js'; 2 | import {platformBrowser} from '@angular/platform-browser'; 3 | import {AppBrowserModule} from './app/app.module'; 4 | 5 | // here we are adding the client bootstrap as a function on the window 6 | (window).bootstrapPrebootClient = function () { 7 | platformBrowser().bootstrapModule(AppBrowserModule); 8 | }; 9 | -------------------------------------------------------------------------------- /src/lib/common/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google LLC All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | export * from './get-node-key'; 9 | export * from './preboot.interfaces'; 10 | export * from './tokens'; 11 | -------------------------------------------------------------------------------- /src/lib/api/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google LLC All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | export * from './event.replayer'; 9 | export * from './event.recorder'; 10 | export * from './inline.preboot.code'; 11 | -------------------------------------------------------------------------------- /src/lib/common/tokens.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google LLC All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | import {InjectionToken} from '@angular/core'; 9 | 10 | export const PREBOOT_NONCE = new InjectionToken('PrebootNonce'); 11 | -------------------------------------------------------------------------------- /src/lib/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.lib", 3 | "compilerOptions": { 4 | "importHelpers": false, 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": ["jasmine"], 8 | "paths": { 9 | "preboot/*": ["./*"] 10 | }, 11 | "experimentalDecorators": true, 12 | "strictNullChecks": false, 13 | "outDir": "../../out-tsc/spec/" 14 | }, 15 | "include": [ 16 | "**/*.spec.ts", 17 | "index.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /tools/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "noUnusedParameters": true, 5 | "lib": ["es2015", "dom", "es2016.array.include"], 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "outDir": "../../out-tsc/lint", 9 | "strictNullChecks": true, 10 | "noEmitOnError": true, 11 | "noImplicitAny": true, 12 | "target": "es5", 13 | "types": [ 14 | "node" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | .idea 5 | .vscode 6 | .DS_Store 7 | **/.DS_Store 8 | 9 | /npm-shrinkwrap.json 10 | 11 | dist/ 12 | dist-tarball/ 13 | node_modules/ 14 | out-tsc/ 15 | debug.log 16 | npm-debug.log 17 | src/**/*.js 18 | src/**/*.js.map 19 | src/**/*.d.ts 20 | utils/**/*.js 21 | utils/**/*.js.map 22 | utils/**/*.d.ts 23 | integration/src/**/*.js 24 | integration/e2e/**/*.js 25 | integration/**/*.js.map 26 | integration/**/*.d.ts 27 | *.metadata.json 28 | *.ngsummary.json 29 | 30 | -------------------------------------------------------------------------------- /tools/tslint-rules/tsLoaderRule.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const Lint = require('tslint'); 3 | 4 | // Custom rule that registers all of the custom rules, written in TypeScript, with ts-node. 5 | // This is necessary, because `tslint` and IDEs won't execute any rules that aren't in a .js file. 6 | require('ts-node').register({ 7 | project: path.join(__dirname, '../tsconfig.json') 8 | }); 9 | 10 | // Add a noop rule so tslint doesn't complain. 11 | exports.Rule = class Rule extends Lint.Rules.AbstractRule { 12 | apply() {} 13 | }; 14 | -------------------------------------------------------------------------------- /integration/src/prerender.ts: -------------------------------------------------------------------------------- 1 | import 'zone.js'; 2 | import {INITIAL_CONFIG, renderModule} from '@angular/platform-server'; 3 | import {readFileSync, writeFileSync} from 'fs-extra'; 4 | 5 | const {AppServerModule} = require('./app/app.module'); 6 | const template = readFileSync('./index.html').toString(); 7 | 8 | const extraProviders = [ 9 | { 10 | provide: INITIAL_CONFIG, 11 | useValue: { 12 | document: template, 13 | url: '/' 14 | } 15 | } 16 | ]; 17 | 18 | renderModule(AppServerModule, {extraProviders}) 19 | .then((html: string) => writeFileSync('../dist/index.html', html)); 20 | -------------------------------------------------------------------------------- /src/lib/api/event.replayer.spec.ts: -------------------------------------------------------------------------------- 1 | import {getMockWindow} from '../common/preboot.mocks'; 2 | import {EventReplayer} from './event.replayer'; 3 | import {PrebootAppData} from '../common/preboot.interfaces'; 4 | 5 | describe('UNIT TEST event.replayer', function () { 6 | describe('switchBuffer()', function () { 7 | it('will do nothing if nothing passed in', function () { 8 | const eventReplayer = new EventReplayer(); 9 | const appData = {}; 10 | 11 | eventReplayer.setWindow(getMockWindow()); 12 | eventReplayer.switchBuffer(appData); 13 | }); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "stripInternal": false, 5 | "experimentalDecorators": true, 6 | "noUnusedParameters": true, 7 | "strictNullChecks": true, 8 | "importHelpers": true, 9 | "newLine": "lf", 10 | "moduleResolution": "node", 11 | "sourceMap": true, 12 | "inlineSources": true, 13 | "lib": [ 14 | "es2015", 15 | "dom" 16 | ], 17 | "skipLibCheck": true, 18 | "noImplicitAny": true 19 | }, 20 | "include": [ 21 | "src/**/*.ts", 22 | "tools/**/*.ts" 23 | ], 24 | "exclude": [ 25 | "node_modules/" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /tools/package-tools/find-build-config.ts: -------------------------------------------------------------------------------- 1 | import {resolve, dirname, join} from 'path'; 2 | import {existsSync} from 'fs'; 3 | 4 | /** Name of the build config file. */ 5 | const BUILD_CONFIG_FILENAME = 'build-config.js'; 6 | 7 | /** Method that searches for a build config file that will be used for packaging. */ 8 | export function findBuildConfig(): string | null { 9 | let currentDir = process.cwd(); 10 | 11 | while (!existsSync(resolve(currentDir, BUILD_CONFIG_FILENAME))) { 12 | let parentDir = dirname(currentDir); 13 | 14 | if (parentDir === currentDir) { 15 | return null; 16 | } 17 | 18 | currentDir = parentDir; 19 | } 20 | 21 | return join(currentDir, BUILD_CONFIG_FILENAME); 22 | } 23 | -------------------------------------------------------------------------------- /integration/src/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | 4 | module.exports = { 5 | mode: 'production', 6 | entry: [ 7 | './node_modules/core-js/client/shim.min.js', 8 | './out-tsc/e2e/postrender.js', 9 | ], 10 | output: { 11 | path: path.resolve('dist'), 12 | filename: 'postrender.js' 13 | }, 14 | 15 | resolve: { 16 | symlinks: true 17 | }, 18 | 19 | // todo: remove this if causing any problems 20 | module: { 21 | exprContextCritical: false 22 | }, 23 | plugins: [ 24 | new webpack.ContextReplacementPlugin( 25 | /angular(\\|\/)core(\\|\/)(esm(\\|\/)src|src)(\\|\/)linker/, 26 | __dirname 27 | ), 28 | ] 29 | }; 30 | -------------------------------------------------------------------------------- /integration/e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "emitDecoratorMetadata": true, 5 | "stripInternal": false, 6 | "experimentalDecorators": true, 7 | "noUnusedParameters": false, 8 | "strictNullChecks": true, 9 | "module": "commonjs", 10 | "moduleResolution": "node", 11 | "noEmitOnError": true, 12 | "noImplicitAny": true, 13 | "inlineSources": true, 14 | "outDir": "../out-tsc/e2e/", 15 | "rootDir": ".", 16 | "sourceMap": true, 17 | "target": "es5", 18 | "lib": ["es2015", "dom"], 19 | "skipLibCheck": true, 20 | "typeRoots": [ 21 | "../node_modules/@types" 22 | ], 23 | "types": [ 24 | "jasmine" 25 | ] 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /integration/src/tsconfig.prerender.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": false, 4 | "stripInternal": false, 5 | "experimentalDecorators": true, 6 | "emitDecoratorMetadata": true, 7 | "noUnusedParameters": false, 8 | "strictNullChecks": true, 9 | "noEmitOnError": true, 10 | "noImplicitAny": true, 11 | "module": "commonjs", 12 | "moduleResolution": "node", 13 | "sourceMap": true, 14 | "outDir": "../out-tsc/e2e/", 15 | "rootDir": ".", 16 | "target": "es2015", 17 | "lib": ["es2015", "dom"], 18 | "skipLibCheck": true, 19 | "types": [], 20 | "suppressImplicitAnyIndexErrors": true, 21 | "baseUrl": "." 22 | }, 23 | "files": [ 24 | "prerender.ts" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /integration/src/tsconfig.postrender.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": false, 4 | "stripInternal": false, 5 | "experimentalDecorators": true, 6 | "emitDecoratorMetadata": true, 7 | "noUnusedParameters": false, 8 | "strictNullChecks": true, 9 | "noEmitOnError": true, 10 | "noImplicitAny": true, 11 | "module": "commonjs", 12 | "moduleResolution": "node", 13 | "sourceMap": true, 14 | "outDir": "../out-tsc/e2e/", 15 | "rootDir": ".", 16 | "target": "es2015", 17 | "lib": ["es2015", "dom"], 18 | "skipLibCheck": true, 19 | "types": ["node"], 20 | "suppressImplicitAnyIndexErrors": true, 21 | "baseUrl": "." 22 | }, 23 | "files": [ 24 | "postrender.ts" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /integration/src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "emitDecoratorMetadata": true, 5 | "stripInternal": false, 6 | "experimentalDecorators": true, 7 | "noUnusedParameters": false, 8 | "strictNullChecks": false, 9 | "module": "commonjs", 10 | "moduleResolution": "node", 11 | "noEmitOnError": true, 12 | "noImplicitAny": true, 13 | "inlineSources": true, 14 | "outDir": "../out-tsc/e2e/", 15 | "rootDir": ".", 16 | "sourceMap": true, 17 | "target": "es5", 18 | "lib": ["es2015", "dom"], 19 | "skipLibCheck": true, 20 | "typeRoots": [ 21 | "../../node_modules/@types/!(node)" 22 | ], 23 | "baseUrl": "./" 24 | }, 25 | "files": [ 26 | "main.ts" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /src/lib/module.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google LLC All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | import {ModuleWithProviders, NgModule} from '@angular/core'; 9 | 10 | import {EventReplayer} from './api/event.replayer'; 11 | import {PrebootOptions} from './common/preboot.interfaces'; 12 | import {PREBOOT_OPTIONS, PREBOOT_PROVIDER} from './provider'; 13 | 14 | @NgModule({ 15 | providers: [EventReplayer, PREBOOT_PROVIDER] 16 | }) 17 | export class PrebootModule { 18 | static withConfig(opts: PrebootOptions): ModuleWithProviders { 19 | return { 20 | ngModule: PrebootModule, 21 | providers: [{provide: PREBOOT_OPTIONS, useValue: opts}] 22 | }; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/lib/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "stripInternal": false, 5 | "experimentalDecorators": true, 6 | "noUnusedParameters": true, 7 | "strictNullChecks": true, 8 | "importHelpers": true, 9 | "newLine": "lf", 10 | "module": "es2015", 11 | "moduleResolution": "node", 12 | "outDir": "../../out-tsc/lib-es2015/", 13 | "rootDir": ".", 14 | "rootDirs": [ 15 | ".", 16 | "../../out-tsc/lib-es2015" 17 | ], 18 | "sourceMap": true, 19 | "inlineSources": true, 20 | "target": "es2015", 21 | "lib": ["es2015", "dom"], 22 | "skipLibCheck": true, 23 | "types": [], 24 | "baseUrl": "." 25 | }, 26 | "files": [ 27 | "./public-api.ts", 28 | "./typings.d.ts" 29 | ], 30 | "angularCompilerOptions": { 31 | "enableIvy": false, 32 | "strictMetadataEmit": true, 33 | "skipTemplateCodegen": true, 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /integration/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const {SpecReporter} = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | useAllAngular2AppRoots: true, 8 | specs: ['./out-tsc/e2e/**/*.spec.js'], 9 | baseUrl: 'http://localhost:9393/', 10 | allScriptsTimeout: 11000, 11 | getPageTimeout: 11000, 12 | capabilities: { 13 | browserName: 'chrome', 14 | // For Travis 15 | chromeOptions: { 16 | args: ['--headless'], 17 | binary: require('puppeteer').executablePath(), 18 | } 19 | }, 20 | directConnect: true, 21 | 22 | framework: 'jasmine', 23 | jasmineNodeOpts: { 24 | showColors: true, 25 | defaultTimeoutInterval: 300000, 26 | print: function () { } 27 | }, 28 | onPrepare() { 29 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: 'pretty' } })); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /src/lib/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "preboot", 3 | "version": "8.0.0", 4 | "description": "Record server view events and play back to Angular client view", 5 | "main": "./bundles/preboot.umd.js", 6 | "module": "./esm5/preboot.es5.js", 7 | "es2015": "./esm2015/preboot.js", 8 | "typings": "./preboot.d.ts", 9 | "author": "Jeff Whelpley", 10 | "license": "MIT", 11 | "contributors": [ 12 | "Tobias Bosch ", 13 | "PatrickJS ", 14 | "Jeff Whelpley " 15 | ], 16 | "keywords": [ 17 | "angular", 18 | "preboot", 19 | "ssr", 20 | "prerender", 21 | "universal" 22 | ], 23 | "peerDependencies": { 24 | "@angular/common": ">=11.0.0", 25 | "@angular/core": ">=11.0.0" 26 | }, 27 | "repository": { 28 | "type": "git", 29 | "url": "https://github.com/angular/preboot" 30 | }, 31 | "bugs": { 32 | "url": "https://github.com/angular/preboot/issues" 33 | }, 34 | "homepage": "https://github.com/angular/preboot" 35 | } 36 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | * **Please check if the PR fulfills these requirements** 2 | - [ ] The commit message follows our guidelines: https://github.com/angular/preboot/blob/master/CONTRIBUTING.md#commit-message-format 3 | - [ ] Tests for the changes have been added (for bug fixes / features) 4 | - [ ] Docs have been added / updated (for bug fixes / features) 5 | 6 | * **What modules are related to this pull-request** 7 | - [ ] server side 8 | - [ ] client side 9 | - [ ] inline 10 | - [ ] build process 11 | - [ ] docs 12 | - [ ] tests 13 | 14 | * **What kind of change does this PR introduce?** (Bug fix, feature, docs update, ...) 15 | remove unused proxy imports from some test files 16 | 17 | 18 | * **What is the current behavior?** (You can also link to an open issue here) 19 | 20 | 21 | 22 | * **What is the new behavior (if this is a feature change)?** 23 | 24 | 25 | 26 | * **Does this PR introduce a breaking change?** (What changes might users need to make in their application due to this PR?) 27 | 28 | 29 | * **Other information**: 30 | -------------------------------------------------------------------------------- /tools/package-tools/build-config.ts: -------------------------------------------------------------------------------- 1 | import {findBuildConfig} from './find-build-config'; 2 | 3 | export interface BuildConfig { 4 | /** Current version of the project. */ 5 | projectVersion: string; 6 | /** Required Angular version for the project. */ 7 | angularVersion: string; 8 | /** Path to the root of the project. */ 9 | projectDir: string; 10 | /** Path to the directory where all packages are living. */ 11 | packagesDir: string; 12 | /** Path to the directory where the output will be stored. */ 13 | outputDir: string; 14 | /** License banner that will be placed inside of every bundle. */ 15 | licenseBanner: string; 16 | } 17 | 18 | // Search for a build config by walking up the current working directory of the Node process. 19 | const buildConfigPath = findBuildConfig(); 20 | 21 | if (!buildConfigPath) { 22 | throw 'Preboot Build tools were not able to find a build config. ' + 23 | 'Please create a "build-config.js" file in your project.'; 24 | } 25 | 26 | // Load the config file using a basic CommonJS import. 27 | export const buildConfig = require(buildConfigPath) as BuildConfig; 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2018 Google LLC. 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 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **Note: for support questions, please use the Universal Slack Channcel or https://gitter.im/angular/universal** 2 | 3 | * **I'm submitting a ...** 4 | - [ ] bug report 5 | - [ ] feature request 6 | 7 | * **Which parts of preboot are affected by this issue?** 8 | - [ ] server side 9 | - [ ] client side 10 | - [ ] inline 11 | - [ ] build process 12 | - [ ] docs 13 | - [ ] tests 14 | 15 | 16 | * **Do you want to request a *feature* or report a *bug*?** 17 | 18 | 19 | 20 | * **What is the current behavior?** 21 | 22 | 23 | 24 | * **If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem** by creating a github repo. 25 | 26 | 27 | 28 | * **What is the expected behavior?** 29 | 30 | 31 | 32 | * **What is the motivation / use case for changing the behavior?** 33 | 34 | 35 | 36 | * **Please tell us about your environment:** 37 | 38 | - Browser: [all | Chrome XX | Firefox XX | IE XX | Safari XX ] 39 | - Language: [all | TypeScript X.X | ES6/7 | ES5 ] 40 | - OS: [all | Mac OS X | Windows | Linux ] 41 | - Platform: [all | NodeJs | Java | PHP | .Net | Ruby] 42 | 43 | 44 | 45 | * **Other information** (e.g. detailed explanation, stacktraces, related issues, suggestions how to fix, links for us to have context, eg. stackoverflow, gitter, etc) 46 | -------------------------------------------------------------------------------- /tools/tslint-rules/noExposedTodoRule.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | import * as Lint from 'tslint'; 3 | import * as utils from 'tsutils'; 4 | 5 | const ERROR_MESSAGE = 6 | 'A TODO may only appear in inline (//) style comments. ' + 7 | 'This is meant to prevent a TODO from being accidentally included in any public API docs.'; 8 | 9 | /** 10 | * Rule that walks through all comments inside of the library and adds failures when it 11 | * detects TODO's inside of multi-line comments. TODOs need to be placed inside of single-line 12 | * comments. 13 | */ 14 | export class Rule extends Lint.Rules.AbstractRule { 15 | 16 | apply(sourceFile: ts.SourceFile) { 17 | return this.applyWithWalker(new NoExposedTodoWalker(sourceFile, this.getOptions())); 18 | } 19 | } 20 | 21 | class NoExposedTodoWalker extends Lint.RuleWalker { 22 | 23 | visitSourceFile(sourceFile: ts.SourceFile) { 24 | utils.forEachComment(sourceFile, (text, commentRange) => { 25 | const isTodoComment = text.substring(commentRange.pos, commentRange.end).includes('TODO:'); 26 | 27 | if (commentRange.kind === ts.SyntaxKind.MultiLineCommentTrivia && isTodoComment) { 28 | this.addFailureAt(commentRange.pos, commentRange.end - commentRange.pos, ERROR_MESSAGE); 29 | } 30 | }); 31 | 32 | super.visitSourceFile(sourceFile); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /integration/e2e/e2e.utils.ts: -------------------------------------------------------------------------------- 1 | import { browser, promise } from 'protractor'; 2 | 3 | declare var bootstrapPrebootClient: any; 4 | 5 | const port = 9393; 6 | 7 | export function loadServerView(): promise.Promise { 8 | browser.waitForAngularEnabled(false); 9 | return browser.get(`http://localhost:${port}/`) 10 | .then(() => browser.refresh()); 11 | } 12 | 13 | export function loadClientView() { 14 | return loadClientScript() 15 | .then(() => browser.executeScript('bootstrapPrebootClient()')); 16 | } 17 | 18 | export function loadClientScript() { 19 | return new Promise((resolve) => { 20 | browser.executeScript(function () { 21 | console.log('executeScript()'); 22 | const scriptTag = document.createElement('script'); 23 | scriptTag.type = 'text/javascript'; 24 | scriptTag.src = 'postrender.js'; 25 | document.getElementsByTagName('html')[0].appendChild(scriptTag); 26 | }); 27 | 28 | waitUntilExists(resolve); 29 | }); 30 | } 31 | 32 | export function waitUntilExists(done: Function) { 33 | browser.executeScript(function () { 34 | return (typeof bootstrapPrebootClient !== 'undefined'); 35 | }) 36 | .then((keyExists: boolean) => { 37 | if (keyExists) { 38 | done(); 39 | } else { 40 | setTimeout(() => waitUntilExists(done), 10); 41 | } 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /src/lib/common/preboot.mocks.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google LLC All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | import {PrebootOptions, PrebootWindow} from './preboot.interfaces'; 9 | import {assign, defaultOptions} from '../api/inline.preboot.code'; 10 | 11 | export function getMockWindow(): PrebootWindow { 12 | return { 13 | prebootData: {}, 14 | } as PrebootWindow; 15 | } 16 | 17 | export function getMockOptions(): PrebootOptions { 18 | return assign({}, defaultOptions, { 19 | window: getMockWindow() 20 | }); 21 | } 22 | 23 | export function getMockElement(): Element { 24 | return { 25 | ___attributes: new Map(), 26 | cloneNode: () => { return { style: {} }; }, 27 | parentNode: { 28 | insertBefore: function () {} 29 | }, 30 | hasAttribute(key: string) { 31 | return this.___attributes.has(key); 32 | }, 33 | getAttribute(key: string) { 34 | return this.___attributes.get(key); 35 | }, 36 | setAttribute(key: string, value: string) { 37 | this.___attributes.set(key, value); 38 | }, 39 | removeAttribute(key: string) { 40 | this.___attributes.delete(key); 41 | } 42 | } as any as Element; 43 | } 44 | -------------------------------------------------------------------------------- /integration/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import {Component, enableProdMode, Inject, NgModule, PLATFORM_ID} from '@angular/core'; 2 | import {BrowserModule} from '@angular/platform-browser'; 3 | import {ServerModule} from '@angular/platform-server'; 4 | import {isPlatformBrowser} from '@angular/common'; 5 | import {PrebootModule} from 'preboot'; 6 | 7 | enableProdMode(); 8 | @Component({ 9 | selector: 'app-root', 10 | template: ` 11 |

{{platform}}

12 |

Here is something

13 | 14 | 19 | `, 20 | }) 21 | export class AppComponent { 22 | 23 | platform: string; 24 | 25 | constructor( @Inject(PLATFORM_ID) public _platform: string) { 26 | this.platform = isPlatformBrowser(_platform) ? 'client view' : 'server view'; 27 | } 28 | } 29 | 30 | @NgModule({ 31 | declarations: [AppComponent], 32 | imports: [ 33 | BrowserModule.withServerTransition({ appId: 'foo' }), 34 | PrebootModule.withConfig({ appRoot: 'app-root' }) 35 | ], 36 | bootstrap: [AppComponent] 37 | }) 38 | export class AppBrowserModule { } 39 | 40 | @NgModule({ 41 | imports: [ 42 | AppBrowserModule, 43 | ServerModule 44 | ], 45 | bootstrap: [AppComponent] 46 | }) 47 | export class AppServerModule { } 48 | -------------------------------------------------------------------------------- /integration/e2e/e2e.browser.spec.ts: -------------------------------------------------------------------------------- 1 | import {browser, $} from 'protractor'; 2 | import { loadServerView, loadClientView } from './e2e.utils'; 3 | 4 | describe('e2e test preboot', function () { 5 | 6 | it('should validate server view', function () { 7 | loadServerView() 8 | .then(() => $('h1').getText()) 9 | .then(text => expect(text).toEqual('server view')); 10 | }); 11 | 12 | it('should validate basic client view', function () { 13 | loadServerView() 14 | .then(() => loadClientView()) 15 | .then(() => $('h1').getText()) 16 | .then(text => expect(text).toEqual('client view')); 17 | }); 18 | 19 | it('should validate typing input to a text box', function () { 20 | const input = 'foo man choo'; 21 | 22 | loadServerView() 23 | .then(() => $('#myTextBox').click()) 24 | .then(() => browser.actions().sendKeys(input).perform()) 25 | .then(() => loadClientView()) 26 | .then(() => $('#myTextBox').getAttribute('value')) 27 | .then(actual => expect(actual).toEqual(input)); 28 | }); 29 | 30 | it('should validate choosing from a select', function () { 31 | const expected = 'foo'; 32 | 33 | loadServerView() 34 | .then(() => $('#mySelect').click()) 35 | .then(() => $('#myVal').click()) 36 | .then(() => loadClientView()) 37 | .then(() => $('#mySelect').$('option:checked').getText()) 38 | .then(actual => expect(actual).toEqual(expected)); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /tools/tslint-rules/noHostDecoratorInConcreteRule.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | import * as Lint from 'tslint'; 3 | 4 | export class Rule extends Lint.Rules.AbstractRule { 5 | apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { 6 | return this.applyWithWalker(new Walker(sourceFile, this.getOptions())); 7 | } 8 | } 9 | 10 | class Walker extends Lint.RuleWalker { 11 | visitClassDeclaration(node: ts.ClassDeclaration) { 12 | if (!node.modifiers || !this.getOptions().length) { return; } 13 | 14 | // Do not check the class if its abstract. 15 | if (!!node.modifiers.find(modifier => modifier.kind === ts.SyntaxKind.AbstractKeyword)) { 16 | return; 17 | } 18 | 19 | node.members 20 | .filter(el => el.decorators) 21 | .map(el => el.decorators!) 22 | .forEach(decorators => { 23 | decorators.forEach(decorator => { 24 | const decoratorText: string = decorator.getChildAt(1).getText(); 25 | const matchedDecorator: string = this.getOptions().find( 26 | (item: string) => decoratorText.startsWith(item)); 27 | if (!!matchedDecorator) { 28 | this.addFailureFromStartToEnd(decorator.getChildAt(1).pos - 1, decorator.end, 29 | `The @${matchedDecorator} decorator may only be used in abstract classes. In ` + 30 | `concrete classes use \`host\` in the component definition instead.`); 31 | } 32 | }); 33 | }); 34 | 35 | super.visitClassDeclaration(node); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/lib/common/get-node-key.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google LLC All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | import { NodeContext } from './preboot.interfaces'; 9 | 10 | /** 11 | * Attempt to generate key from node position in the DOM 12 | */ 13 | export function getNodeKeyForPreboot(nodeContext: NodeContext): string { 14 | const ancestors: Element[] = []; 15 | const root = nodeContext.root; 16 | const node = nodeContext.node; 17 | let temp: Element | null = node; 18 | 19 | // walk up the tree from the target node up to the root 20 | while (temp && temp !== root.serverNode && temp !== root.clientNode) { 21 | ancestors.push(temp); 22 | temp = temp.parentNode as Element; 23 | } 24 | 25 | // note: if temp doesn't exist here it means root node wasn't found 26 | if (temp) { 27 | ancestors.push(temp); 28 | } 29 | 30 | // now go backwards starting from the root, appending the appName to unique 31 | // identify the node later.. 32 | const name = node.nodeName || 'unknown'; 33 | let key = name; 34 | const len = ancestors.length; 35 | 36 | for (let i = len - 1; i >= 0; i--) { 37 | temp = ancestors[i]; 38 | 39 | if (temp.childNodes && i > 0) { 40 | for (let j = 0; j < temp.childNodes.length; j++) { 41 | if (temp.childNodes[j] === ancestors[i - 1]) { 42 | key += '_s' + (j + 1); 43 | break; 44 | } 45 | } 46 | } 47 | } 48 | 49 | return key; 50 | } 51 | -------------------------------------------------------------------------------- /tools/tslint-rules/noRxjsPatchImportsRule.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as ts from 'typescript'; 3 | import * as Lint from 'tslint'; 4 | import * as minimatch from 'minimatch'; 5 | 6 | const ERROR_MESSAGE = 'Uses of RxJS patch imports are forbidden.'; 7 | 8 | /** 9 | * Rule that prevents uses of RxJS patch imports (e.g. `import 'rxjs/add/operator/map'). 10 | * Supports whitelisting via `"no-patch-imports": [true, "\.spec\.ts$"]`. 11 | */ 12 | export class Rule extends Lint.Rules.AbstractRule { 13 | apply(sourceFile: ts.SourceFile) { 14 | return this.applyWithWalker(new Walker(sourceFile, this.getOptions())); 15 | } 16 | } 17 | 18 | class Walker extends Lint.RuleWalker { 19 | 20 | /** Whether the walker should check the current source file. */ 21 | private _enabled: boolean; 22 | 23 | constructor(sourceFile: ts.SourceFile, options: Lint.IOptions) { 24 | super(sourceFile, options); 25 | 26 | // Globs that are used to determine which files to lint. 27 | const fileGlobs = options.ruleArguments || []; 28 | 29 | // Relative path for the current TypeScript source file. 30 | const relativeFilePath = path.relative(process.cwd(), sourceFile.fileName); 31 | 32 | // Whether the file should be checked at all. 33 | this._enabled = fileGlobs.some(p => minimatch(relativeFilePath, p)); 34 | } 35 | 36 | visitImportDeclaration(node: ts.ImportDeclaration) { 37 | // Walk through the imports and check if they start with `rxjs/add`. 38 | if (this._enabled && node.moduleSpecifier.getText().startsWith('rxjs/add', 1)) { 39 | this.addFailureAtNode(node, ERROR_MESSAGE); 40 | } 41 | 42 | super.visitImportDeclaration(node); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /integration/build.js: -------------------------------------------------------------------------------- 1 | const {fork} = require('child_process'); 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const glob = require('glob'); 6 | const { NodeJSFileSystem, setFileSystem } = require('@angular/compiler-cli/src/ngtsc/file_system'); 7 | const ngc = require('@angular/compiler-cli/src/main').main; 8 | const webpack = require('webpack'); 9 | 10 | setFileSystem(new NodeJSFileSystem()) 11 | 12 | const srcDir = path.join(__dirname, 'src/'); 13 | const e2eDir = path.join(__dirname, 'e2e/'); 14 | const distDir = path.join(__dirname, 'dist/'); 15 | const outDir = path.join(__dirname, 'out-tsc/e2e/'); 16 | 17 | const webpackCompiler = webpack(require(path.join(srcDir, 'webpack.config.js'))); 18 | 19 | function runWebpack() { 20 | return new Promise((resolve, reject) => { 21 | webpackCompiler.run((err, stats) => { 22 | if (err) { 23 | reject(err); 24 | } else { 25 | console.log(stats.toString()); 26 | resolve(stats); 27 | } 28 | }) 29 | }) 30 | } 31 | 32 | return Promise.resolve() 33 | // Compile using ngc. 34 | .then(() => ngc(['-p', e2eDir])) 35 | .then(() => ngc(['-p', srcDir])) 36 | // Create dist dir. 37 | .then(() => fs.mkdirSync(distDir)) 38 | // .then(() => ngc(['-p', path.join(srcDir, 'tsconfig.prerender.json')])) 39 | // .then(() => fork(path.join(srcDir, 'prerender.js'), [], {cwd: srcDir})) 40 | .then(() => ngc(['-p', path.join(srcDir, `tsconfig.postrender.json`)])) 41 | .then(() => runWebpack()) 42 | .then(() => ngc(['-p', path.join(srcDir, 'tsconfig.prerender.json')])) 43 | .then(() => fork(path.join(outDir, 'prerender.js'), [], {cwd: srcDir})) 44 | .catch(e => { 45 | console.error('\Build failed. See below for errors.\n'); 46 | console.error(e); 47 | process.exit(1); 48 | }); 49 | -------------------------------------------------------------------------------- /src/lib/common/get-node-key.spec.ts: -------------------------------------------------------------------------------- 1 | import {NodeContext} from './preboot.interfaces'; 2 | import {getNodeKeyForPreboot} from './get-node-key'; 3 | 4 | describe('UNIT TEST get-node-key', function() { 5 | describe('getNodeKeyForPreboot()', function() { 6 | it('should generate a default name', function() { 7 | const nodeContext = { 8 | root: { 9 | serverNode: {}, 10 | clientNode: {}, 11 | }, 12 | node: {} 13 | }; 14 | const expected = 'unknown'; 15 | const actual = getNodeKeyForPreboot(nodeContext); 16 | expect(actual).toEqual(expected); 17 | }); 18 | 19 | it('should generate a name for a deeply nested element', function() { 20 | 21 | const node = document.createElement('foo'); 22 | const serverNode = document.createElement('div'); 23 | const emptyNode = document.createElement('div'); 24 | const levelTwo = document.createElement('div'); 25 | const levelThree = document.createElement('div'); 26 | 27 | levelThree.appendChild(emptyNode.cloneNode()); 28 | levelThree.appendChild(emptyNode.cloneNode()); 29 | levelThree.appendChild(emptyNode.cloneNode()); 30 | levelThree.appendChild(node); 31 | 32 | levelTwo.appendChild(emptyNode.cloneNode()); 33 | levelTwo.appendChild(levelThree); 34 | 35 | serverNode.appendChild(emptyNode.cloneNode()); 36 | serverNode.appendChild(emptyNode.cloneNode()); 37 | serverNode.appendChild(levelTwo); 38 | 39 | const nodeContext = { 40 | root: { 41 | serverNode, 42 | clientNode: emptyNode 43 | }, 44 | node 45 | }; 46 | 47 | const expected = 'FOO_s3_s2_s4'; 48 | const actual = getNodeKeyForPreboot(nodeContext); 49 | expect(actual).toEqual(expected); 50 | }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /integration/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "integration-test", 3 | "version": "1.0.0", 4 | "description": "App for integration test", 5 | "scripts": { 6 | "prebuild": "rm -rf dist out-tsc && yarn install --frozen-lockfile", 7 | "build": "node build.js", 8 | "serve": "lite-server -c=bs-config.json", 9 | "pree2e": "yarn build", 10 | "e2e": "concurrently \"yarn serve\" \"yarn protractor\" --kill-others --success first", 11 | "postinstall": "yarn ngcc && webdriver-manager update --gecko false --standalone false $CHROMEDRIVER_VERSION_ARG", 12 | "protractor": "protractor protractor.conf.js", 13 | "ngcc": "ngcc --properties es2015 browser module main --first-only" 14 | }, 15 | "author": "Jeff Whelpley", 16 | "license": "MIT", 17 | "contributors": [ 18 | "Tobias Bosch ", 19 | "PatrickJS ", 20 | "Jeff Whelpley " 21 | ], 22 | "dependencies": { 23 | "@angular/animations": "^11.0.0", 24 | "@angular/common": "^11.0.0", 25 | "@angular/compiler": "^11.0.0", 26 | "@angular/core": "^11.0.0", 27 | "@angular/platform-browser": "^11.0.0", 28 | "@angular/platform-browser-dynamic": "^11.0.0", 29 | "@angular/platform-server": "^11.0.0", 30 | "core-js": "^2.6.2", 31 | "preboot": "file:../dist", 32 | "rxjs": "^6.6.3", 33 | "tslib": "^2.1.0", 34 | "zone.js": "~0.10.3" 35 | }, 36 | "devDependencies": { 37 | "@angular/compiler-cli": "^11.0.0", 38 | "@types/express": "^4.16.0", 39 | "@types/fs-extra": "^4.0.8", 40 | "@types/jasmine": "^3.6.2", 41 | "@types/uglify-js": "^2.6.32", 42 | "ajv": "^5.5.2", 43 | "concurrently": "^3.6.1", 44 | "express": "^4.16.4", 45 | "glob": "^7.1.3", 46 | "jasmine-spec-reporter": "^6.0.0", 47 | "lite-server": "^2.4.0", 48 | "protractor": "^5.4.2", 49 | "puppeteer": "^10.4.0", 50 | "typescript": "~4.0.3", 51 | "webpack": "^4.29.0" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tools/tslint-rules/requireLicenseBannerRule.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as ts from 'typescript'; 3 | import * as Lint from 'tslint'; 4 | import * as minimatch from 'minimatch'; 5 | 6 | const buildConfig = require('../../build-config'); 7 | 8 | /** License banner that is placed at the top of every public TypeScript file. */ 9 | const licenseBanner = buildConfig.licenseBanner; 10 | 11 | /** Failure message that will be shown if a license banner is missing. */ 12 | const ERROR_MESSAGE = 'Missing license header in this TypeScript file. ' + 13 | 'Every TypeScript file of the library needs to have the Google license banner at the top.'; 14 | 15 | /** TSLint fix that can be used to add the license banner easily. */ 16 | const tslintFix = Lint.Replacement.appendText(0, licenseBanner + '\n\n'); 17 | 18 | /** 19 | * Rule that walks through all TypeScript files of public packages and shows failures if a 20 | * file does not have the license banner at the top of the file. 21 | */ 22 | export class Rule extends Lint.Rules.AbstractRule { 23 | 24 | apply(sourceFile: ts.SourceFile) { 25 | return this.applyWithWalker(new RequireLicenseBannerWalker(sourceFile, this.getOptions())); 26 | } 27 | } 28 | 29 | class RequireLicenseBannerWalker extends Lint.RuleWalker { 30 | 31 | /** Whether the walker should check the current source file. */ 32 | private _enabled: boolean; 33 | 34 | constructor(sourceFile: ts.SourceFile, options: Lint.IOptions) { 35 | super(sourceFile, options); 36 | 37 | // Globs that are used to determine which files to lint. 38 | const fileGlobs = options.ruleArguments; 39 | 40 | // Relative path for the current TypeScript source file. 41 | const relativeFilePath = path.relative(process.cwd(), sourceFile.fileName); 42 | 43 | // Whether the file should be checked at all. 44 | this._enabled = fileGlobs.some(p => minimatch(relativeFilePath, p)); 45 | } 46 | 47 | visitSourceFile(sourceFile: ts.SourceFile) { 48 | if (!this._enabled) { 49 | return; 50 | } 51 | 52 | const fileContent = sourceFile.getFullText(); 53 | const licenseCommentPos = fileContent.indexOf(licenseBanner); 54 | 55 | if (licenseCommentPos !== 0) { 56 | return this.addFailureAt(0, 0, ERROR_MESSAGE, tslintFix); 57 | } 58 | 59 | super.visitSourceFile(sourceFile); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | process.env.CHROME_BIN = require('puppeteer').executablePath(); 2 | 3 | module.exports = function (config) { 4 | config.set({ 5 | basePath: __dirname, 6 | frameworks: ['jasmine'], 7 | 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-sourcemap-loader'), 13 | ], 14 | 15 | client: { 16 | jasmine: { 17 | // Always execute the tests in a random order to ensure that tests don't depend 18 | // accidentally on other tests. 19 | random: true 20 | } 21 | }, 22 | 23 | customLaunchers: { 24 | // From the CLI. Not used here but interesting 25 | // chrome setup for travis CI using chromium 26 | CustomChrome: { 27 | binary: process.env.CHROME_BIN, 28 | base: 'ChromeHeadless', 29 | } 30 | }, 31 | 32 | files: [ 33 | {pattern: 'node_modules/tslib/tslib.js', included: true, watched: false}, 34 | {pattern: 'node_modules/systemjs/dist/system.src.js', included: true, watched: false}, 35 | {pattern: 'node_modules/zone.js/dist/zone.js', included: true, watched: false}, 36 | {pattern: 'node_modules/zone.js/dist/proxy.js', included: true, watched: false}, 37 | {pattern: 'node_modules/zone.js/dist/sync-test.js', included: true, watched: false}, 38 | {pattern: 'node_modules/zone.js/dist/jasmine-patch.js', included: true, watched: false}, 39 | {pattern: 'node_modules/zone.js/dist/async-test.js', included: true, watched: false}, 40 | {pattern: 'node_modules/zone.js/dist/fake-async-test.js', included: true, watched: false}, 41 | 42 | {pattern: 'node_modules/@angular/**/*', included: false, watched: false}, 43 | {pattern: 'node_modules/rxjs/**/*', included: false, watched: false}, 44 | 45 | {pattern: 'karma-test-shim.js', included: true, watched: false}, 46 | {pattern: 'out-tsc/spec/**/*', included: false, watched: true}, 47 | ], 48 | 49 | exclude: [], 50 | preprocessors: { 51 | 'out-tsc/spec/**/*.js': ['sourcemap'] 52 | }, 53 | reporters: ['progress', 'kjhtml'], 54 | 55 | port: 9876, 56 | colors: true, 57 | logLevel: config.LOG_INFO, 58 | autoWatch: true, 59 | browsers: ['CustomChrome'], 60 | singleRun: false 61 | }); 62 | }; 63 | -------------------------------------------------------------------------------- /src/lib/api/inline.preboot.code.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | assign, 3 | getEventRecorderCode, 4 | getInlineDefinition, 5 | getInlineInvocation, 6 | stringifyWithFunctions, 7 | validateOptions 8 | } from './inline.preboot.code'; 9 | import {PrebootOptions} from '../common/preboot.interfaces'; 10 | 11 | describe('UNIT TEST inline.preboot.code', function() { 12 | describe('stringifyWithFunctions()', function() { 13 | it('should do the same thing as stringify if no functions', function 14 | () { 15 | const obj = { foo: 'choo', woo: 'loo', zoo: 5 }; 16 | const expected = JSON.stringify(obj); 17 | const actual = stringifyWithFunctions(obj); 18 | expect(actual).toEqual(expected); 19 | }); 20 | 21 | it('should stringify an object with functions', function() { 22 | const obj = { 23 | blah: 'foo', 24 | zoo: function(blah: number) { 25 | return blah + 1; 26 | } 27 | }; 28 | const expected = '{"blah":"foo","zoo":function ('; 29 | const actual = stringifyWithFunctions(obj); 30 | expect(actual.substring(0, 30)).toEqual(expected); 31 | }); 32 | }); 33 | 34 | describe('assign()', function() { 35 | it('should merge two objects', function () { 36 | const obj1 = { val1: 'foo', val2: 'choo' }; 37 | const obj2 = {val2: 'moo', val3: 'zoo'}; 38 | const expected = {val1: 'foo', val2: 'moo', val3: 'zoo'}; 39 | const actual = assign(obj1, obj2); 40 | expect(actual).toEqual(expected); 41 | }); 42 | }); 43 | 44 | describe('validateOptions()', function() { 45 | it('should throw error if nothing passed in', function() { 46 | expect(() => validateOptions({} as PrebootOptions)).toThrowError(/appRoot is missing/); 47 | }); 48 | }); 49 | 50 | describe('getEventRecorderCode()', function() { 51 | it('should generate valid JavaScript by default', function() { 52 | const code = getEventRecorderCode(); 53 | expect(code).toBeTruthy(); 54 | }); 55 | }); 56 | 57 | describe('getInlineDefinition()', function () { 58 | it('should generate valid JavaScript', function () { 59 | const code = getInlineDefinition({ appRoot: 'foo' }); 60 | expect(code).toBeTruthy(); 61 | }); 62 | }); 63 | 64 | describe('getInlineInvocation()', function () { 65 | it('should generate valid JavaScript', function () { 66 | const code = getInlineInvocation(); 67 | expect(code).toBeTruthy(); 68 | }); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /src/lib/api/event.recorder.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google LLC All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | import {getMockElement} from '../common/preboot.mocks'; 9 | import {createBuffer, getSelection} from './event.recorder'; 10 | import {PrebootSelection, ServerClientRoot} from '../common/preboot.interfaces'; 11 | 12 | describe('UNIT TEST event.recorder', function() { 13 | describe('createBuffer()', function() { 14 | it('should do nothing if serverNode empty', function () { 15 | const root = { 16 | serverNode: undefined 17 | }; 18 | 19 | const actual = createBuffer(root); 20 | expect(actual).toBe(root.serverNode as HTMLElement); 21 | }); 22 | 23 | it('should clone the node and insert before', function () { 24 | const root = { 25 | serverNode: getMockElement() 26 | }; 27 | const clientNode = { 28 | style: { display: 'block' } 29 | } as HTMLElement; 30 | 31 | if (root.serverNode) { 32 | root.serverNode.cloneNode = function () { 33 | return clientNode; 34 | }; 35 | } 36 | 37 | const actual = createBuffer(root); 38 | expect(actual).toBe(clientNode as HTMLElement); 39 | }); 40 | 41 | it('should add the "ng-non-bindable" attribute to serverNode', function () { 42 | const root = { 43 | serverNode: getMockElement() 44 | }; 45 | 46 | createBuffer(root); 47 | expect(root.serverNode!.hasAttribute('ng-non-bindable')).toBe(true); 48 | }); 49 | }); 50 | 51 | describe('getSelection()', function () { 52 | it('should return default if no value', function () { 53 | const node = {}; 54 | const expected: PrebootSelection = { 55 | start: 0, 56 | end: 0, 57 | direction: 'forward' 58 | }; 59 | 60 | const actual = getSelection(node as HTMLInputElement); 61 | expect(actual).toEqual(expected); 62 | }); 63 | 64 | it('should return selection for older browsers', function () { 65 | const node = { value: 'foo' }; 66 | const expected: PrebootSelection = { 67 | start: 3, 68 | end: 3, 69 | direction: 'forward' 70 | }; 71 | 72 | const actual = getSelection(node as HTMLInputElement); 73 | expect(actual).toEqual(expected); 74 | }); 75 | 76 | it('should return selection for modern browsers', function () { 77 | const node = { 78 | value: 'foo', 79 | selectionStart: 1, 80 | selectionEnd: 2, 81 | selectionDirection: 'backward' 82 | }; 83 | const expected: PrebootSelection = { 84 | start: 1, 85 | end: 2, 86 | direction: 'backward' 87 | }; 88 | 89 | const actual = getSelection(node as HTMLInputElement); 90 | expect(actual).toEqual(expected); 91 | }); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /src/lib/common/preboot.interfaces.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google LLC All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | // This is used to identify which events to listen for and what we do with them 9 | export interface EventSelector { 10 | selector: string; // same as jQuery; selector for nodes 11 | events: string[]; // array of event names to listen for 12 | keyCodes?: number[]; // key codes to watch out for 13 | preventDefault?: boolean; // will prevent default handlers if true 14 | freeze?: boolean; // if true, the UI will freeze when this event occurs 15 | action?: Function; // custom action to take with this event 16 | replay?: boolean; // if false, no replay will occur 17 | } 18 | 19 | export interface ServerClientRoot { 20 | serverNode?: HTMLElement; 21 | clientNode?: HTMLElement; 22 | overlay?: HTMLElement; 23 | } 24 | 25 | // interface for the options that can be passed into preboot 26 | export interface PrebootOptions { 27 | /** @deprecated minification has been removed in v6 */ 28 | buffer?: boolean; // if true, attempt to buffer client rendering to hidden div 29 | eventSelectors?: EventSelector[]; // when any of these events occur, they are recorded 30 | appRoot: string | string[]; // define selectors for one or more server roots 31 | replay?: boolean; 32 | disableOverlay?: boolean; 33 | } 34 | 35 | // our wrapper around DOM events in preboot 36 | export interface PrebootEvent { 37 | node: any; 38 | nodeKey?: any; 39 | event: DomEvent; 40 | name: string; 41 | } 42 | 43 | // an actual DOM event object 44 | export interface DomEvent { 45 | which?: number; 46 | type?: string; 47 | target?: any; 48 | preventDefault(): void; 49 | } 50 | 51 | // data on global preboot object for one particular app 52 | export interface PrebootAppData { 53 | root: ServerClientRoot; 54 | events: PrebootEvent[]; 55 | } 56 | 57 | // object that is used to keep track of all the preboot 58 | // listeners (so we can remove the listeners later) 59 | export interface PrebootEventListener { 60 | node: Node; 61 | eventName: string; 62 | handler: EventListener; 63 | } 64 | 65 | export type PrebootSelectionDirection = 'forward' | 'backward' | 'none'; 66 | 67 | export interface PrebootSelection { 68 | start: number; 69 | end: number; 70 | direction: PrebootSelectionDirection; 71 | } 72 | 73 | // object that contains all data about the currently active node in the DOM (i.e. that has focus) 74 | export interface NodeContext { 75 | root: ServerClientRoot; 76 | node: Element; 77 | nodeKey?: string; 78 | selection?: PrebootSelection; 79 | } 80 | 81 | // interface for global object that contains all preboot data 82 | export interface PrebootData { 83 | opts?: PrebootOptions; 84 | activeNode?: NodeContext; 85 | apps?: PrebootAppData[]; 86 | listeners?: PrebootEventListener[]; 87 | } 88 | 89 | export interface PrebootWindow { 90 | prebootData: PrebootData; 91 | getComputedStyle: (elt: Element, pseudoElt?: string) => CSSStyleDeclaration; 92 | document: Document; 93 | } 94 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "preboot", 3 | "version": "8.0.0", 4 | "description": "Record server view events and play back to Angular client view", 5 | "scripts": { 6 | "clean": "rimraf out-tsc dist/* dist-tarball/*", 7 | "build": "ng-packagr --project src/lib/ng-package.json --config src/lib/tsconfig.lib.json", 8 | "postbuild": "cp README.md dist/.", 9 | "e2e": "cd integration && yarn e2e", 10 | "build-test": "ngc -p src/lib/tsconfig.spec.json", 11 | "build-test:watch": "ngc -p src/lib/tsconfig.spec.json -w", 12 | "pretest": "yarn build-test", 13 | "test": "concurrently \"yarn build-test:watch\" \"karma start karma.conf.js\"", 14 | "pretest:once": "yarn build-test", 15 | "test:once": "karma start karma.conf.js --single-run", 16 | "preintegration": "yarn build && cd integration && yarn clean && npm install", 17 | "integration": "yarn integration:aot && yarn integration:jit", 18 | "integration:jit": "cd integration && yarn e2e", 19 | "integration:aot": "cd integration && yarn e2e:aot", 20 | "postinstall": "webdriver-manager update --gecko false --standalone false $CHROMEDRIVER_VERSION_ARG", 21 | "lint": "tslint -c tslint.json --project ./tsconfig.json -t verbose" 22 | }, 23 | "author": "Jeff Whelpley", 24 | "license": "MIT", 25 | "contributors": [ 26 | "Tobias Bosch ", 27 | "PatrickJS ", 28 | "Jeff Whelpley " 29 | ], 30 | "devDependencies": { 31 | "@angular/animations": "^11.0.0", 32 | "@angular/common": "^11.0.0", 33 | "@angular/compiler": "^11.0.0", 34 | "@angular/compiler-cli": "^11.0.0", 35 | "@angular/core": "^11.0.0", 36 | "@angular/platform-browser": "^11.0.0", 37 | "@angular/platform-browser-dynamic": "^11.0.0", 38 | "@angular/platform-server": "^11.0.0", 39 | "@types/express": "^4.16.0", 40 | "@types/jasmine": "^2.8.15", 41 | "@types/minimatch": "^3.0.3", 42 | "@types/node": "^8.10.39", 43 | "@types/uglify-js": "^2.6.32", 44 | "ajv": "^5.5.2", 45 | "camelcase": "^4.1.0", 46 | "concurrently": "^3.6.1", 47 | "express": "^4.16.4", 48 | "glob": "^7.1.3", 49 | "jasmine": "^3.6.3", 50 | "jasmine-core": "^3.6.0", 51 | "jasmine-spec-reporter": "^6.0.0", 52 | "karma": "^6.0.0", 53 | "karma-chrome-launcher": "^3.1.0", 54 | "karma-jasmine": "^4.0.1", 55 | "karma-jasmine-html-reporter": "^1.5.4", 56 | "karma-sourcemap-loader": "^0.3.8", 57 | "minimatch": "^3.0.4", 58 | "ng-packagr": "^11.0.3", 59 | "protractor": "^5.4.2", 60 | "puppeteer": "^10.4.0", 61 | "reflect-metadata": "^0.1.13", 62 | "rimraf": "^2.6.3", 63 | "rxjs": "^6.3.3", 64 | "systemjs": "0.19.43", 65 | "ts-loader": "^3.5.0", 66 | "ts-node": "^9.1.1", 67 | "tslib": "^2.1.0", 68 | "tslint": "^5.12.1", 69 | "typescript": "~4.0.5", 70 | "uglify-js": "^3.4.9", 71 | "webdriver-manager": "^12.1.1", 72 | "zone.js": "~0.10.3" 73 | }, 74 | "repository": { 75 | "type": "git", 76 | "url": "https://github.com/angular/preboot" 77 | }, 78 | "bugs": { 79 | "url": "https://github.com/angular/preboot/issues" 80 | }, 81 | "homepage": "https://github.com/angular/preboot" 82 | } 83 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "./tools/tslint-rules/" 4 | ], 5 | "rules": { 6 | "max-line-length": [true, 100], 7 | // Disable this flag because of SHA tslint#48b0c597f9257712c7d1f04b55ed0aa60e333f6a 8 | // TSLint now shows warnings if types for properties are inferred. This rule needs to be 9 | // disabled because all properties need to have explicit types set to work for Dgeni. 10 | "no-inferrable-types": false, 11 | "class-name": true, 12 | "comment-format": [ 13 | true, 14 | "check-space" 15 | ], 16 | "indent": [ 17 | true, 18 | "spaces" 19 | ], 20 | "eofline": true, 21 | "no-duplicate-variable": true, 22 | "no-eval": true, 23 | "no-arg": true, 24 | "no-internal-module": true, 25 | "no-trailing-whitespace": true, 26 | "no-bitwise": true, 27 | "no-shadowed-variable": true, 28 | "no-unused-expression": true, 29 | "no-var-keyword": true, 30 | "member-access": [true, "no-public"], 31 | "no-debugger": true, 32 | "no-unused-variable": true, 33 | "one-line": [ 34 | true, 35 | "check-catch", 36 | "check-else", 37 | "check-open-brace", 38 | "check-whitespace" 39 | ], 40 | "quotemark": [ 41 | true, 42 | "single", 43 | "avoid-escape" 44 | ], 45 | "semicolon": true, 46 | "typedef-whitespace": [ 47 | true, 48 | { 49 | "call-signature": "nospace", 50 | "index-signature": "nospace", 51 | "parameter": "nospace", 52 | "property-declaration": "nospace", 53 | "variable-declaration": "nospace" 54 | } 55 | ], 56 | "curly": true, 57 | "variable-name": [ 58 | true, 59 | "ban-keywords", 60 | "check-format", 61 | "allow-leading-underscore" 62 | ], 63 | "whitespace": [ 64 | true, 65 | "check-branch", 66 | "check-decl", 67 | "check-operator", 68 | "check-separator", 69 | "check-type", 70 | "check-preblock" 71 | ], 72 | // Bans jasmine helper functions that will prevent the CI from properly running tests. 73 | "ban": [ 74 | true, 75 | ["fit"], 76 | ["fdescribe"], 77 | ["xit"], 78 | ["xdescribe"], 79 | {"name": "Object.assign", "message": "Use the spread operator instead."} 80 | ], 81 | // Avoids inconsistent linebreak styles in source files. Forces developers to use LF linebreaks. 82 | "linebreak-style": [true, "LF"], 83 | // Namespaces are no allowed, because of Closure compiler. 84 | "no-namespace": true, 85 | 86 | // Custom Rules 87 | "ts-loader": true, 88 | "no-exposed-todo": true, 89 | "no-host-decorator-in-concrete": [ 90 | true, 91 | "HostBinding", 92 | "HostListener" 93 | ], 94 | "validate-decorators": [true, { 95 | "Component": { 96 | "encapsulation": "\\.None$", 97 | "moduleId": "^module\\.id$", 98 | "preserveWhitespaces": "false$", 99 | "changeDetection": "\\.OnPush$", 100 | "!styles": ".*", 101 | "!host": "\\[class\\]" 102 | }, 103 | "Directive": { 104 | "!host": "\\[class\\]" 105 | } 106 | }, "src/lib/**/!(*.spec).ts"], 107 | "require-license-banner": [ 108 | true, 109 | "src/lib/**/!(*.spec).ts" 110 | ], 111 | "no-rxjs-patch-imports": [ 112 | true, 113 | "src/lib/**/*.ts" 114 | ] 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/lib/provider.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google LLC All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | import { 9 | APP_BOOTSTRAP_LISTENER, 10 | ApplicationRef, 11 | Inject, 12 | InjectionToken, 13 | Optional, 14 | PLATFORM_ID 15 | } from '@angular/core'; 16 | import {DOCUMENT, isPlatformBrowser, isPlatformServer} from '@angular/common'; 17 | import {filter, take} from 'rxjs/operators'; 18 | 19 | import {EventReplayer} from './api/event.replayer'; 20 | import {PREBOOT_NONCE} from './common/tokens'; 21 | import {getInlineDefinition, getInlineInvocation} from './api/inline.preboot.code'; 22 | import {PrebootOptions} from './common/preboot.interfaces'; 23 | import {validateOptions} from './api'; 24 | 25 | const PREBOOT_SCRIPT_CLASS = 'preboot-inline-script'; 26 | export const PREBOOT_OPTIONS = new InjectionToken('PrebootOptions'); 27 | 28 | function createScriptFromCode(doc: Document, nonce: string|null, inlineCode: string) { 29 | const script = doc.createElement('script'); 30 | if (nonce) { 31 | (script as any).nonce = nonce; 32 | } 33 | script.className = PREBOOT_SCRIPT_CLASS; 34 | script.textContent = inlineCode; 35 | 36 | return script; 37 | } 38 | 39 | export function PREBOOT_FACTORY(doc: Document, 40 | prebootOpts: PrebootOptions, 41 | nonce: string|null, 42 | platformId: Object, 43 | appRef: ApplicationRef, 44 | eventReplayer: EventReplayer) { 45 | return () => { 46 | validateOptions(prebootOpts); 47 | 48 | if (isPlatformServer(platformId)) { 49 | const inlineCodeDefinition = getInlineDefinition(prebootOpts); 50 | const scriptWithDefinition = createScriptFromCode(doc, nonce, inlineCodeDefinition); 51 | const inlineCodeInvocation = getInlineInvocation(); 52 | 53 | const existingScripts = doc.getElementsByClassName(PREBOOT_SCRIPT_CLASS); 54 | 55 | // Check to see if preboot scripts are already inlined before adding them 56 | // to the DOM. If they are, update the nonce to be current. 57 | if (existingScripts.length === 0) { 58 | const baseList: string[] = []; 59 | const appRootSelectors = baseList.concat(prebootOpts.appRoot); 60 | doc.head.appendChild(scriptWithDefinition); 61 | appRootSelectors 62 | .map(selector => ({ 63 | selector, 64 | appRootElem: doc.querySelector(selector) 65 | })) 66 | .forEach(({selector, appRootElem}) => { 67 | if (!appRootElem) { 68 | console.log(`No server node found for selector: ${selector}`); 69 | return; 70 | } 71 | const scriptWithInvocation = createScriptFromCode(doc, nonce, inlineCodeInvocation); 72 | appRootElem.insertBefore(scriptWithInvocation, appRootElem.firstChild); 73 | }); 74 | } else if (existingScripts.length > 0 && nonce) { 75 | (existingScripts[0] as any).nonce = nonce; 76 | } 77 | } 78 | if (isPlatformBrowser(platformId)) { 79 | const replay = prebootOpts.replay != null ? prebootOpts.replay : true; 80 | if (replay) { 81 | appRef.isStable 82 | .pipe( 83 | filter(stable => stable), 84 | take(1) 85 | ).subscribe(() => { 86 | eventReplayer.replayAll(); 87 | }); 88 | } 89 | } 90 | }; 91 | } 92 | 93 | export const PREBOOT_PROVIDER = { 94 | provide: void>>APP_BOOTSTRAP_LISTENER, 95 | useFactory: PREBOOT_FACTORY, 96 | deps: [ 97 | DOCUMENT, 98 | PREBOOT_OPTIONS, 99 | [new Optional(), new Inject(PREBOOT_NONCE)], 100 | PLATFORM_ID, 101 | ApplicationRef, 102 | EventReplayer, 103 | ], 104 | multi: true 105 | }; 106 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Cache key for CircleCI. We want to invalidate the cache whenever the npm shrinkwrap 2 | # changed. 3 | var_1: &cache_key preboot-v2-{{ checksum "yarn.lock" }} 4 | # Use the CircleCI browsers image that comes with NodeJS installed 5 | var_2: &docker_image cimg/node:14.17 6 | 7 | # Executor Definitions 8 | # https://circleci.com/docs/2.0/reusing-config/#authoring-reusable-executors 9 | executors: 10 | action-executor: 11 | docker: 12 | - image: *docker_image 13 | working_directory: ~/ng 14 | 15 | # Workspace initially persisted by the `setup` job. 16 | # https://circleci.com/docs/2.0/workflows/#using-workspaces-to-share-data-among-jobs 17 | # https://circleci.com/blog/deep-diving-into-circleci-workspaces/ 18 | var_3: &workspace_location . 19 | 20 | # Job step for checking out the source code from GitHub. This also ensures that the source code 21 | # is rebased on top of master. 22 | var_4: &checkout_code 23 | checkout: 24 | # After checkout, rebase on top of master. By default, PRs are not rebased on top of master, 25 | # which we want. See https://discuss.circleci.com/t/1662 26 | post: git pull --ff-only origin "refs/pull/${CI_PULL_REQUEST//*pull\//}/merge" 27 | 28 | # Restores the cache that could be available for the current Yarn lock file. The cache usually 29 | # includes the node modules. 30 | var_5: &restore_cache 31 | restore_cache: 32 | key: *cache_key 33 | 34 | # Saves the cache for the current Yarn lock file. We store the node modules 35 | # in order to make subsequent builds faster. 36 | var_6: &save_cache 37 | save_cache: 38 | key: *cache_key 39 | paths: 40 | - ~/.cache/yarn 41 | 42 | # Job step that ensures that the node module dependencies are installed and up-to-date. We use 43 | # Yarn with the frozen lockfile option in order to make sure that lock file and package.json are 44 | # in sync. Unlike in Travis, we don't need to manually purge the node modules if stale because 45 | # CircleCI automatically discards the cache if the checksum of the lock file has changed. 46 | var_7: &yarn_install 47 | run: yarn install --frozen-lockfile --non-interactive 48 | 49 | # Attaches the release output which has been stored in the workspace to the current job. 50 | # https://circleci.com/docs/2.0/workflows/#using-workspaces-to-share-data-among-jobs 51 | var_8: &attach_release_output 52 | attach_workspace: 53 | at: *workspace_location 54 | 55 | 56 | 57 | # ----------------------------- 58 | # Container version of CircleCI 59 | # ----------------------------- 60 | version: 2.1 61 | 62 | orbs: 63 | browser-tools: circleci/browser-tools@1.0.1 64 | 65 | # ----------------------------------------------------------------------------------------- 66 | # Job definitions. Jobs which are defined just here, will not run automatically. Each job 67 | # must be part of a workflow definition in order to run for PRs and push builds. 68 | # ----------------------------------------------------------------------------------------- 69 | jobs: 70 | 71 | build_and_test: 72 | executor: action-executor 73 | steps: 74 | - *checkout_code 75 | - *restore_cache 76 | - *yarn_install 77 | 78 | - run: yarn build 79 | 80 | - browser-tools/install-chrome 81 | - run: yarn test:once 82 | 83 | # Store the release output in the workspace storage. This means that other jobs 84 | # in the same workflow can attach the release output to their job. 85 | - persist_to_workspace: 86 | root: *workspace_location 87 | paths: 88 | - ./dist 89 | 90 | - *save_cache 91 | 92 | e2e: 93 | executor: action-executor 94 | steps: 95 | - browser-tools/install-chrome 96 | - *checkout_code 97 | - *restore_cache 98 | - *attach_release_output 99 | 100 | - run: 101 | name: 'Setup custom environment variables' 102 | # Use matching versions of Chromium (via puppeteer) and ChromeDriver. 103 | # https://github.com/GoogleChrome/puppeteer/releases 104 | # http://chromedriver.chromium.org/downloads 105 | command: | 106 | echo 'export CHROMEDRIVER_VERSION_ARG="--versions.chrome 93.0.4577.0"' >> $BASH_ENV # Redirect into $BASH_ENV 107 | - run: yarn e2e 108 | 109 | - *save_cache 110 | 111 | # ---------------------------------------------------------------------------------------- 112 | # Workflow definitions. A workflow usually groups multiple jobs together. This is useful if 113 | # one job depends on another. 114 | # ---------------------------------------------------------------------------------------- 115 | workflows: 116 | version: 2 117 | 118 | build_and_test: 119 | jobs: 120 | - build_and_test 121 | - e2e: 122 | requires: 123 | - build_and_test 124 | -------------------------------------------------------------------------------- /tools/tslint-rules/validateDecoratorsRule.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as ts from 'typescript'; 3 | import * as Lint from 'tslint'; 4 | import * as minimatch from 'minimatch'; 5 | 6 | /** 7 | * Rule that enforces certain decorator properties to be defined and to match a pattern. 8 | * Properties can be forbidden by prefixing their name with a `!`. 9 | * Supports whitelisting files via the third argument. E.g. 10 | * 11 | * ``` 12 | * "validate-decorators": [true, { 13 | * "Component": { 14 | * "encapsulation": "\\.None$", 15 | * "!styles": ".*" 16 | * } 17 | * }, "src/lib"] 18 | * ``` 19 | */ 20 | export class Rule extends Lint.Rules.AbstractRule { 21 | apply(sourceFile: ts.SourceFile) { 22 | return this.applyWithWalker(new Walker(sourceFile, this.getOptions())); 23 | } 24 | } 25 | 26 | /** Represents a set of required and forbidden decorator properties. */ 27 | type DecoratorRuleSet = { 28 | required: {[key: string]: RegExp}, 29 | forbidden: {[key: string]: RegExp}, 30 | }; 31 | 32 | /** Represents a map between decorator names and rule sets. */ 33 | type DecoratorRules = { 34 | [key: string]: DecoratorRuleSet 35 | }; 36 | 37 | class Walker extends Lint.RuleWalker { 38 | // Whether the file should be checked at all. 39 | private _enabled: boolean; 40 | 41 | // Rules that will be used to validate the decorators. 42 | private _rules: DecoratorRules; 43 | 44 | constructor(sourceFile: ts.SourceFile, options: Lint.IOptions) { 45 | super(sourceFile, options); 46 | 47 | // Globs that are used to determine which files to lint. 48 | const fileGlobs = options.ruleArguments.slice(1) || []; 49 | 50 | // Relative path for the current TypeScript source file. 51 | const relativeFilePath = path.relative(process.cwd(), sourceFile.fileName); 52 | 53 | this._rules = this._generateRules(options.ruleArguments[0]); 54 | this._enabled = Object.keys(this._rules).length > 0 && 55 | fileGlobs.some(p => minimatch(relativeFilePath, p)); 56 | } 57 | 58 | visitClassDeclaration(node: ts.ClassDeclaration) { 59 | if (this._enabled && node.decorators) { 60 | node.decorators 61 | .map(decorator => decorator.expression as any) 62 | .filter(expression => expression.arguments.length && expression.arguments[0].properties) 63 | .forEach(expression => this._validatedDecorator(expression)); 64 | } 65 | 66 | super.visitClassDeclaration(node); 67 | } 68 | 69 | /** 70 | * Validates that a decorator matches all of the defined rules. 71 | * @param decorator Decorator to be checked. 72 | */ 73 | private _validatedDecorator(decorator: any) { 74 | // Get the rules that are relevant for the current decorator. 75 | const rules = this._rules[decorator.expression.getText()]; 76 | 77 | // Don't do anything if there are no rules. 78 | if (!rules) { 79 | return; 80 | } 81 | 82 | // Extract the property names and values. 83 | const props = decorator.arguments[0].properties.map((node: ts.PropertyAssignment) => ({ 84 | name: node.name.getText(), 85 | value: node.initializer.getText(), 86 | node 87 | })); 88 | 89 | // Find all of the required rule properties that are missing from the decorator. 90 | const missing = Object.keys(rules.required) 91 | .filter(key => !props.find((prop: any) => prop.name === key)); 92 | 93 | if (missing.length) { 94 | // Exit early if any of the properties are missing. 95 | this.addFailureAtNode(decorator.parent, 'Missing required properties: ' + missing.join(', ')); 96 | } else { 97 | // If all the necessary properties are defined, ensure that 98 | // they match the pattern and aren't in the forbidden list. 99 | props 100 | .filter((prop: any) => rules.required[prop.name] || rules.forbidden[prop.name]) 101 | .forEach((prop: any) => { 102 | const {name, value, node} = prop; 103 | const requiredPattern = rules.required[name]; 104 | const forbiddenPattern = rules.forbidden[name]; 105 | 106 | if (requiredPattern && !requiredPattern.test(value)) { 107 | this.addFailureAtNode(node, `Invalid value for property. ` + 108 | `Expected value to match "${requiredPattern}".`); 109 | } else if (forbiddenPattern && forbiddenPattern.test(value)) { 110 | this.addFailureAtNode(node, `Property value not allowed. ` + 111 | `Value should not match "${forbiddenPattern}".`); 112 | } 113 | }); 114 | } 115 | } 116 | 117 | /** 118 | * Cleans out the blank rules that are passed through the tslint.json 119 | * and converts the string patterns into regular expressions. 120 | * @param config Config object passed in via the tslint.json. 121 | * @returns Sanitized rules. 122 | */ 123 | private _generateRules(config: {[key: string]: {[key: string]: string}}): DecoratorRules { 124 | const output: DecoratorRules = {}; 125 | 126 | if (config) { 127 | Object.keys(config) 128 | .filter(decoratorName => Object.keys(config[decoratorName]).length > 0) 129 | .forEach(decoratorName => { 130 | output[decoratorName] = Object.keys(config[decoratorName]).reduce((accumulator, prop) => { 131 | const isForbidden = prop.startsWith('!'); 132 | const cleanName = isForbidden ? prop.slice(1) : prop; 133 | const pattern = new RegExp(config[decoratorName][prop]); 134 | 135 | if (isForbidden) { 136 | accumulator.forbidden[cleanName] = pattern; 137 | } else { 138 | accumulator.required[cleanName] = pattern; 139 | } 140 | 141 | return accumulator; 142 | }, {required: {}, forbidden: {}} as DecoratorRuleSet); 143 | }); 144 | } 145 | 146 | return output; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /karma-test-shim.js: -------------------------------------------------------------------------------- 1 | /* global jasmine, __karma__, window*/ 2 | Error.stackTraceLimit = Infinity; 3 | 4 | // The default time that jasmine waits for an asynchronous test to finish is five seconds. 5 | // If this timeout is too short the CI may fail randomly because our asynchronous tests can 6 | // take longer in some situations (e.g Saucelabs and Browserstack tunnels) 7 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; 8 | 9 | __karma__.loaded = function () {}; 10 | 11 | var baseDir = '/base'; 12 | var specFiles = Object.keys(window.__karma__.files).filter(isSpecFile); 13 | 14 | // Configure the base path and map the different node packages. 15 | System.config({ 16 | baseURL: baseDir, 17 | paths: { 18 | 'node:*': 'node_modules/*' 19 | }, 20 | map: { 21 | 'rxjs': 'node:rxjs', 22 | 'main': 'main.js', 23 | 'tslib': 'node:tslib/tslib.js', 24 | 25 | // Angular specific mappings. 26 | '@angular/core': 'node:@angular/core/bundles/core.umd.js', 27 | '@angular/core/testing': 'node:@angular/core/bundles/core-testing.umd.js', 28 | '@angular/common': 'node:@angular/common/bundles/common.umd.js', 29 | '@angular/common/http': 'node:@angular/common/bundles/common-http.umd.js', 30 | '@angular/common/http/testing': 'node:@angular/common/bundles/common-http-testing.umd.js', 31 | '@angular/common/testing': 'node:@angular/common/bundles/common-testing.umd.js', 32 | '@angular/compiler': 'node:@angular/compiler/bundles/compiler.umd.js', 33 | '@angular/compiler/testing': 'node:@angular/compiler/bundles/compiler-testing.umd.js', 34 | '@angular/http': 'node:@angular/http/bundles/http.umd.js', 35 | '@angular/http/testing': 'node:@angular/http/bundles/http-testing.umd.js', 36 | '@angular/forms': 'node:@angular/forms/bundles/forms.umd.js', 37 | '@angular/forms/testing': 'node:@angular/forms/bundles/forms-testing.umd.js', 38 | '@angular/animations': 'node:@angular/animations/bundles/animations.umd.js', 39 | '@angular/animations/browser': 'node:@angular/animations/bundles/animations-browser.umd.js', 40 | '@angular/platform-browser/animations': 41 | 'node:@angular/platform-browser/bundles/platform-browser-animations.umd', 42 | '@angular/platform-browser': 43 | 'node:@angular/platform-browser/bundles/platform-browser.umd.js', 44 | '@angular/platform-browser/testing': 45 | 'node:@angular/platform-browser/bundles/platform-browser-testing.umd.js', 46 | '@angular/platform-browser-dynamic': 47 | 'node:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js', 48 | '@angular/platform-browser-dynamic/testing': 49 | 'node:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic-testing.umd.js', 50 | '@angular/platform-server': 51 | 'node:@angular/platform-server/bundles/platform-server.umd.js', 52 | '@angular/platform-server/testing': 53 | 'node:@angular/platform-server/bundles/platform-server-testing.umd.js', 54 | 55 | // Path mappings for local packages that can be imported inside of tests. 56 | 'preboot': 'out-tsc/spec/public-api.js' 57 | }, 58 | packages: { 59 | // Thirdparty barrels. 60 | 'rxjs': {main: 'index'}, 61 | 'rxjs/operators': {main: 'index'}, 62 | 63 | // Set the default extension for the root package, because otherwise the demo-app can't 64 | // be built within the production mode. Due to missing file extensions. 65 | '.': { 66 | defaultExtension: 'js' 67 | } 68 | } 69 | }); 70 | 71 | // Configure the Angular test bed and run all specs once configured. 72 | configureTestBed() 73 | .then(runSpecs) 74 | .then(__karma__.start, function(error) { 75 | // Passing in the error object directly to Karma won't log out the stack trace and 76 | // passing the `originalErr` doesn't work correctly either. We have to log out the 77 | // stack trace so we can actually debug errors before the tests have started. 78 | console.error(error.originalErr.stack); 79 | __karma__.error(error); 80 | }); 81 | 82 | 83 | /** Runs the specs in Karma. */ 84 | function runSpecs() { 85 | // By importing all spec files, Karma will run the tests directly. 86 | return Promise.all(specFiles.map(function(fileName) { 87 | return System.import(fileName); 88 | })); 89 | } 90 | 91 | /** Whether the specified file is part of Angular Flex-Layout. */ 92 | function isSpecFile(path) { 93 | return path.slice(-8) === '.spec.js' && path.indexOf('node_modules') === -1; 94 | } 95 | 96 | /** Configures Angular's TestBed. */ 97 | function configureTestBed() { 98 | return Promise.all([ 99 | System.import('@angular/core/testing'), 100 | System.import('@angular/platform-browser-dynamic/testing') 101 | ]).then(function (providers) { 102 | var testing = providers[0]; 103 | var testingBrowser = providers[1]; 104 | 105 | var testBed = testing.TestBed.initTestEnvironment( 106 | testingBrowser.BrowserDynamicTestingModule, 107 | testingBrowser.platformBrowserDynamicTesting() 108 | ); 109 | 110 | patchTestBedToDestroyFixturesAfterEveryTest(testBed); 111 | }); 112 | } 113 | 114 | /** 115 | * Monkey-patches TestBed.resetTestingModule such that any errors that occur during component 116 | * destruction are thrown instead of silently logged. Also runs TestBed.resetTestingModule after 117 | * each unit test. 118 | * 119 | * Without this patch, the combination of two behaviors is problematic for Angular Flex-Layout: 120 | * - TestBed.resetTestingModule catches errors thrown on fixture destruction and logs them without 121 | * the errors ever being thrown. This means that any component errors that occur in ngOnDestroy 122 | * can encounter errors silently and still pass unit tests. 123 | * - TestBed.resetTestingModule is only called *before* a test is run, meaning that even *if* the 124 | * aforementioned errors were thrown, they would be reported for the wrong test (the test that's 125 | * about to start, not the test that just finished). 126 | */ 127 | function patchTestBedToDestroyFixturesAfterEveryTest(testBed) { 128 | // Original resetTestingModule function of the TestBed. 129 | var _resetTestingModule = testBed.resetTestingModule; 130 | 131 | // Monkey-patch the resetTestingModule to destroy fixtures outside of a try/catch block. 132 | // With https://github.com/angular/angular/commit/2c5a67134198a090a24f6671dcdb7b102fea6eba 133 | // errors when destroying components are no longer causing Jasmine to fail. 134 | testBed.resetTestingModule = function() { 135 | try { 136 | this._activeFixtures.forEach(function (fixture) { fixture.destroy(); }); 137 | } finally { 138 | this._activeFixtures = []; 139 | // Regardless of errors or not, run the original reset testing module function. 140 | _resetTestingModule.call(this); 141 | } 142 | }; 143 | 144 | // Angular's testing package resets the testing module before each test. This doesn't work well 145 | // for us because it doesn't allow developers to see what test actually failed. 146 | // Fixing this by resetting the testing module after each test. 147 | // https://github.com/angular/angular/blob/master/packages/core/testing/src/before_each.ts#L25 148 | afterEach(function() { 149 | testBed.resetTestingModule(); 150 | }); 151 | } 152 | -------------------------------------------------------------------------------- /src/lib/api/inline.preboot.code.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google LLC All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | import {PrebootOptions} from '../common/preboot.interfaces'; 9 | import {getNodeKeyForPreboot} from '../common/get-node-key'; 10 | 11 | import { 12 | initAll, 13 | start, 14 | createOverlay, 15 | getAppRoot, 16 | handleEvents, 17 | createListenHandler, 18 | getSelection, 19 | createBuffer 20 | } from './event.recorder'; 21 | 22 | const eventRecorder = { 23 | start, 24 | createOverlay, 25 | getAppRoot, 26 | handleEvents, 27 | createListenHandler, 28 | getSelection, 29 | createBuffer 30 | }; 31 | 32 | export const initFunctionName = 'prebootInitFn'; 33 | 34 | // exporting default options in case developer wants to use these + custom on 35 | // top 36 | export const defaultOptions = { 37 | buffer: true, 38 | replay: true, 39 | disableOverlay: false, 40 | 41 | // these are the default events are are listening for an transferring from 42 | // server view to client view 43 | eventSelectors: [ 44 | // for recording changes in form elements 45 | { 46 | selector: 'input,textarea', 47 | events: ['keypress', 'keyup', 'keydown', 'input', 'change'] 48 | }, 49 | { selector: 'select,option', events: ['change'] }, 50 | 51 | // when user hits return button in an input box 52 | { 53 | selector: 'input', 54 | events: ['keyup'], 55 | preventDefault: true, 56 | keyCodes: [13], 57 | freeze: true 58 | }, 59 | 60 | // when user submit form (press enter, click on button/input[type="submit"]) 61 | { 62 | selector: 'form', 63 | events: ['submit'], 64 | preventDefault: true, 65 | freeze: true 66 | }, 67 | 68 | // for tracking focus (no need to replay) 69 | { 70 | selector: 'input,textarea', 71 | events: ['focusin', 'focusout', 'mousedown', 'mouseup'], 72 | replay: false 73 | }, 74 | 75 | // user clicks on a button 76 | { 77 | selector: 'button', 78 | events: ['click'], 79 | preventDefault: true, 80 | freeze: true 81 | } 82 | ] 83 | }; 84 | 85 | /** 86 | * Get the event recorder code based on all functions in event.recorder.ts 87 | * and the getNodeKeyForPreboot function. 88 | */ 89 | export function getEventRecorderCode(): string { 90 | const eventRecorderFunctions: string[] = []; 91 | 92 | for (const funcName in eventRecorder) { 93 | if (eventRecorder.hasOwnProperty(funcName)) { 94 | const fn = (eventRecorder)[funcName].toString(); 95 | const fnCleaned = fn.replace('common_1.', ''); 96 | eventRecorderFunctions.push(fnCleaned); 97 | } 98 | } 99 | 100 | // this is common function used to get the node key 101 | eventRecorderFunctions.push(getNodeKeyForPreboot.toString()); 102 | 103 | // add new line characters for readability 104 | return '\n\n' + eventRecorderFunctions.join('\n\n') + '\n\n'; 105 | } 106 | 107 | /** 108 | * Used by the server side version of preboot. The main purpose is to get the 109 | * inline code that can be inserted into the server view. 110 | * Returns the definitions of the prebootInit function called in code returned by 111 | * getInlineInvocation for each server node separately. 112 | * 113 | * @param customOptions PrebootRecordOptions that override the defaults 114 | * @returns Generated inline preboot code with just functions definitions 115 | * to be used separately 116 | */ 117 | export function getInlineDefinition(customOptions?: PrebootOptions): string { 118 | const opts = assign({}, defaultOptions, customOptions); 119 | 120 | // safety check to make sure options passed in are valid 121 | validateOptions(opts); 122 | 123 | const scriptCode = getEventRecorderCode(); 124 | const optsStr = stringifyWithFunctions(opts); 125 | 126 | // wrap inline preboot code with a self executing function in order to create scope 127 | const initAllStr = initAll.toString(); 128 | return `var ${initFunctionName} = (function() { 129 | ${scriptCode} 130 | return (${initAllStr.replace('common_1.', '')})(${optsStr}); 131 | })();`; 132 | } 133 | 134 | 135 | /** 136 | * Used by the server side version of preboot. The main purpose is to get the 137 | * inline code that can be inserted into the server view. 138 | * Invokes the prebootInit function defined in getInlineDefinition with proper 139 | * parameters. Each appRoot should get a separate inlined code from a separate 140 | * call to getInlineInvocation but only one inlined code from getInlineDefinition. 141 | * 142 | * @returns Generated inline preboot code with just invocations of functions from 143 | * getInlineDefinition 144 | */ 145 | export function getInlineInvocation(): string { 146 | return `${initFunctionName}();`; 147 | } 148 | 149 | /** 150 | * Throw an error if issues with any options 151 | * @param opts 152 | */ 153 | export function validateOptions(opts: PrebootOptions) { 154 | if (!opts.appRoot || !opts.appRoot.length) { 155 | throw new Error( 156 | 'The appRoot is missing from preboot options. ' + 157 | 'This is needed to find the root of your application. ' + 158 | 'Set this value in the preboot options to be a selector for the root element of your app.' 159 | ); 160 | } 161 | } 162 | 163 | /** 164 | * Object.assign() is not fully supporting in TypeScript, so 165 | * this is just a simple implementation of it 166 | * 167 | * @param target The target object 168 | * @param optionSets Any number of addition objects that are added on top of the 169 | * target 170 | * @returns A new object that contains all the merged values 171 | */ 172 | export function assign(target: Object, ...optionSets: any[]): Object { 173 | if (target === undefined || target === null) { 174 | throw new TypeError('Cannot convert undefined or null to object'); 175 | } 176 | 177 | const output = Object(target); 178 | for (let index = 0; index < optionSets.length; index++) { 179 | const source = optionSets[index]; 180 | if (source !== undefined && source !== null) { 181 | for (const nextKey in source) { 182 | if (source.hasOwnProperty && source.hasOwnProperty(nextKey)) { 183 | output[nextKey] = source[nextKey]; 184 | } 185 | } 186 | } 187 | } 188 | 189 | return output; 190 | } 191 | 192 | /** 193 | * Stringify an object and include functions. This is needed since we are 194 | * letting users pass in options that include custom functions for things like 195 | * the freeze handler or action when an event occurs 196 | * 197 | * @param obj This is the object you want to stringify that includes some 198 | * functions 199 | * @returns The stringified version of an object 200 | */ 201 | export function stringifyWithFunctions(obj: Object): string { 202 | const FUNC_START = 'START_FUNCTION_HERE'; 203 | const FUNC_STOP = 'STOP_FUNCTION_HERE'; 204 | 205 | // first stringify except mark off functions with markers 206 | let str = JSON.stringify(obj, function(_key, value) { 207 | // if the value is a function, we want to wrap it with markers 208 | if (!!(value && value.constructor && value.call && value.apply)) { 209 | return FUNC_START + value.toString() + FUNC_STOP; 210 | } else { 211 | return value; 212 | } 213 | }); 214 | 215 | // now we use the markers to replace function strings with actual functions 216 | let startFuncIdx = str.indexOf(FUNC_START); 217 | let stopFuncIdx: number; 218 | let fn: string; 219 | while (startFuncIdx >= 0) { 220 | stopFuncIdx = str.indexOf(FUNC_STOP); 221 | 222 | // pull string out 223 | fn = str.substring(startFuncIdx + FUNC_START.length, stopFuncIdx); 224 | fn = fn.replace(/\\n/g, '\n'); 225 | 226 | str = str.substring(0, startFuncIdx - 1) + fn + 227 | str.substring(stopFuncIdx + FUNC_STOP.length + 1); 228 | startFuncIdx = str.indexOf(FUNC_START); 229 | } 230 | 231 | return str; 232 | } 233 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Preboot 2 | 3 | We would love for you to contribute to Preboot and help make it even better than it is 4 | today! As a contributor, here are the guidelines we would like you to follow: 5 | 6 | - [Code of Conduct](#coc) 7 | - [Question or Problem?](#question) 8 | - [Issues and Bugs](#issue) 9 | - [Feature Requests](#feature) 10 | - [Submission Guidelines](#submit) 11 | - [Coding Rules](#rules) 12 | - [Commit Message Guidelines](#commit) 13 | - [Signing the CLA](#cla) 14 | 15 | ## Code of Conduct 16 | Help us keep Preboot open and inclusive. Please read and follow our [Code of Conduct][coc]. 17 | 18 | ## Got a Question or Problem? 19 | 20 | If you have questions about how to *use* Preboot, please direct them to [Gitter][gitter]. 21 | There is also a [Slack](https://angular-universal.slack.com) ground that we would be happy to invite you to. 22 | Just ping [@jeffwhelpley](https://twitter.com/jeffwhelpley) 23 | or [@gdi2290](https://twitter.com/gdi2290) on [Twitter](https://twitter.com) or [Gitter][gitter]. 24 | 25 | ## Found an Issue? 26 | If you find a bug in the source code or a mistake in the documentation, you can help us by 27 | [submitting an issue](#submit-issue) to our [GitHub Repository][github]. Even better, you can 28 | [submit a Pull Request](#submit-pr) with a fix. 29 | 30 | ## Want a Feature? 31 | You can *request* a new feature by [submitting an issue](#submit-issue) to our [GitHub 32 | Repository][github]. If you would like to *implement* a new feature, please submit an issue with 33 | a proposal for your work first, to be sure that we can use it. 34 | Please consider what kind of change it is: 35 | 36 | * For a **Major Feature**, first open an issue and outline your proposal so that it can be 37 | discussed. This will also allow us to better coordinate our efforts, prevent duplication of work, 38 | and help you to craft the change so that it is successfully accepted into the project. 39 | * **Small Features** can be crafted and directly [submitted as a Pull Request](#submit-pr). 40 | 41 | ## Submission Guidelines 42 | 43 | ### Submitting an Issue 44 | Before you submit an issue, search the archive, maybe your question was already answered. 45 | 46 | If your issue appears to be a bug, and hasn't been reported, open a new issue. 47 | Help us to maximize the effort we can spend fixing issues and adding new 48 | features, by not reporting duplicate issues. Providing the following information will increase the 49 | chances of your issue being dealt with quickly: 50 | 51 | * **Overview of the Issue** - if an error is being thrown a non-minified stack trace helps 52 | * **Preboot Version** - what version is affected 53 | * **Motivation for or Use Case** - explain what you are trying to do and why the current behavior is a bug for you 54 | * **Browsers and Operating System** - is this a problem with all browsers? 55 | * **Reproduce the Error** - provide a live example (using [Plunker][plunker], 56 | [JSFiddle][jsfiddle] or [Runnable][runnable]) or a unambiguous set of steps 57 | * **Related Issues** - has a similar issue been reported before? 58 | * **Suggest a Fix** - if you can't fix the bug yourself, perhaps you can point to what might be 59 | causing the problem (line of code or commit) 60 | 61 | You can file new issues by providing the above information [here](https://github.com/angular/preboot/issues/new). 62 | 63 | ### Submitting a Pull Request (PR) 64 | Before you submit your Pull Request (PR) consider the following guidelines: 65 | 66 | * Search [GitHub](https://github.com/angular/preboot/pulls) for an open or closed PR 67 | that relates to your submission. You don't want to duplicate effort. 68 | * Please sign our [Contributor License Agreement (CLA)](#cla) before sending PRs. 69 | We cannot accept code without this. 70 | * Make your changes in a new git branch: 71 | 72 | ```shell 73 | git checkout -b my-fix-branch master 74 | ``` 75 | 76 | * Create your patch, **including appropriate test cases**. 77 | * Follow our [Coding Rules](#rules). 78 | * Run the full Angular test suite, as described in the [developer documentation][dev-doc], 79 | and ensure that all tests pass. 80 | * Commit your changes using a descriptive commit message that follows our 81 | [commit message conventions](#commit). Adherence to these conventions 82 | is necessary because release notes are automatically generated from these messages. 83 | 84 | ```shell 85 | git commit -a 86 | ``` 87 | Note: the optional commit `-a` command line option will automatically "add" and "rm" edited files. 88 | 89 | * Push your branch to GitHub: 90 | 91 | ```shell 92 | git push origin my-fix-branch 93 | ``` 94 | 95 | * In GitHub, send a pull request to `preboot:master`. 96 | * If we suggest changes then: 97 | * Make the required updates. 98 | * Re-run the Preboot test suites to ensure tests are still passing. 99 | * Rebase your branch and force push to your GitHub repository (this will update your Pull Request): 100 | 101 | ```shell 102 | git rebase master -i 103 | git push -f 104 | ``` 105 | 106 | That's it! Thank you for your contribution! 107 | 108 | #### After your pull request is merged 109 | 110 | After your pull request is merged, you can safely delete your branch and pull the changes 111 | from the main (upstream) repository: 112 | 113 | * Delete the remote branch on GitHub either through the GitHub web UI or your local shell as follows: 114 | 115 | ```shell 116 | git push origin --delete my-fix-branch 117 | ``` 118 | 119 | * Check out the master branch: 120 | 121 | ```shell 122 | git checkout master -f 123 | ``` 124 | 125 | * Delete the local branch: 126 | 127 | ```shell 128 | git branch -D my-fix-branch 129 | ``` 130 | 131 | * Update your master with the latest upstream version: 132 | 133 | ```shell 134 | git pull --ff upstream master 135 | ``` 136 | 137 | ## Coding Rules 138 | To ensure consistency throughout the source code, keep these rules in mind as you are working: 139 | 140 | * All features or bug fixes **must be tested** by one or more specs (unit-tests). 141 | * All public API methods **must be documented**. (Details TBC). 142 | * We follow [Google's JavaScript Style Guide][js-style-guide], but wrap all code at 143 | **100 characters**. An automated formatter is available, see 144 | [DEVELOPER.md](DEVELOPER.md#clang-format). 145 | 146 | ## Commit Message Guidelines 147 | 148 | We have very precise rules over how our git commit messages can be formatted. This leads to **more 149 | readable messages** that are easy to follow when looking through the **project history**. But also, 150 | we use the git commit messages to **generate the Angular change log**. 151 | 152 | ### Commit Message Format 153 | Each commit message consists of a **header**, a **body** and a **footer**. The header has a special 154 | format that includes a **type**, a **scope** and a **subject**: 155 | 156 | ``` 157 | (): 158 | 159 | 160 | 161 |