Skip to content

Commit dcf4650

Browse files
OS-jacobbellShaneK
andauthored
chore(angular): test schematics and code-splitting (#31401)
Issue number: internal --------- ## What is the new behavior? <!-- Please describe the behavior or changes that are being added by this PR. --> - New test project for validating Ionic components are split across chunks with the new per-component exports. - Script for testing schematics. - More exports from the standalone barrel file in `package.json`. - CI step for running `packages/angular` tests. ## Does this introduce a breaking change? - [ ] Yes - [X] No --------- Co-authored-by: Shane <shane@shanessite.net>
1 parent 8d41b5f commit dcf4650

36 files changed

Lines changed: 11758 additions & 21 deletions
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: 'Test Ionic Angular Package'
2+
description: 'Test Ionic Angular Package'
3+
runs:
4+
using: 'composite'
5+
steps:
6+
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
7+
with:
8+
node-version: 24.x
9+
- uses: ./.github/workflows/actions/download-archive
10+
with:
11+
name: ionic-core
12+
path: ./core
13+
filename: CoreBuild.zip
14+
- uses: ./.github/workflows/actions/download-archive
15+
with:
16+
name: ionic-angular
17+
path: ./packages/angular
18+
filename: AngularBuild.zip
19+
- name: 🕸️ Install Angular Dependencies
20+
run: npm ci
21+
shell: bash
22+
working-directory: ./packages/angular
23+
- name: 📐 Run Angular Package Tests
24+
run: npm run test
25+
shell: bash
26+
working-directory: ./packages/angular

.github/workflows/build.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,13 @@ jobs:
129129
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
130130
- uses: ./.github/workflows/actions/build-angular
131131

132+
test-angular-package:
133+
needs: [build-angular]
134+
runs-on: ubuntu-latest
135+
steps:
136+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
137+
- uses: ./.github/workflows/actions/test-angular-package
138+
132139
build-angular-server:
133140
needs: [build-core]
134141
runs-on: ubuntu-latest

docs/angular/change-detection.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,9 @@ const EAGER_COMPONENTS = {
4242

4343
Everything else takes the plain form: `changeDetection: ChangeDetectionStrategy.OnPush`, no eslint-disable, no script entry. See `packages/angular/src/standalone/navigation/nav.ts`, where the comment records why `ion-nav` stays `OnPush` despite hosting pages, and `packages/angular/src/lazy/directives/navigation/ion-nav.ts`, with the same comment.
4444

45-
The `npm run build` script enforces this in two steps:
45+
`npm run build.change-detection` (`packages/angular/scripts/normalize-change-detection.js`) rewrites Angular 22's emitted `ChangeDetectionStrategy.Eager` back to `Default`, since `Eager` only exists from Angular 21.2 onward and earlier linkers in the peer range reject it outright.
4646

47-
- The `build.change-detection` step (`packages/angular/scripts/normalize-change-detection.js`) rewrites Angular 22's emitted `ChangeDetectionStrategy.Eager` back to `Default`, since `Eager` only exists from Angular 21.2 onward and earlier linkers in the peer range reject it outright.
48-
- The `validate.change-detection` step (`packages/angular/scripts/verify-change-detection.js`) fails the build on a component with no strategy, a strategy name that won't link across the whole peer range, or a component going eager without being listed in `EAGER_COMPONENTS`.
47+
`npm run test.change-detection` (`packages/angular/scripts/verify-change-detection.js`) fails on a component with no strategy, a strategy name that won't link across the whole peer range, or a component going eager without being listed in `EAGER_COMPONENTS`.
4948

5049
If that check names a component from `packages/angular/src/lazy/directives/proxies.ts` or `packages/angular/src/standalone/directives/ion-*.ts`, don't edit those files. They are emitted by `@stencil/angular-output-target`, which hardcodes the strategy, so a failure there means the generator changed. Fix or pin that dependency in `core/package.json` instead. Those generated files are 158 of the 182 components and `packages/angular/eslint.config.js` ignores all of them, so lint can never see them, which is why this check exists.
5150

packages/angular/README.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,4 +117,24 @@ Ionic developers can access this by importing from `@ionic/angular/lazy`.
117117
118118
## Change Detection Strategy
119119
120-
Every `@Component` in `src` must declare `changeDetection` explicitly, and the build enforces it. See the [Change Detection guide](https://github.com/ionic-team/ionic-framework/blob/main/docs/angular/change-detection.md).
120+
Every `@Component` in `src` must declare `changeDetection` explicitly, and `npm run test` enforces it. See the [Change Detection guide](https://github.com/ionic-team/ionic-framework/blob/main/docs/angular/change-detection.md).
121+
122+
## Package Validation
123+
124+
`npm run validate` executes several subtasks: installs node modules, lints, builds the package, and runs package tests. `npm run test` can also run the package tests directly. For E2E tests, see [Angular Testing documentation](/docs/angular/testing.md).
125+
126+
### Testing Package Exports
127+
128+
To check that all exports from `package.json` point to files that exist, and that all Ionic components have exports, run `node ./scripts/verify-exports.js` or `npm run test.package`.
129+
130+
### Testing Code Splitting
131+
132+
If an app imports standalone components from `@ionic/angular`, esbuild bundles them together, so a landing page could include components it never uses. If components are instead imported from `@ionic/angular/<component-name>`, esbuild is able to bundle pages with only the components they need. The app in `packages/angular/test/code-split` is used to verify that this code splitting is working.
133+
134+
To run the test, run `node ./scripts/test-code-split.js` or run `npm run test.code-split`. This builds the code-split app and checks if `IonToggle` is excluded from the landing page's bundle.
135+
136+
### Testing Schematics
137+
138+
The schematics files are used when Ionic-Angular is added to a project with `ng add`. The schematics test verifies schematics are included in the package by creating a new starter app and adding the locally built Ionic-Angular package to it.
139+
140+
To run the test, run `node ./scripts/verify-schematics.js` or run `npm run test.schematics`.

packages/angular/package.json

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
},
2525
"homepage": "https://ionicframework.com/",
2626
"scripts": {
27-
"build": "npm run clean && npm run build.ng && npm run build.change-detection && npm run build.css && npm run build.schematics && npm run validate.package && npm run validate.change-detection",
27+
"build": "npm run clean && npm run build.ng && npm run build.change-detection && npm run build.css && npm run build.schematics",
2828
"build.css": "node scripts/build-css.js",
2929
"build.schematics": "node scripts/build-schematics.js",
3030
"build.ng": "ngc",
@@ -38,10 +38,12 @@
3838
"prerelease": "npm run validate && np prerelease --yolo --any-branch --tag next",
3939
"sync": "./scripts/sync.sh",
4040
"local.sync.and.pack": "./scripts/sync-and-pack.sh",
41-
"test": "echo 'angular no tests yet'",
42-
"validate": "npm i && npm run lint && npm run test && npm run build",
43-
"validate.package": "node scripts/verify-exports.js",
44-
"validate.change-detection": "node scripts/verify-change-detection.js"
41+
"test": "npm run test.package && npm run test.change-detection && npm run test.schematics && npm run test.code-split",
42+
"test.package": "node scripts/verify-exports.js",
43+
"test.change-detection": "node scripts/verify-change-detection.js",
44+
"test.schematics": "node ./scripts/verify-schematics.js",
45+
"test.code-split": "node ./scripts/test-code-split.js",
46+
"validate": "npm i && npm run lint && npm run build && npm run test"
4547
},
4648
"exports": {
4749
"./package.json": "./package.json",
@@ -143,6 +145,13 @@
143145
"./ion-title": "./dist/standalone/directives/ion-title.js",
144146
"./ion-toast": "./dist/standalone/directives/ion-toast.js",
145147
"./ion-toolbar": "./dist/standalone/directives/ion-toolbar.js",
148+
"./dom-controller": "./dist/common/providers/dom-controller.js",
149+
"./nav-controller": "./dist/common/providers/nav-controller.js",
150+
"./config": "./dist/common/providers/config.js",
151+
"./platform": "./dist/common/providers/platform.js",
152+
"./nav-params": "./dist/common/directives/navigation/nav-params.js",
153+
"./ion-modal-token": "./dist/standalone/providers/modal-token.js",
154+
"./ionic-route-strategy": "./dist/common/utils/routing.js",
146155
"./action-sheet-controller": "./dist/standalone/providers/action-sheet-controller.js",
147156
"./alert-controller": "./dist/standalone/providers/alert-controller.js",
148157
"./animation-controller": "./dist/standalone/providers/animation-controller.js",
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/**
2+
* Validates that per-component imports let a bundler code-split Ionic components.
3+
* Inspects `www/stats.json`, the esbuild metafile, to see which output chunk a
4+
* component landed in. `ion-toggle` is used by only the home page, so it
5+
* should not be bundled with the landing page.
6+
*
7+
* Build core and the angular package before running this test.
8+
*/
9+
10+
const fs = require('fs-extra');
11+
const path = require('path');
12+
const { execSync } = require('node:child_process');
13+
14+
const PROJECT_DIR = path.join(__dirname, '../test/code-split');
15+
const STATS_FILE = path.join(PROJECT_DIR, 'www/stats.json');
16+
17+
18+
function collectChunks(stats, startChunkName, chunkSet)
19+
{
20+
chunkSet.add(startChunkName);
21+
const chunk = stats.outputs[startChunkName]
22+
for (const imported of chunk.imports) {
23+
if (imported.kind === "import-statement" && !chunkSet.has(imported.path)) {
24+
collectChunks(stats, imported.path, chunkSet);
25+
}
26+
}
27+
}
28+
29+
function findChunksForPage(stats, page) {
30+
const chunkSet = new Set();
31+
32+
for (const [chunkName, chunkData] of Object.entries(stats.outputs || {})) {
33+
if (chunkName.endsWith('.js')) {
34+
const importFound = Object.keys(chunkData.inputs).some(input => input.endsWith(page));
35+
if (importFound) {
36+
collectChunks(stats, chunkName, chunkSet);
37+
}
38+
}
39+
}
40+
41+
return chunkSet;
42+
}
43+
44+
function hasComponentAsInput(stats, chunks, component) {
45+
for (const chunk of chunks) {
46+
for (const inputName of Object.keys(stats.outputs[chunk].inputs)) {
47+
if (inputName.endsWith(component)) {
48+
return true;
49+
}
50+
}
51+
}
52+
return false;
53+
}
54+
55+
function main() {
56+
try {
57+
execSync('npm i', { cwd: PROJECT_DIR });
58+
execSync('./sync.sh', { cwd: PROJECT_DIR });
59+
execSync(`npm run build`, { cwd: PROJECT_DIR, stdio: 'inherit' });
60+
61+
const stats = fs.readJsonSync(STATS_FILE);
62+
const landingPageChunks = findChunksForPage(stats, 'landing.page.ts');
63+
const homePageChunks = findChunksForPage(stats, 'home.page.ts');
64+
65+
if (!hasComponentAsInput(stats, landingPageChunks, 'ion-header.js')) {
66+
throw new Error(`ion-header was not included in landing page.`);
67+
}
68+
if (hasComponentAsInput(stats, landingPageChunks, 'ion-toggle.js')) {
69+
throw new Error(`ion-toggle was not split from landing page.`);
70+
}
71+
72+
if (!hasComponentAsInput(stats, homePageChunks, 'ion-header.js')) {
73+
throw new Error(`ion-header was not included in home page.`);
74+
}
75+
if (!hasComponentAsInput(stats, homePageChunks, 'ion-toggle.js')) {
76+
throw new Error(`ion-toggle was not included in home page.`);
77+
}
78+
79+
console.log('✅ verified code-split');
80+
} catch (error) {
81+
process.exitCode = 1;
82+
console.error(error);
83+
}
84+
}
85+
86+
main();

packages/angular/scripts/verify-exports.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ const KNOWN_EXCLUDED_COMPONENTS = [
1616
'ion-select-popover',
1717
'ion-slides',
1818
];
19+
const KNOWN_NON_CORE_ION_COMPONENTS = [
20+
'ion-modal-token',
21+
];
1922

2023
function getComponentsFromCore() {
2124
const componentsList = fs.readdirSync(CORE_COMPONENTS_DIR, { withFileTypes: true })
@@ -60,7 +63,7 @@ function verify() {
6063

6164
// Check for exports that don't have a corresponding component in core
6265
for (const exportName of Object.keys(ionExports)) {
63-
if (!coreComponents.has(exportName)) {
66+
if (!coreComponents.has(exportName) && !KNOWN_NON_CORE_ION_COMPONENTS.includes(exportName)) {
6467
console.log(`${exportName} is exported without a matching component in core.`);
6568
hasErrors = true;
6669
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
const fs = require('fs-extra');
2+
const path = require('path');
3+
const { execSync } = require('node:child_process');
4+
5+
const testName = 'schematics-test';
6+
const packageRootDir = path.join(__dirname, '..');
7+
const testDir = path.join(packageRootDir, testName);
8+
9+
try {
10+
// Delete old packages
11+
execSync(`rm -f *.tgz`, {cwd: packageRootDir});
12+
13+
// Pack ionic-core
14+
execSync(`npm pack ../../core`, {cwd: packageRootDir});
15+
16+
// Pack ionic-angular
17+
execSync(`npm pack`, {cwd: packageRootDir});
18+
19+
// Create new Angular project
20+
execSync(`npx ng new ${testName} --style css --ssr false --ai-config none`, {cwd: packageRootDir});
21+
22+
// Install ionic-angular and core packages
23+
execSync(`npx ng add --skip-confirmation ../ionic-angular-*`, {cwd: testDir});
24+
execSync(`npm install ../*.tgz --no-save`, {cwd: testDir});
25+
26+
// Run build
27+
execSync(`npm run build`, {cwd: testDir});
28+
} catch(error) {
29+
console.log(error);
30+
process.exitCode = 1;
31+
}
32+
33+
fs.removeSync(testDir);
34+
execSync(`rm -f *.tgz`, {cwd: packageRootDir});

packages/angular/src/standalone/index.ts

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,16 @@ export { MenuController } from './providers/menu-controller';
1414
export { ModalController } from './providers/modal-controller';
1515
export { PopoverController } from './providers/popover-controller';
1616
export { ToastController } from './providers/toast-controller';
17-
export {
18-
DomController,
19-
NavController,
20-
Config,
21-
Platform,
22-
NavParams,
23-
IonModalToken,
24-
IonicRouteStrategy,
17+
export { IonModalToken } from './providers/modal-token';
18+
export { DomController, NavController, Config, Platform, NavParams, IonicRouteStrategy } from '@ionic/angular/common';
19+
export type {
2520
ViewWillEnter,
2621
ViewDidEnter,
2722
ViewWillLeave,
2823
ViewDidLeave,
24+
ModalOptions,
25+
PopoverOptions,
2926
} from '@ionic/angular/common';
30-
export type { ModalOptions, PopoverOptions } from '@ionic/angular/common';
3127
export { IonNav } from './navigation/nav';
3228
export {
3329
IonCheckbox,
@@ -51,11 +47,13 @@ export {
5147
createGesture,
5248
iosTransitionAnimation,
5349
mdTransitionAnimation,
50+
IonicSafeString,
5451
IonicSlides,
5552
getPlatforms,
5653
isPlatform,
5754
getTimeGivenProgression,
58-
// TYPES
55+
} from '@ionic/core/components';
56+
export type {
5957
Animation,
6058
AnimationBuilder,
6159
AnimationCallbackOptions,
@@ -93,7 +91,6 @@ export {
9391
ItemReorderEventDetail,
9492
ItemReorderCustomEvent,
9593
ItemSlidingCustomEvent,
96-
IonicSafeString,
9794
LoadingOptions,
9895
MenuCustomEvent,
9996
ModalDragEventDetail,
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { IonModalToken } from '@ionic/angular/common';

0 commit comments

Comments
 (0)