aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Dylan Conway <dylan.conway567@gmail.com> 2023-08-31 17:44:16 -0700
committerGravatar Dylan Conway <dylan.conway567@gmail.com> 2023-08-31 17:44:16 -0700
commit7528ea00843f7c33b8e3017ca28b83cca556f783 (patch)
tree1bde419e3674efe2d06bec754ee6e82176ec80f0
parent4d944773f0234262e2df0dfd10cb564d3cdf6125 (diff)
parent59edbe645ca568aa6438f8fa53c1a567cf1159c0 (diff)
downloadbun-7528ea00843f7c33b8e3017ca28b83cca556f783.tar.gz
bun-7528ea00843f7c33b8e3017ca28b83cca556f783.tar.zst
bun-7528ea00843f7c33b8e3017ca28b83cca556f783.zip
Merge branch 'main' into dylan/non-enumerable-export-values
-rw-r--r--bench/snippets/cp-r.mjs1
-rw-r--r--src/bun.js/bindings/BunString.cpp11
-rw-r--r--src/bun.js/bindings/bindings.zig9
-rw-r--r--src/bun.js/bindings/webcore/JSDOMFormData.cpp10
-rw-r--r--src/bun.js/webcore/blob.zig9
-rw-r--r--src/install/install.zig91
-rw-r--r--src/logger.zig29
-rw-r--r--test/cli/install/bun-add.test.ts18
-rw-r--r--test/cli/install/bun-install.test.ts149
-rw-r--r--test/js/web/html/FormData.test.ts30
10 files changed, 288 insertions, 69 deletions
diff --git a/bench/snippets/cp-r.mjs b/bench/snippets/cp-r.mjs
index 78fbb2711..a5fe6d84f 100644
--- a/bench/snippets/cp-r.mjs
+++ b/bench/snippets/cp-r.mjs
@@ -1,4 +1,3 @@
import { cp } from "fs/promises";
await cp(process.argv[2], process.argv[3], { recursive: true });
-
diff --git a/src/bun.js/bindings/BunString.cpp b/src/bun.js/bindings/BunString.cpp
index 8c5a1e66b..416d5d334 100644
--- a/src/bun.js/bindings/BunString.cpp
+++ b/src/bun.js/bindings/BunString.cpp
@@ -381,6 +381,17 @@ extern "C" BunString URL__getHref(BunString* input)
return Bun::toStringRef(url.string());
}
+extern "C" BunString URL__getHrefJoin(BunString* baseStr, BunString *relativeStr)
+{
+ auto base = Bun::toWTFString(*baseStr);
+ auto relative = Bun::toWTFString(*relativeStr);
+ auto url = WTF::URL(WTF::URL(base), relative);
+ if (!url.isValid() || url.isEmpty())
+ return { BunStringTag::Dead };
+
+ return Bun::toStringRef(url.string());
+}
+
extern "C" WTF::URL* URL__fromString(BunString* input)
{
auto&& str = Bun::toWTFString(*input);
diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig
index 870b78738..6729ab9ab 100644
--- a/src/bun.js/bindings/bindings.zig
+++ b/src/bun.js/bindings/bindings.zig
@@ -5480,12 +5480,21 @@ pub const URL = opaque {
extern fn URL__getHrefFromJS(JSValue, *JSC.JSGlobalObject) String;
extern fn URL__getHref(*String) String;
extern fn URL__getFileURLString(*String) String;
+ extern fn URL__getHrefJoin(*String, *String) String;
+
pub fn hrefFromString(str: bun.String) String {
JSC.markBinding(@src());
var input = str;
return URL__getHref(&input);
}
+ pub fn join(base: bun.String, relative: bun.String) String {
+ JSC.markBinding(@src());
+ var base_str = base;
+ var relative_str = relative;
+ return URL__getHrefJoin(&base_str, &relative_str);
+ }
+
pub fn fileURLFromString(str: bun.String) String {
JSC.markBinding(@src());
var input = str;
diff --git a/src/bun.js/bindings/webcore/JSDOMFormData.cpp b/src/bun.js/bindings/webcore/JSDOMFormData.cpp
index 09a0a6c08..fdb71341c 100644
--- a/src/bun.js/bindings/webcore/JSDOMFormData.cpp
+++ b/src/bun.js/bindings/webcore/JSDOMFormData.cpp
@@ -286,6 +286,8 @@ static inline JSC::EncodedJSValue jsDOMFormDataPrototypeFunction_append1Body(JSC
RELEASE_AND_RETURN(throwScope, JSValue::encode(toJS<IDLUndefined>(*lexicalGlobalObject, throwScope, [&]() -> decltype(auto) { return impl.append(WTFMove(name), WTFMove(value)); })));
}
+extern "C" BunString Blob__getFileNameString(void* impl);
+
static inline JSC::EncodedJSValue jsDOMFormDataPrototypeFunction_append2Body(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation<JSDOMFormData>::ClassParameter castedThis)
{
auto& vm = JSC::getVM(lexicalGlobalObject);
@@ -298,10 +300,6 @@ static inline JSC::EncodedJSValue jsDOMFormDataPrototypeFunction_append2Body(JSC
RETURN_IF_EXCEPTION(throwScope, encodedJSValue());
EnsureStillAliveScope argument1 = callFrame->uncheckedArgument(1);
- EnsureStillAliveScope argument2 = callFrame->argument(2);
- auto filename = argument2.value().isUndefined() ? String() : convert<IDLUSVString>(*lexicalGlobalObject, argument2.value());
- RETURN_IF_EXCEPTION(throwScope, encodedJSValue());
-
RefPtr<Blob> blobValue = nullptr;
if (argument1.value().inherits<JSBlob>()) {
blobValue = Blob::create(argument1.value());
@@ -312,6 +310,10 @@ static inline JSC::EncodedJSValue jsDOMFormDataPrototypeFunction_append2Body(JSC
}
RETURN_IF_EXCEPTION(throwScope, encodedJSValue());
+ EnsureStillAliveScope argument2 = callFrame->argument(2);
+ auto filename = argument2.value().isUndefined() ? Bun::toWTFString(Blob__getFileNameString(blobValue->impl())) : convert<IDLUSVString>(*lexicalGlobalObject, argument2.value());
+ RETURN_IF_EXCEPTION(throwScope, encodedJSValue());
+
RELEASE_AND_RETURN(throwScope, JSValue::encode(toJS<IDLUndefined>(*lexicalGlobalObject, throwScope, [&]() -> decltype(auto) { return impl.append(WTFMove(name), WTFMove(blobValue), WTFMove(filename)); })));
}
diff --git a/src/bun.js/webcore/blob.zig b/src/bun.js/webcore/blob.zig
index 25f762e97..54c37e679 100644
--- a/src/bun.js/webcore/blob.zig
+++ b/src/bun.js/webcore/blob.zig
@@ -510,11 +510,20 @@ pub const Blob = struct {
this.finalize();
}
+ export fn Blob__getFileNameString(this: *Blob) callconv(.C) bun.String {
+ if (this.getFileName()) |filename| {
+ return bun.String.fromBytes(filename);
+ }
+
+ return bun.String.empty;
+ }
+
comptime {
_ = Blob__dupeFromJS;
_ = Blob__destroy;
_ = Blob__dupe;
_ = Blob__setAsFile;
+ _ = Blob__getFileNameString;
}
pub fn writeFormatForSize(size: usize, writer: anytype, comptime enable_ansi_colors: bool) !void {
diff --git a/src/install/install.zig b/src/install/install.zig
index 768113d35..e8b95b820 100644
--- a/src/install/install.zig
+++ b/src/install/install.zig
@@ -249,61 +249,31 @@ const NetworkTask = struct {
allocator: std.mem.Allocator,
scope: *const Npm.Registry.Scope,
loaded_manifest: ?Npm.PackageManifest,
+ warn_on_error: bool,
) !void {
- const pathname: string = if (!strings.eqlComptime(scope.url.pathname, "/"))
- scope.url.pathname
- else
- @as(string, "");
+ this.url_buf = blk: {
+ const tmp = bun.JSC.URL.join(
+ bun.String.fromUTF8(scope.url.href),
+ bun.String.fromUTF8(name),
+ );
+ defer tmp.deref();
- if (pathname.len > 0) {
- if (scope.url.getPort()) |port_number| {
- this.url_buf = try std.fmt.allocPrint(
- allocator,
- "{s}://{s}:{d}/{s}/{s}",
- .{
- scope.url.displayProtocol(),
- scope.url.displayHostname(),
- port_number,
- strings.withoutTrailingSlash(pathname),
- name,
- },
- );
- } else {
- this.url_buf = try std.fmt.allocPrint(
- allocator,
- "{s}://{s}/{s}/{s}",
- .{
- scope.url.displayProtocol(),
- scope.url.displayHostname(),
- strings.withoutTrailingSlash(pathname),
- name,
- },
- );
- }
- } else {
- if (scope.url.getPort()) |port_number| {
- this.url_buf = try std.fmt.allocPrint(
- allocator,
- "{s}://{s}:{d}/{s}",
- .{
- scope.url.displayProtocol(),
- scope.url.displayHostname(),
- port_number,
- name,
- },
- );
- } else {
- this.url_buf = try std.fmt.allocPrint(
- allocator,
- "{s}://{s}/{s}",
- .{
- scope.url.displayProtocol(),
- scope.url.displayHostname(),
- name,
- },
- );
+ if (tmp.tag == .Dead) {
+ const msg = .{
+ .fmt = "Failed to join registry \"{s}\" and package \"{s}\" URLs",
+ .args = .{ scope.url.href, name },
+ };
+
+ if (warn_on_error)
+ this.package_manager.log.addWarningFmt(null, .{}, allocator, msg.fmt, msg.args) catch unreachable
+ else
+ this.package_manager.log.addErrorFmt(null, .{}, allocator, msg.fmt, msg.args) catch unreachable;
+
+ return error.InvalidURL;
}
- }
+ // This actually duplicates the string! So we defer deref the WTF managed one above.
+ break :blk try tmp.toOwnedSlice(allocator);
+ };
var last_modified: string = "";
var etag: string = "";
@@ -3075,6 +3045,7 @@ pub const PackageManager = struct {
this.allocator,
this.scopeForPackageName(name_str),
loaded_manifest,
+ dependency.behavior.isOptional() or dependency.behavior.isPeer(),
);
this.enqueueNetworkTask(network_task);
}
@@ -3432,7 +3403,21 @@ pub const PackageManager = struct {
i,
&dependency,
resolution,
- ) catch {};
+ ) catch |err| {
+ const note = .{
+ .fmt = "error occured while resolving {s}",
+ .args = .{
+ lockfile.str(&dependency.realname()),
+ },
+ };
+
+ if (dependency.behavior.isOptional() or dependency.behavior.isPeer())
+ this.log.addWarningWithNote(null, .{}, this.allocator, @errorName(err), note.fmt, note.args) catch unreachable
+ else
+ this.log.addZigErrorWithNote(this.allocator, err, note.fmt, note.args) catch unreachable;
+
+ continue;
+ };
}
this.drainDependencyList();
diff --git a/src/logger.zig b/src/logger.zig
index 7340efc3d..dfe60e2af 100644
--- a/src/logger.zig
+++ b/src/logger.zig
@@ -1027,6 +1027,20 @@ pub const Log = struct {
});
}
+ pub fn addZigErrorWithNote(log: *Log, allocator: std.mem.Allocator, err: anyerror, comptime noteFmt: string, args: anytype) !void {
+ @setCold(true);
+ log.errors += 1;
+
+ var notes = try allocator.alloc(Data, 1);
+ notes[0] = rangeData(null, Range.None, allocPrint(allocator, noteFmt, args) catch unreachable);
+
+ try log.addMsg(.{
+ .kind = .err,
+ .data = rangeData(null, Range.None, @errorName(err)),
+ .notes = notes,
+ });
+ }
+
pub fn addRangeWarning(log: *Log, source: ?*const Source, r: Range, text: string) !void {
@setCold(true);
if (!Kind.shouldPrint(.warn, log.level)) return;
@@ -1092,6 +1106,21 @@ pub const Log = struct {
});
}
+ pub fn addWarningWithNote(log: *Log, source: ?*const Source, l: Loc, allocator: std.mem.Allocator, warn: string, comptime note_fmt: string, note_args: anytype) !void {
+ @setCold(true);
+ if (!Kind.shouldPrint(.warn, log.level)) return;
+ log.warnings += 1;
+
+ var notes = try allocator.alloc(Data, 1);
+ notes[0] = rangeData(source, Range{ .loc = l }, allocPrint(allocator, note_fmt, note_args) catch unreachable);
+
+ try log.addMsg(.{
+ .kind = .warn,
+ .data = rangeData(null, Range.None, warn),
+ .notes = notes,
+ });
+ }
+
pub fn addRangeDebug(log: *Log, source: ?*const Source, r: Range, text: string) !void {
@setCold(true);
if (!Kind.shouldPrint(.debug, log.level)) return;
diff --git a/test/cli/install/bun-add.test.ts b/test/cli/install/bun-add.test.ts
index 589b7a87e..4461584e4 100644
--- a/test/cli/install/bun-add.test.ts
+++ b/test/cli/install/bun-add.test.ts
@@ -102,11 +102,10 @@ it("should reject missing package", async () => {
});
expect(stderr).toBeDefined();
const err = await new Response(stderr).text();
- expect(err.replace(/^(.*?) v[^\n]+/, "$1").split(/\r?\n/)).toEqual([
- "bun add",
- `error: file:${add_path} failed to resolve`,
- "",
- ]);
+ expect(err.includes("bun add")).toBeTrue();
+ expect(err.includes("error: MissingPackageJSON")).toBeTrue();
+ expect(err.includes(`note: error occured while resolving file:${add_path}`)).toBeTrue();
+
expect(stdout).toBeDefined();
const out = await new Response(stdout).text();
expect(out).toBe("");
@@ -145,11 +144,10 @@ it("should reject invalid path without segfault", async () => {
});
expect(stderr).toBeDefined();
const err = await new Response(stderr).text();
- expect(err.replace(/^(.*?) v[^\n]+/, "$1").split(/\r?\n/)).toEqual([
- "bun add",
- `error: file://${add_path} failed to resolve`,
- "",
- ]);
+ expect(err.includes("bun add")).toBeTrue();
+ expect(err.includes("error: MissingPackageJSON")).toBeTrue();
+ expect(err.includes(`note: error occured while resolving file://${add_path}`)).toBeTrue();
+
expect(stdout).toBeDefined();
const out = await new Response(stdout).text();
expect(out).toBe("");
diff --git a/test/cli/install/bun-install.test.ts b/test/cli/install/bun-install.test.ts
index 28edac092..09da0bbd4 100644
--- a/test/cli/install/bun-install.test.ts
+++ b/test/cli/install/bun-install.test.ts
@@ -1,5 +1,5 @@
import { file, listen, Socket, spawn } from "bun";
-import { afterAll, afterEach, beforeAll, beforeEach, expect, it } from "bun:test";
+import { afterAll, afterEach, beforeAll, beforeEach, expect, it, describe, test } from "bun:test";
import { bunExe, bunEnv as env } from "harness";
import { access, mkdir, readlink, realpath, rm, writeFile } from "fs/promises";
import { join } from "path";
@@ -6006,3 +6006,150 @@ it("should handle `workspace:*` on both root & child", async () => {
expect(await file(join(package_dir, "node_modules", "baz", "package.json")).text()).toEqual(baz_package);
await access(join(package_dir, "bun.lockb"));
});
+
+describe("Registry URLs", () => {
+ // Some of the non failing URLs are invalid, but bun's URL parser ignores
+ // the validation error and returns a valid serialized URL anyway.
+ const registryURLs: [url: string, fails: boolean][] = [
+ ["asdfghjklqwertyuiop", true],
+ [" ", true],
+ ["::::::::::::::::", true],
+ ["https://ex ample.org/", true],
+ ["example", true],
+ ["https://example.com:demo", true],
+ ["http://[www.example.com]/", true],
+ ["c:", true],
+ ["c:a", true],
+ ["https://registry.npmjs.org/", false],
+ ["https://artifactory.xxx.yyy/artifactory/api/npm/my-npm/", false], // https://github.com/oven-sh/bun/issues/3899
+ ["", false],
+ ["https:example.org", false],
+ ["https://////example.com///", false],
+ ["https://example.com/https:example.org", false],
+ ["https://example.com/[]?[]#[]", false],
+ ["https://example/%?%#%", false],
+ ["c:/", false],
+ ["https://點看", false], // gets converted to punycode
+ ["https://xn--c1yn36f/", false],
+ ];
+
+ for (const entry of registryURLs) {
+ const regURL = entry[0];
+ const fails = entry[1];
+
+ it(`should ${fails ? "fail" : "handle"} joining registry and package URLs (${regURL})`, async () => {
+ await writeFile(join(package_dir, "bunfig.toml"), `[install]\ncache = false\nregistry = "${regURL}"`);
+
+ await writeFile(
+ join(package_dir, "package.json"),
+ JSON.stringify({
+ name: "foo",
+ version: "0.0.1",
+ dependencies: {
+ notapackage: "0.0.2",
+ },
+ }),
+ );
+
+ const { stdout, stderr, exited } = spawn({
+ cmd: [bunExe(), "install"],
+ cwd: package_dir,
+ stdout: null,
+ stdin: "pipe",
+ stderr: "pipe",
+ env,
+ });
+ expect(stdout).toBeDefined();
+ expect(await new Response(stdout).text()).toBeEmpty();
+
+ expect(stderr).toBeDefined();
+ const err = await new Response(stderr).text();
+
+ if (fails) {
+ expect(err.includes(`Failed to join registry \"${regURL}\" and package \"notapackage\" URLs`)).toBeTrue();
+ expect(err.includes("error: InvalidURL")).toBeTrue();
+ } else {
+ expect(err.includes("error: notapackage@0.0.2 failed to resolve")).toBeTrue();
+ }
+ // fails either way, since notapackage is, well, not a real package.
+ expect(await exited).not.toBe(0);
+ });
+ }
+
+ it("shouldn't fail joining invalid registry and package URLs for optional dependencies", async () => {
+ const regURL = "asdfghjklqwertyuiop";
+
+ await writeFile(join(package_dir, "bunfig.toml"), `[install]\ncache = false\nregistry = "${regURL}"`);
+
+ await writeFile(
+ join(package_dir, "package.json"),
+ JSON.stringify({
+ name: "foo",
+ version: "0.0.1",
+ optionalDependencies: {
+ notapackage: "0.0.2",
+ },
+ }),
+ );
+
+ const { stdout, stderr, exited } = spawn({
+ cmd: [bunExe(), "install"],
+ cwd: package_dir,
+ stdout: null,
+ stdin: "pipe",
+ stderr: "pipe",
+ env,
+ });
+ expect(stdout).toBeDefined();
+ expect(await new Response(stdout).text()).not.toBeEmpty();
+
+ expect(stderr).toBeDefined();
+ const err = await new Response(stderr).text();
+
+ expect(err.includes(`Failed to join registry \"${regURL}\" and package \"notapackage\" URLs`)).toBeTrue();
+ expect(err.includes("warn: InvalidURL")).toBeTrue();
+
+ expect(await exited).toBe(0);
+ });
+
+ // TODO: This test should fail if the param `warn_on_error` is true in
+ // `(install.zig).NetworkTask.forManifest()`. Unfortunately, that
+ // code never gets run for peer dependencies unless you do some package
+ // manifest magic. I doubt it'd ever fail, but having a dedicated
+ // test would be nice.
+ test.todo("shouldn't fail joining invalid registry and package URLs for peer dependencies", async () => {
+ const regURL = "asdfghjklqwertyuiop";
+
+ await writeFile(join(package_dir, "bunfig.toml"), `[install]\ncache = false\nregistry = "${regURL}"`);
+
+ await writeFile(
+ join(package_dir, "package.json"),
+ JSON.stringify({
+ name: "foo",
+ version: "0.0.1",
+ peerDependencies: {
+ notapackage: "0.0.2",
+ },
+ }),
+ );
+
+ const { stdout, stderr, exited } = spawn({
+ cmd: [bunExe(), "install"],
+ cwd: package_dir,
+ stdout: null,
+ stdin: "pipe",
+ stderr: "pipe",
+ env,
+ });
+ expect(stdout).toBeDefined();
+ expect(await new Response(stdout).text()).not.toBeEmpty();
+
+ expect(stderr).toBeDefined();
+ const err = await new Response(stderr).text();
+
+ expect(err.includes(`Failed to join registry \"${regURL}\" and package \"notapackage\" URLs`)).toBeTrue();
+ expect(err.includes("warn: InvalidURL")).toBeTrue();
+
+ expect(await exited).toBe(0);
+ });
+});
diff --git a/test/js/web/html/FormData.test.ts b/test/js/web/html/FormData.test.ts
index c742bd33e..5e4861bc8 100644
--- a/test/js/web/html/FormData.test.ts
+++ b/test/js/web/html/FormData.test.ts
@@ -36,6 +36,36 @@ describe("FormData", () => {
expect(formData.getAll("foo")[0]).toBe("bar");
});
+ it("should get filename from file", async () => {
+ const blob = new Blob(["bar"]);
+ const formData = new FormData();
+ formData.append("foo", blob);
+ // @ts-expect-error
+ expect(formData.get("foo").name).toBeUndefined();
+ formData.append("foo2", new File([blob], "foo.txt"));
+ // @ts-expect-error
+ expect(formData.get("foo2").name).toBe("foo.txt");
+ });
+
+ it("should use the correct filenames", async () => {
+ const blob = new Blob(["bar"]) as any;
+ const form = new FormData();
+ form.append("foo", blob);
+ expect(blob.name).toBeUndefined();
+
+ let b1 = form.get("foo") as any;
+ expect(blob.name).toBeUndefined();
+ expect(b1.name).toBeUndefined();
+
+ form.set("foo", b1, "foo.txt");
+ expect(blob.name).toBeUndefined();
+ expect(b1.name).toBeUndefined();
+
+ b1 = form.get("foo") as Blob;
+ expect(blob.name).toBeUndefined();
+ expect(b1.name).toBe("foo.txt");
+ });
+
const multipartFormDataFixturesRawBody = [
{
name: "simple",