@companion-module/tools 3.0.2 → 3.1.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 +10 -0
- package/LICENSE +21 -0
- package/README.md +25 -3
- package/dist/scripts/build-connection.js +15 -2
- package/dist/scripts/build-surface.js +16 -3
- package/dist/scripts/check-connection.js +21 -9
- package/dist/scripts/check-surface.js +25 -10
- package/dist/scripts/lib/build-util.d.ts +9 -3
- package/dist/scripts/lib/build-util.js +30 -49
- package/dist/scripts/lib/bundle-util.d.ts +11 -0
- package/dist/scripts/lib/bundle-util.js +49 -0
- package/dist/scripts/lib/check-util.d.ts +6 -0
- package/dist/scripts/lib/check-util.js +14 -0
- package/dist/scripts/lib/cli-options.d.ts +1 -0
- package/dist/scripts/lib/cli-options.js +4 -0
- package/dist/scripts/lib/external-install-util.d.ts +2 -0
- package/dist/scripts/lib/external-install-util.js +51 -0
- package/dist/scripts/lib/known-package-licenses.d.ts +19 -0
- package/dist/scripts/lib/known-package-licenses.js +78 -0
- package/dist/scripts/lib/license-util.d.ts +55 -0
- package/dist/scripts/lib/license-util.js +427 -0
- package/dist/scripts/lib/license-warning-util.d.ts +27 -0
- package/dist/scripts/lib/license-warning-util.js +371 -0
- package/package.json +7 -3
- package/tsconfig/node22/recommended-esm.json +5 -20
- package/tsconfig/node22/recommended.json +5 -20
- package/tsconfig/node26/recommended.json +26 -0
- package/tsconfig/shared.json +24 -0
|
@@ -0,0 +1,78 @@
|
|
|
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
|
+
export const KNOWN_PACKAGE_LICENSES = {
|
|
9
|
+
// LICENSE.md, "The MIT License", https://github.com/bwindels/exif-parser
|
|
10
|
+
'exif-parser@0.1.12': 'MIT',
|
|
11
|
+
// Readme.md "## License" section in the published tarball, and a LICENSE added to
|
|
12
|
+
// https://github.com/component/indexof after this version was published
|
|
13
|
+
'indexof@0.0.1': 'MIT',
|
|
14
|
+
// Readme.md "## License" section in the published tarball, and a LICENSE added to
|
|
15
|
+
// https://github.com/component/inherit after this version was published
|
|
16
|
+
'component-inherit@0.0.3': 'MIT',
|
|
17
|
+
// Readme.md "## License" section in the published tarball, and a LICENSE added to
|
|
18
|
+
// https://github.com/component/bind after this version was published
|
|
19
|
+
'component-bind@1.0.0': 'MIT',
|
|
20
|
+
// COPYING in the published tarball is the Boost Software License 1.0 with the notice retention paragraph removed,
|
|
21
|
+
// so it imposes strictly less than BSL-1.0 and recording it as that can only ever overstate its obligations.
|
|
22
|
+
// https://github.com/russellmcc/node-binpack
|
|
23
|
+
'binpack@0.1.0': 'BSL-1.0',
|
|
24
|
+
// LICENSE in the published tarball is BSD-3-Clause, including the clause forbidding use of the copyright holder's
|
|
25
|
+
// name to endorse derived products, https://github.com/fairoakslabs/bufferpack
|
|
26
|
+
'bufferpack@0.0.6': 'BSD-3-Clause',
|
|
27
|
+
// LICENSE, "(The MIT License)", in the published tarball, npm metadata claims MIT but package.json declares
|
|
28
|
+
// nothing, https://github.com/vercel/ms
|
|
29
|
+
'ms@0.7.1': 'MIT',
|
|
30
|
+
// cycle.js in the published tarball carries "Public Domain." in its header comment, and the author has since
|
|
31
|
+
// added "license": "Public-Domain" to https://github.com/dscape/cycle
|
|
32
|
+
'cycle@1.0.3': 'Public-Domain',
|
|
33
|
+
// component.json and bower.json both declare "MIT" in the published tarball, only package.json omits it,
|
|
34
|
+
// https://github.com/component/emitter (1.1.2 republished from https://github.com/sindresorhus/component-emitter)
|
|
35
|
+
'emitter-component@1.1.1': 'MIT',
|
|
36
|
+
'emitter-component@1.1.2': 'MIT',
|
|
37
|
+
// LICENSE, "The MIT License (MIT)", in the published tarball, https://github.com/changchang/seq-queue
|
|
38
|
+
'seq-queue@0.0.5': 'MIT',
|
|
39
|
+
// license, "MIT License", in the published tarball, https://github.com/fabiospampinato/atomically
|
|
40
|
+
'atomically@2.0.3': 'MIT',
|
|
41
|
+
// license, "MIT License", in the published tarball, https://github.com/fabiospampinato/stubborn-fs
|
|
42
|
+
'stubborn-fs@1.2.5': 'MIT',
|
|
43
|
+
};
|
|
44
|
+
export function knownPackageLicense(name, version) {
|
|
45
|
+
if (!name || !version)
|
|
46
|
+
return undefined;
|
|
47
|
+
return KNOWN_PACKAGE_LICENSES[`${name}@${version}`];
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Packages which declare a license that is not a valid SPDX expression, mapped to what the license text they publish
|
|
51
|
+
* actually is. Unlike KNOWN_PACKAGE_LICENSES this overrides what a package says about itself, so it is only consulted
|
|
52
|
+
* when the declaration cannot be parsed, and can never turn a real license into a more convenient one.
|
|
53
|
+
*
|
|
54
|
+
* Only add an entry after reading the license text shipped in that exact version, and record what identified it.
|
|
55
|
+
* Keys are exact `name@version`, so a later release declaring something different is unaffected.
|
|
56
|
+
*/
|
|
57
|
+
export const CORRECTED_PACKAGE_LICENSES = {
|
|
58
|
+
// Declares "BSD". LICENSE is BSD-3-Clause: three conditions, the third forbidding use of the author's name to
|
|
59
|
+
// endorse derived products, which is exactly what separates it from BSD-2-Clause.
|
|
60
|
+
'url-template@2.0.8': 'BSD-3-Clause',
|
|
61
|
+
// Declares "MIT License", which is the name rather than the identifier. LICENSE is "The MIT License (MIT)".
|
|
62
|
+
'xml-escape@1.1.0': 'MIT',
|
|
63
|
+
// Declares "Apache 2.0" through the deprecated licenses array. LICENSE is the Apache License 2.0, followed by an
|
|
64
|
+
// MIT notice covering the QR library vendored into vendor/QRCode, whose text the inventory collects as well.
|
|
65
|
+
'qrcode-terminal@0.12.0': 'Apache-2.0',
|
|
66
|
+
// Declares "Apache2", which is neither the identifier nor the name. LICENSE is the verbatim Apache License 2.0
|
|
67
|
+
// including its appendix, with no NOTICE file to propagate, https://git.coolaj86.com/coolaj86/ssl-root-cas.js
|
|
68
|
+
'ssl-root-cas@1.3.1': 'Apache-2.0',
|
|
69
|
+
// Declares the pair through the deprecated licenses array as "AFLv2.1" and "BSD", each linking a line of the Dojo
|
|
70
|
+
// LICENSE the package is covered by. That file names its BSD half "BSD 3-Clause \"New\" License", including the
|
|
71
|
+
// clause forbidding use of the Dojo Foundation's name to endorse derived products, https://github.com/kriszyp/json-schema
|
|
72
|
+
'json-schema@0.2.3': '(AFL-2.1 OR BSD-3-Clause)',
|
|
73
|
+
};
|
|
74
|
+
export function correctedPackageLicense(name, version) {
|
|
75
|
+
if (!name || !version)
|
|
76
|
+
return undefined;
|
|
77
|
+
return CORRECTED_PACKAGE_LICENSES[`${name}@${version}`];
|
|
78
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import * as esbuild from 'esbuild';
|
|
2
|
+
export type ShippedPackageKind = 'project' | 'bundled' | 'external';
|
|
3
|
+
export interface ShippedPackage {
|
|
4
|
+
kind: ShippedPackageKind;
|
|
5
|
+
name: string;
|
|
6
|
+
version?: string;
|
|
7
|
+
declaredLicense?: string;
|
|
8
|
+
/** Only for the project, the package.json license of its own source, which declaredLicense distributes it under */
|
|
9
|
+
sourceLicense?: string;
|
|
10
|
+
/** Where recipients can obtain the source of this package, as required by MPL-2.0 and the GPL family */
|
|
11
|
+
repositoryUrl?: string;
|
|
12
|
+
packageRoot: string;
|
|
13
|
+
contributingPaths: Set<string>;
|
|
14
|
+
}
|
|
15
|
+
export interface MetafilePackageCollection {
|
|
16
|
+
packages: ShippedPackage[];
|
|
17
|
+
diagnostics: string[];
|
|
18
|
+
}
|
|
19
|
+
export interface LegalText {
|
|
20
|
+
role: 'license' | 'notice' | 'source-comment';
|
|
21
|
+
filename: string;
|
|
22
|
+
content: string;
|
|
23
|
+
sha256: string;
|
|
24
|
+
}
|
|
25
|
+
export interface ShippedPackageLegalRecord extends ShippedPackage {
|
|
26
|
+
legalTexts: LegalText[];
|
|
27
|
+
}
|
|
28
|
+
export interface PackageLegalMaterial {
|
|
29
|
+
package: ShippedPackageLegalRecord;
|
|
30
|
+
diagnostics: string[];
|
|
31
|
+
}
|
|
32
|
+
export interface LegalInventory {
|
|
33
|
+
packages: ShippedPackageLegalRecord[];
|
|
34
|
+
diagnostics: string[];
|
|
35
|
+
}
|
|
36
|
+
type PackageJson = {
|
|
37
|
+
name?: string;
|
|
38
|
+
version?: string;
|
|
39
|
+
license?: unknown;
|
|
40
|
+
licenses?: unknown;
|
|
41
|
+
repository?: unknown;
|
|
42
|
+
homepage?: unknown;
|
|
43
|
+
};
|
|
44
|
+
export declare function normalizeRepositoryUrl(packageJson: PackageJson): string | undefined;
|
|
45
|
+
export declare function normalizeInventoryPath(inputPath: string, ownerRoot: string): string;
|
|
46
|
+
export declare function collectInstalledPackages(nodeModulesDir: string): Promise<ShippedPackage[]>;
|
|
47
|
+
export declare function normalizeLegalText(content: string): string;
|
|
48
|
+
export declare function collectPackageLegalMaterial(packageInfo: ShippedPackage): Promise<PackageLegalMaterial>;
|
|
49
|
+
export declare function renderLicenseFile(inventory: LegalInventory): string;
|
|
50
|
+
export declare function renderNoticeFile(inventory: LegalInventory): string | undefined;
|
|
51
|
+
export declare function writeLegalArtifacts(outputDir: string, inventory: LegalInventory): Promise<void>;
|
|
52
|
+
export declare function createLegalInventory(packages: ShippedPackage[]): Promise<LegalInventory>;
|
|
53
|
+
export declare function analyzeShippedLegalInventory(moduleDir: string): Promise<LegalInventory>;
|
|
54
|
+
export declare function collectMetafilePackages(moduleDir: string, metafile: esbuild.Metafile): Promise<MetafilePackageCollection>;
|
|
55
|
+
export {};
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
import { lstat, readdir, realpath, readFile, rm, stat, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import * as esbuild from 'esbuild';
|
|
5
|
+
import { createEsbuildOptions, loadModuleBuildDefinition } from './bundle-util.js';
|
|
6
|
+
import { resolveExternalDependencies, withInstalledExternalTree } from './external-install-util.js';
|
|
7
|
+
import { knownPackageLicense } from './known-package-licenses.js';
|
|
8
|
+
const SHORTHAND_REPOSITORY_HOSTS = {
|
|
9
|
+
github: 'https://github.com/',
|
|
10
|
+
gitlab: 'https://gitlab.com/',
|
|
11
|
+
bitbucket: 'https://bitbucket.org/',
|
|
12
|
+
};
|
|
13
|
+
export function normalizeRepositoryUrl(packageJson) {
|
|
14
|
+
const repository = packageJson.repository;
|
|
15
|
+
const declared = typeof repository === 'string'
|
|
16
|
+
? repository
|
|
17
|
+
: repository && typeof repository === 'object' && typeof repository.url === 'string'
|
|
18
|
+
? repository.url
|
|
19
|
+
: undefined;
|
|
20
|
+
let url = declared?.trim() ?? '';
|
|
21
|
+
if (url) {
|
|
22
|
+
// npm shorthands, either "user/repo" or "<host>:user/repo"
|
|
23
|
+
if (/^[\w.-]+\/[\w.-]+$/.test(url))
|
|
24
|
+
url = `${SHORTHAND_REPOSITORY_HOSTS.github}${url}`;
|
|
25
|
+
const shorthand = /^([a-z]+):([\w.-]+\/[\w.-]+)$/.exec(url);
|
|
26
|
+
if (shorthand && SHORTHAND_REPOSITORY_HOSTS[shorthand[1]]) {
|
|
27
|
+
url = `${SHORTHAND_REPOSITORY_HOSTS[shorthand[1]]}${shorthand[2]}`;
|
|
28
|
+
}
|
|
29
|
+
url = url.replace(/^git\+/, '').replace(/^(?:git|ssh):\/\/(?:git@)?/, 'https://');
|
|
30
|
+
const scpLike = /^git@([^:]+):(.+)$/.exec(url);
|
|
31
|
+
if (scpLike)
|
|
32
|
+
url = `https://${scpLike[1]}/${scpLike[2]}`;
|
|
33
|
+
url = url.replace(/\.git$/, '');
|
|
34
|
+
}
|
|
35
|
+
if (!/^https?:\/\//.test(url))
|
|
36
|
+
url = typeof packageJson.homepage === 'string' ? packageJson.homepage.trim() : '';
|
|
37
|
+
return /^https?:\/\//.test(url) ? url : undefined;
|
|
38
|
+
}
|
|
39
|
+
export function normalizeInventoryPath(inputPath, ownerRoot) {
|
|
40
|
+
const relativePath = path.relative(ownerRoot, inputPath);
|
|
41
|
+
if (relativePath === '' || relativePath === '.')
|
|
42
|
+
return '.';
|
|
43
|
+
if (relativePath === '..' || relativePath.startsWith(`..${path.sep}`) || path.isAbsolute(relativePath)) {
|
|
44
|
+
return '<outside package root>';
|
|
45
|
+
}
|
|
46
|
+
return relativePath.split(path.sep).join('/');
|
|
47
|
+
}
|
|
48
|
+
async function readPackageJson(packageRoot) {
|
|
49
|
+
return JSON.parse(await readFile(path.join(packageRoot, 'package.json'), 'utf8'));
|
|
50
|
+
}
|
|
51
|
+
function asLicenseString(license) {
|
|
52
|
+
if (typeof license === 'string')
|
|
53
|
+
return license;
|
|
54
|
+
// npm deprecated { type, url } and arrays of it long ago, but older packages are still published with them
|
|
55
|
+
if (Array.isArray(license)) {
|
|
56
|
+
const choices = license.map(asLicenseString).filter((choice) => choice !== undefined);
|
|
57
|
+
if (!choices.length)
|
|
58
|
+
return undefined;
|
|
59
|
+
return choices.length === 1 ? choices[0] : `(${choices.join(' OR ')})`; // An array offers a choice of licenses
|
|
60
|
+
}
|
|
61
|
+
if (license && typeof license === 'object')
|
|
62
|
+
return asLicenseString(license.type);
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
/** Reads a package's license, preferring the current field over the deprecated plural one */
|
|
66
|
+
function declaredLicenseOf(packageJson) {
|
|
67
|
+
return (asLicenseString(packageJson.license) ??
|
|
68
|
+
asLicenseString(packageJson.licenses) ??
|
|
69
|
+
// Only for packages declaring nothing at all, so this can never override what a package says about itself
|
|
70
|
+
knownPackageLicense(packageJson.name, packageJson.version));
|
|
71
|
+
}
|
|
72
|
+
async function readManifestLicense(moduleDir) {
|
|
73
|
+
try {
|
|
74
|
+
const manifest = JSON.parse(await readFile(path.join(moduleDir, 'companion', 'manifest.json'), 'utf8'));
|
|
75
|
+
return manifest.license;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return undefined; // Modules without a readable manifest fall back to the package.json license
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async function findPackageRoot(inputPath, moduleDir) {
|
|
82
|
+
const nodeModulesDir = path.join(moduleDir, 'node_modules');
|
|
83
|
+
if (!inputPath.startsWith(`${nodeModulesDir}${path.sep}`))
|
|
84
|
+
return undefined;
|
|
85
|
+
// A package owns everything below the directory node_modules resolves it to. Its own subdirectories can hold a
|
|
86
|
+
// package.json marking their module type, sometimes with a name and version, so only this boundary identifies it.
|
|
87
|
+
const segments = inputPath.split(path.sep);
|
|
88
|
+
const rootIndex = segments.lastIndexOf('node_modules') + 1;
|
|
89
|
+
const packageRoot = segments
|
|
90
|
+
.slice(0, segments[rootIndex]?.startsWith('@') ? rootIndex + 2 : rootIndex + 1)
|
|
91
|
+
.join(path.sep);
|
|
92
|
+
try {
|
|
93
|
+
await readPackageJson(packageRoot);
|
|
94
|
+
return packageRoot;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return undefined; // Without a package.json this is not a package, so the file counts as project code
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function getContributingInputs(metafile) {
|
|
101
|
+
const inputs = new Set();
|
|
102
|
+
for (const [outputPath, output] of Object.entries(metafile.outputs)) {
|
|
103
|
+
if (!outputPath.endsWith('.js'))
|
|
104
|
+
continue;
|
|
105
|
+
for (const [inputPath, input] of Object.entries(output.inputs)) {
|
|
106
|
+
if (input.bytesInOutput > 0)
|
|
107
|
+
inputs.add(inputPath);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return [...inputs];
|
|
111
|
+
}
|
|
112
|
+
function packageFromJson(kind, packageRoot, packageJson) {
|
|
113
|
+
return {
|
|
114
|
+
kind,
|
|
115
|
+
name: packageJson.name ?? path.basename(packageRoot),
|
|
116
|
+
version: packageJson.version,
|
|
117
|
+
declaredLicense: declaredLicenseOf(packageJson),
|
|
118
|
+
repositoryUrl: normalizeRepositoryUrl(packageJson),
|
|
119
|
+
packageRoot,
|
|
120
|
+
contributingPaths: new Set(),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
export async function collectInstalledPackages(nodeModulesDir) {
|
|
124
|
+
const packages = [];
|
|
125
|
+
async function scan(currentNodeModulesDir) {
|
|
126
|
+
let entries;
|
|
127
|
+
try {
|
|
128
|
+
entries = await readdir(currentNodeModulesDir, { withFileTypes: true });
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
134
|
+
if (entry.name === '.bin' || entry.isSymbolicLink())
|
|
135
|
+
continue;
|
|
136
|
+
if (entry.name.startsWith('@')) {
|
|
137
|
+
if (!entry.isDirectory())
|
|
138
|
+
continue;
|
|
139
|
+
await scanScopedPackages(path.join(currentNodeModulesDir, entry.name));
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (!entry.isDirectory())
|
|
143
|
+
continue;
|
|
144
|
+
await scanPackage(path.join(currentNodeModulesDir, entry.name));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
async function scanScopedPackages(scopeDir) {
|
|
148
|
+
const entries = await readdir(scopeDir, { withFileTypes: true });
|
|
149
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
150
|
+
if (entry.isDirectory() && !entry.isSymbolicLink())
|
|
151
|
+
await scanPackage(path.join(scopeDir, entry.name));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
async function scanPackage(packageRoot) {
|
|
155
|
+
try {
|
|
156
|
+
packages.push(packageFromJson('external', packageRoot, await readPackageJson(packageRoot)));
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
await scan(path.join(packageRoot, 'node_modules'));
|
|
162
|
+
}
|
|
163
|
+
await scan(nodeModulesDir);
|
|
164
|
+
return packages;
|
|
165
|
+
}
|
|
166
|
+
const MAX_LEGAL_FILE_SIZE = 1024 * 1024;
|
|
167
|
+
export function normalizeLegalText(content) {
|
|
168
|
+
return content.replace(/\r\n/g, '\n').trim();
|
|
169
|
+
}
|
|
170
|
+
function sha256(content) {
|
|
171
|
+
return createHash('sha256').update(content).digest('hex');
|
|
172
|
+
}
|
|
173
|
+
function isPathInside(childPath, parentPath) {
|
|
174
|
+
const relativePath = path.relative(parentPath, childPath);
|
|
175
|
+
return (relativePath === '' ||
|
|
176
|
+
(!relativePath.startsWith(`..${path.sep}`) && relativePath !== '..' && !path.isAbsolute(relativePath)));
|
|
177
|
+
}
|
|
178
|
+
function legalRole(filename) {
|
|
179
|
+
if (/^(license|licence|copying)/i.test(filename))
|
|
180
|
+
return 'license';
|
|
181
|
+
if (/^notice/i.test(filename))
|
|
182
|
+
return 'notice';
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
async function readLegalText(filePath, packageRoot, role) {
|
|
186
|
+
const linkStat = await lstat(filePath);
|
|
187
|
+
if (linkStat.isSymbolicLink())
|
|
188
|
+
return undefined;
|
|
189
|
+
const fileStat = await stat(filePath);
|
|
190
|
+
if (!fileStat.isFile() || fileStat.size > MAX_LEGAL_FILE_SIZE)
|
|
191
|
+
return undefined;
|
|
192
|
+
const rawContent = await readFile(filePath, 'utf8');
|
|
193
|
+
if (rawContent.includes('\0'))
|
|
194
|
+
return undefined;
|
|
195
|
+
const content = normalizeLegalText(rawContent);
|
|
196
|
+
if (!content)
|
|
197
|
+
return undefined;
|
|
198
|
+
return { role, filename: normalizeInventoryPath(filePath, packageRoot), content, sha256: sha256(content) };
|
|
199
|
+
}
|
|
200
|
+
function sourceLoader(filename) {
|
|
201
|
+
switch (path.extname(filename).toLowerCase()) {
|
|
202
|
+
case '.js':
|
|
203
|
+
case '.mjs':
|
|
204
|
+
case '.cjs':
|
|
205
|
+
return 'js';
|
|
206
|
+
case '.ts':
|
|
207
|
+
return 'ts';
|
|
208
|
+
case '.tsx':
|
|
209
|
+
return 'tsx';
|
|
210
|
+
default:
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
export async function collectPackageLegalMaterial(packageInfo) {
|
|
215
|
+
const diagnostics = [];
|
|
216
|
+
const candidateFiles = new Map();
|
|
217
|
+
for (const entry of await readdir(packageInfo.packageRoot, { withFileTypes: true })) {
|
|
218
|
+
if (!entry.isFile())
|
|
219
|
+
continue;
|
|
220
|
+
const role = legalRole(entry.name);
|
|
221
|
+
if (role)
|
|
222
|
+
candidateFiles.set(entry.name, role);
|
|
223
|
+
}
|
|
224
|
+
const seeLicenseMatch = /^SEE LICENSE IN (.+)$/i.exec(packageInfo.declaredLicense ?? '');
|
|
225
|
+
if (seeLicenseMatch) {
|
|
226
|
+
const explicitPath = path.resolve(packageInfo.packageRoot, seeLicenseMatch[1]);
|
|
227
|
+
if (!isPathInside(explicitPath, packageInfo.packageRoot)) {
|
|
228
|
+
diagnostics.push(`Ignoring license file outside package root: ${seeLicenseMatch[1]}`);
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
candidateFiles.set(normalizeInventoryPath(explicitPath, packageInfo.packageRoot), 'license');
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const legalTexts = [];
|
|
235
|
+
for (const [filename, role] of [...candidateFiles.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
236
|
+
try {
|
|
237
|
+
const legalText = await readLegalText(path.join(packageInfo.packageRoot, filename), packageInfo.packageRoot, role);
|
|
238
|
+
if (legalText)
|
|
239
|
+
legalTexts.push(legalText);
|
|
240
|
+
else
|
|
241
|
+
diagnostics.push(`Ignoring unreadable, binary, or oversized legal file: ${filename}`);
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
diagnostics.push(`Ignoring unreadable legal file: ${filename}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (!legalTexts.some((text) => text.role === 'license')) {
|
|
248
|
+
for (const sourcePath of [...packageInfo.contributingPaths].sort()) {
|
|
249
|
+
const loader = sourceLoader(sourcePath);
|
|
250
|
+
if (!loader) {
|
|
251
|
+
diagnostics.push(`Ignoring unsupported source file for legal comments: ${sourcePath}`);
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
try {
|
|
255
|
+
const source = await readFile(path.join(packageInfo.packageRoot, sourcePath), 'utf8');
|
|
256
|
+
const result = await esbuild.transform(source, { loader, legalComments: 'external' });
|
|
257
|
+
if (result.legalComments) {
|
|
258
|
+
const content = normalizeLegalText(result.legalComments);
|
|
259
|
+
if (content) {
|
|
260
|
+
legalTexts.push({
|
|
261
|
+
role: 'source-comment',
|
|
262
|
+
filename: sourcePath,
|
|
263
|
+
content,
|
|
264
|
+
sha256: sha256(content),
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
diagnostics.push(`Ignoring unreadable source file for legal comments: ${sourcePath}`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const uniqueTexts = new Map();
|
|
275
|
+
for (const legalText of legalTexts) {
|
|
276
|
+
const key = legalText.role === 'source-comment'
|
|
277
|
+
? `${legalText.role}:${legalText.sha256}`
|
|
278
|
+
: `${legalText.role}:${legalText.filename}:${legalText.sha256}`;
|
|
279
|
+
uniqueTexts.set(key, legalText);
|
|
280
|
+
}
|
|
281
|
+
return { package: { ...packageInfo, legalTexts: [...uniqueTexts.values()] }, diagnostics };
|
|
282
|
+
}
|
|
283
|
+
function comparePackages(a, b) {
|
|
284
|
+
if (a.kind === 'project' && b.kind !== 'project')
|
|
285
|
+
return -1;
|
|
286
|
+
if (a.kind !== 'project' && b.kind === 'project')
|
|
287
|
+
return 1;
|
|
288
|
+
return a.name.localeCompare(b.name) || (a.version ?? '').localeCompare(b.version ?? '');
|
|
289
|
+
}
|
|
290
|
+
function packageName(packageInfo) {
|
|
291
|
+
return packageInfo.version ? `${packageInfo.name}@${packageInfo.version}` : packageInfo.name;
|
|
292
|
+
}
|
|
293
|
+
function renderLegalFile(inventory, roles) {
|
|
294
|
+
const groupedTexts = new Map();
|
|
295
|
+
for (const packageInfo of inventory.packages) {
|
|
296
|
+
for (const text of packageInfo.legalTexts) {
|
|
297
|
+
if (!roles.includes(text.role))
|
|
298
|
+
continue;
|
|
299
|
+
const group = groupedTexts.get(text.sha256) ?? { text, packages: new Map() };
|
|
300
|
+
const packageKey = `${packageInfo.kind === 'project' ? 'project' : 'dependency'}:${packageName(packageInfo)}:${packageInfo.declaredLicense ?? ''}`;
|
|
301
|
+
group.packages.set(packageKey, packageInfo);
|
|
302
|
+
groupedTexts.set(text.sha256, group);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (!groupedTexts.size)
|
|
306
|
+
return undefined;
|
|
307
|
+
const sections = [...groupedTexts.values()].sort((a, b) => comparePackages([...a.packages.values()].sort(comparePackages)[0], [...b.packages.values()].sort(comparePackages)[0]));
|
|
308
|
+
const separator = '-'.repeat(80);
|
|
309
|
+
const renderedSections = sections.map((section) => {
|
|
310
|
+
const packages = [...section.packages.values()].sort(comparePackages);
|
|
311
|
+
const packageList = packages
|
|
312
|
+
.map((packageInfo) => {
|
|
313
|
+
const label = `${packageName(packageInfo)} — ${packageInfo.declaredLicense ?? 'UNKNOWN'}`;
|
|
314
|
+
// Tells recipients where to obtain the source, which MPL-2.0 and the GPL family require
|
|
315
|
+
return packageInfo.repositoryUrl ? `${label} (${packageInfo.repositoryUrl})` : label;
|
|
316
|
+
})
|
|
317
|
+
.join(', ');
|
|
318
|
+
return `Packages: ${packageList}\n${separator}\n${normalizeLegalText(section.text.content)}`;
|
|
319
|
+
});
|
|
320
|
+
return `${renderedSections.join('\n\n')}\n`;
|
|
321
|
+
}
|
|
322
|
+
export function renderLicenseFile(inventory) {
|
|
323
|
+
return renderLegalFile(inventory, ['license', 'source-comment']) ?? '';
|
|
324
|
+
}
|
|
325
|
+
export function renderNoticeFile(inventory) {
|
|
326
|
+
return renderLegalFile(inventory, ['notice']);
|
|
327
|
+
}
|
|
328
|
+
export async function writeLegalArtifacts(outputDir, inventory) {
|
|
329
|
+
await writeFile(path.join(outputDir, 'LICENSE'), renderLicenseFile(inventory));
|
|
330
|
+
const notice = renderNoticeFile(inventory);
|
|
331
|
+
const noticePath = path.join(outputDir, 'NOTICE');
|
|
332
|
+
if (notice)
|
|
333
|
+
await writeFile(noticePath, notice);
|
|
334
|
+
else
|
|
335
|
+
await rm(noticePath, { force: true });
|
|
336
|
+
}
|
|
337
|
+
const kindPriority = {
|
|
338
|
+
project: 0,
|
|
339
|
+
bundled: 1,
|
|
340
|
+
external: 2,
|
|
341
|
+
};
|
|
342
|
+
export async function createLegalInventory(packages) {
|
|
343
|
+
const mergedPackages = new Map();
|
|
344
|
+
for (const packageInfo of packages) {
|
|
345
|
+
const existing = mergedPackages.get(packageInfo.packageRoot);
|
|
346
|
+
if (!existing) {
|
|
347
|
+
mergedPackages.set(packageInfo.packageRoot, {
|
|
348
|
+
...packageInfo,
|
|
349
|
+
contributingPaths: new Set(packageInfo.contributingPaths),
|
|
350
|
+
});
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
for (const sourcePath of packageInfo.contributingPaths)
|
|
354
|
+
existing.contributingPaths.add(sourcePath);
|
|
355
|
+
if (kindPriority[packageInfo.kind] < kindPriority[existing.kind])
|
|
356
|
+
existing.kind = packageInfo.kind;
|
|
357
|
+
}
|
|
358
|
+
const diagnostics = [];
|
|
359
|
+
const legalRecords = [];
|
|
360
|
+
for (const packageInfo of mergedPackages.values()) {
|
|
361
|
+
const material = await collectPackageLegalMaterial(packageInfo);
|
|
362
|
+
legalRecords.push(material.package);
|
|
363
|
+
diagnostics.push(...material.diagnostics);
|
|
364
|
+
}
|
|
365
|
+
return { packages: legalRecords, diagnostics };
|
|
366
|
+
}
|
|
367
|
+
export async function analyzeShippedLegalInventory(moduleDir) {
|
|
368
|
+
const definition = await loadModuleBuildDefinition(moduleDir);
|
|
369
|
+
const result = await esbuild.build(createEsbuildOptions(definition, {
|
|
370
|
+
outdir: path.join(moduleDir, '.license-analysis'),
|
|
371
|
+
write: false,
|
|
372
|
+
minify: false,
|
|
373
|
+
sourcemap: false,
|
|
374
|
+
}));
|
|
375
|
+
if (!result.metafile)
|
|
376
|
+
throw new Error('esbuild did not produce a metafile');
|
|
377
|
+
const metafilePackages = await collectMetafilePackages(moduleDir, result.metafile);
|
|
378
|
+
const packages = [...metafilePackages.packages];
|
|
379
|
+
if (definition.externals.length) {
|
|
380
|
+
const dependencies = await resolveExternalDependencies(moduleDir, definition.externals);
|
|
381
|
+
packages.push(...(await withInstalledExternalTree(dependencies, collectInstalledPackages)));
|
|
382
|
+
}
|
|
383
|
+
const inventory = await createLegalInventory(packages);
|
|
384
|
+
return { ...inventory, diagnostics: [...metafilePackages.diagnostics, ...inventory.diagnostics] };
|
|
385
|
+
}
|
|
386
|
+
export async function collectMetafilePackages(moduleDir, metafile) {
|
|
387
|
+
const resolvedModuleDir = await realpath(moduleDir);
|
|
388
|
+
const projectPackageJson = await readPackageJson(resolvedModuleDir);
|
|
389
|
+
const packages = new Map();
|
|
390
|
+
const diagnostics = [];
|
|
391
|
+
// The manifest license is what the packaged module is distributed as, which is what its dependencies must fit.
|
|
392
|
+
// package.json only licenses the module's own source, so it is a fallback for modules not declaring the other.
|
|
393
|
+
const projectLicense = (await readManifestLicense(resolvedModuleDir)) ?? declaredLicenseOf(projectPackageJson);
|
|
394
|
+
for (const input of getContributingInputs(metafile)) {
|
|
395
|
+
if (input.startsWith('<')) {
|
|
396
|
+
diagnostics.push(`Ignoring virtual esbuild input: ${input}`);
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
const inputPath = path.resolve(resolvedModuleDir, input);
|
|
400
|
+
if (!isPathInside(inputPath, resolvedModuleDir)) {
|
|
401
|
+
diagnostics.push(`Ignoring esbuild input outside module directory: ${input}`);
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
// Resolve ownership using logical node_modules path. Realpath would turn linked
|
|
405
|
+
// dependencies into paths outside moduleDir and incorrectly classify them as project code.
|
|
406
|
+
const packageRoot = (await findPackageRoot(inputPath, resolvedModuleDir)) ?? resolvedModuleDir;
|
|
407
|
+
const packageJson = packageRoot === resolvedModuleDir ? projectPackageJson : await readPackageJson(packageRoot);
|
|
408
|
+
const kind = packageRoot === resolvedModuleDir ? 'project' : 'bundled';
|
|
409
|
+
const key = `${kind}:${packageRoot}`;
|
|
410
|
+
let packageInfo = packages.get(key);
|
|
411
|
+
if (!packageInfo) {
|
|
412
|
+
packageInfo = {
|
|
413
|
+
kind,
|
|
414
|
+
name: packageJson.name ?? path.basename(packageRoot),
|
|
415
|
+
version: packageJson.version,
|
|
416
|
+
declaredLicense: kind === 'project' ? asLicenseString(projectLicense) : declaredLicenseOf(packageJson),
|
|
417
|
+
sourceLicense: kind === 'project' ? declaredLicenseOf(projectPackageJson) : undefined,
|
|
418
|
+
repositoryUrl: normalizeRepositoryUrl(packageJson),
|
|
419
|
+
packageRoot,
|
|
420
|
+
contributingPaths: new Set(),
|
|
421
|
+
};
|
|
422
|
+
packages.set(key, packageInfo);
|
|
423
|
+
}
|
|
424
|
+
packageInfo.contributingPaths.add(normalizeInventoryPath(inputPath, packageRoot));
|
|
425
|
+
}
|
|
426
|
+
return { packages: [...packages.values()], diagnostics };
|
|
427
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { LegalInventory, ShippedPackageLegalRecord } from './license-util.js';
|
|
2
|
+
export type ModuleType = 'connection' | 'surface';
|
|
3
|
+
export interface LicensePolicyIssue {
|
|
4
|
+
packageName: string;
|
|
5
|
+
packageVersion?: string;
|
|
6
|
+
packageKind: ShippedPackageLegalRecord['kind'];
|
|
7
|
+
message: string;
|
|
8
|
+
}
|
|
9
|
+
type LicensePolicyOutput = Pick<NodeJS.WriteStream, 'write'>;
|
|
10
|
+
export declare class LicensePolicyError extends Error {
|
|
11
|
+
readonly issues: LicensePolicyIssue[];
|
|
12
|
+
constructor(issues: LicensePolicyIssue[]);
|
|
13
|
+
}
|
|
14
|
+
/** Licenses a module may be declared as, each allowing a different set of dependency licenses */
|
|
15
|
+
export type ProjectLicense = 'MIT' | 'GPL-2.0-only' | 'GPL-3.0-only';
|
|
16
|
+
export declare const SUPPORTED_PROJECT_LICENSES: ProjectLicense[];
|
|
17
|
+
/** Resolve the license the packaged module is distributed under, which selects the policy applied to its dependencies */
|
|
18
|
+
export declare function resolveProjectLicense(inventory: LegalInventory): {
|
|
19
|
+
declaration: string;
|
|
20
|
+
license: ProjectLicense | undefined;
|
|
21
|
+
};
|
|
22
|
+
export declare function createLicensePolicyIssues(inventory: LegalInventory): LicensePolicyIssue[];
|
|
23
|
+
export declare function enforceLicensePolicy(inventory: LegalInventory, options?: {
|
|
24
|
+
ignoreLicenseRules?: boolean;
|
|
25
|
+
stderr?: LicensePolicyOutput;
|
|
26
|
+
}): void;
|
|
27
|
+
export {};
|