diff options
-rw-r--r-- | packages/bun-types/globals.d.ts | 20 | ||||
-rw-r--r-- | src/bun.js/api/bun.zig | 21 | ||||
-rw-r--r-- | src/bun.js/bindings/BunString.cpp | 2 | ||||
-rw-r--r-- | src/bun.js/bindings/ImportMetaObject.cpp | 96 | ||||
-rw-r--r-- | src/bun.js/bindings/ImportMetaObject.h | 1 | ||||
-rw-r--r-- | src/bun.js/bindings/JSBuffer.lut.h | 4 | ||||
-rw-r--r-- | test/js/bun/resolve/import-meta.test.js | 10 | ||||
-rw-r--r-- | test/js/bun/resolve/resolve.test.js (renamed from test/js/bun/resolve/resolve-test.js) | 121 | ||||
-rw-r--r-- | test/js/bun/resolve/resolve.test.ts | 3 |
9 files changed, 195 insertions, 83 deletions
diff --git a/packages/bun-types/globals.d.ts b/packages/bun-types/globals.d.ts index 276b86303..07d926fad 100644 --- a/packages/bun-types/globals.d.ts +++ b/packages/bun-types/globals.d.ts @@ -303,23 +303,23 @@ interface ImportMeta { */ readonly file: string; /** - * Resolve a module ID the same as if you imported it - * - * On failure, throws a `ResolveMessage` + * Resolve a module ID the same as if you imported it. Returns a `file:` URL string. If a relative + * or absolute file path is given, this will always succeed. */ - resolve(moduleId: string): Promise<string>; + resolve(moduleId: string): string; /** - * Resolve a `moduleId` as though it were imported from `parent` - * - * On failure, throws a `ResolveMessage` + * Resolve a module ID as though it were imported from `parent`. Returns a `file:` URL string. If + * a relative or absolute file path is given, this will always succeed. */ // tslint:disable-next-line:unified-signatures - resolve(moduleId: string, parent: string): Promise<string>; + resolve(moduleId: string, parent: string): string; /** * Resolve a module ID the same as if you imported it * * The `parent` argument is optional, and defaults to the current module's path. + * + * Will throw if the module does not exist. */ resolveSync(moduleId: string, parent?: string): string; @@ -359,9 +359,7 @@ interface ImportMeta { } /** - * NodeJS-style `require` function - * - * Internally, uses `import.meta.require` + * NodeJS-style `require` function. Supports loading ES modules that do not use top-level `await`. * * @param moduleId - The module ID to resolve */ diff --git a/src/bun.js/api/bun.zig b/src/bun.js/api/bun.zig index 966c82d38..a1446831f 100644 --- a/src/bun.js/api/bun.zig +++ b/src/bun.js/api/bun.zig @@ -876,6 +876,19 @@ export fn Bun__resolveSync( }; } +export fn Bun__resolveSyncWithStrings( + global: *JSGlobalObject, + specifier: *bun.String, + source: *bun.String, + is_esm: bool, +) JSC.JSValue { + var exception_ = [1]JSC.JSValueRef{null}; + var exception = &exception_; + return doResolveWithArgs(global, specifier.*, source.*, exception, is_esm, true) orelse { + return JSC.JSValue.fromRef(exception[0]); + }; +} + export fn Bun__resolveSyncWithSource( global: *JSGlobalObject, specifier: JSValue, @@ -889,14 +902,6 @@ export fn Bun__resolveSyncWithSource( }; } -comptime { - if (!is_bindgen) { - _ = Bun__resolve; - _ = Bun__resolveSync; - _ = Bun__resolveSyncWithSource; - } -} - pub fn getPublicPathJS(globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSC.JSValue { const arguments = callframe.arguments(1).slice(); if (arguments.len < 1) { diff --git a/src/bun.js/bindings/BunString.cpp b/src/bun.js/bindings/BunString.cpp index 416d5d334..21dc52eb8 100644 --- a/src/bun.js/bindings/BunString.cpp +++ b/src/bun.js/bindings/BunString.cpp @@ -381,7 +381,7 @@ extern "C" BunString URL__getHref(BunString* input) return Bun::toStringRef(url.string()); } -extern "C" BunString URL__getHrefJoin(BunString* baseStr, BunString *relativeStr) +extern "C" BunString URL__getHrefJoin(BunString* baseStr, BunString* relativeStr) { auto base = Bun::toWTFString(*baseStr); auto relative = Bun::toWTFString(*relativeStr); diff --git a/src/bun.js/bindings/ImportMetaObject.cpp b/src/bun.js/bindings/ImportMetaObject.cpp index 4160102a5..097992777 100644 --- a/src/bun.js/bindings/ImportMetaObject.cpp +++ b/src/bun.js/bindings/ImportMetaObject.cpp @@ -269,44 +269,88 @@ JSC_DEFINE_HOST_FUNCTION(functionImportMeta__resolve, { JSC::VM& vm = globalObject->vm(); - switch (callFrame->argumentCount()) { - case 0: { - auto scope = DECLARE_THROW_SCOPE(globalObject->vm()); - // not "requires" because "require" could be confusing - JSC::throwTypeError(globalObject, scope, "import.meta.resolve needs 1 argument (a string)"_s); - scope.release(); - return JSC::JSValue::encode(JSC::JSValue {}); - } - default: { - JSC::JSValue moduleName = callFrame->argument(0); + auto scope = DECLARE_THROW_SCOPE(globalObject->vm()); - if (moduleName.isUndefinedOrNull()) { + auto thisValue = callFrame->thisValue(); + auto specifierValue = callFrame->argument(0); + // 1. Set specifier to ? ToString(specifier). + auto specifier = specifierValue.toWTFString(globalObject); + RETURN_IF_EXCEPTION(scope, JSC::JSValue::encode(JSC::JSValue {})); + + // Node.js allows a second argument for parent + JSValue from; + if (callFrame->argumentCount() >= 2) { + auto fromValue = callFrame->uncheckedArgument(1); + + if (!fromValue.isUndefinedOrNull() && fromValue.isObject()) { + if (auto pathsObject = fromValue.getObject()->getIfPropertyExists(globalObject, JSC::Identifier::fromString(vm, "paths"_s))) { + if (pathsObject.isCell() && pathsObject.asCell()->type() == JSC::JSType::ArrayType) { + auto pathsArray = JSC::jsCast<JSC::JSArray*>(pathsObject); + if (pathsArray->length() > 0) { + fromValue = pathsArray->getIndex(globalObject, 0); + RETURN_IF_EXCEPTION(scope, JSC::JSValue::encode(JSC::JSValue {})); + } + } + } + } + + if (fromValue.isString()) { + from = fromValue; + } else + goto use_default_from; + } else { + use_default_from: + JSC::JSObject* thisObject = JSC::jsDynamicCast<JSC::JSObject*>(thisValue); + if (UNLIKELY(!thisObject)) { auto scope = DECLARE_THROW_SCOPE(globalObject->vm()); - JSC::throwTypeError(globalObject, scope, "import.meta.resolve expects a string"_s); - scope.release(); - return JSC::JSValue::encode(JSC::JSValue {}); + JSC::throwTypeError(globalObject, scope, "import.meta.resolve must be bound to an import.meta object"_s); + RELEASE_AND_RETURN(scope, JSC::JSValue::encode(JSC::JSValue {})); } - JSC__JSValue from; + auto clientData = WebCore::clientData(vm); + JSValue pathProperty = thisObject->getIfPropertyExists(globalObject, clientData->builtinNames().pathPublicName()); - if (callFrame->argumentCount() > 1 && callFrame->argument(1).isString()) { - from = JSC::JSValue::encode(callFrame->argument(1)); + if (LIKELY(pathProperty && pathProperty.isString())) { + from = pathProperty; } else { - JSC::JSObject* thisObject = JSC::jsDynamicCast<JSC::JSObject*>(callFrame->thisValue()); - if (UNLIKELY(!thisObject)) { - auto scope = DECLARE_THROW_SCOPE(globalObject->vm()); - JSC::throwTypeError(globalObject, scope, "import.meta.resolve must be bound to an import.meta object"_s); - return JSC::JSValue::encode(JSC::JSValue {}); - } + auto scope = DECLARE_THROW_SCOPE(globalObject->vm()); + JSC::throwTypeError(globalObject, scope, "import.meta.resolve must be bound to an import.meta object"_s); + RELEASE_AND_RETURN(scope, JSC::JSValue::encode(JSC::JSValue {})); + } + } - auto clientData = WebCore::clientData(vm); + // from.toWTFString() *should* always be the fast case, since above we check that it's a string. + auto fromWTFString = from.toWTFString(globalObject); + RETURN_IF_EXCEPTION(scope, JSC::JSValue::encode(JSC::JSValue {})); - from = JSC::JSValue::encode(thisObject->getIfPropertyExists(globalObject, clientData->builtinNames().pathPublicName())); + // Try to resolve it to a relative file path. This path is not meant to throw module resolution errors. + if (specifier.startsWith("./"_s) || specifier.startsWith("../"_s) || specifier.startsWith("/"_s) || specifier.startsWith("file://"_s)) { + auto fromURL = fromWTFString.startsWith("file://"_s) ? WTF::URL(fromWTFString) : WTF::URL::fileURLWithFileSystemPath(fromWTFString); + if (!fromURL.isValid()) { + JSC::throwTypeError(globalObject, scope, "`parent` is not a valid Filepath / URL"_s); + RELEASE_AND_RETURN(scope, JSC::JSValue::encode(JSC::JSValue {})); } - return Bun__resolve(globalObject, JSC::JSValue::encode(moduleName), from, true); + WTF::URL url(fromURL, specifier); + RELEASE_AND_RETURN(scope, JSValue::encode(jsString(vm, url.string()))); } + + // In Node.js, `node:doesnotexist` resolves to `node:doesnotexist` + if (UNLIKELY(specifier.startsWith("node:")) || UNLIKELY(specifier.startsWith("bun:"))) { + return JSValue::encode(jsString(vm, specifier)); } + + // Run it through the module resolver, errors at this point are actual errors. + auto a = Bun::toString(specifier); + auto b = Bun::toString(fromWTFString); + auto result = JSValue::decode(Bun__resolveSyncWithStrings(globalObject, &a, &b, true)); + if (!result.isString()) { + JSC::throwException(globalObject, scope, result); + return JSC::JSValue::encode(JSC::JSValue {}); + } + + // Stringified URL to a file, going off assumption that all modules would be file URLs + RELEASE_AND_RETURN(scope, JSValue::encode(jsString(vm, makeString("file://"_s, result.toWTFString(globalObject))))); } enum class ImportMetaPropertyOffset : uint32_t { diff --git a/src/bun.js/bindings/ImportMetaObject.h b/src/bun.js/bindings/ImportMetaObject.h index 02b911af0..638277d4b 100644 --- a/src/bun.js/bindings/ImportMetaObject.h +++ b/src/bun.js/bindings/ImportMetaObject.h @@ -13,6 +13,7 @@ extern "C" JSC_DECLARE_HOST_FUNCTION(functionImportMeta__resolveSyncPrivate); extern "C" JSC::EncodedJSValue Bun__resolve(JSC::JSGlobalObject* global, JSC::EncodedJSValue specifier, JSC::EncodedJSValue from, bool is_esm); extern "C" JSC::EncodedJSValue Bun__resolveSync(JSC::JSGlobalObject* global, JSC::EncodedJSValue specifier, JSC::EncodedJSValue from, bool is_esm); extern "C" JSC::EncodedJSValue Bun__resolveSyncWithSource(JSC::JSGlobalObject* global, JSC::EncodedJSValue specifier, BunString* from, bool is_esm); +extern "C" JSC::EncodedJSValue Bun__resolveSyncWithStrings(JSC::JSGlobalObject* global, BunString* specifier, BunString* from, bool is_esm); namespace Zig { diff --git a/src/bun.js/bindings/JSBuffer.lut.h b/src/bun.js/bindings/JSBuffer.lut.h index 47e8cb984..35a3acea9 100644 --- a/src/bun.js/bindings/JSBuffer.lut.h +++ b/src/bun.js/bindings/JSBuffer.lut.h @@ -37,8 +37,8 @@ static const struct CompactHashIndex jsBufferConstructorTableIndex[33] = { static const struct HashTableValue jsBufferConstructorTableValues[10] = { { "alloc"_s, static_cast<unsigned>(PropertyAttribute::Constructable|PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsBufferConstructorFunction_alloc, 1 } }, - { "allocUnsafe"_s, static_cast<unsigned>(PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsBufferConstructorFunction_allocUnsafe, 1 } }, - { "allocUnsafeSlow"_s, static_cast<unsigned>(PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsBufferConstructorFunction_allocUnsafeSlow, 1 } }, + { "allocUnsafe"_s, static_cast<unsigned>(PropertyAttribute::Constructable|PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsBufferConstructorFunction_allocUnsafe, 1 } }, + { "allocUnsafeSlow"_s, static_cast<unsigned>(PropertyAttribute::Constructable|PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsBufferConstructorFunction_allocUnsafeSlow, 1 } }, { "byteLength"_s, static_cast<unsigned>(PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsBufferConstructorFunction_byteLength, 2 } }, { "compare"_s, static_cast<unsigned>(PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsBufferConstructorFunction_compare, 2 } }, { "concat"_s, static_cast<unsigned>(PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsBufferConstructorFunction_concat, 2 } }, diff --git a/test/js/bun/resolve/import-meta.test.js b/test/js/bun/resolve/import-meta.test.js index a940d0c87..0b77b74be 100644 --- a/test/js/bun/resolve/import-meta.test.js +++ b/test/js/bun/resolve/import-meta.test.js @@ -197,3 +197,13 @@ it("import non exist error code", async () => { expect(e.code).toBe("ERR_MODULE_NOT_FOUND"); } }); + +it("import.meta.resolve", () => { + expect(import.meta.resolve("./relative-filepath.js")).toBe( + new URL("./relative-filepath.js", import.meta.url).toString(), + ); + expect(import.meta.resolve("../relative-filepath.js")).toBe( + new URL("../relative-filepath.js", import.meta.url).toString(), + ); + expect(import.meta.resolve(".../relative-filepath.js")).toBe(); +}); diff --git a/test/js/bun/resolve/resolve-test.js b/test/js/bun/resolve/resolve.test.js index 17e2b4eb4..6e1100f1d 100644 --- a/test/js/bun/resolve/resolve-test.js +++ b/test/js/bun/resolve/resolve.test.js @@ -1,29 +1,86 @@ import { it, expect } from "bun:test"; -import { join, resolve } from "path"; +import { mkdirSync, writeFileSync } from "fs"; +import { join, resolve as pathResolve } from "path"; + +try { + mkdirSync(join(import.meta.dir, "./node_modules/package-json-exports/foo"), { + recursive: true, + }); +} catch (exception) {} +writeFileSync(join(import.meta.dir, "./node_modules/package-json-exports/foo/bar.js"), "export const bar = 1;"); +writeFileSync( + join(import.meta.dir, "./node_modules/package-json-exports/foo/references-baz.js"), + "export {bar} from 'package-json-exports/baz';", +); +writeFileSync( + join(import.meta.dir, "./node_modules/package-json-exports/package.json"), + JSON.stringify( + { + name: "package-json-exports", + exports: { + "./baz": "./foo/bar.js", + "./references-baz": "./foo/references-baz.js", + }, + }, + null, + 2, + ), +); + +try { + mkdirSync(join(import.meta.dir, "./node_modules/package-json-imports/foo"), { + recursive: true, + }); +} catch (exception) {} +writeFileSync(join(import.meta.dir, "./node_modules/package-json-imports/foo/bar.js"), "export const bar = 1;"); +writeFileSync( + join(import.meta.dir, "./node_modules/package-json-imports/foo/wildcard.js"), + "export const wildcard = 1;", +); +writeFileSync( + join(import.meta.dir, "./node_modules/package-json-imports/foo/private-foo.js"), + "export {bar} from 'package-json-imports/#foo';", +); +writeFileSync( + join(import.meta.dir, "./node_modules/package-json-imports/package.json"), + JSON.stringify( + { + name: "package-json-imports", + exports: { + "./baz": "./foo/bar.js", + }, + imports: { + "#foo/bar": "./foo/private-foo.js", + "#foo/*.js": "./foo/*.js", + "#foo/extensionless/*": "./foo/*.js", + "#foo": "./foo/private-foo.js", + + "#internal-react": "react", + }, + }, + null, + 2, + ), +); function resolveFrom(from) { return specifier => import.meta.resolveSync(specifier, from); } +const resolve = import.meta.require.resolve.bind(import.meta.require); it("#imports", async () => { - const baz = await import.meta.resolve("#foo", join(await import.meta.resolve("package-json-imports/baz"), "../")); + const baz = await resolve("#foo", join(await resolve("package-json-imports/baz"), "../")); expect(baz.endsWith("foo/private-foo.js")).toBe(true); - const subpath = await import.meta.resolve( - "#foo/bar", - join(await import.meta.resolve("package-json-imports/baz"), "../"), - ); + const subpath = await resolve("#foo/bar", join(await resolve("package-json-imports/baz"), "../")); expect(subpath.endsWith("foo/private-foo.js")).toBe(true); - const react = await import.meta.resolve( - "#internal-react", - join(await import.meta.resolve("package-json-imports/baz"), "../"), - ); + const react = await resolve("#internal-react", join(await resolve("package-json-imports/baz"), "../")); expect(react.endsWith("/react/index.js")).toBe(true); // Check that #foo is not resolved to the package.json file. try { - await import.meta.resolve("#foo"); + await resolve("#foo"); throw new Error("Test failed"); } catch (exception) { expect(exception instanceof ResolveMessage).toBe(true); @@ -33,7 +90,7 @@ it("#imports", async () => { // Chcek that package-json-imports/#foo doesn't work try { - await import.meta.resolve("package-json-imports/#foo"); + await resolve("package-json-imports/#foo"); throw new Error("Test failed"); } catch (exception) { expect(exception instanceof ResolveMessage).toBe(true); @@ -43,60 +100,56 @@ it("#imports", async () => { }); it("#imports with wildcard", async () => { - const run = resolveFrom(resolve(import.meta.dir + "/node_modules/package-json-imports/package.json")); + const run = resolveFrom(pathResolve(import.meta.dir + "/node_modules/package-json-imports/package.json")); - const wildcard = resolve(import.meta.dir + "/node_modules/package-json-imports/foo/wildcard.js"); + const wildcard = pathResolve(import.meta.dir + "/node_modules/package-json-imports/foo/wildcard.js"); expect(run("#foo/wildcard.js")).toBe(wildcard); expect(run("#foo/extensionless/wildcard")).toBe(wildcard); }); -it("import.meta.resolve", async () => { - expect(await import.meta.resolve("./resolve-test.test.js")).toBe(import.meta.path); +it("require.resolve", async () => { + expect(await 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 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 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); + expect((await 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 resolve("./resolve-test.test")).toBe(import.meta.path); // works with tsconfig.json "paths" - expect(await import.meta.resolve("foo/bar")).toBe(join(import.meta.path, "../baz.js")); - expect(await import.meta.resolve("@faasjs/baz")).toBe(join(import.meta.path, "../baz.js")); + expect(await resolve("foo/bar")).toBe(join(import.meta.path, "../baz.js")); + expect(await resolve("@faasjs/baz")).toBe(join(import.meta.path, "../baz.js")); // works with package.json "exports" - expect(await import.meta.resolve("package-json-exports/baz")).toBe( + expect(await resolve("package-json-exports/baz")).toBe( join(import.meta.path, "../node_modules/package-json-exports/foo/bar.js"), ); // if they never exported /package.json, allow reading from it too - expect(await import.meta.resolve("package-json-exports/package.json")).toBe( + expect(await resolve("package-json-exports/package.json")).toBe( join(import.meta.path, "../node_modules/package-json-exports/package.json"), ); // if an unnecessary ".js" extension was added, try against /baz - expect(await import.meta.resolve("package-json-exports/baz.js")).toBe( + expect(await resolve("package-json-exports/baz.js")).toBe( join(import.meta.path, "../node_modules/package-json-exports/foo/bar.js"), ); // works with TypeScript compiler edgecases like: // - If the file ends with .js and it doesn't exist, try again with .ts and .tsx - expect(await import.meta.resolve("./resolve-typescript-file.js")).toBe( - join(import.meta.path, "../resolve-typescript-file.tsx"), - ); - expect(await import.meta.resolve("./resolve-typescript-file.tsx")).toBe( - join(import.meta.path, "../resolve-typescript-file.tsx"), - ); + expect(await resolve("./resolve-typescript-file.js")).toBe(join(import.meta.path, "../resolve-typescript-file.tsx")); + expect(await resolve("./resolve-typescript-file.tsx")).toBe(join(import.meta.path, "../resolve-typescript-file.tsx")); // throws a ResolveMessage on failure try { - await import.meta.resolve("THIS FILE DOESNT EXIST"); + await resolve("THIS FILE DOESNT EXIST"); throw new Error("Test failed"); } catch (exception) { expect(exception instanceof ResolveMessage).toBe(true); @@ -117,8 +170,8 @@ it("Bun.resolveSync", () => { }); it("self-referencing imports works", async () => { - const baz = await import.meta.resolve("package-json-exports/baz"); - const namespace = await import.meta.resolve("package-json-exports/references-baz"); + const baz = await resolve("package-json-exports/baz"); + const namespace = await resolve("package-json-exports/references-baz"); Loader.registry.delete(baz); Loader.registry.delete(namespace); var a = await import(baz); diff --git a/test/js/bun/resolve/resolve.test.ts b/test/js/bun/resolve/resolve.test.ts index a9272fb3f..d36b7776b 100644 --- a/test/js/bun/resolve/resolve.test.ts +++ b/test/js/bun/resolve/resolve.test.ts @@ -3,7 +3,8 @@ import { mkdirSync, writeFileSync, existsSync, rmSync, copyFileSync } from "fs"; import { join } from "path"; import { bunExe, bunEnv, tempDirWithFiles } from "harness"; -it("spawn test file", () => { +// make this something else okay +it.skip("spawn test file", () => { writePackageJSONImportsFixture(); writePackageJSONExportsFixture(); |