Skip to content

Commit d5ffa04

Browse files
committed
start to refactor for the sake of releaseNotes
1 parent 34d96b3 commit d5ffa04

File tree

2 files changed

+166
-111
lines changed

2 files changed

+166
-111
lines changed

tools/npmono/src/cli.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as trpcCli from 'trpc-cli'
2-
import {publish, PublishInput} from './publish'
2+
import {releaseNotes, ReleaseNotesInput, publish, PublishInput} from './publish'
33

44
const t = trpcCli.trpcServer.initTRPC.meta<trpcCli.TrpcCliMeta>().create()
55

@@ -9,6 +9,14 @@ const router = t.router({
99
.mutation(async ({input}) => {
1010
return publish(input)
1111
}),
12+
13+
...(process.env.TEST_RELEASE_NOTES && {
14+
releaseNotes: t.procedure
15+
.input(ReleaseNotesInput) //
16+
.mutation(async ({input}) => {
17+
return releaseNotes(input)
18+
}),
19+
}),
1220
})
1321

1422
const cli = trpcCli.createCli({

tools/npmono/src/publish.ts

Lines changed: 157 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {ListrEnquirerPromptAdapter} from '@listr2/prompt-adapter-enquirer'
22
import {Options, execa} from '@rebundled/execa'
33
import findUp from 'find-up'
44
import * as fs from 'fs'
5-
import {Listr, ListrTaskWrapper} from 'listr2'
5+
import {Listr, ListrTask, ListrTaskWrapper} from 'listr2'
66
import * as path from 'path'
77
import * as semver from 'semver'
88
import {z} from 'trpc-cli'
@@ -18,116 +18,9 @@ export type PublishInput = z.infer<typeof PublishInput>
1818

1919
export const publish = async (input: PublishInput) => {
2020
const {sortPackageJson} = await import('sort-package-json')
21-
const monorepoRoot = path.dirname(findUpOrThrow('pnpm-workspace.yaml'))
22-
process.chdir(monorepoRoot)
2321
const tasks = new Listr(
2422
[
25-
{
26-
title: 'Building',
27-
task: async (_ctx, task) => pipeExeca(task, 'pnpm', ['-w', 'build']),
28-
},
29-
{
30-
title: 'Get temp directory',
31-
rendererOptions: {persistentOutput: true},
32-
task: async (ctx, task) => {
33-
const list = await execa('pnpm', ['list', '--json', '--depth', '0', '--filter', '.'])
34-
const pkgName = JSON.parse(list.stdout)?.[0]?.name as string | undefined
35-
if (!pkgName) throw new Error(`Couldn't get package name from pnpm list output: ${list.stdout}`)
36-
ctx.tempDir = path.join('/tmp/npmono', pkgName, Date.now().toString())
37-
task.output = ctx.tempDir
38-
fs.mkdirSync(ctx.tempDir, {recursive: true})
39-
},
40-
},
41-
{
42-
title: 'Collecting packages',
43-
rendererOptions: {persistentOutput: true},
44-
task: async (ctx, task) => {
45-
const list = await execa('pnpm', [
46-
'list',
47-
'--json',
48-
'--recursive',
49-
'--only-projects',
50-
'--prod',
51-
'--filter',
52-
'./packages/*',
53-
])
54-
55-
ctx.packages = JSON.parse(list.stdout) as never
56-
ctx.packages = ctx.packages.filter(pkg => !pkg.private)
57-
58-
const pwdsCommand = await execa('pnpm', ['recursive', 'exec', 'pwd']) // use `pnpm recursive exec` to get the correct topological sort order // https://github.com/pnpm/pnpm/issues/7716
59-
const pwds = pwdsCommand.stdout
60-
.split('\n')
61-
.map(s => s.trim())
62-
.filter(Boolean)
63-
64-
ctx.packages
65-
.sort((a, b) => a.name.localeCompare(b.name)) // sort alphabetically first, as a tiebreaker (`.sort` is stable)
66-
.sort((a, b) => pwds.indexOf(a.path) - pwds.indexOf(b.path)) // then topologically
67-
68-
ctx.packages.forEach((pkg, i, {length}) => {
69-
const number = Number(`1${'0'.repeat(length.toString().length + 1)}`) + i
70-
pkg.folder = path.join(ctx.tempDir, `${number}.${pkg.name.replace('/', '__')}`)
71-
})
72-
task.output = ctx.packages.map(pkg => `${pkg.name}`).join('\n')
73-
return `Found ${ctx.packages.length} packages to publish`
74-
},
75-
},
76-
{
77-
title: `Writing local packages`,
78-
task: (ctx, task) => {
79-
return task.newListr(
80-
ctx.packages.map(pkg => ({
81-
title: `Packing ${pkg.name}`,
82-
task: async (_ctx, subtask) => {
83-
const localFolder = path.join(pkg.folder, 'local')
84-
await pipeExeca(subtask, 'pnpm', ['pack', '--pack-destination', localFolder], {cwd: pkg.path})
85-
86-
const tgzFileName = fs.readdirSync(localFolder).at(0)!
87-
await pipeExeca(subtask, 'tar', ['-xvzf', tgzFileName], {cwd: localFolder})
88-
},
89-
})),
90-
{concurrent: true},
91-
)
92-
},
93-
},
94-
{
95-
title: `Writing registry packages`,
96-
task: (ctx, task) => {
97-
return task.newListr(
98-
ctx.packages.map(pkg => ({
99-
title: `Pulling ${pkg.name}`,
100-
task: async (_ctx, subtask) => {
101-
const registryFolder = path.join(pkg.folder, 'registry')
102-
fs.mkdirSync(registryFolder, {recursive: true})
103-
// note: `npm pack foobar` will actually pull foobar.1-2-3.tgz from the registry. It's not actually doing a "pack" at all. `pnpm pack` does not do the same thing - it packs the local directory
104-
await pipeExeca(subtask, 'npm', ['pack', pkg.name], {
105-
reject: false,
106-
cwd: registryFolder,
107-
})
108-
109-
const tgzFileName = fs.readdirSync(registryFolder).at(0)
110-
if (!tgzFileName) {
111-
return
112-
}
113-
114-
await pipeExeca(subtask, 'tar', ['-xvzf', tgzFileName], {cwd: registryFolder})
115-
116-
const registryPackageJson = loadRegistryPackageJson(pkg)
117-
if (registryPackageJson) {
118-
const registryPackageJsonPath = packageJsonFilepath(pkg, 'registry')
119-
// avoid churn on package.json field ordering, which npm seems to mess with
120-
fs.writeFileSync(
121-
registryPackageJsonPath,
122-
sortPackageJson(JSON.stringify(registryPackageJson, null, 2)),
123-
)
124-
}
125-
},
126-
})),
127-
{concurrent: true},
128-
)
129-
},
130-
},
23+
...setupContextTasks,
13124
{
13225
title: 'Get version strategy',
13326
rendererOptions: {persistentOutput: true},
@@ -351,6 +244,159 @@ export const publish = async (input: PublishInput) => {
351244
await tasks.run()
352245
}
353246

247+
export const ReleaseNotesInput = z.object({
248+
baseComparisonSha: z.string().optional(),
249+
})
250+
export type ReleaseNotesInput = z.infer<typeof ReleaseNotesInput>
251+
252+
// this doesn't work yet
253+
export const releaseNotes = async (input: ReleaseNotesInput) => {
254+
const tasks = new Listr(
255+
[
256+
...setupContextTasks,
257+
{
258+
title: 'Generate release notes',
259+
task: async (ctx, task) => {
260+
for (const pkg of ctx.packages) {
261+
pkg.baseComparisonSha = input.baseComparisonSha
262+
const body = await getOrCreateChangelog(ctx, pkg)
263+
const title = `${pkg.name}@${pkg.version}`
264+
const message = `👇👇👇${title} changelog👇👇👇\n\n${body}\n\n👆👆👆${title} changelog👆👆👆`
265+
const doRelease = await task.prompt(ListrEnquirerPromptAdapter).run<boolean>({
266+
type: 'confirm',
267+
message: message + '\n\nDraft relesae?',
268+
initial: false,
269+
})
270+
if (doRelease) {
271+
const releaseParams = {title, body}
272+
await execa('open', [
273+
`https://github.com/mmkal/pgkit/releases/new?${new URLSearchParams(releaseParams).toString()}`,
274+
])
275+
}
276+
}
277+
},
278+
},
279+
],
280+
{ctx: {} as Ctx},
281+
)
282+
283+
await tasks.run()
284+
}
285+
286+
export const setupContextTasks: ListrTask<Ctx>[] = [
287+
{
288+
title: 'Set working directory',
289+
task: async () => {
290+
const monorepoRoot = path.dirname(findUpOrThrow('pnpm-workspace.yaml'))
291+
process.chdir(monorepoRoot)
292+
},
293+
},
294+
{
295+
title: 'Building',
296+
task: async (_ctx, task) => pipeExeca(task, 'pnpm', ['-w', 'build']),
297+
},
298+
{
299+
title: 'Get temp directory',
300+
rendererOptions: {persistentOutput: true},
301+
task: async (ctx, task) => {
302+
const list = await execa('pnpm', ['list', '--json', '--depth', '0', '--filter', '.'])
303+
const pkgName = JSON.parse(list.stdout)?.[0]?.name as string | undefined
304+
if (!pkgName) throw new Error(`Couldn't get package name from pnpm list output: ${list.stdout}`)
305+
ctx.tempDir = path.join('/tmp/npmono', pkgName, Date.now().toString())
306+
task.output = ctx.tempDir
307+
fs.mkdirSync(ctx.tempDir, {recursive: true})
308+
},
309+
},
310+
{
311+
title: 'Collecting packages',
312+
rendererOptions: {persistentOutput: true},
313+
task: async (ctx, task) => {
314+
const list = await execa('pnpm', [
315+
'list',
316+
'--json',
317+
'--recursive',
318+
'--only-projects',
319+
'--prod',
320+
'--filter',
321+
'./packages/*',
322+
])
323+
324+
ctx.packages = JSON.parse(list.stdout) as never
325+
ctx.packages = ctx.packages.filter(pkg => !pkg.private)
326+
327+
const pwdsCommand = await execa('pnpm', ['recursive', 'exec', 'pwd']) // use `pnpm recursive exec` to get the correct topological sort order // https://github.com/pnpm/pnpm/issues/7716
328+
const pwds = pwdsCommand.stdout
329+
.split('\n')
330+
.map(s => s.trim())
331+
.filter(Boolean)
332+
333+
ctx.packages
334+
.sort((a, b) => a.name.localeCompare(b.name)) // sort alphabetically first, as a tiebreaker (`.sort` is stable)
335+
.sort((a, b) => pwds.indexOf(a.path) - pwds.indexOf(b.path)) // then topologically
336+
337+
ctx.packages.forEach((pkg, i, {length}) => {
338+
const number = Number(`1${'0'.repeat(length.toString().length + 1)}`) + i
339+
pkg.folder = path.join(ctx.tempDir, `${number}.${pkg.name.replace('/', '__')}`)
340+
})
341+
task.output = ctx.packages.map(pkg => `${pkg.name}`).join('\n')
342+
return `Found ${ctx.packages.length} packages to publish`
343+
},
344+
},
345+
{
346+
title: `Writing local packages`,
347+
task: (ctx, task) => {
348+
return task.newListr(
349+
ctx.packages.map(pkg => ({
350+
title: `Packing ${pkg.name}`,
351+
task: async (_ctx, subtask) => {
352+
const localFolder = path.join(pkg.folder, 'local')
353+
await pipeExeca(subtask, 'pnpm', ['pack', '--pack-destination', localFolder], {cwd: pkg.path})
354+
355+
const tgzFileName = fs.readdirSync(localFolder).at(0)!
356+
await pipeExeca(subtask, 'tar', ['-xvzf', tgzFileName], {cwd: localFolder})
357+
},
358+
})),
359+
{concurrent: true},
360+
)
361+
},
362+
},
363+
{
364+
title: `Writing registry packages`,
365+
task: (ctx, task) => {
366+
return task.newListr(
367+
ctx.packages.map(pkg => ({
368+
title: `Pulling ${pkg.name}`,
369+
task: async (_ctx, subtask) => {
370+
const {sortPackageJson} = await import('sort-package-json')
371+
const registryFolder = path.join(pkg.folder, 'registry')
372+
fs.mkdirSync(registryFolder, {recursive: true})
373+
// note: `npm pack foobar` will actually pull foobar.1-2-3.tgz from the registry. It's not actually doing a "pack" at all. `pnpm pack` does not do the same thing - it packs the local directory
374+
await pipeExeca(subtask, 'npm', ['pack', pkg.name], {
375+
reject: false,
376+
cwd: registryFolder,
377+
})
378+
379+
const tgzFileName = fs.readdirSync(registryFolder).at(0)
380+
if (!tgzFileName) {
381+
return
382+
}
383+
384+
await pipeExeca(subtask, 'tar', ['-xvzf', tgzFileName], {cwd: registryFolder})
385+
386+
const registryPackageJson = loadRegistryPackageJson(pkg)
387+
if (registryPackageJson) {
388+
const registryPackageJsonPath = packageJsonFilepath(pkg, 'registry')
389+
// avoid churn on package.json field ordering, which npm seems to mess with
390+
fs.writeFileSync(registryPackageJsonPath, sortPackageJson(JSON.stringify(registryPackageJson, null, 2)))
391+
}
392+
},
393+
})),
394+
{concurrent: true},
395+
)
396+
},
397+
},
398+
]
399+
354400
const packageJsonFilepath = (pkg: PkgMeta, type: 'local' | 'registry') =>
355401
path.join(pkg.folder, type, 'package', 'package.json')
356402

@@ -392,7 +438,7 @@ const bumpChoices = (oldVersion: string) => {
392438

393439
/** Pessimistic comparison ref. Tries to use the registry package.json's `git.sha` property, and uses the first ever commit to the package folder if that can't be found. */
394440
async function getPackageLastPublishRef(pkg: Pkg) {
395-
const registryRef = loadRegistryPackageJson(pkg)?.git?.sha
441+
const registryRef = pkg.baseComparisonSha || loadRegistryPackageJson(pkg)?.git?.sha
396442
if (registryRef) return registryRef
397443

398444
const {stdout: firstRef} = await execa('git', ['log', '--reverse', '-n', '1', '--pretty=format:%h', '--', '.'], {
@@ -633,6 +679,7 @@ type PkgMeta = {
633679
folder: string
634680
lastPublished: PackageJson | null
635681
targetVersion: string | null
682+
baseComparisonSha: string | undefined
636683
}
637684

638685
// eslint-disable-next-line @typescript-eslint/no-explicit-any

0 commit comments

Comments
 (0)