mirror of
https://github.com/denoland/deno.git
synced 2024-11-29 16:30:56 -05:00
8ae17026cb
This commit adds new "deno check" subcommand. Currently it is an alias for "deno cache" with the difference that remote modules don't emit TS diagnostics by default. Prints warning for "deno run" subcommand if "--check" flag is not present and there's no "--no-check" flag. Adds "DENO_FUTURE_CHECK" env variable that allows to opt into new behavior now.
1076 lines
33 KiB
Rust
1076 lines
33 KiB
Rust
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
|
|
|
|
//! The collection of APIs to be able to take `deno_graph` module graphs and
|
|
//! populate a cache, emit files, and transform a graph into the structures for
|
|
//! loading into an isolate.
|
|
|
|
use crate::cache::CacheType;
|
|
use crate::cache::Cacher;
|
|
use crate::colors;
|
|
use crate::config_file;
|
|
use crate::config_file::ConfigFile;
|
|
use crate::config_file::IgnoredCompilerOptions;
|
|
use crate::config_file::TsConfig;
|
|
use crate::diagnostics::Diagnostics;
|
|
use crate::flags;
|
|
use crate::graph_util::GraphData;
|
|
use crate::graph_util::ModuleEntry;
|
|
use crate::text_encoding::strip_bom;
|
|
use crate::tsc;
|
|
use crate::version;
|
|
|
|
use deno_ast::get_syntax;
|
|
use deno_ast::swc;
|
|
use deno_ast::swc::bundler::Hook;
|
|
use deno_ast::swc::bundler::ModuleRecord;
|
|
use deno_ast::swc::common::comments::SingleThreadedComments;
|
|
use deno_ast::swc::common::FileName;
|
|
use deno_ast::swc::common::Mark;
|
|
use deno_ast::swc::common::SourceMap;
|
|
use deno_ast::swc::common::Span;
|
|
use deno_ast::swc::common::Spanned;
|
|
use deno_ast::swc::parser::error::Error as SwcError;
|
|
use deno_ast::swc::parser::lexer::Lexer;
|
|
use deno_ast::swc::parser::StringInput;
|
|
use deno_ast::Diagnostic;
|
|
use deno_ast::LineAndColumnDisplay;
|
|
use deno_core::anyhow::anyhow;
|
|
use deno_core::anyhow::Context;
|
|
use deno_core::error::AnyError;
|
|
use deno_core::parking_lot::RwLock;
|
|
use deno_core::serde::Deserialize;
|
|
use deno_core::serde::Deserializer;
|
|
use deno_core::serde::Serialize;
|
|
use deno_core::serde::Serializer;
|
|
use deno_core::serde_json;
|
|
use deno_core::serde_json::json;
|
|
use deno_core::serde_json::Value;
|
|
use deno_core::ModuleSpecifier;
|
|
use deno_graph::MediaType;
|
|
use deno_graph::ModuleGraph;
|
|
use deno_graph::ModuleGraphError;
|
|
use deno_graph::ModuleKind;
|
|
use deno_graph::ResolutionError;
|
|
use std::collections::HashMap;
|
|
use std::collections::HashSet;
|
|
use std::fmt;
|
|
use std::rc::Rc;
|
|
use std::result;
|
|
use std::sync::Arc;
|
|
use std::time::Instant;
|
|
|
|
const IGNORE_DIRECTIVES: &[&str] = &[
|
|
"// deno-fmt-ignore-file",
|
|
"// deno-lint-ignore-file",
|
|
"// This code was bundled using `deno bundle` and it's not recommended to edit it manually",
|
|
""
|
|
];
|
|
|
|
/// Represents the "default" type library that should be used when type
|
|
/// checking the code in the module graph. Note that a user provided config
|
|
/// of `"lib"` would override this value.
|
|
#[derive(Debug, Clone, Eq, Hash, PartialEq)]
|
|
pub enum TypeLib {
|
|
DenoWindow,
|
|
DenoWorker,
|
|
UnstableDenoWindow,
|
|
UnstableDenoWorker,
|
|
}
|
|
|
|
impl Default for TypeLib {
|
|
fn default() -> Self {
|
|
Self::DenoWindow
|
|
}
|
|
}
|
|
|
|
impl Serialize for TypeLib {
|
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
where
|
|
S: Serializer,
|
|
{
|
|
let value = match self {
|
|
Self::DenoWindow => vec!["deno.window".to_string()],
|
|
Self::DenoWorker => vec!["deno.worker".to_string()],
|
|
Self::UnstableDenoWindow => {
|
|
vec!["deno.window".to_string(), "deno.unstable".to_string()]
|
|
}
|
|
Self::UnstableDenoWorker => {
|
|
vec!["deno.worker".to_string(), "deno.unstable".to_string()]
|
|
}
|
|
};
|
|
Serialize::serialize(&value, serializer)
|
|
}
|
|
}
|
|
|
|
/// A structure representing stats from an emit operation for a graph.
|
|
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
|
pub struct Stats(pub Vec<(String, u32)>);
|
|
|
|
impl<'de> Deserialize<'de> for Stats {
|
|
fn deserialize<D>(deserializer: D) -> result::Result<Self, D::Error>
|
|
where
|
|
D: Deserializer<'de>,
|
|
{
|
|
let items: Vec<(String, u32)> = Deserialize::deserialize(deserializer)?;
|
|
Ok(Stats(items))
|
|
}
|
|
}
|
|
|
|
impl Serialize for Stats {
|
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
where
|
|
S: Serializer,
|
|
{
|
|
Serialize::serialize(&self.0, serializer)
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for Stats {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
writeln!(f, "Compilation statistics:")?;
|
|
for (key, value) in self.0.clone() {
|
|
writeln!(f, " {}: {}", key, value)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// An enum that represents the base tsc configuration to return.
|
|
pub enum ConfigType {
|
|
/// Return a configuration for bundling, using swc to emit the bundle. This is
|
|
/// independent of type checking.
|
|
Bundle,
|
|
/// Return a configuration to use tsc to type check and optionally emit. This
|
|
/// is independent of either bundling or just emitting via swc
|
|
Check { lib: TypeLib, tsc_emit: bool },
|
|
/// Return a configuration to use swc to emit single module files.
|
|
Emit,
|
|
/// Return a configuration as a base for the runtime `Deno.emit()` API.
|
|
RuntimeEmit { tsc_emit: bool },
|
|
}
|
|
|
|
/// For a given configuration type and optionally a configuration file, return a
|
|
/// tuple of the resulting `TsConfig` struct and optionally any user
|
|
/// configuration options that were ignored.
|
|
pub fn get_ts_config(
|
|
config_type: ConfigType,
|
|
maybe_config_file: Option<&ConfigFile>,
|
|
maybe_user_config: Option<&HashMap<String, Value>>,
|
|
) -> Result<(TsConfig, Option<IgnoredCompilerOptions>), AnyError> {
|
|
let mut ts_config = match config_type {
|
|
ConfigType::Bundle => TsConfig::new(json!({
|
|
"checkJs": false,
|
|
"emitDecoratorMetadata": false,
|
|
"importsNotUsedAsValues": "remove",
|
|
"inlineSourceMap": false,
|
|
"inlineSources": false,
|
|
"sourceMap": false,
|
|
"jsx": "react",
|
|
"jsxFactory": "React.createElement",
|
|
"jsxFragmentFactory": "React.Fragment",
|
|
})),
|
|
ConfigType::Check { tsc_emit, lib } => {
|
|
let mut ts_config = TsConfig::new(json!({
|
|
"allowJs": true,
|
|
"allowSyntheticDefaultImports": true,
|
|
"experimentalDecorators": true,
|
|
"incremental": true,
|
|
"jsx": "react",
|
|
"isolatedModules": true,
|
|
"lib": lib,
|
|
"module": "esnext",
|
|
"resolveJsonModule": true,
|
|
"strict": true,
|
|
"target": "esnext",
|
|
"tsBuildInfoFile": "deno:///.tsbuildinfo",
|
|
"useDefineForClassFields": true,
|
|
// TODO(@kitsonk) remove for Deno 2.0
|
|
"useUnknownInCatchVariables": false,
|
|
}));
|
|
if tsc_emit {
|
|
ts_config.merge(&json!({
|
|
"emitDecoratorMetadata": false,
|
|
"importsNotUsedAsValues": "remove",
|
|
"inlineSourceMap": true,
|
|
"inlineSources": true,
|
|
"outDir": "deno://",
|
|
"removeComments": true,
|
|
}));
|
|
} else {
|
|
ts_config.merge(&json!({
|
|
"noEmit": true,
|
|
}));
|
|
}
|
|
ts_config
|
|
}
|
|
ConfigType::Emit => TsConfig::new(json!({
|
|
"checkJs": false,
|
|
"emitDecoratorMetadata": false,
|
|
"importsNotUsedAsValues": "remove",
|
|
"inlineSourceMap": true,
|
|
"inlineSources": true,
|
|
"sourceMap": false,
|
|
"jsx": "react",
|
|
"jsxFactory": "React.createElement",
|
|
"jsxFragmentFactory": "React.Fragment",
|
|
"resolveJsonModule": true,
|
|
})),
|
|
ConfigType::RuntimeEmit { tsc_emit } => {
|
|
let mut ts_config = TsConfig::new(json!({
|
|
"allowJs": true,
|
|
"allowSyntheticDefaultImports": true,
|
|
"checkJs": false,
|
|
"emitDecoratorMetadata": false,
|
|
"experimentalDecorators": true,
|
|
"importsNotUsedAsValues": "remove",
|
|
"incremental": true,
|
|
"isolatedModules": true,
|
|
"jsx": "react",
|
|
"jsxFactory": "React.createElement",
|
|
"jsxFragmentFactory": "React.Fragment",
|
|
"lib": TypeLib::DenoWindow,
|
|
"module": "esnext",
|
|
"removeComments": true,
|
|
"inlineSourceMap": false,
|
|
"inlineSources": false,
|
|
"sourceMap": true,
|
|
"strict": true,
|
|
"target": "esnext",
|
|
"tsBuildInfoFile": "deno:///.tsbuildinfo",
|
|
"useDefineForClassFields": true,
|
|
// TODO(@kitsonk) remove for Deno 2.0
|
|
"useUnknownInCatchVariables": false,
|
|
}));
|
|
if tsc_emit {
|
|
ts_config.merge(&json!({
|
|
"importsNotUsedAsValues": "remove",
|
|
"outDir": "deno://",
|
|
}));
|
|
} else {
|
|
ts_config.merge(&json!({
|
|
"noEmit": true,
|
|
}));
|
|
}
|
|
ts_config
|
|
}
|
|
};
|
|
let maybe_ignored_options = if let Some(user_options) = maybe_user_config {
|
|
ts_config.merge_user_config(user_options)?
|
|
} else {
|
|
ts_config.merge_tsconfig_from_config_file(maybe_config_file)?
|
|
};
|
|
Ok((ts_config, maybe_ignored_options))
|
|
}
|
|
|
|
/// Transform the graph into root specifiers that we can feed `tsc`. We have to
|
|
/// provide the media type for root modules because `tsc` does not "resolve" the
|
|
/// media type like other modules, as well as a root specifier needs any
|
|
/// redirects resolved. If we aren't checking JavaScript, we need to include all
|
|
/// the emittable files in the roots, so they get type checked and optionally
|
|
/// emitted, otherwise they would be ignored if only imported into JavaScript.
|
|
fn get_tsc_roots(
|
|
roots: &[(ModuleSpecifier, ModuleKind)],
|
|
graph_data: &GraphData,
|
|
check_js: bool,
|
|
) -> Vec<(ModuleSpecifier, MediaType)> {
|
|
if !check_js {
|
|
graph_data
|
|
.entries()
|
|
.into_iter()
|
|
.filter_map(|(specifier, module_entry)| match module_entry {
|
|
ModuleEntry::Module { media_type, .. } => match &media_type {
|
|
MediaType::TypeScript
|
|
| MediaType::Tsx
|
|
| MediaType::Mts
|
|
| MediaType::Cts
|
|
| MediaType::Jsx => Some((specifier.clone(), *media_type)),
|
|
_ => None,
|
|
},
|
|
_ => None,
|
|
})
|
|
.collect()
|
|
} else {
|
|
roots
|
|
.iter()
|
|
.filter_map(|(specifier, _)| match graph_data.get(specifier) {
|
|
Some(ModuleEntry::Module { media_type, .. }) => {
|
|
Some((specifier.clone(), *media_type))
|
|
}
|
|
_ => None,
|
|
})
|
|
.collect()
|
|
}
|
|
}
|
|
|
|
/// A hashing function that takes the source code, version and optionally a
|
|
/// user provided config and generates a string hash which can be stored to
|
|
/// determine if the cached emit is valid or not.
|
|
fn get_version(source_bytes: &[u8], config_bytes: &[u8]) -> String {
|
|
crate::checksum::gen(&[
|
|
source_bytes,
|
|
version::deno().as_bytes(),
|
|
config_bytes,
|
|
])
|
|
}
|
|
|
|
/// Determine if a given module kind and media type is emittable or not.
|
|
pub fn is_emittable(
|
|
kind: &ModuleKind,
|
|
media_type: &MediaType,
|
|
include_js: bool,
|
|
) -> bool {
|
|
if matches!(kind, ModuleKind::Synthetic) {
|
|
return false;
|
|
}
|
|
match &media_type {
|
|
MediaType::TypeScript
|
|
| MediaType::Mts
|
|
| MediaType::Cts
|
|
| MediaType::Tsx
|
|
| MediaType::Jsx => true,
|
|
MediaType::JavaScript | MediaType::Mjs | MediaType::Cjs => include_js,
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
/// Options for performing a check of a module graph. Note that the decision to
|
|
/// emit or not is determined by the `ts_config` settings.
|
|
pub struct CheckOptions {
|
|
/// The check flag from the option which can effect the filtering of
|
|
/// diagnostics in the emit result.
|
|
pub type_check_mode: flags::TypeCheckMode,
|
|
/// Set the debug flag on the TypeScript type checker.
|
|
pub debug: bool,
|
|
/// If true, any files emitted will be cached, even if there are diagnostics
|
|
/// produced. If false, if there are diagnostics, caching emitted files will
|
|
/// be skipped.
|
|
pub emit_with_diagnostics: bool,
|
|
/// The module specifier to the configuration file, passed to tsc so that
|
|
/// configuration related diagnostics are properly formed.
|
|
pub maybe_config_specifier: Option<ModuleSpecifier>,
|
|
/// The derived tsconfig that should be used when checking.
|
|
pub ts_config: TsConfig,
|
|
/// If true, `Check <specifier>` will be written to stdout for each root.
|
|
pub log_checks: bool,
|
|
/// If true, valid existing emits and `.tsbuildinfo` files will be ignored.
|
|
pub reload: bool,
|
|
pub reload_exclusions: HashSet<ModuleSpecifier>,
|
|
}
|
|
|
|
/// The result of a check or emit of a module graph. Note that the actual
|
|
/// emitted sources are stored in the cache and are not returned in the result.
|
|
#[derive(Debug, Default)]
|
|
pub struct CheckEmitResult {
|
|
pub diagnostics: Diagnostics,
|
|
pub stats: Stats,
|
|
}
|
|
|
|
/// Given a set of roots and graph data, type check the module graph and
|
|
/// optionally emit modules, updating the cache as appropriate. Emitting is
|
|
/// determined by the `ts_config` supplied in the options, and if emitting, the
|
|
/// files are stored in the cache.
|
|
///
|
|
/// It is expected that it is determined if a check and/or emit is validated
|
|
/// before the function is called.
|
|
pub fn check_and_maybe_emit(
|
|
roots: &[(ModuleSpecifier, ModuleKind)],
|
|
graph_data: Arc<RwLock<GraphData>>,
|
|
cache: &mut dyn Cacher,
|
|
options: CheckOptions,
|
|
) -> Result<CheckEmitResult, AnyError> {
|
|
let check_js = options.ts_config.get_check_js();
|
|
let segment_graph_data = {
|
|
let graph_data = graph_data.read();
|
|
graph_data.graph_segment(roots).unwrap()
|
|
};
|
|
if valid_emit(
|
|
&segment_graph_data,
|
|
cache,
|
|
&options.ts_config,
|
|
options.reload,
|
|
&options.reload_exclusions,
|
|
) {
|
|
return Ok(Default::default());
|
|
}
|
|
let root_names = get_tsc_roots(roots, &segment_graph_data, check_js);
|
|
if options.log_checks {
|
|
for (root, _) in roots {
|
|
let root_str = root.to_string();
|
|
// `$deno` specifiers are internal, don't print them.
|
|
if !root_str.contains("$deno") {
|
|
log::info!("{} {}", colors::green("Check"), root);
|
|
}
|
|
}
|
|
}
|
|
// while there might be multiple roots, we can't "merge" the build info, so we
|
|
// try to retrieve the build info for first root, which is the most common use
|
|
// case.
|
|
let maybe_tsbuildinfo = if options.reload {
|
|
None
|
|
} else {
|
|
cache.get(CacheType::TypeScriptBuildInfo, &roots[0].0)
|
|
};
|
|
// to make tsc build info work, we need to consistently hash modules, so that
|
|
// tsc can better determine if an emit is still valid or not, so we provide
|
|
// that data here.
|
|
let hash_data = vec![
|
|
options.ts_config.as_bytes(),
|
|
version::deno().as_bytes().to_owned(),
|
|
];
|
|
let config_bytes = options.ts_config.as_bytes();
|
|
|
|
let response = tsc::exec(tsc::Request {
|
|
config: options.ts_config,
|
|
debug: options.debug,
|
|
graph_data: graph_data.clone(),
|
|
hash_data,
|
|
maybe_config_specifier: options.maybe_config_specifier,
|
|
maybe_tsbuildinfo,
|
|
root_names,
|
|
})?;
|
|
|
|
let diagnostics = if options.type_check_mode == flags::TypeCheckMode::Local {
|
|
response.diagnostics.filter(|d| {
|
|
if let Some(file_name) = &d.file_name {
|
|
!file_name.starts_with("http")
|
|
} else {
|
|
true
|
|
}
|
|
})
|
|
} else {
|
|
response.diagnostics
|
|
};
|
|
|
|
// sometimes we want to emit when there are diagnostics, and sometimes we
|
|
// don't. tsc will always return an emit if there are diagnostics
|
|
if (diagnostics.is_empty() || options.emit_with_diagnostics)
|
|
&& !response.emitted_files.is_empty()
|
|
{
|
|
if let Some(info) = &response.maybe_tsbuildinfo {
|
|
// while we retrieve the build info for just the first module, it can be
|
|
// used for all the roots in the graph, so we will cache it for all roots
|
|
for (root, _) in roots {
|
|
cache.set(CacheType::TypeScriptBuildInfo, root, info.clone())?;
|
|
}
|
|
}
|
|
for emit in response.emitted_files.into_iter() {
|
|
if let Some(specifiers) = emit.maybe_specifiers {
|
|
assert!(specifiers.len() == 1);
|
|
// The emitted specifier might not be the file specifier we want, so we
|
|
// resolve it via the graph.
|
|
let graph_data = graph_data.read();
|
|
let specifier = graph_data.follow_redirect(&specifiers[0]);
|
|
let (source_bytes, media_type) = match graph_data.get(&specifier) {
|
|
Some(ModuleEntry::Module {
|
|
code, media_type, ..
|
|
}) => (code.as_bytes(), *media_type),
|
|
_ => {
|
|
log::debug!("skipping emit for {}", specifier);
|
|
continue;
|
|
}
|
|
};
|
|
// Sometimes if `tsc` sees a CommonJS file or a JSON module, it will
|
|
// _helpfully_ output it, which we don't really want to do unless
|
|
// someone has enabled check_js.
|
|
if matches!(media_type, MediaType::Json)
|
|
|| (!check_js
|
|
&& matches!(
|
|
media_type,
|
|
MediaType::JavaScript | MediaType::Cjs | MediaType::Mjs
|
|
))
|
|
{
|
|
log::debug!("skipping emit for {}", specifier);
|
|
continue;
|
|
}
|
|
match emit.media_type {
|
|
MediaType::JavaScript | MediaType::Mjs | MediaType::Cjs => {
|
|
let version = get_version(source_bytes, &config_bytes);
|
|
cache.set(CacheType::Version, &specifier, version)?;
|
|
cache.set(CacheType::Emit, &specifier, emit.data)?;
|
|
}
|
|
MediaType::SourceMap => {
|
|
cache.set(CacheType::SourceMap, &specifier, emit.data)?;
|
|
}
|
|
// this only occurs with the runtime emit, but we are using the same
|
|
// code paths, so we handle it here.
|
|
MediaType::Dts | MediaType::Dcts | MediaType::Dmts => {
|
|
cache.set(CacheType::Declaration, &specifier, emit.data)?;
|
|
}
|
|
_ => unreachable!(
|
|
"unexpected media_type {} {}",
|
|
emit.media_type, specifier
|
|
),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(CheckEmitResult {
|
|
diagnostics,
|
|
stats: response.stats,
|
|
})
|
|
}
|
|
|
|
pub enum BundleType {
|
|
/// Return the emitted contents of the program as a single "flattened" ES
|
|
/// module.
|
|
Module,
|
|
/// Return the emitted contents of the program as a single script that
|
|
/// executes the program using an immediately invoked function execution
|
|
/// (IIFE).
|
|
Classic,
|
|
}
|
|
|
|
impl From<BundleType> for swc::bundler::ModuleType {
|
|
fn from(bundle_type: BundleType) -> Self {
|
|
match bundle_type {
|
|
BundleType::Classic => Self::Iife,
|
|
BundleType::Module => Self::Es,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct BundleOptions {
|
|
pub bundle_type: BundleType,
|
|
pub ts_config: TsConfig,
|
|
pub emit_ignore_directives: bool,
|
|
}
|
|
|
|
/// A module loader for swc which does the appropriate retrieval and transpiling
|
|
/// of modules from the graph.
|
|
struct BundleLoader<'a> {
|
|
cm: Rc<swc::common::SourceMap>,
|
|
emit_options: &'a deno_ast::EmitOptions,
|
|
graph: &'a ModuleGraph,
|
|
}
|
|
|
|
impl swc::bundler::Load for BundleLoader<'_> {
|
|
fn load(
|
|
&self,
|
|
file_name: &swc::common::FileName,
|
|
) -> Result<swc::bundler::ModuleData, AnyError> {
|
|
match file_name {
|
|
swc::common::FileName::Url(specifier) => {
|
|
if let Some(m) = self.graph.get(specifier) {
|
|
let (fm, module) = transpile_module(
|
|
specifier,
|
|
m.maybe_source.as_ref().map(|s| s.as_str()).unwrap_or(""),
|
|
m.media_type,
|
|
self.emit_options,
|
|
self.cm.clone(),
|
|
)?;
|
|
Ok(swc::bundler::ModuleData {
|
|
fm,
|
|
module,
|
|
helpers: Default::default(),
|
|
})
|
|
} else {
|
|
Err(anyhow!(
|
|
"Module \"{}\" unexpectedly missing when bundling.",
|
|
specifier
|
|
))
|
|
}
|
|
}
|
|
_ => unreachable!(
|
|
"Received a request for unsupported filename {:?}",
|
|
file_name
|
|
),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Transpiles a source module into an swc SourceFile.
|
|
fn transpile_module(
|
|
specifier: &ModuleSpecifier,
|
|
source: &str,
|
|
media_type: MediaType,
|
|
options: &deno_ast::EmitOptions,
|
|
cm: Rc<swc::common::SourceMap>,
|
|
) -> Result<(Rc<swc::common::SourceFile>, swc::ast::Module), AnyError> {
|
|
let source = strip_bom(source);
|
|
let source = if media_type == MediaType::Json {
|
|
format!(
|
|
"export default JSON.parse(`{}`);",
|
|
source.replace("${", "\\${").replace('`', "\\`")
|
|
)
|
|
} else {
|
|
source.to_string()
|
|
};
|
|
let source_file =
|
|
cm.new_source_file(FileName::Url(specifier.clone()), source);
|
|
let input = StringInput::from(&*source_file);
|
|
let comments = SingleThreadedComments::default();
|
|
let syntax = if media_type == MediaType::Json {
|
|
get_syntax(MediaType::JavaScript)
|
|
} else {
|
|
get_syntax(media_type)
|
|
};
|
|
let lexer = Lexer::new(syntax, deno_ast::ES_VERSION, input, Some(&comments));
|
|
let mut parser = swc::parser::Parser::new_from(lexer);
|
|
let module = parser
|
|
.parse_module()
|
|
.map_err(|e| swc_err_to_diagnostic(&cm, specifier, e))?;
|
|
let diagnostics = parser
|
|
.take_errors()
|
|
.into_iter()
|
|
.map(|e| swc_err_to_diagnostic(&cm, specifier, e))
|
|
.collect::<Vec<_>>();
|
|
|
|
let top_level_mark = Mark::fresh(Mark::root());
|
|
let program = deno_ast::fold_program(
|
|
swc::ast::Program::Module(module),
|
|
options,
|
|
cm,
|
|
&comments,
|
|
top_level_mark,
|
|
&diagnostics,
|
|
)?;
|
|
let module = match program {
|
|
swc::ast::Program::Module(module) => module,
|
|
_ => unreachable!(),
|
|
};
|
|
|
|
Ok((source_file, module))
|
|
}
|
|
|
|
fn swc_err_to_diagnostic(
|
|
source_map: &SourceMap,
|
|
specifier: &ModuleSpecifier,
|
|
err: SwcError,
|
|
) -> Diagnostic {
|
|
let location = source_map.lookup_char_pos(err.span().lo);
|
|
Diagnostic {
|
|
specifier: specifier.to_string(),
|
|
span: err.span(),
|
|
display_position: LineAndColumnDisplay {
|
|
line_number: location.line,
|
|
column_number: location.col_display + 1,
|
|
},
|
|
kind: err.into_kind(),
|
|
}
|
|
}
|
|
|
|
/// A resolver implementation for swc that resolves specifiers from the graph.
|
|
struct BundleResolver<'a>(&'a ModuleGraph);
|
|
|
|
impl swc::bundler::Resolve for BundleResolver<'_> {
|
|
fn resolve(
|
|
&self,
|
|
referrer: &swc::common::FileName,
|
|
specifier: &str,
|
|
) -> Result<swc::common::FileName, AnyError> {
|
|
let referrer = if let swc::common::FileName::Url(referrer) = referrer {
|
|
referrer
|
|
} else {
|
|
unreachable!(
|
|
"An unexpected referrer was passed when bundling: {:?}",
|
|
referrer
|
|
);
|
|
};
|
|
if let Some(specifier) =
|
|
self.0.resolve_dependency(specifier, referrer, false)
|
|
{
|
|
Ok(deno_ast::swc::common::FileName::Url(specifier.clone()))
|
|
} else {
|
|
Err(anyhow!(
|
|
"Cannot resolve \"{}\" from \"{}\".",
|
|
specifier,
|
|
referrer
|
|
))
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Given a module graph, generate and return a bundle of the graph and
|
|
/// optionally its source map. Unlike emitting with `check_and_maybe_emit` and
|
|
/// `emit`, which store the emitted modules in the cache, this function simply
|
|
/// returns the output.
|
|
pub fn bundle(
|
|
graph: &ModuleGraph,
|
|
options: BundleOptions,
|
|
) -> Result<(String, Option<String>), AnyError> {
|
|
let globals = swc::common::Globals::new();
|
|
deno_ast::swc::common::GLOBALS.set(&globals, || {
|
|
let emit_options: deno_ast::EmitOptions = options.ts_config.into();
|
|
let source_map_config = deno_ast::SourceMapConfig {
|
|
inline_sources: emit_options.inline_sources,
|
|
};
|
|
|
|
let cm = Rc::new(swc::common::SourceMap::new(
|
|
swc::common::FilePathMapping::empty(),
|
|
));
|
|
let loader = BundleLoader {
|
|
graph,
|
|
emit_options: &emit_options,
|
|
cm: cm.clone(),
|
|
};
|
|
let resolver = BundleResolver(graph);
|
|
let config = swc::bundler::Config {
|
|
module: options.bundle_type.into(),
|
|
..Default::default()
|
|
};
|
|
// This hook will rewrite the `import.meta` when bundling to give a consistent
|
|
// behavior between bundled and unbundled code.
|
|
let hook = Box::new(BundleHook);
|
|
let mut bundler = swc::bundler::Bundler::new(
|
|
&globals,
|
|
cm.clone(),
|
|
loader,
|
|
resolver,
|
|
config,
|
|
hook,
|
|
);
|
|
let mut entries = HashMap::new();
|
|
entries.insert(
|
|
"bundle".to_string(),
|
|
swc::common::FileName::Url(graph.roots[0].0.clone()),
|
|
);
|
|
let output = bundler
|
|
.bundle(entries)
|
|
.context("Unable to output during bundling.")?;
|
|
let mut buf = Vec::new();
|
|
let mut srcmap = Vec::new();
|
|
{
|
|
let cfg = swc::codegen::Config { minify: false };
|
|
let mut wr = Box::new(swc::codegen::text_writer::JsWriter::new(
|
|
cm.clone(),
|
|
"\n",
|
|
&mut buf,
|
|
Some(&mut srcmap),
|
|
));
|
|
|
|
if options.emit_ignore_directives {
|
|
// write leading comments in bundled file
|
|
use swc::codegen::text_writer::WriteJs;
|
|
let cmt = IGNORE_DIRECTIVES.join("\n") + "\n";
|
|
wr.write_comment(&cmt)?;
|
|
}
|
|
|
|
let mut emitter = swc::codegen::Emitter {
|
|
cfg,
|
|
cm: cm.clone(),
|
|
comments: None,
|
|
wr,
|
|
};
|
|
emitter
|
|
.emit_module(&output[0].module)
|
|
.context("Unable to emit during bundling.")?;
|
|
}
|
|
let mut code =
|
|
String::from_utf8(buf).context("Emitted code is an invalid string.")?;
|
|
let mut maybe_map: Option<String> = None;
|
|
{
|
|
let mut buf = Vec::new();
|
|
cm.build_source_map_with_config(&mut srcmap, None, source_map_config)
|
|
.to_writer(&mut buf)?;
|
|
if emit_options.inline_source_map {
|
|
let encoded_map = format!(
|
|
"//# sourceMappingURL=data:application/json;base64,{}\n",
|
|
base64::encode(buf)
|
|
);
|
|
code.push_str(&encoded_map);
|
|
} else if emit_options.source_map {
|
|
maybe_map = Some(String::from_utf8(buf)?);
|
|
}
|
|
}
|
|
|
|
Ok((code, maybe_map))
|
|
})
|
|
}
|
|
|
|
pub struct EmitOptions {
|
|
pub ts_config: TsConfig,
|
|
pub reload: bool,
|
|
pub reload_exclusions: HashSet<ModuleSpecifier>,
|
|
}
|
|
|
|
/// Given a module graph, emit any appropriate modules and cache them.
|
|
// TODO(nayeemrmn): This would ideally take `GraphData` like
|
|
// `check_and_maybe_emit()`, but the AST isn't stored in that. Cleanup.
|
|
pub fn emit(
|
|
graph: &ModuleGraph,
|
|
cache: &mut dyn Cacher,
|
|
options: EmitOptions,
|
|
) -> Result<CheckEmitResult, AnyError> {
|
|
let start = Instant::now();
|
|
let config_bytes = options.ts_config.as_bytes();
|
|
let include_js = options.ts_config.get_check_js();
|
|
let emit_options = options.ts_config.into();
|
|
|
|
let mut emit_count = 0_u32;
|
|
let mut file_count = 0_u32;
|
|
for module in graph.modules() {
|
|
file_count += 1;
|
|
if !is_emittable(&module.kind, &module.media_type, include_js) {
|
|
continue;
|
|
}
|
|
let needs_reload =
|
|
options.reload && !options.reload_exclusions.contains(&module.specifier);
|
|
let version = get_version(
|
|
module.maybe_source.as_ref().map(|s| s.as_bytes()).unwrap(),
|
|
&config_bytes,
|
|
);
|
|
let is_valid =
|
|
cache
|
|
.get(CacheType::Version, &module.specifier)
|
|
.map_or(false, |v| {
|
|
v == get_version(
|
|
module.maybe_source.as_ref().map(|s| s.as_bytes()).unwrap(),
|
|
&config_bytes,
|
|
)
|
|
});
|
|
if is_valid && !needs_reload {
|
|
continue;
|
|
}
|
|
let transpiled_source = module
|
|
.maybe_parsed_source
|
|
.as_ref()
|
|
.map(|ps| ps.transpile(&emit_options))
|
|
.unwrap()?;
|
|
emit_count += 1;
|
|
cache.set(CacheType::Emit, &module.specifier, transpiled_source.text)?;
|
|
if let Some(map) = transpiled_source.source_map {
|
|
cache.set(CacheType::SourceMap, &module.specifier, map)?;
|
|
}
|
|
if !is_valid {
|
|
cache.set(CacheType::Version, &module.specifier, version)?;
|
|
}
|
|
}
|
|
|
|
let stats = Stats(vec![
|
|
("Files".to_string(), file_count),
|
|
("Emitted".to_string(), emit_count),
|
|
("Total time".to_string(), start.elapsed().as_millis() as u32),
|
|
]);
|
|
|
|
Ok(CheckEmitResult {
|
|
diagnostics: Diagnostics::default(),
|
|
stats,
|
|
})
|
|
}
|
|
|
|
/// Check a module graph to determine if the graph contains anything that
|
|
/// is required to be emitted to be valid. It determines what modules in the
|
|
/// graph are emittable and for those that are emittable, if there is currently
|
|
/// a valid emit in the cache.
|
|
fn valid_emit(
|
|
graph_data: &GraphData,
|
|
cache: &dyn Cacher,
|
|
ts_config: &TsConfig,
|
|
reload: bool,
|
|
reload_exclusions: &HashSet<ModuleSpecifier>,
|
|
) -> bool {
|
|
let config_bytes = ts_config.as_bytes();
|
|
let emit_js = ts_config.get_check_js();
|
|
for (specifier, module_entry) in graph_data.entries() {
|
|
if let ModuleEntry::Module {
|
|
code, media_type, ..
|
|
} = module_entry
|
|
{
|
|
match media_type {
|
|
MediaType::TypeScript
|
|
| MediaType::Mts
|
|
| MediaType::Cts
|
|
| MediaType::Tsx
|
|
| MediaType::Jsx => {}
|
|
MediaType::JavaScript | MediaType::Mjs | MediaType::Cjs => {
|
|
if !emit_js {
|
|
continue;
|
|
}
|
|
}
|
|
_ => continue,
|
|
}
|
|
if reload && !reload_exclusions.contains(specifier) {
|
|
return false;
|
|
}
|
|
if let Some(version) = cache.get(CacheType::Version, specifier) {
|
|
if version != get_version(code.as_bytes(), &config_bytes) {
|
|
return false;
|
|
}
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
true
|
|
}
|
|
|
|
/// An adapter struct to make a deno_graph::ModuleGraphError display as expected
|
|
/// in the Deno CLI.
|
|
#[derive(Debug)]
|
|
pub struct GraphError(pub ModuleGraphError);
|
|
|
|
impl std::error::Error for GraphError {}
|
|
|
|
impl From<ModuleGraphError> for GraphError {
|
|
fn from(err: ModuleGraphError) -> Self {
|
|
Self(err)
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for GraphError {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match &self.0 {
|
|
ModuleGraphError::ResolutionError(err) => {
|
|
if matches!(
|
|
err,
|
|
ResolutionError::InvalidDowngrade { .. }
|
|
| ResolutionError::InvalidLocalImport { .. }
|
|
) {
|
|
write!(f, "{}", err.to_string_with_range())
|
|
} else {
|
|
self.0.fmt(f)
|
|
}
|
|
}
|
|
_ => self.0.fmt(f),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Convert a module graph to a map of "files", which are used by the runtime
|
|
/// emit to be passed back to the caller.
|
|
pub fn to_file_map(
|
|
graph: &ModuleGraph,
|
|
cache: &dyn Cacher,
|
|
) -> HashMap<String, String> {
|
|
let mut files = HashMap::new();
|
|
for (_, result) in graph.specifiers().into_iter() {
|
|
if let Ok((specifier, _, media_type)) = result {
|
|
if let Some(emit) = cache.get(CacheType::Emit, &specifier) {
|
|
files.insert(format!("{}.js", specifier), emit);
|
|
if let Some(map) = cache.get(CacheType::SourceMap, &specifier) {
|
|
files.insert(format!("{}.js.map", specifier), map);
|
|
}
|
|
} else if matches!(
|
|
media_type,
|
|
MediaType::JavaScript
|
|
| MediaType::Mjs
|
|
| MediaType::Cjs
|
|
| MediaType::Json
|
|
| MediaType::Unknown
|
|
) {
|
|
if let Some(module) = graph.get(&specifier) {
|
|
files.insert(
|
|
specifier.to_string(),
|
|
module
|
|
.maybe_source
|
|
.as_ref()
|
|
.map(|s| s.to_string())
|
|
.unwrap_or_else(|| "".to_string()),
|
|
);
|
|
}
|
|
}
|
|
if let Some(declaration) = cache.get(CacheType::Declaration, &specifier) {
|
|
files.insert(format!("{}.d.ts", specifier), declaration);
|
|
}
|
|
}
|
|
}
|
|
files
|
|
}
|
|
|
|
/// This contains the logic for Deno to rewrite the `import.meta` when bundling.
|
|
pub struct BundleHook;
|
|
|
|
impl Hook for BundleHook {
|
|
fn get_import_meta_props(
|
|
&self,
|
|
span: Span,
|
|
module_record: &ModuleRecord,
|
|
) -> Result<Vec<deno_ast::swc::ast::KeyValueProp>, AnyError> {
|
|
use deno_ast::swc::ast;
|
|
|
|
Ok(vec![
|
|
ast::KeyValueProp {
|
|
key: ast::PropName::Ident(ast::Ident::new("url".into(), span)),
|
|
value: Box::new(ast::Expr::Lit(ast::Lit::Str(ast::Str {
|
|
span,
|
|
value: module_record.file_name.to_string().into(),
|
|
raw: None,
|
|
}))),
|
|
},
|
|
ast::KeyValueProp {
|
|
key: ast::PropName::Ident(ast::Ident::new("main".into(), span)),
|
|
value: Box::new(if module_record.is_entry {
|
|
ast::Expr::Member(ast::MemberExpr {
|
|
span,
|
|
obj: Box::new(ast::Expr::MetaProp(ast::MetaPropExpr {
|
|
span,
|
|
kind: ast::MetaPropKind::ImportMeta,
|
|
})),
|
|
prop: ast::MemberProp::Ident(ast::Ident::new("main".into(), span)),
|
|
})
|
|
} else {
|
|
ast::Expr::Lit(ast::Lit::Bool(ast::Bool { span, value: false }))
|
|
}),
|
|
},
|
|
])
|
|
}
|
|
}
|
|
|
|
impl From<config_file::TsConfig> for deno_ast::EmitOptions {
|
|
fn from(config: config_file::TsConfig) -> Self {
|
|
let options: config_file::EmitConfigOptions =
|
|
serde_json::from_value(config.0).unwrap();
|
|
let imports_not_used_as_values =
|
|
match options.imports_not_used_as_values.as_str() {
|
|
"preserve" => deno_ast::ImportsNotUsedAsValues::Preserve,
|
|
"error" => deno_ast::ImportsNotUsedAsValues::Error,
|
|
_ => deno_ast::ImportsNotUsedAsValues::Remove,
|
|
};
|
|
let (transform_jsx, jsx_automatic, jsx_development) =
|
|
match options.jsx.as_str() {
|
|
"react" => (true, false, false),
|
|
"react-jsx" => (true, true, false),
|
|
"react-jsxdev" => (true, true, true),
|
|
_ => (false, false, false),
|
|
};
|
|
deno_ast::EmitOptions {
|
|
emit_metadata: options.emit_decorator_metadata,
|
|
imports_not_used_as_values,
|
|
inline_source_map: options.inline_source_map,
|
|
inline_sources: options.inline_sources,
|
|
source_map: options.source_map,
|
|
jsx_automatic,
|
|
jsx_development,
|
|
jsx_factory: options.jsx_factory,
|
|
jsx_fragment_factory: options.jsx_fragment_factory,
|
|
jsx_import_source: options.jsx_import_source,
|
|
transform_jsx,
|
|
var_decl_imports: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_is_emittable() {
|
|
assert!(is_emittable(
|
|
&ModuleKind::Esm,
|
|
&MediaType::TypeScript,
|
|
false
|
|
));
|
|
assert!(!is_emittable(
|
|
&ModuleKind::Synthetic,
|
|
&MediaType::TypeScript,
|
|
false
|
|
));
|
|
assert!(!is_emittable(&ModuleKind::Esm, &MediaType::Dts, false));
|
|
assert!(!is_emittable(&ModuleKind::Esm, &MediaType::Dcts, false));
|
|
assert!(!is_emittable(&ModuleKind::Esm, &MediaType::Dmts, false));
|
|
assert!(is_emittable(&ModuleKind::Esm, &MediaType::Tsx, false));
|
|
assert!(!is_emittable(
|
|
&ModuleKind::Esm,
|
|
&MediaType::JavaScript,
|
|
false
|
|
));
|
|
assert!(!is_emittable(&ModuleKind::Esm, &MediaType::Cjs, false));
|
|
assert!(!is_emittable(&ModuleKind::Esm, &MediaType::Mjs, false));
|
|
assert!(is_emittable(&ModuleKind::Esm, &MediaType::JavaScript, true));
|
|
assert!(is_emittable(&ModuleKind::Esm, &MediaType::Jsx, false));
|
|
assert!(!is_emittable(&ModuleKind::Esm, &MediaType::Json, false));
|
|
}
|
|
}
|