diff --git a/.eslintignore b/.eslintignore index bc70e6ae4a..cc151edb48 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,10 +1,11 @@ .next node_modules -test lib demos packages/runtime/src/templates/edge packages/runtime/src/templates/edge-shared packages/runtime/lib packages/runtime/dist-types -jestSetup.js \ No newline at end of file +jestSetup.js +test/e2e +test/fixtures/broken_next_config/next.config.js \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js index 69ae1605ad..1510231221 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -59,5 +59,28 @@ module.exports = { 'promise/catch-or-return': 0, }, }, + { + files: ['test/**', 'packages/**/test/**'], + plugins: ['jest'], + extends: ['plugin:jest/recommended'], + rules: { + // Disable global rules + 'max-nested-callbacks': 'off', + '@typescript-eslint/no-empty-function': 0, + 'max-lines-per-function': 0, + 'unicorn/no-empty-file': 0, + 'prefer-destructuring': 0, + '@typescript-eslint/no-unused-vars': 0, + 'unicorn/no-await-expression-member': 0, + 'import/no-anonymous-default-export': 0, + 'no-shadow': 0, + '@typescript-eslint/no-var-requires': 0, + 'require-await': 0, + // esling-plugin-jest specific rules + 'jest/consistent-test-it': ['error', { fn: 'it', withinDescribe: 'it' }], + 'jest/no-disabled-tests': 0, + 'jest/no-conditional-expect': 0, + }, + }, ], } diff --git a/.prettierignore b/.prettierignore index 52c69c7596..d65b076d46 100644 --- a/.prettierignore +++ b/.prettierignore @@ -21,6 +21,8 @@ node_modules lib tsconfig.json demos/nx-next-monorepo-demo +test/fixtures/broken_next_config/next.config.js +test/e2e **/CHANGELOG.md packages/runtime/lib diff --git a/package-lock.json b/package-lock.json index 0a8432e482..ac4cbff04f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,9 +43,11 @@ "cypress": "^12.10.0", "escape-string-regexp": "^2.0.0", "eslint-config-next": "^12.0.0", + "eslint-plugin-jest": "^27.2.1", "eslint-plugin-promise": "^6.0.0", "eslint-plugin-unicorn": "^43.0.2", "execa": "^5.1.1", + "fs-extra": "^11.1.1", "husky": "^7.0.4", "jest": "^27.0.0", "jest-extended": "^3.2.0", @@ -54,6 +56,8 @@ "mock-fs": "^5.2.0", "netlify-plugin-cypress": "^2.2.1", "npm-run-all": "^4.1.5", + "outdent": "^0.8.0", + "pathe": "^1.1.0", "playwright-chromium": "1.28.1", "prettier": "^2.1.2", "react": "^18.2.0", @@ -2363,6 +2367,20 @@ "node": ">=v14" } }, + "node_modules/@commitlint/read/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@commitlint/resolve-extends": { "version": "17.1.0", "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-17.1.0.tgz", @@ -4854,19 +4872,6 @@ "unstorage": "^1.0.0" } }, - "node_modules/@netlify/ipx/node_modules/fs-extra": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", - "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, "node_modules/@netlify/ipx/node_modules/ufo": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.1.1.tgz", @@ -5889,6 +5894,12 @@ "@types/responselike": "^1.0.0" } }, + "node_modules/@types/chance": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@types/chance/-/chance-1.1.3.tgz", + "integrity": "sha512-X6c6ghhe4/sQh4XzcZWSFaTAUOda38GQHmq9BUanYkOE/EO7ZrkazwKmtsj3xzTjkLWmwULE++23g3d3CCWaWw==", + "dev": true + }, "node_modules/@types/fs-extra": { "version": "9.0.13", "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", @@ -11626,6 +11637,30 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/eslint-plugin-jest": { + "version": "27.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.2.1.tgz", + "integrity": "sha512-l067Uxx7ZT8cO9NJuf+eJHvt6bqJyz2Z29wykyEdz/OtmcELQl2MQGQLX8J94O1cSJWAwUSEvCjwjA7KEK3Hmg==", + "dev": true, + "dependencies": { + "@typescript-eslint/utils": "^5.10.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^5.0.0", + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + }, + "jest": { + "optional": true + } + } + }, "node_modules/eslint-plugin-jsx-a11y": { "version": "6.6.1", "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.6.1.tgz", @@ -13110,16 +13145,16 @@ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" }, "node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", + "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" }, "engines": { - "node": ">=12" + "node": ">=14.14" } }, "node_modules/fs-memo": { @@ -19843,9 +19878,10 @@ } }, "node_modules/pathe": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-0.2.0.tgz", - "integrity": "sha512-sTitTPYnn23esFR3RlqYBWn4c45WGeLcsKzQiUpXJAyfcWkolvlYpV8FLo7JishK946oQwMFUCHXQ9AjGPKExw==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.0.tgz", + "integrity": "sha512-ODbEPR0KKHqECXW1GoxdDb+AZvULmXjVPy4rt+pGo2+TnjJTIPJQSVS6N63n8T2Ip+syHhbn52OewKicV0373w==", + "dev": true }, "node_modules/pause-stream": { "version": "0.0.11", @@ -24314,7 +24350,9 @@ "license": "MIT", "devDependencies": { "@netlify/edge-functions": "^2.0.0", + "@types/chance": "^1.1.3", "@types/node": "^17.0.25", + "chance": "^1.1.11", "next": "^13.3.0", "npm-run-all": "^4.1.5", "typescript": "^4.6.3" @@ -24370,6 +24408,24 @@ "node": ">=12.0.0" } }, + "packages/runtime/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "packages/runtime/node_modules/pathe": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-0.2.0.tgz", + "integrity": "sha512-sTitTPYnn23esFR3RlqYBWn4c45WGeLcsKzQiUpXJAyfcWkolvlYpV8FLo7JishK946oQwMFUCHXQ9AjGPKExw==" + }, "packages/runtime/node_modules/semver": { "version": "7.3.8", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", @@ -25897,6 +25953,19 @@ "fs-extra": "^10.0.0", "git-raw-commits": "^2.0.0", "minimist": "^1.2.6" + }, + "dependencies": { + "fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + } } }, "@commitlint/resolve-extends": { @@ -27630,16 +27699,6 @@ "unstorage": "^1.0.0" }, "dependencies": { - "fs-extra": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", - "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", - "requires": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - } - }, "ufo": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.1.1.tgz", @@ -27651,7 +27710,9 @@ "version": "file:packages/next", "requires": { "@netlify/edge-functions": "^2.0.0", + "@types/chance": "*", "@types/node": "^17.0.25", + "chance": "^1.1.11", "next": "^13.3.0", "npm-run-all": "^4.1.5", "typescript": "^4.6.3" @@ -27700,6 +27761,21 @@ "typescript": "^4.6.3" }, "dependencies": { + "fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "pathe": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-0.2.0.tgz", + "integrity": "sha512-sTitTPYnn23esFR3RlqYBWn4c45WGeLcsKzQiUpXJAyfcWkolvlYpV8FLo7JishK946oQwMFUCHXQ9AjGPKExw==" + }, "semver": { "version": "7.3.8", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", @@ -28396,6 +28472,12 @@ "@types/responselike": "^1.0.0" } }, + "@types/chance": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@types/chance/-/chance-1.1.3.tgz", + "integrity": "sha512-X6c6ghhe4/sQh4XzcZWSFaTAUOda38GQHmq9BUanYkOE/EO7ZrkazwKmtsj3xzTjkLWmwULE++23g3d3CCWaWw==", + "dev": true + }, "@types/fs-extra": { "version": "9.0.13", "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", @@ -32909,6 +32991,15 @@ } } }, + "eslint-plugin-jest": { + "version": "27.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.2.1.tgz", + "integrity": "sha512-l067Uxx7ZT8cO9NJuf+eJHvt6bqJyz2Z29wykyEdz/OtmcELQl2MQGQLX8J94O1cSJWAwUSEvCjwjA7KEK3Hmg==", + "dev": true, + "requires": { + "@typescript-eslint/utils": "^5.10.0" + } + }, "eslint-plugin-jsx-a11y": { "version": "6.6.1", "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.6.1.tgz", @@ -33899,9 +33990,9 @@ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" }, "fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", + "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", "requires": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -39014,9 +39105,10 @@ "dev": true }, "pathe": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-0.2.0.tgz", - "integrity": "sha512-sTitTPYnn23esFR3RlqYBWn4c45WGeLcsKzQiUpXJAyfcWkolvlYpV8FLo7JishK946oQwMFUCHXQ9AjGPKExw==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.0.tgz", + "integrity": "sha512-ODbEPR0KKHqECXW1GoxdDb+AZvULmXjVPy4rt+pGo2+TnjJTIPJQSVS6N63n8T2Ip+syHhbn52OewKicV0373w==", + "dev": true }, "pause-stream": { "version": "0.0.11", diff --git a/package.json b/package.json index f100c951b7..9278d30d4b 100644 --- a/package.json +++ b/package.json @@ -29,8 +29,8 @@ "test:update": "run-s build build:demo test:jest:update" }, "config": { - "eslint": "--cache --format=codeframe --max-warnings=0 \"{packages,src,scripts,tests,.github}/**/*.{ts,js,md,html}\" \"*.{ts,js,md,html}\" \".*.{ts,js,md,html}\"", - "prettier": "--loglevel=warn \"{packages,src,scripts,tests,.github}/**/*.{ts,js,md,yml,json,html}\" \"*.{ts,js,yml,json,html}\" \".*.{ts,js,yml,json,html}\" \"!package-lock.json\"" + "eslint": "--cache --format=codeframe --max-warnings=0 \"{packages,test,.github}/**/*.{ts,js,md,html}\" \"*.{ts,js,md,html}\" \".*.{ts,js,md,html}\"", + "prettier": "--loglevel=warn \"{packages,test,.github}/**/*.{ts,js,md,yml,json,html}\" \"*.{ts,js,yml,json,html}\" \".*.{ts,js,yml,json,html}\" \"!package-lock.json\"" }, "repository": { "type": "git", @@ -67,9 +67,11 @@ "cypress": "^12.10.0", "escape-string-regexp": "^2.0.0", "eslint-config-next": "^12.0.0", + "eslint-plugin-jest": "^27.2.1", "eslint-plugin-promise": "^6.0.0", "eslint-plugin-unicorn": "^43.0.2", "execa": "^5.1.1", + "fs-extra": "^11.1.1", "husky": "^7.0.4", "jest": "^27.0.0", "jest-extended": "^3.2.0", @@ -78,6 +80,8 @@ "mock-fs": "^5.2.0", "netlify-plugin-cypress": "^2.2.1", "npm-run-all": "^4.1.5", + "outdent": "^0.8.0", + "pathe": "^1.1.0", "playwright-chromium": "1.28.1", "prettier": "^2.1.2", "react": "^18.2.0", diff --git a/packages/next/package.json b/packages/next/package.json index cb1f5d164c..15667ad541 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -8,7 +8,9 @@ ], "devDependencies": { "@netlify/edge-functions": "^2.0.0", + "@types/chance": "^1.1.3", "@types/node": "^17.0.25", + "chance": "^1.1.11", "next": "^13.3.0", "npm-run-all": "^4.1.5", "typescript": "^4.6.3" diff --git a/packages/next/test/request.spec.ts b/packages/next/test/request.spec.ts index a421de9544..f772af6f4c 100644 --- a/packages/next/test/request.spec.ts +++ b/packages/next/test/request.spec.ts @@ -2,6 +2,7 @@ import Chance from 'chance' import { NextURL } from 'next/dist/server/web/next-url' import { RequestCookies } from 'next/dist/server/web/spec-extension/cookies' import { NextRequest } from 'next/server' + import { MiddlewareRequest } from '../src/middleware/request' const chance = new Chance() @@ -75,19 +76,19 @@ describe('MiddlewareRequest', () => { it('throws an error when MiddlewareRequest is run outside of edge environment', () => { delete globalThis.Deno - expect(() => new MiddlewareRequest(nextRequest)).toThrowError( + expect(() => new MiddlewareRequest(nextRequest)).toThrow( 'MiddlewareRequest only works in a Netlify Edge Function environment', ) }) it('throws an error when x-nf-request-id header is missing', () => { nextRequest.headers.delete('x-nf-request-id') - expect(() => new MiddlewareRequest(nextRequest)).toThrowError('Missing x-nf-request-id header') + expect(() => new MiddlewareRequest(nextRequest)).toThrow('Missing x-nf-request-id header') }) it('throws an error when request context is missing', () => { globalThis.NFRequestContextMap.delete(requestId) - expect(() => new MiddlewareRequest(nextRequest)).toThrowError( + expect(() => new MiddlewareRequest(nextRequest)).toThrow( `Could not find request context for request id ${requestId}`, ) }) diff --git a/test/fixtures/analysis/background.js.nft.json b/test/fixtures/analysis/background.js.nft.json index e212381029..8872301a94 100644 --- a/test/fixtures/analysis/background.js.nft.json +++ b/test/fixtures/analysis/background.js.nft.json @@ -1 +1 @@ -{"version":1,"files":["../../webpack-api-runtime.js","../../../package.json"]} \ No newline at end of file +{ "version": 1, "files": ["../../webpack-api-runtime.js", "../../../package.json"] } diff --git a/test/fixtures/not-static.json b/test/fixtures/not-static.json index 2cb68341e3..cdb2db3ada 100644 --- a/test/fixtures/not-static.json +++ b/test/fixtures/not-static.json @@ -82,5 +82,4 @@ "build:ci:frontend": "npm ci && npm run build && yarn workspace frontend build" } } - ] diff --git a/test/fixtures/page-extensions/custom/pages/api/custom.api.js b/test/fixtures/page-extensions/custom/pages/api/custom.api.js index 625c0891b2..172f1ae6a4 100644 --- a/test/fixtures/page-extensions/custom/pages/api/custom.api.js +++ b/test/fixtures/page-extensions/custom/pages/api/custom.api.js @@ -1 +1 @@ -// noop \ No newline at end of file +// noop diff --git a/test/fixtures/page-extensions/default/pages/api/default.js b/test/fixtures/page-extensions/default/pages/api/default.js index 625c0891b2..172f1ae6a4 100644 --- a/test/fixtures/page-extensions/default/pages/api/default.js +++ b/test/fixtures/page-extensions/default/pages/api/default.js @@ -1 +1 @@ -// noop \ No newline at end of file +// noop diff --git a/test/fixtures/serverless_next_config/next.config.js b/test/fixtures/serverless_next_config/next.config.js index 0fbd0e535c..f0b0ae0241 100644 --- a/test/fixtures/serverless_next_config/next.config.js +++ b/test/fixtures/serverless_next_config/next.config.js @@ -1,3 +1,3 @@ module.exports = { - target: 'serverless' + target: 'serverless', } diff --git a/test/helpers/analysis.spec.ts b/test/helpers/analysis.spec.ts index a066f56508..0aa0748137 100644 --- a/test/helpers/analysis.spec.ts +++ b/test/helpers/analysis.spec.ts @@ -1,6 +1,7 @@ -import { extractConfigFromFile } from '../../packages/runtime/src/helpers/analysis' import { resolve } from 'pathe' +import { extractConfigFromFile } from '../../packages/runtime/src/helpers/analysis' + describe('static source analysis', () => { beforeEach(() => { // Spy on console.error @@ -40,9 +41,9 @@ describe('static source analysis', () => { }) }) it('should throw if schedule is provided when type is background', async () => { - await expect(extractConfigFromFile(resolve(__dirname, '../fixtures/analysis/background-schedule.ts'))).rejects.toThrow( - 'Unsupported config value in test/fixtures/analysis/background-schedule.ts', - ) + await expect( + extractConfigFromFile(resolve(__dirname, '../fixtures/analysis/background-schedule.ts')), + ).rejects.toThrow('Unsupported config value in test/fixtures/analysis/background-schedule.ts') expect(console.error).toHaveBeenCalledWith( `Invalid config value in test/fixtures/analysis/background-schedule.ts: schedule is not allowed unless type is "experimental-scheduled"`, ) diff --git a/test/helpers/config.spec.ts b/test/helpers/config.spec.ts index 2373e2dc9c..fbdc2479b5 100644 --- a/test/helpers/config.spec.ts +++ b/test/helpers/config.spec.ts @@ -1,10 +1,13 @@ -import { - generateCustomHeaders, - NextConfig -} from "../../packages/runtime/src/helpers/config" import type { NetlifyPluginOptions } from '@netlify/build' -const netlifyConfig = { build: { command: 'npm run build' }, functions: {}, redirects: [], headers: [] } as NetlifyPluginOptions["netlifyConfig"] +import { generateCustomHeaders, NextConfig } from '../../packages/runtime/src/helpers/config' + +const netlifyConfig = { + build: { command: 'npm run build' }, + functions: {}, + redirects: [], + headers: [], +} as NetlifyPluginOptions['netlifyConfig'] describe('generateCustomHeaders', () => { // The routesManifest is the contents of the routes-manifest.json file which will already contain the generated @@ -426,4 +429,4 @@ describe('generateCustomHeaders', () => { }, ]) }) -}) \ No newline at end of file +}) diff --git a/test/helpers/edge.spec.ts b/test/helpers/edge.spec.ts index f4ee91f28e..8ccfffabc8 100644 --- a/test/helpers/edge.spec.ts +++ b/test/helpers/edge.spec.ts @@ -1,6 +1,7 @@ -import { generateRscDataEdgeManifest } from '../../packages/runtime/src/helpers/edge' import type { PrerenderManifest } from 'next/dist/build' +import { generateRscDataEdgeManifest } from '../../packages/runtime/src/helpers/edge' + jest.mock('../../packages/runtime/src/helpers/functionsMetaData', () => { const { NEXT_PLUGIN_NAME } = require('../../packages/runtime/src/constants') return { @@ -10,7 +11,7 @@ jest.mock('../../packages/runtime/src/helpers/functionsMetaData', () => { }) const basePrerenderManifest: PrerenderManifest = { - version: 3, + version: 4, routes: {}, dynamicRoutes: {}, notFoundRoutes: [], @@ -41,13 +42,13 @@ describe('generateRscDataEdgeManifest', () => { expect(edgeManifest).toEqual([ { function: 'rsc-data', - generator: "@netlify/next-runtime@1.0.0", + generator: '@netlify/next-runtime@1.0.0', name: 'RSC data routing', path: '/', }, { function: 'rsc-data', - generator: "@netlify/next-runtime@1.0.0", + generator: '@netlify/next-runtime@1.0.0', name: 'RSC data routing', path: '/index.rsc', }, @@ -94,13 +95,13 @@ describe('generateRscDataEdgeManifest', () => { expect(edgeManifest).toEqual([ { function: 'rsc-data', - generator: "@netlify/next-runtime@1.0.0", + generator: '@netlify/next-runtime@1.0.0', name: 'RSC data routing', pattern: '^/blog/([^/]+?)(?:/)?$', }, { function: 'rsc-data', - generator: "@netlify/next-runtime@1.0.0", + generator: '@netlify/next-runtime@1.0.0', name: 'RSC data routing', pattern: '^/blog/([^/]+?)\\.rsc$', }, diff --git a/test/helpers/files.spec.ts b/test/helpers/files.spec.ts index 02e77e3e5b..02672a3958 100644 --- a/test/helpers/files.spec.ts +++ b/test/helpers/files.spec.ts @@ -1,3 +1,8 @@ +import path, { dirname } from 'path' + +import { readFileSync, copy, ensureDir } from 'fs-extra' +import { resolve, join } from 'pathe' + import { matchMiddleware, stripLocale, @@ -7,18 +12,9 @@ import { unpatchNextFiles, getDependenciesOfFile, getSourceFileForPage, -} from "../../packages/runtime/src/helpers/files" -import { - readFileSync, - copy, - ensureDir, -} from "fs-extra" -import path from "path" -import { dirname } from "path" -import { resolve } from 'pathe' -import { join } from "pathe" -import { Rewrites } from "../../packages/runtime/src/helpers/types" -import { describeCwdTmpDir, moveNextDist } from "../test-utils" +} from '../../packages/runtime/src/helpers/files' +import { Rewrites } from '../../packages/runtime/src/helpers/types' +import { describeCwdTmpDir } from '../test-utils' const TEST_DIR = resolve(__dirname, '..') @@ -63,7 +59,7 @@ const REWRITES: Rewrites = [ ] describe('files utility functions', () => { - test('middleware tester matches correct paths', () => { + it('middleware tester matches correct paths', () => { const middleware = ['middle', 'sub/directory'] const paths = [ 'middle.html', @@ -80,7 +76,7 @@ describe('files utility functions', () => { } }) - test('middleware tester does not match incorrect paths', () => { + it('middleware tester does not match incorrect paths', () => { const middleware = ['middle', 'sub/directory'] const paths = [ 'middl', @@ -97,7 +93,7 @@ describe('files utility functions', () => { } }) - test('middleware tester matches root middleware', () => { + it('middleware tester matches root middleware', () => { const middleware = [''] const paths = [ 'middl', @@ -114,7 +110,7 @@ describe('files utility functions', () => { } }) - test('middleware tester matches root middleware', () => { + it('middleware tester does not match undefined', () => { const paths = [ 'middl', '', @@ -130,7 +126,7 @@ describe('files utility functions', () => { } }) - test('stripLocale correctly strips matching locales', () => { + it('stripLocale correctly strips matching locales', () => { const locales = ['en', 'fr', 'en-GB'] const paths = [ ['en/file.html', 'file.html'], @@ -144,7 +140,7 @@ describe('files utility functions', () => { } }) - test('stripLocale does not touch non-matching matching locales', () => { + it('stripLocale does not touch non-matching matching locales', () => { const locales = ['en', 'fr', 'en-GB'] const paths = ['de/file.html', 'enfile.html', 'en-US/file.html'] for (const path of paths) { @@ -152,21 +148,21 @@ describe('files utility functions', () => { } }) - test('matchesRedirect correctly matches paths with locales', () => { + it('matchesRedirect correctly matches paths with locales', () => { const paths = ['en/redirectme.html', 'en/redirectme.json', 'fr/redirectme.html', 'fr/redirectme.json'] paths.forEach((path) => { expect(matchesRedirect(path, REDIRECTS)).toBeTruthy() }) }) - test("matchesRedirect doesn't match paths with invalid locales", () => { + it("matchesRedirect doesn't match paths with invalid locales", () => { const paths = ['dk/redirectme.html', 'dk/redirectme.json', 'gr/redirectme.html', 'gr/redirectme.json'] paths.forEach((path) => { expect(matchesRedirect(path, REDIRECTS)).toBeFalsy() }) }) - test("matchesRedirect doesn't match internal redirects", () => { + it("matchesRedirect doesn't match internal redirects", () => { const paths = ['en/notrailingslash'] paths.forEach((path) => { expect(matchesRedirect(path, REDIRECTS)).toBeFalsy() @@ -241,4 +237,4 @@ describe('getSourceFileForPage', () => { expect(filePath.replace(TEST_DIR, '')).toBe('/fixtures/page-extensions/custom/pages/api/custom.api.js') }) -}) \ No newline at end of file +}) diff --git a/test/helpers/functions.spec.ts b/test/helpers/functions.spec.ts index 66abefae0a..965339b8b1 100644 --- a/test/helpers/functions.spec.ts +++ b/test/helpers/functions.spec.ts @@ -1,10 +1,10 @@ -import { getExtendedApiRouteConfigs } from "../../packages/runtime/src/helpers/functions" -import { describeCwdTmpDir, moveNextDist } from "../test-utils" +import { getExtendedApiRouteConfigs } from '../../packages/runtime/src/helpers/functions' +import { describeCwdTmpDir, moveNextDist } from '../test-utils' describeCwdTmpDir('api route file analysis', () => { it('extracts correct route configs from source files', async () => { await moveNextDist() - const configs = await getExtendedApiRouteConfigs('.next', process.cwd()) + const configs = await getExtendedApiRouteConfigs('.next', process.cwd(), ['js', 'jsx', 'ts', 'tsx']) // Using a Set means the order doesn't matter expect(new Set(configs)).toEqual( new Set([ @@ -21,4 +21,4 @@ describeCwdTmpDir('api route file analysis', () => { ]), ) }) -}) \ No newline at end of file +}) diff --git a/test/helpers/functionsMetaData.spec.ts b/test/helpers/functionsMetaData.spec.ts index 81f51efc69..1835863d5f 100644 --- a/test/helpers/functionsMetaData.spec.ts +++ b/test/helpers/functionsMetaData.spec.ts @@ -1,6 +1,7 @@ import { readJSON } from 'fs-extra' import mock from 'mock-fs' import { join } from 'pathe' + import { NEXT_PLUGIN_NAME } from '../../packages/runtime/src/constants' import { writeFunctionConfiguration } from '../../packages/runtime/src/helpers/functionsMetaData' @@ -102,4 +103,4 @@ describe('writeFunctionConfiguration', () => { expect(actual).toEqual(expected) }) -}) \ No newline at end of file +}) diff --git a/test/helpers/matchers.spec.ts b/test/helpers/matchers.spec.ts index 10881ac281..96cbca77a9 100644 --- a/test/helpers/matchers.spec.ts +++ b/test/helpers/matchers.spec.ts @@ -1,8 +1,9 @@ -import { makeLocaleOptional, stripLookahead } from '../../packages/runtime/src/helpers/matchers' import { getEdgeFunctionPatternForPage } from '../../packages/runtime/src/helpers/edge' +import { makeLocaleOptional, stripLookahead } from '../../packages/runtime/src/helpers/matchers' + const makeDataPath = (path: string) => `/_next/data/build-id${path === '/' ? '/index' : path}.json` -function checkPath(path: string, regex: string) { +const checkPath = (path: string, regex: string) => { const re = new RegExp(regex) const dataPath = makeDataPath(path) const testPath = re.test(path) @@ -28,7 +29,7 @@ describe('the middleware path matcher', () => { // The regex generated by Next for the path "/static" with i18n enabled (>= 13.3.0) const regexTwo = '^(?:\\/(_next\\/data\\/[^/]{1,}))?(?:\\/((?!_next\\/)[^/.]{1,}))\\/static(.json)?[\\/#\\?]?$' - function assertion(input) { + const assertion = (input) => { expect(checkPath('/static', input)).toBe(false) expect(checkPath('/static', makeLocaleOptional(input))).toBe(true) expect(checkPath('/en/static', makeLocaleOptional(input))).toBe(true) diff --git a/test/helpers/utils.spec.ts b/test/helpers/utils.spec.ts index e02bd3fb42..772072bceb 100644 --- a/test/helpers/utils.spec.ts +++ b/test/helpers/utils.spec.ts @@ -1,5 +1,6 @@ import Chance from 'chance' import { ExperimentalConfig } from 'next/dist/server/config-shared' + import { getCustomImageResponseHeaders, getRemotePatterns, diff --git a/test/helpers/verification.spec.ts b/test/helpers/verification.spec.ts index 07dd33efd8..0ad8885302 100644 --- a/test/helpers/verification.spec.ts +++ b/test/helpers/verification.spec.ts @@ -1,11 +1,12 @@ +import type { NetlifyPluginOptions, NetlifyPluginUtils } from '@netlify/build' import Chance from 'chance' +import { outdent } from 'outdent' + import { checkNextSiteHasBuilt, checkZipSize, getProblematicUserRewrites, } from '../../packages/runtime/src/helpers/verification' -import { outdent } from 'outdent' -import type { NetlifyPluginOptions } from '@netlify/build' import { describeCwdTmpDir, moveNextDist } from '../test-utils' const netlifyConfig = { @@ -15,17 +16,16 @@ const netlifyConfig = { headers: [], } as NetlifyPluginOptions['netlifyConfig'] -import type { NetlifyPluginUtils } from '@netlify/build' type FailBuild = NetlifyPluginUtils['build']['failBuild'] const chance = new Chance() -jest.mock('fs', () => { - return { - ...jest.requireActual('fs'), - existsSync: jest.fn(), - } -}) +const { existsSync } = require('fs') + +jest.mock('fs', () => ({ + ...jest.requireActual('fs'), + existsSync: jest.fn(), +})) // disable chalk colors to easier validate console text output jest.mock(`chalk`, () => { @@ -35,7 +35,6 @@ jest.mock(`chalk`, () => { describe('checkNextSiteHasBuilt', () => { let failBuildMock - const { existsSync } = require('fs') beforeEach(() => { failBuildMock = jest.fn() as unknown as FailBuild @@ -50,8 +49,8 @@ describe('checkNextSiteHasBuilt', () => { existsSync.mockReturnValue(true) const expectedFailureMessage = outdent` - Detected that "next export" was run, but site is incorrectly publishing the ".next" directory. - The publish directory should be set to "out", and you should set the environment variable NETLIFY_NEXT_PLUGIN_SKIP to "true". + Detected that "next export" was run, but site is incorrectly publishing the ".next" directory. + The publish directory should be set to "out", and you should set the environment variable NETLIFY_NEXT_PLUGIN_SKIP to "true". ` checkNextSiteHasBuilt({ publish: '.next', failBuild: failBuildMock }) @@ -65,11 +64,11 @@ describe('checkNextSiteHasBuilt', () => { existsSync.mockReturnValueOnce(false).mockReturnValueOnce(true) const expectedFailureMessage = outdent` - The directory "someCustomDir" does not contain a Next.js production build. Perhaps the build command was not run, or you specified the wrong publish directory. - However, a '.next' directory was found with a production build. - Consider changing your 'publish' directory to '.next' - If you are using "next export" then you should set the environment variable NETLIFY_NEXT_PLUGIN_SKIP to "true". - ` + The directory "someCustomDir" does not contain a Next.js production build. Perhaps the build command was not run, or you specified the wrong publish directory. + However, a '.next' directory was found with a production build. + Consider changing your 'publish' directory to '.next' + If you are using "next export" then you should set the environment variable NETLIFY_NEXT_PLUGIN_SKIP to "true". + ` checkNextSiteHasBuilt({ publish: 'someCustomDir', failBuild: failBuildMock }) @@ -80,10 +79,10 @@ describe('checkNextSiteHasBuilt', () => { existsSync.mockReturnValue(false) const expectedFailureMessage = outdent` - The directory "out" does not contain a Next.js production build. Perhaps the build command was not run, or you specified the wrong publish directory. - Your publish directory is set to "out", but in most cases it should be ".next". - If you are using "next export" then you should set the environment variable NETLIFY_NEXT_PLUGIN_SKIP to "true". - ` + The directory "out" does not contain a Next.js production build. Perhaps the build command was not run, or you specified the wrong publish directory. + Your publish directory is set to "out", but in most cases it should be ".next". + If you are using "next export" then you should set the environment variable NETLIFY_NEXT_PLUGIN_SKIP to "true". + ` checkNextSiteHasBuilt({ publish: 'out', failBuild: failBuildMock }) expect(failBuildMock).toHaveBeenCalledWith(expectedFailureMessage) @@ -92,10 +91,10 @@ describe('checkNextSiteHasBuilt', () => { it('returns default error message when production build was not found', () => { existsSync.mockReturnValue(false) const expectedFailureMessage = outdent` - The directory ".next" does not contain a Next.js production build. Perhaps the build command was not run, or you specified the wrong publish directory. - In most cases it should be set to ".next", unless you have chosen a custom "distDir" in your Next config. - If you are using "next export" then you should set the environment variable NETLIFY_NEXT_PLUGIN_SKIP to "true". - ` + The directory ".next" does not contain a Next.js production build. Perhaps the build command was not run, or you specified the wrong publish directory. + In most cases it should be set to ".next", unless you have chosen a custom "distDir" in your Next config. + If you are using "next export" then you should set the environment variable NETLIFY_NEXT_PLUGIN_SKIP to "true". + ` checkNextSiteHasBuilt({ publish: '.next', failBuild: failBuildMock }) expect(failBuildMock).toHaveBeenCalledWith(expectedFailureMessage) @@ -147,7 +146,7 @@ describe('checkZipSize', () => { try { await checkZipSize('some-file.zip') - } catch (e) { + } catch { // StreamZip is not mocked, so ultimately the call will throw an error, // but we are logging message before that so we can assert it } diff --git a/test/index.spec.ts b/test/index.spec.ts index 376de21c69..1e9e5279f6 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -1,12 +1,27 @@ -import { relative } from 'pathe' +import os from 'os' +import path, { resolve } from 'path' +import process from 'process' + +import type { NetlifyPluginOptions } from '@netlify/build' +import Chance from 'chance' +import { writeJSON, unlink, existsSync, readFileSync, ensureDir, readJson, pathExists, writeFile, move } from 'fs-extra' +import { join, relative } from 'pathe' +import { dir as getTmpDir } from 'tmp-promise' + +// @ts-expect-error - TODO: Convert runtime export to ES6 +// eslint-disable-next-line import/default +import nextRuntimeFactory from '../packages/runtime/src' +import { HANDLER_FUNCTION_NAME, ODB_FUNCTION_NAME, IMAGE_FUNCTION_NAME } from '../packages/runtime/src/constants' +import { watchForMiddlewareChanges } from '../packages/runtime/src/helpers/compiler' +import { getRequiredServerFiles, updateRequiredServerFiles } from '../packages/runtime/src/helpers/config' import { getAllPageDependencies } from '../packages/runtime/src/templates/getPageResolver' -jest.mock('../packages/runtime/src/helpers/utils', () => { - return { - ...jest.requireActual('../packages/runtime/src/helpers/utils'), - isNextAuthInstalled: jest.fn(), - } -}) +import { changeCwd, useFixture, moveNextDist } from './test-utils' + +jest.mock('../packages/runtime/src/helpers/utils', () => ({ + ...jest.requireActual('../packages/runtime/src/helpers/utils'), + isNextAuthInstalled: jest.fn(), +})) jest.mock('../packages/runtime/src/helpers/functionsMetaData', () => { const { NEXT_PLUGIN_NAME } = require('../packages/runtime/src/constants') @@ -15,62 +30,39 @@ jest.mock('../packages/runtime/src/helpers/functionsMetaData', () => { getPluginVersion: async () => `${NEXT_PLUGIN_NAME}@1.0.0`, } }) - -import Chance from "chance" -import { - writeJSON, - unlink, - existsSync, - readFileSync, - ensureDir, - readJson, - pathExists, - writeFile, - move, -} from "fs-extra" -import path from "path" -import process from "process" -import os from "os" -import { dir as getTmpDir } from "tmp-promise" -// @ts-expect-error - TODO: Convert runtime export to ES6 -import nextRuntimeFactory from "../packages/runtime/src" const nextRuntime = nextRuntimeFactory({}) -import { watchForMiddlewareChanges } from "../packages/runtime/src/helpers/compiler" -import { HANDLER_FUNCTION_NAME, ODB_FUNCTION_NAME, IMAGE_FUNCTION_NAME } from "../packages/runtime/src/constants" -import { join } from "pathe" -import { - getRequiredServerFiles, - updateRequiredServerFiles, -} from "../packages/runtime/src/helpers/config" -import { resolve } from "path" -import type { NetlifyPluginOptions } from '@netlify/build' -import { changeCwd, useFixture, moveNextDist } from "./test-utils" const chance = new Chance() const constants = { INTERNAL_FUNCTIONS_SRC: '.netlify/functions-internal', PUBLISH_DIR: '.next', FUNCTIONS_DIST: '.netlify/functions', -} as unknown as NetlifyPluginOptions["constants"] +} as unknown as NetlifyPluginOptions['constants'] const utils = { build: { failBuild(message) { throw new Error(message) }, }, + // eslint-disable-next-line no-void run: async () => void 0, cache: { save: jest.fn(), restore: jest.fn(), }, -} as unknown as NetlifyPluginOptions["utils"] +} as unknown as NetlifyPluginOptions['utils'] const normalizeChunkNames = (source) => source.replaceAll(/\/chunks\/\d+\.js/g, '/chunks/CHUNK_ID.js') const onBuildHasRun = (netlifyConfig) => Boolean(netlifyConfig.functions[HANDLER_FUNCTION_NAME]?.included_files?.some((file) => file.includes('BUILD_ID'))) -const netlifyConfig = { build: { command: 'npm run build' }, functions: {}, redirects: [], headers: [] } as NetlifyPluginOptions["netlifyConfig"] +const netlifyConfig = { + build: { command: 'npm run build' }, + functions: {}, + redirects: [], + headers: [], +} as NetlifyPluginOptions['netlifyConfig'] const defaultArgs = { netlifyConfig, utils, @@ -110,15 +102,15 @@ afterEach(async () => { }) describe('preBuild()', () => { - test('fails if publishing the root of the project', () => { + it('fails if publishing the root of the project', async () => { defaultArgs.netlifyConfig.build.publish = path.resolve('.') - expect(nextRuntime.onPreBuild(defaultArgs)).rejects.toThrowError( + await expect(nextRuntime.onPreBuild(defaultArgs)).rejects.toThrow( /Your publish directory is pointing to the base directory of your site/, ) }) - test('fails if the build version is too old', () => { - expect( + it('fails if the build version is too old', async () => { + await expect( nextRuntime.onPreBuild({ ...defaultArgs, constants: { IS_LOCAL: true, NETLIFY_BUILD_VERSION: '18.15.0' }, @@ -126,8 +118,8 @@ describe('preBuild()', () => { ).rejects.toThrow('This version of the Next Runtime requires netlify-cli') }) - test('passes if the build version is new enough', async () => { - expect( + it('passes if the build version is new enough', async () => { + await expect( nextRuntime.onPreBuild({ ...defaultArgs, constants: { IS_LOCAL: true, NETLIFY_BUILD_VERSION: '18.16.1' }, @@ -160,9 +152,7 @@ describe('onBuild()', () => { const { isNextAuthInstalled } = require('../packages/runtime/src/helpers/utils') beforeEach(() => { - isNextAuthInstalled.mockImplementation(() => { - return true - }) + isNextAuthInstalled.mockImplementation(() => true) }) afterEach(() => { @@ -171,7 +161,7 @@ describe('onBuild()', () => { delete process.env.CONTEXT }) - test('does not set NEXTAUTH_URL if value is already set', async () => { + it('does not set NEXTAUTH_URL if value is already set', async () => { const mockUserDefinedSiteUrl = chance.url() process.env.DEPLOY_PRIME_URL = chance.url() @@ -190,7 +180,7 @@ describe('onBuild()', () => { expect(config.config.env.NEXTAUTH_URL).toEqual(mockUserDefinedSiteUrl) }) - test("sets the NEXTAUTH_URL to the DEPLOY_PRIME_URL when CONTEXT env variable is not 'production'", async () => { + it("sets the NEXTAUTH_URL to the DEPLOY_PRIME_URL when CONTEXT env variable is not 'production'", async () => { const mockUserDefinedSiteUrl = chance.url() process.env.DEPLOY_PRIME_URL = mockUserDefinedSiteUrl process.env.URL = chance.url() @@ -213,7 +203,7 @@ describe('onBuild()', () => { expect(config.config.env.NEXTAUTH_URL).toEqual(mockUserDefinedSiteUrl) }) - test("sets the NEXTAUTH_URL to the user defined site URL when CONTEXT env variable is 'production'", async () => { + it("sets the NEXTAUTH_URL to the user defined site URL when CONTEXT env variable is 'production'", async () => { const mockUserDefinedSiteUrl = chance.url() process.env.DEPLOY_PRIME_URL = chance.url() process.env.URL = mockUserDefinedSiteUrl @@ -236,7 +226,7 @@ describe('onBuild()', () => { expect(config.config.env.NEXTAUTH_URL).toEqual(mockUserDefinedSiteUrl) }) - test('sets the NEXTAUTH_URL specified in the netlify.toml or in the Netlify UI', async () => { + it('sets the NEXTAUTH_URL specified in the netlify.toml or in the Netlify UI', async () => { const mockSiteUrl = chance.url() process.env.NEXTAUTH_URL = mockSiteUrl @@ -251,7 +241,7 @@ describe('onBuild()', () => { delete process.env.NEXTAUTH_URL }) - test('sets NEXTAUTH_URL when next-auth package is detected', async () => { + it('sets NEXTAUTH_URL when next-auth package is detected', async () => { const mockSiteUrl = chance.url() // Value represents the main address to the site and is either @@ -269,7 +259,7 @@ describe('onBuild()', () => { expect(config.config.env.NEXTAUTH_URL).toEqual(mockSiteUrl) }) - test('includes the basePath on NEXTAUTH_URL when present', async () => { + it('includes the basePath on NEXTAUTH_URL when present', async () => { const mockSiteUrl = chance.url() process.env.DEPLOY_PRIME_URL = mockSiteUrl @@ -287,10 +277,8 @@ describe('onBuild()', () => { expect(config.config.env.NEXTAUTH_URL).toEqual(`${mockSiteUrl}/foo`) }) - test('skips setting NEXTAUTH_URL when next-auth package is not found', async () => { - isNextAuthInstalled.mockImplementation(() => { - return false - }) + it('skips setting NEXTAUTH_URL when next-auth package is not found', async () => { + isNextAuthInstalled.mockImplementation(() => false) await moveNextDist() await nextRuntime.onBuild(defaultArgs) @@ -301,7 +289,7 @@ describe('onBuild()', () => { expect(config.config.env.NEXTAUTH_URL).toBeUndefined() }) - test('runs onBuild', async () => { + it('runs onBuild', async () => { await moveNextDist() await nextRuntime.onBuild(defaultArgs) @@ -309,7 +297,7 @@ describe('onBuild()', () => { expect(onBuildHasRun(netlifyConfig)).toBe(true) }) - test('skips if NETLIFY_NEXT_PLUGIN_SKIP is set', async () => { + it('skips if NETLIFY_NEXT_PLUGIN_SKIP is set', async () => { process.env.NETLIFY_NEXT_PLUGIN_SKIP = 'true' await moveNextDist() await nextRuntime.onBuild(defaultArgs) @@ -318,7 +306,7 @@ describe('onBuild()', () => { delete process.env.NETLIFY_NEXT_PLUGIN_SKIP }) - test('skips if NEXT_PLUGIN_FORCE_RUN is "false"', async () => { + it('skips if NEXT_PLUGIN_FORCE_RUN is "false"', async () => { process.env.NEXT_PLUGIN_FORCE_RUN = 'false' await moveNextDist() await nextRuntime.onBuild(defaultArgs) @@ -327,19 +315,21 @@ describe('onBuild()', () => { delete process.env.NEXT_PLUGIN_FORCE_RUN }) - test("fails if BUILD_ID doesn't exist", async () => { + it("fails if BUILD_ID doesn't exist", async () => { await moveNextDist() await unlink(path.join(process.cwd(), '.next/BUILD_ID')) const failBuild = jest.fn().mockImplementation((err) => { throw new Error(err) }) - expect(() => nextRuntime.onBuild({ ...defaultArgs, utils: { ...utils, build: { failBuild } } })).rejects.toThrow( + await expect(() => + nextRuntime.onBuild({ ...defaultArgs, utils: { ...utils, build: { failBuild } } }), + ).rejects.toThrow( `In most cases it should be set to ".next", unless you have chosen a custom "distDir" in your Next config.`, ) expect(failBuild).toHaveBeenCalled() }) - test("fails with helpful warning if BUILD_ID doesn't exist and publish is 'out'", async () => { + it("fails with helpful warning if BUILD_ID doesn't exist and publish is 'out'", async () => { await moveNextDist() await unlink(path.join(process.cwd(), '.next/BUILD_ID')) const failBuild = jest.fn().mockImplementation((err) => { @@ -347,13 +337,13 @@ describe('onBuild()', () => { }) netlifyConfig.build.publish = path.resolve('out') - expect(() => nextRuntime.onBuild({ ...defaultArgs, utils: { ...utils, build: { failBuild } } })).rejects.toThrow( - `Your publish directory is set to "out", but in most cases it should be ".next".`, - ) + await expect(() => + nextRuntime.onBuild({ ...defaultArgs, utils: { ...utils, build: { failBuild } } }), + ).rejects.toThrow(`Your publish directory is set to "out", but in most cases it should be ".next".`) expect(failBuild).toHaveBeenCalled() }) - test('fails build if next export has run', async () => { + it('fails build if next export has run', async () => { await moveNextDist() await writeJSON(path.join(process.cwd(), '.next/export-detail.json'), {}) const failBuild = jest.fn() @@ -361,7 +351,7 @@ describe('onBuild()', () => { expect(failBuild).toHaveBeenCalled() }) - test('copy handlers to the internal functions directory', async () => { + it('copy handlers to the internal functions directory', async () => { await moveNextDist() await nextRuntime.onBuild(defaultArgs) @@ -374,7 +364,7 @@ describe('onBuild()', () => { expect(existsSync(`.netlify/functions-internal/___netlify-odb-handler/handlerUtils.js`)).toBeTruthy() }) - test('writes correct redirects to netlifyConfig', async () => { + it('writes correct redirects to netlifyConfig', async () => { await moveNextDist() await nextRuntime.onBuild(defaultArgs) @@ -384,14 +374,14 @@ describe('onBuild()', () => { expect(sorted).toMatchSnapshot() }) - test('publish dir is/has next dist', async () => { + it('publish dir is/has next dist', async () => { await moveNextDist() await nextRuntime.onBuild(defaultArgs) expect(existsSync(path.resolve('.next/BUILD_ID'))).toBeTruthy() }) - test('generates static files manifest', async () => { + it('generates static files manifest', async () => { await moveNextDist() await nextRuntime.onBuild(defaultArgs) const manifestPath = path.resolve('.next/static-manifest.json') @@ -400,7 +390,7 @@ describe('onBuild()', () => { expect(data).toMatchSnapshot() }) - test('moves static files to root', async () => { + it('moves static files to root', async () => { await moveNextDist() await nextRuntime.onBuild(defaultArgs) const data = JSON.parse(readFileSync(path.resolve('.next/static-manifest.json'), 'utf8')) @@ -410,7 +400,7 @@ describe('onBuild()', () => { }) }) - test('copies default locale files to top level', async () => { + it('copies default locale files to top level', async () => { await moveNextDist() await nextRuntime.onBuild(defaultArgs) const data = JSON.parse(readFileSync(path.resolve('.next/static-manifest.json'), 'utf8')) @@ -421,13 +411,13 @@ describe('onBuild()', () => { if (!file.startsWith(locale)) { return } - const trimmed = file.substring(locale.length) + const trimmed = file.slice(locale.length) expect(existsSync(path.resolve(path.join('.next', trimmed)))).toBeTruthy() }) }) // TODO - TO BE MOVED TO TEST AGAINST A PROJECT WITH MIDDLEWARE IN ANOTHER PR - test.skip('skips static files that match middleware', async () => { + it.skip('skips static files that match middleware', async () => { await moveNextDist() await nextRuntime.onBuild(defaultArgs) @@ -435,7 +425,7 @@ describe('onBuild()', () => { expect(existsSync(path.resolve(path.join('.next', 'server', 'pages', 'en', 'middle.html')))).toBeTruthy() }) - test('sets correct config', async () => { + it('sets correct config', async () => { await moveNextDist() await nextRuntime.onBuild(defaultArgs) @@ -518,7 +508,7 @@ describe('onBuild()', () => { } }) - test('generates a file referencing all page sources', async () => { + it('generates a file referencing all page sources', async () => { await moveNextDist() await nextRuntime.onBuild(defaultArgs) const handlerPagesFile = path.join(constants.INTERNAL_FUNCTIONS_SRC, HANDLER_FUNCTION_NAME, 'pages.js') @@ -530,7 +520,7 @@ describe('onBuild()', () => { expect(normalizeChunkNames(readFileSync(odbHandlerPagesFile, 'utf8'))).toMatchSnapshot() }) - test('generates a file referencing all when publish dir is a subdirectory', async () => { + it('generates a file referencing all when publish dir is a subdirectory', async () => { const dir = 'web/.next' await moveNextDist(dir) netlifyConfig.build.publish = path.resolve(dir) @@ -547,7 +537,7 @@ describe('onBuild()', () => { expect(normalizeChunkNames(readFileSync(odbHandlerPagesFile, 'utf8'))).toMatchSnapshot() }) - test('generates entrypoints with correct references', async () => { + it('generates entrypoints with correct references', async () => { await moveNextDist() await nextRuntime.onBuild(defaultArgs) @@ -566,7 +556,7 @@ describe('onBuild()', () => { expect(readFileSync(odbHandlerFile, 'utf8')).toMatch(`require("../../../.next/required-server-files.json")`) }) - test('handles empty routesManifest.staticRoutes', async () => { + it('handles empty routesManifest.staticRoutes', async () => { await moveNextDist() const manifestPath = path.resolve('.next/routes-manifest.json') const routesManifest = await readJson(manifestPath) @@ -576,7 +566,7 @@ describe('onBuild()', () => { expect(await nextRuntime.onBuild(defaultArgs)).toBeUndefined() }) - test('generates imageconfig file with entries for domains, remotePatterns, and custom response headers', async () => { + it('generates imageconfig file with entries for domains, remotePatterns, and custom response headers', async () => { await moveNextDist() const mockHeaderValue = chance.string() @@ -606,65 +596,64 @@ describe('onBuild()', () => { }) }) - test('generates an ipx function by default', async () => { + it('generates an ipx function by default', async () => { await moveNextDist() await nextRuntime.onBuild(defaultArgs) expect(existsSync(path.join('.netlify', 'functions-internal', '_ipx', '_ipx.js'))).toBeTruthy() }) // Enabled while edge images are off by default - test('does not generate an ipx edge function by default', async () => { + it('does not generate an ipx edge function by default', async () => { await moveNextDist() await nextRuntime.onBuild(defaultArgs) expect(existsSync(path.join('.netlify', 'edge-functions', 'ipx', 'index.ts'))).toBeFalsy() }) - test('generates an ipx edge function if force is set', async () => { + it('generates an ipx edge function if force is set', async () => { process.env.NEXT_FORCE_EDGE_IMAGES = '1' await moveNextDist() await nextRuntime.onBuild(defaultArgs) expect(existsSync(path.join('.netlify', 'edge-functions', 'ipx', 'index.ts'))).toBeTruthy() }) - test('generates edge-functions manifest', async () => { + it('generates edge-functions manifest', async () => { await moveNextDist() await nextRuntime.onBuild(defaultArgs) expect(existsSync(path.join('.netlify', 'edge-functions', 'manifest.json'))).toBeTruthy() }) - test('generates generator field within the edge-functions manifest', async () => { + it('generates generator field within the edge-functions manifest', async () => { await moveNextDist() await nextRuntime.onBuild(defaultArgs) const manifestPath = await readJson(path.resolve('.netlify/edge-functions/manifest.json')) const manifest = manifestPath.functions - + expect(manifest).toEqual( expect.arrayContaining([ expect.objectContaining({ - generator: '@netlify/next-runtime@1.0.0' - }) - ]) + generator: '@netlify/next-runtime@1.0.0', + }), + ]), ) }) - - test('generates generator field within the edge-functions manifest includes IPX', async () => { + it('generates generator field within the edge-functions manifest includes IPX', async () => { process.env.NEXT_FORCE_EDGE_IMAGES = '1' await moveNextDist() await nextRuntime.onBuild(defaultArgs) const manifestPath = await readJson(path.resolve('.netlify/edge-functions/manifest.json')) const manifest = manifestPath.functions - + expect(manifest).toEqual( expect.arrayContaining([ expect.objectContaining({ - generator: '@netlify/next-runtime@1.0.0' - }) - ]) + generator: '@netlify/next-runtime@1.0.0', + }), + ]), ) }) - test('does not generate an ipx function when DISABLE_IPX is set', async () => { + it('does not generate an ipx function when DISABLE_IPX is set', async () => { process.env.DISABLE_IPX = '1' await moveNextDist() await nextRuntime.onBuild(defaultArgs) @@ -672,7 +661,7 @@ describe('onBuild()', () => { delete process.env.DISABLE_IPX }) - test('creates 404 redirect when DISABLE_IPX is set', async () => { + it('creates 404 redirect when DISABLE_IPX is set', async () => { process.env.DISABLE_IPX = '1' await moveNextDist() await nextRuntime.onBuild(defaultArgs) @@ -686,13 +675,13 @@ describe('onBuild()', () => { delete process.env.DISABLE_IPX }) - test('generates an ipx edge function by default', async () => { + it('generates an ipx edge function by default', async () => { await moveNextDist() await nextRuntime.onBuild(defaultArgs) expect(existsSync(path.join('.netlify', 'edge-functions', 'ipx', 'index.ts'))).toBeTruthy() }) - test('does not generate an ipx edge function if the feature is disabled', async () => { + it('does not generate an ipx edge function if the feature is disabled', async () => { process.env.NEXT_DISABLE_EDGE_IMAGES = '1' await moveNextDist() await nextRuntime.onBuild(defaultArgs) @@ -700,7 +689,7 @@ describe('onBuild()', () => { delete process.env.NEXT_DISABLE_EDGE_IMAGES }) - test('does not generate an ipx edge function if Netlify Edge is disabled', async () => { + it('does not generate an ipx edge function if Netlify Edge is disabled', async () => { process.env.NEXT_DISABLE_NETLIFY_EDGE = '1' await moveNextDist() @@ -717,7 +706,7 @@ describe('onBuild()', () => { delete process.env.NEXT_DISABLE_NETLIFY_EDGE }) - test('moves static files to a subdirectory if basePath is set', async () => { + it('moves static files to a subdirectory if basePath is set', async () => { await moveNextDist() const initialConfig = await getRequiredServerFiles(netlifyConfig.build.publish) @@ -736,7 +725,7 @@ describe('onBuild()', () => { }) describe('onPostBuild', () => { - test('saves cache with right paths', async () => { + it('saves cache with right paths', async () => { await moveNextDist() const save = jest.fn() @@ -749,7 +738,7 @@ describe('onPostBuild', () => { expect(save).toHaveBeenCalledWith(path.resolve('.next/cache')) }) - test('warns if old functions exist', async () => { + it('warns if old functions exist', async () => { await moveNextDist() const list = jest.fn().mockResolvedValue([ @@ -785,7 +774,7 @@ describe('onPostBuild', () => { console.log = oldLog }) - test('warns if NETLIFY_NEXT_PLUGIN_SKIP is set', async () => { + it('warns if NETLIFY_NEXT_PLUGIN_SKIP is set', async () => { await moveNextDist() process.env.NETLIFY_NEXT_PLUGIN_SKIP = 'true' @@ -799,7 +788,7 @@ describe('onPostBuild', () => { delete process.env.NETLIFY_NEXT_PLUGIN_SKIP }) - test('warns if NEXT_PLUGIN_FORCE_RUN is "false"', async () => { + it('warns if NEXT_PLUGIN_FORCE_RUN is "false"', async () => { await moveNextDist() process.env.NEXT_PLUGIN_FORCE_RUN = 'false' @@ -814,7 +803,7 @@ describe('onPostBuild', () => { delete process.env.NEXT_PLUGIN_FORCE_RUN }) - test('adds headers to Netlify configuration', async () => { + it('adds headers to Netlify configuration', async () => { await moveNextDist() const show = jest.fn() @@ -901,7 +890,7 @@ describe('onPostBuild', () => { ]) }) - test('appends headers to existing headers in the Netlify configuration', async () => { + it('appends headers to existing headers in the Netlify configuration', async () => { await moveNextDist() netlifyConfig.headers = [ @@ -1003,7 +992,7 @@ describe('onPostBuild', () => { ]) }) - test('appends no additional headers in the Netlify configuration when none are in the routes manifest', async () => { + it('appends no additional headers in the Netlify configuration when none are in the routes manifest', async () => { await moveNextDist() netlifyConfig.headers = [ @@ -1241,7 +1230,7 @@ describe('the dev middleware watcher', () => { await isReady expect(middlewareExists()).toBeFalsy() await writeFile(path.join(process.cwd(), 'middleware.ts'), middlewareSourceTs) - let isBuilt = nextBuild() + const isBuilt = nextBuild() await writeFile(path.join(process.cwd(), 'middleware.js'), middlewareSourceJs) await isBuilt expect(middlewareExists()).toBeFalsy() diff --git a/test/templates/handlerUtils.spec.ts b/test/templates/handlerUtils.spec.ts index 795c48dd36..26516ba2da 100644 --- a/test/templates/handlerUtils.spec.ts +++ b/test/templates/handlerUtils.spec.ts @@ -1,3 +1,9 @@ +import os from 'os' +import path from 'path' + +import { unlink, existsSync, readFileSync, ensureDir } from 'fs-extra' +import { join } from 'pathe' + import { normalizeRoute, unlocalizeRoute, @@ -5,15 +11,6 @@ import { localizeDataRoute, downloadFile, } from '../../packages/runtime/src/templates/handlerUtils' -import { join } from "pathe" -import os from "os" -import path from "path" -import { - unlink, - existsSync, - readFileSync, - ensureDir, -} from "fs-extra" describe('normalizeRoute', () => { it('removes a trailing slash from a route', () => { @@ -124,7 +121,7 @@ describe('downloadFile', () => { const url = 'https://example.com/nonexistentfile' const tmpFile = join(os.tmpdir(), 'next-test', 'downloadfile.txt') await ensureDir(path.dirname(tmpFile)) - await expect(downloadFile(url, tmpFile)).rejects.toThrowError( + await expect(downloadFile(url, tmpFile)).rejects.toThrow( 'Failed to download https://example.com/nonexistentfile: 404 Not Found', ) }) diff --git a/test/templates/server.spec.ts b/test/templates/server.spec.ts index a8870761fd..0ee660df8e 100644 --- a/test/templates/server.spec.ts +++ b/test/templates/server.spec.ts @@ -82,7 +82,10 @@ describe('the netlify next server', () => { const netlifyNextServer = new NetlifyNextServer({ conf: {} }, { ...mockTokenConfig }) const requestHandler = netlifyNextServer.getRequestHandler() - const { req: mockReq, res: mockRes } = createRequestResponseMocks({ url: '/non-i18n/with-revalidate/', headers: { 'x-prerender-revalidate': 'test' }}) + const { req: mockReq, res: mockRes } = createRequestResponseMocks({ + url: '/non-i18n/with-revalidate/', + headers: { 'x-prerender-revalidate': 'test' }, + }) // @ts-expect-error - Types are incorrect for `MockedResponse` await requestHandler(mockReq, mockRes) @@ -99,7 +102,10 @@ describe('the netlify next server', () => { const netlifyNextServer = new NetlifyNextServer({ conf: { ...mocki18nConfig } }, { ...mockTokenConfig }) const requestHandler = netlifyNextServer.getRequestHandler() - const { req: mockReq, res: mockRes } = createRequestResponseMocks({ url: '/i18n/with-revalidate/', headers: { 'x-prerender-revalidate': 'test' }}) + const { req: mockReq, res: mockRes } = createRequestResponseMocks({ + url: '/i18n/with-revalidate/', + headers: { 'x-prerender-revalidate': 'test' }, + }) // @ts-expect-error - Types are incorrect for `MockedResponse` await requestHandler(mockReq, mockRes) @@ -116,7 +122,10 @@ describe('the netlify next server', () => { const netlifyNextServer = new NetlifyNextServer({ conf: {} }, { ...mockTokenConfig }) const requestHandler = netlifyNextServer.getRequestHandler() - const { req: mockReq, res: mockRes } = createRequestResponseMocks({ url: '/blog/rob/hello', headers: { 'x-prerender-revalidate': 'test' }}) + const { req: mockReq, res: mockRes } = createRequestResponseMocks({ + url: '/blog/rob/hello', + headers: { 'x-prerender-revalidate': 'test' }, + }) // @ts-expect-error - Types are incorrect for `MockedResponse` await requestHandler(mockReq, mockRes) @@ -133,7 +142,10 @@ describe('the netlify next server', () => { const netlifyNextServer = new NetlifyNextServer({ conf: { ...mocki18nConfig } }, { ...mockTokenConfig }) const requestHandler = netlifyNextServer.getRequestHandler() - const { req: mockReq, res: mockRes } = createRequestResponseMocks({ url: '/fr/posts/hello', headers: { 'x-prerender-revalidate': 'test' }}) + const { req: mockReq, res: mockRes } = createRequestResponseMocks({ + url: '/fr/posts/hello', + headers: { 'x-prerender-revalidate': 'test' }, + }) // @ts-expect-error - Types are incorrect for `MockedResponse` await requestHandler(mockReq, mockRes) @@ -150,7 +162,10 @@ describe('the netlify next server', () => { const netlifyNextServer = new NetlifyNextServer({ conf: {} }, mockTokenConfig) const requestHandler = netlifyNextServer.getRequestHandler() - const { req: mockReq, res: mockRes } = createRequestResponseMocks({ url: '/not-a-valid-path/', headers: { 'x-prerender-revalidate': 'test' }}) + const { req: mockReq, res: mockRes } = createRequestResponseMocks({ + url: '/not-a-valid-path/', + headers: { 'x-prerender-revalidate': 'test' }, + }) // @ts-expect-error - Types are incorrect for `MockedResponse` await expect(requestHandler(mockReq, mockRes)).rejects.toThrow('not an ISR route') @@ -160,7 +175,10 @@ describe('the netlify next server', () => { const netlifyNextServer = new NetlifyNextServer({ conf: {} }, mockTokenConfig) const requestHandler = netlifyNextServer.getRequestHandler() - const { req: mockReq, res: mockRes } = createRequestResponseMocks({ url: '/posts/hello/', headers: { 'x-prerender-revalidate': 'test' }}) + const { req: mockReq, res: mockRes } = createRequestResponseMocks({ + url: '/posts/hello/', + headers: { 'x-prerender-revalidate': 'test' }, + }) mockedApiFetch.mockResolvedValueOnce({ code: 500, message: 'Failed to revalidate' }) // @ts-expect-error - Types are incorrect for `MockedResponse` @@ -171,7 +189,10 @@ describe('the netlify next server', () => { const netlifyNextServer = new NetlifyNextServer({ conf: {} }, mockTokenConfig) const requestHandler = netlifyNextServer.getRequestHandler() - const { req: mockReq, res: mockRes } = createRequestResponseMocks({ url: '/posts/hello', headers: { 'x-prerender-revalidate': 'test' }}) + const { req: mockReq, res: mockRes } = createRequestResponseMocks({ + url: '/posts/hello', + headers: { 'x-prerender-revalidate': 'test' }, + }) mockedApiFetch.mockRejectedValueOnce(new Error('Unable to connect')) // @ts-expect-error - Types are incorrect for `MockedResponse` diff --git a/test/test-utils.ts b/test/test-utils.ts index 0d5ac9f23b..76313c1a69 100644 --- a/test/test-utils.ts +++ b/test/test-utils.ts @@ -1,14 +1,8 @@ -import path from "path" -import { dirname } from "path" -import cpy from "cpy" -import { - writeJSON, - existsSync, - ensureDir, - readJson, - copy, -} from "fs-extra" -import { dir as getTmpDir } from "tmp-promise" +import path, { dirname } from 'path' + +import cpy from 'cpy' +import { writeJSON, existsSync, ensureDir, readJson, copy } from 'fs-extra' +import { dir as getTmpDir } from 'tmp-promise' const FIXTURES_DIR = `${__dirname}/fixtures` const SAMPLE_PROJECT_DIR = `${__dirname}/../demos/default` @@ -32,11 +26,7 @@ const rewriteAppDir = async function (dir = '.next') { // Move .next from sample project to current directory export const moveNextDist = async function (dir = '.next', copyMods = false) { - if (copyMods) { - await copyModules(['next', 'sharp']) - } else { - await stubModules(['next', 'sharp']) - } + await (copyMods ? copyModules(['next', 'sharp']) : stubModules(['next', 'sharp'])) await ensureDir(dirname(dir)) await copy(path.join(SAMPLE_PROJECT_DIR, '.next'), path.join(process.cwd(), dir)) @@ -74,6 +64,7 @@ export const useFixture = async function (fixtureName) { // Change current cwd() to a temporary directory export const describeCwdTmpDir = (name: string, fn: () => void): void => { + // eslint-disable-next-line jest/valid-title describe(name, () => { let restoreCwd let cleanup @@ -91,4 +82,4 @@ export const describeCwdTmpDir = (name: string, fn: () => void): void => { fn() }) -} \ No newline at end of file +}