diff --git a/.eslintrc.json b/.eslintrc.json index 043b3f61c..bfc905307 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -3,26 +3,13 @@ "parserOptions": { "project": "./tsconfig.json" }, - "extends": [ - "eslint:recommended", - "standard", - "prettier", - "plugin:@typescript-eslint/recommended" - ], - "plugins": [ - "@typescript-eslint", - "unused-imports" - ], + "extends": ["eslint:recommended", "standard", "prettier", "plugin:@typescript-eslint/recommended"], + "plugins": ["@typescript-eslint", "unused-imports"], "env": { "es6": true, "node": true }, - "ignorePatterns": [ - "dist", - "node_modules", - "examples", - "bin" - ], + "ignorePatterns": ["dist", "node_modules", "examples", "bin"], "rules": { "@typescript-eslint/no-unused-vars": "off", // or "@typescript-eslint/no-unused-vars": "off", "unused-imports/no-unused-imports": "error", diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 096f33d09..6d7a95567 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -3,63 +3,62 @@ name: Default CI Pipeline on: [push, pull_request] jobs: - setup-and-test: + setup-and-test: + runs-on: ${{ matrix.os }} - runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [macos-latest, ubuntu-latest, windows-latest] + node-version: [12.x, 14.x, 16.x] - strategy: - matrix: - os: [macos-latest, ubuntu-latest, windows-latest] - node-version: [ 12.x, 14.x, 16.x] + steps: + - name: Checkout + uses: actions/checkout@v2 - steps: - - name: Checkout - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.os }} ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + - name: npm 7 + # npm workspaces requires npm v7 or higher + run: npm i -g npm@7 --registry=https://registry.npmjs.org + - name: Install + run: npm ci - - name: Use Node.js ${{ matrix.os }} ${{ matrix.node-version }} - uses: actions/setup-node@v1 - with: - node-version: ${{ matrix.node-version }} - - name: npm 7 - # npm workspaces requires npm v7 or higher - run: npm i -g npm@7 --registry=https://registry.npmjs.org - - name: Install - run: npm ci + - name: Build + run: npm run build - - name: Build - run: npm run build + - name: Test + run: npm run test - - name: Test - run: npm run test + eslint: + name: eslint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: install node v12 + uses: actions/setup-node@v1 + with: + node-version: 12 + - name: yarn install + run: yarn install + - name: eslint + uses: icrawl/action-eslint@v1 + with: + custom-glob: packages - eslint: - name: eslint - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - name: install node v12 - uses: actions/setup-node@v1 - with: - node-version: 12 - - name: yarn install - run: yarn install - - name: eslint - uses: icrawl/action-eslint@v1 - with: - custom-glob: packages - - prettier: - name: Check coding style - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - name: install node v12 - uses: actions/setup-node@v1 - with: - node-version: 12 - - name: yarn install - run: yarn install - - name: Prettify code - uses: creyD/prettier_action@v4.0 - with: - dry: True + prettier: + name: Check coding style + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: install node v12 + uses: actions/setup-node@v1 + with: + node-version: 12 + - name: yarn install + run: yarn install + - name: Prettify code + uses: creyD/prettier_action@v4.0 + with: + dry: True diff --git a/.prettierrc.json b/.prettierrc.json index 1d4ebc0d9..8c7004572 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -4,4 +4,4 @@ "singleQuote": false, "tabWidth": 4, "endOfLine": "lf" -} \ No newline at end of file +} diff --git a/.vscode/launch.json b/.vscode/launch.json index c0df40578..00fbf3107 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -22,30 +22,20 @@ { "name": "Debug default servient", "request": "launch", - - "runtimeArgs": [ - "run-script", - "debug" - ], + + "runtimeArgs": ["run-script", "debug"], "port": 9229, "runtimeExecutable": "npm", - "skipFiles": [ - "/**" - ], + "skipFiles": ["/**"], "type": "node" }, { "name": "Test", "request": "launch", - "runtimeArgs": [ - "run-script", - "test:only" - ], + "runtimeArgs": ["run-script", "test:only"], "port": 9229, "runtimeExecutable": "npm", - "skipFiles": [ - "/**" - ], + "skipFiles": ["/**"], "type": "pwa-node" }, { @@ -64,9 +54,7 @@ "name": "HTTP Tests", "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", "request": "launch", - "skipFiles": [ - "/**" - ], + "skipFiles": ["/**"], "type": "pwa-node" }, { @@ -85,9 +73,7 @@ "name": "HTTP Test current File", "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", "request": "launch", - "skipFiles": [ - "/**" - ], + "skipFiles": ["/**"], "type": "pwa-node" }, { @@ -108,9 +94,7 @@ "name": "Modbus Tests", "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", "request": "launch", - "skipFiles": [ - "/**" - ], + "skipFiles": ["/**"], "type": "pwa-node" }, { @@ -131,9 +115,7 @@ "name": "Modbus Test current File", "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", "request": "launch", - "skipFiles": [ - "/**" - ], + "skipFiles": ["/**"], "type": "pwa-node" }, { @@ -152,9 +134,7 @@ "name": "COAP Tests", "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", "request": "launch", - "skipFiles": [ - "/**" - ], + "skipFiles": ["/**"], "type": "pwa-node" }, { @@ -173,9 +153,7 @@ "name": "COAP Test current File", "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", "request": "launch", - "skipFiles": [ - "/**" - ], + "skipFiles": ["/**"], "type": "pwa-node" }, { @@ -194,14 +172,12 @@ "name": "MQTT Tests", "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", "request": "launch", - "skipFiles": [ - "/**" - ], + "skipFiles": ["/**"], "type": "pwa-node" }, { "args": [ - "--require", + "--require", "ts-node/register", "--timeout", "999999", @@ -213,14 +189,12 @@ "name": "NetConf Tests", "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", "request": "launch", - "skipFiles": [ - "/**" - ], + "skipFiles": ["/**"], "type": "pwa-node" }, { "args": [ - "--require", + "--require", "ts-node/register", "--timeout", "999999", @@ -232,9 +206,7 @@ "name": "Opcua Tests", "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", "request": "launch", - "skipFiles": [ - "/**" - ], + "skipFiles": ["/**"], "type": "pwa-node" }, { @@ -253,9 +225,7 @@ "name": "Websockets Tests", "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", "request": "launch", - "skipFiles": [ - "/**" - ], + "skipFiles": ["/**"], "type": "pwa-node" }, { @@ -274,9 +244,7 @@ "name": "Core Tests", "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", "request": "launch", - "skipFiles": [ - "/**" - ], + "skipFiles": ["/**"], "type": "pwa-node" }, { @@ -295,9 +263,7 @@ "name": "Core Test File", "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", "request": "launch", - "skipFiles": [ - "/**" - ], + "skipFiles": ["/**"], "type": "pwa-node" }, { @@ -316,9 +282,7 @@ "name": "TDtools Tests", "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", "request": "launch", - "skipFiles": [ - "/**" - ], + "skipFiles": ["/**"], "type": "pwa-node" }, { @@ -337,10 +301,8 @@ "name": "TDtools Test File", "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", "request": "launch", - "skipFiles": [ - "/**" - ], + "skipFiles": ["/**"], "type": "pwa-node" } ] -} \ No newline at end of file +} diff --git a/.vscode/settings.json b/.vscode/settings.json index c5423073e..e1f04b9ad 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,47 +1,43 @@ { "typescript.tsdk": "./node_modules/typescript/lib", "typescript.referencesCodeLens.enabled": true, - "fileHeaderComment.parameter":{ - "*":{ - "author": "the thingweb community", - "license_w3c":[ - "/********************************************************************************", - " * Copyright (c) 2018 - 2019 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", - " ********************************************************************************/" - ] - } -}, -"fileHeaderComment.template":{ - "*":[ - "${commentbegin}", - "${commentprefix} ${license_w3c}", - "${commentend}" - ] -}, - "files.exclude": { - "**/.git": true, - "**/.svn": true, - "**/.hg": true, - "**/.DS_Store": true, - "src/**/*.js" : true, - "test/**/*.js" : true - }, - "files.encoding": "utf8", - "files.eol": "\n", - "files.insertFinalNewline": true, - "files.trimFinalNewlines": true, - "files.trimTrailingWhitespace": true, - "editor.tabSize": 4, - "editor.insertSpaces": true, + "fileHeaderComment.parameter": { + "*": { + "author": "the thingweb community", + "license_w3c": [ + "/********************************************************************************", + " * Copyright (c) 2018 - 2019 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", + " ********************************************************************************/" + ] + } + }, + "fileHeaderComment.template": { + "*": ["${commentbegin}", "${commentprefix} ${license_w3c}", "${commentend}"] + }, + "files.exclude": { + "**/.git": true, + "**/.svn": true, + "**/.hg": true, + "**/.DS_Store": true, + "src/**/*.js": true, + "test/**/*.js": true + }, + "files.encoding": "utf8", + "files.eol": "\n", + "files.insertFinalNewline": true, + "files.trimFinalNewlines": true, + "files.trimTrailingWhitespace": true, + "editor.tabSize": 4, + "editor.insertSpaces": true } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 9f3e3d620..8769bdd94 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -11,20 +11,14 @@ "label": "build", "type": "shell", "command": "npm", - "args": [ - "run", - "build" - ], + "args": ["run", "build"], "group": "build" }, { "label": "test", "type": "shell", "command": "npm", - "args": [ - "run", - "test" - ] + "args": ["run", "test"] } ] -} \ No newline at end of file +} diff --git a/API.md b/API.md index 8abbc53cf..761ba12bd 100644 --- a/API.md +++ b/API.md @@ -1,43 +1,50 @@ ### What to do with the library + The two main functionalities of node-wot are creating WoT Things and interacting with other WoT Things. These can be combined into a Thing that interacts with other Things. #### Creating a WoT Thing + Creating a WoT Thing is called exposing a Thing. Exposing a Thing creates a Thing Description that can be used to by others to interact with this Thing. ##### Starting a Servient + ```javascript -WotCore = require("@node-wot/core") +WotCore = require("@node-wot/core"); let servient = new WotCore.Servient(); let WoT = await servient.start(); ``` ##### In Client mode, add factories + ```javascript -WotCore = require("@node-wot/core") -BindingHttp = require("@node-wot/binding-http") -BindingCoap = require("@node-wot/binding-coap") +WotCore = require("@node-wot/core"); +BindingHttp = require("@node-wot/binding-http"); +BindingCoap = require("@node-wot/binding-coap"); let servient = new NodeWoTCore.Servient(); -servient.addClientFactory(new BindingHttp.HttpClientFactory()) -servient.addClientFactory(new BindingCoap.CoapClientFactory()) +servient.addClientFactory(new BindingHttp.HttpClientFactory()); +servient.addClientFactory(new BindingCoap.CoapClientFactory()); ``` + The different bindings offer client factories. These need to be added in order to be able to access devices through this protocol. For more details on bindings, e.g. configuration options for a specific `*ClientFactory`, look at the `README.md` files in their respective directories. ##### In Server mode, add servers + ```javascript -WotCore = require("@node-wot/core") -CoapServer = require("@node-wot/binding-coap").CoapServer +WotCore = require("@node-wot/core"); +CoapServer = require("@node-wot/binding-coap").CoapServer; let servient = new WotCore.Servient(); servient.addServer(new CoapServer()); ``` -Same as for clients, bindings offer servers. +Same as for clients, bindings offer servers. ##### Credentials + ```javascript let servient = new (require("@node-wot/core")).Servient(); servient.addCredentials({ @@ -47,6 +54,7 @@ servient.addCredentials({ } ); ``` + You can add credentials like this. They are either used to authenticate clients when running in server mode or used to authenticate against servers when running in client mode. @@ -54,36 +62,40 @@ This example uses `username` and `password`, but other authentication mechanisms Authentication data is always mapped to a Thing through its id. ##### Expose a Thing + ```javascript let thing = WoT.produce({ - title: "counter", - description: "counter example Thing" + title: "counter", + description: "counter example Thing", }); // any other code to develop the Thing thing.expose(); ``` + Here, an object named `thing` is produced. At this stage, it has only a name and a description for humans to read. `thing.expose();` exposes/starts the exposed Thing in order to process external requests. This also creates a Thing Description that describes the interfaces of the `counter` thing. - ##### Add a Property definition to the Thing + Properties expose internal state of a Thing that can be directly accessed (get) and optionally manipulated (set). They are added as part of the `WoT.produce` invocation, like so: + ```javascript WoT.produce({ - title: "property", - properties: { - counter: { - type: "integer", - description: "current counter value", - observable: false - } - } + title: "property", + properties: { + counter: { + type: "integer", + description: "current counter value", + observable: false, + }, + }, }); ``` + This creates a Property. Its value can be initializes by calling `writeProperty`, otherwise it reads as `null`. After being written to, the value can be read by other Things. @@ -92,51 +104,52 @@ You can create a Property that has a more complex type, such as an object. This ```javascript WoT.produce({ - title: "complexproperty", - properties: { - color: { - type: "object", - properties: { - r: { type: "integer", minimum: 0, maximum: 255 }, - g: { type: "integer", minimum: 0, maximum: 255 }, - b: { type: "integer", minimum: 0, maximum: 255 }, - } - } - } + title: "complexproperty", + properties: { + color: { + type: "object", + properties: { + r: { type: "integer", minimum: 0, maximum: 255 }, + g: { type: "integer", minimum: 0, maximum: 255 }, + b: { type: "integer", minimum: 0, maximum: 255 }, + }, + }, + }, }); ``` ##### Add a Property read handler ```javascript -thing.setPropertyReadHandler( - "counter", - (propertyName) => { +thing.setPropertyReadHandler("counter", (propertyName) => { console.log("Handling read request for " + propertyName); return new Promise((resolve, reject) => { - resolve(Math.random(100)); - }) - }); + resolve(Math.random(100)); + }); +}); ``` + You can specify if the Thing needs to do something in case of a property read. Here, instead of reading a static value, a new random value is generated on every invocation. ##### Add a Property write handler ```javascript -thing.setPropertyWriteHandler('brightness', (value) => { - return new Promise((resolve, reject) => { - value %= 2; // only even values are valid in this example - setBrightness(value); - resolve(value); - }); +thing.setPropertyWriteHandler("brightness", (value) => { + return new Promise((resolve, reject) => { + value %= 2; // only even values are valid in this example + setBrightness(value); + resolve(value); + }); }); ``` + You can specify if the Thing needs do to something in case of a property write. Here, the value written is used to set the brightness of an LED that requires a specific function (`setBrightness()`) to do that. The property value becomes the value passed to `resolve()`, which in this case would mean the number modulo 2. ##### Add an Action definition to the Thing + Actions offer functions of the Thing. These functions may manipulate the interal state of a Thing in a way that is not possible through setting Properties. Examples are changing internal state that is not exposed as a Property, changing multiple Properties, changing Properties over time or with a process that shall not be disclosed. @@ -160,20 +173,23 @@ WoT.produce({ As can be seen above, `input` and `output` data types can be specified, similar to how property types are described in TDs. ##### Add an Action invoke handler + You need to write what will happen if an Action is invoked. This is done by setting an Action Handler: ```javascript thing.setActionHandler("increment", () => { - console.log("Incrementing"); - return thing.readProperty("counter").then((count) => { - let value = count + 1; - thing.writeProperty("counter", value); - }); + console.log("Incrementing"); + return thing.readProperty("counter").then((count) => { + let value = count + 1; + thing.writeProperty("counter", value); + }); }); - ``` +``` + Here, you see also how to access the properties of a Thing you are creating. ##### Add an Event definition to the Thing + The Event Interaction Affordance describes event sources that asynchronously push messages. This means that instead of communicating state, state transitions (events) are communicated (e.g. "clicked"). Events may be triggered by internal state changes that are not exposed as Properties. @@ -184,20 +200,21 @@ In the following, we will add the Event `onchange`: ```javascript WoT.produce({ - title: "change", - events: { - onchange: { - type: "number" - } - } + title: "change", + events: { + onchange: { + type: "number", + }, + }, }); ``` ##### Emit Event, i.e. notify all listeners subscribed to that Event + ```javascript -setInterval(async() => { - ++counter; - thing.emitEvent("onchange", counter); +setInterval(async () => { + ++counter; + thing.emitEvent("onchange", counter); }, 5000); ``` @@ -221,33 +238,39 @@ Here the event is triggered in regular intervals but emitting an event can be do * to update a Property value; * to run an Action: take the parameters from the request, execute the defined action, and return the result; --> - #### Interacting with another WoT Thing + Interacting with another WoT Thing is called consuming a Thing and works by using its Thing Description. ##### Fetch a Thing Description of a Thing given its URL + ```javascript WoTHelpers.fetch("http://localhost:8080/counter").then(async(td) => { // Do something with the TD } ``` + URLs can have various schemes, including `file://` to read from the local filesystem. ##### Consume a TD of a Thing, including parsing the TD and generating the protocol bindings in order to access lower level functionality + ```javascript -WoTHelpers.fetch("http://localhost:8080/counter").then(async(td) => { - let thing = WoT.consume(td); - // Do something with the consumed Thing +WoTHelpers.fetch("http://localhost:8080/counter").then(async (td) => { + let thing = WoT.consume(td); + // Do something with the consumed Thing }); ``` + Things can be `consume`d no matter if they were fetched with WoTHelpers or not. `consume` only requires a TD as an `Object`, so you could also use `fs.readFile` and `JSON.parse` or inline it into your code. As long at it results in a TD Object, you can receive it over Fax, Morse it or use smoke signals. #### On a consumed Thing + You can access all the interactions this Thing has and interact with them. ##### Read the value of a Property or set of properties + You can read the property values with the `readProperty` function. It is an asynchronous function that will take some time to complete. So you should handle it explicitely. @@ -259,25 +282,30 @@ console.info("count value is", read1); ``` ##### Set the value of a Property or a set of properties + You can write to a property by using the `writeProperty` function. ```javascript thing.writeProperty("color", { r: 255, g: 255, b: 0 }); ``` + ##### Invoke an Action + You can invoke an action by using the `invokeAction` function. It is an asynchronous function that will take some time to complete. So you should handle it explicitly. Here we use the `async`/`await` functionality of NodeJS. Declare the surrounding function as `async`, e.g., the `WoTHelpers.fetch()` resolve handler: + ```javascript WoTHelpers.fetch(myURI).then(async(td) => { ... }); ``` Use `await` to make Promises synchronous (blocking): + ```javascript await thing.invokeAction("increment"); // or passing a value diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 95726dce5..dc2085953 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,7 +4,7 @@ Thanks for your interest in this project. General information regarding source code management, builds, coding standards, and more can be found here: -* https://projects.eclipse.org/projects/iot.thingweb/developer +- https://projects.eclipse.org/projects/iot.thingweb/developer ## Legal Requirements @@ -13,22 +13,22 @@ This process helps us in creating great open source software within a safe legal Thus, before your contribution can be accepted by the project team, contributors must electronically sign the [Eclipse Contributor Agreement (ECA)](http://www.eclipse.org/legal/ECA.php) and follow these preliminary steps: -* Obtain an [Eclipse Foundation account](https://accounts.eclipse.org/) - * Anyone who currently uses Eclipse Bugzilla or Gerrit systems already has one of those - * Newcomers can [create a new account](https://accounts.eclipse.org/user/register?destination=user) -* Add your GiHub username to your Eclipse Foundation account - * ([Log into Eclipse](https://accounts.eclipse.org/)) - * Go to the *Edit Profile* tab - * Fill in the *GitHub ID* under *Social Media Links* and save -* Sign the [Eclipse Contributor Agreement](http://www.eclipse.org/legal/ECA.php) - * ([Log into Eclipse](https://accounts.eclipse.org/)) - * If the *Status* entry *Eclipse Contributor Agreement* has a green checkmark, the ECA is already signed - * If not, go to the *Eclipse Contributor Agreement* tab or follow the corresponding link under *Status* - * Fill out the form and sign it electronically -* Sign-off every commit using the same email address used for your Eclipse account - * Set the Git user email address with `git config user.email ""` - * Add the `-s` flag when you make the commit(s), e.g. `git commit -s -m "feat: add support for magic"` -* Open a [Pull Request](https://github.com/eclipse/thingweb.node-wot/pulls) +- Obtain an [Eclipse Foundation account](https://accounts.eclipse.org/) + - Anyone who currently uses Eclipse Bugzilla or Gerrit systems already has one of those + - Newcomers can [create a new account](https://accounts.eclipse.org/user/register?destination=user) +- Add your GiHub username to your Eclipse Foundation account + - ([Log into Eclipse](https://accounts.eclipse.org/)) + - Go to the _Edit Profile_ tab + - Fill in the _GitHub ID_ under _Social Media Links_ and save +- Sign the [Eclipse Contributor Agreement](http://www.eclipse.org/legal/ECA.php) + - ([Log into Eclipse](https://accounts.eclipse.org/)) + - If the _Status_ entry _Eclipse Contributor Agreement_ has a green checkmark, the ECA is already signed + - If not, go to the _Eclipse Contributor Agreement_ tab or follow the corresponding link under _Status_ + - Fill out the form and sign it electronically +- Sign-off every commit using the same email address used for your Eclipse account + - Set the Git user email address with `git config user.email ""` + - Add the `-s` flag when you make the commit(s), e.g. `git commit -s -m "feat: add support for magic"` +- Open a [Pull Request](https://github.com/eclipse/thingweb.node-wot/pulls) For more information, please see the Eclipse Committer Handbook: https://www.eclipse.org/projects/handbook/#resources-commit @@ -37,6 +37,7 @@ https://www.eclipse.org/projects/handbook/#resources-commit Eclipse Thingweb uses Conventional Changelog, which structure Git commit messages in a way that allows automatic generation of changelogs. Commit messages must be structured as follows: + ``` (): @@ -45,38 +46,41 @@ Commit messages must be structured as follows: