Files
Dannon Baker 5cbd14e8f0 Scaffold @galaxyproject/galaxy-ui internal workspace package
Adds packages/ui/ as a second workspace member alongside api-client --
private, source-only, no library build. Empty src/index.ts barrel;
components and composables move in next.

Wires galaxy-ui into the main client as a workspace:* dep and as an
unconditional resolve.alias in vite.config.mjs (serve and build). The
api-client alias stays serve-only because it has a real dist/ build
and production consumes that via package.json exports; galaxy-ui is
internal-only so we skip the build step entirely and let the main
client's @vitejs/plugin-vue2 handle the Vue 2 SFCs from source in
both dev and prod.

client/packages/README.md now lists both packages and calls out the
"flips to a library build + publish when the interface stabilizes on
Vue 3" intent.
2026-08-24 06:56:36 -04:00

229 lines
9.9 KiB
JavaScript

import { readFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import ViteYaml from "@modyfi/vite-plugin-yaml";
import inject from "@rollup/plugin-inject";
import vue from "@vitejs/plugin-vue2";
import { defineConfig } from "vite";
import { buildMetadataPlugin } from "./vite-plugin-build-metadata.js";
import { galaxyDevServerPlugin } from "./vite-plugin-galaxy-dev-server.js";
import { galaxyLegacyPlugin } from "./vite-plugin-galaxy-legacy.js";
const __dirname = dirname(fileURLToPath(import.meta.url));
const packageJson = JSON.parse(readFileSync(resolve(__dirname, "package.json"), "utf-8"));
/**
* Plugin to fix D3 v3 ES module compatibility
* D3 v3 is an IIFE that expects `this` to be `window` at the module level.
* In ES modules, `this` is `undefined`. This plugin wraps the code to
* execute with the correct global context.
*/
function d3v3CompatPlugin() {
return {
name: "d3v3-compat",
transform(code, id) {
// Only transform the d3v3 main file
if (
(id.includes("node_modules/d3v3/d3.js") || id.includes("node_modules\\d3v3\\d3.js")) &&
code.startsWith("!function()")
) {
// The D3 v3 code is: !function() { ... }();
// We need to change it to: !function() { ... }.call(window);
// This ensures `this` inside the IIFE refers to `window`
const transformed = code.replace(
/\}(\s*)\(\s*\)\s*;?\s*$/,
"}.call(typeof window !== 'undefined' ? window : globalThis);",
);
return {
code: transformed,
map: null,
};
}
return null;
},
};
}
export default defineConfig(({ command }) => ({
// Use relative base so CSS asset references work with any proxy prefix.
// The HTML script tags use url_for() which handles the prefix correctly.
base: "./",
resolve: {
tsconfigPaths: true,
alias: {
// galaxy-ui is internal-only (no library build) and always
// resolved from source -- the main client consumes the .vue
// files directly through @vitejs/plugin-vue2.
"@galaxyproject/galaxy-ui": resolve(__dirname, "packages/ui/src/index.ts"),
// galaxy-api-client has a real tsup dist/ build and production
// uses that via package.json exports. During `vite serve` we
// resolve to source so edits trigger HMR without a rebuild.
...(command === "serve"
? { "@galaxyproject/galaxy-api-client": resolve(__dirname, "packages/api-client/src/index.ts") }
: {}),
},
},
define: {
// Make jQuery available globally for plugins and legacy code
global: "globalThis",
// Polyfill process.env for Node.js modules (like util) that reference it
"process.env": JSON.stringify({}),
// Define build system for runtime checks
__GALAXY_BUILD_SYSTEM__: JSON.stringify("vite"),
// Define build timestamp
__buildTimestamp__: JSON.stringify(new Date().toISOString()),
// Define license from package.json
__license__: JSON.stringify(packageJson.license),
},
plugins: [
vue({
template: {
compilerOptions: {
// Preserve whitespace to match Webpack's vue-loader default behavior
whitespace: "preserve",
},
},
}),
ViteYaml(), // YAML file support
galaxyLegacyPlugin(), // Handle legacy module resolution
buildMetadataPlugin(), // Generate build metadata (replaces DumpMetaPlugin)
d3v3CompatPlugin(), // Fix D3 v3 ES module compatibility
// Inject imports for Buffer
// jQuery is set up as window.$ and window.jQuery by libs.bundled.js
// Note: We don't inject jQuery here to avoid circular dependencies with code splitting
inject({
include: ["**/*.js", "**/*.ts", "**/*.vue"],
exclude: ["**/node_modules/**"],
Buffer: ["buffer", "Buffer"],
}),
galaxyDevServerPlugin(), // Transform proxied Galaxy HTML for HMR support
],
// Note: resolve.extensions are set by galaxyLegacyPlugin
css: {
lightningcss: {
errorRecovery: true,
},
preprocessorOptions: {
scss: {
quietDeps: true,
silenceDeprecations: ["import", "color-functions", "global-builtin"],
includePaths: ["src/style", "src/style/scss", "node_modules"],
},
},
},
build: {
outDir: "dist",
emptyOutDir: true,
// Generate manifest.json for production
manifest: true,
// Disable CSS code splitting - combine all CSS into one file
cssCodeSplit: false,
// Generate sourcemaps when GXY_BUILD_SOURCEMAPS is set
sourcemap: !!process.env.GXY_BUILD_SOURCEMAPS,
rolldownOptions: {
input: {
// Entry points that will be referenced in templates
// libs must be loaded first - it exposes globals (jQuery, bundleEntries, config)
libs: resolve(__dirname, "src/entry/libs.js"),
analysis: resolve(__dirname, "src/entry/analysis/index.ts"),
generic: resolve(__dirname, "src/entry/generic.js"),
},
output: {
entryFileNames: "[name].bundled.js",
chunkFileNames: "[name]-[hash].js",
// CSS should be named 'base.css' to match what templates expect
assetFileNames: (assetInfo) => {
if (assetInfo.name && assetInfo.name.endsWith(".css")) {
return "base.css";
}
return "[name]-[hash].[ext]";
},
manualChunks: (id) => {
// Put jQuery in its own chunk - jquery-migrate is imported dynamically
if (id.includes("node_modules/jquery/") && !id.includes("jquery-migrate")) {
return "jquery-core";
}
// Keep app/* files together to avoid circular dependency issues
// (getGalaxyInstance is re-exported through app/index.js)
if (id.includes("/src/app/")) {
return "galaxy-app";
}
},
},
},
},
server: {
port: process.env.VITE_PORT || 5173,
host: "0.0.0.0",
proxy: {
// Proxy everything except Vite's own routes to Galaxy backend
"^/(?!(@|src/|node_modules/|__vite))": {
target: process.env.GALAXY_URL || "http://127.0.0.1:8080",
changeOrigin: true,
secure: false,
cookieDomainRewrite: "",
configure: (proxy) => {
proxy.on("proxyRes", (proxyRes, req) => {
// Strip Secure flag and fix SameSite from upstream HTTPS cookies so
// they are accepted by the browser on http://localhost.
const cookies = proxyRes.headers["set-cookie"];
if (cookies) {
proxyRes.headers["set-cookie"] = cookies.map((cookie) =>
cookie.replace(/;\s*Secure/gi, "").replace(/;\s*SameSite=None/gi, "; SameSite=Lax"),
);
}
// Rewrite Location header to use the dev server origin instead of
// the Galaxy backend. With changeOrigin=true, Galaxy sees the backend
// Host and generates absolute URLs (e.g. in TUS upload responses).
// Without this rewrite, the browser tries to access the backend directly
// which causes CORS failures.
const location = proxyRes.headers["location"];
if (location) {
const targetUrl = new URL(process.env.GALAXY_URL || "http://127.0.0.1:8080");
const locationUrl = new URL(location, targetUrl);
const fallbackDevHost = req.headers.host || `localhost:${process.env.VITE_PORT || 5173}`;
const devOrigin = req.headers.origin || `http://${fallbackDevHost}`;
// Only rewrite locations generated for the Galaxy backend.
if (locationUrl.origin === targetUrl.origin) {
proxyRes.headers["location"] =
`${devOrigin}${locationUrl.pathname}${locationUrl.search}${locationUrl.hash}`;
}
}
});
},
},
},
cors: true,
hmr: {
protocol: "ws",
host: "localhost",
},
},
worker: {
format: "es",
},
optimizeDeps: {
// Use Rolldown plugin to fix D3 v3's IIFE `this` binding during pre-bundling
rolldownOptions: {
plugins: [
{
name: "d3v3-compat-rolldown",
load(id) {
if (id.includes("node_modules/d3v3/d3.js") || id.includes("node_modules\\d3v3\\d3.js")) {
let contents = readFileSync(id, "utf8");
contents = contents.replace(
/\}(\s*)\(\s*\)\s*;?\s*$/,
"}.call(typeof window !== 'undefined' ? window : globalThis);",
);
return contents;
}
},
},
],
},
},
}));