feat(swc): Add IsModule (#2601)

swc:
 - Allow parsing input as a `Program`. (Closes #2541)
This commit is contained in:
Sven 2021-11-16 11:31:02 +01:00 committed by GitHub
parent 6129e990d4
commit 65d376a91b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 202 additions and 92 deletions

View File

@ -46,7 +46,7 @@ Parses javascript and typescript
### [`/crates/swc_ecma_transforms_base`](crates/swc_ecma_transforms_base)
Theres are three core transforms named `resolver`, `hygiene`, `fixer`. Other transforms depends on them.
There are three core transforms named `resolver`, `hygiene`, `fixer`. Other transforms depend on them.
#### [`/crates/swc_ecma_transforms_base/src/resolver`](crates/swc_ecma_transforms_base/src/resolver)
@ -70,11 +70,11 @@ let a#0 = 1;
}
```
where number after `#` denotes the hygiene id. If two identifiers have same symbol but different hygiene id, it's different.
where the number after the hash (`#`) denotes the hygiene id. If two identifiers have the same symbol but different hygiene ids, they are considered different.
#### [`/crates/swc_ecma_transforms_base/src/hygiene`](crates/swc_ecma_transforms_base/src/hygiene)
Hygiene pass actually changes symbol of identifiers with same symbol but different hygiene id.
The hygiene pass actually changes symbols of identifiers with the same symbol but different hygiene ids to different symbols.
```js
let a#0 = 1;
@ -106,7 +106,7 @@ let v = BinExpr {
};
```
(other passes generates AST like this)
(other passes generate AST like this)
is converted into
@ -127,7 +127,7 @@ and printed as
<!-- TODO: add correct references to files -->
<!-- #### `/ecmascript/transforms/src/compat`
Contains code related to converting new generation javascript codes for old browsers.
Contains code related to converting new generation javascript code into code understood by old browsers.
#### `/ecmascript/transforms/src/modules`
@ -135,15 +135,15 @@ Contains code related to transforming es6 modules to other modules.
#### `/ecmascript/transforms/src/optimization`
Contains code related to making code faster on runtime. Currently only small set of optimization is implemented. -->
Contains code related to making code faster on runtime. Currently only a small set of optimizations is implemented. -->
## Tests
SWC uses the [official ecmascript conformance test suite called test262][test262] for testing.
Parser tests ensures that parsed result of `test262/pass` is identical with `test262/pass-explicit`.
Parser tests ensure that the parsed results of `test262/pass` are identical with `test262/pass-explicit`.
Codegen tests ensures that generated code is equivalent to golden fixture files located at [tests/references](crates/swc_ecma_codegen/tests).
Codegen tests ensure that the generated code is equivalent to the golden fixture files located at [tests/references](crates/swc_ecma_codegen/tests).
[enum_kind]: https://rustdoc.swc.rs/enum_kind/derive.Kind.html
[string_enum]: https://rustdoc.swc.rs/string_enum/derive.StringEnum.html

View File

@ -76,8 +76,8 @@ After cloning the project there are a few steps required to get the project runn
2. Install js dependencies.
```bash
yarn add browserslist
( cd ecmascript/transforms; yarn install )
yarn
( cd ecmascript/transforms; yarn )
```
3. Setup some environment variables which is required for tests.
@ -85,13 +85,18 @@ After cloning the project there are a few steps required to get the project runn
```bash
export RUST_BACKTRACE=full
export PATH="$PATH:$PWD/ecmascript/transforms/node_modules/.bin"
export RUST_MIN_STACK=16777216
```
4. Install deno, if you are going to work on the bundler.
See [official install guide of deno](https://deno.land/manual/getting_started/installation) to install it.
5. Run tests
5. Ensure you're using Node.JS >= 16
Since tests make use of `atob` which was only introduced in node 16.
6. Run tests
```bash
cargo test --all --all-features

View File

@ -5,7 +5,7 @@ extern crate swc_node_base;
extern crate test;
use std::{fs::read_to_string, io::stderr, path::Path};
use swc::config::Options;
use swc::config::{IsModule, Options};
use swc_common::{errors::Handler, sync::Lrc, FilePathMapping, SourceMap};
use test::Bencher;
@ -32,7 +32,7 @@ fn bench_file(b: &mut Bencher, path: &Path) {
fm.clone(),
&handler,
&Options {
is_module: true,
is_module: IsModule::Bool(true),
..Default::default()
},
)

View File

@ -9,7 +9,7 @@ use std::{
io::{self, stderr},
sync::Arc,
};
use swc::config::{Config, JscConfig, Options, SourceMapsConfig};
use swc::config::{Config, IsModule, JscConfig, Options, SourceMapsConfig};
use swc_common::{errors::Handler, FileName, FilePathMapping, SourceFile, SourceMap};
use swc_ecma_ast::{EsVersion, Program};
use swc_ecma_parser::{Syntax, TsConfig};
@ -41,7 +41,7 @@ fn parse(c: &swc::Compiler) -> (Arc<SourceFile>, Program) {
&handler,
EsVersion::Es5,
Syntax::Typescript(Default::default()),
true,
IsModule::Bool(true),
true,
)
.unwrap(),
@ -165,7 +165,7 @@ macro_rules! compat {
..Default::default()
},
swcrc: false,
is_module: true,
is_module: IsModule::Bool(true),
..Default::default()
},
);

View File

@ -6,11 +6,14 @@ use either::Either;
use indexmap::IndexMap;
use once_cell::sync::Lazy;
use regex::Regex;
use serde::{Deserialize, Serialize};
use serde::{
de::{Unexpected, Visitor},
Deserialize, Deserializer, Serialize, Serializer,
};
use std::{
cell::RefCell,
collections::{HashMap, HashSet},
env,
env, fmt,
hash::BuildHasher,
path::{Path, PathBuf},
rc::Rc as RustRc,
@ -54,6 +57,66 @@ use swc_ecma_visit::Fold;
mod tests;
pub mod util;
#[derive(Clone, Debug, Copy)]
pub enum IsModule {
Bool(bool),
Unknown,
}
impl Default for IsModule {
fn default() -> Self {
IsModule::Bool(true)
}
}
impl Serialize for IsModule {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match *self {
IsModule::Bool(ref b) => b.serialize(serializer),
IsModule::Unknown => "unknown".serialize(serializer),
}
}
}
impl<'de> Deserialize<'de> for IsModule {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct IsModuleVisitor;
impl<'de> Visitor<'de> for IsModuleVisitor {
type Value = IsModule;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a boolean or the string 'unknown'")
}
fn visit_bool<E>(self, b: bool) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
return Ok(IsModule::Bool(b));
}
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
match s {
"unknown" => Ok(IsModule::Unknown),
_ => Err(serde::de::Error::invalid_value(Unexpected::Str(s), &self)),
}
}
}
deserializer.deserialize_any(IsModuleVisitor)
}
}
#[derive(Default, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ParseOptions {
@ -62,8 +125,8 @@ pub struct ParseOptions {
#[serde(flatten)]
pub syntax: Syntax,
#[serde(default = "default_is_module")]
pub is_module: bool,
#[serde(default)]
pub is_module: IsModule,
#[serde(default)]
pub target: EsVersion,
@ -125,8 +188,8 @@ pub struct Options {
#[serde(default)]
pub source_root: Option<String>,
#[serde(default = "default_is_module")]
pub is_module: bool,
#[serde(default)]
pub is_module: IsModule,
#[serde(default)]
pub output_path: Option<PathBuf>,
@ -138,10 +201,6 @@ impl Options {
}
}
fn default_is_module() -> bool {
true
}
/// Configuration related to source map generated by swc.
#[derive(Clone, Serialize, Deserialize, Debug)]
#[serde(untagged)]
@ -187,11 +246,11 @@ impl Options {
&self,
cm: &Arc<SourceMap>,
base: &FileName,
parse: impl FnOnce(Syntax, EsVersion, bool) -> Result<Program, Error>,
parse: impl FnOnce(Syntax, EsVersion, IsModule) -> Result<Program, Error>,
output_path: Option<&Path>,
source_file_name: Option<String>,
handler: &Handler,
is_module: bool,
is_module: IsModule,
config: Option<Config>,
comments: Option<&'a SwcComments>,
custom_before_pass: impl FnOnce(&Program) -> P,
@ -813,7 +872,7 @@ pub struct BuiltInput<P: swc_ecma_visit::Fold> {
pub external_helpers: bool,
pub source_maps: SourceMapsConfig,
pub input_source_map: InputSourceMap,
pub is_module: bool,
pub is_module: IsModule,
pub output_path: Option<PathBuf>,
pub source_file_name: Option<String>,

View File

@ -120,7 +120,7 @@ use common::{
collections::AHashMap,
errors::{EmitterWriter, HANDLER},
};
use config::{util::BoolOrObject, JsMinifyCommentOption, JsMinifyOptions};
use config::{util::BoolOrObject, IsModule, JsMinifyCommentOption, JsMinifyOptions};
use dashmap::DashMap;
use once_cell::sync::Lazy;
use serde::Serialize;
@ -399,7 +399,7 @@ impl Compiler {
handler: &Handler,
target: EsVersion,
syntax: Syntax,
is_module: bool,
is_module: IsModule,
parse_comments: bool,
) -> Result<Program, Error> {
self.run(|| {
@ -415,34 +415,23 @@ impl Compiler {
);
let mut parser = Parser::new_from(lexer);
let mut error = false;
let program = if is_module {
let m = parser.parse_module();
for e in parser.take_errors() {
e.into_diagnostic(handler).emit();
error = true;
}
m.map_err(|e| {
e.into_diagnostic(handler).emit();
Error::msg("Syntax Error")
})
.map(Program::Module)?
} else {
let s = parser.parse_script();
for e in parser.take_errors() {
e.into_diagnostic(handler).emit();
error = true;
}
s.map_err(|e| {
e.into_diagnostic(handler).emit();
Error::msg("Syntax Error")
})
.map(Program::Script)?
let program_result = match is_module {
IsModule::Bool(true) => parser.parse_module().map(Program::Module),
IsModule::Bool(false) => parser.parse_script().map(Program::Script),
IsModule::Unknown => parser.parse_program(),
};
for e in parser.take_errors() {
e.into_diagnostic(handler).emit();
error = true;
}
let program = program_result.map_err(|e| {
e.into_diagnostic(handler).emit();
Error::msg("Syntax Error")
})?;
if error {
return Err(anyhow::anyhow!("Syntax Error").context(
"error was recoverable, but proceeding would result in wrong codegen",
@ -982,7 +971,7 @@ impl Compiler {
..Default::default()
}),
true,
IsModule::Bool(true),
true,
)
.context("failed to parse input file")?

View File

@ -1,5 +1,8 @@
use std::path::Path;
use swc::{config::Options, Compiler};
use swc::{
config::{IsModule, Options},
Compiler,
};
use testing::{NormalizedOutput, Tester};
fn file(f: impl AsRef<Path>) -> NormalizedOutput {
@ -13,7 +16,7 @@ fn file(f: impl AsRef<Path>) -> NormalizedOutput {
&handler,
&Options {
swcrc: true,
is_module: true,
is_module: IsModule::Bool(true),
..Default::default()
},
);

View File

@ -7,7 +7,8 @@ use std::{
};
use swc::{
config::{
BuiltInput, Config, JscConfig, ModuleConfig, Options, SourceMapsConfig, TransformConfig,
BuiltInput, Config, IsModule, JscConfig, ModuleConfig, Options, SourceMapsConfig,
TransformConfig,
},
Compiler, TransformOutput,
};
@ -44,7 +45,7 @@ fn file_with_opt(filename: &str, options: Options) -> Result<NormalizedOutput, S
fm,
&handler,
&Options {
is_module: true,
is_module: IsModule::Bool(true),
..options
},
);
@ -79,7 +80,7 @@ fn compile_str(
fm,
&handler,
&Options {
is_module: true,
is_module: IsModule::Bool(true),
..options
},
);
@ -121,7 +122,7 @@ fn project(dir: &str) {
if c.read_config(
&Options {
swcrc: true,
is_module: true,
is_module: IsModule::Bool(true),
..Default::default()
},
@ -138,7 +139,7 @@ fn project(dir: &str) {
&handler,
&Options {
swcrc: true,
is_module: true,
is_module: IsModule::Bool(true),
..Default::default()
},
@ -189,7 +190,7 @@ fn par_project(dir: &str) {
&handler,
&Options {
swcrc: true,
is_module: true,
is_module: IsModule::Bool(true),
source_maps: Some(SourceMapsConfig::Bool(true)),
..Default::default()
},
@ -540,7 +541,7 @@ fn issue_879() {
let f = file_with_opt(
"tests/projects/issue-879/input.ts",
Options {
is_module: true,
is_module: IsModule::Bool(true),
config: Config {
env: Some(Default::default()),
module: Some(ModuleConfig::CommonJs(Default::default())),
@ -614,7 +615,7 @@ fn issue_1549() {
let output = str_with_opt(
"const a = `\r\n`;",
Options {
is_module: true,
is_module: IsModule::Bool(true),
config: Config {
jsc: JscConfig {
target: Some(EsVersion::Es5),
@ -636,7 +637,7 @@ fn deno_10282_1() {
let output = str_with_opt(
"const a = `\r\n`;",
Options {
is_module: true,
is_module: IsModule::Bool(true),
config: Config {
jsc: JscConfig {
target: Some(EsVersion::Es3),
@ -658,7 +659,7 @@ fn deno_10282_2() {
let output = str_with_opt(
"const a = `\r\n`;",
Options {
is_module: true,
is_module: IsModule::Bool(true),
config: Config {
jsc: JscConfig {
target: Some(EsVersion::Es2020),
@ -711,7 +712,7 @@ fn should_visit() {
None,
&handler,
&swc::config::Options {
is_module: true,
is_module: IsModule::Bool(true),
config: swc::config::Config {
jsc: JscConfig {
syntax: Some(Syntax::Es(EsConfig {
@ -818,7 +819,7 @@ fn tests(input_dir: PathBuf) {
&handler,
&Options {
swcrc: true,
is_module: true,
is_module: IsModule::Bool(true),
output_path: Some(output.join(entry.file_name())),
..Default::default()

View File

@ -1,5 +1,7 @@
use swc::{
config::{Config, InputSourceMap, JscConfig, ModuleConfig, Options, SourceMapsConfig},
config::{
Config, InputSourceMap, IsModule, JscConfig, ModuleConfig, Options, SourceMapsConfig,
},
Compiler,
};
use swc_common::FileName;
@ -96,7 +98,7 @@ fn shopify_1_check_filename() {
})),
..Default::default()
},
is_module: true,
is_module: IsModule::Bool(true),
..Default::default()
},
|_| noop(),
@ -161,7 +163,7 @@ fn shopify_2_same_opt() {
env_name: "development".into(),
source_maps: Some(SourceMapsConfig::Bool(false)),
source_file_name: Some("/Users/kdy1/projects/example-swcify/src/App/App.tsx".into()),
is_module: true,
is_module: IsModule::Bool(true),
..Default::default()
};
@ -223,7 +225,7 @@ fn shopify_3_reduce_defaults() {
env_name: "development".into(),
source_maps: Some(SourceMapsConfig::Bool(false)),
source_file_name: Some("/Users/kdy1/projects/example-swcify/src/App/App.tsx".into()),
is_module: true,
is_module: IsModule::Bool(true),
..Default::default()
};
@ -279,7 +281,7 @@ fn shopify_4_reduce_more() {
env_name: "development".into(),
source_maps: Some(SourceMapsConfig::Bool(false)),
source_file_name: Some("/Users/kdy1/projects/example-swcify/src/App/App.tsx".into()),
is_module: true,
is_module: IsModule::Bool(true),
..Default::default()
};

View File

@ -1,5 +1,5 @@
use swc::{
config::{Config, JscConfig, Options},
config::{Config, IsModule, JscConfig, Options},
Compiler,
};
use swc_common::FileName;
@ -17,7 +17,7 @@ fn compile(src: &str, options: Options) -> String {
fm,
&handler,
&Options {
is_module: true,
is_module: IsModule::Bool(true),
..options
},
);
@ -147,3 +147,37 @@ fn test_tsx_escape_xhtml() {
assert_eq!(compiled_es2020, expected);
}
#[test]
fn is_module_unknown_script() {
let source = "module.exports = foo = 2n + 7n;";
let expected = "module.exports = foo = 2n + 7n;\n";
let compiled = compile(
source,
Options {
swcrc: false,
is_module: IsModule::Unknown,
..Default::default()
},
);
assert_eq!(compiled, expected);
}
#[test]
fn is_module_unknown_module() {
let source = "export var foo = 2n + 7n;";
let expected = "export var foo = 2n + 7n;\n";
let compiled = compile(
source,
Options {
swcrc: false,
is_module: IsModule::Unknown,
..Default::default()
},
);
assert_eq!(compiled, expected);
}

View File

@ -10,7 +10,7 @@ use std::{
sync::Arc,
};
use swc::{
config::{Config, Options, SourceMapsConfig},
config::{Config, IsModule, Options, SourceMapsConfig},
Compiler,
};
use testing::{assert_eq, NormalizedOutput, StdErr, Tester};
@ -33,7 +33,7 @@ fn file(f: &str) -> Result<(), StdErr> {
..Default::default()
},
swcrc: true,
is_module: true,
is_module: IsModule::Bool(true),
source_maps: Some(SourceMapsConfig::Bool(true)),
..Default::default()
},
@ -88,7 +88,7 @@ fn inline(f: &str) -> Result<(), StdErr> {
..Default::default()
},
swcrc: true,
is_module: true,
is_module: IsModule::Bool(true),
source_maps: Some(SourceMapsConfig::Str(String::from("inline"))),
..Default::default()
},
@ -153,7 +153,7 @@ fn stacktrace(input_dir: PathBuf) {
&handler,
&Options {
swcrc: true,
is_module: true,
is_module: IsModule::Bool(true),
source_maps: Some(SourceMapsConfig::Str("inline".to_string())),
..Default::default()
},

View File

@ -6,7 +6,7 @@ use std::{
path::{Path, PathBuf},
};
use swc::{
config::{Config, JscConfig, Options},
config::{Config, IsModule, JscConfig, Options},
Compiler,
};
use swc_ecma_ast::EsVersion;
@ -113,7 +113,7 @@ fn compile(input: &Path, output: &Path, opts: Options) {
fm,
&handler,
&Options {
is_module: true,
is_module: IsModule::Bool(true),
config: Config {
jsc: JscConfig {

View File

@ -4,6 +4,7 @@
extern crate test;
use std::{hint::black_box, io::stderr, sync::Arc};
use swc::config::IsModule;
use swc_babel_compat::babelify::{Babelify, Context};
use swc_common::{errors::Handler, FileName, FilePathMapping, SourceFile, SourceMap};
use swc_ecma_ast::{EsVersion, Program};
@ -36,7 +37,7 @@ fn parse(c: &swc::Compiler, src: &str) -> (Arc<SourceFile>, Program) {
&handler,
EsVersion::Es5,
Syntax::Typescript(Default::default()),
true,
IsModule::Bool(true),
true,
)
.unwrap();

View File

@ -9,7 +9,7 @@ use std::{
path::{Path, PathBuf},
sync::Arc,
};
use swc::Compiler;
use swc::{config::IsModule, Compiler};
use swc_babel_ast::File;
use swc_babel_compat::babelify::{normalize::normalize, Babelify, Context};
use swc_common::{
@ -136,7 +136,7 @@ fn run_test(src: String, expected: String, syntax: Syntax, is_module: bool) {
&handler,
Default::default(), // EsVersion (ES version)
syntax,
is_module,
IsModule::Bool(is_module),
true, // parse_conmments
)
.unwrap();

View File

@ -3,7 +3,7 @@ use anyhow::{bail, Context, Error};
use helpers::Helpers;
use std::{collections::HashMap, env, sync::Arc};
use swc::{
config::{GlobalInliningPassEnvs, InputSourceMap, JscConfig, TransformConfig},
config::{GlobalInliningPassEnvs, InputSourceMap, IsModule, JscConfig, TransformConfig},
try_with_handler,
};
use swc_atoms::JsWord;
@ -159,7 +159,7 @@ impl SwcLoader {
&handler,
EsVersion::Es2020,
Default::default(),
true,
IsModule::Bool(true),
true,
)?;
let program = helpers::HELPERS.set(&helpers, || {
@ -230,7 +230,7 @@ impl SwcLoader {
source_maps: None,
source_file_name: None,
source_root: None,
is_module: true,
is_module: IsModule::Bool(true),
output_path: None,
..Default::default()
},
@ -269,7 +269,7 @@ impl SwcLoader {
handler,
EsVersion::Es2020,
config.as_ref().map(|v| v.syntax).unwrap_or_default(),
true,
IsModule::Bool(true),
true,
)
.context("tried to parse as ecmascript as it's excluded by .swcrc")?

View File

@ -0,0 +1,16 @@
const swc = require("../../");
it("should detect script", () => {
const script = swc.parseSync(`const fs = require('fs');`, { isModule: "unknown" });
expect(script.type).toBe("Script");
});
it("should default to isModule: true", () => {
const script = swc.parseSync(`foo;`, {});
expect(script.type).toBe("Module");
});
it("should detect module", () => {
const script = swc.parseSync(`import fs from "fs";`, { isModule: "unknown" });
expect(script.type).toBe("Module");
});

View File

@ -380,7 +380,7 @@ export interface Options extends Config {
plugin?: Plugin;
isModule?: boolean;
isModule?: boolean | 'unknown';
/**
* Destination path. Note that this value is used only to fix source path