Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
node_modules
build
__tests__/**/*.json
# Subpath facades generated by scripts/generate-resolver-facades.js
/helpers.js
/helpers.d.ts
/constants.js
/constants.d.ts
/models.d.ts
/publicReportingAPI.js
/publicReportingAPI.d.ts
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,12 @@ coverage.lcov
coverage/
.npmrc
build

# Subpath facades generated by scripts/generate-resolver-facades.js
/helpers.js
/helpers.d.ts
/constants.js
/constants.d.ts
/models.d.ts
/publicReportingAPI.js
/publicReportingAPI.d.ts
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
### Changed
- The client has been migrated to TypeScript. It now ships with bundled type
definitions (no separate `@types` package required) and exposes constants,
models and helpers via subpath imports (e.g.
`@reportportal/client-javascript/constants`).
- **Breaking Change** Deep `lib/**` imports published up to 5.5.x (e.g.
`require('@reportportal/client-javascript/lib/helpers')`) still resolve for Node,
TypeScript and bundlers via the `exports` map, but are no longer backed by physical files.
Tools that resolve imports by walking the filesystem instead of reading `exports` — most
notably `eslint-import-resolver-node`, the default resolver of `eslint-plugin-import` —
will report [`import/no-unresolved`](https://github.com/import-js/eslint-plugin-import/issues/1810) for these paths. Switch to the new subpath aliases
(`constants`, `models`, `helpers`, `publicReportingAPI`) instead.
### Added
- `retry_of` property is now automatically included in the `startTestItem`
request payload when `retry: true` and a previous attempt exists in the
Expand Down
38 changes: 38 additions & 0 deletions DEV_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,44 @@
Internal notes for contributors. This content is intentionally kept out of
README.md, which is published to package registries.

## Subpath facades

`npm run build` compiles to `build/` and then runs `scripts/generate-resolver-facades.js`,
which writes a one-line re-export at each public subpath alias — `helpers.js`,
`constants.js`, `publicReportingAPI.js` — together with matching `.d.ts` files. `models` only
gets a `.d.ts` facade: `src/models/**` is TypeScript types with no runtime value (confirmed —
the compiled `build/models/*.js` files are all empty `__esModule` stubs), so
`package.json#exports["./models"]` has no `import` / `require` condition. Node enforces that
regardless of whether a `.js` file physically exists at the root, so writing one would just be
misleading. If a consumer's TypeScript setup runs with `isolatedModules` and they import from
`models` without `import type`, they'll now get `ERR_PACKAGE_PATH_NOT_EXPORTED` at runtime
instead of a silently-empty object — that's intentional; there are no known consumers of this
brand-new subpath yet, so this is the cheapest point to make the contract strict. If a genuine
runtime value ever needs to live under `models`, move it to `constants/` (like `MERGE_TYPES`,
which used to live here) instead of adding a runtime condition back.

Supported package imports resolve through the `exports` / `typesVersions` maps in
`package.json` straight to `build` for Node, TypeScript and bundlers; these files are never
on that path. They exist only for tools that resolve imports by walking the filesystem
instead of reading `exports`, chiefly `eslint-import-resolver-node` (the default resolver of
`eslint-plugin-import`), which otherwise reports `import/no-unresolved` for these subpath
imports and forces each consumer to configure an ignore.

The alias list in the script is intentionally fixed and small — it does not mirror every
internal module under `build`. Deep `lib/**` imports as published up to 5.5.x (e.g.
`require('@reportportal/client-javascript/lib/rest')`) resolve via the `exports` map for
Node, TypeScript and bundlers, but are **not** backed by a physical `lib/**` tree — a
filesystem-based resolver hitting one of those undocumented deep paths still needs a local
ignore. If you're bumping a first-party agent past this version and it imports
`@reportportal/client-javascript/lib/**` directly, switch it to the matching short alias in
the same PR rather than adding it here.

Everything the script writes is gitignored and removed by `npm run clean`. It recomputes
each facade's export style (`export *` vs `export =` vs default) from the real compiled
module on every build, so it can't silently drift the way a hand-written file could. When
you add a new top-level subpath to `exports`, add the matching entry to the `ALIASES` map in
the script.

## Code knowledge graph

This repo carries a local **code knowledge graph** ([colbymchenry/codegraph](https://github.com/colbymchenry/codegraph))
Expand Down
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,28 @@ rpClient.checkConnect().then(() => {
});
```

## TypeScript

Starting from version 5.6.0, the client has been migrated to TypeScript and ships with bundled type definitions. It works with both CommonJS and ES module projects:

```typescript
import RPClient from '@reportportal/client-javascript';

const rpClient = new RPClient({
apiKey: 'reportportalApiKey',
endpoint: 'https://your-instance.com:8080/api/v1',
launch: 'LAUNCH_NAME',
project: 'PROJECT_NAME',
});
```

Constants, models and helpers are available via subpath imports:

```typescript
import { RP_STATUSES } from '@reportportal/client-javascript/constants';
import PublicReportingAPI from '@reportportal/client-javascript/publicReportingAPI';
```

## Configuration

When creating a client instance, you need to specify the following options.
Expand Down
22 changes: 14 additions & 8 deletions __tests__/client-id.spec.js → __tests__/client-id.spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
const fs = require('fs');
const util = require('util');
const path = require('path');
const { randomUUID } = require('crypto');
import fs from 'fs';
import util from 'util';
import path from 'path';
import { randomUUID } from 'crypto';

const testHomeDir = path.join(__dirname, '__tmp__', 'rp-home');
process.env.RP_CLIENT_JS_HOME = testHomeDir;
const { getClientId } = require('../src/statistics/client-id');

// A static `import` would be hoisted above the `process.env` assignment above, so the module
// (which reads RP_CLIENT_JS_HOME at load time) is loaded lazily via a dynamic import instead.
let getClientId: () => Promise<string>;

const uuidv4Validation = /^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i;
const clientIdFile = path.join(testHomeDir, '.rp', 'rp.properties');
Expand All @@ -14,18 +17,21 @@ const unlink = util.promisify(fs.unlink);
const readFile = util.promisify(fs.readFile);
const writeFile = util.promisify(fs.writeFile);
const removeTestHomeDir = () => fs.promises.rm(testHomeDir, { recursive: true, force: true });
const unlinkFile = async (filePath) => {
const unlinkFile = async (filePath: string) => {
try {
await unlink(filePath);
} catch (error) {
if (error.code !== 'ENOENT') {
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
throw error;
}
}
};

describe('Client ID test suite', () => {
beforeAll(removeTestHomeDir);
beforeAll(async () => {
await removeTestHomeDir();
({ getClientId } = await import('../src/statistics/client-id'));
});
afterAll(removeTestHomeDir);

it('getClientId should return the same client ID for two calls', async () => {
Expand Down
18 changes: 8 additions & 10 deletions __tests__/config.spec.js → __tests__/config.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
const { getClientConfig, getRequiredOption, getApiKey } = require('../src/lib/commons/config');
const {
ReportPortalRequiredOptionError,
ReportPortalValidationError,
} = require('../src/lib/commons/errors');
import { getClientConfig, getRequiredOption, getApiKey } from '../src/commons/config';
import { ReportPortalRequiredOptionError, ReportPortalValidationError } from '../src/commons/errors';
import type { ReportPortalConfig } from '../src/models/config';

describe('Config commons test suite', () => {
describe('getRequiredOption', () => {
Expand All @@ -26,7 +24,7 @@ describe('Config commons test suite', () => {
it('should throw ReportPortalRequiredOptionError in case of option not present in options', () => {
let error;
try {
getRequiredOption({ other: 1 }, 'project');
getRequiredOption({ other: 1 } as unknown as { project: unknown }, 'project');
} catch (e) {
error = e;
}
Expand Down Expand Up @@ -73,7 +71,7 @@ describe('Config commons test suite', () => {
describe('getClientConfig', () => {
it('should print ReportPortalValidationError error to the console in case of options is not an object type', () => {
jest.spyOn(console, 'dir').mockImplementation();
getClientConfig('options');
getClientConfig('options' as unknown as ReportPortalConfig);

expect(console.dir).toHaveBeenCalledWith(
new ReportPortalValidationError('`options` must be an object.'),
Expand All @@ -85,7 +83,7 @@ describe('Config commons test suite', () => {
getClientConfig({
apiKey: '123',
project: 'prj',
});
} as unknown as ReportPortalConfig);

expect(console.dir).toHaveBeenCalledWith(new ReportPortalRequiredOptionError('endpoint'));
});
Expand All @@ -95,7 +93,7 @@ describe('Config commons test suite', () => {
getClientConfig({
apiKey: '123',
endpoint: 'https://abc.com',
});
} as unknown as ReportPortalConfig);

expect(console.dir).toHaveBeenCalledWith(new ReportPortalRequiredOptionError('project'));
});
Expand All @@ -105,7 +103,7 @@ describe('Config commons test suite', () => {
getClientConfig({
project: 'prj',
endpoint: 'https://abc.com',
});
} as unknown as ReportPortalConfig);

expect(console.dir).toHaveBeenCalledWith(new ReportPortalRequiredOptionError('apiKey'));
});
Expand Down
17 changes: 9 additions & 8 deletions __tests__/helpers.spec.js → __tests__/helpers.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
const os = require('os');
const fs = require('fs');
const glob = require('glob');
const helpers = require('../src/lib/helpers');
const pjson = require('../package.json');
import os from 'os';
import fs from 'fs';
import * as glob from 'glob';
import * as helpers from '../src/helpers';
import type { TestItemParameter } from '../src/models/requests';
import pjson from '../package.json';

describe('Helpers', () => {
describe('formatName', () => {
Expand All @@ -22,7 +23,7 @@ describe('Helpers', () => {

describe('now', () => {
it('returns milliseconds from unix time', () => {
expect(new Date() - helpers.now()).toBeLessThan(100); // less than 100 miliseconds difference
expect(Number(new Date()) - helpers.now()).toBeLessThan(100); // less than 100 miliseconds difference
});
});

Expand Down Expand Up @@ -50,7 +51,7 @@ describe('Helpers', () => {
it('should return correct system attributes', () => {
jest.spyOn(os, 'type').mockReturnValue('osType');
jest.spyOn(os, 'arch').mockReturnValue('osArchitecture');
jest.spyOn(os, 'totalmem').mockReturnValue('1');
jest.spyOn(os, 'totalmem').mockReturnValue(1);
const nodeVersion = process.version;
const expectedAttr = [
{
Expand Down Expand Up @@ -106,7 +107,7 @@ describe('Helpers', () => {
key: 'keyThree',
value: 'valueThree',
},
];
] as TestItemParameter[];

const testCaseId = helpers.generateTestCaseId('codeRef', parameters);

Expand Down
Loading