Skip to content
Merged
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 Cargo.lock

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

3 changes: 2 additions & 1 deletion crates/rspack_binding_api/src/fs_node/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ type Write = ThreadsafeFunction<FnArgs<(i32, Buffer, u32)>, Promise<Either<u32,
type Read = ThreadsafeFunction<FnArgs<(i32, u32, u32)>, Promise<Either<Buffer, ()>>>;
type ReadUtil = ThreadsafeFunction<FnArgs<(i32, u8, u32)>, Promise<Either<Buffer, ()>>>;
type ReadToEnd = ThreadsafeFunction<FnArgs<(i32, u32)>, Promise<Either<Buffer, ()>>>;
type Chmod = ThreadsafeFunction<FnArgs<(String, u32)>, Promise<()>>;

#[derive(Debug)]
#[napi(object, object_to_js = false, js_name = "ThreadsafeNodeFS")]
Expand Down Expand Up @@ -53,7 +54,7 @@ pub struct ThreadsafeNodeFS {
pub read_to_end: ReadToEnd,
// The following functions are not supported by webpack, so they are optional
#[napi(ts_type = "(name: string, mode: number) => Promise<void>")]
pub chmod: Option<ThreadsafeFunction<(String, u32), Promise<()>>>,
pub chmod: Option<Chmod>,
}

#[napi(object, object_to_js = false)]
Expand Down
5 changes: 4 additions & 1 deletion crates/rspack_binding_api/src/fs_node/write.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,10 @@ impl WritableFileSystem for NodeFileSystem {
&& let Some(chmod) = &self.0.chmod
{
let file = path.as_str().to_string();
return chmod.call_with_promise((file, mode)).await.to_fs_result();
return chmod
.call_with_promise((file, mode).into())
.await
.to_fs_result();
}
Ok(())
}
Expand Down
62 changes: 61 additions & 1 deletion crates/rspack_core/src/utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ use rspack_collections::Identifier;
use rspack_util::comparators::{compare_ids, compare_numbers};

use crate::{
BoxModule, ChunkGraph, ChunkGroupByUkey, ChunkGroupUkey, ChunkUkey, Compilation, ModuleGraph,
BoxModule, ChunkGraph, ChunkGroupByUkey, ChunkGroupUkey, ChunkUkey, Compilation,
ConcatenatedModule, ModuleGraph, ModuleIdentifier,
};
mod comment;
mod compile_boolean_matcher;
Expand Down Expand Up @@ -145,6 +146,65 @@ pub fn compare_modules_by_identifier(a: &BoxModule, b: &BoxModule) -> std::cmp::
compare_ids(&a.identifier(), &b.identifier())
}

/// # Returns
/// - `Some(String)` if a hashbang is found in the module's build_info extras
/// - `None` if no hashbang is present or the module doesn't exist
pub fn get_module_hashbang(
module_graph: &ModuleGraph,
module_id: &ModuleIdentifier,
) -> Option<String> {
let module = module_graph.module_by_identifier(module_id)?;

let build_info =
if let Some(concatenated_module) = module.as_any().downcast_ref::<ConcatenatedModule>() {
// For concatenated modules, get the root module's build_info
let root_module_id = concatenated_module.get_root();
module_graph
.module_by_identifier(&root_module_id)
.map_or_else(|| module.build_info(), |m| m.build_info())
} else {
module.build_info()
};

build_info
.extras
.get("hashbang")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
}

/// # Returns
/// - `Some(Vec<String>)` if directives are found in the module's build_info extras
/// - `None` if no directives are present or the module doesn't exist
pub fn get_module_directives(
module_graph: &ModuleGraph,
module_id: &ModuleIdentifier,
) -> Option<Vec<String>> {
let module = module_graph.module_by_identifier(module_id)?;

let build_info =
if let Some(concatenated_module) = module.as_any().downcast_ref::<ConcatenatedModule>() {
// For concatenated modules, get the root module's build_info
let root_module_id = concatenated_module.get_root();
module_graph
.module_by_identifier(&root_module_id)
.map_or_else(|| module.build_info(), |m| m.build_info())
} else {
module.build_info()
};

build_info
.extras
.get("react_directives")
.and_then(|v| v.as_array())
.map(|arr| {
arr
.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect()
})
}

pub fn compare_module_iterables(modules_a: &[&BoxModule], modules_b: &[&BoxModule]) -> Ordering {
let mut a_iter = modules_a.iter();
let mut b_iter = modules_b.iter();
Expand Down
47 changes: 45 additions & 2 deletions crates/rspack_plugin_esm_library/src/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ use rspack_collections::{IdentifierIndexSet, UkeyIndexMap, UkeySet};
use rspack_core::{
AssetInfo, Chunk, ChunkGraph, ChunkRenderContext, ChunkUkey, CodeGenerationDataFilename,
Compilation, ConcatenatedModuleInfo, DependencyId, InitFragment, ModuleIdentifier, PathData,
PathInfo, RuntimeGlobals, SourceType, get_js_chunk_filename_template, get_undo_path,
render_init_fragments,
PathInfo, RuntimeGlobals, SourceType, get_js_chunk_filename_template, get_module_directives,
get_module_hashbang, get_undo_path, render_init_fragments,
rspack_sources::{ConcatSource, RawStringSource, ReplaceSource, Source, SourceExt},
};
use rspack_error::Result;
Expand Down Expand Up @@ -85,6 +85,49 @@ impl EsmLibraryPlugin {

let mut chunk_init_fragments: Vec<Box<dyn InitFragment<ChunkRenderContext> + 'static>> =
chunk_link.init_fragments.clone();

// NOTE: Similar hashbang and directives handling logic.
// See rspack_plugin_rslib/src/plugin.rs render() for why this duplication is necessary.
let entry_modules = compilation.chunk_graph.get_chunk_entry_modules(chunk_ukey);
for entry_module_id in &entry_modules {
let hashbang = get_module_hashbang(&module_graph, entry_module_id);
let directives = get_module_directives(&module_graph, entry_module_id);

if hashbang.is_none() && directives.is_none() {
continue;
}

if let Some(hashbang) = &hashbang {
chunk_init_fragments.insert(
0,
Box::new(rspack_core::NormalInitFragment::new(
format!("{hashbang}\n"),
rspack_core::InitFragmentStage::StageConstants,
i32::MIN,
rspack_core::InitFragmentKey::unique(),
None,
)),
);
}

if let Some(directives) = directives {
for (idx, directive) in directives.iter().enumerate() {
let insert_pos = if hashbang.is_some() { 1 + idx } else { idx };
chunk_init_fragments.insert(
insert_pos,
Box::new(rspack_core::NormalInitFragment::new(
format!("{directive}\n"),
rspack_core::InitFragmentStage::StageConstants,
i32::MIN + 1 + idx as i32,
rspack_core::InitFragmentKey::unique(),
None,
)),
);
}
}
break; // Only process the first entry module with hashbang/directives
}

let mut replace_auto_public_path = false;
let mut replace_static_url = false;

Expand Down
1 change: 1 addition & 0 deletions crates/rspack_plugin_rslib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ version.workspace = true
rspack_cacheable = { workspace = true }
rspack_core = { workspace = true }
rspack_error = { workspace = true }
rspack_fs = { workspace = true }
rspack_hash = { workspace = true }
rspack_hook = { workspace = true }
rspack_plugin_asset = { workspace = true }
Expand Down
44 changes: 44 additions & 0 deletions crates/rspack_plugin_rslib/src/hashbang_parser_plugin.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
use rspack_core::ConstDependency;
use rspack_plugin_javascript::{JavascriptParserPlugin, visitors::JavascriptParser};
use swc_core::ecma::ast::Program;

pub struct HashbangParserPlugin;

impl JavascriptParserPlugin for HashbangParserPlugin {
fn program(&self, parser: &mut JavascriptParser, ast: &Program) -> Option<bool> {
let hashbang = ast
.as_module()
.and_then(|m| m.shebang.as_ref())
.or_else(|| ast.as_script().and_then(|s| s.shebang.as_ref()))?;

// Normalize hashbang to always include "#!" prefix
// SWC may omit the leading "#!" in the shebang value
let normalized_hashbang = if hashbang.starts_with("#!") {
hashbang.to_string()
} else {
format!("#!{}", hashbang)
};

// Store hashbang in build_info for later use during rendering
parser.build_info.extras.insert(
"hashbang".to_string(),
serde_json::Value::String(normalized_hashbang),
);

// Remove hashbang from source code
// If SWC omitted "#!", we still need to remove those two characters
let replace_len = if hashbang.starts_with("#!") {
hashbang.len() as u32
} else {
hashbang.len() as u32 + 2 // include "#!"
};

parser.add_presentational_dependency(Box::new(ConstDependency::new(
(0, replace_len).into(),
"".into(),
None,
)));

None
}
}
2 changes: 2 additions & 0 deletions crates/rspack_plugin_rslib/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
mod asset;
mod hashbang_parser_plugin;
mod import_dependency;
mod import_external;
mod parser_plugin;
mod plugin;
mod react_directives_parser_plugin;
pub use plugin::*;
105 changes: 100 additions & 5 deletions crates/rspack_plugin_rslib/src/plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,24 @@ use std::{
};

use rspack_core::{
Compilation, CompilationParams, CompilerCompilation, CompilerFinishMake, ModuleType,
NormalModuleFactoryParser, ParserAndGenerator, ParserOptions, Plugin,
AssetEmittedInfo, ChunkUkey, Compilation, CompilationParams, CompilerAssetEmitted,
CompilerCompilation, CompilerFinishMake, ModuleType, NormalModuleFactoryParser,
ParserAndGenerator, ParserOptions, Plugin, get_module_directives, get_module_hashbang,
rspack_sources::{ConcatSource, RawStringSource, Source, SourceExt},
};
use rspack_error::Result;
use rspack_hook::{plugin, plugin_hook};
use rspack_plugin_asset::AssetParserAndGenerator;
use rspack_plugin_javascript::{
BoxJavascriptParserPlugin, parser_and_generator::JavaScriptParserAndGenerator,
BoxJavascriptParserPlugin, JavascriptModulesRender, JsPlugin, RenderSource,
parser_and_generator::JavaScriptParserAndGenerator,
};

use crate::{
asset::RslibAssetParserAndGenerator, import_dependency::RslibDependencyTemplate,
asset::RslibAssetParserAndGenerator, hashbang_parser_plugin::HashbangParserPlugin,
import_dependency::RslibDependencyTemplate,
import_external::replace_import_dependencies_for_external_modules,
parser_plugin::RslibParserPlugin,
parser_plugin::RslibParserPlugin, react_directives_parser_plugin::ReactDirectivesParserPlugin,
};

#[derive(Debug)]
Expand Down Expand Up @@ -54,6 +58,8 @@ async fn nmf_parser(
) -> Result<()> {
if let Some(parser) = parser.downcast_mut::<JavaScriptParserAndGenerator>() {
if module_type.is_js_like() {
parser.add_parser_plugin(Box::new(HashbangParserPlugin) as BoxJavascriptParserPlugin);
parser.add_parser_plugin(Box::new(ReactDirectivesParserPlugin) as BoxJavascriptParserPlugin);
parser.add_parser_plugin(
Box::new(RslibParserPlugin::new(self.options.intercept_api_plugin))
as BoxJavascriptParserPlugin,
Expand Down Expand Up @@ -88,6 +94,71 @@ async fn compilation(
RslibDependencyTemplate::template_type(),
Arc::new(RslibDependencyTemplate::default()),
);

// Register render hook for hashbang and directives handling during chunk generation
let hooks = JsPlugin::get_compilation_hooks_mut(compilation.id());
let mut hooks = hooks.write().await;
hooks.render.tap(render::new(self));
drop(hooks);

Ok(())
}

#[plugin_hook(JavascriptModulesRender for RslibPlugin)]
async fn render(
&self,
compilation: &Compilation,
chunk_ukey: &ChunkUkey,
render_source: &mut RenderSource,
) -> Result<()> {
// NOTE: This function handles hashbang and directives for non new ESM library formats.
// Similar logic exists in rspack_plugin_esm_library/src/render.rs for ESM format,
// as that plugin's render path is used instead when ESM library plugin is enabled.
let entry_modules = compilation.chunk_graph.get_chunk_entry_modules(chunk_ukey);
if entry_modules.is_empty() {
return Ok(());
}

let module_graph = compilation.get_module_graph();

for entry_module_id in &entry_modules {
let hashbang = get_module_hashbang(&module_graph, entry_module_id);
let directives = get_module_directives(&module_graph, entry_module_id);

if hashbang.is_none() && directives.is_none() {
continue;
}

let original_source_str = render_source.source.source().into_string_lossy();

let mut new_source = ConcatSource::default();

if let Some(hashbang) = hashbang {
new_source.add(RawStringSource::from(format!("{}\n", hashbang)));
}

if let Some(directives) = directives {
let use_strict_prefix = "\"use strict\";\n";
if let Some(rest) = original_source_str.strip_prefix(use_strict_prefix) {
new_source.add(RawStringSource::from(use_strict_prefix));
for directive in directives {
new_source.add(RawStringSource::from(format!("{}\n", directive)));
}
new_source.add(RawStringSource::from(rest));
} else {
for directive in directives {
new_source.add(RawStringSource::from(format!("{}\n", directive)));
}
new_source.add(render_source.source.clone());
}
} else {
new_source.add(render_source.source.clone());
}

render_source.source = new_source.boxed();
break;
}

Ok(())
}

Expand All @@ -98,6 +169,26 @@ async fn finish_make(&self, compilation: &mut Compilation) -> Result<()> {
Ok(())
}

#[plugin_hook(CompilerAssetEmitted for RslibPlugin)]
async fn asset_emitted(
&self,
compilation: &Compilation,
_filename: &str,
info: &AssetEmittedInfo,
) -> Result<()> {
use rspack_fs::FilePermissions;

let content = info.source.source().into_string_lossy();
if content.starts_with("#!") {
let output_fs = &compilation.output_filesystem;
let permissions = FilePermissions::from_mode(0o755);
output_fs
.set_permissions(&info.target_path, permissions)
.await?;
}
Ok(())
}

impl Plugin for RslibPlugin {
fn name(&self) -> &'static str {
"rslib"
Expand All @@ -111,6 +202,10 @@ impl Plugin for RslibPlugin {
.tap(nmf_parser::new(self));

ctx.compiler_hooks.finish_make.tap(finish_make::new(self));
ctx
.compiler_hooks
.asset_emitted
.tap(asset_emitted::new(self));

Ok(())
}
Expand Down
Loading
Loading