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?