Compare commits
104 Commits
Author | SHA1 | Date | |
---|---|---|---|
c74ded8d5d | |||
62e61168a0 | |||
5e0edecf18 | |||
70cefc00fa | |||
6f14c73b5f | |||
1e6f636608 | |||
eff77f8976 | |||
b5f109d320 | |||
3a53938e8e | |||
db90714a81 | |||
b81ab9d9b2 | |||
5ec9124d29 | |||
f7f035e878 | |||
3caf300544 | |||
6a70af9b6f | |||
2ad3da85a9 | |||
09e8b8b94c | |||
265f4df8b3 | |||
5461ec0636 | |||
937252f99e | |||
6f19c3cc71 | |||
4be54f6dcc | |||
5c6702b898 | |||
f1ecda411a | |||
267d2c392d | |||
2ca82fb28b | |||
b342602ca0 | |||
bb72e1c2c0 | |||
40ad00e75a | |||
de1db395cd | |||
83968d578c | |||
155cd8bd6a | |||
5eebd0fd02 | |||
686aa0d2e7 | |||
f97d72da28 | |||
78d1540ce0 | |||
916dd116c1 | |||
5fd57cb56b | |||
1a106e322c | |||
36ded40032 | |||
44f5f84195 | |||
72655a6a0e | |||
961df11121 | |||
ee6f5e63ee | |||
a40b6adeef | |||
6ee324a0ef | |||
215a48a409 | |||
aeaa6149e4 | |||
75059dfca8 | |||
90c0f30ce1 | |||
9a55303978 | |||
224125fb8e | |||
df9d197508 | |||
ae33716af4 | |||
31b64eda5e | |||
7f3437e3cd | |||
8903bee78d | |||
a02b45a673 | |||
e7883f5997 | |||
39ab01b4af | |||
bb9de1b13b | |||
080e133e9f | |||
a284c58a68 | |||
18bb54831d | |||
141c7ed8a7 | |||
037481f195 | |||
e414e392d3 | |||
4f7e382bc9 | |||
1c0b428606 | |||
90e8625771 | |||
abbce0d4a1 | |||
93c65acc95 | |||
a68d59a4d2 | |||
0133dca698 | |||
6174490e8e | |||
d952a761b2 | |||
05909f776e | |||
779883fbab | |||
349f074bb9 | |||
d209661586 | |||
3ce6036478 | |||
54bf310403 | |||
2ec485048b | |||
caca370e77 | |||
45deb1a8d2 | |||
4c9d3c7148 | |||
320c627d4f | |||
d834e0a220 | |||
86427ac05d | |||
dc59682c15 | |||
45ebf0944c | |||
677aa4f0ea | |||
7f0985f24d | |||
5f2f7e2b39 | |||
26a6ac9651 | |||
072ee31c3f | |||
8fa3fd8ac1 | |||
102422c9c7 | |||
2e95824ff4 | |||
c85c0d3035 | |||
68da8c7841 | |||
a12418ba9c | |||
1664eee3fe | |||
6112f430c9 |
66
.gitea/workflows/default_nottags.yaml
Normal file
66
.gitea/workflows/default_nottags.yaml
Normal file
@@ -0,0 +1,66 @@
|
||||
name: Default (not tags)
|
||||
|
||||
on:
|
||||
push:
|
||||
tags-ignore:
|
||||
- '**'
|
||||
|
||||
env:
|
||||
IMAGE: code.foss.global/host.today/ht-docker-node:npmci
|
||||
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@/${{gitea.repository}}.git
|
||||
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
||||
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
||||
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
|
||||
NPMCI_URL_CLOUDLY: ${{secrets.NPMCI_URL_CLOUDLY}}
|
||||
|
||||
jobs:
|
||||
security:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
container:
|
||||
image: ${{ env.IMAGE }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Install pnpm and npmci
|
||||
run: |
|
||||
pnpm install -g pnpm
|
||||
pnpm install -g @ship.zone/npmci
|
||||
|
||||
- name: Run npm prepare
|
||||
run: npmci npm prepare
|
||||
|
||||
- name: Audit production dependencies
|
||||
run: |
|
||||
npmci command npm config set registry https://registry.npmjs.org
|
||||
npmci command pnpm audit --audit-level=high --prod
|
||||
continue-on-error: true
|
||||
|
||||
- name: Audit development dependencies
|
||||
run: |
|
||||
npmci command npm config set registry https://registry.npmjs.org
|
||||
npmci command pnpm audit --audit-level=high --dev
|
||||
continue-on-error: true
|
||||
|
||||
test:
|
||||
if: ${{ always() }}
|
||||
needs: security
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ${{ env.IMAGE }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Test stable
|
||||
run: |
|
||||
npmci node install stable
|
||||
npmci npm install
|
||||
npmci npm test
|
||||
|
||||
- name: Test build
|
||||
run: |
|
||||
npmci node install stable
|
||||
npmci npm install
|
||||
npmci npm build
|
124
.gitea/workflows/default_tags.yaml
Normal file
124
.gitea/workflows/default_tags.yaml
Normal file
@@ -0,0 +1,124 @@
|
||||
name: Default (tags)
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
env:
|
||||
IMAGE: code.foss.global/host.today/ht-docker-node:npmci
|
||||
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@/${{gitea.repository}}.git
|
||||
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
||||
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
||||
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
|
||||
NPMCI_URL_CLOUDLY: ${{secrets.NPMCI_URL_CLOUDLY}}
|
||||
|
||||
jobs:
|
||||
security:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
container:
|
||||
image: ${{ env.IMAGE }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
pnpm install -g pnpm
|
||||
pnpm install -g @ship.zone/npmci
|
||||
npmci npm prepare
|
||||
|
||||
- name: Audit production dependencies
|
||||
run: |
|
||||
npmci command npm config set registry https://registry.npmjs.org
|
||||
npmci command pnpm audit --audit-level=high --prod
|
||||
continue-on-error: true
|
||||
|
||||
- name: Audit development dependencies
|
||||
run: |
|
||||
npmci command npm config set registry https://registry.npmjs.org
|
||||
npmci command pnpm audit --audit-level=high --dev
|
||||
continue-on-error: true
|
||||
|
||||
test:
|
||||
if: ${{ always() }}
|
||||
needs: security
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ${{ env.IMAGE }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
pnpm install -g pnpm
|
||||
pnpm install -g @ship.zone/npmci
|
||||
npmci npm prepare
|
||||
|
||||
- name: Test stable
|
||||
run: |
|
||||
npmci node install stable
|
||||
npmci npm install
|
||||
npmci npm test
|
||||
|
||||
- name: Test build
|
||||
run: |
|
||||
npmci node install stable
|
||||
npmci npm install
|
||||
npmci npm build
|
||||
|
||||
release:
|
||||
needs: test
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ${{ env.IMAGE }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
pnpm install -g pnpm
|
||||
pnpm install -g @ship.zone/npmci
|
||||
npmci npm prepare
|
||||
|
||||
- name: Release
|
||||
run: |
|
||||
npmci node install stable
|
||||
npmci npm publish
|
||||
|
||||
metadata:
|
||||
needs: test
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ${{ env.IMAGE }}
|
||||
continue-on-error: true
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
pnpm install -g pnpm
|
||||
pnpm install -g @ship.zone/npmci
|
||||
npmci npm prepare
|
||||
|
||||
- name: Code quality
|
||||
run: |
|
||||
npmci command npm install -g typescript
|
||||
npmci npm install
|
||||
|
||||
- name: Trigger
|
||||
run: npmci trigger
|
||||
|
||||
- name: Build docs and upload artifacts
|
||||
run: |
|
||||
npmci node install stable
|
||||
npmci npm install
|
||||
pnpm install -g @git.zone/tsdoc
|
||||
npmci command tsdoc
|
||||
continue-on-error: true
|
3
.gitignore
vendored
3
.gitignore
vendored
@@ -3,7 +3,6 @@
|
||||
# artifacts
|
||||
coverage/
|
||||
public/
|
||||
pages/
|
||||
|
||||
# installs
|
||||
node_modules/
|
||||
@@ -17,4 +16,4 @@ node_modules/
|
||||
dist/
|
||||
dist_*/
|
||||
|
||||
# custom
|
||||
#------# custom
|
137
.gitlab-ci.yml
137
.gitlab-ci.yml
@@ -1,137 +0,0 @@
|
||||
# gitzone ci_default
|
||||
image: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
||||
|
||||
cache:
|
||||
paths:
|
||||
- .npmci_cache/
|
||||
key: '$CI_BUILD_STAGE'
|
||||
|
||||
stages:
|
||||
- security
|
||||
- test
|
||||
- release
|
||||
- metadata
|
||||
|
||||
# ====================
|
||||
# security stage
|
||||
# ====================
|
||||
mirror:
|
||||
stage: security
|
||||
script:
|
||||
- npmci git mirror
|
||||
only:
|
||||
- tags
|
||||
tags:
|
||||
- lossless
|
||||
- docker
|
||||
- notpriv
|
||||
|
||||
auditProductionDependencies:
|
||||
image: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
||||
stage: security
|
||||
script:
|
||||
- npmci npm prepare
|
||||
- npmci command npm install --production --ignore-scripts
|
||||
- npmci command npm config set registry https://registry.npmjs.org
|
||||
- npmci command npm audit --audit-level=high --only=prod --production
|
||||
tags:
|
||||
- docker
|
||||
|
||||
auditDevDependencies:
|
||||
image: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
||||
stage: security
|
||||
script:
|
||||
- npmci npm prepare
|
||||
- npmci command npm install --ignore-scripts
|
||||
- npmci command npm config set registry https://registry.npmjs.org
|
||||
- npmci command npm audit --audit-level=high --only=dev
|
||||
tags:
|
||||
- docker
|
||||
allow_failure: true
|
||||
|
||||
# ====================
|
||||
# test stage
|
||||
# ====================
|
||||
|
||||
testStable:
|
||||
stage: test
|
||||
script:
|
||||
- npmci npm prepare
|
||||
- npmci node install stable
|
||||
- npmci npm install
|
||||
- npmci npm test
|
||||
coverage: /\d+.?\d+?\%\s*coverage/
|
||||
tags:
|
||||
- docker
|
||||
|
||||
testBuild:
|
||||
stage: test
|
||||
script:
|
||||
- npmci npm prepare
|
||||
- npmci node install stable
|
||||
- npmci npm install
|
||||
- npmci command npm run build
|
||||
coverage: /\d+.?\d+?\%\s*coverage/
|
||||
tags:
|
||||
- docker
|
||||
|
||||
release:
|
||||
stage: release
|
||||
script:
|
||||
- npmci node install stable
|
||||
- npmci npm publish
|
||||
only:
|
||||
- tags
|
||||
tags:
|
||||
- lossless
|
||||
- docker
|
||||
- notpriv
|
||||
|
||||
# ====================
|
||||
# metadata stage
|
||||
# ====================
|
||||
codequality:
|
||||
stage: metadata
|
||||
allow_failure: true
|
||||
only:
|
||||
- tags
|
||||
script:
|
||||
- npmci command npm install -g tslint typescript
|
||||
- npmci npm prepare
|
||||
- npmci npm install
|
||||
- npmci command "tslint -c tslint.json ./ts/**/*.ts"
|
||||
tags:
|
||||
- lossless
|
||||
- docker
|
||||
- priv
|
||||
|
||||
trigger:
|
||||
stage: metadata
|
||||
script:
|
||||
- npmci trigger
|
||||
only:
|
||||
- tags
|
||||
tags:
|
||||
- lossless
|
||||
- docker
|
||||
- notpriv
|
||||
|
||||
pages:
|
||||
stage: metadata
|
||||
script:
|
||||
- npmci node install lts
|
||||
- npmci command npm install -g @gitzone/tsdoc
|
||||
- npmci npm prepare
|
||||
- npmci npm install
|
||||
- npmci command tsdoc
|
||||
tags:
|
||||
- lossless
|
||||
- docker
|
||||
- notpriv
|
||||
only:
|
||||
- tags
|
||||
artifacts:
|
||||
expire_in: 1 week
|
||||
paths:
|
||||
- public
|
||||
allow_failure: true
|
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"
|
4
.snyk
4
.snyk
@@ -1,4 +0,0 @@
|
||||
# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.
|
||||
version: v1.12.0
|
||||
ignore: {}
|
||||
patch: {}
|
24
.vscode/launch.json
vendored
24
.vscode/launch.json
vendored
@@ -2,28 +2,10 @@
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "current file",
|
||||
"type": "node",
|
||||
"command": "npm test",
|
||||
"name": "Run npm test",
|
||||
"request": "launch",
|
||||
"args": [
|
||||
"${relativeFile}"
|
||||
],
|
||||
"runtimeArgs": ["-r", "@gitzone/tsrun"],
|
||||
"cwd": "${workspaceRoot}",
|
||||
"protocol": "inspector",
|
||||
"internalConsoleOptions": "openOnSessionStart"
|
||||
},
|
||||
{
|
||||
"name": "test.ts",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"args": [
|
||||
"test/test.ts"
|
||||
],
|
||||
"runtimeArgs": ["-r", "@gitzone/tsrun"],
|
||||
"cwd": "${workspaceRoot}",
|
||||
"protocol": "inspector",
|
||||
"internalConsoleOptions": "openOnSessionStart"
|
||||
"type": "node-terminal"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
76
changelog.md
Normal file
76
changelog.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-08-15 - 5.1.3 - fix(appdata)
|
||||
Fix iteration over overwriteObject in AppData and update configuration for dependency and path handling
|
||||
|
||||
- Replaced incorrect looping constructs in the AppData class to properly iterate over overwriteObject keys
|
||||
- Improved environment variable mapping and file path resolution in multiple TS modules
|
||||
- Updated dependency versions and adjusted git workflow configurations
|
||||
- Enhanced project configuration including TS config and build script adjustments
|
||||
|
||||
## 2024-11-06 - 5.1.2 - fix(appdata)
|
||||
|
||||
Fix iteration over overwriteObject in AppData class
|
||||
|
||||
- Corrected the for loop from in to of inside the AppData class for iterating over overwriteObject keys.
|
||||
|
||||
## 2024-11-05 - 5.1.1 - fix(AppData)
|
||||
|
||||
Fix issue with overwrite object handling in AppData class
|
||||
|
||||
- Corrected logic to handle cases when overwriteObject is undefined.
|
||||
|
||||
## 2024-11-05 - 5.1.0 - feat(appdata)
|
||||
|
||||
Add support for overwriting keys using the overwriteObject option in AppData
|
||||
|
||||
- Introduced the overwriteObject option in IAppDataOptions to allow overwriting specific keys in the AppData class.
|
||||
|
||||
## 2024-06-19 - 5.0.17 - 5.0.23 - Core Updates
|
||||
|
||||
Routine maintenance and updates to the core components.
|
||||
|
||||
- Multiple core updates and fixes improving stability
|
||||
|
||||
## 2024-06-12 - 5.0.13 - 5.0.16 - Core Updates
|
||||
|
||||
Maintenance focus on core systems with enhancements and problem resolutions.
|
||||
|
||||
- Enhancements and updates in the core functionality
|
||||
|
||||
## 2024-05-29 - 5.0.13 - Documentation Update
|
||||
|
||||
Descriptive improvements aligned with current features.
|
||||
|
||||
- Updated core description for better clarity in documentation
|
||||
|
||||
## 2024-04-01 - 5.0.10 - Configuration Update
|
||||
|
||||
Improved configuration management for build processes.
|
||||
|
||||
- Updated `npmextra.json` to reflect changes in git repository management
|
||||
|
||||
## 2024-02-12 - 5.0.0 - 5.0.9 - Major Core Enhancements
|
||||
|
||||
A series of critical updates with resolved issues in the core components.
|
||||
|
||||
- Introduction of new core features
|
||||
- Several core system updates
|
||||
|
||||
## 2024-02-12 - 4.0.16 - Major Version Transition
|
||||
|
||||
Migration to the new major version with impactful changes.
|
||||
|
||||
- BREAKING CHANGE: Significant updates requiring attention for smooth transition
|
||||
|
||||
## 2023-08-24 - 3.0.9 - 4.0.16 - Organization Updates
|
||||
|
||||
Formatted updates with attention to organizational standards and practice.
|
||||
|
||||
- SWITCH to a new organizational scheme
|
||||
|
||||
## 2023-07-11 - 3.0.9 - Organizational Enhancement
|
||||
|
||||
Shifts aligning with contemporary structuring and logistics.
|
||||
|
||||
- Strategic realignment with organizational principles
|
@@ -11,12 +11,32 @@
|
||||
"gitzone": {
|
||||
"projectType": "npm",
|
||||
"module": {
|
||||
"githost": "gitlab.com",
|
||||
"gitscope": "pushrocks",
|
||||
"githost": "code.foss.global",
|
||||
"gitscope": "push.rocks",
|
||||
"gitrepo": "npmextra",
|
||||
"shortDescription": "do more with npm",
|
||||
"npmPackagename": "@pushrocks/npmextra",
|
||||
"license": "MIT"
|
||||
"description": "A utility to enhance npm with additional configuration, tool management capabilities, and a key-value store for project setups.",
|
||||
"npmPackagename": "@push.rocks/npmextra",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"npm",
|
||||
"configuration management",
|
||||
"tool management",
|
||||
"key-value store",
|
||||
"project setup",
|
||||
"typescript",
|
||||
"environment setup",
|
||||
"dependencies management",
|
||||
"npm package enhancement",
|
||||
"automation",
|
||||
"async operations",
|
||||
"app configuration",
|
||||
"smart file handling",
|
||||
"workflow improvement",
|
||||
"persistent storage"
|
||||
]
|
||||
}
|
||||
},
|
||||
"tsdoc": {
|
||||
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
11075
package-lock.json
generated
11075
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
66
package.json
66
package.json
@@ -1,39 +1,41 @@
|
||||
{
|
||||
"name": "@pushrocks/npmextra",
|
||||
"version": "3.0.9",
|
||||
"name": "@push.rocks/npmextra",
|
||||
"version": "5.1.3",
|
||||
"private": false,
|
||||
"description": "do more with npm",
|
||||
"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",
|
||||
"typings": "dist_ts/index.d.ts",
|
||||
"scripts": {
|
||||
"test": "(tstest test/ --web)",
|
||||
"build": "(tsbuild --web)"
|
||||
"test": "(tstest test/ --verbose --logfile --timeout 60)",
|
||||
"build": "(tsbuild --web --allowimplicitany)",
|
||||
"buildDocs": "tsdoc"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+ssh://git@gitlab.com/pushrocks/npmextra.git"
|
||||
"url": "https://code.foss.global/push.rocks/npmextra.git"
|
||||
},
|
||||
"author": "Lossless GmbH",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://gitlab.com/pushrocks/npmextra/issues"
|
||||
"url": "https://code.foss.global/push.rocks/npmextra/issues"
|
||||
},
|
||||
"homepage": "https://gitlab.com/pushrocks/npmextra#README",
|
||||
"homepage": "https://code.foss.global/push.rocks/npmextra#readme",
|
||||
"dependencies": {
|
||||
"@pushrocks/smartfile": "^8.0.8",
|
||||
"@pushrocks/smartlog": "^2.0.39",
|
||||
"@pushrocks/smartpath": "^4.0.3",
|
||||
"@pushrocks/smartpromise": "^3.1.3",
|
||||
"@pushrocks/taskbuffer": "^2.1.13"
|
||||
"@push.rocks/qenv": "^6.1.2",
|
||||
"@push.rocks/smartfile": "^11.2.5",
|
||||
"@push.rocks/smartjson": "^5.0.20",
|
||||
"@push.rocks/smartlog": "^3.1.8",
|
||||
"@push.rocks/smartpath": "^6.0.0",
|
||||
"@push.rocks/smartpromise": "^4.2.3",
|
||||
"@push.rocks/smartrx": "^3.0.10",
|
||||
"@push.rocks/taskbuffer": "^3.1.7",
|
||||
"@tsclass/tsclass": "^9.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@gitzone/tsbuild": "^2.1.25",
|
||||
"@gitzone/tsrun": "^1.2.12",
|
||||
"@gitzone/tstest": "^1.0.52",
|
||||
"@pushrocks/tapbundle": "^3.2.10",
|
||||
"@types/node": "^14.14.22",
|
||||
"tslint": "^6.1.3",
|
||||
"tslint-config-prettier": "^1.18.0"
|
||||
"@git.zone/tsbuild": "^2.6.4",
|
||||
"@git.zone/tsrun": "^1.3.3",
|
||||
"@git.zone/tstest": "^2.3.2",
|
||||
"@types/node": "^20.14.5"
|
||||
},
|
||||
"files": [
|
||||
"ts/**/*",
|
||||
@@ -49,5 +51,27 @@
|
||||
],
|
||||
"browserslist": [
|
||||
"last 1 chrome versions"
|
||||
]
|
||||
],
|
||||
"type": "module",
|
||||
"keywords": [
|
||||
"npm",
|
||||
"configuration management",
|
||||
"tool management",
|
||||
"key-value store",
|
||||
"project setup",
|
||||
"typescript",
|
||||
"environment setup",
|
||||
"dependencies management",
|
||||
"npm package enhancement",
|
||||
"automation",
|
||||
"async operations",
|
||||
"app configuration",
|
||||
"smart file handling",
|
||||
"workflow improvement",
|
||||
"persistent storage"
|
||||
],
|
||||
"packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977",
|
||||
"pnpm": {
|
||||
"overrides": {}
|
||||
}
|
||||
}
|
||||
|
9340
pnpm-lock.yaml
generated
Normal file
9340
pnpm-lock.yaml
generated
Normal file
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
|
0
readme.hints.md
Normal file
0
readme.hints.md
Normal file
498
readme.md
498
readme.md
@@ -1,73 +1,479 @@
|
||||
# @pushrocks/npmextra
|
||||
do more with npm
|
||||
# @push.rocks/npmextra 🚀
|
||||
|
||||
## Availabililty and Links
|
||||
* [npmjs.org (npm package)](https://www.npmjs.com/package/@pushrocks/npmextra)
|
||||
* [gitlab.com (source)](https://gitlab.com/pushrocks/npmextra)
|
||||
* [github.com (source mirror)](https://github.com/pushrocks/npmextra)
|
||||
* [docs (typedoc)](https://pushrocks.gitlab.io/npmextra/)
|
||||
**Supercharge your npm projects with powerful configuration management, tool orchestration, and persistent key-value storage.**
|
||||
|
||||
## Status for master
|
||||
## Install 📦
|
||||
|
||||
Status Category | Status Badge
|
||||
-- | --
|
||||
GitLab Pipelines | [](https://lossless.cloud)
|
||||
GitLab Pipline Test Coverage | [](https://lossless.cloud)
|
||||
npm | [](https://lossless.cloud)
|
||||
Snyk | [](https://lossless.cloud)
|
||||
TypeScript Support | [](https://lossless.cloud)
|
||||
node Support | [](https://nodejs.org/dist/latest-v10.x/docs/api/)
|
||||
Code Style | [](https://lossless.cloud)
|
||||
PackagePhobia (total standalone install weight) | [](https://lossless.cloud)
|
||||
PackagePhobia (package size on registry) | [](https://lossless.cloud)
|
||||
BundlePhobia (total size when bundled) | [](https://lossless.cloud)
|
||||
Platform support | [](https://lossless.cloud) [](https://lossless.cloud)
|
||||
```bash
|
||||
# Using npm
|
||||
npm install @push.rocks/npmextra --save
|
||||
|
||||
## Usage
|
||||
# Using pnpm (recommended)
|
||||
pnpm add @push.rocks/npmextra
|
||||
```
|
||||
|
||||
Use TypeScript for best in class instellisense.
|
||||
## Overview 🎯
|
||||
|
||||
## Using npmextra for managing toolconfigs
|
||||
`@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.
|
||||
|
||||
The main purpose of npmextra is to configure different tools using one npmextra.json. This way complex setups avoid additional complexity caused by too many configuration files.
|
||||
### Why npmextra?
|
||||
|
||||
npmextra.json
|
||||
- **🎛️ 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
|
||||
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: {}
|
||||
});
|
||||
```
|
||||
|
||||
**npmextra.json example:**
|
||||
```json
|
||||
{
|
||||
"sometool": {
|
||||
"defaultKey1": "awesomeValueFromConfig"
|
||||
"eslint": {
|
||||
"extends": "@company/eslint-config",
|
||||
"rules": {
|
||||
"no-console": "warn"
|
||||
}
|
||||
},
|
||||
"prettier": {
|
||||
"semi": false,
|
||||
"singleQuote": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
import { Npmextra } from 'npmextra';
|
||||
### 2. KeyValueStore - Persistent Data Storage
|
||||
|
||||
let myNpmExtra = new Npmextra('my/path/to/cwd'); // cwd argument is optional
|
||||
mergedData = myNpmExtra.dataFor('sometool', {
|
||||
// gets merged with whatever is in the configfile
|
||||
defaultKey1: 'defaultValue1', // so this will get overwritten with "awesomeValueFromConfig"
|
||||
defaultKey2: 'defaultValue2', // this one will pass through unaltered
|
||||
A flexible key-value store that persists data between script executions:
|
||||
|
||||
```typescript
|
||||
import { KeyValueStore } from '@push.rocks/npmextra';
|
||||
|
||||
interface UserSettings {
|
||||
username: string;
|
||||
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']
|
||||
});
|
||||
|
||||
// Write operations
|
||||
await kvStore.writeKey('username', 'john_doe');
|
||||
await kvStore.writeKey('preferences', {
|
||||
theme: 'dark',
|
||||
notifications: true
|
||||
});
|
||||
|
||||
// Read operations
|
||||
const username = await kvStore.readKey('username');
|
||||
const allData = await kvStore.readAll();
|
||||
|
||||
// Check for missing mandatory keys
|
||||
const missingKeys = await kvStore.getMissingMandatoryKeys();
|
||||
if (missingKeys.length > 0) {
|
||||
console.log('Missing required configuration:', missingKeys);
|
||||
}
|
||||
|
||||
// Wait for keys to be present
|
||||
await kvStore.waitForKeysPresent(['apiKey']);
|
||||
```
|
||||
|
||||
**Storage Types:**
|
||||
- `userHomeDir`: Store in user's home directory
|
||||
- `custom`: Specify your own path
|
||||
- `ephemeral`: In-memory only (perfect for testing)
|
||||
|
||||
### 3. AppData - Advanced Environment Management 🌟
|
||||
|
||||
The crown jewel of npmextra - sophisticated environment variable mapping with automatic type conversion:
|
||||
|
||||
```typescript
|
||||
import { AppData } from '@push.rocks/npmextra';
|
||||
|
||||
interface AppConfig {
|
||||
apiUrl: string;
|
||||
apiKey: string;
|
||||
port: number;
|
||||
features: {
|
||||
analytics: boolean;
|
||||
payment: boolean;
|
||||
};
|
||||
cache: {
|
||||
ttl: number;
|
||||
redis: {
|
||||
host: string;
|
||||
password: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const appData = await AppData.createAndInit<AppConfig>({
|
||||
dirPath: '/app/config', // Optional: defaults to smart path selection
|
||||
requiredKeys: ['apiKey', 'apiUrl'],
|
||||
envMapping: {
|
||||
apiUrl: 'API_URL', // Simple mapping
|
||||
apiKey: 'hard:development-key-123', // Hardcoded value
|
||||
port: 'hard:3000', // Hardcoded number
|
||||
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
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Tools that already use the config feature of npmextra
|
||||
### Boolean Conversion Rules
|
||||
|
||||
- [npmts](https://www.npmjs.com/package/npmts)
|
||||
- [npmci](https://www.npmjs.com/package/npmci)
|
||||
- [npmdocker](https://www.npmjs.com/package/npmdocker)
|
||||
AppData intelligently handles boolean conversions:
|
||||
|
||||
## Using the 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
|
||||
// Environment: FEATURE_A="true", FEATURE_B="yes", FEATURE_C="1"
|
||||
{
|
||||
envMapping: {
|
||||
featureA: 'FEATURE_A', // Result: "true" (string)
|
||||
featureB: 'boolean:FEATURE_B', // Result: false (only "true" → true)
|
||||
featureC: 'boolean:FEATURE_C', // Result: false (only "true" → true)
|
||||
featureD: 'hard_boolean:true' // Result: true (hardcoded)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Contribution
|
||||
## Advanced Patterns 🎨
|
||||
|
||||
We are always happy for code contributions. If you are not the code contributing type that is ok. Still, maintaining Open Source repositories takes considerable time and thought. If you like the quality of what we do and our modules are useful to you we would appreciate a little monthly contribution: You can [contribute one time](https://lossless.link/contribute-onetime) or [contribute monthly](https://lossless.link/contribute). :)
|
||||
### Reactive Configuration
|
||||
|
||||
For further information read the linked docs at the top of this readme.
|
||||
Subscribe to configuration changes:
|
||||
|
||||
> MIT licensed | **©** [Lossless GmbH](https://lossless.gmbh)
|
||||
| By using this npm module you agree to our [privacy policy](https://lossless.gmbH/privacy)
|
||||
```typescript
|
||||
const kvStore = new KeyValueStore<Config>({
|
||||
typeArg: 'custom',
|
||||
identityArg: 'myApp'
|
||||
});
|
||||
|
||||
[](https://maintainedby.lossless.com)
|
||||
// Subscribe to changes
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
||||
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();
|
||||
```
|
||||
|
||||
## API Reference 📚
|
||||
|
||||
### Npmextra Class
|
||||
|
||||
```typescript
|
||||
new Npmextra(cwdArg?: string)
|
||||
```
|
||||
- `cwdArg`: Optional working directory path
|
||||
|
||||
**Methods:**
|
||||
- `dataFor<T>(toolName: string, defaultOptions: T): T` - Get merged configuration
|
||||
|
||||
### KeyValueStore Class
|
||||
|
||||
```typescript
|
||||
new KeyValueStore<T>(options: {
|
||||
typeArg: 'custom' | 'userHomeDir' | 'ephemeral';
|
||||
identityArg: string;
|
||||
customPath?: string;
|
||||
mandatoryKeys?: Array<keyof T>;
|
||||
})
|
||||
```
|
||||
|
||||
**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
|
||||
|
||||
### 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
|
||||
|
||||
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository.
|
||||
|
||||
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
||||
|
||||
### Trademarks
|
||||
|
||||
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.
|
||||
|
||||
### Company Information
|
||||
|
||||
Task Venture Capital GmbH
|
||||
Registered at District court Bremen HRB 35230 HB, Germany
|
||||
|
||||
For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
|
||||
|
||||
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"testTool":{
|
||||
"testValue":2
|
||||
}
|
||||
}
|
||||
"testTool": {
|
||||
"testValue": 2
|
||||
}
|
||||
}
|
||||
|
3
test/somekv.json
Normal file
3
test/somekv.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"myKey": "myValue"
|
||||
}
|
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,25 +1,32 @@
|
||||
import { expect, tap } from '@pushrocks/tapbundle';
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
|
||||
import * as npmextra from '../ts/index';
|
||||
import * as npmextra from '../ts/index.js';
|
||||
|
||||
let myKeyValueStore: npmextra.KeyValueStore;
|
||||
let myKeyValueStore: npmextra.KeyValueStore<any>;
|
||||
|
||||
tap.test('should create a keyValueStore', async () => {
|
||||
myKeyValueStore = new npmextra.KeyValueStore('custom', 'test');
|
||||
expect(myKeyValueStore).to.be.instanceof(npmextra.KeyValueStore);
|
||||
myKeyValueStore = new npmextra.KeyValueStore<any>({
|
||||
typeArg: 'custom',
|
||||
identityArg: 'test',
|
||||
customPath: 'test/somekv.json',
|
||||
});
|
||||
expect(myKeyValueStore).toBeInstanceOf(npmextra.KeyValueStore);
|
||||
});
|
||||
|
||||
tap.test('should reset the keyValueStore', async () => {
|
||||
await myKeyValueStore.reset();
|
||||
});
|
||||
|
||||
tap.test('expect result to be empty', async () => {
|
||||
let result = myKeyValueStore.readAll();
|
||||
// tslint:disable-next-line:no-unused-expression
|
||||
expect(result).to.be.empty;
|
||||
let result = await myKeyValueStore.readAll();
|
||||
expect(JSON.stringify(result)).toEqual('{}');
|
||||
});
|
||||
|
||||
tap.test('expect to add an object to the kv Store', async () => {
|
||||
await myKeyValueStore.writeAll({
|
||||
myKey: 'myValue',
|
||||
});
|
||||
await expect(myKeyValueStore.readKey('myKey')).to.eventually.equal('myValue');
|
||||
await expect(await myKeyValueStore.readKey('myKey')).toEqual('myValue');
|
||||
});
|
||||
|
||||
tap.start();
|
||||
export default tap.start();
|
||||
|
30
test/test.ts
30
test/test.ts
@@ -1,33 +1,35 @@
|
||||
import { expect, tap } from '@pushrocks/tapbundle';
|
||||
import path = require('path');
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
|
||||
// module to test
|
||||
import npmExtra = require('../ts/index');
|
||||
import * as npmextra from '../ts/index.js';
|
||||
|
||||
let testNpmextra: npmExtra.Npmextra;
|
||||
let testNpmextra: npmextra.Npmextra;
|
||||
|
||||
tap.test('should create a new Npmtextra instance', async () => {
|
||||
testNpmextra = new npmExtra.Npmextra('./test/');
|
||||
expect(testNpmextra).be.instanceof(npmExtra.Npmextra);
|
||||
testNpmextra = new npmextra.Npmextra('./test/');
|
||||
expect(testNpmextra).toBeInstanceOf(npmextra.Npmextra);
|
||||
});
|
||||
|
||||
tap.test('should state wether a npmextra.json exists', async () => {
|
||||
// tslint:disable-next-line:no-unused-expression
|
||||
expect(testNpmextra.npmextraJsonExists).be.true;
|
||||
expect(testNpmextra.npmextraJsonExists).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('should pass through default value, if not overriden by config from file', async () => {
|
||||
let testData = testNpmextra.dataFor('testTool', { someKey2: 'someValue2' });
|
||||
console.log(testData);
|
||||
expect(testData).have.ownProperty('someKey2');
|
||||
});
|
||||
tap.test(
|
||||
'should pass through default value, if not overriden by config from file',
|
||||
async () => {
|
||||
let testData = testNpmextra.dataFor('testTool', { someKey2: 'someValue2' });
|
||||
console.log(testData);
|
||||
expect(testData).toHaveProperty('someKey2');
|
||||
},
|
||||
);
|
||||
|
||||
tap.test('should read a config file', async () => {
|
||||
let testData = testNpmextra.dataFor<any>('testTool', {
|
||||
someKey2: 'someValue2',
|
||||
});
|
||||
expect(testData).have.ownProperty('someKey2');
|
||||
expect(testData.testValue).equal(2);
|
||||
expect(testData).toHaveProperty('someKey2');
|
||||
expect(testData.testValue).toEqual(2);
|
||||
});
|
||||
|
||||
tap.start();
|
||||
|
8
ts/00_commitinfo_data.ts
Normal file
8
ts/00_commitinfo_data.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* autocreated commitinfo by @push.rocks/commitinfo
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/npmextra',
|
||||
version: '5.1.3',
|
||||
description: 'A utility to enhance npm with additional configuration, tool management capabilities, and a key-value store for project setups.'
|
||||
}
|
@@ -1,4 +1,3 @@
|
||||
import * as plugins from './npmextra.plugins';
|
||||
|
||||
export * from './npmextra.classes.npmextra';
|
||||
export * from './npmextra.classes.keyvaluestore';
|
||||
export * from './npmextra.classes.appdata.js';
|
||||
export * from './npmextra.classes.keyvaluestore.js';
|
||||
export * from './npmextra.classes.npmextra.js';
|
||||
|
234
ts/npmextra.classes.appdata.ts
Normal file
234
ts/npmextra.classes.appdata.ts
Normal file
@@ -0,0 +1,234 @@
|
||||
import * as plugins from './npmextra.plugins.js';
|
||||
import * as paths from './npmextra.paths.js';
|
||||
import { KeyValueStore } from './npmextra.classes.keyvaluestore.js';
|
||||
|
||||
export interface IAppDataOptions<T = any> {
|
||||
dirPath?: string;
|
||||
requiredKeys?: Array<keyof T>;
|
||||
|
||||
/**
|
||||
* wether keys should be persisted on disk or not
|
||||
*/
|
||||
ephermal?: boolean;
|
||||
|
||||
/**
|
||||
* kvStoreKey: 'MY_ENV_VAR'
|
||||
*/
|
||||
envMapping?: plugins.tsclass.typeFest.PartialDeep<T>;
|
||||
overwriteObject?: plugins.tsclass.typeFest.PartialDeep<T>;
|
||||
}
|
||||
|
||||
export class AppData<T = any> {
|
||||
/**
|
||||
* creates appdata. If no pathArg is given, data will be stored here:
|
||||
* ${PWD}/.nogit/appdata
|
||||
* @param pathArg
|
||||
* @returns
|
||||
*/
|
||||
public static async createAndInit<T = any>(
|
||||
optionsArg: IAppDataOptions<T> = {},
|
||||
): Promise<AppData<T>> {
|
||||
const appData = new AppData<T>(optionsArg);
|
||||
await appData.readyDeferred.promise;
|
||||
return appData;
|
||||
}
|
||||
|
||||
// instance
|
||||
public readyDeferred = plugins.smartpromise.defer<void>();
|
||||
public options: IAppDataOptions<T>;
|
||||
private kvStore: KeyValueStore<T>;
|
||||
|
||||
constructor(optionsArg: IAppDataOptions<T> = {}) {
|
||||
this.options = optionsArg;
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* inits app data
|
||||
* @param pathArg
|
||||
*/
|
||||
private async init(pathArg?: string) {
|
||||
if (this.options.dirPath || this.options.ephermal) {
|
||||
// ok, nothing to do here;
|
||||
} else {
|
||||
const appDataDir = '/app/data';
|
||||
const dataDir = '/data';
|
||||
const nogitAppData = '.nogit/appdata';
|
||||
const appDataExists = plugins.smartfile.fs.isDirectory(appDataDir);
|
||||
const dataExists = plugins.smartfile.fs.isDirectory(dataDir);
|
||||
if (appDataExists) {
|
||||
this.options.dirPath = appDataDir;
|
||||
} else if (dataExists) {
|
||||
this.options.dirPath = dataDir;
|
||||
} else {
|
||||
await plugins.smartfile.fs.ensureDir(nogitAppData);
|
||||
this.options.dirPath = nogitAppData;
|
||||
}
|
||||
}
|
||||
|
||||
this.kvStore = new KeyValueStore<T>({
|
||||
typeArg: this.options.ephermal ? 'ephemeral' : 'custom',
|
||||
identityArg: 'appkv',
|
||||
customPath: this.options.dirPath,
|
||||
mandatoryKeys: this.options.requiredKeys as Array<keyof T>,
|
||||
});
|
||||
|
||||
if (this.options.envMapping) {
|
||||
const qenvInstance = new plugins.qenv.Qenv(
|
||||
process.cwd(),
|
||||
plugins.path.join(process.cwd(), '.nogit'),
|
||||
);
|
||||
|
||||
// Recursive function to handle nested objects, now includes key parameter
|
||||
const processEnvMapping = async (
|
||||
key: keyof T,
|
||||
mappingValue: any,
|
||||
parentKey: keyof T | '' = '',
|
||||
): Promise<any> => {
|
||||
if (typeof mappingValue === 'string') {
|
||||
let envValue: string | boolean | T[keyof T];
|
||||
let convert: 'none' | 'json' | 'base64' | 'boolean' = 'none';
|
||||
switch (true) {
|
||||
case mappingValue.startsWith('hard:'):
|
||||
envValue = mappingValue.replace('hard:', '') as T[keyof T];
|
||||
break;
|
||||
case mappingValue.startsWith('hard_boolean:'):
|
||||
envValue = mappingValue.replace('hard_boolean:', '') === 'true';
|
||||
convert = 'boolean';
|
||||
break;
|
||||
case mappingValue.startsWith('hard_json:'):
|
||||
envValue = JSON.parse(
|
||||
mappingValue.replace('hard_json:', ''),
|
||||
) as T[keyof T];
|
||||
convert = 'json';
|
||||
break;
|
||||
case mappingValue.startsWith('hard_base64:'):
|
||||
envValue = Buffer.from(
|
||||
mappingValue.replace('hard_base64:', ''),
|
||||
'base64',
|
||||
).toString() as T[keyof T];
|
||||
convert = 'base64';
|
||||
break;
|
||||
case mappingValue.startsWith('boolean:'):
|
||||
envValue = (await qenvInstance.getEnvVarOnDemand(
|
||||
mappingValue.replace('boolean:', ''),
|
||||
)) as T[keyof T];
|
||||
convert = 'boolean';
|
||||
break;
|
||||
case mappingValue.startsWith('json:'):
|
||||
envValue = (await qenvInstance.getEnvVarOnDemand(
|
||||
mappingValue.replace('json:', ''),
|
||||
)) as T[keyof T];
|
||||
convert = 'json';
|
||||
break;
|
||||
case mappingValue.startsWith('base64:'):
|
||||
envValue = (await qenvInstance.getEnvVarOnDemand(
|
||||
mappingValue.replace('base64:', ''),
|
||||
)) as T[keyof T];
|
||||
convert = 'base64';
|
||||
break;
|
||||
default:
|
||||
envValue = (await qenvInstance.getEnvVarOnDemand(
|
||||
mappingValue,
|
||||
)) as T[keyof T];
|
||||
break;
|
||||
}
|
||||
|
||||
// lets format the env value
|
||||
if (envValue) {
|
||||
if (typeof envValue === 'string' && convert === 'boolean') {
|
||||
envValue = envValue === 'true';
|
||||
}
|
||||
if (
|
||||
typeof envValue === 'string' &&
|
||||
(mappingValue.endsWith('_JSON') || convert === 'json')
|
||||
) {
|
||||
envValue = JSON.parse(envValue as string) as T[keyof T];
|
||||
}
|
||||
if (
|
||||
typeof envValue === 'string' &&
|
||||
(mappingValue.endsWith('_BASE64') || convert === 'base64')
|
||||
) {
|
||||
envValue = Buffer.from(envValue as string, 'base64').toString();
|
||||
}
|
||||
if (!parentKey) {
|
||||
await this.kvStore.writeKey(key, envValue as any);
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (const key in this.options.envMapping) {
|
||||
await processEnvMapping(key as keyof T, this.options.envMapping[key]);
|
||||
}
|
||||
|
||||
if (this.options.overwriteObject) {
|
||||
for (const key of Object.keys(this.options.overwriteObject)) {
|
||||
console.log(
|
||||
`-> heads up: overwriting key ${key} from options.overwriteObject`,
|
||||
);
|
||||
await this.kvStore.writeKey(
|
||||
key as keyof T,
|
||||
this.options.overwriteObject[key],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.readyDeferred.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* returns a kvstore that resides in appdata
|
||||
*/
|
||||
public async getKvStore(): Promise<KeyValueStore<T>> {
|
||||
await this.readyDeferred.promise;
|
||||
return this.kvStore;
|
||||
}
|
||||
|
||||
public async logMissingKeys(): Promise<Array<keyof T>> {
|
||||
const kvStore = await this.getKvStore();
|
||||
const missingMandatoryKeys = await kvStore.getMissingMandatoryKeys();
|
||||
if (missingMandatoryKeys.length > 0) {
|
||||
console.log(
|
||||
`The following mandatory keys are missing in the appdata:\n -> ${missingMandatoryKeys.join(
|
||||
',\n -> ',
|
||||
)}`,
|
||||
);
|
||||
} else {
|
||||
console.log('All mandatory keys are present in the appdata');
|
||||
}
|
||||
return missingMandatoryKeys;
|
||||
}
|
||||
|
||||
public async waitForAndGetKey<K extends keyof T>(
|
||||
keyArg: K,
|
||||
): Promise<T[K] | undefined> {
|
||||
await this.readyDeferred.promise;
|
||||
await this.kvStore.waitForKeysPresent([keyArg]);
|
||||
return this.kvStore.readKey(keyArg);
|
||||
}
|
||||
}
|
@@ -1,73 +1,125 @@
|
||||
import * as plugins from './npmextra.plugins';
|
||||
import * as paths from './npmextra.paths';
|
||||
import * as plugins from './npmextra.plugins.js';
|
||||
import * as paths from './npmextra.paths.js';
|
||||
|
||||
import { Task } from '@pushrocks/taskbuffer';
|
||||
import { Task } from '@push.rocks/taskbuffer';
|
||||
|
||||
export type TKeyValueStore = 'path' | 'gitProject' | 'custom';
|
||||
export type TKeyValueStore = 'custom' | 'userHomeDir' | 'ephemeral';
|
||||
|
||||
export interface IKvStoreConstructorOptions<T> {
|
||||
typeArg: TKeyValueStore;
|
||||
identityArg: string;
|
||||
customPath?: string;
|
||||
mandatoryKeys?: Array<keyof T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* kvStore is a simple key vlaue store to store data about projects between runs
|
||||
* kvStore is a simple key value store to store data about projects between runs
|
||||
*/
|
||||
export class KeyValueStore {
|
||||
private dataObject: any = {};
|
||||
private deletedObject: any = {};
|
||||
export class KeyValueStore<T = any> {
|
||||
private dataObject: Partial<T> = {};
|
||||
private deletedObject: Partial<T> = {};
|
||||
private mandatoryKeys: Set<keyof T> = new Set();
|
||||
public changeSubject = new plugins.smartrx.rxjs.Subject<Partial<T>>();
|
||||
|
||||
private storedStateString: string = '';
|
||||
public syncTask = new Task({
|
||||
name: 'syncTask',
|
||||
buffered: true,
|
||||
bufferMax: 1,
|
||||
execDelay: 0,
|
||||
taskFunction: async () => {
|
||||
this.dataObject = {
|
||||
...plugins.smartfile.fs.toObjectSync(this.filePath),
|
||||
...this.dataObject,
|
||||
};
|
||||
for (const key of Object.keys(this.deletedObject)) {
|
||||
delete this.dataObject[key];
|
||||
if (this.type !== 'ephemeral') {
|
||||
this.dataObject = {
|
||||
...plugins.smartfile.fs.toObjectSync(this.filePath),
|
||||
...this.dataObject,
|
||||
};
|
||||
for (const key of Object.keys(this.deletedObject) as Array<keyof T>) {
|
||||
delete this.dataObject[key];
|
||||
}
|
||||
this.deletedObject = {};
|
||||
await plugins.smartfile.memory.toFs(
|
||||
plugins.smartjson.stringifyPretty(this.dataObject),
|
||||
this.filePath,
|
||||
);
|
||||
}
|
||||
const newStateString = plugins.smartjson.stringify(this.dataObject);
|
||||
|
||||
// change detection
|
||||
if (newStateString !== this.storedStateString) {
|
||||
this.storedStateString = newStateString;
|
||||
this.changeSubject.next(this.dataObject);
|
||||
}
|
||||
this.deletedObject = {};
|
||||
await plugins.smartfile.memory.toFs(JSON.stringify(this.dataObject), this.filePath);
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* computes the identity
|
||||
* computes the identity and filePath
|
||||
*/
|
||||
private initFilePath = () => {
|
||||
// determine the right base directory
|
||||
if (this.type === 'ephemeral') {
|
||||
// No file path is needed for ephemeral type
|
||||
return;
|
||||
}
|
||||
if (this.customPath) {
|
||||
// Use custom path if provided
|
||||
const absolutePath = plugins.smartpath.transform.makeAbsolute(
|
||||
this.customPath,
|
||||
paths.cwd,
|
||||
);
|
||||
this.filePath = absolutePath;
|
||||
if (plugins.smartfile.fs.isDirectorySync(this.filePath)) {
|
||||
this.filePath = plugins.path.join(
|
||||
this.filePath,
|
||||
this.identity + '.json',
|
||||
);
|
||||
}
|
||||
plugins.smartfile.fs.ensureFileSync(this.filePath, '{}');
|
||||
return;
|
||||
}
|
||||
|
||||
let baseDir: string;
|
||||
if (this.type === 'custom') {
|
||||
baseDir = paths.kvCustomDir;
|
||||
} else if (this.type === 'gitProject') {
|
||||
baseDir = paths.kvGitDir;
|
||||
} else if (this.type === 'path') {
|
||||
baseDir = paths.kvPathDir;
|
||||
if (this.type === 'userHomeDir') {
|
||||
baseDir = paths.kvUserHomeDirBase;
|
||||
} else {
|
||||
throw new Error('kv type not supported');
|
||||
}
|
||||
this.filePath = plugins.path.join(baseDir, this.identity + '.json');
|
||||
plugins.smartfile.fs.ensureDirSync(paths.kvCustomDir);
|
||||
plugins.smartfile.fs.ensureDirSync(paths.kvGitDir);
|
||||
plugins.smartfile.fs.ensureDirSync(paths.kvPathDir);
|
||||
plugins.smartfile.fs.ensureDirSync(baseDir);
|
||||
plugins.smartfile.fs.ensureFileSync(this.filePath, '{}');
|
||||
};
|
||||
|
||||
public type: TKeyValueStore; // the type of the kvStore
|
||||
public identity: string; // the identity of the kvStore
|
||||
public filePath: string; // the filePath of the kvStore
|
||||
// if no custom path is provided, try to store at home directory
|
||||
public type: TKeyValueStore;
|
||||
public identity: string;
|
||||
public filePath?: string;
|
||||
private customPath?: string; // Optionally allow custom path
|
||||
|
||||
/**
|
||||
* the constructor of keyvalue store
|
||||
* @param typeArg
|
||||
* @param customStringArg
|
||||
* @param identityArg
|
||||
* @param customPath Optional custom path for the keyValue store
|
||||
*/
|
||||
constructor(typeArg: TKeyValueStore, customStringArg: string) {
|
||||
// set kvStoreType
|
||||
this.type = typeArg;
|
||||
this.identity = customStringArg;
|
||||
constructor(optionsArg: IKvStoreConstructorOptions<T>) {
|
||||
if (optionsArg.customPath && optionsArg.typeArg !== 'custom') {
|
||||
throw new Error('customPath can only be provided if typeArg is custom');
|
||||
}
|
||||
if (optionsArg.typeArg === 'custom' && !optionsArg.customPath) {
|
||||
throw new Error('customPath must be provided if typeArg is custom');
|
||||
}
|
||||
this.type = optionsArg.typeArg;
|
||||
this.identity = optionsArg.identityArg;
|
||||
this.customPath = optionsArg.customPath; // Store custom path if provided
|
||||
this.initFilePath();
|
||||
if (optionsArg.mandatoryKeys) {
|
||||
this.setMandatoryKeys(optionsArg.mandatoryKeys);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* reads all keyValue pairs at once and returns them
|
||||
*/
|
||||
public async readAll() {
|
||||
public async readAll(): Promise<Partial<T>> {
|
||||
await this.syncTask.trigger();
|
||||
return this.dataObject;
|
||||
}
|
||||
@@ -75,21 +127,24 @@ export class KeyValueStore {
|
||||
/**
|
||||
* reads a keyValueFile from disk
|
||||
*/
|
||||
public async readKey(keyArg: string) {
|
||||
public async readKey<K extends keyof T>(keyArg: K): Promise<T[K]> {
|
||||
await this.syncTask.trigger();
|
||||
return this.dataObject[keyArg];
|
||||
return this.dataObject[keyArg] as T[K];
|
||||
}
|
||||
|
||||
/**
|
||||
* writes a specific key to the keyValueStore
|
||||
*/
|
||||
public async writeKey(keyArg: string, valueArg: any) {
|
||||
public async writeKey<K extends keyof T>(
|
||||
keyArg: K,
|
||||
valueArg: T[K],
|
||||
): Promise<void> {
|
||||
await this.writeAll({
|
||||
[keyArg]: valueArg,
|
||||
});
|
||||
} as unknown as Partial<T>);
|
||||
}
|
||||
|
||||
public async deleteKey(keyArg: string) {
|
||||
public async deleteKey<K extends keyof T>(keyArg: K): Promise<void> {
|
||||
this.deletedObject[keyArg] = this.dataObject[keyArg];
|
||||
await this.syncTask.trigger();
|
||||
}
|
||||
@@ -97,7 +152,7 @@ export class KeyValueStore {
|
||||
/**
|
||||
* writes all keyValue pairs in the object argument
|
||||
*/
|
||||
public async writeAll(keyValueObject) {
|
||||
public async writeAll(keyValueObject: Partial<T>): Promise<void> {
|
||||
this.dataObject = { ...this.dataObject, ...keyValueObject };
|
||||
await this.syncTask.trigger();
|
||||
}
|
||||
@@ -105,8 +160,63 @@ export class KeyValueStore {
|
||||
/**
|
||||
* wipes a key value store from disk
|
||||
*/
|
||||
public async wipe() {
|
||||
public async wipe(): Promise<void> {
|
||||
this.dataObject = {};
|
||||
await plugins.smartfile.fs.remove(this.filePath);
|
||||
if (this.type !== 'ephemeral') {
|
||||
await plugins.smartfile.fs.remove(this.filePath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* resets the KeyValueStore to the initial state by syncing first, deleting all keys, and then triggering a sync again
|
||||
*/
|
||||
public async reset(): Promise<void> {
|
||||
await this.syncTask.trigger(); // Sync to get the latest state
|
||||
|
||||
// Delete all keys from the dataObject and add them to deletedObject
|
||||
for (const key of Object.keys(this.dataObject) as Array<keyof T>) {
|
||||
this.deletedObject[key] = this.dataObject[key];
|
||||
delete this.dataObject[key];
|
||||
}
|
||||
|
||||
await this.syncTask.trigger(); // Sync again to reflect the deletion
|
||||
}
|
||||
|
||||
private setMandatoryKeys(keys: Array<keyof T>) {
|
||||
keys.forEach((key) => this.mandatoryKeys.add(key));
|
||||
}
|
||||
|
||||
public async getMissingMandatoryKeys(): Promise<Array<keyof T>> {
|
||||
await this.readAll();
|
||||
return Array.from(this.mandatoryKeys).filter(
|
||||
(key) => !(key in this.dataObject),
|
||||
);
|
||||
}
|
||||
|
||||
public async waitForKeysPresent<K extends keyof T>(
|
||||
keysArg: K[],
|
||||
): Promise<void> {
|
||||
const missingKeys = keysArg.filter((keyArg) => !this.dataObject[keyArg]);
|
||||
if (missingKeys.length === 0) {
|
||||
return;
|
||||
}
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const subscription = this.changeSubject.subscribe(() => {
|
||||
const missingKeys = keysArg.filter(
|
||||
(keyArg) => !this.dataObject[keyArg],
|
||||
);
|
||||
if (missingKeys.length === 0) {
|
||||
subscription.unsubscribe();
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async waitForAndGetKey<K extends keyof T>(
|
||||
keyArg: K,
|
||||
): Promise<T[K] | undefined> {
|
||||
await this.waitForKeysPresent([keyArg]);
|
||||
return this.readKey(keyArg);
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import * as plugins from './npmextra.plugins';
|
||||
import * as paths from './npmextra.paths';
|
||||
import * as plugins from './npmextra.plugins.js';
|
||||
import * as paths from './npmextra.paths.js';
|
||||
|
||||
/**
|
||||
* Npmextra class allows easy configuration of tools
|
||||
@@ -27,7 +27,10 @@ export class Npmextra {
|
||||
/**
|
||||
* 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;
|
||||
if (this.npmextraJsonData[toolnameArg]) {
|
||||
npmextraToolOptions = this.npmextraJsonData[toolnameArg];
|
||||
@@ -45,7 +48,9 @@ export class Npmextra {
|
||||
* checks if the JSON exists
|
||||
*/
|
||||
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() {
|
||||
if (this.npmextraJsonExists) {
|
||||
this.npmextraJsonData = plugins.smartfile.fs.toObjectSync(this.lookupPath);
|
||||
this.npmextraJsonData = plugins.smartfile.fs.toObjectSync(
|
||||
this.lookupPath,
|
||||
);
|
||||
} else {
|
||||
this.npmextraJsonData = {};
|
||||
}
|
||||
|
@@ -1,8 +1,11 @@
|
||||
import * as plugins from './npmextra.plugins';
|
||||
import * as plugins from './npmextra.plugins.js';
|
||||
|
||||
// directories
|
||||
export let cwd = process.cwd();
|
||||
export let packageDir = plugins.path.join(__dirname, '../');
|
||||
export let packageDir = plugins.path.join(
|
||||
plugins.smartpath.get.dirnameFromImportMetaUrl(import.meta.url),
|
||||
'../',
|
||||
);
|
||||
|
||||
// ----------------------
|
||||
// keyValueStore specific
|
||||
@@ -13,22 +16,7 @@ export let home = plugins.smartpath.get.home();
|
||||
/**
|
||||
* keyValue base path
|
||||
*/
|
||||
export let kvBase = plugins.path.join(home, '.npmextra/kv');
|
||||
|
||||
/**
|
||||
* the base directory for custom string based key value store
|
||||
*/
|
||||
export let kvCustomDir = plugins.path.join(kvBase, 'custom');
|
||||
|
||||
/**
|
||||
* the subdir for git based keyValue
|
||||
*/
|
||||
export let kvGitDir = plugins.path.join(kvBase, 'git');
|
||||
|
||||
/**
|
||||
* keyValue for path based keyValue store
|
||||
*/
|
||||
export let kvPathDir = plugins.path.join(kvBase, 'path');
|
||||
export let kvUserHomeDirBase = plugins.path.join(home, '.npmextra/kv');
|
||||
|
||||
// files
|
||||
export let configFile = plugins.path.join(cwd, 'npmextra.json');
|
||||
|
@@ -1,8 +1,25 @@
|
||||
import * as beautylog from '@pushrocks/smartlog';
|
||||
import * as path from 'path';
|
||||
import * as smartfile from '@pushrocks/smartfile';
|
||||
import * as smartpath from '@pushrocks/smartpath';
|
||||
import * as smartpromise from '@pushrocks/smartpromise';
|
||||
import * as taskbuffer from '@pushrocks/taskbuffer';
|
||||
import * as tsclass from '@tsclass/tsclass';
|
||||
|
||||
export { beautylog, path, smartfile, smartpath, smartpromise, taskbuffer };
|
||||
export { tsclass };
|
||||
|
||||
import * as qenv from '@push.rocks/qenv';
|
||||
import * as smartlog from '@push.rocks/smartlog';
|
||||
import * as path from 'path';
|
||||
import * as smartfile from '@push.rocks/smartfile';
|
||||
import * as smartjson from '@push.rocks/smartjson';
|
||||
import * as smartpath from '@push.rocks/smartpath';
|
||||
import * as smartpromise from '@push.rocks/smartpromise';
|
||||
import * as smartrx from '@push.rocks/smartrx';
|
||||
import * as taskbuffer from '@push.rocks/taskbuffer';
|
||||
|
||||
export {
|
||||
qenv,
|
||||
smartlog,
|
||||
path,
|
||||
smartfile,
|
||||
smartjson,
|
||||
smartpath,
|
||||
smartpromise,
|
||||
smartrx,
|
||||
taskbuffer,
|
||||
};
|
||||
|
15
tsconfig.json
Normal file
15
tsconfig.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"useDefineForClassFields": false,
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"esModuleInterop": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {}
|
||||
},
|
||||
"exclude": ["dist_*/**/*.d.ts"]
|
||||
}
|
17
tslint.json
17
tslint.json
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"extends": ["tslint:latest", "tslint-config-prettier"],
|
||||
"rules": {
|
||||
"semicolon": [true, "always"],
|
||||
"no-console": false,
|
||||
"ordered-imports": false,
|
||||
"object-literal-sort-keys": false,
|
||||
"member-ordering": {
|
||||
"options":{
|
||||
"order": [
|
||||
"static-method"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultSeverity": "warning"
|
||||
}
|
Reference in New Issue
Block a user