Skip to content

Commit

Permalink
Next.js: Implement next redirect and the RedirectBoundary
Browse files Browse the repository at this point in the history
  • Loading branch information
yannbf committed May 7, 2024
1 parent af3906d commit a1ca72d
Show file tree
Hide file tree
Showing 8 changed files with 254 additions and 1,222 deletions.
6 changes: 6 additions & 0 deletions code/frameworks/nextjs/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@
"react/no-unknown-property": "off",
"jsx-a11y/anchor-is-valid": "off"
}
},
{
"files": ["**/*.compat.@(tsx|ts)"],
"rules": {
"local-rules/no-uncategorized-errors": "off"
}
}
]
}
12 changes: 12 additions & 0 deletions code/frameworks/nextjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,16 @@
"require": "./dist/next-image-loader-stub.js",
"import": "./dist/next-image-loader-stub.mjs"
},
"./dist/compatibility/segment.compat": {
"types": "./dist/compatibility/segment.compat.d.ts",
"require": "./dist/compatibility/segment.compat.js",
"import": "./dist/compatibility/segment.compat.mjs"
},
"./dist/compatibility/redirect-status-code.compat": {
"types": "./dist/compatibility/redirect-status-code.compat.d.ts",
"require": "./dist/compatibility/redirect-status-code.compat.js",
"import": "./dist/compatibility/redirect-status-code.compat.mjs"
},
"./export-mocks": {
"types": "./dist/export-mocks/index.d.ts",
"require": "./dist/export-mocks/index.js",
Expand Down Expand Up @@ -205,6 +215,8 @@
"./src/export-mocks/headers/index.ts",
"./src/export-mocks/router/index.ts",
"./src/export-mocks/navigation/index.ts",
"./src/compatibility/segment.compat.ts",
"./src/compatibility/redirect-status-code.compat.ts",
"./src/next-image-loader-stub.ts",
"./src/images/decorator.tsx",
"./src/images/next-legacy-image.tsx",
Expand Down
32 changes: 32 additions & 0 deletions code/frameworks/nextjs/src/compatibility/compatibility-map.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { Configuration as WebpackConfig } from 'webpack';
import semver from 'semver';
import { getNextjsVersion, addScopedAlias } from '../utils';

const mapping: Record<string, Record<string, string>> = {
'<14.0.0': {
'next/dist/shared/lib/segment': '@storybook/nextjs/dist/compatibility/segment.compat',
'next/dist/client/components/redirect-status-code':
'@storybook/nextjs/dist/compatibility/redirect-status-code.compat',
},
};

export const getCompatibilityAliases = () => {
const version = getNextjsVersion();
const result: Record<string, string> = {};

Object.keys(mapping).filter((key) => {
if (semver.intersects(version, key)) {
Object.assign(result, mapping[key]);
}
});

return result;
};

export const configureCompatibilityAliases = (baseConfig: WebpackConfig): void => {
const aliases = getCompatibilityAliases();

Object.entries(aliases).forEach(([name, alias]) => {
addScopedAlias(baseConfig, name, alias);
});
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Compatibility for Next 13
export enum RedirectStatusCode {
SeeOther = 303,
TemporaryRedirect = 307,
PermanentRedirect = 308,
}
8 changes: 8 additions & 0 deletions code/frameworks/nextjs/src/compatibility/segment.compat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Compatibility for Next 13
// from https://github.com/vercel/next.js/blob/606f9ff7903b58da51aa043bfe71cd7b6ea306fd/packages/next/src/shared/lib/segment.ts#L4
export function isGroupSegment(segment: string) {
return segment[0] === '(' && segment.endsWith(')');
}

export const PAGE_SEGMENT_KEY = '__PAGE__';
export const DEFAULT_SEGMENT_KEY = '__DEFAULT__';
43 changes: 26 additions & 17 deletions code/frameworks/nextjs/src/export-mocks/webpack.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,38 @@
import { dirname, join } from 'path';
import type { Configuration as WebpackConfig } from 'webpack';
import { getCompatibilityAliases } from '../compatibility/compatibility-map';

const mapping = {
'next/headers': '/dist/export-mocks/headers/index',
'@storybook/nextjs/headers.mock': '/dist/export-mocks/headers/index',
'next/navigation': '/dist/export-mocks/navigation/index',
'@storybook/nextjs/navigation.mock': '/dist/export-mocks/navigation/index',
'next/router': '/dist/export-mocks/router/index',
'@storybook/nextjs/router.mock': '/dist/export-mocks/router/index',
'next/cache': '/dist/export-mocks/cache/index',
'@storybook/nextjs/cache.mock': '/dist/export-mocks/cache/index',
...getCompatibilityAliases(),
};

// Utility that assists in adding aliases to the Webpack configuration
// and also doubles as alias solution for portable stories in Jest/Vitest/etc.
export const getPackageAliases = ({ useESM = false }: { useESM?: boolean } = {}) => {
const extension = useESM ? 'mjs' : 'js';
const packageLocation = dirname(require.resolve('@storybook/nextjs/package.json'));
// Use paths for both next/xyz and @storybook/nextjs/xyz imports
// to make sure they all serve the MJS version of the file
const headersPath = join(packageLocation, `/dist/export-mocks/headers/index.${extension}`);
const navigationPath = join(packageLocation, `/dist/export-mocks/navigation/index.${extension}`);
const cachePath = join(packageLocation, `/dist/export-mocks/cache/index.${extension}`);
const routerPath = join(packageLocation, `/dist/export-mocks/router/index.${extension}`);

return {
'next/headers': headersPath,
'@storybook/nextjs/headers.mock': headersPath,
const getFullPath = (path: string) =>
join(packageLocation, path.replace('@storybook/nextjs', ''));

'next/navigation': navigationPath,
'@storybook/nextjs/navigation.mock': navigationPath,
const aliases = Object.fromEntries(
Object.entries(mapping).map(([originalPath, aliasedPath]) => [
originalPath,
// Use paths for both next/xyz and @storybook/nextjs/xyz imports
// to make sure they all serve the MJS/CJS version of the file
getFullPath(`${aliasedPath}.${extension}`),
])
);

'next/router': routerPath,
'@storybook/nextjs/router.mock': routerPath,

'next/cache': cachePath,
'@storybook/nextjs/cache.mock': cachePath,
};
return aliases;
};

export const configureNextExportMocks = (baseConfig: WebpackConfig): void => {
Expand Down
2 changes: 2 additions & 0 deletions code/frameworks/nextjs/src/preset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { configureFastRefresh } from './fastRefresh/webpack';
import { configureAliases } from './aliases/webpack';
import { logger } from '@storybook/node-logger';
import { configureNextExportMocks } from './export-mocks/webpack';
import { configureCompatibilityAliases } from './compatibility/compatibility-map';

export const addons: PresetProperty<'addons'> = [
dirname(require.resolve(join('@storybook/preset-react-webpack', 'package.json'))),
Expand Down Expand Up @@ -135,6 +136,7 @@ export const webpackFinal: StorybookConfig['webpackFinal'] = async (baseConfig,
configureStyledJsx(baseConfig);
configureNodePolyfills(baseConfig);
configureAliases(baseConfig);
configureCompatibilityAliases(baseConfig);
configureNextExportMocks(baseConfig);

if (isDevelopment) {
Expand Down
Loading

0 comments on commit a1ca72d

Please sign in to comment.