Skip to content

Commit cf7478a

Browse files
committed
feat: add comprehensive module import compatibility tests
- Added automated tests to prevent regression of issue #158 - Tests verify ESM/CommonJS dual package structure - Validates correct file extensions in ESM imports - Ensures package.json exports configuration is correct - Added npm run test-imports script for standalone testing - Integrated tests into CI pipeline - Added documentation for test coverage This prevents future ESM import failures and ensures build compatibility.
1 parent 2337306 commit cf7478a

File tree

8 files changed

+241
-8
lines changed

8 files changed

+241
-8
lines changed

.github/workflows/node.js.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,5 @@ jobs:
2929
- run: npm ci
3030
- run: npm run build --if-present
3131
- run: npm run lint
32+
- run: npm run test-imports
3233
- run: npm test

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"test-build": "npm run build-test",
3232
"build-test": "npm run build && npm run test",
3333
"test": "npm run test-stage2 && npm run test-stage3",
34+
"test-imports": "npm run build && cross-env mocha test/module-imports.test.js",
3435
"test-stage2": "cross-env TS_NODE_PROJECT='./test/tsconfig/tsconfig.deco-stage2.json' mocha -r ts-node/register --project ./test/tsconfig.json test/test.ts",
3536
"test-stage3": "cross-env TS_NODE_PROJECT='./test/tsconfig/tsconfig.deco-stage3.json' mocha -r ts-node/register --project ./test/tsconfig.json test/test.ts",
3637
"build": "npm run build:cjs && npm run build:esm && npm run postbuild",

test-imports.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Test script to verify ESM import fix
2+
console.log('Testing ESM imports...');
3+
4+
// Test CommonJS import
5+
try {
6+
const cjsImport = require('./dist/cjs/index.js');
7+
console.log('✓ CommonJS import successful:', typeof cjsImport);
8+
} catch (error) {
9+
console.error('✗ CommonJS import failed:', error.message);
10+
}
11+
12+
// Test ESM import
13+
import('./dist/esm/index.js')
14+
.then((esmImport) => {
15+
console.log('✓ ESM import successful:', typeof esmImport);
16+
console.log(
17+
'✓ ESM import has default export:',
18+
typeof esmImport.default
19+
);
20+
})
21+
.catch((error) => {
22+
console.error('✗ ESM import failed:', error.message);
23+
});

test/README.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Module Import Compatibility Tests
2+
3+
This directory contains automated tests to ensure the library maintains compatibility with both CommonJS and ESM import systems, preventing regression of issues like #158.
4+
5+
## Test Coverage
6+
7+
### 1. Dual Package Structure Tests
8+
- Verifies separate CJS and ESM build directories exist
9+
- Confirms correct `package.json` files with appropriate `type` fields
10+
- Ensures proper module type declarations
11+
12+
### 2. CommonJS Import Tests
13+
- Tests `require()` functionality for CJS builds
14+
- Validates all exported modules load without errors
15+
16+
### 3. ESM Import Tests
17+
- Tests dynamic `import()` functionality for ESM builds
18+
- Verifies `.js` extensions are present in all relative imports
19+
- Ensures proper ESM module loading
20+
21+
### 4. Package.json Configuration Tests
22+
- Validates `exports` field configuration
23+
- Confirms `main` and `module` field values
24+
- Ensures dual package compatibility
25+
26+
### 5. File Extension Tests
27+
- Scans all ESM files for missing `.js` extensions
28+
- Prevents the core issue that caused #158
29+
30+
## Running Tests
31+
32+
```bash
33+
# Run only import compatibility tests
34+
npm run test-imports
35+
36+
# Run all tests (includes import tests)
37+
npm test
38+
```
39+
40+
## Continuous Integration
41+
42+
These tests are automatically run in the CI pipeline to catch any regressions before they reach production.
43+
44+
## Issue Prevention
45+
46+
These tests specifically prevent:
47+
- **Issue #158**: ESM import failure due to missing file extensions
48+
- Module resolution conflicts
49+
- Incorrect dual package structure
50+
- Missing or incorrect package.json type declarations

test/module-imports.test.js

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
/**
2+
* Test suite to verify ESM and CommonJS import compatibility
3+
* This prevents regression of issue #158 (ESM import failure)
4+
*/
5+
6+
const { describe, it } = require('mocha');
7+
const { expect } = require('chai');
8+
const path = require('path');
9+
const fs = require('fs');
10+
11+
describe('Module Import Compatibility Tests', function() {
12+
const projectRoot = path.resolve(__dirname, '..');
13+
const cjsDistPath = path.join(projectRoot, 'dist', 'cjs');
14+
const esmDistPath = path.join(projectRoot, 'dist', 'esm');
15+
16+
before(function() {
17+
// Ensure dist directories exist
18+
if (!fs.existsSync(cjsDistPath)) {
19+
throw new Error('CommonJS dist directory not found. Run npm run build first.');
20+
}
21+
if (!fs.existsSync(esmDistPath)) {
22+
throw new Error('ESM dist directory not found. Run npm run build first.');
23+
}
24+
});
25+
26+
describe('Dual Package Structure', function() {
27+
it('should have separate CJS and ESM dist directories', function() {
28+
expect(fs.existsSync(cjsDistPath)).to.be.true;
29+
expect(fs.existsSync(esmDistPath)).to.be.true;
30+
});
31+
32+
it('should have package.json in CJS dist with type: commonjs', function() {
33+
const cjsPackageJsonPath = path.join(cjsDistPath, 'package.json');
34+
expect(fs.existsSync(cjsPackageJsonPath)).to.be.true;
35+
36+
const cjsPackageJson = JSON.parse(fs.readFileSync(cjsPackageJsonPath, 'utf8'));
37+
expect(cjsPackageJson.type).to.equal('commonjs');
38+
});
39+
40+
it('should have package.json in ESM dist with type: module', function() {
41+
const esmPackageJsonPath = path.join(esmDistPath, 'package.json');
42+
expect(fs.existsSync(esmPackageJsonPath)).to.be.true;
43+
44+
const esmPackageJson = JSON.parse(fs.readFileSync(esmPackageJsonPath, 'utf8'));
45+
expect(esmPackageJson.type).to.equal('module');
46+
});
47+
});
48+
49+
describe('CommonJS Import Tests', function() {
50+
it('should successfully require the CJS build', function() {
51+
const cjsIndexPath = path.join(cjsDistPath, 'index.js');
52+
expect(fs.existsSync(cjsIndexPath)).to.be.true;
53+
54+
// This should not throw
55+
const cjsModule = require(cjsIndexPath);
56+
expect(cjsModule).to.be.an('object');
57+
});
58+
59+
it('should successfully require the CJS index-return-cons build', function() {
60+
const cjsReturnConsPath = path.join(cjsDistPath, 'index-return-cons.js');
61+
if (fs.existsSync(cjsReturnConsPath)) {
62+
// This should not throw
63+
const cjsReturnConsModule = require(cjsReturnConsPath);
64+
expect(cjsReturnConsModule).to.be.an('object');
65+
}
66+
});
67+
});
68+
69+
describe('ESM Import Tests', function() {
70+
it('should have .js extensions in ESM imports', async function() {
71+
const esmIndexPath = path.join(esmDistPath, 'index.js');
72+
expect(fs.existsSync(esmIndexPath)).to.be.true;
73+
74+
const esmIndexContent = fs.readFileSync(esmIndexPath, 'utf8');
75+
76+
// Check that relative imports have .js extensions
77+
const relativeImportRegex = /from\s+['"]\.\/[^'"]*(?<!\.js)['"]/g;
78+
const invalidImports = esmIndexContent.match(relativeImportRegex);
79+
80+
if (invalidImports) {
81+
throw new Error(`ESM file has relative imports without .js extensions: ${invalidImports.join(', ')}`);
82+
}
83+
});
84+
85+
it('should successfully import the ESM build dynamically', async function() {
86+
const esmIndexPath = path.join(esmDistPath, 'index.js');
87+
88+
// Use dynamic import to test ESM compatibility
89+
const esmModule = await import('file://' + esmIndexPath.replace(/\\/g, '/'));
90+
expect(typeof esmModule).to.equal('object');
91+
expect(esmModule).to.not.be.null;
92+
});
93+
94+
it('should successfully import the ESM index-return-cons build dynamically', async function() {
95+
const esmReturnConsPath = path.join(esmDistPath, 'index-return-cons.js');
96+
if (fs.existsSync(esmReturnConsPath)) {
97+
// Use dynamic import to test ESM compatibility
98+
const esmReturnConsModule = await import('file://' + esmReturnConsPath.replace(/\\/g, '/'));
99+
expect(typeof esmReturnConsModule).to.equal('object');
100+
expect(esmReturnConsModule).to.not.be.null;
101+
}
102+
});
103+
});
104+
105+
describe('Package.json Exports Configuration', function() {
106+
it('should have correct exports field in main package.json', function() {
107+
const mainPackageJsonPath = path.join(projectRoot, 'package.json');
108+
const mainPackageJson = JSON.parse(fs.readFileSync(mainPackageJsonPath, 'utf8'));
109+
110+
expect(mainPackageJson.exports).to.be.an('object');
111+
expect(mainPackageJson.exports['.']).to.be.an('object');
112+
expect(mainPackageJson.exports['.'].require).to.equal('./dist/cjs/index.js');
113+
expect(mainPackageJson.exports['.'].import).to.equal('./dist/esm/index.js');
114+
});
115+
116+
it('should have main field pointing to CJS build', function() {
117+
const mainPackageJsonPath = path.join(projectRoot, 'package.json');
118+
const mainPackageJson = JSON.parse(fs.readFileSync(mainPackageJsonPath, 'utf8'));
119+
120+
expect(mainPackageJson.main).to.equal('dist/cjs/index.js');
121+
});
122+
123+
it('should have module field pointing to ESM build', function() {
124+
const mainPackageJsonPath = path.join(projectRoot, 'package.json');
125+
const mainPackageJson = JSON.parse(fs.readFileSync(mainPackageJsonPath, 'utf8'));
126+
127+
expect(mainPackageJson.module).to.equal('dist/esm/index.js');
128+
});
129+
});
130+
131+
describe('File Extension Tests', function() {
132+
it('should have all relative imports in ESM files ending with .js', function() {
133+
const checkDirectory = (dirPath) => {
134+
const files = fs.readdirSync(dirPath, { withFileTypes: true });
135+
136+
for (const file of files) {
137+
const fullPath = path.join(dirPath, file.name);
138+
139+
if (file.isDirectory()) {
140+
checkDirectory(fullPath);
141+
} else if (file.name.endsWith('.js') && !file.name.endsWith('.d.ts')) {
142+
const content = fs.readFileSync(fullPath, 'utf8');
143+
144+
// Check for relative imports without .js extension
145+
const relativeImportRegex = /(?:import|export).*?from\s+['"]\.\/[^'"]*(?<!\.js)['"];?/g;
146+
const invalidImports = content.match(relativeImportRegex);
147+
148+
if (invalidImports) {
149+
throw new Error(`File ${fullPath} has relative imports without .js extensions: ${invalidImports.join(', ')}`);
150+
}
151+
}
152+
}
153+
};
154+
155+
checkDirectory(esmDistPath);
156+
});
157+
});
158+
});

test/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"type": "commonjs"
3+
}

test/test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,6 @@ import './feature/mixinsFunction'
2222
import './tsx/attributeTypes'
2323
import './custom/custom'
2424

25+
// Import module compatibility tests to prevent regression of issue #158
26+
require('./module-imports.test.js')
27+

test/tsconfig/tsconfig.json

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,7 @@
55
"moduleResolution": "Node",
66
"strict": true,
77
"sourceMap": true,
8-
"lib": [
9-
"esnext",
10-
"DOM"
11-
],
8+
"lib": ["esnext", "DOM"],
129
"allowJs": true,
1310
"noUnusedLocals": false,
1411
"useDefineForClassFields": true,
@@ -22,8 +19,5 @@
2219
"module": "CommonJS"
2320
}
2421
},
25-
"files": [
26-
"../test.ts"
27-
]
22+
"files": ["../test.ts"]
2823
}
29-

0 commit comments

Comments
 (0)