skills/.system/jwebmp-angular/SKILL.md
Generates Angular 20 TypeScript projects from JWebMP annotations and serves SPAs via Vert.x with STOMP/WebSocket bridging. Provides @NgApp, @NgComponent, @NgRoutable, @NgDataService annotations, TypeScript code generation, reactive messaging, Angular control-flow components, and WebSocket group management. Use when working with JWebMP Angular integration, TypeScript generation, Angular components, STOMP/WebSocket communication, or building Angular 20 applications with JWebMP.
npx skillsauth add guicedee/ai-rules jwebmp-angularInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Angular 20 TypeScript project generation and SPA hosting with STOMP/WebSocket bridge for JWebMP.
@NgApp(value = "my-app", bootComponent = AppComponent.class)
public class MyApp extends NGApplication<MyApp> { }
@NgComponent("app-root")
public class AppComponent extends DivSimple<AppComponent>
implements INgComponent<AppComponent> { }
@NgRoutable(path = "dashboard", parent = {AppComponent.class})
@NgComponent("app-dashboard")
public class DashboardPage extends DivSimple<DashboardPage>
implements INgComponent<DashboardPage> { }
export JWEBMP_PROCESS_ANGULAR_TS=true
# Or
mvn verify -Djwebmp.process.angular.ts=true
IGuiceContext.instance();
// → TypeScript generated to ~/.jwebmp/<appName>/
// → Vert.x serves dist at configured routes
// → STOMP/WebSocket bridge live at /eventbus
TypeScriptCompiler
├── AngularAppSetup → Scaffolds angular.json, package.json, tsconfig
├── DependencyManager → Resolves @TsDependency / @TsDevDependency
├── ComponentProcessor → Processes INgComponent, INgDirective, INgDataService
├── AngularModuleProcessor → Generates boot module, routing module
├── TypeScriptCodeGenerator → Renders .ts files from annotations
├── AssetManager → Copies @NgAsset, @NgScript, @NgStyleSheet
└── TypeScriptCodeValidator → Validates generated output
@NgApp(value = "my-app", bootComponent = AppComponent.class)
public class MyApp extends NGApplication<MyApp> { }
@NgComponent("app-header")
public class HeaderComponent implements INgComponent<HeaderComponent> {
@Override
public String render() {
return "<header><h1>My App</h1></header>";
}
}
@NgRoutable(path = "users", parent = {AppComponent.class})
@NgComponent("app-users")
public class UsersPage implements INgComponent<UsersPage> { }
@NgDataService
public class UserService implements INgDataService<UserService> {
@Override
public Object getData(AjaxCall<?> call, AjaxResponse<?> response) {
return userRepository.findAll();
}
}
Angular Client (STOMP)
└─ ws://host/eventbus
├─ SEND /toBus/incoming → Event bus consumer
│ ├─ Deserialize WebSocketMessageReceiver
│ ├─ Dispatch to IWebSocketMessageReceiver (ajax/data/dataSend)
│ ├─ Run in CallScope (WebSocket)
│ └─ Reply via event bus:
│ ├─ dataReturns → publish to per-key addresses
│ ├─ sessionStorage → publish to "SessionStorage"
│ └─ localStorage → publish to "LocalStorage"
└─ SUBSCRIBE /toStomp/* ← Server pushes via StompEventBusPublisher
| Receiver | Action | Purpose |
|---|---|---|
| WebSocketAjaxCallReceiver | ajax | Deserializes AjaxCall, fires event, returns AjaxResponse |
| WebSocketDataRequestCallReceiver | data | Resolves INgDataService, calls getData() |
| WebSocketDataSendCallReceiver | dataSend | Resolves INgDataService, calls receiveData() |
| WSAddToGroupMessageReceiver | AddToWebSocketGroup | Adds session to WebSocket group |
| WSRemoveFromWebsocketGroupMessageReceiver | RemoveFromWebSocketGroup | Removes session from group |
| Setting | Value | Notes |
|---|---|---|
| WebSocket path | /eventbus | STOMP over WebSocket endpoint |
| Server heartbeat | 10000 ms | Server → client |
| Client heartbeat | 50000 ms | Client → server (lenient for background tabs) |
| Sub-protocols | v10.stomp, v11.stomp, v12.stomp | Advertised on HTTP upgrade |
| Idle timeout | 0 (disabled) | Relies on STOMP heartbeats |
NgIf<MyComponent> ifBlock = new NgIf<>(this)
.setCondition("isLoggedIn")
.add(new Paragraph<>().setText("Welcome!"));
NgIfElse<MyComponent> ifElse = new NgIfElse<>(this)
.setCondition("hasData")
.add(new Div<>().setText("Data loaded"))
.setElseBlock(new NgElse<>()
.add(new Div<>().setText("No data")));
Renders:
@if (isLoggedIn) {
<p>Welcome!</p>
}
@if (hasData) {
<div>Data loaded</div>
} @else {
<div>No data</div>
}
NgFor<MyComponent> forLoop = new NgFor<>(this)
.setIterable("users")
.setTrackBy("userId")
.add(new Div<>().setText("{{ user.name }}"));
Renders:
@for (user of users; track userId) {
<div>{{ user.name }}</div>
}
NgLet<MyComponent> letVar = new NgLet<>(this)
.setVariable("total")
.setExpression("calculateTotal()");
Renders:
@let total = calculateTotal();
Scans @NgRoutable classes and generates route tree:
@NgRoutable(path = "dashboard", parent = {AppComponent.class})
public class DashboardPage { }
@NgRoutable(path = "users", parent = {DashboardPage.class})
public class UsersPage { }
@NgRoutable(path = "profile/:id", parent = {DashboardPage.class})
public class ProfilePage { }
Generates:
RouterModule.forRoot([
{
path: 'dashboard',
component: DashboardComponent,
children: [
{ path: 'users', component: UsersComponent },
{ path: 'profile/:id', component: ProfileComponent }
]
}
])
RouterLink link = new RouterLink()
.setRouterLink("/dashboard")
.setText("Go to Dashboard");
RouterLink paramLink = new RouterLink()
.setRouterLink("/profile/123")
.setQueryParams(Map.of("tab", "settings"))
.setText("View Profile");
EnvironmentModule env = new EnvironmentModule()
.setOptions(new EnvironmentOptions()
.setProduction(false)
.setApiUrl("http://localhost:8080/api")
.addCustomProperty("featureFlags", Map.of(
"newUI", true,
"betaFeatures", false
)));
Generates:
export const environment = {
production: false,
apiUrl: 'http://localhost:8080/api',
featureFlags: {
newUI: true,
betaFeatures: false
}
};
| Route | Method | Handler | Purpose |
|---|---|---|---|
| /eventbus/* | WebSocket | STOMP server | WebSocket → STOMP bridge |
| /assets/* | GET | StaticHandler | Angular compiled assets (1-year cache) |
| /{file}.{ext} | GET | StaticHandler | Root-level static files |
| /** (SPA fallback) | GET | sendFile(index.html) | Angular Router routes |
| SPI | Purpose |
|---|---|
| AngularScanPackages | Add packages to Angular classpath scan |
| RenderedAssets | Provide additional build assets |
| NpmrcConfigurator | Customize .npmrc file |
| WebSocketGroupAdd | Custom logic for WebSocket group joins |
| TypescriptIndexPageConfigurator | Customize generated index.html |
| IWebSocketAuthDataProvider | Provide authentication data for WebSocket |
| Environment Variable | Default | Purpose |
|---|---|---|
| JWEBMP_PROCESS_ANGULAR_TS | false | Enable/disable TypeScript generation |
| jwebmp.outputDirectory | — | Override output directory |
| jwebmp | ~ (user home) | Base directory |
| ENVIRONMENT | dev | Runtime environment hint |
| PORT | 8080 | Server port |
The plugin manages Angular dependencies:
{
"dependencies": {
"@angular/core": "^20.0.0",
"@angular/common": "^20.0.0",
"@angular/router": "^20.0.0",
"@angular/forms": "^20.0.0",
"@stomp/ng2-stompjs": "^latest",
"rxjs": "^7.8.0"
}
}
@NgDataService
public class LiveDataService implements INgDataService<LiveDataService> {
@Inject
private DataRepository repository;
@Override
public Object getData(AjaxCall<?> call, AjaxResponse<?> response) {
String entityId = call.getParameters().get("id");
return repository.findById(entityId);
}
@Override
public void receiveData(AjaxCall<?> call, AjaxResponse<?> response) {
String data = call.getParameters().get("data");
repository.save(data);
// Push update to all clients in group
StompEventBusPublisher.publish("/toStomp/updates", data);
}
}
public class ButtonClickEvent extends OnClickAdapter {
@Override
public void onClick(AjaxCall<?> call, AjaxResponse<?> response) {
// Process server-side
String result = processData();
// Update UI
response.addComponent(new Div<>().setText("Result: " + result));
// Publish to WebSocket group
StompEventBusPublisher.publish("/toStomp/notifications", result);
}
}
@NgComponent("chat-room")
public class ChatRoom implements INgComponent<ChatRoom> {
@Override
public void configure(IComponentHierarchyBase<?, ?> component) {
// Component auto-joins WebSocket group
component.setAttribute("websocketgroup", "chat-room-1");
}
}
module com.jwebmp.core.angular {
requires transitive com.jwebmp.core.base.angular.client;
requires transitive com.jwebmp.vertx;
requires transitive io.vertx.eventbusbridge;
requires transitive io.vertx.stomp;
provides IGuicePreStartup with AngularPreStartup;
provides IGuicePostStartup with AngularTSPostStartup;
provides IGuiceModule with AngularTSSiteBinder;
provides VertxRouterConfigurator with AngularTSSiteBinder;
provides IWebSocketMessageReceiver with
WebSocketAjaxCallReceiver,
WebSocketDataRequestCallReceiver,
WebSocketDataSendCallReceiver,
WSAddToGroupMessageReceiver,
WSRemoveFromWebsocketGroupMessageReceiver;
}
AngularTSSiteBinder — Core module + router configurationTypeScriptCompiler — Orchestrates TS generationNGApplication — Base class for @NgAppAngularRoutingModule — Generates RouterModule.forRoot()EnvironmentModule — Generates environment configStompEventBusPublisher — Helper for STOMP publishing<dependency>
<groupId>com.jwebmp.plugins</groupId>
<artifactId>angular</artifactId>
</dependency>
com.jwebmp.core.angularPlugin generates TypeScript but does not run ng build. Build separately:
cd ~/.jwebmp/my-app
npm install
ng build
Dist output served automatically by Vert.x.
development
Install Codex skills into $CODEX_HOME/skills from a curated list or a GitHub repo path. Use when a user asks to list installable skills, install a curated skill, or install a skill from another repo (including private repos).
tools
Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Codex's capabilities with specialized knowledge, workflows, or tool integrations.
development
WebAwesome icon integration for JWebMP — modern, open-source icon library. Provides 1,500+ icons with solid/regular styles, sizing, rotation, animation, and CSS utilities. Drop-in FontAwesome alternative with fresh designs. Use when working with WebAwesome icons, modern icon designs, or as FontAwesome alternative in JWebMP applications.
development
WebAwesome Pro integration for JWebMP with premium icons and features. Extends jwebmp-webawesome with additional styles, premium icons, and advanced features. Use when working with WebAwesome Pro icons or premium WebAwesome features in JWebMP applications.