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: 3 additions & 0 deletions src/js/internal-for-testing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,9 @@ export const npm_manifest_test_helpers = $rust("npm.rs", "PackageManifest.bindin

export const readTarball: (tarball: string) => any = $newRustFunction("pack_command.rs", "bindings.jsReadTarball", 1);

/** `undefined` if `bun <arg>` serves the page at the path `arg`, else the pages that match the pattern `arg`. */
export const htmlPagesOf = $bindgen("html_pages.bind.ts", "pagesOf");

export const isArchitectureMatch: (architecture: string[]) => boolean = $newRustFunction(
"npm.rs",
"Architecture.jsFunctionArchitectureIsMatch",
Expand Down
12 changes: 7 additions & 5 deletions src/js/internal/html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ const {
printBannerAndHandleShortcuts,
} = require("internal/serve_cli");

// `undefined` if `arg` is the path of a page, else the pages that match it.
const pagesOf = $bindgen("html_pages.bind.ts", "pagesOf");

const env = Bun.env;

// This function is called at startup.
Expand Down Expand Up @@ -61,11 +64,10 @@ yourself with Bun.serve().
continue;
}

if (arg.includes("*") || arg.includes("**") || arg.includes("{")) {
const glob = new Bun.Glob(arg);

for (const file of glob.scanSync(cwd)) {
let resolved = path.resolve(cwd, file);
const matches = pagesOf(arg, cwd);
if (matches !== undefined) {
for (let j = 0, matchCount = matches.length; j < matchCount; j++) {
let resolved = path.resolve(cwd, matches[j]);
Comment thread
robobun marked this conversation as resolved.
Resolved
if (resolved.includes(path.sep + "node_modules" + path.sep)) {
continue;
}
Expand Down
46 changes: 32 additions & 14 deletions src/runtime/api/glob.rs
Original file line number Diff line number Diff line change
Expand Up @@ -266,13 +266,24 @@ pub(crate) enum AnyGlobWalker {
}

impl AnyGlobWalker {
fn walk(&mut self) -> Result<bun_sys::Result<()>, bun_core::Error> {
pub(crate) fn walk(&mut self) -> Result<bun_sys::Result<()>, bun_core::Error> {
match self {
AnyGlobWalker::Fs(walker) => walker.walk(),
AnyGlobWalker::Standalone(walker) => walker.walk(),
}
}

/// The paths that `walk` matched, moved out of the walker.
pub(crate) fn take_matched_paths(&mut self) -> Vec<Box<[u8]>> {
fn take<A: walk::Accessor>(walker: &mut walk::GlobWalker<A, false>) -> Vec<Box<[u8]>> {
core::mem::take(&mut *walker.matched_paths).into_entries().0
}
match self {
AnyGlobWalker::Fs(walker) => take(walker),
AnyGlobWalker::Standalone(walker) => take(walker),
}
}

fn matched_paths_to_js<'a>(&self, heap: &JsHeap<'a>) -> bindgen::Result<'a, JsLocal<'a>> {
fn to_js<'a, A: walk::Accessor>(
walker: &walk::GlobWalker<A, false>,
Expand Down Expand Up @@ -352,16 +363,21 @@ impl JobContext for WalkTask {
}

impl Glob {
/// The walker of `new Bun.Glob(pattern).scanSync(cwd)`, for native code that scans as JavaScript does.
pub(crate) fn scan_sync_walker<'a>(
pattern: &[u8],
cwd: &'a [u8],
) -> Result<AnyGlobWalker, ScanError<'a>> {
Self::make_glob_walker(pattern, Some(GlobScanArgument::Cwd(cwd)), "scanSync")
}

fn make_glob_walker<'a>(
&self,
pattern: &[u8],
argument: Option<GlobScanArgument<'a>>,
fn_name: &'static str,
) -> Result<AnyGlobWalker, ScanError<'a>> {
if bun_core::strings::contains_char(self.pattern.slice(), 0) {
return Err(ScanError::NullBytes(
"pattern",
Box::from(self.pattern.slice()),
));
if bun_core::strings::contains_char(pattern, 0) {
return Err(ScanError::NullBytes("pattern", Box::from(pattern)));
}
let match_opts = ScanOpts::new(argument, fn_name)?;

Expand Down Expand Up @@ -399,11 +415,11 @@ impl Glob {
// Match the walker's root choice (`Iterator::init`): an absolute
// pattern roots at its literal prefix and ignores the cwd, a relative
// pattern roots at the cwd. A `/$bunfs/` root walks the graph.
let pattern_is_absolute = resolve_path::is_absolute(self.pattern.slice())
|| (cfg!(windows) && resolve_path::is_absolute_posix(self.pattern.slice()));
let pattern_is_absolute = resolve_path::is_absolute(pattern)
|| (cfg!(windows) && resolve_path::is_absolute_posix(pattern));
let in_standalone_graph = bun_standalone_graph::Graph::get_ref().is_some()
&& if pattern_is_absolute {
bun_standalone_graph::is_bun_standalone_file_path(self.pattern.slice())
bun_standalone_graph::is_bun_standalone_file_path(pattern)
} else {
match_opts
.cwd
Expand All @@ -413,11 +429,11 @@ impl Glob {

if in_standalone_graph {
return Ok(AnyGlobWalker::Standalone(
init_walker::<StandaloneAccessor>(self.pattern.slice(), &match_opts)?,
init_walker::<StandaloneAccessor>(pattern, &match_opts)?,
));
}
Ok(AnyGlobWalker::Fs(init_walker::<walk::SyscallAccessor>(
self.pattern.slice(),
pattern,
&match_opts,
)?))
}
Expand Down Expand Up @@ -446,7 +462,9 @@ impl Glob {
cx: &mut JsScope<'a>,
options: Option<GlobScanArgument<'a>>,
) -> Result<JsLocal<'a>, ScanError<'a>> {
let walker = this.with(&cx.payloads, |glob| glob.make_glob_walker(options, "scan"))?;
let walker = this.with(&cx.payloads, |glob| {
Self::make_glob_walker(glob.pattern.slice(), options, "scan")
})?;
let promise = JSPromiseStrong::init(cx.global());
let value = promise.value();
Job::<WalkTask>::schedule(
Expand All @@ -462,7 +480,7 @@ impl Glob {
heap: &JsHeap<'a>,
options: Option<GlobScanArgument<'a>>,
) -> Result<JsLocal<'a>, ScanError<'a>> {
let mut walker = self.make_glob_walker(options, "scanSync")?;
let mut walker = Self::make_glob_walker(self.pattern.slice(), options, "scanSync")?;
walker
.walk()
.map_err(|err| ScanError::Runtime(err.into()))?
Expand Down
27 changes: 8 additions & 19 deletions src/runtime/cli/check_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -781,26 +781,15 @@ fn paths_to_check(
(!paths.is_empty()).then_some((paths, script_kinds))
}

/// The pages `bun` serves for the argument `page`. If `page` may be a glob, src/js/internal/html.ts
/// expands it with `new Bun.Glob(page).scanSync(cwd)`. Pages in `node_modules` are excluded.
/// The pages `bun` serves for the argument `page`, as src/js/internal/html.ts finds them. Pages in `node_modules` are excluded.
fn pages_of(cwd: &[u8], page: &[u8]) -> Vec<Vec<u8>> {
use bun_glob::{BunGlobWalker, walk};
let mut pages = Vec::new();
if !bun_core::strings::contains_any(page, b"*{") {
pages.push(page.to_vec());
} else if let Ok(Ok(mut walker)) =
// `scanSync`'s defaults: files only.
BunGlobWalker::init_with_cwd(
page, cwd, false, false, false, false, true, None,
)
{
let mut matches = walk::Iterator::new(&mut walker);
if matches!(matches.init(), Ok(Ok(()))) {
while let Ok(Ok(Some(path))) = matches.next() {
pages.push(path.into_vec());
}
}
}
use crate::cli::html_pages::{self, Pages};
let mut pages = match html_pages::pages_of(cwd, page) {
Ok(Pages::Named) => vec![page.to_vec()],
// src/js/internal/html.ts reports the error, when it asks for the pages to serve them.
Ok(Pages::Matched { pages, error: _ }) => pages.into_iter().map(Vec::from).collect(),
Err(_) => Vec::new(),
};
pages.retain(|page| {
let page =
bun_paths::resolve_path::join_abs_string::<bun_paths::platform::Auto>(cwd, &[page]);
Expand Down
9 changes: 9 additions & 0 deletions src/runtime/cli/html_pages.bind.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import * as b from "../../codegen/bindgen-lib.ts";

/** `undefined` if `arg` is the path of a page, else the pages that match the pattern `arg`. A relative path is in `cwd`. */
export const pagesOf = b.fn({
args: { arg: b.string.utf8, cwd: b.string.utf8 },
returns: b.array(b.string.utf8).optional,
throws: true,
rustPath: "crate::cli::html_pages::pages_of_from_js",
});
70 changes: 70 additions & 0 deletions src/runtime/cli/html_pages.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
//! Whether an argument of `bun ./index.html './pages/*.html'` is a page or a pattern, for html.ts and `bun --check`.

use bun_core::strings;
use bun_paths::{platform::Auto, resolve_path};

use crate::api::glob::{Glob, ScanError};

/// What an argument names.
pub(crate) enum Pages {
/// One page. The argument is its path.
Named,
/// The argument is a pattern.
Matched {
/// The pages that match it. Each path is absolute or relative to the working directory.
pages: Vec<Box<[u8]>>,
/// What ended the search early. `pages` then has those that were found before it.
error: Option<bun_sys::Error>,
},
}

/// The pages that the argument `text` names. A relative path is in `cwd`.
pub(crate) fn pages_of<'a>(cwd: &'a [u8], text: &[u8]) -> Result<Pages, ScanError<'a>> {
if !strings::contains_any(text, b"*{") {
return Ok(Pages::Named);
}
// Made before the look for a file: it refuses what `Bun.Glob.prototype.scanSync` refuses, such as a NUL byte.
let mut walker = Glob::scan_sync_walker(text, cwd)?;
if names_file(cwd, text) {
return Ok(Pages::Named);
}
let walked = walker
.walk()
.map_err(|error| ScanError::Runtime(error.into()))?;
let pages = walker.take_matched_paths();
Ok(Pages::Matched {
pages,
error: walked.err(),
})
}

/// Whether `text` is the path of a file that it does not match as a pattern: `{a,b}/x.html` matches `a/x.html`.
fn names_file(cwd: &[u8], text: &[u8]) -> bool {
if bun_glob::r#match(text, text).matches() {
return false;
}
let mut buf = bun_paths::path_buffer_pool::get();
Comment thread
claude[bot] marked this conversation as resolved.
Resolved
let Some(path) =
resolve_path::join_abs_string_buf_z_checked::<Auto>(cwd, &mut buf[..], &[text])
else {
return false;
};
matches!(
bun_sys::exists_at_type(bun_sys::Fd::cwd(), path),
Ok(bun_sys::ExistsAtType::File)
)
}

/// `pagesOf` of html_pages.bind.ts, with the errors of `Bun.Glob.prototype.scanSync`. `None` is `undefined`.
pub(crate) fn pages_of_from_js<'a>(
arg: &[u8],
cwd: &'a [u8],
) -> Result<Option<Vec<Box<[u8]>>>, ScanError<'a>> {
match pages_of(cwd, arg)? {
Pages::Named => Ok(None),
Pages::Matched {
error: Some(error), ..
} => Err(ScanError::Sys(error)),
Pages::Matched { pages, error: None } => Ok(Some(pages)),
}
}
2 changes: 2 additions & 0 deletions src/runtime/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,8 @@ pub(crate) mod filter_arg;
pub(crate) mod filter_run;
#[path = "format_command.rs"]
pub(crate) mod format_command;
#[path = "html_pages.rs"]
pub(crate) mod html_pages;
#[path = "link_command.rs"]
pub(crate) mod link_command;
#[path = "lint_command.rs"]
Expand Down
4 changes: 2 additions & 2 deletions src/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
"runtime/bake",
"runtime/cli/init",
"runtime/cli/create",
"cli/init",
"cli/create",
// Not `cli`: tsc would still walk the link, and then skip `runtime/cli` as a directory that it has seen.
"cli/**",
"jsc/bindings/libuv"
]
}
61 changes: 61 additions & 0 deletions test/cli/check/check.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23796,6 +23796,67 @@ describe.concurrent("--check", () => {
});
});

// A page that an argument names is checked, also when its path is a pattern that matches another page: `{a,b}` matches
// `a`, and `p{1}.html` matches `p1.html`.
test.each([
["{a,b}/app", ["./index.html"], ["index"]],
["{a,b}/app", ["index.html"], ["index"]],
["{a,b}/app", ["./index.html", "./about.html"], ["index", "about"]],
["{a,b}/app", ["./index.html", "<root>/{a,b}/app/about.html"], ["index", "about"]],
[".", ["./{a,b}/app/index.html"], ["index"]],
[".", ["<root>/{a,b}/app/index.html"], ["index"]],
["names", ["./p{1}.html"], ["named"]],
["names", ["./index.html", "./p{1}.html"], ["index", "named"]],
// A pattern that matches its own text stays a pattern: it finds the page with that name too.
...(isWindows ? [] : [["stars", ["./*.html"], ["index", "named"]] as [string, string[], string[]]]),
])("bun --check in %s, %j checks the scripts of the pages that are named", async (cwd, pages, checked) => {
const page = (script: string) => `<!doctype html>\n<script type="module" src="${script}"></script>\n`;
const wrong = (name: string) => `export const ${name}: number = "1";\n`;
using dir = project({
...(isWindows ? {} : { "stars/*.html": page("./named.ts"), "stars/index.html": page("./index.ts") }),
"stars/index.ts": wrong("index"),
"stars/named.ts": wrong("named"),
"{a,b}/app/index.html": page("./index.ts"),
"{a,b}/app/about.html": page("./about.ts"),
"{a,b}/app/index.ts": wrong("index"),
"{a,b}/app/about.ts": wrong("about"),
"a/app/index.html": page("./other.ts"),
"a/app/about.html": page("./other.ts"),
"a/app/other.ts": wrong("other"),
"names/index.html": page("./index.ts"),
"names/p{1}.html": page("./named.ts"),
"names/p1.html": page("./other.ts"),
"names/index.ts": wrong("index"),
"names/named.ts": wrong("named"),
"names/other.ts": wrong("other"),
});
const args = pages.map(it => it.replace("<root>", String(dir)));
const { stderr, exitCode } = await run(join(String(dir), cwd), ["--check", ...args]);
const reported = ["index", "about", "named", "other"].filter(it => stderr.includes(`${it}.ts(1,14): error TS2322`));
expect({ reported, exitCode }).toEqual({ reported: checked, exitCode: 1 });
});

// An error ends the search for pages early. The pages that were found before it are checked. If their scripts have
// no error, the run that follows reports the error of the search. The superuser can read every directory.
test.skipIf(isWindows || isRoot).each([
[`"1"`, "index.ts(1,14): error TS2322"],
[`1`, "EACCES"],
])("bun --check './**/*.html' with a directory that cannot be read, index = %s", async (value, error) => {
using dir = project({
"index.html": `<!doctype html>\n<script type="module" src="./index.ts"></script>\n`,
"index.ts": `export const index: number = ${value};\n`,
"locked/other.html": "",
});
const locked = join(String(dir), "locked");
chmodSync(locked, 0o000);
try {
const { stderr, exitCode } = await run(String(dir), ["--check", "./**/*.html"]);
expect({ reported: stderr.includes(error), exitCode }).toEqual({ reported: true, exitCode: 1 });
} finally {
chmodSync(locked, 0o755);
}
});

// A directory means the tsconfig.json in it, as with `tsc -p`.
test.each([
["tsconfig.build.json", "tsconfig.build.json", "."],
Expand Down
1 change: 1 addition & 0 deletions test/integration/bun-types/bun-types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,7 @@ const PUBLISHED_AS: Record<string, string | null> = {
"dns_internals.bind.ts": null,
"filesystem_router.bind.ts": "typeof Bun",
"fmt_jsc.bind.ts": null,
"html_pages.bind.ts": null,
"html_rewriter.bind.ts": null,
"jest.bind.ts": null,
"jsc_hooks.bind.ts": null,
Expand Down
Loading
Loading