clone

a browser-first diagram editor with live svg previews, local drafts and encrypted, accountless sharing

main
diagramzip / renderers / edge / scripts / check-runtime-licenses.mjs
check-runtime-licenses.mjs 7.0K
1import { createHash } from 'node:crypto'2import { execFileSync } from 'node:child_process'3import { mkdtemp, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises'4import { tmpdir } from 'node:os'5import { basename, join } from 'node:path'6import { fileURLToPath } from 'node:url'78const root = fileURLToPath(new URL('../', import.meta.url))9const configs = fileURLToPath(new URL('../config/units/', import.meta.url))10const output = fileURLToPath(new URL('../artifacts/licenses/npm-runtime-NOTICE.md', import.meta.url))11const lock = JSON.parse(await readFile(join(root, 'package-lock.json'), 'utf8'))12const wrangler = join(root, 'node_modules/.bin/wrangler')13const write = process.argv.includes('--write')1415const iscFallbacks = new Map([16 ['boolbase', 'Copyright (c) Felix Boehm <me@feedic.com>'],17 ['saxes', 'Copyright (c) Louis-Dominique Dubeau <ldd@lddubeau.com>'],18])19const repositoryLicenseFallbacks = new Map([20 ['@excalidraw/excalidraw', join(root, '../../licenses/excalidraw-license.txt')],21])22const licenseMetadataFallbacks = new Map([23 ['khroma', 'MIT'],24])2526function iscText(copyright) {27 return `${copyright}\n\nPermission to use, copy, modify, and/or distribute this software for any\npurpose with or without fee is hereby granted, provided that the above\ncopyright notice and this permission notice appear in all copies.\n\nTHE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES\nWITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\nMERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR\nANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\nWHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\nACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF\nOR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.`28}2930function packageName(input) {31 return input.match(/node_modules\/(?:@[^/]+\/[^/]+|[^/]+)/)?.[0].slice('node_modules/'.length)32}3334function normalizeLegalText(text) {35 return text.replace(/\r\n?/g, '\n').split('\n').map(line => line.trimEnd()).join('\n').trim()36}3738async function legalText(name, license) {39 const directory = join(root, 'node_modules', name)40 const files = (await readdir(directory))41 .filter(file => /^(?:licen[cs]e|copying|notice)(?:[._-]|$)/i.test(file))42 .sort((left, right) => left.localeCompare(right))43 if (files.length) {44 const sections = []45 for (const file of files) sections.push(normalizeLegalText(await readFile(join(directory, file), 'utf8')))46 return sections.join('\n\n')47 }48 const repositoryLicense = repositoryLicenseFallbacks.get(name)49 if (repositoryLicense) return normalizeLegalText(await readFile(repositoryLicense, 'utf8'))50 const copyright = iscFallbacks.get(name)51 if (copyright) return iscText(copyright)52 if (license === 'MIT') {53 return `Copyright (c) contributors to ${name}\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the "Software"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.`54 }55 throw new Error(`Runtime package ${name} does not publish a license or notice file.`)56}5758const temporary = await mkdtemp(join(tmpdir(), 'diagramzip-runtime-licenses-'))59const units = new Map()60try {61 for (const config of (await readdir(configs)).filter(file => file.endsWith('.jsonc')).sort()) {62 const unit = basename(config, '.jsonc')63 const directory = join(temporary, unit)64 await mkdir(directory)65 execFileSync(wrangler, [66 'deploy', '--dry-run', '--config', join(configs, config),67 '--outdir', directory, '--metafile',68 ], { cwd: root, stdio: 'pipe' })69 const meta = JSON.parse(await readFile(join(directory, 'bundle-meta.json'), 'utf8'))70 const packages = new Set(Object.keys(meta.inputs).map(packageName).filter(Boolean))71 units.set(unit, [...packages].sort())72 }73} finally {74 await rm(temporary, { recursive: true, force: true })75}7677const usedBy = new Map()78for (const [unit, packages] of units) {79 for (const name of packages) {80 const consumers = usedBy.get(name) ?? []81 consumers.push(unit)82 usedBy.set(name, consumers)83 }84}8586const packages = []87for (const name of [...usedBy.keys()].sort()) {88 const metadata = lock.packages[`node_modules/${name}`]89 const installed = JSON.parse(await readFile(join(root, 'node_modules', name, 'package.json'), 'utf8'))90 const license = metadata?.license ?? installed.license ?? installed.licenses?.[0]?.type ?? licenseMetadataFallbacks.get(name)91 if (!metadata?.version || !license) throw new Error(`Runtime package ${name} lacks locked version or license metadata.`)92 packages.push({ name, version: metadata.version, license, units: usedBy.get(name), text: await legalText(name, license) })93}9495const groups = new Map()96for (const dependency of packages) {97 const digest = createHash('sha256').update(dependency.text).digest('hex')98 const group = groups.get(digest) ?? { text: dependency.text, packages: [] }99 group.packages.push(`${dependency.name}@${dependency.version}`)100 groups.set(digest, group)101}102103const lines = [104 '# Edge Worker npm runtime notices',105 '',106 'Generated from Wrangler deploy metafiles for `config/units/*.jsonc`. Build, test,',107 'and deployment-tool dependencies that are absent from every emitted Worker bundle',108 'are intentionally excluded. WebAssembly and separately vendored artifacts are',109 'documented by their own notices and the repository-level component manifest.',110 '',111 '| Package | Version | License | Emitted Worker units |',112 '| --- | --- | --- | --- |',113]114for (const dependency of packages) lines.push(`| ${dependency.name} | ${dependency.version} | ${dependency.license} | ${dependency.units.join(', ')} |`)115lines.push('', '## License and notice texts', '')116for (const group of [...groups.values()].sort((left, right) => left.packages[0].localeCompare(right.packages[0]))) {117 lines.push(`### ${group.packages.join(', ')}`, '', '```text', group.text, '```', '')118}119const generated = `${lines.join('\n').trim()}\n`120121if (write) {122 await writeFile(output, generated)123} else {124 const current = await readFile(output, 'utf8').catch(() => '')125 if (current !== generated) throw new Error('Runtime license notice is stale. Run npm run generate:licenses.')126}