feat: add fileset resource type support

Add a new "fileset" resource type that represents a collection of files
stored as a relpath→content map. This enables resource types to manage
multiple files (e.g., config directories, template sets) instead of just
a single file.

Backend:
- Add is_fileset column to resource_type table
- Update CRUD operations and workspace duplication to handle is_fileset
- Add integration tests for fileset resource types

Frontend:
- Add FilesetEditor component with file explorer + Monaco editor
- Extract shared FileExplorer component from RawAppSidebar (dedup)
- Add fileset toggle to EditableSchemaWrapper
- Show fileset editor in ResourceEditor and ApiConnectForm
- Show folder icon for fileset resource types in IconedResourceType

CLI:
- Support fileset resources in sync pull (expand to .fileset/ directory)
- Support fileset resources in sync push (reconstruct from directory)
- Handle !inline_fileset YAML tag in resource resolution

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-02-23 17:50:37 +00:00
co-authored by Claude Opus 4.6
parent 710809ac64
commit d4dc0a088f
575 changed files with 1159 additions and 14866 deletions
+87 -96
View File
@@ -6,16 +6,11 @@
"": {
"name": "wmill-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",
"@windmill-labs/shared-utils": "^1.0.12",
"diff": "^5.2.0",
"esbuild": "0.24.2",
"get-port": "7.1.0",
@@ -23,6 +18,7 @@
"minimatch": "^10.0.0",
"open": "^10.0.0",
"svelte": "^5.45.2",
"tar-stream": "^3.1.7",
"windmill-parser-wasm-csharp": "*",
"windmill-parser-wasm-go": "*",
"windmill-parser-wasm-java": "*",
@@ -44,25 +40,11 @@
"devDependencies": {
"@types/diff": "^5.2.3",
"@types/node": "^22.0.0",
"@types/tar-stream": "^3.1.4",
"@types/ws": "^8.5.0",
"typescript": "^5.7.0"
}
},
"node_modules/@ayonli/jsext": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@ayonli/jsext/-/jsext-1.9.0.tgz",
"integrity": "sha512-hIu6lQhoLr5e26lmt+vzopuZffaAyb623r4+8HlN/rhXgm2ywHslzk7UHiATdfDbfPjBARkB6cfXjVEi3aav6g==",
"license": "MIT",
"dependencies": {
"iconv-lite": "^0.6.3",
"sudo-prompt": "^9.2.1",
"ws": "^8.17.0",
"zod": "^3.23.8"
},
"engines": {
"node": ">=14.18"
}
},
"node_modules/@cliffy/ansi": {
"name": "@jsr/cliffy__ansi",
"version": "1.0.0",
@@ -623,15 +605,6 @@
"resolved": "https://npm.jsr.io/~/11/@jsr/std__fmt/1.0.9.tgz",
"integrity": "sha512-YFJJMozmORj2K91c5J9opWeh0VUwrd+Mwb7Pr0FkVCAKVLu2UhT4LyvJqWiyUT+eF+MdfqQ9F7RtQj4bXn9Smw=="
},
"node_modules/@jsr/std__fs": {
"version": "1.0.23",
"resolved": "https://npm.jsr.io/~/11/@jsr/std__fs/1.0.23.tgz",
"integrity": "sha512-e8jspB3M44E5YhWiLCTqibBBTwVmxQaHN06WvFa/elAKm5E/LfAe8Hj5XGNC8P7a0MIPASlNJsnF1bgO/g+aqg==",
"dependencies": {
"@jsr/std__internal": "^1.0.12",
"@jsr/std__path": "^1.1.4"
}
},
"node_modules/@jsr/std__internal": {
"version": "1.0.12",
"resolved": "https://npm.jsr.io/~/11/@jsr/std__internal/1.0.12.tgz",
@@ -671,38 +644,6 @@
"@jsr/std__regexp": "^1.0.1"
}
},
"node_modules/@std/encoding": {
"name": "@jsr/std__encoding",
"version": "1.0.10",
"resolved": "https://npm.jsr.io/~/11/@jsr/std__encoding/1.0.10.tgz",
"integrity": "sha512-WK2njnDTyKefroRNk2Ooq7GStp6Y0ccAvr4To+Z/zecRAGe7+OSvH9DbiaHpAKwEi2KQbmpWMOYsdNt+TsdmSw=="
},
"node_modules/@std/log": {
"name": "@jsr/std__log",
"version": "0.224.14",
"resolved": "https://npm.jsr.io/~/11/@jsr/std__log/0.224.14.tgz",
"integrity": "sha512-EHT7E0plakyzk/gxMrwqUf3YGCCxN3Is25QrEh7toYA7qwj46R4qY7cIaDEKy8QqI5JHOFHwWXOClcPK6goIoQ==",
"dependencies": {
"@jsr/std__fmt": "^1.0.5",
"@jsr/std__fs": "^1.0.11",
"@jsr/std__io": "^0.225.2"
}
},
"node_modules/@std/path": {
"name": "@jsr/std__path",
"version": "1.1.4",
"resolved": "https://npm.jsr.io/~/11/@jsr/std__path/1.1.4.tgz",
"integrity": "sha512-SK4u9H6NVTfolhPdlvdYXfNFefy1W04AEHWJydryYbk+xqzNiVmr5o7TLJLJFqwHXuwMRhwrn+mcYeUfS0YFaA==",
"dependencies": {
"@jsr/std__internal": "^1.0.12"
}
},
"node_modules/@std/yaml": {
"name": "@jsr/std__yaml",
"version": "1.0.10",
"resolved": "https://npm.jsr.io/~/11/@jsr/std__yaml/1.0.10.tgz",
"integrity": "sha512-1WIM023Kvi48pvPE3UO5YcieambLgywUooLhAkkaObIcMB77F/YP2ILdl+vNfik+vElkl9znmuST9AZo8mbCpA=="
},
"node_modules/@stoplight/ordered-object-literal": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@stoplight/ordered-object-literal/-/ordered-object-literal-1.0.5.tgz",
@@ -784,6 +725,16 @@
"undici-types": "~6.21.0"
}
},
"node_modules/@types/tar-stream": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/@types/tar-stream/-/tar-stream-3.1.4.tgz",
"integrity": "sha512-921gW0+g29mCJX0fRvqeHzBlE/XclDaAG0Ousy1LCghsOhvaKacDeRGEVzQP9IPfKn8Vysy7FEXAIxycpc/CMg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
@@ -852,6 +803,20 @@
"node": ">= 0.4"
}
},
"node_modules/b4a": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz",
"integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==",
"license": "Apache-2.0",
"peerDependencies": {
"react-native-b4a": "*"
},
"peerDependenciesMeta": {
"react-native-b4a": {
"optional": true
}
}
},
"node_modules/balanced-match": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.3.tgz",
@@ -861,6 +826,20 @@
"node": "20 || >=22"
}
},
"node_modules/bare-events": {
"version": "2.8.2",
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
"integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
"license": "Apache-2.0",
"peerDependencies": {
"bare-abort-controller": "*"
},
"peerDependenciesMeta": {
"bare-abort-controller": {
"optional": true
}
}
},
"node_modules/brace-expansion": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz",
@@ -1013,12 +992,27 @@
"@jridgewell/sourcemap-codec": "^1.4.15"
}
},
"node_modules/events-universal": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
"integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
"license": "Apache-2.0",
"dependencies": {
"bare-events": "^2.7.0"
}
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"license": "MIT"
},
"node_modules/fast-fifo": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
"integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
"license": "MIT"
},
"node_modules/fast-uri": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
@@ -1047,18 +1041,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
@@ -1263,12 +1245,6 @@
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
"node_modules/set-immediate-shim": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz",
@@ -1278,6 +1254,17 @@
"node": ">=0.10.0"
}
},
"node_modules/streamx": {
"version": "2.23.0",
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz",
"integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==",
"license": "MIT",
"dependencies": {
"events-universal": "^1.0.0",
"fast-fifo": "^1.3.2",
"text-decoder": "^1.1.0"
}
},
"node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
@@ -1287,13 +1274,6 @@
"safe-buffer": "~5.1.0"
}
},
"node_modules/sudo-prompt": {
"version": "9.2.1",
"resolved": "https://registry.npmjs.org/sudo-prompt/-/sudo-prompt-9.2.1.tgz",
"integrity": "sha512-Mu7R0g4ig9TUuGSxJavny5Rv0egCEtpZRNMrZaYS1vxkiIxGiGUwoezU3LazIQ+KE04hTrTfNPgxU5gzi7F5Pw==",
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
"license": "MIT"
},
"node_modules/svelte": {
"version": "5.53.2",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.53.2.tgz",
@@ -1321,6 +1301,26 @@
"node": ">=18"
}
},
"node_modules/tar-stream": {
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz",
"integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==",
"license": "MIT",
"dependencies": {
"b4a": "^1.6.4",
"fast-fifo": "^1.2.0",
"streamx": "^2.15.0"
}
},
"node_modules/text-decoder": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz",
"integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==",
"license": "Apache-2.0",
"dependencies": {
"b4a": "^1.6.4"
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
@@ -1484,15 +1484,6 @@
"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz",
"integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==",
"license": "MIT"
},
"node_modules/zod": {
"version": "3.25.76",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
}
}
}
+24 -2
View File
@@ -1,5 +1,6 @@
import { stat, writeFile } from "node:fs/promises";
import { stat, writeFile, readdir, readFile } from "node:fs/promises";
import { stringify as yamlStringify } from "yaml";
import nodePath from "node:path";
import {
GlobalOptions,
@@ -27,6 +28,24 @@ export interface ResourceFile {
is_oauth?: boolean; // deprecated
}
async function readFilesetDirectory(dirPath: string): Promise<Record<string, string>> {
const result: Record<string, string> = {};
async function walk(currentPath: string, prefix: string) {
const entries = await readdir(currentPath, { withFileTypes: true });
for (const entry of entries) {
const entryPath = nodePath.join(currentPath, entry.name);
const relPath = prefix ? prefix + "/" + entry.name : entry.name;
if (entry.isDirectory()) {
await walk(entryPath, relPath);
} else if (entry.isFile()) {
result[relPath] = await readFile(entryPath, "utf-8");
}
}
}
await walk(dirPath, "");
return result;
}
export async function pushResource(
workspace: string,
remotePath: string,
@@ -46,7 +65,10 @@ export async function pushResource(
// Helper function to resolve inline content
const resolveInlineContent = async () => {
if (localResource.value["content"]?.startsWith("!inline ")) {
if (typeof localResource.value === "string" && localResource.value.startsWith("!inline_fileset ")) {
const dirPath = localResource.value.split(" ")[1];
localResource.value = await readFilesetDirectory(dirPath.replaceAll("/", SEP));
} else if (localResource.value["content"]?.startsWith("!inline ")) {
const basePath = localResource.value["content"].split(" ")[1];
// If we're processing a branch-specific metadata file, read from branch-specific resource file
+132 -10
View File
@@ -38,6 +38,7 @@ import {
deepEqual,
fetchRemoteVersion,
isFileResource,
isFilesetResource,
isRawAppFile,
isWorkspaceDependencies,
} from "../../utils/utils.ts";
@@ -484,11 +485,53 @@ export function extractInlineScriptsForApps(
return [];
}
type FileResourceTypeInfo = { format_extension: string | null; is_fileset: boolean };
function parseFileResourceTypeMap(
raw: Record<string, string | FileResourceTypeInfo>,
): { formatExtMap: Record<string, string>; filesetMap: Record<string, boolean> } {
const formatExtMap: Record<string, string> = {};
const filesetMap: Record<string, boolean> = {};
for (const [k, v] of Object.entries(raw)) {
if (typeof v === "string") {
formatExtMap[k] = v;
filesetMap[k] = false;
} else {
if (v.format_extension) {
formatExtMap[k] = v.format_extension;
}
filesetMap[k] = v.is_fileset ?? false;
}
}
return { formatExtMap, filesetMap };
}
async function findFilesetResourceFile(changePath: string): Promise<string> {
// Extract the base path before .fileset/
const filesetIdx = changePath.indexOf(".fileset" + SEP);
if (filesetIdx === -1) {
throw new Error(`Not a fileset resource path: ${changePath}`);
}
const basePath = changePath.substring(0, filesetIdx);
const candidates = [basePath + ".resource.json", basePath + ".resource.yaml"];
for (const candidate of candidates) {
try {
const s = await stat(candidate);
if (s.isFile()) return candidate;
} catch {
// not found, try next
}
}
throw new Error(`No resource metadata file found for fileset resource: ${changePath}`);
}
function ZipFSElement(
zip: JSZip,
useYaml: boolean,
defaultTs: "bun" | "deno",
resourceTypeToFormatExtension: Record<string, string>,
resourceTypeToIsFileset: Record<string, boolean>,
ignoreCodebaseChanges: boolean,
): DynFSElement {
async function _internal_file(
@@ -860,10 +903,17 @@ function ZipFSElement(
log.error(`Failed to parse resource.yaml at path: ${p}`);
throw error;
}
const resourceType = parsed["resource_type"];
const formatExtension =
resourceTypeToFormatExtension[parsed["resource_type"]];
resourceTypeToFormatExtension[resourceType];
const isFileset = resourceTypeToIsFileset[resourceType] ?? false;
if (formatExtension) {
if (isFileset) {
parsed["value"] =
"!inline_fileset " +
removeSuffix(p.replaceAll(SEP, "/"), ".resource.json") +
".fileset";
} else if (formatExtension) {
parsed["value"]["content"] =
"!inline " +
removeSuffix(p.replaceAll(SEP, "/"), ".resource.json") +
@@ -918,10 +968,37 @@ function ZipFSElement(
log.error(`Failed to parse resource file content at path: ${p}`);
throw error;
}
const resourceType = parsed["resource_type"];
const formatExtension =
resourceTypeToFormatExtension[parsed["resource_type"]];
resourceTypeToFormatExtension[resourceType];
const isFileset = resourceTypeToIsFileset[resourceType] ?? false;
if (formatExtension) {
if (isFileset && typeof parsed["value"] === "object" && parsed["value"] !== null) {
const filesetBasePath =
removeSuffix(finalPath, ".resource.json") + ".fileset";
// Push directory entry for the fileset
r.push({
isDirectory: true,
path: filesetBasePath,
async *getChildren() {
for (const [relPath, fileContent] of Object.entries(parsed["value"])) {
if (typeof fileContent === "string") {
yield {
isDirectory: false,
path: path.join(filesetBasePath, relPath),
async *getChildren() {},
async getContentText() {
return fileContent;
},
};
}
}
},
async getContentText() {
throw new Error("Cannot get content of directory");
},
});
} else if (formatExtension) {
const fileContent: string = parsed["value"]["content"];
if (typeof fileContent === "string") {
r.push({
@@ -1058,6 +1135,7 @@ export async function elementsToMap(
const path = entry.path;
if (
!isFileResource(path) &&
!isFilesetResource(path) &&
!isRawAppFile(path) &&
!isWorkspaceDependencies(path)
) {
@@ -1103,7 +1181,7 @@ export async function elementsToMap(
}
}
if (skips.skipResources && isFileResource(path)) continue;
if (skips.skipResources && (isFileResource(path) || isFilesetResource(path))) continue;
const ext = json ? ".json" : ".yaml";
if (!skips.includeSchedules && path.endsWith(".schedule" + ext)) continue;
@@ -1715,10 +1793,14 @@ export async function pull(
);
let resourceTypeToFormatExtension: Record<string, string> = {};
let resourceTypeToIsFileset: Record<string, boolean> = {};
try {
resourceTypeToFormatExtension = (await wmill.fileResourceTypeToFileExtMap({
const raw = (await wmill.fileResourceTypeToFileExtMap({
workspace: workspace.workspaceId,
})) as Record<string, string>;
})) as Record<string, string | FileResourceTypeInfo>;
const parsed = parseFileResourceTypeMap(raw);
resourceTypeToFormatExtension = parsed.formatExtMap;
resourceTypeToIsFileset = parsed.filesetMap;
} catch {
// ignore
}
@@ -1745,6 +1827,7 @@ export async function pull(
!opts.json,
opts.defaultTs ?? "bun",
resourceTypeToFormatExtension,
resourceTypeToIsFileset,
true,
);
@@ -2241,10 +2324,14 @@ export async function push(
),
);
let resourceTypeToFormatExtension: Record<string, string> = {};
let resourceTypeToIsFileset: Record<string, boolean> = {};
try {
resourceTypeToFormatExtension = (await wmill.fileResourceTypeToFileExtMap({
const raw = (await wmill.fileResourceTypeToFileExtMap({
workspace: workspace.workspaceId,
})) as Record<string, string>;
})) as Record<string, string | FileResourceTypeInfo>;
const parsed = parseFileResourceTypeMap(raw);
resourceTypeToFormatExtension = parsed.formatExtMap;
resourceTypeToIsFileset = parsed.filesetMap;
} catch {
// ignore
}
@@ -2269,6 +2356,7 @@ export async function push(
!opts.json,
opts.defaultTs ?? "bun",
resourceTypeToFormatExtension,
resourceTypeToIsFileset,
false,
);
@@ -2587,6 +2675,39 @@ export async function push(
continue;
}
}
if (isFilesetResource(change.path)) {
const resourceFilePath = await findFilesetResourceFile(change.path);
if (!alreadySynced.includes(resourceFilePath)) {
alreadySynced.push(resourceFilePath);
const newObj = parseFromPath(
resourceFilePath,
await readFile(resourceFilePath, "utf-8"),
);
let serverPath = resourceFilePath;
const currentBranch = cachedBranchForPush;
if (currentBranch && isBranchSpecificFile(resourceFilePath)) {
serverPath = fromBranchSpecificPath(
resourceFilePath,
currentBranch,
);
}
await pushResource(
workspace.workspaceId,
serverPath,
undefined,
newObj,
resourceFilePath,
);
if (stateTarget) {
await writeFile(stateTarget, change.after, "utf-8");
}
continue;
}
}
const oldObj = parseFromPath(change.path, change.before);
const newObj = parseFromPath(change.path, change.after);
@@ -2619,7 +2740,8 @@ export async function push(
change.path.endsWith(".script.json") ||
change.path.endsWith(".script.yaml") ||
change.path.endsWith(".lock") ||
isFileResource(change.path)
isFileResource(change.path) ||
isFilesetResource(change.path)
) {
continue;
} else if (
+10 -2
View File
@@ -1,6 +1,6 @@
import { minimatch } from "minimatch";
import { getCurrentGitBranch, isGitRepository } from "../utils/git.ts";
import { isFileResource } from "../utils/utils.ts";
import { isFileResource, isFilesetResource } from "../utils/utils.ts";
import { SyncOptions } from "./conf.ts";
import { TRIGGER_TYPES } from "../types.ts";
@@ -165,7 +165,7 @@ export function isItemTypeConfigured(path: string, specificItems: SpecificItemsC
return specificItems.settings !== undefined;
}
if (isFileResource(path)) {
if (isFileResource(path) || isFilesetResource(path)) {
return specificItems.resources !== undefined;
}
@@ -219,6 +219,14 @@ export function isSpecificItem(path: string, specificItems: SpecificItemsConfig
}
}
if (isFilesetResource(path)) {
const basePathMatch = path.match(/^(.+?)\.fileset[/\\]/);
if (basePathMatch && specificItems.resources) {
const basePath = basePathMatch[1] + '.resource.yaml';
return matchesPatterns(basePath, specificItems.resources);
}
}
return false;
}
+2 -2
View File
@@ -14,7 +14,7 @@ import { pushResourceType } from "./commands/resource-type/resource-type.ts";
import { pushVariable } from "./commands/variable/variable.ts";
import { yamlOptions } from "./commands/sync/sync.ts";
import { showDiffs } from "./core/conf.ts";
import { deepEqual, isFileResource, isWorkspaceDependencies } from "./utils/utils.ts";
import { deepEqual, isFileResource, isFilesetResource, isWorkspaceDependencies } from "./utils/utils.ts";
import { pushSchedule } from "./commands/schedule/schedule.ts";
import { pushWorkspaceUser } from "./commands/user/user.ts";
import { pushGroup } from "./commands/user/user.ts";
@@ -333,7 +333,7 @@ export function getTypeStrFromPath(
) {
return typeEnding;
} else {
if (isFileResource(p)) {
if (isFileResource(p) || isFilesetResource(p)) {
return "resource";
}
throw new Error("Could not infer type of path " + JSON.stringify(parsed));
+4
View File
@@ -154,6 +154,10 @@ export function isFileResource(path: string): boolean {
);
}
export function isFilesetResource(path: string): boolean {
return path.includes(".fileset/") || path.includes(".fileset\\");
}
export function isRawAppFile(path: string): boolean {
return isRawAppPath(path);
}
+32 -1
View File
@@ -4,7 +4,7 @@
*/
import { expect, test, describe } from "bun:test";
import { deepEqual, isFileResource, toCamel, capitalize } from "../src/utils/utils.ts";
import { deepEqual, isFileResource, isFilesetResource, toCamel, capitalize } from "../src/utils/utils.ts";
import {
getTypeStrFromPath,
removeType,
@@ -156,6 +156,32 @@ describe("isFileResource", () => {
});
});
// =============================================================================
// isFilesetResource
// =============================================================================
describe("isFilesetResource", () => {
test("detects fileset resource paths (unix separator)", () => {
expect(isFilesetResource("f/test/my_config.fileset/config.yaml")).toBe(true);
expect(isFilesetResource("u/admin/templates.fileset/path/to/file.txt")).toBe(true);
});
test("detects fileset resource paths (windows separator)", () => {
expect(isFilesetResource("f\\test\\my_config.fileset\\config.yaml")).toBe(true);
});
test("rejects non-fileset paths", () => {
expect(isFilesetResource("f/test/my_resource.resource.yaml")).toBe(false);
expect(isFilesetResource("f/test/my_file.resource.file.txt")).toBe(false);
expect(isFilesetResource("f/test/my_script.ts")).toBe(false);
});
test("rejects paths ending with .fileset (no child file)", () => {
// The directory itself is not a fileset resource file - only children are
expect(isFilesetResource("f/test/my_config.fileset")).toBe(false);
});
});
// =============================================================================
// removeType
// =============================================================================
@@ -241,6 +267,11 @@ describe("getTypeStrFromPath", () => {
expect(getTypeStrFromPath("devs.group.yaml")).toBe("group");
});
test("detects fileset resource files as resource type", () => {
expect(getTypeStrFromPath("f/test/my_config.fileset/config.yaml")).toBe("resource");
expect(getTypeStrFromPath("u/admin/templates.fileset/path/to/file.txt")).toBe("resource");
});
test("throws for unknown type", () => {
expect(() => getTypeStrFromPath("f/test/unknown.xyz.yaml")).toThrow();
});