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(turbopack-ecmascript): Add implementation for webpackIgnore and turbopackIgnore (revision of #69113) #69768

Merged
merged 4 commits into from
Sep 9, 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
8 changes: 7 additions & 1 deletion turbopack/crates/turbopack-ecmascript/src/analyzer/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1242,7 +1242,13 @@ impl VisitAstPath for Analyzer<'_> {
if let Some(require_var_id) = extract_var_from_umd_factory(callee, &n.args) {
self.add_value(
require_var_id,
JsValue::WellKnownFunction(WellKnownFunctionKind::Require),
JsValue::WellKnownFunction(WellKnownFunctionKind::Require {
ignore: self
.eval_context
.imports
.get_overrides(n.callee.span())
.ignore,
}),
);
Comment on lines +1245 to 1252
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why having WellKnownFunctionKind ::Require { ignore: true } at all?

This could just be JsValue::Unknown for a ignored require or import.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might've been needed when this code wasn't bailing out as early before. I'll try removing it 👍

}
}
Expand Down
69 changes: 59 additions & 10 deletions turbopack/crates/turbopack-ecmascript/src/analyzer/imports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use std::{
use indexmap::{IndexMap, IndexSet};
use once_cell::sync::Lazy;
use swc_core::{
common::{comments::Comments, source_map::SmallPos, Span, Spanned},
common::{comments::Comments, source_map::SmallPos, BytePos, Span, Spanned},
ecma::{
ast::*,
atoms::{js_word, JsWord},
Expand Down Expand Up @@ -140,16 +140,56 @@ pub(crate) struct ImportMap {
/// True if the module is an ESM module due to top-level await.
has_top_level_await: bool,

/// Locations of webpackIgnore or turbopackIgnore comments
/// This is a webpack feature that allows opting out of static
/// imports, which we should respect.
/// Locations of [webpack-style "magic comments"][magic] that override import behaviors.
///
/// Most commonly, these are `/* webpackIgnore: true */` comments. See [ImportOverrides] for
/// full details.
///
/// [magic]: https://webpack.js.org/api/module-methods/#magic-comments
overrides: HashMap<BytePos, ImportOverrides>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel overrides is a weird name for that.

Maybe annotations, attributes, hints or magic_comments

}

/// Represents a collection of [webpack-style "magic comments"][magic] that override import
/// behaviors.
///
/// [magic]: https://webpack.js.org/api/module-methods/#magic-comments
#[derive(Debug)]
pub(crate) struct ImportOverrides {
/// Should we ignore this import expression when bundling? If so, the import expression will be
/// left as-is in Turbopack's output.
///
/// This is set by using either a `webpackIgnore` or `turbopackIgnore` comment.
///
/// Example:
/// ```js
/// const a = import(/* webpackIgnore: true */ "a");
/// const b = import(/* turbopackIgnore: true */ "b");
/// ```
turbopack_ignores: HashMap<Span, bool>,
pub ignore: bool,
}

impl ImportOverrides {
pub const fn empty() -> Self {
ImportOverrides { ignore: false }
}

pub fn empty_ref() -> &'static Self {
// use `Self::empty` here as `Default::default` isn't const
static DEFAULT_VALUE: ImportOverrides = ImportOverrides::empty();
&DEFAULT_VALUE
}
}

impl Default for ImportOverrides {
fn default() -> Self {
ImportOverrides::empty()
}
}

impl Default for &ImportOverrides {
fn default() -> Self {
ImportOverrides::empty_ref()
}
}

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
Expand Down Expand Up @@ -194,6 +234,10 @@ impl ImportMap {
None
}

pub fn get_overrides(&self, span: Span) -> &ImportOverrides {
self.overrides.get(&span.lo).unwrap_or_default()
}

// TODO this could return &str instead of String to avoid cloning
pub fn get_binding(&self, id: &Id) -> Option<(usize, Option<RcStr>)> {
if let Some((i, i_sym)) = self.imports.get(id) {
Expand Down Expand Up @@ -459,7 +503,7 @@ impl Visit for Analyzer<'_> {
};

// we are interested here in the last comment with a valid directive
let ignore_statement = n
let ignore_directive = n
.args
.first()
.map(|arg| arg.span_lo())
Expand All @@ -478,10 +522,15 @@ impl Visit for Analyzer<'_> {
})
.next();

if let Some((callee_span, ignore_statement)) = callee_span.zip(ignore_statement) {
self.data
.turbopack_ignores
.insert(*callee_span, ignore_statement);
// potentially support more webpack magic comments in the future:
// https://webpack.js.org/api/module-methods/#magic-comments
if let Some((callee_span, ignore_directive)) = callee_span.zip(ignore_directive) {
self.data.overrides.insert(
callee_span.lo,
ImportOverrides {
ignore: ignore_directive,
},
);
};
}

Expand Down
35 changes: 23 additions & 12 deletions turbopack/crates/turbopack-ecmascript/src/analyzer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1627,11 +1627,11 @@ impl JsValue {
format!("path.resolve({cwd})"),
"The Node.js path.resolve method: https://nodejs.org/api/path.html#pathresolvepaths",
),
WellKnownFunctionKind::Import => (
WellKnownFunctionKind::Import { .. } => (
"import".to_string(),
"The dynamic import() method from the ESM specification: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#dynamic_imports"
),
WellKnownFunctionKind::Require => ("require".to_string(), "The require method from CommonJS"),
WellKnownFunctionKind::Require { .. } => ("require".to_string(), "The require method from CommonJS"),
WellKnownFunctionKind::RequireResolve => ("require.resolve".to_string(), "The require.resolve method from CommonJS"),
WellKnownFunctionKind::RequireContext => ("require.context".to_string(), "The require.context method from webpack"),
WellKnownFunctionKind::RequireContextRequire(..) => ("require.context(...)".to_string(), "The require.context(...) method from webpack: https://webpack.js.org/api/module-methods/#requirecontext"),
Expand Down Expand Up @@ -3626,8 +3626,14 @@ pub enum WellKnownFunctionKind {
PathDirname,
/// `0` is the current working directory.
PathResolve(Box<JsValue>),
Import,
Require,
/// Import and Require can be ignored at compile time using the `turbopackIgnore` directive.
/// This is functionality that was introduced in webpack, so we also support `webpackIgnore`.
Import {
ignore: bool,
},
Require {
ignore: bool,
},
RequireResolve,
RequireContext,
RequireContextRequire(Vc<RequireContextValue>),
Expand Down Expand Up @@ -3656,8 +3662,8 @@ pub enum WellKnownFunctionKind {
impl WellKnownFunctionKind {
pub fn as_define_name(&self) -> Option<&[&str]> {
match self {
Self::Import => Some(&["import"]),
Self::Require => Some(&["require"]),
Self::Import { .. } => Some(&["import"]),
Self::Require { .. } => Some(&["require"]),
Self::RequireResolve => Some(&["require", "resolve"]),
Self::RequireContext => Some(&["require", "context"]),
Self::Define => Some(&["define"]),
Expand Down Expand Up @@ -3704,7 +3710,7 @@ pub mod test_utils {
let mut new_value = match v {
JsValue::Call(
_,
box JsValue::WellKnownFunction(WellKnownFunctionKind::Import),
box JsValue::WellKnownFunction(WellKnownFunctionKind::Import { .. }),
ref args,
) => match &args[0] {
JsValue::Constant(v) => JsValue::Module(ModuleValue {
Expand Down Expand Up @@ -3740,8 +3746,12 @@ pub mod test_utils {
Err(err) => v.into_unknown(true, PrettyPrintError(&err).to_string()),
},
JsValue::FreeVar(ref var) => match &**var {
"import" => JsValue::WellKnownFunction(WellKnownFunctionKind::Import),
"require" => JsValue::WellKnownFunction(WellKnownFunctionKind::Require),
"import" => {
JsValue::WellKnownFunction(WellKnownFunctionKind::Import { ignore: false })
}
"require" => {
JsValue::WellKnownFunction(WellKnownFunctionKind::Require { ignore: false })
}
"define" => JsValue::WellKnownFunction(WellKnownFunctionKind::Define),
"__dirname" => "__dirname".into(),
"__filename" => "__filename".into(),
Expand Down Expand Up @@ -3772,7 +3782,7 @@ mod tests {
use std::{mem::take, path::PathBuf, time::Instant};

use swc_core::{
common::Mark,
common::{comments::SingleThreadedComments, Mark},
ecma::{
ast::EsVersion, parser::parse_file_as_program, transforms::base::resolver,
visit::VisitMutWith,
Expand Down Expand Up @@ -3809,11 +3819,12 @@ mod tests {
r.block_on(async move {
let fm = cm.load_file(&input).unwrap();

let comments = SingleThreadedComments::default();
let mut m = parse_file_as_program(
&fm,
Default::default(),
EsVersion::latest(),
None,
Some(&comments),
&mut vec![],
)
.map_err(|err| err.into_diagnostic(handler).emit())?;
Expand All @@ -3823,7 +3834,7 @@ mod tests {
m.visit_mut_with(&mut resolver(unresolved_mark, top_level_mark, false));

let eval_context =
EvalContext::new(&m, unresolved_mark, top_level_mark, None, None);
EvalContext::new(&m, unresolved_mark, top_level_mark, Some(&comments), None);

let mut var_graph = create_graph(&m, &eval_context);

Expand Down
24 changes: 16 additions & 8 deletions turbopack/crates/turbopack-ecmascript/src/analyzer/well_known.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,12 @@ pub async fn well_known_function_call(
WellKnownFunctionKind::PathJoin => path_join(args),
WellKnownFunctionKind::PathDirname => path_dirname(args),
WellKnownFunctionKind::PathResolve(cwd) => path_resolve(*cwd, args),
WellKnownFunctionKind::Import => JsValue::unknown(
WellKnownFunctionKind::Import { .. } => JsValue::unknown(
JsValue::call(Box::new(JsValue::WellKnownFunction(kind)), args),
true,
"import() is not supported",
),
WellKnownFunctionKind::Require => require(args),
WellKnownFunctionKind::Require { ignore } => require(args, ignore),
WellKnownFunctionKind::RequireContextRequire(value) => {
require_context_require(value, args).await?
}
Expand Down Expand Up @@ -322,7 +322,11 @@ pub fn path_dirname(mut args: Vec<JsValue>) -> JsValue {
)
}

pub fn require(args: Vec<JsValue>) -> JsValue {
/// Resolve the contents of a require call, throwing errors
/// if we come across any unsupported syntax.
///
/// `ignore` is true if the require call is marked with `turbopackIgnore` or `webpackIgnore`.
pub fn require(args: Vec<JsValue>, ignore: bool) -> JsValue {
if args.len() == 1 {
if let Some(s) = args[0].as_str() {
JsValue::Module(ModuleValue {
Expand All @@ -332,7 +336,9 @@ pub fn require(args: Vec<JsValue>) -> JsValue {
} else {
JsValue::unknown(
JsValue::call(
Box::new(JsValue::WellKnownFunction(WellKnownFunctionKind::Require)),
Box::new(JsValue::WellKnownFunction(WellKnownFunctionKind::Require {
ignore,
})),
args,
),
true,
Expand All @@ -342,7 +348,9 @@ pub fn require(args: Vec<JsValue>) -> JsValue {
} else {
JsValue::unknown(
JsValue::call(
Box::new(JsValue::WellKnownFunction(WellKnownFunctionKind::Require)),
Box::new(JsValue::WellKnownFunction(WellKnownFunctionKind::Require {
ignore,
})),
args,
),
true,
Expand Down Expand Up @@ -520,13 +528,13 @@ pub fn path_to_file_url(args: Vec<JsValue>) -> JsValue {

pub fn well_known_function_member(kind: WellKnownFunctionKind, prop: JsValue) -> (JsValue, bool) {
let new_value = match (kind, prop.as_str()) {
(WellKnownFunctionKind::Require, Some("resolve")) => {
(WellKnownFunctionKind::Require { .. }, Some("resolve")) => {
JsValue::WellKnownFunction(WellKnownFunctionKind::RequireResolve)
}
(WellKnownFunctionKind::Require, Some("cache")) => {
(WellKnownFunctionKind::Require { .. }, Some("cache")) => {
JsValue::WellKnownObject(WellKnownObjectKind::RequireCache)
}
(WellKnownFunctionKind::Require, Some("context")) => {
(WellKnownFunctionKind::Require { .. }, Some("context")) => {
JsValue::WellKnownFunction(WellKnownFunctionKind::RequireContext)
}
(WellKnownFunctionKind::RequireContextRequire(val), Some("resolve")) => {
Expand Down
10 changes: 9 additions & 1 deletion turbopack/crates/turbopack-ecmascript/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,7 @@ impl EcmascriptParsable for EcmascriptModuleAsset {
impl EcmascriptAnalyzable for EcmascriptModuleAsset {
#[turbo_tasks::function]
fn analyze(self: Vc<Self>) -> Vc<AnalyzeEcmascriptModuleResult> {
analyse_ecmascript_module(self, None, None)
analyse_ecmascript_module(self, None)
}

/// Generates module contents without an analysis pass. This is useful for
Expand Down Expand Up @@ -417,6 +417,7 @@ impl EcmascriptModuleAsset {
pub fn new(
source: Vc<Box<dyn Source>>,
asset_context: Vc<Box<dyn AssetContext>>,

ty: Value<EcmascriptModuleAssetType>,
transforms: Vc<EcmascriptInputTransforms>,
options: Vc<EcmascriptOptions>,
Expand All @@ -428,6 +429,7 @@ impl EcmascriptModuleAsset {
ty: ty.into_value(),
transforms,
options,

compile_time_info,
inner_assets: None,
last_successful_parse: Default::default(),
Expand All @@ -440,6 +442,7 @@ impl EcmascriptModuleAsset {
asset_context: Vc<Box<dyn AssetContext>>,
ty: Value<EcmascriptModuleAssetType>,
transforms: Vc<EcmascriptInputTransforms>,

options: Vc<EcmascriptOptions>,
compile_time_info: Vc<CompileTimeInfo>,
inner_assets: Vc<InnerAssets>,
Expand All @@ -461,6 +464,11 @@ impl EcmascriptModuleAsset {
Ok(self.await?.source)
}

#[turbo_tasks::function]
pub fn analyze(self: Vc<Self>) -> Vc<AnalyzeEcmascriptModuleResult> {
analyse_ecmascript_module(self, None)
}

#[turbo_tasks::function]
pub async fn options(self: Vc<Self>) -> Result<Vc<EcmascriptOptions>> {
Ok(self.await?.options)
Expand Down
6 changes: 3 additions & 3 deletions turbopack/crates/turbopack-ecmascript/src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ async fn parse_internal(
FileContent::Content(file) => match file.content().to_str() {
Ok(string) => {
let transforms = &*transforms.await?;
match parse_content(
match parse_file_content(
string.into_owned(),
fs_path_vc,
fs_path,
Expand Down Expand Up @@ -246,7 +246,7 @@ async fn parse_internal(
})
}

async fn parse_content(
async fn parse_file_content(
string: String,
fs_path_vc: Vc<FileSystemPath>,
fs_path: &FileSystemPath,
Expand Down Expand Up @@ -433,7 +433,7 @@ async fn parse_content(
&parsed_program,
unresolved_mark,
top_level_mark,
None,
Some(&comments),
Some(source),
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ pub struct EsmAssetReference {
pub origin: Vc<Box<dyn ResolveOrigin>>,
pub request: Vc<Request>,
pub annotations: ImportAnnotations,
/// True if the import should be ignored
/// This can happen for example when the webpackIgnore or turbopackIgnore
/// directives are present
pub ignore: bool,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That looks unused, could we remove that?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, I think this was left over from my refactoring efforts.

pub issue_source: Option<Vc<IssueSource>>,
pub export_name: Option<Vc<ModulePart>>,
pub import_externals: bool,
Expand Down Expand Up @@ -123,12 +127,14 @@ impl EsmAssetReference {
annotations: Value<ImportAnnotations>,
export_name: Option<Vc<ModulePart>>,
import_externals: bool,
ignore: bool,
) -> Vc<Self> {
Self::cell(EsmAssetReference {
origin,
request,
issue_source,
annotations: annotations.into_value(),
ignore,
export_name,
import_externals,
})
Expand All @@ -144,6 +150,9 @@ impl EsmAssetReference {
impl ModuleReference for EsmAssetReference {
#[turbo_tasks::function]
async fn resolve_reference(&self) -> Result<Vc<ModuleResolveResult>> {
if self.ignore {
return Ok(ModuleResolveResult::ignored().cell());
}
let ty = if matches!(self.annotations.module_type(), Some("json")) {
EcmaScriptModulesReferenceSubType::ImportWithType(ImportWithType::Json)
} else if let Some(part) = &self.export_name {
Expand Down
Loading
Loading