Compare commits

...

13 Commits

Author SHA1 Message Date
9cf173293d v2.9.0
Some checks failed
Default (tags) / security (push) Successful in 43s
Default (tags) / test (push) Failing after 39s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-02-24 19:00:42 +00:00
3a53fffe3d feat(exports): expose mod_custom, mod_output and interfaces from entry; make processSingleBundle public 2026-02-24 19:00:42 +00:00
8f129527d9 v2.8.4
Some checks failed
Default (tags) / security (push) Successful in 41s
Default (tags) / test (push) Failing after 36s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-02-24 17:03:04 +00:00
5d9052018e fix(): no changes — empty diff, nothing to commit 2026-02-24 17:03:04 +00:00
12c5655251 feat(includeFiles): support {from, to} object form for custom serve paths
includeFiles now accepts string | {from, to} entries. The object form
allows specifying a custom serve path (e.g. ./html/index.html -> index.html)
for base64ts bundled content.
2026-02-24 17:02:46 +00:00
801cab9001 v2.8.3
Some checks failed
Default (tags) / security (push) Successful in 31s
Default (tags) / test (push) Failing after 35s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-23 19:12:30 +00:00
971cb685a7 fix(mod_output): use pattern base dir when computing relative paths for files to serve 2026-01-23 19:12:30 +00:00
0900d1a605 v2.8.2
Some checks failed
Default (tags) / security (push) Successful in 39s
Default (tags) / test (push) Failing after 37s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-23 19:08:55 +00:00
f0fb99c8ae fix(mod_output): resolve absolute and relative entry.path correctly when adding files 2026-01-23 19:08:55 +00:00
6d88adcd1e v2.8.1
Some checks failed
Default (tags) / security (push) Successful in 40s
Default (tags) / test (push) Failing after 34s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-12 02:03:38 +00:00
4349571112 fix(readme): document maxLineLength option for base64ts output and add example and tip 2026-01-12 02:03:38 +00:00
b3080023ab v2.8.0
Some checks failed
Default (tags) / security (push) Successful in 34s
Default (tags) / test (push) Failing after 1m10s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-01-12 01:41:08 +00:00
8b6ae043a2 feat(tsbundle): add configurable maxLineLength for base64ts output and improve build/error handling in child builds 2026-01-12 01:41:08 +00:00
12 changed files with 188 additions and 67 deletions

View File

@@ -1,5 +1,47 @@
# Changelog
## 2026-02-24 - 2.9.0 - feat(exports)
expose mod_custom, mod_output and interfaces from entry; make processSingleBundle public
- Exported ./mod_custom, ./mod_output and ./interfaces from ts/index.ts to expose these modules in the public API.
- Changed processSingleBundle in ts/mod_custom/index.ts from private to public to allow programmatic invocation.
- Non-breaking API expansion; recommend a minor version bump.
## 2026-02-24 - 2.8.4 - fix()
no changes — empty diff, nothing to commit
- Diff contained no modifications; no release required
## 2026-01-23 - 2.8.3 - fix(mod_output)
use pattern base dir when computing relative paths for files to serve
- Compute relativePath using the pattern base directory (dirPath) instead of this.cwd to ensure correct web-serving paths for absolute or relative entry.path values.
- File changed: ts/mod_output/index.ts — replaces plugins.path.relative(this.cwd, fullPath) with plugins.path.relative(dirPath, fullPath) and adds clarifying comment.
## 2026-01-23 - 2.8.2 - fix(mod_output)
resolve absolute and relative entry.path correctly when adding files
- Add check for plugins.path.isAbsolute(entry.path) to avoid incorrectly joining absolute paths with dirPath
- Use entry.path directly when it's absolute, otherwise join with dirPath
- Ensures correct relativePath calculation and prevents invalid file reads
## 2026-01-12 - 2.8.1 - fix(readme)
document maxLineLength option for base64ts output and add example and tip
- Add documented `maxLineLength` configuration option (number, default 0 = unlimited) for `base64ts` output.
- Include example config showing `maxLineLength: 200`.
- Add a tip recommending setting `maxLineLength` to split long base64 strings when using AI tools with line-length limits.
## 2026-01-12 - 2.8.0 - feat(tsbundle)
add configurable maxLineLength for base64ts output and improve build/error handling in child builds
- Add optional maxLineLength?: number to IBundleConfig to control max characters per line for base64ts output (0 or undefined = unlimited).
- Support splitting base64 strings when maxLineLength is specified; generateTypeScript(maxLineLength?) and writeToFile(outputPath, maxLineLength?) updated to accept and apply this setting.
- Pass bundleConfig.maxLineLength through in mod_custom so base64ts output respects bundle configuration.
- Wrap TsBundle.build in mod_custom with try/catch to log failures and skip output handling when build fails.
- tsbundle.class now rejects the bundle promise when the child process exits with a non-zero status.
- mod_esbuild child process now awaits builds, exits with appropriate success/failure codes, and formats esbuild errors for clearer console output.
## 2026-01-12 - 2.7.4 - fix(deps)
bump @push.rocks/smartcli dependency to ^4.0.20

0
cli.js Normal file → Executable file
View File

View File

@@ -1,6 +1,6 @@
{
"name": "@git.zone/tsbundle",
"version": "2.7.4",
"version": "2.9.0",
"private": false,
"description": "a multi-bundler tool supporting esbuild, rolldown, and rspack for painless bundling of web projects",
"main": "dist_ts/index.js",

View File

@@ -91,6 +91,7 @@ tsbundle uses `npmextra.json` for configuration. Here's an example:
| `bundler` | `"esbuild"` \| `"rolldown"` \| `"rspack"` | `"esbuild"` | Which bundler to use |
| `production` | `boolean` | `false` | Enable minification |
| `includeFiles` | `string[]` | `[]` | Glob patterns for additional files (HTML, assets) |
| `maxLineLength` | `number` | `0` (unlimited) | For `base64ts` mode: max chars per line in output |
### Output Modes
@@ -108,6 +109,8 @@ export const files: { path: string; contentBase64: string }[] = [
];
```
**Tip:** If you're working with AI tools that have line length limitations, set `maxLineLength` (e.g., `200`) to split long base64 strings across multiple lines.
## Available Bundlers 🔧
tsbundle supports three modern bundlers:
@@ -241,7 +244,8 @@ Config:
"outputMode": "base64ts",
"bundler": "esbuild",
"production": true,
"includeFiles": ["./html/index.html"]
"includeFiles": ["./html/index.html"],
"maxLineLength": 200
}
]
}

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@git.zone/tsbundle',
version: '2.7.4',
version: '2.9.0',
description: 'a multi-bundler tool supporting esbuild, rolldown, and rspack for painless bundling of web projects'
}

View File

@@ -11,4 +11,7 @@ early.stop();
export * from './tsbundle.class.tsbundle.js';
export * from './mod_assets/index.js';
export * from './mod_html/index.js';
export * from './mod_custom/index.js';
export * from './mod_output/index.js';
export * from './interfaces/index.js';
export { runCli };

View File

@@ -17,13 +17,16 @@ export interface IEnvTransportOptions {
export type TOutputMode = 'bundle' | 'base64ts';
export type TBundler = 'esbuild' | 'rolldown' | 'rspack';
export type TIncludeFile = string | { from: string; to: string };
export interface IBundleConfig {
from: string;
to: string;
outputMode?: TOutputMode;
bundler?: TBundler;
production?: boolean;
includeFiles?: string[];
includeFiles?: TIncludeFile[];
maxLineLength?: number; // For base64ts output: max chars per line. 0 or undefined = unlimited (default)
}
export interface ITsbundleConfig {

View File

@@ -48,7 +48,7 @@ export class CustomBundleHandler {
/**
* Process a single bundle configuration
*/
private async processSingleBundle(bundleConfig: interfaces.IBundleConfig): Promise<void> {
public async processSingleBundle(bundleConfig: interfaces.IBundleConfig): Promise<void> {
const outputMode = bundleConfig.outputMode || 'bundle';
const bundler = bundleConfig.bundler || 'esbuild';
@@ -61,15 +61,21 @@ export class CustomBundleHandler {
// Build the bundle to temp location
const tsbundle = new TsBundle();
await tsbundle.build(
this.cwd,
bundleConfig.from,
tempBundlePath,
{
bundler,
production: bundleConfig.production || false,
}
);
try {
await tsbundle.build(
this.cwd,
bundleConfig.from,
tempBundlePath,
{
bundler,
production: bundleConfig.production || false,
}
);
} catch (error: any) {
console.error(`\n\x1b[31m❌ Bundle failed: ${bundleConfig.from} -> ${bundleConfig.to}\x1b[0m`);
// Don't re-print error details - they were already shown by the child process
return; // Skip output handling since build failed
}
if (outputMode === 'base64ts') {
await this.handleBase64TsOutput(bundleConfig, tempBundlePath);
@@ -99,13 +105,17 @@ export class CustomBundleHandler {
// Add included files
if (bundleConfig.includeFiles && bundleConfig.includeFiles.length > 0) {
for (const pattern of bundleConfig.includeFiles) {
await base64Output.addFilesFromGlob(pattern);
for (const entry of bundleConfig.includeFiles) {
if (typeof entry === 'string') {
await base64Output.addFilesFromGlob(entry);
} else {
await base64Output.addFileWithServePath(entry.from, entry.to);
}
}
}
// Write the TypeScript output
await base64Output.writeToFile(bundleConfig.to);
await base64Output.writeToFile(bundleConfig.to, bundleConfig.maxLineLength);
}
/**
@@ -129,7 +139,8 @@ export class CustomBundleHandler {
const htmlHandler = new HtmlHandler();
const outputDir = plugins.path.dirname(toPath);
for (const pattern of bundleConfig.includeFiles) {
for (const entry of bundleConfig.includeFiles) {
const pattern = typeof entry === 'string' ? entry : entry.from;
await this.copyIncludedFiles(pattern, outputDir);
}
}

View File

@@ -79,41 +79,65 @@ export class TsBundleProcess {
}
const run = async () => {
console.log('running spawned compilation process');
const transportOptions: interfaces.IEnvTransportOptions = JSON.parse(
process.env.transportOptions,
);
console.log('=======> ESBUILD');
console.log(transportOptions);
process.chdir(transportOptions.cwd);
console.log(`switched to ${process.cwd()}`);
const tsbundleProcessInstance = new TsBundleProcess();
if (transportOptions.mode === 'test') {
console.log('building for test:');
tsbundleProcessInstance.buildTest(
plugins.smartpath.transform.makeAbsolute(
transportOptions.from,
process.cwd(),
),
plugins.smartpath.transform.makeAbsolute(
transportOptions.to,
process.cwd(),
),
transportOptions.argv,
);
} else {
console.log('building for production:');
tsbundleProcessInstance.buildProduction(
plugins.smartpath.transform.makeAbsolute(
transportOptions.from,
process.cwd(),
),
plugins.smartpath.transform.makeAbsolute(
transportOptions.to,
process.cwd(),
),
transportOptions.argv,
try {
console.log('running spawned compilation process');
const transportOptions: interfaces.IEnvTransportOptions = JSON.parse(
process.env.transportOptions,
);
console.log('=======> ESBUILD');
console.log(transportOptions);
process.chdir(transportOptions.cwd);
console.log(`switched to ${process.cwd()}`);
const tsbundleProcessInstance = new TsBundleProcess();
if (transportOptions.mode === 'test') {
console.log('building for test:');
await tsbundleProcessInstance.buildTest(
plugins.smartpath.transform.makeAbsolute(
transportOptions.from,
process.cwd(),
),
plugins.smartpath.transform.makeAbsolute(
transportOptions.to,
process.cwd(),
),
transportOptions.argv,
);
} else {
console.log('building for production:');
await tsbundleProcessInstance.buildProduction(
plugins.smartpath.transform.makeAbsolute(
transportOptions.from,
process.cwd(),
),
plugins.smartpath.transform.makeAbsolute(
transportOptions.to,
process.cwd(),
),
transportOptions.argv,
);
}
process.exit(0);
} catch (error: any) {
console.error('\n\x1b[31m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m');
console.error('\x1b[31m❌ BUILD FAILED\x1b[0m');
console.error('\x1b[31m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m\n');
if (error.errors && Array.isArray(error.errors)) {
// esbuild errors - format them nicely
console.error(`Found ${error.errors.length} error(s):\n`);
for (const err of error.errors) {
const file = err.location?.file || 'unknown';
const line = err.location?.line || '?';
const column = err.location?.column || '?';
console.error(` \x1b[36m${file}\x1b[0m:\x1b[33m${line}\x1b[0m:\x1b[33m${column}\x1b[0m`);
console.error(` ${err.text}\n`);
}
} else {
console.error(error.message || error);
}
console.error('\x1b[31m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m\n');
process.exit(1);
}
};

View File

@@ -123,7 +123,8 @@ export class InitHandler {
console.log(` Mode: ${bundle.outputMode || 'bundle'}`);
console.log(` Bundler: ${bundle.bundler || 'esbuild'}`);
if (bundle.includeFiles && bundle.includeFiles.length > 0) {
console.log(` Include: ${bundle.includeFiles.join(', ')}`);
const display = bundle.includeFiles.map(f => typeof f === 'string' ? f : `${f.from} -> ${f.to}`);
console.log(` Include: ${display.join(', ')}`);
}
console.log('');
});
@@ -168,7 +169,8 @@ export class InitHandler {
console.log(` Mode: ${preset.config.outputMode}`);
console.log(` Bundler: ${preset.config.bundler}`);
if (preset.config.includeFiles && preset.config.includeFiles.length > 0) {
console.log(` Include: ${preset.config.includeFiles.join(', ')}`);
const display = preset.config.includeFiles.map(f => typeof f === 'string' ? f : `${f.from} -> ${f.to}`);
console.log(` Include: ${display.join(', ')}`);
}
const confirmInteract = new plugins.smartinteract.SmartInteract();
@@ -293,14 +295,14 @@ export class InitHandler {
/**
* Configure files to include
*/
private async configureIncludeFiles(prefill?: string[]): Promise<string[]> {
const includeFiles: string[] = [];
private async configureIncludeFiles(prefill?: interfaces.TIncludeFile[]): Promise<interfaces.TIncludeFile[]> {
const includeFiles: interfaces.TIncludeFile[] = [];
let addMore = true;
// If we have prefilled values, show them first
if (prefill && prefill.length > 0) {
console.log('\nPre-configured include patterns:');
prefill.forEach((p) => console.log(` - ${p}`));
prefill.forEach((p) => console.log(` - ${typeof p === 'string' ? p : `${p.from} -> ${p.to}`}`));
const keepInteract = new plugins.smartinteract.SmartInteract();
keepInteract.addQuestions([

View File

@@ -22,6 +22,20 @@ export class Base64TsOutput {
});
}
/**
* Add a file with a custom serve path
*/
public async addFileWithServePath(fromPath: string, servePath: string): Promise<void> {
const absolutePath = plugins.smartpath.transform.toAbsolute(fromPath, this.cwd) as string;
const fileExists = await plugins.fs.file(absolutePath).exists();
if (!fileExists) {
console.log(`File does not exist: ${absolutePath}`);
return;
}
const content = await plugins.fs.file(absolutePath).read();
this.addFile(servePath, content);
}
/**
* Add files matching a glob pattern
*/
@@ -56,8 +70,12 @@ export class Base64TsOutput {
for (const entry of entries) {
if (!entry.isDirectory && regex.test(entry.name)) {
const fullPath = plugins.path.join(dirPath, entry.path);
const relativePath = plugins.path.relative(this.cwd, fullPath);
// entry.path may be absolute or relative - handle both cases
const fullPath = plugins.path.isAbsolute(entry.path)
? entry.path
: plugins.path.join(dirPath, entry.path);
// Use path relative to pattern's base dir (not cwd) for web serving
const relativePath = plugins.path.relative(dirPath, fullPath);
const content = await plugins.fs.file(fullPath).read();
this.addFile(relativePath, content);
}
@@ -77,18 +95,26 @@ export class Base64TsOutput {
/**
* Generate TypeScript file content
* @param maxLineLength - Max chars per line for base64 strings. 0 or undefined = unlimited (default)
*/
public generateTypeScript(): string {
const MAX_LINE_LENGTH = 200;
public generateTypeScript(maxLineLength?: number): string {
// Default behavior: no line splitting (unlimited)
if (!maxLineLength || maxLineLength <= 0) {
const filesJson = JSON.stringify(this.files, null, 2);
return `// Auto-generated by tsbundle - do not edit
export const files: { path: string; contentBase64: string }[] = ${filesJson};
`;
}
// Split base64 strings into chunks when maxLineLength is specified
const formatBase64 = (base64: string): string => {
if (base64.length <= MAX_LINE_LENGTH) {
if (base64.length <= maxLineLength) {
return `"${base64}"`;
}
const chunks: string[] = [];
for (let i = 0; i < base64.length; i += MAX_LINE_LENGTH) {
chunks.push(base64.slice(i, i + MAX_LINE_LENGTH));
for (let i = 0; i < base64.length; i += maxLineLength) {
chunks.push(base64.slice(i, i + maxLineLength));
}
return `"" +\n "${chunks.join('" +\n "')}"`;
@@ -110,12 +136,14 @@ ${filesFormatted}
/**
* Write the TypeScript file to disk
* @param outputPath - Output file path
* @param maxLineLength - Max chars per line for base64 strings. 0 or undefined = unlimited (default)
*/
public async writeToFile(outputPath: string): Promise<void> {
public async writeToFile(outputPath: string, maxLineLength?: number): Promise<void> {
const absolutePath = plugins.smartpath.transform.toAbsolute(outputPath, this.cwd) as string;
const outputDir = plugins.path.dirname(absolutePath);
await plugins.fs.directory(outputDir).create();
const content = this.generateTypeScript();
const content = this.generateTypeScript(maxLineLength);
await plugins.fs.file(absolutePath).encoding('utf8').write(content);
console.log(`Generated base64ts output: ${outputPath}`);
}

View File

@@ -45,7 +45,11 @@ export class TsBundle {
);
const childProcess = await threadsimple.start();
childProcess.on('exit', (status) => {
done.resolve();
if (status !== 0) {
done.reject(new Error(`Bundle build failed with exit code ${status}`));
} else {
done.resolve();
}
});
await done.promise;
}