itty-packager 1.6.13 โ†’ 1.8.0

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/CHANGELOG.md ADDED
@@ -0,0 +1,4 @@
1
+ # CHANGELOG
2
+
3
+ ### 1.7.0
4
+ - added support for --otp flag (prompts for NPM OTP) or default interactive auth for publishing
package/README.md CHANGED
@@ -65,6 +65,7 @@ Build your TypeScript library with Rollup, TypeScript compilation, and optional
65
65
  - `--hybrid` - Build both ESM and CJS (default: ESM only)
66
66
  - `--minify` - Minify output with terser (default: `true`)
67
67
  - `--no-minify` - Skip minification
68
+ - `--release-from <dir>` - Release directory - exports relative to this (default: same as `--out`)
68
69
  - `-s, --snippet <name>` - Generate snippet file for README injection
69
70
  - `-h, --help` - Show help
70
71
 
@@ -72,15 +73,16 @@ Build your TypeScript library with Rollup, TypeScript compilation, and optional
72
73
  - Compiles all TypeScript files from `src/` to `dist/`
73
74
  - Generates ESM (`.mjs`) output only
74
75
  - Minifies output by default
75
- - Updates `package.json` exports with correct paths
76
+ - Updates `package.json` exports with correct paths (relative to the release directory)
76
77
  - Single file exports map to root export, multiple files get individual exports
77
78
 
78
79
  **Examples:**
79
80
  ```bash
80
81
  itty build # Basic ESM build, minified
81
- itty build --hybrid --sourcemap # Build both ESM/CJS with sourcemaps
82
- itty build --snippet=connect # Build with snippet generation for README
83
- itty build --from=lib --out=build # Build from lib/ to build/
82
+ itty build --hybrid --sourcemap # Build both ESM/CJS with sourcemaps
83
+ itty build --snippet=connect # Build with snippet generation for README
84
+ itty build --from=lib --out=build # Build from lib/ to build/
85
+ itty build --release-from=. # Exports include output dir prefix (for root releasing)
84
86
  ```
85
87
 
86
88
  ### `itty lint`
@@ -181,10 +183,11 @@ Version bump and release your package to npm with git operations and clean, flat
181
183
  - Git tag uses the same message as the commit
182
184
 
183
185
  **Default Behavior:**
186
+ - Verifies npm auth before making any changes (prompts for login if expired)
184
187
  - Defaults to patch version bump if no type specified
188
+ - Publishes to npm before git operations (so failed publishes don't leave orphaned tags)
185
189
  - Extracts build artifacts to temporary directory
186
190
  - Copies root files: `README.md`, `LICENSE`, `.npmrc` (if they exist)
187
- - Transforms package.json paths (e.g., `./dist/file.mjs` โ†’ `./file.mjs`)
188
191
  - Creates clean, flat package structure in node_modules
189
192
 
190
193
  **Examples:**
@@ -201,22 +204,22 @@ itty release --silent --push # Release with git operations, no interactive pro
201
204
 
202
205
  ## Package Structure
203
206
 
204
- The release command creates a clean package structure by:
207
+ The build + release commands work together to create a clean package structure:
205
208
 
206
- 1. **Extracting build artifacts** from your `dist/` directory to package root
207
- 2. **Copying essential files** like README, LICENSE
208
- 3. **Transforming paths** in package.json to point to root-level files
209
- 4. **Publishing the clean structure** so users get flat imports
209
+ 1. **Build** compiles to `dist/` and writes exports relative to the release directory (no `dist/` prefix by default)
210
+ 2. **Release** extracts build artifacts from `dist/` to a flat package root
211
+ 3. **Copies essential files** like README, LICENSE
212
+ 4. **Publishes the clean structure** so users get flat imports
210
213
 
211
- **Before (in your project):**
214
+ **In your project:**
212
215
  ```
213
- package.json exports: "./dist/connect.mjs"
216
+ package.json exports: "./connect.mjs"
214
217
  dist/connect.mjs
215
218
  README.md
216
219
  LICENSE
217
220
  ```
218
221
 
219
- **After (in node_modules):**
222
+ **In node_modules (after publish):**
220
223
  ```
221
224
  package.json exports: "./connect.mjs"
222
225
  connect.mjs
package/lib/builder.js CHANGED
@@ -14,6 +14,7 @@ export async function build(options = {}) {
14
14
  const {
15
15
  from = 'src',
16
16
  out = 'dist',
17
+ 'release-from': releaseFrom,
17
18
  copy: copyFiles,
18
19
  snippet,
19
20
  sourcemap = false,
@@ -21,6 +22,10 @@ export async function build(options = {}) {
21
22
  minify = true
22
23
  } = options
23
24
 
25
+ // If releasing from the output dir (default), exports need no prefix.
26
+ // If releasing from elsewhere (e.g. root), exports need the output dir prefix.
27
+ const exportPrefix = (releaseFrom ?? out) === out ? '.' : `./${out}`
28
+
24
29
  console.log(`๐Ÿ“ฆ Building from ${from}/ to ${out}/`)
25
30
 
26
31
  // Clean output directory
@@ -55,13 +60,13 @@ export async function build(options = {}) {
55
60
  // Single file maps to root export
56
61
  const file = files[0]
57
62
  const exportObj = {
58
- import: `./${out}/${path.basename(file.esm)}`,
59
- types: `./${out}/${path.basename(file.types)}`,
63
+ import: `${exportPrefix}/${path.basename(file.esm)}`,
64
+ types: `${exportPrefix}/${path.basename(file.types)}`,
60
65
  }
61
66
 
62
67
  // Add CJS export only if hybrid mode is enabled
63
68
  if (hybrid) {
64
- exportObj.require = `./${out}/${path.basename(file.cjs)}`
69
+ exportObj.require = `${exportPrefix}/${path.basename(file.cjs)}`
65
70
  }
66
71
 
67
72
  pkg.exports = {
@@ -71,13 +76,13 @@ export async function build(options = {}) {
71
76
  // Multiple files get individual exports
72
77
  pkg.exports = files.reduce((acc, file) => {
73
78
  const exportObj = {
74
- import: `./${out}/${path.basename(file.esm)}`,
75
- types: `./${out}/${path.basename(file.types)}`,
79
+ import: `${exportPrefix}/${path.basename(file.esm)}`,
80
+ types: `${exportPrefix}/${path.basename(file.types)}`,
76
81
  }
77
82
 
78
83
  // Add CJS export only if hybrid mode is enabled
79
84
  if (hybrid) {
80
- exportObj.require = `./${out}/${path.basename(file.cjs)}`
85
+ exportObj.require = `${exportPrefix}/${path.basename(file.cjs)}`
81
86
  }
82
87
 
83
88
  acc[file.shortPath] = exportObj
@@ -38,6 +38,10 @@ export async function buildCommand(args) {
38
38
  type: 'boolean',
39
39
  description: 'Skip minification'
40
40
  },
41
+ 'release-from': {
42
+ type: 'string',
43
+ description: 'Release directory - exports are relative to this (default: same as --out)'
44
+ },
41
45
  snippet: {
42
46
  type: 'string',
43
47
  short: 's',
@@ -59,22 +63,24 @@ itty build - Build your library with rollup and typescript
59
63
  Usage: itty build [options]
60
64
 
61
65
  Options:
62
- -f, --from <dir> Source directory (default: src)
63
- -o, --out <dir> Output directory (default: dist)
64
- -c, --copy <files> Files to copy to output (comma-separated)
65
- --sourcemap Generate source maps (default: false)
66
- --hybrid Build both ESM and CJS (default: ESM only)
67
- --minify Minify output with terser (default: true)
68
- --no-minify Skip minification
69
- -s, --snippet <name> Generate snippet file for README injection
70
- -h, --help Show help
66
+ -f, --from <dir> Source directory (default: src)
67
+ -o, --out <dir> Output directory (default: dist)
68
+ -c, --copy <files> Files to copy to output (comma-separated)
69
+ --sourcemap Generate source maps (default: false)
70
+ --hybrid Build both ESM and CJS (default: ESM only)
71
+ --minify Minify output with terser (default: true)
72
+ --no-minify Skip minification
73
+ --release-from <dir> Release directory - exports relative to this (default: same as --out)
74
+ -s, --snippet <name> Generate snippet file for README injection
75
+ -h, --help Show help
71
76
 
72
77
  Examples:
73
78
  itty build # Build ESM only, minified, no sourcemaps
74
- itty build --hybrid --sourcemap # Build both ESM/CJS with sourcemaps
75
- itty build --no-minify # Build without minification
76
- itty build --from=lib --out=build # Build from lib/ to build/
77
- itty build --snippet=connect # Build with connect snippet generation
79
+ itty build --hybrid --sourcemap # Build both ESM/CJS with sourcemaps
80
+ itty build --no-minify # Build without minification
81
+ itty build --from=lib --out=build # Build from lib/ to build/
82
+ itty build --snippet=connect # Build with connect snippet generation
83
+ itty build --release-from=. # Exports include output dir prefix (for root releasing)
78
84
  `)
79
85
  return
80
86
  }
@@ -6,49 +6,6 @@ import { prepareCommand } from './prepare.js'
6
6
 
7
7
  const SEMVER_TYPES = ['major', 'minor', 'patch']
8
8
 
9
- function transformPackageExports(pkg, srcDir) {
10
- // Transform package.json exports to remove srcDir prefix from paths
11
- if (pkg.exports) {
12
- const transformPath = (exportPath) => {
13
- if (typeof exportPath === 'string' && exportPath.startsWith(`./${srcDir}/`)) {
14
- return exportPath.replace(`./${srcDir}/`, './')
15
- }
16
- return exportPath
17
- }
18
-
19
- const transformExportObj = (exportObj) => {
20
- if (typeof exportObj === 'string') {
21
- return transformPath(exportObj)
22
- }
23
-
24
- if (typeof exportObj === 'object' && exportObj !== null) {
25
- const transformed = {}
26
- for (const [key, value] of Object.entries(exportObj)) {
27
- if (typeof value === 'string') {
28
- transformed[key] = transformPath(value)
29
- } else if (typeof value === 'object') {
30
- transformed[key] = transformExportObj(value)
31
- } else {
32
- transformed[key] = value
33
- }
34
- }
35
- return transformed
36
- }
37
-
38
- return exportObj
39
- }
40
-
41
- const transformedExports = {}
42
- for (const [key, value] of Object.entries(pkg.exports)) {
43
- transformedExports[key] = transformExportObj(value)
44
- }
45
-
46
- return { ...pkg, exports: transformedExports }
47
- }
48
-
49
- return pkg
50
- }
51
-
52
9
  function versionBump(currentVersion, type) {
53
10
  const parts = currentVersion.split('.').map(Number)
54
11
 
@@ -209,6 +166,54 @@ async function getCommitMessage(newVersion, silent = false) {
209
166
  })
210
167
  }
211
168
 
169
+ async function promptOtp() {
170
+ return new Promise((resolve, reject) => {
171
+ process.stdout.write('๐Ÿ”‘ Enter OTP code: ')
172
+ let code = ''
173
+
174
+ process.stdin.setRawMode(true)
175
+ process.stdin.resume()
176
+
177
+ const handleInput = (chunk) => {
178
+ const key = chunk.toString()
179
+
180
+ if (key === '\x03') {
181
+ cleanup()
182
+ reject(new Error('User cancelled with Ctrl+C'))
183
+ return
184
+ }
185
+
186
+ if (key === '\r' || key === '\n') {
187
+ process.stdout.write('\n')
188
+ cleanup()
189
+ resolve(code.trim())
190
+ return
191
+ }
192
+
193
+ if (key === '\x7f' || key === '\x08') {
194
+ if (code.length > 0) {
195
+ code = code.slice(0, -1)
196
+ process.stdout.write('\b \b')
197
+ }
198
+ return
199
+ }
200
+
201
+ if (key >= '0' && key <= '9') {
202
+ code += key
203
+ process.stdout.write(key)
204
+ }
205
+ }
206
+
207
+ const cleanup = () => {
208
+ process.stdin.setRawMode(false)
209
+ process.stdin.pause()
210
+ process.stdin.removeListener('data', handleInput)
211
+ }
212
+
213
+ process.stdin.on('data', handleInput)
214
+ })
215
+ }
216
+
212
217
  async function runCommand(command, cwd = process.cwd(), verbose = false) {
213
218
  return new Promise((resolve, reject) => {
214
219
  const [cmd, ...args] = command.split(' ')
@@ -318,6 +323,10 @@ export async function releaseCommand(args) {
318
323
  type: 'boolean',
319
324
  description: 'Skip interactive prompts (use default commit message)'
320
325
  },
326
+ otp: {
327
+ type: 'boolean',
328
+ description: 'Prompt for a one-time password (OTP) for npm publish'
329
+ },
321
330
  verbose: {
322
331
  type: 'boolean',
323
332
  short: 'v',
@@ -354,6 +363,7 @@ Publish Options:
354
363
  --prepare Run prepare (lint, test, build) before publishing
355
364
  --silent Skip interactive prompts (use default commit message)
356
365
  --no-license Do not copy LICENSE file to published package
366
+ --otp Prompt for a one-time password (OTP) for npm publish
357
367
  -v, --verbose Show detailed output including npm and git command details
358
368
 
359
369
  Git Options:
@@ -408,6 +418,7 @@ This creates a clean, flat package structure in node_modules.
408
418
  const noLicense = releaseArgs['no-license']
409
419
  const shouldPrepare = releaseArgs.prepare
410
420
  const silent = releaseArgs.silent
421
+ const useOtp = releaseArgs.otp
411
422
  const verbose = releaseArgs.verbose
412
423
 
413
424
  // Read package.json and store original version for potential revert
@@ -430,6 +441,17 @@ This creates a clean, flat package structure in node_modules.
430
441
  throw new Error(`Source directory "${sourceDir}" does not exist. Run "itty build" first.`)
431
442
  }
432
443
 
444
+ // Verify npm auth before making any changes
445
+ if (!dryRun) {
446
+ console.log(`๐Ÿ”‘ Verifying npm auth...`)
447
+ try {
448
+ await runCommand('npm whoami --registry=https://registry.npmjs.org', rootPath, verbose)
449
+ } catch {
450
+ console.log(`โš ๏ธ npm session expired โ€” logging in...`)
451
+ await runCommand('npm login --registry=https://registry.npmjs.org', rootPath, true)
452
+ }
453
+ }
454
+
433
455
  // Clean and create temp directory
434
456
  if (verbose) console.log(`๐Ÿงน Preparing ${releaseArgs.dest}/`)
435
457
  await fs.emptyDir(tempDir)
@@ -478,47 +500,65 @@ This creates a clean, flat package structure in node_modules.
478
500
  }
479
501
  }
480
502
 
481
- // Update package.json in temp directory with transformed paths
482
- const updatedPkg = isRootPublish
483
- ? { ...originalPkg, version: newVersion } // No path transformation for root publishing
484
- : transformPackageExports({ ...originalPkg, version: newVersion }, sourceDir)
503
+ // Update package.json with new version
504
+ const updatedPkg = { ...originalPkg, version: newVersion }
485
505
  const tempPkgPath = path.join(tempDir, 'package.json')
486
506
 
487
- const transformMessage = isRootPublish ? '' : ' (transforming paths)'
488
- if (verbose) console.log(`๐Ÿ“ Updating package.json to v${newVersion}${transformMessage}`)
507
+ if (verbose) console.log(`๐Ÿ“ Updating package.json to v${newVersion}`)
489
508
  await fs.writeJSON(tempPkgPath, updatedPkg, { spaces: 2 })
490
509
 
491
- // Update root package.json first (before git operations)
510
+ // Update root package.json with new version
492
511
  if (!dryRun) {
493
512
  if (verbose) console.log(`๐Ÿ“ Updating root package.json`)
494
513
  await fs.writeJSON(pkgPath, updatedPkg, { spaces: 2 })
495
514
  }
496
515
 
497
- // Git operations (before publishing)
498
- if (!noGit && !dryRun) {
499
- let commitMessage = `released v${newVersion}` // Default message
500
-
501
- if (shouldPush || shouldTag) {
502
- try {
503
- // Get commit message (interactive or default)
504
- commitMessage = await getCommitMessage(newVersion, silent)
505
-
506
- console.log(`๐Ÿ“‹ Committing changes...`)
507
- if (verbose) console.log(`Running: git add . && git commit`)
508
- await runCommand('git add .', rootPath, verbose)
509
- await runCommand(`git commit -m "${commitMessage}"`, rootPath, verbose)
510
- } catch (error) {
511
- if (error.message.includes('cancelled')) {
512
- console.log('โŒ Commit cancelled - reverting version and exiting')
513
- // Revert the version we just updated
514
- await fs.writeJSON(pkgPath, originalPkg, { spaces: 2 })
515
- // Don't rethrow - exit cleanly since this is user-initiated
516
- process.exit(0)
517
- }
518
- throw error
516
+ // Collect interactive inputs before publish
517
+ let commitMessage = `released v${newVersion}`
518
+ if (!noGit && !dryRun && (shouldPush || shouldTag)) {
519
+ try {
520
+ commitMessage = await getCommitMessage(newVersion, silent)
521
+ } catch (error) {
522
+ if (error.message.includes('cancelled')) {
523
+ console.log('โŒ Release cancelled - reverting version and exiting')
524
+ await fs.writeJSON(pkgPath, originalPkg, { spaces: 2 })
525
+ process.exit(0)
519
526
  }
527
+ throw error
528
+ }
529
+ }
530
+
531
+ // NPM publish (before git operations so failed publishes don't leave orphaned tags)
532
+ if (dryRun) {
533
+ console.log('๐Ÿธ Dry run - skipping publish')
534
+ } else {
535
+ let otpCode
536
+ if (useOtp) {
537
+ otpCode = await promptOtp()
538
+ if (!otpCode) throw new Error('No OTP code provided')
520
539
  }
521
540
 
541
+ console.log(`๐Ÿš€ Publishing to npm...`)
542
+
543
+ const publishCmd = [
544
+ 'npm publish',
545
+ '--registry=https://registry.npmjs.org',
546
+ publicAccess ? '--access=public' : '',
547
+ SEMVER_TYPES.includes(releaseType) ? '' : `--tag=${releaseType}`,
548
+ otpCode ? `--otp=${otpCode}` : ''
549
+ ].filter(Boolean).join(' ')
550
+
551
+ if (verbose) console.log(`Running: ${publishCmd}`)
552
+ await runCommand(publishCmd, tempDir, true)
553
+ }
554
+
555
+ // Git operations (only after successful publish)
556
+ if (!noGit && !dryRun && (shouldPush || shouldTag)) {
557
+ console.log(`๐Ÿ“‹ Committing changes...`)
558
+ if (verbose) console.log(`Running: git add . && git commit`)
559
+ await runCommand('git add .', rootPath, verbose)
560
+ await runCommand(`git commit -m "${commitMessage}"`, rootPath, verbose)
561
+
522
562
  if (shouldTag) {
523
563
  console.log(`๐Ÿท๏ธ Creating git tag v${newVersion}`)
524
564
  await runCommand(`git tag -a v${newVersion} -m "${commitMessage}"`, rootPath, verbose)
@@ -535,24 +575,6 @@ This creates a clean, flat package structure in node_modules.
535
575
  }
536
576
  }
537
577
 
538
- // NPM publish as final step
539
- if (dryRun) {
540
- console.log('๐Ÿธ Dry run - skipping publish')
541
- } else {
542
- // Publish from temp directory
543
- console.log(`๐Ÿš€ Publishing to npm...`)
544
-
545
- const publishCmd = [
546
- 'npm publish',
547
- '--registry=https://registry.npmjs.org',
548
- publicAccess ? '--access=public' : '',
549
- SEMVER_TYPES.includes(releaseType) ? '' : `--tag=${releaseType}`
550
- ].filter(Boolean).join(' ')
551
-
552
- if (verbose) console.log(`Running: ${publishCmd}`)
553
- await runCommand(publishCmd, tempDir, verbose)
554
- }
555
-
556
578
  // Cleanup
557
579
  if (!noCleanup) {
558
580
  if (verbose) console.log(`๐Ÿงน Cleaning up ${releaseArgs.dest}/`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "itty-packager",
3
- "version": "1.6.13",
3
+ "version": "1.8.0",
4
4
  "description": "Universal build tool for itty libraries",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,7 +9,7 @@
9
9
  "scripts": {
10
10
  "lint": "bun bin/itty.js lint",
11
11
  "dev": "bun test --coverage --watch",
12
- "release": "bun bin/itty.js release --patch --tag --push --root"
12
+ "release": "bun bin/itty.js release --patch --tag --push --root --otp"
13
13
  },
14
14
  "keywords": [
15
15
  "build",
@@ -67,6 +67,40 @@ const tests: TestTree = {
67
67
  expect(result.exitCode).toBe(0)
68
68
  await expectFile(path.join(project.dir, 'build/main.mjs')).toExist()
69
69
  }
70
+ },
71
+
72
+ 'export paths': {
73
+ 'default: exports have no dist/ prefix': async () => {
74
+ const project = await ProjectFixture.create('exports-default', {
75
+ 'src/index.ts': 'export const a = 1',
76
+ 'package.json': JSON.stringify({
77
+ name: 'test-exports',
78
+ version: '1.0.0',
79
+ type: 'module'
80
+ }, null, 2)
81
+ })
82
+
83
+ await cli.run(['build'], { cwd: project.dir })
84
+ const pkg = JSON.parse(await Bun.file(path.join(project.dir, 'package.json')).text())
85
+ expect(pkg.exports['.'].import).toBe('./index.mjs')
86
+ expect(pkg.exports['.'].types).toBe('./index.d.ts')
87
+ },
88
+
89
+ '--release-from=. adds dist/ prefix': async () => {
90
+ const project = await ProjectFixture.create('exports-release-from', {
91
+ 'src/index.ts': 'export const a = 1',
92
+ 'package.json': JSON.stringify({
93
+ name: 'test-exports-root',
94
+ version: '1.0.0',
95
+ type: 'module'
96
+ }, null, 2)
97
+ })
98
+
99
+ await cli.run(['build', '--release-from=.'], { cwd: project.dir })
100
+ const pkg = JSON.parse(await Bun.file(path.join(project.dir, 'package.json')).text())
101
+ expect(pkg.exports['.'].import).toBe('./dist/index.mjs')
102
+ expect(pkg.exports['.'].types).toBe('./dist/index.d.ts')
103
+ }
70
104
  }
71
105
  }
72
106
  }
@@ -155,20 +155,20 @@ const tests: TestTree = {
155
155
  }
156
156
  },
157
157
 
158
- 'package structure transformation': {
159
- 'copies and transforms dist files': async () => {
160
- const project = await ProjectFixture.create('package-transform', {
158
+ 'package structure': {
159
+ 'copies dist files and preserves exports': async () => {
160
+ const project = await ProjectFixture.create('package-structure', {
161
161
  'dist/index.mjs': 'export const main = "test"',
162
162
  'dist/utils.mjs': 'export const utils = "helper"',
163
163
  'README.md': '# Test Package',
164
164
  'LICENSE': 'MIT License',
165
165
  'package.json': JSON.stringify({
166
- name: 'test-transform',
166
+ name: 'test-structure',
167
167
  version: '1.0.0',
168
168
  type: 'module',
169
169
  exports: {
170
- '.': './dist/index.mjs',
171
- './utils': './dist/utils.mjs'
170
+ '.': './index.mjs',
171
+ './utils': './utils.mjs'
172
172
  }
173
173
  }, null, 2)
174
174
  })
@@ -176,14 +176,14 @@ const tests: TestTree = {
176
176
  const result = await cli.run(['release', '--dry-run', '--no-git', '--no-cleanup'], { cwd: project.dir })
177
177
  expect(result.exitCode).toBe(0)
178
178
 
179
- // Check that temp directory was created with transformed structure
179
+ // Check that temp directory was created with flat structure
180
180
  const tempDir = path.join(project.dir, '.dist')
181
181
  await expectFile(path.join(tempDir, 'index.mjs')).toExist()
182
182
  await expectFile(path.join(tempDir, 'utils.mjs')).toExist()
183
183
  await expectFile(path.join(tempDir, 'README.md')).toExist()
184
184
  await expectFile(path.join(tempDir, 'LICENSE')).toExist()
185
185
 
186
- // Check that package.json was transformed
186
+ // Check that exports are preserved as-is (no transformation needed)
187
187
  const pkgContent = fs.readFileSync(path.join(tempDir, 'package.json'), 'utf-8')
188
188
  const pkg = JSON.parse(pkgContent)
189
189
  expect(pkg.exports).toEqual({