refactor(charging-station): config assets single source of truth (#2157)
* refactor(charging-station): config assets single source of truth
Replace every join(dirname(fileURLToPath(import.meta.url)), "assets", ...)
asset path with a single resolveAssetsDir() helper in src/resolveAssetsDir.ts.
It honors the EVSE_ASSETS_DIR override, otherwise resolves the assets folder
that sits beside the helper - dist/assets in the bundle, src/assets via tsx.
- Configuration.ts, HelpersConfig.ts (templates, id-tags, EV profiles),
Bootstrap.ts (assetsDir) and UIMCPServer.ts (schema dir) now use it.
- start:dev(+debug/prof) run src/start.ts through tsx with no build step, so
dev reads and reloads src/assets directly; production start stays bundled.
- README documents editing src/assets (dist is a build artifact) plus override.
Configuration defaults are preserved: src/assets/config.json is absent from
sources, so the runtime still falls back to the default configuration.
* [autofix.ci] apply automated fixes
* refactor(charging-station): route remaining schema loaders and config diagnostics through resolveAssetsDir
Address PR review: MCPResources.getSchemaBaseDir and OCPPServiceUtils.parseJsonSchemaFile now resolve via resolveAssetsDir() (honoring EVSE_ASSETS_DIR); drop the assets/ prefix from createPayloadOptions so relativePath is relative to the assets dir. The missing-config error now interpolates the resolved configurationFile path.
* [autofix.ci] apply automated fixes
* test(charging-station): add unit tests for resolveAssetsDir assets-dir resolver
Add a unit test for the single source of truth that resolves the assets
directory (EVSE_ASSETS_DIR override; absolute paths used as-is; relative
paths resolved against cwd; module-side fallback). It locks the documented
absolute-used-as-is contract and closes the coverage gap on the resolver.
* chore(charging-station): gitignore dev-generated station config state
Dev mode now resolves assets from src/assets (single source of truth), so the simulator's provisioned station configs and .simulator-state.json are written to src/assets/configurations/ at runtime. Previously (bundle dev) this state landed in dist/assets (already ignored). Ignore the generated directory to keep the working tree clean.
* fix(charging-station): route cert-manager base path via resolveAssetsDir; qualify README asset paths
Review follow-up (MEDIUM findings):
- OCPP20CertificateManager: BASE_CERT_PATH now join(resolveAssetsDir(),
'configurations') instead of hardcoded 'dist/assets/configurations'
(which resolved to a nonexistent directory in tsx/dev/tests mode).
Certificate-manager test cleanup paths now use resolveAssetsDir() too.
- README: charging-station configuration paths now expressed as
<assets-dir>/configurations, where <assets-dir> is dist/assets in
production and src/assets in development (locations, hierarchy, section
header), so developers no longer look in dist/ in dev mode.
* test(assets): relocate resolveAssetsDir test to mirror source tree
resolveAssetsDir.ts lives at the src/ root (required by its
<module-dir>/assets resolution), so its test belongs at the tests/
root, not tests/utils/. Move tests/utils/resolveAssetsDir.test.ts to
tests/resolveAssetsDir.test.ts and adjust the relative import depth.
* docs(assets): make JSDoc asset-path references mode-neutral
Cross-validation of the asset-centralization review findings surfaced
JSDoc comments still assuming the production layout. Express them in
mode-neutral terms consistent with resolveAssetsDir():
- HelpersConfig: drop "bundled" from the id-tags/ev-profiles resolvers;
buildTemplateName references the station-templates subdirectory of the
assets directory.
- Statistics: template-index reconstruction path now
<assets-dir>/configurations/ instead of dist/assets/configurations/.
* test(assets): make path assertions platform-robust (Windows)
Asset-path centralization (resolveAssetsDir) makes asset paths absolute,
which exposed POSIX-only path handling in two tests on Windows:
- OCPP20CertificateManager: the serial-sanitization test parsed the cert
path with split('/').pop(), which on Windows returns the whole
drive-qualified path (contains ':'); use path.basename() instead.
- resolveAssetsDir: the absolute-override tests used a POSIX-only
'/tmp/...' fixture that isAbsolute() rejects on Windows; build the
fixture with path.resolve(cwd, ...) so it is absolute on every platform.
* fix(assets): address PR #2157 review findings
- hermetic tests: tests/stubs/assets fixture (junctions to read-only src/assets sub-trees + writable configurations/) wired via EVSE_ASSETS_DIR in test scripts
- centralize OCPP schema base dir (getOcppSchemaBaseDir) used by MCPResources, UIMCPServer, OCPPServiceUtils
- isPathWithinBase: platform-safe sep-based prefix guard
- resolveAssetsDir tests: strict module-relative assertions
- README: EVSE_ASSETS_DIR layout/writability/pre-start, prod/dev asset-dir mapping, persistState generation
- parseJsonSchemaFile: document SyntaxError throw
* [autofix.ci] apply automated fixes
* docs: relocate EVSE_ASSETS_DIR contract from README to resolveAssetsDir JSDoc
The README is user-facing documentation; the expected asset layout,
writability requirement, and set-before-start timing for the
EVSE_ASSETS_DIR override are developer-facing details. Document the
full contract in the resolveAssetsDir.ts module JSDoc instead, and
keep the README limited to the user workflow (edit src/assets, dist
rebuilt by pnpm build, configurations/ persists across builds).
* test: run the suite hermetically against the assets fixture
The suite asserted configuration defaults while reading whatever
resolveAssetsDir() found: pnpm test from the repo root worked, but
standalone runs (node --import tsx --test <file>) hit the
user-generated src/assets content — a user config.json failed the
suite, and the certificate-manager test wrote into
src/assets/configurations.
- src/resolveAssetsDir.ts: fail fast with AssetsDirError when the
EVSE_ASSETS_DIR override points to a missing or non-directory path
(the <module-dir>/assets fallback stays the trusted default);
export OCPP_SCHEMA_DIR (<assets-dir>/json-schemas/ocpp)
- OCPP 1.6 / 2.0 payload validators use OCPP_SCHEMA_DIR instead of
hardcoded relative strings
- tests/stubs/testEnvBootstrap.mjs (new): node --import module for the
test scripts — sets EVSE_ASSETS_DIR to the absolute fixture path
(honoring an explicit user override), NODE_ENV=test, and ensures the
fixture layout
- tests/stubs/setupAssetsFixture.mjs: split into a non-destructive,
concurrency-safe ensureAssetsFixture() and the direct-entry
full-regeneration setupAssetsFixture()
- package.json: test / test:debug / test:coverage load the bootstrap
via --import instead of a cwd-relative cross-env EVSE_ASSETS_DIR
- Configuration.test.ts and OCPP20CertificateManager.test.ts import the
bootstrap first: standalone runs of these files are hermetic too
- tests/resolveAssetsDir.test.ts: real temp dirs (mkdtemp) instead of
relative paths never created on disk; AssetsDirError tests
- README: the <assets-dir>/configurations note now matches the runtime
(mkdir recursive on write, not a startup generation); idTagsFile /
evProfilesFile documented relative to <assets-dir> instead of
src/assets
* fix: restore --test-reporter-destination in the test:coverage script
The script rewrite accidentally renamed the longstanding
--test-reporter-destination flag to --test-coverage-destination,
which Node 22.x/24.x do not know (CI failed with
'node: bad option: --test-coverage-destination=coverage/lcov.info').
Verified: the exact script runs on Node 22.23.4 and 26.12.1 and
produces a populated lcov file.
* refactor: move AssetsDirError to src/exception per review findings
- Relocate AssetsDirError to src/exception/AssetsDirError.ts to follow the
repository error-class convention (BaseError, OCPPError, TimeoutError +
barrel); re-export it from resolveAssetsDir.ts so the module API is
unchanged.
- Correct the assertAssetsDir JSDoc: call sites span process startup,
per-station setup, and per-transaction paths; the per-call cost is one
statSync only when an override is set (no syscall on the no-override
fallback).
- Assert the full AssetsDirError message (name + message) in
tests/resolveAssetsDir.test.ts, following the repository's strict
error-message assertion convention.
* fix: derive station configuration and diagnostics paths from the resolved assets layout
ChargingStation: derive the per-station configuration file from the
template file's parent directory (the assets directory) instead of a
string replace of 'station-templates', which broke EVSE_ASSETS_DIR
overrides whose path contains 'station-templates'.
OCPP16 GetDiagnostics: resolve the log scan and the diagnostics
archive against the working directory (where winston writes the logs
and where the archive is created) instead of module-relative, so the
command works in dev-from-source; drop the now-unused fileURLToPath
import.
README: note in the <assets-dir> definition that EVSE_ASSETS_DIR may
point to a custom assets directory (absolute, or relative to the
working directory).
* refactor: address remaining review findings on assets resolution
- Rename OCPP_SCHEMA_DIR to OCPP_SCHEMA_SUBDIR (it is a relative path
fragment of the assets directory, not a directory path)
- Drop the unused AssetsDirError re-export (canonical barrel: src/exception)
- JSDoc: document the layout as a contract not validated by the resolver;
correct the ui-protocol/ claim (source-tree-only, artifact for humans)
- README: source-managed content rebuild wording; backtick <assets-dir>
in 3 table cells; idTagsFile/evProfilesFile are file names at the root
of the assets directory (basename-only resolution)
- Bootstrap JSDoc: unset-or-empty override; warn about stale fixture
content on manual full-suite runs; document the first-import contract
and the string-equality fixture check
- Tests: cover the relative-missing override branch (raw and resolved
paths in the error message); assert AssetsDirError messages by
substrings per the suite convention instead of duplicating the format
* [autofix.ci] apply automated fixes
* refactor: centralize asset path derivation in resolveAssetsDir helpers
- add getAssetFilePath, getConfigurationsDir, getStationTemplatesDir and getConfigFilePath so every <assets-dir>/<subpath> is derived from one join primitive
- Bootstrap, HelpersConfig, Configuration and OCPP20CertificateManager use the derived helpers instead of join(resolveAssetsDir(), ...)
- AssetsDirError: drop the redundant name override (BaseError sets name via new.target.name)
- README: fix the \`<assets-dir>\`/configurations wording to <assets-dir>/configurations
- tests: cover getOcppSchemaBaseDir with and without EVSE_ASSETS_DIR
- apply pnpm format (prettier + eslint) on the changed files
* refactor: derive the OCPP 1.6 schema path via getAssetFilePath
parseJsonSchemaFile now uses the single join primitive from resolveAssetsDir, dropping the last direct join(resolveAssetsDir(), ...) call site and the unused node:path import. No behavior change.
* fix(assets): preserve nested persistence and cache paths