Skip to content

Commit

Permalink
feat(runtime): align import.meta.resolve with node.js's implementation (
Browse files Browse the repository at this point in the history
#5827)

* works

* works

* a

* fix zig compiler error

* fix things

* [autofix.ci] apply automated fixes

* a

* not done

* finish this

* [autofix.ci] apply automated fixes

* self check

* delete committed generated file (#9717)

* Fix bug with PipeWriter (#9714)

* fix!: do not lookup cwd in which (#9691)

* do not lookup cwd in which

* fix webkit submodule

* fix compilation on linux

* feedback

* default process.env.NODE_ENV to undefined (#9695)

* small changes

* [autofix.ci] apply automated fixes

* fix(windows) fix node-stream tests/ windows file reader/writer (#9718)

* fix canceled onFileRead

* report continue errors and fix closing

* also fix pipe writer

* avoid possible memory leaks

* Propagate errors in open

---------

Co-authored-by: Jarred Sumner <[email protected]>

* posix failures

* windows fixes

* avoid using c++ labels

* add a resolver

* fix compile test AGAIN

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Meghan Denny <[email protected]>
Co-authored-by: Jarred Sumner <[email protected]>
Co-authored-by: Ciro Spaciari <[email protected]>
  • Loading branch information
5 people authored Apr 1, 2024
1 parent d53e6d6 commit 9e6e8b0
Show file tree
Hide file tree
Showing 21 changed files with 364 additions and 204 deletions.
39 changes: 18 additions & 21 deletions docs/api/import-meta.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ Bun implements the following properties.
import.meta.dir; // => "/path/to/project"
import.meta.file; // => "file.ts"
import.meta.path; // => "/path/to/project/file.ts"
import.meta.url; // => "file:///path/to/project/file.ts"

import.meta.main; // `true` if this file is directly executed by `bun run`
// `false` otherwise

import.meta.resolveSync("zod")
// resolve an import specifier relative to the directory
import.meta.resolve("zod"); // => "file:///path/to/project/node_modules/zod/index.js"
```

{% table %}
Expand All @@ -28,13 +28,18 @@ import.meta.resolveSync("zod")

---

- `import.meta.env`
- An alias to `process.env`.

---

- `import.meta.file`
- The name of the current file, e.g. `index.tsx`

---

- `import.meta.path`
- Absolute path to the current file, e.g. `/path/to/project/index.tx`. Equivalent to `__filename` in CommonJS modules (and Node.js)
- Absolute path to the current file, e.g. `/path/to/project/index.ts`. Equivalent to `__filename` in CommonJS modules (and Node.js)

---

Expand All @@ -43,30 +48,22 @@ import.meta.resolveSync("zod")

---

- `import.meta.url`
- A string url to the current file, e.g. `file:///path/to/project/index.tx`

---

- `import.meta.main`
- `boolean` Indicates whether the current file is the entrypoint to the current `bun` process. Is the file being directly executed by `bun run` or is it being imported?
- Indicates whether the current file is the entrypoint to the current `bun` process. Is the file being directly executed by `bun run` or is it being imported?

---

- `import.meta.env`
- An alias to `process.env`.

---

- `import.meta.resolve{Sync}`
- Resolve a module specifier (e.g. `"zod"` or `"./file.tsx"`) to an absolute path. While file would be imported if the specifier were imported from this file?
- `import.meta.resolve`
- Resolve a module specifier (e.g. `"zod"` or `"./file.tsx"`) to a url. Equivalent to [`import.meta.resolve` in browsers](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import.meta#resolve)

```ts
import.meta.resolveSync("zod");
// => "/path/to/project/node_modules/zod/index.ts"

import.meta.resolveSync("./file.tsx");
// => "/path/to/project/file.tsx"
import.meta.resolve("zod");
// => "file:///path/to/project/node_modules/zod/index.ts"
```

---

- `import.meta.url`
- A `string` url to the current file, e.g. `file:///path/to/project/index.ts`. Equivalent to [`import.meta.url` in browsers](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import.meta#url)

{% /table %}
2 changes: 1 addition & 1 deletion docs/api/utils.md
Original file line number Diff line number Diff line change
Expand Up @@ -635,7 +635,7 @@ Bun.resolveSync("zod", "/path/to/project");
// => "/path/to/project/node_modules/zod/index.ts"
```

To resolve relative to the current working directory, pass `process.cwd` or `"."` as the root.
To resolve relative to the current working directory, pass `process.cwd()` or `"."` as the root.

```ts
Bun.resolveSync("./foo.ts", process.cwd());
Expand Down
4 changes: 1 addition & 3 deletions packages/bun-polyfills/src/global/importmeta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,7 @@ export default function polyfillImportMeta(metaIn: ImportMeta) {
dir: path.dirname(metapath),
file: path.basename(metapath),
require: require2,
async resolve(id: string, parent?: string) {
return this.resolveSync(id, parent);
},
resolve: metaIn.resolve,
resolveSync(id: string, parent?: string) {
return require2.resolve(id, {
paths: typeof parent === 'string' ? [
Expand Down
36 changes: 9 additions & 27 deletions packages/bun-types/globals.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1758,39 +1758,23 @@ declare global {
* ```
*/
readonly env: NodeJS.ProcessEnv;
/**
* Resolve a module ID the same as if you imported it
*
* On failure, throws a `ResolveMessage`
*/
resolve(moduleId: string): Promise<string>;
/**
* Resolve a `moduleId` as though it were imported from `parent`
*
* On failure, throws a `ResolveMessage`
*/
// tslint:disable-next-line:unified-signatures
resolve(moduleId: string, parent: string): Promise<string>;

/**
* @deprecated Use `require.resolve` or `Bun.resolveSync(moduleId, path.dirname(parent))` instead
*
* Resolve a module ID the same as if you imported it
*
* The `parent` argument is optional, and defaults to the current module's path.
*/
resolveSync(moduleId: string, parent?: string): string;

/**
* Load a CommonJS module
*
* Internally, this is a synchronous version of ESModule's `import()`, with extra code for handling:
* - CommonJS modules
* - *.node files
* - *.json files
* Load a CommonJS module within an ES Module. Bun's transpiler rewrites all
* calls to `require` with `import.meta.require` when transpiling ES Modules
* for the runtime.
*
* Warning: **This API is not stable** and may change in the future. Use at your
* own risk. Usually, you should use `require` instead and Bun's transpiler
* will automatically rewrite your code to use `import.meta.require` if
* relevant.
* Warning: **This API is not stable** and may change or be removed in the
* future. Use at your own risk.
*/
require: NodeJS.Require;

Expand All @@ -1814,17 +1798,15 @@ declare global {
readonly main: boolean;

/** Alias of `import.meta.dir`. Exists for Node.js compatibility */
dirname: string;
readonly dirname: string;

/** Alias of `import.meta.path`. Exists for Node.js compatibility */
filename: string;
readonly filename: string;
}

/**
* NodeJS-style `require` function
*
* Internally, uses `import.meta.require`
*
* @param moduleId - The module ID to resolve
*/
var require: NodeJS.Require;
Expand Down
21 changes: 13 additions & 8 deletions src/bun.js/api/BunObject.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1211,6 +1211,19 @@ export fn Bun__resolveSync(
};
}

export fn Bun__resolveSyncWithStrings(
global: *JSGlobalObject,
specifier: *bun.String,
source: *bun.String,
is_esm: bool,
) JSC.JSValue {
Output.scoped(.importMetaResolve, false)("source: {s}, specifier: {s}", .{ source.*, specifier.* });
var exception = [1]JSC.JSValueRef{null};
return doResolveWithArgs(global, specifier.*, source.*, &exception, is_esm, true) orelse {
return JSC.JSValue.fromRef(exception[0]);
};
}

export fn Bun__resolveSyncWithSource(
global: *JSGlobalObject,
specifier: JSValue,
Expand All @@ -1227,14 +1240,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) {
Expand Down
111 changes: 76 additions & 35 deletions src/bun.js/bindings/ImportMetaObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -345,55 +345,96 @@ JSC_DEFINE_HOST_FUNCTION(functionImportMeta__resolve,
JSC::VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(globalObject->vm());

switch (callFrame->argumentCount()) {
case 0: {
// 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 {});
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) {
JSValue fromValue = callFrame->uncheckedArgument(1);

if (!fromValue.isUndefinedOrNull() && fromValue.isObject()) {
if (JSValue 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;
}
}
default: {
JSC::JSValue moduleName = callFrame->argument(0);

if (moduleName.isUndefinedOrNull()) {
if (!from) {
auto* 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 {}));
}
}
ASSERT(from);

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()));
RETURN_IF_EXCEPTION(scope, JSC::JSValue::encode(JSC::JSValue {}));
// 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)
#if OS(WINDOWS)
|| specifier.startsWith(".\\"_s) || specifier.startsWith("..\\"_s) || specifier.startsWith("\\"_s)
#endif
) {
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 {}));
}

if (globalObject->onLoadPlugins.hasVirtualModules()) {
if (moduleName.isString()) {
auto moduleString = moduleName.toWTFString(globalObject);
if (auto resolvedString = globalObject->onLoadPlugins.resolveVirtualModule(moduleString, JSValue::decode(from).toWTFString(globalObject))) {
if (moduleString == resolvedString.value())
return JSC::JSValue::encode(JSPromise::resolvedPromise(globalObject, moduleName));
return JSC::JSValue::encode(JSPromise::resolvedPromise(globalObject, jsString(vm, resolvedString.value())));
}
}
}
WTF::URL url(fromURL, specifier);
RELEASE_AND_RETURN(scope, JSValue::encode(jsString(vm, url.string())));
}

return Bun__resolve(globalObject, JSC::JSValue::encode(moduleName), from, true);
// In Node.js, `node:doesnotexist` resolves to `node:doesnotexist`
if (UNLIKELY(specifier.startsWith("node:"_s)) || UNLIKELY(specifier.startsWith("bun:"_s))) {
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 {});
}

auto resultString = result.toWTFString(globalObject);
if (isAbsolutePath(resultString)) {
// file path -> url
RELEASE_AND_RETURN(scope, JSValue::encode(jsString(vm, WTF::URL::fileURLWithFileSystemPath(resultString).string())));
}
return JSValue::encode(result);
}

JSC_DEFINE_CUSTOM_GETTER(jsImportMetaObjectGetter_url, (JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, PropertyName propertyName))
Expand Down
1 change: 1 addition & 0 deletions src/bun.js/bindings/ImportMetaObject.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down
13 changes: 8 additions & 5 deletions src/bun.js/bindings/ZigGlobalObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3844,16 +3844,19 @@ JSC::JSInternalPromise* GlobalObject::moduleLoaderImportModule(JSGlobalObject* j
} else {
moduleNameZ = Bun::toStringRef(moduleName);
}
auto sourceOriginZ = sourceURL.isEmpty() ? BunStringCwd : Bun::toStringRef(sourceURL.fileSystemPath());
auto sourceOriginZ = sourceURL.isEmpty() ? BunStringCwd
: sourceURL.protocolIsFile()
? Bun::toStringRef(sourceURL.fileSystemPath())
: sourceURL.protocol() == "builtin"_s
? // On Windows, drive letter from standalone mode gets put into the URL host
Bun::toStringRef(sourceURL.string().substring(10 /* builtin:// */))
: Bun::toStringRef(sourceURL.path().toString());
ZigString queryString = { 0, 0 };
resolved.success = false;
Zig__GlobalObject__resolve(&resolved, globalObject, &moduleNameZ, &sourceOriginZ, &queryString);
moduleNameZ.deref();
sourceOriginZ.deref();
#if BUN_DEBUG
// TODO: ASSERT doesnt work right now
RELEASE_ASSERT(startRefCount == moduleName.impl()->refCount());
#endif
ASSERT(startRefCount == moduleName.impl()->refCount());
if (!resolved.success) {
throwException(scope, resolved.result.err, globalObject);
return promise->rejectWithCaughtException(globalObject, scope);
Expand Down
Loading

0 comments on commit 9e6e8b0

Please sign in to comment.