@noego/wood 0.4.3 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +30 -0
  2. package/bin/wood.js +9 -0
  3. package/dist/client/index.cjs.map +1 -1
  4. package/dist/client/index.d.cts +1 -1
  5. package/dist/client/index.d.ts +1 -1
  6. package/dist/client/index.js.map +1 -1
  7. package/dist/codegen/type_generator.cjs +33 -9
  8. package/dist/codegen/type_generator.cjs.map +1 -1
  9. package/dist/codegen/type_generator.js +33 -9
  10. package/dist/codegen/type_generator.js.map +1 -1
  11. package/dist/dev/wood_dev_server.cjs +2 -0
  12. package/dist/dev/wood_dev_server.cjs.map +1 -1
  13. package/dist/dev/wood_dev_server.js +2 -0
  14. package/dist/dev/wood_dev_server.js.map +1 -1
  15. package/dist/dev/yaml_watcher.cjs +8 -1
  16. package/dist/dev/yaml_watcher.cjs.map +1 -1
  17. package/dist/dev/yaml_watcher.d.cts +1 -0
  18. package/dist/dev/yaml_watcher.d.ts +1 -0
  19. package/dist/dev/yaml_watcher.js +8 -1
  20. package/dist/dev/yaml_watcher.js.map +1 -1
  21. package/dist/parser/operations_parser.cjs +23 -2
  22. package/dist/parser/operations_parser.cjs.map +1 -1
  23. package/dist/parser/operations_parser.js +23 -2
  24. package/dist/parser/operations_parser.js.map +1 -1
  25. package/dist/router/wood_router.cjs +28 -4
  26. package/dist/router/wood_router.cjs.map +1 -1
  27. package/dist/router/wood_router.d.cts +6 -0
  28. package/dist/router/wood_router.d.ts +6 -0
  29. package/dist/router/wood_router.js +28 -4
  30. package/dist/router/wood_router.js.map +1 -1
  31. package/dist/runtime/runtime.cjs +2 -0
  32. package/dist/runtime/runtime.cjs.map +1 -1
  33. package/dist/runtime/runtime.js +2 -0
  34. package/dist/runtime/runtime.js.map +1 -1
  35. package/dist/validation/schema_validator.cjs +13 -0
  36. package/dist/validation/schema_validator.cjs.map +1 -1
  37. package/dist/validation/schema_validator.d.cts +6 -0
  38. package/dist/validation/schema_validator.d.ts +6 -0
  39. package/dist/validation/schema_validator.js +13 -0
  40. package/dist/validation/schema_validator.js.map +1 -1
  41. package/package.json +1 -1
  42. package/src/components/ContextMenuProvider.compile.test.ts +20 -0
  43. package/src/components/ContextMenuProvider.svelte +2 -2
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/codegen/type_generator.ts"],"sourcesContent":["import type { OperationDef } from '../types/index.js';\n\nfunction operationHasInput(op: OperationDef): boolean {\n return Boolean(\n op.inputSchema &&\n (op.inputSchema as Record<string, unknown>).type === 'object' &&\n Object.keys((op.inputSchema as Record<string, Record<string, unknown>>).properties ?? {}).length > 0,\n );\n}\n\nfunction jsonSchemaToTS(schema: Record<string, unknown> | undefined, indent: string = ''): string {\n if (!schema) return 'unknown';\n\n const type = schema.type as string | undefined;\n\n if (type === 'string') {\n if (schema.enum) {\n return (schema.enum as string[]).map((v) => `'${v}'`).join(' | ');\n }\n return 'string';\n }\n if (type === 'number' || type === 'integer') return 'number';\n if (type === 'boolean') return 'boolean';\n if (type === 'array') {\n const itemType = jsonSchemaToTS(schema.items as Record<string, unknown>, indent);\n return `Array<${itemType}>`;\n }\n if (type === 'object') {\n const props = (schema.properties ?? {}) as Record<string, Record<string, unknown>>;\n const required = new Set((schema.required as string[]) ?? []);\n const entries = Object.entries(props);\n if (entries.length === 0) return 'Record<string, unknown>';\n\n const lines = entries.map(([key, val]) => {\n const optional = required.has(key) ? '' : '?';\n const valType = jsonSchemaToTS(val, indent + ' ');\n return `${indent} ${key}${optional}: ${valType};`;\n });\n return `{\\n${lines.join('\\n')}\\n${indent}}`;\n }\n\n return 'unknown';\n}\n\nfunction buildNestedTypeTree(operations: Map<string, OperationDef>): Record<string, unknown> {\n const tree: Record<string, unknown> = {};\n for (const [channel, op] of operations) {\n const parts = channel.split('.');\n let current: Record<string, unknown> = tree;\n for (let i = 0; i < parts.length - 1; i++) {\n if (!current[parts[i]]) current[parts[i]] = {};\n current = current[parts[i]] as Record<string, unknown>;\n }\n current[parts[parts.length - 1]] = { __operation: op };\n }\n return tree;\n}\n\nfunction generateTypeTree(node: Record<string, unknown>, indent: string = ' '): string {\n const lines: string[] = [];\n for (const [key, value] of Object.entries(node)) {\n const entry = value as Record<string, unknown>;\n if (entry.__operation) {\n const op = entry.__operation as OperationDef;\n const inputType = jsonSchemaToTS(op.inputSchema as Record<string, unknown>, indent + ' ');\n const outputType = jsonSchemaToTS(op.outputSchema as Record<string, unknown>, indent + ' ');\n const hasInput = operationHasInput(op);\n\n if (hasInput) {\n lines.push(`${indent}${key}(data: ${inputType}): Promise<${outputType}>;`);\n } else {\n lines.push(`${indent}${key}(): Promise<${outputType}>;`);\n }\n } else {\n lines.push(`${indent}${key}: {`);\n lines.push(generateTypeTree(entry, indent + ' '));\n lines.push(`${indent}};`);\n }\n }\n return lines.join('\\n');\n}\n\ntype RpcActionTypeEntry = {\n channel: string;\n inputType: string;\n outputType: string;\n hasInput: boolean;\n};\n\nfunction buildRpcControllerTree(\n operations: Map<string, OperationDef>,\n): Record<string, Record<string, RpcActionTypeEntry>> {\n const tree: Record<string, Record<string, RpcActionTypeEntry>> = {};\n\n for (const operation of operations.values()) {\n const controller = operation.controller;\n const action = operation.action;\n\n if (!tree[controller]) {\n tree[controller] = {};\n }\n\n if (tree[controller][action]) {\n throw new Error(\n `Type codegen failed: duplicate RPC action \"${controller}.${action}\" found in channel \"${operation.channel}\".`,\n );\n }\n\n tree[controller][action] = {\n channel: operation.channel,\n inputType: jsonSchemaToTS(operation.inputSchema as Record<string, unknown>, ' '),\n outputType: jsonSchemaToTS(operation.outputSchema as Record<string, unknown>, ' '),\n hasInput: operationHasInput(operation),\n };\n }\n\n return tree;\n}\n\nfunction generateRpcTypeTree(operations: Map<string, OperationDef>): string {\n const controllerTree = buildRpcControllerTree(operations);\n const controllerNames = Object.keys(controllerTree).sort((a, b) => a.localeCompare(b));\n const lines: string[] = [];\n\n for (const controllerName of controllerNames) {\n lines.push(` ${controllerName}: {`);\n const actions = controllerTree[controllerName];\n const actionNames = Object.keys(actions).sort((a, b) => a.localeCompare(b));\n for (const actionName of actionNames) {\n const action = actions[actionName];\n if (action.hasInput) {\n lines.push(` ${actionName}(data: ${action.inputType}): Promise<${action.outputType}>;`);\n } else {\n lines.push(` ${actionName}(): Promise<${action.outputType}>;`);\n }\n }\n lines.push(' };');\n }\n\n return lines.join('\\n');\n}\n\nexport function generateTypes(operations: Map<string, OperationDef>): string {\n const tree = buildNestedTypeTree(operations);\n const typesCode = generateTypeTree(tree);\n const rpcTypeCode = generateRpcTypeTree(operations);\n\n return `// AUTO-GENERATED by @noego/wood -- do not edit\nexport interface WoodRpcManifestEntry {\n channel: string;\n controller: string;\n action: string;\n path: string[];\n hasInput: boolean;\n}\n\nexport interface WoodRpcApi {\n${rpcTypeCode}\n}\n\nexport interface WoodApi {\n${typesCode}\n __rpcManifest: WoodRpcManifestEntry[];\n __load(route: string, params: Record<string, string>): Promise<unknown>;\n __navigate(windowId: string, route: string): Promise<void>;\n __window: {\n current(): Promise<{ ok: boolean; windowId?: string; defaultRoute?: string }>;\n open(windowId: string, options?: Record<string, unknown>): Promise<void>;\n close(windowId?: string): Promise<void>;\n focus(windowId: string): Promise<void>;\n minimize(windowId?: string): Promise<void>;\n maximize(windowId?: string): Promise<void>;\n };\n __debug: {\n current(): Promise<{\n ok: boolean;\n enabled: boolean;\n mode: 'development' | 'production';\n port?: number;\n windows: Array<{\n windowId: string;\n title: string;\n webContentsId: number;\n url: string;\n focused: boolean;\n }>;\n }>;\n };\n __contextMenu: {\n show(data: {\n items: Array<{ label?: string; action?: string; data?: string; _callbackId?: string; disabled?: boolean; type?: 'separator' }>;\n position: { x: number; y: number };\n }): Promise<void>;\n };\n on(channel: string, callback: (...args: unknown[]) => void): () => void;\n __log(level: string, logger: string, message: string, context?: unknown): Promise<{\n ok: boolean;\n forwarded: boolean;\n configured: boolean;\n reason?: 'unconfigured' | 'callback-error';\n errorMessage?: string;\n }>;\n}\n`;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/codegen/type_generator.ts"],"sourcesContent":["import type { OperationDef } from '../types/index.js';\n\nfunction formatPropertyKey(key: string): string {\n if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key)) {\n return key;\n }\n\n return JSON.stringify(key);\n}\n\nfunction operationHasInput(op: OperationDef): boolean {\n return Boolean(\n op.inputSchema &&\n (op.inputSchema as Record<string, unknown>).type === 'object' &&\n Object.keys((op.inputSchema as Record<string, Record<string, unknown>>).properties ?? {}).length > 0,\n );\n}\n\nfunction jsonSchemaToTS(schema: Record<string, unknown> | undefined, indent: string = ''): string {\n if (!schema) return 'unknown';\n\n const type = schema.type as string | undefined;\n\n if (type === 'string') {\n if (schema.enum) {\n return (schema.enum as string[]).map((v) => `'${v}'`).join(' | ');\n }\n return 'string';\n }\n if (type === 'number' || type === 'integer') return 'number';\n if (type === 'boolean') return 'boolean';\n if (type === 'array') {\n const itemType = jsonSchemaToTS(schema.items as Record<string, unknown>, indent);\n return `Array<${itemType}>`;\n }\n if (type === 'object') {\n const props = (schema.properties ?? {}) as Record<string, Record<string, unknown>>;\n const required = new Set((schema.required as string[]) ?? []);\n const entries = Object.entries(props);\n if (entries.length === 0) return 'Record<string, unknown>';\n\n const lines = entries.map(([key, val]) => {\n const optional = required.has(key) ? '' : '?';\n const valType = jsonSchemaToTS(val, indent + ' ');\n return `${indent} ${formatPropertyKey(key)}${optional}: ${valType};`;\n });\n return `{\\n${lines.join('\\n')}\\n${indent}}`;\n }\n\n return 'unknown';\n}\n\nfunction buildNestedTypeTree(operations: Map<string, OperationDef>): Record<string, unknown> {\n const tree: Record<string, unknown> = {};\n for (const [channel, op] of operations) {\n const parts = channel.split('.');\n let current: Record<string, unknown> = tree;\n for (let i = 0; i < parts.length - 1; i++) {\n if (!current[parts[i]]) current[parts[i]] = {};\n current = current[parts[i]] as Record<string, unknown>;\n }\n current[parts[parts.length - 1]] = { __operation: op };\n }\n return tree;\n}\n\nfunction generateTypeTree(node: Record<string, unknown>, indent: string = ' '): string {\n const lines: string[] = [];\n for (const [key, value] of Object.entries(node)) {\n const entry = value as Record<string, unknown>;\n if (entry.__operation) {\n const op = entry.__operation as OperationDef;\n const inputType = jsonSchemaToTS(op.inputSchema as Record<string, unknown>, indent + ' ');\n const outputType = jsonSchemaToTS(op.outputSchema as Record<string, unknown>, indent + ' ');\n const hasInput = operationHasInput(op);\n\n if (hasInput) {\n lines.push(`${indent}${formatPropertyKey(key)}(data: ${inputType}): Promise<${outputType}>;`);\n } else {\n lines.push(`${indent}${formatPropertyKey(key)}(): Promise<${outputType}>;`);\n }\n } else {\n lines.push(`${indent}${formatPropertyKey(key)}: {`);\n lines.push(generateTypeTree(entry, indent + ' '));\n lines.push(`${indent}};`);\n }\n }\n return lines.join('\\n');\n}\n\ntype RpcActionTypeEntry = {\n channel: string;\n inputType: string;\n outputType: string;\n hasInput: boolean;\n};\n\nfunction buildRpcControllerTree(\n operations: Map<string, OperationDef>,\n): Record<string, Record<string, RpcActionTypeEntry>> {\n const tree: Record<string, Record<string, RpcActionTypeEntry>> = {};\n\n for (const operation of operations.values()) {\n const controller = operation.controller;\n const action = operation.action;\n\n if (!tree[controller]) {\n tree[controller] = {};\n }\n\n if (tree[controller][action]) {\n throw new Error(\n `Type codegen failed: duplicate RPC action \"${controller}.${action}\" found in channel \"${operation.channel}\".`,\n );\n }\n\n tree[controller][action] = {\n channel: operation.channel,\n inputType: jsonSchemaToTS(operation.inputSchema as Record<string, unknown>, ' '),\n outputType: jsonSchemaToTS(operation.outputSchema as Record<string, unknown>, ' '),\n hasInput: operationHasInput(operation),\n };\n }\n\n return tree;\n}\n\nfunction generateRpcTypeTree(operations: Map<string, OperationDef>): string {\n const controllerTree = buildRpcControllerTree(operations);\n const controllerNames = Object.keys(controllerTree).sort((a, b) => a.localeCompare(b));\n const lines: string[] = [];\n\n for (const controllerName of controllerNames) {\n lines.push(` ${formatPropertyKey(controllerName)}: {`);\n const actions = controllerTree[controllerName];\n const actionNames = Object.keys(actions).sort((a, b) => a.localeCompare(b));\n for (const actionName of actionNames) {\n const action = actions[actionName];\n if (action.hasInput) {\n lines.push(` ${formatPropertyKey(actionName)}(data: ${action.inputType}): Promise<${action.outputType}>;`);\n } else {\n lines.push(` ${formatPropertyKey(actionName)}(): Promise<${action.outputType}>;`);\n }\n }\n lines.push(' };');\n }\n\n return lines.join('\\n');\n}\n\nexport function generateTypes(operations: Map<string, OperationDef>): string {\n const tree = buildNestedTypeTree(operations);\n const typesCode = generateTypeTree(tree);\n const rpcTypeCode = generateRpcTypeTree(operations);\n\n return `// AUTO-GENERATED by @noego/wood -- do not edit\nexport interface WoodRpcManifestEntry {\n channel: string;\n controller: string;\n action: string;\n path: string[];\n hasInput: boolean;\n}\n\n/**\n * Project-specific renderer App facade contract.\n *\n * This matches @noego/wood/client getApp() and injected App instances:\n * controller name -> controller action.\n */\nexport interface GeneratedApp {\n${rpcTypeCode}\n}\n\n/** Backward-compatible name for the generated client App facade contract. */\nexport interface WoodRpcApi extends GeneratedApp {}\n\n/**\n * Project-specific raw preload bridge contract.\n *\n * This matches the channel path exposed by preload.generated.ts:\n * operation channel segments -> invoke function.\n */\nexport interface GeneratedWoodBridge {\n${typesCode}\n}\n\nexport interface WoodApi extends GeneratedWoodBridge {\n __rpcManifest: WoodRpcManifestEntry[];\n __load(route: string, params: Record<string, string>): Promise<unknown>;\n __navigate(windowId: string, route: string): Promise<void>;\n __window: {\n current(): Promise<{ ok: boolean; windowId?: string; defaultRoute?: string }>;\n open(windowId: string, options?: Record<string, unknown>): Promise<void>;\n close(windowId?: string): Promise<void>;\n focus(windowId: string): Promise<void>;\n minimize(windowId?: string): Promise<void>;\n maximize(windowId?: string): Promise<void>;\n };\n __debug: {\n current(): Promise<{\n ok: boolean;\n enabled: boolean;\n mode: 'development' | 'production';\n port?: number;\n windows: Array<{\n windowId: string;\n title: string;\n webContentsId: number;\n url: string;\n focused: boolean;\n }>;\n }>;\n };\n __contextMenu: {\n show(data: {\n items: Array<{ label?: string; action?: string; data?: string; _callbackId?: string; disabled?: boolean; type?: 'separator' }>;\n position: { x: number; y: number };\n }): Promise<void>;\n };\n on(channel: string, callback: (...args: unknown[]) => void): () => void;\n __log(level: string, logger: string, message: string, context?: unknown): Promise<{\n ok: boolean;\n forwarded: boolean;\n configured: boolean;\n reason?: 'unconfigured' | 'callback-error';\n errorMessage?: string;\n }>;\n}\n`;\n}\n"],"mappings":"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","names":[]}
@@ -108,6 +108,7 @@ class WoodDevServer {
108
108
  const rendererComponentDir = import_path.default.resolve(this.cwd, this.config.renderer.componentDir);
109
109
  const rendererOutDir = import_path.default.resolve(this.cwd, this.config.renderer.componentDir, "generated");
110
110
  (0, import_fs.mkdirSync)(rendererOutDir, { recursive: true });
111
+ (0, import_fs.writeFileSync)(import_path.default.join(rendererOutDir, "wood.d.ts"), typesCode);
111
112
  (0, import_fs.writeFileSync)(
112
113
  import_path.default.join(rendererOutDir, "app.ts"),
113
114
  (0, import_renderer_app_generator.generateRendererApp)(views, {
@@ -188,6 +189,7 @@ class WoodDevServer {
188
189
  viewsYamlPath: this.config.renderer.openapi,
189
190
  controllersDir: this.config.main.controllers,
190
191
  outDir,
192
+ rendererOutDir: import_path.default.resolve(this.cwd, this.config.renderer.componentDir, "generated"),
191
193
  cwd: this.cwd,
192
194
  onViewsChange: async () => {
193
195
  const viewsContent = (0, import_fs.readFileSync)(
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/dev/wood_dev_server.ts"],"sourcesContent":["import { readFileSync, writeFileSync, mkdirSync } from 'fs';\nimport path from 'path';\nimport { getLogger } from '@noego/logger';\nimport { parseConfig } from '../config/config_parser.cjs';\nimport { parseOperations } from '../parser/operations_parser.cjs';\nimport { parseViews } from '../parser/views_parser.cjs';\nimport { generatePreload } from '../codegen/preload_generator.cjs';\nimport { generateTypes } from '../codegen/type_generator.cjs';\nimport { generateTestMock } from '../codegen/test_mock_generator.cjs';\nimport { generateControllerRegistration } from '../codegen/controller_registration_generator.cjs';\nimport { generateRuntime } from '../codegen/runtime_generator.cjs';\nimport { generateRendererApp } from '../codegen/renderer_app_generator.cjs';\nimport { BackendBridge } from '../bridge/backend_bridge.cjs';\nimport { BackendWatcher } from './backend_watcher.cjs';\nimport { YamlWatcher } from './yaml_watcher.cjs';\nimport { EsbuildBackend } from './esbuild_backend.cjs';\nimport type { WoodConfig } from '../types/index.cjs';\n\nconst logger = getLogger('wood:dev');\n\nexport interface DevServerOptions {\n configPath?: string;\n cwd?: string;\n}\n\nexport class WoodDevServer {\n private config: WoodConfig | null = null;\n private bridge: BackendBridge | null = null;\n private backendWatcher: BackendWatcher | null = null;\n private yamlWatcher: YamlWatcher | null = null;\n private esbuild: EsbuildBackend | null = null;\n private cwd: string;\n\n constructor(options: DevServerOptions = {}) {\n this.cwd = options.cwd ?? process.cwd();\n }\n\n /**\n * Start the Wood development server.\n *\n * Automatically sets process.env.WOOD_MODE = 'development' so the app\n * can conditionally use dynamic controller imports (dev) vs pre-registered\n * controllers from the generated file (production).\n *\n * Steps:\n * 1. Parse config\n * 2. Parse + stitch YAML\n * 3. Generate preload, types, mock\n * 4. Build backend with esbuild\n * 5. (electron-vite would be started here -- omitted for MVP)\n * 6. Spawn backend via bridge\n * 7. Start watchers\n */\n async start(): Promise<void> {\n process.env.WOOD_MODE = 'development';\n\n // 1. Parse config\n const configPath = path.resolve(this.cwd, 'wood.config.yml');\n this.config = parseConfig(configPath);\n logger.info('config loaded', { outDir: this.config.outDir });\n\n // 2. Parse operations YAML\n const opsYamlPath = path.resolve(this.cwd, this.config.main.openapi);\n const opsContent = readFileSync(opsYamlPath, 'utf-8');\n const operations = parseOperations(opsContent);\n logger.info('operations parsed', { count: operations.size });\n\n // 3. Parse views YAML\n const viewsYamlPath = path.resolve(this.cwd, this.config.renderer.openapi);\n const viewsContent = readFileSync(viewsYamlPath, 'utf-8');\n const views = parseViews(viewsContent);\n logger.info('views parsed', { count: views.length });\n\n // 4. Generate files\n const outDir = path.resolve(this.cwd, this.config.outDir);\n mkdirSync(outDir, { recursive: true });\n\n const preloadCode = generatePreload(operations);\n writeFileSync(path.join(outDir, 'preload.generated.ts'), preloadCode);\n\n const typesCode = generateTypes(operations);\n writeFileSync(path.join(outDir, 'wood.d.ts'), typesCode);\n\n const mockCode = generateTestMock(operations);\n writeFileSync(path.join(outDir, 'wood.mock.ts'), mockCode);\n\n const controllersDir = path.resolve(this.cwd, this.config.main.controllers);\n const controllerRegCode = generateControllerRegistration(operations, {\n controllersDir,\n outDir,\n });\n writeFileSync(path.join(outDir, 'controllers.generated.ts'), controllerRegCode);\n\n const runtimeCode = generateRuntime(operations, {\n controllersDir,\n outDir,\n });\n writeFileSync(path.join(outDir, 'runtime.generated.ts'), runtimeCode);\n\n const rendererComponentDir = path.resolve(this.cwd, this.config.renderer.componentDir);\n const rendererOutDir = path.resolve(this.cwd, this.config.renderer.componentDir, 'generated');\n mkdirSync(rendererOutDir, { recursive: true });\n\n writeFileSync(\n path.join(rendererOutDir, 'app.ts'),\n generateRendererApp(views, {\n componentDir: rendererComponentDir,\n outDir: rendererOutDir,\n entryViewKey: this.config.renderer.entry,\n }),\n );\n\n logger.info('codegen complete', { outDir });\n\n // 5. Build backend with esbuild (initial build + create incremental context)\n const backendEntry = this.config.main.entry;\n const backendOutfile = path.join(this.config.outDir, 'backend', 'index.js');\n\n this.esbuild = new EsbuildBackend({\n entryPoint: backendEntry,\n outfile: backendOutfile,\n cwd: this.cwd,\n external: this.config.build ? [] : undefined,\n });\n\n await this.esbuild.build();\n await this.esbuild.createContext();\n\n // 6. Start backend bridge (split-serve)\n if (this.config.dev?.splitServe !== false) {\n const resolvedBackendEntry = path.resolve(this.cwd, backendOutfile);\n this.bridge = new BackendBridge({\n backendEntry: resolvedBackendEntry,\n onReady: () => {\n logger.info('backend ready (split-serve)');\n },\n onPush: (channel, data) => {\n logger.debug('backend push', { channel, data });\n },\n onExit: (code, signal) => {\n logger.debug('backend exited', { code, signal });\n },\n });\n await this.bridge.start();\n }\n\n // 7. Start watchers\n if (this.config.dev?.watch !== false) {\n this.startWatchers(outDir);\n }\n }\n\n /** Stop the dev server and clean up all resources. */\n async stop(): Promise<void> {\n this.backendWatcher?.stop();\n this.yamlWatcher?.stop();\n\n if (this.bridge) {\n await this.bridge.stop();\n logger.info('backend bridge stopped');\n }\n\n if (this.esbuild) {\n await this.esbuild.dispose();\n }\n }\n\n /** Get the parsed config. */\n getConfig(): WoodConfig | null {\n return this.config;\n }\n\n private startWatchers(outDir: string): void {\n if (!this.config) return;\n\n // Backend watcher: server + middleware files -> rebuild + restart\n if (this.bridge && this.esbuild) {\n const backendPatterns = [\n `${this.config.main.controllers}/**/*.ts`,\n `${this.config.main.middleware}/**/*.ts`,\n `${this.config.main.entry}`,\n ...(this.config.main.watch ?? []),\n ];\n\n const esbuild = this.esbuild;\n this.backendWatcher = new BackendWatcher({\n patterns: backendPatterns,\n bridge: this.bridge,\n buildFn: () => esbuild.rebuild().then(() => {}),\n cwd: this.cwd,\n });\n this.backendWatcher.start();\n logger.info('backend watcher started', { patterns: backendPatterns });\n }\n\n // YAML watcher: operations + views YAML -> regenerate codegen\n this.yamlWatcher = new YamlWatcher({\n operationsYamlPath: this.config.main.openapi,\n viewsYamlPath: this.config.renderer.openapi,\n controllersDir: this.config.main.controllers,\n outDir,\n cwd: this.cwd,\n onViewsChange: async () => {\n // Re-parse views and regenerate client router manifest\n const viewsContent = readFileSync(\n path.resolve(this.cwd, this.config!.renderer.openapi),\n 'utf-8',\n );\n const views = parseViews(viewsContent);\n\n const rendererComponentDir = path.resolve(this.cwd, this.config!.renderer.componentDir);\n const rendererOutDir = path.resolve(this.cwd, this.config!.renderer.componentDir, 'generated');\n mkdirSync(rendererOutDir, { recursive: true });\n\n writeFileSync(\n path.join(rendererOutDir, 'app.ts'),\n generateRendererApp(views, {\n componentDir: rendererComponentDir,\n outDir: rendererOutDir,\n entryViewKey: this.config!.renderer.entry,\n }),\n );\n\n logger.info('views renderer codegen regenerated', { rendererOutDir });\n },\n });\n this.yamlWatcher.start();\n logger.info('yaml watcher started');\n }\n}\n"],"mappings":"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","names":["path"]}
1
+ {"version":3,"sources":["../../src/dev/wood_dev_server.ts"],"sourcesContent":["import { readFileSync, writeFileSync, mkdirSync } from 'fs';\nimport path from 'path';\nimport { getLogger } from '@noego/logger';\nimport { parseConfig } from '../config/config_parser.cjs';\nimport { parseOperations } from '../parser/operations_parser.cjs';\nimport { parseViews } from '../parser/views_parser.cjs';\nimport { generatePreload } from '../codegen/preload_generator.cjs';\nimport { generateTypes } from '../codegen/type_generator.cjs';\nimport { generateTestMock } from '../codegen/test_mock_generator.cjs';\nimport { generateControllerRegistration } from '../codegen/controller_registration_generator.cjs';\nimport { generateRuntime } from '../codegen/runtime_generator.cjs';\nimport { generateRendererApp } from '../codegen/renderer_app_generator.cjs';\nimport { BackendBridge } from '../bridge/backend_bridge.cjs';\nimport { BackendWatcher } from './backend_watcher.cjs';\nimport { YamlWatcher } from './yaml_watcher.cjs';\nimport { EsbuildBackend } from './esbuild_backend.cjs';\nimport type { WoodConfig } from '../types/index.cjs';\n\nconst logger = getLogger('wood:dev');\n\nexport interface DevServerOptions {\n configPath?: string;\n cwd?: string;\n}\n\nexport class WoodDevServer {\n private config: WoodConfig | null = null;\n private bridge: BackendBridge | null = null;\n private backendWatcher: BackendWatcher | null = null;\n private yamlWatcher: YamlWatcher | null = null;\n private esbuild: EsbuildBackend | null = null;\n private cwd: string;\n\n constructor(options: DevServerOptions = {}) {\n this.cwd = options.cwd ?? process.cwd();\n }\n\n /**\n * Start the Wood development server.\n *\n * Automatically sets process.env.WOOD_MODE = 'development' so the app\n * can conditionally use dynamic controller imports (dev) vs pre-registered\n * controllers from the generated file (production).\n *\n * Steps:\n * 1. Parse config\n * 2. Parse + stitch YAML\n * 3. Generate preload, types, mock\n * 4. Build backend with esbuild\n * 5. (electron-vite would be started here -- omitted for MVP)\n * 6. Spawn backend via bridge\n * 7. Start watchers\n */\n async start(): Promise<void> {\n process.env.WOOD_MODE = 'development';\n\n // 1. Parse config\n const configPath = path.resolve(this.cwd, 'wood.config.yml');\n this.config = parseConfig(configPath);\n logger.info('config loaded', { outDir: this.config.outDir });\n\n // 2. Parse operations YAML\n const opsYamlPath = path.resolve(this.cwd, this.config.main.openapi);\n const opsContent = readFileSync(opsYamlPath, 'utf-8');\n const operations = parseOperations(opsContent);\n logger.info('operations parsed', { count: operations.size });\n\n // 3. Parse views YAML\n const viewsYamlPath = path.resolve(this.cwd, this.config.renderer.openapi);\n const viewsContent = readFileSync(viewsYamlPath, 'utf-8');\n const views = parseViews(viewsContent);\n logger.info('views parsed', { count: views.length });\n\n // 4. Generate files\n const outDir = path.resolve(this.cwd, this.config.outDir);\n mkdirSync(outDir, { recursive: true });\n\n const preloadCode = generatePreload(operations);\n writeFileSync(path.join(outDir, 'preload.generated.ts'), preloadCode);\n\n const typesCode = generateTypes(operations);\n writeFileSync(path.join(outDir, 'wood.d.ts'), typesCode);\n\n const mockCode = generateTestMock(operations);\n writeFileSync(path.join(outDir, 'wood.mock.ts'), mockCode);\n\n const controllersDir = path.resolve(this.cwd, this.config.main.controllers);\n const controllerRegCode = generateControllerRegistration(operations, {\n controllersDir,\n outDir,\n });\n writeFileSync(path.join(outDir, 'controllers.generated.ts'), controllerRegCode);\n\n const runtimeCode = generateRuntime(operations, {\n controllersDir,\n outDir,\n });\n writeFileSync(path.join(outDir, 'runtime.generated.ts'), runtimeCode);\n\n const rendererComponentDir = path.resolve(this.cwd, this.config.renderer.componentDir);\n const rendererOutDir = path.resolve(this.cwd, this.config.renderer.componentDir, 'generated');\n mkdirSync(rendererOutDir, { recursive: true });\n\n writeFileSync(path.join(rendererOutDir, 'wood.d.ts'), typesCode);\n\n writeFileSync(\n path.join(rendererOutDir, 'app.ts'),\n generateRendererApp(views, {\n componentDir: rendererComponentDir,\n outDir: rendererOutDir,\n entryViewKey: this.config.renderer.entry,\n }),\n );\n\n logger.info('codegen complete', { outDir });\n\n // 5. Build backend with esbuild (initial build + create incremental context)\n const backendEntry = this.config.main.entry;\n const backendOutfile = path.join(this.config.outDir, 'backend', 'index.js');\n\n this.esbuild = new EsbuildBackend({\n entryPoint: backendEntry,\n outfile: backendOutfile,\n cwd: this.cwd,\n external: this.config.build ? [] : undefined,\n });\n\n await this.esbuild.build();\n await this.esbuild.createContext();\n\n // 6. Start backend bridge (split-serve)\n if (this.config.dev?.splitServe !== false) {\n const resolvedBackendEntry = path.resolve(this.cwd, backendOutfile);\n this.bridge = new BackendBridge({\n backendEntry: resolvedBackendEntry,\n onReady: () => {\n logger.info('backend ready (split-serve)');\n },\n onPush: (channel, data) => {\n logger.debug('backend push', { channel, data });\n },\n onExit: (code, signal) => {\n logger.debug('backend exited', { code, signal });\n },\n });\n await this.bridge.start();\n }\n\n // 7. Start watchers\n if (this.config.dev?.watch !== false) {\n this.startWatchers(outDir);\n }\n }\n\n /** Stop the dev server and clean up all resources. */\n async stop(): Promise<void> {\n this.backendWatcher?.stop();\n this.yamlWatcher?.stop();\n\n if (this.bridge) {\n await this.bridge.stop();\n logger.info('backend bridge stopped');\n }\n\n if (this.esbuild) {\n await this.esbuild.dispose();\n }\n }\n\n /** Get the parsed config. */\n getConfig(): WoodConfig | null {\n return this.config;\n }\n\n private startWatchers(outDir: string): void {\n if (!this.config) return;\n\n // Backend watcher: server + middleware files -> rebuild + restart\n if (this.bridge && this.esbuild) {\n const backendPatterns = [\n `${this.config.main.controllers}/**/*.ts`,\n `${this.config.main.middleware}/**/*.ts`,\n `${this.config.main.entry}`,\n ...(this.config.main.watch ?? []),\n ];\n\n const esbuild = this.esbuild;\n this.backendWatcher = new BackendWatcher({\n patterns: backendPatterns,\n bridge: this.bridge,\n buildFn: () => esbuild.rebuild().then(() => {}),\n cwd: this.cwd,\n });\n this.backendWatcher.start();\n logger.info('backend watcher started', { patterns: backendPatterns });\n }\n\n // YAML watcher: operations + views YAML -> regenerate codegen\n this.yamlWatcher = new YamlWatcher({\n operationsYamlPath: this.config.main.openapi,\n viewsYamlPath: this.config.renderer.openapi,\n controllersDir: this.config.main.controllers,\n outDir,\n rendererOutDir: path.resolve(this.cwd, this.config.renderer.componentDir, 'generated'),\n cwd: this.cwd,\n onViewsChange: async () => {\n // Re-parse views and regenerate client router manifest\n const viewsContent = readFileSync(\n path.resolve(this.cwd, this.config!.renderer.openapi),\n 'utf-8',\n );\n const views = parseViews(viewsContent);\n\n const rendererComponentDir = path.resolve(this.cwd, this.config!.renderer.componentDir);\n const rendererOutDir = path.resolve(this.cwd, this.config!.renderer.componentDir, 'generated');\n mkdirSync(rendererOutDir, { recursive: true });\n\n writeFileSync(\n path.join(rendererOutDir, 'app.ts'),\n generateRendererApp(views, {\n componentDir: rendererComponentDir,\n outDir: rendererOutDir,\n entryViewKey: this.config!.renderer.entry,\n }),\n );\n\n logger.info('views renderer codegen regenerated', { rendererOutDir });\n },\n });\n this.yamlWatcher.start();\n logger.info('yaml watcher started');\n }\n}\n"],"mappings":"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","names":["path"]}
@@ -75,6 +75,7 @@ class WoodDevServer {
75
75
  const rendererComponentDir = path.resolve(this.cwd, this.config.renderer.componentDir);
76
76
  const rendererOutDir = path.resolve(this.cwd, this.config.renderer.componentDir, "generated");
77
77
  mkdirSync(rendererOutDir, { recursive: true });
78
+ writeFileSync(path.join(rendererOutDir, "wood.d.ts"), typesCode);
78
79
  writeFileSync(
79
80
  path.join(rendererOutDir, "app.ts"),
80
81
  generateRendererApp(views, {
@@ -155,6 +156,7 @@ class WoodDevServer {
155
156
  viewsYamlPath: this.config.renderer.openapi,
156
157
  controllersDir: this.config.main.controllers,
157
158
  outDir,
159
+ rendererOutDir: path.resolve(this.cwd, this.config.renderer.componentDir, "generated"),
158
160
  cwd: this.cwd,
159
161
  onViewsChange: async () => {
160
162
  const viewsContent = readFileSync(
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/dev/wood_dev_server.ts"],"sourcesContent":["import { readFileSync, writeFileSync, mkdirSync } from 'fs';\nimport path from 'path';\nimport { getLogger } from '@noego/logger';\nimport { parseConfig } from '../config/config_parser.js';\nimport { parseOperations } from '../parser/operations_parser.js';\nimport { parseViews } from '../parser/views_parser.js';\nimport { generatePreload } from '../codegen/preload_generator.js';\nimport { generateTypes } from '../codegen/type_generator.js';\nimport { generateTestMock } from '../codegen/test_mock_generator.js';\nimport { generateControllerRegistration } from '../codegen/controller_registration_generator.js';\nimport { generateRuntime } from '../codegen/runtime_generator.js';\nimport { generateRendererApp } from '../codegen/renderer_app_generator.js';\nimport { BackendBridge } from '../bridge/backend_bridge.js';\nimport { BackendWatcher } from './backend_watcher.js';\nimport { YamlWatcher } from './yaml_watcher.js';\nimport { EsbuildBackend } from './esbuild_backend.js';\nimport type { WoodConfig } from '../types/index.js';\n\nconst logger = getLogger('wood:dev');\n\nexport interface DevServerOptions {\n configPath?: string;\n cwd?: string;\n}\n\nexport class WoodDevServer {\n private config: WoodConfig | null = null;\n private bridge: BackendBridge | null = null;\n private backendWatcher: BackendWatcher | null = null;\n private yamlWatcher: YamlWatcher | null = null;\n private esbuild: EsbuildBackend | null = null;\n private cwd: string;\n\n constructor(options: DevServerOptions = {}) {\n this.cwd = options.cwd ?? process.cwd();\n }\n\n /**\n * Start the Wood development server.\n *\n * Automatically sets process.env.WOOD_MODE = 'development' so the app\n * can conditionally use dynamic controller imports (dev) vs pre-registered\n * controllers from the generated file (production).\n *\n * Steps:\n * 1. Parse config\n * 2. Parse + stitch YAML\n * 3. Generate preload, types, mock\n * 4. Build backend with esbuild\n * 5. (electron-vite would be started here -- omitted for MVP)\n * 6. Spawn backend via bridge\n * 7. Start watchers\n */\n async start(): Promise<void> {\n process.env.WOOD_MODE = 'development';\n\n // 1. Parse config\n const configPath = path.resolve(this.cwd, 'wood.config.yml');\n this.config = parseConfig(configPath);\n logger.info('config loaded', { outDir: this.config.outDir });\n\n // 2. Parse operations YAML\n const opsYamlPath = path.resolve(this.cwd, this.config.main.openapi);\n const opsContent = readFileSync(opsYamlPath, 'utf-8');\n const operations = parseOperations(opsContent);\n logger.info('operations parsed', { count: operations.size });\n\n // 3. Parse views YAML\n const viewsYamlPath = path.resolve(this.cwd, this.config.renderer.openapi);\n const viewsContent = readFileSync(viewsYamlPath, 'utf-8');\n const views = parseViews(viewsContent);\n logger.info('views parsed', { count: views.length });\n\n // 4. Generate files\n const outDir = path.resolve(this.cwd, this.config.outDir);\n mkdirSync(outDir, { recursive: true });\n\n const preloadCode = generatePreload(operations);\n writeFileSync(path.join(outDir, 'preload.generated.ts'), preloadCode);\n\n const typesCode = generateTypes(operations);\n writeFileSync(path.join(outDir, 'wood.d.ts'), typesCode);\n\n const mockCode = generateTestMock(operations);\n writeFileSync(path.join(outDir, 'wood.mock.ts'), mockCode);\n\n const controllersDir = path.resolve(this.cwd, this.config.main.controllers);\n const controllerRegCode = generateControllerRegistration(operations, {\n controllersDir,\n outDir,\n });\n writeFileSync(path.join(outDir, 'controllers.generated.ts'), controllerRegCode);\n\n const runtimeCode = generateRuntime(operations, {\n controllersDir,\n outDir,\n });\n writeFileSync(path.join(outDir, 'runtime.generated.ts'), runtimeCode);\n\n const rendererComponentDir = path.resolve(this.cwd, this.config.renderer.componentDir);\n const rendererOutDir = path.resolve(this.cwd, this.config.renderer.componentDir, 'generated');\n mkdirSync(rendererOutDir, { recursive: true });\n\n writeFileSync(\n path.join(rendererOutDir, 'app.ts'),\n generateRendererApp(views, {\n componentDir: rendererComponentDir,\n outDir: rendererOutDir,\n entryViewKey: this.config.renderer.entry,\n }),\n );\n\n logger.info('codegen complete', { outDir });\n\n // 5. Build backend with esbuild (initial build + create incremental context)\n const backendEntry = this.config.main.entry;\n const backendOutfile = path.join(this.config.outDir, 'backend', 'index.js');\n\n this.esbuild = new EsbuildBackend({\n entryPoint: backendEntry,\n outfile: backendOutfile,\n cwd: this.cwd,\n external: this.config.build ? [] : undefined,\n });\n\n await this.esbuild.build();\n await this.esbuild.createContext();\n\n // 6. Start backend bridge (split-serve)\n if (this.config.dev?.splitServe !== false) {\n const resolvedBackendEntry = path.resolve(this.cwd, backendOutfile);\n this.bridge = new BackendBridge({\n backendEntry: resolvedBackendEntry,\n onReady: () => {\n logger.info('backend ready (split-serve)');\n },\n onPush: (channel, data) => {\n logger.debug('backend push', { channel, data });\n },\n onExit: (code, signal) => {\n logger.debug('backend exited', { code, signal });\n },\n });\n await this.bridge.start();\n }\n\n // 7. Start watchers\n if (this.config.dev?.watch !== false) {\n this.startWatchers(outDir);\n }\n }\n\n /** Stop the dev server and clean up all resources. */\n async stop(): Promise<void> {\n this.backendWatcher?.stop();\n this.yamlWatcher?.stop();\n\n if (this.bridge) {\n await this.bridge.stop();\n logger.info('backend bridge stopped');\n }\n\n if (this.esbuild) {\n await this.esbuild.dispose();\n }\n }\n\n /** Get the parsed config. */\n getConfig(): WoodConfig | null {\n return this.config;\n }\n\n private startWatchers(outDir: string): void {\n if (!this.config) return;\n\n // Backend watcher: server + middleware files -> rebuild + restart\n if (this.bridge && this.esbuild) {\n const backendPatterns = [\n `${this.config.main.controllers}/**/*.ts`,\n `${this.config.main.middleware}/**/*.ts`,\n `${this.config.main.entry}`,\n ...(this.config.main.watch ?? []),\n ];\n\n const esbuild = this.esbuild;\n this.backendWatcher = new BackendWatcher({\n patterns: backendPatterns,\n bridge: this.bridge,\n buildFn: () => esbuild.rebuild().then(() => {}),\n cwd: this.cwd,\n });\n this.backendWatcher.start();\n logger.info('backend watcher started', { patterns: backendPatterns });\n }\n\n // YAML watcher: operations + views YAML -> regenerate codegen\n this.yamlWatcher = new YamlWatcher({\n operationsYamlPath: this.config.main.openapi,\n viewsYamlPath: this.config.renderer.openapi,\n controllersDir: this.config.main.controllers,\n outDir,\n cwd: this.cwd,\n onViewsChange: async () => {\n // Re-parse views and regenerate client router manifest\n const viewsContent = readFileSync(\n path.resolve(this.cwd, this.config!.renderer.openapi),\n 'utf-8',\n );\n const views = parseViews(viewsContent);\n\n const rendererComponentDir = path.resolve(this.cwd, this.config!.renderer.componentDir);\n const rendererOutDir = path.resolve(this.cwd, this.config!.renderer.componentDir, 'generated');\n mkdirSync(rendererOutDir, { recursive: true });\n\n writeFileSync(\n path.join(rendererOutDir, 'app.ts'),\n generateRendererApp(views, {\n componentDir: rendererComponentDir,\n outDir: rendererOutDir,\n entryViewKey: this.config!.renderer.entry,\n }),\n );\n\n logger.info('views renderer codegen regenerated', { rendererOutDir });\n },\n });\n this.yamlWatcher.start();\n logger.info('yaml watcher started');\n }\n}\n"],"mappings":"AAAA,SAAS,cAAc,eAAe,iBAAiB;AACvD,OAAO,UAAU;AACjB,SAAS,iBAAiB;AAC1B,SAAS,mBAAmB;AAC5B,SAAS,uBAAuB;AAChC,SAAS,kBAAkB;AAC3B,SAAS,uBAAuB;AAChC,SAAS,qBAAqB;AAC9B,SAAS,wBAAwB;AACjC,SAAS,sCAAsC;AAC/C,SAAS,uBAAuB;AAChC,SAAS,2BAA2B;AACpC,SAAS,qBAAqB;AAC9B,SAAS,sBAAsB;AAC/B,SAAS,mBAAmB;AAC5B,SAAS,sBAAsB;AAG/B,MAAM,SAAS,UAAU,UAAU;AAO5B,MAAM,cAAc;AAAA,EAQzB,YAAY,UAA4B,CAAC,GAAG;AAP5C,SAAQ,SAA4B;AACpC,SAAQ,SAA+B;AACvC,SAAQ,iBAAwC;AAChD,SAAQ,cAAkC;AAC1C,SAAQ,UAAiC;AAIvC,SAAK,MAAM,QAAQ,OAAO,QAAQ,IAAI;AAAA,EACxC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAkBA,MAAM,QAAuB;AAC3B,YAAQ,IAAI,YAAY;AAGxB,UAAM,aAAa,KAAK,QAAQ,KAAK,KAAK,iBAAiB;AAC3D,SAAK,SAAS,YAAY,UAAU;AACpC,WAAO,KAAK,iBAAiB,EAAE,QAAQ,KAAK,OAAO,OAAO,CAAC;AAG3D,UAAM,cAAc,KAAK,QAAQ,KAAK,KAAK,KAAK,OAAO,KAAK,OAAO;AACnE,UAAM,aAAa,aAAa,aAAa,OAAO;AACpD,UAAM,aAAa,gBAAgB,UAAU;AAC7C,WAAO,KAAK,qBAAqB,EAAE,OAAO,WAAW,KAAK,CAAC;AAG3D,UAAM,gBAAgB,KAAK,QAAQ,KAAK,KAAK,KAAK,OAAO,SAAS,OAAO;AACzE,UAAM,eAAe,aAAa,eAAe,OAAO;AACxD,UAAM,QAAQ,WAAW,YAAY;AACrC,WAAO,KAAK,gBAAgB,EAAE,OAAO,MAAM,OAAO,CAAC;AAGnD,UAAM,SAAS,KAAK,QAAQ,KAAK,KAAK,KAAK,OAAO,MAAM;AACxD,cAAU,QAAQ,EAAE,WAAW,KAAK,CAAC;AAErC,UAAM,cAAc,gBAAgB,UAAU;AAC9C,kBAAc,KAAK,KAAK,QAAQ,sBAAsB,GAAG,WAAW;AAEpE,UAAM,YAAY,cAAc,UAAU;AAC1C,kBAAc,KAAK,KAAK,QAAQ,WAAW,GAAG,SAAS;AAEvD,UAAM,WAAW,iBAAiB,UAAU;AAC5C,kBAAc,KAAK,KAAK,QAAQ,cAAc,GAAG,QAAQ;AAEzD,UAAM,iBAAiB,KAAK,QAAQ,KAAK,KAAK,KAAK,OAAO,KAAK,WAAW;AAC1E,UAAM,oBAAoB,+BAA+B,YAAY;AAAA,MACnE;AAAA,MACA;AAAA,IACF,CAAC;AACD,kBAAc,KAAK,KAAK,QAAQ,0BAA0B,GAAG,iBAAiB;AAE9E,UAAM,cAAc,gBAAgB,YAAY;AAAA,MAC9C;AAAA,MACA;AAAA,IACF,CAAC;AACD,kBAAc,KAAK,KAAK,QAAQ,sBAAsB,GAAG,WAAW;AAEpE,UAAM,uBAAuB,KAAK,QAAQ,KAAK,KAAK,KAAK,OAAO,SAAS,YAAY;AACrF,UAAM,iBAAiB,KAAK,QAAQ,KAAK,KAAK,KAAK,OAAO,SAAS,cAAc,WAAW;AAC5F,cAAU,gBAAgB,EAAE,WAAW,KAAK,CAAC;AAE7C;AAAA,MACE,KAAK,KAAK,gBAAgB,QAAQ;AAAA,MAClC,oBAAoB,OAAO;AAAA,QACzB,cAAc;AAAA,QACd,QAAQ;AAAA,QACR,cAAc,KAAK,OAAO,SAAS;AAAA,MACrC,CAAC;AAAA,IACH;AAEA,WAAO,KAAK,oBAAoB,EAAE,OAAO,CAAC;AAG1C,UAAM,eAAe,KAAK,OAAO,KAAK;AACtC,UAAM,iBAAiB,KAAK,KAAK,KAAK,OAAO,QAAQ,WAAW,UAAU;AAE1E,SAAK,UAAU,IAAI,eAAe;AAAA,MAChC,YAAY;AAAA,MACZ,SAAS;AAAA,MACT,KAAK,KAAK;AAAA,MACV,UAAU,KAAK,OAAO,QAAQ,CAAC,IAAI;AAAA,IACrC,CAAC;AAED,UAAM,KAAK,QAAQ,MAAM;AACzB,UAAM,KAAK,QAAQ,cAAc;AAGjC,QAAI,KAAK,OAAO,KAAK,eAAe,OAAO;AACzC,YAAM,uBAAuB,KAAK,QAAQ,KAAK,KAAK,cAAc;AAClE,WAAK,SAAS,IAAI,cAAc;AAAA,QAC9B,cAAc;AAAA,QACd,SAAS,MAAM;AACb,iBAAO,KAAK,6BAA6B;AAAA,QAC3C;AAAA,QACA,QAAQ,CAAC,SAAS,SAAS;AACzB,iBAAO,MAAM,gBAAgB,EAAE,SAAS,KAAK,CAAC;AAAA,QAChD;AAAA,QACA,QAAQ,CAAC,MAAM,WAAW;AACxB,iBAAO,MAAM,kBAAkB,EAAE,MAAM,OAAO,CAAC;AAAA,QACjD;AAAA,MACF,CAAC;AACD,YAAM,KAAK,OAAO,MAAM;AAAA,IAC1B;AAGA,QAAI,KAAK,OAAO,KAAK,UAAU,OAAO;AACpC,WAAK,cAAc,MAAM;AAAA,IAC3B;AAAA,EACF;AAAA;AAAA,EAGA,MAAM,OAAsB;AAC1B,SAAK,gBAAgB,KAAK;AAC1B,SAAK,aAAa,KAAK;AAEvB,QAAI,KAAK,QAAQ;AACf,YAAM,KAAK,OAAO,KAAK;AACvB,aAAO,KAAK,wBAAwB;AAAA,IACtC;AAEA,QAAI,KAAK,SAAS;AAChB,YAAM,KAAK,QAAQ,QAAQ;AAAA,IAC7B;AAAA,EACF;AAAA;AAAA,EAGA,YAA+B;AAC7B,WAAO,KAAK;AAAA,EACd;AAAA,EAEQ,cAAc,QAAsB;AAC1C,QAAI,CAAC,KAAK,OAAQ;AAGlB,QAAI,KAAK,UAAU,KAAK,SAAS;AAC/B,YAAM,kBAAkB;AAAA,QACtB,GAAG,KAAK,OAAO,KAAK,WAAW;AAAA,QAC/B,GAAG,KAAK,OAAO,KAAK,UAAU;AAAA,QAC9B,GAAG,KAAK,OAAO,KAAK,KAAK;AAAA,QACzB,GAAI,KAAK,OAAO,KAAK,SAAS,CAAC;AAAA,MACjC;AAEA,YAAM,UAAU,KAAK;AACrB,WAAK,iBAAiB,IAAI,eAAe;AAAA,QACvC,UAAU;AAAA,QACV,QAAQ,KAAK;AAAA,QACb,SAAS,MAAM,QAAQ,QAAQ,EAAE,KAAK,MAAM;AAAA,QAAC,CAAC;AAAA,QAC9C,KAAK,KAAK;AAAA,MACZ,CAAC;AACD,WAAK,eAAe,MAAM;AAC1B,aAAO,KAAK,2BAA2B,EAAE,UAAU,gBAAgB,CAAC;AAAA,IACtE;AAGA,SAAK,cAAc,IAAI,YAAY;AAAA,MACjC,oBAAoB,KAAK,OAAO,KAAK;AAAA,MACrC,eAAe,KAAK,OAAO,SAAS;AAAA,MACpC,gBAAgB,KAAK,OAAO,KAAK;AAAA,MACjC;AAAA,MACA,KAAK,KAAK;AAAA,MACV,eAAe,YAAY;AAEzB,cAAM,eAAe;AAAA,UACnB,KAAK,QAAQ,KAAK,KAAK,KAAK,OAAQ,SAAS,OAAO;AAAA,UACpD;AAAA,QACF;AACA,cAAM,QAAQ,WAAW,YAAY;AAErC,cAAM,uBAAuB,KAAK,QAAQ,KAAK,KAAK,KAAK,OAAQ,SAAS,YAAY;AACtF,cAAM,iBAAiB,KAAK,QAAQ,KAAK,KAAK,KAAK,OAAQ,SAAS,cAAc,WAAW;AAC7F,kBAAU,gBAAgB,EAAE,WAAW,KAAK,CAAC;AAE7C;AAAA,UACE,KAAK,KAAK,gBAAgB,QAAQ;AAAA,UAClC,oBAAoB,OAAO;AAAA,YACzB,cAAc;AAAA,YACd,QAAQ;AAAA,YACR,cAAc,KAAK,OAAQ,SAAS;AAAA,UACtC,CAAC;AAAA,QACH;AAEA,eAAO,KAAK,sCAAsC,EAAE,eAAe,CAAC;AAAA,MACtE;AAAA,IACF,CAAC;AACD,SAAK,YAAY,MAAM;AACvB,WAAO,KAAK,sBAAsB;AAAA,EACpC;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/dev/wood_dev_server.ts"],"sourcesContent":["import { readFileSync, writeFileSync, mkdirSync } from 'fs';\nimport path from 'path';\nimport { getLogger } from '@noego/logger';\nimport { parseConfig } from '../config/config_parser.js';\nimport { parseOperations } from '../parser/operations_parser.js';\nimport { parseViews } from '../parser/views_parser.js';\nimport { generatePreload } from '../codegen/preload_generator.js';\nimport { generateTypes } from '../codegen/type_generator.js';\nimport { generateTestMock } from '../codegen/test_mock_generator.js';\nimport { generateControllerRegistration } from '../codegen/controller_registration_generator.js';\nimport { generateRuntime } from '../codegen/runtime_generator.js';\nimport { generateRendererApp } from '../codegen/renderer_app_generator.js';\nimport { BackendBridge } from '../bridge/backend_bridge.js';\nimport { BackendWatcher } from './backend_watcher.js';\nimport { YamlWatcher } from './yaml_watcher.js';\nimport { EsbuildBackend } from './esbuild_backend.js';\nimport type { WoodConfig } from '../types/index.js';\n\nconst logger = getLogger('wood:dev');\n\nexport interface DevServerOptions {\n configPath?: string;\n cwd?: string;\n}\n\nexport class WoodDevServer {\n private config: WoodConfig | null = null;\n private bridge: BackendBridge | null = null;\n private backendWatcher: BackendWatcher | null = null;\n private yamlWatcher: YamlWatcher | null = null;\n private esbuild: EsbuildBackend | null = null;\n private cwd: string;\n\n constructor(options: DevServerOptions = {}) {\n this.cwd = options.cwd ?? process.cwd();\n }\n\n /**\n * Start the Wood development server.\n *\n * Automatically sets process.env.WOOD_MODE = 'development' so the app\n * can conditionally use dynamic controller imports (dev) vs pre-registered\n * controllers from the generated file (production).\n *\n * Steps:\n * 1. Parse config\n * 2. Parse + stitch YAML\n * 3. Generate preload, types, mock\n * 4. Build backend with esbuild\n * 5. (electron-vite would be started here -- omitted for MVP)\n * 6. Spawn backend via bridge\n * 7. Start watchers\n */\n async start(): Promise<void> {\n process.env.WOOD_MODE = 'development';\n\n // 1. Parse config\n const configPath = path.resolve(this.cwd, 'wood.config.yml');\n this.config = parseConfig(configPath);\n logger.info('config loaded', { outDir: this.config.outDir });\n\n // 2. Parse operations YAML\n const opsYamlPath = path.resolve(this.cwd, this.config.main.openapi);\n const opsContent = readFileSync(opsYamlPath, 'utf-8');\n const operations = parseOperations(opsContent);\n logger.info('operations parsed', { count: operations.size });\n\n // 3. Parse views YAML\n const viewsYamlPath = path.resolve(this.cwd, this.config.renderer.openapi);\n const viewsContent = readFileSync(viewsYamlPath, 'utf-8');\n const views = parseViews(viewsContent);\n logger.info('views parsed', { count: views.length });\n\n // 4. Generate files\n const outDir = path.resolve(this.cwd, this.config.outDir);\n mkdirSync(outDir, { recursive: true });\n\n const preloadCode = generatePreload(operations);\n writeFileSync(path.join(outDir, 'preload.generated.ts'), preloadCode);\n\n const typesCode = generateTypes(operations);\n writeFileSync(path.join(outDir, 'wood.d.ts'), typesCode);\n\n const mockCode = generateTestMock(operations);\n writeFileSync(path.join(outDir, 'wood.mock.ts'), mockCode);\n\n const controllersDir = path.resolve(this.cwd, this.config.main.controllers);\n const controllerRegCode = generateControllerRegistration(operations, {\n controllersDir,\n outDir,\n });\n writeFileSync(path.join(outDir, 'controllers.generated.ts'), controllerRegCode);\n\n const runtimeCode = generateRuntime(operations, {\n controllersDir,\n outDir,\n });\n writeFileSync(path.join(outDir, 'runtime.generated.ts'), runtimeCode);\n\n const rendererComponentDir = path.resolve(this.cwd, this.config.renderer.componentDir);\n const rendererOutDir = path.resolve(this.cwd, this.config.renderer.componentDir, 'generated');\n mkdirSync(rendererOutDir, { recursive: true });\n\n writeFileSync(path.join(rendererOutDir, 'wood.d.ts'), typesCode);\n\n writeFileSync(\n path.join(rendererOutDir, 'app.ts'),\n generateRendererApp(views, {\n componentDir: rendererComponentDir,\n outDir: rendererOutDir,\n entryViewKey: this.config.renderer.entry,\n }),\n );\n\n logger.info('codegen complete', { outDir });\n\n // 5. Build backend with esbuild (initial build + create incremental context)\n const backendEntry = this.config.main.entry;\n const backendOutfile = path.join(this.config.outDir, 'backend', 'index.js');\n\n this.esbuild = new EsbuildBackend({\n entryPoint: backendEntry,\n outfile: backendOutfile,\n cwd: this.cwd,\n external: this.config.build ? [] : undefined,\n });\n\n await this.esbuild.build();\n await this.esbuild.createContext();\n\n // 6. Start backend bridge (split-serve)\n if (this.config.dev?.splitServe !== false) {\n const resolvedBackendEntry = path.resolve(this.cwd, backendOutfile);\n this.bridge = new BackendBridge({\n backendEntry: resolvedBackendEntry,\n onReady: () => {\n logger.info('backend ready (split-serve)');\n },\n onPush: (channel, data) => {\n logger.debug('backend push', { channel, data });\n },\n onExit: (code, signal) => {\n logger.debug('backend exited', { code, signal });\n },\n });\n await this.bridge.start();\n }\n\n // 7. Start watchers\n if (this.config.dev?.watch !== false) {\n this.startWatchers(outDir);\n }\n }\n\n /** Stop the dev server and clean up all resources. */\n async stop(): Promise<void> {\n this.backendWatcher?.stop();\n this.yamlWatcher?.stop();\n\n if (this.bridge) {\n await this.bridge.stop();\n logger.info('backend bridge stopped');\n }\n\n if (this.esbuild) {\n await this.esbuild.dispose();\n }\n }\n\n /** Get the parsed config. */\n getConfig(): WoodConfig | null {\n return this.config;\n }\n\n private startWatchers(outDir: string): void {\n if (!this.config) return;\n\n // Backend watcher: server + middleware files -> rebuild + restart\n if (this.bridge && this.esbuild) {\n const backendPatterns = [\n `${this.config.main.controllers}/**/*.ts`,\n `${this.config.main.middleware}/**/*.ts`,\n `${this.config.main.entry}`,\n ...(this.config.main.watch ?? []),\n ];\n\n const esbuild = this.esbuild;\n this.backendWatcher = new BackendWatcher({\n patterns: backendPatterns,\n bridge: this.bridge,\n buildFn: () => esbuild.rebuild().then(() => {}),\n cwd: this.cwd,\n });\n this.backendWatcher.start();\n logger.info('backend watcher started', { patterns: backendPatterns });\n }\n\n // YAML watcher: operations + views YAML -> regenerate codegen\n this.yamlWatcher = new YamlWatcher({\n operationsYamlPath: this.config.main.openapi,\n viewsYamlPath: this.config.renderer.openapi,\n controllersDir: this.config.main.controllers,\n outDir,\n rendererOutDir: path.resolve(this.cwd, this.config.renderer.componentDir, 'generated'),\n cwd: this.cwd,\n onViewsChange: async () => {\n // Re-parse views and regenerate client router manifest\n const viewsContent = readFileSync(\n path.resolve(this.cwd, this.config!.renderer.openapi),\n 'utf-8',\n );\n const views = parseViews(viewsContent);\n\n const rendererComponentDir = path.resolve(this.cwd, this.config!.renderer.componentDir);\n const rendererOutDir = path.resolve(this.cwd, this.config!.renderer.componentDir, 'generated');\n mkdirSync(rendererOutDir, { recursive: true });\n\n writeFileSync(\n path.join(rendererOutDir, 'app.ts'),\n generateRendererApp(views, {\n componentDir: rendererComponentDir,\n outDir: rendererOutDir,\n entryViewKey: this.config!.renderer.entry,\n }),\n );\n\n logger.info('views renderer codegen regenerated', { rendererOutDir });\n },\n });\n this.yamlWatcher.start();\n logger.info('yaml watcher started');\n }\n}\n"],"mappings":"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","names":[]}
@@ -95,14 +95,21 @@ class YamlWatcher {
95
95
  "utf-8"
96
96
  );
97
97
  const operations = (0, import_operations_parser.parseOperations)(opsContent);
98
+ const typesCode = (0, import_type_generator.generateTypes)(operations);
98
99
  (0, import_fs.writeFileSync)(
99
100
  import_path.default.join(this.options.outDir, "preload.generated.ts"),
100
101
  (0, import_preload_generator.generatePreload)(operations)
101
102
  );
102
103
  (0, import_fs.writeFileSync)(
103
104
  import_path.default.join(this.options.outDir, "wood.d.ts"),
104
- (0, import_type_generator.generateTypes)(operations)
105
+ typesCode
105
106
  );
107
+ if (this.options.rendererOutDir) {
108
+ (0, import_fs.writeFileSync)(
109
+ import_path.default.join(this.options.rendererOutDir, "wood.d.ts"),
110
+ typesCode
111
+ );
112
+ }
106
113
  (0, import_fs.writeFileSync)(
107
114
  import_path.default.join(this.options.outDir, "wood.mock.ts"),
108
115
  (0, import_test_mock_generator.generateTestMock)(operations)
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/dev/yaml_watcher.ts"],"sourcesContent":["import { watch, type FSWatcher } from 'chokidar';\nimport { Subject, debounceTime, switchMap, from } from 'rxjs';\nimport { readFileSync, writeFileSync } from 'fs';\nimport path from 'path';\nimport { getLogger } from '@noego/logger';\nimport { parseOperations } from '../parser/operations_parser.cjs';\nimport { generatePreload } from '../codegen/preload_generator.cjs';\nimport { generateTypes } from '../codegen/type_generator.cjs';\nimport { generateTestMock } from '../codegen/test_mock_generator.cjs';\nimport { generateControllerRegistration } from '../codegen/controller_registration_generator.cjs';\nimport { generateRuntime } from '../codegen/runtime_generator.cjs';\n\nconst logger = getLogger('wood:dev:yaml-watcher');\n\nexport interface YamlWatcherOptions {\n operationsYamlPath: string;\n viewsYamlPath: string;\n controllersDir: string;\n outDir: string;\n cwd: string;\n /** Called after views YAML changes to regenerate the client router manifest. */\n onViewsChange?: () => Promise<void>;\n}\n\nexport class YamlWatcher {\n private watcher: FSWatcher | null = null;\n private change$ = new Subject<string>();\n\n constructor(private options: YamlWatcherOptions) {}\n\n start(): void {\n const patterns = [\n this.options.operationsYamlPath,\n this.options.viewsYamlPath,\n path.dirname(this.options.operationsYamlPath) + '/**/*.yaml',\n path.dirname(this.options.operationsYamlPath) + '/**/*.yml',\n path.dirname(this.options.viewsYamlPath) + '/**/*.yaml',\n path.dirname(this.options.viewsYamlPath) + '/**/*.yml',\n ];\n\n this.watcher = watch(patterns, {\n cwd: this.options.cwd,\n ignoreInitial: true,\n });\n\n this.watcher.on('change', (filePath: string) => {\n logger.debug('yaml file changed', { filePath });\n this.change$.next(filePath);\n });\n\n this.change$.pipe(\n debounceTime(100),\n switchMap((filePath) => from(this.regenerate(filePath))),\n ).subscribe({\n error: (err) => {\n logger.error('yaml watcher pipeline error', { error: String(err) });\n },\n });\n }\n\n stop(): void {\n this.watcher?.close();\n this.change$.complete();\n }\n\n private isOperationsYaml(filePath: string): boolean {\n const opsDir = path.dirname(this.options.operationsYamlPath);\n return filePath.startsWith(opsDir) || filePath === this.options.operationsYamlPath;\n }\n\n private isViewsYaml(filePath: string): boolean {\n const viewsDir = path.dirname(this.options.viewsYamlPath);\n return filePath.startsWith(viewsDir) || filePath === this.options.viewsYamlPath;\n }\n\n private async regenerate(filePath: string): Promise<void> {\n try {\n if (this.isOperationsYaml(filePath)) {\n // Operations change -> regenerate preload + types + mock\n const opsContent = readFileSync(\n path.resolve(this.options.cwd, this.options.operationsYamlPath),\n 'utf-8',\n );\n const operations = parseOperations(opsContent);\n\n writeFileSync(\n path.join(this.options.outDir, 'preload.generated.ts'),\n generatePreload(operations),\n );\n writeFileSync(\n path.join(this.options.outDir, 'wood.d.ts'),\n generateTypes(operations),\n );\n writeFileSync(\n path.join(this.options.outDir, 'wood.mock.ts'),\n generateTestMock(operations),\n );\n\n const controllersDir = path.resolve(this.options.cwd, this.options.controllersDir);\n writeFileSync(\n path.join(this.options.outDir, 'controllers.generated.ts'),\n generateControllerRegistration(operations, {\n controllersDir,\n outDir: this.options.outDir,\n }),\n );\n\n writeFileSync(\n path.join(this.options.outDir, 'runtime.generated.ts'),\n generateRuntime(operations, {\n controllersDir,\n outDir: this.options.outDir,\n }),\n );\n\n logger.info('operations codegen regenerated');\n }\n\n if (this.isViewsYaml(filePath)) {\n // Views change -> regenerate client router manifest\n if (this.options.onViewsChange) {\n await this.options.onViewsChange();\n }\n\n logger.info('views manifest regenerated');\n }\n } catch (err) {\n logger.error('yaml regeneration failed', { error: String(err) });\n }\n }\n}\n"],"mappings":"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","names":["path"]}
1
+ {"version":3,"sources":["../../src/dev/yaml_watcher.ts"],"sourcesContent":["import { watch, type FSWatcher } from 'chokidar';\nimport { Subject, debounceTime, switchMap, from } from 'rxjs';\nimport { readFileSync, writeFileSync } from 'fs';\nimport path from 'path';\nimport { getLogger } from '@noego/logger';\nimport { parseOperations } from '../parser/operations_parser.cjs';\nimport { generatePreload } from '../codegen/preload_generator.cjs';\nimport { generateTypes } from '../codegen/type_generator.cjs';\nimport { generateTestMock } from '../codegen/test_mock_generator.cjs';\nimport { generateControllerRegistration } from '../codegen/controller_registration_generator.cjs';\nimport { generateRuntime } from '../codegen/runtime_generator.cjs';\n\nconst logger = getLogger('wood:dev:yaml-watcher');\n\nexport interface YamlWatcherOptions {\n operationsYamlPath: string;\n viewsYamlPath: string;\n controllersDir: string;\n outDir: string;\n rendererOutDir?: string;\n cwd: string;\n /** Called after views YAML changes to regenerate the client router manifest. */\n onViewsChange?: () => Promise<void>;\n}\n\nexport class YamlWatcher {\n private watcher: FSWatcher | null = null;\n private change$ = new Subject<string>();\n\n constructor(private options: YamlWatcherOptions) {}\n\n start(): void {\n const patterns = [\n this.options.operationsYamlPath,\n this.options.viewsYamlPath,\n path.dirname(this.options.operationsYamlPath) + '/**/*.yaml',\n path.dirname(this.options.operationsYamlPath) + '/**/*.yml',\n path.dirname(this.options.viewsYamlPath) + '/**/*.yaml',\n path.dirname(this.options.viewsYamlPath) + '/**/*.yml',\n ];\n\n this.watcher = watch(patterns, {\n cwd: this.options.cwd,\n ignoreInitial: true,\n });\n\n this.watcher.on('change', (filePath: string) => {\n logger.debug('yaml file changed', { filePath });\n this.change$.next(filePath);\n });\n\n this.change$.pipe(\n debounceTime(100),\n switchMap((filePath) => from(this.regenerate(filePath))),\n ).subscribe({\n error: (err) => {\n logger.error('yaml watcher pipeline error', { error: String(err) });\n },\n });\n }\n\n stop(): void {\n this.watcher?.close();\n this.change$.complete();\n }\n\n private isOperationsYaml(filePath: string): boolean {\n const opsDir = path.dirname(this.options.operationsYamlPath);\n return filePath.startsWith(opsDir) || filePath === this.options.operationsYamlPath;\n }\n\n private isViewsYaml(filePath: string): boolean {\n const viewsDir = path.dirname(this.options.viewsYamlPath);\n return filePath.startsWith(viewsDir) || filePath === this.options.viewsYamlPath;\n }\n\n private async regenerate(filePath: string): Promise<void> {\n try {\n if (this.isOperationsYaml(filePath)) {\n // Operations change -> regenerate preload + types + mock\n const opsContent = readFileSync(\n path.resolve(this.options.cwd, this.options.operationsYamlPath),\n 'utf-8',\n );\n const operations = parseOperations(opsContent);\n const typesCode = generateTypes(operations);\n\n writeFileSync(\n path.join(this.options.outDir, 'preload.generated.ts'),\n generatePreload(operations),\n );\n writeFileSync(\n path.join(this.options.outDir, 'wood.d.ts'),\n typesCode,\n );\n if (this.options.rendererOutDir) {\n writeFileSync(\n path.join(this.options.rendererOutDir, 'wood.d.ts'),\n typesCode,\n );\n }\n writeFileSync(\n path.join(this.options.outDir, 'wood.mock.ts'),\n generateTestMock(operations),\n );\n\n const controllersDir = path.resolve(this.options.cwd, this.options.controllersDir);\n writeFileSync(\n path.join(this.options.outDir, 'controllers.generated.ts'),\n generateControllerRegistration(operations, {\n controllersDir,\n outDir: this.options.outDir,\n }),\n );\n\n writeFileSync(\n path.join(this.options.outDir, 'runtime.generated.ts'),\n generateRuntime(operations, {\n controllersDir,\n outDir: this.options.outDir,\n }),\n );\n\n logger.info('operations codegen regenerated');\n }\n\n if (this.isViewsYaml(filePath)) {\n // Views change -> regenerate client router manifest\n if (this.options.onViewsChange) {\n await this.options.onViewsChange();\n }\n\n logger.info('views manifest regenerated');\n }\n } catch (err) {\n logger.error('yaml regeneration failed', { error: String(err) });\n }\n }\n}\n"],"mappings":"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","names":["path"]}
@@ -3,6 +3,7 @@ interface YamlWatcherOptions {
3
3
  viewsYamlPath: string;
4
4
  controllersDir: string;
5
5
  outDir: string;
6
+ rendererOutDir?: string;
6
7
  cwd: string;
7
8
  /** Called after views YAML changes to regenerate the client router manifest. */
8
9
  onViewsChange?: () => Promise<void>;
@@ -3,6 +3,7 @@ interface YamlWatcherOptions {
3
3
  viewsYamlPath: string;
4
4
  controllersDir: string;
5
5
  outDir: string;
6
+ rendererOutDir?: string;
6
7
  cwd: string;
7
8
  /** Called after views YAML changes to regenerate the client router manifest. */
8
9
  onViewsChange?: () => Promise<void>;
@@ -62,14 +62,21 @@ class YamlWatcher {
62
62
  "utf-8"
63
63
  );
64
64
  const operations = parseOperations(opsContent);
65
+ const typesCode = generateTypes(operations);
65
66
  writeFileSync(
66
67
  path.join(this.options.outDir, "preload.generated.ts"),
67
68
  generatePreload(operations)
68
69
  );
69
70
  writeFileSync(
70
71
  path.join(this.options.outDir, "wood.d.ts"),
71
- generateTypes(operations)
72
+ typesCode
72
73
  );
74
+ if (this.options.rendererOutDir) {
75
+ writeFileSync(
76
+ path.join(this.options.rendererOutDir, "wood.d.ts"),
77
+ typesCode
78
+ );
79
+ }
73
80
  writeFileSync(
74
81
  path.join(this.options.outDir, "wood.mock.ts"),
75
82
  generateTestMock(operations)
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/dev/yaml_watcher.ts"],"sourcesContent":["import { watch, type FSWatcher } from 'chokidar';\nimport { Subject, debounceTime, switchMap, from } from 'rxjs';\nimport { readFileSync, writeFileSync } from 'fs';\nimport path from 'path';\nimport { getLogger } from '@noego/logger';\nimport { parseOperations } from '../parser/operations_parser.js';\nimport { generatePreload } from '../codegen/preload_generator.js';\nimport { generateTypes } from '../codegen/type_generator.js';\nimport { generateTestMock } from '../codegen/test_mock_generator.js';\nimport { generateControllerRegistration } from '../codegen/controller_registration_generator.js';\nimport { generateRuntime } from '../codegen/runtime_generator.js';\n\nconst logger = getLogger('wood:dev:yaml-watcher');\n\nexport interface YamlWatcherOptions {\n operationsYamlPath: string;\n viewsYamlPath: string;\n controllersDir: string;\n outDir: string;\n cwd: string;\n /** Called after views YAML changes to regenerate the client router manifest. */\n onViewsChange?: () => Promise<void>;\n}\n\nexport class YamlWatcher {\n private watcher: FSWatcher | null = null;\n private change$ = new Subject<string>();\n\n constructor(private options: YamlWatcherOptions) {}\n\n start(): void {\n const patterns = [\n this.options.operationsYamlPath,\n this.options.viewsYamlPath,\n path.dirname(this.options.operationsYamlPath) + '/**/*.yaml',\n path.dirname(this.options.operationsYamlPath) + '/**/*.yml',\n path.dirname(this.options.viewsYamlPath) + '/**/*.yaml',\n path.dirname(this.options.viewsYamlPath) + '/**/*.yml',\n ];\n\n this.watcher = watch(patterns, {\n cwd: this.options.cwd,\n ignoreInitial: true,\n });\n\n this.watcher.on('change', (filePath: string) => {\n logger.debug('yaml file changed', { filePath });\n this.change$.next(filePath);\n });\n\n this.change$.pipe(\n debounceTime(100),\n switchMap((filePath) => from(this.regenerate(filePath))),\n ).subscribe({\n error: (err) => {\n logger.error('yaml watcher pipeline error', { error: String(err) });\n },\n });\n }\n\n stop(): void {\n this.watcher?.close();\n this.change$.complete();\n }\n\n private isOperationsYaml(filePath: string): boolean {\n const opsDir = path.dirname(this.options.operationsYamlPath);\n return filePath.startsWith(opsDir) || filePath === this.options.operationsYamlPath;\n }\n\n private isViewsYaml(filePath: string): boolean {\n const viewsDir = path.dirname(this.options.viewsYamlPath);\n return filePath.startsWith(viewsDir) || filePath === this.options.viewsYamlPath;\n }\n\n private async regenerate(filePath: string): Promise<void> {\n try {\n if (this.isOperationsYaml(filePath)) {\n // Operations change -> regenerate preload + types + mock\n const opsContent = readFileSync(\n path.resolve(this.options.cwd, this.options.operationsYamlPath),\n 'utf-8',\n );\n const operations = parseOperations(opsContent);\n\n writeFileSync(\n path.join(this.options.outDir, 'preload.generated.ts'),\n generatePreload(operations),\n );\n writeFileSync(\n path.join(this.options.outDir, 'wood.d.ts'),\n generateTypes(operations),\n );\n writeFileSync(\n path.join(this.options.outDir, 'wood.mock.ts'),\n generateTestMock(operations),\n );\n\n const controllersDir = path.resolve(this.options.cwd, this.options.controllersDir);\n writeFileSync(\n path.join(this.options.outDir, 'controllers.generated.ts'),\n generateControllerRegistration(operations, {\n controllersDir,\n outDir: this.options.outDir,\n }),\n );\n\n writeFileSync(\n path.join(this.options.outDir, 'runtime.generated.ts'),\n generateRuntime(operations, {\n controllersDir,\n outDir: this.options.outDir,\n }),\n );\n\n logger.info('operations codegen regenerated');\n }\n\n if (this.isViewsYaml(filePath)) {\n // Views change -> regenerate client router manifest\n if (this.options.onViewsChange) {\n await this.options.onViewsChange();\n }\n\n logger.info('views manifest regenerated');\n }\n } catch (err) {\n logger.error('yaml regeneration failed', { error: String(err) });\n }\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/dev/yaml_watcher.ts"],"sourcesContent":["import { watch, type FSWatcher } from 'chokidar';\nimport { Subject, debounceTime, switchMap, from } from 'rxjs';\nimport { readFileSync, writeFileSync } from 'fs';\nimport path from 'path';\nimport { getLogger } from '@noego/logger';\nimport { parseOperations } from '../parser/operations_parser.js';\nimport { generatePreload } from '../codegen/preload_generator.js';\nimport { generateTypes } from '../codegen/type_generator.js';\nimport { generateTestMock } from '../codegen/test_mock_generator.js';\nimport { generateControllerRegistration } from '../codegen/controller_registration_generator.js';\nimport { generateRuntime } from '../codegen/runtime_generator.js';\n\nconst logger = getLogger('wood:dev:yaml-watcher');\n\nexport interface YamlWatcherOptions {\n operationsYamlPath: string;\n viewsYamlPath: string;\n controllersDir: string;\n outDir: string;\n rendererOutDir?: string;\n cwd: string;\n /** Called after views YAML changes to regenerate the client router manifest. */\n onViewsChange?: () => Promise<void>;\n}\n\nexport class YamlWatcher {\n private watcher: FSWatcher | null = null;\n private change$ = new Subject<string>();\n\n constructor(private options: YamlWatcherOptions) {}\n\n start(): void {\n const patterns = [\n this.options.operationsYamlPath,\n this.options.viewsYamlPath,\n path.dirname(this.options.operationsYamlPath) + '/**/*.yaml',\n path.dirname(this.options.operationsYamlPath) + '/**/*.yml',\n path.dirname(this.options.viewsYamlPath) + '/**/*.yaml',\n path.dirname(this.options.viewsYamlPath) + '/**/*.yml',\n ];\n\n this.watcher = watch(patterns, {\n cwd: this.options.cwd,\n ignoreInitial: true,\n });\n\n this.watcher.on('change', (filePath: string) => {\n logger.debug('yaml file changed', { filePath });\n this.change$.next(filePath);\n });\n\n this.change$.pipe(\n debounceTime(100),\n switchMap((filePath) => from(this.regenerate(filePath))),\n ).subscribe({\n error: (err) => {\n logger.error('yaml watcher pipeline error', { error: String(err) });\n },\n });\n }\n\n stop(): void {\n this.watcher?.close();\n this.change$.complete();\n }\n\n private isOperationsYaml(filePath: string): boolean {\n const opsDir = path.dirname(this.options.operationsYamlPath);\n return filePath.startsWith(opsDir) || filePath === this.options.operationsYamlPath;\n }\n\n private isViewsYaml(filePath: string): boolean {\n const viewsDir = path.dirname(this.options.viewsYamlPath);\n return filePath.startsWith(viewsDir) || filePath === this.options.viewsYamlPath;\n }\n\n private async regenerate(filePath: string): Promise<void> {\n try {\n if (this.isOperationsYaml(filePath)) {\n // Operations change -> regenerate preload + types + mock\n const opsContent = readFileSync(\n path.resolve(this.options.cwd, this.options.operationsYamlPath),\n 'utf-8',\n );\n const operations = parseOperations(opsContent);\n const typesCode = generateTypes(operations);\n\n writeFileSync(\n path.join(this.options.outDir, 'preload.generated.ts'),\n generatePreload(operations),\n );\n writeFileSync(\n path.join(this.options.outDir, 'wood.d.ts'),\n typesCode,\n );\n if (this.options.rendererOutDir) {\n writeFileSync(\n path.join(this.options.rendererOutDir, 'wood.d.ts'),\n typesCode,\n );\n }\n writeFileSync(\n path.join(this.options.outDir, 'wood.mock.ts'),\n generateTestMock(operations),\n );\n\n const controllersDir = path.resolve(this.options.cwd, this.options.controllersDir);\n writeFileSync(\n path.join(this.options.outDir, 'controllers.generated.ts'),\n generateControllerRegistration(operations, {\n controllersDir,\n outDir: this.options.outDir,\n }),\n );\n\n writeFileSync(\n path.join(this.options.outDir, 'runtime.generated.ts'),\n generateRuntime(operations, {\n controllersDir,\n outDir: this.options.outDir,\n }),\n );\n\n logger.info('operations codegen regenerated');\n }\n\n if (this.isViewsYaml(filePath)) {\n // Views change -> regenerate client router manifest\n if (this.options.onViewsChange) {\n await this.options.onViewsChange();\n }\n\n logger.info('views manifest regenerated');\n }\n } catch (err) {\n logger.error('yaml regeneration failed', { error: String(err) });\n }\n }\n}\n"],"mappings":"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","names":[]}
@@ -37,9 +37,24 @@ var import_fs = require("fs");
37
37
  function parseOperations(yamlContent) {
38
38
  const doc = import_js_yaml.default.load(yamlContent);
39
39
  const operations = /* @__PURE__ */ new Map();
40
+ if (doc === null || doc === void 0) {
41
+ throw new Error('Operations document is empty: expected a top-level "operations:" or "module:" mapping.');
42
+ }
43
+ if (typeof doc !== "object" || Array.isArray(doc)) {
44
+ throw new Error('Operations document must be a mapping with "operations:" and/or "module:" keys.');
45
+ }
46
+ if (!doc.operations && !doc.module) {
47
+ throw new Error('Operations document defines no operations: expected an "operations:" or "module:" key.');
48
+ }
49
+ const set = (channel, def) => {
50
+ if (operations.has(channel)) {
51
+ throw new Error(`Duplicate operation channel "${channel}": each channel may only be defined once.`);
52
+ }
53
+ operations.set(channel, def);
54
+ };
40
55
  if (doc.operations) {
41
56
  for (const [channel, entry] of Object.entries(doc.operations)) {
42
- operations.set(channel, {
57
+ set(channel, {
43
58
  channel,
44
59
  controller: entry.controller,
45
60
  action: entry.action,
@@ -53,6 +68,12 @@ function parseOperations(yamlContent) {
53
68
  for (const [moduleName, moduleDef] of Object.entries(doc.module)) {
54
69
  const moduleController = moduleDef.controller;
55
70
  const moduleMiddleware = moduleDef.middleware ?? [];
71
+ if (moduleDef.operations === null || moduleDef.operations === void 0) {
72
+ throw new Error(`Module "${moduleName}" has no "operations:" block.`);
73
+ }
74
+ if (typeof moduleDef.operations !== "object" || Array.isArray(moduleDef.operations)) {
75
+ throw new Error(`Module "${moduleName}" "operations:" must be a mapping of operation names.`);
76
+ }
56
77
  for (const [opName, opEntry] of Object.entries(moduleDef.operations)) {
57
78
  const channel = `${moduleName}.${opName}`;
58
79
  let middleware;
@@ -61,7 +82,7 @@ function parseOperations(yamlContent) {
61
82
  } else {
62
83
  middleware = [...moduleMiddleware, ...opEntry.middleware ?? []];
63
84
  }
64
- operations.set(channel, {
85
+ set(channel, {
65
86
  channel,
66
87
  controller: opEntry.controller ?? moduleController ?? "",
67
88
  action: opEntry.action,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/parser/operations_parser.ts"],"sourcesContent":["import yaml from 'js-yaml';\nimport { readFileSync } from 'fs';\nimport type { OperationDef, RawOperationsDocument, RawModuleDef, RawOperationEntry, RawModuleOperationEntry } from '../types/index.cjs';\n\nexport function parseOperations(yamlContent: string): Map<string, OperationDef> {\n const doc = yaml.load(yamlContent) as RawOperationsDocument;\n const operations = new Map<string, OperationDef>();\n\n // 1. Parse flat operations (top-level `operations:` key)\n if (doc.operations) {\n for (const [channel, entry] of Object.entries(doc.operations)) {\n operations.set(channel, {\n channel,\n controller: entry.controller,\n action: entry.action,\n middleware: entry.middleware ?? [],\n inputSchema: entry.input,\n outputSchema: entry.output,\n });\n }\n }\n\n // 2. Parse module operations (`module:` key)\n if (doc.module) {\n for (const [moduleName, moduleDef] of Object.entries(doc.module)) {\n const moduleController = moduleDef.controller;\n const moduleMiddleware = moduleDef.middleware ?? [];\n\n for (const [opName, opEntry] of Object.entries(moduleDef.operations)) {\n const channel = `${moduleName}.${opName}`;\n\n // Middleware: if operation explicitly sets middleware to [] or null, clear inherited\n // Otherwise, prepend module middleware to operation middleware\n let middleware: string[];\n if (opEntry.middleware === null || (Array.isArray(opEntry.middleware) && opEntry.middleware.length === 0)) {\n middleware = [];\n } else {\n middleware = [...moduleMiddleware, ...(opEntry.middleware ?? [])];\n }\n\n operations.set(channel, {\n channel,\n controller: opEntry.controller ?? moduleController ?? '',\n action: opEntry.action,\n middleware,\n inputSchema: opEntry.input,\n outputSchema: opEntry.output,\n });\n }\n }\n }\n\n // 3. Validate: every operation must have a controller and action\n for (const [channel, op] of operations) {\n if (!op.controller) {\n throw new Error(`Operation \"${channel}\" has no controller defined`);\n }\n if (!op.action) {\n throw new Error(`Operation \"${channel}\" has no action defined`);\n }\n }\n\n return operations;\n}\n\n/** Parse from file path */\nexport function parseOperationsFile(filePath: string): Map<string, OperationDef> {\n const content = readFileSync(filePath, 'utf-8');\n return parseOperations(content);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,qBAAiB;AACjB,gBAA6B;AAGtB,SAAS,gBAAgB,aAAgD;AAC9E,QAAM,MAAM,eAAAA,QAAK,KAAK,WAAW;AACjC,QAAM,aAAa,oBAAI,IAA0B;AAGjD,MAAI,IAAI,YAAY;AAClB,eAAW,CAAC,SAAS,KAAK,KAAK,OAAO,QAAQ,IAAI,UAAU,GAAG;AAC7D,iBAAW,IAAI,SAAS;AAAA,QACtB;AAAA,QACA,YAAY,MAAM;AAAA,QAClB,QAAQ,MAAM;AAAA,QACd,YAAY,MAAM,cAAc,CAAC;AAAA,QACjC,aAAa,MAAM;AAAA,QACnB,cAAc,MAAM;AAAA,MACtB,CAAC;AAAA,IACH;AAAA,EACF;AAGA,MAAI,IAAI,QAAQ;AACd,eAAW,CAAC,YAAY,SAAS,KAAK,OAAO,QAAQ,IAAI,MAAM,GAAG;AAChE,YAAM,mBAAmB,UAAU;AACnC,YAAM,mBAAmB,UAAU,cAAc,CAAC;AAElD,iBAAW,CAAC,QAAQ,OAAO,KAAK,OAAO,QAAQ,UAAU,UAAU,GAAG;AACpE,cAAM,UAAU,GAAG,UAAU,IAAI,MAAM;AAIvC,YAAI;AACJ,YAAI,QAAQ,eAAe,QAAS,MAAM,QAAQ,QAAQ,UAAU,KAAK,QAAQ,WAAW,WAAW,GAAI;AACzG,uBAAa,CAAC;AAAA,QAChB,OAAO;AACL,uBAAa,CAAC,GAAG,kBAAkB,GAAI,QAAQ,cAAc,CAAC,CAAE;AAAA,QAClE;AAEA,mBAAW,IAAI,SAAS;AAAA,UACtB;AAAA,UACA,YAAY,QAAQ,cAAc,oBAAoB;AAAA,UACtD,QAAQ,QAAQ;AAAA,UAChB;AAAA,UACA,aAAa,QAAQ;AAAA,UACrB,cAAc,QAAQ;AAAA,QACxB,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAGA,aAAW,CAAC,SAAS,EAAE,KAAK,YAAY;AACtC,QAAI,CAAC,GAAG,YAAY;AAClB,YAAM,IAAI,MAAM,cAAc,OAAO,6BAA6B;AAAA,IACpE;AACA,QAAI,CAAC,GAAG,QAAQ;AACd,YAAM,IAAI,MAAM,cAAc,OAAO,yBAAyB;AAAA,IAChE;AAAA,EACF;AAEA,SAAO;AACT;AAGO,SAAS,oBAAoB,UAA6C;AAC/E,QAAM,cAAU,wBAAa,UAAU,OAAO;AAC9C,SAAO,gBAAgB,OAAO;AAChC;","names":["yaml"]}
1
+ {"version":3,"sources":["../../src/parser/operations_parser.ts"],"sourcesContent":["import yaml from 'js-yaml';\nimport { readFileSync } from 'fs';\nimport type { OperationDef, RawOperationsDocument, RawModuleDef, RawOperationEntry, RawModuleOperationEntry } from '../types/index.cjs';\n\nexport function parseOperations(yamlContent: string): Map<string, OperationDef> {\n const doc = yaml.load(yamlContent) as RawOperationsDocument;\n const operations = new Map<string, OperationDef>();\n\n if (doc === null || doc === undefined) {\n throw new Error('Operations document is empty: expected a top-level \"operations:\" or \"module:\" mapping.');\n }\n if (typeof doc !== 'object' || Array.isArray(doc)) {\n throw new Error('Operations document must be a mapping with \"operations:\" and/or \"module:\" keys.');\n }\n if (!doc.operations && !doc.module) {\n throw new Error('Operations document defines no operations: expected an \"operations:\" or \"module:\" key.');\n }\n\n // A channel may only be defined once. Two operations sharing a channel would\n // silently overwrite each other (last-wins), dropping an RPC with no warning.\n const set = (channel: string, def: OperationDef): void => {\n if (operations.has(channel)) {\n throw new Error(`Duplicate operation channel \"${channel}\": each channel may only be defined once.`);\n }\n operations.set(channel, def);\n };\n\n // 1. Parse flat operations (top-level `operations:` key)\n if (doc.operations) {\n for (const [channel, entry] of Object.entries(doc.operations)) {\n set(channel, {\n channel,\n controller: entry.controller,\n action: entry.action,\n middleware: entry.middleware ?? [],\n inputSchema: entry.input,\n outputSchema: entry.output,\n });\n }\n }\n\n // 2. Parse module operations (`module:` key)\n if (doc.module) {\n for (const [moduleName, moduleDef] of Object.entries(doc.module)) {\n const moduleController = moduleDef.controller;\n const moduleMiddleware = moduleDef.middleware ?? [];\n\n if (moduleDef.operations === null || moduleDef.operations === undefined) {\n throw new Error(`Module \"${moduleName}\" has no \"operations:\" block.`);\n }\n if (typeof moduleDef.operations !== 'object' || Array.isArray(moduleDef.operations)) {\n throw new Error(`Module \"${moduleName}\" \"operations:\" must be a mapping of operation names.`);\n }\n\n for (const [opName, opEntry] of Object.entries(moduleDef.operations)) {\n const channel = `${moduleName}.${opName}`;\n\n // Middleware: if operation explicitly sets middleware to [] or null, clear inherited\n // Otherwise, prepend module middleware to operation middleware\n let middleware: string[];\n if (opEntry.middleware === null || (Array.isArray(opEntry.middleware) && opEntry.middleware.length === 0)) {\n middleware = [];\n } else {\n middleware = [...moduleMiddleware, ...(opEntry.middleware ?? [])];\n }\n\n set(channel, {\n channel,\n controller: opEntry.controller ?? moduleController ?? '',\n action: opEntry.action,\n middleware,\n inputSchema: opEntry.input,\n outputSchema: opEntry.output,\n });\n }\n }\n }\n\n // 3. Validate: every operation must have a controller and action\n for (const [channel, op] of operations) {\n if (!op.controller) {\n throw new Error(`Operation \"${channel}\" has no controller defined`);\n }\n if (!op.action) {\n throw new Error(`Operation \"${channel}\" has no action defined`);\n }\n }\n\n return operations;\n}\n\n/** Parse from file path */\nexport function parseOperationsFile(filePath: string): Map<string, OperationDef> {\n const content = readFileSync(filePath, 'utf-8');\n return parseOperations(content);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,qBAAiB;AACjB,gBAA6B;AAGtB,SAAS,gBAAgB,aAAgD;AAC9E,QAAM,MAAM,eAAAA,QAAK,KAAK,WAAW;AACjC,QAAM,aAAa,oBAAI,IAA0B;AAEjD,MAAI,QAAQ,QAAQ,QAAQ,QAAW;AACrC,UAAM,IAAI,MAAM,wFAAwF;AAAA,EAC1G;AACA,MAAI,OAAO,QAAQ,YAAY,MAAM,QAAQ,GAAG,GAAG;AACjD,UAAM,IAAI,MAAM,iFAAiF;AAAA,EACnG;AACA,MAAI,CAAC,IAAI,cAAc,CAAC,IAAI,QAAQ;AAClC,UAAM,IAAI,MAAM,wFAAwF;AAAA,EAC1G;AAIA,QAAM,MAAM,CAAC,SAAiB,QAA4B;AACxD,QAAI,WAAW,IAAI,OAAO,GAAG;AAC3B,YAAM,IAAI,MAAM,gCAAgC,OAAO,2CAA2C;AAAA,IACpG;AACA,eAAW,IAAI,SAAS,GAAG;AAAA,EAC7B;AAGA,MAAI,IAAI,YAAY;AAClB,eAAW,CAAC,SAAS,KAAK,KAAK,OAAO,QAAQ,IAAI,UAAU,GAAG;AAC7D,UAAI,SAAS;AAAA,QACX;AAAA,QACA,YAAY,MAAM;AAAA,QAClB,QAAQ,MAAM;AAAA,QACd,YAAY,MAAM,cAAc,CAAC;AAAA,QACjC,aAAa,MAAM;AAAA,QACnB,cAAc,MAAM;AAAA,MACtB,CAAC;AAAA,IACH;AAAA,EACF;AAGA,MAAI,IAAI,QAAQ;AACd,eAAW,CAAC,YAAY,SAAS,KAAK,OAAO,QAAQ,IAAI,MAAM,GAAG;AAChE,YAAM,mBAAmB,UAAU;AACnC,YAAM,mBAAmB,UAAU,cAAc,CAAC;AAElD,UAAI,UAAU,eAAe,QAAQ,UAAU,eAAe,QAAW;AACvE,cAAM,IAAI,MAAM,WAAW,UAAU,+BAA+B;AAAA,MACtE;AACA,UAAI,OAAO,UAAU,eAAe,YAAY,MAAM,QAAQ,UAAU,UAAU,GAAG;AACnF,cAAM,IAAI,MAAM,WAAW,UAAU,uDAAuD;AAAA,MAC9F;AAEA,iBAAW,CAAC,QAAQ,OAAO,KAAK,OAAO,QAAQ,UAAU,UAAU,GAAG;AACpE,cAAM,UAAU,GAAG,UAAU,IAAI,MAAM;AAIvC,YAAI;AACJ,YAAI,QAAQ,eAAe,QAAS,MAAM,QAAQ,QAAQ,UAAU,KAAK,QAAQ,WAAW,WAAW,GAAI;AACzG,uBAAa,CAAC;AAAA,QAChB,OAAO;AACL,uBAAa,CAAC,GAAG,kBAAkB,GAAI,QAAQ,cAAc,CAAC,CAAE;AAAA,QAClE;AAEA,YAAI,SAAS;AAAA,UACX;AAAA,UACA,YAAY,QAAQ,cAAc,oBAAoB;AAAA,UACtD,QAAQ,QAAQ;AAAA,UAChB;AAAA,UACA,aAAa,QAAQ;AAAA,UACrB,cAAc,QAAQ;AAAA,QACxB,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAGA,aAAW,CAAC,SAAS,EAAE,KAAK,YAAY;AACtC,QAAI,CAAC,GAAG,YAAY;AAClB,YAAM,IAAI,MAAM,cAAc,OAAO,6BAA6B;AAAA,IACpE;AACA,QAAI,CAAC,GAAG,QAAQ;AACd,YAAM,IAAI,MAAM,cAAc,OAAO,yBAAyB;AAAA,IAChE;AAAA,EACF;AAEA,SAAO;AACT;AAGO,SAAS,oBAAoB,UAA6C;AAC/E,QAAM,cAAU,wBAAa,UAAU,OAAO;AAC9C,SAAO,gBAAgB,OAAO;AAChC;","names":["yaml"]}
@@ -3,9 +3,24 @@ import { readFileSync } from "fs";
3
3
  function parseOperations(yamlContent) {
4
4
  const doc = yaml.load(yamlContent);
5
5
  const operations = /* @__PURE__ */ new Map();
6
+ if (doc === null || doc === void 0) {
7
+ throw new Error('Operations document is empty: expected a top-level "operations:" or "module:" mapping.');
8
+ }
9
+ if (typeof doc !== "object" || Array.isArray(doc)) {
10
+ throw new Error('Operations document must be a mapping with "operations:" and/or "module:" keys.');
11
+ }
12
+ if (!doc.operations && !doc.module) {
13
+ throw new Error('Operations document defines no operations: expected an "operations:" or "module:" key.');
14
+ }
15
+ const set = (channel, def) => {
16
+ if (operations.has(channel)) {
17
+ throw new Error(`Duplicate operation channel "${channel}": each channel may only be defined once.`);
18
+ }
19
+ operations.set(channel, def);
20
+ };
6
21
  if (doc.operations) {
7
22
  for (const [channel, entry] of Object.entries(doc.operations)) {
8
- operations.set(channel, {
23
+ set(channel, {
9
24
  channel,
10
25
  controller: entry.controller,
11
26
  action: entry.action,
@@ -19,6 +34,12 @@ function parseOperations(yamlContent) {
19
34
  for (const [moduleName, moduleDef] of Object.entries(doc.module)) {
20
35
  const moduleController = moduleDef.controller;
21
36
  const moduleMiddleware = moduleDef.middleware ?? [];
37
+ if (moduleDef.operations === null || moduleDef.operations === void 0) {
38
+ throw new Error(`Module "${moduleName}" has no "operations:" block.`);
39
+ }
40
+ if (typeof moduleDef.operations !== "object" || Array.isArray(moduleDef.operations)) {
41
+ throw new Error(`Module "${moduleName}" "operations:" must be a mapping of operation names.`);
42
+ }
22
43
  for (const [opName, opEntry] of Object.entries(moduleDef.operations)) {
23
44
  const channel = `${moduleName}.${opName}`;
24
45
  let middleware;
@@ -27,7 +48,7 @@ function parseOperations(yamlContent) {
27
48
  } else {
28
49
  middleware = [...moduleMiddleware, ...opEntry.middleware ?? []];
29
50
  }
30
- operations.set(channel, {
51
+ set(channel, {
31
52
  channel,
32
53
  controller: opEntry.controller ?? moduleController ?? "",
33
54
  action: opEntry.action,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/parser/operations_parser.ts"],"sourcesContent":["import yaml from 'js-yaml';\nimport { readFileSync } from 'fs';\nimport type { OperationDef, RawOperationsDocument, RawModuleDef, RawOperationEntry, RawModuleOperationEntry } from '../types/index.js';\n\nexport function parseOperations(yamlContent: string): Map<string, OperationDef> {\n const doc = yaml.load(yamlContent) as RawOperationsDocument;\n const operations = new Map<string, OperationDef>();\n\n // 1. Parse flat operations (top-level `operations:` key)\n if (doc.operations) {\n for (const [channel, entry] of Object.entries(doc.operations)) {\n operations.set(channel, {\n channel,\n controller: entry.controller,\n action: entry.action,\n middleware: entry.middleware ?? [],\n inputSchema: entry.input,\n outputSchema: entry.output,\n });\n }\n }\n\n // 2. Parse module operations (`module:` key)\n if (doc.module) {\n for (const [moduleName, moduleDef] of Object.entries(doc.module)) {\n const moduleController = moduleDef.controller;\n const moduleMiddleware = moduleDef.middleware ?? [];\n\n for (const [opName, opEntry] of Object.entries(moduleDef.operations)) {\n const channel = `${moduleName}.${opName}`;\n\n // Middleware: if operation explicitly sets middleware to [] or null, clear inherited\n // Otherwise, prepend module middleware to operation middleware\n let middleware: string[];\n if (opEntry.middleware === null || (Array.isArray(opEntry.middleware) && opEntry.middleware.length === 0)) {\n middleware = [];\n } else {\n middleware = [...moduleMiddleware, ...(opEntry.middleware ?? [])];\n }\n\n operations.set(channel, {\n channel,\n controller: opEntry.controller ?? moduleController ?? '',\n action: opEntry.action,\n middleware,\n inputSchema: opEntry.input,\n outputSchema: opEntry.output,\n });\n }\n }\n }\n\n // 3. Validate: every operation must have a controller and action\n for (const [channel, op] of operations) {\n if (!op.controller) {\n throw new Error(`Operation \"${channel}\" has no controller defined`);\n }\n if (!op.action) {\n throw new Error(`Operation \"${channel}\" has no action defined`);\n }\n }\n\n return operations;\n}\n\n/** Parse from file path */\nexport function parseOperationsFile(filePath: string): Map<string, OperationDef> {\n const content = readFileSync(filePath, 'utf-8');\n return parseOperations(content);\n}\n"],"mappings":"AAAA,OAAO,UAAU;AACjB,SAAS,oBAAoB;AAGtB,SAAS,gBAAgB,aAAgD;AAC9E,QAAM,MAAM,KAAK,KAAK,WAAW;AACjC,QAAM,aAAa,oBAAI,IAA0B;AAGjD,MAAI,IAAI,YAAY;AAClB,eAAW,CAAC,SAAS,KAAK,KAAK,OAAO,QAAQ,IAAI,UAAU,GAAG;AAC7D,iBAAW,IAAI,SAAS;AAAA,QACtB;AAAA,QACA,YAAY,MAAM;AAAA,QAClB,QAAQ,MAAM;AAAA,QACd,YAAY,MAAM,cAAc,CAAC;AAAA,QACjC,aAAa,MAAM;AAAA,QACnB,cAAc,MAAM;AAAA,MACtB,CAAC;AAAA,IACH;AAAA,EACF;AAGA,MAAI,IAAI,QAAQ;AACd,eAAW,CAAC,YAAY,SAAS,KAAK,OAAO,QAAQ,IAAI,MAAM,GAAG;AAChE,YAAM,mBAAmB,UAAU;AACnC,YAAM,mBAAmB,UAAU,cAAc,CAAC;AAElD,iBAAW,CAAC,QAAQ,OAAO,KAAK,OAAO,QAAQ,UAAU,UAAU,GAAG;AACpE,cAAM,UAAU,GAAG,UAAU,IAAI,MAAM;AAIvC,YAAI;AACJ,YAAI,QAAQ,eAAe,QAAS,MAAM,QAAQ,QAAQ,UAAU,KAAK,QAAQ,WAAW,WAAW,GAAI;AACzG,uBAAa,CAAC;AAAA,QAChB,OAAO;AACL,uBAAa,CAAC,GAAG,kBAAkB,GAAI,QAAQ,cAAc,CAAC,CAAE;AAAA,QAClE;AAEA,mBAAW,IAAI,SAAS;AAAA,UACtB;AAAA,UACA,YAAY,QAAQ,cAAc,oBAAoB;AAAA,UACtD,QAAQ,QAAQ;AAAA,UAChB;AAAA,UACA,aAAa,QAAQ;AAAA,UACrB,cAAc,QAAQ;AAAA,QACxB,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAGA,aAAW,CAAC,SAAS,EAAE,KAAK,YAAY;AACtC,QAAI,CAAC,GAAG,YAAY;AAClB,YAAM,IAAI,MAAM,cAAc,OAAO,6BAA6B;AAAA,IACpE;AACA,QAAI,CAAC,GAAG,QAAQ;AACd,YAAM,IAAI,MAAM,cAAc,OAAO,yBAAyB;AAAA,IAChE;AAAA,EACF;AAEA,SAAO;AACT;AAGO,SAAS,oBAAoB,UAA6C;AAC/E,QAAM,UAAU,aAAa,UAAU,OAAO;AAC9C,SAAO,gBAAgB,OAAO;AAChC;","names":[]}
1
+ {"version":3,"sources":["../../src/parser/operations_parser.ts"],"sourcesContent":["import yaml from 'js-yaml';\nimport { readFileSync } from 'fs';\nimport type { OperationDef, RawOperationsDocument, RawModuleDef, RawOperationEntry, RawModuleOperationEntry } from '../types/index.js';\n\nexport function parseOperations(yamlContent: string): Map<string, OperationDef> {\n const doc = yaml.load(yamlContent) as RawOperationsDocument;\n const operations = new Map<string, OperationDef>();\n\n if (doc === null || doc === undefined) {\n throw new Error('Operations document is empty: expected a top-level \"operations:\" or \"module:\" mapping.');\n }\n if (typeof doc !== 'object' || Array.isArray(doc)) {\n throw new Error('Operations document must be a mapping with \"operations:\" and/or \"module:\" keys.');\n }\n if (!doc.operations && !doc.module) {\n throw new Error('Operations document defines no operations: expected an \"operations:\" or \"module:\" key.');\n }\n\n // A channel may only be defined once. Two operations sharing a channel would\n // silently overwrite each other (last-wins), dropping an RPC with no warning.\n const set = (channel: string, def: OperationDef): void => {\n if (operations.has(channel)) {\n throw new Error(`Duplicate operation channel \"${channel}\": each channel may only be defined once.`);\n }\n operations.set(channel, def);\n };\n\n // 1. Parse flat operations (top-level `operations:` key)\n if (doc.operations) {\n for (const [channel, entry] of Object.entries(doc.operations)) {\n set(channel, {\n channel,\n controller: entry.controller,\n action: entry.action,\n middleware: entry.middleware ?? [],\n inputSchema: entry.input,\n outputSchema: entry.output,\n });\n }\n }\n\n // 2. Parse module operations (`module:` key)\n if (doc.module) {\n for (const [moduleName, moduleDef] of Object.entries(doc.module)) {\n const moduleController = moduleDef.controller;\n const moduleMiddleware = moduleDef.middleware ?? [];\n\n if (moduleDef.operations === null || moduleDef.operations === undefined) {\n throw new Error(`Module \"${moduleName}\" has no \"operations:\" block.`);\n }\n if (typeof moduleDef.operations !== 'object' || Array.isArray(moduleDef.operations)) {\n throw new Error(`Module \"${moduleName}\" \"operations:\" must be a mapping of operation names.`);\n }\n\n for (const [opName, opEntry] of Object.entries(moduleDef.operations)) {\n const channel = `${moduleName}.${opName}`;\n\n // Middleware: if operation explicitly sets middleware to [] or null, clear inherited\n // Otherwise, prepend module middleware to operation middleware\n let middleware: string[];\n if (opEntry.middleware === null || (Array.isArray(opEntry.middleware) && opEntry.middleware.length === 0)) {\n middleware = [];\n } else {\n middleware = [...moduleMiddleware, ...(opEntry.middleware ?? [])];\n }\n\n set(channel, {\n channel,\n controller: opEntry.controller ?? moduleController ?? '',\n action: opEntry.action,\n middleware,\n inputSchema: opEntry.input,\n outputSchema: opEntry.output,\n });\n }\n }\n }\n\n // 3. Validate: every operation must have a controller and action\n for (const [channel, op] of operations) {\n if (!op.controller) {\n throw new Error(`Operation \"${channel}\" has no controller defined`);\n }\n if (!op.action) {\n throw new Error(`Operation \"${channel}\" has no action defined`);\n }\n }\n\n return operations;\n}\n\n/** Parse from file path */\nexport function parseOperationsFile(filePath: string): Map<string, OperationDef> {\n const content = readFileSync(filePath, 'utf-8');\n return parseOperations(content);\n}\n"],"mappings":"AAAA,OAAO,UAAU;AACjB,SAAS,oBAAoB;AAGtB,SAAS,gBAAgB,aAAgD;AAC9E,QAAM,MAAM,KAAK,KAAK,WAAW;AACjC,QAAM,aAAa,oBAAI,IAA0B;AAEjD,MAAI,QAAQ,QAAQ,QAAQ,QAAW;AACrC,UAAM,IAAI,MAAM,wFAAwF;AAAA,EAC1G;AACA,MAAI,OAAO,QAAQ,YAAY,MAAM,QAAQ,GAAG,GAAG;AACjD,UAAM,IAAI,MAAM,iFAAiF;AAAA,EACnG;AACA,MAAI,CAAC,IAAI,cAAc,CAAC,IAAI,QAAQ;AAClC,UAAM,IAAI,MAAM,wFAAwF;AAAA,EAC1G;AAIA,QAAM,MAAM,CAAC,SAAiB,QAA4B;AACxD,QAAI,WAAW,IAAI,OAAO,GAAG;AAC3B,YAAM,IAAI,MAAM,gCAAgC,OAAO,2CAA2C;AAAA,IACpG;AACA,eAAW,IAAI,SAAS,GAAG;AAAA,EAC7B;AAGA,MAAI,IAAI,YAAY;AAClB,eAAW,CAAC,SAAS,KAAK,KAAK,OAAO,QAAQ,IAAI,UAAU,GAAG;AAC7D,UAAI,SAAS;AAAA,QACX;AAAA,QACA,YAAY,MAAM;AAAA,QAClB,QAAQ,MAAM;AAAA,QACd,YAAY,MAAM,cAAc,CAAC;AAAA,QACjC,aAAa,MAAM;AAAA,QACnB,cAAc,MAAM;AAAA,MACtB,CAAC;AAAA,IACH;AAAA,EACF;AAGA,MAAI,IAAI,QAAQ;AACd,eAAW,CAAC,YAAY,SAAS,KAAK,OAAO,QAAQ,IAAI,MAAM,GAAG;AAChE,YAAM,mBAAmB,UAAU;AACnC,YAAM,mBAAmB,UAAU,cAAc,CAAC;AAElD,UAAI,UAAU,eAAe,QAAQ,UAAU,eAAe,QAAW;AACvE,cAAM,IAAI,MAAM,WAAW,UAAU,+BAA+B;AAAA,MACtE;AACA,UAAI,OAAO,UAAU,eAAe,YAAY,MAAM,QAAQ,UAAU,UAAU,GAAG;AACnF,cAAM,IAAI,MAAM,WAAW,UAAU,uDAAuD;AAAA,MAC9F;AAEA,iBAAW,CAAC,QAAQ,OAAO,KAAK,OAAO,QAAQ,UAAU,UAAU,GAAG;AACpE,cAAM,UAAU,GAAG,UAAU,IAAI,MAAM;AAIvC,YAAI;AACJ,YAAI,QAAQ,eAAe,QAAS,MAAM,QAAQ,QAAQ,UAAU,KAAK,QAAQ,WAAW,WAAW,GAAI;AACzG,uBAAa,CAAC;AAAA,QAChB,OAAO;AACL,uBAAa,CAAC,GAAG,kBAAkB,GAAI,QAAQ,cAAc,CAAC,CAAE;AAAA,QAClE;AAEA,YAAI,SAAS;AAAA,UACX;AAAA,UACA,YAAY,QAAQ,cAAc,oBAAoB;AAAA,UACtD,QAAQ,QAAQ;AAAA,UAChB;AAAA,UACA,aAAa,QAAQ;AAAA,UACrB,cAAc,QAAQ;AAAA,QACxB,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAGA,aAAW,CAAC,SAAS,EAAE,KAAK,YAAY;AACtC,QAAI,CAAC,GAAG,YAAY;AAClB,YAAM,IAAI,MAAM,cAAc,OAAO,6BAA6B;AAAA,IACpE;AACA,QAAI,CAAC,GAAG,QAAQ;AACd,YAAM,IAAI,MAAM,cAAc,OAAO,yBAAyB;AAAA,IAChE;AAAA,EACF;AAEA,SAAO;AACT;AAGO,SAAS,oBAAoB,UAA6C;AAC/E,QAAM,UAAU,aAAa,UAAU,OAAO;AAC9C,SAAO,gBAAgB,OAAO;AAChC;","names":[]}
@@ -22,6 +22,7 @@ __export(wood_router_exports, {
22
22
  WoodRouter: () => WoodRouter
23
23
  });
24
24
  module.exports = __toCommonJS(wood_router_exports);
25
+ var import_types = require("../types/index.cjs");
25
26
  var import_schema_validator = require("../validation/schema_validator.cjs");
26
27
  var import_middleware_resolver = require("../middleware/middleware_resolver.cjs");
27
28
  var import_middleware_pipeline = require("../middleware/middleware_pipeline.cjs");
@@ -142,11 +143,19 @@ class WoodRouter {
142
143
  ipcContext.body
143
144
  );
144
145
  if (!result2.valid) {
145
- return {
146
- error: "VALIDATION_ERROR",
146
+ (0, import_tracing.publishMainTrace)("error", {
147
+ source: "wood.router",
148
+ type: "dispatch.validation_error",
147
149
  message: "Request validation failed",
148
- details: result2.errors
149
- };
150
+ ...conversationId !== void 0 ? { conversationId } : {},
151
+ payload: {
152
+ channel,
153
+ controller: opDef.controller,
154
+ action: opDef.action,
155
+ errorCount: result2.errors?.length ?? 0
156
+ }
157
+ });
158
+ throw new import_types.WoodValidationError("Request validation failed", result2.errors ?? []);
150
159
  }
151
160
  }
152
161
  const ControllerClass = await this.controllerResolver.resolveClass(opDef.controller);
@@ -254,6 +263,21 @@ ${details}`
254
263
  }
255
264
  logger.info(`All ${this.operations.size} operations have registered controllers`);
256
265
  }
266
+ /**
267
+ * Eagerly compile every operation's inputSchema so a malformed schema fails
268
+ * at startup (with the offending channel named) instead of throwing an opaque
269
+ * ajv error on the first request to that channel. Call after loadOperations().
270
+ */
271
+ precompileSchemas() {
272
+ let compiled = 0;
273
+ for (const [channel, opDef] of this.operations) {
274
+ if (opDef.inputSchema) {
275
+ this.validator.precompile(channel, opDef.inputSchema);
276
+ compiled += 1;
277
+ }
278
+ }
279
+ logger.info(`Pre-compiled ${compiled} operation input schema(s)`);
280
+ }
257
281
  /** Get all registered operation channels. */
258
282
  getChannels() {
259
283
  return Array.from(this.operations.keys());