diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml index b6bb180..d270166 100644 --- a/.github/workflows/ui-tests.yml +++ b/.github/workflows/ui-tests.yml @@ -19,13 +19,33 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + - name: Setup Node uses: actions/setup-node@v4 with: node-version: "20" + cache: "pnpm" + cache-dependency-path: ui/pnpm-lock.yaml + + - name: Install UI dependencies + run: pnpm install --frozen-lockfile + working-directory: ui + + - name: Build UI + run: pnpm build + working-directory: ui + + - name: Typecheck UI + run: pnpm typecheck + working-directory: ui - name: Run UI tests - run: node --test ui/entrypoint.test.mjs ui/nginx-config.test.mjs ui/public/*.test.mjs + run: pnpm test + working-directory: ui - name: Check entrypoint shell syntax run: bash -n ui/entrypoint.sh diff --git a/ui/Dockerfile b/ui/Dockerfile index 50098ea..9026010 100644 --- a/ui/Dockerfile +++ b/ui/Dockerfile @@ -1,11 +1,23 @@ +FROM node:22-alpine AS build + +WORKDIR /workspace/ui + +RUN corepack enable + +COPY ui/package.json ui/pnpm-lock.yaml ./ +RUN pnpm install --frozen-lockfile + +COPY ui/ ./ +RUN pnpm build + FROM nginxinc/nginx-unprivileged:1.27-alpine USER root RUN apk add --no-cache sed -COPY public/ /usr/share/nginx/html/ -COPY nginx.conf /etc/nginx/conf.d/default.conf -COPY entrypoint.sh /entrypoint.sh +COPY --from=build /workspace/ui/dist/ /usr/share/nginx/html/ +COPY ui/nginx.conf /etc/nginx/conf.d/default.conf +COPY ui/entrypoint.sh /entrypoint.sh RUN chown -R 101:101 /usr/share/nginx/html /entrypoint.sh /etc/nginx/conf.d/default.conf ENV SPRITZ_API_BASE_URL=/api diff --git a/ui/entrypoint.test.mjs b/ui/entrypoint.test.mjs index 68f2eda..9e77833 100644 --- a/ui/entrypoint.test.mjs +++ b/ui/entrypoint.test.mjs @@ -4,13 +4,13 @@ import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import vm from 'node:vm'; -import { uiPath, uiPublicPath } from './test-paths.mjs'; +import { uiPath, uiSourcePublicPath } from './test-paths.mjs'; import { execFileSync } from 'node:child_process'; function renderConfig(env = {}) { const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'spritz-ui-')); - fs.copyFileSync(uiPublicPath('config.js'), path.join(tmpDir, 'config.js')); - fs.copyFileSync(uiPublicPath('index.html'), path.join(tmpDir, 'index.html')); + fs.copyFileSync(uiSourcePublicPath('config.js'), path.join(tmpDir, 'config.js')); + fs.copyFileSync(uiSourcePublicPath('index.html'), path.join(tmpDir, 'index.html')); execFileSync('/bin/sh', [uiPath('entrypoint.sh')], { env: { diff --git a/ui/package.json b/ui/package.json new file mode 100644 index 0000000..9e66b19 --- /dev/null +++ b/ui/package.json @@ -0,0 +1,15 @@ +{ + "name": "@spritz/ui", + "private": true, + "type": "module", + "scripts": { + "build": "tsdown && node scripts/copy-static.mjs", + "typecheck": "tsc --noEmit -p tsconfig.json", + "test": "node --test entrypoint.test.mjs nginx-config.test.mjs public/*.test.mjs" + }, + "devDependencies": { + "@types/node": "^24.5.2", + "tsdown": "^0.16.6", + "typescript": "^5.9.2" + } +} diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml new file mode 100644 index 0000000..625dd33 --- /dev/null +++ b/ui/pnpm-lock.yaml @@ -0,0 +1,776 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@types/node': + specifier: ^24.5.2 + version: 24.12.0 + tsdown: + specifier: ^0.16.6 + version: 0.16.8(typescript@5.9.3) + typescript: + specifier: ^5.9.2 + version: 5.9.3 + +packages: + + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.0': + resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@emnapi/core@1.9.0': + resolution: {integrity: sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==} + + '@emnapi/runtime@1.9.0': + resolution: {integrity: sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==} + + '@emnapi/wasi-threads@1.2.0': + resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@napi-rs/wasm-runtime@1.1.1': + resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} + + '@oxc-project/types@0.115.0': + resolution: {integrity: sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==} + + '@oxc-project/types@0.99.0': + resolution: {integrity: sha512-LLDEhXB7g1m5J+woRSgfKsFPS3LhR9xRhTeIoEBm5WrkwMxn6eZ0Ld0c0K5eHB57ChZX6I3uSmmLjZ8pcjlRcw==} + + '@quansync/fs@1.0.0': + resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==} + + '@rolldown/binding-android-arm64@1.0.0-beta.52': + resolution: {integrity: sha512-MBGIgysimZPqTDcLXI+i9VveijkP5C3EAncEogXhqfax6YXj1Tr2LY3DVuEOMIjWfMPMhtQSPup4fSTAmgjqIw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-android-arm64@1.0.0-rc.9': + resolution: {integrity: sha512-lcJL0bN5hpgJfSIz/8PIf02irmyL43P+j1pTCfbD1DbLkmGRuFIA4DD3B3ZOvGqG0XiVvRznbKtN0COQVaKUTg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.0-beta.52': + resolution: {integrity: sha512-MmKeoLnKu1d9j6r19K8B+prJnIZ7u+zQ+zGQ3YHXGnr41rzE3eqQLovlkvoZnRoxDGPA4ps0pGiwXy6YE3lJyg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-arm64@1.0.0-rc.9': + resolution: {integrity: sha512-J7Zk3kLYFsLtuH6U+F4pS2sYVzac0qkjcO5QxHS7OS7yZu2LRs+IXo+uvJ/mvpyUljDJ3LROZPoQfgBIpCMhdQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.0-beta.52': + resolution: {integrity: sha512-qpHedvQBmIjT8zdnjN3nWPR2qjQyJttbXniCEKKdHeAbZG9HyNPBUzQF7AZZGwmS9coQKL+hWg9FhWzh2dZ2IA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.0-rc.9': + resolution: {integrity: sha512-iwtmmghy8nhfRGeNAIltcNXzD0QMNaaA5U/NyZc1Ia4bxrzFByNMDoppoC+hl7cDiUq5/1CnFthpT9n+UtfFyg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-beta.52': + resolution: {integrity: sha512-dDp7WbPapj/NVW0LSiH/CLwMhmLwwKb3R7mh2kWX+QW85X1DGVnIEyKh9PmNJjB/+suG1dJygdtdNPVXK1hylg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-freebsd-x64@1.0.0-rc.9': + resolution: {integrity: sha512-DLFYI78SCiZr5VvdEplsVC2Vx53lnA4/Ga5C65iyldMVaErr86aiqCoNBLl92PXPfDtUYjUh+xFFor40ueNs4Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.52': + resolution: {integrity: sha512-9e4l6vy5qNSliDPqNfR6CkBOAx6PH7iDV4OJiEJzajajGrVy8gc/IKKJUsoE52G8ud8MX6r3PMl97NfwgOzB7g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.9': + resolution: {integrity: sha512-CsjTmTwd0Hri6iTw/DRMK7kOZ7FwAkrO4h8YWKoX/kcj833e4coqo2wzIFywtch/8Eb5enQ/lwLM7w6JX1W5RQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.52': + resolution: {integrity: sha512-V48oDR84feRU2KRuzpALp594Uqlx27+zFsT6+BgTcXOtu7dWy350J1G28ydoCwKB+oxwsRPx2e7aeQnmd3YJbQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.9': + resolution: {integrity: sha512-2x9O2JbSPxpxMDhP9Z74mahAStibTlrBMW0520+epJH5sac7/LwZW5Bmg/E6CXuEF53JJFW509uP+lSedaUNxg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.52': + resolution: {integrity: sha512-ENLmSQCWqSA/+YN45V2FqTIemg7QspaiTjlm327eUAMeOLdqmSOVVyrQexJGNTQ5M8sDYCgVAig2Kk01Ggmqaw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.9': + resolution: {integrity: sha512-JA1QRW31ogheAIRhIg9tjMfsYbglXXYGNPLdPEYrwFxdbkQCAzvpSCSHCDWNl4hTtrol8WeboCSEpjdZK8qrCg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.9': + resolution: {integrity: sha512-aOKU9dJheda8Kj8Y3w9gnt9QFOO+qKPAl8SWd7JPHP+Cu0EuDAE5wokQubLzIDQWg2myXq2XhTpOVS07qqvT+w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.9': + resolution: {integrity: sha512-OalO94fqj7IWRn3VdXWty75jC5dk4C197AWEuMhIpvVv2lw9fiPhud0+bW2ctCxb3YoBZor71QHbY+9/WToadA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.52': + resolution: {integrity: sha512-klahlb2EIFltSUubn/VLjuc3qxp1E7th8ukayPfdkcKvvYcQ5rJztgx8JsJSuAKVzKtNTqUGOhy4On71BuyV8g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.9': + resolution: {integrity: sha512-cVEl1vZtBsBZna3YMjGXNvnYYrOJ7RzuWvZU0ffvJUexWkukMaDuGhUXn0rjnV0ptzGVkvc+vW9Yqy6h8YX4pg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.0.0-beta.52': + resolution: {integrity: sha512-UuA+JqQIgqtkgGN2c/AQ5wi8M6mJHrahz/wciENPTeI6zEIbbLGoth5XN+sQe2pJDejEVofN9aOAp0kaazwnVg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.9': + resolution: {integrity: sha512-UzYnKCIIc4heAKgI4PZ3dfBGUZefGCJ1TPDuLHoCzgrMYPb5Rv6TLFuYtyM4rWyHM7hymNdsg5ik2C+UD9VDbA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.0.0-beta.52': + resolution: {integrity: sha512-1BNQW8u4ro8bsN1+tgKENJiqmvc+WfuaUhXzMImOVSMw28pkBKdfZtX2qJPADV3terx+vNJtlsgSGeb3+W6Jiw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.9': + resolution: {integrity: sha512-+6zoiF+RRyf5cdlFQP7nm58mq7+/2PFaY2DNQeD4B87N36JzfF/l9mdBkkmTvSYcYPE8tMh/o3cRlsx1ldLfog==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.0-beta.52': + resolution: {integrity: sha512-K/p7clhCqJOQpXGykrFaBX2Dp9AUVIDHGc+PtFGBwg7V+mvBTv/tsm3LC3aUmH02H2y3gz4y+nUTQ0MLpofEEg==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.9': + resolution: {integrity: sha512-rgFN6sA/dyebil3YTlL2evvi/M+ivhfnyxec7AccTpRPccno/rPoNlqybEZQBkcbZu8Hy+eqNJCqfBR8P7Pg8g==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.52': + resolution: {integrity: sha512-a4EkXBtnYYsKipjS7QOhEBM4bU5IlR9N1hU+JcVEVeuTiaslIyhWVKsvf7K2YkQHyVAJ+7/A9BtrGqORFcTgng==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.9': + resolution: {integrity: sha512-lHVNUG/8nlF1IQk1C0Ci574qKYyty2goMiPlRqkC5R+3LkXDkL5Dhx8ytbxq35m+pkHVIvIxviD+TWLdfeuadA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.52': + resolution: {integrity: sha512-5ZXcYyd4GxPA6QfbGrNcQjmjbuLGvfz6728pZMsQvGHI+06LT06M6TPtXvFvLgXtexc+OqvFe1yAIXJU1gob/w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.52': + resolution: {integrity: sha512-tzpnRQXJrSzb8Z9sm97UD3cY0toKOImx+xRKsDLX4zHaAlRXWh7jbaKBePJXEN7gNw7Nm03PBNwphdtA8KSUYQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.9': + resolution: {integrity: sha512-G0oA4+w1iY5AGi5HcDTxWsoxF509hrFIPB2rduV5aDqS9FtDg1CAfa7V34qImbjfhIcA8C+RekocJZA96EarwQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.0-beta.52': + resolution: {integrity: sha512-/L0htLJZbaZFL1g9OHOblTxbCYIGefErJjtYOwgl9ZqNx27P3L0SDfjhhHIss32gu5NWgnxuT2a2Hnnv6QGHKA==} + + '@rolldown/pluginutils@1.0.0-rc.9': + resolution: {integrity: sha512-w6oiRWgEBl04QkFZgmW+jnU1EC9b57Oihi2ot3HNWIQRqgHp5PnYDia5iZ5FF7rpa4EQdiqMDXjlqKGXBhsoXw==} + + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + + '@types/node@24.12.0': + resolution: {integrity: sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==} + + ansis@4.2.0: + resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} + engines: {node: '>=14'} + + ast-kit@2.2.0: + resolution: {integrity: sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==} + engines: {node: '>=20.19.0'} + + birpc@4.0.0: + resolution: {integrity: sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw==} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + + diff@8.0.3: + resolution: {integrity: sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==} + engines: {node: '>=0.3.1'} + + dts-resolver@2.1.3: + resolution: {integrity: sha512-bihc7jPC90VrosXNzK0LTE2cuLP6jr0Ro8jk+kMugHReJVLIpHz/xadeq3MhuwyO4TD4OA3L1Q8pBBFRc08Tsw==} + engines: {node: '>=20.19.0'} + peerDependencies: + oxc-resolver: '>=11.0.0' + peerDependenciesMeta: + oxc-resolver: + optional: true + + empathic@2.0.0: + resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==} + engines: {node: '>=14'} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + get-tsconfig@4.13.6: + resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==} + + hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + quansync@1.0.0: + resolution: {integrity: sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==} + + readdirp@5.0.0: + resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} + engines: {node: '>= 20.19.0'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + rolldown-plugin-dts@0.18.4: + resolution: {integrity: sha512-7UpdiICFd/BhdjKtDPeakCFRk6pbkTGFe0Z6u01egt4c8aoO+JoPGF1Smc+JRuCH2s5j5hBdteBi0e10G0xQdQ==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@ts-macro/tsc': ^0.3.6 + '@typescript/native-preview': '>=7.0.0-dev.20250601.1' + rolldown: ^1.0.0-beta.51 + typescript: ^5.0.0 + vue-tsc: ~3.1.0 + peerDependenciesMeta: + '@ts-macro/tsc': + optional: true + '@typescript/native-preview': + optional: true + typescript: + optional: true + vue-tsc: + optional: true + + rolldown@1.0.0-beta.52: + resolution: {integrity: sha512-Hbnpljue+JhMJrlOjQ1ixp9me7sUec7OjFvS+A1Qm8k8Xyxmw3ZhxFu7LlSXW1s9AX3POE9W9o2oqCEeR5uDmg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + rolldown@1.0.0-rc.9: + resolution: {integrity: sha512-9EbgWge7ZH+yqb4d2EnELAntgPTWbfL8ajiTW+SyhJEC4qhBbkCKbqFV4Ge4zmu5ziQuVbWxb/XwLZ+RIO7E8Q==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + tinyexec@1.0.2: + resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} + engines: {node: '>=18'} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + tsdown@0.16.8: + resolution: {integrity: sha512-6ANw9mgU9kk7SvTBKvpDu/DVJeAFECiLUSeL5M7f5Nm5H97E7ybxmXT4PQ23FySYn32y6OzjoAH/lsWCbGzfLA==} + engines: {node: '>=20.19.0'} + hasBin: true + peerDependencies: + '@arethetypeswrong/core': ^0.18.1 + '@vitejs/devtools': ^0.0.0-alpha.18 + publint: ^0.3.0 + typescript: ^5.0.0 + unplugin-lightningcss: ^0.4.0 + unplugin-unused: ^0.5.0 + peerDependenciesMeta: + '@arethetypeswrong/core': + optional: true + '@vitejs/devtools': + optional: true + publint: + optional: true + typescript: + optional: true + unplugin-lightningcss: + optional: true + unplugin-unused: + optional: true + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + unconfig-core@7.5.0: + resolution: {integrity: sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w==} + + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + + unrun@0.2.32: + resolution: {integrity: sha512-opd3z6791rf281JdByf0RdRQrpcc7WyzqittqIXodM/5meNWdTwrVxeyzbaCp4/Rgls/um14oUaif1gomO8YGg==} + engines: {node: '>=20.19.0'} + hasBin: true + peerDependencies: + synckit: ^0.11.11 + peerDependenciesMeta: + synckit: + optional: true + +snapshots: + + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/parser@7.29.0': + dependencies: + '@babel/types': 7.29.0 + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@emnapi/core@1.9.0': + dependencies: + '@emnapi/wasi-threads': 1.2.0 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.9.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@napi-rs/wasm-runtime@1.1.1': + dependencies: + '@emnapi/core': 1.9.0 + '@emnapi/runtime': 1.9.0 + '@tybys/wasm-util': 0.10.1 + optional: true + + '@oxc-project/types@0.115.0': {} + + '@oxc-project/types@0.99.0': {} + + '@quansync/fs@1.0.0': + dependencies: + quansync: 1.0.0 + + '@rolldown/binding-android-arm64@1.0.0-beta.52': + optional: true + + '@rolldown/binding-android-arm64@1.0.0-rc.9': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-beta.52': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-rc.9': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-beta.52': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-rc.9': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-beta.52': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-rc.9': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.52': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.9': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.52': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.9': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.52': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.9': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.9': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.9': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.52': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.9': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-beta.52': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.9': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.0-beta.52': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.9': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-beta.52': + dependencies: + '@napi-rs/wasm-runtime': 1.1.1 + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.9': + dependencies: + '@napi-rs/wasm-runtime': 1.1.1 + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.52': + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.9': + optional: true + + '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.52': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.52': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.9': + optional: true + + '@rolldown/pluginutils@1.0.0-beta.52': {} + + '@rolldown/pluginutils@1.0.0-rc.9': {} + + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/node@24.12.0': + dependencies: + undici-types: 7.16.0 + + ansis@4.2.0: {} + + ast-kit@2.2.0: + dependencies: + '@babel/parser': 7.29.0 + pathe: 2.0.3 + + birpc@4.0.0: {} + + cac@6.7.14: {} + + chokidar@5.0.0: + dependencies: + readdirp: 5.0.0 + + diff@8.0.3: {} + + dts-resolver@2.1.3: {} + + empathic@2.0.0: {} + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + get-tsconfig@4.13.6: + dependencies: + resolve-pkg-maps: 1.0.0 + + hookable@5.5.3: {} + + jsesc@3.1.0: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + obug@2.1.1: {} + + pathe@2.0.3: {} + + picomatch@4.0.3: {} + + quansync@1.0.0: {} + + readdirp@5.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + rolldown-plugin-dts@0.18.4(rolldown@1.0.0-beta.52)(typescript@5.9.3): + dependencies: + '@babel/generator': 7.29.1 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + ast-kit: 2.2.0 + birpc: 4.0.0 + dts-resolver: 2.1.3 + get-tsconfig: 4.13.6 + magic-string: 0.30.21 + obug: 2.1.1 + rolldown: 1.0.0-beta.52 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - oxc-resolver + + rolldown@1.0.0-beta.52: + dependencies: + '@oxc-project/types': 0.99.0 + '@rolldown/pluginutils': 1.0.0-beta.52 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-beta.52 + '@rolldown/binding-darwin-arm64': 1.0.0-beta.52 + '@rolldown/binding-darwin-x64': 1.0.0-beta.52 + '@rolldown/binding-freebsd-x64': 1.0.0-beta.52 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.52 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.52 + '@rolldown/binding-linux-arm64-musl': 1.0.0-beta.52 + '@rolldown/binding-linux-x64-gnu': 1.0.0-beta.52 + '@rolldown/binding-linux-x64-musl': 1.0.0-beta.52 + '@rolldown/binding-openharmony-arm64': 1.0.0-beta.52 + '@rolldown/binding-wasm32-wasi': 1.0.0-beta.52 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.52 + '@rolldown/binding-win32-ia32-msvc': 1.0.0-beta.52 + '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.52 + + rolldown@1.0.0-rc.9: + dependencies: + '@oxc-project/types': 0.115.0 + '@rolldown/pluginutils': 1.0.0-rc.9 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-rc.9 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.9 + '@rolldown/binding-darwin-x64': 1.0.0-rc.9 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.9 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.9 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.9 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.9 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.9 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.9 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.9 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.9 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.9 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.9 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.9 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.9 + + semver@7.7.4: {} + + tinyexec@1.0.2: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tree-kill@1.2.2: {} + + tsdown@0.16.8(typescript@5.9.3): + dependencies: + ansis: 4.2.0 + cac: 6.7.14 + chokidar: 5.0.0 + diff: 8.0.3 + empathic: 2.0.0 + hookable: 5.5.3 + obug: 2.1.1 + rolldown: 1.0.0-beta.52 + rolldown-plugin-dts: 0.18.4(rolldown@1.0.0-beta.52)(typescript@5.9.3) + semver: 7.7.4 + tinyexec: 1.0.2 + tinyglobby: 0.2.15 + tree-kill: 1.2.2 + unconfig-core: 7.5.0 + unrun: 0.2.32 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - '@ts-macro/tsc' + - '@typescript/native-preview' + - oxc-resolver + - synckit + - vue-tsc + + tslib@2.8.1: + optional: true + + typescript@5.9.3: {} + + unconfig-core@7.5.0: + dependencies: + '@quansync/fs': 1.0.0 + quansync: 1.0.0 + + undici-types@7.16.0: {} + + unrun@0.2.32: + dependencies: + rolldown: 1.0.0-rc.9 diff --git a/ui/public/acp-client.test.mjs b/ui/public/acp-client.test.mjs index 14ff960..f9720d6 100644 --- a/ui/public/acp-client.test.mjs +++ b/ui/public/acp-client.test.mjs @@ -2,7 +2,7 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import vm from 'node:vm'; -import { uiPublicPath } from '../test-paths.mjs'; +import { uiDistPath } from '../test-paths.mjs'; function loadACPClientModule() { const sockets = []; @@ -58,7 +58,7 @@ function loadACPClientModule() { }); context.globalThis = context.window; - vm.runInContext(fs.readFileSync(uiPublicPath('acp-client.js'), 'utf8'), context, { + vm.runInContext(fs.readFileSync(uiDistPath('acp-client.js'), 'utf8'), context, { filename: 'acp-client.js', }); diff --git a/ui/public/acp-layout-css.test.mjs b/ui/public/acp-layout-css.test.mjs index 6e58fca..c439db2 100644 --- a/ui/public/acp-layout-css.test.mjs +++ b/ui/public/acp-layout-css.test.mjs @@ -1,9 +1,9 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; -import { uiPublicPath } from '../test-paths.mjs'; +import { uiDistPath } from '../test-paths.mjs'; -const styles = fs.readFileSync(uiPublicPath('styles.css'), 'utf8'); +const styles = fs.readFileSync(uiDistPath('styles.css'), 'utf8'); function getRule(selector) { const escapedSelector = selector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); diff --git a/ui/public/acp-page-cache.test.mjs b/ui/public/acp-page-cache.test.mjs index ffb19c6..61befdd 100644 --- a/ui/public/acp-page-cache.test.mjs +++ b/ui/public/acp-page-cache.test.mjs @@ -2,7 +2,7 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import vm from 'node:vm'; -import { uiPublicPath } from '../test-paths.mjs'; +import { uiDistPath } from '../test-paths.mjs'; function createStorage(seed = {}) { const values = new Map(Object.entries(seed)); @@ -111,10 +111,10 @@ function loadModules(storageSeed = {}, createACPClient = null) { window.window = window; const context = vm.createContext({ window, document, console, setTimeout, clearTimeout, URL, URLSearchParams }); context.globalThis = context.window; - vm.runInContext(fs.readFileSync(uiPublicPath('acp-render.js'), 'utf8'), context, { + vm.runInContext(fs.readFileSync(uiDistPath('acp-render.js'), 'utf8'), context, { filename: 'acp-render.js', }); - vm.runInContext(fs.readFileSync(uiPublicPath('acp-page.js'), 'utf8'), context, { + vm.runInContext(fs.readFileSync(uiDistPath('acp-page.js'), 'utf8'), context, { filename: 'acp-page.js', }); return window; diff --git a/ui/public/acp-page-layout.test.mjs b/ui/public/acp-page-layout.test.mjs index 5fca34f..cf31e3e 100644 --- a/ui/public/acp-page-layout.test.mjs +++ b/ui/public/acp-page-layout.test.mjs @@ -2,7 +2,7 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import vm from 'node:vm'; -import { uiPublicPath } from '../test-paths.mjs'; +import { uiDistPath } from '../test-paths.mjs'; function createElement(tagName) { return { @@ -76,9 +76,9 @@ function loadModules() { window.window = window; const context = vm.createContext({ window, document, console, setTimeout, clearTimeout }); context.globalThis = context.window; - const renderScript = fs.readFileSync(uiPublicPath('acp-render.js'), 'utf8'); + const renderScript = fs.readFileSync(uiDistPath('acp-render.js'), 'utf8'); vm.runInContext(renderScript, context, { filename: 'acp-render.js' }); - const pageScript = fs.readFileSync(uiPublicPath('acp-page.js'), 'utf8'); + const pageScript = fs.readFileSync(uiDistPath('acp-page.js'), 'utf8'); vm.runInContext(pageScript, context, { filename: 'acp-page.js' }); return { window, document }; } diff --git a/ui/public/acp-page-notice.test.mjs b/ui/public/acp-page-notice.test.mjs index 33b4a87..0ae0b34 100644 --- a/ui/public/acp-page-notice.test.mjs +++ b/ui/public/acp-page-notice.test.mjs @@ -2,7 +2,7 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import vm from 'node:vm'; -import { uiPublicPath } from '../test-paths.mjs'; +import { uiDistPath } from '../test-paths.mjs'; function createElement(tagName) { return { @@ -65,10 +65,10 @@ function loadModules(createACPClient) { window.window = window; const context = vm.createContext({ window, document, console, setTimeout, clearTimeout, URL, URLSearchParams }); context.globalThis = context.window; - vm.runInContext(fs.readFileSync(uiPublicPath('acp-render.js'), 'utf8'), context, { + vm.runInContext(fs.readFileSync(uiDistPath('acp-render.js'), 'utf8'), context, { filename: 'acp-render.js', }); - vm.runInContext(fs.readFileSync(uiPublicPath('acp-page.js'), 'utf8'), context, { + vm.runInContext(fs.readFileSync(uiDistPath('acp-page.js'), 'utf8'), context, { filename: 'acp-page.js', }); return window; diff --git a/ui/public/acp-page-session-binding.test.mjs b/ui/public/acp-page-session-binding.test.mjs index 883ef24..d2d5a97 100644 --- a/ui/public/acp-page-session-binding.test.mjs +++ b/ui/public/acp-page-session-binding.test.mjs @@ -2,7 +2,7 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import vm from 'node:vm'; -import { uiPublicPath } from '../test-paths.mjs'; +import { uiDistPath } from '../test-paths.mjs'; function createElement(tagName) { const listeners = new Map(); @@ -82,10 +82,10 @@ function loadModules(createACPClient) { window.window = window; const context = vm.createContext({ window, document, console, setTimeout, clearTimeout, URL, URLSearchParams }); context.globalThis = context.window; - vm.runInContext(fs.readFileSync(uiPublicPath('acp-render.js'), 'utf8'), context, { + vm.runInContext(fs.readFileSync(uiDistPath('acp-render.js'), 'utf8'), context, { filename: 'acp-render.js', }); - vm.runInContext(fs.readFileSync(uiPublicPath('acp-page.js'), 'utf8'), context, { + vm.runInContext(fs.readFileSync(uiDistPath('acp-page.js'), 'utf8'), context, { filename: 'acp-page.js', }); return window; diff --git a/ui/public/acp-render.test.mjs b/ui/public/acp-render.test.mjs index e3dabdb..3deff9f 100644 --- a/ui/public/acp-render.test.mjs +++ b/ui/public/acp-render.test.mjs @@ -2,7 +2,7 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import vm from 'node:vm'; -import { uiPublicPath } from '../test-paths.mjs'; +import { uiDistPath } from '../test-paths.mjs'; function createElement(tagName) { return { @@ -51,7 +51,7 @@ function loadRenderModule() { window.window = window; const context = vm.createContext({ window, document, console }); context.globalThis = context.window; - const script = fs.readFileSync(uiPublicPath('acp-render.js'), 'utf8'); + const script = fs.readFileSync(uiDistPath('acp-render.js'), 'utf8'); vm.runInContext(script, context, { filename: 'acp-render.js' }); return window.SpritzACPRender; } diff --git a/ui/public/app-chat-route.test.mjs b/ui/public/app-chat-route.test.mjs index cff9c1d..94892f5 100644 --- a/ui/public/app-chat-route.test.mjs +++ b/ui/public/app-chat-route.test.mjs @@ -2,7 +2,7 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import vm from 'node:vm'; -import { uiPublicPath } from '../test-paths.mjs'; +import { uiDistPath } from '../test-paths.mjs'; function createStorage() { const values = new Map(); @@ -152,7 +152,7 @@ test('chat hash route initializes without throwing', () => { }); context.globalThis = context.window; - const script = fs.readFileSync(uiPublicPath('app.js'), 'utf8'); + const script = fs.readFileSync(uiDistPath('app.js'), 'utf8'); assert.doesNotThrow(() => { vm.runInContext(script, context, { filename: 'app.js' }); }); diff --git a/ui/public/app-list-actions.test.mjs b/ui/public/app-list-actions.test.mjs index 56cf89c..24cb923 100644 --- a/ui/public/app-list-actions.test.mjs +++ b/ui/public/app-list-actions.test.mjs @@ -2,7 +2,7 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import vm from 'node:vm'; -import { uiPublicPath } from '../test-paths.mjs'; +import { uiDistPath } from '../test-paths.mjs'; function createStorage() { const values = new Map(); @@ -175,7 +175,7 @@ test('spritz list shows a transitional chat action while workspace chat is still }); context.globalThis = context.window; - const script = fs.readFileSync(uiPublicPath('app.js'), 'utf8'); + const script = fs.readFileSync(uiDistPath('app.js'), 'utf8'); vm.runInContext(script, context, { filename: 'app.js' }); await new Promise((resolve) => setTimeout(resolve, 0)); diff --git a/ui/public/create-form-request.test.mjs b/ui/public/create-form-request.test.mjs index ce6bb2a..55aad8d 100644 --- a/ui/public/create-form-request.test.mjs +++ b/ui/public/create-form-request.test.mjs @@ -1,19 +1,38 @@ import test from 'node:test'; import assert from 'node:assert/strict'; -import { createRequire } from 'node:module'; +import fs from 'node:fs'; +import vm from 'node:vm'; +import { uiDistPath } from '../test-paths.mjs'; + +function loadCreateFormRequestModule() { + const context = { + console, + globalThis: {}, + module: { exports: {} }, + }; + context.globalThis = context; + vm.createContext(context); + vm.runInContext(fs.readFileSync(uiDistPath('create-form-request.js'), 'utf8'), context, { + filename: uiDistPath('create-form-request.js'), + }); + return context.module.exports; +} + +function plain(value) { + return JSON.parse(JSON.stringify(value)); +} test('resolveRepoSelection falls back to repo defaults for presets without repo ownership', async () => { - const require = createRequire(import.meta.url); - const { resolveRepoSelection } = require('./create-form-request.js'); + const { resolveRepoSelection } = loadCreateFormRequestModule(); assert.deepEqual( - resolveRepoSelection({ + plain(resolveRepoSelection({ activePreset: { name: 'Starter (minimal)', image: 'spritz-starter:latest' }, repoValue: '', branchValue: '', defaultRepoUrl: 'https://github.com/example/repo.git', defaultRepoBranch: 'main', - }), + })), { repoUrl: 'https://github.com/example/repo.git', repoBranch: 'main', @@ -22,11 +41,10 @@ test('resolveRepoSelection falls back to repo defaults for presets without repo }); test('resolveRepoSelection preserves explicit blank repo settings owned by the preset', async () => { - const require = createRequire(import.meta.url); - const { resolveRepoSelection } = require('./create-form-request.js'); + const { resolveRepoSelection } = loadCreateFormRequestModule(); assert.deepEqual( - resolveRepoSelection({ + plain(resolveRepoSelection({ activePreset: { name: 'OpenClaw', image: 'spritz-openclaw:latest', @@ -37,7 +55,7 @@ test('resolveRepoSelection preserves explicit blank repo settings owned by the p branchValue: '', defaultRepoUrl: 'https://github.com/example/private.git', defaultRepoBranch: 'staging', - }), + })), { repoUrl: '', repoBranch: '', @@ -46,8 +64,7 @@ test('resolveRepoSelection preserves explicit blank repo settings owned by the p }); test('buildCreatePayload uses presetId and does not serialize preset env overrides', async () => { - const require = createRequire(import.meta.url); - const { buildCreatePayload } = require('./create-form-request.js'); + const { buildCreatePayload } = loadCreateFormRequestModule(); const payload = buildCreatePayload({ name: '', @@ -81,14 +98,13 @@ test('buildCreatePayload uses presetId and does not serialize preset env overrid assert.equal(payload.presetId, 'claude-code'); assert.equal(payload.namePrefix, 'claude-code'); - assert.deepEqual(payload.spec.owner, { id: 'user-123' }); + assert.deepEqual(plain(payload.spec.owner), { id: 'user-123' }); assert.equal(payload.spec.image, undefined); assert.equal(payload.spec.env, undefined); }); test('buildCreatePayload falls back to explicit image when preset is no longer aligned', async () => { - const require = createRequire(import.meta.url); - const { buildCreatePayload } = require('./create-form-request.js'); + const { buildCreatePayload } = loadCreateFormRequestModule(); const payload = buildCreatePayload({ name: '', diff --git a/ui/public/create-form-state.test.mjs b/ui/public/create-form-state.test.mjs index b256381..6c61844 100644 --- a/ui/public/create-form-state.test.mjs +++ b/ui/public/create-form-state.test.mjs @@ -1,6 +1,26 @@ import test from 'node:test'; import assert from 'node:assert/strict'; -import { createRequire } from 'node:module'; +import fs from 'node:fs'; +import vm from 'node:vm'; +import { uiDistPath } from '../test-paths.mjs'; + +function loadCreateFormStateModule() { + const context = { + console, + globalThis: {}, + module: { exports: {} }, + }; + context.globalThis = context; + vm.createContext(context); + vm.runInContext(fs.readFileSync(uiDistPath('create-form-state.js'), 'utf8'), context, { + filename: uiDistPath('create-form-state.js'), + }); + return context.module.exports; +} + +function plain(value) { + return JSON.parse(JSON.stringify(value)); +} function createStorage(seed = {}) { const values = new Map(Object.entries(seed).map(([key, value]) => [key, String(value)])); @@ -21,8 +41,7 @@ function createStorage(seed = {}) { } test('buildCreateFormState keeps preset selection only when image still matches', async () => { - const require = createRequire(import.meta.url); - const { buildCreateFormState } = require('./create-form-state.js'); + const { buildCreateFormState } = loadCreateFormStateModule(); const state = buildCreateFormState({ activePreset: { @@ -37,18 +56,17 @@ test('buildCreateFormState keeps preset selection only when image still matches' userConfig: 'sharedMounts: []', }); - assert.deepEqual(state.selection, { mode: 'custom' }); + assert.deepEqual(plain(state.selection), { mode: 'custom' }); assert.equal(state.fields.image, 'custom-image:latest'); }); test('writeCreateFormState stores reusable form state without a name field', async () => { - const require = createRequire(import.meta.url); const { CREATE_FORM_STORAGE_KEY, buildCreateFormState, readCreateFormState, writeCreateFormState, - } = require('./create-form-state.js'); + } = loadCreateFormStateModule(); const storage = createStorage(); const input = buildCreateFormState({ diff --git a/ui/public/preset-config.test.mjs b/ui/public/preset-config.test.mjs index fa44098..e4723e8 100644 --- a/ui/public/preset-config.test.mjs +++ b/ui/public/preset-config.test.mjs @@ -1,10 +1,28 @@ import test from 'node:test'; import assert from 'node:assert/strict'; -import { createRequire } from 'node:module'; -import { uiPublicPath } from '../test-paths.mjs'; +import fs from 'node:fs'; +import vm from 'node:vm'; +import { uiDistPath } from '../test-paths.mjs'; -const require = createRequire(import.meta.url); -const { parsePresets } = require(uiPublicPath('preset-config.js')); +function loadPresetConfigModule() { + const context = { + console, + globalThis: {}, + module: { exports: {} }, + }; + context.globalThis = context; + vm.createContext(context); + vm.runInContext(fs.readFileSync(uiDistPath('preset-config.js'), 'utf8'), context, { + filename: uiDistPath('preset-config.js'), + }); + return context.module.exports; +} + +const { parsePresets } = loadPresetConfigModule(); + +function plain(value) { + return JSON.parse(JSON.stringify(value)); +} test('parsePresets returns raw arrays directly', () => { const presets = [{ name: 'OpenClaw', image: 'example/openclaw' }]; @@ -20,6 +38,6 @@ test('parsePresets fails closed for malformed values', () => { const parsed = parsePresets('[{"name":"OpenClaw","image":"broken"}', { logger: { error: (...args) => errors.push(args) }, }); - assert.deepEqual(parsed, []); + assert.deepEqual(plain(parsed), []); assert.equal(errors.length, 1); }); diff --git a/ui/public/preset-panel.test.mjs b/ui/public/preset-panel.test.mjs index c634161..5079129 100644 --- a/ui/public/preset-panel.test.mjs +++ b/ui/public/preset-panel.test.mjs @@ -1,6 +1,22 @@ import test from 'node:test'; import assert from 'node:assert/strict'; -import { createRequire } from 'node:module'; +import fs from 'node:fs'; +import vm from 'node:vm'; +import { uiDistPath } from '../test-paths.mjs'; + +function loadPresetPanelModule() { + const context = { + console, + globalThis: {}, + module: { exports: {} }, + }; + context.globalThis = context; + vm.createContext(context); + vm.runInContext(fs.readFileSync(uiDistPath('preset-panel.js'), 'utf8'), context, { + filename: uiDistPath('preset-panel.js'), + }); + return context.module.exports; +} class FakeElement { constructor(tagName, ownerDocument) { @@ -147,8 +163,7 @@ function buildFormFixture() { } test('setupPresetPanel injects the preset selector and updates image fields', async () => { - const require = createRequire(import.meta.url); - const { setupPresetPanel } = require('./preset-panel.js'); + const { setupPresetPanel } = loadPresetPanelModule(); const { document, form, imageInput, repoInput, branchInput, ttlInput } = buildFormFixture(); let activePreset = null; @@ -216,8 +231,7 @@ test('setupPresetPanel injects the preset selector and updates image fields', as }); test('setupPresetPanel restores a saved preset selection and falls back to custom', async () => { - const require = createRequire(import.meta.url); - const { setupPresetPanel } = require('./preset-panel.js'); + const { setupPresetPanel } = loadPresetPanelModule(); const { document, form, imageInput, repoInput, branchInput, ttlInput } = buildFormFixture(); let activePreset = null; @@ -268,8 +282,7 @@ test('setupPresetPanel restores a saved preset selection and falls back to custo }); test('setupPresetPanel clears hidden repo defaults when a preset explicitly owns blank repo fields', async () => { - const require = createRequire(import.meta.url); - const { setupPresetPanel } = require('./preset-panel.js'); + const { setupPresetPanel } = loadPresetPanelModule(); const { document, form, imageInput, repoInput, branchInput } = buildFormFixture(); repoInput.value = 'https://github.com/example/private.git'; diff --git a/ui/scripts/copy-static.mjs b/ui/scripts/copy-static.mjs new file mode 100644 index 0000000..de5ad49 --- /dev/null +++ b/ui/scripts/copy-static.mjs @@ -0,0 +1,39 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const uiDir = path.dirname(path.dirname(fileURLToPath(import.meta.url))); +const sourceDir = path.join(uiDir, 'public'); +const distDir = path.join(uiDir, 'dist'); +const staticFiles = ['config.js', 'index.html', 'styles.css']; +const staticDirectories = ['vendor']; +const builtEntries = [ + 'acp-client', + 'acp-page', + 'acp-render', + 'app', + 'create-form-request', + 'create-form-state', + 'preset-config', + 'preset-panel', +]; + +fs.mkdirSync(distDir, { recursive: true }); +for (const name of staticFiles) { + fs.copyFileSync(path.join(sourceDir, name), path.join(distDir, name)); +} + +for (const name of staticDirectories) { + const sourcePath = path.join(sourceDir, name); + const targetPath = path.join(distDir, name); + fs.rmSync(targetPath, { recursive: true, force: true }); + fs.cpSync(sourcePath, targetPath, { recursive: true }); +} + +for (const entry of builtEntries) { + const iifePath = path.join(distDir, `${entry}.iife.js`); + const normalizedPath = path.join(distDir, `${entry}.js`); + if (fs.existsSync(iifePath)) { + fs.renameSync(iifePath, normalizedPath); + } +} diff --git a/ui/public/acp-client.js b/ui/src/acp-client.ts similarity index 98% rename from ui/public/acp-client.js rename to ui/src/acp-client.ts index a404d70..e6d0ca3 100644 --- a/ui/public/acp-client.js +++ b/ui/src/acp-client.ts @@ -79,7 +79,7 @@ } function requestRPC(method, params) { - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { const id = nextId++; pending.set(String(id), { resolve, reject, method }); try { @@ -199,7 +199,7 @@ if (typeof onReadyChange === 'function') { onReadyChange(false); } - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { ws = new WebSocket(wsUrl); ws.onopen = async () => { try { diff --git a/ui/public/acp-page.js b/ui/src/acp-page.ts similarity index 98% rename from ui/public/acp-page.js rename to ui/src/acp-page.ts index 7147626..129df37 100644 --- a/ui/public/acp-page.js +++ b/ui/src/acp-page.ts @@ -7,6 +7,13 @@ const PRE_CUTOVER_ACP_TRANSCRIPT_CACHE_PREFIX = 'spritz:acp:transcript:'; const PRE_CUTOVER_ACP_TRANSCRIPT_CACHE_INDEX_KEY = 'spritz:acp:transcript:index'; + type ACPPage = any; + type ACPConversationBootstrapOptions = { + forceBootstrap?: boolean; + allowRepairRetry?: boolean; + allowAutoRebind?: boolean; + }; + function chatPagePath(name = '', conversationId = '') { if (!name) return '#chat'; if (!conversationId) return `#chat/${encodeURIComponent(name)}`; @@ -791,7 +798,10 @@ return response; } - function conversationNeedsBootstrap(conversation, options = {}) { + function conversationNeedsBootstrap( + conversation, + options: ACPConversationBootstrapOptions = {}, + ) { if (options.forceBootstrap) return true; const sessionId = String(conversation?.spec?.sessionId || '').trim(); if (!sessionId) return true; @@ -842,7 +852,10 @@ setStatus(page, 'Disconnected. Reconnecting…'); } - async function connectSelectedConversation(page, options = {}) { + async function connectSelectedConversation( + page, + options: ACPConversationBootstrapOptions = {}, + ) { if (!page.selectedAgent || !page.selectedConversation) { resetConversationRuntime(page); renderThread(page); @@ -1064,7 +1077,7 @@ renderAgentPicker(page); renderConversationList(page); renderThread(page); - setStatus(workspaceStatusSummary(page.selectedSpritz).status); + setStatus(page, workspaceStatusSummary(page.selectedSpritz).status); scheduleWorkspaceRefresh(page); return; } @@ -1093,7 +1106,7 @@ if (deps.shellEl?.dataset) deps.shellEl.dataset.view = 'chat'; deps.setHeaderCopy('Spritz', 'Agent chat'); - const page = createACPPageState(name, conversationId, deps); + const page: ACPPage = createACPPageState(name, conversationId, deps); const shell = document.createElement('section'); shell.className = 'card acp-shell'; diff --git a/ui/public/acp-render.js b/ui/src/acp-render.ts similarity index 99% rename from ui/public/acp-render.js rename to ui/src/acp-render.ts index 20a9150..a788441 100644 --- a/ui/public/acp-render.js +++ b/ui/src/acp-render.ts @@ -414,7 +414,13 @@ .replace(/\b\w/g, (match) => match.toUpperCase()); } - function applySessionUpdate(transcript, update, options = {}) { + function applySessionUpdate( + transcript, + update, + options: { + historical?: boolean; + } = {}, + ) { const type = update?.sessionUpdate || 'unknown'; const historical = Boolean(options.historical); if (type === 'user_message_chunk') { diff --git a/ui/public/app.js b/ui/src/app.ts similarity index 95% rename from ui/public/app.js rename to ui/src/app.ts index b5e4ccd..c7c898c 100644 --- a/ui/public/app.js +++ b/ui/src/app.ts @@ -27,16 +27,16 @@ const launchQueryParamsPlaceholder = '__SPRITZ_UI_LAUNCH_QUERY_PARAMS__'; const launchConfig = config.launch || {}; const launchQueryParams = parseTemplateMap(launchConfig.queryParams); const authReturnToPlaceholder = '__SPRITZ_RETURN_TO__'; -const noticeEl = document.getElementById('notice'); -const toastRegionEl = document.getElementById('toast-region'); -const listEl = document.getElementById('list'); -const refreshBtn = document.getElementById('refresh'); -const form = document.getElementById('create-form'); -const randomNameBtn = document.getElementById('name-random'); -const shellEl = document.querySelector('.shell'); -const headerEl = shellEl?.querySelector('header'); -const createSection = form?.closest('section'); -const listSection = listEl?.closest('section'); +const noticeEl = document.getElementById('notice') as HTMLElement | null; +const toastRegionEl = document.getElementById('toast-region') as HTMLElement | null; +const listEl = document.getElementById('list') as HTMLElement | null; +const refreshBtn = document.getElementById('refresh') as HTMLButtonElement | null; +const form = document.getElementById('create-form') as HTMLFormElement | null; +const randomNameBtn = document.getElementById('name-random') as HTMLButtonElement | null; +const shellEl = document.querySelector('.shell') as HTMLElement | null; +const headerEl = shellEl?.querySelector('header') as HTMLElement | null; +const createSection = form?.closest('section') as HTMLElement | null; +const listSection = listEl?.closest('section') as HTMLElement | null; let activeTerminalSession = null; let activeTerminalName = ''; let activeACPPage = null; @@ -91,6 +91,14 @@ const defaultPresets = [ }, ]; +type CreateFormField = HTMLInputElement | HTMLTextAreaElement; +type RequestOptions = RequestInit & { + __authRefreshAttemptId?: number; +}; +type ToastOptions = { + durationMs?: number; +}; + function parseBoolean(value, fallback) { if (value === undefined || value === null || value === '') return fallback; if (typeof value === 'boolean') return value; @@ -365,9 +373,13 @@ function getCreateFormStorage() { return window.localStorage || null; } -function getCreateFormField(name) { +function getCreateFormField(name): CreateFormField | null { if (!form) return null; - return form.querySelector(`input[name="${name}"]`) || form.querySelector(`textarea[name="${name}"]`) || null; + return ( + form.querySelector(`input[name="${name}"]`) || + form.querySelector(`textarea[name="${name}"]`) || + null + ); } function buildCreateFormStateSnapshot() { @@ -409,7 +421,9 @@ function applyPersistedCreateFormState(state) { return true; } -function resolveCreateRepoSelection(repoValue, branchValue) { +function resolveCreateRepoSelection(options) { + const repoValue = options?.repoValue; + const branchValue = options?.branchValue; if (createFormRequestModule && typeof createFormRequestModule.resolveRepoSelection === 'function') { return createFormRequestModule.resolveRepoSelection({ activePreset, @@ -435,7 +449,7 @@ function restoreCreateFormState() { function applyUserConfigDefaults() { if (!form) return; - const textarea = form.querySelector('textarea[name="user_config"]'); + const textarea = form.querySelector('textarea[name="user_config"]'); if (!textarea) return; if (!textarea.value.trim()) { textarea.value = defaultUserConfigYaml; @@ -444,7 +458,7 @@ function applyUserConfigDefaults() { function applyNameDefaults() { if (!form) return; - const input = form.querySelector('input[name="name"]'); + const input = form.querySelector('input[name="name"]'); if (!input) return; if (!input.placeholder) { input.placeholder = 'Leave blank to auto-generate.'; @@ -453,8 +467,8 @@ function applyNameDefaults() { function applyRepoDefaults() { if (!form) return; - const repoInput = form.querySelector('input[name="repo"]'); - const branchInput = form.querySelector('input[name="branch"]'); + const repoInput = form.querySelector('input[name="repo"]'); + const branchInput = form.querySelector('input[name="branch"]'); if (!repoInput || !branchInput) return; if (hideRepoInputs) { @@ -495,7 +509,7 @@ function clearNotice() { showNotice(''); } -function showToast(message, type = 'error', options = {}) { +function showToast(message, type = 'error', options: ToastOptions = {}) { if (!toastRegionEl || !message) return; const toast = document.createElement('div'); toast.className = 'toast'; @@ -781,7 +795,7 @@ async function readResponse(res) { } } -async function request(path, options = {}) { +async function request(path, options: RequestOptions = {}) { const headers = new Headers(options.headers || {}); const token = getAuthToken(); if (token) { @@ -851,13 +865,13 @@ async function suggestSpritzName() { if (!form) { throw new Error('Create form unavailable.'); } - const imageInput = form.querySelector('input[name="image"]'); - const namespaceInput = form.querySelector('input[name="namespace"]'); + const imageInput = form.querySelector('input[name="image"]'); + const namespaceInput = form.querySelector('input[name="namespace"]'); const image = String(imageInput?.value || '').trim(); if (!image) { throw new Error('Image is required before generating a name.'); } - const payload = { image }; + const payload: any = { image }; const namespace = String(namespaceInput?.value || '').trim(); if (namespace) { payload.namespace = namespace; @@ -1193,7 +1207,7 @@ function assetUrl(path) { } function loadStylesheet(href) { - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = href; @@ -1204,7 +1218,7 @@ function loadStylesheet(href) { } function loadScript(src) { - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { const script = document.createElement('script'); script.src = src; script.onload = () => resolve(); @@ -1430,7 +1444,7 @@ if (form && refreshBtn) { if (randomNameBtn) { randomNameBtn.addEventListener('click', async () => { - const input = form.querySelector('input[name="name"]'); + const input = form.querySelector('input[name="name"]'); if (!input) return; randomNameBtn.disabled = true; try { @@ -1446,7 +1460,7 @@ if (form && refreshBtn) { form.addEventListener('submit', async (event) => { event.preventDefault(); - const data = new FormData(form); + const data = new FormData(form as HTMLFormElement); const name = data.get('name'); const image = data.get('image'); const imageValue = (image || '').toString().trim(); @@ -1471,7 +1485,7 @@ if (form && refreshBtn) { ttlValue: ttl, }) : (() => { - const fallbackPayload = { + const fallbackPayload: any = { namespace: data.get('namespace') || undefined, spec: { image: imageValue, diff --git a/ui/public/create-form-request.js b/ui/src/create-form-request.ts similarity index 99% rename from ui/public/create-form-request.js rename to ui/src/create-form-request.ts index 5758ee1..d124be1 100644 --- a/ui/public/create-form-request.js +++ b/ui/src/create-form-request.ts @@ -60,7 +60,7 @@ const presetMatchesImage = !!activePreset && imageValue !== '' && String(activePreset.image || '').trim() === imageValue; - const payload = { + const payload: any = { namespace: namespace || undefined, spec: {}, }; diff --git a/ui/public/create-form-state.js b/ui/src/create-form-state.ts similarity index 100% rename from ui/public/create-form-state.js rename to ui/src/create-form-state.ts diff --git a/ui/src/globals.d.ts b/ui/src/globals.d.ts new file mode 100644 index 0000000..8f13832 --- /dev/null +++ b/ui/src/globals.d.ts @@ -0,0 +1,29 @@ +declare const module: + | { + exports?: unknown; + } + | undefined; + +declare global { + interface Error { + code?: string; + payload?: unknown; + rpcError?: unknown; + status?: number; + } + + interface Window { + SPRITZ_CONFIG?: any; + SpritzACPClient?: any; + SpritzACPRender?: any; + SpritzACPPage?: any; + SpritzCreateFormState?: any; + SpritzCreateFormRequest?: any; + SpritzPresetConfig?: any; + SpritzPresetPanel?: any; + Terminal?: any; + FitAddon?: any; + } +} + +export {}; diff --git a/ui/public/preset-config.js b/ui/src/preset-config.ts similarity index 100% rename from ui/public/preset-config.js rename to ui/src/preset-config.ts diff --git a/ui/public/preset-panel.js b/ui/src/preset-panel.ts similarity index 100% rename from ui/public/preset-panel.js rename to ui/src/preset-panel.ts diff --git a/ui/test-paths.mjs b/ui/test-paths.mjs index 70f4ce0..52ff182 100644 --- a/ui/test-paths.mjs +++ b/ui/test-paths.mjs @@ -13,6 +13,13 @@ export function uiPath(...parts) { /** * Resolve a path relative to the ui/public/ directory. */ -export function uiPublicPath(...parts) { +export function uiSourcePublicPath(...parts) { return path.join(uiDir, 'public', ...parts); } + +/** + * Resolve a path relative to the ui/dist/ directory. + */ +export function uiDistPath(...parts) { + return path.join(uiDir, 'dist', ...parts); +} diff --git a/ui/tsconfig.json b/ui/tsconfig.json new file mode 100644 index 0000000..6362955 --- /dev/null +++ b/ui/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "lib": ["DOM", "DOM.Iterable", "ES2023"], + "module": "ESNext", + "moduleResolution": "Bundler", + "noEmit": true, + "outDir": "dist", + "skipLibCheck": true, + "strict": false, + "target": "ES2023", + "types": ["node"] + }, + "include": ["src/**/*", "public/*.test.mjs", "entrypoint.test.mjs", "nginx-config.test.mjs", "test-paths.mjs"], + "exclude": ["dist", "node_modules"] +} diff --git a/ui/tsdown.config.ts b/ui/tsdown.config.ts new file mode 100644 index 0000000..df2c6c6 --- /dev/null +++ b/ui/tsdown.config.ts @@ -0,0 +1,28 @@ +import { defineConfig } from 'tsdown'; + +const entries = [ + 'acp-client', + 'acp-page', + 'acp-render', + 'app', + 'create-form-request', + 'create-form-state', + 'preset-config', + 'preset-panel', +] as const; + +function browserEntry(entry: string, clean: boolean) { + return defineConfig({ + clean, + dts: false, + entry: `src/${entry}.ts`, + format: 'iife', + minify: false, + outDir: 'dist', + platform: 'browser', + sourcemap: false, + target: 'es2023', + }); +} + +export default entries.map((entry, index) => browserEntry(entry, index === 0));