Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NextJS: Fix v13 next/link #19834

Merged
merged 4 commits into from Nov 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
92 changes: 30 additions & 62 deletions code/frameworks/nextjs/src/config/webpack.ts
@@ -1,12 +1,9 @@
import type { Configuration as WebpackConfig } from 'webpack';
import { PHASE_DEVELOPMENT_SERVER } from 'next/constants';
import findUp from 'find-up';
import { pathExists } from 'fs-extra';
import semver from 'semver';

import type { NextConfig } from 'next';
import dedent from 'ts-dedent';
import { DefinePlugin } from 'webpack';
import { pathToFileURL } from 'node:url';
import { addScopedAlias } from '../utils';
import { addScopedAlias, getNextjsVersion, resolveNextConfig } from '../utils';

export const configureConfig = async ({
baseConfig,
Expand All @@ -25,63 +22,34 @@ export const configureConfig = async ({
return nextConfig;
};

const findNextConfigFile = async (configDir: string) => {
const supportedExtensions = ['mjs', 'js'];
return supportedExtensions.reduce<Promise<undefined | string>>(
async (acc, ext: string | undefined) => {
const resolved = await acc;
if (!resolved) {
acc = findUp(`next.config.${ext}`, { cwd: configDir });
}

return acc;
},
Promise.resolve(undefined)
);
};

const resolveNextConfig = async ({
baseConfig,
nextConfigPath,
configDir,
}: {
baseConfig: WebpackConfig;
nextConfigPath?: string;
configDir: string;
}): Promise<NextConfig> => {
const nextConfigFile = nextConfigPath || (await findNextConfigFile(configDir));

if (!nextConfigFile || (await pathExists(nextConfigFile)) === false) {
throw new Error(
dedent`
Could not find or resolve your Next config file. Please provide the next config file path as a framework option.
const version = getNextjsVersion();

More info: https://github.com/storybookjs/storybook/blob/next/code/frameworks/nextjs/README.md#options
`
);
const setupRuntimeConfig = (baseConfig: WebpackConfig, nextConfig: NextConfig): void => {
const definePluginConfig: Record<string, any> = {
// this mimics what nextjs does client side
// https://github.com/vercel/next.js/blob/57702cb2a9a9dba4b552e0007c16449cf36cfb44/packages/next/client/index.tsx#L101
'process.env.__NEXT_RUNTIME_CONFIG': JSON.stringify({
serverRuntimeConfig: {},
publicRuntimeConfig: nextConfig.publicRuntimeConfig,
}),
};

/**
* In Next12.2, the `newNextLinkBehavior` option was introduced, defaulted to
* falsy in the Next app (`undefined` in the config itself), and `next/link`
* was engineered to opt *in* to it
*
* In Next13, the `newNextLinkBehavior` option now defaults to truthy (still
* `undefined` in the config), and `next/link` was engineered to opt *out*
* of it
*/
const newNextLinkBehavior = nextConfig.experimental?.newNextLinkBehavior;
if (
(semver.gte(version, '13.0.0') && newNextLinkBehavior !== false) ||
(semver.gte(version, '12.2.0') && newNextLinkBehavior)
) {
definePluginConfig['process.env.__NEXT_NEW_LINK_BEHAVIOR'] = true;
}

const nextConfigExport = await import(pathToFileURL(nextConfigFile).href);

const nextConfig =
typeof nextConfigExport === 'function'
? nextConfigExport(PHASE_DEVELOPMENT_SERVER, {
defaultConfig: baseConfig,
})
: nextConfigExport;

return nextConfig;
};

const setupRuntimeConfig = (baseConfig: WebpackConfig, nextConfig: NextConfig): void => {
baseConfig.plugins?.push(
new DefinePlugin({
// this mimics what nextjs does client side
// https://github.com/vercel/next.js/blob/57702cb2a9a9dba4b552e0007c16449cf36cfb44/packages/next/client/index.tsx#L101
'process.env.__NEXT_RUNTIME_CONFIG': JSON.stringify({
serverRuntimeConfig: {},
publicRuntimeConfig: nextConfig.publicRuntimeConfig,
}),
})
);
baseConfig.plugins?.push(new DefinePlugin(definePluginConfig));
};
56 changes: 55 additions & 1 deletion code/frameworks/nextjs/src/utils.ts
@@ -1,6 +1,12 @@
import path from 'path';
import type { Configuration as WebpackConfig } from 'webpack';
import { DefinePlugin } from 'webpack';
import { PHASE_DEVELOPMENT_SERVER } from 'next/constants';
import findUp from 'find-up';
import { pathExists } from 'fs-extra';
import dedent from 'ts-dedent';
import type { Configuration as WebpackConfig } from 'webpack';
import type { NextConfig } from 'next';
import { pathToFileURL } from 'node:url';

export const configureRuntimeNextjsVersionResolution = (baseConfig: WebpackConfig): void => {
baseConfig.plugins?.push(
Expand All @@ -12,6 +18,54 @@ export const configureRuntimeNextjsVersionResolution = (baseConfig: WebpackConfi

export const getNextjsVersion = (): string => require(scopedResolve('next/package.json')).version;

const findNextConfigFile = async (configDir: string) => {
const supportedExtensions = ['mjs', 'js'];
return supportedExtensions.reduce<Promise<undefined | string>>(
async (acc, ext: string | undefined) => {
const resolved = await acc;
if (!resolved) {
acc = findUp(`next.config.${ext}`, { cwd: configDir });
}

return acc;
},
Promise.resolve(undefined)
);
};

export const resolveNextConfig = async ({
baseConfig = {},
nextConfigPath,
configDir,
}: {
baseConfig?: WebpackConfig;
nextConfigPath?: string;
configDir: string;
}): Promise<NextConfig> => {
const nextConfigFile = nextConfigPath || (await findNextConfigFile(configDir));

if (!nextConfigFile || (await pathExists(nextConfigFile)) === false) {
throw new Error(
dedent`
Could not find or resolve your Next config file. Please provide the next config file path as a framework option.

More info: https://github.com/storybookjs/storybook/blob/next/code/frameworks/nextjs/README.md#options
`
);
}

const nextConfigExport = await import(pathToFileURL(nextConfigFile).href);

const nextConfig =
typeof nextConfigExport === 'function'
? nextConfigExport(PHASE_DEVELOPMENT_SERVER, {
defaultConfig: baseConfig,
})
: nextConfigExport;

return nextConfig.default || nextConfig;
};

// This is to help the addon in development
// Without it, webpack resolves packages in its node_modules instead of the example's node_modules
export const addScopedAlias = (baseConfig: WebpackConfig, name: string, alias?: string): void => {
Expand Down