diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 77e552c..b8ed4bc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node: [16.x, 18.x, 20.x, 22.x] + node: [20.x, 22.x] steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 diff --git a/.gitignore b/.gitignore index 38cb1a3..d389a99 100644 --- a/.gitignore +++ b/.gitignore @@ -142,3 +142,4 @@ node_modules *-lock.* .vscode lib/ +.github/instructions/wmt-copilot.instructions.md diff --git a/README.md b/README.md index 6b04b87..c7228a3 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![NPM version][npm-image]][npm-url] [![Build Status][travis-image]][travis-url] [![Dependency Status][daviddm-image]][daviddm-url] -This is an imaginatively named, configurable web server using Fastify atop Node.js. +This is an imaginatively named, configurable web server using Fastify 5.x atop Node.js. The aim is to provide a standardized node web server that can be used to serve your web application without the need for duplicating from another example, or starting from scratch. @@ -11,7 +11,7 @@ The intention is that you will extend via configuration, such that this provides functionality of a Fastify web server, and within your own application you will add on the features, logic, etc unique to your situation. -This module requires Node v16.x.x+. +This module requires Node v20.x.x+ and supports Fastify 5.x. ## Table Of Contents @@ -212,7 +212,6 @@ Configure electrode provided options. - A function to install event listeners for the electrode server startup lifecycle. - The following events are supported: - - `config-composed` - All configurations have been composed into a single one - `server-created` - Fastify server created - `plugins-sorted` - Plugins processed and sorted by priority @@ -450,11 +449,9 @@ The electrode server exports a single API. - `config` is the [electrode server config](#configuration-options) - `decors` - Optional extra `config` or array of `config`. In case you have common config you want to put inside a dedicated module, you can pass them in here. - - If it's an array like `[ decor1, decor2, decor3 ]` then each one is composed into the main config. ie: something similar to `_.merge(mainConfig, decor1, decor2, decor3)`. - `callback` is an optional errback with the signature `function (err, server)` - - where `server` is the Fastify server - **Returns:** a promise resolving to the Fastify server if callback is not provided @@ -496,7 +493,7 @@ identity (no compression) ```js server: { - bodyLimit: 1048576; //new size limit + bodyLimit: 1048576; // new size limit in bytes (1MB) } ``` diff --git a/package.json b/package.json index 3122cff..5cd53ee 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@xarc/fastify-server", - "version": "4.0.10", + "version": "5.0.0", "description": "A configurable Fastify web server", "main": "lib/index.js", "types": "./lib/index.d", @@ -45,19 +45,19 @@ ], "license": "Apache-2.0", "engines": { - "node": ">= 14.0.0" + "node": ">= 22.0.0" }, "files": [ "lib", "src" ], "dependencies": { - "@fastify/static": "^7.0.4", + "@fastify/static": "^8.2.0", "async-eventemitter": "^0.2.4", "chalk": "^4.1.0", "electrode-confippet": "^1.7.0", - "fastify": "^4.29.0", - "fastify-plugin": "^4.5.0", + "fastify": "^5.6.1", + "fastify-plugin": "^5.0.1", "lodash": "^4.17.21", "require-at": "^1.0.6", "tslib": "^2.1.0", @@ -68,9 +68,10 @@ "@istanbuljs/nyc-config-typescript": "^1.0.1", "@types/jest": "^29.5.11", "@types/mocha": "^8.2.0", - "@types/node": "^14.14.16", + "@types/node": "^22.0.0", "@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/parser": "^5.0.0", + "@xarc/run": "^1.1.1", "eslint": "^7.16.0", "eslint-config-walmart": "^2.2.1", "eslint-plugin-filenames": "^1.1.0", @@ -78,7 +79,7 @@ "jest": "^29.7.0", "mitm": "^1.2.0", "npm-run-all": "4.1.5", - "prettier": "3.2.4", + "prettier": "^3.6.0", "run-verify": "^1.2.1", "source-map-support": "^0.5.19", "superagent": "^9.0.2", diff --git a/src/types.ts b/src/types.ts index 988f4ff..ab477e1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -94,10 +94,7 @@ export type { } from "fastify"; /* eslint-enable max-len */ -export type ServerInfo = { - address: string; - port: number; -}; +export type ServerInfo = { address: string; port: number }; export interface ElectrodeFastifyInstance extends FastifyInstance { info: ServerInfo; @@ -204,7 +201,27 @@ export type ElectrodeServerConfig = { * ``` */ plugins?: PluginsConfig; - /** options to be passed to fastify verbatim */ + /** + * options to be passed to fastify verbatim + * + * **Note for Fastify 5 migration:** + * - `requestIdHeader` now defaults to `false` instead of `"request-id"` + * - Router options like `maxParamLength`, `ignoreTrailingSlash`, etc. should be + * moved under `routerOptions` to avoid deprecation warnings + * - Example: + * ```js + * { + * server: { + * requestIdHeader: false, // explicit default in Fastify 5 + * routerOptions: { + * maxParamLength: 1000, + * ignoreTrailingSlash: false, + * caseSensitive: true + * } + * } + * } + * ``` + */ server?: FastifyServerOptions; /** settings specific to Electrode's add-ons for the fastify server */ electrode?: ElectrodeOptions; diff --git a/test/sample/index.js b/test/sample/index.js index 9d3c76e..43f7a12 100644 --- a/test/sample/index.js +++ b/test/sample/index.js @@ -1,5 +1,3 @@ -/* eslint-disable */ - "use strict"; const fastifyPlugin = require("fastify-plugin"); const path = require("path"); @@ -60,4 +58,5 @@ const config = { }); } }; -require("../../")(config); +const { electrodeServer } = require("../../"); +electrodeServer(config); diff --git a/test/spec/check-node-env.spec.ts b/test/spec/check-node-env.spec.ts index 655b566..5ca1815 100644 --- a/test/spec/check-node-env.spec.ts +++ b/test/spec/check-node-env.spec.ts @@ -21,8 +21,18 @@ describe("process-env-abbr", function () { checkNodeEnv(); }); + it("should do nothing for undefined NODE_ENV", function () { + delete process.env.NODE_ENV; + checkNodeEnv(); + }); + + it("should do nothing for null NODE_ENV", function () { + process.env.NODE_ENV = null as any; + checkNodeEnv(); + }); + it("should do nothing for full NODE_ENV", function () { - ["production", "staging", "development"].forEach(x => { + ["production", "staging", "development", "qa", "test"].forEach(x => { process.env.NODE_ENV = x; checkNodeEnv(); }); diff --git a/test/spec/fastify5-config.spec.ts b/test/spec/fastify5-config.spec.ts new file mode 100644 index 0000000..1fc23f1 --- /dev/null +++ b/test/spec/fastify5-config.spec.ts @@ -0,0 +1,327 @@ +// Test to verify Fastify 5 configuration and plugin compatibility +import { electrodeServer } from "../../src/electrode-server"; +import { ElectrodeFastifyInstance } from "../../src/types"; + +describe("Fastify 5 Compatibility", () => { + let server: ElectrodeFastifyInstance | undefined; + + afterEach(async () => { + if (server) { + await server.close(); + server = undefined; + } + }); + + describe("Migration Verification", () => { + it("should use Fastify 5.x", async () => { + server = await electrodeServer({ + deferStart: true, + connection: { + port: 0 // Use random port + }, + plugins: {} + }); + + expect(server.version).toMatch(/^5\./); + expect(server.version).toBe("5.6.1"); + }); + + it("should work with Fastify 5 plugin system", async () => { + const pluginCalled = jest.fn(); + + server = await electrodeServer({ + deferStart: true, + connection: { + port: 0 + }, + plugins: { + testPlugin: { + register: async fastify => { + pluginCalled(); + fastify.get("/test-plugin", async () => { + return { message: "Plugin working with Fastify 5" }; + }); + } + } + } + }); + + await server.start(); + + expect(pluginCalled).toHaveBeenCalled(); + + const response = await server.inject({ + method: "GET", + url: "/test-plugin" + }); + + expect(response.statusCode).toBe(200); + expect(JSON.parse(response.body)).toEqual({ + message: "Plugin working with Fastify 5" + }); + }); + + it("should handle Node.js 20+ features", async () => { + // Verify we're running on Node 20+ + const nodeVersion = process.version; + const majorVersion = parseInt(nodeVersion.split(".")[0].substring(1), 10); + expect(majorVersion).toBeGreaterThanOrEqual(20); + + server = await electrodeServer({ + deferStart: true, + connection: { + port: 0 + }, + plugins: {} + }); + + // Test that the server starts and works properly + await server.start(); + expect(server.info.port).toBeGreaterThan(0); + }); + + it("should maintain compatibility with existing decorator patterns", async () => { + server = await electrodeServer({ + deferStart: true, + connection: { + port: 0 + }, + plugins: {} + }); + + // Test that all the custom decorators still work + expect(server.app).toBeDefined(); + expect(server.app.config).toBeDefined(); + expect(server.info).toBeDefined(); + expect(server.start).toBeDefined(); + expect(typeof server.start).toBe("function"); + }); + }); + + describe("Configuration Compatibility", () => { + it("should handle all common FastifyServerOptions with Fastify 5", async () => { + server = await electrodeServer({ + deferStart: true, + connection: { + port: 0 + }, + server: { + // Test common Fastify server options that might have changed + logger: false, + disableRequestLogging: true, + bodyLimit: 1048576, + connectionTimeout: 0, + keepAliveTimeout: 72000, + forceCloseConnections: false, + requestIdHeader: false, // This changed default in Fastify 5 + requestIdLogLabel: "reqId", + http2: false, + https: false, + maxRequestsPerSocket: 0, + requestTimeout: 0, + pluginTimeout: 10000, + trustProxy: false, + // Router options should be under routerOptions in newer versions + routerOptions: { + maxParamLength: 1000, + ignoreTrailingSlash: false, + ignoreDuplicateSlashes: false, + caseSensitive: true, + allowUnsafeRegex: false + } + } + }); + + expect(server.version).toMatch(/^5\./); + await server.start(); + expect(server.info.port).toBeGreaterThan(0); + }); + + it("should handle logger configuration with Fastify 5", async () => { + server = await electrodeServer({ + deferStart: true, + connection: { + port: 0 + }, + server: { + logger: { + level: "warn", + serializers: { + req: false, + res: false + } + } + } + }); + + expect(server.version).toMatch(/^5\./); + await server.start(); + }); + + it("should work with requestIdHeader set to false (Fastify 5 default)", async () => { + server = await electrodeServer({ + deferStart: true, + connection: { + port: 0 + }, + server: { + requestIdHeader: false // This is now the default in Fastify 5 + } + }); + + await server.start(); + + const response = await server.inject({ + method: "GET", + url: "/non-existent-route" + }); + + // Should not have a request-id header by default + expect(response.headers["request-id"]).toBeUndefined(); + }); + + it("should still work with requestIdHeader enabled", async () => { + server = await electrodeServer({ + deferStart: true, + connection: { + port: 0 + }, + server: { + requestIdHeader: "x-request-id" + }, + plugins: { + testRoute: { + register: async fastify => { + fastify.get("/test-req-id", async request => { + return { requestId: request.id }; + }); + } + } + } + }); + + await server.start(); + + const response = await server.inject({ + method: "GET", + url: "/test-req-id" + }); + + // Check that the request ID was generated and returned + const body = JSON.parse(response.body); + expect(body.requestId).toBeDefined(); + expect(typeof body.requestId).toBe("string"); + }); + + it("should handle new Fastify 5 options if any", async () => { + server = await electrodeServer({ + deferStart: true, + connection: { + port: 0 + }, + server: { + // Any new Fastify 5 specific options would go here + // Currently testing that it doesn't break with standard options + jsonShorthand: true, + ajv: { + customOptions: { + coerceTypes: "array" + } + } + } + }); + + expect(server.version).toMatch(/^5\./); + await server.start(); + }); + }); + + describe("Plugin Registration Compatibility", () => { + it("should handle plugin registration with .after() pattern in Fastify 5", async () => { + let pluginExecuted = false; + let afterExecuted = false; + + server = await electrodeServer({ + deferStart: true, + connection: { + port: 0 + }, + plugins: { + testPlugin: { + register: async fastify => { + pluginExecuted = true; + + // Add a simple route to verify the plugin works + fastify.get("/test-after", async () => { + return { message: "Plugin with .after() works in Fastify 5" }; + }); + + // Test that we can use .after() functionality + fastify.after(() => { + afterExecuted = true; + }); + }, + options: {} + } + } + }); + + expect(pluginExecuted).toBe(true); + + await server.start(); + + // Test the route works + const response = await server.inject({ + method: "GET", + url: "/test-after" + }); + + expect(response.statusCode).toBe(200); + expect(JSON.parse(response.body)).toEqual({ + message: "Plugin with .after() works in Fastify 5" + }); + + // Verify .after() was called + expect(afterExecuted).toBe(true); + }); + + it("should handle multiple plugins with .after() callbacks", async () => { + const pluginOrder: string[] = []; + + server = await electrodeServer({ + deferStart: true, + connection: { + port: 0 + }, + plugins: { + firstPlugin: { + priority: 1, + register: async fastify => { + pluginOrder.push("first-register"); + fastify.after(() => { + pluginOrder.push("first-after"); + }); + } + }, + secondPlugin: { + priority: 2, + register: async fastify => { + pluginOrder.push("second-register"); + fastify.after(() => { + pluginOrder.push("second-after"); + }); + } + } + } + }); + + await server.start(); + + // Verify the execution order + expect(pluginOrder).toContain("first-register"); + expect(pluginOrder).toContain("second-register"); + expect(pluginOrder).toContain("first-after"); + expect(pluginOrder).toContain("second-after"); + }); + }); +});