From 0360300d7a3ef4d2a306e1c1872d3b33a76d7ff4 Mon Sep 17 00:00:00 2001 From: zoomdong <32598811+fireairforce@users.noreply.github.com> Date: Tue, 10 Nov 2026 11:23:38 +0800 Subject: [PATCH 01/10] feat(pack): add native browser module references --- crates/pack-api/src/app.rs | 9 + crates/pack-api/src/library.rs | 9 + crates/pack-core/src/config.rs | 52 + crates/pack-core/src/lib.rs | 1 + crates/pack-core/src/module_reference.rs | 290 +++++ crates/pack-napi/src/lib.rs | 7 + crates/pack-schema/src/lib.rs | 36 + crates/pack-tests/tests/snapshot.rs | 35 +- .../module_references/browser/assert.js | 36 + .../module_references/browser/config.json | 16 + .../browser/input/greeting.ts | 10 + .../module_references/browser/input/index.js | 6 + .../browser/input/runtime.js | 12 + .../module_references/browser/output/main.js | 1060 +++++++++++++++++ .../browser/output/main.js.map | 12 + .../browser/reject-loader.cjs | 3 + .../snapshot/module_references/node/assert.js | 3 + .../module_references/node/config.json | 15 + .../module_references/node/input/greeting.js | 5 + .../module_references/node/input/index.js | 1 + .../module_references/node/output/main.js | 1021 ++++++++++++++++ .../module_references/node/output/main.js.map | 10 + .../reject_bytes/config.json | 41 + .../reject_bytes/expected-error.txt | 1 + .../reject_bytes/input/greeting.js | 1 + .../reject_bytes/input/index.js | 2 + .../reject_inline_loader/config.json | 41 + .../reject_inline_loader/expected-error.txt | 1 + .../reject_inline_loader/input/greeting.js | 1 + .../reject_inline_loader/input/index.js | 2 + .../input/inline-loader.cjs | 1 + .../reject_missing_resource/config.json | 41 + .../expected-error.txt | 1 + .../reject_missing_resource/input/index.js | 1 + .../module_references/reject_text/config.json | 41 + .../reject_text/expected-error.txt | 1 + .../reject_text/input/greeting.js | 1 + .../reject_text/input/index.js | 2 + .../module_references/reject_url/config.json | 41 + .../reject_url/expected-error.txt | 1 + .../reject_url/input/greeting.js | 1 + .../reject_url/input/index.js | 1 + docs/pack-module-references.md | 72 ++ packages/pack-shared/src/config.ts | 27 + packages/pack/config_schema.json | 90 ++ .../src/__test__/moduleReferences.test.ts | 54 + packages/pack/src/binding.d.ts | 2 + packages/pack/src/binding.js | 1 + packages/pack/src/core/moduleReferences.ts | 9 + packages/pack/src/core/project.ts | 6 + packages/pack/src/index.ts | 2 + 51 files changed, 3133 insertions(+), 2 deletions(-) create mode 100644 crates/pack-core/src/module_reference.rs create mode 100644 crates/pack-tests/tests/snapshot/module_references/browser/assert.js create mode 100644 crates/pack-tests/tests/snapshot/module_references/browser/config.json create mode 100644 crates/pack-tests/tests/snapshot/module_references/browser/input/greeting.ts create mode 100644 crates/pack-tests/tests/snapshot/module_references/browser/input/index.js create mode 100644 crates/pack-tests/tests/snapshot/module_references/browser/input/runtime.js create mode 100644 crates/pack-tests/tests/snapshot/module_references/browser/output/main.js create mode 100644 crates/pack-tests/tests/snapshot/module_references/browser/output/main.js.map create mode 100644 crates/pack-tests/tests/snapshot/module_references/browser/reject-loader.cjs create mode 100644 crates/pack-tests/tests/snapshot/module_references/node/assert.js create mode 100644 crates/pack-tests/tests/snapshot/module_references/node/config.json create mode 100644 crates/pack-tests/tests/snapshot/module_references/node/input/greeting.js create mode 100644 crates/pack-tests/tests/snapshot/module_references/node/input/index.js create mode 100644 crates/pack-tests/tests/snapshot/module_references/node/output/main.js create mode 100644 crates/pack-tests/tests/snapshot/module_references/node/output/main.js.map create mode 100644 crates/pack-tests/tests/snapshot/module_references/reject_bytes/config.json create mode 100644 crates/pack-tests/tests/snapshot/module_references/reject_bytes/expected-error.txt create mode 100644 crates/pack-tests/tests/snapshot/module_references/reject_bytes/input/greeting.js create mode 100644 crates/pack-tests/tests/snapshot/module_references/reject_bytes/input/index.js create mode 100644 crates/pack-tests/tests/snapshot/module_references/reject_inline_loader/config.json create mode 100644 crates/pack-tests/tests/snapshot/module_references/reject_inline_loader/expected-error.txt create mode 100644 crates/pack-tests/tests/snapshot/module_references/reject_inline_loader/input/greeting.js create mode 100644 crates/pack-tests/tests/snapshot/module_references/reject_inline_loader/input/index.js create mode 100644 crates/pack-tests/tests/snapshot/module_references/reject_inline_loader/input/inline-loader.cjs create mode 100644 crates/pack-tests/tests/snapshot/module_references/reject_missing_resource/config.json create mode 100644 crates/pack-tests/tests/snapshot/module_references/reject_missing_resource/expected-error.txt create mode 100644 crates/pack-tests/tests/snapshot/module_references/reject_missing_resource/input/index.js create mode 100644 crates/pack-tests/tests/snapshot/module_references/reject_text/config.json create mode 100644 crates/pack-tests/tests/snapshot/module_references/reject_text/expected-error.txt create mode 100644 crates/pack-tests/tests/snapshot/module_references/reject_text/input/greeting.js create mode 100644 crates/pack-tests/tests/snapshot/module_references/reject_text/input/index.js create mode 100644 crates/pack-tests/tests/snapshot/module_references/reject_url/config.json create mode 100644 crates/pack-tests/tests/snapshot/module_references/reject_url/expected-error.txt create mode 100644 crates/pack-tests/tests/snapshot/module_references/reject_url/input/greeting.js create mode 100644 crates/pack-tests/tests/snapshot/module_references/reject_url/input/index.js create mode 100644 docs/pack-module-references.md create mode 100644 packages/pack/src/__test__/moduleReferences.test.ts create mode 100644 packages/pack/src/core/moduleReferences.ts diff --git a/crates/pack-api/src/app.rs b/crates/pack-api/src/app.rs index c90f33ad7..2cc81b0bc 100644 --- a/crates/pack-api/src/app.rs +++ b/crates/pack-api/src/app.rs @@ -153,6 +153,15 @@ impl AppProject { let transition_options = TransitionOptions { named_transitions, + transition_rules: if matches!(&*project.platform().await?, Platform::Web) { + pack_core::module_reference::rules( + project.project_path().owned().await?, + project.config(), + ) + .await? + } else { + Vec::new() + }, ..Default::default() } .cell(); diff --git a/crates/pack-api/src/library.rs b/crates/pack-api/src/library.rs index 06ab9c39a..fdb101e67 100644 --- a/crates/pack-api/src/library.rs +++ b/crates/pack-api/src/library.rs @@ -143,6 +143,15 @@ impl LibraryEndpoint { Ok(ModuleAssetContext::new( // FIXME: TransitionOptions { + transition_rules: if matches!(platform, Platform::Web) { + pack_core::module_reference::rules( + project.project_path().owned().await?, + project.config(), + ) + .await? + } else { + Vec::new() + }, ..Default::default() } .cell(), diff --git a/crates/pack-core/src/config.rs b/crates/pack-core/src/config.rs index ee3f4653e..47287925f 100644 --- a/crates/pack-core/src/config.rs +++ b/crates/pack-core/src/config.rs @@ -1090,10 +1090,52 @@ pub struct ExperimentalConfig { #[derive(Clone, Debug, Deserialize, OperationValue)] #[serde(rename_all = "camelCase")] pub struct ModuleConfig { + #[serde(default)] #[bincode(with = "turbo_bincode::indexmap")] pub rules: FxIndexMap, + #[serde(default)] + pub references: Vec, +} + +/// Browser-only replacement of a source module with framework-provided references. +#[turbo_tasks::value] +#[derive(Clone, Debug, Serialize, Deserialize, OperationValue)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ModuleReferenceConfig { + pub resource: RcStr, + pub references: Vec, + pub exports: Vec, +} + +#[turbo_tasks::value] +#[derive(Clone, Debug, Serialize, Deserialize, OperationValue)] +#[serde(deny_unknown_fields)] +pub struct ModuleReference { + pub id: RcStr, + pub factory: ModuleReferenceFactory, + #[bincode(with = "turbo_bincode::serde_self_describing")] + pub data: JsonValue, +} + +#[turbo_tasks::value] +#[derive(Clone, Debug, Serialize, Deserialize, OperationValue)] +#[serde(deny_unknown_fields)] +pub struct ModuleReferenceFactory { + pub module: RcStr, + pub export: RcStr, +} + +#[turbo_tasks::value] +#[derive(Clone, Debug, Serialize, Deserialize, OperationValue)] +#[serde(deny_unknown_fields)] +pub struct ModuleReferenceExport { + pub name: RcStr, + pub reference: RcStr, } +#[turbo_tasks::value(transparent)] +pub struct ModuleReferences(Vec); + #[turbo_tasks::value] #[derive(Clone, Debug, Serialize, Deserialize, OperationValue)] #[serde(deny_unknown_fields)] @@ -1660,6 +1702,16 @@ impl Config { Vc::cell(self.entry.clone()) } + #[turbo_tasks::function] + pub fn module_references(&self) -> Vc { + Vc::cell( + self.module + .as_ref() + .map(|module| module.references.clone()) + .unwrap_or_default(), + ) + } + #[turbo_tasks::function] pub fn webpack_rules(&self, project_path: FileSystemPath) -> Result> { let Some(turbo_rules) = self.module.as_ref().map(|t| &t.rules) else { diff --git a/crates/pack-core/src/lib.rs b/crates/pack-core/src/lib.rs index 3fa722085..bcbd472b4 100644 --- a/crates/pack-core/src/lib.rs +++ b/crates/pack-core/src/lib.rs @@ -8,6 +8,7 @@ pub mod emit; pub mod import_map; pub mod library; pub mod mode; +pub mod module_reference; pub mod node_polyfill; pub mod server; pub mod server_reference; diff --git a/crates/pack-core/src/module_reference.rs b/crates/pack-core/src/module_reference.rs new file mode 100644 index 000000000..bdf2cab62 --- /dev/null +++ b/crates/pack-core/src/module_reference.rs @@ -0,0 +1,290 @@ +//! Protocol-independent browser module references. The framework owns discovery +//! and transport semantics; the bundler only substitutes explicit module exports. + +use std::{ + collections::{HashMap, HashSet}, + path::{Component, Path}, +}; + +use anyhow::{Context, Result, bail, ensure}; +use turbo_rcstr::RcStr; +use turbo_tasks::{ResolvedVc, Vc}; +use turbo_tasks_fs::{File, FileContent, FileSystemEntryType, FileSystemPath, to_sys_path}; +use turbopack::{ + ModuleAssetContext, + module_options::{RuleCondition, transition_rule::TransitionRule}, + transition::Transition, +}; +use turbopack_core::{ + asset::AssetContent, + context::{AssetContext, ProcessResult}, + reference_type::{EcmaScriptModulesReferenceSubType, ReferenceType}, + source::Source, + virtual_source::VirtualSource, +}; + +use crate::config::{Config, ModuleReferenceConfig}; + +/// Generate only the supplied references. The original module is never read, +/// evaluated, parsed, or included in source-map sourcesContent. +fn generate_reference_module(config: &ModuleReferenceConfig) -> Result { + ensure!( + !config.references.is_empty(), + "module.references must contain a reference" + ); + ensure!( + !config.exports.is_empty(), + "module.references must contain an export" + ); + let mut ids = HashMap::new(); + let mut output = String::new(); + for (index, reference) in config.references.iter().enumerate() { + ensure!( + !reference.id.is_empty(), + "module reference id must not be empty" + ); + ensure!( + ids.insert(reference.id.as_str(), index).is_none(), + "duplicate module reference id: {}", + reference.id + ); + ensure!( + !reference.factory.module.is_empty(), + "module reference factory.module must not be empty" + ); + ensure!( + !reference.factory.export.is_empty(), + "module reference factory.export must not be empty" + ); + output.push_str(&format!( + "import {{ {} as __factory{index} }} from {};\n", + serde_json::to_string(&reference.factory.export)?, + serde_json::to_string(&reference.factory.module)?, + )); + // Parse JSON instead of emitting an object literal: an own __proto__ + // property must remain data rather than changing the object's prototype. + output.push_str(&format!( + "const __reference{index} = __factory{index}(JSON.parse({}));\n", + serde_json::to_string(&serde_json::to_string(&reference.data)?)?, + )); + } + let mut names = HashSet::new(); + for export in &config.exports { + ensure!( + !export.name.is_empty(), + "module reference export name must not be empty" + ); + ensure!( + names.insert(export.name.as_str()), + "duplicate module reference export: {}", + export.name + ); + let index = ids + .get(export.reference.as_str()) + .with_context(|| format!("unknown module reference id: {}", export.reference))?; + output.push_str(&format!( + "export {{ __reference{index} as {} }};\n", + serde_json::to_string(&export.name)?, + )); + } + Ok(output) +} + +fn relative_resource(resource: &str, project: Option<&Path>) -> Result { + ensure!( + !resource.is_empty(), + "module reference resource must not be empty" + ); + let resource = Path::new(resource); + let relative = if resource.is_absolute() { + resource + .strip_prefix( + project.context("absolute module reference resources require a disk filesystem")?, + ) + .context("module reference resource must be inside the project")? + } else { + resource + }; + ensure!( + !relative.components().any(|part| matches!( + part, + Component::ParentDir | Component::RootDir | Component::Prefix(_) + )), + "module reference resource must not escape the project" + ); + let relative = relative + .components() + .filter_map(|part| match part { + Component::Normal(value) => Some(value.to_string_lossy()), + _ => None, + }) + .collect::>() + .join("/"); + ensure!( + !relative.is_empty(), + "module reference resource must identify a file" + ); + Ok(relative) +} + +pub async fn rules(project: FileSystemPath, config: Vc) -> Result> { + let references = config.module_references().await?; + let disk_project = to_sys_path(project.clone()).await?; + let mut seen = HashSet::new(); + let mut rules = Vec::with_capacity(references.len()); + for reference in references.iter() { + let resource = relative_resource(&reference.resource, disk_project.as_deref())?; + if !seen.insert(resource.clone()) { + bail!("duplicate module reference resource: {resource}"); + } + let code = generate_reference_module(reference) + .with_context(|| format!("invalid module reference for {resource}"))?; + let path = project.join(&resource)?; + ensure!( + matches!(*path.get_type().await?, FileSystemEntryType::File), + "module reference resource must be an existing file: {resource}" + ); + rules.push(TransitionRule::new_all( + RuleCondition::ResourcePathEquals(path), + ResolvedVc::upcast(ModuleReferenceTransition { code: code.into() }.resolved_cell()), + )); + } + Ok(rules) +} + +#[turbo_tasks::value] +struct ModuleReferenceTransition { + code: RcStr, +} + +#[turbo_tasks::value_impl] +impl ModuleReferenceTransition { + #[turbo_tasks::function] + async fn generated_source(&self, source: Vc>) -> Result>> { + // A distinct .mjs identity prevents the exact-resource rule from applying + // again and gives source maps only generated reference content. + let ident = source + .ident() + .owned() + .await? + .rename_as("*.[module-reference].mjs"); + Ok(Vc::upcast(VirtualSource::new_with_ident( + ident.into_vc(), + AssetContent::file(FileContent::Content(File::from(self.code.to_string())).cell()), + ))) + } +} + +#[turbo_tasks::value_impl] +impl Transition for ModuleReferenceTransition { + #[turbo_tasks::function] + async fn process( + self: Vc, + source: Vc>, + module_asset_context: Vc, + reference_type: ReferenceType, + ) -> Result> { + ensure!( + supported_reference_type(&reference_type), + "module.references resources cannot be imported as assets or with inline loaders" + ); + // Share the generated source across import kinds/export parts so aliases + // cannot create distinct module objects with an identical asset id. + Ok(module_asset_context.process(self.generated_source(source), reference_type)) + } +} + +fn supported_reference_type(reference_type: &ReferenceType) -> bool { + matches!( + reference_type, + ReferenceType::CommonJs(_) + | ReferenceType::Entry(_) + | ReferenceType::EcmaScriptModules( + EcmaScriptModulesReferenceSubType::Import + | EcmaScriptModulesReferenceSubType::ImportPart(_) + | EcmaScriptModulesReferenceSubType::DynamicImport + | EcmaScriptModulesReferenceSubType::LazyDynamicImport + | EcmaScriptModulesReferenceSubType::Undefined + ) + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + fn config() -> ModuleReferenceConfig { + serde_json::from_value(json!({ + "resource": "src/greeting.ts", + "references": [{ "id": "greeting", "factory": { "module": "./runtime.js", "export": "createReference" }, "data": { "operation": "greet", "__proto__": { "value": 1 } } }], + "exports": [{ "name": "Greeting", "reference": "greeting" }, { "name": "default", "reference": "greeting" }] + })).unwrap() + } + + #[test] + fn aliases_share_one_factory_call_and_json_preserves_data() { + let output = generate_reference_module(&config()).unwrap(); + assert_eq!(output.matches("= __factory0(").count(), 1); + assert!(output.contains("export { __reference0 as \"Greeting\" }")); + assert!(output.contains("export { __reference0 as \"default\" }")); + assert!(output.contains("JSON.parse(")); + assert!(output.contains("__proto__")); + } + + #[test] + fn rejects_ambiguous_or_missing_references() { + let mut duplicate = config(); + duplicate.references.push(duplicate.references[0].clone()); + assert!( + generate_reference_module(&duplicate) + .unwrap_err() + .to_string() + .contains("duplicate module reference id") + ); + let mut duplicate = config(); + duplicate.exports.push(duplicate.exports[0].clone()); + assert!( + generate_reference_module(&duplicate) + .unwrap_err() + .to_string() + .contains("duplicate module reference export") + ); + let mut missing = config(); + missing.exports[0].reference = "missing".into(); + assert!( + generate_reference_module(&missing) + .unwrap_err() + .to_string() + .contains("unknown module reference id") + ); + } + + #[test] + fn resources_are_exact_project_files() { + assert_eq!( + relative_resource("./src/greeting.ts", None).unwrap(), + "src/greeting.ts" + ); + assert_eq!( + relative_resource("/app/src/greeting.ts", Some(Path::new("/app"))).unwrap(), + "src/greeting.ts" + ); + assert!(relative_resource("../other.ts", None).is_err()); + assert!(relative_resource("/other/greeting.ts", Some(Path::new("/app"))).is_err()); + } + + #[test] + fn rejects_asset_import_modes_before_reading_source() { + for kind in ["text", "bytes", "json"] { + assert!(!supported_reference_type( + &ReferenceType::EcmaScriptModules( + EcmaScriptModulesReferenceSubType::ImportWithType(kind.into()) + ) + )); + } + assert!(supported_reference_type(&ReferenceType::EcmaScriptModules( + EcmaScriptModulesReferenceSubType::Import + ))); + } +} diff --git a/crates/pack-napi/src/lib.rs b/crates/pack-napi/src/lib.rs index fc91a6479..566a47fe3 100644 --- a/crates/pack-napi/src/lib.rs +++ b/crates/pack-napi/src/lib.rs @@ -40,6 +40,13 @@ pub mod pack_api; pub mod util; pub mod worker_pool; +/// Capability of the loaded native binary, used by framework integrations to +/// reject unsupported module replacement instead of compiling original sources. +#[napi] +pub fn get_module_references_version() -> u32 { + 1 +} + #[cfg(not(any(feature = "__internal_dhat-heap", feature = "__internal_dhat-ad-hoc")))] #[global_allocator] static ALLOC: turbo_tasks_malloc::TurboMalloc = turbo_tasks_malloc::TurboMalloc; diff --git a/crates/pack-schema/src/lib.rs b/crates/pack-schema/src/lib.rs index 010651ce5..6e91d8c84 100644 --- a/crates/pack-schema/src/lib.rs +++ b/crates/pack-schema/src/lib.rs @@ -985,6 +985,42 @@ pub struct SchemaModuleConfig { #[serde(skip_serializing_if = "Option::is_none")] #[schemars(description = "Module rules configuration")] pub rules: Option>, + /// Exact source modules replaced with generated references in browser builds. + #[serde(skip_serializing_if = "Option::is_none")] + pub references: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct SchemaModuleReferenceConfig { + /// Project-relative path, or absolute path within the project. + pub resource: String, + pub references: Vec, + pub exports: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct SchemaModuleReference { + pub id: String, + pub factory: SchemaModuleReferenceFactory, + /// Browser-visible JSON passed to the factory without protocol interpretation. + pub data: serde_json::Value, +} + +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct SchemaModuleReferenceFactory { + /// Resolved from the replaced resource's directory. + pub module: String, + pub export: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct SchemaModuleReferenceExport { + pub name: String, + pub reference: String, } /// Module rule configuration diff --git a/crates/pack-tests/tests/snapshot.rs b/crates/pack-tests/tests/snapshot.rs index e037ae227..6dba5d048 100644 --- a/crates/pack-tests/tests/snapshot.rs +++ b/crates/pack-tests/tests/snapshot.rs @@ -161,6 +161,21 @@ fn test(resource: PathBuf) { } let _guard = SNAPSHOT_TEST_LOCK.lock().unwrap(); + let expected_error = resource.join("expected-error.txt"); + if expected_error.exists() { + let expected = fs::read_to_string(expected_error).unwrap(); + let output = resource.join("output"); + let error = run(resource).expect_err("fixture must fail before producing output"); + assert!( + format!("{error:#}").contains(expected.trim()), + "unexpected build failure: {error:#}" + ); + assert!( + !output.exists(), + "rejected fixture must not emit browser assets" + ); + return; + } let project_path = run(resource).unwrap(); run_assertion(&project_path).unwrap(); } @@ -190,6 +205,7 @@ fn run_assertion(resource: &Path) -> Result<()> { #[tokio::main(flavor = "multi_thread", worker_threads = 2)] async fn run(resource: PathBuf) -> Result { + let expect_build_error = resource.join("expected-error.txt").exists(); let snapshot_ignore_prefixes = snapshot_ignore_prefixes(&resource)?; let SnapshotProjectOptions { project: project_options, @@ -326,7 +342,7 @@ async fn run(resource: PathBuf) -> Result { let snapshot_ignore_prefixes = Vc::::cell(snapshot_ignore_prefixes) .to_resolved() .await?; - let out_op = run_test_operation(container_op, snapshot_ignore_prefixes); + let out_op = run_test_operation(container_op, snapshot_ignore_prefixes, expect_build_error); read_strongly_consistent_and_apply_effects(snapshot_effects_operation(out_op), |e| e) .await?; read_strongly_consistent_and_apply_effects(output_effects_operation(out_op), |e| e).await?; @@ -524,6 +540,7 @@ fn project_options_from_resource(resource: &Path) -> Result, snapshot_ignore_prefixes: ResolvedVc, + expect_build_error: bool, ) -> Result> { let project_container = container_op.resolve().strongly_consistent().await?; @@ -532,12 +549,26 @@ async fn run_test_operation( get_all_written_entrypoints_with_issues_operation(project_container); let EntrypointsWithIssues { entrypoints: _, - issues: _, + issues, effects: _, } = &*entrypoints_with_issues_op .read_strongly_consistent() .await?; + if expect_build_error { + // Match compiler diagnostics, never snapshot mismatch text. Do not apply + // output effects for rejected builds, matching the public build command. + let errors = issues + .iter() + .filter(|issue| issue.severity <= IssueSeverity::Error) + .collect::>(); + anyhow::ensure!( + !errors.is_empty(), + "expected compiler error diagnostics but build succeeded" + ); + anyhow::bail!("compiler error diagnostics: {errors:?}"); + } + // Get output assets and walk through them let project = project_container.project(); let dist_root = project.dist_root().owned().await?; diff --git a/crates/pack-tests/tests/snapshot/module_references/browser/assert.js b/crates/pack-tests/tests/snapshot/module_references/browser/assert.js new file mode 100644 index 000000000..aadf327ed --- /dev/null +++ b/crates/pack-tests/tests/snapshot/module_references/browser/assert.js @@ -0,0 +1,36 @@ +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const path = require("node:path"); +const vm = require("node:vm"); + +const output = path.join(__dirname, "output"); +for (const filename of fs.readdirSync(output, { recursive: true })) { + const file = path.join(output, filename); + if (!fs.statSync(file).isFile()) continue; + const content = fs.readFileSync(file, "utf8"); + assert.ok(!content.includes("SERVER_IMPLEMENTATION_MUST_NOT_REACH_BROWSER"), filename); + assert.ok(!content.includes("missing-server-implementation"), filename); +} + +const context = { + console, + URL, + document: { currentScript: { src: "https://example.test/main.js" } }, +}; +context.self = context; +vm.runInNewContext(fs.readFileSync(path.join(output, "main.js"), "utf8"), context); + +async function main() { + const api = context.ReferenceDemo; + assert.equal(api.aliasesMatch, true); + assert.equal(api.creations, 1); + assert.equal(api.calls.length, 0); + assert.equal(await api.instance.greet({ name: "Ada" }), "Hello Ada"); + assert.equal(api.calls.length, 1); + assert.equal(api.calls[0].operation, "greet"); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/crates/pack-tests/tests/snapshot/module_references/browser/config.json b/crates/pack-tests/tests/snapshot/module_references/browser/config.json new file mode 100644 index 000000000..59e34ba01 --- /dev/null +++ b/crates/pack-tests/tests/snapshot/module_references/browser/config.json @@ -0,0 +1,16 @@ +{ + "config": { + "entry": [{ "import": "input/index.js", "name": "main", "library": { "name": "ReferenceDemo" } }], + "sourceMaps": true, + "module": { + "rules": { "*.ts": { "loaders": ["./reject-loader.cjs"], "as": "*.js" } }, + "references": [{ + "resource": "input/greeting.ts", + "references": [{ "id": "greeting", "factory": { "module": "./runtime.js", "export": "createReference" }, "data": { "operation": "greet" } }], + "exports": [{ "name": "Greeting", "reference": "greeting" }, { "name": "default", "reference": "greeting" }] + }] + }, + "optimization": { "minify": false, "moduleIds": "named" } + }, + "runtimeType": "Production" +} diff --git a/crates/pack-tests/tests/snapshot/module_references/browser/input/greeting.ts b/crates/pack-tests/tests/snapshot/module_references/browser/input/greeting.ts new file mode 100644 index 000000000..4f6a6297d --- /dev/null +++ b/crates/pack-tests/tests/snapshot/module_references/browser/input/greeting.ts @@ -0,0 +1,10 @@ +import { secret } from "missing-server-implementation"; + +globalThis.SERVER_IMPLEMENTATION_MUST_NOT_REACH_BROWSER = secret; + +export class Greeting { + async greet() { + return secret; + } +} +export default Greeting; diff --git a/crates/pack-tests/tests/snapshot/module_references/browser/input/index.js b/crates/pack-tests/tests/snapshot/module_references/browser/input/index.js new file mode 100644 index 000000000..6d941579b --- /dev/null +++ b/crates/pack-tests/tests/snapshot/module_references/browser/input/index.js @@ -0,0 +1,6 @@ +import DefaultGreeting, { Greeting } from "./greeting.ts"; +import { calls, creations } from "./runtime.js"; + +export const aliasesMatch = DefaultGreeting === Greeting; +export const instance = new Greeting(); +export { calls, creations }; diff --git a/crates/pack-tests/tests/snapshot/module_references/browser/input/runtime.js b/crates/pack-tests/tests/snapshot/module_references/browser/input/runtime.js new file mode 100644 index 000000000..904c514e2 --- /dev/null +++ b/crates/pack-tests/tests/snapshot/module_references/browser/input/runtime.js @@ -0,0 +1,12 @@ +export const calls = []; +export let creations = 0; + +export function createReference(data) { + creations++; + return class { + async greet(input) { + calls.push({ operation: data.operation, input }); + return `Hello ${input.name}`; + } + }; +} diff --git a/crates/pack-tests/tests/snapshot/module_references/browser/output/main.js b/crates/pack-tests/tests/snapshot/module_references/browser/output/main.js new file mode 100644 index 000000000..c847f7929 --- /dev/null +++ b/crates/pack-tests/tests/snapshot/module_references/browser/output/main.js @@ -0,0 +1,1060 @@ +((__UTOOPACK__) => { +if (!Array.isArray(__UTOOPACK__)) { + return; +} + +const CHUNK_BASE_PATH = ""; +const CHUNK_SUFFIX_PATH = ""; +const RELATIVE_ROOT_PATH = ".."; +const RUNTIME_PUBLIC_PATH = ""; +// Library builds deliberately collapse JavaScript into one chunk, so the +// component-chunk runtime path is unsupported in this custom runtime. +const SUPPORT_COMPONENT_CHUNKS = false; +/** + * This file contains runtime types and functions that are shared between all + * TurboPack ECMAScript runtimes. + * + * It will be prepended to the runtime code of each runtime. + */ /* eslint-disable @typescript-eslint/no-unused-vars */ /// +/// +/** + * Describes why a module was instantiated. + * Shared between browser and Node.js runtimes. + */ var SourceType = /*#__PURE__*/ function(SourceType) { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + * SourceData is a ChunkPath. + */ SourceType[SourceType["Runtime"] = 0] = "Runtime"; + /** + * The module was instantiated because a parent module imported it. + * SourceData is a ModuleId. + */ SourceType[SourceType["Parent"] = 1] = "Parent"; + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + * SourceData is an array of ModuleIds or undefined. + */ SourceType[SourceType["Update"] = 2] = "Update"; + return SourceType; +}(SourceType || {}); +/** + * Flag indicating which module object type to create when a module is merged. Set to `true` + * by each runtime that uses ModuleWithDirection (browser dev-base.ts, nodejs dev-base.ts, + * nodejs build-base.ts). Browser production (build-base.ts) leaves it as `false` since it + * uses plain Module objects. + */ let createModuleWithDirectionFlag = false; +/** + * Maps module IDs to the factory functions that instantiate them. + */ const moduleFactories = new Map(); +const REEXPORTED_OBJECTS = new WeakMap(); +/** + * Constructs the `__turbopack_context__` object for a module. + */ function Context(module, exports) { + this.m = module; + // We need to store this here instead of accessing it from the module object to: + // 1. Make it available to factories directly, since we rewrite `this` to + // `__turbopack_context__.e` in CJS modules. + // 2. Support async modules which rewrite `module.exports` to a promise, so we + // can still access the original exports object from functions like + // `esmExport` + // Ideally we could find a new approach for async modules and drop this property altogether. + this.e = exports; +} +const contextPrototype = Context.prototype; +contextPrototype.M = moduleFactories; +const hasOwnProperty = Object.prototype.hasOwnProperty; +const toStringTag = typeof Symbol !== 'undefined' && Symbol.toStringTag; +function defineProp(obj, name, options) { + if (!hasOwnProperty.call(obj, name)) Object.defineProperty(obj, name, options); +} +/** + * Returns the cached module for `id`, or `undefined` if it has not been + * instantiated yet. Rethrows the error if the module's factory threw. + */ function getCachedModule(moduleCache, id) { + const module = moduleCache.get(id); + if (module?.error) { + throw module.error; + } + return module; +} +function getOverwrittenModule(moduleCache, id) { + let module = moduleCache.get(id); + if (module === undefined) { + if (createModuleWithDirectionFlag) { + // set in development modes for hmr support + module = createModuleWithDirection(id); + } else { + module = createModuleObject(id); + } + moduleCache.set(id, module); + } + return module; +} +/** + * Creates the module object. Only done here to ensure all module objects have the same shape. + */ function createModuleObject(id) { + return { + exports: {}, + error: undefined, + id, + namespaceObject: undefined + }; +} +function createModuleWithDirection(id) { + return { + exports: {}, + error: undefined, + id, + namespaceObject: undefined, + parents: [], + children: [] + }; +} +const BindingTag_Value = 0; +/** + * Terminates a module's group of entries in an {@link EsmReexports} list. + */ const REEXPORT_GROUP_END = 0; +/** + * Adds the getters to the exports object. + */ function esm(exports, bindings, dynamic) { + defineProp(exports, '__esModule', { + value: true + }); + if (toStringTag) defineProp(exports, toStringTag, { + value: 'Module' + }); + let i = 0; + while(i < bindings.length){ + const propName = bindings[i++]; + const tagOrFunction = bindings[i++]; + if (typeof tagOrFunction === 'number') { + if (tagOrFunction === BindingTag_Value) { + defineProp(exports, propName, { + value: bindings[i++], + enumerable: true, + writable: false + }); + } else { + throw new Error(`unexpected tag: ${tagOrFunction}`); + } + } else { + const getterFn = tagOrFunction; + if (typeof bindings[i] === 'function') { + const setterFn = bindings[i++]; + defineProp(exports, propName, { + get: getterFn, + set: setterFn, + enumerable: true + }); + } else { + defineProp(exports, propName, { + get: getterFn, + enumerable: true + }); + } + } + } + // The properties defined above are already non-configurable and + // non-writable, so the namespace's existing exports are effectively + // immutable. Sealing additionally makes the object non-extensible, matching + // real ESM-namespace semantics. Modules with dynamic re-exports + // (`export *` from a CommonJS module) must stay extensible so the dynamic + // export proxy can surface keys discovered at runtime, so skip the seal for + // them. + if (!dynamic) Object.seal(exports); +} +/** + * Makes the module an ESM with exports + */ function esmExport(bindings, id, dynamic) { + let module; + let exports; + if (id != null) { + module = getOverwrittenModule(this.c, id); + exports = module.exports; + } else { + module = this.m; + exports = this.e; + } + module.namespaceObject = exports; + esm(exports, bindings, dynamic); +} +contextPrototype.s = esmExport; +/** + * Registers re-exports that all forward to properties of other modules. + * + * This is a compact spelling of the pattern + * + * ```js + * var ns = context.i(moduleId) + * context.s([exportName, () => ns[importedName], ...]) + * ``` + * + * The list is a flat sequence of groups. Each group starts with the source the exports come from, + * followed by that group's entries, and is terminated by the `0` sentinel (or the end of the list). + * + * The group head is either a **module id**, which is instantiated here: + * + * ```js + * context.S([ + * 76061, 'default', 'f', 'named', 'A', 0, + * 29842, 'otherModule', 'f', + * ]) + * ``` + * + * or the **namespace value** of a module that has already been imported, which is used directly: + * + * ```js + * var ns1 = context.i(76061) + * context.S([ns1, 'default', 'f', 'named', 'A']) + * ``` + * + * The producer picks the namespace form when it has generated the import anyway -- because some + * later import must not be reordered past it -- so nothing is instantiated twice. The two are told + * apart by type: a module id is always a string or number. A CommonJS function export produces a + * callable namespace value, so namespace heads can be functions as well as objects. + * + * Entries are `exportName, importedName` pairs, except when a group holds exactly one string. That + * string is then a comma-joined list of the same pairs, which saves the repeated quoting: + * + * ```js + * context.S([ + * 76061, 'default,f,named,A', 0, + * 29842, 'otherModule,f', + * ]) + * ``` + * + * The producer picks that spelling independently for each group whose names contain no commas, + * since that group's names are recovered by splitting on them. + * + * Groups whose head is a module id are instantiated in list order, at the point where the call + * appears, so the producer must not merge such a group across an import of another module. The + * destination reuses a source data value or getter descriptor when one exists, falling back to a + * wrapper getter for dynamic/proxy/inherited properties. + * + * `id` names the module the exports belong to when this module was merged into a scope-hoisting + * group, exactly as it does for {@link EsmExport}. + * + * Only the source descriptor's payload (value or getter) is reused. {@link esm} still defines a + * fresh enumerable, non-configurable destination property, and no source setter is ever forwarded. + */ function esmReexport(list, id) { + const bindings = []; + let i = 0; + while(i < list.length){ + const head = list[i++]; + const start = i; + while(i < list.length && list[i] !== REEXPORT_GROUP_END)i++; + const end = i; + // Skip the sentinel, if this group was terminated by one rather than by the end of the list. + i++; + // Module ids are always strings or numbers. Other values are already-imported namespaces; + // notably, interop with a CommonJS function export produces a callable namespace function. + // `esmImport` may return a promise for an async module, but re-exports of async modules keep + // going through `context.s`, so the producer never routes them here and this stays synchronous. + const namespace = typeof head === 'string' || typeof head === 'number' ? esmImport.call(this, head) : head; + if (end - start === 1) { + const pairs = list[start].split(','); + for(let j = 0; j < pairs.length; j += 2){ + appendReexportBinding(bindings, pairs[j], namespace, pairs[j + 1]); + } + } else { + for(let j = start; j < end; j += 2){ + appendReexportBinding(bindings, list[j], namespace, list[j + 1]); + } + } + } + esmExport.call(this, bindings, id); +} +contextPrototype.S = esmReexport; +function appendReexportBinding(bindings, exportedName, namespace, importedName) { + const descriptor = Reflect.getOwnPropertyDescriptor(namespace, importedName); + if (descriptor) { + if ('value' in descriptor) { + // Code generation only routes immutable imported bindings through this helper, so a data + // descriptor is a constant export and can be captured once. + bindings.push(exportedName, BindingTag_Value, descriptor.value); + return; + } + if (descriptor.get) { + // Accessors remain live by reusing the source getter. `esmReexport` is only called by + // generated code: every group head is either produced by + // `this.i` or is the namespace variable from a generated `this.i` call. Every getter on such + // a namespace is receiver-independent: ESM bindings are compiler-generated arrow functions, + // and the CommonJS/dynamic-namespace paths create arrows in `createGetter` and + // `getOwnPropertyDescriptor`. The destination can therefore reuse the exact function instead + // of allocating another wrapper getter. + bindings.push(exportedName, descriptor.get); + return; + } + } + // Dynamic/proxy/inherited CommonJS edge cases may not expose a usable own descriptor. + bindings.push(exportedName, ()=>namespace[importedName]); +} +function ensureDynamicExports(module, exports) { + let reexportedObjects = REEXPORTED_OBJECTS.get(module); + if (!reexportedObjects) { + REEXPORTED_OBJECTS.set(module, 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` + // is never re-exported by `export *`, so it is never synthesized. + const reexportOwning = (prop)=>{ + if (prop !== 'default') { + for (const obj of reexportedObjects){ + if (hasOwnProperty.call(obj, prop)) return obj; + } + } + return undefined; + }; + // Modules with dynamic re-exports are not sealed by `esm()`, so the + // target beneath the namespace stays extensible. That is what lets the + // `ownKeys` and `getOwnPropertyDescriptor` traps legally report keys that + // exist on `reexportedObjects` but not on the target itself. + module.exports = module.namespaceObject = new Proxy(exports, { + get (target, prop) { + if (hasOwnProperty.call(target, prop) || prop === 'default' || prop === '__esModule') { + return Reflect.get(target, prop); + } + const obj = reexportOwning(prop); + return obj && Reflect.get(obj, prop); + }, + // The namespace is read-only, like a real esm namespace object. The + // re-exported modules can still mutate their own exports (exposed live + // via `get`), but mutating the namespace itself is rejected. Refusing + // here, rather than forwarding to the extensible target, also prevents an + // assignment/definition from shadowing a dynamic re-export. It also + // prevents delete from removing a static export. + set () { + return false; + }, + defineProperty () { + return false; + }, + deleteProperty () { + return false; + }, + // The `has` trap ensures that `'exportName' in starImports` will reflect + // the truth of whether a key is exported. + has (target, prop) { + if (Reflect.has(target, prop)) return true; + if (prop === 'default' || prop === '__esModule') return false; + return reexportOwning(prop) !== undefined; + }, + // ownKeys and getOwnPropertyDescriptor together make the keys enumerable. + // If a value is returned from `ownKeys` but its property descriptor is + // not enumerable, it will not be visible to iterator methods. + // Collectively, they allow code like the following: + // + // ``` + // // module.js re-exports dynamic CJS exports + // export * from './legacyModule.cjs' + // + // // from another JS file, reference the re-exported dynamic values + // import * as Namespace from './module.js' + // Object.keys(Namespace) + // ``` + ownKeys (target) { + const keys = Reflect.ownKeys(target); + for (const obj of reexportedObjects){ + for (const key of Reflect.ownKeys(obj)){ + if (key !== 'default' && !keys.includes(key)) keys.push(key); + } + } + return keys; + }, + getOwnPropertyDescriptor (target, prop) { + const own = Reflect.getOwnPropertyDescriptor(target, prop); + if (own || prop === 'default' || prop === '__esModule') return own; + const obj = reexportOwning(prop); + if (obj) { + // Synthetic keys don't exist on the target, so they MUST be + // reported as configurable. However the set/delete traps above will + // prevent them from actually being changed + return { + enumerable: true, + configurable: true, + get: ()=>Reflect.get(obj, prop) + }; + } + return undefined; + } + }); + } + return reexportedObjects; +} +/** + * Dynamically exports properties from an object + */ function dynamicExport(object, id) { + let module; + let exports; + if (id != null) { + module = getOverwrittenModule(this.c, id); + exports = module.exports; + } else { + module = this.m; + exports = this.e; + } + const reexportedObjects = ensureDynamicExports(module, exports); + if (typeof object === 'object' && object !== null) { + reexportedObjects.push(object); + } +} +contextPrototype.j = dynamicExport; +function exportValue(value, id) { + let module; + if (id != null) { + module = getOverwrittenModule(this.c, id); + } else { + module = this.m; + } + module.exports = value; +} +contextPrototype.v = exportValue; +function exportNamespace(namespace, id) { + let module; + if (id != null) { + module = getOverwrittenModule(this.c, id); + } else { + module = this.m; + } + module.exports = module.namespaceObject = namespace; +} +contextPrototype.n = exportNamespace; +function createGetter(obj, key) { + return ()=>obj[key]; +} +/** + * @returns prototype of the object + */ const getProto = Object.getPrototypeOf ? (obj)=>Object.getPrototypeOf(obj) : (obj)=>obj.__proto__; +/** Prototypes that are not expanded for exports */ const LEAF_PROTOTYPES = [ + null, + getProto({}), + getProto([]), + getProto(getProto) +]; +/** + * @param raw + * @param ns + * @param allowExportDefault + * * `false`: will have the raw module as default export + * * `true`: will have the default property as default export + */ 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)){ + bindings.push(key, createGetter(raw, key)); + if (defaultLocation === -1 && key === 'default') { + defaultLocation = bindings.length - 1; + } + } + } + // this is not really correct + // we should set the `default` getter if the imported module is a `.cjs file` + if (!(allowExportDefault && defaultLocation >= 0)) { + // Replace the binding with one for the namespace itself in order to preserve iteration order. + if (defaultLocation >= 0) { + // Replace the getter with the value + bindings.splice(defaultLocation, 1, BindingTag_Value, raw); + } else { + bindings.push('default', BindingTag_Value, raw); + } + } + esm(ns, bindings); + return ns; +} +function createNS(raw) { + if (typeof raw === 'function') { + return function(...args) { + return raw.apply(this, args); + }; + } else { + return Object.create(null); + } +} +function esmImport(id) { + const module = getOrInstantiateModuleFromParent(id, this.m); + // any ES module has to have `module.namespaceObject` defined. + if (module.namespaceObject) return module.namespaceObject; + // only ESM can be an async module, so we don't need to worry about exports being a promise here. + const raw = module.exports; + return module.namespaceObject = interopEsm(raw, createNS(raw), raw && raw.__esModule); +} +contextPrototype.i = esmImport; +function asyncLoader(moduleId) { + const loader = getOrInstantiateModuleFromParent(moduleId, this.m).exports; + return loader(esmImport.bind(this)); +} +contextPrototype.A = asyncLoader; +// Add a simple runtime require so that environments without one can still pass +// `typeof require` CommonJS checks so that exports are correctly registered. +const runtimeRequire = // @ts-ignore +typeof require === 'function' ? require : function require1() { + throw new Error('Unexpected use of runtime require'); +}; +contextPrototype.t = runtimeRequire; +function commonJsRequire(id) { + return getOrInstantiateModuleFromParent(id, this.m).exports; +} +contextPrototype.r = commonJsRequire; +/** + * Remove fragments and query parameters since they are never part of the context map keys + * + * This matches how we parse patterns at resolving time. Arguably we should only do this for + * strings passed to `import` but the resolve does it for `import` and `require` and so we do + * here as well. + */ function parseRequest(request) { + // Per the URI spec fragments can contain `?` characters, so we should trim it off first + // https://datatracker.ietf.org/doc/html/rfc3986#section-3.5 + const hashIndex = request.indexOf('#'); + if (hashIndex !== -1) { + request = request.substring(0, hashIndex); + } + const queryIndex = request.indexOf('?'); + if (queryIndex !== -1) { + request = request.substring(0, queryIndex); + } + return request; +} +/** + * `require.context` and require/import expression runtime. + */ function moduleContextFactory(ids, mode = 0) { + const context = this; + const parentModule = this.m; + const map = {}; + for(let i = 0; i < ids.length; i += 2){ + const entry = ids[i + 1]; + let id; + let module; + if (typeof entry === 'function') { + // A function is given for a module without an id to load it by: mostly a + // request that throws an error (e.g. one that can't be resolved), and also an + // external. As there is no id, `resolve` calls the function too, so that it + // throws the same error. + id = module = entry; + } else { + id = ()=>entry; + if (mode === 0) { + // inlined require implementation, which gives an ignored module empty exports + module = ()=>entry === undefined ? {} : getOrInstantiateModuleFromParent(entry, parentModule).exports; + } else if (entry === undefined) { + module = ()=>Promise.resolve({}); + } else if (mode === 1) { + // Like `import()`, the module is evaluated asynchronously. + module = ()=>Promise.resolve().then(()=>esmImport.call(context, entry)); + } else { + module = ()=>asyncLoader.call(context, entry); + } + } + map[ids[i]] = { + id, + module + }; + } + function moduleContext(id) { + id = parseRequest(id); + if (hasOwnProperty.call(map, id)) { + return map[id].module(); + } + const e = new Error(`Cannot find module '${id}'`); + e.code = 'MODULE_NOT_FOUND'; + throw e; + } + moduleContext.keys = ()=>{ + return Object.keys(map); + }; + moduleContext.resolve = (id)=>{ + id = parseRequest(id); + if (hasOwnProperty.call(map, id)) { + return map[id].id(); + } + const e = new Error(`Cannot find module '${id}'`); + e.code = 'MODULE_NOT_FOUND'; + throw e; + }; + moduleContext.import = async (id)=>{ + return await moduleContext(id); + }; + return moduleContext; +} +contextPrototype.f = moduleContextFactory; +/** + * Returns the path of a chunk defined by its data. + */ function getChunkPath(chunkData) { + return typeof chunkData === 'string' ? chunkData : chunkData.path; +} +// Load the CompressedModuleFactories of a chunk into the `moduleFactories` Map. +// The flat format alternates one or more module IDs with their factory function. +// Strict factories can be prepended as a nested array. +function installCompressedModuleFactories(chunkModules, offset, newModuleId) { + let i = offset; + const strictFactories = chunkModules[i]; + if (Array.isArray(strictFactories)) { + installCompressedModuleFactories(strictFactories, 0, newModuleId); + i++; + } + while(i < chunkModules.length){ + let end = i + 1; + // Find our factory function + while(end < chunkModules.length && typeof chunkModules[end] !== 'function'){ + end++; + } + if (end === chunkModules.length) { + throw new Error('malformed chunk format, expected a factory function'); + } + // Install the factory for each module ID that doesn't already have one. + // When some IDs in this group already have a factory, reuse that existing + // group factory for the missing IDs to keep all IDs in the group consistent. + // Otherwise, install the factory from this chunk. + const moduleFactoryFn = chunkModules[end]; + let existingGroupFactory = undefined; + for(let j = i; j < end; j++){ + const id = chunkModules[j]; + const existingFactory = moduleFactories.get(id); + if (existingFactory) { + existingGroupFactory = existingFactory; + break; + } + } + const factoryToInstall = existingGroupFactory ?? moduleFactoryFn; + let didInstallFactory = false; + for(let j = i; j < end; j++){ + const id = chunkModules[j]; + if (!moduleFactories.has(id)) { + if (!didInstallFactory) { + if (factoryToInstall === moduleFactoryFn) { + applyModuleFactoryName(moduleFactoryFn); + } + didInstallFactory = true; + } + moduleFactories.set(id, factoryToInstall); + newModuleId?.(id); + } + } + i = end + 1; + } +} +/** + * A pseudo "fake" URL object to resolve to its relative path. + * + * When UrlRewriteBehavior is set to relative, calls to the `new URL()` will construct url without base using this + * runtime function to generate context-agnostic urls between different rendering context, i.e ssr / client to avoid + * hydration mismatch. + * + * This is based on webpack's existing implementation: + * https://github.com/webpack/webpack/blob/87660921808566ef3b8796f8df61bd79fc026108/lib/runtime/RelativeUrlRuntimeModule.js + */ const relativeURL = function relativeURL(inputUrl) { + const realUrl = new URL(inputUrl, 'x:/'); + const values = {}; + for(const key in realUrl)values[key] = realUrl[key]; + values.href = inputUrl; + values.pathname = inputUrl.replace(/[?#].*/, ''); + values.origin = values.protocol = ''; + values.toString = values.toJSON = (..._args)=>inputUrl; + for(const key in values)Object.defineProperty(this, key, { + enumerable: true, + configurable: true, + value: values[key] + }); +}; +relativeURL.prototype = URL.prototype; +contextPrototype.U = relativeURL; +/** + * Utility function to ensure all variants of an enum are handled. + */ function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} +/** + * Constructs an error message for when a module factory is not available. + */ function factoryNotAvailableMessage(moduleId, sourceType, sourceData) { + let instantiationReason; + switch(sourceType){ + case 0: + instantiationReason = `as a runtime entry of chunk ${sourceData}`; + break; + case 1: + instantiationReason = `because it was required from module ${sourceData}`; + break; + case 2: + instantiationReason = 'because of an HMR update'; + break; + default: + invariant(sourceType, (sourceType)=>`Unknown source type: ${sourceType}`); + } + return `Module ${moduleId} was instantiated ${instantiationReason}, but the module factory is not available.`; +} +/** + * Returns a `file://` URL under a synthetic directory named after `root` + * (`ROOT` for the project root), for when the real filesystem path is unknown. + * The root name and path segments are percent-encoded so the result is always + * a valid file URI. + */ function placeholderFileUrl(modulePath, root) { + return `file:///${encodeURIComponent(root ?? 'ROOT')}/${modulePath.split('/').map(encodeURIComponent).join('/')}`; +} +/** + * A stub function to make `require` available but non-functional in ESM. + */ function requireStub(_moduleId) { + throw new Error('dynamic usage of require is not supported'); +} +contextPrototype.z = requireStub; +// Make `globalThis` available to the module in a way that cannot be shadowed by a local variable. +contextPrototype.g = globalThis; +// Each runtime keeps its own public path. Other applications on the page may +// change the global path after this runtime has started. +let runtimePublicPath = typeof globalThis !== 'undefined' ? globalThis.publicPath : undefined; +Object.defineProperty(contextPrototype, 'runtimePublicPath', { + get: ()=>runtimePublicPath, + set: (value)=>{ + runtimePublicPath = value; + } +}); +let cachedAutomaticPublicPath; +function getAutomaticPublicPath() { + if (cachedAutomaticPublicPath !== undefined) { + return cachedAutomaticPublicPath; + } + let scriptUrl; + if (typeof document === 'object') { + const currentScript = document.currentScript; + scriptUrl = currentScript?.src; + if (!scriptUrl) { + const scripts = document.getElementsByTagName('script'); + const script = scripts[scripts.length - 1]; + scriptUrl = script?.src; + } + } + if (!scriptUrl && typeof globalThis.importScripts === 'function' && globalThis.location) { + scriptUrl = String(globalThis.location); + } + cachedAutomaticPublicPath = scriptUrl ? scriptUrl.replace(/^blob:/, '').replace(/#.*$/, '').replace(/\?.*$/, '').replace(/\/[^/]*$/, '/') : ''; + return cachedAutomaticPublicPath; +} +/** + * Gets the public path for runtime assets. + * Uses this runtime's public path and falls back to "/". + */ function getPublicPath(mode) { + if (mode === 'auto') { + return getAutomaticPublicPath(); + } + if (typeof runtimePublicPath === 'string') { + const publicPath = runtimePublicPath; + return publicPath.endsWith('/') ? publicPath : `${publicPath}/`; + } + return '/'; +} +contextPrototype.p = getPublicPath; +function applyModuleFactoryName(factory) { + // Give the module factory a nice name to improve stack traces. + Object.defineProperty(factory, 'name', { + value: 'module evaluation' + }); +} +/** + * This file contains runtime types and functions that are shared between all + * Turbopack UMD library runtimes (DOM and Node.js). + * + * It will be appended to the runtime code of each runtime right after the + * shared runtime utils. + */ /* eslint-disable @typescript-eslint/no-unused-vars */ /// +/// +// Provided by build +let BACKEND; +/** + * Determine the chunk to register from a registration entry. + * In library builds, chunks are always string paths or script objects. + */ function getChunkFromRegistration(chunk) { + if (typeof chunk === "string") { + return chunk; + } else if (chunk) { + return { + src: chunk.getAttribute("src") + }; + } else { + throw new Error("chunk path is empty"); + } +} +/** + * Load CommonJS externals when a UMD bundle runs in a CommonJS environment. + * Browser-targeted UMD bundles need this too because their wrapper supports + * both global and CommonJS consumers. + */ function externalRequire(id, thunk, esm = false) { + let raw; + try { + raw = thunk(); + } catch (err) { + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + return interopEsm(raw, createNS(raw), true); +} +externalRequire.resolve = (id, options)=>{ + return require.resolve(id, options); +}; +contextPrototype.x = externalRequire; +/** + * Adds Webpack-compatible ESM metadata to external values while preserving + * native ESM live bindings. + */ function externalNamespace(mod) { + if (mod && mod.__esModule) return mod; + const ns = Object.create(null); + const isEsmNamespace = mod && toStringTag && mod[toStringTag] === "Module"; + if (mod && (typeof mod === "object" || typeof mod === "function")) { + for(const key in mod){ + if (key === "__esModule" || !isEsmNamespace && key === "default") { + continue; + } + Object.defineProperty(ns, key, { + enumerable: true, + get: createGetter(mod, key) + }); + } + } + if (!isEsmNamespace) { + Object.defineProperty(ns, "default", { + enumerable: true, + value: mod + }); + } + Object.defineProperty(ns, "__esModule", { + value: true + }); + if (toStringTag) { + Object.defineProperty(ns, toStringTag, { + value: "Module" + }); + } + return ns; +} +contextPrototype.N = externalNamespace; +/// +/// +const moduleCache = new Map(); +contextPrototype.c = moduleCache; +/** + * Gets or instantiates a runtime module. + */ // @ts-ignore +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function getOrInstantiateRuntimeModule(chunkPath, moduleId) { + return getCachedModule(moduleCache, moduleId) ?? instantiateModule(moduleId, SourceType.Runtime, chunkPath); +} +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + */ // Used by the backend +// @ts-ignore +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const getOrInstantiateModuleFromParent = (id, sourceModule)=>{ + return getCachedModule(moduleCache, id) ?? instantiateModule(id, SourceType.Parent, sourceModule.id); +}; +function instantiateModule(id, sourceType, sourceData) { + const moduleFactory = moduleFactories.get(id); + if (typeof moduleFactory !== 'function') { + // This can happen if modules incorrectly handle HMR disposes/updates, + // e.g. when they keep a `setTimeout` around which still executes old code + // and contains e.g. a `require("something")` call. + throw new Error(factoryNotAvailableMessage(id, sourceType, sourceData)); + } + const module = createModuleObject(id); + const exports = module.exports; + moduleCache.set(id, module); + // NOTE(alexkirsz) This can fail when the module encounters a runtime error. + const context = new Context(module, exports); + try { + moduleFactory(context, module, exports); + } catch (error) { + module.error = error; + throw error; + } + if (module.namespaceObject && module.exports !== module.namespaceObject) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module.exports, module.namespaceObject); + } + return module; +} +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function registerChunk(registration) { + // An inlined entry-only registration is a bare params object (no source chunk). + if (!Array.isArray(registration)) { + return BACKEND.registerChunk(undefined, registration); + } + const chunk = getChunkFromRegistration(registration[0]); + if (SUPPORT_COMPONENT_CHUNKS) { + markChunkComponentsAvailable(chunk); + } + let runtimeParams; + // When bootstrapping we are passed a single runtimeParams object so we can distinguish purely based on length + if (registration.length === 2) { + runtimeParams = registration[1]; + } else { + runtimeParams = undefined; + installCompressedModuleFactories(registration, /* offset= */ 1); + } + return BACKEND.registerChunk(chunk, runtimeParams); +} +/** + * This file contains the runtime code specific to the Turbopack + * ECMAScript DOM runtime for library builds. + * + * It will be appended to the base runtime code in place of + * runtime-backend-node.ts when the target platform is browser/web. + * + * Since library builds produce a single, self-contained chunk, + * no dynamic chunk loading is needed. The BACKEND simply registers + * modules and instantiates runtime entries. + * + * It also provides browser file URL placeholders and `loadScript` for + * script externals loaded from CDN or other external sources. + */ /* eslint-disable @typescript-eslint/no-unused-vars */ /// +/** + * Match the browser runtime's placeholder URLs without exposing filesystem paths. + */ function resolveFileUrl(modulePath, root) { + // ImportMetaBinding escapes percent signs and control characters before codegen. + return placeholderFileUrl(decodeURIComponent(modulePath ?? ""), root); +} +contextPrototype.F = resolveFileUrl; +const loadedScripts = new Map(); +/** + * Load an external script by creating a