mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-05 08:02:18 +00:00
263 lines
7.5 KiB
TypeScript
263 lines
7.5 KiB
TypeScript
// deno-lint-ignore-file no-explicit-any
|
|
import * as fs from "node:fs";
|
|
import * as path from "node:path";
|
|
import process from "node:process";
|
|
import { spawn } from "node:child_process";
|
|
import { log, colors } from "../../../deps.ts";
|
|
import { windmillUtils } from "../../../deps.ts";
|
|
export interface BundleOptions {
|
|
entryPoint?: string;
|
|
outDir?: string;
|
|
sourcemap?: boolean;
|
|
minify?: boolean;
|
|
production?: boolean;
|
|
}
|
|
|
|
export interface BundleResult {
|
|
js: string;
|
|
css: string;
|
|
}
|
|
|
|
export const DEFAULT_BUILD_OPTIONS = {
|
|
bundle: true,
|
|
format: "iife" as const,
|
|
platform: "browser" as const,
|
|
target: "es2020",
|
|
jsx: "automatic" as const,
|
|
loader: {
|
|
".css": "css" as const,
|
|
},
|
|
logLevel: "info" as const,
|
|
write: true,
|
|
};
|
|
|
|
/**
|
|
* Detects which frontend frameworks are present in package.json
|
|
*/
|
|
export function detectFrameworks(appDir: string): { svelte: boolean; vue: boolean } {
|
|
const packageJsonPath = path.join(appDir, "package.json");
|
|
if (!fs.existsSync(packageJsonPath)) {
|
|
return { svelte: false, vue: false };
|
|
}
|
|
|
|
try {
|
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
const allDeps = {
|
|
...packageJson.dependencies,
|
|
...packageJson.devDependencies,
|
|
};
|
|
|
|
return {
|
|
svelte: "svelte" in allDeps,
|
|
vue: "vue" in allDeps,
|
|
};
|
|
} catch {
|
|
return { svelte: false, vue: false };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates framework-specific esbuild plugins based on detected dependencies
|
|
*/
|
|
export async function createFrameworkPlugins(appDir: string): Promise<any[]> {
|
|
const frameworks = detectFrameworks(appDir);
|
|
const plugins: any[] = [];
|
|
|
|
if (frameworks.svelte) {
|
|
log.info(colors.blue("🔧 Svelte detected, adding svelte plugin..."));
|
|
try {
|
|
const esbuildSvelte = await import("npm:esbuild-svelte@0.9.3");
|
|
const sveltePreprocess = await import("npm:svelte-preprocess@6.0.3");
|
|
plugins.push(
|
|
esbuildSvelte.default({
|
|
preprocess: sveltePreprocess.default(),
|
|
})
|
|
);
|
|
} catch (error: any) {
|
|
log.warn(colors.yellow(`Failed to load svelte plugin: ${error.message}`));
|
|
}
|
|
}
|
|
|
|
if (frameworks.vue) {
|
|
log.info(colors.blue("🔧 Vue detected, adding vue plugin..."));
|
|
try {
|
|
const esbuildPluginVue = await import("npm:esbuild-plugin-vue3@0.5.1");
|
|
plugins.push(esbuildPluginVue.default());
|
|
} catch (error: any) {
|
|
log.warn(colors.yellow(`Failed to load vue plugin: ${error.message}`));
|
|
}
|
|
}
|
|
|
|
return plugins;
|
|
}
|
|
|
|
/**
|
|
* Ensures node_modules exists in the specified directory
|
|
* Runs npm install if node_modules is missing
|
|
* @param appDir Directory to check for node_modules (defaults to entry point directory)
|
|
*/
|
|
export async function ensureNodeModules(appDir?: string): Promise<void> {
|
|
const targetDir = appDir ?? process.cwd();
|
|
const nodeModulesPath = path.join(targetDir, "node_modules");
|
|
|
|
if (!fs.existsSync(nodeModulesPath)) {
|
|
log.info(colors.yellow("📦 node_modules not found, running npm install..."));
|
|
const code = await new Promise<number>((resolve, reject) => {
|
|
const npmInstall = spawn("npm", ["install"], {
|
|
cwd: targetDir,
|
|
stdio: "inherit",
|
|
shell: true,
|
|
});
|
|
npmInstall.on("close", (code) => resolve(code ?? 0));
|
|
npmInstall.on("error", reject);
|
|
});
|
|
if (code !== 0) {
|
|
throw new Error(`npm install failed with exit code ${code}`);
|
|
}
|
|
log.info(colors.green("✅ npm install completed"));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates an esbuild bundle for the app
|
|
* @param options Bundle configuration options
|
|
* @returns Bundle result containing JS and CSS blobs
|
|
*/
|
|
export async function createBundle(
|
|
options: BundleOptions = {}
|
|
): Promise<BundleResult> {
|
|
// Dynamically import esbuild
|
|
const esbuild = await import("npm:esbuild@0.24.2");
|
|
|
|
// Detect frameworks to determine default entry point
|
|
const frameworks = detectFrameworks(process.cwd());
|
|
const defaultEntry = (frameworks.svelte || frameworks.vue) ? "index.ts" : "index.tsx";
|
|
|
|
const entryPoint = options.entryPoint ?? defaultEntry;
|
|
const outDir = options.outDir ?? "dist";
|
|
const sourcemap = options.sourcemap ?? false;
|
|
const minify = options.minify ?? true;
|
|
const production = options.production ?? true;
|
|
|
|
// Verify entry point exists
|
|
if (!fs.existsSync(entryPoint)) {
|
|
throw new Error(
|
|
`Entry point "${entryPoint}" not found. Please ensure the file exists.`
|
|
);
|
|
}
|
|
|
|
// Ensure node_modules exists in the app directory
|
|
const appDir = path.dirname(entryPoint) || process.cwd();
|
|
await ensureNodeModules(appDir);
|
|
|
|
// Load framework-specific plugins (svelte, vue) based on package.json
|
|
const frameworkPlugins = await createFrameworkPlugins(appDir);
|
|
|
|
// Ensure output directory exists
|
|
const distDir = path.join(process.cwd(), outDir);
|
|
if (!fs.existsSync(distDir)) {
|
|
fs.mkdirSync(distDir, { recursive: true });
|
|
}
|
|
|
|
const outfile = path.join(outDir, "bundle.js");
|
|
|
|
// log.info("FOO")
|
|
// log.info("wmillTs" + JSON.stringify(wmillTs));
|
|
// Plugin to provide /wmill.ts as a virtual module
|
|
const wmillTs = (windmillUtils.wmillTsRaw as any).default ?? windmillUtils.wmillTsRaw;
|
|
|
|
const wmillPlugin = {
|
|
name: "wmill-virtual",
|
|
setup(build: any) {
|
|
|
|
|
|
// Intercept imports of /wmill.ts, /wmill, ./wmill.ts, or ./wmill
|
|
build.onResolve({ filter: /^(\.\/|\/)?wmill(\.ts)?$/ }, (args: any) => {
|
|
log.info(colors.yellow(`[wmill-virtual] Intercepted: ${args.path}`));
|
|
return {
|
|
path: args.path,
|
|
namespace: "wmill-virtual",
|
|
};
|
|
});
|
|
|
|
// Provide the virtual module content
|
|
build.onLoad({ filter: /.*/, namespace: "wmill-virtual" }, (args: any) => {
|
|
log.info(colors.yellow(`[wmill-virtual] Loading virtual module: ${args.path}`));
|
|
return {
|
|
contents: wmillTs,
|
|
loader: "ts",
|
|
};
|
|
});
|
|
},
|
|
};
|
|
|
|
const buildOptions = {
|
|
...DEFAULT_BUILD_OPTIONS,
|
|
entryPoints: [entryPoint],
|
|
outfile,
|
|
sourcemap,
|
|
minify,
|
|
define: {
|
|
"process.env.NODE_ENV": production ? '"production"' : '"development"',
|
|
},
|
|
plugins: [...frameworkPlugins, wmillPlugin],
|
|
};
|
|
|
|
log.info(colors.blue("📦 Building bundle..."));
|
|
|
|
try {
|
|
const result = await esbuild.build(buildOptions);
|
|
|
|
if (result.errors.length > 0) {
|
|
log.error(colors.red("❌ Build failed:"));
|
|
result.errors.forEach((error: any) => {
|
|
log.error(colors.red(error.text));
|
|
});
|
|
throw new Error("Build failed with errors");
|
|
}
|
|
|
|
log.info(colors.green("✅ Bundle created successfully"));
|
|
|
|
// Read the generated files
|
|
const jsPath = path.join(process.cwd(), outfile);
|
|
const cssPath = path.join(process.cwd(), outDir, "bundle.css");
|
|
|
|
if (!fs.existsSync(jsPath)) {
|
|
throw new Error(`Expected JS bundle at ${jsPath} but file not found`);
|
|
}
|
|
|
|
const jsContent = fs.readFileSync(jsPath, "utf-8");
|
|
const cssContent = fs.existsSync(cssPath)
|
|
? fs.readFileSync(cssPath, "utf-8")
|
|
: "";
|
|
|
|
try {
|
|
fs.rmSync(distDir, { recursive: true });
|
|
} catch {
|
|
//ignore
|
|
}
|
|
return { js: jsContent, css: cssContent };
|
|
|
|
} finally {
|
|
// Stop esbuild
|
|
await esbuild.stop();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the esbuild build options for use in watch mode (dev server)
|
|
* @param entryPoint Entry point file
|
|
* @returns esbuild build options
|
|
*/
|
|
export function getDevBuildOptions(entryPoint: string = "index.tsx") {
|
|
return {
|
|
...DEFAULT_BUILD_OPTIONS,
|
|
entryPoints: [entryPoint],
|
|
outfile: "dist/bundle.js",
|
|
sourcemap: true,
|
|
define: {
|
|
"process.env.NODE_ENV": '"development"',
|
|
},
|
|
};
|
|
}
|