83 lines
3.5 KiB
TypeScript
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');
|
|
});
|
|
});
|