swc/bundler/tests/fixture.rs
강동윤 b2aec35eb6
Fix issues (#1189)
swc_bundler:
 - Support emitting iife. (Closes #1184)

swc_ecma_parser:
 - Auto-detect script / module (Closes #1164)
 - lexer: Error recovery for strict mode and module mode.
 - More error recovery logic for strict mode violation.
 - Fix panic on invalid input. (Closes #1170)
2020-10-28 21:20:11 +09:00

415 lines
13 KiB
Rust

#![feature(test)]
extern crate test;
use anyhow::{bail, Context, Error};
use std::{
self,
collections::HashMap,
env,
fs::{create_dir_all, read_dir},
io,
path::{Path, PathBuf},
};
use swc_bundler::{BundleKind, Bundler, Config, Load, Resolve};
use swc_common::{sync::Lrc, FileName, Globals, SourceFile, SourceMap, Span};
use swc_ecma_ast::{Expr, Lit, Module, Str};
use swc_ecma_codegen::{text_writer::JsWriter, Emitter};
use swc_ecma_parser::{lexer::Lexer, JscTarget, Parser, StringInput, Syntax, TsConfig};
use swc_ecma_transforms::{fixer, typescript::strip};
use swc_ecma_visit::FoldWith;
use test::{
test_main, DynTestFn, Options, ShouldPanic::No, TestDesc, TestDescAndFn, TestName, TestType,
};
use testing::NormalizedOutput;
use walkdir::WalkDir;
fn add_test<F: FnOnce() + Send + 'static>(
tests: &mut Vec<TestDescAndFn>,
name: String,
ignore: bool,
f: F,
) {
tests.push(TestDescAndFn {
desc: TestDesc {
test_type: TestType::UnitTest,
name: TestName::DynTestName(name.replace("-", "_").replace("/", "::")),
ignore,
should_panic: No,
allow_fail: false,
},
testfn: DynTestFn(Box::new(f)),
});
}
fn reference_tests(tests: &mut Vec<TestDescAndFn>, errors: bool) -> Result<(), io::Error> {
let root = {
let mut root = Path::new(env!("CARGO_MANIFEST_DIR")).to_path_buf();
root.push("tests");
root.push(if errors { "error" } else { "fixture" });
root
};
eprintln!("Loading tests from {}", root.display());
let dir = root;
for entry in WalkDir::new(&dir).into_iter() {
let entry = entry?;
if !entry.path().join("input").exists() {
continue;
}
let ignore = entry
.path()
.file_name()
.unwrap()
.to_string_lossy()
.starts_with(".");
let dir_name = entry
.path()
.strip_prefix(&dir)
.expect("failed to strip prefix")
.to_str()
.unwrap()
.to_string();
let _ = create_dir_all(entry.path().join("output"));
let entries = read_dir(entry.path().join("input"))?
.filter(|e| match e {
Ok(e) => {
if e.path()
.file_name()
.unwrap()
.to_string_lossy()
.starts_with("entry")
{
true
} else {
false
}
}
_ => false,
})
.map(|e| -> Result<_, io::Error> {
let e = e?;
Ok((
e.file_name().to_string_lossy().to_string(),
FileName::Real(e.path()),
))
})
.collect::<Result<HashMap<_, _>, _>>()?;
let name = format!(
"fixture::{}::{}",
if errors { "error" } else { "pass" },
dir_name
);
let ignore = ignore
|| !name.contains(
&env::var("TEST")
.ok()
.unwrap_or("".into())
.replace("::", "/")
.replace("_", "-"),
);
add_test(tests, name, ignore, move || {
eprintln!("\n\n========== Running reference test {}\n", dir_name);
testing::run_test2(false, |cm, _| {
let globals = Globals::default();
let bundler = Bundler::new(
&globals,
cm.clone(),
Loader { cm: cm.clone() },
NodeResolver::new(),
Config {
require: true,
disable_inliner: true,
external_modules: vec![
"assert",
"buffer",
"child_process",
"console",
"cluster",
"crypto",
"dgram",
"dns",
"events",
"fs",
"http",
"http2",
"https",
"net",
"os",
"path",
"perf_hooks",
"process",
"querystring",
"readline",
"repl",
"stream",
"string_decoder",
"timers",
"tls",
"tty",
"url",
"util",
"v8",
"vm",
"wasi",
"worker",
"zlib",
]
.into_iter()
.map(From::from)
.collect(),
module: Default::default(),
},
Box::new(Hook),
);
let modules = bundler
.bundle(entries)
.map_err(|err| println!("{:?}", err))?;
println!("Bundled as {} modules", modules.len());
let mut error = false;
for bundled in modules {
let code = {
let mut buf = vec![];
{
let mut emitter = Emitter {
cfg: swc_ecma_codegen::Config {
..Default::default()
},
cm: cm.clone(),
comments: None,
wr: Box::new(JsWriter::new(cm.clone(), "\n", &mut buf, None)),
};
emitter
.emit_module(&bundled.module.fold_with(&mut fixer(None)))
.unwrap();
}
String::from_utf8_lossy(&buf).to_string()
};
let name = match bundled.kind {
BundleKind::Named { name } | BundleKind::Lib { name } => {
PathBuf::from(name)
}
BundleKind::Dynamic => format!("dynamic.{}.js", bundled.id).into(),
};
let output_path = entry.path().join("output").join(name.file_name().unwrap());
println!("Printing {}", output_path.display());
let s = NormalizedOutput::from(code.to_string());
match s.compare_to_file(&output_path) {
Ok(_) => {}
Err(err) => {
println!("Diff: {:?}", err);
error = true;
}
}
}
if error {
return Err(());
}
Ok(())
})
.expect("failed to process a module");
});
}
Ok(())
}
#[test]
fn pass() {
let args: Vec<_> = env::args().collect();
let mut tests = Vec::new();
reference_tests(&mut tests, false).unwrap();
test_main(&args, tests, Some(Options::new()));
}
struct Hook;
impl swc_bundler::Hook for Hook {
fn get_import_meta_url(&self, span: Span, file: &FileName) -> Result<Option<Expr>, Error> {
Ok(Some(Expr::Lit(Lit::Str(Str {
span,
value: file.to_string().into(),
has_escape: false,
}))))
}
}
pub struct Loader {
cm: Lrc<SourceMap>,
}
impl Load for Loader {
fn load(&self, f: &FileName) -> Result<(Lrc<SourceFile>, Module), Error> {
eprintln!("load: {}", f);
let fm = self.cm.load_file(match f {
FileName::Real(v) => v,
_ => unreachable!(),
})?;
let lexer = Lexer::new(
Syntax::Typescript(TsConfig {
decorators: true,
..Default::default()
}),
JscTarget::Es2020,
StringInput::from(&*fm),
None,
);
let mut parser = Parser::new_from(lexer);
let module = parser.parse_module().unwrap();
let module = module.fold_with(&mut strip());
Ok((fm, module))
}
}
pub struct NodeResolver;
static EXTENSIONS: &[&str] = &["ts", "tsx", "js", "jsx", "json", "node"];
impl NodeResolver {
pub fn new() -> Self {
Self
}
fn wrap(&self, path: PathBuf) -> Result<FileName, Error> {
Ok(FileName::Real(
path.canonicalize().context("failaed to canonicalize")?,
))
}
/// Resolve a path as a file. If `path` refers to a file, it is
/// returned; otherwise the `path` + each extension is tried.
fn resolve_as_file(&self, path: &Path) -> Result<PathBuf, Error> {
// 1. If X is a file, load X as JavaScript text.
if path.is_file() {
return Ok(path.to_path_buf());
}
for ext in EXTENSIONS {
let ext_path = path.with_extension(ext);
if ext_path.is_file() {
return Ok(ext_path);
}
}
bail!("file not found: {}", path.display())
}
/// Resolve a path as a directory, using the "main" key from a
/// package.json file if it exists, or resolving to the
/// index.EXT file if it exists.
fn resolve_as_directory(&self, path: &PathBuf) -> Result<PathBuf, Error> {
// 1. If X/package.json is a file, use it.
let pkg_path = path.join("package.json");
if pkg_path.is_file() {
let main = self.resolve_package_main(&pkg_path);
if main.is_ok() {
return main;
}
}
// 2. LOAD_INDEX(X)
self.resolve_index(path)
}
/// Resolve using the package.json "main" key.
fn resolve_package_main(&self, _: &PathBuf) -> Result<PathBuf, Error> {
bail!("package.json is not supported")
}
/// Resolve a directory to its index.EXT.
fn resolve_index(&self, path: &PathBuf) -> Result<PathBuf, Error> {
// 1. If X/index.js is a file, load X/index.js as JavaScript text.
// 2. If X/index.json is a file, parse X/index.json to a JavaScript object.
// 3. If X/index.node is a file, load X/index.node as binary addon.
for ext in EXTENSIONS {
let ext_path = path.join(format!("index.{}", ext));
if ext_path.is_file() {
return Ok(ext_path);
}
}
bail!("index not found: {}", path.display())
}
/// Resolve by walking up node_modules folders.
fn resolve_node_modules(&self, base_dir: &Path, target: &str) -> Result<PathBuf, Error> {
let node_modules = base_dir.join("node_modules");
if node_modules.is_dir() {
let path = node_modules.join(target);
let result = self
.resolve_as_file(&path)
.or_else(|_| self.resolve_as_directory(&path));
if result.is_ok() {
return result;
}
}
match base_dir.parent() {
Some(parent) => self.resolve_node_modules(parent, target),
None => bail!("not found"),
}
}
}
impl Resolve for NodeResolver {
fn resolve(&self, base: &FileName, target: &str) -> Result<FileName, Error> {
let base = match base {
FileName::Real(v) => v,
_ => bail!("node-resolver supports only files"),
};
// Absolute path
if target.starts_with("/") {
let base_dir = &Path::new("/");
let path = base_dir.join(target);
return self
.resolve_as_file(&path)
.or_else(|_| self.resolve_as_directory(&path))
.and_then(|p| self.wrap(p));
}
let cwd = &Path::new(".");
let base_dir = base.parent().unwrap_or(&cwd);
if target.starts_with("./") || target.starts_with("../") {
let path = base_dir.join(target);
return self
.resolve_as_file(&path)
.or_else(|_| self.resolve_as_directory(&path))
.and_then(|p| self.wrap(p));
}
self.resolve_node_modules(base_dir, target)
.and_then(|p| self.wrap(p))
}
}