From e8ca7c5f952676b653e97362fab43a57a6b79237 Mon Sep 17 00:00:00 2001 From: hugocasa Date: Mon, 8 Dec 2025 18:01:37 +0100 Subject: [PATCH] feat(cli): new command to generate RT namespace + on init (#7317) --- cli/bootstrap/common.ts | 12 ++++ cli/src/commands/init/init.ts | 12 ++++ .../commands/resource-type/resource-type.ts | 55 +++++++++++++++++-- cli/src/utils/resource_types.ts | 33 +++++++++++ cli/src/utils/utils.ts | 10 ++++ 5 files changed, 117 insertions(+), 5 deletions(-) create mode 100644 cli/src/utils/resource_types.ts diff --git a/cli/bootstrap/common.ts b/cli/bootstrap/common.ts index 87eaf159ec..a44639aa19 100644 --- a/cli/bootstrap/common.ts +++ b/cli/bootstrap/common.ts @@ -1,5 +1,17 @@ +import type { ScriptLang } from "../gen/types.gen.ts"; + export type EnumType = string[] | undefined; +export type Schema = { + $schema: string | undefined; + type: string; + "x-windmill-dyn-select-code"?: string; + "x-windmill-dyn-select-lang"?: ScriptLang; + properties: { [name: string]: SchemaProperty }; + order?: string[]; + required: string[]; +}; + export interface SchemaProperty { type: string | undefined; description?: string; diff --git a/cli/src/commands/init/init.ts b/cli/src/commands/init/init.ts index d4e13c65e6..f789f57516 100644 --- a/cli/src/commands/init/init.ts +++ b/cli/src/commands/init/init.ts @@ -4,6 +4,7 @@ import { readLockfile } from "../../utils/metadata.ts"; import { SCRIPT_GUIDANCE } from "../../guidance/script_guidance.ts"; import { FLOW_GUIDANCE } from "../../guidance/flow_guidance.ts"; import { getActiveWorkspaceOrFallback } from "../workspace/workspace.ts"; +import { generateRTNamespace } from "../resource-type/resource-type.ts"; export interface InitOptions { useDefault?: boolean; @@ -280,6 +281,17 @@ ${flowGuidanceContent} log.warn(`Could not create guidance files: ${error}`); } } + + // Generate resource type namespace + try { + await generateRTNamespace(opts as GlobalOptions); + } catch (error) { + log.warn( + `Could not pull resource types and generate TypeScript namespace: ${ + error instanceof Error ? error.message : error + }` + ); + } } const command = new Command() diff --git a/cli/src/commands/resource-type/resource-type.ts b/cli/src/commands/resource-type/resource-type.ts index 60dbd28f65..daeaa3386d 100644 --- a/cli/src/commands/resource-type/resource-type.ts +++ b/cli/src/commands/resource-type/resource-type.ts @@ -1,4 +1,9 @@ // deno-lint-ignore-file no-explicit-any + +import { writeFileSync } from "node:fs"; +import path from "node:path"; +import process from "node:process"; + import { GlobalOptions, isSuperset, @@ -10,6 +15,8 @@ import { resolveWorkspace } from "../../core/context.ts"; import { colors, Command, log, Table } from "../../../deps.ts"; import * as wmill from "../../../gen/services.gen.ts"; import { ResourceType } from "../../../gen/types.gen.ts"; +import { compileResourceTypeToTsType } from "../../utils/resource_types.ts"; +import { capitalize, toCamel } from "../../utils/utils.ts"; export interface ResourceTypeFile { schema?: any; @@ -88,7 +95,13 @@ async function list(opts: GlobalOptions & { schema?: boolean }) { .header(["Workspace", "Name", "Schema"]) .padding(2) .border(true) - .body(res.map((x) => [x.workspace_id ?? "Global", x.name, JSON.stringify(x.schema, null, 2)])) + .body( + res.map((x) => [ + x.workspace_id ?? "Global", + x.name, + JSON.stringify(x.schema, null, 2), + ]) + ) .render(); } else { new Table() @@ -100,11 +113,38 @@ async function list(opts: GlobalOptions & { schema?: boolean }) { } } +export async function generateRTNamespace(opts: GlobalOptions) { + const workspace = await resolveWorkspace(opts); + await requireLogin(opts); + const rts = await wmill.listResourceType({ + workspace: workspace.workspaceId, + }); + + let namespaceContent = "declare namespace RT {\n"; + namespaceContent += rts + .map((resourceType) => { + return ` type ${toCamel( + capitalize(resourceType.name) + )} = ${compileResourceTypeToTsType(resourceType.schema as any).replaceAll( + "\n", + "\n " + )}`; + }) + .join("\n\n"); + namespaceContent += "\n}"; + + writeFileSync(path.join(process.cwd(), "rt.d.ts"), namespaceContent); + + log.info( + colors.green( + "Created rt.d.ts with resource types namespace (RT) for TypeScript." + ) + ); +} + const command = new Command() .description("resource type related commands") - .action(() => - log.info("2 actions available, list and push.") - ) + .action(() => log.info("2 actions available, list and push.")) .command("list", "list all resource types") .option("--schema", "Show schema in the output") .action(list as any) @@ -113,6 +153,11 @@ const command = new Command() "push a local resource spec. This overrides any remote versions." ) .arguments(" ") - .action(push as any); + .action(push as any) + .command( + "generate-namespace", + "Create a TypeScript definition file with the RT namespace generated from the resource types" + ) + .action(generateRTNamespace as any); export default command; diff --git a/cli/src/utils/resource_types.ts b/cli/src/utils/resource_types.ts new file mode 100644 index 0000000000..741cb9fb71 --- /dev/null +++ b/cli/src/utils/resource_types.ts @@ -0,0 +1,33 @@ +import { Schema, SchemaProperty } from "../../bootstrap/common.ts"; + +export function compileResourceTypeToTsType(schema: Schema) { + function rec(x: { [name: string]: SchemaProperty }, root = false) { + let res = "{\n"; + const entries = Object.entries(x); + if (entries.length == 0) { + return "any"; + } + let i = 0; + for (let [name, prop] of entries) { + if (prop.type == "object") { + res += ` ${name}: ${rec(prop.properties ?? {})}`; + } else if (prop.type == "array") { + res += ` ${name}: ${prop?.items?.type ?? "any"}[]`; + } else { + let typ = prop?.type ?? "any"; + if (typ == "integer") { + typ = "number"; + } + res += ` ${name}: ${typ}`; + } + i++; + if (i < entries.length) { + res += ",\n"; + } + } + res += "\n}"; + return res; + } + + return rec(schema.properties, true); +} diff --git a/cli/src/utils/utils.ts b/cli/src/utils/utils.ts index ceee3d9d7a..9e6d44fd2a 100644 --- a/cli/src/utils/utils.ts +++ b/cli/src/utils/utils.ts @@ -275,3 +275,13 @@ export async function fetchRemoteVersion( } log.info(colors.gray("Remote version: " + version)); } + +export function toCamel(s: string) { + return s.replace(/([-_][a-z])/gi, ($1) => { + return $1.toUpperCase().replace("-", "").replace("_", ""); + }); +} + +export function capitalize(str: string): string { + return str.charAt(0).toUpperCase() + str.slice(1); +}