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

Implement loadable with lazy and suspense for next dynamic #42589

Merged
merged 17 commits into from Dec 7, 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
1 change: 0 additions & 1 deletion packages/next-swc/crates/core/src/lib.rs
Expand Up @@ -185,7 +185,6 @@ where
next_dynamic::next_dynamic(
opts.is_development,
opts.is_server,
opts.server_components.is_some(),
file.name.clone(),
opts.pages_dir.clone()
),
Expand Down
62 changes: 2 additions & 60 deletions packages/next-swc/crates/core/src/next_dynamic.rs
Expand Up @@ -5,26 +5,23 @@ use pathdiff::diff_paths;
use swc_core::{
common::{errors::HANDLER, FileName, DUMMY_SP},
ecma::ast::{
ArrayLit, ArrowExpr, BinExpr, BinaryOp, BlockStmtOrExpr, Bool, CallExpr, Callee, Expr,
ArrayLit, ArrowExpr, BinExpr, BinaryOp, BlockStmtOrExpr, CallExpr, Callee, Expr,
ExprOrSpread, Id, Ident, ImportDecl, ImportSpecifier, KeyValueProp, Lit, MemberExpr,
MemberProp, Null, ObjectLit, Prop, PropName, PropOrSpread, Str, Tpl,
MemberProp, ObjectLit, Prop, PropName, PropOrSpread, Str, Tpl,
},
ecma::atoms::js_word,
ecma::utils::ExprFactory,
ecma::visit::{Fold, FoldWith},
};

pub fn next_dynamic(
is_development: bool,
is_server: bool,
is_server_components: bool,
filename: FileName,
pages_dir: Option<PathBuf>,
) -> impl Fold {
NextDynamicPatcher {
is_development,
is_server,
is_server_components,
pages_dir,
filename,
dynamic_bindings: vec![],
Expand All @@ -37,7 +34,6 @@ pub fn next_dynamic(
struct NextDynamicPatcher {
is_development: bool,
is_server: bool,
is_server_components: bool,
pages_dir: Option<PathBuf>,
filename: FileName,
dynamic_bindings: Vec<Id>,
Expand Down Expand Up @@ -233,70 +229,16 @@ impl Fold for NextDynamicPatcher {
value: generated,
})))];

let mut has_ssr_false = false;
let mut has_suspense = false;

if expr.args.len() == 2 {
if let Expr::Object(ObjectLit {
props: options_props,
..
}) = &*expr.args[1].expr
{
for prop in options_props.iter() {
if let Some(KeyValueProp { key, value }) = match prop {
PropOrSpread::Prop(prop) => match &**prop {
Prop::KeyValue(key_value_prop) => Some(key_value_prop),
_ => None,
},
_ => None,
} {
if let Some(Ident {
sym,
span: _,
optional: _,
}) = match key {
PropName::Ident(ident) => Some(ident),
_ => None,
} {
if sym == "ssr" {
if let Some(Lit::Bool(Bool {
value: false,
span: _,
})) = value.as_lit()
{
has_ssr_false = true
}
}
if sym == "suspense" {
if let Some(Lit::Bool(Bool {
value: true,
span: _,
})) = value.as_lit()
{
has_suspense = true
}
}
}
}
}
props.extend(options_props.iter().cloned());
}
}

// Don't strip the `loader` argument if suspense is true
// See https://github.com/vercel/next.js/issues/36636 for background.

// Also don't strip the `loader` argument for server components (both
// server/client layers), since they're aliased to a
// React.lazy implementation.
if has_ssr_false
&& !has_suspense
&& self.is_server
&& !self.is_server_components
{
expr.args[0] = Lit::Null(Null { span: DUMMY_SP }).as_arg();
}

let second_arg = ExprOrSpread {
spread: None,
expr: Box::new(Expr::Object(ObjectLit {
Expand Down
1 change: 0 additions & 1 deletion packages/next-swc/crates/core/tests/errors.rs
Expand Up @@ -46,7 +46,6 @@ fn next_dynamic_errors(input: PathBuf) {
next_dynamic(
true,
false,
false,
FileName::Real(PathBuf::from("/some-project/src/some-file.js")),
Some("/some-project/src".into()),
)
Expand Down
3 changes: 0 additions & 3 deletions packages/next-swc/crates/core/tests/fixture.rs
Expand Up @@ -49,7 +49,6 @@ fn next_dynamic_fixture(input: PathBuf) {
next_dynamic(
true,
false,
false,
FileName::Real(PathBuf::from("/some-project/src/some-file.js")),
Some("/some-project/src".into()),
)
Expand All @@ -62,7 +61,6 @@ fn next_dynamic_fixture(input: PathBuf) {
syntax(),
&|_tr| {
next_dynamic(
false,
false,
false,
FileName::Real(PathBuf::from("/some-project/src/some-file.js")),
Expand All @@ -79,7 +77,6 @@ fn next_dynamic_fixture(input: PathBuf) {
next_dynamic(
false,
true,
false,
FileName::Real(PathBuf::from("/some-project/src/some-file.js")),
Some("/some-project/src".into()),
)
Expand Down
@@ -1,23 +1,21 @@
import dynamic from 'next/dynamic';
const DynamicComponentWithCustomLoading = dynamic(()=>import('../components/hello')
, {
const DynamicComponentWithCustomLoading = dynamic(()=>import('../components/hello'), {
loadableGenerated: {
modules: [
"some-file.js -> " + "../components/hello"
]
},
loading: ()=><p >...</p>
});
const DynamicClientOnlyComponent = dynamic(null, {
const DynamicClientOnlyComponent = dynamic(()=>import('../components/hello'), {
loadableGenerated: {
modules: [
"some-file.js -> " + "../components/hello"
]
},
ssr: false
});
const DynamicClientOnlyComponentWithSuspense = dynamic(()=>import('../components/hello')
, {
const DynamicClientOnlyComponentWithSuspense = dynamic(()=>import('../components/hello'), {
loadableGenerated: {
modules: [
"some-file.js -> " + "../components/hello"
Expand Down
@@ -1,11 +1,10 @@
import dynamic from 'next/dynamic';
const DynamicComponent = dynamic(null, {
const DynamicComponent = dynamic(()=>handleImport(import('./components/hello')), {
loadableGenerated: {
modules: [
"some-file.js -> " + "./components/hello"
]
},
loading: ()=>null
,
loading: ()=>null,
ssr: false
});
2 changes: 1 addition & 1 deletion packages/next/build/swc/options.js
Expand Up @@ -128,7 +128,7 @@ function getBaseSWCOptions({
? {
isServer: !!isServerLayer,
}
: false,
: undefined,
}
}

Expand Down
18 changes: 0 additions & 18 deletions packages/next/build/webpack-config.ts
Expand Up @@ -1707,26 +1707,8 @@ export default async function getBaseWebpackConfig(
},
]
: []),
// Alias `next/dynamic` to React.lazy implementation for RSC
...(hasServerComponents
? [
{
test: codeCondition.test,
issuerLayer(layer: string) {
return (
layer === WEBPACK_LAYERS.client ||
layer === WEBPACK_LAYERS.server
)
},
resolve: {
alias: {
// Alias `next/dynamic` to React.lazy implementation for RSC
[require.resolve('next/dynamic')]: require.resolve(
'next/dist/client/components/dynamic'
),
},
},
},
{
// Alias react-dom for ReactDOM.preload usage.
// Alias react for switching between default set and share subset.
Expand Down
29 changes: 26 additions & 3 deletions packages/next/client/app-index.tsx
Expand Up @@ -7,6 +7,7 @@ import { createFromReadableStream } from 'next/dist/compiled/react-server-dom-we

import { HeadManagerContext } from '../shared/lib/head-manager-context'
import { GlobalLayoutRouterContext } from '../shared/lib/app-router-context'
import { NEXT_DYNAMIC_NO_SSR_CODE } from '../shared/lib/no-ssr-error'

/// <reference types="react-dom/experimental" />

Expand Down Expand Up @@ -116,6 +117,23 @@ if (document.readyState === 'loading') {
DOMContentLoaded()
}

function onRecoverableError(err: any) {
// Using default react onRecoverableError
// x-ref: https://github.com/facebook/react/blob/d4bc16a7d69eb2ea38a88c8ac0b461d5f72cdcab/packages/react-dom/src/client/ReactDOMRoot.js#L83
const defaultOnRecoverableError =
typeof reportError === 'function'
? // In modern browsers, reportError will dispatch an error event,
// emulating an uncaught JavaScript error.
reportError
: (error: any) => {
window.console.error(error)
}

// Skip certain custom errors which are not expected to be reported on client
if (err.digest === NEXT_DYNAMIC_NO_SSR_CODE) return
defaultOnRecoverableError(err)
}

const nextServerDataLoadingGlobal = ((self as any).__next_f =
(self as any).__next_f || [])
nextServerDataLoadingGlobal.forEach(nextServerDataCallback)
Expand Down Expand Up @@ -193,7 +211,9 @@ export function hydrate() {
if (rootLayoutMissingTagsError) {
const reactRootElement = document.createElement('div')
document.body.appendChild(reactRootElement)
const reactRoot = (ReactDOMClient as any).createRoot(reactRootElement)
const reactRoot = (ReactDOMClient as any).createRoot(reactRootElement, {
onRecoverableError,
})

reactRoot.render(
<GlobalLayoutRouterContext.Provider
Expand Down Expand Up @@ -234,11 +254,14 @@ export function hydrate() {
</StrictModeIfEnabled>
)

const options = {
onRecoverableError,
}
const isError = document.documentElement.id === '__next_error__'
const reactRoot = isError
? (ReactDOMClient as any).createRoot(appElement)
? (ReactDOMClient as any).createRoot(appElement, options)
: (React as any).startTransition(() =>
(ReactDOMClient as any).hydrateRoot(appElement, reactEl)
(ReactDOMClient as any).hydrateRoot(appElement, reactEl, options)
)
if (isError) {
reactRoot.render(reactEl)
Expand Down
19 changes: 0 additions & 19 deletions packages/next/client/components/dynamic.tsx

This file was deleted.

4 changes: 2 additions & 2 deletions packages/next/client/components/layout-router.tsx
Expand Up @@ -359,7 +359,7 @@ class RedirectErrorBoundary extends React.Component<
}

static getDerivedStateFromError(error: any) {
if (error.digest?.startsWith('NEXT_REDIRECT')) {
if (error?.digest?.startsWith('NEXT_REDIRECT')) {
const url = error.digest.split(';')[1]
return { redirect: url }
}
Expand Down Expand Up @@ -400,7 +400,7 @@ class NotFoundErrorBoundary extends React.Component<
}

static getDerivedStateFromError(error: any) {
if (error.digest === 'NEXT_NOT_FOUND') {
if (error?.digest === 'NEXT_NOT_FOUND') {
return { notFoundTriggered: true }
}
// Re-throw if error is not for 404
Expand Down
9 changes: 8 additions & 1 deletion packages/next/client/index.tsx
Expand Up @@ -43,6 +43,7 @@ import {
PathnameContextProviderAdapter,
} from '../shared/lib/router/adapters'
import { SearchParamsContext } from '../shared/lib/hooks-client-context'
import { NEXT_DYNAMIC_NO_SSR_CODE } from '../shared/lib/no-ssr-error'

/// <reference types="react-dom/experimental" />

Expand Down Expand Up @@ -510,7 +511,13 @@ function renderReactElement(
const reactEl = fn(shouldHydrate ? markHydrateComplete : markRenderComplete)
if (!reactRoot) {
// Unlike with createRoot, you don't need a separate root.render() call here
reactRoot = ReactDOM.hydrateRoot(domEl, reactEl)
reactRoot = ReactDOM.hydrateRoot(domEl, reactEl, {
onRecoverableError(err: any) {
// Skip certain custom errors which are not expected to throw on client
if (err.message === NEXT_DYNAMIC_NO_SSR_CODE) return
throw err
},
})
// TODO: Remove shouldHydrate variable when React 18 is stable as it can depend on `reactRoot` existing
shouldHydrate = false
} else {
Expand Down