diff --git a/README.md b/README.md index 8a1ddc2..d221183 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# eslint-plugin-no-relative-base-url-imports +# @opencreek/eslint-plugin-ts Disalows relative path across the baseUrl of your tsconfig @@ -10,19 +10,23 @@ You'll first need to install [ESLint](https://eslint.org/): npm i eslint --save-dev ``` -Next, install `eslint-plugin-no-relative-base-url-imports`: +Next, install `@opencreek/eslint-plugin-ts`: ```sh -npm install eslint-plugin-no-relative-base-url-imports --save-dev +npm install @opencreek/eslint-plugin-ts --save-dev +``` + +```sh +yarn add --dev @opencreek/eslint-plugin-ts ``` ## Usage -Add `no-relative-base-url-imports` to the plugins section of your `.eslintrc` configuration file. You can omit the `eslint-plugin-` prefix: +Add `@opencreek/ts` to the plugins section of your `.eslintrc` configuration file. You can omit the `eslint-plugin-` prefix: ```json { - "plugins": ["no-relative-base-url-imports"] + "plugins": ["@opencreek/ts"] } ``` @@ -31,11 +35,28 @@ Then configure the rules you want to use under the rules section. ```json { "rules": { - "no-relative-base-url-imports/rule-name": 2 + "@opencreek/ts/no-relative-imports": [ + "error", + { + "baseUrl": "./src" + } + ] } } ``` ## Supported Rules -- Fill in provided rules here +### `@opencreek/ts/no-relative-imports` Disable relative imports. + +Config options + +```ts +{ + "baseUrl": "./src", // The base url that you have set in the tsconfig + "allowLocalImports": "local" // possible values: "local" | "in-base-path". + // "local": Allows local imports (eg.: "./test") + // "in-base-path": Allows everything that does not go back to the base url level (eg: "../../test" in "src/a/b/c/test.ts") +} + +``` diff --git a/lib/index.ts b/lib/index.ts index 28b20aa..646b7fb 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -20,7 +20,7 @@ export const rules = { export const configs = { recommended: { rules: { - "@opencreek/opencreek/no-relative-imports": "error", + "@opencreek/ts/no-relative-imports": "error", }, }, } diff --git a/lib/rules/no-relative-imports.ts b/lib/rules/no-relative-imports.ts index 7f708cd..ad84a4d 100644 --- a/lib/rules/no-relative-imports.ts +++ b/lib/rules/no-relative-imports.ts @@ -5,8 +5,10 @@ const creator = RuleCreator((rule) => rule) export type Options = { baseUrl?: string + allowLocalImports?: "inside-base-path" | "local" }[] -export type MessageIds = "standard-message" + +export type MessageIds = "no-relative-import" export default creator({ name: "no-relative-imports", meta: { @@ -17,8 +19,7 @@ export default creator({ }, fixable: "code", messages: { - "standard-message": - "No relative imports, that go to back to the baseURl", + "no-relative-import": "No relative imports", }, schema: [ { @@ -35,6 +36,23 @@ export default creator({ create(context) { return { ImportDeclaration(node) { + const options = context.options?.[0] ?? {} + + // no relative import + if (!node.source.value.startsWith(".")) { + return + } + + if ( + options.allowLocalImports === "local" || + options.allowLocalImports === "inside-base-path" + ) { + // We start with a "./" followed by a different char, so it's a local import + if (/^\.\/[^.]/.test(node.source.value)) { + return + } + } + const fileName = context.getPhysicalFilename?.() if (fileName == undefined) { console.error("Got no physical file name ?!") @@ -43,17 +61,30 @@ export default creator({ const basePath = path.resolve( process.cwd(), - context.options?.[0]?.baseUrl ?? "." + options.baseUrl ?? "." + ) + + const fileNameInsideBasePath = fileName.replace(basePath, "") + const levels = fileNameInsideBasePath.split("/").length - 2 + const levelImport = "../".repeat(levels) + + const filePath = path.dirname(fileName) + + const absoluteImportPath = path.resolve( + filePath, + node.source.value ) + const isInBasePath = filePath.startsWith(basePath) + const absoluteImportPathInsideBasePath = + absoluteImportPath.replace(basePath + "/", "") - if (!fileName.startsWith(basePath)) { + // we import something outside of the basePath + if (!absoluteImportPath.startsWith(basePath)) { return } - const relativeFileName = fileName.replace(basePath, "") - const levels = relativeFileName.split("/").length - 2 - const levelImport = "../".repeat(levels) - if (node.source.value.startsWith(levelImport)) { + // always remove imports that go past the base URL + if (node.source.value.startsWith(levelImport) && isInBasePath) { const withoutLevels = node.source.value.replace( levelImport, "" @@ -64,18 +95,40 @@ export default creator({ context.report({ node: node.source, - messageId: "standard-message", + messageId: "no-relative-import", data: {}, fix: (fixer) => { return fixer.replaceText( node.source, - '"' + - node.source.value.replace(levelImport, "") + - '"' + `"${absoluteImportPathInsideBasePath}"` ) }, }) + return + } + + // We report the error, if we disallow relative imports + // Or we cross INTO the baseUrl boundary + if ( + options.allowLocalImports == undefined && + !absoluteImportPath.startsWith(basePath) + ) { + return } + + context.report({ + node: node.source, + messageId: "no-relative-import", + data: {}, + fix: (fixer) => { + return fixer.replaceText( + node.source, + `"${absoluteImportPathInsideBasePath}"` + ) + }, + }) + + return }, } }, diff --git a/package-lock.json b/package-lock.json index 5ab3206..b611db0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { - "name": "@opencreek/eslint-plugin-opencreek", + "name": "@opencreek/eslint-plugin", "version": "0.2.0", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "@opencreek/eslint-plugin-opencreek", - "version": "0.1.0", + "name": "@opencreek/eslint-plugin", + "version": "0.2.0", "license": "MIT", "dependencies": { "@typescript-eslint/experimental-utils": "^5.4.0" diff --git a/package.json b/package.json index 9a93b45..fa2a37d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "@opencreek/eslint-plugin-opencreek", + "name": "@opencreek/eslint-plugin-ts", "version": "0.2.0", - "description": "Disalows relative path across the baseUrl of your tsconfig", + "description": "Disallows relative path across the baseUrl of your tsconfig", "type": "module", "keywords": [ "eslint", @@ -13,7 +13,7 @@ "build/**" ], "author": "Opencreek Technology", - "repository": "https://github.com/opencreek/provider-stack", + "repository": "https://github.com/opencreek/eslint-plugin-ts", "license": "MIT", "publishConfig": { "registry": "https://registry.npmjs.org/", diff --git a/tests/lib/rules/no-relative-imports.test.ts b/tests/lib/rules/no-relative-imports.test.ts index 0352c0b..1ec7acb 100644 --- a/tests/lib/rules/no-relative-imports.test.ts +++ b/tests/lib/rules/no-relative-imports.test.ts @@ -15,18 +15,39 @@ spy.mockReturnValue("/") ruleTester.run("my-rule", noRelativeImports, { valid: [ { - options: [{ baseUrl: "./src" }], + options: [{ baseUrl: "./src", allowLocalImports: "local" }], code: 'import {foo} from "bla/test"', filename: "/src/nested/deep/test.js", }, { - options: [{ baseUrl: "./src" }], + options: [{ baseUrl: "./src", allowLocalImports: "local" }], + code: 'import {foo} from "./bla/test"', + filename: "/src/nested/deep/test.js", + }, + { + options: [{ baseUrl: "./src", allowLocalImports: "local" }], code: 'import {foo} from "../../../bla/test"', filename: "/src/nested/deep/test.js", }, { - options: [{ baseUrl: "./src" }], - code: 'import {foo} from "../bla/test"', + options: [ + { baseUrl: "./src", allowLocalImports: "inside-base-path" }, + ], + code: 'import {foo} from "/bla/test"', + filename: "/src/nested/deep/test.js", + }, + { + options: [ + { baseUrl: "./src", allowLocalImports: "inside-base-path" }, + ], + code: 'import {foo} from "./bla/test"', + filename: "/src/nested/deep/test.js", + }, + { + options: [ + { baseUrl: "./src", allowLocalImports: "inside-base-path" }, + ], + code: 'import {foo} from "../../../bla/test"', filename: "/src/nested/deep/test.js", }, { @@ -34,15 +55,106 @@ ruleTester.run("my-rule", noRelativeImports, { code: 'import {foo} from "../../../bla/test"', filename: "/test/nested/deep/test.js", }, + { + options: [{ baseUrl: "./src" }], + code: 'import {foo} from "nested/bla/test"', + filename: "/src/nested/deep/test.js", + }, + { + options: [{ baseUrl: "./src" }], + code: 'import {foo} from "../../../bla/test"', + filename: "/src/nested/deep/test.js", + }, ], invalid: [ + { + options: [{ baseUrl: "./src", allowLocalImports: "local" }], + code: 'import {foo} from "../test2"', + filename: "/src/nested/deep/test.js", + errors: [ + { + messageId: "no-relative-import", + }, + ], + output: 'import {foo} from "nested/test2"', + }, + { + options: [{ baseUrl: "./src", allowLocalImports: "local" }], + code: 'import {foo} from "../../bla/test2"', + filename: "/src/nested/deep/test.js", + errors: [ + { + messageId: "no-relative-import", + }, + ], + output: 'import {foo} from "bla/test2"', + }, + { + options: [{ baseUrl: "./src", allowLocalImports: "local" }], + code: 'import {foo} from "../../../src/nested/test2"', + filename: "/test/nested/deep/test.js", + errors: [ + { + messageId: "no-relative-import", + }, + ], + output: 'import {foo} from "nested/test2"', + }, + { + options: [ + { baseUrl: "./src", allowLocalImports: "inside-base-path" }, + ], + code: 'import {foo} from "../../bla/test2"', + filename: "/src/nested/deep/test.js", + errors: [ + { + messageId: "no-relative-import", + }, + ], + output: 'import {foo} from "bla/test2"', + }, + { + options: [ + { baseUrl: "./src", allowLocalImports: "inside-base-path" }, + ], + code: 'import {foo} from "../../../src/bla/test2"', + filename: "/test/nested/deep/test.js", + errors: [ + { + messageId: "no-relative-import", + }, + ], + output: 'import {foo} from "bla/test2"', + }, { options: [{ baseUrl: "./src" }], - code: 'import {foo} from "../../bla/test"', + code: 'import {foo} from "./bla/test"', filename: "/src/nested/deep/test.js", errors: [ { - messageId: "standard-message", + messageId: "no-relative-import", + }, + ], + output: 'import {foo} from "nested/deep/bla/test"', + }, + { + options: [{ baseUrl: "./src" }], + code: 'import {foo} from "../bla/test"', + filename: "/src/nested/deep/test.js", + errors: [ + { + messageId: "no-relative-import", + }, + ], + output: 'import {foo} from "nested/bla/test"', + }, + { + options: [{ baseUrl: "./src" }], + code: 'import {foo} from "../../src/bla/test"', + filename: "/somewhere/else/test.js", + errors: [ + { + messageId: "no-relative-import", }, ], output: 'import {foo} from "bla/test"',