itty-packager 1.0.0 → 1.0.2

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 ADDED
@@ -0,0 +1,219 @@
1
+ <br />
2
+
3
+ <p>
4
+ <a href="https://itty.dev/itty-packager" target="_blank">
5
+ <img src="https://github.com/user-attachments/assets/placeholder-image" alt="itty-packager" height="120" />
6
+ </a>
7
+ </p>
8
+
9
+ [![Version](https://img.shields.io/npm/v/itty-packager.svg?style=flat-square)](https://npmjs.com/package/itty-packager)
10
+ [![Bundle Size](https://deno.bundlejs.com/?q=itty-packager&badge&badge-style=flat-square)](https://deno.bundlejs.com/?q=itty-packager)
11
+ [![Coverage Status](https://img.shields.io/coveralls/github/kwhitley/itty-packager?style=flat-square)](https://coveralls.io/github/kwhitley/itty-packager)
12
+ [![Issues](https://img.shields.io/github/issues/kwhitley/itty-packager?style=flat-square)](https://github.com/kwhitley/itty-packager/issues)
13
+ [![Discord](https://img.shields.io/discord/832353585802903572?label=Discord&logo=Discord&style=flat-square&logoColor=fff)](https://discord.gg/53vyrZAu9u)
14
+
15
+ ### [Documentation](https://itty.dev) &nbsp;| &nbsp; [Discord](https://discord.gg/53vyrZAu9u)
16
+
17
+ ---
18
+
19
+ # Universal toolkit for itty libraries
20
+
21
+ Zero-config build, lint, and publish workflows for TypeScript libraries.
22
+
23
+ ## Features
24
+
25
+ - **🔨 Build** - TypeScript compilation with Rollup, minification, and snippet generation
26
+ - **🔍 Lint** - Built-in ESLint configuration with TypeScript support and smart extending
27
+ - **📦 Publish** - Automated version bumping, clean package extraction, and npm publishing
28
+ - **⚡ Zero Config** - Works out of the box, customize only what you need
29
+ - **🎯 Consistent** - Unified tooling across all itty projects
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ npm install --save-dev itty-packager
35
+ ```
36
+
37
+ ## Quick Start
38
+
39
+ Add to your `package.json` scripts:
40
+
41
+ ```json
42
+ {
43
+ "scripts": {
44
+ "build": "itty build",
45
+ "lint": "itty lint",
46
+ "publish": "itty publish"
47
+ }
48
+ }
49
+ ```
50
+
51
+ ## Commands
52
+
53
+ ### `itty build`
54
+
55
+ Build your TypeScript library with Rollup, TypeScript compilation, and optional minification.
56
+
57
+ **Usage:** `itty build [options]`
58
+
59
+ **Options:**
60
+ - `-f, --from <dir>` - Source directory (default: `src`)
61
+ - `-o, --out <dir>` - Output directory (default: `dist`)
62
+ - `-c, --copy <files>` - Files to copy to output (comma-separated)
63
+ - `--sourcemap` - Generate source maps (default: `false`)
64
+ - `--hybrid` - Build both ESM and CJS (default: ESM only)
65
+ - `--minify` - Minify output with terser (default: `true`)
66
+ - `--no-minify` - Skip minification
67
+ - `-s, --snippet <name>` - Generate snippet file for README injection
68
+ - `-h, --help` - Show help
69
+
70
+ **Default Behavior:**
71
+ - Compiles all TypeScript files from `src/` to `dist/`
72
+ - Generates ESM (`.mjs`) output only
73
+ - Minifies output by default
74
+ - Updates `package.json` exports with correct paths
75
+ - Single file exports map to root export, multiple files get individual exports
76
+
77
+ **Examples:**
78
+ ```bash
79
+ itty build # Basic ESM build, minified
80
+ itty build --hybrid --sourcemap # Build both ESM/CJS with sourcemaps
81
+ itty build --snippet=connect # Build with snippet generation for README
82
+ itty build --from=lib --out=build # Build from lib/ to build/
83
+ ```
84
+
85
+ ### `itty lint`
86
+
87
+ Lint your code with ESLint using built-in TypeScript configuration or your local config.
88
+
89
+ **Usage:** `itty lint [files/directories] [options]`
90
+
91
+ **Options:**
92
+ - `--fix` - Automatically fix problems
93
+ - `--max-warnings <n>` - Number of warnings to trigger nonzero exit code
94
+ - `-q, --quiet` - Report errors only
95
+ - `-f, --format <format>` - Output format (stylish, compact, json, etc.)
96
+ - `-h, --help` - Show help
97
+
98
+ **Default Behavior:**
99
+ - Uses built-in TypeScript ESLint config if no local config found
100
+ - Lints entire project excluding `node_modules/`, `dist/`, `build/`, `coverage/`
101
+ - Local configs (`.eslintrc.*`, `eslint.config.*`) override built-in config
102
+ - All ESLint dependencies provided by itty-packager
103
+
104
+ **Config Extension:**
105
+ Create `eslint.config.mjs` to extend the built-in config:
106
+ ```javascript
107
+ import { createConfig } from 'itty-packager/lib/configs/createConfig.mjs'
108
+
109
+ export default createConfig({
110
+ rules: {
111
+ 'no-console': 'off', // Project-specific overrides
112
+ }
113
+ })
114
+ ```
115
+
116
+ **Examples:**
117
+ ```bash
118
+ itty lint # Lint entire project with smart exclusions
119
+ itty lint src # Lint only src directory
120
+ itty lint --fix # Lint and auto-fix issues
121
+ itty lint --format=json # Output results in JSON format
122
+ ```
123
+
124
+ ### `itty publish`
125
+
126
+ Version bump and publish your package to npm with clean, flat package structure.
127
+
128
+ **Usage:** `itty publish [options]`
129
+
130
+ **Version Options (default: patch):**
131
+ - `--major` - Major release X.#.# for breaking changes
132
+ - `--minor` - Minor release #.X.# for feature additions
133
+ - `--patch` - Patch release #.#.X for bug fixes (default)
134
+ - `--type <type>` - Custom release type (alpha, beta, rc, etc.)
135
+
136
+ **Publish Options:**
137
+ - `--src <dir>` - Source directory to publish from (default: `dist`)
138
+ - `--dest <dir>` - Temporary directory for publishing (default: `.dist`)
139
+ - `--dry-run` - Build and prepare but do not publish
140
+ - `--no-cleanup` - Leave temporary directory after publishing
141
+ - `--public` - Publish as public package (`--access=public`)
142
+ - `--no-license` - Do not copy LICENSE file to published package
143
+ - `--no-changelog` - Do not copy CHANGELOG.md file to published package
144
+
145
+ **Git Options:**
146
+ - `--tag` - Create git tag for release
147
+ - `--push` - Push changes and tags to git remote
148
+ - `--no-git` - Skip all git operations
149
+
150
+ **Default Behavior:**
151
+ - Defaults to patch version bump if no type specified
152
+ - Extracts build artifacts to temporary directory
153
+ - Copies root files: `README.md`, `LICENSE`, `CHANGELOG.md`, `.npmrc` (if they exist)
154
+ - Transforms package.json paths (e.g., `./dist/file.mjs` → `./file.mjs`)
155
+ - Creates clean, flat package structure in node_modules
156
+
157
+ **Examples:**
158
+ ```bash
159
+ itty publish # Patch bump and publish from dist/ (default)
160
+ itty publish --minor --tag # Minor bump, publish, and create git tag
161
+ itty publish --type=alpha # Pre-release alpha version
162
+ itty publish --dry-run # Test the publish process
163
+ itty publish --no-license # Publish without copying LICENSE file
164
+ ```
165
+
166
+ ## Package Structure
167
+
168
+ The publish command creates a clean package structure by:
169
+
170
+ 1. **Extracting build artifacts** from your `dist/` directory to package root
171
+ 2. **Copying essential files** like README, LICENSE, CHANGELOG
172
+ 3. **Transforming paths** in package.json to point to root-level files
173
+ 4. **Publishing the clean structure** so users get flat imports
174
+
175
+ **Before (in your project):**
176
+ ```
177
+ package.json exports: "./dist/connect.mjs"
178
+ dist/connect.mjs
179
+ README.md
180
+ LICENSE
181
+ ```
182
+
183
+ **After (in node_modules):**
184
+ ```
185
+ package.json exports: "./connect.mjs"
186
+ connect.mjs
187
+ README.md
188
+ LICENSE
189
+ ```
190
+
191
+ ## Configuration
192
+
193
+ ### ESLint
194
+
195
+ The built-in ESLint config includes:
196
+ - TypeScript support with `@typescript-eslint`
197
+ - Sensible defaults for itty projects
198
+ - Unix line endings, single quotes, no semicolons
199
+ - Disabled rules: `no-empty-function`, `no-explicit-any`, `ban-types`, `ban-ts-comment`
200
+
201
+ Override by creating `eslint.config.mjs` in your project root.
202
+
203
+ ### Package.json
204
+
205
+ Add itty-packager to your scripts for easy access:
206
+
207
+ ```json
208
+ {
209
+ "scripts": {
210
+ "build": "itty build --snippet=mylib --hybrid",
211
+ "lint": "itty lint",
212
+ "publish": "itty publish --tag --push"
213
+ }
214
+ }
215
+ ```
216
+
217
+ ## License
218
+
219
+ MIT
package/bin/itty.js CHANGED
@@ -4,8 +4,9 @@ import { parseArgs } from 'node:util'
4
4
 
5
5
  const subcommands = {
6
6
  build: () => import('../lib/commands/build.js').then(m => m.buildCommand),
7
+ lint: () => import('../lib/commands/lint.js').then(m => m.lintCommand),
8
+ publish: () => import('../lib/commands/publish.js').then(m => m.publishCommand),
7
9
  // Future subcommands can be added here:
8
- // release: () => import('../lib/commands/release.js').then(m => m.releaseCommand),
9
10
  // deploy: () => import('../lib/commands/deploy.js').then(m => m.deployCommand),
10
11
  }
11
12
 
@@ -71,6 +72,8 @@ Usage: itty <subcommand> [options]
71
72
 
72
73
  Subcommands:
73
74
  build Build your library with rollup and typescript
75
+ lint Lint your code with ESLint
76
+ publish Version and publish your package to npm
74
77
 
75
78
  Global Options:
76
79
  -h, --help Show help
@@ -79,6 +82,10 @@ Global Options:
79
82
  Examples:
80
83
  itty build --snippet=connect --hybrid # Build with snippet and CJS support
81
84
  itty build --sourcemap --no-minify # Build with sourcemaps, no minification
85
+ itty lint # Lint entire project (smart exclusions)
86
+ itty lint src # Lint only the src directory
87
+ itty lint --fix # Lint and fix issues automatically
88
+ itty publish --patch # Version bump and publish from dist/
82
89
  itty build --help # Show build-specific help
83
90
 
84
91
  Run 'itty <subcommand> --help' for subcommand-specific options.
@@ -0,0 +1,26 @@
1
+ import { createConfig } from './lib/configs/createConfig.mjs'
2
+
3
+ export default createConfig({
4
+ rules: {
5
+ // Allow console statements in CLI tools
6
+ 'no-console': 'off',
7
+
8
+ // Allow process global in Node.js CLI
9
+ 'no-undef': 'off',
10
+
11
+ // Allow useless escapes in regex patterns
12
+ 'no-useless-escape': 'off',
13
+ },
14
+
15
+ languageOptions: {
16
+ globals: {
17
+ // Node.js globals
18
+ process: 'readonly',
19
+ console: 'readonly',
20
+ Buffer: 'readonly',
21
+ __dirname: 'readonly',
22
+ __filename: 'readonly',
23
+ global: 'readonly',
24
+ }
25
+ }
26
+ })
package/lib/builder.js CHANGED
@@ -14,7 +14,7 @@ export async function build(options = {}) {
14
14
  const {
15
15
  from = 'src',
16
16
  out = 'dist',
17
- copy: copyFiles = 'LICENSE',
17
+ copy: copyFiles,
18
18
  snippet,
19
19
  sourcemap = false,
20
20
  hybrid = false,
@@ -207,7 +207,7 @@ async function injectSnippet(snippetName, outDir) {
207
207
  if (await fs.pathExists(readmePath)) {
208
208
  const readme = await fs.readFile(readmePath, 'utf-8')
209
209
  const newReadme = readme.replace(
210
- /(<!-- BEGIN SNIPPET -->[\r\n]+```(?:js|ts)[\r\n]).*?([\r\n]```[\r\n]+<!-- END SNIPPET -->)/s,
210
+ /(<!-- BEGIN SNIPPET -->[\r\n]+```(?:js|ts)[\r\n]?).*?([\r\n]?```[\r\n]+<!-- END SNIPPET -->)/s,
211
211
  `$1${transformed}$2`
212
212
  )
213
213
 
@@ -5,11 +5,6 @@ export async function buildCommand(args) {
5
5
  const { values: buildArgs } = parseArgs({
6
6
  args,
7
7
  options: {
8
- snippet: {
9
- type: 'string',
10
- short: 's',
11
- description: 'Generate snippet file for README injection'
12
- },
13
8
  from: {
14
9
  type: 'string',
15
10
  short: 'f',
@@ -25,8 +20,7 @@ export async function buildCommand(args) {
25
20
  copy: {
26
21
  type: 'string',
27
22
  short: 'c',
28
- default: 'LICENSE',
29
- description: 'Files to copy to output (default: LICENSE)'
23
+ description: 'Files to copy to output (comma-separated)'
30
24
  },
31
25
  sourcemap: {
32
26
  type: 'boolean',
@@ -44,6 +38,11 @@ export async function buildCommand(args) {
44
38
  type: 'boolean',
45
39
  description: 'Skip minification'
46
40
  },
41
+ snippet: {
42
+ type: 'string',
43
+ short: 's',
44
+ description: 'Generate snippet file for README injection'
45
+ },
47
46
  help: {
48
47
  type: 'boolean',
49
48
  short: 'h',
@@ -60,22 +59,22 @@ itty build - Build your library with rollup and typescript
60
59
  Usage: itty build [options]
61
60
 
62
61
  Options:
63
- -s, --snippet <name> Generate snippet file for README injection
64
62
  -f, --from <dir> Source directory (default: src)
65
63
  -o, --out <dir> Output directory (default: dist)
66
- -c, --copy <files> Files to copy to output (default: LICENSE)
64
+ -c, --copy <files> Files to copy to output (comma-separated)
67
65
  --sourcemap Generate source maps (default: false)
68
66
  --hybrid Build both ESM and CJS (default: ESM only)
69
67
  --minify Minify output with terser (default: true)
70
68
  --no-minify Skip minification
69
+ -s, --snippet <name> Generate snippet file for README injection
71
70
  -h, --help Show help
72
71
 
73
72
  Examples:
74
73
  itty build # Build ESM only, minified, no sourcemaps
75
- itty build --snippet=connect # Build with connect snippet generation
76
74
  itty build --hybrid --sourcemap # Build both ESM/CJS with sourcemaps
77
75
  itty build --no-minify # Build without minification
78
76
  itty build --from=lib --out=build # Build from lib/ to build/
77
+ itty build --snippet=connect # Build with connect snippet generation
79
78
  `)
80
79
  return
81
80
  }
@@ -0,0 +1,174 @@
1
+ import { parseArgs } from 'node:util'
2
+ import { spawn } from 'node:child_process'
3
+ import path from 'node:path'
4
+ import { fileURLToPath } from 'node:url'
5
+ import fs from 'fs-extra'
6
+
7
+ const __filename = fileURLToPath(import.meta.url)
8
+ const __dirname = path.dirname(__filename)
9
+
10
+ export async function lintCommand(args) {
11
+ const { values: lintArgs, positionals } = parseArgs({
12
+ args,
13
+ options: {
14
+ fix: {
15
+ type: 'boolean',
16
+ description: 'Automatically fix problems'
17
+ },
18
+ 'max-warnings': {
19
+ type: 'string',
20
+ description: 'Number of warnings to trigger nonzero exit code'
21
+ },
22
+ quiet: {
23
+ type: 'boolean',
24
+ short: 'q',
25
+ description: 'Report errors only'
26
+ },
27
+ format: {
28
+ type: 'string',
29
+ short: 'f',
30
+ description: 'Output format (stylish, compact, json, etc.)'
31
+ },
32
+ help: {
33
+ type: 'boolean',
34
+ short: 'h',
35
+ description: 'Show help'
36
+ }
37
+ },
38
+ allowPositionals: true,
39
+ strict: false
40
+ })
41
+
42
+ if (lintArgs.help) {
43
+ console.log(`
44
+ itty lint - Lint your code with ESLint
45
+
46
+ Usage: itty lint [files/directories] [options]
47
+
48
+ Options:
49
+ --fix Automatically fix problems
50
+ --max-warnings <n> Number of warnings to trigger nonzero exit code
51
+ -q, --quiet Report errors only
52
+ -f, --format <format> Output format (stylish, compact, json, etc.)
53
+ -h, --help Show help
54
+
55
+ Examples:
56
+ itty lint # Lint entire project (excludes node_modules, dist, build, etc.)
57
+ itty lint src # Lint only src directory
58
+ itty lint --fix # Lint entire project and auto-fix issues
59
+ itty lint src --quiet # Lint src directory, show errors only
60
+ itty lint --format=json # Lint entire project, output in JSON format
61
+
62
+ Note:
63
+ - When no paths are specified, lints entire project excluding common build directories
64
+ - Uses built-in TypeScript ESLint config if no local config found
65
+ - Local configs (eslint.config.mjs, .eslintrc.*, etc.) will override built-in config
66
+ - To extend built-in config: import { createConfig } from 'itty-packager/lib/configs/createConfig.mjs'
67
+ - Use specific paths to override the default exclusions
68
+ `)
69
+ return
70
+ }
71
+
72
+ // Check for local ESLint configs
73
+ const cwd = process.cwd()
74
+ const localConfigFiles = [
75
+ 'eslint.config.mjs',
76
+ 'eslint.config.js',
77
+ '.eslintrc.mjs',
78
+ '.eslintrc.js',
79
+ '.eslintrc.json',
80
+ '.eslintrc'
81
+ ]
82
+
83
+ const hasLocalConfig = localConfigFiles.some(file =>
84
+ fs.existsSync(path.join(cwd, file))
85
+ )
86
+
87
+ // Find itty-packager's path and config
88
+ const packagerPath = path.resolve(__dirname, '../../')
89
+ const builtinConfig = path.join(packagerPath, 'lib', 'configs', 'eslint.config.mjs')
90
+
91
+ // Check if we're in development (has node_modules) or published (use npx)
92
+ const isDevMode = await fs.pathExists(path.join(packagerPath, 'node_modules'))
93
+ const eslintBinary = isDevMode
94
+ ? path.join(packagerPath, 'node_modules', '.bin', 'eslint')
95
+ : 'eslint' // Use npx approach for published version
96
+
97
+ const eslintArgs = []
98
+
99
+ // Use built-in config if no local config exists
100
+ if (!hasLocalConfig) {
101
+ console.log(`🔧 Using built-in ESLint config (no local config found)`)
102
+ eslintArgs.push('--config', builtinConfig)
103
+ } else {
104
+ console.log(`🔧 Using local ESLint config`)
105
+ }
106
+
107
+ // Add positional arguments (files/directories to lint)
108
+ if (positionals.length > 0) {
109
+ eslintArgs.push(...positionals)
110
+ } else {
111
+ // Default to all JS/TS files, excluding common build directories
112
+ eslintArgs.push(
113
+ '.',
114
+ '--ignore-pattern', 'node_modules/',
115
+ '--ignore-pattern', 'dist/',
116
+ '--ignore-pattern', 'build/',
117
+ '--ignore-pattern', 'coverage/',
118
+ '--ignore-pattern', '*.min.js',
119
+ '--ignore-pattern', '*.bundle.js'
120
+ )
121
+ }
122
+
123
+ // Add flags
124
+ if (lintArgs.fix) {
125
+ eslintArgs.push('--fix')
126
+ }
127
+
128
+ if (lintArgs['max-warnings']) {
129
+ eslintArgs.push('--max-warnings', lintArgs['max-warnings'])
130
+ }
131
+
132
+ if (lintArgs.quiet) {
133
+ eslintArgs.push('--quiet')
134
+ }
135
+
136
+ if (lintArgs.format) {
137
+ eslintArgs.push('--format', lintArgs.format)
138
+ }
139
+
140
+ console.log(`🔍 Linting with ESLint...`)
141
+
142
+ // Run ESLint
143
+ return new Promise((resolve, reject) => {
144
+ const useNpx = !isDevMode
145
+ const command = useNpx ? 'npx' : eslintBinary
146
+ const args = useNpx ? ['eslint', ...eslintArgs] : eslintArgs
147
+
148
+ // Set up environment with access to itty-packager's node_modules for ESLint plugins
149
+ const env = {
150
+ ...process.env,
151
+ NODE_PATH: `${path.join(packagerPath, 'node_modules')}:${process.env.NODE_PATH || ''}`
152
+ }
153
+
154
+ const eslint = spawn(command, args, {
155
+ stdio: 'inherit',
156
+ cwd: process.cwd(),
157
+ shell: true,
158
+ env
159
+ })
160
+
161
+ eslint.on('close', (code) => {
162
+ if (code === 0) {
163
+ console.log('✅ Linting completed successfully')
164
+ resolve()
165
+ } else {
166
+ reject(new Error(`ESLint exited with code ${code}`))
167
+ }
168
+ })
169
+
170
+ eslint.on('error', (error) => {
171
+ reject(new Error(`Failed to run ESLint: ${error.message}`))
172
+ })
173
+ })
174
+ }
@@ -0,0 +1,368 @@
1
+ import { parseArgs } from 'node:util'
2
+ import { spawn } from 'node:child_process'
3
+ import fs from 'fs-extra'
4
+ import path from 'node:path'
5
+
6
+ const SEMVER_TYPES = ['major', 'minor', 'patch']
7
+
8
+ function transformPackageExports(pkg, srcDir) {
9
+ // Transform package.json exports to remove srcDir prefix from paths
10
+ if (pkg.exports) {
11
+ const transformPath = (exportPath) => {
12
+ if (typeof exportPath === 'string' && exportPath.startsWith(`./${srcDir}/`)) {
13
+ return exportPath.replace(`./${srcDir}/`, './')
14
+ }
15
+ return exportPath
16
+ }
17
+
18
+ const transformExportObj = (exportObj) => {
19
+ if (typeof exportObj === 'string') {
20
+ return transformPath(exportObj)
21
+ }
22
+
23
+ if (typeof exportObj === 'object' && exportObj !== null) {
24
+ const transformed = {}
25
+ for (const [key, value] of Object.entries(exportObj)) {
26
+ if (typeof value === 'string') {
27
+ transformed[key] = transformPath(value)
28
+ } else if (typeof value === 'object') {
29
+ transformed[key] = transformExportObj(value)
30
+ } else {
31
+ transformed[key] = value
32
+ }
33
+ }
34
+ return transformed
35
+ }
36
+
37
+ return exportObj
38
+ }
39
+
40
+ const transformedExports = {}
41
+ for (const [key, value] of Object.entries(pkg.exports)) {
42
+ transformedExports[key] = transformExportObj(value)
43
+ }
44
+
45
+ return { ...pkg, exports: transformedExports }
46
+ }
47
+
48
+ return pkg
49
+ }
50
+
51
+ function versionBump(currentVersion, type) {
52
+ const parts = currentVersion.split('.').map(Number)
53
+
54
+ switch (type) {
55
+ case 'major':
56
+ return `${parts[0] + 1}.0.0`
57
+ case 'minor':
58
+ return `${parts[0]}.${parts[1] + 1}.0`
59
+ case 'patch':
60
+ return `${parts[0]}.${parts[1]}.${parts[2] + 1}`
61
+ default:
62
+ // For pre-release versions like alpha, beta, rc
63
+ if (currentVersion.includes(`-${type}`)) {
64
+ // Increment the pre-release number
65
+ const [base, prerelease] = currentVersion.split(`-${type}.`)
66
+ const prereleaseNum = parseInt(prerelease) || 0
67
+ return `${base}-${type}.${prereleaseNum + 1}`
68
+ } else {
69
+ // Add pre-release to current version
70
+ return `${currentVersion}-${type}.0`
71
+ }
72
+ }
73
+ }
74
+
75
+ async function runCommand(command, cwd = process.cwd()) {
76
+ return new Promise((resolve, reject) => {
77
+ const [cmd, ...args] = command.split(' ')
78
+ const proc = spawn(cmd, args, {
79
+ stdio: 'inherit',
80
+ cwd,
81
+ shell: true
82
+ })
83
+
84
+ proc.on('close', (code) => {
85
+ if (code === 0) {
86
+ resolve()
87
+ } else {
88
+ reject(new Error(`Command failed with exit code ${code}: ${command}`))
89
+ }
90
+ })
91
+
92
+ proc.on('error', (error) => {
93
+ reject(new Error(`Failed to run command: ${error.message}`))
94
+ })
95
+ })
96
+ }
97
+
98
+ export async function publishCommand(args) {
99
+ const { values: publishArgs } = parseArgs({
100
+ args,
101
+ options: {
102
+ major: {
103
+ type: 'boolean',
104
+ description: 'Major release X.#.# for breaking changes'
105
+ },
106
+ minor: {
107
+ type: 'boolean',
108
+ description: 'Minor release #.X.# for feature additions'
109
+ },
110
+ patch: {
111
+ type: 'boolean',
112
+ description: 'Patch release #.#.X for bug fixes'
113
+ },
114
+ type: {
115
+ type: 'string',
116
+ description: 'Custom release type (alpha, beta, rc, etc.)'
117
+ },
118
+ src: {
119
+ type: 'string',
120
+ default: 'dist',
121
+ description: 'Source directory to publish from (default: dist)'
122
+ },
123
+ dest: {
124
+ type: 'string',
125
+ default: '.dist',
126
+ description: 'Temporary directory for publishing (default: .dist)'
127
+ },
128
+ 'dry-run': {
129
+ type: 'boolean',
130
+ description: 'Build and prepare but do not publish'
131
+ },
132
+ 'no-cleanup': {
133
+ type: 'boolean',
134
+ description: 'Leave temporary directory after publishing'
135
+ },
136
+ public: {
137
+ type: 'boolean',
138
+ description: 'Publish as public package (--access=public)'
139
+ },
140
+ tag: {
141
+ type: 'boolean',
142
+ description: 'Create git tag for release'
143
+ },
144
+ push: {
145
+ type: 'boolean',
146
+ description: 'Push changes and tags to git remote'
147
+ },
148
+ 'no-git': {
149
+ type: 'boolean',
150
+ description: 'Skip all git operations'
151
+ },
152
+ 'no-license': {
153
+ type: 'boolean',
154
+ description: 'Do not copy LICENSE file to published package'
155
+ },
156
+ 'no-changelog': {
157
+ type: 'boolean',
158
+ description: 'Do not copy CHANGELOG.md file to published package'
159
+ },
160
+ help: {
161
+ type: 'boolean',
162
+ short: 'h',
163
+ description: 'Show help'
164
+ }
165
+ },
166
+ allowPositionals: false
167
+ })
168
+
169
+ if (publishArgs.help) {
170
+ console.log(`
171
+ itty publish - Version and publish your package to npm
172
+
173
+ Usage: itty publish [options]
174
+
175
+ Version Options (default: patch):
176
+ --major Major release X.#.# for breaking changes
177
+ --minor Minor release #.X.# for feature additions
178
+ --patch Patch release #.#.X for bug fixes (default)
179
+ --type <type> Custom release type (alpha, beta, rc, etc.)
180
+
181
+ Publish Options:
182
+ --src <dir> Source directory to publish from (default: dist)
183
+ --dest <dir> Temporary directory for publishing (default: .dist)
184
+ --dry-run Build and prepare but do not publish
185
+ --no-cleanup Leave temporary directory after publishing
186
+ --public Publish as public package (--access=public)
187
+ --no-license Do not copy LICENSE file to published package
188
+ --no-changelog Do not copy CHANGELOG.md file to published package
189
+
190
+ Git Options:
191
+ --tag Create git tag for release
192
+ --push Push changes and tags to git remote
193
+ --no-git Skip all git operations
194
+ -h, --help Show help
195
+
196
+ Examples:
197
+ itty publish # Patch version bump and publish from dist/ (default)
198
+ itty publish --minor --tag # Minor bump, publish, and create git tag
199
+ itty publish --type=alpha # Pre-release alpha version
200
+ itty publish --src=lib # Publish from lib/ instead of dist/
201
+ itty publish --dry-run # Test the publish process
202
+
203
+ Note: This command extracts your build artifacts to a temporary directory,
204
+ adds root files (README.md, LICENSE, etc.), and publishes from there.
205
+ This creates a clean, flat package structure in node_modules.
206
+ `)
207
+ return
208
+ }
209
+
210
+ // Determine release type (default to patch)
211
+ const releaseType = publishArgs.major ? 'major'
212
+ : publishArgs.minor ? 'minor'
213
+ : publishArgs.patch ? 'patch'
214
+ : publishArgs.type ? publishArgs.type
215
+ : 'patch' // Default to patch
216
+
217
+ const rootPath = process.cwd()
218
+ const srcDir = path.join(rootPath, publishArgs.src)
219
+
220
+ // Handle root publishing (src=.) by using a different temp directory structure
221
+ const isRootPublish = publishArgs.src === '.'
222
+ const tempDir = isRootPublish
223
+ ? path.join(path.dirname(rootPath), `.${path.basename(rootPath)}-dist`)
224
+ : path.join(rootPath, publishArgs.dest)
225
+ const dryRun = publishArgs['dry-run']
226
+ const noCleanup = publishArgs['no-cleanup']
227
+ const publicAccess = publishArgs.public
228
+ const shouldTag = publishArgs.tag
229
+ const shouldPush = publishArgs.push
230
+ const noGit = publishArgs['no-git']
231
+ const noLicense = publishArgs['no-license']
232
+ const noChangelog = publishArgs['no-changelog']
233
+
234
+ try {
235
+ // Read package.json
236
+ const pkgPath = path.join(rootPath, 'package.json')
237
+ const pkg = await fs.readJSON(pkgPath)
238
+ const newVersion = versionBump(pkg.version, releaseType)
239
+
240
+ console.log(`📦 Publishing ${pkg.name} v${pkg.version} → v${newVersion}`)
241
+ console.log(`📁 Source: ${publishArgs.src}/`)
242
+
243
+ // Check if source directory exists
244
+ if (!await fs.pathExists(srcDir)) {
245
+ throw new Error(`Source directory "${publishArgs.src}" does not exist. Run "itty build" first.`)
246
+ }
247
+
248
+ // Clean and create temp directory
249
+ console.log(`🧹 Preparing ${publishArgs.dest}/`)
250
+ await fs.emptyDir(tempDir)
251
+ await fs.ensureDir(tempDir)
252
+
253
+ // Copy source files to temp directory
254
+ console.log(`📋 Copying ${publishArgs.src}/ to ${path.relative(rootPath, tempDir)}/`)
255
+
256
+ const filter = (src) => {
257
+ // Always exclude node_modules
258
+ if (src.includes('node_modules')) return false
259
+
260
+ // For root publishing, exclude additional files
261
+ if (isRootPublish) {
262
+ const relativePath = path.relative(srcDir, src)
263
+ return !relativePath.startsWith('.git') &&
264
+ !relativePath.includes('.DS_Store') &&
265
+ !relativePath.includes('coverage/') &&
266
+ !relativePath.includes('.nyc_output/')
267
+ }
268
+
269
+ return true
270
+ }
271
+
272
+ await fs.copy(srcDir, tempDir, { filter })
273
+
274
+ // Copy root files that should be included in the package (only for non-root publishing)
275
+ if (!isRootPublish) {
276
+ const rootFiles = [
277
+ 'README.md', // Always copy README
278
+ '.npmrc' // Always copy .npmrc if it exists
279
+ ]
280
+
281
+ // Add optional files based on flags
282
+ if (!noLicense) rootFiles.push('LICENSE')
283
+ if (!noChangelog) rootFiles.push('CHANGELOG.md')
284
+
285
+ for (const file of rootFiles) {
286
+ const srcFile = path.join(rootPath, file)
287
+ const destFile = path.join(tempDir, file)
288
+
289
+ if (await fs.pathExists(srcFile)) {
290
+ console.log(`📄 Copying ${file}`)
291
+ await fs.copy(srcFile, destFile)
292
+ }
293
+ }
294
+ }
295
+
296
+ // Update package.json in temp directory with transformed paths
297
+ const updatedPkg = isRootPublish
298
+ ? { ...pkg, version: newVersion } // No path transformation for root publishing
299
+ : transformPackageExports({ ...pkg, version: newVersion }, publishArgs.src)
300
+ const tempPkgPath = path.join(tempDir, 'package.json')
301
+
302
+ const transformMessage = isRootPublish ? '' : ' (transforming paths)'
303
+ console.log(`📝 Updating package.json to v${newVersion}${transformMessage}`)
304
+ await fs.writeJSON(tempPkgPath, updatedPkg, { spaces: 2 })
305
+
306
+ if (dryRun) {
307
+ console.log('🧪 Dry run - skipping publish')
308
+ } else {
309
+ // Publish from temp directory
310
+ console.log(`🚀 Publishing to npm...`)
311
+
312
+ const publishCmd = [
313
+ 'npm publish',
314
+ publicAccess ? '--access=public' : '',
315
+ SEMVER_TYPES.includes(releaseType) ? '' : `--tag=${releaseType}`
316
+ ].filter(Boolean).join(' ')
317
+
318
+ console.log(`Running: ${publishCmd}`)
319
+ await runCommand(publishCmd, tempDir)
320
+
321
+ // Update root package.json
322
+ console.log(`📝 Updating root package.json`)
323
+ await fs.writeJSON(pkgPath, updatedPkg, { spaces: 2 })
324
+ }
325
+
326
+ // Git operations
327
+ if (!noGit && !dryRun) {
328
+ if (shouldPush || shouldTag) {
329
+ console.log(`📋 Committing changes...`)
330
+ await runCommand('git add .', rootPath)
331
+ await runCommand(`git commit -m "released v${newVersion}"`, rootPath)
332
+ }
333
+
334
+ if (shouldTag) {
335
+ console.log(`🏷️ Creating git tag v${newVersion}`)
336
+ await runCommand(`git tag -a v${newVersion} -m "Release v${newVersion}"`, rootPath)
337
+ }
338
+
339
+ if (shouldPush) {
340
+ console.log(`📤 Pushing to remote...`)
341
+ await runCommand('git push', rootPath)
342
+
343
+ if (shouldTag) {
344
+ console.log(`📤 Pushing tags...`)
345
+ await runCommand('git push --tags', rootPath)
346
+ }
347
+ }
348
+ }
349
+
350
+ // Cleanup
351
+ if (!noCleanup) {
352
+ console.log(`🧹 Cleaning up ${publishArgs.dest}/`)
353
+ await fs.remove(tempDir)
354
+ }
355
+
356
+ console.log(`✅ Successfully published ${pkg.name}@${newVersion}`)
357
+
358
+ } catch (error) {
359
+ console.error(`❌ Publish failed: ${error.message}`)
360
+
361
+ // Cleanup on error
362
+ if (await fs.pathExists(tempDir) && !noCleanup) {
363
+ await fs.remove(tempDir)
364
+ }
365
+
366
+ throw error
367
+ }
368
+ }
@@ -0,0 +1,101 @@
1
+ import typescriptEslint from '@typescript-eslint/eslint-plugin'
2
+ import tsParser from '@typescript-eslint/parser'
3
+ import path from 'node:path'
4
+ import { fileURLToPath } from 'node:url'
5
+ import js from '@eslint/js'
6
+ import { FlatCompat } from '@eslint/eslintrc'
7
+
8
+ const __filename = fileURLToPath(import.meta.url)
9
+ const __dirname = path.dirname(__filename)
10
+ const compat = new FlatCompat({
11
+ baseDirectory: __dirname,
12
+ recommendedConfig: js.configs.recommended,
13
+ allConfig: js.configs.all
14
+ })
15
+
16
+ // Base configuration that all itty projects should use
17
+ const baseConfig = [...compat.extends(
18
+ 'eslint:recommended',
19
+ 'plugin:@typescript-eslint/eslint-recommended',
20
+ 'plugin:@typescript-eslint/recommended',
21
+ ), {
22
+ plugins: {
23
+ '@typescript-eslint': typescriptEslint,
24
+ },
25
+
26
+ languageOptions: {
27
+ parser: tsParser,
28
+ },
29
+
30
+ rules: {
31
+ '@typescript-eslint/no-empty-function': 'off',
32
+ '@typescript-eslint/no-explicit-any': 'off',
33
+ '@typescript-eslint/ban-types': 'off',
34
+ '@typescript-eslint/ban-ts-comment': 'off',
35
+ 'linebreak-style': ['error', 'unix'],
36
+ 'prefer-const': 'off',
37
+
38
+ quotes: ['error', 'single', {
39
+ allowTemplateLiterals: true,
40
+ }],
41
+
42
+ semi: ['error', 'never'],
43
+ },
44
+ }]
45
+
46
+ /**
47
+ * Create an ESLint config by extending the base itty configuration
48
+ * @param {Object} overrides - Configuration overrides
49
+ * @param {Object} overrides.rules - Additional or modified rules
50
+ * @param {Object} overrides.languageOptions - Language options to merge
51
+ * @param {Array} overrides.plugins - Additional plugins
52
+ * @param {Array} overrides.ignores - Files/patterns to ignore
53
+ * @returns {Array} ESLint flat config
54
+ */
55
+ export function createConfig(overrides = {}) {
56
+ const config = [...baseConfig]
57
+
58
+ if (overrides.rules || overrides.languageOptions || overrides.plugins || overrides.ignores) {
59
+ // Create a new config object that extends the base
60
+ const extendedConfig = {
61
+ ...config[config.length - 1], // Take the last config object from base
62
+ }
63
+
64
+ // Merge rules
65
+ if (overrides.rules) {
66
+ extendedConfig.rules = {
67
+ ...extendedConfig.rules,
68
+ ...overrides.rules
69
+ }
70
+ }
71
+
72
+ // Merge language options
73
+ if (overrides.languageOptions) {
74
+ extendedConfig.languageOptions = {
75
+ ...extendedConfig.languageOptions,
76
+ ...overrides.languageOptions
77
+ }
78
+ }
79
+
80
+ // Merge plugins
81
+ if (overrides.plugins) {
82
+ extendedConfig.plugins = {
83
+ ...extendedConfig.plugins,
84
+ ...overrides.plugins
85
+ }
86
+ }
87
+
88
+ // Add ignores
89
+ if (overrides.ignores) {
90
+ extendedConfig.ignores = overrides.ignores
91
+ }
92
+
93
+ // Replace the last config in the array
94
+ config[config.length - 1] = extendedConfig
95
+ }
96
+
97
+ return config
98
+ }
99
+
100
+ // Export the base config as default for direct use
101
+ export default baseConfig
@@ -0,0 +1,3 @@
1
+ import baseConfig from './createConfig.mjs'
2
+
3
+ export default baseConfig
@@ -0,0 +1,30 @@
1
+ // Template for extending the built-in itty-packager ESLint config
2
+ // Copy this to your project root as 'eslint.config.mjs' and customize as needed
3
+
4
+ import { createConfig } from 'itty-packager/lib/configs/createConfig.mjs'
5
+
6
+ export default createConfig({
7
+ // Add custom rules specific to your project
8
+ rules: {
9
+ // Example: Allow console statements
10
+ // 'no-console': 'off',
11
+
12
+ // Example: Require spaces around object braces
13
+ // 'object-curly-spacing': ['error', 'always'],
14
+
15
+ // Example: Prefer template literals over string concatenation
16
+ // 'prefer-template': 'error',
17
+ },
18
+
19
+ // Add project-specific ignores (in addition to defaults)
20
+ ignores: [
21
+ // 'test-fixtures/**',
22
+ // 'docs/**',
23
+ ]
24
+ })
25
+
26
+ // Available options for createConfig():
27
+ // - rules: Object with ESLint rule overrides
28
+ // - languageOptions: Parser and environment settings
29
+ // - plugins: Additional ESLint plugins
30
+ // - ignores: Array of file patterns to ignore
package/package.json CHANGED
@@ -1,26 +1,47 @@
1
1
  {
2
2
  "name": "itty-packager",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Universal build tool for itty libraries",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "itty": "./bin/itty.js"
8
8
  },
9
9
  "scripts": {
10
- "dev": "node bin/itty-packager.js",
11
- "test": "echo 'No tests yet'"
10
+ "lint": "bun bin/itty.js lint",
11
+ "test": "echo 'No tests yet'",
12
+ "release:dry": "bun bin/itty.js publish --patch --tag --dry-run --src=. --no-license --no-changelog",
13
+ "release": "bun bin/itty.js publish --patch --tag --push --src=. --no-license --no-changelog",
14
+ "release:minor": "bun bin/itty.js publish --minor --tag --push --src=. --no-license --no-changelog",
15
+ "release:major": "bun bin/itty.js publish --major --tag --push --src=. --no-license --no-changelog"
12
16
  },
13
17
  "keywords": [
14
18
  "build",
15
19
  "rollup",
16
20
  "typescript",
21
+ "eslint",
22
+ "publish",
23
+ "cli",
24
+ "tool",
17
25
  "itty"
18
26
  ],
19
27
  "author": "Kevin R. Whitley <krwhitley@gmail.com>",
20
28
  "license": "MIT",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/kwhitley/itty-packager.git"
32
+ },
33
+ "bugs": {
34
+ "url": "https://github.com/kwhitley/itty-packager/issues"
35
+ },
36
+ "homepage": "https://github.com/kwhitley/itty-packager#readme",
21
37
  "dependencies": {
38
+ "@eslint/eslintrc": "^3.1.0",
39
+ "@eslint/js": "^9.17.0",
22
40
  "@rollup/plugin-terser": "^0.4.4",
23
41
  "@rollup/plugin-typescript": "^11.1.6",
42
+ "@typescript-eslint/eslint-plugin": "^8.18.0",
43
+ "@typescript-eslint/parser": "^8.18.0",
44
+ "eslint": "^9.17.0",
24
45
  "fs-extra": "^11.2.0",
25
46
  "globby": "^14.1.0",
26
47
  "rimraf": "^6.0.1",
@@ -30,4 +51,4 @@
30
51
  "tslib": "^2.8.1",
31
52
  "typescript": "^5.7.2"
32
53
  }
33
- }
54
+ }