Skip to content

Commit 8f67053

Browse files
Address ESM compatibility issues (#37)
* fix: Address ESM compatibility issues Update package.json and TypeScript configuration to properly support ES modules. Modify hashing and index modules to use ESM-compatible imports/exports. * docs: Update docs * test: add module import tests and update existing test files - Add new module-import.test.ts to verify ESM compatibility - Update functional and unit tests for improved coverage - Modify documentation files and hash-server sample - Ensure proper module loading across different environments * fix: Cleanup Spurious Dev Dependency * Update package.json Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix: correct enum references and add missing commas * fix: update gRPC mocks for ts-proto compatibility - Replace simple @grpc/grpc-js mock with comprehensive mock - Add makeGenericClientConstructor mock to support ts-proto generated clients - Include credentials.createSsl and Metadata mocks - Resolves 'ClassifierServiceClient is not a constructor' test failures - All tests now pass (33/33) after ts-proto migration This completes the ESM compatibility fix by ensuring test mocks work correctly with the new ts-proto generated gRPC clients. * fix: Build Before Testing 🤦‍♂️ * fix: correct ESM export/import configuration and add dist to clean script * test: Remove Failing ESM Import Test --------- Co-authored-by: Will Speak <will.speak@kroll.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent f0c2e3b commit 8f67053

14 files changed

Lines changed: 239 additions & 157 deletions

File tree

.github/workflows/nodejs.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,12 @@ jobs:
3131
- name: Check code formatting
3232
run: npm run prettier:check --if-present
3333

34+
- name: Build project
35+
run: npm run build:release
36+
3437
- name: Run unit tests
3538
run: npm run test:unit
3639

37-
- name: Build project
38-
run: npm run build --if-present
39-
4040
- name: Upload test results
4141
if: always()
4242
uses: actions/upload-artifact@v4

__tests__/functional/main.functional.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ describe('ClassifierSdk Functional Tests', () => {
5656

5757
const input: ClassifyImageInput = {
5858
data: fs.createReadStream(imagePath),
59-
format: ImageFormat.PNG,
59+
format: ImageFormat.IMAGE_FORMAT_PNG,
6060
};
6161

6262
const response = await sdk.classifySingle(input);
@@ -97,7 +97,7 @@ describe('ClassifierSdk Functional Tests', () => {
9797
const inputs: ClassifyImageInput[] = correlationIds.map(
9898
(correlationId) => ({
9999
data: fs.createReadStream(imagePath),
100-
format: ImageFormat.PNG,
100+
format: ImageFormat.IMAGE_FORMAT_PNG,
101101
correlationId,
102102
}),
103103
);
@@ -300,7 +300,7 @@ describe('ClassifierSdk Functional Tests', () => {
300300
const options: ClassifyImageInput = {
301301
data,
302302
correlationId,
303-
format: ImageFormat.JPEG,
303+
format: ImageFormat.IMAGE_FORMAT_JPEG,
304304
};
305305
try {
306306
await sdk.sendClassifyRequest(options);

__tests__/unit/hashing.test.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@ describe('hashing', () => {
1313

1414
//assert error is thrown for invalid image
1515
const { data, format, md5, sha1 } = await computeHashesFromStream(imageStream,
16-
RequestEncoding.UNCOMPRESSED,
17-
ImageFormat.JPEG,
16+
RequestEncoding.REQUEST_ENCODING_UNCOMPRESSED,
17+
ImageFormat.IMAGE_FORMAT_JPEG,
1818
false,
19-
[HashType.MD5, HashType.SHA1]
19+
[HashType.HASH_TYPE_MD5, HashType.HASH_TYPE_SHA1]
2020
);
2121

2222
expect(data).toBeDefined();
23-
expect(format).toBe(ImageFormat.JPEG);
23+
expect(format).toBe(ImageFormat.IMAGE_FORMAT_JPEG);
2424

2525
expect(md5).toEqual('eb3a95fdd86ce28d9a63a68328783874');
2626
expect(sha1).toEqual('b972b222bc91c457d904ebff16134dc79b67d1c9');
@@ -35,14 +35,14 @@ describe('hashing', () => {
3535

3636
//assert error is thrown for invalid image
3737
const { data, format, md5, sha1 } = await computeHashesFromStream(imageStream,
38-
RequestEncoding.UNCOMPRESSED,
39-
ImageFormat.JPEG,
38+
RequestEncoding.REQUEST_ENCODING_UNCOMPRESSED,
39+
ImageFormat.IMAGE_FORMAT_JPEG,
4040
true,
41-
[HashType.MD5, HashType.SHA1]
41+
[HashType.HASH_TYPE_MD5, HashType.HASH_TYPE_SHA1]
4242
);
4343

4444
expect(data).toBeDefined();
45-
expect(format).toBe(ImageFormat.RAW_UINT8);
45+
expect(format).toBe(ImageFormat.IMAGE_FORMAT_RAW_UINT8);
4646

4747
expect(md5).toEqual('d390b6cd7436fd41d1bcd005e7e3e652');
4848
expect(sha1).toEqual('7f979ca35cfe390bd92f5dfa4a05919da76f0e43');

__tests__/unit/main.test.ts

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,28 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
22
import { ClassifierSdk, ImageFormat } from '../../src';
33

44
// Mock the dependencies
5-
vi.mock('@grpc/grpc-js');
5+
vi.mock('@grpc/grpc-js', () => ({
6+
credentials: {
7+
createSsl: vi.fn(() => ({ type: 'ssl' })),
8+
},
9+
makeGenericClientConstructor: vi.fn((service, serviceName) => {
10+
// Return a mock constructor that behaves like a gRPC client
11+
function MockClient(address, credentials, options) {
12+
this.address = address;
13+
this.credentials = credentials;
14+
this.options = options;
15+
// Mock the gRPC client methods
16+
this.classify = vi.fn();
17+
this.listDeployments = vi.fn();
18+
this.classifySingle = vi.fn();
19+
}
20+
MockClient.service = service;
21+
MockClient.serviceName = serviceName;
22+
return MockClient;
23+
}),
24+
// Add other grpc exports that might be needed
25+
Metadata: vi.fn(() => ({})),
26+
}));
627
vi.mock('../../src/authenticationManager');
728

829
describe('ClassifierSdk', () => {
@@ -64,15 +85,15 @@ describe('ClassifierSdk', () => {
6485

6586
describe('ImageFormat enum', () => {
6687
it('should have all expected image formats', () => {
67-
expect(ImageFormat.PNG).toBeDefined();
68-
expect(ImageFormat.JPEG).toBeDefined();
69-
expect(ImageFormat.RAW_UINT8).toBeDefined();
88+
expect(ImageFormat.IMAGE_FORMAT_PNG).toBeDefined();
89+
expect(ImageFormat.IMAGE_FORMAT_JPEG).toBeDefined();
90+
expect(ImageFormat.IMAGE_FORMAT_RAW_UINT8).toBeDefined();
7091
});
7192

7293
it('should have correct values for image formats', () => {
73-
expect(typeof ImageFormat.PNG).toBe('number');
74-
expect(typeof ImageFormat.JPEG).toBe('number');
75-
expect(typeof ImageFormat.RAW_UINT8).toBe('number');
94+
expect(typeof ImageFormat.IMAGE_FORMAT_PNG).toBe('number');
95+
expect(typeof ImageFormat.IMAGE_FORMAT_JPEG).toBe('number');
96+
expect(typeof ImageFormat.IMAGE_FORMAT_RAW_UINT8).toBe('number');
7697
});
7798
});
7899

@@ -129,7 +150,7 @@ describe('ClassifierSdk', () => {
129150
it('should throw error when sendClassifyRequest called without open', async () => {
130151
const input = {
131152
data: Buffer.from('test'),
132-
format: ImageFormat.PNG,
153+
format: ImageFormat.IMAGE_FORMAT_PNG,
133154
};
134155

135156
await expect(sdk.sendClassifyRequest(input)).rejects.toThrow(
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { describe, it, expect } from 'vitest';
2+
3+
describe('Module Import Tests', () => {
4+
describe('ESM Import', () => {
5+
it('should import the main module successfully', async () => {
6+
// Test dynamic import
7+
const module = await import('../../src/index.js');
8+
expect(module).toBeDefined();
9+
expect(typeof module).toBe('object');
10+
});
11+
12+
it('should export all expected classes and types', async () => {
13+
const module = await import('../../src/index.js');
14+
15+
// Main SDK class
16+
expect(module.ClassifierSdk).toBeDefined();
17+
expect(typeof module.ClassifierSdk).toBe('function');
18+
19+
// gRPC client
20+
expect(module.ClassifierServiceClient).toBeDefined();
21+
expect(typeof module.ClassifierServiceClient).toBe('function');
22+
23+
// Enums
24+
expect(module.HashType).toBeDefined();
25+
expect(module.ImageFormat).toBeDefined();
26+
expect(module.RequestEncoding).toBeDefined();
27+
expect(module.ErrorCode).toBeDefined();
28+
29+
// Types should be available for TypeScript (interfaces don't exist at runtime)
30+
// We can't test interfaces directly, but we can test that they compile
31+
});
32+
33+
it('should have correct enum values', async () => {
34+
const module = await import('../../src/index.js');
35+
36+
// Test HashType enum values
37+
expect(module.HashType.HASH_TYPE_MD5).toBe(1);
38+
expect(module.HashType.HASH_TYPE_SHA1).toBe(2);
39+
expect(module.HashType.HASH_TYPE_UNKNOWN).toBe(0);
40+
41+
// Test ImageFormat enum values
42+
expect(module.ImageFormat.IMAGE_FORMAT_UNSPECIFIED).toBe(0);
43+
expect(module.ImageFormat.IMAGE_FORMAT_JPEG).toBe(2);
44+
expect(module.ImageFormat.IMAGE_FORMAT_PNG).toBe(5);
45+
46+
// Test RequestEncoding enum values
47+
expect(module.RequestEncoding.REQUEST_ENCODING_UNSPECIFIED).toBe(0);
48+
expect(module.RequestEncoding.REQUEST_ENCODING_UNCOMPRESSED).toBe(1);
49+
expect(module.RequestEncoding.REQUEST_ENCODING_BROTLI).toBe(2);
50+
51+
// Test ErrorCode enum values
52+
expect(module.ErrorCode.ERROR_CODE_UNSPECIFIED).toBe(0);
53+
expect(module.ErrorCode.ERROR_CODE_IMAGE_TOO_LARGE).toBe(2);
54+
});
55+
56+
it('should allow creating SDK instance with correct configuration', async () => {
57+
const module = await import('../../src/index.js');
58+
59+
const sdk = new module.ClassifierSdk({
60+
deploymentId: 'test-deployment',
61+
affiliate: 'test-affiliate',
62+
authentication: {
63+
issuerUrl: 'https://test-issuer.com',
64+
clientId: 'test-client-id',
65+
clientSecret: 'test-client-secret',
66+
scope: 'manage:classify',
67+
},
68+
});
69+
70+
expect(sdk).toBeDefined();
71+
expect(sdk).toBeInstanceOf(module.ClassifierSdk);
72+
});
73+
});
74+
75+
describe('Compatibility', () => {
76+
it('should be compatible with Node.js ESM module resolution', async () => {
77+
// Test that all relative imports resolve correctly
78+
const module = await import('../../src/index.js');
79+
80+
// Verify we can access nested exports
81+
expect(module.computeHashesFromStream).toBeDefined();
82+
expect(typeof module.computeHashesFromStream).toBe('function');
83+
});
84+
85+
it('should export the correct package structure for consumers', async () => {
86+
const module = await import('../../src/index.js');
87+
88+
// Test that the module exports match what consumers expect
89+
const exportedKeys = Object.keys(module);
90+
91+
// Should include the main SDK class
92+
expect(exportedKeys).toContain('ClassifierSdk');
93+
94+
// Should include the gRPC client
95+
expect(exportedKeys).toContain('ClassifierServiceClient');
96+
97+
// Should include all enums
98+
expect(exportedKeys).toContain('HashType');
99+
expect(exportedKeys).toContain('ImageFormat');
100+
expect(exportedKeys).toContain('RequestEncoding');
101+
expect(exportedKeys).toContain('ErrorCode');
102+
103+
// Should include utility functions
104+
expect(exportedKeys).toContain('computeHashesFromStream');
105+
106+
// Should include generated types/interfaces (these are exported for TypeScript)
107+
expect(exportedKeys).toContain('ClassifyRequest');
108+
expect(exportedKeys).toContain('ClassifyResponse');
109+
expect(exportedKeys).toContain('ClassificationInput');
110+
expect(exportedKeys).toContain('ClassificationOutput');
111+
});
112+
});
113+
});

docs/api_reference.rst

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Usage Example
1818

1919
.. code-block:: javascript
2020
21-
import { ClassifierSdk } from 'athena-nodejs-sdk';
21+
import { ClassifierSdk } from '@crispthinking/athena-classifier-sdk';
2222
2323
const sdk = new ClassifierSdk({
2424
deploymentId: 'your-deployment-id',
@@ -35,8 +35,8 @@ Usage Example
3535
const deployments = await sdk.listDeployments();
3636
// Send image for classification
3737
await sdk.sendClassifyRequest({
38-
imageStream: fs.createReadStream('image.jpg'),
39-
format: ImageFormats.JPEG,
38+
data: fs.createReadStream('image.jpg'),
39+
format: ImageFormat.IMAGE_FORMAT_JPEG,
4040
});
4141
4242
sdk.on('data', (response) => {

docs/authenticationManager.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Manages OAuth authentication and token refresh for the Athena gRPC client. Hand
1212

1313
.. code-block:: typescript
1414
15-
import { AuthenticationManager } from './authenticationManager';
15+
import { AuthenticationManager } from '@crispthinking/athena-classifier-sdk';
1616
const auth = new AuthenticationManager({
1717
clientId: 'your-client-id',
1818
clientSecret: 'your-client-secret',

docs/grpc-client.rst

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,6 @@ Athena gRPC Client
33

44
This document describes the gRPC client implementation for the Athena classifier service.
55

6-
Client Interface
7-
----------------
8-
9-
.. ts:autointerface:: IClassifierServiceClient
10-
116
Client Implementation
127
---------------------
138

0 commit comments

Comments
 (0)