Skip to content

Commit

Permalink
merge canary
Browse files Browse the repository at this point in the history
  • Loading branch information
huozhi committed Jul 12, 2022
2 parents 4280fcc + f0d5619 commit e3f7179
Show file tree
Hide file tree
Showing 7 changed files with 136 additions and 59 deletions.
2 changes: 1 addition & 1 deletion docs/api-reference/next/future/image.md
Expand Up @@ -15,7 +15,7 @@ description: Try the latest Image Optimization with the experimental `next/futur

The `next/future/image` component is an experiment to improve both the performance and developer experience of `next/image` by using the native `<img>` element with better default behavior.

This new component is considered experimental and therefore not covered by semver, and may cause unexpected or broken application behavior. This component uses web native [lazy loading](https://caniuse.com/loading-lazy-attr) and [`aspect-ratio`](https://caniuse.com/mdn-css_properties_aspect-ratio), which currently isn't supported in IE11 or before Safari 15.4.
This new component is considered experimental and therefore not covered by semver, and may cause unexpected or broken application behavior. This component uses browser native [lazy loading](https://caniuse.com/loading-lazy-attr), which may fallback to eager loading for older browsers before Safari 15.4. When using the blur-up placeholder, older browsers before Safari 12 will fallback to empty placeholder. When using styles with `width`/`height` of `auto`, it is possible to cause [Layout Shift](https://web.dev/cls/) on older browsers before [Chrome 79](https://chromestatus.com/feature/5695266130755584), [Firefox 69](https://bugzilla.mozilla.org/show_bug.cgi?id=1547231), and [Safari 14.2](https://bugs.webkit.org/show_bug.cgi?id=201641). For more details, see [this MDN video](https://www.youtube.com/watch?v=4-d_SoCHeWE).

To use `next/future/image`, add the following to your `next.config.js` file:

Expand Down
34 changes: 21 additions & 13 deletions packages/next/client/app-index.tsx
Expand Up @@ -32,10 +32,13 @@ self.__next_require__ = __webpack_require__
// eslint-disable-next-line no-undef
;(self as any).__next_chunk_load__ = (chunk: string) => {
if (chunk.endsWith('.css')) {
const link = document.createElement('link')
link.rel = 'stylesheet'
link.href = '/_next/' + chunk
document.head.appendChild(link)
const existingTag = document.querySelector(`link[href="${chunk}"]`)
if (!existingTag) {
const link = document.createElement('link')
link.rel = 'stylesheet'
link.href = '/_next/' + chunk
document.head.appendChild(link)
}
return Promise.resolve()
}

Expand Down Expand Up @@ -158,33 +161,38 @@ async function loadCss(cssChunkInfoJson: string) {
return (self as any).__next_require__(data.id)
}

return Promise.resolve(1)
return Promise.resolve()
}

function createLoadFlightCssStream(callback?: () => Promise<void>) {
const cssLoadingPromises: Promise<any>[] = []
const loadCssFromStreamData = (data: string) => {
const seg = data.split(':')
if (seg[0] === 'CSS') {
const cssJson = seg.slice(1).join(':')
if (data.startsWith('CSS')) {
const cssJson = data.slice(4).trim()
if (!loadedCss.has(cssJson)) cssLoadingPromises.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 data = new TextDecoder().decode(chunk)
buffer += data
let index
while ((index = buffer.indexOf('\n')) !== -1) {
const line = buffer.slice(0, index)
const line = buffer.slice(0, index + 1)
buffer = buffer.slice(index + 1)
loadCssFromStreamData(line)
if (line.startsWith('CSS:')) {
loadCssFromStreamData(line)
} else {
controller.enqueue(new TextEncoder().encode(line))
}
}
loadCssFromStreamData(buffer)
if (!data.startsWith('CSS:')) {
controller.enqueue(chunk)

if (buffer && !buffer.startsWith('CSS:')) {
controller.enqueue(new TextEncoder().encode(buffer))
buffer = ''
}
},
})
Expand Down
68 changes: 61 additions & 7 deletions packages/next/client/components/app-router.client.tsx
@@ -1,5 +1,5 @@
import React, { useEffect } from 'react'
import { createFromFetch } from 'next/dist/compiled/react-server-dom-webpack'
import { createFromReadableStream } from 'next/dist/compiled/react-server-dom-webpack'
import {
AppRouterContext,
AppTreeContext,
Expand All @@ -16,24 +16,75 @@ import {
// LayoutSegmentsContext,
} from './hooks-client-context'

function fetchFlight(
url: URL,
flightRouterStateData: string
): Promise<Response> {
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
searchParams.append('__flight__', '1')
searchParams.append('__flight_router_state_tree__', flightRouterStateData)

return fetch(flightUrl.toString())
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 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)

if (line.startsWith('CSS:')) {
loadCssFromStreamData(line)
} else {
controller.enqueue(new TextEncoder().encode(line))
}
}
if (buffer && !buffer.startsWith('CSS:')) {
controller.enqueue(new TextEncoder().encode(buffer))
buffer = ''
}
},
flush() {
loadCssFromStreamData(buffer)
},
})

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

return readable
}

export function fetchServerResponse(
url: URL,
flightRouterState: FlightRouterState
): { readRoot: () => FlightData } {
const flightRouterStateData = JSON.stringify(flightRouterState)
return createFromFetch(fetchFlight(url, flightRouterStateData))
return createFromReadableStream(fetchFlight(url, flightRouterStateData))
}

function ErrorOverlay({
Expand All @@ -56,11 +107,13 @@ let initialParallelRoutes: CacheNode['parallelRoutes'] =
export default function AppRouter({
initialTree,
initialCanonicalUrl,
initialStylesheets,
children,
hotReloader,
}: {
initialTree: FlightRouterState
initialCanonicalUrl: string
initialStylesheets: string[]
children: React.ReactNode
hotReloader?: React.ReactNode
}) {
Expand Down Expand Up @@ -259,6 +312,7 @@ export default function AppRouter({
// Root node always has `url`
// Provided in AppTreeContext to ensure it can be overwritten in layout-router
url: canonicalUrl,
stylesheets: initialStylesheets,
}}
>
<ErrorOverlay>{cache.subTreeData}</ErrorOverlay>
Expand Down
7 changes: 6 additions & 1 deletion packages/next/client/components/layout-router.client.tsx
Expand Up @@ -235,7 +235,7 @@ export default function OuterLayoutRouter({
childProp: ChildProp
loading: React.ReactNode | undefined
}) {
const { childNodes, tree, url } = useContext(AppTreeContext)
const { childNodes, tree, url, stylesheets } = useContext(AppTreeContext)

let childNodesForParallelRouter = childNodes.get(parallelRouterKey)
if (!childNodesForParallelRouter) {
Expand All @@ -256,6 +256,11 @@ export default function OuterLayoutRouter({

return (
<>
{stylesheets
? stylesheets.map((href) => (
<link rel="stylesheet" href={`/_next/${href}`} key={href} />
))
: null}
{preservedSegments.map((preservedSegment) => {
return (
<LoadingBoundary loading={loading} key={preservedSegment}>
Expand Down
49 changes: 31 additions & 18 deletions packages/next/server/app-render.tsx
Expand Up @@ -191,11 +191,7 @@ function createServerComponentRenderer(
globalThis.__next_chunk_load__ = () => Promise.resolve()
}

const cssFlightData = getCssFlightData(
ComponentMod,
serverComponentManifest,
dev
)
const cssFlightData = getCssFlightData(ComponentMod, serverComponentManifest)

let RSCStream: ReadableStream<Uint8Array>
const createRSCStream = () => {
Expand All @@ -212,7 +208,7 @@ function createServerComponentRenderer(
}

const writable = transformStream.writable
const ServerComponentWrapper = () => {
return function ServerComponentWrapper() {
const reqStream = createRSCStream()
const response = useFlightResponse(
writable,
Expand All @@ -221,11 +217,8 @@ function createServerComponentRenderer(
serverComponentManifest,
cssFlightData
)
const root = response.readRoot()
return root
return response.readRoot()
}

return ServerComponentWrapper
}

type DynamicParamTypes = 'catchall' | 'optional-catchall' | 'dynamic'
Expand Down Expand Up @@ -330,22 +323,37 @@ function getSegmentParam(segment: string): {
return null
}

function getCssFlightData(
function getCSSInlinedLinkTags(
ComponentMod: any,
serverComponentManifest: any,
dev: boolean
) {
if (dev) return []

const importedServerCSSFiles: string[] =
ComponentMod.__client__?.__next_rsc_css__ || []

return Array.from(
new Set(
importedServerCSSFiles
.map((css) =>
css.endsWith('.css')
? serverComponentManifest[css].default.chunks
: []
)
.flat()
)
)
}

function getCssFlightData(ComponentMod: any, serverComponentManifest: any) {
const importedServerCSSFiles: string[] =
ComponentMod.__client__?.__next_rsc_css__ || []

const cssFiles = importedServerCSSFiles.map(
(css) => serverComponentManifest[css].default
)

if (dev) {
return cssFiles.map((css) => `CSS:${JSON.stringify(css)}\n`).join('')
}

// Multiple css chunks could be merged into one by mini-css-extract-plugin,
// we use a set here to dedupe the css chunks in production.
const cssSet: Set<string> = cssFiles.reduce((res, css) => {
Expand Down Expand Up @@ -773,8 +781,7 @@ export async function renderToHTML(

const cssFlightData = getCssFlightData(
ComponentMod,
serverComponentManifest,
dev
serverComponentManifest
)
const flightData: FlightData = [
// TODO-APP: change walk to output without ''
Expand All @@ -800,6 +807,12 @@ export async function renderToHTML(
// /blog/[slug] /blog/hello-world -> ['children', 'blog', 'children', ['slug', 'hello-world']]
const initialTree = createFlightRouterStateFromLoaderTree(tree)

const initialStylesheets: string[] = getCSSInlinedLinkTags(
ComponentMod,
serverComponentManifest,
dev
)

const { Component: ComponentTree } = createComponentTree({
createSegmentPath: (child) => child,
tree,
Expand All @@ -825,6 +838,7 @@ export async function renderToHTML(
hotReloader={HotReloader && <HotReloader assetPrefix="" />}
initialCanonicalUrl={initialCanonicalUrl}
initialTree={initialTree}
initialStylesheets={initialStylesheets}
>
<ComponentTree />
</AppRouter>
Expand Down Expand Up @@ -905,7 +919,6 @@ export async function renderToHTML(

return await continueFromInitialStream(renderStream, {
dev,
suffix: '',
dataStream: serverComponentsInlinedTransformStream?.readable,
generateStaticHTML: generateStaticHTML || !hasConcurrentFeatures,
flushEffectHandler,
Expand Down
34 changes: 15 additions & 19 deletions packages/next/server/node-web-streams-helper.ts
Expand Up @@ -201,7 +201,7 @@ export async function continueFromInitialStream(
const transforms: Array<TransformStream<Uint8Array, Uint8Array>> = [
createBufferedTransformStream(),
flushEffectHandler ? createFlushEffectStream(flushEffectHandler) : null,
suffixUnclosed != null ? createBufferedSuffix(suffixUnclosed) : null,
suffixUnclosed != null ? createDeferredSuffixStream(suffixUnclosed) : null,
dataStream ? createInlineDataStream(dataStream) : null,
suffixUnclosed != null ? createSuffixStream(closeTag) : null,
dev ? createDevScriptTransformStream() : null,
Expand All @@ -213,45 +213,41 @@ export async function continueFromInitialStream(
)
}

export function createPrefixStream(
prefix: string
export function createSuffixStream(
suffix: string
): TransformStream<Uint8Array, Uint8Array> {
let prefixFlushed = false
return new TransformStream({
transform(chunk, controller) {
if (!prefixFlushed) {
prefixFlushed = true
controller.enqueue(encodeText(prefix))
}
controller.enqueue(chunk)
},
flush(controller) {
if (!prefixFlushed) {
controller.enqueue(encodeText(prefix))
if (suffix) {
controller.enqueue(encodeText(suffix))
}
},
})
}

export function createSuffixStream(
suffix: string
export function createPrefixStream(
prefix: string
): TransformStream<Uint8Array, Uint8Array> {
let prefixFlushed = false
return new TransformStream({
flush(controller) {
if (suffix) {
controller.enqueue(encodeText(suffix))
transform(chunk, controller) {
if (!prefixFlushed && prefix) {
prefixFlushed = true
controller.enqueue(encodeText(prefix))
}
controller.enqueue(chunk)
},
})
}

// Suffix after main body content - scripts before </body>,
// but wait for the major chunks to be enqueued.
export function createBufferedSuffix(
export function createDeferredSuffixStream(
suffix: string
): TransformStream<Uint8Array, Uint8Array> {
let suffixFlushed = false
let suffixFlushTask: Promise<void> | null = null

return new TransformStream({
transform(chunk, controller) {
controller.enqueue(chunk)
Expand Down

0 comments on commit e3f7179

Please sign in to comment.