From da6f271d9be8eb96727d8a8079d0b01aabb36faa Mon Sep 17 00:00:00 2001 From: LongYinan Date: Tue, 12 Apr 2022 22:32:42 +0800 Subject: [PATCH] Interpolate module.exports as default import (#36082) fixes https://github.com/vercel/next.js/issues/34412 ## Bug - [x] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Errors have helpful link attached, see `contributing.md` ## Feature - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have helpful link attached, see `contributing.md` ## Documentation / Examples - [ ] Make sure the linting passes by running `yarn lint` --- .../loaders/next-flight-client-loader.ts | 8 +++++-- packages/next/lib/eslint/runLintCheck.ts | 8 +++++-- .../next/lib/has-necessary-dependencies.ts | 23 ++++++++++++++++++- packages/next/lib/verify-partytown-setup.ts | 8 ++++++- packages/next/lib/verifyTypeScriptSetup.ts | 14 ++++++++--- packages/next/taskfile-swc.js | 15 +++++++++++- packages/next/taskfile.js | 2 +- test/e2e/type-module-interop/index.test.ts | 11 ++++++++- 8 files changed, 77 insertions(+), 12 deletions(-) diff --git a/packages/next/build/webpack/loaders/next-flight-client-loader.ts b/packages/next/build/webpack/loaders/next-flight-client-loader.ts index 326fe9589ab26f9..16016d1ac967458 100644 --- a/packages/next/build/webpack/loaders/next-flight-client-loader.ts +++ b/packages/next/build/webpack/loaders/next-flight-client-loader.ts @@ -10,6 +10,8 @@ import { promisify } from 'util' import { parse } from '../../swc' import { buildExports } from './utils' +const IS_NEXT_CLIENT_BUILT_IN = /[\\/]next[\\/](link|image)\.js$/ + function addExportNames(names: string[], node: any) { if (!node) return switch (node.type) { @@ -57,7 +59,7 @@ async function collectExports( const names: string[] = [] // Next.js built-in client components - if (/[\\/]next[\\/](link|image)\.js$/.test(resourcePath)) { + if (IS_NEXT_CLIENT_BUILT_IN.test(resourcePath)) { names.push('default') } @@ -158,12 +160,14 @@ export default async function transformSource( const moduleRefDef = "const MODULE_REFERENCE = Symbol.for('react.module.reference');\n" + const isNextClientBuiltIn = IS_NEXT_CLIENT_BUILT_IN.test(resourcePath) + const clientRefsExports = names.reduce((res: any, name) => { const moduleRef = '{ $$typeof: MODULE_REFERENCE, filepath: ' + JSON.stringify(resourcePath) + ', name: ' + - JSON.stringify(name) + + JSON.stringify(name === 'default' && isNextClientBuiltIn ? '' : name) + ' };\n' res[name] = moduleRef return res diff --git a/packages/next/lib/eslint/runLintCheck.ts b/packages/next/lib/eslint/runLintCheck.ts index 5a654d9f9e2538a..a7229a6f5911259 100644 --- a/packages/next/lib/eslint/runLintCheck.ts +++ b/packages/next/lib/eslint/runLintCheck.ts @@ -34,8 +34,12 @@ function isValidSeverity(severity: string): severity is Severity { } const requiredPackages = [ - { file: 'eslint', pkg: 'eslint' }, - { file: 'eslint-config-next', pkg: 'eslint-config-next' }, + { file: 'eslint', pkg: 'eslint', exportsRestrict: false }, + { + file: 'eslint-config-next', + pkg: 'eslint-config-next', + exportsRestrict: false, + }, ] async function cliPrompt() { diff --git a/packages/next/lib/has-necessary-dependencies.ts b/packages/next/lib/has-necessary-dependencies.ts index 55097d400f9a8ce..10c3bd67f92d37e 100644 --- a/packages/next/lib/has-necessary-dependencies.ts +++ b/packages/next/lib/has-necessary-dependencies.ts @@ -1,6 +1,10 @@ +import { existsSync } from 'fs' +import { join, relative } from 'path' + export interface MissingDependency { file: string pkg: string + exportsRestrict: boolean } export type NecessaryDependencies = { @@ -15,7 +19,24 @@ export async function hasNecessaryDependencies( let resolutions = new Map() const missingPackages = requiredPackages.filter((p) => { try { - resolutions.set(p.pkg, require.resolve(p.file, { paths: [baseDir] })) + if (p.exportsRestrict) { + const pkgPath = require.resolve(`${p.pkg}/package.json`, { + paths: [baseDir], + }) + const fileNameToVerify = relative(p.pkg, p.file) + if (fileNameToVerify) { + const fileToVerify = join(pkgPath, '..', fileNameToVerify) + if (existsSync(fileToVerify)) { + resolutions.set(p.pkg, join(pkgPath, '..')) + } else { + return true + } + } else { + resolutions.set(p.pkg, pkgPath) + } + } else { + resolutions.set(p.pkg, require.resolve(p.file, { paths: [baseDir] })) + } return false } catch (_) { return true diff --git a/packages/next/lib/verify-partytown-setup.ts b/packages/next/lib/verify-partytown-setup.ts index b1c89e0b1095671..cabae4723d9bfb6 100644 --- a/packages/next/lib/verify-partytown-setup.ts +++ b/packages/next/lib/verify-partytown-setup.ts @@ -61,7 +61,13 @@ export async function verifyPartytownSetup( try { const partytownDeps: NecessaryDependencies = await hasNecessaryDependencies( dir, - [{ file: '@builder.io/partytown', pkg: '@builder.io/partytown' }] + [ + { + file: '@builder.io/partytown', + pkg: '@builder.io/partytown', + exportsRestrict: false, + }, + ] ) if (partytownDeps.missing?.length > 0) { diff --git a/packages/next/lib/verifyTypeScriptSetup.ts b/packages/next/lib/verifyTypeScriptSetup.ts index d5035872882de70..01939302caae771 100644 --- a/packages/next/lib/verifyTypeScriptSetup.ts +++ b/packages/next/lib/verifyTypeScriptSetup.ts @@ -17,9 +17,17 @@ import { missingDepsError } from './typescript/missingDependencyError' import { NextConfigComplete } from '../server/config-shared' const requiredPackages = [ - { file: 'typescript', pkg: 'typescript' }, - { file: '@types/react/index.d.ts', pkg: '@types/react' }, - { file: '@types/node/index.d.ts', pkg: '@types/node' }, + { file: 'typescript', pkg: 'typescript', exportsRestrict: false }, + { + file: '@types/react/index.d.ts', + pkg: '@types/react', + exportsRestrict: true, + }, + { + file: '@types/node/index.d.ts', + pkg: '@types/node', + exportsRestrict: false, + }, ] export async function verifyTypeScriptSetup( diff --git a/packages/next/taskfile-swc.js b/packages/next/taskfile-swc.js index 38395cc19aefb04..71f79fc8647748f 100644 --- a/packages/next/taskfile-swc.js +++ b/packages/next/taskfile-swc.js @@ -14,7 +14,11 @@ module.exports = function (task) { function* ( file, serverOrClient, - { stripExtension, keepImportAssertions = false } = {} + { + stripExtension, + keepImportAssertions = false, + interopClientDefaultExport = false, + } = {} ) { // Don't compile .d.ts if (file.base.endsWith('.d.ts')) return @@ -111,6 +115,15 @@ module.exports = function (task) { } if (output.map) { + if (interopClientDefaultExport) { + output.code += ` +if (typeof exports.default === 'function' || (typeof exports.default === 'object' && exports.default !== null)) { + Object.assign(exports.default, exports); + module.exports = exports.default; +} +` + } + const map = `${file.base}.map` output.code += Buffer.from(`\n//# sourceMappingURL=${map}`) diff --git a/packages/next/taskfile.js b/packages/next/taskfile.js index fc4a1ee697fa957..c9064ba074d39c9 100644 --- a/packages/next/taskfile.js +++ b/packages/next/taskfile.js @@ -1826,7 +1826,7 @@ export async function nextbuild(task, opts) { export async function client(task, opts) { await task .source(opts.src || 'client/**/*.+(js|ts|tsx)') - .swc('client', { dev: opts.dev }) + .swc('client', { dev: opts.dev, interopClientDefaultExport: true }) .target('dist/client') notify('Compiled client files') } diff --git a/test/e2e/type-module-interop/index.test.ts b/test/e2e/type-module-interop/index.test.ts index 918ceaef1151483..686e85b3d20f5c7 100644 --- a/test/e2e/type-module-interop/index.test.ts +++ b/test/e2e/type-module-interop/index.test.ts @@ -11,8 +11,17 @@ describe('Type module interop', () => { next = await createNext({ files: { 'pages/index.js': ` + import Link from 'next/link' + export default function Page() { - return

hello world

+ return ( + <> +

hello world

+ + link to module + + + ) } `, 'pages/modules.jsx': `