diff --git a/docs/api/import-meta.md b/docs/api/import-meta.md index 2c821f751015d..34c265bee4a0a 100644 --- a/docs/api/import-meta.md +++ b/docs/api/import-meta.md @@ -3,9 +3,11 @@ The `import.meta` object is a way for a module to access information about itsel Bun implements the following properties. ```ts#/path/to/project/file.ts -import.meta.dir; // => "/path/to/project" -import.meta.file; // => "file.ts" -import.meta.path; // => "/path/to/project/file.ts" +import.meta.dir; // => "/path/to/project" +import.meta.dirname; // => "/path/to/project" +import.meta.file; // => "file.ts" +import.meta.filename; // => "/path/to/project/file.ts" +import.meta.path; // => "/path/to/project/file.ts" import.meta.main; // `true` if this file is directly executed by `bun run` // `false` otherwise @@ -23,11 +25,21 @@ import.meta.resolveSync("zod") --- +- `import.meta.dirname` +- An alias to `import.meta.dir` + +--- + - `import.meta.file` - The name of the current file, e.g. `index.tsx` --- +- `import.meta.filename` +- An alias to `import.meta.path` or `${import.meta.dir}/${import.meta.file}` + +--- + - `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) diff --git a/packages/bun-types/globals.d.ts b/packages/bun-types/globals.d.ts index 0d6b39a9d0269..83d4ecf786925 100644 --- a/packages/bun-types/globals.d.ts +++ b/packages/bun-types/globals.d.ts @@ -302,10 +302,20 @@ interface ImportMeta { * Does not have a trailing slash */ readonly dir: string; + /** + * Absolute path to the directory containing the source file. (alias for `dir` to match Node.js) + * + * Does not have trailing slash + */ + readonly dirname: string; /** * Filename of the source file */ readonly file: string; + /** + * Absolute path with filename of the source file (to match Node.js) + */ + readonly filename: string; /** * The environment variables of the process * diff --git a/src/bun.js/bindings/ImportMetaObject.cpp b/src/bun.js/bindings/ImportMetaObject.cpp index 2818fa852e0d5..2dcf09b0370cd 100644 --- a/src/bun.js/bindings/ImportMetaObject.cpp +++ b/src/bun.js/bindings/ImportMetaObject.cpp @@ -417,11 +417,13 @@ JSC_DEFINE_HOST_FUNCTION(functionImportMeta__resolve, enum class ImportMetaPropertyOffset : uint32_t { url, dir, + dirname, file, + filename, path, require, }; -static constexpr uint32_t numberOfImportMetaProperties = 5; +static constexpr uint32_t numberOfImportMetaProperties = 7; Zig::ImportMetaObject* ImportMetaObject::create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, const WTF::String& url) { @@ -454,6 +456,14 @@ JSC_DEFINE_CUSTOM_GETTER(jsImportMetaObjectGetter_dir, (JSGlobalObject * globalO return JSValue::encode(thisObject->dirProperty.getInitializedOnMainThread(thisObject)); } +JSC_DEFINE_CUSTOM_GETTER(jsImportMetaObjectGetter_dirname, (JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, PropertyName propertyName)) +{ + ImportMetaObject* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (UNLIKELY(!thisObject)) + return JSValue::encode(jsUndefined()); + + return JSValue::encode(thisObject->dirnameProperty.getInitializedOnMainThread(thisObject)); +} JSC_DEFINE_CUSTOM_GETTER(jsImportMetaObjectGetter_file, (JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, PropertyName propertyName)) { ImportMetaObject* thisObject = jsDynamicCast(JSValue::decode(thisValue)); @@ -462,6 +472,14 @@ JSC_DEFINE_CUSTOM_GETTER(jsImportMetaObjectGetter_file, (JSGlobalObject * global return JSValue::encode(thisObject->fileProperty.getInitializedOnMainThread(thisObject)); } +JSC_DEFINE_CUSTOM_GETTER(jsImportMetaObjectGetter_filename, (JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, PropertyName propertyName)) +{ + ImportMetaObject* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (UNLIKELY(!thisObject)) + return JSValue::encode(jsUndefined()); + + return JSValue::encode(thisObject->filenameProperty.getInitializedOnMainThread(thisObject)); +} JSC_DEFINE_CUSTOM_GETTER(jsImportMetaObjectGetter_path, (JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, PropertyName propertyName)) { ImportMetaObject* thisObject = jsDynamicCast(JSValue::decode(thisValue)); @@ -489,7 +507,9 @@ static const HashTableValue ImportMetaObjectPrototypeValues[] = { { "resolveSync"_s, static_cast(JSC::PropertyAttribute::Function | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::NativeFunctionType, functionImportMeta__resolveSync, 0 } }, { "url"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, jsImportMetaObjectGetter_url, 0 } }, { "dir"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, jsImportMetaObjectGetter_dir, 0 } }, + { "dirname"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, jsImportMetaObjectGetter_dir, 0 } }, { "file"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, jsImportMetaObjectGetter_file, 0 } }, + { "filename"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, jsImportMetaObjectGetter_filename, 0 } }, { "path"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, jsImportMetaObjectGetter_path, 0 } }, { "require"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, jsImportMetaObjectGetter_require, 0 } }, { "env"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, jsImportMetaObjectGetter_env, 0 } }, @@ -615,6 +635,28 @@ void ImportMetaObject::finishCreation(VM& vm) init.set(jsString(init.vm, dirname)); }); + this->dirnameProperty.initLater([](const JSC::LazyProperty::Initializer& init) { + ImportMetaObject* meta = jsCast(init.owner); + + WTF::URL url = isAbsolutePath(meta->url) ? WTF::URL::fileURLWithFileSystemPath(meta->url) : WTF::URL(meta->url); + WTF::String dirname; + + if (url.isValid()) { + if (url.protocolIsFile()) { + dirname = url.fileSystemPath(); + } else { + dirname = url.path().toString(); + } + } + + if (dirname.endsWith(PLATFORM_SEP_s)) { + dirname = dirname.substring(0, dirname.length() - 1); + } else if (dirname.contains(PLATFORM_SEP)) { + dirname = dirname.substring(0, dirname.reverseFind(PLATFORM_SEP)); + } + + init.set(jsString(init.vm, dirname)); + }); this->fileProperty.initLater([](const JSC::LazyProperty::Initializer& init) { ImportMetaObject* meta = jsCast(init.owner); @@ -641,6 +683,24 @@ void ImportMetaObject::finishCreation(VM& vm) init.set(jsString(init.vm, filename)); }); + this->filenameProperty.initLater([](const JSC::LazyProperty::Initializer& init) { + ImportMetaObject* meta = jsCast(init.owner); + + WTF::URL url = isAbsolutePath(meta->url) ? WTF::URL::fileURLWithFileSystemPath(meta->url) : WTF::URL(meta->url); + WTF::String path; + + if (!url.isValid()) { + path = meta->url; + } else { + if (url.protocolIsFile()) { + path = url.fileSystemPath(); + } else { + path = url.path().toString(); + } + } + + init.set(jsString(init.vm, path)); + }); this->pathProperty.initLater([](const JSC::LazyProperty::Initializer& init) { ImportMetaObject* meta = jsCast(init.owner); @@ -666,7 +726,9 @@ void ImportMetaObject::visitChildrenImpl(JSCell* cell, Visitor& visitor) fn->requireProperty.visit(visitor); fn->urlProperty.visit(visitor); fn->dirProperty.visit(visitor); + fn->dirnameProperty.visit(visitor); fn->fileProperty.visit(visitor); + fn->filenameProperty.visit(visitor); fn->pathProperty.visit(visitor); } diff --git a/src/bun.js/bindings/ImportMetaObject.h b/src/bun.js/bindings/ImportMetaObject.h index 02b911af0524c..0fe6d5298d7e8 100644 --- a/src/bun.js/bindings/ImportMetaObject.h +++ b/src/bun.js/bindings/ImportMetaObject.h @@ -52,8 +52,10 @@ class ImportMetaObject final : public JSC::JSNonFinalObject { WTF::String url; LazyProperty requireProperty; LazyProperty dirProperty; + LazyProperty dirnameProperty; LazyProperty urlProperty; LazyProperty fileProperty; + LazyProperty filenameProperty; LazyProperty pathProperty; private: diff --git a/test/js/bun/resolve/import-meta.test.js b/test/js/bun/resolve/import-meta.test.js index dc76a1e1946a7..1f9f064d2f0e8 100644 --- a/test/js/bun/resolve/import-meta.test.js +++ b/test/js/bun/resolve/import-meta.test.js @@ -6,7 +6,7 @@ import * as Module from "node:module"; import { join } from "node:path"; import sync from "./require-json.json"; -const { path, dir } = import.meta; +const { path, dir, dirname, file, filename } = import.meta; it("import.meta.main", () => { const { exitCode } = spawnSync({ @@ -190,6 +190,18 @@ it("import.meta.dir", () => { expect(dir.endsWith("/bun/test/js/bun/resolve")).toBe(true); }); +it("import.meta.dirname", () => { + expect(dirname.endsWith("/bun/test/js/bun/resolve")).toBe(true); +}); + +it("import.meta.file", () => { + expect(file.endsWith("import-meta.test.js")).toBe(true); +}); + +it("import.meta.filename", () => { + expect(filename.endsWith("/bun/test/js/bun/resolve/import-meta.test.js")).toBe(true); +}); + it("import.meta.path", () => { expect(path.endsWith("/bun/test/js/bun/resolve/import-meta.test.js")).toBe(true); }); diff --git a/test/regression/issue/07778.test.ts b/test/regression/issue/07778.test.ts new file mode 100644 index 0000000000000..abd31c7744e1c --- /dev/null +++ b/test/regression/issue/07778.test.ts @@ -0,0 +1,30 @@ +import { bunRunAsScript } from "harness"; +import { tempDirWithFiles } from "harness"; +import path from "path"; + +it("has import.meta behaviour that matches node.js behaviour", () => { + const fileData = ` + console.log(import.meta.dir); + console.log(import.meta.dirname); + console.log(import.meta.file); + console.log(import.meta.filename); + `; + + const testDir = tempDirWithFiles("07778", { + "test.mjs": fileData, + }); + + const { stdout, stderr } = bunRunAsScript(testDir, "test.mjs"); + + expect(stdout).toBeDefined(); + + const [dir, dirname, file, filename] = stdout.split("\n"); + + expect(dir).toEqual(testDir); + expect(dirname).toEqual(testDir); + + expect(file).toEqual("test.mjs"); + expect(filename).toEqual(path.join(testDir, "test.mjs")); + + expect(stderr).toBeDefined(); +});