import * as plugins from './plugins.js'; import * as paths from '../paths.js'; import * as interfaces from '../interfaces/index.js'; // Preset configurations const PRESETS: Record = { 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 { 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 { 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 { 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 { // 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 ): Promise { 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 { 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 { const handler = new InitHandler(); await handler.runWizard(); }