Skip to content

build(audience): make @imtbl/audience publishable on npm#2838

Merged
ImmutableJeffrey merged 4 commits intomainfrom
fix/audience-sdk-bundle-for-npm-publish
Apr 9, 2026
Merged

build(audience): make @imtbl/audience publishable on npm#2838
ImmutableJeffrey merged 4 commits intomainfrom
fix/audience-sdk-bundle-for-npm-publish

Conversation

@ImmutableJeffrey
Copy link
Copy Markdown
Contributor

@ImmutableJeffrey ImmutableJeffrey commented Apr 9, 2026

Summary

Makes @imtbl/audience installable via npm install @imtbl/audience from the npm registry and includes it in the monorepo's publish workflow. Unblocks SDK-66 (Publish WebSDK on npm) and SDK-63 (integrate Web SDK into Play).

The two problems this PR solves

1. The package wasn't installable outside the monorepo. The built output referenced @imtbl/audience-core as a dependency, but audience-core is a private internal package that's never published to npm. Any consumer running npm install @imtbl/audience would hit a missing dependency error.

2. The publish workflow wasn't including audience at all. .github/workflows/publish.yaml runs pnpm -r pack:root to collect packages for publishing. Every other shipped @imtbl/* package defines a pack:root script. packages/audience/sdk did not, so the workflow silently skipped it.

What the commits do

  • tsup.config.js — local build config that bundles all @imtbl/* workspace dependencies (audience-core + transitive metrics) directly into the output. This makes the package fully self-contained — no external @imtbl/* packages needed at runtime.
  • rollup.dts.config.js + rollup-plugin-dts — bundles TypeScript type declarations into a single self-contained file. Without this, consumers would get unresolved type references to @imtbl/audience-core. Leftover per-file .d.ts files are cleaned up after bundling.
  • scripts/prepack.mjs / scripts/postpack.mjs — removes @imtbl/audience-core from package.json before packaging (since it's bundled inline and doesn't exist on npm), then restores it afterwards for local development. This pattern is unique to @imtbl/audience because it's the only published package that depends on a private workspace package.
  • package.jsonpack:root script — registers audience with the publish workflow, matching every other shipped @imtbl/* package.
  • package.json — explicit devDependenciesesbuild-plugin-replace and esbuild-plugins-node-modules-polyfill declared explicitly rather than relying on root hoisting (matches @imtbl/checkout-sdk pattern).
  • .eslintignore — exempts the new build configs from eslint.
  • .gitignore — adds *.prepack-backup to prevent accidental commit if packaging fails mid-way.

Verified locally

  • Built output contains no references to @imtbl/* packages — fully self-contained
  • Type declarations contain no references to @imtbl/* packages
  • Package installs cleanly in a fresh project via npm install ./imtbl-audience-0.0.0.tgz — both runtime and types resolve correctly
  • pnpm --filter @imtbl/audience pack:root produces the package at the repo root on the first try

What this PR still does NOT do (flagged for verification before first publish)

Verify nx release assigns the correct version to @imtbl/audience. The publish workflow seeds versions from @imtbl/metrics for packages in @imtbl/sdk's dependency tree. @imtbl/audience is not in that tree, so it starts at 0.0.0. nx release should bump it to match the rest (since projectsRelationship is fixed) — but this hasn't been independently verified.

Recommended first publish flow:

  1. Merge this PR
  2. Trigger publish.yaml via workflow_dispatch with dry_run: true and release_type: prerelease
  3. Inspect the logs: confirm @imtbl/audience is versioned correctly and included in the package list
  4. If dry run looks good, re-run with dry_run: false
  5. Verify npm install @imtbl/audience@<version> works from a fresh project

Tickets

  • SDK-66 (Publish WebSDK on npm) — this PR is the last code change needed
  • SDK-63 (Integrate Web SDK into Play/Game Page) — Game Page needs npm install @imtbl/audience; this PR makes that possible

Test plan

  • pnpm --filter @imtbl/audience build — ESM + CJS + browser + bundled types all produced
  • pnpm --filter @imtbl/audience pack:root — package produced at repo root
  • Clean install in fresh /tmp project — runtime and types resolve
  • No bare @imtbl/* references in built output
  • CI green
  • workflow_dispatch --dry_run to verify versioning and inclusion in publish

Relationship to other open PRs

Independent of #2836 (foundation: errors + IdentityType) and #2837 (demo + CDN bundle). No file collisions; only adjacent-line overlap in packages/audience/sdk/package.json. Git auto-merges; whichever lands second regenerates pnpm-lock.yaml.

🤖 Generated with Claude Code

@ImmutableJeffrey ImmutableJeffrey requested review from a team as code owners April 9, 2026 00:02
@ImmutableJeffrey ImmutableJeffrey force-pushed the fix/audience-sdk-bundle-for-npm-publish branch from d549817 to 8cf0447 Compare April 9, 2026 00:08
@ImmutableJeffrey ImmutableJeffrey changed the title build(audience): bundle workspace deps into @imtbl/audience tarball build(audience): make @imtbl/audience publishable as an npm tarball Apr 9, 2026
@nx-cloud
Copy link
Copy Markdown

nx-cloud bot commented Apr 9, 2026

View your CI Pipeline Execution ↗ for commit 481813f

Command Status Duration Result
nx run-many -p @imtbl/sdk,@imtbl/checkout-widge... ✅ Succeeded 2s View ↗
nx affected -t build,lint,test ✅ Succeeded 9s View ↗

☁️ Nx Cloud last updated this comment at 2026-04-09 07:26:26 UTC

ImmutableJeffrey and others added 3 commits April 9, 2026 14:51
Makes @imtbl/audience installable from npm (unblocks SDK-66) and
picks it up by the monorepo's publish workflow (unblocks the first
publish).

Changes to packages/audience/sdk only:
- tsup.config.js: local config that extends the monorepo defaults and sets
  noExternal: [/^@imtbl\//] so runtime JS inlines audience-core and its
  transitive metrics dep.
- rollup.dts.config.js + rollup-plugin-dts: post-typegen step that rolls up
  the generated declaration file with respectExternal so public types are
  self-contained.
- scripts/prepack.mjs + scripts/postpack.mjs: strip 'workspace:*' deps from
  the published package.json before pack, restore afterwards so the
  developer working tree stays on workspace form.
- package.json: add 'pack:root' script matching every other shipped
  @imtbl/* package so the root 'pack-npm-packages' recursive invocation
  actually packs @imtbl/audience. Without this the publish workflow
  silently skipped the package.
- .eslintignore: exempt the new tsup.config.js / rollup.dts.config.js from
  project-based eslint (mirrors existing pattern for build-time configs).

Verified locally:
- dist/node/*.js, dist/node/*.cjs, dist/browser/*.js contain no bare
  @imtbl/* imports
- dist/types/index.d.ts contains no @imtbl/* re-exports
- Installing the produced tarball into a fresh /tmp directory via
  'npm install' resolves both the runtime require and the types
- 'pnpm --filter @imtbl/audience pack:root' produces imtbl-audience-0.0.0.tgz
  at the repo root — same location pattern as passport, checkout, etc.

Refs: SDK-66, SDK-63

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Declare esbuild-plugin-replace and esbuild-plugins-node-modules-polyfill
  as explicit devDependencies (were phantom deps via root hoisting)
- Remove @uniswap/swap-router-contracts from noExternal (copy-paste
  artifact from root tsup config — audience has no uniswap dependency)
- Add *.prepack-backup to .gitignore (prevents accidental commit if
  pnpm pack fails between prepack and postpack)
- Clean up leftover per-file .d.ts after rollup bundles them into
  dist/types/index.d.ts (reduces tarball size)
- Use import attribute `with` instead of deprecated `assert` syntax
  (Node 22+ deprecation)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@ImmutableJeffrey ImmutableJeffrey force-pushed the fix/audience-sdk-bundle-for-npm-publish branch from 499e04b to f64a9a5 Compare April 9, 2026 04:52
@ImmutableJeffrey ImmutableJeffrey changed the title build(audience): make @imtbl/audience publishable as an npm tarball build(audience): make @imtbl/audience publishable on npm Apr 9, 2026
…pack

Previously tsup.config.js used a broad regex (/^@imtbl\//) for noExternal
while prepack.mjs used an explicit list ['@imtbl/audience-core',
'@imtbl/metrics']. If a new @imtbl/* package became a direct dep,
tsup would silently bundle it but prepack would leave workspace:* in the
published package.json — breaking `npm install @imtbl/audience`.

Extract the list to scripts/bundled-workspace-deps.mjs as a single source
of truth. Both tsup.config.js and prepack.mjs import from it, so adding a
new bundled workspace dep is a one-line change in one file.

Addresses PR review comment from @nattb8.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@ImmutableJeffrey ImmutableJeffrey added this pull request to the merge queue Apr 9, 2026
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Apr 9, 2026
@ImmutableJeffrey ImmutableJeffrey added this pull request to the merge queue Apr 9, 2026
Merged via the queue into main with commit 0d02b4e Apr 9, 2026
7 checks passed
@ImmutableJeffrey ImmutableJeffrey deleted the fix/audience-sdk-bundle-for-npm-publish branch April 9, 2026 09:21
ImmutableJeffrey added a commit that referenced this pull request Apr 9, 2026
PR #2838 shipped the build fixes that make @imtbl/audience installable
as a standalone npm package. However, the first publish run after merge
(workflow run 24182647672) was unable to actually publish the fixed
artifact because of a pre-existing version collision:

  Skipped package "@imtbl/audience" because v0.0.1-alpha.0 already
  exists in https://registry.npmjs.org/ with tag "alpha"

The collision stems from two root causes that compound:

1. Audience source versions were left at the 0.0.0 template value,
   so nx release's prerelease specifier always computes 0.0.1-alpha.0
   for them — but that version number was burned on 2026-04-08 by an
   earlier publish attempt that pushed the pre-2838 broken build
   (no prepack stripping, @imtbl/audience-core still listed as a
   runtime dep pointing at a package that isn't published). The
   registry refuses to overwrite, so every subsequent publish run
   skips audience indefinitely.

2. The "Initialize current versions" step in publish.yaml seeds
   package versions from @imtbl/metrics for packages inside the
   @imtbl/sdk... and @imtbl/checkout-widgets... dependency trees.
   @imtbl/audience isn't in either tree, so it never gets seeded and
   drifts out of lockstep with the rest of the SDK family — which
   is what produces the 0.0.1-alpha.0 bump in the first place.
   PR #2838's body explicitly flagged this as unverified:
   "nx release *should* bump it to match the rest (since
   projectsRelationship is fixed) — but this hasn't been independently
   verified". It doesn't — without an explicit filter entry, nx reads
   the disk version (0.0.0) and bumps from there.

This patch fixes both sides:

- packages/audience/sdk/package.json and packages/audience/core/package.json:
  bump source version 0.0.0 → 2.15.0-alpha.19 (matching the current
  SDK-family source state). This alone unblocks the next publish —
  nx release will compute 2.15.0-alpha.20 on next run, which is a
  fresh version slot and publishes cleanly.

- .github/workflows/publish.yaml: add --filter @imtbl/audience... to
  the Initialize current versions step. This seeds audience (and
  audience-core via the ... transitive filter) to the latest metrics
  version from npm on every publish run, keeping audience in lockstep
  with the rest of the SDK family long-term. Without this, the next
  publish after 2.15.0-alpha.20 lands would drift audience out of sync
  again.

@imtbl/audience-core is private and bundled inline into @imtbl/audience
via tsup noExternal (PR #2838), so its version never reaches consumers —
but keeping it aligned with the SDK family avoids confusing diffs in
the transient workspace:* resolution step.

Refs: SDK-66, SDK-63
ImmutableJeffrey added a commit that referenced this pull request Apr 9, 2026
…g filter

PR #2838 shipped the build fixes that make @imtbl/audience installable
as a standalone npm package. However, the first publish run after merge
(workflow run 24182647672) was unable to actually publish the fixed
artifact because of a version collision:

  Skipped package "@imtbl/audience" because v0.0.1-alpha.0 already
  exists in https://registry.npmjs.org/ with tag "alpha"

## Root cause

The "Initialize current versions" step in publish.yaml seeds package
versions from @imtbl/metrics for packages inside the @imtbl/sdk... and
@imtbl/checkout-widgets... dependency trees. Each SDK-family package
has "version": "0.0.0" committed in source (as a template); the
Initialize step is the source of truth at publish time.

@imtbl/audience isn't in either of those dependency trees, so it never
gets seeded. nx release reads the committed template value 0.0.0 from
disk and computes 0.0.1-alpha.0 — which was burned on 2026-04-08 by an
earlier publish attempt that pushed the pre-2838 broken build (no
prepack stripping, @imtbl/audience-core still listed as a runtime dep
pointing at a package that's never published since it's private).
The registry refuses to overwrite, so every publish run skips audience
indefinitely.

Evidence the npm copy is still the broken pre-2838 artifact:

  $ npm view @imtbl/audience@0.0.1-alpha.0 dependencies --json
  {"@imtbl/audience-core": "0.0.1-alpha.0"}

  $ npm view @imtbl/audience@0.0.1-alpha.0 time.created
  2026-04-08T04:43:20.487Z   # one day before PR #2838 merged

  $ npm view @imtbl/audience-core versions
  E404 Not Found             # private, never published

  $ cd /tmp/fresh && npm install @imtbl/audience@0.0.1-alpha.0
  E404 on @imtbl/audience-core@0.0.1-alpha.0

PR #2838's body explicitly flagged this as unverified:

  > nx release should bump it to match the rest (since
  > projectsRelationship is fixed) — but this hasn't been independently
  > verified.

Verified now: projectsRelationship: fixed does NOT make nx reconcile
different starting disk versions. It just means all projects bump under
the same release cycle with the same specifier. Without an explicit
filter entry in the Initialize step, audience's disk 0.0.0 is read
as-is and bumps to 0.0.1-alpha.0.

## Fix

Add --filter @imtbl/audience... to the Initialize current versions
step. This seeds @imtbl/audience AND @imtbl/audience-core (via the ...
transitive-deps suffix) to the latest @imtbl/metrics version from npm
on every publish run, matching the pattern used for every other
SDK-family package. No source-version changes needed — audience
follows the existing convention where source stays at 0.0.0 and the
Initialize step is the canonical version writer.

## Expected behaviour on next publish

1. Initialize current versions: reads latest metrics version from npm
   (e.g. 2.15.0-alpha.20), writes it to all SDK-family package.json
   files including audience + audience-core.
2. Setup new package versions (nx release): bumps every seeded package
   from the metrics version to the next prerelease (e.g.
   2.15.0-alpha.21) — audience/audience-core in lockstep with the
   rest of the SDK family.
3. Pack: prepack strips @imtbl/audience-core from audience's
   dependencies, postpack restores, producing imtbl-audience-<new>.tgz
   with no workspace references.
4. Release to NPM: publishes @imtbl/audience at the fresh version slot
   — no collision, because this slot has not been burned.
5. npm install @imtbl/audience@<new-version> in a fresh project
   resolves and installs cleanly.

Refs: SDK-66, SDK-63
ImmutableJeffrey added a commit that referenced this pull request Apr 9, 2026
Addresses review feedback on #2837 from @nattb8: the interactive demo
should live in its own workspace package (matching the repo convention
used by passport/sdk-sample-app, checkout/sdk-sample-app, dex/sdk-sample-app,
bridge/bridge-sample-app) rather than inside the published @imtbl/audience
package directory.

Why this matters beyond aesthetics:

- @imtbl/audience is a published npm package with a dedicated build
  pipeline (#2838): local tsup.config.js, prepack/postpack scripts that
  strip workspace deps from package.json, rollup-plugin-dts to inline
  type re-exports. The sdk package directory should stay focused on
  shipping artifacts; a demo harness is not one.
- The demo was vanilla ES2020 (no TS, no modules, loaded via a script
  tag) while the sdk package is pure TypeScript. Co-locating them forced
  sdk/.eslintignore + an .eslintrc.cjs override block just to keep
  lint-staged from trying to parse demo/*.js with the TS parser. Both
  pieces of config disappear with this move.
- The existing repo-wide root .eslintignore already has a
  `**sample-app**/` glob (for passport/sdk-sample-app and friends), so
  the new directory is automatically excluded from root lint with zero
  local config.

Addresses the reviewer's secondary concern — "this is included in the
CDN bundle too" — at the structural level. For the record, verified
the demo was never literally bundled into dist/cdn/imtbl-audience.global.js:
src/cdn.ts imports only ./sdk, ./config, and @imtbl/audience-core, and
`files: ["dist"]` in package.json already excluded demo/ from the npm
tarball. Confirmed by packing the sdk and inspecting the tarball — it
only contains dist/browser, dist/cdn, dist/node, dist/types, plus
README.md, LICENSE.md, and package.json.

Changes:

New package — packages/audience/sdk-sample-app/
- package.json: private, @imtbl/audience as a workspace:* devDep,
  engines node >= 20.11, `pnpm dev` builds @imtbl/audience then runs
  the local serve script
- serve.mjs: ~90-line Node static server using only the stdlib.
  Serves the sample-app's own files from ./, and routes /vendor/
  to ../sdk/dist/cdn/ so the HTML can load the CDN bundle via a
  same-origin URL (keeps the demo's CSP happy). Blocks serve.mjs,
  package.json, and node_modules from being served, plus path
  traversal attempts via decodeURIComponent + a resolve/startsWith
  guard. Verified with curl: 200 for /, /demo.css, /demo.js and
  /vendor/imtbl-audience.global.js(.map); 403 for /package.json,
  /serve.mjs, /vendor/../../package.json, /%2e%2e/secret; 404 for
  /nonexistent.html.
- index.html, demo.js, demo.css, README.md: git-renamed from
  packages/audience/sdk/demo/. The only content change is in
  index.html — the <script src> moved from ../dist/cdn/... to
  vendor/... — plus README.md was updated with the new run
  instructions and a layout diagram for the new location.

Package cleanup — packages/audience/sdk/
- Remove the `demo` script from package.json (its entry point is
  gone now).
- Revert .eslintrc.cjs to main's 6-line baseline by dropping the
  22-line `demo/**/*.js` overrides block that the PR had added.
- Delete .eslintignore entirely (its only line was `demo/`).
- Update README.md's two `demo/` references to point at
  `../sdk-sample-app/README.md` instead.

Repo-level
- Drop the `packages/audience/sdk/demo/` line from root .eslintignore
  (the existing `**sample-app**/` glob covers the new location).
- Register `packages/audience/sdk-sample-app` in pnpm-workspace.yaml.
- pnpm-lock.yaml picks up a 6-line importer entry for the new package
  (just the workspace:* link to ../sdk, no external deps).

Verification:
- `pnpm --filter @imtbl/audience-core --filter @imtbl/audience run
  lint typecheck test` — 113 core + 51 sdk tests pass, lint/typecheck
  clean on both packages.
- `pnpm --filter @imtbl/audience run build` — ESM (browser+node),
  CDN IIFE (52.04 KB), and rolled-up .d.ts all build clean.
- `pnpm --filter @imtbl/audience-sdk-sample-app run dev` — builds
  the sdk, starts the local server, demo loads at http://localhost:3456/
  with the CDN bundle served from /vendor/.
- `pnpm pack --pack-destination /tmp/...` in the sdk — tarball
  contains only dist/{browser,cdn,node,types}, LICENSE.md, README.md,
  and package.json. No demo, no vendor, no sample-app, no scripts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ImmutableJeffrey added a commit that referenced this pull request Apr 9, 2026
Addresses review feedback on #2837 from @nattb8: the interactive demo
should live in its own workspace package (matching the repo convention
used by passport/sdk-sample-app, checkout/sdk-sample-app, dex/sdk-sample-app,
bridge/bridge-sample-app) rather than inside the published @imtbl/audience
package directory.

Why this matters beyond aesthetics:

- @imtbl/audience is a published npm package with a dedicated build
  pipeline (#2838): local tsup.config.js, prepack/postpack scripts that
  strip workspace deps from package.json, rollup-plugin-dts to inline
  type re-exports. The sdk package directory should stay focused on
  shipping artifacts; a demo harness is not one.
- The demo was vanilla ES2020 (no TS, no modules, loaded via a script
  tag) while the sdk package is pure TypeScript. Co-locating them forced
  sdk/.eslintignore + an .eslintrc.cjs override block just to keep
  lint-staged from trying to parse demo/*.js with the TS parser. Both
  pieces of config disappear with this move.
- The existing repo-wide root .eslintignore already has a
  `**sample-app**/` glob (for passport/sdk-sample-app and friends), so
  the new directory is automatically excluded from root lint with zero
  local config.

Addresses the reviewer's secondary concern — "this is included in the
CDN bundle too" — at the structural level. For the record, verified
the demo was never literally bundled into dist/cdn/imtbl-audience.global.js:
src/cdn.ts imports only ./sdk, ./config, and @imtbl/audience-core, and
`files: ["dist"]` in package.json already excluded demo/ from the npm
tarball. Confirmed by packing the sdk and inspecting the tarball — it
only contains dist/browser, dist/cdn, dist/node, dist/types, plus
README.md, LICENSE.md, and package.json.

Changes:

New package — packages/audience/sdk-sample-app/
- package.json: private, @imtbl/audience as a workspace:* devDep,
  engines node >= 20.11, `pnpm dev` builds @imtbl/audience then runs
  the local serve script
- serve.mjs: ~90-line Node static server using only the stdlib.
  Serves the sample-app's own files from ./, and routes /vendor/
  to ../sdk/dist/cdn/ so the HTML can load the CDN bundle via a
  same-origin URL (keeps the demo's CSP happy). Blocks serve.mjs,
  package.json, and node_modules from being served, plus path
  traversal attempts via decodeURIComponent + a resolve/startsWith
  guard. Verified with curl: 200 for /, /demo.css, /demo.js and
  /vendor/imtbl-audience.global.js(.map); 403 for /package.json,
  /serve.mjs, /vendor/../../package.json, /%2e%2e/secret; 404 for
  /nonexistent.html.
- index.html, demo.js, demo.css, README.md: git-renamed from
  packages/audience/sdk/demo/. The only content change is in
  index.html — the <script src> moved from ../dist/cdn/... to
  vendor/... — plus README.md was updated with the new run
  instructions and a layout diagram for the new location.

Package cleanup — packages/audience/sdk/
- Remove the `demo` script from package.json (its entry point is
  gone now).
- Revert .eslintrc.cjs to main's 6-line baseline by dropping the
  22-line `demo/**/*.js` overrides block that the PR had added.
- Delete .eslintignore entirely (its only line was `demo/`).
- Update README.md's two `demo/` references to point at
  `../sdk-sample-app/README.md` instead.

Repo-level
- Drop the `packages/audience/sdk/demo/` line from root .eslintignore
  (the existing `**sample-app**/` glob covers the new location).
- Register `packages/audience/sdk-sample-app` in pnpm-workspace.yaml.
- pnpm-lock.yaml picks up a 6-line importer entry for the new package
  (just the workspace:* link to ../sdk, no external deps).

Verification:
- `pnpm --filter @imtbl/audience-core --filter @imtbl/audience run
  lint typecheck test` — 113 core + 51 sdk tests pass, lint/typecheck
  clean on both packages.
- `pnpm --filter @imtbl/audience run build` — ESM (browser+node),
  CDN IIFE (52.04 KB), and rolled-up .d.ts all build clean.
- `pnpm --filter @imtbl/audience-sdk-sample-app run dev` — builds
  the sdk, starts the local server, demo loads at http://localhost:3456/
  with the CDN bundle served from /vendor/.
- `pnpm pack --pack-destination /tmp/...` in the sdk — tarball
  contains only dist/{browser,cdn,node,types}, LICENSE.md, README.md,
  and package.json. No demo, no vendor, no sample-app, no scripts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants