diff --git a/src/components/WorkflowEditor.tsx b/src/components/WorkflowEditor.tsx index e3b8368..d666a37 100644 --- a/src/components/WorkflowEditor.tsx +++ b/src/components/WorkflowEditor.tsx @@ -67,6 +67,14 @@ export function WorkflowEditor(props: WorkflowEditorProps) { 'info', ); } + // ApplicationConfig format detected but no file resolver provided — warn the user + // rather than silently converting the format. + if (config._applicationConfig && !onResolveFile) { + addToast( + 'ApplicationConfig format detected. Configure a workspace file resolver to render the full application graph from referenced sub-files.', + 'warning', + ); + } } const mapFromProp = sourceMapProp ? new Map(Object.entries(sourceMapProp)) : undefined; importFromConfig(config, mapFromProp); diff --git a/src/stores/workflowStore.ts b/src/stores/workflowStore.ts index 0bdaef9..f3674d0 100644 --- a/src/stores/workflowStore.ts +++ b/src/stores/workflowStore.ts @@ -481,6 +481,11 @@ const useWorkflowStore = create()( if (Object.keys(importedPipelines).length > 0) { config.pipelines = importedPipelines; } + // Reattach ApplicationConfig metadata so configToYaml / exportMainFileYaml + // can reconstruct the original `application:` format on export. + if (originalConfig?._applicationConfig) { + config._applicationConfig = originalConfig._applicationConfig; + } return config; }, diff --git a/src/types/workflow.ts b/src/types/workflow.ts index 8fe68ea..6db2158 100644 --- a/src/types/workflow.ts +++ b/src/types/workflow.ts @@ -10,6 +10,18 @@ export interface ModuleConfig { ui_position?: { x: number; y: number }; } +/** + * Metadata preserved when a YAML file uses the ApplicationConfig format + * (`application:` top-level key with `workflows[].file` references). + * Stored alongside the merged WorkflowConfig so the original structure can be + * reconstructed on export without converting to the flat WorkflowConfig format. + */ +export interface ApplicationConfigMeta { + name?: string; + version?: string; + workflows: Array<{ file: string }>; +} + export interface WorkflowConfig { name?: string; version?: string; @@ -23,6 +35,8 @@ export interface WorkflowConfig { infrastructure?: Record; sidecars?: unknown[]; _originalKeys?: string[]; + /** Present when the source file used the ApplicationConfig format. */ + _applicationConfig?: ApplicationConfigMeta; /** Preserves unknown top-level keys that are not part of the known schema (e.g. engine:, custom config blocks). */ _extraTopLevelKeys?: Record; } diff --git a/src/utils/index.ts b/src/utils/index.ts index b1ec8bd..4805def 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -12,6 +12,7 @@ export { exportToFiles, exportMainFileYaml, hasFileReferences, + buildApplicationConfigYaml, } from './serialization'; export { layoutNodes } from './autoLayout'; export { diff --git a/src/utils/serialization-applicationconfig.test.ts b/src/utils/serialization-applicationconfig.test.ts new file mode 100644 index 0000000..e3536cd --- /dev/null +++ b/src/utils/serialization-applicationconfig.test.ts @@ -0,0 +1,388 @@ +/** + * Tests for ApplicationConfig format recognition and round-trip preservation. + * + * The ApplicationConfig format uses a top-level `application:` key with + * `workflows[].file` references to sub-files, e.g.: + * + * application: + * name: my-service + * workflows: + * - file: base.yaml + * - file: users.yaml + * + * These tests verify that: + * 1. parseYaml / parseYamlSafe detect and preserve the ApplicationConfig metadata + * 2. configToYaml round-trips the ApplicationConfig format unchanged + * 3. resolveImports sets _applicationConfig on the returned merged config + * 4. buildMainFileContent / exportMainFileYaml emits ApplicationConfig format + * (not flat WorkflowConfig with imports:) when the original was ApplicationConfig + * 5. exportToFiles preserves ApplicationConfig for the main file + */ + +import { describe, it, expect } from 'vitest'; +import { + parseYaml, + parseYamlSafe, + configToYaml, + resolveImports, + exportToFiles, + exportMainFileYaml, + buildApplicationConfigYaml, +} from './serialization.ts'; + +// --------------------------------------------------------------------------- +// Fixtures +// --------------------------------------------------------------------------- + +const APPLICATION_CONFIG_YAML = `\ +application: + name: my-service + version: 1.0.0 + workflows: + - file: base.yaml + - file: users.yaml + - file: billing.yaml +`; + +const APPLICATION_CONFIG_NO_VERSION = `\ +application: + name: my-service + workflows: + - file: base.yaml +`; + +const BASE_YAML = `\ +modules: + - name: cache + type: nosql.redis + config: + host: localhost +`; + +const USERS_YAML = `\ +modules: + - name: user-db + type: database.postgres + config: + dsn: postgres://localhost/users + - name: user-handler + type: api.handler + config: {} +workflows: + http: + server: http-server + router: router + routes: + - method: GET + path: /users + handler: user-handler +`; + +const BILLING_YAML = `\ +modules: + - name: billing-db + type: database.postgres + config: + dsn: postgres://localhost/billing +`; + +// --------------------------------------------------------------------------- +// parseYaml — ApplicationConfig detection +// --------------------------------------------------------------------------- + +describe('parseYaml — ApplicationConfig format detection', () => { + it('detects application: key and sets _applicationConfig', () => { + const config = parseYaml(APPLICATION_CONFIG_YAML); + expect(config._applicationConfig).toBeDefined(); + expect(config._applicationConfig!.name).toBe('my-service'); + expect(config._applicationConfig!.version).toBe('1.0.0'); + expect(config._applicationConfig!.workflows).toEqual([ + { file: 'base.yaml' }, + { file: 'users.yaml' }, + { file: 'billing.yaml' }, + ]); + }); + + it('extracts name and version into top-level config fields', () => { + const config = parseYaml(APPLICATION_CONFIG_YAML); + expect(config.name).toBe('my-service'); + expect(config.version).toBe('1.0.0'); + }); + + it('handles ApplicationConfig without version', () => { + const config = parseYaml(APPLICATION_CONFIG_NO_VERSION); + expect(config._applicationConfig).toBeDefined(); + expect(config._applicationConfig!.name).toBe('my-service'); + expect(config._applicationConfig!.version).toBeUndefined(); + }); + + it('sets imports from file references so hasFileReferences returns true', () => { + const config = parseYaml(APPLICATION_CONFIG_YAML); + expect(config.imports).toEqual(['base.yaml', 'users.yaml', 'billing.yaml']); + }); + + it('produces empty modules/workflows/triggers — content comes from sub-files', () => { + const config = parseYaml(APPLICATION_CONFIG_YAML); + expect(config.modules).toHaveLength(0); + expect(config.workflows).toEqual({}); + expect(config.triggers).toEqual({}); + }); + + it('does NOT detect flat WorkflowConfig as ApplicationConfig', () => { + const flat = `modules:\n - name: foo\n type: http.server\nworkflows: {}\ntriggers: {}\n`; + const config = parseYaml(flat); + expect(config._applicationConfig).toBeUndefined(); + }); +}); + +// --------------------------------------------------------------------------- +// parseYamlSafe — ApplicationConfig detection +// --------------------------------------------------------------------------- + +describe('parseYamlSafe — ApplicationConfig format detection', () => { + it('detects application: key and sets _applicationConfig', () => { + const { config, error } = parseYamlSafe(APPLICATION_CONFIG_YAML); + expect(error).toBeUndefined(); + expect(config._applicationConfig).toBeDefined(); + expect(config._applicationConfig!.name).toBe('my-service'); + expect(config._applicationConfig!.workflows).toHaveLength(3); + }); + + it('extracts name/version into top-level config fields', () => { + const { config } = parseYamlSafe(APPLICATION_CONFIG_YAML); + expect(config.name).toBe('my-service'); + expect(config.version).toBe('1.0.0'); + }); + + it('sets imports from file references', () => { + const { config } = parseYamlSafe(APPLICATION_CONFIG_YAML); + expect(config.imports).toEqual(['base.yaml', 'users.yaml', 'billing.yaml']); + }); +}); + +// --------------------------------------------------------------------------- +// buildApplicationConfigYaml +// --------------------------------------------------------------------------- + +describe('buildApplicationConfigYaml', () => { + it('serialises ApplicationConfigMeta to application: format', () => { + const yaml = buildApplicationConfigYaml({ + name: 'my-service', + version: '1.0.0', + workflows: [{ file: 'base.yaml' }, { file: 'users.yaml' }], + }); + expect(yaml).toContain('application:'); + expect(yaml).toContain('name: my-service'); + expect(yaml).toContain('version: 1.0.0'); + expect(yaml).toContain('- file: base.yaml'); + expect(yaml).toContain('- file: users.yaml'); + expect(yaml).not.toContain('imports:'); + expect(yaml).not.toContain('modules:'); + }); + + it('omits version when not set', () => { + const yaml = buildApplicationConfigYaml({ + name: 'my-service', + workflows: [{ file: 'base.yaml' }], + }); + expect(yaml).toContain('name: my-service'); + expect(yaml).not.toContain('version:'); + }); +}); + +// --------------------------------------------------------------------------- +// configToYaml — ApplicationConfig round-trip +// --------------------------------------------------------------------------- + +describe('configToYaml — ApplicationConfig round-trip', () => { + it('emits application: format when _applicationConfig is set and config is metadata-only', () => { + const config = parseYaml(APPLICATION_CONFIG_YAML); + const output = configToYaml(config); + expect(output).toContain('application:'); + expect(output).toContain('name: my-service'); + expect(output).toContain('workflows:'); + expect(output).toContain('- file: base.yaml'); + expect(output).toContain('- file: users.yaml'); + expect(output).toContain('- file: billing.yaml'); + // Must NOT convert to flat format + expect(output).not.toContain('imports:'); + expect(output).not.toContain('modules:'); + }); + + it('falls back to flat WorkflowConfig when _applicationConfig is set but config has real content', () => { + // Simulate a resolved ApplicationConfig that has merged sub-file modules + const config = parseYaml(APPLICATION_CONFIG_YAML); + config.modules = [{ name: 'cache', type: 'nosql.redis' }]; + const output = configToYaml(config); + // Must NOT emit pure application: format since there is real module content + expect(output).not.toBe(buildApplicationConfigYaml(config._applicationConfig!)); + // Instead serialises the full WorkflowConfig (modules are present) + expect(output).toContain('cache'); + expect(output).toContain('modules:'); + }); + + it('round-trips the ApplicationConfig YAML with minimal whitespace changes', () => { + const config = parseYaml(APPLICATION_CONFIG_YAML); + const output = configToYaml(config); + // The round-tripped YAML must be parseable back to the same structure + const reparsed = parseYaml(output); + expect(reparsed._applicationConfig!.name).toBe('my-service'); + expect(reparsed._applicationConfig!.version).toBe('1.0.0'); + expect(reparsed._applicationConfig!.workflows).toHaveLength(3); + }); +}); + +// --------------------------------------------------------------------------- +// resolveImports — sets _applicationConfig on returned config +// --------------------------------------------------------------------------- + +describe('resolveImports — preserves _applicationConfig metadata', () => { + function makeResolver(files: Record) { + return async (path: string): Promise => files[path] ?? null; + } + + it('sets _applicationConfig on the merged config', async () => { + const resolver = makeResolver({ + 'base.yaml': BASE_YAML, + 'users.yaml': USERS_YAML, + 'billing.yaml': BILLING_YAML, + }); + const { config, error } = await resolveImports(APPLICATION_CONFIG_YAML, resolver); + expect(error).toBeUndefined(); + expect(config._applicationConfig).toBeDefined(); + expect(config._applicationConfig!.name).toBe('my-service'); + expect(config._applicationConfig!.version).toBe('1.0.0'); + expect(config._applicationConfig!.workflows).toEqual([ + { file: 'base.yaml' }, + { file: 'users.yaml' }, + { file: 'billing.yaml' }, + ]); + }); + + it('merged config still has all resolved modules from sub-files', async () => { + const resolver = makeResolver({ + 'base.yaml': BASE_YAML, + 'users.yaml': USERS_YAML, + 'billing.yaml': BILLING_YAML, + }); + const { config } = await resolveImports(APPLICATION_CONFIG_YAML, resolver); + const names = config.modules.map((m) => m.name); + expect(names).toContain('cache'); + expect(names).toContain('user-db'); + expect(names).toContain('user-handler'); + expect(names).toContain('billing-db'); + }); + + it('merged config has workflows from sub-files', async () => { + const resolver = makeResolver({ + 'base.yaml': BASE_YAML, + 'users.yaml': USERS_YAML, + 'billing.yaml': BILLING_YAML, + }); + const { config } = await resolveImports(APPLICATION_CONFIG_YAML, resolver); + expect(config.workflows).toHaveProperty('http'); + }); +}); + +// --------------------------------------------------------------------------- +// exportMainFileYaml — ApplicationConfig format preservation +// --------------------------------------------------------------------------- + +describe('exportMainFileYaml — preserves ApplicationConfig format', () => { + function makeResolver(files: Record) { + return async (path: string): Promise => files[path] ?? null; + } + + it('emits application: format for the main file, not flat imports:', async () => { + const resolver = makeResolver({ + 'base.yaml': BASE_YAML, + 'users.yaml': USERS_YAML, + 'billing.yaml': BILLING_YAML, + }); + const { config, sourceMap } = await resolveImports(APPLICATION_CONFIG_YAML, resolver); + const mainYaml = exportMainFileYaml(config, sourceMap); + + expect(mainYaml).toContain('application:'); + expect(mainYaml).toContain('- file: base.yaml'); + expect(mainYaml).toContain('- file: users.yaml'); + expect(mainYaml).toContain('- file: billing.yaml'); + // Must NOT produce flat format + expect(mainYaml).not.toContain('imports:'); + expect(mainYaml).not.toContain('modules:'); + }); + + it('does not inline sub-file content into the main ApplicationConfig file', async () => { + const resolver = makeResolver({ + 'base.yaml': BASE_YAML, + 'users.yaml': USERS_YAML, + 'billing.yaml': BILLING_YAML, + }); + const { config, sourceMap } = await resolveImports(APPLICATION_CONFIG_YAML, resolver); + const mainYaml = exportMainFileYaml(config, sourceMap); + + // Sub-file module names must not appear in the main ApplicationConfig + expect(mainYaml).not.toContain('cache'); + expect(mainYaml).not.toContain('user-db'); + expect(mainYaml).not.toContain('billing-db'); + }); +}); + +// --------------------------------------------------------------------------- +// exportToFiles — ApplicationConfig format preservation +// --------------------------------------------------------------------------- + +describe('exportToFiles — preserves ApplicationConfig for main file', () => { + function makeResolver(files: Record) { + return async (path: string): Promise => files[path] ?? null; + } + + it('main file is in application: format, sub-files contain their modules', async () => { + const resolver = makeResolver({ + 'base.yaml': BASE_YAML, + 'users.yaml': USERS_YAML, + 'billing.yaml': BILLING_YAML, + }); + const { config, sourceMap } = await resolveImports(APPLICATION_CONFIG_YAML, resolver); + const fileMap = exportToFiles(config, sourceMap); + + const mainYaml = fileMap.get(null)!; + expect(mainYaml).toContain('application:'); + expect(mainYaml).not.toContain('imports:'); + expect(mainYaml).not.toContain('modules:'); + + // Sub-files still contain their modules + expect(fileMap.get('base.yaml')).toContain('cache'); + expect(fileMap.get('users.yaml')).toContain('user-db'); + expect(fileMap.get('billing.yaml')).toContain('billing-db'); + }); + + it('preserves name and version in the emitted application: block', async () => { + const resolver = makeResolver({ + 'base.yaml': BASE_YAML, + 'users.yaml': USERS_YAML, + 'billing.yaml': BILLING_YAML, + }); + const { config, sourceMap } = await resolveImports(APPLICATION_CONFIG_YAML, resolver); + const fileMap = exportToFiles(config, sourceMap); + const mainYaml = fileMap.get(null)!; + + expect(mainYaml).toContain('name: my-service'); + expect(mainYaml).toContain('version: 1.0.0'); + }); + + it('does not write empty YAML files for sub-files that have no exported content', async () => { + // Only base.yaml has content; users.yaml and billing.yaml are missing from the resolver + const resolver = makeResolver({ + 'base.yaml': BASE_YAML, + // users.yaml and billing.yaml are intentionally unresolvable + }); + const { config, sourceMap } = await resolveImports(APPLICATION_CONFIG_YAML, resolver); + const fileMap = exportToFiles(config, sourceMap); + + // base.yaml has content and should be included + expect(fileMap.has('base.yaml')).toBe(true); + // users.yaml and billing.yaml had no resolvable content — should NOT be in the file map + expect(fileMap.has('users.yaml')).toBe(false); + expect(fileMap.has('billing.yaml')).toBe(false); + }); +}); diff --git a/src/utils/serialization-multifile.test.ts b/src/utils/serialization-multifile.test.ts index 56e8dbe..535f16c 100644 --- a/src/utils/serialization-multifile.test.ts +++ b/src/utils/serialization-multifile.test.ts @@ -351,9 +351,10 @@ describe('resolveImports — pipeline sourceMap enables correct round-trip expor const fileMap = exportToFiles(config, sourceMap); - // Main file modules list must be empty (all modules belong to imported files) + // Main file preserves the original ApplicationConfig format — it has no modules: block. const mainYaml = fileMap.get(null)!; - expect(mainYaml).toMatch(/^modules:\s*\[\]/m); + expect(mainYaml).toContain('application:'); + expect(mainYaml).not.toContain('modules:'); // Modules appear in their respective source files expect(fileMap.get('api.yaml')).toContain('http-server'); @@ -372,12 +373,14 @@ describe('resolveImports — pipeline sourceMap enables correct round-trip expor const fileMap = exportToFiles(config, sourceMap); const mainYaml = fileMap.get(null)!; - // Main file should reference imported files, not inline their content - expect(mainYaml).toContain('imports:'); + // Main file preserves ApplicationConfig format with application.workflows[].file references + expect(mainYaml).toContain('application:'); + expect(mainYaml).toContain('workflows:'); // Each imported file path must appear as a reference - const importedFiles = ['api.yaml', 'base.yaml', 'database.yaml']; - const hasAtLeastOneRef = importedFiles.some((f) => mainYaml.includes(f)); - expect(hasAtLeastOneRef).toBe(true); + const importedFiles = ['api.yaml', 'base.yaml']; + importedFiles.forEach((file) => { + expect(mainYaml).toContain(file); + }); }); }); diff --git a/src/utils/serialization.ts b/src/utils/serialization.ts index f209c4d..e04f9f4 100644 --- a/src/utils/serialization.ts +++ b/src/utils/serialization.ts @@ -12,6 +12,7 @@ import type { EventWorkflowConfig, WorkflowTab, ModuleTypeInfo, + ApplicationConfigMeta, } from '../types/workflow.ts'; import { MODULE_TYPE_MAP } from '../types/workflow.ts'; import { layoutNodes } from './autoLayout.ts'; @@ -828,7 +829,35 @@ function extractExtraTopLevelKeys(parsed: Record): Record 0 ? extra : undefined; } +/** + * Serialise an ApplicationConfigMeta back to the original `application:` format YAML. + * Used to preserve the ApplicationConfig structure on export instead of converting + * to the flat WorkflowConfig format. + */ +export function buildApplicationConfigYaml(appConfig: ApplicationConfigMeta): string { + const appBlock: Record = {}; + if (appConfig.name !== undefined) appBlock.name = appConfig.name; + if (appConfig.version !== undefined) appBlock.version = appConfig.version; + appBlock.workflows = appConfig.workflows; + return yaml.dump({ application: appBlock }, { lineWidth: -1, noRefs: true, sortKeys: false }); +} + +function isMetadataOnlyApplicationConfig(config: WorkflowConfig): boolean { + const hasModules = (config.modules?.length ?? 0) > 0; + const hasWorkflows = Object.keys(config.workflows ?? {}).length > 0; + const hasTriggers = Object.keys(config.triggers ?? {}).length > 0; + const hasPipelines = Object.keys(config.pipelines ?? {}).length > 0; + return !hasModules && !hasWorkflows && !hasTriggers && !hasPipelines; +} + export function configToYaml(config: WorkflowConfig): string { + // Preserve `application:` output only while the config is still metadata-only. + // If real main-file content exists, serialise the full WorkflowConfig instead + // so top-level modules/workflows/triggers/pipelines are not dropped. + if (config._applicationConfig && isMetadataOnlyApplicationConfig(config)) { + return buildApplicationConfigYaml(config._applicationConfig); + } + // Strip internal tracking fields and omit empty top-level arrays/objects // that were not present in the original YAML const originalKeys = config._originalKeys; @@ -901,6 +930,34 @@ export function parseYaml(text: string): WorkflowConfig { return { modules: [], workflows: {}, triggers: {}, _originalKeys: [] }; } const _originalKeys = Object.keys(parsed); + + // Detect ApplicationConfig format: top-level `application:` key with `workflows[].file` refs + const application = parsed.application as Record | undefined; + if (application && typeof application === 'object') { + const appWorkflows = application.workflows as Array> | undefined; + if (Array.isArray(appWorkflows) && appWorkflows.some((w) => typeof w.file === 'string')) { + const fileRefs = appWorkflows + .filter((w) => typeof w.file === 'string') + .map((w) => ({ file: w.file as string })); + const _applicationConfig = { + name: application.name as string | undefined, + version: application.version !== undefined ? String(application.version) : undefined, + workflows: fileRefs, + }; + const config: WorkflowConfig = { + modules: (parsed.modules ?? []) as ModuleConfig[], + workflows: (parsed.workflows ?? {}) as Record, + triggers: (parsed.triggers ?? {}) as Record, + imports: fileRefs.map((r) => r.file), + _originalKeys, + _applicationConfig, + }; + if (_applicationConfig.name) config.name = _applicationConfig.name; + if (_applicationConfig.version) config.version = _applicationConfig.version; + return config; + } + } + const config: WorkflowConfig = { modules: (parsed.modules ?? []) as ModuleConfig[], workflows: (parsed.workflows ?? {}) as Record, @@ -948,6 +1005,34 @@ export function parseYamlSafe(text: string): { config: WorkflowConfig; error?: s return { config: { modules: [], workflows: {}, triggers: {}, _originalKeys: [] }, error: 'YAML parsed to non-object value' }; } const _originalKeys = Object.keys(parsed); + + // Detect ApplicationConfig format: top-level `application:` key with `workflows[].file` refs + const application = parsed.application as Record | undefined; + if (application && typeof application === 'object') { + const appWorkflows = application.workflows as Array> | undefined; + if (Array.isArray(appWorkflows) && appWorkflows.some((w) => typeof w.file === 'string')) { + const fileRefs = appWorkflows + .filter((w) => typeof w.file === 'string') + .map((w) => ({ file: w.file as string })); + const _applicationConfig = { + name: application.name as string | undefined, + version: application.version !== undefined ? String(application.version) : undefined, + workflows: fileRefs, + }; + const config: WorkflowConfig = { + modules: (parsed.modules ?? []) as ModuleConfig[], + workflows: (parsed.workflows ?? {}) as Record, + triggers: (parsed.triggers ?? {}) as Record, + imports: fileRefs.map((r) => r.file), + _originalKeys, + _applicationConfig, + }; + if (_applicationConfig.name) config.name = _applicationConfig.name; + if (_applicationConfig.version) config.version = _applicationConfig.version; + return { config }; + } + } + const config: WorkflowConfig = { modules: (parsed.modules ?? []) as ModuleConfig[], workflows: (parsed.workflows ?? {}) as Record, @@ -1245,9 +1330,20 @@ export async function resolveImports( // Handle `application.workflows[].file:` directive — conflicts are reported as errors const application = parsed.application as Record | undefined; + let applicationConfig: ApplicationConfigMeta | undefined; if (application && typeof application === 'object') { const appWorkflows = (application.workflows ?? []) as Array>; if (Array.isArray(appWorkflows)) { + const fileRefs = appWorkflows + .filter((entry) => typeof entry.file === 'string') + .map((entry) => ({ file: entry.file as string })); + if (fileRefs.length > 0) { + applicationConfig = { + name: application.name as string | undefined, + version: application.version !== undefined ? String(application.version) : undefined, + workflows: fileRefs, + }; + } for (const entry of appWorkflows) { const filePath = entry.file as string | undefined; if (!filePath) continue; @@ -1277,6 +1373,12 @@ export async function resolveImports( config._extraTopLevelKeys = extraTopLevelKeys; } + // Preserve ApplicationConfig structure so round-trip export can reconstruct + // the original `application:` format instead of converting to flat imports:. + if (applicationConfig) { + config._applicationConfig = applicationConfig; + } + return { config, sourceMap, @@ -1400,6 +1502,10 @@ function buildMainFileContent( const mainOnlyConfig: WorkflowConfig = { ...config, modules: mainModules, + // For ApplicationConfig format, all workflows and triggers live exclusively in sub-files. + // Clear them from the main-file view so isMetadataOnlyApplicationConfig can correctly + // identify the main file as metadata-only and emit application: format. + ...(config._applicationConfig ? { workflows: {}, triggers: {} } : {}), // Override imports with the computed list of imported file paths (omit the property // entirely when there are no sub-files so configToYaml does not emit `imports: []`) ...(importedFiles.length > 0 ? { imports: importedFiles } : { imports: undefined }),