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

Colocate styles with special entries #42506

Merged
merged 10 commits into from Nov 8, 2022
Merged
Show file tree
Hide file tree
Changes from 3 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
3 changes: 2 additions & 1 deletion packages/next/build/webpack/loaders/next-app-loader.ts
Expand Up @@ -25,6 +25,7 @@ export type ComponentsType = {
readonly [componentKey in ValueOf<typeof FILE_TYPES>]?: ComponentModule
} & {
readonly layoutOrPagePath?: string
readonly filePath?: string
readonly page?: ComponentModule
}

Expand Down Expand Up @@ -108,7 +109,7 @@ async function createTreeCodeFromPath({
return `${
file === FILE_TYPES.layout
? `layoutOrPagePath: ${JSON.stringify(filePath)},`
: ''
: `filePath: ${JSON.stringify(filePath)},`
shuding marked this conversation as resolved.
Show resolved Hide resolved
}'${file}': () => require(${JSON.stringify(filePath)}),`
})
.join('\n')}
Expand Down
40 changes: 20 additions & 20 deletions packages/next/build/webpack/plugins/flight-client-entry-plugin.ts
Expand Up @@ -179,17 +179,18 @@ export class FlightClientEntryPlugin {
for (const connection of compilation.moduleGraph.getOutgoingConnections(
entryModule
)) {
const layoutOrPageDependency = connection.dependency
const layoutOrPageRequest = connection.dependency.request
// Entry can be any user defined entry files such as layout, page, error, loading, etc.
const entryDependency = connection.dependency
const entryRequest = connection.dependency.request
shuding marked this conversation as resolved.
Show resolved Hide resolved

const [clientComponentImports] =
this.collectClientComponentsAndCSSForDependency({
layoutOrPageRequest,
entryRequest,
compilation,
dependency: layoutOrPageDependency,
dependency: entryDependency,
})

const isAbsoluteRequest = path.isAbsolute(layoutOrPageRequest)
const isAbsoluteRequest = path.isAbsolute(entryRequest)

// Next.js internals are put into a separate entry.
if (!isAbsoluteRequest) {
Expand All @@ -200,8 +201,8 @@ export class FlightClientEntryPlugin {
}

const relativeRequest = isAbsoluteRequest
? path.relative(compilation.options.context, layoutOrPageRequest)
: layoutOrPageRequest
? path.relative(compilation.options.context, entryRequest)
: entryRequest

// Replace file suffix as `.js` will be added.
const bundlePath = relativeRequest.replace(/\.(js|ts)x?$/, '')
Expand Down Expand Up @@ -308,14 +309,14 @@ export class FlightClientEntryPlugin {
for (const connection of compilation.moduleGraph.getOutgoingConnections(
entryModule
)) {
const layoutOrPageDependency = connection.dependency
const layoutOrPageRequest = connection.dependency.request
const entryDependency = connection.dependency
const entryRequest = connection.dependency.request

const [, cssImports] =
this.collectClientComponentsAndCSSForDependency({
layoutOrPageRequest,
entryRequest,
compilation,
dependency: layoutOrPageDependency,
dependency: entryDependency,
clientEntryDependencyMap,
})

Expand Down Expand Up @@ -353,12 +354,12 @@ export class FlightClientEntryPlugin {
}

collectClientComponentsAndCSSForDependency({
layoutOrPageRequest,
entryRequest,
compilation,
dependency,
clientEntryDependencyMap,
}: {
layoutOrPageRequest: string
entryRequest: string
compilation: any
dependency: any /* Dependency */
clientEntryDependencyMap?: Record<string, any>
Expand Down Expand Up @@ -397,15 +398,15 @@ export class FlightClientEntryPlugin {
: mod.resourceResolveData?.path + mod.resourceResolveData?.query

// Ensure module is not walked again if it's already been visited
if (!visitedBySegment[layoutOrPageRequest]) {
visitedBySegment[layoutOrPageRequest] = new Set()
if (!visitedBySegment[entryRequest]) {
visitedBySegment[entryRequest] = new Set()
}
const storeKey =
(inClientComponentBoundary ? '0' : '1') + ':' + modRequest
if (!modRequest || visitedBySegment[layoutOrPageRequest].has(storeKey)) {
if (!modRequest || visitedBySegment[entryRequest].has(storeKey)) {
return
}
visitedBySegment[layoutOrPageRequest].add(storeKey)
visitedBySegment[entryRequest].add(storeKey)

const isClientComponent = isClientComponentModule(mod)

Expand All @@ -425,9 +426,8 @@ export class FlightClientEntryPlugin {
}
}

serverCSSImports[layoutOrPageRequest] =
serverCSSImports[layoutOrPageRequest] || []
serverCSSImports[layoutOrPageRequest].push(modRequest)
serverCSSImports[entryRequest] = serverCSSImports[entryRequest] || []
serverCSSImports[entryRequest].push(modRequest)
}

// Check if request is for css file.
Expand Down
18 changes: 13 additions & 5 deletions packages/next/client/components/error-boundary.tsx
Expand Up @@ -6,6 +6,7 @@ export type ErrorComponent = React.ComponentType<{
}>
interface ErrorBoundaryProps {
errorComponent: ErrorComponent
errorStyles: React.ReactNode | undefined
}

/**
Expand All @@ -32,10 +33,13 @@ class ErrorBoundaryHandler extends React.Component<
render() {
if (this.state.error) {
return (
<this.props.errorComponent
error={this.state.error}
reset={this.reset}
/>
<>
{this.props.errorStyles}
<this.props.errorComponent
error={this.state.error}
reset={this.reset}
/>
</>
)
}

Expand All @@ -49,11 +53,15 @@ class ErrorBoundaryHandler extends React.Component<
*/
export function ErrorBoundary({
errorComponent,
errorStyles,
children,
}: ErrorBoundaryProps & { children: React.ReactNode }): JSX.Element {
if (errorComponent) {
return (
<ErrorBoundaryHandler errorComponent={errorComponent}>
<ErrorBoundaryHandler
errorComponent={errorComponent}
errorStyles={errorStyles}
>
{children}
</ErrorBoundaryHandler>
)
Expand Down
4 changes: 3 additions & 1 deletion packages/next/client/components/layout-router.tsx
Expand Up @@ -375,6 +375,7 @@ export default function OuterLayoutRouter({
segmentPath,
childProp,
error,
errorStyles,
loading,
hasLoading,
template,
Expand All @@ -385,6 +386,7 @@ export default function OuterLayoutRouter({
segmentPath: FlightSegmentPath
childProp: ChildProp
error: ErrorComponent
errorStyles: React.ReactNode | undefined
template: React.ReactNode
loading: React.ReactNode | undefined
hasLoading: boolean
Expand Down Expand Up @@ -442,7 +444,7 @@ export default function OuterLayoutRouter({
<TemplateContext.Provider
key={preservedSegment}
value={
<ErrorBoundary errorComponent={error}>
<ErrorBoundary errorComponent={error} errorStyles={errorStyles}>
<LoadingBoundary hasLoading={hasLoading} loading={loading}>
<NotFoundBoundary notFound={notFound}>
<RedirectBoundary>
Expand Down
80 changes: 75 additions & 5 deletions packages/next/server/app-render.tsx
Expand Up @@ -957,6 +957,54 @@ export async function renderToHTMLOrFlight(

const assetPrefix = renderOpts.assetPrefix || ''

const createFragmentTypeWithStyles = async ({
filePath,
getComponent,
styleOnly,
shouldPreload,
}: {
filePath?: string
getComponent?: () => any
styleOnly?: boolean
shouldPreload?: boolean
}): Promise<any> => {
if (!filePath || (!getComponent && !styleOnly)) return

const styles = getCssInlinedLinkTags(
serverComponentManifest,
serverCSSManifest,
filePath,
serverCSSForEntries
)
const cacheBustingUrlSuffix = dev ? `?ts=${Date.now()}` : ''

const styleElements = styles
? styles.map((href, index) => (
<link
rel="stylesheet"
href={`${assetPrefix}/_next/${href}${cacheBustingUrlSuffix}`}
// @ts-ignore
precedence={shouldPreload ? 'high' : undefined}
key={index}
/>
))
: null

if (styleOnly) {
return styleElements
}

if (!getComponent) return
const Comp = await interopDefault(getComponent())

return () => (
<>
{styleElements}
<Comp />
</>
)
}

/**
* Use the provided loader tree to create the React Component tree.
*/
Expand All @@ -966,6 +1014,7 @@ export async function renderToHTMLOrFlight(
segment,
parallelRoutes,
{
filePath,
layoutOrPagePath,
layout,
template,
Expand Down Expand Up @@ -1002,18 +1051,34 @@ export async function renderToHTMLOrFlight(
serverCSSForEntries,
layoutOrPagePath
)
const Template = template
? await interopDefault(template())
: React.Fragment

const Template =
(await createFragmentTypeWithStyles({
filePath,
getComponent: template,
shouldPreload: true,
})) || React.Fragment
const ErrorComponent = error ? await interopDefault(error()) : undefined
const Loading = loading ? await interopDefault(loading()) : undefined
const errorStyles =
filePath && error
? await createFragmentTypeWithStyles({
filePath,
styleOnly: true,
})
: undefined
shuding marked this conversation as resolved.
Show resolved Hide resolved

const Loading = await createFragmentTypeWithStyles({
filePath,
getComponent: loading,
})
const isLayout = typeof layout !== 'undefined'
const isPage = typeof page !== 'undefined'
const layoutOrPageMod = isLayout
? await layout()
: isPage
? await page()
: undefined

/**
* Checks if the current segment is a root layout.
*/
Expand All @@ -1025,7 +1090,10 @@ export async function renderToHTMLOrFlight(
rootLayoutIncluded || rootLayoutAtThisLevel

const NotFound = notFound
? await interopDefault(notFound())
? await createFragmentTypeWithStyles({
filePath,
getComponent: notFound,
})
: rootLayoutAtThisLevel
? DefaultNotFound
: undefined
Expand Down Expand Up @@ -1141,6 +1209,7 @@ export async function renderToHTMLOrFlight(
loading={Loading ? <Loading /> : undefined}
hasLoading={Boolean(Loading)}
error={ErrorComponent}
errorStyles={errorStyles}
template={
<Template>
<RenderFromTemplateContext />
Expand Down Expand Up @@ -1179,6 +1248,7 @@ export async function renderToHTMLOrFlight(
parallelRouterKey={parallelRouteKey}
segmentPath={segmentPath}
error={ErrorComponent}
errorStyles={errorStyles}
loading={Loading ? <Loading /> : undefined}
// TODO-APP: Add test for loading returning `undefined`. This currently can't be tested as the `webdriver()` tab will wait for the full page to load before returning.
hasLoading={Boolean(Loading)}
Expand Down
4 changes: 3 additions & 1 deletion test/e2e/app-dir/app/app/error/client-component/error.js
@@ -1,10 +1,12 @@
'use client'

import styles from './style.module.css'

export default function ErrorBoundary({ error, reset }) {
return (
<>
<p id="error-boundary-message">An error occurred: {error.message}</p>
<button id="reset" onClick={() => reset()}>
<button id="reset" onClick={() => reset()} className={styles.button}>
Try again
</button>
</>
Expand Down
@@ -0,0 +1,3 @@
.button {
font-size: 50px;
}
@@ -1,3 +1,5 @@
import './style.css'

export default function Loading() {
return <h2>Loading...</h2>
}
@@ -0,0 +1,3 @@
body {
color: red;
}
8 changes: 7 additions & 1 deletion test/e2e/app-dir/app/app/not-found/not-found.js
@@ -1,3 +1,9 @@
import styles from './style.module.css'

export default function NotFound() {
return <h1 id="not-found-component">Not Found!</h1>
return (
<h1 id="not-found-component" className={styles.red}>
Not Found!
</h1>
)
}
3 changes: 3 additions & 0 deletions test/e2e/app-dir/app/app/not-found/style.module.css
@@ -0,0 +1,3 @@
.red {
color: red;
}
@@ -0,0 +1,3 @@
.button {
font-size: 100px;
}
@@ -1,13 +1,16 @@
'use client'

import { useState } from 'react'
import styles from './style.module.css'

export default function Template({ children }) {
const [count, setCount] = useState(0)
return (
<>
<h1>Template {count}</h1>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button className={styles.button} onClick={() => setCount(count + 1)}>
Increment
</button>
{children}
</>
)
Expand Down
@@ -0,0 +1,3 @@
.red {
color: red;
}
@@ -1,7 +1,9 @@
import styles from './style.module.css'

export default function Template({ children }) {
return (
<>
<h1>
<h1 className={styles.red}>
Template <span id="performance-now">{performance.now()}</span>
</h1>
{children}
Expand Down