Merge commit 'b9b04fb28d2b158c215f082b3eb0285a5d01258f'
This commit is contained in:
commit
34d25d30c3
|
|
@ -62,6 +62,7 @@
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
- Load and browse online font provider catalogs in the browser app, downloading provider fonts over CORS-enabled fetches instead of redirecting users to the desktop app.
|
||||||
- Preserve app-created component properties and instance-swap targets across `.fig` save and reload cycles. (#548)
|
- Preserve app-created component properties and instance-swap targets across `.fig` save and reload cycles. (#548)
|
||||||
- Reconnect desktop automation to an already-running MCP server by allowing access to its discovery file. (#546)
|
- Reconnect desktop automation to an already-running MCP server by allowing access to its discovery file. (#546)
|
||||||
- Keep text-editing carets, hit testing, and selection highlights aligned with vertically centered or bottom-aligned text. (#539)
|
- Keep text-editing carets, hit testing, and selection highlights aligned with vertically centered or bottom-aligned text. (#539)
|
||||||
|
|
|
||||||
75
Dockerfile.web-vue
Normal file
75
Dockerfile.web-vue
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
#
|
||||||
|
# Dockerfile.web-vue — container for the VUE editor (master, v0.14.0) + MCP.
|
||||||
|
#
|
||||||
|
# Replaces the retired Dockerfile.web-rust (Rust web host, main branch). This
|
||||||
|
# image serves the static SPA build (the same `dist/` artifact that Cloudflare
|
||||||
|
# Pages deploys for app.openpencil.dev) on :3100 AND the @open-pencil/mcp
|
||||||
|
# Streamable-HTTP server on :7600, both from one container via the entrypoint.
|
||||||
|
# w4c-chatapi talks MCP to http://openpencil:7600/mcp (OpenPencilMcp__HttpEndpoint).
|
||||||
|
#
|
||||||
|
# Build:
|
||||||
|
# docker build -f Dockerfile.web-vue -t w4c-openpencil:latest .
|
||||||
|
#
|
||||||
|
# Run:
|
||||||
|
# docker run -p 3100:3100 -p 7600:7600 \
|
||||||
|
# -e OPENPENCIL_MCP_AUTH_TOKEN=... \
|
||||||
|
# -e OPENPENCIL_MCP_CORS_ORIGIN=https://openpencil.wiz4chat.com \
|
||||||
|
# -v openpencil_data:/data \
|
||||||
|
# w4c-openpencil:latest
|
||||||
|
|
||||||
|
# ── Stage 1: builder — bun, install + build the SPA and the MCP packages ─────
|
||||||
|
FROM oven/bun:1 AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Lockfile + manifests first for layer caching.
|
||||||
|
COPY package.json bun.lock bunfig.toml ./
|
||||||
|
COPY packages/scene-graph/package.json packages/scene-graph/
|
||||||
|
COPY packages/pen/package.json packages/pen/
|
||||||
|
COPY packages/kiwi/package.json packages/kiwi/
|
||||||
|
COPY packages/fig/package.json packages/fig/
|
||||||
|
COPY packages/core/package.json packages/core/
|
||||||
|
COPY packages/dom-css/package.json packages/dom-css/
|
||||||
|
COPY packages/vue/package.json packages/vue/
|
||||||
|
COPY packages/cli/package.json packages/cli/
|
||||||
|
COPY packages/mcp/package.json packages/mcp/
|
||||||
|
COPY packages/harness/package.json packages/harness/
|
||||||
|
COPY packages/docs/package.json packages/docs/
|
||||||
|
COPY tools/docs/package.json tools/docs/
|
||||||
|
|
||||||
|
RUN bun install --frozen-lockfile
|
||||||
|
|
||||||
|
# Full source (excludes heavy local build outputs via .dockerignore).
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Same pipeline as the Cloudflare Pages deploy (app.yml): build:packages →
|
||||||
|
# lint → vite build. Lint runs on the vendored fork (includes our theme patch).
|
||||||
|
RUN bun run build
|
||||||
|
|
||||||
|
# Prune to production dependencies so the runtime layer is lean. bun keeps
|
||||||
|
# workspace symlinks; the MCP server's deps (hono, @modelcontextprotocol/sdk,
|
||||||
|
# ws, zod + @open-pencil/core) are all in the prod graph.
|
||||||
|
RUN bun install --production --frozen-lockfile
|
||||||
|
|
||||||
|
# ── Stage 2: runtime — static SPA (:3100) + MCP (:7600) ─────────────────────
|
||||||
|
FROM oven/bun:1-slim AS runtime
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=builder /app/dist ./dist
|
||||||
|
COPY --from=builder /app/package.json ./package.json
|
||||||
|
COPY --from=builder /app/bun.lock ./bun.lock
|
||||||
|
COPY --from=builder /app/bunfig.toml ./bunfig.toml
|
||||||
|
COPY --from=builder /app/node_modules ./node_modules
|
||||||
|
COPY --from=builder /app/packages ./packages
|
||||||
|
COPY --from=builder /app/docker/static-server.mjs ./static-server.mjs
|
||||||
|
COPY --from=builder /app/docker/entrypoint.sh /entrypoint.sh
|
||||||
|
|
||||||
|
# Writable document root for MCP file-scoped tools (open_file / new_document /
|
||||||
|
# save_file). Keep on a volume so documents survive container restarts.
|
||||||
|
ENV OPENPENCIL_MCP_ROOT=/data
|
||||||
|
|
||||||
|
EXPOSE 3100 7600
|
||||||
|
|
||||||
|
ENTRYPOINT ["/entrypoint.sh"]
|
||||||
27
Dockerfile.web-vue.dockerignore
Normal file
27
Dockerfile.web-vue.dockerignore
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
# Per-Dockerfile ignore (BuildKit picks up `<Dockerfile>.dockerignore` in
|
||||||
|
# preference to the root `.dockerignore` for THIS Dockerfile only).
|
||||||
|
#
|
||||||
|
# Keep the build context small: the builder stage runs a clean `bun install`
|
||||||
|
# and `bun run build` inside the image, so none of the host's build outputs
|
||||||
|
# are needed.
|
||||||
|
|
||||||
|
.git
|
||||||
|
.github
|
||||||
|
.vscode
|
||||||
|
|
||||||
|
# JS build outputs — rebuilt inside the image, never copied in.
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.tsbuildinfo
|
||||||
|
coverage
|
||||||
|
|
||||||
|
# Desktop + test artifacts.
|
||||||
|
desktop/target
|
||||||
|
tests/results
|
||||||
|
playwright-report
|
||||||
|
|
||||||
|
# Local scratch / docs.
|
||||||
|
.openpencil-tmp
|
||||||
|
screenshot
|
||||||
|
*.md
|
||||||
60
docker/entrypoint.sh
Executable file
60
docker/entrypoint.sh
Executable file
|
|
@ -0,0 +1,60 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# Entrypoint for the OpenPencil Vue container: runs the static SPA server on
|
||||||
|
# :3100 and the @open-pencil/mcp Streamable-HTTP server on :7600 in the same
|
||||||
|
# container. Forwards SIGTERM/SIGINT to both so docker stop is clean.
|
||||||
|
# POSIX sh only (no `wait -n`) — the runtime image is Alpine/dash.
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
WEB_PORT="${OPENPENCIL_WEB_PORT:-3100}"
|
||||||
|
MCP_PORT="${OPENPENCIL_MCP_PORT:-7600}"
|
||||||
|
|
||||||
|
# The MCP server creates a Unix socket + discovery file by default. In the
|
||||||
|
# container we direct them under /data (writable volume) so the socket never
|
||||||
|
# collides with /run/user/... and documents land on the persisted volume.
|
||||||
|
export OPENPENCIL_MCP_ROOT="${OPENPENCIL_MCP_ROOT:-/data}"
|
||||||
|
export OPENPENCIL_MCP_SOCKET="${OPENPENCIL_MCP_SOCKET:-/data/mcp.sock}"
|
||||||
|
export OPENPENCIL_MCP_DISCOVERY_PATH="${OPENPENCIL_MCP_DISCOVERY_PATH:-/data/mcp.json}"
|
||||||
|
export OPENPENCIL_MCP_CORS_ORIGIN="${OPENPENCIL_MCP_CORS_ORIGIN:-}"
|
||||||
|
# Bind MCP on all interfaces so sibling containers (chatapi) can reach it by
|
||||||
|
# service name (openpencil:7600), not just via localhost port-forwarding.
|
||||||
|
export OPENPENCIL_MCP_HOST="${OPENPENCIL_MCP_HOST:-0.0.0.0}"
|
||||||
|
|
||||||
|
mkdir -p "$OPENPENCIL_MCP_ROOT" "$(dirname "$OPENPENCIL_MCP_SOCKET")" "$(dirname "$OPENPENCIL_MCP_DISCOVERY_PATH")"
|
||||||
|
|
||||||
|
echo "[openpencil] starting static SPA on :${WEB_PORT} and MCP on :${MCP_PORT}"
|
||||||
|
|
||||||
|
# Static SPA.
|
||||||
|
OPENPENCIL_WEB_PORT="$WEB_PORT" bun static-server.mjs &
|
||||||
|
WEB_PID=$!
|
||||||
|
|
||||||
|
# MCP server (Streamable HTTP at /mcp). Bun runs the node-targeted bin.
|
||||||
|
PORT="$MCP_PORT" bun packages/mcp/bin/openpencil-mcp-http.js &
|
||||||
|
MCP_PID=$!
|
||||||
|
|
||||||
|
shutdown() {
|
||||||
|
echo "[openpencil] shutting down (MCP=$MCP_PID, web=$WEB_PID) ..."
|
||||||
|
kill -TERM "$MCP_PID" "$WEB_PID" 2>/dev/null || true
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
trap shutdown TERM INT
|
||||||
|
|
||||||
|
# Wait for both processes; stop the container when either exits.
|
||||||
|
STATUS=0
|
||||||
|
while :; do
|
||||||
|
WEB_ALIVE=0; MCP_ALIVE=0
|
||||||
|
kill -0 "$WEB_PID" 2>/dev/null && WEB_ALIVE=1
|
||||||
|
kill -0 "$MCP_PID" 2>/dev/null && MCP_ALIVE=1
|
||||||
|
if [ "$WEB_ALIVE" = "0" ] || [ "$MCP_ALIVE" = "0" ]; then
|
||||||
|
STATUS=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
# Poll every second; a SIGTERM/SIGINT wakes the loop via the trap's exit.
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "[openpencil] a child process exited; stopping the container"
|
||||||
|
kill -TERM "$MCP_PID" "$WEB_PID" 2>/dev/null || true
|
||||||
|
wait "$MCP_PID" 2>/dev/null || true
|
||||||
|
wait "$WEB_PID" 2>/dev/null || true
|
||||||
|
exit "$STATUS"
|
||||||
135
docker/static-server.mjs
Normal file
135
docker/static-server.mjs
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
// Minimal static SPA server for the OpenPencil Vue editor (dist/).
|
||||||
|
//
|
||||||
|
// Serves the built SPA from OPENPENCIL_WEB_ROOT (default ./dist) on
|
||||||
|
// OPENPENCIL_WEB_PORT (default 3100). Implements the SPA fallback that
|
||||||
|
// Cloudflare Pages provides via dist/_redirects (`/* /index.html 200`), plus
|
||||||
|
// correct MIME types — notably application/wasm for CanvasKit.
|
||||||
|
//
|
||||||
|
// Runs under bun in the container: `bun static-server.mjs`.
|
||||||
|
|
||||||
|
import { createServer } from 'node:http'
|
||||||
|
import { readFile, stat } from 'node:fs/promises'
|
||||||
|
import { extname, join, normalize } from 'node:path'
|
||||||
|
|
||||||
|
const ROOT = process.env.OPENPENCIL_WEB_ROOT ?? join(import.meta.dir, 'dist')
|
||||||
|
const PORT = Number(process.env.OPENPENCIL_WEB_PORT ?? 3100)
|
||||||
|
const HOST = process.env.OPENPENCIL_WEB_HOST ?? '0.0.0.0'
|
||||||
|
|
||||||
|
// W4C fork delta: same-origin proxy for the CORS-hostile online-font catalog
|
||||||
|
// (https://fonts.google.com/metadata/fonts). The browser app (which is a W4C
|
||||||
|
// fork) rewrites those requests to /__font-proxy?url=... so they stay
|
||||||
|
// same-origin; we forward them upstream here. Mirrors vite/font-proxy.ts.
|
||||||
|
const FONT_PROXY_PREFIX = '/__font-proxy'
|
||||||
|
const PROXIED_FONT_HOSTS = ['fonts.google.com']
|
||||||
|
const FONT_PROXY_USER_AGENT =
|
||||||
|
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126 Safari/537.36'
|
||||||
|
|
||||||
|
async function handleFontProxy(req, res, hostHeader) {
|
||||||
|
const target = new URL(req.url ?? '/', `http://${hostHeader}`).searchParams.get('url')
|
||||||
|
if (!target) {
|
||||||
|
res.writeHead(400)
|
||||||
|
res.end('missing url')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let targetUrl
|
||||||
|
try {
|
||||||
|
targetUrl = new URL(decodeURIComponent(target))
|
||||||
|
} catch {
|
||||||
|
res.writeHead(400)
|
||||||
|
res.end('invalid url')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (targetUrl.protocol !== 'https:' || !PROXIED_FONT_HOSTS.includes(targetUrl.host)) {
|
||||||
|
res.writeHead(403)
|
||||||
|
res.end('proxied host denied')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const upstream = await fetch(targetUrl, {
|
||||||
|
headers: {
|
||||||
|
'user-agent': FONT_PROXY_USER_AGENT,
|
||||||
|
accept: 'application/json, text/plain, */*'
|
||||||
|
},
|
||||||
|
redirect: 'follow'
|
||||||
|
})
|
||||||
|
const body = Buffer.from(await upstream.arrayBuffer())
|
||||||
|
const type = upstream.headers.get('content-type')
|
||||||
|
res.writeHead(upstream.status, {
|
||||||
|
'content-type': type ?? 'application/octet-stream',
|
||||||
|
'cache-control': upstream.status === 200 ? 'public, max-age=86400' : 'no-store',
|
||||||
|
'access-control-allow-origin': '*'
|
||||||
|
})
|
||||||
|
res.end(body)
|
||||||
|
} catch (err) {
|
||||||
|
res.writeHead(502)
|
||||||
|
res.end(String(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const MIME = {
|
||||||
|
'.html': 'text/html; charset=utf-8',
|
||||||
|
'.js': 'text/javascript; charset=utf-8',
|
||||||
|
'.mjs': 'text/javascript; charset=utf-8',
|
||||||
|
'.css': 'text/css; charset=utf-8',
|
||||||
|
'.json': 'application/json; charset=utf-8',
|
||||||
|
'.wasm': 'application/wasm',
|
||||||
|
'.png': 'image/png',
|
||||||
|
'.jpg': 'image/jpeg',
|
||||||
|
'.jpeg': 'image/jpeg',
|
||||||
|
'.svg': 'image/svg+xml',
|
||||||
|
'.webp': 'image/webp',
|
||||||
|
'.ico': 'image/x-icon',
|
||||||
|
'.ttf': 'font/ttf',
|
||||||
|
'.otf': 'font/otf',
|
||||||
|
'.woff': 'font/woff',
|
||||||
|
'.woff2': 'font/woff2',
|
||||||
|
'.txt': 'text/plain; charset=utf-8',
|
||||||
|
'.webmanifest': 'application/manifest+json'
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = createServer(async (req, res) => {
|
||||||
|
try {
|
||||||
|
let pathname
|
||||||
|
try {
|
||||||
|
pathname = decodeURIComponent(new URL(req.url ?? '/', `http://${req.headers.host}`).pathname)
|
||||||
|
} catch {
|
||||||
|
pathname = '/'
|
||||||
|
}
|
||||||
|
if (pathname === FONT_PROXY_PREFIX) {
|
||||||
|
await handleFontProxy(req, res, req.headers.host)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (pathname === '/') pathname = '/index.html'
|
||||||
|
|
||||||
|
// Prevent path traversal outside ROOT.
|
||||||
|
const filePath = normalize(join(ROOT, pathname))
|
||||||
|
if (!filePath.startsWith(ROOT)) {
|
||||||
|
res.writeHead(403)
|
||||||
|
res.end('Forbidden')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let fullPath = filePath
|
||||||
|
try {
|
||||||
|
const s = await stat(fullPath)
|
||||||
|
if (s.isDirectory()) fullPath = join(fullPath, 'index.html')
|
||||||
|
} catch {
|
||||||
|
// 404 → SPA fallback to index.html (vite preview behaviour).
|
||||||
|
fullPath = join(ROOT, 'index.html')
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await readFile(fullPath)
|
||||||
|
res.writeHead(200, {
|
||||||
|
'content-type': MIME[extname(fullPath)] ?? 'application/octet-stream',
|
||||||
|
'cache-control': extname(fullPath) === '.html' ? 'no-cache' : 'public, max-age=31536000, immutable'
|
||||||
|
})
|
||||||
|
res.end(body)
|
||||||
|
} catch (err) {
|
||||||
|
res.writeHead(500)
|
||||||
|
res.end(`Internal Server Error: ${String(err)}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
server.listen(PORT, HOST, () => {
|
||||||
|
process.stderr.write(`Static server: http://${HOST}:${PORT} (root ${ROOT})\n`)
|
||||||
|
})
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import type { FontFaceData, RemoteFontSource, ResolveFontResult } from 'unifont'
|
import type { FontFaceData, RemoteFontSource, ResolveFontResult } from 'unifont'
|
||||||
|
|
||||||
import { IS_BROWSER } from '#core/constants'
|
|
||||||
import { parseFontStyle } from '#core/text/face'
|
import { parseFontStyle } from '#core/text/face'
|
||||||
import {
|
import {
|
||||||
createProviderUnifont,
|
createProviderUnifont,
|
||||||
|
|
@ -124,7 +123,6 @@ export class WebFontResolver {
|
||||||
}
|
}
|
||||||
|
|
||||||
preloadFamilies(): void {
|
preloadFamilies(): void {
|
||||||
if (IS_BROWSER && !this.remoteFetch) return
|
|
||||||
for (const provider of this.enabledProviders()) void this.listFamilies(provider)
|
for (const provider of this.enabledProviders()) void this.listFamilies(provider)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -146,7 +144,7 @@ export class WebFontResolver {
|
||||||
characters = ''
|
characters = ''
|
||||||
): Promise<ResolvedWebFont | null> {
|
): Promise<ResolvedWebFont | null> {
|
||||||
const providers = this.enabledProviders()
|
const providers = this.enabledProviders()
|
||||||
if (providers.length === 0 || (IS_BROWSER && !this.remoteFetch)) return null
|
if (providers.length === 0) return null
|
||||||
|
|
||||||
for (const family of families) {
|
for (const family of families) {
|
||||||
for (const provider of providers) {
|
for (const provider of providers) {
|
||||||
|
|
@ -202,7 +200,7 @@ export class WebFontResolver {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadFamilies(provider: WebFontProviderId): Promise<string[]> {
|
private async loadFamilies(provider: WebFontProviderId): Promise<string[]> {
|
||||||
if (typeof fetch === 'undefined' || (IS_BROWSER && !this.remoteFetch)) return []
|
if (typeof fetch === 'undefined') return []
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const unifont = await this.unifont(provider)
|
const unifont = await this.unifont(provider)
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
||||||
` platform path. Parent dir created 0o700. Mainly for test isolation.\n` +
|
` platform path. Parent dir created 0o700. Mainly for test isolation.\n` +
|
||||||
` OPENPENCIL_MCP_TCP Deprecated — TCP is controlled by PORT (>0 = on, 0 = off)\n` +
|
` OPENPENCIL_MCP_TCP Deprecated — TCP is controlled by PORT (>0 = on, 0 = off)\n` +
|
||||||
` OPENPENCIL_MCP_AUTH_TOKEN Bearer token for MCP and RPC auth\n` +
|
` OPENPENCIL_MCP_AUTH_TOKEN Bearer token for MCP and RPC auth\n` +
|
||||||
|
` OPENPENCIL_MCP_HOST TCP bind host (default: 127.0.0.1; set 0.0.0.0 in containers)\n` +
|
||||||
` OPENPENCIL_MCP_ROOT Allowed directory for file-scoped tools (default: current working directory)\n` +
|
` OPENPENCIL_MCP_ROOT Allowed directory for file-scoped tools (default: current working directory)\n` +
|
||||||
` OPENPENCIL_MCP_EVAL Set to 1 to enable the eval tool\n` +
|
` OPENPENCIL_MCP_EVAL Set to 1 to enable the eval tool\n` +
|
||||||
` OPENPENCIL_MCP_CORS_ORIGIN Allowed CORS origin\n` +
|
` OPENPENCIL_MCP_CORS_ORIGIN Allowed CORS origin\n` +
|
||||||
|
|
@ -69,6 +70,7 @@ const handle = await startServer({
|
||||||
httpPort: withTcp ? port : 0,
|
httpPort: withTcp ? port : 0,
|
||||||
withTcp,
|
withTcp,
|
||||||
socketPath: process.env.OPENPENCIL_MCP_SOCKET?.trim() || null,
|
socketPath: process.env.OPENPENCIL_MCP_SOCKET?.trim() || null,
|
||||||
|
tcpHost: process.env.OPENPENCIL_MCP_HOST?.trim() || '127.0.0.1',
|
||||||
enableEval: process.env.OPENPENCIL_MCP_EVAL === '1',
|
enableEval: process.env.OPENPENCIL_MCP_EVAL === '1',
|
||||||
mcpRoot: process.env.OPENPENCIL_MCP_ROOT?.trim() || process.cwd(),
|
mcpRoot: process.env.OPENPENCIL_MCP_ROOT?.trim() || process.cwd(),
|
||||||
// Auth token: undefined → auto-generate, empty string → disable auth,
|
// Auth token: undefined → auto-generate, empty string → disable auth,
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,8 @@ export interface ServerOptions {
|
||||||
socketPath?: string | null
|
socketPath?: string | null
|
||||||
/** Whether to also listen on TCP (in addition to the socket). API default is `false`; the CLI passes `true` by default (derived from PORT, default 7600). */
|
/** Whether to also listen on TCP (in addition to the socket). API default is `false`; the CLI passes `true` by default (derived from PORT, default 7600). */
|
||||||
withTcp?: boolean
|
withTcp?: boolean
|
||||||
|
/** Host to bind the TCP listener to. Defaults to 127.0.0.1 (loopback only). Set to 0.0.0.0 in containers so other services can reach the MCP endpoint by name. */
|
||||||
|
tcpHost?: string
|
||||||
enableEval?: boolean
|
enableEval?: boolean
|
||||||
mcpRoot?: string | null
|
mcpRoot?: string | null
|
||||||
/** Auth token for /mcp and /rpc endpoints. Auto-generated (32-hex) when omitted. Pass null explicitly to disable auth. */
|
/** Auth token for /mcp and /rpc endpoints. Auto-generated (32-hex) when omitted. Pass null explicitly to disable auth. */
|
||||||
|
|
@ -413,7 +415,9 @@ export async function startServer(options: ServerOptions = {}): Promise<ServerHa
|
||||||
let startedAt = ''
|
let startedAt = ''
|
||||||
try {
|
try {
|
||||||
state.socketResult = await startSocketListener(ctx.app, ctx.wss, options.socketPath ?? null)
|
state.socketResult = await startSocketListener(ctx.app, ctx.wss, options.socketPath ?? null)
|
||||||
state.tcpResult = ctx.withTcp ? await tryStartTcp(ctx.app, ctx.wss, ctx.httpPort, state) : null
|
state.tcpResult = ctx.withTcp
|
||||||
|
? await tryStartTcp(ctx.app, ctx.wss, ctx.httpPort, state, options.tcpHost ?? '127.0.0.1')
|
||||||
|
: null
|
||||||
const resolvedSocketPath = state.socketResult?.resolvedPath ?? null
|
const resolvedSocketPath = state.socketResult?.resolvedPath ?? null
|
||||||
const actualHttpPort = state.tcpResult?.port ?? 0
|
const actualHttpPort = state.tcpResult?.port ?? 0
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -135,9 +135,10 @@ export async function startSocketListener(
|
||||||
export async function startTcpListener(
|
export async function startTcpListener(
|
||||||
app: Hono,
|
app: Hono,
|
||||||
wss: WebSocketServer,
|
wss: WebSocketServer,
|
||||||
httpPort: number
|
httpPort: number,
|
||||||
|
tcpHost = '127.0.0.1'
|
||||||
): Promise<{ server: HttpServer; port: number } | null> {
|
): Promise<{ server: HttpServer; port: number } | null> {
|
||||||
const host = '127.0.0.1'
|
const host = tcpHost
|
||||||
const server = createAppServer(app)
|
const server = createAppServer(app)
|
||||||
wireUpgrade(server, wss)
|
wireUpgrade(server, wss)
|
||||||
|
|
||||||
|
|
@ -350,10 +351,11 @@ export async function tryStartTcp(
|
||||||
app: Hono,
|
app: Hono,
|
||||||
wss: WebSocketServer,
|
wss: WebSocketServer,
|
||||||
httpPort: number,
|
httpPort: number,
|
||||||
state: ListenerState
|
state: ListenerState,
|
||||||
|
tcpHost: string
|
||||||
): Promise<{ server: HttpServer; port: number } | null> {
|
): Promise<{ server: HttpServer; port: number } | null> {
|
||||||
try {
|
try {
|
||||||
return await startTcpListener(app, wss, httpPort)
|
return await startTcpListener(app, wss, httpPort, tcpHost)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// If TCP listener fails after the socket listener succeeded, close the
|
// If TCP listener fails after the socket listener succeeded, close the
|
||||||
// socket listener and clean up the socket file to avoid a leak.
|
// socket listener and clean up the socket file to avoid a leak.
|
||||||
|
|
|
||||||
|
|
@ -235,7 +235,7 @@
|
||||||
"requesting": "Wird angefragt…",
|
"requesting": "Wird angefragt…",
|
||||||
"onlineFontProviders": "Online-Schriftanbieter",
|
"onlineFontProviders": "Online-Schriftanbieter",
|
||||||
"downloadMissingWebFonts": "Fehlende Web-Schriften über aktivierte Anbieter herunterladen.",
|
"downloadMissingWebFonts": "Fehlende Web-Schriften über aktivierte Anbieter herunterladen.",
|
||||||
"webFontProvidersRequireDesktopApp": "Online-Schriftanbieter-Kataloge sind in der Web-App nicht verfügbar. Lade die Desktop-App herunter, um Anbieter-Schriften zu durchsuchen und zu laden.",
|
"webFontLoadFailed": "Fehler beim Laden einer Schrift von einem Online-Anbieter. Prüfen Sie Ihre Verbindung und versuchen Sie es erneut.",
|
||||||
"clipboardImageUnavailableWeb": "Das eingefügte Design enthält 1 Bild, das in der Web-App nicht geladen werden kann. Verwende die Desktop-App, um es einzuschließen.",
|
"clipboardImageUnavailableWeb": "Das eingefügte Design enthält 1 Bild, das in der Web-App nicht geladen werden kann. Verwende die Desktop-App, um es einzuschließen.",
|
||||||
"clipboardImagesUnavailableWeb": "Das eingefügte Design enthält {count} Bilder, die in der Web-App nicht geladen werden können. Verwende die Desktop-App, um sie einzuschließen.",
|
"clipboardImagesUnavailableWeb": "Das eingefügte Design enthält {count} Bilder, die in der Web-App nicht geladen werden können. Verwende die Desktop-App, um sie einzuschließen.",
|
||||||
"clipboardImageFetchFailed": "1 Bild konnte nicht aus Figma abgerufen werden. Prüfe, ob die Quelldatei zugänglich ist, und versuche es erneut.",
|
"clipboardImageFetchFailed": "1 Bild konnte nicht aus Figma abgerufen werden. Prüfe, ob die Quelldatei zugänglich ist, und versuche es erneut.",
|
||||||
|
|
|
||||||
|
|
@ -235,7 +235,7 @@
|
||||||
"requesting": "Solicitando…",
|
"requesting": "Solicitando…",
|
||||||
"onlineFontProviders": "Proveedores de fuentes en línea",
|
"onlineFontProviders": "Proveedores de fuentes en línea",
|
||||||
"downloadMissingWebFonts": "Descarga fuentes web faltantes mediante los proveedores activados.",
|
"downloadMissingWebFonts": "Descarga fuentes web faltantes mediante los proveedores activados.",
|
||||||
"webFontProvidersRequireDesktopApp": "Los catálogos de proveedores de fuentes en línea no están disponibles en la app web. Descarga la app de escritorio para explorar y cargar fuentes de proveedores.",
|
"webFontLoadFailed": "No se pudo cargar una fuente de un proveedor en línea. Comprueba tu conexión e inténtalo de nuevo.",
|
||||||
"clipboardImageUnavailableWeb": "El diseño pegado contiene 1 imagen que no se puede cargar en la aplicación web. Usa la aplicación de escritorio para incluirla.",
|
"clipboardImageUnavailableWeb": "El diseño pegado contiene 1 imagen que no se puede cargar en la aplicación web. Usa la aplicación de escritorio para incluirla.",
|
||||||
"clipboardImagesUnavailableWeb": "El diseño pegado contiene {count} imágenes que no se pueden cargar en la aplicación web. Usa la aplicación de escritorio para incluirlas.",
|
"clipboardImagesUnavailableWeb": "El diseño pegado contiene {count} imágenes que no se pueden cargar en la aplicación web. Usa la aplicación de escritorio para incluirlas.",
|
||||||
"clipboardImageFetchFailed": "No se ha podido obtener 1 imagen de Figma. Comprueba que el archivo de origen sea accesible e inténtalo de nuevo.",
|
"clipboardImageFetchFailed": "No se ha podido obtener 1 imagen de Figma. Comprueba que el archivo de origen sea accesible e inténtalo de nuevo.",
|
||||||
|
|
|
||||||
|
|
@ -235,7 +235,7 @@
|
||||||
"requesting": "Demande…",
|
"requesting": "Demande…",
|
||||||
"onlineFontProviders": "Fournisseurs de polices en ligne",
|
"onlineFontProviders": "Fournisseurs de polices en ligne",
|
||||||
"downloadMissingWebFonts": "Télécharger les polices web manquantes via les fournisseurs activés.",
|
"downloadMissingWebFonts": "Télécharger les polices web manquantes via les fournisseurs activés.",
|
||||||
"webFontProvidersRequireDesktopApp": "Les catalogues de fournisseurs de polices en ligne ne sont pas disponibles dans l’app web. Téléchargez l’app de bureau pour parcourir et charger les polices des fournisseurs.",
|
"webFontLoadFailed": "Échec du chargement d'une police auprès d'un fournisseur en ligne. Vérifiez votre connexion et réessayez.",
|
||||||
"clipboardImageUnavailableWeb": "Le design collé contient 1 image qui ne peut pas être chargée dans l’application web. Utilisez l’application de bureau pour l’inclure.",
|
"clipboardImageUnavailableWeb": "Le design collé contient 1 image qui ne peut pas être chargée dans l’application web. Utilisez l’application de bureau pour l’inclure.",
|
||||||
"clipboardImagesUnavailableWeb": "Le design collé contient {count} images qui ne peuvent pas être chargées dans l’application web. Utilisez l’application de bureau pour les inclure.",
|
"clipboardImagesUnavailableWeb": "Le design collé contient {count} images qui ne peuvent pas être chargées dans l’application web. Utilisez l’application de bureau pour les inclure.",
|
||||||
"clipboardImageFetchFailed": "Impossible de récupérer 1 image depuis Figma. Vérifiez que le fichier source est accessible, puis réessayez.",
|
"clipboardImageFetchFailed": "Impossible de récupérer 1 image depuis Figma. Vérifiez que le fichier source est accessible, puis réessayez.",
|
||||||
|
|
|
||||||
|
|
@ -235,7 +235,7 @@
|
||||||
"requesting": "Richiesta…",
|
"requesting": "Richiesta…",
|
||||||
"onlineFontProviders": "Provider di font online",
|
"onlineFontProviders": "Provider di font online",
|
||||||
"downloadMissingWebFonts": "Scarica i font web mancanti tramite i provider abilitati.",
|
"downloadMissingWebFonts": "Scarica i font web mancanti tramite i provider abilitati.",
|
||||||
"webFontProvidersRequireDesktopApp": "I cataloghi dei provider di font online non sono disponibili nell’app web. Scarica l’app desktop per sfogliare e caricare i font dei provider.",
|
"webFontLoadFailed": "Impossibile caricare un font da un provider online. Controlla la connessione e riprova.",
|
||||||
"clipboardImageUnavailableWeb": "Il design incollato contiene 1 immagine che non può essere caricata nell’app web. Usa l’app desktop per includerla.",
|
"clipboardImageUnavailableWeb": "Il design incollato contiene 1 immagine che non può essere caricata nell’app web. Usa l’app desktop per includerla.",
|
||||||
"clipboardImagesUnavailableWeb": "Il design incollato contiene {count} immagini che non possono essere caricate nell’app web. Usa l’app desktop per includerle.",
|
"clipboardImagesUnavailableWeb": "Il design incollato contiene {count} immagini che non possono essere caricate nell’app web. Usa l’app desktop per includerle.",
|
||||||
"clipboardImageFetchFailed": "Impossibile recuperare 1 immagine da Figma. Controlla che il file sorgente sia accessibile e riprova.",
|
"clipboardImageFetchFailed": "Impossibile recuperare 1 immagine da Figma. Controlla che il file sorgente sia accessibile e riprova.",
|
||||||
|
|
|
||||||
|
|
@ -235,7 +235,7 @@
|
||||||
"requesting": "要求中…",
|
"requesting": "要求中…",
|
||||||
"onlineFontProviders": "オンラインフォントプロバイダー",
|
"onlineFontProviders": "オンラインフォントプロバイダー",
|
||||||
"downloadMissingWebFonts": "有効なプロバイダーから不足しているWebフォントをダウンロードします。",
|
"downloadMissingWebFonts": "有効なプロバイダーから不足しているWebフォントをダウンロードします。",
|
||||||
"webFontProvidersRequireDesktopApp": "オンラインフォントプロバイダーのカタログはWebアプリでは利用できません。プロバイダーのフォントを参照して読み込むにはデスクトップアプリをダウンロードしてください。",
|
"webFontLoadFailed": "オンラインプロバイダーからフォントを読み込めませんでした。接続を確認してもう一度お試しください。",
|
||||||
"clipboardImageUnavailableWeb": "貼り付けたデザインには、Webアプリで読み込めない画像が1件含まれています。その画像を含めるにはデスクトップアプリを使用してください。",
|
"clipboardImageUnavailableWeb": "貼り付けたデザインには、Webアプリで読み込めない画像が1件含まれています。その画像を含めるにはデスクトップアプリを使用してください。",
|
||||||
"clipboardImagesUnavailableWeb": "貼り付けたデザインには、Webアプリで読み込めない画像が{count}件含まれています。それらの画像を含めるにはデスクトップアプリを使用してください。",
|
"clipboardImagesUnavailableWeb": "貼り付けたデザインには、Webアプリで読み込めない画像が{count}件含まれています。それらの画像を含めるにはデスクトップアプリを使用してください。",
|
||||||
"clipboardImageFetchFailed": "Figmaから画像1件を取得できませんでした。元のファイルにアクセスできることを確認して、もう一度お試しください。",
|
"clipboardImageFetchFailed": "Figmaから画像1件を取得できませんでした。元のファイルにアクセスできることを確認して、もう一度お試しください。",
|
||||||
|
|
|
||||||
|
|
@ -235,7 +235,7 @@
|
||||||
"requesting": "Żądanie…",
|
"requesting": "Żądanie…",
|
||||||
"onlineFontProviders": "Dostawcy czcionek online",
|
"onlineFontProviders": "Dostawcy czcionek online",
|
||||||
"downloadMissingWebFonts": "Pobieraj brakujące czcionki webowe przez włączonych dostawców.",
|
"downloadMissingWebFonts": "Pobieraj brakujące czcionki webowe przez włączonych dostawców.",
|
||||||
"webFontProvidersRequireDesktopApp": "Katalogi dostawców czcionek online nie są dostępne w aplikacji webowej. Pobierz aplikację desktopową, aby przeglądać i ładować czcionki dostawców.",
|
"webFontLoadFailed": "Nie udało się załadować czcionki od dostawcy online. Sprawdź połączenie i spróbuj ponownie.",
|
||||||
"clipboardImageUnavailableWeb": "Wklejony projekt zawiera 1 obraz, którego nie można wczytać w aplikacji internetowej. Użyj aplikacji komputerowej, aby go dołączyć.",
|
"clipboardImageUnavailableWeb": "Wklejony projekt zawiera 1 obraz, którego nie można wczytać w aplikacji internetowej. Użyj aplikacji komputerowej, aby go dołączyć.",
|
||||||
"clipboardImagesUnavailableWeb": "Wklejony projekt zawiera {count} obrazy, których nie można wczytać w aplikacji internetowej. Użyj aplikacji komputerowej, aby je dołączyć.",
|
"clipboardImagesUnavailableWeb": "Wklejony projekt zawiera {count} obrazy, których nie można wczytać w aplikacji internetowej. Użyj aplikacji komputerowej, aby je dołączyć.",
|
||||||
"clipboardImageFetchFailed": "Nie udało się pobrać 1 obrazu z Figmy. Sprawdź, czy plik źródłowy jest dostępny, i spróbuj ponownie.",
|
"clipboardImageFetchFailed": "Nie udało się pobrać 1 obrazu z Figmy. Sprawdź, czy plik źródłowy jest dostępny, i spróbuj ponownie.",
|
||||||
|
|
|
||||||
|
|
@ -235,7 +235,7 @@
|
||||||
"requesting": "Запрос…",
|
"requesting": "Запрос…",
|
||||||
"onlineFontProviders": "Онлайн-провайдеры шрифтов",
|
"onlineFontProviders": "Онлайн-провайдеры шрифтов",
|
||||||
"downloadMissingWebFonts": "Загружайте отсутствующие веб-шрифты через включённых провайдеров.",
|
"downloadMissingWebFonts": "Загружайте отсутствующие веб-шрифты через включённых провайдеров.",
|
||||||
"webFontProvidersRequireDesktopApp": "Каталоги провайдеров онлайн-шрифтов недоступны в веб-приложении. Скачайте настольное приложение, чтобы просматривать и загружать шрифты провайдеров.",
|
"webFontLoadFailed": "Не удалось загрузить шрифт с онлайн-провайдера. Проверьте подключение к интернету.",
|
||||||
"clipboardImageUnavailableWeb": "Вставленный дизайн содержит 1 изображение, которое нельзя загрузить в веб-приложении. Используйте настольное приложение, чтобы включить его.",
|
"clipboardImageUnavailableWeb": "Вставленный дизайн содержит 1 изображение, которое нельзя загрузить в веб-приложении. Используйте настольное приложение, чтобы включить его.",
|
||||||
"clipboardImagesUnavailableWeb": "Вставленный дизайн содержит {count} изображений, которые нельзя загрузить в веб-приложении. Используйте настольное приложение, чтобы включить их.",
|
"clipboardImagesUnavailableWeb": "Вставленный дизайн содержит {count} изображений, которые нельзя загрузить в веб-приложении. Используйте настольное приложение, чтобы включить их.",
|
||||||
"clipboardImageFetchFailed": "Не удалось получить 1 изображение из Figma. Проверьте доступность исходного файла и повторите попытку.",
|
"clipboardImageFetchFailed": "Не удалось получить 1 изображение из Figma. Проверьте доступность исходного файла и повторите попытку.",
|
||||||
|
|
|
||||||
|
|
@ -235,7 +235,7 @@
|
||||||
"requesting": "正在请求…",
|
"requesting": "正在请求…",
|
||||||
"onlineFontProviders": "在线字体提供商",
|
"onlineFontProviders": "在线字体提供商",
|
||||||
"downloadMissingWebFonts": "通过已启用的提供商下载缺失的网页字体。",
|
"downloadMissingWebFonts": "通过已启用的提供商下载缺失的网页字体。",
|
||||||
"webFontProvidersRequireDesktopApp": "网页版暂不支持在线字体提供商目录。请下载桌面应用来浏览和加载提供商字体。",
|
"webFontLoadFailed": "无法从在线提供商加载字体。请检查网络连接后重试。",
|
||||||
"clipboardImageUnavailableWeb": "粘贴的设计包含 1 张无法在网页应用中加载的图片。请使用桌面应用以包含该图片。",
|
"clipboardImageUnavailableWeb": "粘贴的设计包含 1 张无法在网页应用中加载的图片。请使用桌面应用以包含该图片。",
|
||||||
"clipboardImagesUnavailableWeb": "粘贴的设计包含 {count} 张无法在网页应用中加载的图片。请使用桌面应用以包含这些图片。",
|
"clipboardImagesUnavailableWeb": "粘贴的设计包含 {count} 张无法在网页应用中加载的图片。请使用桌面应用以包含这些图片。",
|
||||||
"clipboardImageFetchFailed": "无法从 Figma 获取 1 张图片。请检查源文件是否可访问,然后重试。",
|
"clipboardImageFetchFailed": "无法从 Figma 获取 1 张图片。请检查源文件是否可访问,然后重试。",
|
||||||
|
|
|
||||||
|
|
@ -94,8 +94,8 @@ export const dialogMessageDefaults = {
|
||||||
requesting: 'Requesting…',
|
requesting: 'Requesting…',
|
||||||
onlineFontProviders: 'Online font providers',
|
onlineFontProviders: 'Online font providers',
|
||||||
downloadMissingWebFonts: 'Download missing web fonts through enabled providers.',
|
downloadMissingWebFonts: 'Download missing web fonts through enabled providers.',
|
||||||
webFontProvidersRequireDesktopApp:
|
webFontLoadFailed:
|
||||||
'Online font provider catalogs are unavailable in the web app. Download the desktop app to browse and load provider fonts.',
|
'Failed to load a font from an online provider. Check your connection and try again.',
|
||||||
clipboardImageUnavailableWeb:
|
clipboardImageUnavailableWeb:
|
||||||
'Pasted design includes 1 image that cannot be loaded in the web app. Use the desktop app to include it.',
|
'Pasted design includes 1 image that cannot be loaded in the web app. Use the desktop app to include it.',
|
||||||
clipboardImagesUnavailableWeb: params(
|
clipboardImagesUnavailableWeb: params(
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ function showWebFontUnavailableToast(): void {
|
||||||
if (webFontUnavailableToastShown || isTauri() || !onlineFontsEnabled.value) return
|
if (webFontUnavailableToastShown || isTauri() || !onlineFontsEnabled.value) return
|
||||||
if (!WEB_FONT_PROVIDER_IDS.some((provider) => fontProviderSettings.value[provider])) return
|
if (!WEB_FONT_PROVIDER_IDS.some((provider) => fontProviderSettings.value[provider])) return
|
||||||
webFontUnavailableToastShown = true
|
webFontUnavailableToastShown = true
|
||||||
toast.warning(dialogMessages.get().webFontProvidersRequireDesktopApp)
|
toast.warning(dialogMessages.get().webFontLoadFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
function configureTauriFontCache() {
|
function configureTauriFontCache() {
|
||||||
|
|
@ -71,7 +71,48 @@ function configureTauriFontCache() {
|
||||||
fontManager.setHostFontLoader(loadSystemFont)
|
fontManager.setHostFontLoader(loadSystemFont)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// W4C fork delta: relay CORS-hostile online-font catalog requests through a
|
||||||
|
// same-origin endpoint so the browser app can build the Google Fonts catalog.
|
||||||
|
// unifont's `google` provider reads `https://fonts.google.com/metadata/fonts`,
|
||||||
|
// which never sends `Access-Control-Allow-Origin` and sets `cross-origin-resource-policy:
|
||||||
|
// same-site`, so it cannot be read from any non-Google origin. The browser here
|
||||||
|
// rewrites those to `/__font-proxy?url=...` (served by a Vite dev plugin and by
|
||||||
|
// docker/static-server.mjs), which keeps the request same-origin while the server
|
||||||
|
// forwards it upstream. Glyph files (fonts.gstatic.com) already send CORS headers
|
||||||
|
// and are fetched directly.
|
||||||
|
const FONT_PROXY_PREFIX = '/__font-proxy'
|
||||||
|
const PROXIED_FONT_HOSTS = new Set(['fonts.google.com'])
|
||||||
|
|
||||||
|
// Capture the genuine native fetch once, at module load. `withFetchProxy` in
|
||||||
|
// the core font resolver temporarily swaps `globalThis.fetch` for a wrapper
|
||||||
|
// that routes every http(s) request through this fetcher, so calling the
|
||||||
|
// global `fetch` here would recurse into ourselves (stack overflow). The
|
||||||
|
// captured reference bypasses that wrapper.
|
||||||
|
const nativeFetch = globalThis.fetch
|
||||||
|
|
||||||
|
function createBrowserFontFetch(): (url: string, init?: RequestInit) => Promise<Response> {
|
||||||
|
return (url, init) => {
|
||||||
|
let host = ''
|
||||||
|
try {
|
||||||
|
host = new URL(url).host
|
||||||
|
} catch {
|
||||||
|
// Leave invalid URLs to native fetch.
|
||||||
|
console.warn(`[fonts] invalid URL skipped for font proxy: ${url}`)
|
||||||
|
}
|
||||||
|
if (PROXIED_FONT_HOSTS.has(host)) {
|
||||||
|
return nativeFetch(`${FONT_PROXY_PREFIX}?url=${encodeURIComponent(url)}`, init)
|
||||||
|
}
|
||||||
|
return nativeFetch(url, init)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function configureBrowserFontProxy() {
|
||||||
|
if (isTauri()) return
|
||||||
|
fontManager.setWebFontFetch(createBrowserFontFetch())
|
||||||
|
}
|
||||||
|
|
||||||
configureTauriFontCache()
|
configureTauriFontCache()
|
||||||
|
configureBrowserFontProxy()
|
||||||
|
|
||||||
interface TauriFontFamily {
|
interface TauriFontFamily {
|
||||||
family: string
|
family: string
|
||||||
|
|
@ -150,7 +191,6 @@ export async function listFamilies(): Promise<FontFamilyOption[]> {
|
||||||
byFamily.set(font.family, { family: font.family, source: 'local' })
|
byFamily.set(font.family, { family: font.family, source: 'local' })
|
||||||
return [...byFamily.values()].sort((a, b) => a.family.localeCompare(b.family))
|
return [...byFamily.values()].sort((a, b) => a.family.localeCompare(b.family))
|
||||||
}
|
}
|
||||||
showWebFontUnavailableToast()
|
|
||||||
return fontManager.listFamilyOptions()
|
return fontManager.listFamilyOptions()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,23 @@ const THEME_STORAGE_KEY = 'open-pencil:theme'
|
||||||
const DEFAULT_THEME: AppTheme = 'dark'
|
const DEFAULT_THEME: AppTheme = 'dark'
|
||||||
|
|
||||||
const theme = useLocalStorage<AppTheme>(THEME_STORAGE_KEY, DEFAULT_THEME)
|
const theme = useLocalStorage<AppTheme>(THEME_STORAGE_KEY, DEFAULT_THEME)
|
||||||
|
|
||||||
|
// ── W4C fork delta ─────────────────────────────────────────────────────────
|
||||||
|
// The w4c-quasar /openpencil page embeds this editor as an iframe and opens it
|
||||||
|
// with `?theme=dark|light` (w4c-quasar/src/config/openpencil.ts). Upstream the
|
||||||
|
// Vue editor persists its theme only in localStorage, so before the app boots
|
||||||
|
// we bridge the URL query parameter into the storage-backed theme ref
|
||||||
|
// (useLocalStorage writes through to localStorage, no direct storage access).
|
||||||
|
// This is our fork change only — it will drift/conflict on an upstream update;
|
||||||
|
// re-apply it when rebasing onto a new openpencil master.
|
||||||
|
if (IS_BROWSER) {
|
||||||
|
const themeParam = new URLSearchParams(window.location.search).get('theme')
|
||||||
|
if (themeParam === 'dark' || themeParam === 'light') {
|
||||||
|
theme.value = themeParam
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ── /W4C fork delta ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const prefersDark = usePreferredDark()
|
const prefersDark = usePreferredDark()
|
||||||
export const resolvedAppTheme = computed<'dark' | 'light'>(() => {
|
export const resolvedAppTheme = computed<'dark' | 'light'>(() => {
|
||||||
if (theme.value === 'auto') return prefersDark.value ? 'dark' : 'light'
|
if (theme.value === 'auto') return prefersDark.value ? 'dark' : 'light'
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import packageJson from './package.json'
|
||||||
import { createOpenPencilAliases } from './vite/aliases'
|
import { createOpenPencilAliases } from './vite/aliases'
|
||||||
import { localAutomationToken, openPencilAutomationPlugin } from './vite/automation'
|
import { localAutomationToken, openPencilAutomationPlugin } from './vite/automation'
|
||||||
import { copyCanvasKitAssetsPlugin } from './vite/canvaskit-assets'
|
import { copyCanvasKitAssetsPlugin } from './vite/canvaskit-assets'
|
||||||
|
import { createDevFontProxyPlugin } from './vite/font-proxy'
|
||||||
import { openPencilPwaPlugin } from './vite/pwa'
|
import { openPencilPwaPlugin } from './vite/pwa'
|
||||||
import { rawMarkdownPlugin } from './vite/raw-markdown'
|
import { rawMarkdownPlugin } from './vite/raw-markdown'
|
||||||
import { createDevServerOptions } from './vite/server'
|
import { createDevServerOptions } from './vite/server'
|
||||||
|
|
@ -33,6 +34,7 @@ export default defineConfig(async ({ command }) => ({
|
||||||
Components({ resolvers: [IconsResolver({ prefix: 'icon' })] }),
|
Components({ resolvers: [IconsResolver({ prefix: 'icon' })] }),
|
||||||
openPencilAutomationPlugin(command, host),
|
openPencilAutomationPlugin(command, host),
|
||||||
vue(),
|
vue(),
|
||||||
|
createDevFontProxyPlugin(),
|
||||||
openPencilPwaPlugin()
|
openPencilPwaPlugin()
|
||||||
],
|
],
|
||||||
clearScreen: false,
|
clearScreen: false,
|
||||||
|
|
|
||||||
83
vite/font-proxy.ts
Normal file
83
vite/font-proxy.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
import type { Plugin } from 'vite'
|
||||||
|
|
||||||
|
// Dev-only same-origin proxy for CORS-hostile online-font catalog endpoints.
|
||||||
|
//
|
||||||
|
// unifont's `google` provider reads `https://fonts.google.com/metadata/fonts` to
|
||||||
|
// build its family catalog. That endpoint never sends `Access-Control-Allow-Origin`
|
||||||
|
// and sets `cross-origin-resource-policy: same-site`, so a browser app served on
|
||||||
|
// its own origin (any origin that is not Google) cannot read it directly — the
|
||||||
|
// provider fails to initialize with "Could not initialize provider `google`".
|
||||||
|
//
|
||||||
|
// The browser relays such requests here instead (see `createFontProxyFetch()` in
|
||||||
|
// src/app/editor/fonts), which keeps the request same-origin and forwards it
|
||||||
|
// server-side to Google. This is a W4C fork delta; upstream openpencil does not
|
||||||
|
// ship it because Tauri/goes through the Rust command that is not CORS-bound.
|
||||||
|
|
||||||
|
export const FONT_PROXY_PREFIX = '/__font-proxy'
|
||||||
|
|
||||||
|
// Only these hosts are CORS-hostile from the browser; everything else
|
||||||
|
// (notably fonts.gstatic.com glyph files) already sends CORS headers and is
|
||||||
|
// fetched directly by the browser.
|
||||||
|
const PROXIED_HOSTS = new Set(['fonts.google.com'])
|
||||||
|
|
||||||
|
export function createDevFontProxyPlugin(): Plugin {
|
||||||
|
return {
|
||||||
|
name: 'open-pencil-dev-font-proxy',
|
||||||
|
configureServer(server) {
|
||||||
|
server.middlewares.use(async (req, res, next) => {
|
||||||
|
let pathname: string
|
||||||
|
try {
|
||||||
|
pathname = new URL(req.url ?? '/', 'http://dev').pathname
|
||||||
|
} catch {
|
||||||
|
next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (pathname !== FONT_PROXY_PREFIX) {
|
||||||
|
next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = new URL(req.url ?? '/', 'http://dev').searchParams.get('url')
|
||||||
|
if (!target) {
|
||||||
|
res.statusCode = 400
|
||||||
|
res.end('missing url')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let targetURL: URL
|
||||||
|
try {
|
||||||
|
targetURL = new URL(decodeURIComponent(target))
|
||||||
|
} catch {
|
||||||
|
res.statusCode = 400
|
||||||
|
res.end('invalid url')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (targetURL.protocol !== 'https:' || !PROXIED_HOSTS.has(targetURL.host)) {
|
||||||
|
res.statusCode = 403
|
||||||
|
res.end('proxied host denied')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const upstream = await fetch(targetURL, {
|
||||||
|
headers: {
|
||||||
|
'user-agent':
|
||||||
|
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126 Safari/537.36',
|
||||||
|
accept: 'application/json, text/plain, */*',
|
||||||
|
},
|
||||||
|
redirect: 'follow',
|
||||||
|
})
|
||||||
|
const body = Buffer.from(await upstream.arrayBuffer())
|
||||||
|
res.statusCode = upstream.status
|
||||||
|
const type = upstream.headers.get('content-type')
|
||||||
|
if (type) res.setHeader('content-type', type)
|
||||||
|
res.setHeader('access-control-allow-origin', '*')
|
||||||
|
res.end(body)
|
||||||
|
} catch (e) {
|
||||||
|
res.statusCode = 502
|
||||||
|
res.end(String(e))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue