├── src ├── assets │ └── .gitkeep ├── app │ ├── app.component.scss │ ├── app.component.ts │ ├── app.module.ts │ └── app.component.html ├── favicon.ico ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── styles.scss ├── index.html ├── main.ts └── polyfills.ts ├── .prettierignore ├── projects └── ngx-template-streams │ ├── src │ ├── fixtures │ │ ├── empty-file.ts │ │ ├── tsconfig.json │ │ ├── empty.component.ts │ │ ├── external-template.component.ts │ │ ├── inline-template.component.ts │ │ ├── full-example.component.ts │ │ └── multiple-classes.ts │ ├── internal │ │ ├── constants.ts │ │ ├── index.ts │ │ ├── transformers │ │ │ ├── index.ts │ │ │ ├── queries.ts │ │ │ ├── __snapshots__ │ │ │ │ ├── transform-inline-template.spec.ts.snap │ │ │ │ ├── add-source-properties.spec.ts.snap │ │ │ │ ├── add-lifecycle-hooks.spec.ts.snap │ │ │ │ └── add-view-decorators.spec.ts.snap │ │ │ ├── transform-inline-template.ts │ │ │ ├── add-source-properties.spec.ts │ │ │ ├── add-source-properties.ts │ │ │ ├── transform-inline-template.spec.ts │ │ │ ├── add-lifecycle-hooks.ts │ │ │ ├── add-lifecycle-hooks.spec.ts │ │ │ ├── add-view-decorators.ts │ │ │ └── add-view-decorators.spec.ts │ │ ├── event-binding-parser.ts │ │ ├── loader.ts │ │ ├── loader.spec.ts │ │ ├── webpack-compiler-host.ts │ │ ├── event-binding-engine.ts │ │ ├── plugin.spec.ts │ │ ├── event-binding-engine.spec.ts │ │ ├── plugin.ts │ │ ├── webpack-compiler-host.spec.ts │ │ └── event-binding-parser.spec.ts │ ├── utils │ │ ├── module-helpers.ts │ │ ├── object-helpers.ts │ │ ├── models.ts │ │ ├── ts-helpers.ts │ │ ├── errors.ts │ │ ├── string-helpers.ts │ │ ├── template-helpers.ts │ │ ├── ast-helpers.ts │ │ └── decorator-helpers.ts │ ├── public_api.ts │ ├── testing │ │ ├── ts-compiler.ts │ │ └── test-helpers.ts │ └── decorators │ │ ├── observable-event.ts │ │ ├── observable-children.ts │ │ ├── observable-child.ts │ │ ├── observable-event.spec.ts │ │ ├── observable-child.spec.ts │ │ └── observable-children.spec.ts │ ├── test.ts │ ├── schematics │ ├── src │ │ └── ng-add │ │ │ ├── version-names.ts │ │ │ ├── schema.ts │ │ │ ├── schema.json │ │ │ ├── index.ts │ │ │ ├── ng-add.spec.ts │ │ │ └── setup.ts │ ├── package.json │ ├── collection.json │ ├── tsconfig.json │ ├── testing │ │ └── create-workspace.ts │ └── utils │ │ └── package-config.ts │ ├── tslint.json │ ├── tsconfig.spec.json │ ├── ng-package.json │ ├── tsconfig.internal.json │ ├── webpack │ └── webpack.config.js │ ├── package.json │ └── tsconfig.lib.json ├── .prettierrc ├── tsconfig.app.json ├── e2e ├── tsconfig.json ├── src │ ├── app.po.ts │ └── app.e2e-spec.ts └── protractor.conf.js ├── .editorconfig ├── jest.config.js ├── tsconfig.spec.json ├── browserslist ├── tsconfig.json ├── .gitignore ├── .github └── ISSUE_TEMPLATE │ ├── 2-feature-request.md │ └── 1-bug_report.md ├── LICENSE ├── tslint.json ├── CODE_OF_CONDUCT.md ├── package.json ├── CONTRIBUTING.md ├── angular.json └── README.md /src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | coverage 2 | fixtures -------------------------------------------------------------------------------- /projects/ngx-template-streams/src/fixtures/empty-file.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/ngx-template-streams/src/fixtures/tsconfig.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /projects/ngx-template-streams/test.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular'; 2 | -------------------------------------------------------------------------------- /projects/ngx-template-streams/src/internal/constants.ts: -------------------------------------------------------------------------------- 1 | export const INTERNAL_PREFIX = '__'; 2 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/typebytes/ngx-template-streams/HEAD/src/favicon.ico -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /projects/ngx-template-streams/schematics/src/ng-add/version-names.ts: -------------------------------------------------------------------------------- 1 | export const ngxBuildPlusVersion = '^8.0.3'; 2 | -------------------------------------------------------------------------------- /projects/ngx-template-streams/schematics/src/ng-add/schema.ts: -------------------------------------------------------------------------------- 1 | export interface AddSchema { 2 | project?: string; 3 | } 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "singleQuote": true, 4 | "useTabs": false, 5 | "tabWidth": 2, 6 | "semi": true, 7 | "bracketSpacing": true 8 | } 9 | -------------------------------------------------------------------------------- /projects/ngx-template-streams/src/internal/index.ts: -------------------------------------------------------------------------------- 1 | export * from './transformers'; 2 | export * from './event-binding-engine'; 3 | export * from './event-binding-parser'; 4 | -------------------------------------------------------------------------------- /projects/ngx-template-streams/src/utils/module-helpers.ts: -------------------------------------------------------------------------------- 1 | export function createModule(moduleExport: string) { 2 | return (template: string) => moduleExport + `"${template}"`; 3 | } 4 | -------------------------------------------------------------------------------- /projects/ngx-template-streams/src/utils/object-helpers.ts: -------------------------------------------------------------------------------- 1 | export function pick(obj: T, keys: Array) { 2 | return keys.map(k => (k in obj ? { [k]: obj[k] } : {})).reduce((res, o) => Object.assign(res, o), {}); 3 | } 4 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/app", 5 | "types": [] 6 | }, 7 | "include": ["src/**/*.ts"], 8 | "exclude": ["src/test.ts", "src/**/*.spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /projects/ngx-template-streams/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "directive-selector": [true, "attribute", "lib", "camelCase"], 5 | "component-selector": [true, "element", "lib", "kebab-case"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /projects/ngx-template-streams/src/fixtures/empty.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'test', 5 | template: 'test', 6 | styleUrls: ['./empty.component.css'] 7 | }) 8 | export class TestComponent {} 9 | -------------------------------------------------------------------------------- /projects/ngx-template-streams/src/public_api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Public API Surface of ngx-template-streams 3 | */ 4 | 5 | export * from './decorators/observable-child'; 6 | export * from './decorators/observable-children'; 7 | export * from './decorators/observable-event'; 8 | -------------------------------------------------------------------------------- /projects/ngx-template-streams/src/utils/models.ts: -------------------------------------------------------------------------------- 1 | export type ObservableDecorator = 'ObservableEvent' | 'ObservableChild' | 'ObservableChildren'; 2 | 3 | export enum LifecycleHook { 4 | ngOnDestroy = 'ngOnDestroy', 5 | ngAfterViewInit = 'ngAfterViewInit' 6 | } 7 | -------------------------------------------------------------------------------- /projects/ngx-template-streams/src/fixtures/external-template.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'test', 5 | templateUrl: './some/path', 6 | styleUrls: ['./app.component.css'] 7 | }) 8 | export class TestComponent {} 9 | -------------------------------------------------------------------------------- /e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /projects/ngx-template-streams/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/spec", 5 | "types": ["jest", "node"], 6 | "module": "commonjs" 7 | }, 8 | "include": ["**/*.spec.ts", "**/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.scss'] 7 | }) 8 | export class AppComponent { 9 | title = 'ngx-template-streams-app'; 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://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 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get(browser.baseUrl) as Promise; 6 | } 7 | 8 | getTitleText() { 9 | return element(by.css('app-root h1')).getText() as Promise; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /projects/ngx-template-streams/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/ngx-template-streams", 4 | "lib": { 5 | "entryFile": "src/public_api.ts" 6 | }, 7 | "whitelistedNonPeerDependencies": ["tslib", "fp-ts", "@phenomnomnominal/tsquery"] 8 | } 9 | -------------------------------------------------------------------------------- /projects/ngx-template-streams/src/utils/ts-helpers.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | export function printFileContent(sourceFile: ts.SourceFile, newLine = ts.NewLineKind.LineFeed): string { 4 | const printer = ts.createPrinter({ 5 | newLine 6 | }); 7 | 8 | return printer.printFile(sourceFile); 9 | } 10 | -------------------------------------------------------------------------------- /projects/ngx-template-streams/tsconfig.internal.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.lib.json", 3 | "compilerOptions": { 4 | "target": "es5", 5 | "module": "commonjs", 6 | "declaration": true, 7 | "skipLibCheck": true, 8 | "outDir": "../../dist/ngx-template-streams" 9 | }, 10 | "include": ["src/internal"] 11 | } 12 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | 4 | import { AppComponent } from './app.component'; 5 | 6 | @NgModule({ 7 | declarations: [AppComponent], 8 | imports: [BrowserModule], 9 | bootstrap: [AppComponent] 10 | }) 11 | export class AppModule {} 12 | -------------------------------------------------------------------------------- /projects/ngx-template-streams/src/fixtures/inline-template.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'test', 5 | template: ` 6 | 7 | `, 8 | styleUrls: ['./app.component.css'] 9 | }) 10 | export class TestComponent {} 11 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'jest-preset-angular', 3 | rootDir: 'projects/ngx-template-streams', 4 | roots: ['src', 'schematics'], 5 | setupFilesAfterEnv: ['/test.ts'], 6 | globals: { 7 | 'ts-jest': { 8 | tsConfig: '/tsconfig.spec.json' 9 | } 10 | }, 11 | watchPathIgnorePatterns: ['.js'] 12 | }; 13 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /projects/ngx-template-streams/src/utils/errors.ts: -------------------------------------------------------------------------------- 1 | export class ObservableEventError extends Error { 2 | constructor(decorator: string, event: string, target: string, message?: string) { 3 | const additionalMessage = message ? ' ' + message : ''; 4 | super(`[${decorator}] Cannot create event stream for '${event}' on target '${target}'.` + additionalMessage); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NgxTemplateStreamsApp 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /projects/ngx-template-streams/src/utils/string-helpers.ts: -------------------------------------------------------------------------------- 1 | const STRING_CAMELIZE_REGEXP = /(-|_|\.|\s)+(.)?/g; 2 | 3 | export function camelize(str: string): string { 4 | return str 5 | .replace(STRING_CAMELIZE_REGEXP, (_match: string, _separator: string, chr: string) => { 6 | return chr ? chr.toUpperCase() : ''; 7 | }) 8 | .replace(/^([A-Z])/, (match: string) => match.toLowerCase()); 9 | } 10 | -------------------------------------------------------------------------------- /projects/ngx-template-streams/schematics/src/ng-add/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/schema", 3 | "id": "NgxTemplateStreamsAdd", 4 | "title": "Add Schema", 5 | "type": "object", 6 | "properties": { 7 | "project": { 8 | "type": "string", 9 | "description": "The name of the project.", 10 | "$default": { 11 | "$source": "projectName" 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /projects/ngx-template-streams/webpack/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | module: { 5 | rules: [ 6 | { 7 | test: /\.html$/, 8 | use: [ 9 | { 10 | loader: path.resolve(__dirname, '../internal/loader') 11 | }, 12 | { 13 | loader: 'raw-loader' 14 | } 15 | ] 16 | } 17 | ] 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic() 12 | .bootstrapModule(AppModule) 13 | .catch(err => console.error(err)); 14 | -------------------------------------------------------------------------------- /projects/ngx-template-streams/src/utils/template-helpers.ts: -------------------------------------------------------------------------------- 1 | export function escapeQuotes(input: string) { 2 | // double escape quotes so that they are not unescaped completely in the template string 3 | return input.replace(/"/g, '\\"'); 4 | } 5 | 6 | export function escapeEventPayload(payload: string) { 7 | // double escape dollar signs ($) because they also represent an escape character 8 | return payload.replace(/\$/g, '$$$$'); 9 | } 10 | -------------------------------------------------------------------------------- /browserslist: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # You can see what browsers were selected by your queries by running: 6 | # npx browserslist 7 | 8 | > 0.5% 9 | last 2 versions 10 | Firefox ESR 11 | not dead 12 | not IE 9-11 # For IE 9-11 support, remove 'not'. -------------------------------------------------------------------------------- /projects/ngx-template-streams/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@typebytes/ngx-template-streams", 3 | "version": "1.2.1", 4 | "license": "MIT", 5 | "schematics": "./schematics/collection.json", 6 | "dependencies": { 7 | "@phenomnomnominal/tsquery": "^3.0.0", 8 | "fp-ts": "^1.19.2" 9 | }, 10 | "peerDependencies": { 11 | "@angular/common": "^8.2.13", 12 | "@angular/core": "^8.2.13", 13 | "@ngtools/webpack": "^8.3.17", 14 | "rxjs": ">= 6.0.0", 15 | "typescript": "^3.0.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /projects/ngx-template-streams/src/fixtures/full-example.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { ObservableEvent } from 'ngx-template-streams'; 3 | import { Observable } from 'rxjs'; 4 | 5 | @Component({ 6 | selector: 'test', 7 | template: ` 8 | 9 | 10 | `, 11 | styleUrls: ['./app.component.css'] 12 | }) 13 | export class TestComponent { 14 | @ObservableEvent() 15 | click$: Observable; 16 | 17 | @ObservableEvent() 18 | focus$: Observable; 19 | } 20 | -------------------------------------------------------------------------------- /projects/ngx-template-streams/src/fixtures/multiple-classes.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { ObservableEvent } from 'ngx-template-streams'; 3 | import { Observable } from 'rxjs'; 4 | 5 | @Component({ 6 | selector: 'a', 7 | template: '\`, 8 | styleUrls: ['./app.component.css'] 9 | }) 10 | export class TestComponent { 11 | } 12 | " 13 | `; 14 | 15 | exports[`inlineTemplateTransformer should not apply transformation if component has an external template 1`] = ` 16 | "import { Component } from '@angular/core'; 17 | @Component({ 18 | selector: 'test', 19 | templateUrl: './some/path', 20 | styleUrls: ['./app.component.css'] 21 | }) 22 | export class TestComponent { 23 | } 24 | " 25 | `; 26 | 27 | exports[`inlineTemplateTransformer should not apply transformation to empty file 1`] = `""`; 28 | 29 | exports[`inlineTemplateTransformer should transform inline template of multiple classes in a single file 1`] = ` 30 | "import { Component } from '@angular/core'; 31 | import { ObservableEvent } from 'ngx-template-streams'; 32 | import { Observable } from 'rxjs'; 33 | @Component({ 34 | selector: 'a', 35 | template: \` 11 | 12 | \`, 13 | styleUrls: ['./app.component.css'] 14 | }) 15 | export class TestComponent { 16 | @ObservableEvent() 17 | click$: Observable; 18 | @ObservableEvent() 19 | focus$: Observable; 20 | __click$; 21 | __focus$; 22 | } 23 | " 24 | `; 25 | 26 | exports[`addSourcePropertiesTransformer should not apply transformation if the file is empty 1`] = `""`; 27 | 28 | exports[`addSourcePropertiesTransformer should return original source file if it has no observable events 1`] = ` 29 | "import { Component } from '@angular/core'; 30 | @Component({ 31 | selector: 'test', 32 | template: 'test', 33 | styleUrls: ['./empty.component.css'] 34 | }) 35 | export class TestComponent { 36 | } 37 | " 38 | `; 39 | 40 | exports[`addSourcePropertiesTransformer should transform multiple classes in a single file 1`] = ` 41 | "import { Component } from '@angular/core'; 42 | import { ObservableEvent } from 'ngx-template-streams'; 43 | import { Observable } from 'rxjs'; 44 | @Component({ 45 | selector: 'a', 46 | template: ' 14 | `; 15 | 16 | const result = updateEventBindings(source); 17 | expect(result).toEqual(source); 18 | }); 19 | 20 | it('should transform simple template stream', () => { 21 | const source = createTemplate` 22 | 23 | `; 24 | 25 | const expected = createTemplate` 26 | 27 | `; 28 | 29 | const result = updateEventBindings(source); 30 | expect(result).toEqual(expected); 31 | }); 32 | 33 | it('should transform multiple and deeply nested template streams', () => { 34 | const source = createTemplate` 35 | 36 |
37 |
38 | 39 |
40 |
41 | `; 42 | 43 | const expected = createTemplate` 44 | 45 |
46 |
47 | 48 |
49 |
50 | `; 51 | 52 | const result = updateEventBindings(source); 53 | expect(result).toEqual(expected); 54 | }); 55 | 56 | test.each([`1`, `'strings work too'`, `{ prop: 1 }`])('should respect various payloads › %s', payload => { 57 | const source = createTemplate` 58 | 59 | `; 60 | 61 | const expected = createTemplate` 62 | 63 | `; 64 | 65 | const result = updateEventBindings(source); 66 | expect(result).toEqual(expected); 67 | }); 68 | 69 | it('should not escape qutes for inline templates', () => { 70 | const source = createInlineTemplate` 71 | 72 | `; 73 | 74 | const expected = createInlineTemplate` 75 | 76 | `; 77 | 78 | const result = updateEventBindings(source, true); 79 | 80 | expect(result).toEqual(expected); 81 | }); 82 | 83 | it('should correctly escape dollar signs as part of the event payload', () => { 84 | const source = createInlineTemplate` 85 | 86 | `; 87 | 88 | const expected = createInlineTemplate` 89 | 90 | `; 91 | 92 | const result = updateEventBindings(source, true); 93 | 94 | expect(result).toEqual(expected); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /projects/ngx-template-streams/src/internal/transformers/add-lifecycle-hooks.ts: -------------------------------------------------------------------------------- 1 | import { some } from 'fp-ts/lib/Option'; 2 | import * as ts from 'typescript'; 3 | import { findNodes, getComponentClass } from '../../utils/ast-helpers'; 4 | import { LifecycleHook, ObservableDecorator } from '../../utils/models'; 5 | import { getEventStreamDecoratorQuery, getLifecycleQuery } from './queries'; 6 | 7 | type DecoratorHooks = { [key in LifecycleHook]: Array }; 8 | 9 | const decoratorHooks: DecoratorHooks = { 10 | [LifecycleHook.ngOnDestroy]: ['ObservableEvent', 'ObservableChild', 'ObservableChildren'], 11 | [LifecycleHook.ngAfterViewInit]: ['ObservableChildren'] 12 | }; 13 | 14 | export function addLifecycleHooksTransformer(context: ts.TransformationContext) { 15 | return (sourceFile: ts.SourceFile) => { 16 | return some(sourceFile) 17 | .chain(addLifecycleHook(LifecycleHook.ngOnDestroy, context)) 18 | .chain(addLifecycleHook(LifecycleHook.ngAfterViewInit, context)) 19 | .getOrElse(sourceFile); 20 | }; 21 | } 22 | 23 | function addLifecycleHook(lifecycle: LifecycleHook, context: ts.TransformationContext) { 24 | return (sourceFile: ts.SourceFile) => { 25 | const result = hasLifecycleHook(lifecycle, sourceFile) 26 | .filter(lifecycleExists => !lifecycleExists && requiresLifecycleHook(lifecycle, sourceFile)) 27 | .chain(_ => getComponentClass(sourceFile)) 28 | .map(updateComponentClass(lifecycle, sourceFile, context)); 29 | 30 | return result.isNone() ? some(sourceFile) : result; 31 | }; 32 | } 33 | 34 | function updateComponentClass(lifecycle: LifecycleHook, sourceFile: ts.SourceFile, context: ts.TransformationContext) { 35 | return (nodes: Array) => { 36 | const visitor: ts.Visitor = (node: ts.Node) => { 37 | if (nodes.includes(node)) { 38 | const classDeclaration = node as ts.ClassDeclaration; 39 | 40 | const lifecycleMethod = ts.createMethod( 41 | [], 42 | undefined, 43 | undefined, 44 | ts.createIdentifier(lifecycle), 45 | undefined, 46 | undefined, 47 | undefined, 48 | undefined, 49 | ts.createBlock([]) 50 | ); 51 | 52 | return ts.updateClassDeclaration( 53 | classDeclaration, 54 | classDeclaration.decorators, 55 | classDeclaration.modifiers, 56 | classDeclaration.name, 57 | classDeclaration.typeParameters, 58 | classDeclaration.heritageClauses, 59 | ts.createNodeArray([...classDeclaration.members, lifecycleMethod]) 60 | ); 61 | } 62 | 63 | return ts.visitEachChild(node, visitor, context); 64 | }; 65 | 66 | return ts.visitNode(sourceFile, visitor); 67 | }; 68 | } 69 | 70 | function requiresLifecycleHook(lifecycle: LifecycleHook, sourceFile: ts.SourceFile) { 71 | const nodes = decoratorHooks[lifecycle].map(decorator => 72 | findNodes(sourceFile, getEventStreamDecoratorQuery(decorator)) 73 | ); 74 | 75 | return nodes.some(declarations => declarations.isSome()); 76 | } 77 | 78 | function hasLifecycleHook(lifecycle: LifecycleHook, sourceFile: ts.SourceFile) { 79 | const nodes = findNodes(sourceFile, getLifecycleQuery(lifecycle)); 80 | return some(nodes.isSome()); 81 | } 82 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | - Using welcoming and inclusive language 12 | - Being respectful of differing viewpoints and experiences 13 | - Gracefully accepting constructive criticism 14 | - Focusing on what is best for the community 15 | - Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | - The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | - Trolling, insulting/derogatory comments, and personal or political attacks 21 | - Public or private harassment 22 | - Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | - Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at elmdominic@gmx.net. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-template-streams-app", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "postinstall": "ngcc --properties es2015 browser module main --first-only --create-ivy-entry-points", 7 | "start": "ng serve", 8 | "clean": "rimraf dist", 9 | "build:app": "ng build app", 10 | "build": "npm run clean && npm run build:lib && npm run build:internal && npm run build:schematics && npm run copy", 11 | "build:lib": "ng build ngx-template-streams", 12 | "build:internal": "tsc -p projects/ngx-template-streams/tsconfig.internal.json", 13 | "build:schematics": "tsc -p projects/ngx-template-streams/schematics/tsconfig.json", 14 | "copy": "npm run copy:readme && npm run copy:webpack && npm run copy:schematics", 15 | "copy:readme": "cpx README.md dist/ngx-template-streams", 16 | "copy:webpack": "cpx \"projects/ngx-template-streams/webpack/**\" dist/ngx-template-streams/webpack", 17 | "copy:schematics": "cpx \"projects/ngx-template-streams/schematics/**/{collection.json,schema.json,files/**}\" dist/ngx-template-streams/schematics", 18 | "format:base": "prettier \"{src,projects}/**/*{.ts,.js,.json}\"", 19 | "format:check": "npm run format:base -- --list-different", 20 | "format:fix": "npm run format:base -- --write", 21 | "style": "npm run lint:check && npm run format:check", 22 | "style:fix": "npm run lint:fix && npm run format:fix", 23 | "lint:check": "ng lint", 24 | "lint:fix": "ng lint app --fix && ng lint ngx-template-streams --fix", 25 | "test": "jest -c jest.config.js", 26 | "test:debug": "node --inspect-brk ./node_modules/jest/bin/jest.js -c jest.config.js --runInBand", 27 | "lint": "ng lint", 28 | "e2e": "ng e2e" 29 | }, 30 | "private": true, 31 | "jest": { 32 | "preset": "jest-preset-angular", 33 | "setupTestFrameworkScriptFile": "/projects/ngx-template-streams/test.ts" 34 | }, 35 | "dependencies": { 36 | "@angular/animations": "~9.0.0-rc.0", 37 | "@angular/common": "~9.0.0-rc.0", 38 | "@angular/compiler": "~9.0.0-rc.0", 39 | "@angular/core": "~9.0.0-rc.0", 40 | "@angular/forms": "~9.0.0-rc.0", 41 | "@angular/platform-browser": "~9.0.0-rc.0", 42 | "@angular/platform-browser-dynamic": "~9.0.0-rc.0", 43 | "@angular/router": "~9.0.0-rc.0", 44 | "@phenomnomnominal/tsquery": "^3.0.0", 45 | "fp-ts": "^1.19.2", 46 | "rxjs": "~6.4.0", 47 | "tslib": "^1.9.0", 48 | "zone.js": "~0.10.2" 49 | }, 50 | "devDependencies": { 51 | "@angular-devkit/build-angular": "~0.800.0", 52 | "@angular-devkit/build-ng-packagr": "~0.800.0", 53 | "@angular/cli": "~8.0.3", 54 | "@angular/compiler-cli": "~9.0.0-rc.0", 55 | "@angular/language-service": "~9.0.0-rc.0", 56 | "@testing-library/angular": "^7.1.0", 57 | "@types/jest": "^24.0.15", 58 | "@types/memory-fs": "^0.3.2", 59 | "@types/node": "^12.0.10", 60 | "@types/rimraf": "^2.0.2", 61 | "@types/webpack": "^4.4.34", 62 | "codelyzer": "^5.0.0", 63 | "cpx": "^1.5.0", 64 | "jest": "^24.8.0", 65 | "jest-preset-angular": "^7.1.1", 66 | "memfs": "^2.15.4", 67 | "memory-fs": "^0.4.1", 68 | "ng-packagr": "^5.1.0", 69 | "prettier": "^1.18.2", 70 | "protractor": "~5.4.0", 71 | "rimraf": "^2.6.3", 72 | "ts-loader": "^6.0.4", 73 | "ts-node": "~7.0.0", 74 | "tsickle": "^0.35.0", 75 | "tslint": "~5.15.0", 76 | "typescript": "~3.6.4", 77 | "webpack-cli": "^3.3.5" 78 | } 79 | } -------------------------------------------------------------------------------- /projects/ngx-template-streams/src/internal/plugin.ts: -------------------------------------------------------------------------------- 1 | import { AngularCompilerPlugin, AngularCompilerPluginOptions } from '@ngtools/webpack'; 2 | import { WebpackCompilerHost } from '@ngtools/webpack/src/compiler_host'; 3 | import * as ts from 'typescript'; 4 | import * as webpack from 'webpack'; 5 | import { jitTransformers } from './transformers'; 6 | import { getSourceFile } from './webpack-compiler-host'; 7 | 8 | // This key is used to access a private property on the AngularCompilerPlugin 9 | // that indicates whether we are running in JIT mode or not 10 | export const JIT_MODE = '_JitMode'; 11 | 12 | /** 13 | * For AOT we have to patch the WebpackCompilerHost's getSourceFile method to run source code transformations 14 | * before the compiler generates AOT artifacts (both Ivy and ViewEngine). 15 | * Platform transformers are called too late in the pipeline. 16 | * See https://github.com/angular/angular-cli/blob/master/packages/ngtools/webpack/src/compiler_host.ts#L265-L291 17 | */ 18 | WebpackCompilerHost.prototype.getSourceFile = getSourceFile; 19 | 20 | export default { 21 | config(config: webpack.Configuration) { 22 | const angularCompilerPlugin = findAngularCompilerPlugin(config) as AngularCompilerPlugin; 23 | 24 | if (!angularCompilerPlugin) { 25 | throw new Error('Could not inject TypeScript Transformer: Webpack AngularCompilerPlugin not found'); 26 | } 27 | 28 | const jitMode = isJitMode(angularCompilerPlugin); 29 | 30 | // Turn off direct template loading. By default this option is `true`, causing 31 | // the plugin to load component templates (HTML) directly from the filesystem. 32 | // This is more efficient than using the raw-loader. However, if we want to add 33 | // a custom html-loader we have to turn off direct template loading. To do so 34 | // we have to remove the old compiler plugin and create a new instance with 35 | // updated options. 36 | 37 | // We also don't run the TypeChecker in a forked process when AOT is enabled. 38 | // Because of template and class transformations this results in type errors. 39 | // TODO: This needs a bit further investigation. 40 | 41 | const options: AngularCompilerPluginOptions = { 42 | ...angularCompilerPlugin.options, 43 | directTemplateLoading: false, 44 | forkTypeChecker: jitMode 45 | }; 46 | 47 | config.plugins = removeCompilerPlugin(config.plugins, angularCompilerPlugin); 48 | 49 | const newCompilerPlugin = new AngularCompilerPlugin(options); 50 | 51 | if (jitMode) { 52 | // Warning: this method is *not* pure and modifies the array of transformers directly 53 | addTransformers(newCompilerPlugin, jitTransformers); 54 | } 55 | 56 | config.plugins.push(newCompilerPlugin); 57 | 58 | return config; 59 | } 60 | }; 61 | 62 | function findAngularCompilerPlugin(config: webpack.Configuration) { 63 | return config.plugins && config.plugins.find(isAngularCompilerPlugin); 64 | } 65 | 66 | function isAngularCompilerPlugin(plugin: webpack.Plugin) { 67 | return plugin instanceof AngularCompilerPlugin; 68 | } 69 | 70 | function addTransformers(acp: any, transformers: Array>): void { 71 | // The AngularCompilerPlugin has no public API to add transformers, use private API _transformers instead 72 | acp._transformers = [...transformers, ...acp._transformers]; 73 | } 74 | 75 | function removeCompilerPlugin(plugins: webpack.Plugin[], acp: webpack.Plugin) { 76 | return plugins.filter(plugin => plugin !== acp); 77 | } 78 | 79 | function isJitMode(plugin: AngularCompilerPlugin) { 80 | return plugin[JIT_MODE]; 81 | } 82 | -------------------------------------------------------------------------------- /projects/ngx-template-streams/src/internal/webpack-compiler-host.spec.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs'; 2 | import { resolve } from 'path'; 3 | import * as ts from 'typescript'; 4 | import { fixtures } from '../testing/test-helpers'; 5 | import { aotTransformers } from './transformers'; 6 | import { getSourceFile, RESOURCE_LOADER } from './webpack-compiler-host'; 7 | 8 | class MockedWebpackCompilerHost { 9 | /* AOT Flag */ 10 | _resourceLoader = null; 11 | 12 | /* Cache for source files */ 13 | _sourceFileCache = new Map(); 14 | 15 | /* Whether or not files should be cached */ 16 | cacheSourceFiles = false; 17 | 18 | resolve(fileName: string) { 19 | return fileName; 20 | } 21 | 22 | readFile(fileName: string) { 23 | return readFileSync(resolve(fixtures, fileName), 'utf-8'); 24 | } 25 | } 26 | 27 | describe('WebpackCompilerHost', () => { 28 | let compilerHost: MockedWebpackCompilerHost; 29 | let patchedGetSourceFile: typeof getSourceFile; 30 | let transformSpy: jest.SpyInstance; 31 | 32 | const enableJIT = () => { 33 | compilerHost[RESOURCE_LOADER] = null; 34 | }; 35 | 36 | const enableAOT = () => { 37 | compilerHost[RESOURCE_LOADER] = {}; 38 | }; 39 | 40 | const enableCaching = () => { 41 | compilerHost.cacheSourceFiles = true; 42 | }; 43 | 44 | beforeEach(() => { 45 | compilerHost = new MockedWebpackCompilerHost(); 46 | patchedGetSourceFile = getSourceFile.bind(compilerHost); 47 | 48 | transformSpy = jest.spyOn(ts, 'transform'); 49 | }); 50 | 51 | afterEach(() => { 52 | jest.restoreAllMocks(); 53 | }); 54 | 55 | it('should not call transformers in JIT mode', () => { 56 | enableJIT(); 57 | 58 | patchedGetSourceFile('full-example.component.ts', ts.ScriptTarget.Latest); 59 | 60 | expect(transformSpy).not.toHaveBeenCalled(); 61 | }); 62 | 63 | it('should not call transformers if the file does not contain a component', () => { 64 | enableAOT(); 65 | 66 | patchedGetSourceFile('empty-file.ts', ts.ScriptTarget.Latest); 67 | 68 | expect(transformSpy).not.toHaveBeenCalled(); 69 | }); 70 | 71 | it('should call transformers if AOT is enabled and file contains a component', () => { 72 | enableAOT(); 73 | 74 | patchedGetSourceFile('full-example.component.ts', ts.ScriptTarget.Latest); 75 | 76 | expect(transformSpy).toHaveBeenCalled(); 77 | expect(transformSpy.mock.calls[0][1]).toBe(aotTransformers); 78 | }); 79 | 80 | it('should not call transformers if the file has been cached', () => { 81 | enableAOT(); 82 | enableCaching(); 83 | 84 | const fileName = 'full-example.component.ts'; 85 | 86 | const sourceFile = patchedGetSourceFile(fileName, ts.ScriptTarget.Latest); 87 | 88 | expect(transformSpy).toHaveBeenCalled(); 89 | expect(compilerHost._sourceFileCache.get(fileName)).toEqual(sourceFile); 90 | 91 | // reset spies 92 | transformSpy.mockReset(); 93 | 94 | // call again with the same file name 95 | patchedGetSourceFile(fileName, ts.ScriptTarget.Latest); 96 | 97 | expect(transformSpy).not.toHaveBeenCalled(); 98 | }); 99 | 100 | it('should gracefully return if an error occurs', () => { 101 | const onError = jest.fn(); 102 | 103 | const sourceFile = patchedGetSourceFile('does-not-exist', ts.ScriptTarget.Latest, onError); 104 | 105 | expect(onError).toHaveBeenCalled(); 106 | expect(onError.mock.calls[0][0]).toContain('ENOENT: no such file or directory'); 107 | expect(transformSpy).not.toHaveBeenCalled(); 108 | expect(sourceFile).not.toBeDefined(); 109 | }); 110 | }); 111 | -------------------------------------------------------------------------------- /projects/ngx-template-streams/src/internal/transformers/__snapshots__/add-lifecycle-hooks.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`addLifecycleHooksTransformer ngAfterViewInit should add ngAfterViewInit if it does not exist 1`] = ` 4 | "@Component() 5 | class TestComponent { 6 | @ObservableChildren() 7 | stream$: Observable; 8 | ngOnDestroy() { } 9 | ngAfterViewInit() { } 10 | } 11 | " 12 | `; 13 | 14 | exports[`addLifecycleHooksTransformer ngAfterViewInit should not add ngAfterViewInit if it exists 1`] = ` 15 | "@Component() 16 | class TestComponent { 17 | @ObservableChildren() 18 | stream$: Observable; 19 | ngAfterViewInit() { 20 | console.log('test'); 21 | } 22 | ngOnDestroy() { } 23 | } 24 | " 25 | `; 26 | 27 | exports[`addLifecycleHooksTransformer ngAfterViewInit should not add ngAfterViewInit › ObservableChild 1`] = ` 28 | "@Component() 29 | class TestComponent { 30 | @ObservableChild() 31 | stream$: Observable; 32 | ngOnDestroy() { } 33 | } 34 | " 35 | `; 36 | 37 | exports[`addLifecycleHooksTransformer ngAfterViewInit should not add ngAfterViewInit › ObservableEvent 1`] = ` 38 | "@Component() 39 | class TestComponent { 40 | @ObservableEvent() 41 | stream$: Observable; 42 | ngOnDestroy() { } 43 | } 44 | " 45 | `; 46 | 47 | exports[`addLifecycleHooksTransformer ngOnDestroy should add ngOnDestroy if it does not exist › ObservableChild 1`] = ` 48 | "@Component() 49 | class TestComponent { 50 | @ObservableChild() 51 | stream$: Observable; 52 | ngOnDestroy() { } 53 | } 54 | " 55 | `; 56 | 57 | exports[`addLifecycleHooksTransformer ngOnDestroy should add ngOnDestroy if it does not exist › ObservableChildren 1`] = ` 58 | "@Component() 59 | class TestComponent { 60 | @ObservableChildren() 61 | stream$: Observable; 62 | ngOnDestroy() { } 63 | ngAfterViewInit() { } 64 | } 65 | " 66 | `; 67 | 68 | exports[`addLifecycleHooksTransformer ngOnDestroy should add ngOnDestroy if it does not exist › ObservableEvent 1`] = ` 69 | "@Component() 70 | class TestComponent { 71 | @ObservableEvent() 72 | stream$: Observable; 73 | ngOnDestroy() { } 74 | } 75 | " 76 | `; 77 | 78 | exports[`addLifecycleHooksTransformer ngOnDestroy should not add ngOnDestroy for unknown decorators 1`] = ` 79 | "@Component() 80 | class TestComponent { 81 | @Input() 82 | myInput: number; 83 | @Output() 84 | myOutput = new EventEmitter(); 85 | @ViewChild('test', { static: true }) 86 | testElement; 87 | } 88 | " 89 | `; 90 | 91 | exports[`addLifecycleHooksTransformer ngOnDestroy should not add ngOnDestroy if it exists › ObservableChild 1`] = ` 92 | "@Component() 93 | class TestComponent { 94 | @ObservableChild() 95 | stream$: Observable; 96 | ngOnDestroy() { 97 | console.log('test'); 98 | } 99 | } 100 | " 101 | `; 102 | 103 | exports[`addLifecycleHooksTransformer ngOnDestroy should not add ngOnDestroy if it exists › ObservableChildren 1`] = ` 104 | "@Component() 105 | class TestComponent { 106 | @ObservableChildren() 107 | stream$: Observable; 108 | ngOnDestroy() { 109 | console.log('test'); 110 | } 111 | ngAfterViewInit() { } 112 | } 113 | " 114 | `; 115 | 116 | exports[`addLifecycleHooksTransformer ngOnDestroy should not add ngOnDestroy if it exists › ObservableEvent 1`] = ` 117 | "@Component() 118 | class TestComponent { 119 | @ObservableEvent() 120 | stream$: Observable; 121 | ngOnDestroy() { 122 | console.log('test'); 123 | } 124 | } 125 | " 126 | `; 127 | 128 | exports[`addLifecycleHooksTransformer should not apply transformation if class doesn not contain known decorators 1`] = ` 129 | "@Component() 130 | class TestComponent { 131 | } 132 | " 133 | `; 134 | 135 | exports[`addLifecycleHooksTransformer should not apply transformation if the file is empty 1`] = `""`; 136 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | [fork]: /fork 4 | [pr]: /compare 5 | [style]: https://prettier.io 6 | [code-of-conduct]: CODE_OF_CONDUCT.md 7 | 8 | Hi there! We're thrilled that you'd like to contribute to this project. Your help is essential for keeping it great. 9 | 10 | Please note that this project is released with a [Contributor Code of Conduct][code-of-conduct]. By participating in this project you agree to abide by its terms. 11 | 12 | ## Submitting a pull request 13 | 14 | 1. [Fork][fork] and clone the repository 15 | 2. Configure and install the dependencies: `yarn install` 16 | 3. Make sure the tests pass on your machine: `yarn test` 17 | 4. Create a new branch: `git checkout -b my-branch-name` 18 | 5. Make your change, add tests, and make sure the tests still pass 19 | 6. A pre-commit hook will make sure that your code is properly formatted, but you can also manually check for linting or formatting errors by running `yarn style` 20 | 7. Push to your fork and [submit a pull request][pr] 21 | 8. Pat yourself on the back and wait for your pull request to be reviewed and merged. 22 | 23 | Here are a few things you can do that will increase the likelihood of your pull request being accepted: 24 | 25 | - Follow the [style guide][style] which is using Prettier. Any linting errors should be shown when running `npm lint:check`. Linting or formatting errors can be automatically fixed by running `yarn style:fix`. 26 | - Write and update tests. 27 | - Keep your change as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as separate pull requests. 28 | - Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html). 29 | 30 | Work in Progress pull request are also welcome to get feedback early on, or if there is something blocked you. 31 | 32 | ## Commit Message Format 33 | 34 | Each commit message consists of a **header**, a **body** and a **footer**. The header has a special 35 | format that includes a **type**, a **scope** and a **subject**: 36 | 37 | ``` 38 | (): 39 | 40 | 41 | 42 |