diff --git a/changelog.md b/changelog.md index b1340d3..9e1a5d3 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,14 @@ # Changelog +## 2025-11-22 - 13.0.1 - fix(smartfile) +Stream and filesystem integrations: remove fs-extra, switch to fs/promises.rename, add Web WritableStream compatibility, and use smartFs recursive directory methods; update plugins exports and README. + +- Remove fs-extra dependency and its types from package.json and stop exporting fsExtra from plugins. +- Replace fs-extra rename with fs/promises.rename in SmartFile.rename to use native promises API. +- Add Web WritableStream handling in StreamFile.writeToDisk (convert to Node.js Writable via Writable.fromWeb) and ensure Web ReadableStream conversion in createReadStream. +- Use smartFs.directory(...).recursive().create() when creating directories and smartFs.directory(path).recursive().list() when listing to ensure recursive behavior. +- Update README to add Issue Reporting and Security section pointing to community.foss.global for bug/vulnerability reports. + ## 2025-11-22 - 13.0.0 - BREAKING CHANGE(SmartFileFactory) Refactor to in-memory file API and introduce SmartFileFactory; delegate filesystem operations to @push.rocks/smartfs; bump to 12.0.0 diff --git a/package.json b/package.json index a1e6d9e..365d228 100644 --- a/package.json +++ b/package.json @@ -48,9 +48,7 @@ "@push.rocks/smartpromise": "^4.2.3", "@push.rocks/smartrequest": "^4.2.1", "@push.rocks/smartstream": "^3.2.5", - "@types/fs-extra": "^11.0.4", "@types/js-yaml": "^4.0.9", - "fs-extra": "^11.3.1", "glob": "^11.0.3", "js-yaml": "^4.1.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4d7a2e9..b58dcb1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: '@push.rocks/smartfile-interfaces': specifier: ^1.0.7 version: 1.0.7 + '@push.rocks/smartfs': + specifier: ^1.0.0 + version: 1.1.0 '@push.rocks/smarthash': specifier: ^3.2.3 version: 3.2.3 @@ -38,15 +41,9 @@ importers: '@push.rocks/smartstream': specifier: ^3.2.5 version: 3.2.5 - '@types/fs-extra': - specifier: ^11.0.4 - version: 11.0.4 '@types/js-yaml': specifier: ^4.0.9 version: 4.0.9 - fs-extra: - specifier: ^11.3.1 - version: 11.3.1 glob: specifier: ^11.0.3 version: 11.0.3 @@ -896,6 +893,9 @@ packages: '@push.rocks/smartfile@11.2.5': resolution: {integrity: sha512-Szmv0dFvDZBLsAOC2kJ0r0J0vZM0zqMAXT1G8XH11maU8pNYtYC1vceTpxoZGy4qbJcko7oGpgNUAlY+8LN3HA==} + '@push.rocks/smartfs@1.1.0': + resolution: {integrity: sha512-fg8JIjFUPPX5laRoBpTaGwhMfZ3Y8mFT4fUaW54Y4J/BfOBa/y0+rIFgvgvqcOZgkQlyZU+FIfL8Z6zezqxyTg==} + '@push.rocks/smartguard@3.1.0': resolution: {integrity: sha512-J23q84f1O+TwFGmd4lrO9XLHUh2DaLXo9PN/9VmTWYzTkQDv5JehmifXVI0esophXcCIfbdIu6hbt7/aHlDF4A==} @@ -3524,6 +3524,7 @@ packages: resolution: {integrity: sha512-5RJYU5zWFXTQ5iRXAo75vlhK5ybZOyqEyg/szw2VtHc6ZOPcC7ruX4nnXk1OqqlY56Z7XT+WCFhV+/XPj4QwtQ==} engines: {node: '>=20.18.0'} hasBin: true + bundledDependencies: [] peek-readable@5.3.1: resolution: {integrity: sha512-GVlENSDW6KHaXcd9zkZltB7tCLosKB/4Hg0fqBJkAoBgYG2Tn1xtMgXtSUuMU9AK/gCm/tTdT8mgAeF4YNeeqw==} @@ -5939,6 +5940,10 @@ snapshots: glob: 11.0.3 js-yaml: 4.1.0 + '@push.rocks/smartfs@1.1.0': + dependencies: + '@push.rocks/smartpath': 6.0.0 + '@push.rocks/smartguard@3.1.0': dependencies: '@push.rocks/smartpromise': 4.2.3 diff --git a/readme.md b/readme.md index 4b8fcba..10831c6 100644 --- a/readme.md +++ b/readme.md @@ -8,6 +8,10 @@ Think of it as your go-to solution for **content manipulation**, **file transformations**, and **in-memory file operations** - all while seamlessly integrating with [@push.rocks/smartfs](https://code.foss.global/push.rocks/smartfs) for actual filesystem operations. +## Issue Reporting and Security + +For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who want to sign a contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly. + ## 💾 Installation ```bash diff --git a/test/helpers/mock-smartfs.ts b/test/helpers/mock-smartfs.ts index d431c86..4cf0b07 100644 --- a/test/helpers/mock-smartfs.ts +++ b/test/helpers/mock-smartfs.ts @@ -1,95 +1,181 @@ /** - * Mock SmartFs implementation for testing until @push.rocks/smartfs is available - * This wraps fs-extra to provide the SmartFs interface + * Mock SmartFs implementation for testing + * Provides fluent API matching @push.rocks/smartfs using native Node.js fs */ -import { ensureDir, pathExists, remove, copy } from 'fs-extra'; import { promises as fsPromises, createReadStream, createWriteStream } from 'fs'; import * as path from 'path'; import { Readable, Writable } from 'stream'; -export class MockSmartFs { - public file(filePath: string) { - return { - async read(): Promise { - return await fsPromises.readFile(filePath); - }, - async write(content: string | Buffer): Promise { - await ensureDir(path.dirname(filePath)); - await fsPromises.writeFile(filePath, content); - }, - async exists(): Promise { - return await pathExists(filePath); - }, - async delete(): Promise { - await remove(filePath); - }, - async stat(): Promise { - return await fsPromises.stat(filePath); - }, - async readStream(): Promise { - return Promise.resolve(createReadStream(filePath)); - }, - async writeStream(): Promise { - await ensureDir(path.dirname(filePath)); - return Promise.resolve(createWriteStream(filePath)); - }, - async copy(dest: string): Promise { - await copy(filePath, dest); - }, - }; +/** + * Mock SmartFsFile - Fluent file operations builder + */ +class MockSmartFsFile { + private filePath: string; + private options: { + encoding?: BufferEncoding; + recursive?: boolean; + } = {}; + + constructor(filePath: string) { + this.filePath = filePath; } - public directory(dirPath: string) { - return { - async list(options?: { recursive?: boolean }): Promise> { - const entries: Array<{ path: string; isFile: boolean; isDirectory: boolean }> = []; + // Configuration methods (return this for chaining) + public encoding(encoding: BufferEncoding): this { + this.options.encoding = encoding; + return this; + } - if (options?.recursive) { - // Recursive listing - const walk = async (dir: string) => { - const items = await fsPromises.readdir(dir); - for (const item of items) { - const fullPath = path.join(dir, item); - const stats = await fsPromises.stat(fullPath); + public recursive(): this { + this.options.recursive = true; + return this; + } - if (stats.isFile()) { - entries.push({ path: fullPath, isFile: true, isDirectory: false }); - } else if (stats.isDirectory()) { - entries.push({ path: fullPath, isFile: false, isDirectory: true }); - await walk(fullPath); - } - } - }; - await walk(dirPath); - } else { - // Non-recursive listing - const items = await fsPromises.readdir(dirPath); - for (const item of items) { - const fullPath = path.join(dirPath, item); - const stats = await fsPromises.stat(fullPath); - entries.push({ - path: fullPath, - isFile: stats.isFile(), - isDirectory: stats.isDirectory(), - }); - } - } + // Action methods (return Promises) + public async read(): Promise { + if (this.options.encoding) { + return await fsPromises.readFile(this.filePath, this.options.encoding); + } + return await fsPromises.readFile(this.filePath); + } - return entries; - }, - async create(options?: { recursive?: boolean }): Promise { - if (options?.recursive) { - await ensureDir(dirPath); - } else { - await fsPromises.mkdir(dirPath); - } - }, - async exists(): Promise { - return await pathExists(dirPath); - }, - async delete(): Promise { - await remove(dirPath); - }, - }; + public async write(content: string | Buffer): Promise { + // Ensure directory exists + const dirPath = path.dirname(this.filePath); + await fsPromises.mkdir(dirPath, { recursive: true }); + + if (this.options.encoding && typeof content === 'string') { + await fsPromises.writeFile(this.filePath, content, this.options.encoding); + } else { + await fsPromises.writeFile(this.filePath, content); + } + } + + public async exists(): Promise { + try { + await fsPromises.access(this.filePath); + return true; + } catch { + return false; + } + } + + public async delete(): Promise { + await fsPromises.unlink(this.filePath); + } + + public async stat(): Promise { + return await fsPromises.stat(this.filePath); + } + + public async readStream(): Promise { + return createReadStream(this.filePath); + } + + public async writeStream(): Promise { + // Ensure directory exists + const dirPath = path.dirname(this.filePath); + await fsPromises.mkdir(dirPath, { recursive: true }); + return createWriteStream(this.filePath); + } + + public async copy(dest: string): Promise { + await fsPromises.copyFile(this.filePath, dest); + } +} + +/** + * Mock SmartFsDirectory - Fluent directory operations builder + */ +class MockSmartFsDirectory { + private dirPath: string; + private options: { + recursive?: boolean; + } = {}; + + constructor(dirPath: string) { + this.dirPath = dirPath; + } + + // Configuration methods (return this for chaining) + public recursive(): this { + this.options.recursive = true; + return this; + } + + // Action methods (return Promises) + public async list(): Promise> { + const entries: Array<{ path: string; isFile: boolean; isDirectory: boolean }> = []; + + if (this.options.recursive) { + // Recursive listing + const walk = async (dir: string) => { + const items = await fsPromises.readdir(dir); + for (const item of items) { + const fullPath = path.join(dir, item); + const stats = await fsPromises.stat(fullPath); + + if (stats.isFile()) { + entries.push({ path: fullPath, isFile: true, isDirectory: false }); + } else if (stats.isDirectory()) { + entries.push({ path: fullPath, isFile: false, isDirectory: true }); + await walk(fullPath); + } + } + }; + await walk(this.dirPath); + } else { + // Non-recursive listing + const items = await fsPromises.readdir(this.dirPath); + for (const item of items) { + const fullPath = path.join(this.dirPath, item); + const stats = await fsPromises.stat(fullPath); + entries.push({ + path: fullPath, + isFile: stats.isFile(), + isDirectory: stats.isDirectory(), + }); + } + } + + return entries; + } + + public async create(): Promise { + if (this.options.recursive) { + await fsPromises.mkdir(this.dirPath, { recursive: true }); + } else { + await fsPromises.mkdir(this.dirPath); + } + } + + public async exists(): Promise { + try { + const stats = await fsPromises.stat(this.dirPath); + return stats.isDirectory(); + } catch { + return false; + } + } + + public async delete(): Promise { + if (this.options.recursive) { + await fsPromises.rm(this.dirPath, { recursive: true, force: true }); + } else { + await fsPromises.rmdir(this.dirPath); + } + } +} + +/** + * Mock SmartFs - Main class matching @push.rocks/smartfs API + */ +export class MockSmartFs { + public file(filePath: string): MockSmartFsFile { + return new MockSmartFsFile(filePath); + } + + public directory(dirPath: string): MockSmartFsDirectory { + return new MockSmartFsDirectory(dirPath); } } diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 93216cb..2082a22 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@push.rocks/smartfile', - version: '13.0.0', + version: '13.0.1', description: 'High-level file representation classes (SmartFile, StreamFile, VirtualDirectory) for efficient in-memory file management in Node.js using TypeScript. Works seamlessly with @push.rocks/smartfs for filesystem operations.' } diff --git a/ts/classes.smartfile.ts b/ts/classes.smartfile.ts index d087507..abeb7b2 100644 --- a/ts/classes.smartfile.ts +++ b/ts/classes.smartfile.ts @@ -203,7 +203,7 @@ export class SmartFile extends plugins.smartjson.Smartjson { ); // Rename the file on disk - await plugins.fsExtra.rename(oldAbsolutePath, newAbsolutePath); + await plugins.fsPromises.rename(oldAbsolutePath, newAbsolutePath); } // Return the new path diff --git a/ts/classes.streamfile.ts b/ts/classes.streamfile.ts index f545b5c..b26b9de 100644 --- a/ts/classes.streamfile.ts +++ b/ts/classes.streamfile.ts @@ -1,5 +1,5 @@ import * as plugins from './plugins.js'; -import { Readable } from 'stream'; +import { Readable, Writable } from 'stream'; type TStreamSource = (streamFile: StreamFile) => Promise; @@ -163,7 +163,13 @@ export class StreamFile { this.checkMultiUse(); const readStream = await this.createReadStream(); - const writeStream = await this.smartFs.file(filePathArg).writeStream(); + let writeStream = await this.smartFs.file(filePathArg).writeStream(); + + // Check if it's a Web WritableStream and convert to Node.js Writable + if (writeStream && typeof (writeStream as any).getWriter === 'function') { + // This is a Web WritableStream, convert it to Node.js Writable + writeStream = Writable.fromWeb(writeStream as any); + } return new Promise((resolve, reject) => { readStream.pipe(writeStream); @@ -181,7 +187,7 @@ export class StreamFile { this.checkMultiUse(); const filePath = plugins.path.join(dirPathArg, this.relativeFilePath); const dirPath = plugins.path.parse(filePath).dir; - await this.smartFs.directory(dirPath).create({ recursive: true }); + await this.smartFs.directory(dirPath).recursive().create(); return this.writeToDisk(filePath); } diff --git a/ts/classes.virtualdirectory.ts b/ts/classes.virtualdirectory.ts index 1dffeaf..cf33550 100644 --- a/ts/classes.virtualdirectory.ts +++ b/ts/classes.virtualdirectory.ts @@ -23,7 +23,7 @@ export class VirtualDirectory { const newVirtualDir = new VirtualDirectory(smartFs, factory); // Use smartFs to list directory and factory to create SmartFiles - const entries = await smartFs.directory(pathArg).list({ recursive: true }); + const entries = await smartFs.directory(pathArg).recursive().list(); const smartfiles = await Promise.all( entries .filter((entry: any) => entry.isFile) diff --git a/ts/plugins.ts b/ts/plugins.ts index dd5bb60..5383446 100644 --- a/ts/plugins.ts +++ b/ts/plugins.ts @@ -32,8 +32,7 @@ export { }; // third party scope -import fsExtra from 'fs-extra'; import * as glob from 'glob'; import yaml from 'js-yaml'; -export { fsExtra, glob, yaml }; +export { glob, yaml };