@crutchcrew/oxlint-plugin-import-access 0.0.0-stage β†’ 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 uhyo
4
+ Copyright (c) 2026 crutchcrew
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,157 @@
1
- # Temporary Holding Version
1
+ # oxlint-plugin-import-access πŸ“¦πŸ”’
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![npm version](https://img.shields.io/npm/v/@crutchcrew/oxlint-plugin-import-access)](https://www.npmjs.com/package/@crutchcrew/oxlint-plugin-import-access)
4
+ [![coverage](https://img.shields.io/endpoint?url=https://crutchcrew.github.io/oxlint-plugin-import-access/badges/coverage.json)](https://github.com/crutchcrew/oxlint-plugin-import-access/actions)
5
+ [![provenance](https://img.shields.io/badge/provenance-verified-brightgreen)](https://www.npmjs.com/package/@crutchcrew/oxlint-plugin-import-access)
6
+ [![license](https://img.shields.io/npm/l/@crutchcrew/oxlint-plugin-import-access)](./LICENSE)
7
+
8
+ JSDoc import access for TypeScript, enforced by [oxlint](https://oxc.rs/docs/guide/usage/linter). Only `@public` exports can be imported from anywhere, and an export without an access tag is `@package`: only files in its own directory can import it. Tag an export `@public` to share it with the whole project, or `@private` so no other file can import it. To make untagged exports `@public` or `@private` instead, set [`defaultImportability`](#options).
9
+
10
+ A native oxlint JS plugin port of [eslint-plugin-import-access](https://github.com/uhyo/eslint-plugin-import-access) that needs **no type checker**: no `typescript` dependency, no `tsconfig.json` project loading, no typed linting setup.
11
+
12
+ ## Access tags
13
+
14
+ ![sub/foo.ts exports api tagged @public, helper without a tag and secret tagged @private. sub/bar.ts, in the same package, can import api and helper but not secret. src/main.ts, outside the package, can import only api.](./docs/images/access.svg)
15
+
16
+ Each export's JSDoc access tag decides which files can import it. A package is a directory with its subdirectories, and by default every directory is one (see [`packageDirectory`](./docs/rule-jsdoc.md#packagedirectory)). In the picture, `sub/foo.ts` has three exports:
17
+
18
+ - `api` is **`@public`**: any file can import it, `sub/bar.ts` as well as `src/main.ts`.
19
+ - `helper` has **no tag**, so it is `@package`: files in `sub/` and its subdirectories can import it, so `sub/bar.ts` can and `src/main.ts` can't.
20
+ - `secret` is **`@private`**: no file can import it, not even `sub/bar.ts` next to it.
21
+
22
+ Imports from Node.js builtins and from anything in `node_modules` are never checked.
23
+
24
+ ## Why?
25
+
26
+ The largest encapsulation unit TypeScript gives you is a file. Once something is exported, the whole project can import it. Keeping things private means keeping them in one file, and files grow large.
27
+
28
+ This plugin adds a directory-level layer between a file and the whole project: an export stays inside its directory (`@package`) until you tag it `@public`. Tag what the rest of the project may use `@public`, and what no other file may use `@private`.
29
+
30
+ ## Why this package
31
+
32
+ | | This package | [eslint-plugin-import-access](https://github.com/uhyo/eslint-plugin-import-access) | [oxlint-plugin-import-access](https://github.com/bitkey-oss/oxlint-plugin-import-access) |
33
+ | -------------------------------------- | ------------ | ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
34
+ | **Linter** | oxlint | ESLint | oxlint |
35
+ | **Needs type information** | ❌ | βœ… typed linting | βœ… TypeScript 7 unstable API |
36
+ | **`typescript` dependency** | ❌ | βœ… | βœ… `^7` |
37
+ | **Works without `tsconfig.json`** | βœ… | ❌ | ❌ |
38
+ | **Checks unsaved edits in the editor** | βœ… | βœ… | ❌ skipped until saved |
39
+ | **Same results as the original rule** | βœ… verifiedΒΉ | – | βœ… |
40
+
41
+ ΒΉ With `defaultImportability: "public"`, which makes exports without an access tag `@public` like in the original. This plugin's default is `"package"`.
42
+
43
+ ## Install
44
+
45
+ Choose your fighter πŸ₯Š
46
+
47
+ ```sh
48
+ pnpm add -D oxlint @crutchcrew/oxlint-plugin-import-access
49
+ ```
50
+
51
+ ```sh
52
+ bun add -D oxlint @crutchcrew/oxlint-plugin-import-access
53
+ ```
54
+
55
+ ```sh
56
+ yarn add -D oxlint @crutchcrew/oxlint-plugin-import-access
57
+ ```
58
+
59
+ ```sh
60
+ npm add -D oxlint @crutchcrew/oxlint-plugin-import-access
61
+ ```
62
+
63
+ Requires Node.js `^20.19.0 || >=22.12.0` (the same as oxlint) and oxlint `>=1.40.0`.
64
+
65
+ ## Configure
66
+
67
+ In **`.oxlintrc.json`**:
68
+
69
+ ```json
70
+ {
71
+ "jsPlugins": ["@crutchcrew/oxlint-plugin-import-access"],
72
+ "rules": {
73
+ "import-access/jsdoc": "error"
74
+ }
75
+ }
76
+ ```
77
+
78
+ Or in **`oxlint.config.ts`**:
79
+
80
+ ```ts
81
+ import { defineConfig } from "oxlint"
82
+
83
+ export default defineConfig({
84
+ jsPlugins: ["@crutchcrew/oxlint-plugin-import-access"],
85
+ rules: {
86
+ // Untagged exports are @package; tag an export @public to import it from anywhere
87
+ "import-access/jsdoc": "error",
88
+ },
89
+ })
90
+ ```
91
+
92
+ That's all: there is no parser or project to configure. Diagnostics also show up in your editor through the [oxc extension](https://oxc.rs/docs/guide/usage/linter/editors) (oxlint's language server).
93
+
94
+ ## Options
95
+
96
+ | Option | Default | Description |
97
+ | ----------------------- | ------------ | ------------------------------------------------------------------------------------------ |
98
+ | `indexLoophole` | `true` | `sub/index.ts` counts as `sub.ts`, so its package-private exports are visible one level up |
99
+ | `filenameLoophole` | `false` | `foo.ts` may import package-private exports from `foo/` |
100
+ | `defaultImportability` | `"package"` | Access of exports without a tag: `"package"` (`@package`), `"public"` or `"private"` |
101
+ | `treatSelfReferenceAs` | `"external"` | Whether imports through your own package name are checked (`"internal"`) or not |
102
+ | `excludeSourcePatterns` | `[]` | Globs (relative to the working directory) of exporter files to skip, e.g. generated code |
103
+ | `packageDirectory` | every dir | Globs of directories that are package boundaries, e.g. `["**", "!**/_internal"]` |
104
+
105
+ All options and access tags (`@public`, `@package`, `@private`, `@access <level>`) behave as in the original plugin, except that `defaultImportability` defaults to `"package"` instead of `"public"`. See the **[rule documentation](./docs/rule-jsdoc.md)** for details and examples.
106
+
107
+ ## How it works
108
+
109
+ The original rule asks the TypeScript type checker which declaration an import refers to. This plugin answers the same question from syntax alone:
110
+
111
+ 1. resolve the import like TypeScript (`tsconfig` `paths`, `package.json` `exports`, `.js` β†’ `.ts`) with [oxc-resolver](https://github.com/oxc-project/oxc-resolver),
112
+ 2. skip builtins and anything reached through `node_modules`,
113
+ 3. read the target file's exports and JSDoc with [oxc-parser](https://github.com/oxc-project/oxc/tree/main/napi/parser) β€” module record and comments only, no syntax tree β€” and cache them,
114
+ 4. follow `export *`, stop at direct re-exports like upstream, and read JSDoc with TypeScript's attachment and tag rules,
115
+ 5. check ambient `declare module "…"` and wildcard `declare module "*.svg"` declarations too.
116
+
117
+ **Verified against the original:** the upstream test suite passes unchanged (same lines, columns, messages), and a differential test compares this plugin with upstream's TypeScript-based rule on every fixture file and option combination, including new fixtures for re-export chains, overloads, enums, `export =`, wildcard modules and JSDoc placement. The [known differences](./docs/rule-jsdoc.md#differences-from-upstream) are edge cases such as module augmentation across files.
118
+
119
+ ## Performance
120
+
121
+ Generated project with nested feature directories, `export *` barrels and 6 imports per module (`bun run bench`), on an Apple M1 (8 cores, Asahi Linux), Node.js 26, oxlint 1.89:
122
+
123
+ | Modules (imports) | ESLint + eslint-plugin-import-access | oxlint + this plugin | Speedup |
124
+ | ----------------- | ------------------------------------ | -------------------- | ------- |
125
+ | 1,000 (6,000) | 1.90 s | **0.31 s** | 6.1Γ— |
126
+ | 2,500 (15,000) | 4.89 s ΒΉ | **0.55 s** | 8.9Γ— |
127
+ | 5,000 (30,000) | fails after 524 s Β² | **0.95 s** | – |
128
+
129
+ ESLint 10 with typescript-eslint 8 typed linting (`projectService`), single run; oxlint best of 3. Both report identical diagnostics on every file the TypeScript-based rule can analyze. Of the 0.95 s, about 0.33 s is oxlint's own cost of running any JS plugin over 5,000 files.
130
+
131
+ ΒΉ TypeScript fails on 16 of the 2,600 files ("Maximum call stack size exceeded"), so those files went unchecked.
132
+ Β² TypeScript fails on every file of this heavily cyclic project.
133
+
134
+ In a long-running editor session, files are re-checked as they change: exporter files are re-validated by size and modification time, and only re-parsed when they changed.
135
+
136
+ ## Coming from eslint-plugin-import-access
137
+
138
+ 1. Install oxlint and this package, and remove `eslint-plugin-import-access` from your ESLint config.
139
+ 2. Move your rule options unchanged to `"import-access/jsdoc"` in the oxlint config. If you didn't set `defaultImportability`, set it to `"public"` to keep the original's behavior: here, untagged exports are `@package` by default.
140
+ 3. For IDE auto-import filtering you can keep using the original package's [TypeScript language service plugin](https://github.com/uhyo/eslint-plugin-import-access/blob/master/docs/ts-server.md) (it only needs TypeScript, not ESLint); this package has no language service plugin of its own.
141
+
142
+ ## Contributing
143
+
144
+ ```sh
145
+ bun install
146
+ bun run test # rule and unit tests (Node.js >= 22)
147
+ bun run build && bun run test:e2e
148
+ bun run check # format, lint + typecheck (oxlint, with this plugin on its own sources), lockfile
149
+ ```
150
+
151
+ ## Credits
152
+
153
+ Based on [eslint-plugin-import-access](https://github.com/uhyo/eslint-plugin-import-access) by [uhyo](https://github.com/uhyo), whose rule design, options, documentation and test fixtures this port keeps.
154
+
155
+ ## License
156
+
157
+ [MIT](./LICENSE)
@@ -0,0 +1,22 @@
1
+ import type { ModuleExports } from "./exportTable.js";
2
+ /**
3
+ * Collect `declare module "name" { … }` blocks of a declaration file that is a script
4
+ * (in a module file the same syntax is a module augmentation, not an ambient module).
5
+ */
6
+ export declare function parseAmbientModules(fileName: string, text: string): Map<string, ModuleExports>;
7
+ /**
8
+ * Index of ambient modules declared in the project's declaration files, built lazily on first use.
9
+ */
10
+ export declare class AmbientModuleIndex {
11
+ #private;
12
+ /**
13
+ * The ambient module named exactly `specifier`. TypeScript prefers these over resolved files.
14
+ */
15
+ exact(rootDir: string, specifier: string): ModuleExports | undefined;
16
+ /**
17
+ * The wildcard ambient module (`declare module "*.svg"`) matching `specifier`;
18
+ * like TypeScript, the pattern with the longest prefix wins.
19
+ */
20
+ pattern(rootDir: string, specifier: string): ModuleExports | undefined;
21
+ clear(): void;
22
+ }
@@ -0,0 +1,60 @@
1
+ import type { PackageOptions } from "../utils/options.js";
2
+ import { AmbientModuleIndex } from "./ambientModules.js";
3
+ import { type ExportEntry, ExportTableCache, type ModuleExports } from "./exportTable.js";
4
+ import { ModuleResolver } from "./resolveModule.js";
5
+ /**
6
+ * A violation: the access level of the export, and its name as TypeScript reports it
7
+ * (`default` for default exports, `export=` for `export =`).
8
+ */
9
+ export type Violation = {
10
+ readonly access: "package" | "private";
11
+ readonly identifier: string;
12
+ };
13
+ type FoundExport = {
14
+ readonly module: ModuleExports;
15
+ readonly name: string;
16
+ readonly entries: readonly ExportEntry[];
17
+ };
18
+ /**
19
+ * Shared state for checking imports: resolver, parsed modules and ambient modules.
20
+ * One instance lives for the whole process; its caches are keyed by file path.
21
+ * @public
22
+ */
23
+ export declare class ImportabilityChecker {
24
+ #private;
25
+ readonly resolver: ModuleResolver;
26
+ readonly exportTables: ExportTableCache;
27
+ readonly ambientModules: AmbientModuleIndex;
28
+ /**
29
+ * Find the module entry TypeScript's `getImmediateAliasedSymbol` would return:
30
+ * a direct export of `name` stops the search (even when it re-exports something else),
31
+ * `export * from` edges are followed.
32
+ */
33
+ findExport(module: ModuleExports, name: string): FoundExport | undefined;
34
+ /**
35
+ * Module that `specifier` leads to from `importerFileName`, if the rule should check it.
36
+ * Mirrors TypeScript's order: exact ambient module, resolved file, wildcard ambient module.
37
+ */
38
+ targetModule(importerFileName: string, specifier: string, projectDirectory: string, assumeAllPublic: boolean): ModuleExports | undefined;
39
+ constructor(watching?: boolean);
40
+ /**
41
+ * Start checking `fileName`. In a long-running host, everything read for earlier files is
42
+ * re-validated: exporter files are stat-ed again and specifiers resolved again.
43
+ */
44
+ nextFile(fileName: string): void;
45
+ clear(): void;
46
+ }
47
+ /**
48
+ * Per-options part of the check: compiled globs and package boundaries.
49
+ * @public
50
+ */
51
+ export declare class ImportabilityPolicy {
52
+ #private;
53
+ readonly options: PackageOptions;
54
+ constructor(options: PackageOptions);
55
+ /**
56
+ * Check importing `name` (the export name, `default` for default imports) via `specifier`.
57
+ */
58
+ check(checker: ImportabilityChecker, importerFileName: string, specifier: string, name: string): Violation | undefined;
59
+ }
60
+ export {};
@@ -0,0 +1,102 @@
1
+ import { type Comment, type Tag } from "../utils/jsdoc.js";
2
+ /**
3
+ * Where the declaration of an export keeps the JSDoc that TypeScript's
4
+ * `symbol.getJsDocTags()` reads.
5
+ */
6
+ export type OwnJsDoc =
7
+ /** Declarations that are statements: functions, classes, `export default …`, `export =`. */
8
+ {
9
+ readonly kind: "statement";
10
+ }
11
+ /** `export { a }`: the specifier, including comments on the same line before it. */
12
+ | {
13
+ readonly kind: "specifier";
14
+ readonly start: number;
15
+ }
16
+ /** `export const a = 1, b = 2`: the declarator, and the statement when it is the only one. */
17
+ | {
18
+ readonly kind: "variable";
19
+ readonly nameStart: number;
20
+ readonly statementEnd: number;
21
+ }
22
+ /** `export * as ns from "…"`: the namespace export has no JSDoc of its own. */
23
+ | {
24
+ readonly kind: "none";
25
+ };
26
+ /**
27
+ * One export of a name by a module, in the shape the importability check needs.
28
+ */
29
+ export type ExportEntry = {
30
+ /**
31
+ * `false` for exports upstream never checks (enums, namespaces, `export import x = …`):
32
+ * TypeScript's `findExportableDeclaration` finds no exportable statement for them.
33
+ */
34
+ readonly checked: boolean;
35
+ /** Start of the whole export statement. */
36
+ readonly statementStart: number;
37
+ readonly own: OwnJsDoc;
38
+ };
39
+ /**
40
+ * Exports of one module file.
41
+ */
42
+ export type ModuleExports = {
43
+ readonly fileName: string;
44
+ /** `undefined` when the fast path proved every export public. */
45
+ readonly names: ReadonlyMap<string, readonly ExportEntry[]> | undefined;
46
+ /** Module requests of `export * from "…"` statements. */
47
+ readonly stars: readonly string[];
48
+ /**
49
+ * JSDoc tags of a name's entries, in the order upstream reads them:
50
+ * the own tags of every declaration, then all tags of the first declaration's statement.
51
+ */
52
+ tagsOf(entries: readonly ExportEntry[]): Tag[];
53
+ };
54
+ /** Name under which a module's `export = value` is stored, as TypeScript names the symbol. */
55
+ export declare const EXPORT_EQUALS = "export=";
56
+ export declare function skipTrivia(text: string, pos: number): number;
57
+ type Declarator = {
58
+ readonly start: number;
59
+ readonly end: number;
60
+ /** Start of an initializer that carries JSDoc for the variable. */
61
+ readonly initializer: number | undefined;
62
+ };
63
+ /**
64
+ * What only the syntax tree can tell. Built lazily, for the few files that need it.
65
+ */
66
+ type SyntaxDetails = {
67
+ /** Start of the first decorator of decorated `export class` statements, by statement start. */
68
+ readonly decoratorStarts: ReadonlyMap<number, number>;
69
+ /** Spans of `export { … }` statements without a source. */
70
+ readonly exportLists: readonly (readonly [number, number])[];
71
+ /** Starts of `export = value` statements. */
72
+ readonly exportEquals: readonly number[];
73
+ /** Declarators of exported variable statements, by statement start. */
74
+ readonly variables: ReadonlyMap<number, readonly Declarator[]>;
75
+ };
76
+ export declare function createModuleExports(fileName: string, text: string, comments: readonly Comment[], names: ReadonlyMap<string, readonly ExportEntry[]> | undefined, stars: readonly string[], details?: () => SyntaxDetails): ModuleExports;
77
+ /**
78
+ * Build the export table of a module from its source text.
79
+ *
80
+ * Mostly oxc-parser's module record and comments are read. The syntax tree, several times
81
+ * slower to get, is only used for files with decorators, `export =`, re-exported imports
82
+ * or JSDoc inside variable statements.
83
+ */
84
+ export declare function parseModuleExports(fileName: string, text: string, assumeAllPublic: boolean): ModuleExports;
85
+ /**
86
+ * Process-wide cache of module export tables.
87
+ *
88
+ * Files are re-validated (stat-ed, re-parsed when changed) at most once per generation.
89
+ * In long-running hosts such as the oxlint language server, every linted file starts a new
90
+ * generation; a CLI run stays in one generation and reads each file once.
91
+ */
92
+ export declare class ExportTableCache {
93
+ #private;
94
+ /** Start a new generation: files will be re-validated on their next lookup. */
95
+ nextGeneration(): void;
96
+ /**
97
+ * Export table of `fileName`, or `undefined` when the file can't be read.
98
+ */
99
+ get(fileName: string, assumeAllPublic: boolean): ModuleExports | undefined;
100
+ clear(): void;
101
+ }
102
+ export {};
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Name of the closest `package.json` above `file`, or `null`.
3
+ */
4
+ export declare function lookupPackageName(file: string): string | null;
5
+ /**
6
+ * Forget cached package.json lookups (for long-running processes and tests).
7
+ */
8
+ export declare function clearPackageNameCache(): void;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Where an import specifier leads.
3
+ *
4
+ * - `internal`: a source file of the project, which the rule checks.
5
+ * - `external`: a builtin module or a file reached through `node_modules`
6
+ * (TypeScript's "external library import", which includes symlinked workspace packages).
7
+ * - `unresolved`: nothing found, or a file that is no module to TypeScript (`.svg`, `.css`…);
8
+ * a wildcard ambient module (`declare module "*.svg"`) may still declare it.
9
+ */
10
+ export type Resolution = {
11
+ readonly kind: "internal";
12
+ readonly fileName: string;
13
+ } | {
14
+ readonly kind: "external";
15
+ } | {
16
+ readonly kind: "unresolved";
17
+ };
18
+ /**
19
+ * Resolves import specifiers like TypeScript would.
20
+ *
21
+ * Results are memoized per importing directory, and relative specifiers also by the absolute
22
+ * path they point at, so every importer of a module shares one resolution.
23
+ */
24
+ export declare class ModuleResolver {
25
+ #private;
26
+ resolve(importerFileName: string, specifier: string): Resolution;
27
+ clear(): void;
28
+ }
@@ -0,0 +1,29 @@
1
+ /** @public */
2
+ export type { Importability, JSDocRuleOptions } from "./utils/options.js";
3
+ /**
4
+ * Shape of the plugin object. Declared here so the published types don't depend on
5
+ * `@oxlint/plugins`; it is checked against oxlint's `Plugin` type below.
6
+ * @public
7
+ */
8
+ export type ImportAccessPlugin = {
9
+ readonly meta: {
10
+ readonly name: "import-access";
11
+ };
12
+ readonly rules: {
13
+ readonly jsdoc: object;
14
+ };
15
+ };
16
+ /**
17
+ * Oxlint JS plugin restricting imports of `@package` and `@private` exports.
18
+ *
19
+ * @example
20
+ * ```json
21
+ * {
22
+ * "jsPlugins": ["@crutchcrew/oxlint-plugin-import-access"],
23
+ * "rules": { "import-access/jsdoc": "error" }
24
+ * }
25
+ * ```
26
+ */
27
+ declare const plugin: ImportAccessPlugin;
28
+ /** @public */
29
+ export default plugin;