Some checks failed
Test examples / Test Examples (20) (push) Has been cancelled
Test examples / Test Examples (22) (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Trigger Release / start (push) Has been cancelled
Stale issue handler / stale (push) Has been cancelled
Update Font Data / create-pull-request (push) Has been cancelled
build-and-deploy / deploy-target (push) Has been cancelled
build-and-deploy / build (push) Has been cancelled
build-and-deploy / stable - aarch64-unknown-linux-musl - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-unknown-linux-musl - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-unknown-linux-gnu - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-unknown-linux-gnu - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-pc-windows-msvc - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-pc-windows-msvc - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-apple-darwin - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-apple-darwin - node@16 (push) Has been cancelled
build-and-deploy / build-wasm (nodejs) (push) Has been cancelled
build-and-deploy / build-wasm (web) (push) Has been cancelled
build-and-deploy / Deploy preview tarball (push) Has been cancelled
build-and-deploy / Potentially publish release (push) Has been cancelled
build-and-deploy / publish-turbopack-npm-packages (push) Has been cancelled
build-and-deploy / Deploy examples (push) Has been cancelled
build-and-deploy / thank you, build (push) Has been cancelled
build-and-deploy / Upload Turbopack Bytesize metrics to Datadog (push) Has been cancelled
Rspack Next.js development integration tests / Rspack integration tests (push) Has been cancelled
Rspack Next.js production integration tests / Rspack integration tests (push) Has been cancelled
Turbopack Next.js development integration tests / Next.js integration tests (push) Has been cancelled
Turbopack Next.js production integration tests / Next.js integration tests (push) Has been cancelled
Update Rspack test manifest / Update and upload Rspack development test manifest (push) Has been cancelled
Update Rspack test manifest / Update and upload Rspack production test manifest (push) Has been cancelled
Upload bundler test manifests to areweturboyet.com / Upload test results (push) Has been cancelled
Update React / create-pull-request (push) Has been cancelled
test-e2e-project-reset-cron / reset-test-project (push) Has been cancelled
Notify about the top 15 issues/PRs/feature requests (most reacted) in the last 90 days / run (push) Has been cancelled
941 lines
27 KiB
TypeScript
941 lines
27 KiB
TypeScript
import type {
|
|
API,
|
|
ASTPath,
|
|
Collection,
|
|
FileInfo,
|
|
ObjectExpression,
|
|
ObjectProperty,
|
|
Property,
|
|
SpreadElement,
|
|
SpreadProperty,
|
|
ObjectMethod,
|
|
} from 'jscodeshift'
|
|
|
|
import fs from 'fs'
|
|
import { join, parse } from 'path'
|
|
import { isNextConfigFile } from './lib/utils'
|
|
import { createParserFromPath } from '../lib/parser'
|
|
|
|
// Middleware config properties that need to be renamed to proxy equivalents
|
|
const CONFIG_PROPERTY_MAP = {
|
|
middlewarePrefetch: 'proxyPrefetch',
|
|
middlewareClientMaxBodySize: 'proxyClientMaxBodySize',
|
|
externalMiddlewareRewritesResolve: 'externalProxyRewritesResolve',
|
|
skipMiddlewareUrlNormalize: 'skipProxyUrlNormalize',
|
|
}
|
|
|
|
// Type imports from 'next/server' that need to be transformed
|
|
const MIDDLEWARE_TYPE_IMPORT_MAP = {
|
|
NextMiddleware: 'NextProxy',
|
|
MiddlewareConfig: 'ProxyConfig',
|
|
}
|
|
|
|
export default function transformer(file: FileInfo) {
|
|
const j = createParserFromPath(file.path)
|
|
const root = j(file.source)
|
|
|
|
if (!root.length) {
|
|
return file.source
|
|
}
|
|
|
|
const isNextConfig =
|
|
isNextConfigFile(file) ||
|
|
(process.env.NODE_ENV === 'test' && /next-config-/.test(file.path))
|
|
const isMiddlewareFile =
|
|
/(^|[/\\])middleware\.|[/\\]src[/\\]middleware\./.test(file.path) ||
|
|
(process.env.NODE_ENV === 'test' && !isNextConfig)
|
|
const hasMiddlewareTypeImports = checkForNextServerTypeImports(root, j)
|
|
|
|
// In test mode, process all files. Otherwise, only process relevant files
|
|
if (process.env.NODE_ENV !== 'test') {
|
|
if (!isMiddlewareFile && !isNextConfig && !hasMiddlewareTypeImports) {
|
|
return file.source
|
|
}
|
|
}
|
|
|
|
let hasChanges = false
|
|
|
|
if (hasMiddlewareTypeImports) {
|
|
const typeImportChanges = transformMiddlewareTypeImports(root, j)
|
|
hasChanges = hasChanges || typeImportChanges
|
|
}
|
|
|
|
if (isMiddlewareFile) {
|
|
const middlewareChanges = transformMiddlewareFunction(root, j)
|
|
hasChanges = hasChanges || middlewareChanges.hasChanges
|
|
|
|
// Remove runtime segment config
|
|
const runtimeChanges = removeRuntimeConfig(root, j)
|
|
hasChanges = hasChanges || runtimeChanges
|
|
}
|
|
|
|
if (isNextConfig) {
|
|
const { hasConfigChanges } = transformNextConfig(root, j)
|
|
hasChanges = hasChanges || hasConfigChanges
|
|
}
|
|
|
|
if (!hasChanges) {
|
|
return file.source
|
|
}
|
|
|
|
const source = root.toSource()
|
|
|
|
// Need to write proxy file and unlink the original middleware file.
|
|
if (isMiddlewareFile) {
|
|
return handleMiddlewareFileRename(file, source)
|
|
}
|
|
|
|
return source
|
|
}
|
|
|
|
function checkForNextServerTypeImports(
|
|
root: Collection<any>,
|
|
j: API['j']
|
|
): boolean {
|
|
return (
|
|
root
|
|
.find(j.ImportDeclaration, {
|
|
source: { value: 'next/server' },
|
|
})
|
|
.find(j.ImportSpecifier)
|
|
.filter(
|
|
(path: ASTPath<any>) =>
|
|
MIDDLEWARE_TYPE_IMPORT_MAP[path.node.imported.name]
|
|
).length > 0
|
|
)
|
|
}
|
|
|
|
function transformMiddlewareTypeImports(
|
|
root: Collection<any>,
|
|
j: API['j']
|
|
): boolean {
|
|
let hasChanges = false
|
|
|
|
// Transform type imports from 'next/server'
|
|
root
|
|
.find(j.ImportDeclaration, {
|
|
source: { value: 'next/server' },
|
|
})
|
|
.forEach((importPath: ASTPath<any>) => {
|
|
const specifiers = importPath.node.specifiers
|
|
if (!specifiers) return
|
|
|
|
specifiers.forEach((specifier: any) => {
|
|
if (
|
|
j.ImportSpecifier.check(specifier) &&
|
|
specifier.imported &&
|
|
MIDDLEWARE_TYPE_IMPORT_MAP[specifier.imported.name]
|
|
) {
|
|
const oldImportName = specifier.imported.name
|
|
const newImportName = MIDDLEWARE_TYPE_IMPORT_MAP[oldImportName]
|
|
|
|
// Update the local name if it matches the original imported name
|
|
if (specifier.local && specifier.local.name === oldImportName) {
|
|
specifier.local.name = newImportName
|
|
}
|
|
|
|
// Transform the import name
|
|
specifier.imported.name = newImportName
|
|
|
|
hasChanges = true
|
|
}
|
|
})
|
|
})
|
|
|
|
// Also transform any type annotations using the old types
|
|
Object.keys(MIDDLEWARE_TYPE_IMPORT_MAP).forEach((oldType) => {
|
|
root
|
|
.find(j.TSTypeReference)
|
|
.filter((path: ASTPath<any>) => {
|
|
return (
|
|
path.node.typeName &&
|
|
path.node.typeName.type === 'Identifier' &&
|
|
path.node.typeName.name === oldType
|
|
)
|
|
})
|
|
.forEach((path: ASTPath<any>) => {
|
|
path.node.typeName.name = MIDDLEWARE_TYPE_IMPORT_MAP[oldType]
|
|
hasChanges = true
|
|
})
|
|
})
|
|
|
|
return hasChanges
|
|
}
|
|
|
|
function transformNextConfig(
|
|
root: Collection<any>,
|
|
j: API['j']
|
|
): { hasConfigChanges: boolean } {
|
|
let hasConfigChanges = false
|
|
|
|
// Collect config-related object expressions instead of processing all
|
|
const configObjects = findNextConfigObjects(root, j)
|
|
configObjects.forEach((objPath: ASTPath<any>) => {
|
|
const result = processConfigObject(objPath.value)
|
|
hasConfigChanges = hasConfigChanges || result.hasChanges
|
|
})
|
|
|
|
// Process function configurations that are likely to be Next.js config
|
|
const configFunctions = findNextConfigFunctions(root, j)
|
|
configFunctions.forEach((path: ASTPath<any>) => {
|
|
const result = processFunctionConfig(path, j)
|
|
hasConfigChanges = hasConfigChanges || result.hasChanges
|
|
})
|
|
|
|
// Process arrow function configurations that are likely to be Next.js config
|
|
const configArrowFunctions = findNextConfigArrowFunctions(root, j)
|
|
configArrowFunctions.forEach((path: ASTPath<any>) => {
|
|
const result = processArrowFunctionConfig(path, j)
|
|
hasConfigChanges = hasConfigChanges || result.hasChanges
|
|
})
|
|
|
|
// Process direct property assignments: config.experimental.middlewarePrefetch = value
|
|
Object.keys(CONFIG_PROPERTY_MAP).forEach((oldProp) => {
|
|
const newProp = CONFIG_PROPERTY_MAP[oldProp]
|
|
|
|
// Handle experimental.* properties
|
|
if (
|
|
oldProp.startsWith('middleware') &&
|
|
oldProp !== 'skipMiddlewareUrlNormalize'
|
|
) {
|
|
root
|
|
.find(j.AssignmentExpression, {
|
|
left: {
|
|
type: 'MemberExpression',
|
|
object: {
|
|
type: 'MemberExpression',
|
|
property: { name: 'experimental' },
|
|
},
|
|
property: { name: oldProp },
|
|
},
|
|
})
|
|
.forEach((path: ASTPath<any>) => {
|
|
path.node.left.property.name = newProp
|
|
hasConfigChanges = true
|
|
})
|
|
} else {
|
|
// Handle top-level properties like skipMiddlewareUrlNormalize
|
|
root
|
|
.find(j.AssignmentExpression, {
|
|
left: {
|
|
type: 'MemberExpression',
|
|
property: { name: oldProp },
|
|
},
|
|
})
|
|
.forEach((path: ASTPath<any>) => {
|
|
path.node.left.property.name = newProp
|
|
hasConfigChanges = true
|
|
})
|
|
}
|
|
})
|
|
|
|
return { hasConfigChanges }
|
|
}
|
|
|
|
function processConfigObject(configObj: ObjectExpression): {
|
|
hasChanges: boolean
|
|
} {
|
|
let hasChanges = false
|
|
|
|
// Check for experimental property
|
|
const experimentalProp = configObj.properties.find(
|
|
(prop) =>
|
|
isStaticProperty(prop) &&
|
|
prop.key &&
|
|
prop.key.type === 'Identifier' &&
|
|
prop.key.name === 'experimental'
|
|
)
|
|
|
|
if (experimentalProp && isStaticProperty(experimentalProp)) {
|
|
const experimentalObj = experimentalProp.value
|
|
if (experimentalObj.type === 'ObjectExpression') {
|
|
// Transform properties in experimental object
|
|
experimentalObj.properties.forEach((prop) => {
|
|
if (
|
|
isStaticProperty(prop) &&
|
|
prop.key &&
|
|
prop.key.type === 'Identifier' &&
|
|
CONFIG_PROPERTY_MAP[prop.key.name] &&
|
|
prop.key.name !== 'skipMiddlewareUrlNormalize' // This is top-level
|
|
) {
|
|
prop.key.name = CONFIG_PROPERTY_MAP[prop.key.name]
|
|
hasChanges = true
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Transform top-level properties
|
|
configObj.properties.forEach((prop) => {
|
|
if (
|
|
isStaticProperty(prop) &&
|
|
prop.key &&
|
|
prop.key.type === 'Identifier' &&
|
|
prop.key.name === 'skipMiddlewareUrlNormalize'
|
|
) {
|
|
prop.key.name = CONFIG_PROPERTY_MAP[prop.key.name]
|
|
hasChanges = true
|
|
}
|
|
})
|
|
|
|
// Also transform any top-level middleware properties (for spread scenarios)
|
|
configObj.properties.forEach((prop) => {
|
|
if (
|
|
isStaticProperty(prop) &&
|
|
prop.key &&
|
|
prop.key.type === 'Identifier' &&
|
|
CONFIG_PROPERTY_MAP[prop.key.name] &&
|
|
prop.key.name !== 'skipMiddlewareUrlNormalize' // Already handled above
|
|
) {
|
|
prop.key.name = CONFIG_PROPERTY_MAP[prop.key.name]
|
|
hasChanges = true
|
|
}
|
|
})
|
|
|
|
return { hasChanges }
|
|
}
|
|
|
|
function processFunctionConfig(
|
|
path: ASTPath<any>,
|
|
j: API['j']
|
|
): { hasChanges: boolean } {
|
|
let hasChanges = false
|
|
|
|
// Look for return statements with object expressions
|
|
j(path)
|
|
.find(j.ReturnStatement)
|
|
.forEach((returnPath: ASTPath<any>) => {
|
|
if (
|
|
returnPath.node.argument &&
|
|
returnPath.node.argument.type === 'ObjectExpression'
|
|
) {
|
|
const result = processConfigObject(returnPath.node.argument)
|
|
hasChanges = hasChanges || result.hasChanges
|
|
}
|
|
})
|
|
|
|
return { hasChanges }
|
|
}
|
|
|
|
function processArrowFunctionConfig(
|
|
path: ASTPath<any>,
|
|
j: API['j']
|
|
): { hasChanges: boolean } {
|
|
let hasChanges = false
|
|
|
|
const body = path.node.body
|
|
|
|
// Handle: () => ({ ... })
|
|
if (body && body.type === 'ObjectExpression') {
|
|
const result = processConfigObject(body)
|
|
hasChanges = hasChanges || result.hasChanges
|
|
}
|
|
|
|
// Handle: () => { return { ... } }
|
|
if (body && body.type === 'BlockStatement') {
|
|
j(path)
|
|
.find(j.ReturnStatement)
|
|
.forEach((returnPath: ASTPath<any>) => {
|
|
if (
|
|
returnPath.node.argument &&
|
|
returnPath.node.argument.type === 'ObjectExpression'
|
|
) {
|
|
const result = processConfigObject(returnPath.node.argument)
|
|
hasChanges = hasChanges || result.hasChanges
|
|
}
|
|
})
|
|
}
|
|
|
|
return { hasChanges }
|
|
}
|
|
|
|
function transformMiddlewareFunction(
|
|
root: Collection<any>,
|
|
j: API['j']
|
|
): { hasChanges: boolean } {
|
|
const proxyIdentifier = generateUniqueIdentifier(root, j, 'proxy')
|
|
const needsAlias = proxyIdentifier !== 'proxy'
|
|
|
|
let hasChanges = false
|
|
// Track if we exported something as 'proxy'
|
|
let exportedAsProxy = false
|
|
|
|
// Handle named export declarations
|
|
root.find(j.ExportNamedDeclaration).forEach((nodePath) => {
|
|
const declaration = nodePath.node.declaration
|
|
|
|
// Handle: export function middleware() {} or export async function middleware() {}
|
|
if (
|
|
j.FunctionDeclaration.check(declaration) &&
|
|
declaration.id?.name === 'middleware'
|
|
) {
|
|
declaration.id.name = proxyIdentifier
|
|
exportedAsProxy = true // Exported function declarations become proxy
|
|
hasChanges = true
|
|
}
|
|
|
|
// Handle: export { middleware }
|
|
if (nodePath.node.specifiers) {
|
|
nodePath.node.specifiers.forEach((specifier) => {
|
|
if (
|
|
j.ExportSpecifier.check(specifier) &&
|
|
j.Identifier.check(specifier.local) &&
|
|
specifier.local.name === 'middleware'
|
|
) {
|
|
// Check if this is exporting middleware as 'middleware' (which should become 'proxy')
|
|
if (
|
|
j.Identifier.check(specifier.exported) &&
|
|
specifier.exported.name === 'middleware'
|
|
) {
|
|
if (needsAlias) {
|
|
// Create export alias: export { _proxy1 as proxy }
|
|
const newSpecifier = j.exportSpecifier.from({
|
|
local: j.identifier(proxyIdentifier),
|
|
exported: j.identifier('proxy'),
|
|
})
|
|
// Replace in the specifiers array
|
|
const specifierIndex = nodePath.node.specifiers.indexOf(specifier)
|
|
nodePath.node.specifiers[specifierIndex] = newSpecifier
|
|
} else {
|
|
// Simple rename: export { proxy }
|
|
specifier.exported = j.identifier('proxy')
|
|
specifier.local = j.identifier('proxy')
|
|
}
|
|
exportedAsProxy = true
|
|
hasChanges = true
|
|
} else {
|
|
// This is exporting middleware as something else (e.g., export { middleware as randomName })
|
|
// Just update the local reference to the new identifier
|
|
specifier.local = j.identifier(proxyIdentifier)
|
|
hasChanges = true
|
|
}
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
// Handle default export declarations
|
|
root.find(j.ExportDefaultDeclaration).forEach((nodePath) => {
|
|
const declaration = nodePath.node.declaration
|
|
|
|
// Handle: export default function middleware() {} or export default async function middleware() {}
|
|
if (
|
|
j.FunctionDeclaration.check(declaration) &&
|
|
declaration.id?.name === 'middleware'
|
|
) {
|
|
declaration.id.name = proxyIdentifier
|
|
hasChanges = true
|
|
}
|
|
})
|
|
|
|
// Handle function declarations that are later exported
|
|
root
|
|
.find(j.FunctionDeclaration, {
|
|
id: { name: 'middleware' },
|
|
})
|
|
.forEach((nodePath) => {
|
|
if (nodePath.node.id) {
|
|
nodePath.node.id.name = proxyIdentifier
|
|
hasChanges = true
|
|
}
|
|
})
|
|
|
|
// Handle variable declarations: const middleware = ...
|
|
root
|
|
.find(j.VariableDeclarator, {
|
|
id: { name: 'middleware' },
|
|
})
|
|
.forEach((nodePath) => {
|
|
if (j.Identifier.check(nodePath.node.id)) {
|
|
nodePath.node.id.name = proxyIdentifier
|
|
hasChanges = true
|
|
}
|
|
})
|
|
|
|
// Update all references to middleware in the scope
|
|
if (hasChanges && needsAlias) {
|
|
root
|
|
.find(j.Identifier, { name: 'middleware' })
|
|
.filter((astPath: ASTPath<any>) => {
|
|
// Don't rename if it's part of an export specifier we already handled
|
|
const parent = astPath.parent
|
|
if (j.ExportSpecifier.check(parent.node)) {
|
|
return false
|
|
}
|
|
|
|
// Don't rename if it's a function/variable declaration we already handled
|
|
if (
|
|
(j.FunctionDeclaration.check(parent.node) &&
|
|
parent.node.id === astPath.node) ||
|
|
(j.VariableDeclarator.check(parent.node) &&
|
|
parent.node.id === astPath.node)
|
|
) {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
})
|
|
.forEach((astPath: ASTPath<any>) => {
|
|
astPath.node.name = proxyIdentifier
|
|
})
|
|
}
|
|
|
|
// If we used a unique identifier AND we exported `as proxy`, add an export alias
|
|
// This handles cases where the export was part of the declaration itself:
|
|
// export function middleware() {} -> export function _proxy1() {} (needs alias)
|
|
// vs cases where export was separate:
|
|
// export { middleware } -> export { _proxy1 as proxy } (already handled)
|
|
if (needsAlias && hasChanges && exportedAsProxy) {
|
|
// Check if we already created a proxy export (from export specifiers like `export { middleware }`)
|
|
const hasExportSpecifier =
|
|
root.find(j.ExportNamedDeclaration).filter((astPath: ASTPath<any>) => {
|
|
return (
|
|
astPath.node.specifiers &&
|
|
astPath.node.specifiers.some(
|
|
(spec) =>
|
|
j.ExportSpecifier.check(spec) &&
|
|
j.Identifier.check(spec.exported) &&
|
|
spec.exported.name === 'proxy'
|
|
)
|
|
)
|
|
}).length > 0
|
|
|
|
// If no proxy export exists yet, create one to maintain the 'proxy' API
|
|
// Example: export function _proxy1() {} + export { _proxy1 as proxy }
|
|
if (!hasExportSpecifier) {
|
|
const exportSpecifier = j.exportSpecifier.from({
|
|
local: j.identifier(proxyIdentifier),
|
|
exported: j.identifier('proxy'),
|
|
})
|
|
|
|
const exportDeclaration = j.exportNamedDeclaration(null, [
|
|
exportSpecifier,
|
|
])
|
|
|
|
// Add the export at the end of the file
|
|
const program = root.find(j.Program)
|
|
if (program.length > 0) {
|
|
program.get('body').value.push(exportDeclaration)
|
|
}
|
|
}
|
|
}
|
|
|
|
return { hasChanges }
|
|
}
|
|
|
|
function handleMiddlewareFileRename(file: FileInfo, source: string): string {
|
|
// We will not modify the original file in real world,
|
|
// so return the source here for testing.
|
|
if (process.env.NODE_ENV === 'test') {
|
|
return source
|
|
}
|
|
|
|
const { dir, ext } = parse(file.path)
|
|
const newFilePath = join(dir, 'proxy' + ext)
|
|
|
|
try {
|
|
fs.writeFileSync(newFilePath, source)
|
|
fs.unlinkSync(file.path)
|
|
// Return empty string to indicate successful file replacement.
|
|
return ''
|
|
} catch (cause) {
|
|
console.error(
|
|
`Failed to write "${newFilePath}" and delete "${file.path}".\n${JSON.stringify({ cause })}`
|
|
)
|
|
return file.source
|
|
}
|
|
}
|
|
|
|
function isStaticProperty(
|
|
prop:
|
|
| Property
|
|
| ObjectProperty
|
|
| SpreadElement
|
|
| SpreadProperty
|
|
| ObjectMethod
|
|
): prop is Property | ObjectProperty {
|
|
return prop.type === 'Property' || prop.type === 'ObjectProperty'
|
|
}
|
|
|
|
function generateUniqueIdentifier(
|
|
root: Collection<any>,
|
|
j: API['j'],
|
|
baseName: string
|
|
): string {
|
|
// First check if baseName itself is available
|
|
if (!hasIdentifierInScope(root, j, baseName)) {
|
|
return baseName
|
|
}
|
|
|
|
// Generate _proxy1, _proxy2, etc.
|
|
let counter = 1
|
|
while (true) {
|
|
const candidate = `_${baseName}${counter}`
|
|
if (!hasIdentifierInScope(root, j, candidate)) {
|
|
return candidate
|
|
}
|
|
counter++
|
|
}
|
|
}
|
|
|
|
function hasIdentifierInScope(
|
|
root: Collection<any>,
|
|
j: API['j'],
|
|
name: string
|
|
): boolean {
|
|
// Check for variable declarations
|
|
const hasVariableDeclaration =
|
|
root
|
|
.find(j.VariableDeclarator)
|
|
.filter(
|
|
(astPath: ASTPath<any>) =>
|
|
j.Identifier.check(astPath.value.id) && astPath.value.id.name === name
|
|
).length > 0
|
|
|
|
// Check for function declarations
|
|
const hasFunctionDeclaration =
|
|
root
|
|
.find(j.FunctionDeclaration)
|
|
.filter(
|
|
(astPath: ASTPath<any>) =>
|
|
astPath.value.id && astPath.value.id.name === name
|
|
).length > 0
|
|
|
|
// Check for import specifiers
|
|
const hasImportSpecifier =
|
|
root
|
|
.find(j.ImportSpecifier)
|
|
.filter(
|
|
(astPath: ASTPath<any>) =>
|
|
j.Identifier.check(astPath.value.local) &&
|
|
astPath.value.local.name === name
|
|
).length > 0
|
|
|
|
return hasVariableDeclaration || hasFunctionDeclaration || hasImportSpecifier
|
|
}
|
|
|
|
function findNextConfigObjects(
|
|
root: Collection<any>,
|
|
j: API['j']
|
|
): ASTPath<any>[] {
|
|
const configObjects: ASTPath<any>[] = []
|
|
|
|
// Find identifiers that are exported as default or assigned to module.exports
|
|
const exportedNames = new Set<string>()
|
|
|
|
// Handle: export default nextConfig or export default wrappedFunction(nextConfig)
|
|
root.find(j.ExportDefaultDeclaration).forEach((path: ASTPath<any>) => {
|
|
if (j.Identifier.check(path.node.declaration)) {
|
|
exportedNames.add(path.node.declaration.name)
|
|
} else if (j.ObjectExpression.check(path.node.declaration)) {
|
|
// Direct object export: export default { ... }
|
|
configObjects.push(path.get('declaration'))
|
|
} else if (j.CallExpression.check(path.node.declaration)) {
|
|
// Handle wrapped exports: export default wrapper(config)
|
|
extractObjectsFromCallExpression(
|
|
path.node.declaration,
|
|
configObjects,
|
|
exportedNames,
|
|
j
|
|
)
|
|
}
|
|
})
|
|
|
|
// Handle: module.exports = nextConfig or module.exports = wrappedFunction(nextConfig)
|
|
root
|
|
.find(j.AssignmentExpression, {
|
|
left: {
|
|
type: 'MemberExpression',
|
|
object: { name: 'module' },
|
|
property: { name: 'exports' },
|
|
},
|
|
})
|
|
.forEach((path: ASTPath<any>) => {
|
|
if (j.Identifier.check(path.node.right)) {
|
|
exportedNames.add(path.node.right.name)
|
|
} else if (j.ObjectExpression.check(path.node.right)) {
|
|
// Direct object assignment: module.exports = { ... }
|
|
configObjects.push(path.get('right'))
|
|
} else if (j.CallExpression.check(path.node.right)) {
|
|
// Handle wrapped assignments: module.exports = wrapper(config)
|
|
extractObjectsFromCallExpression(
|
|
path.node.right,
|
|
configObjects,
|
|
exportedNames,
|
|
j
|
|
)
|
|
}
|
|
})
|
|
|
|
// Find variable declarations for exported names
|
|
exportedNames.forEach((name) => {
|
|
root
|
|
.find(j.VariableDeclarator, { id: { name } })
|
|
.forEach((path: ASTPath<any>) => {
|
|
if (j.ObjectExpression.check(path.node.init)) {
|
|
configObjects.push(path.get('init'))
|
|
}
|
|
})
|
|
})
|
|
|
|
return configObjects
|
|
}
|
|
|
|
function findNextConfigFunctions(
|
|
root: Collection<any>,
|
|
j: API['j']
|
|
): ASTPath<any>[] {
|
|
const configFunctions: ASTPath<any>[] = []
|
|
const exportedNames = new Set<string>()
|
|
|
|
// Handle: export default function or export default functionName
|
|
root.find(j.ExportDefaultDeclaration).forEach((path: ASTPath<any>) => {
|
|
if (j.FunctionDeclaration.check(path.node.declaration)) {
|
|
// export default function configFunction() { ... }
|
|
configFunctions.push(path.get('declaration'))
|
|
} else if (j.Identifier.check(path.node.declaration)) {
|
|
exportedNames.add(path.node.declaration.name)
|
|
}
|
|
})
|
|
|
|
// Handle: module.exports = function
|
|
root
|
|
.find(j.AssignmentExpression, {
|
|
left: {
|
|
type: 'MemberExpression',
|
|
object: { name: 'module' },
|
|
property: { name: 'exports' },
|
|
},
|
|
})
|
|
.forEach((path: ASTPath<any>) => {
|
|
if (j.FunctionExpression.check(path.node.right)) {
|
|
// module.exports = function() { ... }
|
|
configFunctions.push(path.get('right'))
|
|
} else if (j.Identifier.check(path.node.right)) {
|
|
exportedNames.add(path.node.right.name)
|
|
}
|
|
})
|
|
|
|
// Find function declarations for exported names
|
|
exportedNames.forEach((name) => {
|
|
root
|
|
.find(j.FunctionDeclaration, { id: { name } })
|
|
.forEach((path: ASTPath<any>) => {
|
|
configFunctions.push(path)
|
|
})
|
|
})
|
|
|
|
return configFunctions
|
|
}
|
|
|
|
function findNextConfigArrowFunctions(
|
|
root: Collection<any>,
|
|
j: API['j']
|
|
): ASTPath<any>[] {
|
|
const configArrowFunctions: ASTPath<any>[] = []
|
|
const exportedNames = new Set<string>()
|
|
|
|
// Handle: export default arrowFunction
|
|
root.find(j.ExportDefaultDeclaration).forEach((path: ASTPath<any>) => {
|
|
if (j.ArrowFunctionExpression.check(path.node.declaration)) {
|
|
// export default () => { ... }
|
|
configArrowFunctions.push(path.get('declaration'))
|
|
} else if (j.Identifier.check(path.node.declaration)) {
|
|
exportedNames.add(path.node.declaration.name)
|
|
}
|
|
})
|
|
|
|
// Handle: module.exports = arrowFunction
|
|
root
|
|
.find(j.AssignmentExpression, {
|
|
left: {
|
|
type: 'MemberExpression',
|
|
object: { name: 'module' },
|
|
property: { name: 'exports' },
|
|
},
|
|
})
|
|
.forEach((path: ASTPath<any>) => {
|
|
if (j.ArrowFunctionExpression.check(path.node.right)) {
|
|
// module.exports = () => { ... }
|
|
configArrowFunctions.push(path.get('right'))
|
|
} else if (j.Identifier.check(path.node.right)) {
|
|
exportedNames.add(path.node.right.name)
|
|
}
|
|
})
|
|
|
|
// Find variable declarations with arrow functions for exported names
|
|
exportedNames.forEach((name) => {
|
|
root
|
|
.find(j.VariableDeclarator, { id: { name } })
|
|
.forEach((path: ASTPath<any>) => {
|
|
if (j.ArrowFunctionExpression.check(path.node.init)) {
|
|
configArrowFunctions.push(path.get('init'))
|
|
}
|
|
})
|
|
})
|
|
|
|
return configArrowFunctions
|
|
}
|
|
|
|
function extractObjectsFromCallExpression(
|
|
callExpr: any,
|
|
configObjects: ASTPath<any>[],
|
|
exportedNames: Set<string>,
|
|
j: API['j']
|
|
): void {
|
|
// Recursively extract arguments from call expressions
|
|
// E.g., wrapper(anotherWrapper(config)) or wrapper(config)
|
|
if (callExpr.arguments) {
|
|
callExpr.arguments.forEach((arg: any) => {
|
|
if (j.Identifier.check(arg)) {
|
|
exportedNames.add(arg.name)
|
|
} else if (j.ObjectExpression.check(arg)) {
|
|
// This would be unusual but handle direct object arguments
|
|
// We don't have the path here, so we'll skip this case
|
|
// It would be handled by the direct export case anyway
|
|
} else if (j.CallExpression.check(arg)) {
|
|
extractObjectsFromCallExpression(arg, configObjects, exportedNames, j)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
function removeRuntimeConfig(root: Collection<any>, j: API['j']): boolean {
|
|
let hasChanges = false
|
|
|
|
// Remove export const runtime = 'string'
|
|
const directRuntimeExports = root.find(j.ExportNamedDeclaration, {
|
|
declaration: {
|
|
type: 'VariableDeclaration',
|
|
declarations: [
|
|
{
|
|
id: { name: 'runtime' },
|
|
},
|
|
],
|
|
},
|
|
})
|
|
|
|
if (directRuntimeExports.size() > 0) {
|
|
directRuntimeExports.remove()
|
|
hasChanges = true
|
|
}
|
|
|
|
// Remove const runtime = 'string' declarations
|
|
const runtimeVariableDeclarations = root
|
|
.find(j.VariableDeclaration)
|
|
.filter((path) =>
|
|
path.node.declarations.some((decl) => {
|
|
if (j.VariableDeclarator.check(decl) && j.Identifier.check(decl.id)) {
|
|
return decl.id.name === 'runtime'
|
|
}
|
|
return false
|
|
})
|
|
)
|
|
|
|
if (runtimeVariableDeclarations.size() > 0) {
|
|
runtimeVariableDeclarations.forEach((path) => {
|
|
const originalDeclarations = path.node.declarations
|
|
const filteredDeclarations = originalDeclarations.filter((decl) => {
|
|
if (j.VariableDeclarator.check(decl) && j.Identifier.check(decl.id)) {
|
|
return decl.id.name !== 'runtime'
|
|
}
|
|
return true
|
|
})
|
|
|
|
// If we filtered out some declarations, update the node
|
|
if (filteredDeclarations.length !== originalDeclarations.length) {
|
|
// Remove the entire declaration only if no declarators left
|
|
if (filteredDeclarations.length === 0) {
|
|
j(path).remove()
|
|
} else {
|
|
path.node.declarations = filteredDeclarations
|
|
}
|
|
}
|
|
})
|
|
hasChanges = true
|
|
}
|
|
|
|
// Handle export { runtime } and export { runtime, other }
|
|
const namedExports = root
|
|
.find(j.ExportNamedDeclaration)
|
|
.filter((path) => path.node.specifiers && path.node.specifiers.length > 0)
|
|
|
|
namedExports.forEach((path) => {
|
|
const specifiers = path.node.specifiers
|
|
if (!specifiers) return
|
|
|
|
const filteredSpecifiers = specifiers.filter((spec) => {
|
|
if (j.ExportSpecifier.check(spec) && j.Identifier.check(spec.local)) {
|
|
return spec.local.name !== 'runtime'
|
|
}
|
|
return true
|
|
})
|
|
|
|
// If we removed any specifiers
|
|
if (filteredSpecifiers.length !== specifiers.length) {
|
|
hasChanges = true
|
|
|
|
// If no specifiers left, remove the entire export statement
|
|
if (filteredSpecifiers.length === 0) {
|
|
j(path).remove()
|
|
} else {
|
|
// Update the specifiers array
|
|
path.node.specifiers = filteredSpecifiers
|
|
}
|
|
}
|
|
})
|
|
|
|
// Handle runtime property in config objects
|
|
const configExports = root.find(j.ExportNamedDeclaration, {
|
|
declaration: {
|
|
type: 'VariableDeclaration',
|
|
declarations: [
|
|
{
|
|
id: { name: 'config' },
|
|
},
|
|
],
|
|
},
|
|
})
|
|
|
|
configExports.forEach((path) => {
|
|
const declaration = path.node.declaration
|
|
if (j.VariableDeclaration.check(declaration)) {
|
|
declaration.declarations.forEach((decl) => {
|
|
if (
|
|
j.VariableDeclarator.check(decl) &&
|
|
j.Identifier.check(decl.id) &&
|
|
decl.id.name === 'config' &&
|
|
j.ObjectExpression.check(decl.init)
|
|
) {
|
|
const objExpr = decl.init
|
|
const initialLength = objExpr.properties.length
|
|
|
|
// Filter out runtime property
|
|
objExpr.properties = objExpr.properties.filter((prop) => {
|
|
if (
|
|
isStaticProperty(prop) &&
|
|
prop.key &&
|
|
prop.key.type === 'Identifier'
|
|
) {
|
|
return prop.key.name !== 'runtime'
|
|
}
|
|
return true
|
|
})
|
|
|
|
// If we removed any properties
|
|
if (objExpr.properties.length !== initialLength) {
|
|
hasChanges = true
|
|
|
|
// If no properties left, remove the entire config export
|
|
if (objExpr.properties.length === 0) {
|
|
j(path).remove()
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
return hasChanges
|
|
}
|