@o/scripts 0.16.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/release.ts ADDED
@@ -0,0 +1,544 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { cmd } from './cmd'
4
+
5
+ // avoid emitter error
6
+ process.setMaxListeners(50)
7
+ process.stderr.setMaxListeners(50)
8
+ process.stdout.setMaxListeners(50)
9
+
10
+ await cmd`publish takeout packages to npm`
11
+ .args(
12
+ `--patch boolean --minor boolean --major boolean --canary boolean
13
+ --rerun boolean --republish boolean --finish boolean --skip-finish boolean
14
+ --dry-run boolean --skip-test boolean --skip-build boolean --skip-version boolean --skip-publish boolean
15
+ --dirty boolean --tamagui-git-user boolean
16
+ --undocumented boolean --skip-all boolean`
17
+ )
18
+ .run(async ({ args, $, run, path, os }) => {
19
+ const fs = (await import('fs-extra')).default
20
+ const { writeJSON } = await import('fs-extra')
21
+ const pMap = (await import('p-map')).default
22
+
23
+ // for failed publishes that need to re-run
24
+ const reRun = args.rerun
25
+ const rePublish = reRun || args.republish
26
+ const finish = args.finish
27
+ const skipAll = args.skipAll
28
+ const undocumented = args.undocumented
29
+ const skipFinish = args.skipFinish || skipAll || undocumented
30
+
31
+ const canary = args.canary
32
+ const skipVersion = finish || rePublish || args.skipVersion
33
+ const shouldMajor = args.major
34
+ const shouldMinor = args.minor
35
+ const shouldPatch = args.patch
36
+ const dirty = finish || undocumented || args.dirty
37
+ const skipTest =
38
+ finish ||
39
+ rePublish ||
40
+ skipAll ||
41
+ args.skipTest ||
42
+ process.argv.includes('--skip-tests')
43
+ const skipBuild = finish || rePublish || skipAll || args.skipBuild
44
+ const skipPublish = args.skipPublish
45
+ const dryRun = args.dryRun
46
+ const tamaguiGitUser = args.tamaguiGitUser
47
+ async function getWorkspacePackages() {
48
+ // read workspaces from root package.json
49
+ const rootPackageJson = await fs.readJSON(path.join(process.cwd(), 'package.json'))
50
+ const workspaceGlobs = rootPackageJson.workspaces || []
51
+
52
+ // resolve workspace paths
53
+ const packagePaths: { name: string; location: string }[] = []
54
+ for (const glob of workspaceGlobs) {
55
+ if (glob.includes('*')) {
56
+ // handle glob patterns like "./packages/*"
57
+ const baseDir = glob.replace('/*', '')
58
+ const fullPath = path.join(process.cwd(), baseDir)
59
+ if (await fs.pathExists(fullPath)) {
60
+ const dirs = await fs.readdir(fullPath)
61
+ for (const dir of dirs) {
62
+ const pkgPath = path.join(fullPath, dir, 'package.json')
63
+ if (await fs.pathExists(pkgPath)) {
64
+ const pkg = await fs.readJSON(pkgPath)
65
+ packagePaths.push({
66
+ name: pkg.name,
67
+ location: path.join(baseDir, dir),
68
+ })
69
+ }
70
+ }
71
+ }
72
+ } else {
73
+ // handle direct paths like "./src/start"
74
+ const pkgPath = path.join(process.cwd(), glob, 'package.json')
75
+ if (await fs.pathExists(pkgPath)) {
76
+ const pkg = await fs.readJSON(pkgPath)
77
+ packagePaths.push({
78
+ name: pkg.name,
79
+ location: glob,
80
+ })
81
+ }
82
+ }
83
+ }
84
+
85
+ return packagePaths
86
+ }
87
+
88
+ async function loadPackageJsons(packagePaths: { name: string; location: string }[]) {
89
+ const allPackageJsons = await Promise.all(
90
+ packagePaths
91
+ .filter((i) => i.location !== '.' && !i.name.startsWith('@takeout'))
92
+ .map(async ({ name, location }) => {
93
+ const cwd = path.join(process.cwd(), location)
94
+ const json = await fs.readJSON(path.join(cwd, 'package.json'))
95
+ return {
96
+ name,
97
+ cwd,
98
+ json,
99
+ path: path.join(cwd, 'package.json'),
100
+ directory: location,
101
+ }
102
+ })
103
+ )
104
+
105
+ const publishablePackages = allPackageJsons.filter(
106
+ (x) => !x.json.skipPublish && !x.json.private
107
+ )
108
+
109
+ return { allPackageJsons, publishablePackages }
110
+ }
111
+
112
+ // main release flow
113
+ const curVersion = fs.readJSONSync('./packages/helpers/package.json').version
114
+
115
+ // must specify version (unless republishing):
116
+ if (
117
+ !rePublish &&
118
+ !skipVersion &&
119
+ !canary &&
120
+ !shouldPatch &&
121
+ !shouldMinor &&
122
+ !shouldMajor
123
+ ) {
124
+ console.error(`Must specify one of --patch, --minor, or --major`)
125
+ process.exit(1)
126
+ }
127
+
128
+ const nextVersion = (() => {
129
+ if (rePublish || skipVersion) {
130
+ return curVersion
131
+ }
132
+
133
+ if (canary) {
134
+ return `${curVersion.replace(/(-\d+)+$/, '')}-${Date.now()}`
135
+ }
136
+
137
+ const curMajor = +curVersion.split('.')[0] || 0
138
+ const curMinor = +curVersion.split('.')[1] || 0
139
+ const patchAndCanary = curVersion.split('.')[2]
140
+ const [curPatch] = patchAndCanary.split('-')
141
+ const patchVersion = shouldPatch ? +curPatch + 1 : 0
142
+ const minorVersion = curMinor + (shouldMinor ? 1 : 0)
143
+ const majorVersion = curMajor + (shouldMajor ? 1 : 0)
144
+ const next = `${majorVersion}.${minorVersion}.${patchVersion}`
145
+
146
+ return next
147
+ })()
148
+
149
+ if (!skipVersion) {
150
+ console.info(` 🚀 Releasing:`)
151
+ console.info(' Current:', curVersion)
152
+ console.info(` Next: ${nextVersion}`)
153
+ }
154
+
155
+ let restorePackageJsons: (() => Promise<void>) | undefined
156
+ let publishSucceeded = false
157
+ const publishedNames: string[] = []
158
+
159
+ try {
160
+ const hasTrustedPublishingIdentity =
161
+ process.env.GITHUB_ACTIONS === 'true' &&
162
+ Boolean(process.env.ACTIONS_ID_TOKEN_REQUEST_URL) &&
163
+ Boolean(process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN)
164
+
165
+ if (!finish && !skipPublish && !dryRun && !hasTrustedPublishingIdentity) {
166
+ try {
167
+ await run(`npm whoami`, { silent: true, captureOutput: true })
168
+ } catch {
169
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
170
+ throw new Error(
171
+ 'npm authentication is required to publish. Run `npm login` in a terminal, then retry the release.'
172
+ )
173
+ }
174
+
175
+ console.info(
176
+ 'npm authentication is required. Complete the browser login to continue.'
177
+ )
178
+ await run(`npm login`, { interactive: true })
179
+
180
+ try {
181
+ await run(`npm whoami`, { silent: true, captureOutput: true })
182
+ } catch {
183
+ throw new Error(
184
+ 'npm authentication still failed after `npm login`. Verify `npm whoami` succeeds, then retry the release.'
185
+ )
186
+ }
187
+ }
188
+ }
189
+
190
+ // ensure we are up to date
191
+ // ensure we are on main
192
+ if (!canary && !process.env.CI) {
193
+ if ((await run(`git rev-parse --abbrev-ref HEAD`)).stdout.trim() !== 'main') {
194
+ throw new Error(`Not on main`)
195
+ }
196
+ if (!dirty && !rePublish && !finish) {
197
+ await run(`git pull --rebase origin main`)
198
+ }
199
+ }
200
+
201
+ const packagePaths = await getWorkspacePackages()
202
+ const { allPackageJsons, publishablePackages: packageJsons } =
203
+ await loadPackageJsons(packagePaths)
204
+ const originalPackageJsons = new Map<string, string>()
205
+ await Promise.all(
206
+ allPackageJsons.map(async ({ path: pkgPath }) => {
207
+ originalPackageJsons.set(pkgPath, await fs.readFile(pkgPath, 'utf8'))
208
+ })
209
+ )
210
+ restorePackageJsons = async () => {
211
+ await Promise.all(
212
+ [...originalPackageJsons].map(([pkgPath, contents]) =>
213
+ fs.writeFile(pkgPath, contents)
214
+ )
215
+ )
216
+ }
217
+
218
+ if (!finish) {
219
+ console.info(
220
+ `Publishing in order:\n\n${packageJsons.map((x) => x.name).join('\n')}`
221
+ )
222
+ }
223
+
224
+ async function checkDistDirs() {
225
+ await Promise.all(
226
+ packageJsons.map(async ({ cwd, json }) => {
227
+ const distDir = path.join(cwd, 'dist')
228
+ if (json.scripts?.build) {
229
+ if (!(await fs.pathExists(distDir))) {
230
+ console.warn('no dist dir!', distDir)
231
+ process.exit(1)
232
+ }
233
+ }
234
+ })
235
+ )
236
+ }
237
+
238
+ if (tamaguiGitUser) {
239
+ await run(`git config --global user.name 'Tamagui'`)
240
+ await run(`git config --global user.email 'tamagui@users.noreply.github.com`)
241
+ }
242
+
243
+ console.info('install and build')
244
+
245
+ if (!rePublish && !finish) {
246
+ await run(process.env.CI ? `bun install --frozen-lockfile` : `bun install`)
247
+ }
248
+
249
+ if (!skipBuild && !finish) {
250
+ await run(`bun clean`)
251
+ await run(`bun run build`)
252
+ await checkDistDirs()
253
+ }
254
+
255
+ if (!finish) {
256
+ console.info('run checks')
257
+
258
+ if (!skipTest) {
259
+ await run(`bun lint`)
260
+ await run(`bun check`)
261
+ // only in packages
262
+ // await run(`bun test`)
263
+ }
264
+ }
265
+
266
+ if (!dirty && !dryRun && !rePublish) {
267
+ const out = await run(`git status --porcelain`)
268
+ if (out.stdout) {
269
+ throw new Error(`Has unsaved git changes: ${out.stdout}`)
270
+ }
271
+ }
272
+
273
+ // snapshot workspace:* deps before mutation (shallow copy mutates originals)
274
+ const workspaceDeps = new Map<string, Record<string, Record<string, string>>>()
275
+ for (const { json, path: pkgPath } of allPackageJsons) {
276
+ const deps: Record<string, Record<string, string>> = {}
277
+ for (const field of [
278
+ 'dependencies',
279
+ 'devDependencies',
280
+ 'optionalDependencies',
281
+ 'peerDependencies',
282
+ ]) {
283
+ if (!json[field]) continue
284
+ for (const depName in json[field]) {
285
+ if (json[field][depName].startsWith('workspace:')) {
286
+ deps[field] ??= {}
287
+ deps[field][depName] = json[field][depName]
288
+ }
289
+ }
290
+ }
291
+ if (Object.keys(deps).length) workspaceDeps.set(pkgPath, deps)
292
+ }
293
+
294
+ if (!skipVersion && !finish) {
295
+ await Promise.all(
296
+ allPackageJsons.map(async ({ json, path: pkgPath }) => {
297
+ const next = { ...json }
298
+
299
+ next.version = nextVersion
300
+
301
+ for (const field of [
302
+ 'dependencies',
303
+ 'devDependencies',
304
+ 'optionalDependencies',
305
+ 'peerDependencies',
306
+ ]) {
307
+ const nextDeps = next[field]
308
+ if (!nextDeps) continue
309
+ for (const depName in nextDeps) {
310
+ if (allPackageJsons.some((p) => p.name === depName)) {
311
+ nextDeps[depName] = nextVersion
312
+ }
313
+ }
314
+ }
315
+
316
+ await writeJSON(pkgPath, next, { spaces: 2 })
317
+ })
318
+ )
319
+ }
320
+
321
+ if (!finish && !rePublish) {
322
+ await run(`git diff`)
323
+ }
324
+
325
+ if (!finish && !skipPublish) {
326
+ const packDir = path.join(os.tmpdir(), `takeout-release-${nextVersion}`)
327
+ await fs.remove(packDir)
328
+ await fs.ensureDir(packDir)
329
+
330
+ const isPublished = async ({ name }: (typeof packageJsons)[number]) => {
331
+ try {
332
+ const result = await run(`npm view ${name}@${nextVersion} version --json`, {
333
+ cwd: packDir,
334
+ captureOutput: true,
335
+ silent: true,
336
+ })
337
+ return JSON.parse(result.stdout.trim()) === nextVersion
338
+ } catch (error) {
339
+ const message = String(error)
340
+ const cause = error instanceof Error ? (error.cause as any) : undefined
341
+ const output = `${message}\n${cause?.stdout || ''}\n${cause?.stderr || ''}`
342
+ if (/E404|404 Not Found|is not in this registry/i.test(output)) {
343
+ return false
344
+ }
345
+ throw new Error(`Could not verify ${name}@${nextVersion} on npm:\n${output}`)
346
+ }
347
+ }
348
+
349
+ console.info(`Checking ${packageJsons.length} package versions on npm...`)
350
+ const publishedChecks = await pMap(
351
+ packageJsons,
352
+ async (pkg) => ({ pkg, published: await isPublished(pkg) }),
353
+ { concurrency: 8 }
354
+ )
355
+ const pendingPackages = publishedChecks
356
+ .filter(({ pkg, published }) => {
357
+ if (published) {
358
+ console.info(`Skipping ${pkg.name}: this version is already published`)
359
+ return false
360
+ }
361
+ return true
362
+ })
363
+ .map(({ pkg }) => pkg)
364
+
365
+ const prepareOne = async ({ name, cwd, json }: (typeof packageJsons)[number]) => {
366
+ const publishOptions = [canary && `--tag canary`, dryRun && `--dry-run`]
367
+ .filter(Boolean)
368
+ .join(' ')
369
+ const tgzPath = path.join(packDir, `${name.replace('/', '-')}.tgz`)
370
+ const packageJsonPath = path.join(cwd, 'package.json')
371
+ const packageJsonBeforePack = await fs.readFile(packageJsonPath, 'utf8')
372
+ const packageJsonForPublish = JSON.parse(packageJsonBeforePack)
373
+ packageJsonForPublish.repository = {
374
+ type: 'git',
375
+ url: 'https://github.com/tamagui/takeout2.git',
376
+ directory: path.relative(process.cwd(), cwd),
377
+ }
378
+ await writeJSON(packageJsonPath, packageJsonForPublish, { spaces: 2 })
379
+
380
+ // pack with bun (properly converts workspace:* to versions)
381
+ // use swap-exports for packages with build scripts, otherwise just pack
382
+ try {
383
+ if (json.scripts?.build) {
384
+ await run(
385
+ `bun run build --swap-exports -- bun pm pack --filename ${tgzPath}`,
386
+ {
387
+ cwd,
388
+ silent: true,
389
+ }
390
+ )
391
+ } else {
392
+ await run(`bun pm pack --filename ${tgzPath}`, {
393
+ cwd,
394
+ silent: true,
395
+ })
396
+ }
397
+ } finally {
398
+ await fs.writeFile(packageJsonPath, packageJsonBeforePack)
399
+ }
400
+
401
+ const workspaceDir = path.join(packDir, 'workspaces', name.replace('/', '_'))
402
+ await fs.ensureDir(workspaceDir)
403
+ await run(
404
+ `tar -xzf ${JSON.stringify(tgzPath)} -C ${JSON.stringify(workspaceDir)} --strip-components=1`,
405
+ { silent: true }
406
+ )
407
+
408
+ return {
409
+ workspace: path.relative(packDir, workspaceDir),
410
+ publishOptions,
411
+ }
412
+ }
413
+
414
+ if (pendingPackages.length > 0) {
415
+ if (process.stdin.isTTY && process.stdout.isTTY && !dryRun) {
416
+ console.info(
417
+ 'npm will open the browser for 2FA once. Select “do not challenge for the next 5 minutes” so the same short-lived approval can publish the remaining packages.'
418
+ )
419
+ }
420
+
421
+ const prepared = await pMap(pendingPackages, prepareOne, { concurrency: 8 })
422
+ await writeJSON(
423
+ path.join(packDir, 'package.json'),
424
+ {
425
+ name: 'takeout-release',
426
+ private: true,
427
+ workspaces: prepared.map(({ workspace }) => workspace),
428
+ },
429
+ { spaces: 2 }
430
+ )
431
+
432
+ const publishOptions = prepared[0]?.publishOptions || ''
433
+ const webAuthCache = path.join(process.cwd(), 'scripts/cache-npm-webauth.cjs')
434
+ const nodeOptions = [process.env.NODE_OPTIONS, `--require=${webAuthCache}`]
435
+ .filter(Boolean)
436
+ .join(' ')
437
+
438
+ try {
439
+ await run(
440
+ `npm publish --workspaces --ignore-scripts --access public ${publishOptions}`.trim(),
441
+ {
442
+ cwd: packDir,
443
+ env: { NODE_OPTIONS: nodeOptions },
444
+ interactive: true,
445
+ }
446
+ )
447
+ publishedNames.push(...pendingPackages.map(({ name }) => name))
448
+ } catch (error) {
449
+ const postflight = await pMap(
450
+ pendingPackages,
451
+ async (pkg) => ({ pkg, published: await isPublished(pkg) }),
452
+ { concurrency: 8 }
453
+ )
454
+ const completed = postflight.filter(({ published }) => published)
455
+ const missing = postflight.filter(({ published }) => !published)
456
+ publishedNames.push(...completed.map(({ pkg }) => pkg.name))
457
+
458
+ throw new Error(
459
+ `Publish stopped after ${completed.length} packages. Still missing:\n${missing.map(({ pkg }) => pkg.name).join('\n')}\n\nRe-run with --republish to retry only these packages.`,
460
+ { cause: error }
461
+ )
462
+ }
463
+ }
464
+
465
+ publishSucceeded = true
466
+ console.info(`✅ ${dryRun ? '[dry-run] ' : ''}Published\n`)
467
+
468
+ // restore workspace:* protocols after publishing
469
+ if (!dryRun) {
470
+ await Promise.all(
471
+ allPackageJsons.map(async ({ path: pkgPath }) => {
472
+ const saved = workspaceDeps.get(pkgPath)
473
+ if (!saved) return
474
+ const current = await fs.readJSON(pkgPath)
475
+ for (const field in saved) {
476
+ if (!current[field]) continue
477
+ for (const depName in saved[field]) {
478
+ current[field][depName] = saved[field][depName]
479
+ }
480
+ }
481
+ await writeJSON(pkgPath, current, { spaces: 2 })
482
+ })
483
+ )
484
+ }
485
+
486
+ // revert version changes after dry-run
487
+ if (dryRun && !rePublish) {
488
+ await run(`git checkout -- packages/*/package.json`, { silent: true })
489
+ console.info('Reverted version changes\n')
490
+ }
491
+
492
+ // restore package.json files for undocumented releases (no git history)
493
+ if (undocumented) {
494
+ console.info('restoring package.json files...')
495
+ await run(`git checkout -- packages/*/package.json`, { silent: true })
496
+ console.info(`✅ restored package.json files (undocumented release)\n`)
497
+ }
498
+ }
499
+
500
+ if (!skipFinish && !dryRun) {
501
+ // then git tag, commit, push
502
+ if (!finish) {
503
+ await run(`bun install`)
504
+ }
505
+ const tagPrefix = canary ? 'canary' : 'v'
506
+ const gitTag = `${tagPrefix}${nextVersion}`
507
+
508
+ await finishAndCommit()
509
+
510
+ async function finishAndCommit(cwd = process.cwd()) {
511
+ if (!rePublish || reRun || finish) {
512
+ await run(`git add -A`, { cwd })
513
+
514
+ await run(`git commit -m ${gitTag}`, { cwd })
515
+
516
+ if (!canary) {
517
+ await run(`git tag ${gitTag}`, { cwd })
518
+ }
519
+
520
+ if (!dirty) {
521
+ // pull once more before pushing so if there was a push in interim we get it
522
+ await run(`git pull --rebase origin HEAD`, { cwd })
523
+ }
524
+
525
+ await run(`git push origin head`, { cwd })
526
+ if (!canary) {
527
+ await run(`git push origin ${gitTag}`, { cwd })
528
+ }
529
+
530
+ console.info(`✅ Pushed and versioned\n`)
531
+ }
532
+ }
533
+ }
534
+
535
+ console.info(`✅ Done\n`)
536
+ } catch (err) {
537
+ if (!publishSucceeded && publishedNames.length === 0 && restorePackageJsons) {
538
+ await restorePackageJsons()
539
+ console.info('restored package.json files after failed release')
540
+ }
541
+ console.info('\nError:\n', err)
542
+ process.exit(1)
543
+ }
544
+ })
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { spawnSync } from 'node:child_process'
4
+
5
+ export function getEnvironment(resourceName: string) {
6
+ if (!resourceName) {
7
+ console.error(`No resouce name given:
8
+
9
+ bun scripts/get-environment.ts [resourceName]
10
+ `)
11
+ process.exit(1)
12
+ }
13
+
14
+ console.info(`Getting environment for ${resourceName}...`)
15
+ const result = spawnSync('bun', ['sst', 'state', 'export', '--stage', 'production'], {
16
+ encoding: 'utf-8',
17
+ })
18
+ const state = JSON.parse(result.stdout || '{}')
19
+
20
+ const resource = state.latest.resources.find(
21
+ (x: any) => x.outputs?._dev?.title === resourceName
22
+ )
23
+
24
+ if (!resource) {
25
+ console.error(`Can't find resource ${resourceName}`)
26
+ process.exit(1)
27
+ }
28
+
29
+ return resource.outputs._dev.environment as Record<string, string>
30
+ }
31
+
32
+ if (import.meta.main) {
33
+ const { cmd } = await import('./cmd')
34
+
35
+ await cmd`get environment variables from SST production state`.run(async () => {
36
+ console.info(getEnvironment(process.argv[2] || ''))
37
+ })
38
+ }
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { cmd } from './cmd'
4
+
5
+ await cmd`run typescript type checking`.run(async ({ run }) => {
6
+ const { sleep } = await import('@o/helpers')
7
+
8
+ const useTsgo = process.env.TSGO === '1'
9
+ const args = process.argv.slice(2)
10
+
11
+ // hugely cpu intensive, let the dev server start first before using:
12
+ if (process.env.LAZY_TYPECHECK) {
13
+ await sleep(6000)
14
+ }
15
+
16
+ if (useTsgo) {
17
+ await run(`bun tsgo --project ./tsconfig.json --noEmit --preserveWatchOutput ${args}`)
18
+ } else {
19
+ await run(`bun tsc --project ./tsconfig.json --noEmit --preserveWatchOutput ${args}`)
20
+ }
21
+ })