Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a2780e8
feat(actor): adds actor types generation
l2ysho Jan 19, 2026
e5639ee
feat(cli): enable generate-types command in generate-cli-docs
l2ysho Jan 19, 2026
dcc207c
refactor(actor): add validation, tests + cleanup
l2ysho Jan 21, 2026
08afa3a
test(actor): improve generate-types command tests
l2ysho Jan 23, 2026
3f47502
feat(actor): changes default output dir
l2ysho Jan 23, 2026
829ee17
Merge branch 'master' into 993-implement-a-command-for-generating-typ…
l2ysho Jan 26, 2026
da27683
Merge remote-tracking branch 'origin/master' into 993-implement-a-com…
l2ysho Jan 27, 2026
c1052c7
Merge remote-tracking branch 'origin/993-implement-a-command-for-gene…
l2ysho Jan 27, 2026
5cac9c7
Merge remote-tracking branch 'origin/master' into 993-implement-a-com…
l2ysho Jan 28, 2026
01d1ccc
chore: insert empty comment to template
l2ysho Jan 28, 2026
fc18a16
handle default as required
l2ysho Feb 11, 2026
e661b5b
lint fix
l2ysho Feb 11, 2026
09a898b
update docs and fix options
l2ysho Feb 11, 2026
52e6766
add dataset schema to generator
l2ysho Feb 13, 2026
e508ba7
add tests
l2ysho Feb 13, 2026
ad62972
rename command
l2ysho Feb 13, 2026
b3122a0
update docs
l2ysho Feb 13, 2026
a17f11f
yarn fix
l2ysho Feb 16, 2026
187a811
Merge remote-tracking branch 'origin/master' into 993-implement-a-com…
l2ysho Feb 16, 2026
1d180f0
fix docs
l2ysho Feb 16, 2026
6ff216c
fix tests
l2ysho Feb 16, 2026
9a6c93c
fix ambiguous file name
l2ysho Feb 16, 2026
6fd1486
Fix banner
l2ysho Feb 16, 2026
0be7af6
Fix(actor): Makes all schema properties optional
l2ysho Feb 16, 2026
7fb5f9a
Feat(actor): Adds schema optionality
l2ysho Feb 16, 2026
4d5dba9
Fix(actor): Makes properties with defaults optional
l2ysho Feb 16, 2026
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ _mytests
.idea
.zed
docs/changelog.md
.generated

# Yarn files
.yarn/install-state.gz
Expand Down
72 changes: 56 additions & 16 deletions docs/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -419,22 +419,26 @@ DESCRIPTION
Manages runtime data operations inside of a running Actor.

SUBCOMMANDS
actor set-value Sets or removes record into the
default key-value store associated with the Actor run.
actor push-data Saves data to Actor's run
default dataset.
actor get-value Gets a value from the default
key-value store associated with the Actor run.
actor get-public-url Get an HTTP URL that allows
public access to a key-value store item.
actor get-input Gets the Actor input value from
the default key-value store associated with the Actor
run.
actor charge Charge for a specific event in
the pay-per-event Actor run.
actor calculate-memory Calculates the Actor’s dynamic
memory usage based on a memory expression from
actor.json, input data, and run options.
actor set-value Sets or removes record
into the default key-value store associated with
the Actor run.
actor push-data Saves data to Actor's
run default dataset.
actor get-value Gets a value from the
default key-value store associated with the Actor
run.
actor get-public-url Get an HTTP URL that
allows public access to a key-value store item.
actor get-input Gets the Actor input
value from the default key-value store associated
with the Actor run.
actor charge Charge for a specific
event in the pay-per-event Actor run.
actor calculate-memory Calculates the Actor’s
dynamic memory usage based on a memory expression
from actor.json, input data, and run options.
actor generate-schema-types Generate TypeScript
types from Actor schemas.
```

##### `apify actor calculate-memory`
Expand Down Expand Up @@ -491,6 +495,42 @@ FLAGS
charging without actually charging
```

##### `apify actor generate-schema-types`

```sh
DESCRIPTION
Generate TypeScript types from Actor schemas.

Generates types from the input schema and, when no custom path is provided,
also from the dataset schema defined in '.actor/actor.json' under
"storages.dataset".

Reads the input schema from one of these locations (in priority order):
1. Object in '.actor/actor.json' under "input" key
2. JSON file path in '.actor/actor.json' "input" key
3. .actor/INPUT_SCHEMA.json
4. INPUT_SCHEMA.json

Optionally specify custom schema path to use.

USAGE
$ apify actor generate-schema-types [path]
[--all-optional] [-o <value>] [--strict]

ARGUMENTS
path Optional path to the input schema file. If not provided, searches
default locations.

FLAGS
--all-optional Mark all properties as optional in
generated types.
-o, --output=<value> Directory where the generated files
should be outputted. Defaults to src/.generated/actor/ to
stay within the typical tsconfig rootDir.
--strict Whether generated interfaces should be
strict (no index signature [key: string]: unknown).
```

##### `apify actor get-input`

```sh
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@
"istextorbinary": "~9.5.0",
"jju": "~1.4.0",
"js-levenshtein": "^1.1.6",
"json-schema-to-typescript": "^15.0.4",
"lodash.clonedeep": "^4.5.0",
"mime": "~4.1.0",
"open": "~11.0.0",
Expand Down
1 change: 1 addition & 0 deletions scripts/generate-cli-docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const categories: Record<string, CommandsInCategory[]> = {
{ command: Commands.actor },
{ command: Commands.actorCalculateMemory },
{ command: Commands.actorCharge },
{ command: Commands.actorGenerateSchemaTypes },
{ command: Commands.actorGetInput },
{ command: Commands.actorGetPublicUrl },
{ command: Commands.actorGetValue },
Expand Down
2 changes: 2 additions & 0 deletions src/commands/_register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { BuiltApifyCommand } from '../lib/command-framework/apify-command.j
import { ActorIndexCommand } from './actor/_index.js';
import { ActorCalculateMemoryCommand } from './actor/calculate-memory.js';
import { ActorChargeCommand } from './actor/charge.js';
import { ActorGenerateSchemaTypesCommand } from './actor/generate-schema-types.js';
import { ActorGetInputCommand } from './actor/get-input.js';
import { ActorGetPublicUrlCommand } from './actor/get-public-url.js';
import { ActorGetValueCommand } from './actor/get-value.js';
Expand Down Expand Up @@ -77,6 +78,7 @@ export const actorCommands = [
ActorGetInputCommand,
ActorChargeCommand,
ActorCalculateMemoryCommand,
ActorGenerateSchemaTypesCommand,

// top-level
HelpCommand,
Expand Down
2 changes: 2 additions & 0 deletions src/commands/actor/_index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ApifyCommand } from '../../lib/command-framework/apify-command.js';
import { ActorCalculateMemoryCommand } from './calculate-memory.js';
import { ActorChargeCommand } from './charge.js';
import { ActorGenerateSchemaTypesCommand } from './generate-schema-types.js';
import { ActorGetInputCommand } from './get-input.js';
import { ActorGetPublicUrlCommand } from './get-public-url.js';
import { ActorGetValueCommand } from './get-value.js';
Expand All @@ -21,6 +22,7 @@ export class ActorIndexCommand extends ApifyCommand<typeof ActorIndexCommand> {
ActorGetInputCommand,
ActorChargeCommand,
ActorCalculateMemoryCommand,
ActorGenerateSchemaTypesCommand,
];

async run() {
Expand Down
227 changes: 227 additions & 0 deletions src/commands/actor/generate-schema-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
import { mkdir, writeFile } from 'node:fs/promises';
import path from 'node:path';
import process from 'node:process';

import type { JSONSchema4 } from 'json-schema';
import { compile } from 'json-schema-to-typescript';
import deepClone from 'lodash.clonedeep';

import { ApifyCommand } from '../../lib/command-framework/apify-command.js';
import { Args } from '../../lib/command-framework/args.js';
import { Flags } from '../../lib/command-framework/flags.js';
import { LOCAL_CONFIG_PATH } from '../../lib/consts.js';
import { readAndValidateInputSchema, readDatasetSchema } from '../../lib/input_schema.js';
import { info, success } from '../../lib/outputs.js';

export const BANNER_COMMENT = `
/* eslint-disable */
/* biome-ignore-all lint */
/* biome-ignore-all format */
/* prettier-ignore-start */
/*
* This file was automatically generated by json-schema-to-typescript.
* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
* and run apify actor generate-schema-types to regenerate this file.
*/
`;

/**
* Transforms a JSON schema so that all properties without a `default` value are marked as required.
* Properties that have a `default` are left optional, since Apify fills them in at runtime.
* Recurses into nested object properties.
*/
export function makePropertiesRequired(schema: Record<string, unknown>): Record<string, unknown> {
const clone = deepClone(schema);

if (!clone.properties || typeof clone.properties !== 'object') {
return clone;
}

const properties = clone.properties as Record<string, Record<string, unknown>>;
const requiredSet = new Set<string>(Array.isArray(clone.required) ? (clone.required as string[]) : []);

for (const [key, prop] of Object.entries(properties)) {
if (prop.default === undefined) {
requiredSet.add(key);
} else {
requiredSet.delete(key);
}

if (prop.type === 'object' && prop.properties) {
properties[key] = makePropertiesRequired(prop) as Record<string, unknown>;
}
}

clone.required = Array.from(requiredSet);

return clone;
}

/**
* Deep clones a schema and recursively removes all `required` arrays,
* making every property optional at all nesting levels.
*/
export function clearAllRequired(schema: Record<string, unknown>): Record<string, unknown> {
const clone = deepClone(schema);

delete clone.required;

if (clone.properties && typeof clone.properties === 'object') {
const properties = clone.properties as Record<string, Record<string, unknown>>;
for (const [key, prop] of Object.entries(properties)) {
if (prop.type === 'object' && prop.properties) {
properties[key] = clearAllRequired(prop) as Record<string, unknown>;
}
}
}

return clone;
}

/**
* Extracts and prepares the `fields` sub-schema from a dataset schema for compilation.
* Returns `null` if the schema has no compilable fields (empty or missing).
*/
export function prepareDatasetSchemaForCompilation(schema: Record<string, unknown>): Record<string, unknown> | null {
const fields = schema.fields as Record<string, unknown> | undefined;

if (!fields || typeof fields !== 'object' || !fields.properties || typeof fields.properties !== 'object') {
return null;
}

const clone = deepClone(fields);

if (!clone.type) {
clone.type = 'object';
}

return clone;
}

export class ActorGenerateSchemaTypesCommand extends ApifyCommand<typeof ActorGenerateSchemaTypesCommand> {
static override name = 'generate-schema-types' as const;

static override hiddenAliases = ['generate-types'];

static override description = `Generate TypeScript types from Actor schemas.

Generates types from the input schema and, when no custom path is provided,
also from the dataset schema defined in '${LOCAL_CONFIG_PATH}' under "storages.dataset".

Reads the input schema from one of these locations (in priority order):
1. Object in '${LOCAL_CONFIG_PATH}' under "input" key
2. JSON file path in '${LOCAL_CONFIG_PATH}' "input" key
3. .actor/INPUT_SCHEMA.json
4. INPUT_SCHEMA.json

Optionally specify custom schema path to use.`;

static override flags = {
output: Flags.string({
char: 'o',
description:
'Directory where the generated files should be outputted. Defaults to src/.generated/actor/ to stay within the typical tsconfig rootDir.',
required: false,
default: 'src/.generated/actor/',
}),
strict: Flags.boolean({
description: 'Whether generated interfaces should be strict (no index signature [key: string]: unknown).',
required: false,
default: true,
}),
'all-optional': Flags.boolean({
description: 'Mark all properties as optional in generated types.',
required: false,
default: false,
}),
};

static override args = {
path: Args.string({
required: false,
description: 'Optional path to the input schema file. If not provided, searches default locations.',
}),
};

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

const { inputSchema } = await readAndValidateInputSchema({
forcePath: this.args.path,
cwd,
action: 'Generating types from',
});

const name = 'input';

const schemaToCompile = this.flags.allOptional
? clearAllRequired(inputSchema)
: makePropertiesRequired(inputSchema);

const compileOptions = {
bannerComment: BANNER_COMMENT,
maxItems: -1,
unknownAny: true,
format: true,
additionalProperties: !this.flags.strict,
$refOptions: { resolve: { external: false, file: false, http: false } },
};

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

const outputDir = path.resolve(cwd, 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) {
await this.generateDatasetTypes({ cwd, outputDir, compileOptions });
}
}

private async generateDatasetTypes({
cwd,
outputDir,
compileOptions,
}: {
cwd: string;
outputDir: string;
compileOptions: Record<string, unknown>;
}) {
const datasetResult = readDatasetSchema({ cwd });

if (!datasetResult) {
return;
}

const { datasetSchema, datasetSchemaPath } = datasetResult;

if (datasetSchemaPath) {
info({ message: `Generating types from dataset schema at ${datasetSchemaPath}` });
} else {
info({ message: `Generating types from dataset schema embedded in '${LOCAL_CONFIG_PATH}'` });
}

const prepared = prepareDatasetSchemaForCompilation(datasetSchema);

if (!prepared) {
info({ message: 'Dataset schema has no fields defined, skipping type generation.' });
return;
}

const datasetName = 'dataset';

const schemaToCompile = this.flags.allOptional ? clearAllRequired(prepared) : prepared;

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

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

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