@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
JavaScript
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