Compare commits
12 Commits
Author | SHA1 | Date | |
---|---|---|---|
c74ded8d5d | |||
62e61168a0 | |||
5e0edecf18 | |||
70cefc00fa | |||
6f14c73b5f | |||
1e6f636608 | |||
eff77f8976 | |||
b5f109d320 | |||
3a53938e8e | |||
db90714a81 | |||
b81ab9d9b2 | |||
5ec9124d29 |
@@ -6,8 +6,8 @@ on:
|
|||||||
- '**'
|
- '**'
|
||||||
|
|
||||||
env:
|
env:
|
||||||
IMAGE: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
IMAGE: code.foss.global/host.today/ht-docker-node:npmci
|
||||||
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@gitea.lossless.digital/${{gitea.repository}}.git
|
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@/${{gitea.repository}}.git
|
||||||
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
||||||
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
||||||
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
|
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
|
||||||
@@ -26,7 +26,7 @@ jobs:
|
|||||||
- name: Install pnpm and npmci
|
- name: Install pnpm and npmci
|
||||||
run: |
|
run: |
|
||||||
pnpm install -g pnpm
|
pnpm install -g pnpm
|
||||||
pnpm install -g @shipzone/npmci
|
pnpm install -g @ship.zone/npmci
|
||||||
|
|
||||||
- name: Run npm prepare
|
- name: Run npm prepare
|
||||||
run: npmci npm prepare
|
run: npmci npm prepare
|
||||||
|
@@ -6,8 +6,8 @@ on:
|
|||||||
- '*'
|
- '*'
|
||||||
|
|
||||||
env:
|
env:
|
||||||
IMAGE: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
IMAGE: code.foss.global/host.today/ht-docker-node:npmci
|
||||||
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@gitea.lossless.digital/${{gitea.repository}}.git
|
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@/${{gitea.repository}}.git
|
||||||
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
||||||
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
||||||
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
|
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
|
||||||
@@ -26,7 +26,7 @@ jobs:
|
|||||||
- name: Prepare
|
- name: Prepare
|
||||||
run: |
|
run: |
|
||||||
pnpm install -g pnpm
|
pnpm install -g pnpm
|
||||||
pnpm install -g @shipzone/npmci
|
pnpm install -g @ship.zone/npmci
|
||||||
npmci npm prepare
|
npmci npm prepare
|
||||||
|
|
||||||
- name: Audit production dependencies
|
- name: Audit production dependencies
|
||||||
@@ -54,7 +54,7 @@ jobs:
|
|||||||
- name: Prepare
|
- name: Prepare
|
||||||
run: |
|
run: |
|
||||||
pnpm install -g pnpm
|
pnpm install -g pnpm
|
||||||
pnpm install -g @shipzone/npmci
|
pnpm install -g @ship.zone/npmci
|
||||||
npmci npm prepare
|
npmci npm prepare
|
||||||
|
|
||||||
- name: Test stable
|
- name: Test stable
|
||||||
@@ -82,7 +82,7 @@ jobs:
|
|||||||
- name: Prepare
|
- name: Prepare
|
||||||
run: |
|
run: |
|
||||||
pnpm install -g pnpm
|
pnpm install -g pnpm
|
||||||
pnpm install -g @shipzone/npmci
|
pnpm install -g @ship.zone/npmci
|
||||||
npmci npm prepare
|
npmci npm prepare
|
||||||
|
|
||||||
- name: Release
|
- name: Release
|
||||||
@@ -104,7 +104,7 @@ jobs:
|
|||||||
- name: Prepare
|
- name: Prepare
|
||||||
run: |
|
run: |
|
||||||
pnpm install -g pnpm
|
pnpm install -g pnpm
|
||||||
pnpm install -g @shipzone/npmci
|
pnpm install -g @ship.zone/npmci
|
||||||
npmci npm prepare
|
npmci npm prepare
|
||||||
|
|
||||||
- name: Code quality
|
- name: Code quality
|
||||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@@ -3,7 +3,6 @@
|
|||||||
# artifacts
|
# artifacts
|
||||||
coverage/
|
coverage/
|
||||||
public/
|
public/
|
||||||
pages/
|
|
||||||
|
|
||||||
# installs
|
# installs
|
||||||
node_modules/
|
node_modules/
|
||||||
@@ -17,4 +16,4 @@ node_modules/
|
|||||||
dist/
|
dist/
|
||||||
dist_*/
|
dist_*/
|
||||||
|
|
||||||
# custom
|
#------# custom
|
BIN
.serena/cache/typescript/document_symbols_cache_v23-06-25.pkl
vendored
Normal file
BIN
.serena/cache/typescript/document_symbols_cache_v23-06-25.pkl
vendored
Normal file
Binary file not shown.
32
.serena/memories/coding_standards.md
Normal file
32
.serena/memories/coding_standards.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# Coding Standards for npmextra
|
||||||
|
|
||||||
|
## Naming Conventions
|
||||||
|
- **Interfaces**: Prefix with `I` (e.g., `IAppDataOptions`, `ITestOptions`)
|
||||||
|
- **Types**: Prefix with `T` (e.g., `TKeyValueStore`)
|
||||||
|
- **Filenames**: Always lowercase (e.g., `npmextra.classes.appdata.ts`)
|
||||||
|
- **Module structure**: `npmextra.<type>.<name>.ts` pattern
|
||||||
|
|
||||||
|
## Import/Export Patterns
|
||||||
|
- Use ES module syntax (`import`/`export`)
|
||||||
|
- Import all dependencies through `npmextra.plugins.ts`
|
||||||
|
- Reference with full path: `plugins.moduleName.className()`
|
||||||
|
- Export all public APIs through `index.ts`
|
||||||
|
|
||||||
|
## TypeScript Patterns
|
||||||
|
- Use generic types for flexibility (`<T = any>`)
|
||||||
|
- Leverage TypeScript utility types from `@tsclass/tsclass`
|
||||||
|
- Use async/await for asynchronous operations
|
||||||
|
- Use Promises with smartpromise utilities
|
||||||
|
|
||||||
|
## Testing Standards
|
||||||
|
- Import expect from `@git.zone/tstest/tapbundle`
|
||||||
|
- Test files end with `export default tap.start()`
|
||||||
|
- Use descriptive test names with `tap.test()`
|
||||||
|
- Test file naming: `test.*.ts` pattern
|
||||||
|
|
||||||
|
## Code Quality
|
||||||
|
- Make focused, goal-oriented changes
|
||||||
|
- Preserve necessary complexity
|
||||||
|
- Remove redundancy carefully
|
||||||
|
- Keep async patterns where they add value
|
||||||
|
- No comments unless explicitly requested
|
30
.serena/memories/project_overview.md
Normal file
30
.serena/memories/project_overview.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# npmextra Project Overview
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
npmextra is a utility library that enhances npm with additional configuration and tool management capabilities. It provides a key-value store for project setups and centralized configuration management through npmextra.json files.
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
- TypeScript (ES modules)
|
||||||
|
- Node.js
|
||||||
|
- Dependencies:
|
||||||
|
- @push.rocks/qenv - Environment variable management
|
||||||
|
- @push.rocks/smartfile - File system operations
|
||||||
|
- @push.rocks/smartjson - JSON handling
|
||||||
|
- @push.rocks/smartlog - Logging
|
||||||
|
- @push.rocks/smartpath - Path utilities
|
||||||
|
- @push.rocks/smartpromise - Promise utilities
|
||||||
|
- @push.rocks/smartrx - Reactive programming
|
||||||
|
- @push.rocks/taskbuffer - Task management
|
||||||
|
- @tsclass/tsclass - TypeScript utilities
|
||||||
|
|
||||||
|
## Main Components
|
||||||
|
1. **Npmextra** - Main class for managing npmextra.json configurations
|
||||||
|
2. **KeyValueStore** - Persistent key-value storage system
|
||||||
|
3. **AppData** - Advanced data management with environment variable mapping
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
- `ts/` - TypeScript source files
|
||||||
|
- `test/` - Test files using @git.zone/tstest
|
||||||
|
- `dist_ts/` - Compiled JavaScript output
|
||||||
|
- `npmextra.json` - Project configuration
|
||||||
|
- `package.json` - Node.js package configuration
|
31
.serena/memories/suggested_commands.md
Normal file
31
.serena/memories/suggested_commands.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# Suggested Commands for npmextra Development
|
||||||
|
|
||||||
|
## Build Commands
|
||||||
|
- `pnpm run build` - Build the TypeScript project (uses tsbuild)
|
||||||
|
- `pnpm test` - Run tests using tstest
|
||||||
|
|
||||||
|
## Development Commands
|
||||||
|
- `pnpm install` - Install dependencies
|
||||||
|
- `pnpm install --save-dev <package>` - Add development dependencies
|
||||||
|
- `tsx <script>` - Run TypeScript files directly (tsx is globally available)
|
||||||
|
|
||||||
|
## Git Commands
|
||||||
|
- `git status` - Check current changes
|
||||||
|
- `git add .` - Stage changes
|
||||||
|
- `git commit -m "message"` - Commit changes (only when explicitly requested)
|
||||||
|
- `git mv` - Move/rename files to preserve history
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
- `pnpm test` - Run all tests
|
||||||
|
- `tstest test/test.some.ts --verbose` - Run specific test file
|
||||||
|
- Tests use @git.zone/tstest framework with tap-based structure
|
||||||
|
|
||||||
|
## Type Checking
|
||||||
|
- `pnpm run build` - Type check and build the project
|
||||||
|
- `tsbuild check test/**/* --skiplibcheck` - Type check test files only
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
- Source code in `ts/` directory
|
||||||
|
- Tests in `test/` directory
|
||||||
|
- Built output in `dist_ts/` directory
|
||||||
|
- Temporary/debug files in `.nogit/` directory
|
68
.serena/project.yml
Normal file
68
.serena/project.yml
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
# language of the project (csharp, python, rust, java, typescript, go, cpp, or ruby)
|
||||||
|
# * For C, use cpp
|
||||||
|
# * For JavaScript, use typescript
|
||||||
|
# Special requirements:
|
||||||
|
# * csharp: Requires the presence of a .sln file in the project folder.
|
||||||
|
language: typescript
|
||||||
|
|
||||||
|
# whether to use the project's gitignore file to ignore files
|
||||||
|
# Added on 2025-04-07
|
||||||
|
ignore_all_files_in_gitignore: true
|
||||||
|
# list of additional paths to ignore
|
||||||
|
# same syntax as gitignore, so you can use * and **
|
||||||
|
# Was previously called `ignored_dirs`, please update your config if you are using that.
|
||||||
|
# Added (renamed) on 2025-04-07
|
||||||
|
ignored_paths: []
|
||||||
|
|
||||||
|
# whether the project is in read-only mode
|
||||||
|
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
|
||||||
|
# Added on 2025-04-18
|
||||||
|
read_only: false
|
||||||
|
|
||||||
|
|
||||||
|
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
|
||||||
|
# Below is the complete list of tools for convenience.
|
||||||
|
# To make sure you have the latest list of tools, and to view their descriptions,
|
||||||
|
# execute `uv run scripts/print_tool_overview.py`.
|
||||||
|
#
|
||||||
|
# * `activate_project`: Activates a project by name.
|
||||||
|
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
|
||||||
|
# * `create_text_file`: Creates/overwrites a file in the project directory.
|
||||||
|
# * `delete_lines`: Deletes a range of lines within a file.
|
||||||
|
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
|
||||||
|
# * `execute_shell_command`: Executes a shell command.
|
||||||
|
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
|
||||||
|
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
|
||||||
|
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
|
||||||
|
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
|
||||||
|
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
|
||||||
|
# * `initial_instructions`: Gets the initial instructions for the current project.
|
||||||
|
# Should only be used in settings where the system prompt cannot be set,
|
||||||
|
# e.g. in clients you have no control over, like Claude Desktop.
|
||||||
|
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
|
||||||
|
# * `insert_at_line`: Inserts content at a given line in a file.
|
||||||
|
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
|
||||||
|
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
|
||||||
|
# * `list_memories`: Lists memories in Serena's project-specific memory store.
|
||||||
|
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
|
||||||
|
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
|
||||||
|
# * `read_file`: Reads a file within the project directory.
|
||||||
|
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
|
||||||
|
# * `remove_project`: Removes a project from the Serena configuration.
|
||||||
|
# * `replace_lines`: Replaces a range of lines within a file with new content.
|
||||||
|
# * `replace_symbol_body`: Replaces the full definition of a symbol.
|
||||||
|
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
|
||||||
|
# * `search_for_pattern`: Performs a search for a pattern in the project.
|
||||||
|
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
|
||||||
|
# * `switch_modes`: Activates modes by providing a list of their names
|
||||||
|
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
|
||||||
|
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
|
||||||
|
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
|
||||||
|
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
|
||||||
|
excluded_tools: []
|
||||||
|
|
||||||
|
# initial prompt for the project. It will always be given to the LLM upon activating the project
|
||||||
|
# (contrary to the memories, which are loaded on demand).
|
||||||
|
initial_prompt: ""
|
||||||
|
|
||||||
|
project_name: "npmextra"
|
76
changelog.md
Normal file
76
changelog.md
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-08-15 - 5.1.3 - fix(appdata)
|
||||||
|
Fix iteration over overwriteObject in AppData and update configuration for dependency and path handling
|
||||||
|
|
||||||
|
- Replaced incorrect looping constructs in the AppData class to properly iterate over overwriteObject keys
|
||||||
|
- Improved environment variable mapping and file path resolution in multiple TS modules
|
||||||
|
- Updated dependency versions and adjusted git workflow configurations
|
||||||
|
- Enhanced project configuration including TS config and build script adjustments
|
||||||
|
|
||||||
|
## 2024-11-06 - 5.1.2 - fix(appdata)
|
||||||
|
|
||||||
|
Fix iteration over overwriteObject in AppData class
|
||||||
|
|
||||||
|
- Corrected the for loop from in to of inside the AppData class for iterating over overwriteObject keys.
|
||||||
|
|
||||||
|
## 2024-11-05 - 5.1.1 - fix(AppData)
|
||||||
|
|
||||||
|
Fix issue with overwrite object handling in AppData class
|
||||||
|
|
||||||
|
- Corrected logic to handle cases when overwriteObject is undefined.
|
||||||
|
|
||||||
|
## 2024-11-05 - 5.1.0 - feat(appdata)
|
||||||
|
|
||||||
|
Add support for overwriting keys using the overwriteObject option in AppData
|
||||||
|
|
||||||
|
- Introduced the overwriteObject option in IAppDataOptions to allow overwriting specific keys in the AppData class.
|
||||||
|
|
||||||
|
## 2024-06-19 - 5.0.17 - 5.0.23 - Core Updates
|
||||||
|
|
||||||
|
Routine maintenance and updates to the core components.
|
||||||
|
|
||||||
|
- Multiple core updates and fixes improving stability
|
||||||
|
|
||||||
|
## 2024-06-12 - 5.0.13 - 5.0.16 - Core Updates
|
||||||
|
|
||||||
|
Maintenance focus on core systems with enhancements and problem resolutions.
|
||||||
|
|
||||||
|
- Enhancements and updates in the core functionality
|
||||||
|
|
||||||
|
## 2024-05-29 - 5.0.13 - Documentation Update
|
||||||
|
|
||||||
|
Descriptive improvements aligned with current features.
|
||||||
|
|
||||||
|
- Updated core description for better clarity in documentation
|
||||||
|
|
||||||
|
## 2024-04-01 - 5.0.10 - Configuration Update
|
||||||
|
|
||||||
|
Improved configuration management for build processes.
|
||||||
|
|
||||||
|
- Updated `npmextra.json` to reflect changes in git repository management
|
||||||
|
|
||||||
|
## 2024-02-12 - 5.0.0 - 5.0.9 - Major Core Enhancements
|
||||||
|
|
||||||
|
A series of critical updates with resolved issues in the core components.
|
||||||
|
|
||||||
|
- Introduction of new core features
|
||||||
|
- Several core system updates
|
||||||
|
|
||||||
|
## 2024-02-12 - 4.0.16 - Major Version Transition
|
||||||
|
|
||||||
|
Migration to the new major version with impactful changes.
|
||||||
|
|
||||||
|
- BREAKING CHANGE: Significant updates requiring attention for smooth transition
|
||||||
|
|
||||||
|
## 2023-08-24 - 3.0.9 - 4.0.16 - Organization Updates
|
||||||
|
|
||||||
|
Formatted updates with attention to organizational standards and practice.
|
||||||
|
|
||||||
|
- SWITCH to a new organizational scheme
|
||||||
|
|
||||||
|
## 2023-07-11 - 3.0.9 - Organizational Enhancement
|
||||||
|
|
||||||
|
Shifts aligning with contemporary structuring and logistics.
|
||||||
|
|
||||||
|
- Strategic realignment with organizational principles
|
35
package.json
35
package.json
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@push.rocks/npmextra",
|
"name": "@push.rocks/npmextra",
|
||||||
"version": "5.0.21",
|
"version": "5.1.3",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "A utility to enhance npm with additional configuration, tool management capabilities, and a key-value store for project setups.",
|
"description": "A utility to enhance npm with additional configuration, tool management capabilities, and a key-value store for project setups.",
|
||||||
"main": "dist_ts/index.js",
|
"main": "dist_ts/index.js",
|
||||||
"typings": "dist_ts/index.d.ts",
|
"typings": "dist_ts/index.d.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "(tstest test/ --web)",
|
"test": "(tstest test/ --verbose --logfile --timeout 60)",
|
||||||
"build": "(tsbuild --web --allowimplicitany)",
|
"build": "(tsbuild --web --allowimplicitany)",
|
||||||
"buildDocs": "tsdoc"
|
"buildDocs": "tsdoc"
|
||||||
},
|
},
|
||||||
@@ -17,25 +17,24 @@
|
|||||||
"author": "Lossless GmbH",
|
"author": "Lossless GmbH",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://gitlab.com/pushrocks/npmextra/issues"
|
"url": "https://code.foss.global/push.rocks/npmextra/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://code.foss.global/push.rocks/npmextra",
|
"homepage": "https://code.foss.global/push.rocks/npmextra#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@push.rocks/qenv": "^6.0.5",
|
"@push.rocks/qenv": "^6.1.2",
|
||||||
"@push.rocks/smartfile": "^11.0.20",
|
"@push.rocks/smartfile": "^11.2.5",
|
||||||
"@push.rocks/smartjson": "^5.0.20",
|
"@push.rocks/smartjson": "^5.0.20",
|
||||||
"@push.rocks/smartlog": "^3.0.7",
|
"@push.rocks/smartlog": "^3.1.8",
|
||||||
"@push.rocks/smartpath": "^5.0.18",
|
"@push.rocks/smartpath": "^6.0.0",
|
||||||
"@push.rocks/smartpromise": "^4.0.2",
|
"@push.rocks/smartpromise": "^4.2.3",
|
||||||
"@push.rocks/smartrx": "^3.0.7",
|
"@push.rocks/smartrx": "^3.0.10",
|
||||||
"@push.rocks/taskbuffer": "^3.1.7",
|
"@push.rocks/taskbuffer": "^3.1.7",
|
||||||
"@tsclass/tsclass": "^4.0.59"
|
"@tsclass/tsclass": "^9.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@git.zone/tsbuild": "^2.1.80",
|
"@git.zone/tsbuild": "^2.6.4",
|
||||||
"@git.zone/tsrun": "^1.2.44",
|
"@git.zone/tsrun": "^1.3.3",
|
||||||
"@git.zone/tstest": "^1.0.90",
|
"@git.zone/tstest": "^2.3.2",
|
||||||
"@push.rocks/tapbundle": "^5.0.23",
|
|
||||||
"@types/node": "^20.14.5"
|
"@types/node": "^20.14.5"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
@@ -70,5 +69,9 @@
|
|||||||
"smart file handling",
|
"smart file handling",
|
||||||
"workflow improvement",
|
"workflow improvement",
|
||||||
"persistent storage"
|
"persistent storage"
|
||||||
]
|
],
|
||||||
|
"packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977",
|
||||||
|
"pnpm": {
|
||||||
|
"overrides": {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
7655
pnpm-lock.yaml
generated
7655
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
4
pnpm-workspace.yaml
Normal file
4
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
onlyBuiltDependencies:
|
||||||
|
- esbuild
|
||||||
|
- mongodb-memory-server
|
||||||
|
- puppeteer
|
602
readme.md
602
readme.md
@@ -1,295 +1,463 @@
|
|||||||
# @push.rocks/npmextra
|
# @push.rocks/npmextra 🚀
|
||||||
A utility to enhance npm with additional configuration, tool management capabilities, and a key-value store for project setups.
|
|
||||||
|
|
||||||
## Install
|
**Supercharge your npm projects with powerful configuration management, tool orchestration, and persistent key-value storage.**
|
||||||
To install `@push.rocks/npmextra`, use the following npm command:
|
|
||||||
|
## Install 📦
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Using npm
|
||||||
npm install @push.rocks/npmextra --save
|
npm install @push.rocks/npmextra --save
|
||||||
|
|
||||||
|
# Using pnpm (recommended)
|
||||||
|
pnpm add @push.rocks/npmextra
|
||||||
```
|
```
|
||||||
|
|
||||||
This package is available on [npm](https://www.npmjs.com/package/@push.rocks/npmextra) and can be installed into your project as a dependency to enhance npm with additional configuration and tool management capabilities.
|
## Overview 🎯
|
||||||
|
|
||||||
## Usage
|
`@push.rocks/npmextra` is your Swiss Army knife for npm project configuration. It eliminates configuration sprawl by centralizing tool settings, providing intelligent key-value storage, and offering powerful environment variable mapping with automatic type conversions.
|
||||||
`@push.rocks/npmextra` is designed to supplement npm functionalities with enhanced configuration and tool management. It facilitates the management of project configurations and tool setups in a consolidated manner, enabling a smoother workflow and maintenance process. Below are detailed use cases and examples implemented with ESM syntax and TypeScript.
|
|
||||||
|
|
||||||
### Initial Setup and Configuration
|
### Why npmextra?
|
||||||
To start using `npmextra` in your project, first include it with an import statement:
|
|
||||||
|
- **🎛️ Centralized Configuration**: Manage all your tool configs in one `npmextra.json` file
|
||||||
|
- **💾 Persistent Storage**: Smart key-value store with multiple storage strategies
|
||||||
|
- **🔐 Environment Mapping**: Sophisticated env var handling with automatic type conversion
|
||||||
|
- **🏗️ TypeScript First**: Full type safety and IntelliSense support
|
||||||
|
- **⚡ Zero Config**: Works out of the box with sensible defaults
|
||||||
|
- **🔄 Reactive**: Built-in change detection and observables
|
||||||
|
|
||||||
|
## Core Concepts 🏗️
|
||||||
|
|
||||||
|
### 1. Npmextra Configuration Management
|
||||||
|
|
||||||
|
Stop scattering configuration across dozens of files. Centralize everything in `npmextra.json`:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Npmextra } from '@push.rocks/npmextra';
|
import { Npmextra } from '@push.rocks/npmextra';
|
||||||
|
|
||||||
|
// Initialize with current directory
|
||||||
|
const npmextra = new Npmextra();
|
||||||
|
|
||||||
|
// Or specify a custom path
|
||||||
|
const npmextra = new Npmextra('/path/to/project');
|
||||||
|
|
||||||
|
// Get merged configuration for any tool
|
||||||
|
const eslintConfig = npmextra.dataFor<EslintConfig>('eslint', {
|
||||||
|
// Default values if not in npmextra.json
|
||||||
|
extends: 'standard',
|
||||||
|
rules: {}
|
||||||
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
Instantiate the `Npmextra` class optionally with a custom path to your project's working directory. If no path is provided, the current working directory (`process.cwd()`) is used.
|
**npmextra.json example:**
|
||||||
|
|
||||||
```typescript
|
|
||||||
const npmExtraInstance = new Npmextra('/path/to/your/project');
|
|
||||||
```
|
|
||||||
|
|
||||||
### Managing Tool Configurations with `npmextra.json`
|
|
||||||
`@push.rocks/npmextra` excels in unifying tool configurations through a single `npmextra.json` file. Instead of scattering configurations across multiple files, `npmextra` enables you to define tool-specific settings within this centralized configuration file, which can then be accessed programmatically.
|
|
||||||
|
|
||||||
#### Creating and Utilizing `npmextra.json`
|
|
||||||
|
|
||||||
Create a `npmextra.json` in your project root with the following structure:
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"toolname": {
|
"eslint": {
|
||||||
"setting1": "value1",
|
"extends": "@company/eslint-config",
|
||||||
"setting2": "value2"
|
"rules": {
|
||||||
|
"no-console": "warn"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"prettier": {
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
For example, to configure a hypothetical tool named `toolname`, define its settings as shown above.
|
### 2. KeyValueStore - Persistent Data Storage
|
||||||
|
|
||||||
#### Accessing Configuration in Your Project
|
A flexible key-value store that persists data between script executions:
|
||||||
|
|
||||||
With the configuration defined, you can easily access these settings in your TypeScript code as follows:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Import the npmextra module
|
|
||||||
import { Npmextra } from '@push.rocks/npmextra';
|
|
||||||
|
|
||||||
// Create an instance pointing at the current directory
|
|
||||||
const npmExtraInstance = new Npmextra();
|
|
||||||
|
|
||||||
// Retrieve the configuration for 'toolname', merging defaults with any found in npmextra.json
|
|
||||||
const toolConfig = npmExtraInstance.dataFor<{ setting1: string, setting2: string }>('toolname', {
|
|
||||||
setting1: 'defaultValue1',
|
|
||||||
setting2: 'defaultValue2'
|
|
||||||
});
|
|
||||||
|
|
||||||
// toolConfig now contains the merged settings from npmextra.json and provided defaults
|
|
||||||
console.log(toolConfig);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Key-Value Store Management
|
|
||||||
`@push.rocks/npmextra` also includes a Key-Value Store (KeyValueStore) functionality enabling persistent storage of key-value pairs between script executions.
|
|
||||||
|
|
||||||
#### Setting Up KeyValueStore
|
|
||||||
|
|
||||||
To utilize the KeyValueStore, create an instance specifying its scope (e.g., 'userHomeDir') and a unique identity for your application or tool:
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { KeyValueStore } from '@push.rocks/npmextra';
|
import { KeyValueStore } from '@push.rocks/npmextra';
|
||||||
|
|
||||||
const kvStore = new KeyValueStore<'userHomeDir'>({
|
interface UserSettings {
|
||||||
typeArg: 'userHomeDir',
|
username: string;
|
||||||
identityArg: 'myUniqueAppName'
|
apiKey: string;
|
||||||
|
preferences: {
|
||||||
|
theme: 'light' | 'dark';
|
||||||
|
notifications: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Different storage strategies
|
||||||
|
const kvStore = new KeyValueStore<UserSettings>({
|
||||||
|
typeArg: 'userHomeDir', // Store in user's home directory
|
||||||
|
identityArg: 'myApp',
|
||||||
|
mandatoryKeys: ['username', 'apiKey']
|
||||||
});
|
});
|
||||||
```
|
|
||||||
|
|
||||||
You can then use the `writeKey`, `readKey`, `writeAll`, and `readAll` methods to manage your store respectively.
|
// Write operations
|
||||||
|
await kvStore.writeKey('username', 'john_doe');
|
||||||
|
await kvStore.writeKey('preferences', {
|
||||||
|
theme: 'dark',
|
||||||
|
notifications: true
|
||||||
|
});
|
||||||
|
|
||||||
#### Example: Storing and Retrieving Data
|
// Read operations
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Write a single key-value pair
|
|
||||||
await kvStore.writeKey('username', 'johnDoe');
|
|
||||||
|
|
||||||
// Read a single key
|
|
||||||
const username = await kvStore.readKey('username');
|
const username = await kvStore.readKey('username');
|
||||||
console.log(username); // Outputs: johnDoe
|
|
||||||
|
|
||||||
// Write multiple key-value pairs
|
|
||||||
await kvStore.writeAll({
|
|
||||||
email: 'john@example.com',
|
|
||||||
isAdmin: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Read all key-value pairs
|
|
||||||
const allData = await kvStore.readAll();
|
const allData = await kvStore.readAll();
|
||||||
console.log(allData); // Outputs the entire store's contents
|
|
||||||
```
|
|
||||||
|
|
||||||
### Advanced Key-Value Store Management
|
// Check for missing mandatory keys
|
||||||
|
|
||||||
In addition to basic read/write operations, `npmextra`’s `KeyValueStore` supports advanced scenarios like mandatory keys and custom file paths.
|
|
||||||
|
|
||||||
#### Example: Mandatory Keys and Custom Paths
|
|
||||||
|
|
||||||
Consider a scenario where your application requires specific keys to be present in the KeyValueStore. You can define mandatory keys and use a custom path for your store like this:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { KeyValueStore } from '@push.rocks/npmextra';
|
|
||||||
|
|
||||||
interface CustomData {
|
|
||||||
key1: string;
|
|
||||||
key2: number;
|
|
||||||
key3?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const kvStore = new KeyValueStore<CustomData>({
|
|
||||||
typeArg: 'custom',
|
|
||||||
identityArg: 'customApp',
|
|
||||||
customPath: '/custom/path/to/store.json',
|
|
||||||
mandatoryKeys: ['key1', 'key2']
|
|
||||||
});
|
|
||||||
|
|
||||||
// Ensure all mandatory keys are present
|
|
||||||
const missingKeys = await kvStore.getMissingMandatoryKeys();
|
const missingKeys = await kvStore.getMissingMandatoryKeys();
|
||||||
if (missingKeys.length) {
|
if (missingKeys.length > 0) {
|
||||||
console.log(`Missing mandatory keys: ${missingKeys.join(', ')}`);
|
console.log('Missing required configuration:', missingKeys);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the KeyValueStore
|
// Wait for keys to be present
|
||||||
await kvStore.writeKey('key1', 'value1');
|
await kvStore.waitForKeysPresent(['apiKey']);
|
||||||
await kvStore.writeKey('key2', 123);
|
|
||||||
|
|
||||||
const key1Value = await kvStore.readKey('key1');
|
|
||||||
const allData = await kvStore.readAll();
|
|
||||||
|
|
||||||
console.log(key1Value); // Outputs: value1
|
|
||||||
console.log(allData); // Outputs: { key1: 'value1', key2: 123 }
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Combining AppData and KeyValueStore
|
**Storage Types:**
|
||||||
|
- `userHomeDir`: Store in user's home directory
|
||||||
|
- `custom`: Specify your own path
|
||||||
|
- `ephemeral`: In-memory only (perfect for testing)
|
||||||
|
|
||||||
The `AppData` class extends the functionality of `KeyValueStore` by integrating environmental variables, specifying additional configurations, and providing a more structured approach to data management.
|
### 3. AppData - Advanced Environment Management 🌟
|
||||||
|
|
||||||
#### Example: AppData Usage
|
The crown jewel of npmextra - sophisticated environment variable mapping with automatic type conversion:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { AppData } from '@push.rocks/npmextra';
|
import { AppData } from '@push.rocks/npmextra';
|
||||||
|
|
||||||
interface AppSettings {
|
interface AppConfig {
|
||||||
settingA: string;
|
apiUrl: string;
|
||||||
settingB: number;
|
apiKey: string;
|
||||||
nestedSetting: {
|
port: number;
|
||||||
innerSetting: boolean;
|
features: {
|
||||||
}
|
analytics: boolean;
|
||||||
|
payment: boolean;
|
||||||
|
};
|
||||||
|
cache: {
|
||||||
|
ttl: number;
|
||||||
|
redis: {
|
||||||
|
host: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const appDataInstance = await AppData.createAndInit<AppSettings>({
|
const appData = await AppData.createAndInit<AppConfig>({
|
||||||
dirPath: '/custom/path/to/appdata',
|
dirPath: '/app/config', // Optional: defaults to smart path selection
|
||||||
requiredKeys: ['settingA', 'settingB'],
|
requiredKeys: ['apiKey', 'apiUrl'],
|
||||||
envMapping: {
|
envMapping: {
|
||||||
settingA: 'MY_ENV_A',
|
apiUrl: 'API_URL', // Simple mapping
|
||||||
settingB: 'hard:42',
|
apiKey: 'hard:development-key-123', // Hardcoded value
|
||||||
nestedSetting: {
|
port: 'hard:3000', // Hardcoded number
|
||||||
innerSetting: 'MY_ENV_INNER'
|
features: {
|
||||||
|
analytics: 'boolean:ENABLE_ANALYTICS', // Force boolean conversion
|
||||||
|
payment: 'hard_boolean:true' // Hardcoded boolean
|
||||||
|
},
|
||||||
|
cache: {
|
||||||
|
ttl: 'json:CACHE_CONFIG', // Parse JSON from env var
|
||||||
|
redis: {
|
||||||
|
host: 'REDIS_HOST',
|
||||||
|
password: 'base64:REDIS_PASSWORD_B64' // Decode base64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
overwriteObject: {
|
||||||
|
// Force these values regardless of env vars
|
||||||
|
port: 8080
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const store = await appData.getKvStore();
|
||||||
|
const apiUrl = await store.readKey('apiUrl');
|
||||||
|
```
|
||||||
|
|
||||||
|
## AppData Special Cases & Conversions 🎯
|
||||||
|
|
||||||
|
### Environment Variable Prefixes
|
||||||
|
|
||||||
|
AppData supports sophisticated type conversion through prefixes:
|
||||||
|
|
||||||
|
| Prefix | Description | Example | Result |
|
||||||
|
|--------|-------------|---------|--------|
|
||||||
|
| `hard:` | Hardcoded value | `hard:myvalue` | `"myvalue"` |
|
||||||
|
| `hard_boolean:` | Hardcoded boolean | `hard_boolean:true` | `true` |
|
||||||
|
| `hard_json:` | Hardcoded JSON | `hard_json:{"key":"value"}` | `{key: "value"}` |
|
||||||
|
| `hard_base64:` | Hardcoded base64 | `hard_base64:SGVsbG8=` | `"Hello"` |
|
||||||
|
| `boolean:` | Env var as boolean | `boolean:FEATURE_FLAG` | `true/false` |
|
||||||
|
| `json:` | Parse env var as JSON | `json:CONFIG_JSON` | Parsed object |
|
||||||
|
| `base64:` | Decode env var from base64 | `base64:SECRET_B64` | Decoded string |
|
||||||
|
|
||||||
|
### Automatic Suffix Detection
|
||||||
|
|
||||||
|
Variables ending with certain suffixes get automatic conversion:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
envMapping: {
|
||||||
|
// Automatically parsed as JSON if MY_CONFIG_JSON="{"enabled":true}"
|
||||||
|
config: 'MY_CONFIG_JSON',
|
||||||
|
|
||||||
|
// Automatically decoded from base64 if SECRET_KEY_BASE64="SGVsbG8="
|
||||||
|
secret: 'SECRET_KEY_BASE64'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Complex Examples
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const appData = await AppData.createAndInit({
|
||||||
|
envMapping: {
|
||||||
|
// Simple environment variable
|
||||||
|
apiUrl: 'API_URL',
|
||||||
|
|
||||||
|
// Hardcoded values with type conversion
|
||||||
|
debugMode: 'hard_boolean:false',
|
||||||
|
maxRetries: 'hard:5',
|
||||||
|
defaultConfig: 'hard_json:{"timeout":30,"retries":3}',
|
||||||
|
|
||||||
|
// Environment variables with conversion
|
||||||
|
features: 'json:FEATURE_FLAGS', // Expects: {"feature1":true,"feature2":false}
|
||||||
|
isProduction: 'boolean:IS_PROD', // Expects: "true" or "false"
|
||||||
|
apiSecret: 'base64:API_SECRET', // Expects: base64 encoded string
|
||||||
|
|
||||||
|
// Nested structures
|
||||||
|
database: {
|
||||||
|
host: 'DB_HOST',
|
||||||
|
port: 'hard:5432',
|
||||||
|
credentials: {
|
||||||
|
user: 'DB_USER',
|
||||||
|
password: 'base64:DB_PASSWORD_ENCODED',
|
||||||
|
ssl: 'boolean:DB_USE_SSL'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Override any env mappings
|
||||||
|
overwriteObject: {
|
||||||
|
debugMode: true, // Force debug mode regardless of env
|
||||||
|
database: {
|
||||||
|
host: 'localhost' // Force localhost for development
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const appDataKvStore = await appDataInstance.getKvStore();
|
|
||||||
|
|
||||||
// Writing values
|
|
||||||
await appDataKvStore.writeKey('settingA', 'exampleValue');
|
|
||||||
await appDataKvStore.writeKey('settingB', 100);
|
|
||||||
await appDataKvStore.writeKey('nestedSetting', { innerSetting: true });
|
|
||||||
|
|
||||||
// Reading values
|
|
||||||
const settingA = await appDataKvStore.readKey('settingA');
|
|
||||||
const allSettings = await appDataKvStore.readAll();
|
|
||||||
|
|
||||||
console.log(settingA); // Outputs: 'exampleValue'
|
|
||||||
console.log(allSettings); // Outputs: { settingA: 'exampleValue', settingB: 100, nestedSetting: { innerSetting: true } }
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Error Handling and Debugging
|
### Boolean Conversion Rules
|
||||||
|
|
||||||
Proper error handling ensures your integrations with `npmextra` are robust and stable. Below are some strategies for error handling and debugging potential issues.
|
AppData intelligently handles boolean conversions:
|
||||||
|
|
||||||
#### Example: Error Handling in KeyValueStore
|
1. **String "true"/"false"**: Converted to boolean
|
||||||
|
2. **With `boolean:` prefix**: Any env var value is converted (`"true"` → `true`, anything else → `false`)
|
||||||
|
3. **With `hard_boolean:` prefix**: Hardcoded boolean value
|
||||||
|
4. **Regular env vars**: Strings remain strings unless prefixed
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { KeyValueStore } from '@push.rocks/npmextra';
|
// Environment: FEATURE_A="true", FEATURE_B="yes", FEATURE_C="1"
|
||||||
|
{
|
||||||
const kvStore = new KeyValueStore('userHomeDir', 'errorHandlingApp');
|
envMapping: {
|
||||||
|
featureA: 'FEATURE_A', // Result: "true" (string)
|
||||||
try {
|
featureB: 'boolean:FEATURE_B', // Result: false (only "true" → true)
|
||||||
await kvStore.writeKey('importantKey', 'importantValue');
|
featureC: 'boolean:FEATURE_C', // Result: false (only "true" → true)
|
||||||
const value = await kvStore.readKey('importantKey');
|
featureD: 'hard_boolean:true' // Result: true (hardcoded)
|
||||||
console.log(value); // Outputs: importantValue
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('Error managing key-value store:', error);
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Debugging Configuration Issues in `npmextra.json`
|
## Advanced Patterns 🎨
|
||||||
|
|
||||||
To debug configuration issues, you can utilize conditional logging and checks:
|
### Reactive Configuration
|
||||||
|
|
||||||
|
Subscribe to configuration changes:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Npmextra } from '@push.rocks/npmextra';
|
const kvStore = new KeyValueStore<Config>({
|
||||||
|
typeArg: 'custom',
|
||||||
const npmExtraInstance = new Npmextra();
|
identityArg: 'myApp'
|
||||||
const toolConfig = npmExtraInstance.dataFor('toolname', {
|
|
||||||
configKey1: 'defaultValue1',
|
|
||||||
configKey2: 'defaultValue2'
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!toolConfig.configKey1) {
|
// Subscribe to changes
|
||||||
console.error('configKey1 is missing in npmextra.json');
|
kvStore.changeSubject.subscribe((newData) => {
|
||||||
|
console.log('Configuration changed:', newData);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Changes trigger notifications
|
||||||
|
await kvStore.writeKey('theme', 'dark');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing with Ephemeral Storage
|
||||||
|
|
||||||
|
Perfect for unit tests - no file system pollution:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const testStore = new KeyValueStore<TestData>({
|
||||||
|
typeArg: 'ephemeral',
|
||||||
|
identityArg: 'test'
|
||||||
|
});
|
||||||
|
|
||||||
|
// All operations work normally, but nothing persists to disk
|
||||||
|
await testStore.writeKey('testKey', 'testValue');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Smart Path Resolution
|
||||||
|
|
||||||
|
AppData automatically selects the best storage location:
|
||||||
|
|
||||||
|
1. Checks for `/app/data` (containerized environments)
|
||||||
|
2. Falls back to `/data` (alternate container path)
|
||||||
|
3. Uses `.nogit/appdata` (local development)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Automatic path selection
|
||||||
|
const appData = await AppData.createAndInit({
|
||||||
|
// No dirPath specified - smart detection
|
||||||
|
requiredKeys: ['apiKey']
|
||||||
|
});
|
||||||
|
|
||||||
|
// Or force ephemeral for testing
|
||||||
|
const testData = await AppData.createAndInit({
|
||||||
|
ephemeral: true, // No disk persistence
|
||||||
|
requiredKeys: ['testKey']
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Waiting for Configuration
|
||||||
|
|
||||||
|
Block until required configuration is available:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const appData = await AppData.createAndInit<Config>({
|
||||||
|
requiredKeys: ['apiKey', 'apiUrl']
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for specific key
|
||||||
|
const apiKey = await appData.waitForAndGetKey('apiKey');
|
||||||
|
|
||||||
|
// Check missing keys
|
||||||
|
const missingKeys = await appData.logMissingKeys();
|
||||||
|
// Logs: "The following mandatory keys are missing in the appdata:
|
||||||
|
// -> apiKey,
|
||||||
|
// -> apiUrl"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Real-World Example 🌍
|
||||||
|
|
||||||
|
Here's a complete example of a CLI tool using npmextra:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Npmextra, AppData, KeyValueStore } from '@push.rocks/npmextra';
|
||||||
|
|
||||||
|
interface CliConfig {
|
||||||
|
githubToken: string;
|
||||||
|
openaiKey: string;
|
||||||
|
model: 'gpt-3' | 'gpt-4';
|
||||||
|
cache: {
|
||||||
|
enabled: boolean;
|
||||||
|
ttl: number;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(toolConfig);
|
class MyCLI {
|
||||||
|
private npmextra: Npmextra;
|
||||||
|
private appData: AppData<CliConfig>;
|
||||||
|
private cache: KeyValueStore<{[key: string]: any}>;
|
||||||
|
|
||||||
|
async initialize() {
|
||||||
|
// Load tool configuration
|
||||||
|
this.npmextra = new Npmextra();
|
||||||
|
const config = this.npmextra.dataFor<any>('mycli', {
|
||||||
|
defaultModel: 'gpt-3'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Setup app data with env mapping
|
||||||
|
this.appData = await AppData.createAndInit<CliConfig>({
|
||||||
|
requiredKeys: ['githubToken', 'openaiKey'],
|
||||||
|
envMapping: {
|
||||||
|
githubToken: 'GITHUB_TOKEN',
|
||||||
|
openaiKey: 'base64:OPENAI_KEY_ENCODED',
|
||||||
|
model: 'hard:gpt-4',
|
||||||
|
cache: {
|
||||||
|
enabled: 'boolean:ENABLE_CACHE',
|
||||||
|
ttl: 'hard:3600'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize cache
|
||||||
|
this.cache = new KeyValueStore({
|
||||||
|
typeArg: 'userHomeDir',
|
||||||
|
identityArg: 'mycli-cache'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check for missing configuration
|
||||||
|
const missingKeys = await this.appData.logMissingKeys();
|
||||||
|
if (missingKeys.length > 0) {
|
||||||
|
console.error('Please configure the missing keys');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
await this.initialize();
|
||||||
|
|
||||||
|
const config = await this.appData.getKvStore();
|
||||||
|
const settings = await config.readAll();
|
||||||
|
|
||||||
|
console.log(`Using model: ${settings.model}`);
|
||||||
|
console.log(`Cache enabled: ${settings.cache.enabled}`);
|
||||||
|
|
||||||
|
// Use the configuration...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the CLI
|
||||||
|
const cli = new MyCLI();
|
||||||
|
cli.run();
|
||||||
```
|
```
|
||||||
|
|
||||||
### Integration Tests
|
## API Reference 📚
|
||||||
|
|
||||||
Writing tests ensures that your integration with `npmextra` works as expected. Below are examples of integration tests for both `Npmextra` and `KeyValueStore`.
|
### Npmextra Class
|
||||||
|
|
||||||
#### Example: Testing `Npmextra` Class
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { expect, tap } from '@push.rocks/tapbundle';
|
new Npmextra(cwdArg?: string)
|
||||||
import { Npmextra } from '@push.rocks/npmextra';
|
|
||||||
|
|
||||||
let npmExtraInstance: Npmextra;
|
|
||||||
|
|
||||||
tap.test('should create an instance of Npmextra', async () => {
|
|
||||||
npmExtraInstance = new Npmextra();
|
|
||||||
expect(npmExtraInstance).toBeInstanceOf(Npmextra);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('should load configuration from npmextra.json', async () => {
|
|
||||||
const config = npmExtraInstance.dataFor('toolname', {
|
|
||||||
defaultKey1: 'defaultValue1',
|
|
||||||
});
|
|
||||||
expect(config).toHaveProperty('defaultKey1');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.start();
|
|
||||||
```
|
```
|
||||||
|
- `cwdArg`: Optional working directory path
|
||||||
|
|
||||||
#### Example: Testing `KeyValueStore` Class
|
**Methods:**
|
||||||
|
- `dataFor<T>(toolName: string, defaultOptions: T): T` - Get merged configuration
|
||||||
|
|
||||||
|
### KeyValueStore Class
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { expect, tap } from '@push.rocks/tapbundle';
|
new KeyValueStore<T>(options: {
|
||||||
import { KeyValueStore } from '@push.rocks/npmextra';
|
typeArg: 'custom' | 'userHomeDir' | 'ephemeral';
|
||||||
|
identityArg: string;
|
||||||
let kvStore: KeyValueStore<{ key1: string, key2: number }>;
|
customPath?: string;
|
||||||
|
mandatoryKeys?: Array<keyof T>;
|
||||||
tap.test('should create a KeyValueStore instance', async () => {
|
})
|
||||||
kvStore = new KeyValueStore({
|
|
||||||
typeArg: 'userHomeDir',
|
|
||||||
identityArg: 'testApp'
|
|
||||||
});
|
|
||||||
expect(kvStore).toBeInstanceOf(KeyValueStore);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('should write and read back a value', async () => {
|
|
||||||
await kvStore.writeKey('key1', 'value1');
|
|
||||||
const result = await kvStore.readKey('key1');
|
|
||||||
expect(result).toEqual('value1');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('should write and read back multiple values', async () => {
|
|
||||||
await kvStore.writeAll({ key1: 'updatedValue1', key2: 2 });
|
|
||||||
const result = await kvStore.readAll();
|
|
||||||
expect(result).toEqual({ key1: 'updatedValue1', key2: 2 });
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.start();
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Summary
|
**Methods:**
|
||||||
|
- `readKey(key: string): Promise<T>` - Read single value
|
||||||
|
- `writeKey(key: string, value: T): Promise<void>` - Write single value
|
||||||
|
- `readAll(): Promise<T>` - Read all values
|
||||||
|
- `writeAll(data: T): Promise<void>` - Write all values
|
||||||
|
- `deleteKey(key: string): Promise<void>` - Delete a key
|
||||||
|
- `getMissingMandatoryKeys(): Promise<string[]>` - Check missing required keys
|
||||||
|
- `waitForKeysPresent(keys: string[]): Promise<void>` - Wait for keys
|
||||||
|
|
||||||
By centralizing configuration management and offering a versatile key-value store, `@push.rocks/npmextra` significantly simplifies the setup and management of tools and settings in modern JavaScript and TypeScript projects. Whether you're managing project-wide configurations or need persistent storage for key-value pairs, `npmextra` provides an efficient and streamlined solution. Leveraging these robust features will ensure your project is well-configured and maintainable.
|
### AppData Class
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
await AppData.createAndInit<T>(options: {
|
||||||
|
dirPath?: string;
|
||||||
|
requiredKeys?: Array<keyof T>;
|
||||||
|
ephemeral?: boolean;
|
||||||
|
envMapping?: PartialDeep<T>;
|
||||||
|
overwriteObject?: PartialDeep<T>;
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**Methods:**
|
||||||
|
- `getKvStore(): Promise<KeyValueStore<T>>` - Get underlying store
|
||||||
|
- `logMissingKeys(): Promise<Array<keyof T>>` - Log and return missing keys
|
||||||
|
- `waitForAndGetKey<K>(key: K): Promise<T[K]>` - Wait for and retrieve key
|
||||||
|
|
||||||
## License and Legal Information
|
## License and Legal Information
|
||||||
|
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"testTool":{
|
"testTool": {
|
||||||
"testValue":2
|
"testValue": 2
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,5 +1,4 @@
|
|||||||
import { expect, tap } from '@push.rocks/tapbundle';
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
import path = require('path');
|
|
||||||
|
|
||||||
// module to test
|
// module to test
|
||||||
import * as npmextra from '../ts/index.js';
|
import * as npmextra from '../ts/index.js';
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { expect, tap } from '@push.rocks/tapbundle';
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
|
|
||||||
import * as npmextra from '../ts/index.js';
|
import * as npmextra from '../ts/index.js';
|
||||||
|
|
||||||
@@ -29,4 +29,4 @@ tap.test('expect to add an object to the kv Store', async () => {
|
|||||||
await expect(await myKeyValueStore.readKey('myKey')).toEqual('myValue');
|
await expect(await myKeyValueStore.readKey('myKey')).toEqual('myValue');
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.start();
|
export default tap.start();
|
||||||
|
16
test/test.ts
16
test/test.ts
@@ -1,5 +1,4 @@
|
|||||||
import { expect, tap } from '@push.rocks/tapbundle';
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
import path = require('path');
|
|
||||||
|
|
||||||
// module to test
|
// module to test
|
||||||
import * as npmextra from '../ts/index.js';
|
import * as npmextra from '../ts/index.js';
|
||||||
@@ -16,11 +15,14 @@ tap.test('should state wether a npmextra.json exists', async () => {
|
|||||||
expect(testNpmextra.npmextraJsonExists).toBeTrue();
|
expect(testNpmextra.npmextraJsonExists).toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('should pass through default value, if not overriden by config from file', async () => {
|
tap.test(
|
||||||
let testData = testNpmextra.dataFor('testTool', { someKey2: 'someValue2' });
|
'should pass through default value, if not overriden by config from file',
|
||||||
console.log(testData);
|
async () => {
|
||||||
expect(testData).toHaveProperty('someKey2');
|
let testData = testNpmextra.dataFor('testTool', { someKey2: 'someValue2' });
|
||||||
});
|
console.log(testData);
|
||||||
|
expect(testData).toHaveProperty('someKey2');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
tap.test('should read a config file', async () => {
|
tap.test('should read a config file', async () => {
|
||||||
let testData = testNpmextra.dataFor<any>('testTool', {
|
let testData = testNpmextra.dataFor<any>('testTool', {
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* autocreated commitinfo by @pushrocks/commitinfo
|
* autocreated commitinfo by @push.rocks/commitinfo
|
||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/npmextra',
|
name: '@push.rocks/npmextra',
|
||||||
version: '5.0.21',
|
version: '5.1.3',
|
||||||
description: 'A utility to enhance npm with additional configuration, tool management capabilities, and a key-value store for project setups.'
|
description: 'A utility to enhance npm with additional configuration, tool management capabilities, and a key-value store for project setups.'
|
||||||
}
|
}
|
||||||
|
@@ -2,7 +2,6 @@ import * as plugins from './npmextra.plugins.js';
|
|||||||
import * as paths from './npmextra.paths.js';
|
import * as paths from './npmextra.paths.js';
|
||||||
import { KeyValueStore } from './npmextra.classes.keyvaluestore.js';
|
import { KeyValueStore } from './npmextra.classes.keyvaluestore.js';
|
||||||
|
|
||||||
|
|
||||||
export interface IAppDataOptions<T = any> {
|
export interface IAppDataOptions<T = any> {
|
||||||
dirPath?: string;
|
dirPath?: string;
|
||||||
requiredKeys?: Array<keyof T>;
|
requiredKeys?: Array<keyof T>;
|
||||||
@@ -15,7 +14,8 @@ export interface IAppDataOptions<T = any> {
|
|||||||
/**
|
/**
|
||||||
* kvStoreKey: 'MY_ENV_VAR'
|
* kvStoreKey: 'MY_ENV_VAR'
|
||||||
*/
|
*/
|
||||||
envMapping?: plugins.tsclass.typeFest.PartialDeep<T>
|
envMapping?: plugins.tsclass.typeFest.PartialDeep<T>;
|
||||||
|
overwriteObject?: plugins.tsclass.typeFest.PartialDeep<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AppData<T = any> {
|
export class AppData<T = any> {
|
||||||
@@ -25,7 +25,9 @@ export class AppData<T = any> {
|
|||||||
* @param pathArg
|
* @param pathArg
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
public static async createAndInit<T = any>(optionsArg: IAppDataOptions<T> = {}): Promise<AppData<T>> {
|
public static async createAndInit<T = any>(
|
||||||
|
optionsArg: IAppDataOptions<T> = {},
|
||||||
|
): Promise<AppData<T>> {
|
||||||
const appData = new AppData<T>(optionsArg);
|
const appData = new AppData<T>(optionsArg);
|
||||||
await appData.readyDeferred.promise;
|
await appData.readyDeferred.promise;
|
||||||
return appData;
|
return appData;
|
||||||
@@ -68,27 +70,89 @@ export class AppData<T = any> {
|
|||||||
typeArg: this.options.ephermal ? 'ephemeral' : 'custom',
|
typeArg: this.options.ephermal ? 'ephemeral' : 'custom',
|
||||||
identityArg: 'appkv',
|
identityArg: 'appkv',
|
||||||
customPath: this.options.dirPath,
|
customPath: this.options.dirPath,
|
||||||
mandatoryKeys: this.options.requiredKeys as Array<keyof T>
|
mandatoryKeys: this.options.requiredKeys as Array<keyof T>,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.options.envMapping) {
|
if (this.options.envMapping) {
|
||||||
const qenvInstance = new plugins.qenv.Qenv(process.cwd(), plugins.path.join(process.cwd(), '.nogit'));
|
const qenvInstance = new plugins.qenv.Qenv(
|
||||||
|
process.cwd(),
|
||||||
|
plugins.path.join(process.cwd(), '.nogit'),
|
||||||
|
);
|
||||||
|
|
||||||
// Recursive function to handle nested objects, now includes key parameter
|
// Recursive function to handle nested objects, now includes key parameter
|
||||||
const processEnvMapping = async (key: keyof T, mappingValue: any, parentKey: keyof T | '' = ''): Promise<any> => {
|
const processEnvMapping = async (
|
||||||
|
key: keyof T,
|
||||||
|
mappingValue: any,
|
||||||
|
parentKey: keyof T | '' = '',
|
||||||
|
): Promise<any> => {
|
||||||
if (typeof mappingValue === 'string') {
|
if (typeof mappingValue === 'string') {
|
||||||
let envValue: string | T[keyof T];
|
let envValue: string | boolean | T[keyof T];
|
||||||
if (mappingValue.startsWith('hard:')) {
|
let convert: 'none' | 'json' | 'base64' | 'boolean' = 'none';
|
||||||
envValue = mappingValue.replace('hard:', '') as T[keyof T];
|
switch (true) {
|
||||||
} else {
|
case mappingValue.startsWith('hard:'):
|
||||||
envValue = await qenvInstance.getEnvVarOnDemand(mappingValue) as T[keyof T];
|
envValue = mappingValue.replace('hard:', '') as T[keyof T];
|
||||||
|
break;
|
||||||
|
case mappingValue.startsWith('hard_boolean:'):
|
||||||
|
envValue = mappingValue.replace('hard_boolean:', '') === 'true';
|
||||||
|
convert = 'boolean';
|
||||||
|
break;
|
||||||
|
case mappingValue.startsWith('hard_json:'):
|
||||||
|
envValue = JSON.parse(
|
||||||
|
mappingValue.replace('hard_json:', ''),
|
||||||
|
) as T[keyof T];
|
||||||
|
convert = 'json';
|
||||||
|
break;
|
||||||
|
case mappingValue.startsWith('hard_base64:'):
|
||||||
|
envValue = Buffer.from(
|
||||||
|
mappingValue.replace('hard_base64:', ''),
|
||||||
|
'base64',
|
||||||
|
).toString() as T[keyof T];
|
||||||
|
convert = 'base64';
|
||||||
|
break;
|
||||||
|
case mappingValue.startsWith('boolean:'):
|
||||||
|
envValue = (await qenvInstance.getEnvVarOnDemand(
|
||||||
|
mappingValue.replace('boolean:', ''),
|
||||||
|
)) as T[keyof T];
|
||||||
|
convert = 'boolean';
|
||||||
|
break;
|
||||||
|
case mappingValue.startsWith('json:'):
|
||||||
|
envValue = (await qenvInstance.getEnvVarOnDemand(
|
||||||
|
mappingValue.replace('json:', ''),
|
||||||
|
)) as T[keyof T];
|
||||||
|
convert = 'json';
|
||||||
|
break;
|
||||||
|
case mappingValue.startsWith('base64:'):
|
||||||
|
envValue = (await qenvInstance.getEnvVarOnDemand(
|
||||||
|
mappingValue.replace('base64:', ''),
|
||||||
|
)) as T[keyof T];
|
||||||
|
convert = 'base64';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
envValue = (await qenvInstance.getEnvVarOnDemand(
|
||||||
|
mappingValue,
|
||||||
|
)) as T[keyof T];
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// lets format the env value
|
||||||
if (envValue) {
|
if (envValue) {
|
||||||
if (typeof envValue === 'string' && mappingValue.endsWith('_JSON')) {
|
if (typeof envValue === 'string' && convert === 'boolean') {
|
||||||
envValue = JSON.parse(envValue) as T[keyof T];
|
envValue = envValue === 'true';
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
typeof envValue === 'string' &&
|
||||||
|
(mappingValue.endsWith('_JSON') || convert === 'json')
|
||||||
|
) {
|
||||||
|
envValue = JSON.parse(envValue as string) as T[keyof T];
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
typeof envValue === 'string' &&
|
||||||
|
(mappingValue.endsWith('_BASE64') || convert === 'base64')
|
||||||
|
) {
|
||||||
|
envValue = Buffer.from(envValue as string, 'base64').toString();
|
||||||
}
|
}
|
||||||
if (!parentKey) {
|
if (!parentKey) {
|
||||||
await this.kvStore.writeKey(key, envValue);
|
await this.kvStore.writeKey(key, envValue as any);
|
||||||
} else {
|
} else {
|
||||||
return envValue;
|
return envValue;
|
||||||
}
|
}
|
||||||
@@ -100,7 +164,11 @@ export class AppData<T = any> {
|
|||||||
for (const innerKey in mappingValue) {
|
for (const innerKey in mappingValue) {
|
||||||
const nestedValue = mappingValue[innerKey];
|
const nestedValue = mappingValue[innerKey];
|
||||||
// For nested objects, call recursively but do not immediately write to kvStore
|
// For nested objects, call recursively but do not immediately write to kvStore
|
||||||
const nestedResult = await processEnvMapping(innerKey as keyof T, nestedValue, key);
|
const nestedResult = await processEnvMapping(
|
||||||
|
innerKey as keyof T,
|
||||||
|
nestedValue,
|
||||||
|
key,
|
||||||
|
);
|
||||||
resultObject[innerKey as keyof T] = nestedResult;
|
resultObject[innerKey as keyof T] = nestedResult;
|
||||||
}
|
}
|
||||||
if (parentKey === '') {
|
if (parentKey === '') {
|
||||||
@@ -116,6 +184,18 @@ export class AppData<T = any> {
|
|||||||
for (const key in this.options.envMapping) {
|
for (const key in this.options.envMapping) {
|
||||||
await processEnvMapping(key as keyof T, this.options.envMapping[key]);
|
await processEnvMapping(key as keyof T, this.options.envMapping[key]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.options.overwriteObject) {
|
||||||
|
for (const key of Object.keys(this.options.overwriteObject)) {
|
||||||
|
console.log(
|
||||||
|
`-> heads up: overwriting key ${key} from options.overwriteObject`,
|
||||||
|
);
|
||||||
|
await this.kvStore.writeKey(
|
||||||
|
key as keyof T,
|
||||||
|
this.options.overwriteObject[key],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.readyDeferred.resolve();
|
this.readyDeferred.resolve();
|
||||||
@@ -135,8 +215,8 @@ export class AppData<T = any> {
|
|||||||
if (missingMandatoryKeys.length > 0) {
|
if (missingMandatoryKeys.length > 0) {
|
||||||
console.log(
|
console.log(
|
||||||
`The following mandatory keys are missing in the appdata:\n -> ${missingMandatoryKeys.join(
|
`The following mandatory keys are missing in the appdata:\n -> ${missingMandatoryKeys.join(
|
||||||
',\n -> '
|
',\n -> ',
|
||||||
)}`
|
)}`,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
console.log('All mandatory keys are present in the appdata');
|
console.log('All mandatory keys are present in the appdata');
|
||||||
@@ -144,7 +224,9 @@ export class AppData<T = any> {
|
|||||||
return missingMandatoryKeys;
|
return missingMandatoryKeys;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async waitForAndGetKey<K extends keyof T>(keyArg: K): Promise<T[K] | undefined> {
|
public async waitForAndGetKey<K extends keyof T>(
|
||||||
|
keyArg: K,
|
||||||
|
): Promise<T[K] | undefined> {
|
||||||
await this.readyDeferred.promise;
|
await this.readyDeferred.promise;
|
||||||
await this.kvStore.waitForKeysPresent([keyArg]);
|
await this.kvStore.waitForKeysPresent([keyArg]);
|
||||||
return this.kvStore.readKey(keyArg);
|
return this.kvStore.readKey(keyArg);
|
||||||
|
@@ -39,7 +39,7 @@ export class KeyValueStore<T = any> {
|
|||||||
this.deletedObject = {};
|
this.deletedObject = {};
|
||||||
await plugins.smartfile.memory.toFs(
|
await plugins.smartfile.memory.toFs(
|
||||||
plugins.smartjson.stringifyPretty(this.dataObject),
|
plugins.smartjson.stringifyPretty(this.dataObject),
|
||||||
this.filePath
|
this.filePath,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const newStateString = plugins.smartjson.stringify(this.dataObject);
|
const newStateString = plugins.smartjson.stringify(this.dataObject);
|
||||||
@@ -62,10 +62,16 @@ export class KeyValueStore<T = any> {
|
|||||||
}
|
}
|
||||||
if (this.customPath) {
|
if (this.customPath) {
|
||||||
// Use custom path if provided
|
// Use custom path if provided
|
||||||
const absolutePath = plugins.smartpath.transform.makeAbsolute(this.customPath, paths.cwd);
|
const absolutePath = plugins.smartpath.transform.makeAbsolute(
|
||||||
|
this.customPath,
|
||||||
|
paths.cwd,
|
||||||
|
);
|
||||||
this.filePath = absolutePath;
|
this.filePath = absolutePath;
|
||||||
if (plugins.smartfile.fs.isDirectorySync(this.filePath)) {
|
if (plugins.smartfile.fs.isDirectorySync(this.filePath)) {
|
||||||
this.filePath = plugins.path.join(this.filePath, this.identity + '.json');
|
this.filePath = plugins.path.join(
|
||||||
|
this.filePath,
|
||||||
|
this.identity + '.json',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
plugins.smartfile.fs.ensureFileSync(this.filePath, '{}');
|
plugins.smartfile.fs.ensureFileSync(this.filePath, '{}');
|
||||||
return;
|
return;
|
||||||
@@ -129,7 +135,10 @@ export class KeyValueStore<T = any> {
|
|||||||
/**
|
/**
|
||||||
* writes a specific key to the keyValueStore
|
* writes a specific key to the keyValueStore
|
||||||
*/
|
*/
|
||||||
public async writeKey<K extends keyof T>(keyArg: K, valueArg: T[K]): Promise<void> {
|
public async writeKey<K extends keyof T>(
|
||||||
|
keyArg: K,
|
||||||
|
valueArg: T[K],
|
||||||
|
): Promise<void> {
|
||||||
await this.writeAll({
|
await this.writeAll({
|
||||||
[keyArg]: valueArg,
|
[keyArg]: valueArg,
|
||||||
} as unknown as Partial<T>);
|
} as unknown as Partial<T>);
|
||||||
@@ -174,22 +183,28 @@ export class KeyValueStore<T = any> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private setMandatoryKeys(keys: Array<keyof T>) {
|
private setMandatoryKeys(keys: Array<keyof T>) {
|
||||||
keys.forEach(key => this.mandatoryKeys.add(key));
|
keys.forEach((key) => this.mandatoryKeys.add(key));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getMissingMandatoryKeys(): Promise<Array<keyof T>> {
|
public async getMissingMandatoryKeys(): Promise<Array<keyof T>> {
|
||||||
await this.readAll();
|
await this.readAll();
|
||||||
return Array.from(this.mandatoryKeys).filter(key => !(key in this.dataObject));
|
return Array.from(this.mandatoryKeys).filter(
|
||||||
|
(key) => !(key in this.dataObject),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async waitForKeysPresent<K extends keyof T>(keysArg: K[]): Promise<void> {
|
public async waitForKeysPresent<K extends keyof T>(
|
||||||
const missingKeys = keysArg.filter(keyArg => !this.dataObject[keyArg]);
|
keysArg: K[],
|
||||||
|
): Promise<void> {
|
||||||
|
const missingKeys = keysArg.filter((keyArg) => !this.dataObject[keyArg]);
|
||||||
if (missingKeys.length === 0) {
|
if (missingKeys.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
const subscription = this.changeSubject.subscribe(() => {
|
const subscription = this.changeSubject.subscribe(() => {
|
||||||
const missingKeys = keysArg.filter(keyArg => !this.dataObject[keyArg]);
|
const missingKeys = keysArg.filter(
|
||||||
|
(keyArg) => !this.dataObject[keyArg],
|
||||||
|
);
|
||||||
if (missingKeys.length === 0) {
|
if (missingKeys.length === 0) {
|
||||||
subscription.unsubscribe();
|
subscription.unsubscribe();
|
||||||
resolve();
|
resolve();
|
||||||
@@ -198,7 +213,9 @@ export class KeyValueStore<T = any> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async waitForAndGetKey<K extends keyof T>(keyArg: K): Promise<T[K] | undefined> {
|
public async waitForAndGetKey<K extends keyof T>(
|
||||||
|
keyArg: K,
|
||||||
|
): Promise<T[K] | undefined> {
|
||||||
await this.waitForKeysPresent([keyArg]);
|
await this.waitForKeysPresent([keyArg]);
|
||||||
return this.readKey(keyArg);
|
return this.readKey(keyArg);
|
||||||
}
|
}
|
||||||
|
@@ -27,7 +27,10 @@ export class Npmextra {
|
|||||||
/**
|
/**
|
||||||
* merges the supplied options with the ones from npmextra.json
|
* merges the supplied options with the ones from npmextra.json
|
||||||
*/
|
*/
|
||||||
dataFor<IToolConfig>(toolnameArg: string, defaultOptionsArg: any): IToolConfig {
|
dataFor<IToolConfig>(
|
||||||
|
toolnameArg: string,
|
||||||
|
defaultOptionsArg: any,
|
||||||
|
): IToolConfig {
|
||||||
let npmextraToolOptions;
|
let npmextraToolOptions;
|
||||||
if (this.npmextraJsonData[toolnameArg]) {
|
if (this.npmextraJsonData[toolnameArg]) {
|
||||||
npmextraToolOptions = this.npmextraJsonData[toolnameArg];
|
npmextraToolOptions = this.npmextraJsonData[toolnameArg];
|
||||||
@@ -45,7 +48,9 @@ export class Npmextra {
|
|||||||
* checks if the JSON exists
|
* checks if the JSON exists
|
||||||
*/
|
*/
|
||||||
private checkNpmextraJsonExists() {
|
private checkNpmextraJsonExists() {
|
||||||
this.npmextraJsonExists = plugins.smartfile.fs.fileExistsSync(this.lookupPath);
|
this.npmextraJsonExists = plugins.smartfile.fs.fileExistsSync(
|
||||||
|
this.lookupPath,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -64,7 +69,9 @@ export class Npmextra {
|
|||||||
*/
|
*/
|
||||||
private checkNpmextraJsonData() {
|
private checkNpmextraJsonData() {
|
||||||
if (this.npmextraJsonExists) {
|
if (this.npmextraJsonExists) {
|
||||||
this.npmextraJsonData = plugins.smartfile.fs.toObjectSync(this.lookupPath);
|
this.npmextraJsonData = plugins.smartfile.fs.toObjectSync(
|
||||||
|
this.lookupPath,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
this.npmextraJsonData = {};
|
this.npmextraJsonData = {};
|
||||||
}
|
}
|
||||||
|
@@ -4,7 +4,7 @@ import * as plugins from './npmextra.plugins.js';
|
|||||||
export let cwd = process.cwd();
|
export let cwd = process.cwd();
|
||||||
export let packageDir = plugins.path.join(
|
export let packageDir = plugins.path.join(
|
||||||
plugins.smartpath.get.dirnameFromImportMetaUrl(import.meta.url),
|
plugins.smartpath.get.dirnameFromImportMetaUrl(import.meta.url),
|
||||||
'../'
|
'../',
|
||||||
);
|
);
|
||||||
|
|
||||||
// ----------------------
|
// ----------------------
|
||||||
|
@@ -1,8 +1,6 @@
|
|||||||
import * as tsclass from '@tsclass/tsclass';
|
import * as tsclass from '@tsclass/tsclass';
|
||||||
|
|
||||||
export {
|
export { tsclass };
|
||||||
tsclass
|
|
||||||
}
|
|
||||||
|
|
||||||
import * as qenv from '@push.rocks/qenv';
|
import * as qenv from '@push.rocks/qenv';
|
||||||
import * as smartlog from '@push.rocks/smartlog';
|
import * as smartlog from '@push.rocks/smartlog';
|
||||||
@@ -14,4 +12,14 @@ import * as smartpromise from '@push.rocks/smartpromise';
|
|||||||
import * as smartrx from '@push.rocks/smartrx';
|
import * as smartrx from '@push.rocks/smartrx';
|
||||||
import * as taskbuffer from '@push.rocks/taskbuffer';
|
import * as taskbuffer from '@push.rocks/taskbuffer';
|
||||||
|
|
||||||
export { qenv, smartlog, path, smartfile, smartjson, smartpath, smartpromise, smartrx, taskbuffer };
|
export {
|
||||||
|
qenv,
|
||||||
|
smartlog,
|
||||||
|
path,
|
||||||
|
smartfile,
|
||||||
|
smartjson,
|
||||||
|
smartpath,
|
||||||
|
smartpromise,
|
||||||
|
smartrx,
|
||||||
|
taskbuffer,
|
||||||
|
};
|
||||||
|
@@ -1,14 +1,15 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
"useDefineForClassFields": false,
|
"useDefineForClassFields": false,
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"module": "NodeNext",
|
"module": "NodeNext",
|
||||||
"moduleResolution": "NodeNext",
|
"moduleResolution": "NodeNext",
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"verbatimModuleSyntax": true
|
"verbatimModuleSyntax": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {}
|
||||||
},
|
},
|
||||||
"exclude": [
|
"exclude": ["dist_*/**/*.d.ts"]
|
||||||
"dist_*/**/*.d.ts"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user