Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 131 additions & 0 deletions .github/instructions/bridge.instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
---
applyTo: "packages/angular-sdk-components/src/lib/_bridge/**"
description: "Use when modifying the PConnect bridge layer. Covers AngularPConnectService flow, SdkComponentMap, ComponentMapperComponent, and Redux store subscription patterns."
---
# PConnect Bridge Architecture

This directory is the SDK's integration layer that maps the PConnect component tree (provided by `@pega/constellationjs`) to Angular SDK components. The engine decides **what** to render; this bridge decides **how** to render it.

`@pega/constellationjs` provides: `PCore` (global API), `PConnect` (per-component API), and the Redux store (`PCore.getStore()`). The bridge consumes these to wire SDK components into the engine's component tree.

## Files

| File | Responsibility |
|------|---------------|
| `angular-pconnect.ts` | Injectable service that manages store subscriptions, component registration, prop comparison, action wiring, and form field lifecycle |
| `component-mapper/component-mapper.component.ts` | Dynamic component renderer — resolves component names to Angular component classes and creates them via `ViewContainerRef` |
| `helpers/sdk_component_map.ts` | Singleton component registry — maps names to Angular component classes |
| `helpers/sdk-pega-component-map.ts` | Pega-provided component registry (master map of all SDK components) |

## How AngularPConnectService Works

1. **Store subscription**: `subscribeToStore()` subscribes to `PCore.getStore()` with a wrapped callback that fires `onStateChange()` on the component
2. **Component registration**: `registerAndSubscribeComponent()` assigns a unique componentID, subscribes to the store, processes actions, registers form field, and returns an `AngularPConnectData` object with `compID`, `unsubscribeFn`, `validateMessage`, and `actions`
3. **Prop comparison**: `shouldComponentUpdate()` resolves current config props via `getComponentProps()`, deep-compares against previous props using `fast-deep-equal`, and returns `true` if the component should re-render
4. **Action wiring**: `processActions()` sets up `onChange` → `changeHandler` and `onBlur` → `eventHandler` on the PConnect node via `setAction()` (only for editable fields)
5. **Form field lifecycle**: `addFormField()` on registration, `removeFormField()` + context tree node removal on unsubscribe
6. **Validation**: Updates `angularPConnectData.validateMessage` from resolved props and triggers error/spinner messaging

```
Component ngOnInit()
→ registerAndSubscribeComponent(this, this.onStateChange)
→ processActions() sets onChange/onBlur
→ subscribeToStore() registers Redux listener
→ addFormField() registers in engine's form context
→ returns { compID, unsubscribeFn, validateMessage, actions }

Store changes → onStateChange() callback
→ checkAndUpdate()
→ shouldComponentUpdate(this)
→ getComponentProps() resolves configProps + additionalProps
→ deep compare against previous props
→ updates componentPropsArr[compID]
→ updates validateMessage
→ returns true/false
→ if true → updateSelf() (component-specific rendering logic)

Component ngOnDestroy()
→ unsubscribeFn()
→ removeFormField()
→ removeFieldNode/removeViewNode from context tree
→ store.unsubscribe()
```

### Key exports from angular-pconnect.ts
- `AngularPConnectService` — injectable service (`providedIn: 'root'`)
- `AngularPConnectData` — interface for the data returned by registration: `{ compID, unsubscribeFn, validateMessage, actions }`

### AngularPConnectService method summary
- **`registerAndSubscribeComponent(inComp, inCallback)`** — Main entry point: registers component, subscribes to store, wires actions, returns `AngularPConnectData`
- **`shouldComponentUpdate(inComp)`** — Returns `true` if props changed (component should re-render)
- **`getComponentID(inComp)`** — Returns the component's unique bridge ID
- **`getComponentProp(inComp, propName)`** — Returns a specific resolved prop value
- **`getCurrentCompleteProps(inComp)`** — Returns all current resolved props
- **`changeHandler(inComp, event)`** — Delegates to `pConn$.getActionsApi().changeHandler()`
- **`eventHandler(inComp, event)`** — Delegates to `pConn$.getActionsApi().eventHandler()`
- **`getStore()`** — Returns `PCore.getStore()` (cached)
- **`getState()`** — Returns current Redux state

## ComponentMapperComponent (component-mapper/)

Dynamic component renderer that creates Angular components at runtime:

1. **Resolution**: `getComponentFromMap(name)` looks up the component class from the registry
2. **Creation**: `ViewContainerRef.createComponent(component)` dynamically instantiates it
3. **Input binding**: `bindInputProps()` iterates over `props` object and calls `componentRef.setInput(key, value)` for each
4. **Output binding**: `bindOutputEvents()` subscribes to component `@Output()` EventEmitters
5. **Change detection**: `ngOnChanges()` reloads on name change, rebinds inputs on prop changes
6. **Error fallback**: If component not found, renders `ErrorBoundaryComponent`

```html
<!-- Usage in templates -->
<component-mapper name="TextInput" [props]="{ pConn$, formGroup$ }"></component-mapper>
<component-mapper name="View" [props]="{ pConn$: childPConn, formGroup$ }"></component-mapper>
```

### Inputs
- `name` — Component name as registered in the component map (e.g., `'TextInput'`, `'CaseView'`)
- `props` — Object of inputs to pass to the dynamically created component
- `errorMsg` — Error message for ErrorBoundary fallback
- `outputEvents` — Object mapping output event names to callback functions
- `parent` — Parent component reference (required when `outputEvents` is provided)

## SdkComponentMap (helpers/sdk_component_map.ts)

Singleton pattern with two component maps:

| Map | Source | Priority |
|-----|--------|----------|
| `localComponentMap` | `sdk-local-component-map.ts` | **Checked first** — consumer-side overrides |
| `pegaProvidedComponentMap` | `sdk-pega-component-map.ts` | Fallback — SDK's master component registry |

### Initialization
```typescript
// Called once during app startup (in FullPortal/Embedded component)
const theMap = await getSdkComponentMap(localSdkComponentMap);
```

### Component Lookup
```typescript
// Used by ComponentMapperComponent to resolve each component name
const Component = getComponentFromMap('TextInput');
// Resolution order: localComponentMap → pegaProvidedComponentMap → ErrorBoundary
```

### Key exports
- `SdkComponentMap` — The singleton instance (available after initialization)
- `getSdkComponentMap(localMap)` — Async factory; creates and initializes the singleton
- `getComponentFromMap(name)` — Synchronous lookup; returns Angular component class or ErrorBoundary

## Rules for Modifying Bridge Code

- **Do NOT create a separate Redux store** — `PCore.getStore()` IS the store
- **Do NOT bypass `ComponentMapperComponent`** for rendering PConnect-driven children — always use `<component-mapper>`
- **Do NOT bypass `AngularPConnectService`** for state management — all components must register/subscribe through it
- **Component map priority is intentional** — local always overrides Pega-provided
- **The bridge does NOT contain business logic** — it's purely a mapping/wiring/lifecycle layer
- **`SdkComponentMap` is a singleton** — only one instance exists per app lifecycle
- **Form field lifecycle is critical** — `addFormField` on init and `removeFormField` + context tree cleanup on destroy prevents 400 errors from stale field references
- **The `shouldComponentUpdate` deep comparison is intentional for performance** — do not replace with simple reference equality
- **`forwardRef(() => ComponentMapperComponent)`** is required in component imports to avoid circular dependencies
- **The `processActions` binding only applies to editable fields** — `isEditable()` guards this
148 changes: 148 additions & 0 deletions .github/instructions/build-scripts.instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
---
applyTo: "scripts/**,angular.json,tsconfig*.json,packages/angular-sdk-components/ng-package.json"
description: "Use when modifying build scripts, Angular workspace config, or TypeScript config. Covers build pipeline flow, script purposes, and packaging."
---
# Build Scripts

Node.js automation scripts for building and packaging the Angular SDK.

## Scripts Overview

| Script | When Called | Purpose |
|--------|------------|---------|
| `build-overrides.js` | `build-overrides` | Generates the `@pega/angular-sdk-overrides` package by copying components from `_components/` and rewriting relative imports to `@pega/angular-sdk-components` |
| `compress-with-assets.mjs` | `compress-angularsdk` (prod build) | Brotli + gzip compresses all JS, CSS, HTML files in `dist/` |
| `copy-map.js` | `build-angular-sdk-components` | Copies `sdk-local-component-map.ts` from package source to `dist/angular-sdk-components/` |
| `copy-npm-assets-to-components.js` | `build-angular-sdk-components` | Copies SECURITY.md, LICENSE, doc/ to `dist/angular-sdk-components/` |
| `copy-npm-assets-to-overrides.js` | `postbuild-overrides` | Copies SECURITY.md, LICENSE to `packages/angular-sdk-overrides/` |
| `copy-file.js` | — | Generic file copy utility used by other scripts |
| `extra-webpack.config.js` | Angular CLI build (via `@angular-builders/custom-webpack`) | Copies OAuth `auth.html` and `authDone.js` from `@pega/auth` into `dist/` |
| `update-dependencies.js` | `create_and_install_sdk_packages` | Builds both packages, creates `.tgz` files, and installs them into the `angular-sdk` consumer repo |
| `playwright-message.js` | `pretest` (before E2E) | Prints "Running in headless mode" info message |

## Build Pipeline Flow

### `npm run build-angular-sdk-components` (Library package build)
```
1. ng build angular-sdk-components
→ ng-packagr reads ng-package.json
→ entry file: src/public-api.ts
→ output: dist/angular-sdk-components/
2. node scripts/copy-map.js
→ copies sdk-local-component-map.ts to dist/
3. node scripts/copy-npm-assets-to-components.js
→ copies SECURITY.md, LICENSE, doc/ to dist/
```

### `npm run build:dev` (Development app build)
```
parallel (run-p):
- lint (eslint + prettier)
- build-angularsdk:
1. shx rm -rf ./dist
2. ng build --configuration development angular-test-app
3. copy-index → copies index.html to portal.html, fullportal.html,
embedded.html, mashup.html, simpleportal.html
4. make-mashup-dir → creates dist/constellation/prerequisite/
and dist/constellation/assets/icons/
```

### `npm run build:prod` (Production app build)
```
parallel (run-p):
- lint (eslint + prettier)
- prod-build-angularsdk:
1. shx rm -rf ./dist
2. ng build --configuration production angular-test-app
3. copy-index → copies index.html to route-specific HTML files
4. make-mashup-dir → creates mashup directory structure
5. compress-angularsdk → brotli + gzip all JS/CSS/HTML in dist/
```

### `npm run build-overrides` (Override package build)
```
prebuild-overrides:
1. shx rm -rf ./packages/angular-sdk-overrides/lib
2. shx cp -r ./packages/angular-sdk-components/src/lib/_components
→ packages/angular-sdk-overrides/lib

build-overrides:
3. node scripts/build-overrides.js
→ recursively processes all .ts files in overrides/lib/
→ rewrites relative imports (../) to '@pega/angular-sdk-components'

postbuild-overrides:
4. node scripts/copy-npm-assets-to-overrides.js
→ copies SECURITY.md, LICENSE
```

### `npm run build-sdk` (TypeScript compilation)
```
prebuild-sdk:
1. delete-tsbuildinfo → removes stale .tsbuildinfo files
2. clear-lib → rm -rf projects/angular-test-app/lib
3. clear-overrides → rm -rf packages/angular-sdk-overrides/lib

build-sdk:
4. ngc -p tsconfig.build.json → Angular compiler (TypeScript + templates)
```

### `npm run create_and_install_sdk_packages` (Cross-repo install)
```
1. Prompts for angular-sdk project path
2. Builds angular-sdk-components (ng build)
3. Creates .tgz via npm pack
4. Copies .tgz to angular-sdk project
5. Installs it via npm install <tgz>
6. Repeats for angular-sdk-overrides
```

## build-overrides.js Details

This script makes the overrides package consumable as a separate npm package:
1. Components are already copied from `src/lib/_components/` into `packages/angular-sdk-overrides/lib/` (by `prebuild-overrides`)
2. The script recursively scans all `.ts` files in the overrides directory
3. For each file, it finds `import` statements with relative paths (`../`)
4. Rewrites those paths to `@pega/angular-sdk-components` so the overrides package depends on the published SDK package rather than relative file paths

Example transform:
```typescript
// Before (relative path in source)
import { FieldBase } from '../../field.base';
// After (package reference in overrides)
import { FieldBase } from '@pega/angular-sdk-components';
```

## Angular-Specific Build Details

### ng-packagr (Library builds)
The component library uses **ng-packagr** (not Webpack) for building:
- Config: `packages/angular-sdk-components/ng-package.json`
- Entry point: `src/public-api.ts` — all public exports must be listed here
- Output: `dist/angular-sdk-components/` (FESM bundles + typings)
- Builder: `@angular-devkit/build-angular:ng-packagr` (configured in `angular.json`)

### Angular CLI (App builds)
The test app uses Angular CLI with `@angular-builders/custom-webpack`:
- Extends standard Angular build with `extra-webpack.config.js`
- The custom webpack config only adds `CopyWebpackPlugin` for OAuth auth files
- Dev server: `ng serve --port 3500`
- Two Angular projects in workspace: `angular-sdk-components` (library) and `angular-test-app` (application)

### Key differences from the React build
| Concern | React | Angular |
|---------|-------|---------|
| Library bundler | TypeScript compiler (`tsc`) | ng-packagr (FESM bundles) |
| App bundler | Webpack | Angular CLI (esbuild/webpack) |
| Export generation | `build-exports.js` auto-generates | Manual — `public-api.ts` must be edited |
| Component map transform | `edit-pega-components-map-in-lib.js` | Not needed (ng-packagr handles re-exports) |

## Key Points

- Scripts are Node.js (CommonJS, `require`) — not TypeScript (except `compress-with-assets.mjs` which is ESM)
- `shx` is used in npm scripts for cross-platform shell commands (cp, rm, mkdir)
- The override build copies source `.ts` files, not compiled output — customers modify TypeScript directly
- Do NOT edit files in `dist/` manually — they are regenerated by builds
- `public-api.ts` is the sole entry point for the library — if a component isn't exported there, it won't be in the package
- `angular.json` defines both projects — changes to build config go there, not in scripts
- The `copy-index` step creates route-specific HTML files so the Angular router works when accessed directly (e.g., `/portal`, `/embedded`)
Loading
Loading