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
8 changes: 7 additions & 1 deletion src/executable/TypiaGenerateWizard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export namespace TypiaGenerateWizard {
action,
) => {
// PREPARE ASSETS
command.argument("[files...]", "input .ts files (alternative to --input)");
command.option("--input [path]", "input directory");
command.option("--output [directory]", "output directory");
command.option("--project [project]", "tsconfig.json file location");
Expand Down Expand Up @@ -68,7 +69,11 @@ export namespace TypiaGenerateWizard {
};

return action(async (options) => {
options.input ??= await input("input")("input directory");
// If files are provided, input directory is not required
const hasFiles = options.files && options.files.length > 0;
if (!hasFiles) {
options.input ??= await input("input")("input directory");
}
options.output ??= await input("output")("output directory");
options.project ??= await configure();
return options as IArguments;
Expand All @@ -79,5 +84,6 @@ export namespace TypiaGenerateWizard {
input: string;
output: string;
project: string;
files?: string[];
}
}
9 changes: 8 additions & 1 deletion src/executable/setup/ArgumentParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,15 @@ export namespace ArgumentParser {
// TAKE OPTIONS
const action = (closure: (options: Partial<T>) => Promise<T>) =>
new Promise<T>((resolve, reject) => {
commander.program.action(async (options) => {
commander.program.action(async (...args: unknown[]) => {
try {
// Commander passes: (positionalArgs..., options, command)
// Use program.opts() for reliable option extraction
const options = commander.program.opts() as Partial<T>;
// If there are positional arguments (files), attach them to options
if (args.length > 0 && Array.isArray(args[0])) {
(options as Partial<T> & { files?: string[] }).files = args[0];
}
Comment on lines +26 to +34
Copy link

Copilot AI Dec 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new file arguments feature in the CLI lacks automated test coverage. While the repository has comprehensive testing for transformation features, there are no tests for CLI functionality like the ArgumentParser or TypiaGenerateWizard. Consider adding integration tests that verify file argument parsing, validation, and correct handling of both directory and file modes.

Copilot uses AI. Check for mistakes.
resolve(await closure(options));
} catch (exp) {
reject(exp);
Expand Down
79 changes: 51 additions & 28 deletions src/programmers/TypiaProgrammer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,28 @@ export namespace TypiaProgrammer {
input: string;
output: string;
project: string;
files?: string[];
}

export const build = async (
location: TypiaProgrammer.ILocation,
): Promise<void> => {
location.input = path.resolve(location.input);
const hasFiles = location.files && location.files.length > 0;

// Resolve paths
if (!hasFiles) {
location.input = path.resolve(location.input);
}
location.output = path.resolve(location.output);

if ((await is_directory(location.input)) === false)
throw new URIError(
"Error on TypiaGenerator.generate(): input path is not a directory.",
);
else if (fs.existsSync(location.output) === false)
// Validate directories
if (!hasFiles) {
if ((await is_directory(location.input)) === false)
throw new URIError(
"Error on TypiaGenerator.generate(): input path is not a directory.",
);
}
if (fs.existsSync(location.output) === false)
await fs.promises.mkdir(location.output, { recursive: true });
else if ((await is_directory(location.output)) === false) {
const parent: string = path.join(location.output, "..");
Expand All @@ -34,6 +43,11 @@ export namespace TypiaProgrammer {
await fs.promises.mkdir(location.output);
}

// Resolve file paths
const resolvedFiles = hasFiles
? location.files!.map((f) => path.resolve(f))
: [];

Copy link

Copilot AI Dec 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing validation for file existence and accessibility. When files are provided as arguments, there's no check that they exist or are readable before attempting to create the TypeScript program. This could lead to unclear error messages later in the process. Consider adding validation similar to the directory validation for input paths.

Suggested change
// Validate provided files (when using explicit file list)
if (hasFiles) {
for (const file of resolvedFiles) {
try {
const stat = await fs.promises.stat(file);
if (stat.isFile() === false) {
throw new URIError(
"Error on TypiaGenerator.generate(): input file path is not a file: " +
file,
);
}
await fs.promises.access(file, fs.constants.R_OK);
} catch {
throw new URIError(
"Error on TypiaGenerator.generate(): input file does not exist or is not readable: " +
file,
);
}
}
}

Copilot uses AI. Check for mistakes.
// CREATE PROGRAM
const { options: compilerOptions } = ts.parseJsonConfigFileContent(
ts.readConfigFile(location.project, ts.sys.readFile).config,
Expand All @@ -47,16 +61,18 @@ export namespace TypiaProgrammer {
);

const program: ts.Program = ts.createProgram(
await (async () => {
const container: string[] = [];
await gather({
location,
container,
from: location.input,
to: location.output,
});
return container;
})(),
hasFiles
? resolvedFiles
: await (async () => {
const container: string[] = [];
await gather({
location,
container,
from: location.input,
to: location.output,
});
return container;
})(),
compilerOptions,
);

Expand All @@ -65,16 +81,23 @@ export namespace TypiaProgrammer {
const result: ts.TransformationResult<ts.SourceFile> = ts.transform(
program
.getSourceFiles()
.filter(
(file) =>
!file.isDeclarationFile &&
path.resolve(file.fileName).indexOf(location.input) !== -1,
),
[
ImportTransformer.transform({
from: location.input,
to: location.output,
.filter((file) => {
if (file.isDeclarationFile) return false;
const resolved = path.resolve(file.fileName);
return hasFiles
? resolvedFiles.includes(resolved)
: resolved.indexOf(location.input) !== -1;
}),
[
// Skip import transformation when processing individual files
...(hasFiles
? []
: [
ImportTransformer.transform({
from: location.input,
to: location.output,
}),
]),
Comment on lines +92 to +100
Copy link

Copilot AI Dec 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment states "Skip import transformation when processing individual files" but doesn't explain why this is necessary. The ImportTransformer appears to rewrite import paths from the input directory structure to the output directory structure. Skipping this for individual files may cause import paths to break if the generated files have different relative paths. This needs clarification or a more robust solution.

Copilot uses AI. Check for mistakes.
transform(
program,
((compilerOptions.plugins as any[]) ?? []).find(
Expand Down Expand Up @@ -125,9 +148,9 @@ export namespace TypiaProgrammer {
newLine: ts.NewLineKind.LineFeed,
});
for (const file of result.transformed) {
const to: string = path
.resolve(file.fileName)
.replace(location.input, location.output);
const to: string = hasFiles
? path.join(location.output, path.basename(file.fileName))
: path.resolve(file.fileName).replace(location.input, location.output);
Comment on lines +151 to +153
Copy link

Copilot AI Dec 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When using file arguments mode, the output path can collide if multiple input files have the same basename. For example, src/users/user.typia.ts and src/orders/user.typia.ts would both write to output/user.typia.ts, with the second overwriting the first. Consider adding validation to detect duplicate basenames or implement a conflict resolution strategy.

Copilot uses AI. Check for mistakes.

const content: string = printer.printFile(file);
await fs.promises.writeFile(to, content, "utf8");
Expand Down
Loading