Skip to content

Commit

Permalink
feat(rust/rolldown): support inject imports (rolldown#1933)
Browse files Browse the repository at this point in the history
This PR adds the missing pieces for inject plugin.

---------

Co-authored-by: Yunfei <i.heyunfei@gmail.com>
  • Loading branch information
Boshen and hyf0 authored Aug 10, 2024
1 parent 370c22b commit a2bb2ca
Show file tree
Hide file tree
Showing 18 changed files with 287 additions and 14 deletions.
32 changes: 31 additions & 1 deletion crates/rolldown/src/utils/normalize_options.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
use rolldown_common::{ModuleType, NormalizedBundlerOptions, Platform, SourceMapType};
use oxc::minifier::InjectGlobalVariablesConfig;
use rolldown_common::{
InjectImport, ModuleType, NormalizedBundlerOptions, Platform, SourceMapType,
};
use rustc_hash::FxHashMap;

pub struct NormalizeOptionsReturn {
pub options: NormalizedBundlerOptions,
pub resolve_options: rolldown_resolver::ResolveOptions,
}

#[allow(clippy::too_many_lines)] // This function is long, but it's mostly just mapping values
pub fn normalize_options(mut raw_options: crate::BundlerOptions) -> NormalizeOptionsReturn {
// Take out resolve options
let platform = raw_options.platform.unwrap_or(Platform::Browser);
Expand Down Expand Up @@ -64,6 +68,30 @@ pub fn normalize_options(mut raw_options: crate::BundlerOptions) -> NormalizeOpt
raw.into_iter().collect()
};

let oxc_inject_global_variables_config = InjectGlobalVariablesConfig::new(
raw_options
.inject
.as_ref()
.map(|raw_injects| {
raw_injects
.iter()
.map(|raw| match raw {
InjectImport::Named { imported, alias, from } => {
oxc::minifier::InjectImport::named_specifier(
from,
Some(imported),
alias.as_deref().unwrap_or(imported),
)
}
InjectImport::Namespace { alias, from } => {
oxc::minifier::InjectImport::namespace_specifier(from, alias)
}
})
.collect()
})
.unwrap_or_default(),
);

let normalized = NormalizedBundlerOptions {
input: raw_options.input.unwrap_or_default(),
cwd: raw_options
Expand Down Expand Up @@ -107,6 +135,8 @@ pub fn normalize_options(mut raw_options: crate::BundlerOptions) -> NormalizeOpt
experimental: raw_options.experimental.unwrap_or_default(),
minify: raw_options.minify.unwrap_or(false),
define,
inject: raw_options.inject.unwrap_or_default(),
oxc_inject_global_variables_config,
};

NormalizeOptionsReturn { options: normalized, resolve_options: raw_resolve }
Expand Down
11 changes: 9 additions & 2 deletions crates/rolldown/src/utils/parse_to_ecma_ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,13 @@ pub fn parse_to_ecma_ast(
ecma_ast =
plugin_driver.transform_ast(HookTransformAstArgs { cwd: &options.cwd, ast: ecma_ast })?;

pre_process_ecma_ast(ecma_ast, &parsed_type, path, oxc_source_type, replace_global_define_config)
.map(Ok)
pre_process_ecma_ast(
ecma_ast,
&parsed_type,
path,
oxc_source_type,
replace_global_define_config,
options,
)
.map(Ok)
}
46 changes: 38 additions & 8 deletions crates/rolldown/src/utils/pre_process_ecma_ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ use std::path::Path;

use oxc::ast::VisitMut;
use oxc::minifier::{
CompressOptions, Compressor, ReplaceGlobalDefines, ReplaceGlobalDefinesConfig,
CompressOptions, Compressor, InjectGlobalVariables, ReplaceGlobalDefines,
ReplaceGlobalDefinesConfig,
};
use oxc::semantic::{ScopeTree, SymbolTable};
use oxc::semantic::{ScopeTree, SemanticBuilder, SymbolTable};
use oxc::span::SourceType;
use oxc::transformer::{TransformOptions, Transformer};
use rolldown_ecmascript::EcmaAst;

use rolldown_common::NormalizedBundlerOptions;
use rolldown_ecmascript::{EcmaAst, WithMutFields};

use crate::types::oxc_parse_type::OxcParseType;

Expand All @@ -22,7 +25,21 @@ pub fn pre_process_ecma_ast(
path: &Path,
source_type: SourceType,
replace_global_define_config: Option<&ReplaceGlobalDefinesConfig>,
bundle_options: &NormalizedBundlerOptions,
) -> anyhow::Result<(EcmaAst, SymbolTable, ScopeTree)> {
// Build initial semantic data and check for semantic errors.
let semantic_ret = ast.program.with_mut(|WithMutFields { program, source, .. }| {
SemanticBuilder::new(source, source_type).build(program)
});

// TODO:
// if !semantic_ret.errors.is_empty() {
// return Err(anyhow::anyhow!("Semantic Error: {:#?}", semantic_ret.errors));
// }

let (mut symbols, mut scopes) = semantic_ret.semantic.into_symbol_table_and_scope_tree();

// Transform TypeScript and jsx.
if !matches!(parse_type, OxcParseType::Js) {
let trivias = ast.trivias.clone();
let ret = ast.program.with_mut(move |fields| {
Expand All @@ -43,21 +60,34 @@ pub fn pre_process_ecma_ast(
trivias,
transformer_options,
)
.build(fields.program)
.build_with_symbols_and_scopes(symbols, scopes, fields.program)
});

if !ret.errors.is_empty() {
return Err(anyhow::anyhow!("Transform failed, got {:#?}", ret.errors));
}

symbols = ret.symbols;
scopes = ret.scopes;
}

ast.program.with_mut(|fields| -> anyhow::Result<()> {
ast.program.with_mut(|WithMutFields { allocator, program, .. }| -> anyhow::Result<()> {
// Use built-in define plugin.
if let Some(replace_global_define_config) = replace_global_define_config {
ReplaceGlobalDefines::new(fields.allocator, replace_global_define_config.clone())
.build(fields.program);
ReplaceGlobalDefines::new(allocator, replace_global_define_config.clone()).build(program);
}

if !bundle_options.inject.is_empty() {
InjectGlobalVariables::new(
allocator,
bundle_options.oxc_inject_global_variables_config.clone(),
)
.build(&mut symbols, &mut scopes, program);
}

// Perform dead code elimination.
let options = CompressOptions::dead_code_elimination();
Compressor::new(fields.allocator, options).build(fields.program);
Compressor::new(allocator, options).build(program);

Ok(())
})?;
Expand Down
38 changes: 38 additions & 0 deletions crates/rolldown/tests/rolldown/function/inject/_config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"config": {
"inject": [
// import { Promise } from 'es6-promise'
{
"type": "named",
"imported": "Promise",
"from": "./promise-shim"
},
// import { Promise as P } from 'es6-promise'
{
"type": "named",
"imported": "Promise",
"alias": "P",
"from": "./promise-shim"
},
// import $ from 'jquery'
{
"type": "named",
"imported": "default",
"alias": "$",
"from": "./jquery"
},
// import * as fs from 'node:fs'
{
"type": "namespace",
"alias": "fs",
"from": "./node-fs"
},
// `Object.assign`
{
"type": "named",
"imported": "Object.assign",
"from": "./object-assign-shim"
}
]
}
}
30 changes: 30 additions & 0 deletions crates/rolldown/tests/rolldown/function/inject/artifacts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
source: crates/rolldown_testing/src/integration_test.rs
---
# Assets

## main.mjs

```js
import { default as assert } from "node:assert";
//#region promise-shim.js
const Promise = "promise-shim";
//#endregion
//#region jquery.js
var jquery_default = "jquery";
//#endregion
//#region node-fs.js
var node_fs_default = "node-fs";
//#endregion
//#region main.js
assert.strictEqual(Promise, "promise-shim");
assert.strictEqual(Promise, "promise-shim");
assert.strictEqual(jquery_default, "jquery");
assert.strictEqual(node_fs_default, "node-fs");
//#endregion
```
1 change: 1 addition & 0 deletions crates/rolldown/tests/rolldown/function/inject/jquery.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default 'jquery'
8 changes: 8 additions & 0 deletions crates/rolldown/tests/rolldown/function/inject/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import assert from 'node:assert'

assert.strictEqual(Promise, 'promise-shim')
assert.strictEqual(P, 'promise-shim')
assert.strictEqual($, 'jquery')
assert.strictEqual(fs.default, 'node-fs')
// FIXME: oxc injects invalid statements `import { default as 'Object.assign' from 'object-assign-shim'` }`, so it fails.
// assert.strictEqual(Object.assign, 'object-assign-shim')
1 change: 1 addition & 0 deletions crates/rolldown/tests/rolldown/function/inject/node-fs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default 'node-fs'
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default 'object-assign-shim'
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const Promise = 'promise-shim'
Original file line number Diff line number Diff line change
Expand Up @@ -1619,6 +1619,10 @@ expression: "snapshot_outputs.join(\"\\n\")"

- main-!~{000}~.mjs => main--FmiP1j0.mjs

# tests/rolldown/function/inject

- main-!~{000}~.mjs => main-7LIxqMuJ.mjs

# tests/rolldown/function/intro/cjs

- main-!~{000}~.cjs => main-qFgh1E_v.cjs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ pub fn normalize_binding_options(
css_entry_filenames: None,
css_chunk_filenames: None,
define: input_options.define.map(FxIndexMap::from_iter),
inject: None,
};

#[cfg(not(target_family = "wasm"))]
Expand Down
6 changes: 4 additions & 2 deletions crates/rolldown_common/src/inner_bundler_options/mod.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
use rolldown_utils::indexmap::FxIndexMap;
#[cfg(feature = "deserialize_bundler_options")]
use serde_json::Value;
use std::{collections::HashMap, fmt::Debug, path::PathBuf};
use types::inject_import::InjectImport;

#[cfg(feature = "deserialize_bundler_options")]
use schemars::JsonSchema;
#[cfg(feature = "deserialize_bundler_options")]
use serde::{Deserialize, Deserializer};
#[cfg(feature = "deserialize_bundler_options")]
use serde_json::Value;
use types::experimental_options::ExperimentalOptions;

use self::types::treeshake::TreeshakeOptions;
Expand Down Expand Up @@ -104,6 +105,7 @@ pub struct BundlerOptions {
schemars(with = "Option<HashMap<String, String>>")
)]
pub define: Option<FxIndexMap<String, String>>,
pub inject: Option<Vec<InjectImport>>,
}

#[cfg(feature = "deserialize_bundler_options")]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#[cfg(feature = "deserialize_bundler_options")]
use schemars::JsonSchema;
#[cfg(feature = "deserialize_bundler_options")]
use serde::Deserialize;

/// # Usage
/// - `import { Promise } from 'es6-promise'` => `InjectImport::named("Promise", None,"es6-promise")`
/// - `import { Promise as P } from 'es6-promise'` => `InjectImport::named("Promise", Some("P"), "es6-promise")`
/// - `import $ from 'jquery'` => `InjectImport::named("default", Some("$"), "jquery")`
/// - `import $ from 'jquery'` => `InjectImport::default("$", "jquery")`
/// - `import * as fs from 'node:fs'` => `InjectImport::namespace("fs", "node:fs")`
///
/// - `InjectImport::named("Object.assign", None, "es6-object-assign")` is a special form to inject shims to the following code:
/// ```js
/// console.log(Object.assign({ a: 1 }, { b: 2 }));
/// ```
///
/// will be, after the injection, transformed to:
///
/// ```js
/// import object_assign from "es6-object-assign";
/// console.log(object_assign({ a: 1 }, { b: 2 }));
///```
#[derive(Debug)]
#[cfg_attr(
feature = "deserialize_bundler_options",
derive(Deserialize, JsonSchema),
serde(rename_all = "camelCase", deny_unknown_fields, tag = "type")
)]
pub enum InjectImport {
Named { imported: String, alias: Option<String>, from: String },
Namespace { alias: String, from: String },
}

impl InjectImport {
pub fn named(imported: String, alias: Option<String>, from: String) -> Self {
Self::Named { imported, from, alias }
}

pub fn namespace(alias: String, from: String) -> Self {
Self::Namespace { from, alias }
}

pub fn default(alias: String, from: String) -> Self {
Self::Named { imported: "default".to_string(), alias: Some(alias), from }
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
pub mod es_module_flag;
pub mod experimental_options;
pub mod filename_template;
pub mod inject_import;
pub mod input_item;
pub mod is_external;
pub mod module_type;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
use std::path::PathBuf;

use oxc::minifier::InjectGlobalVariablesConfig;
use rustc_hash::FxHashMap;

use super::experimental_options::ExperimentalOptions;
Expand All @@ -13,7 +14,7 @@ use super::{
source_map_type::SourceMapType, sourcemap_ignore_list::SourceMapIgnoreList,
sourcemap_path_transform::SourceMapPathTransform,
};
use crate::{EsModuleFlag, InputItem, ModuleType};
use crate::{EsModuleFlag, InjectImport, InputItem, ModuleType};

#[derive(Debug)]
pub struct NormalizedBundlerOptions {
Expand Down Expand Up @@ -49,4 +50,6 @@ pub struct NormalizedBundlerOptions {
pub experimental: ExperimentalOptions,
pub minify: bool,
pub define: Vec<(/* Target to be replaced */ String, /* Replacement */ String)>,
pub inject: Vec<InjectImport>,
pub oxc_inject_global_variables_config: InjectGlobalVariablesConfig,
}
1 change: 1 addition & 0 deletions crates/rolldown_common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub mod bundler_options {
types::{
es_module_flag::EsModuleFlag,
filename_template::{FileNameRenderOptions, FilenameTemplate},
inject_import::InjectImport,
input_item::InputItem,
is_external::IsExternal,
module_type::ModuleType,
Expand Down
Loading

0 comments on commit a2bb2ca

Please sign in to comment.