license-checker-plugin 1.0.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/LICENSE +21 -0
- package/README.md +239 -0
- package/dist/LicensePluginCore.d.ts +84 -0
- package/dist/LicensePluginCore.js +148 -0
- package/dist/LicenseWebpackPlugin.d.ts +11 -0
- package/dist/LicenseWebpackPlugin.js +53 -0
- package/dist/Recorder.d.ts +14 -0
- package/dist/Recorder.js +46 -0
- package/dist/ViteLicensePlugin.d.ts +16 -0
- package/dist/ViteLicensePlugin.js +54 -0
- package/dist/checker/BuiltInLicenseChecker.d.ts +27 -0
- package/dist/checker/BuiltInLicenseChecker.js +311 -0
- package/dist/checker/LicenseCache.d.ts +9 -0
- package/dist/checker/LicenseCache.js +22 -0
- package/dist/checker/LicenseDatabase.d.ts +8 -0
- package/dist/checker/LicenseDatabase.js +52 -0
- package/dist/formatter/Formatter.d.ts +4 -0
- package/dist/formatter/Formatter.js +2 -0
- package/dist/formatter/HtmlFormatter.d.ts +6 -0
- package/dist/formatter/HtmlFormatter.js +53 -0
- package/dist/formatter/JsonFormatter.d.ts +5 -0
- package/dist/formatter/JsonFormatter.js +17 -0
- package/dist/formatter/MarkdownFormatter.d.ts +5 -0
- package/dist/formatter/MarkdownFormatter.js +18 -0
- package/dist/formatter/TxtFormatter.d.ts +12 -0
- package/dist/formatter/TxtFormatter.js +49 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +13 -0
- package/dist/model/LicenseBuildReport.d.ts +4 -0
- package/dist/model/LicenseBuildReport.js +2 -0
- package/dist/model/LicenseInfo.d.ts +17 -0
- package/dist/model/LicenseInfo.js +2 -0
- package/dist/model/PackageInfo.d.ts +13 -0
- package/dist/model/PackageInfo.js +2 -0
- package/dist/scanner/PackageResolver.d.ts +4 -0
- package/dist/scanner/PackageResolver.js +102 -0
- package/dist/scanner/PackageScanner.d.ts +9 -0
- package/dist/scanner/PackageScanner.js +56 -0
- package/dist/utils/fs.d.ts +3 -0
- package/dist/utils/fs.js +61 -0
- package/dist/utils/hash.d.ts +1 -0
- package/dist/utils/hash.js +7 -0
- package/dist/utils/path.d.ts +3 -0
- package/dist/utils/path.js +64 -0
- package/package.json +58 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Axetroy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# license-checker-plugin
|
|
2
|
+
|
|
3
|
+
A bundler-agnostic plugin that generates third-party license notices for packages actually bundled into your final build. Supports **webpack 5**, **Rspack**, and **Vite**.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Scans the bundler's module graph for used npm packages
|
|
8
|
+
- Reads license metadata using the built-in license checker (zero external dependencies)
|
|
9
|
+
- Emits TXT, JSON, Markdown, or HTML assets at build time
|
|
10
|
+
- Supports compliance rules with `onlyAllow` and `failOn`
|
|
11
|
+
- Filter by package name and/or license type
|
|
12
|
+
- Deduplicates repeated license text
|
|
13
|
+
- Works with webpack 5, Rspack, and Vite
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install license-checker-plugin
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
For webpack/Rspack projects, ensure `webpack` is installed (peer dependency).
|
|
22
|
+
For Vite projects, ensure `vite` is installed (peer dependency).
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
### webpack / Rspack
|
|
27
|
+
|
|
28
|
+
```js
|
|
29
|
+
const { LicenseWebpackPlugin } = require('license-checker-plugin');
|
|
30
|
+
|
|
31
|
+
module.exports = {
|
|
32
|
+
mode: 'production',
|
|
33
|
+
plugins: [
|
|
34
|
+
new LicenseWebpackPlugin({
|
|
35
|
+
filename: 'third-party-licenses.txt',
|
|
36
|
+
format: 'txt',
|
|
37
|
+
includeLicenseText: true,
|
|
38
|
+
onlyAllow: ['MIT', 'Apache-2.0', 'BSD-3-Clause'],
|
|
39
|
+
}),
|
|
40
|
+
],
|
|
41
|
+
};
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Vite
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
// vite.config.ts
|
|
48
|
+
import { defineConfig } from 'vite';
|
|
49
|
+
import { viteLicensePlugin } from 'license-checker-plugin';
|
|
50
|
+
|
|
51
|
+
export default defineConfig({
|
|
52
|
+
plugins: [
|
|
53
|
+
viteLicensePlugin({
|
|
54
|
+
filename: 'third-party-licenses.json',
|
|
55
|
+
format: 'json',
|
|
56
|
+
includeLicenseText: true,
|
|
57
|
+
}),
|
|
58
|
+
],
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Options
|
|
63
|
+
|
|
64
|
+
| Option | Type | Default | Description |
|
|
65
|
+
|---|---|---|---|
|
|
66
|
+
| `filename` | `string` | `licenses.txt` | Output asset name |
|
|
67
|
+
| `format` | `'txt' \| 'json' \| 'markdown' \| 'html'` | `txt` | Output format |
|
|
68
|
+
| `includeLicenseText` | `boolean` | `true` | Include license text when supported |
|
|
69
|
+
| `includeRepository` | `boolean` | `true` | Include repository URLs |
|
|
70
|
+
| `includeHomepage` | `boolean` | `true` | Include homepage URLs |
|
|
71
|
+
| `includeAuthor` | `boolean` | `true` | Include author/publisher details |
|
|
72
|
+
| `excludePackages` | `string[]` | `[]` | Exclude listed packages from output |
|
|
73
|
+
| `onlyAllow` | `string[]` | `[]` | Fail build when a used license is not allowed |
|
|
74
|
+
| `failOn` | `string[]` | `[]` | Fail build when a used license matches the list |
|
|
75
|
+
| `cache` | `boolean` | `true` | Reuse the in-memory license database |
|
|
76
|
+
| `workspaceRoot` | `string` | Bundler's root context | Root path for license scanning |
|
|
77
|
+
| `recorder` | `Recorder` | — | External recorder shared across compiler instances (webpack only) |
|
|
78
|
+
| `recordOnly` | `boolean` | `false` | Record findings without emitting (webpack multi-compiler only) |
|
|
79
|
+
| `waitForRecorderCount` | `number` | — | Wait for N reports before emitting combined asset (webpack only) |
|
|
80
|
+
|
|
81
|
+
> **Note:** `recorder`, `recordOnly`, and `waitForRecorderCount` are webpack-only options and are not available in the Vite plugin.
|
|
82
|
+
|
|
83
|
+
## How it works
|
|
84
|
+
|
|
85
|
+
The plugin uses a **two-phase** architecture:
|
|
86
|
+
|
|
87
|
+
1. **Database phase**: Scans `node_modules` with the built-in license checker to build a comprehensive cache of all package license information.
|
|
88
|
+
2. **Scan phase**: Inspects the bundler's module graph to find which packages are actually used in the final bundle.
|
|
89
|
+
|
|
90
|
+
Only packages that appear in the bundler's output are included in the license asset. DevDependencies that are never imported will not appear.
|
|
91
|
+
|
|
92
|
+
## Filtering dependencies by package or license
|
|
93
|
+
|
|
94
|
+
The plugin starts with the set of dependency entries detected from the bundler's module graph, resolves license metadata for those entries, and then applies the configured filters. Package names and license names are matched with exact, case-sensitive string comparison.
|
|
95
|
+
|
|
96
|
+
### Filter options
|
|
97
|
+
|
|
98
|
+
- `excludePackages`: remove entries whose package name is listed.
|
|
99
|
+
- `onlyAllow`: fail the build if a package license is not in the allowed list.
|
|
100
|
+
- `failOn`: fail the build if a package license matches the list.
|
|
101
|
+
|
|
102
|
+
### Evaluation order
|
|
103
|
+
|
|
104
|
+
1. `excludePackages`
|
|
105
|
+
2. `onlyAllow`
|
|
106
|
+
3. `failOn`
|
|
107
|
+
|
|
108
|
+
All bundled packages are included by default. `excludePackages` removes unwanted entries first, then `onlyAllow` and `failOn` enforce license policies — generating build errors and stopping output when violated.
|
|
109
|
+
|
|
110
|
+
### Transitive dependency behavior
|
|
111
|
+
|
|
112
|
+
`excludePackages` is entry-based. Excluding `foo` removes the `foo` entry itself, but it does not automatically remove every dependency that `foo` depends on.
|
|
113
|
+
|
|
114
|
+
### Combined filter example
|
|
115
|
+
|
|
116
|
+
```js
|
|
117
|
+
new LicenseWebpackPlugin({
|
|
118
|
+
excludePackages: ['react-dom'],
|
|
119
|
+
onlyAllow: ['MIT'],
|
|
120
|
+
failOn: ['MIT-0'],
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
With this configuration:
|
|
125
|
+
|
|
126
|
+
- all bundled packages are included by default
|
|
127
|
+
- `react-dom` is removed from output
|
|
128
|
+
- it fails the build if any remaining package does not have an MIT license
|
|
129
|
+
- it fails the build if any remaining package has an MIT-0 license
|
|
130
|
+
|
|
131
|
+
## Output formats
|
|
132
|
+
|
|
133
|
+
### TXT
|
|
134
|
+
Human-readable notice file for app distribution.
|
|
135
|
+
|
|
136
|
+
### JSON
|
|
137
|
+
Machine-readable output for CI, audits, and internal tooling.
|
|
138
|
+
|
|
139
|
+
### Markdown
|
|
140
|
+
Useful for GitHub releases or repository documentation.
|
|
141
|
+
|
|
142
|
+
### HTML
|
|
143
|
+
Useful for Electron about pages and in-app license views.
|
|
144
|
+
|
|
145
|
+
## Compliance examples
|
|
146
|
+
|
|
147
|
+
```js
|
|
148
|
+
new LicenseWebpackPlugin({
|
|
149
|
+
onlyAllow: ['MIT', 'Apache-2.0'],
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
new LicenseWebpackPlugin({
|
|
153
|
+
failOn: ['GPL-3.0', 'AGPL-3.0'],
|
|
154
|
+
});
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Multi-compiler usage (webpack only)
|
|
158
|
+
|
|
159
|
+
When using webpack's [multi-compiler](https://webpack.js.org/configuration/configuration-types/#exporting-multiple-configurations) mode (an array of configurations) each compiler runs independently. The `recorder` option lets all instances share a single `DefaultRecorder` so that one primary instance can merge all their findings and emit a single combined license file.
|
|
160
|
+
|
|
161
|
+
```js
|
|
162
|
+
const { LicenseWebpackPlugin, DefaultRecorder } = require('license-checker-plugin');
|
|
163
|
+
|
|
164
|
+
const sharedRecorder = new DefaultRecorder();
|
|
165
|
+
|
|
166
|
+
module.exports = [
|
|
167
|
+
{
|
|
168
|
+
name: 'renderer',
|
|
169
|
+
entry: './src/renderer/index.js',
|
|
170
|
+
plugins: [
|
|
171
|
+
new LicenseWebpackPlugin({
|
|
172
|
+
recorder: sharedRecorder,
|
|
173
|
+
recordOnly: true,
|
|
174
|
+
}),
|
|
175
|
+
],
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
name: 'main',
|
|
179
|
+
entry: './src/main/index.js',
|
|
180
|
+
plugins: [
|
|
181
|
+
new LicenseWebpackPlugin({
|
|
182
|
+
filename: 'third-party-licenses.txt',
|
|
183
|
+
recorder: sharedRecorder,
|
|
184
|
+
waitForRecorderCount: 2,
|
|
185
|
+
}),
|
|
186
|
+
],
|
|
187
|
+
},
|
|
188
|
+
];
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### `Recorder` interface
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
export interface Recorder {
|
|
195
|
+
record(report: LicenseBuildReport): void;
|
|
196
|
+
getReports(): LicenseBuildReport[];
|
|
197
|
+
waitForReports(expectedCount?: number, timeoutMs?: number): Promise<LicenseBuildReport[]>;
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
`DefaultRecorder` is the built-in implementation. It stores reports in memory and resolves `waitForReports` as soon as the expected count is reached.
|
|
202
|
+
|
|
203
|
+
## API
|
|
204
|
+
|
|
205
|
+
### `LicenseWebpackPlugin`
|
|
206
|
+
|
|
207
|
+
webpack/Rspack plugin (class, use with `new`).
|
|
208
|
+
|
|
209
|
+
### `viteLicensePlugin(options)`
|
|
210
|
+
|
|
211
|
+
Vite plugin (function, returns a plugin object).
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
import { viteLicensePlugin } from 'license-checker-plugin';
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### `LicensePluginCore`
|
|
218
|
+
|
|
219
|
+
Framework-agnostic core that can be used to build adapters for other bundlers.
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
import { LicensePluginCore } from 'license-checker-plugin';
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### `DefaultRecorder`
|
|
226
|
+
|
|
227
|
+
Built-in recorder implementation for multi-compiler scenarios.
|
|
228
|
+
|
|
229
|
+
## Development
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
npm install
|
|
233
|
+
npm run build
|
|
234
|
+
npm test
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
## License
|
|
238
|
+
|
|
239
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { OutputItem } from './model/LicenseInfo';
|
|
2
|
+
import { PackageInfo } from './model/PackageInfo';
|
|
3
|
+
import { Recorder } from './Recorder';
|
|
4
|
+
export type OutputFormat = 'txt' | 'json' | 'markdown' | 'html';
|
|
5
|
+
export interface LicensePluginOptions {
|
|
6
|
+
/** Output file name (e.g. `licenses.txt`, `third-party-licenses.json`). */
|
|
7
|
+
filename?: string;
|
|
8
|
+
/** Output format. */
|
|
9
|
+
format?: OutputFormat;
|
|
10
|
+
/** Include the full license text in the output. */
|
|
11
|
+
includeLicenseText?: boolean;
|
|
12
|
+
/** Include the repository URL in each entry. */
|
|
13
|
+
includeRepository?: boolean;
|
|
14
|
+
/** Include the homepage URL in each entry. */
|
|
15
|
+
includeHomepage?: boolean;
|
|
16
|
+
/** Include the author/publisher in each entry. */
|
|
17
|
+
includeAuthor?: boolean;
|
|
18
|
+
/** Exclude specific packages from the output by name. */
|
|
19
|
+
excludePackages?: string[];
|
|
20
|
+
/**
|
|
21
|
+
* Allow only these licenses. When set, the build fails if any bundled
|
|
22
|
+
* package has a license not in this list.
|
|
23
|
+
*/
|
|
24
|
+
onlyAllow?: string[];
|
|
25
|
+
/**
|
|
26
|
+
* Fail the build when a bundled package license matches this list.
|
|
27
|
+
* Evaluated after `onlyAllow`.
|
|
28
|
+
*/
|
|
29
|
+
failOn?: string[];
|
|
30
|
+
/**
|
|
31
|
+
* Reuse the in-memory license database across multiple plugin instances
|
|
32
|
+
* (e.g. in multi-compiler setups). Set to `false` to force a fresh scan
|
|
33
|
+
* each time.
|
|
34
|
+
*/
|
|
35
|
+
cache?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Root path for scanning `node_modules`. Defaults to the bundler's root
|
|
38
|
+
* context (project root).
|
|
39
|
+
*/
|
|
40
|
+
workspaceRoot?: string;
|
|
41
|
+
/**
|
|
42
|
+
* External recorder for sharing findings across compiler instances
|
|
43
|
+
* (webpack multi-compiler only).
|
|
44
|
+
* @see DefaultRecorder
|
|
45
|
+
*/
|
|
46
|
+
recorder?: Recorder;
|
|
47
|
+
/**
|
|
48
|
+
* When `true`, only record findings via `recorder` without emitting a
|
|
49
|
+
* license asset. Use together with `recorder` and `waitForRecorderCount`
|
|
50
|
+
* (webpack multi-compiler only).
|
|
51
|
+
*/
|
|
52
|
+
recordOnly?: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Wait for this many reports from the shared recorder before emitting
|
|
55
|
+
* the combined, deduplicated license asset.
|
|
56
|
+
* Requires `recorder` to be set (webpack multi-compiler only).
|
|
57
|
+
*/
|
|
58
|
+
waitForRecorderCount?: number;
|
|
59
|
+
}
|
|
60
|
+
export interface LicensePluginContext {
|
|
61
|
+
reportError(msg: string): void;
|
|
62
|
+
reportWarning(msg: string): void;
|
|
63
|
+
}
|
|
64
|
+
export declare class LicensePluginCore {
|
|
65
|
+
readonly options: Required<Omit<LicensePluginOptions, 'recorder' | 'waitForRecorderCount'>> & {
|
|
66
|
+
recorder: Recorder | undefined;
|
|
67
|
+
waitForRecorderCount: number | undefined;
|
|
68
|
+
};
|
|
69
|
+
private db;
|
|
70
|
+
constructor(options?: LicensePluginOptions);
|
|
71
|
+
initialize(startPath: string, context: LicensePluginContext): Promise<boolean>;
|
|
72
|
+
generateLicenseItems(packages: Map<string, PackageInfo>, context: LicensePluginContext): Promise<{
|
|
73
|
+
items: OutputItem[];
|
|
74
|
+
errors: string[];
|
|
75
|
+
}>;
|
|
76
|
+
private resolveLicenseEntries;
|
|
77
|
+
private checkCompliance;
|
|
78
|
+
private buildOutputItems;
|
|
79
|
+
private recordReport;
|
|
80
|
+
private mergeReports;
|
|
81
|
+
format(items: OutputItem[]): string;
|
|
82
|
+
private filterLicenseFields;
|
|
83
|
+
private createFormatter;
|
|
84
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LicensePluginCore = void 0;
|
|
4
|
+
const LicenseDatabase_1 = require("./checker/LicenseDatabase");
|
|
5
|
+
const HtmlFormatter_1 = require("./formatter/HtmlFormatter");
|
|
6
|
+
const JsonFormatter_1 = require("./formatter/JsonFormatter");
|
|
7
|
+
const MarkdownFormatter_1 = require("./formatter/MarkdownFormatter");
|
|
8
|
+
const TxtFormatter_1 = require("./formatter/TxtFormatter");
|
|
9
|
+
class LicensePluginCore {
|
|
10
|
+
options;
|
|
11
|
+
db;
|
|
12
|
+
constructor(options = {}) {
|
|
13
|
+
this.options = {
|
|
14
|
+
filename: options.filename || 'licenses.txt',
|
|
15
|
+
format: options.format || 'txt',
|
|
16
|
+
includeLicenseText: options.includeLicenseText !== false,
|
|
17
|
+
includeRepository: options.includeRepository !== false,
|
|
18
|
+
includeHomepage: options.includeHomepage !== false,
|
|
19
|
+
includeAuthor: options.includeAuthor !== false,
|
|
20
|
+
excludePackages: options.excludePackages || [],
|
|
21
|
+
onlyAllow: options.onlyAllow || [],
|
|
22
|
+
failOn: options.failOn || [],
|
|
23
|
+
cache: options.cache !== false,
|
|
24
|
+
workspaceRoot: options.workspaceRoot || '',
|
|
25
|
+
recorder: options.recorder,
|
|
26
|
+
recordOnly: options.recordOnly === true,
|
|
27
|
+
waitForRecorderCount: options.waitForRecorderCount,
|
|
28
|
+
};
|
|
29
|
+
this.db = new LicenseDatabase_1.LicenseDatabase();
|
|
30
|
+
}
|
|
31
|
+
async initialize(startPath, context) {
|
|
32
|
+
if (!this.options.cache) {
|
|
33
|
+
this.db = new LicenseDatabase_1.LicenseDatabase();
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
await this.db.initialize(startPath);
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
context.reportError(`LicensePlugin: Failed to initialize license database: ${String(error)}`);
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async generateLicenseItems(packages, context) {
|
|
45
|
+
const entries = this.resolveLicenseEntries(packages);
|
|
46
|
+
const errors = this.checkCompliance(entries);
|
|
47
|
+
if (errors.length > 0) {
|
|
48
|
+
for (const err of errors)
|
|
49
|
+
context.reportError(err);
|
|
50
|
+
return { items: [], errors };
|
|
51
|
+
}
|
|
52
|
+
let items = this.buildOutputItems(entries);
|
|
53
|
+
this.recordReport(items);
|
|
54
|
+
if (this.options.recordOnly)
|
|
55
|
+
return { items: [], errors: [] };
|
|
56
|
+
if (this.options.recorder && this.options.waitForRecorderCount !== undefined) {
|
|
57
|
+
try {
|
|
58
|
+
items = this.mergeReports(items, await this.options.recorder.waitForReports(this.options.waitForRecorderCount));
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
context.reportError(String(error));
|
|
62
|
+
return { items: [], errors: [String(error)] };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
items.sort((a, b) => a.package.name.localeCompare(b.package.name));
|
|
66
|
+
return { items, errors: [] };
|
|
67
|
+
}
|
|
68
|
+
resolveLicenseEntries(packages) {
|
|
69
|
+
const entries = [];
|
|
70
|
+
for (const pkgInfo of packages.values()) {
|
|
71
|
+
if (this.options.excludePackages.includes(pkgInfo.name))
|
|
72
|
+
continue;
|
|
73
|
+
entries.push({
|
|
74
|
+
info: pkgInfo,
|
|
75
|
+
licenseInfo: this.filterLicenseFields(this.db.getLicense(pkgInfo.name, pkgInfo.version)),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return entries;
|
|
79
|
+
}
|
|
80
|
+
checkCompliance(entries) {
|
|
81
|
+
const errors = [];
|
|
82
|
+
for (const { info, licenseInfo } of entries) {
|
|
83
|
+
if (this.options.onlyAllow.length > 0 && !this.options.onlyAllow.includes(licenseInfo.license)) {
|
|
84
|
+
errors.push(`LicensePlugin: License "${licenseInfo.license}" for package "${info.name}@${info.version}" is not in the allowed list: ${this.options.onlyAllow.join(', ')}`);
|
|
85
|
+
}
|
|
86
|
+
else if (this.options.failOn.length > 0 && this.options.failOn.includes(licenseInfo.license)) {
|
|
87
|
+
errors.push(`LicensePlugin: License "${licenseInfo.license}" for package "${info.name}@${info.version}" is in the fail list`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return errors;
|
|
91
|
+
}
|
|
92
|
+
buildOutputItems(entries) {
|
|
93
|
+
return entries.map(({ info, licenseInfo }) => ({
|
|
94
|
+
package: {
|
|
95
|
+
...info,
|
|
96
|
+
repository: this.options.includeRepository ? info.repository : undefined,
|
|
97
|
+
homepage: this.options.includeHomepage ? info.homepage : undefined,
|
|
98
|
+
author: this.options.includeAuthor ? info.author : undefined,
|
|
99
|
+
},
|
|
100
|
+
license: { ...licenseInfo },
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
recordReport(items) {
|
|
104
|
+
if (this.options.recorder) {
|
|
105
|
+
this.options.recorder.record({ items });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
mergeReports(items, allReports) {
|
|
109
|
+
const seen = new Set();
|
|
110
|
+
const merged = [];
|
|
111
|
+
for (const report of allReports) {
|
|
112
|
+
for (const item of report.items) {
|
|
113
|
+
const key = `${item.package.name}@${item.package.version}`;
|
|
114
|
+
if (!seen.has(key)) {
|
|
115
|
+
seen.add(key);
|
|
116
|
+
merged.push(item);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return merged;
|
|
121
|
+
}
|
|
122
|
+
format(items) {
|
|
123
|
+
return this.createFormatter().generate(items);
|
|
124
|
+
}
|
|
125
|
+
filterLicenseFields(licenseInfo) {
|
|
126
|
+
const result = { license: licenseInfo.license };
|
|
127
|
+
if (licenseInfo.licenseFile)
|
|
128
|
+
result.licenseFile = licenseInfo.licenseFile;
|
|
129
|
+
if (this.options.includeLicenseText && licenseInfo.licenseText) {
|
|
130
|
+
result.licenseText = licenseInfo.licenseText;
|
|
131
|
+
}
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
createFormatter() {
|
|
135
|
+
switch (this.options.format) {
|
|
136
|
+
case 'json':
|
|
137
|
+
return new JsonFormatter_1.JsonFormatter();
|
|
138
|
+
case 'markdown':
|
|
139
|
+
return new MarkdownFormatter_1.MarkdownFormatter();
|
|
140
|
+
case 'html':
|
|
141
|
+
return new HtmlFormatter_1.HtmlFormatter();
|
|
142
|
+
case 'txt':
|
|
143
|
+
default:
|
|
144
|
+
return new TxtFormatter_1.TxtFormatter({ includeLicenseText: this.options.includeLicenseText });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
exports.LicensePluginCore = LicensePluginCore;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Compiler, WebpackPluginInstance } from 'webpack';
|
|
2
|
+
import type { LicensePluginOptions, OutputFormat } from './LicensePluginCore';
|
|
3
|
+
export type { LicensePluginOptions, OutputFormat };
|
|
4
|
+
/** @deprecated Use `LicensePluginOptions` instead. */
|
|
5
|
+
export type LicenseWebpackPluginOptions = LicensePluginOptions;
|
|
6
|
+
export declare class LicenseWebpackPlugin implements WebpackPluginInstance {
|
|
7
|
+
private readonly core;
|
|
8
|
+
constructor(options?: LicensePluginOptions);
|
|
9
|
+
apply(compiler: Compiler): void;
|
|
10
|
+
private generateLicenses;
|
|
11
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LicenseWebpackPlugin = void 0;
|
|
4
|
+
const LicensePluginCore_1 = require("./LicensePluginCore");
|
|
5
|
+
const PackageScanner_1 = require("./scanner/PackageScanner");
|
|
6
|
+
const PLUGIN_NAME = 'LicenseWebpackPlugin';
|
|
7
|
+
const DEFAULT_PROCESS_ASSETS_STAGE_REPORT = 5000;
|
|
8
|
+
class LicenseWebpackPlugin {
|
|
9
|
+
core;
|
|
10
|
+
constructor(options = {}) {
|
|
11
|
+
this.core = new LicensePluginCore_1.LicensePluginCore(options);
|
|
12
|
+
}
|
|
13
|
+
apply(compiler) {
|
|
14
|
+
const wp = compiler.webpack;
|
|
15
|
+
const processAssetsStageReport = wp?.Compilation?.PROCESS_ASSETS_STAGE_REPORT ?? DEFAULT_PROCESS_ASSETS_STAGE_REPORT;
|
|
16
|
+
compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (compilation) => {
|
|
17
|
+
compilation.hooks.processAssets.tapPromise({
|
|
18
|
+
name: PLUGIN_NAME,
|
|
19
|
+
stage: processAssetsStageReport,
|
|
20
|
+
}, async () => {
|
|
21
|
+
try {
|
|
22
|
+
await this.generateLicenses(compiler, compilation, wp);
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
compilation.errors.push(error);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
async generateLicenses(compiler, compilation, wp) {
|
|
31
|
+
const startPath = this.core.options.workspaceRoot || compiler.context;
|
|
32
|
+
const context = {
|
|
33
|
+
reportError: (msg) => compilation.errors.push(new Error(msg)),
|
|
34
|
+
reportWarning: (msg) => compilation.warnings.push(new Error(msg)),
|
|
35
|
+
};
|
|
36
|
+
const initialized = await this.core.initialize(startPath, context);
|
|
37
|
+
if (!initialized)
|
|
38
|
+
return;
|
|
39
|
+
const scanner = new PackageScanner_1.PackageScanner();
|
|
40
|
+
const packages = scanner.scan(compilation);
|
|
41
|
+
const { items, errors } = await this.core.generateLicenseItems(packages, context);
|
|
42
|
+
if (errors.length > 0)
|
|
43
|
+
return;
|
|
44
|
+
const sourcesApi = wp?.sources;
|
|
45
|
+
if (!sourcesApi) {
|
|
46
|
+
compilation.warnings.push(new Error('LicenseWebpackPlugin: bundler sources API not available on compiler.webpack; ' +
|
|
47
|
+
'license asset will not be emitted. Ensure you are using webpack 5 or Rspack.'));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
compilation.emitAsset(this.core.options.filename, new sourcesApi.RawSource(this.core.format(items)));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
exports.LicenseWebpackPlugin = LicenseWebpackPlugin;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { LicenseBuildReport } from './model/LicenseBuildReport';
|
|
2
|
+
export interface Recorder {
|
|
3
|
+
record(report: LicenseBuildReport): void;
|
|
4
|
+
getReports(): LicenseBuildReport[];
|
|
5
|
+
waitForReports(expectedCount?: number, timeoutMs?: number): Promise<LicenseBuildReport[]>;
|
|
6
|
+
}
|
|
7
|
+
export declare class DefaultRecorder implements Recorder {
|
|
8
|
+
private reports;
|
|
9
|
+
private waiters;
|
|
10
|
+
record(report: LicenseBuildReport): void;
|
|
11
|
+
getReports(): LicenseBuildReport[];
|
|
12
|
+
waitForReports(expectedCount?: number, timeoutMs?: number): Promise<LicenseBuildReport[]>;
|
|
13
|
+
private flushWaiters;
|
|
14
|
+
}
|
package/dist/Recorder.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DefaultRecorder = void 0;
|
|
4
|
+
class DefaultRecorder {
|
|
5
|
+
reports = [];
|
|
6
|
+
waiters = [];
|
|
7
|
+
record(report) {
|
|
8
|
+
this.reports.push(report);
|
|
9
|
+
this.flushWaiters();
|
|
10
|
+
}
|
|
11
|
+
getReports() {
|
|
12
|
+
return [...this.reports];
|
|
13
|
+
}
|
|
14
|
+
waitForReports(expectedCount, timeoutMs = 30000) {
|
|
15
|
+
if (expectedCount === undefined || this.reports.length >= expectedCount) {
|
|
16
|
+
return Promise.resolve(this.getReports());
|
|
17
|
+
}
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
const timer = setTimeout(() => {
|
|
20
|
+
this.waiters = this.waiters.filter((w) => w !== waiter);
|
|
21
|
+
reject(new Error(`LicenseWebpackPlugin: waitForReports timed out after ${timeoutMs}ms. ` +
|
|
22
|
+
`Expected ${expectedCount} reports but received ${this.reports.length}.`));
|
|
23
|
+
}, timeoutMs);
|
|
24
|
+
const waiter = { expectedCount, resolve, reject, timer };
|
|
25
|
+
this.waiters.push(waiter);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
flushWaiters() {
|
|
29
|
+
const satisfied = [];
|
|
30
|
+
const remaining = [];
|
|
31
|
+
for (const waiter of this.waiters) {
|
|
32
|
+
if (this.reports.length >= waiter.expectedCount) {
|
|
33
|
+
satisfied.push(waiter);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
remaining.push(waiter);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
this.waiters = remaining;
|
|
40
|
+
for (const waiter of satisfied) {
|
|
41
|
+
clearTimeout(waiter.timer);
|
|
42
|
+
waiter.resolve(this.getReports());
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
exports.DefaultRecorder = DefaultRecorder;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { LicensePluginOptions } from './LicensePluginCore';
|
|
2
|
+
interface VitePlugin {
|
|
3
|
+
name: string;
|
|
4
|
+
enforce?: 'pre' | 'post';
|
|
5
|
+
buildStart?: () => void | Promise<void>;
|
|
6
|
+
transform?: (code: string, id: string) => string | null | undefined | void;
|
|
7
|
+
generateBundle?: (this: {
|
|
8
|
+
emitFile: (opts: {
|
|
9
|
+
type: string;
|
|
10
|
+
fileName: string;
|
|
11
|
+
source: string;
|
|
12
|
+
}) => void;
|
|
13
|
+
}, opts: unknown, bundle: unknown) => void | Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
export declare function viteLicensePlugin(options?: LicensePluginOptions): VitePlugin;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.viteLicensePlugin = viteLicensePlugin;
|
|
4
|
+
const LicensePluginCore_1 = require("./LicensePluginCore");
|
|
5
|
+
const PackageResolver_1 = require("./scanner/PackageResolver");
|
|
6
|
+
const PLUGIN_NAME = 'vite-license-plugin';
|
|
7
|
+
function viteLicensePlugin(options = {}) {
|
|
8
|
+
const core = new LicensePluginCore_1.LicensePluginCore(options);
|
|
9
|
+
const resolver = new PackageResolver_1.PackageResolver();
|
|
10
|
+
const resolvedPackages = new Map();
|
|
11
|
+
return {
|
|
12
|
+
name: PLUGIN_NAME,
|
|
13
|
+
enforce: 'post',
|
|
14
|
+
async buildStart() {
|
|
15
|
+
const root = options.workspaceRoot || process.cwd();
|
|
16
|
+
const context = {
|
|
17
|
+
reportError: (msg) => { throw new Error(msg); },
|
|
18
|
+
reportWarning: (msg) => console.warn(`[${PLUGIN_NAME}] ${msg}`),
|
|
19
|
+
};
|
|
20
|
+
await core.initialize(root, context);
|
|
21
|
+
},
|
|
22
|
+
transform(_code, id) {
|
|
23
|
+
if (!id.includes('node_modules'))
|
|
24
|
+
return null;
|
|
25
|
+
const pkgInfo = resolver.resolve(id, 'main');
|
|
26
|
+
if (pkgInfo) {
|
|
27
|
+
const key = `${pkgInfo.name}@${pkgInfo.version}`;
|
|
28
|
+
if (!resolvedPackages.has(key)) {
|
|
29
|
+
resolvedPackages.set(key, pkgInfo);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
},
|
|
34
|
+
async generateBundle(_opts, _bundle) {
|
|
35
|
+
if (resolvedPackages.size === 0)
|
|
36
|
+
return;
|
|
37
|
+
const warnings = [];
|
|
38
|
+
const context = {
|
|
39
|
+
reportError: (msg) => { throw new Error(msg); },
|
|
40
|
+
reportWarning: (msg) => warnings.push(msg),
|
|
41
|
+
};
|
|
42
|
+
const { items } = await core.generateLicenseItems(resolvedPackages, context);
|
|
43
|
+
const source = core.format(items);
|
|
44
|
+
this.emitFile({
|
|
45
|
+
type: 'asset',
|
|
46
|
+
fileName: core.options.filename,
|
|
47
|
+
source,
|
|
48
|
+
});
|
|
49
|
+
for (const w of warnings) {
|
|
50
|
+
console.warn(`[${PLUGIN_NAME}] ${w}`);
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|