Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Master Tooling by Hacking the Angular Compiler

Dominic Elm
December 06, 2019

Master Tooling by Hacking the Angular Compiler

Nowadays, we can hardly imagine the web without tooling. There are tools for code generation, transformations, and, most importantly, build tools. Build tools play an important role in modern web development. They make frameworks like Angular, React, or Vue more approachable and simplify the entire process from start to end. For example, a build tool manipulates our code in powerful ways; it downlevels our code, makes it smaller, and creates a production-grade bundle that is ready for deployment. But how does that work? To understand this, we will specifically look at Angular's build system to implement our own custom template syntax. Along the way, we explore tools such as Webpack, the Angular Compiler, and, most importantly, an Abstract Syntax Tree (AST). ASTs are the basic building blocks of most web tooling that empowers tools like Angular Schematics, Prettier, Babel, or TSLint to do their job. This talk should give you an insight into how Angular builds our applications, and act as an intro to Abstract Syntax Trees, how they work, and what we can do with them.

Special thanks to Kwinten Pisman (https://twitter.com/KwintenP) for helping me putting this talk together.

Links:
https://github.com/typebytes/ngx-template-streams
https://astexplorer.net/
https://github.com/phenomnomnominal/tsquery
https://github.com/urish/tsquery-playground

Dominic Elm

December 06, 2019
Tweet

More Decks by Dominic Elm

Other Decks in Programming

Transcript

  1. <button (click)=“clicks$.next($event)”>Click Me</button> @Component({...}) export class AppComponent implements OnInit {

    clicks$ = new Subject(); ngOnInit() { // we can either manually subscribe // or use the async pipe this.clicks$.subscribe(console.log); } }
  2. <button (click)=“clicks$.next($event)”>Click Me</button> @Component({...}) export class AppComponent implements OnInit {

    clicks$ = new Subject(); ngOnInit() { // we can either manually subscribe // or use the async pipe this.clicks$.subscribe(console.log); } }
  3. <button (click)=“clicks$.next($event)”>Click Me</button> @Component({...}) export class AppComponent implements OnInit {

    clicks$ = new Subject(); ngOnInit() { // we can either manually subscribe // or use the async pipe this.clicks$.subscribe(console.log); } }
  4. <button (click)=“clicks$.next($event)”>Click Me</button> @Component({...}) export class AppComponent implements OnInit {

    clicks$ = new Subject(); ngOnInit() { // we can either manually subscribe // or use the async pipe this.clicks$.subscribe(console.log); } }
  5. <button (*click)=“clicks$”>Click Me</button> @Component({...}) export class AppComponent implements OnInit {

    @ObservableEvent() clicks$: Observable<any>; ngOnInit() { // we can either manually subscribe // or use the async pipe this.clicks$.subscribe(console.log); } }
  6. <button (*click)=“clicks$”>Click Me</button> @Component({...}) export class AppComponent implements OnInit {

    @ObservableEvent() clicks$: Observable<any>; ngOnInit() { // we can either manually subscribe // or use the async pipe this.clicks$.subscribe(console.log); } }
  7. Webpack @elmd_ Ahead of Time Compiler for the Browser Common

    Misconception: It’s not only for JavaScript ⚠
  8. Webpack *.bundle.js *.bundle.js main.bundle.js App @elmd_ Ahead of Time Compiler

    for the Browser Common Misconception: It’s not only for JavaScript ⚠
  9. Webpack Loader Loader Loader ngc <register> <register> Plugin Plugin AngularCompilerPlugin

    <emits> Angular Code AngularCompilerPlugin @elmd_ webpack.config.js
  10. Webpack Loader Loader Loader ngc <register> <register> Plugin Plugin AngularCompilerPlugin

    <emits> Angular Code <waits for> AngularCompilerPlugin @elmd_ webpack.config.js
  11. Webpack Loader Loader Loader ngc <register> <register> Plugin Plugin AngularCompilerPlugin

    <emits> Angular Code <waits for> Resolve Dependencies, Bundle AngularCompilerPlugin @elmd_ webpack.config.js
  12. Webpack Loader Loader Loader ngc <register> <register> Plugin Plugin AngularCompilerPlugin

    <emits> Angular Code <waits for> Resolve Dependencies, Bundle Bundle AngularCompilerPlugin @elmd_ webpack.config.js
  13. webpack.config.js Webpack Loader Loader Loader ngc <register> <register> Plugin Plugin

    AngularCompilerPlugin <emits> Generated Angular Code <waits for> Resolve Dependencies, Bundle AngularCompilerPlugin @elmd_ WebpackCompilerHost ts.CompilerHost
  14. webpack.config.js Webpack Loader Loader Loader ngc <register> <register> Plugin Plugin

    AngularCompilerPlugin <emits> Generated Angular Code <waits for> Resolve Dependencies, Bundle AngularCompilerPlugin ngProgram AOT ts.Program JIT @elmd_ WebpackCompilerHost ts.CompilerHost
  15. webpack.config.js Webpack Loader Loader Loader ngc <register> <register> Plugin Plugin

    AngularCompilerPlugin <emits> Generated Angular Code <waits for> Resolve Dependencies, Bundle AngularCompilerPlugin Analysis AOT ngProgram AOT ts.Program JIT @elmd_ WebpackCompilerHost ts.CompilerHost
  16. webpack.config.js Webpack Loader Loader Loader ngc <register> <register> Plugin Plugin

    AngularCompilerPlugin <emits> Generated Angular Code <waits for> Resolve Dependencies, Bundle AngularCompilerPlugin Resolve AOT Analysis AOT ngProgram AOT ts.Program JIT @elmd_ WebpackCompilerHost ts.CompilerHost
  17. webpack.config.js Webpack Loader Loader Loader ngc <register> <register> Plugin Plugin

    AngularCompilerPlugin <emits> Generated Angular Code <waits for> Resolve Dependencies, Bundle AngularCompilerPlugin Resolve AOT Analysis AOT ngProgram AOT ts.Program JIT @elmd_ WebpackCompilerHost ts.CompilerHost Type Checking AOT
  18. webpack.config.js Webpack Loader Loader Loader ngc <register> <register> Plugin Plugin

    AngularCompilerPlugin <emits> Generated Angular Code <waits for> Resolve Dependencies, Bundle AngularCompilerPlugin Resolve AOT Analysis AOT ngProgram AOT ts.Program JIT Transformer @elmd_ WebpackCompilerHost ts.CompilerHost Type Checking AOT Emit
  19. webpack.config.js Webpack Loader Loader Loader ngc <register> <register> Plugin Plugin

    AngularCompilerPlugin <emits> Generated Angular Code <waits for> Resolve Dependencies, Bundle AngularCompilerPlugin Angular Code Resolve AOT Analysis AOT ngProgram AOT ts.Program JIT Transformer @elmd_ WebpackCompilerHost ts.CompilerHost Type Checking AOT Emit
  20. webpack.config.js Webpack Loader Loader Loader ngc <register> <register> Plugin Plugin

    AngularCompilerPlugin <emits> Angular Code <waits for> Resolve Dependencies, Bundle Bundle AngularCompilerPlugin @elmd_
  21. webpack.config.js Webpack Loader Loader Loader ngc <register> <register> Plugin Plugin

    AngularCompilerPlugin <emits> Angular Code <waits for> Resolve Dependencies, Bundle Bundle AngularCompilerPlugin HTML Loader Custom Loader @elmd_
  22. module.exports = { module: { rules: [ { test: /\.html$/,

    use: [ { loader: path.resolve('path/to/custom/html/loader') }, { loader: 'raw-loader' "// import file as a string } ] } ] } }; @elmd_
  23. export default { pre() { "// before build }, post()

    { "// after build }, config(config: webpack.Configuration) { "// after webpack config was built by the CLI } }; @elmd_
  24. export default { pre() { "// before build }, post()

    { "// after build }, config(config: webpack.Configuration) { const acp = findAngularCompilerPlugin(config) as AngularCompilerPlugin; if (!acp) { throw new Error('AngularCompilerPlugin not found'); } const options: AngularCompilerPluginOptions = { ""...acp.options, directTemplateLoading: false "// ""<-- IMPORTANT }; config.plugins = removeCompilerPlugin(config.plugins, acp); const newCompilerPlugin = new AngularCompilerPlugin(options); @elmd_
  25. export default { pre() { "// before build }, post()

    { "// after build }, config(config: webpack.Configuration) { const acp = findAngularCompilerPlugin(config) as AngularCompilerPlugin; if (!acp) { throw new Error('AngularCompilerPlugin not found'); } const options: AngularCompilerPluginOptions = { ""...acp.options, directTemplateLoading: false "// ""<-- IMPORTANT }; config.plugins = removeCompilerPlugin(config.plugins, acp); const newCompilerPlugin = new AngularCompilerPlugin(options); @elmd_
  26. if (!acp) { throw new Error('AngularCompilerPlugin not found'); } const

    options: AngularCompilerPluginOptions = { ""...acp.options, directTemplateLoading: false "// ""<-- IMPORTANT }; config.plugins = removeCompilerPlugin(config.plugins, acp); const newCompilerPlugin = new AngularCompilerPlugin(options); config.plugins.push(newCompilerPlugin); return config; } }; @elmd_
  27. if (!acp) { throw new Error('AngularCompilerPlugin not found'); } const

    options: AngularCompilerPluginOptions = { ""...acp.options, directTemplateLoading: false "// ""<-- IMPORTANT }; config.plugins = removeCompilerPlugin(config.plugins, acp); const newCompilerPlugin = new AngularCompilerPlugin(options); config.plugins.push(newCompilerPlugin); return config; } }; @elmd_
  28. if (!acp) { throw new Error('AngularCompilerPlugin not found'); } const

    options: AngularCompilerPluginOptions = { ""...acp.options, directTemplateLoading: false "// ""<-- IMPORTANT }; config.plugins = removeCompilerPlugin(config.plugins, acp); const newCompilerPlugin = new AngularCompilerPlugin(options); config.plugins.push(newCompilerPlugin); return config; } }; @elmd_
  29. if (!acp) { throw new Error('AngularCompilerPlugin not found'); } const

    options: AngularCompilerPluginOptions = { ""...acp.options, directTemplateLoading: false "// ""<-- IMPORTANT }; config.plugins = removeCompilerPlugin(config.plugins, acp); const newCompilerPlugin = new AngularCompilerPlugin(options); config.plugins.push(newCompilerPlugin); return config; } }; @elmd_
  30. if (!acp) { throw new Error('AngularCompilerPlugin not found'); } const

    options: AngularCompilerPluginOptions = { ""...acp.options, directTemplateLoading: false "// ""<-- IMPORTANT }; config.plugins = removeCompilerPlugin(config.plugins, acp); const newCompilerPlugin = new AngularCompilerPlugin(options); config.plugins.push(newCompilerPlugin); return config; } }; @elmd_
  31. webpack.config.js Webpack Loader Loader Loader ngc <register> <register> Plugin Plugin

    AngularCompilerPlugin <emits> Generated Angular Code <waits for> Resolve Dependencies, Bundle AngularCompilerPlugin Angular Code Resolve AOT Analysis AOT ngProgram AOT ts.Program JIT Transformer @elmd_ WebpackCompilerHost ts.CompilerHost Type Checking AOT Emit
  32. webpack.config.js Webpack Loader Loader Loader ngc <register> <register> Plugin Plugin

    AngularCompilerPlugin <emits> Generated Angular Code <waits for> Resolve Dependencies, Bundle AngularCompilerPlugin Angular Code Resolve AOT Analysis AOT ngProgram AOT ts.Program JIT Transformer @elmd_ WebpackCompilerHost ts.CompilerHost Type Checking AOT Emit Transformer Custom TS Transformer
  33. let a = 1; SourceFile VariableStatement VariableDeclaration VariableDeclarationList text =

    “a” Identifier @elmd_ NumericalLiteral text = “1”
  34. let a = 1; SourceFile VariableStatement VariableDeclarationList NumericalLiteral text =

    “1” @elmd_ text = “a” Identifier VariableDeclaration
  35. let a = 1; SourceFile VariableStatement VariableDeclarationList NumericalLiteral text =

    “1” @elmd_ text = “a” Identifier VariableDeclaration
  36. let a = 1; SourceFile VariableStatement VariableDeclarationList NumericalLiteral text =

    “1” @elmd_ text = “a” Identifier VariableDeclaration
  37. export function transformer(context: ts.TransformationContext) { return (sourceFile: ts.SourceFile) "=> {

    /** * Todos: * 1. Find `template` property within @Component decorator * 2. Apply HTML transformation * 3. Update AST node * 4. Return updated SourceFile "*/ }; } @elmd_
  38. export function transformer(context: ts.TransformationContext) { return (sourceFile: ts.SourceFile) "=> {

    const template = findInlineTemplate(sourceFile); return updateInlineTemplate(template, sourceFile, context); }; } @elmd_
  39. export function transformer(context: ts.TransformationContext) { return (sourceFile: ts.SourceFile) "=> {

    const template = findInlineTemplate(sourceFile); return updateInlineTemplate(template, sourceFile, context); }; } function findInlineTemplate(sourceFile: ts.SourceFile) { return sourceFile.statements[1].decorators[0].expression .arguments[0].properties[0].initializer.text; } @elmd_
  40. export function transformer(context: ts.TransformationContext) { return (sourceFile: ts.SourceFile) "=> {

    const template = findInlineTemplate(sourceFile); return updateInlineTemplate(template, sourceFile, context); }; } function findInlineTemplate(sourceFile: ts.SourceFile) { return sourceFile.statements[1].decorators[0].expression .arguments[0].properties[0].initializer.text; } @elmd_
  41. @elmd_ export function transformer(context: ts.TransformationContext) { return (sourceFile: ts.SourceFile) "=>

    { const template = findInlineTemplate(sourceFile); return updateInlineTemplate(template, sourceFile, context); }; } function findInlineTemplate(sourceFile: ts.SourceFile) { return tsquery.query(sourceFile, INLINE_TEMPLATE_QUERY); }
  42. @elmd_ export function transformer(context: ts.TransformationContext) { return (sourceFile: ts.SourceFile) "=>

    { const template = findInlineTemplate(sourceFile); return updateInlineTemplate(template, sourceFile, context); }; }
  43. @elmd_ export function transformer(context: ts.TransformationContext) { return (sourceFile: ts.SourceFile) "=>

    { const template = findInlineTemplate(sourceFile); return updateInlineTemplate(template, sourceFile, context); }; } function updateInlineTemplate( nodes: Array<ts.Node>, sourceFile: ts.SourceFile, context: ts.TransformationContext) { const visitor: ts.Visitor = (node: ts.Node) "=> { "// ast walker function return ts.visitEachChild(node, visitor, context); }; return ts.visitNode(sourceFile, visitor); }
  44. @elmd_ export function transformer(context: ts.TransformationContext) { return (sourceFile: ts.SourceFile) "=>

    { const template = findInlineTemplate(sourceFile); return updateInlineTemplate(template, sourceFile, context); }; } function updateInlineTemplate( nodes: Array<ts.Node>, sourceFile: ts.SourceFile, context: ts.TransformationContext) { const visitor: ts.Visitor = (node: ts.Node) "=> { "// ast walker function return ts.visitEachChild(node, visitor, context); }; return ts.visitNode(sourceFile, visitor); }
  45. @elmd_ export function transformer(context: ts.TransformationContext) { return (sourceFile: ts.SourceFile) "=>

    { const template = findInlineTemplate(sourceFile); return updateInlineTemplate(template, sourceFile, context); }; } function updateInlineTemplate( nodes: Array<ts.Node>, sourceFile: ts.SourceFile, context: ts.TransformationContext) { const visitor: ts.Visitor = (node: ts.Node) "=> { "// ast walker function return ts.visitEachChild(node, visitor, context); }; return ts.visitNode(sourceFile, visitor); }
  46. @elmd_ export function transformer(context: ts.TransformationContext) { return (sourceFile: ts.SourceFile) "=>

    { const template = findInlineTemplate(sourceFile); return updateInlineTemplate(template, sourceFile, context); }; } function updateInlineTemplate( nodes: Array<ts.Node>, sourceFile: ts.SourceFile, context: ts.TransformationContext) { const visitor: ts.Visitor = (node: ts.Node) "=> { "// ast walker function return ts.visitEachChild(node, visitor, context); }; return ts.visitNode(sourceFile, visitor); }
  47. @elmd_ function updateInlineTemplate( nodes: Array<ts.Node>, sourceFile: ts.SourceFile, context: ts.TransformationContext) {

    const visitor: ts.Visitor = (node: ts.Node) "=> { "// ast walker function return ts.visitEachChild(node, visitor, context); }; return ts.visitNode(sourceFile, visitor); }
  48. function updateInlineTemplate( nodes: Array<ts.Node>, sourceFile: ts.SourceFile, context: ts.TransformationContext) { const

    visitor: ts.Visitor = (node: ts.Node) "=> { if (nodes.includes(node)) { const prop = node as ts.PropertyAssignment; const currentTpl = prop.initializer.getFullText(); const inlineTpl = ts.createNoSubstitutionTemplateLiteral( updateDirectives(currentTpl, true) ); return ts.updatePropertyAssignment(prop, prop.name, inlineTpl); } return ts.visitEachChild(node, visitor, context); }; return ts.visitNode(sourceFile, visitor); @elmd_
  49. if (!acp) { throw new Error('AngularCompilerPlugin not found'); } const

    options: AngularCompilerPluginOptions = { ""...acp.options, directTemplateLoading: false "// ""<-- IMPORTANT }; config.plugins = removeCompilerPlugin(config.plugins, acp); const newCompilerPlugin = new AngularCompilerPlugin(options); config.plugins.push(newCompilerPlugin); return config; } }; @elmd_
  50. if (!acp) { throw new Error('AngularCompilerPlugin not found'); } const

    options: AngularCompilerPluginOptions = { ""...acp.options, directTemplateLoading: false "// ""<-- IMPORTANT }; config.plugins = removeCompilerPlugin(config.plugins, acp); const newCompilerPlugin = new AngularCompilerPlugin(options); acp._transformers = [inlineTplTransformer, ""...acp._transformers]; config.plugins.push(newCompilerPlugin); return config; } }; @elmd_
  51. Key Takeaways @elmd_ TypeScript Transformers can be used to transform

    our own source code The Angular Compiler doesn’t need to be scary
  52. Key Takeaways @elmd_ TypeScript Transformers can be used to transform

    our own source code ASTs are powerful and the foundation of many tools The Angular Compiler doesn’t need to be scary
  53. Key Takeaways @elmd_ TypeScript Transformers can be used to transform

    our own source code ASTs are powerful and the foundation of many tools Tooling and code automation can be so much fun and save lots of time The Angular Compiler doesn’t need to be scary