codemod: add next/dynamic imports codemod (#67126)

### What

Provide a codemod to transform the promise of the access to named export
properties in dynamic import `next/dynamic`, this codemod transform all
the `next/dynamic` imports to ensure returning an object value with
`default` property, aligning with what `React.lazy` is returning

### Why

Follow up for #66990 

It's not allowed to do dynamic import and access it's named export while
using `next/dynamic` in server component, and the dynamic import module
is from a client component. It's like accessing the nested client side
property of a module
This commit is contained in:
Jiachi Liu 2024-06-25 14:17:44 +02:00 committed by GitHub
parent ea8020158e
commit c8925c36db
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 164 additions and 0 deletions

View file

@ -0,0 +1,7 @@
import dynamic from 'next/dynamic'
const DynamicComponent = dynamic(
() => import('./component').then(mod => {
return mod.default;
})
)

View file

@ -0,0 +1,9 @@
import dynamic from 'next/dynamic'
const DynamicComponent = dynamic(
() => import('./component').then(mod => {
return {
default: mod.default
};
})
)

View file

@ -0,0 +1,5 @@
import dynamic from 'next/dynamic'
const DynamicComponent = dynamic(
() => import('./component').then(mod => mod.Component)
)

View file

@ -0,0 +1,7 @@
import dynamic from 'next/dynamic'
const DynamicComponent = dynamic(
() => import('./component').then(mod => ({
default: mod.Component
}))
)

View file

@ -0,0 +1,5 @@
import dynamic from 'next/dynamic'
const DynamicComponent = dynamic(
() => import('./component')
)

View file

@ -0,0 +1,5 @@
import dynamic from 'next/dynamic'
const DynamicComponent = dynamic(
() => import('./component')
)

View file

@ -0,0 +1,7 @@
import dynamic from 'my-dynamic-call'
const DynamicComponent = dynamic(
() => import('./component').then(mod => {
return mod.Component;
})
)

View file

@ -0,0 +1,7 @@
import dynamic from 'my-dynamic-call'
const DynamicComponent = dynamic(
() => import('./component').then(mod => {
return mod.Component;
})
)

View file

@ -0,0 +1,7 @@
import dynamic from 'next/dynamic'
const DynamicImportSourceNextDynamic1 = dynamic(() => import(source).then(mod => mod))
const DynamicImportSourceNextDynamic2 = dynamic(async () => {
const mod = await import(source)
return mod.Component
})

View file

@ -0,0 +1,7 @@
import dynamic from 'next/dynamic'
const DynamicImportSourceNextDynamic1 = dynamic(() => import(source).then(mod => mod))
const DynamicImportSourceNextDynamic2 = dynamic(async () => {
const mod = await import(source)
return mod.Component
})

View file

@ -0,0 +1,17 @@
/* global jest */
jest.autoMockOff()
const defineTest = require('jscodeshift/dist/testUtils').defineTest
const { readdirSync } = require('fs')
const { join } = require('path')
const fixtureDir = 'next-dynamic-access-named-export'
const fixtureDirPath = join(__dirname, '..', '__testfixtures__', fixtureDir)
const fixtures = readdirSync(fixtureDirPath)
.filter(file => file.endsWith('.input.js'))
.map(file => file.replace('.input.js', ''))
for (const fixture of fixtures) {
const prefix = `${fixtureDir}/${fixture}`;
defineTest(__dirname, fixtureDir, null, prefix, { parser: 'js' });
}

View file

@ -0,0 +1,81 @@
import type { FileInfo, API, ImportDeclaration } from 'jscodeshift'
export default function transformer(file: FileInfo, api: API) {
const j = api.jscodeshift
const root = j(file.source)
// Find the import declaration for 'next/dynamic'
const dynamicImportDeclaration = root.find(j.ImportDeclaration, {
source: { value: 'next/dynamic' },
})
// If the import declaration is found
if (dynamicImportDeclaration.size() > 0) {
const importDecl: ImportDeclaration = dynamicImportDeclaration.get(0).node
const dynamicImportName = importDecl.specifiers?.[0]?.local?.name
if (!dynamicImportName) {
return root.toSource()
}
// Find call expressions where the callee is the imported 'dynamic'
root
.find(j.CallExpression, {
callee: { name: dynamicImportName },
})
.forEach((path) => {
const arrowFunction = path.node.arguments[0]
// Ensure the argument is an ArrowFunctionExpression
if (arrowFunction && arrowFunction.type === 'ArrowFunctionExpression') {
const importCall = arrowFunction.body
// Ensure the parent of the import call is a CallExpression with a .then
if (
importCall &&
importCall.type === 'CallExpression' &&
importCall.callee.type === 'MemberExpression' &&
'name' in importCall.callee.property &&
importCall.callee.property.name === 'then'
) {
const thenFunction = importCall.arguments[0]
// handle case of block statement case `=> { return mod.Component }`
// transform to`=> { return { default: mod.Component } }`
if (
thenFunction &&
thenFunction.type === 'ArrowFunctionExpression' &&
thenFunction.body.type === 'BlockStatement'
) {
const returnStatement = thenFunction.body.body[0]
// Ensure the body of the arrow function has a return statement with a MemberExpression
if (
returnStatement &&
returnStatement.type === 'ReturnStatement' &&
returnStatement.argument?.type === 'MemberExpression'
) {
returnStatement.argument = j.objectExpression([
j.property(
'init',
j.identifier('default'),
returnStatement.argument
),
])
}
}
// handle case `=> mod.Component`
// transform to`=> ({ default: mod.Component })`
if (
thenFunction &&
thenFunction.type === 'ArrowFunctionExpression' &&
thenFunction.body.type === 'MemberExpression'
) {
thenFunction.body = j.objectExpression([
j.property('init', j.identifier('default'), thenFunction.body),
])
}
}
}
})
}
return root.toSource()
}