import { RawScript } from "../gen/types.gen"; const INLINE_SCRIPT_PREFIX = "inline_script"; /** * 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", ruby: "rb", rlang: "r", dbt: "dbt.yaml", // for related places search: ADD_NEW_LANG }; /** * 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"; } /** * Reverse mapping from file extensions to languages. * Used when deriving language from file extension. */ export const EXTENSION_TO_LANGUAGE: Record = { "py": "python3", "bun.ts": "bun", "deno.ts": "deno", "go": "go", "sh": "bash", "ps1": "powershell", "pg.sql": "postgresql", "my.sql": "mysql", "bq.sql": "bigquery", "odb.sql": "oracledb", "sf.sql": "snowflake", "ms.sql": "mssql", "gql": "graphql", "native.ts": "nativets", "frontend.js": "frontend", "php": "php", "rs": "rust", "cs": "csharp", "nu": "nu", "playbook.yml": "ansible", "java": "java", "duckdb.sql": "duckdb", "dbt.yaml": "dbt", "rb": "ruby", // Plain .ts defaults to bun (will be overridden by defaultTs setting) "ts": "bun", }; /** * Gets the language from a file extension. * * @param ext - File extension (e.g., "py", "ts", "bun.ts") * @param defaultTs - Default TypeScript runtime for plain .ts files * @returns The language, or undefined if not recognized */ export function getLanguageFromExtension( ext: string, defaultTs: "bun" | "deno" = "bun" ): SupportedLanguage | undefined { // Check for compound extensions first (e.g., "bun.ts", "pg.sql") const lang = EXTENSION_TO_LANGUAGE[ext]; if (lang) { // For plain .ts, return the default TypeScript runtime if (ext === "ts") { return defaultTs; } return lang; } return undefined; } /** * Sanitizes a summary string for use as a filesystem-safe name. * Removes or replaces characters that are invalid on common filesystems. */ const WINDOWS_RESERVED = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])$/; export function sanitizeForFilesystem( summary: string, options?: { preserveCase?: boolean }, ): string { let name = summary .replaceAll(" ", "_") // Remove characters invalid on Windows/Unix/Mac: / \ : * ? " < > | // Also remove control characters (0x00-0x1F) and DEL (0x7F) // deno-lint-ignore no-control-regex .replace(/[/\\:*?"<>|\x00-\x1f\x7f]/g, "") // Collapse consecutive underscores .replace(/_+/g, "_") // Trim leading/trailing dots and underscores (hidden files, Windows edge cases) .replace(/^[._]+|[._]+$/g, ""); if (!options?.preserveCase) { name = name.toLowerCase(); } // Prefix Windows reserved device names (CON, PRN, AUX, NUL, COM0-9, LPT0-9) return WINDOWS_RESERVED.test(name.toLowerCase()) ? `_${name}` : name; } export interface PathAssigner { assignPath(summary: string | undefined, language: SupportedLanguage): [string, string]; } export interface PathAssignerOptions { defaultTs: "bun" | "deno"; /** When true, skip the .inline_script. suffix in file names */ skipInlineScriptSuffix?: boolean; } /** * Creates a new path assigner for inline scripts. * * @param defaultTs - Default TypeScript runtime ("bun" or "deno") * @param options - Optional configuration (can pass options object instead of defaultTs) * @returns Path assigner function */ export function newPathAssigner(defaultTs: "bun" | "deno" | PathAssignerOptions, options?: { skipInlineScriptSuffix?: boolean }): PathAssigner { // Handle both old signature (defaultTs string) and new signature (options object) const resolvedOptions: PathAssignerOptions = typeof defaultTs === "object" ? defaultTs : { defaultTs, skipInlineScriptSuffix: options?.skipInlineScriptSuffix }; const { defaultTs: tsRuntime, skipInlineScriptSuffix } = resolvedOptions; let counter = 0; // Dedupe case-insensitively so case-only duplicates don't collide on // case-insensitive filesystems (Windows, default macOS). const seen_names = new Set(); function assignPath( summary: string | undefined, language: SupportedLanguage ): [string, string] { let name; name = summary ? sanitizeForFilesystem(summary) : ""; let original_name = name; if (name == "") { original_name = INLINE_SCRIPT_PREFIX; name = `${INLINE_SCRIPT_PREFIX}_0`; } while (seen_names.has(name.toLowerCase())) { counter++; name = `${original_name}_${counter}`; } seen_names.add(name.toLowerCase()); const ext = getLanguageExtension(language, tsRuntime); // When skipInlineScriptSuffix is true, don't add .inline_script. to the path const suffix = skipInlineScriptSuffix ? "." : ".inline_script."; return [`${name}${suffix}`, ext]; } return { assignPath }; } /** * Creates a new path assigner for raw app runnables. * Unlike newPathAssigner, this does NOT add ".inline_script." prefix since * everything in raw_app/backend/ is already known to be for inline scripts. * * @param defaultTs - Default TypeScript runtime ("bun" or "deno") * @returns Path assigner function */ export function newRawAppPathAssigner(defaultTs: "bun" | "deno"): PathAssigner { let counter = 0; // Dedupe case-insensitively so case-only duplicates don't collide on // case-insensitive filesystems (Windows, default macOS). const seen_names = new Set(); function assignPath( summary: string | undefined, language: SupportedLanguage ): [string, string] { let name; name = summary ? sanitizeForFilesystem(summary, { preserveCase: true }) : ""; let original_name = name; if (name == "") { original_name = "runnable"; name = `runnable_0`; } while (seen_names.has(name.toLowerCase())) { counter++; name = `${original_name}_${counter}`; } seen_names.add(name.toLowerCase()); const ext = getLanguageExtension(language, defaultTs); return [`${name}.`, ext]; } return { assignPath }; }