Fix ASL bundling for dynamic css (#64451)
### Why For app page rendering on edge, the `AsyncLocalStorage` (ALS) should be bundled as same instance across layers. We're accessing the ALS in `next/dynamic` modules during SSR for preloading CSS chunks. There's a bug that we can't get the ALS store during SSR in edge, I digged into it and found the root cause is: We have both import paths: `module (rsc layer) -> request ALS (shared layer)` `module (ssr layer) -> request ALS (shared layer)` We expect the ALS to be the same module since we're using the same layer but found that they're treated as different modules due to applying another loader transform on ssr layer. They're resulted in the same `shared` layer, but with different resource queries. This PR excluded that transform so now they're identical across layers. ### What For webpack, we aligned the loaders applying to the async local storage, so that they're resolved as the same module now. For turbopack, we leverage module transition, sort of creating a new `app-shared` layer for these modules, and apply the transition to all async local storage instances therefore the instances of them are only bundled once. To make the turbopack chanegs work, we change how the async local storage modules defined, separate the instance into a single file and mark it as "next-shared" layer with import: ``` any module -> async local storage --- use transition, specify "next-shared" layer ---> async local storage instance ``` Closes NEXT-3085
This commit is contained in:
parent
07a0700dcb
commit
8b82225fea
12 changed files with 114 additions and 16 deletions
|
@ -283,6 +283,10 @@ impl AppProject {
|
|||
))),
|
||||
),
|
||||
("next-ssr".to_string(), Vc::upcast(self.ssr_transition())),
|
||||
(
|
||||
"next-shared".to_string(),
|
||||
Vc::upcast(self.shared_transition()),
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
@ -315,6 +319,10 @@ impl AppProject {
|
|||
"next-ssr".to_string(),
|
||||
Vc::upcast(self.edge_ssr_transition()),
|
||||
),
|
||||
(
|
||||
"next-shared".to_string(),
|
||||
Vc::upcast(self.edge_shared_transition()),
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
@ -344,6 +352,10 @@ impl AppProject {
|
|||
))),
|
||||
),
|
||||
("next-ssr".to_string(), Vc::upcast(self.ssr_transition())),
|
||||
(
|
||||
"next-shared".to_string(),
|
||||
Vc::upcast(self.shared_transition()),
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
@ -359,8 +371,30 @@ impl AppProject {
|
|||
|
||||
#[turbo_tasks::function]
|
||||
fn edge_route_module_context(self: Vc<Self>) -> Vc<ModuleAssetContext> {
|
||||
let transitions = [
|
||||
(
|
||||
ECMASCRIPT_CLIENT_TRANSITION_NAME.to_string(),
|
||||
Vc::upcast(NextEcmascriptClientReferenceTransition::new(
|
||||
Vc::upcast(self.client_transition()),
|
||||
self.edge_ssr_transition(),
|
||||
)),
|
||||
),
|
||||
(
|
||||
"next-dynamic".to_string(),
|
||||
Vc::upcast(NextDynamicTransition::new(Vc::upcast(
|
||||
self.client_transition(),
|
||||
))),
|
||||
),
|
||||
("next-ssr".to_string(), Vc::upcast(self.ssr_transition())),
|
||||
(
|
||||
"next-shared".to_string(),
|
||||
Vc::upcast(self.edge_shared_transition()),
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.collect();
|
||||
ModuleAssetContext::new(
|
||||
Default::default(),
|
||||
Vc::cell(transitions),
|
||||
self.project().edge_compile_time_info(),
|
||||
self.edge_route_module_options_context(),
|
||||
self.edge_route_resolve_options_context(),
|
||||
|
@ -435,6 +469,16 @@ impl AppProject {
|
|||
)
|
||||
}
|
||||
|
||||
#[turbo_tasks::function]
|
||||
fn shared_transition(self: Vc<Self>) -> Vc<ContextTransition> {
|
||||
ContextTransition::new(
|
||||
self.project().server_compile_time_info(),
|
||||
self.ssr_module_options_context(),
|
||||
self.ssr_resolve_options_context(),
|
||||
Vc::cell("app-shared".to_string()),
|
||||
)
|
||||
}
|
||||
|
||||
#[turbo_tasks::function]
|
||||
fn edge_ssr_transition(self: Vc<Self>) -> Vc<ContextTransition> {
|
||||
ContextTransition::new(
|
||||
|
@ -445,6 +489,16 @@ impl AppProject {
|
|||
)
|
||||
}
|
||||
|
||||
#[turbo_tasks::function]
|
||||
fn edge_shared_transition(self: Vc<Self>) -> Vc<ContextTransition> {
|
||||
ContextTransition::new(
|
||||
self.project().edge_compile_time_info(),
|
||||
self.edge_ssr_module_options_context(),
|
||||
self.edge_ssr_resolve_options_context(),
|
||||
Vc::cell("app-edge-shared".to_string()),
|
||||
)
|
||||
}
|
||||
|
||||
#[turbo_tasks::function]
|
||||
async fn runtime_entries(self: Vc<Self>) -> Result<Vc<RuntimeEntries>> {
|
||||
Ok(get_server_runtime_entries(
|
||||
|
|
|
@ -1383,7 +1383,6 @@ export default async function getBaseWebpackConfig(
|
|||
// Alias react for switching between default set and share subset.
|
||||
oneOf: [
|
||||
{
|
||||
exclude: asyncStoragesRegex,
|
||||
issuerLayer: isWebpackServerOnlyLayer,
|
||||
test: {
|
||||
// Resolve it if it is a source code file, and it has NOT been
|
||||
|
@ -1391,7 +1390,7 @@ export default async function getBaseWebpackConfig(
|
|||
and: [
|
||||
codeCondition.test,
|
||||
{
|
||||
not: [optOutBundlingPackageRegex],
|
||||
not: [optOutBundlingPackageRegex, asyncStoragesRegex],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -1499,6 +1498,7 @@ export default async function getBaseWebpackConfig(
|
|||
{
|
||||
test: codeCondition.test,
|
||||
issuerLayer: WEBPACK_LAYERS.serverSideRendering,
|
||||
exclude: asyncStoragesRegex,
|
||||
use: appSSRLayerLoaders,
|
||||
resolve: {
|
||||
mainFields: getMainField(compilerType, true),
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
import type { ActionAsyncStorage } from './action-async-storage.external'
|
||||
import { createAsyncLocalStorage } from './async-local-storage'
|
||||
|
||||
export const actionAsyncStorage: ActionAsyncStorage = createAsyncLocalStorage()
|
|
@ -1,6 +1,9 @@
|
|||
import type { AsyncLocalStorage } from 'async_hooks'
|
||||
import { createAsyncLocalStorage } from './async-local-storage'
|
||||
|
||||
// Share the instance module in the next-shared layer
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
;('TURBOPACK { transition: next-shared }')
|
||||
import { actionAsyncStorage } from './action-async-storage-instance'
|
||||
export interface ActionStore {
|
||||
readonly isAction?: boolean
|
||||
readonly isAppRoute?: boolean
|
||||
|
@ -8,4 +11,4 @@ export interface ActionStore {
|
|||
|
||||
export type ActionAsyncStorage = AsyncLocalStorage<ActionStore>
|
||||
|
||||
export const actionAsyncStorage: ActionAsyncStorage = createAsyncLocalStorage()
|
||||
export { actionAsyncStorage }
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
import { createAsyncLocalStorage } from './async-local-storage'
|
||||
import type { RequestAsyncStorage } from './request-async-storage.external'
|
||||
|
||||
export const requestAsyncStorage: RequestAsyncStorage =
|
||||
createAsyncLocalStorage()
|
|
@ -4,7 +4,10 @@ import type { ResponseCookies } from '../../server/web/spec-extension/cookies'
|
|||
import type { ReadonlyHeaders } from '../../server/web/spec-extension/adapters/headers'
|
||||
import type { ReadonlyRequestCookies } from '../../server/web/spec-extension/adapters/request-cookies'
|
||||
|
||||
import { createAsyncLocalStorage } from './async-local-storage'
|
||||
// Share the instance module in the next-shared layer
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
;('TURBOPACK { transition: next-shared }')
|
||||
import { requestAsyncStorage } from './request-async-storage-instance'
|
||||
import type { DeepReadonly } from '../../shared/lib/deep-readonly'
|
||||
|
||||
export interface RequestStore {
|
||||
|
@ -20,8 +23,7 @@ export interface RequestStore {
|
|||
|
||||
export type RequestAsyncStorage = AsyncLocalStorage<RequestStore>
|
||||
|
||||
export const requestAsyncStorage: RequestAsyncStorage =
|
||||
createAsyncLocalStorage()
|
||||
export { requestAsyncStorage }
|
||||
|
||||
export function getExpectedRequestStore(callingExpression: string) {
|
||||
const store = requestAsyncStorage.getStore()
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
import type { StaticGenerationAsyncStorage } from './static-generation-async-storage.external'
|
||||
import { createAsyncLocalStorage } from './async-local-storage'
|
||||
|
||||
export const staticGenerationAsyncStorage: StaticGenerationAsyncStorage =
|
||||
createAsyncLocalStorage()
|
|
@ -5,7 +5,10 @@ import type { FetchMetrics } from '../../server/base-http'
|
|||
import type { Revalidate } from '../../server/lib/revalidate'
|
||||
import type { PrerenderState } from '../../server/app-render/dynamic-rendering'
|
||||
|
||||
import { createAsyncLocalStorage } from './async-local-storage'
|
||||
// Share the instance module in the next-shared layer
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
;('TURBOPACK { transition: next-shared }')
|
||||
import { staticGenerationAsyncStorage } from './static-generation-async-storage-instance'
|
||||
|
||||
export interface StaticGenerationStore {
|
||||
readonly isStaticGeneration: boolean
|
||||
|
@ -53,5 +56,4 @@ export interface StaticGenerationStore {
|
|||
export type StaticGenerationAsyncStorage =
|
||||
AsyncLocalStorage<StaticGenerationStore>
|
||||
|
||||
export const staticGenerationAsyncStorage: StaticGenerationAsyncStorage =
|
||||
createAsyncLocalStorage()
|
||||
export { staticGenerationAsyncStorage }
|
||||
|
|
|
@ -170,7 +170,6 @@ const WEBPACK_LAYERS = {
|
|||
clientOnly: [
|
||||
WEBPACK_LAYERS_NAMES.serverSideRendering,
|
||||
WEBPACK_LAYERS_NAMES.appPagesBrowser,
|
||||
WEBPACK_LAYERS_NAMES.shared,
|
||||
],
|
||||
nonClientServerTarget: [
|
||||
// middleware and pages api
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
'use client'
|
||||
|
||||
import { getExpectedRequestStore } from '../../../client/components/request-async-storage.external'
|
||||
|
||||
export function PreloadCss({ moduleIds }: { moduleIds: string[] | undefined }) {
|
||||
// Early return in client compilation and only load requestStore on server side
|
||||
if (typeof window !== 'undefined') {
|
||||
return null
|
||||
}
|
||||
const {
|
||||
getExpectedRequestStore,
|
||||
} = require('../../../client/components/request-async-storage.external')
|
||||
const requestStore = getExpectedRequestStore()
|
||||
|
||||
const requestStore = getExpectedRequestStore('next/dynamic css')
|
||||
const allFiles = []
|
||||
|
||||
// Search the current dynamic call unique key id in react loadable manifest,
|
||||
|
|
3
test/e2e/app-dir/dynamic-css/app/ssr/edge/page.js
Normal file
3
test/e2e/app-dir/dynamic-css/app/ssr/edge/page.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
export { default } from '../page'
|
||||
|
||||
export const runtime = 'edge'
|
|
@ -31,6 +31,23 @@ createNextDescribe(
|
|||
})
|
||||
})
|
||||
|
||||
it('should only apply corresponding css for page loaded in edge runtime', async () => {
|
||||
const browser = await next.browser('/ssr/edge')
|
||||
await retry(async () => {
|
||||
expect(
|
||||
await browser.eval(
|
||||
`window.getComputedStyle(document.querySelector('.text')).color`
|
||||
)
|
||||
).toBe('rgb(255, 0, 0)')
|
||||
// Default border width, which is not effected by bar.css that is not loaded in /ssr
|
||||
expect(
|
||||
await browser.eval(
|
||||
`window.getComputedStyle(document.querySelector('.text')).borderWidth`
|
||||
)
|
||||
).toBe('0px')
|
||||
})
|
||||
})
|
||||
|
||||
it('should only apply corresponding css for page loaded that /another', async () => {
|
||||
const browser = await next.browser('/another')
|
||||
await retry(async () => {
|
||||
|
@ -47,5 +64,10 @@ createNextDescribe(
|
|||
).toBe('1px')
|
||||
})
|
||||
})
|
||||
|
||||
it('should not throw with accessing to ALS in preload css', async () => {
|
||||
const output = next.cliOutput
|
||||
expect(output).not.toContain('was called outside a request scope')
|
||||
})
|
||||
}
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue