Skip to content

Commit

Permalink
feat: add basic support of root directory analysis
Browse files Browse the repository at this point in the history
  • Loading branch information
antoine-coulon committed Oct 25, 2022
1 parent 2ebcef7 commit 0b413b9
Show file tree
Hide file tree
Showing 11 changed files with 405 additions and 119 deletions.
9 changes: 8 additions & 1 deletion packages/skott/bin/cli.ts
Expand Up @@ -6,6 +6,8 @@ import { fileURLToPath } from "node:url";

import sade from "sade";

import { kExpectedModuleExtensions } from "../src/modules/walkers/ecmascript/module-resolver.js";

import { displaySkott } from "./main.js";

function readManifestVersion(): string {
Expand All @@ -26,7 +28,7 @@ function readManifestVersion(): string {
}
}

const cli = sade("skott <entrypoint>", true)
const cli = sade("skott [entrypoint]", true)
.version(readManifestVersion())
.describe("Start the Skott analysis to fully build the graph")

Expand All @@ -50,6 +52,11 @@ const cli = sade("skott <entrypoint>", true)
"Either display the result of the analysis as a graph or as a file-tree",
1
)
.option(
"ext, fileExtensions",
"File extensions to consider when building the graph",
[...kExpectedModuleExtensions].join(",")
)
.option(
"-f, --staticFile",
"Generate a static file from the graph. Can be 'none', 'svg', 'png', 'md'.",
Expand Down
36 changes: 23 additions & 13 deletions packages/skott/bin/main.ts
Expand Up @@ -8,8 +8,8 @@ import { generateMermaid } from "ligie";
import ora from "ora";

import skott from "../index.js";
import { kExpectedModuleExtensions } from "../src/modules/walkers/ecmascript/module-resolver.js";
import { SkottInstance, SkottNode, SkottNodeBody } from "../src/skott.js";
import { findWorkspaceEntrypointModule } from "../src/workspace/index.js";

const kLeftSeparator = "└──";

Expand Down Expand Up @@ -302,33 +302,43 @@ type CliOptions = {
showCircularDependencies: boolean;
trackThirdPartyDependencies: boolean;
trackBuiltinDependencies: boolean;
fileExtensions: string;
};

export async function displaySkott(
entrypoint: string,
entrypoint: string | undefined,
options: CliOptions
): Promise<void> {
const entrypointModule =
entrypoint ?? (await findWorkspaceEntrypointModule());

console.log(
`\n Running ${kleur.blue().bold("Skott")} from entrypoint: ${kleur
.yellow()
.underline()
.bold(`${entrypointModule}`)}`
);
if (entrypoint) {
console.log(
`\n Running ${kleur.blue().bold("Skott")} from entrypoint: ${kleur
.yellow()
.underline()
.bold(`${entrypoint}`)}`
);
} else {
console.log(
`\n Running ${kleur.blue().bold("Skott")} from current directory: ${kleur
.yellow()
.underline()
.bold(`${path.basename(process.cwd())}`)}`
);
}

const spinner = ora(`Initializing ${kleur.blue().bold("Skott")}`).start();
const start = performance.now();

const skottInstance = await skott({
entrypoint: entrypointModule,
entrypoint: entrypoint ? entrypoint : undefined,
circularMaxDepth: options.circularMaxDepth ?? Number.POSITIVE_INFINITY,
includeBaseDir: options.includeBaseDir,
dependencyTracking: {
thirdParty: options.trackThirdPartyDependencies,
builtin: options.trackBuiltinDependencies
}
},
fileExtensions: options.fileExtensions
.split(",")
.filter((ext) => kExpectedModuleExtensions.has(ext))
});

const timeTook = `${(performance.now() - start).toFixed(3)}ms`;
Expand Down
25 changes: 25 additions & 0 deletions packages/skott/src/filesystem/file-reader.ts
@@ -1,8 +1,15 @@
import { R_OK } from "node:constants";
import fs from "node:fs/promises";
import path from "node:path";

import {
isDirSupportedByDefault,
isFileSupportedByDefault
} from "../modules/walkers/ecmascript/module-resolver.js";

export interface FileReader {
read: (filename: string) => Promise<string>;
readdir: (root: string, extensions: string[]) => AsyncGenerator<string>;
stats: (filename: string) => Promise<number>;
getCurrentWorkingDir: () => string;
}
Expand All @@ -22,4 +29,22 @@ export class FileSystemReader implements FileReader {
getCurrentWorkingDir(): string {
return process.cwd();
}

async *readdir(
root: string,
fileExtensions: string[]
): AsyncGenerator<string> {
const rootDir = await fs.opendir(root);

for await (const dirent of rootDir) {
if (dirent.isDirectory() && isDirSupportedByDefault(dirent.name)) {
yield* this.readdir(path.join(root, dirent.name), fileExtensions);
} else if (
isFileSupportedByDefault(dirent.name) &&
fileExtensions.includes(path.extname(dirent.name))
) {
yield path.join(root, dirent.name);
}
}
}
}
47 changes: 40 additions & 7 deletions packages/skott/src/modules/walkers/ecmascript/module-resolver.ts
@@ -1,7 +1,7 @@
import { builtinModules } from "node:module";
import path from "node:path";

import { FileReader } from "../../../filesystem/file-reader";
import type { FileReader } from "../../../filesystem/file-reader";

const NODE_PROTOCOL = "node:";

Expand Down Expand Up @@ -42,11 +42,18 @@ export function isBinaryModule(module: string): boolean {
return module.endsWith(".node");
}

export function isTypeScriptModule(module: string): boolean {
return path.extname(module) === ".ts" || path.extname(module) === ".tsx";
export function isJavaScriptModule(module: string): boolean {
const extension = path.extname(module);

return (
extension === ".js" ||
extension === ".jsx" ||
extension === ".mjs" ||
extension === ".cjs"
);
}

const kExpectedModuleExtensions = new Set([
export const kExpectedModuleExtensions = new Set([
".js",
".jsx",
".mjs",
Expand All @@ -55,8 +62,34 @@ const kExpectedModuleExtensions = new Set([
".tsx"
]);

export function isSupportedModule(module: string): boolean {
return kExpectedModuleExtensions.has(path.extname(module));
function isTypeScriptDeclarationFile(module: string): boolean {
return module.endsWith(".d.ts");
}

function isTestFile(fileName: string): boolean {
return fileName.includes(".test") || fileName.includes(".spec");
}

export function isFileSupportedByDefault(fileName: string): boolean {
return (
kExpectedModuleExtensions.has(path.extname(fileName)) &&
!isTypeScriptDeclarationFile(fileName) &&
!isTestFile(fileName)
);
}

// TODO: Use .gitignore instead
export function isDirSupportedByDefault(directoryName: string): boolean {
return (
!directoryName.includes("node_modules") &&
!directoryName.includes("dist") &&
!directoryName.includes("build") &&
!directoryName.includes("coverage") &&
!directoryName.includes("docs") &&
!directoryName.includes("examples") &&
!directoryName.includes("test") &&
!directoryName.includes("__tests__")
);
}

async function isExistingModule(
Expand All @@ -83,7 +116,7 @@ export async function resolveImportedModulePath(
* If the module is supported and it appears that `moduleExists` is false, it
* might be the case where TypeScript is used with ECMAScript modules.
*/
if (isSupportedModule(module) && moduleExists) {
if (isFileSupportedByDefault(module) && moduleExists) {
return module;
}

Expand Down
50 changes: 35 additions & 15 deletions packages/skott/src/skott.ts
Expand Up @@ -13,7 +13,8 @@ import {
isBuiltinModule,
isJSONModule,
isThirdPartyModule,
isTypeScriptModule
isJavaScriptModule,
kExpectedModuleExtensions
} from "./modules/walkers/ecmascript/module-resolver.js";
import {
buildPathAliases,
Expand All @@ -30,13 +31,14 @@ export type SkottNodeBody = {
export type SkottNode = VertexDefinition<SkottNodeBody>;

export interface SkottConfig {
entrypoint: string;
entrypoint?: string;
circularMaxDepth?: number;
includeBaseDir: boolean;
dependencyTracking: {
thirdParty: boolean;
builtin: boolean;
};
fileExtensions: string[];
}

export interface SkottStructure {
Expand All @@ -59,25 +61,20 @@ const defaultConfig = {
dependencyTracking: {
thirdParty: false,
builtin: false
}
},
fileExtensions: [...kExpectedModuleExtensions]
};

export class Skott {
#moduleWalker = new JavaScriptModuleWalker();
#moduleWalker = new TypeScriptModuleWalker();
#projectGraph = new DiGraph<SkottNode>();
#visitedNodes = new Set<string>();
#baseDir = "";

constructor(
private readonly config: SkottConfig = defaultConfig,
private readonly fileReader: FileReader = new FileSystemReader()
) {
if (!this.config.entrypoint) {
throw new Error(
"An entrypoint must be provided to Skott to build the graph"
);
}
}
) {}

private formatNodePath(nodePath: string): string {
/**
Expand Down Expand Up @@ -288,14 +285,16 @@ export class Skott {
};
}

public async initialize(): Promise<SkottInstance> {
private async buildFromEntrypoint(entrypoint: string): Promise<void> {
const entrypointModulePath = await resolveImportedModulePath(
this.config.entrypoint,
entrypoint,
this.fileReader
);

if (isTypeScriptModule(entrypointModulePath)) {
this.#moduleWalker = new TypeScriptModuleWalker();
// TODO: support that in build root also
if (isJavaScriptModule(entrypointModulePath)) {
this.#moduleWalker = new JavaScriptModuleWalker();
} else {
await buildPathAliases(this.fileReader);
}

Expand All @@ -307,6 +306,27 @@ export class Skott {
entrypointModulePath,
rootFileContent
);
}

private async buildFromRootDirectory(): Promise<void> {
// Must be set to "true" without entrypoint as we don't know what the base is
this.config.includeBaseDir = true;
for await (const rootFile of this.fileReader.readdir(
this.fileReader.getCurrentWorkingDir(),
this.config.fileExtensions
)) {
const rootFileContent = await this.fileReader.read(rootFile);
await this.addNode(rootFile);
await this.collectModuleDeclarationsFromFile(rootFile, rootFileContent);
}
}

public async initialize(): Promise<SkottInstance> {
if (this.config.entrypoint) {
await this.buildFromEntrypoint(this.config.entrypoint);
} else {
await this.buildFromRootDirectory();
}

return {
getStructure: this.makeProjectStructure.bind(this),
Expand Down

0 comments on commit 0b413b9

Please sign in to comment.