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
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,7 @@ pub async fn expand_star_exports(
if key == "default" {
continue;
}
esm_exports.entry(key.clone()).or_insert_with(|| asset);
esm_exports.entry(key.clone()).or_insert(asset);
}
for esm_ref in exports.star_exports.iter() {
if let ReferencedAsset::Some(asset) =
Expand Down
107 changes: 72 additions & 35 deletions turbopack/crates/turbopack-ecmascript/src/references/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -782,8 +782,12 @@ pub(crate) async fn analyse_ecmascript_module_internal(
let (webpack_runtime, webpack_entry, webpack_chunks, mut esm_exports) =
set_handler_and_globals(&handler, globals, || {
// TODO migrate to effects
let mut visitor =
ModuleReferencesVisitor::new(eval_context, &import_references, &mut analysis);
let mut visitor = ModuleReferencesVisitor::new(
eval_context,
&import_references,
&mut analysis,
&var_graph,
);
// ModuleReferencesVisitor has already called analysis.add_esm_reexport_reference
// for any references in esm_exports
program.visit_with_ast_path(&mut visitor, &mut Default::default());
Expand Down Expand Up @@ -3193,13 +3197,15 @@ struct ModuleReferencesVisitor<'a> {
webpack_runtime: Option<(RcStr, Span)>,
webpack_entry: bool,
webpack_chunks: Vec<Lit>,
var_graph: &'a VarGraph,
}

impl<'a> ModuleReferencesVisitor<'a> {
fn new(
eval_context: &'a EvalContext,
import_references: &'a [ResolvedVc<EsmAssetReference>],
analysis: &'a mut AnalyzeEcmascriptModuleResultBuilder,
var_graph: &'a VarGraph,
) -> Self {
Self {
eval_context,
Expand All @@ -3210,6 +3216,28 @@ impl<'a> ModuleReferencesVisitor<'a> {
webpack_runtime: None,
webpack_entry: false,
webpack_chunks: Vec::new(),
var_graph,
}
}
}

impl<'a> ModuleReferencesVisitor<'a> {
/// Returns true if the given export identifier is considered "live". This means it might
/// change values after module evaluation.
fn is_export_ident_live(&self, id: &Ident) -> bool {
if let Some(crate::analyzer::graph::VarMeta {
value: _,
assignment_scopes: assignment_kinds,
}) = self.var_graph.values.get(&id.to_id())
{
// If all assignments are in module scope, the export is not live.
*assignment_kinds != crate::analyzer::graph::AssignmentScopes::AllInModuleEvalScope
} else {
// If we haven't computed a value for it, that means it might be
// A free variable
// an imported variable
// In those cases, we just assume that the value is live since we don't know anything
true
}
}
}
Expand All @@ -3218,10 +3246,10 @@ fn as_parent_path(ast_path: &AstNodePath<AstParentNodeRef<'_>>) -> Vec<AstParent
ast_path.iter().map(|n| n.kind()).collect()
}

fn for_each_ident_in_pat(pat: &Pat, f: &mut impl FnMut(RcStr)) {
fn for_each_ident_in_pat(pat: &Pat, f: &mut impl FnMut(&Ident)) {
match pat {
Pat::Ident(BindingIdent { id, .. }) => {
f(id.sym.as_str().into());
f(id);
}
Pat::Array(ArrayPat { elems, .. }) => elems.iter().for_each(|e| {
if let Some(e) = e {
Expand All @@ -3237,7 +3265,7 @@ fn for_each_ident_in_pat(pat: &Pat, f: &mut impl FnMut(RcStr)) {
for_each_ident_in_pat(value, f);
}
ObjectPatProp::Assign(AssignPatProp { key, .. }) => {
f(key.sym.as_str().into());
f(key);
}
ObjectPatProp::Rest(RestPat { arg, .. }) => {
for_each_ident_in_pat(arg, f);
Expand Down Expand Up @@ -3276,6 +3304,7 @@ impl VisitAstPath for ModuleReferencesVisitor<'_> {
.map(find_turbopack_part_id_in_asserts)
.is_some();

// This is for a statement like `export {a, b as c}` with no `from` clause.
if export.src.is_none() {
for spec in export.specifiers.iter() {
fn to_rcstr(name: &ModuleExportName) -> RcStr {
Expand Down Expand Up @@ -3316,14 +3345,24 @@ impl VisitAstPath for ModuleReferencesVisitor<'_> {
EsmExport::ImportedNamespace(ResolvedVc::upcast(esm_ref))
}
} else {
let liveness = match orig {
ModuleExportName::Ident(ident) => {
if self.is_export_ident_live(ident) {
Liveness::Live
} else {
Liveness::Constant
}
}
ModuleExportName::Str(_) => Liveness::Constant,
};

EsmExport::LocalBinding(
binding_name,
if is_fake_esm {
// it is likely that these are not always actually mutable.
Liveness::Mutable
} else {
// If this is `export {foo} from 'mod'` and `foo` is a const
// in mod then we could export as Const here.
Liveness::Live
liveness
},
)
}
Expand All @@ -3346,30 +3385,27 @@ impl VisitAstPath for ModuleReferencesVisitor<'_> {
) {
{
let decl: &Decl = &export.decl;
let insert_export_binding = &mut |name: RcStr, liveness: Liveness| {
let insert_export_binding = &mut |id: &Ident| {
let liveness = if self.is_export_ident_live(id) {
Liveness::Live
} else {
Liveness::Constant
};
let name: RcStr = id.sym.as_str().into();
self.esm_exports
.insert(name.clone(), EsmExport::LocalBinding(name, liveness));
};
match decl {
Decl::Class(ClassDecl { ident, .. }) | Decl::Fn(FnDecl { ident, .. }) => {
// TODO: examine whether the value is ever mutated rather than just checking
// 'const'
insert_export_binding(ident.sym.as_str().into(), Liveness::Live);
insert_export_binding(ident);
}
Decl::Var(var_decl) => {
// TODO: examine whether the value is ever mutated rather than just checking
// 'const'
let liveness = match var_decl.kind {
VarDeclKind::Var => Liveness::Live,
VarDeclKind::Let => Liveness::Live,
VarDeclKind::Const => Liveness::Constant,
};
let decls = &*var_decl.decls;
decls.iter().for_each(|VarDeclarator { name, .. }| {
for_each_ident_in_pat(name, &mut |name| {
insert_export_binding(name, liveness)
})
});
var_decl
.decls
.iter()
.for_each(|VarDeclarator { name, .. }| {
for_each_ident_in_pat(name, insert_export_binding);
});
}
Decl::Using(_) => {
// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export#:~:text=You%20cannot%20use%20export%20on%20a%20using%20or%20await%20using%20declaration
Expand Down Expand Up @@ -3413,21 +3449,22 @@ impl VisitAstPath for ModuleReferencesVisitor<'_> {
) {
match &export.decl {
DefaultDecl::Class(ClassExpr { ident, .. }) | DefaultDecl::Fn(FnExpr { ident, .. }) => {
self.esm_exports.insert(
rcstr!("default"),
EsmExport::LocalBinding(
ident
.as_ref()
.map(|i| i.sym.as_str().into())
.unwrap_or_else(|| magic_identifier::mangle("default export").into()),
// Default export declarations can only be mutated if they have a name.
if ident.is_some() {
let export = match ident {
Some(ident) => EsmExport::LocalBinding(
ident.sym.as_str().into(),
if self.is_export_ident_live(ident) {
Liveness::Live
} else {
Liveness::Constant
},
),
);
// If there is no name, like `export default function(){}` then it is not live.
None => EsmExport::LocalBinding(
magic_identifier::mangle("default export").into(),
Liveness::Constant,
),
};
self.esm_exports.insert(rcstr!("default"), export);
}
DefaultDecl::TsInterfaceDecl(..) => {
// ignore
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,10 +144,10 @@ impl EcmascriptChunkPlaceable for EcmascriptModuleLocalsModule {
EsmExport::ImportedBinding(..) | EsmExport::ImportedNamespace(..) => {
// not included in locals module
}
EsmExport::LocalBinding(local_name, mutable) => {
EsmExport::LocalBinding(local_name, liveness) => {
exports.insert(
name.clone(),
EsmExport::LocalBinding(local_name.clone(), *mutable),
EsmExport::LocalBinding(local_name.clone(), *liveness),
);
}
EsmExport::Error => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import * as liveDefaultClass from './live_default_class.js'
import * as liveExports from './live_exports.js'
import {
default as liveDefaultClass,
setDefaultClass,
} from './live_default_class.js'
import * as constDefaultExportFunction from './const_default_export_function.js'

it('hoisted declarations are live', () => {
Expand All @@ -12,15 +9,15 @@ it('hoisted declarations are live', () => {
})

it('default class export declarations are live', () => {
expect(liveDefaultClass.default()).toBe('defaultClass')
setDefaultClass(
expect(liveDefaultClass.default.default()).toBe('defaultClass')
liveDefaultClass.setDefaultClass(
class {
static default() {
return 'patched'
}
}
)
expect(liveDefaultClass.default()).toBe('patched')
expect(liveDefaultClass.default.default()).toBe('patched')
})

it('default function export declarations are live', () => {
Expand All @@ -36,10 +33,28 @@ it('exported lets are live', () => {
})

it('exported bindings that are not mutated are not live', () => {
// These should be bound to values, but we don't have the analysis yet
expectGetter(liveExports, 'obviouslyneverMutated')
expectGetter(liveExports, 'neverMutated')
expectGetter(constDefaultExportFunction, 'default')
expect(
Object.getOwnPropertyDescriptor(liveExports, 'obviouslyneverMutated')
).toEqual({
configurable: false,
enumerable: true,
value: 'obviouslyneverMutated',
writable: false,
})
expect(Object.getOwnPropertyDescriptor(liveExports, 'neverMutated')).toEqual({
configurable: false,
enumerable: true,
value: 'neverMutated',
writable: false,
})
expect(
Object.getOwnPropertyDescriptor(constDefaultExportFunction, 'default')
).toEqual({
configurable: false,
enumerable: true,
value: constDefaultExportFunction.default,
writable: false,
})
})

it('exported bindings that are free vars are live', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { local } from 'lib'
import { local, namedFunction, default as defaultFunction } from 'lib'

local()
namedFunction()

defaultFunction()

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading