Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: drop support for node 18 #1007

Merged
merged 2 commits into from
Mar 6, 2025
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
217 changes: 87 additions & 130 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"url": "https://github.com/readmeio/api.git"
},
"engines": {
"node": ">=16"
"node": ">=20.10.0"
},
"workspaces": [
"./packages/*"
Expand Down
9 changes: 4 additions & 5 deletions packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"author": "Jon Ursenbach <[email protected]>",
"license": "MIT",
"engines": {
"node": "^18.20.0 || >=20.10.0"
"node": ">=20.10.0"
},
"files": [
"dist",
Expand All @@ -48,7 +48,6 @@
],
"dependencies": {
"@readme/api-core": "file:../core",
"@readme/openapi-parser": "^2.4.0",
"chalk": "^5.3.0",
"ci-info": "^4.0.0",
"commander": "^13.0.0",
Expand All @@ -59,7 +58,8 @@
"js-yaml": "^4.1.0",
"license": "^1.0.3",
"lodash-es": "^4.17.21",
"oas": "^25.0.1",
"oas": "^26.0.1",
"oas-normalize": "^13.1.0",
"ora": "^8.0.1",
"preferred-pm": "^4.0.0",
"prompts": "^2.4.2",
Expand All @@ -72,7 +72,7 @@
},
"devDependencies": {
"@api/test-utils": "file:../test-utils",
"@readme/oas-examples": "^5.12.1",
"@readme/oas-examples": "^5.19.1",
"@types/js-yaml": "^4.0.9",
"@types/lodash-es": "^4.17.12",
"@types/prompts": "^2.4.9",
Expand All @@ -85,7 +85,6 @@
"ajv": "^8.12.0",
"ajv-formats": "^3.0.1",
"nock": "^14.0.1",
"oas-normalize": "^12.0.0",
"openapi-types": "^12.1.3",
"tsup": "^8.4.0",
"tsx": "^4.19.1",
Expand Down
5 changes: 4 additions & 1 deletion packages/api/src/codegen/languages/typescript/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1151,7 +1151,10 @@ Generated at ${createdAt}

return s;
},
});
/**
* @todo can remove this casting after https://github.com/readmeio/oas/pull/956 is published
*/
}) as SchemaObject[];

if (!schema) {
return false;
Expand Down
32 changes: 19 additions & 13 deletions packages/api/src/fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import type { OASDocument } from 'oas/types';
import fs from 'node:fs';
import path from 'node:path';

import OpenAPIParser from '@readme/openapi-parser';
import yaml from 'js-yaml';
import OASNormalize from 'oas-normalize';

export default class Fetcher {
uri: OASDocument | string;
Expand Down Expand Up @@ -124,27 +124,33 @@ export default class Fetcher {
});
}

static validate(json: OASDocument) {
static async validate(json: OASDocument) {
if (json.swagger) {
throw new Error('Sorry, this module only supports OpenAPI definitions.');
}

// The `validate` method handles dereferencing for us.
return OpenAPIParser.validate(json, {
dereference: {
/**
* If circular `$refs` are ignored they'll remain in the API definition as `$ref: String`.
* This allows us to not only do easy circular reference detection but also stringify and
* save dereferenced API definitions back into the cache directory.
*/
circular: 'ignore',
const normalize = new OASNormalize(json, {
parser: {
dereference: {
/**
* If circular `$refs` are ignored they'll remain in the API definition as `$ref: String`.
* This allows us to not only do easy circular reference detection but also stringify and
* save dereferenced API definitions back into the cache directory.
*/
circular: 'ignore',
},
},
}).catch(err => {
if (/is not a valid openapi definition/i.test(err.message)) {
});

await normalize.validate().catch(err => {
// Zhuzh up this error message a bit so our errors here are consistenly prefixed with "Sorry".
if (err.message === 'The supplied API definition is unsupported.') {
throw new Error("Sorry, that doesn't look like a valid OpenAPI definition.");
}

throw err;
});

return normalize.dereference();
}
}
8 changes: 4 additions & 4 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,22 +46,22 @@
"author": "Jon Ursenbach <[email protected]>",
"license": "MIT",
"engines": {
"node": ">=18"
"node": ">=20.10.0"
},
"dependencies": {
"@readme/oas-to-har": "^24.0.0",
"@readme/oas-to-har": "^25.0.1",
"caseless": "^0.12.0",
"datauri": "^4.1.0",
"fetch-har": "^11.0.1",
"json-schema-to-ts": "^3.0.0",
"json-schema-traverse": "^1.0.0",
"lodash.merge": "^4.6.2",
"oas": "^25.0.1",
"oas": "^26.0.1",
"remove-undefined-objects": "^6.0.0"
},
"devDependencies": {
"@api/test-utils": "file:../test-utils",
"@readme/oas-examples": "^5.12.0",
"@readme/oas-examples": "^5.19.1",
"@types/caseless": "^0.12.5",
"@types/lodash.merge": "^4.6.9",
"@vitest/coverage-v8": "^3.0.5",
Expand Down
9 changes: 4 additions & 5 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,10 @@ export default class APICore {
init.signal = controller.signal;
}

return fetchHar(har as Har, {
files: data.files || {},
init,
userAgent: this.userAgent,
})
// `getHarForRequest` returns a partial HAR object, by way of `@readme/oas-to-har` but
// `fetch-har` is typed to expect the full thing. Though we're supplying an incomplete HAR
// object, at least the spec, our partial is fine.
return fetchHar(har as unknown as Har, { files: data.files || {}, init, userAgent: this.userAgent })
.then(async (res: Response) => {
const parsed = await parseResponse<HTTPStatus>(res);

Expand Down
41 changes: 16 additions & 25 deletions packages/core/src/lib/prepareParams.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { ReadStream } from 'node:fs';
import type { Operation } from 'oas/operation';
import type { ParameterObject, SchemaObject } from 'oas/types';
import type { DataForHAR, ParameterObject, SchemaObject } from 'oas/types';

import stream from 'node:stream';

Expand All @@ -13,6 +13,8 @@ import removeUndefinedObjects from 'remove-undefined-objects';

import getJSONSchemaDefaults from './getJSONSchemaDefaults.js';

type DataForHARWithFiles = DataForHAR & { files?: Record<string, Buffer> };

// These headers are normally only defined by the OpenAPI definition but we allow the user to
// manually supply them in their `metadata` parameter if they wish.
const specialHeaders = ['accept', 'authorization'];
Expand Down Expand Up @@ -56,15 +58,15 @@ function isPrimitive(obj: unknown) {
return obj === null || typeof obj === 'number' || typeof obj === 'string';
}

function merge(src: unknown, target: unknown) {
function merge<R = unknown>(src: R, target: R): R {
if (Array.isArray(target)) {
// @todo we need to add support for merging array defaults with array body/metadata arguments
return target;
return target as R;
} else if (!isObject(target)) {
return target;
return target as R;
}

return lodashMerge(src, target);
return lodashMerge<R, R>(src, target);
}

/**
Expand Down Expand Up @@ -144,7 +146,11 @@ async function processFile(
* with `@readme/oas-to-har`.
*
*/
export default async function prepareParams(operation: Operation, body?: unknown, metadata?: Record<string, unknown>) {
export default async function prepareParams(
operation: Operation,
body?: unknown,
metadata?: Record<string, unknown>,
): Promise<DataForHARWithFiles> {
let metadataIntersected = false;
const digestedParameters = digestParameters(operation.getParameters());
const jsonSchema = operation.getParametersAsJSONSchema();
Expand Down Expand Up @@ -189,22 +195,7 @@ export default async function prepareParams(operation: Operation, body?: unknown
}

const jsonSchemaDefaults = jsonSchema ? getJSONSchemaDefaults(jsonSchema) : {};

const params: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
body?: any;
cookie?: Record<string, boolean | number | string>;
files?: Record<string, Buffer>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
formData?: any;
header?: Record<string, boolean | number | string>;
path?: Record<string, boolean | number | string>;
query?: Record<string, boolean | number | string>;
server?: {
selected: number;
variables: Record<string, number | string>;
};
} = jsonSchemaDefaults;
const params: DataForHARWithFiles = jsonSchemaDefaults;

// If a body argument was supplied we need to do a bit of work to see if it's actually a body
// argument or metadata because the library lets you supply either a body, metadata, or body with
Expand Down Expand Up @@ -321,7 +312,7 @@ export default async function prepareParams(operation: Operation, body?: unknown
// Form data should be placed within `formData` instead of `body` for it to properly get picked
// up by `fetch-har`.
if (operation.isFormUrlEncoded()) {
params.formData = merge(params.formData, params.body);
params.formData = merge<DataForHARWithFiles['formData']>(params.formData, params.body);
delete params.body;
}

Expand Down Expand Up @@ -380,7 +371,7 @@ export default async function prepareParams(operation: Operation, body?: unknown
// out anything that they sent that is a parameter from also being sent as part of a form
// data payload for `x-www-form-urlencoded` requests.
if (metadataIntersected && operation.isFormUrlEncoded()) {
if (paramName in params.formData) {
if (params.formData && paramName in params.formData) {
delete params.formData[paramName];
}
}
Expand Down Expand Up @@ -412,7 +403,7 @@ export default async function prepareParams(operation: Operation, body?: unknown
}

if (operation.isFormUrlEncoded()) {
params.formData = merge(params.formData, metadata);
params.formData = merge<DataForHARWithFiles['formData']>(params.formData, metadata);
} else {
// Any other remaining unused metadata will be unused because we don't know where to place
// it in the request.
Expand Down
8 changes: 4 additions & 4 deletions packages/httpsnippet-client-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,23 +33,23 @@
"author": "Jon Ursenbach <[email protected]>",
"license": "MIT",
"engines": {
"node": ">=18"
"node": ">=20.10.0"
},
"dependencies": {
"content-type": "^1.0.5",
"reserved2": "^0.1.5"
},
"peerDependencies": {
"@readme/httpsnippet": "^11.0.0",
"oas": "^25.0.1"
"oas": "^26.0.1"
},
"devDependencies": {
"@readme/oas-examples": "^5.12.0",
"@readme/openapi-parser": "^2.5.0",
"@readme/oas-examples": "^5.19.1",
"@types/content-type": "^1.1.8",
"@types/stringify-object": "^4.0.5",
"@vitest/coverage-v8": "^3.0.5",
"camelcase": "^8.0.0",
"jest-expect-openapi": "^2.0.1",
"stringify-object": "^5.0.0",
"typescript": "^5.8.2",
"vitest": "^3.0.4"
Expand Down
16 changes: 7 additions & 9 deletions packages/httpsnippet-client-api/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ import fs from 'node:fs/promises';
import path from 'node:path';

import { HTTPSnippet, addClientPlugin } from '@readme/httpsnippet';
import readme from '@readme/oas-examples/3.0/json/readme.json';
import openapiParser from '@readme/openapi-parser';
import readme from '@readme/oas-examples/3.0/json/readme.json' with { type: 'json' };
import toBeAValidOpenAPIDefinition from 'jest-expect-openapi';
import { describe, beforeEach, expect, it } from 'vitest';

import plugin from '../src/index.js';

expect.extend({ toBeAValidOpenAPIDefinition });

const DATASETS_DIR = path.join(__dirname, '__datasets__');
const SNIPPETS = readdirSync(DATASETS_DIR);

Expand Down Expand Up @@ -109,18 +111,14 @@ describe('httpsnippet-client-api', () => {

describe('snippets', () => {
describe.each(SNIPPETS)('%s', snippet => {
let mock: SnippetMock;

beforeEach(async () => {
mock = await getSnippetDataset(snippet);
it('should generate the expected snippet', async () => {
const mock = await getSnippetDataset(snippet);

// `OpenAPIParser.validate()` updates the spec that's passed and we just want to validate
// it here so we need to clone the object.
const spec = JSON.parse(JSON.stringify(mock.definition));
await openapiParser.validate(spec);
});
await expect(spec).toBeAValidOpenAPIDefinition();

it('should generate the expected snippet', async () => {
const expected = await fs.readFile(path.join(DATASETS_DIR, snippet, 'output.js'), 'utf-8');

const code = new HTTPSnippet(mock.har).convert('node', 'api', {
Expand Down
3 changes: 2 additions & 1 deletion packages/test-utils/load-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
* Because parts of our OpenAPI dereferencer in `oas` can update variable references passed to it
* we may need to sometimes fully clone a spec to test something. This is just a small DIY wrapper
* for doing so.
*
*/
export function loadSpec(spec: string) {
export async function loadSpec(spec: string) {
return import(spec).then(({ default: data }) => JSON.stringify(data)).then(JSON.parse);
}
2 changes: 1 addition & 1 deletion packages/test-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
},
"devDependencies": {
"@types/caseless": "^0.12.3",
"oas": "^25.0.2",
"oas": "^26.0.1",
"typescript": "^5.8.2"
},
"prettier": "@readme/eslint-config/prettier"
Expand Down