itty-packager 1.0.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/bin/itty.js ADDED
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { parseArgs } from 'node:util'
4
+
5
+ const subcommands = {
6
+ build: () => import('../lib/commands/build.js').then(m => m.buildCommand),
7
+ // Future subcommands can be added here:
8
+ // release: () => import('../lib/commands/release.js').then(m => m.releaseCommand),
9
+ // deploy: () => import('../lib/commands/deploy.js').then(m => m.deployCommand),
10
+ }
11
+
12
+ const { positionals, values: globalArgs } = parseArgs({
13
+ args: process.argv.slice(2),
14
+ options: {
15
+ help: {
16
+ type: 'boolean',
17
+ short: 'h',
18
+ description: 'Show help'
19
+ },
20
+ version: {
21
+ type: 'boolean',
22
+ short: 'v',
23
+ description: 'Show version'
24
+ }
25
+ },
26
+ allowPositionals: true,
27
+ strict: false
28
+ })
29
+
30
+ async function main() {
31
+ const subcommand = positionals[0]
32
+
33
+ if (globalArgs.version) {
34
+ const pkg = await import('../package.json', { with: { type: 'json' } })
35
+ console.log(pkg.default.version)
36
+ process.exit(0)
37
+ }
38
+
39
+ if (!subcommand) {
40
+ showHelp()
41
+ process.exit(0)
42
+ }
43
+
44
+ // If global help is requested but there's a subcommand, let the subcommand handle it
45
+ if (globalArgs.help && !subcommands[subcommand]) {
46
+ showHelp()
47
+ process.exit(0)
48
+ }
49
+
50
+ if (!subcommands[subcommand]) {
51
+ console.error(`❌ Unknown subcommand: ${subcommand}`)
52
+ console.error(`Available subcommands: ${Object.keys(subcommands).join(', ')}`)
53
+ process.exit(1)
54
+ }
55
+
56
+ try {
57
+ const commandModule = await subcommands[subcommand]()
58
+ const remainingArgs = process.argv.slice(3) // Remove 'node', 'itty.js', and subcommand
59
+ await commandModule(remainingArgs)
60
+ } catch (error) {
61
+ console.error('❌ Command failed:', error.message)
62
+ process.exit(1)
63
+ }
64
+ }
65
+
66
+ function showHelp() {
67
+ console.log(`
68
+ itty - Universal toolkit for itty libraries
69
+
70
+ Usage: itty <subcommand> [options]
71
+
72
+ Subcommands:
73
+ build Build your library with rollup and typescript
74
+
75
+ Global Options:
76
+ -h, --help Show help
77
+ -v, --version Show version
78
+
79
+ Examples:
80
+ itty build --snippet=connect --hybrid # Build with snippet and CJS support
81
+ itty build --sourcemap --no-minify # Build with sourcemaps, no minification
82
+ itty build --help # Show build-specific help
83
+
84
+ Run 'itty <subcommand> --help' for subcommand-specific options.
85
+ `)
86
+ }
87
+
88
+ main().catch(error => {
89
+ console.error('❌ Unexpected error:', error.message)
90
+ process.exit(1)
91
+ })
package/lib/builder.js ADDED
@@ -0,0 +1,217 @@
1
+ import terser from '@rollup/plugin-terser'
2
+ import typescript from '@rollup/plugin-typescript'
3
+ import fs from 'fs-extra'
4
+ import { globby } from 'globby'
5
+ import { rollup } from 'rollup'
6
+ import bundleSize from 'rollup-plugin-bundle-size'
7
+ import copy from 'rollup-plugin-copy'
8
+ import { rimraf } from 'rimraf'
9
+ import path from 'path'
10
+
11
+ const DEFAULT_IGNORE_PATTERNS = ['**/*.spec.ts', '**/types.ts', '**/*.ignore.*.ts']
12
+
13
+ export async function build(options = {}) {
14
+ const {
15
+ from = 'src',
16
+ out = 'dist',
17
+ copy: copyFiles = 'LICENSE',
18
+ snippet,
19
+ sourcemap = false,
20
+ hybrid = false,
21
+ minify = true
22
+ } = options
23
+
24
+ console.log(`📦 Building from ${from}/ to ${out}/`)
25
+
26
+ // Clean output directory
27
+ await rimraf(out)
28
+ await fs.ensureDir(out)
29
+
30
+ // Scan files to build
31
+ const pattern = `./${from}/*.ts`
32
+ const files = (await globby(pattern, {
33
+ ignore: DEFAULT_IGNORE_PATTERNS,
34
+ })).map(filePath => ({
35
+ path: filePath,
36
+ name: path.basename(filePath, '.ts'),
37
+ shortPath: filePath.replace(new RegExp(`(/${from})|(\.ts)`, 'g'), '').replace('./index', '.'),
38
+ esm: path.join(out, path.basename(filePath, '.ts') + '.mjs'),
39
+ cjs: path.join(out, path.basename(filePath, '.ts') + '.cjs'),
40
+ types: path.join(out, path.basename(filePath, '.ts') + '.d.ts'),
41
+ })).sort((a, b) => a.shortPath.toLowerCase() < b.shortPath.toLowerCase() ? -1 : 1)
42
+
43
+ if (files.length === 0) {
44
+ throw new Error(`No TypeScript files found in ${from}/`)
45
+ }
46
+
47
+ console.log(`📄 Found ${files.length} file(s):`, files.map(f => f.name).join(', '))
48
+
49
+ // Determine export strategy
50
+ const isSingleFile = files.length === 1
51
+ const pkg = await fs.readJSON('./package.json')
52
+
53
+ // Update package.json exports
54
+ if (isSingleFile) {
55
+ // Single file maps to root export
56
+ const file = files[0]
57
+ const exportObj = {
58
+ import: `./${out}/${path.basename(file.esm)}`,
59
+ types: `./${out}/${path.basename(file.types)}`,
60
+ }
61
+
62
+ // Add CJS export only if hybrid mode is enabled
63
+ if (hybrid) {
64
+ exportObj.require = `./${out}/${path.basename(file.cjs)}`
65
+ }
66
+
67
+ pkg.exports = {
68
+ '.': exportObj
69
+ }
70
+ } else {
71
+ // Multiple files get individual exports
72
+ pkg.exports = files.reduce((acc, file) => {
73
+ const exportObj = {
74
+ import: `./${out}/${path.basename(file.esm)}`,
75
+ types: `./${out}/${path.basename(file.types)}`,
76
+ }
77
+
78
+ // Add CJS export only if hybrid mode is enabled
79
+ if (hybrid) {
80
+ exportObj.require = `./${out}/${path.basename(file.cjs)}`
81
+ }
82
+
83
+ acc[file.shortPath] = exportObj
84
+ return acc
85
+ }, {})
86
+ }
87
+
88
+ // Write updated package.json
89
+ await fs.writeJSON('./package.json', pkg, { spaces: 2 })
90
+
91
+ // Build files with rollup
92
+ const builds = []
93
+
94
+ for (const file of files) {
95
+ // Determine outputs based on hybrid mode
96
+ const outputs = [
97
+ {
98
+ format: 'esm',
99
+ file: file.esm,
100
+ sourcemap,
101
+ }
102
+ ]
103
+
104
+ // Add CJS output only if hybrid mode is enabled
105
+ if (hybrid) {
106
+ outputs.push({
107
+ format: 'cjs',
108
+ file: file.cjs,
109
+ sourcemap,
110
+ })
111
+ }
112
+
113
+ // Build plugins array
114
+ const plugins = [
115
+ typescript({ sourceMap: sourcemap }),
116
+ bundleSize(),
117
+ ]
118
+
119
+ // Add terser only if minify is enabled
120
+ if (minify) {
121
+ plugins.splice(1, 0, terser()) // Insert terser before bundleSize
122
+ }
123
+
124
+ const config = {
125
+ input: file.path,
126
+ output: outputs,
127
+ plugins,
128
+ }
129
+
130
+ // Add copy plugin only to the first build to avoid conflicts
131
+ if (file === files[0] && copyFiles) {
132
+ const copyTargets = copyFiles.split(',').map(f => f.trim()).map(src => ({ src, dest: out }))
133
+ config.plugins.push(copy({ targets: copyTargets }))
134
+ }
135
+
136
+ builds.push(config)
137
+ }
138
+
139
+ // Add snippet build if requested
140
+ if (snippet) {
141
+ const snippetFile = files.find(f => f.name === snippet)
142
+ if (!snippetFile) {
143
+ throw new Error(`Snippet file "${snippet}" not found. Available files: ${files.map(f => f.name).join(', ')}`)
144
+ }
145
+
146
+ const snippetPlugins = [typescript()]
147
+
148
+ // Add terser to snippet only if minify is enabled
149
+ if (minify) {
150
+ snippetPlugins.push(terser())
151
+ }
152
+
153
+ builds.push({
154
+ input: snippetFile.path,
155
+ output: {
156
+ file: path.join(out, `${snippet}.snippet.js`),
157
+ format: 'esm',
158
+ name: snippet,
159
+ },
160
+ plugins: snippetPlugins,
161
+ })
162
+ }
163
+
164
+ // Execute all builds
165
+ for (const config of builds) {
166
+ const bundle = await rollup(config)
167
+
168
+ if (Array.isArray(config.output)) {
169
+ for (const output of config.output) {
170
+ await bundle.write(output)
171
+ }
172
+ } else {
173
+ await bundle.write(config.output)
174
+ }
175
+
176
+ await bundle.close()
177
+ }
178
+
179
+ // Handle README snippet injection if requested
180
+ if (snippet) {
181
+ await injectSnippet(snippet, out)
182
+ }
183
+
184
+ console.log(`✨ Build completed: ${files.length} file(s) built to ${out}/`)
185
+ }
186
+
187
+ async function injectSnippet(snippetName, outDir) {
188
+ const snippetPath = path.join(outDir, `${snippetName}.snippet.js`)
189
+
190
+ if (!await fs.pathExists(snippetPath)) {
191
+ console.warn(`⚠️ Snippet file not found: ${snippetPath}`)
192
+ return
193
+ }
194
+
195
+ const transformCode = code => code
196
+ .replace(/^let\s+(\w+)\s*=/, `let ${snippetName}=`)
197
+ .replace(/;export\s*{[^}]+};?\s*$/, ';')
198
+
199
+ const snippet = await fs.readFile(snippetPath, 'utf-8')
200
+ const transformed = transformCode(snippet).trim()
201
+
202
+ // Remove snippet file
203
+ await fs.unlink(snippetPath)
204
+
205
+ // Update README if it exists
206
+ const readmePath = './README.md'
207
+ if (await fs.pathExists(readmePath)) {
208
+ const readme = await fs.readFile(readmePath, 'utf-8')
209
+ const newReadme = readme.replace(
210
+ /(<!-- BEGIN SNIPPET -->[\r\n]+```(?:js|ts)[\r\n]).*?([\r\n]```[\r\n]+<!-- END SNIPPET -->)/s,
211
+ `$1${transformed}$2`
212
+ )
213
+
214
+ await fs.writeFile(readmePath, newReadme)
215
+ console.log(`📝 README.md updated with ${snippetName} snippet`)
216
+ }
217
+ }
@@ -0,0 +1,96 @@
1
+ import { parseArgs } from 'node:util'
2
+ import { build } from '../builder.js'
3
+
4
+ export async function buildCommand(args) {
5
+ const { values: buildArgs } = parseArgs({
6
+ args,
7
+ options: {
8
+ snippet: {
9
+ type: 'string',
10
+ short: 's',
11
+ description: 'Generate snippet file for README injection'
12
+ },
13
+ from: {
14
+ type: 'string',
15
+ short: 'f',
16
+ default: 'src',
17
+ description: 'Source directory (default: src)'
18
+ },
19
+ out: {
20
+ type: 'string',
21
+ short: 'o',
22
+ default: 'dist',
23
+ description: 'Output directory (default: dist)'
24
+ },
25
+ copy: {
26
+ type: 'string',
27
+ short: 'c',
28
+ default: 'LICENSE',
29
+ description: 'Files to copy to output (default: LICENSE)'
30
+ },
31
+ sourcemap: {
32
+ type: 'boolean',
33
+ description: 'Generate source maps (default: false)'
34
+ },
35
+ hybrid: {
36
+ type: 'boolean',
37
+ description: 'Build both ESM and CJS (default: ESM only)'
38
+ },
39
+ minify: {
40
+ type: 'boolean',
41
+ description: 'Minify output with terser (default: true)'
42
+ },
43
+ 'no-minify': {
44
+ type: 'boolean',
45
+ description: 'Skip minification'
46
+ },
47
+ help: {
48
+ type: 'boolean',
49
+ short: 'h',
50
+ description: 'Show help'
51
+ }
52
+ },
53
+ allowPositionals: false
54
+ })
55
+
56
+ if (buildArgs.help) {
57
+ console.log(`
58
+ itty build - Build your library with rollup and typescript
59
+
60
+ Usage: itty build [options]
61
+
62
+ Options:
63
+ -s, --snippet <name> Generate snippet file for README injection
64
+ -f, --from <dir> Source directory (default: src)
65
+ -o, --out <dir> Output directory (default: dist)
66
+ -c, --copy <files> Files to copy to output (default: LICENSE)
67
+ --sourcemap Generate source maps (default: false)
68
+ --hybrid Build both ESM and CJS (default: ESM only)
69
+ --minify Minify output with terser (default: true)
70
+ --no-minify Skip minification
71
+ -h, --help Show help
72
+
73
+ Examples:
74
+ itty build # Build ESM only, minified, no sourcemaps
75
+ itty build --snippet=connect # Build with connect snippet generation
76
+ itty build --hybrid --sourcemap # Build both ESM/CJS with sourcemaps
77
+ itty build --no-minify # Build without minification
78
+ itty build --from=lib --out=build # Build from lib/ to build/
79
+ `)
80
+ return
81
+ }
82
+
83
+ // Handle minify logic (default true, but --no-minify overrides)
84
+ if (buildArgs['no-minify']) {
85
+ buildArgs.minify = false
86
+ } else if (buildArgs.minify === undefined) {
87
+ buildArgs.minify = true
88
+ }
89
+
90
+ try {
91
+ await build(buildArgs)
92
+ console.log('✅ Build completed successfully')
93
+ } catch (error) {
94
+ throw new Error(`Build failed: ${error.message}`)
95
+ }
96
+ }
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "itty-packager",
3
+ "version": "1.0.0",
4
+ "description": "Universal build tool for itty libraries",
5
+ "type": "module",
6
+ "bin": {
7
+ "itty": "./bin/itty.js"
8
+ },
9
+ "scripts": {
10
+ "dev": "node bin/itty-packager.js",
11
+ "test": "echo 'No tests yet'"
12
+ },
13
+ "keywords": [
14
+ "build",
15
+ "rollup",
16
+ "typescript",
17
+ "itty"
18
+ ],
19
+ "author": "Kevin R. Whitley <krwhitley@gmail.com>",
20
+ "license": "MIT",
21
+ "dependencies": {
22
+ "@rollup/plugin-terser": "^0.4.4",
23
+ "@rollup/plugin-typescript": "^11.1.6",
24
+ "fs-extra": "^11.2.0",
25
+ "globby": "^14.1.0",
26
+ "rimraf": "^6.0.1",
27
+ "rollup": "^4.28.1",
28
+ "rollup-plugin-bundle-size": "^1.0.3",
29
+ "rollup-plugin-copy": "^3.5.0",
30
+ "tslib": "^2.8.1",
31
+ "typescript": "^5.7.2"
32
+ }
33
+ }