Skip to content

Commit 9fa00c0

Browse files
authored
feat(deps): Sync loadPeer() (#3465)
This is now possible because we dropped Node 20 in v28 ( #3242 ) and `require(ESM)` is available since 22.12 without flags. So we can do dynamic module import without async. Could benefit in #3463 A semi-breaking change to `Integration::create()` is postponed until next major. ⌛ Mocking peers in tests requires a rework (in progress) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Refactor** * Peer dependency loading is now synchronous, simplifying server startup flow. * Middleware setup for cookies and uploads now runs synchronously during initialization. * `Integration` TypeScript configuration is optional; prefer `new Integration(...)` over the deprecated `Integration.create()`. * **Tests** * Updated integration and peer-helper tests to reflect synchronous initialization/loader behavior. * Adjusted and streamlined mocks for peers used in tests. * **Documentation** * Updated the README “End-to-End Type Safety” example and added a `v28.6.0` changelog entry. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 08d3644 commit 9fa00c0

14 files changed

Lines changed: 70 additions & 49 deletions

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22

33
## Version 28
44

5+
### v28.6.0
6+
7+
- Changes to the `Integration` generator:
8+
- The `typescript` option is no longer required for constructor;
9+
- `Integration.create()` is now deprecated — use `new Integration()` instead;
10+
- This is possible thanks to the `require(ESM)` feature, available on all supported Node.js versions;
11+
512
### v28.5.0
613

714
- Changes to the proprietary `Date` handling schemas:

README.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1190,11 +1190,9 @@ safety between your API and frontend. Make sure you have `typescript` installed.
11901190
and using the async `printFormatted()` method.
11911191

11921192
```ts
1193-
import typescript from "typescript";
11941193
import { Integration } from "express-zod-api";
11951194

11961195
const client = new Integration({
1197-
typescript, // or await Integration.create() to delegate importing
11981196
routing,
11991197
config,
12001198
variant: "client", // <— optional, see also "types" for a DIY solution

compat-test/int.spec.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,9 @@ import { Integration } from "express-zod-api";
22
import { describe, test, expect } from "vitest";
33

44
describe("Integration", () => {
5-
test("should work with minimum supported TypeScript", async () => {
5+
test("should work with minimum supported TypeScript", () => {
66
expect(
7-
(
8-
await Integration.create({ config: { cors: false }, routing: {} })
9-
).print()
7+
new Integration({ config: { cors: false }, routing: {} }).print()
108
).toContain("export class Client");
119
});
1210
});

express-zod-api/src/config-type.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ interface GracefulOptions {
156156
beforeExit?: () => void | Promise<void>;
157157
}
158158

159+
/** @todo consider removing Promise to make createServer sync in next major */
159160
type ServerHook = (params: {
160161
app: IRouter;
161162
/** @desc Returns child logger for the given request (if configured) or the configured logger otherwise */

express-zod-api/src/integration.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ import type { ClientMethod } from "./method";
1515
import type { CommonConfig } from "./config-type";
1616

1717
interface IntegrationParams {
18-
typescript: typeof ts;
18+
/** @default loadPeer("typescript") */
19+
typescript?: typeof ts;
1920
routing: Routing;
2021
config: CommonConfig;
2122
/**
@@ -81,7 +82,7 @@ export class Integration extends IntegrationBase {
8182
}
8283

8384
public constructor({
84-
typescript,
85+
typescript = loadPeer<typeof ts>("typescript"),
8586
routing,
8687
config,
8788
brandHandling,
@@ -181,10 +182,14 @@ export class Integration extends IntegrationBase {
181182
);
182183
}
183184

185+
/**
186+
* @deprecated use `new Integration()` without `typescript` option on its argument
187+
* @todo remove in the next major — no longer needed
188+
* */
184189
public static async create(params: Omit<IntegrationParams, "typescript">) {
185190
return new Integration({
186191
...params,
187-
typescript: await loadPeer<typeof ts>("typescript"),
192+
typescript: loadPeer<typeof ts>("typescript"),
188193
});
189194
}
190195

@@ -233,8 +238,7 @@ export class Integration extends IntegrationBase {
233238
let format = userDefined;
234239
if (!format) {
235240
try {
236-
const prettierFormat = (await loadPeer<typeof Prettier>("prettier"))
237-
.format;
241+
const prettierFormat = loadPeer<typeof Prettier>("prettier").format;
238242
format = (text) => prettierFormat(text, { filepath: "client.ts" });
239243
} catch {}
240244
}
Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
1+
import { createRequire } from "node:module";
12
import { MissingPeerError } from "./errors";
23

3-
export const loadPeer = async <T>(
4+
let require: NodeJS.Require;
5+
6+
export const loadPeer = <T>(
47
moduleName: string,
58
moduleExport: string = "default",
6-
): Promise<T> => {
9+
): T => {
710
try {
8-
return (await import(moduleName))[moduleExport];
9-
} catch {}
10-
throw new MissingPeerError(moduleName);
11+
const mod = (require ??= createRequire(import.meta.url))(moduleName);
12+
if (moduleExport !== "default") return mod[moduleExport] as T;
13+
return mod.default !== undefined ? mod.default : mod;
14+
} catch {
15+
throw new MissingPeerError(moduleName);
16+
}
1117
};

express-zod-api/src/server-helpers.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -82,12 +82,12 @@ export const createUploadFailureHandler =
8282
next();
8383
};
8484

85-
export const createCookieParser = async ({
85+
export const createCookieParser = ({
8686
config,
8787
}: {
8888
config: ServerConfig;
89-
}): Promise<RequestHandler> => {
90-
const parser = await loadPeer<typeof cookieParser>("cookie-parser");
89+
}): RequestHandler => {
90+
const parser = loadPeer<typeof cookieParser>("cookie-parser");
9191
const { secret, ...rest } = {
9292
...(typeof config.cookies === "object" && config.cookies),
9393
};
@@ -98,14 +98,14 @@ export const createUploadLogger = (
9898
logger: ActualLogger,
9999
): Pick<Console, "log"> => ({ log: logger.debug.bind(logger) });
100100

101-
export const createUploadParsers = async ({
101+
export const createUploadParsers = ({
102102
getLogger,
103103
config,
104104
}: {
105105
getLogger: GetLogger;
106106
config: ServerConfig;
107-
}): Promise<RequestHandler[]> => {
108-
const uploader = await loadPeer<typeof fileUpload>("express-fileupload");
107+
}): RequestHandler[] => {
108+
const uploader = loadPeer<typeof fileUpload>("express-fileupload");
109109
const { limitError, beforeUpload, ...options } = {
110110
...(typeof config.upload === "object" && config.upload),
111111
};

express-zod-api/src/server.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,23 +72,21 @@ export const createServer = async (config: ServerConfig, routing: Routing) => {
7272
.use(loggingMiddleware);
7373

7474
if (config.compression) {
75-
const compressor = await loadPeer<typeof compression>("compression");
75+
const compressor = loadPeer<typeof compression>("compression");
7676
app.use(
7777
compressor(
7878
typeof config.compression === "object" ? config.compression : undefined,
7979
),
8080
);
8181
}
82-
if (config.cookies) app.use(await createCookieParser({ config }));
82+
if (config.cookies) app.use(createCookieParser({ config }));
8383
await config.beforeRouting?.({ app, getLogger });
8484

8585
const parsers: Parsers = {
8686
json: [config.jsonParser || express.json()],
8787
raw: [config.rawParser || express.raw(), moveRaw],
8888
form: [config.formParser || express.urlencoded()],
89-
upload: config.upload
90-
? await createUploadParsers({ config, getLogger })
91-
: [],
89+
upload: config.upload ? createUploadParsers({ config, getLogger }) : [],
9290
};
9391
initRouting({ app, routing, getLogger, config, parsers });
9492

express-zod-api/tests/express-mock.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,6 @@
11
const expressJsonMock = vi.fn();
22
const expressRawMock = vi.fn();
33
const expressUrlencodedMock = vi.fn();
4-
const compressionMock = vi.fn();
5-
const fileUploadMock = vi.fn();
6-
const cookieParserMock = vi.fn();
7-
8-
vi.mock("compression", () => ({ default: compressionMock }));
9-
vi.mock("express-fileupload", () => ({ default: fileUploadMock }));
10-
vi.mock("cookie-parser", () => ({ default: cookieParserMock }));
114

125
const staticHandler = vi.fn();
136
const staticMock = vi.fn(() => staticHandler);
@@ -35,9 +28,6 @@ expressMock.static = staticMock;
3528
vi.mock("express", () => ({ default: expressMock }));
3629

3730
export {
38-
compressionMock,
39-
fileUploadMock,
40-
cookieParserMock,
4131
expressMock,
4232
appMock,
4333
expressJsonMock,

express-zod-api/tests/integration.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ describe("Integration", () => {
5151
);
5252

5353
test("Should treat optionals the same way as z.infer() by default", async () => {
54-
const client = await Integration.create({
54+
const client = new Integration({
5555
config: configMock,
5656
routing: {
5757
v1: {
@@ -74,7 +74,7 @@ describe("Integration", () => {
7474
test.each([undefined, false])(
7575
"Should support HEAD method by default %#",
7676
async (hasHeadMethod) => {
77-
const client = await Integration.create({
77+
const client = new Integration({
7878
config: configMock,
7979
hasHeadMethod,
8080
variant: "types",
@@ -111,7 +111,7 @@ describe("Integration", () => {
111111
handler: vi.fn(),
112112
}),
113113
);
114-
const client = await Integration.create({
114+
const client = new Integration({
115115
config: configMock,
116116
variant: "types",
117117
routing: {

0 commit comments

Comments
 (0)