Skip to content

Commit

Permalink
Leverage mini css plugin hmr for app dir (#38830)
Browse files Browse the repository at this point in the history
Co-authored-by: Tim Neutkens <[email protected]>
  • Loading branch information
huozhi and timneutkens authored Jul 21, 2022
1 parent 5752d4a commit 7921b67
Show file tree
Hide file tree
Showing 13 changed files with 74 additions and 276 deletions.
11 changes: 8 additions & 3 deletions packages/next/build/webpack/config/blocks/css/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -470,16 +470,21 @@ export const css = curry(async function css(
)
}

if (ctx.isClient && ctx.isProduction) {
// Enable full mini-css-extract-plugin hmr for prod mode pages or app dir
if (ctx.isClient && (ctx.isProduction || ctx.experimental.appDir)) {
// Extract CSS as CSS file(s) in the client-side production bundle.
const MiniCssExtractPlugin =
require('../../../plugins/mini-css-extract-plugin').default
fns.push(
plugin(
// @ts-ignore webpack 5 compat
new MiniCssExtractPlugin({
filename: 'static/css/[contenthash].css',
chunkFilename: 'static/css/[contenthash].css',
filename: ctx.isProduction
? 'static/css/[contenthash].css'
: 'static/css/[name].css',
chunkFilename: ctx.isProduction
? 'static/css/[contenthash].css'
: 'static/css/[name].css',
// Next.js guarantees that CSS order "doesn't matter", due to imposed
// restrictions:
// 1. Global CSS can only be defined in a single entrypoint (_app)
Expand Down
45 changes: 19 additions & 26 deletions packages/next/build/webpack/config/blocks/css/loaders/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,37 +9,30 @@ export function getClientStyleLoader({
isDevelopment: boolean
assetPrefix: string
}): webpack.RuleSetUseItem {
if (isDevelopment) {
// Keep next-style-loader for development mode in `pages/`
if (isDevelopment && !isAppDir) {
return {
loader: 'next-style-loader',
options: {
insert: isAppDir
? function (element: Node) {
// There is currently no anchor element in <head>.
// We temporarily insert the element as the last child
// of the first <head>.
const head = document.querySelector('head')!
head.insertBefore(element, head.lastChild)
}
: function (element: Node) {
// By default, style-loader injects CSS into the bottom
// of <head>. This causes ordering problems between dev
// and prod. To fix this, we render a <noscript> tag as
// an anchor for the styles to be placed before. These
// styles will be applied _before_ <style jsx global>.
insert: function (element: Node) {
// By default, style-loader injects CSS into the bottom
// of <head>. This causes ordering problems between dev
// and prod. To fix this, we render a <noscript> tag as
// an anchor for the styles to be placed before. These
// styles will be applied _before_ <style jsx global>.

// These elements should always exist. If they do not,
// this code should fail.
var anchorElement = document.querySelector(
'#__next_css__DO_NOT_USE__'
)!
var parentNode = anchorElement.parentNode! // Normally <head>
// These elements should always exist. If they do not,
// this code should fail.
var anchorElement = document.querySelector(
'#__next_css__DO_NOT_USE__'
)!
var parentNode = anchorElement.parentNode! // Normally <head>

// Each style tag should be placed right before our
// anchor. By inserting before and not after, we do not
// need to track the last inserted element.
parentNode.insertBefore(element, anchorElement)
},
// Each style tag should be placed right before our
// anchor. By inserting before and not after, we do not
// need to track the last inserted element.
parentNode.insertBefore(element, anchorElement)
},
},
}
}
Expand Down
46 changes: 11 additions & 35 deletions packages/next/build/webpack/plugins/flight-manifest-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,44 +119,20 @@ export class FlightManifestPlugin {

if (isCSSModule) {
if (!manifest[resource]) {
if (dev) {
const chunkIdNameMapping = (chunk.ids || []).map((chunkId) => {
return (
chunkId +
':' +
(chunk.name || chunk.id) +
(dev ? '' : '-' + chunk.hash)
)
})
manifest[resource] = {
default: {
id,
name: 'default',
chunks: chunkIdNameMapping,
},
}
moduleIdMapping[id]['default'] = {
id: ssrNamedModuleId,
name: 'default',
chunks: chunkIdNameMapping,
}
manifest.__ssr_module_mapping__ = moduleIdMapping
} else {
const chunks = [...chunk.files].filter((f) => f.endsWith('.css'))
manifest[resource] = {
default: {
id,
name: 'default',
chunks,
},
}
moduleIdMapping[id]['default'] = {
id: ssrNamedModuleId,
const chunks = [...chunk.files].filter((f) => f.endsWith('.css'))
manifest[resource] = {
default: {
id,
name: 'default',
chunks,
}
manifest.__ssr_module_mapping__ = moduleIdMapping
},
}
moduleIdMapping[id]['default'] = {
id: ssrNamedModuleId,
name: 'default',
chunks,
}
manifest.__ssr_module_mapping__ = moduleIdMapping
}
return
}
Expand Down
92 changes: 9 additions & 83 deletions packages/next/client/app-index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,16 @@ self.__next_require__ = __webpack_require__
;(self as any).__next_chunk_load__ = (chunk: string) => {
if (!chunk) return Promise.resolve()
if (chunk.endsWith('.css')) {
const existingTag = document.querySelector(`link[href="${chunk}"]`)
const chunkPath = `/_next/${chunk}`
const existingTag = document.querySelector(`link[href="${chunkPath}"]`)
if (!existingTag) {
const link = document.createElement('link')
link.rel = 'stylesheet'
link.href = '/_next/' + chunk
link.href = chunkPath
document.head.appendChild(link)
}
return Promise.resolve()
}

const [chunkId, chunkFileName] = chunk.split(':')
chunkFilenameMap[chunkId] = `static/chunks/${chunkFileName}.js`

Expand Down Expand Up @@ -147,73 +147,7 @@ function createResponseCache() {
}
const rscCache = createResponseCache()

async function loadCss(cssChunkInfoJson: string) {
const data = JSON.parse(cssChunkInfoJson)
await Promise.all(
data.chunks.map((chunkId: string) => {
// load css related chunks
return (self as any).__next_chunk_load__(chunkId)
})
)
// In development mode, import css in dev when it's wrapped by style loader.
// In production mode, css are standalone chunk that doesn't need to be imported.
if (data.id) {
return (self as any).__next_require__(data.id)
}

return Promise.resolve()
}

function createLoadFlightCssStream(onFlightCssLoaded: () => void) {
const promises: Promise<any>[] = []
let cssFlushed = false

const loadCssFromStreamData = (data: string) => {
if (data.startsWith('CSS')) {
const cssJson = data.slice(4).trim()
promises.push(loadCss(cssJson))
}
}

// TODO-APP: Refine the buffering code here to make it more correct.
let buffer = ''
const loadCssFromFlight = new TransformStream({
transform(chunk, controller) {
const process = (buf: string) => {
if (buf) {
if (buf.startsWith('CSS:')) {
loadCssFromStreamData(buf)
} else {
controller.enqueue(new TextEncoder().encode(buf))

if (!cssFlushed) {
cssFlushed = true
Promise.all(promises).then(() => onFlightCssLoaded())
}
}
}
}

const data = new TextDecoder().decode(chunk)
buffer += data
let index
while ((index = buffer.indexOf('\n')) !== -1) {
const line = buffer.slice(0, index + 1)
buffer = buffer.slice(index + 1)
process(line)
}
process(buffer)
buffer = ''
},
})

return loadCssFromFlight
}

function useInitialServerResponse(
cacheKey: string,
onFlightCssLoaded: () => void
) {
function useInitialServerResponse(cacheKey: string) {
const response = rscCache.get(cacheKey)
if (response) return response

Expand All @@ -223,25 +157,17 @@ function useInitialServerResponse(
},
})

const newResponse = createFromReadableStream(
readable.pipeThrough(createLoadFlightCssStream(onFlightCssLoaded))
)
const newResponse = createFromReadableStream(readable)

rscCache.set(cacheKey, newResponse)
return newResponse
}

function ServerRoot({
cacheKey,
onFlightCssLoaded,
}: {
cacheKey: string
onFlightCssLoaded: () => Promise<void>
}) {
function ServerRoot({ cacheKey }: { cacheKey: string }) {
React.useEffect(() => {
rscCache.delete(cacheKey)
})
const response = useInitialServerResponse(cacheKey, onFlightCssLoaded)
const response = useInitialServerResponse(cacheKey)
const root = response.readRoot()
return root
}
Expand All @@ -266,11 +192,11 @@ function RSCComponent(props: any) {
return <ServerRoot {...props} cacheKey={cacheKey} />
}

export function hydrate(opts?: { onFlightCssLoaded?: () => Promise<void> }) {
export function hydrate() {
renderReactElement(appElement!, () => (
<React.StrictMode>
<Root>
<RSCComponent onFlightCssLoaded={opts?.onFlightCssLoaded} />
<RSCComponent />
</Root>
</React.StrictMode>
))
Expand Down
5 changes: 1 addition & 4 deletions packages/next/client/app-next-dev.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import { hydrate, version } from './app-index'
import { displayContent } from './dev/fouc'

// TODO-APP: implement FOUC guard

// TODO-APP: hydration warning

Expand All @@ -10,6 +7,6 @@ window.next = {
appDir: true,
}

hydrate({ onFlightCssLoaded: displayContent })
hydrate()

// TODO-APP: build indicator
50 changes: 1 addition & 49 deletions packages/next/client/components/app-router.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,27 +18,6 @@ import {
// LayoutSegmentsContext,
} from './hooks-client-context'

async function loadCss(cssChunkInfoJson: string) {
const data = JSON.parse(cssChunkInfoJson)
await Promise.all(
data.chunks.map((chunkId: string) => {
// load css related chunks
return (self as any).__next_chunk_load__(chunkId)
})
)
// In development mode, import css in dev when it's wrapped by style loader.
// In production mode, css are standalone chunk that doesn't need to be imported.
if (data.id) {
;(self as any).__next_require__(data.id)
}
}

const loadCssFromStreamData = (data: string) => {
if (data.startsWith('CSS:')) {
loadCss(data.slice(4).trim())
}
}

function fetchFlight(url: URL, flightRouterStateData: string): ReadableStream {
const flightUrl = new URL(url)
const searchParams = flightUrl.searchParams
Expand All @@ -47,35 +26,8 @@ function fetchFlight(url: URL, flightRouterStateData: string): ReadableStream {

const { readable, writable } = new TransformStream()

// TODO-APP: Refine the buffering code here to make it more correct.
let buffer = ''
const loadCssFromFlight = new TransformStream({
transform(chunk, controller) {
const process = (buf: string) => {
if (buf) {
if (buf.startsWith('CSS:')) {
loadCssFromStreamData(buf)
} else {
controller.enqueue(new TextEncoder().encode(buf))
}
}
}

const data = new TextDecoder().decode(chunk)
buffer += data
let index
while ((index = buffer.indexOf('\n')) !== -1) {
const line = buffer.slice(0, index + 1)
buffer = buffer.slice(index + 1)
process(line)
}
process(buffer)
buffer = ''
},
})

fetch(flightUrl.toString()).then((res) => {
res.body?.pipeThrough(loadCssFromFlight).pipeTo(writable)
res.body?.pipeTo(writable)
})

return readable
Expand Down
Loading

0 comments on commit 7921b67

Please sign in to comment.