Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/snippets/cli/compile.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ Builds [a package to publish](/compiler/libraries#publish-a-package-with-its-typ
</ParamField>

<ParamField path="--outdir" type="string" default="dist">
The folder to write the package to. It cannot be the project's own folder
The folder to write the package to. It cannot be the project's own folder, a folder that holds it, or a folder that
holds an entry point
</ParamField>

<ParamField path="--target" type="string" default="node">
Expand Down
20 changes: 17 additions & 3 deletions src/runtime/cli/build_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,14 @@ impl BuildCommand {
ctx.args.target = Some(api::Target::Bun);
}

// `--app` ignores `--library`, except for the entry points that it finds.
let mut library = None;
if ctx.bundler_options.library
&& (ctx.args.entry_points.is_empty() || !ctx.bundler_options.bake)
{
library = Some(super::compile_command::Library::find(ctx));
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}

if ctx.bundler_options.bake {
return crate::bake::production::build_command(ctx);
}
Expand All @@ -86,8 +94,8 @@ impl BuildCommand {
ctx.bundler_options.compile = false;
}

if ctx.bundler_options.library {
super::compile_command::configure_library(ctx);
if let Some(library) = &mut library {
library.configure(ctx);
}

if ctx.bundler_options.compile && ctx.bundler_options.aot.is_some() {
Expand Down Expand Up @@ -540,6 +548,11 @@ impl BuildCommand {
break 'brk1 &*result;
};

if let Some(library) = &library {
let entry_points = &this_transpiler.options.entry_points;
library.check_root_directory(ctx, src_root_dir, entry_points);
}

this_transpiler.options.root_dir = src_root_dir.into();
this_transpiler.options.code_splitting = ctx.bundler_options.code_splitting;
this_transpiler.options.transform_only = ctx.bundler_options.transform_only;
Expand Down Expand Up @@ -841,8 +854,9 @@ impl BuildCommand {
super::compile_command::add_typed_files(&request, log_ref, &mut output_files);
}

if ctx.bundler_options.library {
if let Some(library) = &library {
super::compile_command::add_package_files(
library,
ctx,
src_root_dir,
&compiler_options,
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/cli/compile_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ pub(crate) use self::build::compile_to_executable;
pub(crate) use self::command::compile_from_context;
pub(crate) use self::configuration::DEFAULT_LIBRARY_OUTDIR;
pub(crate) use self::library::{
TypedOutputs, add_declaration_files, add_package_files, add_typed_files, configure_library,
Library, TypedOutputs, add_declaration_files, add_package_files, add_typed_files,
is_declaration_file, is_typed_file,
};
pub(crate) use self::printing::{
Expand Down
10 changes: 2 additions & 8 deletions src/runtime/cli/compile_command/configuration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ use core::ptr::NonNull;
use std::io::Write as _;

use super::entry_points::find_default_entry_point;
use super::library::find_default_library_entry_points;
use super::{BundlerInputs, CompileProduct, CompileRequest, ExecutableSettings, RequestOrigin};
pub(super) use crate::cli::build_command::compile_outfile;
use crate::cli::command::Context;
Expand Down Expand Up @@ -42,20 +41,15 @@ struct ExecutableName {
}

pub(super) fn use_default_entry_point(ctx: &mut ContextData) {
if !ctx.args.entry_points.is_empty() {
// `Library::find` finds the entry points of a library.
if !ctx.args.entry_points.is_empty() || ctx.bundler_options.library {
return;
}
let working_directory = ctx
.args
.absolute_working_dir
.as_deref()
.expect("Arguments::parse sets the working directory");
if ctx.bundler_options.library {
let found =
find_default_library_entry_points(working_directory, &ctx.bundler_options.outdir);
ctx.args.entry_points = found.unwrap_or_else(|error| error.exit());
return;
}
match find_default_entry_point(working_directory) {
Ok(found) => {
ctx.args.entry_points = vec![found.path];
Expand Down
240 changes: 178 additions & 62 deletions src/runtime/cli/compile_command/library.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use bun_core::{Global, Output};
use bun_options_types::context::ContextData;
use bun_paths::platform::Auto;
use bun_paths::resolve_path::join_abs_string;
use bun_paths::string_paths::without_trailing_slash_windows_path;

pub(crate) use self::declarations::{add_declaration_files, is_declaration_file};
pub(crate) use self::package_files::add_package_files;
Expand All @@ -26,6 +27,7 @@ const EXTERNAL_PACKAGE_FIELDS: [&[u8]; 3] = [
#[derive(Debug, PartialEq, Eq)]
pub(super) enum LibraryError {
OutdirHoldsProject { outdir: Vec<u8> },
OutdirLinksPackageJson { written: Vec<u8> },
EntryPointInsideOutdir { entry_point: Vec<u8> },
UnparsablePackageJson { path: Vec<u8> },
NoEntryPoint { directory: Vec<u8> },
Expand All @@ -40,6 +42,11 @@ impl LibraryError {
outdir,
b"). Use a folder inside it, such as --outdir=dist",
]),
Self::OutdirLinksPackageJson { written } => write(&[
b"--library writes a package.json into --outdir, but ",
written,
b" is a link to the project's own package.json. Choose another --outdir",
]),
Self::EntryPointInsideOutdir { entry_point } => write(&[
b"the entry point ",
entry_point,
Expand Down Expand Up @@ -111,78 +118,187 @@ fn absolute_path(working_directory: &[u8], path: &[u8]) -> Vec<u8> {
join_abs_string::<Auto>(working_directory, &[path]).to_vec()
}

fn is_inside(ancestor: &[u8], path: &[u8]) -> bool {
let below = path.strip_prefix(ancestor);
below.is_some_and(|below| {
below
.first()
.is_some_and(|&byte| bun_paths::is_sep_any(byte))
})
// `path` is `folder` or is inside it. Case is not ignored: the real path settles a name in another case.
fn is_or_holds(folder: &[u8], path: &[u8]) -> bool {
// Only a root has a separator at its end.
let folder = folder.strip_suffix(&[bun_paths::SEP]).unwrap_or(folder);
match path.strip_prefix(folder) {
Some([]) => true,
Some([first, ..]) => bun_paths::is_sep_native(*first),
None => false,
}
}

fn check_outdir(project: &Project, outdir: &[u8]) -> Result<(), LibraryError> {
if project.directory == outdir || is_inside(outdir, &project.directory) {
return Err(LibraryError::OutdirHoldsProject {
outdir: outdir.to_vec(),
});
}
Ok(())
fn real_path(directory: &[u8]) -> Option<Vec<u8>> {
let opened = bun_sys::Dir::open(directory).ok()?;
let mut buffer = bun_paths::path_buffer_pool::get();
Some(opened.get_fd_path(&mut buffer).ok()?.to_vec())
}

struct Outdir {
// `--outdir` on the working directory, with `.` and `..` resolved and no separator at its end.
folder: Vec<u8>,
// Other paths of `folder`: its real path, and its path below the project folder as that is spelled.
aliases: Vec<Vec<u8>>,
as_written: Vec<u8>,
}

pub(super) fn find_default_library_entry_points(
working_directory: &[u8],
outdir: &[u8],
) -> Result<Vec<Box<[u8]>>, LibraryError> {
let project = Project::find(working_directory);
let outdir = absolute_path(working_directory, outdir);
check_outdir(&project, &outdir)?;
let arena = Arena::new();
let package_json = project.parse_package_json(&arena)?;
let found = entry_points::find_entry_points(&project.directory, package_json.as_ref(), &outdir);
if found.is_empty() {
return Err(LibraryError::NoEntryPoint {
directory: project.directory,
});
impl Outdir {
fn find(
working_directory: &[u8],
as_written: &[u8],
project: &Project,
) -> Result<Outdir, LibraryError> {
let folder = join_abs_string::<Auto>(working_directory, &[as_written]);
let mut outdir = Outdir {
folder: without_trailing_slash_windows_path(folder).to_vec(),
aliases: Vec::new(),
as_written: as_written.to_vec(),
};
if outdir.holds(&project.directory) {
return Err(LibraryError::OutdirHoldsProject {
outdir: outdir.folder,
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}
let Ok(opened) = bun_sys::Dir::open(&outdir.folder) else {
return Ok(outdir);
};
let mut buffer = bun_paths::path_buffer_pool::get();
if let Ok(real) = opened.get_fd_path(&mut buffer) {
let real: &[u8] = real;
// A working directory from `getcwd()` has no link in it. On Windows it is kept as it was spelled.
let project_real = match cfg!(windows) {
true => real_path(&project.directory),
false => None,
};
let project_directory = project_real.as_deref().unwrap_or(&project.directory);
if is_or_holds(real, project_directory) {
return Err(LibraryError::OutdirHoldsProject {
outdir: real.to_vec(),
});
}
if real != outdir.folder {
outdir.aliases.push(real.to_vec());
}
if let Some(project_real) = &project_real
&& project_real[..] != project.directory[..]
&& let Some([separator, below @ ..]) = real.strip_prefix(&project_real[..])
&& bun_paths::is_sep_native(*separator)
{
outdir
.aliases
.push(absolute_path(&project.directory, below));
}
}
if let Some(package_json) = &project.package_json
&& let Ok(written) = bun_sys::fstatat(opened.fd, bun_core::zstr!("package.json"))
&& let Ok(own) = bun_sys::stat(&bun_core::ZBox::from_bytes(&package_json.path))
// A file system without inode numbers reports `st_ino == 0`.
&& written.st_ino != 0
&& written.st_ino == own.st_ino
&& written.st_dev == own.st_dev
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
Comment thread
robobun marked this conversation as resolved.
Resolved
{
let written = [&outdir.folder[..], &[bun_paths::SEP], b"package.json"].concat();
return Err(LibraryError::OutdirLinksPackageJson { written });
}
Comment thread
robobun marked this conversation as resolved.
Resolved
Ok(outdir)
}

fn holds(&self, path: &[u8]) -> bool {
let holds = |folder: &[u8]| is_or_holds(folder, path);
holds(&self.folder) || self.aliases.iter().any(|alias| holds(alias))
}
Ok(found)
}

fn find_external_packages(
working_directory: &[u8],
outdir: &[u8],
entry_points: &[Box<[u8]>],
) -> Result<Vec<Box<[u8]>>, LibraryError> {
let project = Project::find(working_directory);
let outdir = absolute_path(working_directory, outdir);
check_outdir(&project, &outdir)?;
let is_overwritten =
|entry_point: &[u8]| is_inside(&outdir, &absolute_path(working_directory, entry_point));
if let Some(entry_point) = entry_points.iter().find(|it| is_overwritten(it)) {
return Err(LibraryError::EntryPointInsideOutdir {
entry_point: entry_point.to_vec(),
});
pub(crate) struct Library {
project: Project,
outdir: Outdir,
external_packages: Vec<Box<[u8]>>,
}

impl Library {
// Finds the entry points too, when `ctx` has none.
#[cold]
pub(crate) fn find(ctx: &mut ContextData) -> Library {
let working_directory = ctx.args.absolute_working_dir.as_deref().unwrap_or_default();
let found = Library::try_find(
working_directory,
&ctx.bundler_options.outdir,
&mut ctx.args.entry_points,
);
found.unwrap_or_else(|error| error.exit())
}
let arena = Arena::new();
let mut packages: Vec<Box<[u8]>> = Vec::new();
if let Some(package_json) = project.parse_package_json(&arena)? {
for field in EXTERNAL_PACKAGE_FIELDS {
if let Some(dependencies) = package_json.get(field) {
dependencies.for_each_property(|name, _, _| packages.push(name.into()));

fn try_find(
working_directory: &[u8],
outdir: &[u8],
entry_points: &mut Vec<Box<[u8]>>,
) -> Result<Library, LibraryError> {
let project = Project::find(working_directory);
let outdir = Outdir::find(working_directory, outdir, &project)?;
let arena = Arena::new();
let package_json = if entry_points.is_empty() {
let package_json = project.parse_package_json(&arena)?;
*entry_points =
entry_points::find_entry_points(&project.directory, package_json.as_ref(), &outdir);
if entry_points.is_empty() {
return Err(LibraryError::NoEntryPoint {
directory: project.directory,
});
}
package_json
} else {
let is_overwritten =
|entry_point: &[u8]| outdir.holds(&absolute_path(working_directory, entry_point));
if let Some(entry_point) = entry_points.iter().find(|it| is_overwritten(it)) {
return Err(LibraryError::EntryPointInsideOutdir {
entry_point: entry_point.to_vec(),
});
}
project.parse_package_json(&arena)?
};
let mut external_packages: Vec<Box<[u8]>> = Vec::new();
if let Some(package_json) = package_json {
for field in EXTERNAL_PACKAGE_FIELDS {
if let Some(dependencies) = package_json.get(field) {
dependencies
.for_each_property(|name, _, _| external_packages.push(name.into()));
}
}
}
Ok(Library {
project,
outdir,
external_packages,
})
}
Ok(packages)
}

#[cold]
pub(crate) fn configure_library(ctx: &mut ContextData) {
let working_directory = ctx.args.absolute_working_dir.as_deref().unwrap_or_default();
let packages = find_external_packages(
working_directory,
&ctx.bundler_options.outdir,
&ctx.args.entry_points,
);
ctx.args
.external
.extend(packages.unwrap_or_else(|error| error.exit()));
// The build leaves the project's dependencies as imports and writes into the folder that was checked.
pub(crate) fn configure(&mut self, ctx: &mut ContextData) {
ctx.args.external.append(&mut self.external_packages);
ctx.bundler_options.outdir = self.outdir.folder.as_slice().into();
}

// An output lands on an entry point only if `--outdir` holds the root directory, which the build resolved.
#[cold]
pub(crate) fn check_root_directory(
&self,
ctx: &ContextData,
root_directory: &[u8],
entry_points: &[Box<[u8]>],
) {
if !self.outdir.holds(root_directory) {
return;
}
let working_directory = ctx.args.absolute_working_dir.as_deref().unwrap_or_default();
let is_inside = |entry_point: &[u8]| {
let path = absolute_path(working_directory, entry_point);
let real = real_path(&path).or_else(|| real_path(bun_paths::dirname(&path)?));
real.is_some_and(|real| self.outdir.holds(&real))
};
if let Some(entry_point) = entry_points.iter().find(|it| is_inside(it)) {
let entry_point = entry_point.to_vec();
LibraryError::EntryPointInsideOutdir { entry_point }.exit();
}
}
}
6 changes: 3 additions & 3 deletions src/runtime/cli/compile_command/library/entry_points.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use bun_ast::Expr;
use bun_paths::platform::Auto;
use bun_paths::resolve_path::join_abs_string_checked;

use super::is_inside;
use super::Outdir;

const TYPESCRIPT_EXTENSIONS: [&[u8]; 3] = [b".ts", b".tsx", b".mts"];
const DECLARATION_EXTENSIONS: [&[u8]; 2] = [b".d.ts", b".d.mts"];
Expand All @@ -20,10 +20,10 @@ const JAVASCRIPT_INDEX_FILES: [&[u8]; 4] =
pub(super) fn find_entry_points(
directory: &[u8],
package_json: Option<&Expr>,
outdir: &[u8],
outdir: &Outdir,
) -> Vec<Box<[u8]>> {
let find_source =
|written: &[u8]| find_file(directory, written).filter(|path| !is_inside(outdir, path));
|written: &[u8]| find_file(directory, written).filter(|path| !outdir.holds(path));

let mut targets = Vec::new();
if let Some(exports) = package_json.and_then(|root| root.get(b"exports")) {
Expand Down
Loading
Loading