@orsetra/camel-pom-gen 1.0.0

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.
package/index.ts ADDED
@@ -0,0 +1,232 @@
1
+ /**
2
+ * @orsetra/camel-pom-gen
3
+ *
4
+ * Generate or sync a Camel K pom.xml by scanning a project's source tree.
5
+ *
6
+ * Usage
7
+ * ─────
8
+ * import { init, syncPom, nodeFsProvider } from '@orsetra/camel-pom-gen'
9
+ * import fs from 'fs'
10
+ * import path from 'path'
11
+ *
12
+ * // Once at IDE startup:
13
+ * init({
14
+ * operatorVersion: '2.11.0',
15
+ * flavor: 'plain-quarkus',
16
+ * fs: nodeFsProvider(fs, path),
17
+ * rootDir: '/workspace/my-integration', // default: '.'
18
+ * })
19
+ *
20
+ * // On every route change:
21
+ * const { pom, resolvedDeps, warnings } = await syncPom()
22
+ * // → writes pom.xml if caller wants: fs.writeFileSync('pom.xml', pom)
23
+ *
24
+ * Source scanning (Maven conventions)
25
+ * ────────────────────────────────────
26
+ * src/main/resources/ → YAML (.yaml/.yml), XML, Groovy, JS, KTS routes
27
+ * src/main/java/ → Java DSL sources
28
+ *
29
+ * Sync behavior
30
+ * ─────────────
31
+ * pom.xml absent → generate from bundled template
32
+ * pom.xml present → update <dependencies> only; everything the developer
33
+ * added manually (extra deps, repos, profiles…) is preserved
34
+ */
35
+
36
+ import { getFsProvider, registerFsProvider } from './src/fs-provider'
37
+ import { inspect } from './src/inspector/index'
38
+ import { manageIntegrationDependencies, createProject, addDependencyGAV } from './src/dependencies/manager'
39
+ import { getDefaultTemplate, renderTemplate, buildTokens } from './src/generator/template'
40
+ import { getBundledCatalog } from './src/catalogs/index'
41
+ import { getRuntimeConfig, DEFAULT_CONFIG } from './src/config'
42
+ import type { RuntimeFlavor, CamelKRuntimeConfig } from './src/config'
43
+ import type { FileSystemProvider } from './src/fs-provider'
44
+ import type { SourceLanguage } from './src/inspector/types'
45
+
46
+ // ── Public types ──────────────────────────────────────────────────────────────
47
+
48
+ export type { RuntimeFlavor, CamelKRuntimeConfig } from './src/config'
49
+ export type { FileSystemProvider } from './src/fs-provider'
50
+ export type { SourceLanguage } from './src/inspector/types'
51
+
52
+ export interface PomGenConfig {
53
+ /** Camel K operator version, e.g. "2.11.0". Defaults to latest known. */
54
+ operatorVersion?: string
55
+ /** Runtime flavor. Derived from operatorVersion when omitted. */
56
+ flavor?: RuntimeFlavor
57
+ /** FileSystem provider — how the package reads and lists project files. */
58
+ fs: FileSystemProvider
59
+ /**
60
+ * Project root directory. Defaults to '.'.
61
+ * The package looks for sources under {rootDir}/src/main/resources/ and
62
+ * {rootDir}/src/main/java/.
63
+ * The pom.xml is read from and written back to {rootDir}/pom.xml.
64
+ */
65
+ rootDir?: string
66
+ }
67
+
68
+ export interface SyncPomResult {
69
+ /** Generated or updated pom.xml content. */
70
+ pom: string
71
+ /** Resolved Maven coordinates of all detected Camel dependencies. */
72
+ resolvedDeps: string[]
73
+ /** Source URIs that could not be matched to a catalog artifact. */
74
+ warnings: string[]
75
+ }
76
+
77
+ // ── Global state ──────────────────────────────────────────────────────────────
78
+
79
+ let _config: CamelKRuntimeConfig = DEFAULT_CONFIG
80
+ let _rootDir = '.'
81
+
82
+ // ── Init ──────────────────────────────────────────────────────────────────────
83
+
84
+ /**
85
+ * Initialize the package. Call once at IDE startup before syncPom().
86
+ */
87
+ export function init(config: PomGenConfig): void {
88
+ const base = getRuntimeConfig(config.operatorVersion ?? DEFAULT_CONFIG.operatorVersion)
89
+ _config = config.flavor && config.flavor !== base.flavor
90
+ ? { ...base, flavor: config.flavor }
91
+ : base
92
+ _rootDir = config.rootDir ?? '.'
93
+ registerFsProvider(config.fs)
94
+ }
95
+
96
+ // ── syncPom ───────────────────────────────────────────────────────────────────
97
+
98
+ /**
99
+ * Scan the project sources, then generate or update pom.xml.
100
+ * Must call init() first.
101
+ */
102
+ export async function syncPom(): Promise<SyncPomResult> {
103
+ const fs = getFsProvider()
104
+ const catalog = getBundledCatalog(_config.catalogVersion)
105
+ if (!catalog) throw new Error(
106
+ `[camel-pom-gen] No bundled catalog for version "${_config.catalogVersion}".`,
107
+ )
108
+
109
+ // Read existing pom.xml (undefined on first run)
110
+ const existingPom = await safeRead(fs, join(_rootDir, 'pom.xml'))
111
+
112
+ // Scan source directories
113
+ const sources = await scanSources(fs, _rootDir)
114
+
115
+ // Build dependency project
116
+ const project = createProject('org.apache.camel.k.integration', 'camel-k-integration', _config.operatorVersion)
117
+ if (_config.flavor === 'quarkus') {
118
+ addDependencyGAV(project, 'org.apache.camel.k', 'camel-k-runtime')
119
+ }
120
+
121
+ const allDeps = new Set<string>()
122
+
123
+ // Inspect each source file
124
+ for (const { name, content, language } of sources) {
125
+ const lang = language ?? inferLanguage(name)
126
+ if (!lang) continue
127
+ for (const dep of inspect(catalog, content, lang).dependencies) allDeps.add(dep)
128
+ }
129
+
130
+ // Preserve deps the developer added manually in the existing pom
131
+ if (existingPom) {
132
+ for (const dep of extractManualDeps(existingPom)) allDeps.add(dep)
133
+ }
134
+
135
+ const warnings = manageIntegrationDependencies(project, catalog, [...allDeps])
136
+ const pom = renderTemplate(getDefaultTemplate(_config.flavor), buildTokens(_config), project)
137
+
138
+ return {
139
+ pom,
140
+ resolvedDeps: project.dependencies.map(d => `${d.groupId}:${d.artifactId}`),
141
+ warnings,
142
+ }
143
+ }
144
+
145
+ // ── Source scanning ───────────────────────────────────────────────────────────
146
+
147
+ const ROUTE_EXTS = new Set(['.yaml', '.yml', '.xml', '.groovy', '.js', '.kts'])
148
+ const JAVA_EXT = '.java'
149
+
150
+ interface ScannedFile { name: string; content: string; language?: SourceLanguage }
151
+
152
+ async function scanSources(fs: FileSystemProvider, rootDir: string): Promise<ScannedFile[]> {
153
+ const resourcesDir = join(rootDir, 'src/main/resources')
154
+ const javaDir = join(rootDir, 'src/main/java')
155
+
156
+ const [resourceFiles, javaFiles] = await Promise.all([
157
+ Promise.resolve(fs.listFiles(resourcesDir)).catch(() => [] as string[]),
158
+ Promise.resolve(fs.listFiles(javaDir)).catch(() => [] as string[]),
159
+ ])
160
+
161
+ const results: ScannedFile[] = []
162
+
163
+ for (const path of resourceFiles) {
164
+ if (!ROUTE_EXTS.has(ext(path))) continue
165
+ const content = await safeRead(fs, path)
166
+ if (content !== undefined) results.push({ name: path, content })
167
+ }
168
+
169
+ for (const path of javaFiles) {
170
+ if (!path.endsWith(JAVA_EXT)) continue
171
+ const content = await safeRead(fs, path)
172
+ if (content !== undefined) results.push({ name: path, content })
173
+ }
174
+
175
+ return results
176
+ }
177
+
178
+ // ── Helpers ───────────────────────────────────────────────────────────────────
179
+
180
+ /** Safely read a file — catches both synchronous throws and rejected Promises. */
181
+ async function safeRead(fs: FileSystemProvider, path: string): Promise<string | undefined> {
182
+ try {
183
+ return await Promise.resolve(fs.readFile(path))
184
+ } catch {
185
+ return undefined
186
+ }
187
+ }
188
+
189
+ function join(...parts: string[]): string {
190
+ const result = parts
191
+ .filter(p => p !== '' && p !== '.')
192
+ .join('/')
193
+ .replace(/\/+/g, '/')
194
+ .replace(/\/$/, '')
195
+ return result || '.'
196
+ }
197
+
198
+ function ext(filename: string): string {
199
+ const i = filename.lastIndexOf('.')
200
+ return i >= 0 ? filename.slice(i) : ''
201
+ }
202
+
203
+ function inferLanguage(filename: string): SourceLanguage | undefined {
204
+ if (filename.endsWith('.java')) return 'java'
205
+ if (filename.endsWith('.yaml') || filename.endsWith('.yml')) return 'yaml'
206
+ if (filename.endsWith('.xml')) return 'xml'
207
+ if (filename.endsWith('.groovy')) return 'groovy'
208
+ if (filename.endsWith('.js')) return 'js'
209
+ if (filename.endsWith('.kts')) return 'kts'
210
+ return undefined
211
+ }
212
+
213
+ function extractManualDeps(pom: string): string[] {
214
+ const RE = /<dependency>\s*<groupId>([^<]+)<\/groupId>\s*<artifactId>([^<]+)<\/artifactId>(?:\s*<version>([^<]+)<\/version>)?/g
215
+ const managed = /^(org\.apache\.camel|io\.quarkus)/
216
+ const result: string[] = []
217
+ let m: RegExpExecArray | null
218
+ while ((m = RE.exec(pom)) !== null) {
219
+ const [, g, a, v] = m
220
+ if (!managed.test(g)) result.push(`mvn:${g}:${a}${v ? ':' + v : ''}`)
221
+ }
222
+ return result
223
+ }
224
+
225
+ // ── Advanced / low-level exports ──────────────────────────────────────────────
226
+
227
+ export { nodeFsProvider, memFsProvider } from './src/fs-provider'
228
+ export { getRuntimeConfig, DEFAULT_CONFIG, OPERATOR_RUNTIME_CONFIGS } from './src/config'
229
+ export { parseCatalogYaml } from './src/catalog/catalog'
230
+ export type { CamelCatalog } from './src/catalog/types'
231
+ export { getBundledCatalog, BUNDLED_CATALOG_VERSIONS } from './src/catalogs/index'
232
+ export { serializePom } from './src/generator/pom'
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@orsetra/camel-pom-gen",
3
+ "version": "1.0.0",
4
+ "description": "Generate Camel K pom.xml from route sources — ports the dependency-detection pipeline from camel-k Go to TypeScript",
5
+ "main": "./index.ts",
6
+ "types": "./index.ts",
7
+ "exports": {
8
+ ".": "./index.ts"
9
+ },
10
+ "files": [
11
+ "index.ts",
12
+ "src"
13
+ ],
14
+ "publishConfig": {
15
+ "access": "public"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/orsetra/console-ui.git",
20
+ "directory": "packages/camel-pom-gen"
21
+ },
22
+ "keywords": [
23
+ "camel",
24
+ "camel-k",
25
+ "pom",
26
+ "maven",
27
+ "dependency",
28
+ "orsetra"
29
+ ],
30
+ "dependencies": {
31
+ "js-yaml": "^4.1.1"
32
+ },
33
+ "scripts": {
34
+ "test": "vitest run",
35
+ "test:watch": "vitest"
36
+ },
37
+ "devDependencies": {
38
+ "@types/js-yaml": "^4.0.9",
39
+ "typescript": "^5",
40
+ "vitest": "^5.0.3"
41
+ }
42
+ }
@@ -0,0 +1,81 @@
1
+ import yaml from 'js-yaml'
2
+ import type { CamelCatalog, CamelArtifact } from './types'
3
+
4
+ interface RawCatalog {
5
+ spec: {
6
+ runtime: CamelCatalog['runtime']
7
+ artifacts: CamelCatalog['artifacts']
8
+ loaders: CamelCatalog['loaders']
9
+ }
10
+ }
11
+
12
+ /** Parse a CamelCatalog from YAML string content. */
13
+ export function parseCatalogYaml(content: string): CamelCatalog {
14
+ const raw = yaml.load(content) as RawCatalog
15
+ if (!raw?.spec) throw new Error('Invalid CamelCatalog YAML: missing spec')
16
+ return {
17
+ runtime: raw.spec.runtime ?? { version: '', provider: '', metadata: {}, dependencies: [], capabilities: {} },
18
+ artifacts: raw.spec.artifacts ?? {},
19
+ loaders: raw.spec.loaders ?? {},
20
+ }
21
+ }
22
+
23
+ /** Find the first artifact that exposes the given URI scheme. */
24
+ export function getArtifactByScheme(catalog: CamelCatalog, schemeId: string): CamelArtifact | undefined {
25
+ for (const artifact of Object.values(catalog.artifacts)) {
26
+ if (artifact.schemes?.some(s => s.id === schemeId)) return artifact
27
+ }
28
+ return undefined
29
+ }
30
+
31
+ /** Find the first artifact that provides the given data format. */
32
+ export function getArtifactByDataFormat(catalog: CamelCatalog, dfId: string): CamelArtifact | undefined {
33
+ for (const artifact of Object.values(catalog.artifacts)) {
34
+ if (artifact.dataformats?.includes(dfId)) return artifact
35
+ }
36
+ return undefined
37
+ }
38
+
39
+ /** Decode a URI like "timer:tick?period=5000" → schemeId "timer" → artifact. */
40
+ export function decodeComponent(catalog: CamelCatalog, uri: string): CamelArtifact | undefined {
41
+ const schemeId = getURIScheme(uri)
42
+ if (!schemeId) return undefined
43
+ return getArtifactByScheme(catalog, schemeId)
44
+ }
45
+
46
+ /** Return the Maven dependency ID string for an artifact: "mvn:groupId:artifactId". */
47
+ export function getDependencyID(artifact: CamelArtifact): string {
48
+ return `mvn:${artifact.groupId}:${artifact.artifactId}`
49
+ }
50
+
51
+ /**
52
+ * Resolve the dependency ID for a DSL loader language.
53
+ * Returns `mvn:groupId:artifactId` or undefined if not found.
54
+ */
55
+ export function getLanguageDependency(catalog: CamelCatalog, language: string): string | undefined {
56
+ const loader = catalog.loaders[language]
57
+ if (loader) return `mvn:${loader.groupId}:${loader.artifactId}`
58
+ return undefined
59
+ }
60
+
61
+ /** Check whether a URI is resolvable (not a property placeholder). */
62
+ export function isResolvable(uri: string): boolean {
63
+ return !uri.includes('{{') && !uri.includes('${')
64
+ }
65
+
66
+ export function getURIScheme(uri: string): string {
67
+ const idx = uri.indexOf(':')
68
+ return idx > 0 ? uri.substring(0, idx) : ''
69
+ }
70
+
71
+ /** Check if the scheme is HTTP-based. */
72
+ export function isHTTPScheme(catalog: CamelCatalog, schemeId: string): boolean {
73
+ const artifact = getArtifactByScheme(catalog, schemeId)
74
+ return artifact?.schemes?.find(s => s.id === schemeId)?.http ?? false
75
+ }
76
+
77
+ /** Check if the scheme is passive. */
78
+ export function isPassiveScheme(catalog: CamelCatalog, schemeId: string): boolean {
79
+ const artifact = getArtifactByScheme(catalog, schemeId)
80
+ return artifact?.schemes?.find(s => s.id === schemeId)?.passive ?? false
81
+ }
@@ -0,0 +1,66 @@
1
+ export interface CamelScheme {
2
+ id: string
3
+ http: boolean
4
+ passive: boolean
5
+ }
6
+
7
+ export interface CamelArtifactDep {
8
+ groupId: string
9
+ artifactId: string
10
+ type?: string
11
+ classifier?: string
12
+ }
13
+
14
+ export interface CamelArtifactExclusion {
15
+ groupId: string
16
+ artifactId: string
17
+ }
18
+
19
+ export interface CamelArtifact {
20
+ groupId: string
21
+ artifactId: string
22
+ schemes?: CamelScheme[]
23
+ /** Data format names this artifact provides (e.g. ["avro"]) */
24
+ dataformats?: string[]
25
+ /** Fully-qualified Java class names provided by this artifact */
26
+ javaTypes?: string[]
27
+ dependencies?: CamelArtifactDep[]
28
+ exclusions?: CamelArtifactExclusion[]
29
+ }
30
+
31
+ export interface CamelLoader {
32
+ groupId: string
33
+ artifactId: string
34
+ /** Language identifiers supported by this DSL loader */
35
+ languages?: string[]
36
+ metadata?: Record<string, string>
37
+ }
38
+
39
+ export interface CamelCapabilityDep {
40
+ groupId: string
41
+ artifactId: string
42
+ classifier?: string
43
+ }
44
+
45
+ export interface CamelCapability {
46
+ dependencies: CamelCapabilityDep[]
47
+ metadata?: Record<string, string>
48
+ runtimeProperties?: { key: string; value: string }[]
49
+ }
50
+
51
+ export interface CamelRuntime {
52
+ version: string
53
+ provider: string
54
+ applicationClass?: string
55
+ metadata: Record<string, string>
56
+ dependencies: CamelCapabilityDep[]
57
+ capabilities: Record<string, CamelCapability>
58
+ }
59
+
60
+ export interface CamelCatalog {
61
+ runtime: CamelRuntime
62
+ /** Keyed by artifactId */
63
+ artifacts: Record<string, CamelArtifact>
64
+ /** Keyed by language name (yaml, java, xml, groovy, js, kts) */
65
+ loaders: Record<string, CamelLoader>
66
+ }