├── 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 |