# Package Scope Proposal

**URL:** <https://es.discourse.group/t/package-scope-proposal/1860>\
**Category:** 💡 Ideas\
**Created:** [December 12, 2023, 10:22pm UTC](https://es.discourse.group/t/package-scope-proposal/1860 "2023-12-12T22:22:45Z")\
**Posts on this page:** 7\
**Page:** 1

<div class="post-metadata">

**Author:** ![ducin](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/ducin/32/1949_2.png) [@ducin](https://es.discourse.group/u/ducin)\
**Post date:** [December 12, 2023, 10:22pm UTC](https://es.discourse.group/t/package-scope-proposal/1860/1 "2023-12-12T22:22:45Z")

</div>

# Package Scope Proposal

A proposal for **restricting imports** within ES Modules related to a new concept called _packages_. It allows to `expose` items which later could be `import`-ed **not** by all JS Modules, but according to package hierarchy.

## Motivation

Within a JS codebase based on ES modules, it's possible to `import` everything which is `export`-ed. On one hand it's expected, but on the other hand, in big codebases (e.g. large business applications, but also complex libraries) there's no _native_ way to **restrict** which part of the project is able to depend on another one. And introducing a certain tool (which delivers such solution) in not always possible.

In different words, if a JS _module_ is implemented as a regular text file, the ecosystem lacks an entity that would be a higher abstraction over a module that doesn't necessarily have to be installed via a package manager (and e.g. be loaded from `node_modules`).

Moreover, lack of encapsulation is a never-ending, re-occurring issue in many JS-based projects. This proposal aims to improve this area.

One more additional benefit of this proposal is that, due to restricting possible imports relying on package hierarchy, visualizing project structure and dependency hierarchy would get simpler, as packages would be considered in the first place, not just imports (if packages would get commonly used, imports would be restricted to packages anyway).

## Proposal

This proposal introduces `package`s - a **hierarchy** of files and directories logically connected to each other. All JS Modules belong to a package (if it's not explicitly stated, they belong to the `default` package - see _Backwards compatibility_ section). Each JS Module can be explicitly labelled as _belonging to a package_ defined by a slash-separated string name.

The hierarchy of packages consists of following terms:

- _sub-package_ - a lesser item within the hierarchy
- _super-package_ - inversion of sub-package
- _own package_ - the same package (file A want to import from file B, both within the same package, so they import from their _own_ package)
- _unrelated package_ - different packages where neither is a sub-package of the other

Members of a package `expose` what can be `import`-ed. The new `expose` keyword works similarly to `import`, but throws an error when trying to _import_ something that belongs to neither this exact package or a its sub-package. Following rules apply:

- if an item is `export`-ed (ES2015), it can be `import`-ed everywhere, no matter the package hierarchy
- if an item is `expose`-d (this proposal), it can be `import`-ed only within its _own package_ or its _super-packages_ (all the way up the hierarchy). Explicitly, it cannot be imported in _sub-packages_ or _unrelated packages_.

### Example

Given the following directory/file structure:

```javascript
src/
  app/
    components/
      ...
    index.js
    ...
  lib/
    sync.js // -> labelled as "lib" package
    events/
      broker.js // -> labelled as "lib/events" package
      index.js
      pubsub.js // -> labelled as "lib/events" package
      __tests__ /
        broker.spec.js
        pubsub.spec.js
    state/
      message.js
      store.js // -> labelled as "lib/state" package
  ...

```

Certain JS modules can be labelled as belonging to a package or not (then it remains in the `default` package). Example package hierarchy could reflect directory structure:

```js
lib
lib/events
lib/state
app
...

```

but **it doesn't have to** (if needed, package structure and directory structure can deviate).

Within the above structure:

- `lib` is the _super-package_ to both `lib/events` and `/lib/state`
- both `lib/events` and `/lib/state` are _sub-packages_ to `lib`
- `lib/events` and `/lib/state` are _unrelated packages_ to each other
- files `src/lib/events/broker.js` and `src/lib/events/pubsub.js` live in their _own package_, `lib/events` (e.g. when one `import`-s from the other)

Following files `expose`:

- `src/lib/sync.js` exposes `function sync(){}`
- `src/lib/events/broker.js` exposes `const broker = {}`
- `src/lib/state/store.js` exposes `const store = {}`

In above hierarchy:

- file `src/lib/sync.js` can `import { broker } "./events/broker.js"` ( **import from sub-package is allowed** )
- file `src/lib/events/broker.js` **cannot** `import { sync } from "../sync"` ( **import from super-package is NOT allowed** )
- file `src/lib/events/broker.js` **cannot** `import { store } from "../state/store"` ( **import from an unrelated package is NOT allowed** )

Importing from packages/sub-packages relies solely on **package hierarchy** , not directory/file structure (although it would be handy to keep them in sync anyway, but that's beyond the proposal).

### Syntax

```js
// file: src/lib/events/broker.js
package "lib/events";

const symbol = Symbol('BROKER') // cannot be imported

expose const broker = {} // can be imported only from "lib/events" or its super-packages

export function notifyBroker(){} // can be imported everywhere - as it is today

```

Labelling a JS Module as belonging to a package is **the first line** of the module (for the sake of readability).

Two new keywords are introduced: `package` and `expose`. The `expose` keyword is not available within the default package (or lack of explicit `package` statement), as it doesn't make sense.

Packages form a hierarchy in a similar way as directories - through the `/` char. This way:

- `lib` is the the _super-package_ of `lib/events`
- `lib/events` is the the _sub-package_ of `lib`
- `lib/events` and `lib/state` are _unrelated_ packages.

### Scopes and Visibility

#### `expose` vs `export`

- `expose`-d items are importable within its _own package_ or its _super-packages_
- `export`-ed items are importable in all packages

#### Object Properties

This proposal doesn't interfere with private properties/methods/... (`#`) in any way. It relates only to module exports.

## Backwards compatibility

Each and every module belongs to a package (or sub-package). To guarantee backwards compatibility without any breaking changes, the `default` package is introduced:

```js
package "default";

// some code ...

```

is equivalent to:

```js
// some code ...

```

The contents of the `default` package cannot be `expose`-d. Hence, they are importable according to what is `export`-ed and what is not.

### compatibility with existing npm packages

The `export` keyword (or `module.exports`) works as before - items can be imported within all packages.

The `expose`-d items **cannot** be imported.

## Prior Art & Userland implementations

Maintainers of big codebases struggle to achieve a way to disallow unwanted imports. Currently the following solutions have been identified to serve a similar purpose:

### [ESLint's `no-restricted-imports`](https://eslint.org/docs/latest/rules/no-restricted-imports)

example:

```json
"no-restricted-imports": ["error", {
    "paths": ["import1", "import2"],
    "patterns": ["import1/private/*", "import2/*", "!import2/good"]
}]

```

Despite additional tooling, this lint is:

- **quite limited** - not hierarchical (!), each pattern/path has to be explicitly declared, etc.
- **centralized - cumbersome to maintain in big codebases** , as the rules are separate from the code itself. All ESLint rules are stored together far away from the code itself. Refactors (e.g. moving files/directories) require additionally updating the global settings of the linter rule. **Defining restricted imports within the code would be way more convenient in daily work.**

### [NX Module Boundaries](https://nx.dev/core-features/enforce-module-boundaries)

example:

```json
  "@nx/enforce-module-boundaries": [
    "error",
    {
      "allow": [],
      // update depConstraints based on your tags
      "depConstraints": [
        {
          "sourceTag": "scope:shared",
          "onlyDependOnLibsWithTags": ["scope:shared"]
        },
        {
          "sourceTag": "scope:admin",
          "onlyDependOnLibsWithTags": ["scope:shared", "scope:admin"]
        },
        {
          "sourceTag": "scope:client",
          "onlyDependOnLibsWithTags": ["scope:shared", "scope:client"]
        }
      ]
    }
  ]

```

- in some way, NX Module Boundaries influenced this proposal
- **centralized** (same cons as above)
- **limitation** : many codebases cannot be turned into a Nx-specific monorepo only for the purpose of using the mentioned feature. **Native support would be available for all.**

### [Java Packages](https://docs.oracle.com/javase/8/docs/api/java/lang/Package.html)

- in some way, java's _package-private scope_ influenced this proposal

---

<div class="post-metadata">

**Author:** ![bergus](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/bergus/32/152_2.png) [@bergus](https://es.discourse.group/u/bergus)\
**Post date:** [December 13, 2023, 12:29am UTC](https://es.discourse.group/t/package-scope-proposal/1860/2 "2023-12-13T00:29:34Z")

</div>

Are you aware of the [`exports` field in _package.json_](https://nodejs.org/api/packages.html#exports)? It does

> [allow] multiple entry points to be defined, [conditional entry resolution support](https://nodejs.org/api/packages.html#conditional-exports) between environments, and **preventing any other entry points besides those defined in [`"exports"`](https://nodejs.org/api/packages.html#exports)**. This encapsulation allows module authors to clearly define the public interface for their package.  
> For new packages targeting the currently supported versions of Node.js, the [`"exports"`](https://nodejs.org/api/packages.html#exports) field is recommended.

I see you wrote that you want a "native" "entity that would be a higher abstraction over a module that doesn't necessarily have to be installed via a package manager", but really those already exist in the ecosystem. One can create and install packages even without a dedicated package manager.

---

<div class="post-metadata">

**Author:** ![ljharb](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/ljharb/32/8_2.png) [@ljharb](https://es.discourse.group/u/ljharb)\
**Post date:** [December 13, 2023, 12:44am UTC](https://es.discourse.group/t/package-scope-proposal/1860/3 "2023-12-13T00:44:20Z")

</div>

This definitely seems to me to be something a package manager and/or a linter should handle, instead of the language.

Also, eslint-plugin-import's solution is quite hierarchical because eslint configs can be nested.

---

<div class="post-metadata">

**Author:** ![mhofman](https://avatars.discourse-cdn.com/v4/letter/m/f14d63/32.png) [@mhofman](https://es.discourse.group/u/mhofman)\
**Post date:** [December 15, 2023, 2:11am UTC](https://es.discourse.group/t/package-scope-proposal/1860/4 "2023-12-15T02:11:11Z")

</div>

I agree this is a package manager concern. Introducing a notion of package identifiers doesn't prevent any module from claiming to belong in a given package, since these identifiers would be forgeable (string based).

---

<div class="post-metadata">

**Author:** ![ducin](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/ducin/32/1949_2.png) [@ducin](https://es.discourse.group/u/ducin)\
**Post date:** [December 16, 2023, 10:00pm UTC](https://es.discourse.group/t/package-scope-proposal/1860/5 "2023-12-16T22:00:35Z")

</div>

@mhofman could you please explain "forgeable (string based)"? What does that mean in practice? TIA

---

<div class="post-metadata">

**Author:** ![mhofman](https://avatars.discourse-cdn.com/v4/letter/m/f14d63/32.png) [@mhofman](https://es.discourse.group/u/mhofman)\
**Post date:** [December 16, 2023, 11:30pm UTC](https://es.discourse.group/t/package-scope-proposal/1860/6 "2023-12-16T23:30:14Z")

</div>

I meant that any string based package scope identifier concept alone can not effectively restrict the exported code from being directly imported by unintended parties. The unintended party can lie / forge who it is, and claim to be in the same scope.

Only the module loader system is actually in a position to implement such restrictions. And it would have to rely on externally declared or inferred scope association information, not anything declared by the importing and exporting code.

The module loader is not part of the language, but part of the host. On Node, package.json `exports` (and on the Web, import maps) can be used to restrict imports for identifiers that use a package name. However currently these host loaders also support relative/absolute path/url based resolution for identifiers, and do not implement any restriction mechanism for these.

---

<div class="post-metadata">

**Author:** ![shrinktofit](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/shrinktofit/32/1773_2.png) [@shrinktofit](https://es.discourse.group/u/shrinktofit)\
**Post date:** [May 24, 2024, 4:04pm UTC](https://es.discourse.group/t/package-scope-proposal/1860/7 "2024-05-24T16:04:38Z")

</div>

I have an idea for module scope instead. 😅

```js
// =================================
// module: internal/A.js
// =================================
export class A {
  // New syntax!!!
  // Class field can have a name `export(X)`, where `X` is an identifier.
  // - the name is unique
  // - a const variable named `X`
  // representing the field name is generated in current module scope
  // - the variable `X` is auto exported
  export(A_foo) = 1;

  // OK: Access within class itself
  #bar() { this[A_foo] = 2; }
}

// OK: Access within current module
function f(a) { a[A_foo] = 2; }

// =================================
// module: internal/B.js
// =================================
import { A, A_foo } from './A.js'; // Also import the field name

// OK: Access in another module
function f(a) { a[A_foo]; }

// =================================
// module: public.js
// =================================
export { A } from './internal/A.js';

// =================================
// module: user.js
// =================================
import { A } from 'public.js;
function f(a) { a[A_foo]; } // Can not access the field, since can not import the name

```
