Home Manual Reference Source Test

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:

  1. Core node.js modules (buffer in our case)
  2. Other external modules (babel-core and atscm in our case)
  3. 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

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

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 use ATSCM_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 forget npm 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 and npm 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 and ATVISE_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.

See husky's documentation on git GUI support.

atscm-v1.1.1 (2021-01-18)

Bug Fixes