Tldraw/scripts/lib/publishing.ts

223 wiersze
6.1 KiB
TypeScript
Czysty Zwykły widok Historia

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'
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'
import { REPO_ROOT } from './file'
import { nicelog } from './nicelog'
import { getAllWorkspacePackages } from './workspace'
2023-04-25 11:01:25 +00:00
export type PackageDetails = {
name: string
dir: string
localDeps: string[]
version: string
}
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
}
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) =>
workspacePackages.some((p) => p.name === dep)
2023-04-25 11:01:25 +00:00
),
}
}
export async function getAllPackageDetails(): Promise<Record<string, PackageDetails>> {
const dirs = readdirSync(join(REPO_ROOT, 'packages'))
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]))
}
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'
)
tldraw zero - package shuffle (#1710) This PR moves code between our packages so that: - @tldraw/editor is a “core” library with the engine and canvas but no shapes, tools, or other things - @tldraw/tldraw contains everything particular to the experience we’ve built for tldraw At first look, this might seem like a step away from customization and configuration, however I believe it greatly increases the configuration potential of the @tldraw/editor while also providing a more accurate reflection of what configuration options actually exist for @tldraw/tldraw. ## Library changes @tldraw/editor re-exports its dependencies and @tldraw/tldraw re-exports @tldraw/editor. - users of @tldraw/editor WITHOUT @tldraw/tldraw should almost always only import things from @tldraw/editor. - users of @tldraw/tldraw should almost always only import things from @tldraw/tldraw. - @tldraw/polyfills is merged into @tldraw/editor - @tldraw/indices is merged into @tldraw/editor - @tldraw/primitives is merged mostly into @tldraw/editor, partially into @tldraw/tldraw - @tldraw/file-format is merged into @tldraw/tldraw - @tldraw/ui is merged into @tldraw/tldraw Many (many) utils and other code is moved from the editor to tldraw. For example, embeds now are entirely an feature of @tldraw/tldraw. The only big chunk of code left in core is related to arrow handling. ## API Changes The editor can now be used without tldraw's assets. We load them in @tldraw/tldraw instead, so feel free to use whatever fonts or images or whatever that you like with the editor. All tools and shapes (except for the `Group` shape) are moved to @tldraw/tldraw. This includes the `select` tool. You should use the editor with at least one tool, however, so you now also need to send in an `initialState` prop to the Editor / <TldrawEditor> component indicating which state the editor should begin in. The `components` prop now also accepts `SelectionForeground`. The complex selection component that we use for tldraw is moved to @tldraw/tldraw. The default component is quite basic but can easily be replaced via the `components` prop. We pass down our tldraw-flavored SelectionFg via `components`. Likewise with the `Scribble` component: the `DefaultScribble` no longer uses our freehand tech and is a simple path instead. We pass down the tldraw-flavored scribble via `components`. The `ExternalContentManager` (`Editor.externalContentManager`) is removed and replaced with a mapping of types to handlers. - Register new content handlers with `Editor.registerExternalContentHandler`. - Register new asset creation handlers (for files and URLs) with `Editor.registerExternalAssetHandler` ### Change Type - [x] `major` — Breaking change ### Test Plan - [x] Unit Tests - [x] End to end tests ### Release Notes - [@tldraw/editor] lots, wip - [@tldraw/ui] gone, merged to tldraw/tldraw - [@tldraw/polyfills] gone, merged to tldraw/editor - [@tldraw/primitives] gone, merged to tldraw/editor / tldraw/tldraw - [@tldraw/indices] gone, merged to tldraw/editor - [@tldraw/file-format] gone, merged to tldraw/tldraw --------- Co-authored-by: alex <alex@dytry.ch>
2023-07-17 21:22:34 +00:00
if (manifest.name === '@tldraw/editor') {
const versionFileContents = `export const version = '${version}'\n`
writeFileSync(path.join(packageDetails.dir, 'src', 'version.ts'), versionFileContents)
}
if (manifest.name === 'tldraw') {
tldraw zero - package shuffle (#1710) This PR moves code between our packages so that: - @tldraw/editor is a “core” library with the engine and canvas but no shapes, tools, or other things - @tldraw/tldraw contains everything particular to the experience we’ve built for tldraw At first look, this might seem like a step away from customization and configuration, however I believe it greatly increases the configuration potential of the @tldraw/editor while also providing a more accurate reflection of what configuration options actually exist for @tldraw/tldraw. ## Library changes @tldraw/editor re-exports its dependencies and @tldraw/tldraw re-exports @tldraw/editor. - users of @tldraw/editor WITHOUT @tldraw/tldraw should almost always only import things from @tldraw/editor. - users of @tldraw/tldraw should almost always only import things from @tldraw/tldraw. - @tldraw/polyfills is merged into @tldraw/editor - @tldraw/indices is merged into @tldraw/editor - @tldraw/primitives is merged mostly into @tldraw/editor, partially into @tldraw/tldraw - @tldraw/file-format is merged into @tldraw/tldraw - @tldraw/ui is merged into @tldraw/tldraw Many (many) utils and other code is moved from the editor to tldraw. For example, embeds now are entirely an feature of @tldraw/tldraw. The only big chunk of code left in core is related to arrow handling. ## API Changes The editor can now be used without tldraw's assets. We load them in @tldraw/tldraw instead, so feel free to use whatever fonts or images or whatever that you like with the editor. All tools and shapes (except for the `Group` shape) are moved to @tldraw/tldraw. This includes the `select` tool. You should use the editor with at least one tool, however, so you now also need to send in an `initialState` prop to the Editor / <TldrawEditor> component indicating which state the editor should begin in. The `components` prop now also accepts `SelectionForeground`. The complex selection component that we use for tldraw is moved to @tldraw/tldraw. The default component is quite basic but can easily be replaced via the `components` prop. We pass down our tldraw-flavored SelectionFg via `components`. Likewise with the `Scribble` component: the `DefaultScribble` no longer uses our freehand tech and is a simple path instead. We pass down the tldraw-flavored scribble via `components`. The `ExternalContentManager` (`Editor.externalContentManager`) is removed and replaced with a mapping of types to handlers. - Register new content handlers with `Editor.registerExternalContentHandler`. - Register new asset creation handlers (for files and URLs) with `Editor.registerExternalAssetHandler` ### Change Type - [x] `major` — Breaking change ### Test Plan - [x] Unit Tests - [x] End to end tests ### Release Notes - [@tldraw/editor] lots, wip - [@tldraw/ui] gone, merged to tldraw/tldraw - [@tldraw/polyfills] gone, merged to tldraw/editor - [@tldraw/primitives] gone, merged to tldraw/editor / tldraw/tldraw - [@tldraw/indices] gone, merged to tldraw/editor - [@tldraw/file-format] gone, merged to tldraw/tldraw --------- Co-authored-by: alex <alex@dytry.ch>
2023-07-17 21:22:34 +00:00
const versionFileContents = `export const version = '${version}'\n`
writeFileSync(
path.join(packageDetails.dir, 'src', 'lib', 'ui', 'version.ts'),
versionFileContents
)
}
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')
}
export function getLatestVersion() {
const packages = getAllPackageDetails()
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
}
export async function publish() {
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' })
const packages = await getAllPackageDetails()
2023-04-25 11:01:25 +00:00
const publishOrder = topologicalSortPackages(packages)
for (const packageDetails of publishOrder) {
const prereleaseTag = parse(packageDetails.version)?.prerelease[0] ?? 'latest'
nicelog(
2023-04-25 11:01:25 +00:00
`Publishing ${packageDetails.name} with version ${packageDetails.version} under tag @${prereleaseTag}`
)
2023-09-26 15:36:32 +00:00
await retry(
async () => {
let output = ''
try {
await exec(
2023-09-26 15:36:32 +00:00
`yarn`,
[
'npm',
'publish',
'--tag',
String(prereleaseTag),
'--tolerate-republish',
'--access',
'public',
],
{
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/', '')
const url = `https://registry.npmjs.org/@tldraw/${unscopedName}/-/${unscopedName}-${newVersion}.tgz`
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
},
{
delay: 3000,
numAttempts: 10,
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()
})
}