1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-22 07:14:47 -05:00

fix(compile): be more deterministic when compiling the same code in different directories (#27395)

Additionaly, this no longer unnecessarily stores the source twice for
file specifiers and fixes some sourcemap issues.

Closes https://github.com/denoland/deno/issues/27284
This commit is contained in:
David Sherret 2024-12-19 12:53:52 -05:00 committed by GitHub
parent 350d9dce41
commit 074444ab6c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 563 additions and 269 deletions

View file

@ -1363,9 +1363,9 @@ impl CliOptions {
Ok(DenoLintConfig {
default_jsx_factory: (!transpile_options.jsx_automatic)
.then(|| transpile_options.jsx_factory.clone()),
.then_some(transpile_options.jsx_factory),
default_jsx_fragment_factory: (!transpile_options.jsx_automatic)
.then(|| transpile_options.jsx_fragment_factory.clone()),
.then_some(transpile_options.jsx_fragment_factory),
})
}

View file

@ -5,6 +5,7 @@ use crate::cache::FastInsecureHasher;
use crate::cache::ParsedSourceCache;
use crate::resolver::CjsTracker;
use deno_ast::EmittedSourceText;
use deno_ast::ModuleKind;
use deno_ast::SourceMapOption;
use deno_ast::SourceRange;
@ -132,6 +133,7 @@ impl Emitter {
&transpile_and_emit_options.0,
&transpile_and_emit_options.1,
)
.map(|r| r.text)
}
})
.await
@ -166,7 +168,8 @@ impl Emitter {
source.clone(),
&self.transpile_and_emit_options.0,
&self.transpile_and_emit_options.1,
)?;
)?
.text;
helper.post_emit_parsed_source(
specifier,
&transpiled_source,
@ -177,6 +180,31 @@ impl Emitter {
}
}
pub fn emit_parsed_source_for_deno_compile(
&self,
specifier: &ModuleSpecifier,
media_type: MediaType,
module_kind: deno_ast::ModuleKind,
source: &Arc<str>,
) -> Result<(String, String), AnyError> {
let mut emit_options = self.transpile_and_emit_options.1.clone();
emit_options.inline_sources = false;
emit_options.source_map = SourceMapOption::Separate;
// strip off the path to have more deterministic builds as we don't care
// about the source name because we manually provide the source map to v8
emit_options.source_map_base = Some(deno_path_util::url_parent(specifier));
let source = EmitParsedSourceHelper::transpile(
&self.parsed_source_cache,
specifier,
media_type,
module_kind,
source.clone(),
&self.transpile_and_emit_options.0,
&emit_options,
)?;
Ok((source.text, source.source_map.unwrap()))
}
/// Expects a file URL, panics otherwise.
pub async fn load_and_emit_for_hmr(
&self,
@ -282,7 +310,7 @@ impl<'a> EmitParsedSourceHelper<'a> {
source: Arc<str>,
transpile_options: &deno_ast::TranspileOptions,
emit_options: &deno_ast::EmitOptions,
) -> Result<String, AnyError> {
) -> Result<EmittedSourceText, AnyError> {
// nothing else needs the parsed source at this point, so remove from
// the cache in order to not transpile owned
let parsed_source = parsed_source_cache
@ -302,8 +330,7 @@ impl<'a> EmitParsedSourceHelper<'a> {
source
}
};
debug_assert!(transpiled_source.source_map.is_none());
Ok(transpiled_source.text)
Ok(transpiled_source)
}
pub fn post_emit_parsed_source(

View file

@ -91,6 +91,7 @@ use super::serialization::DenoCompileModuleData;
use super::serialization::DeserializedDataSection;
use super::serialization::RemoteModulesStore;
use super::serialization::RemoteModulesStoreBuilder;
use super::serialization::SourceMapStore;
use super::virtual_fs::output_vfs;
use super::virtual_fs::BuiltVfs;
use super::virtual_fs::FileBackedVfs;
@ -98,6 +99,7 @@ use super::virtual_fs::VfsBuilder;
use super::virtual_fs::VfsFileSubDataKind;
use super::virtual_fs::VfsRoot;
use super::virtual_fs::VirtualDirectory;
use super::virtual_fs::VirtualDirectoryEntries;
use super::virtual_fs::WindowsSystemRootablePath;
pub static DENO_COMPILE_GLOBAL_NODE_MODULES_DIR_NAME: &str =
@ -203,17 +205,24 @@ pub struct Metadata {
pub otel_config: OtelConfig,
}
#[allow(clippy::too_many_arguments)]
fn write_binary_bytes(
mut file_writer: File,
original_bin: Vec<u8>,
metadata: &Metadata,
npm_snapshot: Option<SerializedNpmResolutionSnapshot>,
remote_modules: &RemoteModulesStoreBuilder,
source_map_store: &SourceMapStore,
vfs: &BuiltVfs,
compile_flags: &CompileFlags,
) -> Result<(), AnyError> {
let data_section_bytes =
serialize_binary_data_section(metadata, npm_snapshot, remote_modules, vfs)
let data_section_bytes = serialize_binary_data_section(
metadata,
npm_snapshot,
remote_modules,
source_map_store,
vfs,
)
.context("Serializing binary data section.")?;
let target = compile_flags.resolve_target();
@ -256,6 +265,7 @@ pub struct StandaloneData {
pub modules: StandaloneModules,
pub npm_snapshot: Option<ValidSerializedNpmResolutionSnapshot>,
pub root_path: PathBuf,
pub source_maps: SourceMapStore,
pub vfs: Arc<FileBackedVfs>,
}
@ -283,13 +293,12 @@ impl StandaloneModules {
pub fn read<'a>(
&'a self,
specifier: &'a ModuleSpecifier,
kind: VfsFileSubDataKind,
) -> Result<Option<DenoCompileModuleData<'a>>, AnyError> {
if specifier.scheme() == "file" {
let path = deno_path_util::url_to_file_path(specifier)?;
let bytes = match self.vfs.file_entry(&path) {
Ok(entry) => self
.vfs
.read_file_all(entry, VfsFileSubDataKind::ModuleGraph)?,
Ok(entry) => self.vfs.read_file_all(entry, kind)?,
Err(err) if err.kind() == ErrorKind::NotFound => {
match RealFs.read_file_sync(&path, None) {
Ok(bytes) => bytes,
@ -307,7 +316,18 @@ impl StandaloneModules {
data: bytes,
}))
} else {
self.remote_modules.read(specifier)
self.remote_modules.read(specifier).map(|maybe_entry| {
maybe_entry.map(|entry| DenoCompileModuleData {
media_type: entry.media_type,
specifier: entry.specifier,
data: match kind {
VfsFileSubDataKind::Raw => entry.data,
VfsFileSubDataKind::ModuleGraph => {
entry.transpiled_data.unwrap_or(entry.data)
}
},
})
})
}
}
}
@ -328,7 +348,8 @@ pub fn extract_standalone(
mut metadata,
npm_snapshot,
remote_modules,
mut vfs_dir,
source_maps,
vfs_root_entries,
vfs_files_data,
} = match deserialize_binary_data_section(data)? {
Some(data_section) => data_section,
@ -351,11 +372,12 @@ pub fn extract_standalone(
metadata.argv.push(arg.into_string().unwrap());
}
let vfs = {
// align the name of the directory with the root dir
vfs_dir.name = root_path.file_name().unwrap().to_string_lossy().to_string();
let fs_root = VfsRoot {
dir: vfs_dir,
dir: VirtualDirectory {
// align the name of the directory with the root dir
name: root_path.file_name().unwrap().to_string_lossy().to_string(),
entries: vfs_root_entries,
},
root_path: root_path.clone(),
start_file_offset: 0,
};
@ -372,6 +394,7 @@ pub fn extract_standalone(
},
npm_snapshot,
root_path,
source_maps,
vfs,
}))
}
@ -451,7 +474,7 @@ impl<'a> DenoCompileBinaryWriter<'a> {
)
}
}
self.write_standalone_binary(options, original_binary).await
self.write_standalone_binary(options, original_binary)
}
async fn get_base_binary(
@ -554,7 +577,7 @@ impl<'a> DenoCompileBinaryWriter<'a> {
/// This functions creates a standalone deno binary by appending a bundle
/// and magic trailer to the currently executing binary.
#[allow(clippy::too_many_arguments)]
async fn write_standalone_binary(
fn write_standalone_binary(
&self,
options: WriteBinOptions<'_>,
original_bin: Vec<u8>,
@ -598,71 +621,81 @@ impl<'a> DenoCompileBinaryWriter<'a> {
.with_context(|| format!("Including {}", path.display()))?;
}
let mut remote_modules_store = RemoteModulesStoreBuilder::default();
let mut code_cache_key_hasher = if self.cli_options.code_cache_enabled() {
Some(FastInsecureHasher::new_deno_versioned())
} else {
None
};
let mut source_maps = Vec::with_capacity(graph.specifiers_count());
// todo(dsherret): transpile in parallel
for module in graph.modules() {
if module.specifier().scheme() == "data" {
continue; // don't store data urls as an entry as they're in the code
}
if let Some(hasher) = &mut code_cache_key_hasher {
if let Some(source) = module.source() {
hasher.write(module.specifier().as_str().as_bytes());
hasher.write(source.as_bytes());
}
}
let (maybe_source, media_type) = match module {
let (maybe_original_source, maybe_transpiled, media_type) = match module {
deno_graph::Module::Js(m) => {
let source = if m.media_type.is_emittable() {
let original_bytes = m.source.as_bytes().to_vec();
let maybe_transpiled = if m.media_type.is_emittable() {
let is_cjs = self.cjs_tracker.is_cjs_with_known_is_script(
&m.specifier,
m.media_type,
m.is_script,
)?;
let module_kind = ModuleKind::from_is_cjs(is_cjs);
let source = self
.emitter
.emit_parsed_source(
let (source, source_map) =
self.emitter.emit_parsed_source_for_deno_compile(
&m.specifier,
m.media_type,
module_kind,
&m.source,
)
.await?;
source.into_bytes()
)?;
if source != m.source.as_ref() {
source_maps.push((&m.specifier, source_map));
Some(source.into_bytes())
} else {
m.source.as_bytes().to_vec()
None
}
} else {
None
};
(Some(source), m.media_type)
(Some(original_bytes), maybe_transpiled, m.media_type)
}
deno_graph::Module::Json(m) => {
(Some(m.source.as_bytes().to_vec()), m.media_type)
(Some(m.source.as_bytes().to_vec()), None, m.media_type)
}
deno_graph::Module::Wasm(m) => {
(Some(m.source.to_vec()), MediaType::Wasm)
(Some(m.source.to_vec()), None, MediaType::Wasm)
}
deno_graph::Module::Npm(_)
| deno_graph::Module::Node(_)
| deno_graph::Module::External(_) => (None, MediaType::Unknown),
| deno_graph::Module::External(_) => (None, None, MediaType::Unknown),
};
if let Some(original_source) = maybe_original_source {
if module.specifier().scheme() == "file" {
let file_path = deno_path_util::url_to_file_path(module.specifier())?;
vfs
.add_file_with_data(
&file_path,
match maybe_source {
Some(source) => source,
None => RealFs.read_file_sync(&file_path, None)?.into_owned(),
},
original_source,
VfsFileSubDataKind::Raw,
)
.with_context(|| {
format!("Failed adding '{}'", file_path.display())
})?;
if let Some(transpiled_source) = maybe_transpiled {
vfs
.add_file_with_data(
&file_path,
transpiled_source,
VfsFileSubDataKind::ModuleGraph,
)
.with_context(|| {
format!("Failed adding '{}'", file_path.display())
})?;
} else if let Some(source) = maybe_source {
remote_modules_store.add(module.specifier(), media_type, source);
}
} else {
remote_modules_store.add(
module.specifier(),
media_type,
original_source,
maybe_transpiled,
);
}
}
}
remote_modules_store.add_redirects(&graph.redirects);
@ -695,6 +728,28 @@ impl<'a> DenoCompileBinaryWriter<'a> {
None => StandaloneRelativeFileBaseUrl::WindowsSystemRoot,
};
let code_cache_key = if self.cli_options.code_cache_enabled() {
let mut hasher = FastInsecureHasher::new_deno_versioned();
for module in graph.modules() {
if let Some(source) = module.source() {
hasher
.write(root_dir_url.specifier_key(module.specifier()).as_bytes());
hasher.write(source.as_bytes());
}
}
Some(hasher.finish())
} else {
None
};
let mut source_map_store = SourceMapStore::with_capacity(source_maps.len());
for (specifier, source_map) in source_maps {
source_map_store.add(
Cow::Owned(root_dir_url.specifier_key(specifier).into_owned()),
Cow::Owned(source_map),
);
}
let node_modules = match self.npm_resolver.as_inner() {
InnerCliNpmResolverRef::Managed(_) => {
npm_snapshot.as_ref().map(|_| NodeModules::Managed {
@ -742,7 +797,7 @@ impl<'a> DenoCompileBinaryWriter<'a> {
let metadata = Metadata {
argv: compile_flags.args.clone(),
seed: self.cli_options.seed(),
code_cache_key: code_cache_key_hasher.map(|h| h.finish()),
code_cache_key,
location: self.cli_options.location_flag().clone(),
permissions: self.cli_options.permission_flags().clone(),
v8_flags: self.cli_options.v8_flags().clone(),
@ -809,6 +864,7 @@ impl<'a> DenoCompileBinaryWriter<'a> {
&metadata,
npm_snapshot.map(|s| s.into_serialized()),
&remote_modules_store,
&source_map_store,
&vfs,
compile_flags,
)
@ -903,10 +959,10 @@ impl<'a> DenoCompileBinaryWriter<'a> {
root_dir.name = DENO_COMPILE_GLOBAL_NODE_MODULES_DIR_NAME.to_string();
let mut new_entries = Vec::with_capacity(root_dir.entries.len());
let mut localhost_entries = IndexMap::new();
for entry in std::mem::take(&mut root_dir.entries) {
for entry in root_dir.entries.take_inner() {
match entry {
VfsEntry::Dir(dir) => {
for entry in dir.entries {
VfsEntry::Dir(mut dir) => {
for entry in dir.entries.take_inner() {
log::debug!("Flattening {} into node_modules", entry.name());
if let Some(existing) =
localhost_entries.insert(entry.name().to_string(), entry)
@ -925,11 +981,11 @@ impl<'a> DenoCompileBinaryWriter<'a> {
}
new_entries.push(VfsEntry::Dir(VirtualDirectory {
name: "localhost".to_string(),
entries: localhost_entries.into_iter().map(|(_, v)| v).collect(),
entries: VirtualDirectoryEntries::new(
localhost_entries.into_iter().map(|(_, v)| v).collect(),
),
}));
// needs to be sorted by name
new_entries.sort_by(|a, b| a.name().cmp(b.name()));
root_dir.entries = new_entries;
root_dir.entries = VirtualDirectoryEntries::new(new_entries);
// it's better to not expose the user's cache directory, so take it out
// of there
@ -937,10 +993,7 @@ impl<'a> DenoCompileBinaryWriter<'a> {
let parent_dir = vfs.get_dir_mut(parent).unwrap();
let index = parent_dir
.entries
.iter()
.position(|entry| {
entry.name() == DENO_COMPILE_GLOBAL_NODE_MODULES_DIR_NAME
})
.binary_search(DENO_COMPILE_GLOBAL_NODE_MODULES_DIR_NAME)
.unwrap();
let npm_global_cache_dir_entry = parent_dir.entries.remove(index);
@ -950,11 +1003,7 @@ impl<'a> DenoCompileBinaryWriter<'a> {
Cow::Borrowed(DENO_COMPILE_GLOBAL_NODE_MODULES_DIR_NAME);
for ancestor in parent.ancestors() {
let dir = vfs.get_dir_mut(ancestor).unwrap();
if let Some(index) = dir
.entries
.iter()
.position(|entry| entry.name() == last_name)
{
if let Ok(index) = dir.entries.binary_search(&last_name) {
dir.entries.remove(index);
}
last_name = Cow::Owned(dir.name.clone());
@ -965,7 +1014,7 @@ impl<'a> DenoCompileBinaryWriter<'a> {
// now build the vfs and add the global cache dir entry there
let mut built_vfs = vfs.build();
built_vfs.root.insert_entry(npm_global_cache_dir_entry);
built_vfs.entries.insert(npm_global_cache_dir_entry);
built_vfs
}
InnerCliNpmResolverRef::Byonm(_) => vfs.build(),

View file

@ -55,6 +55,7 @@ use node_resolver::errors::ClosestPkgJsonError;
use node_resolver::NodeResolutionKind;
use node_resolver::ResolutionMode;
use serialization::DenoCompileModuleSource;
use serialization::SourceMapStore;
use std::borrow::Cow;
use std::rc::Rc;
use std::sync::Arc;
@ -122,6 +123,7 @@ struct SharedModuleLoaderState {
npm_module_loader: Arc<NpmModuleLoader>,
npm_req_resolver: Arc<CliNpmReqResolver>,
npm_resolver: Arc<dyn CliNpmResolver>,
source_maps: SourceMapStore,
vfs: Arc<FileBackedVfs>,
workspace_resolver: WorkspaceResolver,
}
@ -396,7 +398,11 @@ impl ModuleLoader for EmbeddedModuleLoader {
);
}
match self.shared.modules.read(original_specifier) {
match self
.shared
.modules
.read(original_specifier, VfsFileSubDataKind::ModuleGraph)
{
Ok(Some(module)) => {
let media_type = module.media_type;
let (module_specifier, module_type, module_source) =
@ -495,6 +501,46 @@ impl ModuleLoader for EmbeddedModuleLoader {
}
std::future::ready(()).boxed_local()
}
fn get_source_map(&self, file_name: &str) -> Option<Vec<u8>> {
if file_name.starts_with("file:///") {
let url =
deno_path_util::url_from_directory_path(self.shared.vfs.root()).ok()?;
let file_url = ModuleSpecifier::parse(file_name).ok()?;
let relative_path = url.make_relative(&file_url)?;
self.shared.source_maps.get(&relative_path)
} else {
self.shared.source_maps.get(file_name)
}
// todo(https://github.com/denoland/deno_core/pull/1007): don't clone
.map(|s| s.as_bytes().to_vec())
}
fn get_source_mapped_source_line(
&self,
file_name: &str,
line_number: usize,
) -> Option<String> {
let specifier = ModuleSpecifier::parse(file_name).ok()?;
let data = self
.shared
.modules
.read(&specifier, VfsFileSubDataKind::Raw)
.ok()??;
let source = String::from_utf8_lossy(&data.data);
// Do NOT use .lines(): it skips the terminating empty line.
// (due to internally using_terminator() instead of .split())
let lines: Vec<&str> = source.split('\n').collect();
if line_number >= lines.len() {
Some(format!(
"{} Couldn't format source line: Line {} is out of bounds (source may have changed at runtime)",
crate::colors::yellow("Warning"), line_number + 1,
))
} else {
Some(lines[line_number].to_string())
}
}
}
impl NodeRequireLoader for EmbeddedModuleLoader {
@ -590,6 +636,7 @@ pub async fn run(data: StandaloneData) -> Result<i32, AnyError> {
modules,
npm_snapshot,
root_path,
source_maps,
vfs,
} = data;
let deno_dir_provider = Arc::new(DenoDirProvider::new(None));
@ -841,6 +888,7 @@ pub async fn run(data: StandaloneData) -> Result<i32, AnyError> {
)),
npm_resolver: npm_resolver.clone(),
npm_req_resolver,
source_maps,
vfs,
workspace_resolver,
}),

View file

@ -6,6 +6,7 @@ use std::collections::BTreeMap;
use std::collections::HashMap;
use std::io::Write;
use deno_ast::swc::common::source_map;
use deno_ast::MediaType;
use deno_core::anyhow::bail;
use deno_core::anyhow::Context;
@ -20,12 +21,14 @@ use deno_npm::resolution::SerializedNpmResolutionSnapshotPackage;
use deno_npm::resolution::ValidSerializedNpmResolutionSnapshot;
use deno_npm::NpmPackageId;
use deno_semver::package::PackageReq;
use indexmap::IndexMap;
use crate::standalone::virtual_fs::VirtualDirectory;
use super::binary::Metadata;
use super::virtual_fs::BuiltVfs;
use super::virtual_fs::VfsBuilder;
use super::virtual_fs::VirtualDirectoryEntries;
const MAGIC_BYTES: &[u8; 8] = b"d3n0l4nd";
@ -33,21 +36,22 @@ const MAGIC_BYTES: &[u8; 8] = b"d3n0l4nd";
/// * d3n0l4nd
/// * <metadata_len><metadata>
/// * <npm_snapshot_len><npm_snapshot>
/// * <remote_modules_len><remote_modules>
/// * <remote_modules>
/// * <vfs_headers_len><vfs_headers>
/// * <vfs_file_data_len><vfs_file_data>
/// * <source_map_data>
/// * d3n0l4nd
pub fn serialize_binary_data_section(
metadata: &Metadata,
npm_snapshot: Option<SerializedNpmResolutionSnapshot>,
remote_modules: &RemoteModulesStoreBuilder,
source_map_store: &SourceMapStore,
vfs: &BuiltVfs,
) -> Result<Vec<u8>, AnyError> {
let metadata = serde_json::to_string(metadata)?;
let npm_snapshot =
npm_snapshot.map(serialize_npm_snapshot).unwrap_or_default();
let remote_modules_len = Cell::new(0_u64);
let serialized_vfs = serde_json::to_string(&vfs.root)?;
let serialized_vfs = serde_json::to_string(&vfs.entries)?;
let bytes = capacity_builder::BytesBuilder::build(|builder| {
builder.append(MAGIC_BYTES);
@ -63,10 +67,7 @@ pub fn serialize_binary_data_section(
}
// 3. Remote modules
{
builder.append_le(remote_modules_len.get()); // this will be properly initialized on the second pass
let start_index = builder.len();
remote_modules.write(builder);
remote_modules_len.set((builder.len() - start_index) as u64);
}
// 4. VFS
{
@ -78,6 +79,16 @@ pub fn serialize_binary_data_section(
builder.append(file);
}
}
// 5. Source maps
{
builder.append_le(source_map_store.data.len() as u32);
for (specifier, source_map) in &source_map_store.data {
builder.append_le(specifier.len() as u32);
builder.append(specifier);
builder.append_le(source_map.len() as u32);
builder.append(source_map);
}
}
// write the magic bytes at the end so we can use it
// to make sure we've deserialized correctly
@ -91,19 +102,14 @@ pub struct DeserializedDataSection {
pub metadata: Metadata,
pub npm_snapshot: Option<ValidSerializedNpmResolutionSnapshot>,
pub remote_modules: RemoteModulesStore,
pub vfs_dir: VirtualDirectory,
pub source_maps: SourceMapStore,
pub vfs_root_entries: VirtualDirectoryEntries,
pub vfs_files_data: &'static [u8],
}
pub fn deserialize_binary_data_section(
data: &'static [u8],
) -> Result<Option<DeserializedDataSection>, AnyError> {
fn read_bytes_with_len(input: &[u8]) -> Result<(&[u8], &[u8]), AnyError> {
let (input, len) = read_u64(input)?;
let (input, data) = read_bytes(input, len as usize)?;
Ok((input, data))
}
fn read_magic_bytes(input: &[u8]) -> Result<(&[u8], bool), AnyError> {
if input.len() < MAGIC_BYTES.len() {
bail!("Unexpected end of data. Could not find magic bytes.");
@ -115,34 +121,51 @@ pub fn deserialize_binary_data_section(
Ok((input, true))
}
#[allow(clippy::type_complexity)]
fn read_source_map_entry(
input: &[u8],
) -> Result<(&[u8], (Cow<str>, Cow<str>)), AnyError> {
let (input, specifier) = read_string_lossy(input)?;
let (input, source_map) = read_string_lossy(input)?;
Ok((input, (specifier, source_map)))
}
let (input, found) = read_magic_bytes(data)?;
if !found {
return Ok(None);
}
// 1. Metadata
let (input, data) = read_bytes_with_len(input).context("reading metadata")?;
let (input, data) =
read_bytes_with_u64_len(input).context("reading metadata")?;
let metadata: Metadata =
serde_json::from_slice(data).context("deserializing metadata")?;
// 2. Npm snapshot
let (input, data) =
read_bytes_with_len(input).context("reading npm snapshot")?;
read_bytes_with_u64_len(input).context("reading npm snapshot")?;
let npm_snapshot = if data.is_empty() {
None
} else {
Some(deserialize_npm_snapshot(data).context("deserializing npm snapshot")?)
};
// 3. Remote modules
let (input, data) =
read_bytes_with_len(input).context("reading remote modules data")?;
let remote_modules =
RemoteModulesStore::build(data).context("deserializing remote modules")?;
let (input, remote_modules) =
RemoteModulesStore::build(input).context("deserializing remote modules")?;
// 4. VFS
let (input, data) = read_bytes_with_len(input).context("vfs")?;
let vfs_dir: VirtualDirectory =
let (input, data) = read_bytes_with_u64_len(input).context("vfs")?;
let vfs_root_entries: VirtualDirectoryEntries =
serde_json::from_slice(data).context("deserializing vfs data")?;
let (input, vfs_files_data) =
read_bytes_with_len(input).context("reading vfs files data")?;
read_bytes_with_u64_len(input).context("reading vfs files data")?;
// 5. Source maps
let (mut input, source_map_data_len) = read_u32_as_usize(input)?;
let mut source_maps = SourceMapStore::with_capacity(source_map_data_len);
for _ in 0..source_map_data_len {
let (current_input, (specifier, source_map)) =
read_source_map_entry(input)?;
input = current_input;
source_maps.add(specifier, source_map);
}
// finally ensure we read the magic bytes at the end
let (_input, found) = read_magic_bytes(input)?;
@ -154,7 +177,8 @@ pub fn deserialize_binary_data_section(
metadata,
npm_snapshot,
remote_modules,
vfs_dir,
source_maps,
vfs_root_entries,
vfs_files_data,
}))
}
@ -162,19 +186,31 @@ pub fn deserialize_binary_data_section(
#[derive(Default)]
pub struct RemoteModulesStoreBuilder {
specifiers: Vec<(String, u64)>,
data: Vec<(MediaType, Vec<u8>)>,
data: Vec<(MediaType, Vec<u8>, Option<Vec<u8>>)>,
data_byte_len: u64,
redirects: Vec<(String, String)>,
redirects_len: u64,
}
impl RemoteModulesStoreBuilder {
pub fn add(&mut self, specifier: &Url, media_type: MediaType, data: Vec<u8>) {
pub fn add(
&mut self,
specifier: &Url,
media_type: MediaType,
data: Vec<u8>,
maybe_transpiled: Option<Vec<u8>>,
) {
log::debug!("Adding '{}' ({})", specifier, media_type);
let specifier = specifier.to_string();
self.specifiers.push((specifier, self.data_byte_len));
self.data_byte_len += 1 + 8 + data.len() as u64; // media type (1 byte), data length (8 bytes), data
self.data.push((media_type, data));
let maybe_transpiled_len = match &maybe_transpiled {
// data length (4 bytes), data
Some(data) => 4 + data.len() as u64,
None => 0,
};
// media type (1 byte), data length (4 bytes), data, has transpiled (1 byte), transpiled length
self.data_byte_len += 1 + 4 + data.len() as u64 + 1 + maybe_transpiled_len;
self.data.push((media_type, data, maybe_transpiled));
}
pub fn add_redirects(&mut self, redirects: &BTreeMap<Url, Url>) {
@ -193,7 +229,7 @@ impl RemoteModulesStoreBuilder {
builder.append_le(self.redirects.len() as u32);
for (specifier, offset) in &self.specifiers {
builder.append_le(specifier.len() as u32);
builder.append(specifier.as_bytes());
builder.append(specifier);
builder.append_le(*offset);
}
for (from, to) in &self.redirects {
@ -202,10 +238,32 @@ impl RemoteModulesStoreBuilder {
builder.append_le(to.len() as u32);
builder.append(to);
}
for (media_type, data) in &self.data {
builder.append_le(
self
.data
.iter()
.map(|(_, data, maybe_transpiled)| {
1 + 4
+ (data.len() as u64)
+ 1
+ match maybe_transpiled {
Some(transpiled) => 4 + (transpiled.len() as u64),
None => 0,
}
})
.sum::<u64>(),
);
for (media_type, data, maybe_transpiled) in &self.data {
builder.append(serialize_media_type(*media_type));
builder.append_le(data.len() as u64);
builder.append_le(data.len() as u32);
builder.append(data);
if let Some(transpiled) = maybe_transpiled {
builder.append(1);
builder.append_le(transpiled.len() as u32);
builder.append(transpiled);
} else {
builder.append(0);
}
}
}
}
@ -234,6 +292,30 @@ impl DenoCompileModuleSource {
}
}
pub struct SourceMapStore {
data: IndexMap<Cow<'static, str>, Cow<'static, str>>,
}
impl SourceMapStore {
pub fn with_capacity(capacity: usize) -> Self {
Self {
data: IndexMap::with_capacity(capacity),
}
}
pub fn add(
&mut self,
specifier: Cow<'static, str>,
source_map: Cow<'static, str>,
) {
self.data.insert(specifier, source_map);
}
pub fn get(&self, specifier: &str) -> Option<&Cow<'static, str>> {
self.data.get(specifier)
}
}
pub struct DenoCompileModuleData<'a> {
pub specifier: &'a Url,
pub media_type: MediaType,
@ -280,6 +362,13 @@ impl<'a> DenoCompileModuleData<'a> {
}
}
pub struct RemoteModuleEntry<'a> {
pub specifier: &'a Url,
pub media_type: MediaType,
pub data: Cow<'static, [u8]>,
pub transpiled_data: Option<Cow<'static, [u8]>>,
}
enum RemoteModulesStoreSpecifierValue {
Data(usize),
Redirect(Url),
@ -291,7 +380,7 @@ pub struct RemoteModulesStore {
}
impl RemoteModulesStore {
fn build(data: &'static [u8]) -> Result<Self, AnyError> {
fn build(input: &'static [u8]) -> Result<(&'static [u8], Self), AnyError> {
fn read_specifier(input: &[u8]) -> Result<(&[u8], (Url, u64)), AnyError> {
let (input, specifier) = read_string_lossy(input)?;
let specifier = Url::parse(&specifier)?;
@ -334,12 +423,16 @@ impl RemoteModulesStore {
Ok((input, specifiers))
}
let (files_data, specifiers) = read_headers(data)?;
let (input, specifiers) = read_headers(input)?;
let (input, files_data) = read_bytes_with_u64_len(input)?;
Ok(Self {
Ok((
input,
Self {
specifiers,
files_data,
})
},
))
}
pub fn resolve_specifier<'a>(
@ -370,7 +463,7 @@ impl RemoteModulesStore {
pub fn read<'a>(
&'a self,
original_specifier: &'a Url,
) -> Result<Option<DenoCompileModuleData<'a>>, AnyError> {
) -> Result<Option<RemoteModuleEntry<'a>>, AnyError> {
let mut count = 0;
let mut specifier = original_specifier;
loop {
@ -386,12 +479,25 @@ impl RemoteModulesStore {
let input = &self.files_data[*offset..];
let (input, media_type_byte) = read_bytes(input, 1)?;
let media_type = deserialize_media_type(media_type_byte[0])?;
let (input, len) = read_u64(input)?;
let (_input, data) = read_bytes(input, len as usize)?;
return Ok(Some(DenoCompileModuleData {
let (input, data) = read_bytes_with_u32_len(input)?;
check_has_len(input, 1)?;
let (input, has_transpiled) = (&input[1..], input[0]);
let (_, transpiled_data) = match has_transpiled {
0 => (input, None),
1 => {
let (input, data) = read_bytes_with_u32_len(input)?;
(input, Some(data))
}
value => bail!(
"Invalid transpiled data flag: {}. Compiled data is corrupt.",
value
),
};
return Ok(Some(RemoteModuleEntry {
specifier,
media_type,
data: Cow::Borrowed(data),
transpiled_data: transpiled_data.map(Cow::Borrowed),
}));
}
None => {
@ -630,14 +736,32 @@ fn parse_vec_n_times_with_index<TResult>(
Ok((input, results))
}
fn read_bytes(input: &[u8], len: usize) -> Result<(&[u8], &[u8]), AnyError> {
if input.len() < len {
bail!("Unexpected end of data.",);
fn read_bytes_with_u64_len(input: &[u8]) -> Result<(&[u8], &[u8]), AnyError> {
let (input, len) = read_u64(input)?;
let (input, data) = read_bytes(input, len as usize)?;
Ok((input, data))
}
fn read_bytes_with_u32_len(input: &[u8]) -> Result<(&[u8], &[u8]), AnyError> {
let (input, len) = read_u32_as_usize(input)?;
let (input, data) = read_bytes(input, len)?;
Ok((input, data))
}
fn read_bytes(input: &[u8], len: usize) -> Result<(&[u8], &[u8]), AnyError> {
check_has_len(input, len)?;
let (len_bytes, input) = input.split_at(len);
Ok((input, len_bytes))
}
#[inline(always)]
fn check_has_len(input: &[u8], len: usize) -> Result<(), AnyError> {
if input.len() < len {
bail!("Unexpected end of data.");
}
Ok(())
}
fn read_string_lossy(input: &[u8]) -> Result<(&[u8], Cow<str>), AnyError> {
let (input, str_len) = read_u32_as_usize(input)?;
let (input, data_bytes) = read_bytes(input, str_len)?;

View file

@ -67,7 +67,7 @@ impl WindowsSystemRootablePath {
#[derive(Debug)]
pub struct BuiltVfs {
pub root_path: WindowsSystemRootablePath,
pub root: VirtualDirectory,
pub entries: VirtualDirectoryEntries,
pub files: Vec<Vec<u8>>,
}
@ -95,7 +95,7 @@ impl VfsBuilder {
Self {
executable_root: VirtualDirectory {
name: "/".to_string(),
entries: Vec::new(),
entries: Default::default(),
},
files: Vec::new(),
current_offset: 0,
@ -208,23 +208,20 @@ impl VfsBuilder {
continue;
}
let name = component.as_os_str().to_string_lossy();
let index = match current_dir
.entries
.binary_search_by(|e| e.name().cmp(&name))
{
let index = match current_dir.entries.binary_search(&name) {
Ok(index) => index,
Err(insert_index) => {
current_dir.entries.insert(
current_dir.entries.0.insert(
insert_index,
VfsEntry::Dir(VirtualDirectory {
name: name.to_string(),
entries: Vec::new(),
entries: Default::default(),
}),
);
insert_index
}
};
match &mut current_dir.entries[index] {
match &mut current_dir.entries.0[index] {
VfsEntry::Dir(dir) => {
current_dir = dir;
}
@ -248,14 +245,8 @@ impl VfsBuilder {
continue;
}
let name = component.as_os_str().to_string_lossy();
let index = match current_dir
.entries
.binary_search_by(|e| e.name().cmp(&name))
{
Ok(index) => index,
Err(_) => return None,
};
match &mut current_dir.entries[index] {
let entry = current_dir.entries.get_mut_by_name(&name)?;
match entry {
VfsEntry::Dir(dir) => {
current_dir = dir;
}
@ -320,9 +311,9 @@ impl VfsBuilder {
offset,
len: data.len() as u64,
};
match dir.entries.binary_search_by(|e| e.name().cmp(&name)) {
match dir.entries.binary_search(&name) {
Ok(index) => {
let entry = &mut dir.entries[index];
let entry = &mut dir.entries.0[index];
match entry {
VfsEntry::File(virtual_file) => match sub_data_kind {
VfsFileSubDataKind::Raw => {
@ -336,7 +327,7 @@ impl VfsBuilder {
}
}
Err(insert_index) => {
dir.entries.insert(
dir.entries.0.insert(
insert_index,
VfsEntry::File(VirtualFile {
name: name.to_string(),
@ -384,10 +375,10 @@ impl VfsBuilder {
let target = normalize_path(path.parent().unwrap().join(&target));
let dir = self.add_dir_raw(path.parent().unwrap());
let name = path.file_name().unwrap().to_string_lossy();
match dir.entries.binary_search_by(|e| e.name().cmp(&name)) {
match dir.entries.binary_search(&name) {
Ok(_) => {} // previously inserted
Err(insert_index) => {
dir.entries.insert(
dir.entries.0.insert(
insert_index,
VfsEntry::Symlink(VirtualSymlink {
name: name.to_string(),
@ -426,7 +417,7 @@ impl VfsBuilder {
dir: &mut VirtualDirectory,
parts: &[String],
) {
for entry in &mut dir.entries {
for entry in &mut dir.entries.0 {
match entry {
VfsEntry::Dir(dir) => {
strip_prefix_from_symlinks(dir, parts);
@ -454,13 +445,13 @@ impl VfsBuilder {
if self.min_root_dir.as_ref() == Some(&current_path) {
break;
}
match &current_dir.entries[0] {
match &current_dir.entries.0[0] {
VfsEntry::Dir(dir) => {
if dir.name == DENO_COMPILE_GLOBAL_NODE_MODULES_DIR_NAME {
// special directory we want to maintain
break;
}
match current_dir.entries.remove(0) {
match current_dir.entries.0.remove(0) {
VfsEntry::Dir(dir) => {
current_path =
WindowsSystemRootablePath::Path(current_path.join(&dir.name));
@ -480,7 +471,7 @@ impl VfsBuilder {
}
BuiltVfs {
root_path: current_path,
root: current_dir,
entries: current_dir.entries,
files: self.files,
}
}
@ -506,7 +497,7 @@ pub fn output_vfs(vfs: &BuiltVfs, executable_name: &str) {
return; // no need to compute if won't output
}
if vfs.root.entries.is_empty() {
if vfs.entries.is_empty() {
return; // nothing to output
}
@ -696,7 +687,7 @@ fn vfs_as_display_tree(
fn dir_size(dir: &VirtualDirectory, seen_offsets: &mut HashSet<u64>) -> Size {
let mut size = Size::default();
for entry in &dir.entries {
for entry in dir.entries.iter() {
match entry {
VfsEntry::Dir(virtual_directory) => {
size = size + dir_size(virtual_directory, seen_offsets);
@ -760,15 +751,10 @@ fn vfs_as_display_tree(
fn include_all_entries<'a>(
dir_path: &WindowsSystemRootablePath,
vfs_dir: &'a VirtualDirectory,
entries: &'a VirtualDirectoryEntries,
seen_offsets: &mut HashSet<u64>,
) -> Vec<DirEntryOutput<'a>> {
if vfs_dir.name == DENO_COMPILE_GLOBAL_NODE_MODULES_DIR_NAME {
return show_global_node_modules_dir(vfs_dir, seen_offsets);
}
vfs_dir
.entries
entries
.iter()
.map(|entry| DirEntryOutput {
name: Cow::Borrowed(entry.name()),
@ -826,10 +812,12 @@ fn vfs_as_display_tree(
} else {
EntryOutput::Subset(children)
}
} else if vfs_dir.name == DENO_COMPILE_GLOBAL_NODE_MODULES_DIR_NAME {
EntryOutput::Subset(show_global_node_modules_dir(vfs_dir, seen_offsets))
} else {
EntryOutput::Subset(include_all_entries(
&WindowsSystemRootablePath::Path(dir),
vfs_dir,
&vfs_dir.entries,
seen_offsets,
))
}
@ -839,7 +827,7 @@ fn vfs_as_display_tree(
// user might not have context about what's being shown
let mut seen_offsets = HashSet::with_capacity(vfs.files.len());
let mut child_entries =
include_all_entries(&vfs.root_path, &vfs.root, &mut seen_offsets);
include_all_entries(&vfs.root_path, &vfs.entries, &mut seen_offsets);
for child_entry in &mut child_entries {
child_entry.collapse_leaf_nodes();
}
@ -961,27 +949,70 @@ impl VfsEntry {
}
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct VirtualDirectoryEntries(Vec<VfsEntry>);
impl VirtualDirectoryEntries {
pub fn new(mut entries: Vec<VfsEntry>) -> Self {
// needs to be sorted by name
entries.sort_by(|a, b| a.name().cmp(b.name()));
Self(entries)
}
pub fn take_inner(&mut self) -> Vec<VfsEntry> {
std::mem::take(&mut self.0)
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn get_by_name(&self, name: &str) -> Option<&VfsEntry> {
self.binary_search(name).ok().map(|index| &self.0[index])
}
pub fn get_mut_by_name(&mut self, name: &str) -> Option<&mut VfsEntry> {
self
.binary_search(name)
.ok()
.map(|index| &mut self.0[index])
}
pub fn binary_search(&self, name: &str) -> Result<usize, usize> {
self.0.binary_search_by(|e| e.name().cmp(name))
}
pub fn insert(&mut self, entry: VfsEntry) {
match self.binary_search(entry.name()) {
Ok(index) => {
self.0[index] = entry;
}
Err(insert_index) => {
self.0.insert(insert_index, entry);
}
}
}
pub fn remove(&mut self, index: usize) -> VfsEntry {
self.0.remove(index)
}
pub fn iter(&self) -> std::slice::Iter<'_, VfsEntry> {
self.0.iter()
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct VirtualDirectory {
#[serde(rename = "n")]
pub name: String,
// should be sorted by name
#[serde(rename = "e")]
pub entries: Vec<VfsEntry>,
}
impl VirtualDirectory {
pub fn insert_entry(&mut self, entry: VfsEntry) {
let name = entry.name();
match self.entries.binary_search_by(|e| e.name().cmp(name)) {
Ok(index) => {
self.entries[index] = entry;
}
Err(insert_index) => {
self.entries.insert(insert_index, entry);
}
}
}
pub entries: VirtualDirectoryEntries,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
@ -1136,20 +1167,13 @@ impl VfsRoot {
}
};
let component = component.to_string_lossy();
match current_dir
current_entry = current_dir
.entries
.binary_search_by(|e| e.name().cmp(&component))
{
Ok(index) => {
current_entry = current_dir.entries[index].as_ref();
}
Err(_) => {
return Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"path not found",
));
}
}
.get_by_name(&component)
.ok_or_else(|| {
std::io::Error::new(std::io::ErrorKind::NotFound, "path not found")
})?
.as_ref();
}
Ok((final_path, current_entry))
@ -1706,7 +1730,10 @@ mod test {
FileBackedVfs::new(
Cow::Owned(data),
VfsRoot {
dir: vfs.root,
dir: VirtualDirectory {
name: "".to_string(),
entries: vfs.entries,
},
root_path: dest_path.to_path_buf(),
start_file_offset: 0,
},

View file

@ -2,7 +2,6 @@
use deno_core::serde_json;
use test_util as util;
use util::assert_contains;
use util::assert_not_contains;
use util::testdata_path;
use util::TestContext;
@ -90,78 +89,6 @@ fn standalone_args() {
.assert_exit_code(0);
}
#[test]
fn standalone_error() {
let context = TestContextBuilder::new().build();
let dir = context.temp_dir();
let exe = if cfg!(windows) {
dir.path().join("error.exe")
} else {
dir.path().join("error")
};
context
.new_command()
.args_vec([
"compile",
"--output",
&exe.to_string_lossy(),
"./compile/standalone_error.ts",
])
.run()
.skip_output_check()
.assert_exit_code(0);
let output = context.new_command().name(&exe).split_output().run();
output.assert_exit_code(1);
output.assert_stdout_matches_text("");
let stderr = output.stderr();
// On Windows, we cannot assert the file path (because '\').
// Instead we just check for relevant output.
assert_contains!(stderr, "error: Uncaught (in promise) Error: boom!");
assert_contains!(stderr, "\n at boom (file://");
assert_contains!(stderr, "standalone_error.ts:2:9");
assert_contains!(stderr, "at foo (file://");
assert_contains!(stderr, "standalone_error.ts:5:3");
assert_contains!(stderr, "standalone_error.ts:7:1");
}
#[test]
fn standalone_error_module_with_imports() {
let context = TestContextBuilder::new().build();
let dir = context.temp_dir();
let exe = if cfg!(windows) {
dir.path().join("error.exe")
} else {
dir.path().join("error")
};
context
.new_command()
.args_vec([
"compile",
"--output",
&exe.to_string_lossy(),
"./compile/standalone_error_module_with_imports_1.ts",
])
.run()
.skip_output_check()
.assert_exit_code(0);
let output = context
.new_command()
.name(&exe)
.env("NO_COLOR", "1")
.split_output()
.run();
output.assert_stdout_matches_text("hello\n");
let stderr = output.stderr();
// On Windows, we cannot assert the file path (because '\').
// Instead we just check for relevant output.
assert_contains!(stderr, "error: Uncaught (in promise) Error: boom!");
assert_contains!(stderr, "\n at file://");
assert_contains!(stderr, "standalone_error_module_with_imports_2.ts:2:7");
output.assert_exit_code(1);
}
#[test]
fn standalone_load_datauri() {
let context = TestContextBuilder::new().build();

View file

@ -1,6 +1,9 @@
{
"tempDir": true,
"steps": [{
"args": "run -A cleanup.ts",
"output": "[WILDCARD]"
}, {
"if": "unix",
"args": "compile --output using_code_cache --log-level=debug main.ts",
"output": "[WILDCARD]"

View file

@ -0,0 +1,11 @@
import { tmpdir } from "node:os";
// cleanup the code cache file from a previous run
try {
if (Deno.build.os === "windows") {
Deno.removeSync(tmpdir() + "\\deno-compile-using_code_cache.exe.cache");
} else {
Deno.removeSync(tmpdir() + "\\deno-compile-using_code_cache.cache");
}
} catch {
}

View file

@ -1,28 +1,31 @@
{
"tempDir": true,
"steps": [{
"if": "unix",
"args": "compile --output main1 main.ts",
"args": "run -A setup.ts",
"output": "[WILDCARD]"
}, {
"if": "unix",
"args": "compile --output main2 main.ts",
"args": "compile --no-config --output a/main a/main.ts",
"output": "[WILDCARD]"
}, {
"if": "unix",
"args": "run --allow-read=. assert_equal.ts main1 main2",
"args": "compile --no-config --output b/main b/main.ts",
"output": "[WILDCARD]"
}, {
"if": "unix",
"args": "run --allow-read=. assert_equal.ts a/main b/main",
"output": "Same\n"
}, {
"if": "windows",
"args": "compile --output main1.exe main.ts",
"args": "compile --no-config --output a/main.exe a/main.ts",
"output": "[WILDCARD]"
}, {
"if": "windows",
"args": "compile --output main2.exe main.ts",
"args": "compile --no-config --output b/main.exe b/main.ts",
"output": "[WILDCARD]"
}, {
"if": "windows",
"args": "run --allow-read=. assert_equal.ts main1.exe main2.exe",
"args": "run --allow-read=. assert_equal.ts a/main.exe b/main.exe",
"output": "Same\n"
}]
}

View file

@ -0,0 +1,10 @@
// for setup, we create two directories with the same file in each
// and then when compiling we ensure this directory name has no
// effect on the output
makeCopyDir("a");
makeCopyDir("b");
function makeCopyDir(dirName) {
Deno.mkdirSync(dirName);
Deno.copyFileSync("main.ts", `${dirName}/main.ts`);
}

View file

@ -0,0 +1,24 @@
{
"tempDir": true,
"steps": [{
"if": "unix",
"args": "compile --output main standalone_error.ts",
"output": "[WILDCARD]"
}, {
"if": "unix",
"commandName": "./main",
"args": [],
"output": "output.out",
"exitCode": 1
}, {
"if": "windows",
"args": "compile --output main.exe standalone_error.ts",
"output": "[WILDCARD]"
}, {
"if": "windows",
"commandName": "./main.exe",
"args": [],
"output": "output.out",
"exitCode": 1
}]
}

View file

@ -0,0 +1,6 @@
error: Uncaught (in promise) Error: boom!
throw new Error("boom!");
^
at boom (file:///[WILDLINE]standalone_error.ts:2:9)
at foo (file:///[WILDLINE]standalone_error.ts:6:3)
at file:///[WILDLINE]standalone_error.ts:9:1

View file

@ -0,0 +1,24 @@
{
"tempDir": true,
"steps": [{
"if": "unix",
"args": "compile -A --output main main.ts",
"output": "[WILDCARD]"
}, {
"if": "unix",
"commandName": "./main",
"args": [],
"output": "output.out",
"exitCode": 1
}, {
"if": "windows",
"args": "compile -A --output main.exe main.ts",
"output": "[WILDCARD]"
}, {
"if": "windows",
"commandName": "./main.exe",
"args": [],
"output": "output.out",
"exitCode": 1
}]
}

View file

@ -0,0 +1 @@
import "http://localhost:4545/compile/standalone_error_module_with_imports_1.ts";

View file

@ -0,0 +1,5 @@
hello
error: Uncaught (in promise) Error: boom!
throw new Error(value);
^
at http://localhost:4545/compile/standalone_error_module_with_imports_2.ts:7:7

View file

@ -1,2 +1,7 @@
// file has blank lines to make the input line
// different than the output
console.log("hello");
throw new Error("boom!");
const value: string = "boom!";
throw new Error(value);