diff --git a/backend/.sqlx/query-2c256552a430877c42224055aeb81df33d88ff295483cb28369eda42ce58afec.json b/backend/.sqlx/query-2c256552a430877c42224055aeb81df33d88ff295483cb28369eda42ce58afec.json new file mode 100644 index 0000000000000..a7b1b3793be1b --- /dev/null +++ b/backend/.sqlx/query-2c256552a430877c42224055aeb81df33d88ff295483cb28369eda42ce58afec.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO script (\n workspace_id, hash, path, parent_hashes, summary, description, content,\n created_by, created_at, archived, schema, deleted, is_template,\n extra_perms, lock, lock_error_logs, language, kind, tag, draft_only,\n envs, concurrent_limit, concurrency_time_window_s, cache_ttl,\n dedicated_worker, ws_error_handler_muted, priority, timeout,\n delete_after_use, restart_unless_cancelled, concurrency_key,\n visible_to_runner_only, auto_kind, codebase, has_preprocessor,\n on_behalf_of_email, assets, modules\n )\n SELECT\n $1, hash, path, parent_hashes, summary, description, content,\n created_by, created_at, archived, schema, deleted, is_template,\n extra_perms, lock, lock_error_logs, language, kind, tag, draft_only,\n envs, concurrent_limit, concurrency_time_window_s, cache_ttl,\n dedicated_worker, ws_error_handler_muted, priority, timeout,\n delete_after_use, restart_unless_cancelled, concurrency_key,\n visible_to_runner_only, auto_kind, codebase, has_preprocessor,\n on_behalf_of_email, assets, modules\n FROM script\n WHERE workspace_id = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Text" + ] + }, + "nullable": [] + }, + "hash": "2c256552a430877c42224055aeb81df33d88ff295483cb28369eda42ce58afec" +} diff --git a/backend/.sqlx/query-36b556a1c8630547cb7f5f88a1a0f02effb9e62409cd61fa4de60d11d50ee206.json b/backend/.sqlx/query-36b556a1c8630547cb7f5f88a1a0f02effb9e62409cd61fa4de60d11d50ee206.json new file mode 100644 index 0000000000000..66ce2799fd921 --- /dev/null +++ b/backend/.sqlx/query-36b556a1c8630547cb7f5f88a1a0f02effb9e62409cd61fa4de60d11d50ee206.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT j.id\n FROM v2_job_queue q JOIN v2_job j USING (id) LEFT JOIN v2_job_runtime r USING (id) LEFT JOIN v2_job_status s USING (id)\n WHERE r.ping < now() - ($1 || ' seconds')::interval\n AND q.running = true AND j.kind NOT IN ('flow', 'flowpreview', 'flownode', 'singlestepflow') AND j.same_worker = false AND q.suspend_until IS NULL", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false + ] + }, + "hash": "36b556a1c8630547cb7f5f88a1a0f02effb9e62409cd61fa4de60d11d50ee206" +} diff --git a/backend/.sqlx/query-49b18e987e2dfa3c7ab915757ff3b9c0e6e371136b565f9b0f5a3393ef8d8d57.json b/backend/.sqlx/query-49b18e987e2dfa3c7ab915757ff3b9c0e6e371136b565f9b0f5a3393ef8d8d57.json new file mode 100644 index 0000000000000..8d86ff3db6784 --- /dev/null +++ b/backend/.sqlx/query-49b18e987e2dfa3c7ab915757ff3b9c0e6e371136b565f9b0f5a3393ef8d8d57.json @@ -0,0 +1,19 @@ +{ + "db_name": "PostgreSQL", + "query": "WITH update_lock AS (\n UPDATE script SET lock = $1, modules = COALESCE($6, modules) WHERE hash = $2 AND workspace_id = $3\n )\n INSERT INTO lock_hash (workspace_id, path, lockfile_hash)\n VALUES ($3, $4, $5)\n ON CONFLICT (workspace_id, path) DO UPDATE SET lockfile_hash = $5", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Int8", + "Text", + "Varchar", + "Int8", + "Jsonb" + ] + }, + "nullable": [] + }, + "hash": "49b18e987e2dfa3c7ab915757ff3b9c0e6e371136b565f9b0f5a3393ef8d8d57" +} diff --git a/backend/.sqlx/query-4d983f1e3e63a1a70edf5d867d9f23f2069a7a4ba1dcc1331ecccdf1c6a95cb8.json b/backend/.sqlx/query-4d983f1e3e63a1a70edf5d867d9f23f2069a7a4ba1dcc1331ecccdf1c6a95cb8.json new file mode 100644 index 0000000000000..33ba98705df2f --- /dev/null +++ b/backend/.sqlx/query-4d983f1e3e63a1a70edf5d867d9f23f2069a7a4ba1dcc1331ecccdf1c6a95cb8.json @@ -0,0 +1,97 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO script (workspace_id, hash, path, parent_hashes, summary, description, content, created_by, schema, is_template, extra_perms, lock, language, kind, tag, draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, dedicated_worker, ws_error_handler_muted, priority, restart_unless_cancelled, delete_after_use, timeout, concurrency_key, visible_to_runner_only, auto_kind, codebase, has_preprocessor, on_behalf_of_email, schema_validation, assets, debounce_key, debounce_delay_s, cache_ignore_s3_path, runnable_settings_handle, modules) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38, $39)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Int8", + "Varchar", + "Int8Array", + "Text", + "Text", + "Text", + "Varchar", + "Text", + "Bool", + "Jsonb", + "Text", + { + "Custom": { + "name": "script_lang", + "kind": { + "Enum": [ + "python3", + "deno", + "go", + "bash", + "postgresql", + "nativets", + "bun", + "mysql", + "bigquery", + "snowflake", + "graphql", + "powershell", + "mssql", + "php", + "bunnative", + "rust", + "ansible", + "csharp", + "oracledb", + "nu", + "java", + "duckdb", + "ruby" + ] + } + } + }, + { + "Custom": { + "name": "script_kind", + "kind": { + "Enum": [ + "script", + "trigger", + "failure", + "command", + "approval", + "preprocessor" + ] + } + } + }, + "Varchar", + "Bool", + "VarcharArray", + "Int4", + "Int4", + "Int4", + "Bool", + "Bool", + "Int2", + "Bool", + "Bool", + "Int4", + "Varchar", + "Bool", + "Varchar", + "Varchar", + "Bool", + "Text", + "Bool", + "Jsonb", + "Varchar", + "Int4", + "Bool", + "Int8", + "Jsonb" + ] + }, + "nullable": [] + }, + "hash": "4d983f1e3e63a1a70edf5d867d9f23f2069a7a4ba1dcc1331ecccdf1c6a95cb8" +} diff --git a/backend/.sqlx/query-4fdb9dc38c0a8e882a1dee39e42664b4c85fd43edbd7ebd8fd5ad380e5a8e3cc.json b/backend/.sqlx/query-4fdb9dc38c0a8e882a1dee39e42664b4c85fd43edbd7ebd8fd5ad380e5a8e3cc.json new file mode 100644 index 0000000000000..06fc58d7de785 --- /dev/null +++ b/backend/.sqlx/query-4fdb9dc38c0a8e882a1dee39e42664b4c85fd43edbd7ebd8fd5ad380e5a8e3cc.json @@ -0,0 +1,26 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT worker, array_agg(v2_job_queue.id) as ids FROM v2_job_queue LEFT JOIN v2_job ON v2_job_queue.id = v2_job.id LEFT JOIN v2_job_runtime ON v2_job_queue.id = v2_job_runtime.id WHERE v2_job_queue.created_at < now() - ('60 seconds')::interval\n AND running = true AND (ping IS NULL OR ping < now() - ('60 seconds')::interval) AND same_worker = true AND worker IS NOT NULL AND v2_job_queue.suspend_until IS NULL GROUP BY worker", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "worker", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "ids", + "type_info": "UuidArray" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + true, + null + ] + }, + "hash": "4fdb9dc38c0a8e882a1dee39e42664b4c85fd43edbd7ebd8fd5ad380e5a8e3cc" +} diff --git a/backend/.sqlx/query-51f09f073842a6990535b887d8267fab305c21e4d7703bedbadf405b5c2d7582.json b/backend/.sqlx/query-51f09f073842a6990535b887d8267fab305c21e4d7703bedbadf405b5c2d7582.json new file mode 100644 index 0000000000000..89bb7c33cabbf --- /dev/null +++ b/backend/.sqlx/query-51f09f073842a6990535b887d8267fab305c21e4d7703bedbadf405b5c2d7582.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO script\n (workspace_id, hash, path, parent_hashes, summary, description, content, created_by, schema, is_template, extra_perms, lock, language, kind, tag, draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, cache_ignore_s3_path, dedicated_worker, ws_error_handler_muted, priority, restart_unless_cancelled, delete_after_use, timeout, concurrency_key, visible_to_runner_only, auto_kind, codebase, has_preprocessor, on_behalf_of_email, schema_validation, assets, debounce_key, debounce_delay_s, runnable_settings_handle, modules)\n\n SELECT workspace_id, $1, path, array_prepend($2::bigint, COALESCE(parent_hashes, '{}'::bigint[])), summary, description, content, created_by, schema, is_template, extra_perms, NULL, language, kind, tag, draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, cache_ignore_s3_path, dedicated_worker, ws_error_handler_muted, priority, restart_unless_cancelled, delete_after_use, timeout, concurrency_key, visible_to_runner_only, auto_kind, codebase, has_preprocessor, on_behalf_of_email, schema_validation, assets, debounce_key, debounce_delay_s, runnable_settings_handle, modules\n\n FROM script WHERE hash = $2 AND workspace_id = $3;\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Int8", + "Text" + ] + }, + "nullable": [] + }, + "hash": "51f09f073842a6990535b887d8267fab305c21e4d7703bedbadf405b5c2d7582" +} diff --git a/backend/.sqlx/query-77e15dee033788972b6e09ea59fb3771928d04be926821e8b764e8af9cff03bb.json b/backend/.sqlx/query-77e15dee033788972b6e09ea59fb3771928d04be926821e8b764e8af9cff03bb.json new file mode 100644 index 0000000000000..db803a8b0cc73 --- /dev/null +++ b/backend/.sqlx/query-77e15dee033788972b6e09ea59fb3771928d04be926821e8b764e8af9cff03bb.json @@ -0,0 +1,41 @@ +{ + "db_name": "PostgreSQL", + "query": "WITH to_update AS (\n SELECT q.id, q.workspace_id, r.ping, COALESCE(zjc.counter, 0) as counter\n FROM v2_job_queue q\n JOIN v2_job j ON j.id = q.id\n JOIN v2_job_runtime r ON r.id = j.id\n LEFT JOIN zombie_job_counter zjc ON zjc.job_id = q.id\n WHERE ping < now() - ($1 || ' seconds')::interval\n AND running = true\n AND kind NOT IN ('flow', 'flowpreview', 'flownode', 'singlestepflow')\n AND same_worker = false\n AND q.suspend_until IS NULL\n AND (zjc.counter IS NULL OR zjc.counter <= $2)\n FOR UPDATE of q SKIP LOCKED\n ),\n zombie_jobs AS (\n UPDATE v2_job_queue q\n SET running = false, started_at = null\n FROM to_update tu\n WHERE q.id = tu.id AND (tu.counter IS NULL OR tu.counter < $2)\n RETURNING q.id, q.workspace_id, ping, tu.counter\n ),\n update_ping AS (\n UPDATE v2_job_runtime r\n SET ping = null\n FROM zombie_jobs zj\n WHERE r.id = zj.id\n ),\n increment_counter AS (\n INSERT INTO zombie_job_counter (job_id, counter)\n SELECT id, 1 FROM to_update WHERE counter < $2\n ON CONFLICT (job_id) DO UPDATE\n SET counter = zombie_job_counter.counter + 1\n ),\n update_concurrency AS (\n UPDATE concurrency_counter cc\n SET job_uuids = job_uuids - zj.id::text\n FROM zombie_jobs zj\n INNER JOIN concurrency_key ck ON ck.job_id = zj.id\n WHERE cc.concurrency_id = ck.key\n )\n SELECT id AS \"id!\", workspace_id AS \"workspace_id!\", ping, counter + 1 AS counter FROM to_update", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id!", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "workspace_id!", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "ping", + "type_info": "Timestamptz" + }, + { + "ordinal": 3, + "name": "counter", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Text", + "Int4" + ] + }, + "nullable": [ + false, + false, + true, + null + ] + }, + "hash": "77e15dee033788972b6e09ea59fb3771928d04be926821e8b764e8af9cff03bb" +} diff --git a/backend/.sqlx/query-b44afcf1b9c047ac525638f0952c2cb01d65b1b46693331ac157dfca0dab6824.json b/backend/.sqlx/query-b44afcf1b9c047ac525638f0952c2cb01d65b1b46693331ac157dfca0dab6824.json new file mode 100644 index 0000000000000..8dd4e1ea28a69 --- /dev/null +++ b/backend/.sqlx/query-b44afcf1b9c047ac525638f0952c2cb01d65b1b46693331ac157dfca0dab6824.json @@ -0,0 +1,101 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT content AS \"content!: String\",\n lock AS \"lock: String\", language AS \"language: Option\", envs AS \"envs: Vec\", schema AS \"schema: String\", schema_validation AS \"schema_validation: bool\", codebase LIKE '%.tar' as use_tar, codebase LIKE '%.esm%' as is_esm, modules AS \"modules: serde_json::Value\" FROM script WHERE hash = $1 LIMIT 1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "content!: String", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "lock: String", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "language: Option", + "type_info": { + "Custom": { + "name": "script_lang", + "kind": { + "Enum": [ + "python3", + "deno", + "go", + "bash", + "postgresql", + "nativets", + "bun", + "mysql", + "bigquery", + "snowflake", + "graphql", + "powershell", + "mssql", + "php", + "bunnative", + "rust", + "ansible", + "csharp", + "oracledb", + "nu", + "java", + "duckdb", + "ruby" + ] + } + } + } + }, + { + "ordinal": 3, + "name": "envs: Vec", + "type_info": "VarcharArray" + }, + { + "ordinal": 4, + "name": "schema: String", + "type_info": "Json" + }, + { + "ordinal": 5, + "name": "schema_validation: bool", + "type_info": "Bool" + }, + { + "ordinal": 6, + "name": "use_tar", + "type_info": "Bool" + }, + { + "ordinal": 7, + "name": "is_esm", + "type_info": "Bool" + }, + { + "ordinal": 8, + "name": "modules: serde_json::Value", + "type_info": "Jsonb" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + true, + false, + true, + true, + false, + null, + null, + true + ] + }, + "hash": "b44afcf1b9c047ac525638f0952c2cb01d65b1b46693331ac157dfca0dab6824" +} diff --git a/backend/migrations/20260307000000_add_script_modules.up.sql b/backend/migrations/20260307000000_add_script_modules.up.sql new file mode 100644 index 0000000000000..c8bd0ea3bba53 --- /dev/null +++ b/backend/migrations/20260307000000_add_script_modules.up.sql @@ -0,0 +1 @@ +ALTER TABLE script ADD COLUMN IF NOT EXISTS modules JSONB; diff --git a/backend/migrations/20260313000000_script_auto_kind.up.sql b/backend/migrations/20260313000000_script_auto_kind.up.sql new file mode 100644 index 0000000000000..5f962f00d00b2 --- /dev/null +++ b/backend/migrations/20260313000000_script_auto_kind.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE script ADD COLUMN auto_kind VARCHAR(20); +UPDATE script SET auto_kind = 'lib' WHERE no_main_func = true; +ALTER TABLE script DROP COLUMN no_main_func; diff --git a/backend/parsers/windmill-parser-bash/src/lib.rs b/backend/parsers/windmill-parser-bash/src/lib.rs index 023e41a902d4d..3629ee1e79dde 100644 --- a/backend/parsers/windmill-parser-bash/src/lib.rs +++ b/backend/parsers/windmill-parser-bash/src/lib.rs @@ -20,7 +20,7 @@ pub fn parse_bash_sig(code: &str) -> anyhow::Result { star_args: false, star_kwargs: false, args, - no_main_func: None, + auto_kind: None, has_preprocessor: None, }) } else { @@ -36,7 +36,7 @@ pub fn parse_powershell_sig(code: &str) -> anyhow::Result { star_args: false, star_kwargs: false, args, - no_main_func: None, + auto_kind: None, has_preprocessor: None, }) } else { @@ -721,7 +721,7 @@ non_required="${5:-}" oidx: None } ], - no_main_func: None, + auto_kind: None, has_preprocessor: None } ); @@ -819,7 +819,7 @@ non_required="${5:-}" oidx: None } ], - no_main_func: None, + auto_kind: None, has_preprocessor: None } ); @@ -1391,7 +1391,7 @@ param( oidx: None } ], - no_main_func: None, + auto_kind: None, has_preprocessor: None } ); diff --git a/backend/parsers/windmill-parser-csharp/src/lib.rs b/backend/parsers/windmill-parser-csharp/src/lib.rs index 53f6eb89dddbf..26c2607563b94 100644 --- a/backend/parsers/windmill-parser-csharp/src/lib.rs +++ b/backend/parsers/windmill-parser-csharp/src/lib.rs @@ -37,7 +37,11 @@ pub fn parse_csharp_sig_meta(code: &str) -> anyhow::Result { // Traverse the AST to find the Main method signature let main_sig = find_main_signature(root_node, code); - let no_main_func = Some(main_sig.is_none()); + let auto_kind = if main_sig.is_none() { + Some("lib".to_string()) + } else { + None + }; let mut is_async = false; let mut is_public = false; let mut returns_void = false; @@ -84,7 +88,7 @@ pub fn parse_csharp_sig_meta(code: &str) -> anyhow::Result { star_kwargs: false, args, has_preprocessor: None, - no_main_func, + auto_kind, }; Ok(CsharpMainSigMeta { is_async, returns_void, class_name, main_sig, is_public }) diff --git a/backend/parsers/windmill-parser-go/src/lib.rs b/backend/parsers/windmill-parser-go/src/lib.rs index 5df0d4e58eeca..19f1f52b38745 100644 --- a/backend/parsers/windmill-parser-go/src/lib.rs +++ b/backend/parsers/windmill-parser-go/src/lib.rs @@ -41,7 +41,7 @@ pub fn parse_go_sig(code: &str) -> anyhow::Result { star_args: false, star_kwargs: false, args, - no_main_func: Some(false), + auto_kind: None, has_preprocessor: None, }) } else { @@ -49,7 +49,7 @@ pub fn parse_go_sig(code: &str) -> anyhow::Result { star_args: false, star_kwargs: false, args: vec![], - no_main_func: Some(true), + auto_kind: Some("lib".to_string()), has_preprocessor: None, }) } @@ -243,7 +243,7 @@ func main(x int, y string, z bool, l []string, o struct { Name string `json:"nam oidx: None }, ], - no_main_func: Some(false), + auto_kind: None, has_preprocessor: None } ); diff --git a/backend/parsers/windmill-parser-graphql/src/lib.rs b/backend/parsers/windmill-parser-graphql/src/lib.rs index 02d3e9a891909..255bb3df346c5 100644 --- a/backend/parsers/windmill-parser-graphql/src/lib.rs +++ b/backend/parsers/windmill-parser-graphql/src/lib.rs @@ -19,7 +19,7 @@ pub fn parse_graphql_sig(code: &str) -> anyhow::Result { star_args: false, star_kwargs: false, args, - no_main_func: None, + auto_kind: None, has_preprocessor: None, }) } else { @@ -125,7 +125,7 @@ query($i: Int, $arr: [String]!, $wahoo: String = "wahoo") { oidx: None } ], - no_main_func: None, + auto_kind: None, has_preprocessor: None } ); diff --git a/backend/parsers/windmill-parser-java/src/lib.rs b/backend/parsers/windmill-parser-java/src/lib.rs index 559462c902a4c..6d3eeb6f184a5 100644 --- a/backend/parsers/windmill-parser-java/src/lib.rs +++ b/backend/parsers/windmill-parser-java/src/lib.rs @@ -34,7 +34,11 @@ pub fn parse_java_sig_meta(code: &str) -> anyhow::Result { // Traverse the AST to find the Main method signature let main_sig = find_main_signature(root_node, code); - let no_main_func = Some(main_sig.is_none()); + let auto_kind = if main_sig.is_none() { + Some("lib".to_string()) + } else { + None + }; let mut is_public = false; let mut returns_void = false; let mut class_name = None; @@ -76,7 +80,7 @@ pub fn parse_java_sig_meta(code: &str) -> anyhow::Result { star_kwargs: false, args, has_preprocessor: None, - no_main_func, + auto_kind, }; Ok(JavaMainSigMeta { returns_void, class_name, main_sig, is_public }) diff --git a/backend/parsers/windmill-parser-nu/src/lib.rs b/backend/parsers/windmill-parser-nu/src/lib.rs index 76f124efad43f..8bf0615cc0c8f 100644 --- a/backend/parsers/windmill-parser-nu/src/lib.rs +++ b/backend/parsers/windmill-parser-nu/src/lib.rs @@ -36,7 +36,7 @@ pub fn parse_nu_signature(code: &str) -> anyhow::Result { }; let mut sig = MainArgSignature::default(); - sig.no_main_func = Some(false); + sig.auto_kind = None; let batches = args .lines() diff --git a/backend/parsers/windmill-parser-nu/tests/tests.rs b/backend/parsers/windmill-parser-nu/tests/tests.rs index 315041cb55441..6b16864c2692a 100644 --- a/backend/parsers/windmill-parser-nu/tests/tests.rs +++ b/backend/parsers/windmill-parser-nu/tests/tests.rs @@ -54,7 +54,7 @@ mod test { oidx: None } ], - no_main_func: Some(false), + auto_kind: None, has_preprocessor: None, }, sig @@ -81,7 +81,7 @@ mod test { has_default: true, oidx: None },], - no_main_func: Some(false), + auto_kind: None, has_preprocessor: None, }, sig @@ -118,7 +118,7 @@ mod test { oidx: None }, ], - no_main_func: Some(false), + auto_kind: None, has_preprocessor: None, }, sig @@ -230,7 +230,7 @@ mod test { oidx: None }, ], - no_main_func: Some(false), + auto_kind: None, has_preprocessor: None, }, sig @@ -277,7 +277,7 @@ mod test { oidx: None }, ], - no_main_func: Some(false), + auto_kind: None, has_preprocessor: None, }, sig @@ -341,7 +341,7 @@ mod test { // has_default: false, // oidx: None // },], - // no_main_func: Some(false), + // auto_kind: None, // has_preprocessor: None, // }, // sig @@ -371,7 +371,7 @@ mod test { has_default: false, oidx: None },], - no_main_func: Some(false), + auto_kind: None, has_preprocessor: None, }, sig @@ -418,7 +418,7 @@ mod test { oidx: None }, ], - no_main_func: Some(false), + auto_kind: None, has_preprocessor: None, }, sig @@ -446,7 +446,7 @@ mod test { has_default: false, oidx: None },], - no_main_func: Some(false), + auto_kind: None, has_preprocessor: None, }, sig @@ -478,7 +478,7 @@ mod test { // has_default: false, // oidx: None // },], - // no_main_func: Some(false), + // auto_kind: None, // has_preprocessor: None, // }, // sig @@ -540,7 +540,7 @@ mod test { oidx: None } ], - no_main_func: Some(false), + auto_kind: None, has_preprocessor: None, }, sig @@ -633,7 +633,7 @@ mod test { // oidx: None // }, // ], - // no_main_func: Some(false), + // auto_kind: None, // has_preprocessor: None, // }, // sig diff --git a/backend/parsers/windmill-parser-php/src/lib.rs b/backend/parsers/windmill-parser-php/src/lib.rs index 14f30f940c808..a7cb928bc357d 100644 --- a/backend/parsers/windmill-parser-php/src/lib.rs +++ b/backend/parsers/windmill-parser-php/src/lib.rs @@ -99,7 +99,7 @@ pub fn parse_php_signature( star_args: false, star_kwargs: false, args, - no_main_func: Some(false), + auto_kind: None, has_preprocessor, }) } else { @@ -107,7 +107,7 @@ pub fn parse_php_signature( star_args: false, star_kwargs: false, args: vec![], - no_main_func: Some(true), + auto_kind: Some("lib".to_string()), has_preprocessor, }) } @@ -179,7 +179,7 @@ function main(string $input1 = \"hey\", bool $input2 = false, int $input3 = 3, f oidx: None } ], - no_main_func: Some(false), + auto_kind: None, has_preprocessor: None } ); diff --git a/backend/parsers/windmill-parser-py/src/lib.rs b/backend/parsers/windmill-parser-py/src/lib.rs index 97e216acbd790..87848e9c0eaf4 100644 --- a/backend/parsers/windmill-parser-py/src/lib.rs +++ b/backend/parsers/windmill-parser-py/src/lib.rs @@ -16,7 +16,7 @@ use windmill_parser::{json_to_typ, Arg, MainArgSignature, ObjectType, Typ}; use rustpython_parser::{ ast::{ Constant, Expr, ExprAttribute, ExprConstant, ExprDict, ExprList, ExprName, Stmt, - StmtAssign, StmtClassDef, StmtFunctionDef, Suite, + StmtAssign, StmtAsyncFunctionDef, StmtClassDef, StmtFunctionDef, Suite, }, Parse, }; @@ -83,11 +83,17 @@ fn should_parse_for_models(code: &str) -> bool { fn filter_non_main(code: &str, main_name: &str) -> String { let def_main = format!("def {}(", main_name); + let async_def_main = format!("async def {}(", main_name); let mut filtered_code = String::new(); let mut code_iter = code.split("\n"); let mut remaining: String = String::new(); while let Some(line) = code_iter.next() { - if line.starts_with(&def_main) { + if line.starts_with(&async_def_main) { + filtered_code += &async_def_main; + remaining += line.strip_prefix(&async_def_main).unwrap(); + remaining += &code_iter.join("\n"); + break; + } else if line.starts_with(&def_main) { filtered_code += &def_main; remaining += line.strip_prefix(&def_main).unwrap(); remaining += &code_iter.join("\n"); @@ -310,6 +316,11 @@ pub fn parse_python_signature( Stmt::FunctionDef(StmtFunctionDef { name, args, .. }) if name == &main_name => { Some(args.as_ref().clone()) } + Stmt::AsyncFunctionDef(StmtAsyncFunctionDef { name, args, .. }) + if name == &main_name => + { + Some(args.as_ref().clone()) + } _ => None, }); @@ -328,6 +339,11 @@ pub fn parse_python_signature( Stmt::FunctionDef(StmtFunctionDef { name, args, .. }) if &name == &main_name => { Some(*args) } + Stmt::AsyncFunctionDef(StmtAsyncFunctionDef { name, args, .. }) + if &name == &main_name => + { + Some(*args) + } _ => None, }); @@ -344,7 +360,11 @@ pub fn parse_python_signature( star_args: false, star_kwargs: false, args: vec![], - no_main_func: Some(!is_wac_v2), + auto_kind: if is_wac_v2 { + Some("wac".to_string()) + } else { + Some("lib".to_string()) + }, has_preprocessor: Some(has_preprocessor), }); } @@ -455,7 +475,7 @@ pub fn parse_python_signature( } }) .collect(), - no_main_func: Some(false), + auto_kind: None, has_preprocessor: Some(has_preprocessor), }) } else { @@ -463,7 +483,11 @@ pub fn parse_python_signature( star_args: false, star_kwargs: false, args: vec![], - no_main_func: Some(params.is_none()), + auto_kind: if params.is_none() { + Some("lib".to_string()) + } else { + None + }, has_preprocessor: Some(has_preprocessor), }) } @@ -730,7 +754,7 @@ def main(test1: str, name: datetime.datetime = datetime.now(), byte: bytes = byt oidx: None }, ], - no_main_func: Some(false), + auto_kind: None, has_preprocessor: Some(false) } ); @@ -795,7 +819,7 @@ def main(test1: str, oidx: None } ], - no_main_func: Some(false), + auto_kind: None, has_preprocessor: Some(false) } ); @@ -855,7 +879,7 @@ def main(test1: str, oidx: None } ], - no_main_func: Some(false), + auto_kind: None, has_preprocessor: Some(false) } ); @@ -899,7 +923,7 @@ def main(test1: Literal["foo", "bar"], test2: List[Literal["foo", "bar"]]): retu oidx: None } ], - no_main_func: Some(false), + auto_kind: None, has_preprocessor: Some(false) } ); @@ -930,7 +954,7 @@ def main(test1: DynSelect_foo): return has_default: false, oidx: None }], - no_main_func: Some(false), + auto_kind: None, has_preprocessor: Some(false) } ); @@ -954,7 +978,7 @@ def hello(): return star_args: false, star_kwargs: false, args: vec![], - no_main_func: Some(true), + auto_kind: Some("lib".to_string()), has_preprocessor: Some(false) } ); @@ -982,7 +1006,7 @@ def main(): return star_args: false, star_kwargs: false, args: vec![], - no_main_func: Some(false), + auto_kind: None, has_preprocessor: Some(true) } ); @@ -1047,7 +1071,7 @@ def main(a: list, e: List[int], b: list = [1,2,3,4], c = [1,2,3,4], d = ["a", "b oidx: None } ], - no_main_func: Some(false), + auto_kind: None, has_preprocessor: Some(false) } ); @@ -1096,7 +1120,7 @@ def main(a: str, b: Optional[str], c: str | None): return oidx: None }, ], - no_main_func: Some(false), + auto_kind: None, has_preprocessor: Some(false) } ); diff --git a/backend/parsers/windmill-parser-ruby/src/lib.rs b/backend/parsers/windmill-parser-ruby/src/lib.rs index c736cef86ab01..805d2f99333df 100644 --- a/backend/parsers/windmill-parser-ruby/src/lib.rs +++ b/backend/parsers/windmill-parser-ruby/src/lib.rs @@ -29,14 +29,18 @@ pub fn parse_ruby_sig_meta(code: &str) -> anyhow::Result { root_node.clone().to_string(); // Traverse the AST to find the Main method signature let args = find_main_signature(root_node, code)?; - let no_main_func = Some(args.is_none()); + let auto_kind = if args.is_none() { + Some("lib".to_string()) + } else { + None + }; let main_sig = MainArgSignature { star_args: false, star_kwargs: false, args: args.unwrap_or_default(), has_preprocessor: None, - no_main_func, + auto_kind, }; Ok(main_sig) @@ -198,7 +202,7 @@ def private_fn end assert_eq!( sig, - MainArgSignature { no_main_func: Some(true), ..Default::default() } + MainArgSignature { auto_kind: Some("lib".to_string()), ..Default::default() } ); } #[test] @@ -211,7 +215,7 @@ end assert_eq!( sig, - MainArgSignature { no_main_func: Some(false), ..Default::default() } + MainArgSignature { auto_kind: None, ..Default::default() } ); } #[test] @@ -235,7 +239,7 @@ end Arg { name: "b".into(), ..Default::default() }, Arg { name: "c".into(), ..Default::default() } ], - no_main_func: Some(false), + auto_kind: None, ..Default::default() } ); @@ -266,7 +270,7 @@ end ..Default::default() }, ], - no_main_func: Some(false), + auto_kind: None, ..Default::default() } ); @@ -296,7 +300,7 @@ end default: Some(json!({"1": 4, "2": [ 1, 2, 3 ]})), ..Default::default() },], - no_main_func: Some(false), + auto_kind: None, ..Default::default() } ); @@ -355,7 +359,7 @@ end ..Default::default() }, ], - no_main_func: Some(false), + auto_kind: None, ..Default::default() } ); diff --git a/backend/parsers/windmill-parser-ruby/src/wasm_libc.rs b/backend/parsers/windmill-parser-ruby/src/wasm_libc.rs index ddb6705023bb6..924748a0738ce 100644 --- a/backend/parsers/windmill-parser-ruby/src/wasm_libc.rs +++ b/backend/parsers/windmill-parser-ruby/src/wasm_libc.rs @@ -121,11 +121,7 @@ pub unsafe extern "C" fn strncmp(ptr1: *const c_void, ptr2: *const c_void, n: us pub type size_t = usize; use std::slice; #[no_mangle] -pub unsafe extern "C" fn memchr( - haystack: *const c_void, - needle: c_int, - len: usize, -) -> *mut c_void { +pub unsafe extern "C" fn memchr(haystack: *const c_void, needle: c_int, len: usize) -> *mut c_void { if haystack.is_null() || len == 0 { return ptr::null_mut(); // Return null if the input pointer is null or length is zero } @@ -165,7 +161,7 @@ pub unsafe extern "C" fn strchr(mut s: *const c_char, c: c_int) -> *mut c_char { std::ptr::null_mut() // Return null if the character was not found } -// End of AI implemetation +// End of AI implemetation /* -------------------------------- wctype.h -------------------------------- */ #[no_mangle] @@ -202,7 +198,8 @@ pub extern "C" fn iswupper(wc: wint_t) -> c_int { #[no_mangle] pub extern "C" fn iswalpha(wc: wint_t) -> c_int { // Check if the character is an alphabetic character ('A' to 'Z' or 'a' to 'z') - if (wc >= 'A' as wint_t && wc <= 'Z' as wint_t) || (wc >= 'a' as wint_t && wc <= 'z' as wint_t) { + if (wc >= 'A' as wint_t && wc <= 'Z' as wint_t) || (wc >= 'a' as wint_t && wc <= 'z' as wint_t) + { return 1; // Return true (1) } 0 // Return false (0) @@ -216,8 +213,7 @@ pub extern "C" fn iswlower(wc: wint_t) -> c_int { } 0 // Return false (0) } -// End of AI implemetation - +// End of AI implemetation /* --------------------------------- time.h --------------------------------- */ diff --git a/backend/parsers/windmill-parser-rust/src/lib.rs b/backend/parsers/windmill-parser-rust/src/lib.rs index 3999fb044f5e9..0d72ad4893856 100644 --- a/backend/parsers/windmill-parser-rust/src/lib.rs +++ b/backend/parsers/windmill-parser-rust/src/lib.rs @@ -28,7 +28,7 @@ pub fn parse_rust_signature(code: &str) -> anyhow::Result { star_args: false, star_kwargs: false, args, - no_main_func: Some(false), + auto_kind: None, has_preprocessor: None, }) } else { @@ -36,7 +36,7 @@ pub fn parse_rust_signature(code: &str) -> anyhow::Result { star_args: false, star_kwargs: false, args: vec![], - no_main_func: Some(true), + auto_kind: Some("lib".to_string()), has_preprocessor: None, }) } diff --git a/backend/parsers/windmill-parser-sql/src/lib.rs b/backend/parsers/windmill-parser-sql/src/lib.rs index 13fd1a3199a47..4938a8e194e69 100644 --- a/backend/parsers/windmill-parser-sql/src/lib.rs +++ b/backend/parsers/windmill-parser-sql/src/lib.rs @@ -28,7 +28,7 @@ pub fn parse_mysql_sig(code: &str) -> anyhow::Result { star_args: false, star_kwargs: false, args, - no_main_func: None, + auto_kind: None, has_preprocessor: None, }) } else { @@ -44,7 +44,7 @@ pub fn parse_oracledb_sig(code: &str) -> anyhow::Result { star_args: false, star_kwargs: false, args, - no_main_func: None, + auto_kind: None, has_preprocessor: None, }) } else { @@ -65,7 +65,7 @@ pub fn parse_pgsql_sig_with_typed_schema(code: &str) -> anyhow::Result<(MainArgS star_args: false, star_kwargs: false, args, - no_main_func: None, + auto_kind: None, has_preprocessor: None, }, typed_schema, @@ -83,7 +83,7 @@ pub fn parse_bigquery_sig(code: &str) -> anyhow::Result { star_args: false, star_kwargs: false, args, - no_main_func: None, + auto_kind: None, has_preprocessor: None, }) } else { @@ -98,7 +98,7 @@ pub fn parse_duckdb_sig(code: &str) -> anyhow::Result { star_args: false, star_kwargs: false, args, - no_main_func: None, + auto_kind: None, has_preprocessor: None, }) } else { @@ -114,7 +114,7 @@ pub fn parse_snowflake_sig(code: &str) -> anyhow::Result { star_args: false, star_kwargs: false, args, - no_main_func: None, + auto_kind: None, has_preprocessor: None, }) } else { @@ -130,7 +130,7 @@ pub fn parse_mssql_sig(code: &str) -> anyhow::Result { star_args: false, star_kwargs: false, args, - no_main_func: None, + auto_kind: None, has_preprocessor: None, }) } else { @@ -944,7 +944,7 @@ SELECT * FROM table WHERE token=$1::TEXT AND image=$2::BIGINT oidx: Some(2), }, ], - no_main_func: None, + auto_kind: None, has_preprocessor: None } ); @@ -993,7 +993,7 @@ SELECT $2::TEXT; oidx: Some(3), }, ], - no_main_func: None, + auto_kind: None, has_preprocessor: None } ); @@ -1120,7 +1120,7 @@ SELECT ?, ?; oidx: None, }, ], - no_main_func: None, + auto_kind: None, has_preprocessor: None } ); @@ -1168,7 +1168,7 @@ SELECT :param2; oidx: None, }, ], - no_main_func: None, + auto_kind: None, has_preprocessor: None } ); @@ -1208,7 +1208,7 @@ SELECT @token; oidx: None, }, ], - no_main_func: None, + auto_kind: None, has_preprocessor: None } ); @@ -1256,7 +1256,7 @@ SELECT ?; oidx: None, } ], - no_main_func: None, + auto_kind: None, has_preprocessor: None } ); @@ -1304,7 +1304,7 @@ SELECT @P2; oidx: None, }, ], - no_main_func: None, + auto_kind: None, has_preprocessor: None } ); @@ -1353,7 +1353,7 @@ SELECT * FROM table_name WHERE thing = :name4; oidx: None, }, ], - no_main_func: None, + auto_kind: None, has_preprocessor: None } ); @@ -1391,7 +1391,7 @@ SELECT * FROM users WHERE id = $1 AND email = $2::text; oidx: Some(2), }, ], - no_main_func: None, + auto_kind: None, has_preprocessor: None } ); @@ -1429,7 +1429,7 @@ SELECT * FROM users LIMIT $1 OFFSET $2; oidx: Some(2), }, ], - no_main_func: None, + auto_kind: None, has_preprocessor: None } ); @@ -1479,7 +1479,7 @@ WHERE id = $1 oidx: Some(3), }, ], - no_main_func: None, + auto_kind: None, has_preprocessor: None } ); @@ -1506,7 +1506,7 @@ SELECT * FROM users WHERE id = ANY($1); has_default: false, oidx: Some(1), },], - no_main_func: None, + auto_kind: None, has_preprocessor: None } ); @@ -1535,7 +1535,7 @@ SELECT $1::integer; has_default: false, oidx: Some(1), },], - no_main_func: None, + auto_kind: None, has_preprocessor: None } ); @@ -1567,7 +1567,7 @@ SELECT x has_default: false, oidx: None, },], - no_main_func: None, + auto_kind: None, has_preprocessor: None } ); diff --git a/backend/parsers/windmill-parser-ts/src/lib.rs b/backend/parsers/windmill-parser-ts/src/lib.rs index 8240d5224204c..53ecaf277cc4c 100644 --- a/backend/parsers/windmill-parser-ts/src/lib.rs +++ b/backend/parsers/windmill-parser-ts/src/lib.rs @@ -240,6 +240,7 @@ pub fn parse_deno_signature( let mut has_preprocessor = false; let mut entrypoint_params = None; + let mut is_wac = false; let ast = parser .parse_module() @@ -277,6 +278,16 @@ pub fn parse_deno_signature( } } + // export default workflow(async (...) => { ... }) + if entrypoint_params.is_none() { + if let ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr(export_default)) = &item { + if let Some(params) = extract_workflow_params(&export_default.expr) { + entrypoint_params = Some(params); + is_wac = true; + } + } + } + if let ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl { decl, .. })) | ModuleItem::Stmt(Stmt::Decl(decl)) = item { @@ -308,6 +319,20 @@ pub fn parse_deno_signature( entrypoint_params = Some(fn_decl.function.params.clone()); } } + Decl::Var(var_decl) if entrypoint_params.is_none() => { + for decl in &var_decl.decls { + if let Some(name) = &decl.name.as_ident() { + if name.sym.as_ref() == entrypoint_function { + if let Some(init) = &decl.init { + if let Some(params) = extract_workflow_params(init) { + entrypoint_params = Some(params); + is_wac = true; + } + } + } + } + } + } _ => {} } } @@ -315,11 +340,18 @@ pub fn parse_deno_signature( let mut c: u16 = 0; - let is_wac_v2 = entrypoint_params.is_none() - && code.contains("workflow(") - && code.contains("task(") - && code.contains("windmill-client"); - let no_main_func = entrypoint_params.is_none() && !is_wac_v2; + let auto_kind = if is_wac { + Some("wac".to_string()) + } else if entrypoint_params.is_none() { + if code.contains("workflow(") && code.contains("task(") && code.contains("windmill-client") + { + Some("wac".to_string()) + } else { + Some("lib".to_string()) + } + } else { + None + }; let mut type_resolver = HashMap::new(); let r = MainArgSignature { star_args: false, @@ -346,12 +378,65 @@ pub fn parse_deno_signature( .transpose()? .unwrap_or_else(|| vec![]) }, - no_main_func: Some(no_main_func), + auto_kind, has_preprocessor: Some(has_preprocessor), }; Ok(r) } +/// Extract params from `workflow(async (...) => { ... })` or `workflow(async function(...) { ... })` +fn extract_workflow_params(expr: &Expr) -> Option> { + if let Expr::Call(call) = expr { + if let swc_ecma_ast::Callee::Expr(callee) = &call.callee { + if let Expr::Ident(ident) = callee.as_ref() { + if ident.sym.as_ref() == "workflow" { + if let Some(first_arg) = call.args.first() { + match first_arg.expr.as_ref() { + Expr::Arrow(arrow) if arrow.is_async => { + return Some( + arrow + .params + .iter() + .map(|pat| Param { + span: pat.span(), + decorators: vec![], + pat: pat.clone(), + }) + .collect(), + ); + } + Expr::Fn(fn_expr) if fn_expr.function.is_async => { + return Some(fn_expr.function.params.clone()); + } + Expr::Paren(p) => { + return extract_workflow_params_from_inner(&p.expr); + } + _ => {} + } + } + } + } + } + } + None +} + +/// Helper for parenthesized expressions inside workflow() +fn extract_workflow_params_from_inner(expr: &Expr) -> Option> { + match expr { + Expr::Arrow(arrow) if arrow.is_async => Some( + arrow + .params + .iter() + .map(|pat| Param { span: pat.span(), decorators: vec![], pat: pat.clone() }) + .collect(), + ), + Expr::Fn(fn_expr) if fn_expr.function.is_async => Some(fn_expr.function.params.clone()), + Expr::Paren(p) => extract_workflow_params_from_inner(&p.expr), + _ => None, + } +} + fn parse_param( symbol_table: &HashMap, type_resolver: &mut HashMap, diff --git a/backend/parsers/windmill-parser-ts/tests/tests.rs b/backend/parsers/windmill-parser-ts/tests/tests.rs index 9eb1732203739..7a685fbb77cae 100644 --- a/backend/parsers/windmill-parser-ts/tests/tests.rs +++ b/backend/parsers/windmill-parser-ts/tests/tests.rs @@ -45,7 +45,7 @@ mod tests { star_args: false, star_kwargs: false, args: vec![], - no_main_func: Some(false), + auto_kind: None, has_preprocessor: Some(false), } ); @@ -103,7 +103,7 @@ mod tests { oidx: None, }, ], - no_main_func: Some(false), + auto_kind: None, has_preprocessor: Some(false), } ); @@ -152,7 +152,7 @@ mod tests { oidx: None, }, ], - no_main_func: Some(false), + auto_kind: None, has_preprocessor: Some(false), } ); @@ -201,7 +201,7 @@ mod tests { oidx: None, }, ], - no_main_func: Some(false), + auto_kind: None, has_preprocessor: Some(false), } ); @@ -232,7 +232,7 @@ mod tests { has_default: false, oidx: None, },], - no_main_func: Some(false), + auto_kind: None, has_preprocessor: Some(false), } ); @@ -263,7 +263,7 @@ mod tests { has_default: false, oidx: None, },], - no_main_func: Some(false), + auto_kind: None, has_preprocessor: Some(false), } ); @@ -303,7 +303,7 @@ mod tests { has_default: false, oidx: None, }], - no_main_func: Some(false), + auto_kind: None, has_preprocessor: Some(false), } ); @@ -341,7 +341,7 @@ mod tests { has_default: false, oidx: None, }], - no_main_func: Some(false), + auto_kind: None, has_preprocessor: Some(false), } ); @@ -399,7 +399,7 @@ mod tests { has_default: false, oidx: None, }], - no_main_func: Some(false), + auto_kind: None, has_preprocessor: Some(false), } ); @@ -457,7 +457,7 @@ mod tests { oidx: None, }, ], - no_main_func: Some(false), + auto_kind: None, has_preprocessor: Some(false), } ); @@ -506,7 +506,7 @@ mod tests { oidx: None, }, ], - no_main_func: Some(false), + auto_kind: None, has_preprocessor: Some(false), } ); @@ -555,7 +555,7 @@ mod tests { star_args: false, star_kwargs: false, args: vec![], - no_main_func: Some(true), + auto_kind: Some("lib".to_string()), has_preprocessor: Some(false), } ); @@ -582,7 +582,7 @@ mod tests { has_default: false, oidx: None, }], - no_main_func: Some(false), + auto_kind: None, has_preprocessor: Some(false), } ); @@ -611,7 +611,7 @@ mod tests { has_default: false, oidx: None, }], - no_main_func: Some(false), + auto_kind: None, has_preprocessor: Some(false), } ); @@ -640,7 +640,7 @@ mod tests { has_default: false, oidx: None, }], - no_main_func: Some(false), + auto_kind: None, has_preprocessor: Some(false), } ); @@ -689,7 +689,7 @@ mod tests { oidx: None, }, ], - no_main_func: Some(false), + auto_kind: None, has_preprocessor: Some(false), } ); @@ -729,7 +729,7 @@ mod tests { has_default: false, oidx: None, }], - no_main_func: Some(false), + auto_kind: None, has_preprocessor: Some(true), } ); @@ -759,7 +759,7 @@ mod tests { has_default: false, oidx: None, }], - no_main_func: Some(false), + auto_kind: None, has_preprocessor: Some(true), } ); diff --git a/backend/parsers/windmill-parser-wasm/Cargo.lock b/backend/parsers/windmill-parser-wasm/Cargo.lock new file mode 100644 index 0000000000000..5b0eb580f33b5 --- /dev/null +++ b/backend/parsers/windmill-parser-wasm/Cargo.lock @@ -0,0 +1,4868 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "ar_archive_writer" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eb93bbb63b9c227414f6eb3a0adfddca591a8ce1e9b60661bb08969b87e340b" +dependencies = [ + "object", +] + +[[package]] +name = "ariadne" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1cb2a2046bea8ce5e875551f5772024882de0b540c7f93dfc5d6cf1ca8b030c" +dependencies = [ + "yansi", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "ast_node" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9184f2b369b3e8625712493c89b785881f27eedc6cde480a81883cef78868b2" +dependencies = [ + "proc-macro2", + "quote", + "swc_macros_common", + "syn 2.0.117", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "better_scoped_tls" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297b153aa5e573b5863108a6ddc9d5c968bd0b20e75cc614ee9821d2f45679c7" +dependencies = [ + "scoped-tls", +] + +[[package]] +name = "bigdecimal" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d6867f1565b3aad85681f1015055b087fcfd840d6aeee6eee7f2da317603695" +dependencies = [ + "autocfg", + "libm", + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "proc-macro2", + "quote", + "regex", + "rustc-hash 2.1.1", + "shlex", + "syn 2.0.117", +] + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "2.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +dependencies = [ + "serde", +] + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "borsh" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" +dependencies = [ + "borsh-derive", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0686c856aa6aac0c4498f936d7d6a02df690f614c03e4d906d1018062b5c5e2c" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +dependencies = [ + "allocator-api2", +] + +[[package]] +name = "byte-unit" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6d47a4e2961fb8721bcfc54feae6455f2f64e7054f9bc67e875f0e77f4c58d" +dependencies = [ + "rust_decimal", + "schemars 1.2.1", + "serde", + "utf8-width", +] + +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "bytesize" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e93abca9e28e0a1b9877922aacb20576e05d4679ffa78c3d6dc22a26a216659" + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "pure-rust-locales", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "chrono-humanize" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799627e6b4d27827a814e837b9d8a504832086081806d45b1afa34dc982b023b" +dependencies = [ + "chrono", +] + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "clap_lex" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "derive_more" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "unicode-xid", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e004d887f51fcb9fef17317a2f3525c887d8aa3f4f50fed920816a688284a5b7" +dependencies = [ + "serde", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "fancy-regex" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298" +dependencies = [ + "bit-set", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "from_variant" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32016f1242eb82af5474752d00fd8ebcd9004bd69b462b1c91de833972d08ed4" +dependencies = [ + "proc-macro2", + "swc_macros_common", + "syn 2.0.117", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getopts" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" +dependencies = [ + "unicode-width 0.2.2", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "gosyn" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb37859fda6792e95231aef1c5838f4043ec0ee352d8313421e311c606df612" +dependencies = [ + "anyhow", + "strum 0.25.0", + "thiserror 1.0.69", + "unic-ucd-category", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash 0.8.12", + "allocator-api2", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "hstr" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a26def229ea95a8709dad32868d975d0dd40235bd2ce82920e4a8fe692b5e0" +dependencies = [ + "hashbrown 0.14.5", + "new_debug_unreachable", + "once_cell", + "phf", + "rustc-hash 1.1.0", + "triomphe", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", +] + +[[package]] +name = "inventory" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "009ae045c87e7082cb72dab0ccd01ae075dd00141ddc108f43a0ea150a9e7227" +dependencies = [ + "rustversion", +] + +[[package]] +name = "is-macro" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57a3e447e24c22647738e4607f1df1e0ec6f72e16182c4cd199f647cdfb0e4" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "is_ci" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "js-sys" +version = "0.3.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852f13bec5eba4ba9afbeb93fd7c13fe56147f055939ae21c43a29a0ecb2702e" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lalrpop-util" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507460a910eb7b32ee961886ff48539633b788a36b65692b95f225b844c82553" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "libc" +version = "0.2.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libproc" +version = "0.14.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a54ad7278b8bc5301d5ffd2a94251c004feb971feba96c971ea4063645990757" +dependencies = [ + "bindgen", + "errno", + "libc", +] + +[[package]] +name = "libredox" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" +dependencies = [ + "bitflags", + "libc", + "plain", + "redox_syscall 0.7.3", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "lscolors" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53304fff6ab1e597661eee37e42ea8c47a146fca280af902bb76bff8a896e523" +dependencies = [ + "nu-ansi-term", +] + +[[package]] +name = "mach2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +dependencies = [ + "libc", +] + +[[package]] +name = "malachite" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fbdf9cb251732db30a7200ebb6ae5d22fe8e11397364416617d2c2cf0c51cb5" +dependencies = [ + "malachite-base", + "malachite-nz", + "malachite-q", +] + +[[package]] +name = "malachite-base" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ea0ed76adf7defc1a92240b5c36d5368cfe9251640dcce5bd2d0b7c1fd87aeb" +dependencies = [ + "hashbrown 0.14.5", + "itertools 0.11.0", + "libm", + "ryu", +] + +[[package]] +name = "malachite-bigint" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d149aaa2965d70381709d9df4c7ee1fc0de1c614a4efc2ee356f5e43d68749f8" +dependencies = [ + "derive_more", + "malachite", + "num-integer", + "num-traits", + "paste", +] + +[[package]] +name = "malachite-nz" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34a79feebb2bc9aa7762047c8e5495269a367da6b5a90a99882a0aeeac1841f7" +dependencies = [ + "itertools 0.11.0", + "libm", + "malachite-base", +] + +[[package]] +name = "malachite-q" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f235d5747b1256b47620f5640c2a17a88c7569eebdf27cd9cb130e1a619191" +dependencies = [ + "itertools 0.11.0", + "malachite-base", + "malachite-nz", +] + +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "miette" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" +dependencies = [ + "cfg-if", + "miette-derive", + "owo-colors", + "supports-color", + "supports-hyperlinks", + "supports-unicode", + "terminal_size", + "textwrap", + "unicode-width 0.1.14", +] + +[[package]] +name = "miette-derive" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "minicov" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4869b6a491569605d66d3952bcdf03df789e5b536e5f0cf7758a7f08a55ae24d" +dependencies = [ + "cc", + "walkdir", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "ntapi" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" +dependencies = [ + "winapi", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "nu-derive-value" +version = "0.101.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71f7c8ed6ba88a567ec6f7c4cad4a7a8465ab93b8cdaf89d3dc72347a83c2d1f" +dependencies = [ + "heck 0.5.0", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "nu-engine" +version = "0.101.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6619583ed281060a9ea0a3f4532eea918370c94e703b903065f35e5aa49b14" +dependencies = [ + "log", + "nu-glob", + "nu-path", + "nu-protocol", + "nu-utils", + "terminal_size", +] + +[[package]] +name = "nu-glob" +version = "0.101.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acd0a9fe69412acdc8501f5ef19031f9cac119d93823cb957b14ddfe1cb97660" + +[[package]] +name = "nu-parser" +version = "0.101.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2adc2876bd7bc83be15786cedf2cb08a81a9d70fa4b8df569b3f1cbec1e0b58d" +dependencies = [ + "bytesize", + "chrono", + "itertools 0.13.0", + "log", + "nu-engine", + "nu-path", + "nu-protocol", + "nu-utils", + "serde_json", +] + +[[package]] +name = "nu-path" +version = "0.101.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ccd1bbaf370d79118bd1a807abb07d8d1386751d0ae9266baafa91bd0b5523f" +dependencies = [ + "dirs", + "omnipath", + "pwd", +] + +[[package]] +name = "nu-protocol" +version = "0.101.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f49a395b632530d7f46fd24183c7f42423677f70afb3cb4726e3abfe92273b" +dependencies = [ + "byte-unit", + "bytes", + "chrono", + "chrono-humanize", + "dirs", + "dirs-sys", + "fancy-regex", + "heck 0.5.0", + "indexmap", + "log", + "lru", + "miette", + "nix", + "nu-derive-value", + "nu-path", + "nu-system", + "nu-utils", + "num-format", + "serde", + "serde_json", + "thiserror 2.0.18", + "typetag", + "windows-sys 0.48.0", +] + +[[package]] +name = "nu-system" +version = "0.101.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81182f7e64bd5dd16ab844d8e40f78e389d06d95f5a0c419f4701fb8fc163077" +dependencies = [ + "chrono", + "itertools 0.13.0", + "libc", + "libproc", + "log", + "mach2", + "nix", + "ntapi", + "procfs", + "sysinfo", + "windows 0.56.0", +] + +[[package]] +name = "nu-utils" +version = "0.101.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d1468fa8e6e12d9d53c90b44f3d11a37d87502d7a30d145f122341c5b33745" +dependencies = [ + "crossterm_winapi", + "fancy-regex", + "log", + "lscolors", + "nix", + "num-format", + "serde", + "serde_json", + "strip-ansi-escapes", + "sys-locale", + "unicase", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", + "serde", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-format" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a652d9771a63711fd3c3deb670acfbe5c30a4072e664d7a3bf5a9e1056ac72c3" +dependencies = [ + "arrayvec", + "itoa", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "omnipath" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80adb31078122c880307e9cdfd4e3361e6545c319f9b9dcafcb03acd3b51a575" + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "owo-colors" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher 1.0.2", +] + +[[package]] +name = "php-parser-rs" +version = "0.1.3" +source = "git+https://github.com/php-rust-tools/parser?rev=ec4cb411dec09450946ef57920b7ffced7f6495d#ec4cb411dec09450946ef57920b7ffced7f6495d" +dependencies = [ + "ariadne", + "clap", + "schemars 0.8.22", + "serde", + "serde_json", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit 0.23.4", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "procfs" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc5b72d8145275d844d4b5f6d4e1eef00c8cd889edb6035c21675d1bb1f45c9f" +dependencies = [ + "bitflags", + "chrono", + "flate2", + "hex", + "procfs-core", + "rustix 0.38.44", +] + +[[package]] +name = "procfs-core" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "239df02d8349b06fc07398a3a1697b06418223b1c7725085e801e7c0fc6a12ec" +dependencies = [ + "bitflags", + "chrono", + "hex", +] + +[[package]] +name = "psm" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3852766467df634d74f0b2d7819bf8dc483a0eb2e3b0f50f756f9cfe8b0d18d8" +dependencies = [ + "ar_archive_writer", + "cc", +] + +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "pulldown-cmark" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b" +dependencies = [ + "bitflags", + "getopts", + "memchr", + "unicase", +] + +[[package]] +name = "pure-rust-locales" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "869675ad2d7541aea90c6d88c81f46a7f4ea9af8cd0395d38f11a95126998a0d" + +[[package]] +name = "pwd" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72c71c0c79b9701efe4e1e4b563b2016dd4ee789eb99badcb09d61ac4b92e4a2" +dependencies = [ + "libc", + "thiserror 1.0.69", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", + "zerocopy", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "recursive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0786a43debb760f491b1bc0269fe5e84155353c67482b9e60d0cfb596054b43e" +dependencies = [ + "recursive-proc-macro-impl", + "stacker", +] + +[[package]] +name = "recursive-proc-macro-impl" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76009fbe0614077fc1a2ce255e3a1881a2e3a3527097d5dc6d8212c585e7e38b" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_syscall" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-lite" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rkyv" +version = "0.7.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2297bf9c81a3f0dc96bc9521370b88f054168c29826a75e89c55ff196e7ed6a1" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d7b42d4b8d06048d3ac8db0eb31bcb942cbeb709f0b5f2b2ebde398d3038f5" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rust_decimal" +version = "1.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61f703d19852dbf87cbc513643fa81428361eb6940f1ac14fd58155d295a3eb0" +dependencies = [ + "arrayvec", + "borsh", + "bytes", + "num-traits", + "rand 0.8.5", + "rkyv", + "serde", + "serde_json", +] + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustpython-ast" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cdaf8ee5c1473b993b398c174641d3aa9da847af36e8d5eb8291930b72f31a5" +dependencies = [ + "is-macro", + "malachite-bigint", + "rustpython-parser-core", + "static_assertions", +] + +[[package]] +name = "rustpython-parser" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "868f724daac0caf9bd36d38caf45819905193a901e8f1c983345a68e18fb2abb" +dependencies = [ + "anyhow", + "is-macro", + "itertools 0.11.0", + "lalrpop-util", + "log", + "malachite-bigint", + "num-traits", + "phf", + "phf_codegen", + "rustc-hash 1.1.0", + "rustpython-ast", + "rustpython-parser-core", + "tiny-keccak", + "unic-emoji-char", + "unic-ucd-ident", + "unicode_names2", +] + +[[package]] +name = "rustpython-parser-core" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4b6c12fa273825edc7bccd9a734f0ad5ba4b8a2f4da5ff7efe946f066d0f4ad" +dependencies = [ + "is-macro", + "memchr", + "rustpython-parser-vendored", +] + +[[package]] +name = "rustpython-parser-vendored" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04fcea49a4630a3a5d940f4d514dc4f575ed63c14c3e3ed07146634aed7f67a6" +dependencies = [ + "memchr", + "once_cell", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.117", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_json" +version = "1.0.143" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" +dependencies = [ + "indexmap", + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] + +[[package]] +name = "smartstring" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29" +dependencies = [ + "autocfg", + "static_assertions", + "version_check", +] + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlparser" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4591acadbcf52f0af60eafbb2c003232b2b4cd8de5f0e9437cb8b1b59046cc0f" +dependencies = [ + "log", + "recursive", + "sqlparser_derive", +] + +[[package]] +name = "sqlparser_derive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da5fc6819faabb412da764b99d3b713bb55083c11e7e0c00144d386cd6a1939c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "sqlx" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +dependencies = [ + "base64", + "bigdecimal", + "bytes", + "chrono", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.5", + "hashlink", + "indexmap", + "log", + "memchr", + "once_cell", + "percent-encoding", + "rustls", + "serde", + "serde_json", + "sha2", + "smallvec", + "thiserror 2.0.18", + "tokio", + "tokio-stream", + "tracing", + "url", + "uuid", + "webpki-roots 0.26.11", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.117", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +dependencies = [ + "dotenvy", + "either", + "heck 0.5.0", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 2.0.117", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" +dependencies = [ + "atoi", + "base64", + "bigdecimal", + "bitflags", + "byteorder", + "bytes", + "chrono", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand 0.8.5", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.18", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +dependencies = [ + "atoi", + "base64", + "bigdecimal", + "bitflags", + "byteorder", + "chrono", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "num-bigint", + "once_cell", + "rand 0.8.5", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.18", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +dependencies = [ + "atoi", + "chrono", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "thiserror 2.0.18", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "stacker" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d74a23609d509411d10e2176dc2a4346e3b4aea2e7b1869f19fdedbc71c013" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys 0.59.0", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "string_enum" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05e383308aebc257e7d7920224fa055c632478d92744eca77f99be8fa1545b90" +dependencies = [ + "proc-macro2", + "quote", + "swc_macros_common", + "syn 2.0.117", +] + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "strip-ansi-escapes" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a8f8038e7e7969abb3f1b7c2a811225e9296da208539e0f79c5251d6cac0025" +dependencies = [ + "vte", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" +dependencies = [ + "strum_macros 0.25.3", +] + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros 0.27.2", +] + +[[package]] +name = "strum_macros" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.117", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "supports-color" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6" +dependencies = [ + "is_ci", +] + +[[package]] +name = "supports-hyperlinks" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e396b6523b11ccb83120b115a0b7366de372751aa6edf19844dfb13a6af97e91" + +[[package]] +name = "supports-unicode" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" + +[[package]] +name = "swc_allocator" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76aa0eb65c0f39f9b6d82a7e5192c30f7ac9a78f084a21f270de1d8c600ca388" +dependencies = [ + "bumpalo", + "hashbrown 0.14.5", + "ptr_meta", + "rustc-hash 1.1.0", + "triomphe", +] + +[[package]] +name = "swc_atoms" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb6567e4e67485b3e7662b486f1565bdae54bd5b9d6b16b2ba1a9babb1e42125" +dependencies = [ + "hstr", + "once_cell", + "rustc-hash 1.1.0", + "serde", +] + +[[package]] +name = "swc_common" +version = "0.37.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12d0a8eaaf1606c9207077d75828008cb2dfb51b095a766bd2b72ef893576e31" +dependencies = [ + "ast_node", + "better_scoped_tls", + "cfg-if", + "either", + "from_variant", + "new_debug_unreachable", + "num-bigint", + "once_cell", + "rustc-hash 1.1.0", + "serde", + "siphasher 0.3.11", + "swc_allocator", + "swc_atoms", + "swc_eq_ignore_macros", + "swc_visit", + "tracing", + "unicode-width 0.1.14", + "url", +] + +[[package]] +name = "swc_ecma_ast" +version = "0.118.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6f866d12e4d519052b92a0a86d1ac7ff17570da1272ca0c89b3d6f802cd79df" +dependencies = [ + "bitflags", + "is-macro", + "num-bigint", + "phf", + "scoped-tls", + "string_enum", + "swc_atoms", + "swc_common", + "unicode-id-start", +] + +[[package]] +name = "swc_ecma_parser" +version = "0.149.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683dada14722714588b56481399c699378b35b2ba4deb5c4db2fb627a97fb54b" +dependencies = [ + "either", + "new_debug_unreachable", + "num-bigint", + "num-traits", + "phf", + "serde", + "smallvec", + "smartstring", + "stacker", + "swc_atoms", + "swc_common", + "swc_ecma_ast", + "tracing", + "typed-arena", +] + +[[package]] +name = "swc_ecma_visit" +version = "0.104.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b1c6802e68e51f336e8bc9644e9ff9da75d7da9c1a6247d532f2e908aa33e81" +dependencies = [ + "new_debug_unreachable", + "num-bigint", + "swc_atoms", + "swc_common", + "swc_ecma_ast", + "swc_visit", + "tracing", +] + +[[package]] +name = "swc_eq_ignore_macros" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63db0adcff29d220c3d151c5b25c0eabe7e32dd936212b84cdaa1392e3130497" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "swc_macros_common" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27e18fbfe83811ffae2bb23727e45829a0d19c6870bced7c0f545cc99ad248dd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "swc_visit" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ceb044142ba2719ef9eb3b6b454fce61ab849eb696c34d190f04651955c613d" +dependencies = [ + "either", + "new_debug_unreachable", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "sys-locale" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4" +dependencies = [ + "libc", +] + +[[package]] +name = "sysinfo" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c33cd241af0f2e9e3b5c32163b873b29956890b5342e6745b917ce9d490f4af" +dependencies = [ + "core-foundation-sys", + "libc", + "memchr", + "ntapi", + "rayon", + "windows 0.57.0", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "terminal_size" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" +dependencies = [ + "rustix 1.1.4", + "windows-sys 0.60.2", +] + +[[package]] +name = "textwrap" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" +dependencies = [ + "unicode-linebreak", + "unicode-width 0.2.2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_edit 0.19.15", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7211ff1b8f0d3adae1663b7da9ffe396eabe1ca25f0b0bee42b0da29a9ddce93" +dependencies = [ + "indexmap", + "toml_datetime 0.7.0", + "toml_parser", + "winnow 0.7.15", +] + +[[package]] +name = "toml_parser" +version = "1.0.9+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +dependencies = [ + "winnow 0.7.15", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tree-sitter" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0203df02a3b6dd63575cc1d6e609edc2181c9a11867a271b25cfd2abff3ec5ca" +dependencies = [ + "cc", + "regex", + "regex-syntax", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-c-sharp" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67f06accca7b45351758663b8215089e643d53bd9a660ce0349314263737fcb0" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-java" +version = "0.23.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aa6cbcdc8c679b214e616fd3300da67da0e492e066df01bcf5a5921a71e90d6" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-language" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "009994f150cc0cd50ff54917d5bc8bffe8cad10ca10d81c34da2ec421ae61782" + +[[package]] +name = "tree-sitter-ruby" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be0484ea4ef6bb9c575b4fdabde7e31340a8d2dbc7d52b321ac83da703249f95" +dependencies = [ + "cc", + "tree-sitter-language", +] + +[[package]] +name = "triomphe" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd69c5aa8f924c7519d6372789a74eac5b94fb0f8fcf0d4a97eb0bfc3e785f39" +dependencies = [ + "serde", + "stable_deref_trait", +] + +[[package]] +name = "typed-arena" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "typetag" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be2212c8a9b9bcfca32024de14998494cf9a5dfa59ea1b829de98bac374b86bf" +dependencies = [ + "erased-serde", + "inventory", + "once_cell", + "serde", + "typetag-impl", +] + +[[package]] +name = "typetag-impl" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27a7a9b72ba121f6f1f6c3632b85604cac41aedb5ddc70accbebb6cac83de846" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-emoji-char" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b07221e68897210270a38bde4babb655869637af0f69407f96053a34f76494d" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-category" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8d4591f5fcfe1bd4453baaf803c40e1b1e69ff8455c47620440b46efef91c0" +dependencies = [ + "matches", + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-id-start" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81b79ad29b5e19de4260020f8919b443b2ef0277d242ce532ec7b7a2cc8b6007" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unicode_names2" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1673eca9782c84de5f81b82e4109dcfb3611c8ba0d52930ec4a9478f547b2dd" +dependencies = [ + "phf", + "unicode_names2_generator", +] + +[[package]] +name = "unicode_names2_generator" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91e5b84611016120197efd7dc93ef76774f4e084cd73c9fb3ea4a86c570c56e" +dependencies = [ + "getopts", + "log", + "phf_codegen", + "rand 0.8.5", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +dependencies = [ + "getrandom 0.3.4", + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vte" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "231fdcd7ef3037e8330d8e17e61011a2c244126acc0a982f4040ac3f9f0bc077" +dependencies = [ + "memchr", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab10a69fbd0a177f5f649ad4d8d3305499c42bab9aef2f7ff592d0ec8f833819" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb702423545a6007bbc368fde243ba47ca275e549c8a28617f56f6ba53b1d1c" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0b221ff421256839509adbb55998214a70d829d3a28c69b4a6672e9d2a42f67" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc65f4f411d91494355917b605e1480033152658d71f722a90647f56a70c88a0" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc003a991398a8ee604a401e194b6b3a39677b3173d6e74495eb51b82e99a32" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "293c37f4efa430ca14db3721dfbe48d8c33308096bd44d80ebaa775ab71ba1cf" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-bindgen-test" +version = "0.3.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aee0a0f5343de9221a0d233b04520ed8dc2e6728dce180b1dcd9288ec9d9fa3c" +dependencies = [ + "js-sys", + "minicov", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test-macro", +] + +[[package]] +name = "wasm-bindgen-test-macro" +version = "0.3.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a369369e4360c2884c3168d22bded735c43cccae97bbc147586d4b480edd138d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "web-sys" +version = "0.3.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbe734895e869dc429d78c4b433f8d17d95f8d05317440b4fad5ab2d33e596dc" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.6", +] + +[[package]] +name = "webpki-roots" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windmill-parser" +version = "1.654.0" +dependencies = [ + "convert_case", + "serde", + "serde_json", +] + +[[package]] +name = "windmill-parser-bash" +version = "1.654.0" +dependencies = [ + "anyhow", + "lazy_static", + "regex", + "regex-lite", + "serde_json", + "windmill-parser", +] + +[[package]] +name = "windmill-parser-csharp" +version = "1.654.0" +dependencies = [ + "anyhow", + "serde_json", + "tree-sitter", + "tree-sitter-c-sharp", + "wasm-bindgen", + "windmill-parser", +] + +[[package]] +name = "windmill-parser-go" +version = "1.654.0" +dependencies = [ + "anyhow", + "gosyn", + "itertools 0.14.0", + "lazy_static", + "regex", + "windmill-parser", +] + +[[package]] +name = "windmill-parser-graphql" +version = "1.654.0" +dependencies = [ + "anyhow", + "lazy_static", + "regex", + "regex-lite", + "serde_json", + "windmill-parser", +] + +[[package]] +name = "windmill-parser-java" +version = "1.654.0" +dependencies = [ + "anyhow", + "serde_json", + "tree-sitter", + "tree-sitter-java", + "wasm-bindgen", + "windmill-parser", +] + +[[package]] +name = "windmill-parser-nu" +version = "1.654.0" +dependencies = [ + "anyhow", + "nu-parser", + "serde_json", + "wasm-bindgen", + "windmill-parser", +] + +[[package]] +name = "windmill-parser-php" +version = "1.654.0" +dependencies = [ + "anyhow", + "itertools 0.14.0", + "php-parser-rs", + "serde_json", + "windmill-parser", +] + +[[package]] +name = "windmill-parser-py" +version = "1.654.0" +dependencies = [ + "anyhow", + "itertools 0.14.0", + "rustpython-ast", + "rustpython-parser", + "serde_json", + "windmill-parser", + "windmill-parser-sql", +] + +[[package]] +name = "windmill-parser-ruby" +version = "1.654.0" +dependencies = [ + "anyhow", + "lazy_static", + "regex", + "serde_json", + "tree-sitter", + "tree-sitter-ruby", + "wasm-bindgen", + "windmill-parser", +] + +[[package]] +name = "windmill-parser-rust" +version = "1.654.0" +dependencies = [ + "anyhow", + "convert_case", + "itertools 0.14.0", + "lazy_static", + "pulldown-cmark", + "quote", + "regex", + "serde_json", + "syn 2.0.117", + "toml", + "windmill-parser", +] + +[[package]] +name = "windmill-parser-sql" +version = "1.654.0" +dependencies = [ + "anyhow", + "lazy_static", + "regex", + "regex-lite", + "serde", + "serde_json", + "sqlparser", + "windmill-parser", + "windmill-types", +] + +[[package]] +name = "windmill-parser-ts" +version = "1.654.0" +dependencies = [ + "anyhow", + "lazy_static", + "regex", + "serde-wasm-bindgen", + "serde_json", + "swc_common", + "swc_ecma_ast", + "swc_ecma_parser", + "swc_ecma_visit", + "triomphe", + "wasm-bindgen", + "windmill-parser", + "windmill-parser-sql", +] + +[[package]] +name = "windmill-parser-wac" +version = "1.654.0" +dependencies = [ + "anyhow", + "rustpython-ast", + "rustpython-parser", + "serde", + "serde_json", + "sha2", + "swc_common", + "swc_ecma_ast", + "swc_ecma_parser", + "swc_ecma_visit", +] + +[[package]] +name = "windmill-parser-wasm" +version = "1.654.0" +dependencies = [ + "anyhow", + "getrandom 0.2.17", + "getrandom 0.3.4", + "serde_json", + "wasm-bindgen", + "wasm-bindgen-test", + "windmill-parser", + "windmill-parser-bash", + "windmill-parser-csharp", + "windmill-parser-go", + "windmill-parser-graphql", + "windmill-parser-java", + "windmill-parser-nu", + "windmill-parser-php", + "windmill-parser-py", + "windmill-parser-ruby", + "windmill-parser-rust", + "windmill-parser-sql", + "windmill-parser-ts", + "windmill-parser-wac", + "windmill-parser-yaml", +] + +[[package]] +name = "windmill-parser-yaml" +version = "1.654.0" +dependencies = [ + "anyhow", + "serde", + "serde_json", + "windmill-parser", + "yaml-rust", +] + +[[package]] +name = "windmill-types" +version = "1.654.0" +dependencies = [ + "anyhow", + "bitflags", + "chrono", + "hex", + "itertools 0.14.0", + "rand 0.9.0", + "serde", + "serde_json", + "sqlx", + "strum 0.27.2", + "tracing", + "uuid", +] + +[[package]] +name = "windows" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1de69df01bdf1ead2f4ac895dc77c9351aefff65b2f3db429a343f9cbf05e132" +dependencies = [ + "windows-core 0.56.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" +dependencies = [ + "windows-core 0.57.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4698e52ed2d08f8658ab0c39512a7c00ee5fe2688c65f8c0a4f06750d729f2a6" +dependencies = [ + "windows-implement 0.56.0", + "windows-interface 0.56.0", + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" +dependencies = [ + "windows-implement 0.57.0", + "windows-interface 0.57.0", + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link", + "windows-result 0.4.1", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6fc35f58ecd95a9b71c4f2329b911016e6bec66b3f2e6a4aad86bd2e99e2f9b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-implement" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08990546bf4edef8f431fa6326e032865f27138718c587dc21bc0265bbcb57cc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] diff --git a/backend/parsers/windmill-parser-wasm/tests/wasm.rs b/backend/parsers/windmill-parser-wasm/tests/wasm.rs index 204db994d7e2b..14a72b7e2a83b 100644 --- a/backend/parsers/windmill-parser-wasm/tests/wasm.rs +++ b/backend/parsers/windmill-parser-wasm/tests/wasm.rs @@ -140,7 +140,7 @@ export function main(test1?: string, test2: string = \"burkina\", oidx: None } ], - no_main_func: Some(false), + auto_kind: None, has_preprocessor: Some(false) } ); @@ -219,7 +219,7 @@ export function main(test2 = \"burkina\", oidx: None } ], - no_main_func: Some(false), + auto_kind: None, has_preprocessor: Some(false) } ); @@ -270,7 +270,7 @@ export function main(foo: FooBar, {a, b}: FooBar, {c, d}: FooBar = {a: \"foo\", oidx: None } ], - no_main_func: Some(false), + auto_kind: None, has_preprocessor: Some(false) } ); @@ -302,7 +302,7 @@ export function main(foo: (\"foo\" | \"bar\")[]) { has_default: false, oidx: None }], - no_main_func: Some(false), + auto_kind: None, has_preprocessor: Some(false) } ); @@ -446,7 +446,7 @@ Write-Output 'Testing...' oidx: None } ], - no_main_func: None, + auto_kind: None, has_preprocessor: None } ); diff --git a/backend/parsers/windmill-parser-yaml/src/lib.rs b/backend/parsers/windmill-parser-yaml/src/lib.rs index 7c5a6ec43785b..f010d21ab4338 100644 --- a/backend/parsers/windmill-parser-yaml/src/lib.rs +++ b/backend/parsers/windmill-parser-yaml/src/lib.rs @@ -25,7 +25,7 @@ pub fn parse_ansible_sig(inner_content: &str) -> anyhow::Result anyhow::Result, - pub no_main_func: Option, + pub auto_kind: Option, pub has_preprocessor: Option, } diff --git a/backend/src/main.rs b/backend/src/main.rs index 08d59c9dd91ed..1b8009933cf54 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -292,6 +292,7 @@ async fn cache_hub_scripts(file_path: Option) -> anyhow::Result<()> { envs.clone(), false, &mut None, + false, ) .await?; diff --git a/backend/src/monitor.rs b/backend/src/monitor.rs index 803afabf0d800..1ad4fe5fe8228 100644 --- a/backend/src/monitor.rs +++ b/backend/src/monitor.rs @@ -2595,6 +2595,7 @@ async fn handle_zombie_jobs(db: &Pool, base_internal_url: &str, node_n AND running = true AND kind NOT IN ('flow', 'flowpreview', 'flownode', 'singlestepflow') AND same_worker = false + AND q.suspend_until IS NULL AND (zjc.counter IS NULL OR zjc.counter <= $2) FOR UPDATE of q SKIP LOCKED ), @@ -2707,7 +2708,7 @@ async fn handle_zombie_jobs(db: &Pool, base_internal_url: &str, node_n let same_worker_timeout_jobs = { let long_same_worker_jobs = sqlx::query!( "SELECT worker, array_agg(v2_job_queue.id) as ids FROM v2_job_queue LEFT JOIN v2_job ON v2_job_queue.id = v2_job.id LEFT JOIN v2_job_runtime ON v2_job_queue.id = v2_job_runtime.id WHERE v2_job_queue.created_at < now() - ('60 seconds')::interval - AND running = true AND (ping IS NULL OR ping < now() - ('60 seconds')::interval) AND same_worker = true AND worker IS NOT NULL GROUP BY worker", + AND running = true AND (ping IS NULL OR ping < now() - ('60 seconds')::interval) AND same_worker = true AND worker IS NOT NULL AND v2_job_queue.suspend_until IS NULL GROUP BY worker", ) .fetch_all(db) .await @@ -2762,7 +2763,7 @@ async fn handle_zombie_jobs(db: &Pool, base_internal_url: &str, node_n sqlx::query_scalar!("SELECT j.id FROM v2_job_queue q JOIN v2_job j USING (id) LEFT JOIN v2_job_runtime r USING (id) LEFT JOIN v2_job_status s USING (id) WHERE r.ping < now() - ($1 || ' seconds')::interval - AND q.running = true AND j.kind NOT IN ('flow', 'flowpreview', 'flownode', 'singlestepflow') AND j.same_worker = false", + AND q.running = true AND j.kind NOT IN ('flow', 'flowpreview', 'flownode', 'singlestepflow') AND j.same_worker = false AND q.suspend_until IS NULL", ZOMBIE_JOB_TIMEOUT.as_str()) .fetch_all(db) .await diff --git a/backend/summarized_schema.txt b/backend/summarized_schema.txt index 2f86dcb322341..942f518385a27 100644 --- a/backend/summarized_schema.txt +++ b/backend/summarized_schema.txt @@ -146,7 +146,7 @@ resume_job: id(uuid), job(uuid), flow(uuid), created_at(ts), value(jsonb), appro runnable_settings: hash(bigint), debouncing_settings(bigint), concurrency_settings(bigint) schedule: workspace_id(char), path(char), edited_by(char), edited_at(ts), schedule(char), enabled(bool), script_path(char), args(jsonb), extra_perms(jsonb), is_flow(bool), email(char), error(text), timezone(char), on_failure(char), on_recovery(char), on_failure_times(int), on_failure_exact(bool), on_failure_extra_args(jsonb), on_recovery_times(int), on_recovery_extra_args(jsonb), ws_error_handler_muted(bool), retry(jsonb), summary(char), no_flow_overlap(bool), tag(char), paused_until(ts), on_success(char), on_success_extra_args(jsonb), cron_version(text), description(text), dynamic_skip(char) FK: (workspace_id) -> workspace(id) -script: workspace_id(char), hash(bigint), path(char), parent_hashes(bigint[]), summary(text), description(text), content(text), created_by(char), created_at(ts), archived(bool), schema(json), deleted(bool), is_template(bool), extra_perms(jsonb), lock(text), lock_error_logs(text), language(script_lang), kind(script_kind), tag(char), draft_only(bool), envs(char), concurrent_limit(int), concurrency_time_window_s(int), cache_ttl(int), dedicated_worker(bool), ws_error_handler_muted(bool), priority(smallint), timeout(int), delete_after_use(bool), restart_unless_cancelled(bool), concurrency_key(char), visible_to_runner_only(bool), no_main_func(bool), codebase(char), has_preprocessor(bool), on_behalf_of_email(text), schema_validation(bool), assets(jsonb), debounce_key(char), debounce_delay_s(int), cache_ignore_s3_path(bool), runnable_settings_handle(bigint) +script: workspace_id(char), hash(bigint), path(char), parent_hashes(bigint[]), summary(text), description(text), content(text), created_by(char), created_at(ts), archived(bool), schema(json), deleted(bool), is_template(bool), extra_perms(jsonb), lock(text), lock_error_logs(text), language(script_lang), kind(script_kind), tag(char), draft_only(bool), envs(char), concurrent_limit(int), concurrency_time_window_s(int), cache_ttl(int), dedicated_worker(bool), ws_error_handler_muted(bool), priority(smallint), timeout(int), delete_after_use(bool), restart_unless_cancelled(bool), concurrency_key(char), visible_to_runner_only(bool), auto_kind(varchar), codebase(char), has_preprocessor(bool), on_behalf_of_email(text), schema_validation(bool), assets(jsonb), debounce_key(char), debounce_delay_s(int), cache_ignore_s3_path(bool), runnable_settings_handle(bigint) FK: (workspace_id) -> workspace(id) skip_workspace_diff_tally: workspace_id(char), added_at(ts) sqs_trigger: path(char), queue_url(char), aws_resource_path(char), message_attributes(text[]), script_path(char), is_flow(bool), workspace_id(char), edited_by(char), email(char), edited_at(ts), extra_perms(jsonb), error(text), server_id(char), last_server_ping(ts), aws_auth_resource_type(aws_auth_resource_type), error_handler_path(char), error_handler_args(jsonb), retry(jsonb), mode(trigger_mode) diff --git a/backend/tests/agent_workers.rs b/backend/tests/agent_workers.rs index 4f12d5f42c6a2..f422ed3a1295c 100644 --- a/backend/tests/agent_workers.rs +++ b/backend/tests/agent_workers.rs @@ -21,6 +21,7 @@ fn bun_code(code: &str) -> RawCode { concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), + modules: None, } } diff --git a/backend/tests/bun_jobs.rs b/backend/tests/bun_jobs.rs index c69300028d5b6..b2eb89112c75c 100644 --- a/backend/tests/bun_jobs.rs +++ b/backend/tests/bun_jobs.rs @@ -34,6 +34,7 @@ export function main() { cache_ttl: None, cache_ignore_s3_path: None, dedicated_worker: None, + modules: None, }); let result = run_job_in_new_worker_until_complete(&db, false, job, port) @@ -70,6 +71,7 @@ export function main(name: string, count: number) { cache_ttl: None, cache_ignore_s3_path: None, dedicated_worker: None, + modules: None, }); let result = RunJob::from(job) @@ -112,6 +114,7 @@ export function main() { cache_ttl: None, cache_ignore_s3_path: None, dedicated_worker: None, + modules: None, }); let result = run_job_in_new_worker_until_complete(&db, false, job, port) @@ -144,6 +147,7 @@ export function main() { cache_ttl: None, cache_ignore_s3_path: None, dedicated_worker: None, + modules: None, }); let result = run_job_in_new_worker_until_complete(&db, false, job, port) @@ -177,6 +181,7 @@ export function main() { cache_ttl: None, cache_ignore_s3_path: None, dedicated_worker: None, + modules: None, }); let result = run_job_in_new_worker_until_complete(&db, false, job, port) @@ -217,6 +222,7 @@ export async function main() { cache_ttl: None, cache_ignore_s3_path: None, dedicated_worker: None, + modules: None, }); let result = run_job_in_new_worker_until_complete(&db, false, job, port) @@ -256,6 +262,7 @@ export function main() { cache_ttl: None, cache_ignore_s3_path: None, dedicated_worker: None, + modules: None, }); let result = run_job_in_new_worker_until_complete(&db, false, job, port) @@ -288,6 +295,7 @@ export function main() { cache_ttl: None, cache_ignore_s3_path: None, dedicated_worker: None, + modules: None, }); let result = run_job_in_new_worker_until_complete(&db, false, job, port) @@ -330,6 +338,7 @@ export function main() { cache_ttl: None, cache_ignore_s3_path: None, dedicated_worker: None, + modules: None, }); let completed = run_job_in_new_worker_until_complete(&db, false, job, port).await; @@ -370,6 +379,7 @@ export function notMain() { cache_ttl: None, cache_ignore_s3_path: None, dedicated_worker: None, + modules: None, }); let completed = run_job_in_new_worker_until_complete(&db, false, job, port).await; @@ -410,6 +420,7 @@ export function main() { cache_ttl: None, cache_ignore_s3_path: None, dedicated_worker: None, + modules: None, }); let completed = run_job_in_new_worker_until_complete(&db, false, job, port).await; @@ -449,6 +460,7 @@ export function main() { cache_ttl: None, cache_ignore_s3_path: None, dedicated_worker: None, + modules: None, }); let result = run_job_in_new_worker_until_complete(&db, false, job, port) @@ -486,6 +498,7 @@ export function main() { cache_ttl: None, cache_ignore_s3_path: None, dedicated_worker: None, + modules: None, }); let result = run_job_in_new_worker_until_complete(&db, false, job, port) @@ -528,6 +541,7 @@ export function main() { cache_ttl: None, cache_ignore_s3_path: None, dedicated_worker: None, + modules: None, }); let result = run_job_in_new_worker_until_complete(&db, false, job, port) @@ -626,6 +640,7 @@ export function main() { cache_ttl: None, cache_ignore_s3_path: None, dedicated_worker: None, + modules: None, }); let result = run_job_in_new_worker_until_complete(&db, false, job, port) @@ -661,6 +676,7 @@ export function main() { cache_ttl: None, cache_ignore_s3_path: None, dedicated_worker: None, + modules: None, }); let result = run_job_in_new_worker_until_complete(&db, false, job, port) @@ -707,6 +723,7 @@ export function main() { cache_ttl: None, cache_ignore_s3_path: None, dedicated_worker: None, + modules: None, }); let result = run_job_in_new_worker_until_complete(&db, false, job, port) @@ -755,6 +772,7 @@ export function main(x: number) { cache_ttl: None, cache_ignore_s3_path: None, dedicated_worker: None, + modules: None, }); // x=5, main adds 10 = 15 @@ -805,6 +823,7 @@ export function main() { cache_ttl: None, cache_ignore_s3_path: None, dedicated_worker: None, + modules: None, }); let result = run_job_in_new_worker_until_complete(&db, false, job, port) @@ -850,6 +869,7 @@ export function main() { cache_ttl: None, cache_ignore_s3_path: None, dedicated_worker: None, + modules: None, }); let result = run_job_in_new_worker_until_complete(&db, false, job, port) @@ -1175,6 +1195,7 @@ export function main(name: string) { cache_ttl: None, cache_ignore_s3_path: None, dedicated_worker: None, + modules: None, }); let result = RunJob::from(job) @@ -1238,6 +1259,7 @@ export function main(name: string) { cache_ttl: None, cache_ignore_s3_path: None, dedicated_worker: None, + modules: None, }); let result = RunJob::from(job) @@ -1513,6 +1535,7 @@ module.exports.main = function() { cache_ttl: None, cache_ignore_s3_path: None, dedicated_worker: None, + modules: None, }); let result = RunJob::from(job) @@ -1554,6 +1577,7 @@ export function main() { cache_ttl: None, cache_ignore_s3_path: None, dedicated_worker: None, + modules: None, }); let result = RunJob::from(job) @@ -1604,6 +1628,7 @@ module.exports.main = function() { cache_ttl: None, cache_ignore_s3_path: None, dedicated_worker: None, + modules: None, }); use std::sync::atomic::Ordering; @@ -1664,6 +1689,7 @@ export function main() { cache_ttl: None, cache_ignore_s3_path: None, dedicated_worker: None, + modules: None, }); use std::sync::atomic::Ordering; diff --git a/backend/tests/dependency_map.rs b/backend/tests/dependency_map.rs index f3c03aa93226b..ba7b9bd54669d 100644 --- a/backend/tests/dependency_map.rs +++ b/backend/tests/dependency_map.rs @@ -41,11 +41,12 @@ mod dependency_map { deployment_message: None, concurrency_key: None, visible_to_runner_only: None, - no_main_func: None, + auto_kind: None, codebase: None, has_preprocessor: None, on_behalf_of_email: None, assets: vec![], + modules: None, } } async fn init(db: Pool) -> (windmill_api_client::Client, u16, ApiServer) { diff --git a/backend/tests/list_jobs.rs b/backend/tests/list_jobs.rs index 65451bcbb7b12..5db9e3cb14666 100644 --- a/backend/tests/list_jobs.rs +++ b/backend/tests/list_jobs.rs @@ -67,6 +67,7 @@ async fn test_list_jobs_without_include_args(db: Pool) -> anyhow::Resu concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default().into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), + modules: None, })) .arg("x", json!(42)) .push(&db) @@ -123,6 +124,7 @@ async fn test_list_jobs_with_include_args(db: Pool) -> anyhow::Result< concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default().into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), + modules: None, })) .arg("x", json!(42)) .push(&db) @@ -193,6 +195,7 @@ async fn test_list_jobs_completed_with_include_args(db: Pool) -> anyho concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default().into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), + modules: None, })) .arg("x", json!(42)) .run_until_complete(&db, false, port) @@ -265,6 +268,7 @@ async fn test_list_jobs_mixed_queue_and_completed(db: Pool) -> anyhow: concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default().into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), + modules: None, })) .arg("completed_arg", json!("completed_value")) .run_until_complete(&db, false, port) @@ -285,6 +289,7 @@ async fn test_list_jobs_mixed_queue_and_completed(db: Pool) -> anyhow: concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default().into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), + modules: None, })) .arg("queued_arg", json!("queued_value")) .push(&db) @@ -367,6 +372,7 @@ async fn test_list_jobs_multiple_queued_with_include_args(db: Pool) -> concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default().into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), + modules: None, })) .arg("x", json!(1)) .push(&db) @@ -384,6 +390,7 @@ async fn test_list_jobs_multiple_queued_with_include_args(db: Pool) -> concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default().into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), + modules: None, })) .arg("y", json!(2)) .push(&db) @@ -457,6 +464,7 @@ async fn test_queue_list_without_include_args(db: Pool) -> anyhow::Res concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default().into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), + modules: None, })) .arg("x", json!(42)) .push(&db) @@ -519,6 +527,7 @@ async fn test_queue_list_with_include_args(db: Pool) -> anyhow::Result concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default().into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), + modules: None, })) .arg("x", json!(42)) .push(&db) @@ -588,6 +597,7 @@ async fn test_queue_list_multiple_jobs_with_include_args(db: Pool) -> concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default().into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), + modules: None, })) .arg("a", json!("value_a")) .push(&db) @@ -605,6 +615,7 @@ async fn test_queue_list_multiple_jobs_with_include_args(db: Pool) -> concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default().into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), + modules: None, })) .arg("b", json!("value_b")) .push(&db) @@ -686,6 +697,7 @@ async fn test_completed_list_without_include_args(db: Pool) -> anyhow: concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default().into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), + modules: None, })) .arg("x", json!(42)) .run_until_complete(&db, false, port) @@ -751,6 +763,7 @@ async fn test_completed_list_with_include_args(db: Pool) -> anyhow::Re concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default().into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), + modules: None, })) .arg("x", json!(42)) .run_until_complete(&db, false, port) @@ -825,6 +838,7 @@ async fn test_completed_list_multiple_jobs_with_include_args( concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default().into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), + modules: None, })) .arg("a", json!("completed_a")) .run_until_complete(&db, false, port) @@ -845,6 +859,7 @@ async fn test_completed_list_multiple_jobs_with_include_args( concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default().into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), + modules: None, })) .arg("b", json!("completed_b")) .run_until_complete(&db, false, port) diff --git a/backend/tests/nativets_jobs.rs b/backend/tests/nativets_jobs.rs index eaf3a96652f5a..5c57e5c0c844b 100644 --- a/backend/tests/nativets_jobs.rs +++ b/backend/tests/nativets_jobs.rs @@ -39,6 +39,7 @@ fn nativets_code(content: &str) -> JobPayload { cache_ttl: None, cache_ignore_s3_path: None, dedicated_worker: None, + modules: None, }) } diff --git a/backend/tests/nativets_stress.rs b/backend/tests/nativets_stress.rs index 082a43be2cdd3..a175d97af4a75 100644 --- a/backend/tests/nativets_stress.rs +++ b/backend/tests/nativets_stress.rs @@ -156,6 +156,7 @@ async fn push_job(db: &Pool, content: &str, args: &serde_json::Value) cache_ttl: None, cache_ignore_s3_path: None, dedicated_worker: None, + modules: None, }); let tx = PushIsolationLevel::IsolatedRoot(db.clone()); diff --git a/backend/tests/python_jobs.rs b/backend/tests/python_jobs.rs index 22c2313b91223..2ffb85a418e32 100644 --- a/backend/tests/python_jobs.rs +++ b/backend/tests/python_jobs.rs @@ -1,3 +1,4 @@ +use serde_json::json; use sqlx::postgres::Postgres; use sqlx::Pool; use windmill_common::scripts::ScriptLang; @@ -194,6 +195,7 @@ def main(): cache_ttl: None, cache_ignore_s3_path: None, dedicated_worker: None, + modules: None, }); let result = run_job_in_new_worker_until_complete(&db, false, job, port) @@ -245,6 +247,7 @@ def main(): cache_ttl: None, cache_ignore_s3_path: None, dedicated_worker: None, + modules: None, }); let result = run_job_in_new_worker_until_complete(&db, false, job, port) @@ -281,6 +284,7 @@ def main(): cache_ttl: None, cache_ignore_s3_path: None, dedicated_worker: None, + modules: None, }); let result = run_job_in_new_worker_until_complete(&db, false, job, port) @@ -321,6 +325,7 @@ def main(): cache_ttl: None, cache_ignore_s3_path: None, dedicated_worker: None, + modules: None, }); let result = run_job_in_new_worker_until_complete(&db, false, job, port) @@ -359,6 +364,7 @@ def main(): cache_ttl: None, cache_ignore_s3_path: None, dedicated_worker: None, + modules: None, }); let result = run_job_in_new_worker_until_complete(&db, false, job, port) @@ -405,3 +411,54 @@ def main(): run_preview_relative_imports(&db, content, ScriptLang::Python3).await?; Ok(()) } + +#[cfg(feature = "python")] +#[sqlx::test(fixtures("base"))] +async fn test_python_wac_v2_with_args(db: Pool) -> anyhow::Result<()> { + initialize_tracing().await; + let server = ApiServer::start(db.clone()).await?; + let port = server.addr.port(); + + let content = r#" +from wmill import task, workflow + +@task() +def greet(label: str, count: int) -> str: + return f"hello {label} x{count}" + +@workflow +async def main(item: str, qty: int, email: str): + greeting = await greet(item, qty) + return {"item": item, "qty": qty, "email": email, "greeting": greeting} +"# + .to_string(); + + // WAC requires at least 2 workers (parent + task sub-jobs) + let db = &db; + in_test_worker( + db, + async move { + let job = Box::pin( + RunJob::from(JobPayload::Code(RawCode { + language: ScriptLang::Python3, + content, + ..RawCode::default() + })) + .arg("item", json!("widget")) + .arg("qty", json!(5)) + .arg("email", json!("test@example.com")) + .run_until_complete(db, false, port), + ) + .await; + + let result = job.json_result().unwrap(); + assert_eq!(result["item"], json!("widget")); + assert_eq!(result["qty"], json!(5)); + assert_eq!(result["email"], json!("test@example.com")); + assert_eq!(result["greeting"], json!("hello widget x5")); + }, + port, + ) + .await; + Ok(()) +} diff --git a/backend/tests/relock_skip.rs b/backend/tests/relock_skip.rs index ce274796bc6e1..9aae97adf3c5d 100644 --- a/backend/tests/relock_skip.rs +++ b/backend/tests/relock_skip.rs @@ -39,11 +39,12 @@ mod relock_skip { deployment_message: None, concurrency_key: None, visible_to_runner_only: None, - no_main_func: None, + auto_kind: None, codebase: None, has_preprocessor: None, on_behalf_of_email: None, assets: vec![], + modules: None, } } @@ -57,13 +58,10 @@ mod relock_skip { pattern: &str, after: chrono::DateTime, ) -> i64 { - let logs = sqlx::query_scalar!( - "SELECT logs FROM job_logs WHERE created_at > $1", - after - ) - .fetch_all(db) - .await - .unwrap(); + let logs = sqlx::query_scalar!("SELECT logs FROM job_logs WHERE created_at > $1", after) + .fetch_all(db) + .await + .unwrap(); logs.iter() .filter_map(|l| l.as_ref()) @@ -270,8 +268,14 @@ def main(): // We allow up to 3 skips from cascade re-triggers. let skipping_count = count_pattern_in_job_logs(&db, "Skipping relock", before).await; let relocking_count = count_pattern_in_job_logs(&db, "Relocking", before).await; - assert!(skipping_count <= 3, "First deployment should have at most 3 skips from cascade"); - assert!(relocking_count >= 3, "First deployment should have at least 3 relocking jobs"); + assert!( + skipping_count <= 3, + "First deployment should have at most 3 skips from cascade" + ); + assert!( + relocking_count >= 3, + "First deployment should have at least 3 relocking jobs" + ); // Step 2: Redeploy default workspace deps again - should SKIP let before = chrono::Utc::now(); @@ -289,7 +293,10 @@ def main(): in_test_worker(&db, wait_for_jobs_ge(&mut completed, 10), port).await; let skipping_count = count_pattern_in_job_logs(&db, "Skipping relock", before).await; - assert!(skipping_count >= 3, "Second deployment of same content should skip at least 3 times"); + assert!( + skipping_count >= 3, + "Second deployment of same content should skip at least 3 times" + ); // Step 3: Redeploy default workspace deps with different content - should NOT skip let before = chrono::Utc::now(); @@ -308,8 +315,14 @@ def main(): let skipping_count = count_pattern_in_job_logs(&db, "Skipping relock", before).await; let relocking_count = count_pattern_in_job_logs(&db, "Relocking", before).await; - assert!(skipping_count <= 4, "Changed content should have at most 3 skips from cascade"); - assert!(relocking_count >= 3, "Changed content should trigger at least 3 relocking jobs"); + assert!( + skipping_count <= 4, + "Changed content should have at most 3 skips from cascade" + ); + assert!( + relocking_count >= 3, + "Changed content should trigger at least 3 relocking jobs" + ); // Step 4: Deploy named workspace deps first time - should relock (no hash exists yet) // Named deps trigger exactly 3 independent objects with no cascade @@ -329,8 +342,14 @@ def main(): let skipping_count = count_pattern_in_job_logs(&db, "Skipping relock", before).await; let relocking_count = count_pattern_in_job_logs(&db, "Relocking", before).await; - assert_eq!(skipping_count, 0, "Named workspace deps first deployment should not skip"); - assert!(relocking_count > 0, "Named workspace deps first deployment should relock"); + assert_eq!( + skipping_count, 0, + "Named workspace deps first deployment should not skip" + ); + assert!( + relocking_count > 0, + "Named workspace deps first deployment should relock" + ); // Step 5: Deploy named workspace deps again with no change - should SKIP let before = chrono::Utc::now(); @@ -350,8 +369,14 @@ def main(): let skipping_count = count_pattern_in_job_logs(&db, "Skipping relock", before).await; let relocking_count = count_pattern_in_job_logs(&db, "Relocking", before).await; - assert!(skipping_count > 0, "Named workspace deps second deployment should skip"); - assert_eq!(relocking_count, 0, "Named workspace deps second deployment should not relock"); + assert!( + skipping_count > 0, + "Named workspace deps second deployment should skip" + ); + assert_eq!( + relocking_count, 0, + "Named workspace deps second deployment should not relock" + ); // Step 6: Deploy named workspace deps with small change - should NOT skip let before = chrono::Utc::now(); @@ -370,8 +395,14 @@ def main(): let skipping_count = count_pattern_in_job_logs(&db, "Skipping relock", before).await; let relocking_count = count_pattern_in_job_logs(&db, "Relocking", before).await; - assert_eq!(skipping_count, 0, "Named workspace deps with change should not skip"); - assert!(relocking_count > 0, "Named workspace deps with change should relock"); + assert_eq!( + skipping_count, 0, + "Named workspace deps with change should not skip" + ); + assert!( + relocking_count > 0, + "Named workspace deps with change should relock" + ); Ok(()) } diff --git a/backend/tests/script_modules.rs b/backend/tests/script_modules.rs new file mode 100644 index 0000000000000..068555c7df7d7 --- /dev/null +++ b/backend/tests/script_modules.rs @@ -0,0 +1,158 @@ +use serde_json::json; +use sqlx::postgres::Postgres; +use sqlx::Pool; +use std::collections::HashMap; +use windmill_common::jobs::{JobPayload, RawCode}; +use windmill_common::scripts::{ScriptLang, ScriptModule}; +use windmill_test_utils::*; + +// ============================================================================ +// Python: script with inline module via relative import +// ============================================================================ + +#[cfg(feature = "python")] +#[sqlx::test(fixtures("base"))] +async fn test_python_script_with_module(db: Pool) -> anyhow::Result<()> { + initialize_tracing().await; + let server = ApiServer::start(db.clone()).await?; + let port = server.addr.port(); + + let main_content = r#" +from .helper import greet + +def main(name: str): + return greet(name) +"# + .to_owned(); + + let mut modules = HashMap::new(); + modules.insert( + "helper.py".to_string(), + ScriptModule { + content: "def greet(name):\n return f\"hello {name}\"\n".to_string(), + language: ScriptLang::Python3, + lock: None, + }, + ); + + let job = JobPayload::Code(RawCode { + content: main_content, + path: Some("f/test/my_script".to_string()), + language: ScriptLang::Python3, + modules: Some(modules), + ..RawCode::default() + }); + + let result = RunJob::from(job) + .arg("name", json!("world")) + .run_until_complete(&db, false, port) + .await + .json_result() + .unwrap(); + + assert_eq!(result, json!("hello world")); + Ok(()) +} + +// ============================================================================ +// Python: nested module path (subdirectory) +// ============================================================================ + +#[cfg(feature = "python")] +#[sqlx::test(fixtures("base"))] +async fn test_python_script_with_nested_module(db: Pool) -> anyhow::Result<()> { + initialize_tracing().await; + let server = ApiServer::start(db.clone()).await?; + let port = server.addr.port(); + + let main_content = r#" +from .utils.math import add + +def main(a: int, b: int): + return add(a, b) +"# + .to_owned(); + + let mut modules = HashMap::new(); + modules.insert( + "utils/__init__.py".to_string(), + ScriptModule { content: "".to_string(), language: ScriptLang::Python3, lock: None }, + ); + modules.insert( + "utils/math.py".to_string(), + ScriptModule { + content: "def add(a, b):\n return a + b\n".to_string(), + language: ScriptLang::Python3, + lock: None, + }, + ); + + let job = JobPayload::Code(RawCode { + content: main_content, + path: Some("f/test/my_script".to_string()), + language: ScriptLang::Python3, + modules: Some(modules), + ..RawCode::default() + }); + + let result = RunJob::from(job) + .arg("a", json!(3)) + .arg("b", json!(4)) + .run_until_complete(&db, false, port) + .await + .json_result() + .unwrap(); + + assert_eq!(result, json!(7)); + Ok(()) +} + +// ============================================================================ +// Bun: script with inline module via relative import +// ============================================================================ + +#[sqlx::test(fixtures("base"))] +async fn test_bun_script_with_module(db: Pool) -> anyhow::Result<()> { + initialize_tracing().await; + let server = ApiServer::start(db.clone()).await?; + let port = server.addr.port(); + + let main_content = r#" +import { greet } from "./helper.ts"; + +export function main(name: string) { + return greet(name); +} +"# + .to_owned(); + + let mut modules = HashMap::new(); + modules.insert( + "helper.ts".to_string(), + ScriptModule { + content: + "export function greet(name: string): string {\n return `hello ${name}`;\n}\n" + .to_string(), + language: ScriptLang::Bun, + lock: None, + }, + ); + + let job = JobPayload::Code(RawCode { + content: main_content, + path: Some("f/test/my_script".to_string()), + language: ScriptLang::Bun, + modules: Some(modules), + ..RawCode::default() + }); + + let result = RunJob::from(job) + .arg("name", json!("world")) + .run_until_complete(&db, false, port) + .await + .json_result() + .unwrap(); + + assert_eq!(result, json!("hello world")); + Ok(()) +} diff --git a/backend/tests/volume_tests.rs b/backend/tests/volume_tests.rs index 9df30c561511d..27f82dfb386cb 100644 --- a/backend/tests/volume_tests.rs +++ b/backend/tests/volume_tests.rs @@ -596,6 +596,7 @@ export function main() { concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), + modules: None, }); let result = run_job_in_new_worker_until_complete(&db, false, job, port).await; diff --git a/backend/tests/worker.rs b/backend/tests/worker.rs index 8f338bc328231..621ec9ceb5f17 100644 --- a/backend/tests/worker.rs +++ b/backend/tests/worker.rs @@ -867,6 +867,7 @@ func main(derp string) (string, error) { concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), + modules: None, })) .arg("derp", json!("world")) .run_until_complete(&db, false, port) @@ -905,6 +906,7 @@ fn main(world: String) -> Result { debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), cache_ttl: None, dedicated_worker: None, + modules: None, })) .arg("world", json!("Hyrule")) .run_until_complete(&db, false, port) @@ -949,6 +951,7 @@ class Script concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), + modules: None, })) .arg("world", json!("Arakis")) .arg("b", json!(3)) @@ -985,6 +988,7 @@ echo "hello $msg" concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), + modules: None, })) .arg("msg", json!("world")) .run_until_complete(&db, false, port) @@ -1022,6 +1026,7 @@ echo "$result" concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), + modules: None, })) .run_until_complete(&db, false, port) .await; @@ -1056,6 +1061,7 @@ echo "$result" concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), + modules: None, })) .run_until_complete(&db, false, port) .await; @@ -1093,6 +1099,7 @@ def main [ msg: string ] { concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), + modules: None, })) .arg("msg", json!("world")) .run_until_complete(&db, false, port) @@ -1147,6 +1154,7 @@ def main [ concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), + modules: None, })) .arg("a", json!("3")) .arg("b", json!("null")) @@ -1210,6 +1218,7 @@ public class Main { concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), + modules: None, })) .arg("a", json!(3)) .arg("b", json!(3.0)) @@ -1247,6 +1256,7 @@ export async function main(name: string): Promise { concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), + modules: None, })) .arg("name", json!("world")) .run_until_complete(&db, false, port) @@ -1284,6 +1294,7 @@ export async function main(a: number, b: number): Promise { concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), + modules: None, })) .arg("a", json!(3)) .arg("b", json!(7)) @@ -1322,6 +1333,7 @@ export async function main(items: string[]): Promise<{ count: number; items: str concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), + modules: None, })) .arg("items", json!(["a", "b", "c"])) .run_until_complete(&db, false, port) @@ -1360,6 +1372,7 @@ export async function main(a: Date) { concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), + modules: None, })) .arg("a", json!("2024-09-24T10:00:00.000Z")) .run_until_complete(&db, false, port) @@ -1395,6 +1408,7 @@ SELECT 'hello ' || $1::text AS result; concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), + modules: None, })) .arg("name", json!("world")) .arg( @@ -1435,6 +1449,7 @@ SELECT ? AS result; concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), + modules: None, })) .arg("name", json!("world")) .arg( @@ -1475,6 +1490,7 @@ export async function main(name: string): Promise { concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), + modules: None, })) .arg("name", json!("world")) .run_until_complete(&db, false, port) @@ -1510,6 +1526,7 @@ Write-Output "hello $msg" concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), + modules: None, })) .arg("msg", json!("world")) .run_until_complete(&db, false, port) @@ -1546,6 +1563,7 @@ Write-Output "$Name-$Count" concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), + modules: None, })) .arg("Name", json!("test")) .arg("Count", json!(7)) @@ -1580,6 +1598,7 @@ throw "intentional error" concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), + modules: None, })) .arg("x", json!(1)) .run_until_complete(&db, false, port) @@ -1632,6 +1651,7 @@ function main(string $name): string { concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), + modules: None, })) .arg("name", json!("world")) .run_until_complete(&db, false, port) @@ -1669,6 +1689,7 @@ end concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), + modules: None, })) .arg("name", json!("world")) .run_until_complete(&db, false, port) @@ -1705,6 +1726,7 @@ export async function main(a: Date) { concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), + modules: None, })) .arg("a", json!("2024-09-24T10:00:00.000Z")) .run_until_complete(&db, false, port) @@ -1741,6 +1763,7 @@ export async function main(a: Date) { concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), + modules: None, })) .arg("a", json!("2024-09-24T10:00:00.000Z")) .run_until_complete(&db, false, port) @@ -1793,6 +1816,7 @@ export function main(name: string) { cache_ttl: None, cache_ignore_s3_path: None, dedicated_worker: None, + modules: None, })) .arg("name", json!("World")) .run_until_complete(&db, false, port) @@ -1838,6 +1862,7 @@ def main(a: datetime, b: bytes): concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default() .into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), + modules: None, })) .arg("a", json!("2024-09-24T10:00:00.000Z")) .arg("b", json!("dGVzdA==")) diff --git a/backend/windmill-api-client/src/lib.rs b/backend/windmill-api-client/src/lib.rs index 4e37958b7eb7c..f2170b3c2f2bb 100644 --- a/backend/windmill-api-client/src/lib.rs +++ b/backend/windmill-api-client/src/lib.rs @@ -17,10 +17,7 @@ pub struct Client { impl Client { /// Create a new client with an existing reqwest::Client pub fn new_with_client(baseurl: &str, client: reqwest::Client) -> Self { - Self { - baseurl: baseurl.to_string(), - client, - } + Self { baseurl: baseurl.to_string(), client } } /// Get the base URL @@ -49,7 +46,10 @@ impl Client { if response.status().is_success() { Ok(response.text().await?) } else { - Err(Error::UnexpectedResponse(response.status().as_u16(), response.text().await.unwrap_or_default())) + Err(Error::UnexpectedResponse( + response.status().as_u16(), + response.text().await.unwrap_or_default(), + )) } } @@ -69,7 +69,10 @@ impl Client { if response.status().is_success() { Ok(response.text().await?) } else { - Err(Error::UnexpectedResponse(response.status().as_u16(), response.text().await.unwrap_or_default())) + Err(Error::UnexpectedResponse( + response.status().as_u16(), + response.text().await.unwrap_or_default(), + )) } } @@ -97,7 +100,10 @@ impl Client { if response.status().is_success() { Ok(response.json().await?) } else { - Err(Error::UnexpectedResponse(response.status().as_u16(), response.text().await.unwrap_or_default())) + Err(Error::UnexpectedResponse( + response.status().as_u16(), + response.text().await.unwrap_or_default(), + )) } } @@ -117,7 +123,10 @@ impl Client { if response.status().is_success() { Ok(response.text().await?) } else { - Err(Error::UnexpectedResponse(response.status().as_u16(), response.text().await.unwrap_or_default())) + Err(Error::UnexpectedResponse( + response.status().as_u16(), + response.text().await.unwrap_or_default(), + )) } } @@ -139,7 +148,10 @@ impl Client { if response.status().is_success() { Ok(response.text().await?) } else { - Err(Error::UnexpectedResponse(response.status().as_u16(), response.text().await.unwrap_or_default())) + Err(Error::UnexpectedResponse( + response.status().as_u16(), + response.text().await.unwrap_or_default(), + )) } } @@ -151,7 +163,10 @@ impl Client { if response.status().is_success() { Ok(response.json().await?) } else { - Err(Error::UnexpectedResponse(response.status().as_u16(), response.text().await.unwrap_or_default())) + Err(Error::UnexpectedResponse( + response.status().as_u16(), + response.text().await.unwrap_or_default(), + )) } } } @@ -367,7 +382,7 @@ pub mod types { #[serde(default, skip_serializing_if = "Option::is_none")] pub lock: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub no_main_func: Option, + pub auto_kind: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub on_behalf_of_email: Option, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -388,6 +403,8 @@ pub mod types { pub visible_to_runner_only: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub ws_error_handler_muted: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub modules: Option>, } /// Script arguments (used in schedules) @@ -555,12 +572,12 @@ pub mod types { Static { #[serde(rename = "type")] type_: String, - value: serde_json::Value + value: serde_json::Value, }, Javascript { #[serde(rename = "type")] type_: String, - expr: String + expr: String, }, } diff --git a/backend/windmill-api-scripts/src/scripts.rs b/backend/windmill-api-scripts/src/scripts.rs index d93b33670dec2..e68d352a02b84 100644 --- a/backend/windmill-api-scripts/src/scripts.rs +++ b/backend/windmill-api-scripts/src/scripts.rs @@ -65,7 +65,8 @@ use windmill_common::{ schema::should_validate_schema, scripts::{ to_i64, HubScript, ListScriptQuery, ListableScript, NewScript, Schema, Script, ScriptHash, - ScriptHistory, ScriptHistoryUpdate, ScriptKind, ScriptLang, ScriptWithStarred, + ScriptHistory, ScriptHistoryUpdate, ScriptKind, ScriptLang, ScriptModule, + ScriptWithStarred, }, users::username_to_permissioned_as, utils::{not_found_if_none, query_elems_from_hub, require_admin, Pagination, StripPath}, @@ -116,7 +117,7 @@ pub struct ScriptWDraft { #[serde(skip_serializing_if = "Option::is_none")] pub visible_to_runner_only: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub no_main_func: Option, + pub auto_kind: Option, #[serde(skip_serializing_if = "Option::is_none")] pub has_preprocessor: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -124,6 +125,9 @@ pub struct ScriptWDraft { #[serde(skip_serializing_if = "Option::is_none")] #[sqlx(json(nullable))] pub assets: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + #[sqlx(json(nullable))] + pub modules: Option>, #[serde(flatten)] #[sqlx(flatten)] pub runnable_settings: SR, @@ -174,10 +178,11 @@ impl ScriptWDraft { delete_after_use: self.delete_after_use, timeout: self.timeout, visible_to_runner_only: self.visible_to_runner_only, - no_main_func: self.no_main_func, + auto_kind: self.auto_kind, has_preprocessor: self.has_preprocessor, on_behalf_of_email: self.on_behalf_of_email, assets: self.assets, + modules: self.modules, }) } } @@ -295,7 +300,7 @@ async fn list_scripts( "draft.path IS NOT NULL as has_draft", "draft_only", "ws_error_handler_muted", - "no_main_func", + "auto_kind", "codebase IS NOT NULL as use_codebase", "kind" ]) @@ -330,7 +335,7 @@ async fn list_scripts( { // only include scripts that have a main function // do not hide scripts without main if preprocessor is in the kinds - sqlb.and_where("o.no_main_func IS NOT TRUE"); + sqlb.and_where("o.auto_kind IS NULL"); } if !lq.include_draft_only.unwrap_or(false) || authed.is_operator { @@ -825,21 +830,24 @@ async fn create_script_internal<'c>( let validate_schema = should_validate_schema(&ns.content, &ns.language); - let (no_main_func, has_preprocessor) = if matches!(ns.kind, Some(ScriptKind::Preprocessor)) { - (ns.no_main_func, ns.has_preprocessor) + let (auto_kind, has_preprocessor) = if matches!(ns.kind, Some(ScriptKind::Preprocessor)) { + (ns.auto_kind.clone(), ns.has_preprocessor) } else { match lang { ScriptLang::Bun | ScriptLang::Bunnative | ScriptLang::Deno | ScriptLang::Nativets => { let args = windmill_parser_ts::parse_deno_signature(&ns.content, true, true, None); match args { - Ok(args) => (args.no_main_func, args.has_preprocessor), + Ok(args) => ( + ns.auto_kind.clone().or(args.auto_kind), + args.has_preprocessor, + ), Err(e) => { tracing::warn!( "Error parsing deno signature when deploying script {}: {:?}", ns.path, e ); - (None, None) + (ns.auto_kind.clone(), None) } } } @@ -847,18 +855,21 @@ async fn create_script_internal<'c>( ScriptLang::Python3 => { let args = windmill_parser_py::parse_python_signature(&ns.content, None, true); match args { - Ok(args) => (args.no_main_func, args.has_preprocessor), + Ok(args) => ( + ns.auto_kind.clone().or(args.auto_kind), + args.has_preprocessor, + ), Err(e) => { tracing::warn!( "Error parsing python signature when deploying script {}: {:?}", ns.path, e ); - (None, None) + (ns.auto_kind.clone(), None) } } } - _ => (ns.no_main_func, ns.has_preprocessor), + _ => (ns.auto_kind.clone(), ns.has_preprocessor), } }; @@ -894,8 +905,8 @@ async fn create_script_internal<'c>( content, created_by, schema, is_template, extra_perms, lock, language, kind, tag, \ draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, \ dedicated_worker, ws_error_handler_muted, priority, restart_unless_cancelled, \ - delete_after_use, timeout, concurrency_key, visible_to_runner_only, no_main_func, codebase, has_preprocessor, on_behalf_of_email, schema_validation, assets, debounce_key, debounce_delay_s, cache_ignore_s3_path, runnable_settings_handle) \ - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38)", + delete_after_use, timeout, concurrency_key, visible_to_runner_only, auto_kind, codebase, has_preprocessor, on_behalf_of_email, schema_validation, assets, debounce_key, debounce_delay_s, cache_ignore_s3_path, runnable_settings_handle, modules) \ + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38, $39)", &w_id, &hash.0, ns.path, @@ -924,7 +935,7 @@ async fn create_script_internal<'c>( ns.timeout, guarded_concurrency_key, ns.visible_to_runner_only, - no_main_func.filter(|x: &bool| *x), // should be Some(true) or None + auto_kind.as_deref(), codebase, has_preprocessor.filter(|x: &bool| *x), // should be Some(true) or None windmill_common::resolve_on_behalf_of_email( @@ -937,7 +948,8 @@ async fn create_script_internal<'c>( guarded_debounce_key, guarded_debounce_delay_s, ns.cache_ignore_s3_path, - runnable_settings_handle + runnable_settings_handle, + ns.modules.as_ref().and_then(|m| serde_json::to_value(m).ok()) ) .execute(&mut *tx) .await?; @@ -1387,7 +1399,7 @@ async fn get_script_by_path_w_draft( let mut tx = user_db.begin(&authed).await?; let script_o = sqlx::query_as::<_, ScriptWDraft>( - "SELECT hash, script.path, summary, description, content, language, kind, tag, schema, draft_only, envs, runnable_settings_handle, concurrent_limit, concurrency_time_window_s, cache_ttl, cache_ignore_s3_path, ws_error_handler_muted, draft.value as draft, dedicated_worker, priority, restart_unless_cancelled, delete_after_use, timeout, concurrency_key, visible_to_runner_only, no_main_func, has_preprocessor, on_behalf_of_email, assets, debounce_key, debounce_delay_s FROM script LEFT JOIN draft ON + "SELECT hash, script.path, summary, description, content, language, kind, tag, schema, draft_only, envs, runnable_settings_handle, concurrent_limit, concurrency_time_window_s, cache_ttl, cache_ignore_s3_path, ws_error_handler_muted, draft.value as draft, dedicated_worker, priority, restart_unless_cancelled, delete_after_use, timeout, concurrency_key, visible_to_runner_only, auto_kind, has_preprocessor, on_behalf_of_email, assets, modules, debounce_key, debounce_delay_s FROM script LEFT JOIN draft ON script.path = draft.path AND script.workspace_id = draft.workspace_id AND draft.typ = 'script' WHERE script.path = $1 AND script.workspace_id = $2 ORDER BY script.created_at DESC LIMIT 1", diff --git a/backend/windmill-api-workspaces/src/workspaces.rs b/backend/windmill-api-workspaces/src/workspaces.rs index a85383e496488..4747d209afb76 100644 --- a/backend/windmill-api-workspaces/src/workspaces.rs +++ b/backend/windmill-api-workspaces/src/workspaces.rs @@ -3099,8 +3099,8 @@ async fn clone_scripts( envs, concurrent_limit, concurrency_time_window_s, cache_ttl, dedicated_worker, ws_error_handler_muted, priority, timeout, delete_after_use, restart_unless_cancelled, concurrency_key, - visible_to_runner_only, no_main_func, codebase, has_preprocessor, - on_behalf_of_email, assets + visible_to_runner_only, auto_kind, codebase, has_preprocessor, + on_behalf_of_email, assets, modules ) SELECT $1, hash, path, parent_hashes, summary, description, content, @@ -3109,8 +3109,8 @@ async fn clone_scripts( envs, concurrent_limit, concurrency_time_window_s, cache_ttl, dedicated_worker, ws_error_handler_muted, priority, timeout, delete_after_use, restart_unless_cancelled, concurrency_key, - visible_to_runner_only, no_main_func, codebase, has_preprocessor, - on_behalf_of_email, assets + visible_to_runner_only, auto_kind, codebase, has_preprocessor, + on_behalf_of_email, assets, modules FROM script WHERE workspace_id = $2"#, target_workspace_id, diff --git a/backend/windmill-api/openapi-deref.yaml b/backend/windmill-api/openapi-deref.yaml index 7f04a7287e02f..ef3bdbf983a15 100644 --- a/backend/windmill-api/openapi-deref.yaml +++ b/backend/windmill-api/openapi-deref.yaml @@ -9683,8 +9683,8 @@ paths: type: boolean visible_to_runner_only: type: boolean - no_main_func: - type: boolean + auto_kind: + type: string codebase: type: string has_preprocessor: @@ -9706,7 +9706,6 @@ paths: - language - kind - starred - - no_main_func - has_preprocessor /w/{workspace}/scripts/list_paths: get: @@ -9904,8 +9903,8 @@ paths: type: integer visible_to_runner_only: type: boolean - no_main_func: - type: boolean + auto_kind: + type: string codebase: type: string has_preprocessor: @@ -29774,8 +29773,8 @@ components: required: - name - typ - no_main_func: - type: boolean + auto_kind: + type: string nullable: true has_preprocessor: type: boolean @@ -29786,7 +29785,7 @@ components: - args - type - error - - no_main_func + - auto_kind - has_preprocessor ScriptLang: type: string diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 7158f315ec0e8..1569aff3433b4 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -18775,14 +18775,20 @@ components: type: boolean visible_to_runner_only: type: boolean - no_main_func: - type: boolean + auto_kind: + type: string codebase: type: string has_preprocessor: type: boolean on_behalf_of_email: type: string + modules: + type: object + nullable: true + description: "Additional script modules keyed by relative file path" + additionalProperties: + $ref: "#/components/schemas/ScriptModule" required: - hash @@ -18799,7 +18805,6 @@ components: - language - kind - starred - - no_main_func - has_preprocessor NewScript: @@ -18872,8 +18877,8 @@ components: type: integer visible_to_runner_only: type: boolean - no_main_func: - type: boolean + auto_kind: + type: string codebase: type: string has_preprocessor: @@ -18901,6 +18906,12 @@ components: alt_access_type: type: string enum: [r, w, rw] + modules: + type: object + nullable: true + description: "Additional script modules keyed by relative file path" + additionalProperties: + $ref: "#/components/schemas/ScriptModule" required: - path @@ -20050,8 +20061,8 @@ components: required: - name - typ - no_main_func: - type: boolean + auto_kind: + type: string nullable: true has_preprocessor: type: boolean @@ -20062,7 +20073,7 @@ components: - args - type - error - - no_main_func + - auto_kind - has_preprocessor ScriptLang: @@ -20094,6 +20105,23 @@ components: # for related places search: ADD_NEW_LANG ] + ScriptModule: + type: object + description: "An additional module file associated with a script" + properties: + content: + type: string + description: "The source code content of this module" + language: + $ref: "#/components/schemas/ScriptLang" + lock: + type: string + nullable: true + description: "Lock file content for this module's dependencies" + required: + - content + - language + Preview: type: object properties: @@ -20121,6 +20149,12 @@ components: type: string flow_path: type: string + modules: + type: object + nullable: true + description: "Additional script modules keyed by relative file path" + additionalProperties: + $ref: "#/components/schemas/ScriptModule" required: - args diff --git a/backend/windmill-api/src/jobs.rs b/backend/windmill-api/src/jobs.rs index a9cb930b8fcf3..29b9765480acf 100644 --- a/backend/windmill-api/src/jobs.rs +++ b/backend/windmill-api/src/jobs.rs @@ -44,7 +44,7 @@ use windmill_common::runnable_settings::{ }; #[cfg(feature = "run_inline")] use windmill_common::runtime_assets::{register_runtime_asset, InsertRuntimeAssetParams}; -use windmill_common::scripts::ScriptRunnableSettingsInline; +use windmill_common::scripts::{ScriptModule, ScriptRunnableSettingsInline}; use windmill_common::triggers::TriggerMetadata; use windmill_common::utils::{RunnableKind, WarnAfterExt}; use windmill_common::worker::{Connection, CLOUD_HOSTED, WINDMILL_DIR}; @@ -2948,6 +2948,7 @@ struct Preview { lock: Option, format: Option, flow_path: Option, + modules: Option>, } #[cfg(feature = "run_inline")] @@ -3608,6 +3609,7 @@ pub async fn run_workflow_as_code( dedicated_worker: None, // TODO(debouncing): enable for this mode debouncing_settings: DebouncingSettings::default(), + modules: None, }), Some(job.tag.clone()), None, @@ -4615,12 +4617,15 @@ async fn run_preview_script( let tx = PushIsolationLevel::Isolated(user_db.clone(), authed.clone().into()); let preview_args = preview.args.unwrap_or_default(); - let flow_path_extra = preview.flow_path.map(|fp| { - let mut extra = HashMap::new(); - extra.insert("_FLOW_PATH".to_string(), to_raw_value(&fp)); - extra - }); - let push_args = PushArgs { extra: flow_path_extra, args: &preview_args }; + let mut extra = HashMap::new(); + if let Some(fp) = &preview.flow_path { + extra.insert("_FLOW_PATH".to_string(), to_raw_value(fp)); + } + if let Some(ref modules) = preview.modules { + extra.insert("_MODULES".to_string(), to_raw_value(modules)); + } + let extra = if extra.is_empty() { None } else { Some(extra) }; + let push_args = PushArgs { extra, args: &preview_args }; let (uuid, tx) = push( &db, @@ -4643,6 +4648,7 @@ async fn run_preview_script( cache_ttl: None, cache_ignore_s3_path: None, dedicated_worker: preview.dedicated_worker, + modules: preview.modules, }), }, push_args, @@ -4984,6 +4990,7 @@ async fn run_bundle_preview_script( dedicated_worker: preview.dedicated_worker, concurrency_settings: ConcurrencySettingsWithCustom::default(), debouncing_settings: DebouncingSettings::default(), + modules: None, }), PushArgs::from(&args), authed.display_username(), @@ -5777,6 +5784,7 @@ async fn run_dynamic_select( dedicated_worker: None, concurrency_settings: ConcurrencySettings::default().into(), debouncing_settings: DebouncingSettings::default(), + modules: None, }), PushArgs::from(&request.args.unwrap_or_default()), authed.display_username(), diff --git a/backend/windmill-api/src/mcp/utils.rs b/backend/windmill-api/src/mcp/utils.rs index 1cfbfd7085db6..0361dd37e78d8 100644 --- a/backend/windmill-api/src/mcp/utils.rs +++ b/backend/windmill-api/src/mcp/utils.rs @@ -151,11 +151,14 @@ pub async fn get_items sqlx::FromRow<'a, sqlx::postgres::PgRow> + Sen .and_where("o.draft_only IS NOT TRUE"); if item_type == "script" { - sqlb.and_where("(o.no_main_func IS NOT TRUE OR o.no_main_func IS NULL)"); + sqlb.and_where("o.auto_kind IS NULL"); } if let Some(prefix) = path_prefix { - let escaped = prefix.replace('\\', "\\\\").replace('%', "\\%").replace('_', "\\_"); + let escaped = prefix + .replace('\\', "\\\\") + .replace('%', "\\%") + .replace('_', "\\_"); sqlb.and_where("o.path LIKE ? ESCAPE '\\'".bind(&format!("{}%", escaped))); } diff --git a/backend/windmill-api/src/workspaces_export.rs b/backend/windmill-api/src/workspaces_export.rs index 70e676b5b4295..4fe6b8b58c270 100644 --- a/backend/windmill-api/src/workspaces_export.rs +++ b/backend/windmill-api/src/workspaces_export.rs @@ -89,14 +89,20 @@ struct ScriptMetadata { pub restart_unless_cancelled: Option, #[serde(skip_serializing_if = "Option::is_none")] pub visible_to_runner_only: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub no_main_func: Option, + // auto_kind is intentionally excluded from export — it is auto-detected by the + // parser at deploy time from the script content (workflow/task patterns for "wac", + // no main function for "lib"). + #[serde(skip_serializing)] + #[allow(dead_code)] + pub auto_kind: Option, #[serde(skip_serializing_if = "Option::is_none")] pub codebase: Option, #[serde(skip_serializing_if = "Option::is_none")] pub has_preprocessor: Option, #[serde(skip_serializing_if = "Option::is_none")] pub on_behalf_of_email: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub modules: Option>, #[serde(flatten)] pub concurrency_settings: ConcurrencySettings, #[serde(flatten)] @@ -503,10 +509,11 @@ pub(crate) async fn tarball_workspace( delete_after_use: script.delete_after_use, restart_unless_cancelled: script.restart_unless_cancelled, visible_to_runner_only: script.visible_to_runner_only, - no_main_func: script.no_main_func, + auto_kind: script.auto_kind, codebase: script.codebase, has_preprocessor: script.has_preprocessor, on_behalf_of_email: script.on_behalf_of_email, + modules: script.modules, }; let metadata_str = serde_json::to_string_pretty(&metadata).unwrap(); archive diff --git a/backend/windmill-common/src/cache.rs b/backend/windmill-common/src/cache.rs index 27af55ea01416..89010558893fa 100644 --- a/backend/windmill-common/src/cache.rs +++ b/backend/windmill-common/src/cache.rs @@ -12,7 +12,7 @@ use crate::{ error, flows::{FlowNodeId, FlowValue}, schema::SchemaValidator, - scripts::{ScriptHash, ScriptLang}, + scripts::{ScriptHash, ScriptLang, ScriptModule}, }; use anyhow::anyhow; use serde_json::value::to_raw_value; @@ -335,6 +335,7 @@ impl FlowData { pub struct ScriptData { pub lock: Option, pub code: String, + pub modules: Option>, } #[derive(Debug, Clone)] @@ -357,6 +358,7 @@ pub struct RawScript { pub content: String, pub lock: Option, pub meta: Option, + pub modules: Option>, } #[derive(Debug, Deserialize, Serialize)] @@ -364,17 +366,28 @@ pub struct RawScriptApi { pub content: String, pub lock: Option, pub meta: Option, + pub modules: Option>, } impl From for RawScriptApi { fn from(value: RawScript) -> Self { - RawScriptApi { content: value.content, lock: value.lock, meta: value.meta } + RawScriptApi { + content: value.content, + lock: value.lock, + meta: value.meta, + modules: value.modules, + } } } impl From for RawScript { fn from(value: RawScriptApi) -> Self { - RawScript { content: value.content, lock: value.lock, meta: value.meta } + RawScript { + content: value.content, + lock: value.lock, + meta: value.meta, + modules: value.modules, + } } } @@ -632,7 +645,8 @@ pub mod script { schema AS \"schema: String\", \ schema_validation AS \"schema_validation: bool\", \ codebase LIKE '%.tar' as use_tar, \ - codebase LIKE '%.esm%' as is_esm \ + codebase LIKE '%.esm%' as is_esm, \ + modules AS \"modules: serde_json::Value\" \ FROM script WHERE hash = $1 LIMIT 1", hash.0 ) @@ -644,6 +658,7 @@ pub mod script { Ok(RawScript { content: r.content, lock: r.lock, + modules: r.modules.and_then(|v| serde_json::from_value(v).ok()), meta: Some(ScriptMetadata { language: r.language, envs: r.envs, @@ -822,6 +837,7 @@ pub mod job { _ => Ok(RawData::Script(Arc::new(ScriptData { code: code.unwrap_or_default(), lock, + modules: None, }))), }) }; @@ -999,7 +1015,7 @@ const _: () = { let content = src.get_utf8("code.txt")?; let lock = src.get_utf8("lock.txt").ok(); let meta = src.get_json("info.json").ok(); - Ok(Self { content, lock, meta }) + Ok(Self { content, lock, meta, modules: None }) } } @@ -1007,7 +1023,7 @@ const _: () = { type Untrusted = RawScript; fn resolve(src: Self::Untrusted) -> error::Result { - Ok(ScriptData { code: src.content, lock: src.lock }) + Ok(ScriptData { code: src.content, lock: src.lock, modules: src.modules }) } fn export(&self, dst: &impl Storage) -> error::Result<()> { @@ -1033,7 +1049,11 @@ const _: () = { return Err(error::Error::internal_err("Invalid script src".to_string())); }; Ok(ScriptFull { - data: Arc::new(ScriptData { code: src.content, lock: src.lock }), + data: Arc::new(ScriptData { + code: src.content, + lock: src.lock, + modules: src.modules, + }), meta: Arc::new(meta), }) } @@ -1063,7 +1083,11 @@ const _: () = { FlowData::from_raw(flow).map(Arc::new).map(Self::Flow) } RawNode { raw_code: Some(code), raw_lock: lock, .. } => { - Ok(Self::Script(Arc::new(ScriptData { code, lock }))) + Ok(Self::Script(Arc::new(ScriptData { + code, + lock, + modules: None, + }))) } _ => Err(error::Error::internal_err( "Invalid raw data src".to_string(), diff --git a/backend/windmill-common/src/scripts.rs b/backend/windmill-common/src/scripts.rs index 8bec9603c852f..d3035b1b4904b 100644 --- a/backend/windmill-common/src/scripts.rs +++ b/backend/windmill-common/src/scripts.rs @@ -108,11 +108,12 @@ pub async fn prefetch_cached_script( delete_after_use: script.delete_after_use, restart_unless_cancelled: script.restart_unless_cancelled, visible_to_runner_only: script.visible_to_runner_only, - no_main_func: script.no_main_func, + auto_kind: script.auto_kind, codebase: script.codebase, has_preprocessor: script.has_preprocessor, on_behalf_of_email: script.on_behalf_of_email, assets: script.assets, + modules: script.modules, runnable_settings: ScriptRunnableSettingsInline { concurrency_settings: concurrency_settings.maybe_fallback( script.runnable_settings.concurrency_key, @@ -346,11 +347,12 @@ pub async fn fetch_script_for_update<'a>( delete_after_use, restart_unless_cancelled, visible_to_runner_only, - no_main_func, + auto_kind, codebase, has_preprocessor, on_behalf_of_email, - assets + assets, + modules FROM script WHERE path = $1 AND workspace_id = $2 AND archived = false ORDER BY created_at DESC LIMIT 1 FOR UPDATE", ) .bind(path) @@ -419,12 +421,13 @@ pub async fn clone_script<'c>( restart_unless_cancelled: s.restart_unless_cancelled, deployment_message, visible_to_runner_only: s.visible_to_runner_only, - no_main_func: s.no_main_func, + auto_kind: s.auto_kind, codebase: s.codebase, has_preprocessor: s.has_preprocessor, on_behalf_of_email: s.on_behalf_of_email, preserve_on_behalf_of: None, assets: s.assets, + modules: s.modules, }; let new_hash = hash_script(&ns); @@ -442,15 +445,15 @@ pub async fn clone_script<'c>( created_by, schema, is_template, extra_perms, lock, language, kind, tag, \ draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, cache_ignore_s3_path, \ dedicated_worker, ws_error_handler_muted, priority, restart_unless_cancelled, \ - delete_after_use, timeout, concurrency_key, visible_to_runner_only, no_main_func, \ - codebase, has_preprocessor, on_behalf_of_email, schema_validation, assets, debounce_key, debounce_delay_s, runnable_settings_handle) + delete_after_use, timeout, concurrency_key, visible_to_runner_only, auto_kind, \ + codebase, has_preprocessor, on_behalf_of_email, schema_validation, assets, debounce_key, debounce_delay_s, runnable_settings_handle, modules) SELECT workspace_id, $1, path, array_prepend($2::bigint, COALESCE(parent_hashes, '{}'::bigint[])), summary, description, \ content, created_by, schema, is_template, extra_perms, NULL, language, kind, tag, \ draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, cache_ignore_s3_path, \ dedicated_worker, ws_error_handler_muted, priority, restart_unless_cancelled, \ - delete_after_use, timeout, concurrency_key, visible_to_runner_only, no_main_func, \ - codebase, has_preprocessor, on_behalf_of_email, schema_validation, assets, debounce_key, debounce_delay_s, runnable_settings_handle + delete_after_use, timeout, concurrency_key, visible_to_runner_only, auto_kind, \ + codebase, has_preprocessor, on_behalf_of_email, schema_validation, assets, debounce_key, debounce_delay_s, runnable_settings_handle, modules FROM script WHERE hash = $2 AND workspace_id = $3; ", new_hash, s.hash.0, w_id).execute(&mut *tx).await?; diff --git a/backend/windmill-common/src/worker.rs b/backend/windmill-common/src/worker.rs index b3876fbcfa92f..8b20d331c0bef 100644 --- a/backend/windmill-common/src/worker.rs +++ b/backend/windmill-common/src/worker.rs @@ -1361,7 +1361,7 @@ pub async fn fetch_raw_script_from_app_query( .await .map_err(Into::into) .and_then(unwrap_or_error(&loc, "Application script", id)) - .map(|r| RawScript { content: r.code, lock: r.lock, meta: None }) + .map(|r| RawScript { content: r.code, lock: r.lock, meta: None, modules: None }) } pub async fn insert_ping_query( diff --git a/backend/windmill-queue/src/jobs.rs b/backend/windmill-queue/src/jobs.rs index e91bb281e4b8b..655569dda1fc5 100644 --- a/backend/windmill-queue/src/jobs.rs +++ b/backend/windmill-queue/src/jobs.rs @@ -466,6 +466,7 @@ pub async fn push_init_job<'c>( dedicated_worker: None, concurrency_settings: ConcurrencySettingsWithCustom::default(), debouncing_settings: DebouncingSettings::default(), + modules: None, }), PushArgs::from(&ehm), worker_name, @@ -523,6 +524,7 @@ pub async fn push_periodic_bash_job<'c>( dedicated_worker: None, concurrency_settings: ConcurrencySettingsWithCustom::default(), debouncing_settings: DebouncingSettings::default(), + modules: None, }), PushArgs::from(&ehm), worker_name, @@ -4451,7 +4453,7 @@ async fn push_inner<'c, 'd>( mut tx: PushIsolationLevel<'c>, workspace_id: &str, job_payload: JobPayload, - args: PushArgs<'d>, + mut args: PushArgs<'d>, user: &str, mut email: &str, mut permissioned_as: String, @@ -4806,19 +4808,34 @@ async fn push_inner<'c, 'd>( dedicated_worker, concurrency_settings, debouncing_settings, - }) => JobPayloadUntagged { - runnable_id: hash, - runnable_path: path, - raw_code_tuple: Some((content, lock)), - job_kind: JobKind::Preview, - language: Some(language), - concurrency_settings: concurrency_settings.into(), - debouncing_settings, - cache_ttl, - cache_ignore_s3_path, - dedicated_worker, - ..Default::default() - }, + modules, + }) => { + // Inject modules into job args as _MODULES so the worker can extract them + if let Some(ref modules) = modules { + match serde_json::to_string(modules).and_then(|s| RawValue::from_string(s)) { + Ok(raw) => { + let extra = args.extra.get_or_insert_with(HashMap::new); + extra.insert("_MODULES".to_string(), raw); + } + Err(e) => { + tracing::warn!("Failed to serialize modules for preview job: {e}"); + } + } + } + JobPayloadUntagged { + runnable_id: hash, + runnable_path: path, + raw_code_tuple: Some((content, lock)), + job_kind: JobKind::Preview, + language: Some(language), + concurrency_settings: concurrency_settings.into(), + debouncing_settings, + cache_ttl, + cache_ignore_s3_path, + dedicated_worker, + ..Default::default() + } + } JobPayload::Dependencies { hash, language, diff --git a/backend/windmill-runtime-nativets/src/windmill-client.js b/backend/windmill-runtime-nativets/src/windmill-client.js index 33e51577e6e5f..24e781343e24a 100644 --- a/backend/windmill-runtime-nativets/src/windmill-client.js +++ b/backend/windmill-runtime-nativets/src/windmill-client.js @@ -259,8 +259,8 @@ var $Script = { visible_to_runner_only: { type: "boolean", }, - no_main_func: { - type: "boolean", + auto_kind: { + type: "string", }, codebase: { type: "string", @@ -281,7 +281,7 @@ var $Script = { "language", "kind", "starred", - "no_main_func", + "auto_kind", ], }; var $NewScript = { @@ -381,8 +381,8 @@ var $NewScript = { visible_to_runner_only: { type: "boolean", }, - no_main_func: { - type: "boolean", + auto_kind: { + type: "string", }, codebase: { type: "string", diff --git a/backend/windmill-test-utils/src/lib.rs b/backend/windmill-test-utils/src/lib.rs index 5d5c80b47029c..8e83eae1da53f 100644 --- a/backend/windmill-test-utils/src/lib.rs +++ b/backend/windmill-test-utils/src/lib.rs @@ -623,11 +623,12 @@ pub async fn assert_lockfile( deployment_message: None, concurrency_key: None, visible_to_runner_only: None, - no_main_func: None, + auto_kind: None, codebase: None, has_preprocessor: None, on_behalf_of_email: None, assets: vec![], + modules: None, }, ) .await @@ -720,11 +721,12 @@ pub async fn run_deployed_relative_imports( deployment_message: None, concurrency_key: None, visible_to_runner_only: None, - no_main_func: None, + auto_kind: None, codebase: None, has_preprocessor: None, on_behalf_of_email: None, assets: vec![], + modules: None, }, ) .await @@ -810,6 +812,7 @@ pub async fn run_preview_relative_imports( windmill_common::runnable_settings::ConcurrencySettings::default().into(), debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(), + modules: None, })) .push(&db2) .await; diff --git a/backend/windmill-types/src/jobs.rs b/backend/windmill-types/src/jobs.rs index ffd3cc6ef39d0..28c32426957f9 100644 --- a/backend/windmill-types/src/jobs.rs +++ b/backend/windmill-types/src/jobs.rs @@ -10,7 +10,7 @@ use crate::{ flow_status::{FlowStatus, RestartedFrom}, flows::{FlowNodeId, FlowValue, Retry}, runnable_settings::{ConcurrencySettings, ConcurrencySettingsWithCustom, DebouncingSettings}, - scripts::{ScriptHash, ScriptLang}, + scripts::{ScriptHash, ScriptLang, ScriptModule}, }; #[derive(Debug, Deserialize, Clone)] @@ -469,6 +469,7 @@ pub struct RawCode { pub concurrency_settings: ConcurrencySettingsWithCustom, #[serde(flatten)] pub debouncing_settings: DebouncingSettings, + pub modules: Option>, } impl JobPayload { diff --git a/backend/windmill-types/src/scripts.rs b/backend/windmill-types/src/scripts.rs index cc986444cfc79..5eca2be82c047 100644 --- a/backend/windmill-types/src/scripts.rs +++ b/backend/windmill-types/src/scripts.rs @@ -9,11 +9,21 @@ use itertools::Itertools; use serde::de::Error as _; use serde::{ser::SerializeSeq, Deserialize, Deserializer, Serialize}; +use std::collections::HashMap; + use crate::{ assets::AssetWithAltAccessType, runnable_settings::{ConcurrencySettings, DebouncingSettings}, }; +#[derive(Serialize, Deserialize, Debug, Clone, Hash)] +pub struct ScriptModule { + pub content: String, + pub language: ScriptLang, + #[serde(skip_serializing_if = "Option::is_none")] + pub lock: Option, +} + #[derive( Serialize, Deserialize, @@ -351,7 +361,7 @@ pub struct Script { #[serde(skip_serializing_if = "Option::is_none")] pub visible_to_runner_only: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub no_main_func: Option, + pub auto_kind: Option, #[serde(skip_serializing_if = "Option::is_none")] pub codebase: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -361,6 +371,9 @@ pub struct Script { #[serde(skip_serializing_if = "Option::is_none")] #[sqlx(json(nullable))] pub assets: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + #[sqlx(json(nullable))] + pub modules: Option>, #[serde(flatten)] #[sqlx(flatten)] pub runnable_settings: SR, @@ -419,7 +432,7 @@ pub struct ListableScript { pub has_deploy_errors: bool, pub ws_error_handler_muted: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub no_main_func: Option, + pub auto_kind: Option, #[serde(skip_serializing_if = "is_false")] pub use_codebase: bool, #[sqlx(default)] @@ -455,7 +468,7 @@ impl Hash for Schema { } } -#[derive(Serialize, Deserialize, Hash, Debug)] +#[derive(Serialize, Deserialize, Debug)] pub struct NewScript { pub path: String, pub parent_hash: Option, @@ -488,13 +501,60 @@ pub struct NewScript { pub deployment_message: Option, #[serde(skip_serializing_if = "Option::is_none")] pub visible_to_runner_only: Option, - pub no_main_func: Option, + pub auto_kind: Option, pub codebase: Option, pub has_preprocessor: Option, pub on_behalf_of_email: Option, pub preserve_on_behalf_of: Option, #[serde(skip_serializing_if = "Option::is_none")] pub assets: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub modules: Option>, +} + +// IMPORTANT: update this Hash impl when adding fields to NewScript +impl Hash for NewScript { + fn hash(&self, state: &mut H) { + self.path.hash(state); + self.parent_hash.hash(state); + self.summary.hash(state); + self.description.hash(state); + self.content.hash(state); + self.schema.hash(state); + self.is_template.hash(state); + self.lock.hash(state); + self.language.hash(state); + self.kind.hash(state); + self.tag.hash(state); + self.draft_only.hash(state); + self.envs.hash(state); + self.concurrency_settings.hash(state); + self.debouncing_settings.hash(state); + self.cache_ttl.hash(state); + self.cache_ignore_s3_path.hash(state); + self.dedicated_worker.hash(state); + self.ws_error_handler_muted.hash(state); + self.priority.hash(state); + self.timeout.hash(state); + self.delete_after_use.hash(state); + self.restart_unless_cancelled.hash(state); + self.deployment_message.hash(state); + self.visible_to_runner_only.hash(state); + self.auto_kind.hash(state); + self.codebase.hash(state); + self.has_preprocessor.hash(state); + self.on_behalf_of_email.hash(state); + self.preserve_on_behalf_of.hash(state); + self.assets.hash(state); + if let Some(modules) = &self.modules { + let mut sorted: Vec<_> = modules.iter().collect(); + sorted.sort_by_key(|(k, _)| *k); + for (k, v) in sorted { + k.hash(state); + v.hash(state); + } + } + } } fn lock_deserialize<'de, D>(deserializer: D) -> Result, D::Error> diff --git a/backend/windmill-worker/loader.bun.js b/backend/windmill-worker/loader.bun.js index a697ba1d5334a..f64e6d769b9e1 100644 --- a/backend/windmill-worker/loader.bun.js +++ b/backend/windmill-worker/loader.bun.js @@ -77,6 +77,18 @@ const p = { if (args.importer?.startsWith(cdirNodeModules)) { return undefined; } + + // Check if the import resolves to a local module file (written by write_module_files). + // Only check relative paths — absolute/bare specifiers should fall through to the + // remote resolver, matching the Windows loader pattern. + if (args.path.startsWith(".")) { + const localPath = resolve(cdir, args.path); + try { + readFileSync(localPath); + return { path: localPath }; + } catch {} + } + const file_path = args.importer == "./main.ts" || args.importer == resolve("./main.ts") ? current_path diff --git a/backend/windmill-worker/loader.bun.windows.js b/backend/windmill-worker/loader.bun.windows.js index 227c68a56cf71..fedef5fc5aaf6 100644 --- a/backend/windmill-worker/loader.bun.windows.js +++ b/backend/windmill-worker/loader.bun.windows.js @@ -106,6 +106,14 @@ const p = { if (importerFwd.startsWith(cdirNodeModules)) { return undefined; } + // Check if the import resolves to a local module file (written by write_module_files) + if (args.path.startsWith(".")) { + const cwdPath = resolve(cdir, args.path); + try { + readFileSync(cwdPath); + return { path: cwdPath }; + } catch {} + } const isMainTs = args.importer == "./main.ts" || importerFwd.endsWith("/main.ts"); const file_path = isMainTs diff --git a/backend/windmill-worker/src/bun_executor.rs b/backend/windmill-worker/src/bun_executor.rs index ba033f3078065..792e864d06023 100644 --- a/backend/windmill-worker/src/bun_executor.rs +++ b/backend/windmill-worker/src/bun_executor.rs @@ -206,6 +206,7 @@ pub async fn gen_bun_lockfile( workspace_dependencies: &WorkspaceDependenciesPrefetched, npm_mode: bool, occupancy_metrics: &mut Option<&mut OccupancyMetrics>, + quiet: bool, ) -> Result> { let common_bun_proc_envs: HashMap = get_common_bun_proc_envs(None).await; @@ -254,7 +255,8 @@ pub async fn gen_bun_lockfile( let mut child_process = start_child_process(child_cmd, &*BUN_PATH, false).await?; if let Some(db) = db { - handle_child( + let mut quiet_buf = String::new(); + let result = handle_child( job_id, db, mem_peak, @@ -267,10 +269,20 @@ pub async fn gen_bun_lockfile( None, false, occupancy_metrics, - None, + if quiet { Some(&mut quiet_buf) } else { None }, None, ) - .await?; + .await; + if quiet && result.is_err() { + append_logs( + job_id, + w_id, + format!("\n--- BUN BUILD (failed) ---\n{quiet_buf}"), + db, + ) + .await; + } + result?; } else { Box::into_pin(child_process.wait()).await?; } @@ -294,11 +306,14 @@ pub async fn gen_bun_lockfile( common_bun_proc_envs, npm_mode, occupancy_metrics, + quiet, ) .await?; } else { - if let Some(db) = db { - append_logs(job_id, w_id, "\nempty dependencies, skipping install", db).await; + if !quiet { + if let Some(db) = db { + append_logs(job_id, w_id, "\nempty dependencies, skipping install", db).await; + } } } @@ -425,6 +440,7 @@ pub async fn install_bun_lockfile( common_bun_proc_envs: HashMap, npm_mode: bool, occupancy_metrics: &mut Option<&mut OccupancyMetrics>, + quiet: bool, ) -> Result<()> { let mut child_cmd = Command::new(if npm_mode { &*NPM_PATH } else { &*BUN_PATH }); @@ -512,7 +528,7 @@ pub async fn install_bun_lockfile( false }; - if npm_mode || no_cache { + if !quiet && (npm_mode || no_cache) { if let Some(db) = db { append_logs(&job_id.clone(), w_id, npm_logs, db).await; } @@ -524,7 +540,8 @@ pub async fn install_bun_lockfile( let mut child_process = start_child_process(child_cmd, &*BUN_PATH, false).await?; if let Some(db) = db { - handle_child( + let mut quiet_buf = String::new(); + let result = handle_child( job_id, db, mem_peak, @@ -537,11 +554,22 @@ pub async fn install_bun_lockfile( None, false, occupancy_metrics, - None, + if quiet { Some(&mut quiet_buf) } else { None }, None, ) .warn_after_seconds(10) - .await?; + .await; + if quiet && result.is_err() { + // On failure, flush suppressed install output so the user can diagnose + append_logs( + job_id, + w_id, + format!("\n--- BUN INSTALL (failed) ---\n{quiet_buf}"), + db, + ) + .await; + } + result?; } else { Box::into_pin(child_process.wait()).await?; } @@ -1022,6 +1050,7 @@ pub async fn handle_bun_job( occupancy_metrics: &mut OccupancyMetrics, precomputed_agent_info: Option, has_stream: &mut bool, + modules: &Option>, ) -> error::Result> { let mut annotation = windmill_common::worker::TypeScriptAnnotations::parse(inner_content); @@ -1088,6 +1117,23 @@ pub async fn handle_bun_job( let is_wac_v2 = main_override.is_none() && crate::wac_executor::is_wac_v2_ts(inner_content); + // Detect WAC v2 replay (resumed from suspend) to suppress verbose logs. + // The actual step name is logged later by handle_wac_v2_output. + let wac_replay_info: Option = if is_wac_v2 { + if let Connection::Sql(db) = conn { + let checkpoint = crate::wac_executor::load_checkpoint(db, &job.id).await?; + if !checkpoint.completed_steps.is_empty() { + Some(String::new()) + } else { + None + } + } else { + None + } + } else { + None + }; + // For WAC v2, inject variable names into unnamed task() calls so the // runtime can use them for step naming (timeline, graph). // `const double = task(async ...` → `const double = task("double", async ...` @@ -1178,14 +1224,17 @@ pub async fn handle_bun_job( common_bun_proc_envs.clone(), annotation.npm, &mut Some(occupancy_metrics), + wac_replay_info.is_some(), ) .await?; } } MaybeLock::Unresolved { ref workspace_dependencies } => { // if is_sandboxing_enabled() || !empty_trusted_deps || has_custom_config_registry { - let logs1 = "\n\n--- BUN INSTALL ---\n".to_string(); - append_logs(&job.id, &job.workspace_id, logs1, conn).await; + if wac_replay_info.is_none() { + let logs1 = "\n\n--- BUN INSTALL ---\n".to_string(); + append_logs(&job.id, &job.workspace_id, logs1, conn).await; + } gen_bun_lockfile( mem_peak, canceled_by, @@ -1201,6 +1250,7 @@ pub async fn handle_bun_job( workspace_dependencies, annotation.npm, &mut Some(occupancy_metrics), + wac_replay_info.is_some(), ) .await?; @@ -1213,7 +1263,13 @@ pub async fn handle_bun_job( annotation.nodejs = true } - let mut init_logs = if annotation.native { + let mut init_logs = if let Some(ref replay_header) = wac_replay_info { + // WAC v2 replay: use concise header, but still write main.ts if needed + if !annotation.native && !has_bundle_cache && codebase.is_none() { + write_file(job_dir, "main.ts", &remove_pinned_imports(inner_content)?)?; + } + replay_header.clone() + } else if annotation.native { "\n\n--- NATIVE CODE EXECUTION ---\n".to_string() } else if has_bundle_cache { if annotation.nodejs { @@ -1234,6 +1290,18 @@ pub async fn handle_bun_job( "\n\n--- NODE CODE EXECUTION ---\n".to_string() } else { write_file(job_dir, "main.ts", &remove_pinned_imports(inner_content)?)?; + // Module inlining has two phases: + // 1. BUILD phase: loader.bun.js checks for local module files on disk (written by + // write_module_files) and resolves them directly, so they get inlined into the bundle. + // 2. RUN phase: overwrite main.ts with the bundled output below. The runtime wrapper + // imports main.ts, which now contains the inlined modules from the build step. + if modules.as_ref().is_some_and(|m| !m.is_empty()) { + let bundle_path = std::path::Path::new(job_dir).join("out").join("main.js"); + if bundle_path.exists() { + let bundled = std::fs::read_to_string(&bundle_path)?; + write_file(job_dir, "main.ts", &bundled)?; + } + } "\n\n--- BUN CODE EXECUTION ---\n".to_string() }; @@ -1395,7 +1463,7 @@ async function run() {{ return {{ type: "complete", result: dispatch.result ?? null }}; }} if (dispatch.mode === "inline_checkpoint") {{ - return {{ type: "inline_checkpoint", key: dispatch.key, result: dispatch.result ?? null }}; + return {{ type: "inline_checkpoint", key: dispatch.key, result: dispatch.result ?? null, started_at: dispatch.started_at, duration_ms: dispatch.duration_ms }}; }} if (dispatch.mode === "approval") {{ return {{ type: "approval", key: dispatch.key, timeout: dispatch.timeout, form: dispatch.form }}; @@ -1411,6 +1479,9 @@ async function run() {{ try {{ const output = await run(); + if (output.type === "complete") {{ + console.log(`\n--- WAC: complete ---`); + }} const output_json = JSON.stringify(output, (key, value) => typeof value === 'undefined' ? null : value ); @@ -1958,18 +2029,37 @@ try {{ // WAC v2 post-execution: parse output and handle dispatch/suspend if is_wac_v2 { - return handle_wac_v2_output(result, job, conn).await; + return handle_wac_v2_output(result, job, conn, modules).await; } Ok(result) } +/// Resolve a module file from the parent script's modules map. +/// For Script jobs, fetches from the `script` table by hash. +/// For Preview jobs, fetches from `v2_job.raw_code` (modules stored inline). +fn resolve_parent_module( + modules: &Option>, + module_key: &str, +) -> error::Result { + if let Some(modules) = modules { + if let Some(module) = modules.get(module_key) { + return Ok(module.clone()); + } + } + Err(error::Error::ExecutionErr(format!( + "Module '{}' not found in script modules", + module_key + ))) +} + /// Handle WAC v2 output after bun/python exits. Parse result as WacOutput, /// dispatch child jobs on suspend, or return the final result. pub async fn handle_wac_v2_output( result: Box, job: &MiniPulledJob, conn: &Connection, + modules: &Option>, ) -> error::Result> { use crate::wac_executor::{ add_completed_step, load_checkpoint, parse_wac_output, update_checkpoint_for_dispatch, @@ -2152,6 +2242,7 @@ pub async fn handle_wac_v2_output( dedicated_worker: None, concurrency_settings: ConcurrencySettingsWithCustom::default(), debouncing_settings: DebouncingSettings::default(), + modules: None, })) } _ => Err(error::Error::internal_err(format!( @@ -2245,6 +2336,33 @@ pub async fn handle_wac_v2_output( for (step, (_, child_uuid)) in steps.iter().zip(job_ids.iter()) { // Resolve job payload based on dispatch_type let (job_payload, child_args, is_external) = match step.dispatch_type.as_str() { + "script" if step.script.starts_with("./") => { + // Module-relative path: resolve from parent script's modules + let module_key = step.script.strip_prefix("./").unwrap(); + let module = resolve_parent_module(modules, module_key)?; + let payload = JobPayload::Code(RawCode { + content: module.content, + path: job.runnable_path.clone(), + hash: None, + language: module.language, + lock: module.lock, + cache_ttl: job.cache_ttl, + cache_ignore_s3_path: job.cache_ignore_s3_path, + dedicated_worker: None, + concurrency_settings: ConcurrencySettingsWithCustom::default(), + debouncing_settings: DebouncingSettings::default(), + modules: None, + }); + let step_args: HashMap> = step + .args + .iter() + .map(|(k, v)| { + let raw = serde_json::value::to_raw_value(v).unwrap(); + (k.clone(), raw) + }) + .collect(); + (payload, step_args, true) + } "script" => { // Resolve script path to job payload (handles hash, lang, etc.) let (payload, _, _, _, _) = script_path_to_payload( @@ -2630,7 +2748,7 @@ pub async fn handle_wac_v2_output( job.id, seconds, key ))) } - WacOutput::InlineCheckpoint { key, result: value } => { + WacOutput::InlineCheckpoint { key, result: value, started_at, duration_ms } => { let db = match conn { Connection::Sql(db) => db, _ => { @@ -2665,7 +2783,7 @@ pub async fn handle_wac_v2_output( add_completed_step(&mut checkpoint, &key, value); - // Save checkpoint + reset running in a single transaction + // Save checkpoint + write step timeline entry + reset running in a single transaction { let mut tx = db.begin().await?; let status_json = serde_json::to_value(&checkpoint).map_err(|e| { @@ -2689,6 +2807,34 @@ pub async fn handle_wac_v2_output( error::Error::internal_err(format!("Failed to save WAC checkpoint: {e}")) })?; + // Write timeline entry for the inline step (keyed as _step/) + if let Some(ref sa) = started_at { + let mut timeline_val = serde_json::json!({ + "scheduled_for": sa, + "started_at": sa, + "name": key, + }); + if let Some(dur) = duration_ms { + timeline_val["duration_ms"] = serde_json::json!(dur); + } + let step_timeline_key = format!("_step/{}", key); + sqlx::query( + "UPDATE v2_job_status SET workflow_as_code_status = jsonb_set( + COALESCE(workflow_as_code_status, '{}'::jsonb), + ARRAY[$2], + $3 + ) WHERE id = $1", + ) + .bind(&job.id) + .bind(&step_timeline_key) + .bind(&timeline_val) + .execute(&mut *tx) + .await + .map_err(|e| { + error::Error::internal_err(format!("Failed to write step timeline: {e}")) + })?; + } + // Reset running=false so the job is immediately eligible for pickup. // Unlike dispatch (which sets suspend>0), inline checkpoints don't suspend — // the job should be re-run right away to continue past the cached step. @@ -3118,6 +3264,7 @@ pub async fn start_worker( common_bun_proc_envs.clone(), annotation.npm, &mut None, + false, ) .await?; } @@ -3213,6 +3360,7 @@ pub async fn start_worker( common_bun_proc_envs.clone(), annotation.npm, &mut None, + false, ) .await?; tracing::info!("dedicated worker requirements installed: {reqs}"); @@ -3242,6 +3390,7 @@ pub async fn start_worker( .await?, annotation.npm, &mut None, + false, ) .await?; } diff --git a/backend/windmill-worker/src/common.rs b/backend/windmill-worker/src/common.rs index b2371a08e4125..bdecb0526f8a1 100644 --- a/backend/windmill-worker/src/common.rs +++ b/backend/windmill-worker/src/common.rs @@ -112,17 +112,20 @@ pub async fn create_args_and_out_file( conn: &Connection, ) -> Result<(), Error> { if let Some(args) = job.args.as_ref() { - if let Some(x) = transform_json(client, &job.workspace_id, &args.0, job, conn).await? { + if let Some(mut x) = transform_json(client, &job.workspace_id, &args.0, job, conn).await? { + x.remove("_MODULES"); write_file( job_dir, "args.json", &serde_json::to_string(&x).unwrap_or_else(|_| "{}".to_string()), )?; } else { + let mut filtered = args.0.clone(); + filtered.remove("_MODULES"); write_file( job_dir, "args.json", - &serde_json::to_string(&args).unwrap_or_else(|_| "{}".to_string()), + &serde_json::to_string(&filtered).unwrap_or_else(|_| "{}".to_string()), )?; } } else { diff --git a/backend/windmill-worker/src/handle_child.rs b/backend/windmill-worker/src/handle_child.rs index 205da6700aba3..2c78318a1a441 100644 --- a/backend/windmill-worker/src/handle_child.rs +++ b/backend/windmill-worker/src/handle_child.rs @@ -353,6 +353,7 @@ pub async fn handle_child( } pub const OTEL_PREFIX: &str = "OTEL: "; +pub const WAC_STEP_PREFIX: &str = "WM_WAC_STEP: "; pub async fn write_lines( output: impl stream::Stream> + Send, @@ -437,6 +438,19 @@ pub async fn write_lines( tracing::event!(tracing::Level::INFO, otel_suffix); } } + if let Some(step_json) = line.strip_prefix(WAC_STEP_PREFIX) { + // Real-time WAC step start marker — fire-and-forget DB write + let conn = conn.clone(); + let job_id = job_id.clone(); + let step_json = step_json.to_string(); + tokio::spawn(async move { + if let Err(e) = handle_wac_step_marker(&conn, &job_id, &step_json).await + { + tracing::warn!(%job_id, "Failed to write WAC step marker: {e}"); + } + }); + continue; + } if let Some(stream) = extract_stream_from_logs(&line) { let len = stream.len(); if log_remaining >= len { @@ -564,6 +578,58 @@ pub async fn write_lines( } } +/// Handle a real-time WAC step start marker emitted via stdout. +/// Writes a timeline entry (with started_at but no duration_ms) to workflow_as_code_status +/// so the frontend can show the step immediately while it's still running. +async fn handle_wac_step_marker( + conn: &Connection, + job_id: &Uuid, + json_str: &str, +) -> error::Result<()> { + #[derive(serde::Deserialize)] + struct StepMarker { + key: String, + started_at: String, + } + let marker: StepMarker = serde_json::from_str(json_str).map_err(|e| { + error::Error::internal_err(format!("Failed to parse WM_WAC_STEP marker: {e}")) + })?; + + let step_timeline_key = format!("_step/{}", marker.key); + let timeline_val = serde_json::json!({ + "scheduled_for": marker.started_at, + "started_at": marker.started_at, + "name": marker.key, + }); + + match conn { + Connection::Sql(db) => { + sqlx::query( + "INSERT INTO v2_job_status (id, workflow_as_code_status) + VALUES ($1, jsonb_build_object($2, $3::jsonb)) + ON CONFLICT (id) DO UPDATE SET + workflow_as_code_status = jsonb_set( + COALESCE(v2_job_status.workflow_as_code_status, '{}'::jsonb), + ARRAY[$2], + $3::jsonb + )", + ) + .bind(job_id) + .bind(&step_timeline_key) + .bind(&timeline_val) + .execute(db) + .await + .map_err(|e| { + error::Error::internal_err(format!("DB error writing WAC step marker: {e}")) + })?; + } + Connection::Http(_) => { + // Agent workers don't support WAC v2 yet + } + } + Ok(()) +} + pub(crate) async fn get_mem_peak(pid: Option, nsjail: bool) -> i32 { if pid.is_none() { return -1; diff --git a/backend/windmill-worker/src/python_executor.rs b/backend/windmill-worker/src/python_executor.rs index 13ebe769678c8..4dfccacdb83e9 100644 --- a/backend/windmill-worker/src/python_executor.rs +++ b/backend/windmill-worker/src/python_executor.rs @@ -543,6 +543,32 @@ async fn postinstall( Ok(()) } +/// Compute the directory (relative to job_dir) where Python writes the main script. +/// Module files must be placed in this same directory for relative imports to work. +pub fn compute_python_module_dir(script_path: &str) -> String { + let script_path_splitted = script_path.split("/").map(|x| { + if x.starts_with(|x: char| x.is_ascii_digit()) { + format!("_{}", x) + } else { + x.to_string() + } + }); + let dirs_full = script_path_splitted + .clone() + .take(script_path_splitted.clone().count() - 1) + .join("/") + .replace("-", "_") + .replace("@", "."); + if dirs_full.len() > 0 { + dirs_full + .strip_prefix("/") + .unwrap_or(&dirs_full) + .to_string() + } else { + "tmp".to_string() + } +} + #[tracing::instrument(level = "trace", skip_all)] pub async fn handle_python_job( requirements_o: Option<&String>, @@ -563,6 +589,7 @@ pub async fn handle_python_job( occupancy_metrics: &mut OccupancyMetrics, precomputed_agent_info: Option, has_stream: &mut bool, + modules: &Option>, ) -> windmill_common::error::Result> { let script_path = crate::common::use_flow_root_path(job.runnable_path()); @@ -680,6 +707,7 @@ pub async fn handle_python_job( } else { String::new() }; + let main_override = main_name.unwrap_or_else(|| "main".to_string()); let res_to_json_body = python_res_to_json_body(postprocessor); let wrapper_content: String = if is_wac_v2 { @@ -697,8 +725,8 @@ from wmill.client import _run_workflow with open("args.json") as f: kwargs = json.load(f, strict=False) -args = {{}} {transforms} +args = kwargs with open("checkpoint.json") as f: checkpoint = json.load(f, strict=False) @@ -722,6 +750,9 @@ for k, v in list(args.items()): try: output = _run_workflow(workflow_fn, checkpoint, args) + if isinstance(output, dict) and output.get("type") == "complete": + print("") + print("--- WAC: complete ---") output_json = json.dumps(output, separators=(',', ':'), default=str) with open(result_json, 'w') as f: f.write(output_json) @@ -1014,7 +1045,10 @@ mount {{ // WAC v2 post-execution: parse output and handle dispatch/suspend. // Box::pin to avoid bloating handle_python_job's async state machine (stack overflow). if is_wac_v2 { - return Box::pin(crate::bun_executor::handle_wac_v2_output(result, job, conn)).await; + return Box::pin(crate::bun_executor::handle_wac_v2_output( + result, job, conn, modules, + )) + .await; } Ok(result) @@ -1069,6 +1103,7 @@ async fn prepare_wrapper( let relative_imports = RELATIVE_IMPORT_REGEX.is_match(&inner_content); + let dirs = compute_python_module_dir(script_path); let script_path_splitted = script_path.split("/").map(|x| { if x.starts_with(|x: char| x.is_ascii_digit()) { format!("_{}", x) @@ -1076,20 +1111,6 @@ async fn prepare_wrapper( x.to_string() } }); - let dirs_full = script_path_splitted - .clone() - .take(script_path_splitted.clone().count() - 1) - .join("/") - .replace("-", "_") - .replace("@", "."); - let dirs = if dirs_full.len() > 0 { - dirs_full - .strip_prefix("/") - .unwrap_or(&dirs_full) - .to_string() - } else { - "tmp".to_string() - }; let last = script_path_splitted .clone() .last() @@ -2622,3 +2643,56 @@ for line in sys.stdin: ) .await } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_compute_python_module_dir_nested_path() { + assert_eq!( + compute_python_module_dir("f/my_folder/my_script"), + "f/my_folder" + ); + } + + #[test] + fn test_compute_python_module_dir_deep_path() { + assert_eq!(compute_python_module_dir("f/a/b/c/script"), "f/a/b/c"); + } + + #[test] + fn test_compute_python_module_dir_root_level() { + // Root-level script (no parent dirs) should fall back to "tmp" + assert_eq!(compute_python_module_dir("my_script"), "tmp"); + } + + #[test] + fn test_compute_python_module_dir_single_folder() { + assert_eq!(compute_python_module_dir("f/script"), "f"); + } + + #[test] + fn test_compute_python_module_dir_digit_prefix() { + // Dirs starting with digits get underscore-prefixed + assert_eq!( + compute_python_module_dir("1st_folder/script"), + "_1st_folder" + ); + } + + #[test] + fn test_compute_python_module_dir_hyphens_replaced() { + // Hyphens are replaced with underscores + assert_eq!( + compute_python_module_dir("my-folder/sub-dir/script"), + "my_folder/sub_dir" + ); + } + + #[test] + fn test_compute_python_module_dir_at_replaced() { + // @ is replaced with . + assert_eq!(compute_python_module_dir("u/@admin/script"), "u/.admin"); + } +} diff --git a/backend/windmill-worker/src/result_processor.rs b/backend/windmill-worker/src/result_processor.rs index 0b6506ce63a35..e6faf74f693de 100644 --- a/backend/windmill-worker/src/result_processor.rs +++ b/backend/windmill-worker/src/result_processor.rs @@ -16,6 +16,10 @@ use windmill_common::otel_oss::FutureExt; use uuid::Uuid; +/// Set by the result processor when a WAC child completion makes suspend reach 0, +/// signaling the worker main loop to check for suspended jobs immediately. +pub static WAC_SUSPEND_READY: AtomicBool = AtomicBool::new(false); + use windmill_common::{ add_time, error::{self, Error}, @@ -1014,6 +1018,7 @@ pub(crate) async fn handle_wac_child_completion( parent_job = %parent_job_id, "WAC v2 all child jobs completed, unsuspending parent" ); + WAC_SUSPEND_READY.store(true, Ordering::Relaxed); } Ok(Some(())) diff --git a/backend/windmill-worker/src/wac_executor.rs b/backend/windmill-worker/src/wac_executor.rs index 7c350ac92bbc9..1e7ed4b090a40 100644 --- a/backend/windmill-worker/src/wac_executor.rs +++ b/backend/windmill-worker/src/wac_executor.rs @@ -47,7 +47,14 @@ pub enum WacOutput { /// An inline step executed in the parent process — persist result to /// checkpoint and re-run immediately (no child job, no suspend). #[serde(rename = "inline_checkpoint")] - InlineCheckpoint { key: String, result: Value }, + InlineCheckpoint { + key: String, + result: Value, + #[serde(default)] + started_at: Option, + #[serde(default)] + duration_ms: Option, + }, /// Suspend the workflow waiting for an external approval event. /// No child job is dispatched — the parent suspends directly and resumes /// when a user hits the resume/cancel endpoint. @@ -285,16 +292,20 @@ pub async fn prepare_checkpoint_for_resume( /// Detect WAC v2 patterns in TypeScript/Bun code. /// Checks for `import ... from "windmill-client"` containing workflow/task, -/// skipping comment lines. +/// skipping comment lines. Handles both single-line and multi-line imports. pub fn is_wac_v2_ts(code: &str) -> bool { let mut has_wac_import = false; let mut has_workflow = false; let mut has_task = false; + let mut in_import_block = false; + let mut import_block_has_workflow = false; + let mut import_block_has_task = false; for line in code.lines() { let trimmed = line.trim(); if trimmed.starts_with("//") { continue; } + // Single-line import: import { workflow, task } from "windmill-client" if trimmed.contains("windmill-client") && (trimmed.starts_with("import") || trimmed.starts_with("from")) { @@ -305,6 +316,37 @@ pub fn is_wac_v2_ts(code: &str) -> bool { if trimmed.contains("task") { has_task = true; } + in_import_block = false; + } + // Start of multi-line import: import { + else if trimmed.starts_with("import") && trimmed.contains("{") && !trimmed.contains("}") { + in_import_block = true; + import_block_has_workflow = trimmed.contains("workflow"); + import_block_has_task = trimmed.contains("task"); + } + // Inside multi-line import block + else if in_import_block { + if trimmed.contains("workflow") { + import_block_has_workflow = true; + } + if trimmed.contains("task") { + import_block_has_task = true; + } + // End of multi-line import: } from "windmill-client" + if trimmed.contains("windmill-client") { + has_wac_import = true; + if import_block_has_workflow { + has_workflow = true; + } + if import_block_has_task { + has_task = true; + } + in_import_block = false; + } + // End of import block but not windmill-client + if trimmed.contains("}") { + in_import_block = false; + } } if trimmed.contains("export") && trimmed.contains("workflow(") { has_workflow = true; diff --git a/backend/windmill-worker/src/worker.rs b/backend/windmill-worker/src/worker.rs index 2d573fb91bf45..ebc8495a24b59 100644 --- a/backend/windmill-worker/src/worker.rs +++ b/backend/windmill-worker/src/worker.rs @@ -24,6 +24,7 @@ use windmill_common::runtime_assets::init_runtime_asset_loop; use windmill_common::runtime_assets::register_runtime_asset; use windmill_common::scripts::hash_to_codebase_id; use windmill_common::scripts::is_special_codebase_hash; +use windmill_common::scripts::ScriptModule; use windmill_common::utils::report_critical_error; use windmill_common::utils::retrieve_common_worker_prefix; use windmill_common::worker::error_to_value; @@ -2063,6 +2064,7 @@ pub async fn run_worker( } } + let mut was_suspended_job = false; let next_job = { // println!("2: {:?}", instant.elapsed()); #[cfg(feature = "benchmark")] @@ -2146,7 +2148,9 @@ pub async fn run_worker( let suspend_first = suspend_first_success || rand::random::() < likelihood_of_suspend - || last_suspend_first.elapsed().as_secs_f64() > 5.0; + || last_suspend_first.elapsed().as_secs_f64() > 5.0 + || crate::result_processor::WAC_SUSPEND_READY + .swap(false, Ordering::Relaxed); if suspend_first { last_suspend_first = Instant::now(); @@ -2219,6 +2223,7 @@ pub async fn run_worker( } } + was_suspended_job = job.as_ref().is_ok_and(|j| j.suspended); if let Ok(j) = job.as_ref() { let suspend_success = j.suspended; if suspend_first { @@ -2436,7 +2441,9 @@ pub async fn run_worker( .expect("send job completed END"); add_time!(bench, "sent job completed"); } else { - add_outstanding_wait_time(&conn, &job, *OUTSTANDING_WAIT_TIME_THRESHOLD_MS); + if !was_suspended_job { + add_outstanding_wait_time(&conn, &job, *OUTSTANDING_WAIT_TIME_THRESHOLD_MS); + } #[cfg(feature = "prometheus")] register_metric( @@ -3306,10 +3313,22 @@ pub async fn handle_queued_job( "none" }; - logs.push_str(&format!( - "job={} {}={} worker={} hostname={} isolation={}\n", - &job.id, *LOG_TAG_NAME, &job.tag, &worker_name, &hostname, isolation_label - )); + // Skip verbose job header for WAC v2 replays (checkpoint has completed steps) + let is_wac_replay = if let Connection::Sql(db) = conn { + crate::wac_executor::load_checkpoint(db, &job.id) + .await + .map(|c| !c.completed_steps.is_empty()) + .unwrap_or(false) + } else { + false + }; + + if !is_wac_replay { + logs.push_str(&format!( + "job={} {}={} worker={} hostname={} isolation={}\n", + &job.id, *LOG_TAG_NAME, &job.tag, &worker_name, &hostname, isolation_label + )); + } if *NO_LOGS_AT_ALL { logs.push_str("Logs are fully disabled for this worker\n"); @@ -3616,6 +3635,7 @@ pub struct ContentReqLangEnvs { pub envs: Option>, pub codebase: Option, pub schema: Option, + pub modules: Option>, } pub async fn get_hub_script_content_and_requirements( @@ -3635,6 +3655,7 @@ pub async fn get_hub_script_content_and_requirements( envs: None, codebase: None, schema: Some(script.schema.get().to_string()), + modules: None, }) } @@ -3655,6 +3676,7 @@ pub async fn get_script_content_by_hash( Some(_) => Some(script_hash.to_string()), }, schema: None, + modules: data.modules.clone(), }) } @@ -3784,7 +3806,7 @@ async fn handle_code_execution_job( // Box::pin the script fetching match to prevent large enum on stack let ( - ScriptData { code, lock }, + ScriptData { code, lock, modules: modules_from_data }, ScriptMetadata { language, envs, codebase, schema_validator, schema }, ) = match job.kind { JobKind::Preview => { @@ -3809,14 +3831,14 @@ async fn handle_code_execution_job( } } JobKind::Script_Hub => { - let ContentReqLangEnvs { content, lockfile, language, envs, codebase, schema } = + let ContentReqLangEnvs { content, lockfile, language, envs, codebase, schema, .. } = Box::pin(get_hub_script_content_and_requirements( job.runnable_path.as_ref(), conn.as_sql(), )) .await?; - data = ScriptData { code: content, lock: lockfile }; + data = ScriptData { code: content, lock: lockfile, modules: None }; metadata = ScriptMetadata { language, envs, codebase, schema, schema_validator: None }; (&data, &metadata) } @@ -3861,13 +3883,20 @@ async fn handle_code_execution_job( .as_ref() .ok_or_else(|| Error::internal_err("expected script path".to_string()))?; if script_path.starts_with("hub/") { - let ContentReqLangEnvs { content, lockfile, language, envs, codebase, schema } = - Box::pin(get_hub_script_content_and_requirements( - Some(script_path), - conn.as_sql(), - )) - .await?; - data = ScriptData { code: content, lock: lockfile }; + let ContentReqLangEnvs { + content, + lockfile, + language, + envs, + codebase, + schema, + .. + } = Box::pin(get_hub_script_content_and_requirements( + Some(script_path), + conn.as_sql(), + )) + .await?; + data = ScriptData { code: content, lock: lockfile, modules: None }; metadata = ScriptMetadata { language, envs, codebase, schema, schema_validator: None }; (&data, &metadata) @@ -3898,6 +3927,16 @@ async fn handle_code_execution_job( ), }; + // For preview jobs, extract modules from args._MODULES if not already set + let modules = modules_from_data.clone().or_else(|| { + job.args.as_ref().and_then(|args| { + args.get("_MODULES").and_then(|raw| { + serde_json::from_str::>(raw.get()) + .ok() + }) + }) + }); + try_validate_schema( job, conn, @@ -3931,11 +3970,50 @@ async fn handle_code_execution_job( envs, codebase, lock, + &modules, false, ) .await } +pub async fn write_module_files( + job_dir: &str, + modules: &std::collections::HashMap, + base_dir: Option<&str>, +) -> error::Result<()> { + for (relpath, module) in modules { + // Reject path traversal attempts in module paths + if relpath.contains("..") { + tracing::warn!("Skipping module with path traversal: {relpath}"); + continue; + } + let full_path = match base_dir { + Some(dir) => format!("{}/{}/{}", job_dir, dir, relpath), + None => format!("{}/{}", job_dir, relpath), + }; + if let Some(parent) = std::path::Path::new(&full_path).parent() { + tokio::fs::create_dir_all(parent).await?; + } + // For Python modules, create __init__.py in each intermediate directory + // between base_dir and the module's parent so that relative imports work. + if let Some(dir) = base_dir { + let rel = std::path::Path::new(relpath); + let base = std::path::Path::new(job_dir).join(dir); + let mut current = base.clone(); + for component in rel.parent().into_iter().flat_map(|p| p.components()) { + current = current.join(component); + let init_py = current.join("__init__.py"); + if !init_py.exists() { + tokio::fs::write(&init_py, "").await?; + } + } + } + tracing::debug!("Writing module file: {full_path}"); + tokio::fs::write(&full_path, &module.content).await?; + } + Ok(()) +} + pub async fn run_language_executor( job: &MiniPulledJob, conn: &Connection, @@ -3958,8 +4036,24 @@ pub async fn run_language_executor( envs: &Option>, codebase: &Option, lock: &Option, + modules: &Option>, run_inline: bool, ) -> error::Result> { + if let Some(modules) = modules { + #[cfg(feature = "python")] + let base_dir = if language == Some(ScriptLang::Python3) { + let script_path = crate::common::use_flow_root_path(job.runnable_path()); + Some(crate::python_executor::compute_python_module_dir( + &script_path, + )) + } else { + None + }; + #[cfg(not(feature = "python"))] + let base_dir: Option = None; + write_module_files(job_dir, modules, base_dir.as_deref()).await?; + } + if language == Some(ScriptLang::Postgresql) { return Box::pin(do_postgresql( job, @@ -4433,6 +4527,7 @@ mount {{ occupancy_metrics, precomputed_agent_info, has_stream, + modules, )) .await } @@ -4496,6 +4591,7 @@ mount {{ occupancy_metrics, precomputed_agent_info, has_stream, + modules, )) .await } @@ -5008,6 +5104,7 @@ pub fn init_worker_internal_server_inline_utils( &None, &None, &None, + &None, true, ) .await @@ -5088,6 +5185,7 @@ pub fn init_worker_internal_server_inline_utils( &content_info.envs, &content_info.codebase, &content_info.lockfile, + &content_info.modules, true, ) .await diff --git a/backend/windmill-worker/src/worker_flow.rs b/backend/windmill-worker/src/worker_flow.rs index dfc2bbeed9bbb..402df2a0a9ef9 100644 --- a/backend/windmill-worker/src/worker_flow.rs +++ b/backend/windmill-worker/src/worker_flow.rs @@ -5035,6 +5035,7 @@ pub fn raw_script_to_payload( concurrency_settings, // TODO: Should this have debouncing? debouncing_settings: DebouncingSettings::default(), + modules: None, }), tag, delete_after_use, diff --git a/backend/windmill-worker/src/worker_lockfiles.rs b/backend/windmill-worker/src/worker_lockfiles.rs index 8636105243b0d..daba455f1cc47 100644 --- a/backend/windmill-worker/src/worker_lockfiles.rs +++ b/backend/windmill-worker/src/worker_lockfiles.rs @@ -182,13 +182,62 @@ pub async fn handle_dependency_job( let (deployment_message, parent_path) = get_deployment_msg_and_parent_path_from_args(job.args.clone()); + // Generate lockfiles for module files (if any) + let updated_modules = if let Some(modules) = &script_data.modules { + let mut updated = modules.clone(); + for (module_path, module) in updated.iter_mut() { + if module.content.is_empty() { + continue; + } + match capture_dependency_job( + &job.id, + &module.language, + &module.content, + mem_peak, + canceled_by, + job_dir, + db, + worker_name, + &job.workspace_id, + worker_dir, + base_internal_url, + token, + script_path, + occupancy_metrics, + &raw_workspace_dependencies_o, + module.lock.as_deref(), + triggered_by_relative_import, + script_path, + None, + "script", + ) + .await + { + Ok(lock) => { + module.lock = Some(lock); + } + Err(e) => { + tracing::warn!( + "Failed to generate lockfile for module {module_path}: {e}" + ); + } + } + } + Some(updated) + } else { + None + }; + // We do not create new row for this update // That means we can keep current hash and just update lock // Also store lockfile hash for dependency change detection let lockfile_hash = windmill_common::scripts::hash_script(&content); + let updated_modules_json = updated_modules + .as_ref() + .and_then(|m| serde_json::to_value(m).ok()); sqlx::query!( "WITH update_lock AS ( - UPDATE script SET lock = $1 WHERE hash = $2 AND workspace_id = $3 + UPDATE script SET lock = $1, modules = COALESCE($6, modules) WHERE hash = $2 AND workspace_id = $3 ) INSERT INTO lock_hash (workspace_id, path, lockfile_hash) VALUES ($3, $4, $5) @@ -197,7 +246,8 @@ pub async fn handle_dependency_job( ¤t_hash.0, w_id, script_path, - &lockfile_hash + &lockfile_hash, + updated_modules_json ) .execute(db) .await?; @@ -2535,6 +2585,7 @@ async fn capture_dependency_job( &workspace_dependencies, windmill_common::worker::TypeScriptAnnotations::parse(job_raw_code).npm, &mut Some(occupancy_metrics), + false, ) .await? { diff --git a/cli/src/commands/generate-metadata/generate-metadata.ts b/cli/src/commands/generate-metadata/generate-metadata.ts index 77fc4c4f86015..c523996cdc698 100644 --- a/cli/src/commands/generate-metadata/generate-metadata.ts +++ b/cli/src/commands/generate-metadata/generate-metadata.ts @@ -19,7 +19,7 @@ import { ignoreF, } from "../sync/sync.ts"; import { exts } from "../script/script.ts"; -import { isFlowPath, isAppPath, isRawAppPath } from "../../utils/resource_folders.ts"; +import { isFlowPath, isAppPath, isRawAppPath, isScriptModulePath, isModuleEntryPoint } from "../../utils/resource_folders.ts"; import { listSyncCodebases } from "../../utils/codebase.ts"; interface StaleItem { @@ -83,7 +83,8 @@ async function generateMetadata( ignore(p, isD) || isFlowPath(p) || isAppPath(p) || - isRawAppPath(p) + isRawAppPath(p) || + (isScriptModulePath(p) && !isModuleEntryPoint(p)) ); }, false, diff --git a/cli/src/commands/script/script.ts b/cli/src/commands/script/script.ts index 817b9947d7458..68ff946cee7ad 100644 --- a/cli/src/commands/script/script.ts +++ b/cli/src/commands/script/script.ts @@ -9,6 +9,7 @@ import { Confirm } from "@cliffy/prompt/confirm"; import { Table } from "@cliffy/table"; import * as log from "../../core/log.ts"; import { sep as SEP } from "node:path"; +import * as path from "node:path"; import { stringify as yamlStringify } from "yaml"; import { deepEqual } from "../../utils/utils.ts"; import * as wmill from "../../../gen/services.gen.ts"; @@ -51,13 +52,18 @@ import fs from "node:fs"; import { createTarBlob, type TarEntry } from "../../utils/tar.ts"; import { execSync } from "node:child_process"; -import { NewScript, Script } from "../../../gen/types.gen.ts"; +import { NewScript, Script, ScriptModule } from "../../../gen/types.gen.ts"; import { isRawAppBackendPath as isRawAppBackendPathInternal, isAppInlineScriptPath as isAppInlineScriptPathInternal, isFlowInlineScriptPath as isFlowInlineScriptPathInternal, isFlowPath, isAppPath, + isScriptModulePath, + buildModuleFolderPath, + getModuleFolderSuffix, + isModuleEntryPoint, + getScriptBasePathFromModulePath, isRawAppPath, } from "../../utils/resource_folders.ts"; @@ -189,11 +195,17 @@ export async function handleScriptMetadata( codebases: SyncCodebase[], opts: GlobalOptions ): Promise { - if ( - path.endsWith(".script.json") || + // Flat layout: my_script.script.yaml + const isFlatMeta = path.endsWith(".script.json") || path.endsWith(".script.yaml") || - path.endsWith(".script.lock") - ) { + path.endsWith(".script.lock"); + // Folder layout: my_script__mod/script.yaml + const isFolderMeta = !isFlatMeta && isScriptModulePath(path) && ( + path.endsWith("/script.yaml") || + path.endsWith("/script.json") || + path.endsWith("/script.lock") + ); + if (isFlatMeta || isFolderMeta) { const contentPath = await findContentFile(path); return handleFile( contentPath, @@ -226,10 +238,13 @@ export async function handleFile( rawWorkspaceDependencies: Record, codebases: SyncCodebase[] ): Promise { + // Detect module entry point: e.g., my_script__mod/script.ts + const moduleEntryPoint = isModuleEntryPoint(path); if ( !isAppInlineScriptPath(path) && !isFlowInlineScriptPath(path) && !isRawAppBackendPath(path) && + (!isScriptModulePath(path) || moduleEntryPoint) && exts.some((exts) => path.endsWith(exts)) ) { if (alreadySynced.includes(path)) { @@ -238,9 +253,9 @@ export async function handleFile( log.debug(`Processing local script ${path}`); alreadySynced.push(path); - const remotePath = path - .substring(0, path.indexOf(".")) - .replaceAll(SEP, "/"); + const remotePath = moduleEntryPoint + ? getScriptBasePathFromModulePath(path)!.replaceAll(SEP, "/") + : path.substring(0, path.indexOf(".")).replaceAll(SEP, "/"); const language = inferContentTypeFromFilePath(path, opts?.defaultTs); @@ -392,6 +407,13 @@ export async function handleFile( typed.codebase = await codebase.getDigest(forceTar); } + // Scan for modules: folder layout (entry point inside __mod/) or flat layout + const scriptBasePath = moduleEntryPoint + ? getScriptBasePathFromModulePath(path)! + : path.substring(0, path.indexOf(".")); + const moduleFolderPath = scriptBasePath + getModuleFolderSuffix(); + const modules = await readModulesFromDisk(moduleFolderPath, opts?.defaultTs, moduleEntryPoint); + const requestBodyCommon: NewScript = { content, description: typed?.description ?? "", @@ -410,7 +432,6 @@ export async function handleFile( deployment_message: message, restart_unless_cancelled: typed?.restart_unless_cancelled, visible_to_runner_only: typed?.visible_to_runner_only, - no_main_func: typed?.no_main_func, has_preprocessor: typed?.has_preprocessor, priority: typed?.priority, concurrency_key: typed?.concurrency_key, @@ -420,6 +441,7 @@ export async function handleFile( timeout: typed?.timeout, on_behalf_of_email: typed?.on_behalf_of_email, envs: typed?.envs, + modules: modules, }; // console.log(requestBodyCommon.codebase); @@ -450,7 +472,6 @@ export async function handleFile( Boolean(remote.restart_unless_cancelled) && Boolean(typed.visible_to_runner_only) == Boolean(remote.visible_to_runner_only) && - Boolean(typed.no_main_func) == Boolean(remote.no_main_func) && Boolean(typed.has_preprocessor) == Boolean(remote.has_preprocessor) && typed.priority == Boolean(remote.priority) && @@ -461,7 +482,8 @@ export async function handleFile( typed.debounce_delay_s == remote["debounce_delay_s"] && typed.codebase == remote.codebase && typed.on_behalf_of_email == remote.on_behalf_of_email && - deepEqual(typed.envs, remote.envs)) + deepEqual(typed.envs, remote.envs) && + deepEqual(modules ?? null, remote.modules ?? null)) ) { log.info(colors.green(`Script ${remotePath} is up to date`)); return true; @@ -507,6 +529,135 @@ export async function handleFile( return false; } +/** + * Read module files from a __mod/ directory on disk. + * Returns the modules record for the API, or undefined if no module folder exists. + */ +export async function readModulesFromDisk( + moduleFolderPath: string, + defaultTs: "bun" | "deno" | undefined, + folderLayout: boolean = false, +): Promise | undefined> { + if (!fs.existsSync(moduleFolderPath) || !fs.statSync(moduleFolderPath).isDirectory()) { + return undefined; + } + + const modules: Record = {}; + + // In folder layout mode, skip the entry point files (script.*, script.yaml, etc.) + const isEntryPointFile = (name: string, isTopLevel: boolean) => { + if (!folderLayout || !isTopLevel) return false; + return ( + name.startsWith("script.") || + name === "script.lock" || + name === "script.yaml" || + name === "script.json" + ); + }; + + function readDir(dirPath: string, relPrefix: string) { + const entries = fs.readdirSync(dirPath, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(dirPath, entry.name); + const relPath = relPrefix ? relPrefix + "/" + entry.name : entry.name; + const isTopLevel = relPrefix === ""; + + if (entry.isDirectory()) { + readDir(fullPath, relPath); + } else if (entry.isFile() && !entry.name.endsWith(".lock") && !isEntryPointFile(entry.name, isTopLevel)) { + // Skip lock files — they're handled as the `lock` field on ScriptModule + if (exts.some((ext) => entry.name.endsWith(ext))) { + const content = fs.readFileSync(fullPath, "utf-8"); + const language = inferContentTypeFromFilePath(entry.name, defaultTs); + + // Check for an accompanying lock file (helper.lock) + const baseName = entry.name.replace(/\.[^.]+$/, ''); + const lockPath = path.join(dirPath, baseName + ".lock"); + let lock: string | undefined; + if (fs.existsSync(lockPath)) { + lock = fs.readFileSync(lockPath, "utf-8"); + } + + modules[relPath] = { + content, + language: language as ScriptModule["language"], + lock: lock ?? undefined, + }; + } + } + } + } + + readDir(moduleFolderPath, ""); + + if (Object.keys(modules).length === 0) { + return undefined; + } + + log.debug(`Found ${Object.keys(modules).length} module(s) in ${moduleFolderPath}`); + return modules; +} + +/** + * Write module files to a __mod/ directory on disk during pull. + */ +export async function writeModulesToDisk( + moduleFolderPath: string, + modules: Record, + defaultTs: "bun" | "deno" | undefined +): Promise { + // Ensure the module folder exists + fs.mkdirSync(moduleFolderPath, { recursive: true }); + + // Clean up stale module files that are no longer in the modules map + const expectedFiles = new Set(); + for (const [relPath, mod] of Object.entries(modules)) { + expectedFiles.add(relPath); + if (mod.lock) { + expectedFiles.add(relPath.replace(/\.[^.]+$/, '') + ".lock"); + } + } + + function cleanDir(dirPath: string, relPrefix: string) { + if (!fs.existsSync(dirPath) || !fs.statSync(dirPath).isDirectory()) return; + const entries = fs.readdirSync(dirPath, { withFileTypes: true }); + for (const entry of entries) { + const relPath = relPrefix ? relPrefix + "/" + entry.name : entry.name; + if (entry.isDirectory()) { + cleanDir(path.join(dirPath, entry.name), relPath); + // Remove empty directories after cleaning + try { + const remaining = fs.readdirSync(path.join(dirPath, entry.name)); + if (remaining.length === 0) { + fs.rmdirSync(path.join(dirPath, entry.name)); + } + } catch {} + } else if (!expectedFiles.has(relPath)) { + fs.unlinkSync(path.join(dirPath, entry.name)); + } + } + } + cleanDir(moduleFolderPath, ""); + + for (const [relPath, mod] of Object.entries(modules)) { + const fullPath = path.join(moduleFolderPath, relPath); + const dir = path.dirname(fullPath); + fs.mkdirSync(dir, { recursive: true }); + + // Write the module content + fs.writeFileSync(fullPath, mod.content, "utf-8"); + + // Write the lock file if present + if (mod.lock) { + const baseName = relPath.replace(/\.[^.]+$/, ''); + const lockPath = path.join(moduleFolderPath, baseName + ".lock"); + const lockDir = path.dirname(lockPath); + fs.mkdirSync(lockDir, { recursive: true }); + fs.writeFileSync(lockPath, mod.lock, "utf-8"); + } + } +} + async function createScript( bundleContent: string | Blob | undefined, workspaceId: string, @@ -560,7 +711,12 @@ async function createScript( } export async function findContentFile(filePath: string) { - const candidates = filePath.endsWith("script.json") + // Folder layout: __mod/script.yaml -> __mod/script.ts + const isModuleFolderMeta = + filePath.endsWith("/script.yaml") || filePath.endsWith("/script.json") || filePath.endsWith("/script.lock"); + const candidates = isModuleFolderMeta + ? exts.map((x) => filePath.replace(/\/script\.(yaml|json|lock)$/, "/script" + x)) + : filePath.endsWith("script.json") ? exts.map((x) => filePath.replace(".script.json", x)) : filePath.endsWith("script.lock") ? exts.map((x) => filePath.replace(".script.lock", x)) @@ -1029,7 +1185,9 @@ export async function generateMetadata( ignore(p, isD) || isFlowPath(p) || isAppPath(p) || - isRawAppPath(p) + isRawAppPath(p) || + // Skip module helper files; only entry points (script.{ext}) are processed + (isScriptModulePath(p) && !isModuleEntryPoint(p)) ); }, false, @@ -1118,6 +1276,13 @@ async function preview( const content = await readFile(filePath, "utf-8"); const input = opts.data ? await resolve(opts.data) : {}; + // Read modules from __mod/ folder if present + const isFolderLayout = isModuleEntryPoint(filePath); + const moduleFolderPath = isFolderLayout + ? path.dirname(filePath) + : filePath.substring(0, filePath.indexOf(".")) + getModuleFolderSuffix(); + const modules = await readModulesFromDisk(moduleFolderPath, opts?.defaultTs, isFolderLayout); + // Check if this is a codebase script const codebase = language == "bun" ? findCodebase(filePath, codebases) : undefined; @@ -1277,6 +1442,7 @@ async function preview( path: filePath.substring(0, filePath.indexOf(".")).replaceAll(SEP, "/"), args: input, language: language as any, + modules: modules ?? undefined, }, }); diff --git a/cli/src/commands/sync/sync.ts b/cli/src/commands/sync/sync.ts index 6375a55ada427..5de149cad38d0 100644 --- a/cli/src/commands/sync/sync.ts +++ b/cli/src/commands/sync/sync.ts @@ -31,6 +31,7 @@ import { findResourceFile, handleScriptMetadata, removeExtensionToPath, + filePathExtensionFromContentType, } from "../script/script.ts"; import { handleFile } from "../script/script.ts"; @@ -68,7 +69,7 @@ import { readLockfile, workspaceDependenciesPathToLanguageAndFilename, } from "../../utils/metadata.ts"; -import { OpenFlow, NativeServiceName } from "../../../gen/types.gen.ts"; +import { OpenFlow, NativeServiceName, ScriptModule } from "../../../gen/types.gen.ts"; import { pushResource } from "../resource/resource.ts"; import { newPathAssigner, @@ -97,6 +98,10 @@ import { getFolderSuffix, getFolderSuffixWithSep, getNonDottedPaths, + isScriptModulePath, + getModuleFolderSuffix, + isModuleEntryPoint, + getScriptBasePathFromModulePath, } from "../../utils/resource_folders.ts"; // Merge CLI options with effective settings, preserving CLI flags as overrides @@ -534,6 +539,29 @@ function ZipFSElement( resourceTypeToIsFileset: Record, ignoreCodebaseChanges: boolean, ): DynFSElement { + // Pre-scan: find zip base paths of scripts that have modules. + // These scripts use the folder layout: {basePath}__mod/script.{ext} + let _moduleScriptPaths: Set | null = null; + async function getModuleScriptPaths(): Promise> { + if (_moduleScriptPaths === null) { + _moduleScriptPaths = new Set(); + for (const filename in zip.files) { + if (filename.endsWith(".script.json") && !zip.files[filename].dir) { + try { + const content = await zip.files[filename].async("text"); + const parsed = JSON.parse(content); + if (parsed.modules && Object.keys(parsed.modules).length > 0) { + _moduleScriptPaths.add( + filename.slice(0, -".script.json".length) + ); + } + } catch {} + } + } + } + return _moduleScriptPaths; + } + async function _internal_file( p: string, f: JSZip.JSZipObject, @@ -575,7 +603,22 @@ function ZipFSElement( } } - const finalPath = transformPath(); + let finalPath = transformPath(); + + // Redirect content files for scripts with modules into __mod/ folder + if (kind == "other" && exts.some((ext) => p.endsWith(ext))) { + const normalizedP = p.replace(/^\.[\\/]/, ""); + const moduleScripts = await getModuleScriptPaths(); + for (const basePath of moduleScripts) { + if (normalizedP.startsWith(basePath + ".")) { + const ext = normalizedP.slice(basePath.length); // e.g., ".ts", ".py" + const dir = path.dirname(finalPath); + const base = path.basename(basePath); + finalPath = path.join(dir, base + getModuleFolderSuffix(), "script" + ext); + break; + } + } + } const r = [ { @@ -893,15 +936,23 @@ function ZipFSElement( log.error(`Failed to parse script.yaml at path: ${p}`); throw error; } + const hasModules = parsed["modules"] && Object.keys(parsed["modules"]).length > 0; if ( parsed["lock"] && parsed["lock"] != "" && parsed["codebase"] == undefined ) { - parsed["lock"] = - "!inline " + - removeSuffix(p.replaceAll(SEP, "/"), ".json") + - ".lock"; + if (hasModules) { + // Lock lives inside __mod/ folder as script.lock + const scriptBase = removeSuffix(removeSuffix(p.replaceAll(SEP, "/"), ".json"), ".script"); + parsed["lock"] = + "!inline " + scriptBase + getModuleFolderSuffix() + "/script.lock"; + } else { + parsed["lock"] = + "!inline " + + removeSuffix(p.replaceAll(SEP, "/"), ".json") + + ".lock"; + } } else if (parsed["lock"] == "") { parsed["lock"] = ""; } else { @@ -910,6 +961,8 @@ function ZipFSElement( if (ignoreCodebaseChanges && parsed["codebase"]) { parsed["codebase"] = undefined; } + // Modules are stored as files in __mod/ folder, not in metadata + delete parsed["modules"]; return useYaml ? yamlStringify(parsed, yamlOptions) : JSON.stringify(parsed, null, 2); @@ -969,16 +1022,71 @@ function ZipFSElement( throw error; } const lock = parsed["lock"]; + const scriptModules: Record | undefined = parsed["modules"]; + const hasModules = scriptModules && Object.keys(scriptModules).length > 0; + + // Compute base path and module folder + const metaExt = useYaml ? ".yaml" : ".json"; + const scriptBasePath = removeSuffix( + removeSuffix(finalPath, metaExt), + ".script" + ); + const moduleFolderPath = scriptBasePath + getModuleFolderSuffix(); + + if (hasModules) { + // Redirect metadata into __mod/script.yaml + r[0].path = path.join(moduleFolderPath, "script" + metaExt); + } + if (lock && lock != "") { r.push({ isDirectory: false, - path: removeSuffix(finalPath, ".json") + ".lock", + path: hasModules + ? path.join(moduleFolderPath, "script.lock") + : removeSuffix(finalPath, metaExt) + ".lock", async *getChildren() {}, async getContentText() { return lock; }, }); } + + // Extract script modules into __mod/ folder + if (hasModules) { + r.push({ + isDirectory: true, + path: moduleFolderPath, + async *getChildren() { + for (const [relPath, mod] of Object.entries(scriptModules!)) { + // Yield the module content file + yield { + isDirectory: false, + path: path.join(moduleFolderPath, relPath), + async *getChildren() {}, + async getContentText() { + return mod.content; + }, + }; + + // Yield the module lock file if present + if (mod.lock) { + const baseName = relPath.replace(/\.[^.]+$/, ''); + yield { + isDirectory: false, + path: path.join(moduleFolderPath, baseName + ".lock"), + async *getChildren() {}, + async getContentText() { + return mod.lock!; + }, + }; + } + } + }, + async getContentText() { + throw new Error("Cannot get content of directory"); + }, + }); + } } if (kind == "resource") { const content = await f.async("text"); @@ -1154,6 +1262,12 @@ export async function elementsToMap( continue; } const path = entry.path; + // Include module files in the map so they're compared for changes, + // but they're pushed as part of their parent script via handleFile + if (isScriptModulePath(path)) { + map[path] = await entry.getContentText(); + continue; + } if ( !isFileResource(path) && !isFilesetResource(path) && @@ -1600,6 +1714,11 @@ const isNotWmillFile = (p: string, isDirectory: boolean) => { ); } + // Files inside __mod/ folders are script module files — always valid wmill files + if (isScriptModulePath(p)) { + return false; + } + try { const typ = getTypeStrFromPath(p); if ( @@ -1744,6 +1863,37 @@ async function addToChangedIfNotExists(p: string, tracker: ChangeTracker) { if (!tracker.rawApps.includes(folder)) { tracker.rawApps.push(folder); } + } else if (isScriptModulePath(p)) { + if (isModuleEntryPoint(p)) { + // Entry point (e.g. __mod/script.ts) IS the parent script content file + if (!tracker.scripts.includes(p)) { + tracker.scripts.push(p); + } + } else { + // Module file changed — find the parent script content file + const moduleSuffix = getModuleFolderSuffix() + "/"; + const idx = p.indexOf(moduleSuffix); + if (idx !== -1) { + const scriptBasePath = p.substring(0, idx); + // Try folder layout first: __mod/script.{ext} + try { + const contentPath = await findContentFile(scriptBasePath + getModuleFolderSuffix() + "/script.yaml"); + if (contentPath && !tracker.scripts.includes(contentPath)) { + tracker.scripts.push(contentPath); + } + } catch { + // Fall back to flat layout: scriptBasePath.script.yaml + try { + const contentPath = await findContentFile(scriptBasePath + ".script.yaml"); + if (contentPath && !tracker.scripts.includes(contentPath)) { + tracker.scripts.push(contentPath); + } + } catch { + // ignore — content file not found + } + } + } + } } else { if (!tracker.scripts.includes(p)) { tracker.scripts.push(p); @@ -1776,6 +1926,61 @@ async function buildTracker(changes: Change[]) { return tracker; } +/** + * When a module file changes, find and push the parent script. + * The parent script's handleFile will read the __mod/ folder and include all modules. + */ +async function pushParentScriptForModule( + modulePath: string, + workspace: Workspace, + alreadySynced: string[], + message: string | undefined, + opts: (GlobalOptions & { defaultTs?: "bun" | "deno" } & Skips) | undefined, + rawWorkspaceDependencies: Record, + codebases: SyncCodebase[], +): Promise { + const moduleSuffix = getModuleFolderSuffix() + "/"; + const idx = modulePath.indexOf(moduleSuffix); + if (idx === -1) return; + const scriptBasePath = modulePath.substring(0, idx); + const moduleFolderPath = scriptBasePath + getModuleFolderSuffix(); + + // Try folder layout first: look for script.{ext} inside __mod/ + try { + const entryPoint = await findContentFile(moduleFolderPath + "/script.yaml"); + if (entryPoint) { + await handleFile( + entryPoint, + workspace, + alreadySynced, + message, + opts, + rawWorkspaceDependencies, + codebases, + ); + return; + } + } catch {} + + // Fall back to flat layout: look for content file alongside __mod/ + try { + const contentPath = await findContentFile(scriptBasePath + ".script.yaml"); + if (contentPath) { + await handleFile( + contentPath, + workspace, + alreadySynced, + message, + opts, + rawWorkspaceDependencies, + codebases, + ); + } + } catch { + log.debug(`Could not find parent script for module: ${modulePath}`); + } +} + export async function pull( opts: GlobalOptions & SyncOptions & { repository?: string; promotion?: string; branch?: string }, @@ -2704,6 +2909,21 @@ export async function push( await writeFile(stateTarget, change.after, "utf-8"); } continue; + } else if (isScriptModulePath(change.path)) { + // Module file changed — push the parent script + await pushParentScriptForModule( + change.path, + workspace, + alreadySynced, + opts.message, + opts, + rawWorkspaceDependencies, + codebases, + ); + if (stateTarget) { + await writeFile(stateTarget, change.after, "utf-8"); + } + continue; } if (stateTarget) { await mkdir(path.dirname(stateTarget), { recursive: true }); @@ -2828,6 +3048,17 @@ export async function push( ) ) { continue; + } else if (isScriptModulePath(change.path)) { + await pushParentScriptForModule( + change.path, + workspace, + alreadySynced, + opts.message, + opts, + rawWorkspaceDependencies, + codebases, + ); + continue; } if (stateTarget) { await mkdir(path.dirname(stateTarget), { recursive: true }); @@ -2869,6 +3100,19 @@ export async function push( if (change.path.endsWith(".lock")) { continue; } + if (isScriptModulePath(change.path)) { + // Module file deleted — push the parent script (which will now have fewer modules) + await pushParentScriptForModule( + change.path, + workspace, + alreadySynced, + opts.message, + opts, + rawWorkspaceDependencies, + codebases, + ); + continue; + } const typ = getTypeStrFromPath(change.path); if (typ == "script") { diff --git a/cli/src/types.ts b/cli/src/types.ts index 69592664f8ac4..8ba36a0ea0198 100644 --- a/cli/src/types.ts +++ b/cli/src/types.ts @@ -28,6 +28,7 @@ import { isRawAppPath, extractResourceName, buildFolderPath, + isScriptModulePath, } from "./utils/resource_folders.ts"; export interface DifferenceCreate { @@ -259,6 +260,9 @@ export function getTypeStrFromPath( | "settings" | "encryption_key" | "workspace_dependencies" { + if (isScriptModulePath(p)) { + return "script"; + } if (isFlowPath(p)) { return "flow"; } diff --git a/cli/src/utils/metadata.ts b/cli/src/utils/metadata.ts index a220f67956320..f211e3dc71566 100644 --- a/cli/src/utils/metadata.ts +++ b/cli/src/utils/metadata.ts @@ -5,7 +5,8 @@ import * as log from "../core/log.ts"; import { stringify as yamlStringify } from "yaml"; import { yamlParseFile } from "./yaml.ts"; import { readFile, writeFile, stat, rm, readdir } from "node:fs/promises"; -import { readFileSync } from "node:fs"; +import { readFileSync, existsSync, readdirSync, statSync, mkdirSync, writeFileSync } from "node:fs"; +import * as path from "node:path"; import { createRequire } from "node:module"; import { ScriptMetadata, @@ -15,8 +16,10 @@ import { Workspace } from "../commands/workspace/workspace.ts"; import { ScriptLanguage, workspaceDependenciesLanguages, + languageNeedsLock, } from "./script_common.ts"; import { inferContentTypeFromFilePath } from "./script_common.ts"; +import { getModuleFolderSuffix, isModuleEntryPoint, getScriptBasePathFromModulePath } from "./resource_folders.ts"; import { findCodebase, yamlOptions } from "../commands/sync/sync.ts"; import { generateHash, readInlinePathSync, getHeaders } from "./utils.ts"; @@ -185,13 +188,18 @@ export async function generateScriptMetadataInternal( codebases: SyncCodebase[], justUpdateMetadataLock?: boolean ): Promise { - const remotePath = scriptPath - .substring(0, scriptPath.indexOf(".")) - .replaceAll(SEP, "/"); + // Detect folder layout: my_script__mod/script.ts + const isFolderLayout = isModuleEntryPoint(scriptPath); - const language = inferContentTypeFromFilePath(scriptPath, opts.defaultTs); + // remotePath is the Windmill API path (e.g., "u/admin/my_script") + const remotePath = isFolderLayout + ? getScriptBasePathFromModulePath(scriptPath)!.replaceAll(SEP, "/") + : scriptPath.substring(0, scriptPath.indexOf(".")).replaceAll(SEP, "/"); + const language = inferContentTypeFromFilePath(scriptPath, opts.defaultTs); + // For folder layout, parseMetadataFile is called with remotePath which + // will find __mod/script.yaml via the folder layout fallback const metadataWithType = await parseMetadataFile( remotePath, undefined, @@ -207,11 +215,35 @@ export async function generateScriptMetadataInternal( language ); + // Compute the module folder path early so we can include module hashes in stale check + const moduleFolderPath = isFolderLayout + ? path.dirname(scriptPath) + : scriptPath.substring(0, scriptPath.indexOf(".")) + getModuleFolderSuffix(); + + const hasModules = existsSync(moduleFolderPath) && statSync(moduleFolderPath).isDirectory(); - // Note: rawWorkspaceDependencies are now passed in as parameter instead of being searched hierarchically let hash = await generateScriptHash(filteredRawWorkspaceDependencies, scriptContent, metadataContent); - if (await checkifMetadataUptodate(remotePath, hash, undefined)) { + // Compute per-module hashes for stale detection (like flow inline scripts) + let moduleHashes: Record = {}; + if (hasModules) { + moduleHashes = await computeModuleHashes( + moduleFolderPath, opts.defaultTs, rawWorkspaceDependencies, isFolderLayout + ); + } + const hasModuleHashes = Object.keys(moduleHashes).length > 0; + + // If modules exist, combine main script hash + module hashes into a meta-hash + let checkHash = hash; + let checkSubpath: string | undefined; + if (hasModuleHashes) { + const sortedEntries = Object.entries(moduleHashes).sort(([a], [b]) => a.localeCompare(b)); + checkHash = await generateHash(hash + JSON.stringify(sortedEntries)); + checkSubpath = SCRIPT_TOP_HASH; + } + + const conf = await readLockfile(); + if (await checkifMetadataUptodate(remotePath, checkHash, conf, checkSubpath)) { if (!noStaleMessage) { log.info( colors.green(`Script ${remotePath} metadata is up-to-date, skipping`) @@ -219,7 +251,19 @@ export async function generateScriptMetadataInternal( } return; } else if (dryRun) { - return `${remotePath} (${language})`; + let detail = `${remotePath} (${language})`; + if (hasModuleHashes) { + const changed: string[] = []; + for (const [modulePath, moduleHash] of Object.entries(moduleHashes)) { + if (!(await checkifMetadataUptodate(remotePath, moduleHash, conf, modulePath))) { + changed.push(modulePath); + } + } + if (changed.length > 0) { + detail += ` [changed modules: ${changed.join(", ")}]`; + } + } + return detail; } if (!justUpdateMetadataLock && !noStaleMessage) { @@ -244,26 +288,71 @@ export async function generateScriptMetadataInternal( const hasCodebase = findCodebase(scriptPath, codebases) != undefined; if (!hasCodebase) { + const lockPathOverride = isFolderLayout + ? path.dirname(scriptPath) + "/script.lock" + : undefined; await updateScriptLock( workspace, scriptContent, language, remotePath, metadataParsedContent, - filteredRawWorkspaceDependencies + filteredRawWorkspaceDependencies, + lockPathOverride, ); } else { metadataParsedContent.lock = ""; } + + // Generate locks for modules in __mod/ folder + if (hasModules) { + // Identify which modules changed by comparing per-module hashes + let changedModules: string[] | undefined; + if (hasModuleHashes) { + changedModules = []; + for (const [modulePath, moduleHash] of Object.entries(moduleHashes)) { + if (!(await checkifMetadataUptodate(remotePath, moduleHash, conf, modulePath))) { + changedModules.push(modulePath); + } + } + if (changedModules.length === 0) { + changedModules = undefined; // no modules changed, skip lock regeneration + } + } + await updateModuleLocks( + workspace, moduleFolderPath, "", remotePath, + rawWorkspaceDependencies, opts.defaultTs, changedModules, + ); + } } else { - metadataParsedContent.lock = - "!inline " + remotePath.replaceAll(SEP, "/") + ".script.lock"; + if (isFolderLayout) { + metadataParsedContent.lock = + "!inline " + remotePath.replaceAll(SEP, "/") + getModuleFolderSuffix() + "/script.lock"; + } else { + metadataParsedContent.lock = + "!inline " + remotePath.replaceAll(SEP, "/") + ".script.lock"; + } } - let metaPath = remotePath + ".script.yaml"; - let newMetadataContent = yamlStringify(metadataParsedContent, yamlOptions); - if (metadataWithType.isJson) { - metaPath = remotePath + ".script.json"; - newMetadataContent = JSON.stringify(metadataParsedContent); + + // Write metadata back to the correct path + let metaPath: string; + let newMetadataContent: string; + if (isFolderLayout) { + if (metadataWithType.isJson) { + metaPath = path.dirname(scriptPath) + "/script.json"; + newMetadataContent = JSON.stringify(metadataParsedContent); + } else { + metaPath = path.dirname(scriptPath) + "/script.yaml"; + newMetadataContent = yamlStringify(metadataParsedContent, yamlOptions); + } + } else { + if (metadataWithType.isJson) { + metaPath = remotePath + ".script.json"; + newMetadataContent = JSON.stringify(metadataParsedContent); + } else { + metaPath = remotePath + ".script.yaml"; + newMetadataContent = yamlStringify(metadataParsedContent, yamlOptions); + } } const metadataContentUsedForHash = newMetadataContent; @@ -273,7 +362,21 @@ export async function generateScriptMetadataInternal( scriptContent, metadataContentUsedForHash ); - await updateMetadataGlobalLock(remotePath, hash); + + // Store hashes in wmill-lock.yaml + if (hasModuleHashes) { + // Use per-module hash tracking (like flow inline scripts) + const sortedEntries = Object.entries(moduleHashes).sort(([a], [b]) => a.localeCompare(b)); + const metaHash = await generateHash(hash + JSON.stringify(sortedEntries)); + await clearGlobalLock(remotePath); + await updateMetadataGlobalLock(remotePath, metaHash, SCRIPT_TOP_HASH); + for (const [modulePath, moduleHash] of Object.entries(moduleHashes)) { + await updateMetadataGlobalLock(remotePath, moduleHash, modulePath); + } + } else { + await updateMetadataGlobalLock(remotePath, hash); + } + if (!justUpdateMetadataLock) { await writeFile(metaPath, newMetadataContent, "utf-8"); } @@ -299,11 +402,9 @@ export async function updateScriptSchema( } else { delete metadataContent.has_preprocessor; } - if (result.no_main_func) { - metadataContent.no_main_func = result.no_main_func; - } else { - delete metadataContent.no_main_func; - } + // auto_kind is intentionally not written to metadata — it is auto-detected + // by the parser at deploy time from script content. + delete metadataContent.auto_kind; } // --------------------------------------------------------------------------- @@ -502,7 +603,8 @@ async function updateScriptLock( language: ScriptLanguage, remotePath: string, metadataContent: Record, - rawWorkspaceDependencies: Record + rawWorkspaceDependencies: Record, + lockPathOverride?: string, ): Promise { if ( !( @@ -530,7 +632,7 @@ async function updateScriptLock( rawWorkspaceDependencies, ); - const lockPath = remotePath + ".script.lock"; + const lockPath = lockPathOverride ?? remotePath + ".script.lock"; if (lock != "") { await writeFile(lockPath, lock, "utf-8"); metadataContent.lock = "!inline " + lockPath.replaceAll(SEP, "/"); @@ -546,6 +648,82 @@ async function updateScriptLock( } } +/** + * Generate locks for all module files in a __mod/ directory. + * Recursively walks the directory and generates a lock for each module + * whose language requires one. + */ +async function updateModuleLocks( + workspace: Workspace, + dirPath: string, + relPrefix: string, + scriptRemotePath: string, + rawWorkspaceDependencies: Record, + defaultTs: "bun" | "deno" | undefined, + changedModules?: string[], +): Promise { + const entries = readdirSync(dirPath, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(dirPath, entry.name); + const relPath = relPrefix ? relPrefix + "/" + entry.name : entry.name; + + if (entry.isDirectory()) { + await updateModuleLocks(workspace, fullPath, relPath, scriptRemotePath, rawWorkspaceDependencies, defaultTs, changedModules); + } else if (entry.isFile() + && !entry.name.endsWith(".lock") + // In folder layout, skip entry point files (script.{ext}, script.yaml, script.json, script.lock) + && !(relPrefix === "" && entry.name.startsWith("script.")) + ) { + let modLanguage: ScriptLanguage; + try { + modLanguage = inferContentTypeFromFilePath(entry.name, defaultTs); + } catch { + continue; // skip files with unrecognized extensions + } + + if (!languageNeedsLock(modLanguage)) continue; + + // Skip unchanged modules when per-module hash tracking is active + if (changedModules) { + const normalizedRelPath = normalizeLockPath(relPath); + if (!changedModules.includes(normalizedRelPath)) continue; + } + + const moduleContent = readFileSync(fullPath, "utf-8"); + const moduleRemotePath = scriptRemotePath + "/" + relPath; + + log.info(colors.gray(`Generating lock for module ${relPath}`)); + + try { + const lock = await fetchScriptLock( + workspace, + moduleContent, + modLanguage, + moduleRemotePath, + rawWorkspaceDependencies, + ); + + const baseName = entry.name.replace(/\.[^.]+$/, ''); + const lockPath = path.join(dirPath, baseName + ".lock"); + if (lock != "") { + writeFileSync(lockPath, lock, "utf-8"); + } else { + try { + if (existsSync(lockPath)) { + const { rm: rmAsync } = await import("node:fs/promises"); + await rmAsync(lockPath); + } + } catch { + // ignore + } + } + } catch (e) { + log.info(colors.yellow(`Failed to generate lock for module ${relPath}: ${e}`)); + } + } + } +} + //////////////////////////////////////////////////////////////////////////////////////////// // below functions copied from Windmill's FE inferArgs function. TODO: refactor // //////////////////////////////////////////////////////////////////////////////////////////// @@ -557,7 +735,7 @@ export async function inferSchema( ): Promise<{ schema: any; has_preprocessor: boolean | undefined; - no_main_func: boolean | undefined; + auto_kind: string | undefined; }> { let inferedSchema: any; if (language === "python3") { @@ -667,7 +845,7 @@ export async function inferSchema( return { schema: defaultScriptMetadata().schema, has_preprocessor: false, - no_main_func: false, + auto_kind: undefined, }; } @@ -707,7 +885,7 @@ export async function inferSchema( return { schema: currentSchema, has_preprocessor: inferedSchema.has_preprocessor, - no_main_func: inferedSchema.no_main_func, + auto_kind: inferedSchema.auto_kind, }; } @@ -776,62 +954,87 @@ export async function parseMetadataFile( isJson: false, }; } catch { - // no metadata file at all. Create it - log.info( - (await blueColor())( - `Creating script metadata file for ${metadataFilePath}` - ) - ); - metadataFilePath = scriptPath + ".script.yaml"; - let scriptInitialMetadata = defaultScriptMetadata(); - const lockPath = scriptPath + ".script.lock"; - scriptInitialMetadata.lock = "!inline " + lockPath; - const scriptInitialMetadataYaml = yamlStringify( - scriptInitialMetadata as Record, - yamlOptions - ); - - await writeFile(metadataFilePath, scriptInitialMetadataYaml, { flag: "wx", encoding: "utf-8" }); - await writeFile(lockPath, "", { flag: "wx", encoding: "utf-8" }); - - if (generateMetadataIfMissing) { - log.info( - (await blueColor())( - `Generating lockfile and schema for ${metadataFilePath}` - ) - ); + // Try folder layout: {scriptPath}__mod/script.yaml or .json + const moduleFolderMeta = scriptPath + getModuleFolderSuffix(); + try { + metadataFilePath = moduleFolderMeta + "/script.json"; + await stat(metadataFilePath); + return { + path: metadataFilePath, + payload: JSON.parse(await readFile(metadataFilePath, "utf-8")), + isJson: true, + }; + } catch { try { - await generateScriptMetadataInternal( - generateMetadataIfMissing.path, - generateMetadataIfMissing.workspaceRemote, - generateMetadataIfMissing, - false, - false, - generateMetadataIfMissing.rawWorkspaceDependencies, - generateMetadataIfMissing.codebases, - false - ); - scriptInitialMetadata = (await yamlParseFile( - metadataFilePath - )) as ScriptMetadata; - if (!generateMetadataIfMissing.schemaOnly) { - replaceLock(scriptInitialMetadata); - } - } catch (e) { - log.info( - colors.yellow( - `Failed to generate lockfile and schema for ${metadataFilePath}: ${e}` - ) - ); + metadataFilePath = moduleFolderMeta + "/script.yaml"; + await stat(metadataFilePath); + const payload: any = await yamlParseFile(metadataFilePath); + replaceLock(payload); + return { + path: metadataFilePath, + payload, + isJson: false, + }; + } catch { + // fall through to create metadata } } - return { - path: metadataFilePath, - payload: scriptInitialMetadata, - isJson: false, - }; } } + // no metadata file at all. Create it + log.info( + (await blueColor())( + `Creating script metadata file for ${metadataFilePath}` + ) + ); + metadataFilePath = scriptPath + ".script.yaml"; + let scriptInitialMetadata = defaultScriptMetadata(); + const lockPath = scriptPath + ".script.lock"; + scriptInitialMetadata.lock = "!inline " + lockPath; + const scriptInitialMetadataYaml = yamlStringify( + scriptInitialMetadata as Record, + yamlOptions + ); + + await writeFile(metadataFilePath, scriptInitialMetadataYaml, { flag: "wx", encoding: "utf-8" }); + await writeFile(lockPath, "", { flag: "wx", encoding: "utf-8" }); + + if (generateMetadataIfMissing) { + log.info( + (await blueColor())( + `Generating lockfile and schema for ${metadataFilePath}` + ) + ); + try { + await generateScriptMetadataInternal( + generateMetadataIfMissing.path, + generateMetadataIfMissing.workspaceRemote, + generateMetadataIfMissing, + false, + false, + generateMetadataIfMissing.rawWorkspaceDependencies, + generateMetadataIfMissing.codebases, + false + ); + scriptInitialMetadata = (await yamlParseFile( + metadataFilePath + )) as ScriptMetadata; + if (!generateMetadataIfMissing.schemaOnly) { + replaceLock(scriptInitialMetadata); + } + } catch (e) { + log.info( + colors.yellow( + `Failed to generate lockfile and schema for ${metadataFilePath}: ${e}` + ) + ); + } + } + return { + path: metadataFilePath, + payload: scriptInitialMetadata, + isJson: false, + }; } interface Lock { @@ -840,6 +1043,7 @@ interface Lock { } const WMILL_LOCKFILE = "wmill-lock.yaml"; +const SCRIPT_TOP_HASH = "__script_hash"; /** * Normalizes a path to use Linux separators (forward slashes). @@ -908,6 +1112,46 @@ export async function generateScriptHash( ); } +async function computeModuleHashes( + moduleFolderPath: string, + defaultTs: "bun" | "deno" | undefined, + rawWorkspaceDependencies: Record, + isFolderLayout: boolean, +): Promise> { + const hashes: Record = {}; + + async function readDir(dirPath: string, relPrefix: string) { + const entries = readdirSync(dirPath, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(dirPath, entry.name); + const relPath = relPrefix ? relPrefix + "/" + entry.name : entry.name; + const isTopLevel = relPrefix === ""; + + if (entry.isDirectory()) { + await readDir(fullPath, relPath); + } else if ( + entry.isFile() && + !entry.name.endsWith(".lock") && + !(isFolderLayout && isTopLevel && entry.name.startsWith("script.")) + ) { + try { + inferContentTypeFromFilePath(entry.name, defaultTs); + } catch { + continue; + } + const content = readFileSync(fullPath, "utf-8"); + const normalizedPath = normalizeLockPath(relPath); + hashes[normalizedPath] = await generateHash( + content + JSON.stringify(rawWorkspaceDependencies) + ); + } + } + } + + await readDir(moduleFolderPath, ""); + return hashes; +} + export async function clearGlobalLock(path: string): Promise { const conf = await readLockfile(); if (!conf?.locks) { diff --git a/cli/src/utils/resource_folders.ts b/cli/src/utils/resource_folders.ts index e24835ab35fcd..713a47ecb1ed0 100644 --- a/cli/src/utils/resource_folders.ts +++ b/cli/src/utils/resource_folders.ts @@ -433,6 +433,66 @@ export function isRawAppFolderMetadataFile(p: string): boolean { ); } +// ============================================================================ +// Script Module Path Functions +// ============================================================================ + +/** + * The suffix used for script module folders. + * Unlike flows/apps, modules always use `__mod` (never dotted `.mod`) + * to avoid confusion with file extensions. + */ +const MODULE_SUFFIX = "__mod"; + +/** + * Get the module folder suffix (always "__mod") + */ +export function getModuleFolderSuffix(): string { + return MODULE_SUFFIX; +} + +/** + * Check if a path is inside a script module folder. + * Matches patterns like: .../my_script__mod/... + */ +export function isScriptModulePath(p: string): boolean { + return normalizeSep(p).includes(MODULE_SUFFIX + "/"); +} + +/** + * Build the module folder path from a script's base path (without extension). + * e.g., "f/my_script" -> "f/my_script__mod" + */ +export function buildModuleFolderPath(scriptBasePath: string): string { + return scriptBasePath + MODULE_SUFFIX; +} + +/** + * Check if a file inside a __mod/ folder is the main entry point (script.{ext}). + * Entry points are files named "script.*" directly under __mod/ (not in subdirs). + */ +export function isModuleEntryPoint(p: string): boolean { + const norm = normalizeSep(p); + const suffix = MODULE_SUFFIX + "/"; + const idx = norm.indexOf(suffix); + if (idx === -1) return false; + const rest = norm.slice(idx + suffix.length); + return rest.startsWith("script.") && !rest.includes("/"); +} + +/** + * Extract the script base path from a module folder entry. + * e.g., "u/admin/my_script__mod/script.ts" -> "u/admin/my_script" + * e.g., "u/admin/my_script__mod/helper.ts" -> "u/admin/my_script" + */ +export function getScriptBasePathFromModulePath(p: string): string | undefined { + const norm = normalizeSep(p); + const suffix = MODULE_SUFFIX + "/"; + const idx = norm.indexOf(suffix); + if (idx === -1) return undefined; + return norm.slice(0, idx); +} + // ============================================================================ // Sync-related Path Functions // ============================================================================ diff --git a/cli/test/preview.test.ts b/cli/test/preview.test.ts index 93dc7def48ef3..4b54184d1b0d7 100644 --- a/cli/test/preview.test.ts +++ b/cli/test/preview.test.ts @@ -359,6 +359,116 @@ schema: }); }); +// ============================================================================= +// SCRIPT WITH MODULES PREVIEW TESTS +// ============================================================================= + +test("script preview: script with modules (taskScript pattern)", async () => { + await withTestBackend(async (backend, tempDir) => { + await createWmillConfig(tempDir, { defaultTs: "bun" }); + + // Create the main script that uses taskScript to call a module + await createScript( + tempDir, + "f/test/wac_script.ts", + `import { task, taskScript, workflow } from "windmill-client"; + +const helper = taskScript("./helper.ts"); + +const process = task(async (x: string): Promise => { + return \`processed: \${x}\`; +}); + +export const main = workflow(async (x: string = "test") => { + const a = await process(x); + const b = await helper({ a }); + return { processed: a, helper_result: b }; +});` + ); + + // Create the module file in __mod/ folder + const modDir = `${tempDir}/f/test/wac_script__mod`; + await mkdir(modDir, { recursive: true }); + await writeFile( + `${modDir}/helper.ts`, + `export function main(a: string): string { + return \`helper got: \${a}\`; +}`, + "utf-8" + ); + + const result = await backend.runCLICommand( + ["script", "preview", "f/test/wac_script.ts"], + tempDir + ); + + expect(result.code).toEqual(0); + const output = result.stdout + result.stderr; + expect(output).toContain("processed: test"); + expect(output).toContain("helper got:"); + }); +}); + +test("script preview: script with modules (folder layout)", async () => { + await withTestBackend(async (backend, tempDir) => { + await createWmillConfig(tempDir, { defaultTs: "bun" }); + + // Create folder layout: my_script__mod/script.ts + my_script__mod/helper.ts + const modDir = `${tempDir}/f/test/folder_wac__mod`; + await mkdir(modDir, { recursive: true }); + + // Entry point script + await writeFile( + `${modDir}/script.ts`, + `import { task, taskScript, workflow } from "windmill-client"; + +const helper = taskScript("./helper.ts"); + +export const main = workflow(async (name: string = "World") => { + const result = await helper({ name }); + return { greeting: result }; +});`, + "utf-8" + ); + + // Module file + await writeFile( + `${modDir}/helper.ts`, + `export function main(name: string): string { + return \`Hello from module, \${name}!\`; +}`, + "utf-8" + ); + + // Script metadata + await writeFile( + `${modDir}/script.yaml`, + `summary: "Folder layout WAC script" +description: "Test" +lock: "" +schema: + $schema: "https://json-schema.org/draft/2020-12/schema" + type: object + properties: + name: + type: string + default: "World" + required: [] +`, + "utf-8" + ); + + const result = await backend.runCLICommand( + ["script", "preview", `f/test/folder_wac__mod/script.ts`], + tempDir + ); + + expect(result.code).toEqual(0); + const output = result.stdout + result.stderr; + expect(output).toContain("Hello from module, World!"); + }); +}); + // ============================================================================= // FLOW PREVIEW TESTS // ============================================================================= diff --git a/cli/test/resource_folders_unit.test.ts b/cli/test/resource_folders_unit.test.ts index cbcf6d72eaab7..da8ccd7ece390 100644 --- a/cli/test/resource_folders_unit.test.ts +++ b/cli/test/resource_folders_unit.test.ts @@ -32,6 +32,8 @@ import { isRawAppFolderMetadataFile, getDeleteSuffix, transformJsonPathToDir, + isModuleEntryPoint, + getScriptBasePathFromModulePath, } from "../src/utils/resource_folders.ts"; import { removeWorkerPrefix } from "../src/commands/worker-groups/worker-groups.ts"; @@ -504,6 +506,70 @@ describe("transformJsonPathToDir", () => { // removeWorkerPrefix (from worker-groups.ts) // ============================================================================= +// ============================================================================= +// Module Path Functions +// ============================================================================= + +describe("isModuleEntryPoint", () => { + test("detects entry point files in __mod folders", () => { + expect(isModuleEntryPoint("f/my_script__mod/script.ts")).toBe(true); + expect(isModuleEntryPoint("u/admin/tool__mod/script.py")).toBe(true); + expect(isModuleEntryPoint("f/nested/path/script__mod/script.go")).toBe(true); + }); + + test("rejects non-entry-point files in __mod folders", () => { + expect(isModuleEntryPoint("f/my_script__mod/helper.ts")).toBe(false); + expect(isModuleEntryPoint("f/my_script__mod/utils/math.py")).toBe(false); + expect(isModuleEntryPoint("f/my_script__mod/helper.lock")).toBe(false); + }); + + test("rejects entry point files in subdirectories of __mod", () => { + expect(isModuleEntryPoint("f/my_script__mod/sub/script.ts")).toBe(false); + }); + + test("rejects paths without __mod", () => { + expect(isModuleEntryPoint("f/my_script.ts")).toBe(false); + expect(isModuleEntryPoint("f/script.ts")).toBe(false); + }); + + test("handles windows-style separators", () => { + expect(isModuleEntryPoint("f\\my_script__mod\\script.ts")).toBe(true); + expect(isModuleEntryPoint("f\\my_script__mod\\helper.ts")).toBe(false); + }); + + test("matches any extension for script.*", () => { + expect(isModuleEntryPoint("f/x__mod/script.yaml")).toBe(true); + expect(isModuleEntryPoint("f/x__mod/script.json")).toBe(true); + expect(isModuleEntryPoint("f/x__mod/script.lock")).toBe(true); + expect(isModuleEntryPoint("f/x__mod/script.sh")).toBe(true); + }); +}); + +describe("getScriptBasePathFromModulePath", () => { + test("extracts base path from module entry point", () => { + expect(getScriptBasePathFromModulePath("f/my_script__mod/script.ts")).toBe("f/my_script"); + expect(getScriptBasePathFromModulePath("u/admin/tool__mod/script.py")).toBe("u/admin/tool"); + }); + + test("extracts base path from module helper files", () => { + expect(getScriptBasePathFromModulePath("f/my_script__mod/helper.ts")).toBe("f/my_script"); + expect(getScriptBasePathFromModulePath("f/my_script__mod/utils/math.py")).toBe("f/my_script"); + }); + + test("extracts base path from nested script paths", () => { + expect(getScriptBasePathFromModulePath("f/deeply/nested/script__mod/helper.ts")).toBe("f/deeply/nested/script"); + }); + + test("returns undefined for non-module paths", () => { + expect(getScriptBasePathFromModulePath("f/my_script.ts")).toBeUndefined(); + expect(getScriptBasePathFromModulePath("f/my_flow.flow/flow.yaml")).toBeUndefined(); + }); + + test("handles windows-style separators", () => { + expect(getScriptBasePathFromModulePath("f\\my_script__mod\\helper.ts")).toBe("f/my_script"); + }); +}); + describe("removeWorkerPrefix", () => { test("removes worker__ prefix", () => { expect(removeWorkerPrefix("worker__default")).toBe("default"); diff --git a/cli/test/script_modules.test.ts b/cli/test/script_modules.test.ts new file mode 100644 index 0000000000000..c797111e002bb --- /dev/null +++ b/cli/test/script_modules.test.ts @@ -0,0 +1,323 @@ +/** + * Unit tests for script module utilities. + * These tests require no backend — they test standalone logic. + */ + +import { expect, test, describe, beforeEach, afterEach } from "bun:test"; +import * as fs from "node:fs"; +import * as path from "node:path"; +import * as os from "node:os"; +import { + getModuleFolderSuffix, + isScriptModulePath, + buildModuleFolderPath, + isModuleEntryPoint, + getScriptBasePathFromModulePath, +} from "../src/utils/resource_folders.ts"; +import { + writeModulesToDisk, + readModulesFromDisk, +} from "../src/commands/script/script.ts"; +import { getTypeStrFromPath } from "../src/types.ts"; + +// ============================================================================= +// Module Path Utilities +// ============================================================================= + +describe("getModuleFolderSuffix", () => { + test("returns __mod", () => { + expect(getModuleFolderSuffix()).toBe("__mod"); + }); +}); + +describe("isScriptModulePath", () => { + test("detects module paths", () => { + expect(isScriptModulePath("f/my_script__mod/helper.ts")).toBe(true); + expect(isScriptModulePath("u/admin/script__mod/utils/math.py")).toBe(true); + }); + + test("rejects non-module paths", () => { + expect(isScriptModulePath("f/my_script.ts")).toBe(false); + expect(isScriptModulePath("f/my_script__mod")).toBe(false); // no trailing / + expect(isScriptModulePath("f/my_flow__flow/flow.yaml")).toBe(false); + }); + + test("handles windows-style separators", () => { + expect(isScriptModulePath("f\\my_script__mod\\helper.ts")).toBe(true); + }); +}); + +describe("buildModuleFolderPath", () => { + test("appends __mod suffix", () => { + expect(buildModuleFolderPath("f/my_script")).toBe("f/my_script__mod"); + expect(buildModuleFolderPath("u/admin/tool")).toBe("u/admin/tool__mod"); + }); +}); + +// ============================================================================= +// writeModulesToDisk / readModulesFromDisk round-trip +// ============================================================================= + +describe("module read/write round-trip", () => { + let tempDir: string; + + beforeEach(() => { + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "wm-module-test-")); + }); + + afterEach(() => { + fs.rmSync(tempDir, { recursive: true, force: true }); + }); + + test("writes and reads back a simple module", async () => { + const moduleFolderPath = path.join(tempDir, "my_script__mod"); + const modules = { + "helper.ts": { + content: 'export function greet() { return "hi"; }\n', + language: "bun" as const, + }, + }; + + await writeModulesToDisk(moduleFolderPath, modules, "bun"); + + // Verify file exists on disk + expect(fs.existsSync(path.join(moduleFolderPath, "helper.ts"))).toBe(true); + + // Read back + const result = await readModulesFromDisk(moduleFolderPath, "bun", false); + expect(result).toBeDefined(); + expect(Object.keys(result!)).toEqual(["helper.ts"]); + expect(result!["helper.ts"].content).toBe('export function greet() { return "hi"; }\n'); + expect(result!["helper.ts"].language).toBe("bun"); + }); + + test("writes and reads back module with lock file", async () => { + const moduleFolderPath = path.join(tempDir, "my_script__mod"); + const modules = { + "helper.py": { + content: "def greet():\n return 'hi'\n", + language: "python3" as const, + lock: "requests==2.31.0\n", + }, + }; + + await writeModulesToDisk(moduleFolderPath, modules, undefined); + + // Verify lock file exists + expect(fs.existsSync(path.join(moduleFolderPath, "helper.lock"))).toBe(true); + + // Read back + const result = await readModulesFromDisk(moduleFolderPath, undefined, false); + expect(result).toBeDefined(); + expect(result!["helper.py"].lock).toBe("requests==2.31.0\n"); + }); + + test("writes and reads back nested module paths", async () => { + const moduleFolderPath = path.join(tempDir, "my_script__mod"); + const modules = { + "utils/math.py": { + content: "def add(a, b):\n return a + b\n", + language: "python3" as const, + }, + "utils/__init__.py": { + content: "", + language: "python3" as const, + }, + }; + + await writeModulesToDisk(moduleFolderPath, modules, undefined); + + // Verify nested structure + expect(fs.existsSync(path.join(moduleFolderPath, "utils", "math.py"))).toBe(true); + expect(fs.existsSync(path.join(moduleFolderPath, "utils", "__init__.py"))).toBe(true); + + // Read back + const result = await readModulesFromDisk(moduleFolderPath, undefined, false); + expect(result).toBeDefined(); + expect(Object.keys(result!).sort()).toEqual(["utils/__init__.py", "utils/math.py"]); + expect(result!["utils/math.py"].content).toBe("def add(a, b):\n return a + b\n"); + }); + + test("returns undefined for non-existent folder", async () => { + const result = await readModulesFromDisk(path.join(tempDir, "nonexistent__mod"), undefined, false); + expect(result).toBeUndefined(); + }); + + test("returns undefined for empty folder", async () => { + const emptyFolder = path.join(tempDir, "empty__mod"); + fs.mkdirSync(emptyFolder); + const result = await readModulesFromDisk(emptyFolder, undefined, false); + expect(result).toBeUndefined(); + }); + + test("multiple modules of different languages", async () => { + const moduleFolderPath = path.join(tempDir, "my_script__mod"); + const modules = { + "helper.ts": { + content: "export const x = 1;\n", + language: "bun" as const, + }, + "other.ts": { + content: "export const y = 2;\n", + language: "bun" as const, + lock: "some-lock-content\n", + }, + }; + + await writeModulesToDisk(moduleFolderPath, modules, "bun"); + const result = await readModulesFromDisk(moduleFolderPath, "bun", false); + + expect(result).toBeDefined(); + expect(Object.keys(result!).sort()).toEqual(["helper.ts", "other.ts"]); + expect(result!["other.ts"].lock).toBe("some-lock-content\n"); + expect(result!["helper.ts"].lock).toBeUndefined(); + }); + + test("folder layout skips entry point files", async () => { + const moduleFolderPath = path.join(tempDir, "my_script__mod"); + fs.mkdirSync(moduleFolderPath, { recursive: true }); + + // Simulate folder layout: script.ts is the entry point, helper.ts is a module + fs.writeFileSync(path.join(moduleFolderPath, "script.ts"), "export function main() {}\n"); + fs.writeFileSync(path.join(moduleFolderPath, "script.yaml"), "description: test\n"); + fs.writeFileSync(path.join(moduleFolderPath, "script.lock"), ""); + fs.writeFileSync(path.join(moduleFolderPath, "helper.ts"), "export const x = 1;\n"); + + // With folderLayout=true, entry point files should be skipped + const result = await readModulesFromDisk(moduleFolderPath, "bun", true); + expect(result).toBeDefined(); + expect(Object.keys(result!)).toEqual(["helper.ts"]); + + // With folderLayout=false, all files are included + const resultFlat = await readModulesFromDisk(moduleFolderPath, "bun", false); + expect(resultFlat).toBeDefined(); + expect(Object.keys(resultFlat!).sort()).toContain("script.ts"); + expect(Object.keys(resultFlat!).sort()).toContain("helper.ts"); + }); +}); + +// ============================================================================= +// getTypeStrFromPath with module paths +// ============================================================================= + +describe("getTypeStrFromPath with module paths", () => { + test("recognizes module content files as scripts", () => { + expect(getTypeStrFromPath("f/my_script__mod/helper.ts")).toBe("script"); + expect(getTypeStrFromPath("u/admin/tool__mod/utils/math.py")).toBe("script"); + expect(getTypeStrFromPath("f/x__mod/helper.go")).toBe("script"); + }); + + test("recognizes module lock files as scripts", () => { + // Lock files inside __mod/ should NOT throw — they are valid module files + expect(getTypeStrFromPath("f/my_script__mod/helper.lock")).toBe("script"); + expect(getTypeStrFromPath("u/admin/tool__mod/utils/math.lock")).toBe("script"); + }); + + test("recognizes module entry points as scripts", () => { + expect(getTypeStrFromPath("f/my_script__mod/script.ts")).toBe("script"); + expect(getTypeStrFromPath("f/my_script__mod/script.py")).toBe("script"); + }); + + test("recognizes module metadata files as scripts", () => { + // .yaml/.json files inside __mod/ should be recognized + expect(getTypeStrFromPath("f/my_script__mod/script.yaml")).toBe("script"); + expect(getTypeStrFromPath("f/my_script__mod/script.json")).toBe("script"); + }); +}); + +// ============================================================================= +// Module read/write with multiple lock files +// ============================================================================= + +describe("module lock file handling", () => { + let tempDir: string; + + beforeEach(() => { + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "wm-module-lock-test-")); + }); + + afterEach(() => { + fs.rmSync(tempDir, { recursive: true, force: true }); + }); + + test("writes and reads multiple modules each with their own lock", async () => { + const moduleFolderPath = path.join(tempDir, "my_script__mod"); + const modules = { + "api_client.py": { + content: "import requests\ndef fetch(): pass\n", + language: "python3" as const, + lock: "requests==2.31.0\nurllib3==2.0.4\n", + }, + "data_processor.py": { + content: "import pandas\ndef process(): pass\n", + language: "python3" as const, + lock: "pandas==2.1.0\nnumpy==1.25.0\n", + }, + "utils.py": { + content: "def helper(): pass\n", + language: "python3" as const, + // No lock — no external deps + }, + }; + + await writeModulesToDisk(moduleFolderPath, modules, undefined); + + // Verify lock files exist only for modules with locks + expect(fs.existsSync(path.join(moduleFolderPath, "api_client.lock"))).toBe(true); + expect(fs.existsSync(path.join(moduleFolderPath, "data_processor.lock"))).toBe(true); + expect(fs.existsSync(path.join(moduleFolderPath, "utils.lock"))).toBe(false); + + // Read back and verify + const result = await readModulesFromDisk(moduleFolderPath, undefined, false); + expect(result).toBeDefined(); + expect(Object.keys(result!).sort()).toEqual(["api_client.py", "data_processor.py", "utils.py"]); + expect(result!["api_client.py"].lock).toBe("requests==2.31.0\nurllib3==2.0.4\n"); + expect(result!["data_processor.py"].lock).toBe("pandas==2.1.0\nnumpy==1.25.0\n"); + expect(result!["utils.py"].lock).toBeUndefined(); + }); + + test("nested modules with lock files", async () => { + const moduleFolderPath = path.join(tempDir, "my_script__mod"); + const modules = { + "services/api.ts": { + content: "export function callApi() {}\n", + language: "bun" as const, + lock: "axios@1.5.0\n", + }, + "services/db.ts": { + content: "export function query() {}\n", + language: "bun" as const, + lock: "pg@8.11.0\n", + }, + }; + + await writeModulesToDisk(moduleFolderPath, modules, "bun"); + + // Verify nested lock files + expect(fs.existsSync(path.join(moduleFolderPath, "services", "api.lock"))).toBe(true); + expect(fs.existsSync(path.join(moduleFolderPath, "services", "db.lock"))).toBe(true); + + const result = await readModulesFromDisk(moduleFolderPath, "bun", false); + expect(result).toBeDefined(); + expect(result!["services/api.ts"].lock).toBe("axios@1.5.0\n"); + expect(result!["services/db.ts"].lock).toBe("pg@8.11.0\n"); + }); + + test("overwrites existing module folder on re-write", async () => { + const moduleFolderPath = path.join(tempDir, "my_script__mod"); + + // First write + await writeModulesToDisk(moduleFolderPath, { + "old.ts": { content: "old content\n", language: "bun" as const }, + }, "bun"); + expect(fs.existsSync(path.join(moduleFolderPath, "old.ts"))).toBe(true); + + // Second write with different module + await writeModulesToDisk(moduleFolderPath, { + "new.ts": { content: "new content\n", language: "bun" as const }, + }, "bun"); + expect(fs.existsSync(path.join(moduleFolderPath, "new.ts"))).toBe(true); + // old.ts should still exist (writeModulesToDisk doesn't clean up) + // This is intentional — cleanup happens at the sync level + }); +}); diff --git a/cli/test/sync_pull_push.test.ts b/cli/test/sync_pull_push.test.ts index d98b412c39a39..40e8ac4fdf39a 100644 --- a/cli/test/sync_pull_push.test.ts +++ b/cli/test/sync_pull_push.test.ts @@ -39,6 +39,7 @@ import { isAppInlineScriptPath, isFlowInlineScriptPath, isRawAppBackendPath, + getModuleFolderSuffix, } from "../src/utils/resource_folders.ts"; import { newPathAssigner } from "../windmill-utils-internal/src/path-utils/path-assigner.ts"; @@ -2401,3 +2402,315 @@ describe("http trigger sync", () => { }); }); }); + +// ============================================================================= +// Script Module Sync Tests +// ============================================================================= + +describe("script module sync", () => { + test.skipIf(process.platform === "win32")("push script with modules via API and pull back", async () => { + await withTestBackend(async (backend, tempDir) => { + const uniqueId = Date.now(); + const scriptPath = `f/test/module_script_${uniqueId}`; + const modSuffix = getModuleFolderSuffix(); + + // Create a script with modules via API + const resp = await backend.apiRequest!( + `/api/w/${backend.workspace}/scripts/create`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + path: scriptPath, + content: 'import { greet } from "./helper";\nexport async function main() { return greet(); }', + language: "bun", + summary: "Script with modules", + description: "Test script with module files", + schema: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: {}, + required: [], + }, + modules: { + "helper.ts": { + content: 'export function greet() { return "hello from module"; }\n', + language: "bun", + }, + }, + }), + } + ); + expect(resp.status).toBeLessThan(300); + await resp.text(); + + await writeWmillYaml(tempDir); + + // Pull the script + const pullResult = await backend.runCLICommand(["sync", "pull", "--yes"], tempDir); + expect(pullResult.code).toEqual(0); + + // Verify module files exist on disk + const files = await listFilesRecursive(tempDir); + const moduleFile = files.find( + (f) => f.includes(`module_script_${uniqueId}${modSuffix}`) && f.endsWith("helper.ts") + ); + expect(moduleFile).toBeDefined(); + + // Scripts with modules use folder layout: main content is at __mod/script.ts + const mainFile = files.find( + (f) => f.includes(`module_script_${uniqueId}${modSuffix}`) && f.endsWith("script.ts") + ); + expect(mainFile).toBeDefined(); + + // Verify module content + if (moduleFile) { + const content = await readFile(`${tempDir}/${moduleFile}`, "utf-8"); + expect(content).toContain("greet"); + expect(content).toContain("hello from module"); + } + }); + }); + + test("push script with module lock files and pull back", async () => { + await withTestBackend(async (backend, tempDir) => { + const uniqueId = Date.now(); + const scriptPath = `f/test/locked_module_${uniqueId}`; + const modSuffix = getModuleFolderSuffix(); + + // Create a script with a module that has a lock + const resp = await backend.apiRequest!( + `/api/w/${backend.workspace}/scripts/create`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + path: scriptPath, + content: 'export async function main() { return "main"; }', + language: "bun", + summary: "Script with locked module", + schema: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: {}, + required: [], + }, + modules: { + "dep.ts": { + content: 'import lodash from "lodash";\nexport const x = lodash.identity(1);\n', + language: "bun", + lock: "lodash@4.17.21\n", + }, + }, + }), + } + ); + expect(resp.status).toBeLessThan(300); + await resp.text(); + + await writeWmillYaml(tempDir); + + // Pull + const pullResult = await backend.runCLICommand(["sync", "pull", "--yes"], tempDir); + expect(pullResult.code).toEqual(0); + + // Verify lock file exists on disk + const files = await listFilesRecursive(tempDir); + const lockFile = files.find( + (f) => f.includes(`locked_module_${uniqueId}${modSuffix}`) && f.endsWith("dep.lock") + ); + expect(lockFile).toBeDefined(); + + // Verify lock content + if (lockFile) { + const lockContent = await readFile(`${tempDir}/${lockFile}`, "utf-8"); + expect(lockContent).toContain("lodash"); + } + }); + }); + + test("local script with modules pushes and round-trips", async () => { + await withTestBackend(async (backend, tempDir) => { + const uniqueId = Date.now(); + const modSuffix = getModuleFolderSuffix(); + + await writeWmillYaml(tempDir); + + // Create local script with modules using folder layout: + // f/test/modular___mod/script.ts (entry point) + // f/test/modular___mod/script.yaml (metadata) + // f/test/modular___mod/helper.ts (module file) + const scriptDir = `${tempDir}/f/test`; + const scriptName = `modular_${uniqueId}`; + const modDir = `${scriptDir}/${scriptName}${modSuffix}`; + await mkdir(modDir, { recursive: true }); + + // Entry point (main script content) + await writeFile( + `${modDir}/script.ts`, + 'import { helper } from "./helper";\nexport async function main() { return helper(); }', + "utf-8" + ); + // Script metadata + await writeFile( + `${modDir}/script.yaml`, + `summary: "Script with modules" +description: "Test" +schema: + $schema: "https://json-schema.org/draft/2020-12/schema" + type: object + properties: {} + required: [] +is_template: false +lock: "" +kind: script +`, + "utf-8" + ); + + // Module file + await writeFile( + `${modDir}/helper.ts`, + 'export function helper() { return "from module"; }\n', + "utf-8" + ); + + // Push + const pushResult = await backend.runCLICommand(["sync", "push", "--yes"], tempDir); + expect(pushResult.code).toEqual(0); + + // Pull into a fresh directory to verify round-trip + const pullDir = await mkdtemp(join(tmpdir(), "windmill_module_pull_")); + try { + await writeWmillYaml(pullDir); + + const pullResult = await backend.runCLICommand(["sync", "pull", "--yes"], pullDir); + expect(pullResult.code).toEqual(0); + + // Verify module file came back + const files = await listFilesRecursive(pullDir); + const pulledModule = files.find( + (f) => f.includes(`${scriptName}${modSuffix}`) && f.endsWith("helper.ts") + ); + expect(pulledModule).toBeDefined(); + + if (pulledModule) { + const content = await readFile(`${pullDir}/${pulledModule}`, "utf-8"); + expect(content).toContain("from module"); + } + } finally { + await rm(pullDir, { recursive: true }); + } + }); + }); + + test("script with nested module paths pushes and pulls correctly", async () => { + await withTestBackend(async (backend, tempDir) => { + const uniqueId = Date.now(); + const scriptPath = `f/test/nested_mod_${uniqueId}`; + const modSuffix = getModuleFolderSuffix(); + + // Create script with nested modules via API + const resp = await backend.apiRequest!( + `/api/w/${backend.workspace}/scripts/create`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + path: scriptPath, + content: 'export async function main() { return "main"; }', + language: "bun", + summary: "Script with nested modules", + schema: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: {}, + required: [], + }, + modules: { + "utils/format.ts": { + content: 'export function format(s: string) { return s.trim(); }\n', + language: "bun", + }, + "utils/validate.ts": { + content: 'export function validate(s: string) { return s.length > 0; }\n', + language: "bun", + }, + }, + }), + } + ); + expect(resp.status).toBeLessThan(300); + await resp.text(); + + await writeWmillYaml(tempDir); + + const pullResult = await backend.runCLICommand(["sync", "pull", "--yes"], tempDir); + expect(pullResult.code).toEqual(0); + + // Verify nested module files exist + const files = await listFilesRecursive(tempDir); + const formatFile = files.find( + (f) => f.includes(`nested_mod_${uniqueId}${modSuffix}`) && f.includes("utils/format.ts") + ); + const validateFile = files.find( + (f) => f.includes(`nested_mod_${uniqueId}${modSuffix}`) && f.includes("utils/validate.ts") + ); + expect(formatFile).toBeDefined(); + expect(validateFile).toBeDefined(); + }); + }); + + test.skipIf(process.platform === "win32")("pull script with modules does not create stale metadata", async () => { + await withTestBackend(async (backend, tempDir) => { + const uniqueId = Date.now(); + const scriptPath = `f/test/fresh_mod_${uniqueId}`; + + // Create a script with modules on remote + const resp = await backend.apiRequest!( + `/api/w/${backend.workspace}/scripts/create`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + path: scriptPath, + content: 'export async function main() { return "main"; }', + language: "bun", + summary: "Script with modules for freshness test", + schema: { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: {}, + required: [], + }, + modules: { + "helper.ts": { + content: 'export function help() { return true; }\n', + language: "bun", + }, + }, + }), + } + ); + expect(resp.status).toBeLessThan(300); + await resp.text(); + + await writeWmillYaml(tempDir); + + // Pull + const pullResult = await backend.runCLICommand(["sync", "pull", "--yes"], tempDir); + expect(pullResult.code).toEqual(0); + + // Run generate-metadata — should show up-to-date for pulled scripts + const metaResult = await backend.runCLICommand( + ["generate-metadata", "--dry-run"], + tempDir + ); + expect(metaResult.code).toEqual(0); + // The script we just pulled should NOT be stale + // (it may still show other scripts as stale from seedTestData) + const output = metaResult.stdout + metaResult.stderr; + expect(output).not.toContain(`fresh_mod_${uniqueId}`); + }); + }); +}); diff --git a/cli/test/test_fixtures.ts b/cli/test/test_fixtures.ts index 5f648699494b0..cde57e1a95a3e 100644 --- a/cli/test/test_fixtures.ts +++ b/cli/test/test_fixtures.ts @@ -28,6 +28,7 @@ import { writeFile, mkdir } from "node:fs/promises"; import { getFolderSuffix, getMetadataFileName, + getModuleFolderSuffix, } from "../src/utils/resource_folders.ts"; // ============================================================================= @@ -463,6 +464,65 @@ export async function createLocalRawApp( } } +// ============================================================================= +// Script with Modules Fixtures +// ============================================================================= + +export interface ModuleFile { + path: string; + content: string; + lock?: string; +} + +/** + * Creates a script with module files on the local filesystem. + * + * Creates the main script, its metadata, and module files in a __mod/ folder. + * Optionally includes lock files for modules. + * + * @param tempDir - Base directory for the test workspace + * @param dir - Relative path within the workspace (e.g., "f/test") + * @param name - Script name (without extension) + * @param language - Script language (default: "bun") + * @param modules - Module files to create + * + * @example + * await createLocalScriptWithModules(tempDir, "f/test", "my_script", "bun", [ + * { path: "helper.ts", content: "export const x = 1;" }, + * { path: "utils/math.ts", content: "export function add(a, b) { return a + b; }", lock: "lodash@4.0.0\n" }, + * ]); + */ +export async function createLocalScriptWithModules( + tempDir: string, + dir: string, + name: string, + language: "python3" | "deno" | "bun" | "bash" | "go" | "postgresql" = "bun", + modules: ModuleFile[] +): Promise { + // Create the main script + await createLocalScript(tempDir, dir, name, language); + + // Create module files in __mod/ folder + const modSuffix = getModuleFolderSuffix(); + const modDir = `${tempDir}/${dir}/${name}${modSuffix}`; + + for (const mod of modules) { + const fullPath = `${modDir}/${mod.path}`; + const parentDir = fullPath.substring(0, fullPath.lastIndexOf("/")); + await mkdir(parentDir, { recursive: true }); + await writeFile(fullPath, mod.content, "utf-8"); + + // Write lock file if provided + if (mod.lock) { + const baseName = mod.path.substring(0, mod.path.indexOf(".")); + const lockPath = `${modDir}/${baseName}.lock`; + const lockDir = lockPath.substring(0, lockPath.lastIndexOf("/")); + await mkdir(lockDir, { recursive: true }); + await writeFile(lockPath, mod.lock, "utf-8"); + } + } +} + // ============================================================================= // Resource Fixtures (Variables, Resources, Schedules, etc.) // ============================================================================= diff --git a/cli/test/unified_generate_metadata.test.ts b/cli/test/unified_generate_metadata.test.ts index 0e93ae5cea478..a25d7f8bd3215 100644 --- a/cli/test/unified_generate_metadata.test.ts +++ b/cli/test/unified_generate_metadata.test.ts @@ -14,6 +14,7 @@ import { createLocalFlow, createLocalApp, createLocalRawApp, + createLocalScriptWithModules, } from "./test_fixtures.ts"; /** @@ -612,3 +613,167 @@ describe("generate-metadata folder argument", () => { }); }); }); + +// ============================================================================= +// Scripts with modules +// ============================================================================= + +describe("generate-metadata with script modules", () => { + test("script with modules is detected as a single stale item", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspace(backend, tempDir, "module_script_test"); + + // Create a script with module files + await createLocalScriptWithModules(tempDir, "f/test", "order_workflow", "bun", [ + { path: "helper.ts", content: 'export function validate() { return true; }\n' }, + { path: "utils.ts", content: 'export const VERSION = "1.0";\n' }, + ]); + + const result = await backend.runCLICommand( + ["generate-metadata", "--dry-run"], + tempDir, + "module_script_test" + ); + + expect(result.code).toEqual(0); + const output = result.stdout + result.stderr; + // The main script should be listed as stale + expect(output).toContain("order_workflow"); + // Module files should NOT appear as separate stale scripts (only within [changed modules: ...]) + const lines = output.split("\n"); + const staleLines = lines.filter((l: string) => l.includes("f/test/")); + expect(staleLines.length).toBe(1); + expect(staleLines[0]).toContain("order_workflow"); + }); + }); + + test("module files are not treated as standalone scripts", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspace(backend, tempDir, "module_not_standalone_test"); + + // Create a script with modules plus a regular script + await createLocalScriptWithModules(tempDir, "f/test", "my_script", "bun", [ + { path: "helper.ts", content: 'export function greet() { return "hi"; }\n' }, + ]); + await createLocalScript(tempDir, "f/test", "standalone_script"); + + const result = await backend.runCLICommand( + ["generate-metadata", "--dry-run"], + tempDir, + "module_not_standalone_test" + ); + + expect(result.code).toEqual(0); + const output = result.stdout + result.stderr; + // Should list both the main script and standalone script + expect(output).toContain("my_script"); + expect(output).toContain("standalone_script"); + // Should NOT list module helper as a separate entry + // Count occurrences of "Scripts" header — there should be exactly one + expect(output).toContain("Scripts"); + }); + }); + + test("script with modules generates metadata and becomes up-to-date", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspace(backend, tempDir, "module_uptodate_test"); + + await createLocalScriptWithModules(tempDir, "f/test", "my_script", "bun", [ + { path: "helper.ts", content: 'export function greet() { return "hi"; }\n' }, + ]); + + // First run — should find stale items and generate metadata + const result1 = await backend.runCLICommand( + ["generate-metadata", "--yes"], + tempDir, + "module_uptodate_test" + ); + expect(result1.code).toEqual(0); + expect(result1.stdout).toContain("Done"); + + // Second run — should be up-to-date + const result2 = await backend.runCLICommand( + ["generate-metadata", "--yes"], + tempDir, + "module_uptodate_test" + ); + expect(result2.code).toEqual(0); + expect(result2.stdout).toContain("up-to-date"); + }); + }); + + test("modifying a module re-triggers stale detection", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspace(backend, tempDir, "module_modify_test"); + + await createLocalScriptWithModules(tempDir, "f/test", "order_workflow", "bun", [ + { path: "helper.ts", content: 'export function greet() { return "hi"; }\n' }, + { path: "utils.ts", content: 'export const VERSION = "1.0";\n' }, + ]); + + // First run — generate metadata + const result1 = await backend.runCLICommand( + ["generate-metadata", "--yes"], + tempDir, + "module_modify_test" + ); + expect(result1.code).toEqual(0); + + // Second run — should be up-to-date + const result2 = await backend.runCLICommand( + ["generate-metadata", "--dry-run"], + tempDir, + "module_modify_test" + ); + expect(result2.code).toEqual(0); + const output2 = result2.stdout + result2.stderr; + expect(output2).not.toContain("order_workflow"); + + // Modify one module + await writeFile( + `${tempDir}/f/test/order_workflow__mod/helper.ts`, + 'export function greet() { return "hello world"; }\n', + "utf-8" + ); + + // Third run — should detect the script as stale with the changed module + const result3 = await backend.runCLICommand( + ["generate-metadata", "--dry-run"], + tempDir, + "module_modify_test" + ); + expect(result3.code).toEqual(0); + const output3 = result3.stdout + result3.stderr; + expect(output3).toContain("order_workflow"); + expect(output3).toContain("helper.ts"); + // utils.ts was not modified, should not be listed as changed + expect(output3).not.toContain("utils.ts"); + }); + }); + + test("script with modules and lock files does not crash", async () => { + await withTestBackend(async (backend, tempDir) => { + await setupWorkspace(backend, tempDir, "module_with_locks_test"); + + // Create a script with modules that have lock files + await createLocalScriptWithModules(tempDir, "f/test", "my_script", "bun", [ + { + path: "helper.ts", + content: 'import lodash from "lodash";\nexport const x = lodash.identity(1);\n', + lock: "lodash@4.17.21\n", + }, + ]); + + // Should not crash on lock files inside __mod/ + const result = await backend.runCLICommand( + ["generate-metadata", "--dry-run"], + tempDir, + "module_with_locks_test" + ); + + expect(result.code).toEqual(0); + // Should find the main script as stale, not crash on .lock files + expect(result.stdout).toContain("my_script"); + }); + }); +}); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 010d797e34123..01eea55d6715f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -75,17 +75,18 @@ "vscode-languageclient": "~9.0.1", "vscode-uri": "~3.1.0", "vscode-ws-jsonrpc": "~3.5.0", + "windmill-parser-wasm": "file:../backend/parsers/windmill-parser-wasm/pkg-ts", "windmill-parser-wasm-asset": "1.653.0", "windmill-parser-wasm-csharp": "1.510.1", "windmill-parser-wasm-go": "1.510.1", "windmill-parser-wasm-java": "1.510.1", "windmill-parser-wasm-nu": "1.510.1", "windmill-parser-wasm-php": "1.647.1", - "windmill-parser-wasm-py": "1.655.0", + "windmill-parser-wasm-py": "1.657.2", "windmill-parser-wasm-regex": "1.653.0", "windmill-parser-wasm-ruby": "1.526.1", "windmill-parser-wasm-rust": "1.647.1", - "windmill-parser-wasm-ts": "1.655.0", + "windmill-parser-wasm-ts": "1.657.2", "windmill-parser-wasm-yaml": "1.593.0", "windmill-sql-datatype-parser-wasm": "1.512.0", "windmill-utils-internal": "^1.3.4", @@ -160,6 +161,10 @@ "svelte": "^5.0.0" } }, + "../backend/parsers/windmill-parser-wasm/pkg-ts": { + "name": "windmill-parser-wasm-ts", + "version": "1.657.2" + }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -837,7 +842,6 @@ "version": "1.9.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz", "integrity": "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -849,7 +853,6 @@ "version": "1.9.0", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz", "integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -860,7 +863,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1350,7 +1352,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1507,7 +1508,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1524,7 +1524,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1541,7 +1540,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1558,7 +1556,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1575,7 +1572,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1592,7 +1588,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1609,7 +1604,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1626,7 +1620,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1643,7 +1636,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1660,7 +1652,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1677,7 +1668,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1694,7 +1684,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1711,7 +1700,6 @@ "cpu": [ "wasm32" ], - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1728,7 +1716,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1745,7 +1732,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2051,7 +2037,6 @@ "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -6860,7 +6845,7 @@ "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", - "dev": true, + "devOptional": true, "license": "MIT", "bin": { "jiti": "bin/jiti.js" @@ -7359,7 +7344,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7380,7 +7364,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7401,7 +7384,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7422,7 +7404,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7443,7 +7424,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7464,7 +7444,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7485,7 +7464,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7506,7 +7484,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7527,7 +7504,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7548,7 +7524,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -7569,7 +7544,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -12145,21 +12119,6 @@ } } }, - "node_modules/svelte-check/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/svelte-eslint-parser": { "version": "0.43.0", "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.43.0.tgz", @@ -12890,7 +12849,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -13640,6 +13599,10 @@ "node": ">=8" } }, + "node_modules/windmill-parser-wasm": { + "resolved": "../backend/parsers/windmill-parser-wasm/pkg-ts", + "link": true + }, "node_modules/windmill-parser-wasm-asset": { "version": "1.653.0", "resolved": "https://registry.npmjs.org/windmill-parser-wasm-asset/-/windmill-parser-wasm-asset-1.653.0.tgz", @@ -13671,9 +13634,9 @@ "integrity": "sha512-u2qaMkupSdhJibxvkLh3r/y36IARvnYNTLXWvOKxcQ0G/BPUB4+yF5o/yf47vv9zUV5WZv4mrdsKDt/pZDYeDg==" }, "node_modules/windmill-parser-wasm-py": { - "version": "1.655.0", - "resolved": "https://registry.npmjs.org/windmill-parser-wasm-py/-/windmill-parser-wasm-py-1.655.0.tgz", - "integrity": "sha512-kWpAE2Me/8KwNqq4n+0ZoEqMJZ9aCZf1744j0S6Om+F+XyHFR3e7GYK/slJ4cF7zA9TF6mKUSiW4oHJ+WUlNXQ==" + "version": "1.657.2", + "resolved": "https://registry.npmjs.org/windmill-parser-wasm-py/-/windmill-parser-wasm-py-1.657.2.tgz", + "integrity": "sha512-3CN2rziafgCWcZri812+CkzuaE3P3/7dXmV9lSDpK9ma6Esd4zkHRXUFSyRzQE/R7Fxj5mSmSNX6xTff8eX5mw==" }, "node_modules/windmill-parser-wasm-regex": { "version": "1.653.0", @@ -13691,9 +13654,9 @@ "integrity": "sha512-9yGLYZX2Hn9TdTqGY/5Fp50ftzgUsrfBkSK9vJkKJd5Amyg+yXLBGzd8pz6Org+4uxMenz/16wpsgijvo6uhhQ==" }, "node_modules/windmill-parser-wasm-ts": { - "version": "1.655.0", - "resolved": "https://registry.npmjs.org/windmill-parser-wasm-ts/-/windmill-parser-wasm-ts-1.655.0.tgz", - "integrity": "sha512-nzvxILV68MccOKuZcGwjZLB+mWzPMMqEP0FTOb9nmkRWzKPFMxs5ADAXrDZINeNLNv64hYQhGw66ouq0P1G6jQ==" + "version": "1.657.2", + "resolved": "https://registry.npmjs.org/windmill-parser-wasm-ts/-/windmill-parser-wasm-ts-1.657.2.tgz", + "integrity": "sha512-tiOUVsMKTc85m/a2BKpgAN3xTz+OPrUhcjEBPJNTdzrQOir1G5WeNkQ403BW+d1qI0BAVWT0gZnJ+4AhavBP+w==" }, "node_modules/windmill-parser-wasm-yaml": { "version": "1.593.0", diff --git a/frontend/package.json b/frontend/package.json index 2a14c5f01fc47..854d85fa75b0f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -148,17 +148,18 @@ "vscode-languageclient": "~9.0.1", "vscode-uri": "~3.1.0", "vscode-ws-jsonrpc": "~3.5.0", + "windmill-parser-wasm": "file:../backend/parsers/windmill-parser-wasm/pkg-ts", "windmill-parser-wasm-asset": "1.653.0", "windmill-parser-wasm-csharp": "1.510.1", "windmill-parser-wasm-go": "1.510.1", "windmill-parser-wasm-java": "1.510.1", "windmill-parser-wasm-nu": "1.510.1", "windmill-parser-wasm-php": "1.647.1", - "windmill-parser-wasm-py": "1.655.0", + "windmill-parser-wasm-py": "1.657.2", "windmill-parser-wasm-regex": "1.653.0", "windmill-parser-wasm-ruby": "1.526.1", "windmill-parser-wasm-rust": "1.647.1", - "windmill-parser-wasm-ts": "1.655.0", + "windmill-parser-wasm-ts": "1.657.2", "windmill-parser-wasm-yaml": "1.593.0", "windmill-sql-datatype-parser-wasm": "1.512.0", "windmill-utils-internal": "^1.3.4", @@ -583,4 +584,4 @@ "@rollup/rollup-linux-x64-gnu": "^4.35.0", "fsevents": "^2.3.3" } -} \ No newline at end of file +} diff --git a/frontend/src/lib/components/Dev.svelte b/frontend/src/lib/components/Dev.svelte index d13d6eee974e6..79f3be91f16e9 100644 --- a/frontend/src/lib/components/Dev.svelte +++ b/frontend/src/lib/components/Dev.svelte @@ -145,6 +145,7 @@ lock?: string isCodebase?: boolean tag?: string + modules?: { [key: string]: import('$lib/gen').ScriptModule } | null } let currentScript: LastEditScript | undefined = $state(undefined) @@ -206,7 +207,9 @@ done(x) { loadPastTests() } - } + }, + undefined, + currentScript.modules ) } else { sendUserToast(`Bundle received ${lastCommandId} was obsolete, ignoring`, true) @@ -392,7 +395,11 @@ currentScript.language, args, currentScript.tag, - useLock ? currentScript.lock : undefined + useLock ? currentScript.lock : undefined, + undefined, + undefined, + undefined, + currentScript.modules ) } } diff --git a/frontend/src/lib/components/EditorBar.svelte b/frontend/src/lib/components/EditorBar.svelte index 4ddbb70b19f21..ee2108711c6db 100644 --- a/frontend/src/lib/components/EditorBar.svelte +++ b/frontend/src/lib/components/EditorBar.svelte @@ -79,7 +79,7 @@ iconOnly?: boolean validCode?: boolean kind?: 'script' | 'trigger' | 'approval' - template?: 'pgsql' | 'mysql' | 'script' | 'docker' | 'powershell' | 'bunnative' | 'claudesandbox' + template?: 'pgsql' | 'mysql' | 'script' | 'docker' | 'powershell' | 'bunnative' | 'claudesandbox' | 'wac_python' | 'wac_typescript' collabMode?: boolean collabLive?: boolean collabUsers?: { name: string }[] diff --git a/frontend/src/lib/components/FlowStatusViewerInner.svelte b/frontend/src/lib/components/FlowStatusViewerInner.svelte index 25731f82c839a..c75b4c7d3dfbe 100644 --- a/frontend/src/lib/components/FlowStatusViewerInner.svelte +++ b/frontend/src/lib/components/FlowStatusViewerInner.svelte @@ -9,11 +9,13 @@ type FlowModuleValue, type FlowModule, ResourceService, - type CompletedJob + type CompletedJob, + type WorkflowStatus } from '$lib/gen' import { workspaceStore } from '$lib/stores' import { base } from '$lib/base' import FlowJobResult from './FlowJobResult.svelte' + import WorkflowTimeline from './WorkflowTimeline.svelte' import DisplayResult from './DisplayResult.svelte' import { getContext, setContext, tick, untrack } from 'svelte' @@ -237,6 +239,19 @@ untrack(() => flowJobIds)?.flowJobs?.map((x, id) => `iter #${id + 1} not loaded by frontend yet`) ?? [] ) + function asWorkflowStatus(x: any): Record { + if (!x || typeof x !== 'object') return {} + const result: Record = {} + for (const [k, v] of Object.entries(x)) { + if (!k.startsWith('_') || k.startsWith('_step/')) result[k] = v as WorkflowStatus + } + return result + } + + function getStepResults(x: any): Record { + return x?._checkpoint?.completed_steps ?? {} + } + let retry_selected = $state('') let timeout: number | undefined = undefined @@ -879,7 +894,8 @@ tag: job.tag, started_at, parent_module: mod['parent_module'], - script_hash: job.script_hash + script_hash: job.script_hash, + workflow_as_code_status: job['workflow_as_code_status'] }, force ) @@ -917,8 +933,8 @@ retries: mod?.failed_retries?.length, skipped: mod.skipped, agent_actions: mod.agent_actions, - script_hash: job.script_hash - // retries: flowStateStore?.raw_flow + script_hash: job.script_hash, + workflow_as_code_status: job['workflow_as_code_status'] }, force ) @@ -2053,6 +2069,18 @@ /> {/if} + {#if node.workflow_as_code_status} +
+
Workflow timeline
+ +
+ {/if} | null ): Promise { return abstractRun( () => @@ -310,7 +311,8 @@ tag, lock, script_hash: hash, - flow_path: flowPath + flow_path: flowPath, + modules: modules ?? undefined } }), callbacks diff --git a/frontend/src/lib/components/ScriptBuilder.svelte b/frontend/src/lib/components/ScriptBuilder.svelte index 7de0926f0ae64..eb8709751cd93 100644 --- a/frontend/src/lib/components/ScriptBuilder.svelte +++ b/frontend/src/lib/components/ScriptBuilder.svelte @@ -107,6 +107,22 @@ import { isRuleActive } from '$lib/workspaceProtectionRules.svelte' import { buildForkEditUrl } from '$lib/utils/editInFork' import OnBehalfOfSelector, { type OnBehalfOfChoice } from './OnBehalfOfSelector.svelte' + import WacExportDrawer from './scripts/WacExportDrawer.svelte' + import Modal from './common/modal/Modal.svelte' + + const WAC_ALPHA_ACK_KEY = 'windmill_wac_alpha_ack' + let wacAlphaModalOpen = $state(false) + + function showWacAlphaModalIfNeeded() { + if (typeof sessionStorage !== 'undefined' && sessionStorage.getItem(WAC_ALPHA_ACK_KEY) !== 'true') { + wacAlphaModalOpen = true + } + } + + function acknowledgeWacAlpha() { + sessionStorage.setItem(WAC_ALPHA_ACK_KEY, 'true') + wacAlphaModalOpen = false + } let { script = $bindable(), @@ -182,6 +198,7 @@ let editor: Editor | undefined = $state(undefined) let scriptEditor: ScriptEditor | undefined = $state(undefined) let captureTable: CaptureTable | undefined = $state(undefined) + let wacExportDrawer: WacExportDrawer | undefined = $state(undefined) // Draft triggers confirmation modal let draftTriggersModalOpen = $state(false) @@ -362,6 +379,13 @@ } if (script.content == '') { + if (template === 'wac_python') { + script.modules = { 'helper.py': { content: 'def main(a: str) -> str:\n return f"hello {a}"\n', language: 'python3' } } + showWacAlphaModalIfNeeded() + } else if (template === 'wac_typescript') { + script.modules = { 'helper.ts': { content: 'export function main(a: string): string {\n return `hello ${a}`\n}\n', language: 'bun' } } + showWacAlphaModalIfNeeded() + } initContent(script.language, script.kind, template) } @@ -388,7 +412,7 @@ async function initContent( language: SupportedLanguage, kind: Script['kind'] | undefined, - template: 'pgsql' | 'mysql' | 'script' | 'docker' | 'powershell' | 'bunnative' | 'claudesandbox' + template: 'pgsql' | 'mysql' | 'script' | 'docker' | 'powershell' | 'bunnative' | 'claudesandbox' | 'wac_python' | 'wac_typescript' ) { scriptEditor?.disableCollaboration() const templateScript = await isTemplateScript() @@ -403,6 +427,7 @@ } async function handleEditScript(stay: boolean, deployMsg?: string): Promise { + scriptEditor?.flushModuleState() // Fetch latest version and fetch entire script after if needed let actual_parent_hash: string | undefined = undefined @@ -510,10 +535,10 @@ script.kind === 'preprocessor' ? 'preprocessor' : undefined ) if (script.kind === 'preprocessor') { - script.no_main_func = undefined + script.auto_kind = undefined script.has_preprocessor = undefined } else { - script.no_main_func = result?.no_main_func || undefined + script.auto_kind = result?.auto_kind || undefined script.has_preprocessor = result?.has_preprocessor || undefined } } catch (error) { @@ -554,12 +579,13 @@ timeout: script.timeout, concurrency_key: emptyString(script.concurrency_key) ? undefined : script.concurrency_key, visible_to_runner_only: script.visible_to_runner_only, - no_main_func: script.no_main_func, + auto_kind: script.auto_kind, has_preprocessor: script.has_preprocessor, deployment_message: deploymentMsg || undefined, on_behalf_of_email: script.on_behalf_of_email, preserve_on_behalf_of: preserveOnBehalfOf || undefined, - assets: script.assets + assets: script.assets, + modules: script.modules } }) @@ -592,7 +618,7 @@ if (!disableHistoryChange) { history.replaceState(history.state, '', `/scripts/edit/${script.path}`) } - if (stay || (script.no_main_func && script.kind !== 'preprocessor' && !isWorkflowAsCode(script.content, script.language))) { + if (stay || (script.auto_kind === 'lib' && script.kind !== 'preprocessor' && !isWorkflowAsCode(script.content, script.language))) { script.parent_hash = newHash sendUserToast('Deployed') } else { @@ -606,6 +632,7 @@ } async function saveDraft(forceSave = false): Promise { + scriptEditor?.flushModuleState() if (initialPath != '' && !savedScript) { return } @@ -643,10 +670,10 @@ script.kind === 'preprocessor' ? 'preprocessor' : undefined ) if (script.kind === 'preprocessor') { - script.no_main_func = undefined + script.auto_kind = undefined script.has_preprocessor = undefined } else { - script.no_main_func = result?.no_main_func || undefined + script.auto_kind = result?.auto_kind || undefined script.has_preprocessor = result?.has_preprocessor || undefined } } catch (error) { @@ -707,10 +734,11 @@ ? undefined : script.concurrency_key, visible_to_runner_only: script.visible_to_runner_only, - no_main_func: script.no_main_func, + auto_kind: script.auto_kind, has_preprocessor: script.has_preprocessor, on_behalf_of_email: script.on_behalf_of_email, - assets: script.assets + assets: script.assets, + modules: script.modules } }) } @@ -816,7 +844,7 @@ } ] : []), - ...(!script.draft_only && script.kind === 'script' && !script.no_main_func + ...(!script.draft_only && script.kind === 'script' && !script.auto_kind ? [ { label: 'Exit & See details', @@ -825,10 +853,34 @@ } } ] + : []), + ...(isWorkflowAsCode(script.content, script.language) + ? [ + { + label: 'Export as YAML/JSON', + onClick: () => { + wacExportDrawer?.open(script) + } + } + ] : []) ] : [] + if ( + dropdownItems.length === 0 && + isWorkflowAsCode(script.content, script.language) + ) { + dropdownItems = [ + { + label: 'Export as YAML/JSON', + onClick: () => { + wacExportDrawer?.open(script) + } + } + ] + } + return dropdownItems.length > 0 ? dropdownItems : undefined } @@ -1201,7 +1253,7 @@ {/if} -
+
Template + + + + + +
{#if customUi?.settingsPanel?.metadata?.disableScriptKind !== true}
@@ -1948,9 +2040,23 @@ bind:hasPreprocessor bind:captureTable bind:assets={script.assets} + bind:modules={script.modules} enablePreprocessorSnippet />
{:else} Script Builder not available to operators {/if} + + + + +
+

+ Workflow-as-Code is in alpha — use in production at your own risk. It is an alternative to the Flow editor for advanced users. Feedback welcome on GitHub or Discord. +

+
+ +
+
+
diff --git a/frontend/src/lib/components/ScriptEditor.svelte b/frontend/src/lib/components/ScriptEditor.svelte index f310010c1803e..7ac8246ca29cd 100644 --- a/frontend/src/lib/components/ScriptEditor.svelte +++ b/frontend/src/lib/components/ScriptEditor.svelte @@ -2,7 +2,14 @@ import { BROWSER } from 'esm-env' import type { Schema, SupportedLanguage } from '$lib/common' - import { type CompletedJob, type Job, JobService, type Preview, type ScriptLang } from '$lib/gen' + import { + type CompletedJob, + type Job, + JobService, + type Preview, + type ScriptLang, + type ScriptModule + } from '$lib/gen' import { enterpriseLicense, userStore, workspaceStore } from '$lib/stores' import { copyToClipboard, @@ -25,8 +32,10 @@ import WindmillIcon from './icons/WindmillIcon.svelte' import * as Y from 'yjs' import { scriptLangToEditorLang } from '$lib/scripts' + import { langToExt } from '$lib/editorLangUtils' import { WebsocketProvider } from 'y-websocket' import Modal from './common/modal/Modal.svelte' + import Popover from './meltComponents/Popover.svelte' import DiffEditor from './DiffEditor.svelte' import { AlertTriangle, @@ -40,8 +49,11 @@ GitBranch, Play, PlayIcon, + Plus, Terminal, - WandSparkles + Pencil, + WandSparkles, + X } from 'lucide-svelte' import { DebugToolbar, @@ -100,7 +112,16 @@ path: string | undefined lang: Preview['language'] kind?: string | undefined - template?: 'pgsql' | 'mysql' | 'script' | 'docker' | 'powershell' | 'bunnative' | 'claudesandbox' + template?: + | 'pgsql' + | 'mysql' + | 'script' + | 'docker' + | 'powershell' + | 'bunnative' + | 'claudesandbox' + | 'wac_python' + | 'wac_typescript' tag: string | undefined initialArgs?: Record fixedOverflowWidgets?: boolean @@ -123,6 +144,7 @@ lastDeployedCode?: string | undefined disableAi?: boolean assets?: AssetWithAltAccessType[] + modules?: { [key: string]: ScriptModule } | null editorBarRight?: import('svelte').Snippet enablePreprocessorSnippet?: boolean } @@ -155,6 +177,7 @@ lastDeployedCode = undefined, disableAi = false, assets = $bindable(), + modules = $bindable(undefined), editorBarRight, enablePreprocessorSnippet = false }: Props = $props() @@ -163,6 +186,216 @@ let jsonView = $state(false) let schemaHeight = $state(0) + // Module tab state + let activeModuleTab: string | null = $state(null) + // editorCode is what the editor shows; code always holds the main script content + let editorCode: string = $state(code) + // Sync editorCode when code changes externally (template reset, copilot, etc.) + let lastSyncedCode = code + $effect.pre(() => { + if (activeModuleTab === null && code !== lastSyncedCode) { + editorCode = code + lastSyncedCode = code + } + }) + + function switchToModule(modulePath: string) { + if (activeModuleTab !== null && modules && activeModuleTab !== modulePath) { + // Switching from another module: save its content + modules[activeModuleTab] = { ...modules[activeModuleTab], content: editorCode } + } + if (modules && modules[modulePath]) { + activeModuleTab = modulePath + editorCode = modules[modulePath].content + editor?.setCode(editorCode) + } + } + + function switchToMain() { + if (activeModuleTab !== null && modules) { + // Save current module content + modules[activeModuleTab] = { ...modules[activeModuleTab], content: editorCode } + } + activeModuleTab = null + editorCode = code + lastSyncedCode = code + editor?.setCode(editorCode) + } + + let effectiveLang = $derived( + activeModuleTab && modules?.[activeModuleTab] + ? (modules[activeModuleTab].language as Preview['language']) + : lang + ) + + let isWacV2 = $derived.by(() => { + const mainCode = code + const isTsWac = + mainCode.includes('windmill-client') && + mainCode.includes('workflow') && + mainCode.includes('task') + const isPyWac = + (mainCode.includes('import wmill') || mainCode.includes('from wmill')) && + mainCode.includes('workflow') && + mainCode.includes('task') + return isTsWac || isPyWac + }) + let supportsModules = $derived((lang === 'bun' || lang === 'python3') && isWacV2) + let mainFileName = $derived('script.' + langToExt(scriptLangToEditorLang(lang))) + + let modulePathInput = $state('') + let showAddModulePopover = $state(false) + let modulePathInputEl: HTMLInputElement | undefined = $state(undefined) + let modulePathError = $state('') + + let renameModuleInput = $state('') + let renameModuleError = $state('') + let renameModuleInputEl: HTMLInputElement | undefined = $state(undefined) + + const SUPPORTED_MODULE_EXTENSIONS: Record = { + '.ts': 'bun', + '.py': 'python3', + '.go': 'go', + '.sh': 'bash', + '.ps1': 'powershell', + '.sql': 'postgresql', + '.gql': 'graphql', + '.php': 'php', + '.rs': 'rust', + '.yml': 'ansible', + '.cs': 'csharp', + '.nu': 'nu', + '.java': 'java', + '.rb': 'ruby' + } + + function inferModuleLang(filePath: string): ScriptModule['language'] | undefined { + for (const [ext, moduleLang] of Object.entries(SUPPORTED_MODULE_EXTENSIONS)) { + if (filePath.endsWith(ext)) return moduleLang + } + return undefined + } + + function getModuleDefaultContent(filePath: string): string { + if (filePath.endsWith('.py')) { + return `def hello() -> str:\n return "world"\n` + } else if (filePath.endsWith('.ts')) { + return `export function hello(): string {\n return "world"\n}\n` + } else if (filePath.endsWith('.go')) { + return `package inner\n\nfunc Hello() string {\n\treturn "world"\n}\n` + } else if (filePath.endsWith('.sh')) { + return `#!/bin/bash\necho "world"\n` + } else if (filePath.endsWith('.ps1')) { + return `function Hello {\n return "world"\n}\n` + } else if (filePath.endsWith('.sql')) { + return `SELECT 'world' as result;\n` + } else if (filePath.endsWith('.gql')) { + return `query Hello {\n hello\n}\n` + } else if (filePath.endsWith('.php')) { + return ` String {\n "world".to_string()\n}\n` + } else if (filePath.endsWith('.yml')) { + return `---\n- name: Hello\n debug:\n msg: "world"\n` + } else if (filePath.endsWith('.cs')) { + return `public static string Hello() {\n return "world";\n}\n` + } else if (filePath.endsWith('.nu')) { + return `def hello [] {\n "world"\n}\n` + } else if (filePath.endsWith('.java')) { + return `public class Helper {\n public static String hello() {\n return "world";\n }\n}\n` + } else if (filePath.endsWith('.rb')) { + return `def hello\n "world"\nend\n` + } + return '' + } + + function validateModulePath(path: string): string { + if (!path.trim()) return '' + const moduleLang = inferModuleLang(path) + if (!moduleLang) { + const exts = Object.keys(SUPPORTED_MODULE_EXTENSIONS).join(', ') + return `File must end with a supported extension: ${exts}` + } + if (modules?.[path.trim()]) { + return `Module ${path.trim()} already exists` + } + return '' + } + + function addModule() { + const modulePath = modulePathInput.trim() + if (!modulePath) return + const error = validateModulePath(modulePath) + if (error) { + modulePathError = error + return + } + if (!modules) { + modules = {} + } + modules[modulePath] = { + content: getModuleDefaultContent(modulePath), + language: inferModuleLang(modulePath)! + } + modulePathInput = '' + modulePathError = '' + showAddModulePopover = false + switchToModule(modulePath) + } + + function removeModule(modulePath: string) { + if (!modules) return + if (activeModuleTab === modulePath) { + switchToMain() + } + delete modules[modulePath] + modules = { ...modules } + } + + function validateRenameModulePath(newPath: string, oldPath: string): string { + if (!newPath.trim()) return '' + const moduleLang = inferModuleLang(newPath) + if (!moduleLang) { + const exts = Object.keys(SUPPORTED_MODULE_EXTENSIONS).join(', ') + return `File must end with a supported extension: ${exts}` + } + if (newPath.trim() !== oldPath && modules?.[newPath.trim()]) { + return `Module ${newPath.trim()} already exists` + } + return '' + } + + function renameModule(oldPath: string) { + const newPath = renameModuleInput.trim() + if (!newPath || newPath === oldPath) { + return + } + const error = validateRenameModulePath(newPath, oldPath) + if (error) { + renameModuleError = error + return + } + if (!modules) return + const mod = modules[oldPath] + const newLang = inferModuleLang(newPath) + delete modules[oldPath] + modules[newPath] = { ...mod, language: newLang ?? mod.language } + modules = { ...modules } + if (activeModuleTab === oldPath) { + activeModuleTab = newPath + } + renameModuleInput = '' + renameModuleError = '' + } + + export function flushModuleState() { + if (activeModuleTab !== null && modules) { + modules[activeModuleTab] = { ...modules[activeModuleTab], content: editorCode } + activeModuleTab = null + editorCode = code + } + } + $effect.pre(() => { if (schema == undefined) { schema = emptySchema() @@ -329,6 +562,10 @@ export async function runTest() { // Not defined if JobProgressBar not loaded jobProgressBar?.reset() + // Flush module edits back to modules map before running preview + if (activeModuleTab !== null && modules) { + modules[activeModuleTab] = { ...modules[activeModuleTab], content: editorCode } + } //@ts-ignore let job = await jobLoader.runPreview( path, @@ -355,7 +592,9 @@ } console.error(error) } - } + }, + undefined, + modules ) logPanel?.setFocusToLogs() return job @@ -410,8 +649,7 @@ selectedTab = 'main' } else { hasPreprocessor = - (selectedTab === 'preprocessor' ? !result?.no_main_func : result?.has_preprocessor) ?? - false + (selectedTab === 'preprocessor' ? !result?.auto_kind : result?.has_preprocessor) ?? false if (!hasPreprocessor && selectedTab === 'preprocessor') { selectedTab = 'main' @@ -1316,140 +1554,333 @@ +{#snippet addModuleForm(close: () => void)} +
+ + { + modulePathError = validateModulePath(modulePathInput) + }} + onkeydown={(e) => { + if (e.key === 'Enter') addModule() + if (e.key === 'Escape') close() + }} + /> + {#if modulePathError} +

{modulePathError}

+ {/if} +

Supports subfolders, e.g. utils/math.{lang === 'python3' ? 'py' : 'ts'}

+
+ + +
+
+{/snippet} + +{#snippet renameModuleForm(oldPath: string, close: () => void)} +
+ + { + renameModuleError = validateRenameModulePath(renameModuleInput, oldPath) + }} + onkeydown={(e) => { + if (e.key === 'Enter') { + renameModule(oldPath) + close() + } + if (e.key === 'Escape') close() + }} + /> + {#if renameModuleError} +

{renameModuleError}

+ {/if} +
+ + +
+
+{/snippet} + {#snippet editorContent()} -
-
- {#if assets?.length} - - {/if} - {#if isDebuggableScript && customUi?.editorBar?.debug != false} - - {/if} - {#if showDebugPanel && !showDebugConsole} - - {/if} - {#if lang === 'ansible' && hasDelegateToGitRepo} - + {#each Object.keys(modules ?? {}) as modulePath} +
+ +
+ + {#snippet trigger()} + { + e.stopPropagation() + renameModuleInput = modulePath + renameModuleError = '' + }} + onkeydown={(e) => { + if (e.key === 'Enter') { + e.stopPropagation() + renameModuleInput = modulePath + renameModuleError = '' + } + }} + > + + + {/snippet} + {#snippet content({ close })} + {@render renameModuleForm(modulePath, close)} + {/snippet} + + { + e.stopPropagation() + removeModule(modulePath) + }} + onkeydown={(e) => { + if (e.key === 'Enter') { + e.stopPropagation() + removeModule(modulePath) + } + }} + > + + +
+
+ {/each} + - Delegating to git repo - - {/if} - {#if testPanelSize === 0} - +
+ {/if} +
+
+ {#if assets?.length} + + {/if} + {#if isDebuggableScript && customUi?.editorBar?.debug != false} + + {/if} + {#if showDebugPanel && !showDebugConsole} + + {/if} + {#if lang === 'ansible' && hasDelegateToGitRepo} + + {/if} + {#if testPanelSize === 0} + btnClasses="bg-marine-400 hover:bg-marine-200 !text-primary-inverse hover:!text-primary-inverse hover:dark:!text-primary-inverse dark:bg-marine-50 dark:hover:bg-marine-50/70" + color="marine" + /> {/if} - {/if} -
+ {#if !aiChatManager.open && !disableAi} + {#if customUi?.editorBar?.aiGen != false && SUPPORTED_CHAT_SCRIPT_LANGUAGES.includes(lang ?? '')} + + {/if} + {/if} +
- {#if debugConsoleVisible} - - - + {#if debugConsoleVisible} + + + + {@render editorPane()} + + + (showDebugConsole = false)} + workspace={$workspaceStore} + jobId={debugSessionJobId ?? undefined} + /> + + + {:else} + +
{@render editorPane()} - - - (showDebugConsole = false)} - workspace={$workspaceStore} - jobId={debugSessionJobId ?? undefined} - /> - - - {:else} - -
- {@render editorPane()} -
- {/if} +
+ {/if} +
{/snippet} {#snippet editorPane()} - {#key lang} + {#key effectiveLang} { - inferSchema(e.detail) + if (activeModuleTab === null) { + code = editorCode + lastSyncedCode = code + inferSchema(e.detail) + } else if (modules && activeModuleTab) { + modules[activeModuleTab] = { ...modules[activeModuleTab], content: editorCode } + } // Refresh breakpoint positions when code changes (decorations track their lines) if (debugMode && breakpointDecorations.length > 0) { refreshBreakpointPositions() @@ -1458,20 +1889,24 @@ on:saveDraft on:toggleTestPanel={toggleTestPanel} cmdEnterAction={async () => { - await inferSchema(code) + if (activeModuleTab === null) { + await inferSchema(editorCode) + } runTest() }} formatAction={async () => { - await inferSchema(code) + if (activeModuleTab === null) { + await inferSchema(editorCode) + } try { - localStorage.setItem(path ?? 'last_save', code) + localStorage.setItem(path ?? 'last_save', activeModuleTab === null ? editorCode : code) } catch (e) { console.error('Could not save last_save to local storage', e) } dispatch('format') }} class="flex flex-1 h-full !overflow-visible" - scriptLang={lang} + scriptLang={effectiveLang} automaticLayout={true} {fixedOverflowWidgets} {args} diff --git a/frontend/src/lib/components/WorkflowTimeline.svelte b/frontend/src/lib/components/WorkflowTimeline.svelte index 66fbb5b6c5f3d..d71a4bc015a98 100644 --- a/frontend/src/lib/components/WorkflowTimeline.svelte +++ b/frontend/src/lib/components/WorkflowTimeline.svelte @@ -3,17 +3,33 @@ import { displayDate, msToSec } from '$lib/utils' import { onDestroy } from 'svelte' import { getDbClockNow } from '$lib/forLater' - import { Loader2 } from 'lucide-svelte' + import { ChevronDown, ChevronRight, Loader2 } from 'lucide-svelte' import TimelineBar from './TimelineBar.svelte' - import type { WorkflowStatus } from '$lib/gen' + import LogViewer from './LogViewer.svelte' + import ObjectViewer from './propertyPicker/ObjectViewer.svelte' + import { CheckCircle2, XCircle } from 'lucide-svelte' + import { JobService, type Job, type WorkflowStatus } from '$lib/gen' + import { workspaceStore } from '$lib/stores' interface Props { - flow_status: Record; - flowDone?: boolean; + flow_status: Record + flowDone?: boolean + stepResults?: Record + result?: any + success?: boolean + autoExpandResult?: boolean } - let { flow_status, flowDone = false }: Props = $props(); + let { flow_status, flowDone = false, stepResults = {}, result = undefined, success = true, autoExpandResult = false }: Props = $props() + let resultExpanded = $state(false) + + // Auto-expand result row when job completes (only if requested) + $effect(() => { + if (autoExpandResult && flowDone && result !== undefined) { + resultExpanded = true + } + }) let now = $state(getDbClockNow().getTime()) @@ -25,35 +41,94 @@ onDestroy(() => { interval && clearInterval(interval) + pollInterval && clearInterval(pollInterval) }) - let min = $derived(Object.values(flow_status).reduce( - (a, b) => Math.min(a, b.scheduled_for ? new Date(b.scheduled_for).getTime() : Infinity), - Infinity - )) - let max = $derived(flowDone - ? Object.values(flow_status).reduce( - (a, b) => - Math.max(a, b.started_at ? new Date(b.started_at).getTime() + (b.duration_ms ?? 0) : 0), - 0 - ) - : undefined) + + let min = $derived( + Object.values(flow_status).reduce( + (a, b) => Math.min(a, b.scheduled_for ? new Date(b.scheduled_for).getTime() : Infinity), + Infinity + ) + ) + let max = $derived( + flowDone + ? Object.values(flow_status).reduce( + (a, b) => + Math.max( + a, + b.started_at ? new Date(b.started_at).getTime() + (b.duration_ms ?? 0) : 0 + ), + 0 + ) + : undefined + ) let total = $derived(flowDone && max ? max - min : Math.max(now - min, 2000)) + + // Collapsible state + let expandedRows: Record = $state({}) + let childJobs: Record = $state({}) + let loadingJobs: Record = $state({}) + + function isStep(key: string): boolean { + return key.startsWith('_step/') + } + + function stepKey(key: string): string { + return key.slice('_step/'.length) + } + + function toggleRow(id: string) { + expandedRows[id] = !expandedRows[id] + if (expandedRows[id] && !isStep(id) && !childJobs[id]) { + fetchChildJob(id) + } + } + + async function fetchChildJob(id: string) { + const ws = $workspaceStore + if (!ws) return + loadingJobs[id] = true + try { + const job = await JobService.getJob({ workspace: ws, id }) + childJobs[id] = job as Job & { result?: any } + } catch (e) { + console.error(`Failed to fetch job ${id}:`, e) + } finally { + loadingJobs[id] = false + } + } + + // Poll for updates on expanded in-progress jobs + let pollInterval = setInterval(() => { + for (const [id, v] of Object.entries(flow_status)) { + if (isStep(id)) continue + const isRunning = v.duration_ms == undefined && v.started_at != undefined + if (expandedRows[id] && isRunning) { + fetchChildJob(id) + } + } + }, 2000) {#if flow_status}
-
-
{min ? displayDate(new Date(min), true) : ''}
{#if max && min} - {/if}
{max ? displayDate(new Date(max), true) : ''}{#if !max && min}{#if now} +
+
+
+
{min ? displayDate(new Date(min), true) : ''}
+ {#if max && min} + + {/if} +
+ {max ? displayDate(new Date(max), true) : ''} + {#if !max && min} + {#if now} {msToSec(now - min, 3)}s - {/if}{/if}
+ {/if} + + {/if} +
+
@@ -61,26 +136,57 @@
Waiting for executor
-
Execution
- {#each Object.entries(flow_status) as [k, v] (k)} -
-
- {v.name ?? k} -
+ {#each Object.entries(flow_status).sort(([, a], [, b]) => { + const ta = new Date(a.started_at ?? a.scheduled_for ?? 0).getTime() + const tb = new Date(b.started_at ?? b.scheduled_for ?? 0).getTime() + return ta - tb + }) as [k, v] (k)} + {@const isInlineStep = isStep(k)} + {@const isRunning = v.duration_ms == undefined && v.started_at != undefined} + {@const isDone = v.duration_ms != undefined} + {@const isExpanded = expandedRows[k] ?? false} +
+
+ {/if} +
+ + + {#if isExpanded} +
+ {#if isInlineStep} + + {@const result = stepResults[stepKey(k)]} + {#if isDone && result !== undefined} +
+
Result
+
+ +
+
+ {:else} +
Step completed (no result)
+ {/if} + {:else if loadingJobs[k] && !childJobs[k]} +
+ + Loading... +
+ {:else if childJobs[k]} + {@const job = childJobs[k]} + + {#if job.logs || isRunning} +
+
Logs
+ +
+ {/if} + + + {#if isDone && job.result !== undefined} +
+
Result
+
+ +
+
+ {/if} + {:else} +
No data available
+ {/if} +
+ {/if}
{/each} + {#if flowDone && result !== undefined} +
+ + {#if resultExpanded} +
+
+ +
+
+ {/if} +
+ {/if}
{:else} diff --git a/frontend/src/lib/components/common/table/ScriptRow.svelte b/frontend/src/lib/components/common/table/ScriptRow.svelte index c98e326067a88..91a2bd3b3b502 100644 --- a/frontend/src/lib/components/common/table/ScriptRow.svelte +++ b/frontend/src/lib/components/common/table/ScriptRow.svelte @@ -35,11 +35,14 @@ Share, Trash, History, - Globe2 + Globe2, + FileText } from 'lucide-svelte' import ScriptVersionHistory from '$lib/components/ScriptVersionHistory.svelte' + import WacExportDrawer from '$lib/components/scripts/WacExportDrawer.svelte' import { Drawer, DrawerContent } from '..' import NoMainFuncBadge from '$lib/components/NoMainFuncBadge.svelte' + import Popover from '$lib/components/Popover.svelte' import Tooltip from '$lib/components/Tooltip.svelte' import { getDeployUiSettings } from '$lib/components/home/deploy_ui' import { scriptToHubUrl } from '$lib/hub' @@ -106,6 +109,7 @@ const dlt: 'delete' = 'delete' let versionsDrawerOpen: boolean = $state(false) + let wacExportDrawer: WacExportDrawer | undefined = $state(undefined) {#if menuOpen} @@ -115,7 +119,7 @@ Archived {/if} - {#if script.no_main_func && script.kind !== 'preprocessor'} + {#if script.auto_kind === 'lib' && script.kind !== 'preprocessor'} {/if} + {#if script.auto_kind === 'wac'} + + {#snippet text()} + Workflow-as-Code + {/snippet} + wac + + {/if} {#if script.kind !== 'script'} {script.kind === 'failure' ? 'Error handler' : capitalize(script.kind)} { + const fullScript = await ScriptService.getScriptByPath({ + workspace: $workspaceStore!, + path: script.path + }) + wacExportDrawer?.open(fullScript) + } + } + ] + : []), { displayName: 'Duplicate/Fork', icon: GitFork, @@ -412,3 +439,5 @@ {/if} + + diff --git a/frontend/src/lib/components/flows/CreateActionsFlow.svelte b/frontend/src/lib/components/flows/CreateActionsFlow.svelte index f2080fb281c86..73f8cfedbab2a 100644 --- a/frontend/src/lib/components/flows/CreateActionsFlow.svelte +++ b/frontend/src/lib/components/flows/CreateActionsFlow.svelte @@ -7,11 +7,28 @@ import Drawer from '$lib/components/common/drawer/Drawer.svelte' import DrawerContent from '$lib/components/common/drawer/DrawerContent.svelte' import { importFlowStore } from '$lib/components/flows/flowStore.svelte' - import { Loader2, Plus } from 'lucide-svelte' + import { importScriptStore } from '$lib/components/scripts/scriptStore.svelte' + import Modal from '$lib/components/common/modal/Modal.svelte' + import Toggle from '$lib/components/Toggle.svelte' + import Tabs from '$lib/components/common/tabs/Tabs.svelte' + import Tab from '$lib/components/common/tabs/Tab.svelte' + import { PythonIcon, TypeScriptIcon } from '$lib/components/common/languageIcons' + import { Code2, Loader2, Plus } from 'lucide-svelte' import YAML from 'yaml' + + const SKIP_FLOW_MODAL_KEY = 'windmill_skip_flow_modal' + let drawer: Drawer | undefined = $state(undefined) + let wacDrawer: Drawer | undefined = $state(undefined) let pendingRaw: string | undefined = $state(undefined) + let pendingWacRaw: string | undefined = $state(undefined) let importType: 'yaml' | 'json' = $state('yaml') + let wacImportType: 'yaml' | 'json' = $state('yaml') + let flowModalOpen = $state(false) + let wacHovered = $state(false) + let skipModal = $state( + typeof localStorage !== 'undefined' && localStorage.getItem(SKIP_FLOW_MODAL_KEY) === 'true' + ) async function importRaw() { $importFlowStore = @@ -19,58 +36,229 @@ await goto('/flows/add') drawer?.closeDrawer?.() } + + async function importWacRaw() { + const parsed = + wacImportType === 'yaml' ? YAML.parse(pendingWacRaw ?? '') : JSON.parse(pendingWacRaw ?? '') + $importScriptStore = parsed + await goto(`${base}/scripts/add?import=true`) + wacDrawer?.closeDrawer?.() + } + + function handleFlowClick() { + if (skipModal) { + goto(`${base}/flows/add?nodraft=true`) + } else { + flowModalOpen = true + } + } + + function selectFlowEditor() { + flowModalOpen = false + goto(`${base}/flows/add?nodraft=true`) + } + + function selectWacPython() { + flowModalOpen = false + goto(`${base}/scripts/add?nodraft=true&wac=python`) + } + + function selectWacTypescript() { + flowModalOpen = false + goto(`${base}/scripts/add?nodraft=true&wac=typescript`) + } + + function toggleSkipModal() { + skipModal = !skipModal + localStorage.setItem(SKIP_FLOW_MODAL_KEY, String(skipModal)) + }
- + }, + { + label: 'Workflow-as-Code in TypeScript', + onClick: () => selectWacTypescript() + }, + { + label: 'Workflow-as-Code in Python', + onClick: () => selectWacPython() + }, + { + label: 'Import Workflow-as-Code', + onClick: () => { + wacDrawer?.toggleDrawer?.() + } + } + ]} + > + Flow + +
+ + + +
+
+ + + + + +
(wacHovered = true)} + onmouseleave={() => (wacHovered = false)} + > + +
+ Alpha +
+ + +
+
+ +
+
+

Workflow-as-Code

+

+ Write workflows as Python or TypeScript code as a regular Windmill script. +

+
+
+ + +
+ + +
+
+
+ +
+ + Always use the Flow editor (skip this modal) +
+
- + - drawer?.toggleDrawer?.()} - > - {#await import('$lib/components/SimpleEditor.svelte')} - - {:then Module} - - {/await} + drawer?.toggleDrawer?.()}> + + + + {#snippet content()} +
+ {#key importType} + {#await import('$lib/components/SimpleEditor.svelte')} + + {:then Module} + + {/await} + {/key} +
+ {/snippet} +
{#snippet actions()} {/snippet}
+ + + + wacDrawer?.toggleDrawer?.()}> + + + + {#snippet content()} +
+ {#key wacImportType} + {#await import('$lib/components/SimpleEditor.svelte')} + + {:then Module} + + {/await} + {/key} +
+ {/snippet} +
+ {#snippet actions()} + + {/snippet} +
+
diff --git a/frontend/src/lib/components/flows/content/ScriptEditorDrawer.svelte b/frontend/src/lib/components/flows/content/ScriptEditorDrawer.svelte index abdb497e93e0c..ae9d75daebc43 100644 --- a/frontend/src/lib/components/flows/content/ScriptEditorDrawer.svelte +++ b/frontend/src/lib/components/flows/content/ScriptEditorDrawer.svelte @@ -50,7 +50,7 @@ dedicated_worker?: boolean visible_to_runner_only?: boolean on_behalf_of_email?: string - no_main_func?: boolean + auto_kind?: string has_preprocessor?: boolean } | undefined = $state(undefined) @@ -71,7 +71,7 @@ dedicated_worker?: boolean visible_to_runner_only?: boolean on_behalf_of_email?: string - no_main_func?: boolean + auto_kind?: string has_preprocessor?: boolean } | undefined = $state(undefined) @@ -82,7 +82,7 @@ script.schema = script.schema ?? emptySchema() try { const result = await inferArgs(script.language, script.content, script.schema) - script.no_main_func = result?.no_main_func || undefined + script.auto_kind = result?.auto_kind || undefined script.has_preprocessor = result?.has_preprocessor || undefined } catch (error) { sendUserToast(`Could not parse code, are you sure it is valid?`, true) diff --git a/frontend/src/lib/components/graph/model.ts b/frontend/src/lib/components/graph/model.ts index d6fcf4a33a263..3618406371bf9 100644 --- a/frontend/src/lib/components/graph/model.ts +++ b/frontend/src/lib/components/graph/model.ts @@ -1,4 +1,4 @@ -import type { FlowStatusModule, Job } from '$lib/gen' +import type { FlowStatusModule, Job, WorkflowStatus } from '$lib/gen' import type { StateStore } from '$lib/utils' import type { FlowState } from '../flows/flowState' @@ -67,6 +67,7 @@ export type GraphModuleState = { skipped?: boolean agent_actions?: FlowStatusModule['agent_actions'] script_hash?: string + workflow_as_code_status?: WorkflowStatus } export type NestedNodes = GraphItem[] diff --git a/frontend/src/lib/components/runs/JobRunsPreview.svelte b/frontend/src/lib/components/runs/JobRunsPreview.svelte index 274679d3bcfca..62722daac867f 100644 --- a/frontend/src/lib/components/runs/JobRunsPreview.svelte +++ b/frontend/src/lib/components/runs/JobRunsPreview.svelte @@ -50,11 +50,15 @@ if (!x || typeof x !== 'object') return {} const result: Record = {} for (const [k, v] of Object.entries(x)) { - if (!k.startsWith('_')) result[k] = v as WorkflowStatus + if (!k.startsWith('_') || k.startsWith('_step/')) result[k] = v as WorkflowStatus } return result } + function getStepResults(x: any): Record { + return x?._checkpoint?.completed_steps ?? {} + } + function handleFilterByConcurrencyKey(key: string) { dispatch('filterByConcurrencyKey', key) } @@ -156,6 +160,9 @@
{/if} diff --git a/frontend/src/lib/components/scriptEditor/LogPanel.svelte b/frontend/src/lib/components/scriptEditor/LogPanel.svelte index 63eb9d5a4c681..51061f3fba5dd 100644 --- a/frontend/src/lib/components/scriptEditor/LogPanel.svelte +++ b/frontend/src/lib/components/scriptEditor/LogPanel.svelte @@ -96,12 +96,18 @@ if (!x || typeof x !== 'object') return {} const result: Record = {} for (const [k, v] of Object.entries(x)) { - if (!k.startsWith('_')) result[k] = v as WorkflowStatus + if (!k.startsWith('_') || k.startsWith('_step/')) result[k] = v as WorkflowStatus } return result } + function getStepResults(x: any): Record { + return x?._checkpoint?.completed_steps ?? {} + } + let forceJson = $state(false) + let isWac = $derived(!!previewJob?.workflow_as_code_status) + let wacDone = $derived(previewJob?.type == 'CompletedJob') @@ -141,16 +147,20 @@ {#snippet content()}
{#if selectedTab === 'logs'} + {#if isWac} +
+ +
+ {:else} - {#if previewJob?.workflow_as_code_status} - - - - {/if} + {/if} {/if} {#if selectedTab === 'history'}
diff --git a/frontend/src/lib/components/script_builder.ts b/frontend/src/lib/components/script_builder.ts index f224c3bd6adf6..ddc5a01b89d14 100644 --- a/frontend/src/lib/components/script_builder.ts +++ b/frontend/src/lib/components/script_builder.ts @@ -14,7 +14,7 @@ export interface ScriptBuilderProps { disableAi?: boolean fullyLoaded?: boolean initialPath?: string - template?: 'docker' | 'bunnative' | 'claudesandbox' | 'script' + template?: 'docker' | 'bunnative' | 'claudesandbox' | 'wac_python' | 'wac_typescript' | 'script' initialArgs?: Record lockedLanguage?: boolean showMeta?: boolean diff --git a/frontend/src/lib/components/scripts/CreateActionsScript.svelte b/frontend/src/lib/components/scripts/CreateActionsScript.svelte index 9c847c9a3a6ee..48480918e086c 100644 --- a/frontend/src/lib/components/scripts/CreateActionsScript.svelte +++ b/frontend/src/lib/components/scripts/CreateActionsScript.svelte @@ -1,7 +1,7 @@ diff --git a/frontend/src/lib/components/scripts/WacExportDrawer.svelte b/frontend/src/lib/components/scripts/WacExportDrawer.svelte new file mode 100644 index 0000000000000..59f1dca1b073c --- /dev/null +++ b/frontend/src/lib/components/scripts/WacExportDrawer.svelte @@ -0,0 +1,113 @@ + + + + + + drawer?.toggleDrawer()}> +
+ + + + {#snippet content()} +
+
+
+ {#key rawType} + + {/key} +
+ {/snippet} +
+
+
+
diff --git a/frontend/src/lib/components/scripts/scriptStore.svelte.ts b/frontend/src/lib/components/scripts/scriptStore.svelte.ts new file mode 100644 index 0000000000000..abc1fb0d633f9 --- /dev/null +++ b/frontend/src/lib/components/scripts/scriptStore.svelte.ts @@ -0,0 +1,4 @@ +import type { NewScript } from '$lib/gen' +import { writable } from 'svelte/store' + +export const importScriptStore = writable(undefined) diff --git a/frontend/src/lib/infer.ts b/frontend/src/lib/infer.ts index 9861e9b0161e5..b1b822419b055 100644 --- a/frontend/src/lib/infer.ts +++ b/frontend/src/lib/infer.ts @@ -236,7 +236,7 @@ export async function inferArgs( schema: Schema, mainOverride?: string ): Promise<{ - no_main_func: boolean | null + auto_kind: string | null has_preprocessor: boolean | null } | null> { const lastRun = get(loadSchemaLastRun) @@ -398,7 +398,7 @@ export async function inferArgs( await tick() return { - no_main_func: inferedSchema.no_main_func, + auto_kind: inferedSchema.auto_kind, has_preprocessor: inferedSchema.has_preprocessor } } diff --git a/frontend/src/lib/script_helpers.ts b/frontend/src/lib/script_helpers.ts index 5744971f32b40..d05d78f6ef71a 100644 --- a/frontend/src/lib/script_helpers.ts +++ b/frontend/src/lib/script_helpers.ts @@ -3,6 +3,8 @@ import { type Script } from './gen' import type { SupportedLanguage } from './common' import CLAUDE_SANDBOX_INIT_CODE from './templates/claude_sandbox.ts.template?raw' +import WAC_PYTHON_INIT_CODE from './templates/wac_python.py.template?raw' +import WAC_TYPESCRIPT_INIT_CODE from './templates/wac_typescript.ts.template?raw' const PYTHON_FAILURE_MODULE_CODE = `import os @@ -1378,6 +1380,12 @@ export const INITIAL_CODE = { }, claudesandbox: { script: CLAUDE_SANDBOX_INIT_CODE + }, + wac_python: { + script: WAC_PYTHON_INIT_CODE + }, + wac_typescript: { + script: WAC_TYPESCRIPT_INIT_CODE } // for related places search: ADD_NEW_LANG } @@ -1406,6 +1414,8 @@ export function initialCode( | 'powershell' | 'bunnative' | 'claudesandbox' + | 'wac_python' + | 'wac_typescript' | undefined, templateScript?: boolean ): string { @@ -1436,6 +1446,10 @@ export function initialCode( } else { return INITIAL_CODE.deno.script } + } else if (subkind === 'wac_python') { + return INITIAL_CODE.wac_python.script + } else if (subkind === 'wac_typescript') { + return INITIAL_CODE.wac_typescript.script } else if (language === 'python3') { if (kind === 'trigger') { return INITIAL_CODE.python3.trigger @@ -1538,6 +1552,8 @@ export function getResetCode( | 'powershell' | 'bunnative' | 'claudesandbox' + | 'wac_python' + | 'wac_typescript' | undefined ) { if (language === 'deno') { diff --git a/frontend/src/lib/templates/wac_python.py.template b/frontend/src/lib/templates/wac_python.py.template new file mode 100644 index 0000000000000..42f166ecc036d --- /dev/null +++ b/frontend/src/lib/templates/wac_python.py.template @@ -0,0 +1,42 @@ +from wmill import task, task_script, step, sleep, wait_for_approval, get_resume_urls, workflow + +# IMPORTANT: All computation must happen inside @task(), task_script(), or step(). +# Code outside these wrappers is NOT checkpointed and WILL be re-executed +# on every resume or retry. Never put API calls, database writes, or +# non-deterministic logic (e.g. datetime.now()) in the top-level workflow body. + +# task_script() references a module file (see the helper.py tab) +helper = task_script("./helper.py") + + +# @task() wraps a function as a workflow step that runs as a separate job. +# The result is checkpointed — on retry, completed tasks are skipped. +@task() +async def process(x: str) -> str: + return f"processed: {x}" + + +@workflow +async def main(x: str): + a = await process(x) + + # task_script() calls a module file as a separate job (also checkpointed) + b = await helper(a=a) + + # step() runs inline code and checkpoints the result (no child job). + # Use it for lightweight operations you don't want as a separate script. + urls = await step("get_urls", lambda: get_resume_urls()) + + # sleep() suspends the workflow server-side without holding a worker + await sleep(1) + + # wait_for_approval() suspends until an external event resumes it. + # Like sleep(), it does not hold a worker. Approve/reject URLs are + # available in the timeline step's details in the UI. + approval = await wait_for_approval(timeout=3600) + + return { + "processed": a, + "helper_result": b, + "approval": approval, + } diff --git a/frontend/src/lib/templates/wac_typescript.ts.template b/frontend/src/lib/templates/wac_typescript.ts.template new file mode 100644 index 0000000000000..0d22d6d402b11 --- /dev/null +++ b/frontend/src/lib/templates/wac_typescript.ts.template @@ -0,0 +1,48 @@ +import { + task, + taskScript, + step, + sleep, + waitForApproval, + getResumeUrls, + workflow, +} from "windmill-client"; + +// IMPORTANT: All computation must happen inside task(), taskScript(), or step(). +// Code outside these wrappers is NOT checkpointed and WILL be re-executed +// on every resume or retry. Never put API calls, database writes, or +// non-deterministic logic (e.g. Date.now()) in the top-level workflow body. + +// taskScript() references a module file (see the helper.ts tab) +const helper = taskScript("./helper.ts"); + +// task() wraps a function as a workflow step that runs as a separate job. +// The result is checkpointed — on retry, completed tasks are skipped. +const process = task(async (x: string): Promise => { + return `processed: ${x}`; +}); + +export const main = workflow(async (x: string) => { + const a = await process(x); + + // taskScript() calls a module file as a separate job (also checkpointed) + const b = await helper({ a }); + + // step() runs inline code and checkpoints the result (no child job). + // Use it for lightweight operations you don't want as a separate script. + const urls = await step("get_urls", () => getResumeUrls()); + + // sleep() suspends the workflow server-side without holding a worker + await sleep(1); + + // waitForApproval() suspends until an external event resumes it. + // Like sleep(), it does not hold a worker. Approve/reject URLs are + // available in the timeline step's details in the UI. + const approval = await waitForApproval({ timeout: 3600 }); + + return { + processed: a, + helper_result: b, + approval, + }; +}); diff --git a/frontend/src/routes/(root)/(logged)/run/[...run]/+page.svelte b/frontend/src/routes/(root)/(logged)/run/[...run]/+page.svelte index ac73b934fd09a..5bf8f21ac5668 100644 --- a/frontend/src/routes/(root)/(logged)/run/[...run]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/run/[...run]/+page.svelte @@ -284,11 +284,15 @@ if (!x || typeof x !== 'object') return {} const result: Record = {} for (const [k, v] of Object.entries(x)) { - if (!k.startsWith('_')) result[k] = v as WorkflowStatus + if (!k.startsWith('_') || k.startsWith('_step/')) result[k] = v as WorkflowStatus } return result } + function getStepResults(x: any): Record { + return x?._checkpoint?.completed_steps ?? {} + } + function forkPreview() { if (isFlowPreview(job?.job_kind)) { const state = { @@ -790,6 +794,9 @@
diff --git a/frontend/src/routes/(root)/(logged)/scripts/add/+page.svelte b/frontend/src/routes/(root)/(logged)/scripts/add/+page.svelte index afc3f9e270d7f..8f43dde796982 100644 --- a/frontend/src/routes/(root)/(logged)/scripts/add/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/scripts/add/+page.svelte @@ -14,6 +14,8 @@ import { get } from 'svelte/store' import { untrack } from 'svelte' import ScriptEditorSkeleton from '$lib/components/ScriptEditorSkeleton.svelte' + import { importScriptStore } from '$lib/components/scripts/scriptStore.svelte' + import { isWorkflowAsCode } from '$lib/components/graph/wacToFlow' type Script = NewScript & { draft_triggers?: Trigger[] @@ -29,6 +31,8 @@ const showMeta = /true|1/i.test(page.url.searchParams.get('show_meta') ?? '0') const urlArgs = page.url.searchParams.get('initial_args') const collabLang = page.url.searchParams.get('lang') as ScriptLang | null + const wacParam = page.url.searchParams.get('wac') + const importParam = page.url.searchParams.get('import') let initialArgs = urlArgs ? decodeState(urlArgs) : (get(initialArgsStore) ?? {}) if (get(initialArgsStore)) $initialArgsStore = undefined @@ -59,7 +63,7 @@ schema: schema, is_template: false, extra_perms: {}, - language: collabLang ?? ($defaultScripts?.order?.filter( + language: (wacParam === 'python' ? 'python3' : wacParam === 'typescript' ? 'bun' : null) ?? collabLang ?? ($defaultScripts?.order?.filter( (x) => $defaultScripts?.hidden == undefined || !$defaultScripts.hidden.includes(x) )?.[0] ?? 'bun') as ScriptLang, kind: 'script' @@ -122,6 +126,27 @@ loadHub() + let importedWacTemplate: 'wac_python' | 'wac_typescript' | undefined = undefined + if (importParam && $importScriptStore) { + const imported = $importScriptStore + $importScriptStore = undefined + const isWac = isWorkflowAsCode(imported.content ?? '', imported.language ?? '') + script = { + ...defaultScript(), + ...imported, + path: path ?? '', + hash: '', + extra_perms: {} + } + if (isWac) { + importedWacTemplate = + imported.language === 'python3' ? 'wac_python' : 'wac_typescript' + sendUserToast('WAC script loaded from YAML/JSON') + } else { + sendUserToast('Script loaded from YAML/JSON') + } + } + $effect(() => { if ($workspaceStore) { untrack(() => loadTemplate()) @@ -134,6 +159,7 @@ {initialArgs} bind:this={scriptBuilder} lockedLanguage={templatePath != null || hubPath != null} + template={importedWacTemplate ?? (wacParam === 'python' ? 'wac_python' : wacParam === 'typescript' ? 'wac_typescript' : 'script')} onDeploy={(e) => { goto(`/scripts/get/${e.hash}?workspace=${$workspaceStore}`) }} diff --git a/frontend/src/routes/(root)/(logged)/scripts/get/[...hash]/+page.svelte b/frontend/src/routes/(root)/(logged)/scripts/get/[...hash]/+page.svelte index 4fa95cf6474bc..4ba269b74a33a 100644 --- a/frontend/src/routes/(root)/(logged)/scripts/get/[...hash]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/scripts/get/[...hash]/+page.svelte @@ -62,11 +62,14 @@ Trash, Play, ClipboardCopy, - LayoutDashboard + LayoutDashboard, + ChevronDown, + ChevronRight } from 'lucide-svelte' import { SCRIPT_VIEW_SHOW_PUBLISH_TO_HUB } from '$lib/consts' import { scriptToHubUrl } from '$lib/hub' import SharedBadge from '$lib/components/SharedBadge.svelte' + import Popover from '$lib/components/Popover.svelte' import ScriptVersionHistory from '$lib/components/ScriptVersionHistory.svelte' import { createAppFromScript } from '$lib/components/details/createAppFromScript' import { importStore } from '$lib/components/apps/store' @@ -95,6 +98,8 @@ let can_write = $state(false) let isHubScript = $state(false) let deploymentInProgress = $state(false) + let expandedModuleLocks: Record = $state({}) + let expandedModuleCode: Record = $state({}) let deploymentJobId: string | undefined = $state(undefined) let intervalId: number let shareModal: ShareModal | undefined = $state() @@ -208,7 +213,7 @@ kind: 'script', starred: false, schema: hubScript.schema as Script['schema'], - no_main_func: false, + auto_kind: undefined, has_preprocessor: false } can_write = false @@ -352,7 +357,12 @@ }) } - if (script && !$userStore?.operator && !isCloudHosted() && !isRuleActive('DisableWorkspaceForking')) { + if ( + script && + !$userStore?.operator && + !isCloudHosted() && + !isRuleActive('DisableWorkspaceForking') + ) { buttons.push({ label: 'Edit in fork', buttonProps: { @@ -680,6 +690,14 @@ {#if $workspaceStore && script} {/if} + {#if script?.auto_kind === 'wac'} + + {#snippet text()} + Workflow-as-Code + {/snippet} + wac + + {/if} {#if script?.codebase} bundle + {#if script?.modules} + {#each Object.entries(script.modules) as [modulePath, mod]} +
+ + {#if expandedModuleCode[modulePath]} +
+ +
+ {/if} +
+ {/each} + {/if}
@@ -942,6 +987,45 @@ There is no lock file for this script

{/if} + {#if script?.modules} + {@const moduleEntries = Object.entries(script.modules).filter( + ([_, m]) => m.lock + )} + {#each moduleEntries as [modulePath, mod]} +
+ + {#if expandedModuleLocks[modulePath]} +
+
+ {/if} +
+ {/each} + {/if}
diff --git a/python-client/wmill/uv.lock b/python-client/wmill/uv.lock new file mode 100644 index 0000000000000..6c1c3ab21922a --- /dev/null +++ b/python-client/wmill/uv.lock @@ -0,0 +1,139 @@ +version = 1 +revision = 3 +requires-python = ">=3.14" + +[manifest] + +[manifest.dependency-groups] +dev = [ + { name = "httpx", specifier = ">=0.28.1" }, + { name = "pytest", specifier = ">=9.0.2" }, +] + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] diff --git a/python-client/wmill/wmill/client.py b/python-client/wmill/wmill/client.py index c8c30de4f105c..cdd3342771fae 100644 --- a/python-client/wmill/wmill/client.py +++ b/python-client/wmill/wmill/client.py @@ -11,7 +11,7 @@ import warnings import json from json import JSONDecodeError -from typing import Dict, Any, Union, Literal, Optional +from typing import Callable, Dict, Any, Union, Literal, Optional import re import httpx @@ -2431,6 +2431,7 @@ def _next_step(self, name: str, script: str, func=None, dispatch_type: str = "in else: return self._never_resolve() + print(f"\n--- WAC: {key} ---") info = {"name": name or key, "script": script or key, "args": kwargs, "key": key, "dispatch_type": dispatch_type} if _task_options: for opt_key in ("timeout", "tag", "cache_ttl", "priority", "concurrent_limit", "concurrency_key", "concurrency_time_window_s"): @@ -2472,6 +2473,7 @@ async def _wait_for_approval( if self._executing_key is not None: await _asyncio.Future() + print(f"\n--- WAC: wait_for_approval({key}) ---") raise _StepSuspend({ "mode": "approval", "key": key, @@ -2489,6 +2491,7 @@ async def _sleep(self, seconds: int): if self._executing_key is not None: await _asyncio.Future() + print(f"\n--- WAC: sleep({key}, {seconds}s) ---") raise _StepSuspend({ "mode": "sleep", "key": key, @@ -2497,6 +2500,10 @@ async def _sleep(self, seconds: int): }) async def _run_inline_step(self, name: str, fn): + import json as _json_mod + import time as _time_mod + from datetime import datetime as _dt, timezone as _tz + key = self._alloc_key(name or "step") if key in self._completed: @@ -2513,15 +2520,22 @@ async def _run_inline_step(self, name: str, fn): if self._executing_key is not None: await _asyncio.Future() + print(f"\n--- WAC: {key} ---") + started_at = _dt.now(_tz.utc).isoformat() + print(f"WM_WAC_STEP: {_json_mod.dumps({'key': key, 'started_at': started_at})}") + t0 = _time_mod.monotonic() result = fn() if _asyncio.iscoroutine(result): result = await result + duration_ms = int((_time_mod.monotonic() - t0) * 1000) raise _StepSuspend({ "mode": "inline_checkpoint", "steps": [], "key": key, "result": result, + "started_at": started_at, + "duration_ms": duration_ms, }) @@ -2568,7 +2582,7 @@ async def run_external(x: int): ... # Remove None values _task_opts = {k: v for k, v in _task_opts.items() if v is not None} or None - def decorator(func): + def decorator(func) -> Callable[..., Any]: task_path = path task_name = func.__name__ @@ -2585,7 +2599,6 @@ def _merge_args(args, kwargs): merged[f"arg{i}"] = arg return merged - @functools.wraps(func) def wrapper(*args, **kwargs): # WAC v2: inside a @workflow context ctx = _workflow_ctx.get(None) @@ -2815,11 +2828,16 @@ async def _run_workflow_async(func, checkpoint: dict, input_args: dict): if mode == "step_complete": return {"type": "complete", "result": info.get("result")} if mode == "inline_checkpoint": - return { + out = { "type": "inline_checkpoint", "key": info["key"], "result": info.get("result"), } + if "started_at" in info: + out["started_at"] = info["started_at"] + if "duration_ms" in info: + out["duration_ms"] = info["duration_ms"] + return out if mode == "approval": return { "type": "approval", diff --git a/typescript-client/client.ts b/typescript-client/client.ts index 8578c98bb6f02..49087a8b60840 100644 --- a/typescript-client/client.ts +++ b/typescript-client/client.ts @@ -1558,6 +1558,8 @@ export class WorkflowCtx { this._suspended = true; const steps = [...this.pending]; this.pending = []; + const names = steps.map(s => s.name).join(", "); + console.log(`\n--- WAC: ${names} ---`); throw new StepSuspend({ mode: steps.length > 1 ? "parallel" : "sequential", steps, @@ -1589,6 +1591,7 @@ export class WorkflowCtx { } // Throw immediately — approval is always a blocking step + console.log(`\n--- WAC: approval(${key}) ---`); throw new StepSuspend({ mode: "approval", key, @@ -1609,6 +1612,7 @@ export class WorkflowCtx { return { then: () => new Promise(() => {}) }; } + console.log(`\n--- WAC: sleep(${key}, ${seconds}s) ---`); throw new StepSuspend({ mode: "sleep", key, @@ -1636,8 +1640,13 @@ export class WorkflowCtx { return new Promise(() => {}); } + console.log(`\n--- WAC: ${key} ---`); + const startedAt = new Date().toISOString(); + console.log(`WM_WAC_STEP: ${JSON.stringify({ key, started_at: startedAt })}`); + const t0 = Date.now(); const result = await fn(); - throw new StepSuspend({ mode: "inline_checkpoint", steps: [], key, result }); + const durationMs = Date.now() - t0; + throw new StepSuspend({ mode: "inline_checkpoint", steps: [], key, result, started_at: startedAt, duration_ms: durationMs }); } }