feat(tapbundle_serverside): add network port discovery utilities and migrate file I/O to smartfs; refactor runtimes to use Node fs and SmartFs, update server APIs and bump dependencies

This commit is contained in:
2026-03-03 20:15:59 +00:00
parent 4d1896bdf9
commit f23c902658
24 changed files with 2562 additions and 2094 deletions

View File

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

View File

@@ -111,10 +111,7 @@ export class Migration {
* Find all legacy test files in the base directory
*/
async findLegacyFiles(): Promise<string[]> {
const files = await plugins.smartfile.fs.listFileTree(
this.options.baseDir,
this.options.pattern
);
const files = plugins.fs.globSync(this.options.pattern, { cwd: this.options.baseDir }) as string[];
const legacyFiles: string[] = [];
@@ -154,7 +151,7 @@ export class Migration {
const newPath = plugins.path.join(dirName, newFileName);
// Check if target file already exists
if (await plugins.smartfile.fs.fileExists(newPath)) {
if (await plugins.smartfsInstance.file(newPath).exists()) {
return {
oldPath: filePath,
newPath,
@@ -206,7 +203,7 @@ export class Migration {
private async isGitRepository(dir: string): Promise<boolean> {
try {
const gitDir = plugins.path.join(dir, '.git');
return await plugins.smartfile.fs.isDirectory(gitDir);
return await plugins.smartfsInstance.directory(gitDir).exists();
} catch {
return false;
}

View File

@@ -121,7 +121,7 @@ export class BunRuntimeAdapter extends RuntimeAdapter {
// Check for 00init.ts file in test directory
const testDir = plugins.path.dirname(testFile);
const initFile = plugins.path.join(testDir, '00init.ts');
const initFileExists = await plugins.smartfile.fs.fileExists(initFile);
const initFileExists = await plugins.smartfsInstance.file(initFile).exists();
let runCommand = fullCommand;
let loaderPath: string | null = null;
@@ -135,7 +135,7 @@ import '${absoluteInitFile.replace(/\\/g, '/')}';
import '${absoluteTestFile.replace(/\\/g, '/')}';
`;
loaderPath = plugins.path.join(testDir, `.loader_${plugins.path.basename(testFile)}`);
await plugins.smartfile.memory.toFs(loaderContent, loaderPath);
await plugins.smartfsInstance.file(loaderPath).write(loaderContent);
// Rebuild command with loader file
const loaderCommand = this.createCommand(loaderPath, mergedOptions);
@@ -148,8 +148,8 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
if (loaderPath) {
const cleanup = () => {
try {
if (plugins.smartfile.fs.fileExistsSync(loaderPath)) {
plugins.smartfile.fs.removeSync(loaderPath);
if (plugins.fs.existsSync(loaderPath)) {
plugins.fs.rmSync(loaderPath, { force: true });
}
} catch (e) {
// Ignore cleanup errors

View File

@@ -107,7 +107,8 @@ export class ChromiumRuntimeAdapter extends RuntimeAdapter {
const bundleFilePath = plugins.path.join(tsbundleCacheDirPath, bundleFileName);
// lets bundle the test
await plugins.smartfile.fs.ensureEmptyDir(tsbundleCacheDirPath);
try { await plugins.smartfsInstance.directory(tsbundleCacheDirPath).recursive().delete(); } catch (e) { /* may not exist */ }
await plugins.smartfsInstance.directory(tsbundleCacheDirPath).recursive().create();
await this.tsbundleInstance.build(process.cwd(), testFile, bundleFilePath, {
bundler: 'esbuild',
});
@@ -116,15 +117,13 @@ export class ChromiumRuntimeAdapter extends RuntimeAdapter {
const { httpPort, wsPort } = await this.findFreePorts();
// lets create a server
const server = new plugins.typedserver.servertools.Server({
const server = new plugins.typedserver.TypedServer({
cors: true,
port: httpPort,
serveDir: tsbundleCacheDirPath,
});
server.addRoute(
'/test',
new plugins.typedserver.servertools.Handler('GET', async (_req, res) => {
res.type('.html');
res.write(`
server.addRoute('/test', 'GET', async () => {
return new Response(`
<html>
<head>
<script>
@@ -134,11 +133,8 @@ export class ChromiumRuntimeAdapter extends RuntimeAdapter {
</head>
<body></body>
</html>
`);
res.end();
})
);
server.addRoute('/*splat', new plugins.typedserver.servertools.HandlerStatic(tsbundleCacheDirPath));
`, { headers: { 'Content-Type': 'text/html' } });
});
await server.start();
// lets handle realtime comms

View File

@@ -36,9 +36,9 @@ export class DenoRuntimeAdapter extends RuntimeAdapter {
const denoJsonPath = plugins.path.join(process.cwd(), 'deno.json');
const denoJsoncPath = plugins.path.join(process.cwd(), 'deno.jsonc');
if (plugins.smartfile.fs.fileExistsSync(denoJsonPath)) {
if (plugins.fs.existsSync(denoJsonPath)) {
configPath = denoJsonPath;
} else if (plugins.smartfile.fs.fileExistsSync(denoJsoncPath)) {
} else if (plugins.fs.existsSync(denoJsoncPath)) {
configPath = denoJsoncPath;
}
@@ -173,7 +173,7 @@ export class DenoRuntimeAdapter extends RuntimeAdapter {
// Check for 00init.ts file in test directory
const testDir = plugins.path.dirname(testFile);
const initFile = plugins.path.join(testDir, '00init.ts');
const initFileExists = await plugins.smartfile.fs.fileExists(initFile);
const initFileExists = await plugins.smartfsInstance.file(initFile).exists();
let runCommand = fullCommand;
let loaderPath: string | null = null;
@@ -187,7 +187,7 @@ import '${absoluteInitFile.replace(/\\/g, '/')}';
import '${absoluteTestFile.replace(/\\/g, '/')}';
`;
loaderPath = plugins.path.join(testDir, `.loader_${plugins.path.basename(testFile)}`);
await plugins.smartfile.memory.toFs(loaderContent, loaderPath);
await plugins.smartfsInstance.file(loaderPath).write(loaderContent);
// Rebuild command with loader file
const loaderCommand = this.createCommand(loaderPath, mergedOptions);
@@ -200,8 +200,8 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
if (loaderPath) {
const cleanup = () => {
try {
if (plugins.smartfile.fs.fileExistsSync(loaderPath)) {
plugins.smartfile.fs.removeSync(loaderPath);
if (plugins.fs.existsSync(loaderPath)) {
plugins.fs.rmSync(loaderPath, { force: true });
}
} catch (e) {
// Ignore cleanup errors

View File

@@ -102,7 +102,7 @@ export class DockerRuntimeAdapter extends RuntimeAdapter {
}
// Check if Dockerfile exists
if (!await plugins.smartfile.fs.fileExists(dockerfilePath)) {
if (!await plugins.smartfsInstance.file(dockerfilePath).exists()) {
throw new Error(
`Dockerfile not found: ${dockerfilePath}\n` +
`Expected Dockerfile for Docker test variant.`

View File

@@ -123,7 +123,7 @@ export class NodeRuntimeAdapter extends RuntimeAdapter {
// Check for 00init.ts file in test directory
const testDir = plugins.path.dirname(testFile);
const initFile = plugins.path.join(testDir, '00init.ts');
const initFileExists = await plugins.smartfile.fs.fileExists(initFile);
const initFileExists = await plugins.smartfsInstance.file(initFile).exists();
// Determine which file to run
let fileToRun = testFile;
@@ -138,7 +138,7 @@ import '${absoluteInitFile.replace(/\\/g, '/')}';
import '${absoluteTestFile.replace(/\\/g, '/')}';
`;
loaderPath = plugins.path.join(testDir, `.loader_${plugins.path.basename(testFile)}`);
await plugins.smartfile.memory.toFs(loaderContent, loaderPath);
await plugins.smartfsInstance.file(loaderPath).write(loaderContent);
fileToRun = loaderPath;
}
@@ -150,8 +150,8 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
if (loaderPath) {
const cleanup = () => {
try {
if (plugins.smartfile.fs.fileExistsSync(loaderPath)) {
plugins.smartfile.fs.removeSync(loaderPath);
if (plugins.fs.existsSync(loaderPath)) {
plugins.fs.rmSync(loaderPath, { force: true });
}
} catch (e) {
// Ignore cleanup errors

View File

@@ -497,12 +497,10 @@ export class TapParser {
*/
private async handleSnapshot(snapshotData: { path: string; content: string; action: string }) {
try {
const smartfile = await import('@push.rocks/smartfile');
if (snapshotData.action === 'compare') {
// Try to read existing snapshot
try {
const existingSnapshot = await smartfile.fs.toStringSync(snapshotData.path);
const existingSnapshot = await plugins.smartfsInstance.file(snapshotData.path).encoding('utf8').read() as string;
if (existingSnapshot !== snapshotData.content) {
// Snapshot mismatch
if (this.logger) {
@@ -520,8 +518,8 @@ export class TapParser {
if (error.code === 'ENOENT') {
// Snapshot doesn't exist, create it
const dirPath = snapshotData.path.substring(0, snapshotData.path.lastIndexOf('/'));
await smartfile.fs.ensureDir(dirPath);
await smartfile.memory.toFs(snapshotData.content, snapshotData.path);
await plugins.smartfsInstance.directory(dirPath).recursive().create();
await plugins.smartfsInstance.file(snapshotData.path).write(snapshotData.content);
if (this.logger) {
this.logger.testConsoleOutput(`Snapshot created: ${snapshotData.path}`);
}
@@ -532,8 +530,8 @@ export class TapParser {
} else if (snapshotData.action === 'update') {
// Update snapshot
const dirPath = snapshotData.path.substring(0, snapshotData.path.lastIndexOf('/'));
await smartfile.fs.ensureDir(dirPath);
await smartfile.memory.toFs(snapshotData.content, snapshotData.path);
await plugins.smartfsInstance.directory(dirPath).recursive().create();
await plugins.smartfsInstance.file(snapshotData.path).write(snapshotData.content);
if (this.logger) {
this.logger.testConsoleOutput(`Snapshot updated: ${snapshotData.path}`);
}

View File

@@ -1,8 +1,10 @@
import * as plugins from './tstest.plugins.js';
import * as paths from './tstest.paths.js';
import { SmartFile } from '@push.rocks/smartfile';
import { type SmartFile, SmartFileFactory } from '@push.rocks/smartfile';
import { TestExecutionMode } from './index.js';
const smartFileFactory = SmartFileFactory.nodeFs();
// tap related stuff
import { TapCombinator } from './tstest.classes.tap.combinator.js';
import { TapParser } from './tstest.classes.tap.parser.js';
@@ -45,28 +47,28 @@ export class TestDirectory {
switch (this.executionMode) {
case TestExecutionMode.FILE:
// Single file mode
const filePath = plugins.path.isAbsolute(this.testPath)
? this.testPath
const filePath = plugins.path.isAbsolute(this.testPath)
? this.testPath
: plugins.path.join(this.cwd, this.testPath);
if (await plugins.smartfile.fs.fileExists(filePath)) {
this.testfileArray = [await plugins.smartfile.SmartFile.fromFilePath(filePath)];
if (await plugins.smartfsInstance.file(filePath).exists()) {
this.testfileArray = [await smartFileFactory.fromFilePath(filePath)];
} else {
throw new Error(`Test file not found: ${filePath}`);
}
break;
case TestExecutionMode.GLOB:
// Glob pattern mode - use listFileTree which supports glob patterns
// Glob pattern mode - use Node.js fs.globSync for full glob support
const globPattern = this.testPath;
const matchedFiles = await plugins.smartfile.fs.listFileTree(this.cwd, globPattern);
const matchedFiles = plugins.fs.globSync(globPattern, { cwd: this.cwd });
this.testfileArray = await Promise.all(
matchedFiles.map(async (filePath) => {
const absolutePath = plugins.path.isAbsolute(filePath)
? filePath
matchedFiles.map(async (filePath: string) => {
const absolutePath = plugins.path.isAbsolute(filePath)
? filePath
: plugins.path.join(this.cwd, filePath);
return await plugins.smartfile.SmartFile.fromFilePath(absolutePath);
return await smartFileFactory.fromFilePath(absolutePath);
})
);
break;
@@ -79,19 +81,16 @@ export class TestDirectory {
const tsPattern = '**/test*.ts';
const dockerPattern = '**/*.docker.sh';
const [tsFiles, dockerFiles] = await Promise.all([
plugins.smartfile.fs.listFileTree(dirPath, tsPattern),
plugins.smartfile.fs.listFileTree(dirPath, dockerPattern),
]);
const allTestFiles = [...tsFiles, ...dockerFiles];
const tsFiles = plugins.fs.globSync(tsPattern, { cwd: dirPath });
const dockerFiles = plugins.fs.globSync(dockerPattern, { cwd: dirPath });
const allTestFiles = [...tsFiles, ...dockerFiles] as string[];
this.testfileArray = await Promise.all(
allTestFiles.map(async (filePath) => {
const absolutePath = plugins.path.isAbsolute(filePath)
? filePath
: plugins.path.join(dirPath, filePath);
return await plugins.smartfile.SmartFile.fromFilePath(absolutePath);
return await smartFileFactory.fromFilePath(absolutePath);
})
);
break;

View File

@@ -132,7 +132,7 @@ export class TsTest {
}
public async runWatch(ignorePatterns: string[] = []) {
const smartchokInstance = new plugins.smartchok.Smartchok([this.testDir.cwd]);
const smartwatchInstance = new plugins.smartwatch.Smartwatch([this.testDir.cwd]);
console.clear();
this.logger.watchModeStart();
@@ -155,12 +155,12 @@ export class TsTest {
};
// Start watching before subscribing to events
await smartchokInstance.start();
await smartwatchInstance.start();
// Subscribe to file change events
const changeObservable = await smartchokInstance.getObservableFor('change');
const addObservable = await smartchokInstance.getObservableFor('add');
const unlinkObservable = await smartchokInstance.getObservableFor('unlink');
const changeObservable = await smartwatchInstance.getObservableFor('change');
const addObservable = await smartwatchInstance.getObservableFor('add');
const unlinkObservable = await smartwatchInstance.getObservableFor('unlink');
const handleFileChange = (changedPath: string) => {
// Skip if path matches ignore patterns
@@ -194,7 +194,7 @@ export class TsTest {
// Handle Ctrl+C to exit gracefully
process.on('SIGINT', async () => {
this.logger.watchModeStop();
await smartchokInstance.stop();
await smartwatchInstance.stop();
process.exit(0);
});
@@ -316,8 +316,8 @@ export class TsTest {
const initFile = plugins.path.join(testDir, '00init.ts');
let runCommand = `tsrun ${fileNameArg}${tsrunOptions}`;
const initFileExists = await plugins.smartfile.fs.fileExists(initFile);
const initFileExists = await plugins.smartfsInstance.file(initFile).exists();
// If 00init.ts exists, run it first
if (initFileExists) {
// Create a temporary loader file that imports both 00init.ts and the test file
@@ -328,7 +328,7 @@ import '${absoluteInitFile.replace(/\\/g, '/')}';
import '${absoluteTestFile.replace(/\\/g, '/')}';
`;
const loaderPath = plugins.path.join(testDir, `.loader_${plugins.path.basename(fileNameArg)}`);
await plugins.smartfile.memory.toFs(loaderContent, loaderPath);
await plugins.smartfsInstance.file(loaderPath).write(loaderContent);
runCommand = `tsrun ${loaderPath}${tsrunOptions}`;
}
@@ -339,14 +339,14 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
const loaderPath = plugins.path.join(testDir, `.loader_${plugins.path.basename(fileNameArg)}`);
const cleanup = () => {
try {
if (plugins.smartfile.fs.fileExistsSync(loaderPath)) {
plugins.smartfile.fs.removeSync(loaderPath);
if (plugins.fs.existsSync(loaderPath)) {
plugins.fs.rmSync(loaderPath, { force: true });
}
} catch (e) {
// Ignore cleanup errors
}
};
execResultStreaming.childProcess.on('exit', cleanup);
execResultStreaming.childProcess.on('error', cleanup);
}
@@ -445,7 +445,8 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
const bundleFilePath = plugins.path.join(tsbundleCacheDirPath, bundleFileName);
// lets bundle the test
await plugins.smartfile.fs.ensureEmptyDir(tsbundleCacheDirPath);
try { await plugins.smartfsInstance.directory(tsbundleCacheDirPath).recursive().delete(); } catch (e) { /* may not exist */ }
await plugins.smartfsInstance.directory(tsbundleCacheDirPath).recursive().create();
await this.tsbundleInstance.build(process.cwd(), fileNameArg, bundleFilePath, {
bundler: 'esbuild',
});
@@ -454,15 +455,13 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
const { httpPort, wsPort } = await this.findFreePorts();
// lets create a server
const server = new plugins.typedserver.servertools.Server({
const server = new plugins.typedserver.TypedServer({
cors: true,
port: httpPort,
serveDir: tsbundleCacheDirPath,
});
server.addRoute(
'/test',
new plugins.typedserver.servertools.Handler('GET', async (_req, res) => {
res.type('.html');
res.write(`
server.addRoute('/test', 'GET', async () => {
return new Response(`
<html>
<head>
<script>
@@ -472,11 +471,8 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
</head>
<body></body>
</html>
`);
res.end();
})
);
server.addRoute('/*splat', new plugins.typedserver.servertools.HandlerStatic(tsbundleCacheDirPath));
`, { headers: { 'Content-Type': 'text/html' } });
});
await server.start();
// lets handle realtime comms
@@ -640,34 +636,33 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
try {
// Delete 00err and 00diff directories if they exist
if (plugins.smartfile.fs.isDirectorySync(errDir)) {
plugins.smartfile.fs.removeSync(errDir);
if (plugins.fs.existsSync(errDir) && plugins.fs.statSync(errDir).isDirectory()) {
plugins.fs.rmSync(errDir, { recursive: true, force: true });
}
if (plugins.smartfile.fs.isDirectorySync(diffDir)) {
plugins.smartfile.fs.removeSync(diffDir);
if (plugins.fs.existsSync(diffDir) && plugins.fs.statSync(diffDir).isDirectory()) {
plugins.fs.rmSync(diffDir, { recursive: true, force: true });
}
// Get all .log files in log directory (not in subdirectories)
const files = await plugins.smartfile.fs.listFileTree(logDir, '*.log');
const logFiles = files.filter((file: string) => !file.includes('/'));
const entries = await plugins.smartfsInstance.directory(logDir).filter('*.log').list();
const logFiles = entries.filter((entry) => entry.isFile).map((entry) => entry.name);
if (logFiles.length === 0) {
return;
}
// Ensure previous directory exists
await plugins.smartfile.fs.ensureDir(previousDir);
await plugins.smartfsInstance.directory(previousDir).recursive().create();
// Move each log file to previous directory
for (const file of logFiles) {
const filename = plugins.path.basename(file);
for (const filename of logFiles) {
const sourcePath = plugins.path.join(logDir, filename);
const destPath = plugins.path.join(previousDir, filename);
try {
// Copy file to new location and remove original
await plugins.smartfile.fs.copy(sourcePath, destPath);
await plugins.smartfile.fs.remove(sourcePath);
await plugins.smartfsInstance.file(sourcePath).copy(destPath);
await plugins.smartfsInstance.file(sourcePath).delete();
} catch (error) {
// Silently continue if a file can't be moved
}

View File

@@ -1,7 +1,8 @@
// node native
import * as fs from 'fs';
import * as path from 'path';
export { path };
export { fs, path };
// @apiglobal scope
import * as typedserver from '@api.global/typedserver';
@@ -13,25 +14,29 @@ export {
// @push.rocks scope
import * as consolecolor from '@push.rocks/consolecolor';
import * as smartbrowser from '@push.rocks/smartbrowser';
import * as smartchok from '@push.rocks/smartchok';
import * as smartdelay from '@push.rocks/smartdelay';
import * as smartfile from '@push.rocks/smartfile';
import * as smartfs from '@push.rocks/smartfs';
const smartfsInstance = new smartfs.SmartFs(new smartfs.SmartFsProviderNode());
import * as smartlog from '@push.rocks/smartlog';
import * as smartnetwork from '@push.rocks/smartnetwork';
import * as smartpromise from '@push.rocks/smartpromise';
import * as smartshell from '@push.rocks/smartshell';
import * as smartwatch from '@push.rocks/smartwatch';
import * as tapbundle from '../dist_ts_tapbundle/index.js';
export {
consolecolor,
smartbrowser,
smartchok,
smartdelay,
smartfile,
smartfs,
smartfsInstance,
smartlog,
smartnetwork,
smartpromise,
smartshell,
smartwatch,
tapbundle,
};