Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Allow granular visibility for modules #57216

Open
Nefcanto opened this issue Feb 26, 2025 · 17 comments
Open

Allow granular visibility for modules #57216

Nefcanto opened this issue Feb 26, 2025 · 17 comments
Labels
feature request Issues that request new features to be added to Node.js.

Comments

@Nefcanto
Copy link

What is the problem this feature will solve?

I want to share some code only amongst the sibling or descendent files. For example, consider this directory structure:

  • Core
    • Data
    • Validation
    • App
      • Part
        • Extract.js
        • Migrate.js
        • Utils.js

I want the /Core/App/Utils.js to be visible only to the Extract.js and Migrate.js files.

What is the feature you are proposing to solve the problem?

A directive at the top of the file:

internal

What alternatives have you considered?

I reported this requirement to the JS discourse group:

https://es.discourse.group/t/do-we-have-other-granular-levels-for-visibility/2308/1

I think it's better to add a keyword to the language itself:

export internal { someModule }
@Nefcanto Nefcanto added the feature request Issues that request new features to be added to Node.js. label Feb 26, 2025
@github-project-automation github-project-automation bot moved this to Awaiting Triage in Node.js feature requests Feb 26, 2025
@aduh95
Copy link
Contributor

aduh95 commented Feb 26, 2025

I think it's better to add a keyword to the language itself:

Then this is not the right place to ask for it, JavaScript is defined by the ECMAScript spec, changes to the spec are made via proposals

FWIW I think what you're looking for is a lint rule, not a change in syntax.

@aduh95 aduh95 closed this as not planned Won't fix, can't repro, duplicate, stale Feb 26, 2025
@Nefcanto
Copy link
Author

@aduh95 please don't close it immediately as not planned. If you read the ES discourse conversation, one possible way is for the host to provide it.

That's why I asked here too for this feature. Just like "use strict", an internal directive at the top of the file can signal to the Node.js that the modules exported from this file can only be imported from sibling files or descendent files.

@aduh95
Copy link
Contributor

aduh95 commented Feb 26, 2025

What are you expecting from Node.js though? We cannot change the JS language

@Nefcanto
Copy link
Author

@aduh95 I know. The language change should come from the ES group. But since you are the host of the language, you can add a feature to support this through a directive. Is that possible?

I mean, a token that won't break the syntax, means nothing to the ES itself, but has a very specific meaning for the host itself (Node.js).

@Nefcanto
Copy link
Author

@aduh95 also I don't mean a lint rule. I mean gray visibility, between black and white. Not private to the file, neither public to the entire world. Only visible to the family (siblings and descendants).

@aduh95
Copy link
Contributor

aduh95 commented Feb 26, 2025

We'd have to twist the ES spec to get there, there's an assumption that the same ModuleRecord will be shared by all its dependencies (i.e. if you write import * as obj from 'file:///some/file' in several files, obj would ref the same object), so we'd have to either break that assumption or make change to the spec.

I mean gray visibility, between black and white. Not private to the file, neither public to the entire world. Only visible to the family (siblings and descendants).

That really sounds like something TS could be great for

Anyway, let me reopen the issue, even though I'm not convinced there's anything that can happen on Node.js side.

@aduh95 aduh95 reopened this Feb 26, 2025
@Nefcanto
Copy link
Author

@aduh95 thank you for reopening it.

there's an assumption that the same ModuleRecord will be shared by all its dependencies (i.e. if you write import * as obj from 'file:///some/file' in several files, obj would ref the same object)

In other terms, we're talking about singletons. Right? Once a module is imported, Node creates a single instance of it, and uses that instance for other imports. Did I get it right?

so we'd have to either break that assumption or make change to the spec.

I didn't understand this part though. Why changes to that assumption?
Changing the spec would be great. But they have not approved it yet. As I have posted there, almost all languages support something between being private and being universally public. This means that this feature is a well-known feature and would benefit the community.

That really sounds like something TS could be great for

Yes, the TS team can add its own keyword and make some files internal when compiling and even break the compilation. However, TS is not used by all teams. It's better to implement the granular visibility in lower layers. I hope the ES team considers it.

Can't it be done by an internal directive in Node? Something like use client directive in React.

@aduh95
Copy link
Contributor

aduh95 commented Feb 26, 2025

In your example of export internal { someModule }, I expect someModule to be an identifier defined somewhere in the file that would be exposed / importable only to some modules (which would break the expectations as explained in my comment). But it's actually unclear if that's what you're asking, or asking for exposing only some modules, which is what "exports" is for. I would recommend editing the OP to clarify what your actually asking, and explain why "exports" does not work for you

@Nefcanto
Copy link
Author

@aduh95, I think I can give you some examples. That might help clarify:

// utils.js
const someFunction = () => {}

// ../../anotherDirectory/otherFile.js
import { someFunction } from "path-to-utils.js"

This example breaks. Because someFunction is only visible inside the file it is defined in.

// utils2.js
export const utilityFunction2 = () => {

// ../../anotherdirectory/otherFile.js
import { utilityFunction2 } from "path-to-utils2.js"

This resolves. Everyone can import it. Even I think if I package my directory and publish it to npm, it's still importable (I'm not sure though).

Now an example of partial visibility:

// /core/app/part/utils.js
export internal const extractPart = () => {}

// /core/app/validation/isEmail.js
import { extractPart } from "../part/utils.js" // It should break, because `extractPart` is only available to files inside the /core/app/part/**/*.js files

// /core/app/part/migrate.js
import { extractPart } from "./utils.js" // This should resolve, because migrate.js belongs to the same family. The family of all files at /core/app/part/**/*.js

I hope those examples make the requirement clearer. The exports entry is not solving this issue. We are inside a directory. There is no package.json in it.

@ljharb
Copy link
Member

ljharb commented Feb 26, 2025

The host can’t provide syntax in a practical sense, and directives are not a good API. The only way i could see node providing something like this is with separate metadata - and it already does via the “exports” field in packages, somewhat.

This seems much better handled by a linter, and eslint-plugin-import already has rules for exactly that.

@Nefcanto
Copy link
Author

@ljharb, thanks for the response:

directives are not a good API

what argument or evidence supports that? Because big players use them. "use strict", "use client", #!/bin/bash, #nullable enable are all examples of directives and they are very useful and easy to learn and implement.

The only way i could see node providing something like this is with separate metadata

That's why I mean exactly. Even if the metadata is in the file name, like for example "Utils.Internal.js" or even if it comes with a special extension like ijs meaning Internal JavaScript, that can be done by node.

eslint-plugin-import already has rules for exactly that

That would be useful too. I looked at the repo, but could not find the rule. Can you give me a link or so?

@ljharb
Copy link
Member

ljharb commented Feb 27, 2025

Strict mode is why we know directives are not a good API for JavaScript; “use client” isn’t a standard and isn’t universally well-regarded; shebangs are an unavoidable shell legacy, and everything else isn’t JS.

By separate metadata i mean another file - using anything in the filename but the extension would break a lot of tooling and conventions.

Both https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/no-internal-modules.md and https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/no-restricted-paths.md can be used for this purpose.

@aduh95
Copy link
Contributor

aduh95 commented Feb 27, 2025

For example, you can rename the files you don't want to expose to end in .internal.js, and those you want to expose to end in .public.js, and you can put "exports": { "*.js": "*.public.js" } in your package.json and voilà

@Nefcanto
Copy link
Author

@aduh95 I think that is for an npm package, right? I'm inside a directory.

@Nefcanto
Copy link
Author

@ljharb, with respect, you just say "We know it's bad" without giving an argument, or even your personal experience explaining why it's bad.

Regarding the filenames, I understood that if we put anything in the file name (except the extension) then the tools break. How about an extension? We already have .js, .jsx, .tsx, .cjs, and .mjs. These are what I know, we may have other extensions too that I don't know. Is it possible to add the .ijs too?

I looked at the links you sent. Examples are not very clear. I need to read them multiple times. I guess the no-restricted-path is not suitable in this case. It's a conditional import for the server/client. The condition I have in mind is not based on environment, it's based on family. A family means the current directory and every subdirectory (recursive).

@aduh95
Copy link
Contributor

aduh95 commented Feb 28, 2025

@aduh95 I think that is for an npm package, right? I'm inside a directory.

You don’t have to publish your package on npm for it to work, thanks to self-referencing imports; you’d still need a linter to force imports to not use relative specifier to import outside the current directory though

@Nefcanto
Copy link
Author

@aduh95, I read self-referencing imports. I didn't understand it. I don't know how that can help. Thanks for your help though.

# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
feature request Issues that request new features to be added to Node.js.
Projects
Archived in project
Development

No branches or pull requests

3 participants