ui: add lightweight frontend build pipeline

This commit is contained in:
a.tolmachev
2026-04-12 02:28:05 +03:00
parent 2848d74929
commit b92350a6ac
22 changed files with 809 additions and 191 deletions
+1
View File
@@ -122,6 +122,7 @@ Crank - платформа для публикации внешних API в в
```bash
cd apps/ui
npm ci
npm run build
npm run e2e:install
npm run e2e
```
+5 -8
View File
@@ -2,20 +2,18 @@
## Current
### `feat/frontend-build-pipeline`
### `feat/frontend-css-state-cleanup`
Status: in_progress
DoD:
- UI assets are bundled through a lightweight pipeline suitable for the current stack
- local source files remain readable, but shipped JS is no longer served as dozens of unbundled scripts
- vendor dependencies move under package-managed installation where practical
- Docker UI image builds the frontend assets before packaging them into nginx
- existing UI e2e coverage stays green after the build change
- button and control state visuals move from inline `element.style` mutations into CSS classes
- wizard navigation and similar interactive UI use semantic state classes instead of manual style strings
- existing UI e2e coverage stays green after the CSS-state cleanup
## Next
- `feat/frontend-css-state-cleanup`
- `feat/frontend-performance-polish`
## Backlog
@@ -27,7 +25,6 @@ DoD:
- `feat/frontend-template-safety-cleanup`
- `feat/frontend-shell-unification`
- `feat/frontend-wizard-modularization`
- `feat/frontend-build-pipeline`
- `feat/frontend-css-state-cleanup`
- `feat/frontend-performance-polish`
- `feat/frontend-observability-and-testability`
+13 -7
View File
@@ -1,12 +1,18 @@
FROM node:22-alpine AS build
WORKDIR /app
COPY apps/ui/package.json apps/ui/package-lock.json ./
RUN npm ci
COPY apps/ui ./
COPY Crank.png ./Crank.png
RUN npm run build
FROM nginx:1.27-alpine
COPY apps/ui/nginx.conf /etc/nginx/conf.d/default.conf
COPY apps/ui/index.html /usr/share/nginx/html/index.html
COPY apps/ui/css /usr/share/nginx/html/css
COPY apps/ui/data /usr/share/nginx/html/data
COPY apps/ui/html /usr/share/nginx/html/html
COPY apps/ui/icons /usr/share/nginx/html/icons
COPY apps/ui/js /usr/share/nginx/html/js
COPY Crank.png /usr/share/nginx/html/Crank.png
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 3000
+2 -10
View File
@@ -11,14 +11,7 @@
<link rel="stylesheet" href="css/pages.css">
<link rel="stylesheet" href="css/catalog.css">
<link rel="stylesheet" href="css/wizard.css">
<script src="js/config.js"></script>
<script src="js/i18n.js"></script>
<script src="js/dom.js"></script>
<script src="js/workspace.js"></script>
<script src="js/api.js"></script>
<script src="js/ui-feedback.js"></script>
<script src="js/auth.js"></script>
<script>window.CrankAuth.guardProtectedPage();</script>
<script src="%CRANK_BUNDLE_PROTECTED_CORE%"></script>
</head>
<body x-data="agents">
@@ -393,7 +386,6 @@
</div><!-- /drawer -->
<script src="js/vendor/alpine/cdn.min.js" defer></script>
<script src="js/agents.js"></script>
<script src="%CRANK_BUNDLE_AGENTS%"></script>
</body>
</html>
+2 -10
View File
@@ -23,14 +23,7 @@
.scope-checkbox-name { font-size: 13px; font-weight: 500; color: var(--text-primary); }
.scope-checkbox-desc { font-size: 11.5px; color: var(--text-muted); }
</style>
<script src="js/config.js"></script>
<script src="js/i18n.js"></script>
<script src="js/dom.js"></script>
<script src="js/api.js"></script>
<script src="js/ui-feedback.js"></script>
<script src="js/auth.js"></script>
<script>window.CrankAuth.guardProtectedPage();</script>
<script src="js/workspace.js"></script>
<script src="%CRANK_BUNDLE_PROTECTED_CORE%"></script>
</head>
<body>
@@ -277,8 +270,7 @@
</tr>
</template>
<script src="js/nav.js"></script>
<script src="js/api-keys.js"></script>
<script src="%CRANK_BUNDLE_API_KEYS%"></script>
</body>
</html>
+2 -10
View File
@@ -9,14 +9,7 @@
<link rel="stylesheet" href="css/variables.css">
<link rel="stylesheet" href="css/layout.css">
<link rel="stylesheet" href="css/pages.css">
<script src="js/config.js"></script>
<script src="js/i18n.js"></script>
<script src="js/dom.js"></script>
<script src="js/api.js"></script>
<script src="js/ui-feedback.js"></script>
<script src="js/workspace.js"></script>
<script src="js/auth.js"></script>
<script>window.CrankAuth.guardProtectedPage();</script>
<script src="%CRANK_BUNDLE_PROTECTED_CORE%"></script>
</head>
<body>
<nav class="navbar">
@@ -108,7 +101,6 @@
</div>
</div>
<script src="js/nav.js"></script>
<script src="js/async-jobs.js"></script>
<script src="%CRANK_BUNDLE_ASYNC_JOBS%"></script>
</body>
</html>
+1 -5
View File
@@ -8,10 +8,7 @@
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="css/variables.css">
<link rel="stylesheet" href="css/login.css">
<script src="js/config.js"></script>
<script src="js/i18n.js"></script>
<script src="js/api.js"></script>
<script src="js/auth.js"></script>
<script src="%CRANK_BUNDLE_LOGIN%"></script>
</head>
<body>
@@ -52,7 +49,6 @@
</div>
</div>
<script src="js/login.js"></script>
</body>
</html>
+2 -10
View File
@@ -10,14 +10,7 @@
<link rel="stylesheet" href="css/layout.css">
<link rel="stylesheet" href="css/pages.css">
<link rel="stylesheet" href="css/logs.css">
<script src="js/config.js"></script>
<script src="js/i18n.js"></script>
<script src="js/dom.js"></script>
<script src="js/api.js"></script>
<script src="js/ui-feedback.js"></script>
<script src="js/workspace.js"></script>
<script src="js/auth.js"></script>
<script>window.CrankAuth.guardProtectedPage();</script>
<script src="%CRANK_BUNDLE_PROTECTED_CORE%"></script>
</head>
<body>
@@ -143,8 +136,7 @@
</div>
<script src="js/nav.js"></script>
<script src="js/logs.js"></script>
<script src="%CRANK_BUNDLE_LOGS%"></script>
</body>
</html>
+2 -10
View File
@@ -40,14 +40,7 @@
font-weight: 600;
}
</style>
<script src="js/config.js"></script>
<script src="js/i18n.js"></script>
<script src="js/dom.js"></script>
<script src="js/api.js"></script>
<script src="js/ui-feedback.js"></script>
<script src="js/auth.js"></script>
<script>window.CrankAuth.guardProtectedPage();</script>
<script src="js/workspace.js"></script>
<script src="%CRANK_BUNDLE_PROTECTED_CORE%"></script>
</head>
<body>
<nav class="navbar">
@@ -236,7 +229,6 @@
</div>
</div>
<script src="js/nav.js"></script>
<script src="js/secrets.js"></script>
<script src="%CRANK_BUNDLE_SECRETS%"></script>
</body>
</html>
+2 -10
View File
@@ -10,14 +10,7 @@
<link rel="stylesheet" href="css/layout.css">
<link rel="stylesheet" href="css/pages.css">
<link rel="stylesheet" href="css/settings.css">
<script src="js/config.js"></script>
<script src="js/i18n.js"></script>
<script src="js/dom.js"></script>
<script src="js/api.js"></script>
<script src="js/ui-feedback.js"></script>
<script src="js/workspace.js"></script>
<script src="js/auth.js"></script>
<script>window.CrankAuth.guardProtectedPage();</script>
<script src="%CRANK_BUNDLE_PROTECTED_CORE%"></script>
</head>
<body>
@@ -327,8 +320,7 @@
</div><!-- /page -->
<script src="js/nav.js"></script>
<script src="js/settings.js"></script>
<script src="%CRANK_BUNDLE_SETTINGS%"></script>
</body>
</html>
+2 -10
View File
@@ -9,14 +9,7 @@
<link rel="stylesheet" href="css/variables.css">
<link rel="stylesheet" href="css/layout.css">
<link rel="stylesheet" href="css/pages.css">
<script src="js/config.js"></script>
<script src="js/i18n.js"></script>
<script src="js/dom.js"></script>
<script src="js/api.js"></script>
<script src="js/ui-feedback.js"></script>
<script src="js/workspace.js"></script>
<script src="js/auth.js"></script>
<script>window.CrankAuth.guardProtectedPage();</script>
<script src="%CRANK_BUNDLE_PROTECTED_CORE%"></script>
</head>
<body>
<nav class="navbar">
@@ -113,7 +106,6 @@
</div>
</div>
<script src="js/nav.js"></script>
<script src="js/stream-sessions.js"></script>
<script src="%CRANK_BUNDLE_STREAM_SESSIONS%"></script>
</body>
</html>
+2 -10
View File
@@ -10,14 +10,7 @@
<link rel="stylesheet" href="css/layout.css">
<link rel="stylesheet" href="css/pages.css">
<link rel="stylesheet" href="css/usage.css">
<script src="js/config.js"></script>
<script src="js/i18n.js"></script>
<script src="js/dom.js"></script>
<script src="js/api.js"></script>
<script src="js/ui-feedback.js"></script>
<script src="js/workspace.js"></script>
<script src="js/auth.js"></script>
<script>window.CrankAuth.guardProtectedPage();</script>
<script src="%CRANK_BUNDLE_PROTECTED_CORE%"></script>
</head>
<body>
@@ -225,8 +218,7 @@
</tr>
</template>
<script src="js/nav.js"></script>
<script src="js/usage.js"></script>
<script src="%CRANK_BUNDLE_USAGE%"></script>
</body>
</html>
+2 -15
View File
@@ -9,17 +9,7 @@
<link rel="stylesheet" href="../../css/layout.css">
<link rel="stylesheet" href="../../css/wizard.css">
<link rel="stylesheet" href="../../css/pages.css">
<script src="../../js/config.js"></script>
<script src="../../js/i18n.js"></script>
<script src="../../js/dom.js"></script>
<script src="../../js/wizard-state.js"></script>
<script src="../../js/wizard-shell.js"></script>
<script src="../../js/workspace.js"></script>
<script src="../../js/api.js"></script>
<script src="../../js/ui-feedback.js"></script>
<script src="../../js/auth.js"></script>
<script>window.CrankAuth.guardProtectedPage();</script>
<script src="../../js/vendor/js-yaml/js-yaml.min.js"></script>
<script src="%CRANK_BUNDLE_PROTECTED_CORE%"></script>
</head>
<body>
@@ -236,8 +226,6 @@
</div><!-- /wizard-shell -->
<script src="../../js/streaming-form.js"></script>
<script src="../../js/stream-test-run.js"></script>
<div class="modal-overlay" id="quick-secret-modal">
<div class="modal">
<div class="modal-header">
@@ -273,8 +261,7 @@
</div>
</div>
</div>
<script src="../../js/wizard.js"></script>
<script src="../../js/nav.js"></script>
<script src="%CRANK_BUNDLE_WIZARD%"></script>
</body>
</html>
+2 -9
View File
@@ -11,13 +11,7 @@
<link rel="stylesheet" href="css/pages.css">
<link rel="stylesheet" href="css/wizard.css">
<link rel="stylesheet" href="css/workspace-setup.css">
<script src="js/config.js"></script>
<script src="js/i18n.js"></script>
<script src="js/dom.js"></script>
<script src="js/api.js"></script>
<script src="js/ui-feedback.js"></script>
<script src="js/auth.js"></script>
<script>window.CrankAuth.guardProtectedPage();</script>
<script src="%CRANK_BUNDLE_PROTECTED_CORE%"></script>
</head>
<body>
<div class="ws-setup-page">
@@ -327,7 +321,6 @@
</div>
<script src="js/workspace.js"></script>
<script src="js/workspace-setup.js"></script>
<script src="%CRANK_BUNDLE_WORKSPACE_SETUP%"></script>
</body>
</html>
+2 -10
View File
@@ -9,14 +9,7 @@
<link rel="stylesheet" href="css/layout.css">
<link rel="stylesheet" href="css/catalog.css">
<link rel="stylesheet" href="css/pages.css">
<script src="js/config.js"></script>
<script src="js/i18n.js"></script>
<script src="js/dom.js"></script>
<script src="js/workspace.js"></script>
<script src="js/api.js"></script>
<script src="js/ui-feedback.js"></script>
<script src="js/auth.js"></script>
<script>window.CrankAuth.guardProtectedPage();</script>
<script src="%CRANK_BUNDLE_PROTECTED_CORE%"></script>
</head>
<body x-data="catalog()">
@@ -419,8 +412,7 @@
</div><!-- /page -->
<script src="js/catalog.js" defer></script>
<script src="js/vendor/alpine/cdn.min.js" defer></script>
<script src="%CRANK_BUNDLE_OPERATIONS%"></script>
</body>
</html>
+27 -45
View File
@@ -3,7 +3,7 @@
return typeof t === 'function' ? t(key) : key;
}
var state = {
var defaults = {
currentStep: 1,
wizardProtocol: 'rest',
wizardMode: 'create',
@@ -27,6 +27,22 @@
selectedRpcMethod: null,
};
Object.keys(defaults).forEach(function(key) {
if (typeof window[key] !== 'undefined') {
return;
}
var value = defaults[key];
if (Array.isArray(value)) {
window[key] = value.slice();
return;
}
if (value && typeof value === 'object') {
window[key] = Object.assign({}, value);
return;
}
window[key] = value;
});
function defaultProtocolCapabilities() {
return {
rest: {
@@ -53,28 +69,28 @@
}
function currentProtocolCapabilities() {
var capabilities = state.wizardProtocolCapabilities || defaultProtocolCapabilities();
return capabilities[state.wizardProtocol] || capabilities.rest;
var capabilities = window.wizardProtocolCapabilities || defaultProtocolCapabilities();
return capabilities[window.wizardProtocol] || capabilities.rest;
}
async function loadProtocolCapabilities() {
if (!state.wizardWorkspaceId || !window.CrankApi || typeof window.CrankApi.getProtocolCapabilities !== 'function') {
state.wizardProtocolCapabilities = defaultProtocolCapabilities();
return state.wizardProtocolCapabilities;
if (!window.wizardWorkspaceId || !window.CrankApi || typeof window.CrankApi.getProtocolCapabilities !== 'function') {
window.wizardProtocolCapabilities = defaultProtocolCapabilities();
return window.wizardProtocolCapabilities;
}
try {
var response = await window.CrankApi.getProtocolCapabilities(state.wizardWorkspaceId);
var response = await window.CrankApi.getProtocolCapabilities(window.wizardWorkspaceId);
var mapped = {};
(response.items || []).forEach(function(item) {
mapped[item.protocol] = item;
});
state.wizardProtocolCapabilities = mapped;
window.wizardProtocolCapabilities = mapped;
} catch (_error) {
state.wizardProtocolCapabilities = defaultProtocolCapabilities();
window.wizardProtocolCapabilities = defaultProtocolCapabilities();
}
return state.wizardProtocolCapabilities;
return window.wizardProtocolCapabilities;
}
function step3Labels() {
@@ -88,44 +104,10 @@
}
window.CrankWizardState = {
state: state,
state: window,
defaultProtocolCapabilities: defaultProtocolCapabilities,
currentProtocolCapabilities: currentProtocolCapabilities,
loadProtocolCapabilities: loadProtocolCapabilities,
step3Labels: step3Labels,
};
[
'currentStep',
'wizardProtocol',
'wizardMode',
'wizardEditId',
'wizardWorkspaceId',
'wizardCurrentOperation',
'wizardCurrentVersion',
'wizardProtoUpload',
'wizardDescriptorSetUpload',
'wizardSoapWsdlUpload',
'wizardSoapXsdUpload',
'wizardTestResponsePreview',
'grpcDescriptorServices',
'soapServiceCatalog',
'wizardProtocolCapabilities',
'wizardSecrets',
'wizardAuthProfiles',
'selectedUpstreamId',
'editingUpstreamId',
'protoParsed',
'selectedRpcMethod'
].forEach(function(key) {
Object.defineProperty(window, key, {
configurable: true,
get: function() {
return state[key];
},
set: function(value) {
state[key] = value;
},
});
});
}());
+21 -7
View File
@@ -13,6 +13,27 @@ var _doGoToStep = window.CrankWizardShell.doGoToStep;
var loadWizardPanels = window.CrankWizardShell.loadWizardPanels;
var bindProtocolCards = window.CrankWizardShell.bindProtocolCards;
var step3PanelId = window.CrankWizardShell.step3PanelId;
var currentStep = window.currentStep;
var wizardProtocol = window.wizardProtocol;
var wizardMode = window.wizardMode;
var wizardEditId = window.wizardEditId;
var wizardWorkspaceId = window.wizardWorkspaceId;
var wizardCurrentOperation = window.wizardCurrentOperation;
var wizardCurrentVersion = window.wizardCurrentVersion;
var wizardProtoUpload = window.wizardProtoUpload;
var wizardDescriptorSetUpload = window.wizardDescriptorSetUpload;
var wizardSoapWsdlUpload = window.wizardSoapWsdlUpload;
var wizardSoapXsdUpload = window.wizardSoapXsdUpload;
var wizardTestResponsePreview = window.wizardTestResponsePreview;
var grpcDescriptorServices = window.grpcDescriptorServices;
var soapServiceCatalog = window.soapServiceCatalog;
var wizardProtocolCapabilities = window.wizardProtocolCapabilities;
var wizardSecrets = window.wizardSecrets;
var wizardAuthProfiles = window.wizardAuthProfiles;
var selectedUpstreamId = window.selectedUpstreamId;
var editingUpstreamId = window.editingUpstreamId;
var protoParsed = window.protoParsed;
var selectedRpcMethod = window.selectedRpcMethod;
document.addEventListener('DOMContentLoaded', async function() {
document.querySelector('.btn-continue').addEventListener('click', function() {
@@ -163,11 +184,7 @@ var upstreams = [
{ id: 'grpcb', name: 'grpcb.in', url: 'https://grpcb.in:443', staticHeaders: '{}', authProfileId: null, authProfileName: '', authKind: null }
];
var wizardSecrets = [];
var wizardAuthProfiles = [];
var selectedUpstreamId = null;
var dropdownOpen = false;
var editingUpstreamId = null;
function authProfileById(authProfileId) {
return wizardAuthProfiles.find(function(item) { return item.id === authProfileId; }) || null;
@@ -738,9 +755,6 @@ function selectGrpcSource(source) {
gRPC — Proto file parser & UI
══════════════════════════════════════════════ */
var protoParsed = null; // { package, services, messages, streamingCount }
var selectedRpcMethod = null; // { service, method, request, response }
/* Extract top-level blocks with proper brace matching */
function extractBlocks(text, keyword) {
var blocks = [];
+486 -1
View File
@@ -10,7 +10,450 @@
"js-yaml": "4.1.1"
},
"devDependencies": {
"@playwright/test": "^1.59.1"
"@playwright/test": "^1.59.1",
"esbuild": "^0.28.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz",
"integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz",
"integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz",
"integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz",
"integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz",
"integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz",
"integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz",
"integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz",
"integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz",
"integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz",
"integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz",
"integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz",
"integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz",
"integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz",
"integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz",
"integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz",
"integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz",
"integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz",
"integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz",
"integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz",
"integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz",
"integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz",
"integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz",
"integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz",
"integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz",
"integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz",
"integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@playwright/test": {
@@ -59,6 +502,48 @@
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"license": "Python-2.0"
},
"node_modules/esbuild": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz",
"integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.28.0",
"@esbuild/android-arm": "0.28.0",
"@esbuild/android-arm64": "0.28.0",
"@esbuild/android-x64": "0.28.0",
"@esbuild/darwin-arm64": "0.28.0",
"@esbuild/darwin-x64": "0.28.0",
"@esbuild/freebsd-arm64": "0.28.0",
"@esbuild/freebsd-x64": "0.28.0",
"@esbuild/linux-arm": "0.28.0",
"@esbuild/linux-arm64": "0.28.0",
"@esbuild/linux-ia32": "0.28.0",
"@esbuild/linux-loong64": "0.28.0",
"@esbuild/linux-mips64el": "0.28.0",
"@esbuild/linux-ppc64": "0.28.0",
"@esbuild/linux-riscv64": "0.28.0",
"@esbuild/linux-s390x": "0.28.0",
"@esbuild/linux-x64": "0.28.0",
"@esbuild/netbsd-arm64": "0.28.0",
"@esbuild/netbsd-x64": "0.28.0",
"@esbuild/openbsd-arm64": "0.28.0",
"@esbuild/openbsd-x64": "0.28.0",
"@esbuild/openharmony-arm64": "0.28.0",
"@esbuild/sunos-x64": "0.28.0",
"@esbuild/win32-arm64": "0.28.0",
"@esbuild/win32-ia32": "0.28.0",
"@esbuild/win32-x64": "0.28.0"
}
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+3 -2
View File
@@ -2,7 +2,7 @@
"name": "crank-ui",
"private": true,
"scripts": {
"sync:vendor": "mkdir -p js/vendor/alpine js/vendor/js-yaml && cp node_modules/alpinejs/dist/cdn.min.js js/vendor/alpine/cdn.min.js && cp node_modules/js-yaml/dist/js-yaml.min.js js/vendor/js-yaml/js-yaml.min.js",
"build": "node scripts/build.js",
"e2e:install": "playwright install chromium",
"e2e": "playwright test",
"e2e:headed": "playwright test --headed"
@@ -12,6 +12,7 @@
"js-yaml": "4.1.1"
},
"devDependencies": {
"@playwright/test": "^1.59.1"
"@playwright/test": "^1.59.1",
"esbuild": "^0.28.0"
}
}
+223
View File
@@ -0,0 +1,223 @@
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.png'),
path.resolve(ROOT_DIR, '..', '..', 'Crank.png'),
];
const BUNDLES = {
'protected-core': {
files: [
'js/config.js',
'js/i18n.js',
'js/dom.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/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',
],
},
'stream-sessions': {
files: [
'js/nav.js',
'js/stream-sessions.js',
],
},
'async-jobs': {
files: [
'js/nav.js',
'js/async-jobs.js',
],
},
'workspace-setup': {
files: [
'js/workspace-setup.js',
],
},
wizard: {
files: [
'node_modules/js-yaml/dist/js-yaml.min.js',
'js/wizard-state.js',
'js/wizard-shell.js',
'js/streaming-form.js',
'js/stream-test-run.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.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() {
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'));
copyDirectory(path.join(ROOT_DIR, 'html', 'wizard'), path.join(DIST_DIR, 'html', 'wizard'));
copyDirectory(path.join(ROOT_DIR, 'icons'), path.join(DIST_DIR, 'icons'));
copyFile(resolveBrandImage(), path.join(DIST_DIR, 'Crank.png'));
rewriteHtml('index.html', manifest, '');
rewriteHtml('html/login.html', manifest, '..');
rewriteHtml('html/agents.html', manifest, '..');
rewriteHtml('html/api-keys.html', manifest, '..');
rewriteHtml('html/async-jobs.html', manifest, '..');
rewriteHtml('html/logs.html', manifest, '..');
rewriteHtml('html/secrets.html', manifest, '..');
rewriteHtml('html/settings.html', manifest, '..');
rewriteHtml('html/stream-sessions.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;
});
+5
View File
@@ -19,6 +19,11 @@ ADMIN_PASSWORD="${CRANK_E2E_ADMIN_PASSWORD:-change-me-admin-password}"
mkdir -p "$LOG_DIR"
(
cd "$ROOT_DIR/apps/ui"
npm run build >"$LOG_DIR/ui-build.log" 2>&1
)
kill_port_processes() {
local port="$1"
if command -v lsof >/dev/null 2>&1; then
+2 -2
View File
@@ -4,7 +4,7 @@ const http = require('http');
const { pipeline } = require('stream');
const { URL } = require('url');
const ROOT_DIR = path.resolve(__dirname, '..');
const ROOT_DIR = path.resolve(__dirname, '..', 'dist');
const UI_PORT = Number(process.env.CRANK_E2E_UI_PORT || 3300);
const ADMIN_PORT = Number(process.env.CRANK_E2E_ADMIN_PORT || 3301);
const ADMIN_BASE = new URL(`http://127.0.0.1:${ADMIN_PORT}`);
@@ -84,7 +84,7 @@ function mapRoute(urlPath) {
return path.join(ROOT_DIR, urlPath.slice(1));
}
if (urlPath === '/Crank.png') {
return path.resolve(ROOT_DIR, '..', '..', 'Crank.png');
return path.join(ROOT_DIR, 'Crank.png');
}
return null;
}