diff --git a/cli/windmill-utils-internal/.gitignore b/cli/windmill-utils-internal/.gitignore new file mode 100644 index 0000000000..692a4a7d22 --- /dev/null +++ b/cli/windmill-utils-internal/.gitignore @@ -0,0 +1,86 @@ +# Dependencies +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Build outputs +dist/ +build/ +*.tsbuildinfo + +# Generated client +src/gen/ + +# Environment variables +.env +.env.local +.env.*.local + +# IDE/Editor files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Logs +*.log +logs/ + +# Runtime data +pids/ +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage/ +*.lcov + +# nyc test coverage +.nyc_output + +# Dependency directories +jspm_packages/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# TypeScript cache +*.tsbuildinfo + +# Temporary folders +tmp/ +temp/ \ No newline at end of file diff --git a/cli/windmill-utils-internal/gen_wm_client-mac.sh b/cli/windmill-utils-internal/gen_wm_client-mac.sh new file mode 100755 index 0000000000..522efd3924 --- /dev/null +++ b/cli/windmill-utils-internal/gen_wm_client-mac.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +brew install gnu-sed + +script_dirpath="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +output_dirpath="${script_dirpath}/src/gen" + +rm -rf "${output_dirpath}" + +npx --yes @hey-api/openapi-ts@0.53.1 --input "${script_dirpath}/../../backend/windmill-api/openapi.yaml" --output "${output_dirpath}" --useOptions --client legacy/fetch --schemas false +cat < temp_file && mv temp_file "${output_dirpath}/core/OpenAPI.ts" +const getEnv = (key: string) => { + return process.env[key] +}; + +const baseUrl = getEnv("BASE_INTERNAL_URL") ?? getEnv("BASE_URL") ?? "http://localhost:8000"; +const baseUrlApi = (baseUrl ?? '') + "/api"; + +EOF +gsed -i 's/WITH_CREDENTIALS: false/WITH_CREDENTIALS: true/g' "${output_dirpath}/core/OpenAPI.ts" +gsed -i 's/TOKEN: undefined/TOKEN: getEnv("WM_TOKEN")/g' "${output_dirpath}/core/OpenAPI.ts" +gsed -i "s/BASE: '\/api'/BASE: baseUrlApi/g" "${output_dirpath}/core/OpenAPI.ts" + +find "${output_dirpath}" -name "*.ts" -exec gsed -i -E "s/(import.*from[[:space:]]*['\"][^'\"]+)(['\"])/\1.ts\2/g" {} \; + +find "${output_dirpath}" -name "*.ts" -exec gsed -i -E "s/(export.*from[[:space:]]*['\"][^'\"]+)(['\"])/\1.ts\2/g" {} \; \ No newline at end of file diff --git a/cli/windmill-utils-internal/gen_wm_client.sh b/cli/windmill-utils-internal/gen_wm_client.sh new file mode 100755 index 0000000000..684081b04c --- /dev/null +++ b/cli/windmill-utils-internal/gen_wm_client.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +script_dirpath="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +output_dirpath="${script_dirpath}/src/gen" + +rm -rf "${output_dirpath}" + +npx --yes @hey-api/openapi-ts@0.53.1 --input "${script_dirpath}/../../backend/windmill-api/openapi.yaml" --output "${output_dirpath}" --useOptions --client legacy/fetch --schemas false +cat < temp_file && mv temp_file "${output_dirpath}/core/OpenAPI.ts" +const getEnv = (key: string) => { + return process.env[key] +}; + +const baseUrl = getEnv("BASE_INTERNAL_URL") ?? getEnv("BASE_URL") ?? "http://localhost:8000"; +const baseUrlApi = (baseUrl ?? '') + "/api"; + +EOF +sed -i 's/WITH_CREDENTIALS: false/WITH_CREDENTIALS: true/g' "${output_dirpath}/core/OpenAPI.ts" +sed -i 's/TOKEN: undefined/TOKEN: getEnv("WM_TOKEN")/g' "${output_dirpath}/core/OpenAPI.ts" +sed -i "s/BASE: '\/api'/BASE: baseUrlApi/g" "${output_dirpath}/core/OpenAPI.ts" + +find "${output_dirpath}" -name "*.ts" -exec sed -i -E "s/(import.*from[[:space:]]*['\"][^'\"]+)(['\"])/\1.ts\2/g" {} \; + +find "${output_dirpath}" -name "*.ts" -exec sed -i -E "s/(export.*from[[:space:]]*['\"][^'\"]+)(['\"])/\1.ts\2/g" {} \; \ No newline at end of file diff --git a/cli/windmill-utils-internal/package-lock.json b/cli/windmill-utils-internal/package-lock.json new file mode 100644 index 0000000000..19a2425d76 --- /dev/null +++ b/cli/windmill-utils-internal/package-lock.json @@ -0,0 +1,48 @@ +{ + "name": "windmill-utils-internal", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "windmill-utils-internal", + "version": "1.0.0", + "license": "Apache 2.0", + "devDependencies": { + "@types/node": "^24.1.0", + "typescript": "^5.0.0" + } + }, + "node_modules/@types/node": { + "version": "24.1.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz", + "integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.8.0" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/cli/windmill-utils-internal/package.json b/cli/windmill-utils-internal/package.json new file mode 100644 index 0000000000..eaa9823d6b --- /dev/null +++ b/cli/windmill-utils-internal/package.json @@ -0,0 +1,24 @@ +{ + "name": "windmill-utils-internal", + "version": "1.0.0", + "description": "Internal utility functions for Windmill", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "./gen_wm_client.sh && tsc", + "build-mac": "./gen_wm_client-mac.sh && tsc", + "prepublishOnly": "npm run build" + }, + "keywords": [ + "windmill" + ], + "author": "Ruben Fiszel", + "license": "Apache 2.0", + "devDependencies": { + "@types/node": "^24.1.0", + "typescript": "^5.0.0" + }, + "files": [ + "dist/**/*" + ] +} \ No newline at end of file diff --git a/cli/windmill-utils-internal/src/constants.ts b/cli/windmill-utils-internal/src/constants.ts new file mode 100644 index 0000000000..87e896249a --- /dev/null +++ b/cli/windmill-utils-internal/src/constants.ts @@ -0,0 +1,47 @@ +// constants.ts +/** + * Cross-platform path separator constants + * Compatible with both Node.js and Deno environments + */ + +/** + * Detects if the current platform is Windows by checking various environment indicators. + * Uses multiple fallback methods to ensure compatibility across Node.js, Deno, and browser environments. + * + * @returns True if running on Windows, false otherwise + */ +const isWindows = (() => { + // Try Node.js process.platform first (most reliable) + if (typeof process !== "undefined" && process.platform) { + return process.platform === "win32"; + } + + // Try Deno specific detection via globalThis + if (typeof globalThis !== "undefined" && "Deno" in globalThis) { + return (globalThis as any).Deno?.build?.os === "windows"; + } + + // Try navigator.platform (browser/some Deno contexts) + // @ts-ignore + if (typeof navigator !== "undefined" && navigator.platform) { + // @ts-ignore + return navigator.platform.toLowerCase().includes("win"); + } + + // Fallback - assume Unix-like (safest default) + return false; + })(); + + /** + * Path separator constant - equivalent to Deno's SEPARATOR or Node's path.sep + * On Windows: "\" + * On Unix-like systems: "/" + */ + export const SEP = isWindows ? "\\" : "/"; + + /** + * Path delimiter constant for environment variables like PATH + * On Windows: ";" + * On Unix-like systems: ":" + */ + export const DELIMITER = isWindows ? ";" : ":"; \ No newline at end of file diff --git a/cli/windmill-utils-internal/src/index.ts b/cli/windmill-utils-internal/src/index.ts new file mode 100644 index 0000000000..e87d22d473 --- /dev/null +++ b/cli/windmill-utils-internal/src/index.ts @@ -0,0 +1,14 @@ +/** + * @fileoverview Main entry point for windmill-utils-internal package + * + * This module provides utilities for handling Windmill flows, scripts, and schemas: + * - Inline script extraction and replacement + * - Path utilities for different programming languages + * - Schema parsing and conversion utilities + * - Cross-platform path constants + */ + +export * from "./inline-scripts"; +export * from "./path-utils"; +export * from "./parse"; +export { SEP, DELIMITER } from "./constants"; \ No newline at end of file diff --git a/cli/windmill-utils-internal/src/inline-scripts/extractor.ts b/cli/windmill-utils-internal/src/inline-scripts/extractor.ts new file mode 100644 index 0000000000..322b550f8c --- /dev/null +++ b/cli/windmill-utils-internal/src/inline-scripts/extractor.ts @@ -0,0 +1,107 @@ +import { assignPath } from "../path-utils"; +import { SEP } from "../constants"; +import { FlowModule } from "../gen/types.gen"; + +/** + * Represents an inline script extracted from a flow module + */ +interface InlineScript { + /** File path where the script content should be written */ + path: string; + /** The actual script content */ + content: string; +} + +/** + * Extracts inline scripts from flow modules, converting them to separate files + * and replacing the original content with file references. + * + * @param modules - Array of flow modules to process + * @param mapping - Optional mapping of module IDs to custom file paths + * @param defaultTs - Default TypeScript runtime to use ("bun" or "deno") + * @returns Array of inline scripts with their paths and content + */ +export function extractInlineScripts( + modules: FlowModule[], + mapping: Record = {}, + defaultTs?: "bun" | "deno" +): InlineScript[] { + return modules.flatMap((m) => { + if (m.value.type == "rawscript") { + const [basePath, ext] = assignPath( + m.id, + m.value.language, + defaultTs + ); + const path = mapping[m.id] ?? basePath + ext; + const content = m.value.content; + const r = [{ path: path, content: content }]; + m.value.content = "!inline " + path.replace(SEP, "/"); + const lock = m.value.lock; + if (lock && lock != "") { + const lockPath = basePath + "lock"; + m.value.lock = "!inline " + lockPath.replace(SEP, "/"); + r.push({ path: lockPath, content: lock }); + } + return r; + } else if (m.value.type == "forloopflow") { + return extractInlineScripts(m.value.modules, mapping, defaultTs); + } else if (m.value.type == "branchall") { + return m.value.branches.flatMap((b) => + extractInlineScripts(b.modules, mapping, defaultTs) + ); + } else if (m.value.type == "whileloopflow") { + return extractInlineScripts(m.value.modules, mapping, defaultTs); + } else if (m.value.type == "branchone") { + return [ + ...m.value.branches.flatMap((b) => + extractInlineScripts(b.modules, mapping, defaultTs) + ), + ...extractInlineScripts(m.value.default, mapping, defaultTs), + ]; + } else { + return []; + } + }); +} + +/** + * Extracts the current mapping of module IDs to file paths from flow modules + * by analyzing existing inline script references. + * + * @param modules - Array of flow modules to analyze (can be undefined) + * @param mapping - Existing mapping to extend (defaults to empty object) + * @returns Record mapping module IDs to their corresponding file paths + */ +export function extractCurrentMapping( + modules: FlowModule[] | undefined, + mapping: Record = {} +): Record { + if (!modules || !Array.isArray(modules)) { + return mapping; + } + + modules.forEach((m) => { + if (!m?.value?.type) { + return; + } + + if (m.value.type === "rawscript") { + if (m.value.content && m.value.content.startsWith("!inline ")) { + mapping[m.id] = m.value.content.trim().split(" ")[1]; + } + } else if ( + m.value.type === "forloopflow" || + m.value.type === "whileloopflow" + ) { + extractCurrentMapping(m.value.modules, mapping); + } else if (m.value.type === "branchall") { + m.value.branches.forEach((b) => extractCurrentMapping(b.modules, mapping)); + } else if (m.value.type === "branchone") { + m.value.branches.forEach((b) => extractCurrentMapping(b.modules, mapping)); + extractCurrentMapping(m.value.default, mapping); + } + }); + + return mapping; +} \ No newline at end of file diff --git a/cli/windmill-utils-internal/src/inline-scripts/index.ts b/cli/windmill-utils-internal/src/inline-scripts/index.ts new file mode 100644 index 0000000000..eace8d3e4f --- /dev/null +++ b/cli/windmill-utils-internal/src/inline-scripts/index.ts @@ -0,0 +1,2 @@ +export * from "./replacer"; +export * from "./extractor"; \ No newline at end of file diff --git a/cli/windmill-utils-internal/src/inline-scripts/replacer.ts b/cli/windmill-utils-internal/src/inline-scripts/replacer.ts new file mode 100644 index 0000000000..134419ca64 --- /dev/null +++ b/cli/windmill-utils-internal/src/inline-scripts/replacer.ts @@ -0,0 +1,69 @@ +import { FlowModule } from "../gen/types.gen.ts"; + +/** + * Replaces inline script references with actual file content from the filesystem. + * This function recursively processes all flow modules and their nested structures. + * + * @param modules - Array of flow modules to process + * @param fileReader - Function to read file content (typically fs.readFile or similar) + * @param logger - Optional logger object with info and error methods + * @param localPath - Base path for resolving relative file paths + * @param removeLocks - Optional array of paths for which to remove lock files + * @returns Promise that resolves when all inline scripts have been replaced + */ +export async function replaceInlineScripts( + modules: FlowModule[], + fileReader: (...args: any[]) => any, + logger: { + info: (...args: any[]) => void, + error: (...args: any[]) => void, + } = { + info: () => {}, + error: () => {}, + }, + localPath: string, + removeLocks?: string[] + ): Promise { + await Promise.all(modules.map(async (module) => { + if (!module.value) { + throw new Error(`Module value is undefined for module ${module.id}`); + } + + if (module.value.type === "rawscript") { + if (module.value.content.startsWith("!inline")) { + const path = module.value.content.split(" ")[1]; + try { + module.value.content = await fileReader(path); + } catch { + logger.error(`Script file ${path} not found`); + } + const lock = module.value.lock; + if (removeLocks && removeLocks.includes(path)) { + module.value.lock = undefined; + } else if ( + lock && + typeof lock == "string" && + lock.trimStart().startsWith("!inline ") + ) { + const path = lock.split(" ")[1]; + try { + module.value.lock = await fileReader(path); + } catch { + logger.error(`Lock file ${path} not found`); + } + } + } + } else if (module.value.type === "forloopflow" || module.value.type === "whileloopflow") { + await replaceInlineScripts(module.value.modules, fileReader, logger, localPath, removeLocks); + } else if (module.value.type === "branchall") { + await Promise.all(module.value.branches.map(async (branch) => { + await replaceInlineScripts(branch.modules, fileReader, logger, localPath, removeLocks); + })); + } else if (module.value.type === "branchone") { + await Promise.all(module.value.branches.map(async (branch) => { + await replaceInlineScripts(branch.modules, fileReader, logger, localPath, removeLocks); + })); + await replaceInlineScripts(module.value.default, fileReader, logger, localPath, removeLocks); + } + })); + } \ No newline at end of file diff --git a/cli/windmill-utils-internal/src/parse/index.ts b/cli/windmill-utils-internal/src/parse/index.ts new file mode 100644 index 0000000000..fc26ce611a --- /dev/null +++ b/cli/windmill-utils-internal/src/parse/index.ts @@ -0,0 +1 @@ +export * from "./parse-schema"; \ No newline at end of file diff --git a/cli/windmill-utils-internal/src/parse/parse-schema.ts b/cli/windmill-utils-internal/src/parse/parse-schema.ts new file mode 100644 index 0000000000..c9eb5005da --- /dev/null +++ b/cli/windmill-utils-internal/src/parse/parse-schema.ts @@ -0,0 +1,243 @@ +/** + * Type alias for enum values - can be an array of strings or undefined + */ +export type EnumType = string[] | undefined + +/** + * Represents a property in a JSON schema with various validation and display options + */ +export interface SchemaProperty { + type: string | undefined + description?: string + pattern?: string + default?: any + enum?: EnumType + contentEncoding?: 'base64' | 'binary' + format?: string + items?: { + type?: 'string' | 'number' | 'bytes' | 'object' | 'resource' + contentEncoding?: 'base64' + enum?: string[] + resourceType?: string + properties?: { [name: string]: SchemaProperty } + } + min?: number + max?: number + currency?: string + currencyLocale?: string + multiselect?: boolean + customErrorMessage?: string + properties?: { [name: string]: SchemaProperty } + required?: string[] + showExpr?: string + password?: boolean + order?: string[] + nullable?: boolean + dateFormat?: string + title?: string + placeholder?: string + oneOf?: SchemaProperty[] + originalType?: string +} + +/** + * Converts argument signature types to JSON schema properties. + * This function handles various Windmill-specific types and converts them + * to standard JSON schema format while preserving existing property metadata. + * + * @param t - The argument signature type definition (can be string or complex object types) + * @param oldS - Existing schema property to update with new type information + */ +export function argSigToJsonSchemaType( + t: + | string + | { resource: string | null } + | { + list: + | (string | { name?: string; props?: { key: string; typ: any }[] }) + | { str: any } + | { object: { name?: string; props?: { key: string; typ: any }[] } } + | null + } + | { dynselect: string } + | { str: string[] | null } + | { object: { name?: string; props?: { key: string; typ: any }[] } } + | { + oneof: { + label: string + properties: { key: string; typ: any }[] + }[] + }, + oldS: SchemaProperty +): void { + const newS: SchemaProperty = { type: '' } + if (t === 'int') { + newS.type = 'integer' + } else if (t === 'float') { + newS.type = 'number' + } else if (t === 'bool') { + newS.type = 'boolean' + } else if (t === 'email') { + newS.type = 'string' + newS.format = 'email' + } else if (t === 'sql') { + newS.type = 'string' + newS.format = 'sql' + } else if (t === 'yaml') { + newS.type = 'string' + newS.format = 'yaml' + } else if (t === 'bytes') { + newS.type = 'string' + newS.contentEncoding = 'base64' + newS.originalType = 'bytes' + } else if (t === 'datetime') { + newS.type = 'string' + newS.format = 'date-time' + } else if (typeof t !== 'string' && 'oneof' in t) { + newS.type = 'object' + if (t.oneof) { + newS.oneOf = t.oneof.map((obj) => { + const oldObjS = oldS.oneOf?.find((o) => o?.title === obj.label) ?? undefined + const properties: Record = {} + for (const prop of obj.properties) { + if (oldObjS?.properties && prop.key in oldObjS?.properties) { + properties[prop.key] = oldObjS?.properties[prop.key] + } else { + properties[prop.key] = { description: '', type: '' } + } + argSigToJsonSchemaType(prop.typ, properties[prop.key]) + } + return { + type: 'object', + title: obj.label, + properties, + order: oldObjS?.order ?? undefined + } + }) + } + } else if (typeof t !== 'string' && `object` in t) { + newS.type = 'object' + if (t.object.name) { + newS.format = `resource-${t.object.name}` + } + if (t.object.props) { + const properties: Record = {} + for (const prop of t.object.props) { + if (oldS.properties && prop.key in oldS.properties) { + properties[prop.key] = oldS.properties[prop.key] + } else { + properties[prop.key] = { description: '', type: '' } + } + argSigToJsonSchemaType(prop.typ, properties[prop.key]) + } + newS.properties = properties + } + } else if (typeof t !== 'string' && `str` in t) { + newS.type = 'string' + if (t.str) { + newS.originalType = 'enum' + newS.enum = t.str + } else if (oldS.originalType == 'string' && oldS.enum) { + newS.originalType = 'string' + newS.enum = oldS.enum + } else { + newS.originalType = 'string' + newS.enum = undefined + } + } else if (typeof t !== 'string' && `resource` in t) { + newS.type = 'object' + newS.format = `resource-${t.resource}` + } else if (typeof t !== 'string' && `dynselect` in t) { + newS.type = 'object' + newS.format = `dynselect-${t.dynselect}` + } else if (typeof t !== 'string' && `list` in t) { + newS.type = 'array' + if (t.list === 'int' || t.list === 'float') { + newS.items = { type: 'number' } + newS.originalType = 'number[]' + } else if (t.list === 'bytes') { + newS.items = { type: 'string', contentEncoding: 'base64' } + newS.originalType = 'bytes[]' + } else if (t.list && typeof t.list == 'object' && 'str' in t.list && t.list.str) { + newS.items = { type: 'string', enum: t.list.str } + newS.originalType = 'enum[]' + } else if (t.list == 'string' || (t.list && typeof t.list == 'object' && 'str' in t.list)) { + newS.items = { type: 'string', enum: oldS.items?.enum } + newS.originalType = 'string[]' + } else if (t.list && typeof t.list == 'object' && 'resource' in t.list && t.list.resource) { + newS.items = { + type: 'resource', + resourceType: t.list.resource as string + } + newS.originalType = 'resource[]' + } else if (t.list && typeof t.list == 'object' && 'object' in t.list && t.list.object) { + if (t.list.object.name) { + newS.format = `resource-${t.list.object.name}` + } + if (t.list.object.props && t.list.object.props.length > 0) { + const properties: Record = {} + for (const prop of t.list.object.props) { + properties[prop.key] = { description: '', type: '' } + argSigToJsonSchemaType(prop.typ, properties[prop.key]) + } + newS.items = { type: 'object', properties: properties } + } else { + newS.items = { type: 'object' } + } + newS.originalType = 'record[]' + } else { + newS.items = { type: 'object' } + newS.originalType = 'object[]' + } + } else { + newS.type = 'object' + } + + const preservedFields = [ + 'description', + 'pattern', + 'min', + 'max', + 'currency', + 'currencyLocale', + 'multiselect', + 'customErrorMessage', + 'required', + 'showExpr', + 'password', + 'order', + 'dateFormat', + 'title', + 'placeholder' + ] + + preservedFields.forEach((field) => { + // @ts-ignore + if (oldS[field] !== undefined) { + // @ts-ignore + newS[field] = oldS[field] + } + }) + + if (oldS.type != newS.type) { + for (const prop of Object.getOwnPropertyNames(newS)) { + if (prop != 'description') { + // @ts-ignore + delete oldS[prop] + } + } + } else if ((oldS.format == 'date' || oldS.format === 'date-time') && newS.format == 'string') { + newS.format = oldS.format + } else if (newS.format == 'date-time' && oldS.format == 'date') { + newS.format = 'date' + } else if (oldS.items?.type != newS.items?.type) { + delete oldS.items + } + + if (oldS.format && !newS.format) { + oldS.format = undefined + } + + Object.assign(oldS, newS) + +} \ No newline at end of file diff --git a/cli/windmill-utils-internal/src/path-utils/index.ts b/cli/windmill-utils-internal/src/path-utils/index.ts new file mode 100644 index 0000000000..ef23185664 --- /dev/null +++ b/cli/windmill-utils-internal/src/path-utils/index.ts @@ -0,0 +1 @@ +export * from "./path-assigner"; \ No newline at end of file diff --git a/cli/windmill-utils-internal/src/path-utils/path-assigner.ts b/cli/windmill-utils-internal/src/path-utils/path-assigner.ts new file mode 100644 index 0000000000..f6153c9f21 --- /dev/null +++ b/cli/windmill-utils-internal/src/path-utils/path-assigner.ts @@ -0,0 +1,71 @@ +import { RawScript } from "../gen/types.gen"; + +/** + * Union type of all supported programming languages in Windmill + */ +export type SupportedLanguage = RawScript["language"] | "frontend" | "bunnative" | "oracledb" | "rust" | "csharp" | "nu" | "ansible" | "java" | "duckdb"; + +/** + * Mapping of supported languages to their file extensions + */ +export const LANGUAGE_EXTENSIONS: Record = { + python3: "py", + bun: "bun.ts", + deno: "deno.ts", + go: "go", + bash: "sh", + powershell: "ps1", + postgresql: "pg.sql", + mysql: "my.sql", + bigquery: "bq.sql", + oracledb: "odb.sql", + snowflake: "sf.sql", + mssql: "ms.sql", + graphql: "gql", + nativets: "native.ts", + frontend: "frontend.js", + php: "php", + rust: "rs", + csharp: "cs", + nu: "nu", + ansible: "playbook.yml", + java: "java", + duckdb: "duckdb.sql", + bunnative: "ts" +}; + +/** + * Gets the appropriate file extension for a given programming language. + * Handles special cases for TypeScript variants based on the default runtime. + * + * @param language - The programming language to get extension for + * @param defaultTs - Default TypeScript runtime ("bun" or "deno") + * @returns File extension string (without the dot) + */ +export function getLanguageExtension( + language: SupportedLanguage, + defaultTs: "bun" | "deno" = "bun" +): string { + if (language === defaultTs || language === "bunnative") { + return "ts"; + } + return LANGUAGE_EXTENSIONS[language] || "no_ext"; +} + +/** + * Assigns a file path for an inline script based on its ID and language. + * Returns both the base path and extension as separate components. + * + * @param id - Unique identifier for the script + * @param language - Programming language of the script + * @param defaultTs - Default TypeScript runtime ("bun" or "deno") + * @returns Tuple containing [basePath, extension] + */ +export function assignPath( + id: string, + language: SupportedLanguage, + defaultTs: "bun" | "deno" = "bun" +): [string, string] { + const ext = getLanguageExtension(language, defaultTs); + return [`${id}.inline_script.`, ext]; +} \ No newline at end of file diff --git a/cli/windmill-utils-internal/tsconfig.json b/cli/windmill-utils-internal/tsconfig.json new file mode 100644 index 0000000000..395ae592af --- /dev/null +++ b/cli/windmill-utils-internal/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "declaration": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "moduleResolution": "node", + "baseUrl": "./", + "esModuleInterop": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "allowImportingTsExtensions": true + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "dist" + ] +} \ No newline at end of file