mirror-web-cli
Version:
Professional website mirroring tool with intelligent framework preservation, AI-powered analysis, and comprehensive asset optimization
472 lines (387 loc) • 11.3 kB
JavaScript
import fs from 'fs-extra';
import path from 'path';
import chalk from 'chalk';
/**
* File Writer - Generates output HTML/CSS/JS files
*/
export class FileWriter {
constructor(cloner) {
this.cloner = cloner;
}
/**
* Generate all output files
*/
async generateFiles() {
await fs.ensureDir(this.cloner.options.outputDir);
console.log(chalk.gray(' Generating HTML file...'));
await this.generateHTML();
console.log(chalk.gray(' Generating CSS file...'));
await this.generateCSS();
console.log(chalk.gray(' Generating JavaScript file...'));
await this.generateJS();
console.log(chalk.gray(' Creating package files...'));
await this.generatePackageFiles();
}
/**
* Generate optimized HTML file
*/
async generateHTML() {
const $ = this.cloner.$;
// Clean up the HTML
this.cleanHTML($);
// Add our CSS and JS references
$('head').append('<link rel="stylesheet" href="./styles.css">');
$('body').append('<script src="./script.js"></script>');
// Add meta tags
this.addMetaTags($);
const html = $.html();
const htmlPath = path.join(this.cloner.options.outputDir, 'index.html');
await fs.writeFile(htmlPath, this.prettifyHTML(html), 'utf8');
}
/**
* Clean HTML content
*/
cleanHTML($) {
// Remove script tags if cleaning is enabled
if (this.cloner.options.clean) {
$('script').each((_, el) => {
const src = $(el).attr('src');
const content = $(el).html();
if (src && this.cloner.assetManager.isTrackingScript(src)) {
$(el).remove();
} else if (content && this.cloner.assetManager.isTrackingScript(content)) {
$(el).remove();
}
});
// Remove tracking attributes
$('[data-gtm], [data-ga], [data-fb]').removeAttr('data-gtm data-ga data-fb');
// Remove noscript tags
$('noscript').remove();
}
// Remove empty elements
$('script:empty, style:empty, link[href=""]').remove();
// Clean up inline styles that reference removed assets
$('[style]').each((_, el) => {
let style = $(el).attr('style');
if (style) {
// Remove broken background images
style = style.replace(/background-image:\s*url\([^\)]*\);?/g, '');
if (style.trim()) {
$(el).attr('style', style);
} else {
$(el).removeAttr('style');
}
}
});
}
/**
* Add essential meta tags
*/
addMetaTags($) {
const head = $('head');
// Add viewport if missing
if (!$('meta[name="viewport"]').length) {
head.prepend('<meta name="viewport" content="width=device-width, initial-scale=1.0">');
}
// Add charset if missing
if (!$('meta[charset]').length) {
head.prepend('<meta charset="UTF-8">');
}
// Add generator tag
head.append(`<meta name="generator" content="Website Cloner v3.0">`);
head.append(`<meta name="cloned-from" content="${this.cloner.url}">`);
head.append(`<meta name="cloned-date" content="${new Date().toISOString()}">`);
}
/**
* Generate consolidated CSS file
*/
async generateCSS() {
let css = '/* Website Cloner v3.0 - Generated CSS */\n\n';
// Add reset/normalize styles
css += this.getResetCSS();
// Add responsive base styles
css += this.getBaseCSS();
// Combine all extracted CSS
for (const style of this.cloner.assets.styles) {
if (style.content) {
css += `\n/* ${style.type} - ${style.filename} */\n`;
css += style.content;
css += '\n\n';
}
}
// Optimize CSS
css = this.optimizeCSS(css);
const cssPath = path.join(this.cloner.options.outputDir, 'styles.css');
await fs.writeFile(cssPath, css, 'utf8');
}
/**
* Generate consolidated JavaScript file
*/
async generateJS() {
let js = '/* Website Cloner v3.0 - Generated JavaScript */\n\n';
// Add utility functions
js += this.getUtilityJS();
// Add extracted JavaScript (if not cleaning)
if (!this.cloner.options.clean) {
for (const script of this.cloner.assets.scripts) {
if (script.content && !this.cloner.assetManager.isTrackingScript(script.content)) {
js += `\n/* ${script.type} - ${script.filename} */\n`;
js += script.content;
js += '\n\n';
}
}
}
// Add initialization script
js += this.getInitializationJS();
const jsPath = path.join(this.cloner.options.outputDir, 'script.js');
await fs.writeFile(jsPath, js, 'utf8');
}
/**
* Generate package.json and README
*/
async generatePackageFiles() {
// package.json
const packageJson = {
name: this.cloner.domain.replace(/\./g, '-'),
version: '1.0.0',
description: `Cloned website from ${this.cloner.url}`,
main: 'index.html',
scripts: {
start: 'python -m http.server 8000',
serve: 'npx serve .'
},
keywords: ['cloned-website', 'static-site'],
clonedFrom: this.cloner.url,
clonedDate: new Date().toISOString(),
generator: 'Website Cloner v3.0'
};
await fs.writeFile(
path.join(this.cloner.options.outputDir, 'package.json'),
JSON.stringify(packageJson, null, 2),
'utf8'
);
// README.md
const readme = this.generateReadme();
await fs.writeFile(
path.join(this.cloner.options.outputDir, 'README.md'),
readme,
'utf8'
);
}
/**
* Get CSS reset styles
*/
getResetCSS() {
return `/* CSS Reset */
* {
box-sizing: border-box;
}
body {
margin: 0;
padding: 0;
line-height: 1.6;
}
img {
max-width: 100%;
height: auto;
}
a {
text-decoration: none;
}
ul, ol {
margin: 0;
padding: 0;
}
`;
}
/**
* Get base responsive CSS
*/
getBaseCSS() {
return `/* Base Responsive Styles */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 15px;
}
(max-width: 768px) {
.container {
padding: 0 10px;
}
img {
width: 100% !important;
height: auto !important;
}
}
/* Utility Classes */
.responsive-img {
width: 100%;
height: auto;
}
.hidden {
display: none;
}
.text-center {
text-align: center;
}
`;
}
/**
* Get utility JavaScript functions
*/
getUtilityJS() {
return `// Utility Functions
function ready(fn) {
if (document.readyState !== 'loading') {
fn();
} else {
document.addEventListener('DOMContentLoaded', fn);
}
}
function $(selector) {
return document.querySelectorAll(selector);
}
function fadeIn(element, duration = 300) {
element.style.opacity = 0;
element.style.display = 'block';
const start = performance.now();
function animate(currentTime) {
const elapsed = currentTime - start;
const progress = elapsed / duration;
if (progress < 1) {
element.style.opacity = progress;
requestAnimationFrame(animate);
} else {
element.style.opacity = 1;
}
}
requestAnimationFrame(animate);
}
`;
}
/**
* Get initialization JavaScript
*/
getInitializationJS() {
const framework = this.cloner.analysis?.primaryFramework?.name || 'Unknown';
return `// Initialization
ready(function() {
console.log('Website loaded successfully!');
console.log('Original framework: ${framework}');
console.log('Converted to: Universal HTML/CSS/JS');
// Lazy loading for images
const images = $('img[data-src]');
if ('IntersectionObserver' in window) {
const imageObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.removeAttribute('data-src');
imageObserver.unobserve(img);
}
});
});
images.forEach(img => imageObserver.observe(img));
} else {
// Fallback for older browsers
images.forEach(img => {
img.src = img.dataset.src;
img.removeAttribute('data-src');
});
}
// Smooth scrolling for anchor links
const anchorLinks = $('a[href^="#"]');
anchorLinks.forEach(link => {
link.addEventListener('click', function(e) {
e.preventDefault();
const target = document.querySelector(this.getAttribute('href'));
if (target) {
target.scrollIntoView({ behavior: 'smooth' });
}
});
});
});
`;
}
/**
* Optimize CSS content
*/
optimizeCSS(css) {
// Remove comments
css = css.replace(/\/\*[\s\S]*?\*\//g, '');
// Remove extra whitespace
css = css.replace(/\s+/g, ' ');
// Remove empty rules
css = css.replace(/[^{}]*{\s*}/g, '');
return css.trim();
}
/**
* Prettify HTML
*/
prettifyHTML(html) {
// Basic HTML formatting
return html
.replace(/></g, '>\n<')
.replace(/\n\s*\n/g, '\n')
.trim();
}
/**
* Generate README content
*/
generateReadme() {
const framework = this.cloner.analysis?.primaryFramework?.name || 'Unknown';
const stats = this.cloner.getAssetStats();
return `# ${this.cloner.domain}
Cloned website converted to universal HTML/CSS/JS format.
## Original Information
- **Source URL**: ${this.cloner.url}
- **Original Framework**: ${framework}
- **Cloned Date**: ${new Date().toLocaleDateString()}
- **Generator**: Website Cloner v3.0
## Asset Statistics
- Images: ${stats.images}
- Stylesheets: ${stats.styles}
- Scripts: ${stats.scripts}
- Fonts: ${stats.fonts}
- Icons: ${stats.icons}
- Media Files: ${stats.media}
- **Total Assets**: ${stats.total}
## How to Run
### Option 1: Python HTTP Server
\`\`\`bash
python -m http.server 8000
\`\`\`
Then open http://localhost:8000
### Option 2: Node.js Serve
\`\`\`bash
npx serve .
\`\`\`
### Option 3: Any Web Server
Serve the files using any web server (Apache, Nginx, etc.)
## File Structure
\`\`\`
.
├── index.html # Main HTML file
├── styles.css # Consolidated CSS
├── script.js # Consolidated JavaScript
├── assets/
│ ├── images/ # All images
│ ├── styles/ # External stylesheets
│ ├── scripts/ # External scripts
│ ├── fonts/ # Font files
│ ├── icons/ # Icon files
│ └── media/ # Video/audio files
├── package.json # Project metadata
└── README.md # This file
\`\`\`
## Notes
- This is a static conversion of the original dynamic website
- All assets have been downloaded and made local
- Interactive features may have limited functionality
- The design should closely match the original
---
*Generated by Website Cloner v3.0*
`;
}
}