Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support module graph connection js api #8812

Merged
merged 9 commits into from
Dec 23, 2024
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
2 changes: 2 additions & 0 deletions .github/workflows/reusable-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ jobs:
if: ${{ !inputs.skipable }}
uses: ./.github/actions/rustup
with:
save-cache: ${{ github.ref_name == 'main' }} # This should be safe because we have nightly building the cache every day
shared-key: build-${{ inputs.target }}-${{ inputs.profile }}

# Compile dependencies with optimization to make tests run faster
Expand Down Expand Up @@ -426,6 +427,7 @@ jobs:
- name: Install Rust Toolchain
uses: ./.github/actions/rustup
with:
save-cache: ${{ github.ref_name == 'main' }} # This should be safe because we have nightly building the cache every day
shared-key: build-${{ inputs.target }}-${{ inputs.profile }}

- name: Pnpm Cache
Expand Down
9 changes: 9 additions & 0 deletions crates/node_binding/binding.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,9 +236,18 @@ export declare class JsModule {

export declare class JsModuleGraph {
getModule(jsDependency: JsDependency): JsModule | null
getResolvedModule(jsDependency: JsDependency): JsModule | null
getUsedExports(jsModule: JsModule, jsRuntime: string | Array<string>): boolean | Array<string> | null
getIssuer(module: JsModule): JsModule | null
getExportsInfo(module: JsModule): JsExportsInfo
getConnection(dependency: JsDependency): JsModuleGraphConnection | null
getOutgoingConnections(module: JsModule): JsModuleGraphConnection[]
getIncomingConnections(module: JsModule): JsModuleGraphConnection[]
}

export declare class JsModuleGraphConnection {
get dependency(): JsDependency
get module(): JsModule | null
}

export declare class JsResolver {
Expand Down
2 changes: 2 additions & 0 deletions crates/rspack_binding_values/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ mod html;
mod identifier;
mod module;
mod module_graph;
mod module_graph_connection;
mod normal_module_factory;
mod options;
mod path_data;
Expand Down Expand Up @@ -47,6 +48,7 @@ pub use filename::*;
pub use html::*;
pub use module::*;
pub use module_graph::*;
pub use module_graph_connection::*;
pub use normal_module_factory::*;
pub use options::*;
pub use path_data::*;
Expand Down
70 changes: 69 additions & 1 deletion crates/rspack_binding_values/src/module_graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ use napi_derive::napi;
use rspack_core::{Compilation, ModuleGraph, RuntimeSpec};
use rustc_hash::FxHashSet;

use crate::{JsDependency, JsExportsInfo, JsModule, JsModuleWrapper};
use crate::{
JsDependency, JsExportsInfo, JsModule, JsModuleGraphConnectionWrapper, JsModuleWrapper,
};

#[napi]
pub struct JsModuleGraph {
Expand Down Expand Up @@ -39,6 +41,25 @@ impl JsModuleGraph {
Ok(js_module)
}

#[napi(ts_return_type = "JsModule | null")]
pub fn get_resolved_module(
&self,
js_dependency: &JsDependency,
) -> napi::Result<Option<JsModuleWrapper>> {
let (compilation, module_graph) = self.as_ref()?;
Ok(
match module_graph.connection_by_dependency_id(&js_dependency.dependency_id) {
Some(connection) => match connection.resolved_original_module_identifier {
Some(identifier) => compilation.module_by_identifier(&identifier).map(|module| {
JsModuleWrapper::new(module.as_ref(), compilation.id(), Some(compilation))
}),
None => None,
},
None => None,
},
)
}

#[napi]
pub fn get_used_exports(
&self,
Expand Down Expand Up @@ -87,4 +108,51 @@ impl JsModuleGraph {
let exports_info = module_graph.get_exports_info(&module.identifier);
Ok(JsExportsInfo::new(exports_info, compilation))
}

#[napi(ts_return_type = "JsModuleGraphConnection | null")]
pub fn get_connection(
&self,
dependency: &JsDependency,
) -> napi::Result<Option<JsModuleGraphConnectionWrapper>> {
let (compilation, module_graph) = self.as_ref()?;
Ok(
module_graph
.connection_by_dependency_id(&dependency.dependency_id)
.map(|connection| {
JsModuleGraphConnectionWrapper::new(connection.dependency_id, compilation)
}),
)
}

#[napi(ts_return_type = "JsModuleGraphConnection[]")]
pub fn get_outgoing_connections(
&self,
module: &JsModule,
) -> napi::Result<Vec<JsModuleGraphConnectionWrapper>> {
let (compilation, module_graph) = self.as_ref()?;
Ok(
module_graph
.get_outgoing_connections(&module.identifier)
.map(|connection| {
JsModuleGraphConnectionWrapper::new(connection.dependency_id, compilation)
})
.collect::<Vec<_>>(),
)
}

#[napi(ts_return_type = "JsModuleGraphConnection[]")]
pub fn get_incoming_connections(
&self,
module: &JsModule,
) -> napi::Result<Vec<JsModuleGraphConnectionWrapper>> {
let (compilation, module_graph) = self.as_ref()?;
Ok(
module_graph
.get_incoming_connections(&module.identifier)
.map(|connection| {
JsModuleGraphConnectionWrapper::new(connection.dependency_id, compilation)
})
.collect::<Vec<_>>(),
)
}
}
125 changes: 125 additions & 0 deletions crates/rspack_binding_values/src/module_graph_connection.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
use std::{cell::RefCell, ptr::NonNull};

use napi::bindgen_prelude::ToNapiValue;
use napi_derive::napi;
use rspack_core::{Compilation, CompilationId, DependencyId, ModuleGraph};
use rspack_napi::OneShotRef;
use rustc_hash::FxHashMap as HashMap;

use crate::{JsDependencyWrapper, JsModuleWrapper};

#[napi]
pub struct JsModuleGraphConnection {
compilation: NonNull<Compilation>,
dependency_id: DependencyId,
}

impl JsModuleGraphConnection {
fn as_ref(&self) -> napi::Result<(&'static Compilation, ModuleGraph<'static>)> {
let compilation = unsafe { self.compilation.as_ref() };
let module_graph = compilation.get_module_graph();

Ok((compilation, module_graph))
}
}

#[napi]
impl JsModuleGraphConnection {
#[napi(getter, ts_return_type = "JsDependency")]
pub fn dependency(&self) -> napi::Result<JsDependencyWrapper> {
let (compilation, module_graph) = self.as_ref()?;
if let Some(dependency) = module_graph.dependency_by_id(&self.dependency_id) {
Ok(JsDependencyWrapper::new(
dependency.as_ref(),
compilation.id(),
Some(compilation),
))
} else {
Err(napi::Error::from_reason(format!(
"Unable to access Dependency with id = {:#?} now. The Dependency have been removed on the Rust side.",
self.dependency_id
)))
}
}

#[napi(getter, ts_return_type = "JsModule | null")]
pub fn module(&self) -> napi::Result<Option<JsModuleWrapper>> {
let (compilation, module_graph) = self.as_ref()?;
if let Some(connection) = module_graph.connection_by_dependency_id(&self.dependency_id) {
let module = module_graph.module_by_identifier(connection.module_identifier());
Ok(module.map(|m| JsModuleWrapper::new(m.as_ref(), compilation.id(), Some(compilation))))
} else {
Err(napi::Error::from_reason(format!(
"Unable to access ModuleGraphConnection with id = {:#?} now. The ModuleGraphConnection have been removed on the Rust side.",
self.dependency_id
)))
}
}
}

type ModuleGraphConnectionRefs = HashMap<DependencyId, OneShotRef<JsModuleGraphConnection>>;

type ModuleGraphConnectionRefsByCompilationId =
RefCell<HashMap<CompilationId, ModuleGraphConnectionRefs>>;

thread_local! {
static MODULE_GRAPH_CONNECTION_INSTANCE_REFS: ModuleGraphConnectionRefsByCompilationId = Default::default();
}

pub struct JsModuleGraphConnectionWrapper {
compilation_id: CompilationId,
compilation: NonNull<Compilation>,
dependency_id: DependencyId,
}

impl JsModuleGraphConnectionWrapper {
pub fn new(dependency_id: DependencyId, compilation: &Compilation) -> Self {
#[allow(clippy::unwrap_used)]
Self {
dependency_id,
compilation_id: compilation.id(),
compilation: NonNull::new(compilation as *const Compilation as *mut Compilation).unwrap(),
}
}

pub fn cleanup_last_compilation(compilation_id: CompilationId) {
MODULE_GRAPH_CONNECTION_INSTANCE_REFS.with(|refs| {
let mut refs_by_compilation_id = refs.borrow_mut();
refs_by_compilation_id.remove(&compilation_id)
});
}
}

impl ToNapiValue for JsModuleGraphConnectionWrapper {
unsafe fn to_napi_value(
env: napi::sys::napi_env,
val: Self,
) -> napi::Result<napi::sys::napi_value> {
MODULE_GRAPH_CONNECTION_INSTANCE_REFS.with(|refs| {
let mut refs_by_compilation_id = refs.borrow_mut();
let entry = refs_by_compilation_id.entry(val.compilation_id);
let refs = match entry {
std::collections::hash_map::Entry::Occupied(entry) => entry.into_mut(),
std::collections::hash_map::Entry::Vacant(entry) => {
let refs = HashMap::default();
entry.insert(refs)
}
};

match refs.entry(val.dependency_id) {
std::collections::hash_map::Entry::Occupied(occupied_entry) => {
let r = occupied_entry.get();
ToNapiValue::to_napi_value(env, r)
}
std::collections::hash_map::Entry::Vacant(vacant_entry) => {
let js_dependency = JsModuleGraphConnection {
compilation: val.compilation,
dependency_id: val.dependency_id,
};
let r = vacant_entry.insert(OneShotRef::new(env, js_dependency)?);
ToNapiValue::to_napi_value(env, r)
}
}
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import "./foo";
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
const { normalize, join } = require('path');

const PLUGIN_NAME = "Test";

class Plugin {
/**
* @param {import("@rspack/core").Compiler} compiler
*/
apply(compiler) {
compiler.hooks.compilation.tap(PLUGIN_NAME, compilation => {
compilation.hooks.finishModules.tap(PLUGIN_NAME, () => {
const entry = Array.from(compilation.entries.values())[0];
const entryDependency = entry.dependencies[0];
const connection = compilation.moduleGraph.getConnection(entryDependency);
const outgoingConnection = compilation.moduleGraph.getOutgoingConnections(connection.module)[0];
expect(normalize(outgoingConnection.module.request)).toBe(normalize(join(__dirname, 'foo.js')));
});
});

}
}

/** @type {import("@rspack/core").Configuration} */
module.exports = {
target: "web",
node: {
__dirname: false,
__filename: false,
},
plugins: [
new Plugin()
]
};
19 changes: 19 additions & 0 deletions packages/rspack/etc/core.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import { JsHtmlPluginTag } from '@rspack/binding';
import { JsLoaderItem } from '@rspack/binding';
import { JsModule } from '@rspack/binding';
import type { JsModuleGraph } from '@rspack/binding';
import type { JsModuleGraphConnection } from '@rspack/binding';
import { JsRuntimeModule } from '@rspack/binding';
import type { JsStats } from '@rspack/binding';
import type { JsStatsCompilation } from '@rspack/binding';
Expand Down Expand Up @@ -3758,11 +3759,29 @@ class ModuleGraph {
// (undocumented)
static __from_binding(binding: JsModuleGraph): ModuleGraph;
// (undocumented)
getConnection(dependency: Dependency): ModuleGraphConnection | null;
// (undocumented)
getExportsInfo(module: Module): ExportsInfo;
// (undocumented)
getIssuer(module: Module): Module | null;
// (undocumented)
getModule(dependency: Dependency): Module | null;
// (undocumented)
getOutgoingConnections(module: Module): ModuleGraphConnection[];
// (undocumented)
getResolvedModule(dependency: Dependency): Module | null;
}

// @public (undocumented)
class ModuleGraphConnection {
// (undocumented)
static __from_binding(binding: JsModuleGraphConnection): ModuleGraphConnection;
// (undocumented)
static __to_binding(data: ModuleGraphConnection): JsModuleGraphConnection;
// (undocumented)
readonly dependency: Dependency;
// (undocumented)
readonly module: Module | null;
}

// @public (undocumented)
Expand Down
21 changes: 21 additions & 0 deletions packages/rspack/src/ModuleGraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { JsModuleGraph } from "@rspack/binding";
import { Dependency } from "./Dependency";
import { ExportsInfo } from "./ExportsInfo";
import { Module } from "./Module";
import { ModuleGraphConnection } from "./ModuleGraphConnection";

export default class ModuleGraph {
static __from_binding(binding: JsModuleGraph) {
Expand All @@ -19,6 +20,13 @@ export default class ModuleGraph {
return binding ? Module.__from_binding(binding) : null;
}

getResolvedModule(dependency: Dependency): Module | null {
const binding = this.#inner.getResolvedModule(
Dependency.__to_binding(dependency)
);
return binding ? Module.__from_binding(binding) : null;
}

getIssuer(module: Module): Module | null {
const binding = this.#inner.getIssuer(Module.__to_binding(module));
return binding ? Module.__from_binding(binding) : null;
Expand All @@ -29,4 +37,17 @@ export default class ModuleGraph {
this.#inner.getExportsInfo(Module.__to_binding(module))
);
}

getConnection(dependency: Dependency): ModuleGraphConnection | null {
const binding = this.#inner.getConnection(
Dependency.__to_binding(dependency)
);
return binding ? ModuleGraphConnection.__from_binding(binding) : null;
}

getOutgoingConnections(module: Module): ModuleGraphConnection[] {
return this.#inner
.getOutgoingConnections(Module.__to_binding(module))
.map(binding => ModuleGraphConnection.__from_binding(binding));
}
}
Loading
Loading