From 7255060ddd579203281a23b220cb074f65541198 Mon Sep 17 00:00:00 2001 From: hrmny Date: Fri, 9 Jun 2023 22:31:35 +0200 Subject: [PATCH] type check tests (and convert next-test-utils to ts) --- package.json | 4 + packages/next/src/lib/load-custom-routes.ts | 4 +- .../next/src/lib/patch-incorrect-lockfile.ts | 2 +- pnpm-lock.yaml | 6 + test/e2e/app-dir/actions/app-action.test.ts | 2 +- .../app-middleware/app-middleware.test.ts | 1 + test/e2e/app-dir/app-routes/helpers.ts | 7 +- test/e2e/app-dir/app/standalone.test.ts | 2 +- .../app-dir/set-cookies/set-cookies.test.ts | 3 +- .../edge-can-read-request-body/index.test.ts | 1 + .../test/index.test.ts | 1 + test/e2e/next-font/with-proxy.test.ts | 10 +- test/e2e/prerender.test.ts | 4 +- .../config-output-export/test/index.test.ts | 1 + .../integration/draft-mode/test/index.test.ts | 11 +- test/integration/draft-mode/tsconfig.json | 10 +- .../test/index.test.ts | 2 +- .../test/index.test.ts | 2 +- .../typescript/test/index.test.ts | 2 +- .../typescript/test/index.test.ts | 2 +- test/jest.d.ts | 28 +- test/lib/next-modes/next-dev.ts | 2 +- test/lib/next-modes/next-start.ts | 4 +- ...{next-test-utils.js => next-test-utils.ts} | 379 +++++++++++------- .../standalone-mode/type-module/index.test.ts | 2 +- tsconfig.json | 2 +- turbo.json | 1 + 27 files changed, 321 insertions(+), 174 deletions(-) rename test/lib/{next-test-utils.js => next-test-utils.ts} (77%) diff --git a/package.json b/package.json index 6886e40de9bc1a..39d7f3e79b726b 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "genstats": "cross-env LOCAL_STATS=true node .github/actions/next-stats-action/src/index.js", "git-reset": "git reset --hard HEAD", "git-clean": "git clean -d -x -e node_modules -e packages -f", + "typescript": "tsc --noEmit", "lint-typescript": "turbo run typescript", "lint-eslint": "eslint . --ext js,jsx,ts,tsx --max-warnings=0 --config .eslintrc.json --no-eslintrc", "lint-no-typescript": "run-p prettier-check lint-eslint lint-language", @@ -84,7 +85,10 @@ "@swc/helpers": "0.5.1", "@testing-library/react": "13.0.0", "@types/cheerio": "0.22.16", + "@types/cookie": "0.3.3", + "@types/cross-spawn": "6.0.0", "@types/fs-extra": "8.1.0", + "@types/glob": "7.1.1", "@types/html-validator": "5.0.3", "@types/http-proxy": "1.17.3", "@types/jest": "24.0.13", diff --git a/packages/next/src/lib/load-custom-routes.ts b/packages/next/src/lib/load-custom-routes.ts index e413769c902bda..8ccd1f9a41fca2 100644 --- a/packages/next/src/lib/load-custom-routes.ts +++ b/packages/next/src/lib/load-custom-routes.ts @@ -77,11 +77,11 @@ function checkRedirect(route: Redirect): { const invalidParts: string[] = [] let hadInvalidStatus: boolean = false - if (route.statusCode && !allowedStatusCodes.has(route.statusCode)) { + if (route.statusCode && !allowedStatusCodes.has(route['statusCode'])) { hadInvalidStatus = true invalidParts.push(`\`statusCode\` is not undefined or valid statusCode`) } - if (typeof route.permanent !== 'boolean' && !route.statusCode) { + if (typeof route.permanent !== 'boolean' && !route['statusCode']) { invalidParts.push(`\`permanent\` is not set to \`true\` or \`false\``) } diff --git a/packages/next/src/lib/patch-incorrect-lockfile.ts b/packages/next/src/lib/patch-incorrect-lockfile.ts index eec0ae94fb6e2b..78fb73b98a7b7e 100644 --- a/packages/next/src/lib/patch-incorrect-lockfile.ts +++ b/packages/next/src/lib/patch-incorrect-lockfile.ts @@ -57,7 +57,7 @@ export async function patchIncorrectLockfile(dir: string) { const lockfileParsed = JSON.parse(content) const lockfileVersion = parseInt(lockfileParsed?.lockfileVersion, 10) - const expectedSwcPkgs = Object.keys(nextPkgJson.optionalDependencies || {}) + const expectedSwcPkgs = Object.keys(nextPkgJson['optionalDependencies'] || {}) const patchDependency = ( pkg: string, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1d940a5292be5a..57d3030695a770 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -43,7 +43,10 @@ importers: '@swc/helpers': 0.5.1 '@testing-library/react': 13.0.0 '@types/cheerio': 0.22.16 + '@types/cookie': 0.3.3 + '@types/cross-spawn': 6.0.0 '@types/fs-extra': 8.1.0 + '@types/glob': 7.1.1 '@types/html-validator': 5.0.3 '@types/http-proxy': 1.17.3 '@types/jest': 24.0.13 @@ -210,7 +213,10 @@ importers: '@swc/helpers': 0.5.1 '@testing-library/react': 13.0.0_biqbaboplfbrettd7655fr4n2y '@types/cheerio': 0.22.16 + '@types/cookie': 0.3.3 + '@types/cross-spawn': 6.0.0 '@types/fs-extra': 8.1.0 + '@types/glob': 7.1.1 '@types/html-validator': 5.0.3 '@types/http-proxy': 1.17.3 '@types/jest': 24.0.13 diff --git a/test/e2e/app-dir/actions/app-action.test.ts b/test/e2e/app-dir/actions/app-action.test.ts index 6c9d9532a5cd1b..c8ff73759b2699 100644 --- a/test/e2e/app-dir/actions/app-action.test.ts +++ b/test/e2e/app-dir/actions/app-action.test.ts @@ -98,7 +98,7 @@ createNextDescribe( it('should support setting cookies in route handlers with the correct overrides', async () => { const res = await next.fetch('/handler') - const setCookieHeader = res.headers.get('set-cookie') as string[] + const setCookieHeader = res.headers.get('set-cookie') expect(setCookieHeader).toContain('bar=bar2; Path=/') expect(setCookieHeader).toContain('baz=baz2; Path=/') expect(setCookieHeader).toContain('foo=foo1; Path=/') diff --git a/test/e2e/app-dir/app-middleware/app-middleware.test.ts b/test/e2e/app-dir/app-middleware/app-middleware.test.ts index 2756e7e0134e62..711de9b134e07b 100644 --- a/test/e2e/app-dir/app-middleware/app-middleware.test.ts +++ b/test/e2e/app-dir/app-middleware/app-middleware.test.ts @@ -3,6 +3,7 @@ import path from 'path' import cheerio from 'cheerio' import { check, withQuery } from 'next-test-utils' import { createNextDescribe, FileRef } from 'e2e-utils' +import type { Response } from 'node-fetch' createNextDescribe( 'app-dir with middleware', diff --git a/test/e2e/app-dir/app-routes/helpers.ts b/test/e2e/app-dir/app-routes/helpers.ts index c87f0b3e3d07f1..77df81d91164d3 100644 --- a/test/e2e/app-dir/app-routes/helpers.ts +++ b/test/e2e/app-dir/app-routes/helpers.ts @@ -58,7 +58,12 @@ type Cookies = { * @returns any injected metadata on the request */ export function getRequestMeta( - headersOrCookies: Headers | Cookies | ReadonlyHeaders | ReadonlyRequestCookies + headersOrCookies: + | Headers + | import('node-fetch').Headers + | Cookies + | ReadonlyHeaders + | ReadonlyRequestCookies ): Record { const headerOrCookie = headersOrCookies.get(KEY) if (!headerOrCookie) return {} diff --git a/test/e2e/app-dir/app/standalone.test.ts b/test/e2e/app-dir/app/standalone.test.ts index 8cbeee62b6e5a0..8629ff4440ef48 100644 --- a/test/e2e/app-dir/app/standalone.test.ts +++ b/test/e2e/app-dir/app/standalone.test.ts @@ -70,7 +70,7 @@ if (!(globalThis as any).isNextStart) { /Listening on/, { ...process.env, - PORT: appPort, + PORT: appPort.toString(), }, undefined, { diff --git a/test/e2e/app-dir/set-cookies/set-cookies.test.ts b/test/e2e/app-dir/set-cookies/set-cookies.test.ts index 605ffc60f9c2f5..3938c13ddc4682 100644 --- a/test/e2e/app-dir/set-cookies/set-cookies.test.ts +++ b/test/e2e/app-dir/set-cookies/set-cookies.test.ts @@ -1,8 +1,9 @@ import { createNextDescribe } from 'e2e-utils' +import type { Response } from 'node-fetch' import cookies, { nextConfigHeaders } from './cookies.mjs' -function getSetCookieHeaders(res: globalThis.Response): ReadonlyArray { +function getSetCookieHeaders(res: Response): ReadonlyArray { return ( (res.headers as any).getSetCookie?.() ?? (res.headers as any).raw()['set-cookie'] diff --git a/test/e2e/edge-can-read-request-body/index.test.ts b/test/e2e/edge-can-read-request-body/index.test.ts index f57a02cf8c5141..a6d30d3940747d 100644 --- a/test/e2e/edge-can-read-request-body/index.test.ts +++ b/test/e2e/edge-can-read-request-body/index.test.ts @@ -3,6 +3,7 @@ import { NextInstance } from 'test/lib/next-modes/base' import { fetchViaHTTP, renderViaHTTP } from 'next-test-utils' import FormData from 'form-data' import path from 'path' +import type { Response } from 'node-fetch' async function serialize(response: Response) { return { diff --git a/test/e2e/middleware-request-header-overrides/test/index.test.ts b/test/e2e/middleware-request-header-overrides/test/index.test.ts index 03f7296b5b1762..902d91fedfc817 100644 --- a/test/e2e/middleware-request-header-overrides/test/index.test.ts +++ b/test/e2e/middleware-request-header-overrides/test/index.test.ts @@ -5,6 +5,7 @@ import { NextInstance } from 'test/lib/next-modes/base' import { fetchViaHTTP } from 'next-test-utils' import { createNext, FileRef } from 'e2e-utils' import cheerio from 'cheerio' +import type { Response } from 'node-fetch' describe('Middleware Request Headers Overrides', () => { let next: NextInstance diff --git a/test/e2e/next-font/with-proxy.test.ts b/test/e2e/next-font/with-proxy.test.ts index c2d8601b451f74..83cd1fc46461e0 100644 --- a/test/e2e/next-font/with-proxy.test.ts +++ b/test/e2e/next-font/with-proxy.test.ts @@ -1,13 +1,13 @@ import { FileRef, createNext, NextInstance } from 'e2e-utils' import { findPort, renderViaHTTP, fetchViaHTTP } from 'next-test-utils' import { join } from 'path' -import { spawn } from 'cross-spawn' +import spawn from 'cross-spawn' describe('next/font/google with proxy', () => { let next: NextInstance let proxy: any - let PROXY_PORT: number - let SERVER_PORT: number + let PROXY_PORT: string + let SERVER_PORT: string if ((global as any).isNextDeploy) { it('should skip next deploy', () => {}) @@ -15,8 +15,8 @@ describe('next/font/google with proxy', () => { } beforeAll(async () => { - PROXY_PORT = await findPort() - SERVER_PORT = await findPort() + PROXY_PORT = await findPort().toString() + SERVER_PORT = await findPort().toString() proxy = spawn('node', [require.resolve('./with-proxy/server.js')], { stdio: 'inherit', diff --git a/test/e2e/prerender.test.ts b/test/e2e/prerender.test.ts index 41b3db12d87a2e..f21b4224fe236f 100644 --- a/test/e2e/prerender.test.ts +++ b/test/e2e/prerender.test.ts @@ -1850,8 +1850,8 @@ describe('Prerender', () => { previewRes.headers .get('set-cookie') .split(',') - .forEach((c) => { - c = cookie.parse(c) + .forEach((s) => { + const c = cookie.parse(s) const isBypass = c.__prerender_bypass if (isBypass || c.__next_preview_data) { diff --git a/test/integration/config-output-export/test/index.test.ts b/test/integration/config-output-export/test/index.test.ts index 3a8c2089ea5fb3..3389cd8a316ed2 100644 --- a/test/integration/config-output-export/test/index.test.ts +++ b/test/integration/config-output-export/test/index.test.ts @@ -11,6 +11,7 @@ import { import webdriver from 'next-webdriver' import { join } from 'path' import fs from 'fs' +import type { Response } from 'node-fetch' const appDir = join(__dirname, '../') const nextConfig = new File(join(appDir, 'next.config.js')) diff --git a/test/integration/draft-mode/test/index.test.ts b/test/integration/draft-mode/test/index.test.ts index 46cce8c9e4623b..4f8cfd452a4010 100644 --- a/test/integration/draft-mode/test/index.test.ts +++ b/test/integration/draft-mode/test/index.test.ts @@ -43,7 +43,10 @@ describe('Test Draft Mode', () => { const res = await fetchViaHTTP(appPort, '/api/enable') expect(res.status).toBe(200) - const cookies = res.headers.get('set-cookie').split(',').map(cookie.parse) + const cookies = res.headers + .get('set-cookie') + .split(',') + .map((c) => cookie.parse(c)) expect(cookies[0]).toBeTruthy() expect(cookies[0].__prerender_bypass).toBeTruthy() @@ -76,7 +79,7 @@ describe('Test Draft Mode', () => { .get('set-cookie') .replace(/(=(?!Lax)\w{3}),/g, '$1') .split(',') - .map(cookie.parse) + .map((c) => cookie.parse(c)) expect(cookies[0]).toBeTruthy() }) @@ -154,7 +157,7 @@ describe('Test Draft Mode', () => { expect(res.status).toBe(200) const originalCookies = res.headers.get('set-cookie').split(',') - const cookies = originalCookies.map(cookie.parse) + const cookies = originalCookies.map((c) => cookie.parse(c)) expect(cookies.length).toBe(1) expect(cookies[0]).toBeTruthy() @@ -212,7 +215,7 @@ describe('Test Draft Mode', () => { .get('set-cookie') .replace(/(=(?!Lax)\w{3}),/g, '$1') .split(',') - .map(cookie.parse) + .map((c) => cookie.parse(c)) expect(cookies[0]).toBeTruthy() expect(cookies[0]).toMatchObject({ diff --git a/test/integration/draft-mode/tsconfig.json b/test/integration/draft-mode/tsconfig.json index 093985aafb4abc..b8b010bdaf5c4e 100644 --- a/test/integration/draft-mode/tsconfig.json +++ b/test/integration/draft-mode/tsconfig.json @@ -12,7 +12,15 @@ "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, - "jsx": "preserve" + "jsx": "preserve", + "baseUrl": "../../..", + "paths": { + "development-sandbox": ["test/lib/development-sandbox"], + "next-test-utils": ["test/lib/next-test-utils"], + "amp-test-utils": ["test/lib/amp-test-utils"], + "next-webdriver": ["test/lib/next-webdriver"], + "e2e-utils": ["test/lib/e2e-utils"] + } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], "exclude": ["node_modules"] diff --git a/test/integration/edge-runtime-streaming-error/test/index.test.ts b/test/integration/edge-runtime-streaming-error/test/index.test.ts index e9e68e9c492212..bb76cd7f8834cd 100644 --- a/test/integration/edge-runtime-streaming-error/test/index.test.ts +++ b/test/integration/edge-runtime-streaming-error/test/index.test.ts @@ -54,7 +54,7 @@ describe('dev mode', () => { context.appPort = await findPort() context.app = await launchApp(appDir, context.appPort, { ...context.handler, - env: { __NEXT_TEST_WITH_DEVTOOL: 1 }, + env: { __NEXT_TEST_WITH_DEVTOOL: '1' }, }) }) diff --git a/test/integration/edge-runtime-with-node.js-apis/test/index.test.ts b/test/integration/edge-runtime-with-node.js-apis/test/index.test.ts index ffde39196654af..1e749644b7a63e 100644 --- a/test/integration/edge-runtime-with-node.js-apis/test/index.test.ts +++ b/test/integration/edge-runtime-with-node.js-apis/test/index.test.ts @@ -69,7 +69,7 @@ describe.each([ output = '' appPort = await findPort() app = await launchApp(appDir, appPort, { - env: { __NEXT_TEST_WITH_DEVTOOL: 1 }, + env: { __NEXT_TEST_WITH_DEVTOOL: '1' }, onStdout(msg) { output += msg }, diff --git a/test/integration/next-image-legacy/typescript/test/index.test.ts b/test/integration/next-image-legacy/typescript/test/index.test.ts index 26eb8f81c74b0f..d7012891a9fe71 100644 --- a/test/integration/next-image-legacy/typescript/test/index.test.ts +++ b/test/integration/next-image-legacy/typescript/test/index.test.ts @@ -81,7 +81,7 @@ describe('TypeScript Image Component', () => { nextConfig, content.replace('// disableStaticImages', 'disableStaticImages') ) - const app = await launchApp(appDir, await findPort(), []) + const app = await launchApp(appDir, await findPort()) await killApp(app) await fs.writeFile(nextConfig, content) const envTypes = await fs.readFile(join(appDir, 'next-env.d.ts'), 'utf8') diff --git a/test/integration/next-image-new/typescript/test/index.test.ts b/test/integration/next-image-new/typescript/test/index.test.ts index 26eb8f81c74b0f..d7012891a9fe71 100644 --- a/test/integration/next-image-new/typescript/test/index.test.ts +++ b/test/integration/next-image-new/typescript/test/index.test.ts @@ -81,7 +81,7 @@ describe('TypeScript Image Component', () => { nextConfig, content.replace('// disableStaticImages', 'disableStaticImages') ) - const app = await launchApp(appDir, await findPort(), []) + const app = await launchApp(appDir, await findPort()) await killApp(app) await fs.writeFile(nextConfig, content) const envTypes = await fs.readFile(join(appDir, 'next-env.d.ts'), 'utf8') diff --git a/test/jest.d.ts b/test/jest.d.ts index b68ae07700cd38..f233e4b3465064 100644 --- a/test/jest.d.ts +++ b/test/jest.d.ts @@ -1 +1,27 @@ -import 'jest-extended' +/// +/// + +declare namespace jest { + // https://github.com/jestjs/jest/blob/6460335f88cee3dcb9d29c49d55ab02b9d83f994/packages/expect/src/types.ts#L58-L72 + interface MatcherState { + assertionCalls: number + currentTestName?: string + error?: Error + expand?: boolean + expectedAssertionsNumber: number | null + expectedAssertionsNumberError?: Error + isExpectingAssertions: boolean + isExpectingAssertionsError?: Error + isNot?: boolean + numPassingAsserts: number + promise?: string + suppressedErrors: Array + testPath?: string + } + + interface Expect { + // https://github.com/jestjs/jest/blob/6460335f88cee3dcb9d29c49d55ab02b9d83f994/packages/expect/src/index.ts#L461 + // https://github.com/jestjs/jest/blob/6460335f88cee3dcb9d29c49d55ab02b9d83f994/packages/expect/src/jestMatchersObject.ts#L44-L45 + getState(): MatcherState + } +} diff --git a/test/lib/next-modes/next-dev.ts b/test/lib/next-modes/next-dev.ts index 1cc49b3a810d76..b4ff43eae99013 100644 --- a/test/lib/next-modes/next-dev.ts +++ b/test/lib/next-modes/next-dev.ts @@ -1,4 +1,4 @@ -import { spawn } from 'cross-spawn' +import spawn from 'cross-spawn' import { Span } from 'next/src/trace' import { NextInstance } from './base' diff --git a/test/lib/next-modes/next-start.ts b/test/lib/next-modes/next-start.ts index 279b261cd6b4e6..c6531b041e01ca 100644 --- a/test/lib/next-modes/next-start.ts +++ b/test/lib/next-modes/next-start.ts @@ -1,13 +1,13 @@ import path from 'path' import fs from 'fs-extra' import { NextInstance } from './base' -import { spawn, SpawnOptions } from 'cross-spawn' +import spawn from 'cross-spawn' import { Span } from 'next/src/trace' export class NextStartInstance extends NextInstance { private _buildId: string private _cliOutput: string = '' - private spawnOpts: SpawnOptions + private spawnOpts: import('child_process').SpawnOptions public get buildId() { return this._buildId diff --git a/test/lib/next-test-utils.js b/test/lib/next-test-utils.ts similarity index 77% rename from test/lib/next-test-utils.js rename to test/lib/next-test-utils.ts index cc8e33ce801b6c..5ed2a6a0aaf937 100644 --- a/test/lib/next-test-utils.js +++ b/test/lib/next-test-utils.ts @@ -1,4 +1,3 @@ -import spawn from 'cross-spawn' import express from 'express' import { existsSync, @@ -7,27 +6,41 @@ import { writeFileSync, createReadStream, } from 'fs' -import { writeFile } from 'fs-extra' -import getPort from 'get-port' +import { promisify } from 'util' import http from 'http' import https from 'https' -import server from 'next/dist/server/next' -import _pkg from 'next/package.json' -import fetch from 'node-fetch' import path from 'path' + +import spawn from 'cross-spawn' +import { writeFile } from 'fs-extra' +import getPort from 'get-port' +import fetch from 'node-fetch' import qs from 'querystring' import treeKill from 'tree-kill' +import server from 'next/dist/server/next' +import _pkg from 'next/package.json' + +import type { SpawnOptions, ChildProcess } from 'child_process' +import type { RequestInit, Response } from 'node-fetch' +import type { NextServer } from 'next/dist/server/next' +import type { BrowserInterface } from './browsers/base' + export const nextServer = server export const pkg = _pkg export function initNextServerScript( - scriptPath, - successRegexp, - env, - failRegexp, - opts -) { + scriptPath: string, + successRegexp: RegExp, + env: NodeJS.ProcessEnv, + failRegexp?: RegExp, + opts?: { + cwd?: string + nodeArgs?: string[] + onStdout?: (data: any) => void + onStderr?: (data: any) => void + } +): Promise { return new Promise((resolve, reject) => { const instance = spawn( 'node', @@ -83,13 +96,11 @@ export function initNextServerScript( }) } -/** - * @param {string | number} appPortOrUrl - * @param {string} [url] - * @param {string} [hostname] - * @returns - */ -export function getFullUrl(appPortOrUrl, url, hostname) { +export function getFullUrl( + appPortOrUrl: string | number, + url?: string, + hostname?: string +) { let fullUrl = typeof appPortOrUrl === 'string' && appPortOrUrl.startsWith('http') ? appPortOrUrl @@ -114,11 +125,14 @@ export function getFullUrl(appPortOrUrl, url, hostname) { /** * Appends the querystring to the url * - * @param {string} pathname the pathname - * @param {Record | string} query the query object to add to the pathname + * @param pathname the pathname + * @param query the query object to add to the pathname * @returns the pathname with the query */ -export function withQuery(pathname, query) { +export function withQuery( + pathname: string, + query: Record | string +) { const querystring = typeof query === 'string' ? query : qs.stringify(query) if (querystring.length === 0) { return pathname @@ -133,30 +147,21 @@ export function withQuery(pathname, query) { return `${pathname}?${querystring}` } -export function renderViaAPI(app, pathname, query) { - const url = query ? withQuery(pathname, query) : pathname - return app.renderToHTML({ url }, {}, pathname, query) -} - -/** - * @param {string | number} appPort - * @param {string} pathname - * @param {Record | string | undefined} [query] - * @param {import('node-fetch').RequestInit} [opts] - * @returns {Promise} - */ -export function renderViaHTTP(appPort, pathname, query, opts) { +export function renderViaHTTP( + appPort: string | number, + pathname: string, + query?: Record | string | undefined, + opts?: RequestInit +) { return fetchViaHTTP(appPort, pathname, query, opts).then((res) => res.text()) } -/** - * @param {string | number} appPort - * @param {string} pathname - * @param {Record | string | null | undefined} [query] - * @param {import('node-fetch').RequestInit} [opts] - * @returns {Promise} - */ -export function fetchViaHTTP(appPort, pathname, query, opts) { +export function fetchViaHTTP( + appPort: string | number, + pathname: string, + query?: Record | string | null | undefined, + opts?: RequestInit +): Promise { const url = query ? withQuery(pathname, query) : pathname return fetch(getFullUrl(appPort, url), { // in node.js v17 fetch favors IPv6 but Next.js is @@ -177,14 +182,37 @@ export function findPort() { return getPort() } -export function runNextCommand(argv, options = {}) { +export interface NextOptions { + cwd?: string + env?: NodeJS.Dict + nodeArgs?: string[] + + spawnOptions?: SpawnOptions + instance?: (instance: ChildProcess) => void + stderr?: true | 'log' + stdout?: true | 'log' + ignoreFail?: boolean + + onStdout?: (data: any) => void + onStderr?: (data: any) => void +} + +export function runNextCommand( + argv: string[], + options: NextOptions = {} +): Promise<{ + code: number + signal: NodeJS.Signals + stdout: string + stderr: string +}> { const nextDir = path.dirname(require.resolve('next/package')) const nextBin = path.join(nextDir, 'dist/bin/next') const cwd = options.cwd || nextDir // Let Next.js decide the environment const env = { ...process.env, - NODE_ENV: '', + NODE_ENV: undefined, __NEXT_TEST_MODE: 'true', ...options.env, } @@ -279,14 +307,35 @@ export function runNextCommand(argv, options = {}) { }) instance.on('error', (err) => { - err.stdout = stdoutOutput - err.stderr = stderrOutput + err['stdout'] = stdoutOutput + err['stderr'] = stderrOutput reject(err) }) }) } -export function runNextCommandDev(argv, stdOut, opts = {}) { +export interface NextDevOptions { + cwd?: string + env?: NodeJS.Dict + nodeArgs?: string[] + nextBin?: string + + bootupMarker?: RegExp + nextStart?: boolean + turbo?: boolean + + stderr?: false + stdout?: false + + onStdout?: (data: any) => void + onStderr?: (data: any) => void +} + +export function runNextCommandDev( + argv: string[], + stdOut?: boolean, + opts: NextDevOptions = {} +): Promise<(typeof stdOut extends true ? string : ChildProcess) | undefined> { const nextDir = path.dirname(require.resolve('next/package')) const nextBin = opts.nextBin || path.join(nextDir, 'dist/bin/next') const cwd = opts.cwd || nextDir @@ -363,7 +412,7 @@ export function runNextCommandDev(argv, stdOut, opts = {}) { instance.stderr.removeListener('data', handleStderr) if (!didResolve) { didResolve = true - resolve() + resolve(undefined) } }) @@ -374,14 +423,20 @@ export function runNextCommandDev(argv, stdOut, opts = {}) { } // Launch the app in dev mode. -export function launchApp(dir, port, opts) { +export function launchApp( + dir: string, + port: string | number, + opts?: NextDevOptions +) { const options = opts ?? {} const useTurbo = !!process.env.TEST_WASM ? false : options?.turbo ?? shouldRunTurboDevTest() return runNextCommandDev( - [useTurbo ? '--turbo' : undefined, dir, '-p', port].filter(Boolean), + [useTurbo ? '--turbo' : undefined, dir, '-p', port as string].filter( + Boolean + ), undefined, { ...options, @@ -390,30 +445,46 @@ export function launchApp(dir, port, opts) { ) } -export function nextBuild(dir, args = [], opts = {}) { +export function nextBuild( + dir: string, + args: string[] = [], + opts: NextOptions = {} +) { return runNextCommand(['build', dir, ...args], opts) } -export function nextExport(dir, { outdir }, opts = {}) { +export function nextExport(dir: string, { outdir }, opts: NextOptions = {}) { return runNextCommand(['export', dir, '--outdir', outdir], opts) } -export function nextExportDefault(dir, opts = {}) { +export function nextExportDefault(dir: string, opts: NextOptions = {}) { return runNextCommand(['export', dir], opts) } -export function nextLint(dir, args = [], opts = {}) { +export function nextLint( + dir: string, + args: string[] = [], + opts: NextOptions = {} +) { return runNextCommand(['lint', dir, ...args], opts) } -export function nextStart(dir, port, opts = {}) { - return runNextCommandDev(['start', '-p', port, dir], undefined, { +export function nextStart( + dir: string, + port: string | number, + opts: NextDevOptions = {} +) { + return runNextCommandDev(['start', '-p', port as string, dir], undefined, { ...opts, nextStart: true, }) } -export function buildTS(args = [], cwd, env = {}) { +export function buildTS( + args: string[] = [], + cwd?: string, + env?: any +): Promise { cwd = cwd || path.dirname(require.resolve('next/package')) env = { ...process.env, NODE_ENV: undefined, ...env } @@ -441,8 +512,8 @@ export function buildTS(args = [], cwd, env = {}) { }) } -export async function killProcess(pid) { - await new Promise((resolve, reject) => { +export async function killProcess(pid: number): Promise { + return await new Promise((resolve, reject) => { treeKill(pid, (err) => { if (err) { if ( @@ -467,11 +538,11 @@ export async function killProcess(pid) { } // Kill a launched app -export async function killApp(instance) { +export async function killApp(instance: ChildProcess) { await killProcess(instance.pid) } -export async function startApp(app) { +export async function startApp(app: NextServer) { // force require usage instead of dynamic import in jest // x-ref: https://github.com/nodejs/node/issues/35889 process.env.__NEXT_TEST_MODE = 'jest' @@ -482,38 +553,29 @@ export async function startApp(app) { await app.prepare() const handler = app.getRequestHandler() const server = http.createServer(handler) - server.__app = app + server['__app'] = app + + await promisify(server.listen).apply(server) - await promiseCall(server, 'listen') return server } -export async function stopApp(server) { - if (server.__app) { - await server.__app.close() +export async function stopApp(server: http.Server) { + if (server['__app']) { + await server['__app'].close() } - await promiseCall(server, 'close') -} - -export function promiseCall(obj, method, ...args) { - return new Promise((resolve, reject) => { - const newArgs = [ - ...args, - function (err, res) { - if (err) return reject(err) - resolve(res) - }, - ] - - obj[method](...newArgs) - }) + await promisify(server.close).apply(server) } -export function waitFor(millis) { +export function waitFor(millis: number) { return new Promise((resolve) => setTimeout(resolve, millis)) } -export async function startStaticServer(dir, notFoundFile, fixedPort) { +export async function startStaticServer( + dir: string, + notFoundFile?: string, + fixedPort?: number +) { const app = express() const server = http.createServer(app) app.use(express.static(dir)) @@ -524,24 +586,24 @@ export async function startStaticServer(dir, notFoundFile, fixedPort) { }) } - await promiseCall(server, 'listen', fixedPort) + await promisify(server.listen).apply(server, fixedPort) return server } -export async function startCleanStaticServer(dir) { +export async function startCleanStaticServer(dir: string) { const app = express() const server = http.createServer(app) app.use(express.static(dir, { extensions: ['html'] })) - await promiseCall(server, 'listen') + await promisify(server.listen).apply(server) return server } // check for content in 1 second intervals timing out after // 30 seconds export async function check( - contentFn, - regex, + contentFn: () => any | Promise, + regex: any, hardError = true, maxRetries = 30 ) { @@ -551,7 +613,7 @@ export async function check( for (let tries = 0; tries < maxRetries; tries++) { try { content = await contentFn() - if (typeof regex !== typeof /regex/) { + if (!(regex instanceof RegExp)) { if (regex === content) { return true } @@ -574,21 +636,24 @@ export async function check( } export class File { - constructor(path) { + path: string + originalContent: string + + constructor(path: string) { this.path = path this.originalContent = existsSync(this.path) ? readFileSync(this.path, 'utf8') : null } - write(content) { + write(content: string) { if (!this.originalContent) { this.originalContent = content } writeFileSync(this.path, content, 'utf8') } - replace(pattern, newValue) { + replace(pattern: RegExp | string, newValue: string) { const currentContent = readFileSync(this.path, 'utf8') if (pattern instanceof RegExp) { if (!pattern.test(currentContent)) { @@ -619,7 +684,10 @@ export class File { } } -export async function evaluate(browser, input) { +export async function evaluate( + browser: BrowserInterface, + input: string | Function +) { if (typeof input === 'function') { const result = await browser.eval(input) await new Promise((resolve) => setTimeout(resolve, 30)) @@ -629,7 +697,12 @@ export async function evaluate(browser, input) { } } -export async function retry(fn, duration = 3000, interval = 500, description) { +export async function retry( + fn: () => T | Promise, + duration: number = 3000, + interval: number = 500, + description?: string +): Promise { if (duration % interval !== 0) { throw new Error( `invalid duration ${duration} and interval ${interval} mix, duration must be evenly divisible by interval` @@ -656,7 +729,7 @@ export async function retry(fn, duration = 3000, interval = 500, description) { } } -export async function hasRedbox(browser, expected = true) { +export async function hasRedbox(browser: BrowserInterface, expected = true) { for (let i = 0; i < 30; i++) { const result = await evaluate(browser, () => { return Boolean( @@ -678,7 +751,7 @@ export async function hasRedbox(browser, expected = true) { return false } -export async function getRedboxHeader(browser) { +export async function getRedboxHeader(browser: BrowserInterface) { return retry( () => { if (shouldRunTurboDevTest()) { @@ -709,7 +782,7 @@ export async function getRedboxHeader(browser) { ) } -export async function getRedboxSource(browser) { +export async function getRedboxSource(browser: BrowserInterface) { return retry( () => evaluate(browser, () => { @@ -731,7 +804,7 @@ export async function getRedboxSource(browser) { ) } -export async function getRedboxDescription(browser) { +export async function getRedboxDescription(browser: BrowserInterface) { return retry( () => evaluate(browser, () => { @@ -749,23 +822,23 @@ export async function getRedboxDescription(browser) { ) } -export function getBrowserBodyText(browser) { +export function getBrowserBodyText(browser: BrowserInterface) { return browser.eval('document.getElementsByTagName("body")[0].innerText') } -export function normalizeRegEx(src) { +export function normalizeRegEx(src: string) { return new RegExp(src).source.replace(/\^\//g, '^\\/') } -function readJson(path) { - return JSON.parse(readFileSync(path)) +function readJson(path: string) { + return JSON.parse(readFileSync(path, 'utf-8')) } -export function getBuildManifest(dir) { +export function getBuildManifest(dir: string) { return readJson(path.join(dir, '.next/build-manifest.json')) } -export function getPageFileFromBuildManifest(dir, page) { +export function getPageFileFromBuildManifest(dir: string, page: string) { const buildManifest = getBuildManifest(dir) const pageFiles = buildManifest.pages[page] if (!pageFiles) { @@ -784,24 +857,24 @@ export function getPageFileFromBuildManifest(dir, page) { return pageFile } -export function readNextBuildClientPageFile(appDir, page) { +export function readNextBuildClientPageFile(appDir: string, page: string) { const pageFile = getPageFileFromBuildManifest(appDir, page) return readFileSync(path.join(appDir, '.next', pageFile), 'utf8') } -export function getPagesManifest(dir) { +export function getPagesManifest(dir: string) { const serverFile = path.join(dir, '.next/server/pages-manifest.json') return readJson(serverFile) } -export function updatePagesManifest(dir, content) { +export function updatePagesManifest(dir: string, content: any) { const serverFile = path.join(dir, '.next/server/pages-manifest.json') return writeFile(serverFile, content) } -export function getPageFileFromPagesManifest(dir, page) { +export function getPageFileFromPagesManifest(dir: string, page: string) { const pagesManifest = getPagesManifest(dir) const pageFile = pagesManifest[page] if (!pageFile) { @@ -811,18 +884,26 @@ export function getPageFileFromPagesManifest(dir, page) { return pageFile } -export function readNextBuildServerPageFile(appDir, page) { +export function readNextBuildServerPageFile(appDir: string, page: string) { const pageFile = getPageFileFromPagesManifest(appDir, page) return readFileSync(path.join(appDir, '.next', 'server', pageFile), 'utf8') } -/** - * - * @param {string} suiteName - * @param {{env: 'prod' | 'dev', appDir: string}} context - * @param {{beforeAll?: Function; afterAll?: Function; runTests: Function}} options - */ -function runSuite(suiteName, context, options) { +function runSuite( + suiteName: string, + context: { env: 'prod' | 'dev'; appDir: string } & Partial<{ + stderr: string + stdout: string + appPort: number + code: number + server: ChildProcess + }>, + options: { + beforeAll?: Function + afterAll?: Function + runTests: Function + } & NextDevOptions +) { const { appDir, env } = context describe(`${suiteName} ${env}`, () => { beforeAll(async () => { @@ -872,23 +953,29 @@ function runSuite(suiteName, context, options) { }) } -/** - * - * @param {string} suiteName - * @param {string} appDir - * @param {{beforeAll?: Function; afterAll?: Function; runTests: Function; env?: Record}} options - */ -export function runDevSuite(suiteName, appDir, options) { +export function runDevSuite( + suiteName: string, + appDir: string, + options: { + beforeAll?: Function + afterAll?: Function + runTests: Function + env?: NodeJS.ProcessEnv + } +) { return runSuite(suiteName, { appDir, env: 'dev' }, options) } -/** - * - * @param {string} suiteName - * @param {string} appDir - * @param {{beforeAll?: Function; afterAll?: Function; runTests: Function; env?: Record}} options - */ -export function runProdSuite(suiteName, appDir, options) { +export function runProdSuite( + suiteName: string, + appDir: string, + options: { + beforeAll?: Function + afterAll?: Function + runTests: Function + env?: NodeJS.ProcessEnv + } +) { return runSuite(suiteName, { appDir, env: 'prod' }, options) } @@ -898,7 +985,7 @@ export function runProdSuite(suiteName, appDir, options) { * @param {string} eventName * @returns {Array<{}>} */ -export function findAllTelemetryEvents(output, eventName) { +export function findAllTelemetryEvents(output: string, eventName: string) { const regex = /\[telemetry\] ({.+?^})/gms // Pop the last element of each entry to retrieve contents of the capturing group const events = [...output.matchAll(regex)].map((entry) => @@ -916,7 +1003,7 @@ export function findAllTelemetryEvents(output, eventName) { * it makes hard to conform with existing lint rules. Instead, starting off from manual fixture setup and * update test cases accordingly as turbopack changes enable more test cases. */ -export function shouldRunTurboDevTest() { +export function shouldRunTurboDevTest(): boolean { if (!!process.env.TEST_WASM) { return false } @@ -955,10 +1042,12 @@ export function shouldRunTurboDevTest() { return isMatch } +type TestVariants = 'default' | 'turbo' + // WEB-168: There are some differences / incompletes in turbopack implementation enforces jest requires to update // test snapshot when run against turbo. This fn returns describe, or describe.skip dependes on the running context // to avoid force-snapshot update per each runs until turbopack update includes all the changes. -export function getSnapshotTestDescribe(variant) { +export function getSnapshotTestDescribe(variant: TestVariants) { const runningEnv = variant ?? 'default' if (runningEnv !== 'default' && runningEnv !== 'turbo') { throw new Error( @@ -978,20 +1067,20 @@ export function getSnapshotTestDescribe(variant) { * For better editor support, pass in the variants this should run on (`default` and/or `turbo`) as cases. * * This is necessary if separate snapshots are needed for next.js with webpack vs turbopack. - * - * @type {Pick} */ export const describeVariants = { - each: (variants) => (name, fn) => { - if ( - !Array.isArray(variants) || - !variants.every((val) => typeof val === 'string') - ) { - throw new Error('variants need to be an array of strings') - } + each(variants: TestVariants[]) { + return (name: string, fn: (...args: TestVariants[]) => any) => { + if ( + !Array.isArray(variants) || + !variants.every((val) => typeof val === 'string') + ) { + throw new Error('variants need to be an array of strings') + } - for (const variant of variants) { - getSnapshotTestDescribe(variant).each([variant])(name, fn) + for (const variant of variants) { + getSnapshotTestDescribe(variant).each([variant])(name, fn) + } } }, } diff --git a/test/production/standalone-mode/type-module/index.test.ts b/test/production/standalone-mode/type-module/index.test.ts index 51a34e6d0bdce9..24f79917889ee9 100644 --- a/test/production/standalone-mode/type-module/index.test.ts +++ b/test/production/standalone-mode/type-module/index.test.ts @@ -44,7 +44,7 @@ describe('type-module', () => { const server = await initNextServerScript( serverFile, /Listening on/, - { ...process.env, PORT: appPort }, + { ...process.env, PORT: appPort.toString() }, undefined, { cwd: next.testDir } ) diff --git a/tsconfig.json b/tsconfig.json index 911b5d84d7cb8d..e5c58ec3f7ac81 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,10 +3,10 @@ "strict": false, "noEmit": true, "allowJs": true, + "resolveJsonModule": true, "jsx": "react", "module": "esnext", "target": "ESNext", - "skipLibCheck": true, "esModuleInterop": true, "moduleResolution": "node", "baseUrl": ".", diff --git a/turbo.json b/turbo.json index 11b6c1103407b1..d410ce557ded5f 100644 --- a/turbo.json +++ b/turbo.json @@ -38,6 +38,7 @@ "outputs": ["dist/**"] }, "typescript": {}, + "//#typescript": {}, "rust-check": {}, "test-cargo-unit": {}, "//#get-test-timings": {