Files
tsbundle/ts/mod_init/index.ts

378 lines
12 KiB
TypeScript

import * as plugins from './plugins.js';
import * as paths from '../paths.js';
import * as interfaces from '../interfaces/index.js';
// Preset configurations
const PRESETS: Record<string, { description: string; config: interfaces.IBundleConfig }> = {
element: {
description: 'Web component / element bundle',
config: {
from: './ts_web/index.ts',
to: './dist_bundle/bundle.js',
outputMode: 'bundle',
bundler: 'esbuild',
},
},
website: {
description: 'Full website with HTML and assets',
config: {
from: './ts_web/index.ts',
to: './dist_serve/bundle.js',
outputMode: 'bundle',
bundler: 'esbuild',
includeFiles: ['./html/**/*.html', './assets/**/*'],
},
},
npm: {
description: 'NPM package bundle (from ts/)',
config: {
from: './ts/index.ts',
to: './dist_bundle/bundle.js',
outputMode: 'bundle',
bundler: 'esbuild',
},
},
};
export class InitHandler {
private cwd: string;
private npmextraPath: string;
constructor(cwd: string = paths.cwd) {
this.cwd = cwd;
this.npmextraPath = plugins.path.join(this.cwd, 'npmextra.json');
}
/**
* Load existing npmextra.json or create empty config
*/
private async loadExistingConfig(): Promise<any> {
const fileExists = await plugins.fs.file(this.npmextraPath).exists();
if (fileExists) {
const content = (await plugins.fs.file(this.npmextraPath).encoding('utf8').read()) as string;
try {
return JSON.parse(content);
} catch {
return {};
}
}
return {};
}
/**
* Save config to npmextra.json
*/
private async saveConfig(config: any): Promise<void> {
const content = JSON.stringify(config, null, 2);
await plugins.fs.file(this.npmextraPath).encoding('utf8').write(content);
console.log(`\n✅ Configuration saved to npmextra.json`);
}
/**
* Run the interactive init wizard
*/
public async runWizard(): Promise<void> {
console.log('\n🚀 tsbundle configuration wizard\n');
console.log('This wizard will help you configure bundle settings in npmextra.json.\n');
const npmextraJson = await this.loadExistingConfig();
if (!npmextraJson['@git.zone/tsbundle']) {
npmextraJson['@git.zone/tsbundle'] = { bundles: [] };
}
const existingBundles = npmextraJson['@git.zone/tsbundle'].bundles || [];
if (existingBundles.length > 0) {
console.log(`Found ${existingBundles.length} existing bundle configuration(s):\n`);
existingBundles.forEach((bundle: interfaces.IBundleConfig, i: number) => {
console.log(` ${i + 1}. ${bundle.from}${bundle.to} (${bundle.outputMode || 'bundle'})`);
});
console.log('');
}
let addMore = true;
while (addMore) {
const bundle = await this.configureSingleBundle();
if (bundle) {
npmextraJson['@git.zone/tsbundle'].bundles.push(bundle);
console.log(`\n✅ Bundle configuration added!`);
}
const continueInteract = new plugins.smartinteract.SmartInteract();
continueInteract.addQuestions([
{
type: 'confirm',
name: 'addAnother',
message: 'Would you like to add another bundle configuration?',
default: false,
},
]);
const answers = await continueInteract.runQueue();
addMore = answers.getAnswerFor('addAnother');
}
await this.saveConfig(npmextraJson);
console.log('\n📋 Final configuration:\n');
const bundles = npmextraJson['@git.zone/tsbundle'].bundles;
bundles.forEach((bundle: interfaces.IBundleConfig, i: number) => {
console.log(` Bundle ${i + 1}:`);
console.log(` From: ${bundle.from}`);
console.log(` To: ${bundle.to}`);
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(', ')}`);
}
console.log('');
});
console.log('Run `tsbundle` to build your bundles.\n');
}
/**
* Configure a single bundle interactively
*/
private async configureSingleBundle(): Promise<interfaces.IBundleConfig | null> {
// First, ask for preset or custom
const presetInteract = new plugins.smartinteract.SmartInteract();
presetInteract.addQuestions([
{
type: 'list',
name: 'preset',
message: 'Choose a configuration:',
choices: [
{ name: 'element - Web component / element bundle', value: 'element' },
{ name: 'website - Full website with HTML and assets', value: 'website' },
{ name: 'npm - NPM package bundle (from ts/)', value: 'npm' },
{ name: 'custom - Configure manually', value: 'custom' },
],
default: 'element',
},
]);
const presetAnswers = await presetInteract.runQueue();
const selectedPreset = presetAnswers.getAnswerFor('preset') as string;
// If custom, go to full manual configuration
if (selectedPreset === 'custom') {
return this.configureManualBundle();
}
// Show preset config and ask if user wants to use it or customize
const preset = PRESETS[selectedPreset];
console.log(`\n📦 ${preset.description}:`);
console.log(` From: ${preset.config.from}`);
console.log(` To: ${preset.config.to}`);
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 confirmInteract = new plugins.smartinteract.SmartInteract();
confirmInteract.addQuestions([
{
type: 'list',
name: 'action',
message: 'Use this configuration?',
choices: [
{ name: 'Yes, use as-is', value: 'use' },
{ name: 'Customize it', value: 'customize' },
],
default: 'use',
},
]);
const confirmAnswers = await confirmInteract.runQueue();
const action = confirmAnswers.getAnswerFor('action') as string;
if (action === 'use') {
// Return the preset config directly
return { ...preset.config };
}
// Customize: pre-fill with preset values
return this.configureManualBundle(preset.config);
}
/**
* Configure a bundle manually with optional pre-filled values
*/
private async configureManualBundle(
prefill?: Partial<interfaces.IBundleConfig>
): Promise<interfaces.IBundleConfig> {
const interact = new plugins.smartinteract.SmartInteract();
// Basic configuration questions
interact.addQuestions([
{
type: 'input',
name: 'from',
message: 'Entry point TypeScript file:',
default: prefill?.from || './ts_web/index.ts',
},
{
type: 'input',
name: 'to',
message: 'Output file path:',
default: prefill?.to || './dist_bundle/bundle.js',
},
{
type: 'list',
name: 'outputMode',
message: 'Output mode:',
choices: [
{ name: 'bundle - Standard JavaScript bundle file', value: 'bundle' },
{
name: 'base64ts - TypeScript file with base64-encoded content (for Deno compile)',
value: 'base64ts',
},
],
default: prefill?.outputMode || 'bundle',
},
{
type: 'list',
name: 'bundler',
message: 'Bundler to use:',
choices: [
{ name: 'esbuild (fastest, recommended)', value: 'esbuild' },
{ name: 'rolldown (Rust-based, Rollup compatible)', value: 'rolldown' },
{ name: 'rspack (Webpack compatible)', value: 'rspack' },
],
default: prefill?.bundler || 'esbuild',
},
{
type: 'confirm',
name: 'production',
message: 'Enable production mode (minification)?',
default: prefill?.production || false,
},
{
type: 'confirm',
name: 'hasIncludeFiles',
message: 'Include additional files (HTML, assets)?',
default: prefill?.includeFiles && prefill.includeFiles.length > 0 ? true : false,
},
]);
const answers = await interact.runQueue();
const bundle: interfaces.IBundleConfig = {
from: answers.getAnswerFor('from'),
to: answers.getAnswerFor('to'),
outputMode: answers.getAnswerFor('outputMode') as interfaces.TOutputMode,
bundler: answers.getAnswerFor('bundler') as interfaces.TBundler,
production: answers.getAnswerFor('production'),
};
// Update default output path based on mode
if (bundle.outputMode === 'base64ts' && bundle.to === './dist_bundle/bundle.js') {
const suggestInteract = new plugins.smartinteract.SmartInteract();
suggestInteract.addQuestions([
{
type: 'input',
name: 'to',
message: 'For base64ts mode, suggest a .ts output path:',
default: './ts/embedded-bundle.ts',
},
]);
const suggestAnswers = await suggestInteract.runQueue();
bundle.to = suggestAnswers.getAnswerFor('to');
}
// Handle include files
if (answers.getAnswerFor('hasIncludeFiles')) {
bundle.includeFiles = await this.configureIncludeFiles(prefill?.includeFiles);
}
return bundle;
}
/**
* Configure files to include
*/
private async configureIncludeFiles(prefill?: string[]): Promise<string[]> {
const includeFiles: string[] = [];
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}`));
const keepInteract = new plugins.smartinteract.SmartInteract();
keepInteract.addQuestions([
{
type: 'confirm',
name: 'keepPrefill',
message: 'Keep these patterns?',
default: true,
},
]);
const keepAnswers = await keepInteract.runQueue();
if (keepAnswers.getAnswerFor('keepPrefill')) {
includeFiles.push(...prefill);
}
}
console.log('\nAdd files or glob patterns to include (e.g., ./html/index.html, ./assets/**/*):\n');
// Ask if user wants to add more patterns
const addInteract = new plugins.smartinteract.SmartInteract();
addInteract.addQuestions([
{
type: 'confirm',
name: 'addPatterns',
message: includeFiles.length > 0 ? 'Add more patterns?' : 'Add include patterns?',
default: includeFiles.length === 0,
},
]);
const addAnswers = await addInteract.runQueue();
addMore = addAnswers.getAnswerFor('addPatterns');
while (addMore) {
const fileInteract = new plugins.smartinteract.SmartInteract();
fileInteract.addQuestions([
{
type: 'input',
name: 'pattern',
message: 'File or glob pattern:',
default: includeFiles.length === 0 ? './html/index.html' : '',
},
]);
const fileAnswers = await fileInteract.runQueue();
const pattern = fileAnswers.getAnswerFor('pattern');
if (pattern && pattern.trim()) {
includeFiles.push(pattern.trim());
console.log(` Added: ${pattern}`);
}
const continueInteract = new plugins.smartinteract.SmartInteract();
continueInteract.addQuestions([
{
type: 'confirm',
name: 'addMore',
message: 'Add another file/pattern?',
default: false,
},
]);
const continueAnswers = await continueInteract.runQueue();
addMore = continueAnswers.getAnswerFor('addMore');
}
return includeFiles;
}
}
/**
* Run the init command
*/
export async function runInit(): Promise<void> {
const handler = new InitHandler();
await handler.runWizard();
}