itty-packager 1.7.0 โ†’ 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/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
 
@@ -484,6 +441,17 @@ This creates a clean, flat package structure in node_modules.
484
441
  throw new Error(`Source directory "${sourceDir}" does not exist. Run "itty build" first.`)
485
442
  }
486
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
+
487
455
  // Clean and create temp directory
488
456
  if (verbose) console.log(`๐Ÿงน Preparing ${releaseArgs.dest}/`)
489
457
  await fs.emptyDir(tempDir)
@@ -532,75 +500,44 @@ This creates a clean, flat package structure in node_modules.
532
500
  }
533
501
  }
534
502
 
535
- // Update package.json in temp directory with transformed paths
536
- const updatedPkg = isRootPublish
537
- ? { ...originalPkg, version: newVersion } // No path transformation for root publishing
538
- : transformPackageExports({ ...originalPkg, version: newVersion }, sourceDir)
503
+ // Update package.json with new version
504
+ const updatedPkg = { ...originalPkg, version: newVersion }
539
505
  const tempPkgPath = path.join(tempDir, 'package.json')
540
506
 
541
- const transformMessage = isRootPublish ? '' : ' (transforming paths)'
542
- if (verbose) console.log(`๐Ÿ“ Updating package.json to v${newVersion}${transformMessage}`)
507
+ if (verbose) console.log(`๐Ÿ“ Updating package.json to v${newVersion}`)
543
508
  await fs.writeJSON(tempPkgPath, updatedPkg, { spaces: 2 })
544
509
 
545
- // Update root package.json first (before git operations)
510
+ // Update root package.json with new version
546
511
  if (!dryRun) {
547
512
  if (verbose) console.log(`๐Ÿ“ Updating root package.json`)
548
513
  await fs.writeJSON(pkgPath, updatedPkg, { spaces: 2 })
549
514
  }
550
515
 
551
- // Git operations (before publishing)
552
- if (!noGit && !dryRun) {
553
- let commitMessage = `released v${newVersion}` // Default message
554
-
555
- if (shouldPush || shouldTag) {
556
- try {
557
- // Get commit message (interactive or default)
558
- commitMessage = await getCommitMessage(newVersion, silent)
559
-
560
- console.log(`๐Ÿ“‹ Committing changes...`)
561
- if (verbose) console.log(`Running: git add . && git commit`)
562
- await runCommand('git add .', rootPath, verbose)
563
- await runCommand(`git commit -m "${commitMessage}"`, rootPath, verbose)
564
- } catch (error) {
565
- if (error.message.includes('cancelled')) {
566
- console.log('โŒ Commit cancelled - reverting version and exiting')
567
- // Revert the version we just updated
568
- await fs.writeJSON(pkgPath, originalPkg, { spaces: 2 })
569
- // Don't rethrow - exit cleanly since this is user-initiated
570
- process.exit(0)
571
- }
572
- throw error
573
- }
574
- }
575
-
576
- if (shouldTag) {
577
- console.log(`๐Ÿท๏ธ Creating git tag v${newVersion}`)
578
- await runCommand(`git tag -a v${newVersion} -m "${commitMessage}"`, rootPath, verbose)
579
- }
580
-
581
- if (shouldPush) {
582
- console.log(`๐Ÿ“ค Pushing to remote...`)
583
- await runCommand('git push', rootPath, verbose)
584
-
585
- if (shouldTag) {
586
- console.log(`๐Ÿ”– Pushing tags...`)
587
- await runCommand('git push --tags', rootPath, verbose)
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)
588
526
  }
527
+ throw error
589
528
  }
590
529
  }
591
530
 
592
- // NPM publish as final step
531
+ // NPM publish (before git operations so failed publishes don't leave orphaned tags)
593
532
  if (dryRun) {
594
533
  console.log('๐Ÿธ Dry run - skipping publish')
595
534
  } else {
596
- // Prompt for OTP if requested
597
535
  let otpCode
598
536
  if (useOtp) {
599
537
  otpCode = await promptOtp()
600
538
  if (!otpCode) throw new Error('No OTP code provided')
601
539
  }
602
540
 
603
- // Publish from temp directory (always inherit stdio for interactive npm auth)
604
541
  console.log(`๐Ÿš€ Publishing to npm...`)
605
542
 
606
543
  const publishCmd = [
@@ -615,6 +552,29 @@ This creates a clean, flat package structure in node_modules.
615
552
  await runCommand(publishCmd, tempDir, true)
616
553
  }
617
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
+
562
+ if (shouldTag) {
563
+ console.log(`๐Ÿท๏ธ Creating git tag v${newVersion}`)
564
+ await runCommand(`git tag -a v${newVersion} -m "${commitMessage}"`, rootPath, verbose)
565
+ }
566
+
567
+ if (shouldPush) {
568
+ console.log(`๐Ÿ“ค Pushing to remote...`)
569
+ await runCommand('git push', rootPath, verbose)
570
+
571
+ if (shouldTag) {
572
+ console.log(`๐Ÿ”– Pushing tags...`)
573
+ await runCommand('git push --tags', rootPath, verbose)
574
+ }
575
+ }
576
+ }
577
+
618
578
  // Cleanup
619
579
  if (!noCleanup) {
620
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.7.0",
3
+ "version": "1.8.0",
4
4
  "description": "Universal build tool for itty libraries",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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({