Files
upstream-tldraw/internal/scripts/lib/didAnyPackageChange.ts
upstream-mirror a774ac673a
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
2026-09-23 16:12:52 +05:00

184 lines
6.0 KiB
TypeScript

import assert from 'assert'
import { writeFileSync } from 'fs'
import * as diff from 'diff'
import kleur from 'kleur'
import * as tar from 'tar'
import tmp from 'tmp'
import { exec } from './exec'
import { nicelog } from './nicelog'
import { PackageDetails, getAllPackageDetails } from './publishing'
async function getPackageFirstDiff(
pkg: PackageDetails,
publishedVersion: string
): Promise<Diff | null> {
assert(process.env.TLDRAW_BEMO_URL, 'TLDRAW_BEMO_URL env var must be set')
const dir = tmp.dirSync({ unsafeCleanup: true })
const dirPath = dir.name
try {
const unscopedName = pkg.name.replace('@tldraw/', '')
const url = `https://registry.npmjs.org/${pkg.name}/-/${unscopedName}-${publishedVersion}.tgz`
const res = await fetch(url)
if (res.status === 404) {
// The package is not published at this version. This happens when a
// new package was added to the monorepo since the last release, or
// when a previous publish was interrupted after the version-bump
// commit landed but before npm publish completed. In either case
// treat it as an added package so the release proceeds instead of
// failing the entire workflow.
nicelog(
`No published tarball for ${pkg.name}@${publishedVersion} (${url}); treating as added.`
)
return { type: 'added', packageName: pkg.name, filePath: pkg.name }
}
if (res.status >= 400) {
throw new Error(`Failed to fetch ${url}: ${res.status}`)
}
const publishedTarballPath = `${dirPath}/published-package.tgz`
writeFileSync(publishedTarballPath, Buffer.from(await res.arrayBuffer()))
const publishedManifest = getTarballManifestSync(publishedTarballPath)
const localTarballPath = `${dirPath}/local-package.tgz`
await exec('yarn', ['pack', '--out', localTarballPath], { pwd: pkg.dir, env: process.env })
const localManifest = getTarballManifestSync(localTarballPath)
return getManifestFirstDiff(pkg.name, publishedManifest, localManifest)
} finally {
dir.removeCallback()
}
}
type Diff =
| {
type: 'added' | 'removed'
packageName: string
filePath: string
}
| {
type: 'changed'
packageName: string
filePath: string
diff: string
}
// In the `tldraw` package these files are generated during publishing from the
// docs content in `apps/docs/content` (see `generate-tldraw-package-docs.ts`),
// not from SDK source. A docs-only change therefore alters them, which would
// otherwise make this diff check report a package change and cut a spurious SDK
// patch release. Ignore them for the `tldraw` package so that docs-only patches
// don't trigger a new SDK version. Other packages ship a hand-authored DOCS.md
// as real source, so we only skip these for `tldraw`.
//
// `generate-tldraw-package-docs.ts` writes these files to the package root, so
// in the tarball they live at exactly `package/<name>` (tarball entry paths are
// prefixed with `package/`). Match the full path rather than the basename so a
// same-named file in some other folder still counts as a real change.
const GENERATED_DOCS_FILES = new Set(['package/DOCS.md', 'package/RELEASE_NOTES.md'])
const GENERATED_DOCS_PACKAGE = 'tldraw'
function isGeneratedDocsFile(packageName: string, filePath: string) {
if (packageName !== GENERATED_DOCS_PACKAGE) return false
return GENERATED_DOCS_FILES.has(filePath)
}
function getManifestFirstDiff(
packageName: string,
a: Record<string, Buffer>,
b: Record<string, Buffer>
): Diff | null {
const aKeys = Object.keys(a).filter((key) => !isGeneratedDocsFile(packageName, key))
const bKeys = Object.keys(b).filter((key) => !isGeneratedDocsFile(packageName, key))
for (const key of aKeys) {
if (!bKeys.includes(key)) {
return { type: 'removed', packageName, filePath: key }
}
if (!a[key].equals(b[key])) {
return {
type: 'changed',
packageName,
filePath: key,
diff: diff.createTwoFilesPatch(key, key, a[key].toString(), b[key].toString()),
}
}
}
for (const key of bKeys) {
if (!aKeys.includes(key)) {
return { type: 'added', packageName, filePath: key }
}
}
return null
}
function getTarballManifestSync(tarballPath: string) {
const manifest: Record<string, Buffer> = {}
tar.list({
file: tarballPath,
onentry: (entry) => {
entry.on('data', (data) => {
// we could hash these to reduce memory but it's probably fine
const existing = manifest[entry.path]
if (existing) {
manifest[entry.path] = Buffer.concat([existing, data])
} else {
manifest[entry.path] = data
}
})
},
sync: true,
})
return manifest
}
export function formatDiff(diff: Diff) {
let message = kleur.cyan().bold(diff.packageName) + ' changed:'
switch (diff.type) {
case 'added':
message += `\n New file added ${kleur.green().bold(diff.filePath)}`
break
case 'removed':
message += `\n File removed ${kleur.red().bold(diff.filePath)}`
break
case 'changed':
message += `\n File changed ${kleur.yellow().bold(diff.filePath)}\n\n`
message += diff.diff
.split('\n')
.map((line) => {
if (line.startsWith('+')) {
return kleur.green(line)
} else if (line.startsWith('-')) {
return kleur.red(line)
} else {
return line
}
})
.join('\n')
break
}
return message
}
/**
* Compare each local package against the published tarball at
* `publishedVersion` on npm and return the first detected difference, or null
* if every package is identical to its published counterpart.
*
* Callers should pass the latest version that's actually published on npm
* (e.g. from `npm show <pkg> version`) rather than the version in the local
* `package.json`. The local version can drift ahead of npm if a previous
* publish was interrupted after the version-bump commit was pushed; using the
* known-published version keeps the diff check robust to that state.
*/
export async function getAnyPackageDiff(publishedVersion: string): Promise<Diff | null> {
const details = await getAllPackageDetails()
for (const pkg of Object.values(details)) {
const diff = await getPackageFirstDiff(pkg, publishedVersion)
if (diff) {
return diff
}
}
return null
}