mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
323 lines
10 KiB
JavaScript
323 lines
10 KiB
JavaScript
/**
|
|
* Shared esbuild configuration factory for OneUptime frontend services
|
|
* This creates consistent build configurations across all services
|
|
*/
|
|
|
|
const esbuild = require('esbuild');
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
const dotenv = require('dotenv');
|
|
|
|
function createRefractorCompatibilityPlugin() {
|
|
const candidateRoots = [
|
|
path.resolve(__dirname, '../node_modules/refractor'),
|
|
path.resolve(__dirname, '../../node_modules/refractor'),
|
|
];
|
|
|
|
const refractorRoot = candidateRoots.find((packagePath) => fs.existsSync(packagePath));
|
|
|
|
if (!refractorRoot) {
|
|
throw new Error('Unable to locate refractor package for esbuild compatibility plugin.');
|
|
}
|
|
|
|
return {
|
|
name: 'refractor-compatibility',
|
|
setup(build) {
|
|
build.onResolve({ filter: /^refractor\/lib\// }, (args) => {
|
|
const relativePath = args.path.replace(/^refractor\/lib\//, '');
|
|
const candidatePath = path.join(refractorRoot, 'lib', `${relativePath}.js`);
|
|
return { path: candidatePath };
|
|
});
|
|
|
|
build.onResolve({ filter: /^refractor\/lang\// }, (args) => {
|
|
const relativePath = args.path.replace(/^refractor\/lang\//, '');
|
|
const filename = relativePath.endsWith('.js') ? relativePath : `${relativePath}.js`;
|
|
const candidatePath = path.join(refractorRoot, 'lang', filename);
|
|
return { path: candidatePath };
|
|
});
|
|
},
|
|
};
|
|
}
|
|
|
|
// Plugin to force mermaid to use its pre-bundled CJS build (no dynamic imports)
|
|
function createMermaidPlugin() {
|
|
const candidateRoots = [
|
|
path.resolve(__dirname, '../node_modules/mermaid'),
|
|
path.resolve(__dirname, '../../node_modules/mermaid'),
|
|
];
|
|
const mermaidRoot = candidateRoots.find((p) => fs.existsSync(p));
|
|
|
|
return {
|
|
name: 'mermaid-prebundled',
|
|
setup(build) {
|
|
if (!mermaidRoot) return;
|
|
const bundlePath = path.join(mermaidRoot, 'dist', 'mermaid.min.js');
|
|
|
|
// Intercept bare "mermaid" imports and serve the pre-bundled CJS file
|
|
// with an ESM export appended. The CJS file declares a local var
|
|
// __esbuild_esm_mermaid_nm and assigns .mermaid on it, so we inline
|
|
// the file contents and export from the same scope.
|
|
build.onResolve({ filter: /^mermaid$/ }, () => {
|
|
return { path: 'mermaid-wrapper', namespace: 'mermaid-ns' };
|
|
});
|
|
|
|
build.onLoad({ filter: /^mermaid-wrapper$/, namespace: 'mermaid-ns' }, () => {
|
|
let cjsSource = fs.readFileSync(bundlePath, 'utf8');
|
|
// The CJS bundle ends with a line that tries globalThis.__esbuild_esm_mermaid_nm
|
|
// which fails because the var is local-scoped when bundled. Strip it and
|
|
// expose the local var on globalThis ourselves before that line.
|
|
cjsSource = cjsSource.replace(
|
|
/globalThis\["mermaid"\]\s*=\s*globalThis\.__esbuild_esm_mermaid_nm\["mermaid"\]\.default;?\s*$/,
|
|
''
|
|
);
|
|
const contents = cjsSource + `
|
|
;globalThis.__esbuild_esm_mermaid_nm = typeof __esbuild_esm_mermaid_nm !== "undefined" ? __esbuild_esm_mermaid_nm : {};
|
|
var _mermaid_export = __esbuild_esm_mermaid_nm.mermaid;
|
|
if (_mermaid_export && _mermaid_export.default) { _mermaid_export = _mermaid_export.default; }
|
|
export default _mermaid_export;
|
|
export { _mermaid_export as mermaid };
|
|
`;
|
|
return {
|
|
contents,
|
|
loader: 'js',
|
|
resolveDir: path.dirname(bundlePath),
|
|
};
|
|
});
|
|
},
|
|
};
|
|
}
|
|
|
|
// CSS Plugin to handle CSS/SCSS files
|
|
function createCSSPlugin() {
|
|
return {
|
|
name: 'css',
|
|
setup(build) {
|
|
build.onLoad({ filter: /\.s?css$/ }, async (args) => {
|
|
const sass = require('sass');
|
|
const fs = require('fs');
|
|
|
|
let contents = fs.readFileSync(args.path, 'utf8');
|
|
|
|
// Compile SCSS to CSS if it's a SCSS file
|
|
if (args.path.endsWith('.scss') || args.path.endsWith('.sass')) {
|
|
try {
|
|
const result = sass.compile(args.path);
|
|
contents = result.css;
|
|
} catch (error) {
|
|
console.error(`SCSS compilation error in ${args.path}:`, error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Return CSS as a string that will be injected into the page
|
|
return {
|
|
contents: `
|
|
const style = document.createElement('style');
|
|
style.textContent = ${JSON.stringify(contents)};
|
|
document.head.appendChild(style);
|
|
`,
|
|
loader: 'js',
|
|
};
|
|
});
|
|
},
|
|
};
|
|
}
|
|
|
|
// File loader plugin for assets
|
|
function createFileLoaderPlugin() {
|
|
return {
|
|
name: 'file-loader',
|
|
setup(build) {
|
|
build.onLoad({ filter: /\.(png|jpe?g|gif|svg|woff|woff2|eot|ttf|otf)$/ }, async (args) => {
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const contents = fs.readFileSync(args.path);
|
|
const filename = path.basename(args.path);
|
|
const ext = path.extname(filename);
|
|
|
|
// For development, we'll use data URLs for simplicity
|
|
// In production, you might want to copy files to the output directory
|
|
const mimeTypes = {
|
|
'.png': 'image/png',
|
|
'.jpg': 'image/jpeg',
|
|
'.jpeg': 'image/jpeg',
|
|
'.gif': 'image/gif',
|
|
'.svg': 'image/svg+xml',
|
|
'.woff': 'font/woff',
|
|
'.woff2': 'font/woff2',
|
|
'.eot': 'application/vnd.ms-fontobject',
|
|
'.ttf': 'font/ttf',
|
|
'.otf': 'font/otf',
|
|
};
|
|
|
|
const mimeType = mimeTypes[ext.toLowerCase()] || 'application/octet-stream';
|
|
const dataUrl = `data:${mimeType};base64,${contents.toString('base64')}`;
|
|
|
|
return {
|
|
contents: `export default ${JSON.stringify(dataUrl)};`,
|
|
loader: 'js',
|
|
};
|
|
});
|
|
},
|
|
};
|
|
}
|
|
|
|
// Read environment variables from .env file
|
|
function readEnvFile(pathToFile) {
|
|
if (!fs.existsSync(pathToFile)) {
|
|
console.warn(`Environment file not found: ${pathToFile}`);
|
|
return {};
|
|
}
|
|
|
|
const parsed = dotenv.config({ path: pathToFile }).parsed || {};
|
|
const env = {};
|
|
|
|
for (const key in parsed) {
|
|
env[`process.env.${key}`] = JSON.stringify(parsed[key]);
|
|
}
|
|
|
|
return env;
|
|
}
|
|
|
|
function resolvePackageRoot(packageName) {
|
|
const resolutionPaths = [
|
|
process.cwd(),
|
|
__dirname,
|
|
path.resolve(__dirname, '..'),
|
|
path.resolve(__dirname, '../..'),
|
|
];
|
|
|
|
for (const resolutionPath of resolutionPaths) {
|
|
try {
|
|
const packageJsonPath = require.resolve(`${packageName}/package.json`, {
|
|
paths: [resolutionPath],
|
|
});
|
|
|
|
return path.dirname(packageJsonPath);
|
|
} catch (error) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
throw new Error(
|
|
`Unable to locate ${packageName} package for esbuild alias resolution.`,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Create esbuild configuration for a service
|
|
* @param {Object} options - Configuration options
|
|
* @param {string} options.serviceName - Name of the service (dashboard, accounts, admin, status-page)
|
|
* @param {string} options.publicPath - Public path for assets
|
|
* @param {string} [options.entryPoint] - Entry point file (defaults to './src/Index.tsx')
|
|
* @param {string} [options.outdir] - Output directory (defaults to './public/dist')
|
|
* @param {Object} [options.additionalDefines] - Additional define variables
|
|
* @param {Array} [options.additionalExternal] - Additional external modules
|
|
* @param {Object} [options.additionalAlias] - Additional aliases
|
|
*/
|
|
function createConfig(options) {
|
|
const {
|
|
serviceName,
|
|
publicPath,
|
|
entryPoint = './src/Index.tsx',
|
|
outdir = './public/dist',
|
|
additionalDefines = {},
|
|
additionalExternal = [],
|
|
additionalAlias = {}
|
|
} = options;
|
|
|
|
const isDev = process.env.NODE_ENV !== 'production';
|
|
const isAnalyze = process.env.analyze === 'true';
|
|
const reactRoot = resolvePackageRoot('react');
|
|
|
|
return {
|
|
entryPoints: [entryPoint],
|
|
bundle: true,
|
|
outdir,
|
|
format: 'esm',
|
|
platform: 'browser',
|
|
target: 'es2017',
|
|
sourcemap: isDev ? 'inline' : false,
|
|
minify: false,
|
|
treeShaking: true,
|
|
splitting: true,
|
|
publicPath,
|
|
define: {
|
|
'process.env.NODE_ENV': JSON.stringify(isDev ? 'development' : 'production'),
|
|
...additionalDefines,
|
|
},
|
|
external: ['react-native-sqlite-storage', ...additionalExternal],
|
|
alias: {
|
|
'react': reactRoot,
|
|
'react/jsx-runtime': path.join(reactRoot, 'jsx-runtime.js'),
|
|
'react/jsx-dev-runtime': path.join(reactRoot, 'jsx-dev-runtime.js'),
|
|
...additionalAlias,
|
|
},
|
|
plugins: [createMermaidPlugin(), createRefractorCompatibilityPlugin(), createCSSPlugin(), createFileLoaderPlugin()],
|
|
loader: {
|
|
'.tsx': 'tsx',
|
|
'.ts': 'ts',
|
|
'.jsx': 'jsx',
|
|
'.js': 'js',
|
|
'.json': 'json',
|
|
},
|
|
resolveExtensions: ['.tsx', '.ts', '.jsx', '.js', '.json', '.css', '.scss'],
|
|
metafile: isAnalyze,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Build function that handles the build process
|
|
* @param {Object} config - esbuild configuration
|
|
* @param {string} serviceName - Name of the service for logging
|
|
*/
|
|
async function build(config, serviceName) {
|
|
const isAnalyze = process.env.analyze === 'true';
|
|
|
|
try {
|
|
const result = await esbuild.build(config);
|
|
|
|
if (isAnalyze && result.metafile) {
|
|
const analyzeText = await esbuild.analyzeMetafile(result.metafile);
|
|
console.log(`\n📊 Bundle analysis for ${serviceName}:`);
|
|
console.log(analyzeText);
|
|
|
|
// Write metafile for external analysis tools
|
|
const metafilePath = path.join(config.outdir, 'metafile.json');
|
|
fs.writeFileSync(metafilePath, JSON.stringify(result.metafile, null, 2));
|
|
console.log(`📝 Metafile written to: ${metafilePath}`);
|
|
}
|
|
|
|
console.log(`✅ ${serviceName} build completed successfully`);
|
|
} catch (error) {
|
|
console.error(`❌ ${serviceName} build failed:`, error);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Watch function that handles the watch process
|
|
* @param {Object} config - esbuild configuration
|
|
* @param {string} serviceName - Name of the service for logging
|
|
*/
|
|
async function watch(config, serviceName) {
|
|
try {
|
|
const context = await esbuild.context(config);
|
|
await context.watch();
|
|
console.log(`👀 Watching ${serviceName} for changes...`);
|
|
} catch (error) {
|
|
console.error(`❌ ${serviceName} watch failed:`, error);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
createConfig,
|
|
build,
|
|
watch,
|
|
createCSSPlugin,
|
|
createFileLoaderPlugin,
|
|
readEnvFile,
|
|
};
|