1

I'd like to build an app that can read source files of an Angular application and then work with the ASTs of the template files.

Here's a standalone node script that can reproduce the problem:

const Path = require('path');
const fs = require('fs');
const { WorkspaceSymbols } = require('ngast');

try { fs.mkdirSync("./foo") } catch (e) {};
try { fs.mkdirSync("./foo/src") } catch (e) {};
try { fs.mkdirSync("./foo/src/app") } catch (e) {};

const files = {
    "README.md": "# angular-ivy-scfsqv\n\n[Edit on StackBlitz ⚡️](https://stackblitz.com/edit/angular-ivy-scfsqv)",
    "angular.json": "{\n  \"$schema\": \"./node_modules/@angular/cli/lib/config/schema.json\",\n  \"version\": 1,\n  \"newProjectRoot\": \"projects\",\n  \"projects\": {\n    \"demo\": {\n      \"root\": \"\",\n      \"sourceRoot\": \"src\",\n      \"projectType\": \"application\",\n      \"prefix\": \"app\",\n      \"schematics\": {},\n      \"architect\": {\n        \"build\": {\n          \"builder\": \"@angular-devkit/build-angular:browser\",\n          \"options\": {\n            \"outputPath\": \"dist/demo\",\n            \"index\": \"src/index.html\",\n            \"main\": \"src/main.ts\",\n            \"polyfills\": \"src/polyfills.ts\",\n            \"tsConfig\": \"src/tsconfig.app.json\",\n            \"assets\": [\n              \"src/favicon.ico\",\n              \"src/assets\"\n            ],\n            \"styles\": [\n              \"src/styles.css\"\n            ],\n            \"scripts\": []\n          },\n          \"configurations\": {\n            \"production\": {\n              \"fileReplacements\": [\n                {\n                  \"replace\": \"src/environments/environment.ts\",\n                  \"with\": \"src/environments/environment.prod.ts\"\n                }\n              ],\n              \"optimization\": true,\n              \"outputHashing\": \"all\",\n              \"sourceMap\": false,\n              \"extractCss\": true,\n              \"namedChunks\": false,\n              \"aot\": true,\n              \"extractLicenses\": true,\n              \"vendorChunk\": false,\n              \"buildOptimizer\": true\n            }\n          }\n        },\n        \"serve\": {\n          \"builder\": \"@angular-devkit/build-angular:dev-server\",\n          \"options\": {\n            \"browserTarget\": \"demo:build\"\n          },\n          \"configurations\": {\n            \"production\": {\n              \"browserTarget\": \"demo:build:production\"\n            }\n          }\n        },\n        \"extract-i18n\": {\n          \"builder\": \"@angular-devkit/build-angular:extract-i18n\",\n          \"options\": {\n            \"browserTarget\": \"demo:build\"\n          }\n        },\n        \"test\": {\n          \"builder\": \"@angular-devkit/build-angular:karma\",\n          \"options\": {\n            \"main\": \"src/test.ts\",\n            \"polyfills\": \"src/polyfills.ts\",\n            \"tsConfig\": \"src/tsconfig.spec.json\",\n            \"karmaConfig\": \"src/karma.conf.js\",\n            \"styles\": [\n              \"styles.css\"\n            ],\n            \"scripts\": [],\n            \"assets\": [\n              \"src/favicon.ico\",\n              \"src/assets\"\n            ]\n          }\n        },\n        \"lint\": {\n          \"builder\": \"@angular-devkit/build-angular:tslint\",\n          \"options\": {\n            \"tsConfig\": [\n              \"src/tsconfig.app.json\",\n              \"src/tsconfig.spec.json\"\n            ],\n            \"exclude\": [\n              \"**/node_modules/**\"\n            ]\n          }\n        }\n      }\n    }\n  },\n  \"defaultProject\": \"demo\"\n}",
    "package.json": "{\n  \"name\": \"angular\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"dependencies\": {\n    \"@angular/animations\": \"^11.0.8\",\n    \"@angular/common\": \"^11.0.8\",\n    \"@angular/compiler\": \"^11.0.8\",\n    \"@angular/core\": \"^11.0.8\",\n    \"@angular/forms\": \"^11.0.8\",\n    \"@angular/platform-browser\": \"^11.0.8\",\n    \"@angular/platform-browser-dynamic\": \"^11.0.8\",\n    \"@angular/router\": \"^11.0.8\",\n    \"rxjs\": \"^6.6.3\",\n    \"tslib\": \"^2.1.0\",\n    \"zone.js\": \"^0.11.3\"\n  },\n  \"scripts\": {\n    \"ng\": \"ng\",\n    \"start\": \"ng serve\",\n    \"build\": \"ng build\",\n    \"test\": \"ng test\",\n    \"lint\": \"ng lint\",\n    \"e2e\": \"ng e2e\"\n  },\n  \"devDependencies\": {\n    \"@angular-devkit/build-angular\": \"~0.1100.4\",\n    \"@angular/cli\": \"~11.0.4\",\n    \"@angular/compiler-cli\": \"~11.0.4\",\n    \"@types/jasmine\": \"~3.6.0\",\n    \"@types/node\": \"^12.11.1\",\n    \"codelyzer\": \"^6.0.0\",\n    \"jasmine-core\": \"~3.6.0\",\n    \"jasmine-spec-reporter\": \"~5.0.0\",\n    \"karma\": \"~5.1.0\",\n    \"karma-chrome-launcher\": \"~3.1.0\",\n    \"karma-coverage\": \"~2.0.3\",\n    \"karma-jasmine\": \"~4.0.0\",\n    \"karma-jasmine-html-reporter\": \"^1.5.0\",\n    \"protractor\": \"~7.0.0\",\n    \"ts-node\": \"~8.3.0\",\n    \"tslint\": \"~6.1.0\",\n    \"typescript\": \"~4.0.2\"\n  }\n}",
    "tsconfig.json": "{\n  \"compileOnSave\": false,\n  \"compilerOptions\": {\n    \"baseUrl\": \"./\",\n    \"outDir\": \"./dist/out-tsc\",\n    \"sourceMap\": true,\n    \"declaration\": false,\n    \"downlevelIteration\": true,\n    \"experimentalDecorators\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"node\",\n    \"importHelpers\": true,\n    \"target\": \"es2015\",\n    \"typeRoots\": [\n      \"node_modules/@types\"\n    ],\n    \"lib\": [\n      \"es2018\",\n      \"dom\"\n    ]\n  },\n  \"angularCompilerOptions\": {\n    \"enableIvy\": true,\n    \"fullTemplateTypeCheck\": true,\n    \"strictInjectionParameters\": true\n  }\n}",
    "src/index.html": "<link rel=\"stylesheet\" href=\"https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css\">\n<my-app>loading</my-app>",
    "src/main.ts": "import './polyfills';\n\nimport { enableProdMode } from '@angular/core';\nimport { platformBrowserDynamic } from '@angular/platform-browser-dynamic';\n\nimport { AppModule } from './app/app.module';\n\nplatformBrowserDynamic().bootstrapModule(AppModule).then(ref => {\n  // Ensure Angular destroys itself on hot reloads.\n  if (window['ngRef']) {\n    window['ngRef'].destroy();\n  }\n  window['ngRef'] = ref;\n\n  // Otherwise, log the boot error\n}).catch(err => console.error(err));",
    "src/polyfills.ts": "/**\n * This file includes polyfills needed by Angular and is loaded before the app.\n * You can add your own extra polyfills to this file.\n *\n * This file is divided into 2 sections:\n *   1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.\n *   2. Application imports. Files imported after ZoneJS that should be loaded before your main\n *      file.\n *\n * The current setup is for so-called \"evergreen\" browsers; the last versions of browsers that\n * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),\n * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.\n *\n * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html\n */\n\n/***************************************************************************************************\n * BROWSER POLYFILLS\n */\n\n/** IE9, IE10 and IE11 requires all of the following polyfills. **/\n// import 'core-js/es6/symbol';\n// import 'core-js/es6/object';\n// import 'core-js/es6/function';\n// import 'core-js/es6/parse-int';\n// import 'core-js/es6/parse-float';\n// import 'core-js/es6/number';\n// import 'core-js/es6/math';\n// import 'core-js/es6/string';\n// import 'core-js/es6/date';\n// import 'core-js/es6/array';\n// import 'core-js/es6/regexp';\n// import 'core-js/es6/map';\n// import 'core-js/es6/set';\n\n/** IE10 and IE11 requires the following for NgClass support on SVG elements */\n// import 'classlist.js';  // Run `npm install --save classlist.js`.\n\n/** IE10 and IE11 requires the following to support `@angular/animation`. */\n// import 'web-animations-js';  // Run `npm install --save web-animations-js`.\n\n\n/** Evergreen browsers require these. **/\n// import 'core-js/es6/reflect';\n// import 'core-js/es7/reflect';\n\n\n/**\n * Web Animations `@angular/platform-browser/animations`\n * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.\n * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).\n */\n// import 'web-animations-js';  // Run `npm install --save web-animations-js`.\n\n\n\n/***************************************************************************************************\n * Zone JS is required by Angular itself.\n */\nimport 'zone.js/dist/zone';  // Included with Angular CLI.\n\n\n/***************************************************************************************************\n * APPLICATION IMPORTS\n */\n\n/**\n * Date, currency, decimal and percent pipes.\n * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10\n */\n// import 'intl';  // Run `npm install --save intl`.",
    "src/styles.css": "/* Add application styles & imports to this file! */",
    "src/app/app.component.html": "<div class=\"container\">\n<div class=\"row\">\n\t<div class=\"col-md-6\">\n\t\t<h3 class=\"mb-5\">Invoice</h3>\n\t\t<div>\n\t\t\t<div class=\"row mb-2\">\n\t\t\t\t<label for=\"client\" class=\"col-form-label col-2\">Client</label>\n\t\t\t\t<div class=\"col-6\">\n\t\t\t\t\t<input type=\"text\" id=\"client\" class=\"form-control\">\n                </div>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"row mb-2\">\n\t\t\t\t\t<label for=\"amount\" class=\"col-form-label col-2\">Amount</label>\n\t\t\t\t\t<div class=\"col-6\">\n\t\t\t\t\t\t<input type=\"number\" id=\"amount\" class=\"form-control\">\n                </div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"row mb-2\">\n\t\t\t\t\t\t<label for=\"date\" class=\"col-form-label col-2\">Date</label>\n\t\t\t\t\t\t<div class=\"col-6\">\n\t\t\t\t\t\t\t<input type=\"date\" id=\"date\" class=\"form-control\">\n                </div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"col-md-6\">\n\t\t\t\t\t<h3 class=\"mb-5\">Preview</h3>\n\t\t\t\t\t<p>Client: <span class=\"bg-warning rounded p-2\">TODO</span></p>\n\t\t\t\t\t<p>Amount: <span class=\"bg-warning rounded p-2\">TODO</span></p>\n\t\t\t\t\t<p>Date: <span class=\"bg-warning rounded p-2\">TODO</span></p>\n\t\t\t\t</div>\n\t\t\t</div></div>",
    "src/app/app.component.ts": "import { Component, VERSION } from \"@angular/core\";\n\n@Component({\n  selector: \"my-app\",\n  templateUrl: \"./app.component.html\",\n  styleUrls: []\n})\nexport class AppComponent {\n  name = \"Angular \" + VERSION.major;\n}\n",
    "src/app/app.module.ts": "import { NgModule } from '@angular/core';\nimport { BrowserModule } from '@angular/platform-browser';\nimport { FormsModule } from '@angular/forms';\n\nimport { AppComponent } from './app.component';\n\n@NgModule({\n  imports:      [ BrowserModule, FormsModule ],\n  declarations: [ AppComponent ],\n  bootstrap:    [ AppComponent ]\n})\nexport class AppModule { }\n"
};

Object.entries(files).forEach(([path, contents]) => {
    fs.writeFileSync(Path.join(__dirname, `./foo/${path}`), contents);
});

const config = Path.join(process.cwd(), './foo/tsconfig.json');
const workspace = new WorkspaceSymbols(config);
console.log(workspace.getAllComponents()[0].getTemplateAst());

At the end there, workspace.getAllComponents()[0].getTemplateAst() returns null even though there is an app.component.html template present.

I don't really need to compile the entire project - I just need a way to take a single Angular HTML file and get an AST from it. Is that possible?

Jeff Dean
  • 53
  • 6

0 Answers0