Skip to content

Commit 70bda6a

Browse files
authored
feat(es/parser): Support parsing CommonJS (#10600)
**Related issue:** - Closes #10597
1 parent 26eceed commit 70bda6a

File tree

18 files changed

+270
-6
lines changed

18 files changed

+270
-6
lines changed

.changeset/gentle-mangos-perform.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
swc_compiler_base: patch
3+
swc_config: patch
4+
swc_ecma_lexer: patch
5+
swc_ecma_parser: patch
6+
swc_core: patch
7+
---
8+
9+
feat(es/parser): Support parsing CommonJS

bindings/binding_core_wasm/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@ export function minify(src: string, opts?: JsMinifyOptions): Promise<Output>;
1313
export function minifySync(code: string, opts?: JsMinifyOptions): Output;
1414
1515
export function parse(src: string, options: ParseOptions & {
16-
isModule: false;
16+
isModule: false | "commonjs";
1717
}): Promise<Script>;
1818
export function parse(src: string, options?: ParseOptions): Promise<Module>;
1919
export function parseSync(src: string, options: ParseOptions & {
20-
isModule: false;
20+
isModule: false | "commonjs";
2121
}): Script;
2222
export function parseSync(src: string, options?: ParseOptions): Module;
2323

bindings/binding_core_wasm/src/types.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -547,7 +547,7 @@ export interface Options extends Config {
547547
548548
plugin?: Plugin;
549549
550-
isModule?: boolean | 'unknown';
550+
isModule?: boolean | 'unknown' | 'commonjs';
551551
552552
/**
553553
* Destination path. Note that this value is used only to fix source path

crates/swc_compiler_base/src/lib.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@ use swc_config::{file_pattern::FilePattern, is_module::IsModule, types::BoolOr};
2222
use swc_ecma_ast::{EsVersion, Ident, IdentName, Program};
2323
use swc_ecma_codegen::{text_writer::WriteJs, Emitter, Node};
2424
use swc_ecma_minifier::js::JsMinifyCommentOption;
25-
use swc_ecma_parser::{parse_file_as_module, parse_file_as_program, parse_file_as_script, Syntax};
25+
use swc_ecma_parser::{
26+
parse_file_as_commonjs, parse_file_as_module, parse_file_as_program, parse_file_as_script,
27+
Syntax,
28+
};
2629
use swc_ecma_visit::{noop_visit_type, Visit, VisitWith};
2730
use swc_timer::timer;
2831

@@ -79,6 +82,10 @@ pub fn parse_js(
7982
parse_file_as_script(&fm, syntax, target, comments, &mut errors)
8083
.map(Program::Script)
8184
}
85+
IsModule::CommonJS => {
86+
parse_file_as_commonjs(&fm, syntax, target, comments, &mut errors)
87+
.map(Program::Script)
88+
}
8289
IsModule::Unknown => parse_file_as_program(&fm, syntax, target, comments, &mut errors),
8390
};
8491

crates/swc_config/src/is_module.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use crate::merge::Merge;
1111
pub enum IsModule {
1212
Bool(bool),
1313
Unknown,
14+
CommonJS,
1415
}
1516

1617
impl Default for IsModule {
@@ -35,6 +36,7 @@ impl Serialize for IsModule {
3536
match *self {
3637
IsModule::Bool(ref b) => b.serialize(serializer),
3738
IsModule::Unknown => "unknown".serialize(serializer),
39+
IsModule::CommonJS => "commonjs".serialize(serializer),
3840
}
3941
}
4042
}
@@ -61,6 +63,7 @@ impl Visitor<'_> for IsModuleVisitor {
6163
{
6264
match s {
6365
"unknown" => Ok(IsModule::Unknown),
66+
"commonjs" => Ok(IsModule::CommonJS),
6467
_ => Err(serde::de::Error::invalid_value(Unexpected::Str(s), &self)),
6568
}
6669
}

crates/swc_ecma_lexer/src/parser/mod.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,25 @@ impl<I: Tokens<TokenAndSpan>> Parser<I> {
111111
})
112112
}
113113

114+
pub fn parse_commonjs(&mut self) -> PResult<Script> {
115+
trace_cur!(self, parse_commonjs);
116+
117+
// CommonJS module is acctually in a function scope
118+
let ctx = (self.ctx() & !Context::Module)
119+
| Context::InFunction
120+
| Context::InsideNonArrowFunctionScope;
121+
self.set_ctx(ctx);
122+
123+
let start = cur_pos!(self);
124+
let shebang = parse_shebang(self)?;
125+
126+
parse_stmt_block_body(self, true, None).map(|body| Script {
127+
span: span!(self, start),
128+
body,
129+
shebang,
130+
})
131+
}
132+
114133
#[cfg(test)]
115134
pub fn parse_typescript_module(&mut self) -> PResult<Module> {
116135
trace_cur!(self, parse_typescript_module);
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
use std::{ffi::OsStr, path::Path};
2+
3+
use swc_common::{
4+
errors::{ColorConfig, Handler},
5+
sync::Lrc,
6+
SourceMap,
7+
};
8+
use swc_ecma_ast::Program;
9+
use swc_ecma_lexer::{EsSyntax, TsSyntax};
10+
use swc_ecma_parser::{lexer::Lexer, Parser, StringInput, Syntax};
11+
12+
fn main() {
13+
let cm: Lrc<SourceMap> = Default::default();
14+
let handler = Handler::with_tty_emitter(ColorConfig::Auto, true, false, Some(cm.clone()));
15+
16+
// read file path from command line argument or use a default
17+
let file_path = std::env::args()
18+
.nth(1)
19+
.unwrap_or_else(|| "test.js".to_string());
20+
21+
let file_path = Path::new(&file_path);
22+
23+
let ext = file_path.extension().map(OsStr::to_ascii_lowercase);
24+
let is_ts = ext.as_ref().is_some_and(|e| {
25+
e == "ts" || e == "tsx" || e == "mts" || e == "cts" || e == "mtsx" || e == "ctsx"
26+
});
27+
let is_d_ts = is_ts
28+
&& file_path
29+
.file_name()
30+
.and_then(OsStr::to_str)
31+
.is_some_and(|s| {
32+
let mut iter = s.rsplit('.');
33+
34+
if iter.next() != Some("ts") {
35+
return false;
36+
}
37+
38+
iter.next() == Some("d") || iter.next() == Some("d")
39+
});
40+
41+
let is_jsx = ext
42+
.as_ref()
43+
.is_some_and(|e| e == "jsx" || e == "tsx" || e == "mjsx" || e == "cjsx");
44+
let is_esm = ext
45+
.as_ref()
46+
.is_some_and(|e| e == "mjs" || e == "mts" || e == "mjsx" || e == "mtsx");
47+
let is_commonjs = ext
48+
.as_ref()
49+
.is_some_and(|e| e == "cjs" || e == "cts" || e == "cjsx" || e == "ctsx");
50+
51+
let fm = cm.load_file(file_path).expect("failed to load test.ts");
52+
53+
let syntax = if is_ts {
54+
Syntax::Typescript(TsSyntax {
55+
tsx: is_jsx,
56+
dts: is_d_ts,
57+
..Default::default()
58+
})
59+
} else {
60+
Syntax::Es(EsSyntax {
61+
jsx: is_jsx,
62+
..Default::default()
63+
})
64+
};
65+
66+
let lexer = Lexer::new(syntax, Default::default(), StringInput::from(&*fm), None);
67+
68+
let mut parser = Parser::new_from(lexer);
69+
70+
let program = if is_esm {
71+
parser
72+
.parse_module()
73+
.map(Program::Module)
74+
.map_err(|e| e.into_diagnostic(&handler).emit())
75+
} else if is_commonjs {
76+
parser
77+
.parse_commonjs()
78+
.map(Program::Script)
79+
.map_err(|e| e.into_diagnostic(&handler).emit())
80+
} else {
81+
parser
82+
.parse_program()
83+
.map_err(|e| e.into_diagnostic(&handler).emit())
84+
};
85+
86+
for e in parser.take_errors() {
87+
e.into_diagnostic(&handler).emit();
88+
}
89+
90+
let program = program.expect("Failed to parse program.");
91+
92+
eprintln!("Parsed program:\n {program:#?}");
93+
}

crates/swc_ecma_parser/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,4 +205,5 @@ expose!(parse_file_as_expr, Box<Expr>, |p| {
205205
});
206206
expose!(parse_file_as_module, Module, |p| { p.parse_module() });
207207
expose!(parse_file_as_script, Script, |p| { p.parse_script() });
208+
expose!(parse_file_as_commonjs, Script, |p| { p.parse_commonjs() });
208209
expose!(parse_file_as_program, Program, |p| { p.parse_program() });

crates/swc_ecma_parser/src/parser/mod.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,25 @@ impl<I: Tokens> Parser<I> {
131131
})
132132
}
133133

134+
pub fn parse_commonjs(&mut self) -> PResult<Script> {
135+
trace_cur!(self, parse_commonjs);
136+
137+
// CommonJS module is acctually in a function scope
138+
let ctx = (self.ctx() & !Context::Module)
139+
| Context::InFunction
140+
| Context::InsideNonArrowFunctionScope;
141+
self.set_ctx(ctx);
142+
143+
let start = cur_pos!(self);
144+
let shebang = parse_shebang(self)?;
145+
146+
parse_stmt_block_body(self, true, None).map(|body| Script {
147+
span: span!(self, start),
148+
body,
149+
shebang,
150+
})
151+
}
152+
134153
pub fn parse_typescript_module(&mut self) -> PResult<Module> {
135154
trace_cur!(self, parse_typescript_module);
136155

crates/swc_ecma_parser/tests/js.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use crate::common::Normalizer;
1818
mod common;
1919

2020
#[testing::fixture("tests/js/**/*.js")]
21+
#[testing::fixture("tests/js/**/*.cjs")]
2122
fn spec(file: PathBuf) {
2223
let output = file.parent().unwrap().join(format!(
2324
"{}.json",
@@ -28,6 +29,8 @@ fn spec(file: PathBuf) {
2829
}
2930

3031
fn run_spec(file: &Path, output_json: &Path, config_path: &Path) {
32+
let is_commonjs = file.extension().map(|ext| ext == "cjs").unwrap_or_default();
33+
3134
let file_name = file
3235
.display()
3336
.to_string()
@@ -49,7 +52,13 @@ fn run_spec(file: &Path, output_json: &Path, config_path: &Path) {
4952
}
5053

5154
with_parser(false, file, false, config_path, |p, _| {
52-
let program = p.parse_program()?.fold_with(&mut Normalizer {
55+
let program = if is_commonjs {
56+
p.parse_commonjs().map(Program::Script)?
57+
} else {
58+
p.parse_program()?
59+
};
60+
61+
let program = program.fold_with(&mut Normalizer {
5362
drop_span: false,
5463
is_test262: false,
5564
});

0 commit comments

Comments
 (0)