feat: Add Angular, HTML, and SCSS / Sass language servers (#1439)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Pavlo Basanets <Pavlo_Basanets@epam.com>
This commit is contained in:
authored and GitHub committed 2026-05-05 14:33:51 +02:00
1 parent e643e91cd7
commit 3122384822
53 files changed
+3280 -22

No files matched your search

@@ -0,0 +1,5 @@
node_modules
.angular
dist
out-tsc
.angular-install.lock
@@ -0,0 +1,24 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"serena-test-app": {
"projectType": "application",
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:application",
"options": {
"outputPath": "dist/serena-test-app",
"index": "src/index.html",
"browser": "src/main.ts",
"tsConfig": "tsconfig.app.json"
}
}
}
}
}
}
+134
View File
@@ -0,0 +1,134 @@
{
"name": "serena-angular-test-repo",
"version": "0.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "serena-angular-test-repo",
"version": "0.0.0",
"dependencies": {
"@angular/common": "^21.0.0",
"@angular/compiler": "^21.0.0",
"@angular/core": "^21.0.0",
"@angular/platform-browser": "^21.0.0",
"rxjs": "~7.8.0",
"tslib": "^2.6.0",
"zone.js": "~0.15.0"
},
"devDependencies": {
"typescript": "~5.9.0"
}
},
"node_modules/@angular/common": {
"version": "21.2.10",
"resolved": "https://registry.npmjs.org/@angular/common/-/common-21.2.10.tgz",
"integrity": "sha512-WLyi/CRLtgALg2mmaqIuKuPnE4i+8PGt/uuz26pVqx+ASh28/TWr5KSCAMomgxEc8kt4OE7lopoQsTihrQCfEw==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
},
"peerDependencies": {
"@angular/core": "21.2.10",
"rxjs": "^6.5.3 || ^7.4.0"
}
},
"node_modules/@angular/compiler": {
"version": "21.2.10",
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-21.2.10.tgz",
"integrity": "sha512-IrgdFuzzD7NTK3WQaSfowjAPxPbnTqsgR92NsOs5ZaWu3RgLl21dHThNc0BK1KwVwppLUSWmD4qePbcLW71VzQ==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
}
},
"node_modules/@angular/core": {
"version": "21.2.10",
"resolved": "https://registry.npmjs.org/@angular/core/-/core-21.2.10.tgz",
"integrity": "sha512-uxH+mbPiCE7rInWKYOPe9Ytas97+mFM6FhFORoN234yBK3b8he+iDuxX6dsbhEFCxhRmfS6hLxe7BdLY6U6kIA==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
},
"peerDependencies": {
"@angular/compiler": "21.2.10",
"rxjs": "^6.5.3 || ^7.4.0",
"zone.js": "~0.15.0 || ~0.16.0"
},
"peerDependenciesMeta": {
"@angular/compiler": {
"optional": true
},
"zone.js": {
"optional": true
}
}
},
"node_modules/@angular/platform-browser": {
"version": "21.2.10",
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-21.2.10.tgz",
"integrity": "sha512-5WMoHGU8BOV3eO9h3vGMIUDPf+3SHis7+X2dHKMtKfFBUtiO8m/lq2x3PzkkKj1782i7KYt92EqPHuADd/eWOw==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
},
"peerDependencies": {
"@angular/animations": "21.2.10",
"@angular/common": "21.2.10",
"@angular/core": "21.2.10"
},
"peerDependenciesMeta": {
"@angular/animations": {
"optional": true
}
}
},
"node_modules/rxjs": {
"version": "7.8.2",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.1.0"
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/zone.js": {
"version": "0.15.1",
"resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.1.tgz",
"integrity": "sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==",
"license": "MIT"
}
}
}
@@ -0,0 +1,18 @@
{
"name": "serena-angular-test-repo",
"version": "0.0.0",
"private": true,
"description": "Minimal Angular workspace fixture used by Serena language-server tests.",
"dependencies": {
"@angular/common": "^21.0.0",
"@angular/compiler": "^21.0.0",
"@angular/core": "^21.0.0",
"@angular/platform-browser": "^21.0.0",
"rxjs": "~7.8.0",
"tslib": "^2.6.0",
"zone.js": "~0.15.0"
},
"devDependencies": {
"typescript": "~5.9.0"
}
}
@@ -0,0 +1,21 @@
<section>
<h1>{{ title() | exclaim }}</h1>
<p>{{ greeting() }}</p>
<input
type="text"
[value]="userName()"
(input)="setName($any($event.target).value)"
placeholder="Enter your name"
/>
@if (items().length > 0) {
<ul>
@for (item of items(); track item) {
<app-item-card [label]="item"></app-item-card>
}
</ul>
} @else {
<p>No items.</p>
}
</section>
@@ -0,0 +1,26 @@
import { Component, signal } from '@angular/core';
import { GreetingService } from './greeting.service';
import { ItemCardComponent } from './item-card.component';
import { ExclaimPipe } from './exclaim.pipe';
@Component({
selector: 'app-root',
standalone: true,
imports: [ItemCardComponent, ExclaimPipe],
templateUrl: './app.component.html',
})
export class AppComponent {
readonly title = signal('Serena Angular Test');
readonly userName = signal('');
readonly items = signal<string[]>(['alpha', 'beta', 'gamma']);
constructor(private readonly greetings: GreetingService) {}
greeting(): string {
return this.greetings.greet(this.userName() || undefined);
}
setName(name: string): void {
this.userName.set(name);
}
}
@@ -0,0 +1,6 @@
<!-- Template path: ngserver flags the reference to ``undefinedSignal()`` since
it has no matching field on DiagnosticsSampleComponent. The standalone .html
diagnostic exercises the Angular template compiler, not plain-HTML validation. -->
<section>
<h1>{{ undefinedSignal() }}</h1>
</section>
@@ -0,0 +1,15 @@
import { Component } from '@angular/core';
// Two diagnostic sources in one fixture:
// * the type error on ``count`` is reported by tsserver via the Angular plugin
// when diagnosing this .ts file directly.
// * the ``undefinedSignal()`` reference in diagnostics_sample.html is reported
// by ngserver when diagnosing the template attached via ``templateUrl``.
@Component({
selector: 'app-diagnostics-sample',
standalone: true,
templateUrl: './diagnostics_sample.html',
})
export class DiagnosticsSampleComponent {
readonly count: number = 'not-a-number';
}
@@ -0,0 +1,11 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'exclaim',
standalone: true,
})
export class ExclaimPipe implements PipeTransform {
transform(value: string, count: number = 1): string {
return value + '!'.repeat(count);
}
}
@@ -0,0 +1,3 @@
export interface Greeter {
greet(name?: string): string;
}
@@ -0,0 +1,11 @@
import { Injectable } from '@angular/core';
import { Greeter } from './greeter.interface';
@Injectable({ providedIn: 'root' })
export class GreetingService implements Greeter {
private readonly defaultName = 'World';
greet(name?: string): string {
return `Hello, ${name ?? this.defaultName}!`;
}
}
@@ -0,0 +1,18 @@
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-item-card',
standalone: true,
template: `
<li class="item-card" [attr.data-label]="label">
<strong>{{ label }}</strong>
</li>
`,
})
export class ItemCardComponent {
@Input() label: string = '';
formatLabel(): string {
return this.label.trim();
}
}
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Serena Angular Test</title>
</head>
<body>
<app-root></app-root>
</body>
</html>
@@ -0,0 +1,4 @@
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent).catch((err) => console.error(err));
@@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"files": ["src/main.ts"],
"include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.html"]
}
@@ -0,0 +1,25 @@
{
"compileOnSave": false,
"compilerOptions": {
"outDir": "./dist/out-tsc",
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"skipLibCheck": true,
"isolatedModules": true,
"esModuleInterop": true,
"experimentalDecorators": true,
"moduleResolution": "bundler",
"importHelpers": true,
"target": "ES2022",
"module": "ES2022"
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
}