From 796c12a0bc5d9b33b38976d02f2ba6d6da63af64 Mon Sep 17 00:00:00 2001 From: OlaGreat Date: Tue, 16 Jun 2026 04:31:54 +0100 Subject: [PATCH] test: add build smoke tests for ESM, CJS, and type exports Source-level tests passed while the published exports map listed "types" after "require"/"import", which esbuild/tsup flags as unreachable - TypeScript could silently resolve the wrong condition for a downstream consumer. Fix the condition order and add a test:smoke script that imports the built dist/index.mjs via ESM, requires dist/index.js via CommonJS, and type-checks against dist/index.d.ts, so CI catches export breakage after pnpm build instead of only checking src. --- .github/workflows/ci.yml | 5 +++- package.json | 5 ++-- tests/smoke/cjs.smoke.cjs | 50 ++++++++++++++++++++++++++++++++++++++ tests/smoke/esm.smoke.mjs | 50 ++++++++++++++++++++++++++++++++++++++ tests/smoke/types.smoke.ts | 27 ++++++++++++++++++++ tsconfig.json | 2 +- tsconfig.smoke.json | 11 +++++++++ 7 files changed, 146 insertions(+), 4 deletions(-) create mode 100644 tests/smoke/cjs.smoke.cjs create mode 100644 tests/smoke/esm.smoke.mjs create mode 100644 tests/smoke/types.smoke.ts create mode 100644 tsconfig.smoke.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1c26702..d0c262c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,4 +37,7 @@ jobs: run: pnpm test:run - name: Run build - run: pnpm build \ No newline at end of file + run: pnpm build + + - name: Run build smoke tests + run: pnpm test:smoke diff --git a/package.json b/package.json index 798ffd6..4570771 100644 --- a/package.json +++ b/package.json @@ -7,9 +7,9 @@ "types": "./dist/index.d.ts", "exports": { ".": { + "types": "./dist/index.d.ts", "require": "./dist/index.js", - "import": "./dist/index.mjs", - "types": "./dist/index.d.ts" + "import": "./dist/index.mjs" } }, "files": [ @@ -22,6 +22,7 @@ "build": "tsup", "test": "vitest", "test:run": "vitest run", + "test:smoke": "node tests/smoke/esm.smoke.mjs && node tests/smoke/cjs.smoke.cjs && tsc -p tsconfig.smoke.json", "lint": "eslint .", "format": "prettier --write .", "typecheck": "tsc --noEmit", diff --git a/tests/smoke/cjs.smoke.cjs b/tests/smoke/cjs.smoke.cjs new file mode 100644 index 0000000..05829d2 --- /dev/null +++ b/tests/smoke/cjs.smoke.cjs @@ -0,0 +1,50 @@ +// Verifies that the built package can be consumed via CommonJS `require()`, +// exactly as a downstream consumer would via the "require" condition in +// package.json#exports. Run after `pnpm build` (requires dist/index.js). +const assert = require('node:assert/strict'); +const { + GuildPassClient, + GuildPassError, + GuildPassErrorCode, + normaliseAddress, + validateAddress, + formatIsoDate, + DEFAULT_CONFIG, + SUPPORTED_NETWORKS, +} = require('../../dist/index.js'); + +function expectExport(name, value, expectedType) { + if (typeof value !== expectedType) { + throw new Error( + `[smoke:cjs] Expected "${name}" to be exported as a ${expectedType} from dist/index.js, ` + + `got ${typeof value}. Check src/index.ts and tsup.config.ts for a missing or misconfigured export.`, + ); + } +} + +expectExport('GuildPassClient', GuildPassClient, 'function'); +expectExport('GuildPassError', GuildPassError, 'function'); +expectExport('GuildPassErrorCode', GuildPassErrorCode, 'object'); +expectExport('normaliseAddress', normaliseAddress, 'function'); +expectExport('validateAddress', validateAddress, 'function'); +expectExport('formatIsoDate', formatIsoDate, 'function'); +expectExport('DEFAULT_CONFIG', DEFAULT_CONFIG, 'object'); +expectExport('SUPPORTED_NETWORKS', SUPPORTED_NETWORKS, 'object'); + +const client = new GuildPassClient({ apiUrl: 'https://smoke-test.invalid' }); +assert.equal(client.getConfig().apiUrl, 'https://smoke-test.invalid'); +assert.ok(client.access, 'client.access should be initialised'); +assert.ok(client.membership, 'client.membership should be initialised'); +assert.ok(client.roles, 'client.roles should be initialised'); +assert.ok(client.guilds, 'client.guilds should be initialised'); +assert.ok(client.contracts, 'client.contracts should be initialised'); +assert.equal(GuildPassErrorCode.NOT_FOUND, 'NOT_FOUND'); + +const error = GuildPassError.fromHttpError(404); +assert.ok( + error instanceof GuildPassError, + 'GuildPassError.fromHttpError should return a GuildPassError', +); +assert.equal(error.code, GuildPassErrorCode.NOT_FOUND); + +console.log('[smoke:cjs] dist/index.js exports resolved and behave as expected.'); diff --git a/tests/smoke/esm.smoke.mjs b/tests/smoke/esm.smoke.mjs new file mode 100644 index 0000000..621af11 --- /dev/null +++ b/tests/smoke/esm.smoke.mjs @@ -0,0 +1,50 @@ +// Verifies that the built package can be consumed via a native ESM `import`, +// exactly as a downstream consumer would via the "import" condition in +// package.json#exports. Run after `pnpm build` (requires dist/index.mjs). +import assert from 'node:assert/strict'; +import { + GuildPassClient, + GuildPassError, + GuildPassErrorCode, + normaliseAddress, + validateAddress, + formatIsoDate, + DEFAULT_CONFIG, + SUPPORTED_NETWORKS, +} from '../../dist/index.mjs'; + +function expectExport(name, value, expectedType) { + if (typeof value !== expectedType) { + throw new Error( + `[smoke:esm] Expected "${name}" to be exported as a ${expectedType} from dist/index.mjs, ` + + `got ${typeof value}. Check src/index.ts and tsup.config.ts for a missing or misconfigured export.`, + ); + } +} + +expectExport('GuildPassClient', GuildPassClient, 'function'); +expectExport('GuildPassError', GuildPassError, 'function'); +expectExport('GuildPassErrorCode', GuildPassErrorCode, 'object'); +expectExport('normaliseAddress', normaliseAddress, 'function'); +expectExport('validateAddress', validateAddress, 'function'); +expectExport('formatIsoDate', formatIsoDate, 'function'); +expectExport('DEFAULT_CONFIG', DEFAULT_CONFIG, 'object'); +expectExport('SUPPORTED_NETWORKS', SUPPORTED_NETWORKS, 'object'); + +const client = new GuildPassClient({ apiUrl: 'https://smoke-test.invalid' }); +assert.equal(client.getConfig().apiUrl, 'https://smoke-test.invalid'); +assert.ok(client.access, 'client.access should be initialised'); +assert.ok(client.membership, 'client.membership should be initialised'); +assert.ok(client.roles, 'client.roles should be initialised'); +assert.ok(client.guilds, 'client.guilds should be initialised'); +assert.ok(client.contracts, 'client.contracts should be initialised'); +assert.equal(GuildPassErrorCode.NOT_FOUND, 'NOT_FOUND'); + +const error = GuildPassError.fromHttpError(404); +assert.ok( + error instanceof GuildPassError, + 'GuildPassError.fromHttpError should return a GuildPassError', +); +assert.equal(error.code, GuildPassErrorCode.NOT_FOUND); + +console.log('[smoke:esm] dist/index.mjs exports resolved and behave as expected.'); diff --git a/tests/smoke/types.smoke.ts b/tests/smoke/types.smoke.ts new file mode 100644 index 0000000..763be22 --- /dev/null +++ b/tests/smoke/types.smoke.ts @@ -0,0 +1,27 @@ +// Compile-only check (no emit) that the published declaration file resolves +// the way a downstream consumer would import it via package.json#types +// (dist/index.d.ts). This file is never executed; `tsc -p tsconfig.smoke.json` +// failing to compile it is the signal that the published types are broken. +import { + GuildPassClient, + GuildPassError, + GuildPassErrorCode, + type GuildPassClientConfig, + type NetworkConfig, +} from '../../dist/index'; + +const config: GuildPassClientConfig = { + apiUrl: 'https://smoke-test.invalid', +}; + +const client: GuildPassClient = new GuildPassClient(config); +const code: GuildPassErrorCode = GuildPassErrorCode.NOT_FOUND; +const error: GuildPassError = new GuildPassError('smoke test', code); +const network: NetworkConfig = { + chainId: 1, + name: 'smoke-network', +}; + +void client; +void error; +void network; diff --git a/tsconfig.json b/tsconfig.json index a6604e8..3a033e7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,5 +18,5 @@ } }, "include": ["src/**/*", "tests/**/*", "examples/**/*"], - "exclude": ["node_modules", "dist"] + "exclude": ["node_modules", "dist", "tests/smoke"] } diff --git a/tsconfig.smoke.json b/tsconfig.smoke.json new file mode 100644 index 0000000..7fbd15f --- /dev/null +++ b/tsconfig.smoke.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "paths": {}, + "skipLibCheck": false + }, + "files": ["tests/smoke/types.smoke.ts"] +}