From d73468ae930dafb244c146ac580f0913db4b71a3 Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Sat, 4 Nov 2023 03:32:53 +0700 Subject: [PATCH] Require Node.js 18 (#635) --- .github/workflows/test.yml | 6 +-- examples/jest/summary.tsx | 8 ++-- examples/jest/test.tsx | 4 +- .../use-focus-with-id/use-focus-with-id.tsx | 4 +- package.json | 43 +++++++++---------- src/components/App.tsx | 3 +- src/hooks/use-input.ts | 3 +- src/output.ts | 2 +- src/styles.ts | 2 +- test/borders.tsx | 2 +- test/components.tsx | 16 +++---- test/exit.tsx | 6 +-- test/fixtures/use-input.tsx | 2 +- test/focus.tsx | 20 ++++----- test/helpers/run.ts | 8 ++-- test/hooks.tsx | 17 +++----- test/reconciler.tsx | 14 +++--- test/render.tsx | 8 ++-- test/text.tsx | 4 +- tsconfig.json | 5 +-- 20 files changed, 83 insertions(+), 94 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ecb771339..ddc2e1181 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,12 +7,12 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node_version: [14, 16, 18] + node_version: [18, 20] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node_version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node_version }} - run: npm install diff --git a/examples/jest/summary.tsx b/examples/jest/summary.tsx index b254db815..e21654ea9 100644 --- a/examples/jest/summary.tsx +++ b/examples/jest/summary.tsx @@ -2,10 +2,10 @@ import React from 'react'; import {Box, Text} from '../../src/index.js'; type Props = { - isFinished: boolean; - passed: number; - failed: number; - time: string; + readonly isFinished: boolean; + readonly passed: number; + readonly failed: number; + readonly time: string; }; function Summary({isFinished, passed, failed, time}: Props) { diff --git a/examples/jest/test.tsx b/examples/jest/test.tsx index f1effb7f5..7a31905be 100644 --- a/examples/jest/test.tsx +++ b/examples/jest/test.tsx @@ -22,8 +22,8 @@ const getBackgroundForStatus = (status: string): string | undefined => { }; type Props = { - status: string; - path: string; + readonly status: string; + readonly path: string; }; function Test({status, path}: Props) { diff --git a/examples/use-focus-with-id/use-focus-with-id.tsx b/examples/use-focus-with-id/use-focus-with-id.tsx index 98015439f..91e635584 100644 --- a/examples/use-focus-with-id/use-focus-with-id.tsx +++ b/examples/use-focus-with-id/use-focus-with-id.tsx @@ -41,8 +41,8 @@ function Focus() { } type ItemProps = { - id: number; - label: string; + readonly id: number; + readonly label: string; }; function Item({label, id}: ItemProps) { diff --git a/package.json b/package.json index 39f8da78c..f7d58f104 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "default": "./build/index.js" }, "engines": { - "node": ">=14.16" + "node": ">=18" }, "scripts": { "dev": "tsc --watch", @@ -45,33 +45,32 @@ "dependencies": { "@alcalzone/ansi-tokenize": "^0.1.3", "ansi-escapes": "^6.0.0", + "ansi-styles": "^6.2.1", "auto-bind": "^5.0.1", - "chalk": "^5.2.0", + "chalk": "^5.3.0", "cli-boxes": "^3.0.0", "cli-cursor": "^4.0.0", - "cli-truncate": "^3.1.0", + "cli-truncate": "^4.0.0", "code-excerpt": "^4.0.0", "indent-string": "^5.0.0", "is-ci": "^3.0.1", - "is-lower-case": "^2.0.2", - "is-upper-case": "^2.0.2", "lodash": "^4.17.21", "patch-console": "^2.0.0", "react-reconciler": "^0.29.0", "scheduler": "^0.23.0", "signal-exit": "^3.0.7", - "slice-ansi": "^6.0.0", + "slice-ansi": "^7.1.0", "stack-utils": "^2.0.6", - "string-width": "^5.1.2", - "type-fest": "^0.12.0", - "widest-line": "^4.0.1", - "wrap-ansi": "^8.1.0", + "string-width": "^7.0.0", + "type-fest": "^4.6.0", + "widest-line": "^5.0.0", + "wrap-ansi": "^9.0.0", "ws": "^8.12.0", "yoga-wasm-web": "~0.3.3" }, "devDependencies": { - "@faker-js/faker": "^7.6.0", - "@sindresorhus/tsconfig": "3.0.1", + "@faker-js/faker": "^8.2.0", + "@sindresorhus/tsconfig": "^5.0.0", "@types/benchmark": "^2.1.2", "@types/is-ci": "^2.0.0", "@types/lodash": "^4.14.191", @@ -81,29 +80,27 @@ "@types/react-reconciler": "^0.28.2", "@types/scheduler": "^0.16.2", "@types/signal-exit": "^3.0.0", - "@types/sinon": "^9.0.4", - "@types/slice-ansi": "^4.0.0", - "@types/stack-utils": "^1.0.1", - "@types/wrap-ansi": "^3.0.0", + "@types/sinon": "^10.0.20", + "@types/stack-utils": "^2.0.2", "@types/ws": "^8.5.4", "@vdemedes/prettier-config": "^1.0.1", "ava": "^5.1.1", "boxen": "^7.0.1", - "delay": "^5.0.0", + "delay": "^6.0.0", "eslint-config-xo-react": "0.27.0", - "eslint-plugin-react": "7.32.2", + "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "4.6.0", "ms": "^2.1.3", - "node-pty": "0.10.1", + "node-pty": "^1.0.0", "p-queue": "^7.3.4", "prettier": "^2.0.4", "react": "^18.0.0", "react-devtools-core": "^4.19.1", - "sinon": "^12.0.1", - "strip-ansi": "^6.0.0", + "sinon": "^17.0.0", + "strip-ansi": "^7.1.0", "ts-node": "10.9.1", - "typescript": "^4.9.4", - "xo": "0.54.2" + "typescript": "^5.2.2", + "xo": "^0.56.0" }, "peerDependencies": { "@types/react": ">=18.0.0", diff --git a/src/components/App.tsx b/src/components/App.tsx index 419868d81..f65181604 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -261,8 +261,7 @@ export default class App extends PureComponent { focusPrevious = (): void => { this.setState(previousState => { - const lastFocusableId = - previousState.focusables[previousState.focusables.length - 1]?.id; + const lastFocusableId = previousState.focusables.at(-1)?.id; const previousFocusableId = this.findPreviousFocusable(previousState); diff --git a/src/hooks/use-input.ts b/src/hooks/use-input.ts index 870ca0135..2bb05cabf 100644 --- a/src/hooks/use-input.ts +++ b/src/hooks/use-input.ts @@ -1,5 +1,4 @@ import {useEffect} from 'react'; -import {isUpperCase} from 'is-upper-case'; import parseKeypress, {nonAlphanumericKeys} from '../parse-keypress.js'; import reconciler from '../reconciler.js'; import useStdin from './use-stdin.js'; @@ -176,7 +175,7 @@ const useInput = (inputHandler: Handler, options: Options = {}) => { if ( input.length === 1 && typeof input[0] === 'string' && - isUpperCase(input[0]) + input[0].toUpperCase() === input[0] ) { key.shift = true; } diff --git a/src/output.ts b/src/output.ts index d2ad872d9..09e500e8c 100644 --- a/src/output.ts +++ b/src/output.ts @@ -131,7 +131,7 @@ export default class Output { let {x, y} = operation; let lines = text.split('\n'); - const clip = clips[clips.length - 1]; + const clip = clips.at(-1); if (clip) { const clipHorizontally = diff --git a/src/styles.ts b/src/styles.ts index be9cd20b1..eea2e5949 100644 --- a/src/styles.ts +++ b/src/styles.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/prefer-nullish-coalescing */ import {type Boxes, type BoxStyle} from 'cli-boxes'; import {type LiteralUnion} from 'type-fest'; -import {type ForegroundColorName} from 'chalk'; +import {type ForegroundColorName} from 'ansi-styles'; // Note: We import directly from `ansi-styles` to avoid a bug in TypeScript. // eslint-disable-next-line n/file-extension-in-import import Yoga, {type Node as YogaNode} from 'yoga-wasm-web/auto'; diff --git a/test/borders.tsx b/test/borders.tsx index b4cd3639e..663f330c5 100644 --- a/test/borders.tsx +++ b/test/borders.tsx @@ -415,7 +415,7 @@ test('nested boxes - fit-content box with emojis on flex-direction column', t => test('render border after update', t => { const stdout = createStdout(); - function Test({borderColor}: {borderColor?: string}) { + function Test({borderColor}: {readonly borderColor?: string}) { return ( Hello World diff --git a/test/components.tsx b/test/components.tsx index c3e1beecc..06989b1d7 100644 --- a/test/components.tsx +++ b/test/components.tsx @@ -375,7 +375,7 @@ test('static output', t => { test('skip previous output when rendering new static output', t => { const stdout = createStdout(); - function Dynamic({items}: {items: string[]}) { + function Dynamic({items}: {readonly items: string[]}) { return ( {item => {item}} ); @@ -395,7 +395,7 @@ test('skip previous output when rendering new static output', t => { test('render only new items in static output on final render', t => { const stdout = createStdout(); - function Dynamic({items}: {items: string[]}) { + function Dynamic({items}: {readonly items: string[]}) { return ( {item => {item}} ); @@ -426,7 +426,7 @@ test('ensure wrap-ansi doesn’t trim leading whitespace', t => { test('replace child node with text', t => { const stdout = createStdout(); - function Dynamic({replace}: {replace?: boolean}) { + function Dynamic({replace}: {readonly replace?: boolean}) { return {replace ? 'x' : test}; } @@ -476,8 +476,8 @@ test('disable raw mode when all input components are unmounted', t => { renderFirstInput, renderSecondInput }: { - renderFirstInput?: boolean; - renderSecondInput?: boolean; + readonly renderFirstInput?: boolean; + readonly renderSecondInput?: boolean; }) { const {setRawMode} = useStdin(); @@ -597,8 +597,8 @@ test('render different component based on whether stdin is a TTY or not', t => { renderFirstInput, renderSecondInput }: { - renderFirstInput?: boolean; - renderSecondInput?: boolean; + readonly renderFirstInput?: boolean; + readonly renderSecondInput?: boolean; }) { const {isRawModeSupported, setRawMode} = useStdin(); @@ -660,7 +660,7 @@ test('render all frames if CI environment variable equals false', async t => { test('reset prop when it’s removed from the element', t => { const stdout = createStdout(); - function Dynamic({remove}: {remove?: boolean}) { + function Dynamic({remove}: {readonly remove?: boolean}) { return ( { let output = ''; - term.on('data', (data: string) => { + term.onData(data => { if (data === 's') { setTimeout(() => { t.false(isExited); @@ -100,10 +100,10 @@ test.serial('don’t exit while raw mode is active', async t => { let isExited = false; - term.on('exit', (code: any) => { + term.onExit(({exitCode}) => { isExited = true; - if (code === 0) { + if (exitCode === 0) { t.true(output.includes('exited')); t.pass(); resolve(); diff --git a/test/fixtures/use-input.tsx b/test/fixtures/use-input.tsx index 0e9cd14bf..6de9434f2 100644 --- a/test/fixtures/use-input.tsx +++ b/test/fixtures/use-input.tsx @@ -2,7 +2,7 @@ import process from 'node:process'; import React from 'react'; import {render, useInput, useApp} from '../../src/index.js'; -function UserInput({test}: {test: string | undefined}) { +function UserInput({test}: {readonly test: string | undefined}) { const {exit} = useApp(); useInput((input, key) => { diff --git a/test/focus.tsx b/test/focus.tsx index adc185111..f884c8969 100644 --- a/test/focus.tsx +++ b/test/focus.tsx @@ -27,13 +27,13 @@ const emitReadable = (stdin: NodeJS.WriteStream, chunk: string) => { }; type TestProps = { - showFirst?: boolean; - disableSecond?: boolean; - autoFocus?: boolean; - disabled?: boolean; - focusNext?: boolean; - focusPrevious?: boolean; - unmountChildren?: boolean; + readonly showFirst?: boolean; + readonly disableSecond?: boolean; + readonly autoFocus?: boolean; + readonly disabled?: boolean; + readonly focusNext?: boolean; + readonly focusPrevious?: boolean; + readonly unmountChildren?: boolean; }; function Test({ @@ -81,9 +81,9 @@ function Test({ } type ItemProps = { - label: string; - autoFocus: boolean; - disabled?: boolean; + readonly label: string; + readonly autoFocus: boolean; + readonly disabled?: boolean; }; function Item({label, autoFocus, disabled = false}: ItemProps) { diff --git a/test/helpers/run.ts b/test/helpers/run.ts index 4d805922b..3380dc354 100644 --- a/test/helpers/run.ts +++ b/test/helpers/run.ts @@ -42,17 +42,17 @@ export const run: Run = async (fixture, props) => { let output = ''; - term.on('data', (data: string) => { + term.onData(data => { output += data; }); - term.on('exit', (code: number) => { - if (code === 0) { + term.onExit(({exitCode}) => { + if (exitCode === 0) { resolve(output); return; } - reject(new Error(`Process exited with a non-zero code: ${output}`)); + reject(new Error(`Process exited with a non-zero code: ${exitCode}`)); }); }); }; diff --git a/test/hooks.tsx b/test/hooks.tsx index e7f6a3203..d0208ad5f 100644 --- a/test/hooks.tsx +++ b/test/hooks.tsx @@ -1,14 +1,9 @@ import process from 'node:process'; -import {createRequire} from 'node:module'; import url from 'node:url'; -import * as path from 'node:path'; +import path from 'node:path'; import test, {type ExecutionContext} from 'ava'; import stripAnsi from 'strip-ansi'; - -const require = createRequire(import.meta.url); - -// eslint-disable-next-line @typescript-eslint/consistent-type-imports -const {spawn} = require('node-pty') as typeof import('node-pty'); +import {spawn} from 'node-pty'; const __dirname = url.fileURLToPath(new URL('.', import.meta.url)); @@ -56,17 +51,17 @@ const term = (fixture: string, args: string[] = []) => { waitForExit: async () => exitPromise }; - ps.on('data', (data: string) => { + ps.onData(data => { result.output += data; }); - ps.on('exit', (code: number) => { - if (code === 0) { + ps.onExit(({exitCode}) => { + if (exitCode === 0) { resolve(); return; } - reject(new Error(`Process exited with non-zero exit code: ${code}`)); + reject(new Error(`Process exited with non-zero exit code: ${exitCode}`)); }); return result; diff --git a/test/reconciler.tsx b/test/reconciler.tsx index 03675bf3a..d33db56c3 100644 --- a/test/reconciler.tsx +++ b/test/reconciler.tsx @@ -5,7 +5,7 @@ import {Box, Text, render} from '../src/index.js'; import createStdout from './helpers/create-stdout.js'; test('update child', t => { - function Test({update}: {update?: boolean}) { + function Test({update}: {readonly update?: boolean}) { return {update ? 'B' : 'A'}; } @@ -37,7 +37,7 @@ test('update child', t => { }); test('update text node', t => { - function Test({update}: {update?: boolean}) { + function Test({update}: {readonly update?: boolean}) { return ( Hello @@ -74,7 +74,7 @@ test('update text node', t => { }); test('append child', t => { - function Test({append}: {append?: boolean}) { + function Test({append}: {readonly append?: boolean}) { if (append) { return ( @@ -130,7 +130,7 @@ test('append child', t => { }); test('insert child between other children', t => { - function Test({insert}: {insert?: boolean}) { + function Test({insert}: {readonly insert?: boolean}) { if (insert) { return ( @@ -190,7 +190,7 @@ test('insert child between other children', t => { }); test('remove child', t => { - function Test({remove}: {remove?: boolean}) { + function Test({remove}: {readonly remove?: boolean}) { if (remove) { return ( @@ -246,7 +246,7 @@ test('remove child', t => { }); test('reorder children', t => { - function Test({reorder}: {reorder?: boolean}) { + function Test({reorder}: {readonly reorder?: boolean}) { if (reorder) { return ( @@ -306,7 +306,7 @@ test('reorder children', t => { test('replace child node with text', t => { const stdout = createStdout(); - function Dynamic({replace}: {replace?: boolean}) { + function Dynamic({replace}: {readonly replace?: boolean}) { return {replace ? 'x' : test}; } diff --git a/test/render.tsx b/test/render.tsx index 8ce8333bd..9e593d5f0 100644 --- a/test/render.tsx +++ b/test/render.tsx @@ -56,17 +56,17 @@ const term = (fixture: string, args: string[] = []) => { waitForExit: async () => exitPromise }; - ps.on('data', (data: string) => { + ps.onData(data => { result.output += data; }); - ps.on('exit', (code: number) => { - if (code === 0) { + ps.onExit(({exitCode}) => { + if (exitCode === 0) { resolve(); return; } - reject(new Error(`Process exited with non-zero exit code: ${code}`)); + reject(new Error(`Process exited with non-zero exit code: ${exitCode}`)); }); return result; diff --git a/test/text.tsx b/test/text.tsx index fdd8db56b..dd1a61ce9 100644 --- a/test/text.tsx +++ b/test/text.tsx @@ -77,7 +77,7 @@ test('text with inversion', t => { }); test('remeasure text when text is changed', t => { - function Test({add}: {add?: boolean}) { + function Test({add}: {readonly add?: boolean}) { return ( {add ? 'abcx' : 'abc'} @@ -94,7 +94,7 @@ test('remeasure text when text is changed', t => { }); test('remeasure text when text nodes are changed', t => { - function Test({add}: {add?: boolean}) { + function Test({add}: {readonly add?: boolean}) { return ( diff --git a/tsconfig.json b/tsconfig.json index e4877ff9b..b40258148 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,11 +1,10 @@ { "extends": "@sindresorhus/tsconfig", "compilerOptions": { - "moduleResolution": "node16", - "module": "node16", "outDir": "build", "sourceMap": true, - "jsx": "react" + "jsx": "react", + "isolatedModules": true }, "include": ["src"], "ts-node": {