notices.mjs 4.0 KB

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