Updating atscm
You can use atscm to update atscm 😀
Installing new versions
Simply run atscm update
to install the latest version available. Add the --beta
flag to install prerelease versions. Ensure to backup your project before doing so.
Internally, we use npm to install updates, which means that you can also run
npm install --save-dev atscm
instead.
Updating your atscm project
We'll do our best to follow semantic versioning, which means you shouldn't need to update your project sources between non-major releases, e.g. when updating from 1.0.0 to 1.0.1 or 1.1.0.
Between major releases (e.g. from 0.7.0 to 1.0.0) we introduce changes that may break your existing project. Follow these steps to migrate your project to a new major version of atscm:
- Backup your project before, e.g. with git:
git add . && git commit -m "chore: Backup before atscm update"
- Start a fresh atvise server instance and push your current project:
atscm push
- Update atscm:
atscm update
- Pull your project sources from atvise server:
atscm pull --clean
- Afterwards, you can commit commit the changes:
git add . && git commit -m "chore: Update atscm"
Tutorial: Custom Transformer
tl;dr: Jump to the tutorial-custom-transformer repository to see the results.
In this document we'll guide you through the steps necessary to implement a custom Transformer. Our transformer will use Babel to transpile ES2015/ES6 JavaScript to plain ES5 JavaScript that works in all Browsers.
Overview
Custom transformers provide an easy way to extend the build functionality of atscm. Basically, a transformer implements two behaviours: How atvise server nodes are mapped to files (when running atscm pull
) and vice versa (when running atscm push
).
Where to store transformers
Basically, transformers can be stored anywhere inside your atscm project. When using a non-ES5 configuration language (such as ES2015 or TypeScript, chosen when running atscm init
) transformers should also be written in this language. atscm will handle the transpilation of your transformer code automatically. If you plan to write multiple custom transformers for your project, it is recommended to create your transformers in an own directory, e.g ./atscm
.
Step 0: Project setup
In order to have the same starting point, create a new atscm project to follow this tutorial. Run atscm init
and pick ES2015 as configuration language.
As for now the atvise library is written in old ES5 JavaScript, we'll ignore it in our project. Adjust your project configuration accordingly:
// Atviseproject.babel.js
...
export default class MyProject extends Atviseproject {
...
static get ignoreNodes() {
return super.ignoreNodes
.concat(['ns=1;s=SYSTEM.LIBRARY.ATVISE']);
}
}
Now we're ready to pull the project by running:
atscm pull
We'll use the default project files for testing later.
As suggested above, we'll store our custom transformer inside a new directory, ./atscm
. Create the directory and an empty file called BabelTransformer.js:
mkdir atscm
touch atscm/BabelTransformer.js
By now you should have a project containing an ./Atviseproject.babel.js
and an empty ./atscm/BabelTransformer.js
file.
Make sure the ./src
directory contains at least the default Main display which should exist inside ./src/AGENT/DISPLAYS/Main.display
.
Step 1: Import PartialTransformer class
As we don't want to implement things twice we'll subclass atscm's Transformer class. As our transformer shall only be used for JavaScript source files we can even use the PartialTransformer class which supports filtering source files out of the box. As both of these classes are exported from atscm's main file, importing them is pretty straightforward. Inside the BabelTransformer.js file add:
// atscm/BabelTransformer.js
import { PartialTransformer } from 'atscm';
Step 2: Create the BabelTransformer class
The next step is to create and export our Transformer class:
// atscm/BabelTransformer.js
import { PartialTransformer } from 'atscm';
export default class BabelTransformer extends PartialTransformer {}
We just created a PartialTransformer subclass that is exported as the file's default export. For more detailed information on ES2015's module system take a look at the docs.
Step 3: Use BabelTransformer
By default, atscm uses just some standard transformers. Any additional transformers must be configured to use inside the project's Atviseproject file.
First of all, we have to import our newly created BabelTransformer class:
// Atviseproject.babel.js
import { Atviseproject } from 'atscm'
import BabelTransformer from './atscm/BabelTransformer';
export default class MyProject extends Atviseproject { ... }
Now we override the Atviseproject.useTransformers getter to use our transformer:
// Atviseproject.babel.js
...
export default class MyProject extends Atviseproject {
...
static get useTransformers() {
return super.useTransformers
.concat(new BabelTransformer());
}
}
This statement tells atscm to use a new BabelTransformer instance in addition to the default transformers (super.useTransformers
).
To verify everything worked so far run atscm config
. Our new Transformer should show up in the useTransformers section:
$ atscm config
[08:38:16] Configuration at ~/custom-transformer/Atviseproject.babel.js
{ host: '10.211.55.4',
port:
{ opc: 4840,
http: 80 },
useTransformers:
[ DisplayTransformer<>,
ScriptTransformer<>,
BabelTransformer<> ],
...
Step 4: Implement PartialTransformer#shouldBeTransformed
PartialTransformer#shouldBeTransformed is responsible for filtering the files we want to transform. Returning true
means the piped file will be transformed, false
bypasses the file.
In out case we want to edit all JavaScript source files. Therefore we return true for all files with the extension .js
. Edit BabelTransformer.js accordingly:
// atscm/BabelTransformer
...
export default class BabelTransformer extends PartialTransformer {
shouldBeTransformed(file) {
return file.extname === '.js';
}
}
Step 5: Implement Transformer#transformFromFilesystem
Implementing Transformer#transformFromFilesystem is probably the most important part of this tutorial. In here we define the logic that actually creates ES5 code from ES2015 sources.
First of all, we need to install additional dependencies required. Running
npm install --save-dev babel-core babel-preset-2015
will install Babel and it's ES2015 preset. This preset ensures all ES5 compatible browsers will be able to run the resulting code.
We will also need the node.js buffer module. We don't need to install it, as it comes with every node installation.
Next, import these modules as usual:
// atscm/BabelTransformer.js
import { Buffer } from 'buffer';
import { PartialTransformer } from 'atscm';
import { transform } from 'babel-core';
...
The import order follows a pretty usual convention:
- Core node.js modules (buffer in our case)
- Other external modules (babel-core and atscm in our case)
- Relative modules (./atscm/BabelTransformer.js inside Atviseproject.babel.js in our case)
Now we're ready to implement Transformer#transformFromFilesystem. What we're about to do is pretty simple:
- We'll transpile the contents of the passed file with babels transform method
- We clone the passed file and set it's contents to a Buffer containing the resulting code
- We pass the resulting file to other streams
import ...
export default class BabelTransformer extends PartialTransformer {
static shouldBeTransformed(file) { ... }
transformFromFilesystem(file, enc, callback) {
// Create ES5 code
const { code } = transform(file.contents, {
presets: ['es2015']
});
// Create new file with ES5 content
const result = file.clone();
result.contents = Buffer.from(code);
// We're done, pass the new file to other streams
callback(null, result);
}
}
Wow! You just implemented your first custom transformer! Now we can write any scripts using the new ES2015 syntax.
Step 6: Test BabelTransformer
It's time to check if everything works as expected. Create a script file for the Main display containing ES2015 JavaScript:
// src/AGENT/DISPLAYS/Main.display/Main.js
// Class syntax
class Test {
constructor(options = {}, ...otherArgs) {
// Default values and rest params
this.options = options;
this.args = otherArgs.map((arg) => parseInt(arg, 10)); // Arrows and Lexical This
}
}
const a = 13; // Constants
const { options, args } = new Test({ a }, '23'); // Enhanced Object Literals
alert(`Option a: ${options.a}, args: ${args.join(', ')}`); // Template Strings
Run atscm push
to upload the new display script to atvise server. Open your atvise project in your favorite browser (you may have to delete the browser cache) and if everything worked you should see an alert box containing the text "Option a: 13, args: 23". When you inspect the page's source you'll see the display script code was transpiled to ES5.
Step 7: Implement Transformer#transformFromDB
As said at the beginning, atscm transformers allow transformation from and to the filesystem. A babel transpilation is a one-way process, meaning you cannot create ES2015 source code from the resulting ES5 code. Therefore the only thing we can do when transforming from atvise server to the filesystem is to prevent an override.
We do so by implementing Transformer#transformFromDB:
// atscm/BabelTransformer.js
...
export default class BabelTransformer extends PartialTransfromer {
...
transformFromDB(file, enc, callback) {
// Optionally, we could print a warning here
callback(null); // Ignore file, remove it from the stream
}
}
Now we can run atscm push
without overriding our ES2015 source code.
Result
This is how your custom transformer should look now:
// atscm/BabelTransformer.js
import { Buffer } from 'buffer';
import { PartialTransformer } from 'atscm';
import { transform } from 'babel-core';
export default class BabelTransformer extends PartialTransformer {
shouldBeTransformed(file) {
return file.extname === '.js';
}
transformFromFilesystem(file, enc, callback) {
// Create ES5 code
const { code } = transform(file.contents, {
presets: ['es2015'],
});
// Create new file with ES5 content
const result = file.clone();
result.contents = Buffer.from(code);
// We're done, pass the new file to other streams
callback(null, result);
}
transformFromDB(file, enc, callback) {
callback(null); // Ignore file, remove it from the stream
}
}
Conclusion
We just created a custom Transformer in no time. It transpiles ES2015 code on push and prevents overriding this code on pull.
Of course there are many ways to improve the transformer, for example:
- Handle options to configure how babel transpiles the source code
Further reading
- babeljs.io provides a nice overview of ES2015 features. You can also use the REPL to try out these features.
Node ID conflicts
Note that rename files are not available for atscm < v1.0.0. Use
atscm update
to use the latest version
How atscm handles id conflicts
Let's assume we have two atvise server nodes, AGENT.OBJECT.conflictingnode and AGENT.OBJECT.ConflictingNode. These are valid node ids on the server, but when stored to the (case-insensitive) filesystem, the behaviour is undefined.
When atscm discovers such a name conflict it creates a rename file at ./atscm/rename.json
. This file will contain a map where the conflicting ids stored against the name to use to resolve the conflict. by default insert node name is used, e.g.:
{
"AGENT.OBJECTS.ConflictingNode": "insert node name"
}
How to resolve id conflicts
Once an id conflict is recognized and added to the rename file, it is your responsibility to provide non-conflicting node names, e.g.:
{
"AGENT.OBJECTS.ConflictingNode": "ConflictingNode-renamed"
}
After that run atscm pull
again to pull the conflicting nodes.
Guide: gulp.js plugins
Please note: This guide assumes you have a basic knowledge on how gulp.js and custom atscm transformers work. You may go through gulp's getting started guide or the custom transformer tutorial first otherwise.
atscm heavily relies on the gulp.js build tool. Therefore it's pretty easy to integrate existing gulp plugins into atscm transformers.
Using Transformer class
Basically, the only Transformer method you have to override is Transformer#applyToStream. In there, you can pipe your gulp plugin just as you would do in a regular gulp project. The only difference is, that you have to handle the current transform direction as well:
A basic example:
import { Transformer, TransformDirection } from 'atscm';
import fromDBGulpPlugin from 'gulp-plugin-to-use-from-db';
import fromFSGulpPlugin from 'gulp-plugin-to-use-from-fs';
class MyTransformer extends Transformer {
applyToStream(stream, direction) {
if (direction === TransformDirection.FromDB) {
return stream.pipe(fromDBGulpPlugin(/* plugin options */));
}
return stream.pipe(fromFSGulpPlugin(/* plugin options */));
}
}
Using PartialTransformer class
In most cases you'll have to transform only parts of the piped files. This can be done by inheriting from PartialTransfomer class:
Transforming only JavaScript files:
import { PartialTransformer, TransformDirection } from 'atscm';
import fromDBGulpPlugin from 'gulp-plugin-to-use-from-db';
import fromFSGulpPlugin from 'gulp-plugin-to-use-from-fs';
class MyPartialTransformer extends PartialTransformer {
shouldBeTransformed(file) {
return file.extname === '.js';
}
applyToFilteredStream(stream, direction) {
if (direction === TransformDirection.FromDB) {
return stream.pipe(fromDBGulpPlugin(/* plugin options */));
}
return stream.pipe(fromFSGulpPlugin(/* plugin options */));
}
}
Conclusion
Using existing gulp plugins is probably the easiest way to use custom transformers inside an atscm project. As there are thousands of well-tested gulp-plugins out there, you won't have to implemtent any transform logic in most cases.
Give it a try!
Further reading
- Take a look at gulp's plugin page for a list of available plugins.
Guide: Debugging atscm
atscm can be easily debugged using Google Chrome's developer tools. All you have to do to attach the debugger, is to start the command line interface with the --inspect
or --inspect-brk
flag. For this to work you must first find the path to atscm-cli's executable:
which atscm
Note: This only works on Linux and macOS only
You can now use this executable directly and run it with the inspector flags, e.g.:
node --inspect-brk "$(which atscm)" [arguments passed to atscm]
For further details on how to use the debugger, visit the offical nodejs docs on debugging.
Guide: Error handling
Adding source locations
When a throwing an Error that was caused by client code, you should provide location info so it can be traced back to the source code.
To do so, simply add additional properties to the error object, containing the source code, the start location and (optionally) the end location.
function myTransformCode() {
const sourceCode = 'the code transformed';
try {
// Do something that may throw an error...
} catch (error) {
Object.assign(error, {
rawLines: sourceCode, // A string containing the raw source code
location: {
start: {
line: 1, // The line number
column: 1, // The column number
},
// you could add a 'end' property here, with the same signature as 'start'
},
});
// Finally re-throw the error
throw error;
}
}
Example output: XML Parse error
[13:30:28] Using gulpfile ~/Downloads/delete/atscm-330/node_modules/atscm/out/Gulpfile.js
[13:30:28] Starting 'pull'...
[13:30:30] 'pull' errored after 1.09 s
[13:30:30] closing tag mismatch
5 | <atv:gridconfig width="20" gridstyle="lines" enabled="false" height="20"/>
6 | <atv:snapconfig width="10" enabled="false" height="10"/>
> 7 | </metadata>
| ^ closing tag mismatch
8 | </svg>
9 |
- Node: AGENT.DISPLAYS.Main
Details
@babel/code-frame
is used to render the frame.
Guide: Using the API
Learn how to use the atscm API to e.g. run server serverscripts in your node application. Available since atscm v1.0.0. Use
atscm update
to use the latest version
Installation
First of all, make sure your project has atscm installed: Take a look at your package.json file and make sure, atscm is present in the dependencies or (depending on your use case) devDependencies section. Otherwise, install atscm if necessary:
# If you need atscm as a runtime-dependency
npm install --save atscm
# If you need atscm as a development dependency (most likely)
npm install --save-dev atscm
Configuration
Similar to regular atscm projects, you need an Atviseproject file that contains atscm's configuration. A minimal example may look like this:
// Atviseproject.js
const { Atviseproject } = require('atscm');
module.exports = class ApiProject extends Atviseproject {
// Add your configuration here, if needed.
// By default, atvise server is assumed to run on opc.tcp://localhost:4840
};
Before you can finally require the atscm API in your project, you have to set the ATSCM_CONFIG_PATH
environment variable, pointing to your Atviseproject file. You can do this in multiple ways:
You can set it in your app at runtime (recommended)
Adjust your app's entry file (assuming it's called app.js in these examples) to set the variable before you import atscm:
// app.js const { join } = require('path'); process.env.ATSCM_CONFIG_PATH = join(__dirname, '../Atviseproject.js'); // Your app comes here...
You can set it every time you run your application:
E.g. instead of running your app with
node ./app.js
you can useATSCM_CONFIG_PATH="/my-project/Atviseproject.js" node ./app.js
.You can also use npm scripts so you simply have to run
npm run start
:// package.json { "scripts": { "start": "ATSCM_CONFIG_PATH='/my-project/Atviseproject.js' node ./app.js" } }
If your running on windows, use
cross-env
to set the environment variable (don't forgetnpm install cross-env
):// package.json { "scripts": { "start": "cross-env ATSCM_CONFIG_PATH='/my-project/Atviseproject.js' node ./app.js" } }
Usage
Require atscm/api
and call the methods you need:
// Set process.env.ATSCM_CONFIG_PATH here...
const atscm = require('atscm/api');
// You can use atscm here...
Examples
Create an export file for a node
// app.js
// Import node core modules
const { promises: fsp } = require('fs');
const { join, dirname } = require('path');
// Set atscm config env variable
process.env.ATSCM_CONFIG_PATH = join(__dirname, './Atviseproject.js');
// Require atscm and node-opcua APIs
const { NodeId } = require('atscm');
const { callMethod } = require('atscm/api');
const { Variant, DataType, VariantArrayType } = require('node-opcua');
// Configuration: You could also use process.argv here...
const nodesToExport = ['AGENT.DISPLAYS.Main'];
const exportPath = './out/export.xml';
// Our main function
async function createExportFile() {
console.log(`Exporting nodes: ${nodesToExport.join(',')}`);
// Use the 'exportNodes' method to create an xml export on the server
const {
outputArguments: [{ value }],
} = await callMethod(new NodeId('AGENT.OPCUA.METHODS.exportNodes'), [
new Variant({
dataType: DataType.NodeId,
arrayType: VariantArrayType.Array,
value: nodesToExport.map((id) => new NodeId(id)),
}),
]);
// Create the output directory if needed
await fsp.mkdir(dirname(exportPath), { recursive: true });
// Write the export to the file
await fsp.writeFile(exportPath, value);
console.log(`Export written to ${exportPath}`);
}
// Run it and catch any errors
createExportFile().catch((error) => {
console.error(error);
process.exitCode = 1;
});
Note: The example assumes the Atviseproject file is located in the same directory as the app's entry file. Otherwise you have to adjust you code accordingly.
Contribute: Testing atscm
atscm uses both unit and integration tests. Mocha is used as a test runner and nyc for test coverage reports.
Test scripts:
Command | Description |
---|---|
npm run test |
Run all tests |
npm run test:unit |
Run all unit tests |
npm run test:integration |
Run all integration tests |
npm run test:watch |
Re-run all tests when files change |
npm run test:coverage |
Check test coverage |
Unit tests
The unit tests are located inside ./test/src
. Test files are named after the module they test, e.g. unit tests for ./src/my/module.js
are inside ./test/src/my/module.spec.js
.
Integration tests
Integration tests are used to ensure cross-version and -platform compatibility. They are located inside ./test/integration
.
Test setups
For most integration tests, we use test setups to create the proper project structure to test against. These are XML files that can be imported to the running atserver before the tests are run. The easiest way to create such files is with atbuilder:
- First connect atvise builder to your running atserver
- Create and configure the node(s) you want to test against
- Select theses nodes, right-click them and choose "Export hierarchy to XML" from the context menu.
- Save the export file to
./test/fixtures/setup
.
After this, you can use the importSetup method exported from ./test/helpers/atscm.js
to import this setup. See the existing integration tests for examples.
Contributing
We would love for you to contribute to atSCM. As a contributor, here are the guidelines we would like you to follow:
Found a bug?
If you find a bug in the source code, you can help us by submitting an issue to this repository. Even better, you can submit a Pull Request with a fix.
Missing a feature?
You can request a new feature by submitting an issue to this repository. If you would like to implement a new feature, please submit an issue with a proposal for your work first, to be sure that we can use it.
Submission Guidelines
Submitting an issue
Before you submit an issue, please search the issue tracker, maybe an issue for your problem already exists and the discussion might inform you of workarounds readily available.
We can only fix an issue if we can reproduce it. To help us provide the following information in your issue description:
- The original error message: Any console output regarding the issue. Consider running atscm with verbose logging (using the command line option
-LLLL
) to get more error details. - atscm and atscm-cli versions used: The results of
atscm --version
. - atvise server version used
- node and npm versions used: The results of
node --version
andnpm version
. - Special project setup: Any default overrides to your
Atviseproject.js
file, such as custom Transformers.
Submitting a Pull Request (PR)
Before you submit your Pull Request (PR) consider the following guidelines:
- Search GitHub for an open or closed PR that relates to your submission. You don't want to duplicate effort.
- Make your changes in a new git branch: Run
git checkout -b my-fix-branch master
- Create your patch, including appropriate test cases.
- Run the full test suite and ensure all tests pass.
- Commit your changes using a descriptive commit message that follows our commit message conventions. Adherence to these conventions is necessary because release notes are automatically generated from these messages.
- Push your branch to GitHub and create a pull request to merge back to the beta branch.
- Once we reviewed your changes, we'll merge your pull request.
Merge strategy (Maintainers only)
- Accepted changes from fix/feature branches should always be squash-merged to beta.
- Once beta is stable create a regular merge commit to merge back to master.
- After merging to master, changes should be synced back to the beta branch. To do so, run:
git checkout beta git fetch git rebase origin/master # Solve conflicts if any, accepting changes from master git commit -m 'chore: Update from master' git push
Code quality control
All files inside this project are automatically built, linted and tested by CircleCI.
Builds will only pass if they meet the following criteria:
- No ESLint errors: We use ESLint to lint our entire JavaScript code. The config used is eslint-config-lsage. Any lint errors will cause the build to fail.
- Test coverage >= 90%: We use istanbul to validate test coverage is at least 90 percent. Any commits not covered by tests will cause the build to fail.
- Documentation coverage >= 90%: Our source code is documented using ESDoc. We will only merge if your contribution is documented as well.
Setting up the development environment
In order to meet out coding guideline it's very useful to have your development environment set up right.
Linting files
You can lint all source files by running npm run lint
. Although most IDEs support running it directly in the editor:
Jetbrains Webstorm
Webstorm has built-in support for ESLint. Check out their documentation to set it up.
Atom
Atom has several packages that provide support for inline ESLint validation. We recommend you to use linter-eslint.
Running tests
Our mocha tests can be run by calling npm test
. If you want the tests to be run right after you saved your changes, then run npm run test:watch
.
Setup needed to run tests on atvise server
Please note, that you have to provide a valid atvise server connection in order to get tests against atvise server running. You can achieve that by doing one of the following:
- Set environment variables
ATVISE_USERNAME
andATVISE_PASSWORD
to valid credentials for the public atvise demo server at demo.ativse.com. - Adapt host, ports and login credentials inside
./test/fixtures/Atviseproject.babel.js
.
Check test coverage
Test coverage can be checked by running npm run test:coverage
.
Creating API documentation
Run npm run docs
to create ESDoc API documentation.
Commit Message Guideline
We have very precise rules over how our git commit messages can be formatted. This leads to more readable messages that are easy to follow when looking through the project history. But also, we use the git commit messages to generate the changelog.
Commit message format
tl;dr: We use an adaption of the angular commit message convention with the only difference that capitalized subjects are allowed.
Each commit message consists of a header, a body and a footer. The header has a special format that includes a type, a scope and a subject:
<type>(<scope>): <subject>
<body>
<footer>
The header is mandatory and the scope of the header is optional. It cannot be longer than 72 characters.
Samples
Describe a documentation change
docs(changelog): Update changelog for version 1.2.3
Describes a bug fix affecting mapping
fix(mapping): Replace invalid data type for html help documents Prevents html help documents to have an invalid extension unter atvise server v3.1.0. Closes #123
Type
Must be one of the following:
- build: Changes that affect the build system or external dependencies (example scopes: babel, npm)
- chore: Maintainance tasks (example tasks: release)
- ci: Changes to our CI configuration files and scripts (example scopes: circleci, appveyor, codecov)
- docs: Documentation only changes
- feat: A new feature
- fix: A bug fix
- perf: A code change that improves performance
- refactor: A code change that neither fixes a bug nor adds a feature
- revert: Reverts a previous commit.
- style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
- test: Adding missing tests or correcting existing tests
Scope
The scope should describe the feature affected. Must be lower case.
Subject
The subject contains succinct description of the change:
- Use the imperative, present tense: "change" not "changed" nor "changes"
- Capitalize first letter (The only notable difference to the angular commit message convention)
- no dot (.) at the end
Body
Just as in the subject, use the imperative, present tense: "change" not "changed" nor "changes". The body should include the motivation for the change and contrast this with previous behavior.
Footer
The footer should contain any information about Breaking Changes and is also the place to reference GitHub issues that this commit Closes.
Breaking Changes should start with the word BREAKING CHANGE:
with a space or two newlines. The rest of the commit message is then used for this.
Commit message linting
The project is setup to use a git hook that lints commit messages before creating a commit. Do not bypass this hook.