Skip to content

Commit

Permalink
Merge pull request #1 from yukinotech/dev
Browse files Browse the repository at this point in the history
feat support commonjs
  • Loading branch information
yukinotech authored Aug 10, 2023
2 parents 0230d3e + 545aa05 commit b64ab5a
Show file tree
Hide file tree
Showing 15 changed files with 362 additions and 121 deletions.
62 changes: 60 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

make find all circular dependency in typescript easy

support:

- now only support typescript witch use import instead of require()
- typescript alias
- auto ignore typescript import type

# usage

## cli
Expand All @@ -22,7 +28,7 @@ e.g

```shell
cd my-project // my-project is a typescript project
mobius run ./ -t ./tsconfig.json
mobius run ./ -t ./tsconfig.json -m typescript
```

### get help from cli
Expand All @@ -47,6 +53,58 @@ Options:
-h, --help display help for command
```

### some tips for use cli

In most projects, apart from the source code, there are many non-source code components, such as tests.

Here is an example: `./src` contains `TypeScript` code and `./test` contains `commonjs` code.
```
my-node-project/
├─ src/
│ ├─ index.ts
│ ├─ utils/
│ │ ├─ helper.ts
│ │ └─ constants.ts
│ └─ routes/
│ ├─ api.ts
│ └─ web.ts
├─ test/
│ ├─ unit/
│ │ ├─ test-helper.js
│ │ └─ index.test.js
│ └─ integration/
│ ├─ api.test.js
│ └─ web.test.js
├─ package.json
├─ tsconfig.json
├─ README.md
└─ .gitignore
```

Usually, we just want to check whether there are circular dependencies in the source code

In reference to the example above: it is preferable to set <codeDirPath> as `./my-node-project/src`, if you just want to check only within the source code. And since `./src` contains `TypeScript` code. You can use cli like this:

```shell
cd my-node-project
mobius run ./src -t ./tsconfig.json -m typescript
```

Just run for src can be more faster.

And for `./test`,if you also want to to check whether there are circular dependencies in it,just run

```shell
cd my-node-project
mobius run ./test -m commonjs
```

If the -m parameter is not provided like just use `mobius run ./` in project like this, the program might produce confusing results.


## module

```shell
Expand All @@ -69,4 +127,4 @@ const main = async()=>{

- optimizing CLI interaction
- auto find and analysis tsconfig
- support nodejs commonjs and esm
- support nodejs esm
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@yukinotech/mobius",
"version": "1.0.4",
"version": "1.0.5",
"description": "find all circular dependency for typescript",
"main": "./build/index.js",
"bin": {
Expand Down Expand Up @@ -28,12 +28,14 @@
"devDependencies": {
"@swc/cli": "^0.1.62",
"@types/node": "^20.4.5",
"@types/resolve": "^1.20.2",
"prettier": "^3.0.1"
},
"dependencies": {
"@swc/core": "^1.3.69",
"chalk": "^4.1.2",
"commander": "^11.0.0",
"resolve": "^1.22.4",
"typescript": "^5.1.6"
}
}
25 changes: 21 additions & 4 deletions src/bin/mobius.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ program.version(LIB_VERSION).option('-h, --help', 'Show help')
program
.command('run <codeDirPath>')
.description('Run a script')
.option('-t, --tsConfigPath <path>', 'Path to tsconfig.json')
.option('-d, --debug', 'Enable debugging')
.option('-t, --tsConfigPath <path>', 'Path to tsconfig.json')
.option('-m, --mode <mode type>', 'mode type "typescript"|"commonjs"|"esm"')
.option('-s, --thread <threads>', 'thread number', (value) => {
return parseInt(value)
})
Expand All @@ -40,9 +41,24 @@ program
debug('cmdObj', cmdObj)
}

const tsConfigPath = cmdObj?.tsConfigPath
const tsConfigPath = cmdObj?.tsConfigPath as string
let mode = cmdObj.mode as string

if (!mode) {
if (typeof tsConfigPath !== 'string') {
console.error('Error: must provide -m mode : "typescript" | "commonjs" | "esm"')
process.exit(1)
} else {
mode = 'typescript'
}
}

if (mode !== 'typescript' && mode !== 'commonjs' && mode !== 'esm') {
console.error('Error: -m mode must be "typescript" | "commonjs" | "esm"')
process.exit(1)
}

if (typeof tsConfigPath !== 'string') {
if (typeof tsConfigPath !== 'string' && mode === 'typescript') {
console.error('Error: tsConfigPath error, tsConfigPath is not string')
process.exit(1)
}
Expand All @@ -63,7 +79,7 @@ program
process.exit(1)
}

const absoluteTsConfigPath = path.resolve(process.cwd(), tsConfigPath)
const absoluteTsConfigPath = tsConfigPath && path.resolve(process.cwd(), tsConfigPath)
debug('absoluteTsConfigPath', absoluteTsConfigPath)
const absoluteCodeDirPath = path.resolve(process.cwd(), codeDirPath)
debug('absoluteCodeDirPath', absoluteCodeDirPath)
Expand All @@ -72,6 +88,7 @@ program
tsConfigPath: absoluteTsConfigPath,
projectDir: absoluteCodeDirPath,
threadNum,
mode,
})
if (circle.length !== 0) {
console.log('circular dependency:')
Expand Down
2 changes: 1 addition & 1 deletion src/findCycles.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ImportedModule } from './parseSingleFile'
import type { ImportedModule } from './types'

export const findCycles = (graph: Record<string, ImportedModule[]>): string[][] => {
const cycles: string[][] = []
Expand Down
20 changes: 13 additions & 7 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@ import type { CompilerOptions } from 'typescript'
import { findCycles } from './findCycles'
import { debug } from './debug'
import { parseFileWorkerTask } from './task/parseFileWorkerTask'
import type { ImportedModule } from './parseSingleFile'
import { parseTsConfig } from './parseTsConfig'
import { recursiveReadDir } from './recursiveReadDir'
import { isCodeFile } from './utils'
import type { ImportedModule, Mode } from './types'

const processArrayWithWorker = async <T extends string>(
data: T[],
threadNum: number,
tsCompilerOption: CompilerOptions,
tsCompilerOption: CompilerOptions | undefined,
mode: Mode,
): Promise<Record<string, ImportedModule[]>> => {
const totalChunks = Math.min(threadNum, data.length)
const chunkSize = Math.ceil(data.length / totalChunks)
Expand All @@ -18,7 +20,7 @@ const processArrayWithWorker = async <T extends string>(

for (let i = 0; i < totalChunks; i++) {
const chunk = data.slice(i * chunkSize, (i + 1) * chunkSize)
promises.push(traverseArray(chunk, tsCompilerOption))
promises.push(traverseArray(chunk, tsCompilerOption, mode))
}

// wait all Worker result
Expand All @@ -37,11 +39,13 @@ const processArrayWithWorker = async <T extends string>(

const traverseArray = async <T extends string>(
arr: T[],
tsCompilerOption: CompilerOptions,
tsCompilerOption: CompilerOptions | undefined,
mode: Mode,
): Promise<Record<string, ImportedModule[]>> => {
const res = await parseFileWorkerTask({
tsCompilerOption,
codePathList: arr,
mode,
})

return res
Expand All @@ -51,26 +55,28 @@ const mobius = async ({
tsConfigPath,
projectDir,
threadNum,
mode,
}: {
tsConfigPath?: string
projectDir: string
threadNum: number // thread number
mode: Mode
}) => {
debug('recursiveReadDir start')
const fileList = await recursiveReadDir(projectDir)
debug('recursiveReadDir end')
debug('fileList', fileList)

const filterFileList = fileList.filter((item) => {
return item.endsWith('.tsx') || item.endsWith('.ts')
return isCodeFile(item, mode)
})
debug('filterFileList', filterFileList)

const parsedCompilerOptions = parseTsConfig(tsConfigPath)
const parsedCompilerOptions = mode === 'typescript' ? parseTsConfig(tsConfigPath) : undefined

debug('parsedCompilerOptions', parsedCompilerOptions)

const res = await processArrayWithWorker(filterFileList, threadNum, parsedCompilerOptions)
const res = await processArrayWithWorker(filterFileList, threadNum, parsedCompilerOptions, mode)

const cycles = findCycles(res)
return cycles
Expand Down
95 changes: 95 additions & 0 deletions src/parseCommonjsSingleFile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import * as swc from '@swc/core'
import type { Node } from '@swc/core'
import * as resolve from 'resolve'
import fs from 'fs/promises'
import path from 'path'
import ts from 'typescript'
import type { CompilerOptions } from 'typescript'
import { debug } from './debug'
import { isCodeFile } from './utils'
import type { ImportedModule } from './types'

const visit = (node: any, list: string[]) => {
if (
node?.type === 'CallExpression' &&
node?.callee?.type === 'Identifier' &&
node?.callee?.value === 'require'
) {
const value = node?.arguments?.[0]?.expression?.value
list.push(value)
}

for (const key of Object.keys(node)) {
if (Array.isArray(node[key])) {
node[key].map((item: any) => {
visit(item, list)
})
} else if (typeof node[key] === 'object' && node[key] !== null) {
visit(node[key], list)
}
}
}

const findImportFileList = (ast: swc.Module): string[] => {
const list: string[] = []
visit(ast, list)
return list
}

const resolvePath = async (value: string, codeDirPath: string): Promise<string | undefined> => {
return new Promise((res, rej) => {
resolve.default(value, { basedir: codeDirPath }, (err, value) => {
if (err) {
debug('commonjs resolvePath error', err)
res(undefined)
} else {
res(value)
}
})
})
}

export const parseCommonjsSingleFile = async (codePath: string): Promise<ImportedModule[]> => {
const sourceCode = await fs.readFile(codePath, {
encoding: 'utf-8',
})

const res = await swc.parse(sourceCode, {
syntax: 'ecmascript',
jsx: true,
})

const importFile = findImportFileList(res)

const codeDirPath = path.dirname(codePath)
const resolvedImportList = await Promise.all(
importFile.map((value) => {
return resolvePath(value, codeDirPath)
}),
)

debug('resolvedImportList', resolvedImportList)

const filteredImportList: ImportedModule[] = resolvedImportList
.filter((value) => {
if (value === undefined) {
return false
}
if (resolve.isCore(value)) {
return false
}
if (!isCodeFile(value, 'commonjs')) {
return false
}
return true
})
.map((item) => {
return {
typeOnly: false,
value: '',
resolvedFileName: item,
}
})

return filteredImportList
}
Loading

0 comments on commit b64ab5a

Please sign in to comment.