diff --git a/.changeset/node24-support-drop-node20.md b/.changeset/node24-support-drop-node20.md new file mode 100644 index 00000000..a743e59d --- /dev/null +++ b/.changeset/node24-support-drop-node20.md @@ -0,0 +1,15 @@ +--- +'@twilio-labs/serverless-twilio-runtime': major +'create-twilio-function': major +'@twilio-labs/plugin-serverless': major +'@twilio-labs/plugin-assets': major +'twilio-run': major +--- + +**WHAT**: Add Node.js 24 to supported versions and remove Node.js 20. + +**ALSO**: `twilio-run deploy` now warns when a project is configured to deploy to a deprecated runtime such as `node20`, pointing at the `runtime` field in `.twilioserverlessrc`. It is a warning, not a failure — the deploy still proceeds and Twilio Serverless has the final say. Both this check and the local Node.js version check use a deny list of deprecated versions rather than an allow list of supported ones, so an older release of the toolkit never claims a newer Node.js version is invalid. `printVersionWarning` drops its unused `expectedVersion` parameter as part of this. + +**WHY**: `node24` was added as a deploy runtime and made the default in #555, but `engines.node` was never widened, so the toolkit defaulted to deploying to node24 while refusing to declare support for running on Node 24 (#563). Node.js 20 is deprecated on Twilio Serverless and can no longer be used for new builds, so it is removed at the same time. + +**BREAKING CHANGE**: Projects using `create-twilio-function`, `@twilio-labs/plugin-assets`, `@twilio-labs/plugin-serverless`, `@twilio-labs/serverless-twilio-runtime` or `twilio-run` will have to migrate to Node.js 22 or 24. diff --git a/.github/workflows/on-merge-main.yml b/.github/workflows/on-merge-main.yml index 1d363abb..e64ea07d 100644 --- a/.github/workflows/on-merge-main.yml +++ b/.github/workflows/on-merge-main.yml @@ -21,10 +21,10 @@ jobs: with: fetch-depth: 0 token: ${{ secrets.GITHUB_TOKEN }} - - name: Setup Node.js 20 + - name: Setup Node.js 22 uses: actions/setup-node@3235b876344d2a9aa001b8d1453c930bba69e610 # v3 with: - node-version: 20 + node-version: 22 - name: Update npm to 10.x run: | npm i -g npm@10 diff --git a/.nvmrc b/.nvmrc index 209e3ef4..2bd5a0a9 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20 +22 diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index cfa7ff9d..b0ba9f35 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -14,8 +14,8 @@ If you are planning to contribute something that does not have an open issue yet ## Requirements -Make sure you have Node.js 20 or newer installed. Due to compatibility with Twilio -Functions this project has to support at least Node.js 20.0.0. +Make sure you have Node.js 22 or newer installed. Due to compatibility with Twilio +Functions this project has to support at least Node.js 22.0.0. We are using the npm CLI to manage our project. You'll need at least `npm` version 8 or newer. diff --git a/packages/create-twilio-function/package.json b/packages/create-twilio-function/package.json index 122aa998..96012323 100644 --- a/packages/create-twilio-function/package.json +++ b/packages/create-twilio-function/package.json @@ -42,7 +42,7 @@ "yargs": "^17.2.1" }, "engines": { - "node": "^20.x || ^22.x" + "node": "^22.x || ^24.x" }, "files": [ "bin/", diff --git a/packages/create-twilio-function/src/create-twilio-function/versions.js b/packages/create-twilio-function/src/create-twilio-function/versions.js index 134c8acd..056ebaf7 100644 --- a/packages/create-twilio-function/src/create-twilio-function/versions.js +++ b/packages/create-twilio-function/src/create-twilio-function/versions.js @@ -6,7 +6,7 @@ module.exports = { '@twilio/runtime-handler' ].replace(/[\^~]/, ''), twilioRun: pkgJson.dependencies['twilio-run'], - node: '^20.x || ^22.x || ^24.x', + node: '^22.x || ^24.x', typescript: '^5.3.3', serverlessRuntimeTypes: '^4.0.0', copyfiles: '^2.4.1', diff --git a/packages/plugin-assets/package.json b/packages/plugin-assets/package.json index 92556fd3..0f98c142 100644 --- a/packages/plugin-assets/package.json +++ b/packages/plugin-assets/package.json @@ -23,7 +23,7 @@ "rimraf": "^5.0.5" }, "engines": { - "node": "^20.x || ^22.x" + "node": "^22.x || ^24.x" }, "files": [ "/oclif.manifest.json", diff --git a/packages/plugin-serverless/package.json b/packages/plugin-serverless/package.json index 2da2e78a..030b7116 100644 --- a/packages/plugin-serverless/package.json +++ b/packages/plugin-serverless/package.json @@ -22,7 +22,7 @@ "oclif": "^4.0.0" }, "engines": { - "node": "^20.x || ^22.x" + "node": "^22.x || ^24.x" }, "files": [ "/oclif.manifest.json", diff --git a/packages/serverless-twilio-runtime/Readme.md b/packages/serverless-twilio-runtime/Readme.md index c394493c..74b50695 100644 --- a/packages/serverless-twilio-runtime/Readme.md +++ b/packages/serverless-twilio-runtime/Readme.md @@ -8,7 +8,7 @@ Serverless Framework plugin to deploy to the Twilio Runtime. ### Pre-requisites -- Node.js v20.x (this is the runtime version supported by Twilio Functions) +- Node.js v22.x or v24.x (these are the runtime versions supported by Twilio Functions) - Serverless CLI v1.50.0+. You can run npm i -g serverless if you don't already have it. - A Twilio account. If you don't have one you can [sign up quickly](https://www.twilio.com/try-twilio). diff --git a/packages/twilio-run/__tests__/checks/deprecated-runtime.test.ts b/packages/twilio-run/__tests__/checks/deprecated-runtime.test.ts new file mode 100644 index 00000000..5f5ebcea --- /dev/null +++ b/packages/twilio-run/__tests__/checks/deprecated-runtime.test.ts @@ -0,0 +1,78 @@ +import { mocked } from 'jest-mock'; +import { + checkForDeprecatedRuntime, + DEPRECATED_RUNTIMES, +} from '../../src/checks/deprecated-runtime'; +import { logger } from '../../src/utils/logger'; + +jest.mock('../../src/utils/logger', () => { + return { + logger: { + warn: jest.fn(), + }, + }; +}); + +describe('checkForDeprecatedRuntime', () => { + beforeEach(() => { + mocked(logger.warn).mockClear(); + }); + + test('should warn for every deprecated runtime', () => { + DEPRECATED_RUNTIMES.forEach((runtime) => { + expect(checkForDeprecatedRuntime(runtime)).toEqual(false); + }); + expect(logger.warn).toHaveBeenCalledTimes(DEPRECATED_RUNTIMES.length); + }); + + test('should list every even major up to and including 20', () => { + expect(DEPRECATED_RUNTIMES).toEqual([ + 'node4', + 'node6', + 'node8', + 'node10', + 'node12', + 'node14', + 'node16', + 'node18', + 'node20', + ]); + }); + + test('should name the configured runtime in the warning', () => { + checkForDeprecatedRuntime('node20'); + expect(mocked(logger.warn).mock.calls[0][0]).toContain('node20'); + }); + + test('should not warn for currently supported runtimes', () => { + ['node22', 'node24'].forEach((runtime) => { + expect(checkForDeprecatedRuntime(runtime)).toEqual(true); + }); + expect(logger.warn).not.toHaveBeenCalled(); + }); + + test('should not warn for runtimes newer than this release knows about', () => { + // An older toolkit must not claim a future runtime is invalid. + ['node26', 'node28', 'node30'].forEach((runtime) => { + expect(checkForDeprecatedRuntime(runtime)).toEqual(true); + }); + expect(logger.warn).not.toHaveBeenCalled(); + }); + + test('should not warn when no runtime is configured', () => { + expect(checkForDeprecatedRuntime(undefined)).toEqual(true); + expect(logger.warn).not.toHaveBeenCalled(); + }); + + test('should leave unrecognised values to the platform', () => { + expect(checkForDeprecatedRuntime('nope')).toEqual(true); + expect(logger.warn).not.toHaveBeenCalled(); + }); + + test('should not exit the process', () => { + const exit = jest.spyOn(process, 'exit').mockImplementation(); + checkForDeprecatedRuntime('node20'); + expect(exit).not.toHaveBeenCalled(); + exit.mockRestore(); + }); +}); diff --git a/packages/twilio-run/__tests__/checks/nodejs-version.test.ts b/packages/twilio-run/__tests__/checks/nodejs-version.test.ts new file mode 100644 index 00000000..a5d0ffa6 --- /dev/null +++ b/packages/twilio-run/__tests__/checks/nodejs-version.test.ts @@ -0,0 +1,57 @@ +import { mocked } from 'jest-mock'; +import checkNodejsVersion from '../../src/checks/nodejs-version'; +import { logger } from '../../src/utils/logger'; + +jest.mock('../../src/utils/logger', () => { + return { + logger: { + warn: jest.fn(), + }, + }; +}); + +const originalVersion = process.versions.node; + +function setNodeVersion(version: string) { + Object.defineProperty(process.versions, 'node', { + value: version, + configurable: true, + }); +} + +describe('checkNodejsVersion', () => { + beforeEach(() => { + mocked(logger.warn).mockClear(); + }); + + afterEach(() => { + setNodeVersion(originalVersion); + }); + + test('should warn on deprecated Node.js versions', () => { + ['20.11.1', '18.19.0', '16.20.2'].forEach((version) => { + mocked(logger.warn).mockClear(); + setNodeVersion(version); + checkNodejsVersion(); + expect(logger.warn).toHaveBeenCalledTimes(1); + expect(mocked(logger.warn).mock.calls[0][0]).toContain(version); + }); + }); + + test('should not warn on currently supported Node.js versions', () => { + ['22.11.0', '24.0.0'].forEach((version) => { + setNodeVersion(version); + checkNodejsVersion(); + }); + expect(logger.warn).not.toHaveBeenCalled(); + }); + + test('should not warn on Node.js versions newer than this release knows about', () => { + // An older toolkit must not tell users a newer Node.js version is invalid. + ['26.0.0', '28.1.0'].forEach((version) => { + setNodeVersion(version); + checkNodejsVersion(); + }); + expect(logger.warn).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/twilio-run/package.json b/packages/twilio-run/package.json index e9f400d5..5058131e 100644 --- a/packages/twilio-run/package.json +++ b/packages/twilio-run/package.json @@ -122,7 +122,7 @@ "README.md" ], "engines": { - "node": "^20.x || ^22.x" + "node": "^22.x || ^24.x" }, "gitHead": "6db273648ed19474f4125042556b10c051529912" } diff --git a/packages/twilio-run/src/checks/deprecated-runtime.ts b/packages/twilio-run/src/checks/deprecated-runtime.ts new file mode 100644 index 00000000..e3211356 --- /dev/null +++ b/packages/twilio-run/src/checks/deprecated-runtime.ts @@ -0,0 +1,41 @@ +import { stripIndent } from 'common-tags'; +import { logger } from '../utils/logger'; + +/** + * Node.js major versions that are no longer accepted by Twilio Serverless. + * + * This is deliberately a deny list rather than an allow list of supported + * versions: a project pinned to an older version of the toolkit must not warn + * about a runtime that became valid after it was published. A deprecation is a + * settled fact, so an outdated deny list stays correct — at worst it misses a + * newer deprecation, in which case the platform still rejects the build. + */ +export const DEPRECATED_NODE_MAJORS = [4, 6, 8, 10, 12, 14, 16, 18, 20]; + +export const DEPRECATED_RUNTIMES = DEPRECATED_NODE_MAJORS.map( + (major) => `node${major}` +); + +/** + * Warns if a project is configured to deploy to a runtime that Twilio + * Serverless no longer accepts for new builds, pointing at the field to + * change. Twilio Serverless remains the authority on which runtimes it + * accepts, so this only surfaces the problem earlier and never blocks a + * deploy. Unknown or future runtime values are left alone. + */ +export function checkForDeprecatedRuntime(runtime?: string): boolean { + if (typeof runtime !== 'string' || !DEPRECATED_RUNTIMES.includes(runtime)) { + return true; + } + + const title = 'Deprecated Node.js Runtime Configured'; + const msg = stripIndent` + Your project is configured to deploy to "${runtime}", which is deprecated and is no longer accepted for new builds. + + Please update the "runtime" field in your .twilioserverlessrc (or pass the --runtime flag) to a version that is still supported. + `; + + logger.warn(msg, title); + + return false; +} diff --git a/packages/twilio-run/src/checks/nodejs-version.ts b/packages/twilio-run/src/checks/nodejs-version.ts index b68515ae..c9f943c4 100644 --- a/packages/twilio-run/src/checks/nodejs-version.ts +++ b/packages/twilio-run/src/checks/nodejs-version.ts @@ -1,15 +1,11 @@ import { stripIndent } from 'common-tags'; import { logger } from '../utils/logger'; +import { DEPRECATED_NODE_MAJORS } from './deprecated-runtime'; -const SERVERLESS_NODE_JS_VERSION = ['20.', '22.', '24.']; - -export function printVersionWarning( - nodeVersion: string, - expectedVersion: string[] -): void { - const title = 'Different Node.js Version Found'; +export function printVersionWarning(nodeVersion: string): void { + const title = 'Deprecated Node.js Version Found'; const msg = stripIndent` - You are currently running Node.js ${nodeVersion} on this local machine. The production environment for Twilio Serverless currently supports versions ${expectedVersion}x. + You are currently running Node.js ${nodeVersion} on this local machine. This version is deprecated and is no longer supported by the production environment for Twilio Serverless. When you deploy to Twilio Serverless, you may encounter differences between local development and production. @@ -22,11 +18,12 @@ export function printVersionWarning( export default function checkNodejsVersion() { const nodeVersion = process.versions.node; - if ( - !SERVERLESS_NODE_JS_VERSION.some((nodeJsVersion) => - nodeVersion.startsWith(nodeJsVersion) - ) - ) { - printVersionWarning(nodeVersion, SERVERLESS_NODE_JS_VERSION); + const major = Number.parseInt(nodeVersion.split('.')[0], 10); + + // A deny list of deprecated majors rather than an allow list of supported + // ones, so that an older toolkit release never warns about a Node.js version + // that became supported after it was published. + if (DEPRECATED_NODE_MAJORS.includes(major)) { + printVersionWarning(nodeVersion); } } diff --git a/packages/twilio-run/src/commands/deploy.ts b/packages/twilio-run/src/commands/deploy.ts index 62034037..8e70259c 100644 --- a/packages/twilio-run/src/commands/deploy.ts +++ b/packages/twilio-run/src/commands/deploy.ts @@ -5,6 +5,7 @@ import path from 'path'; import { Argv } from 'yargs'; import { checkConfigForCredentials } from '../checks/check-credentials'; import { checkForValidRuntimeHandlerVersion } from '../checks/check-runtime-handler'; +import { checkForDeprecatedRuntime } from '../checks/deprecated-runtime'; import checkLegacyConfig from '../checks/legacy-config'; import checkProjectStructure from '../checks/project-structure'; import { @@ -111,6 +112,8 @@ export async function handler( return; } + checkForDeprecatedRuntime(config.runtime); + debug('Deploy Config %P', config); checkConfigForCredentials(config);