Skip to content

Commit 0d02b4e

Browse files
build(audience): make @imtbl/audience publishable on npm (#2838)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 906606b commit 0d02b4e

9 files changed

Lines changed: 442 additions & 159 deletions

File tree

.eslintignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,5 @@ packages/internal/generated-clients/src/
2121

2222
# put module specific ignore paths here
2323
packages/game-bridge/scripts/**/*.js
24+
packages/audience/sdk/rollup.dts.config.js
25+
packages/audience/sdk/tsup.config.js

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,6 @@ examples/**/test-results/
3434

3535
tests/**/.env
3636
tests/**/playwright-report/
37-
tests/**/test-results/
37+
tests/**/test-results/
38+
39+
*.prepack-backup

packages/audience/sdk/package.json

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,13 @@
1212
"@swc/jest": "^0.2.37",
1313
"@types/jest": "^29.5.12",
1414
"@types/node": "^22.10.7",
15+
"esbuild-plugin-replace": "^1.4.0",
16+
"esbuild-plugins-node-modules-polyfill": "^1.6.7",
1517
"eslint": "^8.56.0",
1618
"jest": "^29.7.0",
1719
"jest-environment-jsdom": "^29.4.3",
20+
"rollup": "^4.22.4",
21+
"rollup-plugin-dts": "^6.4.1",
1822
"ts-jest": "^29.1.0",
1923
"tsup": "^8.3.0",
2024
"typescript": "^5.6.2"
@@ -36,7 +40,9 @@
3640
"default": "./dist/node/index.js"
3741
}
3842
},
39-
"files": ["dist"],
43+
"files": [
44+
"dist"
45+
],
4046
"homepage": "https://github.com/immutable/ts-immutable-sdk#readme",
4147
"main": "dist/node/index.cjs",
4248
"module": "dist/node/index.js",
@@ -47,9 +53,12 @@
4753
"repository": "immutable/ts-immutable-sdk.git",
4854
"scripts": {
4955
"build": "pnpm transpile && pnpm typegen",
50-
"transpile": "tsup src/index.ts --config ../../../tsup.config.js",
51-
"typegen": "tsc --customConditions default --emitDeclarationOnly --outDir dist/types",
56+
"transpile": "tsup --config tsup.config.js",
57+
"typegen": "tsc --customConditions default --emitDeclarationOnly --outDir dist/types && rollup -c rollup.dts.config.js && find dist/types -name '*.d.ts' ! -name 'index.d.ts' -delete",
5258
"lint": "eslint ./src --ext .ts,.jsx,.tsx --max-warnings=0",
59+
"pack:root": "pnpm pack --pack-destination $(dirname $(pnpm root -w))",
60+
"prepack": "node scripts/prepack.mjs",
61+
"postpack": "node scripts/postpack.mjs",
5362
"test": "jest --passWithNoTests",
5463
"test:watch": "jest --watch",
5564
"typecheck": "tsc --customConditions development --noEmit --jsx preserve"
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Roll up the generated .d.ts files so that type re-exports from
2+
// `@imtbl/audience-core` (and its transitive `@imtbl/metrics`) are inlined
3+
// into a single self-contained declaration file. Without this, consumers of
4+
// the published tarball would get unresolved type references, because the
5+
// @imtbl/* packages are bundled into dist/ but not published alongside.
6+
import { dts } from 'rollup-plugin-dts';
7+
8+
// By default, rollup treats every non-relative import as external — so
9+
// `@imtbl/audience-core` type re-exports would stay as bare imports in the
10+
// output. Pass `respectExternal: true` so the plugin walks through node
11+
// resolution to `.d.ts` files for @imtbl/* workspace packages and inlines
12+
// them into the rolled-up declaration file.
13+
export default {
14+
input: 'dist/types/index.d.ts',
15+
output: { file: 'dist/types/index.d.ts', format: 'es' },
16+
plugins: [dts({ respectExternal: true })],
17+
};
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Single source of truth for @imtbl/* workspace packages that get bundled
2+
// into the published @imtbl/audience package.
3+
//
4+
// Used by:
5+
// - ../tsup.config.js (noExternal: inlines the runtime code at build time)
6+
// - ./prepack.mjs (strips workspace:* specifiers from package.json
7+
// before pnpm pack, since these deps are bundled
8+
// into dist/ and @imtbl/audience-core is private)
9+
//
10+
// Adding a new direct @imtbl/* workspace dep to @imtbl/audience? Add it
11+
// here. Otherwise tsup will leave the import as external (broken at runtime
12+
// in consumer projects) or prepack will leave a workspace:* specifier in
13+
// the published package.json (breaks `npm install @imtbl/audience`).
14+
export const BUNDLED_WORKSPACE_DEPS = [
15+
'@imtbl/audience-core',
16+
'@imtbl/metrics',
17+
];
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#!/usr/bin/env node
2+
/*
3+
* postpack: restore the package.json that prepack.mjs backed up.
4+
*
5+
* Runs after `npm pack` / `npm publish` finishes, so the developer's working
6+
* tree goes back to referencing `@imtbl/audience-core: workspace:*` for
7+
* local monorepo development.
8+
*/
9+
import { existsSync, copyFileSync, unlinkSync } from 'node:fs';
10+
11+
const pkgPath = new URL('../package.json', import.meta.url);
12+
const backupPath = new URL('../package.json.prepack-backup', import.meta.url);
13+
14+
if (existsSync(backupPath)) {
15+
copyFileSync(backupPath, pkgPath);
16+
unlinkSync(backupPath);
17+
console.log('[postpack] restored original package.json');
18+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#!/usr/bin/env node
2+
/*
3+
* prepack: strip workspace-protocol deps from package.json before `npm pack`.
4+
*
5+
* The runtime JS (dist/node, dist/browser) and the bundled .d.ts already
6+
* inline `@imtbl/audience-core` and its transitive `@imtbl/metrics` dep, so
7+
* they don't need to be listed as runtime deps in the published package. If
8+
* we left them, npm would choke on the `workspace:*` protocol at install.
9+
*
10+
* A sibling postpack.mjs restores the original package.json after the tarball
11+
* is written, so the developer's working tree is never left modified.
12+
*/
13+
import { readFileSync, writeFileSync, copyFileSync } from 'node:fs';
14+
import { BUNDLED_WORKSPACE_DEPS } from './bundled-workspace-deps.mjs';
15+
16+
const pkgPath = new URL('../package.json', import.meta.url);
17+
const backupPath = new URL('../package.json.prepack-backup', import.meta.url);
18+
19+
copyFileSync(pkgPath, backupPath);
20+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
21+
22+
// Deps bundled into dist/ by tsup: remove from published metadata so
23+
// `npm install @imtbl/audience` doesn't try to resolve them from the
24+
// registry (audience-core is private and never published).
25+
for (const name of BUNDLED_WORKSPACE_DEPS) {
26+
if (pkg.dependencies) delete pkg.dependencies[name];
27+
}
28+
// Clean up empty dependencies object.
29+
if (pkg.dependencies && Object.keys(pkg.dependencies).length === 0) {
30+
delete pkg.dependencies;
31+
}
32+
33+
writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
34+
console.log('[prepack] stripped bundled workspace deps from package.json');
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// @ts-check
2+
// Local tsup config for @imtbl/audience.
3+
//
4+
// Overrides the monorepo's root tsup config by setting `noExternal` to the
5+
// explicit list of `@imtbl/*` workspace deps that should be inlined into the
6+
// built bundle. The same list is used by scripts/prepack.mjs to strip those
7+
// deps from the published package.json. Keeping the two in sync via a shared
8+
// module prevents the "tsup silently bundles a new dep but prepack leaves
9+
// workspace:* in package.json" class of bugs.
10+
import { defineConfig } from 'tsup';
11+
import { nodeModulesPolyfillPlugin } from 'esbuild-plugins-node-modules-polyfill';
12+
import { replace } from 'esbuild-plugin-replace';
13+
import pkg from './package.json' with { type: 'json' };
14+
import { BUNDLED_WORKSPACE_DEPS } from './scripts/bundled-workspace-deps.mjs';
15+
16+
export default defineConfig((options) => {
17+
if (options.watch) {
18+
return {
19+
entry: ['src/index.ts'],
20+
outDir: 'dist/browser',
21+
format: 'esm',
22+
target: 'es2022',
23+
platform: 'browser',
24+
bundle: true,
25+
noExternal: BUNDLED_WORKSPACE_DEPS,
26+
esbuildPlugins: [
27+
nodeModulesPolyfillPlugin({
28+
globals: { Buffer: true, process: true },
29+
modules: ['crypto', 'buffer', 'process'],
30+
}),
31+
replace({
32+
__SDK_VERSION__: pkg.version === '0.0.0' ? '2.0.0' : pkg.version,
33+
}),
34+
],
35+
};
36+
}
37+
38+
return [
39+
// Browser ESM bundle
40+
{
41+
entry: ['src/index.ts'],
42+
outDir: 'dist/browser',
43+
platform: 'browser',
44+
format: 'esm',
45+
target: 'es2022',
46+
minify: true,
47+
bundle: true,
48+
noExternal: BUNDLED_WORKSPACE_DEPS,
49+
treeshake: true,
50+
esbuildPlugins: [
51+
nodeModulesPolyfillPlugin({
52+
globals: { Buffer: true, process: true },
53+
modules: ['crypto', 'buffer', 'process'],
54+
}),
55+
replace({ __SDK_VERSION__: pkg.version }),
56+
],
57+
},
58+
59+
// Node CJS + ESM bundle
60+
{
61+
entry: ['src/index.ts'],
62+
outDir: 'dist/node',
63+
platform: 'node',
64+
format: ['esm', 'cjs'],
65+
target: 'es2022',
66+
minify: true,
67+
bundle: true,
68+
noExternal: BUNDLED_WORKSPACE_DEPS,
69+
treeshake: true,
70+
esbuildPlugins: [
71+
replace({ __SDK_VERSION__: pkg.version }),
72+
],
73+
},
74+
];
75+
});

0 commit comments

Comments
 (0)