Merge remote-tracking branch 'origin/staging' into develop

This commit is contained in:
Andrey Antukh 2026-09-22 10:24:30 +02:00
commit 117c8db0bb
125 changed files with 2989 additions and 1157 deletions

View File

@ -43,17 +43,12 @@ jobs:
ref: ${{ steps.vars.outputs.gh_ref }} ref: ${{ steps.vars.outputs.gh_ref }}
# START: Setup Node and PNPM enabling cache # START: Setup Node and PNPM enabling cache
- name: Setup Node.js - name: Setup pnpm + Node.js
uses: actions/setup-node@v6 uses: pnpm/setup@v2
with: with:
node-version-file: .nvmrc working-directory: plugins
runtime: node@24.21.0
- name: Enable PNPM install: false
working-directory: ./plugins
shell: bash
run: |
corepack enable;
corepack install;
- name: Get pnpm store path - name: Get pnpm store path
id: pnpm-store id: pnpm-store

View File

@ -43,17 +43,12 @@ jobs:
ref: ${{ inputs.gh_ref }} ref: ${{ inputs.gh_ref }}
# START: Setup Node and PNPM enabling cache # START: Setup Node and PNPM enabling cache
- name: Setup Node.js - name: Setup pnpm + Node.js
uses: actions/setup-node@v6 uses: pnpm/setup@v2
with: with:
node-version-file: .nvmrc working-directory: plugins
runtime: node@24.21.0
- name: Enable PNPM install: false
working-directory: ./plugins
shell: bash
run: |
corepack enable;
corepack install;
- name: Get pnpm store path - name: Get pnpm store path
id: pnpm-store id: pnpm-store

View File

@ -41,17 +41,12 @@ jobs:
ref: ${{ steps.vars.outputs.gh_ref }} ref: ${{ steps.vars.outputs.gh_ref }}
# START: Setup Node and PNPM enabling cache # START: Setup Node and PNPM enabling cache
- name: Setup Node.js - name: Setup pnpm + Node.js
uses: actions/setup-node@v6 uses: pnpm/setup@v2
with: with:
node-version-file: .nvmrc working-directory: plugins
runtime: node@24.21.0
- name: Enable PNPM install: false
working-directory: ./plugins
shell: bash
run: |
corepack enable;
corepack install;
- name: Get pnpm store path - name: Get pnpm store path
id: pnpm-store id: pnpm-store

View File

@ -44,8 +44,6 @@ jobs:
- name: Lint - name: Lint
working-directory: ./common working-directory: ./common
run: | run: |
corepack enable;
corepack install;
pnpm install; pnpm install;
pnpm run check-fmt:clj pnpm run check-fmt:clj
pnpm run check-fmt:js pnpm run check-fmt:js

View File

@ -1,69 +0,0 @@
name: "CI: Composable Test Suite"
# Runs the composable component test suite (it exercises component semantics
# through the real Plugin API against the full frontend, so it needs the
# frontend bundle + the plugin runtime, but no backend): the driver serves the
# prebuilt frontend bundle and intercepts every backend RPC with Playwright
# fixtures. See plugins/apps/composable-test-suite/README.md ("Running in CI").
defaults:
run:
shell: bash
on:
pull_request:
paths:
- 'plugins/**'
- 'frontend/**'
- 'common/**'
types:
- opened
- synchronize
- ready_for_review
push:
branches:
- develop
- staging
paths:
- 'plugins/**'
- 'frontend/**'
- 'common/**'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
composable-test-suite:
if: ${{ !github.event.pull_request.draft }}
name: "Run composable test suite (mocked backend)"
runs-on: penpot-extended-runner
container:
image: penpotapp/devenv:latest
volumes:
- /var/cache/github-runner/m2:/root/.m2
- /var/cache/github-runner/gitlib:/root/.gitlibs
steps:
- uses: actions/checkout@v6
# The driver serves the prebuilt bundle from frontend/resources/public.
- name: Build frontend bundle
working-directory: ./frontend
run: ./scripts/build
- name: Install deps
working-directory: ./plugins
run: |
corepack enable;
corepack install;
pnpm install;
- name: Install Playwright Chromium
working-directory: ./plugins
run: pnpm --filter composable-test-suite exec playwright install --with-deps chromium
- name: Run composable test suite (mocked)
working-directory: ./plugins
run: pnpm --filter composable-test-suite run test:ci

View File

@ -1,4 +1,26 @@
name: "CI: Integration" name: "CI: E2E"
# Single entry point for every suite that drives a real frontend bundle.
# The bundle is built ONCE by `build-bundle` and restored by every consumer,
# so adding a suite no longer adds another `frontend/scripts/build` run.
#
# Suites and what they need from the bundle:
#
# - Integration Tests: Playwright specs, backend faked with frontend mocks.
# - Composable test suite: Plugin runtime, backend faked with Playwright RPC
# fixtures. See plugins/apps/composable-test-suite/README.md ("Running in CI").
# - Plugin API Test Suite (mocked): Plugin runtime, backend faked with
# Playwright RPC fixtures and MOCK_BACKEND=1.
# See plugins/apps/plugin-api-test-suite/README.md.
#
# Hand-off: `build-bundle` restores `actions/cache` keyed by the checked-out
# SHA, builds only on a miss, and saves it; every consumer restores the same
# key after `needs: build-bundle` completes. A re-run of the same SHA reuses
# the cached bundle instead of rebuilding it.
#
# NOTE: the job `name:` values are the GitHub check contexts, so they are kept
# stable on purpose (branch protection may require them by name). Only the
# workflow file/name changed.
defaults: defaults:
run: run:
@ -30,12 +52,18 @@ on:
required: true required: true
default: '2' default: '2'
base_url:
description: 'Penpot base URL for the (disabled) live Plugin API suite'
required: false
default: 'https://localhost:3449'
pull_request: pull_request:
paths: paths:
- 'frontend/**' - 'frontend/**'
- 'common/**' - 'common/**'
- 'render-wasm/**' - 'render-wasm/**'
- '.github/workflows/tests-integration.yml' - 'plugins/**'
- '.github/workflows/tests-e2e.yml'
types: types:
- opened - opened
@ -51,18 +79,21 @@ on:
- 'frontend/**' - 'frontend/**'
- 'common/**' - 'common/**'
- 'render-wasm/**' - 'render-wasm/**'
- '.github/workflows/tests-integration.yml' - 'plugins/**'
- '.github/workflows/tests-e2e.yml'
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || inputs.gh_ref || github.ref }} group: ${{ github.workflow }}-${{ github.event.pull_request.number || inputs.gh_ref || github.ref }}
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
build-integration: # ── 1. Build the frontend bundle once ──────────────────────────────────
build-bundle:
if: ${{ !github.event.pull_request.draft }} if: ${{ !github.event.pull_request.draft }}
name: "Build Integration Bundle" name: "Build Integration Bundle"
runs-on: penpot-extended-runner runs-on: penpot-extended-runner
timeout-minutes: 30 timeout-minutes: 30
container: container:
image: penpotapp/devenv:latest image: penpotapp/devenv:latest
volumes: volumes:
@ -85,26 +116,36 @@ jobs:
- name: Extract cache key - name: Extract cache key
id: vars id: vars
run: | run: |
echo "bundle_key=integration-bundle-$(git rev-parse HEAD)" >> $GITHUB_OUTPUT echo "bundle_key=frontend-bundle-$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
- name: Restore Bundle Cache
id: restore
uses: actions/cache/restore@v5
with:
key: ${{ steps.vars.outputs.bundle_key }}
path: frontend/resources/public
- name: Build Bundle - name: Build Bundle
if: steps.restore.outputs.cache-hit != 'true'
working-directory: ./frontend working-directory: ./frontend
run: | run: |
./scripts/build ./scripts/build
- name: Store Bundle Cache - name: Store Bundle Cache
uses: actions/cache@v5 if: steps.restore.outputs.cache-hit != 'true'
uses: actions/cache/save@v5
with: with:
key: ${{ steps.vars.outputs.bundle_key }} key: ${{ steps.vars.outputs.bundle_key }}
path: frontend/resources/public path: frontend/resources/public
# ── 2. Consumers: restore the bundle, never rebuild it ─────────────────
test-integration: test-integration:
if: ${{ !github.event.pull_request.draft }} if: ${{ !github.event.pull_request.draft }}
name: "Integration Tests (${{ matrix.shard }})" name: "Integration Tests (${{ matrix.shard }})"
runs-on: penpot-extended-runner runs-on: penpot-extended-runner
timeout-minutes: 40 timeout-minutes: 40
needs: build-integration needs: build-bundle
strategy: strategy:
fail-fast: false fail-fast: false
@ -129,14 +170,13 @@ jobs:
- name: Restore Cache - name: Restore Cache
uses: actions/cache/restore@v5 uses: actions/cache/restore@v5
with: with:
key: ${{ needs.build-integration.outputs.bundle_key }} key: ${{ needs.build-bundle.outputs.bundle_key }}
path: frontend/resources/public path: frontend/resources/public
fail-on-cache-miss: true
- name: Install deps - name: Install deps
working-directory: ./frontend working-directory: ./frontend
run: | run: |
corepack enable;
corepack install;
pnpm install --frozen-lockfile; pnpm install --frozen-lockfile;
# No-op once the shared volume is warm; keeps the first run working. # No-op once the shared volume is warm; keeps the first run working.
@ -177,6 +217,89 @@ jobs:
if-no-files-found: ignore if-no-files-found: ignore
retention-days: 3 retention-days: 3
composable-test-suite:
if: ${{ github.event_name != 'workflow_dispatch' && !github.event.pull_request.draft }}
name: "Run composable test suite (mocked backend)"
runs-on: penpot-extended-runner
timeout-minutes: 30
needs: build-bundle
container:
image: penpotapp/devenv:latest
volumes:
- /var/cache/github-runner/m2:/root/.m2
- /var/cache/github-runner/gitlib:/root/.gitlibs
steps:
- uses: actions/checkout@v6
# The driver serves the prebuilt bundle from frontend/resources/public.
- name: Restore Cache
uses: actions/cache/restore@v5
with:
key: ${{ needs.build-bundle.outputs.bundle_key }}
path: frontend/resources/public
fail-on-cache-miss: true
- name: Install deps
working-directory: ./plugins
run: |
pnpm install;
- name: Install Playwright Chromium
working-directory: ./plugins
run: pnpm --filter composable-test-suite exec playwright install --with-deps chromium
- name: Run composable test suite (mocked)
working-directory: ./plugins
run: pnpm --filter composable-test-suite run test:ci
api-test-suite-mocked:
if: ${{ github.event_name != 'workflow_dispatch' && !github.event.pull_request.draft }}
name: "Run Plugin API Test Suite (mocked)"
runs-on: penpot-extended-runner
timeout-minutes: 30
needs: build-bundle
container:
image: penpotapp/devenv:latest
volumes:
- /var/cache/github-runner/m2:/root/.m2
- /var/cache/github-runner/gitlib:/root/.gitlibs
steps:
- uses: actions/checkout@v6
# Mocked mode serves the prebuilt bundle from frontend/resources/public.
- name: Restore Cache
uses: actions/cache/restore@v5
with:
key: ${{ needs.build-bundle.outputs.bundle_key }}
path: frontend/resources/public
fail-on-cache-miss: true
- name: Install deps
working-directory: ./plugins
run: |
pnpm install;
- name: Install Playwright Chromium
working-directory: ./plugins
run: pnpm --filter plugin-api-test-suite exec playwright install --with-deps chromium
- name: Generate API surface
working-directory: ./plugins
run: pnpm --filter plugin-api-test-suite run gen:api
- name: Run API test suite (mocked)
working-directory: ./plugins
env:
MOCK_BACKEND: "1"
run: pnpm --filter plugin-api-test-suite run test:ci
# ── 3. Merge the per-shard integration reports ─────────────────────────
merge-reports: merge-reports:
if: ${{ !cancelled() && !github.event.pull_request.draft && needs.test-integration.result != 'skipped' }} if: ${{ !cancelled() && !github.event.pull_request.draft && needs.test-integration.result != 'skipped' }}
name: "Merge Integration Reports" name: "Merge Integration Reports"
@ -200,8 +323,6 @@ jobs:
- name: Install deps - name: Install deps
working-directory: ./frontend working-directory: ./frontend
run: | run: |
corepack enable;
corepack install;
pnpm install --frozen-lockfile; pnpm install --frozen-lockfile;
- name: Download blob reports - name: Download blob reports
@ -245,8 +366,53 @@ jobs:
- name: Upload HTML report - name: Upload HTML report
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v7
if: always()
with: with:
name: integration-html-report name: integration-html-report
path: frontend/playwright-report/ path: frontend/playwright-report/
overwrite: true overwrite: true
retention-days: 7 retention-days: 7
# ── 4. Live Plugin API suite (disabled) ────────────────────────────────
#
# True end-to-end against a LIVE instance: point PENPOT_BASE_URL at a
# reachable instance and provide login credentials via repo secrets. Manual
# because the CI runner has no Docker to stand up a full stack.
#
# api-test-suite-live:
# if: ${{ github.event_name == 'workflow_dispatch' }}
# name: Run Plugin API Test Suite (live)
# runs-on: penpot-extended-runner
# container:
# image: penpotapp/devenv:latest
#
# env:
# PENPOT_BASE_URL: ${{ inputs.base_url }}
# E2E_LOGIN_EMAIL: ${{ secrets.E2E_LOGIN_EMAIL }}
# E2E_LOGIN_PASSWORD: ${{ secrets.E2E_LOGIN_PASSWORD }}
#
# steps:
# - uses: actions/checkout@v6
#
# - name: Setup Node
# uses: actions/setup-node@v6
# with:
# node-version-file: .nvmrc
#
# - name: Install deps
# working-directory: ./plugins
# run: |
# pnpm install;
#
# - name: Install Playwright Chromium
# working-directory: ./plugins
# run: pnpm --filter plugin-api-test-suite exec playwright install --with-deps chromium
#
# - name: Generate API surface
# working-directory: ./plugins
# run: pnpm --filter plugin-api-test-suite run gen:api
#
# # Note: requires a running Penpot instance reachable at PENPOT_BASE_URL.
# - name: Run API test suite
# working-directory: ./plugins
# run: pnpm --filter plugin-api-test-suite run test:ci

View File

@ -46,8 +46,6 @@ jobs:
- name: Lint - name: Lint
working-directory: ./exporter working-directory: ./exporter
run: | run: |
corepack enable;
corepack install;
pnpm install; pnpm install;
pnpm run check-fmt:clj pnpm run check-fmt:clj
pnpm run lint:clj pnpm run lint:clj

View File

@ -48,8 +48,6 @@ jobs:
- name: Lint - name: Lint
working-directory: ./frontend working-directory: ./frontend
run: | run: |
corepack enable;
corepack install;
pnpm install; pnpm install;
pnpm run check-fmt:js pnpm run check-fmt:js
pnpm run check-fmt:clj pnpm run check-fmt:clj

View File

@ -46,8 +46,6 @@ jobs:
- name: Lint - name: Lint
working-directory: ./library working-directory: ./library
run: | run: |
corepack enable;
corepack install;
pnpm install; pnpm install;
pnpm run check-fmt pnpm run check-fmt
pnpm run lint pnpm run lint

View File

@ -1,133 +0,0 @@
name: "CI: Plugin API Test Suite"
# Runs the Plugin API Test Suite (it exercises the real Penpot Plugin API, so it
# needs a running frontend + the plugin runtime). Two jobs:
#
# - api-test-suite-mocked (pull_request / push): the per-PR gate. Serves the
# prebuilt frontend bundle and intercepts every backend RPC with Playwright
# (MOCK_BACKEND=1). No backend / no login. Validates the frontend Plugin API
# binding + in-memory store; backend-result-dependent tests are skipped via the
# `skipIfMocked` tag. See plugins/apps/plugin-api-test-suite/README.md.
#
# - api-test-suite-live (workflow_dispatch): true end-to-end against a LIVE
# instance. Point PENPOT_BASE_URL at a reachable instance and provide login
# credentials via repo secrets. Manual because the CI runner has no Docker to
# stand up a full stack.
defaults:
run:
shell: bash
on:
workflow_dispatch:
inputs:
base_url:
description: "Penpot base URL (e.g. https://localhost:3449)"
required: false
default: "https://localhost:3449"
pull_request:
paths:
- 'plugins/**'
- 'frontend/**'
- 'common/**'
types:
- opened
- synchronize
- ready_for_review
push:
branches:
- develop
- staging
paths:
- 'plugins/**'
- 'frontend/src/app/plugins/**'
- 'common/**'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
api-test-suite-mocked:
if: ${{ github.event_name != 'workflow_dispatch' && !github.event.pull_request.draft }}
name: "Run Plugin API Test Suite (mocked)"
runs-on: penpot-extended-runner
container:
image: penpotapp/devenv:latest
volumes:
- /var/cache/github-runner/m2:/root/.m2
- /var/cache/github-runner/gitlib:/root/.gitlibs
steps:
- uses: actions/checkout@v6
# Mocked mode serves the prebuilt bundle from frontend/resources/public.
- name: Build frontend bundle
working-directory: ./frontend
run: ./scripts/build
- name: Install deps
working-directory: ./plugins
run: |
corepack enable;
corepack install;
pnpm install;
- name: Install Playwright Chromium
working-directory: ./plugins
run: pnpm --filter plugin-api-test-suite exec playwright install --with-deps chromium
- name: Generate API surface
working-directory: ./plugins
run: pnpm --filter plugin-api-test-suite run gen:api
- name: Run API test suite (mocked)
working-directory: ./plugins
env:
MOCK_BACKEND: "1"
run: pnpm --filter plugin-api-test-suite run test:ci
## The following job will launch the whole suite of tests but we need
## to have a full environment in the CI for this to work.
# api-test-suite-live:
# if: ${{ github.event_name == 'workflow_dispatch' }}
# name: Run Plugin API Test Suite (live)
# runs-on: penpot-extended-runner
# container:
# image: penpotapp/devenv:latest
#
# env:
# PENPOT_BASE_URL: ${{ github.event.inputs.base_url }}
# E2E_LOGIN_EMAIL: ${{ secrets.E2E_LOGIN_EMAIL }}
# E2E_LOGIN_PASSWORD: ${{ secrets.E2E_LOGIN_PASSWORD }}
#
# steps:
# - uses: actions/checkout@v6
#
# - name: Setup Node
# uses: actions/setup-node@v6
# with:
# node-version-file: .nvmrc
#
# - name: Install deps
# working-directory: ./plugins
# run: |
# corepack enable;
# corepack install;
# pnpm install;
#
# - name: Install Playwright Chromium
# working-directory: ./plugins
# run: pnpm --filter plugin-api-test-suite exec playwright install --with-deps chromium
#
# - name: Generate API surface
# working-directory: ./plugins
# run: pnpm --filter plugin-api-test-suite run gen:api
#
# # Note: requires a running Penpot instance reachable at PENPOT_BASE_URL.
# - name: Run API test suite
# working-directory: ./plugins
# run: pnpm --filter plugin-api-test-suite run test:ci

View File

@ -44,8 +44,6 @@ jobs:
working-directory: ./plugins working-directory: ./plugins
shell: bash shell: bash
run: | run: |
corepack enable;
corepack install;
pnpm install -r; pnpm install -r;
- name: Run Lint - name: Run Lint

2
.nvmrc
View File

@ -1 +1 @@
v24.19.0 v24.21.0

View File

@ -1,7 +1,113 @@
// Penpot opencode plugin: custom tools for Penpot development.
//
// Dual V1 + V2 implementation from a single file:
// - OpenCode V1 (>= 1.18.29) calls the default export's `server()` and uses
// the returned `tool` map (built with the `tool()` helper from
// `@opencode-ai/plugin`).
// - OpenCode V2 reads the default export's `id` and `setup()` and ignores
// `server()`. Tools are registered via `ctx.tool.transform()` with JSON
// Schema inputs, and `execute` returns `{ content }`.
// See https://opencode.ai/v2/docs/build/plugins/migrate-v1
//
// NOTE: the V2 side intentionally does NOT
// `import { Plugin } from "@opencode/plugin"`. At runtime `Plugin.define` is
// the identity function, so a plain `{ id, setup }` object is equivalent, and
// skipping the import keeps this plugin dependency-free
// (`.opencode/package.json` is gitignored, so a new dependency declared there
// would not travel with this file).
import { tool } from "@opencode-ai/plugin" import { tool } from "@opencode-ai/plugin"
import path from "path" import path from "path"
import { spawn } from "child_process" import { spawn } from "child_process"
function runCommand(command, args, options = {}) {
const {
cwd,
env,
stdin,
closeStdin = false,
successMessage = "Command executed successfully",
} = options
return new Promise((resolve) => {
let stdout = ""
let stderr = ""
const proc = spawn(command, args, { cwd, env })
proc.stdout.on("data", (data) => {
stdout += data.toString()
})
proc.stderr.on("data", (data) => {
stderr += data.toString()
})
proc.on("error", (error) => {
resolve(`Error: ${error.message}`)
})
proc.on("close", (exitCode) => {
const output =
exitCode === 0
? stdout.trim() || successMessage
: `Error (exit ${exitCode}): ${
(stderr || stdout).trim() || "No error output"
}`
resolve(output)
})
// Close stdin so the child cannot wait on it indefinitely. `psql -c`
// never reads stdin, so only the paren-repair pipe mode needs this, but
// closing it unconditionally is harmless there.
if (stdin !== undefined) {
proc.stdin.end(stdin)
} else if (closeStdin) {
proc.stdin.end()
}
})
}
function executePsql(sql, useTestDb, cwd) {
const host = process.env.PENPOT_DB_HOST || "postgres"
const user = process.env.PENPOT_DB_USER || "penpot"
const db = useTestDb
? "penpot_test"
: process.env.PENPOT_DB_NAME || "penpot"
const password = process.env.PENPOT_DB_PASSWORD || "penpot"
const psqlArgs = ["-h", host, "-U", user, "-d", db, "-c", sql]
return runCommand("psql", psqlArgs, {
cwd,
env: { ...process.env, PGPASSWORD: password },
successMessage: "Query executed successfully",
})
}
function executeParenRepair({ files, code }, directory) {
const script = path.join(directory, "scripts/paren-repair")
const fileList = files
? files
.split(",")
.map((file) => file.trim())
.filter(Boolean)
: []
const childArgs =
fileList.length > 0 ? [script, ...fileList] : [script]
return runCommand("bb", childArgs, {
cwd: directory,
stdin: code,
closeStdin: true,
successMessage: "No changes needed",
})
}
// --- V1 tool definitions (OpenCode V1 calls `server()` below) ---
const penpotPsqlTool = tool({ const penpotPsqlTool = tool({
description: description:
"Execute a SQL command against the Penpot database. Uses the defaults from scripts/psql.", "Execute a SQL command against the Penpot database. Uses the defaults from scripts/psql.",
@ -18,46 +124,7 @@ const penpotPsqlTool = tool({
}, },
async execute(args, context) { async execute(args, context) {
const host = process.env.PENPOT_DB_HOST || "postgres" return executePsql(args.sql, args.test === true, context.worktree)
const user = process.env.PENPOT_DB_USER || "penpot"
const db = args.test
? "penpot_test"
: process.env.PENPOT_DB_NAME || "penpot"
const password = process.env.PENPOT_DB_PASSWORD || "penpot"
const psqlArgs = ["-h", host, "-U", user, "-d", db, "-c", args.sql]
return new Promise((resolve) => {
let stdout = ""
let stderr = ""
const proc = spawn("psql", psqlArgs, {
cwd: context.worktree,
env: { ...process.env, PGPASSWORD: password },
})
proc.stdout.on("data", (data) => {
stdout += data.toString()
})
proc.stderr.on("data", (data) => {
stderr += data.toString()
})
proc.on("error", (error) => {
resolve(`Error: ${error.message}`)
})
proc.on("close", (exitCode) => {
const output =
exitCode === 0
? stdout.trim() || "Query executed successfully"
: `Error (exit ${exitCode}): ${
(stderr || stdout).trim() || "No error output"
}`
resolve(output)
})
})
}, },
}) })
@ -82,69 +149,11 @@ const parenRepairTool = tool({
}, },
async execute(args, context) { async execute(args, context) {
const script = path.join(context.worktree, "scripts/paren-repair") return executeParenRepair(args, context.worktree)
const files = args.files
? args.files
.split(",")
.map((file) => file.trim())
.filter(Boolean)
: []
const paramInfo =
files.length > 0
? `files=[${files.join(", ")}]`
: args.code !== undefined
? `code=(${args.code.length} chars)`
: "none"
return new Promise((resolve) => {
const childArgs =
files.length > 0
? [script, ...files]
: [script]
const proc = spawn("bb", childArgs, {
cwd: context.worktree,
})
let stdout = ""
let stderr = ""
proc.stdout.on("data", (data) => {
stdout += data.toString()
})
proc.stderr.on("data", (data) => {
stderr += data.toString()
})
proc.on("error", (error) => {
resolve(`Error: ${error.message}`)
})
proc.on("close", (exitCode) => {
const output =
exitCode === 0
? stdout.trim() || "No changes needed"
: `Error (exit ${exitCode}): ${
(stderr || stdout).trim() || "No error output"
}`
resolve(output)
})
// Close stdin in all cases so the process cannot wait indefinitely.
if (args.code !== undefined) {
proc.stdin.end(args.code)
} else {
proc.stdin.end()
}
})
}, },
}) })
export default async function plugin() { async function server() {
return { return {
tool: { tool: {
"paren-repair": parenRepairTool, "paren-repair": parenRepairTool,
@ -153,147 +162,83 @@ export default async function plugin() {
} }
} }
// --- V2 setup (OpenCode V2 calls `setup()` and ignores `server()`) ---
const penpotPsqlInputSchema = {
type: "object",
properties: {
sql: {
type: "string",
description: "SQL command to execute",
},
test: {
type: "boolean",
description: "Use the penpot_test database",
},
},
required: ["sql"],
additionalProperties: false,
}
const parenRepairInputSchema = {
type: "object",
properties: {
// A string is used instead of an array so OpenCode displays it
// in the generic tool invocation.
files: {
type: "string",
description:
"Comma-separated file paths to fix, for example: frontend/src/app/config.cljs, backend/src/core.clj",
},
code: {
type: "string",
description: "Code string to fix via stdin",
},
},
additionalProperties: false,
}
async function setup(ctx) {
// Plugin instance location. This is not the location of every session the
// tools may run for, but it is the closest V2 equivalent of the V1
// per-execution `context.worktree` (the repo checkout the plugin loaded
// from), which is what both tools need as cwd / script base.
const directory =
ctx.location.directory ?? ctx.location.project?.canonical
// Keep this callback synchronous: transforms are replayable state edits.
// The async work happens later, inside each tool's `execute`.
await ctx.tool.transform((editor) => {
editor.add({
name: "penpot-psql",
description:
"Execute a SQL command against the Penpot database. Uses the defaults from scripts/psql.",
input: penpotPsqlInputSchema,
async execute(input) {
const content = await executePsql(
input.sql,
input.test === true,
directory,
)
return { content }
},
})
editor.add({
name: "paren-repair",
description:
"Fix mismatched parentheses/braces in Clojure files (.clj, .cljs, .cljc) then reformat with cljfmt.",
input: parenRepairInputSchema,
async execute(input) {
const content = await executeParenRepair(input, directory)
return { content }
},
})
})
}
export default {
id: "penpot",
setup,
server,
// import { tool } from "@opencode-ai/plugin" }
// import path from "path"
// import { spawn } from "child_process"
// function formatFiles(files) {
// if (files.length === 0) return "stdin"
// // Keep the visible tool title reasonably short.
// if (files.length <= 3) return files.join(", ")
// return `${files.slice(0, 3).join(", ")} (+${files.length - 3} more)`
// }
// const parenRepairTool = tool({
// description:
// "Fix mismatched parentheses/braces in Clojure files, then reformat with cljfmt.",
// args: {
// files: tool.schema
// .array(tool.schema.string())
// .describe("Array of file paths to fix")
// .optional(),
// code: tool.schema
// .string()
// .describe("Code string to fix via stdin")
// .optional(),
// },
// async execute(args, context) {
// const script = path.join(context.worktree, "scripts/paren-repair")
// const files = (args.files ?? []).map((file) => {
// const absolute = path.isAbsolute(file)
// ? file
// : path.resolve(context.worktree, file)
// return path.relative(context.worktree, absolute)
// })
// const targetSummary =
// files.length > 0
// ? formatFiles(files)
// : args.code !== undefined
// ? `stdin (${args.code.length} chars)`
// : "no input"
// // This updates the tool-call title immediately, while it is running.
// await context.metadata({
// title: `Paren repair: ${targetSummary}`,
// metadata: {
// files,
// codeChars: args.code?.length,
// },
// })
// const childArgs =
// args.files && args.files.length > 0
// ? [script, ...args.files]
// : [script]
// return new Promise((resolve) => {
// const proc = spawn("bb", childArgs, {
// cwd: context.worktree,
// })
// let stdout = ""
// let stderr = ""
// if (args.code !== undefined) {
// proc.stdin.end(args.code)
// }
// proc.stdout.on("data", (data) => {
// stdout += data.toString()
// })
// proc.stderr.on("data", (data) => {
// stderr += data.toString()
// })
// proc.on("close", (exitCode) => {
// const successful = exitCode === 0
// const commandOutput = successful
// ? stdout.trim() || "No changes needed"
// : `Error (exit ${exitCode}): ${(stderr || stdout).trim()}`
// const parameterOutput =
// files.length > 0
// ? `Files passed:\n${files.map((file) => `- ${file}`).join("\n")}`
// : args.code !== undefined
// ? `Input passed through stdin: ${args.code.length} characters`
// : "No files or stdin input were passed"
// resolve({
// title: `Paren repair: ${targetSummary}`,
// output: `${parameterOutput}\n\n${commandOutput}`,
// metadata: {
// files,
// codeChars: args.code?.length,
// exitCode,
// successful,
// },
// })
// })
// proc.on("error", (error) => {
// resolve({
// title: `Paren repair failed: ${targetSummary}`,
// output: [
// files.length > 0
// ? `Files passed:\n${files.map((file) => `- ${file}`).join("\n")}`
// : `Input: ${targetSummary}`,
// `Failed to start bb: ${error.message}`,
// ].join("\n\n"),
// metadata: {
// files,
// codeChars: args.code?.length,
// successful: false,
// },
// })
// })
// })
// },
// })
// export default async function plugin() {
// return {
// tool: {
// "paren-repair": parenRepairTool,
// },
// }
// }

View File

@ -23,7 +23,7 @@
- Team/project commands mix DB changes, email, message bus notifications, media/storage cleanup, feature flags, quotas, and audit metadata. Keep mutations transactional when the existing command does so. - Team/project commands mix DB changes, email, message bus notifications, media/storage cleanup, feature flags, quotas, and audit metadata. Keep mutations transactional when the existing command does so.
- Invitation flows validate muted/bounced emails before sending and use tokenized invitation state. Accepting an invitation is tied to the invited member identity, not just possession of a token. - Invitation flows validate muted/bounced emails before sending and use tokenized invitation state. Accepting an invitation is tied to the invited member identity, not just possession of a token.
- Logical deletion is used for many product objects; prefer existing logical-deletion helpers over hard deletes unless the command already performs permanent cleanup. - Logical deletion is used for many product objects; prefer existing logical-deletion helpers over hard deletes unless the command already performs permanent cleanup.
- Bounced/spam-complaint emails can mute/block a profile for login/registration and email sending. Devenv MailCatcher is the normal local path for registration/email-flow testing. - Bounced/spam-complaint emails can mute/block a profile for login/registration and email sending. Devenv Mailpit is the normal local path for registration/email-flow testing.
## Comments, webhooks, and audit ## Comments, webhooks, and audit
@ -37,4 +37,4 @@
- Enable LDAP login locally with frontend flag `enable-login-with-ldap`; the devenv includes a configured test LDAP service. - Enable LDAP login locally with frontend flag `enable-login-with-ldap`; the devenv includes a configured test LDAP service.
- OIDC testing requires external provider app credentials plus matching backend/frontend config. - OIDC testing requires external provider app credentials plus matching backend/frontend config.
- Backend domain tests usually live under `backend/test/backend_tests/rpc/commands/*_test.clj` or nearby backend test namespaces. Use focused `clojure -M:dev:test --focus ...` from `backend/` when possible. - Backend domain tests usually live under `backend/test/backend_tests/rpc/commands/*_test.clj` or nearby backend test namespaces. Use focused `clojure -M:dev:test --focus ...` from `backend/` when possible.
- For auth/session or HTTP behavior, combine backend tests with the HTTP/session notes in `mem:backend/http-storage-filedata-subtleties` because RPC-level tests may not exercise cookie/header transforms. - For auth/session or HTTP behavior, combine backend tests with the HTTP/session notes in `mem:backend/http-storage-filedata-subtleties` because RPC-level tests may not exercise cookie/header transforms.

View File

@ -18,7 +18,7 @@ You are working on the GitHub project `penpot/penpot`, a monorepo.
- Before `gh issue create``mem:workflow/creating-issues` (title derivation, body template, labels, Issue Type) - Before `gh issue create``mem:workflow/creating-issues` (title derivation, body template, labels, Issue Type)
- Before `gh pr create` / `gh pr edit``mem:workflow/creating-prs` (title format, body structure, "Note:" line) - Before `gh pr create` / `gh pr edit``mem:workflow/creating-prs` (title format, body structure, "Note:" line)
- Before a repo-wide pnpm version update → `mem:workflow/updating-pnpm` (workspace - Before a repo-wide pnpm version update → `mem:workflow/updating-pnpm` (workspace
layout, `corepack use` sweep order, the stamp-missing-field and layout, `scripts/sync-pnpm-version` flow, the stamp-missing-field and
ignored-builds gotchas, verification steps) ignored-builds gotchas, verification steps)
- **Never `git push`, force-push, or modify `git origin`** (or any other remote). The user pushes from their own shell; if a push is required, say so and wait. Never amend a commit that the user has already pushed unless explicitly asked. - **Never `git push`, force-push, or modify `git origin`** (or any other remote). The user pushes from their own shell; if a push is required, say so and wait. Never amend a commit that the user has already pushed unless explicitly asked.
- **Never edit `CHANGES.md` by hand.** The changelog is generated from GitHub milestones during the release process; update it only via the `update-changelog` skill flow or on explicit user request. - **Never edit `CHANGES.md` by hand.** The changelog is generated from GitHub milestones during the release process; update it only via the `update-changelog` skill flow or on explicit user request.
@ -55,7 +55,7 @@ module. You can read it from `mem:<MODULE>/core`
- `docker/` contains devenv related code, not needed unless specifically instructed. - `docker/` contains devenv related code, not needed unless specifically instructed.
When working on devenv startup, compose layout, instance config (`defaults.env`), When working on devenv startup, compose layout, instance config (`defaults.env`),
tmux session lifecycle, MinIO provisioning, or anything in `manage.sh`'s tmux session lifecycle, RustFS provisioning, or anything in `manage.sh`'s
`*-devenv` commands, read `mem:devenv/core`. `*-devenv` commands, read `mem:devenv/core`.
- `experiments/` contains standalone experimental HTML/JS/scripts; treat it as non-core unless the user explicitly asks about it. - `experiments/` contains standalone experimental HTML/JS/scripts; treat it as non-core unless the user explicitly asks about it.
- `sample_media/` contains sample image/icon media and config used as fixtures/demo material; do not infer app behavior from it. - `sample_media/` contains sample image/icon media and config used as fixtures/demo material; do not infer app behavior from it.

View File

@ -1,10 +1,10 @@
# Devenv startup and configuration # Devenv startup and configuration
Compose-based dev environment under `docker/devenv/`, driven by `manage.sh`. Parallel instances share infra + Postgres + MinIO; each instance has its own `main` container, Valkey, source checkout, tmux session. Compose-based dev environment under `docker/devenv/`, driven by `manage.sh`. Parallel instances share infra + Postgres + RustFS; each instance has its own `main` container, Valkey, source checkout, tmux session.
## Compose project layout ## Compose project layout
- `penpotdev-infra`: shared `postgres`, `minio`, `minio-setup`, `mailer`, `ldap`. File: `docker-compose.infra.yml`. - `penpotdev-infra`: shared `postgres`, `rustfs`, `valkey`, `mailer`, `ldap`. File: `docker-compose.infra.yml`.
- `penpotdev-wsN` (N=0,1,…): per-instance `main` + `redis` (Valkey). File: `docker-compose.main.yml`. ws0 (a.k.a. `main`) binds `$PWD`; ws1+ bind clones at `${PENPOT_WORKSPACES_DIR}/wsN/` (default `~/.penpot/penpot_workspaces/`), maintained by the developer. - `penpotdev-wsN` (N=0,1,…): per-instance `main` + `redis` (Valkey). File: `docker-compose.main.yml`. ws0 (a.k.a. `main`) binds `$PWD`; ws1+ bind clones at `${PENPOT_WORKSPACES_DIR}/wsN/` (default `~/.penpot/penpot_workspaces/`), maintained by the developer.
- Optional overlay `docker-compose.opencode.yml`: added by `instance-compose` as an extra `-f` only when `PENPOT_OPENCODE_CONFIG_DIR` is set (i.e. `run-devenv --opencode-config-dir DIR` ran in this process). Bind-mounts the host dir at `/home/penpot/.config/opencode` (`:z`). Flag-only, per-call; not read from ambient env. Parser `parse-opencode-config-dir` absolutizes (`~`, realpath) because compose resolves relative bind sources against the compose file's dir. Only instances brought up with the flag get the mount. - Optional overlay `docker-compose.opencode.yml`: added by `instance-compose` as an extra `-f` only when `PENPOT_OPENCODE_CONFIG_DIR` is set (i.e. `run-devenv --opencode-config-dir DIR` ran in this process). Bind-mounts the host dir at `/home/penpot/.config/opencode` (`:z`). Flag-only, per-call; not read from ambient env. Parser `parse-opencode-config-dir` absolutizes (`~`, realpath) because compose resolves relative bind sources against the compose file's dir. Only instances brought up with the flag get the mount.
- All projects join external network `penpot_shared`. Created idempotently by `ensure-devenv-network`, never removed by lifecycle commands. - All projects join external network `penpot_shared`. Created idempotently by `ensure-devenv-network`, never removed by lifecycle commands.
@ -13,22 +13,22 @@ Compose-based dev environment under `docker/devenv/`, driven by `manage.sh`. Par
- `docker/devenv/defaults.env`: ws0 baseline — container/volume names, runtime env, published host ports, tmux defaults. `manage.sh` aborts if unreadable. - `docker/devenv/defaults.env`: ws0 baseline — container/volume names, runtime env, published host ports, tmux defaults. `manage.sh` aborts if unreadable.
- For ws1+, `instance-env-overrides` computes the per-instance overrides (container/volume names, host ports offset `10000·N`, `PENPOT_PUBLIC_URI`, `PENPOT_REDIS_URI`, `PENPOT_BACKEND_WORKER=false`) and `instance-compose` injects them as env vars at compose time — never written to disk, recomputed each call so they can't drift. ws0 uses `defaults.env` as-is. - For ws1+, `instance-env-overrides` computes the per-instance overrides (container/volume names, host ports offset `10000·N`, `PENPOT_PUBLIC_URI`, `PENPOT_REDIS_URI`, `PENPOT_BACKEND_WORKER=false`) and `instance-compose` injects them as env vars at compose time — never written to disk, recomputed each call so they can't drift. ws0 uses `defaults.env` as-is.
- `backend/scripts/_env`: backend-internal only — secret keys, `PENPOT_FLAGS` (with `enable-backend-worker` gated on `PENPOT_BACKEND_WORKER`), `JAVA_OPTS`, `setup_minio()`. Never duplicates `defaults.env`. - `backend/scripts/_env`: backend-internal only — secret keys, `PENPOT_FLAGS` (with `enable-backend-worker` gated on `PENPOT_BACKEND_WORKER`), `JAVA_OPTS`, `setup_s3_bucket()`. Never duplicates `defaults.env`.
- Compose files use pure `${VAR}` substitution; missing var = compose fails. - Compose files use pure `${VAR}` substitution; missing var = compose fails.
## Invariants ## Invariants
- `infra-compose` / `instance-compose` wrap `docker compose` with `env -i`, then re-inject what compose needs. Stripping is required because `defaults.env` is sourced into manage.sh's shell at startup (stale values would leak); the ws1+ overrides are deliberately re-injected as shell env vars precisely because Compose gives shell precedence over `--env-file`, so they override the `defaults.env` baseline. - `infra-compose` / `instance-compose` wrap `docker compose` with `env -i`, then re-inject what compose needs. Stripping is required because `defaults.env` is sourced into manage.sh's shell at startup (stale values would leak); the ws1+ overrides are deliberately re-injected as shell env vars precisely because Compose gives shell precedence over `--env-file`, so they override the `defaults.env` baseline.
- Volume names pinned via `name:` (PENPOT_*_VOLUME), decoupled from the compose project name. ws1+ inject distinct per-instance volume names; ws0 keeps the historical `penpotdev_*` physical names so project renames never require data migration. - Volume names pinned via `name:` (PENPOT_*_VOLUME), decoupled from the compose project name. Replacement services use new volumes and leave old volumes untouched. PostgreSQL 18 mounts `penpotdev_postgres_data_pg18` at `/var/lib/postgresql`; the old PG16 volume is not migrated automatically.
- Network aliases (`- main`, `- redis`) are not declared in main.yml. Compose's auto-service-alias still registers `redis` on the shared network, so DNS for `redis` is non-deterministic with multiple instances. Backend uses `PENPOT_REDIS_URI=redis://penpot-devenv-wsN-valkey/0` (container_name) instead. - Network aliases (`- main`, `- redis`) are not declared in main.yml. Compose's auto-service-alias still registers `redis` on the shared network, so DNS for `redis` is non-deterministic with multiple instances. Backend uses `PENPOT_REDIS_URI=redis://penpot-devenv-wsN-valkey/0` (container_name) instead.
- No cross-project `depends_on`. `manage.sh ensure-infra-up` `docker wait`s on the `minio-setup` one-shot. - No cross-project `depends_on`. `manage.sh ensure-infra-up` uses Compose `--wait`; PostgreSQL, RustFS, and Mailpit expose healthchecks. PostgreSQL checks TCP so its temporary init server cannot report ready.
- `JAVA_OPTS` in `manage.sh` is shadowed inside the container by `_env`. The `-e JAVA_OPTS=...` flag only matters for processes that don't source `_env`. - `JAVA_OPTS` in `manage.sh` is shadowed inside the container by `_env`. The `-e JAVA_OPTS=...` flag only matters for processes that don't source `_env`.
## Worker policy ## Worker policy
Backend workers run only on ws0. `_env` gates `enable-backend-worker` on `PENPOT_BACKEND_WORKER`; ws1+ inject it as false. Workers are pure fire-and-forget: `wrk/submit!` inserts a row into the shared Postgres `task` table and returns; RPC handlers never wait on completion and workers never publish to msgbus. The reason for "ws0 only" is avoiding multi-instance worker races (cron dedup is best-effort across instances, `wrk/submit!` `dedupe` is racy across submitters); details in `mem:prod-infra/core`. Backend workers run only on ws0. `_env` gates `enable-backend-worker` on `PENPOT_BACKEND_WORKER`; ws1+ inject it as false. Workers are pure fire-and-forget: `wrk/submit!` inserts a row into the shared Postgres `task` table and returns; RPC handlers never wait on completion and workers never publish to msgbus. The reason for "ws0 only" is avoiding multi-instance worker races (cron dedup is best-effort across instances, `wrk/submit!` `dedupe` is racy across submitters); details in `mem:prod-infra/core`.
Each workspace is independent and can be started/stopped in any order. Shared infra (postgres, minio, etc.) is shut down only when no instances remain running. Each workspace is independent and can be started/stopped in any order. Shared infra (Postgres, RustFS, etc.) is shut down only when no instances remain running.
## Port layout ## Port layout
@ -43,7 +43,7 @@ Container-internal ports fixed; host side offset `10000·N`.
| 14181 | 24181 | … | 14281 | Serena MCP | | 14181 | 24181 | … | 14281 | Serena MCP |
| 14182 | 24182 | … | 24282 | Serena dashboard | | 14182 | 24182 | … | 24282 | Serena dashboard |
Everything else (frontend dev, backend API, exporter, storybook, REPLs, plugin dev, MCP inspector/WebSocket) is in-process or same-origin via Caddy/nginx. Infra publishes: mailer 1080, ldap 10389/10636 (singletons, not offset). Everything else (frontend dev, backend API, exporter, storybook, REPLs, plugin dev, MCP inspector/WebSocket) is in-process or same-origin via Caddy/nginx. Infra publishes RustFS S3 API 9000, RustFS console 9001, and Mailpit UI 1080 on loopback; ldap 10389/10636 remains a singleton without offsets. Mailpit stores its SQLite database in `penpotdev_mailpit_data`.
## Tmux + MCP routing ## Tmux + MCP routing

View File

@ -138,13 +138,15 @@ Distinguishing abstractions (the OOP articulation of the shared principles):
control via Playwright; recipe in the README. control via Playwright; recipe in the README.
## CI ## CI
Headless per-PR gate: `.github/workflows/tests-composable-suite.yml` runs Headless per-PR gate: the `composable-test-suite` job in
`pnpm --filter composable-test-suite run test:ci` — mocked backend (frontend e2e static server + `.github/workflows/tests-e2e.yml` runs `pnpm --filter composable-test-suite run test:ci` — mocked
backend (frontend e2e static server +
Playwright RPC fixtures, no backend/login), the in-sandbox bundle injected via `ɵloadPlugin`, Playwright RPC fixtures, no backend/login), the in-sandbox bundle injected via `ɵloadPlugin`,
results streamed via console markers, `TEST_FILTER` by identifier substring. The mocked backend is results streamed via console markers, `TEST_FILTER` by identifier substring. The mocked backend is
NOT a limitation for this suite (everything asserted is frontend store logic; empirically NOT a limitation for this suite (everything asserted is frontend store logic; empirically
confirmed against the interactive runs). Architecture mirrors `plugin-api-test-suite`'s CI driver; confirmed against the interactive runs). Architecture mirrors `plugin-api-test-suite`'s CI driver;
the mock harness exists in THREE places that must stay in sync (provenance note in `ci/run-ci.ts`). the mock harness exists in THREE places that must stay in sync (provenance note in `ci/run-ci.ts`).
The frontend bundle is built once per SHA and restored by this job (`mem:frontend/e2e-ci-workflow`).
Details: README, "Running in CI". Details: README, "Running in CI".
## Substrate ## Substrate

View File

@ -53,6 +53,7 @@ Diagnostics and validation:
- Source-edit compile/hot-reload diagnostics: `mem:frontend/compile-diagnostics`. - Source-edit compile/hot-reload diagnostics: `mem:frontend/compile-diagnostics`.
- Runtime crash recovery: `mem:frontend/handling-crashes`. - Runtime crash recovery: `mem:frontend/handling-crashes`.
- Tests and live verification: `mem:frontend/testing`. - Tests and live verification: `mem:frontend/testing`.
- CI end-to-end workflow (build-once frontend bundle, check names): `mem:frontend/e2e-ci-workflow`.
- Cross-cutting testing principles and anti-patterns: `mem:testing`. - Cross-cutting testing principles and anti-patterns: `mem:testing`.
- Real pointer/keyboard gesture reproduction: `mem:frontend/playwright-gestures`. - Real pointer/keyboard gesture reproduction: `mem:frontend/playwright-gestures`.

View File

@ -0,0 +1,43 @@
# E2E CI workflow (build-once frontend bundle)
`.github/workflows/tests-e2e.yml` ("CI: E2E") is the single workflow for every
suite that drives a real frontend bundle:
- `Integration Tests` — Playwright specs under `frontend/playwright` (sharded).
- `Run composable test suite (mocked backend)``mem:frontend/composable-component-tests`.
- `Run Plugin API Test Suite (mocked)``plugins/apps/plugin-api-test-suite`.
Triggers: PR/push touching `frontend/**`, `common/**`, `render-wasm/**`,
`plugins/**` (or the workflow file), plus `workflow_dispatch` (integration only).
A `plugins/**` change runs the whole set on purpose: the bundle embeds the
built plugins.
## Invariants
- ONE `frontend/scripts/build` per SHA. The `build-bundle` job restores
`actions/cache` key `frontend-bundle-<sha>`, builds only on a miss, and saves
the key before the job ends. A re-run of the same SHA reuses the cache.
- Consumer jobs (`needs: build-bundle`) restore the same key with
`fail-on-cache-miss: true` and NEVER run `frontend/scripts/build`.
- The bundle is `frontend/resources/public`. The integration specs serve it
with `frontend/scripts/e2e-server.js`; each mocked plugin driver serves it
with its own zero-dependency `ci/static-server.ts` (duplicated in both
suites — keep the copies in sync).
- Mocked plugin jobs install only `plugins/` deps, so their drivers must not
import anything from `frontend/node_modules` at runtime (e.g. no
`frontend/scripts/e2e-server.js`, which needs `express`).
- Cache key comes from `git rev-parse HEAD` (the checked-out ref), not
`github.sha`, because `workflow_dispatch` can target a different ref.
- Job `name:` values are the GitHub check contexts. Keep them stable: branch
protection may match them by name. Renaming the workflow file/name is safe.
## Adding a bundle-consuming suite
Add a job with `needs: build-bundle`, a `Restore Cache` step
(`actions/cache/restore@v5`, key `needs.build-bundle.outputs.bundle_key`,
`fail-on-cache-miss: true`), then that suite's own deps. Never add a build step.
## Scope
Distinct from `Bundles Builder` (`.github/workflows/build-bundle.yml`), the
release path that zips the bundle (`manage.sh build-bundle`) and uploads it to S3.

View File

@ -31,6 +31,10 @@ Integration tests fake backend behavior by intercepting network/websocket traffi
Locator priority should follow user-facing semantics: `getByRole`, `getByLabel`, `getByPlaceholder`, `getByText`, then semantic alternatives such as alt/title, with `getByTestId` as the last resort. Name tests from the user's perspective and prefer positive, single-purpose assertions. Locator priority should follow user-facing semantics: `getByRole`, `getByLabel`, `getByPlaceholder`, `getByText`, then semantic alternatives such as alt/title, with `getByTestId` as the last resort. Name tests from the user's perspective and prefer positive, single-purpose assertions.
## CI (E2E)
`.github/workflows/tests-e2e.yml` runs the integration specs, the composable component suite, and the mocked Plugin API suite from one workflow that builds the frontend bundle once per SHA. Before adding a job that needs the bundle, read `mem:frontend/e2e-ci-workflow` (build-once contract, cache key, stable check names).
## Live browser verification ## Live browser verification
Because CLJC compiles to both JVM and CLJS, JVM/common tests can miss frontend-only state caused by browser runtime, WASM modifier math, or real pointer events. Use `mem:frontend/cljs-repl` to inspect live app state and `mem:frontend/playwright-gestures` when real input is needed. Because CLJC compiles to both JVM and CLJS, JVM/common tests can miss frontend-only state caused by browser runtime, WASM modifier math, or real pointer events. Use `mem:frontend/cljs-repl` to inspect live app state and `mem:frontend/playwright-gestures` when real input is needed.

View File

@ -6,7 +6,7 @@ Backend (`app.config`, `PENPOT_*` env vars) is parameterized; deployments choose
- **PostgreSQL**: durable store. Profiles, teams, files, sessions, audit, `storage_object` metadata, the `task` queue, `scheduled_task` cron registry, migrations. File-data also lives here when the file-data backend is `legacy-db`/`db`. One shared DB across all backends. - **PostgreSQL**: durable store. Profiles, teams, files, sessions, audit, `storage_object` metadata, the `task` queue, `scheduled_task` cron registry, migrations. File-data also lives here when the file-data backend is `legacy-db`/`db`. One shared DB across all backends.
- **Redis (Valkey-compatible)**: per-backend message bus and cache. Concrete uses: msgbus Pub/Sub for collaborative-editing broadcasts and team/profile-org notifications fired by RPC handlers (`app.rpc.notifications`, `files_update`, `teams`, `websocket`); file-summary cache gated by `enable-redis-cache`; rate-limit counters; and the dispatcher→runner work hand-off list `penpot.worker.queue:<tenant>:<queue>`. `PENPOT_REDIS_URI`. - **Redis (Valkey-compatible)**: per-backend message bus and cache. Concrete uses: msgbus Pub/Sub for collaborative-editing broadcasts and team/profile-org notifications fired by RPC handlers (`app.rpc.notifications`, `files_update`, `teams`, `websocket`); file-summary cache gated by `enable-redis-cache`; rate-limit counters; and the dispatcher→runner work hand-off list `penpot.worker.queue:<tenant>:<queue>`. `PENPOT_REDIS_URI`.
- **Object storage**: backends `:s3` and `:fs`. S3 in prod; devenv uses MinIO. Holds uploaded media, file-data when the file-data backend is `storage`, exports. Backend-side details (resolve, dedup, bucket set, object lifecycle, and file-data backends): `mem:backend/storage`. - **Object storage**: backends `:s3` and `:fs`. S3 in prod; devenv uses RustFS. Holds uploaded media, file-data when the file-data backend is `storage`, exports. Backend-side details (resolve, dedup, bucket set, object lifecycle, and file-data backends): `mem:backend/storage`.
- **SMTP mailer**: invitations, password resets, email verification (sent via the `:sendmail` worker task). - **SMTP mailer**: invitations, password resets, email verification (sent via the `:sendmail` worker task).
- **LDAP** (optional auth provider): helpers in `app.auth.*`, gated by `enable-login-with-ldap`. - **LDAP** (optional auth provider): helpers in `app.auth.*`, gated by `enable-login-with-ldap`.

View File

@ -29,9 +29,10 @@ bb scripts/paren-repair --help
## Native Tool Available (opencode) ## Native Tool Available (opencode)
A native opencode tool `paren-repair` is available at `.opencode/scripts/paren-repair.ts`. A native opencode tool `paren-repair` is available (defined in
The LLM can call it directly with: `.opencode/plugins/penpot.js`, which registers it for both opencode V1 via
- `files`: Array of file paths to fix `server()` and opencode V2 via `setup()`). The LLM can call it directly with:
- `files`: comma-separated file paths to fix (a string, not an array)
- `code`: Code string to fix via stdin - `code`: Code string to fix via stdin
Example usage by the LLM: Example usage by the LLM:

View File

@ -27,7 +27,9 @@ scripts/psql --host myhost --user myuser --db mydb
## Native Tool Available (opencode) ## Native Tool Available (opencode)
A native opencode tool `penpot-psql` is available. The LLM can call it directly A native opencode tool `penpot-psql` is available (defined in
`.opencode/plugins/penpot.js`, which registers it for both opencode V1 via
`server()` and opencode V2 via `setup()`). The LLM can call it directly
with: with:
- `sql`: SQL command string to execute - `sql`: SQL command string to execute
- `test`: Boolean flag to use the `penpot_test` database - `test`: Boolean flag to use the `penpot_test` database

View File

@ -23,33 +23,46 @@ file (never pipe tool output through filters).
- The store survives `node_modules` cleans. It is content-addressed and - The store survives `node_modules` cleans. It is content-addressed and
integrity-verified, so it cannot go stale; staleness lives in integrity-verified, so it cannot go stale; staleness lives in
node_modules. Only `scripts/clean-node-modules --store` removes it. node_modules. Only `scripts/clean-node-modules --store` removes it.
- Every `package.json` (about 35 of them) must carry a `packageManager` field - Every first-party `package.json` (35 files: workspace roots plus
with the identical `pnpm@<version>+sha512.<hash>` value. Do not let them drift. members) must carry a `packageManager` field with the identical
- CI pins no pnpm version; workflows rely on corepack reading `pnpm@<version>+sha512.<hash>` value. Do not let them drift.
`packageManager`. Fixing the fields fixes CI. `external/` (vendored trees with their own lifecycles), `.opencode/`,
`.pnpm-store/`, `bundles/` and `docker/images/bundle-*` copies are
never stamped.
- pnpm is a system binary everywhere (devenv image, CI runners, Docker
images). Nothing may call corepack: it is gone from Node 25+. pnpm
auto-downloads the `packageManager` version on mismatch
(`pmOnFail: download`, the default), so drift self-heals; aligned pins
just skip the download.
- The pnpm version pin lives in three places that move together:
`PNPM_VERSION` (+ arch SHAs) in `docker/devenv/Dockerfile` and in
`docker/images/Dockerfile.{media-processor,exporter,mcp}`, and the
`packageManager` fields (stamped by the script below). The
`plugins-deploy-*` workflows need no pnpm pin: a single `pnpm/setup`
step (`working-directory: plugins`, `install: false`) reads it from
the manifest and installs the pinned Node via `runtime: node@<exact>`;
bump that pin together with `.nvmrc` on Node updates.
## Procedure ## Procedure
1. Resolve the target tag first and note the version. Example: 1. Resolve the target version first and note it. Example:
`npm view pnpm dist-tags --json` for `next-12` (latest 12.x). The tag `pnpm view pnpm dist-tags --json` for `latest-12` (latest 12.x). The
moves over time; always re-check. tag moves over time; always re-check.
2. List every directory with a `package.json`, excluding `node_modules` 2. Bump `PNPM_VERSION` (+ arch SHAs) in `docker/devenv/Dockerfile` and in
(`fd -H -t f package.json -E node_modules`). This list is the work set; `docker/images/Dockerfile.{media-processor,exporter,mcp}`.
do not maintain a hand-written list. 3. From the repo root, on a host whose system pnpm is the target version
3. Run `corepack use pnpm@<tag>` in workspace roots first, then members. (rebuilt devenv), run `scripts/sync-pnpm-version`. It resolves the
`corepack use` stamps `packageManager` in the nearest package.json and integrity hash via `pnpm view` + node (no npm, no corepack, no
runs an install. Member runs repeat the workspace install; after the root python3) and stamps the identical field into all 35 files, replacing
run they are quick no-ops. only the value line (each file keeps its own indent) and inserting
4. If a run fails, fix the cause (see gotchas) and re-run that directory. the key after `"name"`/`"version"` where missing. Explicit version
instead: `scripts/sync-pnpm-version <version>`.
4. Run `scripts/sync-pnpm-version --install` (plain `pnpm install` in
each of the 11 workspace roots) to refresh lockfile metadata, then
fix any failing workspace (see gotchas) and re-run that directory.
## Gotchas ## Gotchas
- `corepack use` only updates an existing `packageManager` field. If a
package.json lacks the field, corepack walks up to the nearest ancestor
that has one and stamps that file instead; the member stays unstamped.
After the sweep, assert every package.json carries the field. For a
missing one, insert the identical `pnpm@<version>+sha512.<hash>` string,
then re-run `corepack use pnpm@<tag>` in that directory.
- A workspace may fail with `ERR_PNPM_IGNORED_BUILDS`, and pnpm then writes - A workspace may fail with `ERR_PNPM_IGNORED_BUILDS`, and pnpm then writes
a placeholder scaffold into its `pnpm-workspace.yaml`: a placeholder scaffold into its `pnpm-workspace.yaml`:
`allowBuilds: esbuild: set this to true or false`. Current pnpm writes `allowBuilds: esbuild: set this to true or false`. Current pnpm writes
@ -70,10 +83,12 @@ file (never pipe tool output through filters).
## Verification ## Verification
- Every `packageManager` field is byte-identical (same version and hash). - `scripts/sync-pnpm-version --check` passes: every `packageManager`
- `pnpm --version` in each workspace prints the target version. field is byte-identical (same version and hash).
- `pnpm install --frozen-lockfile` succeeds in each of the 11 workspaces. - `pnpm install --frozen-lockfile` succeeds in each of the 11 workspaces.
- `git diff` on lockfiles matches the expectations above. - `git diff` on lockfiles matches the expectations above.
- No `corepack` call remains in scripts, workflows, Dockerfiles or docs
(`rg corepack` shows only `CHANGES.md` history).
## Cleaning stale node_modules ## Cleaning stale node_modules

View File

@ -4,7 +4,7 @@
"license": "MPL-2.0", "license": "MPL-2.0",
"author": "Kaleidos INC Sucursal en España SL", "author": "Kaleidos INC Sucursal en España SL",
"private": true, "private": true,
"packageManager": "pnpm@12.3.4+sha512.961aa41fb077da3a04a441d9f8e15ebc0c96da8ef710b2eb67bf9ee7cb0610eabd48f1fd85f51cffe73846785fa0f87c56a3a872a1d893f8446741b5cce45457", "packageManager": "pnpm@12.5.1+sha512.e3f305bc784a2bc89f5ad3b6138889470fae8d2af5f36b61216ec91c2c3d64089775f9de38aac331044ea40f245cb0d5666392dfdf65824e1907ef6a2c62de5f",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/penpot/penpot" "url": "https://github.com/penpot/penpot"

131
backend/pnpm-lock.yaml generated
View File

@ -7,96 +7,153 @@ importers:
configDependencies: {} configDependencies: {}
packageManagerDependencies: packageManagerDependencies:
pnpm: pnpm:
specifier: 12.3.4 specifier: 12.5.1
version: 12.3.4 version: 12.5.1
packages: packages:
'@pnpm/exe.darwin-arm64@12.3.4': '@pnpm/exe.android-arm64@12.5.1':
resolution: {integrity: sha512-PAyUol8T1+/+ViOiXAt51ECA+QnfXCqz6foL4bW+LsoX0NcVd5XVEM2mRQu+LV4oc7uRz9zf9U0P+XFfuQeDAw==} resolution: {integrity: sha512-6LGBmQuDzUmgpHSuy+haG0jrmkwFNa4EuShDZqK3cIBv4Wfb75CzjJmowkzr+iJg40vnBKMQ62t4Ko6mwsId7A==}
cpu: [arm64]
os: [android]
'@pnpm/exe.android-x64@12.5.1':
resolution: {integrity: sha512-091oCltP3Rx89yxL1Wh5rXkUXEyFzyxIzSHKRFDyDFlQkb8Ox8FaDcm+M8PNkMyqnLA+qeGOLzY7Rjj9izl1Ww==}
cpu: [x64]
os: [android]
'@pnpm/exe.darwin-arm64@12.5.1':
resolution: {integrity: sha512-r1vEMQPVjRexZMyL2HonxJoo3+nuB/+G+tg+f2PK6faRdXX1TDwevKXOxJAfqlPInTy6W/pm912+s+m0NcR4BQ==}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
'@pnpm/exe.darwin-x64@12.3.4': '@pnpm/exe.darwin-x64@12.5.1':
resolution: {integrity: sha512-fxP9JCk0Cdye+ePuj+GJJLMUMTqHGWRdb1dtv4How876uQ2ehxvenpgiYAir/ceO9PsYUZkFTtyZdx+rRu5QOA==} resolution: {integrity: sha512-YHwI1g2B+Y2cAEylOR31r781GWijdJL7Gy7TEn/ZpZ5v/Cuwp5/Pz0EFCgo0M2MKLafAbL/laUMGvOpAdnerAw==}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
'@pnpm/exe.linux-arm64-musl@12.3.4': '@pnpm/exe.freebsd-x64@12.5.1':
resolution: {integrity: sha512-FBOt0/7ye6O6q4AllVV5QMviB6qE6fqkeczV/+MDWQsmo+QJrlfsh6X7CpH/tClVpBZEyIbjpUoT8bNhCYBxEg==} resolution: {integrity: sha512-sFUMHiYFs6q5syFwgaVppzhQSmqnXoJIz/YwTXIOXTDE76ndIpTRpDGAnBCBKjsaEBnqjJA/2l6hpJcNDNoMiA==}
cpu: [x64]
os: [freebsd]
'@pnpm/exe.linux-arm64-musl@12.5.1':
resolution: {integrity: sha512-zupuxYZBuBEGjUz805/4/Asq01UNziFADANt8yp+LdUjU9bbMa3xY78Bx1PsGDqBy7aiad4sdUMK8ZxH4n4gmQ==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl] libc: [musl]
'@pnpm/exe.linux-arm64@12.3.4': '@pnpm/exe.linux-arm64@12.5.1':
resolution: {integrity: sha512-t71AVA7LRqiKTyZ5xMYaZc2n5DfdpMbfokZuiIOXHBOM03ECnF0t4iYwaBDqJgVjlKYUOwaF/bRQajGNA4cJ4w==} resolution: {integrity: sha512-6hbMWW2/eQo1b5wvQPe/ndLHVTH88Rav6N+ffD4/h6IuFMjWCgasFYYsFv0GrvpqREAx3SwmZSvli+0NFXqbxA==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@pnpm/exe.linux-x64-musl@12.3.4': '@pnpm/exe.linux-ppc64@12.5.1':
resolution: {integrity: sha512-RPmk7Jb/aYaFvL2iyDN/AtMY+hUEsue732WmXpcuQ9tBpMnGyA5py7Z3+e+qmQaJ0zY/4ni9jJiyPBQHujmv6w==} resolution: {integrity: sha512-Gg3maDzaE/ZFMV9HIx/BwMN0Y6QbEs13URfxmiYHtKjjLAs5wEQZRMUP87YNLYO7cNaEfbM8GRbS9vPosyD9Gw==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@pnpm/exe.linux-riscv64@12.5.1':
resolution: {integrity: sha512-6fgWxXYMBMsWQ8DAiRevscX4QZQjwh+FsyCj8ZOk8pMXvqeq84MqBLCmS6KckvnbdqJ+HlSVQ7fs/kd0ldwfhA==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@pnpm/exe.linux-s390x@12.5.1':
resolution: {integrity: sha512-nzMZruRvxDhEfYUhzu+7EmM39+82Dgagc/6hPFPqGqzXNGcIG0anuxbUxfeNtI64SHt91KuuYUmCB/inlM+Pag==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@pnpm/exe.linux-x64-musl@12.5.1':
resolution: {integrity: sha512-kHCbegdTNHpzmPtUD4S5fqkVkqgdQR47spF9p2VPP3dIIkZl6Ytk8w0vhlAsAGgge8FmucfwX+vvPKgm0NtOCQ==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl] libc: [musl]
'@pnpm/exe.linux-x64@12.3.4': '@pnpm/exe.linux-x64@12.5.1':
resolution: {integrity: sha512-2ZqOlSPkfwX1h5cR+FPiWf8+F+2hZT/3TvhUK5sigHqwaQCIiq8R7CGxhndKs63JtcLi2a1Qpo+wX/EoyfjyJQ==} resolution: {integrity: sha512-3PkUBYo5z4dgtlnTNIFj7QGpcDUAuqXz9WGVigPDCeccEnhGiRkWmA5102TmYJHtwJP3LfmE+ZF9PGeWhn8p9Q==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@pnpm/exe.win32-arm64@12.3.4': '@pnpm/exe.win32-arm64@12.5.1':
resolution: {integrity: sha512-ANyrHqyqco6SXBysUTRF74itDyyraea7IbFsKFdNXTjcFnfycTDx37EwuhdpPYFNSIh2JhUG4fByclsRfiHX7w==} resolution: {integrity: sha512-49NvjZIXHly56/PLoEDD7VphbO4N8dwMXoApElDFCpVGDNGeKlwlib1tXtnpTF6i2/XIsAj7GgatHB5KHXgBnA==}
cpu: [arm64] cpu: [arm64]
os: [win32] os: [win32]
'@pnpm/exe.win32-x64@12.3.4': '@pnpm/exe.win32-x64@12.5.1':
resolution: {integrity: sha512-WH/KqBPY/hq2Tb7SgQltEZytimcjgKRaCRL/aM9CI0c67iKc5TVmHUhIiL3Ux9FB4bWn36i6XewUcScQI+zG8w==} resolution: {integrity: sha512-ePUmQFPAPJFNMwg1lelDx2PdjHqYRiF0yfg5e6qeDbPNOu97QNo557Ru/SxmCrmt8ydQWNuZXWpdEp019md5vw==}
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
pnpm@12.3.4: pnpm@12.5.1:
resolution: {integrity: sha512-lhqkH7B32joEpEHZ+OFevAyW2o73ELLrZ7+e58sGEOq9SPH9hfUc/+c4RnhfoPh8VqOocqHYk/hEZ0G1zORUVw==} resolution: {integrity: sha512-4/MFvHhKK8ifWtO2E4iJRw+ujSr182thIW7JHCw9ZAiXdfneOKrDMQROpA8kXLDVZmOS399lgk4ZB+9qLGLeXw==}
engines: {node: '>=18.*'} engines: {node: '>=18.*'}
hasBin: true hasBin: true
snapshots: snapshots:
'@pnpm/exe.darwin-arm64@12.3.4': '@pnpm/exe.android-arm64@12.5.1':
optional: true optional: true
'@pnpm/exe.darwin-x64@12.3.4': '@pnpm/exe.android-x64@12.5.1':
optional: true optional: true
'@pnpm/exe.linux-arm64-musl@12.3.4': '@pnpm/exe.darwin-arm64@12.5.1':
optional: true optional: true
'@pnpm/exe.linux-arm64@12.3.4': '@pnpm/exe.darwin-x64@12.5.1':
optional: true optional: true
'@pnpm/exe.linux-x64-musl@12.3.4': '@pnpm/exe.freebsd-x64@12.5.1':
optional: true optional: true
'@pnpm/exe.linux-x64@12.3.4': '@pnpm/exe.linux-arm64-musl@12.5.1':
optional: true optional: true
'@pnpm/exe.win32-arm64@12.3.4': '@pnpm/exe.linux-arm64@12.5.1':
optional: true optional: true
'@pnpm/exe.win32-x64@12.3.4': '@pnpm/exe.linux-ppc64@12.5.1':
optional: true optional: true
pnpm@12.3.4: '@pnpm/exe.linux-riscv64@12.5.1':
optional: true
'@pnpm/exe.linux-s390x@12.5.1':
optional: true
'@pnpm/exe.linux-x64-musl@12.5.1':
optional: true
'@pnpm/exe.linux-x64@12.5.1':
optional: true
'@pnpm/exe.win32-arm64@12.5.1':
optional: true
'@pnpm/exe.win32-x64@12.5.1':
optional: true
pnpm@12.5.1:
optionalDependencies: optionalDependencies:
'@pnpm/exe.darwin-arm64': 12.3.4 '@pnpm/exe.android-arm64': 12.5.1
'@pnpm/exe.darwin-x64': 12.3.4 '@pnpm/exe.android-x64': 12.5.1
'@pnpm/exe.linux-arm64': 12.3.4 '@pnpm/exe.darwin-arm64': 12.5.1
'@pnpm/exe.linux-arm64-musl': 12.3.4 '@pnpm/exe.darwin-x64': 12.5.1
'@pnpm/exe.linux-x64': 12.3.4 '@pnpm/exe.freebsd-x64': 12.5.1
'@pnpm/exe.linux-x64-musl': 12.3.4 '@pnpm/exe.linux-arm64': 12.5.1
'@pnpm/exe.win32-arm64': 12.3.4 '@pnpm/exe.linux-arm64-musl': 12.5.1
'@pnpm/exe.win32-x64': 12.3.4 '@pnpm/exe.linux-ppc64': 12.5.1
'@pnpm/exe.linux-riscv64': 12.5.1
'@pnpm/exe.linux-s390x': 12.5.1
'@pnpm/exe.linux-x64': 12.5.1
'@pnpm/exe.linux-x64-musl': 12.5.1
'@pnpm/exe.win32-arm64': 12.5.1
'@pnpm/exe.win32-x64': 12.5.1
--- ---
lockfileVersion: '9.0' lockfileVersion: '9.0'

View File

@ -97,13 +97,31 @@ export JAVA_OPTS="\
--enable-native-access=ALL-UNNAMED \ --enable-native-access=ALL-UNNAMED \
--add-opens=java.base/java.nio=ALL-UNNAMED"; --add-opens=java.base/java.nio=ALL-UNNAMED";
function setup_minio() { function setup_s3_bucket() {
if [ "${PENPOT_OBJECTS_STORAGE_BACKEND}" != "s3" ]; then if [ "${PENPOT_OBJECTS_STORAGE_BACKEND}" != "s3" ]; then
return 0 return 0
fi fi
# Shared MinIO user/policy provisioning is handled by docker-compose.infra.yml. local attempts=0
# Per process startup only ensures that the configured bucket exists. local endpoint="${PENPOT_OBJECTS_STORAGE_S3_ENDPOINT}"
mc alias set penpot-s3/ "${PENPOT_OBJECTS_STORAGE_S3_ENDPOINT}" minioadmin minioadmin -q local bucket="${PENPOT_OBJECTS_STORAGE_S3_BUCKET}"
mc mb "penpot-s3/${PENPOT_OBJECTS_STORAGE_S3_BUCKET}" -p -q local region="${PENPOT_OBJECTS_STORAGE_S3_REGION}"
until aws s3api head-bucket \
--endpoint-url "$endpoint" \
--region "$region" \
--bucket "$bucket" >/dev/null 2>&1; do
if aws s3 mb "s3://${bucket}" \
--endpoint-url "$endpoint" \
--region "$region" >/dev/null 2>&1; then
return 0
fi
attempts=$((attempts + 1))
if [ "$attempts" -ge 30 ]; then
echo "setup_s3_bucket: unable to create s3://${bucket} after ${attempts} attempts" >&2
return 1
fi
sleep 1
done
} }

View File

@ -7,8 +7,8 @@ if [ -f $SCRIPT_DIR/_env.local ]; then
source $SCRIPT_DIR/_env.local; source $SCRIPT_DIR/_env.local;
fi fi
# Initialize MINIO config # Ensure the object storage bucket exists.
setup_minio; setup_s3_bucket;
export JAVA_OPTS="$JAVA_OPTS -Dlog4j2.configurationFile=log4j2-devenv-repl.xml" export JAVA_OPTS="$JAVA_OPTS -Dlog4j2.configurationFile=log4j2-devenv-repl.xml"
export OPTIONS="-A:jmx-remote -A:dev" export OPTIONS="-A:jmx-remote -A:dev"

View File

@ -8,6 +8,9 @@ if [ -f $SCRIPT_DIR/_env.local ]; then
source $SCRIPT_DIR/_env.local; source $SCRIPT_DIR/_env.local;
fi fi
# Ensure the object storage bucket exists.
setup_s3_bucket;
export OPTIONS="-A:dev" export OPTIONS="-A:dev"
entrypoint=${1:-app.main}; entrypoint=${1:-app.main};

View File

@ -7,8 +7,8 @@ if [ -f $SCRIPT_DIR/_env.local ]; then
source $SCRIPT_DIR/_env.local; source $SCRIPT_DIR/_env.local;
fi fi
# Initialize MINIO config # Ensure the object storage bucket exists.
setup_minio; setup_s3_bucket;
shift 1; shift 1;
set -ex set -ex

View File

@ -75,7 +75,7 @@
[:map {:title "create-file"} [:map {:title "create-file"}
[:name [:string {:max 250}]] [:name [:string {:max 250}]]
[:project-id ::sm/uuid] [:project-id ::sm/uuid]
[:id {:optional true} ::sm/uuid] [:id {:optional true} ::sm/user-provided-uuid]
[:is-shared {:optional true} ::sm/boolean] [:is-shared {:optional true} ::sm/boolean]
[:features {:optional true} ::cfeat/features]]) [:features {:optional true} ::cfeat/features]])

View File

@ -48,7 +48,7 @@
(def ^:private schema:upload-file-media-object (def ^:private schema:upload-file-media-object
[:map {:title "upload-file-media-object"} [:map {:title "upload-file-media-object"}
[:id {:optional true} ::sm/uuid] [:id {:optional true} ::sm/user-provided-uuid]
[:file-id ::sm/uuid] [:file-id ::sm/uuid]
[:is-local ::sm/boolean] [:is-local ::sm/boolean]
[:name [:string {:max 250}]] [:name [:string {:max 250}]]
@ -225,7 +225,7 @@
[:file-id ::sm/uuid] [:file-id ::sm/uuid]
[:is-local ::sm/boolean] [:is-local ::sm/boolean]
[:url ::sm/uri] [:url ::sm/uri]
[:id {:optional true} ::sm/uuid] [:id {:optional true} ::sm/user-provided-uuid]
[:name {:optional true} [:string {:max 250}]]]) [:name {:optional true} [:string {:max 250}]]])
(sv/defmethod ::create-file-media-object-from-url (sv/defmethod ::create-file-media-object-from-url
@ -556,7 +556,7 @@
[:is-local ::sm/boolean] [:is-local ::sm/boolean]
[:name [:string {:max 250}]] [:name [:string {:max 250}]]
[:mtype :string] [:mtype :string]
[:id {:optional true} ::sm/uuid]]) [:id {:optional true} ::sm/user-provided-uuid]])
(sv/defmethod ::assemble-file-media-object (sv/defmethod ::assemble-file-media-object
{::doc/added "2.17" {::doc/added "2.17"

View File

@ -199,7 +199,7 @@
[:map {:title "create-project"} [:map {:title "create-project"}
[:team-id ::sm/uuid] [:team-id ::sm/uuid]
[:name [:string {:max 250 :min 1}]] [:name [:string {:max 250 :min 1}]]
[:id {:optional true} ::sm/uuid]]) [:id {:optional true} ::sm/user-provided-uuid]])
(sv/defmethod ::create-project (sv/defmethod ::create-project
{::doc/added "1.18" {::doc/added "1.18"

View File

@ -523,7 +523,7 @@
[:map {:title "create-team"} [:map {:title "create-team"}
[:name types.team/schema:team-name] [:name types.team/schema:team-name]
[:features {:optional true} ::cfeat/features] [:features {:optional true} ::cfeat/features]
[:id {:optional true} ::sm/uuid] [:id {:optional true} ::sm/user-provided-uuid]
[:organization-id {:optional true} ::sm/uuid] [:organization-id {:optional true} ::sm/uuid]
[:is-default {:optional true} :boolean]]) [:is-default {:optional true} :boolean]])

View File

@ -538,7 +538,7 @@
[:map {:title "create-team-with-invitations"} [:map {:title "create-team-with-invitations"}
[:name [:string {:max 250}]] [:name [:string {:max 250}]]
[:features {:optional true} ::cfeat/features] [:features {:optional true} ::cfeat/features]
[:id {:optional true} ::sm/uuid] [:id {:optional true} ::sm/user-provided-uuid]
[:emails [::sm/set ::sm/email]] [:emails [::sm/set ::sm/email]]
[:role types.team/schema:role]]) [:role types.team/schema:role]])

View File

@ -166,6 +166,33 @@
(t/is (th/ex-info? err)) (t/is (th/ex-info? err))
(t/is (th/ex-of-type? err :not-found)))))) (t/is (th/ex-of-type? err :not-found))))))
(t/deftest create-file-id-version
(let [prof (th/create-profile* 1 {:is-active true})
proj-id (:default-project-id prof)
v3-id "6fa459ea-ee8a-3ca4-894e-db77e160355e"
v4-id "550e8400-e29b-41d4-a716-446655440000"]
;; reserved version (v3) must be rejected at the RPC boundary
(let [data {::th/type :create-file
::rpc/profile-id (:id prof)
:project-id proj-id
:id v3-id
:name "file with v3 id"}
out (th/command! data)]
(t/is (not (th/success? out)))
(t/is (th/ex-of-type? (:error out) :validation))
(t/is (th/ex-of-code? (:error out) :params-validation)))
;; v4 id is accepted
(let [data {::th/type :create-file
::rpc/profile-id (:id prof)
:project-id proj-id
:id v4-id
:name "file with v4 id"}
out (th/command! data)]
(t/is (th/success? out))
(t/is (= v4-id (str (:id (:result out))))))))
(t/deftest file-gc-with-fragments (t/deftest file-gc-with-fragments
(let [profile (th/create-profile* 1) (let [profile (th/create-profile* 1)
file (th/create-file* 1 {:profile-id (:id profile) file (th/create-file* 1 {:profile-id (:id profile)

View File

@ -138,6 +138,66 @@
(t/is (uuid? (:thumbnail-id result)))))) (t/is (uuid? (:thumbnail-id result))))))
(t/deftest upload-file-media-object-id-version
(let [prof (th/create-profile* 1)
_ (th/create-project* 1 {:profile-id (:id prof)
:team-id (:default-team-id prof)})
file (th/create-file* 1 {:profile-id (:id prof)
:project-id (:default-project-id prof)
:is-shared false})
mfile {:filename "sample.jpg"
:path (th/tempfile "backend_tests/test_files/sample.jpg")
:mtype "image/jpeg"
:size 312043}
v3-id "6fa459ea-ee8a-3ca4-894e-db77e160355e"
v4-id "550e8400-e29b-41d4-a716-446655440000"]
;; reserved version (v3) must be rejected at the RPC boundary
(let [params {::th/type :upload-file-media-object
::rpc/profile-id (:id prof)
:file-id (:id file)
:is-local true
:name "testfile"
:content mfile
:id v3-id}
out (th/command! params)]
(t/is (not (th/success? out)))
(t/is (th/ex-of-type? (:error out) :validation))
(t/is (th/ex-of-code? (:error out) :params-validation)))
;; v4 id is accepted
(let [params {::th/type :upload-file-media-object
::rpc/profile-id (:id prof)
:file-id (:id file)
:is-local true
:name "testfile"
:content mfile
:id v4-id}
out (th/command! params)]
(t/is (th/success? out))
(t/is (= v4-id (str (:id (:result out))))))))
(t/deftest create-file-media-object-from-url-id-version
(let [prof (th/create-profile* 1)
_ (th/create-project* 1 {:profile-id (:id prof)
:team-id (:default-team-id prof)})
file (th/create-file* 1 {:profile-id (:id prof)
:project-id (:default-project-id prof)
:is-shared false})
v3-id "6fa459ea-ee8a-3ca4-894e-db77e160355e"]
;; reserved version (v3) must be rejected before any download happens
(let [params {::th/type :create-file-media-object-from-url
::rpc/profile-id (:id prof)
:file-id (:id file)
:is-local true
:url "https://example.com/sample.jpg"
:id v3-id}
out (th/command! params)]
(t/is (not (th/success? out)))
(t/is (th/ex-of-type? (:error out) :validation))
(t/is (th/ex-of-code? (:error out) :params-validation)))))
(t/deftest media-object-from-url-command (t/deftest media-object-from-url-command
(let [prof (th/create-profile* 1) (let [prof (th/create-profile* 1)
proj (th/create-project* 1 {:profile-id (:id prof) proj (th/create-project* 1 {:profile-id (:id prof)
@ -1262,3 +1322,48 @@
(t/is (th/ex-info? error)) (t/is (th/ex-info? error))
(t/is (= :not-found (:type error-data))) (t/is (= :not-found (:type error-data)))
(t/is (= :object-not-found (:code error-data)))))) (t/is (= :object-not-found (:code error-data))))))
(t/deftest assemble-file-media-object-id-version
(let [prof (th/create-profile* 1)
_ (th/create-project* 1 {:profile-id (:id prof)
:team-id (:default-team-id prof)})
file (th/create-file* 1 {:profile-id (:id prof)
:project-id (:default-project-id prof)
:is-shared false})
v3-id "6fa459ea-ee8a-3ca4-894e-db77e160355e"
v4-id "550e8400-e29b-41d4-a716-446655440000"
mtype "image/jpeg"]
;; reserved version (v3) must be rejected without touching any session
(let [out (th/command! {::th/type :assemble-file-media-object
::rpc/profile-id (:id prof)
:session-id (uuid/next)
:file-id (:id file)
:is-local true
:name "assembled-image"
:mtype mtype
:id v3-id})]
(t/is (not (th/success? out)))
(t/is (th/ex-of-type? (:error out) :validation))
(t/is (th/ex-of-code? (:error out) :params-validation)))
;; v4 id is accepted through the full chunked flow
(let [source-path (th/tempfile "backend_tests/test_files/sample.jpg")
chunks (split-file-into-chunks source-path 312043)
session-id (create-session! prof 1)
mfile (make-chunk-mfile (first chunks) mtype)
_ (th/command! {::th/type :upload-chunk
::rpc/profile-id (:id prof)
:session-id session-id
:index 0
:content mfile})
out (th/command! {::th/type :assemble-file-media-object
::rpc/profile-id (:id prof)
:session-id session-id
:file-id (:id file)
:is-local true
:name "assembled-image"
:mtype mtype
:id v4-id})]
(t/is (th/success? out))
(t/is (= v4-id (str (:id (:result out))))))))

View File

@ -123,6 +123,33 @@
(t/is (th/ex-info? error)) (t/is (th/ex-info? error))
(t/is (th/ex-of-type? error :not-found)))) (t/is (th/ex-of-type? error :not-found))))
(t/deftest create-project-id-version
(let [profile (th/create-profile* 1)
team (th/create-team* 1 {:profile-id (:id profile)})
v3-id "6fa459ea-ee8a-3ca4-894e-db77e160355e"
v4-id "550e8400-e29b-41d4-a716-446655440000"]
;; reserved version (v3) must be rejected at the RPC boundary
(let [data {::th/type :create-project
::rpc/profile-id (:id profile)
:team-id (:id team)
:id v3-id
:name "project with v3 id"}
out (th/command! data)]
(t/is (not (th/success? out)))
(t/is (th/ex-of-type? (:error out) :validation))
(t/is (th/ex-of-code? (:error out) :params-validation)))
;; v4 id is accepted
(let [data {::th/type :create-project
::rpc/profile-id (:id profile)
:team-id (:id team)
:id v4-id
:name "project with v4 id"}
out (th/command! data)]
(t/is (th/success? out))
(t/is (= v4-id (str (:id (:result out))))))))
(t/deftest permissions-checks-rename-project (t/deftest permissions-checks-rename-project
(let [profile1 (th/create-profile* 1) (let [profile1 (th/create-profile* 1)
profile2 (th/create-profile* 2) profile2 (th/create-profile* 2)

View File

@ -1116,6 +1116,62 @@
out (th/command! data)] out (th/command! data)]
(t/is (th/success? out))))) (t/is (th/success? out)))))
(t/deftest create-team-id-version
(let [profile (th/create-profile* 1 {:is-active true})
v3-id "6fa459ea-ee8a-3ca4-894e-db77e160355e"
v4-id "550e8400-e29b-41d4-a716-446655440000"
v7-id "0191062e-3f50-7a5e-9f5a-1a2b3c4d5e6f"]
;; reserved version (v3) must be rejected at the RPC boundary
(let [data {::th/type :create-team
::rpc/profile-id (:id profile)
:name "team with v3 id"
:id v3-id}
out (th/command! data)]
(t/is (not (th/success? out)))
(t/is (th/ex-of-type? (:error out) :validation))
(t/is (th/ex-of-code? (:error out) :params-validation)))
;; v4, v7 and v8 ids are accepted
(doseq [id [v4-id v7-id (str (uuid/next))]]
(let [data {::th/type :create-team
::rpc/profile-id (:id profile)
:name (str "team with id " id)
:id id}
out (th/command! data)]
(t/is (th/success? out))
(t/is (= id (str (:id (:result out)))))))))
(t/deftest create-team-with-invitations-id-version
(with-mocks [mock {:target 'app.email/send! :return nil}]
(let [profile (th/create-profile* 1 {:is-active true})
v3-id "6fa459ea-ee8a-3ca4-894e-db77e160355e"
v4-id "550e8400-e29b-41d4-a716-446655440000"]
;; reserved version (v3) must be rejected before any invitation is sent
(let [data {::th/type :create-team-with-invitations
::rpc/profile-id (:id profile)
:name "team with v3 id"
:id v3-id
:emails #{"invitee@example.com"}
:role :editor}
out (th/command! data)]
(t/is (not (th/success? out)))
(t/is (th/ex-of-type? (:error out) :validation))
(t/is (th/ex-of-code? (:error out) :params-validation))
(t/is (= 0 (:call-count @mock))))
;; v4 id is accepted
(let [data {::th/type :create-team-with-invitations
::rpc/profile-id (:id profile)
:name "team with v4 id"
:id v4-id
:emails #{"invitee@example.com"}
:role :editor}
out (th/command! data)]
(t/is (th/success? out))
(t/is (= v4-id (str (:id (:result out)))))))))
(t/deftest create-team-invitations-email-cooldown (t/deftest create-team-invitations-email-cooldown
(with-mocks [mock {:target 'app.email/send! :return nil}] (with-mocks [mock {:target 'app.email/send! :return nil}]
(let [profile1 (th/create-profile* 1 {:is-active true}) (let [profile1 (th/create-profile* 1 {:is-active true})

View File

@ -4,7 +4,7 @@
"license": "MPL-2.0", "license": "MPL-2.0",
"author": "Kaleidos INC Sucursal en España SL", "author": "Kaleidos INC Sucursal en España SL",
"private": true, "private": true,
"packageManager": "pnpm@12.3.4+sha512.961aa41fb077da3a04a441d9f8e15ebc0c96da8ef710b2eb67bf9ee7cb0610eabd48f1fd85f51cffe73846785fa0f87c56a3a872a1d893f8446741b5cce45457", "packageManager": "pnpm@12.5.1+sha512.e3f305bc784a2bc89f5ad3b6138889470fae8d2af5f36b61216ec91c2c3d64089775f9de38aac331044ea40f245cb0d5666392dfdf65824e1907ef6a2c62de5f",
"type": "module", "type": "module",
"repository": { "repository": {
"type": "git", "type": "git",

131
common/pnpm-lock.yaml generated
View File

@ -7,96 +7,153 @@ importers:
configDependencies: {} configDependencies: {}
packageManagerDependencies: packageManagerDependencies:
pnpm: pnpm:
specifier: 12.3.4 specifier: 12.5.1
version: 12.3.4 version: 12.5.1
packages: packages:
'@pnpm/exe.darwin-arm64@12.3.4': '@pnpm/exe.android-arm64@12.5.1':
resolution: {integrity: sha512-PAyUol8T1+/+ViOiXAt51ECA+QnfXCqz6foL4bW+LsoX0NcVd5XVEM2mRQu+LV4oc7uRz9zf9U0P+XFfuQeDAw==} resolution: {integrity: sha512-6LGBmQuDzUmgpHSuy+haG0jrmkwFNa4EuShDZqK3cIBv4Wfb75CzjJmowkzr+iJg40vnBKMQ62t4Ko6mwsId7A==}
cpu: [arm64]
os: [android]
'@pnpm/exe.android-x64@12.5.1':
resolution: {integrity: sha512-091oCltP3Rx89yxL1Wh5rXkUXEyFzyxIzSHKRFDyDFlQkb8Ox8FaDcm+M8PNkMyqnLA+qeGOLzY7Rjj9izl1Ww==}
cpu: [x64]
os: [android]
'@pnpm/exe.darwin-arm64@12.5.1':
resolution: {integrity: sha512-r1vEMQPVjRexZMyL2HonxJoo3+nuB/+G+tg+f2PK6faRdXX1TDwevKXOxJAfqlPInTy6W/pm912+s+m0NcR4BQ==}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
'@pnpm/exe.darwin-x64@12.3.4': '@pnpm/exe.darwin-x64@12.5.1':
resolution: {integrity: sha512-fxP9JCk0Cdye+ePuj+GJJLMUMTqHGWRdb1dtv4How876uQ2ehxvenpgiYAir/ceO9PsYUZkFTtyZdx+rRu5QOA==} resolution: {integrity: sha512-YHwI1g2B+Y2cAEylOR31r781GWijdJL7Gy7TEn/ZpZ5v/Cuwp5/Pz0EFCgo0M2MKLafAbL/laUMGvOpAdnerAw==}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
'@pnpm/exe.linux-arm64-musl@12.3.4': '@pnpm/exe.freebsd-x64@12.5.1':
resolution: {integrity: sha512-FBOt0/7ye6O6q4AllVV5QMviB6qE6fqkeczV/+MDWQsmo+QJrlfsh6X7CpH/tClVpBZEyIbjpUoT8bNhCYBxEg==} resolution: {integrity: sha512-sFUMHiYFs6q5syFwgaVppzhQSmqnXoJIz/YwTXIOXTDE76ndIpTRpDGAnBCBKjsaEBnqjJA/2l6hpJcNDNoMiA==}
cpu: [x64]
os: [freebsd]
'@pnpm/exe.linux-arm64-musl@12.5.1':
resolution: {integrity: sha512-zupuxYZBuBEGjUz805/4/Asq01UNziFADANt8yp+LdUjU9bbMa3xY78Bx1PsGDqBy7aiad4sdUMK8ZxH4n4gmQ==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl] libc: [musl]
'@pnpm/exe.linux-arm64@12.3.4': '@pnpm/exe.linux-arm64@12.5.1':
resolution: {integrity: sha512-t71AVA7LRqiKTyZ5xMYaZc2n5DfdpMbfokZuiIOXHBOM03ECnF0t4iYwaBDqJgVjlKYUOwaF/bRQajGNA4cJ4w==} resolution: {integrity: sha512-6hbMWW2/eQo1b5wvQPe/ndLHVTH88Rav6N+ffD4/h6IuFMjWCgasFYYsFv0GrvpqREAx3SwmZSvli+0NFXqbxA==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@pnpm/exe.linux-x64-musl@12.3.4': '@pnpm/exe.linux-ppc64@12.5.1':
resolution: {integrity: sha512-RPmk7Jb/aYaFvL2iyDN/AtMY+hUEsue732WmXpcuQ9tBpMnGyA5py7Z3+e+qmQaJ0zY/4ni9jJiyPBQHujmv6w==} resolution: {integrity: sha512-Gg3maDzaE/ZFMV9HIx/BwMN0Y6QbEs13URfxmiYHtKjjLAs5wEQZRMUP87YNLYO7cNaEfbM8GRbS9vPosyD9Gw==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@pnpm/exe.linux-riscv64@12.5.1':
resolution: {integrity: sha512-6fgWxXYMBMsWQ8DAiRevscX4QZQjwh+FsyCj8ZOk8pMXvqeq84MqBLCmS6KckvnbdqJ+HlSVQ7fs/kd0ldwfhA==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@pnpm/exe.linux-s390x@12.5.1':
resolution: {integrity: sha512-nzMZruRvxDhEfYUhzu+7EmM39+82Dgagc/6hPFPqGqzXNGcIG0anuxbUxfeNtI64SHt91KuuYUmCB/inlM+Pag==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@pnpm/exe.linux-x64-musl@12.5.1':
resolution: {integrity: sha512-kHCbegdTNHpzmPtUD4S5fqkVkqgdQR47spF9p2VPP3dIIkZl6Ytk8w0vhlAsAGgge8FmucfwX+vvPKgm0NtOCQ==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl] libc: [musl]
'@pnpm/exe.linux-x64@12.3.4': '@pnpm/exe.linux-x64@12.5.1':
resolution: {integrity: sha512-2ZqOlSPkfwX1h5cR+FPiWf8+F+2hZT/3TvhUK5sigHqwaQCIiq8R7CGxhndKs63JtcLi2a1Qpo+wX/EoyfjyJQ==} resolution: {integrity: sha512-3PkUBYo5z4dgtlnTNIFj7QGpcDUAuqXz9WGVigPDCeccEnhGiRkWmA5102TmYJHtwJP3LfmE+ZF9PGeWhn8p9Q==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@pnpm/exe.win32-arm64@12.3.4': '@pnpm/exe.win32-arm64@12.5.1':
resolution: {integrity: sha512-ANyrHqyqco6SXBysUTRF74itDyyraea7IbFsKFdNXTjcFnfycTDx37EwuhdpPYFNSIh2JhUG4fByclsRfiHX7w==} resolution: {integrity: sha512-49NvjZIXHly56/PLoEDD7VphbO4N8dwMXoApElDFCpVGDNGeKlwlib1tXtnpTF6i2/XIsAj7GgatHB5KHXgBnA==}
cpu: [arm64] cpu: [arm64]
os: [win32] os: [win32]
'@pnpm/exe.win32-x64@12.3.4': '@pnpm/exe.win32-x64@12.5.1':
resolution: {integrity: sha512-WH/KqBPY/hq2Tb7SgQltEZytimcjgKRaCRL/aM9CI0c67iKc5TVmHUhIiL3Ux9FB4bWn36i6XewUcScQI+zG8w==} resolution: {integrity: sha512-ePUmQFPAPJFNMwg1lelDx2PdjHqYRiF0yfg5e6qeDbPNOu97QNo557Ru/SxmCrmt8ydQWNuZXWpdEp019md5vw==}
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
pnpm@12.3.4: pnpm@12.5.1:
resolution: {integrity: sha512-lhqkH7B32joEpEHZ+OFevAyW2o73ELLrZ7+e58sGEOq9SPH9hfUc/+c4RnhfoPh8VqOocqHYk/hEZ0G1zORUVw==} resolution: {integrity: sha512-4/MFvHhKK8ifWtO2E4iJRw+ujSr182thIW7JHCw9ZAiXdfneOKrDMQROpA8kXLDVZmOS399lgk4ZB+9qLGLeXw==}
engines: {node: '>=18.*'} engines: {node: '>=18.*'}
hasBin: true hasBin: true
snapshots: snapshots:
'@pnpm/exe.darwin-arm64@12.3.4': '@pnpm/exe.android-arm64@12.5.1':
optional: true optional: true
'@pnpm/exe.darwin-x64@12.3.4': '@pnpm/exe.android-x64@12.5.1':
optional: true optional: true
'@pnpm/exe.linux-arm64-musl@12.3.4': '@pnpm/exe.darwin-arm64@12.5.1':
optional: true optional: true
'@pnpm/exe.linux-arm64@12.3.4': '@pnpm/exe.darwin-x64@12.5.1':
optional: true optional: true
'@pnpm/exe.linux-x64-musl@12.3.4': '@pnpm/exe.freebsd-x64@12.5.1':
optional: true optional: true
'@pnpm/exe.linux-x64@12.3.4': '@pnpm/exe.linux-arm64-musl@12.5.1':
optional: true optional: true
'@pnpm/exe.win32-arm64@12.3.4': '@pnpm/exe.linux-arm64@12.5.1':
optional: true optional: true
'@pnpm/exe.win32-x64@12.3.4': '@pnpm/exe.linux-ppc64@12.5.1':
optional: true optional: true
pnpm@12.3.4: '@pnpm/exe.linux-riscv64@12.5.1':
optional: true
'@pnpm/exe.linux-s390x@12.5.1':
optional: true
'@pnpm/exe.linux-x64-musl@12.5.1':
optional: true
'@pnpm/exe.linux-x64@12.5.1':
optional: true
'@pnpm/exe.win32-arm64@12.5.1':
optional: true
'@pnpm/exe.win32-x64@12.5.1':
optional: true
pnpm@12.5.1:
optionalDependencies: optionalDependencies:
'@pnpm/exe.darwin-arm64': 12.3.4 '@pnpm/exe.android-arm64': 12.5.1
'@pnpm/exe.darwin-x64': 12.3.4 '@pnpm/exe.android-x64': 12.5.1
'@pnpm/exe.linux-arm64': 12.3.4 '@pnpm/exe.darwin-arm64': 12.5.1
'@pnpm/exe.linux-arm64-musl': 12.3.4 '@pnpm/exe.darwin-x64': 12.5.1
'@pnpm/exe.linux-x64': 12.3.4 '@pnpm/exe.freebsd-x64': 12.5.1
'@pnpm/exe.linux-x64-musl': 12.3.4 '@pnpm/exe.linux-arm64': 12.5.1
'@pnpm/exe.win32-arm64': 12.3.4 '@pnpm/exe.linux-arm64-musl': 12.5.1
'@pnpm/exe.win32-x64': 12.3.4 '@pnpm/exe.linux-ppc64': 12.5.1
'@pnpm/exe.linux-riscv64': 12.5.1
'@pnpm/exe.linux-s390x': 12.5.1
'@pnpm/exe.linux-x64': 12.5.1
'@pnpm/exe.linux-x64-musl': 12.5.1
'@pnpm/exe.win32-arm64': 12.5.1
'@pnpm/exe.win32-x64': 12.5.1
--- ---
lockfileVersion: '9.0' lockfileVersion: '9.0'

View File

@ -1,8 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -ex set -ex
corepack enable;
corepack install;
pnpm install; pnpm install;
pnpm run test; pnpm run test;
clojure -M:dev:test; clojure -M:dev:test;

View File

@ -448,6 +448,21 @@
::oapi/type "string" ::oapi/type "string"
::oapi/format "uuid"}}) ::oapi/format "uuid"}})
(register!
{:type ::user-provided-uuid
:pred uuid/user-provided?
:type-properties
{:title "user-provided-uuid"
:description "UUID provided by the user (v4, v7 or v8)"
:error/message "should be a user provided uuid (v4, v7 or v8)"
:gen/gen (sg/uuid)
:decode/string parse-uuid
:decode/json parse-uuid
:encode/string encode-uuid
:encode/json encode-uuid
::oapi/type "string"
::oapi/format "uuid"}})
;; Strict email regex aligned with app.common.spec/email-re. ;; Strict email regex aligned with app.common.spec/email-re.
;; Local part: valid RFC chars, no leading/trailing dot, no consecutive dots. ;; Local part: valid RFC chars, no leading/trailing dot, no consecutive dots.
;; Domain: labels can't start/end with hyphen, no empty labels. ;; Domain: labels can't start/end with hyphen, no empty labels.

View File

@ -70,6 +70,20 @@
[v] [v]
(= zero v)) (= zero v))
(def ^:private user-provided-regex
#"(?i)^[0-9a-f]{8}-[0-9a-f]{4}-[478][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$")
(defn user-provided?
"Returns true when `v` is a UUID instance penpot can accept when
it is provided by the user (v4 random, v7 time-ordered or v8 penpot
time-ordered). Anything else, including UUID strings, returns false;
strings are meant to be decoded into UUID instances first, mirroring
how ::sm/uuid behaves."
[v]
(boolean
(and (uuid? v)
(re-matches user-provided-regex (str v)))))
(defn get-word-high (defn get-word-high
[id] [id]
#?(:clj (.getMostSignificantBits ^UUID id) #?(:clj (.getMostSignificantBits ^UUID id)

View File

@ -9,6 +9,7 @@
[app.common.data :as d] [app.common.data :as d]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.common.schema.generators :as sg] [app.common.schema.generators :as sg]
[app.common.uuid :as uuid]
[clojure.test :as t])) [clojure.test :as t]))
(t/deftest test-set-of-email (t/deftest test-set-of-email
@ -245,3 +246,40 @@
(t/is (false? (sm/validate ::sm/email "user@"))) (t/is (false? (sm/validate ::sm/email "user@")))
(t/is (false? (sm/validate ::sm/email "userdomain.com"))) (t/is (false? (sm/validate ::sm/email "userdomain.com")))
(t/is (false? (sm/validate ::sm/email "user@@domain.com"))))) (t/is (false? (sm/validate ::sm/email "user@@domain.com")))))
(t/deftest test-user-provided-uuid
(let [v4 (uuid/uuid "550e8400-e29b-41d4-a716-446655440000")
v7 (uuid/uuid "0191062e-3f50-7a5e-9f5a-1a2b3c4d5e6f")
v8 (uuid/uuid "0227df82-63d7-8016-8005-48d9c0f33011")
v1 (uuid/uuid "6ba7b810-9dad-11d1-80b4-00c04fd430c8")
v3 (uuid/uuid "6fa459ea-ee8a-3ca4-894e-db77e160355e")
v5 (uuid/uuid "886313e1-3b8a-5372-9b90-0c9aee199e5d")]
(t/testing "accepts v4, v7 and v8 instances"
(t/is (true? (sm/validate ::sm/user-provided-uuid v4)))
(t/is (true? (sm/validate ::sm/user-provided-uuid v7)))
(t/is (true? (sm/validate ::sm/user-provided-uuid v8)))
(t/is (true? (sm/validate ::sm/user-provided-uuid (uuid/random))))
(t/is (true? (sm/validate ::sm/user-provided-uuid (uuid/next)))))
(t/testing "rejects reserved and other versions"
(t/is (false? (sm/validate ::sm/user-provided-uuid v1)))
(t/is (false? (sm/validate ::sm/user-provided-uuid v3)))
(t/is (false? (sm/validate ::sm/user-provided-uuid v5)))
(t/is (false? (sm/validate ::sm/user-provided-uuid uuid/zero)))
(t/is (false? (sm/validate ::sm/user-provided-uuid nil)))
(t/is (false? (sm/validate ::sm/user-provided-uuid "not-an-uuid"))))
(t/testing "decodes strings like the RPC layer does"
(let [decode (sm/decoder ::sm/user-provided-uuid sm/json-transformer)]
(t/is (= v4 (decode (str v4))))
(t/is (= v3 (decode (str v3))))
(t/is (true? (sm/validate ::sm/user-provided-uuid (decode (str v4)))))
(t/is (false? (sm/validate ::sm/user-provided-uuid (decode (str v3)))))))
(t/testing "rejects raw strings without decoding, like ::sm/uuid does"
(t/is (false? (sm/validate ::sm/user-provided-uuid (str v4))))
(t/is (false? (sm/validate ::sm/user-provided-uuid (str v3)))))
(t/testing "generate"
(t/is (true? (sm/validate ::sm/user-provided-uuid (sg/generate ::sm/user-provided-uuid)))))))

View File

@ -95,3 +95,46 @@
(t/is (= (nth expected 1) (aget parts 1))) (t/is (= (nth expected 1) (aget parts 1)))
(t/is (= (nth expected 2) (aget parts 2))) (t/is (= (nth expected 2) (aget parts 2)))
(t/is (= (nth expected 3) (aget parts 3)))))))) (t/is (= (nth expected 3) (aget parts 3))))))))
(t/deftest user-provided-test
;; The version is the first nibble of the 3rd group, the variant is
;; the first nibble of the 4th group (8, 9, a or b means RFC 4122).
(let [v4 (uuid/uuid "550e8400-e29b-41d4-a716-446655440000") ; v4, variant a: valid
v7 (uuid/uuid "0191062e-3f50-7a5e-9f5a-1a2b3c4d5e6f") ; v7, variant 9: valid
v8 (uuid/uuid "0227df82-63d7-8016-8005-48d9c0f33011") ; v8, variant 8: valid
v4-upper (uuid/uuid "550E8400-E29B-41D4-A716-446655440000") ; v4 uppercase, variant A: valid
v1 (uuid/uuid "6ba7b810-9dad-11d1-80b4-00c04fd430c8") ; v1 time-based: rejected
v3 (uuid/uuid "6fa459ea-ee8a-3ca4-894e-db77e160355e") ; v3 MD5 name-based (reserved): rejected
v5 (uuid/uuid "886313e1-3b8a-5372-9b90-0c9aee199e5d") ; v5 SHA-1 name-based: rejected
v6 (uuid/uuid "1e29effc-7a68-6d2c-9b6f-7f6a5f9b5b5b") ; v6 time-ordered: rejected
v4-bad-variant (uuid/uuid "550e8400-e29b-41d4-0716-446655440000")] ; v4 but variant 0 (NCS reserved): rejected
(t/testing "accepts v4, v7 and v8 instances"
(t/is (true? (uuid/user-provided? v4)))
(t/is (true? (uuid/user-provided? v7)))
(t/is (true? (uuid/user-provided? v8)))
(t/is (true? (uuid/user-provided? v4-upper)))
(t/is (true? (uuid/user-provided? (uuid/random)))) ; generates v4
(t/is (true? (uuid/user-provided? (uuid/next))))) ; generates v8
(t/testing "rejects strings, they must be decoded first"
(t/is (false? (uuid/user-provided? (str v4))))
(t/is (false? (uuid/user-provided? (str v7))))
(t/is (false? (uuid/user-provided? (str v8))))
(t/is (false? (uuid/user-provided? (str v3)))))
(t/testing "rejects nil uuid and reserved and other versions"
(t/is (false? (uuid/user-provided? uuid/zero))) ; version 0, variant 0
(t/is (false? (uuid/user-provided? v1)))
(t/is (false? (uuid/user-provided? v3)))
(t/is (false? (uuid/user-provided? v5)))
(t/is (false? (uuid/user-provided? v6)))
(t/is (false? (uuid/user-provided? (uuid/custom 1))))) ; version 0, variant 0
(t/testing "rejects right version with wrong variant"
(t/is (false? (uuid/user-provided? v4-bad-variant))))
(t/testing "rejects non-uuid values"
(t/is (false? (uuid/user-provided? nil)))
(t/is (false? (uuid/user-provided? 42)))
(t/is (false? (uuid/user-provided? "not-an-uuid"))))))

View File

@ -66,7 +66,8 @@ RUN set -ex; \
FROM base AS setup-node FROM base AS setup-node
ENV NODE_VERSION=v24.20.0 \ ENV NODE_VERSION=v24.21.0 \
PNPM_VERSION=12.5.1 \
PATH=/opt/node/bin:$PATH PATH=/opt/node/bin:$PATH
RUN set -eux; \ RUN set -eux; \
@ -75,10 +76,14 @@ RUN set -eux; \
aarch64|arm64) \ aarch64|arm64) \
OPENSSL_ARCH='linux-aarch64'; \ OPENSSL_ARCH='linux-aarch64'; \
BINARY_URL="https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-linux-arm64.tar.gz"; \ BINARY_URL="https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-linux-arm64.tar.gz"; \
PNPM_ARCH='arm64'; \
PNPM_SHA256='84e1290e82c800acd406b6db27e9650e15db3c2344d12162cc305ea1f942c6ff'; \
;; \ ;; \
amd64|x86_64) \ amd64|x86_64) \
OPENSSL_ARCH='linux-x86_64'; \ OPENSSL_ARCH='linux-x86_64'; \
BINARY_URL="https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-linux-x64.tar.gz"; \ BINARY_URL="https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-linux-x64.tar.gz"; \
PNPM_ARCH='x64'; \
PNPM_SHA256='5a397dfb6b3d4b07d3d7769586aeb471048faf04299a492e2808b95a9a1c701f'; \
;; \ ;; \
*) \ *) \
echo "Unsupported arch: ${ARCH}"; \ echo "Unsupported arch: ${ARCH}"; \
@ -89,10 +94,14 @@ RUN set -eux; \
mkdir -p /opt/node; \ mkdir -p /opt/node; \
cd /opt/node; \ cd /opt/node; \
tar -xf /tmp/nodejs.tar.gz --strip-components=1; \ tar -xf /tmp/nodejs.tar.gz --strip-components=1; \
PNPM_URL="https://github.com/pnpm/pnpm/releases/download/v${PNPM_VERSION}/pnpm-linux-${PNPM_ARCH}.tar.gz"; \
curl -LfsSo /tmp/pnpm.tar.gz "${PNPM_URL}"; \
echo "${PNPM_SHA256} */tmp/pnpm.tar.gz" | sha256sum -c -; \
tar -xzf /tmp/pnpm.tar.gz -C /opt/node/bin pnpm; \
chmod 755 /opt/node/bin/pnpm; \
chown -R root /opt/node; \ chown -R root /opt/node; \
find /opt/node/include/node/openssl/archs -mindepth 1 -maxdepth 1 ! -name "$OPENSSL_ARCH" -exec rm -rf {} \; ; \ find /opt/node/include/node/openssl/archs -mindepth 1 -maxdepth 1 ! -name "$OPENSSL_ARCH" -exec rm -rf {} \; ; \
corepack enable; \ rm -f /tmp/nodejs.tar.gz /tmp/pnpm.tar.gz;
rm -rf /tmp/nodejs.tar.gz;
################################################################################ ################################################################################
## OPENCODE SETUP ## OPENCODE SETUP
@ -100,7 +109,7 @@ RUN set -eux; \
FROM base AS setup-opencode FROM base AS setup-opencode
ENV OPENCODE_VERSION=1.18.25 ENV OPENCODE_VERSION=1.18.31
RUN set -ex; \ RUN set -ex; \
ARCH="$(dpkg --print-architecture)"; \ ARCH="$(dpkg --print-architecture)"; \
@ -121,6 +130,37 @@ RUN set -ex; \
chmod +x /tmp/opencode/opencode; \ chmod +x /tmp/opencode/opencode; \
rm -f /tmp/opencode.tar.gz; rm -f /tmp/opencode.tar.gz;
################################################################################
## OPENCODE2 SETUP (V2, side-by-side with V1 `opencode`)
################################################################################
FROM base AS setup-opencode2
ENV OPENCODE2_VERSION=2.0.12
RUN set -eux; \
ARCH="$(dpkg --print-architecture)"; \
case "${ARCH}" in \
aarch64|arm64) \
BINARY_URL="https://registry.npmjs.org/@opencode/cli-linux-arm64/-/cli-linux-arm64-${OPENCODE2_VERSION}.tgz"; \
ESUM='33f0dde9f0f055b6a3665d290371bcfc8c63daceade7a535c4b94d386f2bd917'; \
;; \
amd64|x86_64) \
BINARY_URL="https://registry.npmjs.org/@opencode/cli-linux-x64/-/cli-linux-x64-${OPENCODE2_VERSION}.tgz"; \
ESUM='2a79beb2e24382cb2bdbb709237dbc22f498cf52a106d029a6a3a3243760c78b'; \
;; \
*) \
echo "Unsupported arch: ${ARCH}"; exit 1; \
;; \
esac; \
curl -LfsSo /tmp/opencode2.tgz "${BINARY_URL}"; \
echo "${ESUM} */tmp/opencode2.tgz" | sha256sum -c -; \
mkdir -p /tmp/opencode2; \
tar -xzf /tmp/opencode2.tgz -C /tmp/opencode2 --strip-components=2 package/bin; \
mv /tmp/opencode2/opencode /tmp/opencode2/opencode2; \
chmod +x /tmp/opencode2/opencode2; \
rm -f /tmp/opencode2.tgz;
################################################################################ ################################################################################
## CADDYSERVER SETUP ## CADDYSERVER SETUP
@ -166,12 +206,12 @@ RUN set -eux; \
ARCH="$(dpkg --print-architecture)"; \ ARCH="$(dpkg --print-architecture)"; \
case "${ARCH}" in \ case "${ARCH}" in \
aarch64|arm64) \ aarch64|arm64) \
ESUM='5b222fce0b7076a10ac7ae3b1009a6c2caf4f35bc4e81de72010af6750c5e146'; \ ESUM='9fa5bf865783c43840101fcdc3a5222b60daf4fe11f26806247bf807c08aba38'; \
BINARY_URL='https://cdn.azul.com/zulu/bin/zulu26.32.13-ca-jdk26.0.2-linux_aarch64.tar.gz'; \ BINARY_URL='https://cdn.azul.com/zulu/bin/zulu27.28.101-ca-jdk27.0.0-linux_aarch64.tar.gz'; \
;; \ ;; \
amd64|x86_64) \ amd64|x86_64) \
ESUM='4b7c114917aebd0fc6284fc7111245d7747a4d9603bd12d86b384b1abc9d575d'; \ ESUM='ccbc15c4edbedfdcc03c2d29a2aa2c6daf9e6ffb4cda7dd5ece0fcbb37350267'; \
BINARY_URL='https://cdn.azul.com/zulu/bin/zulu26.32.13-ca-jdk26.0.2-linux_x64.tar.gz'; \ BINARY_URL='https://cdn.azul.com/zulu/bin/zulu27.28.101-ca-jdk27.0.0-linux_x64.tar.gz'; \
;; \ ;; \
*) \ *) \
echo "Unsupported arch: ${ARCH}"; \ echo "Unsupported arch: ${ARCH}"; \
@ -242,16 +282,17 @@ RUN set -eux; \
FROM base AS setup-utils FROM base AS setup-utils
ENV CLJKONDO_VERSION=2026.07.24 \ ENV CLJKONDO_VERSION=2026.08.04 \
BABASHKA_VERSION=1.13.219 \ BABASHKA_VERSION=1.13.223 \
CLJFMT_VERSION=0.16.5 \ CLJFMT_VERSION=0.16.5 \
PIXI_VERSION=0.75.0 \ PIXI_VERSION=0.81.0 \
GITHUB_CLI_VERSION=2.97.0 \ GITHUB_CLI_VERSION=2.101.0 \
UV_VERSION=0.12.1 \ AWS_CLI_VERSION=2.36.49 \
UV_VERSION=0.12.17 \
UV_TOOL_DIR=/opt/uv/tools \ UV_TOOL_DIR=/opt/uv/tools \
UV_TOOL_BIN_DIR=/opt/utils/bin \ UV_TOOL_BIN_DIR=/opt/utils/bin \
UV_PYTHON_INSTALL_DIR=/opt/uv/python \ UV_PYTHON_INSTALL_DIR=/opt/uv/python \
SERENA_VERSION=1.6.1 SERENA_VERSION=1.7.0
RUN set -ex; \ RUN set -ex; \
ARCH="$(dpkg --print-architecture)"; \ ARCH="$(dpkg --print-architecture)"; \
@ -356,24 +397,30 @@ RUN set -ex; \
tar -xv --strip-components=1 -f /tmp/gh.tar.gz; \ tar -xv --strip-components=1 -f /tmp/gh.tar.gz; \
rm -rf /tmp/gh.tar.gz; rm -rf /tmp/gh.tar.gz;
# Install minio client # Install AWS CLI
RUN set -ex; \ RUN set -ex; \
ARCH="$(dpkg --print-architecture)"; \ ARCH="$(dpkg --print-architecture)"; \
case "${ARCH}" in \ case "${ARCH}" in \
aarch64|arm64) \ aarch64|arm64) \
BINARY_URL="https://dl.min.io/client/mc/release/linux-arm64/mc"; \ BINARY_ARCH="aarch64"; \
BINARY_SHA256="2dec26fdaff2f5e34961ea98f456297104f73d8409fc3599c8f2909c6e27a07e"; \
;; \ ;; \
amd64|x86_64) \ amd64|x86_64) \
BINARY_URL="https://dl.min.io/client/mc/release/linux-amd64/mc"; \ BINARY_ARCH="x86_64"; \
BINARY_SHA256="6b51b62d232217350e72d8f2493f47d3461239d011d1d5eef960dfcd8cb07069"; \
;; \ ;; \
*) \ *) \
echo "Unsupported arch: ${ARCH}"; \ echo "Unsupported arch: ${ARCH}"; \
exit 1; \ exit 1; \
;; \ ;; \
esac; \ esac; \
wget -O /tmp/mc ${BINARY_URL}; \ BINARY_URL="https://awscli.amazonaws.com/awscli-exe-linux-${BINARY_ARCH}-${AWS_CLI_VERSION}.zip"; \
mv /tmp/mc /opt/utils/bin/; \ curl -LfsSo /tmp/awscliv2.zip ${BINARY_URL}; \
chmod +x /opt/utils/bin/mc; echo "${BINARY_SHA256} */tmp/awscliv2.zip" | sha256sum -c -; \
cd /tmp; \
unzip -q awscliv2.zip; \
./aws/install --install-dir /opt/utils/aws-cli --bin-dir /opt/utils/bin; \
rm -rf /tmp/aws /tmp/awscliv2.zip;
# Install uv # Install uv
RUN set -ex; \ RUN set -ex; \
@ -503,7 +550,7 @@ RUN set -ex; \
curl -o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc --fail https://www.postgresql.org/media/keys/ACCC4CF8.asc; \ curl -o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc --fail https://www.postgresql.org/media/keys/ACCC4CF8.asc; \
echo "deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] https://apt.postgresql.org/pub/repos/apt trixie-pgdg main" >> /etc/apt/sources.list.d/postgresql.list; \ echo "deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] https://apt.postgresql.org/pub/repos/apt trixie-pgdg main" >> /etc/apt/sources.list.d/postgresql.list; \
apt-get -qq update; \ apt-get -qq update; \
apt-get -qqy install postgresql-client-16; \ apt-get -qqy install postgresql-client-18; \
apt-get clean; \ apt-get clean; \
rm -rf /var/lib/apt/lists/*; rm -rf /var/lib/apt/lists/*;
@ -540,6 +587,7 @@ COPY --from=setup-rust /opt/rustup /opt/rustup
COPY --from=setup-rust /opt/emsdk /opt/emsdk COPY --from=setup-rust /opt/emsdk /opt/emsdk
COPY --from=setup-caddy /usr/bin/caddy /usr/bin/caddy COPY --from=setup-caddy /usr/bin/caddy /usr/bin/caddy
COPY --from=setup-opencode /tmp/opencode/opencode /opt/utils/bin/opencode COPY --from=setup-opencode /tmp/opencode/opencode /opt/utils/bin/opencode
COPY --from=setup-opencode2 /tmp/opencode2/opencode2 /opt/utils/bin/opencode2
COPY files/nginx.conf /etc/nginx/nginx.conf COPY files/nginx.conf /etc/nginx/nginx.conf
COPY files/nginx-mime.types /etc/nginx/mime.types COPY files/nginx-mime.types /etc/nginx/mime.types
@ -560,4 +608,3 @@ COPY files/init.sh /home/init.sh
ENTRYPOINT ["/home/entrypoint.sh"] ENTRYPOINT ["/home/entrypoint.sh"]
CMD ["/home/init.sh"] CMD ["/home/init.sh"]

View File

@ -10,11 +10,12 @@
# Container names and volume names. Volumes are pinned by explicit name # Container names and volume names. Volumes are pinned by explicit name
# (rather than relying on COMPOSE_PROJECT_NAME prefixing) so the physical # (rather than relying on COMPOSE_PROJECT_NAME prefixing) so the physical
# volumes survive project renames without a data migration. ws0 reuses the # volumes survive project renames. Replacement services use new volume names
# pre-Stage-2 physical volume names (penpotdev_*). # so data from the previous service remains available for rollback.
PENPOT_MAIN_CONTAINER_NAME=penpot-devenv-ws0-main PENPOT_MAIN_CONTAINER_NAME=penpot-devenv-ws0-main
PENPOT_POSTGRES_DATA_VOLUME=penpotdev_postgres_data_pg16 PENPOT_POSTGRES_DATA_VOLUME=penpotdev_postgres_data_pg18
PENPOT_MINIO_DATA_VOLUME=penpotdev_minio_data PENPOT_RUSTFS_DATA_VOLUME=penpotdev_rustfs_data
PENPOT_MAILPIT_DATA_VOLUME=penpotdev_mailpit_data
PENPOT_USER_DATA_VOLUME=penpotdev_user_data PENPOT_USER_DATA_VOLUME=penpotdev_user_data
# Backend runtime config (passed to the container env block). Valkey is a # Backend runtime config (passed to the container env block). Valkey is a
@ -28,12 +29,14 @@ PENPOT_DATABASE_PASSWORD=penpot
PENPOT_DATABASE_MAX_POOL_SIZE=20 PENPOT_DATABASE_MAX_POOL_SIZE=20
PENPOT_REDIS_URI=redis://valkey/0 PENPOT_REDIS_URI=redis://valkey/0
# Object storage (MinIO user/policy are provisioned by the infra compose file). # Object storage (RustFS uses these credentials for its root S3 user).
PENPOT_OBJECTS_STORAGE_BACKEND=s3 PENPOT_OBJECTS_STORAGE_BACKEND=s3
PENPOT_OBJECTS_STORAGE_S3_ENDPOINT=http://minio:9000 PENPOT_OBJECTS_STORAGE_S3_ENDPOINT=http://rustfs:9000
PENPOT_OBJECTS_STORAGE_S3_BUCKET=penpot PENPOT_OBJECTS_STORAGE_S3_BUCKET=penpot
PENPOT_OBJECTS_STORAGE_S3_REGION=us-east-1
AWS_ACCESS_KEY_ID=penpot-devenv AWS_ACCESS_KEY_ID=penpot-devenv
AWS_SECRET_ACCESS_KEY=penpot-devenv AWS_SECRET_ACCESS_KEY=penpot-devenv
AWS_DEFAULT_REGION=us-east-1
# Published host ports. Only ports that need to be reachable from outside the # Published host ports. Only ports that need to be reachable from outside the
# container are exposed; everything else (frontend dev server, backend API, # container are exposed; everything else (frontend dev server, backend API,

View File

@ -4,53 +4,47 @@ networks:
external: true external: true
volumes: volumes:
postgres_data_pg16: postgres_data_pg18:
name: ${PENPOT_POSTGRES_DATA_VOLUME} name: ${PENPOT_POSTGRES_DATA_VOLUME}
minio_data: rustfs_data:
name: ${PENPOT_MINIO_DATA_VOLUME} name: ${PENPOT_RUSTFS_DATA_VOLUME}
mailpit_data:
name: ${PENPOT_MAILPIT_DATA_VOLUME}
valkey_data: valkey_data:
name: penpotdev_valkey_data name: penpotdev_valkey_data
services: services:
minio: rustfs:
image: "minio/minio:RELEASE.2025-04-03T14-56-28Z" image: "rustfs/rustfs:1.0.0"
command: minio server /mnt/data --console-address ":9001" command: /data
volumes: volumes:
- "minio_data:/mnt/data" - "rustfs_data:/data"
environment: environment:
- MINIO_ROOT_USER=minioadmin - RUSTFS_ACCESS_KEY=${AWS_ACCESS_KEY_ID}
- MINIO_ROOT_PASSWORD=minioadmin - RUSTFS_SECRET_KEY=${AWS_SECRET_ACCESS_KEY}
- RUSTFS_ADDRESS=:9000
- RUSTFS_CONSOLE_ADDRESS=:9001
- RUSTFS_CONSOLE_ENABLE=true
ports:
- "127.0.0.1:9000:9000"
- "127.0.0.1:9001:9001"
healthcheck:
test: ["CMD", "curl", "--fail", "http://localhost:9000/health"]
interval: 1s
timeout: 5s
retries: 30
networks: networks:
default: default:
aliases: aliases:
- minio - rustfs
minio-setup:
image: "minio/mc:latest"
depends_on:
- minio
entrypoint: ["/bin/sh", "-c"]
command:
- |
attempts=0
until mc alias set penpot-s3 http://minio:9000 minioadmin minioadmin -q; do
attempts=$$((attempts + 1))
if [ "$$attempts" -ge 30 ]; then
echo "minio-setup: gave up waiting for MinIO after $$attempts attempts" >&2
exit 1
fi
sleep 1
done
mc admin user info penpot-s3 penpot-devenv >/dev/null 2>&1 || mc admin user add penpot-s3 penpot-devenv penpot-devenv -q
mc admin policy attach penpot-s3 readwrite --user=penpot-devenv -q
networks:
default:
postgres: postgres:
image: postgres:16.8 image: postgres:18.6
command: postgres -c config_file=/etc/postgresql.conf command: postgres -c config_file=/etc/postgresql.conf
restart: always restart: always
stop_signal: SIGINT stop_signal: SIGINT
@ -62,7 +56,12 @@ services:
volumes: volumes:
- ./files/postgresql.conf:/etc/postgresql.conf:z - ./files/postgresql.conf:/etc/postgresql.conf:z
- ./files/postgresql_init.sql:/docker-entrypoint-initdb.d/init.sql:z - ./files/postgresql_init.sql:/docker-entrypoint-initdb.d/init.sql:z
- postgres_data_pg16:/var/lib/postgresql/data - postgres_data_pg18:/var/lib/postgresql
healthcheck:
test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U $$POSTGRES_USER -d $$POSTGRES_DB"]
interval: 2s
timeout: 5s
retries: 30
networks: networks:
default: default:
aliases: aliases:
@ -80,12 +79,24 @@ services:
- valkey - valkey
mailer: mailer:
image: sj26/mailcatcher:latest image: axllent/mailpit:v1.31.1
restart: always restart: always
volumes:
- "mailpit_data:/data"
environment:
- MP_DATABASE=/data/mailpit.db
- MP_MAX_MESSAGES=5000
- MP_DISABLE_VERSION_CHECK=true
expose: expose:
- '1025' - '1025'
ports: ports:
- "1080:1080" - "127.0.0.1:1080:8025"
healthcheck:
test: ["CMD", "wget", "--quiet", "--spider", "http://localhost:8025/readyz"]
interval: 2s
timeout: 5s
retries: 15
networks: networks:
default: default:

View File

@ -84,8 +84,10 @@ services:
- PENPOT_OBJECTS_STORAGE_BACKEND=${PENPOT_OBJECTS_STORAGE_BACKEND} - PENPOT_OBJECTS_STORAGE_BACKEND=${PENPOT_OBJECTS_STORAGE_BACKEND}
- PENPOT_OBJECTS_STORAGE_S3_ENDPOINT=${PENPOT_OBJECTS_STORAGE_S3_ENDPOINT} - PENPOT_OBJECTS_STORAGE_S3_ENDPOINT=${PENPOT_OBJECTS_STORAGE_S3_ENDPOINT}
- PENPOT_OBJECTS_STORAGE_S3_BUCKET=${PENPOT_OBJECTS_STORAGE_S3_BUCKET} - PENPOT_OBJECTS_STORAGE_S3_BUCKET=${PENPOT_OBJECTS_STORAGE_S3_BUCKET}
- PENPOT_OBJECTS_STORAGE_S3_REGION=${PENPOT_OBJECTS_STORAGE_S3_REGION}
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
- AWS_DEFAULT_REGION=${AWS_DEFAULT_REGION}
- PENPOT_TENANT=${PENPOT_TENANT} - PENPOT_TENANT=${PENPOT_TENANT}
- PENPOT_TMUX_ATTACH=${PENPOT_TMUX_ATTACH} - PENPOT_TMUX_ATTACH=${PENPOT_TMUX_ATTACH}

View File

@ -1,2 +1,2 @@
CREATE DATABASE penpot_test; CREATE DATABASE penpot_test;
CREATE DATABASE penpot_telemetry; CREATE DATABASE penpot_nexus;

View File

@ -91,9 +91,35 @@ COPY --from=penpotapp/imagemagick:7.1.2-27 /opt/imagick /opt/imagick
WORKDIR /opt/penpot/exporter WORKDIR /opt/penpot/exporter
# DHI Node image installs Node at the system level (symlinked into # pnpm ships as a system binary (same tarball + SHA pin as
# /usr/bin), so `./setup`'s internal `corepack enable` needs root to write # docker/devenv/Dockerfile); the generated ./setup expects it on PATH.
# there. Ownership is fixed right after. # Corepack is gone from Node 25+, so nothing here may use it.
ARG PNPM_VERSION=12.5.1
RUN set -eux; \
ARCH="$(dpkg --print-architecture)"; \
case "${ARCH}" in \
aarch64|arm64) \
PNPM_ARCH='arm64'; \
PNPM_SHA256='84e1290e82c800acd406b6db27e9650e15db3c2344d12162cc305ea1f942c6ff'; \
;; \
amd64|x86_64) \
PNPM_ARCH='x64'; \
PNPM_SHA256='5a397dfb6b3d4b07d3d7769586aeb471048faf04299a492e2808b95a9a1c701f'; \
;; \
*) \
echo "Unsupported arch: ${ARCH}"; \
exit 1; \
;; \
esac; \
curl -LfsSo /tmp/pnpm.tar.gz "https://github.com/pnpm/pnpm/releases/download/v${PNPM_VERSION}/pnpm-linux-${PNPM_ARCH}.tar.gz"; \
echo "${PNPM_SHA256} */tmp/pnpm.tar.gz" | sha256sum -c -; \
tar -xzf /tmp/pnpm.tar.gz -C /usr/local/bin pnpm; \
chmod 755 /usr/local/bin/pnpm; \
rm -f /tmp/pnpm.tar.gz; \
pnpm --version;
# `./setup` only needs pnpm on PATH (installed above as root).
# Ownership is fixed right after.
RUN ./setup && chown -R penpot:penpot /opt/penpot/exporter RUN ./setup && chown -R penpot:penpot /opt/penpot/exporter
USER penpot:penpot USER penpot:penpot

View File

@ -8,6 +8,36 @@ COPY $BUNDLE_PATH /opt/penpot/mcp/
WORKDIR /opt/penpot/mcp WORKDIR /opt/penpot/mcp
# pnpm ships as a system binary (same tarball + SHA pin as
# docker/devenv/Dockerfile); ./setup expects it on PATH.
# Corepack is gone from Node 25+, so nothing here may use it.
ARG PNPM_VERSION=12.5.1
RUN set -eux; \
apt-get -qq update; \
apt-get -qqy --no-install-recommends install curl ca-certificates; \
rm -rf /var/lib/apt/lists/*; \
ARCH="$(dpkg --print-architecture)"; \
case "${ARCH}" in \
aarch64|arm64) \
PNPM_ARCH='arm64'; \
PNPM_SHA256='84e1290e82c800acd406b6db27e9650e15db3c2344d12162cc305ea1f942c6ff'; \
;; \
amd64|x86_64) \
PNPM_ARCH='x64'; \
PNPM_SHA256='5a397dfb6b3d4b07d3d7769586aeb471048faf04299a492e2808b95a9a1c701f'; \
;; \
*) \
echo "Unsupported arch: ${ARCH}"; \
exit 1; \
;; \
esac; \
curl -LfsSo /tmp/pnpm.tar.gz "https://github.com/pnpm/pnpm/releases/download/v${PNPM_VERSION}/pnpm-linux-${PNPM_ARCH}.tar.gz"; \
echo "${PNPM_SHA256} */tmp/pnpm.tar.gz" | sha256sum -c -; \
tar -xzf /tmp/pnpm.tar.gz -C /usr/local/bin pnpm; \
chmod 755 /usr/local/bin/pnpm; \
rm -f /tmp/pnpm.tar.gz; \
pnpm --version;
RUN ./setup RUN ./setup

View File

@ -4,6 +4,7 @@ LABEL maintainer="Penpot <docker@penpot.app>"
ENV LANG=en_US.UTF-8 \ ENV LANG=en_US.UTF-8 \
LC_ALL=en_US.UTF-8 \ LC_ALL=en_US.UTF-8 \
NODE_VERSION=v24.20.0 \ NODE_VERSION=v24.20.0 \
PNPM_VERSION=12.5.1 \
DEBIAN_FRONTEND=noninteractive \ DEBIAN_FRONTEND=noninteractive \
PATH=/opt/node/bin:$PATH PATH=/opt/node/bin:$PATH
@ -56,9 +57,13 @@ RUN set -eux; \
case "${ARCH}" in \ case "${ARCH}" in \
aarch64|arm64) \ aarch64|arm64) \
BINARY_URL="https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-linux-arm64.tar.gz"; \ BINARY_URL="https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-linux-arm64.tar.gz"; \
PNPM_ARCH='arm64'; \
PNPM_SHA256='84e1290e82c800acd406b6db27e9650e15db3c2344d12162cc305ea1f942c6ff'; \
;; \ ;; \
amd64|x86_64) \ amd64|x86_64) \
BINARY_URL="https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-linux-x64.tar.gz"; \ BINARY_URL="https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-linux-x64.tar.gz"; \
PNPM_ARCH='x64'; \
PNPM_SHA256='5a397dfb6b3d4b07d3d7769586aeb471048faf04299a492e2808b95a9a1c701f'; \
;; \ ;; \
*) \ *) \
echo "Unsupported arch: ${ARCH}"; \ echo "Unsupported arch: ${ARCH}"; \
@ -71,7 +76,13 @@ RUN set -eux; \
tar -xf /tmp/nodejs.tar.gz --strip-components=1; \ tar -xf /tmp/nodejs.tar.gz --strip-components=1; \
chown -R root /opt/node; \ chown -R root /opt/node; \
rm -rf /tmp/nodejs.tar.gz; \ rm -rf /tmp/nodejs.tar.gz; \
corepack enable; \ PNPM_URL="https://github.com/pnpm/pnpm/releases/download/v${PNPM_VERSION}/pnpm-linux-${PNPM_ARCH}.tar.gz"; \
curl -LfsSo /tmp/pnpm.tar.gz "${PNPM_URL}"; \
echo "${PNPM_SHA256} */tmp/pnpm.tar.gz" | sha256sum -c -; \
tar -xzf /tmp/pnpm.tar.gz -C /opt/node/bin pnpm; \
chmod 755 /opt/node/bin/pnpm; \
rm -f /tmp/pnpm.tar.gz; \
pnpm --version; \
mkdir -p /opt/penpot; \ mkdir -p /opt/penpot; \
chown -R penpot:penpot /opt/penpot; chown -R penpot:penpot /opt/penpot;

View File

@ -10,7 +10,6 @@ To view this site locally, first set up the environment:
# only if necessary # only if necessary
nvm install nvm install
nvm use nvm use
corepack enable
pnpm install pnpm install
``` ```

View File

@ -39,5 +39,5 @@
"markdown-it-anchor": "^9.2.1", "markdown-it-anchor": "^9.2.1",
"markdown-it-plantuml": "^1.4.1" "markdown-it-plantuml": "^1.4.1"
}, },
"packageManager": "pnpm@12.3.4+sha512.961aa41fb077da3a04a441d9f8e15ebc0c96da8ef710b2eb67bf9ee7cb0610eabd48f1fd85f51cffe73846785fa0f87c56a3a872a1d893f8446741b5cce45457" "packageManager": "pnpm@12.5.1+sha512.e3f305bc784a2bc89f5ad3b6138889470fae8d2af5f36b61216ec91c2c3d64089775f9de38aac331044ea40f245cb0d5666392dfdf65824e1907ef6a2c62de5f"
} }

131
docs/pnpm-lock.yaml generated
View File

@ -7,96 +7,153 @@ importers:
configDependencies: {} configDependencies: {}
packageManagerDependencies: packageManagerDependencies:
pnpm: pnpm:
specifier: 12.3.4 specifier: 12.5.1
version: 12.3.4 version: 12.5.1
packages: packages:
'@pnpm/exe.darwin-arm64@12.3.4': '@pnpm/exe.android-arm64@12.5.1':
resolution: {integrity: sha512-PAyUol8T1+/+ViOiXAt51ECA+QnfXCqz6foL4bW+LsoX0NcVd5XVEM2mRQu+LV4oc7uRz9zf9U0P+XFfuQeDAw==} resolution: {integrity: sha512-6LGBmQuDzUmgpHSuy+haG0jrmkwFNa4EuShDZqK3cIBv4Wfb75CzjJmowkzr+iJg40vnBKMQ62t4Ko6mwsId7A==}
cpu: [arm64]
os: [android]
'@pnpm/exe.android-x64@12.5.1':
resolution: {integrity: sha512-091oCltP3Rx89yxL1Wh5rXkUXEyFzyxIzSHKRFDyDFlQkb8Ox8FaDcm+M8PNkMyqnLA+qeGOLzY7Rjj9izl1Ww==}
cpu: [x64]
os: [android]
'@pnpm/exe.darwin-arm64@12.5.1':
resolution: {integrity: sha512-r1vEMQPVjRexZMyL2HonxJoo3+nuB/+G+tg+f2PK6faRdXX1TDwevKXOxJAfqlPInTy6W/pm912+s+m0NcR4BQ==}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
'@pnpm/exe.darwin-x64@12.3.4': '@pnpm/exe.darwin-x64@12.5.1':
resolution: {integrity: sha512-fxP9JCk0Cdye+ePuj+GJJLMUMTqHGWRdb1dtv4How876uQ2ehxvenpgiYAir/ceO9PsYUZkFTtyZdx+rRu5QOA==} resolution: {integrity: sha512-YHwI1g2B+Y2cAEylOR31r781GWijdJL7Gy7TEn/ZpZ5v/Cuwp5/Pz0EFCgo0M2MKLafAbL/laUMGvOpAdnerAw==}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
'@pnpm/exe.linux-arm64-musl@12.3.4': '@pnpm/exe.freebsd-x64@12.5.1':
resolution: {integrity: sha512-FBOt0/7ye6O6q4AllVV5QMviB6qE6fqkeczV/+MDWQsmo+QJrlfsh6X7CpH/tClVpBZEyIbjpUoT8bNhCYBxEg==} resolution: {integrity: sha512-sFUMHiYFs6q5syFwgaVppzhQSmqnXoJIz/YwTXIOXTDE76ndIpTRpDGAnBCBKjsaEBnqjJA/2l6hpJcNDNoMiA==}
cpu: [x64]
os: [freebsd]
'@pnpm/exe.linux-arm64-musl@12.5.1':
resolution: {integrity: sha512-zupuxYZBuBEGjUz805/4/Asq01UNziFADANt8yp+LdUjU9bbMa3xY78Bx1PsGDqBy7aiad4sdUMK8ZxH4n4gmQ==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl] libc: [musl]
'@pnpm/exe.linux-arm64@12.3.4': '@pnpm/exe.linux-arm64@12.5.1':
resolution: {integrity: sha512-t71AVA7LRqiKTyZ5xMYaZc2n5DfdpMbfokZuiIOXHBOM03ECnF0t4iYwaBDqJgVjlKYUOwaF/bRQajGNA4cJ4w==} resolution: {integrity: sha512-6hbMWW2/eQo1b5wvQPe/ndLHVTH88Rav6N+ffD4/h6IuFMjWCgasFYYsFv0GrvpqREAx3SwmZSvli+0NFXqbxA==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@pnpm/exe.linux-x64-musl@12.3.4': '@pnpm/exe.linux-ppc64@12.5.1':
resolution: {integrity: sha512-RPmk7Jb/aYaFvL2iyDN/AtMY+hUEsue732WmXpcuQ9tBpMnGyA5py7Z3+e+qmQaJ0zY/4ni9jJiyPBQHujmv6w==} resolution: {integrity: sha512-Gg3maDzaE/ZFMV9HIx/BwMN0Y6QbEs13URfxmiYHtKjjLAs5wEQZRMUP87YNLYO7cNaEfbM8GRbS9vPosyD9Gw==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@pnpm/exe.linux-riscv64@12.5.1':
resolution: {integrity: sha512-6fgWxXYMBMsWQ8DAiRevscX4QZQjwh+FsyCj8ZOk8pMXvqeq84MqBLCmS6KckvnbdqJ+HlSVQ7fs/kd0ldwfhA==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@pnpm/exe.linux-s390x@12.5.1':
resolution: {integrity: sha512-nzMZruRvxDhEfYUhzu+7EmM39+82Dgagc/6hPFPqGqzXNGcIG0anuxbUxfeNtI64SHt91KuuYUmCB/inlM+Pag==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@pnpm/exe.linux-x64-musl@12.5.1':
resolution: {integrity: sha512-kHCbegdTNHpzmPtUD4S5fqkVkqgdQR47spF9p2VPP3dIIkZl6Ytk8w0vhlAsAGgge8FmucfwX+vvPKgm0NtOCQ==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl] libc: [musl]
'@pnpm/exe.linux-x64@12.3.4': '@pnpm/exe.linux-x64@12.5.1':
resolution: {integrity: sha512-2ZqOlSPkfwX1h5cR+FPiWf8+F+2hZT/3TvhUK5sigHqwaQCIiq8R7CGxhndKs63JtcLi2a1Qpo+wX/EoyfjyJQ==} resolution: {integrity: sha512-3PkUBYo5z4dgtlnTNIFj7QGpcDUAuqXz9WGVigPDCeccEnhGiRkWmA5102TmYJHtwJP3LfmE+ZF9PGeWhn8p9Q==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@pnpm/exe.win32-arm64@12.3.4': '@pnpm/exe.win32-arm64@12.5.1':
resolution: {integrity: sha512-ANyrHqyqco6SXBysUTRF74itDyyraea7IbFsKFdNXTjcFnfycTDx37EwuhdpPYFNSIh2JhUG4fByclsRfiHX7w==} resolution: {integrity: sha512-49NvjZIXHly56/PLoEDD7VphbO4N8dwMXoApElDFCpVGDNGeKlwlib1tXtnpTF6i2/XIsAj7GgatHB5KHXgBnA==}
cpu: [arm64] cpu: [arm64]
os: [win32] os: [win32]
'@pnpm/exe.win32-x64@12.3.4': '@pnpm/exe.win32-x64@12.5.1':
resolution: {integrity: sha512-WH/KqBPY/hq2Tb7SgQltEZytimcjgKRaCRL/aM9CI0c67iKc5TVmHUhIiL3Ux9FB4bWn36i6XewUcScQI+zG8w==} resolution: {integrity: sha512-ePUmQFPAPJFNMwg1lelDx2PdjHqYRiF0yfg5e6qeDbPNOu97QNo557Ru/SxmCrmt8ydQWNuZXWpdEp019md5vw==}
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
pnpm@12.3.4: pnpm@12.5.1:
resolution: {integrity: sha512-lhqkH7B32joEpEHZ+OFevAyW2o73ELLrZ7+e58sGEOq9SPH9hfUc/+c4RnhfoPh8VqOocqHYk/hEZ0G1zORUVw==} resolution: {integrity: sha512-4/MFvHhKK8ifWtO2E4iJRw+ujSr182thIW7JHCw9ZAiXdfneOKrDMQROpA8kXLDVZmOS399lgk4ZB+9qLGLeXw==}
engines: {node: '>=18.*'} engines: {node: '>=18.*'}
hasBin: true hasBin: true
snapshots: snapshots:
'@pnpm/exe.darwin-arm64@12.3.4': '@pnpm/exe.android-arm64@12.5.1':
optional: true optional: true
'@pnpm/exe.darwin-x64@12.3.4': '@pnpm/exe.android-x64@12.5.1':
optional: true optional: true
'@pnpm/exe.linux-arm64-musl@12.3.4': '@pnpm/exe.darwin-arm64@12.5.1':
optional: true optional: true
'@pnpm/exe.linux-arm64@12.3.4': '@pnpm/exe.darwin-x64@12.5.1':
optional: true optional: true
'@pnpm/exe.linux-x64-musl@12.3.4': '@pnpm/exe.freebsd-x64@12.5.1':
optional: true optional: true
'@pnpm/exe.linux-x64@12.3.4': '@pnpm/exe.linux-arm64-musl@12.5.1':
optional: true optional: true
'@pnpm/exe.win32-arm64@12.3.4': '@pnpm/exe.linux-arm64@12.5.1':
optional: true optional: true
'@pnpm/exe.win32-x64@12.3.4': '@pnpm/exe.linux-ppc64@12.5.1':
optional: true optional: true
pnpm@12.3.4: '@pnpm/exe.linux-riscv64@12.5.1':
optional: true
'@pnpm/exe.linux-s390x@12.5.1':
optional: true
'@pnpm/exe.linux-x64-musl@12.5.1':
optional: true
'@pnpm/exe.linux-x64@12.5.1':
optional: true
'@pnpm/exe.win32-arm64@12.5.1':
optional: true
'@pnpm/exe.win32-x64@12.5.1':
optional: true
pnpm@12.5.1:
optionalDependencies: optionalDependencies:
'@pnpm/exe.darwin-arm64': 12.3.4 '@pnpm/exe.android-arm64': 12.5.1
'@pnpm/exe.darwin-x64': 12.3.4 '@pnpm/exe.android-x64': 12.5.1
'@pnpm/exe.linux-arm64': 12.3.4 '@pnpm/exe.darwin-arm64': 12.5.1
'@pnpm/exe.linux-arm64-musl': 12.3.4 '@pnpm/exe.darwin-x64': 12.5.1
'@pnpm/exe.linux-x64': 12.3.4 '@pnpm/exe.freebsd-x64': 12.5.1
'@pnpm/exe.linux-x64-musl': 12.3.4 '@pnpm/exe.linux-arm64': 12.5.1
'@pnpm/exe.win32-arm64': 12.3.4 '@pnpm/exe.linux-arm64-musl': 12.5.1
'@pnpm/exe.win32-x64': 12.3.4 '@pnpm/exe.linux-ppc64': 12.5.1
'@pnpm/exe.linux-riscv64': 12.5.1
'@pnpm/exe.linux-s390x': 12.5.1
'@pnpm/exe.linux-x64': 12.5.1
'@pnpm/exe.linux-x64-musl': 12.5.1
'@pnpm/exe.win32-arm64': 12.5.1
'@pnpm/exe.win32-x64': 12.5.1
--- ---
lockfileVersion: '9.0' lockfileVersion: '9.0'

View File

@ -2,9 +2,6 @@
set -ex set -ex
corepack enable;
corepack install;
rm -rf ./_dist rm -rf ./_dist
pnpm install pnpm install
pnpm run build pnpm run build

View File

@ -62,7 +62,7 @@ See the dedicated section [Agentic Dev Environment](../agentic-devenv/) for deta
### Parallel workspaces ### Parallel workspaces
The devenv runs as separate compose projects: The devenv runs as separate compose projects:
* shared infra (`penpotdev-infra`: Postgres, MinIO, mailer, LDAP) * shared infra (`penpotdev-infra`: Postgres, RustFS, Valkey, mailer, LDAP)
* `penpotdev-wsN` project per runtime instance. * `penpotdev-wsN` project per runtime instance.
- `ws0` (a.k.a. `main`) is the current state of your repo; - `ws0` (a.k.a. `main`) is the current state of your repo;
- `ws1` and up are clones that you maintain explicitly under `${PENPOT_WORKSPACES_DIR}/wsN/` - `ws1` and up are clones that you maintain explicitly under `${PENPOT_WORKSPACES_DIR}/wsN/`
@ -168,7 +168,7 @@ the container's data volume regardless of this flag.
### Shared state and workers ### Shared state and workers
All instances share one Penpot database and one MinIO bucket; users, teams, All instances share one Penpot database and one RustFS bucket; users, teams,
files, and MCP tokens are visible from every instance. Per-instance Valkey files, and MCP tokens are visible from every instance. Per-instance Valkey
keeps msgbus Pub/Sub channels (collab broadcasts, team-org notifications, keeps msgbus Pub/Sub channels (collab broadcasts, team-org notifications,
file-summary cache, rate-limit counters) isolated. file-summary cache, rate-limit counters) isolated.
@ -176,7 +176,7 @@ file-summary cache, rate-limit counters) isolated.
Background workers (`enable-backend-worker`) run only on ws0 — ws1+ overlays Background workers (`enable-backend-worker`) run only on ws0 — ws1+ overlays
disable it. ws1+ RPC handlers still enqueue tasks into the shared Postgres disable it. ws1+ RPC handlers still enqueue tasks into the shared Postgres
`task` table; ws0's dispatcher claims them via `FOR UPDATE SKIP LOCKED` and `task` table; ws0's dispatcher claims them via `FOR UPDATE SKIP LOCKED` and
runs them against the shared DB and MinIO. Workers are fire-and-forget: runs them against the shared DB and RustFS. Workers are fire-and-forget:
`wrk/submit!` inserts a row and returns; RPC handlers never wait on `wrk/submit!` inserts a row and returns; RPC handlers never wait on
completion. The "ws0 only" policy avoids multi-instance worker races (cron completion. The "ws0 only" policy avoids multi-instance worker races (cron
dedup is best-effort across instances, `wrk/submit!` `dedupe` is racy across dedup is best-effort across instances, `wrk/submit!` `dedupe` is racy across
@ -190,7 +190,8 @@ Shared infrastructure shuts down only when no instances remain running.
The devenv compose configuration has been split into two files and reorganized The devenv compose configuration has been split into two files and reorganized
into separate compose projects per runtime instance: into separate compose projects per runtime instance:
- `docker/devenv/docker-compose.infra.yml` (Postgres, MinIO, mailer, LDAP) - `docker/devenv/docker-compose.infra.yml` (Postgres, RustFS, Valkey, mailer,
LDAP)
runs under the compose project `penpotdev-infra`. runs under the compose project `penpotdev-infra`.
- `docker/devenv/docker-compose.main.yml` (one main container + its Valkey) - `docker/devenv/docker-compose.main.yml` (one main container + its Valkey)
runs once per runtime instance under `penpotdev-ws0`, `penpotdev-ws1`, …. runs once per runtime instance under `penpotdev-ws0`, `penpotdev-ws1`, ….
@ -202,10 +203,17 @@ into separate compose projects per runtime instance:
If you had the devenv running on the previous single-project (`penpotdev`) If you had the devenv running on the previous single-project (`penpotdev`)
layout, leftover containers and the auto-generated `penpotdev_default` layout, leftover containers and the auto-generated `penpotdev_default`
network must be removed before bringing the new ws0 instance up. The named network must be removed before bringing the new ws0 instance up. The named
data volumes (`penpotdev_postgres_data_pg16`, `penpotdev_minio_data`, data volumes (`penpotdev_postgres_data_pg18`, `penpotdev_rustfs_data`,
`penpotdev_user_data`, `penpotdev_valkey_data`) are pinned by explicit `penpotdev_mailpit_data`, `penpotdev_user_data`, `penpotdev_valkey_data`) are
`name:` entries in the new compose files and are preserved through the pinned by explicit `name:` entries in the new compose files. The legacy
transition — your Postgres DB, MinIO objects, and home cache survive. `penpotdev_postgres_data_pg16` and `penpotdev_minio_data` volumes remain
untouched. PostgreSQL 16 data and MinIO objects are not migrated automatically.
PostgreSQL 18 stores its versioned data directory under
`/var/lib/postgresql/18/docker`, so the devenv mounts its volume at
`/var/lib/postgresql`. To retain data from PostgreSQL 16, export and restore it
with `pg_dump` and `pg_restore`; do not mount the PostgreSQL 16 volume directly
in the PostgreSQL 18 container.
One-time cleanup, then bring up ws0: One-time cleanup, then bring up ws0:
@ -388,15 +396,29 @@ An example of your cursor configuration can be:
} }
``` ```
## Object storage
The devenv uses RustFS for S3-compatible object storage. Its API is available
at [http://localhost:9000](http://localhost:9000), and its management console
is available at [http://localhost:9001](http://localhost:9001). Log in to the
console with `penpot-devenv` as both the access key and secret key.
Both ports bind only to the host loopback interface and are not exposed to the
local network.
## Email ## Email
To test email sending, the devenv includes [MailCatcher](https://mailcatcher.me/), To test email sending, the devenv includes
a SMTP server that is used for develop. It does not send any mail outbounds. [Mailpit](https://mailpit.axllent.org/), an SMTP server for development. It does
Instead, it stores them in memory and allows to browse them via a web interface not send mail externally. Instead, it stores messages in a persistent Docker
similar to a webmail client. Simply navigate to: volume and provides a webmail-like interface. Simply navigate to:
[http://localhost:1080](http://localhost:1080) [http://localhost:1080](http://localhost:1080)
The inbox persists when the container is recreated. `drop-devenv` preserves
the `penpotdev_mailpit_data` volume, together with the other devenv data
volumes.
## Create user ## Create user
You can register a new user manually, or create new users automatically with this script. From your tmux instance, run: You can register a new user manually, or create new users automatically with this script. From your tmux instance, run:

View File

@ -356,7 +356,7 @@ npx playwright test --ui
> ❗️ **IMPORTANT**: You might need to [install Playwright's browsers and dependencies](https://playwright.dev/docs/intro) in your host machine with: <code class="language-bash">npx playwright install --with-deps</code>. In case you are using a Linux distribution other than Ubuntu, [you might need to install the dependencies manually](https://github.com/microsoft/playwright/issues/11122). > ❗️ **IMPORTANT**: You might need to [install Playwright's browsers and dependencies](https://playwright.dev/docs/intro) in your host machine with: <code class="language-bash">npx playwright install --with-deps</code>. In case you are using a Linux distribution other than Ubuntu, [you might need to install the dependencies manually](https://github.com/microsoft/playwright/issues/11122).
> You will also need pnpm in your host nodejs. For this, do <code class="language-bash">corepack enable</code> and then just <code class="language-bash">pnpm</code>. > You will also need pnpm in your host nodejs (the devenv already ships it; outside it, run <code class="language-bash">npm install -g pnpm</code>) and then just <code class="language-bash">pnpm</code>.
### How to write a test ### How to write a test

View File

@ -4,7 +4,7 @@
"license": "MPL-2.0", "license": "MPL-2.0",
"author": "Kaleidos INC Sucursal en España SL", "author": "Kaleidos INC Sucursal en España SL",
"private": true, "private": true,
"packageManager": "pnpm@12.3.4+sha512.961aa41fb077da3a04a441d9f8e15ebc0c96da8ef710b2eb67bf9ee7cb0610eabd48f1fd85f51cffe73846785fa0f87c56a3a872a1d893f8446741b5cce45457", "packageManager": "pnpm@12.5.1+sha512.e3f305bc784a2bc89f5ad3b6138889470fae8d2af5f36b61216ec91c2c3d64089775f9de38aac331044ea40f245cb0d5666392dfdf65824e1907ef6a2c62de5f",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/penpot/penpot" "url": "https://github.com/penpot/penpot"
@ -30,7 +30,8 @@
}, },
"scripts": { "scripts": {
"clear:shadow-cache": "rm -rf .shadow-cljs && rm -rf target", "clear:shadow-cache": "rm -rf .shadow-cljs && rm -rf target",
"watch:app": "pnpm run clear:shadow-cache && clojure -M:dev:shadow-cljs watch main", "build:wasm": "exit 0",
"watch:app": "pnpm run build:wasm && pnpm run clear:shadow-cache && clojure -M:dev:shadow-cljs watch main",
"watch": "pnpm run watch:app", "watch": "pnpm run watch:app",
"build:app": "clojure -M:dev:shadow-cljs release main", "build:app": "clojure -M:dev:shadow-cljs release main",
"build:wasm": "../render-wasm/build export", "build:wasm": "../render-wasm/build export",

131
exporter/pnpm-lock.yaml generated
View File

@ -7,96 +7,153 @@ importers:
configDependencies: {} configDependencies: {}
packageManagerDependencies: packageManagerDependencies:
pnpm: pnpm:
specifier: 12.3.4 specifier: 12.5.1
version: 12.3.4 version: 12.5.1
packages: packages:
'@pnpm/exe.darwin-arm64@12.3.4': '@pnpm/exe.android-arm64@12.5.1':
resolution: {integrity: sha512-PAyUol8T1+/+ViOiXAt51ECA+QnfXCqz6foL4bW+LsoX0NcVd5XVEM2mRQu+LV4oc7uRz9zf9U0P+XFfuQeDAw==} resolution: {integrity: sha512-6LGBmQuDzUmgpHSuy+haG0jrmkwFNa4EuShDZqK3cIBv4Wfb75CzjJmowkzr+iJg40vnBKMQ62t4Ko6mwsId7A==}
cpu: [arm64]
os: [android]
'@pnpm/exe.android-x64@12.5.1':
resolution: {integrity: sha512-091oCltP3Rx89yxL1Wh5rXkUXEyFzyxIzSHKRFDyDFlQkb8Ox8FaDcm+M8PNkMyqnLA+qeGOLzY7Rjj9izl1Ww==}
cpu: [x64]
os: [android]
'@pnpm/exe.darwin-arm64@12.5.1':
resolution: {integrity: sha512-r1vEMQPVjRexZMyL2HonxJoo3+nuB/+G+tg+f2PK6faRdXX1TDwevKXOxJAfqlPInTy6W/pm912+s+m0NcR4BQ==}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
'@pnpm/exe.darwin-x64@12.3.4': '@pnpm/exe.darwin-x64@12.5.1':
resolution: {integrity: sha512-fxP9JCk0Cdye+ePuj+GJJLMUMTqHGWRdb1dtv4How876uQ2ehxvenpgiYAir/ceO9PsYUZkFTtyZdx+rRu5QOA==} resolution: {integrity: sha512-YHwI1g2B+Y2cAEylOR31r781GWijdJL7Gy7TEn/ZpZ5v/Cuwp5/Pz0EFCgo0M2MKLafAbL/laUMGvOpAdnerAw==}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
'@pnpm/exe.linux-arm64-musl@12.3.4': '@pnpm/exe.freebsd-x64@12.5.1':
resolution: {integrity: sha512-FBOt0/7ye6O6q4AllVV5QMviB6qE6fqkeczV/+MDWQsmo+QJrlfsh6X7CpH/tClVpBZEyIbjpUoT8bNhCYBxEg==} resolution: {integrity: sha512-sFUMHiYFs6q5syFwgaVppzhQSmqnXoJIz/YwTXIOXTDE76ndIpTRpDGAnBCBKjsaEBnqjJA/2l6hpJcNDNoMiA==}
cpu: [x64]
os: [freebsd]
'@pnpm/exe.linux-arm64-musl@12.5.1':
resolution: {integrity: sha512-zupuxYZBuBEGjUz805/4/Asq01UNziFADANt8yp+LdUjU9bbMa3xY78Bx1PsGDqBy7aiad4sdUMK8ZxH4n4gmQ==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl] libc: [musl]
'@pnpm/exe.linux-arm64@12.3.4': '@pnpm/exe.linux-arm64@12.5.1':
resolution: {integrity: sha512-t71AVA7LRqiKTyZ5xMYaZc2n5DfdpMbfokZuiIOXHBOM03ECnF0t4iYwaBDqJgVjlKYUOwaF/bRQajGNA4cJ4w==} resolution: {integrity: sha512-6hbMWW2/eQo1b5wvQPe/ndLHVTH88Rav6N+ffD4/h6IuFMjWCgasFYYsFv0GrvpqREAx3SwmZSvli+0NFXqbxA==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@pnpm/exe.linux-x64-musl@12.3.4': '@pnpm/exe.linux-ppc64@12.5.1':
resolution: {integrity: sha512-RPmk7Jb/aYaFvL2iyDN/AtMY+hUEsue732WmXpcuQ9tBpMnGyA5py7Z3+e+qmQaJ0zY/4ni9jJiyPBQHujmv6w==} resolution: {integrity: sha512-Gg3maDzaE/ZFMV9HIx/BwMN0Y6QbEs13URfxmiYHtKjjLAs5wEQZRMUP87YNLYO7cNaEfbM8GRbS9vPosyD9Gw==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@pnpm/exe.linux-riscv64@12.5.1':
resolution: {integrity: sha512-6fgWxXYMBMsWQ8DAiRevscX4QZQjwh+FsyCj8ZOk8pMXvqeq84MqBLCmS6KckvnbdqJ+HlSVQ7fs/kd0ldwfhA==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@pnpm/exe.linux-s390x@12.5.1':
resolution: {integrity: sha512-nzMZruRvxDhEfYUhzu+7EmM39+82Dgagc/6hPFPqGqzXNGcIG0anuxbUxfeNtI64SHt91KuuYUmCB/inlM+Pag==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@pnpm/exe.linux-x64-musl@12.5.1':
resolution: {integrity: sha512-kHCbegdTNHpzmPtUD4S5fqkVkqgdQR47spF9p2VPP3dIIkZl6Ytk8w0vhlAsAGgge8FmucfwX+vvPKgm0NtOCQ==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl] libc: [musl]
'@pnpm/exe.linux-x64@12.3.4': '@pnpm/exe.linux-x64@12.5.1':
resolution: {integrity: sha512-2ZqOlSPkfwX1h5cR+FPiWf8+F+2hZT/3TvhUK5sigHqwaQCIiq8R7CGxhndKs63JtcLi2a1Qpo+wX/EoyfjyJQ==} resolution: {integrity: sha512-3PkUBYo5z4dgtlnTNIFj7QGpcDUAuqXz9WGVigPDCeccEnhGiRkWmA5102TmYJHtwJP3LfmE+ZF9PGeWhn8p9Q==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@pnpm/exe.win32-arm64@12.3.4': '@pnpm/exe.win32-arm64@12.5.1':
resolution: {integrity: sha512-ANyrHqyqco6SXBysUTRF74itDyyraea7IbFsKFdNXTjcFnfycTDx37EwuhdpPYFNSIh2JhUG4fByclsRfiHX7w==} resolution: {integrity: sha512-49NvjZIXHly56/PLoEDD7VphbO4N8dwMXoApElDFCpVGDNGeKlwlib1tXtnpTF6i2/XIsAj7GgatHB5KHXgBnA==}
cpu: [arm64] cpu: [arm64]
os: [win32] os: [win32]
'@pnpm/exe.win32-x64@12.3.4': '@pnpm/exe.win32-x64@12.5.1':
resolution: {integrity: sha512-WH/KqBPY/hq2Tb7SgQltEZytimcjgKRaCRL/aM9CI0c67iKc5TVmHUhIiL3Ux9FB4bWn36i6XewUcScQI+zG8w==} resolution: {integrity: sha512-ePUmQFPAPJFNMwg1lelDx2PdjHqYRiF0yfg5e6qeDbPNOu97QNo557Ru/SxmCrmt8ydQWNuZXWpdEp019md5vw==}
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
pnpm@12.3.4: pnpm@12.5.1:
resolution: {integrity: sha512-lhqkH7B32joEpEHZ+OFevAyW2o73ELLrZ7+e58sGEOq9SPH9hfUc/+c4RnhfoPh8VqOocqHYk/hEZ0G1zORUVw==} resolution: {integrity: sha512-4/MFvHhKK8ifWtO2E4iJRw+ujSr182thIW7JHCw9ZAiXdfneOKrDMQROpA8kXLDVZmOS399lgk4ZB+9qLGLeXw==}
engines: {node: '>=18.*'} engines: {node: '>=18.*'}
hasBin: true hasBin: true
snapshots: snapshots:
'@pnpm/exe.darwin-arm64@12.3.4': '@pnpm/exe.android-arm64@12.5.1':
optional: true optional: true
'@pnpm/exe.darwin-x64@12.3.4': '@pnpm/exe.android-x64@12.5.1':
optional: true optional: true
'@pnpm/exe.linux-arm64-musl@12.3.4': '@pnpm/exe.darwin-arm64@12.5.1':
optional: true optional: true
'@pnpm/exe.linux-arm64@12.3.4': '@pnpm/exe.darwin-x64@12.5.1':
optional: true optional: true
'@pnpm/exe.linux-x64-musl@12.3.4': '@pnpm/exe.freebsd-x64@12.5.1':
optional: true optional: true
'@pnpm/exe.linux-x64@12.3.4': '@pnpm/exe.linux-arm64-musl@12.5.1':
optional: true optional: true
'@pnpm/exe.win32-arm64@12.3.4': '@pnpm/exe.linux-arm64@12.5.1':
optional: true optional: true
'@pnpm/exe.win32-x64@12.3.4': '@pnpm/exe.linux-ppc64@12.5.1':
optional: true optional: true
pnpm@12.3.4: '@pnpm/exe.linux-riscv64@12.5.1':
optional: true
'@pnpm/exe.linux-s390x@12.5.1':
optional: true
'@pnpm/exe.linux-x64-musl@12.5.1':
optional: true
'@pnpm/exe.linux-x64@12.5.1':
optional: true
'@pnpm/exe.win32-arm64@12.5.1':
optional: true
'@pnpm/exe.win32-x64@12.5.1':
optional: true
pnpm@12.5.1:
optionalDependencies: optionalDependencies:
'@pnpm/exe.darwin-arm64': 12.3.4 '@pnpm/exe.android-arm64': 12.5.1
'@pnpm/exe.darwin-x64': 12.3.4 '@pnpm/exe.android-x64': 12.5.1
'@pnpm/exe.linux-arm64': 12.3.4 '@pnpm/exe.darwin-arm64': 12.5.1
'@pnpm/exe.linux-arm64-musl': 12.3.4 '@pnpm/exe.darwin-x64': 12.5.1
'@pnpm/exe.linux-x64': 12.3.4 '@pnpm/exe.freebsd-x64': 12.5.1
'@pnpm/exe.linux-x64-musl': 12.3.4 '@pnpm/exe.linux-arm64': 12.5.1
'@pnpm/exe.win32-arm64': 12.3.4 '@pnpm/exe.linux-arm64-musl': 12.5.1
'@pnpm/exe.win32-x64': 12.3.4 '@pnpm/exe.linux-ppc64': 12.5.1
'@pnpm/exe.linux-riscv64': 12.5.1
'@pnpm/exe.linux-s390x': 12.5.1
'@pnpm/exe.linux-x64': 12.5.1
'@pnpm/exe.linux-x64-musl': 12.5.1
'@pnpm/exe.win32-arm64': 12.5.1
'@pnpm/exe.win32-x64': 12.5.1
--- ---
lockfileVersion: '9.0' lockfileVersion: '9.0'

View File

@ -5,8 +5,6 @@ set -ex
export CURRENT_VERSION=$1; export CURRENT_VERSION=$1;
export NODE_ENV=production; export NODE_ENV=production;
corepack enable;
corepack install || exit 1;
pnpm install || exit 1; pnpm install || exit 1;
pnpm run build:wasm; pnpm run build:wasm;
@ -35,8 +33,7 @@ cp "$WASM_SRC/render-wasm.js" "$WASM_SRC/render-wasm.wasm" target/$WASM_SRC/;
cat <<EOF | tee target/setup cat <<EOF | tee target/setup
#/usr/bin/env bash #/usr/bin/env bash
set -e; set -e;
corepack enable; command -v pnpm >/dev/null || { echo "error: pnpm not found in PATH" >&2; exit 1; };
corepack install;
pnpm install pnpm install
pnpm exec playwright install chromium; pnpm exec playwright install chromium;
EOF EOF

View File

@ -2,7 +2,5 @@
set -e; set -e;
corepack enable;
corepack install;
pnpm install; pnpm install;
pnpm exec playwright install chromium pnpm exec playwright install chromium

View File

@ -1,7 +1,5 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -ex set -ex
corepack enable;
corepack install;
pnpm install; pnpm install;
pnpm run test; pnpm run test;

View File

@ -4,7 +4,7 @@
"license": "MPL-2.0", "license": "MPL-2.0",
"author": "Kaleidos INC Sucursal en España SL", "author": "Kaleidos INC Sucursal en España SL",
"private": true, "private": true,
"packageManager": "pnpm@12.3.4+sha512.961aa41fb077da3a04a441d9f8e15ebc0c96da8ef710b2eb67bf9ee7cb0610eabd48f1fd85f51cffe73846785fa0f87c56a3a872a1d893f8446741b5cce45457", "packageManager": "pnpm@12.5.1+sha512.e3f305bc784a2bc89f5ad3b6138889470fae8d2af5f36b61216ec91c2c3d64089775f9de38aac331044ea40f245cb0d5666392dfdf65824e1907ef6a2c62de5f",
"browserslist": [ "browserslist": [
"defaults" "defaults"
], ],

View File

@ -4,7 +4,7 @@
"description": "Penpot Draft-JS Wrapper", "description": "Penpot Draft-JS Wrapper",
"main": "index.js", "main": "index.js",
"type": "module", "type": "module",
"packageManager": "pnpm@12.3.4+sha512.961aa41fb077da3a04a441d9f8e15ebc0c96da8ef710b2eb67bf9ee7cb0610eabd48f1fd85f51cffe73846785fa0f87c56a3a872a1d893f8446741b5cce45457", "packageManager": "pnpm@12.5.1+sha512.e3f305bc784a2bc89f5ad3b6138889470fae8d2af5f36b61216ec91c2c3d64089775f9de38aac331044ea40f245cb0d5666392dfdf65824e1907ef6a2c62de5f",
"author": "Andrey Antukh", "author": "Andrey Antukh",
"license": "MPL-2.0", "license": "MPL-2.0",
"dependencies": { "dependencies": {

View File

@ -4,7 +4,7 @@
"description": "Simple library for handling keyboard shortcuts", "description": "Simple library for handling keyboard shortcuts",
"main": "index.js", "main": "index.js",
"type": "module", "type": "module",
"packageManager": "pnpm@12.3.4+sha512.961aa41fb077da3a04a441d9f8e15ebc0c96da8ef710b2eb67bf9ee7cb0610eabd48f1fd85f51cffe73846785fa0f87c56a3a872a1d893f8446741b5cce45457", "packageManager": "pnpm@12.5.1+sha512.e3f305bc784a2bc89f5ad3b6138889470fae8d2af5f36b61216ec91c2c3d64089775f9de38aac331044ea40f245cb0d5666392dfdf65824e1907ef6a2c62de5f",
"author": "Craig Campbell", "author": "Craig Campbell",
"license": "Apache-2.0 WITH LLVM-exception" "license": "Apache-2.0 WITH LLVM-exception"
} }

View File

@ -4,7 +4,7 @@
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"type": "module", "type": "module",
"packageManager": "pnpm@12.3.4+sha512.961aa41fb077da3a04a441d9f8e15ebc0c96da8ef710b2eb67bf9ee7cb0610eabd48f1fd85f51cffe73846785fa0f87c56a3a872a1d893f8446741b5cce45457", "packageManager": "pnpm@12.5.1+sha512.e3f305bc784a2bc89f5ad3b6138889470fae8d2af5f36b61216ec91c2c3d64089775f9de38aac331044ea40f245cb0d5666392dfdf65824e1907ef6a2c62de5f",
"author": "Andrey Antukh", "author": "Andrey Antukh",
"license": "MPL-2.0", "license": "MPL-2.0",
"dependencies": { "dependencies": {

View File

@ -3,7 +3,7 @@
"version": "0.0.1", "version": "0.0.1",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"type": "module", "type": "module",
"packageManager": "pnpm@12.3.4+sha512.961aa41fb077da3a04a441d9f8e15ebc0c96da8ef710b2eb67bf9ee7cb0610eabd48f1fd85f51cffe73846785fa0f87c56a3a872a1d893f8446741b5cce45457", "packageManager": "pnpm@12.5.1+sha512.e3f305bc784a2bc89f5ad3b6138889470fae8d2af5f36b61216ec91c2c3d64089775f9de38aac331044ea40f245cb0d5666392dfdf65824e1907ef6a2c62de5f",
"exports": { "exports": {
".": { ".": {
"import": "./dist/index.js" "import": "./dist/index.js"

131
frontend/pnpm-lock.yaml generated
View File

@ -7,96 +7,153 @@ importers:
configDependencies: {} configDependencies: {}
packageManagerDependencies: packageManagerDependencies:
pnpm: pnpm:
specifier: 12.3.4 specifier: 12.5.1
version: 12.3.4 version: 12.5.1
packages: packages:
'@pnpm/exe.darwin-arm64@12.3.4': '@pnpm/exe.android-arm64@12.5.1':
resolution: {integrity: sha512-PAyUol8T1+/+ViOiXAt51ECA+QnfXCqz6foL4bW+LsoX0NcVd5XVEM2mRQu+LV4oc7uRz9zf9U0P+XFfuQeDAw==} resolution: {integrity: sha512-6LGBmQuDzUmgpHSuy+haG0jrmkwFNa4EuShDZqK3cIBv4Wfb75CzjJmowkzr+iJg40vnBKMQ62t4Ko6mwsId7A==}
cpu: [arm64]
os: [android]
'@pnpm/exe.android-x64@12.5.1':
resolution: {integrity: sha512-091oCltP3Rx89yxL1Wh5rXkUXEyFzyxIzSHKRFDyDFlQkb8Ox8FaDcm+M8PNkMyqnLA+qeGOLzY7Rjj9izl1Ww==}
cpu: [x64]
os: [android]
'@pnpm/exe.darwin-arm64@12.5.1':
resolution: {integrity: sha512-r1vEMQPVjRexZMyL2HonxJoo3+nuB/+G+tg+f2PK6faRdXX1TDwevKXOxJAfqlPInTy6W/pm912+s+m0NcR4BQ==}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
'@pnpm/exe.darwin-x64@12.3.4': '@pnpm/exe.darwin-x64@12.5.1':
resolution: {integrity: sha512-fxP9JCk0Cdye+ePuj+GJJLMUMTqHGWRdb1dtv4How876uQ2ehxvenpgiYAir/ceO9PsYUZkFTtyZdx+rRu5QOA==} resolution: {integrity: sha512-YHwI1g2B+Y2cAEylOR31r781GWijdJL7Gy7TEn/ZpZ5v/Cuwp5/Pz0EFCgo0M2MKLafAbL/laUMGvOpAdnerAw==}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
'@pnpm/exe.linux-arm64-musl@12.3.4': '@pnpm/exe.freebsd-x64@12.5.1':
resolution: {integrity: sha512-FBOt0/7ye6O6q4AllVV5QMviB6qE6fqkeczV/+MDWQsmo+QJrlfsh6X7CpH/tClVpBZEyIbjpUoT8bNhCYBxEg==} resolution: {integrity: sha512-sFUMHiYFs6q5syFwgaVppzhQSmqnXoJIz/YwTXIOXTDE76ndIpTRpDGAnBCBKjsaEBnqjJA/2l6hpJcNDNoMiA==}
cpu: [x64]
os: [freebsd]
'@pnpm/exe.linux-arm64-musl@12.5.1':
resolution: {integrity: sha512-zupuxYZBuBEGjUz805/4/Asq01UNziFADANt8yp+LdUjU9bbMa3xY78Bx1PsGDqBy7aiad4sdUMK8ZxH4n4gmQ==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl] libc: [musl]
'@pnpm/exe.linux-arm64@12.3.4': '@pnpm/exe.linux-arm64@12.5.1':
resolution: {integrity: sha512-t71AVA7LRqiKTyZ5xMYaZc2n5DfdpMbfokZuiIOXHBOM03ECnF0t4iYwaBDqJgVjlKYUOwaF/bRQajGNA4cJ4w==} resolution: {integrity: sha512-6hbMWW2/eQo1b5wvQPe/ndLHVTH88Rav6N+ffD4/h6IuFMjWCgasFYYsFv0GrvpqREAx3SwmZSvli+0NFXqbxA==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@pnpm/exe.linux-x64-musl@12.3.4': '@pnpm/exe.linux-ppc64@12.5.1':
resolution: {integrity: sha512-RPmk7Jb/aYaFvL2iyDN/AtMY+hUEsue732WmXpcuQ9tBpMnGyA5py7Z3+e+qmQaJ0zY/4ni9jJiyPBQHujmv6w==} resolution: {integrity: sha512-Gg3maDzaE/ZFMV9HIx/BwMN0Y6QbEs13URfxmiYHtKjjLAs5wEQZRMUP87YNLYO7cNaEfbM8GRbS9vPosyD9Gw==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@pnpm/exe.linux-riscv64@12.5.1':
resolution: {integrity: sha512-6fgWxXYMBMsWQ8DAiRevscX4QZQjwh+FsyCj8ZOk8pMXvqeq84MqBLCmS6KckvnbdqJ+HlSVQ7fs/kd0ldwfhA==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@pnpm/exe.linux-s390x@12.5.1':
resolution: {integrity: sha512-nzMZruRvxDhEfYUhzu+7EmM39+82Dgagc/6hPFPqGqzXNGcIG0anuxbUxfeNtI64SHt91KuuYUmCB/inlM+Pag==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@pnpm/exe.linux-x64-musl@12.5.1':
resolution: {integrity: sha512-kHCbegdTNHpzmPtUD4S5fqkVkqgdQR47spF9p2VPP3dIIkZl6Ytk8w0vhlAsAGgge8FmucfwX+vvPKgm0NtOCQ==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl] libc: [musl]
'@pnpm/exe.linux-x64@12.3.4': '@pnpm/exe.linux-x64@12.5.1':
resolution: {integrity: sha512-2ZqOlSPkfwX1h5cR+FPiWf8+F+2hZT/3TvhUK5sigHqwaQCIiq8R7CGxhndKs63JtcLi2a1Qpo+wX/EoyfjyJQ==} resolution: {integrity: sha512-3PkUBYo5z4dgtlnTNIFj7QGpcDUAuqXz9WGVigPDCeccEnhGiRkWmA5102TmYJHtwJP3LfmE+ZF9PGeWhn8p9Q==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@pnpm/exe.win32-arm64@12.3.4': '@pnpm/exe.win32-arm64@12.5.1':
resolution: {integrity: sha512-ANyrHqyqco6SXBysUTRF74itDyyraea7IbFsKFdNXTjcFnfycTDx37EwuhdpPYFNSIh2JhUG4fByclsRfiHX7w==} resolution: {integrity: sha512-49NvjZIXHly56/PLoEDD7VphbO4N8dwMXoApElDFCpVGDNGeKlwlib1tXtnpTF6i2/XIsAj7GgatHB5KHXgBnA==}
cpu: [arm64] cpu: [arm64]
os: [win32] os: [win32]
'@pnpm/exe.win32-x64@12.3.4': '@pnpm/exe.win32-x64@12.5.1':
resolution: {integrity: sha512-WH/KqBPY/hq2Tb7SgQltEZytimcjgKRaCRL/aM9CI0c67iKc5TVmHUhIiL3Ux9FB4bWn36i6XewUcScQI+zG8w==} resolution: {integrity: sha512-ePUmQFPAPJFNMwg1lelDx2PdjHqYRiF0yfg5e6qeDbPNOu97QNo557Ru/SxmCrmt8ydQWNuZXWpdEp019md5vw==}
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
pnpm@12.3.4: pnpm@12.5.1:
resolution: {integrity: sha512-lhqkH7B32joEpEHZ+OFevAyW2o73ELLrZ7+e58sGEOq9SPH9hfUc/+c4RnhfoPh8VqOocqHYk/hEZ0G1zORUVw==} resolution: {integrity: sha512-4/MFvHhKK8ifWtO2E4iJRw+ujSr182thIW7JHCw9ZAiXdfneOKrDMQROpA8kXLDVZmOS399lgk4ZB+9qLGLeXw==}
engines: {node: '>=18.*'} engines: {node: '>=18.*'}
hasBin: true hasBin: true
snapshots: snapshots:
'@pnpm/exe.darwin-arm64@12.3.4': '@pnpm/exe.android-arm64@12.5.1':
optional: true optional: true
'@pnpm/exe.darwin-x64@12.3.4': '@pnpm/exe.android-x64@12.5.1':
optional: true optional: true
'@pnpm/exe.linux-arm64-musl@12.3.4': '@pnpm/exe.darwin-arm64@12.5.1':
optional: true optional: true
'@pnpm/exe.linux-arm64@12.3.4': '@pnpm/exe.darwin-x64@12.5.1':
optional: true optional: true
'@pnpm/exe.linux-x64-musl@12.3.4': '@pnpm/exe.freebsd-x64@12.5.1':
optional: true optional: true
'@pnpm/exe.linux-x64@12.3.4': '@pnpm/exe.linux-arm64-musl@12.5.1':
optional: true optional: true
'@pnpm/exe.win32-arm64@12.3.4': '@pnpm/exe.linux-arm64@12.5.1':
optional: true optional: true
'@pnpm/exe.win32-x64@12.3.4': '@pnpm/exe.linux-ppc64@12.5.1':
optional: true optional: true
pnpm@12.3.4: '@pnpm/exe.linux-riscv64@12.5.1':
optional: true
'@pnpm/exe.linux-s390x@12.5.1':
optional: true
'@pnpm/exe.linux-x64-musl@12.5.1':
optional: true
'@pnpm/exe.linux-x64@12.5.1':
optional: true
'@pnpm/exe.win32-arm64@12.5.1':
optional: true
'@pnpm/exe.win32-x64@12.5.1':
optional: true
pnpm@12.5.1:
optionalDependencies: optionalDependencies:
'@pnpm/exe.darwin-arm64': 12.3.4 '@pnpm/exe.android-arm64': 12.5.1
'@pnpm/exe.darwin-x64': 12.3.4 '@pnpm/exe.android-x64': 12.5.1
'@pnpm/exe.linux-arm64': 12.3.4 '@pnpm/exe.darwin-arm64': 12.5.1
'@pnpm/exe.linux-arm64-musl': 12.3.4 '@pnpm/exe.darwin-x64': 12.5.1
'@pnpm/exe.linux-x64': 12.3.4 '@pnpm/exe.freebsd-x64': 12.5.1
'@pnpm/exe.linux-x64-musl': 12.3.4 '@pnpm/exe.linux-arm64': 12.5.1
'@pnpm/exe.win32-arm64': 12.3.4 '@pnpm/exe.linux-arm64-musl': 12.5.1
'@pnpm/exe.win32-x64': 12.3.4 '@pnpm/exe.linux-ppc64': 12.5.1
'@pnpm/exe.linux-riscv64': 12.5.1
'@pnpm/exe.linux-s390x': 12.5.1
'@pnpm/exe.linux-x64': 12.5.1
'@pnpm/exe.linux-x64-musl': 12.5.1
'@pnpm/exe.win32-arm64': 12.5.1
'@pnpm/exe.win32-x64': 12.5.1
--- ---
lockfileVersion: '9.0' lockfileVersion: '9.0'

View File

@ -18,8 +18,6 @@ export NODE_ENV=production;
rm -rf node_modules; rm -rf node_modules;
corepack enable;
corepack install;
pnpm install; pnpm install;
rm -rf target/dist; rm -rf target/dist;
@ -43,7 +41,6 @@ popd;
pushd ../plugins pushd ../plugins
rm -rf node_modules; rm -rf node_modules;
rm -rf dist/apps/; rm -rf dist/apps/;
corepack install;
pnpm -r install; pnpm -r install;
pnpm run build:plugins; pnpm run build:plugins;
popd popd

View File

@ -12,8 +12,6 @@ export VERSION_TAG="${VERSION}-${BUILD_TS}";
export NODE_ENV=production; export NODE_ENV=production;
corepack enable;
corepack install;
pnpm install; pnpm install;
pnpm run build:storybook; pnpm run build:storybook;

View File

@ -1,6 +1,4 @@
#!/usr/bin/env bash #!/usr/bin/env bash
corepack enable;
corepack install;
pnpm install; pnpm install;
pnpm exec playwright install chromium; pnpm exec playwright install chromium;

View File

@ -1,8 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -ex set -ex
corepack enable;
corepack install;
pnpm install; pnpm install;
# Build render wasm binary # Build render wasm binary

View File

@ -4,7 +4,5 @@ TARGET=${1:-app};
set -ex set -ex
corepack enable;
corepack install;
pnpm install; pnpm install;
pnpm run watch:$TARGET pnpm run watch:$TARGET

View File

@ -28,5 +28,5 @@
"vite": "^8.2.0", "vite": "^8.2.0",
"vitest": "^4.1.10" "vitest": "^4.1.10"
}, },
"packageManager": "pnpm@12.3.4+sha512.961aa41fb077da3a04a441d9f8e15ebc0c96da8ef710b2eb67bf9ee7cb0610eabd48f1fd85f51cffe73846785fa0f87c56a3a872a1d893f8446741b5cce45457" "packageManager": "pnpm@12.5.1+sha512.e3f305bc784a2bc89f5ad3b6138889470fae8d2af5f36b61216ec91c2c3d64089775f9de38aac331044ea40f245cb0d5666392dfdf65824e1907ef6a2c62de5f"
} }

View File

@ -3,7 +3,7 @@
"version": "1.2.0-RC1", "version": "1.2.0-RC1",
"license": "MPL-2.0", "license": "MPL-2.0",
"author": "Kaleidos INC Sucursal en España SL", "author": "Kaleidos INC Sucursal en España SL",
"packageManager": "pnpm@12.3.4+sha512.961aa41fb077da3a04a441d9f8e15ebc0c96da8ef710b2eb67bf9ee7cb0610eabd48f1fd85f51cffe73846785fa0f87c56a3a872a1d893f8446741b5cce45457", "packageManager": "pnpm@12.5.1+sha512.e3f305bc784a2bc89f5ad3b6138889470fae8d2af5f36b61216ec91c2c3d64089775f9de38aac331044ea40f245cb0d5666392dfdf65824e1907ef6a2c62de5f",
"type": "module", "type": "module",
"repository": { "repository": {
"type": "git", "type": "git",

131
library/pnpm-lock.yaml generated
View File

@ -7,96 +7,153 @@ importers:
configDependencies: {} configDependencies: {}
packageManagerDependencies: packageManagerDependencies:
pnpm: pnpm:
specifier: 12.3.4 specifier: 12.5.1
version: 12.3.4 version: 12.5.1
packages: packages:
'@pnpm/exe.darwin-arm64@12.3.4': '@pnpm/exe.android-arm64@12.5.1':
resolution: {integrity: sha512-PAyUol8T1+/+ViOiXAt51ECA+QnfXCqz6foL4bW+LsoX0NcVd5XVEM2mRQu+LV4oc7uRz9zf9U0P+XFfuQeDAw==} resolution: {integrity: sha512-6LGBmQuDzUmgpHSuy+haG0jrmkwFNa4EuShDZqK3cIBv4Wfb75CzjJmowkzr+iJg40vnBKMQ62t4Ko6mwsId7A==}
cpu: [arm64]
os: [android]
'@pnpm/exe.android-x64@12.5.1':
resolution: {integrity: sha512-091oCltP3Rx89yxL1Wh5rXkUXEyFzyxIzSHKRFDyDFlQkb8Ox8FaDcm+M8PNkMyqnLA+qeGOLzY7Rjj9izl1Ww==}
cpu: [x64]
os: [android]
'@pnpm/exe.darwin-arm64@12.5.1':
resolution: {integrity: sha512-r1vEMQPVjRexZMyL2HonxJoo3+nuB/+G+tg+f2PK6faRdXX1TDwevKXOxJAfqlPInTy6W/pm912+s+m0NcR4BQ==}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
'@pnpm/exe.darwin-x64@12.3.4': '@pnpm/exe.darwin-x64@12.5.1':
resolution: {integrity: sha512-fxP9JCk0Cdye+ePuj+GJJLMUMTqHGWRdb1dtv4How876uQ2ehxvenpgiYAir/ceO9PsYUZkFTtyZdx+rRu5QOA==} resolution: {integrity: sha512-YHwI1g2B+Y2cAEylOR31r781GWijdJL7Gy7TEn/ZpZ5v/Cuwp5/Pz0EFCgo0M2MKLafAbL/laUMGvOpAdnerAw==}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
'@pnpm/exe.linux-arm64-musl@12.3.4': '@pnpm/exe.freebsd-x64@12.5.1':
resolution: {integrity: sha512-FBOt0/7ye6O6q4AllVV5QMviB6qE6fqkeczV/+MDWQsmo+QJrlfsh6X7CpH/tClVpBZEyIbjpUoT8bNhCYBxEg==} resolution: {integrity: sha512-sFUMHiYFs6q5syFwgaVppzhQSmqnXoJIz/YwTXIOXTDE76ndIpTRpDGAnBCBKjsaEBnqjJA/2l6hpJcNDNoMiA==}
cpu: [x64]
os: [freebsd]
'@pnpm/exe.linux-arm64-musl@12.5.1':
resolution: {integrity: sha512-zupuxYZBuBEGjUz805/4/Asq01UNziFADANt8yp+LdUjU9bbMa3xY78Bx1PsGDqBy7aiad4sdUMK8ZxH4n4gmQ==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl] libc: [musl]
'@pnpm/exe.linux-arm64@12.3.4': '@pnpm/exe.linux-arm64@12.5.1':
resolution: {integrity: sha512-t71AVA7LRqiKTyZ5xMYaZc2n5DfdpMbfokZuiIOXHBOM03ECnF0t4iYwaBDqJgVjlKYUOwaF/bRQajGNA4cJ4w==} resolution: {integrity: sha512-6hbMWW2/eQo1b5wvQPe/ndLHVTH88Rav6N+ffD4/h6IuFMjWCgasFYYsFv0GrvpqREAx3SwmZSvli+0NFXqbxA==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@pnpm/exe.linux-x64-musl@12.3.4': '@pnpm/exe.linux-ppc64@12.5.1':
resolution: {integrity: sha512-RPmk7Jb/aYaFvL2iyDN/AtMY+hUEsue732WmXpcuQ9tBpMnGyA5py7Z3+e+qmQaJ0zY/4ni9jJiyPBQHujmv6w==} resolution: {integrity: sha512-Gg3maDzaE/ZFMV9HIx/BwMN0Y6QbEs13URfxmiYHtKjjLAs5wEQZRMUP87YNLYO7cNaEfbM8GRbS9vPosyD9Gw==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@pnpm/exe.linux-riscv64@12.5.1':
resolution: {integrity: sha512-6fgWxXYMBMsWQ8DAiRevscX4QZQjwh+FsyCj8ZOk8pMXvqeq84MqBLCmS6KckvnbdqJ+HlSVQ7fs/kd0ldwfhA==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@pnpm/exe.linux-s390x@12.5.1':
resolution: {integrity: sha512-nzMZruRvxDhEfYUhzu+7EmM39+82Dgagc/6hPFPqGqzXNGcIG0anuxbUxfeNtI64SHt91KuuYUmCB/inlM+Pag==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@pnpm/exe.linux-x64-musl@12.5.1':
resolution: {integrity: sha512-kHCbegdTNHpzmPtUD4S5fqkVkqgdQR47spF9p2VPP3dIIkZl6Ytk8w0vhlAsAGgge8FmucfwX+vvPKgm0NtOCQ==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl] libc: [musl]
'@pnpm/exe.linux-x64@12.3.4': '@pnpm/exe.linux-x64@12.5.1':
resolution: {integrity: sha512-2ZqOlSPkfwX1h5cR+FPiWf8+F+2hZT/3TvhUK5sigHqwaQCIiq8R7CGxhndKs63JtcLi2a1Qpo+wX/EoyfjyJQ==} resolution: {integrity: sha512-3PkUBYo5z4dgtlnTNIFj7QGpcDUAuqXz9WGVigPDCeccEnhGiRkWmA5102TmYJHtwJP3LfmE+ZF9PGeWhn8p9Q==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@pnpm/exe.win32-arm64@12.3.4': '@pnpm/exe.win32-arm64@12.5.1':
resolution: {integrity: sha512-ANyrHqyqco6SXBysUTRF74itDyyraea7IbFsKFdNXTjcFnfycTDx37EwuhdpPYFNSIh2JhUG4fByclsRfiHX7w==} resolution: {integrity: sha512-49NvjZIXHly56/PLoEDD7VphbO4N8dwMXoApElDFCpVGDNGeKlwlib1tXtnpTF6i2/XIsAj7GgatHB5KHXgBnA==}
cpu: [arm64] cpu: [arm64]
os: [win32] os: [win32]
'@pnpm/exe.win32-x64@12.3.4': '@pnpm/exe.win32-x64@12.5.1':
resolution: {integrity: sha512-WH/KqBPY/hq2Tb7SgQltEZytimcjgKRaCRL/aM9CI0c67iKc5TVmHUhIiL3Ux9FB4bWn36i6XewUcScQI+zG8w==} resolution: {integrity: sha512-ePUmQFPAPJFNMwg1lelDx2PdjHqYRiF0yfg5e6qeDbPNOu97QNo557Ru/SxmCrmt8ydQWNuZXWpdEp019md5vw==}
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
pnpm@12.3.4: pnpm@12.5.1:
resolution: {integrity: sha512-lhqkH7B32joEpEHZ+OFevAyW2o73ELLrZ7+e58sGEOq9SPH9hfUc/+c4RnhfoPh8VqOocqHYk/hEZ0G1zORUVw==} resolution: {integrity: sha512-4/MFvHhKK8ifWtO2E4iJRw+ujSr182thIW7JHCw9ZAiXdfneOKrDMQROpA8kXLDVZmOS399lgk4ZB+9qLGLeXw==}
engines: {node: '>=18.*'} engines: {node: '>=18.*'}
hasBin: true hasBin: true
snapshots: snapshots:
'@pnpm/exe.darwin-arm64@12.3.4': '@pnpm/exe.android-arm64@12.5.1':
optional: true optional: true
'@pnpm/exe.darwin-x64@12.3.4': '@pnpm/exe.android-x64@12.5.1':
optional: true optional: true
'@pnpm/exe.linux-arm64-musl@12.3.4': '@pnpm/exe.darwin-arm64@12.5.1':
optional: true optional: true
'@pnpm/exe.linux-arm64@12.3.4': '@pnpm/exe.darwin-x64@12.5.1':
optional: true optional: true
'@pnpm/exe.linux-x64-musl@12.3.4': '@pnpm/exe.freebsd-x64@12.5.1':
optional: true optional: true
'@pnpm/exe.linux-x64@12.3.4': '@pnpm/exe.linux-arm64-musl@12.5.1':
optional: true optional: true
'@pnpm/exe.win32-arm64@12.3.4': '@pnpm/exe.linux-arm64@12.5.1':
optional: true optional: true
'@pnpm/exe.win32-x64@12.3.4': '@pnpm/exe.linux-ppc64@12.5.1':
optional: true optional: true
pnpm@12.3.4: '@pnpm/exe.linux-riscv64@12.5.1':
optional: true
'@pnpm/exe.linux-s390x@12.5.1':
optional: true
'@pnpm/exe.linux-x64-musl@12.5.1':
optional: true
'@pnpm/exe.linux-x64@12.5.1':
optional: true
'@pnpm/exe.win32-arm64@12.5.1':
optional: true
'@pnpm/exe.win32-x64@12.5.1':
optional: true
pnpm@12.5.1:
optionalDependencies: optionalDependencies:
'@pnpm/exe.darwin-arm64': 12.3.4 '@pnpm/exe.android-arm64': 12.5.1
'@pnpm/exe.darwin-x64': 12.3.4 '@pnpm/exe.android-x64': 12.5.1
'@pnpm/exe.linux-arm64': 12.3.4 '@pnpm/exe.darwin-arm64': 12.5.1
'@pnpm/exe.linux-arm64-musl': 12.3.4 '@pnpm/exe.darwin-x64': 12.5.1
'@pnpm/exe.linux-x64': 12.3.4 '@pnpm/exe.freebsd-x64': 12.5.1
'@pnpm/exe.linux-x64-musl': 12.3.4 '@pnpm/exe.linux-arm64': 12.5.1
'@pnpm/exe.win32-arm64': 12.3.4 '@pnpm/exe.linux-arm64-musl': 12.5.1
'@pnpm/exe.win32-x64': 12.3.4 '@pnpm/exe.linux-ppc64': 12.5.1
'@pnpm/exe.linux-riscv64': 12.5.1
'@pnpm/exe.linux-s390x': 12.5.1
'@pnpm/exe.linux-x64': 12.5.1
'@pnpm/exe.linux-x64-musl': 12.5.1
'@pnpm/exe.win32-arm64': 12.5.1
'@pnpm/exe.win32-x64': 12.5.1
--- ---
lockfileVersion: '9.0' lockfileVersion: '9.0'

View File

@ -1,8 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -ex set -ex
corepack enable;
corepack install;
pnpm install; pnpm install;
pnpm run build:bundle; pnpm run build:bundle;

View File

@ -209,9 +209,7 @@ function ensure-devenv-network {
# Compose-project plumbing for the parallel-workspaces layout. # Compose-project plumbing for the parallel-workspaces layout.
# #
# - Shared infrastructure (postgres, minio, mailer, ldap, minio-setup) runs # - Shared infrastructure (postgres, RustFS, mailer, LDAP, Valkey)
# under project `penpotdev-infra`.
# - Shared infrastructure (postgres, minio, mailer, ldap, valkey, minio-setup)
# runs under project `penpotdev-infra`. # runs under project `penpotdev-infra`.
# - Each runtime instance (ws0, ws1, ...) runs only its own main container # - Each runtime instance (ws0, ws1, ...) runs only its own main container
# under project `penpotdev-wsN`. All workspaces uniformly overlay their # under project `penpotdev-wsN`. All workspaces uniformly overlay their
@ -296,16 +294,11 @@ function devenv-main-running {
[[ -n "$container" ]] && [[ "$(docker inspect -f '{{.State.Running}}' "$container" 2>/dev/null)" = "true" ]] [[ -n "$container" ]] && [[ "$(docker inspect -f '{{.State.Running}}' "$container" 2>/dev/null)" = "true" ]]
} }
# Bring shared infra up and block until minio-setup has provisioned the # Bring shared infra up and block until services with healthchecks are healthy.
# shared MinIO user/policy. Idempotent: a second call when everything is # Removing orphaned containers retires old infra services without deleting
# already up returns immediately. # their named volumes.
function ensure-infra-up { function ensure-infra-up {
infra-compose up -d infra-compose up -d --wait --wait-timeout 60 --remove-orphans
local setup_container
setup_container=$(infra-compose ps -aq minio-setup 2>/dev/null)
if [[ -n "$setup_container" ]]; then
docker wait "$setup_container" >/dev/null 2>&1 || true
fi
} }
# Refuse to sync workspaces if the live repo is in a fragile Git state. # Refuse to sync workspaces if the live repo is in a fragile Git state.

View File

@ -68,7 +68,7 @@ Once the servers are running, continue with step 2.
#### Running the Source Version from the Repository #### Running the Source Version from the Repository
The tools `corepack` and `npx` should be available in your terminal. The tools `pnpm` and `npx` should be available in your terminal.
On Windows, use the Git Bash terminal to ensure compatibility with the provided scripts. On Windows, use the Git Bash terminal to ensure compatibility with the provided scripts.

View File

@ -23,7 +23,7 @@
"type": "git", "type": "git",
"url": "https://github.com/penpot/penpot.git" "url": "https://github.com/penpot/penpot.git"
}, },
"packageManager": "pnpm@12.3.4+sha512.961aa41fb077da3a04a441d9f8e15ebc0c96da8ef710b2eb67bf9ee7cb0610eabd48f1fd85f51cffe73846785fa0f87c56a3a872a1d893f8446741b5cce45457", "packageManager": "pnpm@12.5.1+sha512.e3f305bc784a2bc89f5ad3b6138889470fae8d2af5f36b61216ec91c2c3d64089775f9de38aac331044ea40f245cb0d5666392dfdf65824e1907ef6a2c62de5f",
"devDependencies": { "devDependencies": {
"concurrently": "^10.0.5", "concurrently": "^10.0.5",
"prettier": "^3.9.6" "prettier": "^3.9.6"

View File

@ -4,7 +4,7 @@
"description": "Shared type definitions and interfaces for Penpot MCP", "description": "Shared type definitions and interfaces for Penpot MCP",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
"packageManager": "pnpm@12.3.4+sha512.961aa41fb077da3a04a441d9f8e15ebc0c96da8ef710b2eb67bf9ee7cb0610eabd48f1fd85f51cffe73846785fa0f87c56a3a872a1d893f8446741b5cce45457", "packageManager": "pnpm@12.5.1+sha512.e3f305bc784a2bc89f5ad3b6138889470fae8d2af5f36b61216ec91c2c3d64089775f9de38aac331044ea40f245cb0d5666392dfdf65824e1907ef6a2c62de5f",
"scripts": { "scripts": {
"build": "tsc --build --clean && tsc --build", "build": "tsc --build --clean && tsc --build",
"watch": "tsc --watch", "watch": "tsc --watch",

View File

@ -3,7 +3,7 @@
"private": true, "private": true,
"version": "1.0.0", "version": "1.0.0",
"type": "module", "type": "module",
"packageManager": "pnpm@12.3.4+sha512.961aa41fb077da3a04a441d9f8e15ebc0c96da8ef710b2eb67bf9ee7cb0610eabd48f1fd85f51cffe73846785fa0f87c56a3a872a1d893f8446741b5cce45457", "packageManager": "pnpm@12.5.1+sha512.e3f305bc784a2bc89f5ad3b6138889470fae8d2af5f36b61216ec91c2c3d64089775f9de38aac331044ea40f245cb0d5666392dfdf65824e1907ef6a2c62de5f",
"scripts": { "scripts": {
"start": "vite build --watch --config vite.config.ts", "start": "vite build --watch --config vite.config.ts",
"start:multi-user": "pnpm run start", "start:multi-user": "pnpm run start",

View File

@ -24,7 +24,7 @@
], ],
"author": "", "author": "",
"license": "MIT", "license": "MIT",
"packageManager": "pnpm@12.3.4+sha512.961aa41fb077da3a04a441d9f8e15ebc0c96da8ef710b2eb67bf9ee7cb0610eabd48f1fd85f51cffe73846785fa0f87c56a3a872a1d893f8446741b5cce45457", "packageManager": "pnpm@12.5.1+sha512.e3f305bc784a2bc89f5ad3b6138889470fae8d2af5f36b61216ec91c2c3d64089775f9de38aac331044ea40f245cb0d5666392dfdf65824e1907ef6a2c62de5f",
"dependencies": { "dependencies": {
"@modelcontextprotocol/sdk": "^1.29.0", "@modelcontextprotocol/sdk": "^1.29.0",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",

131
mcp/pnpm-lock.yaml generated
View File

@ -7,96 +7,153 @@ importers:
configDependencies: {} configDependencies: {}
packageManagerDependencies: packageManagerDependencies:
pnpm: pnpm:
specifier: 12.3.4 specifier: 12.5.1
version: 12.3.4 version: 12.5.1
packages: packages:
'@pnpm/exe.darwin-arm64@12.3.4': '@pnpm/exe.android-arm64@12.5.1':
resolution: {integrity: sha512-PAyUol8T1+/+ViOiXAt51ECA+QnfXCqz6foL4bW+LsoX0NcVd5XVEM2mRQu+LV4oc7uRz9zf9U0P+XFfuQeDAw==} resolution: {integrity: sha512-6LGBmQuDzUmgpHSuy+haG0jrmkwFNa4EuShDZqK3cIBv4Wfb75CzjJmowkzr+iJg40vnBKMQ62t4Ko6mwsId7A==}
cpu: [arm64]
os: [android]
'@pnpm/exe.android-x64@12.5.1':
resolution: {integrity: sha512-091oCltP3Rx89yxL1Wh5rXkUXEyFzyxIzSHKRFDyDFlQkb8Ox8FaDcm+M8PNkMyqnLA+qeGOLzY7Rjj9izl1Ww==}
cpu: [x64]
os: [android]
'@pnpm/exe.darwin-arm64@12.5.1':
resolution: {integrity: sha512-r1vEMQPVjRexZMyL2HonxJoo3+nuB/+G+tg+f2PK6faRdXX1TDwevKXOxJAfqlPInTy6W/pm912+s+m0NcR4BQ==}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
'@pnpm/exe.darwin-x64@12.3.4': '@pnpm/exe.darwin-x64@12.5.1':
resolution: {integrity: sha512-fxP9JCk0Cdye+ePuj+GJJLMUMTqHGWRdb1dtv4How876uQ2ehxvenpgiYAir/ceO9PsYUZkFTtyZdx+rRu5QOA==} resolution: {integrity: sha512-YHwI1g2B+Y2cAEylOR31r781GWijdJL7Gy7TEn/ZpZ5v/Cuwp5/Pz0EFCgo0M2MKLafAbL/laUMGvOpAdnerAw==}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
'@pnpm/exe.linux-arm64-musl@12.3.4': '@pnpm/exe.freebsd-x64@12.5.1':
resolution: {integrity: sha512-FBOt0/7ye6O6q4AllVV5QMviB6qE6fqkeczV/+MDWQsmo+QJrlfsh6X7CpH/tClVpBZEyIbjpUoT8bNhCYBxEg==} resolution: {integrity: sha512-sFUMHiYFs6q5syFwgaVppzhQSmqnXoJIz/YwTXIOXTDE76ndIpTRpDGAnBCBKjsaEBnqjJA/2l6hpJcNDNoMiA==}
cpu: [x64]
os: [freebsd]
'@pnpm/exe.linux-arm64-musl@12.5.1':
resolution: {integrity: sha512-zupuxYZBuBEGjUz805/4/Asq01UNziFADANt8yp+LdUjU9bbMa3xY78Bx1PsGDqBy7aiad4sdUMK8ZxH4n4gmQ==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl] libc: [musl]
'@pnpm/exe.linux-arm64@12.3.4': '@pnpm/exe.linux-arm64@12.5.1':
resolution: {integrity: sha512-t71AVA7LRqiKTyZ5xMYaZc2n5DfdpMbfokZuiIOXHBOM03ECnF0t4iYwaBDqJgVjlKYUOwaF/bRQajGNA4cJ4w==} resolution: {integrity: sha512-6hbMWW2/eQo1b5wvQPe/ndLHVTH88Rav6N+ffD4/h6IuFMjWCgasFYYsFv0GrvpqREAx3SwmZSvli+0NFXqbxA==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@pnpm/exe.linux-x64-musl@12.3.4': '@pnpm/exe.linux-ppc64@12.5.1':
resolution: {integrity: sha512-RPmk7Jb/aYaFvL2iyDN/AtMY+hUEsue732WmXpcuQ9tBpMnGyA5py7Z3+e+qmQaJ0zY/4ni9jJiyPBQHujmv6w==} resolution: {integrity: sha512-Gg3maDzaE/ZFMV9HIx/BwMN0Y6QbEs13URfxmiYHtKjjLAs5wEQZRMUP87YNLYO7cNaEfbM8GRbS9vPosyD9Gw==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@pnpm/exe.linux-riscv64@12.5.1':
resolution: {integrity: sha512-6fgWxXYMBMsWQ8DAiRevscX4QZQjwh+FsyCj8ZOk8pMXvqeq84MqBLCmS6KckvnbdqJ+HlSVQ7fs/kd0ldwfhA==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@pnpm/exe.linux-s390x@12.5.1':
resolution: {integrity: sha512-nzMZruRvxDhEfYUhzu+7EmM39+82Dgagc/6hPFPqGqzXNGcIG0anuxbUxfeNtI64SHt91KuuYUmCB/inlM+Pag==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@pnpm/exe.linux-x64-musl@12.5.1':
resolution: {integrity: sha512-kHCbegdTNHpzmPtUD4S5fqkVkqgdQR47spF9p2VPP3dIIkZl6Ytk8w0vhlAsAGgge8FmucfwX+vvPKgm0NtOCQ==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl] libc: [musl]
'@pnpm/exe.linux-x64@12.3.4': '@pnpm/exe.linux-x64@12.5.1':
resolution: {integrity: sha512-2ZqOlSPkfwX1h5cR+FPiWf8+F+2hZT/3TvhUK5sigHqwaQCIiq8R7CGxhndKs63JtcLi2a1Qpo+wX/EoyfjyJQ==} resolution: {integrity: sha512-3PkUBYo5z4dgtlnTNIFj7QGpcDUAuqXz9WGVigPDCeccEnhGiRkWmA5102TmYJHtwJP3LfmE+ZF9PGeWhn8p9Q==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@pnpm/exe.win32-arm64@12.3.4': '@pnpm/exe.win32-arm64@12.5.1':
resolution: {integrity: sha512-ANyrHqyqco6SXBysUTRF74itDyyraea7IbFsKFdNXTjcFnfycTDx37EwuhdpPYFNSIh2JhUG4fByclsRfiHX7w==} resolution: {integrity: sha512-49NvjZIXHly56/PLoEDD7VphbO4N8dwMXoApElDFCpVGDNGeKlwlib1tXtnpTF6i2/XIsAj7GgatHB5KHXgBnA==}
cpu: [arm64] cpu: [arm64]
os: [win32] os: [win32]
'@pnpm/exe.win32-x64@12.3.4': '@pnpm/exe.win32-x64@12.5.1':
resolution: {integrity: sha512-WH/KqBPY/hq2Tb7SgQltEZytimcjgKRaCRL/aM9CI0c67iKc5TVmHUhIiL3Ux9FB4bWn36i6XewUcScQI+zG8w==} resolution: {integrity: sha512-ePUmQFPAPJFNMwg1lelDx2PdjHqYRiF0yfg5e6qeDbPNOu97QNo557Ru/SxmCrmt8ydQWNuZXWpdEp019md5vw==}
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
pnpm@12.3.4: pnpm@12.5.1:
resolution: {integrity: sha512-lhqkH7B32joEpEHZ+OFevAyW2o73ELLrZ7+e58sGEOq9SPH9hfUc/+c4RnhfoPh8VqOocqHYk/hEZ0G1zORUVw==} resolution: {integrity: sha512-4/MFvHhKK8ifWtO2E4iJRw+ujSr182thIW7JHCw9ZAiXdfneOKrDMQROpA8kXLDVZmOS399lgk4ZB+9qLGLeXw==}
engines: {node: '>=18.*'} engines: {node: '>=18.*'}
hasBin: true hasBin: true
snapshots: snapshots:
'@pnpm/exe.darwin-arm64@12.3.4': '@pnpm/exe.android-arm64@12.5.1':
optional: true optional: true
'@pnpm/exe.darwin-x64@12.3.4': '@pnpm/exe.android-x64@12.5.1':
optional: true optional: true
'@pnpm/exe.linux-arm64-musl@12.3.4': '@pnpm/exe.darwin-arm64@12.5.1':
optional: true optional: true
'@pnpm/exe.linux-arm64@12.3.4': '@pnpm/exe.darwin-x64@12.5.1':
optional: true optional: true
'@pnpm/exe.linux-x64-musl@12.3.4': '@pnpm/exe.freebsd-x64@12.5.1':
optional: true optional: true
'@pnpm/exe.linux-x64@12.3.4': '@pnpm/exe.linux-arm64-musl@12.5.1':
optional: true optional: true
'@pnpm/exe.win32-arm64@12.3.4': '@pnpm/exe.linux-arm64@12.5.1':
optional: true optional: true
'@pnpm/exe.win32-x64@12.3.4': '@pnpm/exe.linux-ppc64@12.5.1':
optional: true optional: true
pnpm@12.3.4: '@pnpm/exe.linux-riscv64@12.5.1':
optional: true
'@pnpm/exe.linux-s390x@12.5.1':
optional: true
'@pnpm/exe.linux-x64-musl@12.5.1':
optional: true
'@pnpm/exe.linux-x64@12.5.1':
optional: true
'@pnpm/exe.win32-arm64@12.5.1':
optional: true
'@pnpm/exe.win32-x64@12.5.1':
optional: true
pnpm@12.5.1:
optionalDependencies: optionalDependencies:
'@pnpm/exe.darwin-arm64': 12.3.4 '@pnpm/exe.android-arm64': 12.5.1
'@pnpm/exe.darwin-x64': 12.3.4 '@pnpm/exe.android-x64': 12.5.1
'@pnpm/exe.linux-arm64': 12.3.4 '@pnpm/exe.darwin-arm64': 12.5.1
'@pnpm/exe.linux-arm64-musl': 12.3.4 '@pnpm/exe.darwin-x64': 12.5.1
'@pnpm/exe.linux-x64': 12.3.4 '@pnpm/exe.freebsd-x64': 12.5.1
'@pnpm/exe.linux-x64-musl': 12.3.4 '@pnpm/exe.linux-arm64': 12.5.1
'@pnpm/exe.win32-arm64': 12.3.4 '@pnpm/exe.linux-arm64-musl': 12.5.1
'@pnpm/exe.win32-x64': 12.3.4 '@pnpm/exe.linux-ppc64': 12.5.1
'@pnpm/exe.linux-riscv64': 12.5.1
'@pnpm/exe.linux-s390x': 12.5.1
'@pnpm/exe.linux-x64': 12.5.1
'@pnpm/exe.linux-x64-musl': 12.5.1
'@pnpm/exe.win32-arm64': 12.5.1
'@pnpm/exe.win32-x64': 12.5.1
--- ---
lockfileVersion: '9.0' lockfileVersion: '9.0'

View File

@ -9,9 +9,6 @@ echo "Preparing bundle for types from $URL"
set -ex set -ex
corepack enable;
corepack install;
# Ensure clean working directory # Ensure clean working directory
rm -rf dist; rm -rf dist;
rm -rf node_modules; rm -rf node_modules;
@ -37,8 +34,7 @@ touch ./dist/pnpm-workspace.yaml;
cat <<EOF | tee ./dist/setup cat <<EOF | tee ./dist/setup
#/usr/bin/env bash #/usr/bin/env bash
set -e; set -e;
corepack enable; command -v pnpm >/dev/null || { echo "error: pnpm not found in PATH" >&2; exit 1; };
corepack install;
pnpm install -P pnpm install -P
EOF EOF

View File

@ -2,6 +2,4 @@
set -e; set -e;
corepack enable;
corepack install;
pnpm -r install; pnpm -r install;

View File

@ -14,7 +14,7 @@
"fmt:check": "prettier --check src/ test/", "fmt:check": "prettier --check src/ test/",
"clean": "rm -rf dist/" "clean": "rm -rf dist/"
}, },
"packageManager": "pnpm@12.3.4+sha512.961aa41fb077da3a04a441d9f8e15ebc0c96da8ef710b2eb67bf9ee7cb0610eabd48f1fd85f51cffe73846785fa0f87c56a3a872a1d893f8446741b5cce45457", "packageManager": "pnpm@12.5.1+sha512.e3f305bc784a2bc89f5ad3b6138889470fae8d2af5f36b61216ec91c2c3d64089775f9de38aac331044ea40f245cb0d5666392dfdf65824e1907ef6a2c62de5f",
"dependencies": { "dependencies": {
"express": "^5.2.1", "express": "^5.2.1",
"multer": "^2.2.0", "multer": "^2.2.0",

View File

@ -7,96 +7,153 @@ importers:
configDependencies: {} configDependencies: {}
packageManagerDependencies: packageManagerDependencies:
pnpm: pnpm:
specifier: 12.3.4 specifier: 12.5.1
version: 12.3.4 version: 12.5.1
packages: packages:
'@pnpm/exe.darwin-arm64@12.3.4': '@pnpm/exe.android-arm64@12.5.1':
resolution: {integrity: sha512-PAyUol8T1+/+ViOiXAt51ECA+QnfXCqz6foL4bW+LsoX0NcVd5XVEM2mRQu+LV4oc7uRz9zf9U0P+XFfuQeDAw==} resolution: {integrity: sha512-6LGBmQuDzUmgpHSuy+haG0jrmkwFNa4EuShDZqK3cIBv4Wfb75CzjJmowkzr+iJg40vnBKMQ62t4Ko6mwsId7A==}
cpu: [arm64]
os: [android]
'@pnpm/exe.android-x64@12.5.1':
resolution: {integrity: sha512-091oCltP3Rx89yxL1Wh5rXkUXEyFzyxIzSHKRFDyDFlQkb8Ox8FaDcm+M8PNkMyqnLA+qeGOLzY7Rjj9izl1Ww==}
cpu: [x64]
os: [android]
'@pnpm/exe.darwin-arm64@12.5.1':
resolution: {integrity: sha512-r1vEMQPVjRexZMyL2HonxJoo3+nuB/+G+tg+f2PK6faRdXX1TDwevKXOxJAfqlPInTy6W/pm912+s+m0NcR4BQ==}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
'@pnpm/exe.darwin-x64@12.3.4': '@pnpm/exe.darwin-x64@12.5.1':
resolution: {integrity: sha512-fxP9JCk0Cdye+ePuj+GJJLMUMTqHGWRdb1dtv4How876uQ2ehxvenpgiYAir/ceO9PsYUZkFTtyZdx+rRu5QOA==} resolution: {integrity: sha512-YHwI1g2B+Y2cAEylOR31r781GWijdJL7Gy7TEn/ZpZ5v/Cuwp5/Pz0EFCgo0M2MKLafAbL/laUMGvOpAdnerAw==}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
'@pnpm/exe.linux-arm64-musl@12.3.4': '@pnpm/exe.freebsd-x64@12.5.1':
resolution: {integrity: sha512-FBOt0/7ye6O6q4AllVV5QMviB6qE6fqkeczV/+MDWQsmo+QJrlfsh6X7CpH/tClVpBZEyIbjpUoT8bNhCYBxEg==} resolution: {integrity: sha512-sFUMHiYFs6q5syFwgaVppzhQSmqnXoJIz/YwTXIOXTDE76ndIpTRpDGAnBCBKjsaEBnqjJA/2l6hpJcNDNoMiA==}
cpu: [x64]
os: [freebsd]
'@pnpm/exe.linux-arm64-musl@12.5.1':
resolution: {integrity: sha512-zupuxYZBuBEGjUz805/4/Asq01UNziFADANt8yp+LdUjU9bbMa3xY78Bx1PsGDqBy7aiad4sdUMK8ZxH4n4gmQ==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl] libc: [musl]
'@pnpm/exe.linux-arm64@12.3.4': '@pnpm/exe.linux-arm64@12.5.1':
resolution: {integrity: sha512-t71AVA7LRqiKTyZ5xMYaZc2n5DfdpMbfokZuiIOXHBOM03ECnF0t4iYwaBDqJgVjlKYUOwaF/bRQajGNA4cJ4w==} resolution: {integrity: sha512-6hbMWW2/eQo1b5wvQPe/ndLHVTH88Rav6N+ffD4/h6IuFMjWCgasFYYsFv0GrvpqREAx3SwmZSvli+0NFXqbxA==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@pnpm/exe.linux-x64-musl@12.3.4': '@pnpm/exe.linux-ppc64@12.5.1':
resolution: {integrity: sha512-RPmk7Jb/aYaFvL2iyDN/AtMY+hUEsue732WmXpcuQ9tBpMnGyA5py7Z3+e+qmQaJ0zY/4ni9jJiyPBQHujmv6w==} resolution: {integrity: sha512-Gg3maDzaE/ZFMV9HIx/BwMN0Y6QbEs13URfxmiYHtKjjLAs5wEQZRMUP87YNLYO7cNaEfbM8GRbS9vPosyD9Gw==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@pnpm/exe.linux-riscv64@12.5.1':
resolution: {integrity: sha512-6fgWxXYMBMsWQ8DAiRevscX4QZQjwh+FsyCj8ZOk8pMXvqeq84MqBLCmS6KckvnbdqJ+HlSVQ7fs/kd0ldwfhA==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@pnpm/exe.linux-s390x@12.5.1':
resolution: {integrity: sha512-nzMZruRvxDhEfYUhzu+7EmM39+82Dgagc/6hPFPqGqzXNGcIG0anuxbUxfeNtI64SHt91KuuYUmCB/inlM+Pag==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@pnpm/exe.linux-x64-musl@12.5.1':
resolution: {integrity: sha512-kHCbegdTNHpzmPtUD4S5fqkVkqgdQR47spF9p2VPP3dIIkZl6Ytk8w0vhlAsAGgge8FmucfwX+vvPKgm0NtOCQ==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl] libc: [musl]
'@pnpm/exe.linux-x64@12.3.4': '@pnpm/exe.linux-x64@12.5.1':
resolution: {integrity: sha512-2ZqOlSPkfwX1h5cR+FPiWf8+F+2hZT/3TvhUK5sigHqwaQCIiq8R7CGxhndKs63JtcLi2a1Qpo+wX/EoyfjyJQ==} resolution: {integrity: sha512-3PkUBYo5z4dgtlnTNIFj7QGpcDUAuqXz9WGVigPDCeccEnhGiRkWmA5102TmYJHtwJP3LfmE+ZF9PGeWhn8p9Q==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@pnpm/exe.win32-arm64@12.3.4': '@pnpm/exe.win32-arm64@12.5.1':
resolution: {integrity: sha512-ANyrHqyqco6SXBysUTRF74itDyyraea7IbFsKFdNXTjcFnfycTDx37EwuhdpPYFNSIh2JhUG4fByclsRfiHX7w==} resolution: {integrity: sha512-49NvjZIXHly56/PLoEDD7VphbO4N8dwMXoApElDFCpVGDNGeKlwlib1tXtnpTF6i2/XIsAj7GgatHB5KHXgBnA==}
cpu: [arm64] cpu: [arm64]
os: [win32] os: [win32]
'@pnpm/exe.win32-x64@12.3.4': '@pnpm/exe.win32-x64@12.5.1':
resolution: {integrity: sha512-WH/KqBPY/hq2Tb7SgQltEZytimcjgKRaCRL/aM9CI0c67iKc5TVmHUhIiL3Ux9FB4bWn36i6XewUcScQI+zG8w==} resolution: {integrity: sha512-ePUmQFPAPJFNMwg1lelDx2PdjHqYRiF0yfg5e6qeDbPNOu97QNo557Ru/SxmCrmt8ydQWNuZXWpdEp019md5vw==}
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
pnpm@12.3.4: pnpm@12.5.1:
resolution: {integrity: sha512-lhqkH7B32joEpEHZ+OFevAyW2o73ELLrZ7+e58sGEOq9SPH9hfUc/+c4RnhfoPh8VqOocqHYk/hEZ0G1zORUVw==} resolution: {integrity: sha512-4/MFvHhKK8ifWtO2E4iJRw+ujSr182thIW7JHCw9ZAiXdfneOKrDMQROpA8kXLDVZmOS399lgk4ZB+9qLGLeXw==}
engines: {node: '>=18.*'} engines: {node: '>=18.*'}
hasBin: true hasBin: true
snapshots: snapshots:
'@pnpm/exe.darwin-arm64@12.3.4': '@pnpm/exe.android-arm64@12.5.1':
optional: true optional: true
'@pnpm/exe.darwin-x64@12.3.4': '@pnpm/exe.android-x64@12.5.1':
optional: true optional: true
'@pnpm/exe.linux-arm64-musl@12.3.4': '@pnpm/exe.darwin-arm64@12.5.1':
optional: true optional: true
'@pnpm/exe.linux-arm64@12.3.4': '@pnpm/exe.darwin-x64@12.5.1':
optional: true optional: true
'@pnpm/exe.linux-x64-musl@12.3.4': '@pnpm/exe.freebsd-x64@12.5.1':
optional: true optional: true
'@pnpm/exe.linux-x64@12.3.4': '@pnpm/exe.linux-arm64-musl@12.5.1':
optional: true optional: true
'@pnpm/exe.win32-arm64@12.3.4': '@pnpm/exe.linux-arm64@12.5.1':
optional: true optional: true
'@pnpm/exe.win32-x64@12.3.4': '@pnpm/exe.linux-ppc64@12.5.1':
optional: true optional: true
pnpm@12.3.4: '@pnpm/exe.linux-riscv64@12.5.1':
optional: true
'@pnpm/exe.linux-s390x@12.5.1':
optional: true
'@pnpm/exe.linux-x64-musl@12.5.1':
optional: true
'@pnpm/exe.linux-x64@12.5.1':
optional: true
'@pnpm/exe.win32-arm64@12.5.1':
optional: true
'@pnpm/exe.win32-x64@12.5.1':
optional: true
pnpm@12.5.1:
optionalDependencies: optionalDependencies:
'@pnpm/exe.darwin-arm64': 12.3.4 '@pnpm/exe.android-arm64': 12.5.1
'@pnpm/exe.darwin-x64': 12.3.4 '@pnpm/exe.android-x64': 12.5.1
'@pnpm/exe.linux-arm64': 12.3.4 '@pnpm/exe.darwin-arm64': 12.5.1
'@pnpm/exe.linux-arm64-musl': 12.3.4 '@pnpm/exe.darwin-x64': 12.5.1
'@pnpm/exe.linux-x64': 12.3.4 '@pnpm/exe.freebsd-x64': 12.5.1
'@pnpm/exe.linux-x64-musl': 12.3.4 '@pnpm/exe.linux-arm64': 12.5.1
'@pnpm/exe.win32-arm64': 12.3.4 '@pnpm/exe.linux-arm64-musl': 12.5.1
'@pnpm/exe.win32-x64': 12.3.4 '@pnpm/exe.linux-ppc64': 12.5.1
'@pnpm/exe.linux-riscv64': 12.5.1
'@pnpm/exe.linux-s390x': 12.5.1
'@pnpm/exe.linux-x64': 12.5.1
'@pnpm/exe.linux-x64-musl': 12.5.1
'@pnpm/exe.win32-arm64': 12.5.1
'@pnpm/exe.win32-x64': 12.5.1
--- ---
lockfileVersion: '9.0' lockfileVersion: '9.0'

View File

@ -1,6 +1,4 @@
#!/bin/bash #!/bin/bash
set -e set -e
cd "$(dirname "$0")/.." cd "$(dirname "$0")/.."
corepack enable
corepack install
pnpm install pnpm install

View File

@ -4,7 +4,7 @@
"license": "MPL-2.0", "license": "MPL-2.0",
"author": "Kaleidos INC Sucursal en España SL", "author": "Kaleidos INC Sucursal en España SL",
"private": true, "private": true,
"packageManager": "pnpm@12.3.4+sha512.961aa41fb077da3a04a441d9f8e15ebc0c96da8ef710b2eb67bf9ee7cb0610eabd48f1fd85f51cffe73846785fa0f87c56a3a872a1d893f8446741b5cce45457", "packageManager": "pnpm@12.5.1+sha512.e3f305bc784a2bc89f5ad3b6138889470fae8d2af5f36b61216ec91c2c3d64089775f9de38aac331044ea40f245cb0d5666392dfdf65824e1907ef6a2c62de5f",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/penpot/penpot" "url": "https://github.com/penpot/penpot"

View File

@ -13,5 +13,5 @@
"lint": "eslint .", "lint": "eslint .",
"test": "vitest" "test": "vitest"
}, },
"packageManager": "pnpm@12.3.4+sha512.961aa41fb077da3a04a441d9f8e15ebc0c96da8ef710b2eb67bf9ee7cb0610eabd48f1fd85f51cffe73846785fa0f87c56a3a872a1d893f8446741b5cce45457" "packageManager": "pnpm@12.5.1+sha512.e3f305bc784a2bc89f5ad3b6138889470fae8d2af5f36b61216ec91c2c3d64089775f9de38aac331044ea40f245cb0d5666392dfdf65824e1907ef6a2c62de5f"
} }

View File

@ -172,7 +172,9 @@ pnpm --filter composable-test-suite run test:ci
This builds the in-sandbox entry (`src/ci/headless.ts`) as a single This builds the in-sandbox entry (`src/ci/headless.ts`) as a single
self-executing bundle and hands it to the driver (`ci/run-ci.ts`), which self-executing bundle and hands it to the driver (`ci/run-ci.ts`), which
serves the prebuilt frontend bundle via the frontend e2e static server, serves the prebuilt frontend bundle with a zero-dependency static server
built into the driver (`ci/static-server.ts`, same bundle on the same port —
no `frontend/` install needed),
intercepts every backend RPC with Playwright fixtures (no backend, no login), intercepts every backend RPC with Playwright fixtures (no backend, no login),
opens the mocked workspace file, injects the bundle directly into the plugin opens the mocked workspace file, injects the bundle directly into the plugin
sandbox, and streams each test's result from the page console — failing the sandbox, and streams each test's result from the page console — failing the
@ -182,9 +184,10 @@ backend's only role is persistence, which the mock answers with a canned
response. response.
Prerequisites: the frontend bundle must exist at `frontend/resources/public` Prerequisites: the frontend bundle must exist at `frontend/resources/public`
(the devenv watch build suffices; CI builds it via `frontend/scripts/build`), (the devenv watch build suffices), and the Playwright browser must be installed
and the Playwright browser must be installed
(`pnpm --filter composable-test-suite exec playwright install chromium`). (`pnpm --filter composable-test-suite exec playwright install chromium`).
In CI the shared E2E workflow (`.github/workflows/tests-e2e.yml`) builds that
bundle once per commit and this job restores it; do not add a build step.
Options via environment variables: Options via environment variables:

View File

@ -1,8 +1,8 @@
import { spawn, type ChildProcess } from "node:child_process";
import { readFileSync } from "node:fs"; import { readFileSync } from "node:fs";
import { dirname, resolve } from "node:path"; import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { chromium, type Page } from "playwright"; import { chromium, type Page } from "playwright";
import { startStaticServer, type StaticServer } from "./static-server.ts";
// Out-of-sandbox CI driver (Node + Playwright) for the composable test suite, // Out-of-sandbox CI driver (Node + Playwright) for the composable test suite,
// following the plugin-api-test-suite's CI driver. NOTE on provenance: the mock // following the plugin-api-test-suite's CI driver. NOTE on provenance: the mock
@ -11,7 +11,8 @@ import { chromium, type Page } from "playwright";
// (frontend/playwright, the origin), the plugin-api-test-suite driver, and this // (frontend/playwright, the origin), the plugin-api-test-suite driver, and this
// file. If workspace loading changes and this driver times out waiting for the // file. If workspace loading changes and this driver times out waiting for the
// viewport, diff against those two first. It serves the prebuilt // viewport, diff against those two first. It serves the prebuilt
// frontend bundle via the frontend e2e static server, intercepts every backend // frontend bundle with the zero-dependency static server in
// `ci/static-server.ts`, intercepts every backend
// RPC with Playwright `page.route` (reusing the frontend e2e mock fixtures), // RPC with Playwright `page.route` (reusing the frontend e2e mock fixtures),
// injects the prebuilt `headless.js` bundle into the plugin sandbox via // injects the prebuilt `headless.js` bundle into the plugin sandbox via
// `globalThis.ɵloadPlugin`, and captures the results from the page console. // `globalThis.ɵloadPlugin`, and captures the results from the page console.
@ -30,6 +31,7 @@ const here = dirname(fileURLToPath(import.meta.url));
// here = <root>/plugins/apps/composable-test-suite/ci // here = <root>/plugins/apps/composable-test-suite/ci
const repoRoot = resolve(here, "../../../../"); const repoRoot = resolve(here, "../../../../");
const frontendDir = resolve(repoRoot, "frontend"); const frontendDir = resolve(repoRoot, "frontend");
const staticRoot = resolve(frontendDir, "resources/public");
const e2eDataDir = resolve(frontendDir, "playwright/data"); const e2eDataDir = resolve(frontendDir, "playwright/data");
const BASE_URL = "http://localhost:3000"; const BASE_URL = "http://localhost:3000";
@ -96,14 +98,15 @@ async function waitForServer(url: string, timeoutMs = 30000): Promise<void> {
} }
} }
function startE2eServer(): ChildProcess { function startE2eServer(): Promise<StaticServer> {
// Reuse the frontend e2e static server: it serves frontend/resources/public // Serve the prebuilt frontend bundle from `frontend/resources/public` on
// on port 3000, which is also the host the app opens its notifications // port 3000, which is also the host the app opens its notifications
// WebSocket against — so the WS mock below matches without extra config. // WebSocket against — so the WS mock below matches without extra config.
return spawn("node", ["scripts/e2e-server.js"], { // This used to shell out to the express-based
cwd: frontendDir, // `frontend/scripts/e2e-server.js`, but that resolves `express` from
stdio: "inherit", // `frontend/node_modules`, which the CI jobs never install (only
}); // `plugins/` deps), so the driver crashed before serving anything.
return startStaticServer(staticRoot, 3000);
} }
// Install the frontend e2e WebSocket mock so the workspace's notifications // Install the frontend e2e WebSocket mock so the workspace's notifications
@ -204,7 +207,7 @@ function printReport(results: ReportedResult[]) {
async function main() { async function main() {
const bundle = readFileSync(headlessBundlePath, "utf-8"); const bundle = readFileSync(headlessBundlePath, "utf-8");
const server = startE2eServer(); const server = await startE2eServer();
await waitForServer(BASE_URL); await waitForServer(BASE_URL);
const browser = await chromium.launch(); const browser = await chromium.launch();
@ -280,7 +283,7 @@ async function main() {
]); ]);
await browser.close(); await browser.close();
server.kill(); await server.close();
printReport(results); printReport(results);

View File

@ -0,0 +1,79 @@
import { strict as assert } from "node:assert";
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { after, before, describe, it } from "node:test";
import { startStaticServer, type StaticServer } from "./static-server.ts";
describe("static-server", () => {
let dir: string = "";
let server: StaticServer | undefined;
const baseUrl = (): string => {
if (!server) throw new Error("static server not started");
return server.url;
};
before(async () => {
dir = await mkdtemp(join(tmpdir(), "penpot-static-server-"));
await mkdir(join(dir, "js"), { recursive: true });
await writeFile(join(dir, "index.html"), "<!doctype html><html></html>");
await writeFile(join(dir, "js", "app.js"), 'console.log("hi");');
await writeFile(join(dir, "data.bin"), Buffer.from([0, 1, 2]));
server = await startStaticServer(dir, 0);
});
after(async () => {
await server?.close();
// Closing twice must be safe (the driver closes unconditionally).
await server?.close();
await rm(dir, { recursive: true, force: true });
});
it("serves / as index.html", async () => {
const res = await fetch(`${baseUrl()}/`);
assert.equal(res.status, 200);
assert.match(res.headers.get("content-type") ?? "", /text\/html/);
assert.match(await res.text(), /<!doctype html>/);
});
it("serves nested files with a javascript content type", async () => {
const res = await fetch(`${baseUrl()}/js/app.js`);
assert.equal(res.status, 200);
assert.match(res.headers.get("content-type") ?? "", /javascript/);
assert.equal(await res.text(), 'console.log("hi");');
});
it("ignores query strings", async () => {
const res = await fetch(`${baseUrl()}/index.html?rev=123`);
assert.equal(res.status, 200);
assert.match(await res.text(), /<!doctype html>/);
});
it("falls back to octet-stream for unknown extensions", async () => {
const res = await fetch(`${baseUrl()}/data.bin`);
assert.equal(res.status, 200);
assert.equal(res.headers.get("content-type"), "application/octet-stream");
});
it("answers HEAD without a body", async () => {
const res = await fetch(`${baseUrl()}/index.html`, { method: "HEAD" });
assert.equal(res.status, 200);
assert.match(res.headers.get("content-type") ?? "", /text\/html/);
assert.equal(await res.text(), "");
});
it("rejects other methods", async () => {
const res = await fetch(`${baseUrl()}/index.html`, { method: "POST" });
assert.equal(res.status, 405);
});
it("returns 404 for missing files", async () => {
const res = await fetch(`${baseUrl()}/nope/missing.js`);
assert.equal(res.status, 404);
});
it("blocks path traversal outside the root", async () => {
const res = await fetch(`${baseUrl()}/..%2f..%2fsecret`);
assert.equal(res.status, 403);
});
});

View File

@ -0,0 +1,141 @@
import { createServer, type Server } from "node:http";
import { readFile, stat } from "node:fs/promises";
import { extname, join, resolve, sep } from "node:path";
// Zero-dependency static file server for the mocked-backend CI driver.
//
// It replaces `frontend/scripts/e2e-server.js` (express-based) on purpose:
// that script resolves `express`/`compression` from `frontend/node_modules`,
// which the CI jobs never install (they only run `pnpm install` inside
// `plugins/` and restore the prebuilt bundle), so the driver crashed with
// ERR_MODULE_NOT_FOUND and timed out waiting for localhost:3000. Serving the
// bundle from here keeps the suite runnable with only `plugins/`
// dependencies — the documented local workflow — and identical in CI.
//
// NOTE on provenance: this file is duplicated in
// `plugins/apps/plugin-api-test-suite/ci/static-server.ts`. Keep the two in
// sync (same as the mock harness in `run-ci.ts`).
const MIME_TYPES: Record<string, string> = {
".css": "text/css; charset=utf-8",
".gif": "image/gif",
".html": "text/html; charset=utf-8",
".ico": "image/x-icon",
".jpeg": "image/jpeg",
".jpg": "image/jpeg",
".js": "application/javascript; charset=utf-8",
".json": "application/json; charset=utf-8",
".map": "application/json; charset=utf-8",
".mjs": "application/javascript; charset=utf-8",
".otf": "font/otf",
".png": "image/png",
".svg": "image/svg+xml",
".ttf": "font/ttf",
".txt": "text/plain; charset=utf-8",
".wasm": "application/wasm",
".webmanifest": "application/manifest+json",
".webp": "image/webp",
".woff": "font/woff",
".woff2": "font/woff2",
".xml": "application/xml; charset=utf-8",
};
const INDEX = "index.html";
export interface StaticServer {
/** Base URL the server listens on (e.g. `http://localhost:3000`). */
url: string;
/** Stop accepting connections; safe to call more than once. */
close: () => Promise<void>;
}
/**
* Serve `root` over HTTP on `port` (`0` picks a free port, reported in
* `url`). Directory requests fall back to `index.html`; the app uses hash
* routing, so no other fallback is needed.
*/
export function startStaticServer(root: string, port: number): Promise<StaticServer> {
const docRoot = resolve(root);
const server: Server = createServer(async (req, res) => {
try {
if (req.method !== "GET" && req.method !== "HEAD") {
res.writeHead(405, { "Content-Type": "text/plain; charset=utf-8" });
res.end("Method Not Allowed");
return;
}
const rawPath = (req.url ?? "/").split(/[?#]/, 1)[0] ?? "/";
let pathname: string;
try {
pathname = decodeURIComponent(rawPath);
} catch {
res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
res.end("Bad Request");
return;
}
const resolved = resolve(docRoot, `.${sep}${pathname}`);
if (resolved !== docRoot && !resolved.startsWith(docRoot + sep)) {
res.writeHead(403, { "Content-Type": "text/plain; charset=utf-8" });
res.end("Forbidden");
return;
}
let filePath = resolved;
const info = await stat(filePath).catch((error: unknown) => {
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
throw error;
});
if (info === null) {
res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
res.end("Not Found");
return;
}
if (info.isDirectory()) {
filePath = join(filePath, INDEX);
}
const body = await readFile(filePath).catch((error: unknown) => {
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
throw error;
});
if (body === null) {
res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
res.end("Not Found");
return;
}
const contentType = MIME_TYPES[extname(filePath).toLowerCase()] ?? "application/octet-stream";
res.writeHead(200, {
"Content-Type": contentType,
"Content-Length": body.length,
});
res.end(req.method === "GET" ? body : undefined);
} catch {
if (!res.headersSent) {
res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
}
res.end("Internal Server Error");
}
});
return new Promise((fulfill, reject) => {
server.once("error", reject);
server.listen(port, "0.0.0.0", () => {
server.off("error", reject);
const address = server.address();
const actualPort = typeof address === "object" && address !== null ? address.port : port;
fulfill({
url: `http://localhost:${actualPort}`,
close: () =>
new Promise<void>((done, fail) => {
if (!server.listening) {
done();
return;
}
server.close((error) => (error ? fail(error) : done()));
}),
});
});
});
}

View File

@ -9,6 +9,7 @@
"build": "tsc && vite build", "build": "tsc && vite build",
"build:headless": "vite build --config vite.config.headless.ts", "build:headless": "vite build --config vite.config.headless.ts",
"test:ci": "pnpm run build:headless && tsx ci/run-ci.ts", "test:ci": "pnpm run build:headless && tsx ci/run-ci.ts",
"test:unit": "tsx --test ci/static-server.test.ts",
"preview": "vite preview", "preview": "vite preview",
"bootstrap": "pnpm install --ignore-workspace && pnpm run build && pnpm run start", "bootstrap": "pnpm install --ignore-workspace && pnpm run build && pnpm run start",
"types:check": "tsc --noEmit", "types:check": "tsc --noEmit",
@ -26,5 +27,5 @@
"vite": "^8.2.2", "vite": "^8.2.2",
"vite-live-preview": "^0.4.0" "vite-live-preview": "^0.4.0"
}, },
"packageManager": "pnpm@12.3.4+sha512.961aa41fb077da3a04a441d9f8e15ebc0c96da8ef710b2eb67bf9ee7cb0610eabd48f1fd85f51cffe73846785fa0f87c56a3a872a1d893f8446741b5cce45457" "packageManager": "pnpm@12.5.1+sha512.e3f305bc784a2bc89f5ad3b6138889470fae8d2af5f36b61216ec91c2c3d64089775f9de38aac331044ea40f245cb0d5666392dfdf65824e1907ef6a2c62de5f"
} }

View File

@ -13,5 +13,5 @@
"lint": "eslint .", "lint": "eslint .",
"test": "vitest" "test": "vitest"
}, },
"packageManager": "pnpm@12.3.4+sha512.961aa41fb077da3a04a441d9f8e15ebc0c96da8ef710b2eb67bf9ee7cb0610eabd48f1fd85f51cffe73846785fa0f87c56a3a872a1d893f8446741b5cce45457" "packageManager": "pnpm@12.5.1+sha512.e3f305bc784a2bc89f5ad3b6138889470fae8d2af5f36b61216ec91c2c3d64089775f9de38aac331044ea40f245cb0d5666392dfdf65824e1907ef6a2c62de5f"
} }

Some files were not shown because too many files have changed in this diff Show More