Skip to content

Commit

Permalink
Merge branch 'canary' into fix-next-node-buildin-module-error-message
Browse files Browse the repository at this point in the history
  • Loading branch information
kodiakhq[bot] authored May 2, 2022
2 parents 3d3f658 + c905fda commit 242ca9a
Show file tree
Hide file tree
Showing 38 changed files with 781 additions and 32 deletions.
7 changes: 5 additions & 2 deletions packages/next/build/entries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { __ApiPreviewProps } from '../server/api-utils'
import { isTargetLikeServerless } from '../server/utils'
import { warn } from './output/log'
import { parse } from '../build/swc'
import { isFlightPage, withoutRSCExtensions } from './utils'
import { isServerComponentPage, withoutRSCExtensions } from './utils'
import { normalizePathSep } from '../shared/lib/page-path/normalize-path-sep'
import { normalizePagePath } from '../shared/lib/page-path/normalize-page-path'

Expand Down Expand Up @@ -251,7 +251,10 @@ export function getEdgeServerEntry(opts: {
absolutePagePath: opts.absolutePagePath,
buildId: opts.buildId,
dev: opts.isDev,
isServerComponent: isFlightPage(opts.config, opts.absolutePagePath),
isServerComponent: isServerComponentPage(
opts.config,
opts.absolutePagePath
),
page: opts.page,
stringifiedConfig: JSON.stringify(opts.config),
}
Expand Down
12 changes: 8 additions & 4 deletions packages/next/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ import {
copyTracedFiles,
isReservedPage,
isCustomErrorPage,
isFlightPage,
isServerComponentPage,
} from './utils'
import getBaseWebpackConfig from './webpack-config'
import { PagesManifest } from './webpack/plugins/pages-manifest-plugin'
Expand Down Expand Up @@ -206,7 +206,11 @@ export default async function build(
setGlobal('telemetry', telemetry)

const publicDir = path.join(dir, 'public')
const pagesDir = findPagesDir(dir)
const { pages: pagesDir, root: rootDir } = findPagesDir(
dir,
config.experimental.rootDir
)

const hasPublicDir = await fileExists(publicDir)

telemetry.record(
Expand Down Expand Up @@ -240,7 +244,7 @@ export default async function build(
.traceAsyncFn(() =>
verifyTypeScriptSetup(
dir,
pagesDir,
[pagesDir, rootDir].filter(Boolean) as string[],
!ignoreTypeScriptErrors,
config,
cacheDir
Expand Down Expand Up @@ -997,7 +1001,7 @@ export default async function build(
: undefined

if (hasServerComponents && pagePath) {
if (isFlightPage(config, pagePath)) {
if (isServerComponentPage(config, pagePath)) {
isServerComponent = true
}
}
Expand Down
4 changes: 2 additions & 2 deletions packages/next/build/jest/jest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,11 @@ export default function nextJest(options: { dir?: string } = {}) {
let jsConfig
let resolvedBaseUrl
let isEsmProject = false
let pagesDir
let pagesDir: string | undefined

if (options.dir) {
const resolvedDir = resolve(options.dir)
pagesDir = findPagesDir(resolvedDir)
pagesDir = findPagesDir(resolvedDir).pages
const packageConfig = loadClosestPackageJson(resolvedDir)
isEsmProject = packageConfig.type === 'module'

Expand Down
2 changes: 1 addition & 1 deletion packages/next/build/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1108,7 +1108,7 @@ export function withoutRSCExtensions(pageExtensions: string[]): string[] {
)
}

export function isFlightPage(
export function isServerComponentPage(
nextConfig: NextConfigComplete,
filePath: string
): boolean {
Expand Down
205 changes: 205 additions & 0 deletions packages/next/client/root-index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
/* global location */
import '../build/polyfills/polyfill-module'
// @ts-ignore react-dom/client exists when using React 18
import ReactDOMClient from 'react-dom/client'
// @ts-ignore startTransition exists when using React 18
import React, { useState, startTransition } from 'react'
import { RefreshContext } from './streaming/refresh'
import { createFromFetch } from 'next/dist/compiled/react-server-dom-webpack'

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

export const version = process.env.__NEXT_VERSION

const appElement: HTMLElement | Document | null = document

let reactRoot: any = null

function renderReactElement(
domEl: HTMLElement | Document,
fn: () => JSX.Element
): void {
const reactEl = fn()
if (!reactRoot) {
// Unlike with createRoot, you don't need a separate root.render() call here
reactRoot = (ReactDOMClient as any).hydrateRoot(domEl, reactEl)
} else {
reactRoot.render(reactEl)
}
}

const getCacheKey = () => {
const { pathname, search } = location
return pathname + search
}

const encoder = new TextEncoder()

let initialServerDataBuffer: string[] | undefined = undefined
let initialServerDataWriter: WritableStreamDefaultWriter | undefined = undefined
let initialServerDataLoaded = false
let initialServerDataFlushed = false

function nextServerDataCallback(seg: [number, string, string]) {
if (seg[0] === 0) {
initialServerDataBuffer = []
} else {
if (!initialServerDataBuffer)
throw new Error('Unexpected server data: missing bootstrap script.')

if (initialServerDataWriter) {
initialServerDataWriter.write(encoder.encode(seg[2]))
} else {
initialServerDataBuffer.push(seg[2])
}
}
}

// There might be race conditions between `nextServerDataRegisterWriter` and
// `DOMContentLoaded`. The former will be called when React starts to hydrate
// the root, the latter will be called when the DOM is fully loaded.
// For streaming, the former is called first due to partial hydration.
// For non-streaming, the latter can be called first.
// Hence, we use two variables `initialServerDataLoaded` and
// `initialServerDataFlushed` to make sure the writer will be closed and
// `initialServerDataBuffer` will be cleared in the right time.
function nextServerDataRegisterWriter(writer: WritableStreamDefaultWriter) {
if (initialServerDataBuffer) {
initialServerDataBuffer.forEach((val) => {
writer.write(encoder.encode(val))
})
if (initialServerDataLoaded && !initialServerDataFlushed) {
writer.close()
initialServerDataFlushed = true
initialServerDataBuffer = undefined
}
}

initialServerDataWriter = writer
}

// When `DOMContentLoaded`, we can close all pending writers to finish hydration.
const DOMContentLoaded = function () {
if (initialServerDataWriter && !initialServerDataFlushed) {
initialServerDataWriter.close()
initialServerDataFlushed = true
initialServerDataBuffer = undefined
}
initialServerDataLoaded = true
}
// It's possible that the DOM is already loaded.
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', DOMContentLoaded, false)
} else {
DOMContentLoaded()
}

const nextServerDataLoadingGlobal = ((self as any).__next_s =
(self as any).__next_s || [])
nextServerDataLoadingGlobal.forEach(nextServerDataCallback)
nextServerDataLoadingGlobal.push = nextServerDataCallback

function createResponseCache() {
return new Map<string, any>()
}
const rscCache = createResponseCache()

function fetchFlight(href: string, props?: any) {
const url = new URL(href, location.origin)
const searchParams = url.searchParams
searchParams.append('__flight__', '1')
if (props) {
searchParams.append('__props__', JSON.stringify(props))
}
return fetch(url.toString())
}

function useServerResponse(cacheKey: string, serialized?: string) {
let response = rscCache.get(cacheKey)
if (response) return response

if (initialServerDataBuffer) {
const t = new TransformStream()
const writer = t.writable.getWriter()
response = createFromFetch(Promise.resolve({ body: t.readable }))
nextServerDataRegisterWriter(writer)
} else {
const fetchPromise = serialized
? (() => {
const t = new TransformStream()
const writer = t.writable.getWriter()
writer.ready.then(() => {
writer.write(new TextEncoder().encode(serialized))
})
return Promise.resolve({ body: t.readable })
})()
: fetchFlight(getCacheKey())
response = createFromFetch(fetchPromise)
}

rscCache.set(cacheKey, response)
return response
}

const ServerRoot = ({
cacheKey,
serialized,
}: {
cacheKey: string
serialized?: string
}) => {
React.useEffect(() => {
rscCache.delete(cacheKey)
})
const response = useServerResponse(cacheKey, serialized)
const root = response.readRoot()
return root
}

function Root({ children }: React.PropsWithChildren<{}>): React.ReactElement {
if (process.env.__NEXT_TEST_MODE) {
// eslint-disable-next-line react-hooks/rules-of-hooks
React.useEffect(() => {
window.__NEXT_HYDRATED = true

if (window.__NEXT_HYDRATED_CB) {
window.__NEXT_HYDRATED_CB()
}
}, [])
}

return children as React.ReactElement
}

const RSCComponent = (props: any) => {
const cacheKey = getCacheKey()
const { __flight_serialized__ } = props
const [, dispatch] = useState({})
const rerender = () => dispatch({})
// If there is no cache, or there is serialized data already
function refreshCache(nextProps: any) {
startTransition(() => {
const currentCacheKey = getCacheKey()
const response = createFromFetch(fetchFlight(currentCacheKey, nextProps))

rscCache.set(currentCacheKey, response)
rerender()
})
}

return (
<RefreshContext.Provider value={refreshCache}>
<ServerRoot cacheKey={cacheKey} serialized={__flight_serialized__} />
</RefreshContext.Provider>
)
}

export function hydrate() {
renderReactElement(appElement!, () => (
<React.StrictMode>
<Root>
<RSCComponent />
</Root>
</React.StrictMode>
))
}
8 changes: 8 additions & 0 deletions packages/next/client/root-next.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { hydrate, version } from './root-index'

window.next = {
version,
root: true,
}

hydrate()
3 changes: 2 additions & 1 deletion packages/next/lib/eslint/runLintCheck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,8 @@ async function lint(
}
}

const pagesDir = findPagesDir(baseDir)
// TODO: should we apply these rules to "root" dir as well?
const pagesDir = findPagesDir(baseDir).pages

if (nextEslintPluginIsEnabled) {
let updatedPagesDir = false
Expand Down
35 changes: 25 additions & 10 deletions packages/next/lib/find-pages-dir.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,37 @@ export const existsSync = (f: string): boolean => {
}
}

export function findPagesDir(dir: string): string {
// prioritize ./pages over ./src/pages
let curDir = path.join(dir, 'pages')
function findDir(dir: string, name: 'pages' | 'root'): string | null {
// prioritize ./${name} over ./src/${name}
let curDir = path.join(dir, name)
if (existsSync(curDir)) return curDir

curDir = path.join(dir, 'src/pages')
curDir = path.join(dir, 'src', name)
if (existsSync(curDir)) return curDir

// Check one level up the tree to see if the pages directory might be there
if (existsSync(path.join(dir, '..', 'pages'))) {
return null
}

export function findPagesDir(
dir: string,
root?: boolean
): { pages: string; root?: string } {
const pagesDir = findDir(dir, 'pages')
let rootDir: undefined | string

if (root) {
rootDir = findDir(dir, 'root') || undefined
}

// TODO: allow "root" dir without pages dir
if (pagesDir === null) {
throw new Error(
'> No `pages` directory found. Did you mean to run `next` in the parent (`../`) directory?'
"> Couldn't find a `pages` directory. Please create one under the project root"
)
}

throw new Error(
"> Couldn't find a `pages` directory. Please create one under the project root"
)
return {
pages: pagesDir,
root: rootDir,
}
}
18 changes: 10 additions & 8 deletions packages/next/lib/typescript/getTypeScriptIntent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export type TypeScriptIntent = { firstTimeSetup: boolean }

export async function getTypeScriptIntent(
baseDir: string,
pagesDir: string,
intentDirs: string[],
config: NextConfigComplete
): Promise<TypeScriptIntent | false> {
const tsConfigPath = path.join(baseDir, config.typescript.tsconfigPath)
Expand All @@ -28,13 +28,15 @@ export async function getTypeScriptIntent(
// project for the user when we detect TypeScript files. So, we need to check
// the `pages/` directory for a TypeScript file.
// Checking all directories is too slow, so this is a happy medium.
const typescriptFiles = await recursiveReadDir(
pagesDir,
/.*\.(ts|tsx)$/,
/(node_modules|.*\.d\.ts)/
)
if (typescriptFiles.length) {
return { firstTimeSetup: true }
for (const dir of intentDirs) {
const typescriptFiles = await recursiveReadDir(
dir,
/.*\.(ts|tsx)$/,
/(node_modules|.*\.d\.ts)/
)
if (typescriptFiles.length) {
return { firstTimeSetup: true }
}
}

return false
Expand Down
Loading

0 comments on commit 242ca9a

Please sign in to comment.