Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
- implement unit tests for converters

# 1.1 Refinement
- Better cmd description and help texts
- Alternative config path via `--config` flag
- `--force` flag to overwrite existing output files
- `@error` tag should not be required in JSDoc comments with default config
- You can change this behavior via config if needed
- `sourceFile.requireErrorTag` config option
Expand Down
62 changes: 43 additions & 19 deletions bin/commands/init.command.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import {SwerrCommand} from "./interfaces/swerr-command.js";
import path from "path";
import * as fs from "node:fs";
import {LogUtils} from "@swerr/core";
import {SWERR_CONFIG_FILE} from "../config.js";

const initConfigTemplate = `import {markdownConverter, htmlConverter} from "@swerr/converter"

// See more configuration options at https://swerr.apidocumentation.com/guide/introduction/config
export default {
sourceFile: {
inputDir: "./src/errors", // Directory to scan for error definitions
inputDir: "./src", // Directory to scan for error definitions
meta: {
projectName: "Your Application Name",
description: "The Application description",
Expand All @@ -19,7 +19,19 @@ export default {
},
options: {
ignoreDirs: [], // Directories to ignore during scanning (optional)
whitelistExtensions: [".js"] // File extensions to include during scanning (optional)
whitelistExtensions: [".js", ".ts"], // File extensions to include during scanning (optional)
// Optional: Custom error class detector function
// errorClassDetector: (ctx) => {
// // Example: Only include blocks with @error tag
// return ctx.jsDocTags.some(tag => tag.name === "error");
//
// // Example: Include blocks in files ending with "Exception.js" OR with @error tag
// // return ctx.fileName.endsWith("Exception.js") ||
// // ctx.jsDocTags.some(tag => tag.name === "error");
//
// // Example: Check if the file content contains "extends Error"
// // return ctx.fileContent.includes("extends Error");
// }
}
},
converter: [ // Example converters
Expand All @@ -38,23 +50,35 @@ export default {
]
}`;

export const initCommand: SwerrCommand<[string, string]> = {
command: "init",
description: "Create a basic swerr config file.",
action: async () => {
const configFilePath = path.resolve(process.cwd(), SWERR_CONFIG_FILE);

if (fs.existsSync(configFilePath)) {
LogUtils.error(`A ${SWERR_CONFIG_FILE} file already exists in the current directory.`);
process.exit(1);
}

export const initCommand = async (options: { force?: boolean; config?: string }) => {
const configFilePath = path.resolve(process.cwd(), options.config ?? SWERR_CONFIG_FILE);
const targetName = path.basename(configFilePath);

const existedBefore = fs.existsSync(configFilePath);
if (existedBefore && !options.force) {
try {
await fs.promises.writeFile(configFilePath, initConfigTemplate);
LogUtils.success(SWERR_CONFIG_FILE + " file has been created successfully.");
} catch (err) {
LogUtils.error(`Failed to create ${SWERR_CONFIG_FILE} file: ${err}`);
process.exit(1);
const stat = fs.lstatSync(configFilePath);
if (stat.isDirectory()) {
LogUtils.error(`A directory named ${targetName} already exists in the current directory.`);
} else {
LogUtils.error(`A ${targetName} file already exists in the current directory.`);
}
} catch {
LogUtils.error(`A ${targetName} entry already exists in the current directory.`);
}
process.exit(1);
}

try {
await fs.promises.mkdir(path.dirname(configFilePath), { recursive: true });
await fs.promises.writeFile(configFilePath, initConfigTemplate, { encoding: "utf8" });
if (existedBefore && options.force) {
LogUtils.success(`${targetName} file has been overwritten successfully.`);
} else {
LogUtils.success(`${targetName} file has been created successfully.`);
}
} catch (err) {
LogUtils.error(`Failed to create ${targetName} file: ${err}`);
process.exit(1);
}
}
6 changes: 0 additions & 6 deletions bin/commands/interfaces/swerr-command.ts

This file was deleted.

161 changes: 81 additions & 80 deletions bin/commands/run.command.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import {SwerrCommand} from "./interfaces/swerr-command.js";
import path from "path";
import * as fs from "node:fs";
import {scanJsdocs} from "../extraction/swerr-scan.js";
Expand All @@ -8,92 +7,94 @@ import {existsSync} from "node:fs";
import {pathToFileURL} from "node:url";
import {LogUtils, SwerrConfig, SwerrScheme} from "@swerr/core";

export const runCommand: SwerrCommand<[string, string]> = {
command: "run [configPath]",
description: "Create swerr documentation based on the config file.",
action: async (configPath: string | undefined) => {
const swerrConfig = await config(configPath);
const sourceDir = swerrConfig?.sourceFile?.inputDir || null
const outputDir = swerrConfig?.sourceFile?.export?.outputDir || null
LogUtils.success("Swerr Configuration loaded.");

if (!sourceDir || !outputDir) {
LogUtils.error("Source and output directories must be specified either via configuration file.");
process.exit(1);
}

const absoluteSourceDir = path.resolve(process.cwd(), sourceDir);
const absoluteOutputDir = path.resolve(process.cwd(), outputDir);

const sourceExists = fs.existsSync(absoluteSourceDir);
if (!sourceExists) {
LogUtils.error(`Source directory "${absoluteSourceDir}" does not exist.`);
process.exit(1);
}

try {
await fs.promises.mkdir(absoluteOutputDir, {recursive: true});
} catch (err) {
LogUtils.error(`Failed to create output directory "${absoluteOutputDir}": ${err}`);
process.exit(1);
}

scanJsdocs(absoluteSourceDir, swerrConfig?.sourceFile.options || {}).then(async result => {
LogUtils.info(`Scanned ${result.blocks.length} JSDocs block(s) from ${result.scannedFiles} file(s).`);
const scheme = translateToSourceScheme(result, swerrConfig)
LogUtils.info(`Translated scan result to swerr Scheme with ${scheme.errors.length} error(s).`);
await saveSourceScheme(swerrConfig!, absoluteOutputDir, scheme);
await runConverter(swerrConfig!, scheme);
}).catch(err => {
LogUtils.error(`Error during scanning: ${err}`);
})
}
export const runCommand = async (configPath: string | undefined) => {
const swerrConfig = await config(configPath);
const sourceDir = swerrConfig?.sourceFile?.inputDir || null
const outputDir = swerrConfig?.sourceFile?.export?.outputDir || null

if (!swerrConfig) {
LogUtils.error(`Swerr configuration file not found. Please create a ${SWERR_CONFIG_FILE} file in the current directory or specify a custom path using --config option.`);
process.exit(1);
}

if (!sourceDir || !outputDir) {
LogUtils.error("Source and output directories must be specified either via configuration file.");
process.exit(1);
}

LogUtils.success("Swerr Configuration loaded.");

const absoluteSourceDir = path.resolve(process.cwd(), sourceDir);
const absoluteOutputDir = path.resolve(process.cwd(), outputDir);

const sourceExists = fs.existsSync(absoluteSourceDir);
if (!sourceExists) {
LogUtils.error(`Source directory "${absoluteSourceDir}" does not exist.`);
process.exit(1);
}

try {
await fs.promises.mkdir(absoluteOutputDir, {recursive: true});
} catch (err) {
LogUtils.error(`Failed to create output directory "${absoluteOutputDir}": ${err}`);
process.exit(1);
}

scanJsdocs(absoluteSourceDir, swerrConfig?.sourceFile.options || {}).then(async result => {
LogUtils.info(`Scanned ${result.blocks.length} JSDocs block(s) from ${result.scannedFiles} file(s).`);
const scheme = translateToSourceScheme(result, swerrConfig)
LogUtils.info(`Translated scan result to swerr Scheme with ${scheme.errors.length} error(s).`);
await saveSourceScheme(swerrConfig!, absoluteOutputDir, scheme);
await runConverter(swerrConfig!, scheme);
}).catch(err => {
LogUtils.error(`Error during scanning: ${err}`);
})
}

async function config(configPath: string | undefined): Promise<SwerrConfig | null> {
try {
if (!configPath) {
configPath = path.resolve(process.cwd(), SWERR_CONFIG_FILE);
} else {
configPath = path.resolve(process.cwd(), configPath);
}
let cfg: SwerrConfig | null = null;
if (existsSync(configPath)) {
LogUtils.info(`Loading configuration from ${configPath}`);
try {
const imported = await import(pathToFileURL(configPath).href);
cfg = imported.default ?? imported;
return cfg;
} catch (err) {
LogUtils.error(`Failed to load configuration from ${configPath}: ${err}`);
process.exit(1);
}
}
return null;
} catch (err) {
LogUtils.error(`Error loading configuration: ${err}`);
process.exit(1);
}
try {
if (!configPath) {
configPath = path.resolve(process.cwd(), SWERR_CONFIG_FILE);
} else {
configPath = path.resolve(process.cwd(), configPath);
}
let cfg: SwerrConfig | null = null;
if (existsSync(configPath)) {
LogUtils.info(`Loading configuration from ${configPath}`);
try {
const imported = await import(pathToFileURL(configPath).href);
cfg = imported.default ?? imported;
return cfg;
} catch (err) {
LogUtils.error(`Failed to load configuration from ${configPath}: ${err}`);
process.exit(1);
}
}
return null;
} catch (err) {
LogUtils.error(`Error loading configuration: ${err}`);
process.exit(1);
}
}

async function saveSourceScheme(config: SwerrConfig, absoluteOutputDir: string, scheme: SwerrScheme) {
if (!config?.sourceFile?.export?.saveToFile) return
const fileName = config?.sourceFile?.export?.fileName || "swerr-docs.json";
const outputFilePath = path.join(absoluteOutputDir, fileName);
const docContent = JSON.stringify(scheme, null, 2);
try {
await fs.promises.writeFile(outputFilePath, docContent, "utf8");
LogUtils.success(`Swerr Source File written to ${outputFilePath}`);
} catch (err) {
console.error(`Failed to write documentation to "${outputFilePath}":`, err);
process.exit(1);
}
if (!config?.sourceFile?.export?.saveToFile) return
const fileName = config?.sourceFile?.export?.fileName || "swerr-docs.json";
const outputFilePath = path.join(absoluteOutputDir, fileName);
const docContent = JSON.stringify(scheme, null, 2);
try {
await fs.promises.writeFile(outputFilePath, docContent, "utf8");
LogUtils.success(`Swerr Source File written to ${outputFilePath}`);
} catch (err) {
console.error(`Failed to write documentation to "${outputFilePath}":`, err);
process.exit(1);
}
}

async function runConverter(config: SwerrConfig, scheme: SwerrScheme) {
for (const converter of config.converter) {
await converter.factory(converter.config, scheme);
}
for (const converter of config.converter) {
await converter.factory(converter.config, scheme);
}
}
26 changes: 23 additions & 3 deletions bin/extraction/swerr-scan.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { promises as fs, Dirent } from "node:fs";
import * as path from "node:path";
import {JsdocBlock, JsdocTag} from "./types/jsdoc.js";
import {JsdocBlock, JsdocTag, ErrorClassDetectorContext} from "./types/jsdoc.js";
import {ScanOptions, ScanResult} from "./types/scan.js";
import {DEFAULT_IGNORE_DIRS, DEFAULT_MAX_FILE_SIZE} from "../config.js";
import {LogUtils} from "@swerr/core";
Expand Down Expand Up @@ -204,19 +204,39 @@ export async function scanJsdocs(rootDir: string, options: ScanOptions): Promise

const newlineIdx = collectNewlineIndices(text);
const found = findJsdocBlocks(text);
const fileName = path.basename(filePath);

for (const b of found) {
const startLine = indexToLine(b.index, newlineIdx);
const lines = normalizeJsdocContentLines(b.raw);
const parsed = parseNormalizedJsdocLines(lines);

blocks.push({
const block: JsdocBlock = {
filePath,
startLine,
raw: b.raw,
description: parsed.description,
tags: parsed.tags,
});
};

// Apply custom error class detector if provided
if (options.errorClassDetector) {
const ctx: ErrorClassDetectorContext = {
jsDocTags: block.tags,
fileName,
fileContent: text,
block,
};

if (options.errorClassDetector(ctx)) {
blocks.push(block);
} else {
LogUtils.debug(`Custom detector: Skipping JSDoc block in ${filePath} at line ${startLine}`);
}
} else {
// Default behavior: include all blocks (filtering happens in translate-to-source-scheme)
blocks.push(block);
}
}
}

Expand Down
17 changes: 16 additions & 1 deletion bin/extraction/types/jsdoc.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,19 @@ export type JsdocBlock = {
description: string;
/** All tags in encounter order */
tags: JsdocTag[];
};
};

/**
* Context object passed to the error class detector function.
* Provides all relevant information to determine if a js file represents an error class.
*/
export type ErrorClassDetectorContext = {
/** The JSDoc tags found in the block */
jsDocTags: JsdocTag[];
/** The file name (basename) */
fileName: string;
/** The full file content */
fileContent: string;
/** The JSDoc block itself */
block: JsdocBlock;
};
12 changes: 11 additions & 1 deletion bin/extraction/types/scan.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {SwerrConfig} from "../../core/interfaces/swerr-config.js";
import {ErrorClassDetectorContext} from "./jsdoc.js";

export type ScanResult = {
rootDir: string;
Expand All @@ -7,4 +8,13 @@ export type ScanResult = {
skippedFiles: number;
};

export type ScanOptions = SwerrConfig["sourceFile"]["options"]
export type ScanOptions = SwerrConfig["sourceFile"]["options"] & {
/**
* Custom function to determine if a JSDoc block represents an error class.
* If not provided, defaults to checking for the @error tag.
*
* @param ctx - Context object containing jsDocTags, fileName, fileContent, and the block itself
* @returns true if the block represents an error class, false otherwise
*/
errorClassDetector?: (ctx: ErrorClassDetectorContext) => boolean;
}
Loading