Skip to content

Commit dc0e2f6

Browse files
authored
Merge pull request #2652 from intersective/angular-eos-upgrades-trunk
[CORE-8147] Angular eos upgrades trunk
2 parents f383b50 + 77c390f commit dc0e2f6

58 files changed

Lines changed: 381 additions & 1459 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

angular.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,9 +274,11 @@
274274
"test": {
275275
"builder": "@angular-devkit/build-angular:karma",
276276
"options": {
277+
"builderMode": "application",
277278
"main": "projects/request/src/test.ts",
278279
"karmaConfig": "projects/request/karma.conf.js",
279280
"tsConfig": "projects/request/tsconfig.spec.json",
281+
"include": ["projects/request/src/**/*.spec.ts"],
280282
"scripts": []
281283
}
282284
}

docs/upgrades/angular-18-to-19-security-fixes.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,37 @@ review and update as needed:
341341
- `Apollo.use()`, `Apollo.watchQuery()`, `Apollo.mutate()`, `Apollo.query()` signatures
342342
- check for changes in `ApolloModule.forRoot()` vs standalone provider pattern
343343

344+
### 5.5. Lambda@Edge forwarder — S3 path prefix (COMPLETED March 2026)
345+
346+
**Root cause:** The Angular 19 `application` builder (`@angular-devkit/build-angular ^19`) changed the build output structure by introducing a `browser/` subdirectory:
347+
348+
| Builder | Angular version | Output path | S3 key prefix |
349+
|---------|-----------------|-------------|---------------|
350+
| `browser` (webpack) | 17 / 18 | `dist/v3/{locale}/` | `/{locale}/` |
351+
| `application` (esbuild) | **19+** | `dist/v3/browser/{locale}/` | `/browser/{locale}/` |
352+
353+
This mismatch caused `lambda/forwarder/index.js` to rewrite all CloudFront URIs to non-existent S3 keys (e.g. `/en-US/index.html` instead of `/browser/en-US/index.html`). S3 returned `403 AccessDenied` — surfaced as raw XML to every user loading the app.
354+
355+
**Fix applied to `lambda/forwarder/index.js`:**
356+
357+
```javascript
358+
// before (Angular 17/18)
359+
request.uri = "/en-US/index.html"; // fallback
360+
request.uri = `/${locale}/index.html`; // spa route
361+
// (no static-asset rewrite needed — assets were at root)
362+
363+
// after (Angular 19+)
364+
request.uri = "/browser/en-US/index.html"; // fallback
365+
request.uri = `/browser/${locale}/index.html`; // spa route
366+
} else if (!request.uri.startsWith('/browser/')) {
367+
request.uri = `/browser${request.uri}`; // static assets
368+
}
369+
```
370+
371+
**Validation:** Confirmed working on `p2-stage` after deployment (March 2026).
372+
373+
**Future Angular upgrades:** check `ls dist/v3/` after building to verify the output structure before merging. If thenstructure changes again, update `forwarder/index.js` in the same PR. See `lambda/README.md` for the full dependency documentation.
374+
344375
---
345376

346377
## Phase 6 — Testing & Validation
@@ -446,6 +477,7 @@ npm audit > ./output/angular-19-upgrade-audit.log 2>&1
446477
| TypeScript 5.6 stricter checks | incremental fix of any new type errors |
447478
| ngx-quill v27 regressions | minimal risk — usually just peer dep bump |
448479
| CI build failures | run full CI pipeline before merging |
480+
| **Lambda@Edge S3 path mismatch** (**occurred** March 2026) | Angular 19 `application` builder adds `browser/` subdirectory; `lambda/forwarder/index.js` must be updated in the same PR — see §5.5 |
449481

450482
**Rollback plan:** revert to the pre-upgrade commit on `angular-eos-upgrades-prerelease` branch. all changes are confined to `package.json`, `package-lock.json`, and targeted source fixes.
451483

@@ -480,10 +512,13 @@ npm audit > ./output/angular-19-upgrade-audit.log 2>&1
480512
- [ ] update uppy service/component if API changed
481513
- [ ] update apollo-angular usage if API changed
482514
- [ ] fix any Angular 19 deprecation warnings
515+
- [x] update `lambda/forwarder/index.js` for `browser/` path prefix *(completed March 2026)*
483516
- [ ] **Phase 6:** Validation
484517
- [ ] `npm run prebuildv3` succeeds
485518
- [ ] `ng build v3` succeeds
486519
- [ ] `npm test` passes
487520
- [ ] `npm run lint` passes
488521
- [ ] `npm audit` shows reduced vulnerability count
489522
- [ ] manual testing on staging
523+
- [ ] verify app loads at `https://app.<stack>.practera.com/en-US` without S3 AccessDenied
524+
- [ ] verify deep-links with query params (magic-link login) resolve to correct locale

lambda/README.md

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,76 @@
11
### Description
22

3-
This directory will hold `lambda@edge` functions.
3+
This directory holds `lambda@edge` functions deployed to CloudFront as `origin-request` handlers.
44

5-
`forwarder` - lambda function that sits infront of the CDN, handles `globalization` redirection.
6-
`versioner` - function to create lambda function version.
5+
`forwarder` - rewrites incoming CloudFront URIs to the correct S3 object keys for the Angular i18n locale builds.
6+
`versioner` - creates a numbered Lambda version ARN that CloudFront requires for Lambda@Edge associations.
7+
8+
---
9+
10+
### i18n / Angular Build Output Dependency
11+
12+
The `forwarder` function is **tightly coupled** to the Angular build output directory structure. Breaking this coupling causes S3 `AccessDenied` (403) errors surfaced as raw XML to end users.
13+
14+
#### How Angular i18n builds are deployed
15+
16+
`projects/v3` is compiled with `"localize": true` in `angular.json`, producing one subfolder per locale:
17+
18+
| Angular version | Build output path | S3 object key prefix |
19+
|-----------------|-------------------|----------------------|
20+
| 17 / 18 (webpack builder) | `dist/v3/{locale}/` | `/{locale}/` |
21+
| **19+ (application builder)** | `dist/v3/browser/{locale}/` | `/browser/{locale}/` |
22+
23+
The `aws s3 sync dist/v3/ s3://$BUCKET --delete` step (CI/CD step 22) mirrors this structure directly into S3, so the S3 key prefix always matches the build output.
24+
25+
#### What the forwarder does
26+
27+
CloudFront does **not** forward query strings to S3 (`QueryString: false`). When a user visits a deep-link such as `/en-US?auth_token=…`, CloudFront calls the forwarder with `request.uri = "/en-US"`. The forwarder:
28+
29+
1. Extracts the first path segment as the locale (`en-US`, `ja`, `ms`, `es`).
30+
2. For unknown locales or bare `/`, falls back to the default locale.
31+
3. For SPA routes (no file extension), rewrites to `/{prefix}/{locale}/index.html`.
32+
4. For static assets (with file extension), rewrites to `/{prefix}{original_uri}`.
33+
5. Passes `version.json` requests through unchanged (whitelist).
34+
35+
#### Supported locales
36+
37+
The locale whitelist in `forwarder/index.js` must stay in sync with the locales configured in `angular.json`:
38+
39+
```javascript
40+
const locales = ["en-US", "ja", "ms", "es"];
41+
```
42+
43+
To add a new locale: update both `angular.json` (`i18n.locales`) **and** the `locales` array in `forwarder/index.js`.
44+
45+
#### ⚠️ What to check on every Angular major version upgrade
46+
47+
When upgrading Angular major versions, verify the build output structure has not changed before deploying:
48+
49+
```bash
50+
# after building, check what subdirectories are produced
51+
ls dist/v3/
52+
# Angular 19+: should show browser/
53+
# Angular 17/18: should show en-US/ ja/ ms/ es/
54+
```
55+
56+
If the output structure changes, update the path prefix in `forwarder/index.js` **before** merging to trunk so both changes deploy together in the same CI/CD run.
57+
58+
#### Incident history
59+
60+
| Date | Trigger | Symptom | Fix |
61+
|------|---------|---------|-----|
62+
| March 2026 | Angular 18 → 19 upgrade | S3 `AccessDenied` XML shown to all users on `app.p2-stage.practera.com` | Added `/browser/` prefix to all rewritten URIs in `forwarder/index.js` |
63+
64+
---
765

866
### Deployment
967

10-
Once `AWS` credentials is ready, just run `deploy.sh`. Make sure you installed [sam](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html) on your machine.
68+
Once `AWS` credentials are ready, run `deploy.sh`. Requires [AWS SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html).
69+
70+
In CI/CD (`p2-stage-appv3.yml`) the deploy sequence is:
71+
1. Build Angular app (`ng build v3`)
72+
2. Deploy Lambda@Edge (`bash lambda/deploy.sh`) → exports `HandlerVersionArn`
73+
3. Deploy CloudFormation/Serverless stack (picks up new `HandlerVersionArn`)
74+
4. Sync `dist/v3/` to S3
75+
76+
Both the Lambda function and the S3 content must reflect the same output path structure.

lambda/forwarder/index.js

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
const path = require('path');
22

3-
exports.handler = async (evt, context, cb) => {
3+
exports.handler = async (evt) => {
44
const { request } = evt.Records[0].cf;
55

66
console.log(`Original Uri: ${request.uri}`);
@@ -11,26 +11,27 @@ exports.handler = async (evt, context, cb) => {
1111
const locales = ["en-US", "ja", "ms", "es"];
1212
const lastPartUrl = uriParts[uriParts.length - 1];
1313

14-
// whitelisted version.json request
14+
// whitelisted version.json request — note: query strings are in request.querystring,
15+
// not in request.uri, so only the path filename is tested here
1516
console.log("trailingURL::", lastPartUrl);
16-
if (lastPartUrl.match(/^version\.json(?:\?t=\d+)?$/) !== null) {
17-
return cb(null, request);
17+
if (lastPartUrl.match(/^version\.json$/) !== null) {
18+
return request;
1819
}
1920

2021
if (locale === "" || locale === "index.html" || !locales.includes(locale)) {
2122
request.uri = "/browser/en-US/index.html";
2223
console.log("Go to default page and locale.");
23-
return cb(null, request);
24+
return request;
2425
}
2526

2627
const fileExt = path.extname(lastPartUrl);
2728
if (!fileExt) {
2829
request.uri = `/browser/${locale}/index.html`;
2930
} else if (!request.uri.startsWith('/browser/')) {
30-
// rewrite static asset paths to match Angular 17+ application builder output
31+
// rewrite static asset paths to match Angular 19+ application builder output
3132
request.uri = `/browser${request.uri}`;
3233
}
3334

3435
console.log(`New Uri: ${request.uri}`);
35-
return cb(null, request);
36+
return request;
3637
};

lambda/forwarder/template.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ Resources:
3737
Properties:
3838
Handler: index.handler
3939
CodeUri: ./bin/handler.zip
40-
Runtime: nodejs18.x
40+
Runtime: nodejs22.x
4141
Timeout: 10
4242
Role: !GetAtt HandlerRole.Arn
4343
HandlerVersion:
@@ -53,4 +53,4 @@ Outputs:
5353
Description: "Arn Version for Lambda function to associate unto CDN"
5454
Value: !GetAtt HandlerVersion.FunctionArn
5555
Export:
56-
Name: !Sub "${StackName}-HandlerVersion-${Env}"
56+
Name: !Sub "${StackName}-HandlerVersion-${Env}"

lambda/versioner/template.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ Resources:
1919
Properties:
2020
Handler: index.handler
2121
CodeUri: ./bin/handler.zip
22-
Runtime: nodejs18.x
22+
Runtime: nodejs22.x
2323
Timeout: 10
2424
Role: !GetAtt LambdaVersionHelperRole.Arn
2525
LambdaVersionHelperRole:
@@ -51,4 +51,4 @@ Outputs:
5151
Description: "Lambda Function Versioner ARN"
5252
Value: !GetAtt Versioner.Arn
5353
Export:
54-
Name: !Sub "${StackName}-LambdaVersionerArn-${Env}"
54+
Name: !Sub "${StackName}-LambdaVersionerArn-${Env}"

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,6 @@
6161
"core-js": "^3.21.1",
6262
"dayjs": "^1.11.10",
6363
"exif-js": "^2.3.0",
64-
"filestack-js": "^3.30.0",
6564
"franc-min": "^6.2.0",
6665
"graphql": "^16.8.1",
6766
"ics": "^3.7.2",

projects/request/karma.conf.js

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,8 @@ module.exports = function (config) {
1414
],
1515
client: {
1616
jasmine: {
17-
// you can add configuration options for Jasmine here
18-
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
19-
// for example, you can disable the random execution with `random: false`
20-
// or set a specific seed with `seed: 4321`
17+
random: false,
18+
timeoutInterval: 10000,
2119
},
2220
clearContext: false // leave Jasmine Spec Runner output visible in browser
2321
},
@@ -37,8 +35,11 @@ module.exports = function (config) {
3735
colors: true,
3836
logLevel: config.LOG_INFO,
3937
autoWatch: true,
40-
browsers: ['Chrome'],
38+
browsers: ['ChromeHeadless'],
4139
singleRun: false,
42-
restartOnFileChange: true
40+
restartOnFileChange: true,
41+
browserNoActivityTimeout: 120000,
42+
browserDisconnectTimeout: 30000,
43+
browserDisconnectTolerance: 3,
4344
});
4445
};

projects/request/ng-package.json

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,5 @@
33
"dest": "../../dist/request",
44
"lib": {
55
"entryFile": "src/public-api.ts"
6-
},
7-
"allowedNonPeerDependencies": [
8-
"lodash"
9-
]
6+
}
107
}

projects/request/package.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,10 @@
22
"name": "request",
33
"version": "0.0.1",
44
"peerDependencies": {
5-
"@angular/common": "^17.0.0 || ^18.0.0 || ^19.0.0",
6-
"@angular/core": "^17.0.0 || ^18.0.0 || ^19.0.0"
5+
"@angular/common": "^18.0.0 || ^19.0.0",
6+
"@angular/core": "^18.0.0 || ^19.0.0"
77
},
88
"dependencies": {
9-
"lodash-es": "^4.17.21",
109
"tslib": "^2.3.0"
1110
}
1211
}

0 commit comments

Comments
 (0)