Skip to content
Draft
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
1 change: 1 addition & 0 deletions crates/pack-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ swc_core = { workspace = true, features = [
"ecma_parser",
"ecma_parser_typescript",
"ecma_preset_env",
"ecma_quote",
"ecma_transforms",
"ecma_transforms_optimization",
"ecma_transforms_react",
Expand Down
7 changes: 5 additions & 2 deletions crates/pack-core/js/src/library/runtime-backend-dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,11 @@ contextPrototype.Q = loadScript;
}

if (params.runtimeModuleIds.length > 0) {
for (const moduleId of params.runtimeModuleIds) {
getOrInstantiateRuntimeModule(chunk as ChunkPath, moduleId);
for (let i = 0; i < params.runtimeModuleIds.length; i++) {
getOrInstantiateRuntimeModule(
chunk as ChunkPath,
params.runtimeModuleIds[i],
);
}
}
},
Expand Down
7 changes: 7 additions & 0 deletions crates/pack-core/src/client/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ use crate::{
default_export_namer::get_default_export_namer_rule,
emotion::get_emotion_transform_rule, jsx_dev_filename::get_jsx_dev_filename_rule,
jsx_import_preserver::get_jsx_import_preserver_rule,
legacy_for_of::get_legacy_for_of_rule,
remove_console::get_remove_console_transform_rule,
styled_components::get_styled_components_transform_rule,
styled_jsx::get_styled_jsx_transform_rule,
Expand Down Expand Up @@ -371,6 +372,12 @@ pub async fn get_client_module_options_context(
client_rules.push(get_type_only_import_rule(enable_mdx_rs.is_some()));
foreign_client_rules.push(get_type_only_import_rule(enable_mdx_rs.is_some()));

if target_browsers.await?.ie.is_some() {
let legacy_for_of_rule = get_legacy_for_of_rule(enable_mdx_rs.is_some());
client_rules.push(legacy_for_of_rule.clone());
foreign_client_rules.push(legacy_for_of_rule);
}

// Ignore .d.ts files - they are TypeScript declaration files and should not be bundled
let ignore_dts_rule = ModuleRule::new(
turbopack::module_options::RuleCondition::ResourcePathEndsWith(".d.ts".to_string()),
Expand Down
4 changes: 4 additions & 0 deletions crates/pack-core/src/library/chunking_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -715,6 +715,10 @@ impl ChunkingContext for LibraryChunkingContext {
..Default::default()
},
);
// Use default CSS chunking to keep single-file libraries in one chunk.
if !self.shared_chunks {
return Ok(Vc::cell(map));
}
map.insert(
ResolvedVc::upcast(Vc::<CssChunkType>::default().to_resolved().await?),
ChunkingConfig {
Expand Down
7 changes: 6 additions & 1 deletion crates/pack-core/src/library/ecmascript/target.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ use turbopack_core::{
};
use turbopack_ecmascript::parse::{IdentCollector, generate_js_source_map};

use crate::shared::transforms::legacy_for_of::add_legacy_for_of_fallback;

pub(super) enum CodegenStage {
Lower,
EmitMinified,
Expand All @@ -50,6 +52,7 @@ pub(super) async fn generate_library_code(
stage: CodegenStage,
) -> Result<Code> {
let versions = *environment.runtime_versions().await?;
let legacy_for_of = versions.ie.is_some();
let target = if *environment
.runtime_versions()
.supports_arrow_functions()
Expand Down Expand Up @@ -121,9 +124,11 @@ pub(super) async fn generate_library_code(
.into(),
Assumptions::default(),
));
let has_for_of_helper =
legacy_for_of && add_legacy_for_of_fallback(&mut program, unresolved_mark);
let statements_before = statement_count(&program);
program.mutate(inject_helpers(unresolved_mark));
statement_count(&program) > statements_before
has_for_of_helper || statement_count(&program) > statements_before
})
} else {
false
Expand Down
155 changes: 155 additions & 0 deletions crates/pack-core/src/shared/transforms/legacy_for_of.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
use anyhow::Result;
use async_trait::async_trait;
use swc_core::{
common::{DUMMY_SP, Mark, SyntaxContext, util::take::Take},
ecma::{
ast::*,
transforms::base::resolver,
utils::{prepend_stmt, private_ident},
visit::{VisitMut, VisitMutWith},
},
quote,
};
use turbopack::module_options::ModuleRule;
use turbopack_ecmascript::{CustomTransformer, TransformContext};

use super::{EcmascriptTransformStage, get_ecma_transform_rule};

pub fn get_legacy_for_of_rule(enable_mdx_rs: bool) -> ModuleRule {
get_ecma_transform_rule(
Box::new(LegacyForOfTransformer),
enable_mdx_rs,
EcmascriptTransformStage::Postprocess,
)
}

#[derive(Debug)]
struct LegacyForOfTransformer;

#[async_trait]
impl CustomTransformer for LegacyForOfTransformer {
async fn transform(&self, program: &mut Program, ctx: &TransformContext<'_>) -> Result<()> {
add_legacy_for_of_fallback(program, ctx.unresolved_mark);
Ok(())
}
}

/// SWC's strict for-of lowering assumes Symbol.iterator exists. Supply Babel-compatible
/// fallbacks while retaining SWC's iterator closing, labels and error handling unchanged.
/// Run immediately after preset-env, before emission loses the generated-node provenance.
pub(crate) fn add_legacy_for_of_fallback(program: &mut Program, unresolved_mark: Mark) -> bool {
let helper = private_ident!("__utoo_iterator__");
let mut visitor = LegacyForOfVisitor {
helper: helper.clone(),
changed: false,
};
program.visit_mut_with(&mut visitor);
if !visitor.changed {
return false;
}

// Keep the real iterator first, including overrides on arrays. Only the fallback copies
// strings/Arguments/typed arrays; ordinary arrays retain their live length semantics.
let statement = quote!(
"function iterator(value) {
var method = typeof Symbol !== 'undefined' && value[Symbol.iterator] || value['@@iterator'];
if (method) return method.call(value);
if (!Array.isArray(value)) {
var name;
if (typeof value === 'string') name = 'Arguments';
else {
name = Object.prototype.toString.call(value).slice(8, -1);
if (name === 'Object' && value.constructor) name = value.constructor.name;
}
if (name === 'Map' || name === 'Set') value = Array.from(value);
else if (name === 'Arguments' || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(name)) {
var copy = Array(value.length);
for (var i = 0; i < copy.length; i++) copy[i] = value[i];
value = copy;
} else throw new TypeError('Invalid attempt to iterate non-iterable instance.');
}
var index = 0;
return { next: function() {
return index >= value.length ? { done: true } : { done: false, value: value[index++] };
} };
}" as Stmt
);
let mut helper_program = Program::Script(Script {
body: vec![statement],
..Default::default()
});
helper_program.mutate(resolver(unresolved_mark, Mark::new(), false));
let Program::Script(mut script) = helper_program else {
unreachable!();
};
let mut statement = script.body.pop().unwrap();
let Stmt::Decl(Decl::Fn(function)) = &mut statement else {
unreachable!();
};
function.ident = helper;
match program {
Program::Module(module) => prepend_stmt(&mut module.body, statement.into()),
Program::Script(script) => prepend_stmt(&mut script.body, statement),
}
true
}

struct LegacyForOfVisitor {
helper: Ident,
changed: bool,
}

impl VisitMut for LegacyForOfVisitor {
fn visit_mut_for_stmt(&mut self, statement: &mut ForStmt) {
statement.visit_mut_children_with(self);
let Some(VarDeclOrExpr::VarDecl(declaration)) = &mut statement.init else {
return;
};
let [iterator, step] = declaration.decls.as_mut_slice() else {
return;
};
// Source-written loops/calls have non-dummy spans. The two fresh bindings and
// generated call identify SWC's strict lowering, not arbitrary Symbol.iterator use.
if declaration.span != DUMMY_SP || iterator.span != DUMMY_SP || step.span != DUMMY_SP {
return;
}
let (Pat::Ident(iterator_ident), Pat::Ident(step_ident)) = (&iterator.name, &step.name)
else {
return;
};
if iterator_ident.id.sym != "_iterator"
|| step_ident.id.sym != "_step"
|| iterator_ident.id.ctxt == SyntaxContext::empty()
|| iterator_ident.id.ctxt != step_ident.id.ctxt
|| step.init.is_some()
{
return;
}
let Some(box Expr::Call(call)) = &mut iterator.init else {
return;
};
if call.span != DUMMY_SP || !call.args.is_empty() {
return;
}
let Callee::Expr(box Expr::Member(member)) = &mut call.callee else {
return;
};
let MemberProp::Computed(property) = &member.prop else {
return;
};
if !matches!(property.expr.as_ref(), Expr::Member(MemberExpr {
obj: box Expr::Ident(Ident { sym, .. }),
prop: MemberProp::Ident(IdentName { sym: property, .. }),
..
}) if sym == "Symbol" && property == "iterator")
{
return;
}
call.args.push(ExprOrSpread {
spread: None,
expr: member.obj.take(),
});
call.callee = Callee::Expr(Box::new(Expr::Ident(self.helper.clone())));
self.changed = true;
}
}
1 change: 1 addition & 0 deletions crates/pack-core/src/shared/transforms/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ pub mod image;
pub mod inline_css;
pub mod jsx_dev_filename;
pub mod jsx_import_preserver;
pub mod legacy_for_of;
pub mod modularize_imports;
pub mod remove_console;
pub mod styled_components;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ const SUPPORT_COMPONENT_CHUNKS = false;
* uses plain Module objects.
*/ let createModuleWithDirectionFlag = false;
const REEXPORTED_OBJECTS = new WeakMap();
// Legacy re-exports need the original CommonJS enumerable keys, not the
// namespace's synthetic getters for non-enumerable properties.
const LEGACY_CJS_SOURCES = typeof Proxy === 'function' ? undefined : new WeakMap();
/**
* Constructs the `__turbopack_context__` object for a module.
*/ function Context(module, exports) {
Expand Down Expand Up @@ -279,6 +282,10 @@ function ensureDynamicExports(module, exports) {
let reexportedObjects = REEXPORTED_OBJECTS.get(module);
if (!reexportedObjects) {
REEXPORTED_OBJECTS.set(module, reexportedObjects = []);
if (typeof Proxy !== 'function') {
module.exports = module.namespaceObject = exports;
return reexportedObjects;
}
// Returns the re-exported object that provides `prop` as an own property,
// or `undefined` if none does. The traps share this logic so they always
// agree on which keys are synthesized from `reexportedObjects`. `default`
Expand Down Expand Up @@ -380,7 +387,20 @@ function ensureDynamicExports(module, exports) {
exports = this.e;
}
const reexportedObjects = ensureDynamicExports(module, exports);
if (typeof object === 'object' && object !== null) {
if (typeof Proxy !== 'function') {
if (object !== null && (typeof object === 'object' || typeof object === 'function')) {
const source = LEGACY_CJS_SOURCES?.has(object) ? LEGACY_CJS_SOURCES.get(object) : object;
// Like webpack, snapshot export names while keeping their values live.
for(const key in source){
if (key !== 'default' && key !== '__esModule') {
defineProp(exports, key, {
enumerable: true,
get: createGetter(source, key)
});
}
}
}
} else if (typeof object === 'object' && object !== null) {
reexportedObjects.push(object);
}
}
Expand Down Expand Up @@ -426,8 +446,10 @@ function createGetter(obj, key) {
*/ function interopEsm(raw, ns, allowExportDefault) {
const bindings = [];
let defaultLocation = -1;
for(let current = raw; (typeof current === 'object' || typeof current === 'function') && !LEAF_PROTOTYPES.includes(current); current = getProto(current)){
for (const key of Object.getOwnPropertyNames(current)){
for(let current = raw; (typeof current === 'object' || typeof current === 'function') && LEAF_PROTOTYPES.indexOf(current) === -1; current = getProto(current)){
const keys = Object.getOwnPropertyNames(current);
for(let i = 0; i < keys.length; i++){
const key = keys[i];
bindings.push(key, createGetter(raw, key));
if (defaultLocation === -1 && key === 'default') {
defaultLocation = bindings.length - 1;
Expand All @@ -446,6 +468,7 @@ function createGetter(obj, key) {
}
}
esm(ns, bindings);
LEGACY_CJS_SOURCES?.set(ns, raw);
return ns;
}
function createNS(raw) {
Expand Down Expand Up @@ -610,7 +633,9 @@ function installCompressedModuleFactories(chunkModules, offset, moduleFactories,
value: values[key]
});
};
relativeURL.prototype = URL.prototype;
if (typeof URL === 'function') {
relativeURL.prototype = URL.prototype;
}
contextPrototype.U = relativeURL;
/**
* Utility function to ensure all variants of an enum are handled.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@
"entry": false,
"recorded": false,
"id": "entries/detail-server.5d841f26.js",
"size": 39218,
"size": 40388,
"hash": "",
"files": [
"entries/detail-server.5d841f26.js"
Expand All @@ -97,7 +97,7 @@
"entry": false,
"recorded": false,
"id": "entries/index-server.5c83aa4e.js",
"size": 39230,
"size": 40400,
"hash": "",
"files": [
"entries/index-server.5c83aa4e.js"
Expand All @@ -109,7 +109,7 @@
"entry": false,
"recorded": false,
"id": "entries/server.5d80c3de.js",
"size": 39197,
"size": 40367,
"hash": "",
"files": [
"entries/server.5d80c3de.js"
Expand Down Expand Up @@ -145,7 +145,7 @@
"type": "asset",
"name": "entries/detail-server.5d841f26.js",
"info": {},
"size": 39218,
"size": 40388,
"emitted": false,
"comparedForEmit": false,
"cached": false,
Expand All @@ -157,7 +157,7 @@
"type": "asset",
"name": "entries/index-server.5c83aa4e.js",
"info": {},
"size": 39230,
"size": 40400,
"emitted": false,
"comparedForEmit": false,
"cached": false,
Expand All @@ -169,7 +169,7 @@
"type": "asset",
"name": "entries/server.5d80c3de.js",
"info": {},
"size": 39197,
"size": 40367,
"emitted": false,
"comparedForEmit": false,
"cached": false,
Expand Down
Loading