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 +91 -0
- package/lib/builder.js +217 -0
- package/lib/commands/build.js +96 -0
- package/package.json +33 -0
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
|
+
}
|