Skip to content

Commit

Permalink
BREAKING CHANGE: Rewrite All! (#9)
Browse files Browse the repository at this point in the history
* BREAKING CHANGE: Rewrite All!

* Update README

* default output

* feat: support function

* fix: ignore req.get("host") to get parameter

* test: fix
  • Loading branch information
azu committed Jul 21, 2023
1 parent a4d512a commit 043aeb3
Show file tree
Hide file tree
Showing 9 changed files with 479 additions and 499 deletions.
27 changes: 16 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,33 @@ Create dependency graph for express routing.

Install with [npm](https://www.npmjs.com/):

npm install express-router-dependency-graph -g
npx express-router-dependency-graph "src/**/*.ts"

## Usage

Usage
$ express-router-dependency-graph --rootDir=path/to/project
$ express-router-dependency-graph [input]

Options
--rootDir [Path:String] path to root dir of source code. The directory should have package.json [required]
--cwd [Path:String] current working directory. Default: process.cwd()
--rootBaseUrl [Path:String] if pass it, replace rootDir with rootDirBaseURL in output.
--format ["json" | "markdown"] output format. Default: json
--format ["json" | "markdown"] output format. Default: markdown

Examples
$ express-router-dependency-graph --rootDir=./

:memo: `--rootDir=<dir>` the directory should have package.json.

This package.json should have `express` dependencies.
# analyze all ts files in src directory
$ express-router-dependency-graph "src/**/*.ts"
# analyze all ts files in src directory and output json
$ express-router-dependency-graph "src/**/*.ts" --format=json
# analyze all js and files in src directory
$ express-router-dependency-graph "src/**/*.ts" "src/**/*.js"
# change rootDir to rootDirBaseURL to output
$ express-router-dependency-graph "src/**/*.ts" --rootBaseUrl="https://github.com/owner/repo/tree/master/src"
# include node_modules
$ express-router-dependency-graph "src/**/*.ts" --noDefaultExcludes

## Example

Example output: `markdown`
Example output: `--format=markdown`

- File: file path
- Method: get | post | put | delete | `use`(express's use)
Expand All @@ -51,7 +56,7 @@ Example output: `markdown`
| | post | /updateUserById | requireWrite | src/user.ts#L14-L15 |
| | delete | /deleteUserById | requireWrite | src/user.ts#L16-L17 |`

JSON output:
Example output: `--format=json`

```json5
[
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@
},
"dependencies": {
"@babel/parser": "^7.21.4",
"dependency-cruiser": "^12.11.1",
"esquery": "^1.5.0",
"globby": "^13.2.2",
"markdown-table": "^3.0.3",
"meow": "^11.0.0"
},
Expand Down
62 changes: 39 additions & 23 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,55 @@
import meow from "meow";
import * as path from "node:path";
import { analyzeDependency } from "./index.js";
import { analyzeDependencies } from "./index.js";
import { globby } from "globby";

export const cli = meow(
`
Usage
$ express-router-dependency-graph --rootDir=path/to/project
$ express-router-dependency-graph [input]
Options
--includeOnly [String] only include modules satisfying a pattern. https://github.com/sverweij/dependency-cruiser/blob/develop/doc/cli.md#--include-only-only-include-modules-satisfying-a-pattern
--doNotFollow [String] don't cruise modules adhering to this pattern any further. https://github.com/sverweij/dependency-cruiser/blob/develop/doc/cli.md#./options-reference.md#donotfollow-dont-cruise-modules-any-further
--rootDir [Path:String] path to root dir of source code [required]
--cwd [Path:String] current working directory. Default: process.cwd()
--rootBaseUrl [Path:String] if pass it, replace rootDir with rootDirBaseURL in output.
--format ["json" | "markdown"] output format. Default: json
--format ["json" | "markdown"] output format. Default: markdown
Examples
$ express-router-dependency-graph --rootDir=./
# analyze all ts files in src directory
$ express-router-dependency-graph "src/**/*.ts"
# analyze all ts files in src directory and output json
$ express-router-dependency-graph "src/**/*.ts" --format=json
# analyze all js and files in src directory
$ express-router-dependency-graph "src/**/*.ts" "src/**/*.js"
# change rootDir to rootDirBaseURL to output
$ express-router-dependency-graph "src/**/*.ts" --rootBaseUrl="https://github.com/owner/repo/tree/master/src"
# include node_modules
$ express-router-dependency-graph "src/**/*.ts" --no-default-excludes
`,
{
flags: {
rootDir: {
cwd: {
type: "string",
isRequired: true
isRequired: true,
default: process.cwd()
},
rootBaseUrl: {
type: "string",
default: ""
},
includeOnly: {
type: "string",
isMultiple: true
defaultExcludes: {
type: "boolean",
default: true
},
doNotFollow: {
excludes: {
type: "string",
isMultiple: true,
default: ["^node_modules"]
default: [
"!**/node_modules/**",
"!**/dist/**",
"!**/build/**",
"!**/coverage/**",
"!**/test/**",
"!**/__tests__/**"
]
},
format: {
type: "string",
Expand All @@ -48,19 +63,20 @@ export const cli = meow(
);

export const run = async (
_input = cli.input,
input = cli.input,
flags = cli.flags
): Promise<{ exitStatus: number; stdout: string | null; stderr: Error | null }> => {
const result = await analyzeDependency({
rootDir: path.resolve(process.cwd(), flags.rootDir),
const filePaths = await globby(flags.defaultExcludes ? input.concat(flags.excludes) : input, {
cwd: flags.cwd
});
const result = await analyzeDependencies({
cwd: flags.cwd,
filePaths,
rootBaseUrl: flags.rootBaseUrl,
outputFormat: flags.format as "json" | "markdown",
includeOnly: flags.includeOnly,
doNotFollow: flags.doNotFollow
outputFormat: flags.format as "json" | "markdown"
});
return {
// @ts-expect-error
stdout: flags.format === "json" ? JSON.stringify(result) : result,
stdout: typeof result === "object" ? JSON.stringify(result) : result,
stderr: null,
exitStatus: 0
};
Expand Down
112 changes: 69 additions & 43 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,11 @@
import { cruise } from "dependency-cruiser";
import type { IDependency, IModule, IReporterOutput } from "dependency-cruiser";
import { parse } from "@babel/parser";
import path from "node:path";
import fs from "node:fs/promises";
import query from "esquery";
import { markdownTable } from "markdown-table";

const findRouting = async (filePath: string) => {
const findRouting = async ({ AST, fileContent }: { AST: any; fileContent: string }) => {
try {
const fileContent = await fs.readFile(filePath, "utf-8");
const AST = parse(fileContent, {
sourceType: "module",
plugins: ["jsx", "typescript"]
});
const search = (method: "get" | "post" | "delete" | "put" | "use", AST: any) => {
const selector = `CallExpression:has(MemberExpression > Identifier[name="${method}"])`;
const results = query(AST, selector);
Expand All @@ -25,13 +18,27 @@ const findRouting = async (filePath: string) => {
if (!pathValue) {
return []; // skip: it will only includes middleware
}
// single argument should be ignored
// req.get("host"); it is not routing
if (node.arguments.length === 1) {
return [];
}
const middlewareArguments =
method === "use"
? // @ts-ignore
node.arguments?.slice(1) ?? []
: // @ts-ignore
node.arguments?.slice(1, node.arguments.length - 1) ?? [];
const middlewares = middlewareArguments.map((arg: { start: number; end: number }) => {
const middlewares = middlewareArguments.map((arg: { type: string; start: number; end: number }) => {
// app.use(() => {});
if (arg.type === "ArrowFunctionExpression") {
return "Anonymous Function";
}
// app.use(function () {});
if (arg.type === "FunctionExpression") {
// @ts-ignore
return arg?.id?.name ?? "Anonymous Function";
}
return fileContent.slice(arg.start, arg.end);
});
return [
Expand All @@ -58,58 +65,77 @@ const findRouting = async (filePath: string) => {
return [];
}
};
const toAbsolute = (f: string) => {
return path.resolve(process.cwd(), f);
const toAbsolute = (cwd: string, f: string) => {
return path.resolve(cwd, f);
};

export async function analyzeDependency({
const hasImportExpress = (AST: any) => {
// import express from "express";
if (query(AST, "ImportDeclaration[source.value='express']").length > 0) {
return true;
}
// const express = require("express");
if (query(AST, "CallExpression[callee.name='require'][arguments.0.value='express']").length > 0) {
return true;
}
// const express = await import("express");
if (query(AST, "ImportExpression[source.value='express']").length > 0) {
return true;
}
return false;
};

interface AnalyzeDependencyParams {
filePath: string;
}

export async function analyzeDependency({ filePath }: AnalyzeDependencyParams) {
const fileContent = await fs.readFile(filePath, "utf-8");
try {
const AST = parse(fileContent, {
sourceType: "module",
plugins: ["jsx", "typescript"]
});
if (!hasImportExpress(AST)) {
return [];
}
return findRouting({ AST, fileContent });
} catch (e) {
console.error("Error while analyzing", filePath);
console.error(e);
return [];
}
}

export async function analyzeDependencies({
outputFormat,
rootDir,
filePaths,
rootBaseUrl = "",
includeOnly,
doNotFollow
cwd
}: {
rootDir: string;
filePaths: string[];
rootBaseUrl: string;
outputFormat: "markdown" | "json";
includeOnly?: string | string[];
doNotFollow?: string | string[];
cwd: string;
}) {
const ROOT_DIR = rootDir;
const hasImportExpress = (dep: IDependency) => {
return (
(dep.dependencyTypes.includes("npm") || dep.dependencyTypes.includes("npm-dev")) && dep.module === "express"
);
};
const underTheRoot = (module: IModule) => {
return toAbsolute(module.source).startsWith(ROOT_DIR);
};
const hasModuleImportExpress = (module: IModule) => {
return module.dependencies.some((dep) => hasImportExpress(dep));
};
const toRelative = (f: string) => {
return path.relative(ROOT_DIR, toAbsolute(f));
return path.relative(cwd, f);
};
const ARRAY_OF_FILES_AND_DIRS_TO_CRUISE: string[] = [ROOT_DIR];
const cruiseResult: IReporterOutput = cruise(ARRAY_OF_FILES_AND_DIRS_TO_CRUISE, {
includeOnly,
doNotFollow
});
if (typeof cruiseResult.output !== "object") {
throw new Error("NO OUTPUT");
}
const modules = cruiseResult.output.modules.filter(hasModuleImportExpress).filter(underTheRoot);
const allResults = await Promise.all(
modules.map(async (mo) => {
filePaths.map(async (filePath) => {
const absoluteFilePath = toAbsolute(cwd, filePath);
return {
filePath: toAbsolute(mo.source),
routers: await findRouting(toAbsolute(mo.source))
filePath: absoluteFilePath,
routers: await analyzeDependency({ filePath: absoluteFilePath })
};
})
);
if (outputFormat === "markdown") {
const table = [["File", "Method", "Routing", "Middlewares", "FilePath"]];
for (const result of allResults) {
if (result.routers.length === 0) {
continue;
}
table.push([`${rootBaseUrl}${toRelative(result.filePath)}`]);
result.routers.forEach((router) => {
table.push([
Expand Down
9 changes: 9 additions & 0 deletions test/fixtures/app.use-function/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Router } from "express";
const app = Router();
app.post("/getEvents", async (req, res, next) => {});

app.use("/useEvents", async (req, res, next) => {
// use anonymous function
});

export = app;
8 changes: 8 additions & 0 deletions test/fixtures/req.get-but-it-is-not-router/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Router } from "express";

const app = Router();

app.get("/get", (req) => {
const host = req.get("host"); // it is not router
});
export default app;
Loading

0 comments on commit 043aeb3

Please sign in to comment.