Compare commits

...

6 Commits

9 changed files with 812 additions and 714 deletions

View File

@@ -1,5 +1,26 @@
# Changelog # Changelog
## 2025-11-17 - 2.8.2 - fix(logging)
Include runtime identifier in per-test logfile name and sanitize runtime string
- Append a sanitized runtime identifier to the per-test log filename (format: <safeFilename>__<safeRuntime>.log) so runs for different runtimes don't clash
- Sanitize runtime names by lowercasing and removing non-alphanumeric characters to produce filesystem-safe filenames
## 2025-11-17 - 2.8.1 - fix(config)
Remove Bun config file and set deno.json useDefineForClassFields to false for compatibility
- Removed bunfig.toml (Bun-specific TypeScript decorator configuration) — stops shipping a project-local Bun transpiler config.
- Updated deno.json: set compilerOptions.useDefineForClassFields = false to keep legacy class field semantics and avoid runtime/emit incompatibilities in Deno.
## 2025-11-17 - 2.8.0 - feat(runtime-adapters)
Enable TypeScript decorator support for Deno and Bun runtimes and add decorator tests
- Add bunfig.toml to enable experimentalDecorators for Bun runtime
- Add deno.json to enable experimentalDecorators and set target/lib for Deno
- Update Bun runtime adapter to note bunfig.toml discovery so Bun runs with decorator support
- Update Deno runtime adapter to auto-detect deno.json / deno.jsonc and pass configPath in default options
- Add integration tests for decorators (test/decorator.all.ts) to verify decorator support across runtimes
## 2025-10-26 - 2.7.0 - feat(tapbundle_protocol) ## 2025-10-26 - 2.7.0 - feat(tapbundle_protocol)
Add package export for tapbundle_protocol to expose protocol utilities Add package export for tapbundle_protocol to expose protocol utilities

13
deno.json Normal file
View File

@@ -0,0 +1,13 @@
{
"compilerOptions": {
"experimentalDecorators": true,
"useDefineForClassFields": false,
"lib": [
"ES2022",
"DOM"
],
"target": "ES2022"
},
"nodeModulesDir": true,
"version": "2.8.2"
}

1371
deno.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "@git.zone/tstest", "name": "@git.zone/tstest",
"version": "2.7.0", "version": "2.8.2",
"private": false, "private": false,
"description": "a test utility to run tests that match test/**/*.ts", "description": "a test utility to run tests that match test/**/*.ts",
"exports": { "exports": {

91
test/decorator.all.ts Normal file
View File

@@ -0,0 +1,91 @@
import { tap, expect } from '../ts_tapbundle/index.js';
/**
* Simple class decorator for testing decorator support across runtimes
*/
function testDecorator(target: any) {
target.decoratorApplied = true;
target.decoratorData = 'Decorator was applied successfully';
return target;
}
/**
* Method decorator for testing
*/
function logMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const result = originalMethod.apply(this, args);
return `[logged] ${result}`;
};
return descriptor;
}
/**
* Parameter decorator for testing
*/
function validateParam(target: any, propertyKey: string, parameterIndex: number) {
// Mark that parameter validation decorator was applied
if (!target.decoratedParams) {
target.decoratedParams = {};
}
if (!target.decoratedParams[propertyKey]) {
target.decoratedParams[propertyKey] = [];
}
target.decoratedParams[propertyKey].push(parameterIndex);
}
/**
* Test class with decorators
*/
@testDecorator
class TestClass {
public name: string = 'test';
@logMethod
public greet(message: string): string {
return `Hello, ${message}!`;
}
public getValue(): number {
return 42;
}
public testParams(@validateParam value: string): string {
return value;
}
}
// Tests
tap.test('Class decorator should be applied', async () => {
expect((TestClass as any).decoratorApplied).toEqual(true);
expect((TestClass as any).decoratorData).toEqual('Decorator was applied successfully');
});
tap.test('Method decorator should modify method behavior', async () => {
const instance = new TestClass();
const result = instance.greet('World');
expect(result).toEqual('[logged] Hello, World!');
});
tap.test('Regular methods should work normally', async () => {
const instance = new TestClass();
expect(instance.getValue()).toEqual(42);
expect(instance.name).toEqual('test');
});
tap.test('Parameter decorator should be applied', async () => {
const decoratedParams = (TestClass.prototype as any).decoratedParams;
expect(decoratedParams).toBeDefined();
expect(decoratedParams.testParams).toBeDefined();
expect(decoratedParams.testParams).toContain(0);
});
tap.test('Decorator metadata preservation', async () => {
const instance = new TestClass();
expect(instance instanceof TestClass).toEqual(true);
expect(instance.constructor.name).toEqual('TestClass');
expect(instance.testParams('hello')).toEqual('hello');
});
export default tap.start();

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@git.zone/tstest', name: '@git.zone/tstest',
version: '2.7.0', version: '2.8.2',
description: 'a test utility to run tests that match test/**/*.ts' description: 'a test utility to run tests that match test/**/*.ts'
} }

View File

@@ -69,6 +69,9 @@ export class BunRuntimeAdapter extends RuntimeAdapter {
const args: string[] = ['run']; const args: string[] = ['run'];
// Note: Bun automatically discovers bunfig.toml in the current directory
// This ensures TypeScript decorator support is enabled if bunfig.toml is present
// Add extra args // Add extra args
if (mergedOptions.extraArgs && mergedOptions.extraArgs.length > 0) { if (mergedOptions.extraArgs && mergedOptions.extraArgs.length > 0) {
args.push(...mergedOptions.extraArgs); args.push(...mergedOptions.extraArgs);

View File

@@ -31,8 +31,20 @@ export class DenoRuntimeAdapter extends RuntimeAdapter {
* Get default Deno options * Get default Deno options
*/ */
protected getDefaultOptions(): DenoOptions { protected getDefaultOptions(): DenoOptions {
// Auto-detect deno.json or deno.jsonc config file for TypeScript decorator support
let configPath: string | undefined;
const denoJsonPath = plugins.path.join(process.cwd(), 'deno.json');
const denoJsoncPath = plugins.path.join(process.cwd(), 'deno.jsonc');
if (plugins.smartfile.fs.fileExistsSync(denoJsonPath)) {
configPath = denoJsonPath;
} else if (plugins.smartfile.fs.fileExistsSync(denoJsoncPath)) {
configPath = denoJsoncPath;
}
return { return {
...super.getDefaultOptions(), ...super.getDefaultOptions(),
configPath,
permissions: [ permissions: [
'--allow-read', '--allow-read',
'--allow-env', '--allow-env',

View File

@@ -200,15 +200,18 @@ export class TsTestLogger {
.replace(/\//g, '__') // Replace path separators with double underscores .replace(/\//g, '__') // Replace path separators with double underscores
.replace(/\.ts$/, '') // Remove .ts extension .replace(/\.ts$/, '') // Remove .ts extension
.replace(/^\.\.__|^\.__|^__/, ''); // Clean up leading separators from relative paths .replace(/^\.\.__|^\.__|^__/, ''); // Clean up leading separators from relative paths
this.currentTestLogFile = path.join('.nogit', 'testlogs', `${safeFilename}.log`); // Sanitize runtime name for use in filename (lowercase, no spaces/dots/special chars)
const safeRuntime = runtime.toLowerCase().replace(/[^a-z0-9]/g, '');
this.currentTestLogFile = path.join('.nogit', 'testlogs', `${safeFilename}__${safeRuntime}.log`);
// Ensure the directory exists // Ensure the directory exists
const logDir = path.dirname(this.currentTestLogFile); const logDir = path.dirname(this.currentTestLogFile);
if (!fs.existsSync(logDir)) { if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true }); fs.mkdirSync(logDir, { recursive: true });
} }
// Clear the log file for this test // Clear the log file for this test
fs.writeFileSync(this.currentTestLogFile, ''); fs.writeFileSync(this.currentTestLogFile, '');
} }