From c8925c36dbd07f322db2c0506963e5e55e980cbd Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Tue, 25 Jun 2024 14:17:44 +0200 Subject: [PATCH] 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 --- .../access-named-export-block.input.js | 7 ++ .../access-named-export-block.output.js | 9 +++ .../access-named-export.input.js | 5 ++ .../access-named-export.output.js | 7 ++ .../no-access-to-named-export.input.js | 5 ++ .../no-access-to-named-export.output.js | 5 ++ .../non-next-dynamic-dynamic-import.input.js | 7 ++ .../non-next-dynamic-dynamic-import.output.js | 7 ++ .../unsupported-transform.input.js | 7 ++ .../unsupported-transform.output.js | 7 ++ .../next-dynamic-access-named-export.js | 17 ++++ .../next-dynamic-access-named-export.ts | 81 +++++++++++++++++++ 12 files changed, 164 insertions(+) create mode 100644 packages/next-codemod/transforms/__testfixtures__/next-dynamic-access-named-export/access-named-export-block.input.js create mode 100644 packages/next-codemod/transforms/__testfixtures__/next-dynamic-access-named-export/access-named-export-block.output.js create mode 100644 packages/next-codemod/transforms/__testfixtures__/next-dynamic-access-named-export/access-named-export.input.js create mode 100644 packages/next-codemod/transforms/__testfixtures__/next-dynamic-access-named-export/access-named-export.output.js create mode 100644 packages/next-codemod/transforms/__testfixtures__/next-dynamic-access-named-export/no-access-to-named-export.input.js create mode 100644 packages/next-codemod/transforms/__testfixtures__/next-dynamic-access-named-export/no-access-to-named-export.output.js create mode 100644 packages/next-codemod/transforms/__testfixtures__/next-dynamic-access-named-export/non-next-dynamic-dynamic-import.input.js create mode 100644 packages/next-codemod/transforms/__testfixtures__/next-dynamic-access-named-export/non-next-dynamic-dynamic-import.output.js create mode 100644 packages/next-codemod/transforms/__testfixtures__/next-dynamic-access-named-export/unsupported-transform.input.js create mode 100644 packages/next-codemod/transforms/__testfixtures__/next-dynamic-access-named-export/unsupported-transform.output.js create mode 100644 packages/next-codemod/transforms/__tests__/next-dynamic-access-named-export.js create mode 100644 packages/next-codemod/transforms/next-dynamic-access-named-export.ts diff --git a/packages/next-codemod/transforms/__testfixtures__/next-dynamic-access-named-export/access-named-export-block.input.js b/packages/next-codemod/transforms/__testfixtures__/next-dynamic-access-named-export/access-named-export-block.input.js new file mode 100644 index 0000000000000..871a18a1aec84 --- /dev/null +++ b/packages/next-codemod/transforms/__testfixtures__/next-dynamic-access-named-export/access-named-export-block.input.js @@ -0,0 +1,7 @@ +import dynamic from 'next/dynamic' + +const DynamicComponent = dynamic( + () => import('./component').then(mod => { + return mod.default; + }) +) diff --git a/packages/next-codemod/transforms/__testfixtures__/next-dynamic-access-named-export/access-named-export-block.output.js b/packages/next-codemod/transforms/__testfixtures__/next-dynamic-access-named-export/access-named-export-block.output.js new file mode 100644 index 0000000000000..56b62819aa893 --- /dev/null +++ b/packages/next-codemod/transforms/__testfixtures__/next-dynamic-access-named-export/access-named-export-block.output.js @@ -0,0 +1,9 @@ +import dynamic from 'next/dynamic' + +const DynamicComponent = dynamic( + () => import('./component').then(mod => { + return { + default: mod.default + }; + }) +) diff --git a/packages/next-codemod/transforms/__testfixtures__/next-dynamic-access-named-export/access-named-export.input.js b/packages/next-codemod/transforms/__testfixtures__/next-dynamic-access-named-export/access-named-export.input.js new file mode 100644 index 0000000000000..53fc00d2e06f3 --- /dev/null +++ b/packages/next-codemod/transforms/__testfixtures__/next-dynamic-access-named-export/access-named-export.input.js @@ -0,0 +1,5 @@ +import dynamic from 'next/dynamic' + +const DynamicComponent = dynamic( + () => import('./component').then(mod => mod.Component) +) \ No newline at end of file diff --git a/packages/next-codemod/transforms/__testfixtures__/next-dynamic-access-named-export/access-named-export.output.js b/packages/next-codemod/transforms/__testfixtures__/next-dynamic-access-named-export/access-named-export.output.js new file mode 100644 index 0000000000000..88d579819e767 --- /dev/null +++ b/packages/next-codemod/transforms/__testfixtures__/next-dynamic-access-named-export/access-named-export.output.js @@ -0,0 +1,7 @@ +import dynamic from 'next/dynamic' + +const DynamicComponent = dynamic( + () => import('./component').then(mod => ({ + default: mod.Component + })) +) \ No newline at end of file diff --git a/packages/next-codemod/transforms/__testfixtures__/next-dynamic-access-named-export/no-access-to-named-export.input.js b/packages/next-codemod/transforms/__testfixtures__/next-dynamic-access-named-export/no-access-to-named-export.input.js new file mode 100644 index 0000000000000..f83202c2c32f9 --- /dev/null +++ b/packages/next-codemod/transforms/__testfixtures__/next-dynamic-access-named-export/no-access-to-named-export.input.js @@ -0,0 +1,5 @@ +import dynamic from 'next/dynamic' + +const DynamicComponent = dynamic( + () => import('./component') +) diff --git a/packages/next-codemod/transforms/__testfixtures__/next-dynamic-access-named-export/no-access-to-named-export.output.js b/packages/next-codemod/transforms/__testfixtures__/next-dynamic-access-named-export/no-access-to-named-export.output.js new file mode 100644 index 0000000000000..f83202c2c32f9 --- /dev/null +++ b/packages/next-codemod/transforms/__testfixtures__/next-dynamic-access-named-export/no-access-to-named-export.output.js @@ -0,0 +1,5 @@ +import dynamic from 'next/dynamic' + +const DynamicComponent = dynamic( + () => import('./component') +) diff --git a/packages/next-codemod/transforms/__testfixtures__/next-dynamic-access-named-export/non-next-dynamic-dynamic-import.input.js b/packages/next-codemod/transforms/__testfixtures__/next-dynamic-access-named-export/non-next-dynamic-dynamic-import.input.js new file mode 100644 index 0000000000000..a06cb7dcb64c8 --- /dev/null +++ b/packages/next-codemod/transforms/__testfixtures__/next-dynamic-access-named-export/non-next-dynamic-dynamic-import.input.js @@ -0,0 +1,7 @@ +import dynamic from 'my-dynamic-call' + +const DynamicComponent = dynamic( + () => import('./component').then(mod => { + return mod.Component; + }) +) diff --git a/packages/next-codemod/transforms/__testfixtures__/next-dynamic-access-named-export/non-next-dynamic-dynamic-import.output.js b/packages/next-codemod/transforms/__testfixtures__/next-dynamic-access-named-export/non-next-dynamic-dynamic-import.output.js new file mode 100644 index 0000000000000..a06cb7dcb64c8 --- /dev/null +++ b/packages/next-codemod/transforms/__testfixtures__/next-dynamic-access-named-export/non-next-dynamic-dynamic-import.output.js @@ -0,0 +1,7 @@ +import dynamic from 'my-dynamic-call' + +const DynamicComponent = dynamic( + () => import('./component').then(mod => { + return mod.Component; + }) +) diff --git a/packages/next-codemod/transforms/__testfixtures__/next-dynamic-access-named-export/unsupported-transform.input.js b/packages/next-codemod/transforms/__testfixtures__/next-dynamic-access-named-export/unsupported-transform.input.js new file mode 100644 index 0000000000000..a180f87975b95 --- /dev/null +++ b/packages/next-codemod/transforms/__testfixtures__/next-dynamic-access-named-export/unsupported-transform.input.js @@ -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 +}) diff --git a/packages/next-codemod/transforms/__testfixtures__/next-dynamic-access-named-export/unsupported-transform.output.js b/packages/next-codemod/transforms/__testfixtures__/next-dynamic-access-named-export/unsupported-transform.output.js new file mode 100644 index 0000000000000..a180f87975b95 --- /dev/null +++ b/packages/next-codemod/transforms/__testfixtures__/next-dynamic-access-named-export/unsupported-transform.output.js @@ -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 +}) diff --git a/packages/next-codemod/transforms/__tests__/next-dynamic-access-named-export.js b/packages/next-codemod/transforms/__tests__/next-dynamic-access-named-export.js new file mode 100644 index 0000000000000..82bee2d7d26a4 --- /dev/null +++ b/packages/next-codemod/transforms/__tests__/next-dynamic-access-named-export.js @@ -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' }); +} diff --git a/packages/next-codemod/transforms/next-dynamic-access-named-export.ts b/packages/next-codemod/transforms/next-dynamic-access-named-export.ts new file mode 100644 index 0000000000000..25064f1d5374a --- /dev/null +++ b/packages/next-codemod/transforms/next-dynamic-access-named-export.ts @@ -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() +}