Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/ast/e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2154,6 +2154,12 @@ pub struct Import {
pub expr: ExprNodeIndex,
pub options: ExprNodeIndex,
pub import_record_index: u32,
/// True for `import.defer(...)` — the dynamic form of the TC39
/// "Deferred Module Evaluation" proposal. The module graph is fetched
/// and linked, but evaluation is deferred until a property of the
/// returned namespace is accessed.
/// https://tc39.es/proposal-defer-import-eval/
pub phase_defer: bool,
// TODO:
// Comments inside "import()" expressions have special meaning for Webpack.
// Preserving comments inside these expressions makes it possible to use
Expand Down
1 change: 1 addition & 0 deletions src/ast/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2546,6 +2546,7 @@ impl Data {
expr: el.expr.deep_clone_no_detach(bump)?,
options: el.options.deep_clone_no_detach(bump)?,
import_record_index: el.import_record_index,
phase_defer: el.phase_defer,
});
Ok(Data::EImport(StoreRef::from_bump(item)))
}
Expand Down
7 changes: 4 additions & 3 deletions src/ast/import_record.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,10 @@ bitflags::bitflags! {
const WRAP_WITH_TO_ESM = 1 << 13;
const WRAP_WITH_TO_COMMONJS = 1 << 14;

/// "import defer * as ns from 'path'" — defer evaluation of the
/// imported module until a property on the namespace object is
/// accessed. Requires `CONTAINS_IMPORT_STAR`.
/// "import defer * as ns from 'path'" (kind `Stmt`, requires
/// `CONTAINS_IMPORT_STAR`) or "import.defer('path')" (kind `Dynamic`)
/// — defer evaluation of the imported module until a property on the
/// namespace object is accessed.
const PHASE_DEFER = 1 << 15;
}
}
Expand Down
5 changes: 5 additions & 0 deletions src/js_parser/p.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1106,6 +1106,9 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
(state.is_await_target && self.fn_or_arrow_data_visit.try_body_count != 0)
|| state.is_then_catch_target,
);
self.import_records.items_mut()[import_record_index as usize]
.flags
.set(bun_ast::ImportRecordFlags::PHASE_DEFER, state.phase_defer);
self.import_records_for_current_part
.push(import_record_index);

Expand All @@ -1114,6 +1117,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
expr: arg,
import_record_index,
options: state.import_options,
phase_defer: state.phase_defer,
},
state.loc,
);
Expand All @@ -1137,6 +1141,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
expr: arg,
options: state.import_options,
import_record_index: u32::MAX,
phase_defer: state.phase_defer,
},
Comment thread
robobun marked this conversation as resolved.
state.loc,
)
Expand Down
19 changes: 17 additions & 2 deletions src/js_parser/parse/parse_import_export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,26 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
/// Note: The caller has already parsed the "import" keyword
pub fn parse_import_expr(&mut self, loc: bun_ast::Loc, level: Level) -> Result<Expr, Error> {
let p = self;
// "import.defer(...)" — the dynamic form of the TC39 "Deferred Module
// Evaluation" proposal. Unlike "import.meta" it is a dynamic import,
// so it does not mark the file as ESM.
let mut phase_defer = false;
// Parse an "import.meta" expression
if p.lexer.token == T::TDot {
p.esm_import_keyword = js_lexer::range_of_identifier(p.source, loc);
p.lexer.next()?;
if p.lexer.is_contextual_keyword(b"meta") {
p.esm_import_keyword = js_lexer::range_of_identifier(p.source, loc);
p.lexer.next()?;
p.has_import_meta = true;
return Ok(p.new_expr(E::ImportMeta {}, loc));
} else if p.lexer.is_contextual_keyword(b"defer") {
// ImportCall : `import` `.` `defer` `(` AssignmentExpression `,`? `)`
// `is_contextual_keyword` compares the raw token, so an escaped
// `def\u0065r` falls through to the error branch instead.
p.lexer.next()?;
phase_defer = true;
} else {
p.lexer.expected_string(b"\"meta\"")?;
p.lexer.expected_string(b"\"meta\" or \"defer\"")?;
}
Comment thread
robobun marked this conversation as resolved.
}

Expand Down Expand Up @@ -83,11 +93,15 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
if let Some(slice) = slice_opt {
let import_record_index =
p.add_import_record(bun_ast::ImportKind::Dynamic, value.loc, slice);
p.import_records.items_mut()[import_record_index as usize]
.flags
.set(bun_ast::ImportRecordFlags::PHASE_DEFER, phase_defer);
return Ok(p.new_expr(
E::Import {
expr: value,
import_record_index,
options: import_options,
phase_defer,
},
loc,
));
Expand All @@ -102,6 +116,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
// .leading_interior_comments = comments,
import_record_index: u32::MAX,
options: import_options,
phase_defer,
},
loc,
))
Expand Down
3 changes: 3 additions & 0 deletions src/js_parser/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -927,6 +927,8 @@ pub struct TransposeState {
pub import_record_tag: Option<bun_ast::ImportRecordTag>,
pub import_loader: Option<bun_ast::Loader>,
pub import_options: Expr,
/// True when transposing an `import.defer(...)` expression.
pub phase_defer: bool,
}

impl Default for TransposeState {
Expand All @@ -939,6 +941,7 @@ impl Default for TransposeState {
import_record_tag: None,
import_loader: None,
import_options: Expr::EMPTY,
phase_defer: false,
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/js_parser/repl_transforms.rs
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ impl<'a, const TS: bool, const SCAN: bool> P<'a, TS, SCAN> {
// import X from 'mod' -> var X = (await import('mod')).default
// import { a, b } from 'mod' -> var {a, b} = await import('mod')
// import * as X from 'mod' -> var X = await import('mod')
// import defer * as X from 'mod' -> var X = await import.defer('mod')
// import 'mod' -> await import('mod')
let path_str: &'static [u8] = self.import_records.items()
[import_data.import_record_index as usize]
Expand All @@ -268,6 +269,7 @@ impl<'a, const TS: bool, const SCAN: bool> P<'a, TS, SCAN> {
expr: str_expr,
options: Expr::EMPTY,
import_record_index: u32::MAX,
phase_defer: import_data.phase_defer,
},
stmt.loc,
);
Expand Down
1 change: 1 addition & 0 deletions src/js_parser/visit/visit_expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1835,6 +1835,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
import_options: e_.options,
loc: e_.expr.loc,
import_loader: e_.import_record_loader(),
phase_defer: e_.phase_defer,
..Default::default()
};

Expand Down
14 changes: 13 additions & 1 deletion src/js_printer/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2975,7 +2975,17 @@ pub mod __gated_printer {

// Allow it to fail at runtime, if it should
if module_type != bundle_opts::Format::InternalBakeDev {
self.print(b"import(");
if record.flags.contains(ImportRecordFlags::PHASE_DEFER) && !wrap_with_to_esm {
// `import.defer(...)` — keep the defer phase so the engine
// defers evaluation of the imported module. When the record
// needs the `__toESM` interop wrapper (cross-chunk CommonJS
// target), the `.then((m)=>__toESM(m.default))` below would
// touch the namespace immediately and defeat the defer, so
// degrade to a regular dynamic import instead.
self.print(b"import.defer(");
} else {
self.print(b"import(");
}
self.print_import_record_path(record);
} else {
self.print_symbol(self.options.hmr_ref);
Expand Down Expand Up @@ -3640,6 +3650,8 @@ pub mod __gated_printer {
if self.options.module_type == bundle_opts::Format::InternalBakeDev {
self.print_symbol(self.options.hmr_ref);
self.print(b".dynamicImport(");
} else if e.phase_defer {
self.print(b"import.defer(");
} else {
self.print(b"import(");
}
Expand Down
10 changes: 6 additions & 4 deletions src/jsc/bindings/NodeVM.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ JSC::JSFunction* constructAnonymousFunction(JSC::JSGlobalObject* globalObject, c
return function;
}

JSPromise* importModule(JSGlobalObject* globalObject, JSString* moduleName, RefPtr<JSC::ScriptFetchParameters> parameters, const SourceOrigin& sourceOrigin)
JSPromise* importModule(JSGlobalObject* globalObject, JSString* moduleName, RefPtr<JSC::ScriptFetchParameters> parameters, const SourceOrigin& sourceOrigin, bool deferred)
{
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
Expand All @@ -274,7 +274,7 @@ JSPromise* importModule(JSGlobalObject* globalObject, JSString* moduleName, RefP
if (isUseMainContextDefaultLoaderConstant(globalObject, dynamicImportCallback)) {
auto defer = fetcher->temporarilyUseDefaultLoader();
Zig::GlobalObject* zigGlobalObject = defaultGlobalObject(globalObject);
RELEASE_AND_RETURN(scope, zigGlobalObject->moduleLoaderImportModule(zigGlobalObject, zigGlobalObject->moduleLoader(), moduleName, WTF::move(parameters), sourceOrigin, false));
RELEASE_AND_RETURN(scope, zigGlobalObject->moduleLoaderImportModule(zigGlobalObject, zigGlobalObject->moduleLoader(), moduleName, WTF::move(parameters), sourceOrigin, deferred));
} else if (!dynamicImportCallback || !dynamicImportCallback.isCallable()) {
throwException(globalObject, scope, createError(globalObject, ErrorCode::ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING, "A dynamic import callback was not specified."_s));
return nullptr;
Expand Down Expand Up @@ -1478,13 +1478,15 @@ static JSPromise* moduleLoaderImportModuleInner(NodeVMGlobalObject* globalObject

JSPromise* NodeVMGlobalObject::moduleLoaderImportModule(JSGlobalObject* globalObject, JSC::JSModuleLoader* moduleLoader, JSC::JSString* moduleName, RefPtr<JSC::ScriptFetchParameters> parameters, const JSC::SourceOrigin& sourceOrigin, bool deferred)
{
UNUSED_PARAM(deferred);
auto* nodeVmGlobalObject = static_cast<NodeVMGlobalObject*>(globalObject);

if (JSPromise* result = NodeVM::importModule(nodeVmGlobalObject, moduleName, parameters, sourceOrigin)) {
if (JSPromise* result = NodeVM::importModule(nodeVmGlobalObject, moduleName, parameters, sourceOrigin, deferred)) {
return result;
}

// The `importModuleDynamically` callback API has no notion of an import
// phase, so a deferred import resolved through it behaves like a regular
// dynamic import.
return moduleLoaderImportModuleInner(nodeVmGlobalObject, moduleLoader, moduleName, WTF::move(parameters), sourceOrigin);
}

Expand Down
2 changes: 1 addition & 1 deletion src/jsc/bindings/NodeVM.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ NodeVMGlobalObject* getGlobalObjectFromContext(JSGlobalObject* globalObject, JSV
JSC::EncodedJSValue INVALID_ARG_VALUE_VM_VARIATION(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, WTF::ASCIILiteral name, JSC::JSValue value);
// For vm.compileFunction we need to return an anonymous function expression. This code is adapted from/inspired by JSC::constructFunction, which is used for function declarations.
JSC::JSFunction* constructAnonymousFunction(JSC::JSGlobalObject* globalObject, const ArgList& args, const SourceOrigin& sourceOrigin, CompileFunctionOptions&& options, JSC::SourceTaintedOrigin sourceTaintOrigin, JSC::JSScope* scope);
JSPromise* importModule(JSGlobalObject* globalObject, JSString* moduleNameValue, RefPtr<JSC::ScriptFetchParameters> parameters, const SourceOrigin& sourceOrigin);
JSPromise* importModule(JSGlobalObject* globalObject, JSString* moduleNameValue, RefPtr<JSC::ScriptFetchParameters> parameters, const SourceOrigin& sourceOrigin, bool deferred);
bool isContext(JSC::JSGlobalObject* globalObject, JSValue);
bool getContextArg(JSC::JSGlobalObject* globalObject, JSValue& contextArg);
bool isUseMainContextDefaultLoaderConstant(JSC::JSGlobalObject* globalObject, JSValue value);
Expand Down
10 changes: 5 additions & 5 deletions src/jsc/bindings/ZigGlobalObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3486,14 +3486,13 @@ JSC::JSPromise* GlobalObject::moduleLoaderImportModule(JSGlobalObject* jsGlobalO
const SourceOrigin& sourceOrigin,
bool deferred)
{
UNUSED_PARAM(deferred);
auto* globalObject = static_cast<Zig::GlobalObject*>(jsGlobalObject);

VM& vm = JSC::getVM(globalObject);
auto scope = DECLARE_THROW_SCOPE(vm);

{
JSC::JSPromise* result = NodeVM::importModule(globalObject, moduleNameValue, parameters, sourceOrigin);
JSC::JSPromise* result = NodeVM::importModule(globalObject, moduleNameValue, parameters, sourceOrigin, deferred);
RETURN_IF_EXCEPTION(scope, nullptr);
if (result) {
return result;
Expand Down Expand Up @@ -3528,7 +3527,7 @@ JSC::JSPromise* GlobalObject::moduleLoaderImportModule(JSGlobalObject* jsGlobalO
if (auto resolution = globalObject->onLoadPlugins.resolveVirtualModule(moduleName, sourceURL.protocolIsFile() ? sourceOriginStringHolder : String())) {
resolvedIdentifier = JSC::Identifier::fromString(vm, resolution.value());

auto result = JSC::importModule(globalObject, resolvedIdentifier, JSC::Identifier(), parameters, nullptr, /* deferred */ false, referrerAsyncOrder);
auto result = JSC::importModule(globalObject, resolvedIdentifier, JSC::Identifier(), parameters, nullptr, deferred, referrerAsyncOrder);
if (scope.exception()) [[unlikely]] {
return JSC::JSPromise::rejectedPromiseWithCaughtException(globalObject, scope);
}
Expand Down Expand Up @@ -3586,9 +3585,10 @@ JSC::JSPromise* GlobalObject::moduleLoaderImportModule(JSGlobalObject* jsGlobalO

// The C++ module loader now extracts `with.type` into a
// ScriptFetchParameters before calling this hook, so `parameters` is
// already the parsed RefPtr (or null). Just forward it.
// already the parsed RefPtr (or null). Just forward it, along with the
// defer phase of `import.defer(...)`.
auto result = JSC::importModule(globalObject, resolvedIdentifier,
JSC::Identifier(), WTF::move(parameters), nullptr, /* deferred */ false, referrerAsyncOrder);
JSC::Identifier(), WTF::move(parameters), nullptr, deferred, referrerAsyncOrder);
if (scope.exception()) [[unlikely]] {
return JSC::JSPromise::rejectedPromiseWithCaughtException(globalObject, scope);
}
Expand Down
7 changes: 3 additions & 4 deletions src/runtime/bake/BakeGlobalObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,11 @@ bakeModuleLoaderImportModule(JSC::JSGlobalObject* global,
const JSC::SourceOrigin& sourceOrigin,
bool deferred)
{
UNUSED_PARAM(deferred);
WTF::String keyString = moduleNameValue->getString(global);
if (keyString.startsWith("bake:/"_s)) {
auto& vm = JSC::getVM(global);
return JSC::importModule(global, JSC::Identifier::fromString(vm, keyString),
JSC::Identifier(), WTF::move(parameters), nullptr);
JSC::Identifier(), WTF::move(parameters), nullptr, deferred);
}

if (!sourceOrigin.isNull() && sourceOrigin.string().startsWith("bake:/"_s)) {
Expand All @@ -46,11 +45,11 @@ bakeModuleLoaderImportModule(JSC::JSGlobalObject* global,
RETURN_IF_EXCEPTION(scope, nullptr);

return JSC::importModule(global, JSC::Identifier::fromString(vm, result.toWTFString()),
JSC::Identifier(), WTF::move(parameters), nullptr);
JSC::Identifier(), WTF::move(parameters), nullptr, deferred);
}

// TODO: make static cast instead of jscast
return uncheckedDowncast<Zig::GlobalObject>(global)->moduleLoaderImportModule(global, moduleLoader, moduleNameValue, WTF::move(parameters), sourceOrigin, false);
return uncheckedDowncast<Zig::GlobalObject>(global)->moduleLoaderImportModule(global, moduleLoader, moduleNameValue, WTF::move(parameters), sourceOrigin, deferred);
}

JSC::Identifier bakeModuleLoaderResolve(JSC::JSGlobalObject* jsGlobal,
Expand Down
71 changes: 71 additions & 0 deletions test/bundler/bundler_edgecase.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2541,6 +2541,77 @@ describe("bundler", () => {
`);
},
});
// Dynamic `import.defer()` (TC39 deferred module evaluation). When the
// target module is inlined into the bundle, the defer phase is dropped —
// same documented limitation as static `import defer` — and it behaves
// like a regular dynamic import.
itBundled("edgecase/DynamicImportDeferBundled", {
files: {
"/entry.js": /* js */ `
const ns = await import.defer("./dep.js");
console.log("after import");
console.log("value:", ns.value);
`,
"/dep.js": /* js */ `
console.log("dep evaluated");
export const value = 42;
`,
},
target: "bun",
run: {
stdout: "dep evaluated\nafter import\nvalue: 42",
},
});
// When the target stays external, the defer phase must be preserved in the
// emitted code so the runtime can still defer evaluation.
itBundled("edgecase/DynamicImportDeferExternal", {
files: {
"/entry.js": /* js */ `
export async function load() {
const ns = await import.defer("x-external");
return ns.value;
}
export function loadComputed(name) {
return import.defer(name);
}
`,
},
external: ["x-external"],
target: "bun",
onAfterBundle(api) {
const out = api.readFile("/out.js");
expect(out).toContain('import.defer("x-external")');
expect(out).toContain("import.defer(name)");
},
});
// With code splitting, a deferred dynamic import of a CommonJS module needs
// the `__toESM` interop wrapper, whose `.then((m) => __toESM(m.default))`
// callback touches the namespace immediately. Emitting `import.defer()`
// there would be pointless (it would be defeated on the same line), so the
// defer phase is dropped and a regular dynamic import is emitted.
itBundled("edgecase/DynamicImportDeferSplittingCommonJS", {
files: {
"/entry.js": /* js */ `
const ns = await import.defer("./dep.cjs");
console.log("value:", ns.default.value);
`,
"/dep.cjs": /* js */ `
module.exports = { value: 7 };
`,
},
splitting: true,
outdir: "/out",
target: "bun",
onAfterBundle(api) {
const out = api.readFile("/out/entry.js");
expect(out).toContain("__toESM(");
expect(out).not.toContain("import.defer(");
},
run: {
file: "/out/entry.js",
stdout: "value: 7",
},
});
});

for (const backend of ["api", "cli"] as const) {
Expand Down
Loading
Loading