Compare commits

...

31 Commits

Author SHA1 Message Date
16d47ea348 11.2.7 2025-08-18 00:14:21 +00:00
dc92b7fe93 fix(ci): Remove .npmrc containing hard-coded npm registry configuration 2025-08-18 00:14:21 +00:00
0001b433bf 11.2.6 2025-08-18 00:13:03 +00:00
cd147ca38e fix(fs): Improve fs and stream handling, enhance SmartFile/StreamFile, update tests and CI configs 2025-08-18 00:13:03 +00:00
9b0d89b9ef 11.2.5 2025-05-26 08:24:56 +00:00
b34ae1362d fix(dev): Update dev dependencies and add local permission settings 2025-05-26 08:24:56 +00:00
2fa68c23a9 11.2.4 2025-05-24 00:29:37 +00:00
4083a36d8c fix(config): Add local permissions configuration for pnpm test commands in .claude/settings.local.json 2025-05-24 00:29:37 +00:00
3988ccbcb3 11.2.3 2025-05-21 13:27:10 +00:00
6eadbc654f 11.2.2 2025-05-21 13:26:29 +00:00
fda1543701 fix(tests/settings): Improve test assertions and update local settings permissions 2025-05-21 13:26:29 +00:00
a9660eda9a 11.2.1 2025-05-21 13:24:41 +00:00
dfd1db152b fix(fs): Fix inconsistent glob matching in listFileTree and update test imports and dependency versions for enhanced stability. 2025-05-21 13:24:41 +00:00
7a32835a74 11.2.0 2025-01-29 18:23:54 +01:00
e78682d9b4 feat(fs): Enhanced copy method with optional replaceTargetDir option for directory replacement 2025-01-29 18:23:54 +01:00
8dceea67be 11.1.9 2025-01-29 18:20:15 +01:00
40018532a7 fix(fs): Fix directory handling in copy and copySync functions 2025-01-29 18:20:14 +01:00
f6fb28d32f 11.1.8 2025-01-29 18:14:02 +01:00
2d1ac0bd50 fix(fs): Fixed copy and copySync functions to ensure they always overwrite files. 2025-01-29 18:14:02 +01:00
04a25221a5 11.1.7 2025-01-29 18:10:48 +01:00
13081b7344 fix(fs): Refactor copy and copySync functions to simplify return type 2025-01-29 18:10:48 +01:00
0abbe8bbd7 11.1.6 2025-01-29 12:21:49 +01:00
de2a250a45 fix(fs): Fix issues with fs file copy functions. 2025-01-29 12:21:49 +01:00
1657d0e1c6 11.1.5 2025-01-07 04:58:31 +01:00
e6b8240031 fix(fs): Improve waitForFileToBeReady function to handle directories and file stabilization 2025-01-07 04:58:31 +01:00
be011a4637 11.1.4 2025-01-07 04:41:18 +01:00
dbddf2a8ba fix(fs): Fix file existence check in waitForFileToBeReady method. 2025-01-07 04:41:17 +01:00
207320ff26 11.1.3 2025-01-07 04:36:32 +01:00
be99bdae66 fix(fs): Fix TypeScript type issue in fs module 2025-01-07 04:36:32 +01:00
768d970918 11.1.2 2025-01-07 04:35:56 +01:00
a9799e05ee fix(fs): Fix issues in file stability check and directory existence verification in fs module 2025-01-07 04:35:56 +01:00
25 changed files with 5134 additions and 5304 deletions

View File

@@ -6,8 +6,8 @@ on:
- '**'
env:
IMAGE: registry.gitlab.com/hosttoday/ht-docker-node:npmci
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@gitea.lossless.digital/${{gitea.repository}}.git
IMAGE: code.foss.global/host.today/ht-docker-node:npmci
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@/${{gitea.repository}}.git
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
@@ -26,7 +26,7 @@ jobs:
- name: Install pnpm and npmci
run: |
pnpm install -g pnpm
pnpm install -g @shipzone/npmci
pnpm install -g @ship.zone/npmci
- name: Run npm prepare
run: npmci npm prepare

View File

@@ -6,8 +6,8 @@ on:
- '*'
env:
IMAGE: registry.gitlab.com/hosttoday/ht-docker-node:npmci
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@gitea.lossless.digital/${{gitea.repository}}.git
IMAGE: code.foss.global/host.today/ht-docker-node:npmci
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@/${{gitea.repository}}.git
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
@@ -26,7 +26,7 @@ jobs:
- name: Prepare
run: |
pnpm install -g pnpm
pnpm install -g @shipzone/npmci
pnpm install -g @ship.zone/npmci
npmci npm prepare
- name: Audit production dependencies
@@ -54,7 +54,7 @@ jobs:
- name: Prepare
run: |
pnpm install -g pnpm
pnpm install -g @shipzone/npmci
pnpm install -g @ship.zone/npmci
npmci npm prepare
- name: Test stable
@@ -82,7 +82,7 @@ jobs:
- name: Prepare
run: |
pnpm install -g pnpm
pnpm install -g @shipzone/npmci
pnpm install -g @ship.zone/npmci
npmci npm prepare
- name: Release
@@ -104,7 +104,7 @@ jobs:
- name: Prepare
run: |
pnpm install -g pnpm
pnpm install -g @shipzone/npmci
pnpm install -g @ship.zone/npmci
npmci npm prepare
- name: Code quality

7
.gitignore vendored
View File

@@ -3,7 +3,6 @@
# artifacts
coverage/
public/
pages/
# installs
node_modules/
@@ -17,4 +16,8 @@ node_modules/
dist/
dist_*/
# custom
# AI
.claude/
.serena/
#------# custom

View File

@@ -1,6 +1,127 @@
# Changelog
## 2025-08-18 - 11.2.7 - fix(ci)
Remove .npmrc containing hard-coded npm registry configuration
- Removed .npmrc which contained 'registry=https://registry.npmjs.org/'
- Avoids committing environment-specific npm registry configuration; rely on user or CI environment settings instead
## 2025-08-18 - 11.2.6 - fix(fs)
Improve fs and stream handling, enhance SmartFile/StreamFile, update tests and CI configs
- Fix listFileTree to correctly handle '**/' patterns by running both root and nested patterns and deduplicating results.
- Enhance waitForFileToBeReady: support directory paths (wait for first file) and improved file-stability checks with timeouts and retries.
- StreamFile improvements: support Web ReadableStream -> Node Readable conversion, better multi-use buffering, more robust fromUrl/fromBuffer/fromStream implementations, and accurate byte-length computation.
- SmartFile updates: switch fromUrl to SmartRequest, robust rename (optional on-disk rename), safer read/write paths and consistent Buffer handling for hashing and content edits.
- fs module tweaks: copy/copySync gained replaceTargetDir option, improved toObjectSync error messages, toReadStream now validates existence, and various synchronous/async API consistency fixes.
- memory module: consistent async/sync write APIs and smartfileArrayToFs formatting/behavior fixes.
- fsstream: improved stream processing and SmartReadStream robustness (error handling, read logic and watcher improvements).
- Tests: reformatted and strengthened tests (more explicit assertions, added tests for '**/*.ts' and '**/*' edge cases, updated imports to tapbundle usage).
- CI/workflows: updated IMAGE and NPMCI_COMPUTED_REPOURL values and switched npmci package install to @ship.zone/npmci in workflow files.
- package.json: bumped various dependencies, updated test script (timeout, logfile), added typings/main fields, homepage fix, pnpm overrides and minor metadata fixes.
- .gitignore: added entries for AI tool folders (.claude/, .serena/).
- Docs/readme: expanded README with clearer examples, features and TypeScript usage; updated readme hints for listFileTree behavior.
## 2025-05-26 - 11.2.5 - fix(dev)
Update dev dependencies and add local permission settings
- Bump @git.zone/tsbuild from 2.5.2 to 2.6.4
- Bump @git.zone/tstest from 1.9.0 to 2.2.5
- Add .claude/settings.local.json to configure permissions for Bash(pnpm test:\*)
## 2025-05-24 - 11.2.4 - fix(config)
Add local permissions configuration for pnpm test commands in .claude/settings.local.json
- Introduced .claude/settings.local.json to allow Bash(pnpm test:\*) permissions
- Ensured local testing commands have proper execution rights
## 2025-05-21 - 11.2.2 - fix(tests/settings)
Improve test assertions and update local settings permissions
- Refactor StreamFile tests to assert content string type using toBeTypeofString
- Update file existence tests to use resolves.toBeTrue and resolves.toBeFalse for cleaner promise handling
- Add .claude/settings.local.json to allow specific Bash permissions for pnpm test commands
## 2025-05-21 - 11.2.1 - fix(fs)
Fix inconsistent glob matching in listFileTree and update test imports and dependency versions for enhanced stability.
- Enhanced listFileTree to support \*\*/ patterns by using dual patterns (root and nested) with deduplication.
- Updated test imports to use '@git.zone/tstest/tapbundle' for consistency across test files.
- Bumped dependency versions (@push.rocks/lik, smartpromise, smartrequest, glob) in package.json.
- Added npm configuration (.npmrc) and local settings for improved test verbosity.
## 2025-01-29 - 11.2.0 - feat(fs)
Enhanced copy method with optional replaceTargetDir option for directory replacement
- Added optional 'replaceTargetDir' option to 'copy' and 'copySync' methods in 'fs.ts'.
- The 'replaceTargetDir' option allows replacing the target directory if both source and target are directories.
## 2025-01-29 - 11.1.9 - fix(fs)
Fix directory handling in copy and copySync functions
- Ensured existing directories at destination are removed before copying over them in async copy.
- Added a similar check and handling for synchronous copySync when destination is a directory.
## 2025-01-29 - 11.1.8 - fix(fs)
Fixed copy and copySync functions to ensure they always overwrite files.
- Fixed bug in copy function where files were not being overwritten when they already existed at the destination.
- Fixed bug in copySync function to ensure files are overwritten to match the async function's behavior.
## 2025-01-29 - 11.1.7 - fix(fs)
Refactor copy and copySync functions to simplify return type
- Changed the return type of fs.copy and fs.copySync from boolean to void.
- Removed unnecessary promise handling in fs.copy.
## 2025-01-29 - 11.1.6 - fix(fs)
Fix issues with fs file copy functions.
- Updated dependencies in package.json.
- Corrected comments for asynchronous and synchronous file copy functions in fs.ts.
## 2025-01-07 - 11.1.5 - fix(fs)
Improve waitForFileToBeReady function to handle directories and file stabilization
- Enhanced the waitForFileToBeReady to handle directory paths by checking for file existence within directories and waiting for stabilization.
- Modified the watcher logic to cater to changes when monitoring directories for file appearance.
- Introduced a helper function to ensure paths exist and another to resolve the first file in directories.
- Corrected logic for polling and stabilizing files within directories.
## 2025-01-07 - 11.1.4 - fix(fs)
Fix file existence check in waitForFileToBeReady method.
- Ensured that the directory and file exist before setting up the watcher in waitForFileToBeReady.
- Changed ensureDirectoryExists to ensureFileExists for correct file path verification.
- Handled ENOENT errors correctly to retry file existence checks until timeout is reached.
## 2025-01-07 - 11.1.3 - fix(fs)
Fix TypeScript type issue in fs module
- Corrected a TypeScript type in the fs module's checkFileStability function.
## 2025-01-07 - 11.1.2 - fix(fs)
Fix issues in file stability check and directory existence verification in fs module
- Removed unused variable 'isFileAvailable' in 'waitForFileToBeReady'.
- Fixed logic for ensuring the target directory exists before setting up file stability watcher.
- Refactored directory existence logic into 'ensureDirectoryExists' function.
## 2025-01-07 - 11.1.1 - fix(fs)
Improve waitForFileToBeReady function for file stability detection
- Enhanced error handling and file stability checks in waitForFileToBeReady function
@@ -8,17 +129,20 @@ Improve waitForFileToBeReady function for file stability detection
- Improved directory access check before file availability check
## 2025-01-07 - 11.1.0 - feat(SmartFile)
Add rename functionality to SmartFile class
- Implemented a new method to rename a file within the SmartFile class.
- The rename method updates the file path and optionally writes the renamed file to the disk.
## 2024-12-15 - 11.0.23 - fix(fs)
Handle errors in toObjectSync method
- Added error handling in toObjectSync function to capture and provide more informative error messages.
## 2024-06-23 - 11.0.22 - fix(core)
Update dependencies and changelog
- Updated @push.rocks/smartstream to ^3.0.44
@@ -26,6 +150,7 @@ Update dependencies and changelog
- Updated @types/node to ^20.14.8
## 2024-06-23 - 11.0.21 - fix(dependencies)
Update dependencies to latest versions
- Updated @push.rocks/smartpromise to ^4.0.4
@@ -34,267 +159,318 @@ Update dependencies to latest versions
- Updated @types/node to ^20.14.8
## 2024-06-07 - 11.0.20 - Changelog
11.0.20
## 2024-06-07 - 11.0.19 - fix(core): update
11.0.19
- fix(core): update
## 2024-06-07 - 11.0.18 - fix(core): update
11.0.18
- fix(core): update
## 2024-06-06 - 11.0.17 - fix(core): update
11.0.17
- fix(core): update
## 2024-06-06 - 11.0.16 - fix(core): update
11.0.16
- fix(core): update
## 2024-05-29 - 11.0.16 - update description
11.0.16
- update description
## 2024-05-17 - 11.0.15 - fix(core): update
11.0.15
- fix(core): update
## 2024-04-14 - 11.0.14 - update tsconfig
11.0.14
- update tsconfig
## 2024-04-12 - 11.0.13 - fix(core): update
11.0.13
- fix(core): update
## 2024-04-12 - 11.0.12 - fix(core): update
11.0.12
- fix(core): update
## 2024-04-12 - 11.0.11 - fix(core): update
11.0.11
- fix(core): update
## 2024-04-03 - 11.0.10 - fix(core): update
11.0.10
- fix(core): update
## 2024-04-03 - 11.0.9 - fix(core): update
11.0.9
- fix(core): update
## 2024-04-02 - 11.0.8 - fix(core): update
11.0.8
- fix(core): update
## 2024-04-02 - 11.0.7 - fix(core): update
11.0.7
- fix(core): update
## 2024-04-02 - 11.0.6 - fix(core): update
11.0.6
- fix(core): update
## 2024-04-01 - 11.0.5 - update npmextra.json
11.0.5
- update npmextra.json: githost
## 2024-04-01 - 11.0.4 - fix(core): update
11.0.4
- fix(core): update
## 2023-11-24 - 11.0.3 - fix(core): update
11.0.3
- fix(core): update
## 2023-11-07 - 11.0.2 - fix(core): update
11.0.2
- fix(core): update
## 2023-11-07 - 11.0.1 - fix(core): update
11.0.1
- fix(core): update
## 2023-11-06 - 11.0.0 - fix(core): update
11.0.0
- fix(core): update
## 2023-11-06 - 10.0.40 - BREAKING CHANGE(core): update
10.0.40
- BREAKING CHANGE(core): update
## 2023-11-04 - 10.0.39 - fix(core): update
10.0.39
- fix(core): update
## 2023-11-04 - 10.0.38 - fix(core): update
10.0.38
- fix(core): update
## 2023-11-04 - 10.0.37 - fix(core): update
10.0.37
- fix(core): update
## 2023-11-03 - 10.0.36 - fix(core): update
10.0.36
- fix(core): update
## 2023-11-03 - 10.0.35 - fix(core): update
10.0.35
- fix(core): update
## 2023-11-03 - 10.0.34 - fix(core): update
10.0.34
- fix(core): update
## 2023-11-03 - 10.0.33 - fix(core): update
10.0.33
- fix(core): update
## 2023-10-12 - 10.0.32 - fix(core): update
10.0.32
- fix(core): update
## 2023-09-22 - 10.0.31 - fix(core): update
10.0.31
- fix(core): update
## 2023-08-31 - 10.0.30 - fix(core): update
10.0.30
- fix(core): update
## 2023-08-23 - 10.0.29 - fix(core): update
10.0.29
- fix(core): update
## 2023-07-12 - 10.0.28 - fix(core): update
10.0.28
- fix(core): update
## 2023-07-10 - 10.0.27 - fix(core): update
10.0.27
- fix(core): update
## 2023-07-10 - 10.0.26 - fix(core): update
10.0.26
- fix(core): update
## 2023-07-08 - 10.0.25 - fix(core): update
10.0.25
- fix(core): update
## 2023-06-25 - 10.0.24 to 10.0.14 - Series of Fixes
10.0.24 to 10.0.14
- Series of fixes in the core module
## 2023-01-09 - 10.0.13 to 10.0.6 - Series of Fixes
10.0.13 to 10.0.6
- Series of fixes in the core module
## 2022-09-05 - 10.0.5 to 10.0.3 - Series of Fixes
10.0.5 to 10.0.3
- Series of fixes in the core module
## 2022-06-07 - 10.0.2 to 10.0.1 - Series of Fixes
10.0.2 to 10.0.1
- Series of fixes in the core module
## 2022-06-07 - 9.0.7 - BREAKING CHANGE(core): switch to esm
9.0.7
- BREAKING CHANGE(core): switch to esm
## 2022-03-11 - 9.0.6 to 9.0.2 - Series of Fixes
9.0.6 to 9.0.2
- Series of fixes in the core module
## 2021-12-01 - 9.0.1 - fix(core): update
9.0.1
- fix(core): update
## 2021-12-01 - 9.0.0 - fix(absolute pathing)
9.0.0
- add functions for easily getting absolute paths
## 2021-11-30 - 8.0.11 - BREAKING CHANGE(relative pathing)
8.0.11
- improved relative pathing
## 2020-08-10 - 8.0.10 to 7.0.12 - Series of Fixes and Updates
8.0.10 to 7.0.12
- Series of fixes in the core module
- BREAKING CHANGE(Smartfile class): switch to a Buffer-only approach
## 2019-02-17 - 7.0.0 - fix(core): update dependencies
7.0.0
- fix(core): update dependencies
## 2019-01-27 - 6.0.12 - BREAKING CHANGE(smartfile.fs.fileExists)
6.0.12
- now returns a Promise<boolean>
## 2018-08-19 - 6.0.11 to 6.0.6 - Series of Fixes
6.0.11 to 6.0.6
- Series of fixes in core and dependencies
## 2018-07-03 - 6.0.5 to 5.0.0 - Series of Fixes
6.0.5 to 5.0.0
- Series of fixes in core and dependencies
## 2018-02-16 - 4.2.28 - BREAKING CHANGE(scope)
4.2.28
- switch to pushrocks scope

View File

@@ -36,13 +36,10 @@
}
},
"tsdoc": {
"classes": [
"SmartFile",
"StreamFile"
],
"classes": ["SmartFile", "StreamFile"],
"descriptions": [
"the purpose of the StreamFile class is to provide a hybrid interface between streaming files and simple handling when writing and reading those files multiple times."
],
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
}
}
}

View File

@@ -1,13 +1,13 @@
{
"name": "@push.rocks/smartfile",
"private": false,
"version": "11.1.1",
"version": "11.2.7",
"description": "Provides comprehensive tools for efficient file management in Node.js using TypeScript, including handling streams, virtual directories, and various file operations.",
"main": "dist_ts/index.js",
"typings": "dist_ts/index.d.ts",
"type": "module",
"scripts": {
"test": "(tstest test/)",
"test": "(tstest test/ --verbose --logfile --timeout 120)",
"build": "(tsbuild --web --allowimplicitany)",
"buildDocs": "tsdoc"
},
@@ -38,33 +38,31 @@
"author": "Lossless GmbH <hello@lossless.com> (https://lossless.com)",
"license": "MIT",
"bugs": {
"url": "https://gitlab.com/push.rocks/smartfile/issues"
"url": "https://code.foss.global/push.rocks/smartfile/issues"
},
"homepage": "https://code.foss.global/push.rocks/smartfile",
"homepage": "https://code.foss.global/push.rocks/smartfile#readme",
"dependencies": {
"@push.rocks/lik": "^6.1.0",
"@push.rocks/lik": "^6.2.2",
"@push.rocks/smartdelay": "^3.0.5",
"@push.rocks/smartfile-interfaces": "^1.0.7",
"@push.rocks/smarthash": "^3.0.4",
"@push.rocks/smarthash": "^3.2.3",
"@push.rocks/smartjson": "^5.0.20",
"@push.rocks/smartmime": "^2.0.4",
"@push.rocks/smartpath": "^5.0.18",
"@push.rocks/smartpromise": "^4.1.0",
"@push.rocks/smartrequest": "^2.0.23",
"@push.rocks/smartpath": "^6.0.0",
"@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/glob": "^8.1.0",
"@types/js-yaml": "^4.0.9",
"fs-extra": "^11.2.0",
"glob": "^11.0.0",
"fs-extra": "^11.3.1",
"glob": "^11.0.3",
"js-yaml": "^4.1.0"
},
"devDependencies": {
"@git.zone/tsbuild": "^2.2.0",
"@git.zone/tsbuild": "^2.6.4",
"@git.zone/tsrun": "^1.3.3",
"@git.zone/tstest": "^1.0.90",
"@push.rocks/tapbundle": "^5.5.4",
"@types/node": "^22.10.5"
"@git.zone/tstest": "^2.3.4",
"@types/node": "^22.15.21"
},
"files": [
"ts/**/*",
@@ -80,5 +78,9 @@
],
"browserslist": [
"last 1 chrome versions"
]
],
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39",
"pnpm": {
"overrides": {}
}
}

8715
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1 +1,33 @@
# SmartFile Implementation Hints
## listFileTree Function Enhancement (ts/fs.ts:367-415)
### Issue Fixed
The `listFileTree` function previously had inconsistent behavior with `**/*.extension` patterns across different systems and glob implementations. Some implementations would miss root-level files when using patterns like `**/*.ts`.
### Solution Implemented
Modified the function to explicitly handle `**/` patterns by:
1. Detecting when a pattern starts with `**/`
2. Extracting the file pattern after `**/` (e.g., `*.ts` from `**/*.ts`)
3. Running both the original pattern and the extracted root pattern
4. Using a Set to deduplicate results and ensure consistent ordering
### Key Benefits
- Guarantees consistent behavior across all systems
- Ensures both root-level and nested files are found with `**/*` patterns
- Maintains backward compatibility
- No performance degradation due to efficient deduplication
### Test Coverage
Added comprehensive tests to verify:
- Both root and nested files are found with `**/*.ts`
- No duplicate entries in results
- Edge cases with various file extensions work correctly
This fix ensures tools like `tsbuild check **/*.ts` work reliably across all systems.

429
readme.md
View File

@@ -1,226 +1,315 @@
# @push.rocks/smartfile
# @push.rocks/smartfile 📁
> Provides a robust suite of tools for managing files in Node.js using TypeScript.
> **A powerful, TypeScript-based file management library for Node.js**
## Install
## 🚀 What is smartfile?
To integrate `@push.rocks/smartfile` into your project, run:
`@push.rocks/smartfile` is your go-to solution for file operations in Node.js. It offers a clean, promise-based API for handling files, directories, streams, and even virtual filesystems - all while maintaining maximum performance and reliability.
Think of it as `fs` on steroids, with TypeScript superpowers! 💪
## 💾 Installation
```bash
npm install @push.rocks/smartfile
```
## Usage
## ✨ Features
`@push.rocks/smartfile` offers extensive file management utilities, enabling seamless file processing with TypeScript in a Node.js environment. Below are detailed examples showcasing various features of the module.
- 🔥 **Streaming Support** - Handle massive files with ease using `StreamFile`
- 📦 **Virtual Directories** - Work with in-memory file structures
- 🌐 **URL Support** - Directly work with files from URLs
- 🎯 **TypeScript First** - Full type safety and IntelliSense support
-**Promise-based API** - Modern async/await patterns throughout
- 🛠️ **Comprehensive Toolset** - From basic CRUD to advanced operations
### Quick Start
First, ensure you're working in an environment that supports ECMAScript modules (ESM) and TypeScript. Heres how youd generally import and use `@push.rocks/smartfile`:
## 📚 Quick Start
```typescript
import { SmartFile, StreamFile, VirtualDirectory, fs, memory, interpreter } from '@push.rocks/smartfile';
import * as smartfile from '@push.rocks/smartfile';
// Read a file
const content = await smartfile.fs.toStringSync('./my-file.txt');
// Write a file
await smartfile.memory.toFs('Hello World!', './output.txt');
// Work with JSON
const data = await smartfile.fs.toObjectSync('./data.json');
```
### Working with `SmartFile`
## 🎨 Core Components
#### Reading Files
### SmartFile Class
To read from a file and convert it to a `SmartFile` instance:
The `SmartFile` class represents a single file with powerful manipulation capabilities:
```typescript
const myJsonSmartFile: SmartFile = await SmartFile.fromFilePath('./somePath/data.json');
const jsonData = JSON.parse(myJsonSmartFile.contents.toString());
console.log(jsonData); // Assuming the file contains JSON content
import { SmartFile } from '@push.rocks/smartfile';
// Create from file path
const fileFromPath = await SmartFile.fromFilePath('./data.json');
// Create from URL
const fileFromUrl = await SmartFile.fromUrl('https://example.com/config.json');
// Create from text
const fileFromText = await SmartFile.fromString(
'./my-file.txt',
'This is my content',
'utf8'
);
// Create from Buffer
const fileFromBuffer = await SmartFile.fromBuffer(
'./binary.dat',
Buffer.from([0x00, 0x01, 0x02])
);
// Edit content
await fileFromPath.editContentAsString(async (content) => {
return content.replace(/old/g, 'new');
});
// Write to disk
await fileFromPath.write();
// Get content
const contentString = fileFromPath.parseContentAsString();
const contentBuffer = fileFromPath.parseContentAsBuffer();
```
#### Writing Files
### StreamFile Class 🌊
To write data to a file through a `SmartFile`:
Perfect for handling large files without memory overhead:
```typescript
const filePath: string = './output/outputData.json';
const content: string = JSON.stringify({ key: 'value' });
await memory.toFs(content, filePath);
import { StreamFile } from '@push.rocks/smartfile';
// Create from path
const streamFile = await StreamFile.fromPath('./bigfile.zip');
// Create from URL
const urlStream = await StreamFile.fromUrl('https://example.com/large.mp4');
// Create from buffer
const bufferStream = StreamFile.fromBuffer(
Buffer.from('streaming content'),
'stream.txt'
);
// Write to disk
await streamFile.writeToDisk('./output/bigfile.zip');
// Get as buffer (careful with large files!)
const buffer = await streamFile.getContentAsBuffer();
// Get as stream
const readStream = await streamFile.createReadStream();
```
### Streaming Large Files with `StreamFile`
### VirtualDirectory Class 📂
When dealing with large files, you can use `StreamFile` to handle such files efficiently, minimizing memory usage:
Manage collections of files as virtual filesystems:
```typescript
const largeFile: StreamFile = await StreamFile.fromPath('./largeInput/largeFile.mp4');
await largeFile.writeToDisk('./largeOutput/largeFileCopy.mp4');
import { VirtualDirectory } from '@push.rocks/smartfile';
// Create from filesystem
const vDir = await VirtualDirectory.fromFsDirPath('./src');
// Create from file array
const vDirFromFiles = await VirtualDirectory.fromFileArray([
await SmartFile.fromFilePath('./file1.txt'),
await SmartFile.fromFilePath('./file2.txt')
]);
// Add files
vDir.addSmartfiles([
await SmartFile.fromString('./virtual/new.txt', 'content')
]);
// List files
const files = vDir.listFiles();
const directories = vDir.listDirectories();
// Get file
const file = vDir.getFileByPath('./some/path.txt');
// Save to disk
await vDir.saveToDisk('./output');
// Load from disk
await vDir.loadFromDisk('./source');
```
### Working with Virtual Directories
## 🛠️ File Operations
Handling multiple files as if they were part of a file system:
### Basic Operations
```typescript
const virtualDir = await VirtualDirectory.fromFsDirPath('./data/inputDir');
await virtualDir.saveToDisk('./data/outputDir');
// Check existence
const exists = await smartfile.fs.fileExists('./file.txt');
const existsSync = smartfile.fs.fileExistsSync('./file.txt');
// Read operations
const content = await smartfile.fs.toStringSync('./file.txt');
const buffer = await smartfile.fs.toBuffer('./file.txt');
const object = await smartfile.fs.toObjectSync('./data.json');
// Write operations
await smartfile.memory.toFs('content', './output.txt');
smartfile.memory.toFsSync('content', './output-sync.txt');
// Copy operations
await smartfile.fs.copy('./source.txt', './dest.txt');
await smartfile.fs.copy('./src-dir', './dest-dir');
// Delete operations
await smartfile.fs.remove('./file.txt');
await smartfile.fs.removeSync('./file-sync.txt');
await smartfile.fs.removeMany(['./file1.txt', './file2.txt']);
// Ensure operations (create if not exists)
await smartfile.fs.ensureDir('./my/deep/directory');
await smartfile.fs.ensureFile('./my/file.txt');
await smartfile.fs.ensureEmptyDir('./empty-dir');
```
### File System Operations
`@push.rocks/smartfile` provides a suite of utilities for common file system operations such as copying, deleting, and listing files or directories.
#### Copying a File
### Directory Operations
```typescript
await fs.copy('./sourceFile.txt', './destinationFile.txt');
// List contents
const files = await smartfile.fs.listFiles('./directory');
const folders = await smartfile.fs.listFolders('./directory');
const items = await smartfile.fs.listAllItems('./directory');
// Get file tree
const tree = await smartfile.fs.listFileTree('./src', '**/*.ts');
// Directory checks
const isDir = await smartfile.fs.isDirectory('./path');
const isFile = await smartfile.fs.isFile('./path');
```
#### Deleting a Directory
### Advanced Features
```typescript
await fs.remove('./directoryToDelete');
// Wait for file to be ready
await smartfile.fs.waitForFileToBeReady('./file.txt');
// Stream operations
const readStream = smartfile.fsStream.createReadStream('./input.txt');
const writeStream = smartfile.fsStream.createWriteStream('./output.txt');
// File type detection
const fileType = smartfile.interpreter.filetype('./document.pdf');
// Returns: 'pdf'
// Smart read stream (with custom processing)
const smartStream = new smartfile.fsStream.SmartReadStream('./data.txt');
smartStream.on('data', (chunk) => {
// Process chunk
console.log(chunk.toString());
});
```
#### Listing Files in a Directory
## 🔄 Working with Multiple Files
```typescript
const fileList: string[] = await fs.listFiles('./someDirectory');
console.log(fileList);
```
// Process multiple SmartFiles
const files = await smartfile.fs.fileTreeToObject(
'./src',
'**/*.{ts,js}'
);
### Advanced File Management
For specialized file operations, such as editing the contents of a file or streaming files from URLs, `@push.rocks/smartfile` includes advanced management features.
#### Editing a Files Contents
```typescript
const smartFileToEdit: SmartFile = await SmartFile.fromFilePath('./editableFile.txt');
await smartFileToEdit.editContentAsString(async (content) => content.replace(/originalText/g, 'newText'));
await smartFileToEdit.write();
```
#### Streaming a File from a URL
```typescript
const streamedFile: StreamFile = await StreamFile.fromUrl('https://example.com/file.pdf');
await streamedFile.writeToDisk('./downloadedFiles/file.pdf');
```
### Working with File Buffers and Streams
`@push.rocks/smartfile` allows you to easily work with files using Buffers and Streams, facilitating operations like file transformations, uploads, and downloads.
#### Creating a SmartFile from a Buffer
```typescript
const buffer: Buffer = Buffer.from('Sample data');
const bufferSmartFile: SmartFile = await SmartFile.fromBuffer('./bufferFile.txt', buffer);
await bufferSmartFile.write();
```
### Using `VirtualDirectory` for Complex File Management
`VirtualDirectory` simplifies the management of multiple files that are otherwise scattered across different directories or created at runtime.
#### Creating a `VirtualDirectory`
To create a `VirtualDirectory` from an existing file directory:
```typescript
const virtualDirectory = await VirtualDirectory.fromFsDirPath('./someDirectory');
```
#### Adding More Files
You can add more `SmartFile` instances to your `VirtualDirectory`:
```typescript
const additionalFiles = [
await SmartFile.fromFilePath('./anotherDirectory/file1.txt'),
await SmartFile.fromFilePath('./anotherDirectory/file2.txt')
// Write array to disk
const smartfiles = [
await SmartFile.fromString('file1.txt', 'content1'),
await SmartFile.fromString('file2.txt', 'content2')
];
virtualDirectory.addSmartfiles(additionalFiles);
await smartfile.memory.smartfileArrayToFs(smartfiles, './output');
```
#### Saving `VirtualDirectory` to Disk
## 🎯 Real-World Examples
Save your entire `VirtualDirectory` to disk:
### Website Bundler
```typescript
// Bundle website assets
const website = await VirtualDirectory.fromFsDirPath('./website');
const bundle = await website.smartfileArray;
// Process all CSS files
for (const file of bundle.filter(f => f.path.endsWith('.css'))) {
await file.editContentAsString(async (css) => {
// Minify CSS here
return css.replace(/\s+/g, ' ');
});
}
// Save processed bundle
await website.saveToDisk('./dist');
```
### File Watcher & Processor
```typescript
// Watch for new files and process them
import { SmartFile, StreamFile } from '@push.rocks/smartfile';
async function processLargeFile(filePath: string) {
const streamFile = await StreamFile.fromPath(filePath);
// Stream to processed location
await streamFile.writeToDisk(`./processed/${path.basename(filePath)}`);
// Clean up original
await smartfile.fs.remove(filePath);
}
```
### Configuration Manager
```typescript
// Load and merge config files
const defaultConfig = await smartfile.fs.toObjectSync('./config.default.json');
const userConfig = await smartfile.fs.toObjectSync('./config.user.json');
const merged = { ...defaultConfig, ...userConfig };
await smartfile.memory.toFs(
JSON.stringify(merged, null, 2),
'./config.final.json'
);
```
## 🌟 API Reference
### Core Modules
- `fs` - File system operations
- `fsStream` - Streaming operations
- `memory` - Memory/buffer operations
- `interpreter` - File type detection
### Main Classes
- `SmartFile` - Single file representation
- `StreamFile` - Streaming file operations
- `VirtualDirectory` - Virtual filesystem management
## 🏗️ TypeScript Support
Full TypeScript support with comprehensive type definitions:
```typescript
await virtualDirectory.saveToDisk('./outputDirectory');
import type { SmartFile, StreamFile, VirtualDirectory } from '@push.rocks/smartfile';
// All methods are fully typed
const processFile = async (file: SmartFile): Promise<void> => {
const content = file.parseContentAsString();
// TypeScript knows content is string
};
```
### Utilizing StreamFile for Efficient File Handling
Using `StreamFile` can be especially beneficial for large files or when performing streaming operations.
#### Streaming from a URL
`StreamFile` provides capabilities to stream files directly from URLs, making it easier to work with remote content.
```typescript
const urlStreamFile: StreamFile = await StreamFile.fromUrl('https://example.com/largefile.zip');
await urlStreamFile.writeToDisk('./downloadedFiles/largefile.zip');
```
### Combining Buffer and Stream Approaches
Create `StreamFile` from a buffer for efficient, multi-use streams.
```typescript
const buffer = Buffer.from('Streaming buffer content');
const bufferStreamFile = StreamFile.fromBuffer(buffer, 'bufferBasedStream.txt');
await bufferStreamFile.writeToDisk('./streams/bufferBasedStream.txt');
```
### Read and Write Operations with StreamFile
Perform read and write operations efficiently using `StreamFile`.
```typescript
const fileStream = await StreamFile.fromPath('./inputData/largeFile.data');
await fileStream.writeToDisk('./outputData/largeFileCopy.data');
```
Check for completeness of your read and write operations, ensuring the integrity of file content.
```typescript
const readBuffer = await fileStream.getContentAsBuffer();
console.log(readBuffer.toString());
```
### Ensuring File Readiness for Streaming
Ensure a file is ready for streaming or create a custom readable stream incorporating data transformation.
```typescript
const smartReadStream = new SmartReadStream('./incomingData/sample.data');
smartReadStream.on('data', (chunk) => {
console.log('New Data Chunk:', chunk.toString());
});
```
### File Transformation with SmartReadStream
Perform transformations on the stream of data while reading:
```typescript
smartReadStream.on('data', (chunk) => {
// Perform some transformation
const transformedChunk = chunk.toString().toUpperCase();
console.log('Transformed Data Chunk:', transformedChunk);
});
```
### Streaming with SmartReadStream
Stream data from a `SmartReadStream` to a file efficiently managing large datasets.
```typescript
const transformedWriteStream = fs.createWriteStream('./processedData/transformed.data');
smartReadStream.pipe(transformedWriteStream);
```
`@push.rocks/smartfile` significantly simplifies the handling of complex file operations in Node.js projects, making these tasks straightforward while maintaining efficiency and ease of use. Explore and leverage these features to enhance your project's file management capabilities.
## License and Legal Information
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository.
@@ -238,4 +327,4 @@ Registered at District court Bremen HRB 35230 HB, Germany
For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.

View File

@@ -1,5 +1,5 @@
import * as path from 'path';
import { expect, tap } from '@push.rocks/tapbundle';
import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as smartfile from '../ts/index.js'; // adjust the import path as needed
// Test assets path
@@ -9,55 +9,85 @@ const testAssetsPath = './test/testassets/';
// StreamFile tests
// ---------------------------
tap.test('StreamFile.fromPath should create a StreamFile from a file path', async () => {
const streamFile = await smartfile.StreamFile.fromPath(path.join(testAssetsPath, 'mytest.json'));
expect(streamFile).toBeInstanceOf(smartfile.StreamFile);
const contentBuffer = await streamFile.getContentAsBuffer();
expect(contentBuffer).toBeInstanceOf(Buffer);
});
tap.test(
'StreamFile.fromPath should create a StreamFile from a file path',
async () => {
const streamFile = await smartfile.StreamFile.fromPath(
path.join(testAssetsPath, 'mytest.json'),
);
expect(streamFile).toBeInstanceOf(smartfile.StreamFile);
const contentBuffer = await streamFile.getContentAsBuffer();
expect(contentBuffer).toBeInstanceOf(Buffer);
},
);
tap.test('StreamFile.fromUrl should create a StreamFile from a URL', async () => {
const streamFile = await smartfile.StreamFile.fromUrl('http://example.com/somefile.json');
expect(streamFile).toBeInstanceOf(smartfile.StreamFile);
});
tap.test(
'StreamFile.fromUrl should create a StreamFile from a URL',
async () => {
const streamFile = await smartfile.StreamFile.fromUrl(
'http://example.com/somefile.json',
);
expect(streamFile).toBeInstanceOf(smartfile.StreamFile);
},
);
tap.test('StreamFile.fromBuffer should create a StreamFile from a Buffer', async () => {
const buffer = Buffer.from('Some content');
const streamFile = smartfile.StreamFile.fromBuffer(buffer, 'bufferfile.txt');
expect(streamFile).toBeInstanceOf(smartfile.StreamFile);
});
tap.test(
'StreamFile.fromBuffer should create a StreamFile from a Buffer',
async () => {
const buffer = Buffer.from('Some content');
const streamFile = smartfile.StreamFile.fromBuffer(
buffer,
'bufferfile.txt',
);
expect(streamFile).toBeInstanceOf(smartfile.StreamFile);
},
);
tap.test('StreamFile should write the stream to disk', async () => {
const streamFile = await smartfile.StreamFile.fromPath(path.join(testAssetsPath, 'mytest.json'));
await streamFile.writeToDisk(path.join(testAssetsPath, 'temp', 'mytest.json'));
const streamFile = await smartfile.StreamFile.fromPath(
path.join(testAssetsPath, 'mytest.json'),
);
await streamFile.writeToDisk(
path.join(testAssetsPath, 'temp', 'mytest.json'),
);
// Verify the file was written
expect(
// We'll use the fileExists method from your smartfile library
// Replace with the actual method you use to check file existence
await smartfile.fs.fileExists(path.join(testAssetsPath, 'temp', 'mytest.json'))
await smartfile.fs.fileExists(
path.join(testAssetsPath, 'temp', 'mytest.json'),
),
).toBeTrue();
});
tap.test('StreamFile should write to a directory', async () => {
const streamFile = await smartfile.StreamFile.fromPath(path.join(testAssetsPath, 'mytest.json'));
const streamFile = await smartfile.StreamFile.fromPath(
path.join(testAssetsPath, 'mytest.json'),
);
await streamFile.writeToDir(path.join(testAssetsPath, 'temp'));
// Verify the file was written
expect(
await smartfile.fs.fileExists(path.join(testAssetsPath, 'temp', 'mytest.json'))
await smartfile.fs.fileExists(
path.join(testAssetsPath, 'temp', 'mytest.json'),
),
).toBeTrue();
});
tap.test('StreamFile should return content as a buffer', async () => {
const streamFile = await smartfile.StreamFile.fromPath(path.join(testAssetsPath, 'mytest.json'));
const streamFile = await smartfile.StreamFile.fromPath(
path.join(testAssetsPath, 'mytest.json'),
);
const contentBuffer = await streamFile.getContentAsBuffer();
expect(contentBuffer).toBeInstanceOf(Buffer);
// Further checks on the content can be added here if necessary
});
tap.test('StreamFile should return content as a string', async () => {
const streamFile = await smartfile.StreamFile.fromPath(path.join(testAssetsPath, 'mytest.json'));
const streamFile = await smartfile.StreamFile.fromPath(
path.join(testAssetsPath, 'mytest.json'),
);
const contentString = await streamFile.getContentAsString();
expect(typeof contentString).toBeTypeofString();
expect(contentString).toBeTypeofString();
// Verify the content matches what's expected
// This assumes the file contains a JSON object with a key 'key1' with value 'this works'
expect(JSON.parse(contentString).key1).toEqual('this works');

View File

@@ -1,85 +1,168 @@
import * as smartfile from '../ts/index.js';
import * as path from 'path';
import { expect, tap } from '@push.rocks/tapbundle';
import { expect, tap } from '@git.zone/tstest/tapbundle';
// ---------------------------
// smartfile.fs
// ---------------------------
tap.test('.fs.fileExistsSync -> should return an accurate boolean', async () => {
// tslint:disable-next-line: no-unused-expression
expect(smartfile.fs.fileExistsSync('./test/testassets/mytest.json')).toBeTrue();
// tslint:disable-next-line: no-unused-expression
expect(smartfile.fs.fileExistsSync('./test/testassets/notthere.json')).toBeFalse();
});
tap.test(
'.fs.fileExistsSync -> should return an accurate boolean',
async () => {
// tslint:disable-next-line: no-unused-expression
expect(
smartfile.fs.fileExistsSync('./test/testassets/mytest.json'),
).toBeTrue();
// tslint:disable-next-line: no-unused-expression
expect(
smartfile.fs.fileExistsSync('./test/testassets/notthere.json'),
).toBeFalse();
},
);
tap.test('.fs.fileExists -> should resolve or reject a promise', async () => {
expect(smartfile.fs.fileExists('./test/testassets/mytest.json')).toBeInstanceOf(Promise);
await smartfile.fs.fileExists('./test/testassets/mytest.json');
await smartfile.fs.fileExists('./test/testassets/notthere.json').catch((err) => {
return expect(err.message).toEqual(
"ENOENT: no such file or directory, access './test/testassets/notthere.json'"
await expect(
smartfile.fs.fileExists('./test/testassets/mytest.json'),
).resolves.toBeTrue();
await expect(
smartfile.fs.fileExists('./test/testassets/notthere.json'),
).resolves.toBeFalse();
});
tap.test(
'.fs.listFoldersSync() -> should get the file type from a string',
async () => {
expect(smartfile.fs.listFoldersSync('./test/testassets/')).toContain(
'testfolder',
);
});
});
expect(smartfile.fs.listFoldersSync('./test/testassets/')).not.toContain(
'notExistentFolder',
);
},
);
tap.test('.fs.listFoldersSync() -> should get the file type from a string', async () => {
expect(smartfile.fs.listFoldersSync('./test/testassets/')).toContain('testfolder');
expect(smartfile.fs.listFoldersSync('./test/testassets/')).not.toContain('notExistentFolder');
});
tap.test(
'.fs.listFolders() -> should get the file type from a string',
async () => {
const folderArrayArg = await smartfile.fs.listFolders('./test/testassets/');
expect(folderArrayArg).toContain('testfolder');
expect(folderArrayArg).not.toContain('notExistentFolder');
},
);
tap.test('.fs.listFolders() -> should get the file type from a string', async () => {
const folderArrayArg = await smartfile.fs.listFolders('./test/testassets/');
expect(folderArrayArg).toContain('testfolder');
expect(folderArrayArg).not.toContain('notExistentFolder');
});
tap.test(
'.fs.listFilesSync() -> should get the file type from a string',
async () => {
expect(smartfile.fs.listFilesSync('./test/testassets/')).toContain(
'mytest.json',
);
expect(smartfile.fs.listFilesSync('./test/testassets/')).not.toContain(
'notExistentFile',
);
expect(
smartfile.fs.listFilesSync('./test/testassets/', /mytest\.json/),
).toContain('mytest.json');
expect(
smartfile.fs.listFilesSync('./test/testassets/', /mytests.json/),
).not.toContain('mytest.json');
},
);
tap.test('.fs.listFilesSync() -> should get the file type from a string', async () => {
expect(smartfile.fs.listFilesSync('./test/testassets/')).toContain('mytest.json');
expect(smartfile.fs.listFilesSync('./test/testassets/')).not.toContain('notExistentFile');
expect(smartfile.fs.listFilesSync('./test/testassets/', /mytest\.json/)).toContain('mytest.json');
expect(smartfile.fs.listFilesSync('./test/testassets/', /mytests.json/)).not.toContain(
'mytest.json'
);
});
tap.test('.fs.listFiles() -> should get the file type from a string', async () => {
const folderArrayArg = await smartfile.fs.listFiles('./test/testassets/');
expect(folderArrayArg).toContain('mytest.json');
expect(folderArrayArg).not.toContain('notExistentFile');
});
tap.test(
'.fs.listFiles() -> should get the file type from a string',
async () => {
const folderArrayArg = await smartfile.fs.listFiles('./test/testassets/');
expect(folderArrayArg).toContain('mytest.json');
expect(folderArrayArg).not.toContain('notExistentFile');
},
);
tap.test('.fs.listFileTree() -> should get a file tree', async () => {
const folderArrayArg = await smartfile.fs.listFileTree(
path.resolve('./test/testassets/'),
'**/*.txt'
'**/*.txt',
);
expect(folderArrayArg).toContain('testfolder/testfile1.txt');
expect(folderArrayArg).not.toContain('mytest.json');
});
tap.test('.fs.fileTreeToObject -> should read a file tree into an Object', async () => {
const fileArrayArg = await smartfile.fs.fileTreeToObject(
path.resolve('./test/testassets/'),
'**/*.txt'
);
expect(fileArrayArg[0]).toBeInstanceOf(smartfile.SmartFile);
expect(fileArrayArg[0].contents.toString()).toEqual(fileArrayArg[0].contentBuffer.toString());
});
tap.test(
'.fs.listFileTree() -> should find both root and nested .ts files with **/*.ts pattern',
async () => {
const tsFiles = await smartfile.fs.listFileTree(process.cwd(), '**/*.ts');
// Should find both root-level and nested TypeScript files
expect(tsFiles).toContain('ts/index.ts');
expect(tsFiles).toContain('ts/classes.smartfile.ts');
expect(tsFiles).toContain('test/test.ts');
// Should find files in multiple levels of nesting
expect(tsFiles.filter((f) => f.endsWith('.ts')).length).toBeGreaterThan(5);
// Verify it finds files at all levels (root 'ts/' and nested 'test/')
const hasRootLevelTs = tsFiles.some(
(f) => f.startsWith('ts/') && f.endsWith('.ts'),
);
const hasNestedTs = tsFiles.some(
(f) => f.startsWith('test/') && f.endsWith('.ts'),
);
expect(hasRootLevelTs).toBeTrue();
expect(hasNestedTs).toBeTrue();
},
);
tap.test(
'.fs.listFileTree() -> should handle edge cases with **/ patterns consistently',
async () => {
// Test that our fix ensures no duplicate files in results
const jsonFiles = await smartfile.fs.listFileTree(
path.resolve('./test/testassets/'),
'**/*.json',
);
const uniqueFiles = [...new Set(jsonFiles)];
expect(jsonFiles.length).toEqual(uniqueFiles.length);
// Test that it finds root level files with **/ patterns
const txtFiles = await smartfile.fs.listFileTree(
path.resolve('./test/testassets/'),
'**/*.txt',
);
// Should include both direct files and nested files
expect(txtFiles).toContain('mytest.txt');
expect(txtFiles).toContain('testfolder/testfile1.txt');
},
);
tap.test(
'.fs.fileTreeToObject -> should read a file tree into an Object',
async () => {
const fileArrayArg = await smartfile.fs.fileTreeToObject(
path.resolve('./test/testassets/'),
'**/*.txt',
);
expect(fileArrayArg[0]).toBeInstanceOf(smartfile.SmartFile);
expect(fileArrayArg[0].contents.toString()).toEqual(
fileArrayArg[0].contentBuffer.toString(),
);
},
);
tap.test('.fs.copy() -> should copy a directory', async () => {
await smartfile.fs.copy('./test/testassets/testfolder/', './test/testassets/temp/');
await smartfile.fs.copy(
'./test/testassets/testfolder/',
'./test/testassets/temp/',
);
});
tap.test('.fs.copy() -> should copy a file', async () => {
await smartfile.fs.copy('./test/testassets/mytest.yaml', './test/testassets/temp/mytest.yaml');
await smartfile.fs.copy(
'./test/testassets/mytest.yaml',
'./test/testassets/temp/mytest.yaml',
);
});
tap.test('.fs.copy() -> should copy a file and rename it', async () => {
await smartfile.fs.copy(
'./test/testassets/mytest.yaml',
'./test/testassets/temp/mytestRenamed.yaml'
'./test/testassets/temp/mytestRenamed.yaml',
);
});
@@ -89,70 +172,105 @@ tap.test('.fs.remove -> should remove single files', async () => {
await smartfile.fs.remove('./test/testassets/temp/mytestRenamed.yaml');
});
tap.test('.fs.removeSync -> should remove single files synchronouly', async () => {
smartfile.fs.removeSync('./test/testassets/temp/testfile1.txt');
expect(smartfile.fs.fileExistsSync('./test/testassets/temp/testfile1.txt')).toBeFalse();
});
tap.test(
'.fs.removeSync -> should remove single files synchronouly',
async () => {
smartfile.fs.removeSync('./test/testassets/temp/testfile1.txt');
expect(
smartfile.fs.fileExistsSync('./test/testassets/temp/testfile1.txt'),
).toBeFalse();
},
);
tap.test('.fs.removeMany -> should remove and array of files', async () => {
smartfile.fs
.removeMany(['./test/testassets/temp/testfile1.txt', './test/testassets/temp/testfile2.txt'])
.removeMany([
'./test/testassets/temp/testfile1.txt',
'./test/testassets/temp/testfile2.txt',
])
.then(() => {
expect(smartfile.fs.fileExistsSync('./test/testassets/temp/testfile1.txt')).toBeFalse();
expect(smartfile.fs.fileExistsSync('./test/testassets/temp/testfile2.txt')).toBeFalse();
expect(
smartfile.fs.fileExistsSync('./test/testassets/temp/testfile1.txt'),
).toBeFalse();
expect(
smartfile.fs.fileExistsSync('./test/testassets/temp/testfile2.txt'),
).toBeFalse();
});
});
tap.test('.fs.removeManySync -> should remove and array of single files synchronouly', async () => {
smartfile.fs.removeManySync([
'./test/testassets/temp/testfile1.txt',
'./test/testassets/temp/testfile2.txt',
]);
expect(smartfile.fs.fileExistsSync('./test/testassets/temp/testfile1.txt')).toBeFalse();
expect(smartfile.fs.fileExistsSync('./test/testassets/temp/testfile2.txt')).toBeFalse();
});
tap.test(
'.fs.removeManySync -> should remove and array of single files synchronouly',
async () => {
smartfile.fs.removeManySync([
'./test/testassets/temp/testfile1.txt',
'./test/testassets/temp/testfile2.txt',
]);
expect(
smartfile.fs.fileExistsSync('./test/testassets/temp/testfile1.txt'),
).toBeFalse();
expect(
smartfile.fs.fileExistsSync('./test/testassets/temp/testfile2.txt'),
).toBeFalse();
},
);
tap.test('.fs.toObjectSync() -> should read an .yaml file to an object', async () => {
const testData = smartfile.fs.toObjectSync('./test/testassets/mytest.yaml');
expect(testData.key1).toEqual('this works');
expect(testData.key2).toEqual('this works too');
});
tap.test(
'.fs.toObjectSync() -> should read an .yaml file to an object',
async () => {
const testData = smartfile.fs.toObjectSync('./test/testassets/mytest.yaml');
expect(testData.key1).toEqual('this works');
expect(testData.key2).toEqual('this works too');
},
);
tap.test(
'.fs.toObjectSync() -> should state unknown file type for unknown file types',
async () => {
const testData = smartfile.fs.toObjectSync('./test/testassets/mytest.txt');
}
},
);
tap.test('.fs.toObjectSync() -> should read an .json file to an object', async () => {
const testData = smartfile.fs.toObjectSync('./test/testassets/mytest.json');
expect(testData.key1).toEqual('this works');
expect(testData.key2).toEqual('this works too');
});
tap.test(
'.fs.toObjectSync() -> should read an .json file to an object',
async () => {
const testData = smartfile.fs.toObjectSync('./test/testassets/mytest.json');
expect(testData.key1).toEqual('this works');
expect(testData.key2).toEqual('this works too');
},
);
tap.test('.fs.toStringSync() -> should read a file to a string', async () => {
expect(smartfile.fs.toStringSync('./test/testassets/mytest.txt')).toEqual('Some TestString &&%$');
expect(smartfile.fs.toStringSync('./test/testassets/mytest.txt')).toEqual(
'Some TestString &&%$',
);
});
// ---------------------------
// smartfile.interpreter
// ---------------------------
tap.test('.interpreter.filetype() -> should get the file type from a string', async () => {
expect(smartfile.interpreter.filetype('./somefolder/data.json')).toEqual('json');
});
tap.test(
'.interpreter.filetype() -> should get the file type from a string',
async () => {
expect(smartfile.interpreter.filetype('./somefolder/data.json')).toEqual(
'json',
);
},
);
// ---------------------------
// smartfile.memory
// ---------------------------
tap.test('.memory.toFs() -> should write a file to disk and return a promise', async () => {
const localString = 'myString';
await smartfile.memory.toFs(
localString,
path.join(process.cwd(), './test/testassets/temp/testMemToFs.txt')
);
});
tap.test(
'.memory.toFs() -> should write a file to disk and return a promise',
async () => {
const localString = 'myString';
await smartfile.memory.toFs(
localString,
path.join(process.cwd(), './test/testassets/temp/testMemToFs.txt'),
);
},
);
tap.test(
'.memory.toFsSync() -> should write a file to disk and return true if successfull',
@@ -160,9 +278,9 @@ tap.test(
const localString = 'myString';
smartfile.memory.toFsSync(
localString,
path.join(process.cwd(), './test/testassets/temp/testMemToFsSync.txt')
path.join(process.cwd(), './test/testassets/temp/testMemToFsSync.txt'),
);
}
},
);
// ---------------------------
@@ -172,7 +290,7 @@ tap.test(
tap.test('.Smartfile -> should produce vinyl compatible files', async () => {
const smartfileArray = await smartfile.fs.fileTreeToObject(
process.cwd(),
'./test/testassets/testfolder/**/*'
'./test/testassets/testfolder/**/*',
);
const localSmartfile = smartfileArray[0];
expect(localSmartfile).toBeInstanceOf(smartfile.SmartFile);
@@ -186,7 +304,10 @@ tap.test('.Smartfile -> should produce vinyl compatible files', async () => {
});
tap.test('should output a smartfile array to disk', async () => {
const smartfileArray = await smartfile.fs.fileTreeToObject('./test/testassets/testfolder/', '*');
const smartfileArray = await smartfile.fs.fileTreeToObject(
'./test/testassets/testfolder/',
'*',
);
for (const smartfileInstance of smartfileArray) {
console.log(smartfileInstance.relative);
console.log(smartfileInstance.path);
@@ -195,14 +316,18 @@ tap.test('should output a smartfile array to disk', async () => {
}
await smartfile.memory.smartfileArrayToFs(
smartfileArray,
path.resolve('./test/testassets/temp/testoutput/')
path.resolve('./test/testassets/temp/testoutput/'),
);
});
tap.test('should create, store and retrieve valid smartfiles', async () => {
const fileString = 'hi there';
const filePath = './test/testassets/utf8.txt';
const smartfileInstance = await smartfile.SmartFile.fromString(filePath, fileString, 'utf8');
const smartfileInstance = await smartfile.SmartFile.fromString(
filePath,
fileString,
'utf8',
);
smartfileInstance.write();
const smartfileInstance2 = await smartfile.SmartFile.fromFilePath(filePath);
const retrievedString = smartfileInstance.contents.toString();
@@ -212,7 +337,11 @@ tap.test('should create, store and retrieve valid smartfiles', async () => {
tap.test('should get a hash', async () => {
const fileString = 'hi there';
const filePath = './test/testassets/utf8.txt';
const smartfileInstance = await smartfile.SmartFile.fromString(filePath, fileString, 'utf8');
const smartfileInstance = await smartfile.SmartFile.fromString(
filePath,
fileString,
'utf8',
);
const hash = await smartfileInstance.getHash();
console.log(hash);
});

View File

@@ -1,14 +1,18 @@
import { tap, expect } from '@push.rocks/tapbundle';
import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as smartfile from '../ts/index.js';
tap.test('should create a virtualdirectory', async () => {
const virtualDir = await smartfile.VirtualDirectory.fromFsDirPath('./test/testassets/testfolder');
const virtualDir = await smartfile.VirtualDirectory.fromFsDirPath(
'./test/testassets/testfolder',
);
expect(virtualDir.smartfileArray.length).toEqual(4);
});
tap.test('should write to a directory', async () => {
const virtualDir = await smartfile.VirtualDirectory.fromFsDirPath('./test/testassets/testfolder');
const virtualDir = await smartfile.VirtualDirectory.fromFsDirPath(
'./test/testassets/testfolder',
);
virtualDir.saveToDisk('./test/testassets/test');
});

View File

@@ -5,4 +5,3 @@
"nestedkey1": "hello"
}
}

View File

@@ -1,4 +1,4 @@
key1: this works
key2: this works too
key3:
nestedkey1: hello
nestedkey1: hello

View File

@@ -5,4 +5,3 @@
"nestedkey1": "hello"
}
}

View File

@@ -1,4 +1,4 @@
key1: this works
key2: this works too
key3:
nestedkey1: hello
nestedkey1: hello

View File

@@ -5,4 +5,3 @@
"nestedkey1": "hello"
}
}

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@push.rocks/smartfile',
version: '11.1.1',
version: '11.2.7',
description: 'Provides comprehensive tools for efficient file management in Node.js using TypeScript, including handling streams, virtual directories, and various file operations.'
}

View File

@@ -20,7 +20,10 @@ export class SmartFile extends plugins.smartjson.Smartjson {
* creates a Smartfile from a filePath
* @param filePath
*/
public static async fromFilePath(filePath: string, baseArg: string = process.cwd()) {
public static async fromFilePath(
filePath: string,
baseArg: string = process.cwd(),
) {
filePath = plugins.path.resolve(filePath);
const fileBuffer = fs.toBufferSync(filePath);
const smartfile = new SmartFile({
@@ -34,7 +37,7 @@ export class SmartFile extends plugins.smartjson.Smartjson {
public static async fromBuffer(
filePath: string,
contentBufferArg: Buffer,
baseArg: string = process.cwd()
baseArg: string = process.cwd(),
) {
const smartfile = new SmartFile({
contentBuffer: contentBufferArg,
@@ -49,7 +52,7 @@ export class SmartFile extends plugins.smartjson.Smartjson {
filePath: string,
contentStringArg: string,
encodingArg: 'utf8' | 'binary',
baseArg = process.cwd()
baseArg = process.cwd(),
) {
const smartfile = new SmartFile({
contentBuffer: Buffer.from(contentStringArg, encodingArg),
@@ -73,7 +76,7 @@ export class SmartFile extends plugins.smartjson.Smartjson {
public static async fromStream(
stream: plugins.stream.Readable,
filePath: string,
baseArg: string = process.cwd()
baseArg: string = process.cwd(),
): Promise<SmartFile> {
return new Promise<SmartFile>((resolve, reject) => {
const chunks: Buffer[] = [];
@@ -92,8 +95,12 @@ export class SmartFile extends plugins.smartjson.Smartjson {
}
public static async fromUrl(urlArg: string) {
const response = await plugins.smartrequest.getBinary(urlArg);
const smartfile = await SmartFile.fromBuffer(urlArg, response.body);
const response = await plugins.smartrequest.SmartRequest.create()
.url(urlArg)
.accept('binary')
.get();
const buffer = Buffer.from(await response.arrayBuffer());
const smartfile = await SmartFile.fromBuffer(urlArg, buffer);
return smartfile;
}
@@ -159,7 +166,10 @@ export class SmartFile extends plugins.smartjson.Smartjson {
* set contents from string
* @param contentString
*/
public setContentsFromString(contentString: string, encodingArg: 'utf8' | 'binary' = 'utf8') {
public setContentsFromString(
contentString: string,
encodingArg: 'utf8' | 'binary' = 'utf8',
) {
this.contents = Buffer.from(contentString, encodingArg);
}
@@ -169,7 +179,10 @@ export class SmartFile extends plugins.smartjson.Smartjson {
* - no argument write to exactly where the file was picked up
*/
public async write() {
let writePath = plugins.smartpath.transform.makeAbsolute(this.path, this.base);
let writePath = plugins.smartpath.transform.makeAbsolute(
this.path,
this.base,
);
console.log(`writing to ${writePath}`);
await memory.toFs(this.contentBuffer, writePath);
}
@@ -202,7 +215,9 @@ export class SmartFile extends plugins.smartjson.Smartjson {
* read file from disk
*/
public async read() {
this.contentBuffer = await fs.toBuffer(plugins.path.join(this.base, this.path));
this.contentBuffer = await fs.toBuffer(
plugins.path.join(this.base, this.path),
);
}
/**
@@ -220,7 +235,10 @@ export class SmartFile extends plugins.smartjson.Smartjson {
* @param writeToDisk (optional) If true, also renames the file on the disk.
* @returns The updated file path after renaming.
*/
public async rename(newName: string, writeToDisk: boolean = false): Promise<string> {
public async rename(
newName: string,
writeToDisk: boolean = false,
): Promise<string> {
// Validate the new name
if (!newName || typeof newName !== 'string') {
throw new Error('Invalid new name provided.');
@@ -238,8 +256,14 @@ export class SmartFile extends plugins.smartjson.Smartjson {
// Optionally write the renamed file to disk
if (writeToDisk) {
const oldAbsolutePath = plugins.smartpath.transform.makeAbsolute(oldFilePath, this.base);
const newAbsolutePath = plugins.smartpath.transform.makeAbsolute(newFilePath, this.base);
const oldAbsolutePath = plugins.smartpath.transform.makeAbsolute(
oldFilePath,
this.base,
);
const newAbsolutePath = plugins.smartpath.transform.makeAbsolute(
newFilePath,
this.base,
);
// Rename the file on disk
await plugins.fsExtra.rename(oldAbsolutePath, newAbsolutePath);
@@ -310,8 +334,12 @@ export class SmartFile extends plugins.smartjson.Smartjson {
public async getHash(typeArg: 'path' | 'content' | 'all' = 'all') {
const pathHash = await plugins.smarthash.sha256FromString(this.path);
const contentHash = await plugins.smarthash.sha256FromBuffer(this.contentBuffer);
const combinedHash = await plugins.smarthash.sha256FromString(pathHash + contentHash);
const contentHash = await plugins.smarthash.sha256FromBuffer(
this.contentBuffer,
);
const combinedHash = await plugins.smarthash.sha256FromString(
pathHash + contentHash,
);
switch (typeArg) {
case 'path':
return pathHash;
@@ -329,7 +357,9 @@ export class SmartFile extends plugins.smartjson.Smartjson {
this.path = this.path.replace(new RegExp(oldFileName + '$'), fileNameArg);
}
public async editContentAsString(editFuncArg: (fileStringArg: string) => Promise<string>) {
public async editContentAsString(
editFuncArg: (fileStringArg: string) => Promise<string>,
) {
const newFileString = await editFuncArg(this.contentBuffer.toString());
this.contentBuffer = Buffer.from(newFileString);
}

View File

@@ -3,7 +3,7 @@ import * as smartfileFs from './fs.js';
import * as smartfileFsStream from './fsstream.js';
import { Readable } from 'stream';
type TStreamSource = (streamFile: StreamFile) => Promise<Readable>;
type TStreamSource = (streamFile: StreamFile) => Promise<Readable | ReadableStream>;
/**
* The StreamFile class represents a file as a stream.
@@ -13,28 +13,41 @@ export class StreamFile {
// STATIC
public static async fromPath(filePath: string): Promise<StreamFile> {
const streamSource: TStreamSource = async (streamFileArg) => smartfileFsStream.createReadStream(filePath);
const streamSource: TStreamSource = async (streamFileArg) =>
smartfileFsStream.createReadStream(filePath);
const streamFile = new StreamFile(streamSource, filePath);
streamFile.multiUse = true;
streamFile.byteLengthComputeFunction = async () => {
const stats = await smartfileFs.stat(filePath);
return stats.size;
}
};
return streamFile;
}
public static async fromUrl(url: string): Promise<StreamFile> {
const streamSource: TStreamSource = async (streamFileArg) => plugins.smartrequest.getStream(url); // Replace with actual plugin method
const streamSource: TStreamSource = async (streamFileArg) => {
const response = await plugins.smartrequest.SmartRequest.create()
.url(url)
.get();
return response.stream();
};
const streamFile = new StreamFile(streamSource);
streamFile.multiUse = true;
streamFile.byteLengthComputeFunction = async () => {
const response = await plugins.smartrequest.getBinary(url); // TODO: switch to future .getBinaryByteLength()
return response.body.length;
}
const response = await plugins.smartrequest.SmartRequest.create()
.url(url)
.accept('binary')
.get();
const buffer = Buffer.from(await response.arrayBuffer());
return buffer.length;
};
return streamFile;
}
public static fromBuffer(buffer: Buffer, relativeFilePath?: string): StreamFile {
public static fromBuffer(
buffer: Buffer,
relativeFilePath?: string,
): StreamFile {
const streamSource: TStreamSource = async (streamFileArg) => {
const stream = new Readable();
stream.push(buffer);
@@ -54,7 +67,11 @@ export class StreamFile {
* @param multiUse If true, the stream can be read multiple times, caching its content.
* @returns A StreamFile instance.
*/
public static fromStream(stream: Readable, relativeFilePath?: string, multiUse: boolean = false): StreamFile {
public static fromStream(
stream: Readable,
relativeFilePath?: string,
multiUse: boolean = false,
): StreamFile {
const streamSource: TStreamSource = (streamFileArg) => {
if (streamFileArg.multiUse) {
// If multi-use is enabled and we have cached content, create a new readable stream from the buffer
@@ -86,7 +103,6 @@ export class StreamFile {
return streamFile;
}
// INSTANCE
relativeFilePath?: string;
private streamSource: TStreamSource;
@@ -115,7 +131,16 @@ export class StreamFile {
* Creates a new readable stream from the source.
*/
public async createReadStream(): Promise<Readable> {
return this.streamSource(this);
const stream = await this.streamSource(this);
// Check if it's a Web ReadableStream and convert to Node.js Readable
if (stream && typeof (stream as any).getReader === 'function') {
// This is a Web ReadableStream, convert it to Node.js Readable
return Readable.fromWeb(stream as any);
}
// It's already a Node.js Readable stream
return stream as Readable;
}
/**
@@ -171,4 +196,4 @@ export class StreamFile {
return null;
}
}
}
}

View File

@@ -2,33 +2,33 @@ import { SmartFile } from './classes.smartfile.js';
import * as plugins from './plugins.js';
import * as fs from './fs.js';
export interface IVirtualDirectoryConstructorOptions {
mode: ''
mode: '';
}
/**
* a virtual directory exposes a fs api
*/
export class VirtualDirectory {
consstructor(options = {}) {
}
consstructor(options = {}) {}
// STATIC
public static async fromFsDirPath(pathArg: string): Promise<VirtualDirectory> {
public static async fromFsDirPath(
pathArg: string,
): Promise<VirtualDirectory> {
const newVirtualDir = new VirtualDirectory();
newVirtualDir.addSmartfiles(await fs.fileTreeToObject(pathArg, '**/*'));
return newVirtualDir;
}
public static async fromVirtualDirTransferableObject(
virtualDirTransferableObjectArg: plugins.smartfileInterfaces.VirtualDirTransferableObject
virtualDirTransferableObjectArg: plugins.smartfileInterfaces.VirtualDirTransferableObject,
): Promise<VirtualDirectory> {
const newVirtualDir = new VirtualDirectory();
for (const fileArg of virtualDirTransferableObjectArg.files) {
newVirtualDir.addSmartfiles([SmartFile.enfoldFromJson(fileArg) as SmartFile]);
newVirtualDir.addSmartfiles([
SmartFile.enfoldFromJson(fileArg) as SmartFile,
]);
}
return newVirtualDir;
}
@@ -52,7 +52,9 @@ export class VirtualDirectory {
public async toVirtualDirTransferableObject(): Promise<plugins.smartfileInterfaces.VirtualDirTransferableObject> {
return {
files: this.smartfileArray.map((smartfileArg) => smartfileArg.foldToJson()),
files: this.smartfileArray.map((smartfileArg) =>
smartfileArg.foldToJson(),
),
};
}
@@ -78,7 +80,10 @@ export class VirtualDirectory {
return newVirtualDir;
}
public async addVirtualDirectory(virtualDir: VirtualDirectory, newRoot: string): Promise<void> {
public async addVirtualDirectory(
virtualDir: VirtualDirectory,
newRoot: string,
): Promise<void> {
for (const file of virtualDir.smartfileArray) {
file.path = plugins.path.join(newRoot, file.path);
}

362
ts/fs.ts
View File

@@ -72,25 +72,47 @@ export const isFile = (pathArg): boolean => {
===============================================================*/
/**
* copies a file from A to B on the local disk
* copies a file or directory from A to B on the local disk
*/
export const copy = async (fromArg: string, toArg: string): Promise<boolean> => {
const done = plugins.smartpromise.defer<boolean>();
plugins.fsExtra.copy(fromArg, toArg, {}, (err) => {
if (err) {
throw new Error(`Could not copy from ${fromArg} to ${toArg}: ${err}`);
}
done.resolve(true);
});
return done.promise;
export const copy = async (
fromArg: string,
toArg: string,
optionsArg?: plugins.fsExtra.CopyOptions & { replaceTargetDir?: boolean },
): Promise<void> => {
if (
optionsArg?.replaceTargetDir &&
isDirectory(fromArg) &&
isDirectory(toArg)
) {
await remove(toArg);
}
return await plugins.fsExtra.copy(
fromArg,
toArg,
optionsArg as plugins.fsExtra.CopyOptions,
);
};
/**
* copies a file SYNCHRONOUSLY from A to B on the local disk
* copies a file or directory SYNCHRONOUSLY from A to B on the local disk
*/
export const copySync = (fromArg: string, toArg: string): boolean => {
plugins.fsExtra.copySync(fromArg, toArg);
return true;
export const copySync = (
fromArg: string,
toArg: string,
optionsArg?: plugins.fsExtra.CopyOptionsSync & { replaceTargetDir?: boolean },
): void => {
if (
optionsArg?.replaceTargetDir &&
isDirectory(fromArg) &&
isDirectory(toArg)
) {
removeSync(toArg);
}
return plugins.fsExtra.copySync(
fromArg,
toArg,
optionsArg as plugins.fsExtra.CopyOptionsSync,
);
};
/**
@@ -132,7 +154,10 @@ export const ensureEmptyDirSync = (dirPathArg: string) => {
* @returns Promise<void>
* @exec ASYNC
*/
export const ensureFile = async (filePathArg, initFileStringArg): Promise<void> => {
export const ensureFile = async (
filePathArg,
initFileStringArg,
): Promise<void> => {
ensureFileSync(filePathArg, initFileStringArg);
};
@@ -143,7 +168,10 @@ export const ensureFile = async (filePathArg, initFileStringArg): Promise<void>
* @returns Promise<void>
* @exec SYNC
*/
export const ensureFileSync = (filePathArg: string, initFileStringArg: string): void => {
export const ensureFileSync = (
filePathArg: string,
initFileStringArg: string,
): void => {
if (fileExistsSync(filePathArg)) {
return null;
} else {
@@ -199,13 +227,15 @@ export const removeManySync = (filePathArrayArg: string[]): void => {
export const toObjectSync = (filePathArg, fileTypeArg?) => {
const fileString = plugins.fsExtra.readFileSync(filePathArg, 'utf8');
let fileType;
fileTypeArg ? (fileType = fileTypeArg) : (fileType = interpreter.filetype(filePathArg));
fileTypeArg
? (fileType = fileTypeArg)
: (fileType = interpreter.filetype(filePathArg));
try {
return interpreter.objectFile(fileString, fileType);
} catch (err) {
err.message = `Failed to read file at ${filePathArg}` + err.message;
throw err;
};
}
};
/**
@@ -213,7 +243,10 @@ export const toObjectSync = (filePathArg, fileTypeArg?) => {
*/
export const toStringSync = (filePath: string): string => {
const encoding = plugins.smartmime.getEncodingForPathSync(filePath);
let fileString: string | Buffer = plugins.fsExtra.readFileSync(filePath, encoding);
let fileString: string | Buffer = plugins.fsExtra.readFileSync(
filePath,
encoding,
);
if (Buffer.isBuffer(fileString)) {
fileString = fileString.toString('binary');
}
@@ -240,7 +273,10 @@ export const toReadStream = (filePath: string): plugins.fs.ReadStream => {
return plugins.fsExtra.createReadStream(filePath);
};
export const fileTreeToHash = async (dirPathArg: string, miniMatchFilter: string) => {
export const fileTreeToHash = async (
dirPathArg: string,
miniMatchFilter: string,
) => {
const fileTreeObject = await fileTreeToObject(dirPathArg, miniMatchFilter);
let combinedString = '';
for (const smartfile of fileTreeObject) {
@@ -255,7 +291,10 @@ export const fileTreeToHash = async (dirPathArg: string, miniMatchFilter: string
* @param dirPathArg the directory to start from
* @param miniMatchFilter a minimatch filter of what files to include
*/
export const fileTreeToObject = async (dirPathArg: string, miniMatchFilter: string) => {
export const fileTreeToObject = async (
dirPathArg: string,
miniMatchFilter: string,
) => {
// handle absolute miniMatchFilter
let dirPath: string;
if (plugins.path.isAbsolute(miniMatchFilter)) {
@@ -282,7 +321,7 @@ export const fileTreeToObject = async (dirPathArg: string, miniMatchFilter: stri
contentBuffer: fileBuffer,
base: dirPath,
path: filePath,
})
}),
);
}
return smartfileArray;
@@ -292,7 +331,10 @@ export const fileTreeToObject = async (dirPathArg: string, miniMatchFilter: stri
* lists Folders in a directory on local disk
* @returns Promise with an array that contains the folder names
*/
export const listFolders = async (pathArg: string, regexFilter?: RegExp): Promise<string[]> => {
export const listFolders = async (
pathArg: string,
regexFilter?: RegExp,
): Promise<string[]> => {
return listFoldersSync(pathArg, regexFilter);
};
@@ -300,9 +342,14 @@ export const listFolders = async (pathArg: string, regexFilter?: RegExp): Promis
* lists Folders SYNCHRONOUSLY in a directory on local disk
* @returns an array with the folder names as strings
*/
export const listFoldersSync = (pathArg: string, regexFilter?: RegExp): string[] => {
export const listFoldersSync = (
pathArg: string,
regexFilter?: RegExp,
): string[] => {
let folderArray = plugins.fsExtra.readdirSync(pathArg).filter((file) => {
return plugins.fsExtra.statSync(plugins.path.join(pathArg, file)).isDirectory();
return plugins.fsExtra
.statSync(plugins.path.join(pathArg, file))
.isDirectory();
});
if (regexFilter) {
folderArray = folderArray.filter((fileItem) => {
@@ -316,7 +363,10 @@ export const listFoldersSync = (pathArg: string, regexFilter?: RegExp): string[]
* lists Files in a directory on local disk
* @returns Promise
*/
export const listFiles = async (pathArg: string, regexFilter?: RegExp): Promise<string[]> => {
export const listFiles = async (
pathArg: string,
regexFilter?: RegExp,
): Promise<string[]> => {
return listFilesSync(pathArg, regexFilter);
};
@@ -324,7 +374,10 @@ export const listFiles = async (pathArg: string, regexFilter?: RegExp): Promise<
* lists Files SYNCHRONOUSLY in a directory on local disk
* @returns an array with the folder names as strings
*/
export const listFilesSync = (pathArg: string, regexFilter?: RegExp): string[] => {
export const listFilesSync = (
pathArg: string,
regexFilter?: RegExp,
): string[] => {
let fileArray = plugins.fsExtra.readdirSync(pathArg).filter((file) => {
return plugins.fsExtra.statSync(plugins.path.join(pathArg, file)).isFile();
});
@@ -340,7 +393,10 @@ export const listFilesSync = (pathArg: string, regexFilter?: RegExp): string[] =
* lists all items (folders AND files) in a directory on local disk
* @returns Promise<string[]>
*/
export const listAllItems = async (pathArg: string, regexFilter?: RegExp): Promise<string[]> => {
export const listAllItems = async (
pathArg: string,
regexFilter?: RegExp,
): Promise<string[]> => {
return listAllItemsSync(pathArg, regexFilter);
};
@@ -349,7 +405,10 @@ export const listAllItems = async (pathArg: string, regexFilter?: RegExp): Promi
* @returns an array with the folder names as strings
* @executes SYNC
*/
export const listAllItemsSync = (pathArg: string, regexFilter?: RegExp): string[] => {
export const listAllItemsSync = (
pathArg: string,
regexFilter?: RegExp,
): string[] => {
let allItmesArray = plugins.fsExtra.readdirSync(pathArg).filter((file) => {
return plugins.fsExtra.statSync(plugins.path.join(pathArg, file)).isFile();
});
@@ -369,7 +428,7 @@ export const listAllItemsSync = (pathArg: string, regexFilter?: RegExp): string[
export const listFileTree = async (
dirPathArg: string,
miniMatchFilter: string,
absolutePathsBool: boolean = false
absolutePathsBool: boolean = false,
): Promise<string[]> => {
// handle absolute miniMatchFilter
let dirPath: string;
@@ -385,7 +444,28 @@ export const listFileTree = async (
dot: true,
};
let fileList = await plugins.glob.glob(miniMatchFilter, options);
// Fix inconsistent **/* glob behavior across systems
// Some glob implementations don't include root-level files when using **/*
// To ensure consistent behavior, we expand **/* patterns to include both root and nested files
let patterns: string[];
if (miniMatchFilter.startsWith('**/')) {
// Extract the part after **/ (e.g., "*.ts" from "**/*.ts")
const rootPattern = miniMatchFilter.substring(3);
// Use both the root pattern and the original pattern to ensure we catch everything
patterns = [rootPattern, miniMatchFilter];
} else {
patterns = [miniMatchFilter];
}
// Collect results from all patterns
const allFiles = new Set<string>();
for (const pattern of patterns) {
const files = await plugins.glob.glob(pattern, options);
files.forEach((file) => allFiles.add(file));
}
let fileList = Array.from(allFiles).sort();
if (absolutePathsBool) {
fileList = fileList.map((filePath) => {
return plugins.path.resolve(plugins.path.join(dirPath, filePath));
@@ -397,94 +477,187 @@ export const listFileTree = async (
/**
* Watches for file stability before resolving the promise.
* @param filePathArg The path of the file to monitor.
* Ensures that the directory/file exists before setting up the watcher.
*
* **New behavior**: If the given path is a directory, this function will:
* 1. Wait for that directory to exist (creating a timeout if needed).
* 2. Watch the directory until at least one file appears.
* 3. Then wait for the first file in the directory to stabilize before resolving.
*
* @param fileOrDirPathArg The path of the file or directory to monitor.
* @param timeoutMs The maximum time to wait for the file to stabilize (in milliseconds). Default is 60 seconds.
* @returns A promise that resolves when the file is stable or rejects on timeout or error.
* @returns A promise that resolves when the target is stable or rejects on timeout/error.
*/
export const waitForFileToBeReady = (
filePathArg: string,
timeoutMs: number = 60000
export const waitForFileToBeReady = async (
fileOrDirPathArg: string,
timeoutMs: number = 60000,
): Promise<void> => {
return new Promise(async (resolve, reject) => {
const startTime = Date.now();
/**
* Ensure that a path (file or directory) exists. If it doesn't yet exist,
* wait until it does (or time out).
* @param pathToCheck The file or directory path to check.
*/
const ensurePathExists = async (pathToCheck: string): Promise<void> => {
while (true) {
try {
await plugins.smartpromise.fromCallback((cb) =>
plugins.fs.access(pathToCheck, plugins.fs.constants.F_OK, cb),
);
return;
} catch (err: any) {
if (err.code !== 'ENOENT') {
throw err; // Propagate unexpected errors
}
if (Date.now() - startTime > timeoutMs) {
throw new Error(`Timeout waiting for path to exist: ${pathToCheck}`);
}
await plugins.smartdelay.delayFor(500);
}
}
};
/**
* Checks if a file (not directory) is stable by comparing sizes
* across successive checks.
* @param filePathArg The path of the file to check.
* @returns A promise that resolves once the file stops changing.
*/
const waitForSingleFileToBeStable = async (
filePathArg: string,
): Promise<void> => {
let lastFileSize = -1;
let fileIsStable = false;
let isFileAvailable = false;
const startTime = Date.now();
const fileDir = plugins.path.dirname(filePathArg);
// We'll create a helper for repeated stats-checking logic
const checkFileStability = async () => {
try {
const stats: any = await plugins.smartpromise.fromCallback((cb) =>
plugins.fs.stat(filePathArg, cb)
const stats = await plugins.smartpromise.fromCallback<plugins.fs.Stats>(
(cb) => plugins.fs.stat(filePathArg, cb),
);
isFileAvailable = true;
if (stats.isDirectory()) {
// If it unexpectedly turns out to be a directory here, throw
throw new Error(
`Expected a file but found a directory: ${filePathArg}`,
);
}
if (stats.size === lastFileSize) {
fileIsStable = true;
} else {
lastFileSize = stats.size;
fileIsStable = false;
}
} catch (err) {
if (err.code === 'ENOENT') {
isFileAvailable = false; // File not available yet
} else {
throw err; // Propagate other errors
} catch (err: any) {
// Ignore only if file not found
if (err.code !== 'ENOENT') {
throw err;
}
}
};
const checkDirectory = async () => {
try {
await plugins.smartpromise.fromCallback((cb) =>
plugins.fs.access(fileDir, plugins.fs.constants.R_OK, cb)
);
return true;
} catch {
return false;
}
};
// Ensure file exists first
await ensurePathExists(filePathArg);
const watcher = plugins.fs.watch(filePathArg, { persistent: true }, async () => {
if (isFileAvailable && !fileIsStable) {
await checkFileStability();
}
});
watcher.on('error', (error) => {
watcher.close();
reject(error);
});
// Set up a watcher on the file itself
const fileWatcher = plugins.fs.watch(
filePathArg,
{ persistent: true },
async () => {
if (!fileIsStable) {
await checkFileStability();
}
},
);
try {
// Poll until stable or timeout
while (!fileIsStable) {
// Check for timeout
if (Date.now() - startTime > timeoutMs) {
watcher.close();
reject(new Error(`Timeout waiting for file to be ready: ${filePathArg}`));
return;
throw new Error(
`Timeout waiting for file to stabilize: ${filePathArg}`,
);
}
// Ensure directory exists
if (!await checkDirectory()) {
await plugins.smartdelay.delayFor(500); // Wait and retry
continue;
}
// Check file stability
await checkFileStability();
if (!fileIsStable) {
await plugins.smartdelay.delayFor(1000); // Polling interval
await plugins.smartdelay.delayFor(1000);
}
}
watcher.close();
resolve();
} catch (err) {
watcher.close();
reject(err);
} finally {
fileWatcher.close();
}
});
};
/**
* Main logic: check if we have a directory or file at fileOrDirPathArg.
* If directory, wait for first file in the directory to appear and stabilize.
* If file, do the old single-file wait logic.
*/
const statsForGivenPath = await (async () => {
try {
await ensurePathExists(fileOrDirPathArg);
return await plugins.smartpromise.fromCallback<plugins.fs.Stats>((cb) =>
plugins.fs.stat(fileOrDirPathArg, cb),
);
} catch (err) {
// If there's an error (including timeout), just rethrow
throw err;
}
})();
if (!statsForGivenPath.isDirectory()) {
// It's a file just do the single-file stability wait
await waitForSingleFileToBeStable(fileOrDirPathArg);
return;
}
// Otherwise, it's a directory. Wait for the first file inside to appear and be stable
const dirPath = fileOrDirPathArg;
// Helper to find the first file in the directory if it exists
const getFirstFileInDirectory = async (): Promise<string | null> => {
const entries = await plugins.smartpromise.fromCallback<string[]>((cb) =>
plugins.fs.readdir(dirPath, cb),
);
// We only want actual files, not subdirectories
for (const entry of entries) {
const entryPath = plugins.path.join(dirPath, entry);
const entryStats =
await plugins.smartpromise.fromCallback<plugins.fs.Stats>((cb) =>
plugins.fs.stat(entryPath, cb),
);
if (entryStats.isFile()) {
return entryPath;
}
}
return null;
};
// Wait for a file to appear in this directory
let firstFilePath = await getFirstFileInDirectory();
if (!firstFilePath) {
// Set up a watcher on the directory to see if a file appears
const directoryWatcher = plugins.fs.watch(dirPath, { persistent: true });
try {
// We'll poll for the existence of a file in that directory
while (!firstFilePath) {
if (Date.now() - startTime > timeoutMs) {
throw new Error(
`Timeout waiting for a file to appear in directory: ${dirPath}`,
);
}
firstFilePath = await getFirstFileInDirectory();
if (!firstFilePath) {
await plugins.smartdelay.delayFor(1000);
}
}
} finally {
directoryWatcher.close();
}
}
// Now that we have a file path, wait for that file to stabilize
await waitForSingleFileToBeStable(firstFilePath);
};
/**
@@ -498,7 +671,7 @@ export let toFs = async (
filePathArg: string,
optionsArg: {
respectRelative?: boolean;
} = {}
} = {},
) => {
const done = plugins.smartpromise.defer();
@@ -528,10 +701,15 @@ export let toFs = async (
throw new Error('fileContent is neither string nor Smartfile');
}
await ensureDir(plugins.path.parse(filePath).dir);
plugins.fsExtra.writeFile(filePath, fileContent, { encoding: fileEncoding }, done.resolve);
plugins.fsExtra.writeFile(
filePath,
fileContent,
{ encoding: fileEncoding },
done.resolve,
);
return await done.promise;
};
export const stat = async (filePathArg: string) => {
return plugins.fsPromises.stat(filePathArg);
}
};

View File

@@ -13,17 +13,17 @@ export const createWriteStream = (pathArg: string) => {
export const processFile = async (
filePath: string,
asyncFunc: (fileStream: plugins.stream.Readable) => Promise<void>
asyncFunc: (fileStream: plugins.stream.Readable) => Promise<void>,
): Promise<void> => {
return new Promise((resolve, reject) => {
const fileStream = createReadStream(filePath);
asyncFunc(fileStream).then(resolve).catch(reject);
});
}
};
export const processDirectory = async (
directoryPath: string,
asyncFunc: (fileStream: plugins.stream.Readable) => Promise<void>
asyncFunc: (fileStream: plugins.stream.Readable) => Promise<void>,
): Promise<void> => {
const files = plugins.fs.readdirSync(directoryPath, { withFileTypes: true });
@@ -41,12 +41,15 @@ export const processDirectory = async (
/**
* Checks if a file is ready to be streamed (exists and is not empty).
*/
export const isFileReadyForStreaming = async (filePathArg: string): Promise<boolean> => {
export const isFileReadyForStreaming = async (
filePathArg: string,
): Promise<boolean> => {
try {
const stats = await plugins.fs.promises.stat(filePathArg);
return stats.size > 0;
} catch (error) {
if (error.code === 'ENOENT') { // File does not exist
if (error.code === 'ENOENT') {
// File does not exist
return false;
}
throw error; // Rethrow other unexpected errors
@@ -56,7 +59,9 @@ export const isFileReadyForStreaming = async (filePathArg: string): Promise<bool
/**
* Waits for a file to be ready for streaming (exists and is not empty).
*/
export const waitForFileToBeReadyForStreaming = (filePathArg: string): Promise<void> => {
export const waitForFileToBeReadyForStreaming = (
filePathArg: string,
): Promise<void> => {
return new Promise((resolve, reject) => {
// Normalize and resolve the file path
const filePath = plugins.path.resolve(filePathArg);
@@ -80,11 +85,15 @@ export const waitForFileToBeReadyForStreaming = (filePathArg: string): Promise<v
};
// Set up file watcher
const watcher = plugins.fs.watch(filePath, { persistent: false }, (eventType) => {
if (eventType === 'change' || eventType === 'rename') {
checkFile(resolve, reject);
}
});
const watcher = plugins.fs.watch(
filePath,
{ persistent: false },
(eventType) => {
if (eventType === 'change' || eventType === 'rename') {
checkFile(resolve, reject);
}
},
);
// Check file immediately in case it's already ready
checkFile(resolve, reject);
@@ -105,7 +114,11 @@ export class SmartReadStream extends plugins.stream.Readable {
private endDelay: number;
private reading: boolean = false;
constructor(filePath: string, endDelay = 60000, opts?: plugins.stream.ReadableOptions) {
constructor(
filePath: string,
endDelay = 60000,
opts?: plugins.stream.ReadableOptions,
) {
super(opts);
this.filePath = filePath;
this.endDelay = endDelay;
@@ -165,26 +178,33 @@ export class SmartReadStream extends plugins.stream.Readable {
this.emit('error', err);
return;
}
plugins.fs.read(fd, buffer, 0, chunkSize, this.lastReadSize, (err, bytesRead, buffer) => {
if (err) {
this.emit('error', err);
return;
}
if (bytesRead > 0) {
this.lastReadSize += bytesRead;
this.push(buffer.slice(0, bytesRead)); // Push the data onto the stream
} else {
this.reading = false; // No more data to read for now
this.resetEndTimeout();
}
plugins.fs.close(fd, (err) => {
plugins.fs.read(
fd,
buffer,
0,
chunkSize,
this.lastReadSize,
(err, bytesRead, buffer) => {
if (err) {
this.emit('error', err);
return;
}
});
});
if (bytesRead > 0) {
this.lastReadSize += bytesRead;
this.push(buffer.slice(0, bytesRead)); // Push the data onto the stream
} else {
this.reading = false; // No more data to read for now
this.resetEndTimeout();
}
plugins.fs.close(fd, (err) => {
if (err) {
this.emit('error', err);
}
});
},
);
});
}
@@ -192,4 +212,4 @@ export class SmartReadStream extends plugins.stream.Readable {
this.cleanup();
callback(error);
}
}
}

View File

@@ -27,7 +27,7 @@ export interface IToFsOptions {
export let toFs = async (
fileContentArg: string | Buffer | SmartFile | StreamFile,
filePathArg: string,
optionsArg: IToFsOptions = {}
optionsArg: IToFsOptions = {},
) => {
const done = plugins.smartpromise.defer();
@@ -57,7 +57,12 @@ export let toFs = async (
throw new Error('fileContent is neither string nor Smartfile');
}
await smartfileFs.ensureDir(plugins.path.parse(filePath).dir);
plugins.fsExtra.writeFile(filePath, fileContent, { encoding: fileEncoding }, done.resolve);
plugins.fsExtra.writeFile(
filePath,
fileContent,
{ encoding: fileEncoding },
done.resolve,
);
return await done.promise;
};
@@ -84,7 +89,10 @@ export const toFsSync = (fileArg: string, filePathArg: string) => {
plugins.fsExtra.writeFileSync(filePath, fileString, { encoding: 'utf8' });
};
export let smartfileArrayToFs = async (smartfileArrayArg: SmartFile[], dirArg: string) => {
export let smartfileArrayToFs = async (
smartfileArrayArg: SmartFile[],
dirArg: string,
) => {
await smartfileFs.ensureDir(dirArg);
for (const smartfile of smartfileArrayArg) {
await toFs(smartfile, dirArg, {

View File

@@ -6,9 +6,9 @@
"module": "NodeNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"verbatimModuleSyntax": true
"verbatimModuleSyntax": true,
"baseUrl": ".",
"paths": {}
},
"exclude": [
"dist_*/**/*.d.ts"
]
"exclude": ["dist_*/**/*.d.ts"]
}