mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-18 16:02:10 +00:00
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:
@@ -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:
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
@jsr:registry=https://npm.jsr.io
|
||||
@@ -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
@@ -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
@@ -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=="],
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
[test]
|
||||
preload = ["./test/setup.ts"]
|
||||
timeout = 60000
|
||||
root = "./test"
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
-83
@@ -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
@@ -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");
|
||||
},
|
||||
});
|
||||
@@ -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";
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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) ||
|
||||
|
||||
@@ -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
@@ -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(() => {
|
||||
|
||||
@@ -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]")
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
|
||||
|
||||
@@ -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...`);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
|
||||
|
||||
@@ -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}`));
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,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: [
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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}`)
|
||||
|
||||
@@ -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,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";
|
||||
|
||||
@@ -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
@@ -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
@@ -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,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,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
@@ -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"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
import { Provider } from "../../deps.ts";
|
||||
import { Provider } from "@cliffy/command/upgrade";
|
||||
|
||||
export type NpmProviderOptions = { main?: string; logger?: any } & (
|
||||
| {
|
||||
|
||||
+23
-26
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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("\n❌ Backend 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"];
|
||||
}
|
||||
|
||||
+21
-51
@@ -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,
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}});
|
||||
});
|
||||
|
||||
@@ -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
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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
@@ -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");
|
||||
});
|
||||
|
||||
@@ -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
@@ -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);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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
@@ -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
@@ -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();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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("");
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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.");
|
||||
}
|
||||
+264
-264
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
}});
|
||||
});
|
||||
|
||||
+842
-675
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
Reference in New Issue
Block a user