Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 1 addition & 12 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 23 additions & 14 deletions packages/ums-cli/src/utils/file-operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,38 @@ import { join } from 'path';
import { glob } from 'glob';

/**
* Reads a persona file and returns its content as a string
* Extracts error message from unknown error type
*/
export async function readPersonaFile(path: string): Promise<string> {
function getErrorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}

/**
* Generic file read operation with error handling
*/
async function readFileWithContext(
path: string,
context: string
): Promise<string> {
try {
return await readFileAsync(path, 'utf-8');
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to read persona file '${path}': ${message}`);
throw new Error(`Failed to read ${context} '${path}': ${getErrorMessage(error)}`);
}
}

/**
* Reads a persona file and returns its content as a string
*/
export async function readPersonaFile(path: string): Promise<string> {
return readFileWithContext(path, 'persona file');
}

/**
* Reads a module file and returns its content as a string
*/
export async function readModuleFile(path: string): Promise<string> {
try {
return await readFileAsync(path, 'utf-8');
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to read module file '${path}': ${message}`);
}
return readFileWithContext(path, 'module file');
}

/**
Expand All @@ -45,8 +56,7 @@ export async function writeOutputFile(
try {
await writeFileAsync(path, content, 'utf-8');
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to write output file '${path}': ${message}`);
throw new Error(`Failed to write output file '${path}': ${getErrorMessage(error)}`);
}
}

Expand All @@ -66,9 +76,8 @@ export async function discoverModuleFiles(paths: string[]): Promise<string[]> {
allFiles.push(...files);
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new Error(
`Failed to discover modules in path '${path}': ${message}`
`Failed to discover modules in path '${path}': ${getErrorMessage(error)}`
);
}
}
Expand Down
108 changes: 108 additions & 0 deletions packages/ums-sdk/src/loaders/loader-utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/**
* Tests for loader utilities
*/

import { describe, it, expect } from 'vitest';
import { filePathToUrl, formatValidationErrors } from './loader-utils.js';
import type { ValidationResult } from 'ums-lib';

describe('loader-utils', () => {
describe('filePathToUrl', () => {
it('should convert file path to file URL', () => {
const filePath = '/path/to/module.ts';
const url = filePathToUrl(filePath);

expect(url).toContain('file://');
expect(url).toContain('module.ts');
});

it('should handle paths with special characters', () => {
const filePath = '/path/to/my module.ts';
const url = filePathToUrl(filePath);

expect(url).toContain('file://');
expect(url).toContain('my%20module.ts');
});
});

describe('formatValidationErrors', () => {
it('should format validation errors with paths', () => {
const validation: ValidationResult = {
valid: false,
errors: [
{ path: 'metadata.name', message: 'Name is required' },
{ path: 'components[0]', message: 'Invalid component' }
],
warnings: []
};

const result = formatValidationErrors(validation);
expect(result).toBe('metadata.name: Name is required; components[0]: Invalid component');
});

it('should use default path for errors without path', () => {
const validation: ValidationResult = {
valid: false,
errors: [
{ message: 'Invalid structure' }
],
warnings: []
};

const result = formatValidationErrors(validation);
expect(result).toBe('module: Invalid structure');
});

it('should use custom default path', () => {
const validation: ValidationResult = {
valid: false,
errors: [
{ message: 'Invalid structure' }
],
warnings: []
};

const result = formatValidationErrors(validation, 'persona');
expect(result).toBe('persona: Invalid structure');
});

it('should handle mixed errors with and without paths', () => {
const validation: ValidationResult = {
valid: false,
errors: [
{ path: 'id', message: 'ID is required' },
{ message: 'Missing schema version' },
{ path: 'version', message: 'Invalid version format' }
],
warnings: []
};

const result = formatValidationErrors(validation);
expect(result).toBe('id: ID is required; module: Missing schema version; version: Invalid version format');
});

it('should handle single error', () => {
const validation: ValidationResult = {
valid: false,
errors: [
{ path: 'id', message: 'ID is required' }
],
warnings: []
};

const result = formatValidationErrors(validation);
expect(result).toBe('id: ID is required');
});

it('should handle empty errors array', () => {
const validation: ValidationResult = {
valid: true,
errors: [],
warnings: []
};

const result = formatValidationErrors(validation);
expect(result).toBe('');
});
});
});
30 changes: 30 additions & 0 deletions packages/ums-sdk/src/loaders/loader-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* Shared utilities for module and persona loaders
*/

import { pathToFileURL } from 'node:url';
import type { ValidationResult } from 'ums-lib';

/**
* Convert file path to file URL for dynamic import
* @param filePath - Absolute path to file
* @returns File URL string suitable for dynamic import
*/
export function filePathToUrl(filePath: string): string {
return pathToFileURL(filePath).href;
}

/**
* Format validation errors into a single error message
* @param validation - Validation result from ums-lib
* @param defaultPath - Default path to use if error has no path (e.g., 'module', 'persona')
* @returns Formatted error message string
*/
export function formatValidationErrors(
validation: ValidationResult,
defaultPath = 'module'
): string {
return validation.errors
.map(e => `${e.path ?? defaultPath}: ${e.message}`)
.join('; ');
}
18 changes: 6 additions & 12 deletions packages/ums-sdk/src/loaders/module-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
*/

import { readFile } from 'node:fs/promises';
import { pathToFileURL } from 'node:url';
import {
moduleIdToExportName,
parseModule,
Expand All @@ -25,7 +24,8 @@ import {
ModuleNotFoundError,
InvalidExportError,
} from '../errors/index.js';
import { checkFileExists } from '../utils/file-utils.js';
import { checkFileExists, isFileNotFoundError } from '../utils/file-utils.js';
import { filePathToUrl, formatValidationErrors } from './loader-utils.js';

/**
* Checks if an object looks like a UMS Module (duck typing).
Expand Down Expand Up @@ -66,7 +66,7 @@ export class ModuleLoader {
await checkFileExists(filePath);

// Convert file path to file URL for dynamic import
const fileUrl = pathToFileURL(filePath).href;
const fileUrl = filePathToUrl(filePath);

// Dynamically import the TypeScript file (tsx handles compilation)
const moduleExports = (await import(fileUrl)) as Record<string, unknown>;
Expand Down Expand Up @@ -120,11 +120,8 @@ export class ModuleLoader {
// Delegate to ums-lib for full UMS v2.0 spec validation
const validation = validateModule(parsedModule);
if (!validation.valid) {
const errorMessages = validation.errors
.map(e => `${e.path ?? 'module'}: ${e.message}`)
.join('; ');
throw new ModuleLoadError(
`Module validation failed: ${errorMessages}`,
`Module validation failed: ${formatValidationErrors(validation, 'module')}`,
filePath
);
}
Expand Down Expand Up @@ -162,11 +159,8 @@ export class ModuleLoader {
try {
return await readFile(filePath, 'utf-8');
} catch (error) {
if (error && typeof error === 'object' && 'code' in error) {
const nodeError = error as NodeJS.ErrnoException;
if (nodeError.code === 'ENOENT') {
throw new ModuleNotFoundError(filePath);
}
if (isFileNotFoundError(error)) {
throw new ModuleNotFoundError(filePath);
}
throw new ModuleLoadError(
`Failed to read file: ${error instanceof Error ? error.message : String(error)}`,
Copy link

Copilot AI Dec 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This error message construction still uses the inline pattern that was supposed to be refactored. Consider extracting this to a helper function like getErrorMessage() in the CLI package to maintain consistency with the stated goal of eliminating duplicated error handling patterns. The same pattern exists in several other SDK files (high-level-api.ts lines 142 and 196, module-discovery.ts line 91, standard-library.ts line 60, build-orchestrator.ts line 163).

Copilot uses AI. Check for mistakes.
Expand Down
9 changes: 3 additions & 6 deletions packages/ums-sdk/src/loaders/persona-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@
* - Validation (UMS v2.0 spec compliance)
*/

import { pathToFileURL } from 'node:url';
import { parsePersona, validatePersona, type Persona } from 'ums-lib';
import { ModuleLoadError, ModuleNotFoundError } from '../errors/index.js';
import { checkFileExists } from '../utils/file-utils.js';
import { filePathToUrl, formatValidationErrors } from './loader-utils.js';

/**
* PersonaLoader - Loads and validates TypeScript persona files
Expand All @@ -34,7 +34,7 @@ export class PersonaLoader {
await checkFileExists(filePath);

// Convert file path to file URL for dynamic import
const fileUrl = pathToFileURL(filePath).href;
const fileUrl = filePathToUrl(filePath);

// Dynamically import the TypeScript file
const personaExports = (await import(fileUrl)) as Record<string, unknown>;
Expand Down Expand Up @@ -69,11 +69,8 @@ export class PersonaLoader {
// Delegate to ums-lib for full UMS v2.0 spec validation
const validation = validatePersona(parsedPersona);
if (!validation.valid) {
const errorMessages = validation.errors
.map(e => `${e.path ?? 'persona'}: ${e.message}`)
.join('; ');
throw new ModuleLoadError(
`Persona validation failed: ${errorMessages}`,
`Persona validation failed: ${formatValidationErrors(validation, 'persona')}`,
filePath
);
}
Expand Down
Loading