const { transform } = require('esbuild'); const crypto = require('crypto'); const fs = require('fs'); const path = require('path'); const ROOT_DIR = path.resolve(__dirname, '..'); const DIST_DIR = path.join(ROOT_DIR, 'dist'); const BUNDLE_DIR = path.join(DIST_DIR, 'js', 'bundles'); const BRAND_IMAGE_PATHS = [ path.join(ROOT_DIR, 'crank-community.png'), path.resolve(ROOT_DIR, '..', '..', 'crank-community.png'), ]; const BUNDLES = { 'protected-core': { files: [ 'js/config.js', 'js/i18n.js', 'js/slot-registry.js', 'js/overlay-loader.js', 'js/dom.js', 'js/diagnostics.js', 'js/workspace.js', 'js/api.js', 'js/ui-feedback.js', 'js/auth.js', ], footer: 'window.CrankAuth.guardProtectedPage();\n', }, login: { files: [ 'js/config.js', 'js/i18n.js', 'js/diagnostics.js', 'js/api.js', 'js/auth.js', 'js/login.js', ], }, operations: { files: [ 'node_modules/alpinejs/dist/cdn.min.js', 'js/catalog.js', ], }, agents: { files: [ 'node_modules/alpinejs/dist/cdn.min.js', 'js/agents.js', ], }, 'api-keys': { files: [ 'js/nav.js', 'js/api-keys.js', ], }, logs: { files: [ 'js/nav.js', 'js/logs.js', ], }, usage: { files: [ 'js/nav.js', 'js/usage.js', ], }, secrets: { files: [ 'js/nav.js', 'js/secrets.js', ], }, settings: { files: [ 'js/nav.js', 'js/settings.js', ], }, 'workspace-setup': { files: [ 'js/diagnostics.js', 'js/workspace-setup.js', ], }, wizard: { files: [ 'node_modules/js-yaml/dist/js-yaml.min.js', 'js/wizard-state.js', 'js/wizard-shell.js', 'js/wizard-upstreams.js', 'js/wizard-model.js', 'js/wizard-live.js', 'js/wizard.js', 'js/nav.js', ], }, }; function sha(input) { return crypto.createHash('sha256').update(input).digest('hex').slice(0, 12); } function sourcePath(relativePath) { return path.join(ROOT_DIR, relativePath); } function ensureDirectory(directoryPath) { fs.mkdirSync(directoryPath, { recursive: true }); } function removeDirectory(directoryPath) { fs.rmSync(directoryPath, { recursive: true, force: true }); } function copyFile(source, destination) { ensureDirectory(path.dirname(destination)); fs.copyFileSync(source, destination); } function copyDirectory(source, destination) { ensureDirectory(destination); for (const entry of fs.readdirSync(source, { withFileTypes: true })) { const sourceEntry = path.join(source, entry.name); const destinationEntry = path.join(destination, entry.name); if (entry.isDirectory()) { copyDirectory(sourceEntry, destinationEntry); continue; } copyFile(sourceEntry, destinationEntry); } } function readSource(relativePath) { return fs.readFileSync(sourcePath(relativePath), 'utf8'); } function resolveBrandImage() { const match = BRAND_IMAGE_PATHS.find(function(filePath) { return fs.existsSync(filePath); }); if (!match) { throw new Error('unable to resolve crank-community.png for UI build'); } return match; } async function buildBundle(name, config) { const source = config.files.map(readSource).join('\n\n') + '\n' + (config.footer || ''); const result = await transform(source, { loader: 'js', minify: true, target: 'es2019', legalComments: 'none', }); const hash = sha(result.code); const fileName = `${name}.${hash}.js`; const relativeOutputPath = path.posix.join('js', 'bundles', fileName); fs.writeFileSync(path.join(BUNDLE_DIR, fileName), result.code); return relativeOutputPath; } function rewriteHtml(relativePath, replacements, prefix) { const source = readSource(relativePath); const output = source.replace(/%CRANK_BUNDLE_([A-Z0-9_]+)%/g, function(_match, token) { const key = token.toLowerCase().replace(/_/g, '-'); const filePath = replacements[key]; if (!filePath) { throw new Error(`missing bundle placeholder ${key}`); } return prefix ? path.posix.join(prefix, filePath) : filePath; }); const destination = path.join(DIST_DIR, relativePath); ensureDirectory(path.dirname(destination)); fs.writeFileSync(destination, output); } async function main() { var overlayDirectory = process.env.CRANK_UI_OVERLAY_DIR || ''; removeDirectory(DIST_DIR); ensureDirectory(BUNDLE_DIR); const manifest = {}; for (const [name, config] of Object.entries(BUNDLES)) { manifest[name] = await buildBundle(name, config); } copyDirectory(path.join(ROOT_DIR, 'css'), path.join(DIST_DIR, 'css')); copyDirectory(path.join(ROOT_DIR, 'data'), path.join(DIST_DIR, 'data')); ensureDirectory(path.join(DIST_DIR, 'html', 'wizard')); [ 'html/wizard/index.html', 'html/wizard/step1.html', 'html/wizard/step2.html', 'html/wizard/step3-rest.html', 'html/wizard/step4.html', 'html/wizard/step5.html', ].forEach(function(relativePath) { copyFile(path.join(ROOT_DIR, relativePath), path.join(DIST_DIR, relativePath)); }); copyDirectory(path.join(ROOT_DIR, 'icons'), path.join(DIST_DIR, 'icons')); copyFile(resolveBrandImage(), path.join(DIST_DIR, 'Crank.png')); if (overlayDirectory && fs.existsSync(overlayDirectory)) { copyDirectory(overlayDirectory, path.join(DIST_DIR, 'overlay')); } rewriteHtml('index.html', manifest, ''); rewriteHtml('html/login.html', manifest, '..'); rewriteHtml('html/agents.html', manifest, '..'); rewriteHtml('html/api-keys.html', manifest, '..'); rewriteHtml('html/logs.html', manifest, '..'); rewriteHtml('html/secrets.html', manifest, '..'); rewriteHtml('html/settings.html', manifest, '..'); rewriteHtml('html/usage.html', manifest, '..'); rewriteHtml('html/workspace-setup.html', manifest, '..'); rewriteHtml('html/wizard/index.html', manifest, '../..'); fs.writeFileSync( path.join(DIST_DIR, 'build-manifest.json'), JSON.stringify(manifest, null, 2) + '\n', ); } main().catch(function(error) { console.error(error); process.exitCode = 1; });