Skip to content

Commit a08b242

Browse files
l2yshoClaudeclaude
authored
feat: support directory path as argument in generate-schema-types command (#1069)
closes #1033 --------- Co-authored-by: Claude <claude@container> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 51c3ca4 commit a08b242

2 files changed

Lines changed: 116 additions & 11 deletions

File tree

src/commands/actor/generate-schema-types.ts

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { mkdir, writeFile } from 'node:fs/promises';
1+
import { mkdir, stat, writeFile } from 'node:fs/promises';
22
import path from 'node:path';
33
import process from 'node:process';
44

@@ -54,7 +54,9 @@ Reads the input schema from one of these locations (in priority order):
5454
3. .actor/INPUT_SCHEMA.json
5555
4. INPUT_SCHEMA.json
5656
57-
Optionally specify custom schema path to use.`;
57+
Optionally specify a custom schema file path, or a directory path.
58+
When a directory is provided, all schemas are discovered from it
59+
just as if the command were run from that directory with no argument.`;
5860

5961
static override group = 'Actor Runtime';
6062

@@ -98,16 +100,34 @@ Optionally specify custom schema path to use.`;
98100
static override args = {
99101
path: Args.string({
100102
required: false,
101-
description: 'Optional path to the input schema file. If not provided, searches default locations.',
103+
description:
104+
'Optional path to an input schema file or a directory containing Actor schemas. If a directory is given, all schema types are generated from it. If not provided, searches default locations in the current directory.',
102105
}),
103106
};
104107

105108
async run() {
106109
const cwd = process.cwd();
107110

111+
let forcePath: string | undefined;
112+
let effectiveCwd = cwd;
113+
114+
if (this.args.path) {
115+
const resolvedPath = path.resolve(cwd, this.args.path);
116+
// Ignore stat errors (e.g. path does not exist); downstream will surface a clear message.
117+
const isDirectory = await stat(resolvedPath)
118+
.then((s) => s.isDirectory())
119+
.catch(() => false);
120+
121+
if (isDirectory) {
122+
effectiveCwd = resolvedPath;
123+
} else {
124+
forcePath = resolvedPath;
125+
}
126+
}
127+
108128
const { inputSchema } = await readAndValidateInputSchema({
109-
forcePath: this.args.path,
110-
cwd,
129+
forcePath,
130+
cwd: effectiveCwd,
111131
getMessage: (schemaPath) =>
112132
schemaPath
113133
? `Generating types from input schema at ${schemaPath}`
@@ -131,20 +151,21 @@ Optionally specify custom schema path to use.`;
131151

132152
const result = await compile(stripTitles(schemaToCompile) as JSONSchema4, name, compileOptions);
133153

134-
const outputDir = path.resolve(cwd, this.flags.output);
154+
const outputDir = path.resolve(effectiveCwd, this.flags.output);
135155
await mkdir(outputDir, { recursive: true });
136156

137157
const outputFile = path.join(outputDir, `${name}.ts`);
138158
await writeFile(outputFile, result, 'utf-8');
139159

140160
success({ message: `Generated types written to ${outputFile}` });
141161

142-
// When no custom path is provided, also generate types from additional schemas
143-
if (!this.args.path) {
162+
// When no specific file path is provided, also generate types from additional schemas
163+
// (this includes both "no argument" and "directory argument" modes)
164+
if (!forcePath) {
144165
const schemaResults = await Promise.allSettled([
145-
this.generateDatasetTypes({ cwd, outputDir, compileOptions }),
146-
this.generateOutputTypes({ cwd, outputDir, compileOptions }),
147-
this.generateKvsTypes({ cwd, outputDir, compileOptions }),
166+
this.generateDatasetTypes({ cwd: effectiveCwd, outputDir, compileOptions }),
167+
this.generateOutputTypes({ cwd: effectiveCwd, outputDir, compileOptions }),
168+
this.generateKvsTypes({ cwd: effectiveCwd, outputDir, compileOptions }),
148169
]);
149170

150171
const schemaLabels = ['Dataset', 'Output', 'Key-Value Store'];

test/local/commands/actor/generate-schema-types.test.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,90 @@ describe('apify actor generate-schema-types', () => {
488488
});
489489
});
490490

491+
describe('directory path argument', () => {
492+
it('should discover and generate all schema types from a directory', async () => {
493+
const projectDir = joinPath('my-actor-project');
494+
await mkdir(projectDir, { recursive: true });
495+
496+
await setupActorConfig(projectDir, {
497+
datasetSchemaRef: validDatasetSchemaPath,
498+
outputSchemaRef: validOutputSchemaPath,
499+
kvsSchemaRef: validKvsSchemaPath,
500+
});
501+
502+
await testRunCommand(ActorGenerateSchemaTypesCommand, {
503+
args_path: projectDir,
504+
});
505+
506+
// All schema types should be generated inside the project directory
507+
const outputDir = join(projectDir, 'src', '__generated__', 'actor');
508+
509+
const inputFile = await readFile(join(outputDir, 'input.ts'), 'utf-8');
510+
expect(inputFile).toContain('export interface');
511+
512+
const datasetFile = await readFile(join(outputDir, 'dataset.ts'), 'utf-8');
513+
expect(datasetFile).toContain('export interface');
514+
515+
const outputFile = await readFile(join(outputDir, 'output.ts'), 'utf-8');
516+
expect(outputFile).toContain('export interface');
517+
518+
const kvsFile = await readFile(join(outputDir, 'key-value-store.ts'), 'utf-8');
519+
expect(kvsFile).toContain('export interface');
520+
});
521+
522+
it('should discover input schema from default locations in the directory', async () => {
523+
const projectDir = joinPath('default-locations-project');
524+
const actorDir = join(projectDir, '.actor');
525+
await mkdir(actorDir, { recursive: true });
526+
527+
// Place input schema at a default location without actor.json referencing it
528+
const inputSchema = {
529+
title: 'Test',
530+
type: 'object',
531+
schemaVersion: 1,
532+
properties: {
533+
query: { title: 'Query', description: 'Search query', type: 'string', editor: 'textfield' },
534+
},
535+
};
536+
537+
await writeFile(join(actorDir, 'INPUT_SCHEMA.json'), JSON.stringify(inputSchema));
538+
539+
await testRunCommand(ActorGenerateSchemaTypesCommand, {
540+
args_path: projectDir,
541+
});
542+
543+
const outputDir = join(projectDir, 'src', '__generated__', 'actor');
544+
const generatedFile = await readFile(join(outputDir, 'input.ts'), 'utf-8');
545+
expect(generatedFile).toContain('export interface');
546+
expect(generatedFile).toContain('query');
547+
});
548+
549+
it('should output types inside the provided directory by default', async () => {
550+
const projectDir = joinPath('output-location-project');
551+
await setupActorConfig(projectDir, {});
552+
553+
await testRunCommand(ActorGenerateSchemaTypesCommand, {
554+
args_path: projectDir,
555+
});
556+
557+
// Output should be inside the project directory, not in cwd
558+
const outputFile = join(projectDir, 'src', '__generated__', 'actor', 'input.ts');
559+
const generatedFile = await readFile(outputFile, 'utf-8');
560+
expect(generatedFile).toContain('export interface');
561+
});
562+
563+
it('should fail with clear error when directory has no schemas', async () => {
564+
const emptyDir = joinPath('empty-project');
565+
await mkdir(emptyDir, { recursive: true });
566+
567+
await testRunCommand(ActorGenerateSchemaTypesCommand, {
568+
args_path: emptyDir,
569+
});
570+
571+
expect(lastErrorMessage()).include('Input schema has not been found');
572+
});
573+
});
574+
491575
it('should write successful schemas and report error for the failing one', async () => {
492576
const outputDir = joinPath('partial-fail-output');
493577

0 commit comments

Comments
 (0)