| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960 |
- import fs from 'node:fs'
- import path from 'node:path'
- /** License inventory is derived from actual bundled inputs, not all dev tools. */
- export function writeNotices(packageRoot, builds) {
- const found = new Map()
- for (const build of builds) {
- for (const input of Object.keys(build.metafile.inputs)) {
- if (!input.replaceAll('\\', '/').includes('node_modules/')) continue
- let directory = path.dirname(fs.realpathSync(path.resolve(input)))
- let manifest
- while (true) {
- const filename = path.join(directory, 'package.json')
- if (fs.existsSync(filename)) {
- const candidate = JSON.parse(fs.readFileSync(filename, 'utf8'))
- if (candidate.name && candidate.version) { manifest = candidate; break }
- }
- const parent = path.dirname(directory)
- if (parent === directory) throw new Error(`No package owner for bundled input: ${input}`)
- directory = parent
- }
- const key = `${manifest.name}@${manifest.version}`
- if (found.has(key)) continue
- const license = typeof manifest.license === 'string' ? manifest.license : manifest.license?.type
- const allowed = /^(MIT|ISC|Apache-2\.0|BSD-2-Clause|BSD-3-Clause|0BSD|\(MPL-2\.0 OR Apache-2\.0\))$/
- if (!allowed.test(license ?? '')) throw new Error(`Review bundled license before release: ${key} (${license})`)
- const files = fs.readdirSync(directory).filter(name => /^(licen[cs]e|copying|notice)([._-]|$)/i.test(name) && fs.statSync(path.join(directory, name)).isFile()).sort()
- if (!files.length) throw new Error(`No original license file found: ${key}`)
- const repository = typeof manifest.repository === 'string' ? manifest.repository : manifest.repository?.url
- found.set(key, { name: manifest.name, version: manifest.version, license, repository, directory, files })
- }
- }
- const entries = [...found.values()].sort((a, b) => `${a.name}@${a.version}`.localeCompare(`${b.name}@${b.version}`, 'en'))
- const licenses = path.join(packageRoot, 'licenses')
- fs.mkdirSync(licenses, { recursive: true })
- const expected = new Set()
- const rows = []
- for (const entry of entries) {
- const filename = `${entry.name.replaceAll('@', '').replaceAll('/', '__')}--${entry.version}.txt`
- expected.add(filename)
- const text = [`${entry.name}@${entry.version}`, `SPDX: ${entry.license}`, `Repository: ${entry.repository ?? 'see package metadata'}`, '',
- ...entry.files.flatMap(name => [`--- ${name} ---`, fs.readFileSync(path.join(entry.directory, name), 'utf8'), ''])].join('\n')
- fs.writeFileSync(path.join(licenses, filename), text)
- rows.push(`| ${entry.name} | ${entry.version} | ${entry.license} | [Original notices](licenses/${filename}) |`)
- }
- // Only previously generated regular notice files in this exact directory.
- for (const name of fs.readdirSync(licenses)) {
- if (/--[\d][\w.+-]*\.txt$/.test(name) && !expected.has(name) && fs.lstatSync(path.join(licenses, name)).isFile()) fs.unlinkSync(path.join(licenses, name))
- }
- fs.writeFileSync(path.join(packageRoot, 'THIRD_PARTY_NOTICES.md'), [
- '# Third-party notices', '',
- 'Generated from the Host and Client build inputs. Original copyright notices and license texts are preserved in `licenses/`.',
- 'The application is GPL-3.0-only; these components retain their own licenses. For DOMPurify, this distribution elects the Apache-2.0 option.',
- 'Shared DSH/React peers are provided by the host and are not included in this bundle. Their exact versions are recorded in package.json.', '',
- '| Component | Version | License | Text |', '| --- | --- | --- | --- |', ...rows, '',
- 'The release includes source materials and a manifest for these bundled components. Build from the corresponding public source tag with the lockfile.', '',
- ].join('\n'))
- fs.writeFileSync(path.join(packageRoot, 'lib', 'third-party-inputs.json'), JSON.stringify(entries, null, 2) + '\n')
- console.log(`[licenses] ${path.basename(packageRoot)}: ${entries.length} bundled components`)
- }
|