@take-out/scripts 0.4.5 → 0.4.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@take-out/scripts",
3
- "version": "0.4.5",
3
+ "version": "0.4.7",
4
4
  "type": "module",
5
5
  "main": "./src/cmd.ts",
6
6
  "sideEffects": false,
@@ -29,8 +29,8 @@
29
29
  },
30
30
  "dependencies": {
31
31
  "@clack/prompts": "^0.8.2",
32
- "@take-out/helpers": "0.4.5",
33
- "@take-out/run": "0.4.5",
32
+ "@take-out/helpers": "0.4.7",
33
+ "@take-out/run": "0.4.7",
34
34
  "picocolors": "^1.1.1"
35
35
  }
36
36
  }
@@ -204,7 +204,7 @@ export async function run(
204
204
  console.error(`run() error: ${errorMsg}: ${stderr || ''}`)
205
205
  }
206
206
 
207
- const error = new Error(errorMsg, { cause: { exitCode } })
207
+ const error = new Error(errorMsg, { cause: { exitCode, stdout, stderr } })
208
208
  Error.captureStackTrace(error, runInternal)
209
209
  throw error
210
210
  }
package/src/release.ts CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  import { cmd } from './cmd'
4
4
 
5
+ function isPublishAuthOrOtpError(error: unknown) {
6
+ return /EOTP|one-time password/i.test(getPublishErrorMessage(error))
7
+ }
8
+
9
+ function redactNpmOtp(message: string) {
10
+ return message.replace(/--otp(?:=|\s+)\S+/g, '--otp=******')
11
+ }
12
+
13
+ function getPublishErrorMessage(error: unknown) {
14
+ const cause = error instanceof Error ? (error.cause as any) : undefined
15
+ return redactNpmOtp(
16
+ [error instanceof Error ? error.message : String(error), cause?.stdout, cause?.stderr]
17
+ .filter(Boolean)
18
+ .join('\n'),
19
+ )
20
+ }
21
+
5
22
  // avoid emitter error
6
23
  process.setMaxListeners(50)
7
24
  process.stderr.setMaxListeners(50)
@@ -15,7 +32,7 @@ await cmd`publish takeout packages to npm`
15
32
  --dirty boolean --tamagui-git-user boolean --sync-on-zero boolean --skip-on-zero-sync boolean
16
33
  --undocumented boolean --skip-all boolean`,
17
34
  )
18
- .run(async ({ args, $, run, path, os }) => {
35
+ .run(async ({ args, $, run, path, os, prompt }) => {
19
36
  const fs = (await import('fs-extra')).default
20
37
  const { writeJSON } = await import('fs-extra')
21
38
  const pMap = (await import('p-map')).default
@@ -49,6 +66,8 @@ await cmd`publish takeout packages to npm`
49
66
  const tamaguiGitUser = args.tamaguiGitUser
50
67
  const syncOnZeroOnly = args.syncOnZero
51
68
  const skipOnZeroSync = args.skipOnZeroSync
69
+ const canPromptForNpmOtp =
70
+ !finish && !undocumented && !process.argv.includes('--ci') && !process.env.CI
52
71
 
53
72
  async function syncOnZeroIn() {
54
73
  if (!(await fs.pathExists(onZeroGithub))) return
@@ -277,6 +296,10 @@ await cmd`publish takeout packages to npm`
277
296
  console.info(` Next: ${nextVersion}`)
278
297
  }
279
298
 
299
+ let restorePackageJsons: (() => Promise<void>) | undefined
300
+ let publishSucceeded = false
301
+ const publishedNames: string[] = []
302
+
280
303
  try {
281
304
  // sync on-zero IN (before release)
282
305
  if (!skipOnZeroSync && !finish && !rePublish) {
@@ -297,6 +320,19 @@ await cmd`publish takeout packages to npm`
297
320
  const packagePaths = await getWorkspacePackages()
298
321
  const { allPackageJsons, publishablePackages: packageJsons } =
299
322
  await loadPackageJsons(packagePaths)
323
+ const originalPackageJsons = new Map<string, string>()
324
+ await Promise.all(
325
+ allPackageJsons.map(async ({ path: pkgPath }) => {
326
+ originalPackageJsons.set(pkgPath, await fs.readFile(pkgPath, 'utf8'))
327
+ }),
328
+ )
329
+ restorePackageJsons = async () => {
330
+ await Promise.all(
331
+ [...originalPackageJsons].map(([pkgPath, contents]) =>
332
+ fs.writeFile(pkgPath, contents),
333
+ ),
334
+ )
335
+ }
300
336
 
301
337
  if (!finish) {
302
338
  console.info(
@@ -409,44 +445,129 @@ await cmd`publish takeout packages to npm`
409
445
  const packDir = path.join(os.tmpdir(), `takeout-release-${nextVersion}`)
410
446
  await fs.ensureDir(packDir)
411
447
 
412
- await pMap(
413
- packageJsons,
414
- async ({ name, cwd, json }) => {
415
- const publishOptions = [canary && `--tag canary`, dryRun && `--dry-run`]
416
- .filter(Boolean)
417
- .join(' ')
418
- const tgzPath = path.join(packDir, `${name.replace('/', '-')}.tgz`)
448
+ let cachedOtp = process.env.npm_config_otp || process.env.NPM_CONFIG_OTP
449
+ let otpPromptInFlight: Promise<string> | undefined
450
+ const getOtp = (reason: string, optional = false): Promise<string> => {
451
+ if (otpPromptInFlight) return otpPromptInFlight
452
+ otpPromptInFlight = (async () => {
453
+ console.info(`\n${reason}`)
454
+ const code = await prompt.text({
455
+ message: optional
456
+ ? 'npm 2FA code (6 digits, empty to skip)'
457
+ : 'npm 2FA code (6 digits)',
458
+ validate: (value) => {
459
+ const next = String(value ?? '').trim()
460
+ if (optional && !next) return
461
+ if (/^\d{6}$/.test(next)) return
462
+ return 'Enter a 6-digit code'
463
+ },
464
+ })
465
+ if (prompt.isCancel(code)) {
466
+ prompt.cancel('Publish cancelled')
467
+ throw new Error('No OTP provided, aborting publish')
468
+ }
469
+ const otp = String(code ?? '').trim()
470
+ if (!otp) {
471
+ if (optional) return ''
472
+ throw new Error('No OTP provided, aborting publish')
473
+ }
474
+ cachedOtp = otp
475
+ return otp
476
+ })().finally(() => {
477
+ otpPromptInFlight = undefined
478
+ })
479
+ return otpPromptInFlight
480
+ }
419
481
 
420
- // pack with bun (properly converts workspace:* to versions)
421
- // use swap-exports for packages with build scripts, otherwise just pack
422
- if (json.scripts?.build) {
423
- await run(
424
- `bun run build --swap-exports -- bun pm pack --filename ${tgzPath}`,
425
- {
426
- cwd,
427
- silent: true,
428
- },
429
- )
430
- } else {
431
- await run(`bun pm pack --filename ${tgzPath}`, {
482
+ if (!cachedOtp && canPromptForNpmOtp && !dryRun) {
483
+ await getOtp(
484
+ 'Most Takeout npm publishes require 2FA. Provide the current code now so every package publish uses it.',
485
+ true,
486
+ )
487
+ }
488
+
489
+ const failedPublishes: string[] = []
490
+ const publishOne = async ({ name, cwd, json }: (typeof packageJsons)[number]) => {
491
+ const publishOptions = [canary && `--tag canary`, dryRun && `--dry-run`]
492
+ .filter(Boolean)
493
+ .join(' ')
494
+ const tgzPath = path.join(packDir, `${name.replace('/', '-')}.tgz`)
495
+
496
+ // pack with bun (properly converts workspace:* to versions)
497
+ // use swap-exports for packages with build scripts, otherwise just pack
498
+ if (json.scripts?.build) {
499
+ await run(
500
+ `bun run build --swap-exports -- bun pm pack --filename ${tgzPath}`,
501
+ {
432
502
  cwd,
433
503
  silent: true,
504
+ },
505
+ )
506
+ } else {
507
+ await run(`bun pm pack --filename ${tgzPath}`, {
508
+ cwd,
509
+ silent: true,
510
+ })
511
+ }
512
+
513
+ let attempt = 0
514
+ let otp = cachedOtp
515
+ while (true) {
516
+ attempt++
517
+ try {
518
+ const otpOption = otp ? `--otp=${otp}` : ''
519
+ await run(`npm publish ${tgzPath} ${publishOptions} ${otpOption}`.trim(), {
520
+ cwd: packDir,
521
+ silent: true,
522
+ captureOutput: true,
523
+ env: otp ? { npm_config_otp: otp } : undefined,
434
524
  })
525
+ publishedNames.push(name)
526
+ console.info(`${dryRun ? '[dry-run] ' : ''}Published ${name}`)
527
+ return
528
+ } catch (err) {
529
+ if (isPublishAuthOrOtpError(err) && attempt < 3) {
530
+ if (otp && cachedOtp === otp) cachedOtp = undefined
531
+ otp = await getOtp(
532
+ attempt === 1
533
+ ? `npm requires a 2FA code to publish ${name}`
534
+ : `npm 2FA code expired, need a fresh one for ${name}`,
535
+ )
536
+ continue
537
+ }
538
+ if (rePublish) {
539
+ console.warn(
540
+ `⚠️ ${name}: publish failed (likely already published), continuing`,
541
+ )
542
+ return
543
+ }
544
+ console.error(`Failed to publish ${name}:`, getPublishErrorMessage(err))
545
+ failedPublishes.push(name)
546
+ return
435
547
  }
548
+ }
549
+ }
436
550
 
437
- // publish the tgz directly
438
- await run(`npm publish ${tgzPath} ${publishOptions}`.trim(), {
439
- cwd: packDir,
440
- silent: true,
441
- })
551
+ const [firstPackage, ...restPackages] = packageJsons
552
+ if (firstPackage) {
553
+ await publishOne(firstPackage)
554
+ }
555
+ if (failedPublishes.length > 0) {
556
+ throw new Error(
557
+ `Failed to publish ${failedPublishes.length} packages:\n${failedPublishes.join('\n')}\n\nRe-run after fixing the publish error.`,
558
+ )
559
+ }
560
+ await pMap(restPackages, publishOne, {
561
+ concurrency: 15,
562
+ })
442
563
 
443
- console.info(`${dryRun ? '[dry-run] ' : ''}Published ${name}`)
444
- },
445
- {
446
- concurrency: 15,
447
- },
448
- )
564
+ if (failedPublishes.length > 0) {
565
+ throw new Error(
566
+ `Failed to publish ${failedPublishes.length} packages:\n${failedPublishes.join('\n')}\n\nRe-run with --republish to retry.`,
567
+ )
568
+ }
449
569
 
570
+ publishSucceeded = true
450
571
  console.info(`✅ ${dryRun ? '[dry-run] ' : ''}Published\n`)
451
572
 
452
573
  // restore workspace:* protocols after publishing
@@ -523,6 +644,10 @@ await cmd`publish takeout packages to npm`
523
644
 
524
645
  console.info(`✅ Done\n`)
525
646
  } catch (err) {
647
+ if (!publishSucceeded && publishedNames.length === 0 && restorePackageJsons) {
648
+ await restorePackageJsons()
649
+ console.info('restored package.json files after failed release')
650
+ }
526
651
  console.info('\nError:\n', err)
527
652
  process.exit(1)
528
653
  }