Skip to content

Commit

Permalink
feat: support loader options (#8)
Browse files Browse the repository at this point in the history
* feat: support swc options

* feat: support transform rule of remove export and keep export

* fix: optimize code

* chore: remove comments
  • Loading branch information
ClarkXia authored Nov 23, 2023
1 parent 6fe41be commit 8a3631b
Show file tree
Hide file tree
Showing 8 changed files with 210 additions and 52 deletions.
6 changes: 5 additions & 1 deletion crates/binding_options/src/options/raw_module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ pub fn get_builtin_loader(builtin: &str, options: Option<&str>) -> BoxLoader {

if builtin.starts_with(COMPILATION_LOADER_IDENTIFIER) {
return Arc::new(
loader_compilation::CompilationLoader::new().with_identifier(builtin.into()),
loader_compilation::CompilationLoader::new(
serde_json::from_str(options.unwrap_or("{}")).unwrap_or_else(|e| {
panic!("Could not parse builtin:compilation-loader options:{options:?},error: {e:?}")
}),
).with_identifier(builtin.into()),
);
}

Expand Down
4 changes: 3 additions & 1 deletion crates/loader_compilation/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ anyhow = { workspace = true }
async-trait = { workspace = true }
dashmap = { workspace = true }
either = "1"
fxhash= "0.2.1"
fxhash = "0.2.1"
lazy_static = "1.4.0"
once_cell = { workspace = true }
rspack_ast = { path = "../.rspack_crates/rspack_ast" }
rspack_core = { path = "../.rspack_crates/rspack_core" }
rspack_error = { path = "../.rspack_crates/rspack_error" }
rspack_loader_runner = { path = "../.rspack_crates/rspack_loader_runner" }
rspack_plugin_javascript = { path = "../.rspack_crates/rspack_plugin_javascript" }
regex = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = "1.0.100"
swc_config = { workspace = true }
Expand Down
12 changes: 0 additions & 12 deletions crates/loader_compilation/src/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,18 +203,6 @@ impl SwcCompiler {

program
}

pub fn comments(&self) -> &SingleThreadedComments {
&self.comments
}

pub fn options(&self) -> &Options {
&self.options
}

pub fn cm(&self) -> &Arc<SourceMap> {
&self.cm
}
}

pub(crate) trait IntoJsAst {
Expand Down
116 changes: 93 additions & 23 deletions crates/loader_compilation/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
use std::{path::Path, collections::HashMap, sync::Mutex};
use lazy_static::lazy_static;
use rspack_ast::RspackAst;
use rspack_core::{rspack_sources::SourceMap, LoaderRunnerContext};
use rspack_core::{rspack_sources::SourceMap, LoaderRunnerContext, Mode};
use rspack_loader_runner::{Identifiable, Identifier, Loader, LoaderContext};
use rspack_error::{
internal_error, Result,
internal_error, Result, Diagnostic,
};
use swc_core::base::config::{InputSourceMap, Options, OutputCharset};
use swc_core::{base::config::{InputSourceMap, Options, OutputCharset, Config, TransformConfig}, ecma::transforms::react::Runtime};
use rspack_plugin_javascript::{
ast::{self, SourceMapConfig},
TransformOutput,
Expand All @@ -14,14 +16,41 @@ mod transform;

use transform::*;
use compiler::{SwcCompiler, IntoJsAst};

pub struct LoaderOptions {
pub swc_options: Config,
pub transform_features: TransformFeatureOptions,
}

pub struct CompilationOptions {
swc_options: Options,
transform_features: TransformFeatureOptions,
}
pub struct CompilationLoader {
identifier: Identifier,
loader_options: CompilationOptions,
}

pub const COMPILATION_LOADER_IDENTIFIER: &str = "builtin:compilation-loader";

impl From<LoaderOptions> for CompilationOptions {
fn from(value: LoaderOptions) -> Self {
let transform_features = TransformFeatureOptions::default();
CompilationOptions {
swc_options: Options {
config: value.swc_options,
..Default::default()
},
transform_features,
}
}
}

impl CompilationLoader {
pub fn new() -> Self {
pub fn new(options: LoaderOptions) -> Self {
Self {
identifier: COMPILATION_LOADER_IDENTIFIER.into(),
loader_options: options.into(),
}
}

Expand All @@ -32,38 +61,81 @@ impl CompilationLoader {
}
}

lazy_static! {
static ref GLOBAL_FILE_ACCESS: Mutex<HashMap<String, bool>> = Mutex::new(HashMap::new());
static ref GLOBAL_ROUTES_CONFIG: Mutex<Option<Vec<String>>> = Mutex::new(None);
}

#[async_trait::async_trait]
impl Loader<LoaderRunnerContext> for CompilationLoader {
async fn run(&self, loader_context: &mut LoaderContext<'_, LoaderRunnerContext>) -> Result<()> {
let resource_path = loader_context.resource_path.to_path_buf();
let Some(content) = std::mem::take(&mut loader_context.content) else {
return Err(internal_error!("No content found"));
};
// TODO: init loader with custom options.
let mut swc_options = Options::default();

// TODO: merge config with built-in config.
let swc_options = {
let mut swc_options = self.loader_options.swc_options.clone();
if swc_options.config.jsc.transform.as_ref().is_some() {
let mut transform = TransformConfig::default();
let default_development = matches!(loader_context.context.options.mode, Mode::Development);
transform.react.development = Some(default_development);
transform.react.runtime = Some(Runtime::Automatic);
}

if let Some(pre_source_map) = std::mem::take(&mut loader_context.source_map) {
if let Ok(source_map) = pre_source_map.to_json() {
swc_options.config.input_source_map = Some(InputSourceMap:: Str(source_map))
if let Some(pre_source_map) = std::mem::take(&mut loader_context.source_map) {
if let Ok(source_map) = pre_source_map.to_json() {
swc_options.config.input_source_map = Some(InputSourceMap:: Str(source_map))
}
}

if swc_options.config.jsc.experimental.plugins.is_some() {
loader_context.emit_diagnostic(Diagnostic::warn(
COMPILATION_LOADER_IDENTIFIER.to_string(),
"Experimental plugins are not currently supported.".to_string(),
0,
0,
));
}
}

if swc_options.config.jsc.target.is_some() && swc_options.config.env.is_some() {
loader_context.emit_diagnostic(Diagnostic::warn(
COMPILATION_LOADER_IDENTIFIER.to_string(),
"`env` and `jsc.target` cannot be used together".to_string(),
0,
0,
));
}
swc_options
};
let devtool = &loader_context.context.options.devtool;
let source = content.try_into_string()?;
let compiler = SwcCompiler::new(resource_path.clone(), source.clone(), swc_options)?;

let transform_options = SwcPluginOptions {
keep_export: Some(KeepExportOptions {
export_names: vec!["default".to_string()],
}),
remove_export: Some(RemoveExportOptions {
remove_names: vec!["default".to_string()],
}),
};
let transform_options = &self.loader_options.transform_features;
let compiler_context:&str = loader_context.context.options.context.as_ref();
let mut file_access = GLOBAL_FILE_ACCESS.lock().unwrap();
let mut routes_config = GLOBAL_ROUTES_CONFIG.lock().unwrap();
let file_accessed = file_access.contains_key(&resource_path.to_string_lossy().to_string());

if routes_config.is_none() || file_accessed {
// Load routes config for transform.
let routes_config_path: std::path::PathBuf = Path::new(compiler_context).join(".ice/route-manifest.json");
*routes_config = Some(load_routes_config(&routes_config_path).unwrap());

if file_accessed {
// If file accessed, then we need to clear the map for the current compilation.
file_access.clear();
}
}
file_access.insert(resource_path.to_string_lossy().to_string(), true);

let built = compiler.parse(None, |_| {
transform(transform_options)
transform(
&resource_path,
routes_config.as_ref().unwrap(),
transform_options
)
})?;

let codegen_options = ast::CodegenOptions {
Expand All @@ -87,7 +159,7 @@ impl Loader<LoaderRunnerContext> for CompilationLoader {

// If swc-loader is the latest loader available,
// then loader produces AST, which could be used as an optimization.
if loader_context.loader_index() == 0
if loader_context.loader_index() == 1
&& (loader_context
.current_loader()
.composed_index_by_identifier(&self.identifier)
Expand All @@ -109,8 +181,6 @@ impl Loader<LoaderRunnerContext> for CompilationLoader {
}
}

pub const COMPILATION_LOADER_IDENTIFIER: &str = "builtin:compilation-loader";

impl Identifiable for CompilationLoader {
fn identifier(&self) -> Identifier {
self.identifier
Expand Down
Empty file.
91 changes: 80 additions & 11 deletions crates/loader_compilation/src/transform/mod.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
use std::path::Path;
use anyhow::{Error, Context};
use either::Either;
use serde::Deserialize;
use swc_core::common::chain;
use swc_core::ecma::{
transforms::base::pass::noop,
visit::{as_folder, Fold, VisitMut, Visit},
ast::Module,
transforms::base::pass::noop, visit::Fold,
};

mod keep_export;
Expand All @@ -14,41 +15,109 @@ use remove_export::remove_export;

macro_rules! either {
($config:expr, $f:expr) => {
if let Some(config) = $config {
if let Some(config) = &$config {
Either::Left($f(config))
} else {
Either::Right(noop())
}
};
($config:expr, $f:expr, $enabled:expr) => {
if $enabled {
if $enabled() {
either!($config, $f)
} else {
Either::Right(noop())
}
};
}

// Only define the stuct which is used in the following function.
#[derive(Deserialize, Debug)]
struct NestedRoutesManifest {
file: String,
children: Option<Vec<NestedRoutesManifest>>,
}

fn get_routes_file(routes: Vec<NestedRoutesManifest>) -> Vec<String> {
let mut result: Vec<String> = vec![];
for route in routes {
// Add default prefix of src/pages/ to the route file.
let mut path_str = String::from("src/pages/");
path_str.push_str(&route.file);

result.push(path_str.to_string());

if let Some(children) = route.children {
result.append(&mut get_routes_file(children));
}
}
result
}

fn parse_routes_config(c: String) -> Result<Vec<String>, Error> {
let routes = serde_json::from_str(&c)?;
Ok(get_routes_file(routes))
}

pub(crate) fn load_routes_config(path: &Path) -> Result<Vec<String>, Error> {
let content = std::fs::read_to_string(path).context("failed to read routes config")?;
parse_routes_config(content)
}

fn match_route_entry(resource_path: &Path, routes: &Vec<String>) -> bool {
let resource_path_str = resource_path.to_str().unwrap();
for route in routes {
if resource_path_str.ends_with(&route.to_string()) {
return true;
}
}
false
}

fn match_app_entry(resource_path: &Path) -> bool {
let resource_path_str = resource_path.to_str().unwrap();
// File path ends with src/app.(ts|tsx|js|jsx)
let regex_for_app = regex::Regex::new(r"src/app\.(ts|tsx|js|jsx)$").unwrap();
regex_for_app.is_match(resource_path_str)
}

#[derive(Default, Debug, Clone)]
pub struct KeepExportOptions {
pub export_names: Vec<String>,
}

#[derive(Default, Debug, Clone)]
pub struct RemoveExportOptions {
pub remove_names: Vec<String>,
}

pub struct SwcPluginOptions {
#[derive(Default, Debug)]
pub struct TransformFeatureOptions {
pub keep_export: Option<KeepExportOptions>,
pub remove_export: Option<RemoveExportOptions>,
}

pub(crate) fn transform<'a>(plugin_options: SwcPluginOptions) -> impl Fold + 'a {
pub(crate) fn transform<'a>(
resource_path: &'a Path,
routes_config: &Vec<String>,
feature_options: &TransformFeatureOptions,
) -> impl Fold + 'a {
chain!(
either!(plugin_options.keep_export, |options: KeepExportOptions| {
keep_export(options.export_names)
either!(feature_options.keep_export, |options: &KeepExportOptions| {
let mut exports_name = options.export_names.clone();
// Special case for app entry.
// When keep pageConfig, we should also keep the default export of app entry.
if match_app_entry(resource_path) && exports_name.contains(&String::from("pageConfig")) {
exports_name.push(String::from("default"));
}
keep_export(exports_name)
}, || {
match_app_entry(resource_path) || match_route_entry(resource_path, routes_config)
}),
either!(plugin_options.remove_export, |options: RemoveExportOptions| {
remove_export(options.remove_names)
either!(feature_options.remove_export, |options: &RemoveExportOptions| {
remove_export(options.remove_names.clone())
}, || {
// Remove export only work for app entry and route entry.
match_app_entry(resource_path) || match_route_entry(resource_path, routes_config)
}),
)
}
11 changes: 7 additions & 4 deletions crates/loader_compilation/tests/fixtures.rs
Original file line number Diff line number Diff line change
@@ -1,24 +1,27 @@
use std::{str::FromStr,env, fs,path::{Path, PathBuf}, sync::Arc};
use loader_compilation::CompilationLoader;
use loader_compilation::{CompilationLoader, LoaderOptions};
use rspack_core::{
run_loaders, CompilerContext, CompilerOptions, Loader, LoaderRunnerContext, ResourceData, SideEffectOption,
};
use serde_json::json;
use swc_core::base::config::{PluginConfig, Config};

async fn loader_test(actual: impl AsRef<Path>, expected: impl AsRef<Path>) {
let tests_path = PathBuf::from(concat!(env!("CARGO_MANIFEST_DIR"))).join("tests");
let expected_path = tests_path.join(expected);
let actual_path = tests_path.join(actual);
let parent_path = actual_path.parent().unwrap().to_path_buf();

let mut options = Config::default();
let (result, _) = run_loaders(
&[Arc::new(CompilationLoader::new()) as Arc<dyn Loader<LoaderRunnerContext>>],
&[Arc::new(CompilationLoader::new(LoaderOptions {
swc_options: options,
transform_features: Default::default(),
})) as Arc<dyn Loader<LoaderRunnerContext>>],
&ResourceData::new(actual_path.to_string_lossy().to_string(), actual_path),
&[],
CompilerContext {
options: std::sync::Arc::new(CompilerOptions {
context: rspack_core::Context::default(),
context: rspack_core::Context::new(parent_path.to_string_lossy().to_string()),
dev_server: rspack_core::DevServerOptions::default(),
devtool: rspack_core::Devtool::from("source-map".to_string()),
mode: rspack_core::Mode::None,
Expand Down
Loading

0 comments on commit 8a3631b

Please sign in to comment.