123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221 |
- 'use strict'
- const { constants } = require('node:fs')
- const fs = require('node:fs/promises')
- const path = require('node:path')
- const webpack = require('webpack')
- class WebpackSPDXPlugin {
- #options
-
- constructor(opts = {}) {
- this.#options = { override: {}, ...opts }
- }
- apply(compiler) {
- compiler.hooks.thisCompilation.tap('spdx-plugin', (compilation) => {
-
-
-
- compilation.hooks.processAssets.tapPromise(
- {
- name: 'spdx-plugin',
- stage: compilation.constructor.PROCESS_ASSETS_STAGE_REPORT,
- },
- () => this.emitLicenses(compilation),
- )
- })
- }
-
- async #findPackage(dir) {
- if (!dir || dir === '/' || dir === '.') {
- return null
- }
- const packageJson = `${dir}/package.json`
- try {
- await fs.access(packageJson, constants.F_OK)
- } catch (e) {
- return await this.#findPackage(path.dirname(dir))
- }
- const { private: isPrivatePacket, name } = JSON.parse(await fs.readFile(packageJson))
-
-
- if (isPrivatePacket === true || !name) {
- return (await this.#findPackage(path.dirname(dir))) ?? packageJson
- }
- return packageJson
- }
-
- async emitLicenses(compilation, callback) {
- const logger = compilation.getLogger('spdx-plugin')
-
- const packageInformation = new Map()
- const warnings = new Set()
-
- const sourceMap = new Map()
- for (const chunk of compilation.chunks) {
- for (const file of chunk.files) {
- if (sourceMap.has(file)) {
- sourceMap.get(file).add(chunk)
- } else {
- sourceMap.set(file, new Set([chunk]))
- }
- }
- }
- for (const [asset, chunks] of sourceMap.entries()) {
-
- const modules = new Set()
-
- const addModule = (module) => {
- if (module && !modules.has(module)) {
- modules.add(module)
- for (const dep of module.dependencies) {
- addModule(compilation.moduleGraph.getModule(dep))
- }
- }
- }
- chunks.forEach((chunk) => chunk.getModules().forEach(addModule))
- const sources = [...modules].map((module) => module.identifier())
- .map((source) => {
- const skipped = [
- 'delegated',
- 'external',
- 'container entry',
- 'ignored',
- 'remote',
- 'data:',
- ]
-
- if (skipped.some((prefix) => source.startsWith(prefix))) {
- return ''
- }
-
- if (source.startsWith('webpack/runtime')) {
- return require.resolve('webpack')
- }
-
- if (source.includes('!')) {
- return source.split('!').at(-1)
- }
- if (source.includes('|')) {
- return source
- .split('|')
- .filter((s) => s.startsWith(path.sep))
- .at(0)
- }
- return source
- })
- .filter((s) => !!s)
- .map((s) => s.split('?', 2)[0])
-
- if (sources.length === 0) {
- logger.warn(`Skipping ${asset} because it does not contain any source information`)
- continue
- }
-
- const packages = new Set()
-
- for (const sourcePath of sources) {
- const pkg = await this.#findPackage(path.dirname(sourcePath))
- if (!pkg) {
- logger.warn(`No package for source found (${sourcePath})`)
- continue
- }
- if (!packageInformation.has(pkg)) {
-
- const { author: packageAuthor, name, version, license: packageLicense, licenses } = JSON.parse(await fs.readFile(pkg))
-
- let license = !packageLicense && licenses
- ? licenses.map((entry) => entry.type ?? entry).join(' OR ')
- : packageLicense
- if (license?.includes(' ') && !license?.startsWith('(')) {
- license = `(${license})`
- }
-
- const author = typeof packageAuthor === 'object'
- ? `${packageAuthor.name}` + (packageAuthor.mail ? ` <${packageAuthor.mail}>` : '')
- : packageAuthor ?? `${name} developers`
- packageInformation.set(pkg, {
- version,
-
- name: name ?? path.basename(path.dirname(pkg)),
- author,
- license,
- })
- }
- packages.add(pkg)
- }
- let output = 'This file is generated from multiple sources. Included packages:\n'
- const authors = new Set()
- const licenses = new Set()
- for (const packageName of [...packages].sort()) {
- const pkg = packageInformation.get(packageName)
- const license = this.#options.override[pkg.name] ?? pkg.license
-
- if (!license && !warnings.has(pkg.name)) {
- logger.warn(`Missing license information for package ${pkg.name}, you should add it to the 'override' option.`)
- warnings.add(pkg.name)
- }
- licenses.add(license || 'unknown')
- authors.add(pkg.author)
- output += `- ${pkg.name}\n\t- version: ${pkg.version}\n\t- license: ${license}\n`
- }
- output = `\n\n${output}`
- for (const author of [...authors].sort()) {
- output = `SPDX-FileCopyrightText: ${author}\n${output}`
- }
- for (const license of [...licenses].sort()) {
- output = `SPDX-License-Identifier: ${license}\n${output}`
- }
- compilation.emitAsset(
- asset.split('?', 2)[0] + '.license',
- new webpack.sources.RawSource(output),
- )
- }
- if (callback) {
- return callback()
- }
- }
- }
- module.exports = WebpackSPDXPlugin
|