🎯 Exemples recommandés
Balanced sample collections from various categories for you to explore
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
💻 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>© 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 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
}
}