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
43 changes: 32 additions & 11 deletions src/commands/actor/generate-schema-types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { mkdir, writeFile } from 'node:fs/promises';
import { mkdir, stat, writeFile } from 'node:fs/promises';
import path from 'node:path';
import process from 'node:process';

Expand Down Expand Up @@ -54,7 +54,9 @@ Reads the input schema from one of these locations (in priority order):
3. .actor/INPUT_SCHEMA.json
4. INPUT_SCHEMA.json

Optionally specify custom schema path to use.`;
Optionally specify a custom schema file path, or a directory path.
When a directory is provided, all schemas are discovered from it
just as if the command were run from that directory with no argument.`;

static override flags = {
output: Flags.string({
Expand All @@ -79,16 +81,34 @@ Optionally specify custom schema path to use.`;
static override args = {
path: Args.string({
required: false,
description: 'Optional path to the input schema file. If not provided, searches default locations.',
description:
'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.',
}),
};

async run() {
const cwd = process.cwd();

let forcePath: string | undefined;
let effectiveCwd = cwd;

if (this.args.path) {
const resolvedPath = path.resolve(cwd, this.args.path);
// Ignore stat errors (e.g. path does not exist); downstream will surface a clear message.
const isDirectory = await stat(resolvedPath)
.then((s) => s.isDirectory())
.catch(() => false);

if (isDirectory) {
effectiveCwd = resolvedPath;
} else {
forcePath = resolvedPath;
}
}

const { inputSchema } = await readAndValidateInputSchema({
forcePath: this.args.path,
cwd,
forcePath,
cwd: effectiveCwd,
getMessage: (schemaPath) =>
schemaPath
? `Generating types from input schema at ${schemaPath}`
Expand All @@ -112,20 +132,21 @@ Optionally specify custom schema path to use.`;

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

const outputDir = path.resolve(cwd, this.flags.output);
const outputDir = path.resolve(effectiveCwd, this.flags.output);
await mkdir(outputDir, { recursive: true });

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

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

// When no custom path is provided, also generate types from additional schemas
if (!this.args.path) {
// When no specific file path is provided, also generate types from additional schemas
// (this includes both "no argument" and "directory argument" modes)
if (!forcePath) {
const schemaResults = await Promise.allSettled([
this.generateDatasetTypes({ cwd, outputDir, compileOptions }),
this.generateOutputTypes({ cwd, outputDir, compileOptions }),
this.generateKvsTypes({ cwd, outputDir, compileOptions }),
this.generateDatasetTypes({ cwd: effectiveCwd, outputDir, compileOptions }),
this.generateOutputTypes({ cwd: effectiveCwd, outputDir, compileOptions }),
this.generateKvsTypes({ cwd: effectiveCwd, outputDir, compileOptions }),
]);

const schemaLabels = ['Dataset', 'Output', 'Key-Value Store'];
Expand Down
84 changes: 84 additions & 0 deletions test/local/commands/actor/generate-schema-types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,90 @@ describe('apify actor generate-schema-types', () => {
});
});

describe('directory path argument', () => {
it('should discover and generate all schema types from a directory', async () => {
const projectDir = joinPath('my-actor-project');
await mkdir(projectDir, { recursive: true });

await setupActorConfig(projectDir, {
datasetSchemaRef: validDatasetSchemaPath,
outputSchemaRef: validOutputSchemaPath,
kvsSchemaRef: validKvsSchemaPath,
});

await testRunCommand(ActorGenerateSchemaTypesCommand, {
args_path: projectDir,
});

// All schema types should be generated inside the project directory
const outputDir = join(projectDir, 'src', '.generated', 'actor');

const inputFile = await readFile(join(outputDir, 'input.ts'), 'utf-8');
expect(inputFile).toContain('export interface');

const datasetFile = await readFile(join(outputDir, 'dataset.ts'), 'utf-8');
expect(datasetFile).toContain('export interface');

const outputFile = await readFile(join(outputDir, 'output.ts'), 'utf-8');
expect(outputFile).toContain('export interface');

const kvsFile = await readFile(join(outputDir, 'key-value-store.ts'), 'utf-8');
expect(kvsFile).toContain('export interface');
});

it('should discover input schema from default locations in the directory', async () => {
const projectDir = joinPath('default-locations-project');
const actorDir = join(projectDir, '.actor');
await mkdir(actorDir, { recursive: true });

// Place input schema at a default location without actor.json referencing it
const inputSchema = {
title: 'Test',
type: 'object',
schemaVersion: 1,
properties: {
query: { title: 'Query', description: 'Search query', type: 'string', editor: 'textfield' },
},
};

await writeFile(join(actorDir, 'INPUT_SCHEMA.json'), JSON.stringify(inputSchema));

await testRunCommand(ActorGenerateSchemaTypesCommand, {
args_path: projectDir,
});

const outputDir = join(projectDir, 'src', '.generated', 'actor');
const generatedFile = await readFile(join(outputDir, 'input.ts'), 'utf-8');
expect(generatedFile).toContain('export interface');
expect(generatedFile).toContain('query');
});

it('should output types inside the provided directory by default', async () => {
const projectDir = joinPath('output-location-project');
await setupActorConfig(projectDir, {});

await testRunCommand(ActorGenerateSchemaTypesCommand, {
args_path: projectDir,
});

// Output should be inside the project directory, not in cwd
const outputFile = join(projectDir, 'src', '.generated', 'actor', 'input.ts');
const generatedFile = await readFile(outputFile, 'utf-8');
expect(generatedFile).toContain('export interface');
});

it('should fail with clear error when directory has no schemas', async () => {
const emptyDir = joinPath('empty-project');
await mkdir(emptyDir, { recursive: true });

await testRunCommand(ActorGenerateSchemaTypesCommand, {
args_path: emptyDir,
});

expect(lastErrorMessage()).include('Input schema has not been found');
});
});

it('should write successful schemas and report error for the failing one', async () => {
const outputDir = joinPath('partial-fail-output');

Expand Down
Loading