Folder encapsulation - Encapsulated folders to be consumed

Encapsulation and privacy are fundamental to code quality.
Currently encapsulation is possible only within the scope of a module. So taking encapsulation seriously can result in huge files. Developers prefer to limit size of files for readability and organization, and hence encapsulation is sacrificed.

Imagine the following React project:

app 
   src
     components
        post 
           Post.jsx
           Post.style.js
           Post.test.jsx
           content 
              PostContent.jsx
              PostContent.style.js
              PostContent.test.jsx
           actions 
              PostActions.jsx
              PostActions.style.js
              PostActions.test.jsx
           comments
              PostComments.jsx
              PostComments.style.js
              PostComments.test.jsx
        settings
           Settings.jsx
           Settings.test.jsx
           Settings.style.js        

The Post component is composed from 3 sub-components - PostContent, PostActions and PostComments. Those are sub-components built specifically for the Post component and in accordance to the Post spec. They should only be used within the post folder; But all exports within the post folder could be imported within Settings.jsx and the entire project. PostContent.style.js` should only be used by PostContent.jsx, but could be imported anywhere.

This forces developers to give names which are unique on the project level, as all export is acessible to the entire project. And yet there are cases of autoComplete mistake, in which due to identical exports in the project, the wrong Container, or the wrong StyleObject or the wrong utility-function are imported.

A popular pattern is to include an index.js in the folder, that re-exports entities from the folder. The developer declares which entities he would like to expose out of that folder. But in practice index.js doesn't create encapsulation. Objects that are exported from another file in the folder could be imported in the entire project. Objects that are reexported from index.js can now be imported from both the index file, and the original file.

Proposal

  • exports.js - existence of files by that name will create an enapsulated folder. exports from that file will be available in the entire project. All other exports in the folder will only be available within the folder.
  • To be backwards compatible user will have to opt in to the feature in package.json

The concept of a "file" doesn't exist in the ECMAScript spec.

However, in node, this problem is solved at the app level with eslint, and at the package level with the "exports" field in package.json.

1 Like

exports field in package.json refers to how a package could be consumed - the ability to consume different paths from a package. I don't think it is related here. Package in general (with or without exports field) has a perfect encapsulation, but it also means an entirely separate project.

I'm asking here about folder within a project.
I understand what you are saying - that folder is out of scope of ECMAScript. ESLint is an interesting direction.

See https://eslint.org/docs/latest/rules/no-restricted-imports and https://www.npmjs.com/package/eslint-plugin-import

I see value in having something of this nature as well. I'm just not sure how best to go about implementing it. The problem with having a special exports.js file, is if I import something like https://example.com/dira/dirb/dirc/dird/resource, I'd be requiring the JavaScript engine to make a bunch of requests to /exports.js, /dira/exports.js, /dira/dirb/exports.js, /dira/dirb/dirc/exports.js, and /dira/dirb/dirc/dird/exports.js to see if any of those exist, so it can decide if it should forbid you from importing that resource. Which is a bit ridiculous. You would perhaps have to follow some sort of naming convention as well, like "folders that start with package_* are expected to have an exports.js file", in order to tell the engine where to expect to find them.

A tool I currently like to use is dependency cruiser, which lets you create whatever import rules you want. So, with it, I can make any folder with an index.js be considered a "package", and from the outside you're only allowed to import resources via the "index.js" file. Of course, adding yet-another-tool to your build system to handle these sorts of things certainly isn't ideal, but it does work.

@ljharb How is the spec written for an import? When you do import whatefer from "...", is the stuff between the "..." implementation specific, where each implementation can choose how to decide what file you're talking about?

Yes, that’s my understanding.

1 Like

https://tc39.es/ecma262/multipage/ecmascript-language-scripts-and-modules.html#sec-hostresolveimportedmodule

1 Like

The lint solution, and the dependency cruiser can both be helpful, but an ideal solution will also integrate with the editor to show only proper import suggestions.

Anyway, it's clearly out of scope for TC39.

Following discussion here created a suggestion on eslint-plugin-import.

2 Likes