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
52 changes: 52 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -198,3 +198,55 @@ jobs:

---
<sub>🔄 Updated automatically on each push to this PR</sub>

- name: Download feature bundle baseline
uses: dawidd6/action-download-artifact@v3
with:
workflow: publish.yml
branch: main
name: bundle-feature-baseline
path: e2e/bundle-check-app
if_no_artifact_found: warn
continue-on-error: true

- name: Run feature bundle check
id: bundle-features
run: |
pnpm nx nxBundle bundle-check-app --skip-sync --no-agents
{
echo 'report<<BUNDLE_FEATURE_EOF'
cat e2e/bundle-check-app/dist/bundle-feature-report.md
echo 'BUNDLE_FEATURE_EOF'
} >> "$GITHUB_OUTPUT"

- name: Find feature bundle comment
id: find-feature-comment
uses: peter-evans/find-comment@v4
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: 'github-actions[bot]'
body-includes: <!-- bundle-feature-check -->

- name: Create or update feature bundle comment
uses: peter-evans/create-or-update-comment@v5
with:
comment-id: ${{ steps.find-feature-comment.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}
edit-mode: replace
body: |
<!-- bundle-feature-check -->
## Tree-shaken Feature Bundle Sizes

Minified + gzip level-9 cost of each SDK feature in isolation, as a consumer would receive it.

${{ steps.bundle-features.outputs.report }}

<details>
<summary>How these are measured</summary>

Each fixture imports a single feature and is bundled with Rollup + esbuild + terser (full minification, ESM, tree-shaking on). Numbers reflect what a consumer ships and what their users download, not the raw dist size.

</details>

---
<sub>Updated automatically on each push to this PR</sub>
10 changes: 10 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,16 @@ jobs:
path: previous_sizes.json
retention-days: 30

- name: Run feature bundle check
run: pnpm nx nxBundle bundle-check-app --skip-sync

- name: Upload feature bundle baseline
uses: actions/upload-artifact@v5
with:
name: bundle-feature-baseline
path: e2e/bundle-check-app/dist/bundle-feature-baseline.json
retention-days: 30
Comment on lines +99 to +104
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

For actions/upload-artifact@v5, what is the default behavior of if-no-files-found, and does it fail the workflow by default?

💡 Result:

The default behavior of if-no-files-found for actions/upload-artifact@v5 is 'warn', which outputs a warning but does not fail the action or the workflow.[1][2][3][4] It does not fail the workflow by default; the action succeeds with a warning when no files are found.[1][5]

Citations:


Add if-no-files-found: error to fail fast when the feature baseline file is missing.

The default if-no-files-found behavior is warn, which allows the action to succeed silently if the baseline JSON is absent. This breaks the CI invariant that the baseline must exist for downstream comparisons. Setting it to error ensures the workflow fails immediately if the file is not generated.

Proposed change
       - name: Upload feature bundle baseline
         uses: actions/upload-artifact@v5
         with:
           name: bundle-feature-baseline
           path: e2e/bundle-check-app/dist/bundle-feature-baseline.json
+          if-no-files-found: error
           retention-days: 30
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/publish.yml around lines 99 - 104, The upload-artifact
step named "Upload feature bundle baseline" currently relies on the default
if-no-files-found behavior (warn) which can let the job pass when
e2e/bundle-check-app/dist/bundle-feature-baseline.json is missing; update the
actions/upload-artifact@v5 step (the block with name: Upload feature bundle
baseline and name: bundle-feature-baseline) to include the input
if-no-files-found: error so the workflow fails fast when the baseline JSON is
not present.


snapshot:
# Guard against publishing snapshots from the protected release branch.
if: >-
Expand Down
31 changes: 31 additions & 0 deletions e2e/bundle-check-app/fixtures/davinci-client-flow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { davinci } from '@forgerock/davinci-client';

const client = await davinci({
config: {
serverConfig: { wellknown: 'https://example.com/.well-known/openid-configuration' },
clientId: 'test-client',
redirectUri: 'https://example.com/callback',
scope: 'openid profile',
},
});

let node = await client.start();

// Walk the flow until it reaches a terminal node
while (node.status === 'continue') {
for (const collector of node.collectors) {
if (collector.category === 'SingleValueCollector' && collector.type === 'TextCollector') {
client.update(collector)('test-value');
}
if (collector.category === 'SingleValueCollector' && collector.type === 'PasswordCollector') {
client.update(collector)('test-password');
}
}
node = await client.next();
}

if (node.status === 'success') {
console.log('Login successful', node.session);
} else if (node.status === 'error' || node.status === 'failure') {
console.error('Login failed', node.error);
}
13 changes: 13 additions & 0 deletions e2e/bundle-check-app/fixtures/davinci-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { davinci } from '@forgerock/davinci-client';

const client = await davinci({
config: {
serverConfig: { wellknown: 'https://example.com/.well-known/openid-configuration' },
clientId: 'test-client',
redirectUri: 'https://example.com/callback',
scope: 'openid profile',
},
});

const node = await client.start();
console.log(node);
20 changes: 20 additions & 0 deletions e2e/bundle-check-app/fixtures/device-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { deviceClient } from '@forgerock/device-client';

const client = deviceClient({
serverConfig: {
baseUrl: 'https://example.com/am',
},
realmPath: 'root',
});

// Retrieve OATH (TOTP/HOTP) devices for a user
const oathDevices = await client.oath.get({
userId: 'user@example.com',
});
console.log(oathDevices);

// Retrieve Push notification devices
const pushDevices = await client.push.get({
userId: 'user@example.com',
});
console.log(pushDevices);
13 changes: 13 additions & 0 deletions e2e/bundle-check-app/fixtures/effect-barrel-option.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Same usage but imports from the top-level 'effect' barrel instead of 'effect/Option'
// Tests whether the barrel import prevents tree-shaking vs the subpath
import { Option, pipe } from 'effect';

const result = pipe(
Option.fromNullable(Math.random() > 0.5 ? 'hello' : null),
Option.match({
onNone: () => 'none',
onSome: (v) => v.toUpperCase(),
}),
);

console.log(result);
14 changes: 14 additions & 0 deletions e2e/bundle-check-app/fixtures/effect-subpath-option.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Imports Option via the subpath: effect/Option
// Tests whether Rollup tree-shakes to only fromNullable + match
import * as Option from 'effect/Option';
import { pipe } from 'effect/Function';

const result = pipe(
Option.fromNullable(Math.random() > 0.5 ? 'hello' : null),
Option.match({
onNone: () => 'none',
onSome: (v) => v.toUpperCase(),
}),
Comment on lines +3 to +11
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

In Rollup tree-shaking, does import * as X from "module"generally retain more code thanimport { a, b } from "module"when onlyaandb are used?

💡 Result:

Yes, in Rollup tree-shaking, import * as X from "module" generally retains more code than import { a, b } from "module" when only a and b are used.[1][2][3] Rollup's tree-shaking analyzes static ES module imports to include only used exports.[4][1] Named imports like { a, b } explicitly mark only those exports as used, allowing Rollup to eliminate unused code from "module".[1] Namespace imports (import * as X) create a namespace object containing all exports.[5][2] While recent improvements (e.g., PR #6230) enable partial tree-shaking by including only accessed properties like X.a and X.b in some cases,[2] it generally retains more code than named imports for several reasons: - Namespace creation may include additional logic or properties, especially with re-exports or dynamic access.[2][6] - Property access X.a can trigger side-effect assumptions (e.g., getters), potentially preserving more.[7][8] - Historical bugs and complexities with namespaces (e.g., issues #1951, #4751) show less optimal shaking compared to direct named imports.[9][10] - General bundler behavior and StackOverflow consensus note namespace imports hinder optimal tree-shaking due to dynamic-like access.[3][11] Official docs emphasize named imports for minimal bundles.[1] For best tree-shaking when using only specific exports, prefer named imports.[1][2]

Citations:


🏁 Script executed:

# Check if the file exists and view its current content
cat -n e2e/bundle-check-app/fixtures/effect-subpath-option.ts

Repository: ForgeRock/ping-javascript-sdk

Length of output: 559


🏁 Script executed:

# List other fixture files to see if there's a pattern
ls -la e2e/bundle-check-app/fixtures/

Repository: ForgeRock/ping-javascript-sdk

Length of output: 1376


🏁 Script executed:

cat -n e2e/bundle-check-app/fixtures/effect-barrel-option.ts

Repository: ForgeRock/ping-javascript-sdk

Length of output: 556


Use named imports to ensure accurate tree-shaking measurements for this subpath fixture.

The file's comment states it tests whether Rollup tree-shakes to only fromNullable and match. Using import * as Option creates a namespace object, which Rollup's tree-shaker retains more code for compared to explicit named imports, making this fixture less reliable for measuring subpath import effectiveness. Named imports allow the bundler to precisely identify and include only the used symbols.

Proposed change
-import * as Option from 'effect/Option';
+import { fromNullable, match } from 'effect/Option';
 import { pipe } from 'effect/Function';
 
 const result = pipe(
-  Option.fromNullable(Math.random() > 0.5 ? 'hello' : null),
-  Option.match({
+  fromNullable(Math.random() > 0.5 ? 'hello' : null),
+  match({
     onNone: () => 'none',
     onSome: (v) => v.toUpperCase(),
   }),
 );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import * as Option from 'effect/Option';
import { pipe } from 'effect/Function';
const result = pipe(
Option.fromNullable(Math.random() > 0.5 ? 'hello' : null),
Option.match({
onNone: () => 'none',
onSome: (v) => v.toUpperCase(),
}),
import { fromNullable, match } from 'effect/Option';
import { pipe } from 'effect/Function';
const result = pipe(
fromNullable(Math.random() > 0.5 ? 'hello' : null),
match({
onNone: () => 'none',
onSome: (v) => v.toUpperCase(),
}),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/bundle-check-app/fixtures/effect-subpath-option.ts` around lines 3 - 11,
The test fixture currently imports the entire Option namespace which prevents
precise tree-shaking; replace the namespace import with explicit named imports
for the used symbols (fromNullable and match) by updating the import of Option
to import { fromNullable, match } from 'effect/Option' and then update usages in
the pipe (Option.fromNullable → fromNullable, Option.match → match) so Rollup
can accurately include only those symbols.

);

console.log(result);
14 changes: 14 additions & 0 deletions e2e/bundle-check-app/fixtures/journey-client-device.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { journey } from '@forgerock/journey-client';
import { Device } from '@forgerock/journey-client/device';

const client = await journey({
config: { serverConfig: { wellknown: 'https://example.com/.well-known/openid-configuration' } },
});

const step = await client.start();

if (step.type === 'Step') {
const device = new Device();
const profile = await device.getProfile({ collectLocation: false });
console.log(profile);
}
13 changes: 13 additions & 0 deletions e2e/bundle-check-app/fixtures/journey-client-policy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { journey } from '@forgerock/journey-client';
import { Policy } from '@forgerock/journey-client/policy';

const client = await journey({
config: { serverConfig: { wellknown: 'https://example.com/.well-known/openid-configuration' } },
});

const step = await client.start();

if (step.type === 'Step') {
const errors = Policy.parseErrors(step.callbacks);
console.log(errors);
}
13 changes: 13 additions & 0 deletions e2e/bundle-check-app/fixtures/journey-client-qr-code.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { journey } from '@forgerock/journey-client';
import { QRCode } from '@forgerock/journey-client/qr-code';

const client = await journey({
config: { serverConfig: { wellknown: 'https://example.com/.well-known/openid-configuration' } },
});

const step = await client.start();

if (step.type === 'Step') {
const qrCode = QRCode.getQRCodeData(step);
console.log(qrCode);
}
13 changes: 13 additions & 0 deletions e2e/bundle-check-app/fixtures/journey-client-recovery-codes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { journey } from '@forgerock/journey-client';
import { RecoveryCodes } from '@forgerock/journey-client/recovery-codes';

const client = await journey({
config: { serverConfig: { wellknown: 'https://example.com/.well-known/openid-configuration' } },
});

const step = await client.start();

if (step.type === 'Step') {
const codes = RecoveryCodes.getCodes(step);
console.log(codes);
}
16 changes: 16 additions & 0 deletions e2e/bundle-check-app/fixtures/journey-client-webauthn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { journey } from '@forgerock/journey-client';
import { WebAuthn, WebAuthnStepType } from '@forgerock/journey-client/webauthn';

const client = await journey({
config: { serverConfig: { wellknown: 'https://example.com/.well-known/openid-configuration' } },
});

let step = await client.start();

while (step.type === 'Step') {
const stepType = WebAuthn.getWebAuthnStepType(step);
if (stepType === WebAuthnStepType.Authenticate || stepType === WebAuthnStepType.Register) {
await WebAuthn.getWebAuthnOutcome(step);
}
step = await client.next(step);
}
15 changes: 15 additions & 0 deletions e2e/bundle-check-app/fixtures/journey-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { journey } from '@forgerock/journey-client';

const client = await journey({
config: { serverConfig: { wellknown: 'https://example.com/.well-known/openid-configuration' } },
});

let step = await client.start();

while (step.type === 'Step') {
if (step.callbacks.some((cb) => cb.type === 'RedirectCallback')) {
await client.redirect(step);
break;
}
step = await client.next(step);
}
35 changes: 35 additions & 0 deletions e2e/bundle-check-app/fixtures/journey-full.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* Realistic consumer pattern: journey authentication + WebAuthn + device fingerprinting.
* Mirrors what a real app would import for a complete login experience.
*/
import { journey } from '@forgerock/journey-client';
import { WebAuthn } from '@forgerock/journey-client/webauthn';
import { Device } from '@forgerock/journey-client/device';

const config = {
serverConfig: { wellknown: 'https://example.com/.well-known/openid-configuration' },
};

// Collect device profile before authentication
const device = new Device();
const profile = await device.getProfile({ location: false, metadata: false });
console.log('device profile collected', profile);

const client = await journey({ config });
let step = await client.start();

while (step.type === 'Step') {
// Check if this step requires WebAuthn
const webAuthnType = WebAuthn.getWebAuthnStepType(step);
if (webAuthnType !== 'None') {
console.log('webauthn step type', webAuthnType);
}

step = await client.next(step);
}

if (step.type === 'LoginSuccess') {
console.log('authenticated', step.getSessionToken());
} else if (step.type === 'LoginFailure') {
console.error('authentication failed', step);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
22 changes: 22 additions & 0 deletions e2e/bundle-check-app/fixtures/oidc-client-logout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { oidc } from '@forgerock/oidc-client';

const client = await oidc({
config: {
serverConfig: { wellknown: 'https://example.com/.well-known/openid-configuration' },
clientId: 'test-client',
redirectUri: 'https://example.com/callback',
scope: 'openid profile',
},
});

if ('error' in client) {
console.error(client.error);
} else {
// Revoke tokens (server-side invalidation)
const revokeResult = await client.token.revoke();
console.log(revokeResult);

// Full logout (revoke + end session endpoint)
const logoutResult = await client.logout();
console.log(logoutResult);
}
30 changes: 30 additions & 0 deletions e2e/bundle-check-app/fixtures/oidc-client-token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { oidc } from '@forgerock/oidc-client';

const client = await oidc({
config: {
serverConfig: { wellknown: 'https://example.com/.well-known/openid-configuration' },
clientId: 'test-client',
redirectUri: 'https://example.com/callback',
scope: 'openid profile',
},
});

if ('error' in client) {
console.error(client.error);
} else {
// Token exchange after authorization code callback
const tokens = await client.token.exchange('auth-code-123', 'state-abc');
if ('error' in tokens) {
console.error(tokens.error);
} else {
console.log(tokens);
}

// Retrieve stored tokens (with auto-renew if backgroundRenew is set)
const stored = await client.token.get({ backgroundRenew: false });
if ('error' in stored) {
console.error(stored.error);
} else {
console.log(stored);
}
}
13 changes: 13 additions & 0 deletions e2e/bundle-check-app/fixtures/oidc-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { oidc } from '@forgerock/oidc-client';

const client = await oidc({
config: {
serverConfig: { wellknown: 'https://example.com/.well-known/openid-configuration' },
clientId: 'test-client',
redirectUri: 'https://example.com/callback',
scope: 'openid profile',
},
});

const url = await client.getAuthorizationUrl();
console.log(url);
15 changes: 15 additions & 0 deletions e2e/bundle-check-app/fixtures/protect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { protect } from '@forgerock/protect';

const api = protect({
envId: 'example-env-id',
behavioralDataCollection: true,
});

const startResult = await api.start();

if (startResult && 'error' in startResult) {
console.error(startResult.error);
} else {
const data = await api.getData();
console.log(data);
}
11 changes: 11 additions & 0 deletions e2e/bundle-check-app/fixtures/redirect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { journey } from '@forgerock/journey-client';

const client = await journey({
config: { serverConfig: { wellknown: 'https://example.com/.well-known/openid-configuration' } },
});

const step = await client.start();

if (step.type === 'Step') {
await client.redirect(step);
}
Loading
Loading