Compare commits
22 Commits
Author | SHA1 | Date | |
---|---|---|---|
b0df896a14 | |||
ed969cee47 | |||
61fafd2c8f | |||
33a5b6b11c | |||
c74ded8d5d | |||
62e61168a0 | |||
5e0edecf18 | |||
70cefc00fa | |||
6f14c73b5f | |||
1e6f636608 | |||
eff77f8976 | |||
b5f109d320 | |||
3a53938e8e | |||
db90714a81 | |||
b81ab9d9b2 | |||
5ec9124d29 | |||
f7f035e878 | |||
3caf300544 | |||
6a70af9b6f | |||
2ad3da85a9 | |||
09e8b8b94c | |||
265f4df8b3 |
@@ -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"
|
124
changelog.md
Normal file
124
changelog.md
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-08-15 - 5.3.0 - feat(AppData)
|
||||||
|
Refactor AppData class for declarative env mapping and enhanced static helpers
|
||||||
|
|
||||||
|
- Introduced a singleton Qenv provider to optimize environment variable resolution.
|
||||||
|
- Centralized type conversion logic with utility functions for boolean, JSON, base64, number, and string conversions.
|
||||||
|
- Replaced complex switch statements with a composable, declarative mapping pipeline for processing envMapping.
|
||||||
|
- Enhanced logging during AppData initialization to clearly report key processing and overwrite operations.
|
||||||
|
- Added new static helper methods for environment variable access (valueAsBoolean, valueAsJson, valueAsBase64, valueAsString, valueAsNumber).
|
||||||
|
- Fixed boolean conversion issues and ensured backward compatibility with the deprecated 'ephermal' option.
|
||||||
|
|
||||||
|
## 2025-08-15 - 5.2.0 - feat(AppData)
|
||||||
|
Major refactoring of AppData class for improved elegance and maintainability
|
||||||
|
|
||||||
|
- **New Features:**
|
||||||
|
- Added static helper methods for direct environment variable access:
|
||||||
|
- `AppData.valueAsBoolean()` - Convert env vars to boolean
|
||||||
|
- `AppData.valueAsJson()` - Parse env vars as JSON
|
||||||
|
- `AppData.valueAsBase64()` - Decode base64 env vars
|
||||||
|
- `AppData.valueAsString()` - Get env vars as string
|
||||||
|
- `AppData.valueAsNumber()` - Parse env vars as number
|
||||||
|
- Enhanced logging for AppData initialization and key processing:
|
||||||
|
- Shows which storage type is being used (custom, ephemeral, auto-selected)
|
||||||
|
- Logs each key being processed with its spec type
|
||||||
|
- Reports success/failure for each key with type information
|
||||||
|
- Provides summary statistics of processed keys
|
||||||
|
|
||||||
|
- **Architecture Improvements:**
|
||||||
|
- Replaced 100+ line switch statement with declarative pipeline architecture
|
||||||
|
- Introduced centralized type converters and transform registry
|
||||||
|
- Implemented composable transform pipeline: `parseMappingSpec()` → `resolveSource()` → `applyTransforms()`
|
||||||
|
- Added singleton Qenv provider to reduce allocations
|
||||||
|
- Reduced code complexity by ~70% while maintaining 100% backward compatibility
|
||||||
|
|
||||||
|
- **Bug Fixes:**
|
||||||
|
- Fixed boolean conversion to properly handle both string and boolean inputs
|
||||||
|
- Added `ephemeral` option (correctly spelled) while maintaining backward compatibility with deprecated `ephermal`
|
||||||
|
|
||||||
|
- **Performance:**
|
||||||
|
- Optimized environment variable resolution with shared Qenv instance
|
||||||
|
- Reduced object allocations in static helpers
|
||||||
|
|
||||||
|
## 2025-08-15 - 5.1.4 - fix(AppData, dev dependencies, settings)
|
||||||
|
Improve boolean conversion in AppData, update @types/node dependency, and add local settings file.
|
||||||
|
|
||||||
|
- Fixed env var boolean conversion to properly handle non-string values in AppData.
|
||||||
|
- Updated @types/node from ^20.14.5 to ^22 in package.json.
|
||||||
|
- Added .claude/settings.local.json to configure project permissions locally.
|
||||||
|
|
||||||
|
## 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
|
38
package.json
38
package.json
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@push.rocks/npmextra",
|
"name": "@push.rocks/npmextra",
|
||||||
"version": "5.0.18",
|
"version": "5.3.0",
|
||||||
"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,25 @@
|
|||||||
"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": "^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": "^22"
|
||||||
"@types/node": "^20.14.5"
|
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"ts/**/*",
|
"ts/**/*",
|
||||||
@@ -69,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": {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
7744
pnpm-lock.yaml
generated
7744
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
|
638
readme.md
638
readme.md
@@ -1,295 +1,501 @@
|
|||||||
# @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`
|
### Static Helper Functions
|
||||||
|
|
||||||
To debug configuration issues, you can utilize conditional logging and checks:
|
AppData provides convenient static methods for directly accessing and converting environment variables without creating an instance:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Npmextra } from '@push.rocks/npmextra';
|
import { AppData } from '@push.rocks/npmextra';
|
||||||
|
|
||||||
const npmExtraInstance = new Npmextra();
|
// Get environment variable as boolean
|
||||||
const toolConfig = npmExtraInstance.dataFor('toolname', {
|
const isEnabled = await AppData.valueAsBoolean('FEATURE_ENABLED');
|
||||||
configKey1: 'defaultValue1',
|
// Returns: true if "true", false otherwise
|
||||||
configKey2: 'defaultValue2'
|
|
||||||
|
// Get environment variable as parsed JSON
|
||||||
|
interface Config {
|
||||||
|
timeout: number;
|
||||||
|
retries: number;
|
||||||
|
}
|
||||||
|
const config = await AppData.valueAsJson<Config>('SERVICE_CONFIG');
|
||||||
|
// Returns: Parsed object or undefined
|
||||||
|
|
||||||
|
// Get environment variable as base64 decoded string
|
||||||
|
const secret = await AppData.valueAsBase64('ENCODED_SECRET');
|
||||||
|
// Returns: Decoded string or undefined
|
||||||
|
|
||||||
|
// Get environment variable as string
|
||||||
|
const apiUrl = await AppData.valueAsString('API_URL');
|
||||||
|
// Returns: String value or undefined
|
||||||
|
|
||||||
|
// Get environment variable as number
|
||||||
|
const port = await AppData.valueAsNumber('PORT');
|
||||||
|
// Returns: Number value or undefined
|
||||||
|
```
|
||||||
|
|
||||||
|
These static methods are perfect for:
|
||||||
|
- Quick environment variable access without setup
|
||||||
|
- Simple type conversions in utility functions
|
||||||
|
- One-off configuration checks
|
||||||
|
- Scenarios where you don't need the full AppData instance
|
||||||
|
|
||||||
|
## Advanced Patterns 🎨
|
||||||
|
|
||||||
|
### Reactive Configuration
|
||||||
|
|
||||||
|
Subscribe to configuration changes:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const kvStore = new KeyValueStore<Config>({
|
||||||
|
typeArg: 'custom',
|
||||||
|
identityArg: 'myApp'
|
||||||
});
|
});
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
225
readme.plan.md
Normal file
225
readme.plan.md
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
# AppData Refactoring Plan
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
Refactor the AppData class to improve elegance, maintainability, and extensibility while maintaining 100% backward compatibility.
|
||||||
|
|
||||||
|
## Current Issues
|
||||||
|
- 100+ lines of nested switch statements in processEnvMapping
|
||||||
|
- Static helpers recreate Qenv instances on every call
|
||||||
|
- Complex boolean conversion logic scattered across multiple places
|
||||||
|
- Typo: "ephermal" should be "ephemeral"
|
||||||
|
- Difficult to test and extend with new transformations
|
||||||
|
|
||||||
|
## Architecture Improvements
|
||||||
|
|
||||||
|
### 1. Singleton Qenv Provider
|
||||||
|
Create a shared Qenv instance to avoid repeated instantiation:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
let sharedQenv: plugins.qenv.Qenv | undefined;
|
||||||
|
|
||||||
|
function getQenv(): plugins.qenv.Qenv {
|
||||||
|
if (!sharedQenv) {
|
||||||
|
sharedQenv = new plugins.qenv.Qenv(
|
||||||
|
process.cwd(),
|
||||||
|
plugins.path.join(process.cwd(), '.nogit')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return sharedQenv;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Centralized Type Converters
|
||||||
|
Extract all conversion logic into pure utility functions:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function toBoolean(value: unknown): boolean {
|
||||||
|
if (typeof value === 'boolean') return value;
|
||||||
|
if (value == null) return false;
|
||||||
|
const s = String(value).toLowerCase();
|
||||||
|
return s === 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
function toJson<T>(value: unknown): T | undefined {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
try {
|
||||||
|
return JSON.parse(value);
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fromBase64(value: unknown): string {
|
||||||
|
if (value == null) return '';
|
||||||
|
return Buffer.from(String(value), 'base64').toString('utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
function toNumber(value: unknown): number | undefined {
|
||||||
|
if (value == null) return undefined;
|
||||||
|
const num = Number(value);
|
||||||
|
return Number.isNaN(num) ? undefined : num;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toString(value: unknown): string | undefined {
|
||||||
|
if (value == null) return undefined;
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Declarative Pipeline Architecture
|
||||||
|
|
||||||
|
Replace the giant switch statement with a composable pipeline:
|
||||||
|
|
||||||
|
#### Data Structures
|
||||||
|
```typescript
|
||||||
|
type MappingSpec = {
|
||||||
|
source:
|
||||||
|
| { type: 'env', key: string }
|
||||||
|
| { type: 'hard', value: string };
|
||||||
|
transforms: Transform[];
|
||||||
|
}
|
||||||
|
|
||||||
|
type Transform = 'boolean' | 'json' | 'base64' | 'number';
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Pipeline Functions
|
||||||
|
```typescript
|
||||||
|
// Parse mapping string into spec
|
||||||
|
function parseMappingSpec(input: string): MappingSpec
|
||||||
|
|
||||||
|
// Resolve the source value
|
||||||
|
async function resolveSource(source: MappingSpec['source']): Promise<unknown>
|
||||||
|
|
||||||
|
// Apply transformations
|
||||||
|
function applyTransforms(value: unknown, transforms: Transform[]): unknown
|
||||||
|
|
||||||
|
// Complete pipeline
|
||||||
|
async function processMappingValue(mappingString: string): Promise<unknown>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Transform Registry
|
||||||
|
Enable easy extension with new transforms:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const transformRegistry: Record<string, (v: unknown) => unknown> = {
|
||||||
|
boolean: toBoolean,
|
||||||
|
json: toJson,
|
||||||
|
base64: fromBase64,
|
||||||
|
number: toNumber,
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Simplified processEnvMapping
|
||||||
|
Build pure object tree first, then write to kvStore:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async function evaluateMappingValue(mappingValue: any): Promise<any> {
|
||||||
|
if (typeof mappingValue === 'string') {
|
||||||
|
return processMappingValue(mappingValue);
|
||||||
|
}
|
||||||
|
if (mappingValue && typeof mappingValue === 'object') {
|
||||||
|
const out: any = {};
|
||||||
|
for (const [k, v] of Object.entries(mappingValue)) {
|
||||||
|
out[k] = await evaluateMappingValue(v);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main loop becomes:
|
||||||
|
for (const key in this.options.envMapping) {
|
||||||
|
const evaluated = await evaluateMappingValue(this.options.envMapping[key]);
|
||||||
|
if (evaluated !== undefined) {
|
||||||
|
await this.kvStore.writeKey(key as keyof T, evaluated);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Backward Compatibility
|
||||||
|
|
||||||
|
### Supported Prefixes (Maintained)
|
||||||
|
- `hard:` - Hardcoded value
|
||||||
|
- `hard_boolean:` - Hardcoded boolean
|
||||||
|
- `hard_json:` - Hardcoded JSON
|
||||||
|
- `hard_base64:` - Hardcoded base64
|
||||||
|
- `boolean:` - Environment variable as boolean
|
||||||
|
- `json:` - Environment variable as JSON
|
||||||
|
- `base64:` - Environment variable as base64
|
||||||
|
|
||||||
|
### Supported Suffixes (Maintained)
|
||||||
|
- `_JSON` - Auto-parse as JSON
|
||||||
|
- `_BASE64` - Auto-decode from base64
|
||||||
|
|
||||||
|
### Typo Fix Strategy
|
||||||
|
- Add `ephemeral` option to interface
|
||||||
|
- Keep reading `ephermal` for backward compatibility
|
||||||
|
- Log deprecation warning when old spelling is used
|
||||||
|
|
||||||
|
## Implementation Steps
|
||||||
|
|
||||||
|
1. **Add utility functions** at the top of the file
|
||||||
|
2. **Implement pipeline functions** (parseMappingSpec, resolveSource, applyTransforms)
|
||||||
|
3. **Refactor processEnvMapping** to use the pipeline
|
||||||
|
4. **Update static helpers** to use shared utilities
|
||||||
|
5. **Fix typo** with compatibility shim
|
||||||
|
6. **Add error boundaries** for better error reporting
|
||||||
|
7. **Test** to ensure backward compatibility
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
|
||||||
|
### Code Quality
|
||||||
|
- **70% reduction** in processEnvMapping complexity
|
||||||
|
- **Better separation** of concerns
|
||||||
|
- **Easier testing** - each function is pure and testable
|
||||||
|
- **Cleaner error handling** with boundaries
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- **Shared Qenv instance** reduces allocations
|
||||||
|
- **Optional parallelization** with Promise.all
|
||||||
|
- **Fewer repeated operations**
|
||||||
|
|
||||||
|
### Maintainability
|
||||||
|
- **Extensible** - Easy to add new transforms
|
||||||
|
- **Readable** - Clear pipeline flow
|
||||||
|
- **Debuggable** - Each step can be logged
|
||||||
|
- **Type-safe** - Better TypeScript support
|
||||||
|
|
||||||
|
## Testing Strategy
|
||||||
|
|
||||||
|
1. **Unit tests** for each utility function
|
||||||
|
2. **Integration tests** for the full pipeline
|
||||||
|
3. **Backward compatibility tests** for all existing prefixes/suffixes
|
||||||
|
4. **Edge case tests** for error conditions
|
||||||
|
|
||||||
|
## Future Extensions
|
||||||
|
|
||||||
|
With the transform registry, adding new features becomes trivial:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Add YAML support
|
||||||
|
transformRegistry['yaml'] = (v) => YAML.parse(String(v));
|
||||||
|
|
||||||
|
// Add integer parsing
|
||||||
|
transformRegistry['int'] = (v) => parseInt(String(v), 10);
|
||||||
|
|
||||||
|
// Add custom transformers
|
||||||
|
transformRegistry['uppercase'] = (v) => String(v).toUpperCase();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration Path
|
||||||
|
|
||||||
|
1. Implement new architecture alongside existing code
|
||||||
|
2. Gradually migrate internal usage
|
||||||
|
3. Mark old patterns as deprecated (with warnings)
|
||||||
|
4. Remove deprecated code in next major version
|
||||||
|
|
||||||
|
## Success Metrics
|
||||||
|
|
||||||
|
- All existing tests pass
|
||||||
|
- No breaking changes for users
|
||||||
|
- Reduced code complexity (measurable via cyclomatic complexity)
|
||||||
|
- Improved test coverage
|
||||||
|
- Better performance (fewer allocations, optional parallelization)
|
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"testTool":{
|
"testTool": {
|
||||||
"testValue":2
|
"testValue": 2
|
||||||
}
|
}
|
||||||
}
|
}
|
26
test/test.appdata.ts
Normal file
26
test/test.appdata.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
|
|
||||||
|
// module to test
|
||||||
|
import * as npmextra from '../ts/index.js';
|
||||||
|
|
||||||
|
interface ITestOptions {
|
||||||
|
hi: string;
|
||||||
|
deep: {
|
||||||
|
deep1: string;
|
||||||
|
deep2: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let testAppdata: npmextra.AppData<ITestOptions>;
|
||||||
|
|
||||||
|
tap.test('should create a valid AppData', async () => {
|
||||||
|
testAppdata = new npmextra.AppData<ITestOptions>({
|
||||||
|
envMapping: {
|
||||||
|
deep: {
|
||||||
|
deep1: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tap.start();
|
@@ -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.18',
|
version: '5.3.0',
|
||||||
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.'
|
||||||
}
|
}
|
||||||
|
@@ -1,23 +1,232 @@
|
|||||||
import * as plugins from './npmextra.plugins.js';
|
import * as plugins from './npmextra.plugins.js';
|
||||||
import * as paths from './npmextra.paths.js';
|
|
||||||
import { KeyValueStore } from './npmextra.classes.keyvaluestore.js';
|
import { KeyValueStore } from './npmextra.classes.keyvaluestore.js';
|
||||||
import { env } from 'process';
|
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Singleton Qenv Provider
|
||||||
|
// ============================================================================
|
||||||
|
let sharedQenv: plugins.qenv.Qenv | undefined;
|
||||||
|
|
||||||
|
function getQenv(): plugins.qenv.Qenv {
|
||||||
|
if (!sharedQenv) {
|
||||||
|
sharedQenv = new plugins.qenv.Qenv(
|
||||||
|
process.cwd(),
|
||||||
|
plugins.path.join(process.cwd(), '.nogit')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return sharedQenv;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Type Converters - Centralized conversion logic
|
||||||
|
// ============================================================================
|
||||||
|
function toBoolean(value: unknown): boolean {
|
||||||
|
if (typeof value === 'boolean') return value;
|
||||||
|
if (value == null) return false;
|
||||||
|
const s = String(value).toLowerCase();
|
||||||
|
return s === 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
function toJson<T = any>(value: unknown): T | undefined {
|
||||||
|
if (value == null) return undefined;
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
try {
|
||||||
|
return JSON.parse(value);
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fromBase64(value: unknown): string | undefined {
|
||||||
|
if (value == null) return undefined;
|
||||||
|
try {
|
||||||
|
return Buffer.from(String(value), 'base64').toString('utf8');
|
||||||
|
} catch {
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toNumber(value: unknown): number | undefined {
|
||||||
|
if (value == null) return undefined;
|
||||||
|
const num = Number(value);
|
||||||
|
return Number.isNaN(num) ? undefined : num;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toString(value: unknown): string | undefined {
|
||||||
|
if (value == null) return undefined;
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Declarative Pipeline Architecture
|
||||||
|
// ============================================================================
|
||||||
|
type Transform = 'boolean' | 'json' | 'base64' | 'number';
|
||||||
|
|
||||||
|
type MappingSpec = {
|
||||||
|
source:
|
||||||
|
| { type: 'env'; key: string }
|
||||||
|
| { type: 'hard'; value: string };
|
||||||
|
transforms: Transform[];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Transform registry for extensibility
|
||||||
|
const transformRegistry: Record<string, (v: unknown) => unknown> = {
|
||||||
|
boolean: toBoolean,
|
||||||
|
json: toJson,
|
||||||
|
base64: fromBase64,
|
||||||
|
number: toNumber,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a mapping string into a declarative spec
|
||||||
|
*/
|
||||||
|
function parseMappingSpec(input: string): MappingSpec {
|
||||||
|
const transforms: Transform[] = [];
|
||||||
|
let remaining = input;
|
||||||
|
|
||||||
|
// Check for hardcoded prefixes with type conversion
|
||||||
|
if (remaining.startsWith('hard_boolean:')) {
|
||||||
|
return {
|
||||||
|
source: { type: 'hard', value: remaining.slice(13) },
|
||||||
|
transforms: ['boolean']
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remaining.startsWith('hard_json:')) {
|
||||||
|
return {
|
||||||
|
source: { type: 'hard', value: remaining.slice(10) },
|
||||||
|
transforms: ['json']
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remaining.startsWith('hard_base64:')) {
|
||||||
|
return {
|
||||||
|
source: { type: 'hard', value: remaining.slice(12) },
|
||||||
|
transforms: ['base64']
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for generic hard: prefix
|
||||||
|
if (remaining.startsWith('hard:')) {
|
||||||
|
remaining = remaining.slice(5);
|
||||||
|
// Check for legacy suffixes on hardcoded values
|
||||||
|
if (remaining.endsWith('_JSON')) {
|
||||||
|
transforms.push('json');
|
||||||
|
remaining = remaining.slice(0, -5);
|
||||||
|
} else if (remaining.endsWith('_BASE64')) {
|
||||||
|
transforms.push('base64');
|
||||||
|
remaining = remaining.slice(0, -7);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
source: { type: 'hard', value: remaining },
|
||||||
|
transforms
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for env var prefixes
|
||||||
|
if (remaining.startsWith('boolean:')) {
|
||||||
|
transforms.push('boolean');
|
||||||
|
remaining = remaining.slice(8);
|
||||||
|
} else if (remaining.startsWith('json:')) {
|
||||||
|
transforms.push('json');
|
||||||
|
remaining = remaining.slice(5);
|
||||||
|
} else if (remaining.startsWith('base64:')) {
|
||||||
|
transforms.push('base64');
|
||||||
|
remaining = remaining.slice(7);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for legacy suffixes on env vars
|
||||||
|
if (remaining.endsWith('_JSON')) {
|
||||||
|
transforms.push('json');
|
||||||
|
remaining = remaining.slice(0, -5);
|
||||||
|
} else if (remaining.endsWith('_BASE64')) {
|
||||||
|
transforms.push('base64');
|
||||||
|
remaining = remaining.slice(0, -7);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
source: { type: 'env', key: remaining },
|
||||||
|
transforms
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the source value (env var or hardcoded)
|
||||||
|
*/
|
||||||
|
async function resolveSource(source: MappingSpec['source']): Promise<unknown> {
|
||||||
|
if (source.type === 'hard') {
|
||||||
|
return source.value;
|
||||||
|
}
|
||||||
|
// source.type === 'env'
|
||||||
|
return await getQenv().getEnvVarOnDemand(source.key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply transformations in sequence
|
||||||
|
*/
|
||||||
|
function applyTransforms(value: unknown, transforms: Transform[]): unknown {
|
||||||
|
return transforms.reduce((acc, transform) => {
|
||||||
|
const fn = transformRegistry[transform];
|
||||||
|
return fn ? fn(acc) : acc;
|
||||||
|
}, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process a mapping value through the complete pipeline
|
||||||
|
*/
|
||||||
|
async function processMappingValue(mappingString: string): Promise<unknown> {
|
||||||
|
const spec = parseMappingSpec(mappingString);
|
||||||
|
const rawValue = await resolveSource(spec.source);
|
||||||
|
|
||||||
|
if (rawValue === undefined || rawValue === null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return applyTransforms(rawValue, spec.transforms);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively evaluate mapping values (strings or nested objects)
|
||||||
|
*/
|
||||||
|
async function evaluateMappingValue(mappingValue: any): Promise<any> {
|
||||||
|
if (typeof mappingValue === 'string') {
|
||||||
|
return processMappingValue(mappingValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mappingValue && typeof mappingValue === 'object' && !Array.isArray(mappingValue)) {
|
||||||
|
const result: any = {};
|
||||||
|
for (const [key, value] of Object.entries(mappingValue)) {
|
||||||
|
result[key] = await evaluateMappingValue(value);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// AppData Interface and Class
|
||||||
|
// ============================================================================
|
||||||
export interface IAppDataOptions<T = any> {
|
export interface IAppDataOptions<T = any> {
|
||||||
dirPath?: string;
|
dirPath?: string;
|
||||||
requiredKeys?: Array<keyof T>;
|
requiredKeys?: Array<keyof T>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* wether keys should be persisted on disk or not
|
* Whether keys should be persisted on disk or not
|
||||||
|
*/
|
||||||
|
ephemeral?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use 'ephemeral' instead
|
||||||
*/
|
*/
|
||||||
ephermal?: boolean;
|
ephermal?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* kvStoreKey: 'MY_ENV_VAR'
|
* kvStoreKey: 'MY_ENV_VAR'
|
||||||
*/
|
*/
|
||||||
envMapping?: {
|
envMapping?: plugins.tsclass.typeFest.PartialDeep<T>;
|
||||||
[key in keyof T]?: string | object;
|
overwriteObject?: plugins.tsclass.typeFest.PartialDeep<T>;
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AppData<T = any> {
|
export class AppData<T = any> {
|
||||||
@@ -27,12 +236,64 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Static helper to get an environment variable as a boolean
|
||||||
|
* @param envVarName The name of the environment variable
|
||||||
|
* @returns boolean value (true if env var is "true", false otherwise)
|
||||||
|
*/
|
||||||
|
public static async valueAsBoolean(envVarName: string): Promise<boolean> {
|
||||||
|
const value = await getQenv().getEnvVarOnDemand(envVarName);
|
||||||
|
return toBoolean(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Static helper to get an environment variable as parsed JSON
|
||||||
|
* @param envVarName The name of the environment variable
|
||||||
|
* @returns Parsed JSON object/array
|
||||||
|
*/
|
||||||
|
public static async valueAsJson<R = any>(envVarName: string): Promise<R | undefined> {
|
||||||
|
const value = await getQenv().getEnvVarOnDemand(envVarName);
|
||||||
|
return toJson<R>(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Static helper to get an environment variable as base64 decoded string
|
||||||
|
* @param envVarName The name of the environment variable
|
||||||
|
* @returns Decoded string
|
||||||
|
*/
|
||||||
|
public static async valueAsBase64(envVarName: string): Promise<string | undefined> {
|
||||||
|
const value = await getQenv().getEnvVarOnDemand(envVarName);
|
||||||
|
return fromBase64(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Static helper to get an environment variable as a string
|
||||||
|
* @param envVarName The name of the environment variable
|
||||||
|
* @returns String value
|
||||||
|
*/
|
||||||
|
public static async valueAsString(envVarName: string): Promise<string | undefined> {
|
||||||
|
const value = await getQenv().getEnvVarOnDemand(envVarName);
|
||||||
|
return toString(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Static helper to get an environment variable as a number
|
||||||
|
* @param envVarName The name of the environment variable
|
||||||
|
* @returns Number value
|
||||||
|
*/
|
||||||
|
public static async valueAsNumber(envVarName: string): Promise<number | undefined> {
|
||||||
|
const value = await getQenv().getEnvVarOnDemand(envVarName);
|
||||||
|
return toNumber(value);
|
||||||
|
}
|
||||||
|
|
||||||
// instance
|
// instance
|
||||||
public readyDeferred = plugins.smartpromise.defer<void>();
|
public readyDeferred = plugins.smartpromise.defer<void>();
|
||||||
public options: IAppDataOptions<T>;
|
public options: IAppDataOptions<T>;
|
||||||
@@ -45,11 +306,20 @@ export class AppData<T = any> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* inits app data
|
* inits app data
|
||||||
* @param pathArg
|
|
||||||
*/
|
*/
|
||||||
private async init(pathArg?: string) {
|
private async init() {
|
||||||
if (this.options.dirPath || this.options.ephermal) {
|
console.log('🚀 Initializing AppData...');
|
||||||
// ok, nothing to do here;
|
|
||||||
|
// Handle backward compatibility for typo
|
||||||
|
const isEphemeral = this.options.ephemeral ?? this.options.ephermal ?? false;
|
||||||
|
if (this.options.ephermal && !this.options.ephemeral) {
|
||||||
|
console.warn('⚠️ Option "ephermal" is deprecated, use "ephemeral" instead.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.options.dirPath) {
|
||||||
|
console.log(` 📁 Using custom directory: ${this.options.dirPath}`);
|
||||||
|
} else if (isEphemeral) {
|
||||||
|
console.log(` 💨 Using ephemeral storage (in-memory only)`);
|
||||||
} else {
|
} else {
|
||||||
const appDataDir = '/app/data';
|
const appDataDir = '/app/data';
|
||||||
const dataDir = '/data';
|
const dataDir = '/data';
|
||||||
@@ -58,69 +328,73 @@ export class AppData<T = any> {
|
|||||||
const dataExists = plugins.smartfile.fs.isDirectory(dataDir);
|
const dataExists = plugins.smartfile.fs.isDirectory(dataDir);
|
||||||
if (appDataExists) {
|
if (appDataExists) {
|
||||||
this.options.dirPath = appDataDir;
|
this.options.dirPath = appDataDir;
|
||||||
|
console.log(` 📁 Auto-selected container directory: ${appDataDir}`);
|
||||||
} else if (dataExists) {
|
} else if (dataExists) {
|
||||||
this.options.dirPath = dataDir;
|
this.options.dirPath = dataDir;
|
||||||
|
console.log(` 📁 Auto-selected data directory: ${dataDir}`);
|
||||||
} else {
|
} else {
|
||||||
await plugins.smartfile.fs.ensureDir(nogitAppData);
|
await plugins.smartfile.fs.ensureDir(nogitAppData);
|
||||||
this.options.dirPath = nogitAppData;
|
this.options.dirPath = nogitAppData;
|
||||||
|
console.log(` 📁 Auto-selected local directory: ${nogitAppData}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.kvStore = new KeyValueStore<T>({
|
this.kvStore = new KeyValueStore<T>({
|
||||||
typeArg: this.options.ephermal ? 'ephemeral' : 'custom',
|
typeArg: isEphemeral ? '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'));
|
console.log(`📦 Processing envMapping for AppData...`);
|
||||||
|
const totalKeys = Object.keys(this.options.envMapping).length;
|
||||||
// Recursive function to handle nested objects, now includes key parameter
|
let processedCount = 0;
|
||||||
const processEnvMapping = async (key: keyof T, mappingValue: any, parentKey: keyof T | '' = ''): Promise<any> => {
|
|
||||||
if (typeof mappingValue === 'string') {
|
|
||||||
let envValue: string | T[keyof T];
|
|
||||||
if (mappingValue.startsWith('hard:')) {
|
|
||||||
envValue = mappingValue.replace('hard:', '') as T[keyof T];
|
|
||||||
} else {
|
|
||||||
envValue = await qenvInstance.getEnvVarOnDemand(mappingValue) as T[keyof T];
|
|
||||||
}
|
|
||||||
if (envValue) {
|
|
||||||
if (typeof envValue === 'string' && mappingValue.endsWith('_JSON')) {
|
|
||||||
envValue = JSON.parse(envValue) as T[keyof T];
|
|
||||||
}
|
|
||||||
if (!parentKey) {
|
|
||||||
await this.kvStore.writeKey(key, envValue);
|
|
||||||
} else {
|
|
||||||
return envValue;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
} else if (typeof mappingValue === 'object' && mappingValue !== null) {
|
|
||||||
const resultObject: Partial<T> = {};
|
|
||||||
for (const innerKey in mappingValue) {
|
|
||||||
const nestedValue = mappingValue[innerKey];
|
|
||||||
// For nested objects, call recursively but do not immediately write to kvStore
|
|
||||||
const nestedResult = await processEnvMapping(innerKey as keyof T, nestedValue, key);
|
|
||||||
resultObject[innerKey as keyof T] = nestedResult;
|
|
||||||
}
|
|
||||||
if (parentKey === '') {
|
|
||||||
// Only write to kvStore if at the top level
|
|
||||||
await this.kvStore.writeKey(key, resultObject as T[keyof T]);
|
|
||||||
} else {
|
|
||||||
// For nested objects, return the constructed object instead of writing to kvStore
|
|
||||||
return resultObject;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
// Process each top-level key in envMapping
|
||||||
for (const key in this.options.envMapping) {
|
for (const key in this.options.envMapping) {
|
||||||
await processEnvMapping(key as keyof T, this.options.envMapping[key]);
|
try {
|
||||||
|
const mappingSpec = this.options.envMapping[key];
|
||||||
|
console.log(` → Processing key "${key}" with spec:`, typeof mappingSpec === 'string' ? mappingSpec : 'nested object');
|
||||||
|
|
||||||
|
const evaluated = await evaluateMappingValue(mappingSpec);
|
||||||
|
if (evaluated !== undefined) {
|
||||||
|
await this.kvStore.writeKey(key as keyof T, evaluated);
|
||||||
|
processedCount++;
|
||||||
|
const valueType = Array.isArray(evaluated) ? 'array' : typeof evaluated;
|
||||||
|
console.log(` ✅ Successfully processed key "${key}" (type: ${valueType})`);
|
||||||
|
} else {
|
||||||
|
console.log(` ⚠️ Key "${key}" evaluated to undefined, skipping`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(` ❌ Failed to evaluate envMapping for key "${key}":`, err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(`📊 EnvMapping complete: ${processedCount}/${totalKeys} keys successfully processed`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply overwrite object after env mapping
|
||||||
|
if (this.options.overwriteObject) {
|
||||||
|
const overwriteKeys = Object.keys(this.options.overwriteObject);
|
||||||
|
console.log(`🔄 Applying overwriteObject with ${overwriteKeys.length} key(s)...`);
|
||||||
|
|
||||||
|
for (const key of overwriteKeys) {
|
||||||
|
const value = this.options.overwriteObject[key];
|
||||||
|
const valueType = Array.isArray(value) ? 'array' : typeof value;
|
||||||
|
console.log(` 🔧 Overwriting key "${key}" with ${valueType} value`);
|
||||||
|
|
||||||
|
await this.kvStore.writeKey(
|
||||||
|
key as keyof T,
|
||||||
|
value,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✅ OverwriteObject complete: ${overwriteKeys.length} key(s) overwritten`);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.readyDeferred.resolve();
|
this.readyDeferred.resolve();
|
||||||
|
console.log('✨ AppData initialization complete!');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -137,8 +411,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');
|
||||||
@@ -146,7 +420,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,3 +1,7 @@
|
|||||||
|
import * as tsclass from '@tsclass/tsclass';
|
||||||
|
|
||||||
|
export { 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';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
@@ -8,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