feat(cli): new command to generate RT namespace + on init (#7317)

This commit is contained in:
hugocasa
2025-12-08 18:01:37 +01:00
committed by GitHub
parent 3699ce7a8f
commit e8ca7c5f95
5 changed files with 117 additions and 5 deletions

View File

@@ -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;

View File

@@ -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()

View File

@@ -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("<file_path:string> <name:string>")
.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;

View File

@@ -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);
}

View File

@@ -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);
}