From de169d2a9a4f33db8483fe71f2f0ef9a8b02fccd Mon Sep 17 00:00:00 2001 From: Aiji Uejima Date: Wed, 26 Jan 2022 23:35:24 +0900 Subject: [PATCH] feat: auto remove middleware when A/B tests are stopping --- src/manage-middleware.ts | 27 +++++++++++++ src/prepare-middleware.ts | 43 --------------------- src/utils-for-middleware.ts | 76 +++++++++++++++++++++++++++++++++++++ src/with-split.ts | 35 ++++++++--------- 4 files changed, 118 insertions(+), 63 deletions(-) create mode 100644 src/manage-middleware.ts delete mode 100644 src/prepare-middleware.ts create mode 100644 src/utils-for-middleware.ts diff --git a/src/manage-middleware.ts b/src/manage-middleware.ts new file mode 100644 index 0000000..79115a4 --- /dev/null +++ b/src/manage-middleware.ts @@ -0,0 +1,27 @@ +import { join } from 'path' +import { + exploreUnmanagedMiddlewares, + installMiddleware, + removeMiddleware +} from './utils-for-middleware' + +export const manageMiddleware = ( + filePaths: string[], + prefix: string | undefined, + command: 'install' | 'remove' +) => { + const joinedFilePaths = filePaths.map((p) => join(prefix ?? '', p)) + joinedFilePaths.forEach((path) => { + if (command === 'install') installMiddleware(path) + if (command === 'remove') removeMiddleware(path) + }) + + exploreUnmanagedMiddlewares( + join(prefix ?? '', 'src', 'pages'), + command === 'remove' ? [] : joinedFilePaths + ) + exploreUnmanagedMiddlewares( + join(prefix ?? '', 'pages'), + command === 'remove' ? [] : joinedFilePaths + ) +} diff --git a/src/prepare-middleware.ts b/src/prepare-middleware.ts deleted file mode 100644 index 19fdc1d..0000000 --- a/src/prepare-middleware.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { statSync, unlinkSync, writeFileSync, readFileSync } from 'node:fs' -import { resolve } from 'app-root-path' - -export const installMiddleware = (middlewarePath: string) => { - validateMiddlewarePath(middlewarePath) - const path = resolve(middlewarePath) - if ( - statSync(path, { - throwIfNoEntry: false - }) && - !/export.+middleware.+from.+next-with-split/.test( - readFileSync(path).toString() - ) - ) { - throw new Error(`Manually created middleware is present: ${middlewarePath}`) - } - console.log('split traffic enabled, installing middleware: ', middlewarePath) - writeFileSync(path, scriptText) -} - -export const removeMiddleware = (middlewarePath: string) => { - validateMiddlewarePath(middlewarePath) - const path = resolve(middlewarePath) - if ( - statSync(path, { - throwIfNoEntry: false - }) - ) { - console.log('split traffic disabled, removing middleware: ', middlewarePath) - unlinkSync(path) - } -} - -const validateMiddlewarePath = (path: string) => { - if (!/pages\/.*_middleware\.(js|ts)$/.test(path)) - throw new Error(`Invalid middleware path: ${path}`) -} - -export const scriptText = `// This file was installed automatically by the with-next-split command. -// Note: Do not update this file manually. -// See https://github.com/aiji42/next-with-split/tree/main#auto-installremove-middleware-file -export { middleware } from 'next-with-split' -` diff --git a/src/utils-for-middleware.ts b/src/utils-for-middleware.ts new file mode 100644 index 0000000..9949c28 --- /dev/null +++ b/src/utils-for-middleware.ts @@ -0,0 +1,76 @@ +import { + unlinkSync, + writeFileSync, + readFileSync, + readdirSync, + existsSync +} from 'fs' +import { resolve } from 'app-root-path' + +const DOC_LINK = + 'https://github.com/aiji42/next-with-split/tree/main#auto-installremove-middleware-file' +const LIBRARY_NAME = 'next-with-split' + +export const installMiddleware = (middlewarePath: string) => { + validateMiddlewarePath(middlewarePath) + const path = resolve(middlewarePath) + if ( + existsSync(path) && + !isNextWithSplitMiddleware(readFileSync(path).toString()) + ) { + throw new Error(`Manually created middleware is present: ${middlewarePath}`) + } + console.log('split traffic enabled, installing middleware: ', middlewarePath) + writeFileSync(path, scriptText) +} + +export const removeMiddleware = (middlewarePath: string) => { + validateMiddlewarePath(middlewarePath) + const path = resolve(middlewarePath) + if (existsSync(path)) { + console.log('split traffic disabled, removing middleware: ', middlewarePath) + unlinkSync(path) + } +} + +export const exploreUnmanagedMiddlewares = ( + rootDir: string, + excludes: string[] +) => { + if (!existsSync(rootDir)) return + const middlewares = fileList(resolve(rootDir)).filter((path) => + path.includes('_middleware') + ) + const resolvedExcludes = excludes.map(resolve) + + const unmanaged = middlewares.find((m) => { + return resolvedExcludes.some((e) => e === m) + ? false + : readFileSync(m).toString().includes(LIBRARY_NAME) + }) + if (unmanaged) + throw new Error( + `There is middleware that is not managed by ${LIBRARY_NAME}; ${unmanaged}\nSee ${DOC_LINK}` + ) +} + +const fileList = (dir: string): string[] => + readdirSync(dir, { withFileTypes: true }).flatMap((dirent) => + dirent.isFile() + ? [`${dir}/${dirent.name}`] + : fileList(`${dir}/${dirent.name}`) + ) + +const validateMiddlewarePath = (path: string) => { + if (!/_middleware\.(js|ts)$/.test(path)) + throw new Error(`Invalid middleware path: ${path}`) +} + +const isNextWithSplitMiddleware = (content: string): boolean => + content.includes(LIBRARY_NAME) + +export const scriptText = `// This file was installed automatically by ${LIBRARY_NAME}. +// Note: Do not update this file manually. +// See ${DOC_LINK} +export { middleware } from "${LIBRARY_NAME}" +` diff --git a/src/with-split.ts b/src/with-split.ts index b9a0af1..4ee0821 100644 --- a/src/with-split.ts +++ b/src/with-split.ts @@ -2,14 +2,14 @@ import { SplitOptions } from './types' import { makeRuntimeConfig } from './make-runtime-config' import { NextConfig } from 'next/dist/server/config' import { ImageConfig } from 'next/dist/server/image-config' -import { exec } from 'child_process' +import { manageMiddleware } from './manage-middleware' type WithSplitArgs = { splits?: SplitOptions currentBranch?: string isOriginal?: boolean hostname?: string - middleware?: { manage?: boolean; paths?: string[] } + middleware?: { manage?: boolean; paths?: string[]; appRootDir?: string } } export const withSplit = @@ -26,18 +26,24 @@ export const withSplit = : JSON.parse(process.env.SPLIT_CONFIG_BY_SPECTRUM ?? '{}') if (['true', '1'].includes(process.env.SPLIT_DISABLE ?? '')) { - middleware.manage && manageMiddleware(middleware.paths ?? [], 'remove') + middleware.manage && + manageMiddleware( + middleware.paths ?? [], + middleware.appRootDir, + 'remove' + ) return nextConfig } const isMain = ['true', '1'].includes(process.env.SPLIT_ACTIVE ?? '') || (manuals?.isOriginal ?? process.env.VERCEL_ENV === 'production') + const splitting = Object.keys(splits).length > 0 && isMain const assetHost = manuals?.hostname ?? process.env.VERCEL_URL const currentBranch = manuals?.currentBranch ?? process.env.VERCEL_GIT_COMMIT_REF ?? '' - if (Object.keys(splits).length > 0 && isMain) { + if (splitting) { console.log('Split tests are active.') console.table( Object.entries(splits).map(([testKey, options]) => { @@ -55,7 +61,11 @@ export const withSplit = } middleware.manage && - manageMiddleware(middleware.paths ?? [], isMain ? 'install' : 'remove') + manageMiddleware( + middleware.paths ?? [], + middleware.appRootDir, + splitting ? 'install' : 'remove' + ) if (isSubjectedSplitTest(splits, currentBranch)) process.env.NEXT_PUBLIC_IS_TARGET_SPLIT_TESTING = 'true' @@ -93,18 +103,3 @@ const isSubjectedSplitTest = ( ) return branches.includes(currentBranch) } - -const manageMiddleware = (paths: string[], command: 'install' | 'remove') => { - paths.forEach((path) => { - exec(`npx next-with-split ${command} ${path}`, (err, stdout, stderr) => { - if (stdout) console.log(stdout) - if (err) { - if (stderr) throw new Error(stderr) - throw err - } - if (stderr) throw new Error(stderr) - }) - }) - - // TODO: Explores the pages directory and alerts if there is middleware outside of its control. -}