Skip to content

Commit 9b45393

Browse files
authored
[dynamicIO] cache tracking for import() (#74152)
This PR introduces some extra tracking which makes us treat `import(...)` like a call to a cached function. Thanks to this, `await import(...)` will no longer cause dynamicity errors in `dynamicIO`. The motivation is the same as allowing `fs.readFileSync` -- if something is available on the server at prerender time, we don't consider it IO. Fixes #72589 Closes #75132 ### Implementation notes The tracking is implemented via an SWC transform (`track_dynamic_imports.ts`) that turns `import(...)` into `trackDynamicImport(import(...))`. `trackDynamicImport(promise)` tracks the promise globally, without using `workUnitStore.cacheSignal`. The prospective render subscribes to pending modules using `trackPendingModules(cacheSignal)`, which causes the prospective render to wait for all `import()`s to finish before proceeding to the final render. The mechanism is analogous to `'use cache'`, but the "result" of an `import()` is stored *in the module cache* instead; when we invoke the `import()` again in the actual prerender, it will resolve at microtask-speed, like we need it to. The transform is enabled for all modules that run server-side, both RSC and SSR, because the prerender runs both. This also includes route handlers, because we also use a `cacheSignal` when prerendering those. Notably, we also instrument `import()` in `node_modules` to account for libraries that do lazy initialization. I've also had to adjust the prospective client render in PPR mode to wait for `cacheSignal.cacheReady()` - otherwise, it wouldn't wait for `import()`s to resolve before doing the final client render, which'd subsequently cause a dynamicity error. (we didn't need to wait for `cacheSignal` before, because all cached promises would've already been awaited in the server prerender) Finally, we no longer need `warmFlightResponse`, because `trackPendingModules` already makes the `cacheSignal` wait for all loading chunks to finish, so we don't need to do it ahead of time.
1 parent 7790f72 commit 9b45393

File tree

83 files changed

+1082
-143
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

83 files changed

+1082
-143
lines changed

.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ pids
2828
coverage
2929

3030
# test output
31-
test/**/out*
31+
test/**/out/*
3232
test/**/next-env.d.ts
3333
.DS_Store
3434
/e2e-tests
@@ -42,7 +42,7 @@ test/traces
4242
.nvmrc
4343

4444
# examples
45-
examples/**/out
45+
examples/**/out/*
4646
examples/**/.env*.local
4747

4848
pr-stats.md

crates/next-core/src/next_import_map.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -965,6 +965,13 @@ async fn insert_next_shared_aliases(
965965
"next/dist/build/webpack/loaders/next-flight-loader/cache-wrapper",
966966
),
967967
);
968+
import_map.insert_exact_alias(
969+
"private-next-rsc-track-dynamic-import",
970+
request_to_import_mapping(
971+
project_path,
972+
"next/dist/build/webpack/loaders/next-flight-loader/track-dynamic-import",
973+
),
974+
);
968975

969976
insert_turbopack_dev_alias(import_map).await?;
970977
insert_package_alias(

crates/next-core/src/next_server/transforms.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ use crate::{
1212
next_shared::transforms::{
1313
get_next_dynamic_transform_rule, get_next_font_transform_rule, get_next_image_rule,
1414
get_next_lint_transform_rule, get_next_modularize_imports_rule,
15-
get_next_pages_transforms_rule, get_server_actions_transform_rule,
16-
next_amp_attributes::get_next_amp_attr_rule,
15+
get_next_pages_transforms_rule, get_next_track_dynamic_imports_transform_rule,
16+
get_server_actions_transform_rule, next_amp_attributes::get_next_amp_attr_rule,
1717
next_cjs_optimizer::get_next_cjs_optimizer_rule,
1818
next_disallow_re_export_all_in_page::get_next_disallow_export_all_in_page_rule,
1919
next_edge_node_api_assert::next_edge_node_api_assert,
@@ -178,6 +178,10 @@ pub async fn get_next_server_transforms_rules(
178178
ServerContextType::Middleware { .. } | ServerContextType::Instrumentation { .. } => false,
179179
};
180180

181+
if is_app_dir && *next_config.enable_dynamic_io().await? {
182+
rules.push(get_next_track_dynamic_imports_transform_rule(mdx_rs));
183+
}
184+
181185
if !foreign_code {
182186
rules.push(
183187
get_next_dynamic_transform_rule(true, is_server_components, is_app_dir, mode, mdx_rs)

crates/next-core/src/next_shared/transforms/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ pub(crate) mod next_pure;
1616
pub(crate) mod next_react_server_components;
1717
pub(crate) mod next_shake_exports;
1818
pub(crate) mod next_strip_page_exports;
19+
pub(crate) mod next_track_dynamic_imports;
1920
pub(crate) mod react_remove_properties;
2021
pub(crate) mod relay;
2122
pub(crate) mod remove_console;
@@ -30,6 +31,7 @@ pub use next_dynamic::get_next_dynamic_transform_rule;
3031
pub use next_font::get_next_font_transform_rule;
3132
pub use next_lint::get_next_lint_transform_rule;
3233
pub use next_strip_page_exports::get_next_pages_transforms_rule;
34+
pub use next_track_dynamic_imports::get_next_track_dynamic_imports_transform_rule;
3335
pub use server_actions::get_server_actions_transform_rule;
3436
use turbo_tasks::{ReadRef, ResolvedVc, Value};
3537
use turbo_tasks_fs::FileSystemPath;
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
use anyhow::Result;
2+
use async_trait::async_trait;
3+
use next_custom_transforms::transforms::track_dynamic_imports::*;
4+
use swc_core::ecma::ast::Program;
5+
use turbopack::module_options::ModuleRule;
6+
use turbopack_ecmascript::{CustomTransformer, TransformContext};
7+
8+
use super::get_ecma_transform_rule;
9+
10+
pub fn get_next_track_dynamic_imports_transform_rule(mdx_rs: bool) -> ModuleRule {
11+
get_ecma_transform_rule(Box::new(NextTrackDynamicImports {}), mdx_rs, false)
12+
}
13+
14+
#[derive(Debug)]
15+
struct NextTrackDynamicImports {}
16+
17+
#[async_trait]
18+
impl CustomTransformer for NextTrackDynamicImports {
19+
#[tracing::instrument(level = tracing::Level::TRACE, name = "next_track_dynamic_imports", skip_all)]
20+
async fn transform(&self, program: &mut Program, ctx: &TransformContext<'_>) -> Result<()> {
21+
program.mutate(track_dynamic_imports(ctx.unresolved_mark));
22+
Ok(())
23+
}
24+
}

crates/next-custom-transforms/src/chain_transforms.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,9 @@ pub struct TransformOptions {
121121

122122
#[serde(default)]
123123
pub css_env: Option<swc_core::ecma::preset_env::Config>,
124+
125+
#[serde(default)]
126+
pub track_dynamic_imports: bool,
124127
}
125128

126129
pub fn custom_before_pass<'a, C>(
@@ -333,6 +336,14 @@ where
333336
)),
334337
None => Either::Right(noop_pass()),
335338
},
339+
match &opts.track_dynamic_imports {
340+
true => Either::Left(
341+
crate::transforms::track_dynamic_imports::track_dynamic_imports(
342+
unresolved_mark,
343+
),
344+
),
345+
false => Either::Right(noop_pass()),
346+
},
336347
match &opts.cjs_require_optimizer {
337348
Some(config) => Either::Left(visit_mut_pass(
338349
crate::transforms::cjs_optimizer::cjs_optimizer(

crates/next-custom-transforms/src/transforms/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ pub mod react_server_components;
1818
pub mod server_actions;
1919
pub mod shake_exports;
2020
pub mod strip_page_exports;
21+
pub mod track_dynamic_imports;
2122
pub mod warn_for_edge_runtime;
2223

2324
//[TODO] PACK-1564: need to decide reuse vs. turbopack specific
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
use swc_core::{
2+
common::{source_map::PURE_SP, util::take::Take, Mark, SyntaxContext},
3+
ecma::{
4+
ast::*,
5+
utils::{prepend_stmt, private_ident, quote_ident, quote_str},
6+
visit::{noop_visit_mut_type, visit_mut_pass, VisitMut, VisitMutWith},
7+
},
8+
quote,
9+
};
10+
11+
pub fn track_dynamic_imports(unresolved_mark: Mark) -> impl VisitMut + Pass {
12+
visit_mut_pass(ImportReplacer::new(unresolved_mark))
13+
}
14+
15+
struct ImportReplacer {
16+
unresolved_ctxt: SyntaxContext,
17+
has_dynamic_import: bool,
18+
wrapper_function_local_ident: Ident,
19+
}
20+
21+
impl ImportReplacer {
22+
pub fn new(unresolved_mark: Mark) -> Self {
23+
ImportReplacer {
24+
unresolved_ctxt: SyntaxContext::empty().apply_mark(unresolved_mark),
25+
has_dynamic_import: false,
26+
wrapper_function_local_ident: private_ident!("$$trackDynamicImport__"),
27+
}
28+
}
29+
}
30+
31+
impl VisitMut for ImportReplacer {
32+
noop_visit_mut_type!();
33+
34+
fn visit_mut_program(&mut self, program: &mut Program) {
35+
program.visit_mut_children_with(self);
36+
// if we wrapped a dynamic import while visiting the children, we need to import the wrapper
37+
38+
if self.has_dynamic_import {
39+
let import_args = MakeNamedImportArgs {
40+
original_ident: quote_ident!("trackDynamicImport").into(),
41+
local_ident: self.wrapper_function_local_ident.clone(),
42+
source: "private-next-rsc-track-dynamic-import",
43+
unresolved_ctxt: self.unresolved_ctxt,
44+
};
45+
match program {
46+
Program::Module(module) => {
47+
prepend_stmt(&mut module.body, make_named_import_esm(import_args));
48+
}
49+
Program::Script(script) => {
50+
// CJS modules can still use `import()`. for CJS, we have to inject the helper
51+
// using `require` instead of `import` to avoid accidentally turning them
52+
// into ESM modules.
53+
prepend_stmt(&mut script.body, make_named_import_cjs(import_args));
54+
}
55+
}
56+
}
57+
}
58+
59+
fn visit_mut_expr(&mut self, expr: &mut Expr) {
60+
expr.visit_mut_children_with(self);
61+
62+
// before: `import(...)`
63+
// after: `$$trackDynamicImport__(import(...))`
64+
65+
if let Expr::Call(CallExpr {
66+
callee: Callee::Import(_),
67+
..
68+
}) = expr
69+
{
70+
self.has_dynamic_import = true;
71+
let replacement_expr = quote!(
72+
"$wrapper_fn($expr)" as Expr,
73+
wrapper_fn = self.wrapper_function_local_ident.clone(),
74+
expr: Expr = expr.take()
75+
)
76+
.with_span(PURE_SP);
77+
*expr = replacement_expr
78+
}
79+
}
80+
}
81+
82+
struct MakeNamedImportArgs<'a> {
83+
original_ident: Ident,
84+
local_ident: Ident,
85+
source: &'a str,
86+
unresolved_ctxt: SyntaxContext,
87+
}
88+
89+
fn make_named_import_esm(args: MakeNamedImportArgs) -> ModuleItem {
90+
let MakeNamedImportArgs {
91+
original_ident,
92+
local_ident,
93+
source,
94+
..
95+
} = args;
96+
let mut item = quote!(
97+
"import { $original_ident as $local_ident } from 'dummy'" as ModuleItem,
98+
original_ident = original_ident,
99+
local_ident = local_ident,
100+
);
101+
// the import source cannot be parametrized in `quote!()`, so patch it manually
102+
let decl = item.as_mut_module_decl().unwrap().as_mut_import().unwrap();
103+
decl.src = Box::new(source.into());
104+
item
105+
}
106+
107+
fn make_named_import_cjs(args: MakeNamedImportArgs) -> Stmt {
108+
let MakeNamedImportArgs {
109+
original_ident,
110+
local_ident,
111+
source,
112+
unresolved_ctxt,
113+
} = args;
114+
quote!(
115+
"const { [$original_name]: $local_ident } = $require($source)" as Stmt,
116+
original_name: Expr = quote_str!(original_ident.sym).into(),
117+
local_ident = local_ident,
118+
source: Expr = quote_str!(source).into(),
119+
// the builtin `require` is considered an unresolved identifier.
120+
// we have to match that, or it won't be recognized as
121+
// a proper `require()` call.
122+
require = quote_ident!(unresolved_ctxt, "require")
123+
)
124+
}

crates/next-custom-transforms/tests/fixture.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ use next_custom_transforms::transforms::{
2121
server_actions::{self, server_actions, ServerActionsMode},
2222
shake_exports::{shake_exports, Config as ShakeExportsConfig},
2323
strip_page_exports::{next_transform_strip_page_exports, ExportFilter},
24+
track_dynamic_imports::track_dynamic_imports,
2425
warn_for_edge_runtime::warn_for_edge_runtime,
2526
};
2627
use rustc_hash::FxHashSet;
@@ -930,6 +931,29 @@ fn test_source_maps(input: PathBuf) {
930931
);
931932
}
932933

934+
#[fixture("tests/fixture/track-dynamic-imports/**/input.js")]
935+
fn track_dynamic_imports_fixture(input: PathBuf) {
936+
let output = input.parent().unwrap().join("output.js");
937+
test_fixture(
938+
syntax(),
939+
&|_tr| {
940+
let unresolved_mark = Mark::new();
941+
let top_level_mark = Mark::new();
942+
(
943+
resolver(unresolved_mark, top_level_mark, false),
944+
track_dynamic_imports(unresolved_mark),
945+
)
946+
},
947+
&input,
948+
&output,
949+
FixtureTestConfig {
950+
// auto detect script/module to test CJS handling
951+
module: None,
952+
..Default::default()
953+
},
954+
);
955+
}
956+
933957
fn lint_to_fold<R>(r: R) -> impl Pass
934958
where
935959
R: Visit,
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export default async function Page() {
2+
const { foo } = await import('some-module')
3+
return foo()
4+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { trackDynamicImport as $$trackDynamicImport__ } from "private-next-rsc-track-dynamic-import";
2+
export default async function Page() {
3+
const { foo } = await /*#__PURE__*/ $$trackDynamicImport__(import('some-module'));
4+
return foo();
5+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export default async function Page() {
2+
await import((await import('get-name')).default)
3+
return null
4+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { trackDynamicImport as $$trackDynamicImport__ } from "private-next-rsc-track-dynamic-import";
2+
export default async function Page() {
3+
await /*#__PURE__*/ $$trackDynamicImport__(import((await /*#__PURE__*/ $$trackDynamicImport__(import('get-name'))).default));
4+
return null;
5+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export default async function Page() {
2+
const { foo } = await import('some-module')
3+
// name conflict
4+
$$trackDynamicImport__()
5+
return foo()
6+
}
7+
8+
export function $$trackDynamicImport__() {}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { trackDynamicImport as $$trackDynamicImport__ } from "private-next-rsc-track-dynamic-import";
2+
export default async function Page() {
3+
const { foo } = await /*#__PURE__*/ $$trackDynamicImport__(import('some-module'));
4+
// name conflict
5+
$$trackDynamicImport__1();
6+
return foo();
7+
}
8+
function $$trackDynamicImport__1() {}
9+
export { $$trackDynamicImport__1 as $$trackDynamicImport__ };
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
const promise = import('some-module')
2+
3+
export default async function Page() {
4+
const { foo } = await promise
5+
return foo()
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { trackDynamicImport as $$trackDynamicImport__ } from "private-next-rsc-track-dynamic-import";
2+
const promise = /*#__PURE__*/ $$trackDynamicImport__(import('some-module'));
3+
export default async function Page() {
4+
const { foo } = await promise;
5+
return foo();
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
async function foo() {
2+
const { foo } = await import('some-module')
3+
return foo()
4+
}
5+
6+
exports.foo = foo
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
const { ["trackDynamicImport"]: $$trackDynamicImport__ } = require("private-next-rsc-track-dynamic-import");
2+
async function foo() {
3+
const { foo } = await /*#__PURE__*/ $$trackDynamicImport__(import('some-module'));
4+
return foo();
5+
}
6+
exports.foo = foo;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/// <reference types="./next" />
2+
/// <reference types="./modules" />
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
declare module 'some-module' {
2+
export function foo(): null
3+
}
4+
declare module 'get-name' {
5+
const name: string
6+
export default name
7+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
declare module 'private-next-rsc-track-dynamic-import' {
2+
export function trackDynamicImport<T>(promise: Promise<T>): Promise<T>
3+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"compilerOptions": {
3+
"noEmit": true,
4+
"rootDir": ".",
5+
6+
"allowJs": true,
7+
"checkJs": true,
8+
9+
"lib": ["ESNext", "DOM"],
10+
"skipLibCheck": true,
11+
12+
"strict": true,
13+
"jsx": "preserve",
14+
15+
"target": "ESNext",
16+
"esModuleInterop": true,
17+
"module": "Preserve",
18+
"moduleResolution": "bundler",
19+
"moduleDetection": "force"
20+
},
21+
"files": ["./index.ts"], // loads ambient declarations for modules used in tests
22+
"include": ["./**/*/input.js", "./**/*/output.js"]
23+
}

0 commit comments

Comments
 (0)