Skip to content

Commit

Permalink
Remove CLI ESBuild step (#161)
Browse files Browse the repository at this point in the history
* Remove CLI ESBuild step

This fix aims to address various ESBuild issues of
users by removing the ESBuild step from the CLI
commands and parsing parsing the OpenAPI spec
directly from TS/JS files without any build step by
using tsx.

The `generate` and `validate` functions are now
also exposed as entry points for the ESBuild output,
allowing those functions to be used for custom CLIs.

* Add routes with external dependency to example app

This allows testing that API routes with third-party dependencies
like JSDom don't cause issues in the OpenAPI generation.

This commit also adds an example on writing custom scripts
for generating/validating the OpenAPI spec.

The deep object comparison as part of the OpenAPI
generation/validation is also replaced with a faster
stringified comparison between the generated and
existing OpenAPI specs.

Co-authored-by: Austin Kelleher <[email protected]>

---------

Co-authored-by: Austin Kelleher <[email protected]>
  • Loading branch information
blomqma and austinkelleher authored Apr 16, 2024
1 parent 19e5e38 commit 509fa3a
Show file tree
Hide file tree
Showing 18 changed files with 476 additions and 160 deletions.
13 changes: 9 additions & 4 deletions apps/example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,23 @@
"dev": "pnpm prebuild && next dev",
"build": "pnpm prebuild && next build",
"start": "next start",
"generate": "pnpm prebuild && next-rest-framework generate",
"validate": "pnpm prebuild && next-rest-framework validate",
"generate": "pnpm prebuild && NODE_OPTIONS='--import=tsx' next-rest-framework generate",
"validate": "pnpm prebuild && NODE_OPTIONS='--import=tsx' next-rest-framework validate",
"custom-generate-openapi": "pnpm prebuild && tsx ./src/scripts/custom-generate-openapi.ts",
"custom-validate-openapi": "pnpm prebuild && tsx ./src/scripts/custom-validate-openapi.ts",
"lint": "tsc && next lint"
},
"dependencies": {
"jsdom": "24.0.0",
"next-rest-framework": "workspace:*",
"tsx": "4.7.2",
"zod-form-data": "2.0.2"
},
"devDependencies": {
"@types/jsdom": "^21.1.6",
"autoprefixer": "10.0.1",
"eslint-config-next": "14.0.4",
"postcss": "8.4.33",
"tailwindcss": "3.3.0",
"eslint-config-next": "14.0.4"
"tailwindcss": "3.3.0"
}
}
51 changes: 51 additions & 0 deletions apps/example/public/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,31 @@
}
}
},
"/api/v1/route-with-external-dep": {
"get": {
"operationId": "routeWithExternalDep",
"responses": {
"200": {
"description": "Response for status 200",
"content": {
"text/html": {
"schema": {
"$ref": "#/components/schemas/RouteWithExternalDep200ResponseBody"
}
}
}
},
"500": {
"description": "An unknown error occurred, trying again might help.",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/UnexpectedError" }
}
}
}
}
}
},
"/api/v1/route-with-params/{slug}": {
"get": {
"operationId": "getParams",
Expand Down Expand Up @@ -526,6 +551,31 @@
}
}
},
"/api/v2/route-with-external-dep": {
"get": {
"operationId": "routeWithExternalDep",
"responses": {
"200": {
"description": "Response for status 200",
"content": {
"text/html": {
"schema": {
"$ref": "#/components/schemas/RouteWithExternalDep200ResponseBody"
}
}
}
},
"500": {
"description": "An unknown error occurred, trying again might help.",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/UnexpectedError" }
}
}
}
}
}
},
"/api/v2/route-with-params/{slug}": {
"get": {
"operationId": "getPathParams",
Expand Down Expand Up @@ -1076,6 +1126,7 @@
"file": { "type": "string", "format": "binary" }
}
},
"RouteWithExternalDep200ResponseBody": { "type": "string" },
"UnexpectedError": {
"type": "object",
"properties": { "message": { "type": "string" } },
Expand Down
23 changes: 23 additions & 0 deletions apps/example/src/app/api/v2/route-with-external-dep/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { TypedNextResponse, route, routeOperation } from 'next-rest-framework';
import { JSDOM } from 'jsdom';
import { z } from 'zod';

export const { GET } = route({
routeWithExternalDep: routeOperation({
method: 'GET'
})
.outputs([
{
contentType: 'text/html',
status: 200,
body: z.string()
}
])
.handler(() => {
const dom = new JSDOM('<!DOCTYPE html><p>Hello world</p>');

return new TypedNextResponse(dom.serialize(), {
headers: { 'Content-Type': 'text/html' }
});
})
});
21 changes: 21 additions & 0 deletions apps/example/src/pages/api/v1/route-with-external-dep/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { apiRoute, apiRouteOperation } from 'next-rest-framework';
import { JSDOM } from 'jsdom';
import { z } from 'zod';

export default apiRoute({
routeWithExternalDep: apiRouteOperation({
method: 'GET'
})
.outputs([
{
contentType: 'text/html',
status: 200,
body: z.string()
}
])
.handler((_req, res) => {
const dom = new JSDOM('<!DOCTYPE html><p>Hello world</p>');
res.setHeader('Content-Type', 'text/html');
res.send(dom.serialize());
})
});
7 changes: 7 additions & 0 deletions apps/example/src/scripts/custom-generate-openapi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { generate } from 'next-rest-framework/dist/cli/generate';

generate({ configPath: '/api/v2' })
.then(() => {
console.log('Completed building OpenAPI schema from custom script.');
})
.catch(console.error);
7 changes: 7 additions & 0 deletions apps/example/src/scripts/custom-validate-openapi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { validate } from 'next-rest-framework/dist/cli/validate';

validate({ configPath: '/api/v2' })
.then(() => {
console.log('Completed validating OpenAPI schema from custom script.');
})
.catch(console.error);
11 changes: 6 additions & 5 deletions docs/docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,10 +207,11 @@ The RPC operation handler function is a strongly-typed function to implement the

## [CLI](#cli)

The Next REST Framework CLI supports generating and validating the `openapi.json` file:
The CLI commands will parse your Next.js APIs and generate/validate the `openapi.json` file.
If using TypeScript, you will need to install `tsx` and use it as the Node.js loader for the CLI commands below: `npm install --save-dev tsx`

- `npx next-rest-framework generate` to generate the `openapi.json` file.
- `npx next-rest-framework validate` to validate that the `openapi.json` file is up-to-date.
- `NODE_OPTIONS='--import=tsx' npx next-rest-framework generate` to generate the `openapi.json` file.
- `NODE_OPTIONS='--import=tsx' npx next-rest-framework validate` to validate that the `openapi.json` file is up-to-date.

The `next-rest-framework validate` command is useful to have as part of the static checks in your CI/CD pipeline. Both commands support the following options:

Expand All @@ -222,7 +223,7 @@ A good practice is to set these in your `package.json` as both commands are need

```json
"scripts": {
"generate": "next-rest-framework generate",
"validate": "next-rest-framework validate",
"generate": "NODE_OPTIONS='--import=tsx' next-rest-framework generate",
"validate": "NODE_OPTIONS='--import=tsx' next-rest-framework validate",
}
```
7 changes: 4 additions & 3 deletions docs/docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ sidebar_position: 2
You also need the following dependencies installed in you Next.js project:

- [Next.js](https://github.com/vercel/next.js) >= v12
- [Zod](https://github.com/colinhacks/zod) >= v3
- [TypeScript](https://www.typescriptlang.org/) >= v3
- Optional, needed if working with forms: [zod-form-data](https://www.npmjs.com/package/zod-form-data) >= v2
- Optional (needed for validating input): [Zod](https://github.com/colinhacks/zod) >= v3
- Optional: [TypeScript](https://www.typescriptlang.org/) >= v3
- Optional (needed when using the CLI commands and using TypeScript): [tsx](https://github.com/privatenumber/tsx) >= v4
- Optional (needed if working with forms): [zod-form-data](https://www.npmjs.com/package/zod-form-data) >= v2

## [Installation](#installation)

Expand Down
7 changes: 4 additions & 3 deletions docs/docs/intro.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,10 @@ Next REST Framework is an open-source, opinionated, lightweight, easy-to-use set
You also need the following dependencies installed in you Next.js project:

- [Next.js](https://github.com/vercel/next.js) >= v12
- [Zod](https://github.com/colinhacks/zod) >= v3
- [TypeScript](https://www.typescriptlang.org/) >= v3
- Optional, needed if working with forms: [zod-form-data](https://www.npmjs.com/package/zod-form-data) >= v2
- Optional (needed for validating input): [Zod](https://github.com/colinhacks/zod) >= v3
- Optional: [TypeScript](https://www.typescriptlang.org/) >= v3
- Optional (needed when using the CLI commands and using TypeScript): [tsx](https://github.com/privatenumber/tsx) >= v4
- Optional (needed if working with forms): [zod-form-data](https://www.npmjs.com/package/zod-form-data) >= v2

### [Installation](#installation)

Expand Down
18 changes: 10 additions & 8 deletions packages/next-rest-framework/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,10 @@ This is a monorepo containing the following packages / projects:
You also need the following dependencies installed in you Next.js project:

- [Next.js](https://github.com/vercel/next.js) >= v12
- [Zod](https://github.com/colinhacks/zod) >= v3
- [TypeScript](https://www.typescriptlang.org/) >= v3
- Optional, needed if working with forms: [zod-form-data](https://www.npmjs.com/package/zod-form-data) >= v2
- Optional (needed for validating input): [Zod](https://github.com/colinhacks/zod) >= v3
- Optional: [TypeScript](https://www.typescriptlang.org/) >= v3
- Optional (needed when using the CLI commands and using TypeScript): [tsx](https://github.com/privatenumber/tsx) >= v4
- Optional (needed if working with forms): [zod-form-data](https://www.npmjs.com/package/zod-form-data) >= v2

## [Installation](#installation)

Expand Down Expand Up @@ -886,10 +887,11 @@ The RPC operation handler function is a strongly-typed function to implement the

## [CLI](#cli)

The Next REST Framework CLI supports generating and validating the `openapi.json` file:
The CLI commands will parse your Next.js APIs and generate/validate the `openapi.json` file.
If using TypeScript, you will need to install [tsx](https://github.com/privatenumber/tsx) and use it as the Node.js loader for the CLI commands below: `npm install --save-dev tsx`

- `npx next-rest-framework generate` to generate the `openapi.json` file.
- `npx next-rest-framework validate` to validate that the `openapi.json` file is up-to-date.
- `NODE_OPTIONS='--import=tsx' npx next-rest-framework generate` to generate the `openapi.json` file.
- `NODE_OPTIONS='--import=tsx' npx next-rest-framework validate` to validate that the `openapi.json` file is up-to-date.

The `next-rest-framework validate` command is useful to have as part of the static checks in your CI/CD pipeline. Both commands support the following options:

Expand All @@ -901,8 +903,8 @@ A good practice is to set these in your `package.json` as both commands are need

```json
"scripts": {
"generate": "next-rest-framework generate",
"validate": "next-rest-framework validate",
"generate": "NODE_OPTIONS='--import=tsx' next-rest-framework generate",
"validate": "NODE_OPTIONS='--import=tsx' next-rest-framework validate",
}
```

Expand Down
3 changes: 1 addition & 2 deletions packages/next-rest-framework/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,6 @@
"dependencies": {
"chalk": "4.1.2",
"commander": "10.0.1",
"esbuild": "0.19.11",
"fast-glob": "3.3.2",
"formidable": "^3.5.1",
"lodash": "4.17.21",
"prettier": "3.0.2",
Expand All @@ -51,6 +49,7 @@
"@types/jest": "29.5.4",
"@types/lodash": "4.14.197",
"@types/qs": "6.9.11",
"esbuild": "0.19.11",
"jest": "29.6.4",
"next": "*",
"node-mocks-http": "1.13.0",
Expand Down
1 change: 0 additions & 1 deletion packages/next-rest-framework/src/cli/constants.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
export const OPEN_API_VERSION = '3.0.1';
export const NEXT_REST_FRAMEWORK_TEMP_FOLDER_NAME = '.next-rest-framework';
9 changes: 2 additions & 7 deletions packages/next-rest-framework/src/cli/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { existsSync, readFileSync, writeFileSync } from 'fs';
import { join } from 'path';
import * as prettier from 'prettier';
import { findConfig, generateOpenApiSpec } from './utils';
import { isEqualWith } from 'lodash';

const writeOpenApiSpec = async ({
path,
Expand Down Expand Up @@ -35,11 +34,7 @@ const writeOpenApiSpec = async ({
};

// Regenerate the OpenAPI spec if it has changed.
export const syncOpenApiSpecFromBuild = async ({
configPath
}: {
configPath?: string;
}) => {
export const generate = async ({ configPath }: { configPath?: string }) => {
const config = await findConfig({ configPath });

if (!config) {
Expand All @@ -53,7 +48,7 @@ export const syncOpenApiSpecFromBuild = async ({
const data = readFileSync(path);
const openApiSpec = JSON.parse(data.toString());

if (!isEqualWith(openApiSpec, spec)) {
if (!(JSON.stringify(openApiSpec) === JSON.stringify(spec))) {
console.info(
chalk.yellowBright(
'OpenAPI spec changed, regenerating `openapi.json`...'
Expand Down
15 changes: 4 additions & 11 deletions packages/next-rest-framework/src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@

import { Command } from 'commander';
import chalk from 'chalk';
import { clearTmpFolder, compileEndpoints } from './utils';
import { validateOpenApiSpecFromBuild } from './validate';
import { syncOpenApiSpecFromBuild } from './generate';
import { generate } from './generate';
import { validate } from './validate';

const program = new Command();

Expand All @@ -19,18 +18,15 @@ program
const configPath: string = options.configPath ?? '';

try {
await compileEndpoints();
console.info(chalk.yellowBright('Generating OpenAPI spec...'));

await syncOpenApiSpecFromBuild({
await generate({
configPath
});
} catch (e) {
console.error(e);
process.exit(1);
}

await clearTmpFolder();
});

program
Expand All @@ -44,10 +40,9 @@ program
const configPath: string = options.configPath ?? '';

try {
await compileEndpoints();
console.info(chalk.yellowBright('Validating OpenAPI spec...'));

const valid = await validateOpenApiSpecFromBuild({
const valid = await validate({
configPath
});

Expand All @@ -58,8 +53,6 @@ program
console.error(e);
process.exit(1);
}

await clearTmpFolder();
});

program.parse(process.argv);
Loading

0 comments on commit 509fa3a

Please sign in to comment.