@companion-module/tools 2.7.2 → 2.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.9.0](https://github.com/bitfocus/companion-module-tools/compare/v2.8.0...v2.9.0) (2026-10-29)
4
+
5
+
6
+ ### Features
7
+
8
+ * support node26 ([51caa8c](https://github.com/bitfocus/companion-module-tools/commit/51caa8c05b054537184677e74292c9631d0052c5))
9
+
10
+ ## [2.8.0](https://github.com/bitfocus/companion-module-tools/compare/v2.7.2...v2.8.0) (2026-08-28)
11
+
12
+
13
+ ### Features
14
+
15
+ * backport license validator ([d26e43c](https://github.com/bitfocus/companion-module-tools/commit/d26e43cd581824f37f95099342bf015e6097b9f7))
16
+
17
+
3
18
  ## [2.7.2](https://github.com/bitfocus/companion-module-tools/compare/v2.7.1...v2.7.2) (2026-05-11)
4
19
 
5
20
 
package/README.md CHANGED
@@ -14,6 +14,49 @@ More information on this command is available [on the wiki](https://github.com/b
14
14
 
15
15
  Generate the new format manifest from an old `package.json`.
16
16
 
17
+ ## Node.js versions
18
+
19
+ Modules declare the Node.js version they run on with `runtime.type` in `companion/manifest.json`. New modules, and
20
+ existing ones when convenient, are recommended to use `node26`.
21
+
22
+ A matching TypeScript config is available for each version, and `node26` is the recommended one:
23
+
24
+ ```json
25
+ {
26
+ "extends": "@companion-module/tools/tsconfig/node26/recommended.json"
27
+ }
28
+ ```
29
+
30
+ It uses `"module": "node20"`, so a file is compiled as ESM or CommonJS according to the `type` in its nearest
31
+ `package.json`. It targets ES2025, which requires TypeScript 6 or later. The `node18` and `node22` configs remain
32
+ available for modules that have not moved yet.
33
+
34
+ ## Licensing
35
+
36
+ A module has two licenses, and they are not the same thing:
37
+
38
+ - `license` in `package.json` is the license of the code you wrote. This must be `MIT`, so module source always stays
39
+ portable and can be reused anywhere.
40
+ - `license` in `companion/manifest.json` is the license the packaged module is distributed under. A build bundles your
41
+ code together with its dependencies, so this has to be a license that whole blob can be shipped under.
42
+
43
+ Your own code stays MIT even when a dependency is copyleft. If you bundle a GPL-3.0 dependency, your source stays MIT
44
+ while the manifest declares `GPL-3.0-only`, because that is what the combined package must be distributed as. Anyone
45
+ reusing your source on its own still gets it under MIT.
46
+
47
+ The build and check commands validate your production dependencies against the manifest license. Supported values are
48
+ `MIT` (recommended, it can use the most of npm), `GPL-2.0-only` and `GPL-3.0-only`. Dual licensing is not supported,
49
+ and a module which declares no license in its manifest falls back to the one in `package.json`.
50
+
51
+ Everything reachable through `dependencies` is checked, whether or not webpack ends up bundling it. `devDependencies`
52
+ are never checked. A build also writes an aggregated `LICENSE` (and a `NOTICE`, where dependencies ship one) into the
53
+ package, collected from the license files of everything it ships.
54
+
55
+ Some older packages ship a license file but declare no license in their `package.json`, so there is nothing to check
56
+ against. Those are listed in `scripts/lib/known-package-licenses.js` once their license has been confirmed from what
57
+ they publish, so open a PR there if you hit one. A license a package declares itself always wins over that list.
58
+
59
+ Pass `--ignore-license-rules` to report license problems as warnings instead of failing the command.
17
60
 
18
61
  ## Upgrading from v1.x to v2.0
19
62
 
@@ -21,8 +64,8 @@ v2.0 of this library includes some breaking changes to how eslint and prettier h
21
64
 
22
65
  This change was done for two reasons:
23
66
 
24
- 1) Very few modules use eslint, making this extra weight for them for no gain.
25
- 2) Recent versions of yarn do not expose these binaries in a way which is easily callable by modules, requiring tricks to be able to execute them.
67
+ 1. Very few modules use eslint, making this extra weight for them for no gain.
68
+ 2. Recent versions of yarn do not expose these binaries in a way which is easily callable by modules, requiring tricks to be able to execute them.
26
69
 
27
70
  To resolve this, you will need to do a `yarn add --dev eslint prettier` in your modules to install the dependencies, and update any scripts to remove the invocation hacks.
28
71
 
@@ -42,7 +85,7 @@ If using TypeScript, you should specify a `typescriptRoot`:
42
85
  import { generateEslintConfig } from '@companion-module/tools/eslint/config.mjs'
43
86
 
44
87
  export default generateEslintConfig({
45
- enableTypescript: true,
88
+ enableTypescript: true,
46
89
  })
47
90
  ```
48
91
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@companion-module/tools",
3
- "version": "2.7.2",
3
+ "version": "2.9.0",
4
4
  "type": "module",
5
5
  "main": "index.js",
6
6
  "license": "MIT",
@@ -15,8 +15,11 @@
15
15
  "companion-surface-build": "scripts/build-surface.js",
16
16
  "companion-surface-check": "scripts/check-surface.js"
17
17
  },
18
+ "scripts": {
19
+ "test": "node --test test/*.test.mjs"
20
+ },
18
21
  "engines": {
19
- "node": "^18.18 || ^22.18"
22
+ "node": "^18.18 || ^22.18 || ^26"
20
23
  },
21
24
  "files": [
22
25
  "eslint",
@@ -35,6 +38,7 @@
35
38
  "find-up": "^7.0.0",
36
39
  "parse-author": "^2.0.0",
37
40
  "semver": "^7.7.4",
41
+ "spdx-expression-parse": "^5.0.0",
38
42
  "tar": "^7.5.9",
39
43
  "webpack": "^5.105.2",
40
44
  "webpack-cli": "^6.0.1",
@@ -3,17 +3,20 @@
3
3
  import 'zx/globals'
4
4
 
5
5
  import { buildPackage } from './lib/build-util.js'
6
+ import { LicensePolicyError } from './lib/license-warning-util.js'
7
+ import { ignoreLicenseRules } from './lib/cli-options.js'
6
8
 
7
9
  if (process.platform === 'win32') {
8
10
  usePowerShell() // to enable powershell
9
11
  }
10
12
 
11
13
  if (argv.help) {
12
- console.log('Usage: companion-module-build [--dev] [--prerelease]')
14
+ console.log('Usage: companion-module-build [--dev] [--prerelease] [--ignore-license-rules]')
13
15
  console.log('Builds the companion connection module')
14
16
  console.log(' --dev: Build in development mode. This will not minify the code, making it easier to debug.')
15
17
  console.log(' --prerelease: Build in prerelease mode. This gets added as metadata to the manifest')
16
18
  console.log(' --output <filename>: Output to a specific filename, without a file extension')
19
+ console.log(' --ignore-license-rules: Report license policy issues as warnings instead of failing')
17
20
  process.exit(0)
18
21
  }
19
22
 
@@ -24,4 +27,11 @@ if (!validateManifest) {
24
27
  validateManifest = manifestPkg.validateManifest
25
28
  }
26
29
 
27
- await buildPackage('@companion-module/base', validateManifest, 'connection', '>=1.4.0 <3.0.0')
30
+ try {
31
+ await buildPackage('@companion-module/base', validateManifest, 'connection', '>=1.4.0 <3.0.0', {
32
+ ignoreLicenseRules: ignoreLicenseRules(argv),
33
+ })
34
+ } catch (e) {
35
+ if (e instanceof LicensePolicyError) process.exitCode = 1
36
+ else throw e
37
+ }
@@ -3,20 +3,30 @@
3
3
  import 'zx/globals'
4
4
 
5
5
  import { buildPackage } from './lib/build-util.js'
6
+ import { LicensePolicyError } from './lib/license-warning-util.js'
7
+ import { ignoreLicenseRules } from './lib/cli-options.js'
6
8
 
7
9
  if (process.platform === 'win32') {
8
10
  usePowerShell() // to enable powershell
9
11
  }
10
12
 
11
13
  if (argv.help) {
12
- console.log('Usage: companion-module-build [--dev] [--prerelease]')
14
+ console.log('Usage: companion-module-build [--dev] [--prerelease] [--ignore-license-rules]')
13
15
  console.log('Builds the companion connection module')
14
16
  console.log(' --dev: Build in development mode. This will not minify the code, making it easier to debug.')
15
17
  console.log(' --prerelease: Build in prerelease mode. This gets added as metadata to the manifest')
16
18
  console.log(' --output <filename>: Output to a specific filename, without a file extension')
19
+ console.log(' --ignore-license-rules: Report license policy issues as warnings instead of failing')
17
20
  process.exit(0)
18
21
  }
19
22
 
20
23
  const { validateSurfaceManifest } = await import('@companion-surface/base')
21
24
 
22
- await buildPackage('@companion-surface/base', validateSurfaceManifest, 'surface', '>=1.0.0 <2.0.0')
25
+ try {
26
+ await buildPackage('@companion-surface/base', validateSurfaceManifest, 'surface', '>=1.0.0 <2.0.0', {
27
+ ignoreLicenseRules: ignoreLicenseRules(argv),
28
+ })
29
+ } catch (e) {
30
+ if (e instanceof LicensePolicyError) process.exitCode = 1
31
+ else throw e
32
+ }
@@ -7,11 +7,21 @@ import { fs } from 'zx'
7
7
  import { findUp } from 'find-up'
8
8
  import { validateManifest } from '@companion-module/base'
9
9
  import { createRequire } from 'module'
10
+ import { checkPackage } from './lib/check-util.js'
11
+ import { LicensePolicyError } from './lib/license-warning-util.js'
12
+ import { ignoreLicenseRules } from './lib/cli-options.js'
10
13
 
11
14
  if (process.platform === 'win32') {
12
15
  usePowerShell() // to enable powershell
13
16
  }
14
17
 
18
+ if (argv.help) {
19
+ console.log('Usage: companion-module-check [--ignore-license-rules]')
20
+ console.log('Checks the companion connection module')
21
+ console.log(' --ignore-license-rules: Report license policy issues as warnings instead of failing')
22
+ process.exit(0)
23
+ }
24
+
15
25
  const require = createRequire(import.meta.url)
16
26
 
17
27
  async function findModuleDir(cwd) {
@@ -30,11 +40,12 @@ console.log(`Checking for: ${process.cwd()}`)
30
40
  console.log(`Tools path: ${toolsDir}`)
31
41
  console.log(`Framework path: ${frameworkDir}`)
32
42
 
33
- const manifestJson = JSON.parse(await fs.readFile(path.resolve('./companion/manifest.json')))
34
-
35
43
  try {
36
- validateManifest(manifestJson)
44
+ await checkPackage({ validateManifest, ignoreLicenseRules: ignoreLicenseRules(argv) })
37
45
  } catch (e) {
38
- console.error('Manifest validation failed', e)
39
- process.exit(1)
46
+ if (e instanceof LicensePolicyError) process.exitCode = 1
47
+ else {
48
+ console.error('Manifest validation failed', e)
49
+ process.exit(1)
50
+ }
40
51
  }
@@ -7,11 +7,21 @@ import { fs } from 'zx'
7
7
  import { findUp } from 'find-up'
8
8
  import { validateSurfaceManifest } from '@companion-surface/base'
9
9
  import { createRequire } from 'module'
10
+ import { checkPackage } from './lib/check-util.js'
11
+ import { LicensePolicyError } from './lib/license-warning-util.js'
12
+ import { ignoreLicenseRules } from './lib/cli-options.js'
10
13
 
11
14
  if (process.platform === 'win32') {
12
15
  usePowerShell() // to enable powershell
13
16
  }
14
17
 
18
+ if (argv.help) {
19
+ console.log('Usage: companion-surface-check [--ignore-license-rules]')
20
+ console.log('Checks the companion surface module')
21
+ console.log(' --ignore-license-rules: Report license policy issues as warnings instead of failing')
22
+ process.exit(0)
23
+ }
24
+
15
25
  const require = createRequire(import.meta.url)
16
26
 
17
27
  async function findModuleDir(cwd) {
@@ -30,11 +40,15 @@ console.log(`Checking for: ${process.cwd()}`)
30
40
  console.log(`Tools path: ${toolsDir}`)
31
41
  console.log(`Framework path: ${frameworkDir}`)
32
42
 
33
- const manifestJson = JSON.parse(await fs.readFile(path.resolve('./companion/manifest.json')))
34
-
35
43
  try {
36
- validateSurfaceManifest(manifestJson)
44
+ await checkPackage({
45
+ validateManifest: validateSurfaceManifest,
46
+ ignoreLicenseRules: ignoreLicenseRules(argv),
47
+ })
37
48
  } catch (e) {
38
- console.error('Manifest validation failed', e)
39
- process.exit(1)
49
+ if (e instanceof LicensePolicyError) process.exitCode = 1
50
+ else {
51
+ console.error('Manifest validation failed', e)
52
+ process.exit(1)
53
+ }
40
54
  }
@@ -6,6 +6,13 @@ import { findUp } from 'find-up'
6
6
  import * as tar from 'tar'
7
7
  import { createRequire } from 'module'
8
8
  import * as semver from 'semver'
9
+ import {
10
+ collectInstalledPackages,
11
+ collectProductionPackages,
12
+ createLegalInventory,
13
+ writeLegalArtifacts,
14
+ } from './license-util.js'
15
+ import { enforceLicensePolicy } from './license-warning-util.js'
9
16
 
10
17
  function toSanitizedDirname(name) {
11
18
  return name.replace(/[^a-zA-Z0-9-\.]/g, '-').replace(/[-+]/g, '-')
@@ -21,7 +28,7 @@ async function findModuleDir(cwd) {
21
28
  return path.dirname(pkgJsonPath)
22
29
  }
23
30
 
24
- export async function buildPackage(frameworkPackageName, validateManifest, moduleType, versionRange) {
31
+ export async function buildPackage(frameworkPackageName, validateManifest, moduleType, versionRange, options = {}) {
25
32
  // const toolsDir = path.join(__dirname, '..')
26
33
  const moduleDir = process.cwd()
27
34
  const toolsDir = await findModuleDir(require.resolve('@companion-module/tools'))
@@ -234,6 +241,19 @@ export async function buildPackage(frameworkPackageName, validateManifest, modul
234
241
  }
235
242
  }
236
243
 
244
+ // The externals the build just resolved are exactly the names the bundler kept out of main.js
245
+ const shippedPackages = await collectProductionPackages(moduleDir, Object.keys(packageJson.dependencies))
246
+ if (Object.keys(packageJson.dependencies).length) {
247
+ // The externals tree yarn just installed is the exact set which ships alongside the bundle
248
+ shippedPackages.packages.push(...(await collectInstalledPackages(path.join(packageBaseDir, 'node_modules'))))
249
+ }
250
+ const legalInventory = await createLegalInventory(shippedPackages.packages)
251
+ for (const diagnostic of [...shippedPackages.diagnostics, ...legalInventory.diagnostics]) {
252
+ console.warn(`License inventory: ${diagnostic}`)
253
+ }
254
+ await writeLegalArtifacts(packageBaseDir, legalInventory)
255
+ enforceLicensePolicy(legalInventory, { ignoreLicenseRules: options.ignoreLicenseRules })
256
+
237
257
  // Create tgz of the build
238
258
  let tgzFile = toSanitizedDirname(`${manifestJson.id}-${manifestJson.version}`)
239
259
  if (typeof argv['output'] === 'string') {
@@ -0,0 +1,23 @@
1
+ import { readFile } from 'node:fs/promises'
2
+ import path from 'node:path'
3
+ import { enforceLicensePolicy } from './license-warning-util.js'
4
+ import { analyzeShippedLegalInventory } from './license-util.js'
5
+
6
+ /**
7
+ * @param {object} options
8
+ * @param {(manifest: any, looseChecks: boolean) => void} options.validateManifest
9
+ * @param {string} [options.cwd]
10
+ * @param {boolean} [options.ignoreLicenseRules]
11
+ * @param {Pick<NodeJS.WriteStream, 'write' | 'isTTY'>} [options.stderr]
12
+ * @returns {Promise<void>}
13
+ */
14
+ export async function checkPackage(options) {
15
+ const cwd = options.cwd ?? process.cwd()
16
+ const manifest = JSON.parse(await readFile(path.join(cwd, 'companion', 'manifest.json'), 'utf8'))
17
+ options.validateManifest(manifest, false)
18
+ const inventory = await analyzeShippedLegalInventory(cwd)
19
+ enforceLicensePolicy(inventory, {
20
+ ignoreLicenseRules: options.ignoreLicenseRules,
21
+ stderr: options.stderr,
22
+ })
23
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * @param {Record<string, unknown>} argv
3
+ * @returns {boolean}
4
+ */
5
+ export function ignoreLicenseRules(argv) {
6
+ const value = argv['ignore-license-rules']
7
+ return value === true || value === 'true'
8
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Packages which ship no license declaration in their package.json, but whose license has been confirmed by reading
3
+ * what they publish. Only add an entry after checking that version yourself, and record where the license was found.
4
+ *
5
+ * Keys are exact `name@version`, as a later release can change license. A declared license always wins over this list,
6
+ * so an entry can never hide what a package says about itself.
7
+ *
8
+ * @type {Record<string, string>}
9
+ */
10
+ export const KNOWN_PACKAGE_LICENSES = {
11
+ // LICENSE.md, "The MIT License", https://github.com/bwindels/exif-parser
12
+ 'exif-parser@0.1.12': 'MIT',
13
+ // Readme.md "## License" section in the published tarball, and a LICENSE added to
14
+ // https://github.com/component/indexof after this version was published
15
+ 'indexof@0.0.1': 'MIT',
16
+ // Readme.md "## License" section in the published tarball, and a LICENSE added to
17
+ // https://github.com/component/inherit after this version was published
18
+ 'component-inherit@0.0.3': 'MIT',
19
+ // Readme.md "## License" section in the published tarball, and a LICENSE added to
20
+ // https://github.com/component/bind after this version was published
21
+ 'component-bind@1.0.0': 'MIT',
22
+ // COPYING in the published tarball is the Boost Software License 1.0 with the notice retention paragraph removed,
23
+ // so it imposes strictly less than BSL-1.0 and recording it as that can only ever overstate its obligations.
24
+ // https://github.com/russellmcc/node-binpack
25
+ 'binpack@0.1.0': 'BSL-1.0',
26
+ // LICENSE in the published tarball is BSD-3-Clause, including the clause forbidding use of the copyright holder's
27
+ // name to endorse derived products, https://github.com/fairoakslabs/bufferpack
28
+ 'bufferpack@0.0.6': 'BSD-3-Clause',
29
+ // LICENSE, "(The MIT License)", in the published tarball, npm metadata claims MIT but package.json declares
30
+ // nothing, https://github.com/vercel/ms
31
+ 'ms@0.7.1': 'MIT',
32
+ // cycle.js in the published tarball carries "Public Domain." in its header comment, and the author has since
33
+ // added "license": "Public-Domain" to https://github.com/dscape/cycle
34
+ 'cycle@1.0.3': 'Public-Domain',
35
+ // component.json and bower.json both declare "MIT" in the published tarball, only package.json omits it,
36
+ // https://github.com/component/emitter (1.1.2 republished from https://github.com/sindresorhus/component-emitter)
37
+ 'emitter-component@1.1.1': 'MIT',
38
+ 'emitter-component@1.1.2': 'MIT',
39
+ // LICENSE, "The MIT License (MIT)", in the published tarball, https://github.com/changchang/seq-queue
40
+ 'seq-queue@0.0.5': 'MIT',
41
+ // license, "MIT License", in the published tarball, https://github.com/fabiospampinato/atomically
42
+ 'atomically@2.0.3': 'MIT',
43
+ // license, "MIT License", in the published tarball, https://github.com/fabiospampinato/stubborn-fs
44
+ 'stubborn-fs@1.2.5': 'MIT',
45
+ }
46
+
47
+ /**
48
+ * @param {string | undefined} name
49
+ * @param {string | undefined} version
50
+ * @returns {string | undefined}
51
+ */
52
+ export function knownPackageLicense(name, version) {
53
+ if (!name || !version) return undefined
54
+ return KNOWN_PACKAGE_LICENSES[`${name}@${version}`]
55
+ }
56
+
57
+ /**
58
+ * Packages which declare a license that is not a valid SPDX expression, mapped to what the license text they publish
59
+ * actually is. Unlike KNOWN_PACKAGE_LICENSES this overrides what a package says about itself, so it is only consulted
60
+ * when the declaration cannot be parsed, and can never turn a real license into a more convenient one.
61
+ *
62
+ * Only add an entry after reading the license text shipped in that exact version, and record what identified it.
63
+ * Keys are exact `name@version`, so a later release declaring something different is unaffected.
64
+ *
65
+ * @type {Record<string, string>}
66
+ */
67
+ export const CORRECTED_PACKAGE_LICENSES = {
68
+ // Declares "BSD". LICENSE is BSD-3-Clause: three conditions, the third forbidding use of the author's name to
69
+ // endorse derived products, which is exactly what separates it from BSD-2-Clause.
70
+ 'url-template@2.0.8': 'BSD-3-Clause',
71
+ // Declares "MIT License", which is the name rather than the identifier. LICENSE is "The MIT License (MIT)".
72
+ 'xml-escape@1.1.0': 'MIT',
73
+ // Declares "Apache 2.0" through the deprecated licenses array. LICENSE is the Apache License 2.0, followed by an
74
+ // MIT notice covering the QR library vendored into vendor/QRCode, whose text the inventory collects as well.
75
+ 'qrcode-terminal@0.12.0': 'Apache-2.0',
76
+ // Declares "Apache2", which is neither the identifier nor the name. LICENSE is the verbatim Apache License 2.0
77
+ // including its appendix, with no NOTICE file to propagate, https://git.coolaj86.com/coolaj86/ssl-root-cas.js
78
+ 'ssl-root-cas@1.3.1': 'Apache-2.0',
79
+ // Declares the pair through the deprecated licenses array as "AFLv2.1" and "BSD", each linking a line of the Dojo
80
+ // LICENSE the package is covered by. That file names its BSD half "BSD 3-Clause \"New\" License", including the
81
+ // clause forbidding use of the Dojo Foundation's name to endorse derived products, https://github.com/kriszyp/json-schema
82
+ 'json-schema@0.2.3': '(AFL-2.1 OR BSD-3-Clause)',
83
+ }
84
+
85
+ /**
86
+ * @param {string | undefined} name
87
+ * @param {string | undefined} version
88
+ * @returns {string | undefined}
89
+ */
90
+ export function correctedPackageLicense(name, version) {
91
+ if (!name || !version) return undefined
92
+ return CORRECTED_PACKAGE_LICENSES[`${name}@${version}`]
93
+ }