diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 932896c..9e36181 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,3 +37,6 @@ jobs: - name: Run tests run: npm test + + - name: Smoke test packaged artifact + run: npm run test:smoke diff --git a/package.json b/package.json index fb3b53e..ac758dc 100644 --- a/package.json +++ b/package.json @@ -5,14 +5,19 @@ "registry": "https://npm.pkg.github.com" }, "description": "Comprehensive, fully-typed Node.js/TypeScript library for the SuperOps.ai GraphQL API", - "main": "dist/index.js", - "module": "dist/index.mjs", - "types": "dist/index.d.ts", + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", "exports": { ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.mjs", - "require": "./dist/index.js" + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } } }, "files": [ @@ -23,6 +28,7 @@ "test": "vitest run", "test:watch": "vitest", "test:coverage": "vitest run --coverage", + "test:smoke": "node scripts/smoke-test.mjs", "lint": "eslint src tests --ext .ts", "typecheck": "tsc --noEmit", "prepublishOnly": "npm run build" diff --git a/scripts/smoke-test.mjs b/scripts/smoke-test.mjs new file mode 100644 index 0000000..05c3129 --- /dev/null +++ b/scripts/smoke-test.mjs @@ -0,0 +1,84 @@ +#!/usr/bin/env node +/** + * Package smoke test. + * + * Unit and integration tests run against `src/` through TypeScript — they + * never load `dist/` or resolve the package.json `exports` map. A broken + * exports map (see issue #2: a phantom `dist/index.mjs`) therefore passes + * every other check and only fails for real consumers. + * + * This test packs the tarball exactly as `npm publish` would, installs it + * into a throwaway directory, and loads it by package name via both + * `import` and `require`. If either entry point or a known export is + * missing, it exits non-zero. + */ +import { execFileSync } from 'node:child_process'; +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const PKG = '@wyre-technology/node-superops'; +const EXPECTED_EXPORT = 'SuperOpsClient'; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const workDir = mkdtempSync(join(tmpdir(), 'superops-smoke-')); + +/** Run a command, surfacing stdout/stderr on failure. */ +function run(cmd, args, opts = {}) { + return execFileSync(cmd, args, { encoding: 'utf8', stdio: 'pipe', ...opts }); +} + +try { + console.log('• Packing tarball…'); + const packed = JSON.parse( + run('npm', ['pack', '--json', '--pack-destination', workDir], { cwd: root }), + ); + const tarball = join(workDir, packed[0].filename); + + console.log('• Installing tarball into a clean directory…'); + writeFileSync( + join(workDir, 'package.json'), + JSON.stringify({ name: 'smoke-consumer', private: true, version: '0.0.0' }), + ); + run('npm', ['install', '--no-save', '--silent', tarball], { cwd: workDir }); + + console.log(`• Resolving "${PKG}" via import (ESM)…`); + run( + 'node', + [ + '--input-type=module', + '-e', + `import('${PKG}').then((m) => { + if (typeof m.${EXPECTED_EXPORT} !== 'function') { + throw new Error('ESM entry missing export ${EXPECTED_EXPORT}'); + } + });`, + ], + { cwd: workDir }, + ); + + console.log(`• Resolving "${PKG}" via require (CJS)…`); + run( + 'node', + [ + '--input-type=commonjs', + '-e', + `const m = require('${PKG}'); + if (typeof m.${EXPECTED_EXPORT} !== 'function') { + throw new Error('CJS entry missing export ${EXPECTED_EXPORT}'); + }`, + ], + { cwd: workDir }, + ); + + console.log('\n✓ Smoke test passed — package resolves via import and require.'); +} catch (err) { + console.error('\n✗ Smoke test failed.\n'); + if (err.stdout) console.error(err.stdout); + if (err.stderr) console.error(err.stderr); + else console.error(err.message); + process.exitCode = 1; +} finally { + rmSync(workDir, { recursive: true, force: true }); +}