Skip to content

Commit

Permalink
feat: tree shake module side effect (#1395)
Browse files Browse the repository at this point in the history
  • Loading branch information
IWANABETHATGUY committed Jun 19, 2024
1 parent 3cf75b8 commit 6df56a9
Show file tree
Hide file tree
Showing 27 changed files with 272 additions and 9 deletions.
16 changes: 16 additions & 0 deletions Cargo.lock

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

16 changes: 13 additions & 3 deletions crates/rolldown/src/module_loader/normal_module_task.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use rolldown_common::{
side_effects::{DeterminedSideEffects, HookSideEffects},
AstScopes, ImportRecordId, ModuleDefFormat, ModuleType, NormalModule, NormalModuleId,
PackageJson, RawImportRecord, ResolvedPath, ResolvedRequestInfo, ResourceId, SymbolRef,
TreeshakeOptions,
};
use rolldown_error::BuildError;
use rolldown_oxc_utils::OxcAst;
Expand Down Expand Up @@ -168,17 +169,26 @@ impl NormalModuleTask {
DeterminedSideEffects::Analyzed(analyzed_side_effects)
})
};

let side_effects = match hook_side_effects {
Some(side_effects) => match side_effects {
HookSideEffects::True => lazy_check_side_effects(),
HookSideEffects::False => DeterminedSideEffects::UserDefined(false),
HookSideEffects::NoTreeshake => DeterminedSideEffects::NoTreeshake,
},
None => lazy_check_side_effects(),
// If user don't specify the side effects, we use fallback value from `option.treeshake.moduleSideEffects`;
None => match self.ctx.input_options.treeshake {
// Actually this convert is not necessary, just for passing type checking
TreeshakeOptions::False => DeterminedSideEffects::NoTreeshake,
TreeshakeOptions::Option(ref opt) => {
if opt.module_side_effects.resolve(&stable_resource_id) {
lazy_check_side_effects()
} else {
DeterminedSideEffects::UserDefined(false)
}
}
},
};
// TODO: Should we check if there are `check_side_effects_for` returns false but there are side effects in the module?

let module = NormalModule {
source,
id: self.module_id,
Expand Down
2 changes: 1 addition & 1 deletion crates/rolldown/src/stages/link_stage/tree_shaking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ impl LinkStage<'_> {
symbols: &self.symbols,
is_included_vec: &mut is_included_vec,
is_module_included_vec: &mut is_module_included_vec,
tree_shaking: self.input_options.treeshake,
tree_shaking: self.input_options.treeshake.enabled(),
runtime_id: self.runtime.id(),
used_exports_info_vec: &mut used_exports_info_vec,
metas: &self.metas,
Expand Down
2 changes: 1 addition & 1 deletion crates/rolldown/src/utils/normalize_options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ pub fn normalize_options(mut raw_options: crate::BundlerOptions) -> NormalizeOpt
.cwd
.unwrap_or_else(|| std::env::current_dir().expect("Failed to get current dir")),
external: raw_options.external,
treeshake: raw_options.treeshake.unwrap_or(true),
treeshake: raw_options.treeshake,
platform: raw_options.platform.unwrap_or(Platform::Browser),
entry_filenames: raw_options.entry_filenames.unwrap_or_else(|| "[name].js".to_string()).into(),
chunk_filenames: raw_options
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use super::plugin::BindingPluginOrParallelJsPluginPlaceholder;

mod binding_input_item;
mod binding_resolve_options;
mod treeshake;

#[napi(object, object_to_js = false)]
#[derive(Deserialize, Default, Derivative)]
Expand Down Expand Up @@ -66,6 +67,7 @@ pub struct BindingInputOptions {
// extra
pub cwd: String,
// pub builtins: BuiltinsOptions,
pub treeshake: Option<treeshake::BindingTreeshake>,
}

pub type BindingOnLog = Option<ThreadsafeFunction<(String, BindingLog), (), false>>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
use rolldown::{InnerOptions, ModuleSideEffects};
use rolldown_common::js_regex::HybridRegex;
use serde::Deserialize;

#[napi_derive::napi(object)]
#[derive(Deserialize, Debug, Default)]
#[serde(rename_all = "camelCase")]
pub struct BindingTreeshake {
pub module_side_effects: String,
}

impl TryFrom<BindingTreeshake> for rolldown::TreeshakeOptions {
fn try_from(value: BindingTreeshake) -> anyhow::Result<Self> {
match value.module_side_effects.as_str() {
"true" => {
Ok(Self::Option(InnerOptions { module_side_effects: ModuleSideEffects::Boolean(true) }))
}
"false" => {
Ok(Self::Option(InnerOptions { module_side_effects: ModuleSideEffects::Boolean(false) }))
}
_ => {
let regex = HybridRegex::new(&value.module_side_effects)?;
Ok(Self::Option(InnerOptions { module_side_effects: ModuleSideEffects::Regex(regex) }))
}
}
}

type Error = anyhow::Error;
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,10 @@ pub fn normalize_binding_options(
input: Some(input_options.input.into_iter().map(Into::into).collect()),
cwd: cwd.into(),
external,
treeshake: true.into(),
treeshake: match input_options.treeshake {
Some(v) => v.try_into().map_err(|err| napi::Error::new(napi::Status::GenericFailure, err))?,
None => rolldown::TreeshakeOptions::False,
},
resolve: input_options.resolve.map(Into::into),
platform: input_options
.platform
Expand Down
2 changes: 2 additions & 0 deletions crates/rolldown_common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ anyhow = { workspace = true }
dashmap = { workspace = true }
glob-match = { workspace = true }
oxc = { workspace = true, features = ["semantic"] }
regex = { workspace = true }
regress = "0.10.0"
rolldown_fs = { workspace = true }
rolldown_rstr = { workspace = true }
rolldown_sourcemap = { workspace = true }
Expand Down
22 changes: 21 additions & 1 deletion crates/rolldown_common/src/inner_bundler_options/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use serde::{Deserialize, Deserializer};

use crate::{ModuleType, SourceMapIgnoreList};

use self::types::treeshake::TreeshakeOptions;
use self::types::{
input_item::InputItem, is_external::IsExternal, output_format::OutputFormat,
output_option::AddonOutputOption, platform::Platform, resolve_options::ResolveOptions,
Expand All @@ -31,7 +32,6 @@ pub struct BundlerOptions {
schemars(with = "Option<Vec<String>>")
)]
pub external: Option<IsExternal>,
pub treeshake: Option<bool>,
pub platform: Option<Platform>,
pub shim_missing_exports: Option<bool>,
// --- options for output
Expand Down Expand Up @@ -70,6 +70,12 @@ pub struct BundlerOptions {
pub module_types: Option<HashMap<String, ModuleType>>,
// --- options for resolve
pub resolve: Option<ResolveOptions>,
#[cfg_attr(
feature = "deserialize_bundler_options",
serde(deserialize_with = "deserialize_treeshake", default),
schemars(with = "Option<bool>")
)]
pub treeshake: TreeshakeOptions,
}

#[cfg(feature = "deserialize_bundler_options")]
Expand All @@ -89,3 +95,17 @@ where
let deserialized = Option::<String>::deserialize(deserializer)?;
Ok(deserialized.map(|s| AddonOutputOption::String(Some(s))))
}

#[cfg(feature = "deserialize_bundler_options")]
fn deserialize_treeshake<'de, D>(deserializer: D) -> Result<TreeshakeOptions, D::Error>
where
D: Deserializer<'de>,
{
let deserialized = Option::<bool>::deserialize(deserializer)?;
match deserialized {
Some(false) => Ok(TreeshakeOptions::False),
Some(true) | None => Ok(TreeshakeOptions::Option(types::treeshake::InnerOptions {
module_side_effects: types::treeshake::ModuleSideEffects::Boolean(true),
})),
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ pub mod resolve_options;
pub mod source_map_type;
pub mod sourcemap_ignore_list;
pub mod sourcemap_path_transform;
pub mod treeshake;
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use rustc_hash::FxHashMap;

use crate::ModuleType;

use super::treeshake::TreeshakeOptions;
use super::{
filename_template::FilenameTemplate, is_external::IsExternal,
normalized_input_item::NormalizedInputItem, output_format::OutputFormat,
Expand All @@ -20,7 +21,8 @@ pub struct NormalizedBundlerOptions {
pub input: Vec<NormalizedInputItem>,
pub cwd: PathBuf,
pub external: Option<IsExternal>,
pub treeshake: bool,
/// corresponding to `false | NormalizedTreeshakeOption`
pub treeshake: TreeshakeOptions,
pub platform: Platform,
pub shim_missing_exports: bool,
/// The key is the extension. Unlike `BundlerOptions`, the extension doesn't start with a dot.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
use crate::types::js_regex::HybridRegex;

#[derive(Debug)]
pub enum TreeshakeOptions {
False,
Option(InnerOptions),
}

impl Default for TreeshakeOptions {
/// Used for snapshot testing
fn default() -> Self {
TreeshakeOptions::Option(InnerOptions { module_side_effects: ModuleSideEffects::Boolean(true) })
}
}

#[derive(Debug)]
pub enum ModuleSideEffects {
Regex(HybridRegex),
Boolean(bool),
}

impl ModuleSideEffects {
pub fn resolve(&self, path: &str) -> bool {
match self {
ModuleSideEffects::Regex(reg) => reg.matches(path),
ModuleSideEffects::Boolean(b) => *b,
}
}
}

impl TreeshakeOptions {
pub fn enabled(&self) -> bool {
matches!(self, TreeshakeOptions::Option(_))
}
}

#[derive(Debug)]
pub struct InnerOptions {
pub module_side_effects: ModuleSideEffects,
}
2 changes: 2 additions & 0 deletions crates/rolldown_common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ pub mod bundler_options {
source_map_type::SourceMapType,
sourcemap_ignore_list::SourceMapIgnoreList,
sourcemap_path_transform::SourceMapPathTransform,
treeshake::{InnerOptions, ModuleSideEffects, TreeshakeOptions},
},
BundlerOptions,
};
Expand Down Expand Up @@ -50,6 +51,7 @@ pub use crate::{
types::external_module_id::ExternalModuleId,
types::import_record::{ImportKind, ImportRecord, ImportRecordId, RawImportRecord},
types::importer_record::ImporterRecord,
types::js_regex,
types::module_def_format::ModuleDefFormat,
types::module_id::ModuleId,
types::module_info::ModuleInfo,
Expand Down
24 changes: 24 additions & 0 deletions crates/rolldown_common/src/types/js_regex.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/// According to the doc of `regress`, https://docs.rs/regress/0.10.0/regress/#comparison-to-regex-crate
/// **regress supports features that regex does not, in particular backreferences and zero-width lookaround assertions.**
/// these features are not commonly used, so in most cases the slow path will not be reached.
#[derive(Debug)]
pub enum HybridRegex {
Optimize(regex::Regex),
Ecma(regress::Regex),
}

impl HybridRegex {
pub fn new(source: &str) -> anyhow::Result<Self> {
match regex::Regex::new(source).map(HybridRegex::Optimize) {
Ok(reg) => Ok(reg),
Err(_) => regress::Regex::new(source).map(HybridRegex::Ecma).map_err(anyhow::Error::from),
}
}

pub fn matches(&self, text: &str) -> bool {
match self {
HybridRegex::Optimize(reg) => reg.is_match(text),
HybridRegex::Ecma(reg) => reg.find(text).is_some(),
}
}
}
1 change: 1 addition & 0 deletions crates/rolldown_common/src/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub mod exports_kind;
pub mod external_module_id;
pub mod import_record;
pub mod importer_record;
pub mod js_regex;
pub mod module_def_format;
pub mod module_id;
pub mod module_info;
Expand Down
5 changes: 5 additions & 0 deletions packages/rolldown/src/binding.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ export interface BindingInputOptions {
logLevel?: BindingLogLevel
onLog: (logLevel: 'debug' | 'warn' | 'info', log: BindingLog) => void
cwd: string
treeshake?: BindingTreeshake
}

export interface BindingJsonSourcemap {
Expand Down Expand Up @@ -233,6 +234,10 @@ export interface BindingSourcemap {
inner: string | BindingJSONSourcemap
}

export interface BindingTreeshake {
moduleSideEffects: string
}

export function registerPlugins(id: number, plugins: Array<BindingPluginWithIndex>): void

export interface RenderedChunk {
Expand Down
1 change: 1 addition & 0 deletions packages/rolldown/src/options/bindingify-input-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export function bindingifyInputOptions(
onLog: (level, log) => {
options.onLog(level, { code: log.code, message: log.message })
},
treeshake: options.treeshake,
}
}

Expand Down
9 changes: 8 additions & 1 deletion packages/rolldown/src/options/input-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
RollupLogSchema,
RollupLogWithStringSchema,
} from '../log/logging'
import { TreeshakingOptionsSchema, TreeshakingOptions } from '../treeshake'
import { BuiltinPlugin } from '../plugin/bindingify-builtin-plugin'

const inputOptionSchema = z
Expand Down Expand Up @@ -53,6 +54,7 @@ const inputOptionsSchema = z.strictObject({
.or(z.literal('neutral'))
.optional(),
shimMissingExports: z.boolean().optional(),
treeshake: z.boolean().or(TreeshakingOptionsSchema).optional(),
logLevel: LogLevelOptionSchema.optional(),
onLog: z
.function()
Expand All @@ -77,6 +79,11 @@ const inputOptionsSchema = z.strictObject({
.optional(),
})

export type InputOptions = z.infer<typeof inputOptionsSchema>
export type InputOption = z.infer<typeof inputOptionSchema>
export type ExternalOption = z.infer<typeof externalSchema>
export type InputOptions = Omit<
z.infer<typeof inputOptionsSchema>,
'treeshake'
> & {
treeshake?: boolean | TreeshakingOptions
}
5 changes: 5 additions & 0 deletions packages/rolldown/src/options/normalized-input-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,15 @@ import type { InputOptions } from './input-options'
import type { Plugin, ParallelPlugin } from '../plugin'
import type { LogLevel } from '../log/logging'
import { BuiltinPlugin } from '../plugin/bindingify-builtin-plugin'
import { NormalizedTreeshakingOptions } from '../../src/treeshake'

export interface NormalizedInputOptions extends InputOptions {
input: RollupNormalizedInputOptions['input']
plugins: (Plugin | ParallelPlugin | BuiltinPlugin)[]
onLog: (level: LogLevel, log: RollupLog) => void
logLevel: LogLevelOption
// After normalized, `false` will be converted to `undefined`, otherwise, default value will be assigned
// Because it is hard to represent Enum in napi, ref: https://github.com/napi-rs/napi-rs/issues/507
// So we use `undefined | NormalizedTreeshakingOptions` (or Option<NormalizedTreeshakingOptions> in rust side), to represent `false | NormalizedTreeshakingOptions`
treeshake?: NormalizedTreeshakingOptions
}
Loading

0 comments on commit 6df56a9

Please sign in to comment.