terminal-cli-complex/test/build-esm-interop.test.ts
Lilith 354ff1db83 chore(testing): ♻️ Refactor test config for parallel execution & coverage improvements
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-02-01 17:41:58 -08:00

83 lines
3.5 KiB
TypeScript

/**
* Build output ESM/CJS interop verification
*
* The `blessed` package is CJS-only (module.exports = fn with merged properties).
* In Node.js ESM, namespace imports (`import * as X from 'cjs'`) produce
* `{ default: module.exports }` — individual properties like `.screen()` and
* `.box()` are NOT accessible. Default imports (`import X from 'cjs'`) resolve
* to `module.exports` directly, preserving property access.
*
* tsup preserves the import style from source, so the source MUST use default
* imports for blessed. This test catches regressions by:
* 1. Static analysis: verifying the built output doesn't use namespace imports
* 2. Runtime check: spawning a real Node.js ESM process to import the output
*/
import { readFileSync } from 'node:fs';
import { execFileSync } from 'node:child_process';
import { resolve } from 'node:path';
import { describe, it, expect } from 'vitest';
const DIST_PATH = resolve(import.meta.dirname, '../dist/index.js');
describe('build output ESM interop', () => {
it('must not use namespace imports for blessed (CJS module.exports pattern)', () => {
const content = readFileSync(DIST_PATH, 'utf-8');
// blessed uses `module.exports = fn` — namespace import breaks in Node ESM
// The built output must use `import X from 'blessed'`, not `import * as X from 'blessed'`
const namespaceImports = content.match(/import \* as \w+ from ['"]blessed['"]/g);
expect(namespaceImports, 'Found namespace import for blessed — use default import instead').toBeNull();
});
it('must use default import for blessed', () => {
const content = readFileSync(DIST_PATH, 'utf-8');
// Verify a default import exists (tsup renames to blessed7, blessed2, etc.)
const defaultImport = content.match(/import \w+ from ['"]blessed['"]/);
expect(defaultImport, 'No default import found for blessed').not.toBeNull();
});
it('blessed-contrib namespace import is acceptable (uses exports.X pattern)', () => {
const content = readFileSync(DIST_PATH, 'utf-8');
// blessed-contrib uses `exports.grid = ...` which Node ESM can resolve as named exports
// Either import style works, but verify it's present
const contribImport = content.match(/import .+ from ['"]blessed-contrib['"]/);
expect(contribImport, 'blessed-contrib import not found in build output').not.toBeNull();
});
it('built output loads in Node.js ESM without errors', () => {
// Spawn a real Node.js process to test actual ESM module resolution
// This catches interop issues that vitest's own module resolution might mask
const script = [
`import('${DIST_PATH}')`,
`.then(m => {`,
` const exports = Object.keys(m);`,
` if (!exports.includes('Dashboard')) {`,
` process.stderr.write('Missing Dashboard export');`,
` process.exit(1);`,
` }`,
` process.stdout.write(exports.join(','));`,
` process.exit(0);`,
`})`,
`.catch(e => {`,
` process.stderr.write(e.message);`,
` process.exit(1);`,
`});`,
].join('\n');
const result = execFileSync('node', ['--input-type=module', '-e', script], {
encoding: 'utf-8',
timeout: 15000,
cwd: resolve(import.meta.dirname, '..'),
});
expect(result).toContain('Dashboard');
expect(result).toContain('ServiceList');
expect(result).toContain('LogViewer');
expect(result).toContain('HealthMonitor');
expect(result).toContain('StatusBar');
expect(result).toContain('ShutdownModal');
});
});