Repository: bpmn-io/bpmnlint (js)

Issue:
Support scoped packages
We are using GitHub Registry which currently only supports scoped packages. As a result, none of our packages can start with `bpmnlint-plugin-`. Is it possible to allow packages that are scoped, but start with that rule? An example would be `@myorg/bpmlint-plugin-my-org-rules`.

Pull request description:
feat: support scoped rules
Intended to support scoped packages as highlighted in Issue https://github.com/bpmn-io/bpmnlint/issues/35.

Closes #35

Patch:
diff --git a/lib/linter.js b/lib/linter.js
index 79bc8fc..8a546e3 100644
--- a/lib/linter.js
+++ b/lib/linter.js
@@ -125,7 +125,7 @@ Linter.prototype.resolveConfig = function(name) {
       throw new Error(`unknown config <${name}>`);
     }
 
-    const actualConfig = this.cachedConfigs[id] = normalizeConfig(config, pkg);
+    const actualConfig = this.cachedConfigs[id] = this.normalizeConfig(config, pkg);
 
     return actualConfig;
   });
@@ -199,7 +199,7 @@ Linter.prototype.resolveConfiguredRules = function(config) {
     })
   ).then((inheritedRules) => {
 
-    const overrideRules = normalizeConfig(config, 'bpmnlint').rules;
+    const overrideRules = this.normalizeConfig(config, 'bpmnlint').rules;
 
     const rules = [ ...inheritedRules, overrideRules ].reduce((rules, currentRules) => {
       return {
@@ -275,93 +275,145 @@ Linter.prototype.parseRuleValue = function(value) {
   };
 };
 
-Linter.prototype.parseRuleName = function(name) {
+Linter.prototype.parseRuleName = function(name, localPackage = 'bpmnlint') {
 
-  const slashIdx = name.indexOf('/');
+  /**
+   * We recognize the following rule name patterns:
+   *
+   * {RULE_NAME} => PKG = 'bpmnlint'
+   * bpmnlint/{RULE_NAME} => PKG = 'bpmnlint'
+   * {PACKAGE_SHORTCUT}/{RULE_NAME} => PKG = 'bpmnlint-plugin-{PACKAGE_SHORTCUT}'
+   * bpmnlint-plugin-{PACKAGE_SHORTCUT}/{RULE_NAME} => PKG = 'bpmnlint-plugin-{PACKAGE_SHORTCUT}'
+   * @scope/{PACKAGE_SHORTCUT}/{RULE_NAME} => PKG = '@scope/bpmnlint-plugin-{PACKAGE_SHORTCUT}'
+   * @scope/bpmnlint-plugin-{PACKAGE_SHORTCUT}/{RULE_NAME} => PKG = '@scope/bpmnlint-plugin-{PACKAGE_SHORTCUT}'
+   */
 
-  // resolve rule as built-in, if unprefixed
-  if (slashIdx === -1) {
-    return {
-      pkg: 'bpmnlint',
-      ruleName: name
-    };
+  const match = /^(?:(?:(@[^/]+)\/)?([^@]{1}[^/]*)\/)?([^/]+)$/.exec(name);
+
+  if (!match) {
+    throw new Error(`unparseable rule name <${name}>`);
   }
 
-  const pkg = name.substring(0, slashIdx);
-  const ruleName = name.substring(slashIdx + 1);
+  const [
+    _,
+    ns,
+    packageName,
+    ruleName
+  ] = match;
 
-  if (pkg === 'bpmnlint') {
+  if (!packageName) {
     return {
-      pkg: 'bpmnlint',
-      ruleName
-    };
-  } else {
-    return {
-      pkg: 'bpmnlint-plugin-' + pkg,
+      pkg: localPackage,
       ruleName
     };
   }
+
+  const pkg = `${ns ? ns + '/' : '' }${prefixPackage(packageName)}`;
+
+  return {
+    pkg,
+    ruleName
+  };
 };
 
 
 Linter.prototype.parseConfigName = function(name) {
 
-  const localMatch = /^bpmnlint:(.*)$/.exec(name);
+  /**
+   * We recognize the following config name patterns:
+   *
+   * bpmnlint:{CONFIG_NAME} => PKG = 'bpmnlint'
+   * plugin:{PACKAGE_SHORTCUT}/{CONFIG_NAME} => PKG = 'bpmnlint-plugin-{PACKAGE_SHORTCUT}'
+   * plugin:bpmnlint-plugin-{PACKAGE_SHORTCUT}/{CONFIG_NAME} => PKG = 'bpmnlint-plugin-{PACKAGE_SHORTCUT}'
+   * plugin:@scope/{PACKAGE_SHORTCUT}/{CONFIG_NAME} => PKG = '@scope/bpmnlint-plugin-{PACKAGE_SHORTCUT}'
+   * plugin:@scope/bpmnlint-plugin-{PACKAGE_SHORTCUT}/{CONFIG_NAME} => PKG = '@scope/bpmnlint-plugin-{PACKAGE_SHORTCUT}'
+   */
+
+  const match = /^(?:(?:plugin:(?:(@[^/]+)\/)?([^@]{1}[^/]*)\/)|bpmnlint:)([^/]+)$/.exec(name);
+
+  if (!match) {
+    throw new Error(`unparseable config name <${name}>`);
+  }
+
+  const [
+    _,
+    ns,
+    packageName,
+    configName
+  ] = match;
 
-  if (localMatch) {
+  if (!packageName) {
     return {
       pkg: 'bpmnlint',
-      configName: localMatch[1]
+      configName
     };
   }
 
-  const pluginMatch = /^plugin:([^/]+)\/(.+)$/.exec(name);
-
-  if (!pluginMatch) {
-    throw new Error(`invalid config name <${ name }>`);
-  }
+  const pkg = `${ns ? ns + '/' : '' }${prefixPackage(packageName)}`;
 
   return {
-    pkg: 'bpmnlint-plugin-' + pluginMatch[1],
-    configName: pluginMatch[2]
+    pkg,
+    configName
   };
 };
 
 
-// helpers ///////////////////////////
+Linter.prototype.getSimplePackageName = function(name) {
+
+  /**
+   * We recognize the following package name patterns:
+   *
+   * bpmnlint => PKG = 'bpmnlint'
+   * {PACKAGE_SHORTCUT} => PKG = PACKAGE_SHORTCUT
+   * bpmnlint-plugin-{PACKAGE_SHORTCUT}' => PKG = PACKAGE_SHORTCUT
+   * @scope/{PACKAGE_SHORTCUT} => PKG = '@scope/{PACKAGE_SHORTCUT}'
+   * @scope/bpmnlint-plugin-{PACKAGE_SHORTCUT}' => PKG = '@scope/PACKAGE_SHORTCUT'
+   */
+
+  const match = /^(?:(@[^/]+)\/)?([^/]+)$/.exec(name);
+
+  if (!match) {
+    throw new Error(`unparseable package name <${name}>`);
+  }
+
+  const [
+    _,
+    ns,
+    packageName
+  ] = match;
+
+  return `${ns ? ns + '/' : '' }${unprefixPackage(packageName)}`;
+};
+
 
 /**
  * Validate and return validated config.
  *
  * @param  {Object} config
- * @param  {String} pkg
+ * @param  {String} localPackage
  *
  * @return {Object} validated config
  */
-function normalizeConfig(config, pkg) {
+Linter.prototype.normalizeConfig = function(config, localPackage) {
 
   const rules = config.rules || {};
 
-  const rulePrefix = pkg.startsWith('bpmnlint-plugin-') && pkg.replace('bpmnlint-plugin-', '');
-
   const validatedRules = Object.keys(rules).reduce((normalizedRules, name) => {
 
     const value = rules[name];
 
-    // drop bpmnlint prefix, if existing
-    if (name.startsWith('bpmnlint/')) {
-      name = name.replace('bpmnlint/', '');
-    } else
-
-    if (rulePrefix) {
+    const {
+      pkg,
+      ruleName
+    } = this.parseRuleName(name, localPackage);
 
-      // prefix local rule definition
-      if (!name.startsWith(rulePrefix)) {
-        name = `${rulePrefix}/${name}`;
-      }
-    }
+    const normalizedName = (
+      pkg === 'bpmnlint'
+        ? ruleName
+        : `${this.getSimplePackageName(pkg)}/${ruleName}`
+    );
 
-    normalizedRules[name] = value;
+    normalizedRules[normalizedName] = value;
 
     return normalizedRules;
   }, {});
@@ -370,4 +422,30 @@ function normalizeConfig(config, pkg) {
     ...config,
     rules: validatedRules
   };
+};
+
+
+// helpers ///////////////////////////
+
+function prefixPackage(pkg) {
+
+  if (pkg === 'bpmnlint') {
+    return 'bpmnlint';
+  }
+
+  if (pkg.startsWith('bpmnlint-plugin-')) {
+    return pkg;
+  }
+
+  return `bpmnlint-plugin-${pkg}`;
+}
+
+
+function unprefixPackage(pkg) {
+
+  if (pkg.startsWith('bpmnlint-plugin-')) {
+    return pkg.substring('bpmnlint-plugin-'.length);
+  }
+
+  return pkg;
 }
\ No newline at end of file


Test patch:
diff --git a/test/integration/bpmnlint-plugin-test-ns/index.js b/test/integration/bpmnlint-plugin-test-ns/index.js
new file mode 100644
index 0000000..3532799
--- /dev/null
+++ b/test/integration/bpmnlint-plugin-test-ns/index.js
@@ -0,0 +1,9 @@
+module.exports = {
+  configs: {
+    recommended: {
+      rules: {
+        'no-label-xxx': 'error'
+      }
+    }
+  }
+};
\ No newline at end of file
diff --git a/test/integration/bpmnlint-plugin-test-ns/package.json b/test/integration/bpmnlint-plugin-test-ns/package.json
new file mode 100644
index 0000000..6d29ea9
--- /dev/null
+++ b/test/integration/bpmnlint-plugin-test-ns/package.json
@@ -0,0 +1,6 @@
+{
+  "name": "@ns/bpmnlint-plugin-test",
+  "version": "0.0.0",
+  "description": "A namespaced test bpmnlint plug-in",
+  "main": "index.js"
+}
diff --git a/test/integration/bpmnlint-plugin-test-ns/rules/no-label-xxx.js b/test/integration/bpmnlint-plugin-test-ns/rules/no-label-xxx.js
new file mode 100644
index 0000000..ad2818e
--- /dev/null
+++ b/test/integration/bpmnlint-plugin-test-ns/rules/no-label-xxx.js
@@ -0,0 +1,15 @@
+/**
+ * Rule that reports xxx labels.
+ */
+module.exports = function() {
+
+  function check(node, reporter) {
+    if (/^xxx/.test(node.name || '')) {
+      reporter.report(node.id, 'Element has non-sense label <' + node.name + '>');
+    }
+  }
+
+  return {
+    check: check
+  };
+};
\ No newline at end of file
diff --git a/test/integration/cli-spec.js b/test/integration/cli-spec.js
index ebbfd95..5dfeefc 100644
--- a/test/integration/cli-spec.js
+++ b/test/integration/cli-spec.js
@@ -158,6 +158,73 @@ describe('cli', function() {
 
   });
 
+
+  describe('should handle namespaced packages', function() {
+
+    before(function() {
+
+      this.timeout(30000);
+
+      return exec('install-local', [], __dirname + '/cli/ns');
+    });
+
+
+    describe('providing rules', function() {
+
+      test({
+        cmd: [ 'bpmnlint', '-c', 'uses-rules.json', 'diagram.bpmn' ],
+        cwd: __dirname + '/cli/ns',
+      });
+
+
+      test({
+        cmd: [ 'bpmnlint', '-c', 'uses-rules.json', 'diagram-invalid.bpmn' ],
+        cwd: __dirname + '/cli/ns',
+        expect: {
+          code: 1,
+          stderr: EMPTY,
+          stdout: `
+
+            ${diagramPath('ns/diagram-invalid.bpmn')}
+              StartEvent  error  Element has non-sense label <xxx>  test2/no-label-xxx
+              StartEvent  error  Element has non-sense label <xxx>  @ns/test/no-label-xxx
+
+            ✖ 2 problems (2 errors, 0 warnings)
+          `
+        }
+      });
+
+    });
+
+
+    describe('providing configuration', function() {
+
+      test({
+        cmd: [ 'bpmnlint', '-c', 'extends.json', 'diagram.bpmn' ],
+        cwd: __dirname + '/cli/ns',
+      });
+
+
+      test({
+        cmd: [ 'bpmnlint', '-c', 'extends.json', 'diagram-invalid.bpmn' ],
+        cwd: __dirname + '/cli/ns',
+        expect: {
+          code: 1,
+          stderr: EMPTY,
+          stdout: `
+
+            ${diagramPath('ns/diagram-invalid.bpmn')}
+              StartEvent  error  Element has non-sense label <xxx>  @ns/test/no-label-xxx
+              StartEvent  error  Element has non-sense label <xxx>  test2/no-label-xxx
+
+            ✖ 2 problems (2 errors, 0 warnings)
+          `
+        }
+      });
+
+    });
+  });
+
 });
 
 
diff --git a/test/integration/cli/ns/diagram-invalid.bpmn b/test/integration/cli/ns/diagram-invalid.bpmn
new file mode 100644
index 0000000..bbbdde9
--- /dev/null
+++ b/test/integration/cli/ns/diagram-invalid.bpmn
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="sid-38422fae-e03e-43a3-bef4-bd33b32041b2" targetNamespace="http://bpmn.io/bpmn" exporter="Camunda Modeler" exporterVersion="2.2.4">
+  <process id="Process" isExecutable="false">
+    <startEvent id="StartEvent" name="xxx" />
+  </process>
+  <bpmndi:BPMNDiagram id="BpmnDiagram_1">
+    <bpmndi:BPMNPlane id="BpmnPlane_1" bpmnElement="Process">
+      <bpmndi:BPMNShape id="StartEvent_di" bpmnElement="StartEvent">
+        <omgdc:Bounds x="218" y="160" width="36" height="36" />
+        <bpmndi:BPMNLabel>
+          <omgdc:Bounds x="228" y="203" width="17" height="14" />
+        </bpmndi:BPMNLabel>
+      </bpmndi:BPMNShape>
+    </bpmndi:BPMNPlane>
+  </bpmndi:BPMNDiagram>
+</definitions>
diff --git a/test/integration/cli/ns/diagram.bpmn b/test/integration/cli/ns/diagram.bpmn
new file mode 100644
index 0000000..b1ede40
--- /dev/null
+++ b/test/integration/cli/ns/diagram.bpmn
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="sid-38422fae-e03e-43a3-bef4-bd33b32041b2" targetNamespace="http://bpmn.io/bpmn" exporter="Camunda Modeler" exporterVersion="2.2.4">
+  <process id="Process" isExecutable="false">
+    <startEvent id="StartEvent" name="GOOD" />
+  </process>
+  <bpmndi:BPMNDiagram id="BpmnDiagram_1">
+    <bpmndi:BPMNPlane id="BpmnPlane_1" bpmnElement="Process">
+      <bpmndi:BPMNShape id="StartEvent_di" bpmnElement="StartEvent">
+        <omgdc:Bounds x="218" y="160" width="36" height="36" />
+        <bpmndi:BPMNLabel>
+          <omgdc:Bounds x="220" y="203" width="34" height="14" />
+        </bpmndi:BPMNLabel>
+      </bpmndi:BPMNShape>
+    </bpmndi:BPMNPlane>
+  </bpmndi:BPMNDiagram>
+</definitions>
diff --git a/test/integration/cli/ns/extends.json b/test/integration/cli/ns/extends.json
new file mode 100644
index 0000000..c6b9af8
--- /dev/null
+++ b/test/integration/cli/ns/extends.json
@@ -0,0 +1,6 @@
+{
+  "extends": "plugin:@ns/bpmnlint-plugin-test/recommended",
+  "rules": {
+    "test2/no-label-xxx": "error"
+  }
+}
\ No newline at end of file
diff --git a/test/integration/cli/ns/package.json b/test/integration/cli/ns/package.json
new file mode 100644
index 0000000..342ff35
--- /dev/null
+++ b/test/integration/cli/ns/package.json
@@ -0,0 +1,9 @@
+{
+  "name": "ns",
+  "version": "0.0.0",
+  "private": true,
+  "localDependencies": {
+    "bpmnlint-plugin-test2": "../../bpmnlint-plugin-test2",
+    "@ns/bpmnlint-plugin-test": "../../bpmnlint-plugin-test-ns"
+  }
+}
diff --git a/test/integration/cli/ns/uses-rules.json b/test/integration/cli/ns/uses-rules.json
new file mode 100644
index 0000000..46b13f0
--- /dev/null
+++ b/test/integration/cli/ns/uses-rules.json
@@ -0,0 +1,6 @@
+{
+  "rules": {
+    "test2/no-label-xxx": "error",
+    "@ns/bpmnlint-plugin-test/no-label-xxx": "error"
+  }
+}
\ No newline at end of file
diff --git a/test/spec/linter-spec.js b/test/spec/linter-spec.js
index 6d9c2b4..fb2cac7 100644
--- a/test/spec/linter-spec.js
+++ b/test/spec/linter-spec.js
@@ -422,6 +422,129 @@ describe('linter', function() {
 
       });
 
+
+      describe('scoped', function() {
+
+        // given
+        const resolver = {
+
+          resolveRule(pkg, ruleName) {
+            return { check() { } };
+          },
+
+          resolveConfig(pkg, configName) {
+
+            if (pkg === 'bpmnlint-plugin-test') {
+              if (configName === 'recommended') {
+                return {
+                  rules: {
+                    '@ns/bpmnlint-plugin-test/bar': 'warn'
+                  }
+                };
+              }
+            }
+
+            if (pkg === '@ns/bpmnlint-plugin-test') {
+              if (configName === 'recommended') {
+                return {
+                  rules: {
+                    'bar': 'off'
+                  }
+                };
+              }
+
+              if (configName === 'other') {
+                return {
+                  rules: {
+                    'foo': 'warn'
+                  }
+                };
+              }
+            }
+
+            throw new Error(`unexpected config <${configName}>`);
+          }
+
+        };
+
+
+        it('should normalize shortcut', async function() {
+
+          // given
+          const linter = new Linter({ resolver });
+
+          const config = {
+            extends: [
+              'plugin:@ns/test/other'
+            ],
+            rules: {
+              '@ns/bpmnlint-plugin-test/foo': 'error'
+            }
+          };
+
+          // when
+          const rules = await linter.resolveConfiguredRules(config);
+
+          // then
+          expect(rules).to.eql({
+            '@ns/test/foo': 'error'
+          });
+        });
+
+
+        it('should normalize full package name', async function() {
+
+          // given
+          const linter = new Linter({ resolver });
+
+          const config = {
+            extends: [
+              'plugin:@ns/bpmnlint-plugin-test/other'
+            ],
+            rules: {
+              '@ns/test/foo': 'error'
+            }
+          };
+
+          // when
+          const rules = await linter.resolveConfiguredRules(config);
+
+          // then
+          expect(rules).to.eql({
+            '@ns/test/foo': 'error'
+          });
+        });
+
+
+        it('should normalize multiple', async function() {
+
+          // given
+          const linter = new Linter({ resolver });
+
+          const config = {
+            extends: [
+              'plugin:@ns/test/other',
+              'plugin:@ns/bpmnlint-plugin-test/recommended',
+              'plugin:test/recommended'
+            ],
+            rules: {
+              '@ns/bpmnlint-plugin-test/bar': 'off',
+              '@ns/bpmnlint-plugin-test/foo': 'error'
+            }
+          };
+
+          // when
+          const rules = await linter.resolveConfiguredRules(config);
+
+          // then
+          expect(rules).to.eql({
+            '@ns/test/foo': 'error',
+            '@ns/test/bar': 'off'
+          });
+        });
+
+      });
+
     });
 
 
@@ -457,7 +580,7 @@ describe('linter', function() {
   });
 
 
-  describe('#parseConfigName', function() {
+  describe('#parseRuleName', function() {
 
     const linter = new Linter({
       resolver: fakeRule({})
@@ -502,6 +625,61 @@ describe('linter', function() {
       });
     });
 
+
+    it('should parse external with full package name', function() {
+
+      // when
+      const parsed = linter.parseRuleName('bpmnlint-plugin-foo/label-required');
+
+      // then
+      expect(parsed).to.eql({
+        pkg: 'bpmnlint-plugin-foo',
+        ruleName: 'label-required'
+      });
+    });
+
+
+    it ('should parse scoped', function() {
+
+      // when
+      const parsed = linter.parseRuleName('@scoped/bpmnlint-plugin-foo/label-required');
+
+      // then
+      expect(parsed).to.eql({
+        pkg: '@scoped/bpmnlint-plugin-foo',
+        ruleName: 'label-required'
+      });
+    });
+
+
+    it ('should parse scoped with full package name', function() {
+
+      // when
+      const parsed = linter.parseRuleName('@scoped/foo/label-required');
+
+      // then
+      expect(parsed).to.eql({
+        pkg: '@scoped/bpmnlint-plugin-foo',
+        ruleName: 'label-required'
+      });
+    });
+
+
+    it ('should fail to parse with missing package', function() {
+
+      expect(() => {
+        linter.parseRuleName('@scoped/label-required');
+      }).to.throw('unparseable rule name <@scoped/label-required>');
+    });
+
+
+    it ('should fail to parse nonsense', function() {
+
+      expect(() => {
+        linter.parseRuleName('foo/bar/goings');
+      }).to.throw('unparseable rule name <foo/bar/goings>');
+    });
+
   });
 
 
@@ -555,12 +733,69 @@ describe('linter', function() {
     });
 
 
+    it('should parse external with full package name', function() {
+
+      // when
+      const parsed = linter.parseConfigName('plugin:bpmnlint-plugin-foo/bar');
+
+      // then
+      expect(parsed).to.eql({
+        pkg: 'bpmnlint-plugin-foo',
+        configName: 'bar'
+      });
+    });
+
+
+    it('should parse scoped', function() {
+      const parsed = linter.parseConfigName('plugin:@ns/foo/bar');
+
+      expect(parsed).to.eql({
+        pkg: '@ns/bpmnlint-plugin-foo',
+        configName: 'bar'
+      });
+    });
+
+
+    it('should parse scoped with full package name', function() {
+      const parsed = linter.parseConfigName('plugin:@ns/bpmnlint-plugin-foo/bar');
+
+      expect(parsed).to.eql({
+        pkg: '@ns/bpmnlint-plugin-foo',
+        configName: 'bar'
+      });
+    });
+
+
     it('should throw on invalid name', async function() {
 
       expect(() => {
         linter.parseConfigName('foo:bar');
-      }).to.throw('invalid config name <foo:bar>');
+      }).to.throw('unparseable config name <foo:bar>');
+
+    });
+
+
+    it('should throw error on invalid scoped name', function() {
 
+      expect(() => {
+        linter.parseConfigName('plugin:@ns/not-valid');
+      }).to.throw('unparseable config name <plugin:@ns/not-valid>');
+    });
+
+
+    it('should throw error on namespaced default config', function() {
+
+      expect(() => {
+        linter.parseConfigName('bpmnlint:@ns/not-valid');
+      }).to.throw('unparseable config name <bpmnlint:@ns/not-valid>');
+    });
+
+
+    it('should throw error on non-sense name', function() {
+
+      expect(() => {
+        linter.parseConfigName('bpmnlint:asd/not-valid/asd');
+      }).to.throw('unparseable config name <bpmnlint:asd/not-valid/asd>');
     });
 
   });
diff --git a/test/spec/support/compile-config-spec.js b/test/spec/support/compile-config-spec.js
index 77975c0..238d8b1 100644
--- a/test/spec/support/compile-config-spec.js
+++ b/test/spec/support/compile-config-spec.js
@@ -30,6 +30,21 @@ describe('support/compile-config', function() {
   });
 
 
+  it('should import namespaced', async function() {
+
+    // when
+    const code = await compileConfig({
+      rules: {
+        '@foo/bar/rule': 'warn'
+      }
+    });
+
+    // then
+    expect(code).to.contain('import rule_0 from \'@foo/bpmnlint-plugin-bar/rules/rule\'');
+    expect(code).to.contain('cache[\'@foo/bpmnlint-plugin-bar/rule\'] = rule_0');
+  });
+
+
   it('should resolve extends', async function() {
 
     // when