├── .editorconfig ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json └── tasks.json ├── README.md ├── angular.json ├── package-lock.json ├── package.json ├── public └── favicon.ico ├── src ├── app │ ├── app.component.ts │ ├── app.config.ts │ ├── compile.ts │ ├── prettier.ts │ ├── printer.ts │ ├── templates │ │ ├── async-pipe.ts │ │ ├── banana.ts │ │ ├── child-component.ts │ │ ├── default.ts │ │ ├── event-bindings.ts │ │ ├── for-loops.ts │ │ ├── if-else-reference.ts │ │ ├── if-then-else.ts │ │ ├── index.ts │ │ ├── nested-nodes.ts │ │ ├── ng-model.ts │ │ ├── simple-at-let.ts │ │ └── template-literal.ts │ └── zip.ts ├── index.html ├── main.ts ├── styles.scss └── styles │ ├── _button.scss │ └── _colors.scss ├── tsconfig.app.json ├── tsconfig.json └── tsconfig.spec.json /.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 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | on: push 2 | name: 🚀 Deploy website on push 3 | jobs: 4 | web-deploy: 5 | name: 🎉 Deploy 6 | runs-on: ubuntu-latest 7 | if: github.ref == 'refs/heads/main' 8 | steps: 9 | - name: Setup Node.js for use with actions 10 | uses: actions/setup-node@v4 11 | with: 12 | node-version: 20 13 | 14 | - name: 🚚 Get latest code 15 | uses: actions/checkout@v4 16 | 17 | - name: Clean install dependencies 18 | run: npm ci --force 19 | 20 | - name: Build app 21 | run: npm run build 22 | 23 | - name: Rewrite base href 24 | uses: SteveSandersonMS/ghaction-rewrite-base-href@v1 25 | with: 26 | html_path: ./dist/angular-compiler-output/browser/index.html 27 | base_href: /angular-compiler-output/ 28 | 29 | - name: deploy 30 | uses: peaceiris/actions-gh-pages@v4 31 | with: 32 | github_token: ${{ secrets.GITHUB_TOKEN }} 33 | publish_dir: ./dist/angular-compiler-output/browser 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # Compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /bazel-out 8 | 9 | # Node 10 | /node_modules 11 | npm-debug.log 12 | yarn-error.log 13 | 14 | # IDEs and editors 15 | .idea/ 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # Visual Studio Code 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | .history/* 30 | 31 | # Miscellaneous 32 | /.angular/cache 33 | .sass-cache/ 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | testem.log 38 | /typings 39 | 40 | # System files 41 | .DS_Store 42 | Thumbs.db 43 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 3 | "recommendations": ["angular.ng-template"] 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "name": "ng serve", 7 | "type": "chrome", 8 | "request": "launch", 9 | "preLaunchTask": "npm: start", 10 | "url": "http://localhost:4200/" 11 | }, 12 | { 13 | "name": "ng test", 14 | "type": "chrome", 15 | "request": "launch", 16 | "preLaunchTask": "npm: test", 17 | "url": "http://localhost:9876/debug.html" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 3 | "version": "2.0.0", 4 | "tasks": [ 5 | { 6 | "type": "npm", 7 | "script": "start", 8 | "isBackground": true, 9 | "problemMatcher": { 10 | "owner": "typescript", 11 | "pattern": "$tsc", 12 | "background": { 13 | "activeOnStart": true, 14 | "beginsPattern": { 15 | "regexp": "(.*?)" 16 | }, 17 | "endsPattern": { 18 | "regexp": "bundle generation complete" 19 | } 20 | } 21 | } 22 | }, 23 | { 24 | "type": "npm", 25 | "script": "test", 26 | "isBackground": true, 27 | "problemMatcher": { 28 | "owner": "typescript", 29 | "pattern": "$tsc", 30 | "background": { 31 | "activeOnStart": true, 32 | "beginsPattern": { 33 | "regexp": "(.*?)" 34 | }, 35 | "endsPattern": { 36 | "regexp": "bundle generation complete" 37 | } 38 | } 39 | } 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular Compiler Output 2 | 3 | This demo aims to teach the basics of the Angular template compiler by showing the JS output for a given angular template. 4 | 5 | [Go the the demo](https://jeanmeche.github.io/angular-compiler-output/) 6 | 7 | ## Dependencies 8 | 9 | * Angular 10 | * [Prettier](https://prettier.io/) for the prettifying 11 | * [shiki](https://github.com/shikijs/shiki) for highlighting 12 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "angular-compiler-output": { 7 | "projectType": "application", 8 | "schematics": {}, 9 | "root": "", 10 | "sourceRoot": "src", 11 | "prefix": "app", 12 | "architect": { 13 | "build": { 14 | "builder": "@angular/build:application", 15 | "options": { 16 | "outputPath": "dist/angular-compiler-output", 17 | "index": "src/index.html", 18 | "browser": "src/main.ts", 19 | "tsConfig": "tsconfig.app.json", 20 | "assets": [ 21 | { 22 | "glob": "**/*", 23 | "input": "public" 24 | } 25 | ], 26 | "styles": [ 27 | "@angular/material/prebuilt-themes/azure-blue.css", 28 | "src/styles.scss" 29 | ], 30 | "scripts": [] 31 | }, 32 | "configurations": { 33 | "production": { 34 | "outputHashing": "all" 35 | }, 36 | "development": { 37 | "optimization": false, 38 | "extractLicenses": false, 39 | "sourceMap": true 40 | } 41 | }, 42 | "defaultConfiguration": "production" 43 | }, 44 | "serve": { 45 | "builder": "@angular/build:dev-server", 46 | "configurations": { 47 | "production": { 48 | "buildTarget": "angular-compiler-output:build:production" 49 | }, 50 | "development": { 51 | "buildTarget": "angular-compiler-output:build:development" 52 | } 53 | }, 54 | "defaultConfiguration": "development" 55 | }, 56 | "extract-i18n": { 57 | "builder": "@angular/build:extract-i18n", 58 | "options": { 59 | "buildTarget": "angular-compiler-output:build" 60 | } 61 | }, 62 | "test": { 63 | "builder": "@angular/build:karma", 64 | "options": { 65 | "polyfills": [ 66 | "zone.js", 67 | "zone.js/testing" 68 | ], 69 | "tsConfig": "tsconfig.spec.json", 70 | "assets": [ 71 | { 72 | "glob": "**/*", 73 | "input": "public" 74 | } 75 | ], 76 | "styles": [ 77 | "@angular/material/prebuilt-themes/azure-blue.css", 78 | "src/styles.css" 79 | ], 80 | "scripts": [] 81 | } 82 | } 83 | } 84 | } 85 | }, 86 | "cli": { 87 | "analytics": "6a505997-9ce1-42b3-9d81-62a5be67a151" 88 | }, 89 | "schematics": { 90 | "@schematics/angular:component": { 91 | "type": "component" 92 | }, 93 | "@schematics/angular:directive": { 94 | "type": "directive" 95 | }, 96 | "@schematics/angular:service": { 97 | "type": "service" 98 | }, 99 | "@schematics/angular:guard": { 100 | "typeSeparator": "." 101 | }, 102 | "@schematics/angular:interceptor": { 103 | "typeSeparator": "." 104 | }, 105 | "@schematics/angular:module": { 106 | "typeSeparator": "." 107 | }, 108 | "@schematics/angular:pipe": { 109 | "typeSeparator": "." 110 | }, 111 | "@schematics/angular:resolver": { 112 | "typeSeparator": "." 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-compiler-output", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "watch": "ng build --watch --configuration development", 9 | "test": "ng test" 10 | }, 11 | "private": true, 12 | "dependencies": { 13 | "@angular/cdk": "^20.0.0", 14 | "@angular/common": "^20.0.0", 15 | "@angular/compiler": "^20.0.0", 16 | "@angular/core": "^20.0.0", 17 | "@angular/forms": "^20.0.0", 18 | "@angular/material": "^20.0.0", 19 | "@angular/platform-browser": "^20.0.0", 20 | "@angular/router": "^20.0.0", 21 | "nxt-json-view": "^19.0.0", 22 | "prettier": "^3.2.5", 23 | "rxjs": "~7.8.0", 24 | "shiki": "^1.2.0", 25 | "tslib": "^2.3.0", 26 | "zone.js": "~0.15.0" 27 | }, 28 | "devDependencies": { 29 | "@angular/build": "^20.0.0", 30 | "@angular/cli": "^20.0.0", 31 | "@angular/compiler-cli": "^20.0.0", 32 | "@types/jasmine": "~5.1.0", 33 | "@types/prettier": "^3.0.0", 34 | "jasmine-core": "~5.1.0", 35 | "karma": "~6.4.0", 36 | "karma-chrome-launcher": "~3.2.0", 37 | "karma-coverage": "~2.2.0", 38 | "karma-jasmine": "~5.1.0", 39 | "karma-jasmine-html-reporter": "~2.1.0", 40 | "typescript": "~5.8.3" 41 | } 42 | } -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JeanMeche/angular-compiler-output/1f694263f7ef02dc40e06cbb8ac6455be51f61fb/public/favicon.ico -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { CdkMenuModule } from '@angular/cdk/menu'; 2 | import { 3 | Component, 4 | HostListener, 5 | inject, 6 | resource, 7 | signal, 8 | VERSION, 9 | } from '@angular/core'; 10 | import { FormsModule } from '@angular/forms'; 11 | import { ActivatedRoute, Router } from '@angular/router'; 12 | import { MatIcon } from '@angular/material/icon'; 13 | 14 | import { compileFormatAndHighlight } from './compile'; 15 | import { formatAngularTemplate } from './prettier'; 16 | import { Template, templates } from './templates'; 17 | import { unzip, zip } from './zip'; 18 | import { DomSanitizer } from '@angular/platform-browser'; 19 | 20 | // Please don't blame for what you're gonna read 21 | 22 | @Component({ 23 | selector: 'app-root', 24 | standalone: true, 25 | imports: [FormsModule, CdkMenuModule, MatIcon], 26 | template: ` 27 |
Angular Template Compiler - based on {{ version }}
28 |
29 |
30 |
31 |

Template

32 | 33 |
34 |
35 | 36 | 40 |
41 |
42 | 43 | 44 |
    45 | @for (template of templates; track $index) { 46 |
  • 47 | 54 |
  • 55 | } 56 |
57 |
58 | 59 | 66 | 73 |
74 | 80 |
81 |
82 |

The compiled template

83 | @if (errors().length > 0) { 84 |
85 | @for (error of errors(); track error) { 86 |
L{{ error.line }}: {{ error.message }}
87 | } 88 |
89 | } 90 |
91 | renderFlag: 1=create, 2=update 92 | 93 |
94 | 139 |
140 | `, 141 | }) 142 | export class AppComponent { 143 | protected readonly version = VERSION.full; 144 | protected readonly router = inject(Router); 145 | protected readonly activatedRoute = inject(ActivatedRoute); 146 | protected readonly sanitzer = inject(DomSanitizer); 147 | 148 | protected readonly templates = templates; 149 | protected readonly template = signal(templates[0].content); 150 | protected readonly errors = signal<{ message: string; line: number }[]>([]); 151 | protected readonly currentTemplate = signal(templates[0].label); 152 | 153 | protected readonly compiledTemplate = resource({ 154 | params: this.template, 155 | loader: async ({ params: template }) => 156 | await this.compileTemplate(template), 157 | }); 158 | 159 | constructor() { 160 | this.activatedRoute.queryParams.subscribe((params) => { 161 | if (params['template']) { 162 | this.selectCustom(); 163 | this.template.set(unzip(params['template'])); 164 | } 165 | }); 166 | } 167 | 168 | selectTemplate(template: Template): void { 169 | this.template.set(template.content); 170 | this.currentTemplate.set(template.label); 171 | } 172 | 173 | selectCustom() { 174 | this.currentTemplate.set('Custom'); 175 | } 176 | 177 | async compileTemplate(template: string) { 178 | const { output, errors } = await compileFormatAndHighlight(template); 179 | this.errors.set( 180 | errors?.map((e) => { 181 | return { message: e.msg, line: e.span.start.line }; 182 | }) ?? [], 183 | ); 184 | 185 | // the output contains inline styles, so we need to trust it 186 | return { output: this.sanitzer.bypassSecurityTrustHtml(output), errors }; 187 | } 188 | 189 | async pretty() { 190 | const newTemplateStr = await formatAngularTemplate(this.template()); 191 | this.template.set(newTemplateStr); 192 | } 193 | 194 | save() { 195 | this.router.navigate([], { 196 | queryParams: { template: zip(this.template()) }, 197 | }); 198 | } 199 | 200 | @HostListener('document:keydown', ['$event']) 201 | handleKeyboardEvent(event: KeyboardEvent) { 202 | if (event.key === 's' && (event.metaKey || event.ctrlKey)) { 203 | event.preventDefault(); 204 | this.save(); 205 | } 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /src/app/app.config.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ApplicationConfig, 3 | inject, 4 | provideZonelessChangeDetection, 5 | } from '@angular/core'; 6 | import { provideRouter, withComponentInputBinding } from '@angular/router'; 7 | import { initHightlighter } from './compile'; 8 | import { MatIconRegistry } from '@angular/material/icon'; 9 | import { provideAppInitializer } from '@angular/core'; 10 | import { provideEnvironmentInitializer } from '@angular/core'; 11 | 12 | export const appConfig: ApplicationConfig = { 13 | providers: [ 14 | provideZonelessChangeDetection(), 15 | provideRouter([],withComponentInputBinding()), 16 | provideAppInitializer(async () => await initHightlighter()), 17 | provideEnvironmentInitializer(() => { 18 | const matIconReg = inject(MatIconRegistry); 19 | matIconReg.setDefaultFontSetClass('material-symbols-outlined'); 20 | }), 21 | ], 22 | }; 23 | -------------------------------------------------------------------------------- /src/app/compile.ts: -------------------------------------------------------------------------------- 1 | import * as ng from '@angular/compiler'; 2 | import { HighlighterGeneric, createHighlighter } from 'shiki'; 3 | 4 | import { formatJs } from './prettier'; 5 | import { Context, Printer } from './printer'; 6 | 7 | let highlighter: HighlighterGeneric; 8 | 9 | export async function initHightlighter() { 10 | highlighter = await createHighlighter({ 11 | themes: ['github-dark'], 12 | langs: ['javascript'], 13 | }); 14 | } 15 | 16 | interface CompileOutput { 17 | output: string; 18 | errors: ng.ParseError[] | null; 19 | } 20 | 21 | export function compileTemplate(templateStr: string): CompileOutput { 22 | const constantPool = new ng.ConstantPool(); 23 | const template = ng.parseTemplate(templateStr, 'template.html', { 24 | preserveWhitespaces: false, 25 | }); 26 | 27 | const CMP_NAME = 'TestCmp'; 28 | 29 | const out = ng.compileComponentFromMetadata( 30 | { 31 | name: CMP_NAME, 32 | isStandalone: true, 33 | selector: 'test-cmp', 34 | host: { 35 | attributes: {}, 36 | listeners: {}, 37 | properties: {}, 38 | specialAttributes: {}, 39 | }, 40 | inputs: {}, 41 | outputs: {}, 42 | lifecycle: { 43 | usesOnChanges: false, 44 | }, 45 | hostDirectives: null, 46 | declarations: [], 47 | declarationListEmitMode: ng.DeclarationListEmitMode.Direct, 48 | deps: [], 49 | animations: null, 50 | defer: { 51 | dependenciesFn: null, 52 | mode: ng.DeferBlockDepsEmitMode.PerComponent, 53 | }, 54 | i18nUseExternalIds: false, 55 | interpolation: ng.DEFAULT_INTERPOLATION_CONFIG, 56 | isSignal: false, 57 | providers: null, 58 | queries: [], 59 | styles: [], 60 | template, 61 | encapsulation: ng.ViewEncapsulation.Emulated, 62 | exportAs: null, 63 | fullInheritance: false, 64 | changeDetection: null, 65 | relativeContextFilePath: 'template.html', 66 | type: { 67 | value: new ng.WrappedNodeExpr(CMP_NAME), 68 | type: new ng.WrappedNodeExpr(CMP_NAME), 69 | }, 70 | typeArgumentCount: 0, 71 | typeSourceSpan: null!, 72 | usesInheritance: false, 73 | viewProviders: null, 74 | viewQueries: [], 75 | relativeTemplatePath: '', 76 | }, 77 | constantPool, 78 | ng.makeBindingParser(ng.DEFAULT_INTERPOLATION_CONFIG), 79 | ); 80 | 81 | const printer = new Printer(); 82 | let strExpression = out.expression.visitExpression( 83 | printer, 84 | new Context(false), 85 | ); 86 | 87 | for (const stmt of constantPool.statements) { 88 | const strStmt = stmt.visitStatement(printer, new Context(true)); 89 | 90 | strExpression += `\n\n${strStmt}`; 91 | } 92 | 93 | return { output: strExpression, errors: template.errors }; 94 | } 95 | 96 | export async function compileFormatAndHighlight( 97 | template: string, 98 | ): Promise { 99 | const { output: unformated, errors } = compileTemplate(template); 100 | 101 | const formatted = await formatJs(unformated); 102 | const highlighted = highlighter.codeToHtml(formatted, { 103 | lang: 'javascript', 104 | theme: 'github-dark', 105 | }); 106 | 107 | return { output: highlighted, errors }; 108 | } 109 | -------------------------------------------------------------------------------- /src/app/prettier.ts: -------------------------------------------------------------------------------- 1 | import parserBabel from 'prettier/plugins/babel'; 2 | import prettierPluginEstree from 'prettier/plugins/estree'; 3 | import parserHtml from 'prettier/plugins/html'; 4 | import prettier from 'prettier/standalone'; 5 | 6 | 7 | export async function formatJs(code: string): Promise { 8 | return prettier.format(code, { 9 | parser: 'babel', 10 | plugins: [parserBabel, prettierPluginEstree], 11 | singleQuote: true, 12 | }); 13 | } 14 | 15 | export async function formatAngularTemplate(template: string): Promise { 16 | return prettier.format(template, { 17 | parser: 'angular', 18 | plugins: [parserHtml], 19 | }); 20 | } -------------------------------------------------------------------------------- /src/app/printer.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 | 9 | // A Huge thanks to Alex Rickabaugh for writing this 🙏. 10 | 11 | import * as ng from '@angular/compiler'; 12 | 13 | const UNARY_OPERATORS = new Map([ 14 | [ng.UnaryOperator.Minus, '-'], 15 | [ng.UnaryOperator.Plus, '+'], 16 | ]); 17 | 18 | const BINARY_OPERATORS = new Map([ 19 | [ng.BinaryOperator.And, '&&'], 20 | [ng.BinaryOperator.Bigger, '>'], 21 | [ng.BinaryOperator.BiggerEquals, '>='], 22 | [ng.BinaryOperator.BitwiseAnd, '&'], 23 | [ng.BinaryOperator.BitwiseOr, '|'], 24 | [ng.BinaryOperator.Divide, '/'], 25 | [ng.BinaryOperator.Equals, '=='], 26 | [ng.BinaryOperator.Identical, '==='], 27 | [ng.BinaryOperator.Lower, '<'], 28 | [ng.BinaryOperator.LowerEquals, '<='], 29 | [ng.BinaryOperator.Minus, '-'], 30 | [ng.BinaryOperator.Modulo, '%'], 31 | [ng.BinaryOperator.Multiply, '*'], 32 | [ng.BinaryOperator.NotEquals, '!='], 33 | [ng.BinaryOperator.NotIdentical, '!=='], 34 | [ng.BinaryOperator.Or, '||'], 35 | [ng.BinaryOperator.Plus, '+'], 36 | [ng.BinaryOperator.NullishCoalesce, '??'], 37 | ]); 38 | 39 | export class Context { 40 | constructor(readonly isStatement: boolean) {} 41 | 42 | get withExpressionMode(): Context { 43 | return this.isStatement ? new Context(false) : this; 44 | } 45 | 46 | get withStatementMode(): Context { 47 | return !this.isStatement ? new Context(true) : this; 48 | } 49 | } 50 | 51 | export class Printer implements ng.ExpressionVisitor, ng.StatementVisitor { 52 | visitDeclareVarStmt(stmt: ng.DeclareVarStmt, context: Context): string { 53 | let varStmt = stmt.hasModifier(ng.StmtModifier.Final) ? 'const' : 'let'; 54 | varStmt += ' ' + stmt.name; 55 | if (stmt.value) { 56 | varStmt += 57 | ' = ' + stmt.value.visitExpression(this, context.withExpressionMode); 58 | } 59 | return this.attachComments(varStmt, stmt.leadingComments); 60 | } 61 | 62 | visitDeclareFunctionStmt( 63 | stmt: ng.DeclareFunctionStmt, 64 | context: Context, 65 | ): string { 66 | let fn = `function ${stmt.name}(${stmt.params.map((p) => p.name).join(', ')}) {`; 67 | fn += this.visitStatements(stmt.statements, context.withStatementMode); 68 | fn += '}'; 69 | return this.attachComments(fn, stmt.leadingComments); 70 | } 71 | 72 | visitExpressionStmt(stmt: ng.ExpressionStatement, context: Context): string { 73 | return this.attachComments( 74 | stmt.expr.visitExpression(this, context.withStatementMode) + ';', 75 | stmt.leadingComments, 76 | ); 77 | } 78 | 79 | visitReturnStmt(stmt: ng.ReturnStatement, context: Context): string { 80 | return this.attachComments( 81 | 'return ' + 82 | stmt.value.visitExpression(this, context.withExpressionMode) + 83 | ';', 84 | stmt.leadingComments, 85 | ); 86 | } 87 | 88 | visitIfStmt(stmt: ng.IfStmt, context: Context): string { 89 | let ifStmt = 'if ('; 90 | ifStmt += stmt.condition.visitExpression(this, context); 91 | ifStmt += 92 | ') {' + 93 | this.visitStatements(stmt.trueCase, context.withStatementMode) + 94 | '}'; 95 | if (stmt.falseCase.length > 0) { 96 | ifStmt += ' else {'; 97 | ifStmt += this.visitStatements(stmt.falseCase, context.withStatementMode); 98 | ifStmt += '}'; 99 | } 100 | return this.attachComments(ifStmt, stmt.leadingComments); 101 | } 102 | 103 | visitReadVarExpr(ast: ng.ReadVarExpr, _context: Context): string { 104 | return ast.name; 105 | } 106 | 107 | visitWriteVarExpr(expr: ng.WriteVarExpr, context: Context): string { 108 | const assignment = `${expr.name} = ${expr.value.visitExpression(this, context)}`; 109 | return context.isStatement ? assignment : `(${assignment})`; 110 | } 111 | 112 | visitWriteKeyExpr(expr: ng.WriteKeyExpr, context: Context): string { 113 | const exprContext = context.withExpressionMode; 114 | const receiver = expr.receiver.visitExpression(this, exprContext); 115 | const key = expr.index.visitExpression(this, exprContext); 116 | const value = expr.value.visitExpression(this, exprContext); 117 | const assignment = `${receiver}[${key}] = ${value}`; 118 | return context.isStatement ? assignment : `(${assignment})`; 119 | } 120 | 121 | visitWritePropExpr(expr: ng.WritePropExpr, context: Context): string { 122 | const receiver = expr.receiver.visitExpression(this, context); 123 | const value = expr.value.visitExpression(this, context); 124 | return `${receiver}.${expr.name} = ${value}`; 125 | } 126 | 127 | visitInvokeFunctionExpr( 128 | ast: ng.InvokeFunctionExpr, 129 | context: Context, 130 | ): string { 131 | const fn = ast.fn.visitExpression(this, context); 132 | const args = ast.args.map((arg) => arg.visitExpression(this, context)); 133 | return this.setSourceMapRange( 134 | // TODO: purity (ast.pure) 135 | `${fn}(${args.join(', ')})`, 136 | ast.sourceSpan, 137 | ); 138 | } 139 | 140 | visitTaggedTemplateExpr( 141 | ast: ng.TaggedTemplateLiteralExpr, 142 | context: Context, 143 | ): string { 144 | throw new Error('only important for i18n'); 145 | // return this.setSourceMapRange( 146 | // this.createTaggedTemplateExpression( 147 | // ast.tag.visitExpression(this, context), 148 | // { 149 | // elements: ast.template.elements.map((e) => 150 | // createTemplateElement({ 151 | // cooked: e.text, 152 | // raw: e.rawText, 153 | // range: e.sourceSpan ?? ast.sourceSpan, 154 | // }) 155 | // ), 156 | // expressions: ast.template.expressions.map((e) => 157 | // e.visitExpression(this, context) 158 | // ), 159 | // } 160 | // ), 161 | // ast.sourceSpan 162 | // ); 163 | } 164 | 165 | visitInstantiateExpr(ast: ng.InstantiateExpr, context: Context): string { 166 | const ctor = ast.classExpr.visitExpression(this, context); 167 | const args = ast.args.map((arg) => arg.visitExpression(this, context)); 168 | return `new ${ctor}(${args.join(', ')})`; 169 | } 170 | 171 | visitLiteralExpr(ast: ng.LiteralExpr, _context: Context): string { 172 | let value: string; 173 | if (typeof ast.value === 'string') { 174 | value = `'${ast.value.replaceAll(`'`, `\\'`).replaceAll('\n', '\\n')}'`; 175 | } else if (ast.value === undefined) { 176 | value = 'undefined'; 177 | } else if (ast.value === null) { 178 | value = 'null'; 179 | } else { 180 | value = ast.value.toString(); 181 | } 182 | return this.setSourceMapRange(value, ast.sourceSpan); 183 | } 184 | 185 | visitLocalizedString(ast: ng.LocalizedString, context: Context): string { 186 | throw new Error('only important for i18n'); 187 | // A `$localize` message consists of `messageParts` and `expressions`, which 188 | // get interleaved together. The interleaved pieces look like: 189 | // `[messagePart0, expression0, messagePart1, expression1, messagePart2]` 190 | // 191 | // Note that there is always a message part at the start and end, and so 192 | // therefore `messageParts.length === expressions.length + 1`. 193 | // 194 | // Each message part may be prefixed with "metadata", which is wrapped in 195 | // colons (:) delimiters. The metadata is attached to the first and 196 | // subsequent message parts by calls to `serializeI18nHead()` and 197 | // `serializeI18nTemplatePart()` respectively. 198 | // 199 | // The first message part (i.e. `ast.messageParts[0]`) is used to initialize 200 | // `messageParts` array. const elements: TemplateElement[] = [ 201 | // createTemplateElement(ast.serializeI18nHead()), 202 | // ]; 203 | // const expressions: string[] = []; 204 | // for (let i = 0; i < ast.expressions.length; i++) { 205 | // const placeholder = this.setSourceMapRange( 206 | // ast.expressions[i].visitExpression(this, context), 207 | // ast.getPlaceholderSourceSpan(i) 208 | // ); 209 | // expressions.push(placeholder); 210 | // elements.push( 211 | // createTemplateElement(ast.serializeI18nTemplatePart(i + 1)) 212 | // ); 213 | // } 214 | 215 | // const localizeTag = this.factory.createIdentifier('$localize'); 216 | // return this.setSourceMapRange( 217 | // this.createTaggedTemplateExpression(localizeTag, { 218 | // elements, 219 | // expressions, 220 | // }), 221 | // ast.sourceSpan 222 | // ); 223 | } 224 | 225 | // private createTaggedTemplateExpression( 226 | // tag: string, 227 | // template: TemplateLiteral 228 | // ): string { 229 | // return this.downlevelTaggedTemplates 230 | // ? this.createES5TaggedTemplateFunctionCall(tag, template) 231 | // : this.factory.createTaggedTemplate(tag, template); 232 | // } 233 | 234 | // /** 235 | // * Translate the tagged template literal into a call that is compatible 236 | // with ES5, using the 237 | // * imported `__makeTemplateObject` helper for ES5 formatted output. 238 | // */ 239 | // private createES5TaggedTemplateFunctionCall( 240 | // tagHandler: string, 241 | // { elements, expressions }: TemplateLiteral 242 | // ): string { 243 | // // Ensure that the `__makeTemplateObject()` helper has been imported. 244 | // const { moduleImport, symbol } = this.imports.generateNamedImport( 245 | // 'tslib', 246 | // '__makeTemplateObject' 247 | // ); 248 | // const __makeTemplateObjectHelper = 249 | // moduleImport === null 250 | // ? this.factory.createIdentifier(symbol) 251 | // : this.factory.createPropertyAccess(moduleImport, symbol); 252 | 253 | // // Collect up the cooked and raw strings into two separate arrays. 254 | // const cooked: string[] = []; 255 | // const raw: string[] = []; 256 | // for (const element of elements) { 257 | // cooked.push( 258 | // this.factory.setSourceMapRange( 259 | // this.factory.createLiteral(element.cooked), 260 | // element.range 261 | // ) 262 | // ); 263 | // raw.push( 264 | // this.factory.setSourceMapRange( 265 | // this.factory.createLiteral(element.raw), 266 | // element.range 267 | // ) 268 | // ); 269 | // } 270 | 271 | // // Generate the helper call in the form: `__makeTemplateObject([cooked], 272 | // [raw]);` const templateHelperCall = this.factory.createCallExpression( 273 | // __makeTemplateObjectHelper, 274 | // [ 275 | // this.factory.createArrayLiteral(cooked), 276 | // this.factory.createArrayLiteral(raw), 277 | // ], 278 | // /* pure */ false 279 | // ); 280 | 281 | // // Finally create the tagged handler call in the form: 282 | // // `tag(__makeTemplateObject([cooked], [raw]), ...expressions);` 283 | // return this.factory.createCallExpression( 284 | // tagHandler, 285 | // [templateHelperCall, ...expressions], 286 | // /* pure */ false 287 | // ); 288 | // } 289 | 290 | visitExternalExpr(ast: ng.ExternalExpr, _context: Context): string { 291 | if (ast.value.name === null) { 292 | if (ast.value.moduleName === null) { 293 | throw new Error('Invalid import without name nor moduleName'); 294 | } 295 | return 'ng'; 296 | } 297 | // If a moduleName is specified, this is a normal import. If there's no 298 | // module name, it's a reference to a global/ambient symbol. 299 | if (ast.value.moduleName !== null) { 300 | return `${ast.value.name}`; 301 | } else { 302 | // The symbol is ambient, so just reference it. 303 | return ast.value.name; 304 | } 305 | } 306 | 307 | visitConditionalExpr(ast: ng.ConditionalExpr, context: Context): string { 308 | let cond: string = ast.condition.visitExpression(this, context); 309 | 310 | // Ordinarily the ternary operator is right-associative. The following are 311 | // equivalent: 312 | // `a ? b : c ? d : e` => `a ? b : (c ? d : e)` 313 | // 314 | // However, occasionally Angular needs to produce a left-associative 315 | // conditional, such as in the case of a null-safe navigation production: 316 | // `{{a?.b ? c : d}}`. This template produces a ternary of the form: 317 | // `a == null ? null : rest of expression` 318 | // If the rest of the expression is also a ternary though, this would 319 | // produce the form: 320 | // `a == null ? null : a.b ? c : d` 321 | // which, if left as right-associative, would be incorrectly associated as: 322 | // `a == null ? null : (a.b ? c : d)` 323 | // 324 | // In such cases, the left-associativity needs to be enforced with 325 | // parentheses: 326 | // `(a == null ? null : a.b) ? c : d` 327 | // 328 | // Such parentheses could always be included in the condition (guaranteeing 329 | // correct behavior) in all cases, but this has a code size cost. Instead, 330 | // parentheses are added only when a conditional expression is directly used 331 | // as the condition of another. 332 | // 333 | // TODO(alxhub): investigate better logic for precendence of conditional 334 | // operators 335 | if (ast.condition instanceof ng.ConditionalExpr) { 336 | // The condition of this ternary needs to be wrapped in parentheses to 337 | // maintain left-associativity. 338 | cond = `(${cond})`; 339 | } 340 | 341 | return ( 342 | cond + 343 | ' ? ' + 344 | ast.trueCase.visitExpression(this, context) + 345 | ' : ' + 346 | ast.falseCase!.visitExpression(this, context) 347 | ); 348 | } 349 | 350 | visitDynamicImportExpr(ast: ng.DynamicImportExpr, context: any) { 351 | return `import('${ast.url}')`; 352 | } 353 | 354 | visitNotExpr(ast: ng.NotExpr, context: Context): string { 355 | return '!' + ast.condition.visitExpression(this, context); 356 | } 357 | 358 | visitFunctionExpr(ast: ng.FunctionExpr, context: Context): string { 359 | let fn = `function `; 360 | if (ast.name) { 361 | fn += ast.name; 362 | } 363 | fn += `(` + ast.params.map((param) => param.name).join(', ') + ') {'; 364 | fn += this.visitStatements(ast.statements, context); 365 | fn += '}'; 366 | return fn; 367 | } 368 | 369 | visitArrowFunctionExpr(ast: ng.ArrowFunctionExpr, context: any) { 370 | const params = ast.params.map((param) => param.name).join(', '); 371 | let body: string; 372 | if (Array.isArray(ast.body)) { 373 | body = '{' + this.visitStatements(ast.body, context) + '}'; 374 | } else { 375 | body = ast.body.visitExpression(this, context); 376 | } 377 | return `(${params}) => (${body})`; 378 | } 379 | 380 | visitBinaryOperatorExpr( 381 | ast: ng.BinaryOperatorExpr, 382 | context: Context, 383 | ): string { 384 | if (!BINARY_OPERATORS.has(ast.operator)) { 385 | throw new Error( 386 | `Unknown binary operator: ${ng.BinaryOperator[ast.operator]}`, 387 | ); 388 | } 389 | return ( 390 | ast.lhs.visitExpression(this, context) + 391 | BINARY_OPERATORS.get(ast.operator)! + 392 | '(' + 393 | ast.rhs.visitExpression(this, context) + 394 | ')' 395 | ); 396 | } 397 | 398 | visitReadPropExpr(ast: ng.ReadPropExpr, context: Context): string { 399 | return ast.receiver.visitExpression(this, context) + '.' + ast.name; 400 | } 401 | 402 | visitReadKeyExpr(ast: ng.ReadKeyExpr, context: Context): string { 403 | const receiver = ast.receiver.visitExpression(this, context); 404 | const key = ast.index.visitExpression(this, context); 405 | return `${receiver}[${key}]`; 406 | } 407 | 408 | visitLiteralArrayExpr(ast: ng.LiteralArrayExpr, context: Context): string { 409 | const entries = ast.entries.map((expr) => 410 | this.setSourceMapRange( 411 | expr.visitExpression(this, context), 412 | ast.sourceSpan, 413 | ), 414 | ); 415 | return '[' + entries.join(', ') + ']'; 416 | } 417 | 418 | visitLiteralMapExpr(ast: ng.LiteralMapExpr, context: Context): string { 419 | const properties: string[] = ast.entries.map((entry) => { 420 | let key = entry.key; 421 | if (entry.quoted) { 422 | key = `'` + key.replaceAll(`'`, `\\'`) + `'`; 423 | } 424 | return key + ': ' + entry.value.visitExpression(this, context); 425 | }); 426 | return this.setSourceMapRange( 427 | '{' + properties.join(', ') + '}', 428 | ast.sourceSpan, 429 | ); 430 | } 431 | 432 | visitCommaExpr(ast: ng.CommaExpr, context: Context): never { 433 | throw new Error('Method not implemented.'); 434 | } 435 | 436 | visitWrappedNodeExpr( 437 | ast: ng.WrappedNodeExpr, 438 | _context: Context, 439 | ): string { 440 | return ast.node; 441 | } 442 | 443 | visitTypeofExpr(ast: ng.TypeofExpr, context: Context): string { 444 | return 'typeof ' + ast.expr.visitExpression(this, context); 445 | } 446 | 447 | visitUnaryOperatorExpr(ast: ng.UnaryOperatorExpr, context: Context): string { 448 | if (!UNARY_OPERATORS.has(ast.operator)) { 449 | throw new Error( 450 | `Unknown unary operator: ${ng.UnaryOperator[ast.operator]}`, 451 | ); 452 | } 453 | return ( 454 | UNARY_OPERATORS.get(ast.operator)! + 455 | ast.expr.visitExpression(this, context) 456 | ); 457 | } 458 | 459 | visitTaggedTemplateLiteralExpr( 460 | ast: ng.TaggedTemplateLiteralExpr, 461 | context: any, 462 | ) { 463 | throw new Error('Method not implemented.'); 464 | } 465 | 466 | visitTemplateLiteralExpr(ast: ng.TemplateLiteralExpr, context: any) { 467 | let str = '`'; 468 | for (let i = 0; i < ast.elements.length; i++) { 469 | str += ast.elements[i].visitExpression(this, this); 470 | const expression = i < ast.expressions.length ? ast.expressions[i] : null; 471 | if (expression !== null) { 472 | str += '${' + expression.visitExpression(this, this) + '}'; 473 | } 474 | } 475 | str += '`'; 476 | return str; 477 | } 478 | 479 | visitTemplateLiteralElementExpr( 480 | ast: ng.TemplateLiteralElementExpr, 481 | context: any, 482 | ) { 483 | return ast.text; 484 | } 485 | 486 | visitVoidExpr(ast: ng.VoidExpr, context: any) { 487 | return 'void ' + ast.expr.visitExpression(this, context); 488 | } 489 | visitParenthesizedExpr(ast: ng.ParenthesizedExpr, context: any) { 490 | return '(' + ast.expr.visitExpression(this, context); + ')'; 491 | } 492 | 493 | private visitStatements( 494 | statements: ng.Statement[], 495 | context: Context, 496 | ): string { 497 | return statements 498 | .map((stmt) => stmt.visitStatement(this, context)) 499 | .filter((stmt) => stmt !== undefined) 500 | .join('\n'); 501 | } 502 | 503 | private setSourceMapRange( 504 | ast: string, 505 | span: ng.ParseSourceSpan | null, 506 | ): string { 507 | return ast; 508 | } 509 | 510 | private attachComments( 511 | statement: string, 512 | leadingComments: ng.LeadingComment[] | undefined, 513 | ): string { 514 | // if (leadingComments !== undefined) { 515 | // this.factory.attachComments(statement, leadingComments); 516 | // } 517 | return statement; 518 | } 519 | } 520 | -------------------------------------------------------------------------------- /src/app/templates/async-pipe.ts: -------------------------------------------------------------------------------- 1 | export const asyncPipeTemplate = `
2 | {{projects$ | async}} 3 |
4 | 5 |
6 | @if (users$ | async; as users) { 7 | {{ users.length }} 8 | } 9 |
` 10 | -------------------------------------------------------------------------------- /src/app/templates/banana.ts: -------------------------------------------------------------------------------- 1 | export const bananaBoxTemplate = `` -------------------------------------------------------------------------------- /src/app/templates/child-component.ts: -------------------------------------------------------------------------------- 1 | export const childComponentTemplate = `` 2 | -------------------------------------------------------------------------------- /src/app/templates/default.ts: -------------------------------------------------------------------------------- 1 | export const defaultTemplate = 2 | `
This is a static text node.
And this is a dynamic text node: {{test}}
3 | @if(isVisible) { 4 | Some label 5 | }` 6 | -------------------------------------------------------------------------------- /src/app/templates/event-bindings.ts: -------------------------------------------------------------------------------- 1 | export const eventBindingsTemplate = ` 2 | 3 | ` 4 | -------------------------------------------------------------------------------- /src/app/templates/for-loops.ts: -------------------------------------------------------------------------------- 1 | export const forLoopsTemplate = 2 | `
For loops examples
3 | @for(item of [1,2,3,4]; track $index) { 4 | {{item}} 5 | } 6 | 7 |
8 | 9 | {{item}}` 10 | -------------------------------------------------------------------------------- /src/app/templates/if-else-reference.ts: -------------------------------------------------------------------------------- 1 | export const ifElseReferenceTemplate = 2 | `
3 | Welcome back, friend. 4 |
5 | 6 | 7 | Please friend, login. 8 | ` 9 | -------------------------------------------------------------------------------- /src/app/templates/if-then-else.ts: -------------------------------------------------------------------------------- 1 | export const ifThenElseTemplate = 2 | `@if (a > b) { 3 | {{a}} is greater than {{b}} 4 | } @else if (b > a) { 5 | {{a}} is less than {{b}} 6 | } @else { 7 | {{a}} is equal to {{b}} 8 | }` 9 | -------------------------------------------------------------------------------- /src/app/templates/index.ts: -------------------------------------------------------------------------------- 1 | import { asyncPipeTemplate } from "./async-pipe"; 2 | import { bananaBoxTemplate } from "./banana"; 3 | import { childComponentTemplate } from "./child-component"; 4 | import { defaultTemplate } from "./default"; 5 | import { eventBindingsTemplate } from "./event-bindings"; 6 | import { forLoopsTemplate } from "./for-loops"; 7 | import { ifElseReferenceTemplate } from "./if-else-reference"; 8 | import { ifThenElseTemplate } from "./if-then-else"; 9 | import { nestedNodesTemplate } from "./nested-nodes"; 10 | import { NgModelTemplate } from "./ng-model"; 11 | import { simpleAtLet } from "./simple-at-let"; 12 | import { templateLiteral } from "./template-literal"; 13 | 14 | export type Template = Record<'label' | 'content', string>; 15 | 16 | export const templates: Template[] = [ 17 | { label: 'default', content: defaultTemplate }, 18 | { label: 'nested nodes', content: nestedNodesTemplate }, 19 | { label: '@if/@else', content: ifThenElseTemplate }, 20 | { label: '@for vs ngFor', content: forLoopsTemplate }, 21 | { label: 'ngIf/else w/ template reference', content: ifElseReferenceTemplate }, 22 | { label: 'event bindings', content: eventBindingsTemplate }, 23 | { label: 'ng-model', content: NgModelTemplate }, 24 | { label: 'async pipe', content: asyncPipeTemplate }, 25 | { label: 'child component', content: childComponentTemplate }, 26 | { label: 'simple @let', content: simpleAtLet}, 27 | { label: 'double binding (banna in a box)', content: bananaBoxTemplate }, 28 | { label: 'template literal', content: templateLiteral }, 29 | ]; 30 | -------------------------------------------------------------------------------- /src/app/templates/nested-nodes.ts: -------------------------------------------------------------------------------- 1 | export const nestedNodesTemplate = 2 | `
3 |
some header
4 |
5 |
some section
6 | 7 |
8 |
some footer
9 |
` 10 | -------------------------------------------------------------------------------- /src/app/templates/ng-model.ts: -------------------------------------------------------------------------------- 1 | export const NgModelTemplate = 2 | ` 3 | 4 |

Value: {{ name }}

5 |

Valid: {{ ctrl.valid }}

6 | 7 | ` 8 | -------------------------------------------------------------------------------- /src/app/templates/simple-at-let.ts: -------------------------------------------------------------------------------- 1 | export const simpleAtLet = 2 | `
3 | @let myVar = 5; 4 | {{myVar}} 5 |
` 6 | -------------------------------------------------------------------------------- /src/app/templates/template-literal.ts: -------------------------------------------------------------------------------- 1 | export const templateLiteral = 2 | `
{{\`-- \${foo} --\`}}
` 3 | -------------------------------------------------------------------------------- /src/app/zip.ts: -------------------------------------------------------------------------------- 1 | // Apply LZW-compression to a string and return base64 compressed string. 2 | export function zip(s: string): string { 3 | try { 4 | var dict: Record = {}; 5 | var data = (s + '').split(''); 6 | var out: any[] = []; 7 | var currChar; 8 | var phrase = data[0]; 9 | var code = 256; 10 | for (var i = 1; i < data.length; i++) { 11 | currChar = data[i]; 12 | if (dict[phrase + currChar] != null) { 13 | phrase += currChar; 14 | } else { 15 | out.push(phrase.length > 1 ? dict[phrase] : phrase.charCodeAt(0)); 16 | dict[phrase + currChar] = code; 17 | code++; 18 | phrase = currChar; 19 | } 20 | } 21 | out.push(phrase.length > 1 ? dict[phrase] : phrase.charCodeAt(0)); 22 | for (var j = 0; j < out.length; j++) { 23 | out[j] = String.fromCharCode(out[j]); 24 | } 25 | return utoa(out.join('')); 26 | } catch (e) { 27 | console.log('Failed to zip string return empty string', e); 28 | return ''; 29 | } 30 | } 31 | 32 | // Decompress an LZW-encoded base64 string 33 | export function unzip(base64ZippedString: string) { 34 | try { 35 | var s = atou(base64ZippedString); 36 | var dict: Record = {}; 37 | var data = (s + '').split(''); 38 | var currChar = data[0]; 39 | var oldPhrase = currChar; 40 | var out = [currChar]; 41 | var code = 256; 42 | var phrase; 43 | for (var i = 1; i < data.length; i++) { 44 | var currCode = data[i].charCodeAt(0); 45 | if (currCode < 256) { 46 | phrase = data[i]; 47 | } else { 48 | phrase = dict[currCode] ? dict[currCode] : oldPhrase + currChar; 49 | } 50 | out.push(phrase); 51 | currChar = phrase.charAt(0); 52 | dict[code] = oldPhrase + currChar; 53 | code++; 54 | oldPhrase = phrase; 55 | } 56 | return out.join(''); 57 | } catch (e) { 58 | console.log('Failed to unzip string return empty string', e); 59 | return ''; 60 | } 61 | } 62 | 63 | // ucs-2 string to base64 encoded ascii 64 | function utoa(str: string): string { 65 | return window.btoa(unescape(encodeURIComponent(str))); 66 | } 67 | // base64 encoded ascii to ucs-2 string 68 | function atou(str: string): string { 69 | return decodeURIComponent(escape(window.atob(str))); 70 | } 71 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AngularCompilerOutput 6 | 7 | 8 | 9 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { bootstrapApplication } from '@angular/platform-browser'; 2 | import { appConfig } from './app/app.config'; 3 | import { AppComponent } from './app/app.component'; 4 | 5 | bootstrapApplication(AppComponent, appConfig) 6 | .catch((err) => console.error(err)); 7 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | /* Add application styles & imports to this file! */ 3 | 4 | // Following styles have been mostly borrowed from the angular.dev style 5 | @use "styles/button"; 6 | @use "styles/colors"; 7 | @import "@angular/cdk/overlay-prebuilt.css"; 8 | 9 | @include button.button(); 10 | 11 | html, 12 | body { 13 | height: 100%; 14 | } 15 | body { 16 | margin: 0; 17 | font-family: Roboto, "Helvetica Neue", sans-serif; 18 | } 19 | 20 | header { 21 | text-align: center; 22 | font-size: 16pt; 23 | font-weight: bold; 24 | padding: 8px; 25 | margin-bottom: 8px; 26 | color: white; 27 | background: var(--red-to-pink-to-purple-horizontal-gradient); 28 | } 29 | 30 | body { 31 | font-family: sans-serif; 32 | } 33 | 34 | main { 35 | padding: 18px 18px 32px; 36 | } 37 | 38 | h2 { 39 | margin-top: 0; 40 | } 41 | 42 | section { 43 | display: flex; 44 | gap: 12px; 45 | } 46 | 47 | hr { 48 | border-radius: 8px; 49 | height: 8px; 50 | background: var(--red-to-pink-to-purple-horizontal-gradient); 51 | border: none; 52 | } 53 | 54 | .error { 55 | color: var(--hot-red); 56 | } 57 | 58 | textarea { 59 | flex: 1; 60 | } 61 | 62 | pre { 63 | padding: 12px; 64 | border-radius: 8px; 65 | } 66 | 67 | .shiki { 68 | code { 69 | counter-reset: step; 70 | counter-increment: step 0; 71 | } 72 | 73 | code .line::before { 74 | content: counter(step); 75 | counter-increment: step; 76 | width: 1rem; 77 | margin-right: 1.5rem; 78 | display: inline-block; 79 | text-align: right; 80 | color: rgba(115, 138, 148, 0.4); 81 | } 82 | } 83 | 84 | .controls { 85 | display: flex; 86 | flex-direction: column; 87 | align-items: end; 88 | justify-content: space-between; 89 | gap: 8px; 90 | 91 | option { 92 | padding: 8px; 93 | } 94 | } 95 | 96 | .twitter { 97 | background: var(--red-to-pink-to-purple-horizontal-gradient); 98 | background-clip: text; 99 | -webkit-text-fill-color: transparent; 100 | text-decoration: underline; 101 | 102 | a { 103 | font-weight: bold; 104 | } 105 | } 106 | 107 | .adev-template-select { 108 | margin-block-end: 0.5rem; 109 | 110 | label { 111 | color: var(--quaternary-contrast); 112 | font-size: 0.875rem; 113 | margin-block-end: 0.3rem; 114 | margin-inline-start: 0.45rem; 115 | display: block; 116 | } 117 | 118 | // cdk select button 119 | button { 120 | font-size: 0.875rem; 121 | border: 1px solid var(--senary-contrast); 122 | border-radius: 0.25rem; 123 | width: 200px; 124 | display: flex; 125 | justify-content: space-between; 126 | align-items: center; 127 | padding-block: 0.5rem; 128 | font-weight: 400; 129 | transition: border 0.3s ease; 130 | span { 131 | color: var(--primary-contrast); 132 | transition: color 0.3s ease; 133 | margin-inline-start: 0.1rem; 134 | } 135 | 136 | docs-icon { 137 | font-size: 1.3rem; 138 | color: var(--quaternary-contrast); 139 | transition: color 0.3s ease; 140 | } 141 | } 142 | } 143 | 144 | // select dropdown 145 | .adev-template-dropdown { 146 | border: 1px solid var(--senary-contrast); 147 | border-radius: 0.25rem; 148 | padding: 0; 149 | transform: translateY(-0.7rem); 150 | 151 | li { 152 | list-style: none; 153 | width: 198px; 154 | box-sizing: border-box; 155 | 156 | button { 157 | background: var(--page-background); 158 | font-size: 0.875rem; 159 | width: 100%; 160 | text-align: left; 161 | padding-block: 0.5rem; 162 | color: var(--quaternary-contrast); 163 | transition: 164 | color 0.3s ease, 165 | background 0.3s ease; 166 | font-weight: 400; 167 | 168 | &:hover { 169 | background: var(--senary-contrast); 170 | color: var(--primary-contrast); 171 | } 172 | } 173 | } 174 | } 175 | 176 | .mat-typography { 177 | font: 178 | 400 1rem / 1.5rem Roboto, 179 | sans-serif; 180 | letter-spacing: 0.031rem; 181 | } 182 | -------------------------------------------------------------------------------- /src/styles/_button.scss: -------------------------------------------------------------------------------- 1 | @mixin button() { 2 | button { 3 | font-family: var(--inter-font); 4 | background: transparent; 5 | -webkit-appearance: none; 6 | border: 0; 7 | font-weight: 600; 8 | 9 | // Remove excess padding and border in Firefox 4+ 10 | &::-moz-focus-inner { 11 | border: 0; 12 | padding: 0; 13 | } 14 | 15 | &:disabled { 16 | cursor: not-allowed; 17 | } 18 | } 19 | 20 | @property --angle { 21 | syntax: ""; 22 | initial-value: 90deg; 23 | inherits: false; 24 | } 25 | 26 | @keyframes spin-gradient { 27 | 0% { 28 | --angle: 90deg; 29 | } 30 | 100% { 31 | --angle: 450deg; 32 | } 33 | } 34 | 35 | .docs-primary-btn { 36 | cursor: pointer; 37 | border: none; 38 | outline: none; 39 | position: relative; 40 | border-radius: 0.25rem; 41 | padding: 0.75rem 1.5rem; 42 | width: max-content; 43 | color: transparent; 44 | 45 | // border gradient / background 46 | --angle: 90deg; 47 | background: linear-gradient( 48 | var(--angle), 49 | var(--orange-red) 0%, 50 | var(--vivid-pink) 50%, 51 | var(--electric-violet) 100% 52 | ); 53 | 54 | docs-icon { 55 | z-index: var(--z-index-content); 56 | position: relative; 57 | } 58 | 59 | // text & radial gradient 60 | &::before { 61 | content: attr(text); 62 | position: absolute; 63 | inset: 1px; 64 | background: var(--page-bg-radial-gradient); 65 | border-radius: 0.2rem; 66 | display: flex; 67 | align-items: center; 68 | justify-content: center; 69 | transition: 70 | opacity 0.3s ease, 71 | background 0.3s ease; 72 | color: var(--primary-contrast); 73 | } 74 | 75 | // solid color negative space - CSS transition supported 76 | &::after { 77 | content: attr(text); 78 | position: absolute; 79 | inset: 1px; 80 | background: var(--page-background); 81 | border-radius: 0.2rem; 82 | display: flex; 83 | align-items: center; 84 | justify-content: center; 85 | transition: 86 | opacity 0.3s ease, 87 | background 0.3s ease; 88 | color: var(--primary-contrast); 89 | } 90 | 91 | &:hover { 92 | animation: spin-gradient 4s linear infinite forwards; 93 | &::before { 94 | background-color: var(--page-background); 95 | background: var(--soft-pink-radial-gradient); 96 | opacity: 0.9; 97 | } 98 | &::after { 99 | opacity: 0; 100 | } 101 | } 102 | 103 | &:active { 104 | &::before { 105 | opacity: 0.8; 106 | } 107 | } 108 | 109 | &:disabled { 110 | //gradient stroke 111 | background: var(--quinary-contrast); 112 | color: var(--quinary-contrast); 113 | 114 | &::before { 115 | background-color: var(--page-background); 116 | background: var(--page-bg-radial-gradient); 117 | opacity: 1; 118 | } 119 | 120 | docs-icon { 121 | color: var(--quinary-contrast); 122 | } 123 | } 124 | 125 | docs-icon { 126 | z-index: var(--z-index-icon); 127 | color: var(--primary-contrast); 128 | } 129 | } 130 | 131 | .docs-secondary-btn { 132 | border: 1px solid var(--senary-contrast); 133 | background: var(--page-background); 134 | padding: 0.75rem 1.5rem; 135 | border-radius: 0.25rem; 136 | color: var(--primary-contrast); 137 | transition: background 0.3s ease; 138 | 139 | docs-icon { 140 | color: var(--quaternary-contrast); 141 | transition: color 0.3s ease; 142 | } 143 | 144 | &:hover { 145 | background: var(--septenary-contrast); 146 | 147 | docs-icon { 148 | color: var(--primary-contrast); 149 | } 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/styles/_colors.scss: -------------------------------------------------------------------------------- 1 | // Colors 2 | // Using OKLCH color space for better color reproduction on P3 displays, 3 | // as well as better human-readability 4 | // --> https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/oklch 5 | 6 | @mixin root-definitions() { 7 | // PRIMITIVES 8 | // Colors 9 | --bright-blue: oklch(51.01% 0.274 263.83); // #0546ff 10 | --indigo-blue: oklch(51.64% 0.229 281.65); // #5c44e4 11 | --electric-violet: oklch(53.18% 0.28 296.97); // #8514f5 12 | --french-violet: oklch(47.66% 0.246 305.88); // #8001c6 13 | --vivid-pink: oklch(69.02% 0.277 332.77); // #f637e3 14 | --hot-pink: oklch(59.91% 0.239 8.14); // #e90464 15 | --hot-red: oklch(61.42% 0.238 15.34); // #f11653 16 | --orange-red: oklch(63.32% 0.24 31.68); // #fa2c04 17 | --super-green: oklch( 18 | 79.12% 0.257 155.13 19 | ); // #00c572 // Used for success, merge additions, etc. 20 | 21 | // subtle-purple is used for inline-code bg, docs-card hover bg & docs-code header bg 22 | --subtle-purple: color-mix(in srgb, var(--bright-blue) 5%, white 10%); 23 | --light-blue: color-mix(in srgb, var(--bright-blue), white 50%); 24 | --light-violet: color-mix(in srgb, var(--electric-violet), white 65%); 25 | --light-orange: color-mix(in srgb, var(--orange-red), white 50%); 26 | --light-pink: color-mix(in srgb, var(--vivid-pink) 10%, white 80%); 27 | 28 | // SYMBOLIC COLORS 29 | // Used for Type Labels 30 | --symbolic-purple: oklch(42.86% 0.29 266.4); //#1801ea 31 | --symbolic-gray: oklch(66.98% 0 0); // #959595 32 | --symbolic-blue: oklch(42.45% 0.223 263.38); // #0037c5; 33 | --symbolic-pink: oklch(63.67% 0.254 13.47); // #ff025c 34 | --symbolic-orange: oklch( 35 | 64.73% 0.23769984683784018 33.18328352127882 36 | ); // #fe3700 37 | --symbolic-yellow: oklch(78.09% 0.163 65.69); // #fd9f28 38 | --symbolic-green: oklch(67.83% 0.229 142.73); // #00b80a 39 | --symbolic-cyan: oklch( 40 | 67.05% 0.1205924489987394 181.34025902203868 41 | ); // #00ad9a 42 | --symbolic-magenta: oklch( 43 | 51.74% 0.25453048882711515 315.26261625862725 44 | ); // #9c00c8 45 | --symbolic-teal: oklch(57.59% 0.083 230.58); // #3f82a1 46 | --symbolic-brown: oklch(49.06% 0.128 46.41); // #994411 47 | --symbolic-lime: oklch( 48 | 70.33% 0.2078857836035299 135.66843631046476 49 | ); // #5dba00 50 | 51 | // Grays 52 | --gray-1000: oklch(16.93% 0.004 285.95); // #0f0f11 53 | --gray-900: oklch(19.37% 0.006 300.98); // #151417 54 | --gray-800: oklch(25.16% 0.008 308.11); // #232125 55 | --gray-700: oklch(36.98% 0.014 302.71); // #413e46 56 | --gray-600: oklch(44% 0.019 306.08); // #55505b 57 | --gray-500: oklch(54.84% 0.023 304.99); // #746e7c 58 | --gray-400: oklch(70.9% 0.015 304.04); // #a39fa9 59 | --gray-300: oklch(84.01% 0.009 308.34); // #ccc9cf 60 | --gray-200: oklch(91.75% 0.004 301.42); // #e4e3e6 61 | --gray-100: oklch(97.12% 0.002 325.59); // #f6f5f6 62 | --gray-50: oklch(98.81% 0 0); // #fbfbfb 63 | 64 | // GRADIENTS 65 | --red-to-pink-horizontal-gradient: linear-gradient( 66 | 90deg, 67 | var(--hot-pink) 11.42%, 68 | var(--hot-red) 34.83%, 69 | var(--vivid-pink) 60.69% 70 | ); 71 | 72 | --red-to-pink-to-purple-horizontal-gradient: linear-gradient( 73 | 90deg, 74 | var(--orange-red) 0%, 75 | var(--vivid-pink) 50%, 76 | var(--electric-violet) 100% 77 | ); 78 | 79 | --pink-to-highlight-to-purple-to-blue-horizontal-gradient: linear-gradient( 80 | 140deg, 81 | var(--vivid-pink) 0%, 82 | var(--vivid-pink) 15%, 83 | color-mix(in srgb, var(--vivid-pink), var(--electric-violet) 50%) 25%, 84 | color-mix(in srgb, var(--vivid-pink), var(--electric-violet) 10%) 35%, 85 | color-mix(in srgb, var(--vivid-pink), var(--orange-red) 50%) 42%, 86 | color-mix(in srgb, var(--vivid-pink), var(--orange-red) 50%) 44%, 87 | color-mix(in srgb, var(--vivid-pink), var(--page-background) 70%) 47%, 88 | var(--electric-violet) 48%, 89 | var(--bright-blue) 60% 90 | ); 91 | 92 | --purple-to-blue-horizontal-gradient: linear-gradient( 93 | 90deg, 94 | var(--electric-violet) 0%, 95 | var(--bright-blue) 100% 96 | ); 97 | --purple-to-blue-vertical-gradient: linear-gradient( 98 | 0deg, 99 | var(--electric-violet) 0%, 100 | var(--bright-blue) 100% 101 | ); 102 | 103 | --red-to-orange-horizontal-gradient: linear-gradient( 104 | 90deg, 105 | var(--hot-pink) 0%, 106 | var(--orange-red) 100% 107 | ); 108 | --red-to-orange-vertical-gradient: linear-gradient( 109 | 0deg, 110 | var(--hot-pink) 0%, 111 | var(--orange-red) 100% 112 | ); 113 | 114 | --pink-to-purple-horizontal-gradient: linear-gradient( 115 | 90deg, 116 | var(--vivid-pink) 0%, 117 | var(--electric-violet) 100% 118 | ); 119 | --pink-to-purple-vertical-gradient: linear-gradient( 120 | 0deg, 121 | var(--electric-violet) 0%, 122 | var(--vivid-pink) 100% 123 | ); 124 | 125 | --purple-to-light-purple-vertical-gradient: linear-gradient( 126 | 0deg, 127 | var(--french-violet) 0%, 128 | var(--light-violet) 100% 129 | ); 130 | 131 | --green-to-cyan-vertical-gradient: linear-gradient( 132 | 0deg, 133 | var(--symbolic-cyan) 0%, 134 | var(--super-green) 100% 135 | ); 136 | 137 | --blue-to-teal-vertical-gradient: linear-gradient( 138 | 0deg, 139 | var(--bright-blue) 0%, 140 | var(--light-blue) 100% 141 | ); 142 | 143 | --blue-to-cyan-vertical-gradient: linear-gradient( 144 | 0deg, 145 | var(--bright-blue) 0%, 146 | var(--symbolic-cyan) 100% 147 | ); 148 | 149 | --black-to-gray-vertical-gradient: linear-gradient( 150 | 0deg, 151 | var(--primary-contrast) 0%, 152 | var(--gray-400) 100% 153 | ); 154 | 155 | --red-to-pink-vertical-gradient: linear-gradient( 156 | 0deg, 157 | var(--hot-red) 0%, 158 | var(--vivid-pink) 100% 159 | ); 160 | --orange-to-pink-vertical-gradient: linear-gradient( 161 | 0deg, 162 | var(--vivid-pink) 0%, 163 | var(--light-orange) 100% 164 | ); 165 | 166 | // Radial Gradients 167 | --page-bg-radial-gradient: radial-gradient(circle, white 0%, white 100%); 168 | --soft-pink-radial-gradient: radial-gradient( 169 | circle at center bottom, 170 | var(--light-pink) 0%, 171 | white 80% 172 | ); 173 | 174 | // ABSTRACTIONS light - dark 175 | // --full-contrast: black - white 176 | // --primary-constrast: gray-900 - gray-100 177 | // --secondary-contrast: gray-800 - gray-300 178 | // --tertiary-contrast: gray-700 - gray-300 179 | // --quaternary-contrast: gray-500 - gray-400 180 | // --quinary-contrast: gray-300 - gray-500 181 | // --senary-contrast: gray-200 - gray-700 182 | // --septenary-contrast: gray-100 - gray-800 183 | // --octonary-contrast: gray-50 - gray-900 184 | // --page-background white - gray-1000 185 | 186 | // LIGHT MODE is default 187 | // contrast - light mode 188 | --full-contrast: black; 189 | --primary-contrast: var(--gray-900); 190 | --secondary-contrast: var(--gray-800); 191 | --tertiary-contrast: var(--gray-700); 192 | --quaternary-contrast: var(--gray-500); 193 | --quinary-contrast: var(--gray-300); 194 | --senary-contrast: var(--gray-200); 195 | --septenary-contrast: var(--gray-100); 196 | --octonary-contrast: var(--gray-50); 197 | --page-background: white; 198 | 199 | // Home page 200 | // for the "unfilled" portion of the word that hasn't 201 | // been highlighted by the gradient 202 | --gray-unfilled: var(--gray-400); 203 | // TODO: convert oklch to hex at build time 204 | --webgl-page-background: #ffffff; 205 | --webgl-gray-unfilled: #a39fa9; 206 | } 207 | 208 | @mixin dark-mode-definitions() { 209 | // Contrasts 210 | --full-contrast: white; 211 | --primary-contrast: var(--gray-50); 212 | --secondary-contrast: var(--gray-300); 213 | --tertiary-contrast: var(--gray-300); 214 | --quaternary-contrast: var(--gray-400); 215 | --quinary-contrast: var(--gray-500); 216 | --senary-contrast: var(--gray-700); 217 | --septenary-contrast: var(--gray-800); 218 | --octonary-contrast: var(--gray-900); 219 | --page-background: var(--gray-1000); 220 | 221 | --bright-blue: color-mix( 222 | in srgb, 223 | oklch(51.01% 0.274 263.83), 224 | var(--full-contrast) 60% 225 | ); 226 | --indigo-blue: color-mix( 227 | in srgb, 228 | oklch(51.64% 0.229 281.65), 229 | var(--full-contrast) 70% 230 | ); 231 | --electric-violet: color-mix( 232 | in srgb, 233 | oklch(53.18% 0.28 296.97), 234 | var(--full-contrast) 70% 235 | ); 236 | --french-violet: color-mix( 237 | in srgb, 238 | oklch(47.66% 0.246 305.88), 239 | var(--full-contrast) 70% 240 | ); 241 | --vivid-pink: color-mix( 242 | in srgb, 243 | oklch(69.02% 0.277 332.77), 244 | var(--full-contrast) 70% 245 | ); 246 | --hot-pink: color-mix( 247 | in srgb, 248 | oklch(59.91% 0.239 8.14), 249 | var(--full-contrast) 70% 250 | ); 251 | --hot-red: color-mix( 252 | in srgb, 253 | oklch(61.42% 0.238 15.34), 254 | var(--full-contrast) 70% 255 | ); 256 | --orange-red: color-mix( 257 | in srgb, 258 | oklch(63.32% 0.24 31.68), 259 | var(--full-contrast) 60% 260 | ); 261 | --super-green: color-mix( 262 | in srgb, 263 | oklch(79.12% 0.257 155.13), 264 | var(--full-contrast) 70% 265 | ); 266 | 267 | --light-pink: color-mix( 268 | in srgb, 269 | var(--vivid-pink) 5%, 270 | var(--page-background) 75% 271 | ); 272 | 273 | --symbolic-purple: color-mix( 274 | in srgb, 275 | oklch(42.86% 0.29 266.4), 276 | var(--full-contrast) 65% 277 | ); 278 | --symbolic-gray: color-mix( 279 | in srgb, 280 | oklch(66.98% 0 0), 281 | var(--full-contrast) 65% 282 | ); 283 | --symbolic-blue: color-mix( 284 | in srgb, 285 | oklch(42.45% 0.223 263.38), 286 | var(--full-contrast) 65% 287 | ); 288 | --symbolic-pink: color-mix( 289 | in srgb, 290 | oklch(63.67% 0.254 13.47), 291 | var(--full-contrast) 65% 292 | ); 293 | --symbolic-orange: color-mix( 294 | in srgb, 295 | oklch(64.73% 0.23769984683784018 33.18328352127882), 296 | var(--full-contrast) 65% 297 | ); 298 | --symbolic-yellow: color-mix( 299 | in srgb, 300 | oklch(78.09% 0.163 65.69), 301 | var(--full-contrast) 65% 302 | ); 303 | --symbolic-green: color-mix( 304 | in srgb, 305 | oklch(67.83% 0.229 142.73), 306 | var(--full-contrast) 65% 307 | ); 308 | --symbolic-cyan: color-mix( 309 | in srgb, 310 | oklch(67.05% 0.1205924489987394 181.34025902203868), 311 | var(--full-contrast) 65% 312 | ); 313 | --symbolic-magenta: color-mix( 314 | in srgb, 315 | oklch(51.74% 0.25453048882711515 315.26261625862725), 316 | var(--full-contrast) 65% 317 | ); 318 | --symbolic-teal: color-mix( 319 | in srgb, 320 | oklch(57.59% 0.083 230.58), 321 | var(--full-contrast) 65% 322 | ); 323 | --symbolic-brown: color-mix( 324 | in srgb, 325 | oklch(49.06% 0.128 46.41), 326 | var(--full-contrast) 65% 327 | ); 328 | --symbolic-lime: color-mix( 329 | in srgb, 330 | oklch(70.33% 0.2078857836035299 135.66843631046476), 331 | var(--full-contrast) 65% 332 | ); 333 | 334 | --page-bg-radial-gradient: radial-gradient(circle, black 0%, black 100%); 335 | --soft-pink-radial-gradient: radial-gradient( 336 | circle at center bottom, 337 | var(--light-pink) 0%, 338 | color-mix(in srgb, black, transparent 15%) 80% 339 | ); 340 | 341 | // Home page - dark mode 342 | --gray-unfilled: var(--gray-700); 343 | // TODO: convert oklch to hex at build time 344 | --webgl-page-background: #0f0f11; 345 | --webgl-gray-unfilled: #413e46; 346 | 347 | .docs-toggle { 348 | input { 349 | &:checked + .docs-slider { 350 | background: var(--pink-to-purple-horizontal-gradient) !important; 351 | } 352 | } 353 | } 354 | } 355 | 356 | @mixin mdc-definitions() { 357 | --mdc-snackbar-container-shape: 0.25rem; 358 | --mdc-snackbar-container-color: var(--page-background); 359 | --mdc-snackbar-supporting-text-color: var(--primary-contrast); 360 | } 361 | 362 | // LIGHT MODE (Explicit) 363 | .docs-light-mode { 364 | background-color: #ffffff; 365 | @include root-definitions(); 366 | @include mdc-definitions(); 367 | .docs-invert-mode { 368 | @include dark-mode-definitions(); 369 | @include mdc-definitions(); 370 | } 371 | } 372 | 373 | // DARK MODE (Explicit) 374 | .docs-dark-mode { 375 | background-color: oklch(16.93% 0.004 285.95); 376 | @include root-definitions(); 377 | @include dark-mode-definitions(); 378 | @include mdc-definitions(); 379 | .docs-invert-mode { 380 | @include root-definitions(); 381 | @include mdc-definitions(); 382 | } 383 | } 384 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "outDir": "./dist/out-tsc", 6 | "strict": true, 7 | "noImplicitOverride": true, 8 | "noPropertyAccessFromIndexSignature": true, 9 | "noImplicitReturns": true, 10 | "noFallthroughCasesInSwitch": true, 11 | "skipLibCheck": true, 12 | "esModuleInterop": true, 13 | "sourceMap": true, 14 | "declaration": false, 15 | "experimentalDecorators": true, 16 | "moduleResolution": "bundler", 17 | "importHelpers": true, 18 | "target": "ES2022", 19 | "module": "ES2022", 20 | "useDefineForClassFields": false, 21 | "lib": [ 22 | "ES2022", 23 | "dom" 24 | ] 25 | }, 26 | "angularCompilerOptions": { 27 | "enableI18nLegacyMessageIdFormat": false, 28 | "strictInjectionParameters": true, 29 | "strictInputAccessModifiers": true, 30 | "strictTemplates": true 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "include": [ 11 | "src/**/*.spec.ts", 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | --------------------------------------------------------------------------------