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