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 destructuring of import.meta #7229

Merged
merged 2 commits into from
Jul 19, 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
Original file line number Diff line number Diff line change
@@ -1,22 +1,39 @@
use rspack_core::{ConstDependency, SpanExt};
use itertools::Itertools;
use rspack_core::{property_access, ConstDependency, SpanExt};
use rspack_error::miette::Severity;
use swc_core::common::{Span, Spanned};
use swc_core::ecma::ast::MemberProp;
use url::Url;

use super::JavascriptParserPlugin;
use crate::utils::eval;
use crate::visitors::expr_name;
use crate::visitors::ExportedVariableInfo;
use crate::visitors::JavascriptParser;
use crate::visitors::{create_traceable_error, RootName};
use crate::visitors::{expr_name, AllowedMemberTypes};
use crate::visitors::{ExportedVariableInfo, MemberExpressionInfo};

// Port from https://github.com/webpack/webpack/blob/main/lib/dependencies/ImportMetaPlugin.js
// TODO:
// - scan `import.meta.url.indexOf("index.js")`
// - evaluate expression. eg `import.meta.env && import.meta.env.xx` should be `false`
pub struct ImportMetaPlugin;

impl ImportMetaPlugin {
fn import_meta_url(&self, parser: &JavascriptParser) -> String {
Url::from_file_path(&parser.resource_data.resource)
.expect("should be a path")
.to_string()
}

fn import_meta_webpack_version(&self) -> String {
"5".to_string()
}

fn import_meta_unknown_property(&self, members: &Vec<String>) -> String {
format!(
r#"/* unsupported import.meta.{} */ undefined{}"#,
members.join("."),
property_access(members, 1)
)
}
}

impl JavascriptParserPlugin for ImportMetaPlugin {
fn evaluate_typeof(
&self,
Expand Down Expand Up @@ -57,8 +74,11 @@ impl JavascriptParserPlugin for ImportMetaPlugin {
if ident == expr_name::IMPORT_META_WEBPACK {
Some(eval::evaluate_to_number(5_f64, start, end))
} else if ident == expr_name::IMPORT_META_URL {
let url = Url::from_file_path(&parser.resource_data.resource).expect("should be a path");
Some(eval::evaluate_to_string(url.to_string(), start, end))
Some(eval::evaluate_to_string(
self.import_meta_url(parser),
start,
end,
))
} else {
None
}
Expand Down Expand Up @@ -112,29 +132,60 @@ impl JavascriptParserPlugin for ImportMetaPlugin {
span: Span,
) -> Option<bool> {
if root_name == expr_name::IMPORT_META {
// import.meta
// warn when access import.meta directly
parser.warning_diagnostics.push(Box::new(create_traceable_error(
if let Some(referenced_properties_in_destructuring) =
parser.destructuring_assignment_properties_for(&span)
{
let mut content = vec![];
for prop in referenced_properties_in_destructuring {
if prop == "url" {
content.push(format!(r#"url: "{}""#, self.import_meta_url(parser)))
} else if prop == "webpack" {
content.push(format!(
r#"webpack: {}"#,
self.import_meta_webpack_version()
));
} else {
content.push(format!(
r#"[{}]: {}"#,
serde_json::to_string(&prop).expect("json stringify failed"),
self.import_meta_unknown_property(&vec![prop])
));
}
}
parser
.presentational_dependencies
.push(Box::new(ConstDependency::new(
span.real_lo(),
span.real_hi(),
format!("({{{}}})", content.join(",")).into(),
None,
)));
Some(true)
} else {
// import.meta
// warn when access import.meta directly
parser.warning_diagnostics.push(Box::new(create_traceable_error(
"Critical dependency".into(),
"Accessing import.meta directly is unsupported (only property access or destructuring is supported)".into(),
parser.source_file,
span.into()
).with_severity(Severity::Warning)));

let content = if parser.is_asi_position(span.lo()) {
";({})"
} else {
"({})"
};
parser
.presentational_dependencies
.push(Box::new(ConstDependency::new(
span.real_lo(),
span.real_hi(),
content.into(),
None,
)));
Some(true)
let content = if parser.is_asi_position(span.lo()) {
";({})"
} else {
"({})"
};
parser
.presentational_dependencies
.push(Box::new(ConstDependency::new(
span.real_lo(),
span.real_hi(),
content.into(),
None,
)));
Some(true)
}
} else {
None
}
Expand All @@ -148,13 +199,12 @@ impl JavascriptParserPlugin for ImportMetaPlugin {
) -> Option<bool> {
if for_name == expr_name::IMPORT_META_URL {
// import.meta.url
let url = Url::from_file_path(&parser.resource_data.resource).expect("should be a path");
parser
.presentational_dependencies
.push(Box::new(ConstDependency::new(
member_expr.span().real_lo(),
member_expr.span().real_hi(),
format!("'{url}'").into(),
format!("'{}'", self.import_meta_url(parser)).into(),
None,
)));
Some(true)
Expand All @@ -165,7 +215,7 @@ impl JavascriptParserPlugin for ImportMetaPlugin {
.push(Box::new(ConstDependency::new(
member_expr.span().real_lo(),
member_expr.span().real_hi(),
"5".to_string().into(),
self.import_meta_webpack_version().into(),
None,
)));
Some(true)
Expand All @@ -183,12 +233,25 @@ impl JavascriptParserPlugin for ImportMetaPlugin {
match root_info {
ExportedVariableInfo::Name(root) => {
if root == expr_name::IMPORT_META {
let members = parser
.get_member_expression_info(expr, AllowedMemberTypes::Expression)
.and_then(|info| match info {
MemberExpressionInfo::Expression(res) => Some(res.members),
_ => None,
});
parser
.presentational_dependencies
.push(Box::new(ConstDependency::new(
expr.span().real_lo(),
expr.span().real_hi(),
"undefined".into(),
members
.map(|members| {
self.import_meta_unknown_property(
&members.iter().map(|x| x.to_string()).collect_vec(),
)
})
.unwrap_or("undefined".to_string())
.into(),
None,
)));
return Some(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ pub struct JavascriptParser<'parser> {
pub(crate) member_expr_in_optional_chain: bool,
// TODO: delete `properties_in_destructuring`
pub(crate) properties_in_destructuring: FxHashMap<Atom, FxHashSet<Atom>>,
pub(crate) destructuring_assignment_properties: Option<FxHashMap<Span, FxHashSet<String>>>,
pub(crate) semicolons: &'parser mut FxHashSet<BytePos>,
pub(crate) statement_path: Vec<StatementPath>,
pub(crate) prev_statement: Option<StatementPath>,
Expand Down Expand Up @@ -379,6 +380,7 @@ impl<'parser> JavascriptParser<'parser> {
module_identifier,
member_expr_in_optional_chain: false,
properties_in_destructuring: Default::default(),
destructuring_assignment_properties: None,
semicolons,
statement_path: Default::default(),
current_tag_info: None,
Expand Down Expand Up @@ -854,6 +856,7 @@ impl<'parser> JavascriptParser<'parser> {

pub fn walk_program(&mut self, ast: &Program) {
if self.plugin_drive.clone().program(self, ast).is_none() {
self.destructuring_assignment_properties = Some(FxHashMap::default());
match ast {
Program::Module(m) => {
self.set_strict(true);
Expand All @@ -874,6 +877,7 @@ impl<'parser> JavascriptParser<'parser> {
self.walk_statements(&s.body);
}
};
self.destructuring_assignment_properties = None;
}
self.plugin_drive.clone().finish(self);
}
Expand Down Expand Up @@ -906,6 +910,13 @@ impl<'parser> JavascriptParser<'parser> {
pub fn is_unresolved_ident(&mut self, str: &str) -> bool {
self.definitions_db.get(self.definitions, str).is_none()
}

pub fn destructuring_assignment_properties_for(&self, span: &Span) -> Option<FxHashSet<String>> {
self
.destructuring_assignment_properties
.as_ref()
.and_then(|x| x.get(span).cloned())
}
}

impl<'parser> JavascriptParser<'parser> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@ impl<'parser> JavascriptParser<'parser> {
if let Some(assign) = stmt.expr.as_assign() {
self.pre_walk_assignment_expression(assign)
}
if let Some(paren) = stmt.expr.as_paren()
&& let Some(assign) = paren.expr.as_assign()
{
self.pre_walk_assignment_expression(assign)
}
}

pub(super) fn block_pre_walk_variable_declaration(&mut self, decl: &VarDecl) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::borrow::Cow;

use rustc_hash::FxHashSet;
use swc_core::common::Spanned;
use swc_core::ecma::ast::{AssignExpr, BlockStmt, CatchClause, Decl, DoWhileStmt};
use swc_core::ecma::ast::{ForInStmt, ForOfStmt, ForStmt, IfStmt, LabeledStmt, WithStmt};
use swc_core::ecma::ast::{ModuleDecl, ModuleItem, ObjectPat, ObjectPatProp, Stmt, WhileStmt};
Expand Down Expand Up @@ -193,9 +194,15 @@ impl<'parser> JavascriptParser<'parser> {
for prop in &obj_pat.props {
match prop {
ObjectPatProp::KeyValue(prop) => {
let name = eval::eval_prop_name(&prop.key);
if let Some(id) = name.and_then(|id| id.as_string()) {
keys.insert(id);
if let Some(ident_key) = prop.key.as_ident() {
keys.insert(ident_key.sym.to_string());
} else {
let name = eval::eval_prop_name(&prop.key);
if let Some(id) = name.and_then(|id| id.as_string()) {
keys.insert(id);
} else {
return None;
}
}
}
ObjectPatProp::Assign(prop) => {
Expand All @@ -208,29 +215,66 @@ impl<'parser> JavascriptParser<'parser> {
}

fn pre_walk_variable_declarator(&mut self, declarator: &VarDeclarator) {
if self.destructuring_assignment_properties.is_none() {
return;
}

let Some(init) = declarator.init.as_ref() else {
return;
};
let Some(obj_pat) = declarator.name.as_object() else {
return;
};
let keys = self._pre_walk_object_pattern(obj_pat);
if keys.is_none() {
let Some(keys) = self._pre_walk_object_pattern(obj_pat) else {
return;
};

let destructuring_assignment_properties = self
.destructuring_assignment_properties
.as_mut()
.unwrap_or_else(|| unreachable!());

if let Some(await_expr) = declarator
.init
.as_ref()
.and_then(|decl| decl.as_await_expr())
{
destructuring_assignment_properties.insert(await_expr.arg.span(), keys);
} else {
destructuring_assignment_properties.insert(declarator.init.span(), keys);
}

if let Some(assign) = init.as_assign() {
self.pre_walk_assignment_expression(assign);
}
}

pub(super) fn pre_walk_assignment_expression(&mut self, assign: &AssignExpr) {
if self.destructuring_assignment_properties.is_none() {
return;
}
let Some(left) = assign.left.as_pat().and_then(|pat| pat.as_object()) else {
return;
};
let keys = self._pre_walk_object_pattern(left);
if keys.is_none() {
let Some(mut keys) = self._pre_walk_object_pattern(left) else {
return;
};

let destructuring_assignment_properties = self
.destructuring_assignment_properties
.as_mut()
.unwrap_or_else(|| unreachable!());

if let Some(set) = destructuring_assignment_properties.remove(&assign.span()) {
keys.extend(set);
}

if let Some(await_expr) = assign.right.as_await_expr() {
destructuring_assignment_properties.insert(await_expr.arg.span(), keys);
} else {
destructuring_assignment_properties.insert(assign.right.span(), keys);
}

if let Some(right) = assign.right.as_assign() {
self.pre_walk_assignment_expression(right)
}
Expand Down
16 changes: 8 additions & 8 deletions tests/webpack-test/cases/esm/import-meta/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,17 @@ it("should return correct import.meta.webpack", () => {
it("should return undefined for unknown property", () => {
expect(import.meta.other).toBe(undefined);
if (typeof import.meta.other !== "undefined") require("fail");
// expect(() => import.meta.other.other.other).toThrowError();
expect(() => import.meta.other.other.other).toThrowError();
});

it("should add warning on direct import.meta usage", () => {
expect(Object.keys(import.meta)).toHaveLength(0);
});

// it("should support destructuring assignment", () => {
// let version, url2, c;
// ({ webpack: version } = { url: url2 } = { c } = import.meta);
// expect(version).toBeTypeOf("number");
// expect(url2).toBe(url);
// expect(c).toBe(undefined);
// });
it("should support destructuring assignment", () => {
let version, url2, c;
({ webpack: version } = { url: url2 } = { c } = import.meta);
expect(version).toBeTypeOf("number");
expect(url2).toBe(url);
expect(c).toBe(undefined);
});
17 changes: 0 additions & 17 deletions tests/webpack-test/cases/esm/import-meta/test.filter.js

This file was deleted.

Loading