Skip to content

Commit

Permalink
feat: auto remove middleware when A/B tests are stopping
Browse files Browse the repository at this point in the history
  • Loading branch information
aiji42 committed Jan 26, 2022
1 parent 5f82a78 commit de169d2
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 63 deletions.
27 changes: 27 additions & 0 deletions src/manage-middleware.ts
Original file line number Diff line number Diff line change
@@ -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
)
}
43 changes: 0 additions & 43 deletions src/prepare-middleware.ts

This file was deleted.

76 changes: 76 additions & 0 deletions src/utils-for-middleware.ts
Original file line number Diff line number Diff line change
@@ -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}"
`
35 changes: 15 additions & 20 deletions src/with-split.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand All @@ -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]) => {
Expand All @@ -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'
Expand Down Expand Up @@ -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.
}

0 comments on commit de169d2

Please sign in to comment.