swc-project / swc

Rust-based platform for the Web
https://swc.rs
Apache License 2.0
30.96k stars 1.21k forks source link

[@swc-bundler] Unreachable reached when bundling TS #6946

Closed ccgauche closed 1 year ago

ccgauche commented 1 year ago

When bundling TS, Using this code adapted from the GitHub examples:

use std::{
    collections::HashMap,
    path::Path,
    time::{Duration, Instant},
};

use anyhow::Error;

use swc_atoms::js_word;
use swc_bundler::{Bundle, Bundler, Load, ModuleData, ModuleRecord};
use swc_common::{
    sync::Lrc,
    FileName, Globals, Mark, SourceMap, Span, GLOBALS, FilePathMapping,
};
use swc_ecma_ast::*;
use swc_ecma_codegen::{
    text_writer::{omit_trailing_semi, JsWriter, WriteJs},
    Emitter,
};
use swc_ecma_loader::{
    resolvers::{lru::CachingResolver, node::NodeModulesResolver},
    TargetEnv,
};
use swc_ecma_minifier::option::{
    CompressOptions, ExtraOptions, MangleOptions, MinifyOptions, TopLevelOptions,
};
use swc_ecma_parser::{parse_file_as_module, Syntax, TsConfig};
use swc_ecma_transforms_base::fixer::fixer;
use swc_ecma_visit::VisitMutWith;

use crate::swc2::create_handler;

fn print_bundles(cm: Lrc<SourceMap>, modules: Vec<Bundle>, minify: bool) -> String {
    let mut last= String::new();
    for bundled in modules {
        let code = {
            let mut buf = vec![];
            {
                let wr = JsWriter::new(cm.clone(), "\n", &mut buf, None);
                let mut emitter = Emitter {
                    cfg: swc_ecma_codegen::Config {
                        minify,
                        target: EsVersion::Es2015,
                        ..Default::default()
                    },
                    cm: cm.clone(),
                    comments: None,
                    wr: if minify {
                        Box::new(omit_trailing_semi(wr)) as Box<dyn WriteJs>
                    } else {
                        Box::new(wr) as Box<dyn WriteJs>
                    },
                };

                emitter.emit_module(&bundled.module).unwrap();
            }

            String::from_utf8_lossy(&buf).to_string()
        };

        last = code;
    }
    last
}

fn do_test(_entry: &Path, entries: HashMap<String, FileName>, inline: bool, minify: bool) -> String {
    let cm = Lrc::new(SourceMap::new(FilePathMapping::empty()));
    let start = Instant::now();

    let globals = Box::leak(Box::new(Globals::default()));
    let mut bundler = Bundler::new(
        globals,
        cm.clone(),
        Loader { cm: cm.clone() },
        CachingResolver::new(
            4096,
            NodeModulesResolver::new(TargetEnv::Browser, Default::default(), true),
        ),
        swc_bundler::Config {
            require: false,
            disable_inliner: !inline,
            external_modules: Default::default(),
            disable_fixer: minify,
            disable_hygiene: minify,
            disable_dce: false,
            module: Default::default(),
        },
        Box::new(Hook),
    );

    let mut modules = bundler
        .bundle(entries)
        .map_err(|err| println!("{}", err)).unwrap();
    println!("Bundled as {} modules", modules.len());

    {
        let dur = start.elapsed();
        println!("Bundler.bundle() took {}", to_ms(dur));
    }

    let error = false;
    if minify {
        let start = Instant::now();

        modules = modules
            .into_iter()
            .map(|mut b| {
                GLOBALS.set(globals, || {
                    b.module = swc_ecma_minifier::optimize(
                        b.module.into(),
                        cm.clone(),
                        None,
                        None,
                        &MinifyOptions {
                            compress: Some(CompressOptions {
                                top_level: Some(TopLevelOptions { functions: true }),
                                ..Default::default()
                            }),
                            mangle: Some(MangleOptions {
                                top_level: Some(true),
                                ..Default::default()
                            }),
                            ..Default::default()
                        },
                        &ExtraOptions {
                            unresolved_mark: Mark::new(),
                            top_level_mark: Mark::new(),
                        },
                    )
                    .expect_module();
                    b.module.visit_mut_with(&mut fixer(None));
                    b
                })
            })
            .collect();

        let dur = start.elapsed();
        println!("Minification took {}", to_ms(dur));
    }

    let k = {
        let cm = cm;
        print_bundles(cm, modules, minify)
    };
    if error {
        panic!("Error occured");
    }
    k

}

fn to_ms(dur: Duration) -> String {
    format!("{}ms", dur.as_millis())
}

pub fn main(main_file: &str) -> Result<String, Error> {
    let minify = false;

    let mut entries = HashMap::default();
    entries.insert("main".to_string(), FileName::Real(main_file.into()));

    let start = Instant::now();
    let k = do_test(Path::new(&main_file), entries, false, minify);
    let dur = start.elapsed();
    println!("Took {}", to_ms(dur));

    Ok(k)
}

struct Hook;

impl swc_bundler::Hook for Hook {
    fn get_import_meta_props(
        &self,
        span: Span,
        module_record: &ModuleRecord,
    ) -> Result<Vec<KeyValueProp>, Error> {
        let file_name = module_record.file_name.to_string();

        Ok(vec![
            KeyValueProp {
                key: PropName::Ident(Ident::new(js_word!("url"), span)),
                value: Box::new(Expr::Lit(Lit::Str(Str {
                    span,
                    raw: None,
                    value: file_name.into(),
                }))),
            },
            KeyValueProp {
                key: PropName::Ident(Ident::new(js_word!("main"), span)),
                value: Box::new(if module_record.is_entry {
                    Expr::Member(MemberExpr {
                        span,
                        obj: Box::new(Expr::MetaProp(MetaPropExpr {
                            span,
                            kind: MetaPropKind::ImportMeta,
                        })),
                        prop: MemberProp::Ident(Ident::new(js_word!("main"), span)),
                    })
                } else {
                    Expr::Lit(Lit::Bool(Bool { span, value: false }))
                }),
            },
        ])
    }
}

pub struct Loader {
    pub cm: Lrc<SourceMap>,
}

impl Load for Loader {
    fn load(&self, f: &FileName) -> Result<ModuleData, Error> {
        let fm = match f {
            FileName::Real(path) => self.cm.load_file(path)?,
            e => Err(anyhow::anyhow!("Invalid filename {}",e))?
        };

        let module = parse_file_as_module(
            &fm,
            Syntax::Typescript(TsConfig {
                decorators: true,
                ..Default::default()
            }),
            EsVersion::Es2015,
            None,
            &mut vec![],
        )
        .unwrap_or_else(|err| {
            let handler = create_handler(self.cm.clone());
            err.into_diagnostic(&handler).emit();
            panic!("failed to parse")
        });

        Ok(ModuleData {
            fm,
            module,
            helpers: Default::default(),
        })
    }
}

An unreachable is reached, the fix is quite simple (5 lines), I'll push it and link the PR here. In bundler/export.rs line 168 should be:

                        Decl::TsEnum(ref e) => &e.id,
                        Decl::TsInterface(ref i) => &i.id,
                        Decl::TsTypeAlias(ref a) => &a.id,
                        _ => unreachable!("Decl in ExportDecl: {:?}", decl.decl),

Those two TS files and be used to test the bug:

to_import.ts

export interface A {
}

Entry point

import { A } from './to_import';
swc-bot commented 1 year ago

This closed issue has been automatically locked because it had no new activity for a month. If you are running into a similar issue, please create a new issue with the steps to reproduce. Thank you.