Compare commits

...

4 Commits

Author SHA1 Message Date
Jarred Sumner
5a85d819ad Fixes #7503 2023-12-07 01:50:50 -08:00
Jarred Sumner
1ef0816531 Fix target 2023-12-07 00:52:44 -08:00
autofix-ci[bot]
41221c2277 [autofix.ci] apply automated fixes 2023-12-07 04:44:29 +00:00
dave caruso
ea9b4c0f68 fix(resolver): allow builtins to be imported via subpath imports 2023-12-06 20:12:09 -08:00
7 changed files with 131 additions and 40 deletions

View File

@@ -3813,15 +3813,17 @@ pub const Blob = struct {
return this.store != null and this.store.?.data == .file;
}
pub fn toStringWithBytes(this: *Blob, global: *JSGlobalObject, buf: []const u8, comptime lifetime: Lifetime) JSValue {
pub fn toStringWithBytes(this: *Blob, global: *JSGlobalObject, buf_: []const u8, comptime lifetime: Lifetime) JSValue {
// null == unknown
// false == can't be
const could_be_all_ascii = this.is_all_ascii orelse this.store.?.is_all_ascii;
const buf = strings.withoutUTF8BOM(buf_);
if (could_be_all_ascii == null or !could_be_all_ascii.?) {
// if toUTF16Alloc returns null, it means there are no non-ASCII characters
// instead of erroring, invalid characters will become a U+FFFD replacement character
if (strings.toUTF16AllocAllowBOM(bun.default_allocator, buf, false, true) catch unreachable) |external| {
if (strings.toUTF16Alloc(bun.default_allocator, buf, false) catch unreachable) |external| {
if (lifetime != .temporary)
this.setIsASCIIFlag(false);
@@ -3848,21 +3850,36 @@ pub const Blob = struct {
// we don't need to clone
.clone => {
this.store.?.ref();
// we don't need to worry about UTF-8 BOM in this case because the store owns the memory.
return ZigString.init(buf).external(global, this.store.?, Store.external);
},
.transfer => {
var store = this.store.?;
std.debug.assert(store.data == .bytes);
this.transfer();
// we don't need to worry about UTF-8 BOM in this case because the store owns the memory.
return ZigString.init(buf).external(global, store, Store.external);
},
// strings are immutable
// sharing isn't really a thing
.share => {
this.store.?.ref();
// we don't need to worry about UTF-8 BOM in this case because the store owns the memory.s
return ZigString.init(buf).external(global, this.store.?, Store.external);
},
.temporary => {
// if there was a UTF-8 BOM, we need to clone the buffer because
// external doesn't support this case here yet.
if (buf.len != buf_.len) {
var out = bun.String.createLatin1(buf);
defer {
bun.default_allocator.free(buf_);
out.deref();
}
return out.toJS(global);
}
return ZigString.init(buf).toExternalValue(global);
},
}
@@ -3892,7 +3909,8 @@ pub const Blob = struct {
return toJSONWithBytes(this, global, view_, lifetime);
}
pub fn toJSONWithBytes(this: *Blob, global: *JSGlobalObject, buf: []const u8, comptime lifetime: Lifetime) JSValue {
pub fn toJSONWithBytes(this: *Blob, global: *JSGlobalObject, buf_: []const u8, comptime lifetime: Lifetime) JSValue {
const buf = strings.withoutUTF8BOM(buf_);
if (buf.len == 0) return global.createSyntaxErrorInstance("Unexpected end of JSON input", .{});
// null == unknown
// false == can't be
@@ -3903,7 +3921,7 @@ pub const Blob = struct {
var stack_fallback = std.heap.stackFallback(4096, bun.default_allocator);
const allocator = stack_fallback.get();
// if toUTF16Alloc returns null, it means there are no non-ASCII characters
if (strings.toUTF16AllocAllowBOM(allocator, buf, false, true) catch null) |external| {
if (strings.toUTF16Alloc(allocator, buf, false) catch null) |external| {
if (comptime lifetime != .temporary) this.setIsASCIIFlag(false);
const result = ZigString.init16(external).toJSONObject(global);
allocator.free(external);
@@ -4532,11 +4550,19 @@ pub const InternalBlob = struct {
was_string: bool = false,
pub fn toStringOwned(this: *@This(), globalThis: *JSC.JSGlobalObject) JSValue {
if (strings.toUTF16AllocAllowBOM(globalThis.allocator(), this.bytes.items, false, true) catch &[_]u16{}) |out| {
const bytes_without_bom = strings.withoutUTF8BOM(this.bytes.items);
if (strings.toUTF16Alloc(globalThis.allocator(), bytes_without_bom, false) catch &[_]u16{}) |out| {
const return_value = ZigString.toExternalU16(out.ptr, out.len, globalThis);
return_value.ensureStillAlive();
this.deinit();
return return_value;
} else if
// If there was a UTF8 BOM, we clone it
(bytes_without_bom.len != this.bytes.items.len) {
defer this.deinit();
var out = bun.String.createLatin1(this.bytes.items[3..]);
defer out.deref();
return out.toJS(globalThis);
} else {
var str = ZigString.init(this.toOwnedSlice());
str.mark();

View File

@@ -1569,10 +1569,15 @@ pub const Resolver = struct {
// Find the parent directory with the "package.json" file
var dir_info_package_json: ?*DirInfo = dir_info;
while (dir_info_package_json != null and dir_info_package_json.?.package_json == null) : (dir_info_package_json = dir_info_package_json.?.getParent()) {}
while (dir_info_package_json != null and dir_info_package_json.?.package_json == null)
dir_info_package_json = dir_info_package_json.?.getParent();
// Check for subpath imports: https://nodejs.org/api/packages.html#subpath-imports
if (dir_info_package_json != null and strings.hasPrefix(import_path, "#") and !forbid_imports and dir_info_package_json.?.package_json.?.imports != null) {
if (dir_info_package_json != null and
strings.hasPrefixComptime(import_path, "#") and
!forbid_imports and
dir_info_package_json.?.package_json.?.imports != null)
{
return r.loadPackageImports(import_path, dir_info_package_json.?, kind, global_cache);
}
@@ -2871,7 +2876,9 @@ pub const Resolver = struct {
const esmodule = ESModule{
.conditions = switch (kind) {
ast.ImportKind.require, ast.ImportKind.require_resolve => r.opts.conditions.require,
ast.ImportKind.require,
ast.ImportKind.require_resolve,
=> r.opts.conditions.require,
else => r.opts.conditions.import,
},
.allocator = r.allocator,
@@ -2881,7 +2888,27 @@ pub const Resolver = struct {
const esm_resolution = esmodule.resolveImports(import_path, imports_map.root);
if (esm_resolution.status == .PackageResolve)
if (esm_resolution.status == .PackageResolve) {
// https://github.com/oven-sh/bun/issues/4972
// Resolve a subpath import to a Bun or Node.js builtin
//
// Code example:
//
// import { readFileSync } from '#fs';
//
// package.json:
//
// "imports": {
// "#fs": "node:fs"
// }
if (JSC.HardcodedModule.Aliases.get(esm_resolution.path, r.opts.target)) |builtin| {
return .{
.success = .{
.path_pair = .{ .primary = bun.fs.Path.init(builtin.path) },
},
};
}
return r.loadNodeModules(
esm_resolution.path,
kind,
@@ -2889,6 +2916,7 @@ pub const Resolver = struct {
global_cache,
true,
);
}
if (r.handleESMResolution(esm_resolution, package_json.source.path.name.dir, kind, package_json, "")) |result| {
return .{ .success = result };

View File

@@ -1313,13 +1313,9 @@ pub fn copyLatin1IntoASCII(dest: []u8, src: []const u8) void {
const utf8_bom = [_]u8{ 0xef, 0xbb, 0xbf };
pub fn toUTF16Alloc(allocator: std.mem.Allocator, bytes: []const u8, comptime fail_if_invalid: bool) !?[]u16 {
return toUTF16AllocAllowBOM(allocator, bytes, fail_if_invalid, false);
}
pub fn withoutUTF8BOM(bytes: []const u8) []const u8 {
if (bytes.len > 3 and strings.eqlComptime(bytes[0..3], utf8_bom)) {
return bytes[3..];
if (strings.hasPrefixComptime(bytes, utf8_bom)) {
return bytes[utf8_bom.len..];
} else {
return bytes;
}
@@ -1328,20 +1324,8 @@ pub fn withoutUTF8BOM(bytes: []const u8) []const u8 {
/// Convert a UTF-8 string to a UTF-16 string IF there are any non-ascii characters
/// If there are no non-ascii characters, this returns null
/// This is intended to be used for strings that go to JavaScript
pub fn toUTF16AllocAllowBOM(allocator: std.mem.Allocator, bytes_: []const u8, comptime fail_if_invalid: bool, comptime allow_bom: bool) !?[]u16 {
var bytes = bytes_;
pub fn toUTF16Alloc(allocator: std.mem.Allocator, bytes: []const u8, comptime fail_if_invalid: bool) !?[]u16 {
if (strings.firstNonASCII(bytes)) |i| {
if (comptime allow_bom) {
// we could avoid the allocation here when it's otherwise ASCII. But
// it gets really complicated because most memory allocators need
// the head pointer to be the allocated one so if we instead return
// a non-head pointer and try to free that the allocator might not
// be able to free it, and we would have a big problem.
if (i == 0 and bytes.len > 3 and strings.eqlComptime(bytes[0..3], utf8_bom)) {
bytes = bytes[3..];
}
}
const output_: ?std.ArrayList(u16) = if (comptime bun.FeatureFlags.use_simdutf) simd: {
const trimmed = bun.simdutf.trim.utf8(bytes);

View File

@@ -0,0 +1,8 @@
{
"name": "hello",
"imports": {
"#async_hooks": "async_hooks",
"#bun": "bun",
"#bun_test": "bun:test"
}
}

View File

@@ -51,20 +51,20 @@ it("#imports with wildcard", async () => {
});
it("import.meta.resolve", async () => {
expect(await import.meta.resolve("./resolve-test.test.js")).toBe(import.meta.path);
expect(await import.meta.resolve("./resolve-test.js")).toBe(import.meta.path);
expect(await import.meta.resolve("./resolve-test.test.js", import.meta.path)).toBe(import.meta.path);
expect(await import.meta.resolve("./resolve-test.js", import.meta.path)).toBe(import.meta.path);
expect(
// optional second param can be any path, including a dir
await import.meta.resolve("./resolve/resolve-test.test.js", join(import.meta.path, "../")),
await import.meta.resolve("./resolve/resolve-test.js", join(import.meta.path, "../")),
).toBe(import.meta.path);
// can be a package path
expect((await import.meta.resolve("react", import.meta.path)).length > 0).toBe(true);
// file extensions are optional
expect(await import.meta.resolve("./resolve-test.test")).toBe(import.meta.path);
expect(await import.meta.resolve("./resolve-test")).toBe(import.meta.path);
// works with tsconfig.json "paths"
expect(await import.meta.resolve("foo/bar")).toBe(join(import.meta.path, "../baz.js"));
@@ -108,12 +108,12 @@ it("import.meta.resolve", async () => {
// the slightly lower level API, which doesn't prefill the second param
// and expects a directory instead of a filepath
it("Bun.resolve", async () => {
expect(await Bun.resolve("./resolve-test.test.js", import.meta.dir)).toBe(import.meta.path);
expect(await Bun.resolve("./resolve-test.js", import.meta.dir)).toBe(import.meta.path);
});
// synchronous
it("Bun.resolveSync", () => {
expect(Bun.resolveSync("./resolve-test.test.js", import.meta.dir)).toBe(import.meta.path);
expect(Bun.resolveSync("./resolve-test.js", import.meta.dir)).toBe(import.meta.path);
});
it("self-referencing imports works", async () => {

View File

@@ -7,18 +7,14 @@ it("spawn test file", () => {
writePackageJSONImportsFixture();
writePackageJSONExportsFixture();
copyFileSync(join(import.meta.dir, "resolve-test.js"), join(import.meta.dir, "resolve-test.test.js"));
const { exitCode } = Bun.spawnSync({
cmd: [bunExe(), "test", "resolve-test.test.js"],
cmd: [bunExe(), "test", "./resolve-test.js"],
env: bunEnv,
cwd: import.meta.dir,
stdio: ["inherit", "inherit", "inherit"],
});
expect(exitCode).toBe(0);
rmSync(join(import.meta.dir, "resolve-test.test.js"));
expect(existsSync(join(import.meta.dir, "resolve-test.test.js"))).toBe(false);
});
function writePackageJSONExportsFixture() {
@@ -78,6 +74,8 @@ function writePackageJSONImportsFixture() {
"#foo": "./foo/private-foo.js",
"#internal-react": "react",
"#to_node_module": "async_hooks",
},
},
null,
@@ -291,3 +289,18 @@ it("import long string should not segfault", async () => {
await import.meta.require.resolve("a".repeat(10000));
} catch {}
});
it("import override to node builtin", async () => {
// @ts-expect-error
expect(await import("#async_hooks")).toBeDefined();
});
it("import override to bun", async () => {
// @ts-expect-error
expect(await import("#bun")).toBeDefined();
});
it.todo("import override to bun:test", async () => {
// @ts-expect-error
expect(await import("#bun_test")).toBeDefined();
});

View File

@@ -1,7 +1,39 @@
import { describe, expect, it, test } from "bun:test";
describe("UTF-8 BOM should be ignored", () => {
test("handles empty strings", async () => {
const blob = new Response(new Blob([Buffer.from([0xef, 0xbb, 0xbf])]));
expect(await blob.text()).toHaveLength(0);
expect(async () => await blob.json()).toThrow();
});
test("handles UTF8 BOM + emoji", async () => {
const blob = new Response(new Blob([Buffer.from([0xef, 0xbb, 0xbf]), Buffer.from("🌎")]));
expect(await blob.text()).toHaveLength(2);
expect(async () => await blob.json()).toThrow();
});
describe("Blob", () => {
describe("with emoji", () => {
it("in text()", async () => {
const blob = new Blob(["\uFEFFHello, World! 🌎"], { type: "text/plain" });
expect(await blob.text()).toBe("Hello, World! 🌎");
});
it("in json()", async () => {
const blob = new Blob(['\uFEFF{"hello":"World 🌎"}'], { type: "application/json" });
expect(await blob.json()).toStrictEqual({ "hello": "World 🌎" } as any);
});
it("in formData()", async () => {
const blob = new Blob(["\uFEFFhello=world 🌎"], { type: "application/x-www-form-urlencoded" });
const formData = await blob.formData();
expect(formData.get("hello")).toBe("world 🌎");
});
});
it("in text()", async () => {
const blob = new Blob(["\uFEFFHello, World!"], { type: "text/plain" });
expect(await blob.text()).toBe("Hello, World!");