Skip to content

Commit

Permalink
Merge branch 'canary' into add-statsig-app-router-example
Browse files Browse the repository at this point in the history
  • Loading branch information
daniel-statsig authored Oct 3, 2024
2 parents b5a8112 + d0cbe64 commit e5fba43
Show file tree
Hide file tree
Showing 31 changed files with 765 additions and 144 deletions.
3 changes: 2 additions & 1 deletion packages/next/src/build/webpack-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ import {
getBabelLoader,
getReactCompilerLoader,
} from './get-babel-loader-config'
import type { NextFlightLoaderOptions } from './webpack/loaders/next-flight-loader'

type ExcludesFalse = <T>(x: T | false) => x is T
type ClientEntries = {
Expand Down Expand Up @@ -525,7 +526,7 @@ export default async function getBaseWebpackConfig(
loader: 'next-flight-loader',
options: {
isEdgeServer,
},
} satisfies NextFlightLoaderOptions,
}

const appServerLayerLoaders = hasAppDir
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,35 @@ import type { webpack } from 'next/dist/compiled/webpack/webpack'
import { RSC_MOD_REF_PROXY_ALIAS } from '../../../../lib/constants'
import {
BARREL_OPTIMIZATION_PREFIX,
DEFAULT_RUNTIME_WEBPACK,
EDGE_RUNTIME_WEBPACK,
RSC_MODULE_TYPES,
} from '../../../../shared/lib/constants'
import { warnOnce } from '../../../../shared/lib/utils/warn-once'
import { getRSCModuleInformation } from '../../../analysis/get-page-static-info'
import { formatBarrelOptimizedResource } from '../../utils'
import { getModuleBuildInfo } from '../get-module-build-info'
import type {
javascript,
LoaderContext,
} from 'next/dist/compiled/webpack/webpack'
import picomatch from 'next/dist/compiled/picomatch'

export interface NextFlightLoaderOptions {
isEdgeServer: boolean
}

type SourceType = javascript.JavascriptParser['sourceType'] | 'commonjs'

const noopHeadPath = require.resolve('next/dist/client/components/noop-head')
// For edge runtime it will be aliased to esm version by webpack
const MODULE_PROXY_PATH =
'next/dist/build/webpack/loaders/next-flight-loader/module-proxy'

type SourceType = 'auto' | 'commonjs' | 'module'
const isSharedRuntime = picomatch('**/next/dist/**/*.shared-runtime.js', {
dot: true, // required for .pnpm paths
})

export function getAssumedSourceType(
mod: webpack.Module,
sourceType: SourceType
Expand Down Expand Up @@ -45,7 +61,7 @@ export function getAssumedSourceType(
}

export default function transformSource(
this: any,
this: LoaderContext<NextFlightLoaderOptions>,
source: string,
sourceMap: any
) {
Expand All @@ -56,10 +72,11 @@ export default function transformSource(

const options = this.getOptions()
const { isEdgeServer } = options
const module = this._module!

// Assign the RSC meta information to buildInfo.
// Exclude next internal files which are not marked as client files
const buildInfo = getModuleBuildInfo(this._module)
const buildInfo = getModuleBuildInfo(module)
buildInfo.rsc = getRSCModuleInformation(source, true)

// Resource key is the unique identifier for the resource. When RSC renders
Expand All @@ -75,19 +92,20 @@ export default function transformSource(
// Because of that, we must add another query param to the resource key to
// differentiate them.
let resourceKey: string = this.resourcePath
if (this._module?.matchResource?.startsWith(BARREL_OPTIMIZATION_PREFIX)) {
if (module.matchResource?.startsWith(BARREL_OPTIMIZATION_PREFIX)) {
resourceKey = formatBarrelOptimizedResource(
resourceKey,
this._module.matchResource
module.matchResource
)
}

// A client boundary.
if (buildInfo.rsc?.type === RSC_MODULE_TYPES.client) {
const assumedSourceType = getAssumedSourceType(
this._module,
this._module?.parser?.sourceType
module,
(module.parser as javascript.JavascriptParser).sourceType
)

const clientRefs = buildInfo.rsc.clientRefs!

if (assumedSourceType === 'module') {
Expand All @@ -100,6 +118,18 @@ export default function transformSource(
return
}

if (!isSharedRuntime(resourceKey)) {
// Prevent module concatenation, and prevent export names from being
// mangled, in production builds, so that exports of client reference
// modules can be resolved by React using the metadata from the client
// manifest.
this._compilation!.moduleGraph.getExportsInfo(
module
).setUsedInUnknownWay(
isEdgeServer ? EDGE_RUNTIME_WEBPACK : DEFAULT_RUNTIME_WEBPACK
)
}

// `proxy` is the module proxy that we treat the module as a client boundary.
// For ESM, we access the property of the module proxy directly for each export.
// This is bit hacky that treating using a CJS like module proxy for ESM's exports,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ const pluginState = getProxiedPluginState({
serverModuleIds: {} as Record<string, string | number>,
edgeServerModuleIds: {} as Record<string, string | number>,

rscModuleIds: {} as Record<string, string | number>,
edgeRscModuleIds: {} as Record<string, string | number>,

injectedClientEntries: {} as Record<string, string>,
})

Expand Down Expand Up @@ -209,6 +212,20 @@ export class FlightClientEntryPlugin {
: modPath + modQuery
: mod.resource

if (typeof modId !== 'undefined' && modResource) {
if (mod.layer === WEBPACK_LAYERS.reactServerComponents) {
const key = path
.relative(compiler.context, modResource)
.replace(/\/next\/dist\/esm\//, '/next/dist/')

if (this.isEdgeServer) {
pluginState.edgeRscModuleIds[key] = modId
} else {
pluginState.rscModuleIds[key] = modId
}
}
}

if (mod.layer !== WEBPACK_LAYERS.serverSideRendering) {
return
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ export type ManifestChunks = Array<string>
const pluginState = getProxiedPluginState({
serverModuleIds: {} as Record<string, string | number>,
edgeServerModuleIds: {} as Record<string, string | number>,

rscModuleIds: {} as Record<string, string | number>,
edgeRscModuleIds: {} as Record<string, string | number>,
})

export interface ManifestNode {
Expand Down Expand Up @@ -86,6 +89,12 @@ export type ClientReferenceManifest = {
entryJSFiles?: {
[entry: string]: string[]
}
rscModuleMapping: {
[moduleId: string]: ManifestNode
}
edgeRscModuleMapping: {
[moduleId: string]: ManifestNode
}
}

function getAppPathRequiredChunks(
Expand Down Expand Up @@ -173,6 +182,11 @@ function mergeManifest(
manifestToMerge.edgeSSRModuleMapping
)
Object.assign(manifest.entryCSSFiles, manifestToMerge.entryCSSFiles)
Object.assign(manifest.rscModuleMapping, manifestToMerge.rscModuleMapping)
Object.assign(
manifest.edgeRscModuleMapping,
manifestToMerge.edgeRscModuleMapping
)
}

const PLUGIN_NAME = 'ClientReferenceManifestPlugin'
Expand Down Expand Up @@ -268,6 +282,8 @@ export class ClientReferenceManifestPlugin {
edgeSSRModuleMapping: {},
clientModules: {},
entryCSSFiles: {},
rscModuleMapping: {},
edgeRscModuleMapping: {},
}

// Absolute path without the extension
Expand Down Expand Up @@ -295,6 +311,9 @@ export class ClientReferenceManifestPlugin {
const moduleIdMapping = manifest.ssrModuleMapping
const edgeModuleIdMapping = manifest.edgeSSRModuleMapping

const rscIdMapping = manifest.rscModuleMapping
const edgeRscIdMapping = manifest.edgeRscModuleMapping

// Note that this isn't that reliable as webpack is still possible to assign
// additional queries to make sure there's no conflict even using the `named`
// module ID strategy.
Expand All @@ -303,6 +322,11 @@ export class ClientReferenceManifestPlugin {
mod.resourceResolveData?.path || resource
)

const rscNamedModuleId = relative(
context,
mod.resourceResolveData?.path || resource
)

if (!ssrNamedModuleId.startsWith('.'))
ssrNamedModuleId = `./${ssrNamedModuleId.replace(/\\/g, '/')}`

Expand Down Expand Up @@ -345,7 +369,6 @@ export class ClientReferenceManifestPlugin {
function addSSRIdMapping() {
const exportName = resource
if (
// TODO: Add mapping from client module IDs to RSC module IDs
typeof pluginState.serverModuleIds[ssrNamedModuleId] !== 'undefined'
) {
moduleIdMapping[modId] = moduleIdMapping[modId] || {}
Expand Down Expand Up @@ -375,12 +398,47 @@ export class ClientReferenceManifestPlugin {
}
}

function addRSCIdMapping() {
const exportName = resource
if (
typeof pluginState.rscModuleIds[rscNamedModuleId] !== 'undefined'
) {
rscIdMapping[modId] = rscIdMapping[modId] || {}
rscIdMapping[modId]['*'] = {
...manifest.clientModules[exportName],
// During SSR, we don't have external chunks to load on the server
// side with our architecture of Webpack / Turbopack. We can keep
// this field empty to save some bytes.
chunks: [],
id: pluginState.rscModuleIds[rscNamedModuleId],
}
}

if (
typeof pluginState.edgeRscModuleIds[rscNamedModuleId] !==
'undefined'
) {
edgeRscIdMapping[modId] = edgeRscIdMapping[modId] || {}
edgeRscIdMapping[modId]['*'] = {
...manifest.clientModules[exportName],
// During SSR, we don't have external chunks to load on the server
// side with our architecture of Webpack / Turbopack. We can keep
// this field empty to save some bytes.
chunks: [],
id: pluginState.edgeRscModuleIds[rscNamedModuleId],
}
}
}

addClientReference()
addSSRIdMapping()
addRSCIdMapping()

manifest.clientModules = moduleReferences
manifest.ssrModuleMapping = moduleIdMapping
manifest.edgeSSRModuleMapping = edgeModuleIdMapping
manifest.rscModuleMapping = rscIdMapping
manifest.edgeRscModuleMapping = edgeRscIdMapping
}

const checkedChunkGroups = new Set()
Expand Down Expand Up @@ -479,6 +537,8 @@ export class ClientReferenceManifestPlugin {
edgeSSRModuleMapping: {},
clientModules: {},
entryCSSFiles: {},
rscModuleMapping: {},
edgeRscModuleMapping: {},
}

const segments = [...entryNameToGroupName(pageName).split('/'), 'page']
Expand Down
15 changes: 6 additions & 9 deletions packages/next/src/server/app-render/encryption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
stringToUint8Array,
} from './encryption-utils'

import type { ManifestNode } from '../../build/webpack/plugins/flight-manifest-plugin'
const isEdgeRuntime = process.env.NEXT_RUNTIME === 'edge'

const textEncoder = new TextEncoder()
const textDecoder = new TextDecoder()
Expand Down Expand Up @@ -96,16 +96,11 @@ export async function decryptActionBoundArgs(
actionId: string,
encrypted: Promise<string>
) {
const clientReferenceManifestSingleton = getClientReferenceManifestSingleton()

// Decrypt the serialized string with the action id as the salt.
const decryped = await decodeActionBoundArg(actionId, await encrypted)

// TODO: We can't use the client reference manifest to resolve the modules
// on the server side - instead they need to be recovered as the module
// references (proxies) again.
// For now, we'll just use an empty module map.
const ssrModuleMap: {
[moduleExport: string]: ManifestNode
} = {}
// Using Flight to deserialize the args from the string.
const deserialized = await createFromReadableStream(
new ReadableStream({
Expand All @@ -120,7 +115,9 @@ export async function decryptActionBoundArgs(
// to be added to the current execution. Instead, we'll wait for any ClientReference
// to be emitted which themselves will handle the preloading.
moduleLoading: null,
moduleMap: ssrModuleMap,
moduleMap: isEdgeRuntime
? clientReferenceManifestSingleton.edgeRscModuleMapping
: clientReferenceManifestSingleton.rscModuleMapping,
},
}
)
Expand Down
8 changes: 8 additions & 0 deletions packages/next/src/server/base-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1017,6 +1017,14 @@ export default abstract class Server<
req.headers['x-forwarded-proto'] ??= isHttps ? 'https' : 'http'
req.headers['x-forwarded-for'] ??= originalRequest?.socket?.remoteAddress

// Validate that if i18n isn't configured or the passed parameters are not
// valid it should be removed from the query.
if (!this.i18nProvider?.validateQuery(parsedUrl.query)) {
delete parsedUrl.query.__nextLocale
delete parsedUrl.query.__nextDefaultLocale
delete parsedUrl.query.__nextInferredLocaleFromDefault
}

// This should be done before any normalization of the pathname happens as
// it captures the initial URL.
this.attachRequestMeta(req, parsedUrl)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import * as React from 'react'

const errorRef: { current: null | string } = { current: null }

// React.cache is currently only available in canary/experimental React channels.
const cache =
typeof React.cache === 'function'
? React.cache
: (fn: (key: unknown) => void) => fn

// We don't want to dedupe across requests.
// The developer might've just attempted to fix the warning so we should warn again if it still happens.
const flushCurrentErrorIfNew = cache(
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- cache key
(key: unknown) => {
try {
console.error(errorRef.current)
} finally {
errorRef.current = null
}
}
)

/**
* Creates a function that logs an error message that is deduped by the userland
* callsite.
* This requires no indirection between the call of this function and the userland
* callsite i.e. there's only a single library frame above this.
* Do not use on the Client where sourcemaps and ignore listing might be enabled.
* Only use that for warnings need a fix independent of the callstack.
*
* @param getMessage
* @returns
*/
export function createDedupedByCallsiteServerErrorLoggerDev<Args extends any[]>(
getMessage: (...args: Args) => string
) {
return function logDedupedError(...args: Args) {
const message = getMessage(...args)

if (process.env.NODE_ENV !== 'production') {
const callStackFrames = new Error().stack?.split('\n')
if (callStackFrames === undefined || callStackFrames.length < 4) {
console.error(message)
} else {
// Error:
// logDedupedError
// asyncApiBeingAccessedSynchronously
// <userland callsite>
// TODO: This breaks if sourcemaps with ignore lists are enabled.
const key = callStackFrames[3]
errorRef.current = message
flushCurrentErrorIfNew(key)
}
} else {
console.error(message)
}
}
}
Loading

0 comments on commit e5fba43

Please sign in to comment.