Herramienta de Empaquetado Parcel

Ejemplos de la herramienta de empaquetado Parcel de configuración cero incluyendo configuración de proyecto, plugins y configuración avanzada

💻 Configuración Básica de Parcel json

🟢 simple

Comenzando con la herramienta de empaquetado Parcel de configuración cero para aplicaciones web modernas

⏱️ 15 min 🏷️ parcel, setup, configuration
Prerequisites: Node.js knowledge, JavaScript basics
// Parcel Basic Setup Examples
// Zero-configuration bundler for modern web applications

// ===== PROJECT STRUCTURE =====
/*
my-project/
├── src/
│   ├── index.html         # Entry HTML file
│   ├── index.js           # Main JavaScript entry
│   ├── styles/
│   │   └── main.css       # CSS styles
│   ├── images/
│   │   └── logo.png       # Image assets
│   └── components/
│       └── App.js         # React component
├── public/               # Static assets
│   └── favicon.ico
├── package.json          # Project configuration
├── .babelrc              # Babel configuration
├── .parcelrc             # Parcel configuration (optional)
└── tsconfig.json         # TypeScript configuration (if needed)
*/

// ===== package.json =====
{
  "name": "my-parcel-app",
  "version": "1.0.0",
  "description": "Modern web application built with Parcel",
  "main": "src/index.js",
  "scripts": {
    "start": "parcel",
    "build": "parcel build",
    "dev": "parcel src/index.html",
    "serve": "parcel serve src/index.html",
    "clean": "rm -rf dist .cache",
    "test": "jest",
    "lint": "eslint src/**/*.{js,jsx,ts,tsx}",
    "type-check": "tsc --noEmit"
  },
  "keywords": ["parcel", "bundler", "javascript", "css", "html"],
  "author": "Your Name",
  "license": "MIT",
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-router-dom": "^6.8.0",
    "axios": "^1.3.0",
    "lodash": "^4.17.21"
  },
  "devDependencies": {
    "parcel": "^2.8.0",
    "@parcel/config-default": "^2.8.0",
    "@parcel/core": "^2.8.0",
    "@parcel/bundler-default": "^2.8.0",
    "@parcel/optimizer-css": "^2.8.0",
    "@parcel/optimizer-htmlnano": "^2.8.0",
    "@parcel/packager-css": "^2.8.0",
    "@parcel/packager-html": "^2.8.0",
    "@parcel/packager-js": "^2.8.0",
    "@parcel/packager-raw": "^2.8.0",
    "@parcel/reporter-cli": "^2.8.0",
    "@parcel/transformer-babel": "^2.8.0",
    "@parcel/transformer-css": "^2.8.0",
    "@parcel/transformer-html": "^2.8.0",
    "@parcel/transformer-image": "^2.8.0",
    "@parcel/transformer-js": "^2.8.0",
    "@parcel/transformer-json": "^2.8.0",
    "@parcel/transformer-postcss": "^2.8.0",
    "@parcel/transformer-raw": "^2.8.0",
    "@parcel/transformer-sass": "^2.8.0",
    "@parcel/transformer-svg": "^2.8.0",
    "@parcel/transformer-typescript-tsc": "^2.8.0",
    "process": "^0.11.10",
    "@babel/core": "^7.20.0",
    "@babel/preset-env": "^7.20.0",
    "@babel/preset-react": "^7.18.0",
    "@babel/preset-typescript": "^7.18.0",
    "typescript": "^4.9.0",
    "eslint": "^8.34.0",
    "eslint-config-react-app": "^7.0.1",
    "prettier": "^2.8.0",
    "sass": "^1.58.0",
    "tailwindcss": "^3.2.0",
    "autoprefixer": "^10.4.0",
    "postcss": "^8.4.0",
    "jest": "^29.4.0",
    "@testing-library/react": "^13.4.0",
    "@testing-library/jest-dom": "^5.16.0"
  },
  "browserslist": [
    "last 2 versions",
    "not dead",
    "not ie 11"
  ]
}

// ===== .parcelrc (Optional Configuration) =====
{
  "extends": "@parcel/config-default",
  "transformers": {
    "*.{js,jsx,ts,tsx}": [
      "@parcel/transformer-js",
      "@parcel/transformer-babel"
    ],
    "*.{css,scss,sass}": [
      "@parcel/transformer-css",
      "@parcel/transformer-postcss"
    ],
    "*.{html}": [
      "@parcel/transformer-html"
    ],
    "*.{json}": [
      "@parcel/transformer-json"
    ],
    "*.{svg}": [
      "@parcel/transformer-svg"
    ],
    "*.{png,jpg,jpeg,gif,webp}": [
      "@parcel/transformer-image"
    ]
  },
  "optimizers": {
    "*.{css}": [
      "@parcel/optimizer-css"
    ],
    "*.{html}": [
      "@parcel/optimizer-htmlnano"
    ]
  },
  "browsers": {
    "production": [
      "> 0.25%",
      "not dead"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  },
  "reporters": [
    "@parcel/reporter-cli"
  ]
}

// ===== .babelrc =====
{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {
          "browsers": [
            "last 2 versions",
            "not dead",
            "not ie 11"
          ]
        },
        "modules": false,
        "useBuiltIns": "usage",
        "corejs": 3
      }
    ],
    "@babel/preset-react",
    "@babel/preset-typescript"
  ],
  "plugins": [
    "@babel/plugin-transform-runtime",
    "@babel/plugin-proposal-class-properties",
    "@babel/plugin-proposal-object-rest-spread"
  ]
}

// ===== tsconfig.json =====
{
  "compilerOptions": {
    "target": "ES2020",
    "lib": ["DOM", "DOM.Iterable", "ES6"],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "module": "ESNext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@components/*": ["src/components/*"],
      "@utils/*": ["src/utils/*"],
      "@assets/*": ["src/assets/*"]
    }
  },
  "include": [
    "src/**/*",
    "public/**/*"
  ],
  "exclude": [
    "node_modules",
    "dist",
    ".cache"
  ]
}

// ===== src/index.html =====
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>My Parcel App</title>
    <link rel="shortcut icon" href="/favicon.ico">
    <meta name="description" content="Modern web application built with Parcel">
</head>
<body>
    <div id="root"></div>
    <!-- Parcel will automatically inject scripts here -->
</body>
</html>

// ===== src/index.js =====
import React from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import './styles/main.css';
import App from './components/App';

const container = document.getElementById('root');
const root = createRoot(container);

root.render(
    <React.StrictMode>
        <BrowserRouter>
            <App />
        </BrowserRouter>
    </React.StrictMode>
);

// Hot Module Replacement support
if (module.hot) {
    module.hot.accept('./components/App', () => {
        const NextApp = require('./components/App').default;
        root.render(
            <React.StrictMode>
                <BrowserRouter>
                    <NextApp />
                </BrowserRouter>
            </React.StrictMode>
        );
    });
}

// ===== src/components/App.js =====
import React from 'react';
import { Routes, Route } from 'react-router-dom';
import Home from './Home';
import About from './About';
import Contact from './Contact';
import Navigation from './Navigation';

const App = () => {
    return (
        <div className="app">
            <Navigation />
            <main className="main-content">
                <Routes>
                    <Route path="/" element={<Home />} />
                    <Route path="/about" element={<About />} />
                    <Route path="/contact" element={<Contact />} />
                </Routes>
            </main>
            <footer className="footer">
                <p>&copy; 2024 My Parcel App. All rights reserved.</p>
            </footer>
        </div>
    );
};

export default App;

// ===== src/styles/main.css =====
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';

/* Custom styles */
.app {
    min-height: 100vh;
    display: flex;
    flex-direction: column;
}

.main-content {
    flex: 1;
    padding: 2rem;
    max-width: 1200px;
    margin: 0 auto;
    width: 100%;
}

.footer {
    background: #333;
    color: white;
    text-align: center;
    padding: 1rem;
    margin-top: auto;
}

/* CSS Modules support */
.container {
    @apply max-w-6xl mx-auto px-4;
}

.btn {
    @apply bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded transition-colors duration-200;
}

.card {
    @apply bg-white rounded-lg shadow-md p-6 mb-4;
}

// ===== tailwind.config.js =====
module.exports = {
  content: [
    "./src/**/*.{html,js,jsx,ts,tsx}",
    "./public/**/*.html"
  ],
  theme: {
    extend: {
      colors: {
        primary: {
          50: '#eff6ff',
          500: '#3b82f6',
          600: '#2563eb',
          700: '#1d4ed8'
        }
      },
      fontFamily: {
        sans: ['Inter', 'system-ui', 'sans-serif']
      }
    },
  },
  plugins: [
    require('@tailwindcss/forms'),
    require('@tailwindcss/typography')
  ]
}

// ===== postcss.config.js =====
module.exports = {
  plugins: [
    'tailwindcss',
    'autoprefixer'
  ]
}

// ===== .env =====
# Environment variables
API_URL=https://api.example.com
APP_NAME=My Parcel App
APP_VERSION=1.0.0
NODE_ENV=development

// ===== .env.production =====
API_URL=https://api.prod.example.com
APP_NAME=My Parcel App
APP_VERSION=1.0.0
NODE_ENV=production

// ===== jest.config.js =====
module.exports = {
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
  moduleNameMapping: {
    '\\.(css|less|scss|sass)$': 'identity-obj-proxy',
    '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '<rootDir>/__mocks__/fileMock.js'
  },
  collectCoverageFrom: [
    'src/**/*.{js,jsx,ts,tsx}',
    '!src/**/*.d.ts',
    '!src/index.js',
    '!src/reportWebVitals.js'
  ]
};

// ===== .eslintrc.json =====
{
  "extends": [
    "react-app",
    "react-app/jest"
  ],
  "rules": {
    "no-console": "warn",
    "no-unused-vars": "error",
    "prefer-const": "error"
  },
  "overrides": [
    {
      "files": ["**/*.test.js", "**/*.test.jsx", "**/*.test.ts", "**/*.test.tsx"],
      "env": {
        "jest": true
      }
    }
  ]
}

// ===== .prettierrc =====
{
  "semi": true,
  "trailingComma": "es5",
  "singleQuote": true,
  "printWidth": 80,
  "tabWidth": 2,
  "useTabs": false
}

💻 Plugins y Configuración de Parcel javascript

🟡 intermediate ⭐⭐⭐⭐

Configuración avanzada de Parcel con plugins, transformadores personalizados y optimización de construcción

⏱️ 35 min 🏷️ parcel, plugins, configuration
Prerequisites: Parcel basics, JavaScript ES6+, Plugin development
// Advanced Parcel Configuration and Custom Plugins
// Extending Parcel with custom transformers, plugins, and optimization

// ===== CUSTOM TRANSFORMER PLUGIN =====
// parcel-transformer-custom.js
const { Transformer } = require('@parcel/core');

class CustomTransformer extends Transformer {
    constructor(options = {}) {
        super(options);
        this.customOptions = options;
    }

    // Check if this transformer can handle the asset
    async canTransform(asset) {
        const extension = asset.filePath.split('.').pop().toLowerCase();
        return ['custom', 'xyz'].includes(extension);
    }

    // Transform the asset
    async transform(asset) {
        const source = await asset.getCode();
        const filePath = asset.filePath;

        // Custom transformation logic
        let transformedCode = source;

        // Example: Add timestamp
        const timestamp = new Date().toISOString();
        transformedCode = `
// Generated from ${filePath} at ${timestamp}
${transformedCode}

export const metadata = {
    sourceFile: '${filePath}',
    generatedAt: '${timestamp}',
    transformer: 'custom'
};
        `;

        return {
            content: transformedCode,
            map: null // No source map for this example
        };
    }
}

module.exports = (options) => {
    return new CustomTransformer(options);
};

// ===== CUSTOM BUNDLER PLUGIN =====
// parcel-plugin-analyzer.js
const { Bundler } = require('@parcel/core');
const fs = require('fs');
const path = require('path');

class AnalyzerBundler extends Bundler {
    constructor(...args) {
        super(...args);
        this.bundleStats = new Map();
    }

    async bundle() {
        const startTime = Date.now();

        // Run the original bundling process
        const bundleGraph = await super.bundle();

        const endTime = Date.now();
        const buildTime = endTime - startTime;

        // Analyze bundle
        await this.analyzeBundle(bundleGraph, buildTime);

        return bundleGraph;
    }

    async analyzeBundle(bundleGraph, buildTime) {
        const stats = {
            buildTime,
            bundles: [],
            assets: [],
            totalSize: 0,
            dependencies: new Set()
        };

        // Analyze bundles
        bundleGraph.getBundles().forEach(bundle => {
            const bundleInfo = {
                id: bundle.id,
                filePath: bundle.filePath,
                type: bundle.type,
                size: 0,
                assets: []
            };

            bundle.getEntryAssets().forEach(asset => {
                const assetSize = asset.stats.size;
                bundleInfo.size += assetSize;
                bundleInfo.assets.push({
                    id: asset.id,
                    filePath: asset.filePath,
                    type: asset.type,
                    size: assetSize
                });

                stats.assets.push({
                    filePath: asset.filePath,
                    size: assetSize,
                    type: asset.type
                });
            });

            stats.bundles.push(bundleInfo);
            stats.totalSize += bundleInfo.size;
        });

        // Collect dependencies
        bundleGraph.getDependencyGraph().getNodes().forEach(node => {
            if (node.value.type === 'dependency') {
                stats.dependencies.add(node.value.specifier);
            }
        });

        // Write analysis report
        await this.writeAnalysisReport(stats);
    }

    async writeAnalysisReport(stats) {
        const report = {
            timestamp: new Date().toISOString(),
            buildTime: stats.buildTime,
            bundles: stats.bundles,
            totalSize: stats.totalSize,
            dependencyCount: stats.dependencies.size,
            dependencies: Array.from(stats.dependencies)
        };

        // Write JSON report
        const reportPath = path.join(this.options.outDir, 'bundle-analysis.json');
        fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));

        // Write HTML report
        const htmlReport = this.generateHTMLReport(report);
        const htmlPath = path.join(this.options.outDir, 'bundle-analysis.html');
        fs.writeFileSync(htmlPath, htmlReport);

        console.log(`📊 Bundle analysis written to ${reportPath}`);
        console.log(`🌐 HTML report available at ${htmlPath}`);
    }

    generateHTMLReport(report) {
        return `
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Bundle Analysis Report</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 20px; background: #f5f5f5; }
        .container { max-width: 1200px; margin: 0 auto; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
        .header { text-align: center; margin-bottom: 30px; }
        .stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 30px; }
        .stat-card { background: #f8f9fa; padding: 20px; border-radius: 8px; text-align: center; }
        .stat-number { font-size: 2em; font-weight: bold; color: #007bff; }
        table { width: 100%; border-collapse: collapse; margin-bottom: 20px; }
        th, td { border: 1px solid #ddd; padding: 12px; text-align: left; }
        th { background: #f8f9fa; font-weight: bold; }
        .chart { height: 300px; background: #f8f9fa; border-radius: 8px; margin: 20px 0; display: flex; align-items: center; justify-content: center; }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1>Bundle Analysis Report</h1>
            <p>Generated on ${report.timestamp}</p>
        </div>

        <div class="stats">
            <div class="stat-card">
                <div class="stat-number">${report.bundles.length}</div>
                <div>Bundles</div>
            </div>
            <div class="stat-card">
                <div class="stat-number">${(report.totalSize / 1024).toFixed(2)} KB</div>
                <div>Total Size</div>
            </div>
            <div class="stat-card">
                <div class="stat-number">${report.buildTime} ms</div>
                <div>Build Time</div>
            </div>
            <div class="stat-card">
                <div class="stat-number">${report.dependencyCount}</div>
                <div>Dependencies</div>
            </div>
        </div>

        <h2>Bundle Details</h2>
        <table>
            <thead>
                <tr>
                    <th>Bundle</th>
                    <th>Type</th>
                    <th>Size</th>
                    <th>Assets</th>
                </tr>
            </thead>
            <tbody>
                ${report.bundles.map(bundle => `
                    <tr>
                        <td>${bundle.filePath || bundle.id}</td>
                        <td>${bundle.type}</td>
                        <td>${(bundle.size / 1024).toFixed(2)} KB</td>
                        <td>${bundle.assets.length}</td>
                    </tr>
                `).join('')}
            </tbody>
        </table>
    </div>
</body>
</html>
        `;
    }
}

module.exports = (options) => {
    return new AnalyzerBundler(options);
};

// ===== ADVANCED .parcelrc CONFIGURATION =====
{
  "extends": "@parcel/config-default",
  "bundler": "./parcel-plugin-analyzer.js",
  "transformers": {
    "*.{js,jsx,ts,tsx}": [
      "@parcel/transformer-js",
      "@parcel/transformer-babel"
    ],
    "*.{css,scss,sass,less}": [
      "@parcel/transformer-css",
      "@parcel/transformer-postcss"
    ],
    "*.{html,hbs}": [
      "@parcel/transformer-html"
    ],
    "*.{json}": [
      "@parcel/transformer-json"
    ],
    "*.{svg}": [
      "@parcel/transformer-svg"
    ],
    "*.{png,jpg,jpeg,gif,webp,avif}": [
      "@parcel/transformer-image"
    ],
    "*.{txt,md}": [
      "./parcel-transformer-markdown.js"
    ],
    "*.{custom,xyz}": [
      "./parcel-transformer-custom.js"
    ]
  },
  "optimizers": {
    "*.{css,scss,sass}": [
      "@parcel/optimizer-css",
      "./parcel-optimizer-critical-css.js"
    ],
    "*.{html}": [
      "@parcel/optimizer-htmlnano",
      "./parcel-optimizer-minify-html.js"
    ],
    "*.{js,jsx,ts,tsx}": [
      "@parcel/optimizer-terser"
    ],
    "*.{json}": [
      "./parcel-optimizer-json.js"
    ]
  },
  "namers": {
    "js": "[name].[hash:8].js",
    "css": "[name].[hash:8].css",
    "html": "[name].[hash:8].html",
    "image": "[name].[hash:8].[ext]",
    "*": "[name].[hash:8].[ext]"
  },
  "compressors": {
    "*.{png,jpg,jpeg,gif,webp}": [
      "@parcel/compressor-sharp"
    ],
    "*.{svg}": [
      "@parcel/compressor-svgo"
    ]
  },
  "packagers": {
    "*.{js,jsx,ts,tsx}": [
      "@parcel/packager-js"
    ],
    "*.{css,scss,sass,less}": [
      "@parcel/packager-css"
    ],
    "*.{html,hbs}": [
      "@parcel/packager-html"
    ],
    "*.{json}": [
      "@parcel/packager-json"
    ],
    "*.{png,jpg,jpeg,gif,webp,svg}": [
      "@parcel/packager-raw"
    ]
  },
  "reporters": [
    "@parcel/reporter-cli",
    "@parcel/reporter-dev-server",
    "./parcel-reporter-metrics.js"
  ],
  "resolvers": [
    "@parcel/resolver-default",
    "./parcel-resolver-custom.js"
  ],
  "runtimes": {
    "library": "@parcel/runtime-js",
    "service-worker": "@parcel/runtime-service-worker"
  },
  "browsers": {
    "production": [
      "> 0.2%",
      "not dead",
      "not op_mini all",
      "not ie 11"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  },
  "serve": {
    "port": 1234,
    "host": "localhost",
    "https": false,
    "hmr": true,
    "hmrPort": 1235
  },
  "build": {
    "target": "browser",
    "scopeHoist": false,
    "distDir": "dist",
    "cache": true,
    "cacheDir": ".cache",
    "publicUrl": "/",
    "sourceMap": true
  }
}

// ===== CUSTOM OPTIMIZER PLUGIN =====
// parcel-optimizer-critical-css.js
const { Optimizer } = require('@parcel/core');
const postcss = require('postcss');
const cssCritical = require('postcss-critical');

class CriticalCSSOptimizer extends Optimizer {
    async optimize({ bundle, bundleGraph, options }) {
        // Only optimize CSS bundles
        if (bundle.type !== 'css') {
            return { contents: await bundle.getCode() };
        }

        const source = await bundle.getCode();
        const criticalCSS = await this.extractCriticalCSS(source, bundleGraph);

        return {
            contents: criticalCSS,
          map: null
        };
    }

    async extractCriticalCSS(css, bundleGraph) {
        // Get HTML entries to know what's critical
        const htmlBundles = bundleGraph.getBundles().filter(b => b.type === 'html');

        if (htmlBundles.length === 0) {
            return css; // No HTML to extract critical CSS from
        }

        try {
          const result = await postcss([
            cssCritical({
              extract: true,
              inline: false,
              outputPath: 'critical.css'
            })
          ]).process(css, { from: undefined });

          return result.css;
        } catch (error) {
          console.warn('Failed to extract critical CSS:', error);
          return css;
        }
    }
}

module.exports = (options) => {
    return new CriticalCSSOptimizer(options);
};

// ===== PARCEL API USAGE =====
// build-script.js
import { Parcel } from '@parcel/core';
import { WorkerFarm } from '@parcel/workers';
import { defaultConfig } from '@parcel/config-default';
import { fork } from 'child_process';
import path from 'path';

async function buildProject() {
    const parcel = new Parcel({
        entries: ['src/index.html'],
        config: defaultConfig,
        defaultConfig: {},
        patchConsole: true,
        logLevel: 'info',
        shouldPatchConsole: true,
        shouldAutoInstall: true,
        shouldUseWorkerThreads: true,
        shouldProgressReporting: true,
        cacheDir: '.cache',
        mode: 'production',
        env: {
          NODE_ENV: 'production',
          PUBLIC_URL: '/app'
        },
        production: true,
        serveOptions: {
          host: 'localhost',
          port: 1234
        },
        shouldDisableCache: false,
        shouldContentHash: true
      });

    try {
        await parcel.run();
        console.log('Build completed successfully!');

        // Get build metrics
        const bundleGraph = parcel.getBundleGraph();
        const bundles = bundleGraph.getBundles();

        console.log('\nBuild Summary:');
        bundles.forEach(bundle => {
            console.log(`- ${bundle.filePath} (${bundle.type})`);
        });

    } catch (error) {
        console.error('Build failed:', error);
        process.exit(1);
    } finally {
        await parcel.stop();
    }
}

// Run the build
buildProject();

// ===== CUSTOM REPORTER PLUGIN =====
// parcel-reporter-metrics.js
const { Reporter } = require('@parcel/core');
const fs = require('fs');
const path = require('path');

class MetricsReporter extends Reporter {
    constructor(options) {
        super(options);
        this.metrics = {
            buildStartTime: null,
            buildEndTime: null,
            bundles: [],
            assets: [],
            cacheHits: 0,
            cacheMisses: 0,
            errors: [],
            warnings: []
        };
    }

    async report({ bundleGraph, buildTime, options }) {
        this.metrics.buildStartTime = Date.now() - buildTime;
        this.metrics.buildEndTime = Date.now();

        // Collect bundle metrics
        bundleGraph.getBundles().forEach(bundle => {
            this.metrics.bundles.push({
                id: bundle.id,
                filePath: bundle.filePath,
                type: bundle.type,
                size: this.getBundleSize(bundle)
            });

            bundle.getEntryAssets().forEach(asset => {
                this.metrics.assets.push({
                    id: asset.id,
                    filePath: asset.filePath,
                    type: asset.type,
                    size: asset.stats.size
                });
            });
        });

        // Write metrics to file
        await this.writeMetrics();

        // Display metrics
        this.displayMetrics();
    }

    getBundleSize(bundle) {
        let size = 0;
        bundle.getEntryAssets().forEach(asset => {
            size += asset.stats.size;
        });
        return size;
    }

    async writeMetrics() {
        const metricsPath = path.join(this.options.outDir, 'build-metrics.json');
        const metrics = {
            ...this.metrics,
            buildDuration: this.metrics.buildEndTime - this.metrics.buildStartTime,
            totalBundleSize: this.metrics.bundles.reduce((sum, b) => sum + b.size, 0),
            totalAssetSize: this.metrics.assets.reduce((sum, a) => sum + a.size, 0),
            timestamp: new Date().toISOString()
        };

        fs.writeFileSync(metricsPath, JSON.stringify(metrics, null, 2));
    }

    displayMetrics() {
        console.log('\n📊 Build Metrics Report');
        console.log('========================');
        console.log(`Build Time: ${this.metrics.buildEndTime - this.metrics.buildStartTime}ms`);
        console.log(`Bundles: ${this.metrics.bundles.length}`);
        console.log(`Assets: ${this.metrics.assets.length}`);
        console.log(`Total Size: ${(this.metrics.bundles.reduce((sum, b) => sum + b.size, 0) / 1024).toFixed(2)}KB`);

        console.log('\nBundle Details:');
        this.metrics.bundles.forEach(bundle => {
            console.log(`  ${bundle.filePath}: ${(bundle.size / 1024).toFixed(2)}KB`);
        });
    }
}

module.exports = (options) => {
    return new MetricsReporter(options);
};

// ===== WORKFLOW INTEGRATION =====
// .github/workflows/build.yml
name: Build with Parcel

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  build:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [16.x, 18.x]

    steps:
    - name: Checkout code
      uses: actions/checkout@v3

    - name: Setup Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v3
      with:
        node-version: ${{ matrix.node-version }}
        cache: 'npm'

    - name: Install dependencies
      run: npm ci

    - name: Run tests
      run: npm test

    - name: Build with Parcel
      run: npm run build

    - name: Upload build artifacts
      uses: actions/upload-artifact@v3
      with:
        name: dist-${{ matrix.node-version }}
        path: dist/

    - name: Deploy to staging (if main branch)
      if: github.ref == 'refs/heads/main'
      run: |
        echo "Deploy to staging environment"
        # Add deployment commands here

// ===== VERCEL CONFIGURATION =====
// vercel.json
{
  "buildCommand": "npm run build",
  "outputDirectory": "dist",
  "installCommand": "npm ci",
  "framework": null,
  "devCommand": "npm start",
  "functions": {},
  "headers": [
    {
      "source": "/static/(.*)",
      "headers": [
        {
          "key": "Cache-Control",
          "value": "public, max-age=31536000, immutable"
        }
      ]
    }
  ],
  "rewrites": [
    {
      "source": "/(.*)",
      "destination": "/index.html"
    }
  ]
}

// ===== NETLIFY CONFIGURATION =====
// netlify.toml
[build]
  publish = "dist"
  command = "npm run build"

[build.environment]
  NODE_VERSION = "18"

[[headers]]
  for = "/static/*"
  [headers.values]
    Cache-Control = "public, max-age=31536000, immutable"

[[redirects]]
  from = "/*"
  to = "/index.html"
  status = 200

💻 Flujo de Trabajo y Despliegue de Parcel yaml

🟡 intermediate ⭐⭐⭐

Flujo de trabajo de desarrollo completo de Parcel incluyendo pruebas, CI/CD y estrategias de despliegue

⏱️ 30 min 🏷️ parcel, workflow, deployment
Prerequisites: Parcel basics, DevOps concepts, CI/CD knowledge
# Parcel Development Workflow and Deployment
# Complete setup for modern web application development with Parcel

# ===== DOCKER SETUP =====

# Dockerfile for Node.js application with Parcel
FROM node:18-alpine AS base

# Set working directory
WORKDIR /app

# Copy package files
COPY package*.json ./

# Install dependencies
RUN npm ci --only=production && npm cache clean --force

# Development stage
FROM base AS development
RUN npm ci
COPY . .

EXPOSE 1234
CMD ["npm", "start"]

# Production build stage
FROM base AS build
RUN npm ci
COPY . .
RUN npm run build

# Production stage
FROM nginx:alpine AS production

# Copy build artifacts
COPY --from=build /app/dist /usr/share/nginx/html

# Copy nginx configuration
COPY nginx.conf /etc/nginx/nginx.conf

EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

# ===== DOCKER COMPOSE =====

# docker-compose.yml
version: '3.8'

services:
  app:
    build:
      context: .
      target: development
    ports:
      - "1234:1234"
      - "1235:1235"  # HMR port
    volumes:
      - .:/app
      - /app/node_modules
      - /app/.cache
    environment:
      - NODE_ENV=development
      - CHOKIDAR_USEPOLLING=true
    command: npm start

  build:
    build:
      context: .
      target: build
    volumes:
      - ./dist:/app/dist
    environment:
      - NODE_ENV=production
    command: npm run build

  production:
    build:
      context: .
      target: production
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      - build

  # Testing service
  test:
    build:
      context: .
      target: development
    volumes:
      - .:/app
      - /app/node_modules
      - /app/.cache
    environment:
      - NODE_ENV=test
      - CI=true
    command: npm test

# ===== Nginx Configuration =====

# nginx.conf
events {
    worker_connections 1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    # Gzip compression
    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_types
        text/plain
        text/css
        text/xml
        text/javascript
        application/javascript
        application/xml+rss
        application/json;

    server {
        listen 80;
        server_name localhost;
        root /usr/share/nginx/html;
        index index.html;

        # Security headers
        add_header X-Frame-Options "SAMEORIGIN" always;
        add_header X-XSS-Protection "1; mode=block" always;
        add_header X-Content-Type-Options "nosniff" always;
        add_header Referrer-Policy "no-referrer-when-downgrade" always;
        add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:;" always;

        # Handle client-side routing
        location / {
            try_files $uri $uri/ /index.html;
        }

        # Cache static assets
        location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
            expires 1y;
            add_header Cache-Control "public, immutable";
        }

        # Health check endpoint
        location /health {
            access_log off;
            return 200 "healthy\n";
            add_header Content-Type text/plain;
        }
    }
}

# ===== GITHUB ACTIONS WORKFLOW =====

# .github/workflows/ci.yml
name: CI/CD Pipeline

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

env:
  NODE_VERSION: 18
  CACHE_VERSION: v1

jobs:
  # Lint and type checking
  lint:
    runs-on: ubuntu-latest

    steps:
    - name: Checkout code
      uses: actions/checkout@v3

    - name: Setup Node.js
      uses: actions/setup-node@v3
      with:
        node-version: ${{ env.NODE_VERSION }}
        cache: 'npm'

    - name: Install dependencies
      run: npm ci

    - name: Run ESLint
      run: npm run lint

    - name: Run TypeScript check
      run: npm run type-check

  # Test suite
  test:
    runs-on: ubuntu-latest

    steps:
    - name: Checkout code
      uses: actions/checkout@v3

    - name: Setup Node.js
      uses: actions/setup-node@v3
      with:
        node-version: ${{ env.NODE_VERSION }}
        cache: 'npm'

    - name: Install dependencies
      run: npm ci

    - name: Run tests
      run: npm run test:ci

    - name: Upload coverage reports
      uses: codecov/codecov-action@v3
      if: success()
      with:
        directory: ./coverage

  # Build application
  build:
    runs-on: ubuntu-latest
    needs: [lint, test]

    strategy:
      matrix:
        environment: [development, production]

    steps:
    - name: Checkout code
      uses: actions/checkout@v3

    - name: Setup Node.js
      uses: actions/setup-node@v3
      with:
        node-version: ${{ env.NODE_VERSION }}
        cache: 'npm'

    - name: Install dependencies
      run: npm ci

    - name: Build application
      run: npm run build:${{ matrix.environment }}
      env:
        NODE_ENV: ${{ matrix.environment }}

    - name: Analyze bundle
      run: npm run analyze

    - name: Upload build artifacts
      uses: actions/upload-artifact@v3
      with:
        name: build-${{ matrix.environment }}
        path: |
          dist/
          bundle-analysis.html
        retention-days: 7

  # Security scanning
  security:
    runs-on: ubuntu-latest
    needs: [lint, test]

    steps:
    - name: Checkout code
      uses: actions/checkout@v3

    - name: Run security audit
      run: npm audit --audit-level=high

    - name: Run Snyk security scan
      uses: snyk/actions/node@master
      env:
        SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}

  # Docker build and push
  docker:
    runs-on: ubuntu-latest
    needs: [build]
    if: github.ref == 'refs/heads/main'

    steps:
    - name: Checkout code
      uses: actions/checkout@v3

    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v2

    - name: Login to Docker Hub
      uses: docker/login-action@v2
      with:
        username: ${{ secrets.DOCKER_USERNAME }}
        password: ${{ secrets.DOCKER_PASSWORD }}

    - name: Build and push Docker image
      uses: docker/build-push-action@v4
      with:
        context: .
        push: true
        tags: |
          ${{ secrets.DOCKER_USERNAME }}/my-parcel-app:latest
          ${{ secrets.DOCKER_USERNAME }}/my-parcel-app:${{ github.sha }}
        cache-from: type=gha
        cache-to: type=gha,mode=max

  # Deploy to staging
  deploy-staging:
    runs-on: ubuntu-latest
    needs: [build, docker]
    if: github.ref == 'refs/heads/develop'
    environment: staging

    steps:
    - name: Deploy to staging
      uses: appleboy/ssh-action@master
      with:
        host: ${{ secrets.STAGING_HOST }}
        username: ${{ secrets.STAGING_USERNAME }}
        key: ${{ secrets.STAGING_SSH_KEY }}
        script: |
          cd /opt/my-parcel-app
          docker-compose pull
          docker-compose up -d
          docker-compose exec app npm run migrate

    - name: Run integration tests
      run: npm run test:e2e

  # Deploy to production
  deploy-production:
    runs-on: ubuntu-latest
    needs: [build, docker]
    if: github.ref == 'refs/heads/main'
    environment: production

    steps:
    - name: Deploy to production
      uses: appleboy/ssh-action@master
      with:
        host: ${{ secrets.PROD_HOST }}
        username: ${{ secrets.PROD_USERNAME }}
        key: ${{ secrets.PROD_SSH_KEY }}
        script: |
          cd /opt/my-parcel-app
          docker-compose pull
          docker-compose up -d
          docker-compose exec app npm run migrate

# ===== GITLAB CI/CD =====

# .gitlab-ci.yml
stages:
  - lint
  - test
  - build
  - deploy

variables:
  NODE_VERSION: "18"
  DOCKER_DRIVER: overlay2
  DOCKER_TLS_CERTDIR: "/certs"

cache:
  key: "$CI_COMMIT_REF_SLUG"
  paths:
    - node_modules/

before_script:
  - npm ci

# Linting
lint:
  stage: lint
  script:
    - npm run lint
    - npm run type-check
  only:
    - branches
    - merge_requests

# Testing
test:
  stage: test
  script:
    - npm run test:ci
    - npm run test:e2e
  coverage: '/All files[^|]*\|[^|]*\s+\d+.\d+\s*/'
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: coverage/cobertura-coverage.xml
    paths:
      - coverage/
  only:
    - branches
    - merge_requests

# Building
build:
  stage: build
  script:
    - npm run build
  artifacts:
    paths:
      - dist/
      - bundle-analysis.html
    expire_in: 1 week
  only:
    - branches

# Deploy to staging
deploy_staging:
  stage: deploy
  script:
    - echo "Deploy to staging environment"
  environment:
    name: staging
    url: https://staging.myapp.com
  only:
    - develop

# Deploy to production
deploy_production:
  stage: deploy
  script:
    - echo "Deploy to production environment"
  environment:
    name: production
    url: https://myapp.com
  when: manual
  only:
    - main

# ===== TRAVIS CI =====

# .travis.yml
language: node_js
node_js:
  - "18"
cache:
  npm: true
  directories:
    - .cache

branches:
  only:
    - main
    - develop
    - /^release\/.*$/

install:
  - npm ci

script:
  - npm run lint
  - npm run type-check
  - npm run test:ci
  - npm run build

after_success:
  - npm run coverage

deploy:
  provider: script
  script:
    - echo "Deploying to staging"
  on:
    branch: develop

# ===== VAGRANT SETUP =====

# Vagrantfile
Vagrant.configure("2") do |config|
  config.vm.box = "ubuntu/focal64"

  # Forward ports
  config.vm.network "forwarded_port", guest: 1234, host: 1234
  config.vm.network "forwarded_port", guest: 1235, host: 1235

  # Sync folders
  config.vm.synced_folder ".", "/vagrant",
    mount_options: ["dmode=777,fmode=777"],
    exclude: [".git/", "node_modules/", ".cache/"]

  # Provision the VM
  config.vm.provision "shell", inline: <<-SHELL
    apt-get update
    apt-get install -y curl git

    # Install Node.js
    curl -fsSL https://deb.nodesource.com/setup_18.x | bash -
    apt-get install -y nodejs

    # Install PM2 for process management
    npm install -g pm2

    # Install project dependencies
    cd /vagrant
    npm ci
  SHELL

  # Set up PM2 process
  config.vm.provision "shell", run: "always", inline: <<-SHELL
    cd /vagrant
    pm2 delete all 2>/dev/null || true
    pm2 start ecosystem.config.js --env development
  SHELL
end

# PM2 ecosystem configuration
module.exports = {
  apps: [{
    name: 'my-parcel-app',
    script: 'npm',
    args: 'start',
    instances: 'max',
    exec_mode: 'cluster',
    env: {
      NODE_ENV: 'development',
      PORT: 1234
    },
    env_production: {
      NODE_ENV: 'production',
      PORT: 1234
    },
    log_date_format: 'YYYY-MM-DD HH:mm Z',
    error_file: 'logs/err.log',
    out_file: 'logs/out.log',
    log_file: 'logs/combined.log',
    time: true
  }]
};

# ===== KUBERNETES DEPLOYMENT =====

# k8s/deployment.yml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-parcel-app
  labels:
    app: my-parcel-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-parcel-app
  template:
    metadata:
      labels:
        app: my-parcel-app
    spec:
      containers:
      - name: my-parcel-app
        image: myusername/my-parcel-app:latest
        ports:
        - containerPort: 80
        env:
        - name: NODE_ENV
          value: "production"
        - name: API_URL
          valueFrom:
            secretKeyRef:
              name: app-secrets
              key: API_URL
        resources:
          requests:
            memory: "256Mi"
            cpu: "250m"
          limits:
            memory: "512Mi"
            cpu: "500m"
        livenessProbe:
          httpGet:
            path: /health
            port: 80
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /health
            port: 80
          initialDelaySeconds: 5
          periodSeconds: 5

---
apiVersion: v1
kind: Service
metadata:
  name: my-parcel-app-service
spec:
  selector:
    app: my-parcel-app
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
  type: LoadBalancer

---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-parcel-app-ingress
  annotations:
    kubernetes.io/ingress.class: nginx
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
    nginx.ingress.kubernetes.io/rate-limit: "100"
spec:
  tls:
  - hosts:
    - myapp.com
    secretName: myapp-tls
  rules:
  - host: myapp.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: my-parcel-app-service
            port:
              number: 80

# ===== TERRAFORM INFRASTRUCTURE =====

# terraform/main.tf
provider "aws" {
  region = var.aws_region
}

# S3 bucket for static assets
resource "aws_s3_bucket" "static_assets" {
  bucket = "${var.project_name}-static-assets-${random_id.bucket_suffix.result}"
  acl = "private"

  website {
    index_document = "index.html"
    error_document = "404.html"
  }

  tags = {
    Name = var.project_name
    Environment = var.environment
  }
}

# CloudFront distribution
resource "aws_cloudfront_distribution" "cdn" {
  origin {
    domain_name = aws_s3_bucket.static_assets.bucket_regional_domain_name
    origin_id = "S3-${aws_s3_bucket.static_assets.bucket}"

    s3_origin_config {
      origin_access_identity = aws_cloudfront_origin_access_identity.default.id
    }
  }

  enabled = true
  is_ipv6_enabled = true
  comment = "${var.project_name} CDN"

  default_cache_behavior {
    allowed_methods = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
    cached_methods = ["GET", "HEAD"]
    target_origin_id = "S3-${aws_s3_bucket.static_assets.bucket}"

    forwarded_values {
      query_string = false
      cookies {
        forward = "none"
      }
    }

    min_ttl = 0
    default_ttl = 3600
    max_ttl = 86400
    compress = true

    viewer_protocol_policy = "redirect-to-https"
  }

  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }

  viewer_certificate {
    cloudfront_default_certificate = true
  }

  tags = {
    Name = var.project_name
    Environment = var.environment
  }
}

# Route 53 hosted zone
resource "aws_route53_record" "www" {
  zone_id = data.aws_route53_zone.main.zone_id
  name    = "www.${var.domain_name}"
  type    = "A"

  alias {
    name                   = aws_cloudfront_distribution.cdn.domain_name
    zone_id               = aws_cloudfront_distribution.cdn.hosted_zone_id
    evaluate_target_health = true
  }
}