Skip to content

Commit fddbfed

Browse files
feat(audience): CDN bundle API parity and artifact test
- Expose AudienceEvents, canTrack, and canIdentify on the CDN global so ImmutableAudience matches every runtime value the npm entry exports. Re-export ImmutableAudienceGlobal from the npm entry so TS consumers can type a window.ImmutableAudience reference. - Add pnpm test:cdn-artifact. Reads dist/cdn/imtbl-audience.global.js off disk and runs it in a jsdom realm, asserting the full global shape. The existing src/cdn.test.ts imports source through swc/jest, so a broken tsup.cdn.js (wrong globalName, noExternal slip, missing __SDK_VERSION__ replacement) shipped green — this closes that gap. - Align the watch-mode version fallback in tsup.config.js with the CDN config ('0.0.0-local'). Two dev-version strings in one package meant locally-built npm and locally-built CDN reported different versions. - Enable treeshake in tsup.cdn.js to match the ESM/CJS production configs. Bundle 52.53 KB -> 51.88 KB. - Log the already-loaded version in the double-import warning and tell the studio to remove the old <script> tag to upgrade. The old warning said what happened, not what to do. - Declare ./cdn as an explicit subpath export in package.json. The file shipped via files: ["dist"] but had no contract with consumers — a future dist/cdn/ rename would have broken silently.
1 parent 441a0a5 commit fddbfed

File tree

10 files changed

+145
-15
lines changed

10 files changed

+145
-15
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import type { Config } from 'jest';
2+
3+
// Runs the CDN artifact smoke test in isolation from the default suite.
4+
// Default suite (jest.config.ts) scopes to src/ and tests TypeScript source;
5+
// this config scopes to test/ and asserts against the built IIFE in dist/cdn.
6+
const config: Config = {
7+
roots: ['<rootDir>/test'],
8+
// jsdom, not node: the bundle touches `navigator` at parse time (via a
9+
// transitive audience-core dep), so a bare node realm throws before the
10+
// side-effect global assignment runs. jsdom matches the real CDN target.
11+
testEnvironment: 'jsdom',
12+
transform: {
13+
'^.+\\.(t|j)sx?$': '@swc/jest',
14+
},
15+
};
16+
17+
export default config;

packages/audience/sdk/package.json

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,22 @@
2727
"node": ">=20.11.0"
2828
},
2929
"exports": {
30-
"development": {
31-
"types": "./src/index.ts",
32-
"browser": "./dist/browser/index.js",
33-
"require": "./dist/node/index.cjs",
34-
"default": "./dist/node/index.js"
30+
".": {
31+
"development": {
32+
"types": "./src/index.ts",
33+
"browser": "./dist/browser/index.js",
34+
"require": "./dist/node/index.cjs",
35+
"default": "./dist/node/index.js"
36+
},
37+
"default": {
38+
"types": "./dist/types/index.d.ts",
39+
"browser": "./dist/browser/index.js",
40+
"require": "./dist/node/index.cjs",
41+
"default": "./dist/node/index.js"
42+
}
3543
},
36-
"default": {
37-
"types": "./dist/types/index.d.ts",
38-
"browser": "./dist/browser/index.js",
39-
"require": "./dist/node/index.cjs",
40-
"default": "./dist/node/index.js"
44+
"./cdn": {
45+
"default": "./dist/cdn/imtbl-audience.global.js"
4146
}
4247
},
4348
"files": [
@@ -61,6 +66,7 @@
6166
"prepack": "node scripts/prepack.mjs",
6267
"postpack": "node scripts/postpack.mjs",
6368
"test": "jest --passWithNoTests",
69+
"test:cdn-artifact": "pnpm transpile:cdn && jest --config jest.cdn-artifact.config.ts",
6470
"test:watch": "jest --watch",
6571
"typecheck": "tsc --customConditions development --noEmit --jsx preserve"
6672
},

packages/audience/sdk/src/cdn.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ describe('cdn entry point', () => {
1818

1919
expect(g).toBeDefined();
2020
expect(typeof g!.Audience.init).toBe('function');
21+
expect(typeof g!.AudienceEvents).toBe('object');
22+
expect(typeof g!.canTrack).toBe('function');
23+
expect(typeof g!.canIdentify).toBe('function');
2124
expect(g!.IdentityType.Passport).toBe('passport');
2225
expect(g!.IdentityType.Steam).toBe('steam');
2326
expect(g!.IdentityType.Custom).toBe('custom');
@@ -44,6 +47,7 @@ describe('cdn entry point', () => {
4447
(globalThis as unknown as { ImmutableAudience?: unknown }).ImmutableAudience,
4548
).toBe(sentinel);
4649
expect(warn).toHaveBeenCalledWith(expect.stringContaining('loaded twice'));
50+
expect(warn).toHaveBeenCalledWith(expect.stringContaining('Remove the old <script> tag'));
4751
warn.mockRestore();
4852
});
4953
});

packages/audience/sdk/src/cdn.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
1-
import { AudienceError, IdentityType } from '@imtbl/audience-core';
1+
import {
2+
AudienceError,
3+
IdentityType,
4+
canIdentify,
5+
canTrack,
6+
} from '@imtbl/audience-core';
27

38
import { Audience } from './sdk';
9+
import { AudienceEvents } from './events';
410
import { LIBRARY_VERSION } from './config';
511

612
export type ImmutableAudienceGlobal = {
713
Audience: typeof Audience;
814
AudienceError: typeof AudienceError;
15+
AudienceEvents: typeof AudienceEvents;
916
IdentityType: typeof IdentityType;
17+
canIdentify: typeof canIdentify;
18+
canTrack: typeof canTrack;
1019
version: string;
1120
};
1221

@@ -16,13 +25,20 @@ const globalObj = (
1625
) as unknown as { ImmutableAudience?: ImmutableAudienceGlobal };
1726

1827
if (globalObj.ImmutableAudience) {
28+
const existingVersion = globalObj.ImmutableAudience.version ?? 'unknown';
1929
// eslint-disable-next-line no-console
20-
console.warn('[@imtbl/audience] CDN bundle loaded twice; keeping the first instance.');
30+
console.warn(
31+
`[@imtbl/audience] CDN bundle loaded twice; keeping v${existingVersion}. `
32+
+ 'Remove the old <script> tag to load a different version.',
33+
);
2134
} else {
2235
globalObj.ImmutableAudience = {
2336
Audience,
2437
AudienceError,
38+
AudienceEvents,
2539
IdentityType,
40+
canIdentify,
41+
canTrack,
2642
version: LIBRARY_VERSION,
2743
};
2844
}

packages/audience/sdk/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export { Audience } from './sdk';
22
export { AudienceEvents } from './events';
33
export { IdentityType, canTrack, canIdentify } from '@imtbl/audience-core';
4+
export type { ImmutableAudienceGlobal } from './cdn';
45
export type { AudienceConfig } from './types';
56
export type {
67
AudienceEventName,
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import * as fs from 'fs';
2+
import * as path from 'path';
3+
4+
// Tests the actual built IIFE, not the TypeScript source. Catches failure
5+
// modes that src/cdn.test.ts cannot: a broken tsup.cdn.js config, a missing
6+
// noExternal entry, an unreplaced __SDK_VERSION__ placeholder, or an IIFE
7+
// wrapper that clobbers the side-effect global assignment.
8+
//
9+
// Runs under testEnvironment: 'jsdom' so `window`, `navigator`, and
10+
// `globalThis` are all present — matching a real <script>-tag load.
11+
12+
const ARTIFACT_PATH = path.resolve(
13+
__dirname,
14+
'../dist/cdn/imtbl-audience.global.js',
15+
);
16+
17+
type AudienceGlobal = {
18+
Audience: { init: unknown };
19+
AudienceError: unknown;
20+
AudienceEvents: Record<string, string>;
21+
IdentityType: Record<string, string>;
22+
canIdentify: unknown;
23+
canTrack: unknown;
24+
version: string;
25+
};
26+
27+
describe('CDN bundle artifact', () => {
28+
let g: AudienceGlobal;
29+
30+
beforeAll(() => {
31+
if (!fs.existsSync(ARTIFACT_PATH)) {
32+
throw new Error(
33+
`CDN artifact not found at ${ARTIFACT_PATH}. `
34+
+ 'Run `pnpm transpile:cdn` (or `pnpm build`) before this test.',
35+
);
36+
}
37+
const source = fs.readFileSync(ARTIFACT_PATH, 'utf8');
38+
// Evaluates the pre-built bundle in the test's realm, the same way a
39+
// <script> tag does. Not user input — it's our own build output — so
40+
// the implied-eval rule doesn't apply. vm.runInThisContext was tried
41+
// first but runs in Node's root context, bypassing jsdom's window.
42+
// eslint-disable-next-line @typescript-eslint/no-implied-eval, no-new-func
43+
new Function(source)();
44+
g = (globalThis as unknown as { ImmutableAudience: AudienceGlobal })
45+
.ImmutableAudience;
46+
});
47+
48+
afterAll(() => {
49+
delete (globalThis as unknown as { ImmutableAudience?: unknown })
50+
.ImmutableAudience;
51+
});
52+
53+
it('attaches ImmutableAudience to globalThis as a side effect', () => {
54+
expect(g).toBeDefined();
55+
});
56+
57+
it('exposes every runtime value that the npm entry exports', () => {
58+
expect(typeof g.Audience).toBe('function');
59+
expect(typeof g.Audience.init).toBe('function');
60+
expect(typeof g.AudienceError).toBe('function');
61+
expect(typeof g.AudienceEvents).toBe('object');
62+
expect(typeof g.IdentityType).toBe('object');
63+
expect(typeof g.canIdentify).toBe('function');
64+
expect(typeof g.canTrack).toBe('function');
65+
expect(typeof g.version).toBe('string');
66+
});
67+
68+
it('replaces the __SDK_VERSION__ placeholder at build time', () => {
69+
expect(g.version).not.toBe('__SDK_VERSION__');
70+
expect(g.version.length).toBeGreaterThan(0);
71+
});
72+
73+
it('populates the IdentityType enum', () => {
74+
expect(g.IdentityType.Passport).toBe('passport');
75+
expect(g.IdentityType.Steam).toBe('steam');
76+
expect(g.IdentityType.Custom).toBe('custom');
77+
});
78+
});
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
22
"extends": "./tsconfig.json",
3-
"include": ["src"],
3+
"include": ["src", "test", "jest.cdn-artifact.config.ts"],
44
"exclude": []
55
}

packages/audience/sdk/tsconfig.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,12 @@
66
"customConditions": ["development"]
77
},
88
"include": ["src"],
9-
"exclude": ["dist", "jest.config.ts", "node_modules", "src/**/*.test.ts"]
9+
"exclude": [
10+
"dist",
11+
"jest.config.ts",
12+
"jest.cdn-artifact.config.ts",
13+
"node_modules",
14+
"src/**/*.test.ts",
15+
"test"
16+
]
1017
}

packages/audience/sdk/tsup.cdn.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export default defineConfig({
1616
target: 'es2018',
1717
platform: 'browser',
1818
dts: false,
19+
treeshake: true,
1920
// IIFE has no runtime module resolution — inline everything, including npm deps.
2021
noExternal: [/.*/],
2122
esbuildPlugins: [

packages/audience/sdk/tsup.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export default defineConfig((options) => {
2929
modules: ['crypto', 'buffer', 'process'],
3030
}),
3131
replace({
32-
__SDK_VERSION__: pkg.version === '0.0.0' ? '2.0.0' : pkg.version,
32+
__SDK_VERSION__: pkg.version === '0.0.0' ? '0.0.0-local' : pkg.version,
3333
}),
3434
],
3535
};

0 commit comments

Comments
 (0)