diff --git a/Dockerfile b/Dockerfile index ff227fec6..aefed35cb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -35,4 +35,4 @@ STOPSIGNAL SIGINT ENTRYPOINT [ "node", "dist/cli.js" ] CMD [ "-h" ] -## docker build -t wot-servient ./docker/Dockerfile +## docker build -t node-wot ./docker/Dockerfile diff --git a/README.md b/README.md index 6200191f7..0905035ef 100644 --- a/README.md +++ b/README.md @@ -119,16 +119,16 @@ Go into the repository: cd node-wot ``` -Build the Docker image named `wot-servient` from the `Dockerfile`: +Build the Docker image named `node-wot` from the `Dockerfile`: ``` npm run build:docker ``` -Run the wot-servient as a container: +Run the `node-wot` as a container: ``` -docker run --rm wot-servient -h +docker run --rm node-wot -h ``` ## Examples @@ -231,12 +231,12 @@ Can't find your preferred MediaType? More codecs can be easily added by implemen Run all the steps above including "Link Packages" and then run this: ``` -wot-servient -h +node-wot -h cd examples/scripts -wot-servient +node-wot ``` -Without the "Link Packages" step, the `wot-servient` command is not available and `node` needs to be used (e.g., Windows CMD shell): +Without the "Link Packages" step, the `node-wot` command is not available and `node` needs to be used (e.g., Windows CMD shell): ``` # expose @@ -256,9 +256,9 @@ First [build the docker image](#as-a-docker-image) and then run the counter exam ``` # expose -docker run -it --init -p 8080:8080/tcp -p 5683:5683/udp -v "$(pwd)"/examples:/srv/examples --rm wot-servient /srv/examples/scripts/counter.js +docker run -it --init -p 8080:8080/tcp -p 5683:5683/udp -v "$(pwd)"/examples:/srv/examples --rm node-wot /srv/examples/scripts/counter.js # consume -docker run -it --init -v "$(pwd)"/examples:/srv/examples --rm --net=host wot-servient /srv/examples/scripts/counter-client.js --client-only +docker run -it --init -v "$(pwd)"/examples:/srv/examples --rm --net=host node-wot /srv/examples/scripts/counter-client.js --client-only ``` - The counter exposes the HTTP endpoint at 8080/tcp and the CoAP endpoint at 5683/udp and they are bound to the host machine (with `-p 8080:8080/tcp -p 5683:5683/udp`). diff --git a/package-lock.json b/package-lock.json index e9afb9754..cd6b93592 100644 --- a/package-lock.json +++ b/package-lock.json @@ -362,6 +362,29 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "dev": true, @@ -462,6 +485,8 @@ }, "node_modules/@jsep-plugin/assignment": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@jsep-plugin/assignment/-/assignment-1.3.0.tgz", + "integrity": "sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==", "dev": true, "license": "MIT", "engines": { @@ -473,6 +498,8 @@ }, "node_modules/@jsep-plugin/regex": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@jsep-plugin/regex/-/regex-1.0.4.tgz", + "integrity": "sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg==", "dev": true, "license": "MIT", "engines": { @@ -1630,6 +1657,13 @@ "@types/node": "*" } }, + "node_modules/@types/tmp": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.2.6.tgz", + "integrity": "sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/tough-cookie": { "version": "4.0.5", "license": "MIT" @@ -2142,6 +2176,7 @@ }, "node_modules/acorn": { "version": "8.15.0", + "devOptional": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -2160,6 +2195,7 @@ }, "node_modules/acorn-walk": { "version": "8.3.4", + "devOptional": true, "license": "MIT", "dependencies": { "acorn": "^8.11.0" @@ -3597,8 +3633,9 @@ }, "node_modules/cross-spawn": { "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, - "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -3681,6 +3718,8 @@ }, "node_modules/debug": { "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -4799,6 +4838,8 @@ }, "node_modules/express": { "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "dev": true, "license": "MIT", "dependencies": { @@ -4855,6 +4896,12 @@ "dev": true, "license": "MIT" }, + "node_modules/express/node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "dev": true + }, "node_modules/express/node_modules/safe-buffer": { "version": "5.2.1", "dev": true, @@ -5214,13 +5261,16 @@ } }, "node_modules/form-data": { - "version": "4.0.2", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { @@ -5284,6 +5334,21 @@ "version": "1.0.0", "license": "ISC" }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "license": "MIT", @@ -5425,7 +5490,9 @@ } }, "node_modules/glob": { - "version": "10.4.5", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "dev": true, "license": "ISC", "dependencies": { @@ -6488,7 +6555,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -6504,6 +6573,8 @@ }, "node_modules/jsep": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz", + "integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==", "dev": true, "license": "MIT", "engines": { @@ -6526,7 +6597,6 @@ "node_modules/json-schema-faker": { "version": "0.5.9", "dev": true, - "license": "MIT", "dependencies": { "json-schema-ref-parser": "^6.1.0", "jsonpath-plus": "^10.3.0" @@ -6554,7 +6624,9 @@ } }, "node_modules/json-schema-ref-parser/node_modules/js-yaml": { - "version": "3.14.1", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, "license": "MIT", "dependencies": { @@ -6565,10 +6637,18 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/json-schema-ref-parser/node_modules/sprintf-js": { - "version": "1.0.3", + "node_modules/json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", "dev": true, - "license": "BSD-3-Clause" + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, + "engines": { + "node": ">=16" + } }, "node_modules/json-schema-traverse": { "version": "1.0.0", @@ -6639,7 +6719,9 @@ } }, "node_modules/koa": { - "version": "2.16.1", + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/koa/-/koa-2.16.3.tgz", + "integrity": "sha512-zPPuIt+ku1iCpFBRwseMcPYQ1cJL8l60rSmKeOuGfOXyE6YnTBmf2aEFNL2HQGrD0cPcLO/t+v9RTgC+fwEh/g==", "dev": true, "license": "MIT", "dependencies": { @@ -6999,8 +7081,9 @@ }, "node_modules/micromatch": { "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, - "license": "MIT", "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -7456,11 +7539,6 @@ "@sinonjs/commons": "^3.0.1" } }, - "node_modules/nise/node_modules/path-to-regexp": { - "version": "6.3.0", - "dev": true, - "license": "MIT" - }, "node_modules/node-addon-api": { "version": "7.0.0", "license": "MIT" @@ -9042,9 +9120,10 @@ "license": "ISC" }, "node_modules/path-to-regexp": { - "version": "0.1.12", - "dev": true, - "license": "MIT" + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true }, "node_modules/path-type": { "version": "4.0.0", @@ -9098,11 +9177,13 @@ } }, "node_modules/playwright": { - "version": "1.52.0", + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", + "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.52.0" + "playwright-core": "1.57.0" }, "bin": { "playwright": "cli.js" @@ -9115,7 +9196,9 @@ } }, "node_modules/playwright-core": { - "version": "1.52.0", + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", + "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -10756,7 +10839,9 @@ } }, "node_modules/tar-fs": { - "version": "3.0.8", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz", + "integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==", "dev": true, "license": "MIT", "dependencies": { @@ -10865,6 +10950,16 @@ "dev": true, "license": "MIT" }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "dev": true, @@ -10908,6 +11003,12 @@ "node": ">=18" } }, + "node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "dev": true + }, "node_modules/ts-api-utils": { "version": "2.1.0", "dev": true, @@ -11159,7 +11260,9 @@ } }, "node_modules/tslint/node_modules/js-yaml": { - "version": "3.14.1", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, "license": "MIT", "dependencies": { @@ -11657,20 +11760,6 @@ "extsprintf": "^1.2.0" } }, - "node_modules/vm2": { - "version": "3.9.18", - "license": "MIT", - "dependencies": { - "acorn": "^8.7.0", - "acorn-walk": "^8.2.0" - }, - "bin": { - "vm2": "bin/vm2" - }, - "engines": { - "node": ">=6.0" - } - }, "node_modules/web-streams-polyfill": { "version": "4.1.0", "license": "MIT", @@ -12338,13 +12427,15 @@ } }, "packages/browser-bundle/node_modules/glob": { - "version": "11.0.2", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^4.0.1", - "minimatch": "^10.0.0", + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" @@ -12360,7 +12451,9 @@ } }, "packages/browser-bundle/node_modules/jackspeak": { - "version": "4.1.0", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", + "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -12382,11 +12475,13 @@ } }, "packages/browser-bundle/node_modules/minimatch": { - "version": "10.0.1", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", + "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^2.0.1" + "@isaacs/brace-expansion": "^5.0.0" }, "engines": { "node": "20 || >=22" @@ -12452,15 +12547,18 @@ "@thingweb/thing-model": "^1.0.4", "ajv": "^8.11.0", "commander": "^9.1.0", + "debug": "^4.4.0", "dotenv": "^16.4.7", - "lodash": "^4.17.21", - "vm2": "3.9.18" + "lodash": "^4.17.21" }, "bin": { - "wot-servient": "bin/index.js" + "node-wot": "bin/index.js" }, "devDependencies": { - "@types/lodash": "^4.14.199" + "@types/lodash": "^4.14.199", + "@types/tmp": "^0.2.6", + "json-schema-to-ts": "^3.1.1", + "tmp": "^0.2.5" }, "optionalDependencies": { "ts-node": "10.9.1" diff --git a/package.json b/package.json index a51ab1dc0..165ce5076 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ } }, "scripts": { - "build": "tsc -b && npm run build -w packages/browser-bundle", + "build": "npm run build:transform -w packages/cli && tsc -b && npm run build -w packages/browser-bundle", "pretest": "npm run build", "start": "cd packages/cli && npm run start", "debug": "cd packages/cli && npm run debug", @@ -31,8 +31,8 @@ "prepare": "husky install", "publish": "npm publish --workspaces", "check:versions": "node utils/check_package_version_consistency.js", - "build:docker": "docker build -t wot-servient .", - "build:podman": "podman build -t wot-servient .", + "build:docker": "docker build -t node-wot .", + "build:podman": "podman build -t node-wot .", "clean:dist": "npm exec --workspaces -- npx rimraf tsconfig.tsbuildinfo dist", "update:wot-typescript-definitions": "npx npm-check-updates -u -f \"wot-typescript-definitions\" --deep", "link": "npm link -ws", diff --git a/packages/binding-mqtt/README.md b/packages/binding-mqtt/README.md index 5308eb4e5..b37c583f5 100644 --- a/packages/binding-mqtt/README.md +++ b/packages/binding-mqtt/README.md @@ -200,11 +200,11 @@ Please setup node-wot as described at the [node-wot main page](https://github.co ``` -Start the script by the command `wot-servient mqtt-publish.js` or `node ../../packages/cli/dist/cli.js mqtt-publish.js`. +Start the script by the command `node-wot mqtt-publish.js` or `node ../../packages/cli/dist/cli.js mqtt-publish.js`. - example-mqtt-subscription.js: Shows how node-wot consumes a Thing Description to do MQTT subscription on the provided event (=latest counter value) as well as initiate the action (reset counter). -Start the script by the command `wot-servient -c mqtt-subscribe.js` or `node ../../packages/cli/dist/cli.js -c mqtt-subscribe.js`. +Start the script by the command `node-wotù -c mqtt-subscribe.js` or `node ../../packages/cli/dist/cli.js -c mqtt-subscribe.js`. ### More Details diff --git a/packages/binding-mqtt/src/mqtt-broker-server.ts b/packages/binding-mqtt/src/mqtt-broker-server.ts index 810f3fddc..8484a9647 100644 --- a/packages/binding-mqtt/src/mqtt-broker-server.ts +++ b/packages/binding-mqtt/src/mqtt-broker-server.ts @@ -65,7 +65,7 @@ export default class MqttBrokerServer implements ProtocolServer { private port = -1; private address?: string = undefined; - private brokerURI: string; + private brokerURI?: string; private readonly things: Map = new Map(); @@ -77,15 +77,14 @@ export default class MqttBrokerServer implements ProtocolServer { private hostedBroker?: net.Server; constructor(config: MqttBrokerServerConfig) { - this.config = config ?? this.defaults; - this.config.uri = this.config.uri ?? this.defaults.uri; - // if there is a MQTT protocol indicator missing, add this - if (config.uri.indexOf("://") === -1) { + if (config.uri?.indexOf("://") === -1) { config.uri = this.scheme + "://" + config.uri; } this.brokerURI = config.uri; + this.config = config ?? this.defaults; + this.config.uri = this.config.uri ?? this.defaults.uri; } public async expose(thing: ExposedThing): Promise { @@ -446,6 +445,10 @@ export default class MqttBrokerServer implements ProtocolServer { private async startBroker() { return new Promise((resolve, reject) => { + if (this.brokerURI == null) { + throw new Error("Unexpected configuration state. Broker was started but brokerURI is null"); + } + this.hostedServer = Server({}); let server: tls.Server | net.Server; if (this.config.key) { diff --git a/packages/binding-mqtt/src/mqtt.ts b/packages/binding-mqtt/src/mqtt.ts index 9df00c9a1..cccfac80e 100644 --- a/packages/binding-mqtt/src/mqtt.ts +++ b/packages/binding-mqtt/src/mqtt.ts @@ -51,7 +51,7 @@ export interface MqttClientConfig { } export interface MqttBrokerServerConfig { - uri: string; + uri?: string; user?: string; psw?: string; clientId?: string; diff --git a/packages/cli/.gitignore b/packages/cli/.gitignore new file mode 100644 index 000000000..9f7645a6d --- /dev/null +++ b/packages/cli/.gitignore @@ -0,0 +1,2 @@ +src/generated +test/resources diff --git a/packages/cli/README.md b/packages/cli/README.md index 9dcfac40f..5633a9f55 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -27,108 +27,73 @@ If you do so, anyway, you can specify the entry point as follows: ```JavaScript "scripts":{ - "start": "wot-servient main.js" + "start": "node-wot main.js" } ``` There are several ways to start the application: a. Execute `npm start`. -b. Execute `./node_modules/.bin/wot-servient main.js`. +b. Execute `./node_modules/.bin/node-wot main.js`. c. Execute `node ./node_modules/@node-wot/cli/dist/cli.js main.js`. d. If you have installed `@node-wot/cli` globally you can even start the application right -away using this command `wot-servient main.js`. However, in the current implementation, the +away using this command `node-wot main.js`. However, in the current implementation, the import of local dependencies is not supported in this case. -wot-servient can execute multiple files at once, for example as follows: +node-wot can execute multiple files at once, for example as follows: ``` -wot-servient script1.js ./src/script2.js +node-wot script1.js ./src/script2.js ``` ### Configuration The `-h` option explains the functionality and also how node-wot can be configured based on `wot-servient.conf.json`. -- `wot-servient -h` _or_ -- `node packages\cli\dist\cli.js` +- `node-wot -h` _or_ +- `node packages\cli\dist\cli.js -h` The `-h` help option shows the following output: ``` -Usage: wot-servient [options] [files...] +Usage: node-wot [options] [command] [files...] Run a WoT Servient in the current directory. Arguments: - files script files to execute. If no script is given, all .js files in the current directory are - loaded. If one or more script is given, these files are loaded instead of the directory. + files script files to execute. If no script is given, all .js files in the current directory are loaded. If one or more script is given, these files are loaded instead of the directory. Options: - -v, --version display node-wot version - -i, --inspect [host]:[port] activate inspector on host:port (default: 127.0.0.1:9229) - -ib, --inspect-brk [host]:[port] activate inspector on host:port (default: 127.0.0.1:9229) - -c, --client-only do not start any servers (enables multiple instances without port conflicts) - -cp, --compiler load module as a compiler - -f, --config-file load configuration from specified file (default: "wot-servient.conf.json") - -p, --config-params override configuration parameters [key1:=value1 key2:=value2 ...] (e.g. http.port:=8080) - -h, --help show this help - -wot-servient.conf.json syntax: + -v, --version display node-wot version + -c, --client-only do not start any servers (enables multiple instances without port conflicts) + -ll, --logLevel choose the desired log level. WARNING: if DEBUG env variable is specified this option gets overridden. (choices: "debug", "info", "warn", "error") + -f, --config-file load configuration from specified file (default: $(pwd)/wot-servient.conf.json) + -p, --config-params override configuration parameters [key1:=value1 key2:=value2 ...] (e.g. http.port:=8080) + -h, --help show this help + +Commands: + schema prints the json schema for the configuration file + +Configuration + +Settings can be applied through three methods, in order of precedence (highest to lowest): + +1. Command-Line Parameters (-p path.to.set=value) +2. Environment Variables (NODE_WOT_PATH_TO_SET=value) (supports .env files too) +3. Configuration File + +For the complete list of available configuration fields and their data types, run: + +node-wot schema + +In your configuration files you can the following to enable IDE config validation: + { - "servient": { - "clientOnly": CLIENTONLY, - "staticAddress": STATIC, - "scriptAction": RUNSCRIPT - }, - "http": { - "port": HPORT, - "proxy": PROXY, - "allowSelfSigned": ALLOW - }, - "mqtt" : { - "broker": BROKER-URL, - "username": BROKER-USERNAME, - "password": BROKER-PASSWORD, - "clientId": BROKER-UNIQUEID, - "protocolVersion": MQTT_VERSION - }, - "credentials": { - THING_ID1: { - "token": TOKEN - }, - THING_ID2: { - "username": USERNAME, - "password": PASSWORD - } - } + "$schema": "./node_modules/@node-wot/cli/dist/wot-servient-schema.conf.json" + ... } - -wot-servient.conf.json fields: - CLIENTONLY : boolean setting if no servers shall be started (default=false) - STATIC : string with hostname or IP literal for static address config - RUNSCRIPT : boolean to activate the 'runScript' Action (default=false) - HPORT : integer defining the HTTP listening port - PROXY : object with "href" field for the proxy URI, - "scheme" field for either "basic" or "bearer", and - corresponding credential fields as defined below - ALLOW : boolean whether self-signed certificates should be allowed - BROKER-URL : URL to an MQTT broker that publisher and subscribers will use - BROKER-UNIQUEID : unique id set by MQTT client while connecting to the broker - MQTT_VERSION : number indicating the MQTT protocol version to be used (3, 4, or 5) - THING_IDx : string with TD "id" for which credentials should be configured - TOKEN : string for providing a Bearer token - USERNAME : string for providing a Basic Auth username - PASSWORD : string for providing a Basic Auth password - --------------------------------------------------------------------------- - -Environment variables must be provided in a .env file in the current working directory. - -Example: -VAR1=Value1 -VAR2=Value2 ``` Additionally, you can look at [the JSON Schema](https://github.com/eclipse-thingweb/node-wot/blob/master/packages/cli/src/wot-servient-schema.conf.json) to understand possible values for each field. @@ -148,7 +113,7 @@ ADDRESS=http://hello.com To debug, use the option `--inspect` or `--inspect-brk` if you want to hang until your debug client is connected. Then start [Chrome Dev Tools](chrome://inspect) or [vscode debugger](https://code.visualstudio.com/docs/nodejs/nodejs-debugging#_attaching-to-nodejs) or your preferred v8 inspector to debug your code. -For further details check: `wot-servient --help` +For further details check: `node-wot --help` ### Examples diff --git a/packages/cli/eslint.config.mjs b/packages/cli/eslint.config.mjs new file mode 100644 index 000000000..fb2f8f1ab --- /dev/null +++ b/packages/cli/eslint.config.mjs @@ -0,0 +1,4 @@ +import { defineConfig, globalIgnores } from "eslint/config"; +import baseConfig from "../../eslint.config.mjs"; + +export default defineConfig([baseConfig, globalIgnores(["src/generated/**.ts", "./import-json.js"])]); diff --git a/packages/cli/import-json-to-ts.js b/packages/cli/import-json-to-ts.js new file mode 100644 index 000000000..20857da9b --- /dev/null +++ b/packages/cli/import-json-to-ts.js @@ -0,0 +1,17 @@ +const { readFileSync, writeFileSync } = require("fs"); +const { existsSync, mkdirSync } = require("fs"); + +const schema = readFileSync("./src/wot-servient-schema.conf.json", "utf8"); +const package = readFileSync("./package.json", "utf8"); +const { version } = JSON.parse(package); + +const generatedDir = "./src/generated"; +if (!existsSync(generatedDir)) { + mkdirSync(generatedDir, { recursive: true }); +} + +writeFileSync( + "./src/generated/wot-servient-schema.conf.ts", + `const schema = ${schema.trimEnd()} as const \nexport default schema;` +); +writeFileSync("./src/generated/version.ts", `const version = "${version}" as const \nexport default version;`); diff --git a/packages/cli/package.json b/packages/cli/package.json index 53d9047e3..4240b3d07 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -14,7 +14,7 @@ "main": "bin/index.js", "types": "dist/cli.d.ts", "bin": { - "wot-servient": "bin/index.js" + "node-wot": "bin/index.js" }, "optionalDependencies": { "ts-node": "10.9.1" @@ -28,18 +28,19 @@ "@thingweb/thing-model": "^1.0.4", "ajv": "^8.11.0", "commander": "^9.1.0", + "debug": "^4.4.0", "dotenv": "^16.4.7", - "lodash": "^4.17.21", - "vm2": "3.9.18" + "lodash": "^4.17.21" }, "scripts": { - "build": "tsc -b", + "build:transform": "node import-json-to-ts.js", + "build": "npm run build:transform && tsc -b", "start": "ts-node src/cli.ts", "debug": "node -r ts-node/register --inspect-brk=9229 src/cli.ts", "lint": "eslint .", "lint:fix": "eslint . --fix", "format": "prettier --write \"src/**/*.ts\" \"**/*.json\"", - "test": "mocha --require ts-node/register --extension ts" + "test": "mocha --recursive --require ts-node/register --extension ts" }, "bugs": { "url": "https://github.com/eclipse-thingweb/node-wot/issues" @@ -47,6 +48,9 @@ "homepage": "https://github.com/eclipse-thingweb/node-wot/tree/master/packages/cli#readme", "keywords": [], "devDependencies": { - "@types/lodash": "^4.14.199" + "@types/lodash": "^4.14.199", + "@types/tmp": "^0.2.6", + "json-schema-to-ts": "^3.1.1", + "tmp": "^0.2.5" } } diff --git a/packages/cli/src/cli-default-servient.ts b/packages/cli/src/cli-default-servient.ts index 0c58007de..997c68182 100644 --- a/packages/cli/src/cli-default-servient.ts +++ b/packages/cli/src/cli-default-servient.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ /******************************************************************************** * Copyright (c) 2018 Contributors to the Eclipse Foundation * @@ -25,97 +24,29 @@ import { HttpServer, HttpClientFactory, HttpsClientFactory } from "@node-wot/bin import { CoapServer, CoapClientFactory, CoapsClientFactory } from "@node-wot/binding-coap"; import { MqttBrokerServer, MqttClientFactory } from "@node-wot/binding-mqtt"; import { FileClientFactory } from "@node-wot/binding-file"; -import { CompilerFunction, NodeVM } from "vm2"; -import { ThingModelHelpers } from "@thingweb/thing-model"; +import { LogLevel, setLogLevel } from "./utils"; +import { ConfigurationAfterDefaults } from "./configuration"; -const { debug, error, info } = createLoggers("cli", "cli-default-servient"); +const { debug, info } = createLoggers("cli", "cli-default-servient"); -// Helper function needed for `mergeConfigs` function -function isObject(item: unknown) { - return item != null && typeof item === "object" && !Array.isArray(item); -} - -/** - * Helper function merging default parameters into a custom config file. - * - * @param {object} target - an object containing default config parameters - * @param {object} source - an object containing custom config parameters - * - * @return {object} The new config file containing both custom and default parameters - */ -function mergeConfigs(target: any, source: any): any { - const output = Object.assign({}, target); - Object.keys(source).forEach((key) => { - if (!(key in target)) { - Object.assign(output, { [key]: source[key] }); - } else { - if (isObject(target[key]) && isObject(source[key])) { - output[key] = mergeConfigs(target[key], source[key]); - } else { - Object.assign(output, { [key]: source[key] }); - } - } - }); - return output; -} -export interface ScriptOptions { - argv?: Array; - compiler?: CompilerFunction; - env?: Record; -} export default class DefaultServient extends Servient { - private static readonly defaultConfig = { - servient: { - clientOnly: false, - scriptAction: false, - }, - http: { - port: 8080, - allowSelfSigned: false, - }, - coap: { - port: 5683, - }, - log: { - level: "info", - }, - }; - private uncaughtListeners: Array = []; - private runtime: typeof WoT | undefined; - public readonly config: any; + public readonly config: ConfigurationAfterDefaults; // current log level public logLevel = "info"; - public constructor(clientOnly: boolean, config?: any) { + public constructor(config: ConfigurationAfterDefaults) { super(); - // init config - this.config = - typeof config === "object" - ? mergeConfigs(DefaultServient.defaultConfig, config) - : DefaultServient.defaultConfig; - - // apply flags - if (clientOnly) { - this.config.servient ??= {}; - this.config.servient.clientOnly = true; - } - - // set log level before any output - this.setLogLevel(this.config.log.level); + this.config = config; // load credentials from config this.addCredentials(this.config.credentials); - // remove secrets from original for displaying config (already added) - if (this.config.credentials != null) { - delete this.config.credentials; - } - // display debug("DefaultServient configured with"); - debug(`${this.config}`); + // remove secrets from original for displaying config + debug(`%O`, { ...this.config, credentials: null }); // apply config if (typeof this.config.servient.staticAddress === "string") { @@ -160,188 +91,59 @@ export default class DefaultServient extends Servient { } /** - * Runs the script in a new sandbox - * @param {string} code - the script to run - * @param {string} filename - the filename of the script - */ - public runScript(code: string, filename = "script"): unknown { - if (!this.runtime) { - throw new Error("WoT runtime not loaded; have you called start()?"); - } - const helpers = new Helpers(this); - const context = { - WoT: this.runtime, - WoTHelpers: helpers, - ModelHelpers: new ThingModelHelpers(helpers), - }; - - const vm = new NodeVM({ - sandbox: context, - }); - - const listener = (err: Error) => { - this.logScriptError(`Asynchronous script error '${filename}'`, err); - // TODO: clean up script resources - process.exit(1); - }; - process.prependListener("uncaughtException", listener); - this.uncaughtListeners.push(listener); - - try { - return vm.run(code, filename); - } catch (err) { - if (err instanceof Error) { - this.logScriptError(`Servient found error in script '${filename}'`, err); - } else { - error(`Servient found error in script '${filename}' ${err}`); - } - return undefined; - } - } - - /** - * Runs the script in privileged context (dangerous). In practice, this means that the script can - * require system modules. - * @param {string} code - the script to run - * @param {string} filename - the filename of the script - * @param {object} options - pass cli variables or envs to the script + * start */ - public runPrivilegedScript(code: string, filename = "script", options: ScriptOptions = {}): unknown { - if (!this.runtime) { - throw new Error("WoT runtime not loaded; have you called start()?"); - } - const helpers = new Helpers(this); - const context = { - WoT: this.runtime, - WoTHelpers: helpers, - ModelHelpers: new ThingModelHelpers(helpers), - }; - - const vm = new NodeVM({ - sandbox: context, - require: { - external: true, - builtin: ["*"], + public async start(): Promise { + const superWoT = await super.start(); + + info("DefaultServient started"); + + const servientProducedThing = await superWoT.produce({ + title: "servient", + description: "node-wot CLI Servient", + properties: { + things: { + type: "object", + description: "Get things", + observable: false, + readOnly: true, + }, + }, + actions: { + setLogLevel: { + description: "Set log level", + input: { + type: "string", + enum: ["debug", "info", "warn", "error"], + }, + output: { type: "string" }, + }, + shutdown: { + description: "Stop servient", + output: { type: "string" }, + }, }, - argv: options.argv, - compiler: options.compiler, - env: options.env, }); - const listener = (err: Error) => { - this.logScriptError(`Asynchronous script error '${filename}'`, err); - // TODO: clean up script resources - process.exit(1); - }; - process.prependListener("uncaughtException", listener); - this.uncaughtListeners.push(listener); - - try { - return vm.run(code, filename); - } catch (err) { - if (err instanceof Error) { - this.logScriptError(`Servient found error in privileged script '${filename}'`, err); - } else { - error(`Servient found error in privileged script '${filename}' ${err}`); - } + servientProducedThing.setActionHandler("setLogLevel", async (payload) => { + const level = (await Helpers.parseInteractionOutput(payload)) as LogLevel; + setLogLevel(level); + this.logLevel = level; + return `Log level set to '${this.logLevel}'`; + }); + servientProducedThing.setActionHandler("shutdown", async () => { + debug("shutting down by remote"); + await this.shutdown(); return undefined; - } - } + }); + servientProducedThing.setPropertyReadHandler("things", async () => { + debug("returning things"); + return this.getThings(); + }); - private logScriptError(description: string, err: Error): void { - let message: string; - if (typeof err === "object" && err.stack != null) { - const match = err.stack.match(/evalmachine\.:([0-9]+:[0-9]+)/); - if (Array.isArray(match)) { - message = `and halted at line ${match[1]}\n ${err}`; - } else { - message = `and halted with ${err.stack}`; - } - } else { - message = `that threw ${typeof err} instead of Error\n ${err}`; - } - error(`Servient caught ${description} ${message}`); - } + await servientProducedThing.expose(); - /** - * start - */ - public start(): Promise { - return new Promise((resolve, reject) => { - super - .start() - .then((myWoT) => { - info("DefaultServient started"); - this.runtime = myWoT; - // TODO think about builder pattern that starts with produce() ends with expose(), which exposes/publishes the Thing - myWoT - .produce({ - title: "servient", - description: "node-wot CLI Servient", - properties: { - things: { - type: "object", - description: "Get things", - observable: false, - readOnly: true, - }, - }, - actions: { - setLogLevel: { - description: "Set log level", - input: { oneOf: [{ type: "string" }, { type: "number" }] }, - output: { type: "string" }, - }, - shutdown: { - description: "Stop servient", - output: { type: "string" }, - }, - runScript: { - description: "Run script", - input: { type: "string" }, - output: { type: "string" }, - }, - }, - }) - .then((thing) => { - thing.setActionHandler("setLogLevel", async (level) => { - const ll = await Helpers.parseInteractionOutput(level); - if (typeof ll === "number") { - this.setLogLevel(ll as number); - } else if (typeof ll === "string") { - this.setLogLevel(ll as string); - } else { - // try to convert it to strings - this.setLogLevel(ll + ""); - } - return `Log level set to '${this.logLevel}'`; - }); - thing.setActionHandler("shutdown", async () => { - debug("shutting down by remote"); - await this.shutdown(); - return undefined; - }); - thing.setActionHandler("runScript", async (script) => { - const scriptv = await Helpers.parseInteractionOutput(script); - debug("running script", scriptv); - this.runScript(scriptv as string); - return undefined; - }); - thing.setPropertyReadHandler("things", async () => { - debug("returning things"); - return this.getThings(); - }); - thing - .expose() - .then(() => { - // pass on WoTFactory - resolve(myWoT); - }) - .catch((err) => reject(err)); - }); - }) - .catch((err) => reject(err)); - }); + return superWoT; } public async shutdown(): Promise { @@ -351,60 +153,4 @@ export default class DefaultServient extends Servient { process.removeListener("uncaughtException", listener); }); } - - // Save default loggers (needed when changing log levels) - private readonly loggers: any = { - warn: console.warn, - info: console.info, - debug: console.debug, - }; - - private setLogLevel(logLevel: string | number): void { - if (logLevel === "error" || logLevel === 0) { - console.warn = () => { - /* nothing */ - }; - console.info = () => { - /* nothing */ - }; - console.debug = () => { - /* nothing */ - }; - - this.logLevel = "error"; - } else if (logLevel === "warn" || logLevel === "warning" || logLevel === 1) { - console.warn = this.loggers.warn; - console.info = () => { - /* nothing */ - }; - console.debug = () => { - /* nothing */ - }; - - this.logLevel = "warn"; - } else if (logLevel === "info" || logLevel === 2) { - console.warn = this.loggers.warn; - console.info = this.loggers.info; - console.debug = () => { - /* nothing */ - }; - - this.logLevel = "info"; - } else if (logLevel === "debug" || logLevel === 3) { - console.warn = this.loggers.warn; - console.info = this.loggers.info; - console.debug = this.loggers.debug; - - this.logLevel = "debug"; - } else { - // Fallback to default ("info") - console.warn = this.loggers.warn; - console.info = this.loggers.info; - console.debug = () => { - /* nothing */ - }; - - this.logLevel = "info"; - } - } } diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index b4adf1efc..a2e979339 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -15,33 +15,33 @@ // default implementation of W3C WoT Servient (http(s) and file bindings) import DefaultServient from "./cli-default-servient"; -import ErrnoException = NodeJS.ErrnoException; // tools -import fs = require("fs"); -import * as dotenv from "dotenv"; import * as path from "path"; -import { Command, InvalidArgumentError, Argument } from "commander"; -import Ajv, { ValidateFunction, ErrorObject } from "ajv"; -import ConfigSchema from "./wot-servient-schema.conf.json"; -import _ from "lodash"; -import { version } from "@node-wot/core/package.json"; -import { createLoggers } from "@node-wot/core"; -import inspector from "inspector"; - -const { error, info, warn } = createLoggers("cli", "cli"); +import { Command, Argument, Option } from "commander"; +import Ajv, { ValidateFunction } from "ajv"; +import ConfigSchema from "./generated/wot-servient-schema.conf"; +import version from "./generated/version"; +import { createLoggers, Helpers } from "@node-wot/core"; +import { loadEnvVariables } from "./utils"; +import { runScripts } from "./script-runner"; +import { readdir } from "fs/promises"; +import { parseConfigFile, parseConfigParams } from "./parsers"; +import { setLogLevel } from "./utils/set-log-level"; +import { buildConfig, buildConfigFromFile, Configuration, defaultConfiguration } from "./configuration"; +import { cloneDeep } from "lodash"; + +const { error, info, warn, debug } = createLoggers("cli", "cli"); const program = new Command(); -const ajv = new Ajv({ strict: true }); -const schemaValidator = ajv.compile(ConfigSchema) as ValidateFunction; +const ajv = new Ajv({ strict: true, allErrors: true }); +const schemaValidator = ajv.compile(ConfigSchema) as ValidateFunction; const defaultFile = "wot-servient.conf.json"; const baseDir = "."; -const dotEnvConfigParameters: DotEnvConfigParameter = {}; - // General commands program - .name("wot-servient") + .name("node-wot") .description( ` Run a WoT Servient in the current directory. @@ -54,148 +54,45 @@ Run a WoT Servient in the current directory. program.addHelpText( "after", ` -wot-servient.conf.json syntax: -{ - "servient": { - "clientOnly": CLIENTONLY, - "staticAddress": STATIC, - "scriptAction": RUNSCRIPT - }, - "http": { - "port": HPORT, - "address": HADDRESS, - "baseUri": HBASEURI, - "urlRewrite": HURLREWRITE, - "proxy": PROXY, - "allowSelfSigned": ALLOW - }, - "mqtt" : { - "broker": BROKER-URL, - "username": BROKER-USERNAME, - "password": BROKER-PASSWORD, - "clientId": BROKER-UNIQUEID, - "protocolVersion": MQTT_VERSION - }, - "credentials": { - THING_ID1: { - "token": TOKEN - }, - THING_ID2: { - "username": USERNAME, - "password": PASSWORD - } - } -} +Configuration -wot-servient.conf.json fields: - CLIENTONLY : boolean setting if no servers shall be started (default=false) - STATIC : string with hostname or IP literal for static address config - RUNSCRIPT : boolean to activate the 'runScript' Action (default=false) - HPORT : integer defining the HTTP listening port - HADDRESS : string defining HTTP address - HBASEURI : string defining HTTP base URI - HURLREWRITE : map (from URL -> to URL) defining HTTP URL rewrites - PROXY : object with "href" field for the proxy URI, - "scheme" field for either "basic" or "bearer", and - corresponding credential fields as defined below - ALLOW : boolean whether self-signed certificates should be allowed - BROKER-URL : URL to an MQTT broker that publisher and subscribers will use - BROKER-UNIQUEID : unique id set by MQTT client while connecting to the broker - MQTT_VERSION : number indicating the MQTT protocol version to be used (3, 4, or 5) - THING_IDx : string with TD "id" for which credentials should be configured - TOKEN : string for providing a Bearer token - USERNAME : string for providing a Basic Auth username - PASSWORD : string for providing a Basic Auth password - --------------------------------------------------------------------------- +Settings can be applied through three methods, in order of precedence (highest to lowest): -Environment variables must be provided in a .env file in the current working directory. +1. Command-Line Parameters (-p path.to.set=value) +2. Environment Variables (NODE_WOT_PATH_TO_SET=value) (supports .env files too) +3. Configuration File -Example: -VAR1=Value1 -VAR2=Value2` -); +For the complete list of available configuration fields and their data types, run: -// Typings -type DotEnvConfigParameter = { - [key: string]: unknown; -}; -interface DebugParams { - shouldBreak: boolean; - host: string; - port: number; -} +node-wot schema -// Parsers & validators -function parseIp(value: string, previous: string) { - if (!/^([a-z]*|[\d.]*)(:[0-9]{2,5})?$/.test(value)) { - throw new InvalidArgumentError("Invalid host:port combo"); - } +In your configuration files you can the following to enable IDE config validation: - return value; -} -function parseConfigFile(filename: string, previous: string) { - try { - const open = filename || path.join(baseDir, defaultFile); - const data = fs.readFileSync(open, "utf-8"); - if (!schemaValidator(JSON.parse(data))) { - throw new InvalidArgumentError( - `Config file contains invalid an JSON: ${(schemaValidator.errors ?? []) - .map((o: ErrorObject) => o.message) - .join("\n")}` - ); - } - return filename; - } catch (err) { - throw new InvalidArgumentError(`Error reading config file: ${err}`); - } -} -function parseConfigParams(param: string, previous: unknown) { - // Validate key-value pair - if (!/^([a-zA-Z0-9_.]+):=([a-zA-Z0-9_]+)$/.test(param)) { - throw new InvalidArgumentError("Invalid key-value pair"); - } - const fieldNamePath = param.split(":=")[0]; - const fieldNameValue = param.split(":=")[1]; - let fieldNameValueCast; - if (Number(fieldNameValue)) { - fieldNameValueCast = +fieldNameValue; - } else if (fieldNameValue === "true" || fieldNameValue === "false") { - fieldNameValueCast = Boolean(fieldNameValue); - } else { - fieldNameValueCast = fieldNamePath; - } - - // Build object using dot-notation JSON path - const obj = _.set({}, fieldNamePath, fieldNameValueCast); - if (!schemaValidator(obj)) { - throw new InvalidArgumentError( - `Config parameter '${param}' is not valid: ${(schemaValidator.errors ?? []) - .map((o: ErrorObject) => o.message) - .join("\n")}` - ); - } - // Concatenate validated parameters - let result = previous ?? {}; - result = _.merge(result, obj); - return result; +{ + "$schema": "./node_modules/@node-wot/cli/dist/wot-servient-schema.conf.json" + ... } + ` +); // CLI options declaration program - .option("-i, --inspect [host]:[port]", "activate inspector on host:port (default: 127.0.0.1:9229)", parseIp) - .option("-ib, --inspect-brk [host]:[port]", "activate inspector on host:port (default: 127.0.0.1:9229)", parseIp) .option("-c, --client-only", "do not start any servers (enables multiple instances without port conflicts)") - .option("-cp, --compiler ", "load module as a compiler") + .addOption( + new Option( + "-ll, --logLevel ", + "choose the desired log level. WARNING: if DEBUG env variable is specified this option gets overridden." + ).choices(["debug", "info", "warn", "error"]) + ) .option( "-f, --config-file ", - "load configuration from specified file", - parseConfigFile, - "wot-servient.conf.json" + "load configuration from specified file (default: $(pwd)/wot-servient.conf.json)", + (value, previous) => parseConfigFile(value, previous) ) .option( "-p, --config-params ", "override configuration parameters [key1:=value1 key2:=value2 ...] (e.g. http.port:=8080)", - parseConfigParams + (value, previous) => parseConfigParams(value, previous, schemaValidator) ); // CLI arguments @@ -206,189 +103,63 @@ program.addArgument( ) ); -program.parse(process.argv); -const options = program.opts(); -const args = program.args; +program + .command("schema") + .description("prints the json schema for the configuration file") + .action(() => { + // eslint-disable-next-line no-console + console.log(JSON.stringify(ConfigSchema, null, 2)); + }); -// .env parsing -const env: dotenv.DotenvConfigOutput = dotenv.config(); -const errorNoException: ErrnoException | undefined = env.error; -if (errorNoException?.code !== "ENOENT") { - throw env.error; -} else if (env.parsed) { - for (const [key, value] of Object.entries(env.parsed)) { - // Parse and validate on configfile-related entries - if (key.startsWith("config.")) { - dotEnvConfigParameters[key.replace("config.", "")] = value; - } +program.action(async function (_, options, cmd) { + // Allow user to personalized the env + if (process.env.DEBUG == null) { + // by default enable error logs and warnings + // user can override using command line option + // or later by config file. + setLogLevel(options.logLevel ?? "warn"); } -} -// Functions -async function buildConfig(): Promise { - const fileToOpen = options?.configFile ?? path.join(baseDir, defaultFile); - let configFileData = {}; + const args = cmd.args; + const env = loadEnvVariables(); + const defaultFilePath = path.join(baseDir, defaultFile); + let servient: DefaultServient; + + debug("command line options %O", options); + debug("command line arguments %O", args); + debug("command line environment variables", args); - // JSON config file try { - configFileData = JSON.parse(await fs.promises.readFile(fileToOpen, "utf-8")); + const config = await buildConfigFromFile(options, defaultFilePath, env, schemaValidator); + setLogLevel(options.logLevel ?? config.log.level); + config.servient.clientOnly = options.clientOnly ?? config.servient.clientOnly; + servient = new DefaultServient(config); } catch (err) { - error(`WoT-Servient config file error: ${err}`); - } - - // .env file - for (const [key, value] of Object.entries(dotEnvConfigParameters)) { - const obj = _.set({}, key, value); - configFileData = _.merge(configFileData, obj); - } - - // CLI arguments - if (options?.configParams != null) { - configFileData = _.merge(configFileData, options.configParams); - } - - return configFileData; -} -const loadCompilerFunction = function (compilerModule: string | undefined) { - if (compilerModule != null) { - const compilerMod = require(compilerModule); - - if (compilerMod.create == null) { - throw new Error("No create function defined for " + compilerModule); + if ((err as NodeJS.ErrnoException)?.code !== "ENOENT" || options.configFile != null) { + error("node-wot configuration file error:\n%O\nClose.", err); + process.exit((err as NodeJS.ErrnoException).errno ?? 1); } - const compilerObject = compilerMod.create(); + warn(`node-wot using defaults as %s does not exist`, defaultFile); - if (compilerObject.compile == null) { - throw new Error("No compile function defined for create return object"); - } - return compilerObject.compile; - } - return undefined; -}; -const loadEnvVariables = function () { - const env: dotenv.DotenvConfigOutput = dotenv.config(); - - const errorNoException: ErrnoException | undefined = env.error; - // ignore file not found but throw otherwise - if (errorNoException?.code !== "ENOENT") { - throw env.error; + const config = await buildConfig(options, cloneDeep(defaultConfiguration), env, schemaValidator); + config.servient.clientOnly = options.clientOnly ?? config.servient.clientOnly; + servient = new DefaultServient(config); } - return env; -}; - -const runScripts = async function (servient: DefaultServient, scripts: Array, debug?: DebugParams) { - const env = loadEnvVariables(); - - const launchScripts = (scripts: Array) => { - const compile = loadCompilerFunction(options.compiler); - scripts.forEach((fname: string) => { - info(`WoT-Servient reading script ${fname}`); - fs.readFile(fname, "utf8", (err, data) => { - if (err) { - error(`WoT-Servient experienced error while reading script. ${err}`); - } else { - // limit printout to first line - info( - `WoT-Servient running script '${data.substr(0, data.indexOf("\n")).replace("\r", "")}'... (${ - data.split(/\r\n|\r|\n/).length - } lines)` - ); - fname = path.resolve(fname); - servient.runPrivilegedScript(data, fname, { - argv: args, - env: env.parsed, - compiler: compile, - }); - } - }); - }); - }; + const runtime = await servient.start(); + const helpers = new Helpers(servient); - if (debug && debug.shouldBreak) { - // Activate inspector only if is not already opened and wait for the debugger to attach - inspector.url() == null && inspector.open(debug.port, debug.host, true); - - // Set a breakpoint at the first line of of first script - // the breakpoint gives time to inspector clients to set their breakpoints - const session = new inspector.Session(); - session.connect(); - session.post("Debugger.enable", (error: Error) => { - if (error != null) { - warn("Cannot set breakpoint; reason: cannot enable debugger"); - warn(error.toString()); - } - - session.post( - "Debugger.setBreakpointByUrl", - { - lineNumber: 0, - url: "file:///" + path.resolve(scripts[0]).replace(/\\/g, "/"), - }, - (err: Error | null) => { - if (err != null) { - warn("Cannot set breakpoint"); - warn(error.toString()); - } - launchScripts(scripts); - } - ); - }); - } else { - // Activate inspector only if is not already opened and don't wait - debug != null && inspector.url() == null && inspector.open(debug.port, debug.host, false); - launchScripts(scripts); + if (args.length > 0) { + return runScripts({ runtime, helpers }, args, options.inspect ?? options.inspectBrk); } -}; -const runAllScripts = function (servient: DefaultServient, debug?: DebugParams) { - fs.readdir(baseDir, (err, files) => { - if (err) { - warn(`WoT-Servient experienced error while loading directory. ${err}`); - return; - } + const files = await readdir(baseDir); + const scripts = files.filter((file) => !file.startsWith(".") && file.slice(-3) === ".js"); - // unhidden .js files - const scripts = files.filter((file) => { - return file.substr(0, 1) !== "." && file.slice(-3) === ".js"; - }); - info(`WoT-Servient using current directory with ${scripts.length} script${scripts.length > 1 ? "s" : ""}`); + info(`node-wot using current directory with %d script${scripts.length > 1 ? "s" : ""}`, scripts.length); - runScripts( - servient, - scripts.map((filename) => path.resolve(path.join(baseDir, filename))), - debug - ); - }); -}; + return runScripts({ runtime, helpers }, scripts, options.inspect ?? options.inspectBrk); +}); -buildConfig() - .then((conf) => { - return new DefaultServient(options.clientOnly, conf); - }) - .catch((err) => { - if (err.code === "ENOENT" && options.configFile == null) { - warn(`WoT-Servient using defaults as '${defaultFile}' does not exist`); - return new DefaultServient(options.clientOnly); - } else { - error(`"WoT-Servient config file error. ${err}`); - process.exit(err.errno); - } - }) - .then((servient) => { - servient - .start() - .then(() => { - if (args.length > 0) { - info(`WoT-Servient loading ${args.length} command line script${args.length > 1 ? "s" : ""}`); - return runScripts(servient, args, options.inspect ?? options.inspectBrk); - } else { - return runAllScripts(servient, options.inspect ?? options.inspectBrk); - } - }) - .catch((err) => { - error(`WoT-Servient cannot start. ${err}`); - }); - }) - .catch((err) => error(`WoT-Servient main error. ${err}`)); +program.parse(process.argv); diff --git a/packages/cli/src/configuration.ts b/packages/cli/src/configuration.ts new file mode 100644 index 000000000..419e29d9d --- /dev/null +++ b/packages/cli/src/configuration.ts @@ -0,0 +1,132 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the W3C Software Notice and + * Document License (2015-05-13) which is available at + * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document. + * + * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513 + ********************************************************************************/ + +import { FromSchema } from "json-schema-to-ts"; +import schema from "./generated/wot-servient-schema.conf"; +import { DotenvParseOutput } from "dotenv"; +import _ from "lodash"; +import { readFile } from "fs/promises"; +import { ValidateFunction, ValidationError } from "ajv"; +import { stringToJSValue } from "./utils"; +import { createLoggers } from "@node-wot/core"; + +const { debug } = createLoggers("cli", "cli-default-servient"); + +type Merge = { [K in keyof T as K extends keyof U ? never : K]: T[K] } & { + [L in keyof U & keyof T]: Merge; +} & { [J in keyof U as J extends keyof T ? never : J]: U[J] }; + +type Mutable = { -readonly [K in keyof T]: Mutable }; +type Generalize = T extends number + ? number + : T extends string + ? string + : T extends boolean + ? boolean + : T extends Array + ? Array> + : T extends object + ? { [K in keyof T]: Generalize } + : T; +export type Configuration = FromSchema; + +export const defaultConfiguration = Object.freeze({ + servient: { + clientOnly: false, + }, + http: { + port: 8080, + allowSelfSigned: false, + }, + coap: { + port: 5683, + }, + credentials: {}, + log: { level: "warn" }, +} as const satisfies Configuration); + +export type ConfigurationAfterDefaults = Merge>>; + +/** + * Helper function to convert an ENV key to a camelCased path + * using the schema as reference (e.g., SERVIENT_STATICADDRESS -> servient.staticAddress) + */ +function envKeyToConfigPath(envKey: string): string { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let properties: { [x: string]: any } = schema.properties; + const pathParts = envKey.toLowerCase().replace(/__/g, ".").replace(/_/g, ".").split("."); + let path = ""; + for (const part of pathParts) { + const matchedProperty = Object.keys(properties).find((prop) => prop.toLowerCase() === part); + if (matchedProperty != null) { + path += (path.length > 0 ? "." : "") + matchedProperty; + const nextProperty = properties[matchedProperty as keyof typeof properties]; + if ("properties" in nextProperty) { + properties = nextProperty.properties; + } + } else { + // If no matching property is found, append the original part we are going to catch + // errors in the validation phase later + path += (path.length > 0 ? "." : "") + part; + } + } + return path; +} + +export async function buildConfig( + options: Record, + configuration: Configuration, + dotEnvConfigParameters: DotenvParseOutput, + validator: ValidateFunction +): Promise { + let config = configuration; + + for (const [key, value] of Object.entries(dotEnvConfigParameters)) { + debug("Applying env variable %s=%s", key, value); + const path = envKeyToConfigPath(key); + debug("Mapped to config path %s", path); + const obj = _.set({}, path, stringToJSValue(value)); + config = _.merge(config, obj); + } + + if (options?.configParams != null) { + config = _.merge(config, options.configParams); + } + + config = _.merge({}, defaultConfiguration, config); + + if (!validator(config)) { + throw new ValidationError(validator.errors ?? []); + } + + return config as ConfigurationAfterDefaults; +} + +export async function buildConfigFromFile( + options: Record, + defaultFile: string, + dotEnvConfigParameters: DotenvParseOutput, + validator: ValidateFunction +): Promise { + let fileToOpen = defaultFile; + + if (typeof options.configFile === "string") { + fileToOpen = options.configFile; + } + + const configFileData = JSON.parse(await readFile(fileToOpen, "utf-8")); + + return buildConfig(options, configFileData, dotEnvConfigParameters, validator); +} diff --git a/packages/cli/src/executor.ts b/packages/cli/src/executor.ts new file mode 100644 index 000000000..1ec6863d5 --- /dev/null +++ b/packages/cli/src/executor.ts @@ -0,0 +1,51 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the W3C Software Notice and + * Document License (2015-05-13) which is available at + * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document. + * + * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513 + ********************************************************************************/ + +import { createLoggers, Helpers } from "@node-wot/core"; +const { debug } = createLoggers("cli", "executor"); + +export interface WoTContext { + runtime: typeof WoT; + helpers: Helpers; +} + +export class Executor { + public async exec(file: string, wotContext: WoTContext): Promise { + debug(`Executing WoT script from file: ${file}`); + const userScriptPathArg = file; + const isTypeScriptScript = + userScriptPathArg && (userScriptPathArg.endsWith(".ts") || userScriptPathArg.endsWith(".tsx")); + global.WoT = wotContext.runtime; + + if (isTypeScriptScript === true) { + require("ts-node/register"); + } + + try { + // Execute the user's script + // Node.js will now handle .ts files automatically if ts-node is registered + // TODO: For ESM modules a more complex check might be needed. + if (file.endsWith(".mjs")) { + return await import(`file:///${file}`); + } else { + return require(file); + } + } catch (error) { + // eslint-disable-next-line no-console + console.error("Error running WoT script:", error); + process.exit(1); + } + } +} diff --git a/packages/cli/src/parsers/config-file-parser.ts b/packages/cli/src/parsers/config-file-parser.ts new file mode 100644 index 000000000..6979cbbc9 --- /dev/null +++ b/packages/cli/src/parsers/config-file-parser.ts @@ -0,0 +1,31 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the W3C Software Notice and + * Document License (2015-05-13) which is available at + * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document. + * + * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513 + ********************************************************************************/ + +import { InvalidArgumentError } from "commander"; +import { readFileSync } from "fs"; + +export function parseConfigFile(filename: string, previous: unknown) { + try { + const open = filename; + const data = readFileSync(open, "utf-8"); + JSON.parse(data); + return filename; + } catch (err) { + if (err instanceof InvalidArgumentError) { + throw err; + } + throw new InvalidArgumentError(`Error reading config file: ${err}`); + } +} diff --git a/packages/cli/src/parsers/config-params-parser.ts b/packages/cli/src/parsers/config-params-parser.ts new file mode 100644 index 000000000..e6e70de5d --- /dev/null +++ b/packages/cli/src/parsers/config-params-parser.ts @@ -0,0 +1,41 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the W3C Software Notice and + * Document License (2015-05-13) which is available at + * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document. + * + * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513 + ********************************************************************************/ +import { ErrorObject, ValidateFunction } from "ajv"; +import { InvalidArgumentError } from "commander"; +import _ from "lodash"; +import { stringToJSValue } from "../utils"; + +export function parseConfigParams(param: string, previous: unknown, validator: ValidateFunction) { + // Validate key-value pair + if (!/^([a-zA-Z0-9_.]+):=([a-zA-Z0-9_]+)$/.test(param)) { + throw new InvalidArgumentError("Invalid key-value pair"); + } + const fieldNamePath = param.split(":=")[0]; + const fieldNameValue = stringToJSValue(param.split(":=")[1]); + + // Build object using dot-notation JSON path + const obj = _.set({}, fieldNamePath, fieldNameValue); + if (!validator(obj)) { + throw new InvalidArgumentError( + `Config parameter '${param}' is not valid: ${(validator.errors ?? []) + .map((o: ErrorObject) => o.message) + .join("\n")}` + ); + } + // Concatenate validated parameters + let result = previous ?? {}; + result = _.merge(result, obj); + return result; +} diff --git a/packages/cli/src/parsers/index.ts b/packages/cli/src/parsers/index.ts new file mode 100644 index 000000000..4f13af99e --- /dev/null +++ b/packages/cli/src/parsers/index.ts @@ -0,0 +1,16 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + *s + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the W3C Software Notice and + * Document License (2015-05-13) which is available at + * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document. + * + * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513 + ********************************************************************************/ +export * from "./config-file-parser"; +export * from "./config-params-parser"; diff --git a/packages/cli/src/script-runner.ts b/packages/cli/src/script-runner.ts new file mode 100644 index 000000000..5fa597de2 --- /dev/null +++ b/packages/cli/src/script-runner.ts @@ -0,0 +1,84 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the W3C Software Notice and + * Document License (2015-05-13) which is available at + * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document. + * + * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513 + ********************************************************************************/ +import { createLoggers } from "@node-wot/core"; +import inspector from "inspector"; +import path from "path"; +import { readFile } from "fs/promises"; +import { Executor, WoTContext } from "./executor"; + +const { error, info, warn } = createLoggers("cli", "cli", "script-runner"); + +export interface DebugParams { + shouldBreak: boolean; + host: string; + port: number; +} +export async function runScripts(context: WoTContext, scripts: string[], debug?: DebugParams) { + const executor = new Executor(); + const launchScripts = (scripts: Array) => { + scripts.forEach(async (fname: string) => { + info(`node-wot reading script ${fname}`); + try { + const data = await readFile(fname, "utf-8"); + // limit printout to first line + info( + `node-wot running script '${data.substr(0, data.indexOf("\n")).replace("\r", "")}'... (${ + data.split(/\r\n|\r|\n/).length + } lines)` + ); + + fname = path.resolve(fname); + await executor.exec(fname, context); + } catch (err) { + error(`node-wot experienced error while reading script. %O`, err); + } + }); + }; + + if (debug && debug.shouldBreak) { + // Activate inspector only if is not already opened and wait for the debugger to attach + inspector.url() == null && inspector.open(debug.port, debug.host, true); + + // Set a breakpoint at the first line of of first script + // the breakpoint gives time to inspector clients to set their breakpoints + const session = new inspector.Session(); + session.connect(); + session.post("Debugger.enable", (error: Error) => { + if (error != null) { + warn("Cannot set breakpoint; reason: cannot enable debugger"); + warn(error.toString()); + } + + session.post( + "Debugger.setBreakpointByUrl", + { + lineNumber: 0, + url: "file:///" + path.resolve(scripts[0]).replace(/\\/g, "/"), + }, + (err: Error | null) => { + if (err != null) { + warn("Cannot set breakpoint"); + warn(error.toString()); + } + launchScripts(scripts); + } + ); + }); + } else { + // Activate inspector only if is not already opened and don't wait + debug != null && inspector.url() == null && inspector.open(debug.port, debug.host, false); + launchScripts(scripts); + } +} diff --git a/packages/cli/src/utils/index.ts b/packages/cli/src/utils/index.ts new file mode 100644 index 000000000..0882eaf98 --- /dev/null +++ b/packages/cli/src/utils/index.ts @@ -0,0 +1,17 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the W3C Software Notice and + * Document License (2015-05-13) which is available at + * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document. + * + * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513 + ********************************************************************************/ +export * from "./load-env-variables"; +export * from "./set-log-level"; +export * from "./string-to-js-value"; diff --git a/packages/cli/src/utils/load-env-variables.ts b/packages/cli/src/utils/load-env-variables.ts new file mode 100644 index 000000000..8c23e06f2 --- /dev/null +++ b/packages/cli/src/utils/load-env-variables.ts @@ -0,0 +1,34 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the W3C Software Notice and + * Document License (2015-05-13) which is available at + * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document. + * + * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513 + ********************************************************************************/ +import * as dotenv from "dotenv"; +import ErrnoException = NodeJS.ErrnoException; + +export function loadEnvVariables(prefix: string = "WOT_SERVIENT_"): { [key: string]: string } { + const env: dotenv.DotenvConfigOutput = dotenv.config(); + const errornoException: ErrnoException | undefined = env.error; + // ignore file not found but throw otherwise + if (errornoException != null && errornoException.code !== "ENOENT") { + throw env.error; + } + + // Filter out not node-wot related variables + return Object.keys(process.env) + .filter((key) => key.startsWith(prefix)) + .reduce((obj: { [key: string]: string }, key: string) => { + const shortKey = key.substring(prefix.length); + obj[shortKey] = process.env[key] as string; + return obj; + }, {}); +} diff --git a/packages/cli/src/utils/set-log-level.ts b/packages/cli/src/utils/set-log-level.ts new file mode 100644 index 000000000..877985cf3 --- /dev/null +++ b/packages/cli/src/utils/set-log-level.ts @@ -0,0 +1,35 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the W3C Software Notice and + * Document License (2015-05-13) which is available at + * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document. + * + * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513 + ********************************************************************************/ +import { createLoggers } from "@node-wot/core"; +import * as logger from "debug"; + +export type LogLevel = keyof ReturnType; +export function setLogLevel(level: LogLevel): void { + logger.disable(); + switch (level) { + case "debug": + logger.enable("node-wot:*"); + break; + case "info": + logger.enable("node-wot:**:error,node-wot:**:warn,node-wot:**:info"); + break; + case "warn": + logger.enable("node-wot:**:error,node-wot:**:warn"); + break; + case "error": + logger.enable("node-wot:**:error"); + break; + } +} diff --git a/packages/cli/src/utils/string-to-js-value.ts b/packages/cli/src/utils/string-to-js-value.ts new file mode 100644 index 000000000..2288ef88f --- /dev/null +++ b/packages/cli/src/utils/string-to-js-value.ts @@ -0,0 +1,31 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the W3C Software Notice and + * Document License (2015-05-13) which is available at + * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document. + * + * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513 + ********************************************************************************/ + +/** + * Converts a string to a Number, Boolean, or return the original string. + * Useful for parsing envs or CLI arguments. + * + * @param value - The string value to convert. + * @returns The converted value as Number, Boolean, or String. + */ +export function stringToJSValue(value: string) { + if (!isNaN(Number(value))) { + return +value; + } else if (value === "true" || value === "false") { + return value === "true"; + } else { + return value; + } +} diff --git a/packages/cli/src/wot-servient-schema.conf.json b/packages/cli/src/wot-servient-schema.conf.json index 95123eb04..91405d250 100644 --- a/packages/cli/src/wot-servient-schema.conf.json +++ b/packages/cli/src/wot-servient-schema.conf.json @@ -3,21 +3,21 @@ "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { + "$schema": { "type": "string" }, "servient": { "type": "object", "properties": { "clientOnly": { + "description": "setting if no servers shall be started", "type": "boolean", "default": false }, "staticAddress": { - "type": "string" - }, - "scriptAction": { - "type": "boolean", - "default": false + "type": "string", + "description": "hostname or IP literal for static address config" } - } + }, + "additionalProperties": false }, "http": { "type": "object", @@ -33,10 +33,12 @@ }, "urlRewrite": { "type": "object", + "description": "map (from URL -> to URL) defining HTTP URL rewrites", "additionalProperties": { "type": "string" } }, "proxy": { "type": "object", + "description": "object with 'href' field for the proxy URI, scheme field for either 'basic' or 'bearer', and corresponding credential fields as defined below", "required": ["href"], "properties": { "href": { @@ -54,9 +56,11 @@ "password": { "type": "string" } - } + }, + "additionalProperties": false }, "allowSelfSigned": { + "description": "whether self-signed certificates should be allowed", "type": "boolean" }, "serverKey": { @@ -65,7 +69,8 @@ "serverCert": { "type": "string" } - } + }, + "additionalProperties": false }, "mqtt": { "type": "object", @@ -84,10 +89,11 @@ }, "protocolVersion": { "type": "integer", - "examples": [3, 4, 5], + "enum": [3, 4, 5], "default": 5 } - } + }, + "additionalProperties": false }, "coap": { "type": "object", @@ -95,7 +101,8 @@ "port": { "type": "integer" } - } + }, + "additionalProperties": false }, "credentials": { "type": "object", @@ -114,28 +121,17 @@ } } } - } + }, + "additionalProperties": false }, "log": { "type": "object", - "oneOf": [ - { - "properties": { - "level": { - "type": "integer", - "enum": [0, 1, 2, 3] - } - } - }, - { - "properties": { - "level": { - "type": "string", - "enum": ["debug", "info", "warn", "error"] - } - } + "properties": { + "level": { + "type": "string", + "enum": ["debug", "info", "warn", "error"] } - ] + } } }, "additionalProperties": false diff --git a/packages/cli/test/.eslintrc.json b/packages/cli/test/.eslintrc.json deleted file mode 100644 index 53d0343f1..000000000 --- a/packages/cli/test/.eslintrc.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "extends": "../.eslintrc.json", - "rules": { - "import/no-extraneous-dependencies": "off" - } -} diff --git a/packages/cli/test/configuration.ts b/packages/cli/test/configuration.ts new file mode 100644 index 000000000..82412f10d --- /dev/null +++ b/packages/cli/test/configuration.ts @@ -0,0 +1,152 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the W3C Software Notice and + * Document License (2015-05-13) which is available at + * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document. + * + * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513 + ********************************************************************************/ + +import { suite, test } from "@testdeck/mocha"; +import { should, expect, use as chaiUse } from "chai"; +import { buildConfig, buildConfigFromFile, defaultConfiguration, Configuration } from "../src/configuration"; +import Ajv, { ValidateFunction } from "ajv"; +import ConfigSchema from "../src/generated/wot-servient-schema.conf"; +import chaiAsPromised from "chai-as-promised"; +import { writeFileSync, unlinkSync } from "fs"; +import { ValidationError } from "ajv"; +import tmp from "tmp"; + +should(); +chaiUse(chaiAsPromised); + +@suite("Configuration management") +class ConfigurationTest { + private static validator: ValidateFunction; + private testFilePath!: string; + + static before() { + tmp.setGracefulCleanup(); + const ajv = new Ajv({ strict: true, allErrors: true }); + ConfigurationTest.validator = ajv.compile(ConfigSchema) as ValidateFunction; + } + + before() { + this.testFilePath = tmp.fileSync({ postfix: ".json" }).name; + } + + after() { + try { + unlinkSync(this.testFilePath); + } catch { + // File may not exist + } + } + + @test async "should use default configuration when none provided"() { + const result = await buildConfig({}, defaultConfiguration, {}, ConfigurationTest.validator); + + expect(result).to.have.property("http"); + expect(result.http.port).to.equal(8080); + expect(result.coap.port).to.equal(5683); + expect(result.log.level).to.equal("warn"); + } + + @test async "should handle credentials in config"() { + const config = { ...defaultConfiguration, credentials: { THING_ID_1: { username: "user", password: "pass" } } }; + const result = await buildConfig({}, config, {}, ConfigurationTest.validator); + + expect(result.credentials).to.have.property("THING_ID_1"); + } + + @test async "should merge environment variables with defaults"() { + const env = { HTTP_PORT: "9000" }; + const result = await buildConfig({}, defaultConfiguration, env, ConfigurationTest.validator); + + expect(result.http.port).to.equal(9000); + } + + @test async "should apply config parameters"() { + const options = { configParams: { http: { port: 8888 } } }; + const result = await buildConfig(options, defaultConfiguration, {}, ConfigurationTest.validator); + + expect(result.http.port).to.equal(8888); + } + + @test async "should merge environment variables and config parameters"() { + const env = { HTTP_PORT: "9000" }; + const options = { configParams: { coap: { port: 6000 } } }; + const result = await buildConfig(options, defaultConfiguration, env, ConfigurationTest.validator); + + expect(result.http.port).to.equal(9000); + expect(result.coap.port).to.equal(6000); + } + + @test "should validate merged configuration"() { + const options = { configParams: { http: { port: "invalid" } } }; + + expect(buildConfig(options, defaultConfiguration, {}, ConfigurationTest.validator)).to.eventually.throw( + ValidationError + ); + } + + @test async "should apply default values to provided config"() { + const customConfig = { http: { port: 8888 } }; + const result = await buildConfig({}, customConfig, {}, ConfigurationTest.validator); + + expect(result.http.port).to.equal(8888); + expect(result.coap.port).to.equal(defaultConfiguration.coap.port); + expect(result.log.level).to.equal(defaultConfiguration.log.level); + } + + @test async "should read and build config from file"() { + const config = { http: { port: 7777 } }; + writeFileSync(this.testFilePath, JSON.stringify(config)); + + const result = await buildConfigFromFile({}, this.testFilePath, {}, ConfigurationTest.validator); + + expect(result.http.port).to.equal(7777); + } + + @test async "should merge file config with environment variables"() { + const config = { http: { port: 7777 } }; + writeFileSync(this.testFilePath, JSON.stringify(config)); + + const env = { COAP_PORT: "6000" }; + const result = await buildConfigFromFile({}, this.testFilePath, env, ConfigurationTest.validator); + + expect(result.http.port).to.equal(7777); + expect(result.coap.port).to.equal(6000); + } + + @test async "should handle configFile option"() { + const config = { http: { port: 5555 } }; + writeFileSync(this.testFilePath, JSON.stringify(config)); + + const options = { configFile: this.testFilePath }; + const result = await buildConfigFromFile(options, this.testFilePath, {}, ConfigurationTest.validator); + + expect(result.http.port).to.equal(5555); + } + + @test "should throw error for invalid config file"() { + writeFileSync(this.testFilePath, "{ invalid json }"); + + expect(buildConfigFromFile({}, this.testFilePath, {}, ConfigurationTest.validator)).to.eventually.throw(); + } + + @test async "should convert string env variables to appropriate types"() { + const env = { HTTP_PORT: "8080", SERVIENT_CLIENTONLY: "true", COAP_PORT: "5683" }; + const result = await buildConfig({}, defaultConfiguration, env, ConfigurationTest.validator); + + expect(result.http.port).to.equal(8080); + expect(result.servient.clientOnly).to.equal(true); + expect(result.coap.port).to.equal(5683); + } +} diff --git a/packages/cli/test/executor.ts b/packages/cli/test/executor.ts new file mode 100644 index 000000000..0628a93a8 --- /dev/null +++ b/packages/cli/test/executor.ts @@ -0,0 +1,96 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the W3C Software Notice and + * Document License (2015-05-13) which is available at + * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document. + * + * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513 + ********************************************************************************/ + +import { suite, test } from "@testdeck/mocha"; +import { should, expect } from "chai"; +import { Executor, WoTContext } from "../src/executor"; +import { writeFileSync } from "fs"; +import { Helpers } from "@node-wot/core"; +import tmp from "tmp"; + +should(); + +@suite("Executor") +class ExecutorTest { + private executor!: Executor; + private basetTestFilePath!: string; + private mockWoTContext!: WoTContext; + + static before() { + tmp.setGracefulCleanup(); + } + + before() { + this.executor = new Executor(); + this.mockWoTContext = { + // We are not using WoT inside this testing scripts + runtime: {} as typeof WoT, + helpers: {} as Helpers, + }; + } + + after() {} + + @test async "should execute JavaScript file"() { + const scriptContent = "module.exports = 'test result';"; + const testFile = tmp.fileSync({ postfix: ".js" }).name; + writeFileSync(testFile, scriptContent); + + const result = await this.executor.exec(testFile, this.mockWoTContext); + + expect(result).to.equal("test result"); + } + + @test async "should have WoT defined"() { + const scriptContent = "module.exports = typeof global.WoT !== 'undefined';"; + const testFile = tmp.fileSync({ postfix: ".js" }).name; + writeFileSync(testFile, scriptContent); + + const result = await this.executor.exec(testFile, this.mockWoTContext); + + expect(result).to.be.true; + } + + @test async "should handle module exports"() { + const scriptContent = "module.exports = { message: 'hello' };"; + const testFile = tmp.fileSync({ postfix: ".js" }).name; + writeFileSync(testFile, scriptContent); + + const result = await this.executor.exec(testFile, this.mockWoTContext); + + expect(result).to.have.property("message", "hello"); + } + + @test async "should detect TypeScript files by .ts extension"() { + const scriptContent = "export const value: number = 42;"; + const testFile = tmp.fileSync({ postfix: ".ts" }).name; + writeFileSync(testFile, scriptContent); + + const { value } = (await this.executor.exec(testFile, this.mockWoTContext)) as { + value: number; + }; + + expect(value).to.be.eq(42); + } + + @test async "should handle .mjs files as ES modules"() { + const filePath = tmp.fileSync({ postfix: ".mjs" }).name; + const scriptContent = "export const value = 'es module';"; + writeFileSync(filePath, scriptContent); + + const { value } = (await this.executor.exec(filePath, this.mockWoTContext)) as { value: string }; + expect(value).to.be.eq("es module"); + } +} diff --git a/packages/cli/test/parsers/config-file-parser.ts b/packages/cli/test/parsers/config-file-parser.ts new file mode 100644 index 000000000..5aa8c31a2 --- /dev/null +++ b/packages/cli/test/parsers/config-file-parser.ts @@ -0,0 +1,91 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the W3C Software Notice and + * Document License (2015-05-13) which is available at + * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document. + * + * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513 + ********************************************************************************/ + +import { suite, test } from "@testdeck/mocha"; +import { should, expect } from "chai"; +import { parseConfigFile } from "../../src/parsers/config-file-parser"; +import { InvalidArgumentError } from "commander"; +import { writeFileSync, unlinkSync } from "fs"; +import tmp from "tmp"; + +should(); + +@suite("parseConfigFile parser") +class ConfigFileParserTest { + private testFilePath!: string; + + static before() { + tmp.setGracefulCleanup(); + } + + before() { + this.testFilePath = tmp.fileSync({ postfix: ".json" }).name; + } + + after() { + try { + unlinkSync(this.testFilePath); + } catch { + // File may not exist if test failed before creation + } + } + + @test "should parse valid JSON config file"() { + const validConfig = { http: { port: 8080 }, coap: { port: 5683 } }; + writeFileSync(this.testFilePath, JSON.stringify(validConfig), { flag: "w+" }); + + const result = parseConfigFile(this.testFilePath, undefined); + + expect(result).to.equal(this.testFilePath); + } + + @test "should throw error for invalid JSON"() { + writeFileSync(this.testFilePath, "{ invalid json }"); + + expect(() => parseConfigFile(this.testFilePath, undefined)).to.throw(InvalidArgumentError); + } + + @test "should throw error for non-existent file"() { + expect(() => parseConfigFile("/nonexistent/file.json", undefined)).to.throw(InvalidArgumentError); + } + + @test "should throw error for empty JSON object"() { + writeFileSync(this.testFilePath, "{}"); + + const result = parseConfigFile(this.testFilePath, undefined); + expect(result).to.equal(this.testFilePath); + } + + @test "should handle complex JSON structures"() { + const complexConfig = { + servient: { clientOnly: false }, + http: { port: 8080, allowSelfSigned: false }, + coap: { port: 5683 }, + credentials: { user: "admin" }, + }; + writeFileSync(this.testFilePath, JSON.stringify(complexConfig)); + + const result = parseConfigFile(this.testFilePath, undefined); + + expect(result).to.equal(this.testFilePath); + } + + @test "should handle JSON array at root"() { + writeFileSync(this.testFilePath, JSON.stringify([1, 2, 3])); + + const result = parseConfigFile(this.testFilePath, undefined); + expect(result).to.equal(this.testFilePath); + } +} diff --git a/packages/cli/test/parsers/config-params-parser.ts b/packages/cli/test/parsers/config-params-parser.ts new file mode 100644 index 000000000..4a2cba80c --- /dev/null +++ b/packages/cli/test/parsers/config-params-parser.ts @@ -0,0 +1,96 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the W3C Software Notice and + * Document License (2015-05-13) which is available at + * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document. + * + * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513 + ********************************************************************************/ + +import { suite, test } from "@testdeck/mocha"; +import { should, expect } from "chai"; +import { parseConfigParams } from "../../src/parsers/config-params-parser"; +import { InvalidArgumentError } from "commander"; +import Ajv, { ValidateFunction } from "ajv"; +import ConfigSchema from "../../src/generated/wot-servient-schema.conf"; +import { Configuration } from "../../src/configuration"; + +should(); + +@suite("parseConfigParams parser") +class ConfigParamsParserTest { + private static validator: ValidateFunction; + + static before() { + const ajv = new Ajv({ strict: true, allErrors: true }); + ConfigParamsParserTest.validator = ajv.compile(ConfigSchema) as ValidateFunction; + } + + @test "should parse valid config parameter"() { + const result = parseConfigParams("http.port:=8080", undefined, ConfigParamsParserTest.validator); + + expect(result).to.have.property("http"); + expect((result as any).http).to.have.property("port", 8080); + } + + @test "should parse nested config parameter"() { + const result = parseConfigParams("servient.clientOnly:=true", undefined, ConfigParamsParserTest.validator); + + expect(result).to.have.property("servient"); + expect((result as any).servient).to.have.property("clientOnly", true); + } + + @test "should throw error for invalid key-value format"() { + expect(() => parseConfigParams("invalid_format", undefined, ConfigParamsParserTest.validator)).to.throw( + InvalidArgumentError + ); + } + + @test "should throw error for missing colon-equals separator"() { + expect(() => parseConfigParams("http.port=8080", undefined, ConfigParamsParserTest.validator)).to.throw( + InvalidArgumentError + ); + } + + @test "should throw error for invalid config parameter"() { + expect(() => + parseConfigParams("nonexistent.path:=value", undefined, ConfigParamsParserTest.validator) + ).to.throw(InvalidArgumentError); + } + + @test "should merge with previous parameters"() { + let result = parseConfigParams("http.port:=8080", undefined, ConfigParamsParserTest.validator); + result = parseConfigParams("coap.port:=5683", result, ConfigParamsParserTest.validator); + + expect(result).to.have.property("http"); + expect(result).to.have.property("coap"); + expect((result as any).http.port).to.equal(8080); + expect((result as any).coap.port).to.equal(5683); + } + + @test "should handle boolean values"() { + const result = parseConfigParams("servient.clientOnly:=true", undefined, ConfigParamsParserTest.validator); + + expect((result as any).servient.clientOnly).to.equal(true); + } + + @test "should handle numeric values"() { + const result = parseConfigParams("http.port:=9000", undefined, ConfigParamsParserTest.validator); + + expect((result as any).http.port).to.equal(9000); + } + + @test "should override previous parameter"() { + let result = parseConfigParams("http.port:=8080", undefined, ConfigParamsParserTest.validator); + result = parseConfigParams("http.port:=9000", result, ConfigParamsParserTest.validator); + + expect((result as any).http.port).to.equal(9000); + } +} diff --git a/packages/cli/test/runtime-test.ts b/packages/cli/test/runtime-test.ts deleted file mode 100644 index 3030e4d61..000000000 --- a/packages/cli/test/runtime-test.ts +++ /dev/null @@ -1,181 +0,0 @@ -/******************************************************************************** - * Copyright (c) 2022 Contributors to the Eclipse Foundation - * - * See the NOTICE file(s) distributed with this work for additional - * information regarding copyright ownership. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v. 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0, or the W3C Software Notice and - * Document License (2015-05-13) which is available at - * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document. - * - * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513 - ********************************************************************************/ - -/** - * Basic test suite to demonstrate test setup - * uncomment the @skip to see failing tests - * - * h0ru5: there is currently some problem with VSC failing to recognize experimentalDecorators option, it is present in both tsconfigs - */ - -import { suite, test } from "@testdeck/mocha"; -import { should, assert } from "chai"; -import DefaultServient from "../src/cli-default-servient"; - -import fs from "fs"; -import { EventEmitter } from "stream"; -// should must be called to augment all variables -should(); - -@suite("Test suite for script runtime") -class WoTRuntimeTest { - static servient: DefaultServient; - - static WoT: typeof WoT; - - exit: (code?: number) => never = () => { - throw new Error(""); - }; - - static async before() { - EventEmitter.setMaxListeners(20); - this.servient = new DefaultServient(true); - await this.servient.start(); - } - - beforeEach() { - this.exit = process.exit; - } - - afterEach() { - process.exit = this.exit; - } - - static async after(): Promise { - await this.servient.shutdown(); - } - - @test "should provide cli args"() { - const envScript = `module.exports = process.argv[0]`; - - const test = WoTRuntimeTest.servient.runPrivilegedScript(envScript, undefined, { argv: ["myArg"] }); - assert.equal(test, "myArg"); - } - - @test "should use the compiler function"() { - const envScript = `this is not js`; - - const test = WoTRuntimeTest.servient.runPrivilegedScript(envScript, undefined, { - compiler: () => { - return "module.exports = 'ok'"; - }, - }); - assert.equal(test, "ok"); - } - - @test "should provide env variables"() { - const envScript = `module.exports = process.env.MY_VAR`; - const test = WoTRuntimeTest.servient.runPrivilegedScript(envScript, undefined, { env: { MY_VAR: "test" } }); - assert.equal(test, "test"); - } - - @test "should hide system env variables"() { - const envScript = `module.exports = process.env.OS`; - - const test = WoTRuntimeTest.servient.runPrivilegedScript(envScript); - assert.equal(test, undefined); - } - - @test "should require node builtin module"() { - const envScript = `module.exports = require("fs")`; - - const test = WoTRuntimeTest.servient.runPrivilegedScript(envScript); - assert.equal(test, fs); - } - - @test "should catch synchronous errors"() { - const failNowScript = `throw new Error("Synchronous error in Servient sandbox");`; - - assert.doesNotThrow(() => { - WoTRuntimeTest.servient.runScript(failNowScript); - }); - assert.doesNotThrow(() => { - WoTRuntimeTest.servient.runPrivilegedScript(failNowScript); - }); - } - - @test "should catch bad errors"() { - const failNowScript = `throw "Bad synchronous error in Servient sandbox";`; - - assert.doesNotThrow(() => { - WoTRuntimeTest.servient.runScript(failNowScript); - }); - assert.doesNotThrow(() => { - WoTRuntimeTest.servient.runPrivilegedScript(failNowScript); - }); - } - - @test "should catch bad asynchronous errors for runScript"(done: Mocha.Done) { - // Mocha does not like string errors: https://github.com/trufflesuite/ganache-cli/issues/658 - // so here I am removing its listeners for uncaughtException. - // WARNING: Remove this line as soon the issue is resolved. - const listeners = this.clearUncaughtListeners(); - let called = false; - - this.mockupProcessExitWithFunction(() => { - if (!called) { - done(); - this.restoreUncaughtListeners(listeners); - called = true; - } - }); - - const failThenScript = `setTimeout( () => { throw "Bad asynchronous error in Servient sandbox"; }, 1);`; - - assert.doesNotThrow(() => { - WoTRuntimeTest.servient.runScript(failThenScript); - }); - } - - @test "should catch bad asynchronous errors for runPrivilegedScript"(done: Mocha.Done) { - // Mocha does not like string errors: https://github.com/trufflesuite/ganache-cli/issues/658 - // so here I am removing its listeners for uncaughtException. - // WARNING: Remove this line as soon the issue is resolved. - const listeners = this.clearUncaughtListeners(); - let called = false; - - this.mockupProcessExitWithFunction(() => { - if (!called) { - done(); - this.restoreUncaughtListeners(listeners); - called = true; - } - }); - - const failThenScript = `setTimeout( () => { throw "Bad asynchronous error in Servient sandbox"; }, 1);`; - assert.doesNotThrow(() => { - WoTRuntimeTest.servient.runPrivilegedScript(failThenScript); - }); - } - - private mockupProcessExitWithFunction(func: () => void) { - // Mockup is needed cause servient will call process.exit() - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - process.exit = func; - } - - private clearUncaughtListeners() { - const listeners = process.listeners("uncaughtException"); - process.removeAllListeners("uncaughtException"); - return listeners; - } - - private restoreUncaughtListeners(listeners: Array) { - listeners.forEach((element) => { - process.on("uncaughtException", element); - }); - } -} diff --git a/packages/cli/test/utils/load-env-variables.ts b/packages/cli/test/utils/load-env-variables.ts new file mode 100644 index 000000000..7326a190e --- /dev/null +++ b/packages/cli/test/utils/load-env-variables.ts @@ -0,0 +1,76 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the W3C Software Notice and + * Document License (2015-05-13) which is available at + * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document. + * + * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513 + ********************************************************************************/ + +import { suite, test } from "@testdeck/mocha"; +import { should, expect } from "chai"; +import { loadEnvVariables } from "../../src/utils/load-env-variables"; + +should(); + +@suite("loadEnvVariables utility") +class LoadEnvVariablesTest { + private originalEnv!: NodeJS.ProcessEnv; + + beforeEach() { + this.originalEnv = { ...process.env }; + } + + afterEach() { + process.env = this.originalEnv; + } + + @test "should filter environment variables by prefix"() { + process.env.WOT_SERVIENT_HTTP_PORT = "8080"; + process.env.WOT_SERVIENT_COAP_PORT = "5683"; + process.env.OTHER_VAR = "value"; + + const result = loadEnvVariables(); + + expect(result).to.have.property("HTTP_PORT", "8080"); + expect(result).to.have.property("COAP_PORT", "5683"); + expect(result).to.not.have.property("OTHER_VAR"); + } + + @test "should return empty object when no matching variables are found"() { + delete process.env.WOT_SERVIENT_HTTP_PORT; + delete process.env.WOT_SERVIENT_COAP_PORT; + + const result = loadEnvVariables(); + + expect(result).to.be.an("object"); + expect(Object.keys(result).length).to.equal(0); + } + + @test "should use custom prefix"() { + process.env.CUSTOM_PREFIX_VAR1 = "value1"; + process.env.CUSTOM_PREFIX_VAR2 = "value2"; + process.env.WOT_SERVIENT_VAR3 = "value3"; + + const result = loadEnvVariables("CUSTOM_PREFIX_"); + + expect(result).to.have.property("VAR1", "value1"); + expect(result).to.have.property("VAR2", "value2"); + expect(result).to.not.have.property("VAR3"); + } + + @test "should remove prefix from keys"() { + process.env.WOT_SERVIENT_MYKEY = "myvalue"; + + const result = loadEnvVariables(); + + expect(result).to.have.property("MYKEY", "myvalue"); + expect(Object.keys(result)).to.not.include("WOT_SERVIENT_MYKEY"); + } +} diff --git a/packages/cli/test/utils/set-log-level.ts b/packages/cli/test/utils/set-log-level.ts new file mode 100644 index 000000000..301eabe98 --- /dev/null +++ b/packages/cli/test/utils/set-log-level.ts @@ -0,0 +1,60 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the W3C Software Notice and + * Document License (2015-05-13) which is available at + * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document. + * + * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513 + ********************************************************************************/ + +import { suite, test } from "@testdeck/mocha"; +import { should, expect } from "chai"; +import { setLogLevel } from "../../src/utils/set-log-level"; +import * as logger from "debug"; + +should(); + +@suite("setLogLevel utility") +class SetLogLevelTest { + @test "should set debug log level"() { + setLogLevel("debug"); + // Verify by checking that debug enables all node-wot loggers + expect(logger.enabled("node-wot:test")).to.be.true; + } + + @test "should set info log level"() { + setLogLevel("info"); + // info level should enable error, warn, and info logs + expect(logger.enabled("node-wot:test:error")).to.be.true; + expect(logger.enabled("node-wot:test:warn")).to.be.true; + expect(logger.enabled("node-wot:test:info")).to.be.true; + } + + @test "should set warn log level"() { + setLogLevel("warn"); + // warn level should enable error and warn logs + expect(logger.enabled("node-wot:test:error")).to.be.true; + expect(logger.enabled("node-wot:test:warn")).to.be.true; + } + + @test "should set error log level"() { + setLogLevel("error"); + // error level should only enable error logs + expect(logger.enabled("node-wot:test:error")).to.be.true; + } + + @test "should disable all loggers before reconfiguring"() { + setLogLevel("debug"); + expect(logger.enabled("node-wot:test")).to.be.true; + + setLogLevel("error"); + // After switching to error level, debug logs should be disabled + expect(logger.enabled("node-wot:test:debug")).to.be.false; + } +} diff --git a/packages/cli/test/utils/string-to-js-value.ts b/packages/cli/test/utils/string-to-js-value.ts new file mode 100644 index 000000000..9ae93f42c --- /dev/null +++ b/packages/cli/test/utils/string-to-js-value.ts @@ -0,0 +1,58 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the W3C Software Notice and + * Document License (2015-05-13) which is available at + * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document. + * + * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513 + ********************************************************************************/ + +import { suite, test } from "@testdeck/mocha"; +import { should, expect } from "chai"; +import { stringToJSValue } from "../../src/utils/string-to-js-value"; + +should(); + +@suite("stringToJSValue utility") +class StringToJSValueTest { + @test "should convert numeric string to number"() { + expect(stringToJSValue("42")).to.equal(42); + expect(stringToJSValue("0")).to.equal(0); + expect(stringToJSValue("1000")).to.equal(1000); + } + + @test "should convert string 'true' to boolean true"() { + expect(stringToJSValue("true")).to.equal(true); + } + + @test "should convert string 'false' to boolean false"() { + expect(stringToJSValue("false")).to.equal(false); + } + + @test "should return original string for non-numeric, non-boolean values"() { + expect(stringToJSValue("hello")).to.equal("hello"); + expect(stringToJSValue("myValue")).to.equal("myValue"); + } + + @test "should handle floating point numbers"() { + expect(stringToJSValue("3.14")).to.equal(3.14); + expect(stringToJSValue("0.5")).to.equal(0.5); + } + + @test "should handle negative numbers"() { + expect(stringToJSValue("-42")).to.equal(-42); + expect(stringToJSValue("-3.14")).to.equal(-3.14); + } + + @test "should return string for non-strictly-boolean values"() { + expect(stringToJSValue("True")).to.equal("True"); + expect(stringToJSValue("False")).to.equal("False"); + expect(stringToJSValue("TRUE")).to.equal("TRUE"); + } +} diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json index ffcc38f9d..6447e8437 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -3,9 +3,9 @@ "compilerOptions": { "outDir": "dist", "rootDir": "src", - "resolveJsonModule": true, "esModuleInterop": true }, + "exclude": ["eslint.config.mjs"], "include": ["src/**/*", "src/*.json"], "references": [ { "path": "../core" }, diff --git a/packages/examples/src/scripts/counter.ts b/packages/examples/src/scripts/counter.ts index 55afa7c4e..9f9a11b29 100644 --- a/packages/examples/src/scripts/counter.ts +++ b/packages/examples/src/scripts/counter.ts @@ -23,7 +23,6 @@ // * multi-language // * image contentTypes for properties (Note: the contentType applies to all forms of the property) // * links with entry containing rel and sizes - let count: number; let lastChange: string; diff --git a/packages/examples/tsconfig.json b/packages/examples/tsconfig.json index 118676f68..77ff8d75f 100644 --- a/packages/examples/tsconfig.json +++ b/packages/examples/tsconfig.json @@ -8,6 +8,6 @@ "sourceMap": false, "removeComments": false }, - "include": ["src/**/*"], + "include": ["src/**/*", "../../node_modules/wot-typescript-definitions/**/*.d.ts"], "references": [] }