2023-04-25 11:01:25 +00:00
|
|
|
import { execSync } from 'child_process'
|
|
|
|
import { fetch } from 'cross-fetch'
|
2023-09-26 15:36:32 +00:00
|
|
|
import { existsSync, readFileSync, readdirSync, writeFileSync } from 'fs'
|
2023-05-04 09:25:31 +00:00
|
|
|
import path, { join } from 'path'
|
2023-04-25 11:01:25 +00:00
|
|
|
import { compare, parse } from 'semver'
|
2023-09-26 15:36:32 +00:00
|
|
|
import { exec } from './exec'
|
2024-01-16 14:38:05 +00:00
|
|
|
import { REPO_ROOT } from './file'
|
2023-06-01 18:01:49 +00:00
|
|
|
import { nicelog } from './nicelog'
|
2024-02-29 16:06:19 +00:00
|
|
|
import { getAllWorkspacePackages } from './workspace'
|
2023-04-25 11:01:25 +00:00
|
|
|
|
|
|
|
export type PackageDetails = {
|
|
|
|
name: string
|
|
|
|
dir: string
|
|
|
|
localDeps: string[]
|
|
|
|
version: string
|
|
|
|
}
|
|
|
|
|
2024-02-29 16:06:19 +00:00
|
|
|
async function getPackageDetails(dir: string): Promise<PackageDetails | null> {
|
2023-04-25 11:01:25 +00:00
|
|
|
const packageJsonPath = path.join(dir, 'package.json')
|
|
|
|
if (!existsSync(packageJsonPath)) {
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
const packageJson = JSON.parse(readFileSync(path.join(dir, 'package.json'), 'utf8'))
|
|
|
|
if (packageJson.private) {
|
|
|
|
return null
|
|
|
|
}
|
2024-02-29 16:06:19 +00:00
|
|
|
|
|
|
|
const workspacePackages = await getAllWorkspacePackages()
|
2023-04-25 11:01:25 +00:00
|
|
|
return {
|
|
|
|
name: packageJson.name,
|
|
|
|
dir,
|
|
|
|
version: packageJson.version,
|
|
|
|
localDeps: Object.keys(packageJson.dependencies ?? {}).filter((dep) =>
|
2024-02-29 16:06:19 +00:00
|
|
|
workspacePackages.some((p) => p.name === dep)
|
2023-04-25 11:01:25 +00:00
|
|
|
),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-29 16:06:19 +00:00
|
|
|
export async function getAllPackageDetails(): Promise<Record<string, PackageDetails>> {
|
2024-01-16 14:38:05 +00:00
|
|
|
const dirs = readdirSync(join(REPO_ROOT, 'packages'))
|
2024-02-29 16:06:19 +00:00
|
|
|
const details = await Promise.all(
|
|
|
|
dirs.map((dir) => getPackageDetails(path.join(REPO_ROOT, 'packages', dir)))
|
|
|
|
)
|
|
|
|
const results = details.filter((x): x is PackageDetails => Boolean(x))
|
2023-04-25 11:01:25 +00:00
|
|
|
|
|
|
|
return Object.fromEntries(results.map((result) => [result.name, result]))
|
|
|
|
}
|
|
|
|
|
2024-02-29 16:06:19 +00:00
|
|
|
export async function setAllVersions(version: string) {
|
|
|
|
const packages = await getAllPackageDetails()
|
2023-04-25 11:01:25 +00:00
|
|
|
for (const packageDetails of Object.values(packages)) {
|
|
|
|
const manifest = JSON.parse(readFileSync(path.join(packageDetails.dir, 'package.json'), 'utf8'))
|
|
|
|
manifest.version = version
|
|
|
|
writeFileSync(
|
|
|
|
path.join(packageDetails.dir, 'package.json'),
|
|
|
|
JSON.stringify(manifest, null, '\t') + '\n'
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2024-02-29 18:09:30 +00:00
|
|
|
await exec('yarn', ['refresh-assets', '--force'], { env: { ALLOW_REFRESH_ASSETS_CHANGES: '1' } })
|
2024-02-29 17:12:40 +00:00
|
|
|
|
2023-04-25 11:01:25 +00:00
|
|
|
const lernaJson = JSON.parse(readFileSync('lerna.json', 'utf8'))
|
|
|
|
lernaJson.version = version
|
|
|
|
writeFileSync('lerna.json', JSON.stringify(lernaJson, null, '\t') + '\n')
|
|
|
|
|
|
|
|
execSync('yarn')
|
|
|
|
}
|
|
|
|
|
2024-02-29 16:28:11 +00:00
|
|
|
export async function getLatestVersion() {
|
|
|
|
const packages = await getAllPackageDetails()
|
2023-04-25 11:01:25 +00:00
|
|
|
|
|
|
|
const allVersions = Object.values(packages).map((p) => parse(p.version)!)
|
|
|
|
allVersions.sort(compare)
|
|
|
|
|
|
|
|
const latestVersion = allVersions[allVersions.length - 1]
|
|
|
|
|
|
|
|
if (!latestVersion) {
|
|
|
|
throw new Error('Could not find latest version')
|
|
|
|
}
|
|
|
|
|
|
|
|
return latestVersion
|
|
|
|
}
|
|
|
|
|
|
|
|
function topologicalSortPackages(packages: Record<string, PackageDetails>) {
|
|
|
|
const sorted: PackageDetails[] = []
|
|
|
|
const visited = new Set<string>()
|
|
|
|
|
|
|
|
function visit(packageName: string, path: string[]) {
|
|
|
|
if (visited.has(packageName)) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
visited.add(packageName)
|
|
|
|
const packageDetails = packages[packageName]
|
|
|
|
if (!packageDetails) {
|
|
|
|
throw new Error(`Could not find package ${packageName}. path: ${path.join(' -> ')}`)
|
|
|
|
}
|
|
|
|
packageDetails.localDeps.forEach((dep) => visit(dep, [...path, dep]))
|
|
|
|
sorted.push(packageDetails)
|
|
|
|
}
|
|
|
|
|
|
|
|
Object.keys(packages).forEach((packageName) => visit(packageName, [packageName]))
|
|
|
|
|
|
|
|
return sorted
|
|
|
|
}
|
|
|
|
|
2024-03-04 09:39:19 +00:00
|
|
|
export async function publish(distTag?: string) {
|
2023-04-25 11:01:25 +00:00
|
|
|
const npmToken = process.env.NPM_TOKEN
|
|
|
|
if (!npmToken) {
|
|
|
|
throw new Error('NPM_TOKEN not set')
|
|
|
|
}
|
|
|
|
|
|
|
|
execSync(`yarn config set npmAuthToken ${npmToken}`, { stdio: 'inherit' })
|
|
|
|
execSync(`yarn config set npmRegistryServer https://registry.npmjs.org`, { stdio: 'inherit' })
|
|
|
|
|
2024-02-29 16:06:19 +00:00
|
|
|
const packages = await getAllPackageDetails()
|
2023-04-25 11:01:25 +00:00
|
|
|
|
|
|
|
const publishOrder = topologicalSortPackages(packages)
|
|
|
|
|
|
|
|
for (const packageDetails of publishOrder) {
|
2024-03-04 09:39:19 +00:00
|
|
|
const tag = distTag ?? parse(packageDetails.version)?.prerelease[0] ?? 'latest'
|
2023-06-01 18:01:49 +00:00
|
|
|
nicelog(
|
2024-03-04 09:39:19 +00:00
|
|
|
`Publishing ${packageDetails.name} with version ${packageDetails.version} under tag @${tag}`
|
2023-04-25 11:01:25 +00:00
|
|
|
)
|
|
|
|
|
2023-09-26 15:36:32 +00:00
|
|
|
await retry(
|
|
|
|
async () => {
|
|
|
|
let output = ''
|
|
|
|
try {
|
2023-09-26 16:44:47 +00:00
|
|
|
await exec(
|
2023-09-26 15:36:32 +00:00
|
|
|
`yarn`,
|
2024-03-04 09:39:19 +00:00
|
|
|
['npm', 'publish', '--tag', String(tag), '--tolerate-republish', '--access', 'public'],
|
2023-09-26 15:36:32 +00:00
|
|
|
{
|
|
|
|
pwd: packageDetails.dir,
|
|
|
|
processStdoutLine: (line) => {
|
|
|
|
output += line + '\n'
|
|
|
|
nicelog(line)
|
|
|
|
},
|
|
|
|
processStderrLine: (line) => {
|
|
|
|
output += line + '\n'
|
|
|
|
nicelog(line)
|
|
|
|
},
|
|
|
|
}
|
|
|
|
)
|
|
|
|
} catch (e) {
|
|
|
|
if (output.includes('You cannot publish over the previously published versions')) {
|
|
|
|
// --tolerate-republish seems to not work for canary versions??? so let's just ignore this error
|
|
|
|
return
|
|
|
|
}
|
|
|
|
throw e
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{
|
|
|
|
delay: 10_000,
|
|
|
|
numAttempts: 5,
|
|
|
|
}
|
|
|
|
)
|
2023-04-25 11:01:25 +00:00
|
|
|
|
2023-09-26 15:36:32 +00:00
|
|
|
await retry(
|
|
|
|
async ({ attempt, total }) => {
|
|
|
|
nicelog('Waiting for package to be published... attempt', attempt, 'of', total)
|
2023-04-25 11:01:25 +00:00
|
|
|
// fetch the new package directly from the npm registry
|
|
|
|
const newVersion = packageDetails.version
|
|
|
|
const unscopedName = packageDetails.name.replace('@tldraw/', '')
|
|
|
|
|
2024-02-29 16:59:05 +00:00
|
|
|
const url = `https://registry.npmjs.org/${packageDetails.name}/-/${unscopedName}-${newVersion}.tgz`
|
2023-06-01 18:01:49 +00:00
|
|
|
nicelog('looking for package at url: ', url)
|
2023-04-25 11:01:25 +00:00
|
|
|
const res = await fetch(url, {
|
|
|
|
method: 'HEAD',
|
|
|
|
})
|
|
|
|
if (res.status >= 400) {
|
|
|
|
throw new Error(`Package not found: ${res.status}`)
|
|
|
|
}
|
2023-09-26 15:36:32 +00:00
|
|
|
},
|
|
|
|
{
|
2024-03-11 09:42:28 +00:00
|
|
|
delay: 4000,
|
|
|
|
numAttempts: 20,
|
2023-04-25 11:01:25 +00:00
|
|
|
}
|
2023-09-26 15:36:32 +00:00
|
|
|
)
|
2023-04-25 11:01:25 +00:00
|
|
|
}
|
|
|
|
}
|
2023-09-26 15:36:32 +00:00
|
|
|
|
|
|
|
function retry(
|
|
|
|
fn: (args: { attempt: number; remaining: number; total: number }) => Promise<void>,
|
|
|
|
opts: {
|
|
|
|
numAttempts: number
|
|
|
|
delay: number
|
|
|
|
}
|
|
|
|
): Promise<void> {
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
let attempts = 0
|
|
|
|
function attempt() {
|
|
|
|
fn({ attempt: attempts, remaining: opts.numAttempts - attempts, total: opts.numAttempts })
|
|
|
|
.then(resolve)
|
|
|
|
.catch((err) => {
|
|
|
|
attempts++
|
|
|
|
if (attempts >= opts.numAttempts) {
|
|
|
|
reject(err)
|
|
|
|
} else {
|
|
|
|
setTimeout(attempt, opts.delay)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
attempt()
|
|
|
|
})
|
|
|
|
}
|