windmill-cli 1.589.3 → 1.590.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/esm/deps/jsr.io/@windmill-labs/shared-utils/{1.0.10 → 1.0.11}/lib.es.js +4 -4
- package/esm/deps.js +1 -1
- package/esm/gen/core/OpenAPI.js +1 -1
- package/esm/gen/services.gen.js +149 -27
- package/esm/src/commands/app/app_metadata.js +81 -47
- package/esm/src/commands/app/apps.js +2 -0
- package/esm/src/commands/app/bundle.js +6 -2
- package/esm/src/commands/app/dev.js +40 -14
- package/esm/src/commands/app/lint.js +159 -0
- package/esm/src/commands/app/raw_apps.js +169 -24
- package/esm/src/commands/init/init.js +8 -0
- package/esm/src/commands/resource-type/resource-type.js +29 -2
- package/esm/src/commands/script/script.js +11 -0
- package/esm/src/commands/sync/sync.js +49 -17
- package/esm/src/main.js +1 -1
- package/esm/src/utils/resource_types.js +32 -0
- package/esm/src/utils/utils.js +8 -0
- package/esm/windmill-utils-internal/src/path-utils/path-assigner.js +78 -0
- package/package.json +1 -1
- package/types/bootstrap/common.d.ts +12 -0
- package/types/bootstrap/common.d.ts.map +1 -1
- package/types/deps/jsr.io/@windmill-labs/shared-utils/{1.0.10 → 1.0.11}/lib.es.d.ts.map +1 -1
- package/types/deps.d.ts +1 -1
- package/types/gen/services.gen.d.ts +74 -16
- package/types/gen/services.gen.d.ts.map +1 -1
- package/types/gen/types.gen.d.ts +568 -62
- package/types/gen/types.gen.d.ts.map +1 -1
- package/types/src/commands/app/app_metadata.d.ts +1 -1
- package/types/src/commands/app/app_metadata.d.ts.map +1 -1
- package/types/src/commands/app/apps.d.ts.map +1 -1
- package/types/src/commands/app/bundle.d.ts.map +1 -1
- package/types/src/commands/app/dev.d.ts.map +1 -1
- package/types/src/commands/app/lint.d.ts +12 -0
- package/types/src/commands/app/lint.d.ts.map +1 -0
- package/types/src/commands/app/metadata.d.ts +2 -3
- package/types/src/commands/app/metadata.d.ts.map +1 -1
- package/types/src/commands/app/raw_apps.d.ts +26 -1
- package/types/src/commands/app/raw_apps.d.ts.map +1 -1
- package/types/src/commands/init/init.d.ts.map +1 -1
- package/types/src/commands/resource-type/resource-type.d.ts +3 -1
- package/types/src/commands/resource-type/resource-type.d.ts.map +1 -1
- package/types/src/commands/script/script.d.ts +5 -0
- package/types/src/commands/script/script.d.ts.map +1 -1
- package/types/src/commands/sync/sync.d.ts.map +1 -1
- package/types/src/main.d.ts +1 -1
- package/types/src/utils/resource_types.d.ts +3 -0
- package/types/src/utils/resource_types.d.ts.map +1 -0
- package/types/src/utils/utils.d.ts +2 -0
- package/types/src/utils/utils.d.ts.map +1 -1
- package/types/windmill-utils-internal/src/gen/types.gen.d.ts +568 -62
- package/types/windmill-utils-internal/src/gen/types.gen.d.ts.map +1 -1
- package/types/windmill-utils-internal/src/path-utils/path-assigner.d.ts +22 -0
- package/types/windmill-utils-internal/src/path-utils/path-assigner.d.ts.map +1 -1
- /package/types/deps/jsr.io/@windmill-labs/shared-utils/{1.0.10 → 1.0.11}/lib.es.d.ts +0 -0
|
@@ -15,6 +15,7 @@ import { requireLogin } from "../../core/auth.js";
|
|
|
15
15
|
import { GLOBAL_CONFIG_OPT } from "../../core/conf.js";
|
|
16
16
|
import { replaceInlineScripts } from "./apps.js";
|
|
17
17
|
import { APP_BACKEND_FOLDER, inferRunnableSchemaFromFile, } from "./app_metadata.js";
|
|
18
|
+
import { loadRunnablesFromBackend } from "./raw_apps.js";
|
|
18
19
|
const DEFAULT_PORT = 4000;
|
|
19
20
|
const DEFAULT_HOST = "localhost";
|
|
20
21
|
// HTML template with live reload
|
|
@@ -128,8 +129,12 @@ async function dev(opts) {
|
|
|
128
129
|
const wmillPlugin = {
|
|
129
130
|
name: "wmill-virtual",
|
|
130
131
|
setup(build) {
|
|
131
|
-
// Intercept imports of
|
|
132
|
-
|
|
132
|
+
// Intercept imports of wmill with various path formats:
|
|
133
|
+
// - wmill, wmill.ts (bare import)
|
|
134
|
+
// - /wmill, /wmill.ts (absolute)
|
|
135
|
+
// - ./wmill, ./wmill.ts (same directory)
|
|
136
|
+
// - ../wmill, ../../wmill, etc. (parent directories)
|
|
137
|
+
build.onResolve({ filter: /^(\.\.\/)+wmill(\.ts)?$|^(\.\/|\/)?wmill(\.ts)?$/ }, (args) => {
|
|
133
138
|
log.info(colors.yellow(`[wmill-virtual] Intercepted: ${args.path}`));
|
|
134
139
|
return {
|
|
135
140
|
path: args.path,
|
|
@@ -469,16 +474,30 @@ const command = new Command()
|
|
|
469
474
|
export default command;
|
|
470
475
|
/**
|
|
471
476
|
* Generates wmill.d.ts with type definitions for runnables.
|
|
472
|
-
*
|
|
477
|
+
* Loads runnables from separate YAML files in the backend folder (new format)
|
|
478
|
+
* or falls back to raw_app.yaml (old format).
|
|
479
|
+
* Merges in-memory inferred schemas with runnables.
|
|
473
480
|
*
|
|
474
481
|
* @param schemaOverrides - In-memory schema overrides (runnableId -> schema)
|
|
475
482
|
*/
|
|
476
483
|
async function genRunnablesTs(schemaOverrides = {}) {
|
|
477
484
|
log.info(colors.blue("🔄 Generating wmill.d.ts..."));
|
|
478
|
-
const
|
|
479
|
-
const
|
|
485
|
+
const localPath = process.cwd();
|
|
486
|
+
const backendPath = path.join(localPath, APP_BACKEND_FOLDER);
|
|
487
|
+
// Load runnables from separate files (new format) or fall back to raw_app.yaml (old format)
|
|
488
|
+
let runnables = await loadRunnablesFromBackend(backendPath);
|
|
489
|
+
if (Object.keys(runnables).length === 0) {
|
|
490
|
+
// Fall back to old format
|
|
491
|
+
try {
|
|
492
|
+
const rawApp = (await yamlParseFile(path.join(localPath, "raw_app.yaml")));
|
|
493
|
+
runnables = rawApp?.["runnables"] ?? {};
|
|
494
|
+
}
|
|
495
|
+
catch {
|
|
496
|
+
runnables = {};
|
|
497
|
+
}
|
|
498
|
+
}
|
|
480
499
|
// Apply schema overrides from in-memory cache
|
|
481
|
-
if (
|
|
500
|
+
if (Object.keys(schemaOverrides).length > 0) {
|
|
482
501
|
for (const [runnableId, schema] of Object.entries(schemaOverrides)) {
|
|
483
502
|
if (runnables[runnableId]?.inlineScript) {
|
|
484
503
|
runnables[runnableId].inlineScript.schema = schema;
|
|
@@ -497,9 +516,16 @@ async function genRunnablesTs(schemaOverrides = {}) {
|
|
|
497
516
|
async function loadRunnables() {
|
|
498
517
|
try {
|
|
499
518
|
const localPath = process.cwd();
|
|
500
|
-
const
|
|
501
|
-
|
|
502
|
-
|
|
519
|
+
const backendPath = path.join(localPath, APP_BACKEND_FOLDER);
|
|
520
|
+
// Load runnables from separate files (new format) or fall back to raw_app.yaml (old format)
|
|
521
|
+
let runnables = await loadRunnablesFromBackend(backendPath);
|
|
522
|
+
if (Object.keys(runnables).length === 0) {
|
|
523
|
+
// Fall back to old format
|
|
524
|
+
const rawApp = (await yamlParseFile(path.join(localPath, "raw_app.yaml")));
|
|
525
|
+
runnables = rawApp?.runnables ?? {};
|
|
526
|
+
}
|
|
527
|
+
replaceInlineScripts(runnables, backendPath + SEP, true);
|
|
528
|
+
return runnables;
|
|
503
529
|
}
|
|
504
530
|
catch (error) {
|
|
505
531
|
log.error(colors.red(`Failed to load runnables: ${error.message}`));
|
|
@@ -539,12 +565,12 @@ async function executeRunnable(runnable, workspace, appPath, runnableId, args) {
|
|
|
539
565
|
cache_ttl: inlineScript.cache_ttl,
|
|
540
566
|
};
|
|
541
567
|
}
|
|
542
|
-
else if (
|
|
543
|
-
|
|
544
|
-
const
|
|
568
|
+
else if (runnable.type === "path" && runnable.runType && runnable.path) {
|
|
569
|
+
// Path-based runnables have type: "path" and runType: "script"|"hubscript"|"flow"
|
|
570
|
+
const prefix = runnable.runType;
|
|
545
571
|
requestBody.path =
|
|
546
|
-
|
|
547
|
-
? `${
|
|
572
|
+
prefix !== "hubscript"
|
|
573
|
+
? `${prefix}/${runnable.path}`
|
|
548
574
|
: `script/${runnable.path}`;
|
|
549
575
|
}
|
|
550
576
|
const uuid = await wmill.executeComponent({
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// deno-lint-ignore-file no-explicit-any
|
|
2
|
+
import * as dntShim from "../../../_dnt.shims.js";
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
import { Command, colors, log, yamlParseFile } from "../../../deps.js";
|
|
7
|
+
import { createBundle } from "./bundle.js";
|
|
8
|
+
import { APP_BACKEND_FOLDER } from "./app_metadata.js";
|
|
9
|
+
import { loadRunnablesFromBackend } from "./raw_apps.js";
|
|
10
|
+
/**
|
|
11
|
+
* Validates the structure of raw_app.yaml
|
|
12
|
+
*/
|
|
13
|
+
function validateRawAppYaml(appData) {
|
|
14
|
+
const errors = [];
|
|
15
|
+
const warnings = [];
|
|
16
|
+
// Check required fields
|
|
17
|
+
if (!appData.summary) {
|
|
18
|
+
errors.push("Missing required field: 'summary'");
|
|
19
|
+
}
|
|
20
|
+
else if (typeof appData.summary !== "string") {
|
|
21
|
+
errors.push("Field 'summary' must be a string");
|
|
22
|
+
}
|
|
23
|
+
// Note: 'runnables' is no longer required in raw_app.yaml
|
|
24
|
+
// Runnables can be stored in separate files in the backend folder
|
|
25
|
+
return { errors, warnings };
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Validates that runnables exist either in backend/*.yaml files or in raw_app.yaml
|
|
29
|
+
*/
|
|
30
|
+
async function validateRunnables(appDir, appData) {
|
|
31
|
+
const errors = [];
|
|
32
|
+
const warnings = [];
|
|
33
|
+
const backendPath = path.join(appDir, APP_BACKEND_FOLDER);
|
|
34
|
+
// Load runnables from separate files (new format)
|
|
35
|
+
const runnablesFromBackend = await loadRunnablesFromBackend(backendPath);
|
|
36
|
+
const hasBackendRunnables = Object.keys(runnablesFromBackend).length > 0;
|
|
37
|
+
// Check for runnables in raw_app.yaml (old format)
|
|
38
|
+
const hasYamlRunnables = appData.runnables &&
|
|
39
|
+
typeof appData.runnables === "object" &&
|
|
40
|
+
!Array.isArray(appData.runnables) &&
|
|
41
|
+
Object.keys(appData.runnables).length > 0;
|
|
42
|
+
if (!hasBackendRunnables && !hasYamlRunnables) {
|
|
43
|
+
errors.push("No runnables found. Expected either:\n" +
|
|
44
|
+
" - Runnable YAML files in the 'backend/' folder (e.g., backend/myRunnable.yaml)\n" +
|
|
45
|
+
" - Or a 'runnables' field in raw_app.yaml (legacy format)");
|
|
46
|
+
}
|
|
47
|
+
else if (hasBackendRunnables) {
|
|
48
|
+
log.info(colors.gray(` Found ${Object.keys(runnablesFromBackend).length} runnable(s) in backend folder`));
|
|
49
|
+
}
|
|
50
|
+
else if (hasYamlRunnables) {
|
|
51
|
+
log.info(colors.gray(` Found ${Object.keys(appData.runnables).length} runnable(s) in raw_app.yaml (legacy format)`));
|
|
52
|
+
warnings.push("Using legacy format with runnables in raw_app.yaml. Consider migrating to separate files in backend/");
|
|
53
|
+
}
|
|
54
|
+
return { errors, warnings };
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Checks if the app can be built successfully
|
|
58
|
+
*/
|
|
59
|
+
async function validateBuild(appDir) {
|
|
60
|
+
const errors = [];
|
|
61
|
+
const warnings = [];
|
|
62
|
+
try {
|
|
63
|
+
log.info(colors.blue("🔨 Testing build..."));
|
|
64
|
+
// Try to create a bundle - this will validate that all dependencies are in place
|
|
65
|
+
await createBundle({
|
|
66
|
+
production: true,
|
|
67
|
+
minify: false,
|
|
68
|
+
});
|
|
69
|
+
log.info(colors.green("✅ Build successful"));
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
errors.push(`Build failed: ${error.message}`);
|
|
73
|
+
}
|
|
74
|
+
return { errors, warnings };
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Validates a raw app folder
|
|
78
|
+
*/
|
|
79
|
+
async function lintRawApp(appDir, opts) {
|
|
80
|
+
const errors = [];
|
|
81
|
+
const warnings = [];
|
|
82
|
+
// Check if we're in a .raw_app folder
|
|
83
|
+
const currentDirName = path.basename(appDir);
|
|
84
|
+
if (!currentDirName.endsWith(".raw_app")) {
|
|
85
|
+
errors.push(`Not a raw app folder: '${currentDirName}' does not end with '.raw_app'`);
|
|
86
|
+
return { valid: false, errors, warnings };
|
|
87
|
+
}
|
|
88
|
+
// Check if raw_app.yaml exists
|
|
89
|
+
const rawAppPath = path.join(appDir, "raw_app.yaml");
|
|
90
|
+
if (!fs.existsSync(rawAppPath)) {
|
|
91
|
+
errors.push("Missing raw_app.yaml file");
|
|
92
|
+
return { valid: false, errors, warnings };
|
|
93
|
+
}
|
|
94
|
+
log.info(colors.blue("📋 Validating raw_app.yaml structure..."));
|
|
95
|
+
// Parse and validate raw_app.yaml
|
|
96
|
+
let appData;
|
|
97
|
+
try {
|
|
98
|
+
appData = await yamlParseFile(rawAppPath);
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
errors.push(`Failed to parse raw_app.yaml: ${error.message}`);
|
|
102
|
+
return { valid: false, errors, warnings };
|
|
103
|
+
}
|
|
104
|
+
const yamlValidation = validateRawAppYaml(appData);
|
|
105
|
+
errors.push(...yamlValidation.errors);
|
|
106
|
+
warnings.push(...yamlValidation.warnings);
|
|
107
|
+
if (errors.length > 0) {
|
|
108
|
+
return { valid: false, errors, warnings };
|
|
109
|
+
}
|
|
110
|
+
log.info(colors.green("✅ raw_app.yaml structure is valid"));
|
|
111
|
+
// Validate runnables (either in backend folder or in raw_app.yaml)
|
|
112
|
+
log.info(colors.blue("📋 Validating runnables..."));
|
|
113
|
+
const runnablesValidation = await validateRunnables(appDir, appData);
|
|
114
|
+
errors.push(...runnablesValidation.errors);
|
|
115
|
+
warnings.push(...runnablesValidation.warnings);
|
|
116
|
+
if (errors.length > 0) {
|
|
117
|
+
return { valid: false, errors, warnings };
|
|
118
|
+
}
|
|
119
|
+
log.info(colors.green("✅ Runnables are valid"));
|
|
120
|
+
// Validate build
|
|
121
|
+
const buildValidation = await validateBuild(appDir);
|
|
122
|
+
errors.push(...buildValidation.errors);
|
|
123
|
+
warnings.push(...buildValidation.warnings);
|
|
124
|
+
return {
|
|
125
|
+
valid: errors.length === 0,
|
|
126
|
+
errors,
|
|
127
|
+
warnings,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Main lint command
|
|
132
|
+
*/
|
|
133
|
+
async function lint(opts, appFolder) {
|
|
134
|
+
const targetDir = appFolder ?? process.cwd();
|
|
135
|
+
log.info(colors.bold.blue(`\n🔍 Linting raw app: ${targetDir}\n`));
|
|
136
|
+
const result = await lintRawApp(targetDir, opts);
|
|
137
|
+
// Display results
|
|
138
|
+
if (result.warnings.length > 0) {
|
|
139
|
+
log.info(colors.yellow("\n⚠️ Warnings:"));
|
|
140
|
+
result.warnings.forEach((warning) => {
|
|
141
|
+
log.info(colors.yellow(` - ${warning}`));
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
if (result.errors.length > 0) {
|
|
145
|
+
log.info(colors.red("\n❌ Errors:"));
|
|
146
|
+
result.errors.forEach((error) => {
|
|
147
|
+
log.info(colors.red(` - ${error}`));
|
|
148
|
+
});
|
|
149
|
+
log.info(colors.red("\n❌ Lint failed\n"));
|
|
150
|
+
dntShim.Deno.exit(1);
|
|
151
|
+
}
|
|
152
|
+
log.info(colors.green("\n✅ All checks passed\n"));
|
|
153
|
+
}
|
|
154
|
+
const command = new Command()
|
|
155
|
+
.description("Lint a raw app folder to validate structure and buildability")
|
|
156
|
+
.arguments("[app_folder:string]")
|
|
157
|
+
.option("--fix", "Attempt to fix common issues (not implemented yet)")
|
|
158
|
+
.action(lint);
|
|
159
|
+
export default command;
|
|
@@ -2,12 +2,147 @@
|
|
|
2
2
|
import * as dntShim from "../../../_dnt.shims.js";
|
|
3
3
|
import { requireLogin } from "../../core/auth.js";
|
|
4
4
|
import { resolveWorkspace, validatePath } from "../../core/context.js";
|
|
5
|
-
import { colors, log, SEP, windmillUtils, yamlParseFile, } from "../../../deps.js";
|
|
5
|
+
import { colors, log, SEP, windmillUtils, yamlParseFile, yamlStringify, } from "../../../deps.js";
|
|
6
6
|
import * as wmill from "../../../gen/services.gen.js";
|
|
7
|
+
import path from "node:path";
|
|
7
8
|
import { isSuperset } from "../../types.js";
|
|
8
9
|
import { replaceInlineScripts, repopulateFields } from "./apps.js";
|
|
9
10
|
import { createBundle, detectFrameworks } from "./bundle.js";
|
|
10
11
|
import { APP_BACKEND_FOLDER } from "./app_metadata.js";
|
|
12
|
+
import { writeIfChanged } from "../../utils/utils.js";
|
|
13
|
+
import { yamlOptions } from "../sync/sync.js";
|
|
14
|
+
import { EXTENSION_TO_LANGUAGE, getLanguageFromExtension, } from "../../../windmill-utils-internal/src/path-utils/path-assigner.js";
|
|
15
|
+
/**
|
|
16
|
+
* Finds the content file for a runnable by looking for files matching the runnableId.
|
|
17
|
+
* Returns the file extension and content, or undefined if not found.
|
|
18
|
+
*/
|
|
19
|
+
async function findRunnableContentFile(backendPath, runnableId, allFiles) {
|
|
20
|
+
// Look for files matching pattern: {runnableId}.{ext}
|
|
21
|
+
// where ext is a known language extension
|
|
22
|
+
for (const fileName of allFiles) {
|
|
23
|
+
// Skip yaml and lock files
|
|
24
|
+
if (fileName.endsWith(".yaml") || fileName.endsWith(".lock")) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
// Check if file starts with runnableId followed by a dot
|
|
28
|
+
if (!fileName.startsWith(runnableId + ".")) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
// Extract extension (everything after the first dot following runnableId)
|
|
32
|
+
const ext = fileName.substring(runnableId.length + 1);
|
|
33
|
+
// Check if this is a recognized extension
|
|
34
|
+
if (EXTENSION_TO_LANGUAGE[ext]) {
|
|
35
|
+
try {
|
|
36
|
+
const content = await dntShim.Deno.readTextFile(path.join(backendPath, fileName));
|
|
37
|
+
return { ext, content };
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Loads all runnables from separate YAML files in the backend folder.
|
|
48
|
+
* Each runnable is stored in a file named `<runnableId>.yaml`.
|
|
49
|
+
*
|
|
50
|
+
* Converts from file format to API format:
|
|
51
|
+
* - For inline scripts (type: 'inline'): derives inlineScript from sibling files
|
|
52
|
+
* - For path-based runnables (type: 'script'|'hubscript'|'flow'): converts to API format
|
|
53
|
+
* e.g., { type: "script" } -> { type: "path", runType: "script" }
|
|
54
|
+
*
|
|
55
|
+
* Returns an empty object if the backend folder doesn't exist.
|
|
56
|
+
*
|
|
57
|
+
* @param backendPath - Path to the backend folder
|
|
58
|
+
* @param defaultTs - Default TypeScript runtime ("bun" or "deno")
|
|
59
|
+
*/
|
|
60
|
+
export async function loadRunnablesFromBackend(backendPath, defaultTs = "bun") {
|
|
61
|
+
const runnables = {};
|
|
62
|
+
try {
|
|
63
|
+
// First, collect all files in the backend folder
|
|
64
|
+
const allFiles = [];
|
|
65
|
+
for await (const entry of dntShim.Deno.readDir(backendPath)) {
|
|
66
|
+
if (entry.isFile) {
|
|
67
|
+
allFiles.push(entry.name);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Process YAML files (runnable metadata files)
|
|
71
|
+
for (const fileName of allFiles) {
|
|
72
|
+
if (!fileName.endsWith(".yaml")) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const runnableId = fileName.replace(".yaml", "");
|
|
76
|
+
const filePath = path.join(backendPath, fileName);
|
|
77
|
+
const runnable = (await yamlParseFile(filePath));
|
|
78
|
+
// If this is an inline script (type: 'inline'), derive inlineScript from files
|
|
79
|
+
if (runnable?.type === "inline") {
|
|
80
|
+
const contentFile = await findRunnableContentFile(backendPath, runnableId, allFiles);
|
|
81
|
+
if (contentFile) {
|
|
82
|
+
const language = getLanguageFromExtension(contentFile.ext, defaultTs);
|
|
83
|
+
// Try to load lock file
|
|
84
|
+
let lock;
|
|
85
|
+
try {
|
|
86
|
+
lock = await dntShim.Deno.readTextFile(path.join(backendPath, `${runnableId}.lock`));
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// No lock file, that's fine
|
|
90
|
+
}
|
|
91
|
+
// Reconstruct inlineScript object
|
|
92
|
+
runnable.inlineScript = {
|
|
93
|
+
content: contentFile.content,
|
|
94
|
+
language,
|
|
95
|
+
...(lock ? { lock } : {}),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
else if (runnable?.type === "script" ||
|
|
100
|
+
runnable?.type === "hubscript" ||
|
|
101
|
+
runnable?.type === "flow") {
|
|
102
|
+
// For path-based runnables, convert from file format to API format
|
|
103
|
+
// { type: "script" } -> { type: "path", runType: "script" }
|
|
104
|
+
// { type: "hubscript" } -> { type: "path", runType: "hubscript" }
|
|
105
|
+
// { type: "flow" } -> { type: "path", runType: "flow" }
|
|
106
|
+
const { type, schema: _schema, ...rest } = runnable;
|
|
107
|
+
runnable.type = "path";
|
|
108
|
+
runnable.runType = type;
|
|
109
|
+
// Remove schema if present
|
|
110
|
+
delete runnable.schema;
|
|
111
|
+
Object.assign(runnable, rest);
|
|
112
|
+
}
|
|
113
|
+
runnables[runnableId] = runnable;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
if (error.name !== "NotFound") {
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return runnables;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Writes a single runnable to its YAML file in the backend folder.
|
|
125
|
+
* The file will be named `<runnableId>.yaml`.
|
|
126
|
+
*
|
|
127
|
+
* Converts from API format to file format:
|
|
128
|
+
* - For inline scripts: keeps type: "inline"
|
|
129
|
+
* - For path-based runnables: converts { type: "path", runType: "script" } to { type: "script" }
|
|
130
|
+
* and removes schema field
|
|
131
|
+
*/
|
|
132
|
+
export function writeRunnableToBackend(backendPath, runnableId, runnable) {
|
|
133
|
+
let runnableToWrite = { ...runnable };
|
|
134
|
+
// Convert path-based runnables from API format to file format
|
|
135
|
+
if (runnable.type === "path" && runnable.runType) {
|
|
136
|
+
// { type: "path", runType: "script" } -> { type: "script" }
|
|
137
|
+
const { type: _type, runType, schema: _schema, ...rest } = runnable;
|
|
138
|
+
runnableToWrite = {
|
|
139
|
+
type: runType,
|
|
140
|
+
...rest,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
const filePath = path.join(backendPath, `${runnableId}.yaml`);
|
|
144
|
+
writeIfChanged(filePath, yamlStringify(runnableToWrite, yamlOptions));
|
|
145
|
+
}
|
|
11
146
|
const alreadySynced = [];
|
|
12
147
|
async function collectAppFiles(localPath) {
|
|
13
148
|
const files = {};
|
|
@@ -27,9 +162,9 @@ async function collectAppFiles(localPath) {
|
|
|
27
162
|
}
|
|
28
163
|
else if (entry.isFile) {
|
|
29
164
|
// Skip raw_app.yaml as it's metadata, not an app file
|
|
30
|
-
// Skip
|
|
31
|
-
if (
|
|
32
|
-
|
|
165
|
+
// Skip package-lock.json as it's generated
|
|
166
|
+
if (entry.name === "raw_app.yaml" ||
|
|
167
|
+
entry.name === "package-lock.json") {
|
|
33
168
|
continue;
|
|
34
169
|
}
|
|
35
170
|
const content = await dntShim.Deno.readTextFile(fullPath);
|
|
@@ -67,11 +202,31 @@ export async function pushRawApp(workspace, remotePath, localPath, message) {
|
|
|
67
202
|
if (!localPath.endsWith(SEP)) {
|
|
68
203
|
localPath += SEP;
|
|
69
204
|
}
|
|
70
|
-
const
|
|
71
|
-
const localApp = (await yamlParseFile(
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
205
|
+
const appFilePath = localPath + "raw_app.yaml";
|
|
206
|
+
const localApp = (await yamlParseFile(appFilePath));
|
|
207
|
+
// Load runnables from separate YAML files in the backend folder
|
|
208
|
+
// Falls back to reading from raw_app.yaml if no separate files exist (backward compat)
|
|
209
|
+
const backendPath = path.join(localPath, APP_BACKEND_FOLDER);
|
|
210
|
+
const runnablesFromBackend = await loadRunnablesFromBackend(backendPath);
|
|
211
|
+
let runnables;
|
|
212
|
+
if (Object.keys(runnablesFromBackend).length > 0) {
|
|
213
|
+
// Use runnables from separate files (new format)
|
|
214
|
+
runnables = runnablesFromBackend;
|
|
215
|
+
log.info(colors.gray(`Loaded ${Object.keys(runnables).length} runnables from backend folder`));
|
|
216
|
+
}
|
|
217
|
+
else if (localApp.runnables) {
|
|
218
|
+
// Fall back to runnables from raw_app.yaml (old format)
|
|
219
|
+
runnables = localApp.runnables;
|
|
220
|
+
log.info(colors.gray(`Loaded ${Object.keys(runnables).length} runnables from raw_app.yaml (legacy format)`));
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
runnables = {};
|
|
224
|
+
}
|
|
225
|
+
replaceInlineScripts(runnables, backendPath + SEP, true);
|
|
226
|
+
repopulateFields(runnables);
|
|
227
|
+
// Create a temporary app object for policy generation
|
|
228
|
+
const appForPolicy = { ...localApp, runnables };
|
|
229
|
+
await generatingPolicy(appForPolicy, remotePath, localApp?.["public"] ?? false);
|
|
75
230
|
const files = await collectAppFiles(localPath);
|
|
76
231
|
async function createBundleRaw() {
|
|
77
232
|
log.info(colors.yellow.bold(`Creating raw app ${remotePath} bundle...`));
|
|
@@ -86,7 +241,7 @@ export async function pushRawApp(workspace, remotePath, localPath, message) {
|
|
|
86
241
|
});
|
|
87
242
|
}
|
|
88
243
|
if (app) {
|
|
89
|
-
if (isSuperset(localApp, app)) {
|
|
244
|
+
if (isSuperset({ ...localApp, runnables }, app)) {
|
|
90
245
|
log.info(colors.green(`App ${remotePath} is up to date`));
|
|
91
246
|
return;
|
|
92
247
|
}
|
|
@@ -97,10 +252,10 @@ export async function pushRawApp(workspace, remotePath, localPath, message) {
|
|
|
97
252
|
path: remotePath,
|
|
98
253
|
formData: {
|
|
99
254
|
app: {
|
|
100
|
-
value: { runnables
|
|
255
|
+
value: { runnables, files },
|
|
101
256
|
path: remotePath,
|
|
102
257
|
summary: localApp.summary,
|
|
103
|
-
policy:
|
|
258
|
+
policy: appForPolicy.policy,
|
|
104
259
|
deployment_message: message,
|
|
105
260
|
custom_path: localApp.custom_path,
|
|
106
261
|
},
|
|
@@ -115,10 +270,10 @@ export async function pushRawApp(workspace, remotePath, localPath, message) {
|
|
|
115
270
|
workspace,
|
|
116
271
|
formData: {
|
|
117
272
|
app: {
|
|
118
|
-
value: { runnables
|
|
273
|
+
value: { runnables, files },
|
|
119
274
|
path: remotePath,
|
|
120
275
|
summary: localApp.summary,
|
|
121
|
-
policy:
|
|
276
|
+
policy: appForPolicy.policy,
|
|
122
277
|
deployment_message: message,
|
|
123
278
|
custom_path: localApp.custom_path,
|
|
124
279
|
},
|
|
@@ -126,16 +281,6 @@ export async function pushRawApp(workspace, remotePath, localPath, message) {
|
|
|
126
281
|
css,
|
|
127
282
|
},
|
|
128
283
|
});
|
|
129
|
-
// await wmill.createApp({
|
|
130
|
-
// workspace,
|
|
131
|
-
// requestBody: {
|
|
132
|
-
// path: remotePath,
|
|
133
|
-
// deployment_message: message,
|
|
134
|
-
// value: { runnables: localApp.runnables, files },
|
|
135
|
-
// summary: localApp.summary,
|
|
136
|
-
// policy: localApp.policy,
|
|
137
|
-
// },
|
|
138
|
-
// });
|
|
139
284
|
}
|
|
140
285
|
}
|
|
141
286
|
export async function generatingPolicy(app, path, publicApp) {
|
|
@@ -4,6 +4,7 @@ import { readLockfile } from "../../utils/metadata.js";
|
|
|
4
4
|
import { SCRIPT_GUIDANCE } from "../../guidance/script_guidance.js";
|
|
5
5
|
import { FLOW_GUIDANCE } from "../../guidance/flow_guidance.js";
|
|
6
6
|
import { getActiveWorkspaceOrFallback } from "../workspace/workspace.js";
|
|
7
|
+
import { generateRTNamespace } from "../resource-type/resource-type.js";
|
|
7
8
|
/**
|
|
8
9
|
* Bootstrap a windmill project with a wmill.yaml file
|
|
9
10
|
*/
|
|
@@ -200,6 +201,13 @@ ${flowGuidanceContent}
|
|
|
200
201
|
log.warn(`Could not create guidance files: ${error}`);
|
|
201
202
|
}
|
|
202
203
|
}
|
|
204
|
+
// Generate resource type namespace
|
|
205
|
+
try {
|
|
206
|
+
await generateRTNamespace(opts);
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
log.warn(`Could not pull resource types and generate TypeScript namespace: ${error instanceof Error ? error.message : error}`);
|
|
210
|
+
}
|
|
203
211
|
}
|
|
204
212
|
const command = new Command()
|
|
205
213
|
.description("Bootstrap a windmill project with a wmill.yaml file")
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
// deno-lint-ignore-file no-explicit-any
|
|
2
2
|
import * as dntShim from "../../../_dnt.shims.js";
|
|
3
|
+
import { writeFileSync } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import process from "node:process";
|
|
3
6
|
import { isSuperset, parseFromFile, removeType, } from "../../types.js";
|
|
4
7
|
import { requireLogin } from "../../core/auth.js";
|
|
5
8
|
import { resolveWorkspace } from "../../core/context.js";
|
|
6
9
|
import { colors, Command, log, Table } from "../../../deps.js";
|
|
7
10
|
import * as wmill from "../../../gen/services.gen.js";
|
|
11
|
+
import { compileResourceTypeToTsType } from "../../utils/resource_types.js";
|
|
12
|
+
import { capitalize, toCamel } from "../../utils/utils.js";
|
|
8
13
|
export async function pushResourceType(workspace, remotePath, resource, localResource) {
|
|
9
14
|
remotePath = removeType(remotePath, "resource-type");
|
|
10
15
|
try {
|
|
@@ -61,7 +66,11 @@ async function list(opts) {
|
|
|
61
66
|
.header(["Workspace", "Name", "Schema"])
|
|
62
67
|
.padding(2)
|
|
63
68
|
.border(true)
|
|
64
|
-
.body(res.map((x) => [
|
|
69
|
+
.body(res.map((x) => [
|
|
70
|
+
x.workspace_id ?? "Global",
|
|
71
|
+
x.name,
|
|
72
|
+
JSON.stringify(x.schema, null, 2),
|
|
73
|
+
]))
|
|
65
74
|
.render();
|
|
66
75
|
}
|
|
67
76
|
else {
|
|
@@ -73,6 +82,22 @@ async function list(opts) {
|
|
|
73
82
|
.render();
|
|
74
83
|
}
|
|
75
84
|
}
|
|
85
|
+
export async function generateRTNamespace(opts) {
|
|
86
|
+
const workspace = await resolveWorkspace(opts);
|
|
87
|
+
await requireLogin(opts);
|
|
88
|
+
const rts = await wmill.listResourceType({
|
|
89
|
+
workspace: workspace.workspaceId,
|
|
90
|
+
});
|
|
91
|
+
let namespaceContent = "declare namespace RT {\n";
|
|
92
|
+
namespaceContent += rts
|
|
93
|
+
.map((resourceType) => {
|
|
94
|
+
return ` type ${toCamel(capitalize(resourceType.name))} = ${compileResourceTypeToTsType(resourceType.schema).replaceAll("\n", "\n ")}`;
|
|
95
|
+
})
|
|
96
|
+
.join("\n\n");
|
|
97
|
+
namespaceContent += "\n}";
|
|
98
|
+
writeFileSync(path.join(process.cwd(), "rt.d.ts"), namespaceContent);
|
|
99
|
+
log.info(colors.green("Created rt.d.ts with resource types namespace (RT) for TypeScript."));
|
|
100
|
+
}
|
|
76
101
|
const command = new Command()
|
|
77
102
|
.description("resource type related commands")
|
|
78
103
|
.action(() => log.info("2 actions available, list and push."))
|
|
@@ -81,5 +106,7 @@ const command = new Command()
|
|
|
81
106
|
.action(list)
|
|
82
107
|
.command("push", "push a local resource spec. This overrides any remote versions.")
|
|
83
108
|
.arguments("<file_path:string> <name:string>")
|
|
84
|
-
.action(push)
|
|
109
|
+
.action(push)
|
|
110
|
+
.command("generate-namespace", "Create a TypeScript definition file with the RT namespace generated from the resource types")
|
|
111
|
+
.action(generateRTNamespace);
|
|
85
112
|
export default command;
|
|
@@ -17,6 +17,16 @@ import { mergeConfigWithConfigFile, readConfigFile, } from "../../core/conf.js";
|
|
|
17
17
|
import { listSyncCodebases } from "../../utils/codebase.js";
|
|
18
18
|
import fs from "node:fs";
|
|
19
19
|
import { execSync } from "node:child_process";
|
|
20
|
+
/**
|
|
21
|
+
* Checks if a path is inside a raw app backend folder.
|
|
22
|
+
* Matches patterns like: .../myApp.raw_app/backend/...
|
|
23
|
+
*/
|
|
24
|
+
export function isRawAppBackendPath(filePath) {
|
|
25
|
+
// Normalize path separators for consistent matching
|
|
26
|
+
const normalizedPath = filePath.replaceAll(SEP, "/");
|
|
27
|
+
// Check if path contains pattern: *.raw_app/backend/
|
|
28
|
+
return /\.raw_app\/backend\//.test(normalizedPath);
|
|
29
|
+
}
|
|
20
30
|
async function push(opts, filePath) {
|
|
21
31
|
opts = await mergeConfigWithConfigFile(opts);
|
|
22
32
|
const workspace = await resolveWorkspace(opts);
|
|
@@ -80,6 +90,7 @@ export async function handleScriptMetadata(path, workspace, alreadySynced, messa
|
|
|
80
90
|
}
|
|
81
91
|
export async function handleFile(path, workspace, alreadySynced, message, opts, rawWorkspaceDependencies, codebases) {
|
|
82
92
|
if (!path.includes(".inline_script.") &&
|
|
93
|
+
!isRawAppBackendPath(path) &&
|
|
83
94
|
exts.some((exts) => path.endsWith(exts))) {
|
|
84
95
|
if (alreadySynced.includes(path)) {
|
|
85
96
|
return true;
|