@dexto/image-bundler 1.5.8 → 1.6.1

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/src/generator.ts CHANGED
@@ -1,53 +1,37 @@
1
1
  /**
2
- * Code generator for base images
2
+ * Code generator for images
3
3
  *
4
- * Transforms image definitions into importable packages with:
5
- * - Side-effect provider registration
6
- * - createAgent() factory
7
- * - Utility exports
8
- * - Metadata exports
4
+ * Transforms image definitions + convention folders into importable packages with:
5
+ * - A typed `DextoImage` default export (no side effects)
6
+ * - Optional utility re-exports
9
7
  */
10
8
 
11
- import type { ImageDefinition } from '@dexto/core';
9
+ import type { ImageDefinition } from './image-definition/types.js';
12
10
  import type { GeneratedCode } from './types.js';
13
- import type { DiscoveredProviders } from './bundler.js';
11
+ import type { DiscoveredFactories } from './bundler.js';
14
12
 
15
13
  /**
16
14
  * Generate JavaScript entry point for an image
17
15
  */
18
16
  export function generateEntryPoint(
19
17
  definition: ImageDefinition,
20
- coreVersion: string,
21
- discoveredProviders?: DiscoveredProviders
18
+ discoveredFactories: DiscoveredFactories
22
19
  ): GeneratedCode {
23
- // Generate imports section
24
- const imports = generateImports(definition, discoveredProviders);
25
-
26
- // Generate provider registration section
27
- const registrations = generateProviderRegistrations(definition, discoveredProviders);
28
-
29
- // Generate factory function
30
- const factory = generateFactory();
31
-
32
- // Generate utility exports
20
+ const imports = generateImports(definition, discoveredFactories);
21
+ const helpers = definition.extends ? generateHelpers() : '';
22
+ const imageModule = generateImageModule(definition, discoveredFactories);
33
23
  const utilityExports = generateUtilityExports(definition);
34
24
 
35
- // Generate metadata export
36
- const metadata = generateMetadata(definition, coreVersion);
37
-
38
- // Combine all sections
39
- const js = `// AUTO-GENERATED by @dexto/bundler
25
+ const js = `// AUTO-GENERATED by @dexto/image-bundler
40
26
  // Do not edit this file directly. Edit dexto.image.ts instead.
41
27
 
42
28
  ${imports}
43
29
 
44
- ${registrations}
30
+ ${helpers}
45
31
 
46
- ${factory}
32
+ ${imageModule}
47
33
 
48
34
  ${utilityExports}
49
-
50
- ${metadata}
51
35
  `;
52
36
 
53
37
  // Generate TypeScript definitions
@@ -56,159 +40,111 @@ ${metadata}
56
40
  return { js, dts };
57
41
  }
58
42
 
43
+ function sanitizeIdentifier(value: string): string {
44
+ const sanitized = value.replace(/[^a-zA-Z0-9_$]/g, '_');
45
+ if (/^[a-zA-Z_$]/.test(sanitized)) {
46
+ return sanitized;
47
+ }
48
+ return `_${sanitized}`;
49
+ }
50
+
51
+ function toFactoryImportSymbol(prefix: string, type: string): string {
52
+ return sanitizeIdentifier(`${prefix}_${type}`);
53
+ }
54
+
59
55
  function generateImports(
60
56
  definition: ImageDefinition,
61
- discoveredProviders?: DiscoveredProviders
57
+ discoveredFactories: DiscoveredFactories
62
58
  ): string {
63
59
  const imports: string[] = [];
64
60
 
65
- // Import base image first (if extending) - triggers side-effect provider registration
66
61
  if (definition.extends) {
67
- imports.push(`// Import base image for provider registration (side effect)`);
68
- imports.push(`import '${definition.extends}';`);
69
- imports.push(``);
62
+ imports.push(`import baseImage from '${definition.extends}';`);
70
63
  }
71
64
 
72
- // Core imports
73
- imports.push(`import { DextoAgent } from '@dexto/core';`);
65
+ imports.push(`import { defaultLoggerFactory } from '@dexto/core';`);
74
66
 
75
- // Always import all registries since we re-export them in generateFactory()
76
- // This ensures the re-exports don't reference unimported identifiers
77
- imports.push(
78
- `import { customToolRegistry, pluginRegistry, compactionRegistry, blobStoreRegistry } from '@dexto/core';`
67
+ const toolProviders = [...discoveredFactories.tools].sort((a, b) =>
68
+ a.type.localeCompare(b.type)
69
+ );
70
+ const hookProviders = [...discoveredFactories.hooks].sort((a, b) =>
71
+ a.type.localeCompare(b.type)
72
+ );
73
+ const compactionProviders = [...discoveredFactories.compaction].sort((a, b) =>
74
+ a.type.localeCompare(b.type)
75
+ );
76
+ const blobProviders = [...discoveredFactories.storage.blob].sort((a, b) =>
77
+ a.type.localeCompare(b.type)
78
+ );
79
+ const databaseProviders = [...discoveredFactories.storage.database].sort((a, b) =>
80
+ a.type.localeCompare(b.type)
81
+ );
82
+ const cacheProviders = [...discoveredFactories.storage.cache].sort((a, b) =>
83
+ a.type.localeCompare(b.type)
79
84
  );
80
85
 
81
- // Import discovered providers
82
- if (discoveredProviders) {
83
- const categories = [
84
- { key: 'blobStore', label: 'Blob Storage' },
85
- { key: 'customTools', label: 'Custom Tools' },
86
- { key: 'compaction', label: 'Compaction' },
87
- { key: 'plugins', label: 'Plugins' },
88
- ] as const;
89
-
90
- for (const { key, label } of categories) {
91
- const providers = discoveredProviders[key];
92
- if (providers.length > 0) {
93
- imports.push(``);
94
- imports.push(`// ${label} providers (auto-discovered)`);
95
- providers.forEach((path, index) => {
96
- const varName = `${key}Provider${index}`;
97
- imports.push(`import * as ${varName} from '${path}';`);
98
- });
99
- }
100
- }
86
+ if (
87
+ toolProviders.length > 0 ||
88
+ hookProviders.length > 0 ||
89
+ compactionProviders.length > 0 ||
90
+ blobProviders.length > 0 ||
91
+ databaseProviders.length > 0 ||
92
+ cacheProviders.length > 0
93
+ ) {
94
+ imports.push('');
95
+ imports.push('// Factories (convention folders; each must `export const factory = ...`)');
101
96
  }
102
97
 
103
- return imports.join('\n');
104
- }
105
-
106
- function generateProviderRegistrations(
107
- definition: ImageDefinition,
108
- discoveredProviders?: DiscoveredProviders
109
- ): string {
110
- const registrations: string[] = [];
111
-
112
- if (definition.extends) {
113
- registrations.push(
114
- `// Base image providers already registered via import of '${definition.extends}'`
115
- );
116
- registrations.push('');
98
+ for (const entry of toolProviders) {
99
+ const symbol = toFactoryImportSymbol('tools', entry.type);
100
+ imports.push(`import { factory as ${symbol} } from '${entry.importPath}';`);
117
101
  }
118
-
119
- registrations.push('// SIDE EFFECT: Register providers on import');
120
- registrations.push('');
121
-
122
- // Auto-register discovered providers
123
- if (discoveredProviders) {
124
- const categoryMap = [
125
- { key: 'blobStore', registry: 'blobStoreRegistry', label: 'Blob Storage' },
126
- { key: 'customTools', registry: 'customToolRegistry', label: 'Custom Tools' },
127
- { key: 'compaction', registry: 'compactionRegistry', label: 'Compaction' },
128
- { key: 'plugins', registry: 'pluginRegistry', label: 'Plugins' },
129
- ] as const;
130
-
131
- for (const { key, registry, label } of categoryMap) {
132
- const providers = discoveredProviders[key];
133
- if (providers.length === 0) continue;
134
-
135
- registrations.push(`// Auto-register ${label} providers`);
136
- providers.forEach((path, index) => {
137
- const varName = `${key}Provider${index}`;
138
- registrations.push(`// From ${path}`);
139
- registrations.push(`for (const exported of Object.values(${varName})) {`);
140
- registrations.push(
141
- ` if (exported && typeof exported === 'object' && 'type' in exported && 'create' in exported) {`
142
- );
143
- registrations.push(` try {`);
144
- registrations.push(` ${registry}.register(exported);`);
145
- registrations.push(
146
- ` console.log(\`✓ Registered ${key}: \${exported.type}\`);`
147
- );
148
- registrations.push(` } catch (err) {`);
149
- registrations.push(` // Ignore duplicate registration errors`);
150
- registrations.push(
151
- ` if (!err.message?.includes('already registered')) throw err;`
152
- );
153
- registrations.push(` }`);
154
- registrations.push(` }`);
155
- registrations.push(`}`);
156
- });
157
- registrations.push('');
158
- }
102
+ for (const entry of hookProviders) {
103
+ const symbol = toFactoryImportSymbol('hooks', entry.type);
104
+ imports.push(`import { factory as ${symbol} } from '${entry.importPath}';`);
159
105
  }
160
-
161
- // Handle manual registration functions (backwards compatibility)
162
- for (const [category, config] of Object.entries(definition.providers)) {
163
- if (!config) continue;
164
-
165
- if (config.register) {
166
- // Async registration function with duplicate prevention
167
- registrations.push(`// Register ${category} via custom function (from dexto.image.ts)`);
168
- registrations.push(`await (async () => {`);
169
- registrations.push(` try {`);
170
- registrations.push(
171
- ` ${config.register
172
- .toString()
173
- .replace(/^async\s*\(\)\s*=>\s*{/, '')
174
- .replace(/}$/, '')}`
175
- );
176
- registrations.push(` } catch (err) {`);
177
- registrations.push(` // Ignore duplicate registration errors`);
178
- registrations.push(` if (!err.message?.includes('already registered')) {`);
179
- registrations.push(` throw err;`);
180
- registrations.push(` }`);
181
- registrations.push(` }`);
182
- registrations.push(`})();`);
183
- registrations.push('');
184
- }
106
+ for (const entry of compactionProviders) {
107
+ const symbol = toFactoryImportSymbol('compaction', entry.type);
108
+ imports.push(`import { factory as ${symbol} } from '${entry.importPath}';`);
109
+ }
110
+ for (const entry of blobProviders) {
111
+ const symbol = toFactoryImportSymbol('storage_blob', entry.type);
112
+ imports.push(`import { factory as ${symbol} } from '${entry.importPath}';`);
113
+ }
114
+ for (const entry of databaseProviders) {
115
+ const symbol = toFactoryImportSymbol('storage_database', entry.type);
116
+ imports.push(`import { factory as ${symbol} } from '${entry.importPath}';`);
117
+ }
118
+ for (const entry of cacheProviders) {
119
+ const symbol = toFactoryImportSymbol('storage_cache', entry.type);
120
+ imports.push(`import { factory as ${symbol} } from '${entry.importPath}';`);
185
121
  }
186
122
 
187
- return registrations.join('\n');
123
+ return imports.join('\n');
188
124
  }
189
125
 
190
- function generateFactory(): string {
191
- return `/**
192
- * Create a Dexto agent using this image's registered providers.
193
- *
194
- * @param config - Agent configuration
195
- * @param configPath - Optional path to config file
196
- * @returns DextoAgent instance with providers already registered
197
- */
198
- export function createAgent(config, configPath) {
199
- return new DextoAgent(config, configPath);
126
+ function generateHelpers(): string {
127
+ return `function isPlainObject(value) {
128
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
200
129
  }
201
130
 
202
- /**
203
- * Re-export registries for runtime customization.
204
- * This allows apps to add custom providers without depending on @dexto/core directly.
205
- */
206
- export {
207
- customToolRegistry,
208
- pluginRegistry,
209
- compactionRegistry,
210
- blobStoreRegistry,
211
- } from '@dexto/core';`;
131
+ function mergeImageDefaults(baseDefaults, overrideDefaults) {
132
+ if (!baseDefaults) return overrideDefaults;
133
+ if (!overrideDefaults) return baseDefaults;
134
+
135
+ const merged = { ...baseDefaults, ...overrideDefaults };
136
+ for (const [key, baseValue] of Object.entries(baseDefaults)) {
137
+ const overrideValue = overrideDefaults[key];
138
+ if (!isPlainObject(baseValue) || !isPlainObject(overrideValue)) {
139
+ continue;
140
+ }
141
+ merged[key] = {
142
+ ...baseValue,
143
+ ...overrideValue,
144
+ };
145
+ }
146
+ return merged;
147
+ }`;
212
148
  }
213
149
 
214
150
  function generateUtilityExports(definition: ImageDefinition): string {
@@ -250,32 +186,128 @@ function generateUtilityExports(definition: ImageDefinition): string {
250
186
  return sections.join('\n');
251
187
  }
252
188
 
253
- function generateMetadata(definition: ImageDefinition, coreVersion: string): string {
254
- const metadata: Record<string, any> = {
255
- name: definition.name,
256
- version: definition.version,
257
- description: definition.description,
258
- target: definition.target || 'custom',
259
- constraints: definition.constraints || [],
260
- builtAt: new Date().toISOString(),
261
- coreVersion: coreVersion,
262
- };
263
-
264
- // Include extends information if present
265
- if (definition.extends) {
266
- metadata.extends = definition.extends;
189
+ function generateImageModule(
190
+ definition: ImageDefinition,
191
+ discoveredFactories: DiscoveredFactories
192
+ ): string {
193
+ const derivedDefaults =
194
+ definition.defaults !== undefined
195
+ ? JSON.stringify(definition.defaults, null, 4)
196
+ : 'undefined';
197
+
198
+ const toolsEntries = discoveredFactories.tools
199
+ .slice()
200
+ .sort((a, b) => a.type.localeCompare(b.type))
201
+ .map((entry) => {
202
+ const symbol = toFactoryImportSymbol('tools', entry.type);
203
+ return ` ${JSON.stringify(entry.type)}: ${symbol},`;
204
+ });
205
+
206
+ const hookEntries = discoveredFactories.hooks
207
+ .slice()
208
+ .sort((a, b) => a.type.localeCompare(b.type))
209
+ .map((entry) => {
210
+ const symbol = toFactoryImportSymbol('hooks', entry.type);
211
+ return ` ${JSON.stringify(entry.type)}: ${symbol},`;
212
+ });
213
+
214
+ const compactionEntries = discoveredFactories.compaction
215
+ .slice()
216
+ .sort((a, b) => a.type.localeCompare(b.type))
217
+ .map((entry) => {
218
+ const symbol = toFactoryImportSymbol('compaction', entry.type);
219
+ return ` ${JSON.stringify(entry.type)}: ${symbol},`;
220
+ });
221
+
222
+ const blobEntries = discoveredFactories.storage.blob
223
+ .slice()
224
+ .sort((a, b) => a.type.localeCompare(b.type))
225
+ .map((entry) => {
226
+ const symbol = toFactoryImportSymbol('storage_blob', entry.type);
227
+ return ` ${JSON.stringify(entry.type)}: ${symbol},`;
228
+ });
229
+
230
+ const databaseEntries = discoveredFactories.storage.database
231
+ .slice()
232
+ .sort((a, b) => a.type.localeCompare(b.type))
233
+ .map((entry) => {
234
+ const symbol = toFactoryImportSymbol('storage_database', entry.type);
235
+ return ` ${JSON.stringify(entry.type)}: ${symbol},`;
236
+ });
237
+
238
+ const cacheEntries = discoveredFactories.storage.cache
239
+ .slice()
240
+ .sort((a, b) => a.type.localeCompare(b.type))
241
+ .map((entry) => {
242
+ const symbol = toFactoryImportSymbol('storage_cache', entry.type);
243
+ return ` ${JSON.stringify(entry.type)}: ${symbol},`;
244
+ });
245
+
246
+ const metadataLines: string[] = [];
247
+ metadataLines.push(` name: ${JSON.stringify(definition.name)},`);
248
+ metadataLines.push(` version: ${JSON.stringify(definition.version)},`);
249
+ metadataLines.push(` description: ${JSON.stringify(definition.description)},`);
250
+
251
+ if (definition.target !== undefined) {
252
+ metadataLines.push(` target: ${JSON.stringify(definition.target)},`);
253
+ } else if (definition.extends) {
254
+ metadataLines.push(` target: baseImage.metadata.target,`);
267
255
  }
268
256
 
269
- // Include bundled plugins if present
270
- if (definition.bundledPlugins && definition.bundledPlugins.length > 0) {
271
- metadata.bundledPlugins = definition.bundledPlugins;
257
+ if (definition.extends) {
258
+ const derivedConstraints = JSON.stringify(definition.constraints ?? []);
259
+ metadataLines.push(
260
+ ` constraints: Array.from(new Set([...(baseImage.metadata.constraints ?? []), ...${derivedConstraints}])),`
261
+ );
262
+ } else if (definition.constraints !== undefined) {
263
+ metadataLines.push(` constraints: ${JSON.stringify(definition.constraints)},`);
272
264
  }
273
265
 
274
- return `/**
275
- * Image metadata
276
- * Generated at build time
277
- */
278
- export const imageMetadata = ${JSON.stringify(metadata, null, 4)};`;
266
+ const defaultsExpression = definition.extends
267
+ ? `mergeImageDefaults(baseImage.defaults, ${derivedDefaults})`
268
+ : derivedDefaults;
269
+
270
+ const toolsSpread = definition.extends ? ` ...baseImage.tools,\n` : '';
271
+ const hooksSpread = definition.extends ? ` ...baseImage.hooks,\n` : '';
272
+ const compactionSpread = definition.extends ? ` ...baseImage.compaction,\n` : '';
273
+
274
+ const blobSpread = definition.extends ? ` ...baseImage.storage.blob,\n` : '';
275
+ const databaseSpread = definition.extends ? ` ...baseImage.storage.database,\n` : '';
276
+ const cacheSpread = definition.extends ? ` ...baseImage.storage.cache,\n` : '';
277
+
278
+ const loggerExpression = definition.extends
279
+ ? `baseImage.logger ?? defaultLoggerFactory`
280
+ : `defaultLoggerFactory`;
281
+
282
+ return `const image = {
283
+ metadata: {
284
+ ${metadataLines.join('\n')}
285
+ },
286
+ defaults: ${defaultsExpression},
287
+ tools: {
288
+ ${toolsSpread}${toolsEntries.join('\n')}
289
+ },
290
+ storage: {
291
+ blob: {
292
+ ${blobSpread}${blobEntries.join('\n')}
293
+ },
294
+ database: {
295
+ ${databaseSpread}${databaseEntries.join('\n')}
296
+ },
297
+ cache: {
298
+ ${cacheSpread}${cacheEntries.join('\n')}
299
+ },
300
+ },
301
+ hooks: {
302
+ ${hooksSpread}${hookEntries.join('\n')}
303
+ },
304
+ compaction: {
305
+ ${compactionSpread}${compactionEntries.join('\n')}
306
+ },
307
+ logger: ${loggerExpression},
308
+ };
309
+
310
+ export default image;`;
279
311
  }
280
312
 
281
313
  function generateTypeDefinitions(definition: ImageDefinition): string {
@@ -311,26 +343,13 @@ function generateTypeDefinitions(definition: ImageDefinition): string {
311
343
  return `// AUTO-GENERATED TypeScript definitions
312
344
  // Do not edit this file directly
313
345
 
314
- import type { DextoAgent, AgentConfig, ImageMetadata } from '@dexto/core';
315
-
316
- /**
317
- * Create a Dexto agent using this image's registered providers.
318
- */
319
- export declare function createAgent(config: AgentConfig, configPath?: string): DextoAgent;
320
-
321
- /**
322
- * Image metadata
323
- */
324
- export declare const imageMetadata: ImageMetadata;
346
+ import type { DextoImage } from '@dexto/agent-config';
325
347
 
326
- /**
327
- * Re-exported registries for runtime customization
328
- */
329
- export {
330
- customToolRegistry,
331
- pluginRegistry,
332
- compactionRegistry,
333
- blobStoreRegistry,
334
- } from '@dexto/core';${utilityExports}
335
- `;
348
+ /**
349
+ * Typed image module (no side effects)
350
+ */
351
+ declare const image: DextoImage;
352
+ export default image;
353
+ ${utilityExports}
354
+ `;
336
355
  }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Image Definition Types (bundler-only)
3
+ *
4
+ * The bundler consumes a `dexto.image.ts` file that declares metadata and defaults.
5
+ * Concrete tools/storage/hooks/compaction factories are discovered from convention folders
6
+ * and must `export const factory = ...` from their `index.ts`.
7
+ */
8
+
9
+ import type { ImageDefaults } from '@dexto/agent-config';
10
+
11
+ /**
12
+ * Image definition structure consumed by `@dexto/image-bundler`.
13
+ *
14
+ * Note: Provider factories are discovered from folders; this file is metadata + defaults only.
15
+ */
16
+ export interface ImageDefinition {
17
+ /** Unique name for this image (e.g., 'image-local') */
18
+ name: string;
19
+ /** Semantic version of this image */
20
+ version: string;
21
+ /** Brief description of this image's purpose */
22
+ description: string;
23
+
24
+ /** Target deployment environment (for documentation and validation) */
25
+ target?: string;
26
+
27
+ /** Runtime constraints this image requires (for validation and error messages) */
28
+ constraints?: string[];
29
+
30
+ /** Parent image package name to extend (optional) */
31
+ extends?: string;
32
+
33
+ /**
34
+ * Default configuration values (merged into agent config; config wins).
35
+ *
36
+ * This must match the `AgentConfig` shape. Unknown fields will be rejected by schema validation.
37
+ */
38
+ defaults?: ImageDefaults;
39
+
40
+ /**
41
+ * Utility exports (optional).
42
+ * Maps export name to file path (relative to image root).
43
+ */
44
+ utils?: Record<string, string>;
45
+
46
+ /**
47
+ * Selective named exports from packages (optional).
48
+ */
49
+ exports?: Record<string, string[]>;
50
+ }
51
+
52
+ /**
53
+ * Metadata about a built image (generated by bundler).
54
+ */
55
+ export interface ImageMetadata {
56
+ /** Image name */
57
+ name: string;
58
+ /** Image version */
59
+ version: string;
60
+ /** Description */
61
+ description: string;
62
+ /** Target environment */
63
+ target?: string;
64
+ /** Runtime constraints */
65
+ constraints: string[];
66
+ /** Build timestamp */
67
+ builtAt: string;
68
+ /** Core version this image was built for */
69
+ coreVersion: string;
70
+ /** Base image this extends (if any) */
71
+ extends?: string;
72
+ }
@@ -0,0 +1,70 @@
1
+ import type { ImageDefinition } from './types.js';
2
+
3
+ /**
4
+ * Validate an image definition.
5
+ * Throws if the definition is invalid.
6
+ *
7
+ * Used by bundler to validate images before building.
8
+ */
9
+ export function validateImageDefinition(definition: ImageDefinition): void {
10
+ // Basic validation
11
+ if (!definition.name || typeof definition.name !== 'string') {
12
+ throw new Error('Image name must be a non-empty string');
13
+ }
14
+
15
+ if (!definition.version || typeof definition.version !== 'string') {
16
+ throw new Error('Image version must be a non-empty string');
17
+ }
18
+
19
+ if (!definition.description || typeof definition.description !== 'string') {
20
+ throw new Error('Image description must be a non-empty string');
21
+ }
22
+
23
+ // Validate version format (basic semver check)
24
+ const versionRegex = /^\d+\.\d+\.\d+(-[a-zA-Z0-9.-]+)?$/;
25
+ if (!versionRegex.test(definition.version)) {
26
+ throw new Error(
27
+ `Image version '${definition.version}' is not valid semver. Expected format: x.y.z`
28
+ );
29
+ }
30
+
31
+ // Validate target if provided
32
+ if (definition.target !== undefined) {
33
+ if (typeof definition.target !== 'string' || definition.target.trim().length === 0) {
34
+ throw new Error(`Image target must be a non-empty string when provided`);
35
+ }
36
+ }
37
+
38
+ if (definition.constraints) {
39
+ if (!Array.isArray(definition.constraints)) {
40
+ throw new Error('Image constraints must be an array');
41
+ }
42
+
43
+ for (const constraint of definition.constraints) {
44
+ if (typeof constraint !== 'string' || constraint.trim().length === 0) {
45
+ throw new Error(`Image constraint must be a non-empty string`);
46
+ }
47
+ }
48
+ }
49
+
50
+ // Validate utils if provided
51
+ if (definition.utils) {
52
+ for (const [name, path] of Object.entries(definition.utils)) {
53
+ if (typeof path !== 'string') {
54
+ throw new Error(`Utility '${name}' path must be a string`);
55
+ }
56
+ if (!path.startsWith('./')) {
57
+ throw new Error(
58
+ `Utility '${name}' path must be relative (start with './'). Got: ${path}`
59
+ );
60
+ }
61
+ }
62
+ }
63
+
64
+ // Validate extends if provided
65
+ if (definition.extends) {
66
+ if (typeof definition.extends !== 'string') {
67
+ throw new Error('Image extends must be a string (parent image name)');
68
+ }
69
+ }
70
+ }
package/src/index.ts CHANGED
@@ -2,8 +2,9 @@
2
2
  * @dexto/bundler
3
3
  *
4
4
  * Bundles Dexto base images from dexto.image.ts definitions
5
- * into importable packages with side-effect provider registration.
5
+ * into importable packages exporting a typed `DextoImage` (no side effects).
6
6
  */
7
7
 
8
8
  export { bundle } from './bundler.js';
9
9
  export type { BundleOptions, BundleResult, GeneratedCode } from './types.js';
10
+ export type { ImageDefinition, ImageMetadata } from './image-definition/types.js';
package/src/types.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ImageDefinition, ImageMetadata } from '@dexto/core';
1
+ import type { ImageMetadata } from './image-definition/types.js';
2
2
 
3
3
  export interface BundleOptions {
4
4
  /** Path to dexto.image.ts file */