diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c42269606f..08cadb3319 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,6 +7,10 @@ on: schedule: - cron: '0 0 * * *' +concurrency: + group: ${{ github.head_ref }} + cancel-in-progress: true + jobs: build: name: Unit tests diff --git a/package-lock.json b/package-lock.json index 0317852cc9..b0038705b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6043,9 +6043,9 @@ "devOptional": true }, "node_modules/@types/react": { - "version": "18.0.28", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.28.tgz", - "integrity": "sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew==", + "version": "18.0.26", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.26.tgz", + "integrity": "sha512-hCR3PJQsAIXyxhTNSiDFY//LhnMZWpNNr5etoCqx/iUfGc5gXWtQR2Phl908jVR6uPXacojQWTg4qRpkxTuGug==", "devOptional": true, "dependencies": { "@types/prop-types": "*", @@ -21278,9 +21278,9 @@ "dev": true }, "node_modules/sass": { - "version": "1.56.2", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.56.2.tgz", - "integrity": "sha512-ciEJhnyCRwzlBCB+h5cCPM6ie/6f8HrhZMQOf5vlU60Y1bI1rx5Zb0vlDZvaycHsg/MqFfF1Eq2eokAa32iw8w==", + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.56.1.tgz", + "integrity": "sha512-VpEyKpyBPCxE7qGDtOcdJ6fFbcpOM+Emu7uZLxVrkX8KVU/Dp5UF7WLvzqRuUhB6mqqQt1xffLoG+AndxTZrCQ==", "devOptional": true, "dependencies": { "chokidar": ">=3.0.0 <4.0.0", @@ -24212,6 +24212,7 @@ "@netlify/ipx": "^1.3.3", "@vercel/node-bridge": "^2.1.0", "chalk": "^4.1.2", + "chokidar": "^3.5.3", "destr": "^1.1.1", "execa": "^5.1.1", "follow-redirects": "^1.15.2", @@ -27517,6 +27518,7 @@ "@types/node": "^17.0.25", "@vercel/node-bridge": "^2.1.0", "chalk": "^4.1.2", + "chokidar": "^3.5.3", "destr": "^1.1.1", "execa": "^5.1.1", "follow-redirects": "^1.15.2", @@ -28408,9 +28410,9 @@ "devOptional": true }, "@types/react": { - "version": "18.0.28", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.28.tgz", - "integrity": "sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew==", + "version": "18.0.26", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.26.tgz", + "integrity": "sha512-hCR3PJQsAIXyxhTNSiDFY//LhnMZWpNNr5etoCqx/iUfGc5gXWtQR2Phl908jVR6uPXacojQWTg4qRpkxTuGug==", "devOptional": true, "requires": { "@types/prop-types": "*", @@ -39961,9 +39963,9 @@ "dev": true }, "sass": { - "version": "1.56.2", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.56.2.tgz", - "integrity": "sha512-ciEJhnyCRwzlBCB+h5cCPM6ie/6f8HrhZMQOf5vlU60Y1bI1rx5Zb0vlDZvaycHsg/MqFfF1Eq2eokAa32iw8w==", + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.56.1.tgz", + "integrity": "sha512-VpEyKpyBPCxE7qGDtOcdJ6fFbcpOM+Emu7uZLxVrkX8KVU/Dp5UF7WLvzqRuUhB6mqqQt1xffLoG+AndxTZrCQ==", "devOptional": true, "requires": { "chokidar": ">=3.0.0 <4.0.0", diff --git a/package.json b/package.json index 90314ef23e..d71d9b98d3 100644 --- a/package.json +++ b/package.json @@ -109,7 +109,8 @@ "\\.[jt]sx?$": "babel-jest" }, "verbose": true, - "testTimeout": 60000 + "testTimeout": 60000, + "maxWorkers": 1 }, "jest-junit": { "outputDirectory": "reports", diff --git a/packages/runtime/package.json b/packages/runtime/package.json index fec04b4dcc..619b802e77 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -16,6 +16,7 @@ "@netlify/ipx": "^1.3.3", "@vercel/node-bridge": "^2.1.0", "chalk": "^4.1.2", + "chokidar": "^3.5.3", "destr": "^1.1.1", "execa": "^5.1.1", "follow-redirects": "^1.15.2", diff --git a/packages/runtime/src/helpers/compiler.ts b/packages/runtime/src/helpers/compiler.ts new file mode 100644 index 0000000000..abec9812c9 --- /dev/null +++ b/packages/runtime/src/helpers/compiler.ts @@ -0,0 +1,125 @@ +import { promises } from 'fs' +import { join } from 'path' + +import { build } from '@netlify/esbuild' +import { FSWatcher, watch } from 'chokidar' + +// For more information on Next.js middleware, see https://nextjs.org/docs/advanced-features/middleware + +// These are the locations that a middleware file can exist in a Next.js application +// If other possible locations are added by Next.js, they should be added here. +const MIDDLEWARE_FILE_LOCATIONS: Readonly = [ + 'middleware.js', + 'middleware.ts', + 'src/middleware.js', + 'src/middleware.ts', +] + +const toFileList = (watched: Record>) => + Object.entries(watched).flatMap(([dir, files]) => files.map((file) => join(dir, file))) + +/** + * Compile the middleware file using esbuild + */ + +const buildMiddlewareFile = async (entryPoints: Array, base: string) => { + try { + await build({ + entryPoints, + outfile: join(base, '.netlify', 'middleware.js'), + bundle: true, + format: 'esm', + target: 'esnext', + absWorkingDir: base, + }) + } catch (error) { + console.error(error.toString()) + } +} + +/** + * We only compile middleware if there's exactly one file. If there's more than one, we log a warning and don't compile. + */ +const shouldFilesBeCompiled = (watchedFiles: Array, isFirstRun: boolean) => { + if (watchedFiles.length === 0) { + if (!isFirstRun) { + // Only log on subsequent builds, because having it on first build makes it seem like a warning, when it's a normal state + console.log('No middleware found') + } + return false + } + if (watchedFiles.length > 1) { + console.log('Multiple middleware files found:') + console.log(watchedFiles.join('\n')) + console.log('This is not supported.') + return false + } + return true +} + +const updateWatchedFiles = async (watcher: FSWatcher, base: string, isFirstRun = false) => { + try { + // Start by deleting the old file. If we error out, we don't want to leave the old file around + await promises.unlink(join(base, '.netlify', 'middleware.js')) + } catch { + // Ignore, because it's fine if it didn't exist + } + // The list of watched files is an object with the directory as the key and an array of files as the value. + // We need to flatten this into a list of files + const watchedFiles = toFileList(watcher.getWatched()) + if (!shouldFilesBeCompiled(watchedFiles, isFirstRun)) { + watcher.emit('build') + return + } + console.log(`${isFirstRun ? 'Building' : 'Rebuilding'} middleware ${watchedFiles[0]}...`) + await buildMiddlewareFile(watchedFiles, base) + console.log('...done') + watcher.emit('build') +} + +/** + * Watch for changes to the middleware file location. When a change is detected, recompile the middleware file. + * + * @param base The base directory to watch + * @returns a file watcher and a promise that resolves when the initial scan is complete. + */ +export const watchForMiddlewareChanges = (base: string) => { + const watcher = watch(MIDDLEWARE_FILE_LOCATIONS, { + // Try and ensure renames just emit one event + atomic: true, + // Don't emit for every watched file, just once after the scan is done + ignoreInitial: true, + cwd: base, + }) + + watcher + .on('change', (path) => { + console.log(`File ${path} has been changed`) + updateWatchedFiles(watcher, base) + }) + .on('add', (path) => { + console.log(`File ${path} has been added`) + updateWatchedFiles(watcher, base) + }) + .on('unlink', (path) => { + console.log(`File ${path} has been removed`) + updateWatchedFiles(watcher, base) + }) + + return { + watcher, + isReady: new Promise((resolve) => { + watcher.on('ready', async () => { + console.log('Initial scan for middleware file complete. Ready for changes.') + // This only happens on the first scan + await updateWatchedFiles(watcher, base, true) + console.log('Ready') + resolve() + }) + }), + nextBuild: () => + new Promise((resolve) => { + watcher.once('build', resolve) + }), + } +} diff --git a/packages/runtime/src/helpers/dev.ts b/packages/runtime/src/helpers/dev.ts index 14ce9910db..4047f33836 100644 --- a/packages/runtime/src/helpers/dev.ts +++ b/packages/runtime/src/helpers/dev.ts @@ -1,11 +1,7 @@ -import type { Buffer } from 'buffer' import { resolve } from 'path' -import { Transform } from 'stream' -import { OnPreBuild } from '@netlify/build' +import type { OnPreBuild } from '@netlify/build' import execa from 'execa' -import { unlink } from 'fs-extra' -import mergeStream from 'merge-stream' import { writeDevEdgeFunction } from './edge' import { patchNextFiles } from './files' @@ -17,37 +13,13 @@ export const onPreDev: OnPreBuild = async ({ constants, netlifyConfig }) => { // Need to patch the files, because build might not have been run await patchNextFiles(base) - // Clean up old functions - await unlink(resolve('.netlify', 'middleware.js')).catch(() => { - // Ignore if it doesn't exist - }) await writeDevEdgeFunction(constants) - - // Eventually we might want to do this via esbuild's API, but for now the CLI works fine - const common = [`--bundle`, `--outdir=${resolve('.netlify')}`, `--format=esm`, `--target=esnext`, '--watch'] - const opts = { - all: true, - env: { ...process.env, FORCE_COLOR: '1' }, - } - // TypeScript - const tsout = execa(`esbuild`, [...common, resolve(base, 'middleware.ts')], opts).all - - // JavaScript - const jsout = execa(`esbuild`, [...common, resolve(base, 'middleware.js')], opts).all - - const filter = new Transform({ - transform(chunk: Buffer, encoding, callback) { - const str = chunk.toString(encoding) - - // Skip if message includes this, because we run even when the files are missing - if (!str.includes('[ERROR] Could not resolve')) { - this.push(chunk) - } - callback() + // Don't await this or it will never finish + execa.node( + resolve(__dirname, '..', '..', 'lib', 'helpers', 'middlewareWatcher.js'), + [base, process.env.NODE_ENV === 'test' ? '--once' : ''], + { + stdio: 'inherit', }, - }) - - mergeStream(tsout, jsout).pipe(filter).pipe(process.stdout) - - // Don't return the promise because we don't want to wait for the child process to finish + ) } diff --git a/packages/runtime/src/helpers/middlewareWatcher.ts b/packages/runtime/src/helpers/middlewareWatcher.ts new file mode 100644 index 0000000000..90cc3f998e --- /dev/null +++ b/packages/runtime/src/helpers/middlewareWatcher.ts @@ -0,0 +1,13 @@ +import { resolve } from 'path' + +import { watchForMiddlewareChanges } from './compiler' + +const run = async () => { + const { isReady, watcher } = watchForMiddlewareChanges(resolve(process.argv[2])) + await isReady + if (process.argv[3] === '--once') { + watcher.close() + } +} + +run() diff --git a/packages/runtime/src/templates/getPageResolver.ts b/packages/runtime/src/templates/getPageResolver.ts index 1326558c30..5f5c83c3b9 100644 --- a/packages/runtime/src/templates/getPageResolver.ts +++ b/packages/runtime/src/templates/getPageResolver.ts @@ -34,7 +34,7 @@ export const getResolverForDependencies = ({ }) => { const pageFiles = dependencies.map((file) => `require.resolve('${relative(functionDir, file)}')`) return outdent/* javascript */ ` - // This file is purely to allow nft to know about these pages. + // This file is purely to allow nft to know about these pages. exports.resolvePages = () => { try { ${pageFiles.join('\n ')} diff --git a/test/__snapshots__/index.spec.js.snap b/test/__snapshots__/index.spec.js.snap index 31b8ba4181..c45ce6cf80 100644 --- a/test/__snapshots__/index.spec.js.snap +++ b/test/__snapshots__/index.spec.js.snap @@ -58,7 +58,7 @@ Array [ `; exports[`onBuild() generates a file referencing all API route sources: for _api_hello-background-background 1`] = ` -"// This file is purely to allow nft to know about these pages. +"// This file is purely to allow nft to know about these pages. exports.resolvePages = () => { try { require.resolve('../../../.next/package.json') @@ -77,7 +77,7 @@ exports.resolvePages = () => { `; exports[`onBuild() generates a file referencing all API route sources: for _api_hello-scheduled-handler 1`] = ` -"// This file is purely to allow nft to know about these pages. +"// This file is purely to allow nft to know about these pages. exports.resolvePages = () => { try { require.resolve('../../../.next/package.json') @@ -96,7 +96,7 @@ exports.resolvePages = () => { `; exports[`onBuild() generates a file referencing all page sources 1`] = ` -"// This file is purely to allow nft to know about these pages. +"// This file is purely to allow nft to know about these pages. exports.resolvePages = () => { try { require.resolve('../../../.next/package.json') @@ -156,7 +156,7 @@ exports.resolvePages = () => { `; exports[`onBuild() generates a file referencing all page sources 2`] = ` -"// This file is purely to allow nft to know about these pages. +"// This file is purely to allow nft to know about these pages. exports.resolvePages = () => { try { require.resolve('../../../.next/package.json') @@ -216,7 +216,7 @@ exports.resolvePages = () => { `; exports[`onBuild() generates a file referencing all when publish dir is a subdirectory 1`] = ` -"// This file is purely to allow nft to know about these pages. +"// This file is purely to allow nft to know about these pages. exports.resolvePages = () => { try { require.resolve('../../../web/.next/package.json') @@ -276,7 +276,7 @@ exports.resolvePages = () => { `; exports[`onBuild() generates a file referencing all when publish dir is a subdirectory 2`] = ` -"// This file is purely to allow nft to know about these pages. +"// This file is purely to allow nft to know about these pages. exports.resolvePages = () => { try { require.resolve('../../../web/.next/package.json') diff --git a/test/index.spec.js b/test/index.spec.js index ba6438dbd6..06a22330e8 100644 --- a/test/index.spec.js +++ b/test/index.spec.js @@ -1,3 +1,4 @@ +import execa from 'execa' import { relative } from 'pathe' import { getAllPageDependencies } from '../packages/runtime/src/templates/getPageResolver' @@ -9,7 +10,18 @@ jest.mock('../packages/runtime/src/helpers/utils', () => { }) const Chance = require('chance') -const { writeJSON, unlink, existsSync, readFileSync, copy, ensureDir, readJson, pathExists } = require('fs-extra') +const { + writeJSON, + unlink, + existsSync, + readFileSync, + copy, + ensureDir, + readJson, + pathExists, + writeFile, + move, +} = require('fs-extra') const path = require('path') const process = require('process') const os = require('os') @@ -19,7 +31,7 @@ const { downloadFile } = require('../packages/runtime/src/templates/handlerUtils const { getExtendedApiRouteConfigs } = require('../packages/runtime/src/helpers/functions') const nextRuntimeFactory = require('../packages/runtime/src') const nextRuntime = nextRuntimeFactory({}) - +const { watchForMiddlewareChanges } = require('../packages/runtime/src/helpers/compiler') const { HANDLER_FUNCTION_NAME, ODB_FUNCTION_NAME, IMAGE_FUNCTION_NAME } = require('../packages/runtime/src/constants') const { join } = require('pathe') const { @@ -35,7 +47,7 @@ const { updateRequiredServerFiles, generateCustomHeaders, } = require('../packages/runtime/src/helpers/config') -const { dirname } = require('path') +const { dirname, resolve } = require('path') const { getProblematicUserRewrites } = require('../packages/runtime/src/helpers/verification') const chance = new Chance() @@ -121,8 +133,12 @@ const rewriteAppDir = async function (dir = '.next') { } // Move .next from sample project to current directory -export const moveNextDist = async function (dir = '.next') { - await stubModules(['next', 'sharp']) +export const moveNextDist = async function (dir = '.next', copyMods = false) { + if (copyMods) { + await copyModules(['next', 'sharp']) + } else { + await stubModules(['next', 'sharp']) + } await ensureDir(dirname(dir)) await copy(path.join(SAMPLE_PROJECT_DIR, '.next'), path.join(process.cwd(), dir)) @@ -136,6 +152,14 @@ export const moveNextDist = async function (dir = '.next') { await rewriteAppDir(dir) } +const copyModules = async function (modules) { + for (const mod of modules) { + const source = dirname(require.resolve(`${mod}/package.json`)) + const dest = path.join(process.cwd(), 'node_modules', mod) + await copy(source, dest) + } +} + const stubModules = async function (modules) { for (const mod of modules) { const dir = path.join(process.cwd(), 'node_modules', mod) @@ -184,7 +208,9 @@ afterEach(async () => { // Cleans up the temporary directory from `getTmpDir()` and do not make it // the current directory anymore restoreCwd() - await cleanup() + if (!process.env.TEST_SKIP_CLEANUP) { + await cleanup() + } }) describe('preBuild()', () => { @@ -1752,3 +1778,266 @@ describe('api route file analysis', () => { ) }) }) + +const middlewareSourceTs = /* typescript */ ` +import { NextResponse } from 'next/server' +export async function middleware(req: NextRequest) { + return NextResponse.next() +} +` + +const middlewareSourceJs = /* javascript */ ` +import { NextResponse } from 'next/server' +export async function middleware(req) { + return NextResponse.next() +} +` + +const wait = (seconds = 0.5) => new Promise((resolve) => setTimeout(resolve, seconds * 1000)) + +const middlewareExists = () => existsSync(resolve('.netlify', 'middleware.js')) + +describe('onPreDev', () => { + let runtime + beforeAll(async () => { + runtime = await nextRuntimeFactory({}, { events: new Set(['onPreDev']) }) + }) + + it('should generate the runtime with onPreDev', () => { + expect(runtime).toHaveProperty('onPreDev') + }) + + it('should compile middleware', async () => { + await moveNextDist('.next', true) + await writeFile(path.join(process.cwd(), 'middleware.ts'), middlewareSourceTs) + expect(middlewareExists()).toBeFalsy() + + await runtime.onPreDev(defaultArgs) + await wait() + + expect(middlewareExists()).toBeTruthy() + }) +}) + +// skipping for now as the feature works +// but the tests only seem to run successfully when run locally +describe('the dev middleware watcher', () => { + const watchers = [] + + afterEach(async () => { + await Promise.all( + watchers.map((watcher) => { + console.log('closing watcher') + return watcher.close() + }), + ) + watchers.length = 0 + }) + + it('should compile a middleware file and then exit when killed', async () => { + console.log('starting should compile a middleware file and then exit when killed') + await moveNextDist('.next', true) + await writeFile(path.join(process.cwd(), 'middleware.ts'), middlewareSourceTs) + expect(middlewareExists()).toBeFalsy() + const { watcher, isReady } = watchForMiddlewareChanges(process.cwd()) + watchers.push(watcher) + await isReady + expect(middlewareExists()).toBeTruthy() + }) + + it.skip('should compile a file if it is written after the watcher starts', async () => { + console.log('starting should compile a file if it is written after the watcher starts') + await moveNextDist('.next', true) + const { watcher, isReady, nextBuild } = watchForMiddlewareChanges(process.cwd()) + watchers.push(watcher) + await isReady + expect(middlewareExists()).toBeFalsy() + const isBuilt = nextBuild() + await writeFile(path.join(process.cwd(), 'middleware.ts'), middlewareSourceTs) + await isBuilt + expect(middlewareExists()).toBeTruthy() + }) + + it.skip('should remove the output if the middleware is removed after the watcher starts', async () => { + console.log('starting should remove the output if the middleware is removed after the watcher starts') + await moveNextDist('.next', true) + const { watcher, isReady, nextBuild } = watchForMiddlewareChanges(process.cwd()) + watchers.push(watcher) + await isReady + expect(middlewareExists()).toBeFalsy() + let isBuilt = nextBuild() + await writeFile(path.join(process.cwd(), 'middleware.ts'), middlewareSourceTs) + await isBuilt + expect(middlewareExists()).toBeTruthy() + isBuilt = nextBuild() + await unlink(path.join(process.cwd(), 'middleware.ts')) + await isBuilt + expect(middlewareExists()).toBeFalsy() + }) + + it.skip('should remove the output if invalid middleware is written after the watcher starts', async () => { + console.log('starting should remove the output if invalid middleware is written after the watcher starts') + await moveNextDist('.next', true) + const { watcher, isReady, nextBuild } = watchForMiddlewareChanges(process.cwd()) + watchers.push(watcher) + await isReady + expect(middlewareExists()).toBeFalsy() + let isBuilt = nextBuild() + await writeFile(path.join(process.cwd(), 'middleware.ts'), middlewareSourceTs) + await isBuilt + expect(middlewareExists()).toBeTruthy() + isBuilt = nextBuild() + await writeFile(path.join(process.cwd(), 'middleware.ts'), 'this is not valid middleware') + await isBuilt + expect(middlewareExists()).toBeFalsy() + }) + + it.skip('should recompile the middleware if it is moved into the src directory after the watcher starts', async () => { + console.log( + 'starting should recompile the middleware if it is moved into the src directory after the watcher starts', + ) + await moveNextDist('.next', true) + const { watcher, isReady, nextBuild } = watchForMiddlewareChanges(process.cwd()) + watchers.push(watcher) + await isReady + expect(middlewareExists()).toBeFalsy() + let isBuilt = nextBuild() + await writeFile(path.join(process.cwd(), 'middleware.ts'), middlewareSourceTs) + await isBuilt + expect(middlewareExists()).toBeTruthy() + isBuilt = nextBuild() + await move(path.join(process.cwd(), 'middleware.ts'), path.join(process.cwd(), 'src', 'middleware.ts')) + await isBuilt + expect(middlewareExists()).toBeTruthy() + }) + + it.skip('should recompile the middleware if it is moved into the root directory after the watcher starts', async () => { + console.log( + 'starting should recompile the middleware if it is moved into the root directory after the watcher starts', + ) + await moveNextDist('.next', true) + const { watcher, isReady, nextBuild } = watchForMiddlewareChanges(process.cwd()) + watchers.push(watcher) + await isReady + expect(middlewareExists()).toBeFalsy() + let isBuilt = nextBuild() + await ensureDir(path.join(process.cwd(), 'src')) + await writeFile(path.join(process.cwd(), 'src', 'middleware.ts'), middlewareSourceTs) + await isBuilt + expect(middlewareExists()).toBeTruthy() + isBuilt = nextBuild() + await move(path.join(process.cwd(), 'src', 'middleware.ts'), path.join(process.cwd(), 'middleware.ts')) + await isBuilt + expect(middlewareExists()).toBeTruthy() + }) + + it.skip('should compile the middleware if invalid source is replaced with valid source after the watcher starts', async () => { + console.log( + 'starting should compile the middleware if invalid source is replaced with valid source after the watcher starts', + ) + await moveNextDist('.next', true) + const { watcher, isReady, nextBuild } = watchForMiddlewareChanges(process.cwd()) + watchers.push(watcher) + await isReady + expect(middlewareExists()).toBeFalsy() + let isBuilt = nextBuild() + await writeFile(path.join(process.cwd(), 'middleware.ts'), 'this is not valid middleware') + await isBuilt + expect(middlewareExists()).toBeFalsy() + isBuilt = nextBuild() + await writeFile(path.join(process.cwd(), 'middleware.ts'), middlewareSourceTs) + await isBuilt + expect(middlewareExists()).toBeTruthy() + }) + + it.skip('should not compile middleware if more than one middleware file exists', async () => { + console.log('starting should not compile middleware if more than one middleware file exists') + await moveNextDist('.next', true) + const { watcher, isReady, nextBuild } = watchForMiddlewareChanges(process.cwd()) + watchers.push(watcher) + await isReady + expect(middlewareExists()).toBeFalsy() + await writeFile(path.join(process.cwd(), 'middleware.ts'), middlewareSourceTs) + let isBuilt = nextBuild() + await writeFile(path.join(process.cwd(), 'middleware.js'), middlewareSourceJs) + await isBuilt + expect(middlewareExists()).toBeFalsy() + }) + + it.skip('should not compile middleware if a second middleware file is added after the watcher starts', async () => { + console.log('starting should not compile middleware if a second middleware file is added after the watcher starts') + await moveNextDist('.next', true) + const { watcher, isReady, nextBuild } = watchForMiddlewareChanges(process.cwd()) + watchers.push(watcher) + await isReady + expect(middlewareExists()).toBeFalsy() + let isBuilt = nextBuild() + await writeFile(path.join(process.cwd(), 'middleware.ts'), middlewareSourceTs) + await isBuilt + expect(middlewareExists()).toBeTruthy() + isBuilt = nextBuild() + await writeFile(path.join(process.cwd(), 'middleware.js'), middlewareSourceJs) + await isBuilt + expect(middlewareExists()).toBeFalsy() + }) + + it.skip('should compile middleware if a second middleware file is removed after the watcher starts', async () => { + console.log('starting should compile middleware if a second middleware file is removed after the watcher starts') + await moveNextDist('.next', true) + const { watcher, isReady, nextBuild } = watchForMiddlewareChanges(process.cwd()) + watchers.push(watcher) + await isReady + expect(middlewareExists()).toBeFalsy() + let isBuilt = nextBuild() + await writeFile(path.join(process.cwd(), 'middleware.ts'), middlewareSourceTs) + await isBuilt + isBuilt = nextBuild() + await writeFile(path.join(process.cwd(), 'middleware.js'), middlewareSourceJs) + await isBuilt + expect(middlewareExists()).toBeFalsy() + isBuilt = nextBuild() + await unlink(path.join(process.cwd(), 'middleware.js')) + await isBuilt + expect(middlewareExists()).toBeTruthy() + }) + + it.skip('should generate the correct output for each case when middleware is compiled, added, removed and for error states', async () => { + console.log( + 'starting should generate the correct output for each case when middleware is compiled, added, removed and for error states', + ) + await moveNextDist('.next', true) + const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => {}) + const consoleErrorSpy = jest + .spyOn(console, 'error') + .mockImplementation((args) => console.warn(args?.errors?.[0]?.text)) + const { watcher, isReady, nextBuild } = watchForMiddlewareChanges(process.cwd()) + watchers.push(watcher) + await isReady + expect(middlewareExists()).toBeFalsy() + expect(consoleLogSpy).toHaveBeenCalledWith('Initial scan for middleware file complete. Ready for changes.') + consoleLogSpy.mockClear() + let isBuilt = nextBuild() + await writeFile(path.join(process.cwd(), 'middleware.ts'), middlewareSourceTs) + await isBuilt + expect(consoleLogSpy).toHaveBeenCalledWith('Rebuilding middleware middleware.ts...') + consoleLogSpy.mockClear() + consoleErrorSpy.mockClear() + isBuilt = nextBuild() + await writeFile(path.join(process.cwd(), 'middleware.ts'), 'this is not valid middleware') + await isBuilt + expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('Error: Build failed with 1 error')) + + isBuilt = nextBuild() + await writeFile(path.join(process.cwd(), 'middleware.ts'), middlewareSourceTs) + await isBuilt + isBuilt = nextBuild() + expect(middlewareExists()).toBeTruthy() + consoleLogSpy.mockClear() + + await writeFile(path.join(process.cwd(), 'middleware.js'), middlewareSourceJs) + await isBuilt + expect(consoleLogSpy).toHaveBeenCalledWith('Multiple middleware files found:') + consoleLogSpy.mockClear() + expect(middlewareExists()).toBeFalsy() + }) +})