[
  {
    "path": ".github/renovate.json5",
    "content": "{\n  extends: [\n    'config:base',\n    ':disableRateLimiting',\n    ':maintainLockFilesWeekly',\n    ':automergeAll',\n    'group:nodeJs',\n    'helpers:pinGitHubActionDigests',\n  ],\n  timezone: 'Asia/Tokyo',\n  postUpdateOptions: ['yarnDedupeHighest'],\n  minimumReleaseAge: '7 days',\n}\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non:\n  pull_request:\n    branches: [main]\n\njobs:\n  jest:\n    name: Jest\n    runs-on: ubuntu-latest\n    timeout-minutes: 5\n    strategy:\n      matrix:\n        node: ['18', '20', '22']\n    steps:\n      - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6\n      - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6\n        with:\n          node-version: ${{ matrix.node }}\n          cache: 'yarn'\n      - run: yarn\n      - run: yarn test\n"
  },
  {
    "path": ".github/workflows/publish.yml",
    "content": "name: Publish\n\non:\n  workflow_dispatch:\n    inputs:\n      versionClass:\n        type: choice\n        required: true\n        options:\n          - 'patch'\n          - 'minor'\n          - 'major'\n  schedule:\n    - cron: '0 0 15 * *' # at AM 9 JST on day of month 15\n\nconcurrency:\n  group: ${{ github.workflow }}\n  cancel-in-progress: true\n\npermissions:\n  id-token: write # Required for OIDC\n  contents: write # create tag and release\n\njobs:\n  publish:\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6\n      - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6\n        with:\n          node-version: '24'\n          cache: 'yarn'\n          registry-url: 'https://registry.npmjs.org'\n\n      # Trusted publishing requires npm CLI version 11.5.1 or later.\n      - name: Ensure npm >= 11.5.1\n        run: npm i -g npm@^11.5.1\n\n      - name: Install dependencies\n        run: yarn install --frozen-lockfile\n\n      - name: Bump version and publish\n        id: bump-version\n        run: |\n          git config user.name '[bot] github action (${{ github.workflow }})'\n          git config user.email 'engineer-team@knowledgework.com'\n          yarn version --${{ github.event_name == 'schedule' && 'patch' || github.event.inputs.versionClass }}\n          yarn run publish\n          echo \"new_version=$(jq -r .version package.json)\" >> \"$GITHUB_OUTPUT\"\n\n      - name: Create release\n        uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9\n        with:\n          github-token: ${{ secrets.GITHUB_TOKEN }}\n          script: |\n            await github.rest.repos.createRelease({\n              owner: \"${{github.repository_owner}}\",\n              repo: \"${{github.repository}}\".split('/')[1],\n              tag_name: \"v${{ steps.bump-version.outputs.new_version }}\",\n              generate_release_notes: true,\n            })\n"
  },
  {
    "path": ".gitignore",
    "content": "/node_modules/\n/.idea/\n/.vscode/\n/coverage/\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2021 Knowledge Work\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# eslint-plugin-strict-dependencies\n\nESLint plugin to define custom module dependency rules.\n\nNOTE: `eslint-plugin-strict-dependencies` uses tsconfig, tsconfig.json must be present.\n\n## Installation\n\n```\nnpm install eslint-plugin-strict-dependencies --save-dev\n```\n\n## Supported Rules\n\n- strict-dependencies\n  - module: `string` (Glob or Forward matching string)\n    - Target module path\n  - targetMembers: `string[]`\n    - Target member name\n    - e.x. `[\"Suspense\"]` in `import { Suspense } from 'react'`\n  - allowReferenceFrom: `string[]` (Glob or Forward matching string)\n    - Paths of files where target module imports are allowed.\n  - allowSameModule: `boolean`\n    - Whether it can be imported by other files in the same directory\n  - excludeTypeImportChecks: `boolean`\n    - Whether to exclude type import checks\n    - e.x. `import type { Suspense } from 'react'`\n\n### Options\n\n- resolveRelativeImport: `boolean[default = false]`\n  - Whether to resolve relative import as in the following example\n  - `src/components/aaa.ts`\n    ```typescript\n    import bbb from './bbb';\n    ```\n     - `resolveRelativeImport = false`: Resolve as `./bbb` (excluded from lint target)\n     - `resolveRelativeImport = true`:  Resolve as `src/components/bbb`: (included from lint target)\n\n- pathIndexMap: `object[default = null]`\n  - In eslint-plugin-strict-dependencies, path alias resolution is performed based on the paths specified in the tsconfig.\n  - By default, the value with an index number of `0` is used, but you can specify an option to use a value with any index number.\n  - Specify it as in the following example:\n    - `tsconfig.json`\n      ```json\n      {\n        \"compilerOptions\": {\n            \"*\": [\"aaa/*\", \"bbb/*\"]\n          },\n      }\n      ```\n    - `pathIndexMap = { \"*\": 1 } `: `\"bbb/*\"` is used.\n\n## Usage\n\n.eslintrc:\n\n```js\n\"plugins\": [\n  \"strict-dependencies\",\n],\n\"rules\": {\n  \"strict-dependencies/strict-dependencies\": [\n    \"error\",\n    [\n      /**\n       * Example:\n       * Limit the dependencies in the following directions\n       * pages -> components/page -> components/ui\n       */\n      {\n        \"module\": \"src/components/page\",\n        \"allowReferenceFrom\": [\"src/pages\"],\n        // components/page can't import other components/page\n        \"allowSameModule\": false\n      },\n      {\n        \"module\": \"src/components/ui\",\n        \"allowReferenceFrom\": [\"src/components/page\"],\n        // components/ui can import other components/ui\n        \"allowSameModule\": true,\n        // components/ui exclude type import checks\n        \"excludeTypeImportChecks\": true\n      },\n\n      /**\n       * example:\n       * Disallow to import `next/router` directly. it should always be imported using `libs/router.ts`.\n       */\n      {\n        \"module\": \"next/router\",\n        \"allowReferenceFrom\": [\"src/libs/router.ts\"],\n        \"allowSameModule\": false\n      },\n\n      /**\n       * example:\n       * Disallow to import Suspense from react. it should always be imported using `libs/react.ts`.\n       */\n        {\n            \"module\": \"react\",\n            \"targetMembers\": [\"Suspense\"],\n            \"allowReferenceFrom\": [\"src/libs/react.ts\"],\n            \"allowSameModule\": false\n        },\n    ],\n    // options\n    // {\n    //   \"resolveRelativeImport\": true\n    //   \"pathIndexMap\": { \"*\": 1 }\n    // }\n  ]\n}\n\n```\n\n\n## License\n\nMIT\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\n## Reporting Security Issues\n\nPlease do not report security vulnerabilities through public GitHub issues.\n\nInstead, please report them to the Security Team.\n\n```\nKnowledge Work Security Team\ninfo-security@knowledgework.com\n```"
  },
  {
    "path": "__tests__/index.js",
    "content": "const {create} = require('../strict-dependencies')\nconst path = require('path')\nconst resolveImportPath = require('../strict-dependencies/resolveImportPath')\n\njest.mock('../strict-dependencies/resolveImportPath')\n\nconst mockImportDeclaration = {\n  type: 'ImportDeclaration',\n  start: 72,\n  end: 116,\n  specifiers: [\n    // specifiersにはImportDefaultSpecifier/ImportNamespaceSpecifier/ImportSpecifierがあり、実際には同時に動くことはないが念のため3つともテストケースに対して用意する\n    {\n      // import * as React from 'react'\n      \"type\": \"ImportNamespaceSpecifier\",\n      \"start\": 52,\n      \"end\": 62,\n      \"local\": {\n        \"type\": \"Identifier\",\n        \"start\": 57,\n        \"end\": 62,\n        \"name\": \"React\"\n      }\n    },\n    {\n      // import DefaultExport from '@/components/ui/Text';\n      \"type\": \"ImportDefaultSpecifier\",\n      \"start\": 79,\n      \"end\": 92,\n      \"local\": {\n        \"type\": \"Identifier\",\n        \"start\": 79,\n        \"end\": 92,\n        \"name\": \"DefaultExport\"\n      }\n    },\n    {\n      // import { Text, TextProps } from '@/components/ui/Text';\n      'type': 'ImportSpecifier',\n      'start': 81,\n      'end': 85,\n      'imported': {\n        'type': 'Identifier',\n        'start': 81,\n        'end': 85,\n        'name': 'Text'\n      },\n      'local': {\n        'type': 'Identifier',\n        'start': 81,\n        'end': 85,\n        'name': 'Text'\n      }\n    },\n    {\n      'type': 'ImportSpecifier',\n      'start': 87,\n      'end': 96,\n      'imported': {\n        'type': 'Identifier',\n        'start': 87,\n        'end': 96,\n        'name': 'TextProps'\n      },\n      'local': {\n        'type': 'Identifier',\n        'start': 87,\n        'end': 96,\n        'name': 'TextProps'\n      }\n    }\n  ],\n  source: {\n    type: 'Literal',\n    start: 93,\n    end: 115,\n    value: '@/components/ui/Text',\n    raw: '\"@/components/ui/Text\"',\n  },\n};\n\nconst mockImportDeclarationImportKindType = {\n  importKind: 'type',\n  ...mockImportDeclaration\n};\n\ndescribe('create', () => {\n  it('should return object', () => {\n    const created = create({options: [[]]})\n    expect(typeof created).toBe('object')\n    expect(created).toHaveProperty('ImportDeclaration')\n  })\n})\n\ndescribe('create.ImportDeclaration', () => {\n  it('should do nothing if no dependencies', () => {\n    resolveImportPath.mockReturnValue('src/components/ui/Text')\n    const getFilename = jest.fn(() => path.join(process.cwd(), 'src/components/aaa/bbb.ts'))\n    const report = jest.fn()\n    const {ImportDeclaration: checkImport} = create({\n      options: [[]],\n      getFilename,\n      report,\n    })\n\n    checkImport(mockImportDeclaration)\n\n    expect(getFilename).toBeCalledTimes(1)\n    expect(report).not.toBeCalled()\n  })\n\n  it('should do nothing if not matched with importPath', () => {\n    // importPath: src/components/ui/Text\n    // dependency.module: src/libs\n\n    resolveImportPath.mockReturnValue('src/components/ui/Text')\n    const getFilename = jest.fn(() => path.join(process.cwd(), 'src/components/aaa/bbb.ts'))\n    const report = jest.fn()\n    const {ImportDeclaration: checkImport} = create({\n      options: [\n        [{module: 'src/libs'}],\n      ],\n      getFilename,\n      report,\n    })\n\n    checkImport(mockImportDeclaration)\n\n    expect(getFilename).toBeCalledTimes(1)\n    expect(report).not.toBeCalled()\n  })\n\n  it('should not report if allowed', () => {\n    // relativePath: src/components/pages/aaa.ts\n    // importPath: src/components/ui/Text\n    // dependency.module: src/components/ui, dependency.allowReferenceFrom: ['src/components/pages'], allowSameModule: true\n\n    resolveImportPath.mockReturnValue('src/components/ui/Text')\n    const getFilename = jest.fn(() => path.join(process.cwd(), 'src/components/pages/aaa.ts'))\n    const report = jest.fn()\n    const {ImportDeclaration: checkImport} = create({\n      options: [\n        [{module: 'src/components/ui', allowReferenceFrom: ['src/components/pages'], allowSameModule: true}],\n      ],\n      getFilename,\n      report,\n    })\n\n    checkImport(mockImportDeclaration)\n\n    expect(getFilename).toBeCalledTimes(1)\n    expect(report).not.toBeCalled()\n  })\n\n  it('should not report if allowed with glob pattern', () => {\n    // relativePath: src/components/pages/aaa.ts\n    // importPath: src/components/ui/Text\n    // dependency.module: src/components/ui, dependency.allowReferenceFrom: ['src/components/**/*.ts'], allowSameModule: true\n\n    resolveImportPath.mockReturnValue('src/components/ui/Text')\n    const getFilename = jest.fn(() => path.join(process.cwd(), 'src/components/pages/aaa.ts'))\n    const report = jest.fn()\n    const {ImportDeclaration: checkImport} = create({\n      options: [\n        [{module: 'src/components/ui', allowReferenceFrom: ['src/components/**/*.ts'], allowSameModule: true}],\n      ],\n      getFilename,\n      report,\n    })\n\n    checkImport(mockImportDeclaration)\n\n    expect(getFilename).toBeCalledTimes(1)\n    expect(report).not.toBeCalled()\n  })\n\n  it('should report if not allowed', () => {\n    // relativePath: src/components/test/aaa.ts\n    // importPath: src/components/ui/Text\n    // dependency.module: src/components/ui, dependency.allowReferenceFrom: ['src/components/pages'], allowSameModule: true\n\n    resolveImportPath.mockReturnValue('src/components/ui/Text')\n    const getFilename = jest.fn(() => path.join(process.cwd(), 'src/components/test/aaa.ts'))\n    const report = jest.fn()\n    const {ImportDeclaration: checkImport} = create({\n      options: [\n        [{module: 'src/components/ui', allowReferenceFrom: ['src/components/pages'], allowSameModule: true}],\n      ],\n      getFilename,\n      report,\n    })\n\n    checkImport(mockImportDeclaration)\n\n    expect(getFilename).toBeCalledTimes(1)\n    expect(report.mock.calls).toHaveLength(1)\n    expect(report.mock.calls[0][1]).toBe('import \\'src/components/ui/Text\\' is not allowed from src/components/test/aaa.ts.')\n  })\n\n  it('should report if not allowed with glob pattern', () => {\n    // relativePath: src/components/test/aaa.tsx\n    // importPath: src/components/ui/Text\n    // dependency.module: src/components/ui, dependency.allowReferenceFrom: ['src/components/**/*.ts'], allowSameModule: true\n\n    resolveImportPath.mockReturnValue('src/components/ui/Text')\n    const getFilename = jest.fn(() => path.join(process.cwd(), 'src/components/test/aaa.tsx'))\n    const report = jest.fn()\n    const {ImportDeclaration: checkImport} = create({\n      options: [\n        [{module: 'src/components/ui', allowReferenceFrom: ['src/components/**/*.ts'], allowSameModule: true}],\n      ],\n      getFilename,\n      report,\n    })\n\n    checkImport(mockImportDeclaration)\n\n    expect(getFilename).toBeCalledTimes(1)\n    expect(report.mock.calls).toHaveLength(1)\n    expect(report.mock.calls[0][1]).toBe('import \\'src/components/ui/Text\\' is not allowed from src/components/test/aaa.tsx.')\n  })\n\n  it('should not report if allowed from same module', () => {\n    // relativePath: src/components/pages/aaa.ts\n    // importPath: src/components/ui/Text\n    // dependency.module: src/components/ui, dependency.allowReferenceFrom: ['src/components/pages'], allowSameModule: true\n\n    resolveImportPath.mockReturnValue('src/components/ui/Text')\n    const getFilename = jest.fn(() => path.join(process.cwd(), 'src/components/ui/aaa.ts'))\n    const report = jest.fn()\n    const {ImportDeclaration: checkImport} = create({\n      options: [\n        [{module: 'src/components/ui', allowReferenceFrom: ['src/aaa'], allowSameModule: true}],\n      ],\n      getFilename,\n      report,\n    })\n\n    checkImport(mockImportDeclaration)\n\n    expect(getFilename).toBeCalledTimes(1)\n    expect(report).not.toBeCalled()\n  })\n\n  it('should report if not allowed from same module', () => {\n    // relativePath: src/components/pages/aaa.ts\n    // importPath: src/components/ui/Text\n    // dependency.module: src/components/ui, dependency.allowReferenceFrom: ['src/components/pages'], allowSameModule: true\n\n    resolveImportPath.mockReturnValue('src/components/ui/Text')\n    const getFilename = jest.fn(() =>\n      path.join(process.cwd(), 'src/components/ui/aaa.ts')\n    )\n    const report = jest.fn()\n    const { ImportDeclaration: checkImport } = create({\n      options: [\n        [\n          {\n            module: 'src/components/ui',\n            allowReferenceFrom: ['src/aaa'],\n            allowSameModule: false,\n          },\n        ],\n      ],\n      getFilename,\n      report,\n    })\n\n    checkImport(mockImportDeclaration)\n\n    expect(getFilename).toBeCalledTimes(1)\n    expect(report.mock.calls).toHaveLength(1)\n    expect(report.mock.calls[0][1]).toBe('import \\'src/components/ui/Text\\' is not allowed from src/components/ui/aaa.ts.')\n  })\n\n  it('should report if not allowed specifier from target module', () => {\n    // relativePath: src/components/pages/aaa.ts\n    // importPath: src/components/ui/Text\n    // dependency.module: src/components/ui, dependency.targetMembers: ['Text'], dependency.allowReferenceFrom: ['src/components/pages'], allowSameModule: true\n\n    resolveImportPath.mockReturnValue('src/components/ui/Text')\n    const getFilename = jest.fn(() =>\n      path.join(process.cwd(), 'src/pages/index.tsx')\n    )\n    const report = jest.fn()\n    const {ImportDeclaration: checkImport} = create({\n      options: [\n        [{module: 'src/components/ui', allowReferenceFrom: ['src/aaa'], allowSameModule: false}],\n      ],\n      getFilename,\n      report,\n    })\n\n    checkImport(mockImportDeclaration)\n\n    expect(getFilename).toBeCalledTimes(1)\n    expect(report.mock.calls).toHaveLength(1)\n    expect(report.mock.calls[0][1]).toBe('import \\'src/components/ui/Text\\' is not allowed from src/pages/index.tsx.')\n  })\n\n  it('should not report if allowed specifier from target module', () => {\n    // relativePath: src/components/pages/aaa.ts\n    // importPath: src/components/ui/Text\n    // dependency.module: src/components/ui, dependency.targetMembers: ['Text'], dependency.allowReferenceFrom: ['src/components/pages'], allowSameModule: true\n\n    resolveImportPath.mockReturnValue('src/components/ui/Text')\n    const getFilename = jest.fn(() =>\n      path.join(process.cwd(), 'src/pages/index.tsx')\n    )\n    const report = jest.fn()\n    const { ImportDeclaration: checkImport } = create({\n      options: [\n        [\n          {\n            module: 'src/components/ui',\n            targetMembers: ['Text'],\n            allowReferenceFrom: ['src/pages'],\n          },\n        ],\n      ],\n      getFilename,\n      report,\n    })\n\n    checkImport(mockImportDeclaration)\n\n    expect(getFilename).toBeCalledTimes(1)\n    expect(report.mock.calls).toHaveLength(0)\n    expect(report).not.toBeCalled()\n  })\n\n  it('should report if not allowed specifier from target module', () => {\n    // relativePath: src/components/pages/aaa.ts\n    // importPath: src/components/ui/Text\n    // dependency.module: src/components/ui, dependency.targetMembers: ['Text'], dependency.allowReferenceFrom: ['src/components/pages'], allowSameModule: true\n\n    resolveImportPath.mockReturnValue('src/components/ui/Text')\n    const getFilename = jest.fn(() =>\n      path.join(process.cwd(), 'src/components/button.tsx')\n    )\n    const report = jest.fn()\n    const { ImportDeclaration: checkImport } = create({\n      options: [\n        [\n          {\n            module: 'src/components/ui',\n            targetMembers: ['Text'],\n            allowReferenceFrom: ['src/pages'],\n          },\n        ],\n      ],\n      getFilename,\n      report,\n    })\n\n    checkImport(mockImportDeclaration)\n\n    expect(getFilename).toBeCalledTimes(1)\n    expect(report.mock.calls).toHaveLength(1)\n    expect(report.mock.calls[0][1]).toBe('import specifier \\'Text\\' is not allowed from src/components/button.tsx.')\n  })\n\n  it('should report if not allowed multiple specifiers from target module', () => {\n    // relativePath: src/components/button.tsx\n    // importPath: src/components/ui/Text\n    // dependency.module: src/components/ui, dependency.targetMembers: ['Text', 'TextProps'], dependency.allowReferenceFrom: ['src/pages'], allowSameModule: true\n\n    resolveImportPath.mockReturnValue('src/components/ui/Text')\n    const getFilename = jest.fn(() =>\n      path.join(process.cwd(), 'src/components/button.tsx')\n    )\n    const report = jest.fn()\n    const { ImportDeclaration: checkImport } = create({\n      options: [\n        [\n          {\n            module: 'src/components/ui',\n            targetMembers: ['Text', 'TextProps'],\n            allowReferenceFrom: ['src/pages'],\n          },\n        ],\n      ],\n      getFilename,\n      report,\n    })\n\n    checkImport(mockImportDeclaration)\n\n    expect(getFilename).toBeCalledTimes(1)\n    expect(report.mock.calls).toHaveLength(1)\n    expect(report.mock.calls[0][1]).toBe('import specifier \\'Text, TextProps\\' is not allowed from src/components/button.tsx.')\n  })\n\n  it('should not report if only allowed specifier from target module', () => {\n    // relativePath: src/components/pages/aaa.ts\n    // importPath: src/components/ui/Text\n    // dependency.module: src/components/ui, dependency.targetMembers: ['SomeRestrictedModule'], dependency.allowReferenceFrom: ['src/pages'], allowSameModule: true\n\n    resolveImportPath.mockReturnValue('src/components/ui/Text')\n    const getFilename = jest.fn(() =>\n      path.join(process.cwd(), 'src/components/button.tsx')\n    )\n    const report = jest.fn()\n    const { ImportDeclaration: checkImport } = create({\n      options: [\n        [\n          {\n            module: 'src/components/ui',\n            targetMembers: ['SomeRestrictedModule'],\n            allowReferenceFrom: ['src/pages'],\n          },\n        ],\n      ],\n      getFilename,\n      report,\n    })\n\n    checkImport(mockImportDeclaration)\n\n    expect(getFilename).toBeCalledTimes(1)\n    expect(report.mock.calls).toHaveLength(0)\n    expect(report).not.toBeCalled();\n  })\n\n  it('should pass relativeFilePath value to resolveImportPath if resolveRelativeImport is true', () => {\n    resolveImportPath.mockReturnValue('src/components/ui/Text')\n    const getFilename = jest.fn(() => path.join(process.cwd(), 'src/components/ui/aaa.ts'))\n    const report = jest.fn()\n    const {ImportDeclaration: checkImport} = create({\n      options: [\n        [{module: 'src/components/ui', allowReferenceFrom: ['src/aaa'], allowSameModule: true}],\n        {resolveRelativeImport: true},\n      ],\n      getFilename,\n      report,\n    })\n\n    checkImport(mockImportDeclaration)\n\n    expect(resolveImportPath).toBeCalledWith('@/components/ui/Text', 'src/components/ui/aaa.ts', {})\n    expect(getFilename).toBeCalledTimes(1)\n    expect(report).not.toBeCalled()\n  })\n\n  it('should pass empty relativeFilePath value to resolveImportPath if resolveRelativeImport is falsy', () => {\n    resolveImportPath.mockReturnValue('../components/ui/Text')\n    const getFilename = jest.fn(() => path.join(process.cwd(), 'src/components/ui/aaa.ts'))\n    const report = jest.fn()\n    const {ImportDeclaration: checkImport} = create({\n      options: [\n        [{module: 'src/components/ui', allowReferenceFrom: ['src/aaa'], allowSameModule: true}],\n      ],\n      getFilename,\n      report,\n    })\n\n    checkImport(mockImportDeclaration)\n\n    expect(resolveImportPath).toBeCalledWith('@/components/ui/Text', null, {})\n    expect(getFilename).toBeCalledTimes(1)\n    expect(report).not.toBeCalled()\n  })\n\n  it('should not report if excludeTypeImportChecks is true', () => {\n    resolveImportPath.mockReturnValue('src/components/ui/Text')\n    const getFilename = jest.fn(() =>\n      path.join(process.cwd(), 'src/pages/index.tsx')\n    )\n    const report = jest.fn()\n    const { ImportDeclaration: checkImport } = create({\n      options: [\n        [\n          {\n            module: 'src/components/ui',\n            allowReferenceFrom: ['src/aaa'],\n            allowSameModule: false,\n            excludeTypeImportChecks: true,\n          },\n        ],\n      ],\n      getFilename,\n      report,\n    })\n\n    checkImport(mockImportDeclarationImportKindType)\n\n    expect(getFilename).toBeCalledTimes(1)\n    expect(report).not.toBeCalled()\n  })\n})\n"
  },
  {
    "path": "__tests__/resolveImportPath.js",
    "content": "const resolveImportPath = require('../strict-dependencies/resolveImportPath')\nconst {readFileSync} = require('fs')\n\njest.mock('fs')\n\ndescribe('resolveImportPath', () => {\n  it('should resolve relative path', () => {\n    // > src/pages/aaa/bbb.ts\n    // import Text from '../../components/ui/Text'\n\n    readFileSync.mockReturnValue(JSON.stringify({}))\n    expect(resolveImportPath('../../components/ui/Text', 'src/pages/aaa/bbb.ts', {})).toBe('src/components/ui/Text')\n  })\n\n  it('should not resolve relative path if relativeFilePath is empty', () => {\n    // > src/pages/aaa/bbb.ts\n    // import Text from '../../components/ui/Text'\n\n    readFileSync.mockReturnValue(JSON.stringify({}))\n    expect(resolveImportPath('../../components/ui/Text', null, {})).toBe('../../components/ui/Text')\n  })\n\n  it('should do nothing if tsconfig.json does not exist', () => {\n    readFileSync.mockImplementation(() => {\n      throw new Error()\n    })\n    expect(resolveImportPath('components/aaa/bbb', null, {})).toBe('components/aaa/bbb')\n  })\n\n  it('should do nothing if no paths setting', () => {\n    readFileSync.mockReturnValue(JSON.stringify({}))\n    expect(resolveImportPath('components/aaa/bbb', null, {})).toBe('components/aaa/bbb')\n  })\n\n  describe('should resolve tsconfig paths', () => {\n    [\n      ['@/components/', 'components/', 'components/aaa/bbb'],\n      ['@/components', 'components', 'components/aaa/bbb'],\n      ['@/components/*', 'components/*', 'components/aaa/bbb'],\n    ].forEach(([target, resolve, expected]) => {\n      it(`${target}: [${resolve}]`, () => {\n        readFileSync.mockReturnValue(JSON.stringify({\n          compilerOptions: {\n            paths: {\n              [target]: [resolve],\n            },\n          },\n        }))\n\n        expect(resolveImportPath('components/aaa/bbb', null, {})).toBe('components/aaa/bbb')\n        expect(resolveImportPath('@/components/aaa/bbb', null, {})).toBe(expected)\n      })\n    })\n  })\n\n  describe('should resolve tsconfig paths with baseUrl', () => {\n    [\n      ['.', 'components/aaa/bbb'],\n      ['./', 'components/aaa/bbb'],\n      ['../', '../components/aaa/bbb'],\n      ['src', 'src/components/aaa/bbb'],\n      ['./src', 'src/components/aaa/bbb'],\n      ['src/', 'src/components/aaa/bbb'],\n      ['./src/', 'src/components/aaa/bbb'],\n    ].forEach(([baseUrl, expected]) => {\n      it(baseUrl, () => {\n        readFileSync.mockReturnValue(JSON.stringify({\n          compilerOptions: {\n            baseUrl,\n            paths: {\n              '@/components/': ['components/'],\n            },\n          },\n        }))\n\n        expect(resolveImportPath('components/aaa/bbb', null, {})).toBe('components/aaa/bbb')\n        expect(resolveImportPath('@/components/aaa/bbb', null, {})).toBe(expected)\n      })\n    })\n  })\n\n  describe('resolveImportPath with pathIndexMap parameter', () => {\n    const tsConfigWithMultiplePaths = JSON.stringify({\n      compilerOptions: {\n        paths: {\n          '@/components/*': ['src/components/*', 'src/alternativeComponents/*'],\n        },\n      },\n    });\n\n    it('should resolve path alias with specified index in pathIndexMap', () => {\n      readFileSync.mockReturnValue(tsConfigWithMultiplePaths);\n      expect(resolveImportPath('@/components/aaa/bbb', null, { '@/components/*': 1 })).toBe('src/alternativeComponents/aaa/bbb');\n    });\n  \n    it('should resolve path alias with default index:0 if specified index does not exist', () => {\n      readFileSync.mockReturnValue(tsConfigWithMultiplePaths);\n      expect(resolveImportPath('@/components/aaa/bbb', null, { '@/components/*': 5 })).toBe('src/components/aaa/bbb');\n    });\n  \n    it('should resolve path alias with default index:0 if pathIndexMap is an empty object', () => {\n      readFileSync.mockReturnValue(tsConfigWithMultiplePaths);\n      expect(resolveImportPath('@/components/aaa/bbb', null, {})).toBe('src/components/aaa/bbb');\n    });\n  })\n})\n"
  },
  {
    "path": "index.js",
    "content": "/* eslint-disable */\n\nconst strictDependencies = require('./strict-dependencies')\n\nmodule.exports = {\n  rules: {\n    'strict-dependencies': strictDependencies,\n  },\n}\n"
  },
  {
    "path": "jest.config.js",
    "content": "module.exports = {\n  clearMocks: true,\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"eslint-plugin-strict-dependencies\",\n  \"description\": \"ESlint plugin to define custom module dependency rules.\",\n  \"version\": \"1.3.33\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/knowledge-work/eslint-plugin-strict-dependencies.git\"\n  },\n  \"keywords\": [\n    \"eslint\",\n    \"eslintplugin\",\n    \"lint\",\n    \"rule\",\n    \"check\",\n    \"import\",\n    \"module\",\n    \"directory\",\n    \"strict\",\n    \"dependencies\"\n  ],\n  \"license\": \"MIT\",\n  \"main\": \"index.js\",\n  \"files\": [\n    \"strict-dependencies\",\n    \"index.js\"\n  ],\n  \"dependencies\": {\n    \"is-glob\": \"4.0.3\",\n    \"micromatch\": \"4.0.8\",\n    \"normalize-path\": \"3.0.0\",\n    \"require-strip-json-comments\": \"2.0.0\"\n  },\n  \"devDependencies\": {\n    \"jest\": \"29.7.0\"\n  },\n  \"scripts\": {\n    \"test\": \"jest --coverage\",\n    \"preversion\": \"echo \\\"Run check for version $npm_package_version\\\" && yarn run test\",\n    \"publish\": \"git push origin main && git push --tags && npm publish . --access public --registry=https://registry.npmjs.org\"\n  }\n}\n"
  },
  {
    "path": "strict-dependencies/index.js",
    "content": "/* eslint-disable */\n\nconst path = require('path')\nconst mm = require('micromatch')\nconst isGlob = require('is-glob')\nconst normalize = require('normalize-path')\n\nconst resolveImportPath = require('./resolveImportPath')\n\n/**\n * pathのmatcher。\n * eslintrcで設定できる値は以下のケースを扱う\n * - globパターン指定\n * - globパターン以外の場合 => 前方部分一致\n */\nconst isMatch = (str, pattern) =>\n  isGlob(pattern) ? mm.isMatch(str, pattern) : str.startsWith(pattern)\n\nmodule.exports = {\n  meta: {\n    type: 'suggestion',\n    schema: [\n      {\n        type: 'array',\n        items: [\n          {\n            type: 'object',\n            properties: {\n              module: {\n                type: 'string',\n              },\n              allowReferenceFrom: {\n                type: 'array',\n                items: [\n                  {\n                    type: 'string',\n                  },\n                ],\n              },\n              allowSameModule: {\n                type: 'boolean',\n              },\n              excludeTypeImportChecks: {\n                type: 'boolean'\n              }\n            },\n          },\n        ],\n      },\n      {\n        type: 'object',\n        properties: {\n          resolveRelativeImport: {\n            type: 'boolean',\n          },\n          pathIndexMap: {\n            type: 'object'\n          }\n        },\n      },\n    ],\n  },\n  create: (context) => {\n    const dependencies = context.options[0]\n    const options = context.options.length > 1 ? context.options[1] : {}\n    const resolveRelativeImport = options.resolveRelativeImport\n    const pathIndexMap = options.pathIndexMap ? options.pathIndexMap : {}\n\n    function checkImport(node) {\n      const fileFullPath = context.getFilename()\n      const relativeFilePath = normalize(path.relative(process.cwd(), fileFullPath))\n      const importPath = resolveImportPath(node.source.value, resolveRelativeImport ? relativeFilePath : null, pathIndexMap)\n\n      // specにはImportDefaultSpecifier/ImportNamespaceSpecifier/ImportSpecifierがあり、ImportSpecifierの場合はimportedが存在する\n      const importedModules = node.specifiers.filter(spec => 'imported' in spec).map(spec => spec.imported.name)\n\n      dependencies\n        .forEach((dependency) => {\n          // そもそもmoduleがimportPathと一致していない場合は必ず報告しない\n          if (!isMatch(importPath, dependency.module)) return;\n\n          /**\n           * 1. 参照元チェックをしてAllowであればそこで処理を終了する\n           * 2. targetMembersが指定されていれば、targetMembersと実際にimportしているモジュールを比較して含まれていればエラーレポートして処理を終了する\n           * 3. targetMembersが指定されていない場合は、エラー扱いなのでエラーレポートして処理を終了する\n           */\n\n          const isAllowedByPath =\n          // 参照元が許可されている\n          dependency.allowReferenceFrom.some((allowPath) =>\n            isMatch(relativeFilePath, allowPath),\n          ) || // または同一モジュール間の参照が許可されている場合\n          (dependency.allowSameModule && isMatch(relativeFilePath, dependency.module))\n          // または明示的に対象外としたtype importである場合\n          || (dependency.excludeTypeImportChecks && node.importKind === 'type')\n\n          if (isAllowedByPath) return\n\n          // importedされた値・型名もLintのターゲットに入っている場合の検出処理\n          function getCommonElements(arrA, arrB) {\n            return arrA.filter(element => arrB.includes(element));\n          }\n          if (dependency.targetMembers && Array.isArray(dependency.targetMembers)) {\n            const commonImportedList = getCommonElements(dependency.targetMembers, importedModules)\n            if (commonImportedList.length > 0) {\n              context.report(node, `import specifier '${commonImportedList.join(', ')}' is not allowed from ${relativeFilePath}.`)\n            }\n            return;\n          }\n\n          context.report(node, `import '${importPath}' is not allowed from ${relativeFilePath}.`)\n        })\n    }\n\n    return {\n      ImportDeclaration: checkImport,\n    }\n  },\n}\n"
  },
  {
    "path": "strict-dependencies/resolveImportPath.js",
    "content": "/* eslint-disable */\n\nconst path = require('path')\nconst parseJSON = require('require-strip-json-comments')\nconst normalize = require('normalize-path')\n\n/**\n * import文のrootからのパスを求める\n */\nmodule.exports = (importPath, relativeFilePath, pathIndexMap) => {\n  // { [importAlias: string]: OriginalPath }\n  const importAliasMap = {}\n\n  // Load tsconfig option\n  // MEMO: tscとか使って簡単に読める方法がありそう\n  try {\n    const tsConfigFilePath = path.join(process.cwd(), '/tsconfig.json')\n    // Exists ts config\n    const tsConfig = parseJSON(tsConfigFilePath)\n    if (tsConfig.compilerOptions && tsConfig.compilerOptions.paths) {\n      Object.keys(tsConfig.compilerOptions.paths).forEach((key) => {\n        const matchedKey = Object.keys(pathIndexMap).find(k => k === key)\n        // MEMO: pathIndexMapの指定がない場合 or 指定されているindexにアクセスしても値が得られない場合は[0]固定\n        const pathIndex = matchedKey ? pathIndexMap[matchedKey] : 0\n        const pathValue = tsConfig.compilerOptions.paths[key][pathIndex] ? tsConfig.compilerOptions.paths[key][pathIndex] : tsConfig.compilerOptions.paths[key][0]\n        importAliasMap[key] = tsConfig.compilerOptions.baseUrl ? path.join(tsConfig.compilerOptions.baseUrl, pathValue) : pathValue\n      })\n    }\n  } catch (e) {\n    // DO NOTHING\n  }\n\n  if (relativeFilePath && (importPath.startsWith('./') || importPath.startsWith('../'))) {\n    importPath = path.join(path.dirname(relativeFilePath), importPath)\n  }\n\n  const absolutePath = Object.keys(importAliasMap).reduce((resolvedImportPath, key) => {\n    // FIXME: use glob module instead of replace('*')\n    return resolvedImportPath.replace(key.replace('*', ''), importAliasMap[key].replace('*', ''))\n  }, importPath)\n\n  return normalize(absolutePath)\n}\n"
  }
]