Outil de Bundling Parcel

Exemples de l'outil de bundling Parcel de configuration zéro incluant la configuration de projet, les plugins et la configuration avancée

Key Facts

Category
Build Tools
Items
3
Format Families
json, yaml

Sample Overview

Exemples de l'outil de bundling Parcel de configuration zéro incluant la configuration de projet, les plugins et la configuration avancée This sample set belongs to Build Tools and can be used to test related workflows inside Elysia Tools.

💻 Configuration de Base Parcel json

🟢 simple

Commencer avec l'outil de bundling Parcel de configuration zéro pour les applications web modernes

⏱️ 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'],
  moduleNameMapper: {
    '\\.(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 et Configuration Parcel javascript

🟡 intermediate ⭐⭐⭐⭐

Configuration avancée de Parcel avec plugins, transformateurs personnalisés et optimisation de construction

⏱️ 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

💻 Flux de Travail et Déploiement Parcel yaml

🟡 intermediate ⭐⭐⭐

Flux de travail de développement complet de Parcel incluant les tests, CI/CD et les stratégies de déploiement

⏱️ 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
  }
}