-
Notifications
You must be signed in to change notification settings - Fork 22
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
Expose JavaScript API for generating OpenAPI schema #160
Changes from 4 commits
7eab795
18fde62
3932d38
1dc03ab
a818e40
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import { | ||
compileEndpoints, | ||
syncOpenApiSpecFromBuild, | ||
clearTmpFolder | ||
} from 'next-rest-framework/generate'; | ||
|
||
async function main() { | ||
try { | ||
await compileEndpoints({ | ||
buildOptions: { | ||
external: ['jsdom'] | ||
} | ||
}); | ||
|
||
console.info('Generating OpenAPI spec...'); | ||
|
||
await syncOpenApiSpecFromBuild({}); | ||
} catch (e) { | ||
console.error(e); | ||
process.exit(1); | ||
} | ||
|
||
await clearTmpFolder(); | ||
} | ||
|
||
main() | ||
.then(() => { | ||
console.log('Completed building OpenAPI schema from custom script'); | ||
}) | ||
.catch(console.error); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import { TypedNextResponse, route, routeOperation } from 'next-rest-framework'; | ||
import { z } from 'zod'; | ||
import { JSDOM } from 'jsdom'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. An example using |
||
|
||
export const runtime = 'edge'; | ||
|
||
export const { GET } = route({ | ||
getDomInfo: routeOperation({ | ||
method: 'GET' | ||
}) | ||
.outputs([ | ||
{ | ||
status: 200, | ||
contentType: 'application/json', | ||
body: z.object({ html: z.string() }) | ||
} | ||
]) | ||
.handler(() => { | ||
const dom = new JSDOM('<!DOCTYPE html><p>Hello world</p>'); | ||
|
||
return TypedNextResponse.json({ | ||
html: dom.serialize() | ||
}); | ||
}) | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './src/generate'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This root |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export { clearTmpFolder, compileEndpoints } from './utils'; | ||
export { validateOpenApiSpecFromBuild } from './validate'; | ||
export { syncOpenApiSpecFromBuild } from './generate'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,7 @@ | ||
import { rm } from 'fs/promises'; | ||
import chalk from 'chalk'; | ||
import { globSync } from 'fast-glob'; | ||
import { build } from 'esbuild'; | ||
import { type BuildOptions, build } from 'esbuild'; | ||
import { type NrfOasData } from '../shared/paths'; | ||
import { type NextRestFrameworkConfig } from '../types'; | ||
import { existsSync, readdirSync } from 'fs'; | ||
|
@@ -24,7 +24,9 @@ export const clearTmpFolder = async () => { | |
}; | ||
|
||
// Compile the Next.js routes and API routes to CJS modules with esbuild. | ||
export const compileEndpoints = async () => { | ||
export const compileEndpoints = async ({ | ||
buildOptions | ||
}: { buildOptions?: BuildOptions } = {}) => { | ||
await clearTmpFolder(); | ||
console.info(chalk.yellowBright('Compiling endpoints...')); | ||
const entryPoints = globSync(['./**/*.ts', '!**/node_modules/**']); | ||
|
@@ -36,10 +38,20 @@ export const compileEndpoints = async () => { | |
format: 'cjs', | ||
platform: 'node', | ||
outdir: NEXT_REST_FRAMEWORK_TEMP_FOLDER_NAME, | ||
outExtension: { '.js': '.cjs' } | ||
outExtension: { '.js': '.cjs' }, | ||
...buildOptions, | ||
external: buildExternalDependencies(buildOptions) | ||
}); | ||
}; | ||
|
||
function buildExternalDependencies(buildOptions: BuildOptions = {}): string[] { | ||
// explicitly add the `esbuild` as an external build dependency as recommended | ||
// by esbuild to avoid bundling it in the output. | ||
// | ||
// ref: https://github.com/evanw/esbuild//issues/2698#issuecomment-1325160925 | ||
return ['esbuild', ...(buildOptions.external ?? [])]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since this is now part of a JS API, we don't want
|
||
} | ||
|
||
// Traverse the base path and find all nested files. | ||
const getNestedFiles = (basePath: string, dir: string): string[] => { | ||
const dirents = readdirSync(join(basePath, dir), { withFileTypes: true }); | ||
|
@@ -105,8 +117,7 @@ export const findConfig = async ({ configPath }: { configPath?: string }) => { | |
route | ||
); | ||
|
||
const url = new URL(`file://${filePathToRoute}`).toString(); | ||
const res = await import(url).then((mod) => mod.default); | ||
const res = await dynamicallyImportRoute(filePathToRoute); | ||
|
||
const handlers: any[] = Object.entries(res) | ||
.filter(([key]) => isValidMethod(key)) | ||
|
@@ -165,9 +176,7 @@ export const findConfig = async ({ configPath }: { configPath?: string }) => { | |
route | ||
); | ||
|
||
const url = new URL(`file://${filePathToRoute}`).toString(); | ||
const res = await import(url).then((mod) => mod.default); | ||
|
||
const res = await dynamicallyImportRoute(filePathToRoute); | ||
const _config = res.default._nextRestFrameworkConfig; | ||
|
||
if (_config) { | ||
|
@@ -227,6 +236,17 @@ export const findConfig = async ({ configPath }: { configPath?: string }) => { | |
return config; | ||
}; | ||
|
||
async function dynamicallyImportRoute(filePathToRoute: string) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was needed to work with |
||
try { | ||
// First try to import the file as a URL and assume it has a default export. | ||
// If that fails, try to import the module directly at its absolute path. | ||
const url = new URL(`file://${filePathToRoute}`).toString(); | ||
return await import(url).then((mod) => mod.default); | ||
} catch (err) {} | ||
|
||
return await import(filePathToRoute).then((mod) => mod); | ||
} | ||
|
||
// Generate the OpenAPI paths from the Next.js routes and API routes from the build output. | ||
export const generatePathsFromBuild = async ({ | ||
config | ||
|
@@ -352,8 +372,7 @@ export const generatePathsFromBuild = async ({ | |
route | ||
); | ||
|
||
const url = new URL(`file://${filePathToRoute}`).toString(); | ||
const res = await import(url).then((mod) => mod.default); | ||
const res = await dynamicallyImportRoute(filePathToRoute); | ||
|
||
const handlers: any[] = Object.entries(res) | ||
.filter(([key]) => isValidMethod(key)) | ||
|
@@ -404,8 +423,7 @@ export const generatePathsFromBuild = async ({ | |
apiRoute | ||
); | ||
|
||
const url = new URL(`file://${filePathToRoute}`).toString(); | ||
const res = await import(url).then((mod) => mod.default); | ||
const res = await dynamicallyImportRoute(filePathToRoute); | ||
|
||
const data = await res.default._getPathsForRoute( | ||
getApiRouteName(apiRoute) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added this + example route below for demonstrating the issue + testing.