Do we have other granular levels for visibility?

In ESM, a function is either limited to the module (file) or is globally available.
Once a function or anything else is exported, anyone can import it.

Do we have other granular levels?

Why do I ask for this? We tend to take code cleanness to extremes. We break a big coherent file into smaller files and wrap them inside a directory.

For example, instead of having a big file called Database.js with a thousand lines and 20 functions, we create a directory called Database and then break that file into 20 files each with 50 lines or so. Then we create a file inside it called Database.js and import functions from those 20 files, and at last you can consider this:

import { connect } from "./Connection.js"
import { get } from " ./Get.js"
import { run } from "./Run.js"
import { bulkInsert } from "./BulkInsert.js"
// other imports here

export const db = {
    connect,
    get,
    run,
    bulkInsert,
}

The only problem with this method is that each of those functions is available independently.

Is there a way to limit their visibility to the enclosing directory only?

We don't want developers to do this:

import { get } from "./path-to-database-get.js"

const result = await get("some query")

Rather, we want them to do this:

import { db } from "./path-to-database.js"

const result = await db.get("some query")

We can create static code analysis for that of course. But if we could physically prevent it, that would have made much more value.

For example, consider this imaginary export:

exportDir const get = async query => {
}

And this get method would be visible only for sibling files inside the same directory.

Every scope is a granular level of visibility/reachability.

For your specific question, in node you’d do that by putting the database stuff in a package, with the exports field defined. In a browser, you’d probably want to use a bundler and not expose the individual modules at all.

I do feel like static analysis can be a useful way to solve this problem as well. I've been fond of dependency cruiser which lets you described arbitrary rules you would like enforced, such as "you can not import from a file starting with an underscore unless you are either in the directory containing that file, or one of the sub-directories of that parent one", or something like that.

It would be nice if there was native support for this sort of thing as well, but it gets tricky, because how does JavaScript know if it is allowed to import a particular file or not, without, for example, making a bunch of extra requests to look for config files or something telling it what it can and can not import.

1 Like

@theScottyJam, that's an interesting npm package.
The point is there would be no config file and no need for extra requests.

I asked ChatGPT for a table of access modifiers in different languages:

Language Modifier Scope Description
C# public Accessible from anywhere
internal Accessible within the same assembly
protected Accessible within the class and derived classes
private Accessible only within the class
protected internal Accessible within the same assembly and derived classes
private protected Accessible within the same class or derived classes in the same assembly
Java public Accessible from anywhere
protected Accessible within the class, package, and subclasses
(default) Accessible within the same package (no keyword needed)
private Accessible only within the class
C++ public Accessible from anywhere
protected Accessible within the class and derived classes
private Accessible only within the class
Python (default) Public by default (no strict access control)
_protected Convention: intended for internal use but still accessible
__private Name-mangled to prevent accidental access
JavaScript (default) Public by default (no strict access control)
#private Accessible only within the class (ES6 private fields)
TypeScript public Accessible from anywhere
protected Accessible within the class and derived classes
private Accessible only within the class
Swift public Accessible from anywhere
internal Accessible within the same module
fileprivate Accessible within the same file
private Accessible only within the enclosing declaration
Kotlin public Accessible from anywhere
internal Accessible within the same module
protected Accessible within the class and derived classes
private Accessible only within the class
PHP public Accessible from anywhere
protected Accessible within the class and derived classes
private Accessible only within the class
Go (default) Public if identifier starts with an uppercase letter
(lowercase) Private if identifier starts with a lowercase letter
Rust pub Accessible from anywhere
pub(crate) Accessible within the same crate
pub(super) Accessible within the parent module
pub(in path) Accessible within a specific module

I think C#'s internal would be a perfect candidate here. Since in JS we don't have assemblies (other than Wasm) and usually, packages are directories and whatever is inside them or their subdirectories, I think this can be helpful:

export internal const someFunction = () => {

This function would only be importable by sibling files or from subdirectories. But not from the parent directories. And it should be easy to check.

The JavaScript specification has no notion of file locations or directories. Where the code comes from is up to the "host" environment.

Similar discussion here: Package Scope Proposal

Does this mean that the Node.js or the browser manages loading code from "directories and files"?

Or in other words, does this mean that in theory I can create a host that loads code from database?

Exactly. The host determines what it means to import a particular string, if it's a filesystem path or a http url, or could be from an in-memory database. The core language does not specify.

That's an interesting architecture. However, there still needs to be some keyword to specify the scope of visibility. It's an abstract idea. Let's say I want to create a host that loads strings from database.

My host sees this code:

import { someModule } form "somePath"

I will search my database for something like this:

export { someModule }

And if I find it, I load that record. Otherwise, I know it's not exported thus I won't load it.

This is 0 or 1 visibility. Either something is not visible to the outside world (other records, other files, other urls) or it's visible to the entire world.

What do you think? How can I limit the visibility? How can we get gray between black and white?

A host implements a HostLoadImportedModule hook, which knows what the current module is and what the requested module's path is. You could say make it fail to resolve a module if the requested module's path is not within the current module's visibiilty. In Node, you could easily do this with a custom module resolver/loader.

I have not created a resolver/loader for Node. I do not have any experience in that field. Does my custom resolver/loader replaces the Node's default resolver?

Because if it does, then that's not a good solution. I should reinvent a big wheel, just to add a small functionality.

But if my custom resolver can be added to a pipe, like the middleware architecture, then that's fantastic. I can add a small code to check that the importing module belongs to the family and otherwise throw error and break the build with a clear message.

So, do you know which one is the case?

It's a middleware

1 Like

It’s indeed a chain; you don’t have to reinvent anything.

1 Like