@vertesia/build-tools 0.24.0-dev.202601221707

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 (90) hide show
  1. package/LICENSE +13 -0
  2. package/README.md +334 -0
  3. package/lib/build-tools.js +1730 -0
  4. package/lib/build-tools.js.map +1 -0
  5. package/lib/cjs/index.js +39 -0
  6. package/lib/cjs/index.js.map +1 -0
  7. package/lib/cjs/package.json +3 -0
  8. package/lib/cjs/parsers/frontmatter.js +25 -0
  9. package/lib/cjs/parsers/frontmatter.js.map +1 -0
  10. package/lib/cjs/plugin.js +150 -0
  11. package/lib/cjs/plugin.js.map +1 -0
  12. package/lib/cjs/presets/index.js +19 -0
  13. package/lib/cjs/presets/index.js.map +1 -0
  14. package/lib/cjs/presets/prompt.js +185 -0
  15. package/lib/cjs/presets/prompt.js.map +1 -0
  16. package/lib/cjs/presets/raw.js +25 -0
  17. package/lib/cjs/presets/raw.js.map +1 -0
  18. package/lib/cjs/presets/skill-collection.js +83 -0
  19. package/lib/cjs/presets/skill-collection.js.map +1 -0
  20. package/lib/cjs/presets/skill.js +224 -0
  21. package/lib/cjs/presets/skill.js.map +1 -0
  22. package/lib/cjs/types.js +6 -0
  23. package/lib/cjs/types.js.map +1 -0
  24. package/lib/cjs/utils/asset-copy.js +61 -0
  25. package/lib/cjs/utils/asset-copy.js.map +1 -0
  26. package/lib/cjs/utils/asset-discovery.js +100 -0
  27. package/lib/cjs/utils/asset-discovery.js.map +1 -0
  28. package/lib/cjs/utils/widget-compiler.js +115 -0
  29. package/lib/cjs/utils/widget-compiler.js.map +1 -0
  30. package/lib/esm/index.js +26 -0
  31. package/lib/esm/index.js.map +1 -0
  32. package/lib/esm/parsers/frontmatter.js +19 -0
  33. package/lib/esm/parsers/frontmatter.js.map +1 -0
  34. package/lib/esm/plugin.js +144 -0
  35. package/lib/esm/plugin.js.map +1 -0
  36. package/lib/esm/presets/index.js +8 -0
  37. package/lib/esm/presets/index.js.map +1 -0
  38. package/lib/esm/presets/prompt.js +181 -0
  39. package/lib/esm/presets/prompt.js.map +1 -0
  40. package/lib/esm/presets/raw.js +22 -0
  41. package/lib/esm/presets/raw.js.map +1 -0
  42. package/lib/esm/presets/skill-collection.js +77 -0
  43. package/lib/esm/presets/skill-collection.js.map +1 -0
  44. package/lib/esm/presets/skill.js +221 -0
  45. package/lib/esm/presets/skill.js.map +1 -0
  46. package/lib/esm/types.js +5 -0
  47. package/lib/esm/types.js.map +1 -0
  48. package/lib/esm/utils/asset-copy.js +54 -0
  49. package/lib/esm/utils/asset-copy.js.map +1 -0
  50. package/lib/esm/utils/asset-discovery.js +94 -0
  51. package/lib/esm/utils/asset-discovery.js.map +1 -0
  52. package/lib/esm/utils/widget-compiler.js +76 -0
  53. package/lib/esm/utils/widget-compiler.js.map +1 -0
  54. package/lib/types/index.d.ts +24 -0
  55. package/lib/types/index.d.ts.map +1 -0
  56. package/lib/types/parsers/frontmatter.d.ts +19 -0
  57. package/lib/types/parsers/frontmatter.d.ts.map +1 -0
  58. package/lib/types/plugin.d.ts +10 -0
  59. package/lib/types/plugin.d.ts.map +1 -0
  60. package/lib/types/presets/index.d.ts +8 -0
  61. package/lib/types/presets/index.d.ts.map +1 -0
  62. package/lib/types/presets/prompt.d.ts +63 -0
  63. package/lib/types/presets/prompt.d.ts.map +1 -0
  64. package/lib/types/presets/raw.d.ts +16 -0
  65. package/lib/types/presets/raw.d.ts.map +1 -0
  66. package/lib/types/presets/skill-collection.d.ts +26 -0
  67. package/lib/types/presets/skill-collection.d.ts.map +1 -0
  68. package/lib/types/presets/skill.d.ts +139 -0
  69. package/lib/types/presets/skill.d.ts.map +1 -0
  70. package/lib/types/types.d.ts +115 -0
  71. package/lib/types/types.d.ts.map +1 -0
  72. package/lib/types/utils/asset-copy.d.ts +20 -0
  73. package/lib/types/utils/asset-copy.d.ts.map +1 -0
  74. package/lib/types/utils/asset-discovery.d.ts +38 -0
  75. package/lib/types/utils/asset-discovery.d.ts.map +1 -0
  76. package/lib/types/utils/widget-compiler.d.ts +15 -0
  77. package/lib/types/utils/widget-compiler.d.ts.map +1 -0
  78. package/package.json +69 -0
  79. package/src/index.ts +52 -0
  80. package/src/parsers/frontmatter.ts +32 -0
  81. package/src/plugin.ts +166 -0
  82. package/src/presets/index.ts +8 -0
  83. package/src/presets/prompt.ts +227 -0
  84. package/src/presets/raw.ts +24 -0
  85. package/src/presets/skill-collection.ts +86 -0
  86. package/src/presets/skill.ts +271 -0
  87. package/src/types.ts +140 -0
  88. package/src/utils/asset-copy.ts +63 -0
  89. package/src/utils/asset-discovery.ts +138 -0
  90. package/src/utils/widget-compiler.ts +98 -0
package/src/plugin.ts ADDED
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Core Rollup plugin implementation for transforming imports
3
+ */
4
+
5
+ import type { Plugin } from 'rollup';
6
+ import { readFileSync } from 'node:fs';
7
+ import path from 'node:path';
8
+ import type { PluginConfig, TransformerRule, AssetFile } from './types.js';
9
+ import { copyAssets } from './utils/asset-copy.js';
10
+ import { compileWidgets } from './utils/widget-compiler.js';
11
+ import type { WidgetMetadata } from './utils/asset-discovery.js';
12
+
13
+ /**
14
+ * Creates a Rollup plugin that transforms imports based on configured rules
15
+ */
16
+ export function vertesiaImportPlugin(config: PluginConfig): Plugin {
17
+ const { transformers, assetsDir = './dist', widgetConfig } = config;
18
+
19
+ if (!transformers || transformers.length === 0) {
20
+ throw new Error('vertesiaImportPlugin: At least one transformer must be configured');
21
+ }
22
+
23
+ // Track assets to copy and widgets to compile
24
+ const assetsToProcess: AssetFile[] = [];
25
+ const widgetsToCompile: WidgetMetadata[] = [];
26
+ const shouldCopyAssets = assetsDir !== false;
27
+ const shouldCompileWidgets = widgetConfig !== undefined && assetsDir !== false;
28
+
29
+ return {
30
+ name: 'vertesia-import-plugin',
31
+
32
+ /**
33
+ * Resolve import IDs to handle pattern-based imports
34
+ */
35
+ resolveId(source: string, importer: string | undefined) {
36
+ // Check if any transformer pattern matches
37
+ for (const transformer of transformers) {
38
+ if (transformer.pattern.test(source)) {
39
+ // Handle relative imports
40
+ if (source.startsWith('.') && importer) {
41
+ const cleanSource = source.replace(transformer.pattern, '');
42
+ // Strip query parameters from importer to get the file path
43
+ const cleanImporter = importer.indexOf('?') >= 0
44
+ ? importer.substring(0, importer.indexOf('?'))
45
+ : importer;
46
+ // Always use dirname to get the directory containing the importer
47
+ const baseDir = path.dirname(cleanImporter);
48
+ const resolved = path.resolve(baseDir, cleanSource);
49
+ // Return with the pattern suffix to identify it in load
50
+ const suffix = source.match(transformer.pattern)?.[0] || '';
51
+ return resolved + suffix;
52
+ }
53
+ return source;
54
+ }
55
+ }
56
+ return null; // Let other plugins handle it
57
+ },
58
+
59
+ /**
60
+ * Load and transform the file content
61
+ */
62
+ async load(id: string) {
63
+ // Find matching transformer
64
+ let matchedTransformer: TransformerRule | undefined;
65
+ let cleanId = id;
66
+
67
+ for (const transformer of transformers) {
68
+ if (transformer.pattern.test(id)) {
69
+ matchedTransformer = transformer;
70
+ // Remove query parameters to get actual file path
71
+ // For example: '/path/file.md?skill' -> '/path/file.md'
72
+ // '/path/file.html?raw' -> '/path/file.html'
73
+ const queryIndex = id.indexOf('?');
74
+ cleanId = queryIndex >= 0 ? id.substring(0, queryIndex) : id;
75
+ break;
76
+ }
77
+ }
78
+
79
+ if (!matchedTransformer) {
80
+ return null; // Not for us
81
+ }
82
+
83
+ try {
84
+ // Read file content (skip for virtual transforms)
85
+ const content = matchedTransformer.virtual
86
+ ? ''
87
+ : readFileSync(cleanId, 'utf-8');
88
+
89
+ // Transform the content
90
+ const result = await matchedTransformer.transform(content, cleanId);
91
+
92
+ // Collect assets if any
93
+ if (result.assets && shouldCopyAssets) {
94
+ assetsToProcess.push(...result.assets);
95
+ }
96
+
97
+ // Collect widgets if any
98
+ if (result.widgets && shouldCompileWidgets) {
99
+ widgetsToCompile.push(...result.widgets);
100
+ }
101
+
102
+ // Validate if schema provided
103
+ if (matchedTransformer.schema) {
104
+ const validation = matchedTransformer.schema.safeParse(result.data);
105
+ if (!validation.success) {
106
+ const errors = validation.error.errors
107
+ .map((err) => ` - ${err.path.join('.')}: ${err.message}`)
108
+ .join('\n');
109
+ throw new Error(
110
+ `Validation failed for ${id}:\n${errors}`
111
+ );
112
+ }
113
+ }
114
+
115
+ // Generate code
116
+ const imports = result.imports ? result.imports.join('\n') + '\n\n' : '';
117
+ if (result.code) {
118
+ // Custom code provided - prepend imports
119
+ return imports + result.code;
120
+ } else {
121
+ // Default: export data (escape if string, otherwise stringify as JSON)
122
+ const dataJson = JSON.stringify(result.data, null, 2);
123
+ return `${imports}export default ${dataJson};`;
124
+ }
125
+ } catch (error) {
126
+ const message = error instanceof Error ? error.message : String(error);
127
+ this.error(`Failed to transform ${id}: ${message}`);
128
+ }
129
+ },
130
+
131
+ /**
132
+ * Copy assets and compile widgets after all modules are loaded
133
+ */
134
+ async buildEnd() {
135
+ // Copy script assets
136
+ if (shouldCopyAssets && assetsToProcess.length > 0) {
137
+ try {
138
+ const copied = copyAssets(assetsToProcess, assetsDir as string);
139
+ console.log(`Copied ${copied} asset file(s) to ${assetsDir}`);
140
+ } catch (error) {
141
+ const message = error instanceof Error ? error.message : String(error);
142
+ this.warn(`Failed to copy assets: ${message}`);
143
+ }
144
+ }
145
+
146
+ // Compile widgets
147
+ if (shouldCompileWidgets && widgetsToCompile.length > 0) {
148
+ try {
149
+ const widgetsDir = config.widgetsDir || 'widgets';
150
+ const outputDir = path.join(assetsDir as string, widgetsDir);
151
+
152
+ console.log(`Compiling ${widgetsToCompile.length} widget(s)...`);
153
+ const compiled = await compileWidgets(
154
+ widgetsToCompile,
155
+ outputDir,
156
+ widgetConfig
157
+ );
158
+ console.log(`Compiled ${compiled} widget(s) to ${outputDir}`);
159
+ } catch (error) {
160
+ const message = error instanceof Error ? error.message : String(error);
161
+ this.error(`Failed to compile widgets: ${message}`);
162
+ }
163
+ }
164
+ }
165
+ };
166
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Preset transformers for common use cases
3
+ */
4
+
5
+ export { skillTransformer, SkillDefinitionSchema, type SkillDefinition, type SkillContentType } from './skill.js';
6
+ export { skillCollectionTransformer } from './skill-collection.js';
7
+ export { rawTransformer } from './raw.js';
8
+ export { promptTransformer, PromptDefinitionSchema, type PromptDefinition, type PromptContentType, PromptRole, TemplateType } from './prompt.js';
@@ -0,0 +1,227 @@
1
+ /**
2
+ * Prompt transformer preset for template files with frontmatter
3
+ * Supports .jst, .hbs, and plain text files
4
+ */
5
+
6
+ import { z } from 'zod';
7
+ import type { TransformerPreset } from '../types.js';
8
+ import { parseFrontmatter } from '../parsers/frontmatter.js';
9
+ import path from 'path';
10
+ import { TemplateType } from '@vertesia/common';
11
+ import { PromptRole } from '@llumiverse/common';
12
+
13
+ /**
14
+ * Re-export types for backwards compatibility
15
+ */
16
+ export { TemplateType, PromptRole };
17
+
18
+ /**
19
+ * Template type alias
20
+ */
21
+ export type PromptContentType = TemplateType;
22
+
23
+ /**
24
+ * Zod schema for prompt frontmatter validation
25
+ */
26
+ const PromptFrontmatterSchema = z.object({
27
+ // Required fields
28
+ role: z.nativeEnum(PromptRole, {
29
+ errorMap: () => ({ message: 'Role must be one of: safety, system, user, assistant, negative' })
30
+ }),
31
+
32
+ // Optional fields
33
+ content_type: z.nativeEnum(TemplateType).optional(),
34
+ schema: z.string().optional(),
35
+ name: z.string().optional(),
36
+ externalId: z.string().optional(),
37
+ }).strict();
38
+
39
+ /**
40
+ * MUST be kept in sync with @vertesia/common InCodePrompt
41
+ * Zod schema for prompt definition
42
+ */
43
+ export const PromptDefinitionSchema = z.object({
44
+ role: z.nativeEnum(PromptRole),
45
+ content: z.string(),
46
+ content_type: z.nativeEnum(TemplateType),
47
+ schema: z.any().optional(),
48
+ name: z.string().optional(),
49
+ externalId: z.string().optional(),
50
+ });
51
+
52
+ /**
53
+ * TypeScript type inferred from the Zod schema
54
+ */
55
+ export type PromptDefinition = z.infer<typeof PromptDefinitionSchema>;
56
+
57
+ /**
58
+ * Normalize schema path for import
59
+ * - Adds './' prefix if not a relative path
60
+ * - Replaces .ts with .js
61
+ * - Adds .js if no extension
62
+ *
63
+ * @param schemaPath - Original schema path from frontmatter
64
+ * @returns Normalized path for ES module import
65
+ */
66
+ function normalizeSchemaPath(schemaPath: string): string {
67
+ let normalized = schemaPath.trim();
68
+
69
+ // Add './' prefix if not already a relative path
70
+ if (!normalized.startsWith('.')) {
71
+ normalized = './' + normalized;
72
+ }
73
+
74
+ // Get the extension
75
+ const ext = path.extname(normalized);
76
+
77
+ if (ext === '.ts') {
78
+ // Replace .ts with .js
79
+ normalized = normalized.slice(0, -3) + '.js';
80
+ } else if (!ext) {
81
+ // No extension, add .js
82
+ normalized = normalized + '.js';
83
+ }
84
+ // If extension is already .js or something else, leave as is
85
+
86
+ return normalized;
87
+ }
88
+
89
+ /**
90
+ * Infer content type from file extension
91
+ *
92
+ * @param filePath - Path to the prompt file
93
+ * @returns Inferred content type
94
+ */
95
+ function inferContentType(filePath: string): TemplateType {
96
+ const ext = path.extname(filePath).toLowerCase();
97
+
98
+ switch (ext) {
99
+ case '.jst':
100
+ return TemplateType.jst;
101
+ case '.hbs':
102
+ return TemplateType.handlebars;
103
+ default:
104
+ return TemplateType.text;
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Build a PromptDefinition from frontmatter and content
110
+ *
111
+ * @param frontmatter - Parsed frontmatter object
112
+ * @param content - Prompt content (body of the file)
113
+ * @param filePath - Path to the prompt file (for content type inference)
114
+ * @returns Prompt definition object and optional imports
115
+ */
116
+ function buildPromptDefinition(
117
+ frontmatter: Record<string, any>,
118
+ content: string,
119
+ filePath: string
120
+ ): { prompt: PromptDefinition; imports?: string[]; schemaImportName?: string } {
121
+ // Determine content type from frontmatter or file extension
122
+ const content_type: TemplateType =
123
+ frontmatter.content_type || inferContentType(filePath);
124
+
125
+ const prompt: PromptDefinition = {
126
+ role: frontmatter.role,
127
+ content,
128
+ content_type,
129
+ };
130
+
131
+ // Add optional fields
132
+ if (frontmatter.name) {
133
+ prompt.name = frontmatter.name;
134
+ }
135
+ if (frontmatter.externalId) {
136
+ prompt.externalId = frontmatter.externalId;
137
+ }
138
+
139
+ // Handle schema import if specified
140
+ let imports: string[] | undefined;
141
+ let schemaImportName: string | undefined;
142
+
143
+ if (frontmatter.schema) {
144
+ const normalizedPath = normalizeSchemaPath(frontmatter.schema);
145
+ schemaImportName = '__promptSchema';
146
+ imports = [`import ${schemaImportName} from '${normalizedPath}';`];
147
+ }
148
+
149
+ return { prompt, imports, schemaImportName };
150
+ }
151
+
152
+ /**
153
+ * Prompt transformer preset
154
+ * Transforms template files with ?prompt suffix into prompt definition objects
155
+ *
156
+ * Supported file types:
157
+ * - .jst (JavaScript template literals) → content_type: 'jst'
158
+ * - .hbs (Handlebars templates) → content_type: 'handlebars'
159
+ * - .txt or other → content_type: 'text'
160
+ *
161
+ * @example
162
+ * ```typescript
163
+ * import PROMPT from './prompt.hbs?prompt';
164
+ * // PROMPT is an InCodePrompt object
165
+ * ```
166
+ */
167
+ export const promptTransformer: TransformerPreset = {
168
+ pattern: /\?prompt$/,
169
+ schema: PromptDefinitionSchema,
170
+ transform: (content: string, filePath: string) => {
171
+ const { frontmatter, content: promptContent } = parseFrontmatter(content);
172
+
173
+ // Validate frontmatter
174
+ const frontmatterValidation = PromptFrontmatterSchema.safeParse(frontmatter);
175
+ if (!frontmatterValidation.success) {
176
+ const errors = frontmatterValidation.error.errors
177
+ .map((err) => {
178
+ const path = err.path.length > 0 ? err.path.join('.') : 'frontmatter';
179
+ return ` - ${path}: ${err.message}`;
180
+ })
181
+ .join('\n');
182
+ throw new Error(
183
+ `Invalid frontmatter in ${filePath}:\n${errors}`
184
+ );
185
+ }
186
+
187
+ // Build prompt definition
188
+ const { prompt, imports, schemaImportName } = buildPromptDefinition(
189
+ frontmatter,
190
+ promptContent,
191
+ filePath
192
+ );
193
+
194
+ // If schema is specified, generate custom code with schema reference
195
+ if (schemaImportName) {
196
+ // Build the code manually to avoid JSON.stringify issues with schema reference
197
+ const lines = [
198
+ 'export default {',
199
+ ` role: "${prompt.role}",`,
200
+ ` content: ${JSON.stringify(prompt.content)},`,
201
+ ` content_type: "${prompt.content_type}",`,
202
+ ` schema: ${schemaImportName}`,
203
+ ];
204
+
205
+ if (prompt.name) {
206
+ lines.splice(4, 0, ` name: ${JSON.stringify(prompt.name)},`);
207
+ }
208
+ if (prompt.externalId) {
209
+ lines.splice(4, 0, ` externalId: ${JSON.stringify(prompt.externalId)},`);
210
+ }
211
+
212
+ lines.push('};');
213
+ const code = lines.join('\n');
214
+
215
+ return {
216
+ data: prompt,
217
+ imports,
218
+ code,
219
+ };
220
+ }
221
+
222
+ // Standard case without schema
223
+ return {
224
+ data: prompt,
225
+ };
226
+ }
227
+ };
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Raw transformer preset for importing file content as strings
3
+ */
4
+
5
+ import type { TransformerPreset } from '../types.js';
6
+
7
+ /**
8
+ * Raw transformer preset
9
+ * Transforms any file with ?raw suffix into a string export
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * import template from './template.html?raw';
14
+ * // template is a string containing the file content
15
+ * ```
16
+ */
17
+ export const rawTransformer: TransformerPreset = {
18
+ pattern: /\?raw$/,
19
+ transform: (content: string) => {
20
+ return {
21
+ data: content
22
+ };
23
+ }
24
+ };
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Skill collection transformer for directory-based skill imports
3
+ * Scans a directory for subdirectories containing SKILL.md files
4
+ */
5
+
6
+ import { readdirSync, statSync, existsSync } from 'node:fs';
7
+ import path from 'node:path';
8
+ import type { TransformerPreset } from '../types.js';
9
+
10
+ /**
11
+ * Skill collection transformer preset
12
+ * Transforms directory imports with ?skills suffix into an array of skill imports
13
+ *
14
+ * Matches:
15
+ * - ./all?skills (recommended - generates all.js in the directory)
16
+ * - ./_skills?skills (generates _skills.js in the directory)
17
+ * - Any path ending with a filename and ?skills
18
+ *
19
+ * NOTE: A filename before ?skills is REQUIRED to avoid naming conflicts.
20
+ * The filename becomes the output module name.
21
+ *
22
+ * @example
23
+ * ```typescript
24
+ * import skills from './all?skills';
25
+ * // Scans current directory for subdirectories with SKILL.md
26
+ * // Generates all.js containing array of all skills
27
+ * ```
28
+ */
29
+ export const skillCollectionTransformer: TransformerPreset = {
30
+ pattern: /\/[^/?]+\?skills$/,
31
+ virtual: true, // Indicates this doesn't transform a real file
32
+ transform: (_content: string, filePath: string) => {
33
+ // Remove ?skills suffix and the filename to get directory path
34
+ // Example: /path/code/all?skills -> /path/code/all -> /path/code/
35
+ const pathWithoutQuery = filePath.replace(/\?skills$/, '');
36
+ const dirPath = path.dirname(pathWithoutQuery);
37
+
38
+ if (!existsSync(dirPath)) {
39
+ throw new Error(`Directory not found: ${dirPath}`);
40
+ }
41
+
42
+ if (!statSync(dirPath).isDirectory()) {
43
+ throw new Error(`Not a directory: ${dirPath}`);
44
+ }
45
+
46
+ // Scan for subdirectories containing SKILL.md
47
+ const entries = readdirSync(dirPath);
48
+ const imports: string[] = [];
49
+ const names: string[] = [];
50
+
51
+ for (const entry of entries) {
52
+ const entryPath = path.join(dirPath, entry);
53
+
54
+ try {
55
+ if (statSync(entryPath).isDirectory()) {
56
+ const skillFile = path.join(entryPath, 'SKILL.md');
57
+ if (existsSync(skillFile)) {
58
+ // Generate unique identifier from directory name
59
+ const identifier = `Skill_${entry.replace(/[^a-zA-Z0-9_]/g, '_')}`;
60
+ imports.push(`import ${identifier} from './${entry}/SKILL.md';`);
61
+ names.push(identifier);
62
+ }
63
+ }
64
+ } catch (err) {
65
+ // Skip entries that can't be read
66
+ continue;
67
+ }
68
+ }
69
+
70
+ if (names.length === 0) {
71
+ console.warn(`No SKILL.md files found in subdirectories of ${dirPath}`);
72
+ }
73
+
74
+ // Generate code that imports all skills and exports as array
75
+ const code = [
76
+ ...imports,
77
+ '',
78
+ `export default [${names.join(', ')}];`
79
+ ].join('\n');
80
+
81
+ return {
82
+ data: null, // Not used when custom code is provided
83
+ code
84
+ };
85
+ }
86
+ };