UNPKG

@pulzar/core

Version:

Next-generation Node.js framework for ultra-fast web applications with zero-reflection DI, GraphQL, WebSockets, events, and edge runtime support

484 lines (482 loc) 15.9 kB
import * as esbuild from "esbuild"; import * as fs from "fs/promises"; import * as path from "path"; import { watch } from "chokidar"; import { getPlatformCapabilities, createEdgeError, } from "./types"; const DEFAULT_EXTERNALS = [ // Node.js built-ins that should be external in edge environments "fs", "path", "os", "crypto", "stream", "buffer", "util", "events", "url", "querystring", "http", "https", "net", "tls", "zlib", "child_process", "cluster", "dgram", "dns", "readline", "repl", // Node.js modules with node: prefix "node:fs", "node:path", "node:os", "node:crypto", "node:stream", "node:buffer", "node:util", "node:events", "node:url", "node:http", "node:https", "node:net", "node:tls", "node:zlib", ]; export class EdgeBundler { options; esbuildContext; watcher; isWatching = false; constructor(options) { this.options = { minify: true, sourcemap: false, external: [], define: {}, format: "esm", splitting: false, metafile: false, bundle: true, keepNames: false, legalComments: "none", treeShaking: true, watch: false, polyfills: true, frameworkName: "Pulzar", ...options, }; // Platform-specific defaults const capabilities = getPlatformCapabilities(this.options.platform); if (!this.options.target) { this.options.target = this.getPlatformTarget(); } } /** * Bundle the application for edge deployment */ async bundle() { const startTime = Date.now(); try { this.options.onBuildStart?.(); const buildOptions = await this.createBuildOptions(); const result = await esbuild.build(buildOptions); const duration = Date.now() - startTime; const bundleResult = await this.processBuildResult(result, duration); this.options.onBuildEnd?.(result, duration); return bundleResult; } catch (error) { const duration = Date.now() - startTime; const edgeError = error instanceof Error ? error : new Error(String(error)); this.options.onBuildError?.(edgeError); return { success: false, outputFiles: [], warnings: [], errors: [ { text: edgeError.message, detail: "", id: "", location: null, notes: [], pluginName: "", }, ], duration, size: 0, }; } } /** * Start watching for changes and rebuild automatically */ async startWatch() { if (this.isWatching) { throw createEdgeError("Bundler is already watching", 400, this.options.platform); } try { const buildOptions = await this.createBuildOptions(); this.esbuildContext = await esbuild.context(buildOptions); // Start esbuild watch await this.esbuildContext.watch(); // Setup file watcher for additional files this.setupFileWatcher(); this.isWatching = true; console.log(`📦 ${this.options.frameworkName} bundler watching for changes...`); } catch (error) { throw createEdgeError(`Failed to start watch mode: ${error}`, 500, this.options.platform); } } /** * Stop watching */ async stopWatch() { if (!this.isWatching) return; if (this.esbuildContext) { await this.esbuildContext.dispose(); this.esbuildContext = undefined; } if (this.watcher) { await this.watcher.close(); this.watcher = undefined; } this.isWatching = false; console.log(`📦 ${this.options.frameworkName} bundler stopped watching`); } /** * Analyze bundle contents */ async analyze() { const buildOptions = await this.createBuildOptions(); buildOptions.metafile = true; const result = await esbuild.build(buildOptions); if (!result.metafile) { throw createEdgeError("Failed to generate metafile for analysis", 500, this.options.platform); } const analysis = await esbuild.analyzeMetafile(result.metafile, { verbose: true, }); const dependencies = this.extractDependencies(result.metafile); return { metafile: result.metafile, analysis, dependencies, }; } /** * Get platform-specific build target */ getPlatformTarget() { const platformTargets = { cloudflare: "es2022", vercel: "es2020", deno: "es2022", netlify: "es2020", "aws-lambda": "es2020", "azure-functions": "es2019", }; return platformTargets[this.options.platform] || "es2020"; } /** * Create esbuild configuration */ async createBuildOptions() { const { platform, entry, outDir, outFile } = this.options; const capabilities = getPlatformCapabilities(platform); // Platform-specific externals const platformExternals = this.getPlatformExternals(); const allExternals = [ ...DEFAULT_EXTERNALS, ...platformExternals, ...(this.options.external || []), ]; const buildOptions = { entryPoints: [entry], outdir: outFile ? undefined : outDir, outfile: outFile, bundle: this.options.bundle, minify: this.options.minify, sourcemap: this.options.sourcemap, target: this.options.target, format: this.options.format === "esm" ? "esm" : "cjs", platform: platform === "deno" ? "neutral" : "browser", splitting: this.options.splitting && this.options.format === "esm", treeShaking: this.options.treeShaking, metafile: this.options.metafile, keepNames: this.options.keepNames, legalComments: this.options.legalComments, external: allExternals, define: { "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV || "production"), "process.env.PLATFORM": JSON.stringify(platform), "process.env.FRAMEWORK": JSON.stringify(this.options.frameworkName), ...(platform === "cloudflare" && { global: "globalThis" }), ...this.options.define, }, plugins: [ this.createPolyfillPlugin(), this.createPlatformPlugin(), this.createSizeAnalyzerPlugin(), ], logLevel: "info", color: true, }; // Platform-specific optimizations if (platform === "cloudflare") { buildOptions.conditions = ["workerd", "worker", "browser"]; } else if (platform === "deno") { buildOptions.conditions = ["deno", "worker", "browser"]; } return buildOptions; } /** * Get platform-specific externals */ getPlatformExternals() { switch (this.options.platform) { case "cloudflare": // Cloudflare Workers have very limited Node.js APIs return [ "fs", "path", "os", "child_process", "cluster", "dgram", "dns", "module", "readline", "repl", "tls", "v8", "vm", ]; case "vercel": case "netlify": // These platforms have some Node.js compatibility return ["fs", "child_process", "cluster"]; case "deno": // Deno has its own APIs return ["fs", "path", "child_process", "cluster"]; default: return []; } } /** * Create polyfill plugin */ createPolyfillPlugin() { const options = this.options; return { name: `${(options.frameworkName || "pulzar").toLowerCase()}-polyfills`, setup(build) { if (!options.polyfills) return; build.onLoad({ filter: /\.(ts|js)$/ }, async (args) => { const contents = await fs.readFile(args.path, "utf8"); // Add polyfills to entry file if (args.path.includes(options.entry)) { const polyfills = ` // ${options.frameworkName || "Pulzar"} Edge Runtime Polyfills for ${options.platform} ${options.platform === "cloudflare" ? ` if (typeof global === 'undefined') { globalThis.global = globalThis; } ` : ""} ${options.platform === "deno" ? ` if (typeof process === 'undefined' && typeof Deno !== 'undefined') { globalThis.process = { env: Deno.env.toObject(), platform: 'deno', version: Deno.version.deno, }; } ` : ""} // Import polyfills if available try { await import('./polyfills.js'); } catch (e) { // Polyfills not available } `.trim(); return { contents: polyfills + "\n" + contents, loader: args.path.endsWith(".ts") ? "ts" : "js", }; } return null; }); }, }; } /** * Create platform-specific plugin */ createPlatformPlugin() { const options = this.options; return { name: `${(options.frameworkName || "pulzar").toLowerCase()}-platform`, setup(build) { // Replace Node.js modules with platform-specific alternatives if (options.platform === "cloudflare") { build.onResolve({ filter: /^(fs|path|os)$/ }, (args) => { return { path: args.path, namespace: "cloudflare-stub", }; }); build.onLoad({ filter: /.*/, namespace: "cloudflare-stub" }, (args) => { return { contents: `export default {}; // ${args.path} not available in Cloudflare Workers`, loader: "js", }; }); } }, }; } /** * Create bundle size analyzer plugin */ createSizeAnalyzerPlugin() { const options = this.options; return { name: `${(options.frameworkName || "pulzar").toLowerCase()}-size-analyzer`, setup(build) { build.onEnd((result) => { if (result.metafile) { const totalSize = Object.values(result.metafile.outputs).reduce((total, output) => total + output.bytes, 0); const maxSize = getPlatformCapabilities(options.platform).maxRequestSize; if (totalSize > maxSize) { console.warn(`⚠️ Bundle size (${Math.round(totalSize / 1024)}KB) exceeds ${options.platform} limit (${Math.round(maxSize / 1024)}KB)`); } else { console.log(`📦 Bundle size: ${Math.round(totalSize / 1024)}KB (${Math.round((totalSize / maxSize) * 100)}% of ${options.platform} limit)`); } } }); }, }; } /** * Setup file watcher for additional files */ setupFileWatcher() { const watchPaths = [ this.options.entry, path.dirname(this.options.entry) + "/**/*.{ts,js,json}", ]; this.watcher = watch(watchPaths, { ignored: ["node_modules/**", this.options.outDir + "/**"], persistent: true, }); this.watcher.on("change", (filePath) => { console.log(`📝 File changed: ${path.relative(process.cwd(), filePath)}`); }); this.watcher.on("error", (error) => { console.error("👀 Watcher error:", error); }); } /** * Process build result */ async processBuildResult(result, duration) { const outputFiles = []; let totalSize = 0; if (result.metafile) { for (const [outputPath, output] of Object.entries(result.metafile.outputs)) { outputFiles.push(outputPath); totalSize += output.bytes; } } // If no metafile, estimate size from output directory if (outputFiles.length === 0) { try { const files = await fs.readdir(this.options.outDir); for (const file of files) { const filePath = path.join(this.options.outDir, file); const stats = await fs.stat(filePath); if (stats.isFile()) { outputFiles.push(filePath); totalSize += stats.size; } } } catch (error) { // Output directory might not exist yet } } return { success: result.errors.length === 0, outputFiles, metafile: result.metafile, warnings: result.warnings, errors: result.errors, duration, size: totalSize, }; } /** * Extract dependencies from metafile */ extractDependencies(metafile) { const totalSize = this.getTotalBundleSize(metafile); const dependencies = []; for (const [inputPath, input] of Object.entries(metafile.inputs)) { if (input.bytes > 0) { const name = path.basename(inputPath); const percentage = (input.bytes / totalSize) * 100; dependencies.push({ name, size: input.bytes, percentage, }); } } return dependencies.sort((a, b) => b.size - a.size); } /** * Get total bundle size from metafile */ getTotalBundleSize(metafile) { return Object.values(metafile.outputs).reduce((total, output) => total + output.bytes, 0); } /** * Get build statistics */ getStats() { return { platform: this.options.platform, isWatching: this.isWatching, options: { ...this.options }, }; } } /** * Create and configure an edge bundler */ export function createEdgeBundler(options) { return new EdgeBundler(options); } /** * Quick bundle function for simple use cases */ export async function bundleForEdge(entry, platform, outDir, options = {}) { const bundler = createEdgeBundler({ entry, platform, outDir, ...options, }); return bundler.bundle(); } export default EdgeBundler; //# sourceMappingURL=bundler.js.map