Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Suggestion: making exports visible only to the same directory with JSDoc @package tag #41425

Open
5 tasks done
uhyo opened this issue Nov 6, 2020 · 12 comments
Open
5 tasks done
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@uhyo
Copy link
Contributor

uhyo commented Nov 6, 2020

Search Terms

JSDoc package directory package-private

Suggestion

If an export is annotated with @package, that export is visible only to files in the same directory.

I can think of two levels of @package support: a soft one would remove these exports from auto completion if not visible. A hard one would also emit a compile error.

See also: Use JSDoc: @package

Use Cases

We organize source code using directory structure. We often make exports that are meant to be referenced only by sibling modules. However, JavaScript/TypeScript has no idea of scoping based on file system, so we are free to import such "local" exports everywhere. TypeScript could help us do file system-based scoping.

Examples

// ----- src/foo/bar.ts
/**
 * @package
 */
export fooBar = "fooBar";

// ----- src/foo/index.ts
// Can import fooBar from "./bar"
import { fooBar } from "./bar";

// ----- index.ts
// CANNOT import fooBar from "./foo/bar"
import { fooBar } from "./foo/bar";

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.
@shrinktofit
Copy link

I'd recommend configurating these package entries in tsconfig.

@RyanCavanaugh RyanCavanaugh added Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript labels Nov 12, 2020
@uhyo
Copy link
Contributor Author

uhyo commented Jun 28, 2021

Just FYI, I created a userland solution on this as a combination of ESLint plugin and TypeScript Language Service Plugin.

https://github.com/uhyo/eslint-plugin-import-access

However, the implementation of Language Service Plugin is kind of barbarous, so support from TypeScript is still wanted.

@OzelotVanilla
Copy link

I have another suggestion to this: use modifier before export to set the visibility. By default, export is public export that every file can see this.

For example, if you want to export some function only inside of the directory (package), use inpackage export:

For file src/sort/sort.ts:

export function sort(/* some args here */) {  }
inpackage export function quickSortPartition(/* */) {  }

For file src/sort/ordered_stat.ts:

import { quickSortPartition } from "./sort.ts"

function findNthSmallest(/* */)
{
    // Can use partition function here
    quickSortPartition()
}

But for other file src/test.ts:

// Cannot import here
import { quickSortPartition } from "./sort/sort.ts"

@A-Shleifman
Copy link

I've been thinking about this quite a bit recently.

I like the idea of having a prefix for export declarations but:

  1. inpackage - I find it a bit too cumbersome
  2. package - unlike Java, where we explicitly declare packages, we don't have packages in JS. Actually, we do, but those are npm packages, so the term package might be confusing for pure JS developers.
  3. I was thinking about private - personally I like the idea of private export const .... Private class members are only accessible within the same class, while private exports could only be accessible within their directory. But I'm sure some people will be against using private for exports. The other problem - what if we want to make the export available one dir or two dirs above the current one? We could have a types folder which should be accessible by the parent and all children. Something like private(../..) export definitely doesn't look right.

All of this led me to create a plugin (inspired by @uhyo's plugin) that adds support for @scope ../.. comments that define scopes for exports.

I wanted to create a TS decorator, that would make something like this possible:

@scope("../..")
export default "";

but, unfortunately, TS doesn't support export decorators. If TS allows it then at least we would be able to create ESLint/TS plugins that would read the decorators instead of comments (which look quite a bit uglier)

@HQ6968
Copy link

HQ6968 commented Aug 25, 2023

Suggestion : can use dir named "internal" , this dir can be import same level dir file. same as golang internal dir rule !

@decoded4620
Copy link

@A-Shleifman I think TS does support decorators, doesn't it? https://www.typescriptlang.org/docs/handbook/decorators.html Unless the terminology is wrong and you mean @annotations? Either way, I'm pretty sure both are supported in TS at this point, no?

@A-Shleifman
Copy link

@decoded4620, I was talking about export decorators meaning decorators that go before export statements, which are not supported by TS.

@azerum
Copy link

azerum commented Dec 30, 2023

I was thinking about idea similar to one of @HQ6968. Here is list of my ideas/opinions. I've left questions that probably can be answered by somebody with more experience

The idea

  1. Code can be grouped into folders (for the lack of better term I will literally call these "folders"), which have two parts: private and public. As an initial idea, let's say all the private code goes into private/ subfolder, and the rest of code is public:
src/
  foo/
    private/
      foo-private.ts
    foo-public.ts
    bar/
      foo-bar-public.ts
  bar
    bar.ts

So code in module bar.ts can import foo/foo-public.ts, foo/bar/foo-bar-public.ts, but not foo/private/foo-private.ts

  1. Modules inside private/ folders should be excluded from auto complete, unless we are typing code inside their folder.
  • When typing inside bar/bar.ts, we will never see anything from foo/private/*
  • However, when typing inside foo/*, we will see everything from foo/private/*

Scope is given to the entire module, not to individual identifiers

  • Comparing to this proposal and @scope() suggested above, this approach makes everything within a module (i.e. file) public or private

In my opinion, giving scope to individual functions/variables is harder to reason about: to see the entire public API of a component, or entire private API within the component, you would have to browse each individual file, since each file might contain a mix of both

Is there ever a good case to mix public and private things in one module? Can we just split the code in multiple modules for such cases?

Dependencies between folders

It might be useful to declare dependencies between folders, instead of allowing every folder to import any other folder

  • In my opinion, this should be done explicitly at the top of the folder, so you can see what folders does the folder depend on, without having to browse its code

I like how TS Project references do that: each folder has tsconfig.json at its top, with references properties declaring dependencies on other folder. What if TS simply allowed imports from folders listed in references, but not from their private/ subfolders?

NPM dependencies are shared for all folders

I think it's convenient that all such folder would be able to import any NPM packages from the root "package.json". If you want different components of code not to share each other's NPM dependencies, you can use a monorepo (e.g. Rush.js)

Nested folders

What if a folder contains another folder? Can the parent folder access private/ of the child folder? Can the child access the private/ folder of parent? We will need to think about it

Child accessing things of parent

I like idea of #41316, which introduces protected access level - things in parent visible to child. We could have protected/ folder for it

Parent accessing things of child

@scope(../..) allows to declare function/variable accessible from parent folder inside a child folder. In my opinion, folders should be "encapsulated": they can access the surrounding code (e.g. protected things of parent), but the surrounding code should not be able to access internals of component inside

No barrel files

  • Comparing to packlets, we don't have to maintain index.ts files

IMHO, barrel files are tiresome to maintain. It might be a good idea to have all public API declared in one file, but is there practical problem of browsing all folders except private/ to understand the public API of a component?

@A-Shleifman
Copy link

Is there ever a good case to mix public and private things in one module?

Yes! This might not be the best example, but let's say we have a file defining a React Context for an encapsulated Component:

const context = createContext({});

export const ContextProvider = context.Provider; 
// 👆 should only be accessible by the internal component files

export const useComponentContext = () => useContext(context); 
// 👆 should be accessible by any component anywhere wrapped in the Component providing the context

but the surrounding code should not be able to access internals of component inside

Generally, that's true. That's why we're having this conversation, but there are exceptions. The most basic would be an index file re-exporting an export from a child folder.

utils
├── schema-utils/
│   ├── parsers.ts
│   └── validators.ts
└── index.ts

In this case, it should be possible for the index.ts file to import from schema-utils/validators.ts for example. I have a big project with a few of those exceptions even outside index files.

@A-Shleifman
Copy link

I would love a solution to this problem to be integrated into TS, but while we wait, here's a v2 of eslint-plugin-export-scope

@ildede
Copy link

ildede commented Oct 24, 2024

Personally, I would use this kind of export primarly for testing purpose: Exporting a "private" function that could be seen only by the associated test file.

@JoaoSoaresDev
Copy link

This would be a great addition to TS. For me, it would allow me to better break it down my file structure ensuring that modules are self-contained. For e.g., in my React projects, I would have my folder structure like:

src/
  components/
    button/
      index.tsx // contains the TSX
      style.ts // styles are only exported internally, so no other modules outside button/ can access these styles
      validation.ts // same here, can't be access outside
      interface.ts // same here, can't be access outside

If we had something like the internal export that was presented here, this would ensure other components wouldn't use the styles or validation that are only for the button component. In that way I don't need to keep everything within the same file.

This would also allow exporting two modules with same name in different scopes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

10 participants