refactor(cli): migrate CLI from Deno to Bun/Node.js (#8041)

* fix: only enable EE features in test backend when license key is available

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: skip EE tests without license key and exclude test-skills from test discovery

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: unskip passing tests and add duplicate (remote, workspaceId) check in addWorkspace

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(cli): migrate from Deno APIs to Node.js/Bun-compatible APIs

Replace Deno-specific APIs with Node.js equivalents across the entire CLI
codebase to enable running on Node.js/Bun. Switch build system from dnt
to bun, update imports from jsr:/npm: prefixed to bare specifiers, and
add package.json/tsconfig.json for the Node.js ecosystem.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* all

* test(cli): expand test coverage with new integration and unit tests

Add standalone_commands.test.ts covering folder list, schedule list,
resource-type list/push/update, script show/run/bootstrap, and user
commands. Add unit tests for filePathExtensionFromContentType and
removeExtensionToPath. Add git_unit, local_encryption_unit,
resource_folders_unit, and settings_unit test files. Fix schedule
cron expressions (6-field format), add includeSchedules flag, improve
test setup with pre-build and auto-cleanup, and support TEST_CLI_RUNTIME=node.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(cli): replace Deno.readFile with node:fs in WASM loaders and add schema parsing tests

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(cli): switch WASM parsers from local files to npm packages

Use published windmill-parser-wasm-* npm packages instead of local
wasm/ files. A loadParser() helper uses createRequire to resolve the
.wasm binary from node_modules and passes it to init() via
readFileSync, avoiding fetch() and Deno.readFile() patches.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test(cli): add coverage for --locks-required lint feature

Add 15 tests covering the lock-checking functionality merged from main:
- checkMissingLocks: standalone scripts (python, bun, bash), inline
  lock file resolution (valid, empty, missing), flow inline rawscripts
  (with/without locks, nested forloopflow), app inline scripts, raw
  apps without backend folder
- runLint --locks-required integration: reports issues when locks
  missing, skips checks when flag absent, passes when locks exist

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* ci(cli): replace Deno with Bun in CI workflows

- cli-tests.yml: remove Deno setup, use `bun test` instead of
  `deno test`, add `bun install` step for dependency installation
- npm_on_release.yml: replace Deno setup with Bun setup for CLI
  publishing
- build.sh: add `bun install` before building so CI has dependencies

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(cli): pre-start backend in test preload and remove Deno test leftovers

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(cli): normalize path separators for Windows compatibility

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* more tests + windows

* ci(cli): use Blacksmith runner for Windows tests

Switch test-windows job from windows-latest to blacksmith-16vcpu-windows-2025
for faster CI execution.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(cli): fix Windows path separator expectations in unit tests

buildMetadataPath and extractResourceName normalize to forward slashes
internally, so tests should not expect platform-specific separators in
their output.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(cli): fix Windows CI test failures for dev_server and script_run

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(cli): set BUN_PATH and NODE_BIN_PATH for backend worker on Windows

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* ci(cli): add SSH debug step on Windows test failure

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(cli): use native path separators for ignore check in dev mode on Windows

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-02-21 22:19:04 +01:00
committed by GitHub
parent c4de11a406
commit a2cefdf0a2
122 changed files with 7738 additions and 6326 deletions
+21 -22
View File
@@ -23,16 +23,16 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Deno
uses: denoland/setup-deno@v2
with:
deno-version: v2.x
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Generate Windmill client
working-directory: cli
run: ./gen_wm_client.sh
@@ -69,11 +69,6 @@ jobs:
cache: true
cache-workspaces: backend
- name: Setup Deno
uses: denoland/setup-deno@v2
with:
deno-version: v2.x
- name: Setup Node.js
uses: actions/setup-node@v4
with:
@@ -90,6 +85,10 @@ jobs:
- name: Symlink Node to /usr/bin/node
run: sudo ln -sf $(which node) /usr/bin/node
- name: Install dependencies
working-directory: cli
run: bun install
- name: Generate Windmill clients
working-directory: cli
run: |
@@ -101,12 +100,10 @@ jobs:
env:
DATABASE_URL: postgres://postgres:changeme@localhost:5432
CI_MINIMAL_FEATURES: "true"
run: |
deno test --no-check --allow-all test/ \
--ignore=test/cargo_backend_example.test.ts
run: bun test --timeout 120000 test/
test-windows:
runs-on: windows-latest
runs-on: blacksmith-16vcpu-windows-2025
steps:
- name: Checkout code
@@ -126,11 +123,6 @@ jobs:
cache: true
cache-workspaces: backend
- name: Setup Deno
uses: denoland/setup-deno@v2
with:
deno-version: v2.x
- name: Setup Node.js
uses: actions/setup-node@v4
with:
@@ -150,6 +142,10 @@ jobs:
echo "BUN_PATH=$bunPath" >> $env:GITHUB_OUTPUT
echo "NODE_BIN_PATH=$nodePath" >> $env:GITHUB_OUTPUT
- name: Install dependencies
working-directory: cli
run: bun install
- name: Generate Windmill clients
working-directory: cli
shell: bash
@@ -165,9 +161,12 @@ jobs:
CI_MINIMAL_FEATURES: "true"
BUN_PATH: ${{ steps.runtime-paths.outputs.BUN_PATH }}
NODE_BIN_PATH: ${{ steps.runtime-paths.outputs.NODE_BIN_PATH }}
run: |
deno test --no-check --allow-all test/ `
--ignore=test/cargo_backend_example.test.ts
run: bun test --timeout 120000 test/
- name: Keep runner alive for SSH debug
if: failure()
shell: pwsh
run: Start-Sleep -Seconds 3600
# Combined summary job for branch protection
test-summary:
+2 -2
View File
@@ -25,9 +25,9 @@ jobs:
with:
node-version: "20.x"
registry-url: "https://registry.npmjs.org"
- uses: denoland/setup-deno@v2
- uses: oven-sh/setup-bun@v2
with:
deno-version: v2.x
bun-version: latest
- run: cd cli && ./build.sh && cd npm && npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+1
View File
@@ -0,0 +1 @@
@jsr:registry=https://npm.jsr.io
+83
View File
@@ -0,0 +1,83 @@
import { VERSION } from "./src/main.ts";
import { readFileSync, writeFileSync, rmSync, cpSync } from "node:fs";
import { join } from "node:path";
const outDir = "./npm";
// Parser npm packages — used as externals and added to generated package.json
const parserPackages = [
"windmill-parser-wasm-py", "windmill-parser-wasm-ts",
"windmill-parser-wasm-regex", "windmill-parser-wasm-go",
"windmill-parser-wasm-php", "windmill-parser-wasm-rust",
"windmill-parser-wasm-yaml", "windmill-parser-wasm-csharp",
"windmill-parser-wasm-nu", "windmill-parser-wasm-java",
"windmill-parser-wasm-ruby",
];
const parserExternals = parserPackages.flatMap(p => ["--external", p]);
// Clean output directory
rmSync(outDir, { recursive: true, force: true });
// Build with bun — bundle everything except esbuild (platform-specific binary),
// svelte (optional, only needed for `wmill app bundle/dev`), and parser packages
// (loaded at runtime via init() with readFileSync for the .wasm binary).
console.log("Bundling with bun build...");
const buildResult = Bun.spawnSync([
"bun", "build", "src/main.ts",
"--outdir", join(outDir, "esm"),
"--target", "node",
"--format", "esm",
"--external", "esbuild",
"--external", "svelte",
"--external", "svelte/compiler",
...parserExternals,
], { cwd: import.meta.dir, stdout: "inherit", stderr: "inherit" });
if (buildResult.exitCode !== 0) {
console.error("Build failed");
process.exit(1);
}
// Add shebang to main.js
const mainJsPath = join(outDir, "esm", "main.js");
const mainJs = readFileSync(mainJsPath, "utf-8");
writeFileSync(mainJsPath, "#!/usr/bin/env node\n" + mainJs, "utf-8");
// Copy LICENSE and README
cpSync("../LICENSE", join(outDir, "LICENSE"));
cpSync("README.md", join(outDir, "README.md"));
// Generate package.json
const packageJson = {
name: "windmill-cli",
version: VERSION,
description: "CLI for Windmill",
license: "Apache 2.0",
type: "module",
main: "esm/main.js",
bin: {
wmill: "esm/main.js",
},
repository: {
type: "git",
url: "git+https://github.com/windmill-labs/windmill.git",
},
bugs: {
url: "https://github.com/windmill-labs/windmill/issues",
},
dependencies: {
esbuild: "^0.24.2",
...Object.fromEntries(parserPackages.map(p => [p, "*"])),
},
optionalDependencies: {
svelte: "^5.0.0",
},
};
writeFileSync(
join(outDir, "package.json"),
JSON.stringify(packageJson, null, 2) + "\n",
"utf-8"
);
console.log(`Built npm package v${VERSION} to ${outDir}/`);
+5 -6
View File
@@ -9,12 +9,11 @@ set -e
# Generate utils client files
./windmill-utils-internal/gen_wm_client.sh
# Add .ts extensions to windmill-utils-internal
./windmill-utils-internal/remove-ts-ext.sh -r
# Install dependencies
bun install
# Run dnt
echo "Running dnt..."
deno run -A dnt.ts
# Build npm package with bun
echo "Building npm package..."
bun run build-npm.ts
echo "Build complete!"
+319
View File
@@ -0,0 +1,319 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "windmill-cli-dev",
"dependencies": {
"@ayonli/jsext": "^1.9.0",
"@cliffy/ansi": "npm:@jsr/cliffy__ansi@1.0.0",
"@cliffy/command": "npm:@jsr/cliffy__command@1.0.0",
"@cliffy/prompt": "npm:@jsr/cliffy__prompt@1.0.0",
"@cliffy/table": "npm:@jsr/cliffy__table@1.0.0",
"@std/encoding": "npm:@jsr/std__encoding@1.0.10",
"@std/log": "npm:@jsr/std__log@0.224.14",
"@std/path": "npm:@jsr/std__path@1.1.4",
"@std/yaml": "npm:@jsr/std__yaml@1.0.10",
"@windmill-labs/shared-utils": "npm:@jsr/windmill-labs__shared-utils@1.0.12",
"diff": "^5.2.0",
"esbuild": "0.24.2",
"get-port": "7.1.0",
"jszip": "3.8.0",
"minimatch": "^10.0.0",
"open": "^10.0.0",
"svelte": "^5.45.2",
"windmill-parser-wasm-csharp": "*",
"windmill-parser-wasm-go": "*",
"windmill-parser-wasm-java": "*",
"windmill-parser-wasm-nu": "*",
"windmill-parser-wasm-php": "*",
"windmill-parser-wasm-py": "*",
"windmill-parser-wasm-regex": "*",
"windmill-parser-wasm-ruby": "*",
"windmill-parser-wasm-rust": "*",
"windmill-parser-wasm-ts": "*",
"windmill-parser-wasm-yaml": "*",
"windmill-yaml-validator": "1.1.1",
"ws": "8.18.0",
"yaml": "^2.7.0",
},
"devDependencies": {
"@types/diff": "^5.2.3",
"@types/node": "^22.0.0",
"@types/ws": "^8.5.0",
"typescript": "^5.7.0",
},
},
},
"packages": {
"@ayonli/jsext": ["@ayonli/jsext@1.9.0", "", { "dependencies": { "iconv-lite": "^0.6.3", "sudo-prompt": "^9.2.1", "ws": "^8.17.0", "zod": "^3.23.8" } }, "sha512-hIu6lQhoLr5e26lmt+vzopuZffaAyb623r4+8HlN/rhXgm2ywHslzk7UHiATdfDbfPjBARkB6cfXjVEi3aav6g=="],
"@cliffy/ansi": ["@jsr/cliffy__ansi@1.0.0", "https://npm.jsr.io/~/11/@jsr/cliffy__ansi/1.0.0.tgz", { "dependencies": { "@jsr/cliffy__internal": "1.0.0", "@jsr/std__encoding": "^1.0.10", "@jsr/std__fmt": "^1.0.9", "@jsr/std__io": "~0.225.3" } }, "sha512-JesgTdgR0aW1mZv96VqvRHr2efzr4MgDFMnoT+hkhaiCpmyBz33sHM5peAoMJUbGVfEfQAsysIXvvgoFYoveYg=="],
"@cliffy/command": ["@jsr/cliffy__command@1.0.0", "https://npm.jsr.io/~/11/@jsr/cliffy__command/1.0.0.tgz", { "dependencies": { "@jsr/cliffy__flags": "1.0.0", "@jsr/cliffy__internal": "1.0.0", "@jsr/cliffy__table": "1.0.0", "@jsr/std__fmt": "^1.0.9", "@jsr/std__semver": "^1.0.8", "@jsr/std__text": "^1.0.17" } }, "sha512-oObplVtu1tvpkhgpuPDHZidx9g3axVOfRMQGmw7ZSGxp0+vZIJGiEtpcSvlN0XfuEhOG8neqfVBSSE9txrKanw=="],
"@cliffy/prompt": ["@jsr/cliffy__prompt@1.0.0", "https://npm.jsr.io/~/11/@jsr/cliffy__prompt/1.0.0.tgz", { "dependencies": { "@jsr/cliffy__ansi": "1.0.0", "@jsr/cliffy__internal": "1.0.0", "@jsr/cliffy__keycode": "1.0.0", "@jsr/std__assert": "^1.0.18", "@jsr/std__fmt": "^1.0.9", "@jsr/std__io": "~0.225.3", "@jsr/std__path": "^1.1.4", "@jsr/std__text": "^1.0.17" } }, "sha512-JDuHcCAjScV0IUj389brneF6AzJyyP0pK8mymsrGN5/PGQfqK8zr96QpFlo1wmo8BY/3JQAdNfy6NZkPCJ6VWA=="],
"@cliffy/table": ["@jsr/cliffy__table@1.0.0", "https://npm.jsr.io/~/11/@jsr/cliffy__table/1.0.0.tgz", { "dependencies": { "@jsr/std__fmt": "^1.0.9" } }, "sha512-VoLxH0DjofHWPWKUc5N+oCwXB6O6e+carnhp23yJTa7qokBb+SCrTIABEgQdIe/p0bxgmZhz17xt2efaAxXvbQ=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.24.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.24.2", "", { "os": "android", "cpu": "arm" }, "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.24.2", "", { "os": "android", "cpu": "arm64" }, "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.24.2", "", { "os": "android", "cpu": "x64" }, "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.24.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.24.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.24.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.24.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.24.2", "", { "os": "linux", "cpu": "arm" }, "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.24.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.24.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.24.2", "", { "os": "linux", "cpu": "none" }, "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.24.2", "", { "os": "linux", "cpu": "none" }, "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.24.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.24.2", "", { "os": "linux", "cpu": "none" }, "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.24.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.24.2", "", { "os": "linux", "cpu": "x64" }, "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q=="],
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.24.2", "", { "os": "none", "cpu": "arm64" }, "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.24.2", "", { "os": "none", "cpu": "x64" }, "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw=="],
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.24.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.24.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.24.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.24.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.24.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.24.2", "", { "os": "win32", "cpu": "x64" }, "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@jsr/cliffy__ansi": ["@jsr/cliffy__ansi@1.0.0", "https://npm.jsr.io/~/11/@jsr/cliffy__ansi/1.0.0.tgz", { "dependencies": { "@jsr/cliffy__internal": "1.0.0", "@jsr/std__encoding": "^1.0.10", "@jsr/std__fmt": "^1.0.9", "@jsr/std__io": "~0.225.3" } }, "sha512-JesgTdgR0aW1mZv96VqvRHr2efzr4MgDFMnoT+hkhaiCpmyBz33sHM5peAoMJUbGVfEfQAsysIXvvgoFYoveYg=="],
"@jsr/cliffy__flags": ["@jsr/cliffy__flags@1.0.0", "https://npm.jsr.io/~/11/@jsr/cliffy__flags/1.0.0.tgz", { "dependencies": { "@jsr/cliffy__internal": "1.0.0", "@jsr/std__text": "^1.0.17" } }, "sha512-j/v3J8MWu0tkYyisZ2w1HxELxxL/qg6vey9+fRkbTJ+S9J0GeLUn2joouikG7aXpULKCXHTjJ9XH9gQx+F3npw=="],
"@jsr/cliffy__internal": ["@jsr/cliffy__internal@1.0.0", "https://npm.jsr.io/~/11/@jsr/cliffy__internal/1.0.0.tgz", { "dependencies": { "@jsr/std__fmt": "^1.0.9" } }, "sha512-YPkbccbuu+kE55k+nia5jJx5Tu/IolBDXZTAgEA+YRGOzq8I1VkXajwykFXvSbXeVee3zQBU7y0HajVDB7ujQA=="],
"@jsr/cliffy__keycode": ["@jsr/cliffy__keycode@1.0.0", "https://npm.jsr.io/~/11/@jsr/cliffy__keycode/1.0.0.tgz", {}, "sha512-1ot+y8oZheBTpfgCazWjSOAK2Y2nOQD7NwMuiSAkcRuc1t7VizQZfDpZtBx97NlkYWgjn6ylArt2xyhiyLKRhA=="],
"@jsr/cliffy__table": ["@jsr/cliffy__table@1.0.0", "https://npm.jsr.io/~/11/@jsr/cliffy__table/1.0.0.tgz", { "dependencies": { "@jsr/std__fmt": "^1.0.9" } }, "sha512-VoLxH0DjofHWPWKUc5N+oCwXB6O6e+carnhp23yJTa7qokBb+SCrTIABEgQdIe/p0bxgmZhz17xt2efaAxXvbQ=="],
"@jsr/std__assert": ["@jsr/std__assert@1.0.19", "https://npm.jsr.io/~/11/@jsr/std__assert/1.0.19.tgz", { "dependencies": { "@jsr/std__internal": "^1.0.12" } }, "sha512-pEj6RPkGbqlgRmyKwATp4cUs6+ijxtdrv3bq8v1d2I2CEcMEyPaO8cVKro61wGRDH4cNg8Zx6haztvK/9m7gkA=="],
"@jsr/std__bytes": ["@jsr/std__bytes@1.0.6", "https://npm.jsr.io/~/11/@jsr/std__bytes/1.0.6.tgz", {}, "sha512-St6yKggjFGhxS52IFLJWvkchRFbAKg2Xh8UxA4S1EGz7GJ2Ui+ssDDldj/w2c8vCxvl6qgR0HaYbKeFJNqujmA=="],
"@jsr/std__encoding": ["@jsr/std__encoding@1.0.10", "https://npm.jsr.io/~/11/@jsr/std__encoding/1.0.10.tgz", {}, "sha512-WK2njnDTyKefroRNk2Ooq7GStp6Y0ccAvr4To+Z/zecRAGe7+OSvH9DbiaHpAKwEi2KQbmpWMOYsdNt+TsdmSw=="],
"@jsr/std__fmt": ["@jsr/std__fmt@1.0.9", "https://npm.jsr.io/~/11/@jsr/std__fmt/1.0.9.tgz", {}, "sha512-YFJJMozmORj2K91c5J9opWeh0VUwrd+Mwb7Pr0FkVCAKVLu2UhT4LyvJqWiyUT+eF+MdfqQ9F7RtQj4bXn9Smw=="],
"@jsr/std__fs": ["@jsr/std__fs@1.0.21", "https://npm.jsr.io/~/11/@jsr/std__fs/1.0.21.tgz", { "dependencies": { "@jsr/std__internal": "^1.0.12", "@jsr/std__path": "^1.1.4" } }, "sha512-k/agrcKGm6KD89ci3AEyRmu3wRWf9JZNliOF4ZUxagTHiySmxjiKU3Lk+d2ksRtwEi7oWlLGS0AVM9Lciwc/xg=="],
"@jsr/std__internal": ["@jsr/std__internal@1.0.12", "https://npm.jsr.io/~/11/@jsr/std__internal/1.0.12.tgz", {}, "sha512-6xReMW9p+paJgqoFRpOE2nogJFvzPfaLHLIlyADYjKMUcwDyjKZxryIbgcU+gxiTygn8yCjld1HoI0ET4/iZeA=="],
"@jsr/std__io": ["@jsr/std__io@0.225.3", "https://npm.jsr.io/~/11/@jsr/std__io/0.225.3.tgz", { "dependencies": { "@jsr/std__bytes": "^1.0.6" } }, "sha512-IDXY253ipW6FV34CJVxO+3ubfvSEEzw9N2W303KnLe9K/Y9+v/ID1dQYf9VsCCOFMpFtCmOLqzIZsRqv6yQnWw=="],
"@jsr/std__path": ["@jsr/std__path@1.1.4", "https://npm.jsr.io/~/11/@jsr/std__path/1.1.4.tgz", { "dependencies": { "@jsr/std__internal": "^1.0.12" } }, "sha512-SK4u9H6NVTfolhPdlvdYXfNFefy1W04AEHWJydryYbk+xqzNiVmr5o7TLJLJFqwHXuwMRhwrn+mcYeUfS0YFaA=="],
"@jsr/std__regexp": ["@jsr/std__regexp@1.0.1", "https://npm.jsr.io/~/11/@jsr/std__regexp/1.0.1.tgz", {}, "sha512-AnGeP//DHpPvhCWjI5dR4o013JhCQioD8yMF8drD7PWb0X4kvmO35hbZi+NZhfSolz4Ts2cpPzJY+DUpi2XE9A=="],
"@jsr/std__semver": ["@jsr/std__semver@1.0.8", "https://npm.jsr.io/~/11/@jsr/std__semver/1.0.8.tgz", {}, "sha512-YhkykPU2Majz66e+rQbP0okYc7kKv+U32aguLPCXZZAL+vEVmBA+khHjPHhLBpWR073gzU3WHqGRgB7a/aXCjg=="],
"@jsr/std__text": ["@jsr/std__text@1.0.17", "https://npm.jsr.io/~/11/@jsr/std__text/1.0.17.tgz", { "dependencies": { "@jsr/std__regexp": "^1.0.1" } }, "sha512-oZsihl1bcTy1Ixzven8rin8kjChj1zDJWqgpS0oSMGCJDzyB365gtIfAvcMmji+M+FcIWo3goDXfHcFYt+k/kg=="],
"@std/encoding": ["@jsr/std__encoding@1.0.10", "https://npm.jsr.io/~/11/@jsr/std__encoding/1.0.10.tgz", {}, "sha512-WK2njnDTyKefroRNk2Ooq7GStp6Y0ccAvr4To+Z/zecRAGe7+OSvH9DbiaHpAKwEi2KQbmpWMOYsdNt+TsdmSw=="],
"@std/log": ["@jsr/std__log@0.224.14", "https://npm.jsr.io/~/11/@jsr/std__log/0.224.14.tgz", { "dependencies": { "@jsr/std__fmt": "^1.0.5", "@jsr/std__fs": "^1.0.11", "@jsr/std__io": "^0.225.2" } }, "sha512-EHT7E0plakyzk/gxMrwqUf3YGCCxN3Is25QrEh7toYA7qwj46R4qY7cIaDEKy8QqI5JHOFHwWXOClcPK6goIoQ=="],
"@std/path": ["@jsr/std__path@1.1.4", "https://npm.jsr.io/~/11/@jsr/std__path/1.1.4.tgz", { "dependencies": { "@jsr/std__internal": "^1.0.12" } }, "sha512-SK4u9H6NVTfolhPdlvdYXfNFefy1W04AEHWJydryYbk+xqzNiVmr5o7TLJLJFqwHXuwMRhwrn+mcYeUfS0YFaA=="],
"@std/yaml": ["@jsr/std__yaml@1.0.10", "https://npm.jsr.io/~/11/@jsr/std__yaml/1.0.10.tgz", {}, "sha512-1WIM023Kvi48pvPE3UO5YcieambLgywUooLhAkkaObIcMB77F/YP2ILdl+vNfik+vElkl9znmuST9AZo8mbCpA=="],
"@stoplight/ordered-object-literal": ["@stoplight/ordered-object-literal@1.0.5", "", {}, "sha512-COTiuCU5bgMUtbIFBuyyh2/yVVzlr5Om0v5utQDgBCuQUOPgU1DwoffkTfg4UBQOvByi5foF4w4T+H9CoRe5wg=="],
"@stoplight/types": ["@stoplight/types@14.1.1", "", { "dependencies": { "@types/json-schema": "^7.0.4", "utility-types": "^3.10.0" } }, "sha512-/kjtr+0t0tjKr+heVfviO9FrU/uGLc+QNX3fHJc19xsCNYqU7lVhaXxDmEID9BZTjG+/r9pK9xP/xU02XGg65g=="],
"@stoplight/yaml": ["@stoplight/yaml@4.3.0", "", { "dependencies": { "@stoplight/ordered-object-literal": "^1.0.5", "@stoplight/types": "^14.1.1", "@stoplight/yaml-ast-parser": "0.0.50", "tslib": "^2.2.0" } }, "sha512-JZlVFE6/dYpP9tQmV0/ADfn32L9uFarHWxfcRhReKUnljz1ZiUM5zpX+PH8h5CJs6lao3TuFqnPm9IJJCEkE2w=="],
"@stoplight/yaml-ast-parser": ["@stoplight/yaml-ast-parser@0.0.50", "", {}, "sha512-Pb6M8TDO9DtSVla9yXSTAxmo9GVEouq5P40DWXdOie69bXogZTkgvopCq+yEvTMA0F6PEvdJmbtTV3ccIp11VQ=="],
"@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.9", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA=="],
"@types/diff": ["@types/diff@5.2.3", "", {}, "sha512-K0Oqlrq3kQMaO2RhfrNQX5trmt+XLyom88zS0u84nnIcLvFnRUMRRHmrGny5GSM+kNO9IZLARsdQHDzkhAgmrQ=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
"@types/node": ["@types/node@22.19.11", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w=="],
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
"@windmill-labs/shared-utils": ["@jsr/windmill-labs__shared-utils@1.0.12", "https://npm.jsr.io/~/11/@jsr/windmill-labs__shared-utils/1.0.12.tgz", {}, "sha512-bJOacyfxxNPwNTzA4AxCB5iGFop0h3mCgs+E9j3ZaJYDo1soblY16CebnQ56EPy/M3V344X/QoOFBORyRo1Mnw=="],
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
"ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
"aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
"balanced-match": ["balanced-match@4.0.3", "", {}, "sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g=="],
"brace-expansion": ["brace-expansion@5.0.2", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw=="],
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
"core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="],
"default-browser": ["default-browser@5.5.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw=="],
"default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="],
"define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="],
"devalue": ["devalue@5.6.3", "", {}, "sha512-nc7XjUU/2Lb+SvEFVGcWLiKkzfw8+qHI7zn8WYXKkLMgfGSHbgCEaR6bJpev8Cm6Rmrb19Gfd/tZvGqx9is3wg=="],
"diff": ["diff@5.2.2", "", {}, "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A=="],
"esbuild": ["esbuild@0.24.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.24.2", "@esbuild/android-arm": "0.24.2", "@esbuild/android-arm64": "0.24.2", "@esbuild/android-x64": "0.24.2", "@esbuild/darwin-arm64": "0.24.2", "@esbuild/darwin-x64": "0.24.2", "@esbuild/freebsd-arm64": "0.24.2", "@esbuild/freebsd-x64": "0.24.2", "@esbuild/linux-arm": "0.24.2", "@esbuild/linux-arm64": "0.24.2", "@esbuild/linux-ia32": "0.24.2", "@esbuild/linux-loong64": "0.24.2", "@esbuild/linux-mips64el": "0.24.2", "@esbuild/linux-ppc64": "0.24.2", "@esbuild/linux-riscv64": "0.24.2", "@esbuild/linux-s390x": "0.24.2", "@esbuild/linux-x64": "0.24.2", "@esbuild/netbsd-arm64": "0.24.2", "@esbuild/netbsd-x64": "0.24.2", "@esbuild/openbsd-arm64": "0.24.2", "@esbuild/openbsd-x64": "0.24.2", "@esbuild/sunos-x64": "0.24.2", "@esbuild/win32-arm64": "0.24.2", "@esbuild/win32-ia32": "0.24.2", "@esbuild/win32-x64": "0.24.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA=="],
"esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="],
"esrap": ["esrap@2.2.3", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-8fOS+GIGCQZl/ZIlhl59htOlms6U8NvX6ZYgYHpRU/b6tVSh3uHkOHZikl3D4cMbYM0JlpBe+p/BkZEi8J9XIQ=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
"get-port": ["get-port@7.1.0", "", {}, "sha512-QB9NKEeDg3xxVwCCwJQ9+xycaz6pBB6iQ76wiWMl1927n0Kir6alPiP+yuiICLLU4jpMe08dXfpebuQppFA2zw=="],
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
"immediate": ["immediate@3.0.6", "", {}, "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="],
"is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="],
"is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="],
"is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="],
"isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="],
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"jszip": ["jszip@3.8.0", "", { "dependencies": { "lie": "~3.3.0", "pako": "~1.0.2", "readable-stream": "~2.3.6", "set-immediate-shim": "~1.0.1" } }, "sha512-cnpQrXvFSLdsR9KR5/x7zdf6c3m8IhZfZzSblFEHSqBaVwD2nvJ4CuCKLyvKvwBgZm08CgfSoiTBQLm5WW9hGw=="],
"lie": ["lie@3.3.0", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="],
"locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="],
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"minimatch": ["minimatch@10.2.2", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw=="],
"open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="],
"pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="],
"process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="],
"readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
"run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="],
"safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
"set-immediate-shim": ["set-immediate-shim@1.0.1", "", {}, "sha512-Li5AOqrZWCVA2n5kryzEmqai6bKSIvpz5oUJHPVj6+dsbD3X1ixtsY5tEnsaNpH3pFAHmG8eIHUrtEtohrg+UQ=="],
"string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
"sudo-prompt": ["sudo-prompt@9.2.1", "", {}, "sha512-Mu7R0g4ig9TUuGSxJavny5Rv0egCEtpZRNMrZaYS1vxkiIxGiGUwoezU3LazIQ+KE04hTrTfNPgxU5gzi7F5Pw=="],
"svelte": ["svelte@5.53.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.6.3", "esm-env": "^1.2.1", "esrap": "^2.2.2", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-WzxFHZhhD23Qzu7JCYdvm1rxvRSzdt9HtHO8TScMBX51bLRFTcJmATVqjqXG+6Ln6hrViGCo9DzwOhAasxwC/w=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"utility-types": ["utility-types@3.11.0", "", {}, "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw=="],
"windmill-parser-wasm-csharp": ["windmill-parser-wasm-csharp@1.510.1", "", {}, "sha512-qm09YmnbeYHLwYn1jUnObVzPhYO9NZKMlIO7nlo7zPJBXqksgG5fK/KCtwGw9rChrnz+DsvM9wP5FhrwRLMtwQ=="],
"windmill-parser-wasm-go": ["windmill-parser-wasm-go@1.510.1", "", {}, "sha512-HOkk6LXK0wrwvkn+zjm3Gxo90HmyL6TYqmLo2yp8fZuppy7GOngT27zwYeBtwONiPyvDKskzoqPQoEfd8VuUsQ=="],
"windmill-parser-wasm-java": ["windmill-parser-wasm-java@1.510.1", "", {}, "sha512-Zle+JZT/ZwUArUVacUudYlS+CaHp2lSnkqD/IhWaRUG+gcv26VbERnrrHPonqXbVMS+eA9ElfXrFM5j0ukaXUw=="],
"windmill-parser-wasm-nu": ["windmill-parser-wasm-nu@1.510.1", "", {}, "sha512-AJLFiUy6af+LpUe7CddDo4+JOmw3c0K/1iOWh8NdTwXcLDj90lL6089mdsVo1apyloLgrTbcuFDzZMXVGBgtCg=="],
"windmill-parser-wasm-php": ["windmill-parser-wasm-php@1.574.1", "", {}, "sha512-COyid6B1RYs+bpzUCInsA4HY/WZkpDLfkQ90+AqU/TVTpzYSbAC2JCbIwy0cRElBvlhI4bQ+9Wg6hSQKMpEkpA=="],
"windmill-parser-wasm-py": ["windmill-parser-wasm-py@1.628.3", "", {}, "sha512-TlluqknZpg8cZ+A3m6JFLPseY2PpKtDsxdj26fAnCUzKPtse8TxQR+n0dwC80rfW5TwdWSulvNGRDgcNuf7CTw=="],
"windmill-parser-wasm-regex": ["windmill-parser-wasm-regex@1.639.0", "", {}, "sha512-qvYM4sYxB6M0xrqwBljS2fWqOMk6rp++60TRltJnzZDzVaWQrKjTGwNMmfepGAIWy1OGVKp0SCVERhe2P+O6tQ=="],
"windmill-parser-wasm-ruby": ["windmill-parser-wasm-ruby@1.526.1", "", {}, "sha512-rMBQA8s21wmL2kA5ztRs/ZgVA3ckxe9/NLjxl3iQPL0CX6DlvfaUH0O+AnhpXXDMyBs1Y1SZIhcnbnvsHZ3R8g=="],
"windmill-parser-wasm-rust": ["windmill-parser-wasm-rust@1.558.1", "", {}, "sha512-21S7lm1KF8zO1187rbq14hzPHII2RdM2+D44MoAh1F6VoaScj+Puq0z5B1O/hwn/95R/a9jBlL2D8jbkXtlD1A=="],
"windmill-parser-wasm-ts": ["windmill-parser-wasm-ts@1.623.1", "", {}, "sha512-FBwi/zXxjhZcCvi04oFdNivazru1ynIqSbafHSArfaaBWesBO3nye9UO/WXUlWZm5a7BExbU+3R/eVJrGaornw=="],
"windmill-parser-wasm-yaml": ["windmill-parser-wasm-yaml@1.593.0", "", {}, "sha512-Gyx4aR2jsJYuDrD3mCNTmz7LWOQQXPw5yKNCC1xRgUOPfjsD/tINAFfsBLwVOSmlQQcFZO+wHm4KtDtXOcnGVw=="],
"windmill-yaml-validator": ["windmill-yaml-validator@1.1.1", "", { "dependencies": { "@stoplight/yaml": "^4.3.0", "ajv": "^8.17.1" } }, "sha512-CVgAwEoBdJhF39q2N012QffhlGPRIyIWd8gj7NnfG+/lMWgH2k5CBLtKIt6cPF8Bxz+6DGC3st1ARSsecDtbTg=="],
"ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="],
"wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="],
"yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="],
"zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="],
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
}
}
+4
View File
@@ -0,0 +1,4 @@
[test]
preload = ["./test/setup.ts"]
timeout = 60000
root = "./test"
-20
View File
@@ -1,20 +0,0 @@
{
"imports": {
"@cliffy/ansi": "jsr:@windmill-labs/cliffy-ansi@^1.0.0-rc.5",
"@cliffy/command": "jsr:@windmill-labs/cliffy-command@^1.0.0-rc.5",
"@cliffy/prompt": "jsr:@windmill-labs/cliffy-prompt@^1.0.0-rc.6",
"@cliffy/table": "jsr:@windmill-labs/cliffy-table@^1.0.0-rc.5",
"@deno/dnt": "jsr:@deno/dnt@^0.41.3",
"@std/encoding": "jsr:@std/encoding@^1.0.10",
"@std/fs": "jsr:@std/fs@^1.0.21",
"@std/io": "jsr:@std/io@^0.224.9",
"@std/log": "jsr:@std/log@^0.224.14",
"@std/net": "jsr:@std/net@^1.0.6",
"@std/path": "jsr:@std/path@^1.1.4",
"@std/streams": "jsr:@std/streams@^1.0.16",
"@std/yaml": "jsr:@std/yaml@^1.0.10",
"@types/diff": "npm:@types/diff@^5.2.3",
"ws": "npm:ws@8.18.0"
},
"nodeModulesDir": "auto"
}
Generated
-1806
View File
File diff suppressed because it is too large Load Diff
-83
View File
@@ -1,83 +0,0 @@
// cliffy
export { Command } from "jsr:@windmill-labs/cliffy-command@1.0.0-rc.5";
export { Table } from "jsr:@windmill-labs/cliffy-table@1.0.0-rc.5";
export { colors } from "jsr:@windmill-labs/cliffy-ansi@1.0.0-rc.5/colors";
export { Secret } from "jsr:@windmill-labs/cliffy-prompt@1.0.0-rc.6/secret";
export { Select } from "jsr:@windmill-labs/cliffy-prompt@1.0.0-rc.6/select";
export { Confirm } from "jsr:@windmill-labs/cliffy-prompt@1.0.0-rc.6/confirm";
export { Input } from "jsr:@windmill-labs/cliffy-prompt@1.0.0-rc.6/input";
export { UpgradeCommand } from "jsr:@windmill-labs/cliffy-command@1.0.0-rc.5/upgrade";
export { NpmProvider } from "jsr:@windmill-labs/cliffy-command@1.0.0-rc.5/upgrade/provider/npm";
export { Provider } from "jsr:@windmill-labs/cliffy-command@1.0.0-rc.5/upgrade";
export { CompletionsCommand } from "jsr:@windmill-labs/cliffy-command@1.0.0-rc.5/completions";
// std
export { ensureDir } from "jsr:@std/fs";
export { SEPARATOR as SEP } from "jsr:@std/path";
export * as path from "jsr:@std/path";
export { encodeHex } from "jsr:@std/encoding@1.0.4";
export { writeAllSync } from "jsr:@std/io/write-all";
export { copy } from "jsr:@std/io/copy";
export { readAll } from "jsr:@std/io/read-all";
export * as log from "jsr:@std/log";
export { stringify as yamlStringify } from "jsr:@std/yaml";
import { parse as yamlParse, ParseOptions } from "jsr:@std/yaml";
export async function yamlParseFile(path: string, options: ParseOptions = {}) {
try {
return yamlParse(await Deno.readTextFile(path), options);
} catch (e) {
throw new Error(`Error parsing yaml ${path}`, { cause: e });
}
}
export function yamlParseContent(
path: string,
content: string,
options: ParseOptions = {},
) {
try {
return yamlParse(content, options);
} catch (e) {
throw new Error(`Error parsing yaml ${path}`, { cause: e });
}
}
// other
export * as Diff from "npm:diff";
export { minimatch } from "npm:minimatch";
export { default as JSZip } from "npm:jszip@3.8.0";
export * as express from "npm:express";
export * as http from "node:http";
export { WebSocket, WebSocketServer } from "npm:ws";
export * as getPort from "npm:get-port@7.1.0";
export * as open from "npm:open";
export * as esMain from "npm:es-main";
export * as windmillUtils from "jsr:@windmill-labs/shared-utils@1.0.12";
// needed for dnt transform
import * as wsTypes from "npm:@types/ws";
import { OpenAPI } from "./gen/index.ts";
export function setClient(token?: string, baseUrl?: string) {
if (baseUrl === undefined) {
baseUrl = getEnv("BASE_INTERNAL_URL") ??
getEnv("BASE_URL") ??
"http://localhost:8000";
}
if (token === undefined) {
token = getEnv("WM_TOKEN") ?? "no_token";
}
OpenAPI.WITH_CREDENTIALS = true;
OpenAPI.TOKEN = token;
OpenAPI.BASE = baseUrl + "/api";
}
const getEnv = (key: string) => {
return Deno.env.get(key);
};
-87
View File
@@ -1,87 +0,0 @@
// ex. scripts/build_npm.ts
import { build, emptyDir } from "jsr:@deno/dnt@0.42.3";
import { VERSION } from "./src/main.ts";
await emptyDir("./npm");
await build({
entryPoints: [
"src/main.ts",
{
kind: "bin",
name: "wmill", // command name
path: "./src/main.ts",
},
],
outDir: "./npm",
test: false, // Disable all tests in npm build since they use Deno-specific APIs
shims: {
// see JS docs for overview and more options
deno: true,
// shims to only use in the tests
customDev: [{
// this is what `timers: "dev"` does internally
package: {
name: "@deno/shim-timers",
version: "~0.1.0",
},
globalNames: ["setTimeout", "setInterval"],
}],
},
scriptModule: false,
filterDiagnostic(diagnostic) {
if (
diagnostic.file?.fileName.includes("node_modules/") ||
diagnostic.file?.fileName.includes("src/deps/") ||
diagnostic.file?.fileName.includes("src/deps.ts") ||
diagnostic.file?.fileName.includes("src/utils/utils.ts")
) {
return false; // ignore all diagnostics in this file
}
// console.log(diagnostic.file?.fileName);
return true;
},
declaration: "separate",
package: {
// package.json properties
name: "windmill-cli",
version: VERSION,
description: "CLI for Windmill",
license: "Apache 2.0",
main: "esm/main.js",
repository: {
type: "git",
url: "git+https://github.com/windmill-labs/windmill.git",
},
bugs: {
url: "https://github.com/windmill-labs/windmill/issues",
},
},
postBuild() {
// steps to run after building and before running the tests
// add shebang to npm/esm/main.js
const dirs = [
"nu",
"ts",
"regex",
"py",
"go",
"php",
"rust",
"yaml",
"csharp",
"java",
"ruby",
// for related places search: ADD_NEW_LANG
];
for (const l of dirs) {
Deno.copyFileSync(
"wasm/" + l + "/windmill_parser_wasm_bg.wasm",
"npm/esm/wasm/" + l + "/windmill_parser_wasm_bg.wasm"
);
}
Deno.copyFileSync("../LICENSE", "npm/LICENSE");
Deno.copyFileSync("README.md", "npm/README.md");
},
});
+2 -2
View File
@@ -6,8 +6,8 @@ rm -rf "${script_dirpath}/gen"
npx --yes @hey-api/openapi-ts@0.53.1 --input "${script_dirpath}/../backend/windmill-api/openapi.yaml" --output "${script_dirpath}/gen" --useOptions --client legacy/fetch --schemas false
cat <<EOF - gen/core/OpenAPI.ts > temp_file && mv temp_file gen/core/OpenAPI.ts
const getEnv = (key: string) => {
return Deno.env.get(key)
const getEnv = (key: string): string | undefined => {
return process.env[key]
};
const baseUrl = getEnv("BASE_INTERNAL_URL") ?? getEnv("BASE_URL") ?? "http://localhost:8000";
+54
View File
@@ -0,0 +1,54 @@
{
"name": "wmill-dev",
"private": true,
"type": "module",
"bin": {
"wmill": "src/main.ts"
},
"scripts": {
"dev": "bun run src/main.ts",
"build": "./build.sh",
"test": "bun test test/",
"check": "bunx tsc --noEmit",
"gen-client": "./gen_wm_client.sh && ./windmill-utils-internal/gen_wm_client.sh"
},
"dependencies": {
"@ayonli/jsext": "^1.9.0",
"@cliffy/ansi": "npm:@jsr/cliffy__ansi@1.0.0",
"@cliffy/command": "npm:@jsr/cliffy__command@1.0.0",
"@cliffy/prompt": "npm:@jsr/cliffy__prompt@1.0.0",
"@cliffy/table": "npm:@jsr/cliffy__table@1.0.0",
"@std/encoding": "npm:@jsr/std__encoding@1.0.10",
"@std/log": "npm:@jsr/std__log@0.224.14",
"@std/path": "npm:@jsr/std__path@1.1.4",
"@std/yaml": "npm:@jsr/std__yaml@1.0.10",
"@windmill-labs/shared-utils": "npm:@jsr/windmill-labs__shared-utils@1.0.12",
"diff": "^5.2.0",
"esbuild": "0.24.2",
"svelte": "^5.45.2",
"get-port": "7.1.0",
"jszip": "3.8.0",
"minimatch": "^10.0.0",
"open": "^10.0.0",
"windmill-parser-wasm-csharp": "*",
"windmill-parser-wasm-go": "*",
"windmill-parser-wasm-java": "*",
"windmill-parser-wasm-nu": "*",
"windmill-parser-wasm-php": "*",
"windmill-parser-wasm-py": "*",
"windmill-parser-wasm-regex": "*",
"windmill-parser-wasm-ruby": "*",
"windmill-parser-wasm-rust": "*",
"windmill-parser-wasm-ts": "*",
"windmill-parser-wasm-yaml": "*",
"windmill-yaml-validator": "1.1.1",
"ws": "8.18.0",
"yaml": "^2.7.0"
},
"devDependencies": {
"@types/diff": "^5.2.3",
"@types/ws": "^8.5.0",
"@types/node": "^22.0.0",
"typescript": "^5.7.0"
}
}
+7 -10
View File
@@ -1,15 +1,12 @@
// deno-lint-ignore-file no-explicit-any
import { requireLogin } from "../../core/auth.ts";
import { resolveWorkspace, validatePath } from "../../core/context.ts";
import {
colors,
Command,
log,
SEP,
Table,
windmillUtils,
yamlParseFile,
} from "../../../deps.ts";
import { Command } from "@cliffy/command";
import { Table } from "@cliffy/table";
import { colors } from "@cliffy/ansi/colors";
import * as log from "@std/log";
import { SEPARATOR as SEP } from "@std/path";
import * as windmillUtils from "@windmill-labs/shared-utils";
import { yamlParseFile } from "../../utils/yaml.ts";
import * as wmill from "../../../gen/services.gen.ts";
import { ListableApp, Policy } from "../../../gen/types.gen.ts";
+11 -13
View File
@@ -1,12 +1,10 @@
// deno-lint-ignore-file no-explicit-any
import path from "node:path";
import {
SEP,
colors,
log,
yamlParseFile,
yamlStringify,
} from "../../../deps.ts";
import { readFile, mkdir } from "node:fs/promises";
import { colors } from "@cliffy/ansi/colors";
import * as log from "@std/log";
import { SEPARATOR as SEP } from "@std/path";
import { yamlParseFile } from "../../utils/yaml.ts";
import { stringify as yamlStringify } from "@std/yaml";
import { GlobalOptions } from "../../types.ts";
import {
checkifMetadataUptodate,
@@ -86,7 +84,7 @@ async function generateAppHash(
}
} catch (error: any) {
// If runnables folder doesn't exist, that's okay
if (error.name !== "NotFound") {
if (error.code !== "ENOENT") {
throw error;
}
}
@@ -351,7 +349,7 @@ async function updateRawAppRunnables(
// Ensure runnables folder exists
try {
await Deno.mkdir(runnablesFolder, { recursive: true });
await mkdir(runnablesFolder, { recursive: true });
} catch {
// Folder may already exist
}
@@ -736,7 +734,7 @@ export async function inferRunnableSchemaFromFile(
);
let content: string;
try {
content = await Deno.readTextFile(fullFilePath);
content = await readFile(fullFilePath, "utf-8");
} catch {
log.warn(colors.yellow(`Could not read file: ${fullFilePath}`));
return undefined;
@@ -786,7 +784,7 @@ export async function generateLocksCommand(
const { generateAppLocksInternal } = await import("./app_metadata.ts");
const { elementsToMap, FSFSElement } = await import("../sync/sync.ts");
const { ignoreF } = await import("../sync/sync.ts");
const { Confirm } = await import("../../../deps.ts");
const { Confirm } = await import("@cliffy/prompt/confirm");
if (appPath == "") {
appPath = undefined;
@@ -813,7 +811,7 @@ export async function generateLocksCommand(
// Generate metadata for all apps
const ignore = await ignoreF(opts);
const elems = await elementsToMap(
await FSFSElement(Deno.cwd(), [], true),
await FSFSElement(process.cwd(), [], true),
(p, isD) => {
return (
ignore(p, isD) ||
+6 -6
View File
@@ -1,10 +1,10 @@
// deno-lint-ignore-file no-explicit-any
import * as fs from "node:fs";
import * as path from "node:path";
import process from "node:process";
import { spawn } from "node:child_process";
import { log, colors } from "../../../deps.ts";
import { windmillUtils } from "../../../deps.ts";
import * as log from "@std/log";
import { colors } from "@cliffy/ansi/colors";
import * as windmillUtils from "@windmill-labs/shared-utils";
export interface BundleOptions {
entryPoint?: string;
outDir?: string;
@@ -66,7 +66,7 @@ function createSveltePlugin(appDir: string): any {
setup(build: any) {
build.onLoad({ filter: /\.svelte$/ }, async (args: any) => {
// Import svelte compiler from the project's node_modules
const svelte = await import("npm:svelte@5.45.2/compiler");
const svelte = await import("svelte/compiler");
// Load the file from the file system
const source = await fs.promises.readFile(args.path, "utf8");
@@ -118,7 +118,7 @@ export async function createFrameworkPlugins(appDir: string): Promise<any[]> {
log.info(colors.blue("🔧 Vue detected, adding vue plugin..."));
throw new Error("Vue plugin not supported yet");
// try {
// const esbuildPluginVue = await import("npm:esbuild-plugin-vue3@0.5.1");
// const esbuildPluginVue = await import("esbuild-plugin-vue3");
// plugins.push(esbuildPluginVue.default());
// } catch (error: any) {
// log.warn(colors.yellow(`Failed to load vue plugin: ${error.message}`));
@@ -164,7 +164,7 @@ export async function createBundle(
options: BundleOptions = {}
): Promise<BundleResult> {
// Dynamically import esbuild
const esbuild = await import("npm:esbuild@0.24.2");
const esbuild = await import("esbuild");
// Detect frameworks to determine default entry point
const frameworks = detectFrameworks(process.cwd());
+112 -135
View File
@@ -1,14 +1,11 @@
// deno-lint-ignore-file no-explicit-any
import {
colors,
Command,
getPort,
log,
open,
SEP,
windmillUtils,
yamlParseFile,
} from "../../../deps.ts";
import { Command } from "@cliffy/command";
import { colors } from "@cliffy/ansi/colors";
import * as log from "@std/log";
import { SEPARATOR as SEP } from "@std/path";
import * as windmillUtils from "@windmill-labs/shared-utils";
import { yamlParseFile } from "../../utils/yaml.ts";
import * as getPort from "get-port";
import * as open from "open";
import { GlobalOptions } from "../../types.ts";
import * as http from "node:http";
import * as fs from "node:fs";
@@ -16,7 +13,8 @@ import * as path from "node:path";
import process from "node:process";
import { Buffer } from "node:buffer";
import { writeFileSync } from "node:fs";
import { WebSocket, WebSocketServer } from "npm:ws";
import { readFile } from "node:fs/promises";
import { WebSocket, WebSocketServer } from "ws";
import {
createFrameworkPlugins,
detectFrameworks,
@@ -336,7 +334,7 @@ async function dev(opts: DevOptions, appFolder?: string) {
if (!fs.existsSync(targetDir)) {
log.error(colors.red(`Error: Directory not found: ${targetDir}`));
Deno.exit(1);
process.exit(1);
}
}
@@ -355,7 +353,7 @@ async function dev(opts: DevOptions, appFolder?: string) {
}' or specify one as argument.`,
),
);
Deno.exit(1);
process.exit(1);
}
// Check for raw_app.yaml in target directory
@@ -369,7 +367,7 @@ async function dev(opts: DevOptions, appFolder?: string) {
} folder containing a raw_app.yaml file.`,
),
);
Deno.exit(1);
process.exit(1);
}
// Resolve workspace and authenticate (from original cwd to find wmill.yaml)
@@ -387,7 +385,7 @@ async function dev(opts: DevOptions, appFolder?: string) {
const appPath = rawApp?.custom_path ?? "u/unknown/newapp";
// Dynamically import esbuild only when the dev command is called
const esbuild = await import("npm:esbuild@0.24.2");
const esbuild = await import("esbuild");
const port = opts.port ??
(await getPort.default({
@@ -410,7 +408,7 @@ async function dev(opts: DevOptions, appFolder?: string) {
`Entry point "${entryPoint}" not found. Please specify a valid entry point with --entry.`,
),
);
Deno.exit(1);
process.exit(1);
}
// Ensure node_modules exists
@@ -525,99 +523,85 @@ async function dev(opts: DevOptions, appFolder?: string) {
// Watch runnables folder for changes
const runnablesPath = path.join(process.cwd(), APP_BACKEND_FOLDER);
let runnablesWatcher: Deno.FsWatcher | undefined;
let runnablesWatcher: fs.FSWatcher | undefined;
if (fs.existsSync(runnablesPath)) {
log.info(
colors.blue(`👁️ Watching runnables folder at: ${runnablesPath}\n`),
);
runnablesWatcher = Deno.watchFs(runnablesPath);
runnablesWatcher = fs.watch(runnablesPath, { recursive: true });
// Per-file debounce timeouts for schema inference (longer debounce for typing)
const schemaInferenceTimeouts: Record<string, ReturnType<typeof setTimeout>> = {};
const SCHEMA_DEBOUNCE_MS = 500; // Wait 500ms after last change before inferring schema
// Handle runnables file changes in the background
(async () => {
try {
for await (const event of runnablesWatcher!) {
// Process each changed path with individual debouncing
for (const changedPath of event.paths) {
const relativePath = path.relative(process.cwd(), changedPath);
const relativeToRunnables = path.relative(
runnablesPath,
changedPath,
);
// Handle runnables file changes via callback
runnablesWatcher.on("change", (_eventType, filename) => {
if (!filename) return;
const fileStr = typeof filename === "string" ? filename : filename.toString();
const changedPath = path.join(runnablesPath, fileStr);
const relativePath = path.relative(process.cwd(), changedPath);
const relativeToRunnables = fileStr;
// Skip non-modify events for schema inference
if (event.kind !== "modify" && event.kind !== "create") {
continue;
}
// Skip lock files
if (changedPath.endsWith(".lock")) {
return;
}
// Skip lock files
if (changedPath.endsWith(".lock")) {
continue;
}
// Log the change event
log.info(
colors.cyan(
`📝 Runnable changed [${_eventType}]: ${relativePath}`,
),
);
// Log the change event
// Debounce schema inference per file (wait for typing to finish)
if (schemaInferenceTimeouts[changedPath]) {
clearTimeout(schemaInferenceTimeouts[changedPath]);
}
schemaInferenceTimeouts[changedPath] = setTimeout(async () => {
delete schemaInferenceTimeouts[changedPath];
try {
log.info(
colors.cyan(
`📝 Inferring schema for: ${relativeToRunnables}`,
),
);
// Infer schema for this runnable (returns schema in memory, doesn't write to file)
const result = await inferRunnableSchemaFromFile(
process.cwd(),
relativeToRunnables,
);
if (result) {
// Store inferred schema in memory
inferredSchemas[result.runnableId] = result.schema;
log.info(
colors.cyan(
`📝 Runnable changed [${event.kind}]: ${relativePath}`,
colors.green(
` Inferred Schemas: ${
JSON.stringify(
inferredSchemas,
null,
2,
)
}`,
),
);
// Debounce schema inference per file (wait for typing to finish)
if (schemaInferenceTimeouts[changedPath]) {
clearTimeout(schemaInferenceTimeouts[changedPath]);
}
schemaInferenceTimeouts[changedPath] = setTimeout(async () => {
delete schemaInferenceTimeouts[changedPath];
try {
log.info(
colors.cyan(
`📝 Inferring schema for: ${relativeToRunnables}`,
),
);
// Infer schema for this runnable (returns schema in memory, doesn't write to file)
const result = await inferRunnableSchemaFromFile(
process.cwd(),
relativeToRunnables,
);
if (result) {
// log.info(colors.green(` Schema: ${JSON.stringify(result.schema, null, 2)}`));
// log.info(colors.green(` Runnable ID: ${result.runnableId}`));
// Store inferred schema in memory
inferredSchemas[result.runnableId] = result.schema;
log.info(
colors.green(
` Inferred Schemas: ${
JSON.stringify(
inferredSchemas,
null,
2,
)
}`,
),
);
// Regenerate wmill.d.ts with updated schema from memory
await genRunnablesTs(inferredSchemas);
}
} catch (error: any) {
log.error(
colors.red(`Error inferring schema: ${error.message}`),
);
}
}, SCHEMA_DEBOUNCE_MS);
// Regenerate wmill.d.ts with updated schema from memory
await genRunnablesTs(inferredSchemas);
}
} catch (error: any) {
log.error(
colors.red(`Error inferring schema: ${error.message}`),
);
}
} catch (error: any) {
if (error.name !== "Interrupted") {
log.error(colors.red(`Error watching runnables: ${error.message}`));
}
}
})();
}, SCHEMA_DEBOUNCE_MS);
});
runnablesWatcher.on("error", (error: Error) => {
log.error(colors.red(`Error watching runnables: ${error.message}`));
});
} else {
log.info(
colors.gray(
@@ -781,7 +765,7 @@ async function dev(opts: DevOptions, appFolder?: string) {
const fileName = path.basename(filePath);
try {
const sqlContent = await Deno.readTextFile(filePath);
const sqlContent = await readFile(filePath, "utf-8");
if (!sqlContent.trim()) {
log.info(colors.gray(`Skipping empty file: ${fileName}`));
@@ -837,7 +821,7 @@ async function dev(opts: DevOptions, appFolder?: string) {
// If there's a current SQL file being shown, send it to the new client
if (currentSqlFile && fs.existsSync(currentSqlFile)) {
try {
const sqlContent = await Deno.readTextFile(currentSqlFile);
const sqlContent = await readFile(currentSqlFile, "utf-8");
const datatable = await getDatatableConfig();
const fileName = path.basename(currentSqlFile);
@@ -1164,7 +1148,7 @@ async function dev(opts: DevOptions, appFolder?: string) {
});
// Watch sql_to_apply folder for SQL migration files
let sqlWatcher: Deno.FsWatcher | undefined;
let sqlWatcher: fs.FSWatcher | undefined;
// Helper to scan for existing SQL files and add them to the queue
async function scanExistingSqlFiles(): Promise<void> {
@@ -1207,53 +1191,46 @@ async function dev(opts: DevOptions, appFolder?: string) {
log.info(
colors.blue(`🗃️ Watching sql_to_apply folder at: ${sqlToApplyPath}\n`),
);
sqlWatcher = Deno.watchFs(sqlToApplyPath);
sqlWatcher = fs.watch(sqlToApplyPath, { recursive: true });
// Debounce timeout for SQL file changes
const sqlDebounceTimeouts: Record<string, ReturnType<typeof setTimeout>> = {};
const SQL_DEBOUNCE_MS = 300;
// Handle SQL file changes in the background
(async () => {
try {
for await (const event of sqlWatcher!) {
for (const changedPath of event.paths) {
// Only handle .sql files
if (!changedPath.endsWith(".sql")) {
continue;
}
// Handle SQL file changes via callback
sqlWatcher.on("change", (_eventType, filename) => {
if (!filename) return;
const fileStr = typeof filename === "string" ? filename : filename.toString();
const changedPath = path.join(sqlToApplyPath, fileStr);
// Only handle modify and create events
if (event.kind !== "modify" && event.kind !== "create") {
continue;
}
const fileName = path.basename(changedPath);
// Debounce per file
if (sqlDebounceTimeouts[changedPath]) {
clearTimeout(sqlDebounceTimeouts[changedPath]);
}
sqlDebounceTimeouts[changedPath] = setTimeout(async () => {
delete sqlDebounceTimeouts[changedPath];
log.info(colors.cyan(`📋 SQL file detected: ${fileName}`));
// Add to queue and process
queueSqlFile(changedPath);
await processNextSqlFile();
}, SQL_DEBOUNCE_MS);
}
}
} catch (error: any) {
if (error.name !== "Interrupted") {
log.error(
colors.red(`Error watching sql_to_apply: ${error.message}`),
);
}
// Only handle .sql files
if (!changedPath.endsWith(".sql")) {
return;
}
})();
const fileName = path.basename(changedPath);
// Debounce per file
if (sqlDebounceTimeouts[changedPath]) {
clearTimeout(sqlDebounceTimeouts[changedPath]);
}
sqlDebounceTimeouts[changedPath] = setTimeout(async () => {
delete sqlDebounceTimeouts[changedPath];
log.info(colors.cyan(`📋 SQL file detected: ${fileName}`));
// Add to queue and process
queueSqlFile(changedPath);
await processNextSqlFile();
}, SQL_DEBOUNCE_MS);
});
sqlWatcher.on("error", (error: Error) => {
log.error(
colors.red(`Error watching sql_to_apply: ${error.message}`),
);
});
// Scan for existing SQL files after a delay (to let WebSocket clients connect)
setTimeout(() => {
+15 -10
View File
@@ -1,12 +1,18 @@
import { colors, Command, log, yamlParseFile } from "../../../deps.ts";
import * as fs from "node:fs";
import { writeFile } from "node:fs/promises";
import path from "node:path";
import process from "node:process";
import { Command } from "@cliffy/command";
import { colors } from "@cliffy/ansi/colors";
import * as log from "@std/log";
import { yamlParseFile } from "../../utils/yaml.ts";
import { GlobalOptions } from "../../types.ts";
import { resolveWorkspace } from "../../core/context.ts";
import { requireLogin } from "../../core/auth.ts";
import * as wmill from "../../../gen/services.gen.ts";
import { DataTableSchema } from "../../../gen/types.gen.ts";
import { generateAgentsDocumentation } from "../sync/sync.ts";
import path from "node:path";
import * as fs from "node:fs";
import {
getFolderSuffix,
hasFolderSuffix,
@@ -192,14 +198,14 @@ export async function regenerateAgentDocs(
// Generate and write AGENTS.md
const agentsContent = generateAgentsDocumentation(localData);
await Deno.writeTextFile(path.join(targetDir, "AGENTS.md"), agentsContent);
await writeFile(path.join(targetDir, "AGENTS.md"), agentsContent, "utf-8");
// Generate and write CLAUDE.md referencing AGENTS.md
await Deno.writeTextFile(path.join(targetDir, "CLAUDE.md"), `Instructions are in @AGENTS.md\n`);
await writeFile(path.join(targetDir, "CLAUDE.md"), `Instructions are in @AGENTS.md\n`, "utf-8");
// Generate and write DATATABLES.md
const datatablesContent = generateDatatablesMarkdown(schemas, localData);
await Deno.writeTextFile(path.join(targetDir, "DATATABLES.md"), datatablesContent);
await writeFile(path.join(targetDir, "DATATABLES.md"), datatablesContent, "utf-8");
if (!silent) {
log.info(colors.green(`✓ Generated AGENTS.md, CLAUDE.md, and DATATABLES.md`));
@@ -229,7 +235,7 @@ async function generateAgents(
appFolder?: string
) {
// Resolve the app folder
const cwd = Deno.cwd();
const cwd = process.cwd();
let targetDir = cwd;
if (appFolder) {
@@ -252,7 +258,7 @@ async function generateAgents(
)
);
log.info(colors.gray("Usage: wmill app generate-agents [app_folder]"));
Deno.exit(1);
process.exit(1);
}
}
@@ -262,7 +268,7 @@ async function generateAgents(
log.error(
colors.red(`Error: raw_app.yaml not found in ${targetDir}`)
);
Deno.exit(1);
process.exit(1);
}
// Resolve workspace and authenticate
@@ -272,7 +278,6 @@ async function generateAgents(
await regenerateAgentDocs(workspace.workspaceId, targetDir);
}
// deno-lint-ignore no-explicit-any
const command = new Command()
.description("regenerate AGENTS.md and DATATABLES.md from remote workspace")
.arguments("[app_folder:string]")
+5 -3
View File
@@ -1,8 +1,10 @@
// deno-lint-ignore-file no-explicit-any
import * as fs from "node:fs";
import * as path from "node:path";
import process from "node:process";
import { colors, Command, log, yamlParseFile } from "../../../deps.ts";
import { Command } from "@cliffy/command";
import { colors } from "@cliffy/ansi/colors";
import * as log from "@std/log";
import { yamlParseFile } from "../../utils/yaml.ts";
import { GlobalOptions } from "../../types.ts";
import { createBundle } from "./bundle.ts";
import { APP_BACKEND_FOLDER } from "./app_metadata.ts";
@@ -224,7 +226,7 @@ async function lint(opts: LintOptions, appFolder?: string) {
log.info(colors.red(` - ${error}`));
});
log.info(colors.red("\n❌ Lint failed\n"));
Deno.exit(1);
process.exit(1);
}
log.info(colors.green("\n✅ All checks passed\n"));
+29 -32
View File
@@ -1,13 +1,11 @@
import {
colors,
Command,
Confirm,
ensureDir,
Input,
log,
Select,
yamlStringify,
} from "../../../deps.ts";
import { stat, writeFile, mkdir } from "node:fs/promises";
import { Command } from "@cliffy/command";
import { colors } from "@cliffy/ansi/colors";
import { Confirm } from "@cliffy/prompt/confirm";
import { Input } from "@cliffy/prompt/input";
import { Select } from "@cliffy/prompt/select";
import * as log from "@std/log";
import { stringify as yamlStringify } from "@std/yaml";
import { GlobalOptions } from "../../types.ts";
import { generateAgentsDocumentation, generateDatatablesDocumentation, yamlOptions } from "../sync/sync.ts";
import { resolveWorkspace } from "../../core/context.ts";
@@ -480,11 +478,11 @@ CREATE SCHEMA IF NOT EXISTS ${schemaName};
// Create the directory structure - preserve full path (e.g., f/foobar/x/y becomes f/foobar/x/y.raw_app)
const folderName = buildFolderPath(appPath, "raw_app");
const appDir = path.join(Deno.cwd(), folderName);
const appDir = path.join(process.cwd(), folderName);
// Check if directory already exists
try {
await Deno.stat(appDir);
await stat(appDir);
const overwrite = await Confirm.prompt({
message: `Directory '${folderName}' already exists. Overwrite?`,
default: false,
@@ -497,9 +495,9 @@ CREATE SCHEMA IF NOT EXISTS ${schemaName};
// Directory doesn't exist, which is good
}
await ensureDir(appDir);
await ensureDir(path.join(appDir, "backend"));
await ensureDir(path.join(appDir, "sql_to_apply"));
await mkdir(appDir, { recursive: true });
await mkdir(path.join(appDir, "backend"), { recursive: true });
await mkdir(path.join(appDir, "sql_to_apply"), { recursive: true });
// Create raw_app.yaml with data configuration
const rawAppConfig: Record<string, unknown> = {
@@ -511,15 +509,15 @@ CREATE SCHEMA IF NOT EXISTS ${schemaName};
rawAppConfig.data = dataConfig;
}
await Deno.writeTextFile(
await writeFile(
path.join(appDir, "raw_app.yaml"),
yamlStringify(rawAppConfig, yamlOptions)
yamlStringify(rawAppConfig, yamlOptions), "utf-8"
);
// Create template files
for (const [filePath, content] of Object.entries(template.files)) {
const fullPath = path.join(appDir, filePath.slice(1)); // Remove leading slash
await Deno.writeTextFile(fullPath, content.trim() + "\n");
await writeFile(fullPath, content.trim() + "\n", "utf-8");
}
// Create AGENTS.md - main documentation for AI agents
@@ -532,22 +530,22 @@ CREATE SCHEMA IF NOT EXISTS ${schemaName};
: undefined;
const agentsContent = generateAgentsDocumentation(dataForDocs);
await Deno.writeTextFile(
await writeFile(
path.join(appDir, "AGENTS.md"),
agentsContent
agentsContent, "utf-8"
);
// Create CLAUDE.md referencing AGENTS.md
await Deno.writeTextFile(
await writeFile(
path.join(appDir, "CLAUDE.md"),
`Instructions are in @AGENTS.md\n`
`Instructions are in @AGENTS.md\n`, "utf-8"
);
// Create DATATABLES.md with the configured data
const datatablesContent = generateDatatablesDocumentation(dataForDocs);
await Deno.writeTextFile(
await writeFile(
path.join(appDir, "DATATABLES.md"),
datatablesContent
datatablesContent, "utf-8"
);
// Create example backend runnable
@@ -555,20 +553,20 @@ CREATE SCHEMA IF NOT EXISTS ${schemaName};
type: "inline",
path: undefined,
};
await Deno.writeTextFile(
await writeFile(
path.join(appDir, "backend", "a.yaml"),
yamlStringify(exampleRunnable, yamlOptions)
yamlStringify(exampleRunnable, yamlOptions), "utf-8"
);
await Deno.writeTextFile(
await writeFile(
path.join(appDir, "backend", "a.ts"),
`export async function main(x: number): Promise<string> {
return \`Hello from backend! x = \${x}\`;
}
`
`, "utf-8"
);
// Create sql_to_apply README
await Deno.writeTextFile(
await writeFile(
path.join(appDir, "sql_to_apply", "README.md"),
`# SQL Migrations Folder
@@ -601,9 +599,9 @@ This folder is for SQL migration files that will be applied to datatables during
// Create schema creation SQL file if a new schema was requested
if (createSchemaSQL && schemaName) {
await Deno.writeTextFile(
await writeFile(
path.join(appDir, "sql_to_apply", `000_create_schema_${schemaName}.sql`),
createSchemaSQL
createSchemaSQL, "utf-8"
);
}
@@ -666,7 +664,6 @@ This folder is for SQL migration files that will be applied to datatables during
log.info(colors.gray(" 4. wmill sync push (to deploy when ready)"));
}
// deno-lint-ignore no-explicit-any
const command = new Command()
.description("create a new raw app from a template")
.action(newApp as any);
+22 -21
View File
@@ -1,17 +1,15 @@
// deno-lint-ignore-file no-explicit-any
import { requireLogin } from "../../core/auth.ts";
import { resolveWorkspace, validatePath } from "../../core/context.ts";
import {
colors,
log,
SEP,
windmillUtils,
yamlParseFile,
yamlStringify,
} from "../../../deps.ts";
import { colors } from "@cliffy/ansi/colors";
import * as log from "@std/log";
import { SEPARATOR as SEP } from "@std/path";
import * as windmillUtils from "@windmill-labs/shared-utils";
import { yamlParseFile } from "../../utils/yaml.ts";
import { stringify as yamlStringify } from "@std/yaml";
import * as wmill from "../../../gen/services.gen.ts";
import { Policy } from "../../../gen/types.gen.ts";
import path from "node:path";
import { readFile, readdir } from "node:fs/promises";
import { GlobalOptions, isSuperset } from "../../types.ts";
import { deepEqual } from "../../utils/utils.ts";
@@ -67,8 +65,8 @@ async function findRunnableContentFile(
// Check if this is a recognized extension
if (EXTENSION_TO_LANGUAGE[ext]) {
try {
const content = await Deno.readTextFile(
path.join(backendPath, fileName),
const content = await readFile(
path.join(backendPath, fileName), "utf-8",
);
return { ext, content };
} catch {
@@ -130,8 +128,9 @@ export async function loadRunnablesFromBackend(
try {
// First, collect all files in the backend folder
const allFiles: string[] = [];
for await (const entry of Deno.readDir(backendPath)) {
if (entry.isFile) {
const _entries = await readdir(backendPath, { withFileTypes: true });
for (const entry of _entries) {
if (entry.isFile()) {
allFiles.push(entry.name);
}
}
@@ -165,8 +164,9 @@ export async function loadRunnablesFromBackend(
// Try to load lock file
let lock: string | undefined;
try {
lock = await Deno.readTextFile(
lock = await readFile(
path.join(backendPath, `${runnableId}.lock`),
"utf-8",
);
} catch {
// No lock file, that's fine
@@ -226,8 +226,8 @@ export async function loadRunnablesFromBackend(
// Try to load lock file
let lock: string | undefined;
try {
lock = await Deno.readTextFile(
path.join(backendPath, `${runnableId}.lock`),
lock = await readFile(
path.join(backendPath, `${runnableId}.lock`), "utf-8",
);
} catch {
// No lock file, that's fine
@@ -245,7 +245,7 @@ export async function loadRunnablesFromBackend(
}
}
} catch (error: any) {
if (error.name !== "NotFound") {
if (error.code !== "ENOENT") {
throw error;
}
}
@@ -291,11 +291,12 @@ async function collectAppFiles(
const files: Record<string, string> = {};
async function readDirRecursive(dir: string, basePath: string = "/") {
for await (const entry of Deno.readDir(dir)) {
const dirEntries = await readdir(dir, { withFileTypes: true });
for (const entry of dirEntries) {
const fullPath = dir + entry.name;
const relativePath = basePath + entry.name;
if (entry.isDirectory) {
if (entry.isDirectory()) {
// Skip the runnables, node_modules, and sql_to_apply subfolders
if (
entry.name === APP_BACKEND_FOLDER ||
@@ -307,7 +308,7 @@ async function collectAppFiles(
continue;
}
await readDirRecursive(fullPath + SEP, relativePath + "/");
} else if (entry.isFile) {
} else if (entry.isFile()) {
// Skip generated/metadata files that shouldn't be part of the app
if (
entry.name === "raw_app.yaml" ||
@@ -318,7 +319,7 @@ async function collectAppFiles(
) {
continue;
}
const content = await Deno.readTextFile(fullPath);
const content = await readFile(fullPath, "utf-8");
files[relativePath] = content;
}
}
@@ -1,8 +1,9 @@
// deno-lint-ignore-file no-explicit-any
import { requireLogin } from "../../core/auth.ts";
import { resolveWorkspace } from "../../core/context.ts";
import { GlobalOptions } from "../../types.ts";
import { colors, Command, log } from "../../../deps.ts";
import { colors } from "@cliffy/ansi/colors";
import { Command } from "@cliffy/command";
import * as log from "@std/log";
import * as wmill from "../../../gen/services.gen.ts";
import fs from "node:fs";
import { workspaceDependenciesPathToLanguageAndFilename } from "../../utils/metadata.ts";
+41 -37
View File
@@ -1,15 +1,14 @@
import {
Command,
SEP,
WebSocketServer,
express,
getPort,
http,
log,
open,
WebSocket,
yamlParseFile,
} from "../../../deps.ts";
import { Command } from "@cliffy/command";
import * as log from "@std/log";
import { SEPARATOR as SEP } from "@std/path";
import { yamlParseFile } from "../../utils/yaml.ts";
import { WebSocket, WebSocketServer } from "ws";
import * as getPort from "get-port";
import * as http from "node:http";
import * as open from "open";
import { readFile, realpath } from "node:fs/promises";
import { watch } from "node:fs";
import { getTypeStrFromPath, GlobalOptions } from "../../types.ts";
import { ignoreF } from "../sync/sync.ts";
import { requireLogin } from "../../core/auth.ts";
@@ -40,25 +39,30 @@ async function dev(opts: GlobalOptions & SyncOptions) {
const conf = await readConfigFile();
let currentLastEdit: LastEditScript | LastEditFlow | undefined = undefined;
const watcher = Deno.watchFs(".");
const base = await Deno.realPath(".");
const fsWatcher = watch(".", { recursive: true });
const base = await realpath(".");
opts = await mergeConfigWithConfigFile(opts);
const ignore = await ignoreF(opts);
const changesTimeouts: Record<string, number> = {};
async function watchChanges() {
for await (const event of watcher) {
// console.log(">>>> event", event);
const key = event.paths.join(",");
if (changesTimeouts[key]) {
clearTimeout(changesTimeouts[key]);
}
// @ts-ignore
changesTimeouts[key] = setTimeout(async () => {
delete changesTimeouts[key];
await loadPaths(event.paths);
}, 100);
}
const changesTimeouts: Record<string, ReturnType<typeof setTimeout>> = {};
function watchChanges() {
return new Promise<void>((_resolve, _reject) => {
fsWatcher.on("change", (_eventType, filename) => {
if (!filename) return;
const filePath = typeof filename === "string" ? filename : filename.toString();
const key = filePath;
if (changesTimeouts[key]) {
clearTimeout(changesTimeouts[key]);
}
changesTimeouts[key] = setTimeout(async () => {
delete changesTimeouts[key];
await loadPaths([filePath]);
}, 100);
});
fsWatcher.on("error", (err) => {
_reject(err);
});
});
}
const flowFolderSuffix = getFolderSuffixWithSep("flow");
@@ -72,8 +76,9 @@ async function dev(opts: GlobalOptions & SyncOptions) {
if (paths.length == 0) {
return;
}
const cpath = (await Deno.realPath(paths[0])).replace(base + SEP, "");
if (!ignore(cpath, false)) {
const nativePath = (await realpath(paths[0])).replace(base + SEP, "");
const cpath = nativePath.replaceAll("\\", "/");
if (!ignore(nativePath, false)) {
const typ = getTypeStrFromPath(cpath);
log.info("Detected change in " + cpath + " (" + typ + ")");
if (typ == "flow") {
@@ -83,13 +88,11 @@ async function dev(opts: GlobalOptions & SyncOptions) {
)) as FlowFile;
await replaceInlineScripts(
localFlow.value.modules,
async (path: string) => await Deno.readTextFile(localPath + path),
async (path: string) => await readFile(localPath + path, "utf-8"),
log,
localPath,
SEP,
undefined,
// (path: string, newPath: string) => Deno.renameSync(path, newPath),
// (path: string) => Deno.removeSync(path),
);
currentLastEdit = {
type: "flow",
@@ -99,7 +102,7 @@ async function dev(opts: GlobalOptions & SyncOptions) {
log.info("Updated " + localPath);
broadcastChanges(currentLastEdit);
} else if (typ == "script") {
const content = await Deno.readTextFile(cpath);
const content = await readFile(cpath, "utf-8");
const splitted = cpath.split(".");
const wmPath = splitted[0];
const lang = inferContentTypeFromFilePath(cpath, conf.defaultTs);
@@ -150,8 +153,10 @@ async function dev(opts: GlobalOptions & SyncOptions) {
}
async function startApp() {
const app = express.default();
const server = http.createServer(app);
const server = http.createServer((_req, res) => {
res.writeHead(200);
res.end();
});
const wss = new WebSocketServer({ server });
// WebSocket server event listeners
@@ -224,7 +229,6 @@ const command = new Command()
"--includes <pattern...:string>",
"Filter paths givena glob pattern or path"
)
// deno-lint-ignore no-explicit-any
.action(dev as any);
export default command;
+15 -8
View File
@@ -1,8 +1,15 @@
// deno-lint-ignore-file no-explicit-any
import { GlobalOptions, isSuperset } from "../../types.ts";
import { Confirm, SEP, log, yamlStringify } from "../../../deps.ts";
import { colors, Command, Table, yamlParseFile } from "../../../deps.ts";
import { colors } from "@cliffy/ansi/colors";
import { Command } from "@cliffy/command";
import { Confirm } from "@cliffy/prompt/confirm";
import { Table } from "@cliffy/table";
import * as log from "@std/log";
import { SEPARATOR as SEP } from "@std/path";
import { stringify as yamlStringify } from "@std/yaml";
import { yamlParseFile } from "../../utils/yaml.ts";
import * as wmill from "../../../gen/services.gen.ts";
import { readFile } from "node:fs/promises";
import { mkdirSync, writeFileSync } from "node:fs";
import { requireLogin } from "../../core/auth.ts";
import { resolveWorkspace, validatePath } from "../../core/context.ts";
@@ -51,7 +58,7 @@ export async function pushFlow(
await replaceInlineScripts(
localFlow.value.modules,
async (path: string) => await Deno.readTextFile(localPath + path),
async (path: string) => await readFile(localPath + path, "utf-8"),
log,
localPath,
SEP
@@ -225,7 +232,7 @@ async function preview(
// Replace inline scripts with their actual content
await replaceInlineScripts(
localFlow.value.modules,
async (path: string) => await Deno.readTextFile(flowPath + path),
async (path: string) => await readFile(flowPath + path, "utf-8"),
log,
flowPath,
SEP
@@ -286,7 +293,7 @@ async function generateLocks(
const ignore = await ignoreF(opts);
const elems = Object.keys(
await elementsToMap(
await FSFSElement(Deno.cwd(), [], true),
await FSFSElement(process.cwd(), [], true),
(p, isD) => {
return (
ignore(p, isD) ||
@@ -348,7 +355,7 @@ export function bootstrap(
}
const flowDirFullPath = `${flowPath}.flow`;
Deno.mkdirSync(flowDirFullPath, { recursive: false });
mkdirSync(flowDirFullPath, { recursive: false });
const newFlowDefinition = defaultFlowDefinition();
if (opts.summary !== undefined) {
@@ -363,7 +370,7 @@ export function bootstrap(
);
const flowYamlPath = `${flowDirFullPath}/flow.yaml`;
Deno.writeTextFile(flowYamlPath, newFlowDefinitionYaml, { createNew: true });
writeFileSync(flowYamlPath, newFlowDefinitionYaml, { flag: "wx", encoding: "utf-8" });
}
const command = new Command()
+11 -14
View File
@@ -1,11 +1,10 @@
import {
SEP,
colors,
log,
path,
yamlParseFile,
yamlStringify,
} from "../../../deps.ts";
import { colors } from "@cliffy/ansi/colors";
import * as log from "@std/log";
import * as path from "@std/path";
import { SEPARATOR as SEP } from "@std/path";
import { stringify as yamlStringify } from "@std/yaml";
import { yamlParseFile } from "../../utils/yaml.ts";
import { readFile } from "node:fs/promises";
import { GlobalOptions } from "../../types.ts";
import {
readLockfile,
@@ -37,7 +36,7 @@ async function generateFlowHash(
folder: string,
defaultTs: "bun" | "deno" | undefined
) {
const elems = await FSFSElement(path.join(Deno.cwd(), folder), [], true);
const elems = await FSFSElement(path.join(process.cwd(), folder), [], true);
const hashes: Record<string, string> = {};
for await (const f of elems.getChildren()) {
if (exts.some((e) => f.path.endsWith(e))) {
@@ -124,13 +123,11 @@ export async function generateFlowLockInternal(
log.info(`Recomputing locks of ${changedScripts.join(", ")} in ${folder}`);
await replaceInlineScripts(
flowValue.value.modules,
async (path: string) => await Deno.readTextFile(folder + SEP + path),
async (path: string) => await readFile(folder + SEP + path, "utf-8"),
log,
folder + SEP!,
SEP,
changedScripts
// (path: string, newPath: string) => Deno.renameSync(path, newPath),
// (path: string) => Deno.removeSync(path)
);
//removeChangedLocks
@@ -148,12 +145,12 @@ export async function generateFlowLockInternal(
opts.defaultTs
);
inlineScripts.forEach((s) => {
writeIfChanged(Deno.cwd() + SEP + folder + SEP + s.path, s.content);
writeIfChanged(process.cwd() + SEP + folder + SEP + s.path, s.content);
});
// Overwrite `flow.yaml` with the new lockfile references
writeIfChanged(
Deno.cwd() + SEP + folder + SEP + "flow.yaml",
process.cwd() + SEP + folder + SEP + "flow.yaml",
yamlStringify(flowValue as Record<string, any>)
);
}
+9 -4
View File
@@ -1,5 +1,10 @@
// deno-lint-ignore-file no-explicit-any
import { colors, Command, log, SEP, Table } from "../../../deps.ts";
import { stat } from "node:fs/promises";
import { colors } from "@cliffy/ansi/colors";
import { Command } from "@cliffy/command";
import { Table } from "@cliffy/table";
import * as log from "@std/log";
import { SEPARATOR as SEP } from "@std/path";
import * as wmill from "../../../gen/services.gen.ts";
import { requireLogin } from "../../core/auth.ts";
@@ -103,8 +108,8 @@ async function push(opts: GlobalOptions, filePath: string, remotePath: string) {
return;
}
const fstat = await Deno.stat(filePath);
if (!fstat.isFile) {
const fstat = await stat(filePath);
if (!fstat.isFile()) {
throw new Error("file path must refer to a file.");
}
@@ -1,4 +1,4 @@
import { Command } from "../../../deps.ts";
import { Command } from "@cliffy/command";
import { pullGitSyncSettings } from "./pull.ts";
import { pushGitSyncSettings } from "./push.ts";
@@ -1,4 +1,7 @@
import { colors, Confirm } from "../../../deps.ts";
import process from "node:process";
import { colors } from "@cliffy/ansi/colors";
import { Confirm } from "@cliffy/prompt/confirm";
import * as wmill from "../../../gen/services.gen.ts";
import { GitSyncRepository } from "./types.ts";
@@ -24,7 +27,7 @@ export async function handleLegacyRepositoryMigration(
const workspaceIncludePath = gitSyncSettings.include_path;
const workspaceIncludeType = gitSyncSettings.include_type;
if (Deno.stdout.isTerminal() && !opts.yes) {
if (!!process.stdout.isTTY && !opts.yes) {
// Interactive mode - show migration prompt
console.log(colors.yellow('\n⚠️ Legacy git-sync settings detected!'));
console.log(`\nRepository "${selectedRepo.git_repo_resource_path}" has legacy settings format.`);
@@ -139,6 +142,6 @@ export async function handleLegacyRepositoryMigration(
console.error('3. Push local settings to override backend settings:');
console.error(' wmill gitsync-settings push\n');
}
Deno.exit(1);
process.exit(1);
}
}
+9 -6
View File
@@ -1,4 +1,7 @@
import { colors, log, yamlStringify } from "../../../deps.ts";
import { writeFile } from "node:fs/promises";
import { colors } from "@cliffy/ansi/colors";
import * as log from "@std/log";
import { stringify as yamlStringify } from "@std/yaml";
import { GlobalOptions } from "../../types.ts";
import { requireLogin } from "../../core/auth.ts";
import { resolveWorkspace } from "../../core/context.ts";
@@ -173,7 +176,7 @@ export async function pullGitSyncSettings(
}
// Write the new configuration
await Deno.writeTextFile("wmill.yaml", yamlStringify(updatedConfig));
await writeFile("wmill.yaml", yamlStringify(updatedConfig), "utf-8");
if (opts.jsonOutput) {
console.log(
@@ -286,7 +289,7 @@ export async function pullGitSyncSettings(
);
const hasConflict = !deepEqual(gitSyncBackend, gitSyncCurrent);
if (hasConflict && !opts.yes && Deno.stdin.isTerminal()) {
if (hasConflict && !opts.yes && !!process.stdin.isTTY) {
// Show the diff first
log.info("Changes that would be applied locally:");
const changes = generateChanges(effectiveCurrentSettings, backendSyncOptions);
@@ -295,7 +298,7 @@ export async function pullGitSyncSettings(
}
// Interactive mode - ask user
const { Select } = await import("../../../deps.ts");
const { Select } = await import("@cliffy/prompt/select");
const choice = await Select.prompt({
message: "Settings conflict detected. How would you like to proceed?",
options: [
@@ -369,7 +372,7 @@ export async function pullGitSyncSettings(
}
// Write updated configuration
await Deno.writeTextFile("wmill.yaml", yamlStringify(updatedConfig));
await writeFile("wmill.yaml", yamlStringify(updatedConfig), "utf-8");
if (opts.jsonOutput) {
console.log(
@@ -446,7 +449,7 @@ export async function pullGitSyncSettings(
}
// Write updated configuration
await Deno.writeTextFile("wmill.yaml", yamlStringify(updatedConfig));
await writeFile("wmill.yaml", yamlStringify(updatedConfig), "utf-8");
if (opts.jsonOutput) {
console.log(
+8 -4
View File
@@ -1,4 +1,8 @@
import { colors, log, Confirm } from "../../../deps.ts";
import process from "node:process";
import { colors } from "@cliffy/ansi/colors";
import * as log from "@std/log";
import { Confirm } from "@cliffy/prompt/confirm";
import { GlobalOptions } from "../../types.ts";
import { requireLogin } from "../../core/auth.ts";
import { resolveWorkspace } from "../../core/context.ts";
@@ -34,7 +38,7 @@ export async function pushGitSyncSettings(
} catch (error) {
if (error instanceof Error && error.message.includes("overrides")) {
log.error(error.message);
Deno.exit(1);
process.exit(1);
}
throw error;
}
@@ -51,7 +55,7 @@ export async function pushGitSyncSettings(
"No wmill.yaml file found. Please run 'wmill init' first to create the configuration file.",
),
);
Deno.exit(1);
process.exit(1);
}
// Read local configuration
@@ -247,7 +251,7 @@ export async function pushGitSyncSettings(
}
// Ask for confirmation unless --yes is passed or not in TTY
if (!opts.yes && Deno.stdin.isTerminal()) {
if (!opts.yes && !!process.stdin.isTTY) {
const confirmed = await Confirm.prompt({
message: `Do you want to apply these changes to the remote?`,
default: true,
+2 -1
View File
@@ -1,4 +1,5 @@
import { colors, log } from "../../../deps.ts";
import { colors } from "@cliffy/ansi/colors";
import * as log from "@std/log";
import { deepEqual, selectRepository } from "../../utils/utils.ts";
import { SyncOptions, getEffectiveSettings, DEFAULT_SYNC_OPTIONS } from "../../core/conf.ts";
import { GitSyncRepository, GIT_SYNC_FIELDS } from "./types.ts";
+2 -2
View File
@@ -1,5 +1,5 @@
// deno-lint-ignore-file no-explicit-any
import { Command, log } from "../../../deps.ts";
import { Command } from "@cliffy/command";
import * as log from "@std/log";
import * as wmill from "../../../gen/services.gen.ts";
import { requireLogin } from "../../core/auth.ts";
+24 -19
View File
@@ -1,4 +1,9 @@
import { colors, Command, log, yamlStringify, Confirm } from "../../../deps.ts";
import { stat, writeFile, rm, mkdir } from "node:fs/promises";
import { colors } from "@cliffy/ansi/colors";
import { Command } from "@cliffy/command";
import { Confirm } from "@cliffy/prompt/confirm";
import * as log from "@std/log";
import { stringify as yamlStringify } from "@std/yaml";
import { GlobalOptions } from "../../types.ts";
import { readLockfile } from "../../utils/metadata.ts";
import { getActiveWorkspaceOrFallback } from "../workspace/workspace.ts";
@@ -36,7 +41,7 @@ export interface InitOptions {
* Bootstrap a windmill project with a wmill.yaml file
*/
async function initAction(opts: InitOptions) {
if (await Deno.stat("wmill.yaml").catch(() => null)) {
if (await stat("wmill.yaml").catch(() => null)) {
log.error(colors.red("wmill.yaml already exists"));
} else {
// Import DEFAULT_SYNC_OPTIONS from conf.ts
@@ -63,7 +68,7 @@ async function initAction(opts: InitOptions) {
}
initialConfig.nonDottedPaths = true;
await Deno.writeTextFile("wmill.yaml", yamlStringify(initialConfig));
await writeFile("wmill.yaml", yamlStringify(initialConfig), "utf-8");
log.info(colors.green("wmill.yaml created with default settings"));
// Create lock file
@@ -80,12 +85,12 @@ async function initAction(opts: InitOptions) {
const shouldBind = opts.bindProfile === true;
const shouldPrompt =
opts.bindProfile === undefined &&
Deno.stdin.isTerminal() &&
!!process.stdin.isTTY &&
!opts.useDefault;
const shouldSkip =
opts.bindProfile != true &&
(opts.useDefault || !Deno.stdin.isTerminal());
(opts.useDefault || !!!process.stdin.isTTY);
if (!shouldSkip) {
// Show workspace info if we're binding or prompting
@@ -132,7 +137,7 @@ async function initAction(opts: InitOptions) {
currentConfig.gitBranches[currentBranch].workspaceId =
activeWorkspace.workspaceId;
await Deno.writeTextFile("wmill.yaml", yamlStringify(currentConfig));
await writeFile("wmill.yaml", yamlStringify(currentConfig), "utf-8");
log.info(
colors.green(
@@ -183,7 +188,7 @@ async function initAction(opts: InitOptions) {
if (useBackendSettings === undefined) {
// Interactive prompt
const { Select } = await import("../../../deps.ts");
const { Select } = await import("@cliffy/prompt/select");
const choice = await Select.prompt({
message:
"Git-sync settings found on backend. What would you like to do?",
@@ -206,13 +211,13 @@ async function initAction(opts: InitOptions) {
if (choice === "cancel") {
// Clean up the created files
try {
await Deno.remove("wmill.yaml");
await Deno.remove("wmill-lock.yaml");
await rm("wmill.yaml");
await rm("wmill-lock.yaml");
} catch (e) {
// Ignore cleanup errors
}
log.info("Init cancelled");
Deno.exit(0);
process.exit(0);
}
useBackendSettings = choice === "backend";
@@ -256,32 +261,32 @@ async function initAction(opts: InitOptions) {
).join("\n");
// Create AGENTS.md file with minimal instructions
if (!(await Deno.stat("AGENTS.md").catch(() => null))) {
await Deno.writeTextFile(
if (!(await stat("AGENTS.md").catch(() => null))) {
await writeFile(
"AGENTS.md",
generateAgentsMdContent(skillsReference)
generateAgentsMdContent(skillsReference), "utf-8"
);
log.info(colors.green("Created AGENTS.md"));
}
// Create CLAUDE.md file, referencing AGENTS.md
if (!(await Deno.stat("CLAUDE.md").catch(() => null))) {
await Deno.writeTextFile(
if (!(await stat("CLAUDE.md").catch(() => null))) {
await writeFile(
"CLAUDE.md",
`Instructions are in @AGENTS.md
`
`, "utf-8"
);
log.info(colors.green("Created CLAUDE.md"));
}
// Create .claude/skills/ directory and skill files
try {
await Deno.mkdir(".claude/skills", { recursive: true });
await mkdir(".claude/skills", { recursive: true });
await Promise.all(
SKILLS.map(async (skill) => {
const skillDir = `.claude/skills/${skill.name}`;
await Deno.mkdir(skillDir, { recursive: true });
await mkdir(skillDir, { recursive: true });
let skillContent = SKILL_CONTENT[skill.name];
if (skillContent) {
@@ -304,7 +309,7 @@ async function initAction(opts: InitOptions) {
}
}
await Deno.writeTextFile(`${skillDir}/SKILL.md`, skillContent);
await writeFile(`${skillDir}/SKILL.md`, skillContent, "utf-8");
}
})
);
+37 -41
View File
@@ -1,16 +1,17 @@
import {
Command,
Confirm,
path,
Select,
setClient,
Table,
yamlParseFile,
yamlStringify,
} from "../../../deps.ts";
import { readFile, writeFile, readdir, mkdir, rm, stat } from "node:fs/promises";
import { appendFile } from "node:fs/promises";
import { colors } from "@cliffy/ansi/colors";
import { Command } from "@cliffy/command";
import { Confirm } from "@cliffy/prompt/confirm";
import { Input } from "@cliffy/prompt/input";
import { Select } from "@cliffy/prompt/select";
import { Table } from "@cliffy/table";
import * as log from "@std/log";
import * as path from "@std/path";
import { stringify as yamlStringify } from "@std/yaml";
import { setClient } from "../../core/client.ts";
import { yamlParseFile } from "../../utils/yaml.ts";
import * as wmill from "../../../gen/services.gen.ts";
import { colors, Input, log } from "../../../deps.ts";
import { loginInteractive } from "../../core/login.ts";
import {
getActiveInstanceFilePath,
@@ -51,7 +52,7 @@ export interface Instance {
export async function allInstances(): Promise<Instance[]> {
try {
const file = await getInstancesConfigFilePath();
const txt = await Deno.readTextFile(file);
const txt = await readFile(file, "utf-8");
return txt
.split("\n")
.map((line) => {
@@ -118,26 +119,19 @@ export async function addInstance(
async function appendInstance(instance: Instance) {
instance.remote = new URL(instance.remote).toString(); // add trailing slash in all cases!
await removeInstance(instance.name);
const file = await Deno.open(await getInstancesConfigFilePath(), {
append: true,
write: true,
read: true,
create: true,
});
await file.write(new TextEncoder().encode(JSON.stringify(instance) + "\n"));
file.close();
const filePath = await getInstancesConfigFilePath();
await appendFile(filePath, JSON.stringify(instance) + "\n", "utf-8");
}
async function removeInstance(name: string) {
const orgWorkspaces = await allInstances();
await Deno.writeTextFile(
await writeFile(
await getInstancesConfigFilePath(),
orgWorkspaces
.filter((x) => x.name !== name)
.map((x) => JSON.stringify(x))
.join("\n") + "\n",
.join("\n") + "\n", "utf-8",
);
}
@@ -289,7 +283,7 @@ async function instancePull(opts: InstanceSyncOptions) {
const totalChanges = uChanges + sChanges + cChanges + gChanges;
const rootDir = Deno.cwd();
const rootDir = process.cwd();
if (totalChanges > 0) {
let confirm = true;
@@ -308,7 +302,7 @@ async function instancePull(opts: InstanceSyncOptions) {
if (confirm) {
if (uChanges > 0) {
if (opts.folderPerInstance && opts.prefixSettings) {
await Deno.mkdir(path.join(rootDir, opts.prefix), {
await mkdir(path.join(rootDir, opts.prefix), {
recursive: true,
});
}
@@ -348,10 +342,10 @@ async function instancePull(opts: InstanceSyncOptions) {
const workspaceName = opts?.folderPerInstance
? instance.prefix + "/" + remoteWorkspace.id
: instance.prefix + "_" + remoteWorkspace.id;
await Deno.mkdir(path.join(rootDir, workspaceName), {
await mkdir(path.join(rootDir, workspaceName), {
recursive: true,
});
await Deno.chdir(path.join(rootDir, workspaceName));
process.chdir(path.join(rootDir, workspaceName));
await addWorkspace(
{
remote: instance.remote,
@@ -397,7 +391,7 @@ async function instancePull(opts: InstanceSyncOptions) {
if (confirmDelete) {
for (const workspace of localWorkspacesToDelete) {
await removeWorkspace(workspace.id, false, {});
await Deno.remove(path.join(rootDir, workspace.dir), {
await rm(path.join(rootDir, workspace.dir), {
recursive: true,
});
}
@@ -467,7 +461,7 @@ async function instancePush(opts: InstanceSyncOptions) {
if (opts.includeWorkspaces) {
instances = await allInstances();
const rootDir = Deno.cwd();
const rootDir = process.cwd();
let localPrefix;
if (opts.prefix) {
@@ -506,7 +500,7 @@ async function instancePush(opts: InstanceSyncOptions) {
for (const localWorkspace of localWorkspaces) {
log.info("\nPushing workspace " + localWorkspace.id);
try {
await Deno.chdir(path.join(rootDir, localWorkspace.dir));
process.chdir(path.join(rootDir, localWorkspace.dir));
} catch (_) {
throw new Error(
"Workspace folder not found, are you in the right directory?",
@@ -515,7 +509,7 @@ async function instancePush(opts: InstanceSyncOptions) {
try {
const workspaceSettings = (await yamlParseFile(
path.join(Deno.cwd(), "settings.yaml"),
path.join(process.cwd(), "settings.yaml"),
)) as SimplifiedSettings;
await workspaceSetup(
{
@@ -586,12 +580,13 @@ async function getLocalWorkspaces(
) {
const localWorkspaces: { dir: string; id: string }[] = [];
if (!(await Deno.stat(localPrefix).catch(() => null))) {
await Deno.mkdir(localPrefix);
if (!(await stat(localPrefix).catch(() => null))) {
await mkdir(localPrefix);
}
if (folderPerInstance) {
for await (const dir of Deno.readDir(rootDir + "/" + localPrefix)) {
if (dir.isDirectory) {
const prefixEntries = await readdir(rootDir + "/" + localPrefix, { withFileTypes: true });
for (const dir of prefixEntries) {
if (dir.isDirectory()) {
const dirName = dir.name;
localWorkspaces.push({
dir: localPrefix + "/" + dirName,
@@ -600,7 +595,8 @@ async function getLocalWorkspaces(
}
}
} else {
for await (const dir of Deno.readDir(rootDir)) {
const rootEntries = await readdir(rootDir, { withFileTypes: true });
for (const dir of rootEntries) {
const dirName = dir.name;
if (dirName.startsWith(localPrefix + "_")) {
localWorkspaces.push({
@@ -631,9 +627,9 @@ async function switchI(opts: {}, instanceName: string) {
return;
}
await Deno.writeTextFile(
await writeFile(
await getActiveInstanceFilePath(),
instanceName,
instanceName, "utf-8",
);
log.info(colors.green.underline(`Switched to instance ${instanceName}`));
@@ -646,7 +642,7 @@ export async function getActiveInstance(opts: {
return opts.instance;
}
try {
return await Deno.readTextFile(await getActiveInstanceFilePath());
return await readFile(await getActiveInstanceFilePath(), "utf-8");
} catch {
return undefined;
}
@@ -657,7 +653,7 @@ async function getConfig(opts: InstanceSyncOptions & { outputFile?: string }) {
const config = await wmill.getInstanceConfig();
const yaml = yamlStringify(config as Record<string, unknown>);
if (opts.outputFile) {
await Deno.writeTextFile(opts.outputFile, yaml);
await writeFile(opts.outputFile, yaml, "utf-8");
log.info(colors.green(`Instance config written to ${opts.outputFile}`));
} else {
console.log(yaml);
+4 -2
View File
@@ -1,8 +1,10 @@
// deno-lint-ignore-file no-explicit-any
import { GlobalOptions } from "../../types.ts";
import { requireLogin } from "../../core/auth.ts";
import { resolveWorkspace } from "../../core/context.ts";
import { colors, Command, Confirm, log } from "../../../deps.ts";
import { Command } from "@cliffy/command";
import { colors } from "@cliffy/ansi/colors";
import { Confirm } from "@cliffy/prompt/confirm";
import * as log from "@std/log";
import { mergeConfigWithConfigFile } from "../../core/conf.ts";
import * as fs from "node:fs/promises";
import * as wmill from "../../../gen/services.gen.ts";
+29 -27
View File
@@ -1,11 +1,12 @@
import {
colors,
Command,
log,
path,
SEP,
yamlParseFile,
} from "../../../deps.ts";
import { stat, readdir } from "node:fs/promises";
import process from "node:process";
import { colors } from "@cliffy/ansi/colors";
import { Command } from "@cliffy/command";
import * as log from "@std/log";
import * as path from "@std/path";
import { SEPARATOR as SEP } from "@std/path";
import { yamlParseFile } from "../../utils/yaml.ts";
import { GlobalOptions } from "../../types.ts";
import { mergeConfigWithConfigFile } from "../../core/conf.ts";
import {
@@ -17,7 +18,7 @@ import {
getValidationTargetFromFilename,
type ValidationTarget,
WindmillYamlValidator,
} from "npm:windmill-yaml-validator@1.1.1";
} from "windmill-yaml-validator";
import {
inferContentTypeFromFilePath,
languageNeedsLock,
@@ -159,8 +160,8 @@ async function checkInlineFile(
): Promise<boolean> {
const fullPath = path.join(baseDir, relativePath.trim());
try {
const stat = await Deno.stat(fullPath);
return stat.size > 0;
const s = await stat(fullPath);
return s.size > 0;
} catch {
return false;
}
@@ -272,8 +273,9 @@ async function checkRawAppRunnables(
const issues: FileIssue[] = [];
const allFiles: string[] = [];
for await (const entry of Deno.readDir(backendDir)) {
if (entry.isFile) {
const entries = await readdir(backendDir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isFile()) {
allFiles.push(entry.name);
}
}
@@ -316,8 +318,8 @@ async function checkRawAppRunnables(
const lockFile = path.join(backendDir, `${runnableId}.lock`);
let hasLock = false;
try {
const stat = await Deno.stat(lockFile);
hasLock = stat.size > 0;
const s = await stat(lockFile);
hasLock = s.size > 0;
} catch {
// No lock file
}
@@ -375,8 +377,8 @@ async function checkRawAppRunnables(
const lockFile = path.join(backendDir, `${runnableId}.lock`);
let hasLock = false;
try {
const stat = await Deno.stat(lockFile);
hasLock = stat.size > 0;
const s = await stat(lockFile);
hasLock = s.size > 0;
} catch {
// No lock file
}
@@ -404,10 +406,10 @@ export async function checkMissingLocks(
opts: GlobalOptions & { defaultTs?: "bun" | "deno" },
directory?: string,
): Promise<FileIssue[]> {
const initialCwd = Deno.cwd();
const initialCwd = process.cwd();
const targetDirectory = directory
? path.resolve(initialCwd, directory)
: Deno.cwd();
: process.cwd();
const { ...syncOpts } = opts;
const mergedOpts = await mergeConfigWithConfigFile(syncOpts);
@@ -483,7 +485,7 @@ export async function checkMissingLocks(
let language: ScriptLanguage | null = null;
for (const ext of exts) {
try {
await Deno.stat(path.join(targetDirectory, basePath + ext));
await stat(path.join(targetDirectory, basePath + ext));
language = inferContentTypeFromFilePath(basePath + ext, defaultTs);
break;
} catch {
@@ -582,7 +584,7 @@ export async function checkMissingLocks(
const backendDir = path.join(rawAppDir, "backend");
try {
await Deno.stat(backendDir);
await stat(backendDir);
} catch {
continue; // No backend folder
}
@@ -606,20 +608,20 @@ export async function runLint(
opts: LintOptions,
directory?: string,
): Promise<LintReport> {
const initialCwd = Deno.cwd();
const initialCwd = process.cwd();
const explicitTargetDirectory = directory
? path.resolve(initialCwd, directory)
: undefined;
const { json: _json, ...syncOpts } = opts;
const mergedOpts = await mergeConfigWithConfigFile(syncOpts);
const targetDirectory = explicitTargetDirectory ?? Deno.cwd();
const targetDirectory = explicitTargetDirectory ?? process.cwd();
const stats = await Deno.stat(targetDirectory).catch(() => null);
const stats = await stat(targetDirectory).catch(() => null);
if (!stats) {
throw new Error(`Directory not found: ${targetDirectory}`);
}
if (!stats.isDirectory) {
if (!stats.isDirectory()) {
throw new Error(`Path is not a directory: ${targetDirectory}`);
}
@@ -745,7 +747,7 @@ async function lint(opts: LintOptions, directory?: string) {
const report = await runLint(opts, directory);
printReport(report, !!opts.json);
if (report.exitCode !== 0) {
Deno.exit(report.exitCode);
process.exit(report.exitCode);
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
@@ -764,7 +766,7 @@ async function lint(opts: LintOptions, directory?: string) {
} else {
log.error(colors.red(`${message}`));
}
Deno.exit(1);
process.exit(1);
}
}
+3 -2
View File
@@ -1,5 +1,6 @@
import { Command, Table } from "../../../deps.ts";
import { log } from "../../../deps.ts";
import { Command } from "@cliffy/command";
import { Table } from "@cliffy/table";
import * as log from "@std/log";
import * as wmill from "../../../gen/services.gen.ts";
import { pickInstance } from "../instance/instance.ts";
@@ -1,6 +1,5 @@
// deno-lint-ignore-file no-explicit-any
import { writeFileSync } from "node:fs";
import { stat } from "node:fs/promises";
import path from "node:path";
import process from "node:process";
@@ -12,7 +11,10 @@ import {
} from "../../types.ts";
import { requireLogin } from "../../core/auth.ts";
import { resolveWorkspace } from "../../core/context.ts";
import { colors, Command, log, Table } from "../../../deps.ts";
import { colors } from "@cliffy/ansi/colors";
import { Command } from "@cliffy/command";
import { Table } from "@cliffy/table";
import * as log from "@std/log";
import * as wmill from "../../../gen/services.gen.ts";
import { ResourceType } from "../../../gen/types.gen.ts";
import { compileResourceTypeToTsType } from "../../utils/resource_types.ts";
@@ -65,8 +67,8 @@ export async function pushResourceType(
type PushOptions = GlobalOptions;
async function push(opts: PushOptions, filePath: string, name: string) {
const fstat = await Deno.stat(filePath);
if (!fstat.isFile) {
const fstat = await stat(filePath);
if (!fstat.isFile()) {
throw new Error("file path must refer to a file.");
}
const workspace = await resolveWorkspace(opts);
+9 -4
View File
@@ -1,4 +1,5 @@
// deno-lint-ignore-file no-explicit-any
import { stat } from "node:fs/promises";
import {
GlobalOptions,
isSuperset,
@@ -7,7 +8,11 @@ import {
} from "../../types.ts";
import { requireLogin } from "../../core/auth.ts";
import { resolveWorkspace, validatePath } from "../../core/context.ts";
import { colors, Command, log, SEP, Table } from "../../../deps.ts";
import { Command } from "@cliffy/command";
import { Table } from "@cliffy/table";
import { colors } from "@cliffy/ansi/colors";
import * as log from "@std/log";
import { SEPARATOR as SEP } from "@std/path";
import * as wmill from "../../../gen/services.gen.ts";
import { Resource } from "../../../gen/types.gen.ts";
import { readInlinePathSync } from "../../utils/utils.ts";
@@ -109,8 +114,8 @@ async function push(opts: PushOptions, filePath: string, remotePath: string) {
return;
}
const fstat = await Deno.stat(filePath);
if (!fstat.isFile) {
const fstat = await stat(filePath);
if (!fstat.isFile()) {
throw new Error("file path must refer to a file.");
}
+9 -4
View File
@@ -1,5 +1,10 @@
// deno-lint-ignore-file no-explicit-any
import { colors, Command, log, SEP, Table } from "../../../deps.ts";
import { stat } from "node:fs/promises";
import { Command } from "@cliffy/command";
import { Table } from "@cliffy/table";
import { colors } from "@cliffy/ansi/colors";
import * as log from "@std/log";
import { SEPARATOR as SEP } from "@std/path";
import { requireLogin } from "../../core/auth.ts";
import { resolveWorkspace, validatePath } from "../../core/context.ts";
import * as wmill from "../../../gen/services.gen.ts";
@@ -114,8 +119,8 @@ async function push(opts: GlobalOptions, filePath: string, remotePath: string) {
return;
}
const fstat = await Deno.stat(filePath);
if (!fstat.isFile) {
const fstat = await stat(filePath);
if (!fstat.isFile()) {
throw new Error("file path must refer to a file.");
}
+43 -54
View File
@@ -1,18 +1,15 @@
// deno-lint-ignore-file no-explicit-any
import { GlobalOptions } from "../../types.ts";
import { requireLogin } from "../../core/auth.ts";
import { resolveWorkspace, validatePath } from "../../core/context.ts";
import {
colors,
Command,
Confirm,
log,
readAll,
SEP,
Table,
writeAllSync,
yamlStringify,
} from "../../../deps.ts";
import { readFile, writeFile, stat } from "node:fs/promises";
import { Buffer } from "node:buffer";
import { colors } from "@cliffy/ansi/colors";
import { Command } from "@cliffy/command";
import { Confirm } from "@cliffy/prompt/confirm";
import { Table } from "@cliffy/table";
import * as log from "@std/log";
import { SEPARATOR as SEP } from "@std/path";
import { stringify as yamlStringify } from "@std/yaml";
import { deepEqual } from "../../utils/utils.ts";
import * as wmill from "../../../gen/services.gen.ts";
import * as specificItems from "../../core/specific_items.ts";
@@ -51,7 +48,7 @@ import {
} from "../../core/conf.ts";
import { SyncCodebase, listSyncCodebases } from "../../utils/codebase.ts";
import fs from "node:fs";
import { type Tarball } from "npm:@ayonli/jsext/archive";
import { type Tarball } from "@ayonli/jsext/archive";
import { execSync } from "node:child_process";
import { NewScript, Script } from "../../../gen/types.gen.ts";
@@ -106,8 +103,8 @@ async function push(opts: PushOptions, filePath: string) {
return;
}
const fstat = await Deno.stat(filePath);
if (!fstat.isFile) {
const fstat = await stat(filePath);
if (!fstat.isFile()) {
throw new Error("file path must refer to a file.");
}
@@ -159,9 +156,9 @@ export async function findResourceFile(path: string) {
const validCandidates = (
await Promise.all(
candidates.map((x) => {
return Deno.stat(x)
return stat(x)
.catch(() => undefined)
.then((x) => x?.isFile)
.then((x) => x?.isFile())
.then((e) => {
return { path: x, file: e };
});
@@ -261,7 +258,7 @@ export async function handleFile(
}).toString();
log.info("Custom bundler executed for " + path);
} else {
const esbuild = await import("npm:esbuild@0.24.2");
const esbuild = await import("esbuild");
log.info(`Started bundling ${path} ...`);
const startTime = performance.now();
@@ -295,7 +292,7 @@ export async function handleFile(
);
}
if (outputFiles.length > 1) {
const archiveNpm = await import("npm:@ayonli/jsext/archive");
const archiveNpm = await import("@ayonli/jsext/archive");
log.info(
`Found multiple output files for ${path}, creating a tarball... ${outputFiles
.map((file) => file.path)
@@ -314,7 +311,7 @@ export async function handleFile(
continue;
}
log.info(`Adding file: ${file.path.substring(1)}`);
// deno-lint-ignore no-explicit-any
const fil = new File([file.contents as any], file.path.substring(1));
tarball.append(fil);
}
@@ -327,7 +324,7 @@ export async function handleFile(
bundleContent = tarball;
} else {
if (Array.isArray(codebase.assets) && codebase.assets.length > 0) {
const archiveNpm = await import("npm:@ayonli/jsext/archive");
const archiveNpm = await import("@ayonli/jsext/archive");
log.info(
`Using the following asset configuration for ${path}: ${JSON.stringify(
codebase.assets
@@ -384,7 +381,7 @@ export async function handleFile(
} catch {
log.debug(`Script ${remotePath} does not exist on remote`);
}
const content = await Deno.readTextFile(path);
const content = await readFile(path, "utf-8");
if (opts?.skipScriptsMetadata) {
// if (codebase) {
@@ -392,17 +389,6 @@ export async function handleFile(
// await updateScriptSchema(content, language, typed, path);
// if (typedBefore != typed.schema) {
// log.info(`Updated metadata for bundle ${path}`);
// showDiff(
// yamlStringify(typedBefore, yamlOptions),
// yamlStringify(typed.schema, yamlOptions)
// );
// await Deno.writeTextFile(
// remotePath + ".script.yaml",
// yamlStringify(typed as Record<string, any>, yamlOptions)
// );
// }
// }
// else {
typed = structuredClone(remote);
// }
}
@@ -544,7 +530,7 @@ async function streamToBlob(stream: ReadableStream<Uint8Array>): Promise<Blob> {
chunks.push(value);
}
// deno-lint-ignore no-explicit-any
const blob = new Blob(chunks as any);
return blob;
}
@@ -611,9 +597,9 @@ export async function findContentFile(filePath: string) {
const validCandidates = (
await Promise.all(
candidates.map((x) => {
return Deno.stat(x)
return stat(x)
.catch(() => undefined)
.then((x) => x?.isFile)
.then((x) => x?.isFile())
.then((e) => {
return { path: x, file: e };
});
@@ -778,10 +764,12 @@ export async function resolve(input: string): Promise<Record<string, any>> {
}
if (input == "@-") {
input = new TextDecoder().decode(await readAll(Deno.stdin));
const chunks: Buffer[] = [];
for await (const chunk of process.stdin) chunks.push(chunk);
input = new TextDecoder().decode(Buffer.concat(chunks));
}
if (input[0] == "@") {
input = await Deno.readTextFile(input.substring(1));
input = await readFile(input.substring(1), "utf-8");
}
try {
return JSON.parse(input);
@@ -830,7 +818,7 @@ async function run(
break;
} catch {
new Promise((resolve, _) => setTimeout(() => resolve(undefined), 100));
await new Promise((resolve) => setTimeout(resolve, 100));
}
}
}
@@ -872,6 +860,7 @@ export async function track_job(workspace: string, id: string) {
log.info("failed to get job updated. skipping log streaming.");
break;
}
await new Promise((resolve) => setTimeout(resolve, 500));
continue;
}
@@ -881,7 +870,7 @@ export async function track_job(workspace: string, id: string) {
}
if (updates.new_logs) {
writeAllSync(Deno.stdout, new TextEncoder().encode(updates.new_logs));
process.stdout.write(updates.new_logs);
logOffset += updates.new_logs.length;
}
@@ -951,8 +940,8 @@ async function bootstrap(
const scriptMetadataFileFullPath = scriptPath + ".script.yaml";
try {
await Deno.stat(scriptCodeFileFullPath);
await Deno.stat(scriptMetadataFileFullPath);
await stat(scriptCodeFileFullPath);
await stat(scriptMetadataFileFullPath);
throw new Error("File already exists in repository");
} catch {
// file does not exist, we can continue
@@ -971,14 +960,14 @@ async function bootstrap(
yamlOptions
);
await Deno.writeTextFile(scriptCodeFileFullPath, scriptInitialCode, {
createNew: true,
await writeFile(scriptCodeFileFullPath, scriptInitialCode, {
flag: 'wx', encoding: 'utf-8',
});
await Deno.writeTextFile(
await writeFile(
scriptMetadataFileFullPath,
scriptInitialMetadataYaml,
{
createNew: true,
flag: 'wx', encoding: 'utf-8',
}
);
}
@@ -1028,7 +1017,7 @@ async function generateMetadata(
// TODO: test this as well.
const ignore = await ignoreF(opts);
const elems = await elementsToMap(
await FSFSElement(Deno.cwd(), codebases, false),
await FSFSElement(process.cwd(), codebases, false),
(p, isD) => {
return (
(!isD && !exts.some((ext) => p.endsWith(ext))) ||
@@ -1107,8 +1096,8 @@ async function preview(
return;
}
const fstat = await Deno.stat(filePath);
if (!fstat.isFile) {
const fstat = await stat(filePath);
if (!fstat.isFile()) {
throw new Error("file path must refer to a file.");
}
@@ -1120,7 +1109,7 @@ async function preview(
const codebases = await listSyncCodebases(opts);
const language = inferContentTypeFromFilePath(filePath, opts?.defaultTs);
const content = await Deno.readTextFile(filePath);
const content = await readFile(filePath, "utf-8");
const input = opts.data ? await resolve(opts.data) : {};
// Check if this is a codebase script
@@ -1139,7 +1128,7 @@ async function preview(
maxBuffer: 1024 * 1024 * 50,
}).toString();
} else {
const esbuild = await import("npm:esbuild@0.24.2");
const esbuild = await import("esbuild");
if (!opts.silent) {
log.info(`Bundling ${filePath} for preview...`);
@@ -1166,7 +1155,7 @@ async function preview(
// Handle multiple output files (create tarball)
if (out.outputFiles.length > 1) {
const archiveNpm = await import("npm:@ayonli/jsext/archive");
const archiveNpm = await import("@ayonli/jsext/archive");
if (!opts.silent) {
log.info(`Creating tarball for multiple output files...`);
}
@@ -1177,7 +1166,7 @@ async function preview(
tarball.append(new File([mainContent], "main.js", { type: "text/plain" }));
for (const file of out.outputFiles) {
if (file.path == "/" + mainPath) continue;
// deno-lint-ignore no-explicit-any
const fil = new File([file.contents as any], file.path.substring(1));
tarball.append(fil);
}
@@ -1185,7 +1174,7 @@ async function preview(
isTar = true;
} else if (Array.isArray(codebase.assets) && codebase.assets.length > 0) {
// Handle assets
const archiveNpm = await import("npm:@ayonli/jsext/archive");
const archiveNpm = await import("@ayonli/jsext/archive");
if (!opts.silent) {
log.info(`Adding assets to tarball...`);
}
+2 -1
View File
@@ -1,4 +1,5 @@
import { colors, log } from "../../../deps.ts";
import { colors } from "@cliffy/ansi/colors";
import * as log from "@std/log";
let GLOBAL_VERSIONS: {
remoteMajor: number | undefined;
+4 -2
View File
@@ -1,6 +1,8 @@
// deno-lint-ignore-file no-explicit-any
import { GlobalOptions } from "../../types.ts";
import { colors, Command, JSZip, log } from "../../../deps.ts";
import { colors } from "@cliffy/ansi/colors";
import { Command } from "@cliffy/command";
import * as log from "@std/log";
import JSZip from "jszip";
import { Workspace } from "../workspace/workspace.ts";
import { getHeaders } from "../../utils/utils.ts";
+3 -2
View File
@@ -1,5 +1,6 @@
// deno-lint-ignore-file no-explicit-any
import { colors, Command, log } from "../../../deps.ts";
import { colors } from "@cliffy/ansi/colors";
import { Command } from "@cliffy/command";
import * as log from "@std/log";
import { GlobalOptions } from "../../types.ts";
function stub(_opts: GlobalOptions, _dir?: string) {
+53 -76
View File
@@ -1,18 +1,16 @@
import { requireLogin } from "../../core/auth.ts";
import { fetchVersion, resolveWorkspace } from "../../core/context.ts";
import {
colors,
Command,
Confirm,
ensureDir,
JSZip,
log,
minimatch,
path,
SEP,
yamlParseContent,
yamlStringify,
} from "../../../deps.ts";
import { readFile, writeFile, readdir, stat, rm, copyFile, mkdir } from "node:fs/promises";
import { colors } from "@cliffy/ansi/colors";
import { Command } from "@cliffy/command";
import { Confirm } from "@cliffy/prompt/confirm";
import * as log from "@std/log";
import * as path from "@std/path";
import { SEPARATOR as SEP } from "@std/path";
import { stringify as yamlStringify } from "@std/yaml";
import JSZip from "jszip";
import { minimatch } from "minimatch";
import { yamlParseContent } from "../../utils/yaml.ts";
import * as wmill from "../../../gen/services.gen.ts";
import {
@@ -178,7 +176,7 @@ async function addCodebaseDigestIfRelevant(
let isTs = true;
const replacedPath = path.replace(".script.yaml", ".ts");
try {
await Deno.stat(replacedPath);
await stat(replacedPath);
} catch {
isTs = false;
}
@@ -231,10 +229,11 @@ export async function FSFSElement(
async *getChildren(): AsyncIterable<DynFSElement> {
if (!isDir) return [];
try {
for await (const e of Deno.readDir(localP)) {
const entries = await readdir(localP, { withFileTypes: true });
for (const e of entries) {
yield _internal_element(
path.join(localP, e.name),
e.isDirectory,
e.isDirectory(),
codebases,
);
}
@@ -242,11 +241,8 @@ export async function FSFSElement(
log.warn(`Error reading dir: ${localP}, ${e}`);
}
},
// async getContentBytes(): Promise<Uint8Array> {
// return await Deno.readFile(localP);
// },
async getContentText(): Promise<string> {
const content = await Deno.readTextFile(localP);
const content = await readFile(localP, "utf-8");
const itemPath = localP.substring(p.length + 1);
const r = await addCodebaseDigestIfRelevant(
itemPath,
@@ -258,7 +254,7 @@ export async function FSFSElement(
},
};
}
return _internal_element(p, (await Deno.stat(p)).isDirectory, codebases);
return _internal_element(p, (await stat(p)).isDirectory(), codebases);
}
function prioritizeName(name: string): string {
@@ -573,7 +569,6 @@ function ZipFSElement(
isDirectory: false,
path: path.join(finalPath, s.path),
async *getChildren() {},
// deno-lint-ignore require-await
async getContentText() {
return s.content;
},
@@ -584,7 +579,6 @@ function ZipFSElement(
isDirectory: false,
path: path.join(finalPath, "flow.yaml"),
async *getChildren() {},
// deno-lint-ignore require-await
async getContentText() {
return yamlStringify(flow, yamlOptions);
},
@@ -618,7 +612,6 @@ function ZipFSElement(
isDirectory: false,
path: path.join(finalPath, s.path),
async *getChildren() {},
// deno-lint-ignore require-await
async getContentText() {
return s.content;
},
@@ -633,7 +626,6 @@ function ZipFSElement(
isDirectory: false,
path: path.join(finalPath, "app.yaml"),
async *getChildren() {},
// deno-lint-ignore require-await
async getContentText() {
return yamlStringify(app, yamlOptions);
},
@@ -690,8 +682,7 @@ function ZipFSElement(
isDirectory: false,
path: path.join(finalPath, filePath.substring(1)),
async *getChildren() {},
// deno-lint-ignore require-await
async getContentText() {
async getContentText() {
if (typeof content !== "string") {
throw new Error(
`Content of raw app file ${filePath} is not a string`,
@@ -712,7 +703,6 @@ function ZipFSElement(
isDirectory: false,
path: path.join(finalPath, APP_BACKEND_FOLDER, s.path),
async *getChildren() {},
// deno-lint-ignore require-await
async getContentText() {
return s.content;
},
@@ -792,7 +782,6 @@ function ZipFSElement(
`${runnableId}.yaml`,
),
async *getChildren() {},
// deno-lint-ignore require-await
async getContentText() {
return yamlStringify(simplifiedRunnable, yamlOptions);
},
@@ -813,7 +802,6 @@ function ZipFSElement(
isDirectory: false,
path: path.join(finalPath, "raw_app.yaml"),
async *getChildren() {},
// deno-lint-ignore require-await
async getContentText() {
return yamlStringify(rawApp, yamlOptions);
},
@@ -824,7 +812,6 @@ function ZipFSElement(
isDirectory: false,
path: path.join(finalPath, "DATATABLES.md"),
async *getChildren() {},
// deno-lint-ignore require-await
async getContentText() {
return generateDatatablesDocumentation(data);
},
@@ -917,7 +904,6 @@ function ZipFSElement(
isDirectory: false,
path: removeSuffix(finalPath, ".json") + ".lock",
async *getChildren() {},
// deno-lint-ignore require-await
async getContentText() {
return lock;
},
@@ -946,7 +932,6 @@ function ZipFSElement(
".resource.file." +
formatExtension,
async *getChildren() {},
// deno-lint-ignore require-await
async getContentText() {
return fileContent;
},
@@ -975,11 +960,6 @@ function ZipFSElement(
}
}
},
// // deno-lint-ignore require-await
// async getContentBytes(): Promise<Uint8Array> {
// throw new Error("Cannot get content of folder");
// },
// deno-lint-ignore require-await
async getContentText(): Promise<string> {
throw new Error("Cannot get content of folder");
},
@@ -1580,7 +1560,7 @@ export async function ignoreF(wmillconf: {
}
try {
await Deno.stat(".wmillignore");
await stat(".wmillignore");
throw Error(".wmillignore is not supported anymore, switch to wmill.yaml");
} catch {
//expected
@@ -1636,7 +1616,6 @@ interface ChangeTracker {
rawApps: string[];
}
// deno-lint-ignore no-inner-declarations
async function addToChangedIfNotExists(p: string, tracker: ChangeTracker) {
const isScript = exts.some((e) => p.endsWith(e));
if (isScript) {
@@ -1700,13 +1679,13 @@ export async function pull(
} catch (error) {
if (error instanceof Error && error.message.includes("overrides")) {
log.error(error.message);
Deno.exit(1);
process.exit(1);
}
throw error;
}
if (opts.stateful) {
await ensureDir(path.join(Deno.cwd(), ".wmill"));
await mkdir(path.join(process.cwd(), ".wmill"), { recursive: true });
}
const workspace = await resolveWorkspace(opts, opts.branch);
@@ -1769,8 +1748,8 @@ export async function pull(
);
const local = !opts.stateful
? await FSFSElement(Deno.cwd(), codebases, true)
: await FSFSElement(path.join(Deno.cwd(), ".wmill"), [], true);
? await FSFSElement(process.cwd(), codebases, true)
: await FSFSElement(path.join(process.cwd(), ".wmill"), [], true);
const changes = await compareDynFSElement(
remote,
@@ -1852,12 +1831,12 @@ export async function pull(
}
}
const target = path.join(Deno.cwd(), targetPath);
const stateTarget = path.join(Deno.cwd(), ".wmill", targetPath);
const target = path.join(process.cwd(), targetPath);
const stateTarget = path.join(process.cwd(), ".wmill", targetPath);
if (change.name === "edited") {
if (opts.stateful) {
try {
const currentLocal = await Deno.readTextFile(target);
const currentLocal = await readFile(target, "utf-8");
if (
currentLocal !== change.before &&
currentLocal !== change.after
@@ -1915,16 +1894,16 @@ export async function pull(
}`,
);
}
await Deno.writeTextFile(target, change.after);
await writeFile(target, change.after, "utf-8");
if (opts.stateful) {
await ensureDir(path.dirname(stateTarget));
await Deno.copyFile(target, stateTarget);
await mkdir(path.dirname(stateTarget), { recursive: true });
await copyFile(target, stateTarget);
}
} else if (change.name === "added") {
await ensureDir(path.dirname(target));
await mkdir(path.dirname(target), { recursive: true });
if (opts.stateful) {
await ensureDir(path.dirname(stateTarget));
await mkdir(path.dirname(stateTarget), { recursive: true });
log.info(
`Adding ${getTypeStrFromPath(change.path)} ${targetPath}${
targetPath !== change.path
@@ -1933,7 +1912,7 @@ export async function pull(
}`,
);
}
await Deno.writeTextFile(target, change.content);
await writeFile(target, change.content, "utf-8");
log.info(
`Writing ${getTypeStrFromPath(change.path)} ${targetPath}${
targetPath !== change.path
@@ -1942,20 +1921,20 @@ export async function pull(
}`,
);
if (opts.stateful) {
await Deno.copyFile(target, stateTarget);
await copyFile(target, stateTarget);
}
} else if (change.name === "deleted") {
try {
log.info(
`Deleting ${getTypeStrFromPath(change.path)} ${change.path}`,
);
await Deno.remove(target);
await rm(target);
if (opts.stateful) {
await Deno.remove(stateTarget);
await rm(stateTarget);
}
} catch {
if (opts.stateful) {
await Deno.remove(stateTarget);
await rm(stateTarget);
}
}
}
@@ -1973,7 +1952,7 @@ export async function pull(
- pushing the changes with \`wmill push --skip-pull\` to override wmill with all your local changes
`),
);
Deno.exit(1);
process.exit(1);
}
}
log.info("All local changes pulled, now updating wmill-lock.yaml");
@@ -2189,7 +2168,7 @@ export async function push(
} catch (error) {
if (error instanceof Error && error.message.includes("overrides")) {
log.error(error.message);
Deno.exit(1);
process.exit(1);
}
throw error;
}
@@ -2217,7 +2196,7 @@ export async function push(
printReport(lintReport, !!opts.jsonOutput);
if (!lintReport.success) {
log.error(colors.red("Push aborted due to lint failures."));
Deno.exit(1);
process.exit(1);
}
}
@@ -2292,7 +2271,7 @@ export async function push(
false,
);
const local = await FSFSElement(path.join(Deno.cwd(), ""), codebases, false);
const local = await FSFSElement(path.join(process.cwd(), ""), codebases, false);
const changes = await compareDynFSElement(
local,
remote,
@@ -2463,7 +2442,7 @@ export async function push(
let stateful = opts.stateful;
if (stateful) {
try {
await Deno.stat(path.join(Deno.cwd(), ".wmill"));
await stat(path.join(process.cwd(), ".wmill"));
} catch {
stateful = false;
}
@@ -2526,8 +2505,8 @@ export async function push(
let stateTarget = undefined;
if (stateful) {
try {
stateTarget = path.join(Deno.cwd(), ".wmill", change.path);
await Deno.stat(stateTarget);
stateTarget = path.join(process.cwd(), ".wmill", change.path);
await stat(stateTarget);
} catch {
stateTarget = undefined;
}
@@ -2546,7 +2525,7 @@ export async function push(
)
) {
if (stateTarget) {
await Deno.writeTextFile(stateTarget, change.after);
await writeFile(stateTarget, change.after, "utf-8");
}
continue;
} else if (
@@ -2561,12 +2540,12 @@ export async function push(
)
) {
if (stateTarget) {
await Deno.writeTextFile(stateTarget, change.after);
await writeFile(stateTarget, change.after, "utf-8");
}
continue;
}
if (stateTarget) {
await ensureDir(path.dirname(stateTarget));
await mkdir(path.dirname(stateTarget), { recursive: true });
log.info(
`Editing ${getTypeStrFromPath(change.path)} ${change.path}`,
);
@@ -2579,7 +2558,7 @@ export async function push(
const newObj = parseFromPath(
resourceFilePath,
await Deno.readTextFile(resourceFilePath),
await readFile(resourceFilePath, "utf-8"),
);
// For branch-specific resources, push to the base path on the workspace server
@@ -2602,7 +2581,7 @@ export async function push(
resourceFilePath,
);
if (stateTarget) {
await Deno.writeTextFile(stateTarget, change.after);
await writeFile(stateTarget, change.after, "utf-8");
}
continue;
}
@@ -2632,7 +2611,7 @@ export async function push(
);
if (stateTarget) {
await Deno.writeTextFile(stateTarget, change.after);
await writeFile(stateTarget, change.after, "utf-8");
}
} else if (change.name === "added") {
if (
@@ -2656,7 +2635,7 @@ export async function push(
continue;
}
if (stateTarget) {
await ensureDir(path.dirname(stateTarget));
await mkdir(path.dirname(stateTarget), { recursive: true });
log.info(
`Adding ${getTypeStrFromPath(change.path)} ${change.path}`,
);
@@ -2689,7 +2668,7 @@ export async function push(
);
if (stateTarget) {
await Deno.writeTextFile(stateTarget, change.content);
await writeFile(stateTarget, change.content, "utf-8");
}
} else if (change.name === "deleted") {
if (change.path.endsWith(".lock")) {
@@ -2754,7 +2733,7 @@ export async function push(
let folderExists = false;
if (rawAppFolder) {
try {
await Deno.stat(rawAppFolder);
await stat(rawAppFolder);
folderExists = true;
} catch {
// folder doesn't exist
@@ -2922,7 +2901,7 @@ export async function push(
}
if (stateTarget) {
try {
await Deno.remove(stateTarget);
await rm(stateTarget);
} catch {
// state target may not exist already
}
@@ -3051,7 +3030,6 @@ const command = new Command()
"--branch <branch:string>",
"Override the current git branch (works even outside a git repository)",
)
// deno-lint-ignore no-explicit-any
.action(pull as any)
.command("push")
.description("Push any local changes and apply them remotely.")
@@ -3113,7 +3091,6 @@ const command = new Command()
"--locks-required",
"Fail if scripts or flow inline scripts that need locks have no locks",
)
// deno-lint-ignore no-explicit-any
.action(push as any);
export default command;
+9 -3
View File
@@ -1,3 +1,5 @@
import { stat } from "node:fs/promises";
import * as wmill from "../../../gen/services.gen.ts";
import {
GcpTrigger,
@@ -13,7 +15,11 @@ import {
NativeTriggerData,
NativeServiceName,
} from "../../../gen/types.gen.ts";
import { colors, Command, log, SEP, Table } from "../../../deps.ts";
import { Command } from "@cliffy/command";
import { Table } from "@cliffy/table";
import { colors } from "@cliffy/ansi/colors";
import * as log from "@std/log";
import { SEPARATOR as SEP } from "@std/path";
import {
GlobalOptions,
isSuperset,
@@ -372,8 +378,8 @@ async function push(opts: GlobalOptions, filePath: string, remotePath: string) {
return;
}
const fstat = await Deno.stat(filePath);
if (!fstat.isFile) {
const fstat = await stat(filePath);
if (!fstat.isFile()) {
throw new Error("file path must refer to a file.");
}
+14 -13
View File
@@ -1,4 +1,5 @@
// deno-lint-ignore-file no-explicit-any
import { writeFile } from "node:fs/promises";
import { requireLogin } from "../../core/auth.ts";
import {
GlobalOptions,
@@ -7,14 +8,12 @@ import {
removePathPrefix,
} from "../../types.ts";
import { compareInstanceObjects, InstanceSyncOptions } from "../instance/instance.ts";
import {
colors,
Command,
log,
Table,
yamlStringify,
yamlParseFile,
} from "../../../deps.ts";
import { colors } from "@cliffy/ansi/colors";
import { Command } from "@cliffy/command";
import { Table } from "@cliffy/table";
import * as log from "@std/log";
import { stringify as yamlStringify } from "@std/yaml";
import { yamlParseFile } from "../../utils/yaml.ts";
import * as wmill from "../../../gen/services.gen.ts";
import {
ExportedInstanceGroup,
@@ -417,9 +416,10 @@ export async function pullInstanceUsers(
return compareInstanceObjects(remoteUsers, localUsers, "email", "user");
} else {
log.info("Pulling users from instance...");
await Deno.writeTextFile(
await writeFile(
instanceUsersPath,
yamlStringify(remoteUsers as any)
yamlStringify(remoteUsers as any),
"utf-8"
);
log.info(colors.green(`Users written to ${instanceUsersPath}`));
}
@@ -486,9 +486,10 @@ export async function pullInstanceGroups(
} else {
log.info("Pulling groups from instance...");
await Deno.writeTextFile(
await writeFile(
instanceGroupsPath,
yamlStringify(remoteGroups as any)
yamlStringify(remoteGroups as any),
"utf-8"
);
log.info(colors.green(`Groups written to ${instanceGroupsPath}`));
+10 -4
View File
@@ -1,4 +1,5 @@
// deno-lint-ignore-file no-explicit-any
import { stat } from "node:fs/promises";
import { requireLogin } from "../../core/auth.ts";
import { resolveWorkspace, validatePath } from "../../core/context.ts";
import {
@@ -7,7 +8,12 @@ import {
parseFromFile,
removeType,
} from "../../types.ts";
import { colors, Command, Confirm, log, SEP, Table } from "../../../deps.ts";
import { Command } from "@cliffy/command";
import { Table } from "@cliffy/table";
import { colors } from "@cliffy/ansi/colors";
import { Confirm } from "@cliffy/prompt/confirm";
import * as log from "@std/log";
import { SEPARATOR as SEP } from "@std/path";
import * as wmill from "../../../gen/services.gen.ts";
import { ListableVariable } from "../../../gen/types.gen.ts";
@@ -108,8 +114,8 @@ async function push(
return;
}
const fstat = await Deno.stat(filePath);
if (!fstat.isFile) {
const fstat = await stat(filePath);
if (!fstat.isFile()) {
throw new Error("file path must refer to a file.");
}
@@ -1,6 +1,8 @@
import { Command, Confirm, setClient, Table } from "../../../deps.ts";
import { log } from "../../../deps.ts";
import { Command } from "@cliffy/command";
import { Table } from "@cliffy/table";
import { Confirm } from "@cliffy/prompt/confirm";
import * as log from "@std/log";
import { setClient } from "../../core/client.ts";
import { allInstances, getActiveInstance, InstanceSyncOptions, pickInstance } from "../instance/instance.ts";
import * as wmill from "../../../gen/services.gen.ts";
import { pullInstanceConfigs, pushInstanceConfigs } from "../../core/settings.ts";
+3 -2
View File
@@ -1,5 +1,6 @@
import { Command, Table } from "../../../deps.ts";
import { log } from "../../../deps.ts";
import { Command } from "@cliffy/command";
import { Table } from "@cliffy/table";
import * as log from "@std/log";
import * as wmill from "../../../gen/services.gen.ts";
import { pickInstance } from "../instance/instance.ts";
+5 -3
View File
@@ -1,6 +1,8 @@
// deno-lint-ignore-file no-explicit-any
import { GlobalOptions } from "../../types.ts";
import { colors, Input, log, setClient } from "../../../deps.ts";
import { colors } from "@cliffy/ansi/colors";
import { Input } from "@cliffy/prompt/input";
import * as log from "@std/log";
import { setClient } from "../../core/client.ts";
import { allWorkspaces, list, removeWorkspace } from "./workspace.ts";
import * as wmill from "../../../gen/services.gen.ts";
import { getCurrentGitBranch, getOriginalBranchForWorkspaceForks, isGitRepository } from "../../utils/git.ts";
@@ -159,7 +161,7 @@ async function deleteWorkspaceFork(
}
if (!opts.yes) {
const { Select } = await import("../../../deps.ts");
const { Select } = await import("@cliffy/prompt/select");
const choice = await Select.prompt({
message: `Are you sure you want to delete the forked workspace with id: \`${workspace.workspaceId}\`? This action will delete the workspace `,
options: [
+41 -29
View File
@@ -1,19 +1,18 @@
// deno-lint-ignore-file no-explicit-any
import { readFile, writeFile, open as fsOpen } from "node:fs/promises";
import process from "node:process";
import { GlobalOptions } from "../../types.ts";
import {
getActiveWorkspaceConfigFilePath,
getWorkspaceConfigFilePath,
} from "../../../windmill-utils-internal/src/config/config.ts";
import { loginInteractive, tryGetLoginInfo } from "../../core/login.ts";
import {
colors,
Command,
Confirm,
Input,
log,
setClient,
Table,
} from "../../../deps.ts";
import { colors } from "@cliffy/ansi/colors";
import { Command } from "@cliffy/command";
import { Confirm } from "@cliffy/prompt/confirm";
import { Input } from "@cliffy/prompt/input";
import { Table } from "@cliffy/table";
import * as log from "@std/log";
import { setClient } from "../../core/client.ts";
import { requireLogin } from "../../core/auth.ts";
import { createWorkspaceFork, deleteWorkspaceFork } from "./fork.ts";
@@ -31,7 +30,7 @@ export async function allWorkspaces(
): Promise<Workspace[]> {
try {
const file = await getWorkspaceConfigFilePath(configDirOverride);
const txt = await Deno.readTextFile(file);
const txt = await readFile(file, "utf-8");
return txt
.split("\n")
.map((line) => {
@@ -55,7 +54,7 @@ async function getActiveWorkspaceName(
}
try {
const file = await getActiveWorkspaceConfigFilePath(opts?.configDir);
return await Deno.readTextFile(file);
return await readFile(file, "utf-8");
} catch {
return undefined;
}
@@ -146,7 +145,7 @@ export async function setActiveWorkspace(
configDirOverride?: string
) {
const file = await getActiveWorkspaceConfigFilePath(configDirOverride);
await Deno.writeTextFile(file, workspaceName);
await writeFile(file, workspaceName, "utf-8");
}
export async function add(
@@ -202,7 +201,7 @@ export async function add(
remote = new URL(remote).toString(); // add trailing slash in all cases!
let token = await tryGetLoginInfo(opts);
if (!token && Deno.stdin.isTerminal && !Deno.stdin.isTerminal()) {
if (!token && !(process.stdin.isTTY ?? false)) {
log.info("Not a TTY, can't login interactively. Pass the token in --token");
return;
}
@@ -257,7 +256,7 @@ export async function add(
for (const workspace of workspaces) {
log.info(`- ${workspace.id} (name: ${workspace.name})`);
}
Deno.exit(1);
process.exit(1);
}
const added = await addWorkspace(
@@ -287,7 +286,7 @@ export async function addWorkspace(workspace: Workspace, opts: any): Promise<boo
// Check for conflicts before adding
const existingWorkspaces = await allWorkspaces(opts.configDir);
const isInteractive =
Deno.stdin.isTerminal() && Deno.stdout.isTerminal() && !opts.force;
(process.stdin.isTTY ?? false) && (process.stdout.isTTY ?? false) && !opts.force;
// Check 1: Workspace name already exists
const nameConflict = existingWorkspaces.find(
@@ -339,20 +338,32 @@ export async function addWorkspace(workspace: Workspace, opts: any): Promise<boo
}
}
// Check 2: Same (remote, workspaceId) already exists with a different name
const backendConflict = existingWorkspaces.find(
(w) =>
w.remote === workspace.remote &&
w.workspaceId === workspace.workspaceId &&
w.name !== workspace.name
);
if (backendConflict) {
if (opts.force) {
// Remove the conflicting workspace before adding the new one
await removeWorkspace(backendConflict.name, true, opts);
} else {
throw new Error(
`Backend constraint violation: (${workspace.remote}, ${workspace.workspaceId}) already exists as "${backendConflict.name}". Use --force to overwrite.`
);
}
}
// Remove existing workspace with same name (if updating)
await removeWorkspace(workspace.name, true, opts);
// Add the new workspace
const filePath = await getWorkspaceConfigFilePath(opts.configDir);
const file = await Deno.open(filePath, {
append: true,
write: true,
read: true,
create: true,
});
await file.write(new TextEncoder().encode(JSON.stringify(workspace) + "\n"));
file.close();
const fh = await fsOpen(filePath, "a");
await fh.write(JSON.stringify(workspace) + "\n");
await fh.close();
return true;
}
@@ -377,12 +388,13 @@ export async function removeWorkspace(
}
const filePath = await getWorkspaceConfigFilePath(opts.configDir);
await Deno.writeTextFile(
await writeFile(
filePath,
orgWorkspaces
.filter((x) => x.name !== name)
.map((x) => JSON.stringify(x))
.join("\n") + "\n"
.join("\n") + "\n",
"utf-8"
);
if (!silent) {
@@ -506,9 +518,9 @@ async function bind(
}
// Write back the updated config
const { yamlStringify } = await import("../../../deps.ts");
const { stringify: yamlStringify } = await import("@std/yaml");
try {
await Deno.writeTextFile("wmill.yaml", yamlStringify(config));
await writeFile("wmill.yaml", yamlStringify(config), "utf-8");
} catch (error) {
log.error(colors.red(`Failed to save configuration: ${(error as Error).message}`));
return;
+3 -2
View File
@@ -1,5 +1,6 @@
// deno-lint-ignore-file no-explicit-any
import { colors, log, setClient } from "../../deps.ts";
import { colors } from "@cliffy/ansi/colors";
import * as log from "@std/log";
import { setClient } from "./client.ts";
import * as wmill from "../../gen/services.gen.ts";
import { GlobalUserInfo } from "../../gen/types.gen.ts";
+4 -3
View File
@@ -1,4 +1,5 @@
import { log } from "../../deps.ts";
import * as log from "@std/log";
import { readFile, writeFile } from "node:fs/promises";
import { getStore } from "./store.ts";
export interface BranchProfileMapping {
@@ -16,7 +17,7 @@ export async function getBranchProfilesPath(configDirOverride?: string): Promise
export async function loadBranchProfiles(configDirOverride?: string): Promise<BranchProfileMapping> {
try {
const path = await getBranchProfilesPath(configDirOverride);
const content = await Deno.readTextFile(path);
const content = await readFile(path, "utf-8");
return JSON.parse(content);
} catch {
// File doesn't exist or invalid JSON - return empty mapping
@@ -29,7 +30,7 @@ export async function saveBranchProfiles(
configDirOverride?: string
): Promise<void> {
const path = await getBranchProfilesPath(configDirOverride);
await Deno.writeTextFile(path, JSON.stringify(mapping, null, 2));
await writeFile(path, JSON.stringify(mapping, null, 2), "utf-8");
}
export function getBranchProfileKey(
+15
View File
@@ -0,0 +1,15 @@
import { OpenAPI } from "../../gen/index.ts";
export function setClient(token?: string, baseUrl?: string) {
if (baseUrl === undefined) {
baseUrl = process.env["BASE_INTERNAL_URL"] ??
process.env["BASE_URL"] ??
"http://localhost:8000";
}
if (token === undefined) {
token = process.env["WM_TOKEN"] ?? "no_token";
}
OpenAPI.WITH_CREDENTIALS = true;
OpenAPI.TOKEN = token;
OpenAPI.BASE = baseUrl + "/api";
}
+10 -6
View File
@@ -1,4 +1,7 @@
import { log, yamlParseFile, Confirm, yamlStringify } from "../../deps.ts";
import * as log from "@std/log";
import { yamlParseFile } from "../utils/yaml.ts";
import { Confirm } from "@cliffy/prompt/confirm";
import { stringify as yamlStringify } from "@std/yaml";
import {
getCurrentGitBranch,
getOriginalBranchForWorkspaceForks,
@@ -6,6 +9,7 @@ import {
} from "../utils/git.ts";
import { join, dirname, resolve, relative } from "node:path";
import { existsSync } from "node:fs";
import { writeFile } from "node:fs/promises";
import { execSync } from "node:child_process";
import { setNonDottedPaths } from "../utils/resource_folders.ts";
@@ -133,7 +137,7 @@ function getGitRepoRoot(): string | null {
export const GLOBAL_CONFIG_OPT = { noCdToRoot: false };
function findWmillYaml(): string | null {
const startDir = resolve(Deno.cwd());
const startDir = resolve(process.cwd());
const isInGitRepo = isGitRepository();
const gitRoot = isInGitRepo ? getGitRepoRoot() : null;
@@ -174,7 +178,7 @@ function findWmillYaml(): string | null {
log.warn(`⚠️ wmill.yaml found in parent directory: ${relativePath}`);
// Change working directory to where wmill.yaml was found
Deno.chdir(configDir);
process.chdir(configDir);
log.info(`📁 Changed working directory to: ${configDir}`);
}
@@ -251,7 +255,7 @@ export async function readConfigFile(): Promise<SyncOptions> {
// Perform single atomic write if any migrations are needed
if (needsConfigWrite) {
try {
await Deno.writeTextFile(wmillYamlPath, yamlStringify(conf));
await writeFile(wmillYamlPath, yamlStringify(conf), "utf-8");
// Log all migration messages after successful write
migrationMessages.forEach((msg) => {
if (msg.startsWith("⚠️")) {
@@ -418,7 +422,7 @@ export async function validateBranchConfiguration(
// Current branch must be defined in gitBranches config
if (currentBranch && !gitBranches[currentBranch]) {
// In interactive mode, offer to create the branch
if (Deno.stdin.isTerminal()) {
if (!!process.stdin.isTTY) {
const availableBranches = Object.keys(gitBranches).join(", ");
log.info(
`Current Git branch '${currentBranch}' is not defined in the gitBranches configuration.\n` +
@@ -458,7 +462,7 @@ export async function validateBranchConfiguration(
}
currentConfig.gitBranches[currentBranch] = { overrides: {} };
await Deno.writeTextFile("wmill.yaml", yamlStringify(currentConfig));
await writeFile("wmill.yaml", yamlStringify(currentConfig), "utf-8");
log.info(
`✅ Created empty branch configuration for '${currentBranch}'`
+12 -9
View File
@@ -1,5 +1,8 @@
// deno-lint-ignore-file no-explicit-any
import { colors, log, Select, Confirm, Input } from "../../deps.ts";
import { colors } from "@cliffy/ansi/colors";
import * as log from "@std/log";
import { Select } from "@cliffy/prompt/select";
import { Confirm } from "@cliffy/prompt/confirm";
import { Input } from "@cliffy/prompt/input";
import { loginInteractive } from "./login.ts";
import { GlobalOptions } from "../types.ts";
@@ -56,7 +59,7 @@ async function selectFromMultipleProfiles(
}
// No last used or it no longer exists - prompt for selection
if (!Deno.stdin.isTerminal() || !Deno.stdout.isTerminal()) {
if (!!!process.stdin.isTTY || !!!process.stdout.isTTY) {
const selectedProfile = profiles[0];
log.info(
colors.yellow(
@@ -129,7 +132,7 @@ async function createWorkspaceProfileInteractively(
);
}
if (!Deno.stdin.isTerminal() || !Deno.stdout.isTerminal()) {
if (!!!process.stdin.isTTY || !!!process.stdout.isTTY) {
log.info(
"Not a TTY, cannot create profile interactively. Use 'wmill workspace add' first."
);
@@ -382,7 +385,7 @@ export async function resolveWorkspace(
normalizedBaseUrl = new URL(opts.baseUrl).toString(); // add trailing slash if not present
} catch (error) {
log.info(colors.red(`Invalid base URL: ${opts.baseUrl}`));
return Deno.exit(-1);
return process.exit(-1);
}
// Try to find existing workspace profile by name, then by workspaceId + remote
@@ -423,7 +426,7 @@ export async function resolveWorkspace(
`Base URL mismatch: --base-url is ${normalizedBaseUrl} but workspace profile "${opts.workspace}" uses ${existingWorkspace.remote}`
)
);
return Deno.exit(-1);
return process.exit(-1);
}
// Use the existing workspace profile (preserves workspace name)
return {
@@ -446,7 +449,7 @@ export async function resolveWorkspace(
"If you specify a base URL with --base-url, you must also specify a workspace (--workspace) and token (--token)."
)
);
return Deno.exit(-1);
return process.exit(-1);
}
}
@@ -479,7 +482,7 @@ export async function resolveWorkspace(
`Failed to resolve workspace profile for workspace fork. This most likely means that the original branch \`${originalBranch}\` where \`${branch}\` is originally forked from, is not setup in the wmill.yaml. You need to update the \`gitBranches\` section for \`${originalBranch}\` to include workspaceId and baseUrl.`
)
);
return Deno.exit(-1);
return process.exit(-1);
}
}
@@ -492,7 +495,7 @@ export async function resolveWorkspace(
// If everything failed, show error
log.info(colors.red.bold("No workspace given and no default set."));
return Deno.exit(-1);
return process.exit(-1);
}
export async function fetchVersion(baseUrl: string): Promise<string> {
+9 -31
View File
@@ -1,10 +1,15 @@
import { GlobalOptions } from "../types.ts";
import { colors, getPort, log, open, Secret, Select } from "../../deps.ts";
import { colors } from "@cliffy/ansi/colors";
import * as getPort from "get-port";
import * as log from "@std/log";
import * as open from "open";
import { Secret } from "@cliffy/prompt/secret";
import { Select } from "@cliffy/prompt/select";
import * as http from "node:http";
export async function loginInteractive(remote: string) {
let token: string | undefined;
if (Deno.stdin.isTerminal && !Deno.stdin.isTerminal()) {
if (!process.stdin.isTTY) {
log.info("Not a TTY, can't login interactively.");
return undefined;
}
@@ -30,7 +35,6 @@ export async function loginInteractive(remote: string) {
return token;
}
// deno-lint-ignore require-await
export async function tryGetLoginInfo(
opts: GlobalOptions
): Promise<string | undefined> {
@@ -45,8 +49,8 @@ export async function browserLogin(
baseUrl: string
): Promise<string | undefined> {
const env =
Deno.env.get("TOKEN_PORT") != undefined
? parseInt(Deno.env.get("TOKEN_PORT")!)
process.env["TOKEN_PORT"] != undefined
? parseInt(process.env["TOKEN_PORT"]!)
: undefined;
const port = await getPort.default({ port: env });
@@ -55,32 +59,6 @@ export async function browserLogin(
return undefined;
}
// const server = Deno.listen({ transport: "tcp", port });
// const url = `${baseUrl}user/cli?port=${port}`;
// log.info(`Login by going to ${url}`);
// try {
// await open.openApp(open.apps.browser, { arguments: [url] });
// log.info("Opened browser for you");
// } catch {
// console.error(`Failed to open browser, please navigate to ${url}`);
// }
// const firstConnection = await server.accept();
// const httpFirstConnection = Deno.serveHttp(firstConnection);
// const firstRequest = (await httpFirstConnection.nextRequest())!;
// const params = new URL(firstRequest.request.url!).searchParams;
// const token = params.get("token");
// // const _workspace = params.get("workspace");
// await firstRequest?.respondWith(
// Response.redirect(baseUrl + "user/cli-success", 302)
// );
// setTimeout(() => {
// httpFirstConnection.close();
// server.close();
// }, 10);
// return token ?? undefined;
return new Promise((resolve) => {
const server = http.createServer((req, res) => {
const params = new URL(req.url!, `http://${req.headers.host}`)
+12 -5
View File
@@ -1,5 +1,10 @@
import process from "node:process";
import { colors, Confirm, log, yamlParseFile, yamlStringify } from "../../deps.ts";
import { writeFile } from "node:fs/promises";
import { colors } from "@cliffy/ansi/colors";
import { Confirm } from "@cliffy/prompt/confirm";
import * as log from "@std/log";
import { yamlParseFile } from "../utils/yaml.ts";
import { stringify as yamlStringify } from "@std/yaml";
import * as wmill from "../../gen/services.gen.ts";
import { AIConfig, Config, GlobalSetting } from "../../gen/types.gen.ts";
import { compareInstanceObjects, InstanceSyncOptions } from "../commands/instance/instance.ts";
@@ -493,9 +498,10 @@ export async function pullInstanceSettings(
remoteSettings,
"encode"
);
await Deno.writeTextFile(
await writeFile(
instanceSettingsPath,
yamlStringify(processedSettings)
yamlStringify(processedSettings),
"utf-8"
);
log.info(colors.green(`Settings written to ${instanceSettingsPath}`));
@@ -602,9 +608,10 @@ export async function pullInstanceConfigs(
} else {
log.info("Pulling configs from instance");
await Deno.writeTextFile(
await writeFile(
instanceConfigsPath,
yamlStringify(remoteConfigs as any)
yamlStringify(remoteConfigs as any),
"utf-8"
);
log.info(colors.green(`Configs written to ${instanceConfigsPath}`));
+1 -1
View File
@@ -1,4 +1,4 @@
import { minimatch } from "../../deps.ts";
import { minimatch } from "minimatch";
import { getCurrentGitBranch, isGitRepository } from "../utils/git.ts";
import { isFileResource } from "../utils/utils.ts";
import { SyncOptions } from "./conf.ts";
+2 -2
View File
@@ -1,4 +1,4 @@
import { ensureDir } from "../../deps.ts";
import { mkdir } from "node:fs/promises";
import { getConfigDirPath } from "../../windmill-utils-internal/src/config/config.ts";
function hash_string(str: string): number {
@@ -17,6 +17,6 @@ function hash_string(str: string): number {
export async function getStore(baseUrl: string, configDirOverride?: string): Promise<string> {
const baseHash = Math.abs(hash_string(baseUrl)).toString(16);
const baseStore = (await getConfigDirPath(configDirOverride)) + baseHash + "/";
await ensureDir(baseStore);
await mkdir(baseStore, { recursive: true });
return baseStore;
}
+23 -57
View File
@@ -1,16 +1,9 @@
import {
Command,
CompletionsCommand,
UpgradeCommand,
esMain,
log,
} from "../deps.ts";
import { Command } from "@cliffy/command";
import { CompletionsCommand } from "@cliffy/command/completions";
import { UpgradeCommand } from "@cliffy/command/upgrade";
import * as log from "@std/log";
// Node.js-specific imports for symlink resolution in isMain()
// These are only used in Node.js, not Deno
// dnt-shim-ignore
import { realpathSync } from "node:fs";
// dnt-shim-ignore
import { fileURLToPath } from "node:url";
import flow from "./commands/flow/flow.ts";
import app from "./commands/app/app.ts";
@@ -72,13 +65,6 @@ export {
workspaceAdd,
};
// addEventListener("error", (event) => {
// if (event.error) {
// console.error("Error details of: " + event.error.message);
// console.error(JSON.stringify(event.error, null, 4));
// }
// });
export const VERSION = "1.641.0";
// Re-exported from constants.ts to maintain backwards compatibility
@@ -165,7 +151,9 @@ const command = new Command()
const backendVersion = await fetchVersion(workspace.remote);
console.log("Backend Version: " + backendVersion);
} catch (e) {
console.warn("Cannot fetch backend version: " + e);
console.warn(
`Cannot fetch backend version from ${workspace.remote} (workspace: ${workspace.name}): ${e}`
);
}
} else {
console.warn(
@@ -188,15 +176,16 @@ const command = new Command()
async function main() {
try {
if (Deno.args.length === 0) {
const args = process.argv.slice(2);
if (args.length === 0) {
command.showHelp();
}
const LOG_LEVEL =
Deno.args.includes("--verbose") || Deno.args.includes("--debug")
args.includes("--verbose") || args.includes("--debug")
? "DEBUG"
: "INFO";
// const NO_COLORS = Deno.args.includes("--no-colors");
setShowDiffs(Deno.args.includes("--show-diffs"));
// const NO_COLORS = args.includes("--no-colors");
setShowDiffs(args.includes("--show-diffs"));
const isWin = await getIsWin();
log.setup({
@@ -219,7 +208,7 @@ async function main() {
if (extraHeaders) {
OpenAPI.HEADERS = extraHeaders;
}
await command.parse(Deno.args);
await command.parse(args);
} catch (e) {
if (e && typeof e === "object" && "name" in e && e.name === "ApiError") {
console.log(
@@ -231,41 +220,18 @@ async function main() {
}
function isMain() {
// dnt-shim-ignore
const { Deno } = globalThis as any;
// Handle symlinks properly: resolve symlinks when comparing process.argv[1]
// with import.meta.url, so `wmill` symlink matches the real file path.
try {
const scriptPath = process.argv[1];
if (!scriptPath) return false;
const isDeno = Deno != undefined;
const realScriptPath = realpathSync(scriptPath);
const modulePath = fileURLToPath(import.meta.url);
if (isDeno) {
const isMain = import.meta.main;
if (isMain) {
if (!Deno.args.includes("completions")) {
if (Deno.env.get("SKIP_DENO_DEPRECATION_WARNING") !== "true") {
log.warn(
"Using the deno runtime for the Windmill CLI is deprecated, you can now use node: deno uninstall wmill && npm install -g windmill-cli. To skip this warning set SKIP_DENO_DEPRECATION_WARNING=true"
);
}
}
}
return isMain;
} else {
// For Node.js, we need to handle symlinks properly.
// The dnt polyfill doesn't resolve symlinks when comparing process.argv[1]
// with import.meta.url, so `wmill` symlink doesn't match the real file path.
// We resolve symlinks manually to get accurate comparison.
try {
const scriptPath = process.argv[1];
if (!scriptPath) return false;
const realScriptPath = realpathSync(scriptPath);
const modulePath = fileURLToPath(import.meta.url);
return realScriptPath === modulePath;
} catch {
// Fallback to esMain if something fails
//@ts-ignore
return esMain.default(import.meta);
}
return realScriptPath === modulePath;
} catch {
return false;
}
}
if (isMain()) {
+10 -13
View File
@@ -1,14 +1,11 @@
// deno-lint-ignore-file no-explicit-any
import {
colors,
Diff,
log,
path,
SEP,
yamlParseContent,
yamlStringify,
} from "../deps.ts";
import { colors } from "@cliffy/ansi/colors";
import * as Diff from "diff";
import * as log from "@std/log";
import * as path from "@std/path";
import { SEPARATOR as SEP } from "@std/path";
import { stringify as yamlStringify } from "@std/yaml";
import { yamlParseContent } from "./utils/yaml.ts";
import { readFileSync } from "node:fs";
import { pushApp } from "./commands/app/app.ts";
import { pushFolder } from "./commands/folder/folder.ts";
import { pushFlow } from "./commands/flow/flow.ts";
@@ -228,9 +225,9 @@ export function parseFromPath(p: string, content: string): any {
}
export function parseFromFile(p: string): any {
if (p.endsWith(".json")) {
return JSON.parse(Deno.readTextFileSync(p));
return JSON.parse(readFileSync(p, "utf-8"));
} else if (p.endsWith(".yaml") || p.endsWith(".yml")) {
return yamlParseContent(p, Deno.readTextFileSync(p));
return yamlParseContent(p, readFileSync(p, "utf-8"));
} else {
throw new Error("Could not read file " + p);
}
+1 -1
View File
@@ -1,5 +1,5 @@
import { Codebase, SyncOptions } from "../core/conf.ts";
import { log } from "../../deps.ts";
import * as log from "@std/log";
import { digestDir } from "./utils.ts";
export type SyncCodebase = Codebase & {
+1 -1
View File
@@ -1,4 +1,4 @@
import { log } from "../../deps.ts";
import * as log from "@std/log";
import { execSync } from "node:child_process";
import { WM_FORK_PREFIX } from "../core/constants.ts";
+72 -89
View File
@@ -1,6 +1,12 @@
// deno-lint-ignore-file no-explicit-any
import { GlobalOptions } from "../types.ts";
import { SEP, colors, log, yamlParseFile, yamlStringify } from "../../deps.ts";
import { SEPARATOR as SEP } from "@std/path";
import { colors } from "@cliffy/ansi/colors";
import * as log from "@std/log";
import { stringify as yamlStringify } from "@std/yaml";
import { yamlParseFile } from "./yaml.ts";
import { readFile, writeFile, stat, rm, readdir } from "node:fs/promises";
import { readFileSync } from "node:fs";
import { createRequire } from "node:module";
import {
ScriptMetadata,
defaultScriptMetadata,
@@ -18,6 +24,25 @@ import { SyncCodebase } from "./codebase.ts";
import { argSigToJsonSchemaType } from "../../windmill-utils-internal/src/parse/parse-schema.ts";
import { getIsWin } from "./utils.ts";
const _require = createRequire(import.meta.url);
const _parserCache = new Map<string, Promise<any>>();
function loadParser(pkgName: string): Promise<any> {
let p = _parserCache.get(pkgName);
if (!p) {
p = (async () => {
const mod = await import(pkgName);
const wasmPath = _require.resolve(
`${pkgName}/windmill_parser_wasm_bg.wasm`
);
await mod.default(readFileSync(wasmPath));
return mod;
})();
_parserCache.set(pkgName, p);
}
return p;
}
export class LockfileGenerationError extends Error {
constructor(message: string) {
super(message);
@@ -31,11 +56,12 @@ export async function getRawWorkspaceDependencies(): Promise<Record<string, stri
const rawWorkspaceDeps: Record<string, string> = {};
try {
for await (const entry of Deno.readDir("dependencies")) {
if (entry.isDirectory) continue;
const entries = await readdir("dependencies", { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory()) continue;
const filePath = `dependencies/${entry.name}`;
const content = await Deno.readTextFile(filePath);
const content = await readFile(filePath, "utf-8");
// Find matching language
for (const lang of workspaceDependenciesLanguages) {
@@ -120,7 +146,7 @@ export async function filterWorkspaceDependenciesForScripts(
if (content.startsWith("!inline ")) {
const filePath = folder + sep + content.replace("!inline ", "");
try {
content = await Deno.readTextFile(filePath);
content = await readFile(filePath, "utf-8");
} catch {
continue;
}
@@ -173,8 +199,8 @@ export async function generateScriptMetadataInternal(
);
// read script content
const scriptContent = await Deno.readTextFile(scriptPath);
const metadataContent = await Deno.readTextFile(metadataWithType.path);
const scriptContent = await readFile(scriptPath, "utf-8");
const metadataContent = await readFile(metadataWithType.path, "utf-8");
const filteredRawWorkspaceDependencies = filterWorkspaceDependencies(
rawWorkspaceDependencies,
@@ -250,7 +276,7 @@ export async function generateScriptMetadataInternal(
);
await updateMetadataGlobalLock(remotePath, hash);
if (!justUpdateMetadataLock) {
await Deno.writeTextFile(metaPath, newMetadataContent);
await writeFile(metaPath, newMetadataContent, "utf-8");
}
return `${remotePath} (${language})`;
}
@@ -490,12 +516,12 @@ async function updateScriptLock(
const lockPath = remotePath + ".script.lock";
if (lock != "") {
await Deno.writeTextFile(lockPath, lock);
await writeFile(lockPath, lock, "utf-8");
metadataContent.lock = "!inline " + lockPath.replaceAll(SEP, "/");
} else {
try {
if (await Deno.stat(lockPath)) {
await Deno.remove(lockPath);
if (await stat(lockPath)) {
await rm(lockPath);
}
} catch (e) {
log.info(colors.yellow(`Error removing lock file ${lockPath}: ${e}`));
@@ -519,139 +545,98 @@ export async function inferSchema(
}> {
let inferedSchema: any;
if (language === "python3") {
const { parse_python } = await import(
"../../wasm/py/windmill_parser_wasm.js"
);
const { parse_python } = await loadParser("windmill-parser-wasm-py");
inferedSchema = JSON.parse(parse_python(content));
} else if (language === "nativets") {
const { parse_deno } = await import(
"../../wasm/ts/windmill_parser_wasm.js"
);
const { parse_deno } = await loadParser("windmill-parser-wasm-ts");
inferedSchema = JSON.parse(parse_deno(content));
} else if (language === "bun") {
const { parse_deno } = await import(
"../../wasm/ts/windmill_parser_wasm.js"
);
const { parse_deno } = await loadParser("windmill-parser-wasm-ts");
inferedSchema = JSON.parse(parse_deno(content));
} else if (language === "deno") {
const { parse_deno } = await import(
"../../wasm/ts/windmill_parser_wasm.js"
);
const { parse_deno } = await loadParser("windmill-parser-wasm-ts");
inferedSchema = JSON.parse(parse_deno(content));
} else if (language === "go") {
const { parse_go } = await import("../../wasm/go/windmill_parser_wasm.js");
const { parse_go } = await loadParser("windmill-parser-wasm-go");
inferedSchema = JSON.parse(parse_go(content));
} else if (language === "mysql") {
const { parse_mysql } = await import(
"../../wasm/regex/windmill_parser_wasm.js"
);
const { parse_mysql } = await loadParser("windmill-parser-wasm-regex");
inferedSchema = JSON.parse(parse_mysql(content));
inferedSchema.args = [
{ name: "database", typ: { resource: "mysql" } },
...inferedSchema.args,
];
} else if (language === "bigquery") {
const { parse_bigquery } = await import(
"../../wasm/regex/windmill_parser_wasm.js"
);
const { parse_bigquery } = await loadParser("windmill-parser-wasm-regex");
inferedSchema = JSON.parse(parse_bigquery(content));
inferedSchema.args = [
{ name: "database", typ: { resource: "bigquery" } },
...inferedSchema.args,
];
} else if (language === "oracledb") {
const { parse_oracledb } = await import(
"../../wasm/regex/windmill_parser_wasm.js"
);
const { parse_oracledb } = await loadParser("windmill-parser-wasm-regex");
inferedSchema = JSON.parse(parse_oracledb(content));
inferedSchema.args = [
{ name: "database", typ: { resource: "oracledb" } },
...inferedSchema.args,
];
} else if (language === "snowflake") {
const { parse_snowflake } = await import(
"../../wasm/regex/windmill_parser_wasm.js"
);
const { parse_snowflake } = await loadParser("windmill-parser-wasm-regex");
inferedSchema = JSON.parse(parse_snowflake(content));
inferedSchema.args = [
{ name: "database", typ: { resource: "snowflake" } },
...inferedSchema.args,
];
} else if (language === "mssql") {
const { parse_mssql } = await import(
"../../wasm/regex/windmill_parser_wasm.js"
);
const { parse_mssql } = await loadParser("windmill-parser-wasm-regex");
inferedSchema = JSON.parse(parse_mssql(content));
inferedSchema.args = [
{ name: "database", typ: { resource: "ms_sql_server" } },
...inferedSchema.args,
];
} else if (language === "postgresql") {
const { parse_sql } = await import(
"../../wasm/regex/windmill_parser_wasm.js"
);
const { parse_sql } = await loadParser("windmill-parser-wasm-regex");
inferedSchema = JSON.parse(parse_sql(content));
inferedSchema.args = [
{ name: "database", typ: { resource: "postgresql" } },
...inferedSchema.args,
];
} else if (language === "duckdb") {
const { parse_duckdb } = await import(
"../../wasm/regex/windmill_parser_wasm.js"
);
const { parse_duckdb } = await loadParser("windmill-parser-wasm-regex");
inferedSchema = JSON.parse(parse_duckdb(content));
} else if (language === "graphql") {
const { parse_graphql } = await import(
"../../wasm/regex/windmill_parser_wasm.js"
);
const { parse_graphql } = await loadParser("windmill-parser-wasm-regex");
inferedSchema = JSON.parse(parse_graphql(content));
inferedSchema.args = [
{ name: "api", typ: { resource: "graphql" } },
...inferedSchema.args,
];
} else if (language === "bash") {
const { parse_bash } = await import(
"../../wasm/regex/windmill_parser_wasm.js"
);
const { parse_bash } = await loadParser("windmill-parser-wasm-regex");
inferedSchema = JSON.parse(parse_bash(content));
} else if (language === "powershell") {
const { parse_powershell } = await import(
"../../wasm/regex/windmill_parser_wasm.js"
);
const { parse_powershell } = await loadParser("windmill-parser-wasm-regex");
inferedSchema = JSON.parse(parse_powershell(content));
} else if (language === "php") {
const { parse_php } = await import(
"../../wasm/php/windmill_parser_wasm.js"
);
const { parse_php } = await loadParser("windmill-parser-wasm-php");
inferedSchema = JSON.parse(parse_php(content));
} else if (language === "rust") {
const { parse_rust } = await import(
"../../wasm/rust/windmill_parser_wasm.js"
);
const { parse_rust } = await loadParser("windmill-parser-wasm-rust");
inferedSchema = JSON.parse(parse_rust(content));
} else if (language === "csharp") {
const { parse_csharp } = await import(
"../../wasm/csharp/windmill_parser_wasm.js"
);
const { parse_csharp } = await loadParser("windmill-parser-wasm-csharp");
inferedSchema = JSON.parse(parse_csharp(content));
} else if (language === "nu") {
const { parse_nu } = await import("../../wasm/nu/windmill_parser_wasm.js");
const { parse_nu } = await loadParser("windmill-parser-wasm-nu");
inferedSchema = JSON.parse(parse_nu(content));
} else if (language === "ansible") {
const { parse_ansible } = await import(
"../../wasm/yaml/windmill_parser_wasm.js"
);
const { parse_ansible } = await loadParser("windmill-parser-wasm-yaml");
inferedSchema = JSON.parse(parse_ansible(content));
} else if (language === "java") {
const { parse_java } = await import(
"../../wasm/java/windmill_parser_wasm.js"
);
const { parse_java } = await loadParser("windmill-parser-wasm-java");
inferedSchema = JSON.parse(parse_java(content));
} else if (language === "ruby") {
const { parse_ruby } = await import(
"../../wasm/ruby/windmill_parser_wasm.js"
);
const { parse_ruby } = await loadParser("windmill-parser-wasm-ruby");
inferedSchema = JSON.parse(parse_ruby(content));
// for related places search: ADD_NEW_LANG
} else {
@@ -751,16 +736,16 @@ export async function parseMetadataFile(
): Promise<{ isJson: boolean; payload: any; path: string }> {
let metadataFilePath = scriptPath + ".script.json";
try {
await Deno.stat(metadataFilePath);
await stat(metadataFilePath);
return {
path: metadataFilePath,
payload: JSON.parse(await Deno.readTextFile(metadataFilePath)),
payload: JSON.parse(await readFile(metadataFilePath, "utf-8")),
isJson: true,
};
} catch {
try {
metadataFilePath = scriptPath + ".script.yaml";
await Deno.stat(metadataFilePath);
await stat(metadataFilePath);
const payload: any = await yamlParseFile(metadataFilePath);
replaceLock(payload);
@@ -785,12 +770,8 @@ export async function parseMetadataFile(
yamlOptions
);
await Deno.writeTextFile(metadataFilePath, scriptInitialMetadataYaml, {
createNew: true,
});
await Deno.writeTextFile(lockPath, "", {
createNew: true,
});
await writeFile(metadataFilePath, scriptInitialMetadataYaml, { flag: "wx", encoding: "utf-8" });
await writeFile(lockPath, "", { flag: "wx", encoding: "utf-8" });
if (generateMetadataIfMissing) {
log.info(
@@ -857,7 +838,7 @@ export async function readLockfile(): Promise<Lock> {
}
} catch {
const lock = { locks: {}, version: "v2" as const };
await Deno.writeTextFile(WMILL_LOCKFILE, yamlStringify(lock, yamlOptions));
await writeFile(WMILL_LOCKFILE, yamlStringify(lock, yamlOptions), "utf-8");
log.info(colors.green("wmill-lock.yaml created"));
return lock;
@@ -925,9 +906,10 @@ export async function clearGlobalLock(path: string): Promise<void> {
}
});
}
await Deno.writeTextFile(
await writeFile(
WMILL_LOCKFILE,
yamlStringify(conf as Record<string, any>, yamlOptions)
yamlStringify(conf as Record<string, any>, yamlOptions),
"utf-8"
);
}
}
@@ -957,8 +939,9 @@ export async function updateMetadataGlobalLock(
conf.locks[path] = hash;
}
}
await Deno.writeTextFile(
await writeFile(
WMILL_LOCKFILE,
yamlStringify(conf as Record<string, any>, yamlOptions)
yamlStringify(conf as Record<string, any>, yamlOptions),
"utf-8"
);
}
+20 -11
View File
@@ -8,7 +8,9 @@
* (.flow, .app, .raw_app) or dunder-prefixed names (__flow, __app, __raw_app).
*/
import { log, SEP, yamlParseFile } from "../../deps.ts";
import * as log from "@std/log";
import { SEPARATOR as SEP } from "@std/path";
import { yamlParseFile } from "./yaml.ts";
import * as fs from "node:fs";
import * as path from "node:path";
import process from "node:process";
@@ -154,25 +156,30 @@ export function getMetadataPathSuffix(
// Path Detection Functions
// ============================================================================
/** Normalize path separators to forward slash for cross-platform matching */
function normalizeSep(p: string): string {
return p.replaceAll("\\", "/");
}
/**
* Check if a path is inside a flow folder
*/
export function isFlowPath(p: string): boolean {
return p.includes(getFolderSuffixes().flow + SEP);
return normalizeSep(p).includes(getFolderSuffixes().flow + "/");
}
/**
* Check if a path is inside an app folder
*/
export function isAppPath(p: string): boolean {
return p.includes(getFolderSuffixes().app + SEP);
return normalizeSep(p).includes(getFolderSuffixes().app + "/");
}
/**
* Check if a path is inside a raw_app folder
*/
export function isRawAppPath(p: string): boolean {
return p.includes(getFolderSuffixes().raw_app + SEP);
return normalizeSep(p).includes(getFolderSuffixes().raw_app + "/");
}
/**
@@ -248,10 +255,11 @@ export function extractResourceName(
p: string,
type: FolderResourceType
): string | null {
const suffix = getFolderSuffixes()[type] + SEP;
const index = p.indexOf(suffix);
const normalized = normalizeSep(p);
const suffix = getFolderSuffixes()[type] + "/";
const index = normalized.indexOf(suffix);
if (index === -1) return null;
return p.substring(0, index);
return normalized.substring(0, index);
}
/**
@@ -262,10 +270,11 @@ export function extractFolderPath(
p: string,
type: FolderResourceType
): string | null {
const suffix = getFolderSuffixes()[type] + SEP;
const index = p.indexOf(suffix);
const normalized = normalizeSep(p);
const suffix = getFolderSuffixes()[type] + "/";
const index = normalized.indexOf(suffix);
if (index === -1) return null;
return p.substring(0, index) + suffix;
return normalized.substring(0, index) + suffix;
}
/**
@@ -291,7 +300,7 @@ export function buildMetadataPath(
return (
resourceName +
getFolderSuffixes()[type] +
SEP +
"/" +
METADATA_FILES[type][format]
);
}
+1 -1
View File
@@ -1,4 +1,4 @@
import { Provider } from "../../deps.ts";
import { Provider } from "@cliffy/command/upgrade";
export type NpmProviderOptions = { main?: string; logger?: any } & (
| {
+23 -26
View File
@@ -2,8 +2,13 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck This file is copied from a JS project, so it's not type-safe.
import { colors, encodeHex, log, SEP } from "../../deps.ts";
import { colors } from "@cliffy/ansi/colors";
import { encodeHex } from "@std/encoding";
import * as log from "@std/log";
import { SEPARATOR as SEP } from "@std/path";
import crypto from "node:crypto";
import { readFileSync, writeFileSync } from "node:fs";
import { readdir, readFile } from "node:fs/promises";
import { fetchVersion } from "../core/context.ts";
import { updateGlobalVersions } from "../commands/sync/global.ts";
import { isRawAppPath } from "./resource_folders.ts";
@@ -86,7 +91,7 @@ export function deepEqual<T>(a: T, b: T): boolean {
}
export function getHeaders(): Record<string, string> | undefined {
const headers = Deno.env.get("HEADERS");
const headers = process.env["HEADERS"];
if (headers) {
const parsedHeaders = Object.fromEntries(
headers.split(",").map((h) => h.split(":").map((s) => s.trim()))
@@ -102,11 +107,12 @@ export function getHeaders(): Record<string, string> | undefined {
export async function digestDir(path: string, conf: string) {
const hashes: string = [];
for await (const e of Deno.readDir(path)) {
const entries = await readdir(path, { withFileTypes: true });
for (const e of entries) {
const npath = path + "/" + e.name;
if (e.isFile) {
hashes.push(await generateHashFromBuffer(await Deno.readFile(npath)));
} else if (e.isDirectory && !e.isSymlink) {
if (e.isFile()) {
hashes.push(await generateHashFromBuffer(await readFile(npath)));
} else if (e.isDirectory() && !e.isSymbolicLink()) {
hashes.push(await digestDir(npath, ""));
}
}
@@ -125,13 +131,9 @@ export async function generateHashFromBuffer(
return encodeHex(hashBuffer);
}
// export async function readInlinePath(path: string): Promise<string> {
// return await Deno.readTextFile(path.replaceAll("/", SEP));
// }
export function readInlinePathSync(path: string): string {
try {
return Deno.readTextFileSync(path.replaceAll("/", SEP));
return readFileSync(path.replaceAll("/", SEP), "utf-8");
} catch (error) {
log.warn(`Error reading inline path: ${path}, ${error}`);
return "";
@@ -161,13 +163,10 @@ export function isWorkspaceDependencies(path: string): boolean {
return path.startsWith("dependencies/");
}
export function printSync(input: string | Uint8Array, to = Deno.stdout) {
let bytesWritten = 0;
const bytes =
typeof input === "string" ? new TextEncoder().encode(input) : input;
while (bytesWritten < bytes.length) {
bytesWritten += to.writeSync(bytes.subarray(bytesWritten));
}
export function printSync(input: string | Uint8Array) {
process.stdout.write(
typeof input === "string" ? input : Buffer.from(input)
);
}
// Repository interface for shared selection logic
@@ -194,7 +193,7 @@ export async function selectRepository<T extends Repository>(
}
// Check if we're in a non-interactive environment
const isInteractive = Deno.stdin.isTerminal() && Deno.stdout.isTerminal();
const isInteractive = !!process.stdin.isTTY && !!process.stdout.isTTY;
if (!isInteractive) {
const repoPaths = repositories.map((r) =>
@@ -208,7 +207,7 @@ export async function selectRepository<T extends Repository>(
}
// Import Select dynamically to avoid dependency issues
const { Select } = await import("../../deps.ts");
const { Select } = await import("@cliffy/prompt/select");
console.log(
`\nMultiple repositories found. Please select which repository to ${
@@ -249,21 +248,19 @@ export async function getIsWin(): Promise<boolean> {
*/
export function writeIfChanged(path: string, content: string): boolean {
try {
const existing = Deno.readTextFileSync(path);
const existing = readFileSync(path, "utf-8");
if (existing === content) {
// console.log(`Content unchanged for ${path}`);
return false; // Content unchanged, skip write
}
} catch (error) {
} catch (error: any) {
// File doesn't exist or can't be read, proceed with write
if (!(error instanceof Deno.errors.NotFound)) {
if (error?.code !== "ENOENT") {
// If it's not a "not found" error, we might want to know about it
// but still proceed with the write attempt
}
}
// console.log(`Writing content to ${path}`);
Deno.writeTextFileSync(path, content);
writeFileSync(path, content, "utf-8");
return true; // File was written
}
+22
View File
@@ -0,0 +1,22 @@
import { parse as yamlParse, type ParseOptions } from "@std/yaml";
import { readFile } from "node:fs/promises";
export async function yamlParseFile(path: string, options: ParseOptions = {}) {
try {
return yamlParse(await readFile(path, "utf-8"), options);
} catch (e) {
throw new Error(`Error parsing yaml ${path}`, { cause: e });
}
}
export function yamlParseContent(
path: string,
content: string,
options: ParseOptions = {},
) {
try {
return yamlParse(content, options);
} catch (e) {
throw new Error(`Error parsing yaml ${path}`, { cause: e });
}
}
+140 -120
View File
@@ -8,11 +8,16 @@
* - Backend code compiled or ready to compile
*
* Usage:
* DATABASE_URL=postgres://postgres:changeme@localhost:5432 deno test --allow-all test/my_test.ts
* DATABASE_URL=postgres://postgres:changeme@localhost:5432 bun test test/my_test.ts
*/
import { ensureDir } from "https://deno.land/std@0.224.0/fs/mod.ts";
import { fromFileUrl, resolve, dirname } from "https://deno.land/std@0.224.0/path/mod.ts";
import { resolve, dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { statSync } from "node:fs";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { createServer } from "node:net";
import { Subprocess } from "bun";
export interface CargoBackendConfig {
/** PostgreSQL connection string (without database name) */
@@ -43,7 +48,7 @@ export interface CargoBackendConfig {
export class CargoBackend {
private config: Required<CargoBackendConfig>;
private process: Deno.ChildProcess | null = null;
private process: Subprocess | null = null;
private dbName: string;
private isRunning = false;
private actualPort: number;
@@ -58,19 +63,21 @@ export class CargoBackend {
// Determine default features based on environment
// CI mode: minimal features (zip only)
// Local mode: full features (zip, private, enterprise)
const isCI = Deno.env.get("CI_MINIMAL_FEATURES") === "true";
const defaultFeatures = isCI ? ["zip"] : ["zip", "private", "enterprise"];
// Local mode with license key: full features (zip, private, enterprise, license)
// Local mode without license key: zip only (EE features reject API calls without valid license)
const isCI = process.env["CI_MINIMAL_FEATURES"] === "true";
const hasLicenseKey = !!process.env["EE_LICENSE_KEY"];
const defaultFeatures = isCI ? ["zip"] : (hasLicenseKey ? ["zip", "private", "enterprise", "license"] : ["zip"]);
// Parse additional features from environment variable
const envFeatures = Deno.env.get("TEST_FEATURES")?.split(",").filter(f => f.trim()) || [];
const envFeatures = process.env["TEST_FEATURES"]?.split(",").filter(f => f.trim()) || [];
const allFeatures = [...new Set([...defaultFeatures, ...envFeatures, ...(config.features || [])])];
this.config = {
postgresUrl: config.postgresUrl || Deno.env.get("DATABASE_URL") || "postgres://postgres:changeme@localhost:5432",
postgresUrl: config.postgresUrl || process.env["DATABASE_URL"] || "postgres://postgres:changeme@localhost:5432",
port: config.port || 0,
backendDir,
binaryPath: config.binaryPath || Deno.env.get("WINDMILL_BINARY") || "",
binaryPath: config.binaryPath || process.env["WINDMILL_BINARY"] || "",
features: allFeatures,
release: config.release ?? false,
workspace: config.workspace || "test",
@@ -84,8 +91,7 @@ export class CargoBackend {
private findBackendDir(): string {
// Try to find backend directory relative to CLI
// Use fromFileUrl to properly handle Windows paths (e.g., file:///D:/...)
const cliTestDir = fromFileUrl(new URL(".", import.meta.url));
const cliTestDir = dirname(fileURLToPath(import.meta.url));
// Use resolve() for proper cross-platform path resolution
const candidates = [
resolve(cliTestDir, "..", "..", "backend"),
@@ -97,8 +103,8 @@ export class CargoBackend {
for (const candidate of candidates) {
try {
const cargoPath = resolve(candidate, "Cargo.toml");
const stat = Deno.statSync(cargoPath);
if (stat.isFile) {
const stat = statSync(cargoPath);
if (stat.isFile()) {
return candidate;
}
} catch {
@@ -129,19 +135,19 @@ export class CargoBackend {
return;
}
console.log("🚀 Starting Cargo-based Windmill backend...");
console.log("Starting Cargo-based Windmill backend...");
// Create test config directory
if (!this.config.testConfigDir) {
this.config.testConfigDir = await Deno.makeTempDir({ prefix: "wmill_test_config_" });
console.log(`📁 Created test config directory: ${this.config.testConfigDir}`);
this.config.testConfigDir = await mkdtemp(join(tmpdir(), "wmill_test_config_"));
console.log(`Created test config directory: ${this.config.testConfigDir}`);
}
// Find a free port if not specified
if (this.actualPort === 0) {
this.actualPort = await this.findFreePort();
}
console.log(`📡 Using port: ${this.actualPort}`);
console.log(`Using port: ${this.actualPort}`);
// Create the test database
await this.createDatabase();
@@ -156,7 +162,7 @@ export class CargoBackend {
await this.initializeAndAuthenticate();
this.isRunning = true;
console.log("Cargo backend is ready!");
console.log("Cargo backend is ready!");
console.log(` Server: ${this.baseUrl}`);
console.log(` Database: ${this.dbName}`);
console.log(` Workspace: ${this.config.workspace}`);
@@ -170,15 +176,15 @@ export class CargoBackend {
return;
}
console.log("🛑 Stopping Cargo backend...");
console.log("Stopping Cargo backend...");
// Kill the backend process
if (this.process) {
try {
this.process.kill("SIGTERM");
this.process.kill();
// Wait a bit for graceful shutdown
await Promise.race([
this.process.status,
this.process.exited,
new Promise(resolve => setTimeout(resolve, 5000)),
]);
} catch {
@@ -193,25 +199,29 @@ export class CargoBackend {
// Cleanup test config directory
if (this.config.testConfigDir?.includes("wmill_test_config_")) {
try {
await Deno.remove(this.config.testConfigDir, { recursive: true });
console.log(`🗑️ Cleaned up test config directory`);
await rm(this.config.testConfigDir, { recursive: true, force: true });
console.log(`Cleaned up test config directory`);
} catch {
// Ignore cleanup errors
}
}
this.isRunning = false;
console.log("Backend stopped");
console.log("Backend stopped");
}
/**
* Find a free port
*/
private async findFreePort(): Promise<number> {
const listener = Deno.listen({ port: 0 });
const port = (listener.addr as Deno.NetAddr).port;
listener.close();
return port;
return new Promise((resolve, reject) => {
const server = createServer();
server.listen(0, () => {
const port = (server.address() as any).port;
server.close(() => resolve(port));
});
server.on('error', reject);
});
}
/**
@@ -231,66 +241,65 @@ export class CargoBackend {
* Create the test database
*/
private async createDatabase(): Promise<void> {
console.log(`📦 Creating test database: ${this.dbName}`);
console.log(`Creating test database: ${this.dbName}`);
const baseUrl = this.getBasePostgresUrl();
const cmd = new Deno.Command("psql", {
args: [
`${baseUrl}/postgres`,
"-c",
`CREATE DATABASE "${this.dbName}";`,
],
stdout: "piped",
stderr: "piped",
const proc = Bun.spawn(["psql", `${baseUrl}/postgres`, "-c", `CREATE DATABASE "${this.dbName}";`], {
stdout: "pipe",
stderr: "pipe",
});
const result = await cmd.output();
if (result.code !== 0) {
const stderr = new TextDecoder().decode(result.stderr);
const [stdout, stderr] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
]);
const exitCode = await proc.exited;
if (exitCode !== 0) {
throw new Error(`Failed to create database: ${stderr}`);
}
console.log("Test database created");
console.log("Test database created");
}
/**
* Drop the test database
*/
private async dropDatabase(): Promise<void> {
console.log(`🗑️ Dropping test database: ${this.dbName}`);
console.log(`Dropping test database: ${this.dbName}`);
const baseUrl = this.getBasePostgresUrl();
// Terminate existing connections
const terminateCmd = new Deno.Command("psql", {
args: [
`${baseUrl}/postgres`,
"-c",
`SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '${this.dbName}' AND pid <> pg_backend_pid();`,
],
stdout: "piped",
stderr: "piped",
const terminateProc = Bun.spawn(["psql", `${baseUrl}/postgres`, "-c",
`SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '${this.dbName}' AND pid <> pg_backend_pid();`], {
stdout: "pipe",
stderr: "pipe",
});
await terminateCmd.output();
await Promise.all([
new Response(terminateProc.stdout).text(),
new Response(terminateProc.stderr).text(),
]);
await terminateProc.exited;
// Drop the database
const dropCmd = new Deno.Command("psql", {
args: [
`${baseUrl}/postgres`,
"-c",
`DROP DATABASE IF EXISTS "${this.dbName}";`,
],
stdout: "piped",
stderr: "piped",
const dropProc = Bun.spawn(["psql", `${baseUrl}/postgres`, "-c",
`DROP DATABASE IF EXISTS "${this.dbName}";`], {
stdout: "pipe",
stderr: "pipe",
});
const result = await dropCmd.output();
if (result.code !== 0) {
const stderr = new TextDecoder().decode(result.stderr);
const [, stderr] = await Promise.all([
new Response(dropProc.stdout).text(),
new Response(dropProc.stderr).text(),
]);
const exitCode = await dropProc.exited;
if (exitCode !== 0) {
console.warn(`Warning: Failed to drop database: ${stderr}`);
} else {
console.log("Test database dropped");
console.log("Test database dropped");
}
}
@@ -305,7 +314,7 @@ export class CargoBackend {
const databaseUrl = `${baseUrl}/${this.dbName}?sslmode=disable`;
const env: Record<string, string> = {
...Deno.env.toObject(),
...process.env as Record<string, string>,
DATABASE_URL: databaseUrl,
PORT: String(this.actualPort),
MODE: "standalone", // Run server + worker in one process
@@ -324,24 +333,28 @@ export class CargoBackend {
SUPERADMIN_PASSWORD: this.config.password,
};
// On Windows, ensure BUN_PATH and NODE_BIN_PATH are set for the worker.
// The Rust defaults (/usr/bin/bun, /usr/bin/node) don't exist on Windows.
if (process.platform === "win32") {
env.BUN_PATH = env.BUN_PATH || Bun.which("bun") || process.execPath;
env.NODE_BIN_PATH = env.NODE_BIN_PATH || Bun.which("node") || "node";
}
// Add license key if available
const licenseKey = Deno.env.get("EE_LICENSE_KEY");
const licenseKey = process.env["EE_LICENSE_KEY"];
if (licenseKey) {
env.LICENSE_KEY = licenseKey;
}
let cmd: Deno.Command;
if (this.config.binaryPath) {
// Use pre-built binary if explicitly specified
console.log(`🔧 Starting backend using binary: ${this.config.binaryPath}`);
console.log(`Starting backend using binary: ${this.config.binaryPath}`);
console.log(` DATABASE_URL: ${databaseUrl}`);
cmd = new Deno.Command(this.config.binaryPath, {
args: [],
this.process = Bun.spawn([this.config.binaryPath], {
env,
stdout: "piped",
stderr: "piped",
stdout: "pipe",
stderr: "pipe",
});
} else {
// Use cargo run with features
@@ -353,27 +366,25 @@ export class CargoBackend {
cargoArgs.push("--features", this.config.features.join(","));
}
console.log(`🔧 Starting backend via: cargo ${cargoArgs.join(" ")}`);
console.log(`Starting backend via: cargo ${cargoArgs.join(" ")}`);
console.log(` DATABASE_URL: ${databaseUrl}`);
console.log(` Backend dir: ${this.config.backendDir}`);
cmd = new Deno.Command("cargo", {
args: cargoArgs,
this.process = Bun.spawn(["cargo", ...cargoArgs], {
cwd: this.config.backendDir,
env,
stdout: "piped",
stderr: "piped",
stdout: "pipe",
stderr: "pipe",
});
}
this.process = cmd.spawn();
this.stderrChunks = [];
this.stdoutChunks = [];
// Capture output in background
this.captureProcessOutput();
console.log(`Backend process started (PID: ${this.process.pid})`);
console.log(`Backend process started (PID: ${this.process.pid})`);
}
/**
@@ -395,7 +406,7 @@ export class CargoBackend {
if (value) {
this.stdoutChunks.push(value);
if (this.config.verbose) {
Deno.stdout.writeSync(value);
process.stdout.write(value);
}
}
}
@@ -415,7 +426,7 @@ export class CargoBackend {
if (value) {
this.stderrChunks.push(value);
if (this.config.verbose) {
Deno.stderr.writeSync(value);
process.stderr.write(value);
}
}
}
@@ -458,7 +469,7 @@ export class CargoBackend {
* Wait for the API to be responsive
*/
private async waitForAPI(): Promise<void> {
console.log("Waiting for API to be responsive (this may take a few minutes if compiling)...");
console.log("Waiting for API to be responsive (this may take a few minutes if compiling)...");
// Allow up to 10 minutes for cargo build + startup
const maxAttempts = 300; // 10 minutes with 2-second intervals
@@ -473,7 +484,7 @@ export class CargoBackend {
if (response.ok) {
const version = await response.text();
console.log(`📡 API ready (version: ${version.trim()})`);
console.log(`API ready (version: ${version.trim()})`);
return;
}
await response.text(); // Consume response
@@ -485,7 +496,7 @@ export class CargoBackend {
if (this.process) {
try {
const status = await Promise.race([
this.process.status,
this.process.exited,
new Promise<null>(resolve => setTimeout(() => resolve(null), 100)),
]);
if (status !== null) {
@@ -493,14 +504,14 @@ export class CargoBackend {
await new Promise(resolve => setTimeout(resolve, 500));
const stderr = this.getStderr();
const stdout = this.getStdout();
console.error("\nBackend process crashed!");
console.error("\nBackend process crashed!");
if (stdout) {
console.error("=== STDOUT ===\n" + stdout.slice(-2000));
}
if (stderr) {
console.error("=== STDERR ===\n" + stderr.slice(-2000));
}
throw new Error(`Backend process exited with code ${status.code}`);
throw new Error(`Backend process exited with code ${status}`);
}
} catch (e) {
if (e instanceof Error && e.message.includes("exited")) {
@@ -529,7 +540,7 @@ export class CargoBackend {
* Initialize test data and authenticate
*/
private async initializeAndAuthenticate(): Promise<void> {
console.log("🔧 Initializing test workspace...");
console.log("Initializing test workspace...");
// Create test workspace via API
await this.createWorkspace();
@@ -537,7 +548,7 @@ export class CargoBackend {
// Login to get token
await this.authenticate();
console.log("Test workspace initialized");
console.log("Test workspace initialized");
}
/**
@@ -581,7 +592,7 @@ export class CargoBackend {
}
} else {
await createWsResponse.text();
console.log(` Created workspace: ${this.config.workspace}`);
console.log(` Created workspace: ${this.config.workspace}`);
}
}
@@ -589,7 +600,7 @@ export class CargoBackend {
* Authenticate and get token
*/
private async authenticate(): Promise<void> {
console.log("🔑 Authenticating...");
console.log("Authenticating...");
const loginResponse = await fetch(`${this.baseUrl}/api/auth/login`, {
method: "POST",
@@ -605,7 +616,7 @@ export class CargoBackend {
}
this.token = await loginResponse.text();
console.log("Authentication successful");
console.log("Authentication successful");
}
/**
@@ -618,8 +629,9 @@ export class CargoBackend {
/**
* Create CLI command with proper authentication
*/
createCLICommand(args: string[], workingDir: string, workspaceName?: string): Deno.Command {
createCLICommand(args: string[], workingDir: string, workspaceName?: string): { command: string, args: string[], cwd: string, env: Record<string, string> } {
const workspace = workspaceName || this.config.workspace;
const cliDir = join(dirname(fileURLToPath(import.meta.url)), "..");
const fullArgs = [
"--base-url", this.baseUrl,
"--workspace", workspace,
@@ -628,20 +640,21 @@ export class CargoBackend {
...args,
];
const denoPath = Deno.execPath();
const cliMainPath = fromFileUrl(new URL("../src/main.ts", import.meta.url));
const useNode = process.env["TEST_CLI_RUNTIME"] === "node";
const runtime = useNode ? "node" : "bun";
const entrypoint = useNode
? join(cliDir, "npm", "esm", "main.js")
: join(cliDir, "src", "main.ts");
const runtimeArgs = useNode ? [entrypoint] : ["run", entrypoint];
console.log("🔧 CLI Command:", [denoPath, "run", "-A", cliMainPath, ...fullArgs].join(" "));
console.log("CLI Command:", [runtime, ...runtimeArgs, ...fullArgs].join(" "));
return new Deno.Command(denoPath, {
args: ["run", "-A", cliMainPath, ...fullArgs],
return {
command: runtime,
args: [...runtimeArgs, ...fullArgs],
cwd: workingDir,
stdout: "piped",
stderr: "piped",
env: {
SKIP_DENO_DEPRECATION_WARNING: "true",
},
});
env: { ...process.env as Record<string, string> },
};
}
/**
@@ -653,13 +666,20 @@ export class CargoBackend {
code: number;
}> {
const cmd = this.createCLICommand(args, workingDir, workspaceName);
const result = await cmd.output();
const proc = Bun.spawn([cmd.command, ...cmd.args], {
cwd: cmd.cwd,
env: cmd.env,
stdout: "pipe",
stderr: "pipe",
});
return {
stdout: new TextDecoder().decode(result.stdout),
stderr: new TextDecoder().decode(result.stderr),
code: result.code,
};
const [stdout, stderr] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
]);
const code = await proc.exited;
return { stdout, stderr, code };
}
/**
@@ -677,7 +697,7 @@ export class CargoBackend {
* Reset workspace to clean state
*/
async reset(): Promise<void> {
console.log("🔄 Resetting workspace...");
console.log("Resetting workspace...");
// Delete all content via API
await Promise.all([
@@ -689,7 +709,7 @@ export class CargoBackend {
this.deleteAll("folders"),
]);
console.log("Workspace reset complete");
console.log("Workspace reset complete");
}
private async deleteAll(resourceType: string): Promise<void> {
@@ -731,13 +751,13 @@ export async function withCargoBackend<T>(
await globalCargoBackend.start();
}
const tempDir = await Deno.makeTempDir({ prefix: "windmill_cli_test_" });
const tempDir = await mkdtemp(join(tmpdir(), "windmill_cli_test_"));
try {
await globalCargoBackend.reset();
return await testFn(globalCargoBackend, tempDir);
} finally {
await Deno.remove(tempDir, { recursive: true });
await rm(tempDir, { recursive: true, force: true });
}
}
@@ -752,15 +772,15 @@ export async function cleanupCargoBackend(): Promise<void> {
}
/**
* Check if running in CI minimal mode (skip EE-dependent tests)
* Check if EE-dependent tests should be skipped
*
* When CI_MINIMAL_FEATURES=true:
* - Backend runs with only "zip" feature (no private/enterprise)
* - Tests requiring EE features should be skipped
* Returns true when:
* - CI_MINIMAL_FEATURES=true (CI mode with zip-only features)
* - EE_LICENSE_KEY is not set (EE features reject API calls without valid license)
*
* Use this in test definitions:
* ignore: shouldSkipOnCI()
* test.skipIf(shouldSkipOnCI())("my EE test", ...)
*/
export function shouldSkipOnCI(): boolean {
return Deno.env.get("CI_MINIMAL_FEATURES") === "true";
return process.env["CI_MINIMAL_FEATURES"] === "true" || !process.env["EE_LICENSE_KEY"];
}
@@ -16,76 +16,54 @@
* VERBOSE=1 deno test --allow-all test/cargo_backend_example.test.ts
*/
import {
assertEquals,
assertExists,
} from "https://deno.land/std@0.224.0/assert/mod.ts";
import { expect, test } from "bun:test";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { CargoBackend } from "./cargo_backend.ts";
// Single backend instance for all tests
let backend: CargoBackend;
// Setup before all tests
Deno.test({
name: "setup: start cargo backend",
fn: async () => {
test("setup: start cargo backend", async () => {
backend = new CargoBackend({
verbose: Deno.env.get("VERBOSE") === "1",
verbose: process.env.VERBOSE === "1",
});
await backend.start();
assertExists(backend.baseUrl);
assertExists(backend.authToken);
},
sanitizeResources: false,
sanitizeOps: false,
expect(backend.baseUrl).toBeDefined();
expect(backend.authToken).toBeDefined();
});
Deno.test({
name: "API: version endpoint responds",
fn: async () => {
test("API: version endpoint responds", async () => {
const response = await fetch(`${backend.baseUrl}/api/version`);
assertEquals(response.ok, true);
expect(response.ok).toEqual(true);
const version = await response.text();
assertExists(version);
expect(version).toBeDefined();
console.log(` Backend version: ${version.trim()}`);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "API: workspace exists",
fn: async () => {
test("API: workspace exists", async () => {
const response = await backend.apiRequest(
`/api/w/${backend.workspace}/workspaces/get_settings`,
);
assertEquals(response.ok, true);
expect(response.ok).toEqual(true);
await response.text();
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "CLI: wmill --version works",
fn: async () => {
const tempDir = await Deno.makeTempDir({ prefix: "wmill_test_" });
test("CLI: wmill --version works", async () => {
const tempDir = await mkdtemp(join(tmpdir(), "wmill_test_"));
try {
const result = await backend.runCLICommand(["--version"], tempDir);
assertEquals(result.code, 0);
expect(result.code).toEqual(0);
console.log(` CLI version: ${result.stdout.trim()}`);
} finally {
await Deno.remove(tempDir, { recursive: true });
await rm(tempDir, { recursive: true });
}
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "CLI: wmill sync pull works",
fn: async () => {
const tempDir = await Deno.makeTempDir({ prefix: "wmill_test_" });
test("CLI: wmill sync pull works", async () => {
const tempDir = await mkdtemp(join(tmpdir(), "wmill_test_"));
try {
const result = await backend.runCLICommand(
["sync", "pull", "--yes"],
@@ -97,19 +75,11 @@ Deno.test({
console.log(` stderr: ${result.stderr.slice(0, 200)}`);
}
} finally {
await Deno.remove(tempDir, { recursive: true });
await rm(tempDir, { recursive: true });
}
},
sanitizeResources: false,
sanitizeOps: false,
});
// Cleanup after all tests
Deno.test({
name: "cleanup: stop cargo backend",
fn: async () => {
test("cleanup: stop cargo backend", async () => {
await backend.stop();
},
sanitizeResources: false,
sanitizeOps: false,
});
+31 -31
View File
@@ -1,4 +1,4 @@
import { assertEquals, assertExists } from "https://deno.land/std@0.224.0/assert/mod.ts";
import { expect, test } from "bun:test";
import { getEffectiveSettings, type SyncOptions } from "../src/core/conf.ts";
// =============================================================================
@@ -6,7 +6,7 @@ import { getEffectiveSettings, type SyncOptions } from "../src/core/conf.ts";
// Tests for getEffectiveSettings with branchOverride parameter
// =============================================================================
Deno.test("getEffectiveSettings: applies branch overrides when branchOverride is provided", async () => {
test("getEffectiveSettings: applies branch overrides when branchOverride is provided", async () => {
const config: SyncOptions = {
defaultTs: "bun",
includes: ["f/**"],
@@ -28,18 +28,18 @@ Deno.test("getEffectiveSettings: applies branch overrides when branchOverride is
// Test with staging branch override
const stagingSettings = await getEffectiveSettings(config, undefined, true, true, "staging");
assertEquals(stagingSettings.includes, ["staging/**"]);
assertEquals(stagingSettings.skipVariables, true);
assertEquals(stagingSettings.skipSecrets, undefined);
expect(stagingSettings.includes).toEqual(["staging/**"]);
expect(stagingSettings.skipVariables).toEqual(true);
expect(stagingSettings.skipSecrets).toEqual(undefined);
// Test with production branch override
const prodSettings = await getEffectiveSettings(config, undefined, true, true, "production");
assertEquals(prodSettings.includes, ["prod/**"]);
assertEquals(prodSettings.skipSecrets, true);
assertEquals(prodSettings.skipVariables, undefined);
expect(prodSettings.includes).toEqual(["prod/**"]);
expect(prodSettings.skipSecrets).toEqual(true);
expect(prodSettings.skipVariables).toEqual(undefined);
});
Deno.test("getEffectiveSettings: uses top-level settings when branchOverride has no overrides", async () => {
test("getEffectiveSettings: uses top-level settings when branchOverride has no overrides", async () => {
const config: SyncOptions = {
defaultTs: "bun",
includes: ["f/**"],
@@ -52,12 +52,12 @@ Deno.test("getEffectiveSettings: uses top-level settings when branchOverride has
};
const settings = await getEffectiveSettings(config, undefined, true, true, "staging");
assertEquals(settings.includes, ["f/**"]);
assertEquals(settings.skipVariables, true);
assertEquals(settings.defaultTs, "bun");
expect(settings.includes).toEqual(["f/**"]);
expect(settings.skipVariables).toEqual(true);
expect(settings.defaultTs).toEqual("bun");
});
Deno.test("getEffectiveSettings: uses top-level settings for unknown branch", async () => {
test("getEffectiveSettings: uses top-level settings for unknown branch", async () => {
const config: SyncOptions = {
defaultTs: "bun",
includes: ["f/**"],
@@ -71,11 +71,11 @@ Deno.test("getEffectiveSettings: uses top-level settings for unknown branch", as
};
const settings = await getEffectiveSettings(config, undefined, true, true, "nonexistent");
assertEquals(settings.includes, ["f/**"]);
assertEquals(settings.defaultTs, "bun");
expect(settings.includes).toEqual(["f/**"]);
expect(settings.defaultTs).toEqual("bun");
});
Deno.test("getEffectiveSettings: promotionOverrides take precedence when promotion specified", async () => {
test("getEffectiveSettings: promotionOverrides take precedence when promotion specified", async () => {
const config: SyncOptions = {
defaultTs: "bun",
includes: ["f/**"],
@@ -94,16 +94,16 @@ Deno.test("getEffectiveSettings: promotionOverrides take precedence when promoti
// Test without promotion flag - should use regular overrides
const normalSettings = await getEffectiveSettings(config, undefined, true, true, "production");
assertEquals(normalSettings.includes, ["prod/**"]);
assertEquals(normalSettings.skipVariables, undefined);
expect(normalSettings.includes).toEqual(["prod/**"]);
expect(normalSettings.skipVariables).toEqual(undefined);
// Test with promotion flag - should use promotionOverrides
const promoSettings = await getEffectiveSettings(config, "production", true, true);
assertEquals(promoSettings.includes, ["promoted/**"]);
assertEquals(promoSettings.skipVariables, true);
expect(promoSettings.includes).toEqual(["promoted/**"]);
expect(promoSettings.skipVariables).toEqual(true);
});
Deno.test("getEffectiveSettings: branchOverride works without gitBranches config", async () => {
test("getEffectiveSettings: branchOverride works without gitBranches config", async () => {
const config: SyncOptions = {
defaultTs: "bun",
includes: ["f/**"],
@@ -111,11 +111,11 @@ Deno.test("getEffectiveSettings: branchOverride works without gitBranches config
// Should not throw even with branchOverride but no gitBranches
const settings = await getEffectiveSettings(config, undefined, true, true, "staging");
assertEquals(settings.includes, ["f/**"]);
assertEquals(settings.defaultTs, "bun");
expect(settings.includes).toEqual(["f/**"]);
expect(settings.defaultTs).toEqual("bun");
});
Deno.test("getEffectiveSettings: preserves all top-level settings in merged result", async () => {
test("getEffectiveSettings: preserves all top-level settings in merged result", async () => {
const config: SyncOptions = {
defaultTs: "bun",
includes: ["f/**"],
@@ -134,11 +134,11 @@ Deno.test("getEffectiveSettings: preserves all top-level settings in merged resu
};
const settings = await getEffectiveSettings(config, undefined, true, true, "staging");
assertEquals(settings.defaultTs, "bun");
assertEquals(settings.includes, ["f/**"]);
assertEquals(settings.excludes, ["*.test.ts"]);
assertEquals(settings.skipVariables, true); // Overridden
assertEquals(settings.skipResources, false);
assertEquals(settings.skipFlows, false);
assertEquals(settings.parallel, 4);
expect(settings.defaultTs).toEqual("bun");
expect(settings.includes).toEqual(["f/**"]);
expect(settings.excludes).toEqual(["*.test.ts"]);
expect(settings.skipVariables).toEqual(true); // Overridden
expect(settings.skipResources).toEqual(false);
expect(settings.skipFlows).toEqual(false);
expect(settings.parallel).toEqual(4);
});
+69 -75
View File
@@ -3,6 +3,25 @@
* Manages real Windmill EE backend containers for CLI testing
*/
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
async function runCommand(cmd: string, args: string[], opts?: { cwd?: string, env?: Record<string, string> }): Promise<{ code: number, stdout: string, stderr: string }> {
const proc = Bun.spawn([cmd, ...args], {
stdout: 'pipe',
stderr: 'pipe',
cwd: opts?.cwd,
env: { ...process.env, ...opts?.env },
});
const [code, stdout, stderr] = await Promise.all([
proc.exited,
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
]);
return { code, stdout, stderr };
}
export interface ContainerConfig {
composeFile?: string;
baseUrl?: string;
@@ -71,25 +90,19 @@ export class ContainerizedBackend {
// Create isolated test config directory if not provided
if (!this.config.testConfigDir) {
this.config.testConfigDir = await Deno.makeTempDir({ prefix: 'wmill_test_config_' });
this.config.testConfigDir = await mkdtemp(join(tmpdir(), 'wmill_test_config_'));
console.log(`📁 Created test config directory: ${this.config.testConfigDir}`);
}
// Start containers with EE license key
const startCmd = new Deno.Command('docker', {
args: ['compose', '-f', this.config.composeFile, 'up', '-d'],
stdout: 'piped',
stderr: 'piped',
const startResult = await runCommand('docker', ['compose', '-f', this.config.composeFile, 'up', '-d'], {
env: {
...Deno.env.toObject(),
...(Deno.env.get('EE_LICENSE_KEY') && { EE_LICENSE_KEY: Deno.env.get('EE_LICENSE_KEY')! })
...(process.env.EE_LICENSE_KEY && { EE_LICENSE_KEY: process.env.EE_LICENSE_KEY })
}
});
const startResult = await startCmd.output();
if (startResult.code !== 0) {
const stderr = new TextDecoder().decode(startResult.stderr);
throw new Error(`Failed to start containers: ${stderr}`);
throw new Error(`Failed to start containers: ${startResult.stderr}`);
}
// Wait for services to be healthy
@@ -117,22 +130,16 @@ export class ContainerizedBackend {
console.log('🛑 Stopping containerized backend...');
const stopCmd = new Deno.Command('docker', {
args: ['compose', '-f', this.config.composeFile, 'down', '-v'],
stdout: 'piped',
stderr: 'piped',
await runCommand('docker', ['compose', '-f', this.config.composeFile, 'down', '-v'], {
env: {
...Deno.env.toObject(),
...(Deno.env.get('EE_LICENSE_KEY') && { EE_LICENSE_KEY: Deno.env.get('EE_LICENSE_KEY')! })
...(process.env.EE_LICENSE_KEY && { EE_LICENSE_KEY: process.env.EE_LICENSE_KEY })
}
});
await stopCmd.output();
// Clean up test config directory if we created it
if (this.config.testConfigDir && this.config.testConfigDir.includes('wmill_test_config_')) {
try {
await Deno.remove(this.config.testConfigDir, { recursive: true });
await rm(this.config.testConfigDir, { recursive: true });
console.log(`🗑️ Cleaned up test config directory: ${this.config.testConfigDir}`);
} catch (error) {
console.warn(`⚠️ Failed to clean up test config directory: ${error}`);
@@ -1013,7 +1020,7 @@ export async function main(
/**
* Create CLI command with proper authentication
*/
createCLICommand(args: string[], workingDir: string, workspaceName?: string): Deno.Command {
createCLICommand(args: string[], workingDir: string, workspaceName?: string): { cmd: string[], cwd: string } {
const workspace = workspaceName || this.config.workspace;
const fullArgs = [
'--base-url', this.config.baseUrl,
@@ -1022,21 +1029,21 @@ export async function main(
'--config-dir', this.config.testConfigDir,
...args
];
const denoPath = Deno.execPath();
const cliMainPath = new URL('../src/main.ts', import.meta.url).pathname;
console.log('🔧 CLI Command:', [denoPath, 'run', '-A', cliMainPath, ...fullArgs].join(' '));
const useNode = process.env["TEST_CLI_RUNTIME"] === "node";
const cliDir = new URL('..', import.meta.url).pathname;
const entrypoint = useNode
? new URL('../npm/esm/main.js', import.meta.url).pathname
: new URL('../src/main.ts', import.meta.url).pathname;
const runtime = useNode ? 'node' : 'bun';
const runtimeArgs = useNode ? [entrypoint] : ['run', entrypoint];
return new Deno.Command(denoPath, {
args: ['run', '-A', cliMainPath, ...fullArgs],
console.log('CLI Command:', [runtime, ...runtimeArgs, ...fullArgs].join(' '));
return {
cmd: [runtime, ...runtimeArgs, ...fullArgs],
cwd: workingDir,
stdout: 'piped',
stderr: 'piped',
env: {
'SKIP_DENO_DEPRECATION_WARNING': 'true'
}
});
};
}
/**
@@ -1047,14 +1054,18 @@ export async function main(
stderr: string;
code: number;
}> {
const cmd = this.createCLICommand(args, workingDir, workspaceName);
const result = await cmd.output();
return {
stdout: new TextDecoder().decode(result.stdout),
stderr: new TextDecoder().decode(result.stderr),
code: result.code
};
const { cmd, cwd } = this.createCLICommand(args, workingDir, workspaceName);
const proc = Bun.spawn(cmd, {
stdout: 'pipe',
stderr: 'pipe',
cwd,
});
const [code, stdout, stderr] = await Promise.all([
proc.exited,
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
]);
return { stdout, stderr, code };
}
/**
@@ -1192,42 +1203,30 @@ export async function main(
ON CONFLICT (workspace_id, kind) DO UPDATE SET key = EXCLUDED.key;
`;
const execCmd = new Deno.Command('docker', {
args: ['compose', '-f', this.config.composeFile, 'exec', '-T', 'test_db',
'psql', '-U', 'postgres', '-d', 'windmill_test', '-c', initSQL],
stdout: 'piped',
stderr: 'piped',
const result = await runCommand('docker', ['compose', '-f', this.config.composeFile, 'exec', '-T', 'test_db',
'psql', '-U', 'postgres', '-d', 'windmill_test', '-c', initSQL], {
env: {
...Deno.env.toObject(),
...(Deno.env.get('EE_LICENSE_KEY') && { EE_LICENSE_KEY: Deno.env.get('EE_LICENSE_KEY')! })
...(process.env.EE_LICENSE_KEY && { EE_LICENSE_KEY: process.env.EE_LICENSE_KEY })
}
});
const result = await execCmd.output();
if (result.code !== 0) {
const stderr = new TextDecoder().decode(result.stderr);
throw new Error(`Failed to initialize test data: ${stderr}`);
throw new Error(`Failed to initialize test data: ${result.stderr}`);
}
console.log('✅ Test workspace initialized');
// Verify license key was stored
const checkLicenseCmd = new Deno.Command('docker', {
args: ['compose', '-f', this.config.composeFile, 'exec', '-T', 'test_db',
'psql', '-U', 'postgres', '-d', 'windmill_test', '-c',
"SELECT name, value FROM global_settings WHERE name = 'license_key';"],
stdout: 'piped',
stderr: 'piped',
const checkResult = await runCommand('docker', ['compose', '-f', this.config.composeFile, 'exec', '-T', 'test_db',
'psql', '-U', 'postgres', '-d', 'windmill_test', '-c',
"SELECT name, value FROM global_settings WHERE name = 'license_key';"], {
env: {
...Deno.env.toObject(),
EE_LICENSE_KEY: Deno.env.get('EE_LICENSE_KEY') || 'REMOVED_HARDCODED_LICENSE'
EE_LICENSE_KEY: process.env.EE_LICENSE_KEY || 'REMOVED_HARDCODED_LICENSE'
}
});
const checkResult = await checkLicenseCmd.output();
if (checkResult.code === 0) {
const output = new TextDecoder().decode(checkResult.stdout);
console.log('🔍 License key in database:', output.trim());
console.log('License key in database:', checkResult.stdout.trim());
}
}
@@ -1240,19 +1239,14 @@ export async function main(
let attempts = 0;
while (attempts < maxAttempts) {
const healthCmd = new Deno.Command('docker', {
args: ['compose', '-f', this.config.composeFile, 'ps', '--format', 'json'],
stdout: 'piped',
stderr: 'piped',
const result = await runCommand('docker', ['compose', '-f', this.config.composeFile, 'ps', '--format', 'json'], {
env: {
...Deno.env.toObject(),
...(Deno.env.get('EE_LICENSE_KEY') && { EE_LICENSE_KEY: Deno.env.get('EE_LICENSE_KEY')! })
...(process.env.EE_LICENSE_KEY && { EE_LICENSE_KEY: process.env.EE_LICENSE_KEY })
}
});
const result = await healthCmd.output();
if (result.code === 0) {
const output = new TextDecoder().decode(result.stdout);
const output = result.stdout;
if (output.trim()) {
const containers = output.trim().split('\n').map(line => JSON.parse(line));
@@ -1345,15 +1339,15 @@ export async function withContainerizedBackend<T>(
}
}
const tempDir = await Deno.makeTempDir({ prefix: 'windmill_cli_test_' });
const tempDir = await mkdtemp(join(tmpdir(), 'windmill_cli_test_'));
try {
await globalBackend.reset();
await globalBackend.seedTestData();
return await testFn(globalBackend, tempDir);
} finally {
await Deno.remove(tempDir, { recursive: true });
await rm(tempDir, { recursive: true });
}
}
+417
View File
@@ -0,0 +1,417 @@
/**
* Dev Server Smoke Tests
*
* Tests for `wmill dev` and `wmill app dev` commands.
* Verifies server startup, WebSocket connectivity, and file-change broadcasting.
*
* Run with:
* bun test test/dev_server.test.ts
*/
import { expect, test } from "bun:test";
import { writeFile, mkdir } from "node:fs/promises";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { createServer } from "node:net";
import { Subprocess } from "bun";
import WebSocket from "ws";
import { withTestBackend } from "./test_backend.ts";
/** Find a free port by binding to port 0 */
async function findFreePort(): Promise<number> {
return new Promise((resolve, reject) => {
const server = createServer();
server.listen(0, () => {
const port = (server.address() as any).port;
server.close(() => resolve(port));
});
server.on("error", reject);
});
}
/** Wait for a condition with timeout */
async function waitFor<T>(
fn: () => T | Promise<T>,
timeoutMs: number,
label: string,
): Promise<T> {
const deadline = Date.now() + timeoutMs;
let lastError: unknown;
while (Date.now() < deadline) {
try {
const result = await fn();
if (result) return result;
} catch (e) {
lastError = e;
}
await new Promise((r) => setTimeout(r, 200));
}
throw new Error(`Timed out waiting for: ${label} (after ${timeoutMs}ms). Last error: ${lastError}`);
}
/** Get CLI main.ts path */
function getCLIMainPath(): string {
return join(dirname(fileURLToPath(import.meta.url)), "..", "src", "main.ts");
}
// =============================================================================
// TEST 1: `wmill dev` smoke test
// =============================================================================
test(
"wmill dev: starts server, broadcasts file changes over WebSocket",
async () => {
await withTestBackend(async (backend, tempDir) => {
// Create wmill.yaml config
await writeFile(
join(tempDir, "wmill.yaml"),
"defaultTs: bun\n",
"utf-8",
);
// Create a script file
const scriptDir = join(tempDir, "f", "test");
await mkdir(scriptDir, { recursive: true });
await writeFile(
join(scriptDir, "hello.ts"),
'export function main() { return "hello"; }\n',
"utf-8",
);
await writeFile(
join(scriptDir, "hello.script.yaml"),
`summary: "test"\ndescription: ""\nlock: ""\nschema:\n $schema: "https://json-schema.org/draft/2020-12/schema"\n type: object\n properties: {}\n required: []\n`,
"utf-8",
);
// Push the script so the workspace has content
const pushResult = await backend.runCLICommand(
["sync", "push", "--yes"],
tempDir,
);
if (pushResult.code !== 0) {
console.error("Push stderr:", pushResult.stderr);
console.error("Push stdout:", pushResult.stdout);
}
expect(pushResult.code).toEqual(0);
// Build the CLI command for `wmill dev`
const cliMainPath = getCLIMainPath();
const args = [
"run",
cliMainPath,
"--base-url",
backend.baseUrl,
"--workspace",
backend.workspace,
"--token",
backend.token!,
"--config-dir",
backend.testConfigDir,
"dev",
];
let proc: Subprocess | null = null;
let ws: WebSocket | null = null;
try {
// Spawn wmill dev as background process
proc = Bun.spawn(["bun", ...args], {
cwd: tempDir,
stdout: "pipe",
stderr: "pipe",
env: { ...process.env },
});
// Read stdout to find the port
const stdoutReader = proc.stdout.getReader();
let stdoutBuffer = "";
let port: number | null = null;
// Wait for "Server listening on port XXXX" message
const portMatch = await waitFor(
async () => {
try {
const { done, value } = await Promise.race([
stdoutReader.read(),
new Promise<{ done: true; value: undefined }>((r) =>
setTimeout(() => r({ done: true, value: undefined }), 500),
),
]);
if (!done && value) {
stdoutBuffer += new TextDecoder().decode(value);
}
} catch {
// Reader may be exhausted
}
const match = stdoutBuffer.match(
/Server listening on port (\d+)/,
);
return match;
},
30000,
"dev server to start",
);
port = parseInt(portMatch[1], 10);
expect(port).toBeGreaterThan(0);
stdoutReader.releaseLock();
// Connect WebSocket
ws = new WebSocket(`ws://localhost:${port}`);
// Wait for connection to open
await new Promise<void>((resolve, reject) => {
const timeout = setTimeout(
() => reject(new Error("WebSocket connection timeout")),
5000,
);
ws!.on("open", () => {
clearTimeout(timeout);
resolve();
});
ws!.on("error", (err) => {
clearTimeout(timeout);
reject(err);
});
});
expect(ws.readyState).toEqual(WebSocket.OPEN);
// Set up a promise to receive the next WebSocket message
const isWindows = process.platform === "win32";
const messagePromise = new Promise<any>((resolve, reject) => {
const timeout = setTimeout(
() => reject(new Error("WebSocket message timeout")),
isWindows ? 30000 : 10000,
);
ws!.on("message", (data) => {
clearTimeout(timeout);
try {
resolve(JSON.parse(data.toString()));
} catch (e) {
reject(e);
}
});
});
// Modify the script file on disk
// Windows fs.watch() needs more time to initialize with recursive: true
await new Promise((r) => setTimeout(r, isWindows ? 2000 : 300));
await writeFile(
join(scriptDir, "hello.ts"),
'export function main() { return "modified"; }\n',
"utf-8",
);
// Wait for WebSocket message
const message = await messagePromise;
// Verify the message
expect(message.type).toEqual("script");
expect(message.content).toContain("modified");
expect(message.path).toContain("f/test/hello");
expect(message.language).toBeTruthy();
} finally {
if (ws) {
ws.close();
}
if (proc) {
proc.kill();
await proc.exited;
}
}
});
},
{ timeout: 60000 },
);
// =============================================================================
// TEST 2: `wmill app dev` smoke test
// =============================================================================
test(
"wmill app dev: starts HTTP server, serves HTML, provides SSE endpoint",
async () => {
await withTestBackend(async (backend, tempDir) => {
// Create wmill.yaml config
await writeFile(
join(tempDir, "wmill.yaml"),
"defaultTs: bun\n",
"utf-8",
);
// Create a raw app directory with the right suffix
const appDir = join(tempDir, "f", "test", "myapp.raw_app");
await mkdir(appDir, { recursive: true });
// Create raw_app.yaml
await writeFile(
join(appDir, "raw_app.yaml"),
`custom_path: f/test/myapp\n`,
"utf-8",
);
// Create package.json (minimal, with react dependency)
await writeFile(
join(appDir, "package.json"),
JSON.stringify(
{
name: "test-app",
private: true,
dependencies: {
react: "^18.0.0",
"react-dom": "^18.0.0",
},
},
null,
2,
),
"utf-8",
);
// Create index.tsx entry point
await writeFile(
join(appDir, "index.tsx"),
`import React from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
const root = createRoot(document.getElementById("root")!);
root.render(<App />);
`,
"utf-8",
);
// Create App.tsx
await writeFile(
join(appDir, "App.tsx"),
`import React from "react";
export default function App() {
return <div>Hello from test app</div>;
}
`,
"utf-8",
);
// Run npm install in the app directory
const npmInstall = Bun.spawn(["npm", "install"], {
cwd: appDir,
stdout: "pipe",
stderr: "pipe",
});
await Promise.all([
new Response(npmInstall.stdout).text(),
new Response(npmInstall.stderr).text(),
]);
const npmExitCode = await npmInstall.exited;
expect(npmExitCode).toEqual(0);
// Find a free port
const port = await findFreePort();
// Build the CLI command for `wmill app dev`
const cliMainPath = getCLIMainPath();
const args = [
"run",
cliMainPath,
"--base-url",
backend.baseUrl,
"--workspace",
backend.workspace,
"--token",
backend.token!,
"--config-dir",
backend.testConfigDir,
"app",
"dev",
appDir,
"--no-open",
"--port",
String(port),
];
let proc: Subprocess | null = null;
try {
// Spawn wmill app dev as background process
proc = Bun.spawn(["bun", ...args], {
cwd: tempDir,
stdout: "pipe",
stderr: "pipe",
env: { ...process.env },
});
// Collect stderr in background for debugging
const stderrReader = proc.stderr.getReader();
let stderrBuffer = "";
(async () => {
try {
while (true) {
const { done, value } = await stderrReader.read();
if (done) break;
stderrBuffer += new TextDecoder().decode(value);
}
} catch {
// Process may have exited
}
})();
// Wait for server to be ready by polling the HTTP endpoint
await waitFor(
async () => {
try {
const res = await fetch(`http://localhost:${port}/`, {
signal: AbortSignal.timeout(1000),
});
if (res.ok) {
await res.text();
return true;
}
await res.text();
} catch {
// Not ready yet
}
return false;
},
60000,
"app dev server to be ready",
);
// Verify GET / returns HTML
const htmlRes = await fetch(`http://localhost:${port}/`);
const contentType = htmlRes.headers.get("content-type");
const htmlBody = await htmlRes.text();
expect(contentType).toContain("text/html");
expect(htmlBody).toContain("<!DOCTYPE html>");
expect(htmlBody).toContain("<div id=\"root\">");
// Verify GET /__events returns SSE stream
const controller = new AbortController();
const sseTimeout = setTimeout(() => controller.abort(), 5000);
try {
const sseRes = await fetch(`http://localhost:${port}/__events`, {
signal: controller.signal,
});
const sseContentType = sseRes.headers.get("content-type");
expect(sseContentType).toContain("text/event-stream");
// Read a small chunk to verify SSE sends data
const reader = sseRes.body!.getReader();
const { value } = await reader.read();
const chunk = new TextDecoder().decode(value);
expect(chunk).toContain("data: connected");
reader.cancel();
} finally {
clearTimeout(sseTimeout);
}
} finally {
if (proc) {
proc.kill();
await proc.exited;
}
}
});
},
{ timeout: 120000 },
);
@@ -1,4 +1,4 @@
import { assertEquals } from "https://deno.land/std@0.224.0/assert/mod.ts";
import { expect, test } from "bun:test";
// Import the function we need to test
import { elementsToMap } from "../src/commands/sync/sync.ts";
@@ -63,7 +63,7 @@ const defaultSkips = {};
// REGRESSION TEST: Remote base files should NOT be skipped
// =============================================================================
Deno.test("elementsToMap: remote base file is NOT skipped when configured as branch-specific (isRemote=true)", async () => {
test("elementsToMap: remote base file is NOT skipped when configured as branch-specific (isRemote=true)", async () => {
// This is the key regression test.
// When pulling from remote, the workspace only has base paths (e.g., TestVar.variable.yaml)
// These should NOT be skipped even if configured as branch-specific, because the remote
@@ -94,14 +94,10 @@ Deno.test("elementsToMap: remote base file is NOT skipped when configured as bra
);
// The base file should be in the map
assertEquals(
Object.keys(result).includes("f/Shared/Variable/TestVar.variable.yaml"),
true,
"Remote base file should NOT be skipped when isRemote=true"
);
expect(Object.keys(result).includes("f/Shared/Variable/TestVar.variable.yaml")).toEqual(true);
});
Deno.test("elementsToMap: local base file IS skipped when configured as branch-specific (isRemote=false)", async () => {
test("elementsToMap: local base file IS skipped when configured as branch-specific (isRemote=false)", async () => {
// When processing local files, if a base file is configured as branch-specific,
// it should be skipped because we expect the branch-specific version to be used instead.
@@ -130,14 +126,10 @@ Deno.test("elementsToMap: local base file IS skipped when configured as branch-s
);
// The base file should NOT be in the map (skipped because branch-specific expected)
assertEquals(
Object.keys(result).includes("f/Shared/Variable/TestVar.variable.yaml"),
false,
"Local base file SHOULD be skipped when isRemote=false and configured as branch-specific"
);
expect(Object.keys(result).includes("f/Shared/Variable/TestVar.variable.yaml")).toEqual(false);
});
Deno.test("elementsToMap: local branch-specific file is mapped to base path (isRemote=false)", async () => {
test("elementsToMap: local branch-specific file is mapped to base path (isRemote=false)", async () => {
// When processing local files with branch-specific naming, they should be mapped to base paths
const config: SpecificItemsConfig = {
@@ -164,15 +156,8 @@ Deno.test("elementsToMap: local branch-specific file is mapped to base path (isR
);
// The branch-specific file should be mapped to the base path
assertEquals(
Object.keys(result).includes("f/Shared/Variable/TestVar.variable.yaml"),
true,
"Branch-specific file should be mapped to base path"
);
assertEquals(
result["f/Shared/Variable/TestVar.variable.yaml"],
"value: staging-test\nis_secret: false",
);
expect(Object.keys(result).includes("f/Shared/Variable/TestVar.variable.yaml")).toEqual(true);
expect(result["f/Shared/Variable/TestVar.variable.yaml"]).toEqual("value: staging-test\nis_secret: false");
});
// =============================================================================
@@ -183,7 +168,7 @@ Deno.test("elementsToMap: local branch-specific file is mapped to base path (isR
// - Expected: No deletion, the files should match
// =============================================================================
Deno.test("elementsToMap: pull scenario - remote and local maps should align correctly", async () => {
test("elementsToMap: pull scenario - remote and local maps should align correctly", async () => {
const config: SpecificItemsConfig = {
variables: ["f/Shared/Variable/**"],
};
@@ -233,23 +218,15 @@ Deno.test("elementsToMap: pull scenario - remote and local maps should align cor
const remoteKeys = Object.keys(remoteMap);
const localKeys = Object.keys(localMap);
assertEquals(
remoteKeys.includes("f/Shared/Variable/TestVar.variable.yaml"),
true,
"Remote map should include base path"
);
assertEquals(
localKeys.includes("f/Shared/Variable/TestVar.variable.yaml"),
true,
"Local map should include base path (mapped from branch-specific)"
);
expect(remoteKeys.includes("f/Shared/Variable/TestVar.variable.yaml")).toEqual(true);
expect(localKeys.includes("f/Shared/Variable/TestVar.variable.yaml")).toEqual(true);
});
// =============================================================================
// NON-CONFIGURED ITEMS: Should work the same regardless of isRemote
// =============================================================================
Deno.test("elementsToMap: non-configured items included regardless of isRemote", async () => {
test("elementsToMap: non-configured items included regardless of isRemote", async () => {
const config: SpecificItemsConfig = {
variables: ["f/Other/**"], // Only "Other" folder is branch-specific
};
@@ -286,23 +263,15 @@ Deno.test("elementsToMap: non-configured items included regardless of isRemote",
);
// Both should include the file since it's not in the branch-specific config
assertEquals(
Object.keys(remoteResult).includes("f/Shared/Variable/TestVar.variable.yaml"),
true,
"Non-configured item should be included when isRemote=true"
);
assertEquals(
Object.keys(localResult).includes("f/Shared/Variable/TestVar.variable.yaml"),
true,
"Non-configured item should be included when isRemote=false"
);
expect(Object.keys(remoteResult).includes("f/Shared/Variable/TestVar.variable.yaml")).toEqual(true);
expect(Object.keys(localResult).includes("f/Shared/Variable/TestVar.variable.yaml")).toEqual(true);
});
// =============================================================================
// RESOURCE TYPE TESTS
// =============================================================================
Deno.test("elementsToMap: remote resource base file not skipped when configured", async () => {
test("elementsToMap: remote resource base file not skipped when configured", async () => {
const config: SpecificItemsConfig = {
resources: ["f/db/**"],
};
@@ -326,18 +295,14 @@ Deno.test("elementsToMap: remote resource base file not skipped when configured"
true, // isRemote
);
assertEquals(
Object.keys(result).includes("f/db/connection.resource.yaml"),
true,
"Remote resource base file should NOT be skipped"
);
expect(Object.keys(result).includes("f/db/connection.resource.yaml")).toEqual(true);
});
// =============================================================================
// TRIGGER TYPE TESTS
// =============================================================================
Deno.test("elementsToMap: remote trigger base file not skipped when configured", async () => {
test("elementsToMap: remote trigger base file not skipped when configured", async () => {
const config: SpecificItemsConfig = {
triggers: ["f/webhooks/**"],
};
@@ -361,18 +326,14 @@ Deno.test("elementsToMap: remote trigger base file not skipped when configured",
true, // isRemote
);
assertEquals(
Object.keys(result).includes("f/webhooks/handler.http_trigger.yaml"),
true,
"Remote trigger base file should NOT be skipped"
);
expect(Object.keys(result).includes("f/webhooks/handler.http_trigger.yaml")).toEqual(true);
});
// =============================================================================
// SETTINGS TYPE TESTS
// =============================================================================
Deno.test("elementsToMap: remote settings.yaml not skipped when configured", async () => {
test("elementsToMap: remote settings.yaml not skipped when configured", async () => {
const config: SpecificItemsConfig = {
settings: true,
};
@@ -396,18 +357,14 @@ Deno.test("elementsToMap: remote settings.yaml not skipped when configured", asy
true, // isRemote
);
assertEquals(
Object.keys(result).includes("settings.yaml"),
true,
"Remote settings.yaml should NOT be skipped"
);
expect(Object.keys(result).includes("settings.yaml")).toEqual(true);
});
// =============================================================================
// FOLDER TYPE TESTS
// =============================================================================
Deno.test("elementsToMap: remote folder meta not skipped when configured", async () => {
test("elementsToMap: remote folder meta not skipped when configured", async () => {
const config: SpecificItemsConfig = {
folders: ["f/env_*"],
};
@@ -431,18 +388,14 @@ Deno.test("elementsToMap: remote folder meta not skipped when configured", async
true, // isRemote
);
assertEquals(
Object.keys(result).includes("f/env_staging/folder.meta.yaml"),
true,
"Remote folder meta should NOT be skipped"
);
expect(Object.keys(result).includes("f/env_staging/folder.meta.yaml")).toEqual(true);
});
// =============================================================================
// BACKWARD COMPATIBILITY: isRemote undefined behaves like local (false)
// =============================================================================
Deno.test("elementsToMap: isRemote undefined behaves like local (backward compatible)", async () => {
test("elementsToMap: isRemote undefined behaves like local (backward compatible)", async () => {
const config: SpecificItemsConfig = {
variables: ["f/**"],
};
@@ -468,9 +421,5 @@ Deno.test("elementsToMap: isRemote undefined behaves like local (backward compat
);
// Base file should be skipped (same behavior as isRemote=false)
assertEquals(
Object.keys(result).includes("f/test.variable.yaml"),
false,
"isRemote undefined should behave like isRemote=false (skip base file)"
);
expect(Object.keys(result).includes("f/test.variable.yaml")).toEqual(false);
});
+409
View File
@@ -0,0 +1,409 @@
/**
* Integration tests for folder and schedule CLI commands.
* Tests list and push operations via CLI and direct API.
*/
import { expect, test, describe } from "bun:test";
import { writeFile, mkdir, readFile } from "node:fs/promises";
import { join } from "node:path";
import { withTestBackend } from "./test_backend.ts";
import { addWorkspace } from "../workspace.ts";
async function setupWorkspaceProfile(backend: any): Promise<void> {
await addWorkspace(
{
remote: backend.baseUrl,
workspaceId: backend.workspace,
name: "localhost_test",
token: backend.token,
},
{ force: true, configDir: backend.testConfigDir }
);
}
// =============================================================================
// Folder Tests
// =============================================================================
describe("folder", () => {
test("list returns seeded folders", async () => {
await withTestBackend(async (backend, tempDir) => {
await setupWorkspaceProfile(backend);
const result = await backend.runCLICommand(["folder"], tempDir);
expect(result.code).toEqual(0);
// seedTestData creates "test" folder
expect(result.stdout).toContain("test");
});
});
test("push creates a new folder via sync push", async () => {
await withTestBackend(async (backend, tempDir) => {
await setupWorkspaceProfile(backend);
const uniqueId = Date.now();
const folderName = `inttest${uniqueId}`;
// Create wmill.yaml
await writeFile(
join(tempDir, "wmill.yaml"),
`defaultTs: bun\nincludes:\n - "**"\nexcludes: []\n`,
"utf-8"
);
// Create folder meta file
await mkdir(join(tempDir, "f", folderName), { recursive: true });
await writeFile(
join(tempDir, "f", folderName, "folder.meta.yaml"),
`display_name: "Integration Test Folder ${uniqueId}"\nowners:\n - "admin@windmill.dev"\nextra_perms: {}\n`,
"utf-8"
);
// Push
const pushResult = await backend.runCLICommand(
["sync", "push", "--yes", "--includes", `f/${folderName}/**`],
tempDir
);
expect(pushResult.code).toEqual(0);
// Verify folder was created via API
const apiResp = await backend.apiRequest!(
`/api/w/${backend.workspace}/folders/get/${folderName}`
);
expect(apiResp.status).toEqual(200);
const folderData = await apiResp.json();
expect(folderData.name).toBe(folderName);
});
});
test("push updates an existing folder", async () => {
await withTestBackend(async (backend, tempDir) => {
await setupWorkspaceProfile(backend);
const uniqueId = Date.now();
const folderName = `updfolder${uniqueId}`;
// Create folder via API
const createResp = await backend.apiRequest!(
`/api/w/${backend.workspace}/folders/create`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: folderName }),
}
);
expect(createResp.status).toBeLessThan(300);
await createResp.text();
// Create wmill.yaml and updated folder meta
await writeFile(
join(tempDir, "wmill.yaml"),
`defaultTs: bun\nincludes:\n - "**"\nexcludes: []\n`,
"utf-8"
);
await mkdir(join(tempDir, "f", folderName), { recursive: true });
await writeFile(
join(tempDir, "f", folderName, "folder.meta.yaml"),
`display_name: "Updated Display Name"\nowners:\n - "u/admin"\nextra_perms:\n u/admin: true\n`,
"utf-8"
);
// Push the update
const pushResult = await backend.runCLICommand(
["sync", "push", "--yes", "--includes", `f/${folderName}/**`],
tempDir
);
expect(pushResult.code).toEqual(0);
// Verify the display_name was updated
const apiResp = await backend.apiRequest!(
`/api/w/${backend.workspace}/folders/get/${folderName}`
);
expect(apiResp.status).toEqual(200);
const folderData = await apiResp.json();
expect(folderData.display_name).toBe("Updated Display Name");
});
});
test("pull retrieves folder metadata", async () => {
await withTestBackend(async (backend, tempDir) => {
await setupWorkspaceProfile(backend);
const uniqueId = Date.now();
const folderName = `pullfolder${uniqueId}`;
// Create folder via API
const createResp = await backend.apiRequest!(
`/api/w/${backend.workspace}/folders/create`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: folderName }),
}
);
expect(createResp.status).toBeLessThan(300);
await createResp.text();
// Create wmill.yaml
await writeFile(
join(tempDir, "wmill.yaml"),
`defaultTs: bun\nincludes:\n - "f/${folderName}/**"\nexcludes: []\nskipVariables: true\nskipResources: true\n`,
"utf-8"
);
// Pull
const pullResult = await backend.runCLICommand(
["sync", "pull", "--yes"],
tempDir
);
expect(pullResult.code).toEqual(0);
// Check the folder meta file was created
const content = await readFile(
join(tempDir, "f", folderName, "folder.meta.yaml"), "utf-8"
);
expect(content).toBeDefined();
});
});
});
// =============================================================================
// Schedule Tests
// =============================================================================
describe("schedule", () => {
test("list returns empty table for fresh workspace", async () => {
await withTestBackend(async (backend, tempDir) => {
await setupWorkspaceProfile(backend);
const result = await backend.runCLICommand(["schedule"], tempDir);
expect(result.code).toEqual(0);
// Table headers should be present
expect(result.stdout).toContain("Path");
expect(result.stdout).toContain("Schedule");
});
});
test("push creates a schedule targeting an existing script", async () => {
await withTestBackend(async (backend, tempDir) => {
await setupWorkspaceProfile(backend);
const uniqueId = Date.now();
// First create a script that the schedule can target
const scriptResp = await backend.apiRequest!(
`/api/w/${backend.workspace}/scripts/create`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
path: `f/test/sched_target_${uniqueId}`,
content: 'export async function main() { return "ok"; }',
language: "bun",
summary: "Schedule target script",
description: "",
schema: {
$schema: "https://json-schema.org/draft/2020-12/schema",
type: "object",
properties: {},
required: [],
},
}),
}
);
expect(scriptResp.status).toBeLessThan(300);
await scriptResp.text();
// Create wmill.yaml with includeSchedules
await writeFile(
join(tempDir, "wmill.yaml"),
`defaultTs: bun\nincludes:\n - "**"\nexcludes: []\nincludeSchedules: true\n`,
"utf-8"
);
// Create schedule file
await mkdir(join(tempDir, "f", "test"), { recursive: true });
await writeFile(
join(tempDir, `f/test/cron_${uniqueId}.schedule.yaml`),
`path: "f/test/cron_${uniqueId}"\nschedule: "0 0 */6 * * *"\nscript_path: "f/test/sched_target_${uniqueId}"\nis_flow: false\nargs: {}\nenabled: false\ntimezone: "UTC"\n`,
"utf-8"
);
// Push
const pushResult = await backend.runCLICommand(
["sync", "push", "--yes", "--includes", `f/test/cron_${uniqueId}**`],
tempDir
);
expect(pushResult.code).toEqual(0);
// Verify via API
const apiResp = await backend.apiRequest!(
`/api/w/${backend.workspace}/schedules/get/f/test/cron_${uniqueId}`
);
expect(apiResp.status).toEqual(200);
const schedData = await apiResp.json();
expect(schedData.schedule).toBe("0 0 */6 * * *");
expect(schedData.script_path).toBe(`f/test/sched_target_${uniqueId}`);
expect(schedData.enabled).toBe(false);
});
});
test("push updates a schedule's cron expression", async () => {
await withTestBackend(async (backend, tempDir) => {
await setupWorkspaceProfile(backend);
const uniqueId = Date.now();
// Create target script via API
const scriptResp = await backend.apiRequest!(
`/api/w/${backend.workspace}/scripts/create`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
path: `f/test/upd_sched_target_${uniqueId}`,
content: 'export async function main() { return "ok"; }',
language: "bun",
summary: "Target",
description: "",
schema: {
$schema: "https://json-schema.org/draft/2020-12/schema",
type: "object",
properties: {},
required: [],
},
}),
}
);
expect(scriptResp.status).toBeLessThan(300);
await scriptResp.text();
// Create schedule via API
const createResp = await backend.apiRequest!(
`/api/w/${backend.workspace}/schedules/create`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
path: `f/test/upd_cron_${uniqueId}`,
schedule: "0 0 * * * *",
script_path: `f/test/upd_sched_target_${uniqueId}`,
is_flow: false,
args: {},
enabled: false,
timezone: "UTC",
}),
}
);
expect(createResp.status).toBeLessThan(300);
await createResp.text();
// Create wmill.yaml with includeSchedules and updated schedule
await writeFile(
join(tempDir, "wmill.yaml"),
`defaultTs: bun\nincludes:\n - "**"\nexcludes: []\nincludeSchedules: true\n`,
"utf-8"
);
await mkdir(join(tempDir, "f", "test"), { recursive: true });
await writeFile(
join(tempDir, `f/test/upd_cron_${uniqueId}.schedule.yaml`),
`path: "f/test/upd_cron_${uniqueId}"\nschedule: "0 30 2 * * *"\nscript_path: "f/test/upd_sched_target_${uniqueId}"\nis_flow: false\nargs: {}\nenabled: false\ntimezone: "UTC"\n`,
"utf-8"
);
// Push the update
const pushResult = await backend.runCLICommand(
["sync", "push", "--yes", "--includes", `f/test/upd_cron_${uniqueId}**`],
tempDir
);
expect(pushResult.code).toEqual(0);
// Verify the schedule was updated
const apiResp = await backend.apiRequest!(
`/api/w/${backend.workspace}/schedules/get/f/test/upd_cron_${uniqueId}`
);
expect(apiResp.status).toEqual(200);
const schedData = await apiResp.json();
expect(schedData.schedule).toBe("0 30 2 * * *");
});
});
test("pull retrieves schedules into local files", async () => {
await withTestBackend(async (backend, tempDir) => {
await setupWorkspaceProfile(backend);
const uniqueId = Date.now();
// Create target script via API
const scriptResp = await backend.apiRequest!(
`/api/w/${backend.workspace}/scripts/create`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
path: `f/test/pull_sched_target_${uniqueId}`,
content: 'export async function main() { return "ok"; }',
language: "bun",
summary: "Target for pull test",
description: "",
schema: {
$schema: "https://json-schema.org/draft/2020-12/schema",
type: "object",
properties: {},
required: [],
},
}),
}
);
expect(scriptResp.status).toBeLessThan(300);
await scriptResp.text();
// Create schedule via API
const createResp = await backend.apiRequest!(
`/api/w/${backend.workspace}/schedules/create`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
path: `f/test/pull_cron_${uniqueId}`,
schedule: "0 15 3 * * 1",
script_path: `f/test/pull_sched_target_${uniqueId}`,
is_flow: false,
args: {},
enabled: false,
timezone: "UTC",
}),
}
);
expect(createResp.status).toBeLessThan(300);
await createResp.text();
// Create wmill.yaml
await writeFile(
join(tempDir, "wmill.yaml"),
`defaultTs: bun\nincludes:\n - "f/test/pull_cron_${uniqueId}**"\nexcludes: []\nincludeSchedules: true\nskipVariables: true\nskipResources: true\nskipScripts: true\n`,
"utf-8"
);
// Pull
const pullResult = await backend.runCLICommand(
["sync", "pull", "--yes"],
tempDir
);
expect(pullResult.code).toEqual(0);
// Check the schedule file was created
const content = await readFile(
join(tempDir, `f/test/pull_cron_${uniqueId}.schedule.yaml`), "utf-8"
);
expect(content).toContain("0 15 3 * * 1");
expect(content).toContain(`f/test/pull_sched_target_${uniqueId}`);
});
});
});
+230
View File
@@ -0,0 +1,230 @@
/**
* Tests for WASM schema parsing across all supported languages.
*
* Calls `inferSchema` directly no backend needed, fully local.
* Verifies that each language's WASM parser loads correctly and produces
* the expected JSON schema output.
*/
import { expect, test, describe } from "bun:test";
import { inferSchema } from "../src/utils/metadata.ts";
import type { ScriptLanguage } from "../src/utils/script_common.ts";
interface LanguageTestCase {
language: ScriptLanguage;
content: string;
/** Property name to verify in schema.properties */
expectedParam: string;
/** Expected JSON schema type, or undefined to skip type check */
expectedType?: string;
/** If set, verify this resource format exists on the named param */
expectedResourceParam?: { name: string; format: string };
}
const languageTestCases: LanguageTestCase[] = [
{
language: "python3",
content: `def main(x: str):\n return x\n`,
expectedParam: "x",
expectedType: "string",
},
{
language: "bun",
content: `export async function main(x: string) {\n return x;\n}\n`,
expectedParam: "x",
expectedType: "string",
},
{
language: "deno",
content: `export async function main(x: string) {\n return x;\n}\n`,
expectedParam: "x",
expectedType: "string",
},
{
language: "nativets",
content: `export async function main(x: string) {\n return x;\n}\n`,
expectedParam: "x",
expectedType: "string",
},
{
language: "go",
content: `package inner\n\nfunc main(x string) (interface{}, error) {\n\treturn x, nil\n}\n`,
expectedParam: "x",
expectedType: "string",
},
{
language: "bash",
// Bash parser infers params from variable assignments like x="$1"
content: `x="$1"\necho "$x"\n`,
expectedParam: "x",
expectedType: "string",
},
{
language: "powershell",
content: `param([string]$x)\nWrite-Output $x\n`,
expectedParam: "x",
expectedType: "string",
},
{
language: "postgresql",
content: `-- $1 name = default :: text\nSELECT $1::TEXT\n`,
expectedParam: "name",
expectedType: "string",
expectedResourceParam: { name: "database", format: "resource-postgresql" },
},
{
language: "mysql",
// MySQL parser only auto-detects the database resource param
content: `SELECT 1\n`,
expectedParam: "database",
expectedType: "object",
expectedResourceParam: { name: "database", format: "resource-mysql" },
},
{
language: "bigquery",
content: `SELECT 1\n`,
expectedParam: "database",
expectedType: "object",
expectedResourceParam: { name: "database", format: "resource-bigquery" },
},
{
language: "snowflake",
content: `SELECT 1\n`,
expectedParam: "database",
expectedType: "object",
expectedResourceParam: { name: "database", format: "resource-snowflake" },
},
{
language: "mssql",
content: `SELECT 1\n`,
expectedParam: "database",
expectedType: "object",
expectedResourceParam: {
name: "database",
format: "resource-ms_sql_server",
},
},
{
language: "oracledb",
content: `SELECT 1 FROM dual\n`,
expectedParam: "database",
expectedType: "object",
expectedResourceParam: { name: "database", format: "resource-oracledb" },
},
{
language: "duckdb",
// DuckDB parser doesn't auto-add a database resource
content: `SELECT 1\n`,
expectedParam: undefined as any,
expectedType: undefined,
},
{
language: "graphql",
content: `query($name: String) {\n user(name: $name) { id }\n}\n`,
expectedParam: "name",
expectedType: "string",
expectedResourceParam: { name: "api", format: "resource-graphql" },
},
{
language: "php",
content: `<?php\nfunction main(string $x) {\n return $x;\n}\n`,
expectedParam: "x",
expectedType: "string",
},
{
language: "rust",
content: `fn main(x: String) -> Result<String, String> {\n Ok(x)\n}\n`,
expectedParam: "x",
expectedType: "string",
},
{
language: "csharp",
content: `class Script {\n public static string Main(string x) {\n return x;\n }\n}\n`,
expectedParam: "x",
expectedType: "string",
},
{
language: "nu",
content: `def main [x: string] {\n print $x\n}\n`,
expectedParam: "x",
expectedType: "string",
},
{
language: "ansible",
content: `---\ninventory:\n - resource_type: ansible_inventory\n---\n- name: Test\n hosts: 127.0.0.1\n connection: local\n tasks:\n - name: Echo\n debug:\n msg: "hello"\n`,
// Ansible parser produces "inventory.ini" as param name
expectedParam: "inventory.ini",
expectedType: undefined,
},
{
language: "java",
content: `public class Main {\n public static String main(String x) {\n return x;\n }\n}\n`,
expectedParam: "x",
expectedType: "string",
},
{
language: "ruby",
content: `def main(x)\n puts x\nend\n`,
expectedParam: "x",
expectedType: undefined, // Ruby is dynamically typed
},
];
describe("generate-metadata schema parsing", () => {
for (const tc of languageTestCases) {
test(`${tc.language}: WASM parser loads and infers schema`, async () => {
const result = await inferSchema(
tc.language,
tc.content,
{},
`test.${tc.language}`
);
expect(result).toBeDefined();
expect(result.schema).toBeDefined();
expect(result.schema.properties).toBeDefined();
if (tc.expectedParam) {
expect(result.schema.properties[tc.expectedParam]).toBeDefined();
if (tc.expectedType !== undefined) {
expect(result.schema.properties[tc.expectedParam].type).toEqual(
tc.expectedType
);
}
}
if (tc.expectedResourceParam) {
const rp = result.schema.properties[tc.expectedResourceParam.name];
expect(rp).toBeDefined();
expect(rp.type).toEqual("object");
expect(rp.format).toEqual(tc.expectedResourceParam.format);
}
});
}
});
const allLanguages: ScriptLanguage[] = [
"python3", "bun", "deno", "nativets", "go", "bash", "powershell",
"postgresql", "mysql", "bigquery", "snowflake", "mssql", "oracledb",
"duckdb", "graphql", "php", "rust", "csharp", "nu", "ansible", "java", "ruby",
];
describe("generate-metadata invalid input handling", () => {
for (const lang of allLanguages) {
test(`${lang}: does not crash on invalid input`, async () => {
const result = await inferSchema(
lang,
"THIS IS INVALID GARBAGE @#$%^&*()",
{},
`test.${lang}`
);
expect(result).toBeDefined();
expect(result.schema).toBeDefined();
expect(result.schema.properties).toBeDefined();
// Should return a valid (possibly empty) schema, not throw
expect(typeof result.schema.properties).toBe("object");
});
}
});
+73
View File
@@ -0,0 +1,73 @@
/**
* Unit tests for git utility functions.
* Tests pure functions only no git subprocess calls.
*/
import { expect, test, describe } from "bun:test";
import {
getOriginalBranchForWorkspaceForks,
getWorkspaceIdForWorkspaceForkFromBranchName,
} from "../src/utils/git.ts";
// =============================================================================
// getOriginalBranchForWorkspaceForks
// =============================================================================
describe("getOriginalBranchForWorkspaceForks", () => {
test("extracts original branch from valid fork branch name", () => {
expect(getOriginalBranchForWorkspaceForks("wm-fork/main/my-workspace")).toBe("main");
});
test("extracts multi-segment original branch", () => {
expect(
getOriginalBranchForWorkspaceForks("wm-fork/feature/cool-thing/my-workspace")
).toBe("feature/cool-thing");
});
test("returns null for null input", () => {
expect(getOriginalBranchForWorkspaceForks(null)).toBeNull();
});
test("returns null for empty string", () => {
expect(getOriginalBranchForWorkspaceForks("")).toBeNull();
});
test("returns null for non-fork branch", () => {
expect(getOriginalBranchForWorkspaceForks("main")).toBeNull();
expect(getOriginalBranchForWorkspaceForks("feature/my-feature")).toBeNull();
});
test("returns null for branch that starts with wm-fork but has no slashes after", () => {
expect(getOriginalBranchForWorkspaceForks("wm-fork")).toBeNull();
});
test("returns null when branch segment between slashes is empty", () => {
// "wm-fork//workspace" — start=8, end=8, end - start = 0
expect(getOriginalBranchForWorkspaceForks("wm-fork//workspace")).toBeNull();
});
});
// =============================================================================
// getWorkspaceIdForWorkspaceForkFromBranchName
// =============================================================================
describe("getWorkspaceIdForWorkspaceForkFromBranchName", () => {
test("extracts workspace id from valid fork branch name", () => {
expect(
getWorkspaceIdForWorkspaceForkFromBranchName("wm-fork/main/my-workspace")
).toBe("wm-fork-my-workspace");
});
test("returns null for non-fork branch", () => {
expect(getWorkspaceIdForWorkspaceForkFromBranchName("main")).toBeNull();
expect(
getWorkspaceIdForWorkspaceForkFromBranchName("feature/my-feature")
).toBeNull();
});
test("extracts workspace id with multi-segment original branch", () => {
expect(
getWorkspaceIdForWorkspaceForkFromBranchName("wm-fork/feature/cool/ws-id")
).toBe("wm-fork-ws-id");
});
});
+23 -40
View File
@@ -1,4 +1,5 @@
import { assertEquals, assertStringIncludes } from "https://deno.land/std@0.224.0/assert/mod.ts";
import { expect, test } from "bun:test";
import { writeFile, readFile } from "node:fs/promises";
import { withTestBackend } from "./test_backend.ts";
import { shouldSkipOnCI } from "./cargo_backend.ts";
import { addWorkspace } from "../workspace.ts";
@@ -9,12 +10,7 @@ import { addWorkspace } from "../workspace.ts";
// These tests require EE features (private, enterprise) and are skipped in CI
// =============================================================================
Deno.test({
name: "GitSync Settings: default mode writes to top-level",
ignore: shouldSkipOnCI(), // Requires EE features
sanitizeResources: false,
sanitizeOps: false,
fn: async () => {
test.skipIf(shouldSkipOnCI())("GitSync Settings: default mode writes to top-level", async () => {
await withTestBackend(async (backend, tempDir) => {
// Set up workspace
const testWorkspace = {
@@ -44,11 +40,11 @@ Deno.test({
});
// Create initial wmill.yaml with different settings
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
includes:
- f/**
excludes: []
skipVariables: false`);
skipVariables: false`, "utf-8");
// Pull with default flag
const result = await backend.runCLICommand([
@@ -57,25 +53,19 @@ skipVariables: false`);
'--default'
], tempDir);
assertEquals(result.code, 0, `Default mode pull should succeed: ${result.stderr}`);
expect(result.code).toEqual(0);
// Read updated config
const updatedConfig = await Deno.readTextFile(`${tempDir}/wmill.yaml`);
const updatedConfig = await readFile(`${tempDir}/wmill.yaml`, "utf-8");
// Should update top-level settings, not create overrides
assertStringIncludes(updatedConfig, "includes:\n - f/special/**");
assertStringIncludes(updatedConfig, "excludes:\n - '*.test.ts'");
assertStringIncludes(updatedConfig, "extraIncludes:\n - g/**");
expect(updatedConfig).toContain("includes:\n - f/special/**");
expect(updatedConfig).toContain("excludes:\n - '*.test.ts'");
expect(updatedConfig).toContain("extraIncludes:\n - g/**");
});
}
});
Deno.test({
name: "GitSync Settings: pull shows correct diff output",
ignore: shouldSkipOnCI(), // Requires EE features
sanitizeResources: false,
sanitizeOps: false,
fn: async () => {
test.skipIf(shouldSkipOnCI())("GitSync Settings: pull shows correct diff output", async () => {
await withTestBackend(async (backend, tempDir) => {
// Set up workspace
const testWorkspace = {
@@ -105,12 +95,12 @@ Deno.test({
});
// Create wmill.yaml with different settings
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
includes:
- f/**
excludes: []
skipVariables: true
skipResources: false`);
skipResources: false`, "utf-8");
// Pull with diff flag
const result = await backend.runCLICommand([
@@ -119,22 +109,16 @@ skipResources: false`);
'--diff'
], tempDir);
assertEquals(result.code, 0, `Diff mode should succeed: ${result.stderr}`);
expect(result.code).toEqual(0);
// Should show differences
assertStringIncludes(result.stdout, "Changes that would be applied locally:");
expect(result.stdout).toContain("Changes that would be applied locally:");
// Should show the change for skipResources (ignoring ANSI color codes)
assertStringIncludes(result.stdout, "skipResources:");
expect(result.stdout).toContain("skipResources:");
});
}
});
Deno.test({
name: "GitSync Settings: replace mode overwrites existing config",
ignore: shouldSkipOnCI(), // Requires EE features
sanitizeResources: false,
sanitizeOps: false,
fn: async () => {
test.skipIf(shouldSkipOnCI())("GitSync Settings: replace mode overwrites existing config", async () => {
await withTestBackend(async (backend, tempDir) => {
// Set up workspace
const testWorkspace = {
@@ -164,12 +148,12 @@ Deno.test({
});
// Create initial wmill.yaml with settings that should be replaced
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
includes:
- f/old/**
excludes:
- "*.old.ts"
skipVariables: true`);
skipVariables: true`, "utf-8");
// Pull with replace flag
const result = await backend.runCLICommand([
@@ -178,14 +162,13 @@ skipVariables: true`);
'--replace'
], tempDir);
assertEquals(result.code, 0, `Replace mode pull should succeed: ${result.stderr}`);
expect(result.code).toEqual(0);
// Read updated config
const updatedConfig = await Deno.readTextFile(`${tempDir}/wmill.yaml`);
const updatedConfig = await readFile(`${tempDir}/wmill.yaml`, "utf-8");
// Should have replaced settings from backend
assertStringIncludes(updatedConfig, "f/replaced/**");
assertStringIncludes(updatedConfig, "*.backup.ts");
expect(updatedConfig).toContain("f/replaced/**");
expect(updatedConfig).toContain("*.backup.ts");
});
}
});
+70 -86
View File
@@ -1,4 +1,5 @@
import { assertEquals, assertStringIncludes, assert } from "https://deno.land/std@0.224.0/assert/mod.ts";
import { expect, test } from "bun:test";
import { writeFile } from "node:fs/promises";
import { withTestBackend } from "./test_backend.ts";
import { addWorkspace } from "../workspace.ts";
import { parseJsonFromCLIOutput } from "./test_config_helpers.ts";
@@ -27,16 +28,12 @@ async function setupWorkspaceProfile(backend: any): Promise<void> {
// - test apps, resources, variables via seedTestData()
// No additional setup needed!
Deno.test({
name: "CLI include flags bypass restrictive path filtering",
sanitizeResources: false,
sanitizeOps: false,
fn: async () => {
test("CLI include flags bypass restrictive path filtering", async () => {
await withTestBackend(async (backend, tempDir) => {
await setupWorkspaceProfile(backend);
// Create wmill.yaml with very restrictive includes that would exclude special files
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
includes:
- "f/**"
excludes: []
@@ -45,24 +42,24 @@ skipResources: true
includeUsers: false
includeGroups: false
includeSettings: false
includeKey: false`);
includeKey: false`, "utf-8");
// Test: CLI flags should override config and bypass path filtering
const result = await backend.runCLICommand([
'sync', 'pull',
'sync', 'pull',
'--include-users',
'--include-groups',
'--include-groups',
'--include-settings',
'--include-key',
'--dry-run',
'--dry-run',
'--json-output'
], tempDir);
assertEquals(result.code, 0, `Command failed: ${result.stderr}`);
expect(result.code).toEqual(0);
const output = parseJsonFromCLIOutput(result.stdout);
const changePaths = output.changes.map((c: any) => c.path);
// Assert that special files are included despite restrictive path filtering
// Normalize paths for cross-platform comparison (Windows uses backslashes)
const normalizedPaths = changePaths.map((p: string) => p.replace(/\\/g, '/'));
@@ -70,119 +67,106 @@ includeKey: false`);
const hasGroup = normalizedPaths.some((path: string) => path.includes('groups/test_group.group.yaml'));
const hasSettings = changePaths.some((path: string) => path === 'settings.yaml');
const hasEncryptionKey = changePaths.some((path: string) => path === 'encryption_key.yaml');
assert(hasUser, `Admin user should be included despite restrictive includes. Found paths: ${normalizedPaths.join(', ')}`);
assert(hasGroup, `'test_group' should be included despite restrictive includes. Found paths: ${normalizedPaths.join(', ')}`);
assert(hasSettings, `Settings should be included despite restrictive includes. Found paths: ${changePaths.join(', ')}`);
assert(hasEncryptionKey, `Encryption key should be included despite restrictive includes. Found paths: ${changePaths.join(', ')}`);
});
}});
Deno.test({
name: "CLI flags override wmill.yaml include settings",
sanitizeResources: false,
sanitizeOps: false,
fn: async () => {
expect(hasUser).toBe(true);
expect(hasGroup).toBe(true);
expect(hasSettings).toBe(true);
expect(hasEncryptionKey).toBe(true);
});
});
test("CLI flags override wmill.yaml include settings", async () => {
await withTestBackend(async (backend, tempDir) => {
await setupWorkspaceProfile(backend);
// Config explicitly disables includes, but CLI should override
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
includes:
- "**"
excludes: []
includeUsers: false
includeGroups: false`);
includeGroups: false`, "utf-8");
// CLI flags should override config file settings
const result = await backend.runCLICommand([
'sync', 'pull',
'--include-users',
'--include-groups',
'--include-groups',
'--dry-run',
'--json-output'
], tempDir);
assertEquals(result.code, 0, `Command failed: ${result.stderr}`);
expect(result.code).toEqual(0);
const output = parseJsonFromCLIOutput(result.stdout);
const changePaths = output.changes.map((c: any) => c.path);
// Normalize paths for cross-platform comparison (Windows uses backslashes)
const normalizedPaths = changePaths.map((p: string) => p.replace(/\\/g, '/'));
const hasUser = normalizedPaths.some((path: string) => path.includes('admin@windmill.dev.user.yaml'));
const hasGroup = normalizedPaths.some((path: string) => path.includes('groups/test_group.group.yaml'));
assert(hasUser, `CLI --include-users should override config includeUsers: false. Found paths: ${normalizedPaths.join(', ')}`);
assert(hasGroup, `CLI --include-groups should override config includeGroups: false. Found paths: ${normalizedPaths.join(', ')}`);
expect(hasUser).toBe(true);
expect(hasGroup).toBe(true);
});
}});
});
Deno.test({
name: "Skip flags work correctly with getTypeStrFromPath and lock files",
ignore: true, // TODO: Requires backend app creation to work (currently failing with v2_job_queue constraint)
sanitizeResources: false,
sanitizeOps: false,
fn: async () => {
test("Skip flags work correctly with getTypeStrFromPath and lock files", async () => {
await withTestBackend(async (backend, tempDir) => {
await setupWorkspaceProfile(backend);
// Create wmill.yaml with skip flags enabled
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
includes:
- "**"
excludes: []
skipScripts: true
skipFlows: false
includeUsers: true`);
includeUsers: true`, "utf-8");
const result = await backend.runCLICommand([
'sync', 'pull',
'--dry-run',
'--json-output'
], tempDir);
assertEquals(result.code, 0, `Command failed: ${result.stderr}`);
expect(result.code).toEqual(0);
const output = parseJsonFromCLIOutput(result.stdout);
const changePaths = output.changes.map((c: any) => c.path);
// Scripts should be skipped (including lock files) - the backend doesn't create scripts by default
const hasScript = changePaths.some((path: string) =>
const hasScript = changePaths.some((path: string) =>
path.endsWith('.py') || path.endsWith('.ts') || path.endsWith('.go') || path.endsWith('.sh')
);
const hasScriptLock = changePaths.some((path: string) => path.endsWith('.script.lock'));
// Apps should be included (the backend creates test apps)
const hasApp = changePaths.some((path: string) => path.includes('test_dashboard') || path.endsWith('.app.yaml'));
// Users should still be included
const hasUser = changePaths.some((path: string) => path.includes('admin@windmill.dev.user.yaml'));
assert(!hasScript, `Standalone scripts should be skipped when skipScripts: true. Found paths: ${changePaths.join(', ')}`);
assert(!hasScriptLock, `Script lock files should be skipped when skipScripts: true. Found paths: ${changePaths.join(', ')}`);
assert(hasApp, `Apps should be included (inline scripts are part of apps). Found paths: ${changePaths.join(', ')}`);
assert(hasUser, `Users should be included when includeUsers: true. Found paths: ${changePaths.join(', ')}`);
});
}});
Deno.test({
name: "Mixed include and skip flags work together",
sanitizeResources: false,
sanitizeOps: false,
fn: async () => {
expect(hasScript).toBe(false);
expect(hasScriptLock).toBe(false);
expect(hasApp).toBe(true);
expect(hasUser).toBe(true);
});
});
test("Mixed include and skip flags work together", async () => {
await withTestBackend(async (backend, tempDir) => {
await setupWorkspaceProfile(backend);
// Create restrictive config with mixed settings
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
includes:
- "f/**"
excludes: []
skipScripts: true
includeUsers: false
includeSettings: false`);
includeSettings: false`, "utf-8");
const result = await backend.runCLICommand([
'sync', 'pull',
'--skip-scripts', // Reinforce script skipping
@@ -190,25 +174,25 @@ includeSettings: false`);
'--dry-run',
'--json-output'
], tempDir);
assertEquals(result.code, 0, `Command failed: ${result.stderr}`);
expect(result.code).toEqual(0);
const output = parseJsonFromCLIOutput(result.stdout);
const changePaths = output.changes.map((c: any) => c.path);
// Scripts should be excluded
const hasScript = changePaths.some((path: string) =>
const hasScript = changePaths.some((path: string) =>
path.endsWith('.py') || path.endsWith('.ts') || path.endsWith('.go') || path.endsWith('.sh')
);
// Users should be included (CLI override)
const hasUser = changePaths.some((path: string) => path.includes('admin@windmill.dev.user.yaml'));
// Settings should be excluded (no CLI override, restrictive path filtering)
const hasSettings = changePaths.some((path: string) => path === 'settings.yaml');
assert(!hasScript, `Scripts should be excluded due to skipScripts. Found paths: ${changePaths.join(', ')}`);
assert(hasUser, `Users should be included due to CLI --include-users override. Found paths: ${changePaths.join(', ')}`);
assert(!hasSettings, `Settings should be excluded (no CLI override + restrictive paths). Found paths: ${changePaths.join(', ')}`);
expect(hasScript).toBe(false);
expect(hasUser).toBe(true);
expect(hasSettings).toBe(false);
});
}});
});
+57 -68
View File
@@ -3,7 +3,8 @@
* This creates a unit test that directly tests the logic without needing a backend
*/
import { assertEquals, assertStringIncludes, assert } from "https://deno.land/std@0.224.0/assert/mod.ts";
import { expect, test } from "bun:test";
import { readFile } from "node:fs/promises";
import { DEFAULT_SYNC_OPTIONS } from "../src/core/conf.ts";
import { withTestBackend } from "./test_backend.ts";
import { shouldSkipOnCI } from "./cargo_backend.ts";
@@ -36,55 +37,50 @@ function createWorkspaceProfileNoRepos(workspace: any): any {
return workspaceProfile;
}
Deno.test("Init: createWorkspaceProfile includes defaults when no repositories exist", () => {
console.log('🧪 Testing init logic for workspace with no git-sync repositories...');
test("Init: createWorkspaceProfile includes defaults when no repositories exist", () => {
console.log('Testing init logic for workspace with no git-sync repositories...');
const workspaceProfile = createWorkspaceProfileNoRepos(mockWorkspace);
console.log('Generated workspace profile:', JSON.stringify(workspaceProfile, null, 2));
// Verify basic workspace info
assertEquals(workspaceProfile.baseUrl, 'https://app.windmill.dev/');
assertEquals(workspaceProfile.workspaceId, 'test-workspace');
expect(workspaceProfile.baseUrl).toEqual('https://app.windmill.dev/');
expect(workspaceProfile.workspaceId).toEqual('test-workspace');
// Verify default sync settings are included
assert(Array.isArray(workspaceProfile.includes), 'Should have includes array');
assertEquals(workspaceProfile.includes.length, 1, 'Should have one include pattern');
assertEquals(workspaceProfile.includes[0], 'f/**', 'Should include f/** pattern');
assert(Array.isArray(workspaceProfile.excludes), 'Should have excludes array');
assertEquals(workspaceProfile.excludes.length, 0, 'Should have empty excludes array');
assertEquals(workspaceProfile.defaultTs, 'bun', 'Should have bun as default TypeScript runtime');
console.log('Workspace profile correctly includes default sync settings when no repositories exist');
expect(Array.isArray(workspaceProfile.includes)).toBeTruthy();
expect(workspaceProfile.includes.length).toEqual(1);
expect(workspaceProfile.includes[0]).toEqual('f/**');
expect(Array.isArray(workspaceProfile.excludes)).toBeTruthy();
expect(workspaceProfile.excludes.length).toEqual(0);
expect(workspaceProfile.defaultTs).toEqual('bun');
console.log('Workspace profile correctly includes default sync settings when no repositories exist');
});
Deno.test("Init: verify DEFAULT_SYNC_OPTIONS has expected values", () => {
console.log('🔍 Verifying DEFAULT_SYNC_OPTIONS contains expected values...');
test("Init: verify DEFAULT_SYNC_OPTIONS has expected values", () => {
console.log('Verifying DEFAULT_SYNC_OPTIONS contains expected values...');
console.log('DEFAULT_SYNC_OPTIONS:', JSON.stringify(DEFAULT_SYNC_OPTIONS, null, 2));
// Verify the default options include the expected f/** pattern
assert(Array.isArray(DEFAULT_SYNC_OPTIONS.includes), 'DEFAULT_SYNC_OPTIONS should have includes array');
assertEquals(DEFAULT_SYNC_OPTIONS.includes.length, 1, 'Should have one include pattern');
assertEquals(DEFAULT_SYNC_OPTIONS.includes[0], 'f/**', 'Should default to f/** pattern');
assert(Array.isArray(DEFAULT_SYNC_OPTIONS.excludes), 'DEFAULT_SYNC_OPTIONS should have excludes array');
assertEquals(DEFAULT_SYNC_OPTIONS.excludes.length, 0, 'Should have empty excludes array by default');
assertEquals(DEFAULT_SYNC_OPTIONS.defaultTs, 'bun', 'Should default to bun runtime');
console.log('DEFAULT_SYNC_OPTIONS has expected values');
expect(Array.isArray(DEFAULT_SYNC_OPTIONS.includes)).toBeTruthy();
expect(DEFAULT_SYNC_OPTIONS.includes.length).toEqual(1);
expect(DEFAULT_SYNC_OPTIONS.includes[0]).toEqual('f/**');
expect(Array.isArray(DEFAULT_SYNC_OPTIONS.excludes)).toBeTruthy();
expect(DEFAULT_SYNC_OPTIONS.excludes.length).toEqual(0);
expect(DEFAULT_SYNC_OPTIONS.defaultTs).toEqual('bun');
console.log('DEFAULT_SYNC_OPTIONS has expected values');
});
Deno.test({
name: "Init: --use-backend flag applies git-sync settings",
ignore: shouldSkipOnCI(), // Requires EE features
sanitizeResources: false,
sanitizeOps: false,
fn: async () => {
await withTestBackend(async (backend, tempDir) => {
test.skipIf(shouldSkipOnCI())("Init: --use-backend flag applies git-sync settings", async () => {
await withTestBackend(async (backend, tempDir) => {
// Set up workspace
const testWorkspace = {
remote: backend.baseUrl,
@@ -122,29 +118,23 @@ Deno.test({
'--repository', 'u/test/init_repo'
], tempDir);
assertEquals(result.code, 0, `Init with --use-backend should succeed: ${result.stderr}`);
expect(result.code).toEqual(0);
// Verify wmill.yaml was created with backend settings
const wmillYaml = await Deno.readTextFile(`${tempDir}/wmill.yaml`);
const wmillYaml = await readFile(`${tempDir}/wmill.yaml`, "utf-8");
// Should have backend-applied settings written to top-level (not overrides)
assertStringIncludes(wmillYaml, "f/backend/**", "Should include backend's include_path");
assertStringIncludes(wmillYaml, "*.test.ts", "Should include backend's exclude_path");
assertStringIncludes(wmillYaml, "g/**", "Should include backend's extra_include_path");
expect(wmillYaml).toContain("f/backend/**");
expect(wmillYaml).toContain("*.test.ts");
expect(wmillYaml).toContain("g/**");
// Should have empty overrides section for consistency
assertStringIncludes(wmillYaml, "gitBranches: {}");
});
}
expect(wmillYaml).toContain("gitBranches: {}");
});
});
Deno.test({
name: "Init: --use-default bypasses backend settings check",
ignore: shouldSkipOnCI(), // Requires EE features
sanitizeResources: false,
sanitizeOps: false,
fn: async () => {
await withTestBackend(async (backend, tempDir) => {
test.skipIf(shouldSkipOnCI())("Init: --use-default bypasses backend settings check", async () => {
await withTestBackend(async (backend, tempDir) => {
// Set up workspace
const testWorkspace = {
remote: backend.baseUrl,
@@ -181,18 +171,17 @@ Deno.test({
'--use-default'
], tempDir);
assertEquals(result.code, 0, `Init with --use-default should succeed: ${result.stderr}`);
expect(result.code).toEqual(0);
// Verify wmill.yaml was created with default settings only
const wmillYaml = await Deno.readTextFile(`${tempDir}/wmill.yaml`);
const wmillYaml = await readFile(`${tempDir}/wmill.yaml`, "utf-8");
// Should have default settings, not backend settings
assertStringIncludes(wmillYaml, "includes:\n - f/**", "Should use default includes");
assertStringIncludes(wmillYaml, "defaultTs: bun", "Should use default TypeScript runtime");
expect(wmillYaml).toContain("includes:\n - f/**");
expect(wmillYaml).toContain("defaultTs: bun");
// Should NOT have backend-specific settings
assertEquals(wmillYaml.includes("f/should-be-ignored/**"), false, "Should not include backend settings");
assertStringIncludes(wmillYaml, "gitBranches: {}", "Should have empty overrides section for consistency");
});
}
});
expect(wmillYaml.includes("f/should-be-ignored/**")).toEqual(false);
expect(wmillYaml).toContain("gitBranches: {}");
});
});
+119 -115
View File
@@ -1,8 +1,7 @@
import {
assert,
assertEquals,
assertStringIncludes,
} from "https://deno.land/std@0.224.0/assert/mod.ts";
import { expect, test } from "bun:test";
import { mkdtemp, rm, mkdir, writeFile } from "node:fs/promises";
import os from "node:os";
import * as path from "@std/path";
import {
formatValidationError,
runLint,
@@ -11,30 +10,31 @@ import {
async function withTempDir(
fn: (tempDir: string) => Promise<void>,
): Promise<void> {
const tempDir = await Deno.makeTempDir({ prefix: "wmill_lint_test_" });
const originalCwd = Deno.cwd();
const tempDir = await mkdtemp(path.join(os.tmpdir(), "wmill_lint_test_"));
const originalCwd = process.cwd();
try {
Deno.chdir(tempDir);
process.chdir(tempDir);
await fn(tempDir);
} finally {
Deno.chdir(originalCwd);
await Deno.remove(tempDir, { recursive: true });
process.chdir(originalCwd);
await rm(tempDir, { recursive: true });
}
}
Deno.test("lint: validates flow, schedule, and trigger yaml files", async () => {
test("lint: validates flow, schedule, and trigger yaml files", async () => {
await withTempDir(async (tempDir) => {
await Deno.mkdir(`${tempDir}/f/my_flow.flow`, { recursive: true });
await Deno.writeTextFile(
await mkdir(`${tempDir}/f/my_flow.flow`, { recursive: true });
await writeFile(
`${tempDir}/f/my_flow.flow/flow.yaml`,
`summary: My flow
value:
modules: []
`,
"utf-8"
);
await Deno.mkdir(`${tempDir}/f/jobs`, { recursive: true });
await Deno.writeTextFile(
await mkdir(`${tempDir}/f/jobs`, { recursive: true });
await writeFile(
`${tempDir}/f/jobs/daily.schedule.yaml`,
`schedule: "0 0 12 * * *"
timezone: "UTC"
@@ -42,10 +42,11 @@ enabled: true
script_path: "f/jobs/daily_sync"
is_flow: false
`,
"utf-8"
);
await Deno.mkdir(`${tempDir}/f/triggers`, { recursive: true });
await Deno.writeTextFile(
await mkdir(`${tempDir}/f/triggers`, { recursive: true });
await writeFile(
`${tempDir}/f/triggers/hook.http_trigger.yaml`,
`script_path: "f/triggers/http_handler"
is_flow: false
@@ -58,93 +59,97 @@ workspaced_route: false
wrap_body: false
raw_string: false
`,
"utf-8"
);
await Deno.writeTextFile(
await writeFile(
`${tempDir}/f/triggers/inbox.email_trigger.yaml`,
`script_path: "f/triggers/email_handler"
is_flow: false
local_part: "inbox"
`,
"utf-8"
);
const report = await runLint({} as any, tempDir);
assertEquals(report.exitCode, 0);
assertEquals(report.validatedFiles, 4);
assertEquals(report.validFiles, 4);
assertEquals(report.invalidFiles, 0);
assertEquals(report.warnings.length, 0);
expect(report.exitCode).toEqual(0);
expect(report.validatedFiles).toEqual(4);
expect(report.validFiles).toEqual(4);
expect(report.invalidFiles).toEqual(0);
expect(report.warnings.length).toEqual(0);
});
});
Deno.test("lint: returns errors for invalid schedule documents", async () => {
test("lint: returns errors for invalid schedule documents", async () => {
await withTempDir(async (tempDir) => {
await Deno.mkdir(`${tempDir}/f/jobs`, { recursive: true });
await Deno.writeTextFile(
await mkdir(`${tempDir}/f/jobs`, { recursive: true });
await writeFile(
`${tempDir}/f/jobs/broken.schedule.yaml`,
`timezone: "UTC"
enabled: true
script_path: "f/jobs/broken"
is_flow: false
`,
"utf-8"
);
const report = await runLint({} as any, tempDir);
assertEquals(report.exitCode, 1);
assertEquals(report.validatedFiles, 1);
assertEquals(report.invalidFiles, 1);
assertEquals(report.issues[0].path, "f/jobs/broken.schedule.yaml");
assert(
expect(report.exitCode).toEqual(1);
expect(report.validatedFiles).toEqual(1);
expect(report.invalidFiles).toEqual(1);
expect(report.issues[0].path).toEqual("f/jobs/broken.schedule.yaml");
expect(
report.issues[0].errors.some((message) =>
message.includes("missing required property 'schedule'")
),
);
).toBeTruthy();
});
});
Deno.test("lint: warns and skips unsupported native trigger schemas", async () => {
test("lint: warns and skips unsupported native trigger schemas", async () => {
await withTempDir(async (tempDir) => {
await Deno.mkdir(`${tempDir}/f/triggers`, { recursive: true });
await Deno.writeTextFile(
await mkdir(`${tempDir}/f/triggers`, { recursive: true });
await writeFile(
`${tempDir}/f/triggers/webhook.script.123.nextcloud_native_trigger.yaml`,
`path: "f/triggers/native"
`,
"utf-8"
);
const report = await runLint({} as any, tempDir);
assertEquals(report.exitCode, 0);
assertEquals(report.validatedFiles, 0);
assertEquals(report.skippedUnsupportedFiles, 1);
assertEquals(report.warnings.length, 1);
assertStringIncludes(
expect(report.exitCode).toEqual(0);
expect(report.validatedFiles).toEqual(0);
expect(report.skippedUnsupportedFiles).toEqual(1);
expect(report.warnings.length).toEqual(1);
expect(
report.warnings[0].message,
"Unsupported trigger schema",
);
).toContain("Unsupported trigger schema");
const failOnWarnReport = await runLint(
{ failOnWarn: true } as any,
tempDir,
);
assertEquals(failOnWarnReport.exitCode, 1);
expect(failOnWarnReport.exitCode).toEqual(1);
});
});
Deno.test("lint: uses wmill.yaml include filters for file discovery", async () => {
test("lint: uses wmill.yaml include filters for file discovery", async () => {
await withTempDir(async (tempDir) => {
await Deno.writeTextFile(
await writeFile(
`${tempDir}/wmill.yaml`,
`defaultTs: bun
includes:
- "f/allowed/**"
excludes: []
`,
"utf-8"
);
await Deno.mkdir(`${tempDir}/f/allowed`, { recursive: true });
await Deno.writeTextFile(
await mkdir(`${tempDir}/f/allowed`, { recursive: true });
await writeFile(
`${tempDir}/f/allowed/ok.schedule.yaml`,
`schedule: "0 0 12 * * *"
timezone: "UTC"
@@ -152,113 +157,109 @@ enabled: true
script_path: "f/jobs/ok"
is_flow: false
`,
"utf-8"
);
await Deno.mkdir(`${tempDir}/f/blocked`, { recursive: true });
await Deno.writeTextFile(
await mkdir(`${tempDir}/f/blocked`, { recursive: true });
await writeFile(
`${tempDir}/f/blocked/bad.schedule.yaml`,
`timezone: "UTC"
enabled: true
script_path: "f/jobs/bad"
is_flow: false
`,
"utf-8"
);
const report = await runLint({} as any, tempDir);
assertEquals(report.exitCode, 0);
assertEquals(report.validatedFiles, 1);
assertEquals(report.validFiles, 1);
assertEquals(report.invalidFiles, 0);
assertEquals(report.issues.length, 0);
expect(report.exitCode).toEqual(0);
expect(report.validatedFiles).toEqual(1);
expect(report.validFiles).toEqual(1);
expect(report.invalidFiles).toEqual(0);
expect(report.issues.length).toEqual(0);
});
});
// --- formatValidationError unit tests ---
Deno.test("formatValidationError: required keyword", () => {
assertEquals(
test("formatValidationError: required keyword", () => {
expect(
formatValidationError({
instancePath: "/value",
keyword: "required",
message: "must have required property 'modules'",
params: { missingProperty: "modules" },
}),
"/value missing required property 'modules'",
);
).toEqual("/value missing required property 'modules'");
});
Deno.test("formatValidationError: additionalProperties keyword", () => {
assertEquals(
test("formatValidationError: additionalProperties keyword", () => {
expect(
formatValidationError({
instancePath: "/value",
keyword: "additionalProperties",
message: "must NOT have additional properties",
params: { additionalProperty: "typo_field" },
}),
"/value has unknown property 'typo_field'",
);
).toEqual("/value has unknown property 'typo_field'");
});
Deno.test("formatValidationError: enum keyword filters null values", () => {
assertEquals(
test("formatValidationError: enum keyword filters null values", () => {
expect(
formatValidationError({
instancePath: "/http_method",
keyword: "enum",
message: "must be equal to one of the allowed values",
params: { allowedValues: [null, "get", "post", "put"] },
}),
"/http_method must be one of: 'get', 'post', 'put'",
);
).toEqual("/http_method must be one of: 'get', 'post', 'put'");
});
Deno.test("formatValidationError: falls back to message", () => {
assertEquals(
test("formatValidationError: falls back to message", () => {
expect(
formatValidationError({
instancePath: "/timeout",
keyword: "type",
message: "must be integer",
}),
"/timeout must be integer",
);
).toEqual("/timeout must be integer");
});
Deno.test("formatValidationError: uses / for empty instancePath", () => {
assertEquals(
test("formatValidationError: uses / for empty instancePath", () => {
expect(
formatValidationError({
instancePath: "",
keyword: "required",
message: "must have required property 'summary'",
params: { missingProperty: "summary" },
}),
"/ missing required property 'summary'",
);
).toEqual("/ missing required property 'summary'");
});
Deno.test("formatValidationError: generic fallback when no message", () => {
assertEquals(
test("formatValidationError: generic fallback when no message", () => {
expect(
formatValidationError({ instancePath: "/field", keyword: "custom" }),
"/field validation error",
);
).toEqual("/field validation error");
});
// --- runLint integration tests ---
Deno.test("lint: throws for non-existent directory", async () => {
test("lint: throws for non-existent directory", async () => {
let threw = false;
try {
await runLint({} as any, "/tmp/wmill_lint_nonexistent_" + Date.now());
} catch (e) {
threw = true;
assertStringIncludes((e as Error).message, "Directory not found");
expect((e as Error).message).toContain("Directory not found");
}
assert(threw, "Expected runLint to throw for non-existent directory");
expect(threw).toBeTruthy();
});
Deno.test("lint: json-shaped report contains all fields", async () => {
test("lint: json-shaped report contains all fields", async () => {
await withTempDir(async (tempDir) => {
await Deno.mkdir(`${tempDir}/f/jobs`, { recursive: true });
await Deno.writeTextFile(
await mkdir(`${tempDir}/f/jobs`, { recursive: true });
await writeFile(
`${tempDir}/f/jobs/ok.schedule.yaml`,
`schedule: "0 0 * * *"
timezone: "UTC"
@@ -266,33 +267,34 @@ enabled: true
script_path: "f/jobs/ok"
is_flow: false
`,
"utf-8"
);
const report = await runLint({ json: true } as any, tempDir);
// Verify the report object has the shape expected by --json output
assertEquals(typeof report.scannedFiles, "number");
assertEquals(typeof report.validatedFiles, "number");
assertEquals(typeof report.validFiles, "number");
assertEquals(typeof report.invalidFiles, "number");
assertEquals(typeof report.skippedUnsupportedFiles, "number");
assert(Array.isArray(report.warnings));
assert(Array.isArray(report.issues));
assertEquals(typeof report.success, "boolean");
assertEquals(typeof report.exitCode, "number");
expect(typeof report.scannedFiles).toEqual("number");
expect(typeof report.validatedFiles).toEqual("number");
expect(typeof report.validFiles).toEqual("number");
expect(typeof report.invalidFiles).toEqual("number");
expect(typeof report.skippedUnsupportedFiles).toEqual("number");
expect(Array.isArray(report.warnings)).toBeTruthy();
expect(Array.isArray(report.issues)).toBeTruthy();
expect(typeof report.success).toEqual("boolean");
expect(typeof report.exitCode).toEqual("number");
// JSON.stringify should round-trip cleanly
const json = JSON.parse(JSON.stringify(report));
assertEquals(json.success, true);
assertEquals(json.exitCode, 0);
expect(json.success).toEqual(true);
expect(json.exitCode).toEqual(0);
});
});
Deno.test("lint: --fail-on-warn with mixed valid and warning files", async () => {
test("lint: --fail-on-warn with mixed valid and warning files", async () => {
await withTempDir(async (tempDir) => {
// A valid schedule
await Deno.mkdir(`${tempDir}/f/jobs`, { recursive: true });
await Deno.writeTextFile(
await mkdir(`${tempDir}/f/jobs`, { recursive: true });
await writeFile(
`${tempDir}/f/jobs/ok.schedule.yaml`,
`schedule: "0 0 * * *"
timezone: "UTC"
@@ -300,36 +302,38 @@ enabled: true
script_path: "f/jobs/ok"
is_flow: false
`,
"utf-8"
);
// An unsupported native trigger that produces a warning
await Deno.mkdir(`${tempDir}/f/triggers`, { recursive: true });
await Deno.writeTextFile(
await mkdir(`${tempDir}/f/triggers`, { recursive: true });
await writeFile(
`${tempDir}/f/triggers/webhook.script.123.nextcloud_native_trigger.yaml`,
`path: "f/triggers/native"
`,
"utf-8"
);
// Without --fail-on-warn: passes
const normalReport = await runLint({} as any, tempDir);
assertEquals(normalReport.exitCode, 0);
assertEquals(normalReport.success, true);
assertEquals(normalReport.validFiles, 1);
assertEquals(normalReport.warnings.length, 1);
expect(normalReport.exitCode).toEqual(0);
expect(normalReport.success).toEqual(true);
expect(normalReport.validFiles).toEqual(1);
expect(normalReport.warnings.length).toEqual(1);
// With --fail-on-warn: fails due to warning
const strictReport = await runLint({ failOnWarn: true } as any, tempDir);
assertEquals(strictReport.exitCode, 1);
assertEquals(strictReport.success, false);
assertEquals(strictReport.validFiles, 1);
assertEquals(strictReport.warnings.length, 1);
expect(strictReport.exitCode).toEqual(1);
expect(strictReport.success).toEqual(false);
expect(strictReport.validFiles).toEqual(1);
expect(strictReport.warnings.length).toEqual(1);
});
});
Deno.test("lint: reports enum errors with allowed values for invalid trigger", async () => {
test("lint: reports enum errors with allowed values for invalid trigger", async () => {
await withTempDir(async (tempDir) => {
await Deno.mkdir(`${tempDir}/f/triggers`, { recursive: true });
await Deno.writeTextFile(
await mkdir(`${tempDir}/f/triggers`, { recursive: true });
await writeFile(
`${tempDir}/f/triggers/hook.http_trigger.yaml`,
`script_path: "f/triggers/http_handler"
is_flow: false
@@ -341,14 +345,14 @@ workspaced_route: false
wrap_body: false
raw_string: false
`,
"utf-8"
);
const report = await runLint({} as any, tempDir);
assertEquals(report.invalidFiles, 1);
assert(
expect(report.invalidFiles).toEqual(1);
expect(
report.issues[0].errors.some((msg) => msg.includes("must be one of:")),
`Expected 'must be one of' error but got: ${report.issues[0].errors}`,
);
).toBeTruthy();
});
});
+331
View File
@@ -0,0 +1,331 @@
import { expect, test, describe } from "bun:test";
import { mkdtemp, rm, mkdir, writeFile } from "node:fs/promises";
import os from "node:os";
import * as path from "@std/path";
import { checkMissingLocks, runLint } from "../src/commands/lint/lint.ts";
async function withTempDir(
fn: (tempDir: string) => Promise<void>,
): Promise<void> {
const tempDir = await mkdtemp(path.join(os.tmpdir(), "wmill_lint_locks_"));
const originalCwd = process.cwd();
try {
process.chdir(tempDir);
await fn(tempDir);
} finally {
process.chdir(originalCwd);
await rm(tempDir, { recursive: true });
}
}
// Helper to create a script with metadata and optional lock
async function createScript(
tempDir: string,
scriptBase: string,
ext: string,
opts: { lock?: string; lockFileContent?: string } = {},
) {
const dir = path.dirname(path.join(tempDir, scriptBase));
await mkdir(dir, { recursive: true });
// Script content file
await writeFile(path.join(tempDir, scriptBase + ext), "# placeholder", "utf-8");
// Metadata YAML
const lockLine = opts.lock !== undefined ? `lock: "${opts.lock}"` : "lock: ''";
await writeFile(
path.join(tempDir, scriptBase + ".script.yaml"),
`summary: test\n${lockLine}\nschema:\n properties: {}\n`,
"utf-8",
);
// Lock file (if inline reference)
if (opts.lockFileContent !== undefined) {
await writeFile(
path.join(tempDir, scriptBase + ".script.lock"),
opts.lockFileContent,
"utf-8",
);
}
}
// --- checkMissingLocks unit tests ---
describe("checkMissingLocks", () => {
test("reports missing lock for python script", async () => {
await withTempDir(async (tempDir) => {
await createScript(tempDir, "f/my_script", ".py", { lock: "" });
const issues = await checkMissingLocks({} as any, tempDir);
expect(issues.length).toBe(1);
expect(issues[0].target).toBe("script");
expect(issues[0].errors[0]).toContain("Missing lock");
expect(issues[0].errors[0]).toContain("python3");
});
});
test("no issues for python script with inline lock file", async () => {
await withTempDir(async (tempDir) => {
await createScript(tempDir, "f/my_script", ".py", {
lock: "!inline f/my_script.script.lock",
lockFileContent: "some-dep==1.0.0",
});
const issues = await checkMissingLocks({} as any, tempDir);
expect(issues.length).toBe(0);
});
});
test("reports missing lock when inline lock file is empty", async () => {
await withTempDir(async (tempDir) => {
await createScript(tempDir, "f/my_script", ".py", {
lock: "!inline f/my_script.script.lock",
lockFileContent: "",
});
const issues = await checkMissingLocks({} as any, tempDir);
expect(issues.length).toBe(1);
expect(issues[0].errors[0]).toContain("Missing lock");
});
});
test("no issues for bash script without lock (lock not required)", async () => {
await withTempDir(async (tempDir) => {
await createScript(tempDir, "f/my_bash", ".sh", { lock: "" });
const issues = await checkMissingLocks({} as any, tempDir);
expect(issues.length).toBe(0);
});
});
test("reports missing lock for bun script", async () => {
await withTempDir(async (tempDir) => {
await createScript(tempDir, "f/my_ts", ".ts", { lock: "" });
const issues = await checkMissingLocks(
{ defaultTs: "bun" } as any,
tempDir,
);
expect(issues.length).toBe(1);
expect(issues[0].errors[0]).toContain("Missing lock");
expect(issues[0].errors[0]).toContain("bun");
});
});
test("reports missing lock for flow inline rawscript", async () => {
await withTempDir(async (tempDir) => {
await mkdir(`${tempDir}/f/my_flow.flow`, { recursive: true });
await writeFile(
`${tempDir}/f/my_flow.flow/flow.yaml`,
`summary: test flow
value:
modules:
- id: step1
value:
type: rawscript
language: python3
content: "print('hello')"
`,
"utf-8",
);
const issues = await checkMissingLocks({} as any, tempDir);
expect(issues.length).toBe(1);
expect(issues[0].target).toBe("flow_inline_script");
expect(issues[0].errors[0]).toContain("step1");
expect(issues[0].errors[0]).toContain("python3");
});
});
test("no issues for flow inline rawscript with lock", async () => {
await withTempDir(async (tempDir) => {
await mkdir(`${tempDir}/f/my_flow.flow`, { recursive: true });
await writeFile(
`${tempDir}/f/my_flow.flow/flow.yaml`,
`summary: test flow
value:
modules:
- id: step1
value:
type: rawscript
language: python3
content: "print('hello')"
lock: "some-dep==1.0.0"
`,
"utf-8",
);
const issues = await checkMissingLocks({} as any, tempDir);
expect(issues.length).toBe(0);
});
});
test("reports missing lock for nested flow modules (forloopflow)", async () => {
await withTempDir(async (tempDir) => {
await mkdir(`${tempDir}/f/my_flow.flow`, { recursive: true });
await writeFile(
`${tempDir}/f/my_flow.flow/flow.yaml`,
`summary: test flow
value:
modules:
- id: loop1
value:
type: forloopflow
modules:
- id: inner_step
value:
type: rawscript
language: python3
content: "print('inner')"
`,
"utf-8",
);
const issues = await checkMissingLocks({} as any, tempDir);
expect(issues.length).toBe(1);
expect(issues[0].errors[0]).toContain("inner_step");
});
});
test("reports missing lock for app inline script", async () => {
await withTempDir(async (tempDir) => {
await mkdir(`${tempDir}/f/my_app.app`, { recursive: true });
await writeFile(
`${tempDir}/f/my_app.app/app.yaml`,
`value:
grid:
- data:
inlineScript:
language: python3
content: "x = 1"
`,
"utf-8",
);
const issues = await checkMissingLocks({} as any, tempDir);
expect(issues.length).toBe(1);
expect(issues[0].target).toBe("app_inline_script");
expect(issues[0].errors[0]).toContain("python3");
});
});
test("no issues for app inline script with lock", async () => {
await withTempDir(async (tempDir) => {
await mkdir(`${tempDir}/f/my_app.app`, { recursive: true });
await writeFile(
`${tempDir}/f/my_app.app/app.yaml`,
`value:
grid:
- data:
inlineScript:
language: python3
content: "x = 1"
lock: "some-dep==1.0.0"
`,
"utf-8",
);
const issues = await checkMissingLocks({} as any, tempDir);
expect(issues.length).toBe(0);
});
});
test("no issues for flow with non-lock-requiring language (bash)", async () => {
await withTempDir(async (tempDir) => {
await mkdir(`${tempDir}/f/my_flow.flow`, { recursive: true });
await writeFile(
`${tempDir}/f/my_flow.flow/flow.yaml`,
`summary: test flow
value:
modules:
- id: step1
value:
type: rawscript
language: bash
content: "echo hello"
`,
"utf-8",
);
const issues = await checkMissingLocks({} as any, tempDir);
expect(issues.length).toBe(0);
});
});
test("skips raw app without backend folder", async () => {
await withTempDir(async (tempDir) => {
await mkdir(`${tempDir}/f/my_rawapp.raw_app`, { recursive: true });
await writeFile(
`${tempDir}/f/my_rawapp.raw_app/raw_app.yaml`,
`summary: test raw app
`,
"utf-8",
);
// No backend/ folder created
const issues = await checkMissingLocks({} as any, tempDir);
expect(issues.length).toBe(0);
});
});
});
// --- runLint --locks-required integration tests ---
describe("runLint with --locks-required", () => {
test("reports lock issues when locksRequired is true", async () => {
await withTempDir(async (tempDir) => {
await createScript(tempDir, "f/my_script", ".py", { lock: "" });
const report = await runLint({ locksRequired: true } as any, tempDir);
expect(report.success).toBe(false);
expect(report.exitCode).toBe(1);
expect(report.issues.length).toBeGreaterThanOrEqual(1);
expect(
report.issues.some((i) => i.errors.some((e) => e.includes("Missing lock"))),
).toBe(true);
});
});
test("does not check locks when locksRequired is false", async () => {
await withTempDir(async (tempDir) => {
await createScript(tempDir, "f/my_script", ".py", { lock: "" });
const report = await runLint({} as any, tempDir);
// Without locksRequired, no lock issues should appear
expect(
report.issues.some((i) => i.errors.some((e) => e.includes("Missing lock"))),
).toBe(false);
});
});
test("passes when locksRequired is true and locks exist", async () => {
await withTempDir(async (tempDir) => {
await createScript(tempDir, "f/my_script", ".py", {
lock: "!inline f/my_script.script.lock",
lockFileContent: "some-dep==1.0.0",
});
const report = await runLint({ locksRequired: true } as any, tempDir);
expect(report.success).toBe(true);
expect(report.exitCode).toBe(0);
expect(
report.issues.some((i) => i.errors.some((e) => e.includes("Missing lock"))),
).toBe(false);
});
});
});
+94
View File
@@ -0,0 +1,94 @@
/**
* Unit tests for local_encryption.ts encrypt/decrypt functions.
* Tests round-trip encryption, different key lengths, and error handling.
*/
import { expect, test, describe } from "bun:test";
import { encrypt, decrypt } from "../src/utils/local_encryption.ts";
// =============================================================================
// encrypt / decrypt round-trip
// =============================================================================
describe("encrypt and decrypt", () => {
test("round-trip with a simple message", async () => {
const key = "my-secret-key";
const message = "Hello, World!";
const encrypted = await encrypt(message, key);
const decrypted = await decrypt(encrypted, key);
expect(decrypted).toBe(message);
});
test("round-trip with empty string", async () => {
const key = "key";
const encrypted = await encrypt("", key);
const decrypted = await decrypt(encrypted, key);
expect(decrypted).toBe("");
});
test("round-trip with long message", async () => {
const key = "test-key-123";
const message = "A".repeat(10000);
const encrypted = await encrypt(message, key);
const decrypted = await decrypt(encrypted, key);
expect(decrypted).toBe(message);
});
test("round-trip with unicode characters", async () => {
const key = "unicode-key";
const message = "Hello 🌍 世界 مرحبا";
const encrypted = await encrypt(message, key);
const decrypted = await decrypt(encrypted, key);
expect(decrypted).toBe(message);
});
test("round-trip with very short key", async () => {
const key = "k";
const message = "short key test";
const encrypted = await encrypt(message, key);
const decrypted = await decrypt(encrypted, key);
expect(decrypted).toBe(message);
});
test("round-trip with very long key", async () => {
const key = "x".repeat(1000);
const message = "long key test";
const encrypted = await encrypt(message, key);
const decrypted = await decrypt(encrypted, key);
expect(decrypted).toBe(message);
});
test("encrypted output is base64", async () => {
const encrypted = await encrypt("test", "key");
// base64 characters: A-Z, a-z, 0-9, +, /, =
expect(encrypted).toMatch(/^[A-Za-z0-9+/=]+$/);
});
test("same message encrypted twice produces different ciphertexts (random IV)", async () => {
const key = "determinism-test";
const message = "same input";
const enc1 = await encrypt(message, key);
const enc2 = await encrypt(message, key);
expect(enc1).not.toBe(enc2);
});
test("decrypting with wrong key throws", async () => {
const encrypted = await encrypt("secret", "correct-key");
await expect(decrypt(encrypted, "wrong-key")).rejects.toThrow();
});
test("decrypting corrupted ciphertext throws", async () => {
await expect(decrypt("not-valid-ciphertext-at-all!!", "key")).rejects.toThrow();
});
test("round-trip with JSON content", async () => {
const key = "json-key";
const message = JSON.stringify({ license_key: "abc-123", secret: true });
const encrypted = await encrypt(message, key);
const decrypted = await decrypt(encrypted, key);
expect(JSON.parse(decrypted)).toEqual({
license_key: "abc-123",
secret: true,
});
});
});
+81 -108
View File
@@ -10,11 +10,8 @@
* vs new logic (caches by key, skips duplicate fetches).
*/
import {
assertEquals,
assertNotEquals,
} from "https://deno.land/std@0.224.0/assert/mod.ts";
import { encodeHex } from "https://deno.land/std@0.224.0/encoding/hex.ts";
import { expect, test } from "bun:test";
import { encodeHex } from "@std/encoding";
// ---------------------------------------------------------------------------
// Mirrors extractWorkspaceDepsAnnotation + computeLockCacheKey from
@@ -163,7 +160,7 @@ async function fetchScriptLockNew(
// Part 1 — Annotation parsing
// =============================================================================
Deno.test("python: manual requirements with external refs + inline deps", () => {
test("python: manual requirements with external refs + inline deps", () => {
const code = `# requirements: default, base
#requests==2.31.0
#pandas>=1.5.0
@@ -171,40 +168,40 @@ Deno.test("python: manual requirements with external refs + inline deps", () =>
def main():
pass`;
const r = extractWorkspaceDepsAnnotation(code, "python3")!;
assertEquals(r.mode, "manual");
assertEquals(r.external, ["default", "base"]);
assertEquals(r.inline, "requests==2.31.0\npandas>=1.5.0");
expect(r.mode).toEqual("manual");
expect(r.external).toEqual(["default", "base"]);
expect(r.inline).toEqual("requests==2.31.0\npandas>=1.5.0");
});
Deno.test("python: extra_requirements mode", () => {
test("python: extra_requirements mode", () => {
const code = `# extra_requirements: utils
#numpy>=1.24.0
def main():
pass`;
const r = extractWorkspaceDepsAnnotation(code, "python3")!;
assertEquals(r.mode, "extra");
assertEquals(r.external, ["utils"]);
assertEquals(r.inline, "numpy>=1.24.0");
expect(r.mode).toEqual("extra");
expect(r.external).toEqual(["utils"]);
expect(r.inline).toEqual("numpy>=1.24.0");
});
Deno.test("python: empty requirements (opt-out)", () => {
test("python: empty requirements (opt-out)", () => {
const code = `# requirements:
def main():
pass`;
const r = extractWorkspaceDepsAnnotation(code, "python3")!;
assertEquals(r.mode, "manual");
assertEquals(r.external, []);
assertEquals(r.inline, null);
expect(r.mode).toEqual("manual");
expect(r.external).toEqual([]);
expect(r.inline).toEqual(null);
});
Deno.test("python: no annotation → null", () => {
test("python: no annotation → null", () => {
const code = `def main():
print("hello")`;
assertEquals(extractWorkspaceDepsAnnotation(code, "python3"), null);
expect(extractWorkspaceDepsAnnotation(code, "python3")).toEqual(null);
});
Deno.test("bun: package_json annotation with inline", () => {
test("bun: package_json annotation with inline", () => {
const code = `// package_json: utils, base
//{
// "dependencies": {
@@ -214,47 +211,47 @@ Deno.test("bun: package_json annotation with inline", () => {
export function main() {}`;
const r = extractWorkspaceDepsAnnotation(code, "bun")!;
assertEquals(r.mode, "manual");
assertEquals(r.external, ["utils", "base"]);
assertEquals(r.inline, `{
expect(r.mode).toEqual("manual");
expect(r.external).toEqual(["utils", "base"]);
expect(r.inline).toEqual(`{
"dependencies": {
"axios": "^1.6.0"
}
}`);
});
Deno.test("go: go_mod annotation", () => {
test("go: go_mod annotation", () => {
const code = `// go_mod: base,
//github.com/gin-gonic/gin v1.9.1
package main
func main() {}`;
const r = extractWorkspaceDepsAnnotation(code, "go")!;
assertEquals(r.mode, "manual");
assertEquals(r.external, ["base"]);
assertEquals(r.inline, "github.com/gin-gonic/gin v1.9.1");
expect(r.mode).toEqual("manual");
expect(r.external).toEqual(["base"]);
expect(r.inline).toEqual("github.com/gin-gonic/gin v1.9.1");
});
Deno.test("unsupported language → null", () => {
assertEquals(extractWorkspaceDepsAnnotation("print(1)", "deno"), null);
assertEquals(extractWorkspaceDepsAnnotation("print(1)", "bash"), null);
test("unsupported language → null", () => {
expect(extractWorkspaceDepsAnnotation("print(1)", "deno")).toEqual(null);
expect(extractWorkspaceDepsAnnotation("print(1)", "bash")).toEqual(null);
});
// =============================================================================
// Part 2 — Cache key computation
// =============================================================================
Deno.test("same annotation + language + deps → same key", async () => {
test("same annotation + language + deps → same key", async () => {
const code = `# requirements: default
#requests==2.31.0
print("hello")`;
const deps = { "dependencies/requirements.in": "requests==2.31.0" };
const a = await computeLockCacheKey(code, "python3", deps);
const b = await computeLockCacheKey(code, "python3", deps);
assertEquals(a, b);
expect(a).toEqual(b);
});
Deno.test("different code, same annotation → same key", async () => {
test("different code, same annotation → same key", async () => {
const codeA = `# requirements: default
#requests==2.31.0
print("hello")`;
@@ -262,13 +259,10 @@ print("hello")`;
#requests==2.31.0
print("world")`;
const deps = { "dependencies/requirements.in": "requests==2.31.0" };
assertEquals(
await computeLockCacheKey(codeA, "python3", deps),
await computeLockCacheKey(codeB, "python3", deps),
);
expect(await computeLockCacheKey(codeA, "python3", deps)).toEqual(await computeLockCacheKey(codeB, "python3", deps));
});
Deno.test("different annotation inline → different key", async () => {
test("different annotation inline → different key", async () => {
const codeA = `# requirements: default
#requests==2.31.0
print("hello")`;
@@ -276,67 +270,46 @@ print("hello")`;
#flask==3.0.0
print("hello")`;
const deps = { "dependencies/requirements.in": "requests==2.31.0" };
assertNotEquals(
await computeLockCacheKey(codeA, "python3", deps),
await computeLockCacheKey(codeB, "python3", deps),
);
expect(await computeLockCacheKey(codeA, "python3", deps)).not.toEqual(await computeLockCacheKey(codeB, "python3", deps));
});
Deno.test("different annotation external refs → different key", async () => {
test("different annotation external refs → different key", async () => {
const codeA = `# requirements: default
print("hello")`;
const codeB = `# requirements: base
print("hello")`;
const deps = { "dependencies/requirements.in": "requests==2.31.0" };
assertNotEquals(
await computeLockCacheKey(codeA, "python3", deps),
await computeLockCacheKey(codeB, "python3", deps),
);
expect(await computeLockCacheKey(codeA, "python3", deps)).not.toEqual(await computeLockCacheKey(codeB, "python3", deps));
});
Deno.test("manual vs extra mode → different key", async () => {
test("manual vs extra mode → different key", async () => {
const codeA = `# requirements: default
print("hello")`;
const codeB = `# extra_requirements: default
print("hello")`;
const deps = { "dependencies/requirements.in": "requests==2.31.0" };
assertNotEquals(
await computeLockCacheKey(codeA, "python3", deps),
await computeLockCacheKey(codeB, "python3", deps),
);
expect(await computeLockCacheKey(codeA, "python3", deps)).not.toEqual(await computeLockCacheKey(codeB, "python3", deps));
});
Deno.test("no annotation, same code → same key", async () => {
test("no annotation, same code → same key", async () => {
const deps = { "dependencies/requirements.in": "requests==2.31.0" };
assertEquals(
await computeLockCacheKey("print('a')", "python3", deps),
await computeLockCacheKey("print('b')", "python3", deps),
);
expect(await computeLockCacheKey("print('a')", "python3", deps)).toEqual(await computeLockCacheKey("print('b')", "python3", deps));
});
Deno.test("different deps → different key", async () => {
test("different deps → different key", async () => {
const code = `# requirements: default
print("hello")`;
assertNotEquals(
await computeLockCacheKey(code, "python3", { d: "a" }),
await computeLockCacheKey(code, "python3", { d: "b" }),
);
expect(await computeLockCacheKey(code, "python3", { d: "a" })).not.toEqual(await computeLockCacheKey(code, "python3", { d: "b" }));
});
Deno.test("different language → different key", async () => {
test("different language → different key", async () => {
const deps = { d: "v" };
assertNotEquals(
await computeLockCacheKey("x", "bun", deps),
await computeLockCacheKey("x", "python3", deps),
);
expect(await computeLockCacheKey("x", "bun", deps)).not.toEqual(await computeLockCacheKey("x", "python3", deps));
});
Deno.test("dep key order does not matter", async () => {
test("dep key order does not matter", async () => {
const code = "print('hello')";
assertEquals(
await computeLockCacheKey(code, "python3", { a: "1", b: "2" }),
await computeLockCacheKey(code, "python3", { b: "2", a: "1" }),
);
expect(await computeLockCacheKey(code, "python3", { a: "1", b: "2" })).toEqual(await computeLockCacheKey(code, "python3", { b: "2", a: "1" }));
});
// =============================================================================
@@ -360,7 +333,7 @@ function makeRemoteFn(): {
// -- Two scripts, same annotation + language + deps -------------------------
Deno.test("old logic: two scripts same annotation → 2 remote calls", async () => {
test("old logic: two scripts same annotation → 2 remote calls", async () => {
const { remoteFn, callCount } = makeRemoteFn();
const deps = { "dependencies/requirements.in": "requests==2.31.0" };
@@ -370,10 +343,10 @@ Deno.test("old logic: two scripts same annotation → 2 remote calls", async ()
];
for (const s of scripts) await fetchScriptLockOld(s, remoteFn);
assertEquals(callCount(), 2);
expect(callCount()).toEqual(2);
});
Deno.test("new logic: two scripts same annotation → 1 remote call (cache shared)", async () => {
test("new logic: two scripts same annotation → 1 remote call (cache shared)", async () => {
const { remoteFn, callCount } = makeRemoteFn();
const cache = new Map<string, string>();
const deps = { "dependencies/requirements.in": "requests==2.31.0" };
@@ -385,13 +358,13 @@ Deno.test("new logic: two scripts same annotation → 1 remote call (cache share
const results: string[] = [];
for (const s of scripts) results.push(await fetchScriptLockNew(s, remoteFn, cache));
assertEquals(callCount(), 1);
assertEquals(results[0], results[1]);
expect(callCount()).toEqual(1);
expect(results[0]).toEqual(results[1]);
});
// -- Two scripts, different annotations + same deps -------------------------
Deno.test("new logic: different annotations same deps → 2 remote calls", async () => {
test("new logic: different annotations same deps → 2 remote calls", async () => {
const { remoteFn, callCount } = makeRemoteFn();
const cache = new Map<string, string>();
const deps = { "dependencies/requirements.in": "requests==2.31.0" };
@@ -403,13 +376,13 @@ Deno.test("new logic: different annotations same deps → 2 remote calls", async
const results: string[] = [];
for (const s of scripts) results.push(await fetchScriptLockNew(s, remoteFn, cache));
assertEquals(callCount(), 2);
assertNotEquals(results[0], results[1]);
expect(callCount()).toEqual(2);
expect(results[0]).not.toEqual(results[1]);
});
// -- Two scripts, same annotation + different deps --------------------------
Deno.test("new logic: same annotation different deps → 2 remote calls", async () => {
test("new logic: same annotation different deps → 2 remote calls", async () => {
const { remoteFn, callCount } = makeRemoteFn();
const cache = new Map<string, string>();
@@ -422,13 +395,13 @@ Deno.test("new logic: same annotation different deps → 2 remote calls", async
const results: string[] = [];
for (const s of scripts) results.push(await fetchScriptLockNew(s, remoteFn, cache));
assertEquals(callCount(), 2);
assertNotEquals(results[0], results[1]);
expect(callCount()).toEqual(2);
expect(results[0]).not.toEqual(results[1]);
});
// -- Many scripts, same annotation + deps -----------------------------------
Deno.test("old logic: 5 scripts same annotation+deps → 5 remote calls", async () => {
test("old logic: 5 scripts same annotation+deps → 5 remote calls", async () => {
const { remoteFn, callCount } = makeRemoteFn();
const deps = { "dependencies/requirements.in": "requests==2.31.0" };
const ann = "# requirements: default\n";
@@ -442,10 +415,10 @@ Deno.test("old logic: 5 scripts same annotation+deps → 5 remote calls", async
];
for (const s of scripts) await fetchScriptLockOld(s, remoteFn);
assertEquals(callCount(), 5);
expect(callCount()).toEqual(5);
});
Deno.test("new logic: 5 scripts same annotation+deps → 1 remote call", async () => {
test("new logic: 5 scripts same annotation+deps → 1 remote call", async () => {
const { remoteFn, callCount } = makeRemoteFn();
const cache = new Map<string, string>();
const deps = { "dependencies/requirements.in": "requests==2.31.0" };
@@ -461,15 +434,15 @@ Deno.test("new logic: 5 scripts same annotation+deps → 1 remote call", async (
const results: string[] = [];
for (const s of scripts) results.push(await fetchScriptLockNew(s, remoteFn, cache));
assertEquals(callCount(), 1);
expect(callCount()).toEqual(1);
for (let i = 1; i < results.length; i++) {
assertEquals(results[0], results[i]);
expect(results[0]).toEqual(results[i]);
}
});
// -- Many scripts, 2 annotation groups + same deps -------------------------
Deno.test("new logic: 4 scripts with 2 annotation groups → 2 remote calls", async () => {
test("new logic: 4 scripts with 2 annotation groups → 2 remote calls", async () => {
const { remoteFn, callCount } = makeRemoteFn();
const cache = new Map<string, string>();
const deps = { "dependencies/requirements.in": "requests==2.31.0" };
@@ -483,15 +456,15 @@ Deno.test("new logic: 4 scripts with 2 annotation groups → 2 remote calls", as
const results: string[] = [];
for (const s of scripts) results.push(await fetchScriptLockNew(s, remoteFn, cache));
assertEquals(callCount(), 2);
assertEquals(results[0], results[2]); // same annotation "default"
assertEquals(results[1], results[3]); // same annotation "base"
assertNotEquals(results[0], results[1]);
expect(callCount()).toEqual(2);
expect(results[0]).toEqual(results[2]); // same annotation "default"
expect(results[1]).toEqual(results[3]); // same annotation "base"
expect(results[0]).not.toEqual(results[1]);
});
// -- Scripts with no workspace deps (empty) ---------------------------------
Deno.test("new logic: empty deps → no caching", async () => {
test("new logic: empty deps → no caching", async () => {
const { remoteFn, callCount } = makeRemoteFn();
const cache = new Map<string, string>();
@@ -501,13 +474,13 @@ Deno.test("new logic: empty deps → no caching", async () => {
];
for (const s of scripts) await fetchScriptLockNew(s, remoteFn, cache);
assertEquals(callCount(), 2);
assertEquals(cache.size, 0);
expect(callCount()).toEqual(2);
expect(cache.size).toEqual(0);
});
// -- No annotation scripts with raw deps → share cache ---------------------
Deno.test("new logic: no annotation + same deps → 1 remote call", async () => {
test("new logic: no annotation + same deps → 1 remote call", async () => {
const { remoteFn, callCount } = makeRemoteFn();
const cache = new Map<string, string>();
const deps = { "dependencies/requirements.in": "requests==2.31.0" };
@@ -519,13 +492,13 @@ Deno.test("new logic: no annotation + same deps → 1 remote call", async () =>
const results: string[] = [];
for (const s of scripts) results.push(await fetchScriptLockNew(s, remoteFn, cache));
assertEquals(callCount(), 1);
assertEquals(results[0], results[1]);
expect(callCount()).toEqual(1);
expect(results[0]).toEqual(results[1]);
});
// -- Mix of annotated and non-annotated scripts -----------------------------
Deno.test("new logic: mix of annotated and non-annotated → separate cache groups", async () => {
test("new logic: mix of annotated and non-annotated → separate cache groups", async () => {
const { remoteFn, callCount } = makeRemoteFn();
const cache = new Map<string, string>();
const deps = { "dependencies/requirements.in": "requests==2.31.0" };
@@ -539,15 +512,15 @@ Deno.test("new logic: mix of annotated and non-annotated → separate cache grou
const results: string[] = [];
for (const s of scripts) results.push(await fetchScriptLockNew(s, remoteFn, cache));
assertEquals(callCount(), 2); // one for annotated group, one for no-annotation group
assertEquals(results[0], results[2]); // both annotated "default"
assertEquals(results[1], results[3]); // both no annotation
assertNotEquals(results[0], results[1]); // annotated ≠ non-annotated
expect(callCount()).toEqual(2); // one for annotated group, one for no-annotation group
expect(results[0]).toEqual(results[2]); // both annotated "default"
expect(results[1]).toEqual(results[3]); // both no annotation
expect(results[0]).not.toEqual(results[1]); // annotated ≠ non-annotated
});
// -- Cache returns correct lock value ---------------------------------------
Deno.test("new logic: cached value matches original remote response", async () => {
test("new logic: cached value matches original remote response", async () => {
const cache = new Map<string, string>();
const deps = { "dependencies/requirements.in": "requests==2.31.0" };
@@ -566,7 +539,7 @@ Deno.test("new logic: cached value matches original remote response", async () =
remoteFn, cache,
);
assertEquals(callIdx, 1);
assertEquals(r1, "resolved-lock-content-abc123");
assertEquals(r2, "resolved-lock-content-abc123");
expect(callIdx).toEqual(1);
expect(r1).toEqual("resolved-lock-content-abc123");
expect(r2).toEqual("resolved-lock-content-abc123");
});
-620
View File
@@ -1,620 +0,0 @@
import {
assert,
assertEquals,
} from "https://deno.land/std@0.224.0/assert/mod.ts";
import {
checkMissingLocks,
runLint,
} from "../src/commands/lint/lint.ts";
async function withTempDir(
fn: (tempDir: string) => Promise<void>,
): Promise<void> {
const tempDir = await Deno.makeTempDir({ prefix: "wmill_locks_test_" });
const originalCwd = Deno.cwd();
try {
Deno.chdir(tempDir);
await fn(tempDir);
} finally {
Deno.chdir(originalCwd);
await Deno.remove(tempDir, { recursive: true });
}
}
// --- checkMissingLocks unit tests ---
Deno.test("locks-required: passes for python script with non-empty lock file", async () => {
await withTempDir(async (tempDir) => {
await Deno.mkdir(`${tempDir}/f/folder`, { recursive: true });
await Deno.writeTextFile(
`${tempDir}/f/folder/my_script.py`,
`import pandas\ndef main(): pass`,
);
await Deno.writeTextFile(
`${tempDir}/f/folder/my_script.script.yaml`,
`summary: ""\ndescription: ""\nlock: "!inline f/folder/my_script.script.lock"\nkind: script\nschema:\n $schema: "https://json-schema.org/draft/2020-12/schema"\n type: object\n properties: {}\n required: []\n`,
);
await Deno.writeTextFile(
`${tempDir}/f/folder/my_script.script.lock`,
`pandas==2.0.0\nnumpy==1.24.0\n`,
);
const issues = await checkMissingLocks({} as any, tempDir);
assertEquals(issues.length, 0);
});
});
Deno.test("locks-required: fails for python script with empty lock file", async () => {
await withTempDir(async (tempDir) => {
await Deno.mkdir(`${tempDir}/f/folder`, { recursive: true });
await Deno.writeTextFile(
`${tempDir}/f/folder/my_script.py`,
`def main(): pass`,
);
await Deno.writeTextFile(
`${tempDir}/f/folder/my_script.script.yaml`,
`summary: ""\ndescription: ""\nlock: "!inline f/folder/my_script.script.lock"\nkind: script\nschema:\n $schema: "https://json-schema.org/draft/2020-12/schema"\n type: object\n properties: {}\n required: []\n`,
);
await Deno.writeTextFile(
`${tempDir}/f/folder/my_script.script.lock`,
``,
);
const issues = await checkMissingLocks({} as any, tempDir);
assertEquals(issues.length, 1);
assertEquals(issues[0].target, "script");
assert(issues[0].errors[0].includes("Missing lock"));
assert(issues[0].errors[0].includes("python3"));
});
});
Deno.test("locks-required: fails for python script with missing lock file", async () => {
await withTempDir(async (tempDir) => {
await Deno.mkdir(`${tempDir}/f/folder`, { recursive: true });
await Deno.writeTextFile(
`${tempDir}/f/folder/my_script.py`,
`def main(): pass`,
);
await Deno.writeTextFile(
`${tempDir}/f/folder/my_script.script.yaml`,
`summary: ""\ndescription: ""\nlock: "!inline f/folder/nonexistent.script.lock"\nkind: script\nschema:\n $schema: "https://json-schema.org/draft/2020-12/schema"\n type: object\n properties: {}\n required: []\n`,
);
const issues = await checkMissingLocks({} as any, tempDir);
assertEquals(issues.length, 1);
assertEquals(issues[0].target, "script");
});
});
Deno.test("locks-required: fails for python script with lock field empty string", async () => {
await withTempDir(async (tempDir) => {
await Deno.mkdir(`${tempDir}/f/folder`, { recursive: true });
await Deno.writeTextFile(
`${tempDir}/f/folder/my_script.py`,
`def main(): pass`,
);
await Deno.writeTextFile(
`${tempDir}/f/folder/my_script.script.yaml`,
`summary: ""\ndescription: ""\nlock: ""\nkind: script\nschema:\n $schema: "https://json-schema.org/draft/2020-12/schema"\n type: object\n properties: {}\n required: []\n`,
);
const issues = await checkMissingLocks({} as any, tempDir);
assertEquals(issues.length, 1);
assertEquals(issues[0].target, "script");
});
});
Deno.test("locks-required: skips bash scripts (no locks needed)", async () => {
await withTempDir(async (tempDir) => {
await Deno.mkdir(`${tempDir}/f/folder`, { recursive: true });
await Deno.writeTextFile(
`${tempDir}/f/folder/my_script.sh`,
`#!/bin/bash\necho hello`,
);
await Deno.writeTextFile(
`${tempDir}/f/folder/my_script.script.yaml`,
`summary: ""\ndescription: ""\nlock: ""\nkind: script\nschema:\n $schema: "https://json-schema.org/draft/2020-12/schema"\n type: object\n properties: {}\n required: []\n`,
);
const issues = await checkMissingLocks({} as any, tempDir);
assertEquals(issues.length, 0);
});
});
Deno.test("locks-required: skips SQL scripts (no locks needed)", async () => {
await withTempDir(async (tempDir) => {
await Deno.mkdir(`${tempDir}/f/folder`, { recursive: true });
await Deno.writeTextFile(
`${tempDir}/f/folder/my_query.pg.sql`,
`SELECT 1;`,
);
await Deno.writeTextFile(
`${tempDir}/f/folder/my_query.script.yaml`,
`summary: ""\ndescription: ""\nlock: ""\nkind: script\nschema:\n $schema: "https://json-schema.org/draft/2020-12/schema"\n type: object\n properties: {}\n required: []\n`,
);
const issues = await checkMissingLocks({} as any, tempDir);
assertEquals(issues.length, 0);
});
});
Deno.test("locks-required: checks bun typescript scripts", async () => {
await withTempDir(async (tempDir) => {
await Deno.mkdir(`${tempDir}/f/folder`, { recursive: true });
await Deno.writeTextFile(
`${tempDir}/f/folder/my_script.bun.ts`,
`export async function main() { return "hello"; }`,
);
await Deno.writeTextFile(
`${tempDir}/f/folder/my_script.script.yaml`,
`summary: ""\ndescription: ""\nlock: ""\nkind: script\nschema:\n $schema: "https://json-schema.org/draft/2020-12/schema"\n type: object\n properties: {}\n required: []\n`,
);
const issues = await checkMissingLocks({} as any, tempDir);
assertEquals(issues.length, 1);
assert(issues[0].errors[0].includes("bun"));
});
});
Deno.test("locks-required: checks deno typescript scripts", async () => {
await withTempDir(async (tempDir) => {
await Deno.mkdir(`${tempDir}/f/folder`, { recursive: true });
await Deno.writeTextFile(
`${tempDir}/f/folder/my_script.deno.ts`,
`export async function main() { return "hello"; }`,
);
await Deno.writeTextFile(
`${tempDir}/f/folder/my_script.script.yaml`,
`summary: ""\ndescription: ""\nlock: ""\nkind: script\nschema:\n $schema: "https://json-schema.org/draft/2020-12/schema"\n type: object\n properties: {}\n required: []\n`,
);
const issues = await checkMissingLocks({} as any, tempDir);
assertEquals(issues.length, 1);
assert(issues[0].errors[0].includes("deno"));
});
});
// --- Flow inline script tests ---
Deno.test("locks-required: fails for flow with unlocked inline python script", async () => {
await withTempDir(async (tempDir) => {
await Deno.mkdir(`${tempDir}/f/my_flow.flow`, { recursive: true });
await Deno.writeTextFile(
`${tempDir}/f/my_flow.flow/inline_script_0.inline_script.py`,
`def main(): pass`,
);
await Deno.writeTextFile(
`${tempDir}/f/my_flow.flow/flow.yaml`,
`summary: My flow
value:
modules:
- id: a
value:
type: rawscript
language: python3
content: "!inline inline_script_0.inline_script.py"
lock: ""
`,
);
const issues = await checkMissingLocks({} as any, tempDir);
assertEquals(issues.length, 1);
assertEquals(issues[0].target, "flow_inline_script");
assert(issues[0].errors[0].includes("python3"));
assert(issues[0].errors[0].includes("'a'"));
});
});
Deno.test("locks-required: passes for flow with locked inline python script", async () => {
await withTempDir(async (tempDir) => {
await Deno.mkdir(`${tempDir}/f/my_flow.flow`, { recursive: true });
await Deno.writeTextFile(
`${tempDir}/f/my_flow.flow/inline_script_0.inline_script.py`,
`import pandas\ndef main(): pass`,
);
await Deno.writeTextFile(
`${tempDir}/f/my_flow.flow/inline_script_0.inline_script.lock`,
`pandas==2.0.0\n`,
);
await Deno.writeTextFile(
`${tempDir}/f/my_flow.flow/flow.yaml`,
`summary: My flow
value:
modules:
- id: a
value:
type: rawscript
language: python3
content: "!inline inline_script_0.inline_script.py"
lock: "!inline inline_script_0.inline_script.lock"
`,
);
const issues = await checkMissingLocks({} as any, tempDir);
assertEquals(issues.length, 0);
});
});
Deno.test("locks-required: skips flow inline bash scripts", async () => {
await withTempDir(async (tempDir) => {
await Deno.mkdir(`${tempDir}/f/my_flow.flow`, { recursive: true });
await Deno.writeTextFile(
`${tempDir}/f/my_flow.flow/flow.yaml`,
`summary: My flow
value:
modules:
- id: a
value:
type: rawscript
language: bash
content: "echo hello"
lock: ""
`,
);
const issues = await checkMissingLocks({} as any, tempDir);
assertEquals(issues.length, 0);
});
});
Deno.test("locks-required: checks nested flow modules (forloopflow)", async () => {
await withTempDir(async (tempDir) => {
await Deno.mkdir(`${tempDir}/f/my_flow.flow`, { recursive: true });
await Deno.writeTextFile(
`${tempDir}/f/my_flow.flow/flow.yaml`,
`summary: My flow
value:
modules:
- id: loop
value:
type: forloopflow
modules:
- id: inner
value:
type: rawscript
language: python3
content: "def main(): pass"
`,
);
const issues = await checkMissingLocks({} as any, tempDir);
assertEquals(issues.length, 1);
assert(issues[0].errors[0].includes("'inner'"));
});
});
Deno.test("locks-required: checks nested flow modules (branchone)", async () => {
await withTempDir(async (tempDir) => {
await Deno.mkdir(`${tempDir}/f/my_flow.flow`, { recursive: true });
await Deno.writeTextFile(
`${tempDir}/f/my_flow.flow/flow.yaml`,
`summary: My flow
value:
modules:
- id: branch
value:
type: branchone
branches:
- modules:
- id: branch_script
value:
type: rawscript
language: bun
content: "export async function main() {}"
default:
- id: default_script
value:
type: rawscript
language: python3
content: "def main(): pass"
`,
);
const issues = await checkMissingLocks({} as any, tempDir);
assertEquals(issues.length, 2);
const ids = issues.map((i) => i.errors[0]);
assert(ids.some((e) => e.includes("'branch_script'")));
assert(ids.some((e) => e.includes("'default_script'")));
});
});
// --- Integration with runLint ---
Deno.test("locks-required: runLint includes lock issues when flag is set", async () => {
await withTempDir(async (tempDir) => {
await Deno.mkdir(`${tempDir}/f/folder`, { recursive: true });
await Deno.writeTextFile(
`${tempDir}/f/folder/my_script.py`,
`def main(): pass`,
);
await Deno.writeTextFile(
`${tempDir}/f/folder/my_script.script.yaml`,
`summary: ""\ndescription: ""\nlock: ""\nkind: script\nschema:\n $schema: "https://json-schema.org/draft/2020-12/schema"\n type: object\n properties: {}\n required: []\n`,
);
const report = await runLint({ locksRequired: true } as any, tempDir);
assertEquals(report.success, false);
assertEquals(report.exitCode, 1);
assert(report.issues.some((i) => i.target === "script"));
});
});
Deno.test("locks-required: runLint skips lock check when flag is not set", async () => {
await withTempDir(async (tempDir) => {
await Deno.mkdir(`${tempDir}/f/folder`, { recursive: true });
await Deno.writeTextFile(
`${tempDir}/f/folder/my_script.py`,
`def main(): pass`,
);
await Deno.writeTextFile(
`${tempDir}/f/folder/my_script.script.yaml`,
`summary: ""\ndescription: ""\nlock: ""\nkind: script\nschema:\n $schema: "https://json-schema.org/draft/2020-12/schema"\n type: object\n properties: {}\n required: []\n`,
);
const report = await runLint({} as any, tempDir);
assertEquals(report.success, true);
assertEquals(report.exitCode, 0);
assertEquals(report.issues.length, 0);
});
});
// --- Multiple scripts ---
Deno.test("locks-required: reports multiple missing locks", async () => {
await withTempDir(async (tempDir) => {
await Deno.mkdir(`${tempDir}/f/folder`, { recursive: true });
// Python script without lock
await Deno.writeTextFile(
`${tempDir}/f/folder/script1.py`,
`def main(): pass`,
);
await Deno.writeTextFile(
`${tempDir}/f/folder/script1.script.yaml`,
`summary: ""\ndescription: ""\nlock: ""\nkind: script\nschema:\n $schema: "https://json-schema.org/draft/2020-12/schema"\n type: object\n properties: {}\n required: []\n`,
);
// Go script without lock
await Deno.writeTextFile(
`${tempDir}/f/folder/script2.go`,
`package main\nfunc main() {}`,
);
await Deno.writeTextFile(
`${tempDir}/f/folder/script2.script.yaml`,
`summary: ""\ndescription: ""\nlock: ""\nkind: script\nschema:\n $schema: "https://json-schema.org/draft/2020-12/schema"\n type: object\n properties: {}\n required: []\n`,
);
// Bash script (should pass - no lock needed)
await Deno.writeTextFile(
`${tempDir}/f/folder/script3.sh`,
`#!/bin/bash\necho ok`,
);
await Deno.writeTextFile(
`${tempDir}/f/folder/script3.script.yaml`,
`summary: ""\ndescription: ""\nlock: ""\nkind: script\nschema:\n $schema: "https://json-schema.org/draft/2020-12/schema"\n type: object\n properties: {}\n required: []\n`,
);
const issues = await checkMissingLocks({} as any, tempDir);
assertEquals(issues.length, 2);
assert(issues.every((i) => i.target === "script"));
});
});
// --- Normal app inline script tests ---
Deno.test("locks-required: fails for app with unlocked inline python script", async () => {
await withTempDir(async (tempDir) => {
await Deno.mkdir(`${tempDir}/f/my_app.app`, { recursive: true });
await Deno.writeTextFile(
`${tempDir}/f/my_app.app/app.yaml`,
`summary: My app
value:
grid:
- data:
inlineScript:
content: "def main(): pass"
language: python3
lock: ""
`,
);
const issues = await checkMissingLocks({} as any, tempDir);
assertEquals(issues.length, 1);
assertEquals(issues[0].target, "app_inline_script");
assert(issues[0].errors[0].includes("python3"));
});
});
Deno.test("locks-required: passes for app with locked inline python script", async () => {
await withTempDir(async (tempDir) => {
await Deno.mkdir(`${tempDir}/f/my_app.app`, { recursive: true });
await Deno.writeTextFile(
`${tempDir}/f/my_app.app/inline_script_0.inline_script.lock`,
`pandas==2.0.0\n`,
);
await Deno.writeTextFile(
`${tempDir}/f/my_app.app/app.yaml`,
`summary: My app
value:
grid:
- data:
inlineScript:
content: "import pandas"
language: python3
lock: "!inline inline_script_0.inline_script.lock"
`,
);
const issues = await checkMissingLocks({} as any, tempDir);
assertEquals(issues.length, 0);
});
});
Deno.test("locks-required: skips app inline bash scripts", async () => {
await withTempDir(async (tempDir) => {
await Deno.mkdir(`${tempDir}/f/my_app.app`, { recursive: true });
await Deno.writeTextFile(
`${tempDir}/f/my_app.app/app.yaml`,
`summary: My app
value:
grid:
- data:
inlineScript:
content: "echo hello"
language: bash
lock: ""
`,
);
const issues = await checkMissingLocks({} as any, tempDir);
assertEquals(issues.length, 0);
});
});
Deno.test("locks-required: finds deeply nested app inline scripts", async () => {
await withTempDir(async (tempDir) => {
await Deno.mkdir(`${tempDir}/f/my_app.app`, { recursive: true });
await Deno.writeTextFile(
`${tempDir}/f/my_app.app/app.yaml`,
`summary: My app
value:
grid:
- components:
- nested:
deeper:
inlineScript:
content: "export async function main() {}"
language: bun
lock: ""
`,
);
const issues = await checkMissingLocks({} as any, tempDir);
assertEquals(issues.length, 1);
assertEquals(issues[0].target, "app_inline_script");
assert(issues[0].errors[0].includes("bun"));
});
});
// --- Raw app backend script tests ---
Deno.test("locks-required: fails for raw app with unlocked backend python script", async () => {
await withTempDir(async (tempDir) => {
await Deno.mkdir(`${tempDir}/f/my_app.raw_app/backend`, { recursive: true });
await Deno.writeTextFile(
`${tempDir}/f/my_app.raw_app/raw_app.yaml`,
`summary: My raw app
`,
);
await Deno.writeTextFile(
`${tempDir}/f/my_app.raw_app/backend/get_data.yaml`,
`type: inline
`,
);
await Deno.writeTextFile(
`${tempDir}/f/my_app.raw_app/backend/get_data.py`,
`def main(): pass`,
);
const issues = await checkMissingLocks({} as any, tempDir);
assertEquals(issues.length, 1);
assertEquals(issues[0].target, "raw_app_inline_script");
assert(issues[0].errors[0].includes("python3"));
assert(issues[0].errors[0].includes("get_data"));
});
});
Deno.test("locks-required: passes for raw app with locked backend python script", async () => {
await withTempDir(async (tempDir) => {
await Deno.mkdir(`${tempDir}/f/my_app.raw_app/backend`, { recursive: true });
await Deno.writeTextFile(
`${tempDir}/f/my_app.raw_app/raw_app.yaml`,
`summary: My raw app
`,
);
await Deno.writeTextFile(
`${tempDir}/f/my_app.raw_app/backend/get_data.yaml`,
`type: inline
`,
);
await Deno.writeTextFile(
`${tempDir}/f/my_app.raw_app/backend/get_data.py`,
`import pandas\ndef main(): pass`,
);
await Deno.writeTextFile(
`${tempDir}/f/my_app.raw_app/backend/get_data.lock`,
`pandas==2.0.0\n`,
);
const issues = await checkMissingLocks({} as any, tempDir);
assertEquals(issues.length, 0);
});
});
Deno.test("locks-required: raw app auto-detects code files without YAML config", async () => {
await withTempDir(async (tempDir) => {
await Deno.mkdir(`${tempDir}/f/my_app.raw_app/backend`, { recursive: true });
await Deno.writeTextFile(
`${tempDir}/f/my_app.raw_app/raw_app.yaml`,
`summary: My raw app
`,
);
// No .yaml config, just a code file
await Deno.writeTextFile(
`${tempDir}/f/my_app.raw_app/backend/fetch_users.bun.ts`,
`export async function main() { return []; }`,
);
const issues = await checkMissingLocks({} as any, tempDir);
assertEquals(issues.length, 1);
assertEquals(issues[0].target, "raw_app_inline_script");
assert(issues[0].errors[0].includes("bun"));
assert(issues[0].errors[0].includes("fetch_users"));
});
});
Deno.test("locks-required: skips raw app bash backend scripts", async () => {
await withTempDir(async (tempDir) => {
await Deno.mkdir(`${tempDir}/f/my_app.raw_app/backend`, { recursive: true });
await Deno.writeTextFile(
`${tempDir}/f/my_app.raw_app/raw_app.yaml`,
`summary: My raw app
`,
);
await Deno.writeTextFile(
`${tempDir}/f/my_app.raw_app/backend/cleanup.yaml`,
`type: inline
`,
);
await Deno.writeTextFile(
`${tempDir}/f/my_app.raw_app/backend/cleanup.sh`,
`#!/bin/bash\necho done`,
);
const issues = await checkMissingLocks({} as any, tempDir);
assertEquals(issues.length, 0);
});
});
+100 -143
View File
@@ -12,9 +12,9 @@
* 3. The modifications are correctly applied on the server
*/
import { assertEquals, assert } from "https://deno.land/std@0.224.0/assert/mod.ts";
import { ensureDir } from "https://deno.land/std@0.224.0/fs/mod.ts";
import * as path from "https://deno.land/std@0.224.0/path/mod.ts";
import { expect, test } from "bun:test";
import * as path from "@std/path";
import { writeFile, readFile, stat } from "node:fs/promises";
import { withTestBackend } from "./test_backend.ts";
import { addWorkspace } from "../workspace.ts";
import { parseJsonFromCLIOutput } from "./test_config_helpers.ts";
@@ -250,27 +250,19 @@ async function verifyNoDiffOnPull(backend: any, tempDir: string): Promise<void>
["sync", "pull", "--yes", "--dry-run", "--json-output"],
tempDir
);
assertEquals(pullResult.code, 0, `Pull for diff check should succeed: ${pullResult.stderr}`);
expect(pullResult.code).toEqual(0);
const output = parseJsonFromCLIOutput(pullResult.stdout);
const changes = output.changes || [];
assertEquals(
changes.length,
0,
`Should have no changes after push, but found: ${JSON.stringify(changes.map((c: any) => c.path))}`
);
expect(changes.length).toEqual(0);
}
// =============================================================================
// TESTS
// =============================================================================
Deno.test({
name: "Mixed Case Paths: pull and push script with capitalized folder",
sanitizeResources: false,
sanitizeOps: false,
fn: async () => {
test("Mixed Case Paths: pull and push script with capitalized folder", async () => {
await withTestBackend(async (backend, tempDir) => {
await setupWorkspaceProfile(backend);
@@ -285,56 +277,51 @@ Deno.test({
await createScript(backend, scriptPath, originalContent, "My Test Script");
// Create wmill.yaml
await Deno.writeTextFile(
await writeFile(
path.join(tempDir, "wmill.yaml"),
`defaultTs: bun
includes:
- "**"
excludes: []
`
`,
"utf-8"
);
// Pull
const pullResult = await backend.runCLICommand(["sync", "pull", "--yes"], tempDir);
assertEquals(pullResult.code, 0, `Pull should succeed: ${pullResult.stderr}`);
expect(pullResult.code).toEqual(0);
// Verify file exists with correct path (normalized for comparison)
const expectedScriptPath = path.join(tempDir, "f", "MyFolder", "MyScript.ts");
const scriptExists = await Deno.stat(expectedScriptPath).then(() => true).catch(() => false);
assert(scriptExists, `Script file should exist at ${expectedScriptPath}`);
const scriptExists = await stat(expectedScriptPath).then(() => true).catch(() => false);
expect(scriptExists).toBeTruthy();
// Read and verify content
const pulledContent = await Deno.readTextFile(expectedScriptPath);
assert(pulledContent.includes("original content"), "Pulled content should match original");
const pulledContent = await readFile(expectedScriptPath, "utf-8");
expect(pulledContent.includes("original content")).toBeTruthy();
// Modify the script
const modifiedContent = `export async function main() {
return "modified content from test";
}`;
await Deno.writeTextFile(expectedScriptPath, modifiedContent);
await writeFile(expectedScriptPath, modifiedContent, "utf-8");
// Push
const pushResult = await backend.runCLICommand(["sync", "push", "--yes"], tempDir);
assertEquals(pushResult.code, 0, `Push should succeed: ${pushResult.stderr}`);
expect(pushResult.code).toEqual(0);
// Verify modification on server
const updatedScript = await getScript(backend, scriptPath);
assert(
updatedScript.content.includes("modified content from test"),
`Server should have modified content. Got: ${updatedScript.content}`
);
expect(
updatedScript.content.includes("modified content from test")
).toBeTruthy();
// Verify no diff on subsequent pull (idempotency)
await verifyNoDiffOnPull(backend, tempDir);
});
},
});
Deno.test({
name: "Mixed Case Paths: pull and push flow with capitalized folder",
sanitizeResources: false,
sanitizeOps: false,
fn: async () => {
test("Mixed Case Paths: pull and push flow with capitalized folder", async () => {
await withTestBackend(async (backend, tempDir) => {
await setupWorkspaceProfile(backend);
@@ -349,59 +336,51 @@ Deno.test({
await createFlow(backend, flowPath, originalContent, "Data Processor Flow");
// Create wmill.yaml
await Deno.writeTextFile(
await writeFile(
path.join(tempDir, "wmill.yaml"),
`defaultTs: bun
includes:
- "**"
excludes: []
`
`,
"utf-8"
);
// Pull
const pullResult = await backend.runCLICommand(["sync", "pull", "--yes"], tempDir);
assertEquals(pullResult.code, 0, `Pull should succeed: ${pullResult.stderr}`);
expect(pullResult.code).toEqual(0);
// Verify flow directory exists
const flowDir = path.join(tempDir, "f", "MyFlows", "DataProcessor.flow");
const flowDirExists = await Deno.stat(flowDir).then(s => s.isDirectory).catch(() => false);
assert(flowDirExists, `Flow directory should exist at ${flowDir}`);
const flowDirExists = await stat(flowDir).then(s => s.isDirectory()).catch(() => false);
expect(flowDirExists).toBeTruthy();
// Modify the flow metadata (summary) instead of inline script
const flowMetadataPath = path.join(flowDir, "flow.yaml");
const flowMetadataExists = await Deno.stat(flowMetadataPath).then(() => true).catch(() => false);
assert(flowMetadataExists, `Flow metadata should exist at ${flowMetadataPath}`);
const flowMetadataExists = await stat(flowMetadataPath).then(() => true).catch(() => false);
expect(flowMetadataExists).toBeTruthy();
const flowMetadata = await Deno.readTextFile(flowMetadataPath);
const flowMetadata = await readFile(flowMetadataPath, "utf-8");
const modifiedMetadata = flowMetadata.replace(
/summary:.*$/m,
'summary: "Modified Data Processor Flow from test"'
);
await Deno.writeTextFile(flowMetadataPath, modifiedMetadata);
await writeFile(flowMetadataPath, modifiedMetadata, "utf-8");
// Push
const pushResult = await backend.runCLICommand(["sync", "push", "--yes"], tempDir);
assertEquals(pushResult.code, 0, `Push should succeed: ${pushResult.stderr}`);
expect(pushResult.code).toEqual(0);
// Verify modification on server
const updatedFlow = await getFlow(backend, flowPath);
assertEquals(
updatedFlow.summary,
"Modified Data Processor Flow from test",
`Server should have modified flow summary. Got: ${updatedFlow.summary}`
);
expect(updatedFlow.summary).toEqual("Modified Data Processor Flow from test");
// Verify no diff on subsequent pull (idempotency)
await verifyNoDiffOnPull(backend, tempDir);
});
},
});
Deno.test({
name: "Mixed Case Paths: pull and push app with capitalized folder",
sanitizeResources: false,
sanitizeOps: false,
fn: async () => {
test("Mixed Case Paths: pull and push app with capitalized folder", async () => {
await withTestBackend(async (backend, tempDir) => {
await setupWorkspaceProfile(backend);
@@ -413,56 +392,48 @@ Deno.test({
await createApp(backend, appPath, "My Dashboard App");
// Create wmill.yaml
await Deno.writeTextFile(
await writeFile(
path.join(tempDir, "wmill.yaml"),
`defaultTs: bun
includes:
- "**"
excludes: []
`
`,
"utf-8"
);
// Pull
const pullResult = await backend.runCLICommand(["sync", "pull", "--yes"], tempDir);
assertEquals(pullResult.code, 0, `Pull should succeed: ${pullResult.stderr}`);
expect(pullResult.code).toEqual(0);
// Verify app directory exists
const appDir = path.join(tempDir, "f", "MyApps", "Dashboard.app");
const appDirExists = await Deno.stat(appDir).then(s => s.isDirectory).catch(() => false);
assert(appDirExists, `App directory should exist at ${appDir}`);
const appDirExists = await stat(appDir).then(s => s.isDirectory()).catch(() => false);
expect(appDirExists).toBeTruthy();
// Modify the app metadata
const appMetadataPath = path.join(appDir, "app.yaml");
const appMetadata = await Deno.readTextFile(appMetadataPath);
const appMetadata = await readFile(appMetadataPath, "utf-8");
const modifiedMetadata = appMetadata.replace(
/summary:.*$/m,
'summary: "Modified Dashboard App from test"'
);
await Deno.writeTextFile(appMetadataPath, modifiedMetadata);
await writeFile(appMetadataPath, modifiedMetadata, "utf-8");
// Push
const pushResult = await backend.runCLICommand(["sync", "push", "--yes"], tempDir);
assertEquals(pushResult.code, 0, `Push should succeed: ${pushResult.stderr}`);
expect(pushResult.code).toEqual(0);
// Verify modification on server
const updatedApp = await getApp(backend, appPath);
assertEquals(
updatedApp.summary,
"Modified Dashboard App from test",
`Server should have modified app summary. Got: ${updatedApp.summary}`
);
expect(updatedApp.summary).toEqual("Modified Dashboard App from test");
// Verify no diff on subsequent pull (idempotency)
await verifyNoDiffOnPull(backend, tempDir);
});
},
});
Deno.test({
name: "Mixed Case Paths: pull and push variable with capitalized folder",
sanitizeResources: false,
sanitizeOps: false,
fn: async () => {
test("Mixed Case Paths: pull and push variable with capitalized folder", async () => {
await withTestBackend(async (backend, tempDir) => {
await setupWorkspaceProfile(backend);
@@ -474,55 +445,47 @@ Deno.test({
await createVariable(backend, varPath, "original-api-key-value", "API Key Variable");
// Create wmill.yaml
await Deno.writeTextFile(
await writeFile(
path.join(tempDir, "wmill.yaml"),
`defaultTs: bun
includes:
- "**"
excludes: []
`
`,
"utf-8"
);
// Pull
const pullResult = await backend.runCLICommand(["sync", "pull", "--yes"], tempDir);
assertEquals(pullResult.code, 0, `Pull should succeed: ${pullResult.stderr}`);
expect(pullResult.code).toEqual(0);
// Verify variable file exists
const varFilePath = path.join(tempDir, "f", "MyVars", "ApiKey.variable.yaml");
const varExists = await Deno.stat(varFilePath).then(() => true).catch(() => false);
assert(varExists, `Variable file should exist at ${varFilePath}`);
const varExists = await stat(varFilePath).then(() => true).catch(() => false);
expect(varExists).toBeTruthy();
// Modify the variable
const varContent = await Deno.readTextFile(varFilePath);
const varContent = await readFile(varFilePath, "utf-8");
const modifiedVarContent = varContent.replace(
/value:.*$/m,
'value: "modified-api-key-from-test"'
);
await Deno.writeTextFile(varFilePath, modifiedVarContent);
await writeFile(varFilePath, modifiedVarContent, "utf-8");
// Push
const pushResult = await backend.runCLICommand(["sync", "push", "--yes"], tempDir);
assertEquals(pushResult.code, 0, `Push should succeed: ${pushResult.stderr}`);
expect(pushResult.code).toEqual(0);
// Verify modification on server
const updatedVar = await getVariable(backend, varPath);
assertEquals(
updatedVar.value,
"modified-api-key-from-test",
`Server should have modified variable value. Got: ${updatedVar.value}`
);
expect(updatedVar.value).toEqual("modified-api-key-from-test");
// Verify no diff on subsequent pull (idempotency)
await verifyNoDiffOnPull(backend, tempDir);
});
},
});
Deno.test({
name: "Mixed Case Paths: deeply nested capitalized folders",
sanitizeResources: false,
sanitizeOps: false,
fn: async () => {
test("Mixed Case Paths: deeply nested capitalized folders", async () => {
await withTestBackend(async (backend, tempDir) => {
await setupWorkspaceProfile(backend);
@@ -539,52 +502,47 @@ Deno.test({
await createScript(backend, scriptPath, originalContent, "Nested Script");
// Create wmill.yaml
await Deno.writeTextFile(
await writeFile(
path.join(tempDir, "wmill.yaml"),
`defaultTs: bun
includes:
- "**"
excludes: []
`
`,
"utf-8"
);
// Pull
const pullResult = await backend.runCLICommand(["sync", "pull", "--yes"], tempDir);
assertEquals(pullResult.code, 0, `Pull should succeed: ${pullResult.stderr}`);
expect(pullResult.code).toEqual(0);
// Verify file exists
const scriptFilePath = path.join(tempDir, "f", "MyProject", "SubFolder_A.ts");
const scriptExists = await Deno.stat(scriptFilePath).then(() => true).catch(() => false);
assert(scriptExists, `Nested script should exist at ${scriptFilePath}`);
const scriptExists = await stat(scriptFilePath).then(() => true).catch(() => false);
expect(scriptExists).toBeTruthy();
// Modify
const modifiedContent = `export async function main() {
return "deeply nested modified from test";
}`;
await Deno.writeTextFile(scriptFilePath, modifiedContent);
await writeFile(scriptFilePath, modifiedContent, "utf-8");
// Push
const pushResult = await backend.runCLICommand(["sync", "push", "--yes"], tempDir);
assertEquals(pushResult.code, 0, `Push should succeed: ${pushResult.stderr}`);
expect(pushResult.code).toEqual(0);
// Verify on server
const updatedScript = await getScript(backend, scriptPath);
assert(
updatedScript.content.includes("deeply nested modified from test"),
`Server should have modified nested content`
);
expect(
updatedScript.content.includes("deeply nested modified from test")
).toBeTruthy();
// Verify no diff on subsequent pull (idempotency)
await verifyNoDiffOnPull(backend, tempDir);
});
},
});
Deno.test({
name: "Mixed Case Paths: multiple resources in same capitalized folder",
sanitizeResources: false,
sanitizeOps: false,
fn: async () => {
test("Mixed Case Paths: multiple resources in same capitalized folder", async () => {
await withTestBackend(async (backend, tempDir) => {
await setupWorkspaceProfile(backend);
@@ -608,65 +566,63 @@ Deno.test({
await createResource(backend, "f/SharedFolder/ResourceOne", "any", { key: "original" });
// Create wmill.yaml
await Deno.writeTextFile(
await writeFile(
path.join(tempDir, "wmill.yaml"),
`defaultTs: bun
includes:
- "**"
excludes: []
`
`,
"utf-8"
);
// Pull
const pullResult = await backend.runCLICommand(["sync", "pull", "--yes"], tempDir);
assertEquals(pullResult.code, 0, `Pull should succeed: ${pullResult.stderr}`);
expect(pullResult.code).toEqual(0);
// Verify all files exist
const folderPath = path.join(tempDir, "f", "SharedFolder");
const script1Exists = await Deno.stat(path.join(folderPath, "ScriptOne.ts")).then(() => true).catch(() => false);
const script2Exists = await Deno.stat(path.join(folderPath, "ScriptTwo.ts")).then(() => true).catch(() => false);
const var1Exists = await Deno.stat(path.join(folderPath, "VarOne.variable.yaml")).then(() => true).catch(() => false);
const res1Exists = await Deno.stat(path.join(folderPath, "ResourceOne.resource.yaml")).then(() => true).catch(() => false);
const script1Exists = await stat(path.join(folderPath, "ScriptOne.ts")).then(() => true).catch(() => false);
const script2Exists = await stat(path.join(folderPath, "ScriptTwo.ts")).then(() => true).catch(() => false);
const var1Exists = await stat(path.join(folderPath, "VarOne.variable.yaml")).then(() => true).catch(() => false);
const res1Exists = await stat(path.join(folderPath, "ResourceOne.resource.yaml")).then(() => true).catch(() => false);
assert(script1Exists, "ScriptOne should exist");
assert(script2Exists, "ScriptTwo should exist");
assert(var1Exists, "VarOne should exist");
assert(res1Exists, "ResourceOne should exist");
expect(script1Exists).toBeTruthy();
expect(script2Exists).toBeTruthy();
expect(var1Exists).toBeTruthy();
expect(res1Exists).toBeTruthy();
// Modify script one
await Deno.writeTextFile(
await writeFile(
path.join(folderPath, "ScriptOne.ts"),
'export async function main() { return "script one MODIFIED"; }'
'export async function main() { return "script one MODIFIED"; }',
"utf-8"
);
// Modify script two
await Deno.writeTextFile(
await writeFile(
path.join(folderPath, "ScriptTwo.ts"),
'export async function main() { return "script two MODIFIED"; }'
'export async function main() { return "script two MODIFIED"; }',
"utf-8"
);
// Push
const pushResult = await backend.runCLICommand(["sync", "push", "--yes"], tempDir);
assertEquals(pushResult.code, 0, `Push should succeed: ${pushResult.stderr}`);
expect(pushResult.code).toEqual(0);
// Verify modifications on server
const script1 = await getScript(backend, "f/SharedFolder/ScriptOne");
const script2 = await getScript(backend, "f/SharedFolder/ScriptTwo");
assert(script1.content.includes("script one MODIFIED"), "Script one should be modified on server");
assert(script2.content.includes("script two MODIFIED"), "Script two should be modified on server");
expect(script1.content.includes("script one MODIFIED")).toBeTruthy();
expect(script2.content.includes("script two MODIFIED")).toBeTruthy();
// Verify no diff on subsequent pull (idempotency)
await verifyNoDiffOnPull(backend, tempDir);
});
},
});
Deno.test({
name: "Mixed Case Paths: CamelCase folder names with numbers",
sanitizeResources: false,
sanitizeOps: false,
fn: async () => {
test("Mixed Case Paths: CamelCase folder names with numbers", async () => {
await withTestBackend(async (backend, tempDir) => {
await setupWorkspaceProfile(backend);
@@ -683,40 +639,41 @@ Deno.test({
);
// Create wmill.yaml
await Deno.writeTextFile(
await writeFile(
path.join(tempDir, "wmill.yaml"),
`defaultTs: bun
includes:
- "**"
excludes: []
`
`,
"utf-8"
);
// Pull
const pullResult = await backend.runCLICommand(["sync", "pull", "--yes"], tempDir);
assertEquals(pullResult.code, 0, `Pull should succeed: ${pullResult.stderr}`);
expect(pullResult.code).toEqual(0);
// Verify file exists
const scriptFilePath = path.join(tempDir, "f", "Project2024", "DataHandler_V2.ts");
const scriptExists = await Deno.stat(scriptFilePath).then(() => true).catch(() => false);
assert(scriptExists, `Script should exist at ${scriptFilePath}`);
const scriptExists = await stat(scriptFilePath).then(() => true).catch(() => false);
expect(scriptExists).toBeTruthy();
// Modify
await Deno.writeTextFile(
await writeFile(
scriptFilePath,
'export async function main() { return "handler v2 MODIFIED"; }'
'export async function main() { return "handler v2 MODIFIED"; }',
"utf-8"
);
// Push
const pushResult = await backend.runCLICommand(["sync", "push", "--yes"], tempDir);
assertEquals(pushResult.code, 0, `Push should succeed: ${pushResult.stderr}`);
expect(pushResult.code).toEqual(0);
// Verify on server
const updatedScript = await getScript(backend, scriptPath);
assert(updatedScript.content.includes("handler v2 MODIFIED"), "Server should have modified content");
expect(updatedScript.content.includes("handler v2 MODIFIED")).toBeTruthy();
// Verify no diff on subsequent pull (idempotency)
await verifyNoDiffOnPull(backend, tempDir);
});
},
});
+39 -58
View File
@@ -1,4 +1,5 @@
import { assertEquals, assert, assertStringIncludes } from "https://deno.land/std@0.224.0/assert/mod.ts";
import { expect, test } from "bun:test";
import { writeFile } from "node:fs/promises";
import { withTestBackend } from "./test_backend.ts";
import { addWorkspace } from "../workspace.ts";
import { parseJsonFromCLIOutput } from "./test_config_helpers.ts";
@@ -20,16 +21,12 @@ async function setupWorkspaceProfile(backend: any, workspaceName: string): Promi
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
}
Deno.test({
name: "Multi-Branch: sync pull with branch-specific overrides",
sanitizeResources: false,
sanitizeOps: false,
fn: async () => {
test("Multi-Branch: sync pull with branch-specific overrides", async () => {
await withTestBackend(async (backend, tempDir) => {
await setupWorkspaceProfile(backend, "multi_branch_test");
// Create wmill.yaml with gitBranches configuration
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
includes:
- "**"
excludes: []
@@ -46,7 +43,7 @@ gitBranches:
prod:
overrides:
skipVariables: true
skipResources: true`);
skipResources: true`, "utf-8");
// Test main branch - should include variables and resources
const mainResult = await backend.runCLICommand([
@@ -56,7 +53,7 @@ gitBranches:
'--json-output'
], tempDir, "multi_branch_test");
assertEquals(mainResult.code, 0, `Main branch sync should succeed: ${mainResult.stderr}`);
expect(mainResult.code).toEqual(0);
const mainData = parseJsonFromCLIOutput(mainResult.stdout);
const mainPaths = (mainData.changes || []).map((c: any) => c.path);
@@ -64,8 +61,8 @@ gitBranches:
const mainHasVariables = mainPaths.some((path: string) => path.includes('.variable.yaml'));
const mainHasResources = mainPaths.some((path: string) => path.includes('.resource.yaml'));
assertEquals(mainHasVariables, true, "Main branch should include variables");
assertEquals(mainHasResources, true, "Main branch should include resources");
expect(mainHasVariables).toEqual(true);
expect(mainHasResources).toEqual(true);
// Test staging branch - should skip variables but include resources
const stagingResult = await backend.runCLICommand([
@@ -75,7 +72,7 @@ gitBranches:
'--json-output'
], tempDir, "multi_branch_test");
assertEquals(stagingResult.code, 0, `Staging branch sync should succeed: ${stagingResult.stderr}`);
expect(stagingResult.code).toEqual(0);
const stagingData = parseJsonFromCLIOutput(stagingResult.stdout);
const stagingPaths = (stagingData.changes || []).map((c: any) => c.path);
@@ -83,8 +80,8 @@ gitBranches:
const stagingHasVariables = stagingPaths.some((path: string) => path.includes('.variable.yaml'));
const stagingHasResources = stagingPaths.some((path: string) => path.includes('.resource.yaml'));
assertEquals(stagingHasVariables, false, "Staging branch should skip variables");
assertEquals(stagingHasResources, true, "Staging branch should include resources");
expect(stagingHasVariables).toEqual(false);
expect(stagingHasResources).toEqual(true);
// Test prod branch - should skip both variables and resources
const prodResult = await backend.runCLICommand([
@@ -94,7 +91,7 @@ gitBranches:
'--json-output'
], tempDir, "multi_branch_test");
assertEquals(prodResult.code, 0, `Prod branch sync should succeed: ${prodResult.stderr}`);
expect(prodResult.code).toEqual(0);
const prodData = parseJsonFromCLIOutput(prodResult.stdout);
const prodPaths = (prodData.changes || []).map((c: any) => c.path);
@@ -102,21 +99,16 @@ gitBranches:
const prodHasVariables = prodPaths.some((path: string) => path.includes('.variable.yaml'));
const prodHasResources = prodPaths.some((path: string) => path.includes('.resource.yaml'));
assertEquals(prodHasVariables, false, "Prod branch should skip variables");
assertEquals(prodHasResources, false, "Prod branch should skip resources");
expect(prodHasVariables).toEqual(false);
expect(prodHasResources).toEqual(false);
});
}
});
Deno.test({
name: "Multi-Branch: branch override with includes filtering",
sanitizeResources: false,
sanitizeOps: false,
fn: async () => {
test("Multi-Branch: branch override with includes filtering", async () => {
await withTestBackend(async (backend, tempDir) => {
await setupWorkspaceProfile(backend, "includes_branch_test");
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
includes:
- "**"
@@ -131,7 +123,7 @@ gitBranches:
includes:
- "f/**"
- "users/**"
skipVariables: false`);
skipVariables: false`, "utf-8");
// Test feature branch - should skip variables and only include f/**
const featureResult = await backend.runCLICommand([
@@ -141,7 +133,7 @@ gitBranches:
'--json-output'
], tempDir, "includes_branch_test");
assertEquals(featureResult.code, 0, `Feature branch sync should succeed: ${featureResult.stderr}`);
expect(featureResult.code).toEqual(0);
const featureData = parseJsonFromCLIOutput(featureResult.stdout);
const featurePaths = (featureData.changes || []).map((c: any) => c.path);
@@ -151,8 +143,8 @@ gitBranches:
const featureHasVariables = normalizedFeaturePaths.some((path: string) => path.includes('.variable.yaml'));
const featureHasUsers = normalizedFeaturePaths.some((path: string) => path.startsWith('users/'));
assertEquals(featureHasVariables, false, "Feature branch should skip variables");
assertEquals(featureHasUsers, false, "Feature branch should not include users (not in includes)");
expect(featureHasVariables).toEqual(false);
expect(featureHasUsers).toEqual(false);
// Test release branch - should include variables and users
const releaseResult = await backend.runCLICommand([
@@ -163,7 +155,7 @@ gitBranches:
'--json-output'
], tempDir, "includes_branch_test");
assertEquals(releaseResult.code, 0, `Release branch sync should succeed: ${releaseResult.stderr}`);
expect(releaseResult.code).toEqual(0);
const releaseData = parseJsonFromCLIOutput(releaseResult.stdout);
const releasePaths = (releaseData.changes || []).map((c: any) => c.path);
@@ -173,21 +165,16 @@ gitBranches:
const releaseHasVariables = normalizedReleasePaths.some((path: string) => path.includes('.variable.yaml'));
const releaseHasUsers = normalizedReleasePaths.some((path: string) => path.startsWith('users/'));
assertEquals(releaseHasVariables, true, "Release branch should include variables");
assertEquals(releaseHasUsers, true, "Release branch should include users");
expect(releaseHasVariables).toEqual(true);
expect(releaseHasUsers).toEqual(true);
});
}
});
Deno.test({
name: "Multi-Branch: fallback to base config when branch not defined",
sanitizeResources: false,
sanitizeOps: false,
fn: async () => {
test("Multi-Branch: fallback to base config when branch not defined", async () => {
await withTestBackend(async (backend, tempDir) => {
await setupWorkspaceProfile(backend, "fallback_test");
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
includes:
- "**"
skipVariables: true
@@ -197,7 +184,7 @@ gitBranches:
main:
overrides:
skipVariables: false
skipResources: false`);
skipResources: false`, "utf-8");
// Test undefined branch - should use base config (skip variables and resources)
const undefinedResult = await backend.runCLICommand([
@@ -207,7 +194,7 @@ gitBranches:
'--json-output'
], tempDir, "fallback_test");
assertEquals(undefinedResult.code, 0, `Undefined branch sync should succeed: ${undefinedResult.stderr}`);
expect(undefinedResult.code).toEqual(0);
const undefinedData = parseJsonFromCLIOutput(undefinedResult.stdout);
const undefinedPaths = (undefinedData.changes || []).map((c: any) => c.path);
@@ -216,8 +203,8 @@ gitBranches:
const undefinedHasResources = undefinedPaths.some((path: string) => path.includes('.resource.yaml'));
// Should use base config since branch is not defined
assertEquals(undefinedHasVariables, false, "Undefined branch should use base config skipVariables: true");
assertEquals(undefinedHasResources, false, "Undefined branch should use base config skipResources: true");
expect(undefinedHasVariables).toEqual(false);
expect(undefinedHasResources).toEqual(false);
// Test defined main branch - should use branch overrides
const mainResult = await backend.runCLICommand([
@@ -227,7 +214,7 @@ gitBranches:
'--json-output'
], tempDir, "fallback_test");
assertEquals(mainResult.code, 0, `Main branch sync should succeed: ${mainResult.stderr}`);
expect(mainResult.code).toEqual(0);
const mainData = parseJsonFromCLIOutput(mainResult.stdout);
const mainPaths = (mainData.changes || []).map((c: any) => c.path);
@@ -235,21 +222,16 @@ gitBranches:
const mainHasVariables = mainPaths.some((path: string) => path.includes('.variable.yaml'));
const mainHasResources = mainPaths.some((path: string) => path.includes('.resource.yaml'));
assertEquals(mainHasVariables, true, "Main branch should use override skipVariables: false");
assertEquals(mainHasResources, true, "Main branch should use override skipResources: false");
expect(mainHasVariables).toEqual(true);
expect(mainHasResources).toEqual(true);
});
}
});
Deno.test({
name: "Multi-Branch: branch inherits unspecified settings from base",
sanitizeResources: false,
sanitizeOps: false,
fn: async () => {
test("Multi-Branch: branch inherits unspecified settings from base", async () => {
await withTestBackend(async (backend, tempDir) => {
await setupWorkspaceProfile(backend, "inherit_test");
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
includes:
- "**"
skipVariables: true
@@ -259,7 +241,7 @@ skipApps: true
gitBranches:
partial:
overrides:
skipVariables: false`);
skipVariables: false`, "utf-8");
// Test partial branch - should inherit skipResources and skipApps from base
const result = await backend.runCLICommand([
@@ -269,7 +251,7 @@ gitBranches:
'--json-output'
], tempDir, "inherit_test");
assertEquals(result.code, 0, `Partial branch sync should succeed: ${result.stderr}`);
expect(result.code).toEqual(0);
const data = parseJsonFromCLIOutput(result.stdout);
const paths = (data.changes || []).map((c: any) => c.path);
@@ -279,10 +261,9 @@ gitBranches:
const hasApps = paths.some((path: string) => path.includes('.app/') || path.endsWith('.app.yaml'));
// skipVariables is overridden to false
assertEquals(hasVariables, true, "Partial branch should include variables (override)");
expect(hasVariables).toEqual(true);
// skipResources and skipApps are inherited from base (true)
assertEquals(hasResources, false, "Partial branch should skip resources (inherited)");
assertEquals(hasApps, false, "Partial branch should skip apps (inherited)");
expect(hasResources).toEqual(false);
expect(hasApps).toEqual(false);
});
}
});
+34 -58
View File
@@ -1,4 +1,5 @@
import { assertEquals, assert } from "https://deno.land/std@0.224.0/assert/mod.ts";
import { expect, test } from "bun:test";
import { writeFile } from "node:fs/promises";
import { getEffectiveSettings } from "../src/core/conf.ts";
import { withTestBackend } from "./test_backend.ts";
import { addWorkspace } from "../workspace.ts";
@@ -9,11 +10,7 @@ import { parseJsonFromCLIOutput } from "./test_config_helpers.ts";
// Tests for gitBranches override inheritance and file filtering behavior
// =============================================================================
Deno.test({
name: "Override Settings: branch override inherits non-overridden settings from base config",
sanitizeResources: false,
sanitizeOps: false,
fn: async () => {
test("Override Settings: branch override inherits non-overridden settings from base config", async () => {
const config = {
includes: ["default/**"],
skipVariables: true, // Base has this as true
@@ -39,21 +36,16 @@ Deno.test({
);
// Override values should be used
assertEquals(effective.includes, ["override/**"], "Must use override includes");
assertEquals(effective.skipApps, true, "Must use override skipApps");
expect(effective.includes).toEqual(["override/**"]);
expect(effective.skipApps).toEqual(true);
// Should inherit skip flags from base config
assertEquals(effective.skipVariables, true, "Must inherit skipVariables=true from base config");
assertEquals(effective.skipResources, true, "Must inherit skipResources=true from base config");
assertEquals(effective.defaultTs, "bun", "Must inherit defaultTs from base config");
}
expect(effective.skipVariables).toEqual(true);
expect(effective.skipResources).toEqual(true);
expect(effective.defaultTs).toEqual("bun");
});
Deno.test({
name: "Override Settings: branch-specific settings take precedence",
sanitizeResources: false,
sanitizeOps: false,
fn: async () => {
test("Override Settings: branch-specific settings take precedence", async () => {
const config = {
includes: ["default/**"],
skipVariables: false,
@@ -81,8 +73,8 @@ Deno.test({
true,
"main"
);
assertEquals(mainEffective.includes, ["main/**"], "Main branch must use its own includes");
assertEquals(mainEffective.skipVariables, true, "Main branch must use its own skipVariables");
expect(mainEffective.includes).toEqual(["main/**"]);
expect(mainEffective.skipVariables).toEqual(true);
// Test dev branch
const devEffective = await getEffectiveSettings(
@@ -92,20 +84,15 @@ Deno.test({
true,
"dev"
);
assertEquals(devEffective.includes, ["dev/**"], "Dev branch must use its own includes");
assertEquals(devEffective.skipVariables, false, "Dev branch must use its own skipVariables");
}
expect(devEffective.includes).toEqual(["dev/**"]);
expect(devEffective.skipVariables).toEqual(false);
});
// =============================================================================
// INTEGRATION TESTS - File Filtering Behavior with gitBranches
// =============================================================================
Deno.test({
name: "Integration: sync pull with skipVariables branch override excludes variable files",
sanitizeResources: false,
sanitizeOps: false,
fn: async () => {
test("Integration: sync pull with skipVariables branch override excludes variable files", async () => {
await withTestBackend(async (backend, tempDir) => {
// Set up workspace
const testWorkspace = {
@@ -117,7 +104,7 @@ Deno.test({
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
// Create wmill.yaml with gitBranches override that skips variables
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
includes:
- "**"
skipVariables: false
@@ -125,7 +112,7 @@ skipVariables: false
gitBranches:
test_branch:
overrides:
skipVariables: true`);
skipVariables: true`, "utf-8");
// Run sync pull with --branch to force using test_branch config
const result = await backend.runCLICommand([
@@ -135,29 +122,24 @@ gitBranches:
'--json-output'
], tempDir);
assertEquals(result.code, 0, `Sync pull should succeed: ${result.stderr}`);
expect(result.code).toEqual(0);
// Parse output and verify variable files are NOT included
const output = parseJsonFromCLIOutput(result.stdout);
const changePaths = (output.changes || []).map((c: any) => c.path);
const hasVariableFile = changePaths.some((path: string) => path.includes('.variable.yaml'));
assertEquals(hasVariableFile, false, "Variable files should NOT be included due to skipVariables override");
expect(hasVariableFile).toEqual(false);
// Verify other files ARE included
const hasOtherFiles = changePaths.some((path: string) =>
!path.includes('.variable.yaml') && !path.includes('wmill.yaml')
);
assert(hasOtherFiles, `Other files should be included. Found paths: ${changePaths.join(', ')}`);
expect(hasOtherFiles).toBeTruthy();
});
}
});
Deno.test({
name: "Integration: sync pull respects includes branch override for file filtering",
sanitizeResources: false,
sanitizeOps: false,
fn: async () => {
test("Integration: sync pull respects includes branch override for file filtering", async () => {
await withTestBackend(async (backend, tempDir) => {
// Set up workspace
const testWorkspace = {
@@ -169,7 +151,7 @@ Deno.test({
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
// Create wmill.yaml with gitBranches override for includes
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
includes:
- "**"
@@ -178,7 +160,7 @@ gitBranches:
overrides:
includes:
- "users/**"
- "groups/**"`);
- "groups/**"`, "utf-8");
// Run sync pull with --branch to use restricted includes
const result = await backend.runCLICommand([
@@ -190,7 +172,7 @@ gitBranches:
'--json-output'
], tempDir);
assertEquals(result.code, 0, `Sync pull should succeed: ${result.stderr}`);
expect(result.code).toEqual(0);
// Parse output
const output = parseJsonFromCLIOutput(result.stdout);
@@ -202,20 +184,15 @@ gitBranches:
const hasUserFiles = normalizedPaths.some((path: string) => path.includes('users/'));
const hasGroupFiles = normalizedPaths.some((path: string) => path.includes('groups/'));
assert(hasUserFiles || hasGroupFiles, `User or group files should be included. Found: ${normalizedPaths.join(', ')}`);
expect(hasUserFiles || hasGroupFiles).toBeTruthy();
// Verify f/** files are NOT included (due to restrictive includes)
const hasFolderFiles = normalizedPaths.some((path: string) => path.startsWith('f/'));
assertEquals(hasFolderFiles, false, `f/ files should NOT be included due to restrictive includes. Found: ${normalizedPaths.join(', ')}`);
expect(hasFolderFiles).toEqual(false);
});
}
});
Deno.test({
name: "Integration: different branches have different settings",
sanitizeResources: false,
sanitizeOps: false,
fn: async () => {
test("Integration: different branches have different settings", async () => {
await withTestBackend(async (backend, tempDir) => {
// Set up workspace
const testWorkspace = {
@@ -227,7 +204,7 @@ Deno.test({
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
// Create wmill.yaml with different settings per branch
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
includes:
- "**"
skipVariables: false
@@ -241,7 +218,7 @@ gitBranches:
dev:
overrides:
skipVariables: false
skipResources: false`);
skipResources: false`, "utf-8");
// Test prod branch - should skip variables and resources
const prodResult = await backend.runCLICommand([
@@ -251,7 +228,7 @@ gitBranches:
'--json-output'
], tempDir);
assertEquals(prodResult.code, 0, `Prod sync pull should succeed: ${prodResult.stderr}`);
expect(prodResult.code).toEqual(0);
const prodOutput = parseJsonFromCLIOutput(prodResult.stdout);
const prodPaths = (prodOutput.changes || []).map((c: any) => c.path);
@@ -259,8 +236,8 @@ gitBranches:
const prodHasVariables = prodPaths.some((path: string) => path.includes('.variable.yaml'));
const prodHasResources = prodPaths.some((path: string) => path.includes('.resource.yaml'));
assertEquals(prodHasVariables, false, "Prod branch should skip variables");
assertEquals(prodHasResources, false, "Prod branch should skip resources");
expect(prodHasVariables).toEqual(false);
expect(prodHasResources).toEqual(false);
// Test dev branch - should include variables and resources
const devResult = await backend.runCLICommand([
@@ -270,7 +247,7 @@ gitBranches:
'--json-output'
], tempDir);
assertEquals(devResult.code, 0, `Dev sync pull should succeed: ${devResult.stderr}`);
expect(devResult.code).toEqual(0);
const devOutput = parseJsonFromCLIOutput(devResult.stdout);
const devPaths = (devOutput.changes || []).map((c: any) => c.path);
@@ -278,8 +255,7 @@ gitBranches:
const devHasVariables = devPaths.some((path: string) => path.includes('.variable.yaml'));
const devHasResources = devPaths.some((path: string) => path.includes('.resource.yaml'));
assertEquals(devHasVariables, true, "Dev branch should include variables");
assertEquals(devHasResources, true, "Dev branch should include resources");
expect(devHasVariables).toEqual(true);
expect(devHasResources).toEqual(true);
});
}
});
+186 -229
View File
@@ -1,5 +1,6 @@
import { assertEquals, assertStringIncludes } from "https://deno.land/std@0.224.0/assert/mod.ts";
import { withTestBackend, cleanupTestBackend } from "./test_backend.ts";
import { expect, test } from "bun:test";
import { mkdir, writeFile } from "node:fs/promises";
import { withTestBackend } from "./test_backend.ts";
// =============================================================================
// PREVIEW COMMAND INTEGRATION TESTS
@@ -53,7 +54,7 @@ async function createWmillConfig(
}
}
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, yamlContent);
await writeFile(`${tempDir}/wmill.yaml`, yamlContent, "utf-8");
}
// Helper to create a script file with metadata
@@ -67,8 +68,8 @@ async function createScript(
}
): Promise<void> {
const dir = `${tempDir}/${path.substring(0, path.lastIndexOf("/"))}`;
await Deno.mkdir(dir, { recursive: true });
await Deno.writeTextFile(`${tempDir}/${path}`, content);
await mkdir(dir, { recursive: true });
await writeFile(`${tempDir}/${path}`, content, "utf-8");
// Create metadata file
const metaPath = path.replace(/\.[^.]+$/, ".script.yaml");
@@ -84,7 +85,7 @@ schema:
default: "World"
required: []
`;
await Deno.writeTextFile(`${tempDir}/${metaPath}`, metaContent);
await writeFile(`${tempDir}/${metaPath}`, metaContent, "utf-8");
}
// Helper to create a flow directory with flow.yaml
@@ -97,7 +98,7 @@ async function createFlow(
}
): Promise<void> {
const dir = `${tempDir}/${flowPath}`;
await Deno.mkdir(dir, { recursive: true });
await mkdir(dir, { recursive: true });
const flowYaml = `summary: "${options.summary}"
description: "Test flow"
@@ -118,125 +119,109 @@ schema:
default: "World"
required: []
`;
await Deno.writeTextFile(`${dir}/flow.yaml`, flowYaml);
await writeFile(`${dir}/flow.yaml`, flowYaml, "utf-8");
}
// =============================================================================
// SCRIPT PREVIEW TESTS
// =============================================================================
Deno.test({
name: "script preview: regular script (non-codebase)",
async fn() {
await withTestBackend(async (backend, tempDir) => {
await createWmillConfig(tempDir, { defaultTs: "bun" });
await createScript(
tempDir,
"f/test/simple_script.ts",
`export function main(name: string = "World") {
test("script preview: regular script (non-codebase)", async () => {
await withTestBackend(async (backend, tempDir) => {
await createWmillConfig(tempDir, { defaultTs: "bun" });
await createScript(
tempDir,
"f/test/simple_script.ts",
`export function main(name: string = "World") {
return \`Hello, \${name}!\`;
}`
);
);
const result = await backend.runCLICommand(
["script", "preview", "f/test/simple_script.ts"],
tempDir
);
const result = await backend.runCLICommand(
["script", "preview", "f/test/simple_script.ts"],
tempDir
);
assertEquals(result.code, 0, `Preview failed: ${result.stderr}\n${result.stdout}`);
assertStringIncludes(result.stdout + result.stderr, "Hello, World!");
});
},
sanitizeResources: false,
sanitizeOps: false,
expect(result.code).toEqual(0);
expect(result.stdout + result.stderr).toContain("Hello, World!");
});
});
Deno.test({
name: "script preview: codebase script (CJS)",
async fn() {
await withTestBackend(async (backend, tempDir) => {
await createWmillConfig(tempDir, {
defaultTs: "bun",
codebases: [{ relative_path: "f/codebase", includes: ["**"] }],
});
test("script preview: codebase script (CJS)", async () => {
await withTestBackend(async (backend, tempDir) => {
await createWmillConfig(tempDir, {
defaultTs: "bun",
codebases: [{ relative_path: "f/codebase", includes: ["**"] }],
});
await createScript(
tempDir,
"f/codebase/cjs_script.ts",
`export function main(name: string = "World") {
await createScript(
tempDir,
"f/codebase/cjs_script.ts",
`export function main(name: string = "World") {
console.log("CJS codebase script running");
return \`Hello from CJS codebase, \${name}!\`;
}`
);
);
const result = await backend.runCLICommand(
["script", "preview", "f/codebase/cjs_script.ts"],
tempDir
);
const result = await backend.runCLICommand(
["script", "preview", "f/codebase/cjs_script.ts"],
tempDir
);
assertEquals(result.code, 0, `Preview failed: ${result.stderr}\n${result.stdout}`);
assertStringIncludes(result.stdout + result.stderr, "Hello from CJS codebase, World!");
});
},
sanitizeResources: false,
sanitizeOps: false,
expect(result.code).toEqual(0);
expect(result.stdout + result.stderr).toContain("Hello from CJS codebase, World!");
});
});
Deno.test({
name: "script preview: codebase script (ESM)",
async fn() {
await withTestBackend(async (backend, tempDir) => {
await createWmillConfig(tempDir, {
defaultTs: "bun",
codebases: [{ relative_path: "f/codebase_esm", includes: ["**"], format: "esm" }],
});
test("script preview: codebase script (ESM)", async () => {
await withTestBackend(async (backend, tempDir) => {
await createWmillConfig(tempDir, {
defaultTs: "bun",
codebases: [{ relative_path: "f/codebase_esm", includes: ["**"], format: "esm" }],
});
await createScript(
tempDir,
"f/codebase_esm/esm_script.ts",
`export function main(name: string = "World") {
await createScript(
tempDir,
"f/codebase_esm/esm_script.ts",
`export function main(name: string = "World") {
console.log("ESM codebase script running");
return \`Hello from ESM codebase, \${name}!\`;
}`
);
);
const result = await backend.runCLICommand(
["script", "preview", "f/codebase_esm/esm_script.ts"],
tempDir
);
const result = await backend.runCLICommand(
["script", "preview", "f/codebase_esm/esm_script.ts"],
tempDir
);
assertEquals(result.code, 0, `Preview failed: ${result.stderr}\n${result.stdout}`);
assertStringIncludes(result.stdout + result.stderr, "Hello from ESM codebase, World!");
});
},
sanitizeResources: false,
sanitizeOps: false,
expect(result.code).toEqual(0);
expect(result.stdout + result.stderr).toContain("Hello from ESM codebase, World!");
});
});
Deno.test({
name: "script preview: codebase script with assets (tar)",
async fn() {
await withTestBackend(async (backend, tempDir) => {
await createWmillConfig(tempDir, {
defaultTs: "bun",
codebases: [{
relative_path: "f/codebase_tar",
includes: ["**"],
assets: [{ from: "f/codebase_tar/data.json", to: "data.json" }],
}],
});
test("script preview: codebase script with assets (tar)", async () => {
await withTestBackend(async (backend, tempDir) => {
await createWmillConfig(tempDir, {
defaultTs: "bun",
codebases: [{
relative_path: "f/codebase_tar",
includes: ["**"],
assets: [{ from: "f/codebase_tar/data.json", to: "data.json" }],
}],
});
// Create asset file
await Deno.mkdir(`${tempDir}/f/codebase_tar`, { recursive: true });
await Deno.writeTextFile(
`${tempDir}/f/codebase_tar/data.json`,
JSON.stringify({ message: "Hello from asset!" })
);
// Create asset file
await mkdir(`${tempDir}/f/codebase_tar`, { recursive: true });
await writeFile(
`${tempDir}/f/codebase_tar/data.json`,
JSON.stringify({ message: "Hello from asset!" }),
"utf-8"
);
await createScript(
tempDir,
"f/codebase_tar/tar_script.ts",
`import * as fs from "fs";
await createScript(
tempDir,
"f/codebase_tar/tar_script.ts",
`import * as fs from "fs";
export function main(name: string = "World") {
console.log("Tar codebase script running");
@@ -244,46 +229,42 @@ export function main(name: string = "World") {
const parsed = JSON.parse(data);
return \`Hello \${name}! Asset says: \${parsed.message}\`;
}`
);
);
const result = await backend.runCLICommand(
["script", "preview", "f/codebase_tar/tar_script.ts"],
tempDir
);
const result = await backend.runCLICommand(
["script", "preview", "f/codebase_tar/tar_script.ts"],
tempDir
);
assertEquals(result.code, 0, `Preview failed: ${result.stderr}\n${result.stdout}`);
assertStringIncludes(result.stdout + result.stderr, "Hello World! Asset says: Hello from asset!");
});
},
sanitizeResources: false,
sanitizeOps: false,
expect(result.code).toEqual(0);
expect(result.stdout + result.stderr).toContain("Hello World! Asset says: Hello from asset!");
});
});
Deno.test({
name: "script preview: codebase script ESM + tar (assets)",
async fn() {
await withTestBackend(async (backend, tempDir) => {
await createWmillConfig(tempDir, {
defaultTs: "bun",
codebases: [{
relative_path: "f/codebase_esm_tar",
includes: ["**"],
format: "esm",
assets: [{ from: "f/codebase_esm_tar/config.json", to: "config.json" }],
}],
});
test("script preview: codebase script ESM + tar (assets)", async () => {
await withTestBackend(async (backend, tempDir) => {
await createWmillConfig(tempDir, {
defaultTs: "bun",
codebases: [{
relative_path: "f/codebase_esm_tar",
includes: ["**"],
format: "esm",
assets: [{ from: "f/codebase_esm_tar/config.json", to: "config.json" }],
}],
});
// Create asset file
await Deno.mkdir(`${tempDir}/f/codebase_esm_tar`, { recursive: true });
await Deno.writeTextFile(
`${tempDir}/f/codebase_esm_tar/config.json`,
JSON.stringify({ setting: "esm_tar_value" })
);
// Create asset file
await mkdir(`${tempDir}/f/codebase_esm_tar`, { recursive: true });
await writeFile(
`${tempDir}/f/codebase_esm_tar/config.json`,
JSON.stringify({ setting: "esm_tar_value" }),
"utf-8"
);
await createScript(
tempDir,
"f/codebase_esm_tar/esm_tar_script.ts",
`import * as fs from "fs";
await createScript(
tempDir,
"f/codebase_esm_tar/esm_tar_script.ts",
`import * as fs from "fs";
export function main(name: string = "World") {
console.log("ESM + tar codebase script running");
@@ -291,68 +272,65 @@ export function main(name: string = "World") {
const parsed = JSON.parse(config);
return \`Hello \${name}! Config setting: \${parsed.setting}\`;
}`
);
);
const result = await backend.runCLICommand(
["script", "preview", "f/codebase_esm_tar/esm_tar_script.ts"],
tempDir
);
const result = await backend.runCLICommand(
["script", "preview", "f/codebase_esm_tar/esm_tar_script.ts"],
tempDir
);
assertEquals(result.code, 0, `Preview failed: ${result.stderr}\n${result.stdout}`);
assertStringIncludes(result.stdout + result.stderr, "Hello World! Config setting: esm_tar_value");
});
},
sanitizeResources: false,
sanitizeOps: false,
expect(result.code).toEqual(0);
expect(result.stdout + result.stderr).toContain("Hello World! Config setting: esm_tar_value");
});
});
Deno.test({
name: "script preview: codebase with imports (simulates ../shared layout)",
async fn() {
await withTestBackend(async (backend, tempDir) => {
// This test simulates a codebase that could be in a parent directory.
// The structure is:
// tempDir/
// wmill.yaml (codebase at ".")
// f/
// lib/
// helper.ts (shared module)
// main_script.ts (imports helper)
//
// This tests that codebase bundling correctly includes imported modules,
// which is the key functionality needed for ../shared codebases during sync.
// Note: Preview requires valid windmill paths (u/, g/, f/), so we run
// from within the codebase directory.
test("script preview: codebase with imports (simulates ../shared layout)", async () => {
await withTestBackend(async (backend, tempDir) => {
// This test simulates a codebase that could be in a parent directory.
// The structure is:
// tempDir/
// wmill.yaml (codebase at ".")
// f/
// lib/
// helper.ts (shared module)
// main_script.ts (imports helper)
//
// This tests that codebase bundling correctly includes imported modules,
// which is the key functionality needed for ../shared codebases during sync.
// Note: Preview requires valid windmill paths (u/, g/, f/), so we run
// from within the codebase directory.
await createWmillConfig(tempDir, {
defaultTs: "bun",
codebases: [{ relative_path: ".", includes: ["**"] }],
});
await createWmillConfig(tempDir, {
defaultTs: "bun",
codebases: [{ relative_path: ".", includes: ["**"] }],
});
// Create helper module
await Deno.mkdir(`${tempDir}/f/lib`, { recursive: true });
await Deno.writeTextFile(
`${tempDir}/f/lib/helper.ts`,
`export function greet(name: string): string {
// Create helper module
await mkdir(`${tempDir}/f/lib`, { recursive: true });
await writeFile(
`${tempDir}/f/lib/helper.ts`,
`export function greet(name: string): string {
return \`Hello from shared codebase, \${name}!\`;
}`
);
}`,
"utf-8"
);
// Create main script that imports the helper
await Deno.writeTextFile(
`${tempDir}/f/lib/main_script.ts`,
`import { greet } from "./helper";
// Create main script that imports the helper
await writeFile(
`${tempDir}/f/lib/main_script.ts`,
`import { greet } from "./helper";
export function main(name: string = "World") {
console.log("Running codebase script with imports");
return greet(name);
}`
);
}`,
"utf-8"
);
// Create script metadata
await Deno.writeTextFile(
`${tempDir}/f/lib/main_script.script.yaml`,
`summary: "Test script with imports"
// Create script metadata
await writeFile(
`${tempDir}/f/lib/main_script.script.yaml`,
`summary: "Test script with imports"
description: "Test script that imports from helper module"
lock: ""
schema:
@@ -363,64 +341,43 @@ schema:
type: string
default: "World"
required: []
`
);
`,
"utf-8"
);
// Run preview - the script should be bundled with the helper module
const result = await backend.runCLICommand(
["script", "preview", "f/lib/main_script.ts"],
tempDir
);
// Run preview - the script should be bundled with the helper module
const result = await backend.runCLICommand(
["script", "preview", "f/lib/main_script.ts"],
tempDir
);
assertEquals(result.code, 0, `Preview failed: ${result.stderr}\n${result.stdout}`);
// The script should be bundled (includes the helper) and run successfully
assertStringIncludes(
result.stdout + result.stderr,
"Hello from shared codebase, World!",
`Expected codebase script output not found. Got: ${result.stdout}\n${result.stderr}`
);
});
},
sanitizeResources: false,
sanitizeOps: false,
expect(result.code).toEqual(0);
// The script should be bundled (includes the helper) and run successfully
expect(
result.stdout + result.stderr,
).toContain("Hello from shared codebase, World!");
});
});
// =============================================================================
// FLOW PREVIEW TESTS
// =============================================================================
Deno.test({
name: "flow preview: simple flow",
async fn() {
await withTestBackend(async (backend, tempDir) => {
await createWmillConfig(tempDir, { defaultTs: "bun" });
await createFlow(tempDir, "f/test/simple_flow.flow", {
summary: "Test flow",
scriptContent: `export function main(name: string = "World") { return \`Flow says: Hello, \${name}!\`; }`,
});
const result = await backend.runCLICommand(
["flow", "preview", "f/test/simple_flow.flow"],
tempDir
);
assertEquals(result.code, 0, `Flow preview failed: ${result.stderr}\n${result.stdout}`);
assertStringIncludes(result.stdout + result.stderr, "Flow says: Hello, World!");
test("flow preview: simple flow", async () => {
await withTestBackend(async (backend, tempDir) => {
await createWmillConfig(tempDir, { defaultTs: "bun" });
await createFlow(tempDir, "f/test/simple_flow.flow", {
summary: "Test flow",
scriptContent: `export function main(name: string = "World") { return \`Flow says: Hello, \${name}!\`; }`,
});
},
sanitizeResources: false,
sanitizeOps: false,
const result = await backend.runCLICommand(
["flow", "preview", "f/test/simple_flow.flow"],
tempDir
);
expect(result.code).toEqual(0);
expect(result.stdout + result.stderr).toContain("Flow says: Hello, World!");
});
});
// =============================================================================
// CLEANUP
// =============================================================================
Deno.test({
name: "cleanup test backend",
async fn() {
await cleanupTestBackend();
},
sanitizeResources: false,
sanitizeOps: false,
});
+86 -108
View File
@@ -1,8 +1,8 @@
import { assertEquals, assert, assertStringIncludes } from "https://deno.land/std@0.224.0/assert/mod.ts";
import { expect, test } from "bun:test";
import { withTestBackend } from "./test_backend.ts";
import { addWorkspace } from "../workspace.ts";
import * as path from "https://deno.land/std@0.224.0/path/mod.ts";
import { ensureDir } from "https://deno.land/std@0.224.0/fs/mod.ts";
import * as path from "@std/path";
import { writeFile, readFile, stat, rm, mkdir } from "node:fs/promises";
// =============================================================================
// RAW APP SYNC TESTS
@@ -92,7 +92,7 @@ policy:
async function fileExists(filePath: string): Promise<boolean> {
try {
await Deno.stat(filePath);
await stat(filePath);
return true;
} catch {
return false;
@@ -100,7 +100,7 @@ async function fileExists(filePath: string): Promise<boolean> {
}
async function readFileContent(filePath: string): Promise<string> {
return await Deno.readTextFile(filePath);
return await readFile(filePath, "utf-8");
}
/**
@@ -108,35 +108,32 @@ async function readFileContent(filePath: string): Promise<string> {
* Uses .raw_app folder suffix with raw_app.yaml metadata
*/
async function createRawAppOnDisk(appDir: string): Promise<void> {
await ensureDir(appDir);
await ensureDir(path.join(appDir, "inline_scripts"));
await mkdir(appDir, { recursive: true });
await mkdir(path.join(appDir, "inline_scripts"), { recursive: true });
// Create raw_app.yaml metadata file
await Deno.writeTextFile(path.join(appDir, "raw_app.yaml"), RAW_APP_YAML);
await writeFile(path.join(appDir, "raw_app.yaml"), RAW_APP_YAML, "utf-8");
// Create app source files
await Deno.writeTextFile(path.join(appDir, "App.tsx"), APP_TSX);
await Deno.writeTextFile(path.join(appDir, "index.css"), INDEX_CSS);
await Deno.writeTextFile(path.join(appDir, "index.tsx"), INDEX_TSX);
await Deno.writeTextFile(path.join(appDir, "package.json"), PACKAGE_JSON);
await writeFile(path.join(appDir, "App.tsx"), APP_TSX, "utf-8");
await writeFile(path.join(appDir, "index.css"), INDEX_CSS, "utf-8");
await writeFile(path.join(appDir, "index.tsx"), INDEX_TSX, "utf-8");
await writeFile(path.join(appDir, "package.json"), PACKAGE_JSON, "utf-8");
// Create inline script in inline_scripts folder
await Deno.writeTextFile(
await writeFile(
path.join(appDir, "inline_scripts", "a.inline_script.ts"),
INLINE_SCRIPT_A
INLINE_SCRIPT_A,
"utf-8"
);
await Deno.writeTextFile(
await writeFile(
path.join(appDir, "inline_scripts", "a.inline_script.lock"),
INLINE_SCRIPT_A_LOCK
INLINE_SCRIPT_A_LOCK,
"utf-8"
);
}
Deno.test({
name: "Raw App: full sync workflow - push, pull, modify, push, clear, pull",
ignore: false,
sanitizeResources: false,
sanitizeOps: false,
fn: async () => {
test("Raw App: full sync workflow - push, pull, modify, push, clear, pull", async () => {
await withTestBackend(async (backend, tempDir) => {
// Set up workspace
const testWorkspace = {
@@ -148,14 +145,14 @@ Deno.test({
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
// Create wmill.yaml
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
includes:
- "**"
excludes: []`);
excludes: []`, "utf-8");
// Create folder structure
const appDir = path.join(tempDir, "f", "test", "my_raw_app.raw_app");
await ensureDir(path.join(tempDir, "f", "test"));
await mkdir(path.join(tempDir, "f", "test"), { recursive: true });
await createRawAppOnDisk(appDir);
// =========================================================================
@@ -166,23 +163,23 @@ excludes: []`);
'--yes'
], tempDir, "raw_app_test");
assertEquals(pushResult1.code, 0, `Initial sync push should succeed: ${pushResult1.stderr}`);
expect(pushResult1.code).toEqual(0);
// =========================================================================
// STEP 2: Clear disk and pull - verify raw app is pulled correctly
// =========================================================================
await Deno.remove(appDir, { recursive: true });
assert(!(await fileExists(appDir)), "App directory should be deleted before pull");
await rm(appDir, { recursive: true });
expect(!(await fileExists(appDir))).toBeTruthy();
const pullResult1 = await backend.runCLICommand([
'sync', 'pull',
'--yes'
], tempDir, "raw_app_test");
assertEquals(pullResult1.code, 0, `Sync pull should succeed: ${pullResult1.stderr}`);
expect(pullResult1.code).toEqual(0);
// Verify raw app directory structure was created
assert(await fileExists(appDir), `Raw app directory should exist at ${appDir}`);
expect(await fileExists(appDir)).toBeTruthy();
// Verify files were pulled
const appTsxPath = path.join(appDir, "App.tsx");
@@ -191,22 +188,22 @@ excludes: []`);
const packageJsonPath = path.join(appDir, "package.json");
const inlineScriptPath = path.join(appDir, "inline_scripts", "a.inline_script.ts");
assert(await fileExists(appTsxPath), "App.tsx should exist");
assert(await fileExists(indexCssPath), "index.css should exist");
assert(await fileExists(indexTsxPath), "index.tsx should exist");
assert(await fileExists(packageJsonPath), "package.json should exist");
assert(await fileExists(inlineScriptPath), "Inline script a.inline_script.ts should exist");
expect(await fileExists(appTsxPath)).toBeTruthy();
expect(await fileExists(indexCssPath)).toBeTruthy();
expect(await fileExists(indexTsxPath)).toBeTruthy();
expect(await fileExists(packageJsonPath)).toBeTruthy();
expect(await fileExists(inlineScriptPath)).toBeTruthy();
// Verify file contents
const appTsxContent = await readFileContent(appTsxPath);
assertStringIncludes(appTsxContent, "hello world", "App.tsx should contain 'hello world'");
assertStringIncludes(appTsxContent, "backend.a", "App.tsx should reference backend.a");
expect(appTsxContent).toContain("hello world");
expect(appTsxContent).toContain("backend.a");
const indexCssContent = await readFileContent(indexCssPath);
assertStringIncludes(indexCssContent, ".myclass", "index.css should contain .myclass");
expect(indexCssContent).toContain(".myclass");
const inlineScriptContent = await readFileContent(inlineScriptPath);
assertStringIncludes(inlineScriptContent, "export async function main", "Inline script should have main function");
expect(inlineScriptContent).toContain("export async function main");
// =========================================================================
// STEP 3: Modify files locally
@@ -214,15 +211,15 @@ excludes: []`);
// Modify App.tsx - change the heading
const modifiedAppTsx = appTsxContent.replace("hello world", "hello modified world");
await Deno.writeTextFile(appTsxPath, modifiedAppTsx);
await writeFile(appTsxPath, modifiedAppTsx, "utf-8");
// Modify index.css - change the border color
const modifiedIndexCss = indexCssContent.replace("gray", "blue");
await Deno.writeTextFile(indexCssPath, modifiedIndexCss);
await writeFile(indexCssPath, modifiedIndexCss, "utf-8");
// Modify inline script - change the return value
const modifiedInlineScript = inlineScriptContent.replace("return x", "return `modified: ${x}`");
await Deno.writeTextFile(inlineScriptPath, modifiedInlineScript);
await writeFile(inlineScriptPath, modifiedInlineScript, "utf-8");
// =========================================================================
// STEP 4: Push changes
@@ -232,13 +229,13 @@ excludes: []`);
'--yes'
], tempDir, "raw_app_test");
assertEquals(pushResult2.code, 0, `Sync push should succeed: ${pushResult2.stderr}`);
expect(pushResult2.code).toEqual(0);
// =========================================================================
// STEP 5: Clear disk (delete the app directory)
// =========================================================================
await Deno.remove(appDir, { recursive: true });
assert(!(await fileExists(appDir)), "App directory should be deleted");
await rm(appDir, { recursive: true });
expect(!(await fileExists(appDir))).toBeTruthy();
// =========================================================================
// STEP 6: Pull again and verify modifications persisted
@@ -248,37 +245,31 @@ excludes: []`);
'--yes'
], tempDir, "raw_app_test");
assertEquals(pullResult2.code, 0, `Second sync pull should succeed: ${pullResult2.stderr}`);
expect(pullResult2.code).toEqual(0);
// Verify app directory exists again
assert(await fileExists(appDir), "Raw app directory should exist after second pull");
expect(await fileExists(appDir)).toBeTruthy();
// Verify all files were pulled again
assert(await fileExists(appTsxPath), "App.tsx should exist after second pull");
assert(await fileExists(indexCssPath), "index.css should exist after second pull");
assert(await fileExists(indexTsxPath), "index.tsx should exist after second pull");
assert(await fileExists(packageJsonPath), "package.json should exist after second pull");
assert(await fileExists(inlineScriptPath), "Inline script should exist after second pull");
expect(await fileExists(appTsxPath)).toBeTruthy();
expect(await fileExists(indexCssPath)).toBeTruthy();
expect(await fileExists(indexTsxPath)).toBeTruthy();
expect(await fileExists(packageJsonPath)).toBeTruthy();
expect(await fileExists(inlineScriptPath)).toBeTruthy();
// Verify modifications were persisted
const pulledAppTsx = await readFileContent(appTsxPath);
assertStringIncludes(pulledAppTsx, "hello modified world", "Modifications to App.tsx should persist");
expect(pulledAppTsx).toContain("hello modified world");
const pulledIndexCss = await readFileContent(indexCssPath);
assertStringIncludes(pulledIndexCss, "blue", "Modifications to index.css should persist");
expect(pulledIndexCss).toContain("blue");
const pulledInlineScript = await readFileContent(inlineScriptPath);
assertStringIncludes(pulledInlineScript, "modified:", "Modifications to inline script should persist");
expect(pulledInlineScript).toContain("modified:");
});
}
});
Deno.test({
name: "Raw App: add new file and push",
ignore: false,
sanitizeResources: false,
sanitizeOps: false,
fn: async () => {
test("Raw App: add new file and push", async () => {
await withTestBackend(async (backend, tempDir) => {
// Set up workspace
const testWorkspace = {
@@ -290,14 +281,14 @@ Deno.test({
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
// Create wmill.yaml
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
includes:
- "**"
excludes: []`);
excludes: []`, "utf-8");
// Create initial raw app
const appDir = path.join(tempDir, "f", "test", "new_file_app.raw_app");
await ensureDir(path.join(tempDir, "f", "test"));
await mkdir(path.join(tempDir, "f", "test"), { recursive: true });
await createRawAppOnDisk(appDir);
// Initial push
@@ -306,14 +297,14 @@ excludes: []`);
'--yes'
], tempDir, "raw_app_new_file_test");
assertEquals(pushResult1.code, 0, `Initial sync push should succeed: ${pushResult1.stderr}`);
expect(pushResult1.code).toEqual(0);
// Add a new file
const newFilePath = path.join(appDir, "utils.ts");
await Deno.writeTextFile(newFilePath, `export function formatValue(val: string): string {
await writeFile(newFilePath, `export function formatValue(val: string): string {
return \`Formatted: \${val}\`;
}
`);
`, "utf-8");
// Push changes
const pushResult2 = await backend.runCLICommand([
@@ -321,32 +312,26 @@ excludes: []`);
'--yes'
], tempDir, "raw_app_new_file_test");
assertEquals(pushResult2.code, 0, `Sync push with new file should succeed: ${pushResult2.stderr}`);
expect(pushResult2.code).toEqual(0);
// Clear and pull again
await Deno.remove(appDir, { recursive: true });
await rm(appDir, { recursive: true });
const pullResult = await backend.runCLICommand([
'sync', 'pull',
'--yes'
], tempDir, "raw_app_new_file_test");
assertEquals(pullResult.code, 0, `Sync pull should succeed: ${pullResult.stderr}`);
expect(pullResult.code).toEqual(0);
// Verify new file was persisted
assert(await fileExists(newFilePath), "New file utils.ts should exist after pull");
expect(await fileExists(newFilePath)).toBeTruthy();
const newFileContent = await readFileContent(newFilePath);
assertStringIncludes(newFileContent, "formatValue", "New file content should persist");
expect(newFileContent).toContain("formatValue");
});
}
});
Deno.test({
name: "Raw App: delete file and push",
ignore: false,
sanitizeResources: false,
sanitizeOps: false,
fn: async () => {
test("Raw App: delete file and push", async () => {
await withTestBackend(async (backend, tempDir) => {
// Set up workspace
const testWorkspace = {
@@ -358,14 +343,14 @@ Deno.test({
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
// Create wmill.yaml
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
includes:
- "**"
excludes: []`);
excludes: []`, "utf-8");
// Create initial raw app
const appDir = path.join(tempDir, "f", "test", "delete_file_app.raw_app");
await ensureDir(path.join(tempDir, "f", "test"));
await mkdir(path.join(tempDir, "f", "test"), { recursive: true });
await createRawAppOnDisk(appDir);
// Initial push
@@ -374,20 +359,20 @@ excludes: []`);
'--yes'
], tempDir, "raw_app_delete_file_test");
assertEquals(pushResult1.code, 0, `Initial sync push should succeed: ${pushResult1.stderr}`);
expect(pushResult1.code).toEqual(0);
const indexCssPath = path.join(appDir, "index.css");
const appTsxPath = path.join(appDir, "App.tsx");
assert(await fileExists(indexCssPath), "index.css should exist after initial push");
expect(await fileExists(indexCssPath)).toBeTruthy();
// First, update App.tsx to remove the CSS import (otherwise bundle will fail)
const appTsxContent = await readFileContent(appTsxPath);
const updatedAppTsx = appTsxContent.replace("import './index.css'\n", "");
await Deno.writeTextFile(appTsxPath, updatedAppTsx);
await writeFile(appTsxPath, updatedAppTsx, "utf-8");
// Delete the CSS file
await Deno.remove(indexCssPath);
assert(!(await fileExists(indexCssPath)), "index.css should be deleted locally");
await rm(indexCssPath);
expect(!(await fileExists(indexCssPath))).toBeTruthy();
// Push changes
const pushResult2 = await backend.runCLICommand([
@@ -395,33 +380,27 @@ excludes: []`);
'--yes'
], tempDir, "raw_app_delete_file_test");
assertEquals(pushResult2.code, 0, `Sync push after delete should succeed: ${pushResult2.stderr}`);
expect(pushResult2.code).toEqual(0);
// Clear and pull again
await Deno.remove(appDir, { recursive: true });
await rm(appDir, { recursive: true });
const pullResult = await backend.runCLICommand([
'sync', 'pull',
'--yes'
], tempDir, "raw_app_delete_file_test");
assertEquals(pullResult.code, 0, `Sync pull should succeed: ${pullResult.stderr}`);
expect(pullResult.code).toEqual(0);
// Verify the deleted file is NOT pulled (it was deleted from backend)
assert(!(await fileExists(indexCssPath)), "Deleted index.css should not exist after pull");
expect(!(await fileExists(indexCssPath))).toBeTruthy();
// But other files should still exist
assert(await fileExists(appTsxPath), "App.tsx should still exist after pull");
expect(await fileExists(appTsxPath)).toBeTruthy();
});
}
});
Deno.test({
name: "Raw App: dry-run push shows expected changes",
ignore: false,
sanitizeResources: false,
sanitizeOps: false,
fn: async () => {
test("Raw App: dry-run push shows expected changes", async () => {
await withTestBackend(async (backend, tempDir) => {
// Set up workspace
const testWorkspace = {
@@ -433,14 +412,14 @@ Deno.test({
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
// Create wmill.yaml
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
includes:
- "**"
excludes: []`);
excludes: []`, "utf-8");
// Create raw app
const appDir = path.join(tempDir, "f", "test", "dry_run_app.raw_app");
await ensureDir(path.join(tempDir, "f", "test"));
await mkdir(path.join(tempDir, "f", "test"), { recursive: true });
await createRawAppOnDisk(appDir);
// Dry-run push
@@ -450,7 +429,7 @@ excludes: []`);
'--json-output'
], tempDir, "raw_app_dry_run_test");
assertEquals(dryRunResult.code, 0, `Dry-run push should succeed: ${dryRunResult.stderr}`);
expect(dryRunResult.code).toEqual(0);
// Parse JSON output (may be pretty-printed across multiple lines)
let jsonOutput = null;
@@ -469,13 +448,12 @@ excludes: []`);
}
}
assert(jsonOutput !== null, `Should have JSON output. Got: ${dryRunResult.stdout}`);
assert(Array.isArray(jsonOutput.changes), `Should have changes array. Got: ${JSON.stringify(jsonOutput)}`);
expect(jsonOutput !== null).toBeTruthy();
expect(Array.isArray(jsonOutput.changes)).toBeTruthy();
// Should include raw app in changes
const changePaths = jsonOutput.changes.map((c: any) => c.path);
const hasRawApp = changePaths.some((p: string) => p.includes("dry_run_app"));
assert(hasRawApp, `Dry-run should show raw app. Found: ${changePaths.join(', ')}`);
expect(hasRawApp).toBeTruthy();
});
}
});
+525
View File
@@ -0,0 +1,525 @@
/**
* Unit tests for resource_folders.ts path detection and manipulation functions.
* Tests both dotted (.flow, .app, .raw_app) and non-dotted (__flow, __app, __raw_app) modes.
*/
import { expect, test, describe, beforeEach } from "bun:test";
import {
setNonDottedPaths,
getNonDottedPaths,
getFolderSuffixes,
getFolderSuffix,
getMetadataFileName,
getMetadataPathSuffix,
isFlowPath,
isAppPath,
isRawAppPath,
isFolderResourcePath,
detectFolderResourceType,
isRawAppBackendPath,
isAppInlineScriptPath,
isFlowInlineScriptPath,
extractResourceName,
extractFolderPath,
buildFolderPath,
buildMetadataPath,
hasFolderSuffix,
validateFolderName,
extractNameFromFolder,
isFlowMetadataFile,
isAppMetadataFile,
isRawAppMetadataFile,
isRawAppFolderMetadataFile,
getDeleteSuffix,
transformJsonPathToDir,
} from "../src/utils/resource_folders.ts";
import { removeWorkerPrefix } from "../src/commands/worker-groups/worker-groups.ts";
// =============================================================================
// Helper: reset to dotted mode before each test
// =============================================================================
beforeEach(() => {
setNonDottedPaths(false);
});
// =============================================================================
// Configuration Functions
// =============================================================================
describe("setNonDottedPaths / getNonDottedPaths", () => {
test("defaults to false (dotted)", () => {
expect(getNonDottedPaths()).toBe(false);
});
test("can be set to true", () => {
setNonDottedPaths(true);
expect(getNonDottedPaths()).toBe(true);
});
test("can be toggled back to false", () => {
setNonDottedPaths(true);
setNonDottedPaths(false);
expect(getNonDottedPaths()).toBe(false);
});
});
describe("getFolderSuffixes", () => {
test("returns dotted suffixes by default", () => {
const suffixes = getFolderSuffixes();
expect(suffixes.flow).toBe(".flow");
expect(suffixes.app).toBe(".app");
expect(suffixes.raw_app).toBe(".raw_app");
});
test("returns non-dotted suffixes when configured", () => {
setNonDottedPaths(true);
const suffixes = getFolderSuffixes();
expect(suffixes.flow).toBe("__flow");
expect(suffixes.app).toBe("__app");
expect(suffixes.raw_app).toBe("__raw_app");
});
});
describe("getFolderSuffix", () => {
test("returns correct suffix for each type (dotted)", () => {
expect(getFolderSuffix("flow")).toBe(".flow");
expect(getFolderSuffix("app")).toBe(".app");
expect(getFolderSuffix("raw_app")).toBe(".raw_app");
});
test("returns correct suffix for each type (non-dotted)", () => {
setNonDottedPaths(true);
expect(getFolderSuffix("flow")).toBe("__flow");
expect(getFolderSuffix("app")).toBe("__app");
expect(getFolderSuffix("raw_app")).toBe("__raw_app");
});
});
// =============================================================================
// Metadata File Names
// =============================================================================
describe("getMetadataFileName", () => {
test("returns correct metadata file names", () => {
expect(getMetadataFileName("flow", "yaml")).toBe("flow.yaml");
expect(getMetadataFileName("flow", "json")).toBe("flow.json");
expect(getMetadataFileName("app", "yaml")).toBe("app.yaml");
expect(getMetadataFileName("app", "json")).toBe("app.json");
expect(getMetadataFileName("raw_app", "yaml")).toBe("raw_app.yaml");
expect(getMetadataFileName("raw_app", "json")).toBe("raw_app.json");
});
});
describe("getMetadataPathSuffix", () => {
test("returns correct path suffix (dotted)", () => {
expect(getMetadataPathSuffix("flow", "yaml")).toBe(".flow/flow.yaml");
expect(getMetadataPathSuffix("app", "json")).toBe(".app/app.json");
expect(getMetadataPathSuffix("raw_app", "yaml")).toBe(".raw_app/raw_app.yaml");
});
test("returns correct path suffix (non-dotted)", () => {
setNonDottedPaths(true);
expect(getMetadataPathSuffix("flow", "yaml")).toBe("__flow/flow.yaml");
expect(getMetadataPathSuffix("app", "json")).toBe("__app/app.json");
expect(getMetadataPathSuffix("raw_app", "yaml")).toBe("__raw_app/raw_app.yaml");
});
});
// =============================================================================
// Path Detection Functions (dotted mode)
// =============================================================================
describe("isFlowPath (dotted)", () => {
test("detects flow paths", () => {
expect(isFlowPath("f/my_flow.flow/flow.yaml")).toBe(true);
expect(isFlowPath("u/admin/test.flow/step.ts")).toBe(true);
});
test("rejects non-flow paths", () => {
expect(isFlowPath("f/my_script.ts")).toBe(false);
expect(isFlowPath("f/my_app.app/app.yaml")).toBe(false);
});
});
describe("isAppPath (dotted)", () => {
test("detects app paths", () => {
expect(isAppPath("f/my_app.app/app.yaml")).toBe(true);
expect(isAppPath("u/admin/dashboard.app/inline.ts")).toBe(true);
});
test("rejects non-app paths", () => {
expect(isAppPath("f/my_script.ts")).toBe(false);
expect(isAppPath("f/my_flow.flow/flow.yaml")).toBe(false);
});
});
describe("isRawAppPath (dotted)", () => {
test("detects raw_app paths", () => {
expect(isRawAppPath("f/my_raw.raw_app/raw_app.yaml")).toBe(true);
});
test("rejects non-raw_app paths", () => {
expect(isRawAppPath("f/my_app.app/app.yaml")).toBe(false);
expect(isRawAppPath("f/my_script.ts")).toBe(false);
});
});
// =============================================================================
// Path Detection Functions (non-dotted mode)
// =============================================================================
describe("isFlowPath (non-dotted)", () => {
test("detects non-dotted flow paths", () => {
setNonDottedPaths(true);
expect(isFlowPath("f/my_flow__flow/flow.yaml")).toBe(true);
});
test("rejects dotted flow paths in non-dotted mode", () => {
setNonDottedPaths(true);
expect(isFlowPath("f/my_flow.flow/flow.yaml")).toBe(false);
});
});
describe("isAppPath (non-dotted)", () => {
test("detects non-dotted app paths", () => {
setNonDottedPaths(true);
expect(isAppPath("f/my_app__app/app.yaml")).toBe(true);
});
});
describe("isRawAppPath (non-dotted)", () => {
test("detects non-dotted raw_app paths", () => {
setNonDottedPaths(true);
expect(isRawAppPath("f/my_raw__raw_app/raw_app.yaml")).toBe(true);
});
});
// =============================================================================
// Composite Path Detection
// =============================================================================
describe("isFolderResourcePath", () => {
test("returns true for any folder resource path", () => {
expect(isFolderResourcePath("f/x.flow/flow.yaml")).toBe(true);
expect(isFolderResourcePath("f/x.app/app.yaml")).toBe(true);
expect(isFolderResourcePath("f/x.raw_app/raw_app.yaml")).toBe(true);
});
test("returns false for non-folder paths", () => {
expect(isFolderResourcePath("f/script.ts")).toBe(false);
expect(isFolderResourcePath("f/var.variable.yaml")).toBe(false);
});
});
describe("detectFolderResourceType", () => {
test("detects flow type", () => {
expect(detectFolderResourceType("f/x.flow/flow.yaml")).toBe("flow");
});
test("detects app type", () => {
expect(detectFolderResourceType("f/x.app/app.yaml")).toBe("app");
});
test("detects raw_app type", () => {
expect(detectFolderResourceType("f/x.raw_app/raw_app.yaml")).toBe("raw_app");
});
test("returns null for non-folder paths", () => {
expect(detectFolderResourceType("f/script.ts")).toBeNull();
});
});
// =============================================================================
// Inline Script / Backend Path Detection
// =============================================================================
describe("isRawAppBackendPath", () => {
test("detects raw app backend paths (dotted)", () => {
expect(isRawAppBackendPath("f/my_app.raw_app/backend/handler.ts")).toBe(true);
});
test("rejects non-backend raw app paths", () => {
expect(isRawAppBackendPath("f/my_app.raw_app/raw_app.yaml")).toBe(false);
});
test("detects raw app backend paths (non-dotted)", () => {
setNonDottedPaths(true);
expect(isRawAppBackendPath("f/my_app__raw_app/backend/handler.ts")).toBe(true);
});
});
describe("isAppInlineScriptPath", () => {
test("detects inline script paths in apps", () => {
expect(isAppInlineScriptPath("f/dashboard.app/inline_0.ts")).toBe(true);
});
test("rejects non-app paths", () => {
expect(isAppInlineScriptPath("f/script.ts")).toBe(false);
});
});
describe("isFlowInlineScriptPath", () => {
test("detects inline script paths in flows", () => {
expect(isFlowInlineScriptPath("f/pipeline.flow/step_0.ts")).toBe(true);
});
test("rejects non-flow paths", () => {
expect(isFlowInlineScriptPath("f/script.ts")).toBe(false);
});
});
// =============================================================================
// Path Manipulation Functions
// =============================================================================
describe("extractResourceName", () => {
test("extracts name from flow path", () => {
expect(extractResourceName("f/my_flow.flow/flow.yaml", "flow")).toBe("f/my_flow");
});
test("extracts name from app path", () => {
expect(extractResourceName("f/dashboard.app/app.yaml", "app")).toBe("f/dashboard");
});
test("extracts name from raw_app path", () => {
expect(extractResourceName("f/my_raw.raw_app/raw_app.yaml", "raw_app")).toBe("f/my_raw");
});
test("returns null when type doesn't match", () => {
expect(extractResourceName("f/script.ts", "flow")).toBeNull();
});
test("works in non-dotted mode", () => {
setNonDottedPaths(true);
expect(extractResourceName("f/my_flow__flow/flow.yaml", "flow")).toBe("f/my_flow");
});
});
describe("extractFolderPath", () => {
test("extracts folder path from flow", () => {
expect(extractFolderPath("f/my_flow.flow/flow.yaml", "flow")).toBe("f/my_flow.flow/");
});
test("returns null when type doesn't match", () => {
expect(extractFolderPath("f/script.ts", "flow")).toBeNull();
});
});
describe("buildFolderPath", () => {
test("builds folder path (dotted)", () => {
expect(buildFolderPath("f/my_flow", "flow")).toBe("f/my_flow.flow");
expect(buildFolderPath("f/dashboard", "app")).toBe("f/dashboard.app");
expect(buildFolderPath("f/my_raw", "raw_app")).toBe("f/my_raw.raw_app");
});
test("builds folder path (non-dotted)", () => {
setNonDottedPaths(true);
expect(buildFolderPath("f/my_flow", "flow")).toBe("f/my_flow__flow");
expect(buildFolderPath("f/dashboard", "app")).toBe("f/dashboard__app");
expect(buildFolderPath("f/my_raw", "raw_app")).toBe("f/my_raw__raw_app");
});
});
describe("buildMetadataPath", () => {
test("builds metadata path (dotted, yaml)", () => {
expect(buildMetadataPath("f/my_flow", "flow", "yaml")).toBe("f/my_flow.flow/flow.yaml");
});
test("builds metadata path (dotted, json)", () => {
expect(buildMetadataPath("f/dashboard", "app", "json")).toBe("f/dashboard.app/app.json");
});
test("builds metadata path (non-dotted)", () => {
setNonDottedPaths(true);
expect(buildMetadataPath("f/my_flow", "flow", "yaml")).toBe("f/my_flow__flow/flow.yaml");
});
});
// =============================================================================
// Folder Validation Functions
// =============================================================================
describe("hasFolderSuffix", () => {
test("returns true for matching suffix", () => {
expect(hasFolderSuffix("my_flow.flow", "flow")).toBe(true);
expect(hasFolderSuffix("dashboard.app", "app")).toBe(true);
expect(hasFolderSuffix("my_raw.raw_app", "raw_app")).toBe(true);
});
test("returns false for non-matching suffix", () => {
expect(hasFolderSuffix("my_flow.app", "flow")).toBe(false);
expect(hasFolderSuffix("script.ts", "flow")).toBe(false);
});
test("works in non-dotted mode", () => {
setNonDottedPaths(true);
expect(hasFolderSuffix("my_flow__flow", "flow")).toBe(true);
expect(hasFolderSuffix("my_flow.flow", "flow")).toBe(false);
});
});
describe("validateFolderName", () => {
test("returns null for valid folder name", () => {
expect(validateFolderName("my_flow.flow", "flow")).toBeNull();
});
test("returns error message for invalid folder name", () => {
const result = validateFolderName("my_flow.app", "flow");
expect(result).not.toBeNull();
expect(result).toContain("my_flow.app");
expect(result).toContain(".flow");
});
});
describe("extractNameFromFolder", () => {
test("extracts name by removing suffix (dotted)", () => {
expect(extractNameFromFolder("my_flow.flow", "flow")).toBe("my_flow");
expect(extractNameFromFolder("dashboard.app", "app")).toBe("dashboard");
expect(extractNameFromFolder("my_raw.raw_app", "raw_app")).toBe("my_raw");
});
test("returns original name if suffix doesn't match", () => {
expect(extractNameFromFolder("my_script", "flow")).toBe("my_script");
});
test("extracts name (non-dotted)", () => {
setNonDottedPaths(true);
expect(extractNameFromFolder("my_flow__flow", "flow")).toBe("my_flow");
});
});
// =============================================================================
// Metadata File Detection Functions
// =============================================================================
describe("isFlowMetadataFile", () => {
test("detects dotted flow metadata files", () => {
expect(isFlowMetadataFile("f/my_flow.flow.json")).toBe(true);
expect(isFlowMetadataFile("f/my_flow.flow.yaml")).toBe(true);
});
test("rejects non-flow metadata files", () => {
expect(isFlowMetadataFile("f/my_app.app.json")).toBe(false);
expect(isFlowMetadataFile("f/script.ts")).toBe(false);
});
test("detects non-dotted flow metadata files when configured", () => {
setNonDottedPaths(true);
expect(isFlowMetadataFile("f/my_flow__flow.json")).toBe(true);
expect(isFlowMetadataFile("f/my_flow__flow.yaml")).toBe(true);
// API format (dotted) is always detected
expect(isFlowMetadataFile("f/my_flow.flow.json")).toBe(true);
});
});
describe("isAppMetadataFile", () => {
test("detects dotted app metadata files", () => {
expect(isAppMetadataFile("f/dashboard.app.json")).toBe(true);
expect(isAppMetadataFile("f/dashboard.app.yaml")).toBe(true);
});
test("rejects non-app metadata files", () => {
expect(isAppMetadataFile("f/my_flow.flow.json")).toBe(false);
});
test("detects non-dotted app metadata files when configured", () => {
setNonDottedPaths(true);
expect(isAppMetadataFile("f/dashboard__app.json")).toBe(true);
// API format always detected
expect(isAppMetadataFile("f/dashboard.app.json")).toBe(true);
});
});
describe("isRawAppMetadataFile", () => {
test("detects dotted raw_app metadata files", () => {
expect(isRawAppMetadataFile("f/my_raw.raw_app.json")).toBe(true);
expect(isRawAppMetadataFile("f/my_raw.raw_app.yaml")).toBe(true);
});
test("rejects non-raw_app metadata files", () => {
expect(isRawAppMetadataFile("f/my_app.app.json")).toBe(false);
});
test("detects non-dotted raw_app metadata files when configured", () => {
setNonDottedPaths(true);
expect(isRawAppMetadataFile("f/my_raw__raw_app.json")).toBe(true);
expect(isRawAppMetadataFile("f/my_raw.raw_app.json")).toBe(true);
});
});
describe("isRawAppFolderMetadataFile", () => {
test("detects raw_app folder metadata file (dotted)", () => {
expect(isRawAppFolderMetadataFile("f/my_raw.raw_app/raw_app.yaml")).toBe(true);
expect(isRawAppFolderMetadataFile("f/my_raw.raw_app/raw_app.json")).toBe(true);
});
test("rejects non-metadata files", () => {
expect(isRawAppFolderMetadataFile("f/my_raw.raw_app/backend/handler.ts")).toBe(false);
});
});
// =============================================================================
// Sync-related Path Functions
// =============================================================================
describe("getDeleteSuffix", () => {
test("returns correct delete suffix", () => {
expect(getDeleteSuffix("flow", "yaml")).toBe(".flow/flow.yaml");
expect(getDeleteSuffix("app", "json")).toBe(".app/app.json");
expect(getDeleteSuffix("raw_app", "yaml")).toBe(".raw_app/raw_app.yaml");
});
test("returns correct delete suffix (non-dotted)", () => {
setNonDottedPaths(true);
expect(getDeleteSuffix("flow", "yaml")).toBe("__flow/flow.yaml");
});
});
describe("transformJsonPathToDir", () => {
test("transforms API dotted .flow.json to dotted dir", () => {
expect(transformJsonPathToDir("f/my_flow.flow.json", "flow")).toBe("f/my_flow.flow");
});
test("transforms API dotted .app.json to dotted dir", () => {
expect(transformJsonPathToDir("f/dashboard.app.json", "app")).toBe("f/dashboard.app");
});
test("transforms API dotted to non-dotted dir when configured", () => {
setNonDottedPaths(true);
expect(transformJsonPathToDir("f/my_flow.flow.json", "flow")).toBe("f/my_flow__flow");
});
test("handles already-configured format", () => {
setNonDottedPaths(true);
expect(transformJsonPathToDir("f/my_flow__flow.json", "flow")).toBe("f/my_flow__flow");
});
test("returns unchanged path when suffix doesn't match", () => {
expect(transformJsonPathToDir("f/script.ts", "flow")).toBe("f/script.ts");
});
});
// =============================================================================
// removeWorkerPrefix (from worker-groups.ts)
// =============================================================================
describe("removeWorkerPrefix", () => {
test("removes worker__ prefix", () => {
expect(removeWorkerPrefix("worker__default")).toBe("default");
expect(removeWorkerPrefix("worker__gpu")).toBe("gpu");
});
test("returns name unchanged if no prefix", () => {
expect(removeWorkerPrefix("default")).toBe("default");
expect(removeWorkerPrefix("gpu")).toBe("gpu");
});
test("handles empty string", () => {
expect(removeWorkerPrefix("")).toBe("");
});
test("handles worker__ as the entire name", () => {
expect(removeWorkerPrefix("worker__")).toBe("");
});
});
+45 -83
View File
@@ -7,21 +7,17 @@
* the env variables aren't there anymore.
*/
import { assertEquals, assert } from "https://deno.land/std@0.224.0/assert/mod.ts";
import { ensureDir } from "https://deno.land/std@0.224.0/fs/mod.ts";
import { expect, test } from "bun:test";
import { writeFile, readFile, mkdir } from "node:fs/promises";
import { withTestBackend } from "./test_backend.ts";
Deno.test({
name: "Integration: Script envs field is preserved during sync pull/push cycle",
sanitizeResources: false,
sanitizeOps: false,
async fn() {
test("Integration: Script envs field is preserved during sync pull/push cycle", async () => {
await withTestBackend(async (backend, tempDir) => {
const uniqueId = Date.now();
const scriptPath = `f/test/envs_script_${uniqueId}`;
// Step 1: Create a script via API with envs set
await ensureDir(`${tempDir}/f/test`);
await mkdir(`${tempDir}/f/test`, { recursive: true });
// Create folder first
const folderResp = await backend.apiRequest!(`/api/w/${backend.workspace}/folders/create`, {
@@ -46,102 +42,77 @@ Deno.test({
}),
});
assertEquals(
createResp.ok,
true,
`Failed to create script: ${await createResp.text()}`,
);
expect(createResp.ok).toEqual(true);
// Verify the script was created with envs
const getResp = await backend.apiRequest!(
`/api/w/${backend.workspace}/scripts/get/p/${scriptPath}`,
);
const createdScriptText = await getResp.text();
assertEquals(getResp.ok, true, `Failed to get script: ${createdScriptText}`);
expect(getResp.ok).toEqual(true);
const createdScript = JSON.parse(createdScriptText);
assertEquals(
createdScript.envs,
["MY_ENV_VAR", "ANOTHER_VAR"],
"Script should have envs after creation",
);
expect(createdScript.envs).toEqual(["MY_ENV_VAR", "ANOTHER_VAR"]);
// Step 2: Create wmill.yaml and sync pull
await Deno.writeTextFile(
await writeFile(
`${tempDir}/wmill.yaml`,
`defaultTs: bun
includes:
- "f/test/envs_script_${uniqueId}**"
excludes: []
`,
"utf-8",
);
const pullResult = await backend.runCLICommand(["sync", "pull", "--yes"], tempDir);
assertEquals(
pullResult.code,
0,
`Pull should succeed.\nstdout: ${pullResult.stdout}\nstderr: ${pullResult.stderr}`,
);
expect(pullResult.code).toEqual(0);
// Verify the pulled metadata contains envs
const metadataPath = `${tempDir}/f/test/envs_script_${uniqueId}.script.yaml`;
const metadataContent = await Deno.readTextFile(metadataPath);
assert(
const metadataContent = await readFile(metadataPath, "utf-8");
expect(
metadataContent.includes("envs:") ||
metadataContent.includes("MY_ENV_VAR") ||
metadataContent.includes("ANOTHER_VAR"),
`Pulled metadata should contain envs. Content:\n${metadataContent}`,
);
).toBeTruthy();
// Step 3: Modify the script locally (change content)
const scriptFilePath = `${tempDir}/f/test/envs_script_${uniqueId}.ts`;
const originalContent = await Deno.readTextFile(scriptFilePath);
await Deno.writeTextFile(
const originalContent = await readFile(scriptFilePath, "utf-8");
await writeFile(
scriptFilePath,
originalContent.replace("Hello world", "Hello world modified"),
"utf-8",
);
// Step 4: Sync push
const pushResult = await backend.runCLICommand(["sync", "push", "--yes"], tempDir);
assertEquals(
pushResult.code,
0,
`Push should succeed.\nstdout: ${pushResult.stdout}\nstderr: ${pushResult.stderr}`,
);
expect(pushResult.code).toEqual(0);
// Step 5: Verify envs are still present on the remote
const getResp2 = await backend.apiRequest!(
`/api/w/${backend.workspace}/scripts/get/p/${scriptPath}`,
);
const updatedScriptText = await getResp2.text();
assertEquals(getResp2.ok, true, `Failed to get script after push: ${updatedScriptText}`);
expect(getResp2.ok).toEqual(true);
const updatedScript = JSON.parse(updatedScriptText);
assertEquals(
updatedScript.envs,
["MY_ENV_VAR", "ANOTHER_VAR"],
`Script envs should be preserved after push. Got: ${JSON.stringify(updatedScript.envs)}`,
);
expect(updatedScript.envs).toEqual(["MY_ENV_VAR", "ANOTHER_VAR"]);
// Also verify the content was updated
assert(
expect(
updatedScript.content.includes("Hello world modified"),
"Script content should be updated",
);
).toBeTruthy();
});
},
});
Deno.test({
name: "Integration: Script envs field changes are detected and pushed",
sanitizeResources: false,
sanitizeOps: false,
async fn() {
test("Integration: Script envs field changes are detected and pushed", async () => {
await withTestBackend(async (backend, tempDir) => {
const uniqueId = Date.now();
const scriptPath = `f/test/envs_change_${uniqueId}`;
// Create folder
await ensureDir(`${tempDir}/f/test`);
await mkdir(`${tempDir}/f/test`, { recursive: true });
await backend.apiRequest!(`/api/w/${backend.workspace}/folders/create`, {
method: "POST",
headers: { "Content-Type": "application/json" },
@@ -162,25 +133,26 @@ Deno.test({
kind: "script",
}),
});
assertEquals(createResp.ok, true, `Failed to create script: ${await createResp.text()}`);
expect(createResp.ok).toEqual(true);
// Setup wmill.yaml
await Deno.writeTextFile(
await writeFile(
`${tempDir}/wmill.yaml`,
`defaultTs: bun
includes:
- "f/test/envs_change_${uniqueId}**"
excludes: []
`,
"utf-8",
);
// Pull
const pullResult = await backend.runCLICommand(["sync", "pull", "--yes"], tempDir);
assertEquals(pullResult.code, 0, `Pull failed: ${pullResult.stderr}`);
expect(pullResult.code).toEqual(0);
// Modify envs in the local metadata file
const metadataPath = `${tempDir}/f/test/envs_change_${uniqueId}.script.yaml`;
let metadataContent = await Deno.readTextFile(metadataPath);
let metadataContent = await readFile(metadataPath, "utf-8");
// Replace the envs line(s)
if (metadataContent.includes("envs:")) {
@@ -193,40 +165,31 @@ excludes: []
// Add envs if not present
metadataContent += "\nenvs:\n - NEW_VAR1\n - NEW_VAR2\n";
}
await Deno.writeTextFile(metadataPath, metadataContent);
await writeFile(metadataPath, metadataContent, "utf-8");
// Push
const pushResult = await backend.runCLICommand(["sync", "push", "--yes"], tempDir);
assertEquals(pushResult.code, 0, `Push failed: ${pushResult.stderr}`);
expect(pushResult.code).toEqual(0);
// Verify envs were updated on remote
const getResp = await backend.apiRequest!(
`/api/w/${backend.workspace}/scripts/get/p/${scriptPath}`,
);
const scriptText = await getResp.text();
assertEquals(getResp.ok, true, `Failed to get script: ${scriptText}`);
expect(getResp.ok).toEqual(true);
const script = JSON.parse(scriptText);
assertEquals(
script.envs,
["NEW_VAR1", "NEW_VAR2"],
`Script envs should be updated to new values. Got: ${JSON.stringify(script.envs)}`,
);
expect(script.envs).toEqual(["NEW_VAR1", "NEW_VAR2"]);
});
},
});
Deno.test({
name: "Integration: Script with empty envs is handled correctly",
sanitizeResources: false,
sanitizeOps: false,
async fn() {
test("Integration: Script with empty envs is handled correctly", async () => {
await withTestBackend(async (backend, tempDir) => {
const uniqueId = Date.now();
const scriptPath = `f/test/empty_envs_${uniqueId}`;
// Create folder
await ensureDir(`${tempDir}/f/test`);
await mkdir(`${tempDir}/f/test`, { recursive: true });
await backend.apiRequest!(`/api/w/${backend.workspace}/folders/create`, {
method: "POST",
headers: { "Content-Type": "application/json" },
@@ -246,32 +209,34 @@ Deno.test({
kind: "script",
}),
});
assertEquals(createResp.ok, true, `Failed to create script: ${await createResp.text()}`);
expect(createResp.ok).toEqual(true);
// Setup wmill.yaml
await Deno.writeTextFile(
await writeFile(
`${tempDir}/wmill.yaml`,
`defaultTs: bun
includes:
- "f/test/empty_envs_${uniqueId}**"
excludes: []
`,
"utf-8",
);
// Pull
const pullResult = await backend.runCLICommand(["sync", "pull", "--yes"], tempDir);
assertEquals(pullResult.code, 0, `Pull failed: ${pullResult.stderr}`);
expect(pullResult.code).toEqual(0);
// Modify content
const scriptFilePath = `${tempDir}/f/test/empty_envs_${uniqueId}.ts`;
await Deno.writeTextFile(
await writeFile(
scriptFilePath,
`export async function main() {\n return "Modified no envs";\n}`,
"utf-8",
);
// Push
const pushResult = await backend.runCLICommand(["sync", "push", "--yes"], tempDir);
assertEquals(pushResult.code, 0, `Push failed: ${pushResult.stderr}`);
expect(pushResult.code).toEqual(0);
// Verify script was updated and envs is still null/empty
const getResp = await backend.apiRequest!(
@@ -279,16 +244,13 @@ excludes: []
);
const script = await getResp.json();
assert(
expect(
script.content.includes("Modified no envs"),
"Script content should be updated",
);
).toBeTruthy();
// envs should be null, empty, or undefined
assert(
expect(
!script.envs || script.envs.length === 0,
`Script envs should remain empty. Got: ${JSON.stringify(script.envs)}`,
);
).toBeTruthy();
});
},
});
+197
View File
@@ -0,0 +1,197 @@
/**
* Unit tests for settings.ts pure functions.
* Tests migrateToGroupedFormat which converts legacy flat settings to grouped format.
*/
import { expect, test, describe } from "bun:test";
import { migrateToGroupedFormat } from "../src/core/settings.ts";
// =============================================================================
// migrateToGroupedFormat
// =============================================================================
describe("migrateToGroupedFormat", () => {
test("migrates legacy auto_invite fields to grouped format", () => {
const legacy = {
name: "my-workspace",
auto_invite_enabled: true,
auto_invite_as: "operator",
auto_invite_mode: "add",
};
const result = migrateToGroupedFormat(legacy);
expect(result.auto_invite).toEqual({
enabled: true,
operator: true,
mode: "add",
});
});
test("migrates legacy auto_invite with non-operator role", () => {
const legacy = {
name: "ws",
auto_invite_enabled: true,
auto_invite_as: "developer",
auto_invite_mode: "invite",
};
const result = migrateToGroupedFormat(legacy);
expect(result.auto_invite).toEqual({
enabled: true,
operator: false,
mode: "invite",
});
});
test("migrates legacy auto_invite when disabled", () => {
const legacy = {
name: "ws",
auto_invite_enabled: false,
auto_invite_as: "operator",
};
const result = migrateToGroupedFormat(legacy);
expect(result.auto_invite!.enabled).toBe(false);
});
test("preserves already-grouped auto_invite", () => {
const grouped = {
name: "ws",
auto_invite: { enabled: true, operator: false, mode: "invite" as const },
};
const result = migrateToGroupedFormat(grouped);
expect(result.auto_invite).toEqual({
enabled: true,
operator: false,
mode: "invite",
});
});
test("migrates legacy error_handler string to grouped format", () => {
const legacy = {
name: "ws",
error_handler: "u/admin/error_handler",
error_handler_extra_args: { notify: true },
error_handler_muted_on_cancel: true,
};
const result = migrateToGroupedFormat(legacy);
expect(result.error_handler).toEqual({
path: "u/admin/error_handler",
extra_args: { notify: true },
muted_on_cancel: true,
});
});
test("preserves already-grouped error_handler", () => {
const grouped = {
name: "ws",
error_handler: {
path: "u/admin/handler",
extra_args: {},
muted_on_cancel: false,
},
};
const result = migrateToGroupedFormat(grouped);
expect(result.error_handler).toEqual({
path: "u/admin/handler",
extra_args: {},
muted_on_cancel: false,
});
});
test("migrates legacy success_handler string to grouped format", () => {
const legacy = {
name: "ws",
success_handler: "u/admin/on_success",
success_handler_extra_args: { channel: "#deploys" },
};
const result = migrateToGroupedFormat(legacy);
expect(result.success_handler).toEqual({
path: "u/admin/on_success",
extra_args: { channel: "#deploys" },
});
});
test("preserves already-grouped success_handler", () => {
const grouped = {
name: "ws",
success_handler: { path: "u/admin/handler", extra_args: {} },
};
const result = migrateToGroupedFormat(grouped);
expect(result.success_handler).toEqual({
path: "u/admin/handler",
extra_args: {},
});
});
test("copies non-legacy fields through", () => {
const settings = {
name: "my-workspace",
webhook: "https://example.com/hook",
deploy_to: "staging",
default_app: "u/admin/dashboard",
mute_critical_alerts: true,
color: "#ff0000",
};
const result = migrateToGroupedFormat(settings);
expect(result.name).toBe("my-workspace");
expect(result.webhook).toBe("https://example.com/hook");
expect(result.deploy_to).toBe("staging");
expect(result.default_app).toBe("u/admin/dashboard");
expect(result.mute_critical_alerts).toBe(true);
expect(result.color).toBe("#ff0000");
});
test("handles minimal settings with only name", () => {
const result = migrateToGroupedFormat({ name: "ws" });
expect(result.name).toBe("ws");
expect(result.auto_invite).toBeUndefined();
expect(result.error_handler).toBeUndefined();
expect(result.success_handler).toBeUndefined();
});
test("defaults name to empty string when missing", () => {
const result = migrateToGroupedFormat({});
expect(result.name).toBe("");
});
test("defaults auto_invite_mode to invite when missing", () => {
const legacy = {
name: "ws",
auto_invite_enabled: true,
auto_invite_as: "operator",
};
const result = migrateToGroupedFormat(legacy);
expect(result.auto_invite!.mode).toBe("invite");
});
test("defaults error_handler_muted_on_cancel to false when missing", () => {
const legacy = {
name: "ws",
error_handler: "u/admin/handler",
};
const result = migrateToGroupedFormat(legacy);
expect(result.error_handler!.muted_on_cancel).toBe(false);
});
test("preserves ai_config, large_file_storage, git_sync, default_scripts, operator_settings", () => {
const settings = {
name: "ws",
ai_config: { provider: "openai" },
large_file_storage: { type: "s3" },
git_sync: { enabled: true },
default_scripts: { python: "template.py" },
operator_settings: { hideCode: true },
};
const result = migrateToGroupedFormat(settings);
expect(result.ai_config).toEqual({ provider: "openai" });
expect(result.large_file_storage).toEqual({ type: "s3" });
expect(result.git_sync).toEqual({ enabled: true });
expect(result.default_scripts).toEqual({ python: "template.py" });
expect(result.operator_settings).toEqual({ hideCode: true });
});
test("does not include undefined fields in result", () => {
const result = migrateToGroupedFormat({ name: "ws" });
expect("webhook" in result).toBe(false);
expect("deploy_to" in result).toBe(false);
expect("color" in result).toBe(false);
});
});
+94
View File
@@ -0,0 +1,94 @@
/**
* Global test setup preloaded before all test files.
*
* 1. Builds the backend binary so `cargo run` starts instantly.
* 2. Starts a shared backend instance so integration tests don't
* bear the startup cost inside their per-test timeout window.
*/
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { statSync } from "node:fs";
const __dirname = resolve(fileURLToPath(import.meta.url), "..");
function findBackendDir(): string {
const candidates = [
resolve(__dirname, "..", "..", "backend"),
resolve(__dirname, "..", "..", "..", "backend"),
resolve(".", "backend"),
resolve("..", "backend"),
];
for (const candidate of candidates) {
try {
const cargoPath = resolve(candidate, "Cargo.toml");
const stat = statSync(cargoPath);
if (stat.isFile()) {
return candidate;
}
} catch {
// Continue searching
}
}
throw new Error("Could not find backend directory.");
}
// Build the backend binary so `cargo run` is fast for all tests
const backendDir = findBackendDir();
const isCI = process.env["CI_MINIMAL_FEATURES"] === "true";
const hasLicenseKey = !!process.env["EE_LICENSE_KEY"];
const features = isCI
? ["zip"]
: hasLicenseKey
? ["zip", "private", "enterprise", "license"]
: ["zip"];
const cargoArgs = ["build", "--features", features.join(",")];
console.log(`Pre-building backend: cargo ${cargoArgs.join(" ")}`);
const proc = Bun.spawn(["cargo", ...cargoArgs], {
cwd: backendDir,
stdout: "inherit",
stderr: "inherit",
env: {
...process.env as Record<string, string>,
SQLX_OFFLINE: "true",
},
});
const exitCode = await proc.exited;
if (exitCode !== 0) {
throw new Error(`cargo build failed with exit code ${exitCode}`);
}
console.log("Backend build complete.");
// Start the shared backend instance so it's ready before any test runs.
// This avoids the first integration test timing out while the backend
// creates its database, starts the process, and waits for the health check.
if (process.env["DATABASE_URL"]) {
const { getTestBackend } = await import("./test_backend.ts");
console.log("Pre-starting test backend...");
await getTestBackend();
console.log("Test backend is ready for all tests.");
}
// When TEST_CLI_RUNTIME=node, also build the npm package so tests
// can invoke `node npm/esm/main.js` instead of `bun run src/main.ts`
if (process.env["TEST_CLI_RUNTIME"] === "node") {
const cliDir = resolve(__dirname, "..");
console.log("Building npm package for Node runtime testing...");
const npmBuild = Bun.spawn(["bun", "run", "build-npm.ts"], {
cwd: cliDir,
stdout: "inherit",
stderr: "inherit",
env: process.env as Record<string, string>,
});
const npmExit = await npmBuild.exited;
if (npmExit !== 0) {
throw new Error(`npm build failed with exit code ${npmExit}`);
}
console.log("npm package built — tests will use Node runtime.");
}
File diff suppressed because it is too large Load Diff
+514
View File
@@ -0,0 +1,514 @@
/**
* Integration tests for standalone CLI commands that previously had zero coverage.
*
* Tests:
* - `wmill folder` (list)
* - `wmill schedule` (list with data)
* - `wmill resource-type list` and `wmill resource-type push`
* - `wmill script show`, `wmill script run`, `wmill script bootstrap`
* - `wmill user` (list, add, remove)
*/
import { expect, test, describe } from "bun:test";
import { writeFile, mkdir, stat, readFile } from "node:fs/promises";
import { join } from "node:path";
import { withTestBackend, type TestBackend } from "./test_backend.ts";
import { shouldSkipOnCI } from "./cargo_backend.ts";
import { addWorkspace } from "../workspace.ts";
async function setupWorkspaceProfile(backend: TestBackend): Promise<void> {
await addWorkspace(
{
remote: backend.baseUrl,
workspaceId: backend.workspace,
name: "localhost_test",
token: backend.token!,
},
{ force: true, configDir: backend.testConfigDir }
);
}
/** Create a script on the remote via API and return its path */
async function createRemoteScript(
backend: TestBackend,
scriptPath: string,
content: string = 'export async function main() { return "hello"; }'
): Promise<void> {
const resp = await backend.apiRequest!(
`/api/w/${backend.workspace}/scripts/create`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
path: scriptPath,
content,
language: "bun",
summary: "Test script",
description: "Created by integration test",
schema: {
$schema: "https://json-schema.org/draft/2020-12/schema",
type: "object",
properties: {},
required: [],
},
}),
}
);
expect(resp.status).toBeLessThan(300);
await resp.text();
}
// =============================================================================
// Folder List
// =============================================================================
describe("folder list command", () => {
test("lists seeded folders", async () => {
await withTestBackend(async (backend, tempDir) => {
await setupWorkspaceProfile(backend);
const result = await backend.runCLICommand(["folder"], tempDir);
expect(result.code).toEqual(0);
// seedTestData creates a "test" folder
expect(result.stdout).toContain("test");
// Table headers should be present
expect(result.stdout).toContain("Name");
});
});
});
// =============================================================================
// Schedule List
// =============================================================================
describe("schedule list command", () => {
test("lists a schedule created via API", async () => {
await withTestBackend(async (backend, tempDir) => {
await setupWorkspaceProfile(backend);
const uniqueId = Date.now();
const scriptPath = `f/test/sched_list_target_${uniqueId}`;
const schedulePath = `f/test/sched_list_${uniqueId}`;
// Create target script
await createRemoteScript(backend, scriptPath);
// Create schedule via API
const createResp = await backend.apiRequest!(
`/api/w/${backend.workspace}/schedules/create`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
path: schedulePath,
schedule: "0 0 12 * * *",
script_path: scriptPath,
is_flow: false,
args: {},
enabled: false,
timezone: "UTC",
}),
}
);
expect(createResp.status).toBeLessThan(300);
await createResp.text();
// List schedules via CLI
const result = await backend.runCLICommand(["schedule"], tempDir);
expect(result.code).toEqual(0);
expect(result.stdout).toContain(schedulePath);
expect(result.stdout).toContain("0 0 12 * * *");
});
});
});
// =============================================================================
// Resource Type List & Push
// =============================================================================
describe("resource-type commands", () => {
test("list returns exit code 0", async () => {
await withTestBackend(async (backend, tempDir) => {
await setupWorkspaceProfile(backend);
const result = await backend.runCLICommand(
["resource-type", "list"],
tempDir
);
expect(result.code).toEqual(0);
// Table headers should be present
expect(result.stdout).toContain("Name");
});
});
test("push creates a new resource type", async () => {
await withTestBackend(async (backend, tempDir) => {
await setupWorkspaceProfile(backend);
const uniqueId = Date.now();
const rtName = `test_rt_${uniqueId}`;
// Create a resource type JSON file
const rtFile = join(tempDir, `${rtName}.resource-type.json`);
await writeFile(
rtFile,
JSON.stringify({
schema: {
type: "object",
properties: {
host: { type: "string" },
port: { type: "integer" },
},
},
description: "Test resource type from integration test",
}),
"utf-8"
);
// Push via CLI — the name argument must include the .resource-type.json suffix
const pushResult = await backend.runCLICommand(
["resource-type", "push", rtFile, `${rtName}.resource-type.json`],
tempDir
);
expect(pushResult.code).toEqual(0);
// Verify via API
const apiResp = await backend.apiRequest!(
`/api/w/${backend.workspace}/resources/type/get/${rtName}`
);
expect(apiResp.status).toEqual(200);
const rtData = await apiResp.json();
expect(rtData.name).toBe(rtName);
expect(rtData.schema).toBeDefined();
expect(rtData.schema.properties.host.type).toBe("string");
});
});
test("push updates an existing resource type", async () => {
await withTestBackend(async (backend, tempDir) => {
await setupWorkspaceProfile(backend);
const uniqueId = Date.now();
const rtName = `test_rt_upd_${uniqueId}`;
// Create resource type via API first
const createResp = await backend.apiRequest!(
`/api/w/${backend.workspace}/resources/type/create`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: rtName,
schema: {
type: "object",
properties: { old_field: { type: "string" } },
},
description: "Original",
}),
}
);
expect(createResp.status).toBeLessThan(300);
await createResp.text();
// Create updated resource type file
const rtFile = join(tempDir, `${rtName}.resource-type.json`);
await writeFile(
rtFile,
JSON.stringify({
schema: {
type: "object",
properties: {
new_field: { type: "number" },
},
},
description: "Updated description",
}),
"utf-8"
);
// Push update via CLI — the name argument must include the .resource-type.json suffix
const pushResult = await backend.runCLICommand(
["resource-type", "push", rtFile, `${rtName}.resource-type.json`],
tempDir
);
expect(pushResult.code).toEqual(0);
// Verify the update via API
const apiResp = await backend.apiRequest!(
`/api/w/${backend.workspace}/resources/type/get/${rtName}`
);
expect(apiResp.status).toEqual(200);
const rtData = await apiResp.json();
expect(rtData.description).toBe("Updated description");
expect(rtData.schema.properties.new_field.type).toBe("number");
});
});
});
// =============================================================================
// Script Show
// =============================================================================
describe("script show command", () => {
test("shows script content", async () => {
await withTestBackend(async (backend, tempDir) => {
await setupWorkspaceProfile(backend);
const uniqueId = Date.now();
const scriptPath = `f/test/show_script_${uniqueId}`;
const scriptContent = `export async function main() { return "show_test_${uniqueId}"; }`;
await createRemoteScript(backend, scriptPath, scriptContent);
const result = await backend.runCLICommand(
["script", "show", scriptPath],
tempDir
);
expect(result.code).toEqual(0);
// Should display the script content
const output = result.stdout + result.stderr;
expect(output).toContain(`show_test_${uniqueId}`);
expect(output).toContain(scriptPath);
});
});
});
// =============================================================================
// Script Run
// =============================================================================
describe("script run command", () => {
test("runs a script and returns result", { timeout: 60000 }, async () => {
await withTestBackend(async (backend, tempDir) => {
await setupWorkspaceProfile(backend);
const uniqueId = Date.now();
const scriptPath = `f/test/run_script_${uniqueId}`;
const scriptContent = `export async function main() { return { value: "run_result_${uniqueId}" }; }`;
await createRemoteScript(backend, scriptPath, scriptContent);
const result = await backend.runCLICommand(
["script", "run", scriptPath, "--silent"],
tempDir
);
expect(result.code).toEqual(0);
expect(result.stdout).toContain(`run_result_${uniqueId}`);
});
});
});
// =============================================================================
// Script Bootstrap
// =============================================================================
describe("script bootstrap command", () => {
test("creates TypeScript script files", async () => {
await withTestBackend(async (backend, tempDir) => {
await setupWorkspaceProfile(backend);
// Create a wmill.yaml so bootstrap can read config
await writeFile(
join(tempDir, "wmill.yaml"),
`defaultTs: bun\nincludes:\n - "**"\nexcludes: []\n`,
"utf-8"
);
await mkdir(join(tempDir, "f", "test"), { recursive: true });
const result = await backend.runCLICommand(
[
"script",
"bootstrap",
"f/test/new_script",
"bun",
"--summary",
"My new script",
],
tempDir
);
expect(result.code).toEqual(0);
// Verify the code file was created
const codeStat = await stat(join(tempDir, "f/test/new_script.ts"));
expect(codeStat.isFile()).toBe(true);
// Verify the metadata file was created
const metaStat = await stat(
join(tempDir, "f/test/new_script.script.yaml")
);
expect(metaStat.isFile()).toBe(true);
// Verify metadata content
const metaContent = await readFile(
join(tempDir, "f/test/new_script.script.yaml"),
"utf-8"
);
expect(metaContent).toContain("My new script");
});
});
test("creates Python script files", async () => {
await withTestBackend(async (backend, tempDir) => {
await setupWorkspaceProfile(backend);
await writeFile(
join(tempDir, "wmill.yaml"),
`defaultTs: bun\nincludes:\n - "**"\nexcludes: []\n`,
"utf-8"
);
await mkdir(join(tempDir, "f", "test"), { recursive: true });
const result = await backend.runCLICommand(
["script", "bootstrap", "f/test/py_script", "python3"],
tempDir
);
expect(result.code).toEqual(0);
const codeStat = await stat(join(tempDir, "f/test/py_script.py"));
expect(codeStat.isFile()).toBe(true);
const metaStat = await stat(
join(tempDir, "f/test/py_script.script.yaml")
);
expect(metaStat.isFile()).toBe(true);
});
});
test("creates Bash script files", async () => {
await withTestBackend(async (backend, tempDir) => {
await setupWorkspaceProfile(backend);
await writeFile(
join(tempDir, "wmill.yaml"),
`defaultTs: bun\nincludes:\n - "**"\nexcludes: []\n`,
"utf-8"
);
await mkdir(join(tempDir, "f", "test"), { recursive: true });
const result = await backend.runCLICommand(
["script", "bootstrap", "f/test/bash_script", "bash"],
tempDir
);
expect(result.code).toEqual(0);
const codeStat = await stat(join(tempDir, "f/test/bash_script.sh"));
expect(codeStat.isFile()).toBe(true);
});
});
test("creates Go script files", async () => {
await withTestBackend(async (backend, tempDir) => {
await setupWorkspaceProfile(backend);
await writeFile(
join(tempDir, "wmill.yaml"),
`defaultTs: bun\nincludes:\n - "**"\nexcludes: []\n`,
"utf-8"
);
await mkdir(join(tempDir, "f", "test"), { recursive: true });
const result = await backend.runCLICommand(
["script", "bootstrap", "f/test/go_script", "go"],
tempDir
);
expect(result.code).toEqual(0);
const codeStat = await stat(join(tempDir, "f/test/go_script.go"));
expect(codeStat.isFile()).toBe(true);
});
});
});
// =============================================================================
// User List, Add, Remove
// =============================================================================
describe("user commands", () => {
test("list shows existing admin user", async () => {
await withTestBackend(async (backend, tempDir) => {
await setupWorkspaceProfile(backend);
const result = await backend.runCLICommand(["user"], tempDir);
expect(result.code).toEqual(0);
// The admin user is always created by the test backend
expect(result.stdout).toContain("admin@windmill.dev");
// Table headers
expect(result.stdout).toContain("email");
});
});
test.skipIf(shouldSkipOnCI())("add creates a new user and remove deletes it", async () => {
await withTestBackend(async (backend, tempDir) => {
await setupWorkspaceProfile(backend);
const uniqueId = Date.now();
const email = `testuser_${uniqueId}@example.com`;
const password = "testpass123";
// Add user
const addResult = await backend.runCLICommand(
["user", "add", email, password],
tempDir
);
expect(addResult.code).toEqual(0);
// Verify the user appears in the list
const listResult = await backend.runCLICommand(["user"], tempDir);
expect(listResult.code).toEqual(0);
expect(listResult.stdout).toContain(email);
// Remove user
const removeResult = await backend.runCLICommand(
["user", "remove", email],
tempDir
);
expect(removeResult.code).toEqual(0);
// Verify the user no longer appears
const listAfterResult = await backend.runCLICommand(["user"], tempDir);
expect(listAfterResult.code).toEqual(0);
expect(listAfterResult.stdout).not.toContain(email);
});
});
test.skipIf(shouldSkipOnCI())("add with --superadmin flag creates superadmin user", async () => {
await withTestBackend(async (backend, tempDir) => {
await setupWorkspaceProfile(backend);
const uniqueId = Date.now();
const email = `superuser_${uniqueId}@example.com`;
const password = "superpass123";
// Add superadmin user
const addResult = await backend.runCLICommand(
["user", "add", email, password, "--superadmin"],
tempDir
);
expect(addResult.code).toEqual(0);
// Verify user exists and is superadmin
const listResult = await backend.runCLICommand(["user"], tempDir);
expect(listResult.code).toEqual(0);
expect(listResult.stdout).toContain(email);
// Clean up
await backend.runCLICommand(["user", "remove", email], tempDir);
});
});
});
+36 -51
View File
@@ -1,4 +1,5 @@
import { assertEquals, assertStringIncludes, assert } from "https://deno.land/std@0.224.0/assert/mod.ts";
import { expect, test } from "bun:test";
import { writeFile } from "node:fs/promises";
import { readConfigFile, getEffectiveSettings } from "../src/core/conf.ts";
import { withTestBackend } from "./test_backend.ts";
import { addWorkspace } from "../workspace.ts";
@@ -26,17 +27,13 @@ async function setupWorkspaceProfile(backend: any): Promise<void> {
// INTEGRATION TESTS WITH REAL BACKEND
// =============================================================================
Deno.test({
name: "Integration: wmill.yaml configuration produces expected results",
sanitizeResources: false,
sanitizeOps: false,
fn: async () => {
test("Integration: wmill.yaml configuration produces expected results", async () => {
await withTestBackend(async (backend, tempDir) => {
// Set up workspace profile with name "localhost_test"
await setupWorkspaceProfile(backend);
// Create wmill.yaml with settings
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
includes:
- f/**
- settings.yaml
@@ -46,7 +43,7 @@ skipVariables: true
skipResources: true
includeSettings: true
includeSchedules: true
includeTriggers: true`);
includeTriggers: true`, "utf-8");
// Test pull with wmill.yaml configuration
const yamlResult = await backend.runCLICommand(['sync', 'pull', '--dry-run', '--json-output'], tempDir);
@@ -56,7 +53,7 @@ includeTriggers: true`);
console.log("Stdout:", yamlResult.stdout);
console.log("Stderr:", yamlResult.stderr);
}
assertEquals(yamlResult.code, 0);
expect(yamlResult.code).toEqual(0);
// Extract JSON from CLI output (skip log messages)
const yamlData = parseJsonFromCLIOutput(yamlResult.stdout);
@@ -65,7 +62,7 @@ includeTriggers: true`);
const hasSettings = (yamlData.changes || []).some((change: any) =>
change.type === 'added' && change.path === 'settings.yaml'
);
assertEquals(hasSettings, true);
expect(hasSettings).toEqual(true);
// Should NOT include resources or variables (due to skip flags)
const hasResources = (yamlData.changes || []).some((change: any) =>
@@ -74,72 +71,64 @@ includeTriggers: true`);
const hasVariables = (yamlData.changes || []).some((change: any) =>
change.type === 'added' && change.path?.includes('.variable.yaml')
);
assertEquals(hasResources, false);
assertEquals(hasVariables, false);
expect(hasResources).toEqual(false);
expect(hasVariables).toEqual(false);
});
}});
});
Deno.test({
name: "Integration: settings.yaml inclusion respects includeSettings flag",
sanitizeResources: false,
sanitizeOps: false,
fn: async () => {
test("Integration: settings.yaml inclusion respects includeSettings flag", async () => {
await withTestBackend(async (backend, tempDir) => {
// Set up workspace profile with name "localhost_test"
await setupWorkspaceProfile(backend);
// Test 1: includeSettings: true should include settings.yaml
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
includes:
- "**"
includeSettings: true`);
includeSettings: true`, "utf-8");
const includeResult = await backend.runCLICommand(['sync', 'pull', '--dry-run', '--json-output'], tempDir);
assertEquals(includeResult.code, 0);
expect(includeResult.code).toEqual(0);
// Extract JSON from CLI output (skip log messages)
const includeData = parseJsonFromCLIOutput(includeResult.stdout);
const hasSettingsInclude = (includeData.changes || []).some((change: any) =>
change.type === 'added' && change.path === 'settings.yaml'
);
assertEquals(hasSettingsInclude, true);
expect(hasSettingsInclude).toEqual(true);
// Test 2: includeSettings: false should NOT include settings.yaml
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
includes:
- "**"
includeSettings: false`);
includeSettings: false`, "utf-8");
const excludeResult = await backend.runCLICommand(['sync', 'pull', '--dry-run', '--json-output'], tempDir);
assertEquals(excludeResult.code, 0);
expect(excludeResult.code).toEqual(0);
// Extract JSON from CLI output (skip log messages)
const excludeData = parseJsonFromCLIOutput(excludeResult.stdout);
const hasSettingsExclude = (excludeData.changes || []).some((change: any) =>
change.type === 'added' && change.path === 'settings.yaml'
);
assertEquals(hasSettingsExclude, false);
expect(hasSettingsExclude).toEqual(false);
});
}});
});
Deno.test({
name: "Integration: resource/variable filtering respects skip flags",
sanitizeResources: false,
sanitizeOps: false,
fn: async () => {
test("Integration: resource/variable filtering respects skip flags", async () => {
await withTestBackend(async (backend, tempDir) => {
// Set up workspace profile with name "localhost_test"
await setupWorkspaceProfile(backend);
// Test skipResources: true
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
includes:
- "**"
skipResources: true
skipVariables: false`);
skipVariables: false`, "utf-8");
const result = await backend.runCLICommand(['sync', 'pull', '--dry-run', '--json-output'], tempDir);
assertEquals(result.code, 0);
expect(result.code).toEqual(0);
// Extract JSON from CLI output (skip log messages)
const data = parseJsonFromCLIOutput(result.stdout);
@@ -148,42 +137,38 @@ skipVariables: false`);
const hasResources = (data.changes || []).some((change: any) =>
change.type === 'added' && change.path?.includes('.resource.yaml')
);
assertEquals(hasResources, false);
expect(hasResources).toEqual(false);
// Should include variables (not skipped)
const hasVariables = (data.changes || []).some((change: any) =>
change.type === 'added' && change.path?.includes('.variable.yaml')
);
assertEquals(hasVariables, true);
expect(hasVariables).toEqual(true);
});
}});
});
// =============================================================================
// CLI FLAG OVERRIDE TESTS
// Tests for CLI flags overriding configuration file settings
// =============================================================================
Deno.test({
name: "CLI skip flags override wmill.yaml configuration",
sanitizeResources: false,
sanitizeOps: false,
fn: async () => {
test("CLI skip flags override wmill.yaml configuration", async () => {
await withTestBackend(async (backend, tempDir) => {
// Set up workspace profile with name "localhost_test"
await setupWorkspaceProfile(backend);
// Create wmill.yaml that INCLUDES resources by default (skipResources: false)
await Deno.writeTextFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
includes:
- f/**
- u/**
skipResources: false
skipResourceTypes: false
includeSettings: true`);
includeSettings: true`, "utf-8");
// Test 1: Without CLI flags - should respect wmill.yaml (include resources)
const configResult = await backend.runCLICommand(['sync', 'pull', '--dry-run', '--json-output'], tempDir);
assertEquals(configResult.code, 0);
expect(configResult.code).toEqual(0);
const configData = parseJsonFromCLIOutput(configResult.stdout);
@@ -192,7 +177,7 @@ includeSettings: true`);
const hasResources = (configData.changes || []).some((change: any) =>
change.type === 'added' && change.path?.includes('.resource.yaml')
);
assertEquals(hasResources, true, "Resources should be included by wmill.yaml config");
expect(hasResources).toEqual(true);
// Test 2: With CLI --skip-resources flag - should override wmill.yaml to skip resources
const overrideResult = await backend.runCLICommand([
@@ -200,7 +185,7 @@ includeSettings: true`);
'--skip-resources', // CLI flag should override config to skip resources
'--skip-resource-types' // CLI flag should override config to skip resource types
], tempDir);
assertEquals(overrideResult.code, 0);
expect(overrideResult.code).toEqual(0);
const overrideData = parseJsonFromCLIOutput(overrideResult.stdout);
@@ -208,12 +193,12 @@ includeSettings: true`);
const hasResourcesOverride = (overrideData.changes || []).some((change: any) =>
change.type === 'added' && change.path?.includes('.resource.yaml')
);
assertEquals(hasResourcesOverride, false, "CLI --skip-resources flag should override wmill.yaml to exclude resources");
expect(hasResourcesOverride).toEqual(false);
// Should NOT include resource types (CLI flag overrides config)
const hasResourceTypesOverride = (overrideData.changes || []).some((change: any) =>
change.type === 'added' && change.path?.includes('.resource-type.yaml')
);
assertEquals(hasResourceTypesOverride, false, "CLI --skip-resource-types flag should override wmill.yaml to exclude resource types");
expect(hasResourceTypesOverride).toEqual(false);
});
}});
});
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More